@iexec/iapp
Version:
A CLI to guide you through the process of building an iExec iApp
338 lines (312 loc) • 10.4 kB
text/typescript
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();
}