UNPKG

@iexec/web3mail

Version:

This product enables users to confidentially store data–such as mail address, documents, personal information ...

317 lines (302 loc) 10 kB
import { Buffer } from 'buffer'; import { DEFAULT_CONTENT_TYPE, MAX_DESIRED_APP_ORDER_PRICE, MAX_DESIRED_DATA_ORDER_PRICE, MAX_DESIRED_WORKERPOOL_ORDER_PRICE, PROD_WORKERPOOL_ADDRESS, } from '../config/config.js'; import { handleIfProtocolError, WorkflowError } from '../utils/errors.js'; import { generateSecureUniqueId } from '../utils/generateUniqueId.js'; import * as ipfs from '../utils/ipfs-service.js'; import { checkProtectedDataValidity } from '../utils/subgraphQuery.js'; import { addressOrEnsSchema, addressSchema, booleanSchema, contentTypeSchema, emailContentSchema, emailSubjectSchema, labelSchema, positiveNumberSchema, senderNameSchema, throwIfMissing, } from '../utils/validators.js'; import { checkUserVoucher, filterWorkerpoolOrders, } from './sendEmail.models.js'; import { DappAddressConsumer, DappWhitelistAddressConsumer, IExecConsumer, IpfsGatewayConfigConsumer, IpfsNodeConfigConsumer, SendEmailParams, SendEmailResponse, SubgraphConsumer, } from './types.js'; export type SendEmail = typeof sendEmail; export const sendEmail = async ({ graphQLClient = throwIfMissing(), iexec = throwIfMissing(), workerpoolAddressOrEns = PROD_WORKERPOOL_ADDRESS, dappAddressOrENS, dappWhitelistAddress, ipfsNode, ipfsGateway, emailSubject, emailContent, contentType = DEFAULT_CONTENT_TYPE, label, dataMaxPrice = MAX_DESIRED_DATA_ORDER_PRICE, appMaxPrice = MAX_DESIRED_APP_ORDER_PRICE, workerpoolMaxPrice = MAX_DESIRED_WORKERPOOL_ORDER_PRICE, senderName, protectedData, useVoucher = false, }: IExecConsumer & SubgraphConsumer & DappAddressConsumer & DappWhitelistAddressConsumer & IpfsNodeConfigConsumer & IpfsGatewayConfigConsumer & SendEmailParams): Promise<SendEmailResponse> => { const vDatasetAddress = addressOrEnsSchema() .required() .label('protectedData') .validateSync(protectedData); const vEmailSubject = emailSubjectSchema() .required() .label('emailSubject') .validateSync(emailSubject); const vEmailContent = emailContentSchema() .required() .label('emailContent') .validateSync(emailContent); const vContentType = contentTypeSchema() .required() .label('contentType') .validateSync(contentType); const vSenderName = senderNameSchema() .label('senderName') .validateSync(senderName); const vLabel = labelSchema().label('label').validateSync(label); const vWorkerpoolAddressOrEns = addressOrEnsSchema() .required() .label('WorkerpoolAddressOrEns') .validateSync(workerpoolAddressOrEns); const vDappAddressOrENS = addressOrEnsSchema() .required() .label('dappAddressOrENS') .validateSync(dappAddressOrENS); const vDappWhitelistAddress = addressSchema() .required() .label('dappWhitelistAddress') .validateSync(dappWhitelistAddress); const vDataMaxPrice = positiveNumberSchema() .label('dataMaxPrice') .validateSync(dataMaxPrice); const vAppMaxPrice = positiveNumberSchema() .label('appMaxPrice') .validateSync(appMaxPrice); const vWorkerpoolMaxPrice = positiveNumberSchema() .label('workerpoolMaxPrice') .validateSync(workerpoolMaxPrice); const vUseVoucher = booleanSchema() .label('useVoucher') .validateSync(useVoucher); // Check protected data schema through subgraph const isValidProtectedData = await checkProtectedDataValidity( graphQLClient, vDatasetAddress ); if (!isValidProtectedData) { throw new Error( 'This protected data does not contain "email:string" in its schema.' ); } const requesterAddress = await iexec.wallet.getAddress(); let userVoucher; if (vUseVoucher) { try { userVoucher = await iexec.voucher.showUserVoucher(requesterAddress); checkUserVoucher({ userVoucher }); } catch (err) { if (err?.message?.startsWith('No Voucher found for address')) { throw new Error( 'Oops, it seems your wallet is not associated with any voucher. Check on https://builder.iex.ec/' ); } throw err; } } try { const [ datasetorderForApp, datasetorderForWhitelist, apporder, workerpoolorder, ] = await Promise.all([ // Fetch dataset order for web3mail app iexec.orderbook .fetchDatasetOrderbook(vDatasetAddress, { app: dappAddressOrENS, requester: requesterAddress, }) .then((datasetOrderbook) => { const desiredPriceDataOrderbook = datasetOrderbook.orders.filter( (order) => order.order.datasetprice <= vDataMaxPrice ); return desiredPriceDataOrderbook[0]?.order; // may be undefined }), // Fetch dataset order for web3mail whitelist iexec.orderbook .fetchDatasetOrderbook(vDatasetAddress, { app: vDappWhitelistAddress, requester: requesterAddress, }) .then((datasetOrderbook) => { const desiredPriceDataOrderbook = datasetOrderbook.orders.filter( (order) => order.order.datasetprice <= vDataMaxPrice ); return desiredPriceDataOrderbook[0]?.order; // may be undefined }), // Fetch app order iexec.orderbook .fetchAppOrderbook(dappAddressOrENS, { minTag: ['tee', 'scone'], maxTag: ['tee', 'scone'], workerpool: workerpoolAddressOrEns, }) .then((appOrderbook) => { const desiredPriceAppOrderbook = appOrderbook.orders.filter( (order) => order.order.appprice <= vAppMaxPrice ); const desiredPriceAppOrder = desiredPriceAppOrderbook[0]?.order; if (!desiredPriceAppOrder) { throw new Error('No App order found for the desired price'); } return desiredPriceAppOrder; }), // Fetch workerpool order for App or AppWhitelist Promise.all([ // for app iexec.orderbook.fetchWorkerpoolOrderbook({ workerpool: workerpoolAddressOrEns, app: vDappAddressOrENS, dataset: vDatasetAddress, requester: requesterAddress, // public orders + user specific orders isRequesterStrict: useVoucher, // If voucher, we only want user specific orders minTag: ['tee', 'scone'], maxTag: ['tee', 'scone'], category: 0, }), // for app whitelist iexec.orderbook.fetchWorkerpoolOrderbook({ workerpool: workerpoolAddressOrEns, app: vDappWhitelistAddress, dataset: vDatasetAddress, requester: requesterAddress, // public orders + user specific orders isRequesterStrict: useVoucher, // If voucher, we only want user specific orders minTag: ['tee', 'scone'], maxTag: ['tee', 'scone'], category: 0, }), ]).then( ([workerpoolOrderbookForApp, workerpoolOrderbookForAppWhitelist]) => { const desiredPriceWorkerpoolOrder = filterWorkerpoolOrders({ workerpoolOrders: [ ...workerpoolOrderbookForApp.orders, ...workerpoolOrderbookForAppWhitelist.orders, ], workerpoolMaxPrice: vWorkerpoolMaxPrice, useVoucher: vUseVoucher, userVoucher, }); if (!desiredPriceWorkerpoolOrder) { throw new Error('No Workerpool order found for the desired price'); } return desiredPriceWorkerpoolOrder; } ), ]); if (!workerpoolorder) { throw new Error('No Workerpool order found for the desired price'); } const datasetorder = datasetorderForApp || datasetorderForWhitelist; if (!datasetorder) { throw new Error('No Dataset order found for the desired price'); } // Push requester secrets const requesterSecretId = generateSecureUniqueId(16); const emailContentEncryptionKey = iexec.dataset.generateEncryptionKey(); const encryptedFile = await iexec.dataset .encrypt(Buffer.from(vEmailContent, 'utf8'), emailContentEncryptionKey) .catch((e) => { throw new WorkflowError({ message: 'Failed to encrypt email content', errorCause: e, }); }); const cid = await ipfs .add(encryptedFile, { ipfsNode: ipfsNode, ipfsGateway: ipfsGateway, }) .catch((e) => { throw new WorkflowError({ message: 'Failed to upload encrypted email content', errorCause: e, }); }); const multiaddr = `/ipfs/${cid}`; await iexec.secrets.pushRequesterSecret( requesterSecretId, JSON.stringify({ emailSubject: vEmailSubject, emailContentMultiAddr: multiaddr, contentType: vContentType, senderName: vSenderName, emailContentEncryptionKey, }) ); const requestorderToSign = await iexec.order.createRequestorder({ app: vDappAddressOrENS, category: workerpoolorder.category, dataset: vDatasetAddress, datasetmaxprice: datasetorder.datasetprice, appmaxprice: apporder.appprice, workerpoolmaxprice: workerpoolorder.workerpoolprice, tag: ['tee', 'scone'], workerpool: vWorkerpoolAddressOrEns, params: { iexec_secrets: { 1: requesterSecretId, }, iexec_args: vLabel, }, }); const requestorder = await iexec.order.signRequestorder(requestorderToSign); // Match orders and compute task ID const { dealid } = await iexec.order.matchOrders( { apporder: apporder, datasetorder: datasetorder, workerpoolorder: workerpoolorder, requestorder: requestorder, }, { useVoucher: vUseVoucher } ); const taskId = await iexec.deal.computeTaskId(dealid, 0); return { taskId, }; } catch (error) { handleIfProtocolError(error); throw new WorkflowError({ message: 'Failed to sendEmail', errorCause: error, }); } };