@iexec/iapp
Version:
A CLI to guide you through the process of building an iExec iApp
257 lines • 11.8 kB
JavaScript
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