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("="))