UNPKG

@iexec/web3telegram

Version:

Enables secure, blockchain-based messaging by encrypting Telegram user IDs for privacy. It lets users message Ethereum account holders without knowing their Telegram details.

311 lines (297 loc) 10.2 kB
import { Buffer } from 'buffer'; import { MAX_DESIRED_APP_ORDER_PRICE, MAX_DESIRED_DATA_ORDER_PRICE, MAX_DESIRED_WORKERPOOL_ORDER_PRICE, } 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, telegramContentSchema, positiveNumberSchema, labelSchema, throwIfMissing, addressSchema, senderNameSchema, booleanSchema, } from '../utils/validators.js'; import { SendTelegramParams, SendTelegramResponse } from './types.js'; import { checkUserVoucher, filterWorkerpoolOrders, } from '../utils/sendTelegram.models.js'; import { DappAddressConsumer, DappWhitelistAddressConsumer, IExecConsumer, IpfsGatewayConfigConsumer, IpfsNodeConfigConsumer, SubgraphConsumer, } from './internalTypes.js'; export type SendTelegram = typeof sendTelegram; export const sendTelegram = async ({ graphQLClient = throwIfMissing(), iexec = throwIfMissing(), workerpoolAddressOrEns = throwIfMissing(), dappAddressOrENS, dappWhitelistAddress, ipfsNode, ipfsGateway, senderName, telegramContent, label, dataMaxPrice = MAX_DESIRED_DATA_ORDER_PRICE, appMaxPrice = MAX_DESIRED_APP_ORDER_PRICE, workerpoolMaxPrice = MAX_DESIRED_WORKERPOOL_ORDER_PRICE, protectedData, useVoucher = false, }: IExecConsumer & SubgraphConsumer & DappAddressConsumer & DappWhitelistAddressConsumer & IpfsNodeConfigConsumer & IpfsGatewayConfigConsumer & SendTelegramParams): Promise<SendTelegramResponse> => { try { const vDatasetAddress = addressOrEnsSchema() .required() .label('protectedData') .validateSync(protectedData); const vSenderName = senderNameSchema() .label('senderName') .validateSync(senderName); const vTelegramContent = telegramContentSchema() .required() .label('telegramContent') .validateSync(telegramContent); 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 validity through subgraph const isValidProtectedData = await checkProtectedDataValidity( graphQLClient, vDatasetAddress ); if (!isValidProtectedData) { throw new Error( 'This protected data does not contain "telegram_chatId: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; } } const [ datasetorderForApp, datasetorderForWhitelist, apporder, workerpoolorder, ] = await Promise.all([ // Fetch dataset order for web3telegram app iexec.orderbook .fetchDatasetOrderbook({ dataset: 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 web3telegram whitelist iexec.orderbook .fetchDatasetOrderbook({ dataset: 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({ app: 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; } ), ]); const datasetorder = datasetorderForApp || datasetorderForWhitelist; if (!datasetorder) { throw new Error('No Dataset order found for the desired price'); } // Push requester secrets const telegramContentEncryptionKey = iexec.dataset.generateEncryptionKey(); const encryptedFile = await iexec.dataset .encrypt( Buffer.from(vTelegramContent, 'utf8'), telegramContentEncryptionKey ) .catch((e) => { throw new WorkflowError({ message: 'Failed to encrypt message content', errorCause: e, }); }); // Push telegram message to IPFS const cid = await ipfs .add(encryptedFile, { ipfsNode, ipfsGateway, }) .catch((e) => { throw new WorkflowError({ message: 'Failed to upload encrypted telegram content', errorCause: e, }); }); const multiaddr = `/ipfs/${cid}`; const requesterSecretId = generateSecureUniqueId(16); await iexec.secrets.pushRequesterSecret( requesterSecretId, JSON.stringify({ senderName: vSenderName, telegramContentMultiAddr: multiaddr, telegramContentEncryptionKey, }) ); 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: dealId } = await iexec.order.matchOrders( { apporder: apporder, datasetorder: datasetorder, workerpoolorder: workerpoolorder, requestorder: requestorder, }, { useVoucher: vUseVoucher } ); const taskId = await iexec.deal.computeTaskId(dealId, 0); return { dealId, taskId, }; } catch (error) { // Protocol error detected, re-throwing as-is if ((error as any)?.isProtocolError === true) { throw error; } // Handle protocol errors - this will throw if it's an ApiCallError // handleIfProtocolError transforms ApiCallError into a WorkflowError with isProtocolError=true handleIfProtocolError(error); // For all other errors throw new WorkflowError({ message: 'Failed to sendTelegram', errorCause: error, }); } };