From 31dd635ee688bcdc20236fa2a10fad1e5de6391f Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Fri, 19 Jun 2026 12:12:02 +0300 Subject: [PATCH 1/5] allow rawAlgo and rawDatasets --- src/cli.ts | 28 ++- src/commands.ts | 364 ++++++++---------------------- src/helpers.ts | 147 ++++++++++++ test/resolveComputeInputs.test.ts | 178 +++++++++++++++ 4 files changed, 441 insertions(+), 276 deletions(-) create mode 100644 test/resolveComputeInputs.test.ts diff --git a/src/cli.ts b/src/cli.ts index c958ee8..87fb502 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -264,9 +264,12 @@ export async function createCLI() { .description("Starts a compute job") .argument( "", - "Dataset DIDs (comma-separated) OR (empty array for none)" + "Dataset DIDs (comma-separated), an empty array for none, OR a JSON ComputeAsset object/array with a fileObject (raw datasets, no DID). Mixed input must be valid JSON, e.g. '[\"did:op:abc\",{\"fileObject\":{...}}]'" + ) + .argument( + "", + "Algorithm DID, OR a JSON ComputeAlgorithm object with a fileObject and meta (raw algorithm, no DID)" ) - .argument("", "Algorithm DID") .argument("", "Compute environment ID") .argument("", "maxJobDuration for compute job") .argument("", "Payment token for compute") @@ -277,9 +280,12 @@ export async function createCLI() { ) .option( "-d, --datasets ", - "Dataset DIDs (comma-separated) OR (empty array for none)" + "Dataset DIDs (comma-separated), an empty array for none, OR a JSON ComputeAsset object/array with a fileObject (raw datasets, no DID)" + ) + .option( + "-a, --algo ", + "Algorithm DID, OR a JSON ComputeAlgorithm object with a fileObject and meta (raw algorithm, no DID)" ) - .option("-a, --algo ", "Algorithm DID") .option("-e, --env ", "Compute environment ID") .option("--maxJobDuration ", "Compute maxJobDuration") .option("-t, --token ", "Compute payment token") @@ -384,9 +390,12 @@ export async function createCLI() { .description("Starts a FREE compute job") .argument( "", - "Dataset DIDs (comma-separated) OR (empty array for none)" + "Dataset DIDs (comma-separated), an empty array for none, OR a JSON ComputeAsset object/array with a fileObject (raw datasets, no DID). Mixed input must be valid JSON, e.g. '[\"did:op:abc\",{\"fileObject\":{...}}]'" + ) + .argument( + "", + "Algorithm DID, OR a JSON ComputeAlgorithm object with a fileObject and meta (raw algorithm, no DID)" ) - .argument("", "Algorithm DID") .argument("", "Compute environment ID") .argument( "[output]", @@ -394,9 +403,12 @@ export async function createCLI() { ) .option( "-d, --datasets ", - "Dataset DIDs (comma-separated) OR (empty array for none)" + "Dataset DIDs (comma-separated), an empty array for none, OR a JSON ComputeAsset object/array with a fileObject (raw datasets, no DID)" + ) + .option( + "-a, --algo ", + "Algorithm DID, OR a JSON ComputeAlgorithm object with a fileObject and meta (raw algorithm, no DID)" ) - .option("-a, --algo ", "Algorithm DID") .option("-e, --env ", "Compute environment ID") .option( "-o, --output [output]", diff --git a/src/commands.ts b/src/commands.ts index 148b4e9..403cc34 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -11,10 +11,11 @@ import { IndexerWaitParams, fixAndParseProviderFees, getConfigByChainId, + resolveComputeInputs, } from "./helpers.js"; import { Aquarius, - ComputeAlgorithm, + ComputeAsset, ComputeJob, Config, ConfigHelper, @@ -30,6 +31,7 @@ import { AccesslistFactory, AccessListContract, } from "@oceanprotocol/lib"; +import { Asset, DDO } from "@oceanprotocol/ddo-js"; import { Signer, ethers, getAddress } from "ethers"; import { interactiveFlow } from "./interactiveFlow.js"; import { publishAsset } from "./publishAsset.js"; @@ -252,67 +254,15 @@ export class Commands { } public async initializeCompute(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(","); - } - } else { - inputDatasets.push(inputDatasetsString); - } - - const ddos = []; - - for (const dataset in inputDatasets) { - const dataDdo = await this.aquarius.waitForIndexer( - inputDatasets[dataset], - null, - null, - this.indexingParams.retryInterval, - this.indexingParams.maxRetries - ); - if (!dataDdo) { - console.error( - "Error fetching DDO " + dataset[1] + ". Does this asset exists?" - ); - return; - } else { - ddos.push(dataDdo); - } - } - if ( - inputDatasets.length > 0 && - (ddos.length <= 0 || ddos.length != inputDatasets.length) - ) { - console.error("Not all the data ddos are available."); - return; - } - let providerURI = this.oceanNodeUrl; - if (ddos.length > 0) { - providerURI = ddos[0].services[0].serviceEndpoint; - } - - const algoDdo = await this.aquarius.waitForIndexer( + const resolved = await resolveComputeInputs( + args[1], args[2], - null, - null, - this.indexingParams.retryInterval, - this.indexingParams.maxRetries + this.aquarius, + this.indexingParams, + this.oceanNodeUrl ); - if (!algoDdo) { - console.error( - "Error fetching DDO " + args[1] + ". Does this asset exists?" - ); - return; - } + if (!resolved) return; + const { assets, algo, ddos, algoDdo, providerURI } = resolved; const computeEnvs = await ProviderInstance.getComputeEnvironments( this.oceanNodeUrl @@ -346,19 +296,16 @@ export class Commands { return; } - const algo: ComputeAlgorithm = { - documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, - }; - - const assets = []; - for (const dataDdo in ddos) { + // Validate orderability only for DID-based datasets (raw fileObject assets + // have no DDO and are validated by the node). + for (let i = 0; i < assets.length; i++) { + const dataDdo = ddos[i]; + if (!dataDdo) continue; const canStartCompute = isOrderable( - ddos[dataDdo], - ddos[dataDdo].services[0].id, + dataDdo, + dataDdo.services[0].id, algo, - algoDdo + algoDdo as Asset | DDO ); if (!canStartCompute) { console.error( @@ -366,10 +313,6 @@ export class Commands { ); return; } - assets.push({ - documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, - }); } const maxJobDuration = Number(args[4]); if (!maxJobDuration) { @@ -481,66 +424,15 @@ export class Commands { } 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(","); - } - } else { - inputDatasets.push(inputDatasetsString); - } - - const ddos = []; - - for (const dataset in inputDatasets) { - const dataDdo = await this.aquarius.waitForIndexer( - inputDatasets[dataset], - null, - null, - this.indexingParams.retryInterval, - this.indexingParams.maxRetries - ); - if (!dataDdo) { - console.error( - "Error fetching DDO " + dataset[1] + ". Does this asset exists?" - ); - return; - } else { - ddos.push(dataDdo); - } - } - if ( - inputDatasets.length > 0 && - (ddos.length <= 0 || ddos.length != inputDatasets.length) - ) { - console.error("Not all the data ddos are available."); - return; - } - let providerURI = this.oceanNodeUrl; - if (ddos.length > 0) { - providerURI = ddos[0].services[0].serviceEndpoint; - } - const algoDdo = await this.aquarius.waitForIndexer( + const resolved = await resolveComputeInputs( + args[1], args[2], - null, - null, - this.indexingParams.retryInterval, - this.indexingParams.maxRetries + this.aquarius, + this.indexingParams, + this.oceanNodeUrl ); - if (!algoDdo) { - console.error( - "Error fetching DDO " + args[1] + ". Does this asset exists?" - ); - return; - } + if (!resolved) return; + const { assets, algo, ddos, algoDdo, providerURI } = resolved; const computeEnvs = await ProviderInstance.getComputeEnvironments( this.oceanNodeUrl @@ -573,19 +465,16 @@ export class Commands { return; } - const algo: ComputeAlgorithm = { - documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, - }; - - const assets = []; - for (const dataDdo in ddos) { + // Validate orderability only for DID-based datasets (raw fileObject assets + // have no DDO and are validated by the node). + for (let i = 0; i < assets.length; i++) { + const dataDdo = ddos[i]; + if (!dataDdo) continue; const canStartCompute = isOrderable( - ddos[dataDdo], - ddos[dataDdo].services[0].id, + dataDdo, + dataDdo.services[0].id, algo, - algoDdo + algoDdo as Asset | DDO ); if (!canStartCompute) { console.error( @@ -593,52 +482,57 @@ export class Commands { ); return; } - assets.push({ - documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, - }); } const providerInitializeComputeJob = args[4]; // provider fees + payment const parsedProviderInitializeComputeJob = fixAndParseProviderFees( providerInitializeComputeJob ); - console.log("Ordering algorithm: ", args[2]); const datatoken = new Datatoken( this.signer, (await this.signer.provider.getNetwork()).chainId.toString(), this.config ); - algo.transferTxId = await handleComputeOrder( - parsedProviderInitializeComputeJob?.algorithm, - algoDdo, - this.signer, - computeEnv.consumerAddress, - 0, - datatoken, - this.config, - parsedProviderInitializeComputeJob?.algorithm?.providerFee, - providerURI - ); - if (!algo.transferTxId) { - console.error( - "Error ordering compute for algorithm with DID: " + - args[2] + - ". Do you have enough tokens?" + // Only order DID-based algorithms; raw (fileObject) algorithms have no datatoken. + if (algoDdo) { + console.log("Ordering algorithm: ", args[2]); + algo.transferTxId = await handleComputeOrder( + parsedProviderInitializeComputeJob?.algorithm, + algoDdo as Asset, + this.signer, + computeEnv.consumerAddress, + 0, + datatoken, + this.config, + parsedProviderInitializeComputeJob?.algorithm?.providerFee, + providerURI ); - return; + if (!algo.transferTxId) { + console.error( + "Error ordering compute for algorithm with DID: " + + args[2] + + ". Do you have enough tokens?" + ); + return; + } } console.log("Ordering assets: ", args[1]); - for (let i = 0; i < ddos.length; i++) { + // Only order DID-based datasets; raw (fileObject) assets keep their index + // slot in `assets`/`ddos` but have no datatoken to order. + for (let i = 0; i < assets.length; i++) { + const dataDdo = ddos[i]; + if (!dataDdo) continue; + const feeEntry = parsedProviderInitializeComputeJob?.datasets?.[i]; + if (!feeEntry) continue; assets[i].transferTxId = await handleComputeOrder( - parsedProviderInitializeComputeJob?.datasets[i], - ddos[i], + feeEntry, + dataDdo as Asset, this.signer, computeEnv.consumerAddress, 0, datatoken, this.config, - parsedProviderInitializeComputeJob?.datasets[i].providerFee, + feeEntry.providerFee, providerURI ); if (!assets[i].transferTxId) { @@ -765,27 +659,25 @@ export class Commands { console.log("Starting compute job using provider: ", providerURI); const additionalDatasets = assets.length > 1 ? assets.slice(1) : null; + const describeAsset = (asset: ComputeAsset) => + asset.documentId || asset.fileObject?.type || "raw asset"; if (assets.length > 0) { console.log( "Starting compute job on " + - assets[0].documentId + + describeAsset(assets[0]) + " with additional datasets:" + - (!additionalDatasets ? "none" : additionalDatasets[0].documentId) + (!additionalDatasets ? "none" : describeAsset(additionalDatasets[0])) ); } 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" + (!additionalDatasets ? "none" : describeAsset(additionalDatasets[0])) ); - assets.push(additionalDatasets); } + // All datasets (primary + additional) are already in `assets` per C2D V2 specs; + // they are passed together below. (C2D V1 took additionalDatasets separately.) let output = null; if (args[8]) { @@ -825,68 +717,15 @@ export class Commands { } public async freeComputeStart(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(","); - } - } else { - inputDatasets.push(inputDatasetsString); - } - - const ddos = []; - - for (const dataset in inputDatasets) { - const dataDdo = await this.aquarius.waitForIndexer( - inputDatasets[dataset], - null, - null, - this.indexingParams.retryInterval, - this.indexingParams.maxRetries - ); - if (!dataDdo) { - console.error( - "Error fetching DDO " + dataset[1] + ". Does this asset exists?" - ); - return; - } else { - ddos.push(dataDdo); - } - } - - if ( - inputDatasets.length > 0 && - (ddos.length <= 0 || ddos.length != inputDatasets.length) - ) { - console.error("Not all the data ddos are available."); - return; - } - let providerURI = this.oceanNodeUrl; - if (ddos.length > 0) { - providerURI = ddos[0].services[0].serviceEndpoint; - } - - const algoDdo = await this.aquarius.waitForIndexer( + const resolved = await resolveComputeInputs( + args[1], args[2], - null, - null, - this.indexingParams.retryInterval, - this.indexingParams.maxRetries + this.aquarius, + this.indexingParams, + this.oceanNodeUrl ); - if (!algoDdo) { - console.error( - "Error fetching DDO " + args[1] + ". Does this asset exists?" - ); - return; - } + if (!resolved) return; + const { assets, algo, ddos, algoDdo, providerURI } = resolved; const computeEnvs = await ProviderInstance.getComputeEnvironments( this.oceanNodeUrl @@ -899,10 +738,6 @@ export class Commands { return; } - const mytime = new Date(); - const computeMinutes = 5; - mytime.setMinutes(mytime.getMinutes() + computeMinutes); - const computeEnvID = args[3]; // NO chainId needed anymore (is not part of ComputeEnvironment spec anymore) // const chainComputeEnvs = computeEnvs[computeEnvID]; // was algoDdo.chainId @@ -925,19 +760,16 @@ export class Commands { return; } - const algo: ComputeAlgorithm = { - documentId: algoDdo.id, - serviceId: algoDdo.services[0].id, - meta: algoDdo.metadata.algorithm, - }; - - const assets = []; - for (const dataDdo in ddos) { + // Validate orderability only for DID-based datasets (raw fileObject assets + // have no DDO and are validated by the node). + for (let i = 0; i < assets.length; i++) { + const dataDdo = ddos[i]; + if (!dataDdo) continue; const canStartCompute = isOrderable( - ddos[dataDdo], - ddos[dataDdo].services[0].id, + dataDdo, + dataDdo.services[0].id, algo, - algoDdo + algoDdo as Asset | DDO ); if (!canStartCompute) { console.error( @@ -945,36 +777,29 @@ export class Commands { ); return; } - assets.push({ - documentId: ddos[dataDdo].id, - serviceId: ddos[dataDdo].services[0].id, - }); } console.log("Starting compute job using provider: ", providerURI); const additionalDatasets = assets.length > 1 ? assets.slice(1) : null; + const describeAsset = (asset: ComputeAsset) => + asset.documentId || asset.fileObject?.type || "raw asset"; if (assets.length > 0) { console.log( "Starting compute job on " + - assets[0].documentId + + describeAsset(assets[0]) + " with additional datasets:" + - (!additionalDatasets ? "none" : additionalDatasets[0].documentId) + (!additionalDatasets ? "none" : describeAsset(additionalDatasets[0])) ); } 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" + (!additionalDatasets ? "none" : describeAsset(additionalDatasets[0])) ); - assets.push(additionalDatasets); } + // All datasets (primary + additional) are already in `assets` per C2D V2 specs; + // they are passed together below. (C2D V1 took additionalDatasets separately.) let output = null; if (args[4]) { @@ -1827,6 +1652,9 @@ export class Commands { const bucketId = args[1]; const filePath = args[2]; const fileName = args[3] || (filePath ? path.basename(filePath) : undefined); + console.log("Bucket ID:", bucketId); + console.log("File Path:", filePath); + console.log("File Name:", fileName); if (!bucketId || !filePath) { console.error(chalk.red("bucketId and filePath are required")); return; diff --git a/src/helpers.ts b/src/helpers.ts index afe59d6..78a171d 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -20,6 +20,7 @@ import { DownloadResponse, ProviderFees, ComputeAlgorithm, + ComputeAsset, LoggerInstance, createAsset } from "@oceanprotocol/lib"; @@ -282,6 +283,152 @@ export async function isOrderable( return true; } +export interface ResolvedComputeInputs { + assets: ComputeAsset[]; // index-aligned with `ddos` + algo: ComputeAlgorithm; + ddos: (Asset | DDO | null)[]; // null where the asset is raw (fileObject) + algoDdo: Asset | DDO | null; // null when the algo is raw + providerURI: string; // first DID-based DDO endpoint, else fallback +} + +// Parses a single compute input string (datasets or algo) into a list of tokens. +// Each token is either a DID string or a raw ComputeAsset/ComputeAlgorithm object. +// Supports: a bare DID, a JSON object, a JSON array of DIDs and/or objects, and the +// legacy unquoted `[did:a,did:b]` form (kept for backward compatibility). +function parseComputeInput(raw: string): (string | Record)[] { + if (raw === undefined || raw === null) return []; + const trimmed = String(raw).trim(); + if (trimmed.length === 0) return []; + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) return parsed; + if (parsed && typeof parsed === "object") return [parsed]; + // a JSON primitive (e.g. a quoted string) -> treat as a single DID + return [String(parsed)]; + } catch { + // not valid JSON: bare DID or legacy `[did:a,did:b]` + if (trimmed.includes("[") && trimmed.includes("]")) { + const processed = trimmed.replaceAll("]", "").replaceAll("[", ""); + if (processed.indexOf(",") > -1) { + return processed + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); + } + // `[did]` or `[]` + const single = processed.trim(); + return single.length > 0 ? [single] : []; + } + return [trimmed]; + } +} + +// Resolves the raw datasets/algo CLI strings into fully-built ComputeAsset[] and +// ComputeAlgorithm, plus index-aligned DDO arrays (null where the entry is raw). +// DID entries are resolved via Aquarius; raw (fileObject) entries are passed through +// untouched. Returns null (after logging) when a DID cannot be resolved. +export async function resolveComputeInputs( + datasetsInput: string, + algoInput: string, + aquarius: Aquarius, + indexingParams: IndexerWaitParams, + fallbackProviderURI: string +): Promise { + const datasetTokens = parseComputeInput(datasetsInput); + + const assets: ComputeAsset[] = []; + const ddos: (Asset | DDO | null)[] = []; + + for (const token of datasetTokens) { + if (typeof token === "string") { + const dataDdo = await aquarius.waitForIndexer( + token, + undefined, + undefined, + indexingParams.retryInterval, + indexingParams.maxRetries + ); + if (!dataDdo) { + console.error( + "Error fetching DDO " + token + ". Does this asset exists?" + ); + return null; + } + assets.push({ + documentId: dataDdo.id, + serviceId: dataDdo.services[0].id, + }); + ddos.push(dataDdo); + } else if (token && typeof token === "object") { + const rawAsset = token as unknown as ComputeAsset; + if (!rawAsset.fileObject) { + console.error( + "Error: raw dataset asset must contain a 'fileObject'. Got: " + + JSON.stringify(token) + ); + return null; + } + assets.push({ + ...rawAsset, + documentId: rawAsset.documentId ?? "", + serviceId: rawAsset.serviceId ?? "", + }); + ddos.push(null); + } else { + console.error("Error: invalid dataset input entry: " + String(token)); + return null; + } + } + + let providerURI = fallbackProviderURI; + const firstDdo = ddos.find((d) => d !== null); + if (firstDdo) { + providerURI = firstDdo.services[0].serviceEndpoint; + } + + // Resolve the algorithm (single entry: DID or raw object) + const algoTokens = parseComputeInput(algoInput); + const algoToken = algoTokens.length > 0 ? algoTokens[0] : algoInput; + let algo: ComputeAlgorithm; + let algoDdo: Asset | DDO | null = null; + + if (typeof algoToken === "string") { + algoDdo = await aquarius.waitForIndexer( + algoToken, + undefined, + undefined, + indexingParams.retryInterval, + indexingParams.maxRetries + ); + if (!algoDdo) { + console.error( + "Error fetching DDO " + algoToken + ". Does this asset exists?" + ); + return null; + } + algo = { + documentId: algoDdo.id, + serviceId: algoDdo.services[0].id, + meta: algoDdo.metadata.algorithm, + }; + } else if (algoToken && typeof algoToken === "object") { + const rawAlgo = algoToken as unknown as ComputeAlgorithm; + if (!rawAlgo.fileObject) { + console.error( + "Error: raw algorithm must contain a 'fileObject'. Got: " + + JSON.stringify(algoToken) + ); + return null; + } + algo = rawAlgo; + } else { + console.error("Error: invalid algorithm input: " + String(algoToken)); + return null; + } + + return { assets, algo, ddos, algoDdo, providerURI }; +} + // The ranges and the amount of usable IP's: diff --git a/test/resolveComputeInputs.test.ts b/test/resolveComputeInputs.test.ts new file mode 100644 index 0000000..3f2ab7d --- /dev/null +++ b/test/resolveComputeInputs.test.ts @@ -0,0 +1,178 @@ +import { expect } from "chai"; +import { Aquarius } from "@oceanprotocol/lib"; +import { + resolveComputeInputs, + IndexerWaitParams, +} from "../src/helpers.js"; + +// A fake Aquarius whose waitForIndexer returns a deterministic DDO per DID, +// so the resolver can be exercised without a live indexer. +function makeAquarius(known?: Set): Aquarius { + return { + waitForIndexer: async (did: string) => { + if (known && !known.has(did)) return null; + return { + id: did, + services: [ + { + id: `service-${did}`, + serviceEndpoint: `http://provider-for-${did}`, + }, + ], + metadata: { algorithm: { container: { image: "img" } } }, + }; + }, + } as unknown as Aquarius; +} + +const indexingParams: IndexerWaitParams = { + maxRetries: 1, + retryInterval: 1, +}; +const FALLBACK = "http://node-fallback"; + +const RAW_DATASET = { + fileObject: { type: "url", url: "https://example.com/data.csv", method: "GET" }, +}; +const RAW_ALGO = { + fileObject: { type: "url", url: "https://example.com/algo.py", method: "GET" }, + meta: { container: { image: "oceanprotocol/algo_dockers" } }, +}; + +describe("resolveComputeInputs", function () { + it("resolves a single dataset DID and a single algorithm DID", async function () { + const res = await resolveComputeInputs( + "did:op:dataset1", + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res).to.not.equal(null); + expect(res.assets).to.have.length(1); + expect(res.assets[0].documentId).to.equal("did:op:dataset1"); + expect(res.ddos[0]).to.not.equal(null); + expect(res.algoDdo).to.not.equal(null); + expect(res.algo.documentId).to.equal("did:op:algo1"); + expect(res.algo.meta).to.not.equal(undefined); + expect(res.providerURI).to.equal("http://provider-for-did:op:dataset1"); + }); + + it("supports the legacy unquoted [did:a,did:b] datasets form", async function () { + const res = await resolveComputeInputs( + "[did:op:a,did:op:b]", + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res.assets).to.have.length(2); + expect(res.assets.map((a) => a.documentId)).to.deep.equal([ + "did:op:a", + "did:op:b", + ]); + expect(res.ddos.every((d) => d !== null)).to.equal(true); + }); + + it("treats a JSON object datasets arg as a raw asset (no DDO, fallback provider)", async function () { + const res = await resolveComputeInputs( + JSON.stringify(RAW_DATASET), + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res.assets).to.have.length(1); + expect(res.assets[0].fileObject).to.not.equal(undefined); + expect(res.assets[0].documentId).to.equal(""); + expect(res.ddos[0]).to.equal(null); + expect(res.providerURI).to.equal(FALLBACK); + }); + + it("handles a mixed JSON array of a DID and a raw asset, keeping index alignment", async function () { + const res = await resolveComputeInputs( + JSON.stringify(["did:op:dataset1", RAW_DATASET]), + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res.assets).to.have.length(2); + expect(res.ddos[0]).to.not.equal(null); + expect(res.ddos[1]).to.equal(null); + expect(res.assets[1].fileObject).to.not.equal(undefined); + // providerURI derives from the first DID-based DDO + expect(res.providerURI).to.equal("http://provider-for-did:op:dataset1"); + }); + + it("picks the provider from the first DID even when a raw asset comes first", async function () { + const res = await resolveComputeInputs( + JSON.stringify([RAW_DATASET, "did:op:dataset1"]), + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res.ddos[0]).to.equal(null); + expect(res.ddos[1]).to.not.equal(null); + expect(res.providerURI).to.equal("http://provider-for-did:op:dataset1"); + }); + + it("treats a JSON object algorithm arg as a raw algorithm (algoDdo null)", async function () { + const res = await resolveComputeInputs( + "did:op:dataset1", + JSON.stringify(RAW_ALGO), + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res.algoDdo).to.equal(null); + expect(res.algo.fileObject).to.not.equal(undefined); + expect(res.algo.meta).to.not.equal(undefined); + }); + + it("returns an empty asset list for an empty array, using fallback provider", async function () { + const res = await resolveComputeInputs( + "[]", + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res.assets).to.have.length(0); + expect(res.providerURI).to.equal(FALLBACK); + }); + + it("fails (returns null) when a raw dataset object lacks a fileObject", async function () { + const res = await resolveComputeInputs( + JSON.stringify({ documentId: "x" }), + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res).to.equal(null); + }); + + it("fails (returns null) when a raw algorithm object lacks a fileObject", async function () { + const res = await resolveComputeInputs( + "did:op:dataset1", + JSON.stringify({ meta: { container: {} } }), + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res).to.equal(null); + }); + + it("fails (returns null) when a dataset DID cannot be resolved", async function () { + const res = await resolveComputeInputs( + "did:op:missing", + "did:op:algo1", + makeAquarius(new Set(["did:op:algo1"])), + indexingParams, + FALLBACK + ); + expect(res).to.equal(null); + }); +}); From 07d176d060895f16d48e3bd73263a3ce48462c74 Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Fri, 19 Jun 2026 12:12:18 +0300 Subject: [PATCH 2/5] update README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 4b91849..dc1f3f0 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,17 @@ e.g.: `'[{"id":"cpu","amount":3},{"id":"ram","amount":16772672536},{"id":"disk", - `--accept` option can be set to `true` or `false`. If it is set to `false` a prompt will be displayed to the user for manual accepting the payment before starting a compute job. If it is set to `true`, the compute job starts automatically, without user input. - `output` is an optional stringified JSON object specifying a remote storage backend where job results will be uploaded. Supported types include S3, FTP, URL, Arweave, and IPFS. If omitted, results are stored on the node's local disk. e.g.: `'{"remoteStorage":{"type":"s3","s3Access":{"endpoint":"https://s3.amazonaws.com","region":"us-east-1","bucket":"my-results","objectKey":"jobs/result.tar","accessKeyId":"AKIA...","secretAccessKey":"..."}}}'` +**Raw (unpublished) datasets and algorithms:** + +Instead of a DID, you can pass a full `ComputeAsset` (datasets) or `ComputeAlgorithm` (algorithm) JSON object with a `fileObject`, to run compute on raw data/algorithms that are not published as assets. Raw entries have no DID and are not ordered (no datatoken). DID-based and raw entries can be mixed within the datasets argument (the value must then be valid JSON, with DIDs quoted). JSON must be single-quoted on the shell. + +- Raw algorithm against a published dataset that allows raw algorithms (`allowRawAlgorithm: true`): + `npm run cli startCompute -- did:op:dataset '{"fileObject":{"type":"url","url":"https://example.com/algo.py","method":"GET"},"meta":{"container":{"entrypoint":"python $ALGO","image":"oceanprotocol/algo_dockers","tag":"python-branin","checksum":"sha256:..."}}}' env1 900 paymentToken resources --accept true` +- Raw dataset(s) against a published algorithm: + `npm run cli startCompute -- '[{"fileObject":{"type":"url","url":"https://example.com/data.csv","method":"GET"}}]' did:op:algo env1 900 paymentToken resources --accept true` +- Mixed datasets (a published DID and a raw file): + `npm run cli startCompute -- '["did:op:dataset",{"fileObject":{"type":"ipfs","hash":"Qm..."}}]' did:op:algo env1 900 paymentToken resources --accept true` + --- **Start Free Compute:** @@ -223,6 +234,8 @@ e.g.: `'[{"id":"cpu","amount":3},{"id":"ram","amount":16772672536},{"id":"disk", (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`. +- Like `startCompute`, the datasets and algorithm arguments accept raw `ComputeAsset`/`ComputeAlgorithm` JSON objects with a `fileObject` (no DID), and mixed DID + raw datasets. e.g.: + `npm run cli startFreeCompute did:op:dataset '{"fileObject":{"type":"url","url":"https://example.com/algo.py","method":"GET"},"meta":{"container":{"entrypoint":"python $ALGO","image":"oceanprotocol/algo_dockers","tag":"python-branin","checksum":"sha256:..."}}}' env1` --- From 930aa6acfcb9676c8ba653ac5c0ac5a0c9fd1d29 Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Fri, 19 Jun 2026 12:17:49 +0300 Subject: [PATCH 3/5] remove debug lines --- src/commands.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 403cc34..6a016cc 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1652,9 +1652,6 @@ export class Commands { const bucketId = args[1]; const filePath = args[2]; const fileName = args[3] || (filePath ? path.basename(filePath) : undefined); - console.log("Bucket ID:", bucketId); - console.log("File Path:", filePath); - console.log("File Name:", fileName); if (!bucketId || !filePath) { console.error(chalk.red("bucketId and filePath are required")); return; From d9dcca2e84753a5dc0f0970669c3211da015d462 Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Fri, 19 Jun 2026 12:23:34 +0300 Subject: [PATCH 4/5] fix review --- src/commands.ts | 6 +++--- src/helpers.ts | 7 +++++++ test/resolveComputeInputs.test.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 6a016cc..09ec0f4 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -301,7 +301,7 @@ export class Commands { for (let i = 0; i < assets.length; i++) { const dataDdo = ddos[i]; if (!dataDdo) continue; - const canStartCompute = isOrderable( + const canStartCompute = await isOrderable( dataDdo, dataDdo.services[0].id, algo, @@ -470,7 +470,7 @@ export class Commands { for (let i = 0; i < assets.length; i++) { const dataDdo = ddos[i]; if (!dataDdo) continue; - const canStartCompute = isOrderable( + const canStartCompute = await isOrderable( dataDdo, dataDdo.services[0].id, algo, @@ -765,7 +765,7 @@ export class Commands { for (let i = 0; i < assets.length; i++) { const dataDdo = ddos[i]; if (!dataDdo) continue; - const canStartCompute = isOrderable( + const canStartCompute = await isOrderable( dataDdo, dataDdo.services[0].id, algo, diff --git a/src/helpers.ts b/src/helpers.ts index 78a171d..ff1c382 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -319,6 +319,13 @@ function parseComputeInput(raw: string): (string | Record)[] { const single = processed.trim(); return single.length > 0 ? [single] : []; } + // legacy unbracketed comma-separated DIDs: `did:op:a,did:op:b` + if (trimmed.indexOf(",") > -1) { + return trimmed + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); + } return [trimmed]; } } diff --git a/test/resolveComputeInputs.test.ts b/test/resolveComputeInputs.test.ts index 3f2ab7d..4f1d8a7 100644 --- a/test/resolveComputeInputs.test.ts +++ b/test/resolveComputeInputs.test.ts @@ -74,6 +74,22 @@ describe("resolveComputeInputs", function () { expect(res.ddos.every((d) => d !== null)).to.equal(true); }); + it("supports the legacy unbracketed comma-separated DIDs form", async function () { + const res = await resolveComputeInputs( + "did:op:a,did:op:b", + "did:op:algo1", + makeAquarius(), + indexingParams, + FALLBACK + ); + expect(res.assets).to.have.length(2); + expect(res.assets.map((a) => a.documentId)).to.deep.equal([ + "did:op:a", + "did:op:b", + ]); + expect(res.ddos.every((d) => d !== null)).to.equal(true); + }); + it("treats a JSON object datasets arg as a raw asset (no DDO, fallback provider)", async function () { const res = await resolveComputeInputs( JSON.stringify(RAW_DATASET), From 5366520541a7434f91bb40b684f447d0a2314199 Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Fri, 19 Jun 2026 13:22:52 +0300 Subject: [PATCH 5/5] remove isOrderable, as checks are made on node --- src/commands.ts | 62 +++---------------------------------------------- src/helpers.ts | 32 ------------------------- 2 files changed, 3 insertions(+), 91 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 09ec0f4..e8083b1 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -6,7 +6,6 @@ import { handleComputeOrder, updateAssetMetadata, downloadFile, - isOrderable, getIndexingWaitSettings, IndexerWaitParams, fixAndParseProviderFees, @@ -31,7 +30,7 @@ import { AccesslistFactory, AccessListContract, } from "@oceanprotocol/lib"; -import { Asset, DDO } from "@oceanprotocol/ddo-js"; +import { Asset } from "@oceanprotocol/ddo-js"; import { Signer, ethers, getAddress } from "ethers"; import { interactiveFlow } from "./interactiveFlow.js"; import { publishAsset } from "./publishAsset.js"; @@ -262,7 +261,7 @@ export class Commands { this.oceanNodeUrl ); if (!resolved) return; - const { assets, algo, ddos, algoDdo, providerURI } = resolved; + const { assets, algo, providerURI } = resolved; const computeEnvs = await ProviderInstance.getComputeEnvironments( this.oceanNodeUrl @@ -296,24 +295,6 @@ export class Commands { return; } - // Validate orderability only for DID-based datasets (raw fileObject assets - // have no DDO and are validated by the node). - for (let i = 0; i < assets.length; i++) { - const dataDdo = ddos[i]; - if (!dataDdo) continue; - const canStartCompute = await isOrderable( - dataDdo, - dataDdo.services[0].id, - algo, - algoDdo as Asset | DDO - ); - if (!canStartCompute) { - console.error( - "Error Cannot start compute job using the datasets DIDs & algorithm DID provided" - ); - return; - } - } const maxJobDuration = Number(args[4]); if (!maxJobDuration) { console.error( @@ -465,24 +446,6 @@ export class Commands { return; } - // Validate orderability only for DID-based datasets (raw fileObject assets - // have no DDO and are validated by the node). - for (let i = 0; i < assets.length; i++) { - const dataDdo = ddos[i]; - if (!dataDdo) continue; - const canStartCompute = await isOrderable( - dataDdo, - dataDdo.services[0].id, - algo, - algoDdo as Asset | DDO - ); - if (!canStartCompute) { - console.error( - "Error Cannot start compute job using the datasets DIDs & algorithm DID provided" - ); - return; - } - } const providerInitializeComputeJob = args[4]; // provider fees + payment const parsedProviderInitializeComputeJob = fixAndParseProviderFees( providerInitializeComputeJob @@ -725,7 +688,7 @@ export class Commands { this.oceanNodeUrl ); if (!resolved) return; - const { assets, algo, ddos, algoDdo, providerURI } = resolved; + const { assets, algo, providerURI } = resolved; const computeEnvs = await ProviderInstance.getComputeEnvironments( this.oceanNodeUrl @@ -760,25 +723,6 @@ export class Commands { return; } - // Validate orderability only for DID-based datasets (raw fileObject assets - // have no DDO and are validated by the node). - for (let i = 0; i < assets.length; i++) { - const dataDdo = ddos[i]; - if (!dataDdo) continue; - const canStartCompute = await isOrderable( - dataDdo, - dataDdo.services[0].id, - algo, - algoDdo as Asset | DDO - ); - if (!canStartCompute) { - console.error( - "Error Cannot start compute job using the datasets DIDs & algorithm DID provided" - ); - return; - } - } - console.log("Starting compute job using provider: ", providerURI); const additionalDatasets = assets.length > 1 ? assets.slice(1) : null; const describeAsset = (asset: ComputeAsset) => diff --git a/src/helpers.ts b/src/helpers.ts index ff1c382..63e49af 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -21,7 +21,6 @@ import { ProviderFees, ComputeAlgorithm, ComputeAsset, - LoggerInstance, createAsset } from "@oceanprotocol/lib"; import { homedir } from "os"; @@ -252,37 +251,6 @@ export async function handleComputeOrder( return orderStartedTx.transactionHash; } -export async function isOrderable( - asset: Asset | DDO, - serviceId: string, - algorithm: ComputeAlgorithm, - algorithmDDO: Asset | DDO -): Promise { - const datasetService = asset.services.find((s) => s.id === serviceId); - if (!datasetService) return false; - - if (datasetService.type === "compute") { - if (algorithm.meta) { - if (datasetService.compute.allowRawAlgorithm) return true; - return false; - } - if (algorithm.documentId) { - const algoService = algorithmDDO.services.find( - (s) => s.id === algorithm.serviceId - ); - if (algoService && algoService.type === "compute") { - if (algoService.serviceEndpoint !== datasetService.serviceEndpoint) { - LoggerInstance.error( - "ERROR: Both assets with compute service are not served by the same provider" - ); - return false; - } - } - } - } - return true; -} - export interface ResolvedComputeInputs { assets: ComputeAsset[]; // index-aligned with `ddos` algo: ComputeAlgorithm;