UNPKG

@iexec/iapp

Version:

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

257 lines 11.8 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 { RUN_OUTPUT_DIR, TASK_OBSERVATION_TIMEOUT } from '../config/config.js'; import { addRunData } from '../utils/cacheExecutions.js'; import { getSpinner } from '../cli-helpers/spinner.js'; import { handleCliError } from '../cli-helpers/handleCliError.js'; import { getIExec } 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 { readIAppConfig } from '../utils/iAppConfigFile.js'; import { ensureBalances } from '../cli-helpers/ensureBalances.js'; import { askForAcknowledgment } from '../cli-helpers/askForAcknowledgment.js'; import { warnBeforeTxFees } from '../cli-helpers/warnBeforeTxFees.js'; import { resolveChainConfig } from '../cli-helpers/resolveChainConfig.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, }) { const spinner = getSpinner(); try { await goToProjectRoot({ spinner }); await cleanRunOutput({ spinner, outputFolder: RUN_OUTPUT_DIR }); const { defaultChain } = await readIAppConfig(); const chainConfig = resolveChainConfig({ chain, defaultChain, spinner, }); spinner.start('checking inputs...'); // initialize iExec const readOnlyIexec = getIExec(chainConfig); // input checks if ((await readOnlyIexec.app.checkDeployedApp(iAppAddress)) === false) { throw Error('No iApp found at the specified address.'); } const { app } = await readOnlyIexec.app.showApp(iAppAddress); // determine TEE framework based on app properties (SCONE apps define `appMREnclave`, TDX apps do NOT define `appMREnclave`) const isTdxApp = !app.appMREnclave; const teeFramework = isTdxApp ? 'tdx' : 'scone'; if (teeFramework === 'scone') { throw new Error('The selected iApp is using the SGX SCONE TEE framework which is no longer supported. The iApp must be redeployed with TDX.'); } if (protectedData.length > 0) { await Promise.all(protectedData.map(async (dataset) => { if ((await readOnlyIexec.dataset.checkDeployedDataset(dataset)) === false) { throw Error(`No protectedData found at ${dataset}.`); } const isSecretSet = await readOnlyIexec.dataset.checkDatasetSecretExists(dataset, { teeFramework, }); if (!isSecretSet) { throw Error(`The protectedData secret key for ${dataset} is not registered in the Secret Management Service (SMS) of iExec protocol.`); } })); } await warnBeforeTxFees({ spinner }); // Get wallet from privateKey const signer = await askForWallet({ spinner }); const userAddress = await signer.getAddress(); const iexec = getIExec({ ...chainConfig, signer, }); // App Order spinner.start('Creating app order...'); const apporderTemplate = await iexec.order.createApporder({ app: iAppAddress, requesterrestrict: userAddress, tag: ['tee', teeFramework], }); const apporder = await iexec.order.signApporder(apporderTemplate); spinner.succeed('AppOrder created'); // Dataset Order let bulkCid; let volume = 1; let datasetorders = []; if (protectedData.length > 0) { spinner.start('Fetching protectedData access...'); datasetorders = await Promise.all(protectedData.map(async (dataset) => { const datasetOrderbook = await iexec.orderbook.fetchDatasetOrderbook({ dataset, app: iAppAddress, requester: userAddress, minTag: ['tee'], // TEE framework tag is ignored for dataset order matching, as dataset can be shared between SCONE and TDX apps bulkOnly: protectedData.length > 1, // bulk if multiple datasets }); const 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 ${dataset}, please grantAccess to it`); } return datasetorder; })); spinner.succeed('ProtectedData access found'); if (protectedData.length > 1) { spinner.start('Preparing bulk access...'); const bulk = await iexec.order.prepareDatasetBulk(datasetorders); bulkCid = bulk.cid; volume = bulk.volume; } } // 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, teeFramework, }); return [key, name]; }))); spinner.succeed('Requester secrets provisioned'); } // Workerpool Order spinner.start('Fetching workerpool order...'); const workerpoolOrderbook = await iexec.orderbook.fetchWorkerpoolOrderbook({ workerpool: chainConfig.tdxWorkerpool, app: iAppAddress, minTag: apporder.tag, minVolume: volume, // TODO handle multiple matches if not enough volume }); const workerpoolorder = workerpoolOrderbook.orders[0]?.order; if (!workerpoolorder) { throw Error('No workerpool order found, Wait until some workerpool order come back'); } spinner.succeed('Workerpool order fetched'); spinner.start('Creating request order...'); const requestorderToSign = await iexec.order.createRequestorder({ app: iAppAddress, category: workerpoolorder.category, dataset: datasetorders.length === 1 ? datasetorders[0].dataset : ethers.ZeroAddress, appmaxprice: apporder.appprice, datasetmaxprice: datasetorders.length === 1 ? datasetorders[0].datasetprice.toString() : 0, workerpoolmaxprice: workerpoolorder.workerpoolprice, tag: ['tee', teeFramework], volume, params: { iexec_args: args, iexec_input_files: inputFiles.length > 0 ? inputFiles : undefined, iexec_secrets, bulk_cid: bulkCid, }, }); const requestorder = await iexec.order.signRequestorder(requestorderToSign); spinner.succeed('RequestOrder created'); const matchOrderParams = { apporder, datasetorder: datasetorders.length === 1 ? datasetorders[0] : 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({ app: iAppAddress, dealid, taskids: [taskid], txHash, chainName: chainConfig.name, }); 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; 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 }); } catch (error) { handleCliError({ spinner, error }); } } /** * push a requester secret with a random uuid * @returns {string} secretName */ async function pushRequesterSecret({ iexec, value, teeFramework, }) { const secretName = uuidV4(); await iexec.secrets.pushRequesterSecret(secretName, value, { teeFramework }); return secretName; } async function cleanRunOutput({ spinner, outputFolder, }) { // 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(); } //# sourceMappingURL=run.js.map