diff --git a/packages/sdk/src/lib/dataProtectorCore/processBulkRequest.ts b/packages/sdk/src/lib/dataProtectorCore/processBulkRequest.ts index 9c0a73068..94f7fc4cd 100644 --- a/packages/sdk/src/lib/dataProtectorCore/processBulkRequest.ts +++ b/packages/sdk/src/lib/dataProtectorCore/processBulkRequest.ts @@ -1,10 +1,10 @@ import { sumTags } from 'iexec/utils'; import { SCONE_TAG } from '../../config/config.js'; import { - WorkflowError, - processProtectedDataErrorMessage, handleIfProtocolError, + processProtectedDataErrorMessage, ValidationError, + WorkflowError, } from '../../utils/errors.js'; import { checkUserVoucher, @@ -50,6 +50,7 @@ export const processBulkRequest = async < bulkRequest, workerpool, useVoucher = false, + allowDeposit = false, voucherOwner, path, pemPrivateKey, @@ -66,6 +67,9 @@ export const processBulkRequest = async < .default(defaultWorkerpool) // Default workerpool if none is specified .label('workerpool') .validateSync(workerpool); + const vAllowDeposit = booleanSchema() + .label('allowDeposit') + .validateSync(allowDeposit); const vUseVoucher = booleanSchema() .label('useVoucher') .validateSync(useVoucher); @@ -215,6 +219,7 @@ export const processBulkRequest = async < const matchOptions: MatchOptions = { useVoucher: vUseVoucher, ...(vVoucherOwner ? { voucherAddress: userVoucher?.address } : {}), + allowDeposit: vAllowDeposit, }; const { diff --git a/packages/sdk/src/lib/dataProtectorCore/processProtectedData.ts b/packages/sdk/src/lib/dataProtectorCore/processProtectedData.ts index 3ca626a81..cff8105c9 100644 --- a/packages/sdk/src/lib/dataProtectorCore/processProtectedData.ts +++ b/packages/sdk/src/lib/dataProtectorCore/processProtectedData.ts @@ -6,10 +6,10 @@ import { SCONE_TAG, } from '../../config/config.js'; import { - WorkflowError, - processProtectedDataErrorMessage, handleIfProtocolError, + processProtectedDataErrorMessage, ValidationError, + WorkflowError, } from '../../utils/errors.js'; import { checkUserVoucher, @@ -17,8 +17,8 @@ import { } from '../../utils/processProtectedData.models.js'; import { pushRequesterSecret } from '../../utils/pushRequesterSecret.js'; import { - getPemFormattedKeyPair, formatPemPublicKeyForSMS, + getPemFormattedKeyPair, } from '../../utils/rsa.js'; import { addressOrEnsSchema, @@ -64,6 +64,7 @@ export const processProtectedData = async < inputFiles, secrets, workerpool, + allowDeposit = false, useVoucher = false, voucherOwner, encryptResult = false, @@ -103,6 +104,9 @@ export const processProtectedData = async < .default(defaultWorkerpool) // Default workerpool if none is specified .label('workerpool') .validateSync(workerpool); + const vAllowDeposit = booleanSchema() + .label('allowDeposit') + .validateSync(allowDeposit); const vUseVoucher = booleanSchema() .label('useVoucher') .validateSync(useVoucher); @@ -369,9 +373,9 @@ export const processProtectedData = async < }; const matchOptions: MatchOptions = { useVoucher: vUseVoucher, + allowDeposit: vAllowDeposit, ...(vVoucherOwner ? { voucherAddress: userVoucher?.address } : {}), }; - const { dealid: dealId, txHash } = await iexec.order.matchOrders( orders, matchOptions diff --git a/packages/sdk/src/lib/types/commonTypes.ts b/packages/sdk/src/lib/types/commonTypes.ts index 4dd8367f4..0e0b8d1df 100644 --- a/packages/sdk/src/lib/types/commonTypes.ts +++ b/packages/sdk/src/lib/types/commonTypes.ts @@ -123,6 +123,7 @@ export interface SearchableDataSchema export type MatchOptions = { useVoucher: boolean; voucherAddress?: string; + allowDeposit?: boolean; }; export type DefaultWorkerpoolConsumer = { diff --git a/packages/sdk/src/lib/types/coreTypes.ts b/packages/sdk/src/lib/types/coreTypes.ts index ee6470434..abdf6dbfd 100644 --- a/packages/sdk/src/lib/types/coreTypes.ts +++ b/packages/sdk/src/lib/types/coreTypes.ts @@ -380,6 +380,12 @@ export type ProcessProtectedDataParams = { */ workerpool?: AddressOrENS; + /** + * A boolean that indicates whether to allow automatic deposit from wallet when account balance is insufficient to cover the cost of the task. + * @default false + */ + allowDeposit?: boolean; + /** * A boolean that indicates whether to use a voucher or no. */ @@ -567,6 +573,12 @@ export type ProcessBulkRequestParams = { */ useVoucher?: boolean; + /** + * A boolean that indicates whether to allow automatic deposit from wallet when account balance is insufficient to cover the cost of the bulk request. + * @default false + */ + allowDeposit?: boolean; + /** * Override the voucher contract to use, must be combined with useVoucher: true the user must be authorized by the voucher's owner to use it. */ diff --git a/packages/sdk/tests/e2e/dataProtectorCore/processProtectedData.test.ts b/packages/sdk/tests/e2e/dataProtectorCore/processProtectedData.test.ts index 6ba2f71ba..8ea47477c 100644 --- a/packages/sdk/tests/e2e/dataProtectorCore/processProtectedData.test.ts +++ b/packages/sdk/tests/e2e/dataProtectorCore/processProtectedData.test.ts @@ -10,6 +10,7 @@ import { MAX_EXPECTED_WEB2_SERVICES_TIME, deployRandomApp, getTestConfig, + setNRlcBalance, } from '../../test-utils.js'; describe('dataProtectorCore.processProtectedData() (waitForResult: false)', () => { @@ -392,4 +393,184 @@ describe('dataProtectorCore.processProtectedData() (waitForResult: false)', () = 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); }); + + describe('allowDeposit', () => { + let payableWorkerpoolAddress: string; + const workerpoolprice = 1000; + beforeAll(async () => { + const workerpoolOwnerWallet = Wallet.createRandom(); + const [ethProvider, options] = getTestConfig( + workerpoolOwnerWallet.privateKey + ); + + const iexecWorkerpoolOwner = new IExec( + { ethProvider }, + options.iexecOptions + ); + + await setNRlcBalance(workerpoolOwnerWallet.address, 100 * 10e9); + await iexecWorkerpoolOwner.account.deposit(100 * 10e9); + const { address: deployedWorkerpoolAddress } = + await iexecWorkerpoolOwner.workerpool.deployWorkerpool({ + description: 'payable test workerpool', + owner: await iexecWorkerpoolOwner.wallet.getAddress(), + }); + payableWorkerpoolAddress = deployedWorkerpoolAddress; + + await iexecWorkerpoolOwner.order + .createWorkerpoolorder({ + workerpool: deployedWorkerpoolAddress, + category: 0, + workerpoolprice, + volume: 1000, + tag: ['tee', 'scone'], + }) + .then(iexecWorkerpoolOwner.order.signWorkerpoolorder) + .then(iexecWorkerpoolOwner.order.publishWorkerpoolorder); + }); + it( + 'should throw error when insufficient funds and allowDeposit is false', + async () => { + const { processProtectedData } = await import( + '../../../src/lib/dataProtectorCore/processProtectedData.js' + ); + + // wallet has enough nRLC + await setNRlcBalance(wallet.address, workerpoolprice * 10e9); + // but account has no enough funds to process the data (less than workerpoolprice) + await iexec.account.deposit(1 * 10e9); + + let caughtError: Error | undefined; + try { + await processProtectedData({ + iexec, + protectedData: protectedData.address, + app: appAddress, + defaultWorkerpool: payableWorkerpoolAddress, + workerpool: payableWorkerpoolAddress, + workerpoolMaxPrice: 100000, + secrets: { + 1: 'ProcessProtectedData test subject', + 2: 'email content for test processData', + }, + args: '_args_test_process_data_', + path: 'computed.json', + waitForResult: false, + }); + } catch (firstError) { + try { + await processProtectedData({ + iexec, + protectedData: protectedData.address, + app: appAddress, + defaultWorkerpool: payableWorkerpoolAddress, + workerpool: payableWorkerpoolAddress, + workerpoolMaxPrice: 100000, + secrets: { + 1: 'ProcessProtectedData test subject', + 2: 'email content for test processData', + }, + args: '_args_test_process_data_', + path: 'computed.json', + waitForResult: false, + }); + } catch (secondError) { + caughtError = secondError as Error; + } + } + + expect(caughtError).toBeDefined(); + expect(caughtError).toBeInstanceOf(Error); + expect(caughtError?.message).toBe('Failed to process protected data'); + const causeMsg = + (caughtError as any)?.errorCause?.message || + (caughtError as any)?.cause?.message || + (caughtError as any)?.cause || + (caughtError as any)?.errorCause; + + expect(causeMsg).toBe( + `Cost per task (${workerpoolprice} nRlc) is greater than requester account stake (0). Orders can't be matched. If you are the requester, you should deposit to top up your account` + ); + }, + 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should process protected data when no funds are deposited and allowDeposit is true', + async () => { + const { processProtectedData } = await import( + '../../../src/lib/dataProtectorCore/processProtectedData.js' + ); + // wallet has enough nRLC but account has no funds + await setNRlcBalance(wallet.address, workerpoolprice * 10e9); + + const walletBefore = await iexec.wallet.checkBalances( + await iexec.wallet.getAddress() + ); + const res = await processProtectedData({ + iexec, + protectedData: protectedData.address, + + app: appAddress, + defaultWorkerpool: workerpoolAddress, + workerpool: workerpoolAddress, + secrets: { + 1: 'ProcessProtectedData test subject', + 2: 'email content for test processData', + }, + args: '_args_test_process_data_', + path: 'computed.json', + waitForResult: false, + allowDeposit: true, + }); + const walletAfter = await iexec.wallet.checkBalances( + await iexec.wallet.getAddress() + ); + expect(walletAfter.nRLC.lt(walletBefore.nRLC)).toBe(true); + expect(res.dealId).toEqual(expect.any(String)); + expect(res.taskId).toEqual(expect.any(String)); + expect(res.txHash).toEqual(expect.any(String)); + }, + 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should process protected data when insufficient funds are deposited and allowDeposit is true', + async () => { + const { processProtectedData } = await import( + '../../../src/lib/dataProtectorCore/processProtectedData.js' + ); + // wallet has enough nRLC but account has insufficient funds + await setNRlcBalance(wallet.address, workerpoolprice * 10e9); + await iexec.account.deposit(10 * 10e9); + + const walletBefore = await iexec.wallet.checkBalances( + await iexec.wallet.getAddress() + ); + const res = await processProtectedData({ + iexec, + protectedData: protectedData.address, + app: appAddress, + defaultWorkerpool: workerpoolAddress, + workerpool: workerpoolAddress, + secrets: { + 1: 'ProcessProtectedData test subject', + 2: 'email content for test processData', + }, + args: '_args_test_process_data_', + path: 'computed.json', + waitForResult: false, + allowDeposit: true, + }); + const walletAfter = await iexec.wallet.checkBalances( + await iexec.wallet.getAddress() + ); + expect(walletAfter.nRLC.lt(walletBefore.nRLC)).toBe(true); + expect(res.dealId).toEqual(expect.any(String)); + expect(res.taskId).toEqual(expect.any(String)); + expect(res.txHash).toEqual(expect.any(String)); + }, + 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + }); });