diff --git a/src/web3mail/sendEmail.ts b/src/web3mail/sendEmail.ts index 8f08c487..96c5b95a 100644 --- a/src/web3mail/sendEmail.ts +++ b/src/web3mail/sendEmail.ts @@ -22,11 +22,6 @@ import { senderNameSchema, throwIfMissing, } from '../utils/validators.js'; -import { - checkUserVoucher, - filterWorkerpoolOrders, -} from './sendEmail.models.js'; -import { SendEmailParams, SendEmailResponse } from './types.js'; import { DappAddressConsumer, DappWhitelistAddressConsumer, @@ -35,6 +30,11 @@ import { IpfsNodeConfigConsumer, SubgraphConsumer, } from './internalTypes.js'; +import { + checkUserVoucher, + filterWorkerpoolOrders, +} from './sendEmail.models.js'; +import { SendEmailParams, SendEmailResponse } from './types.js'; export type SendEmail = typeof sendEmail; @@ -56,6 +56,7 @@ export const sendEmail = async ({ senderName, protectedData, useVoucher = false, + allowDeposit = false, }: IExecConsumer & SubgraphConsumer & DappAddressConsumer & @@ -120,6 +121,10 @@ export const sendEmail = async ({ .label('useVoucher') .validateSync(useVoucher); + const vAllowDeposit = booleanSchema() + .label('allowDeposit') + .validateSync(allowDeposit); + // Check protected data schema through subgraph const isValidProtectedData = await checkProtectedDataValidity( graphQLClient, @@ -316,6 +321,9 @@ export const sendEmail = async ({ const requestorder = await iexec.order.signRequestorder(requestorderToSign); // Match orders and compute task ID + // TODO: Remove @ts-ignore once iexec SDK is updated to a version that includes allowDeposit in matchOrders types + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - allowDeposit is supported at runtime but not yet in TypeScript types const { dealid: dealId } = await iexec.order.matchOrders( { apporder: apporder, @@ -323,7 +331,10 @@ export const sendEmail = async ({ workerpoolorder: workerpoolorder, requestorder: requestorder, }, - { useVoucher: vUseVoucher } + // TODO: Remove @ts-ignore once iexec SDK is updated to a version that includes allowDeposit in matchOrders types + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - allowDeposit is supported at runtime but not yet in TypeScript types + { useVoucher: vUseVoucher, allowDeposit: vAllowDeposit } ); const taskId = await iexec.deal.computeTaskId(dealId, 0); diff --git a/src/web3mail/sendEmailCampaign.ts b/src/web3mail/sendEmailCampaign.ts index 15f4b37d..c2a042de 100644 --- a/src/web3mail/sendEmailCampaign.ts +++ b/src/web3mail/sendEmailCampaign.ts @@ -5,6 +5,7 @@ import { addressOrEnsSchema, campaignRequestSchema, throwIfMissing, + booleanSchema, } from '../utils/validators.js'; import { CampaignRequest, @@ -19,6 +20,7 @@ export const sendEmailCampaign = async ({ dataProtector = throwIfMissing(), workerpoolAddressOrEns = throwIfMissing(), campaignRequest, + allowDeposit = false, }: DataProtectorConsumer & SendEmailCampaignParams): Promise => { const vCampaignRequest = campaignRequestSchema() @@ -31,6 +33,10 @@ export const sendEmailCampaign = async ({ .label('workerpoolAddressOrEns') .validateSync(workerpoolAddressOrEns); + const vAllowDeposit = booleanSchema() + .label('allowDeposit') + .validateSync(allowDeposit); + if ( vCampaignRequest.workerpool !== NULL_ADDRESS && vCampaignRequest.workerpool.toLowerCase() !== @@ -43,10 +49,14 @@ export const sendEmailCampaign = async ({ try { // Process the prepared bulk request + // TODO: Remove @ts-ignore once @iexec/dataprotector is updated to a version that includes allowDeposit in ProcessBulkRequestParams types + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - allowDeposit is supported at runtime but not yet in TypeScript types const processBulkRequestResponse = await dataProtector.processBulkRequest({ bulkRequest: vCampaignRequest, workerpool: vWorkerpoolAddressOrEns, waitForResult: false, + allowDeposit: vAllowDeposit, }); return processBulkRequestResponse; diff --git a/src/web3mail/types.ts b/src/web3mail/types.ts index 18cee01b..af414652 100644 --- a/src/web3mail/types.ts +++ b/src/web3mail/types.ts @@ -1,6 +1,6 @@ +import type { BulkRequest } from '@iexec/dataprotector'; import { EnhancedWallet } from 'iexec'; import { IExecConfigOptions } from 'iexec/IExecConfig'; -import type { BulkRequest } from '@iexec/dataprotector'; export type Web3SignerProvider = EnhancedWallet; @@ -64,6 +64,7 @@ export type SendEmailParams = { appMaxPrice?: number; workerpoolMaxPrice?: number; useVoucher?: boolean; + allowDeposit?: boolean; }; export type FetchMyContactsParams = { @@ -180,6 +181,11 @@ export type SendEmailCampaignParams = { * Workerpool address or ENS to use for processing */ workerpoolAddressOrEns?: AddressOrENS; + /** + * If true, allows automatic deposit of funds when balance is insufficient + * @default false + */ + allowDeposit?: boolean; }; export type SendEmailCampaignResponse = { diff --git a/tests/e2e/sendEmail.test.ts b/tests/e2e/sendEmail.test.ts index cdd6d8bc..f5457d82 100644 --- a/tests/e2e/sendEmail.test.ts +++ b/tests/e2e/sendEmail.test.ts @@ -4,6 +4,12 @@ import { } from '@iexec/dataprotector'; import { beforeAll, describe, expect, it } from '@jest/globals'; import { HDNodeWallet } from 'ethers'; +import { IExec } from 'iexec'; +import { NULL_ADDRESS } from 'iexec/utils'; +import { + DEFAULT_CHAIN_ID, + getChainDefaultConfig, +} from '../../src/config/config.js'; import { IExecWeb3mail, WorkflowError } from '../../src/index.js'; import { MAX_EXPECTED_BLOCKTIME, @@ -22,12 +28,6 @@ import { getTestWeb3SignerProvider, waitSubgraphIndexing, } from '../test-utils.js'; -import { IExec } from 'iexec'; -import { NULL_ADDRESS } from 'iexec/utils'; -import { - DEFAULT_CHAIN_ID, - getChainDefaultConfig, -} from '../../src/config/config.js'; describe('web3mail.sendEmail()', () => { let consumerWallet: HDNodeWallet; @@ -41,14 +41,14 @@ describe('web3mail.sendEmail()', () => { let learnProdWorkerpoolAddress: string; const iexecOptions = getTestIExecOption(); const prodWorkerpoolPublicPrice = 1000; - + const workerpoolprice = 1_000; beforeAll(async () => { // (default) prod workerpool (not free) always available await createAndPublishWorkerpoolOrder( TEST_CHAIN.prodWorkerpool, TEST_CHAIN.prodWorkerpoolOwnerWallet, NULL_ADDRESS, - 1_000, + workerpoolprice, prodWorkerpoolPublicPrice ); // learn prod pool (free) assumed always available @@ -657,4 +657,76 @@ describe('web3mail.sendEmail()', () => { }); }); }); + + describe('allowDeposit', () => { + let protectData: ProtectedDataWithSecretProps; + consumerWallet = getRandomWallet(); + const dataPricePerAccess = 1000; + let web3mailConsumerInstance: IExecWeb3mail; + beforeAll(async () => { + protectData = await dataProtector.protectData({ + data: { email: 'example@test.com' }, + name: 'test do not use', + }); + await dataProtector.grantAccess({ + authorizedApp: getChainDefaultConfig(DEFAULT_CHAIN_ID).dappAddress, + protectedData: protectData.address, + authorizedUser: consumerWallet.address, // consumer wallet + numberOfAccess: 1000, + pricePerAccess: dataPricePerAccess, + }); + await waitSubgraphIndexing(); + web3mailConsumerInstance = new IExecWeb3mail( + ...getTestConfig(consumerWallet.privateKey) + ); + }, 2 * MAX_EXPECTED_BLOCKTIME); + it( + 'should throw error if insufficient total balance to cover task cost and allowDeposit is false', + async () => { + let error; + try { + await web3mailConsumerInstance.sendEmail({ + emailSubject: 'e2e mail object for test', + emailContent: 'e2e mail content for test', + protectedData: protectData.address, + dataMaxPrice: dataPricePerAccess, + workerpoolMaxPrice: workerpoolprice, + allowDeposit: false, + }); + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(WorkflowError); + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe('Failed to sendEmail'); + const causeMsg = + error.errorCause?.message || + error.cause?.message || + error.cause || + error.errorCause; + expect(String(causeMsg)).toContain( + "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" + ); + }, + 3 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it.skip( + 'should send email after depositing sufficient funds to cover task cost when allowDeposit is true', + async () => { + const result = await web3mailConsumerInstance.sendEmail({ + emailSubject: 'e2e mail object for test', + emailContent: 'e2e mail content for test', + protectedData: protectData.address, + dataMaxPrice: dataPricePerAccess, + workerpoolMaxPrice: workerpoolprice, + allowDeposit: true, + }); + expect(result).toBeDefined(); + expect(result).toHaveProperty('taskId'); + expect(result).toHaveProperty('dealId'); + }, + 3 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + }); });