UNPKG

@acurast/cli

Version:

A cli to interact with the Acurast Cloud.

452 lines 22.4 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Option } from 'commander'; import { loadConfig } from '../acurast/loadConfig.js'; import { getEnv, validateDeployEnvVars, RPC, getProjectEnvVars, } from '../config.js'; import { Listr } from 'listr2'; import { createJob } from '../acurast/createJob.js'; import { storeDeployment } from '../acurast/storeDeployment.js'; import { acurastColor } from '../util.js'; import { humanTime } from '../util/humanTime.js'; import { convertConfigToJob, DEFAULT_START_DELAY, isStartAtMsFromNow, isStartAtTimestamp, } from '../acurast/convertConfigToJob.js'; import { validateConfig } from '../util/validateConfig.js'; import { DeploymentStatus } from '../acurast/types.js'; import { consoleOutput } from '../util/console-output.js'; import { getWallet } from '../util/getWallet.js'; import { getBalance } from '../util/getBalance.js'; import { ApiPromise, WsProvider } from '@polkadot/api'; import { getFaucetLinkForAddress } from '../constants.js'; import * as ora from '../util/ora.js'; import { filelogger } from '../util/fileLogger.js'; import { BigNumber } from 'bignumber.js'; const ACURAST_DECIMALS = 12; const DEFAULT_BASE_FEE = new BigNumber('2000000000'); const DEFAULT_FEE_PER_MILLIS = new BigNumber(1); const DEFAULT_FEE_PER_BYTE = new BigNumber(1); const suggestReward = (duration, storage) => { if (duration === null) { throw Error('Invalid duration'); } const minDefaultReward = DEFAULT_FEE_PER_MILLIS.times(duration) .plus(DEFAULT_FEE_PER_BYTE.times(storage)) .plus(DEFAULT_BASE_FEE); const suggestedReward = minDefaultReward.times(2); return suggestedReward.shiftedBy(-ACURAST_DECIMALS).toNumber(); }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); export const addCommandDeploy = (program) => { program .command('deploy [project]') .description('Deploy the current project to the Acurast platform.') .addOption(new Option('-d, --dry-run', 'Run the deploy step without actually deploying the project.')) .addOption(new Option('-o, --output <format>', 'Output a json on each of the steps of the deployment process. This is useful if the CLI is started from a script or another program.') .choices(['text', 'json']) .default('text')) .addOption(new Option('-ee, --exit-early', 'Do not wait for the deployment to finish. The CLI will exit as soon as it has submitted the deployment to the Acurast platform. Note: If environment variables are set, the CLI will have to wait longer.')) .addOption(new Option('-n, --non-interactive', 'Do not ask for any input. Use this when triggering the CLI in a CD/CI pipeline.')) .addOption(new Option('-u, --only-upload', 'Only upload to IPFS and quit.')) .action((project, options) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c; const log = consoleOutput(options.output); const toAcurastColor = (text) => { if (options.output === 'json') { return text; } return acurastColor(text); }; const DEBUG = getEnv('DEBUG'); if (DEBUG === 'true' && options) { // console.log("Options", options); } let config; try { config = loadConfig(project); } catch (e) { log(e.message); return; } // console.log(config); if (!config) { throw new Error('No project found'); } const configResult = validateConfig(config); if (!configResult.success) { log(''); log('⚠️ Project config is invalid:'); log(''); log(configResult.error); filelogger.error(`Config is invalid ${JSON.stringify(configResult.error)}`); return; } try { validateDeployEnvVars(); } catch (e) { filelogger.error(`Deploy env vars are invalid ${JSON.stringify(e.message)}`); log(e.message); return; } let envVars = []; try { envVars = getProjectEnvVars(config); } catch (e) { filelogger.error(`Project env vars are invalid ${JSON.stringify(e.message)}`); log(e.message); return; } log(''); log(`Deploying project "${config.projectName}"`); log(''); if (configResult.notes) { filelogger.warn(`Project config is valid, but here are some notes: ${JSON.stringify(configResult.notes)}`); log('⚠️ Project config is valid, but here are some notes:'); configResult.notes.forEach((issue) => { log(`- ${issue.message}`); }); log(''); } const spinner = ora.default('Fetching account balance...'); spinner.start(); const wallet = yield getWallet(); const wsProvider = new WsProvider(RPC); const api = yield ApiPromise.create({ provider: wsProvider, noInitWarn: true, }); const balance = yield getBalance(wallet.address, api); filelogger.debug(`Balance: ${balance} cACU`); yield api.disconnect(); spinner.stop(); if (balance === 0) { log(`Your balance is 0. Visit ${toAcurastColor(getFaucetLinkForAddress(wallet.address))} to get some tokens.`); log(''); return; } else if (balance < 1) { log(`Your balance is low. Visit ${toAcurastColor(getFaucetLinkForAddress(wallet.address))} to get some tokens.`); log(''); } log('The CLI will use the following address: ' + wallet.address); log(''); // TODO: Deduplicate this code const now = Date.now(); let startTime = now + DEFAULT_START_DELAY; if (config.startAt) { if (isStartAtMsFromNow(config.startAt)) { startTime = now + config.startAt.msFromNow; } if (isStartAtTimestamp(config.startAt)) { startTime = new Date(config.startAt.timestamp).getTime(); } } if (startTime < now) { log(`Start time cannot be in the past`); filelogger.error(`Start time cannot be in the past: ${startTime}`); return; } filelogger.debug(`Start time: ${startTime}`); filelogger.debug(`The deployment will be scheduled to start in ${humanTime(now - startTime, true)}. (${new Date(startTime).toLocaleString()}) It will run for ${config.execution.type === 'onetime' ? humanTime(config.execution.maxExecutionTimeInMs, true) : humanTime(config.execution.numberOfExecutions * config.execution.intervalInMs)}.`); log(`The deployment will be scheduled to start in ${toAcurastColor(`${humanTime(now - startTime, true)}`)}. (${new Date(startTime).toLocaleString()}) It will run for ${toAcurastColor(config.execution.type === 'onetime' ? humanTime(config.execution.maxExecutionTimeInMs, true) : humanTime(config.execution.numberOfExecutions * config.execution.intervalInMs))}.`); log(''); const hasEnvironmentVariables = ((_b = (_a = config.includeEnvironmentVariables) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; const numberOfExecutions = config.execution.type === 'onetime' ? 1 : config.execution.numberOfExecutions; const costPerExecution = config.maxCostPerExecution; const costPerExecutionAndReplicas = config.maxCostPerExecution * config.numberOfReplicas; const totalCost = numberOfExecutions * costPerExecutionAndReplicas; const pluralize = (number, text) => { return number === 1 ? text : text + 's'; }; log(`There will be ${toAcurastColor(numberOfExecutions.toString())} ${pluralize(numberOfExecutions, 'execution')} with ${toAcurastColor(config.numberOfReplicas.toString())} ${pluralize(config.numberOfReplicas, 'replica')}. (Total runs: ${toAcurastColor((numberOfExecutions * config.numberOfReplicas).toString())})`); log(`Each replica has a cost of ${toAcurastColor((costPerExecution / 1000000000000).toString())} cACU, which means each execution will cost ${toAcurastColor((costPerExecutionAndReplicas / 1000000000000).toString())} cACU.`); log(`The total cost will be ${toAcurastColor((totalCost / 1000000000000).toString())} cACU.`); log(''); const job = convertConfigToJob(config); const suggestedReward = suggestReward(job.schedule.duration, job.storage); log(`The calculated suggested reward for your deployment is ${toAcurastColor(suggestedReward.toString())} cACU.`); log(''); if (options.dryRun) { filelogger.debug('🧪 Dry run, not deploying.'); log('🧪 Dry run, not deploying.'); return; } filelogger.debug('🚀 Deploying...'); log('🚀 Deploying...'); log(''); const originalConfig = structuredClone(config); const deploymentTime = new Date(); let jobRegistrationTemp = undefined; const jobRegistration = createJob(config, job, RPC, envVars, (_c = options.onlyUpload) !== null && _c !== void 0 ? _c : false, (status, data) => __awaiter(void 0, void 0, void 0, function* () { // console.log(status, data) if (options.output === 'json') { log('', JSON.stringify({ status, data })); } if (status === DeploymentStatus.Uploaded) { // ipfsHash // console.log(status, data); } else if (status === DeploymentStatus.Prepared) { // console.log(status, data); jobRegistrationTemp = data.job; yield storeDeployment(deploymentTime, originalConfig, jobRegistrationTemp); } else if (status === DeploymentStatus.Submit) { // txHash // console.log(status, data); } else if (status === DeploymentStatus.WaitingForMatch) { if (!jobRegistrationTemp) { throw new Error('Deployment Registration is null!'); } yield storeDeployment(deploymentTime, originalConfig, jobRegistrationTemp, data.jobIds[0]); if (options.output === 'json' && options.exitEarly && !hasEnvironmentVariables) { process.exit(0); } // jobIds // console.log(status, data); } else if (status === DeploymentStatus.Matched) { // console.log(status, data); } else if (status === DeploymentStatus.Acknowledged) { // acknowledged // console.log(status, data); } else if (status === DeploymentStatus.Started) { // console.log(status, data); } else if (status === DeploymentStatus.EnvironmentVariablesSet) { // console.log(status, data); if (options.output === 'json' && options.exitEarly && hasEnvironmentVariables) { process.exit(0); } } else if (status === DeploymentStatus.ExecutionDone) { // console.log(status, data); } else if (status === DeploymentStatus.Finalized) { // console.log(status, data); process.exit(0); } else { throw new Error('Unknown status'); } if (statusPromises[status]) { statusPromises[status].resolve(data); } })); const statusPromises = { [DeploymentStatus.Uploaded]: createStatusPromise(), [DeploymentStatus.Prepared]: createStatusPromise(), [DeploymentStatus.Submit]: createStatusPromise(), [DeploymentStatus.WaitingForMatch]: createStatusPromise(), [DeploymentStatus.Matched]: createStatusPromise(), [DeploymentStatus.Acknowledged]: createStatusPromise(), [DeploymentStatus.EnvironmentVariablesSet]: createStatusPromise(), [DeploymentStatus.Started]: createStatusPromise(), [DeploymentStatus.ExecutionDone]: createStatusPromise(), [DeploymentStatus.Finalized]: createStatusPromise(), }; function createStatusPromise() { let resolveFunction; const promise = new Promise((resolve) => { resolveFunction = resolve; }); return { promise, resolve: resolveFunction }; } if (options.output === 'text') { function awaitStatus(status) { return __awaiter(this, void 0, void 0, function* () { return statusPromises[status].promise; }); } let count = 1000000; // TODO: replace with duration until start time let deployingTimer; const cancelUpdateTitle = (task, success) => { deployingTimer && clearTimeout(deployingTimer); task.title = 'Deploying project'; if (!success) { tasks.tasks.forEach((task) => { task.complete(); }); } }; const updateTitle = (task) => { deployingTimer = setTimeout(() => { task.title = 'Deploying project (first execution scheduled in ' + count + 's)'; // task.title = // 'Waiting for executions (first execution scheduled in ' + // count + // 's)' if (count > 0) { count--; updateTitle(task); } else { cancelUpdateTitle(task, false); } }, 1000); }; const tasks = new Listr([ { title: 'Deploying project', task: (ctx, deployTask) => deployTask.newListr([ { title: 'Submit to Acurast', task: (ctx, task) => __awaiter(void 0, void 0, void 0, function* () { const { job } = yield awaitStatus(DeploymentStatus.Prepared); count = Math.floor((job.schedule.startTime - Date.now()) / 1000); updateTitle(tasks.tasks[0]); task.title = `Submitted to Acurast (${job.script})`; }), }, { title: 'Waiting for deployment to be registered', task: (ctx, task) => __awaiter(void 0, void 0, void 0, function* () { yield awaitStatus(DeploymentStatus.Submit); const { jobIds } = yield awaitStatus(DeploymentStatus.WaitingForMatch); task.title = 'Deployment registered' + ` (ID: ${jobIds .map((jobId) => jobId[1]) .join(' | ')})`; }), }, { title: 'Waiting for deployment to be matched with processors', enabled: () => !options.exitEarly || (options.exitEarly && hasEnvironmentVariables), task: (ctx, task) => __awaiter(void 0, void 0, void 0, function* () { yield awaitStatus(DeploymentStatus.Matched); task.title = 'Matched'; }), }, { title: 'Waiting for processor acknowledgements', enabled: () => !options.exitEarly || (options.exitEarly && hasEnvironmentVariables), task: (ctx, task) => task.newListr([ { title: `Acknowledged by 0/${config.numberOfReplicas}`, task: (ctx, task) => __awaiter(void 0, void 0, void 0, function* () { filelogger.info(`Waiting for processor acknowledgements: ${config.numberOfReplicas} expected`); let allAcknowledged = false; // while (!allAcknowledged) { // TODO: Make reactive const { acknowledged } = yield awaitStatus(DeploymentStatus.Acknowledged); filelogger.info(`Acknowledged by ${acknowledged}/${config.numberOfReplicas}`); task.title = `Acknowledged by ${acknowledged}/${config.numberOfReplicas}`; // } }), }, // { // title: 'DEMO: 5Ffda...fdkga', // task: async (ctx, task): Promise<void> => { // await delay(3000) // }, // }, // { // title: 'DEMO: 5Dcar...gdahs', // task: async (ctx, task): Promise<void> => { // await delay(2000) // throw new Error( // task.title + ' Processor did not acknowledge' // ) // }, // }, // { // title: 'DEMO: 5Dacs...lwpfd', // task: async (ctx, task): Promise<void> => { // await delay(1000) // }, // }, ], { concurrent: true, exitOnError: true }), }, { title: 'Setting environment variables', enabled: () => hasEnvironmentVariables, task: (ctx, task) => __awaiter(void 0, void 0, void 0, function* () { const { envVars } = yield awaitStatus(DeploymentStatus.EnvironmentVariablesSet); task.title = `Environment variables set`; }), }, ]), }, // { // title: // 'Waiting for executions (this may take a while, feel free to cancel this task and check back later)', // task: (ctx, task): Listr => // task.newListr( // [ // { // title: 'Waiting for Execution', // task: async (ctx, task): Promise<void> => { // await delay(3000) // task.title = 'DEMO: Execution succeeded' // }, // }, // { // title: 'Waiting for Execution', // task: async (ctx, task): Promise<void> => { // await delay(3000) // throw new Error('DEMO: Execution failed') // }, // }, // ], // { exitOnError: false } // ), // }, // { // title: 'Waiting for job to Finalize', // task: async (ctx, task): Promise<void> => { // await delay(3000) // task.title = 'Finalized' // }, // }, ], { concurrent: false, rendererOptions: { collapseSubtasks: false } }); jobRegistration .then((job) => { // console.log(job); }) .catch((err) => { // console.error(err) // if (err.message) { // throw new Error(err.message) // } throw err; }); try { yield tasks.run(); process.exit(0); } catch (e) { console.log('Error', e); process.exit(1); } } })); }; //# sourceMappingURL=deploy.js.map