UNPKG

@iexec/iapp

Version:

A CLI to guide you through the process of building an iExec iApp

338 lines (312 loc) 10.4 kB
import { v4 as uuidV4 } from 'uuid'; import { ethers } from 'ethers'; import { utils } from 'iexec'; import { mkdir, rm } from 'node:fs/promises'; import { askForWallet } from '../cli-helpers/askForWallet.js'; import { SCONE_TAG, RUN_OUTPUT_DIR, TASK_OBSERVATION_TIMEOUT, } from '../config/config.js'; import { addRunData } from '../utils/cacheExecutions.js'; import { getSpinner, type Spinner } from '../cli-helpers/spinner.js'; import { handleCliError } from '../cli-helpers/handleCliError.js'; import { getIExecDebug } from '../utils/iexec.js'; import { extractZipToFolder } from '../utils/extractZipToFolder.js'; import { askShowResult } from '../cli-helpers/askShowResult.js'; import { goToProjectRoot } from '../cli-helpers/goToProjectRoot.js'; import * as color from '../cli-helpers/color.js'; import { IExec } from 'iexec'; import { getChainConfig, readIAppConfig } from '../utils/iAppConfigFile.js'; import { getIExecTdx, WORKERPOOL_TDX } from '../utils/tdx-poc.js'; import { useTdx } from '../utils/featureFlags.js'; import { ensureBalances } from '../cli-helpers/ensureBalances.js'; import { askForAcknowledgment } from '../cli-helpers/askForAcknowledgment.js'; import { warnBeforeTxFees } from '../cli-helpers/warnBeforeTxFees.js'; export async function run({ iAppAddress, args, protectedData, inputFile: inputFiles = [], // rename variable (it's an array) requesterSecret: requesterSecrets = [], // rename variable (it's an array) chain, }: { iAppAddress: string; args?: string; protectedData?: string; inputFile?: string[]; requesterSecret?: { key: number; value: string }[]; chain?: string; }) { const spinner = getSpinner(); try { await goToProjectRoot({ spinner }); cleanRunOutput({ spinner, outputFolder: RUN_OUTPUT_DIR }); await runInDebug({ iAppAddress, args, protectedData, inputFiles, requesterSecrets, spinner, chain, }); } catch (error) { handleCliError({ spinner, error }); } } export async function runInDebug({ iAppAddress, args, protectedData, inputFiles = [], requesterSecrets = [], spinner, chain, }: { iAppAddress: string; args?: string; protectedData?: string; inputFiles?: string[]; requesterSecrets?: { key: number; value: string }[]; chain?: string; spinner: Spinner; }) { const { defaultChain } = await readIAppConfig(); const chainName = chain || defaultChain; const chainConfig = getChainConfig(chainName); spinner.info(`Using chain ${chainName}`); await warnBeforeTxFees({ spinner, chain: chainConfig.name }); // Is valid iApp address if (!ethers.isAddress(iAppAddress)) { throw Error( 'The iApp address is invalid. Be careful ENS name is not implemented yet ...' ); } if (protectedData) { // Is valid protectedData address if (!ethers.isAddress(protectedData)) { throw Error( 'The protectedData address is invalid. Be careful ENS name is not implemented yet ...' ); } } // Get wallet from privateKey const signer = await askForWallet({ spinner }); const userAddress = await signer.getAddress(); let iexec: IExec; if (useTdx) { iexec = getIExecTdx({ ...chainConfig, signer }); } else { iexec = getIExecDebug({ ...chainConfig, signer, }); } // Make some ProtectedData preflight check if (protectedData) { try { // Check the protectedData has its privateKey registered into the debug sms const isSecretSet = await iexec.dataset.checkDatasetSecretExists( protectedData, { teeFramework: 'scone', } ); if (!isSecretSet) { throw Error( `Your protectedData secret key is not registered in the debug secret management service (SMS) of iexec protocol` ); } } catch (err) { throw Error( `Error while running your iApp with your protectedData: ${(err as Error)?.message}` ); } } // Requester secrets let iexec_secrets; if (requesterSecrets.length > 0) { spinner.start('Provisioning requester secrets...'); iexec_secrets = Object.fromEntries( await Promise.all( requesterSecrets.map(async ({ key, value }) => { const name = await pushRequesterSecret({ iexec, value }); return [key, name]; }) ) ); spinner.succeed('Requester secrets provisioned'); } // Workerpool Order spinner.start('Fetching workerpool order...'); const workerpoolOrderbook = await iexec.orderbook.fetchWorkerpoolOrderbook({ workerpool: useTdx ? WORKERPOOL_TDX : chainConfig.workerpoolDebug, app: iAppAddress, dataset: protectedData || ethers.ZeroAddress, minTag: SCONE_TAG, maxTag: SCONE_TAG, }); const workerpoolorder = workerpoolOrderbook.orders[0]?.order; if (!workerpoolorder) { throw Error( 'No WorkerpoolOrder found, Wait until some workerpoolOrder come back' ); } spinner.succeed('Workerpool order fetched'); // App Order spinner.start('Creating app order...'); const apporderTemplate = await iexec.order.createApporder({ app: iAppAddress, requesterrestrict: userAddress, tag: SCONE_TAG, }); const apporder = await iexec.order.signApporder(apporderTemplate); spinner.succeed('AppOrder created'); // Dataset Order let datasetorder; if (protectedData) { spinner.start('Fetching protectedData access...'); const datasetOrderbook = await iexec.orderbook.fetchDatasetOrderbook( protectedData, { app: iAppAddress, workerpool: workerpoolorder.workerpool, requester: userAddress, minTag: SCONE_TAG, maxTag: SCONE_TAG, } ); datasetorder = datasetOrderbook.orders[0]?.order; if (!datasetorder) { throw Error( 'No matching ProtectedData access found, It seems your iApp is not allowed to access the protectedData, please grantAccess to it' ); } spinner.succeed('ProtectedData access found'); } spinner.start('Creating request order...'); const requestorderToSign = await iexec.order.createRequestorder({ app: iAppAddress, category: workerpoolorder.category, dataset: protectedData || ethers.ZeroAddress, appmaxprice: apporder.appprice, datasetmaxprice: datasetorder?.datasetprice || 0, workerpoolmaxprice: workerpoolorder.workerpoolprice, tag: SCONE_TAG, workerpool: workerpoolorder.workerpool, params: { iexec_args: args, iexec_input_files: inputFiles.length > 0 ? inputFiles : undefined, iexec_secrets, }, }); const requestorder = await iexec.order.signRequestorder(requestorderToSign); spinner.succeed('RequestOrder created'); const matchOrderParams = { apporder, datasetorder: protectedData ? datasetorder : undefined, workerpoolorder, requestorder, }; spinner.start('Checking balances...'); const { total, sponsored } = await iexec.order.estimateMatchOrders(matchOrderParams); const priceToPay = total.sub(sponsored); const balances = await ensureBalances({ spinner, iexec, nRlcMin: priceToPay, }); if (!priceToPay.isZero()) { await askForAcknowledgment({ spinner, message: `You will spend ${utils.formatRLC(priceToPay)} RLC to run your iApp. Would you like to continue?`, }); } if (balances.stake.lt(priceToPay)) { const toDeposit = priceToPay.sub(balances.stake); await askForAcknowledgment({ spinner, message: `Current account stake is ${utils.formatRLC(balances.stake)} RLC, you need to deposit an additional ${utils.formatRLC(toDeposit)} RLC from your wallet. Would you like to continue?`, }); await iexec.account.deposit(toDeposit); } spinner.start('Matching orders...'); const { dealid, txHash } = await iexec.order.matchOrders(matchOrderParams); const taskid = await iexec.deal.computeTaskId(dealid, 0); await addRunData({ iAppAddress, dealid, taskid, txHash, chainName }); spinner.succeed( `Deal created successfully - deal: ${dealid} ${color.link(`${chainConfig.iexecExplorerUrl}/deal/${dealid}`)} - task: ${taskid}` ); spinner.start('Observing task...'); const taskObservable = await iexec.task.obsTask(taskid, { dealid: dealid }); const taskTimeoutWarning = setTimeout(() => { const spinnerText = spinner.text; spinner.warn('Task is taking longer than expected...'); spinner.info( `Tip: You can debug this task using ${color.command(`iapp debug ${taskid}`)}` ); spinner.start(spinnerText); // restart spinning }, TASK_OBSERVATION_TIMEOUT); await new Promise((resolve, reject) => { taskObservable.subscribe({ next: () => {}, error: (e) => reject(e), complete: () => resolve(undefined), }); }).finally(() => { clearTimeout(taskTimeoutWarning); }); const task = await iexec.task.show(taskid); const { location } = task.results as { storage: string; location?: string }; spinner.succeed(`Task finalized You can download the result of your task here: ${color.link(`${chainConfig.ipfsGatewayUrl}${location}`)}`); const downloadAnswer = await spinner.prompt({ type: 'confirm', name: 'continue', message: 'Would you like to download the result?', initial: true, }); if (!downloadAnswer.continue) { spinner.stop(); process.exit(1); } spinner.start('Downloading result...'); const outputFolder = RUN_OUTPUT_DIR; const taskResult = await iexec.task.fetchResults(taskid); const resultBuffer = await taskResult.arrayBuffer(); await extractZipToFolder(resultBuffer, outputFolder); spinner.succeed(`Result downloaded to ${color.file(outputFolder)}`); await askShowResult({ spinner, outputPath: outputFolder }); } /** * push a requester secret with a random uuid * @returns {string} secretName */ async function pushRequesterSecret({ iexec, value, }: { iexec: IExec; value: string; }) { const secretName = uuidV4(); await iexec.secrets.pushRequesterSecret(secretName, value); return secretName; } async function cleanRunOutput({ spinner, outputFolder, }: { spinner: Spinner; outputFolder: string; }) { // just start the spinner, no need to persist success in terminal spinner.start('Cleaning output directory...'); await rm(outputFolder, { recursive: true, force: true }); await mkdir(outputFolder); spinner.reset(); }