diff --git a/README.md b/README.md index 4b918499..a0287a13 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,14 @@ export INDEXING_RETRY_INTERVAL='3000' export AVOID_LOOP_RUN='true/false' ``` +- Optional, set SSI_WALLET_API, SSI_WALLET_ID, SSI_WALLET_DID to support v5 DDOs (assets using credentialSubject and SSI policy flows). + +``` +export SSI_WALLET_API="https://your-ssi-wallet.example/api" +export SSI_WALLET_ID="did:example:your-wallet-did-or-id" +export export SSI_WALLET_DID="did:example" +``` + ### Build the TypeScript code @@ -108,7 +116,7 @@ npm run cli h E.g. run publish command -Make sure to update chainId from the assets from `metadata` folder. +Make sure to update chainId and serviceEnpoint from the assets from `metadata` folder. ``` npm run cli publish metadata/simpleDownloadDataset.json @@ -186,23 +194,29 @@ npm run cli [options] **Download:** - **Positional:** - `npm run cli download did:op:123 ./custom-folder` + `npm run cli download did:op:123 ./custom-folder serviceId` - **Named Options:** - `npm run cli download --did did:op:123 --folder ./custom-folder` + `npm run cli download --did did:op:123 --folder ./custom-folder --service serviceId` (Order of `--did` and `--folder` does not matter.) +- **Rules:** + serviceId is optional. If omitted, the CLI defaults to the first available download service. + --- **Start Compute:** - **Positional:** - `npm run cli startCompute -- did1,did2 algoDid env1 maxJobDuration paymentToken resources --accept true` + `npm run cli startCompute -- did1,did2 algoDid env1 maxJobDuration paymentToken resources svc1,svc2 algoServiceId` - **Named Options:** - `npm run cli startCompute --datasets did1,did2 --algo algoDid --env env1 --maxJobDuration maxJobDuration --token paymentToken --resources resources --accept true` + `npm run cli startCompute --datasets did1,did2 --algo algoDid --env env1 --maxJobDuration maxJobDuration --token paymentToken --resources resources --accept true --services svc1,svc2 ----algo-service algoServiceId` (Options can be provided in any order.) +- **Rules:** + serviceIds and algoServiceId are optional. If omitted, the CLI defaults to the first available service. + - `maxJobDuration` is a required parameter an represents the time measured in seconds for job maximum execution, the payment is based on this maxJobDuration value, user needs to provide this. - `paymentToken` is required and represents the address of the token that is supported by the environment for processing the compute job payment. It can be retrieved from `getComputeEnvironments` command output. @@ -218,8 +232,14 @@ e.g.: `'[{"id":"cpu","amount":3},{"id":"ram","amount":16772672536},{"id":"disk", - **Positional:** `npm run cli startFreeCompute did1,did2 algoDid env1` -- **Named Options:** - `npm run cli startFreeCompute --datasets did1,did2 --algo algoDid --env env1` +- **Named Options:** + `npm run cli startFreeCompute --datasets did1,did2 --algo algoDid --env env1 --services svc1,svc2 ----algo-service algoServiceId` + (Options can be provided in any order.) + + - `output` is an optional stringified JSON object specifying a remote storage backend where job results will be uploaded. Same format as `startCompute`. + +- **Rules:** + serviceIds and algoServiceId are optional. If omitted, the CLI defaults to the first available service.` (Options can be provided in any order.) - `output` is an optional stringified JSON object specifying a remote storage backend where job results will be uploaded. Same format as `startCompute`. @@ -419,10 +439,12 @@ e.g.: `'[{"id":"cpu","amount":3},{"id":"ram","amount":16772672536},{"id":"disk", - **download:** `-d, --did ` `-f, --folder [destinationFolder]` (Default: `.`) + `-s, --service ` (Optional, target a specific service) -- **startCompute:** - `-d, --datasets ` - `-a, --algo ` + +- **startCompute:** + `-d, --datasets ` + `-a, --algo ` `-e, --env ` `--init ` `--maxJobDuration ` @@ -430,12 +452,16 @@ e.g.: `'[{"id":"cpu","amount":3},{"id":"ram","amount":16772672536},{"id":"disk", `--resources ` `--amountToDeposit ` (Id `''`, it will fallback to initialize compute payment amount.) `-o, --output [output]` (Optional. Stringified JSON object specifying a remote storage backend for job results.) + `-s, --services [serviceIds]` (Optional, comma-separated; must match datasetDids length, positional 1–1) + `-x, --algo-service [algoServiceId]` (Optional, override algorithm service) - **startFreeCompute:** `-d, --datasets ` `-a, --algo ` `-e, --env ` `-o, --output [output]` (Optional. Stringified JSON object specifying a remote storage backend for job results.) + `-s, --services [serviceIds]` (Optional, comma-separated; must match datasetDids length, positional 1–1) + `-x, --algo-service [algoServiceId]` (Optional, override algorithm service) - **getComputeEnvironments:** diff --git a/eslint.config.mjs b/eslint.config.mjs index 9e9d4745..8ecd2dad 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,10 +4,11 @@ import tseslint from "typescript-eslint"; export default [ - {languageOptions: { globals: globals.browser }}, + { ignores: ["dist/**"] }, + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, pluginJs.configs.recommended, ...tseslint.configs.recommended, {rules: { '@typescript-eslint/no-explicit-any': 'warn', }} -]; \ No newline at end of file +]; diff --git a/metadata/jsAlgoV5.json b/metadata/jsAlgoV5.json new file mode 100644 index 00000000..b88d45a8 --- /dev/null +++ b/metadata/jsAlgoV5.json @@ -0,0 +1,199 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2" + ], + "id": "did:ope", + "version": "5.0.0", + "credentialSubject": { + "chainId": 8996, + "metadata": { + "created": "2025-09-09T11:57:52Z", + "updated": "2025-09-09T11:57:52Z", + "type": "algorithm", + "name": "cli algo test", + "description": { + "@value": "testing algo", + "@direction": "", + "@language": "" + }, + "tags": [ + "test" + ], + "author": "", + "license": { + "name": "https://raw.githubusercontent.com/oceanprotocol/c2d-examples/main/branin_and_gpr/branin.arff", + "licenseDocuments": [ + { + "sha256": "71a828f4ddc3fee436d80fc5b02535fc5e13ae8f19eae87ec02734bb01126c5b", + "mirrors": [ + { + "method": "get", + "type": "url", + "url": "https://raw.githubusercontent.com/oceanprotocol/c2d-examples/main/branin_and_gpr/branin.arff" + } + ], + "name": "https://raw.githubusercontent.com/oceanprotocol/c2d-examples/main/branin_and_gpr/branin.arff", + "fileType": "text/plain; charset=utf-8" + } + ] + }, + "links": {}, + "additionalInformation": { + "termsAndConditions": true + }, + "algorithm": { + "language": "arff", + "version": "0.1", + "container": { + "entrypoint": "node $ALGO", + "image": "node", + "tag": "latest", + "checksum": "sha256:c3688c7b5cc3b159ddf4f744594de62461df7ef43124378f118cb470e4f72bcb" + } + }, + "copyrightHolder": "", + "providedBy": "" + }, + "services": [ + { + "compute": { + "publisherTrustedAlgorithms": [], + "publisherTrustedAlgorithmPublishers": [ + "*" + ], + "allowRawAlgorithm": false, + "allowNetworkAccess": true + }, + "credentials": { + "allow": [ + { + "values": [ + { + "request_credentials": [ + { + "format": "jwt_vc_json", + "policies": [], + "type": "UniversityDegree" + } + ], + "vc_policies": [ + "not-before", + "revoked-status-list", + "signature" + ] + } + ], + "type": "SSIpolicy" + }, + { + "values": [ + { + "address": "*" + } + ], + "type": "address" + } + ], + "match_deny": "any", + "deny": [] + }, + "name": "test service", + "files": { + "datatokenAddress": "0x0", + "nftAddress": "0x0", + "files": [ + { + "type": "url", + "url": "https://raw.githubusercontent.com/oceanprotocol/c2d-examples/main/branin_and_gpr/branin.arff", + "method": "GET" + } + ] + }, + "description": { + "@value": "testing new service", + "@direction": "ltr", + "@language": "en" + }, + "id": "23033c06ea9adc6ff5dff42938417c7750e75efbdfbfecf5d6d544705450df24", + "datatokenAddress": "0x0", + "serviceEndpoint": "https://ocean-node-vm3.oceanenterprise.io", + "state": 0, + "type": "compute", + "timeout": 31556952 + } + ], + "nftAddress": "0x0", + "credentials": { + "allow": [ + { + "values": [ + { + "request_credentials": [ + { + "format": "jwt_vc_json", + "policies": [], + "type": "UniversityDegree" + } + ], + "vc_policies": [ + "not-before", + "revoked-status-list", + "signature" + ] + } + ], + "type": "SSIpolicy" + }, + { + "values": [ + { + "address": "*" + } + ], + "type": "address" + } + ], + "deny": [], + "match_deny": "any" + } + }, + "additionalDdos": [], + "type": [ + "VerifiableCredential" + ], + "issuer": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiekxlRGh1UG15QndrampiV1pRZTBPeHdEMjMxYmx3aXFTMGQ0VTJETnZFbyIsIngiOiJta3NvaEladWlsblJWWmhTbmwxNGRpdTZZWDF3UUFFX0ozbWJoWDNDOFRjIn0", + "indexedMetadata": { + "stats": [ + { + "symbol": "OEAT", + "name": "Access Token", + "orders": 0, + "datatokenAddress": "", + "serviceId": "23033c06ea9adc6ff5dff42938417c7750e75efbdfbfecf5d6d544705450df24", + "prices": [ + { + "exchangeId": "", + "price": "2.0", + "contract": "", + "type": "fixedrate", + "token": "" + } + ] + } + ], + "nft": { + "state": 0, + "address": "", + "name": "Data NFT", + "symbol": "OEC-NFT", + "owner": "", + "created": "", + "tokenURI": "" + }, + "event": { + }, + "purgatory": { + "state": false + } + } +} \ No newline at end of file diff --git a/metadata/simpleDownloadDatasetV5.json b/metadata/simpleDownloadDatasetV5.json new file mode 100644 index 00000000..14bd89cd --- /dev/null +++ b/metadata/simpleDownloadDatasetV5.json @@ -0,0 +1,167 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2" + ], + "id": "did:ope", + "version": "5.0.0", + "type": ["VerifiableCredential"], + "additionalDdos": [], + "credentialSubject": { + "chainId": 8996, + "metadata": { + "created": "2025-02-19T10:23:59Z", + "updated": "2025-02-19T10:23:59Z", + "type": "dataset", + "name": "The Digital Project Management Office (DigitalPMO)", + "description": { + "@value": "fdsfsdfsdfsdfsdfsd", + "@direction": "", + "@language": "" + }, + "tags": [], + "author": "", + "license": { + "name": "https://www.google.de", + "licenseDocuments": [ + { + "name": "https://www.google.de", + "fileType": "text/html; charset=ISO-8859-1", + "sha256": "5a051e8e73057a521eadf3f80219495c4ed04d63397e5817a72ec2c335025b44", + "mirrors": [ + { + "type": "url", + "method": "get", + "url": "https://www.google.de" + } + ] + } + ] + }, + "links": {}, + "additionalInformation": { + "termsAndConditions": true + }, + "copyrightHolder": "", + "providedBy": "" + }, + "services": [ + { + "id": "ccb398c50d6abd5b456e8d7242bd856a1767a890b537c2f8c10ba8b8a10e6025", + "type": "access", + "files": { + "datatokenAddress": "0x0", + "nftAddress": "0x0", + "files": [ + { + "type": "url", + "url": "https://raw.githubusercontent.com/oceanprotocol/c2d-examples/main/branin_and_gpr/branin.arff", + "method": "GET" + } + ] + }, + "state": 0, + "datatokenAddress": "", + "serviceEndpoint": "https://ocean-node-vm3.oceanenterprise.io", + "timeout": 0, + "name": "", + "credentials": { + "allow": [ + { + "type": "address", + "values": [ + { + "address": "*" + } + ] + } + ], + "deny": [], + "match_deny": "any" + } + } + ], + "credentials": { + "allow": [ + { + "type": "SSIpolicy", + "values": [ + { + "request_credentials": [ + { + "format": "jwt_vc_json", + "policies": [], + "type": "UniversityDegree" + } + ], + "vc_policies": [ + "signature", + "not-before", + "revoked-status-list" + ], + "vp_policies": [ + { + "policy": "holder-binding" + }, + { + "policy": "presentation-definition" + }, + { + "policy": "minimum-credentials", + "args": "1" + }, + { + "policy": "maximum-credentials", + "args": "2" + } + ] + } + ] + }, + { + "type": "address", + "values": [ + { + "address": "*" + } + ] + } + ], + "deny": [], + "match_deny": "any" + } + }, + "indexedMetadata": { + "stats": [ + { + "datatokenAddress": "", + "name": "Access Token", + "symbol": "OEAT", + "serviceId": "ccb398c50d6abd5b456e8d7242bd856a1767a890b537c2f8c10ba8b8a10e6025", + "orders": 0, + "prices": [ + { + "type": "dispenser", + "price": "0", + "contract": "", + "token": "" + } + ] + } + ], + "nft": { + "state": 0, + "address": "", + "name": "Data NFT", + "symbol": "OEC-NFT", + "owner": "", + "created": "", + "tokenURI": "" + }, + "event": { + }, + "purgatory": { + "state": false + } + }, + "issuer": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibUJjZDJXbGQ0YVNCaHpJR0RKNTVPc1NHWUY4R2h6Vmt2MkVkTC1KYko5MCIsIngiOiJJT1R2UWRqQlRxMzZMbTFuTXdkaTYzN00zdXMycUR3blN5MC13djVkNFRBIn0" +} diff --git a/package-lock.json b/package-lock.json index df4d3c49..13bf7dc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@oceanprotocol/contracts": "^2.5.0", "@oceanprotocol/ddo-js": "^0.3.0", "@oceanprotocol/lib": "^8.0.6", + "axios": "^1.11.0", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", @@ -1880,13 +1881,13 @@ } }, "node_modules/@ecies/ciphers": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.5.tgz", - "integrity": "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", "license": "MIT", "engines": { "bun": ">=1", - "deno": ">=2", + "deno": ">=2.7.10", "node": ">=16" }, "peerDependencies": { @@ -2625,9 +2626,9 @@ } }, "node_modules/@ethersproject/bignumber/node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT", "peer": true }, @@ -2824,9 +2825,9 @@ } }, "node_modules/@ethersproject/signing-key/node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT", "peer": true }, @@ -3184,9 +3185,9 @@ } }, "node_modules/@oceanprotocol/lib": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-8.0.6.tgz", - "integrity": "sha512-QjyS7qOlpJHrVaUDDhuNMwTvu9fCIhfgkL6emGZ5MGHnn0Rk8G6q7giOTR8DjuCBK8+YJHMC1CuUnYrgDClaEw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-8.1.0.tgz", + "integrity": "sha512-LDh7RLrBwBypacAA99+x3+Ti3eKs2FRptE+90S2KnIDqBTOgEoR6iQHjjw47AGm23YBgNF7f0W2vocfhYnfkqA==", "license": "Apache-2.0", "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", @@ -3537,6 +3538,16 @@ "@zazuko/prefixes": "^2.0.1" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -4000,9 +4011,9 @@ "license": "MIT" }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -4390,6 +4401,33 @@ "license": "MIT", "peer": true }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -4569,31 +4607,31 @@ "peer": true }, "node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "license": "MIT", "peer": true }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", "peer": true, "dependencies": { - "bytes": "~3.1.2", + "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "unpipe": "1.0.0" }, "engines": { "node": ">= 0.8", @@ -4610,27 +4648,6 @@ "ms": "2.0.0" } }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4638,16 +4655,6 @@ "license": "MIT", "peer": true }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4656,9 +4663,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -5983,9 +5990,9 @@ } }, "node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6124,9 +6131,9 @@ } }, "node_modules/eciesjs": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz", - "integrity": "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==", + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", "license": "MIT", "dependencies": { "@ecies/ciphers": "^0.2.5", @@ -7460,9 +7467,9 @@ } }, "node_modules/ethereumjs-util/node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT", "peer": true }, @@ -7656,40 +7663,40 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "~2.4.1", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", - "statuses": "~2.0.1", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -7895,9 +7902,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7905,9 +7912,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", "dependencies": { @@ -8037,12 +8044,32 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -9631,9 +9658,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -9810,12 +9837,12 @@ } }, "node_modules/jws": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", - "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "license": "MIT", "dependencies": { - "jwa": "^1.4.2", + "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, @@ -10496,10 +10523,9 @@ } }, "node_modules/min-document": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", - "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", - "license": "MIT", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", "peer": true, "dependencies": { "dom-walk": "^0.1.0" @@ -10520,9 +10546,9 @@ "peer": true }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -10636,9 +10662,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10667,9 +10693,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", "dependencies": { @@ -11453,9 +11479,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT", "peer": true }, @@ -11546,9 +11572,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -12406,9 +12432,9 @@ } }, "node_modules/pretty-quick/node_modules/picomatch": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.2.tgz", - "integrity": "sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", "dev": true, "license": "MIT", "engines": { @@ -12463,6 +12489,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -12496,13 +12531,13 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", "peer": true, "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12580,48 +12615,17 @@ } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", "peer": true, "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -12944,9 +12948,9 @@ } }, "node_modules/request/node_modules/qs": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", - "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "license": "BSD-3-Clause", "peer": true, "engines": { @@ -13087,16 +13091,16 @@ } }, "node_modules/rlp/node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT", "peer": true }, "node_modules/rollup": { - "version": "2.80.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", - "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", "bin": { @@ -13324,9 +13328,9 @@ } }, "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -13527,16 +13531,6 @@ "license": "MIT", "peer": true }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/scrypt-js": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", @@ -14267,18 +14261,18 @@ } }, "node_modules/svgo": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", - "integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", "dev": true, "license": "MIT", "dependencies": { + "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", - "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { @@ -14666,9 +14660,9 @@ } }, "node_modules/ts-node/node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -15145,9 +15139,9 @@ } }, "node_modules/typescript-eslint/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15155,13 +15149,13 @@ } }, "node_modules/typescript-eslint/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -15739,9 +15733,9 @@ } }, "node_modules/web3-eth-iban/node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT", "peer": true }, @@ -15885,9 +15879,9 @@ } }, "node_modules/web3-utils/node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT", "peer": true }, @@ -16185,9 +16179,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { diff --git a/package.json b/package.json index 35203f38..9fa78750 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@oceanprotocol/contracts": "^2.5.0", "@oceanprotocol/ddo-js": "^0.3.0", "@oceanprotocol/lib": "^8.0.6", + "axios": "^1.11.0", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", diff --git a/src/cli.ts b/src/cli.ts index c958ee8d..2b4a0697 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -221,11 +221,14 @@ export async function createCLI() { .description("Downloads an asset into specified folder") .argument("", "The asset DID") .argument("[folder]", "Destination folder", ".") + .argument("[serviceId]", "Service ID (optional)") .option("-d, --did ", "The asset DID") .option("-f, --folder [folder]", "Destination folder", ".") - .action(async (did, folder, options) => { + .option("-s, --service ", "Service ID") + .action(async (did, folder, serviceId, options) => { const assetDid = options.did || did; - const destFolder = options.folder || folder || "."; + const destFolder = options.folder || folder || '.'; + const svcId = options.service || serviceId; if (!assetDid) { console.error(chalk.red("DID is required")); // process.exit(1); @@ -233,7 +236,7 @@ export async function createCLI() { } const { signer, chainId } = await initializeSigner(); const commands = new Commands(signer, chainId); - await commands.download([null, assetDid, destFolder]); + await commands.download([null, assetDid, destFolder, svcId]); }); // allowAlgo command @@ -262,10 +265,7 @@ export async function createCLI() { program .command("startCompute") .description("Starts a compute job") - .argument( - "", - "Dataset DIDs (comma-separated) OR (empty array for none)" - ) + .argument("", "Dataset DIDs (comma-separated) OR (empty array for none)") .argument("", "Algorithm DID") .argument("", "Compute environment ID") .argument("", "maxJobDuration for compute job") @@ -275,35 +275,22 @@ export async function createCLI() { "[output]", "Output backend to save job results to. Supported types include S3, FTP, URL, Arweave, etc. Defaults to node local disk if omitted." ) - .option( - "-d, --datasets ", - "Dataset DIDs (comma-separated) OR (empty array for none)" - ) + .argument("[serviceIds]", "Service IDs (comma-separated; positional mapping with datasetDIDs)") + .argument("[algoServiceId]", "Algorithm Service ID (optional)") + .option("-d, --datasets ", "Dataset DIDs (comma-separated) OR (empty array for none)") .option("-a, --algo ", "Algorithm DID") .option("-e, --env ", "Compute environment ID") .option("--maxJobDuration ", "Compute maxJobDuration") .option("-t, --token ", "Compute payment token") + .option("-s, --services [serviceIds]", "Service IDs (comma-separated; positional mapping with datasetDIDs)") + .option("-x, --algo-service [algoServiceId]", "Algorithm Service ID (optional)") .option("--resources ", "Compute resources") - .option( - "--accept [boolean]", - "Auto-confirm payment for compute job (true/false)", - toBoolean - ) + .option("--accept [boolean]", "Auto-confirm payment for compute job (true/false)", toBoolean) .option( "-o, --output [output]", "Output backend to save job results to. Supported types include S3, FTP, URL, Arweave, etc. Defaults to node local disk if omitted." ) - .action( - async ( - datasetDids, - algoDid, - computeEnvId, - maxJobDuration, - paymentToken, - resources, - output, - options - ) => { + .action(async (datasetDids, algoDid, computeEnvId, maxJobDuration, paymentToken, resources, output, serviceIds, algoServiceId, options) => { const dsDids = options.datasets || datasetDids; const aDid = options.algo || algoDid; const envId = options.env || computeEnvId; @@ -311,15 +298,38 @@ export async function createCLI() { const token = options.token || paymentToken; const res = options.resources || resources; const outputLocation = options.output || output; + const svcIds = options.services ?? serviceIds ?? ''; + const algoSvcId = options.algoService ?? algoServiceId ?? ''; if (!dsDids || !aDid || !envId || !jobDuration || !token || !res) { - console.error(chalk.red("Missing required arguments")); + console.error(chalk.red('Missing required arguments')); // process.exit(1); + return + } + + const dsArr = + dsDids === '[]' + ? [] + : dsDids.split(',').map(s => s.trim()).filter(Boolean); + + const svArr = svcIds + ? svcIds.split(',').map(s => s.trim()).filter(Boolean) + : undefined; + + // Optional check: serviceIds must match length if provided + if (svArr && svArr.length !== dsArr.length) { + console.error( + chalk.red( + `Length mismatch: datasetDids=${dsArr.length} vs serviceIds=${svArr.length}. ` + + 'If serviceIds is provided, it must match datasetDids length (positional 1–1).' + ) + ); return; } const { signer, chainId } = await initializeSigner(); const commands = new Commands(signer, chainId); - const initArgs = [null, dsDids, aDid, envId, jobDuration, token, res]; + const initArgs = [null, dsDids, aDid, envId, jobDuration, token, res, output, svcIds, algoSvcId]; + console.log('initArgs:', initArgs); const initResp = await commands.initializeCompute(initArgs); if (!initResp) { @@ -361,17 +371,7 @@ export async function createCLI() { console.log(chalk.cyan("Auto-confirm enabled with --yes flag.")); } - const computeArgs = [ - null, - dsDids, - aDid, - envId, - JSON.stringify(initResp), - jobDuration, - token, - res, - outputLocation, - ]; + const computeArgs = [null, dsDids, aDid, envId, JSON.stringify(initResp), jobDuration, token, res, outputLocation, svcIds, algoSvcId]; await commands.computeStart(computeArgs); console.log(chalk.green("Compute job started successfully.")); @@ -382,45 +382,60 @@ export async function createCLI() { program .command("startFreeCompute") .description("Starts a FREE compute job") - .argument( - "", - "Dataset DIDs (comma-separated) OR (empty array for none)" - ) + .argument("", "Dataset DIDs (comma-separated) OR (empty array for none)") .argument("", "Algorithm DID") .argument("", "Compute environment ID") .argument( "[output]", "Output backend to save job results to. Supported types include S3, FTP, URL, Arweave, etc. Defaults to node local disk if omitted." ) - .option( - "-d, --datasets ", - "Dataset DIDs (comma-separated) OR (empty array for none)" - ) + .argument("[serviceIds]", "Service IDs (comma-separated; positional mapping with datasetDIDs)") + .argument("[algoServiceId]", "Algorithm Service ID (optional)") + .option("-d, --datasets ", "Dataset DIDs (comma-separated) OR (empty array for none)") .option("-a, --algo ", "Algorithm DID") .option("-e, --env ", "Compute environment ID") .option( "-o, --output [output]", "Output backend to save job results to. Supported types include S3, FTP, URL, Arweave, etc. Defaults to node local disk if omitted." ) - .action(async (datasetDids, algoDid, computeEnvId, output, options) => { + .option("-s, --services [serviceIds]", "Service IDs (comma-separated; positional mapping with datasetDIDs)") + .option("-x, --algo-service [algoServiceId]", "Algorithm Service ID (optional)") + .action(async (datasetDids, algoDid, computeEnvId, output, serviceIds, algoServiceId, options) => { const dsDids = options.datasets || datasetDids; const aDid = options.algo || algoDid; const envId = options.env || computeEnvId; const outputLocation = options.output || output; + const svcIds = options.services ?? serviceIds ?? ''; + const algoSvcId = options.algoService ?? algoServiceId ?? ''; + if (!dsDids || !aDid || !envId) { console.error(chalk.red("Missing required arguments")); // process.exit(1); + return + } + + const dsArr = + dsDids === '[]' + ? [] + : dsDids.split(',').map(s => s.trim()).filter(Boolean); + + const svArr = svcIds + ? svcIds.split(',').map(s => s.trim()).filter(Boolean) + : undefined; + + // Optional check: serviceIds must match length if provided + if (svArr && svArr.length !== dsArr.length) { + console.error( + chalk.red( + `Length mismatch: datasetDids=${dsArr.length} vs serviceIds=${svArr.length}. ` + + 'If serviceIds is provided, it must match datasetDids length (positional 1–1).' + ) + ); return; } const { signer, chainId } = await initializeSigner(); const commands = new Commands(signer, chainId); - await commands.freeComputeStart([ - null, - dsDids, - aDid, - envId, - outputLocation, - ]); + await commands.freeComputeStart([null, dsDids, aDid, envId, outputLocation, svcIds, algoSvcId]); }); // getComputeEnvironments command @@ -479,7 +494,7 @@ export async function createCLI() { .description("Displays the compute job status") .argument("", "Dataset DID") .argument("", "Job ID") - .argument("", "Agreement ID") + .argument("[agreementId]", "Agreement ID") .option("-d, --dataset ", "Dataset DID") .option("-j, --job ", "Job ID") .option("-a, --agreement [agreementId]", "Agreement ID") diff --git a/src/commands.ts b/src/commands.ts index 148b4e9d..abf8a529 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -30,10 +30,12 @@ import { AccesslistFactory, AccessListContract, } from "@oceanprotocol/lib"; +import { Asset, DDOManager } from '@oceanprotocol/ddo-js'; import { Signer, ethers, getAddress } from "ethers"; import { interactiveFlow } from "./interactiveFlow.js"; import { publishAsset } from "./publishAsset.js"; -import chalk from "chalk"; +import chalk from 'chalk'; +import { getPolicyServerOBJ, getPolicyServerOBJs } from "./policyServerHelper.js"; export class Commands { public signer: Signer; @@ -79,12 +81,15 @@ export class Commands { } const encryptDDO = args[2] === "false" ? false : true; try { + const ddoInstance = DDOManager.getDDOClass(asset); + const { indexedMetadata } = ddoInstance.getAssetFields(); + const { services } = ddoInstance.getDDOFields(); // add some more checks const urlAssetId = await createAssetUtil( - asset.indexedMetadata.nft.name, - asset.indexedMetadata.nft.symbol, + indexedMetadata.nft.name, + indexedMetadata.nft.symbol, this.signer, - asset.services[0].files.files, + (services[0].files as any).files ?? services[0].files, asset, this.oceanNodeUrl, this.config, @@ -111,11 +116,14 @@ export class Commands { const encryptDDO = args[2] === "false" ? false : true; // add some more checks try { + const ddoInstance = DDOManager.getDDOClass(algoAsset); + const { indexedMetadata } = ddoInstance.getAssetFields(); + const { services } = ddoInstance.getDDOFields(); const algoDid = await createAssetUtil( - algoAsset.indexedMetadata.nft.name, - algoAsset.indexedMetadata.nft.symbol, + indexedMetadata.nft.name, + indexedMetadata.nft.symbol, this.signer, - algoAsset.services[0].files.files, + (services[0].files as any).files ?? services[0].files, algoAsset, this.oceanNodeUrl, this.config, @@ -190,8 +198,9 @@ export class Commands { } public async download(args: string[]) { + const did = args[1]; const dataDdo = await this.aquarius.waitForIndexer( - args[1], + did, null, null, this.indexingParams.retryInterval, @@ -199,17 +208,27 @@ export class Commands { ); if (!dataDdo) { console.error( - "Error fetching DDO " + args[1] + ". Does this asset exists?" + "Error fetching DDO " + did + ". Does this asset exists?" ); return; } + const ddoInstance = DDOManager.getDDOClass(dataDdo); + const { services, version } = ddoInstance.getDDOFields(); + const serviceId = args[3] ? args[3] : services[0].id; + let policyServer = null + try { + if (version >= '5.0.0') { + policyServer = await getPolicyServerOBJ(dataDdo, serviceId, this.signer, this.oceanNodeUrl); + } + } catch (error) { + throw new Error('Error getting Policy Server Object: ' + error.message) + } const datatoken = new Datatoken( this.signer, this.config.chainId, this.config ); - const tx = await orderAsset( dataDdo, this.signer, @@ -220,7 +239,7 @@ export class Commands { if (!tx) { console.error( - "Error ordering access for " + args[1] + ". Do you have enough tokens?" + "Error ordering access for " + did + ". Do you have enough tokens?" ); return; } @@ -229,11 +248,12 @@ export class Commands { const downloadResult = await ProviderInstance.getDownloadUrl( dataDdo.id, - dataDdo.services[0].id, + serviceId, 0, orderTx.hash, this.oceanNodeUrl, - this.signer + this.signer, + policyServer ); try { const destPath = args[2] ? args[2] : "."; @@ -255,20 +275,21 @@ export class Commands { const inputDatasetsString = args[1]; let inputDatasets = []; - if ( - inputDatasetsString.includes("[") && - inputDatasetsString.includes("]") - ) { - const processedInput = inputDatasetsString - .replaceAll("]", "") - .replaceAll("[", ""); - if (processedInput.indexOf(",") > -1) { - inputDatasets = processedInput.split(","); - } + if (!inputDatasetsString || inputDatasetsString.trim() === '[]') { + inputDatasets = []; } else { - inputDatasets.push(inputDatasetsString); + const cleaned = inputDatasetsString.replaceAll('[', '').replaceAll(']', ''); + inputDatasets = cleaned.split(',').map(s => s.trim()).filter(Boolean); } + const inputServicesString = args[8]; + let inputServices: string[] = []; + if (typeof inputServicesString === 'string' && inputServicesString.trim().length > 0) { + inputServices = inputServicesString.split(',').map(s => s.trim()).filter(Boolean); + } else if (Array.isArray(inputServicesString)) { + inputServices = inputServicesString.map(String).map(s => s.trim()).filter(Boolean); + } + const ddos = []; for (const dataset in inputDatasets) { @@ -296,10 +317,11 @@ export class Commands { return; } let providerURI = this.oceanNodeUrl; + const ddoInstance = DDOManager.getDDOClass(ddos[0]); + const { services } = ddoInstance.getDDOFields(); if (ddos.length > 0) { - providerURI = ddos[0].services[0].serviceEndpoint; + providerURI = services[0].serviceEndpoint; } - const algoDdo = await this.aquarius.waitForIndexer( args[2], null, @@ -329,7 +351,6 @@ export class Commands { // NO chainId needed anymore (is not part of ComputeEnvironment spec anymore) // const chainComputeEnvs = computeEnvs[computeEnvID]; // was algoDdo.chainId let computeEnv = null; // chainComputeEnvs[0]; - if (computeEnvID && computeEnvID.length > 1) { for (const index in computeEnvs) { if (computeEnvID == computeEnvs[index].id) { @@ -345,18 +366,66 @@ export class Commands { ); return; } - + const ddoAlgoInstance = DDOManager.getDDOClass(algoDdo); + const { services: servicesAlgo, metadata: metadataAlgo, version: versionAlgo } = ddoAlgoInstance.getDDOFields(); + const algoServiceIdInput = args[9] as string | undefined; + let chosenAlgoServiceId = servicesAlgo[0].id; + if (typeof algoServiceIdInput === 'string' && algoServiceIdInput.trim().length > 0) { + const expectedAlgoServiceId = algoServiceIdInput.trim(); + const matchAlgoSvc = servicesAlgo.find((s: any) => s.id === expectedAlgoServiceId); + if (!matchAlgoSvc) { + console.error( + `Algorithm Service ID "${expectedAlgoServiceId}" not found in algo DDO ${algoDdo.id}. ` + + 'Provide a valid service.id from the algorithm asset or omit the argument to use the default (services[0]).' + ); + return; + } + chosenAlgoServiceId = expectedAlgoServiceId; + } const algo: ComputeAlgorithm = { documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, + serviceId: chosenAlgoServiceId, + meta: metadataAlgo.algorithm, }; + const assetAlgo: { + documentId: string; + serviceId: string; + asset: Asset; + version?: string; + } = { + documentId: algoDdo.id, + serviceId: chosenAlgoServiceId, + asset: algoDdo, + version: versionAlgo + }; const assets = []; for (const dataDdo in ddos) { + const ddoInstanceDdo = DDOManager.getDDOClass(ddos[dataDdo]); + const { services: servicesDdo, version: versionDdo } = ddoInstanceDdo.getDDOFields(); + let chosenServiceId = servicesDdo[0].id; + if (inputServices.length > 0) { + const expectedServiceId = inputServices[Number(dataDdo)]; + const match = servicesDdo.find((s: any) => s.id === expectedServiceId); + if (!match) { + console.error( + `Service ID "${expectedServiceId}" not found in dataset ${inputDatasets[Number(dataDdo)]}. ` + + 'Ensure serviceIds[i] exists in the corresponding dataset services.' + ); + return; + } + chosenServiceId = expectedServiceId; + if (Number(dataDdo) === 0 && match.serviceEndpoint) { + providerURI = match.serviceEndpoint; + } + } else { + if (Number(dataDdo) === 0 && servicesDdo[0]?.serviceEndpoint) { + providerURI = servicesDdo[0].serviceEndpoint; + } + } const canStartCompute = isOrderable( ddos[dataDdo], - ddos[dataDdo].services[0].id, + chosenServiceId, algo, algoDdo ); @@ -368,10 +437,12 @@ export class Commands { } assets.push({ documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, + serviceId: chosenServiceId, + asset: ddos[dataDdo], + version: versionDdo }); } - const maxJobDuration = Number(args[4]); + const maxJobDuration = Number(args[4]) if (!maxJobDuration) { console.error( "Error initializing Provider for the compute job using dataset DID " + @@ -396,7 +467,7 @@ export class Commands { if (maxJobDuration > computeEnv.maxJobDuration) { supportedMaxJobDuration = computeEnv.maxJobDuration; } - const paymentToken = args[5]; + const paymentToken = args[5] if (!paymentToken) { console.error( "Error initializing Provider for the compute job using dataset DID " + @@ -407,7 +478,7 @@ export class Commands { ); return; } - const { chainId } = await this.signer.provider.getNetwork(); + const { chainId } = await this.signer.provider.getNetwork() if (!Object.keys(computeEnv.fees).includes(chainId.toString())) { console.error( "Error starting paid compute using dataset DID " + @@ -439,7 +510,7 @@ export class Commands { ); return; } - const resources = args[6]; // resources object should be stringified in cli when calling initializeCompute + const resources = args[6] // resources object should be stringified in cli when calling initializeCompute if (!resources) { console.error( "Error initializing Provider for the compute job using dataset DID " + @@ -450,6 +521,7 @@ export class Commands { ); return; } + const policiesServer = await getPolicyServerOBJs(assets, assetAlgo, this.signer, this.oceanNodeUrl); const parsedResources = JSON.parse(resources); const providerInitializeComputeJob = await ProviderInstance.initializeCompute( @@ -461,7 +533,8 @@ export class Commands { providerURI, await this.signer.getAddress(), parsedResources, - Number(chainId) + Number(chainId), + policiesServer ); if ( !providerInitializeComputeJob || @@ -475,29 +548,32 @@ export class Commands { ); return; } - console.log(chalk.yellow("\n--- Payment Details ---")); + console.log(chalk.yellow('\n--- Payment Details ---')); console.log(JSON.stringify(providerInitializeComputeJob, null, 2)); return providerInitializeComputeJob; + } public async computeStart(args: string[]) { const inputDatasetsString = args[1]; let inputDatasets = []; - if ( - inputDatasetsString.includes("[") && - inputDatasetsString.includes("]") - ) { - const processedInput = inputDatasetsString - .replaceAll("]", "") - .replaceAll("[", ""); - if (processedInput.indexOf(",") > -1) { - inputDatasets = processedInput.split(","); - } + if (!inputDatasetsString || inputDatasetsString.trim() === '[]') { + inputDatasets = []; } else { - inputDatasets.push(inputDatasetsString); + const cleaned = inputDatasetsString.replaceAll('[', '').replaceAll(']', ''); + inputDatasets = cleaned.split(',').map(s => s.trim()).filter(Boolean); } + const inputServicesString = args[9]; + let inputServices: string[] = []; + if (typeof inputServicesString === 'string' && inputServicesString.trim().length > 0) { + inputServices = inputServicesString.split(',').map(s => s.trim()).filter(Boolean); + } else if (Array.isArray(inputServicesString)) { + inputServices = inputServicesString.map(String).map(s => s.trim()).filter(Boolean); + } + + const ddos = []; for (const dataset in inputDatasets) { @@ -525,8 +601,10 @@ export class Commands { return; } let providerURI = this.oceanNodeUrl; + const ddoInstance = DDOManager.getDDOClass(ddos[0]); + const { services } = ddoInstance.getDDOFields(); if (ddos.length > 0) { - providerURI = ddos[0].services[0].serviceEndpoint; + providerURI = services[0].serviceEndpoint; } const algoDdo = await this.aquarius.waitForIndexer( args[2], @@ -572,18 +650,81 @@ export class Commands { ); return; } - + const ddoInstanceAlgo = DDOManager.getDDOClass(algoDdo); + const { services: servicesAlgo, metadata: metadataAlgo, version: versionAlgo } = ddoInstanceAlgo.getDDOFields(); + const algoServiceIdInput = args[10] as string | undefined; + let chosenAlgoServiceId = servicesAlgo[0].id; + if (typeof algoServiceIdInput === 'string' && algoServiceIdInput.trim().length > 0) { + const expectedAlgoServiceId = algoServiceIdInput.trim(); + const matchAlgoSvc = servicesAlgo.find((s: any) => s.id === expectedAlgoServiceId); + if (!matchAlgoSvc) { + console.error( + `Algorithm Service ID "${expectedAlgoServiceId}" not found in algo DDO ${algoDdo.id}. ` + + 'Provide a valid service.id from the algorithm asset or omit the argument to use the default (services[0]).' + ); + return; + } + chosenAlgoServiceId = expectedAlgoServiceId; + } + const algoServiceIndex = servicesAlgo.findIndex((s: any) => s.id === chosenAlgoServiceId); + if (algoServiceIndex < 0) { + console.error(`Could not resolve serviceIndex for algorithm serviceId ${chosenAlgoServiceId}`); + return; + } const algo: ComputeAlgorithm = { documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, + serviceId: chosenAlgoServiceId, + meta: metadataAlgo.algorithm, }; + const assetAlgo: { + documentId: string; + serviceId: string; + asset: Asset; + version?: string; + } = { + documentId: algoDdo.id, + serviceId: chosenAlgoServiceId, + asset: algoDdo, + version: versionAlgo + }; + const assets = []; + const datasetServiceIndex: number[] = []; for (const dataDdo in ddos) { + const ddoInstanceDdo = DDOManager.getDDOClass(ddos[dataDdo]); + const { services: servicesDdo, version: versionDdo } = ddoInstanceDdo.getDDOFields(); + let chosenServiceId = servicesDdo[0].id; + if (inputServices.length > 0) { + const expectedServiceId = inputServices[Number(dataDdo)]; + const match = servicesDdo.find((s: any) => s.id === expectedServiceId); + if (!match) { + console.error( + `Service ID "${expectedServiceId}" not found in dataset ${inputDatasets[Number(dataDdo)]}. ` + + 'Ensure serviceIds[i] exists in the corresponding dataset services.' + ); + return; + } + chosenServiceId = expectedServiceId; + if (Number(dataDdo) === 0 && match.serviceEndpoint) { + providerURI = match.serviceEndpoint; + } + } else { + if (Number(dataDdo) === 0 && servicesDdo[0]?.serviceEndpoint) { + providerURI = servicesDdo[0].serviceEndpoint; + } + } + const chosenServiceIndex = servicesDdo.findIndex((s: any) => s.id === chosenServiceId); + if (chosenServiceIndex < 0) { + console.error( + `Could not resolve serviceIndex for dataset ${ddos[dataDdo].id} with serviceId ${chosenServiceId}` + ); + return; + } + datasetServiceIndex.push(chosenServiceIndex); const canStartCompute = isOrderable( ddos[dataDdo], - ddos[dataDdo].services[0].id, + chosenServiceId, algo, algoDdo ); @@ -595,13 +736,13 @@ export class Commands { } assets.push({ documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, + serviceId: chosenServiceId, + asset: ddos[dataDdo], + version: versionDdo }); } const providerInitializeComputeJob = args[4]; // provider fees + payment - const parsedProviderInitializeComputeJob = fixAndParseProviderFees( - providerInitializeComputeJob - ); + const parsedProviderInitializeComputeJob = fixAndParseProviderFees(providerInitializeComputeJob) console.log("Ordering algorithm: ", args[2]); const datatoken = new Datatoken( this.signer, @@ -613,7 +754,7 @@ export class Commands { algoDdo, this.signer, computeEnv.consumerAddress, - 0, + algoServiceIndex, datatoken, this.config, parsedProviderInitializeComputeJob?.algorithm?.providerFee, @@ -635,7 +776,7 @@ export class Commands { ddos[i], this.signer, computeEnv.consumerAddress, - 0, + datasetServiceIndex[i], datatoken, this.config, parsedProviderInitializeComputeJob?.datasets[i].providerFee, @@ -651,7 +792,7 @@ export class Commands { } } // payment check - const maxJobDuration = Number(args[5]); + const maxJobDuration = Number(args[5]) if (!maxJobDuration) { console.error( "Error initializing Provider for the compute job using dataset DID " + @@ -676,8 +817,8 @@ export class Commands { if (maxJobDuration > computeEnv.maxJobDuration) { supportedMaxJobDuration = computeEnv.maxJobDuration; } - const { chainId } = await this.signer.provider.getNetwork(); - const paymentToken = args[6]; + const { chainId } = await this.signer.provider.getNetwork() + const paymentToken = args[6] if (!paymentToken) { console.error( "Error starting paid compute using dataset DID " + @@ -719,7 +860,7 @@ export class Commands { ); return; } - const resources = args[7]; // resources object should be stringified in cli when calling initializeCompute + const resources = args[7] // resources object should be stringified in cli when calling initializeCompute if (!resources) { console.error( "Error starting paid compute using dataset DID " + @@ -764,29 +905,6 @@ export class Commands { console.log("Starting compute job using provider: ", providerURI); - const additionalDatasets = assets.length > 1 ? assets.slice(1) : null; - if (assets.length > 0) { - console.log( - "Starting compute job on " + - assets[0].documentId + - " with additional datasets:" + - (!additionalDatasets ? "none" : additionalDatasets[0].documentId) - ); - } else { - console.log( - "Starting compute job on " + - algo.documentId + - " with additional datasets:" + - (!additionalDatasets ? "none" : additionalDatasets[0].documentId) - ); - } - if (additionalDatasets !== null) { - console.log( - "Adding additional datasets to dataset, according to C2D V2 specs" - ); - assets.push(additionalDatasets); - } - let output = null; if (args[8]) { try { @@ -796,6 +914,7 @@ export class Commands { return; } } + const policiesServer = await getPolicyServerOBJs(assets, assetAlgo, this.signer, this.oceanNodeUrl); const computeJobs = await ProviderInstance.computeStart( providerURI, @@ -810,7 +929,8 @@ export class Commands { null, null, // additionalDatasets, only c2d v1 - output + output, + policiesServer ); console.log("computeJobs: ", computeJobs); @@ -828,20 +948,19 @@ export class Commands { const inputDatasetsString = args[1]; let inputDatasets = []; - if ( - inputDatasetsString.includes("[") && - inputDatasetsString.includes("]") - ) { - const processedInput = inputDatasetsString - .replaceAll("]", "") - .replaceAll("[", ""); - if (processedInput.indexOf(",") > -1) { - inputDatasets = processedInput.split(","); - } + if (!inputDatasetsString || inputDatasetsString.trim() === '[]') { + inputDatasets = []; } else { - inputDatasets.push(inputDatasetsString); - } - + const cleaned = inputDatasetsString.replaceAll('[', '').replaceAll(']', ''); + inputDatasets = cleaned.split(',').map(s => s.trim()).filter(Boolean); + } + const inputServicesString = args[5]; + let inputServices: string[] = []; + if (typeof inputServicesString === 'string' && inputServicesString.trim().length > 0) { + inputServices = inputServicesString.split(',').map(s => s.trim()).filter(Boolean); + } else if (Array.isArray(inputServicesString)) { + inputServices = inputServicesString.map(String).map(s => s.trim()).filter(Boolean); + } const ddos = []; for (const dataset in inputDatasets) { @@ -870,8 +989,10 @@ export class Commands { return; } let providerURI = this.oceanNodeUrl; + const ddoInstance = DDOManager.getDDOClass(ddos[0]); + const { services } = ddoInstance.getDDOFields(); if (ddos.length > 0) { - providerURI = ddos[0].services[0].serviceEndpoint; + providerURI = services[0].serviceEndpoint; } const algoDdo = await this.aquarius.waitForIndexer( @@ -891,7 +1012,6 @@ export class Commands { const computeEnvs = await ProviderInstance.getComputeEnvironments( this.oceanNodeUrl ); - if (!computeEnvs || computeEnvs.length < 1) { console.error( "Error fetching compute environments. No compute environments available." @@ -907,7 +1027,6 @@ export class Commands { // NO chainId needed anymore (is not part of ComputeEnvironment spec anymore) // const chainComputeEnvs = computeEnvs[computeEnvID]; // was algoDdo.chainId let computeEnv = null; // chainComputeEnvs[0]; - if (computeEnvID && computeEnvID.length > 1) { for (const env of computeEnvs) { if (computeEnvID == env.id && env.free) { @@ -924,18 +1043,68 @@ export class Commands { ); return; } - + const ddoInstanceAlgo = DDOManager.getDDOClass(algoDdo); + const { services: servicesAlgo, metadata: metadataAlgo, version: versionAlgo } = ddoInstanceAlgo.getDDOFields(); + + const algoServiceIdInput = args[6] as string | undefined; + let chosenAlgoServiceId = servicesAlgo[0].id; + if (typeof algoServiceIdInput === 'string' && algoServiceIdInput.trim().length > 0) { + const expectedAlgoServiceId = algoServiceIdInput.trim(); + const matchAlgoSvc = servicesAlgo.find((s: any) => s.id === expectedAlgoServiceId); + if (!matchAlgoSvc) { + console.error( + `Algorithm Service ID "${expectedAlgoServiceId}" not found in algo DDO ${algoDdo.id}. ` + + 'Provide a valid service.id from the algorithm asset or omit the argument to use the default (services[0]).' + ); + return; + } + chosenAlgoServiceId = expectedAlgoServiceId; + } const algo: ComputeAlgorithm = { documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, + serviceId: chosenAlgoServiceId, + meta: metadataAlgo.algorithm, }; + const assetAlgo: { + documentId: string; + serviceId: string; + asset: Asset; + version?: string; + } = { + documentId: algoDdo.id, + serviceId: chosenAlgoServiceId, + asset: algoDdo, + version: versionAlgo + }; + const assets = []; for (const dataDdo in ddos) { + const ddoInstanceDdo = DDOManager.getDDOClass(ddos[dataDdo]); + const { services: servicesDdo, version: versionDdo } = ddoInstanceDdo.getDDOFields(); + let chosenServiceId = servicesDdo[0].id; + if (inputServices.length > 0) { + const expectedServiceId = inputServices[Number(dataDdo)]; + const match = servicesDdo.find((s: any) => s.id === expectedServiceId); + if (!match) { + console.error( + `Service ID "${expectedServiceId}" not found in dataset ${inputDatasets[Number(dataDdo)]}. ` + + 'Ensure serviceIds[i] exists in the corresponding dataset services.' + ); + return; + } + chosenServiceId = expectedServiceId; + if (Number(dataDdo) === 0 && match.serviceEndpoint) { + providerURI = match.serviceEndpoint; + } + } else { + if (Number(dataDdo) === 0 && servicesDdo[0]?.serviceEndpoint) { + providerURI = servicesDdo[0].serviceEndpoint; + } + } const canStartCompute = isOrderable( ddos[dataDdo], - ddos[dataDdo].services[0].id, + chosenServiceId, algo, algoDdo ); @@ -947,35 +1116,12 @@ export class Commands { } assets.push({ documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, + serviceId: chosenServiceId, + asset: ddos[dataDdo], + version: versionDdo }); } - console.log("Starting compute job using provider: ", providerURI); - const additionalDatasets = assets.length > 1 ? assets.slice(1) : null; - if (assets.length > 0) { - console.log( - "Starting compute job on " + - assets[0].documentId + - " with additional datasets:" + - (!additionalDatasets ? "none" : additionalDatasets[0].documentId) - ); - } else { - console.log( - "Starting compute job on " + - algo.documentId + - " with additional datasets:" + - (!additionalDatasets ? "none" : additionalDatasets[0].documentId) - ); - } - - if (additionalDatasets !== null) { - console.log( - "Adding additional datasets to dataset, according to C2D V2 specs" - ); - assets.push(additionalDatasets); - } - let output = null; if (args[4]) { try { @@ -986,6 +1132,7 @@ export class Commands { } } + const policiesServer = await getPolicyServerOBJs(assets, assetAlgo, this.signer, this.oceanNodeUrl); const computeJobs = await ProviderInstance.freeComputeStart( providerURI, this.signer, @@ -995,10 +1142,10 @@ export class Commands { null, null, null, - output + output, + policiesServer ); - console.log("compute jobs: ", computeJobs); if (computeJobs && computeJobs[0]) { const { jobId } = computeJobs[0]; @@ -1082,21 +1229,24 @@ export class Commands { this.indexingParams.retryInterval, this.indexingParams.maxRetries ); + if (!asset) { console.error( "Error fetching DDO " + args[1] + ". Does this asset exists?" ); return; } - - if (asset.indexedMetadata.nft.owner !== (await this.signer.getAddress())) { + const ddoInstance = DDOManager.getDDOClass(asset); + const { indexedMetadata } = ddoInstance.getAssetFields(); + const { services } = ddoInstance.getDDOFields(); + if (indexedMetadata.nft.owner !== (await this.signer.getAddress())) { console.error( "You are not the owner of this asset, and there for you cannot update it." ); return; } - if (asset.services[0].type !== "compute") { + if (services[0].type !== "compute") { console.error( "Error getting computeService for " + args[1] + @@ -1117,13 +1267,15 @@ export class Commands { ); return; } + const algoInstance = DDOManager.getDDOClass(algoAsset); + const { services: servicesAlgo, metadata: metadataAlgo } = algoInstance.getDDOFields(); const encryptDDO = args[3] === "false" ? false : true; let filesChecksum; try { filesChecksum = await ProviderInstance.checkDidFiles( algoAsset.id, - algoAsset.services[0].id, - algoAsset.services[0].serviceEndpoint, + servicesAlgo[0].id, + servicesAlgo[0].serviceEndpoint, true ); } catch (e) { @@ -1132,14 +1284,15 @@ export class Commands { } const containerChecksum = - algoAsset.metadata.algorithm.container.entrypoint + - algoAsset.metadata.algorithm.container.checksum; + metadataAlgo.algorithm.container.entrypoint + + metadataAlgo.algorithm.container.checksum; const trustedAlgorithm = { did: algoAsset.id, containerSectionChecksum: getHash(containerChecksum), filesChecksum: filesChecksum?.[0]?.checksum, + serviceId: servicesAlgo[0].id, }; - asset.services[0].compute.publisherTrustedAlgorithms.push(trustedAlgorithm); + services[0].compute.publisherTrustedAlgorithms.push(trustedAlgorithm); try { const txid = await updateAssetMetadata( this.signer, @@ -1169,13 +1322,16 @@ export class Commands { ); return; } - if (asset.indexedMetadata.nft.owner !== (await this.signer.getAddress())) { + const ddoInstance = DDOManager.getDDOClass(asset); + const { indexedMetadata } = ddoInstance.getAssetFields(); + const { services } = ddoInstance.getDDOFields(); + if (indexedMetadata.nft.owner !== (await this.signer.getAddress())) { console.error( "You are not the owner of this asset, and there for you cannot update it." ); return; } - if (asset.services[0].type !== "compute") { + if (services[0].type !== "compute") { console.error( "Error getting computeService for " + args[1] + @@ -1183,7 +1339,7 @@ export class Commands { ); return; } - if (asset.services[0].compute.publisherTrustedAlgorithms) { + if (services[0].compute.publisherTrustedAlgorithms) { console.error( " " + args[1] + ". Does this asset has an computeService?" ); @@ -1191,12 +1347,12 @@ export class Commands { } const encryptDDO = args[3] === "false" ? false : true; const indexToDelete = - asset.services[0].compute.publisherTrustedAlgorithms.findIndex( + services[0].compute.publisherTrustedAlgorithms.findIndex( (item) => item.did === args[2] ); if (indexToDelete !== -1) { - asset.services[0].compute.publisherTrustedAlgorithms.splice( + services[0].compute.publisherTrustedAlgorithms.splice( indexToDelete, 1 ); @@ -1549,8 +1705,7 @@ export class Commands { console.log(`Transferable: ${transferable}`); console.log(`Owner: ${owner}`); console.log( - `Initial users: ${ - initialUsers.length > 0 ? initialUsers.join(", ") : "none" + `Initial users: ${initialUsers.length > 0 ? initialUsers.join(", ") : "none" }` ); diff --git a/src/helpers.ts b/src/helpers.ts index afe59d63..a08f7e70 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -3,7 +3,7 @@ import fetch from "cross-fetch"; import { promises as fs, readFileSync } from "fs"; import * as path from "path"; import * as sapphire from '@oasisprotocol/sapphire-paratime'; -import { Asset, DDO } from '@oceanprotocol/ddo-js'; +import { Asset, DDO, DDOManager } from '@oceanprotocol/ddo-js'; import { AccesslistFactory, Aquarius, Nft, @@ -118,7 +118,6 @@ export async function createAssetUtil( accessListFactory?: string, allowAccessList?: string, denyAccessList?: string, - ) { const isAddress = typeof templateIDorAddress === 'string' const isTemplateIndex = typeof templateIDorAddress === 'number' @@ -159,10 +158,12 @@ export async function updateAssetMetadata( let flags; let metadata; const validateResult = await aquariusInstance.validate(updatedDdo, owner, oceanNodeUrl); + const ddoInstance = DDOManager.getDDOClass(updatedDdo); + const { chainId, nftAddress } = ddoInstance.getDDOFields(); if (encryptDDO) { const providerResponse = await ProviderInstance.encrypt( updatedDdo, - updatedDdo.chainId, + chainId, oceanNodeUrl, owner ); @@ -177,7 +178,7 @@ export async function updateAssetMetadata( } const updateDdoTX = await nft.setMetadata( - updatedDdo.nftAddress, + nftAddress, await owner.getAddress(), 0, oceanNodeUrl, @@ -257,7 +258,9 @@ export async function isOrderable( algorithm: ComputeAlgorithm, algorithmDDO: Asset | DDO ): Promise { - const datasetService = asset.services.find((s) => s.id === serviceId); + const ddoInstanceAsset = DDOManager.getDDOClass(asset); + const { services: servicesAsset } = ddoInstanceAsset.getDDOFields(); + const datasetService = servicesAsset.find((s) => s.id === serviceId); if (!datasetService) return false; if (datasetService.type === "compute") { @@ -421,4 +424,4 @@ export async function getConfigByChainId(chainId: number) { } return chainConfig; -} \ No newline at end of file +} diff --git a/src/policyServerHelper.ts b/src/policyServerHelper.ts new file mode 100644 index 00000000..7127adbc --- /dev/null +++ b/src/policyServerHelper.ts @@ -0,0 +1,418 @@ +import { Asset } from "@oceanprotocol/ddo-js" +import { PolicyServerActions, PolicyServerGetPdAction, PolicyServerInitiateAction, PolicyServerInitiateActionData, PolicyServerInitiateComputeActionData, PolicyServerPresentationDefinition, SsiVerifiableCredential, SsiWalletDid, SsiWalletSession } from "./policyServerInterfaces" +import axios from "axios" +import { Signer } from "ethers" + +export async function connectToSSIWallet( + owner: Signer, + api: string +): Promise { + if (!api) { + throw new Error('No SSI Wallet API configured') + } + + try { + let response = await axios.get(`${api}/wallet-api/auth/account/web3/nonce`) + + const nonce = response.data + const payload = { + challenge: nonce, + signed: await owner.signMessage(nonce), + publicKey: await owner.getAddress() + } + + response = await axios.post( + `${api}/wallet-api/auth/account/web3/signed`, + payload + ) + return response.data + } catch (error) { + throw error.response + } +} + +export async function sendPresentationRequest( + walletId: string, + did: string, + presentationRequest: string, + selectedCredentials: string[], + token: string, + api: string +): Promise<{ redirectUri: string }> { + if (!api) { + throw new Error('No SSI Wallet API configured') + } + try { + const response = await axios.post( + `${api}/wallet-api/wallet/${walletId}/exchange/usePresentationRequest`, + { + did, + presentationRequest, + selectedCredentials + }, + { + headers: { + Authorization: `Bearer ${token}` + }, + withCredentials: true + } + ) + + return response.data + } catch (error) { + throw error.response + } +} + +export async function resolvePresentationRequest( + walletId: string, + presentationRequest: string, + token: string, + api: string +): Promise { + if (!api) { + throw new Error('No SSI Wallet API configured') + } + try { + const response = await axios.post( + `${api}/wallet-api/wallet/${walletId}/exchange/resolvePresentationRequest`, + presentationRequest, + { + headers: { + Authorization: `Bearer ${token}` + }, + withCredentials: true + } + ) + + return response.data + } catch (error) { + throw error.response + } +} + +export async function getWalletDids( + walletId: string, + token: string, + api: string +): Promise { + if (!api) { + throw new Error('No SSI Wallet API configured') + } + try { + const response = await axios.get( + `${api}/wallet-api/wallet/${walletId}/dids`, + { + headers: { + Authorization: `Bearer ${token}` + }, + withCredentials: true + } + ) + + return response.data + } catch (error) { + throw error.response + } +} + + +export async function requestCredentialPresentation( + asset: Asset, + consumerAddress: string, + serviceId: string, + providerUrl: string +): Promise<{ + success: boolean + openid4vc: string + policyServerData: PolicyServerInitiateActionData, +}> { + try { + const sessionId = crypto.randomUUID() + + const policyServer: PolicyServerInitiateActionData = { + sessionId, + successRedirectUri: ``, + errorRedirectUri: ``, + responseRedirectUri: ``, + presentationDefinitionUri: `` + } + + const action: PolicyServerInitiateAction = { + action: PolicyServerActions.INITIATE, + ddo: asset, + policyServer, + serviceId, + consumerAddress, + documentId: asset.id + } + const response = await axios.post( + `${providerUrl}/api/services/PolicyServerPassthrough`, + { + policyServerPassthrough: action + } + ) + + if (response.data.length === 0) { + // eslint-disable-next-line no-throw-literal + throw { success: false, message: 'No openid4vc url found' } + } + + return { + success: response.data?.success, + openid4vc: response.data?.message, + policyServerData: policyServer + } + } catch (error) { + if (error.request?.response) { + const err = JSON.parse(error.request.response) + throw err + } + if (error.response?.data) { + throw error.response?.data + } + throw error + } +} + +export async function matchCredentialForPresentationDefinition( + api: string, + walletId: string, + presentationDefinition: any, + token: string +): Promise { + if (!api) { + throw new Error('No SSI Wallet API configured') + } + try { + const response = await axios.post( + `${api}/wallet-api/wallet/${walletId}/exchange/matchCredentialsForPresentationDefinition`, + presentationDefinition, + { + headers: { + Authorization: `Bearer ${token}` + }, + withCredentials: true + } + ) + + return response.data + } catch (error) { + throw error.response + } +} + +export async function getPd( + sessionId: string, + providerUrl: string +): Promise { + try { + const action: PolicyServerGetPdAction = { + action: PolicyServerActions.GET_PD, + sessionId + } + const response = await axios.post( + `${providerUrl}/api/services/PolicyServerPassthrough`, + { + policyServerPassthrough: action + } + ) + + if (typeof response.data === 'string' && response.data.length === 0) { + // eslint-disable-next-line no-throw-literal + throw { + success: false, + message: 'Could not read presentation definition' + } + } + + return response.data?.message + } catch (error) { + if (error.response?.data) { + throw error.response?.data + } + throw error + } +} + +export function extractURLSearchParams( + urlString: string +): Record { + const url = new URL(urlString) + const { searchParams } = url + const params: Record = {} + searchParams.forEach((value, key) => (params[key] = value)) + return params +} + +export async function getPolicyServerOBJ( + ddo: Asset, + serviceId: string, + signer: Signer, + providerUrl: string +): Promise { + try { + const accountId = await signer.getAddress() + const presentationResult = await requestCredentialPresentation( + ddo, + accountId, + serviceId, + providerUrl + ) + if ( + !presentationResult.openid4vc || + !presentationResult.success || + !presentationResult.policyServerData.sessionId + ) { + throw new Error('No valid openid4vc url found') + } + if ( + presentationResult.openid4vc && + typeof presentationResult.openid4vc === 'object' && + (presentationResult.openid4vc as any).redirectUri && + (presentationResult.openid4vc as any).redirectUri.includes( + 'success' + ) + ) { + const { id } = extractURLSearchParams( + (presentationResult.openid4vc as any).redirectUri + ) + return { + sessionId: id, + successRedirectUri: '', + errorRedirectUri: '', + responseRedirectUri: '', + presentationDefinitionUri: '' + } + } + const verifierSessionId = presentationResult.policyServerData.sessionId + + const presentationDefinition = await getPd(verifierSessionId, providerUrl) + const ssiApi = process.env.SSI_WALLET_API + if (!ssiApi) { + throw new Error('No SSI_WALLET_API configured') + } + const sessionToken = await connectToSSIWallet(signer, ssiApi) + const walletId = process.env.SSI_WALLET_ID + if (!walletId) { + throw new Error('No SSI_WALLET_ID configured') + } + const verifiableCredentials = await matchCredentialForPresentationDefinition( + ssiApi, + walletId, + presentationDefinition, + sessionToken.token + ) + const dids = await getWalletDids( + walletId, + sessionToken.token, + ssiApi + ) + if (!dids || dids.length === 0) { + throw new Error('No DIDs found in wallet') + } + const resolvedPresentationRequest = await resolvePresentationRequest( + walletId, + presentationResult.openid4vc, + sessionToken.token, + ssiApi + ) + const myDid = process.env.SSI_WALLET_DID + if (myDid && !dids.find((d) => d.did === myDid)) { + throw new Error(`DID ${myDid} not found in wallet`) + } + const did = myDid ? myDid : dids[0].did + const result = await sendPresentationRequest( + walletId, + did, + resolvedPresentationRequest, + verifiableCredentials.map((vc) => vc.id), + sessionToken.token, + ssiApi + ) + if ( + 'errorMessage' in result || + (result.redirectUri && result.redirectUri.includes('error')) + ) { + throw new Error('Credential presentation failed') + } + return { + sessionId: verifierSessionId, + successRedirectUri: '', + errorRedirectUri: '', + responseRedirectUri: '', + presentationDefinitionUri: '' + } + } catch (error: any) { + console.error('getPolicyServerOBJ error:', error) + if (error?.message) { + throw new Error(`getPolicyServerOBJ failed: ${error.message}`) + } + throw new Error('getPolicyServerOBJ failed') + } +} + +export async function getPolicyServerOBJs( + ddos: { + documentId: string + serviceId: string + asset: Asset + version?: string + }[], + algo: { + documentId: string + serviceId: string + asset: Asset + version?: string + }, + signer: Signer, + providerUrl: string +): Promise { + try { + const results: PolicyServerInitiateComputeActionData[] = [] + + // --- datasets + for (const ddo of ddos) { + if (!ddo.version || ddo.version < '5.0.0') { + return null + } + const result = await getPolicyServerOBJ( + ddo.asset, + ddo.serviceId, + signer, + providerUrl + ) + results.push({ + ...result, + documentId: ddo.documentId, + serviceId: ddo.serviceId + }) + } + + // --- algo + if (!algo?.version || algo.version < '5.0.0') { + return null + } + if (algo.serviceId) { + const algoResult = await getPolicyServerOBJ( + algo.asset, + algo.serviceId, + signer, + providerUrl + ) + results.push({ + ...algoResult, + documentId: algo.documentId, + serviceId: algo.serviceId + }) + } + + return results + } catch (error: any) { + console.error('getPolicyServerOBJs error:', error) + if (error?.message) { + throw new Error(`getPolicyServerOBJs failed: ${error.message}`) + } + throw new Error('getPolicyServerOBJs failed') + } +} diff --git a/src/policyServerInterfaces.ts b/src/policyServerInterfaces.ts new file mode 100644 index 00000000..99a44f0d --- /dev/null +++ b/src/policyServerInterfaces.ts @@ -0,0 +1,105 @@ +export interface SsiWalletSession { + session_id: string + status: string + token: string + expiration: Date +} + +export interface SsiVerifiableCredential { + id: string + parsedDocument: { + id: string + type: string[] + issuer: string + issuanceDate: Date + credentialSubject: Record + } +} + +export interface SsiWalletDid { + alias: string + did: string + document: string + keyId: string +} + +export enum PolicyServerActions { + INITIATE = 'initiate', + GET_PD = 'getPD', + CHECK_SESSION_ID = 'checkSessionId', + PRESENTATION_REQUEST = 'presentationRequest', + DOWNLOAD = 'download', + PASSTHROUGH = 'passthrough' +} + +export interface PolicyServerResponse { + success: boolean + message: string + httpStatus: number +} + +export interface PolicyServerInitiateActionData { + sessionId: string + successRedirectUri: string + errorRedirectUri: string + responseRedirectUri: string + presentationDefinitionUri: string +} + +export interface PolicyServerInitiateComputeActionData + extends PolicyServerInitiateActionData { + documentId: string + serviceId: string +} + + +export interface PolicyServerInitiateComputeActionData + extends PolicyServerInitiateActionData { + documentId: string + serviceId: string +} + +export interface PolicyServerInitiateAction { + action: PolicyServerActions.INITIATE + ddo: any + policyServer: PolicyServerInitiateActionData + serviceId: string + consumerAddress: string + documentId: string +} + +export interface PolicyServerGetPdAction { + action: PolicyServerActions.GET_PD + sessionId: string +} + +export interface PolicyServerCheckSessionIdAction { + action: PolicyServerActions.CHECK_SESSION_ID + sessionId: string +} + +export interface PolicyServerPresentationRequestAction { + action: PolicyServerActions.PRESENTATION_REQUEST + sessionId: string + vp_token: any + response: any + presentation_submission: any +} + +export interface PolicyServerDownloadAction { + action: PolicyServerActions.DOWNLOAD + policyServer: { + sessionId: string + } +} + +export interface PolicyServerPassthrough { + action: PolicyServerActions.PASSTHROUGH + url: string + httpMethod: 'GET' + body: any +} + +export interface PolicyServerPresentationDefinition { + input_descriptors: any[] +} diff --git a/test/accessList.test.ts b/test/accessList.test.ts index 72f776a8..fd49b1a4 100644 --- a/test/accessList.test.ts +++ b/test/accessList.test.ts @@ -291,4 +291,3 @@ describe("Ocean CLI Access List", function () { }); }); }); - diff --git a/test/consumeFlow.test.ts b/test/consumeFlow.test.ts index 0f7e25d0..7b871810 100644 --- a/test/consumeFlow.test.ts +++ b/test/consumeFlow.test.ts @@ -68,6 +68,28 @@ describe("Ocean CLI Publishing", function() { }); + it("should publish a dataset v5 using 'npm run cli publish'", async function() { + const metadataFile = path.resolve(projectRoot, "metadata/simpleDownloadDatasetV5.json"); + + // Ensure the metadata file exists + if (!fs.existsSync(metadataFile)) { + throw new Error("Metadata file not found: " + metadataFile); + } + + process.env.PRIVATE_KEY = "0x1d751ded5a32226054cd2e71261039b65afb9ee1c746d055dd699b1150a5befc"; + // Using this account: 0x529043886F21D9bc1AE0feDb751e34265a246e47 + process.env.RPC = "http://127.0.0.1:8545"; + process.env.NODE_URL = "http://127.0.0.1:8001"; + process.env.ADDRESS_FILE = path.join(process.env.HOME || "", ".ocean/ocean-contracts/artifacts/address.json"); + + const output = await runCommand(`npm run cli publish ${metadataFile}`); + const jsonMatch = output.match(/did:ope:[a-f0-9]{64}/); + if (!jsonMatch) { + console.error("Raw output:", output); + throw new Error("Could not find did in the output"); + } + }); + it("should publish a compute dataset using 'npm run cli publish'", async function() { const metadataFile = path.resolve(projectRoot, "metadata/simpleComputeDataset.json"); // Ensure the metadata file exists diff --git a/test/interactivePublishFlow.ts b/test/interactivePublishFlow.ts index b8cd82ea..6e92a647 100644 --- a/test/interactivePublishFlow.ts +++ b/test/interactivePublishFlow.ts @@ -43,7 +43,9 @@ describe("Ocean CLI Interactive Publishing", function() { if (child.stdin) { const inputInterval = setInterval(() => { if (inputIndex < inputs.length) { + if (child.stdin) { child.stdin.write(inputs[inputIndex]); + } inputIndex++; } else { clearInterval(inputInterval); diff --git a/test/paidComputeFlow.test.ts b/test/paidComputeFlow.test.ts index 8847cb38..06e97322 100644 --- a/test/paidComputeFlow.test.ts +++ b/test/paidComputeFlow.test.ts @@ -148,14 +148,16 @@ describe("Ocean CLI Paid Compute", function() { console.log(`Fetched Compute Env ID: ${computeEnvId}`); }); - it("should start paid compute on compute dataset and algorithm", async function() { + it("should start paid compute on compute dataset and algorithm with services id for dataset and algorithm", async function() { const computeEnvs = await ProviderInstance.getComputeEnvironments('http://127.0.0.1:8001'); const env = computeEnvs[0]; expect(env).to.be.an('object').and.to.not.be.null.and.to.not.be.undefined; resources = [] const paymentToken = getAddresses().Ocean - const output = await runCommand(`npm run cli -- startCompute ${computeDatasetDid} ${jsAlgoDid} ${computeEnvId} 900 ${paymentToken} '${JSON.stringify(resources)}' --accept true`); + const serviceIdDataset = 'ccb398c50d6abd5b456e8d7242bd856a1767a890b537c2f8c10ba8b8a10e6025' + const serviceIdAlgorithm = 'db164c1b981e4d2974e90e61bda121512e6909c1035c908d68933ae4cfaba6b0' + const output = await runCommand(`npm run cli -- startCompute ${computeDatasetDid} ${jsAlgoDid} ${computeEnvId} 900 ${paymentToken} '${JSON.stringify(resources)}' 'null' ${serviceIdDataset} ${serviceIdAlgorithm} --accept true`); const jobIdMatch = output.match(/JobID:\s*([^\s]+)/); const agreementIdMatch = output.match(/Agreement ID:\s*([^\s]+)/); diff --git a/test/setup.test.ts b/test/setup.test.ts index 87024833..c0452c65 100644 --- a/test/setup.test.ts +++ b/test/setup.test.ts @@ -42,7 +42,7 @@ describe("Ocean CLI Setup", function() { expect(stdout).to.contain("Starts a FREE compute job"); expect(stdout).to.contain("stopCompute [options] "); expect(stdout).to.contain("Stops a compute job"); - expect(stdout).to.contain("getJobStatus [options] "); + expect(stdout).to.contain("getJobStatus [options] [agreementId]"); expect(stdout).to.contain("Displays the compute job status"); expect(stdout).to.contain("downloadJobResults [destinationFolder]"); expect(stdout).to.contain("Downloads compute job results");