diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbe6d54..c49f004 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,6 +141,9 @@ jobs: working-directory: ${{ github.workspace }}/barge run: | bash -x start_ocean.sh --with-typesense 2>&1 > start_ocean.log & + env: + NODE_VERSION: pr-1479 + CONTRACTS_VERSION: 3.2.0-rc.0 - run: npm ci - run: npm run build - run: docker image ls diff --git a/CLAUDE.md b/CLAUDE.md index 92b3eb2..d830cef 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,8 +78,9 @@ All registered in `src/cli.ts` via Commander (`commander` v13). Every command su - Assets: `getDDO`, `publish`, `publishAlgo`, `editAsset` (alias `edit`), `download`, `allowAlgo`. - Compute: `startCompute`, `startFreeCompute`, `getComputeEnvironments` (alias `getC2DEnvs`), `computeStreamableLogs`, `stopCompute`, `getJobStatus`, `downloadJobResults`. - Tokens/auth: `mintOcean`, `generateAuthToken`, `invalidateAuthToken`. -- Escrow: `depositEscrow`, `getUserFundsEscrow`, `withdrawFromEscrow`, `authorizeEscrow`, `getAuthorizationsEscrow`. +- Escrow: `depositEscrow`, `getUserFundsEscrow`, `withdrawFromEscrow`, `authorizeEscrow`, `getAuthorizationsEscrow`, `getEscrowInfo` (alias `escrowInfo`). - Access lists: `createAccessList`, `addToAccessList`, `checkAccessList`, `removeFromAccessList`. +- Subsidy providers: `getSubsidyProviders` (alias `subsidyProviders`), `getSubsidyStatus` (alias `subsidyStatus`). Provider addresses come **only** from the node's status (`nodeSubsidyInfo` in `nodeConnection.ts`), never from bundled config — a node may not support subsidies. `getSubsidyStatus` uses the ocean.js `SubsidyView`/`OPFSubsidyProvider`/`OneTimeSubsidyProvider` wrappers. Compute/service commands (`startCompute`, `startService`, `extendService`) also take `--subsidyProviders ` (tri-state: omit → node default, `none`/empty → `[]`, CSV → those; parsed by `parseSubsidyProviders` in `helpers.ts`). `startFreeCompute` ignores it. - Persistent storage buckets: `createBucket`, `addFileToBucket`, `listBuckets`, `listFilesInBucket`, `getFileObject`, `deleteFile`. - Admin: `downloadNodeLogs`. - Node selection: `setNode` (alias `useNode`), `getNode` (alias `currentNode`). @@ -184,7 +185,7 @@ The `startCompute` **action in `cli.ts`** orchestrates a two-phase flow (not a s ### Escrow, access lists, persistent storage, auth, node logs -- **Escrow** (`EscrowContract` from ocean.js, address from `address.json`): `depositEscrow` approves then deposits; `withdrawFromEscrow` checks balance first; `authorizeEscrow` sets per-payee lock limits; `getUserFundsEscrow` / `getAuthorizationsEscrow` read state. +- **Escrow** (`EscrowContract` from ocean.js, address from `ConfigHelper` via `requireAddress(chainId, "escrow", …)`): `depositEscrow` approves then deposits; `withdrawFromEscrow` checks balance first; `authorizeEscrow` sets per-payee lock limits; `getUserFundsEscrow` / `getAuthorizationsEscrow` read state. **Escrow v2** (ocean.js escrow-v2 release / contracts `^3.2.0-rc.0`): `authorize` now **always overwrites** the on-chain record (it no longer no-ops when one exists — that is what makes renew/shorten/revoke reachable) and takes a trailing `expiryTimestamp` (unix seconds; `0` = indefinite, a past value revokes), surfaced as `authorizeEscrow --expiry`; `authorizeEscrowPayee` therefore dropped its old "already authorized → null tx" branch. `getAuthorizationsEscrow` prints the new 7th tuple field `expiryTimestamp`, and reads `Current Locked Amount` as **payer-funded only** (`P`); provider-sponsored tokens (`S`) live in a separate bucket. `getEscrowInfo [token]` is a read-only diagnostic: it reads `version`/`escrowKind`/`maxSponsorsPerLock` directly and feature-detects `isEscrowLockSubsidy`/`isEscrowEnterprise` via ERC-165 (`supportsInterface`), plus `getSponsoredTotal`/`getReclaimable` and the enterprise `feeCollector`/`isTokenAllowed` when a token is given; it degrades gracefully against a legacy escrow. The CLI never creates locks itself (the node does at compute/service start), so `createLock`/`reLock`'s new `jobType`/`subsidyProviders` params are only relevant through the node; `--subsidyProviders` on `startCompute`/`startService`/`extendService` is what feeds pre-funded sponsorship. - **Access lists** (`AccesslistFactory` / `AccessListContract` from ocean.js): `createAccessList` deploys via the factory (address from `address.json`); membership is an ERC721 balance — `addToAccessList` mints/`batchMint`s NFTs, `checkAccessList` tests `balance > 0`, `removeFromAccessList` burns each token via `tokenOfOwnerByIndex`. - **Persistent storage** (`ProviderInstance.*PersistentStorage*`): bucket CRUD + file upload/list/get/delete. `addFileToBucket` streams the file with a progress logger and a 30-minute (`UPLOAD_TIMEOUT_MS`) abort timeout; buckets can be gated by an access-list address. - **Auth tokens**: `generateAuthToken` / `invalidateAuthToken` via `ProviderInstance`. diff --git a/README.md b/README.md index 1b6cb1d..d8c4778 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,17 @@ Notes: --- +> **Escrow v2.** The escrow contracts now support **lock-time pre-funded sponsorship** (a subsidy +> provider can back part or all of a lock when it is created, so a fully-sponsored user can transact +> with no deposit) and **authorization expiry** (a payee key can be time-boxed or revoked). The CLI +> reflects this: `authorizeEscrow` takes an `--expiry` and re-running it overwrites the record +> (renew/shorten/revoke), `getAuthorizationsEscrow` prints the expiry, and `getEscrowInfo` reports +> the escrow's capabilities and the sponsored bucket. Reading escrow state, note that +> `getUserFunds().locked` and an authorization's `Current Locked Amount` now count only the payer's +> **own** locked funds — provider-sponsored tokens live in a separate sponsored bucket +> (`getEscrowInfo `). These commands require the Escrow v2 deployment; they still work (and +> degrade gracefully) against a legacy escrow. See the full change set in `PR_description.md`. + **Deposit to Escrow:** - **Positional:** @@ -640,6 +651,20 @@ Notes: - `maxLockedAmount`: Maximum amount that can be locked by payee - `maxLockSeconds`: Maximum lock duration in seconds - `maxLockCounts`: Maximum number of locks allowed +- Options: + - `-e, --expiry `: **(Escrow v2)** unix timestamp (seconds) after which the payee + can no longer create/extend locks. `0` = indefinite (default). Re-running `authorizeEscrow` + **overwrites** the existing authorization (renew/shorten); passing an `--expiry` in the past + **revokes** it. Claim and cancel are never gated by expiry, so existing locks stay claimable. + + Examples — revoke a payee, then re-grant for 1 hour: + + ```bash + # revoke now (any past timestamp works) + npm run cli authorizeEscrow 0x 0x 1000 3600 10 --expiry 1 + # re-authorize, expiring 1h from now + npm run cli authorizeEscrow 0x 0x 1000 3600 10 --expiry $(( $(date +%s) + 3600 )) + ``` --- @@ -651,6 +676,29 @@ Notes: - **Named Options:** `npm run cli getAuthorizationsEscrow --token 0x1234...tokenAddress --payee 0x5678...payeeAddress` +- Prints the current/max lock limits, current locks, and **(Escrow v2)** the authorization's + `expiryTimestamp` (`indefinite (0)`, or the ISO date, flagged `EXPIRED/revoked` when in the past). + Note that `Current Locked Amount` now reflects only the **payer-funded** portion of live locks — + any provider-sponsored amount lives in the separate sponsored bucket (see `getEscrowInfo`). + +--- + +**Get Escrow Info (Escrow v2):** + +- **Positional:** + `npm run cli getEscrowInfo` or `npm run cli getEscrowInfo 0x1234...tokenAddress` + +- **Named Options:** + `npm run cli getEscrowInfo --token 0x1234...tokenAddress --chainId 8453` + +- Reads the escrow deployment on the active (or `--chainId`) chain via ERC-165 capability + discovery and prints: the on-chain `version`, the `escrowKind` (`COMMUNITY` vs `ENTERPRISE`), + whether lock-time sponsorship (`IEscrowLockSubsidy`) and the enterprise fee gate + (`IEscrowEnterprise`) are supported, and `maxSponsorsPerLock`. When a `token` is supplied it also + prints the sponsored-bucket total and your reclaimable amount (lock-subsidy escrows), and the fee + collector + whether the token is allowed (enterprise escrows). Works against a legacy (pre-v2) + escrow too — it simply reports it as legacy. Alias: `escrowInfo`. + --- **Create Access List:** @@ -699,6 +747,48 @@ Notes: --- +### Subsidy Providers + +Subsidy providers are on-chain contracts that sponsor part or all of a job's cost (a subsidy released +back to the payer, plus an optional bonus paid to the node), subject to allow-lists, job-type +restrictions and per-period caps. **The Ocean Node is the source of truth for which subsidy providers +are usable** — a node may not support them at all — so the CLI reads provider addresses from the node's +status, never from bundled contract addresses. + +**See which subsidy providers a node supports:** + +- `npm run cli getSubsidyProviders` + + Prints the per-chain subsidy-provider contracts the current node advertises, and whether the node + enforces a whitelist on user-supplied providers (`subsidyProviderFilter`). The same info is also shown + by `getNode`. + +**Check your subsidy limits / remaining credit for a token:** + +- **Positional:** + `npm run cli getSubsidyStatus 0x1234...tokenAddress` + +- **Named Options:** + `npm run cli getSubsidyStatus --token 0x1234...tokenAddress --chainId 8453 --node 0xNodeAddr --jobType compute --amount 10` + + Discovers the provider contract(s) from the node, then for each prints its kind, per-window buckets + (limit / used / remaining / reset), the amount claimable now, the contract's available balance and your + eligibility. `--subsidy 0xAddr[,0xAddr2]` narrows to specific node-advertised contract(s); + `--node`/`--jobType`/`--amount` together also print a subsidy quote (`{subsidy, bonus}`). + +**Choose subsidy providers when starting compute / services:** + +- Add `--subsidyProviders ` to `startCompute`, `startService`, or `extendService`: + - omit the flag → the node uses its configured defaults, + - `--subsidyProviders none` (or empty) → claim with **no** subsidy, + - `--subsidyProviders 0xA,0xB` → claim against exactly those contracts. + + e.g. `npm run cli startCompute -- did:op:dataset did:op:algo env1 900 0xToken '[...resources...]' --accept true --subsidyProviders 0xA,0xB` + + (`startFreeCompute` ignores subsidy providers — a free job does no escrow claim.) + +--- + **Allow Algorithm on Dataset:** - **Positional:** @@ -944,12 +1034,18 @@ Notes: `-p, --payee ` `-m, --maxLockedAmount ` `-s, --maxLockSeconds ` - `-c, --maxLockCounts ` + `-c, --maxLockCounts ` + `-e, --expiry ` (Escrow v2; `0` = indefinite, a past value revokes) + `--chainId ` - **getAuthorizationsEscrow:** `-t, --token ` `-p, --payee ` +- **getEscrowInfo (escrowInfo):** + `[token]` / `-t, --token ` + `--chainId ` + - **createAccessList:** `-n, --name ` `-s, --symbol ` diff --git a/package-lock.json b/package-lock.json index 04aa592..a18e4e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "license": "Apache-2.0", "dependencies": { "@oasisprotocol/sapphire-ethers-v6": "^6.0.1", - "@oceanprotocol/contracts": "^2.9.0", + "@oceanprotocol/contracts": "^3.2.0-rc.0", "@oceanprotocol/ddo-js": "^0.4.1", - "@oceanprotocol/lib": "^9.0.0", + "@oceanprotocol/lib": "9.3.0-next.3", "axios": "^1.19.0", "chalk": "^4.1.2", "commander": "^13.1.0", @@ -1211,32 +1211,32 @@ "license": "MIT" }, "node_modules/@libp2p/bootstrap": { - "version": "12.0.29", - "resolved": "https://registry.npmjs.org/@libp2p/bootstrap/-/bootstrap-12.0.29.tgz", - "integrity": "sha512-HJqRyr75io10F/zaJCMGhrdOi7an4bJmgw12DLrBR/hPteeFAIZ4bXeiV+GNxw5vmRJoEpfUTVzIwPTsRJMViw==", + "version": "12.0.32", + "resolved": "https://registry.npmjs.org/@libp2p/bootstrap/-/bootstrap-12.0.32.tgz", + "integrity": "sha512-MpGPeijiYP7a//EsbcrWAuWjHNoGhIqv4ONcGXkuTgSCJbJVWpZrR2uA+I/UW7wW0UB4a3PmHJWAd4klMgZqlA==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", - "@libp2p/interface-internal": "^3.1.11", - "@libp2p/peer-id": "^6.0.14", + "@libp2p/interface": "^3.3.0", + "@libp2p/interface-internal": "^3.1.13", + "@libp2p/peer-id": "^6.0.15", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", "main-event": "^1.0.1" } }, "node_modules/@libp2p/circuit-relay-v2": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@libp2p/circuit-relay-v2/-/circuit-relay-v2-4.2.11.tgz", - "integrity": "sha512-q47qWuJxmNocAX4oRAUWpp2d56aiymsih9ORCkBdQGUkBzynPyBsucvCX5bnF8tb80Ji5wBHl4jCa1DmrW1T1A==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@libp2p/circuit-relay-v2/-/circuit-relay-v2-4.2.13.tgz", + "integrity": "sha512-AGEzj72W7Ae5eizaM+HeDzJBR28zK4yYp4BYvUOHQViT62W97FIITo/9Gc5rlkKti+gJJAurBm3cHV4A8Wcoxw==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/crypto": "^5.1.22", - "@libp2p/interface": "^3.2.5", - "@libp2p/interface-internal": "^3.1.11", - "@libp2p/peer-collections": "^7.0.26", - "@libp2p/peer-id": "^6.0.14", - "@libp2p/peer-record": "^9.0.15", - "@libp2p/utils": "^7.3.2", + "@libp2p/crypto": "^5.1.23", + "@libp2p/interface": "^3.3.0", + "@libp2p/interface-internal": "^3.1.13", + "@libp2p/peer-collections": "^7.0.28", + "@libp2p/peer-id": "^6.0.15", + "@libp2p/peer-record": "^9.0.16", + "@libp2p/utils": "^7.4.1", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", "any-signal": "^4.1.1", @@ -1250,28 +1250,10 @@ "uint8arrays": "^6.1.1" } }, - "node_modules/@libp2p/circuit-relay-v2/node_modules/nanoid": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-6.0.1.tgz", - "integrity": "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^22 || ^24 || >=26" - } - }, "node_modules/@libp2p/circuit-relay-v2/node_modules/protons-runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.0.0.tgz", - "integrity": "sha512-r/e72006xoVND4Uvf1aIs4OQOkJaASBU3ip4DzOWAktoKAkTiOYqKoS3TYMBm8HnnYU8+h0uEzr5gIRwk/pmTg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.1.2.tgz", + "integrity": "sha512-j7Y7MfCwPCxYwopyqDt8+CoJULzdajSBrFAzzfBHEhOTfdvOJ2z1QLK9aK34bSBz6V/9JAAlrsc/rFkIbqppuw==", "license": "Apache-2.0 OR MIT", "dependencies": { "uint8-varint": "^3.0.0", @@ -1289,12 +1271,12 @@ } }, "node_modules/@libp2p/crypto": { - "version": "5.1.22", - "resolved": "https://registry.npmjs.org/@libp2p/crypto/-/crypto-5.1.22.tgz", - "integrity": "sha512-yVk4BefSiEz044kjsiLSEoJTbhRoyamqx4kFI9LnX8/asr4wnqDWKTXcn6mSx/RVNEzmDO70l3vOvcMCl4aLCQ==", + "version": "5.1.23", + "resolved": "https://registry.npmjs.org/@libp2p/crypto/-/crypto-5.1.23.tgz", + "integrity": "sha512-u6XVMD1YpUJgjS5MAayrxlzi+hQcj3FHY0wS6/M/T93ntyCW13BmmRzFb2ESamk65PEuSCAChmQeSzRV2sh2rQ==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", + "@libp2p/interface": "^3.3.0", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "multiformats": "^14.0.0", @@ -1351,17 +1333,17 @@ } }, "node_modules/@libp2p/identify": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@libp2p/identify/-/identify-4.1.12.tgz", - "integrity": "sha512-nNBnxSclPVsvRsaLlwX9cz7gO020KjWEsaSzqDryWz6VStbLLdcwJYkVUP3do7VoFHkkF8PMQkScW7XCA27ZOA==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@libp2p/identify/-/identify-4.1.14.tgz", + "integrity": "sha512-c7oU0DSKzkMNsanruOQc4Zy6mSf3fq8/W+4E/I936HiHZc/LIbUuCWaxbgmQexdfKqECH1qH4q9IwaCUWmjPMQ==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/crypto": "^5.1.22", - "@libp2p/interface": "^3.2.5", - "@libp2p/interface-internal": "^3.1.11", - "@libp2p/peer-id": "^6.0.14", - "@libp2p/peer-record": "^9.0.15", - "@libp2p/utils": "^7.3.2", + "@libp2p/crypto": "^5.1.23", + "@libp2p/interface": "^3.3.0", + "@libp2p/interface-internal": "^3.1.13", + "@libp2p/peer-id": "^6.0.15", + "@libp2p/peer-record": "^9.0.16", + "@libp2p/utils": "^7.4.1", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", "it-drain": "^3.0.10", @@ -1373,9 +1355,9 @@ } }, "node_modules/@libp2p/identify/node_modules/protons-runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.0.0.tgz", - "integrity": "sha512-r/e72006xoVND4Uvf1aIs4OQOkJaASBU3ip4DzOWAktoKAkTiOYqKoS3TYMBm8HnnYU8+h0uEzr5gIRwk/pmTg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.1.2.tgz", + "integrity": "sha512-j7Y7MfCwPCxYwopyqDt8+CoJULzdajSBrFAzzfBHEhOTfdvOJ2z1QLK9aK34bSBz6V/9JAAlrsc/rFkIbqppuw==", "license": "Apache-2.0 OR MIT", "dependencies": { "uint8-varint": "^3.0.0", @@ -1393,9 +1375,9 @@ } }, "node_modules/@libp2p/interface": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@libp2p/interface/-/interface-3.2.5.tgz", - "integrity": "sha512-RdTchTdakBBc4ShKKsvoME/bJYsrCoVDpdYQVdd4TQ30TCb8QwWKGClqAtw7gfLNuaUKm41xz06kASW6AZpbDA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@libp2p/interface/-/interface-3.3.0.tgz", + "integrity": "sha512-SXahM/4IgpiFKTtocbXYSTA1wEUVjktmT1yBCzBbc2Vgsu1VBCg6SvsFmXo2Hbeyev3D8EU+y3uaCKDB43C/Vg==", "license": "Apache-2.0 OR MIT", "dependencies": { "@multiformats/dns": "^1.0.6", @@ -1407,13 +1389,13 @@ } }, "node_modules/@libp2p/interface-internal": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/@libp2p/interface-internal/-/interface-internal-3.1.11.tgz", - "integrity": "sha512-lHJueWKdfkf/nrbVkGNUWrqDvF3T9Brn4hyONQmhTqG2mWyWKntRR/CkPjVgMRDSF2fuyzDJGg+n9tC0Gphk8g==", + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/@libp2p/interface-internal/-/interface-internal-3.1.13.tgz", + "integrity": "sha512-+vjQzpQl+0uoWCyNwV5NDnv1RAK9LDeWGeMbx/Fl2ZWrndtuFUPa9LxwqHXrTly6YOhgUatSzVqZBWnrQ1PxGg==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", - "@libp2p/peer-collections": "^7.0.26", + "@libp2p/interface": "^3.3.0", + "@libp2p/peer-collections": "^7.0.28", "@multiformats/multiaddr": "^13.0.3", "progress-events": "^1.0.1" } @@ -1428,19 +1410,19 @@ } }, "node_modules/@libp2p/kad-dht": { - "version": "16.4.3", - "resolved": "https://registry.npmjs.org/@libp2p/kad-dht/-/kad-dht-16.4.3.tgz", - "integrity": "sha512-i3FDNr9XLHcl8X27MRWrra3UyZw4o1esIen+66CNwFzZOTe85ndv6zNDOpgd2di1JT+E/xhKrghd+0wO3zfC6w==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@libp2p/kad-dht/-/kad-dht-16.4.5.tgz", + "integrity": "sha512-Pxmk3pIfe5Ih1xr+CY1uzw8AXRY+2VI6p3iebM4ljueu+dovEPIahW8tq4F2iNHIQftuc9Svncw4JikwrU7I1w==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/crypto": "^5.1.22", - "@libp2p/interface": "^3.2.5", - "@libp2p/interface-internal": "^3.1.11", - "@libp2p/peer-collections": "^7.0.26", - "@libp2p/peer-id": "^6.0.14", - "@libp2p/ping": "^3.1.11", + "@libp2p/crypto": "^5.1.23", + "@libp2p/interface": "^3.3.0", + "@libp2p/interface-internal": "^3.1.13", + "@libp2p/peer-collections": "^7.0.28", + "@libp2p/peer-id": "^6.0.15", + "@libp2p/ping": "^3.1.13", "@libp2p/record": "^4.0.15", - "@libp2p/utils": "^7.3.2", + "@libp2p/utils": "^7.4.1", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", "any-signal": "^4.1.1", @@ -1467,9 +1449,9 @@ } }, "node_modules/@libp2p/kad-dht/node_modules/protons-runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.0.0.tgz", - "integrity": "sha512-r/e72006xoVND4Uvf1aIs4OQOkJaASBU3ip4DzOWAktoKAkTiOYqKoS3TYMBm8HnnYU8+h0uEzr5gIRwk/pmTg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.1.2.tgz", + "integrity": "sha512-j7Y7MfCwPCxYwopyqDt8+CoJULzdajSBrFAzzfBHEhOTfdvOJ2z1QLK9aK34bSBz6V/9JAAlrsc/rFkIbqppuw==", "license": "Apache-2.0 OR MIT", "dependencies": { "uint8-varint": "^3.0.0", @@ -1487,12 +1469,12 @@ } }, "node_modules/@libp2p/logger": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-6.2.12.tgz", - "integrity": "sha512-FwCdJ5hGFDvWI/SULZ4Iez+k+JtdMAj4/EK6bWmkh08ORIztwKpt0IXCOe8nIfKhBKwLYJu00BnZDRsedX7+7Q==", + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-6.2.13.tgz", + "integrity": "sha512-+tkZuRPoiFdh+frG//3hQ3goV1uMzRblpZ+mwtezgLXkRNqB5YKBLl+c/rtrurwAUqjCuI/yoEPJBiCizg2z6w==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", + "@libp2p/interface": "^3.3.0", "@multiformats/multiaddr": "^13.0.3", "interface-datastore": "^10.0.1", "multiformats": "^14.0.0", @@ -1500,13 +1482,13 @@ } }, "node_modules/@libp2p/multistream-select": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/@libp2p/multistream-select/-/multistream-select-7.0.26.tgz", - "integrity": "sha512-bbKMgu7W/J6eknQODGpySshZq9QWEYbIM/fMYrihbdIT/eS1h+8U6ko2qylfwbDyYlnUoYP45Cqi+oxoNwGH3Q==", + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/@libp2p/multistream-select/-/multistream-select-7.0.28.tgz", + "integrity": "sha512-X6rmxeAlFeTDHDc4IPujz+y9TdA8cSMFfWyGJfxdIbQpDc1PwZI/Ttt3YWp0LAT9sbSYVFaWA6cGTN9EnMWtGQ==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", - "@libp2p/utils": "^7.3.2", + "@libp2p/interface": "^3.3.0", + "@libp2p/utils": "^7.4.1", "it-length-prefixed": "^11.0.1", "uint8arraylist": "^3.0.2", "uint8arrays": "^6.1.1" @@ -1522,38 +1504,38 @@ } }, "node_modules/@libp2p/peer-collections": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/@libp2p/peer-collections/-/peer-collections-7.0.26.tgz", - "integrity": "sha512-hjJbC/Jo1TjxqaKph2W6ARRnrNoBCEbSQMUsGT17JXQktJc7ImVGLoFy2VsZoJGMX4RLN+gKp6f8McFGb5Vi1w==", + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/@libp2p/peer-collections/-/peer-collections-7.0.28.tgz", + "integrity": "sha512-+evafZ5oSnGRfB7z+ycf09ehzNhCKoOZaiHgi6TpnDitpRQM5KQeN0H9Ep2uNJtzyW944HOG41BqcSaHbEHv7Q==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", - "@libp2p/peer-id": "^6.0.14", - "@libp2p/utils": "^7.3.2", + "@libp2p/interface": "^3.3.0", + "@libp2p/peer-id": "^6.0.15", + "@libp2p/utils": "^7.4.1", "multiformats": "^14.0.0" } }, "node_modules/@libp2p/peer-id": { - "version": "6.0.14", - "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-6.0.14.tgz", - "integrity": "sha512-tquBouwbUXgntPtT5q4vfaXbNWmSzh2zo4p2u8IzclVvggXOpmyBlAUNPXDiFqZvQpsxVBrTTemmn0+AwDwecg==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-6.0.15.tgz", + "integrity": "sha512-aIbYJcldMNOIzmm9J57aESjZInYFdrQ9AY0ij+qn0Oam2vBxDfEIJZxJu+JzVkB2rVvT+FHDQXKWT2nAqNgB8w==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/crypto": "^5.1.22", - "@libp2p/interface": "^3.2.5", + "@libp2p/crypto": "^5.1.23", + "@libp2p/interface": "^3.3.0", "multiformats": "^14.0.0", "uint8arrays": "^6.1.1" } }, "node_modules/@libp2p/peer-record": { - "version": "9.0.15", - "resolved": "https://registry.npmjs.org/@libp2p/peer-record/-/peer-record-9.0.15.tgz", - "integrity": "sha512-WZH9Xkdvpr6TlM6eDdE/KZJmk558jrAC/ufNHnbWXdEUxF/GTzx7RsrBgaC6cPGBTNTjzICfO6vZKR2US0Fl9Q==", + "version": "9.0.16", + "resolved": "https://registry.npmjs.org/@libp2p/peer-record/-/peer-record-9.0.16.tgz", + "integrity": "sha512-J4QoPr0F1v6xqjHjAJ9KPbg4jdDlGutDk2K3Km6+o0nWzZCgCGeSEIDOO2UvPa2bFoZTL62cD73fn8tm5K06cg==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/crypto": "^5.1.22", - "@libp2p/interface": "^3.2.5", - "@libp2p/peer-id": "^6.0.14", + "@libp2p/crypto": "^5.1.23", + "@libp2p/interface": "^3.3.0", + "@libp2p/peer-id": "^6.0.15", "@multiformats/multiaddr": "^13.0.3", "multiformats": "^14.0.0", "protons-runtime": "^7.0.0", @@ -1563,9 +1545,9 @@ } }, "node_modules/@libp2p/peer-record/node_modules/protons-runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.0.0.tgz", - "integrity": "sha512-r/e72006xoVND4Uvf1aIs4OQOkJaASBU3ip4DzOWAktoKAkTiOYqKoS3TYMBm8HnnYU8+h0uEzr5gIRwk/pmTg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.1.2.tgz", + "integrity": "sha512-j7Y7MfCwPCxYwopyqDt8+CoJULzdajSBrFAzzfBHEhOTfdvOJ2z1QLK9aK34bSBz6V/9JAAlrsc/rFkIbqppuw==", "license": "Apache-2.0 OR MIT", "dependencies": { "uint8-varint": "^3.0.0", @@ -1583,16 +1565,16 @@ } }, "node_modules/@libp2p/peer-store": { - "version": "12.0.26", - "resolved": "https://registry.npmjs.org/@libp2p/peer-store/-/peer-store-12.0.26.tgz", - "integrity": "sha512-VoLjd7SQrYN45w3pnGl8YQQn+9tzMsoZxdmSG7VWoMiQhMLyxCtkctsVi9OCdT1IkdelPRq84KHklkK0D4FG9w==", + "version": "12.0.28", + "resolved": "https://registry.npmjs.org/@libp2p/peer-store/-/peer-store-12.0.28.tgz", + "integrity": "sha512-dj4qRhN01kml/It5/4P4DVcjfEZXJ2uZVVItCrs2YBGG2HLEYfWQZYROVbTCXDOCWwZK46aJ8eSLt4orsefq7Q==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/crypto": "^5.1.22", - "@libp2p/interface": "^3.2.5", - "@libp2p/peer-collections": "^7.0.26", - "@libp2p/peer-id": "^6.0.14", - "@libp2p/peer-record": "^9.0.15", + "@libp2p/crypto": "^5.1.23", + "@libp2p/interface": "^3.3.0", + "@libp2p/peer-collections": "^7.0.28", + "@libp2p/peer-id": "^6.0.15", + "@libp2p/peer-record": "^9.0.16", "@multiformats/multiaddr": "^13.0.3", "interface-datastore": "^10.0.1", "it-all": "^3.0.9", @@ -1605,9 +1587,9 @@ } }, "node_modules/@libp2p/peer-store/node_modules/protons-runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.0.0.tgz", - "integrity": "sha512-r/e72006xoVND4Uvf1aIs4OQOkJaASBU3ip4DzOWAktoKAkTiOYqKoS3TYMBm8HnnYU8+h0uEzr5gIRwk/pmTg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.1.2.tgz", + "integrity": "sha512-j7Y7MfCwPCxYwopyqDt8+CoJULzdajSBrFAzzfBHEhOTfdvOJ2z1QLK9aK34bSBz6V/9JAAlrsc/rFkIbqppuw==", "license": "Apache-2.0 OR MIT", "dependencies": { "uint8-varint": "^3.0.0", @@ -1625,13 +1607,13 @@ } }, "node_modules/@libp2p/ping": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/@libp2p/ping/-/ping-3.1.11.tgz", - "integrity": "sha512-3ByJjpzxMAEJv8ZjWeLYv9k0Ba3eVxPRcWc8H79/RQhTQwY2zZvHYX4gQUmyqA9Sv0Q6peL4TwUW7E3wgN27Mw==", + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/@libp2p/ping/-/ping-3.1.13.tgz", + "integrity": "sha512-zB0TYbxWD73z3kqHrC2PDk7CAX0wQ0smz6mZ9okE4MoFgJv+XtMaIB2pB/4eT7xjWwdc2w3mYRmq+lMyXl3nXQ==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", - "@libp2p/interface-internal": "^3.1.11", + "@libp2p/interface": "^3.3.0", + "@libp2p/interface-internal": "^3.1.13", "p-event": "^7.0.0", "race-signal": "^2.0.0", "uint8arraylist": "^3.0.2", @@ -1659,9 +1641,9 @@ } }, "node_modules/@libp2p/record/node_modules/protons-runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.0.0.tgz", - "integrity": "sha512-r/e72006xoVND4Uvf1aIs4OQOkJaASBU3ip4DzOWAktoKAkTiOYqKoS3TYMBm8HnnYU8+h0uEzr5gIRwk/pmTg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-7.1.2.tgz", + "integrity": "sha512-j7Y7MfCwPCxYwopyqDt8+CoJULzdajSBrFAzzfBHEhOTfdvOJ2z1QLK9aK34bSBz6V/9JAAlrsc/rFkIbqppuw==", "license": "Apache-2.0 OR MIT", "dependencies": { "uint8-varint": "^3.0.0", @@ -1679,13 +1661,13 @@ } }, "node_modules/@libp2p/tcp": { - "version": "11.0.26", - "resolved": "https://registry.npmjs.org/@libp2p/tcp/-/tcp-11.0.26.tgz", - "integrity": "sha512-OOLb1Ov6zQRo983fEPRUvfcWmFrm2oJlwPgf7M6pzLyQOsTxpgb2MV37Az8ANy5GEAG4JJ+KPZn3g7JfEp33xA==", + "version": "11.0.28", + "resolved": "https://registry.npmjs.org/@libp2p/tcp/-/tcp-11.0.28.tgz", + "integrity": "sha512-+YD59zz1V8YT47ZiInqoHAeSx45BC2bb5FVu3Bplo5zOxMMNXcH5WATIgn+IYIbusNPpMcs5wKd0QimQwzBl6Q==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", - "@libp2p/utils": "^7.3.2", + "@libp2p/interface": "^3.3.0", + "@libp2p/utils": "^7.4.1", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", "main-event": "^1.0.1", @@ -1704,15 +1686,15 @@ } }, "node_modules/@libp2p/utils": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@libp2p/utils/-/utils-7.3.2.tgz", - "integrity": "sha512-nVfJkTE3QMa/LVKVD5sKDB8birKua0BR0oKoqnLSXsX1ez89PvhR1zZjBfARSGVZdv5qUmblcdcvjJ76HD0CWA==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@libp2p/utils/-/utils-7.4.1.tgz", + "integrity": "sha512-e9cZeop+awI0rFczG4DvAwVeHqBhuCZfTyskvgONfKQrsT7L0BSlTlTTzZoFF8Dnb+QrzHsoIpnl0PrCr0h+Hg==", "license": "Apache-2.0 OR MIT", "dependencies": { "@chainsafe/is-ip": "^2.1.0", "@chainsafe/netmask": "^2.0.0", - "@libp2p/interface": "^3.2.5", - "@libp2p/logger": "^6.2.12", + "@libp2p/interface": "^3.3.0", + "@libp2p/logger": "^6.2.13", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", "@sindresorhus/fnv1a": "^3.1.0", @@ -1766,13 +1748,13 @@ } }, "node_modules/@libp2p/websockets": { - "version": "10.1.19", - "resolved": "https://registry.npmjs.org/@libp2p/websockets/-/websockets-10.1.19.tgz", - "integrity": "sha512-inY5dWhm+mCrnS/wcHIWtDZ908c1W7UkpXE9i6uvnJVapWbvonvX9GEsTLvh4NbL7SOfnOHGkwDVgx1M2RxcRA==", + "version": "10.1.21", + "resolved": "https://registry.npmjs.org/@libp2p/websockets/-/websockets-10.1.21.tgz", + "integrity": "sha512-2iItZGEfTlGFUEcFCghtlsIiQ+fBowopysBB/8ya0MvZkRic/6o9oSklWT5pWDmXtmqmbp1Dko/iLcBOhDOgFw==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@libp2p/interface": "^3.2.5", - "@libp2p/utils": "^7.3.2", + "@libp2p/interface": "^3.3.0", + "@libp2p/utils": "^7.4.1", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", "@multiformats/multiaddr-to-uri": "^12.0.0", @@ -1945,9 +1927,9 @@ } }, "node_modules/@oceanprotocol/contracts": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-2.9.0.tgz", - "integrity": "sha512-B3dQNxIYD7bASNE066vfZu6Ik5uHZ/1c+QEcUvAsfoNvUUJ5+uQfIvhwrpdMCza1GtE11EW2glDPZvbr2LwFsg==", + "version": "3.2.0-rc.0", + "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-3.2.0-rc.0.tgz", + "integrity": "sha512-QBqSZz98AGqClr8HKKSmFFRrz8DvFhLT8swUWz+XbnTwgTXSnR2vg7UojLhAI10MbeSwfh37YcbGvdcnl3DRmA==", "license": "Apache-2.0" }, "node_modules/@oceanprotocol/ddo-js": { @@ -1964,23 +1946,23 @@ } }, "node_modules/@oceanprotocol/lib": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-9.0.0.tgz", - "integrity": "sha512-nm0rSNIqbfvBzi4MSvMvll3YvwRuavgJ+iL/wHf/IPtZ/w++GOwx6c8U3Y3vv6/WuKVxz+Oe+uRjCMglJh+BpQ==", + "version": "9.3.0-next.3", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-9.3.0-next.3.tgz", + "integrity": "sha512-5hTQjJEcDyyCIMDP+egDA/cMYhaV2SKDUIf3u6AZp3mR0Bv/MEydwspHCGo/7LLw8OlqwwHLWjr9fi6zcyFjFA==", "license": "Apache-2.0", "dependencies": { "@chainsafe/libp2p-noise": "^17.0.0", "@chainsafe/libp2p-yamux": "^8.0.1", - "@libp2p/bootstrap": "^12.0.29", - "@libp2p/circuit-relay-v2": "^4.2.11", - "@libp2p/identify": "^4.1.12", - "@libp2p/interface": "^3.2.5", - "@libp2p/kad-dht": "^16.4.2", - "@libp2p/peer-id": "^6.0.14", - "@libp2p/ping": "^3.1.11", - "@libp2p/tcp": "^11.0.26", - "@libp2p/utils": "^7.3.2", - "@libp2p/websockets": "^10.1.19", + "@libp2p/bootstrap": "^12.0.31", + "@libp2p/circuit-relay-v2": "^4.2.13", + "@libp2p/identify": "^4.1.14", + "@libp2p/interface": "^3.3.0", + "@libp2p/kad-dht": "^16.4.5", + "@libp2p/peer-id": "^6.0.15", + "@libp2p/ping": "^3.1.13", + "@libp2p/tcp": "^11.0.28", + "@libp2p/utils": "^7.4.1", + "@libp2p/websockets": "^10.1.21", "@multiformats/multiaddr": "^13.0.3", "@oasisprotocol/sapphire-ethers-v6": "^6.0.1", "@oceanprotocol/ddo-js": "^0.4.1", @@ -1991,7 +1973,7 @@ "eciesjs": "^0.5.0", "ethers": "^6.17.0", "jsonwebtoken": "^9.0.3", - "libp2p": "^3.3.8", + "libp2p": "^3.3.10", "multiformats": "^14.0.5", "uint8arrays": "^6.1.1" }, @@ -5213,22 +5195,22 @@ } }, "node_modules/libp2p": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-3.3.8.tgz", - "integrity": "sha512-CMDYzEflxjtDkRV4vx8u4H93VxnPJOnntqd1pumrb5meorpe3hppxvwvgUc/eQ/5+ZwRQCgFi1rUfaaQt0zy0g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-3.3.11.tgz", + "integrity": "sha512-+J3Dw1RnGdcpQaPN1HD+Z6xVS3qY0vnsqE2Tu//pfoiJxabaZVkc2FBNnNQ1Sj+Th2qoloeJ11jvxuNXSa1gtA==", "license": "Apache-2.0 OR MIT", "dependencies": { "@chainsafe/is-ip": "^2.1.0", "@chainsafe/netmask": "^2.0.0", - "@libp2p/crypto": "^5.1.22", - "@libp2p/interface": "^3.2.5", - "@libp2p/interface-internal": "^3.1.11", - "@libp2p/logger": "^6.2.12", - "@libp2p/multistream-select": "^7.0.26", - "@libp2p/peer-collections": "^7.0.26", - "@libp2p/peer-id": "^6.0.14", - "@libp2p/peer-store": "^12.0.26", - "@libp2p/utils": "^7.3.2", + "@libp2p/crypto": "^5.1.23", + "@libp2p/interface": "^3.3.0", + "@libp2p/interface-internal": "^3.1.13", + "@libp2p/logger": "^6.2.13", + "@libp2p/multistream-select": "^7.0.28", + "@libp2p/peer-collections": "^7.0.28", + "@libp2p/peer-id": "^6.0.15", + "@libp2p/peer-store": "^12.0.28", + "@libp2p/utils": "^7.4.1", "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^13.0.3", "@multiformats/multiaddr-matcher": "^3.0.2", @@ -5585,6 +5567,24 @@ "node": ">=12.0" } }, + "node_modules/nanoid": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-6.0.2.tgz", + "integrity": "sha512-hIvVOb9Qr1p/9CNNLOe0OlWvgx/ziTsVwKN/GDSsuArN2K0wDiafP3s0SWmsm7o2BdnyS8Z9Z64NFBUbn6ziBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^22 || ^24 || >=26" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5920,9 +5920,9 @@ } }, "node_modules/p-retry": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", - "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.1.tgz", + "integrity": "sha512-NAigyu8hfvJe7MsS18mnc4is0MZtau3QMdBklaJKK23oBQb6occO3UPM3vHmTckW8KhfyjMZaUOqdTFEYliHgg==", "license": "MIT", "dependencies": { "is-network-error": "^1.3.0" diff --git a/package.json b/package.json index 05684cc..8dc7267 100644 --- a/package.json +++ b/package.json @@ -75,9 +75,9 @@ }, "dependencies": { "@oasisprotocol/sapphire-ethers-v6": "^6.0.1", - "@oceanprotocol/contracts": "^2.9.0", + "@oceanprotocol/contracts": "^3.2.0-rc.0", "@oceanprotocol/ddo-js": "^0.4.1", - "@oceanprotocol/lib": "^9.0.0", + "@oceanprotocol/lib": "9.3.0-next.3", "axios": "^1.19.0", "chalk": "^4.1.2", "commander": "^13.1.0", diff --git a/src/cli.ts b/src/cli.ts index 33fe40e..c6f9983 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,12 +11,17 @@ import { ServiceStatusNumber, ServiceRestartParams, } from "@oceanprotocol/lib"; -import { parseComputeInput, toBoolean } from "./helpers.js"; +import { + parseComputeInput, + toBoolean, + parseSubsidyProviders, +} from "./helpers.js"; import { getCurrentNodeUrl, getCurrentEnvId, hasNode, nodeChainIds, + nodeSubsidyInfo, setCurrentNodeUrl, setCurrentEnvId, clearCurrentEnvId, @@ -87,17 +92,18 @@ const HELP_GROUPS: HelpGroup[] = [ }, { heading: "Chains & RPC", - commands: [ - "addChain", - "removeChain", - "listChains", - "setChain", - "getChain", - ], + commands: ["addChain", "removeChain", "listChains", "setChain", "getChain"], }, { heading: "Assets — publish, edit, consume", - commands: ["publish", "publishAlgo", "editAsset", "allowAlgo", "getDDO", "download"], + commands: [ + "publish", + "publishAlgo", + "editAsset", + "allowAlgo", + "getDDO", + "download", + ], }, { heading: "Compute", @@ -140,6 +146,7 @@ const HELP_GROUPS: HelpGroup[] = [ "withdrawFromEscrow", "authorizeEscrow", "getAuthorizationsEscrow", + "getEscrowInfo", ], }, { @@ -151,6 +158,10 @@ const HELP_GROUPS: HelpGroup[] = [ "removeFromAccessList", ], }, + { + heading: "Subsidy providers", + commands: ["getSubsidyProviders", "getSubsidyStatus"], + }, { heading: "Persistent storage (buckets)", commands: [ @@ -179,14 +190,19 @@ function groupedCommandNames(): string[] { // vanish from help), one grouped twice, or a group naming a command that no longer exists. function assertHelpGroupsCoverAll(program: Command): void { const grouped = groupedCommandNames(); - const dupes = [...new Set(grouped.filter((n, i) => grouped.indexOf(n) !== i))]; + const dupes = [ + ...new Set(grouped.filter((n, i) => grouped.indexOf(n) !== i)), + ]; const registered = program.commands.map((c) => c.name()); const missing = registered.filter((n) => !grouped.includes(n)); const unknown = grouped.filter((n) => !registered.includes(n)); const problems: string[] = []; - if (dupes.length) problems.push(`listed in more than one group: ${dupes.join(", ")}`); - if (missing.length) problems.push(`not in any help group: ${missing.join(", ")}`); - if (unknown.length) problems.push(`grouped but not registered: ${unknown.join(", ")}`); + if (dupes.length) + problems.push(`listed in more than one group: ${dupes.join(", ")}`); + if (missing.length) + problems.push(`not in any help group: ${missing.join(", ")}`); + if (unknown.length) + problems.push(`grouped but not registered: ${unknown.join(", ")}`); if (problems.length) { throw new Error(`Help groups out of sync — ${problems.join("; ")}`); } @@ -204,7 +220,10 @@ export function formatGroupedHelp(program: Command): string { return ` ${label}${gap}${desc}`; }; - const out: string[] = [chalk.bold(`Ocean CLI v${pkg.version} — commands`), ""]; + const out: string[] = [ + chalk.bold(`Ocean CLI v${pkg.version} — commands`), + "", + ]; for (const group of HELP_GROUPS) { out.push(chalk.cyan.bold(group.heading)); for (const n of group.commands ?? []) out.push(line(n)); @@ -227,6 +246,32 @@ export function formatGroupedHelp(program: Command): string { // (dev via tsx) and dist/ (published), since both sit one level below the root. const pkg = createRequire(import.meta.url)("../package.json"); +// Print the subsidy-provider info a node advertises in its status (shared by getNode and +// getSubsidyProviders). `indent` lets getNode nest it under the node summary. +function printSubsidyInfo( + info: { providers: Record; filter: boolean }, + indent = " ", +): void { + const chains = Object.keys(info.providers).filter( + (c) => (info.providers[c]?.length ?? 0) > 0, + ); + if (chains.length === 0) { + console.log( + chalk.yellow(`${indent}Subsidy providers: none advertised by this node.`), + ); + } else { + console.log(`${indent}Subsidy providers:`); + for (const c of chains.sort()) { + console.log(`${indent} chain ${c}: ${info.providers[c].join(", ")}`); + } + } + console.log( + `${indent}Subsidy provider filter (whitelist enforced): ${ + info.filter ? "on" : "off" + }`, + ); +} + // Parse a CLI JSON array-of-strings option (e.g. --cmd '["python","app.py"]'). // Returns the array, or throws with a clear message for the action to surface. function parseJsonStringArray(name: string, value: string): string[] { @@ -293,7 +338,9 @@ function resolveChainId(flag?: string | number): number { if (flag !== undefined && flag !== null && `${flag}`.trim() !== "") { const id = Number(flag); if (!Number.isInteger(id) || id <= 0) { - throw new Error(`Invalid --chainId "${flag}": must be a positive integer.`); + throw new Error( + `Invalid --chainId "${flag}": must be a positive integer.`, + ); } if (!hasChain(id)) { throw new Error( @@ -612,7 +659,9 @@ export async function createCLI() { const configured = nodeChains.filter((c) => hasChain(Number(c))); if (configured.length) { console.log( - chalk.green(` RPC configured for chain(s): ${configured.join(", ")}`), + chalk.green( + ` RPC configured for chain(s): ${configured.join(", ")}`, + ), ); } if (missing.length) { @@ -627,11 +676,85 @@ export async function createCLI() { } catch { // RPC not configured / unavailable — the node info above is still useful. } + printSubsidyInfo(nodeSubsidyInfo(status)); } else { console.log(chalk.yellow("Node is not reachable right now.")); } }); + // getSubsidyProviders command — show the subsidy-provider contracts the current node + // advertises per chain (and whether it enforces a whitelist on user-supplied ones). + // Node-dependent (deliberately NOT in NODE_FREE_COMMANDS): the preAction gate refuses it + // until a node is set. Implemented inline like getNode — no signer/Commands needed. + program + .command("getSubsidyProviders") + .alias("subsidyProviders") + .description( + "Show the subsidy-provider contracts the current Ocean Node supports (per chain)", + ) + .action(async () => { + const current = getCurrentNodeUrl(); + if (!current) { + console.log( + chalk.yellow( + "No Ocean Node set. Run `setNode ` to pick one.", + ), + ); + return; + } + const status = await validateNode(current); + if (!status) { + console.log(chalk.yellow("Node is not reachable right now.")); + return; + } + console.log(`Ocean Node: ${current}`); + printSubsidyInfo(nodeSubsidyInfo(status)); + }); + + // getSubsidyStatus command — read-only, provider-agnostic subsidy report for a token on a + // chain: kind, per-window buckets (limit/used/remaining), claimable-now amount, contract + // balance, eligibility, and (with --node/--jobType/--amount) a quote. Provider addresses are + // discovered from the node; --subsidy only narrows to a subset the node advertises. + program + .command("getSubsidyStatus") + .alias("subsidyStatus") + .description( + "Show subsidy limits / remaining credit for a token from the node's subsidy providers", + ) + .argument("", "Token address to report limits/credit for") + .option("-t, --token ", "Token address to report limits/credit for") + .option( + "--chainId ", + "Subsidy/escrow chain (default: active chain)", + ) + .option( + "--subsidy ", + "Narrow to specific node-advertised provider contract(s) (comma-separated); else all the node lists", + ) + .option( + "--node
", + "Node address, for isNodeAllowed / quoteSubsidy", + ) + .option( + "--jobType ", + "Job type for isJobTypeSubsidized / quoteSubsidy (compute | service | none)", + ) + .option( + "--amount ", + "Job amount to quote a subsidy for, in natural token units (e.g. 1.5). Needs --node and --jobType.", + ) + .action(async (token, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + if ((await routeExplicit(commands, options.chainId)) === null) return; + await commands.getSubsidyStatus(token || options.token, { + subsidy: options.subsidy, + node: options.node, + jobType: options.jobType, + amount: options.amount, + }); + }); + // --------------------------------------------------------------------------- // Chain / RPC management (node-free — see NODE_FREE_COMMANDS). Implemented directly // here, mirroring setNode/getNode: the registry is the single source of truth and no @@ -755,7 +878,9 @@ export async function createCLI() { program .command("setChain") .alias("useChain") - .description("Set the default (active) chain (must be registered; persists)") + .description( + "Set the default (active) chain (must be registered; persists)", + ) .argument("", "Chain id to make default") .option("-c, --chainId ", "Chain id to make default") .action(async (chainIdArg, options) => { @@ -978,6 +1103,10 @@ export async function createCLI() { "-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.", ) + .option( + "--subsidyProviders ", + "Subsidy provider contract addresses (comma-separated) to claim against. Omit to use the node's defaults; pass 'none' (or empty) to explicitly claim with no subsidy.", + ) .action( async ( datasetDids, @@ -1044,6 +1173,20 @@ export async function createCLI() { ); return; } + + // Tri-state: undefined (node default) | [] (no subsidy) | [addr,...]. + // Validate up front so a malformed address is rejected before compute + // initialization and the payment prompt. + let subsidyProviders: string[] | undefined; + try { + subsidyProviders = parseSubsidyProviders(options.subsidyProviders); + } catch (e) { + console.error( + chalk.red(`Invalid --subsidyProviders: ${(e as Error).message}`), + ); + return; + } + const { signer, chainId } = await initializeSigner(); const commands = new Commands(signer, chainId); @@ -1132,7 +1275,11 @@ export async function createCLI() { algoSvcId, ]; - const started = await commands.computeStart(computeArgs, paymentChainId); + const started = await commands.computeStart( + computeArgs, + paymentChainId, + subsidyProviders, + ); if (started) { console.log(chalk.green("Compute job started successfully.")); } @@ -1447,6 +1594,10 @@ export async function createCLI() { "--chainId ", "Payment/escrow chain (default: active chain); must be one the env prices on", ) + .option( + "--subsidyProviders ", + "Subsidy provider contract addresses (comma-separated) to claim against. Omit for node defaults; 'none' (or empty) for no subsidy.", + ) .action(async (computeEnvId, duration, paymentToken, options) => { const envId = options.env || computeEnvId; const token = paymentToken; @@ -1474,11 +1625,13 @@ export async function createCLI() { let ports: number[] | undefined; let cmd: string[] | undefined; let entrypoint: string[] | undefined; + let subsidyProviders: string[] | undefined; try { if (options.ports) ports = parsePorts(options.ports); if (options.cmd) cmd = parseJsonStringArray("--cmd", options.cmd); if (options.entrypoint) entrypoint = parseJsonStringArray("--entrypoint", options.entrypoint); + subsidyProviders = parseSubsidyProviders(options.subsidyProviders); } catch (e) { console.error(chalk.red((e as Error).message)); return; @@ -1508,6 +1661,7 @@ export async function createCLI() { accept: options.accept, wait: options.wait, timeout: options.timeout, + subsidyProviders, }); }); @@ -1636,7 +1790,14 @@ export async function createCLI() { "Auto-confirm payment (true/false)", toBoolean, ) - .option("--chainId ", "Payment/escrow chain (default: active chain)") + .option( + "--chainId ", + "Payment/escrow chain (default: active chain)", + ) + .option( + "--subsidyProviders ", + "Subsidy provider contract addresses (comma-separated) to claim against. Omit for node defaults; 'none' (or empty) for no subsidy.", + ) .action(async (serviceId, additionalDuration, paymentToken, options) => { const id = options.service || serviceId; const addl = options.duration || additionalDuration; @@ -1657,10 +1818,25 @@ export async function createCLI() { ); return; } + let subsidyProviders: string[] | undefined; + try { + subsidyProviders = parseSubsidyProviders(options.subsidyProviders); + } catch (e) { + console.error( + chalk.red(`Invalid --subsidyProviders: ${(e as Error).message}`), + ); + return; + } const { signer, chainId } = await initializeSigner(); const commands = new Commands(signer, chainId); if ((await routeExplicit(commands, options.chainId)) === null) return; - await commands.extendService(id, addl, token, options.accept); + await commands.extendService( + id, + addl, + token, + options.accept, + subsidyProviders, + ); }); // restartService command @@ -1899,7 +2075,9 @@ export async function createCLI() { // Escrow authorization command program .command("authorizeEscrow") - .description("Authorize a payee to lock and claim funds from escrow") + .description( + "Authorize (or re-authorize) a payee to lock and claim funds from escrow. Re-running overwrites the existing limits (renew/shorten); pass a past --expiry to revoke", + ) .argument("", "Address of the token to authorize") .argument("", "Address of the payee to authorize") .argument("", "Maximum amount that can be locked by payee") @@ -1919,6 +2097,10 @@ export async function createCLI() { "-c, --maxLockCounts ", "Maximum number of locks allowed", ) + .option( + "-e, --expiry ", + "Escrow v2: unix timestamp (seconds) after which the payee can no longer create/extend locks. 0 = indefinite (default). A past timestamp revokes; claim/cancel are never gated by it", + ) .option("--chainId ", "Escrow chain (default: active chain)") .action( async ( @@ -1944,6 +2126,7 @@ export async function createCLI() { maxLockedAmountValue, maxLockSecondsValue, maxLockCountsValue, + options.expiry, ); if (!success) { @@ -1955,6 +2138,27 @@ export async function createCLI() { }, ); + // Escrow v2 diagnostics: escrow kind/version, ERC-165 capabilities, and (with a token) the + // sponsored bucket / enterprise fee gate. + program + .command("getEscrowInfo") + .alias("escrowInfo") + .description( + "Show escrow capabilities for the active chain (version, kind, lock-time sponsorship, enterprise fee gate). Pass a token to also read the sponsored bucket and fee gate", + ) + .argument( + "[token]", + "Optional token address to read the sponsored bucket / fee gate for", + ) + .option("-t, --token ", "Optional token address") + .option("--chainId ", "Escrow chain (default: active chain)") + .action(async (token, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + if ((await routeExplicit(commands, options.chainId)) === null) return; + await commands.getEscrowInfo(options.token || token); + }); + program .command("getAuthorizationsEscrow") .description("Get authorizations for escrow") @@ -2110,7 +2314,7 @@ export async function createCLI() { "-m, --maxLogs [maxLogs]", "Maximum number of logs to retrieve (default: 100, max: 1000)", ) - .action(async (output, last, from, to, options) => { + .action(async (output, last, from, to, maxLogs, options) => { const { signer, chainId } = await initializeSigner(); const commands = new Commands(signer, chainId); await commands.downloadNodeLogs([ @@ -2118,7 +2322,7 @@ export async function createCLI() { options.last || last, options.from || from, options.to || to, - options.maxLogs, + options.maxLogs || maxLogs, ]); }); @@ -2223,8 +2427,14 @@ export async function createCLI() { "--token ", "Restrict to payment-token address(es), comma-separated; applied to every --chain", ) - .option("--max-price ", "Drop paid results costing more than this (human units)") - .option("--duration ", "Assumed job duration for the cost estimate") + .option( + "--max-price ", + "Drop paid results costing more than this (human units)", + ) + .option( + "--duration ", + "Assumed job duration for the cost estimate", + ) .option( "--order-by ", "Order results: price | freeCapacity | resources | leastBusy", diff --git a/src/commands.ts b/src/commands.ts index 83bee58..4903ace 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -40,6 +40,11 @@ import { getTokenDecimals, AccesslistFactory, AccessListContract, + SubsidyView, + SubsidyKind, + SubsidyPeriod, + OPFSubsidyProvider, + OneTimeSubsidyProvider, ComputeResourceRequest, ComputeSearchDimensionResult, ServiceJob, @@ -76,7 +81,13 @@ import { statusLabel, isTerminal, } from "./serviceHelpers.js"; -import { ensureP2PReady, getSearchSeedNode } from "./nodeConnection.js"; +import { + ensureP2PReady, + getSearchSeedNode, + getCurrentNodeUrl, + validateNode, + nodeSubsidyInfo, +} from "./nodeConnection.js"; import { ResourceSearchParams, ProviderEnvRow, @@ -544,7 +555,10 @@ export class Commands { ); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); - console.error(chalk.red("Error getting Policy Server Object:"), message); + console.error( + chalk.red("Error getting Policy Server Object:"), + message, + ); return; } } @@ -904,7 +918,11 @@ export class Commands { return providerInitializeComputeJob; } - public async computeStart(args: string[], paymentChainId?: number) { + public async computeStart( + args: string[], + paymentChainId?: number, + subsidyProviders?: string[], + ) { const resolved = await resolveComputeInputs( args[1], args[2], @@ -1467,6 +1485,13 @@ export class Commands { // additionalDatasets, only c2d v1 output, policiesServer, + undefined, // signal + undefined, // queueMaxWaitTime + undefined, // dockerRegistryAuth + undefined, // outputBucketId + // Consumer-selected subsidy providers (ocean.js #2160): tri-state — undefined lets + // the node use its configured defaults, [] claims with none, a list uses exactly those. + subsidyProviders, ); console.log("computeJobs: ", computeJobs); @@ -2061,6 +2086,7 @@ export class Commands { accept?: boolean; wait?: boolean; timeout?: number; + subsidyProviders?: string[]; }): Promise { try { const { chainId } = await this.signer.provider.getNetwork(); @@ -2304,6 +2330,8 @@ export class Commands { duration: opts.duration, userData, // plain object; ocean.js encrypts it to the node payment: { chainId: chainIdNum, token: opts.paymentToken }, + // Tri-state consumer-selected subsidy providers (ocean.js #2160 / #2163). + subsidyProviders: opts.subsidyProviders, }; const jobs = await ProviderInstance.serviceStart( @@ -2470,6 +2498,7 @@ export class Commands { additionalDuration: number, paymentToken?: string, accept?: boolean, + subsidyProviders?: string[], ): Promise { try { if (!Number.isInteger(additionalDuration) || additionalDuration <= 0) { @@ -2606,6 +2635,8 @@ export class Commands { additionalDuration, { chainId: chainIdNum, token }, AbortSignal.timeout(120_000), + // Tri-state consumer-selected subsidy providers (ocean.js #2160). + subsidyProviders, ); const newJob = extended?.[0]; if (!newJob) { @@ -3112,6 +3143,10 @@ export class Commands { maxLockedAmount: string, maxLockSeconds: string, maxLockCounts: string, + // Escrow v2: unix timestamp (seconds) after which the payee can no longer create/extend + // locks. Omitted/undefined → "0" (indefinite, the pre-v2 behaviour). A past timestamp + // revokes; claim and cancel are never gated by it, so funds are never stuck. + expiryTimestamp?: string, ) { try { // Neither the Escrow contract nor ocean.js rejects a zero/negative limit — @@ -3131,6 +3166,22 @@ export class Commands { } } + // expiryTimestamp is optional; when given it must be a non-negative integer (unix + // seconds). "0" is the indefinite default. A value in the past is allowed on purpose — + // that is how a revoke is expressed. + let expiry = "0"; + if (expiryTimestamp !== undefined && expiryTimestamp !== "") { + if (!/^\d+$/.test(expiryTimestamp.trim())) { + console.error( + chalk.red( + `expiryTimestamp must be a non-negative unix timestamp in seconds (got "${expiryTimestamp}"). Use 0 for indefinite.`, + ), + ); + return false; + } + expiry = expiryTimestamp.trim(); + } + const escrowAddress = requireAddress( Number(this.config.chainId), "escrow", @@ -3139,40 +3190,23 @@ export class Commands { const escrow = new EscrowContract(getAddress(escrowAddress), this.signer); - console.log("Authorizing payee..."); + // Escrow v2: authorize() always sends a transaction now — it creates OR overwrites the + // on-chain record (it no longer silently no-ops when one already exists). That is what + // makes renew / shorten / revoke reachable: re-authorizing updates the limits, and a past + // expiry revokes. So authorizeTx is always a TransactionResponse here (never null). + const expiryNote = + expiry === "0" + ? "indefinite" + : `expiry ${expiry} (${new Date(Number(expiry) * 1000).toISOString()})`; + console.log(`Authorizing payee... (${expiryNote})`); const authorizeTx = await escrow.authorize( getAddress(token), getAddress(payee), maxLockedAmount, maxLockSeconds, maxLockCounts, + expiry, ); - // ocean.js sends NO transaction when an authorization already exists for - // (payer, token, payee) — authorizeTx is null and the existing limits stay - // as they are. Say so explicitly: dereferencing null here used to surface - // as a TypeError under "Authorization failed", which reads like a chain - // error and hides the fact that the old, possibly lower, maxLockSeconds / - // maxLockedAmount / maxLockCounts are still in force. - if (!authorizeTx) { - const existing = await escrow.getAuthorizations( - getAddress(token), - await this.signer.getAddress(), - getAddress(payee), - ); - console.log( - chalk.yellow( - `Payee ${payee} is already authorized for token ${token} — the existing ` + - `authorization was left untouched (it cannot be raised or lowered here).`, - ), - ); - if (existing?.length) { - const a = existing[0]; - console.log( - ` maxLockedAmount (wei): ${a.maxLockedAmount} maxLockSeconds: ${a.maxLockSeconds} maxLockCounts: ${a.maxLockCounts}`, - ); - } - return true; - } await authorizeTx.wait(); console.log(`Successfully authorized payee ${payee} for token ${token}`); @@ -3220,16 +3254,365 @@ export class Commands { decimals, ); + // Escrow v2: the auth tuple gained a 7th field, expiryTimestamp (unix seconds; 0 = + // indefinite). A legacy (v1) escrow returns a 6-field tuple, so read defensively and treat + // a missing value as indefinite. + const expiryRaw = authorization.expiryTimestamp ?? authorization[6] ?? "0"; + const expirySeconds = Number(expiryRaw); + const expiryLabel = + !expirySeconds || expirySeconds === 0 + ? "indefinite (0)" + : `${expirySeconds} (${new Date(expirySeconds * 1000).toISOString()})${ + expirySeconds * 1000 < Date.now() ? " — EXPIRED/revoked" : "" + }`; + console.log("Authorizations found:"); - console.log(`- Current Locked Amount: ${Number(currentLockedAmount)}`); + // Escrow v2: currentLockedAmount reflects only the payer's OWN locked funds (P), not any + // provider-sponsored portion (S). The sponsored bucket is read via getEscrowInfo. + console.log( + `- Current Locked Amount (payer-funded): ${Number(currentLockedAmount)}`, + ); console.log(`- Current Locks: ${authorization.currentLocks}`); console.log(`- Max locked amount: ${Number(maxLockedAmount)}`); console.log(`- Max lock seconds: ${authorization.maxLockSeconds}`); console.log(`- Max lock counts: ${authorization.maxLockCounts}`); + console.log(`- Expiry: ${expiryLabel}`); return authorizations; } + /** + * Read-only Escrow v2 diagnostics for the active (or --chainId) chain. Uses ERC-165 capability + * discovery so it also prints something sensible against a legacy (pre-v2) escrow. When a token + * is supplied it additionally prints, for a lock-subsidy escrow, the sponsored bucket total and + * the signer's reclaimable amount, and for an enterprise escrow, the token's fee gate. + */ + public async getEscrowInfo(token?: string) { + const chainId = Number(this.config.chainId); + const escrowAddress = requireAddress(chainId, "escrow", "Escrow"); + const escrow = new EscrowContract( + getAddress(escrowAddress), + this.signer, + chainId, + ); + + console.log(`Escrow ${escrowAddress} (chain ${chainId}):`); + + // version() / escrowKind() only exist on a v2 escrow — a legacy escrow reverts or lacks the + // method, which surfaces as a thrown error; catch and report "legacy (pre-v2)". + let isV2 = true; + try { + const version = await escrow.version(); + console.log(`- Version: ${version}`); + } catch { + isV2 = false; + console.log("- Version: legacy (pre-v2, no on-chain version())"); + } + + if (isV2) { + try { + const kind = await escrow.escrowKind(); + console.log( + `- Kind: ${kind === 1 ? "ENTERPRISE (fee-gated)" : "COMMUNITY (permissionless)"}`, + ); + } catch { + /* escrowKind not available — ignore */ + } + } + + // supportsInterface() is safe against a legacy escrow (returns false on revert). + const lockSubsidy = await escrow.isEscrowLockSubsidy(); + const enterprise = await escrow.isEscrowEnterprise(); + console.log(`- Lock-time sponsorship (IEscrowLockSubsidy): ${lockSubsidy}`); + console.log(`- Enterprise (IEscrowEnterprise): ${enterprise}`); + + if (lockSubsidy) { + try { + const max = await escrow.maxSponsorsPerLock(); + console.log(`- Max sponsors per lock: ${max}`); + } catch { + /* ignore */ + } + } + + if (!token) { + if (lockSubsidy || enterprise) { + console.log( + chalk.gray( + "Pass a token address to also see the sponsored bucket / fee gate for that token.", + ), + ); + } + return; + } + + const tokenAddress = getAddress(token); + + if (lockSubsidy) { + try { + const sponsoredTotal = await escrow.getSponsoredTotal(tokenAddress); + const reclaimable = await escrow.getReclaimable( + await this.signer.getAddress(), + tokenAddress, + ); + console.log(`- Sponsored bucket total (${token}): ${sponsoredTotal}`); + console.log(`- Your reclaimable (${token}): ${reclaimable}`); + } catch (error) { + console.error(chalk.yellow("Could not read sponsorship state:"), error); + } + } + + if (enterprise) { + try { + const collector = await escrow.feeCollector(); + const allowed = await escrow.isTokenAllowed(tokenAddress); + console.log(`- Fee collector: ${collector}`); + console.log(`- Token allowed (${token}): ${allowed}`); + } catch (error) { + console.error( + chalk.yellow("Could not read enterprise fee gate:"), + error, + ); + } + } + } + + // Map the user's --jobType to the on-chain JobType enum (NONE=0, COMPUTE=1, SERVICE=2, + // fixed by the escrow claim ABI). Accepts a name or a raw number; throws on anything else. + private mapJobType(jt?: string): number | undefined { + if (jt === undefined) return undefined; + const s = jt.trim().toLowerCase(); + if (s === "compute") return 1; + if (s === "service") return 2; + if (s === "none") return 0; + if (/^\d+$/.test(s)) return Number(s); + throw new Error( + `Unknown --jobType '${jt}' (use compute | service | none, or a number).`, + ); + } + + /** + * Resolve the subsidy-provider contract addresses to query. The Ocean Node is the single + * source of truth: a node may not support subsidies at all, and the lib's bundled contract + * addresses could list a provider the node will never claim against — so addresses come only + * from the node's status `subsidyProviders[chainId]`. `--subsidy` merely narrows to a subset + * of what the node advertises. Never reads config / ADDRESS_FILE. + */ + private resolveSubsidyProviderAddresses( + chainId: number, + nodeProviders: Record, + override?: string, + ): string[] { + const fromNode = (nodeProviders?.[String(chainId)] ?? []).map((a) => + getAddress(a), + ); + if (fromNode.length === 0) { + throw new Error( + `This node does not support subsidy providers on chain ${chainId}.`, + ); + } + if (!override || !override.trim()) return [...new Set(fromNode)]; + const wanted = override.split(",").map((a) => getAddress(a.trim())); + const nodeSet = new Set(fromNode); + const kept = wanted.filter((a) => nodeSet.has(a)); + const dropped = wanted.filter((a) => !nodeSet.has(a)); + if (dropped.length) { + console.warn( + chalk.yellow( + `Ignoring --subsidy address(es) the node does not advertise: ${dropped.join( + ", ", + )}`, + ), + ); + } + if (kept.length === 0) { + throw new Error( + `None of the --subsidy addresses are advertised by the node on chain ${chainId}.`, + ); + } + return [...new Set(kept)]; + } + + /** + * Read-only, provider-agnostic subsidy report (ocean.js #2163 / contracts #1052). Discovers + * the provider contracts from the current node, then for each one uses the `SubsidyView` base + * wrapper (ERC-165) to print a unified report — kind, per-window buckets (limit/used/remaining), + * the claimable amount now, contract balance and eligibility — plus kind-specific detail for + * rolling-window (OPF) and one-time providers. Answers "do I have credit available?". + */ + public async getSubsidyStatus( + token: string, + opts: { + subsidy?: string; + node?: string; + jobType?: string; + amount?: string; + } = {}, + ): Promise { + const chainId = Number(this.config.chainId); + const payer = await this.signer.getAddress(); + const tokenAddress = getAddress(token); + + // Provider addresses come from the node, never from bundled config. + const nodeUrl = getCurrentNodeUrl(); + const status = nodeUrl ? await validateNode(nodeUrl) : null; + if (!status) { + console.error( + chalk.red( + "Could not read node status (no reachable Ocean Node). The node advertises which subsidy providers it supports.", + ), + ); + return; + } + const { providers } = nodeSubsidyInfo(status); + + let addresses: string[]; + let jobType: number | undefined; + try { + addresses = this.resolveSubsidyProviderAddresses( + chainId, + providers, + opts.subsidy, + ); + jobType = this.mapJobType(opts.jobType); + } catch (e) { + console.error(chalk.red((e as Error).message)); + return; + } + + for (const address of addresses) { + console.log( + chalk.cyan(`\n=== Subsidy provider ${address} (chain ${chainId}) ===`), + ); + try { + const view = new SubsidyView(this.signer, address, chainId); + // The node already vouches these are subsidy providers, so we don't gate on + // `isSubsidyView()` (ERC-165) — it can under-report. `subsidyKind()` is the + // liveness probe: if it reverts, the address isn't a readable subsidy view. + let kind: SubsidyKind; + try { + kind = await view.subsidyKind(); + } catch { + console.warn( + chalk.yellow( + ` ${address} is not a readable subsidy provider (subsidyKind() reverted) — skipping.`, + ), + ); + continue; + } + console.log(` Kind: ${SubsidyKind[kind] ?? kind}`); + + const report = await view.subsidyBuckets(payer, tokenAddress); + console.log(` Paused: ${report.paused}`); + console.log(` User allowed: ${report.userAllowed}`); + console.log(` Token enabled: ${report.tokenEnabled}`); + if (!report.buckets || report.buckets.length === 0) { + console.log(" Buckets: (none)"); + } else { + console.log(" Buckets:"); + for (const b of report.buckets) { + const period = SubsidyPeriod[b.period] ?? b.period; + const cap = b.unlimited ? "unlimited" : b.limit; + const reset = + b.resetsAt && b.resetsAt !== "0" ? ` resetsAt=${b.resetsAt}` : ""; + console.log( + ` - ${period}: limit=${cap} used=${b.used} remaining=${b.remaining}${reset}`, + ); + } + } + + console.log( + ` Remaining subsidy (claimable now): ${await view.remainingSubsidy( + payer, + tokenAddress, + )}`, + ); + console.log( + ` Contract available balance: ${await view.availableBalance( + tokenAddress, + )}`, + ); + console.log( + ` User allowed (isUserAllowed): ${await view.isUserAllowed(payer)}`, + ); + if (opts.node) { + console.log( + ` Node allowed (isNodeAllowed): ${await view.isNodeAllowed( + getAddress(opts.node), + )}`, + ); + } + if (jobType !== undefined) { + console.log( + ` Job type subsidized (${opts.jobType}): ${await view.isJobTypeSubsidized( + jobType, + )}`, + ); + } + + // Optional quote — needs a node address, a job type and an amount. + if (opts.amount && opts.node && jobType !== undefined) { + const quote = await view.quoteSubsidy( + getAddress(opts.node), + payer, + jobType, + tokenAddress, + opts.amount, + opts.amount, // subsidyNeeded: request the full amount; the contract caps it + ); + console.log( + ` Quote for amount ${opts.amount}: subsidy=${quote.subsidy} bonus=${quote.bonus}`, + ); + } + + // Kind-specific detail (subsidyBuckets already covers the core numbers). + if (kind === SubsidyKind.ROLLING_WINDOW) { + const opf = new OPFSubsidyProvider(this.signer, address, chainId); + console.log(" Rolling-window detail:"); + console.log( + ` remaining daily/weekly/monthly: ${await opf.remainingDaily( + payer, + tokenAddress, + )} / ${await opf.remainingWeekly( + payer, + tokenAddress, + )} / ${await opf.remainingMonthly(payer, tokenAddress)}`, + ); + console.log( + ` used daily/weekly/monthly: ${await opf.dailyUsedBy( + payer, + tokenAddress, + )} / ${await opf.weeklyUsedBy( + payer, + tokenAddress, + )} / ${await opf.monthlyUsedBy(payer, tokenAddress)}`, + ); + } else if (kind === SubsidyKind.ONE_TIME) { + const one = new OneTimeSubsidyProvider(this.signer, address, chainId); + const cfg = await one.getTokenConfig(tokenAddress); + console.log(" One-time detail:"); + console.log( + ` token config: pctBps=${cfg.pctBps} defaultCredit=${cfg.defaultCredit} enabled=${cfg.enabled}`, + ); + console.log( + ` effective/remaining/used credit: ${await one.effectiveCredit( + payer, + tokenAddress, + )} / ${await one.remainingCredit( + payer, + tokenAddress, + )} / ${await one.usedBy(payer, tokenAddress)}`, + ); + } + } catch (error) { + console.error( + chalk.red(` Error reading ${address}:`), + (error as Error).message ?? error, + ); + } + } + } + public async createAccessList(args: string[]): Promise { try { const name = args[0]; diff --git a/src/helpers.ts b/src/helpers.ts index 2054c5d..bfbde3f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -707,3 +707,21 @@ export function summarizeComputeEnvFees(env: { return `${header}: pays on\n${lines.join("\n")}`; } + +/** + * Parse the `--subsidyProviders` CLI value into the tri-state the Ocean Node expects + * (ocean-node #1485, ocean.js #2160): + * - flag omitted -> `undefined` (the node uses its configured defaults) + * - `none` / empty string -> `[]` (explicitly claim with NO subsidy providers) + * - a CSV of addresses -> `string[]` (use exactly these, EIP-55 normalized) + * Preserving the `undefined` vs `[]` distinction is essential — the ocean.js request + * body is truthy-guarded (`if (subsidyProviders)`), and `[]` is truthy, so an empty + * array is transmitted while `undefined` is omitted. + * Throws on a malformed address. + */ +export function parseSubsidyProviders(raw?: string): string[] | undefined { + if (raw === undefined) return undefined; + const trimmed = raw.trim(); + if (trimmed === "" || trimmed.toLowerCase() === "none") return []; + return trimmed.split(",").map((a) => ethers.getAddress(a.trim())); +} diff --git a/src/nodeConnection.ts b/src/nodeConnection.ts index 2ffde3d..81d3c08 100644 --- a/src/nodeConnection.ts +++ b/src/nodeConnection.ts @@ -268,3 +268,28 @@ export function nodeChainIds(status: NodeStatus): string[] { ]; return [...new Set(ids)]; } + +/** + * Subsidy-provider info the node advertises in its status (ocean-node #1479/#1485): + * `subsidyProviders` is a per-chain map (chainId -> checksummed contract addresses) and + * `subsidyProviderFilter` tells whether the node restricts user-supplied providers to that + * configured set. The node is the single source of truth for which providers are usable — + * a node may not support subsidies at all, in which case `providers` is `{}`. + * + * ocean.js's `NodeStatus` type may not (yet) declare these fields, but the node returns + * them at runtime, so we read them through a local narrowing cast rather than depend on + * the lib type. Older nodes that omit them yield `{}` / `false`. + */ +export function nodeSubsidyInfo(status: NodeStatus): { + providers: Record; + filter: boolean; +} { + const s = status as unknown as { + subsidyProviders?: Record; + subsidyProviderFilter?: boolean; + }; + return { + providers: s.subsidyProviders ?? {}, + filter: Boolean(s.subsidyProviderFilter), + }; +} diff --git a/test/escrow.test.ts b/test/escrow.test.ts index 2582225..e5bffcb 100644 --- a/test/escrow.test.ts +++ b/test/escrow.test.ts @@ -137,5 +137,56 @@ describe("Ocean CLI Escrow", function () { expect(output).to.include("Max locked amount: 1"); expect(output).to.include("Max lock seconds: 3600"); expect(output).to.include("Max lock counts: 10"); + // Escrow v2: the authorization now carries an expiry; the one set above was indefinite. + expect(output).to.include("Expiry:"); + }); + + it("should re-authorize a payee with an expiry (Escrow v2 overwrite)", async function () { + // A future expiry. authorize() in Escrow v2 overwrites the existing record instead of + // no-op'ing, so this renews the authorization with a bounded expiry. + const expiry = Math.floor(Date.now() / 1000) + 3600; + + const output = await runCommand( + `npm run cli -- authorizeEscrow ${tokenAddress} ${payee.address} 1 3600 10 --expiry ${expiry}`, + ); + expect(output).to.include("Authorization successful"); + + const info = await runCommand( + `npm run cli getAuthorizationsEscrow ${tokenAddress} ${payee.address}`, + ); + // Expiry is now a concrete timestamp, not "indefinite (0)". + expect(info).to.include("Expiry:"); + expect(info).to.not.include("indefinite (0)"); + }); + + it("should revoke a payee with a past expiry (Escrow v2)", async function () { + // A timestamp in the past revokes: the payee can no longer create new locks, but existing + // locks stay claimable/cancellable. + const output = await runCommand( + `npm run cli -- authorizeEscrow ${tokenAddress} ${payee.address} 1 3600 10 --expiry 1`, + ); + expect(output).to.include("Authorization successful"); + + const info = await runCommand( + `npm run cli getAuthorizationsEscrow ${tokenAddress} ${payee.address}`, + ); + expect(info).to.include("EXPIRED/revoked"); + }); + + it("should reject a non-numeric expiry", async function () { + const output = await runCommand( + `npm run cli -- authorizeEscrow ${tokenAddress} ${payee.address} 1 3600 10 --expiry not-a-timestamp`, + ); + expect(output).to.include("Authorization failed"); + }); + + it("should report escrow capabilities via getEscrowInfo", async function () { + const output = await runCommand( + `npm run cli getEscrowInfo ${tokenAddress}`, + ); + // Against the Escrow v2 deployment this prints the version and the ERC-165 capability flags. + expect(output).to.include("Version:"); + expect(output).to.include("Lock-time sponsorship (IEscrowLockSubsidy):"); + expect(output).to.include("Enterprise (IEscrowEnterprise):"); }); }); diff --git a/test/subsidyProviders.test.ts b/test/subsidyProviders.test.ts new file mode 100644 index 0000000..eba4035 --- /dev/null +++ b/test/subsidyProviders.test.ts @@ -0,0 +1,40 @@ +import { expect } from "chai"; +import { parseSubsidyProviders } from "../src/helpers.js"; + +// Pure unit test — no infra needed. Verifies the tri-state parsing of the +// `--subsidyProviders` CLI value and EIP-55 address normalization. +describe("parseSubsidyProviders", () => { + it("returns undefined when the flag is omitted (node default)", () => { + expect(parseSubsidyProviders(undefined)).to.equal(undefined); + }); + + it("returns [] for 'none' (explicitly no subsidy)", () => { + expect(parseSubsidyProviders("none")).to.deep.equal([]); + expect(parseSubsidyProviders("NONE")).to.deep.equal([]); + }); + + it("returns [] for an empty / whitespace string", () => { + expect(parseSubsidyProviders("")).to.deep.equal([]); + expect(parseSubsidyProviders(" ")).to.deep.equal([]); + }); + + it("parses a single address", () => { + const a = "0x4344d4bc29531db736378e9a3da85bf1eff0cb22"; + expect(parseSubsidyProviders(a)).to.deep.equal([ + "0x4344D4Bc29531DB736378e9A3dA85BF1eff0CB22", + ]); + }); + + it("parses and normalizes a comma-separated list, trimming whitespace", () => { + const raw = + " 0x4344d4bc29531db736378e9a3da85bf1eff0cb22 , 0x0000000000000000000000000000000000000001 "; + expect(parseSubsidyProviders(raw)).to.deep.equal([ + "0x4344D4Bc29531DB736378e9A3dA85BF1eff0CB22", + "0x0000000000000000000000000000000000000001", + ]); + }); + + it("throws on a malformed address", () => { + expect(() => parseSubsidyProviders("0xnot-an-address")).to.throw(); + }); +});