diff --git a/metadata/simpleDownloadDatasetV5.json b/metadata/simpleDownloadDatasetV5.json new file mode 100644 index 0000000..7c64942 --- /dev/null +++ b/metadata/simpleDownloadDatasetV5.json @@ -0,0 +1,168 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2" + ], + "id": "did:ope", + "version": "5.0.0", + "type": ["VerifiableCredential"], + "additionalDdos": [], + "credentialSubject": { + "id": "did:ope", + "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 614f079..5160837 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@oceanprotocol/contracts": "^2.4.0", "@oceanprotocol/ddo-js": "^0.1.3", "@oceanprotocol/lib": "^5.0.0", + "axios": "^1.11.0", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", @@ -4954,6 +4955,33 @@ "license": "MIT", "peer": true }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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", @@ -8538,6 +8566,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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", @@ -13081,6 +13129,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/package.json b/package.json index 43f2045..07b0717 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@oceanprotocol/contracts": "^2.4.0", "@oceanprotocol/ddo-js": "^0.1.3", "@oceanprotocol/lib": "^5.0.0", + "axios": "^1.11.0", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", @@ -59,4 +60,4 @@ "ts-node": "^10.9.1", "tsx": "^4.19.3" } -} \ No newline at end of file +} diff --git a/src/cli.ts b/src/cli.ts index b00d7b0..0c440c5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -141,11 +141,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 svcId = options.service || serviceId; if (!assetDid) { console.error(chalk.red('DID is required')); // process.exit(1); @@ -153,7 +156,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 @@ -325,7 +328,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 41807b8..208dee5 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -29,11 +29,12 @@ import { EscrowContract, getTokenDecimals } from "@oceanprotocol/lib"; -import { Asset } from '@oceanprotocol/ddo-js'; +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 { getPolicyServerOBJ, getPolicyServerOBJs } from "./policyServerHelper.js"; export class Commands { public signer: Signer; @@ -79,12 +80,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, + services[0].files, asset, this.oceanNodeUrl, this.config, @@ -111,11 +115,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, + services[0].files, algoAsset, this.oceanNodeUrl, this.config, @@ -190,8 +197,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 +207,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 +238,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 +247,12 @@ export class Commands { const urlDownloadUrl = await ProviderInstance.getDownloadUrl( dataDdo.id, - dataDdo.services[0].id, + serviceId, 0, orderTx.hash, this.oceanNodeUrl, - this.signer + this.signer, + policyServer ); try { const path = args[2] ? args[2] : "."; @@ -289,10 +308,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, @@ -322,7 +342,7 @@ 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]; - + console.log('computeEnvs: ', computeEnvs); if (computeEnvID && computeEnvID.length > 1) { for (const index in computeEnvs) { if (computeEnvID == computeEnvs[index].id) { @@ -338,18 +358,32 @@ export class Commands { ); return; } - + const ddoAlgoInstance = DDOManager.getDDOClass(algoDdo); + const { services: servicesAlgo, metadata: metadataAlgo, version: versionAlgo } = ddoAlgoInstance.getDDOFields(); const algo: ComputeAlgorithm = { documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, + serviceId: servicesAlgo[0].id, + meta: metadataAlgo.algorithm, }; + const assetAlgo: { + documentId: string; + serviceId: string; + asset: Asset; + version?: string; + } = { + documentId: algoDdo.id, + serviceId: servicesAlgo[0].id, + asset: algoDdo, + version: versionAlgo + }; const assets = []; for (const dataDdo in ddos) { + const ddoInstanceDdo = DDOManager.getDDOClass(ddos[dataDdo]); + const { services: servicesDdo, version: versionDdo } = ddoInstanceDdo.getDDOFields(); const canStartCompute = isOrderable( ddos[dataDdo], - ddos[dataDdo].services[0].id, + servicesDdo[0].id, algo, algoDdo ); @@ -361,7 +395,9 @@ export class Commands { } assets.push({ documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, + serviceId: servicesDdo[0].id, + asset: ddos[dataDdo], + version: versionDdo }); } const maxJobDuration = Number(args[4]) @@ -443,6 +479,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( @@ -454,7 +491,8 @@ export class Commands { providerURI, this.signer, // V1 was this.signer.getAddress() parsedResources, - Number(chainId) + Number(chainId), + policiesServer ); if ( !providerInitializeComputeJob || @@ -519,8 +557,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], @@ -566,18 +606,34 @@ export class Commands { ); return; } - + const ddoInstanceAlgo = DDOManager.getDDOClass(algoDdo); + const { services: servicesAlgo, metadata: metadataAlgo, version: versionAlgo } = ddoInstanceAlgo.getDDOFields(); const algo: ComputeAlgorithm = { documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, + serviceId: servicesAlgo[0].id, + meta: metadataAlgo.algorithm, + }; + + const assetAlgo: { + documentId: string; + serviceId: string; + asset: Asset; + version?: string; + } = { + documentId: algoDdo.id, + serviceId: servicesAlgo[0].id, + asset: algoDdo, + version: versionAlgo }; const assets = []; for (const dataDdo in ddos) { + const ddoInstanceDdo = DDOManager.getDDOClass(ddos[dataDdo]); + const { services: servicesDdo, version: versionDdo } = ddoInstanceDdo.getDDOFields(); + const canStartCompute = isOrderable( ddos[dataDdo], - ddos[dataDdo].services[0].id, + servicesDdo[0].id, algo, algoDdo ); @@ -589,7 +645,9 @@ export class Commands { } assets.push({ documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, + serviceId: servicesDdo[0].id, + asset: ddos[dataDdo], + version: versionDdo }); } const providerInitializeComputeJob = args[4]; // provider fees + payment @@ -777,6 +835,7 @@ export class Commands { const output: ComputeOutput = { metadataUri: await getMetadataURI(), }; + const policiesServer = await getPolicyServerOBJs(assets, assetAlgo, this.signer, this.oceanNodeUrl); const computeJobs = await ProviderInstance.computeStart( providerURI, @@ -792,6 +851,7 @@ export class Commands { null, // additionalDatasets, only c2d v1 output, + policiesServer ); console.log("compute jobs: ", computeJobs); @@ -851,8 +911,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( @@ -872,7 +934,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." @@ -888,7 +949,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) { @@ -905,18 +965,33 @@ export class Commands { ); return; } - + const ddoInstanceAlgo = DDOManager.getDDOClass(algoDdo); + const { services: servicesAlgo, metadata: metadataAlgo, version: versionAlgo } = ddoInstanceAlgo.getDDOFields(); const algo: ComputeAlgorithm = { documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, + serviceId: servicesAlgo[0].id, + meta: metadataAlgo.algorithm, + }; + + const assetAlgo: { + documentId: string; + serviceId: string; + asset: Asset; + version?: string; + } = { + documentId: algoDdo.id, + serviceId: servicesAlgo[0].id, + asset: algoDdo, + version: versionAlgo }; const assets = []; for (const dataDdo in ddos) { + const ddoInstanceDdo = DDOManager.getDDOClass(ddos[dataDdo]); + const { services: servicesDdo, version: versionDdo } = ddoInstanceDdo.getDDOFields(); const canStartCompute = isOrderable( ddos[dataDdo], - ddos[dataDdo].services[0].id, + servicesDdo[0].id, algo, algoDdo ); @@ -928,7 +1003,9 @@ export class Commands { } assets.push({ documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, + serviceId: servicesDdo[0].id, + asset: ddos[dataDdo], + version: versionDdo }); } @@ -961,6 +1038,7 @@ export class Commands { metadataUri: await getMetadataURI(), }; + const policiesServer = await getPolicyServerOBJs(assets, assetAlgo, this.signer, this.oceanNodeUrl); const computeJobs = await ProviderInstance.freeComputeStart( providerURI, this.signer, @@ -970,7 +1048,8 @@ export class Commands { null, null, null, - output + output, + policiesServer ); console.log("compute jobs: ", computeJobs); @@ -1056,21 +1135,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] + @@ -1091,13 +1173,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) { @@ -1106,14 +1190,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, @@ -1143,13 +1228,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] + @@ -1157,7 +1245,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?" ); @@ -1165,12 +1253,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 ); diff --git a/src/helpers.ts b/src/helpers.ts index 0d7647b..eb8c5f9 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, @@ -160,10 +160,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 ); metadata = await providerResponse; @@ -177,7 +179,7 @@ export async function updateAssetMetadata( } const updateDdoTX = await nft.setMetadata( - updatedDdo.nftAddress, + nftAddress, await owner.getAddress(), 0, oceanNodeUrl, @@ -202,9 +204,9 @@ export async function handleComputeOrder( consumeMarkerFee?: ConsumeMarketFee ) { /* We do have 3 possible situations: - - have validOrder and no providerFees -> then order is valid, providerFees are valid, just use it in startCompute - - have validOrder and providerFees -> then order is valid but providerFees are not valid, we need to call reuseOrder and pay only providerFees - - no validOrder -> we need to call startOrder, to pay 1 DT & providerFees + - have validOrder and no providerFees -> then order is valid, providerFees are valid, just use it in startCompute + - have validOrder and providerFees -> then order is valid but providerFees are not valid, we need to call reuseOrder and pay only providerFees + - no validOrder -> we need to call startOrder, to pay 1 DT & providerFees */ const hasProviderFees = order.providerFee && order.providerFee.providerFeeAmount // no need to approve if it is 0 @@ -257,7 +259,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") { @@ -418,4 +422,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 0000000..5e188d1 --- /dev/null +++ b/src/policyServerHelper.ts @@ -0,0 +1,399 @@ +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 + } + 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') + } + 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 0000000..5d6cdf9 --- /dev/null +++ b/src/policyServerInterfaces.ts @@ -0,0 +1,104 @@ +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 +} + +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/setup.test.ts b/test/setup.test.ts index b1c7dc4..37646ef 100644 --- a/test/setup.test.ts +++ b/test/setup.test.ts @@ -41,7 +41,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");