diff --git a/package-lock.json b/package-lock.json index b4fcd9c..5ba71e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.3.1", - "@oceanprotocol/ddo-js": "^0.1.0", - "@oceanprotocol/lib": "^4.1.4", + "@oceanprotocol/ddo-js": "^0.1.1", + "@oceanprotocol/lib": "^4.2.0", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", @@ -3561,9 +3561,10 @@ "license": "Apache-2.0" }, "node_modules/@oceanprotocol/ddo-js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/ddo-js/-/ddo-js-0.1.0.tgz", - "integrity": "sha512-dQmwKZiq6/D7mIDWQVsCuZzMtkWPKHYL7fVM9xk5qXByka/aieziVBLP+L2ObfYZFX77aOJLR8Bj4q5kpGwd/Q==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@oceanprotocol/ddo-js/-/ddo-js-0.1.1.tgz", + "integrity": "sha512-RsDUiWfPjylj/xqk4HtUr3qzYzurlBLd2/YW/oP9vC7Gh0uGgJsCShu+ao7hC0Xid4y0iORhkSgiaCtshVGchQ==", + "license": "Apache-2.0", "dependencies": { "@rdfjs/formats-common": "^3.1.0", "@types/rdfjs__formats-common": "^3.1.5", @@ -3626,10 +3627,24 @@ "node": ">= 14.16" } }, + "node_modules/@oceanprotocol/ddo-js/node_modules/rdf-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-2.0.0.tgz", + "integrity": "sha512-jlQ+h7EvnXmncmk8OzOYR8T3gNfd4g0LQXbflHkEkancic8dh0Tdt5RiRq8vUFndjIeNHt1RWeA5TAj6rgrtng==", + "license": "MIT", + "dependencies": { + "rdf-data-factory": "^2.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/rubensworks/" + } + }, "node_modules/@oceanprotocol/lib": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-4.1.4.tgz", - "integrity": "sha512-AGZi7/YckwxmoN0dyK+iGl58vr8VvQqk9dG6cU2x9nSmJDD1xcgFOinpV2fqw+6pohjjEvLAr+DgzJpdh7PJaw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-4.2.0.tgz", + "integrity": "sha512-UMWWw8UAugC2sZ5EO2fz8Jh1BUbeBaSS8sapjTd67QE10FJyJRdCX0ce0oc9FZOLDgQX0n9jal8/Fc5rNbhPpQ==", + "license": "Apache-2.0", "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.3.0", @@ -13308,18 +13323,6 @@ "readable-stream": "3 - 4" } }, - "node_modules/rdf-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-2.0.0.tgz", - "integrity": "sha512-jlQ+h7EvnXmncmk8OzOYR8T3gNfd4g0LQXbflHkEkancic8dh0Tdt5RiRq8vUFndjIeNHt1RWeA5TAj6rgrtng==", - "dependencies": { - "rdf-data-factory": "^2.0.0" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/rubensworks/" - } - }, "node_modules/rdf-validate-datatype": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/rdf-validate-datatype/-/rdf-validate-datatype-0.2.2.tgz", @@ -18784,9 +18787,9 @@ "integrity": "sha512-43Vz+DJfoNZLteg91sYvNDR5tJLWcAXl78VoSTriw38j81p6FSLcPNCHzPZKwX9FxyMk3uQe9U4u9REUBEQUfw==" }, "@oceanprotocol/ddo-js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/ddo-js/-/ddo-js-0.1.0.tgz", - "integrity": "sha512-dQmwKZiq6/D7mIDWQVsCuZzMtkWPKHYL7fVM9xk5qXByka/aieziVBLP+L2ObfYZFX77aOJLR8Bj4q5kpGwd/Q==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@oceanprotocol/ddo-js/-/ddo-js-0.1.1.tgz", + "integrity": "sha512-RsDUiWfPjylj/xqk4HtUr3qzYzurlBLd2/YW/oP9vC7Gh0uGgJsCShu+ao7hC0Xid4y0iORhkSgiaCtshVGchQ==", "requires": { "@rdfjs/formats-common": "^3.1.0", "@types/rdfjs__formats-common": "^3.1.5", @@ -18833,13 +18836,21 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==" + }, + "rdf-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-2.0.0.tgz", + "integrity": "sha512-jlQ+h7EvnXmncmk8OzOYR8T3gNfd4g0LQXbflHkEkancic8dh0Tdt5RiRq8vUFndjIeNHt1RWeA5TAj6rgrtng==", + "requires": { + "rdf-data-factory": "^2.0.0" + } } } }, "@oceanprotocol/lib": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-4.1.4.tgz", - "integrity": "sha512-AGZi7/YckwxmoN0dyK+iGl58vr8VvQqk9dG6cU2x9nSmJDD1xcgFOinpV2fqw+6pohjjEvLAr+DgzJpdh7PJaw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-4.2.0.tgz", + "integrity": "sha512-UMWWw8UAugC2sZ5EO2fz8Jh1BUbeBaSS8sapjTd67QE10FJyJRdCX0ce0oc9FZOLDgQX0n9jal8/Fc5rNbhPpQ==", "requires": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.3.0", @@ -25863,14 +25874,6 @@ "readable-stream": "3 - 4" } }, - "rdf-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-2.0.0.tgz", - "integrity": "sha512-jlQ+h7EvnXmncmk8OzOYR8T3gNfd4g0LQXbflHkEkancic8dh0Tdt5RiRq8vUFndjIeNHt1RWeA5TAj6rgrtng==", - "requires": { - "rdf-data-factory": "^2.0.0" - } - }, "rdf-validate-datatype": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/rdf-validate-datatype/-/rdf-validate-datatype-0.2.2.tgz", diff --git a/package.json b/package.json index ff1ff74..8ac4319 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.3.1", - "@oceanprotocol/ddo-js": "^0.1.0", - "@oceanprotocol/lib": "^4.1.4", + "@oceanprotocol/ddo-js": "^0.1.1", + "@oceanprotocol/lib": "^4.2.0", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", diff --git a/src/cli.ts b/src/cli.ts index 7bfbb78..1305947 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,10 +8,10 @@ import { unitsToAmount } from '@oceanprotocol/lib'; import { toBoolean } from './helpers.js'; async function initializeSigner() { - + const provider = new ethers.providers.JsonRpcProvider(process.env.RPC); let signer; - + if (process.env.PRIVATE_KEY) { signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); } else { @@ -38,7 +38,7 @@ export async function createCLI() { console.error(chalk.red("Have you forgot to set env NODE_URL?")); process.exit(1); } - + const program = new Command(); program @@ -203,7 +203,7 @@ export async function createCLI() { const jobDuration = options.maxJobDuration || maxJobDuration; const token = options.token || paymentToken; const res = options.resources || resources; - if (!dsDids || !aDid ||!envId || !jobDuration || !token || !res) { + if (!dsDids || !aDid || !envId || !jobDuration || !token || !res) { console.error(chalk.red('Missing required arguments')); // process.exit(1); return @@ -244,7 +244,7 @@ export async function createCLI() { await commands.computeStart(computeArgs); console.log(chalk.green('Compute job started successfully.')); - }); + }); // startFreeCompute command program @@ -315,7 +315,7 @@ export async function createCLI() { } const { signer, chainId } = await initializeSigner(); const commands = new Commands(signer, chainId); - const args = [null, dsDid, jId]; + const args = [null, dsDid, jId]; if (agrId) args.push(agrId); await commands.computeStop(args); }); @@ -369,5 +369,127 @@ export async function createCLI() { await commands.mintOceanTokens(); }); + // Generate new auth token + program + .command('generateAuthToken') + .description('Generate new auth token') + .action(async () => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.generateAuthToken(); + }); + + + // Invalidate auth token + program + .command('invalidateAuthToken') + .description('Invalidate auth token') + .argument('', 'Auth token') + .option('-t, --token ', 'Auth token') + .action(async (token, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.invalidateAuthToken([token || options.token]); + }); + + // Escrow deposit command + program + .command('depositEscrow') + .description('Deposit tokens into the escrow contract') + .argument('', 'Address of the token to deposit') + .argument('', 'Amount of tokens to deposit') + .option('-t, --token ', 'Address of the token to deposit') + .option('-a, --amount ', 'Amount of tokens to deposit') + .action(async (token, amount, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + const tokenAddress = options.token || token; + const amountToDeposit = options.amount || amount; + const success = await commands.depositToEscrow(signer, tokenAddress, amountToDeposit, chainId); + if (!success) { + console.log(chalk.red('Deposit failed')); + return; + } + + console.log(chalk.green('Deposit successful')); + }); + + // Check escrow deposited balance + program + .command('getUserFundsEscrow') + .description('Get deposited token amount in escrow for user') + .argument('', 'Address of the token to check') + .option('-t, --token ', 'Address of the token to check') + .action(async (token, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.getEscrowBalance(token || options.token); + }); + + // Withdraw from escrow + program + .command('withdrawFromEscrow') + .description('Withdraw tokens from escrow') + .argument('', 'Address of the token to check') + .argument('', 'Amount of tokens to withdraw') + .option('-t, --token ', 'Address of the token to check') + .option('-a, --amount ', 'Amount of tokens to withdraw') + .action(async (token, amount, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.withdrawFromEscrow(token || options.token, amount); + }); + + // Escrow authorization command + program + .command('authorizeEscrow') + .description('Authorize a payee to lock and claim funds from escrow') + .argument('', 'Address of the token to authorize') + .argument('', 'Address of the payee to authorize') + .argument('', 'Maximum amount that can be locked by payee') + .argument('', 'Maximum lock duration in seconds') + .argument('', 'Maximum number of locks allowed') + .option('-t, --token ', 'Address of the token to authorize') + .option('-p, --payee ', 'Address of the payee to authorize') + .option('-m, --maxLockedAmount ', 'Maximum amount that can be locked by payee') + .option('-s, --maxLockSeconds ', 'Maximum lock duration in seconds') + .option('-c, --maxLockCounts ', 'Maximum number of locks allowed') + .action(async (token, payee, maxLockedAmount, maxLockSeconds, maxLockCounts, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + const tokenAddress = options.token || token; + const payeeAddress = options.payee || payee; + const maxLockedAmountValue = options.maxLockedAmount || maxLockedAmount; + const maxLockSecondsValue = options.maxLockSeconds || maxLockSeconds; + const maxLockCountsValue = options.maxLockCounts || maxLockCounts; + + const success = await commands.authorizeEscrowPayee( + tokenAddress, + payeeAddress, + maxLockedAmountValue, + maxLockSecondsValue, + maxLockCountsValue, + ); + + if (!success) { + console.log(chalk.red('Authorization failed')); + return; + } + + console.log(chalk.green('Authorization successful')); + }); + + program + .command('getAuthorizationsEscrow') + .description('Get authorizations for escrow') + .argument('', 'Address of the token to check') + .argument('', 'Address of the payee to check') + .option('-t, --token ', 'Address of the token to check') + .action(async (token, payee, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.getAuthorizationsEscrow(token || options.token, payee || options.payee); + }); + return program; } \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 80c251d..d75e406 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -9,7 +9,8 @@ import { getMetadataURI, getIndexingWaitSettings, IndexerWaitParams, - fixAndParseProviderFees + fixAndParseProviderFees, + getConfigByChainId } from "./helpers.js"; import { Aquarius, @@ -25,7 +26,8 @@ import { orderAsset, sendTx, unitsToAmount, - EscrowContract + EscrowContract, + getTokenDecimals } from "@oceanprotocol/lib"; import { Asset } from '@oceanprotocol/ddo-js'; import { Signer, ethers } from "ethers"; @@ -366,20 +368,20 @@ export class Commands { if (!maxJobDuration) { console.error( "Error initializing Provider for the compute job using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because maxJobDuration was not provided." + args[1] + + " and algorithm DID " + + args[2] + + " because maxJobDuration was not provided." ); return; } if (maxJobDuration < 0) { console.error( "Error initializing Provider for the compute job using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because maxJobDuration is less than 0. It should be in seconds." + args[1] + + " and algorithm DID " + + args[2] + + " because maxJobDuration is less than 0. It should be in seconds." ); return; } @@ -391,10 +393,10 @@ export class Commands { if (!paymentToken) { console.error( "Error initializing Provider for the compute job using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because paymentToken was not provided." + args[1] + + " and algorithm DID " + + args[2] + + " because paymentToken was not provided." ); return; } @@ -402,13 +404,13 @@ export class Commands { if (!Object.keys(computeEnv.fees).includes(chainId.toString())) { console.error( "Error starting paid compute using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because chainId is not supported by compute environment. " + - args[3] + - ". Supported chain IDs: " + - computeEnv.fees.keys() + args[1] + + " and algorithm DID " + + args[2] + + " because chainId is not supported by compute environment. " + + args[3] + + ". Supported chain IDs: " + + computeEnv.fees.keys() ); return; } @@ -422,11 +424,11 @@ export class Commands { if (found === false) { console.error( "Error initializing Provider for the compute job using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because paymentToken is not supported by this environment " + - args[3] + args[1] + + " and algorithm DID " + + args[2] + + " because paymentToken is not supported by this environment " + + args[3] ); return; } @@ -434,10 +436,10 @@ export class Commands { if (!resources) { console.error( "Error initializing Provider for the compute job using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because resources for compute were not provided." + args[1] + + " and algorithm DID " + + args[2] + + " because resources for compute were not provided." ); return; } @@ -459,9 +461,9 @@ export class Commands { ) { console.error( "Error initializing Provider for the compute job using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + args[1] + + " and algorithm DID " + + args[2] ); return; } @@ -644,20 +646,20 @@ export class Commands { if (!maxJobDuration) { console.error( "Error initializing Provider for the compute job using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because maxJobDuration was not provided." + args[1] + + " and algorithm DID " + + args[2] + + " because maxJobDuration was not provided." ); return; } if (maxJobDuration < 0) { console.error( "Error starting paid compute using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because maxJobDuration is less than 0. It should be in seconds." + args[1] + + " and algorithm DID " + + args[2] + + " because maxJobDuration is less than 0. It should be in seconds." ); return; } @@ -670,23 +672,23 @@ export class Commands { if (!paymentToken) { console.error( "Error starting paid compute using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because paymentToken was not provided." + args[1] + + " and algorithm DID " + + args[2] + + " because paymentToken was not provided." ); return; } if (!Object.keys(computeEnv.fees).includes(chainId.toString())) { console.error( "Error starting paid compute using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because chainId is not supported by compute environment. " + - args[3] + - ". Supported chain IDs: " + - computeEnv.fees.keys() + args[1] + + " and algorithm DID " + + args[2] + + " because chainId is not supported by compute environment. " + + args[3] + + ". Supported chain IDs: " + + computeEnv.fees.keys() ); return; } @@ -700,11 +702,11 @@ export class Commands { if (found === false) { console.error( "Error starting paid compute using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because paymentToken is not supported by this environment " + - args[3] + args[1] + + " and algorithm DID " + + args[2] + + " because paymentToken is not supported by this environment " + + args[3] ); return; } @@ -712,10 +714,10 @@ export class Commands { if (!resources) { console.error( "Error starting paid compute using dataset DID " + - args[1] + - " and algorithm DID " + - args[2] + - " because resources for compute were not provided." + args[1] + + " and algorithm DID " + + args[2] + + " because resources for compute were not provided." ); return; } @@ -773,7 +775,7 @@ export class Commands { const output: ComputeOutput = { metadataUri: await getMetadataURI(), }; - + const computeJobs = await ProviderInstance.computeStart( providerURI, this.signer, @@ -1251,37 +1253,207 @@ export class Commands { } public async mintOceanTokens() { - const minAbi = [ - { - constant: false, - inputs: [ - { name: "to", type: "address" }, - { name: "value", type: "uint256" }, - ], - name: "mint", - outputs: [{ name: "", type: "bool" }], - payable: false, - stateMutability: "nonpayable", - type: "function", - }, - ]; - - const tokenContract = new ethers.Contract( - this.config.oceanTokenAddress, - minAbi, - this.signer + try { + const config = await getConfigByChainId(Number(this.config.chainId)); + const minAbi = [ + { + constant: false, + inputs: [ + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + ], + name: "mint", + outputs: [{ name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + ]; + + const tokenContract = new ethers.Contract( + config?.Ocean, + minAbi, + this.signer + ); + const estGasPublisher = await tokenContract.estimateGas.mint( + await this.signer.getAddress(), + await amountToUnits(null, null, "1000", 18) + ); + const tx = await sendTx( + estGasPublisher, + this.signer, + 1, + tokenContract.mint, + await this.signer.getAddress(), + amountToUnits(null, null, "1000", 18) + ); + await tx.wait(); + } catch (error) { + console.error("Error minting Ocean tokens:", error); + } + } + + public async generateAuthToken() { + const authToken = await ProviderInstance.generateAuthToken( + this.signer, + this.oceanNodeUrl, + ); + console.log(`Auth token successfully generated: ${authToken}`); + } + + public async invalidateAuthToken(args: string[]) { + const authToken = args[0]; + const result = await ProviderInstance.invalidateAuthToken( + this.signer, + authToken, + this.oceanNodeUrl, ); - const estGasPublisher = await tokenContract.estimateGas.mint( - this.signer.getAddress(), - amountToUnits(null, null, "1000", 18) + if (!result.success) { + console.log('Auth token could not be invalidated'); + return; + } + + console.log(`Auth token successfully invalidated`); + } + + public async getEscrowBalance(token: string): Promise { + const config = await getConfigByChainId(Number(this.config.chainId)); + const escrow = new EscrowContract( + ethers.utils.getAddress(config.Escrow), + this.signer, + Number(this.config.chainId) ); - await sendTx( - estGasPublisher, + + try { + const balance = await escrow.getUserFunds(await this.signer.getAddress(), token); + const decimals = await getTokenDecimals(this.signer, token); + const available = balance.available; + const amount = await unitsToAmount(this.signer, token, available, decimals); + console.log(`Escrow user funds for token ${token}: ${amount}`); + return Number(amount); + } catch (error) { + console.error("Error getting escrow balance:", error); + } + } + + public async withdrawFromEscrow(token: string, amount: string): Promise { + const config = await getConfigByChainId(Number(this.config.chainId)); + const escrow = new EscrowContract( + ethers.utils.getAddress(config.Escrow), this.signer, - 1, - tokenContract.mint, - await this.signer.getAddress(), - amountToUnits(null, null, "1000", 18) + Number(this.config.chainId) + ); + + const balance = await this.getEscrowBalance(token); + if (balance < Number(amount)) { + console.error(`Insufficient balance in escrow for token ${token}`); + return; + } + + const withdrawTx = await escrow.withdraw([token], [amount]); + await withdrawTx.wait(); + console.log(`Successfully withdrawn ${amount} ${token} from escrow`); + } + + public async depositToEscrow(signer: Signer, token: string, amount: string, chainId: number) { + try { + const amountInUnits = await amountToUnits(signer, token, amount, 18); + const config = await getConfigByChainId(chainId); + const escrowAddress = config.Escrow; + + const tokenContract = new ethers.Contract( + token, + ['function approve(address spender, uint256 amount) returns (bool)'], + signer + ); + + const escrow = new EscrowContract( + ethers.utils.getAddress(escrowAddress), + signer, + chainId + ); + + console.log('Approving token transfer...') + const approveTx = await tokenContract.approve(escrowAddress, amountInUnits); + await approveTx.wait(); + console.log(`Successfully approved ${amount} ${token} to escrow`); + + + console.log('Depositing to escrow...') + const depositTx = await escrow.deposit(token, amount); + await depositTx.wait(); + return true; + + } catch (error) { + console.error("Error depositing to escrow:", error); + return false; + } + } + + public async authorizeEscrowPayee( + token: string, + payee: string, + maxLockedAmount: string, + maxLockSeconds: string, + maxLockCounts: string + ) { + try { + const config = await getConfigByChainId(Number(this.config.chainId)); + const escrowAddress = config.Escrow; + + const escrow = new EscrowContract( + ethers.utils.getAddress(escrowAddress), + this.signer + ); + + console.log("Authorizing payee..."); + const authorizeTx = await escrow.authorize( + ethers.utils.getAddress(token), + ethers.utils.getAddress(payee), + maxLockedAmount, + maxLockSeconds, + maxLockCounts + ); + await authorizeTx.wait(); + console.log(`Successfully authorized payee ${payee} for token ${token}`); + + return true; + } catch (error) { + console.error("Error authorizing payee:", error); + return false; + } + } + + public async getAuthorizationsEscrow(token: string, payee: string) { + const config = await getConfigByChainId(Number(this.config.chainId)); + const payer = await this.signer.getAddress(); + const tokenAddress = ethers.utils.getAddress(token); + const payerAddress = ethers.utils.getAddress(payer); + const payeeAddress = ethers.utils.getAddress(payee); + const decimals = await getTokenDecimals(this.signer, token); + const escrow = new EscrowContract( + ethers.utils.getAddress(config.Escrow), + this.signer, + Number(this.config.chainId) ); + + const authorizations = await escrow.getAuthorizations(tokenAddress, payerAddress, payeeAddress); + const authorization = authorizations[0] + if (!authorization || authorization.length === 0) { + console.log('No authorizations found'); + return; + } + + const currentLockedAmount = await unitsToAmount(this.signer, token, authorization.currentLockedAmount.toString(), decimals); + const maxLockedAmount = await unitsToAmount(this.signer, token, authorization.maxLockedAmount.toString(), decimals); + + console.log('Authorizations found:') + console.log(`- Current Locked Amount: ${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}`) + + return authorizations; } } diff --git a/src/helpers.ts b/src/helpers.ts index 7cc8195..fcbf5d2 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,6 @@ import { ethers, Signer } from "ethers"; import fetch from "cross-fetch"; -import { promises as fs } from "fs"; +import { promises as fs, readFileSync } from "fs"; import * as path from "path"; import * as sapphire from '@oasisprotocol/sapphire-paratime'; import { Asset, DDO } from '@oceanprotocol/ddo-js'; @@ -24,10 +24,9 @@ import { LoggerInstance, createAsset } from "@oceanprotocol/lib"; -import { hexlify } from "ethers/lib/utils"; -import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json'; - +import { homedir } from "os"; +const ERC20Template = readFileSync('./node_modules/@oceanprotocol/contracts/artifacts/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.json', 'utf8') as any; export async function downloadFile( url: string, @@ -173,7 +172,7 @@ export async function updateAssetMetadata( else { const stringDDO = JSON.stringify(updatedDdo); const bytes = Buffer.from(stringDDO); - metadata = hexlify(bytes); + metadata = ethers.utils.hexlify(bytes); flags = 0 } @@ -382,27 +381,41 @@ export function getIndexingWaitSettings(): IndexerWaitParams { } export function fixAndParseProviderFees(rawString: string) { - // Remove surrounding quotes if present - if (rawString.startsWith('"') && rawString.endsWith('"')) { - rawString = rawString.slice(1, -1).replace(/\\"/g, '"'); - } - - const fixed = rawString - .replace(/([{,])(\s*)([a-zA-Z0-9_]+)\s*:/g, '$1"$3":') - .replace(/:\s*(did:[^,}\]]+)/g, ':"$1"') - .replace(/:\s*(0x[a-fA-F0-9]+)/g, ':"$1"') - .replace(/providerData:\s*([^,}\]]+)/g, 'providerData:"$1"') - .replace(/:false/g, ':false') - .replace(/:true/g, ':true'); - - return JSON.parse(fixed); + // Remove surrounding quotes if present + if (rawString.startsWith('"') && rawString.endsWith('"')) { + rawString = rawString.slice(1, -1).replace(/\\"/g, '"'); + } + + const fixed = rawString + .replace(/([{,])(\s*)([a-zA-Z0-9_]+)\s*:/g, '$1"$3":') + .replace(/:\s*(did:[^,}\]]+)/g, ':"$1"') + .replace(/:\s*(0x[a-fA-F0-9]+)/g, ':"$1"') + .replace(/providerData:\s*([^,}\]]+)/g, 'providerData:"$1"') + .replace(/:false/g, ':false') + .replace(/:true/g, ':true'); + + return JSON.parse(fixed); } export function toBoolean(value) { - if (typeof value === 'boolean') return value; - if (typeof value === 'string') { - const val = value.trim().toLowerCase(); - return val === 'true' || val === '1' || val === 'yes' || val === 'y'; - } - return Boolean(value); + if (typeof value === 'boolean') return value; + if (typeof value === 'string') { + const val = value.trim().toLowerCase(); + return val === 'true' || val === '1' || val === 'yes' || val === 'y'; + } + return Boolean(value); +} + +export async function getConfigByChainId(chainId: number) { + const addressFilePath = process.env.ADDRESS_FILE || `${homedir}/.ocean/ocean-contracts/artifacts/address.json`; + const addressFile = await fs.readFile(addressFilePath, 'utf8'); + + const data = JSON.parse(addressFile); + const chainConfig = Object.values(data).find((network: any) => network.chainId === chainId) as any; + + if (!chainConfig) { + throw new Error(`Chain ${chainId} not found in address file`); + } + + return chainConfig; } \ No newline at end of file diff --git a/test/escrow.test.ts b/test/escrow.test.ts new file mode 100644 index 0000000..4e49af1 --- /dev/null +++ b/test/escrow.test.ts @@ -0,0 +1,130 @@ +import { expect } from "chai"; +import { homedir } from 'os'; +import { runCommand } from "./util.js"; +import { getConfigByChainId } from "../src/helpers.js"; +import { ethers } from "ethers"; +import { EscrowContract } from "@oceanprotocol/lib"; + +describe("Ocean CLI Escrow", function () { + this.timeout(60000); // 60 second timeout + + let chainConfig: any; + let tokenAddress: string; + let payee: ethers.Wallet; + let payer: ethers.Wallet; + let escrowAddress: string; + + before(async function () { + process.env.AVOID_LOOP_RUN = "true"; + process.env.PRIVATE_KEY = "0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58"; + process.env.RPC = "http://localhost:8545"; + process.env.NODE_URL = "http://localhost:8000"; + process.env.ADDRESS_FILE = `${homedir}/.ocean/ocean-contracts/artifacts/address.json`; + + chainConfig = await getConfigByChainId(8996); + tokenAddress = chainConfig.Ocean; + escrowAddress = chainConfig.Escrow; + + const provider = new ethers.providers.JsonRpcProvider(process.env.RPC); + payer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + payee = new ethers.Wallet('0xef4b441145c1d0f3b4bc6d61d29f5c6e502359481152f869247c7a4244d45209', provider); + + + await runCommand(`npm run cli -- mintOcean`); + }); + + it("should deposit tokens into escrow", async function () { + const depositAmount = "1"; + + const initialTokenBalance = await runCommand(`npm run cli getUserFundsEscrow ${tokenAddress}`); + const initialTokenBalanceNumber = initialTokenBalance.split(`${tokenAddress}: `)[1].trim(); + const numberInitialTokenBalance = Number(initialTokenBalanceNumber); + + const output = await runCommand( + `npm run cli depositEscrow ${tokenAddress} ${depositAmount}` + ); + + const finalTokenBalance = await runCommand(`npm run cli getUserFundsEscrow ${tokenAddress}`); + const finalTokenBalanceNumber = finalTokenBalance.split(`${tokenAddress}: `)[1].trim(); + const numberFinalTokenBalance = Number(finalTokenBalanceNumber); + + expect(output).to.include("Deposit successful"); + expect(numberFinalTokenBalance).to.equal(Number(depositAmount) + numberInitialTokenBalance); + }); + + it("should withdraw tokens from escrow", async function () { + const withdrawAmount = "1"; + const output = await runCommand( + `npm run cli withdrawFromEscrow ${tokenAddress} ${withdrawAmount}` + ); + + expect(output).to.include("Successfully withdrawn"); + }); + + it("should fail to deposit with invalid amount", async function () { + const invalidAmount = "1000000000000000"; + + const output = await runCommand( + `npm run cli depositEscrow ${tokenAddress} ${invalidAmount}` + ); + + + expect(output).to.include("Deposit failed"); + }); + + it("should authorize a payee", async function () { + const maxLockedAmount = "1"; + const maxLockSeconds = "3600"; + const maxLockCounts = "10"; + + const output = await runCommand( + `npm run cli authorizeEscrow ${tokenAddress} ${payee.address} ${maxLockedAmount} ${maxLockSeconds} ${maxLockCounts}` + ); + + const escrow = new EscrowContract( + ethers.utils.getAddress(escrowAddress), + payer, + chainConfig.chainId + ); + + const authorizations = await escrow.getAuthorizations( + tokenAddress, + payer.address, + payee.address + ); + + const maxLockedAmountFromEscrowBN = authorizations[0].maxLockedAmount; + const maxLockedAmountFromEscrow = ethers.utils.formatEther(maxLockedAmountFromEscrowBN); + + expect(Number(maxLockedAmountFromEscrow)).to.equal(Number(maxLockedAmount)); + + + expect(output).to.satisfy((msg: string) => + msg.includes("Authorization successful") || msg.includes("already authorized") + ); + }); + + + + it("should fail to authorize with invalid parameters", async function () { + const invalidAmount = "10000000000"; + const invalidDuration = "0"; + const invalidCounts = "0"; + + const output = await runCommand( + `npm run cli authorizeEscrow ${tokenAddress} ${payee.address} ${invalidAmount} ${invalidDuration} ${invalidCounts}` + ); + + expect(output).to.include("Authorization failed"); + }); + + it("should get authorizations for a payee", async function () { + const output = await runCommand( + `npm run cli getAuthorizationsEscrow ${tokenAddress} ${payee.address}` + ); + + expect(output).to.include("Max locked amount: 1"); + expect(output).to.include("Max lock seconds: 3600"); + expect(output).to.include("Max lock counts: 10"); + }); +}); \ No newline at end of file