@acurast/cli
Version:
A cli to interact with the Acurast Cloud.
871 lines • 40 kB
JavaScript
import { Option } from 'commander';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { Listr } from 'listr2';
import { BigNumber } from 'bignumber.js';
import { confirm, select, input } from '@inquirer/prompts';
import { loadAcurastConfig, deployProject, } from '@acurast/sdk/deploy';
import { walletFromMnemonic, getBalance, convertConfigToJob, DEFAULT_START_DELAY, isStartAtMsFromNow, isStartAtTimestamp, jobIdFromChainJson, listAssignedProcessorAddressesForJob, } from '@acurast/sdk/chain';
import { parseByteSize, hasBenchmarkFilters } from '@acurast/sdk/chain';
import { checkMatch } from '@acurast/sdk/matcher';
import { DeploymentStatus, AssignmentStrategyVariant } from '@acurast/sdk/types';
import { validateCliConfig } from '../util/validateCliConfig.js';
import { checkMatchWithReward } from '@acurast/sdk/matcher';
import { getDevtoolsViewKey, buildDevtoolsUrl, injectDevtoolsSnippet, } from '@acurast/devtools';
import { getEnv, validateDeployEnvVars, getProjectEnvVars, getRpcForNetwork, getSymbolForNetwork, getMatcherUrlForNetwork, getIpfsConfig, getHubUrl, } from '../config.js';
import { getSigningMode, getLoggedInAddress, getExpiredAuth, touchAuth, } from '../util/authStore.js';
import { signingNoticeLine, expiredSessionMessage, } from '../util/signingNotice.js';
import { startSignServer } from '../util/cliServer.js';
import { RemoteSigner } from '../acurast/remoteSigner.js';
import { buildDeploySummary } from '../acurast/deploySummary.js';
import { storeDeployment } from '../acurast/storeDeployment.js';
import { acurastColor } from '../util.js';
import { humanTime } from '../util/humanTime.js';
import { consoleOutput } from '../util/console-output.js';
import { printBundleContents } from '../util/printBundleContents.js';
import { getFaucetLinkForAddress } from '../constants.js';
import * as ora from '../util/ora.js';
import { filelogger } from '../util/fileLogger.js';
import { fetchAndDisplayPricing } from '../util/fetchAndDisplayPricing.js';
import { LocalStorage } from '../util/LocalStorage.js';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const ACURAST_DECIMALS = 12;
/**
* Build the signer for a deploy. In `local` mode this is the mnemonic-derived
* `KeyringPair` (default/fallback). In `remote` mode the browser wallet signs:
* we start a local bridge server and return an injected {@link RemoteSigner}
* bound to the address persisted by `acurast login`.
*/
/**
* Render an on-chain job id as the composite "Acurast:<owner>:<number>" form.
*
* `jobIds[0]` from the match event is the `[MultiOrigin, number]` tuple, and
* the origin carries the owning account. Returns undefined for shapes we
* cannot read, or for non-Acurast origins, whose consumer is raw bytes rather
* than an account — callers fall back to the bare number.
*/
export function compositeDeploymentId(raw) {
if (!Array.isArray(raw) || raw.length < 2)
return undefined;
const [origin, number] = raw;
if (origin === null || typeof origin !== 'object')
return undefined;
const entries = Object.entries(origin);
const acurast = entries.find(([key]) => key.toLowerCase() === 'acurast');
if (acurast === undefined || typeof acurast[1] !== 'string')
return undefined;
const seq = String(number);
if (!/^\d+$/.test(seq))
return undefined;
return `Acurast:${acurast[1]}:${seq}`;
}
export async function resolveSigner(mode, config, ipfsRef) {
if (mode === 'remote') {
const address = getLoggedInAddress();
if (!address) {
throw new Error('Remote signing requires a logged-in address. Run `acurast login` first, or set ACURAST_MNEMONIC.');
}
const signServer = await startSignServer(getHubUrl());
let closed = false;
const cleanup = () => {
if (closed)
return;
closed = true;
try {
signServer.close();
}
catch {
// ignore
}
};
// `process.exit()` bypasses try/finally, so guarantee the server is closed
// on normal exit and on Ctrl-C / termination.
process.once('exit', cleanup);
process.once('SIGINT', () => {
cleanup();
process.exit(130);
});
process.once('SIGTERM', () => {
cleanup();
process.exit(143);
});
const signer = new RemoteSigner({
bridgeUrl: signServer.bridgeUrl,
requestSignature: signServer.requestSignature,
getSummary: (_payload, callIndex) => buildDeploySummary(config, {
kind: callIndex === 0 ? 'deploy' : 'setEnvironments',
ipfsRef: ipfsRef.value,
}),
onOpen: (_id, url) => {
console.log('\nOpening your browser to sign the transaction with your wallet...');
console.log(`If it does not open automatically, visit:\n ${url}\n`);
},
});
touchAuth();
return { wallet: { address, signer }, cleanup };
}
return {
wallet: await walletFromMnemonic(getEnv('ACURAST_MNEMONIC'), {
name: 'AcurastCli',
}),
cleanup: () => { },
};
}
export async function promptPricingAdjustment(advice, config, matcherUrl, accountId, options) {
const symbol = getSymbolForNetwork(config.network);
if (advice.status === 'sufficient') {
return undefined; // No adjustment needed
}
if (advice.status === 'overpaying') {
const suggestedDisplay = advice.suggestedPrice
? new BigNumber(advice.suggestedPrice)
.shiftedBy(-ACURAST_DECIMALS)
.toFixed()
: null;
const choices = [];
if (suggestedDisplay && advice.suggestedPrice) {
choices.push({
value: 'suggested',
name: `Lower to suggested price (${suggestedDisplay} ${symbol})`,
});
}
choices.push({ value: 'keep', name: 'Keep current price' }, { value: 'custom', name: 'Enter a custom price' });
const action = await select({
message: 'Your price is higher than needed. What would you like to do?',
choices,
});
if (action === 'keep')
return undefined;
if (action === 'suggested' && advice.suggestedPrice) {
return advice.suggestedPrice.toFixed();
}
if (action === 'custom') {
return promptCustomPrice(config, matcherUrl, accountId, symbol);
}
}
if (advice.status === 'insufficient') {
const suggestedDisplay = advice.suggestedPrice
? new BigNumber(advice.suggestedPrice)
.shiftedBy(-ACURAST_DECIMALS)
.toFixed()
: null;
const choices = [];
if (suggestedDisplay && advice.suggestedPrice) {
choices.push({
value: 'suggested',
name: `Use suggested price (${suggestedDisplay} ${symbol} — covers ${advice.requiredProcessors} ${advice.requiredProcessors === 1 ? 'processor' : 'processors'})`,
});
}
choices.push({ value: 'custom', name: 'Enter a custom price' }, { value: 'keep', name: 'Continue with current price (may not match)' }, { value: 'abort', name: 'Abort deployment' });
const action = await select({
message: 'Not enough processors at your current price. What would you like to do?',
choices,
});
if (action === 'abort')
return 'abort';
if (action === 'keep')
return undefined;
if (action === 'suggested' && advice.suggestedPrice) {
return advice.suggestedPrice.toFixed();
}
if (action === 'custom') {
return promptCustomPrice(config, matcherUrl, accountId, symbol);
}
}
return undefined;
}
async function promptCustomPrice(config, matcherUrl, accountId, symbol, retries = 3) {
for (let i = 0; i < retries; i++) {
const priceStr = await input({
message: `Enter price in ${symbol} (e.g., 0.05):`,
validate: (val) => {
const n = new BigNumber(val);
if (n.isNaN() || n.lte(0))
return 'Please enter a valid positive number';
return true;
},
});
const priceSatoshi = new BigNumber(priceStr)
.shiftedBy(ACURAST_DECIMALS)
.integerValue(BigNumber.ROUND_CEIL);
// Verify match with the new price
const job = convertConfigToJob({
...config,
maxCostPerExecution: priceSatoshi.toNumber(),
});
const result = await checkMatchWithReward(matcherUrl, config, job, accountId, priceSatoshi.toFixed());
if (result.ok) {
const matched = result.data.matched_processors;
console.log(` At ${priceStr} ${symbol}: ${matched} ${matched === 1 ? 'processor' : 'processors'} matched (${config.numberOfReplicas} required)`);
if (result.data.matched_processors >= config.numberOfReplicas) {
const proceed = await confirm({
message: `Use ${priceStr} ${symbol} per execution?`,
default: true,
});
if (proceed)
return priceSatoshi.toFixed();
}
else {
if (i < retries - 1) {
console.log(' Still not enough processors. Try a higher price.');
}
}
}
else {
console.log(` Could not verify price: ${result.error}`);
}
}
const action = await select({
message: 'Could not find a sufficient price. What would you like to do?',
choices: [
{ value: 'keep', name: 'Continue with original price' },
{ value: 'abort', name: 'Abort deployment' },
],
});
return action === 'abort' ? 'abort' : undefined;
}
export const addCommandDeploy = (program) => {
const deployCmd = program
.command('deploy [project]')
// Options shared with the `vps` subcommand (e.g. --dry-run) must not be
// consumed by `deploy` when they appear after the subcommand name.
.enablePositionalOptions()
.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.'))
.addOption(new Option('--min-memory <size>', 'Minimum total RAM (e.g. 4GB, 512MiB). Merges with benchmarkFilters in acurast.json.'))
.addOption(new Option('--min-cpu-score <n>', 'Minimum CPU single-core benchmark score').argParser((v) => {
const n = Number.parseInt(v, 10);
if (Number.isNaN(n) || n < 0) {
throw new Error(`Invalid --min-cpu-score: ${v}`);
}
return n;
}))
.addOption(new Option('--min-storage <size>', 'Minimum available storage capacity (e.g. 64GB)'))
.addOption(new Option('--min-cpu-multi-score <n>', 'Minimum CPU multi-core benchmark score').argParser((v) => {
const n = Number.parseInt(v, 10);
if (Number.isNaN(n) || n < 0) {
throw new Error(`Invalid --min-cpu-multi-score: ${v}`);
}
return n;
}))
.action(async (project, options) => {
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 = loadAcurastConfig({ project });
}
catch (e) {
log(e.message);
return;
}
// console.log(config);
if (!config) {
throw new Error('No project found');
}
const configResult = validateCliConfig(config);
if (!configResult.success) {
log('');
log('⚠️ Project config is invalid:');
log('');
log(configResult.error);
filelogger.error(`Config is invalid ${JSON.stringify(configResult.error)}`);
return;
}
config = configResult.data;
try {
const bf = { ...config.benchmarkFilters };
if (options.minMemory !== undefined) {
bf.minRamTotalBytes = Number(parseByteSize(options.minMemory));
}
if (options.minCpuScore !== undefined) {
bf.minCpuSingleCoreScore = options.minCpuScore;
}
if (options.minStorage !== undefined) {
bf.minStorageAvailBytes = Number(parseByteSize(options.minStorage));
}
if (options.minCpuMultiScore !== undefined) {
bf.minCpuMultiCoreScore = options.minCpuMultiScore;
}
if (options.minMemory !== undefined ||
options.minCpuScore !== undefined ||
options.minStorage !== undefined ||
options.minCpuMultiScore !== undefined) {
config.benchmarkFilters = bf;
}
}
catch (e) {
log(`Invalid benchmark deploy option: ${e.message}`);
return;
}
const benchValidation = validateCliConfig(config);
if (!benchValidation.success) {
log('');
log('⚠️ Project config is invalid after benchmark options:');
log('');
log(benchValidation.error);
return;
}
config = benchValidation.data;
await executeDeployFlow(config, () => getProjectEnvVars(config), options, configResult.notes);
});
return deployCmd;
};
/**
* The shared deploy pipeline: signer resolution, balance check, pricing
* advice, on-chain submission and progress rendering. Used by
* `acurast deploy` and `acurast deploy vps`.
*/
export async function executeDeployFlow(config, getEnvVars, options, notes, beforeExit) {
const log = consoleOutput(options.output);
const toAcurastColor = (text) => {
if (options.output === 'json') {
return text;
}
return acurastColor(text);
};
const signingMode = getSigningMode();
filelogger.info(`Signing mode: ${signingMode}`);
// An aged-out login silently resolves back to `local`, so without a mnemonic
// the env-var check below would blame a missing ACURAST_MNEMONIC. Say what
// actually happened instead.
if (signingMode === 'local' && !process.env.ACURAST_MNEMONIC) {
const expired = getExpiredAuth();
if (expired) {
filelogger.warn(`Login expired (${expired.scope}), last logged in at ${expired.record.loggedInAt}`);
log(expiredSessionMessage(expired));
return;
}
}
try {
validateDeployEnvVars({ requireMnemonic: signingMode === 'local' });
}
catch (e) {
filelogger.error(`Deploy env vars are invalid ${JSON.stringify(e.message)}`);
log(e.message);
return;
}
let envVars = [];
try {
envVars = getEnvVars();
}
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 (notes) {
filelogger.warn(`Project config is valid, but here are some notes: ${JSON.stringify(notes)}`);
log('⚠️ Project config is valid, but here are some notes:');
notes.forEach((issue) => {
log(`- ${issue.message}`);
});
log('');
}
const spinner = ora.default('Fetching account balance...');
spinner.start();
// Primed from the `Uploaded` status callback so the remote-signing
// summary shown on the hub can include the IPFS reference.
const ipfsRef = { value: null };
const { wallet } = await resolveSigner(signingMode, config, ipfsRef);
log(signingNoticeLine(signingMode, wallet.address));
log('');
const rpcEndpoint = getRpcForNetwork(config.network);
filelogger.info(`Connecting to ${config.network} RPC: ${rpcEndpoint}`);
const wsProvider = new WsProvider(rpcEndpoint);
const api = await ApiPromise.create({
provider: wsProvider,
noInitWarn: true,
});
const balance = await getBalance(api, wallet.address);
const symbol = getSymbolForNetwork(config.network);
filelogger.debug(`Balance: ${balance} ${symbol}`);
await api.disconnect();
spinner.stop();
if (balance === 0) {
if (config.network === 'canary') {
log(`Your balance is 0. Visit ${toAcurastColor(getFaucetLinkForAddress(wallet.address))} to get some tokens.`);
}
else {
log(`Your balance is 0. You need ${symbol} tokens to deploy.`);
log(`Acquire ${symbol} (see ${toAcurastColor('https://docs.acurast.com/token-holders/how-to-get-acu/')}) and send it to your address:`);
log(` ${toAcurastColor(wallet.address)}`);
}
log('');
return;
}
else if (balance < 1) {
if (config.network === 'canary') {
log(`Your balance is low. Visit ${toAcurastColor(getFaucetLinkForAddress(wallet.address))} to get some tokens.`);
}
else {
log(`Your balance is low. You need more ${symbol} tokens to deploy.`);
log(`Acquire ${symbol} (see ${toAcurastColor('https://docs.acurast.com/token-holders/how-to-get-acu/')}) and send it to your address:`);
log(` ${toAcurastColor(wallet.address)}`);
}
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('');
// Gate on the actual env vars, not config.includeEnvironmentVariables:
// `deploy vps` passes vars directly without listing them in the config. If
// this is false the CLI exits right after acknowledgements — while the SDK
// is still submitting the env vars — and they never reach the processor.
const hasEnvironmentVariables = envVars.length > 0;
// --- Pricing check via matcher API ---
const matcherUrl = getMatcherUrlForNetwork(config.network);
const isInteractive = !options.nonInteractive && options.output === 'text';
const hasInstantMatch = config.assignmentStrategy.type === AssignmentStrategyVariant.Single &&
config.assignmentStrategy.instantMatch &&
config.assignmentStrategy.instantMatch.length > 0;
if (hasInstantMatch) {
log('Instant match processors specified — skipping market pricing check.');
log('');
}
const pricingSpinner = ora.default('Checking processor pricing...');
const pricingAdvice = await fetchAndDisplayPricing(config, wallet.address, options, pricingSpinner);
// Interactive pricing adjustment (deploy-specific)
if (pricingAdvice && isInteractive && !options.dryRun && matcherUrl) {
const adjustedPrice = await promptPricingAdjustment(pricingAdvice, config, matcherUrl, wallet.address, options);
if (adjustedPrice === 'abort') {
log('Deployment aborted.');
return;
}
if (adjustedPrice !== undefined) {
config.maxCostPerExecution = Number(adjustedPrice);
log(`Updated max cost per execution to ${toAcurastColor(new BigNumber(adjustedPrice).shiftedBy(-12).toFixed())} ${symbol}`);
log('');
}
}
if (hasBenchmarkFilters(config) && matcherUrl && !hasInstantMatch) {
const previewJob = convertConfigToJob(config);
const matchRes = await checkMatch(matcherUrl, config, previewJob, wallet.address);
if (options.output === 'text') {
if (matchRes.ok) {
log(`Processors matching benchmark filters at current reward: ${matchRes.data.matched_processors} (${config.numberOfReplicas} required)`);
}
else {
log(`Could not verify benchmark filter match: ${matchRes.error}`);
}
log('Assigned processor addresses are listed from chain storage after the deployment is matched.');
log('');
}
// A dry run submits nothing, so an insufficient match must not abort it —
// otherwise it never reaches the "Dry run, not deploying" exit.
if (options.nonInteractive && !options.dryRun) {
if (!matchRes.ok ||
matchRes.data.matched_processors < config.numberOfReplicas) {
log('Deploy aborted: benchmark filters do not yield enough processors at the current price (non-interactive mode).');
return;
}
}
else if (isInteractive && !options.dryRun) {
const go = await confirm({
message: 'Deploy with these benchmark filters?',
default: true,
});
if (!go) {
log('Deployment cancelled.');
return;
}
}
}
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;
let deployedJobId = undefined;
let deployedJobIdRaw = undefined;
const job = convertConfigToJob(config);
const devtoolsApiUrl = getEnv('ACURAST_DEVTOOLS_API_URL');
const jobRegistration = deployProject(config, job, {
wallet,
rpcEndpoint,
ipfs: getIpfsConfig(),
envVars,
onlyUpload: options.onlyUpload ?? false,
keyStore: new LocalStorage(),
logger: filelogger,
transformBundle: async ({ zipPath, entrypoint }) => {
const finalPath = config.enableDevtools
? await injectDevtoolsSnippet(zipPath, entrypoint, devtoolsApiUrl)
: zipPath;
if (options.output === 'text') {
printBundleContents(finalPath, config.projectName, log);
}
return finalPath;
},
statusCallback: async (status, data) => {
// console.log(status, data)
if (options.output === 'json') {
log('', JSON.stringify({ status, data }));
}
if (status === DeploymentStatus.Uploaded) {
// Make the IPFS reference available to the remote-signing summary.
if (data?.ipfsHash)
ipfsRef.value = data.ipfsHash;
}
else if (status === DeploymentStatus.Prepared) {
// console.log(status, data);
jobRegistrationTemp = data.job;
await 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!');
}
deployedJobId = String(data.jobIds[0]?.[1] ?? data.jobIds[0]);
deployedJobIdRaw = data.jobIds[0];
await 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') {
async function awaitStatus(status) {
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: async (ctx, task) => {
const { job } = await 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: async (ctx, task) => {
await awaitStatus(DeploymentStatus.Submit);
const { jobIds } = await 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: async (ctx, task) => {
const matchData = await awaitStatus(DeploymentStatus.Matched);
task.title = 'Matched';
if (options.output !== 'text' || !matchData?.jobIds?.length) {
return;
}
const provider = new WsProvider(rpcEndpoint);
const api = await ApiPromise.create({
provider,
noInitWarn: true,
});
try {
log('');
log('Assigned processors:');
for (const rawJobId of matchData.jobIds) {
try {
const jobId = jobIdFromChainJson(rawJobId);
const addresses = await listAssignedProcessorAddressesForJob(api, jobId);
if (addresses.length === 0) {
log(` Job ${jobId[1]}: no rows yet (storage may update shortly)`);
}
else {
log(` Job ${jobId[1]}:`);
for (const a of addresses) {
log(` ${a}`);
}
}
}
catch (e) {
const msg = e instanceof Error ? e.message : String(e);
filelogger.debug(`assignedProcessors list failed: ${msg}`);
log(` (Could not list assigned processors: ${msg})`);
}
}
log('');
}
finally {
await api.disconnect();
}
},
},
{
title: 'Waiting for processor acknowledgements',
enabled: () => !options.exitEarly ||
(options.exitEarly && hasEnvironmentVariables),
task: (ctx, task) => task.newListr([
{
title: `Acknowledged by 0/${config.numberOfReplicas}`,
task: async (ctx, task) => {
filelogger.info(`Waiting for processor acknowledgements: ${config.numberOfReplicas} expected`);
let allAcknowledged = false;
// while (!allAcknowledged) {
// TODO: Make reactive
const { acknowledged } = await 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: async (ctx, task) => {
const result = await awaitStatus(DeploymentStatus.EnvironmentVariablesSet);
if (result?.error) {
throw result.error;
}
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 {
await tasks.run();
if (config.enableDevtools && deployedJobId) {
try {
// Send the full "Acurast:<owner>:<number>" id: the devtools API
// resolves the owner from it to confirm we own this deployment. A
// bare number leaves it dependent on an indexer lookup instead.
const deploymentId = compositeDeploymentId(deployedJobIdRaw) ?? deployedJobId;
const viewKeyResponse = await getDevtoolsViewKey(deploymentId, {
apiUrl: getEnv('ACURAST_DEVTOOLS_API_URL'),
// The deployment's own signer: the API checks the signature
// against the owner recorded on chain, so it must be the account
// that registered the job. Also covers remote signing, where
// there is no mnemonic to fall back on.
signer: wallet,
network: config.network,
logger: filelogger,
});
log('');
log(`DevTools: ${toAcurastColor(buildDevtoolsUrl(getEnv('ACURAST_DEVTOOLS_URL'), deployedJobId, viewKeyResponse.viewKey))}`);
log(`View key expires at ${new Date(viewKeyResponse.expiresAt).toLocaleString()}`);
log('');
}
catch (e) {
filelogger.error(`Failed to get devtools view key: ${e.message}`);
log(`Warning: Could not retrieve DevTools view key: ${e.message}`);
}
}
if (beforeExit) {
await beforeExit({
jobId: deployedJobIdRaw,
jobIdString: deployedJobId,
});
}
process.exit(0);
}
catch (e) {
console.log('Error', e);
process.exit(1);
}
}
}
//# sourceMappingURL=deploy.js.map