diff --git a/README.md b/README.md index 21f25e2d7a..d163caa7cf 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This repo contains the typescript clients and projects for Threefold grid. - [rmb peer client](./packages/rmb_peer_client/README.md) - [rmb peer server](./packages/rmb_peer_server/README.md) - [Playground](./packages/playground/README.md) +- [UI](./packages/UI/README.md) ## Requirements diff --git a/packages/UI/docs/pdf_viewer.md b/packages/UI/docs/pdf_viewer.md index 2d20e1d3e2..ddbc0f536b 100644 --- a/packages/UI/docs/pdf_viewer.md +++ b/packages/UI/docs/pdf_viewer.md @@ -28,11 +28,13 @@ To use the PDF Signer Web Component, follow these steps: 2. Navigate to the `repository/packages/UI` directory. -3. Run `yarn build` to generate the required distribution files. +3. Choose which provider you are going to use [see providers section](#using-providers-and-extensions) -4. Locate the `dist` folder created in the previous step. +4. Run `yarn build` to generate the required distribution files. -5. Copy the `dist/threefold-ui.umd.js` file and include it in your project's HTML files. +5. Locate the `dist` folder created in the previous step. + +6. Copy the `dist/threefold-ui.umd.js` file and include it in your project's HTML files. ```html @@ -101,6 +103,8 @@ Here's an example of how to use the PDF Signer Web Component in your HTML file: In the example above, replace `` and `` with the actual URLs for your PDF document and the destination where signed documents should be sent. Also, for the ``, use one of the following network options: `[main, test, qa, dev]`. +PS: Please make sure that you have a `PDF URL` with `CORS-ORIGIN` enabled. + Feel free to customize the HTML structure and styles to match your application's design and requirements. **Now you can serve your HTML file on any live-server plugin.** diff --git a/packages/UI/docs/script_editor.md b/packages/UI/docs/script_editor.md index d536334a58..6b339cdf3b 100644 --- a/packages/UI/docs/script_editor.md +++ b/packages/UI/docs/script_editor.md @@ -27,11 +27,13 @@ To create an instance of the Script Editor, follow these steps: 2. Navigate to the `repository/packages/UI` directory. -3. Run `yarn build` to generate the required distribution files. +3. Choose which provider you are going to use [see providers section](#using-providers-and-extensions) -4. Locate the `dist` folder created in the previous step. +4. Run `yarn build` to generate the required distribution files. -5. Copy the `dist/threefold-ui.umd.js` file and include it in your project's HTML files. +5. Locate the `dist` folder created in the previous step. + +6. Copy the `dist/threefold-ui.umd.js` file and include it in your project's HTML files. ```html diff --git a/packages/UI/examples/server-example/src/server.ts b/packages/UI/examples/server-example/src/server.ts index 679dde433b..c39d1b1bee 100644 --- a/packages/UI/examples/server-example/src/server.ts +++ b/packages/UI/examples/server-example/src/server.ts @@ -48,16 +48,12 @@ const verify = async (payload: Payload) => { app.post("/api/verify", async (req: Request, res: Response) => { const payload: Payload = req.body; - let content: Uint8Array = new Uint8Array(); try { if (payload.pdfUrl) { const response = await axios.get(payload.pdfUrl, { responseType: "arraybuffer" }); - content = Uint8Array.from(Buffer.from(response.data, "base64")); - } else { - content = Uint8Array.from(Buffer.from(payload.content || "", "base64")); + payload.content = Uint8Array.from(Buffer.from(response.data, "base64")).toString(); } - payload.content = content.toString(); const verified = await verify(payload); if (verified) { diff --git a/packages/UI/src/components/PDFSignerViewComponent.vue b/packages/UI/src/components/PDFSignerViewComponent.vue index f4c308d184..680791384b 100644 --- a/packages/UI/src/components/PDFSignerViewComponent.vue +++ b/packages/UI/src/components/PDFSignerViewComponent.vue @@ -111,7 +111,10 @@ export default { pdfData.value = data.toString(); numOfPages.value = pdf.numPages; } catch (error: any) { - showError({ isError: true, errorMessage: error.message }); + showError({ + isError: true, + errorMessage: "Please make sure that you have provided a PDF URL with CORS enabled.", + }); } finally { loadingPdf.value = false; } diff --git a/packages/grid_client/scripts/add_node_to_network.ts b/packages/grid_client/scripts/add_node_to_network.ts new file mode 100644 index 0000000000..670d1e87a2 --- /dev/null +++ b/packages/grid_client/scripts/add_node_to_network.ts @@ -0,0 +1,18 @@ +import { getClient } from "./client_loader"; +import { log } from "./utils"; + +async function main() { + const grid3 = await getClient(); + try { + // if the network is not created, it will create one and add this node to it. + const res = await grid3.networks.addNode({ + name: "wedtest", + ipRange: "10.249.0.0/16", + nodeId: 14, + }); + log(res); + } finally { + grid3.disconnect(); + } +} +main(); diff --git a/packages/grid_client/scripts/test_networks.ts b/packages/grid_client/scripts/get_network_config.ts similarity index 100% rename from packages/grid_client/scripts/test_networks.ts rename to packages/grid_client/scripts/get_network_config.ts diff --git a/packages/grid_client/src/high_level/models.ts b/packages/grid_client/src/high_level/models.ts index dba6f49986..ed6470a750 100644 --- a/packages/grid_client/src/high_level/models.ts +++ b/packages/grid_client/src/high_level/models.ts @@ -15,6 +15,7 @@ class TwinDeployment { public nodeId: number, public network: Network | null = null, public solutionProviderId: number | null = null, + public returnNetworkContracts = false, ) {} } diff --git a/packages/grid_client/src/high_level/network.ts b/packages/grid_client/src/high_level/network.ts new file mode 100644 index 0000000000..56dc1b689b --- /dev/null +++ b/packages/grid_client/src/high_level/network.ts @@ -0,0 +1,57 @@ +import { Addr } from "netaddr"; + +import { DeploymentFactory, Network } from "../primitives"; +import { WorkloadTypes, Znet } from "../zos"; +import { HighLevelBase } from "./base"; +import { Operations, TwinDeployment } from "./models"; + +class NetworkHL extends HighLevelBase { + async addNode(networkName: string, ipRange: string, nodeId: number, solutionProviderId: number, description = "") { + const network = new Network(networkName, ipRange, this.config); + await network.load(); + const networkMetadata = JSON.stringify({ + type: "network", + name: networkName, + projectName: this.config.projectName, + }); + + const workload = await network.addNode(nodeId, networkMetadata, description); + if (!workload) { + throw Error(`Node ${nodeId} is already exist on network ${networkName}`); + } + + const twinDeployments: TwinDeployment[] = []; + const deploymentFactory = new DeploymentFactory(this.config); + const deployment = deploymentFactory.create([workload], 0, networkMetadata, description, 0); + twinDeployments.push( + new TwinDeployment(deployment, Operations.deploy, 0, nodeId, network, solutionProviderId, true), + ); + + if (!(await network.exists())) { + return twinDeployments; + } + // update network if it's already exist + for (const deployment of network.deployments) { + const d = await deploymentFactory.fromObj(deployment); + for (const workload of d.workloads) { + const data = workload.data as Znet; + if (workload.type !== WorkloadTypes.network || !Addr(network.ipRange).contains(Addr(data.subnet))) { + continue; + } + workload.data = network.updateNetwork(data); + workload.version += 1; + break; + } + twinDeployments.push(new TwinDeployment(d, Operations.update, 0, 0, network, solutionProviderId, true)); + } + return twinDeployments; + } + + async hasNode(networkName: string, ipRange: string, nodeId: number): Promise { + const network = new Network(networkName, ipRange, this.config); + await network.load(); + return network.nodeExists(nodeId); + } +} + +export { NetworkHL }; diff --git a/packages/grid_client/src/high_level/twinDeploymentHandler.ts b/packages/grid_client/src/high_level/twinDeploymentHandler.ts index 27c72a39d7..fb24b26108 100644 --- a/packages/grid_client/src/high_level/twinDeploymentHandler.ts +++ b/packages/grid_client/src/high_level/twinDeploymentHandler.ts @@ -265,6 +265,43 @@ class TwinDeploymentHandler { return deployments; } + async checkFarmIps(twinDeployments: TwinDeployment[]) { + const farmIPs: Map = new Map(); + + for (const twinDeployment of twinDeployments) { + if (twinDeployment.operation !== Operations.deploy) { + continue; + } + + if (twinDeployment.publicIps === 0) { + continue; + } + + const node = await this.nodes.getNode(twinDeployment.nodeId); + if (!node) { + continue; + } + if (!farmIPs.has(node.farmId)) { + farmIPs.set(node.farmId, twinDeployment.publicIps); + } else { + farmIPs.set(node.farmId, farmIPs.get(node.farmId)! + twinDeployment.publicIps); + } + } + + for (const farmId of farmIPs.keys()) { + const _farm = await this.tfclient.farms.get({ id: farmId }); + const freeIps = _farm.publicIps.filter(res => res.contractId === 0).length; + + if (freeIps < farmIPs.get(farmId)!) { + throw Error( + `Farm ${farmId} doesn't have enough public IPs: requested IPs=${farmIPs.get( + farmId, + )}, available IPs=${freeIps}`, + ); + } + } + } + async checkNodesCapacity(twinDeployments: TwinDeployment[]) { for (const twinDeployment of twinDeployments) { let workloads: Workload[] = []; @@ -439,6 +476,8 @@ class TwinDeploymentHandler { twinDeployments = await this.merge(twinDeployments); await this.validate(twinDeployments); await this.checkNodesCapacity(twinDeployments); + await this.checkFarmIps(twinDeployments); + const contracts = { created: [], updated: [], deleted: [] }; const resultContracts = { created: [], updated: [], deleted: [] }; let nodeExtrinsics: ExtrinsicResult[] = []; @@ -476,6 +515,7 @@ class TwinDeploymentHandler { if (twinDeployment.deployment.challenge_hash() === contract.contractType.nodeContract.deploymentHash) { twinDeployment.deployment.contract_id = contract.contractId; if ( + twinDeployment.returnNetworkContracts || !( twinDeployment.deployment.workloads.length === 1 && twinDeployment.deployment.workloads[0].type === WorkloadTypes.network @@ -500,6 +540,7 @@ class TwinDeploymentHandler { if (twinDeployment.deployment.challenge_hash() === contract.contractType.nodeContract.deploymentHash) { twinDeployment.nodeId = contract.contractType.nodeContract.nodeId; if ( + twinDeployment.returnNetworkContracts || !( twinDeployment.deployment.workloads.length === 1 && twinDeployment.deployment.workloads[0].type === WorkloadTypes.network diff --git a/packages/grid_client/src/modules/models.ts b/packages/grid_client/src/modules/models.ts index a8c02acdda..ea8ed1e3b0 100644 --- a/packages/grid_client/src/modules/models.ts +++ b/packages/grid_client/src/modules/models.ts @@ -596,6 +596,7 @@ class FarmFilterOptions { @Expose() @IsOptional() @IsInt() @Min(1) nodeRentedBy?: number; @Expose() @IsOptional() @IsInt() page?: number; @Expose() @IsOptional() @IsInt() size?: number; + @Expose() @IsOptional() @IsInt() farmId?: number; } class CalculatorModel { @@ -639,6 +640,20 @@ class pingFarmModel { @Expose() @IsInt() @IsNotEmpty() @Min(1) farmId: number; } +class NetworkAddNodeModel { + @Expose() @IsString() @IsNotEmpty() @IsAlphanumeric() @MaxLength(NameLength) name: string; + @Expose() @IsString() @IsNotEmpty() ipRange: string; + @Expose() @IsInt() @IsNotEmpty() @Min(1) nodeId: number; + @Expose() @IsInt() @IsOptional() solutionProviderId?: number; + @Expose() @IsString() @IsOptional() description?: string; +} + +class NetworkHasNodeModel { + @Expose() @IsString() @IsNotEmpty() @IsAlphanumeric() @MaxLength(NameLength) name: string; + @Expose() @IsString() @IsNotEmpty() ipRange: string; + @Expose() @IsInt() @IsNotEmpty() @Min(1) nodeId: number; +} + class NetworkGetModel { @Expose() @IsString() @IsNotEmpty() @IsAlphanumeric() @MaxLength(NameLength) name: string; } @@ -768,6 +783,8 @@ export { SetServiceContractFeesModel, SetServiceContractMetadataModel, GetServiceContractModel, + NetworkAddNodeModel, + NetworkHasNodeModel, NetworkGetModel, NodeGetModel, SetDedicatedNodeExtraFeesModel, diff --git a/packages/grid_client/src/modules/networks.ts b/packages/grid_client/src/modules/networks.ts index 433c22bfb1..ae7d0c2c73 100644 --- a/packages/grid_client/src/modules/networks.ts +++ b/packages/grid_client/src/modules/networks.ts @@ -3,14 +3,32 @@ import * as PATH from "path"; import { GridClientConfig } from "../config"; import { expose } from "../helpers/expose"; import { validateInput } from "../helpers/validator"; +import { NetworkHL } from "../high_level/network"; import { BaseModule } from "./base"; -import { NetworkGetModel } from "./models"; +import { NetworkAddNodeModel, NetworkGetModel, NetworkHasNodeModel } from "./models"; +import { checkBalance } from "./utils"; class NetworkModule extends BaseModule { moduleName = "networks"; + network: NetworkHL; constructor(public config: GridClientConfig) { super(config); + this.network = new NetworkHL(config); + } + + @expose + @validateInput + @checkBalance + async addNode(options: NetworkAddNodeModel) { + const twinDeployments = await this.network.addNode( + options.name, + options.ipRange, + options.nodeId, + options.solutionProviderId!, + options.description, + ); + return { contracts: await this.twinDeploymentHandler.handle(twinDeployments) }; } @expose @@ -18,10 +36,16 @@ class NetworkModule extends BaseModule { return await this._list(); } + @expose + @validateInput + async hasNode(options: NetworkHasNodeModel): Promise { + return await this.network.hasNode(options.name, options.ipRange, options.nodeId); + } + @expose @validateInput async getWireGuardConfigs(options: NetworkGetModel) { - const path = PATH.join(this.getDeploymentPath(options.name), "info.json"); + const path = PATH.join(this.config.storePath, this.moduleName, options.name, "info.json"); const networkInfo = await this.backendStorage.load(path); return networkInfo["wireguardConfigs"]; } diff --git a/packages/grid_client/src/primitives/nodes.ts b/packages/grid_client/src/primitives/nodes.ts index d20eeac6e6..51356034c0 100644 --- a/packages/grid_client/src/primitives/nodes.ts +++ b/packages/grid_client/src/primitives/nodes.ts @@ -23,7 +23,7 @@ interface FarmInfo { interface PublicIps { id: string; ip: string; - contractId: number; + contract_id: number; // Added to match the one in the farm interface || TODO: Should we replace the whole http requests to be done with the gridProxy. gateway: string; } @@ -176,7 +176,7 @@ class Nodes { farms = await this.getAllFarms(url); } return farms - .filter(farm => farm.publicIps.filter(ip => ip.contractId === 0).length > 0) + .filter(farm => farm.publicIps.filter(ip => ip.contract_id === 0).length > 0) .map(farm => farm.farmId) .includes(farmId); } @@ -382,6 +382,7 @@ class Nodes { node_has_gpu: options.nodeHasGPU, node_rented_by: options.nodeRentedBy, node_certified: options.nodeCertified, + farm_id: options.farmId, }; return Object.entries(params) .map(param => param.join("="))