@acurast/cli
Version:
A cli to interact with the Acurast Cloud.
309 lines • 14.8 kB
JavaScript
import { Option } from 'commander';
import fs from 'fs';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { AcurastService, walletFromMnemonic, getBalance, setEnvVars, getAcknowledgedProcessors, editScript, transferEditor, } from '@acurast/sdk/chain';
import { MultiOrigin } from '@acurast/sdk/types';
import { getEnv, getIndexerConfigForNetwork, getProjectEnvVars, getRpcForNetwork, getSymbolForNetwork, } from '../config.js';
import { filelogger } from '../util/fileLogger.js';
import * as ora from '../util/ora.js';
import { ACURAST_DEPLOYMENTS_PATH } from '../constants.js';
import { LocalStorage } from '../util/LocalStorage.js';
export const addCommandDeployments = (program) => {
program
.command('deployments')
.description('Manage deployments')
.argument('[arg]', 'Deployment ID or command (ls/list)')
.addOption(new Option('-e, --update-env-vars', 'Load the environment variables of a deployment and update them.'))
.addOption(new Option('-c, --cleanup', 'Remove old, finished deployments. This will return any unused funds locked in the deployment back to the user.'))
.addOption(new Option('-n, --network <network>', 'Network to use (mainnet, canary, or devnet)')
.choices(['mainnet', 'canary', 'devnet'])
.default('mainnet'))
.action(async (arg, options) => {
const wallet = await walletFromMnemonic(getEnv('ACURAST_MNEMONIC'), {
name: 'AcurastCli',
});
const readDeploymentFile = (deploymentId) => {
try {
const deploymentFilename = fs
.readdirSync(ACURAST_DEPLOYMENTS_PATH)
.find((f) => f.endsWith(`-${deploymentId}.json`));
if (deploymentFilename) {
return JSON.parse(fs.readFileSync(`${ACURAST_DEPLOYMENTS_PATH}/${deploymentFilename}`, 'utf8'));
}
}
catch {
// Directory may not exist yet
}
return undefined;
};
if (arg === 'ls' || arg === 'list') {
const spinner = ora.default('Loading deployments...');
spinner.start();
const indexerConfig = getIndexerConfigForNetwork(options.network);
let data;
try {
const response = await fetch(indexerConfig.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'API-Key': indexerConfig.apiKey,
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'getEvents',
params: {
pallet: 'Acurast',
variant: 'JobRegistrationStoredV2',
account_id: wallet.address,
sort_order: 'desc',
},
id: 1,
}),
});
data = (await response.json());
}
catch (err) {
spinner.stop();
console.log('Failed to fetch deployments:', err.message);
return;
}
spinner.stop();
if (data.error) {
console.log('Error fetching deployments:', data.error.message);
return;
}
const items = data.result?.items || [];
if (items.length === 0) {
console.log('No deployments found');
}
else {
console.log('You have the following deployments:');
items.forEach((item) => {
const jobId = item.data[1];
const blockTime = new Date(item.block_time).toLocaleDateString();
console.log(`${jobId} (registered ${blockTime})`);
});
}
return;
}
const deploymentId = Number(arg);
if (options.cleanup) {
const network = ((deploymentId &&
readDeploymentFile(deploymentId)?.config.network) ||
options.network);
const rpcForCleanup = getRpcForNetwork(network);
filelogger.info(`Connecting to ${network} RPC: ${rpcForCleanup}`);
const acurast = new AcurastService(rpcForCleanup);
if (deploymentId) {
const spinner = ora.default(`Cleaning up deployment ${deploymentId}...`);
spinner.start();
await acurast.deregisterJob(wallet, deploymentId);
spinner.stop();
console.log(`Done`);
}
else {
const spinner = ora.default(`Cleaning up old deployments...`);
spinner.start();
const jobs = await acurast.getAllJobs();
const now = Date.now();
const filteredJobs = jobs
.filter((job) => job.id[0].acurast === wallet.address)
.filter((job) => job.registration.schedule.endTime < now)
.sort((a, b) => a.id[1] - b.id[1]);
spinner.stop();
console.log(`Found ${filteredJobs.length} deployments to clean up`);
const rpcEndpoint = getRpcForNetwork(network);
filelogger.info(`Connecting to ${network} RPC: ${rpcEndpoint}`);
const wsProvider = new WsProvider(rpcEndpoint);
const api = await ApiPromise.create({
provider: wsProvider,
noInitWarn: true,
});
let balanceBefore = await getBalance(api, wallet.address);
for (const job of filteredJobs) {
spinner.start(`Cleaning up deployment ${job.id[1]}...`);
await acurast.deregisterJob(wallet, job.id[1]);
const balanceNew = await getBalance(api, wallet.address);
const diff = balanceNew - balanceBefore;
const symbol = getSymbolForNetwork(network);
spinner.succeed(`Deployment ${job.id[1]} cleaned up${diff > 0 ? `. ${symbol} regained: ${diff}` : ``}`);
balanceBefore = balanceNew;
}
await api.disconnect();
spinner.stop();
}
await acurast.disconnect();
return;
}
if (!deploymentId || isNaN(deploymentId)) {
console.log('Please provide a deployment ID');
return;
}
const deploymentFileData = readDeploymentFile(deploymentId);
if (!deploymentFileData) {
console.log('Could not find deployment file.');
return;
}
const network = deploymentFileData.config.network;
const rpcEndpoint = getRpcForNetwork(network);
filelogger.info(`Connecting to ${network} RPC: ${rpcEndpoint}`);
const job = {
id: deploymentFileData.deploymentId,
registration: deploymentFileData.registration,
};
if (options.updateEnvVars) {
const envVars = getProjectEnvVars(deploymentFileData.config);
job.envVars = envVars;
const spinner = ora.default(`Setting environment variables for deployment ${deploymentId}...`);
spinner.start();
if (!job.envVars || job.envVars.length === 0) {
throw new Error('No environment variables found for deployment');
}
const startTime = deploymentFileData.registration?.schedule?.startTime;
const abortIfPastStartMs = startTime && startTime > Date.now()
? startTime - 60000
: undefined;
const { hash } = await setEnvVars(job, {
wallet,
rpcEndpoint,
keyStore: new LocalStorage(),
abortIfPastStartMs,
});
spinner.succeed(`${job.envVars?.length} environment variables set`);
console.log('Transaction ID:', hash);
}
else {
if (job.id) {
console.log('Click here to open the deployment in your browser:');
console.log(`https://hub.acurast.com/job-detail/acurast-${job.id[0].acurast}-${deploymentId}`);
}
console.log('Deployment:', job);
const acurast = new AcurastService(rpcEndpoint);
await acurast.connect();
if (!acurast.api) {
throw new Error('API not connected');
}
const assignments = await getAcknowledgedProcessors(acurast.api, job.id);
console.log('Assignments:', JSON.stringify(assignments, null, 2));
acurast.disconnect();
return;
}
});
const deploymentsCommand = program.commands.find((c) => c.name() === 'deployments');
const updateCommand = deploymentsCommand
.command('update')
.description('Update deployment properties');
const parseDeploymentId = (deploymentId) => {
const parts = deploymentId.split(':');
if (parts.length !== 3) {
console.error('Invalid deployment ID format. Expected format: "origin:address:number" (e.g., "Acurast:5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL:123456")');
return null;
}
const [origin, address, numberStr] = parts;
const number = Number(numberStr);
if (isNaN(number)) {
console.error('Invalid deployment number. Please provide a valid number.');
return null;
}
if (origin !== 'Acurast') {
console.error('Invalid origin. Currently only "Acurast" is supported.');
return null;
}
if (!address.match(/^5[a-km-zA-HJ-NP-Z1-9]{47}$/)) {
console.error('Invalid address format. Please provide a valid AccountId32 address.');
return null;
}
return { origin, address, number };
};
updateCommand
.command('script')
.description('Update the script of a mutable deployment')
.argument('<deployment-id>', 'The deployment/job ID in format "origin:address:number" (e.g., "Acurast:5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL:123456")')
.argument('<script-ipfs>', 'IPFS hash of the new script (e.g., "ipfs://QmNewScriptHash")')
.addOption(new Option('-n, --network <network>', 'Network to use (mainnet, canary, or devnet)')
.choices(['mainnet', 'canary', 'devnet'])
.default('mainnet'))
.addOption(new Option('--dry-run', 'Preview the update without applying'))
.action(async (deploymentId, scriptIpfs, options) => {
const parsed = parseDeploymentId(deploymentId);
if (!parsed)
return;
if (!scriptIpfs.startsWith('ipfs://')) {
console.error('Invalid script format. Please provide an IPFS hash starting with "ipfs://"');
return;
}
if (options.dryRun) {
console.log(`[DRY RUN] Would update script for deployment [${parsed.origin}, ${parsed.address}, ${parsed.number}] with: ${scriptIpfs}`);
return;
}
const wallet = await walletFromMnemonic(getEnv('ACURAST_MNEMONIC'), {
name: 'AcurastCli',
});
const rpcForUpdate = getRpcForNetwork(options.network);
filelogger.info(`Connecting to ${options.network} RPC: ${rpcForUpdate}`);
const acurast = new AcurastService(rpcForUpdate);
const spinner = ora.default(`Updating script for deployment [${parsed.origin}, ${parsed.address}, ${parsed.number}]...`);
spinner.start();
try {
await acurast.connect();
if (!acurast.api) {
throw new Error('API not connected');
}
const txHash = await editScript(acurast.api, wallet, [MultiOrigin.Acurast, parsed.address, parsed.number], scriptIpfs);
spinner.succeed(`Script updated successfully`);
console.log('Transaction ID:', txHash);
}
catch (error) {
spinner.fail(`Failed to update script: ${error}`);
}
finally {
await acurast.disconnect();
}
});
updateCommand
.command('editor')
.description('Transfer editor permissions for a mutable deployment')
.argument('<deployment-id>', 'The deployment/job ID in format "origin:address:number" (e.g., "Acurast:5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL:123456")')
.argument('<new-editor-address>', 'The AccountId32 address of the new editor')
.addOption(new Option('-n, --network <network>', 'Network to use (mainnet, canary, or devnet)')
.choices(['mainnet', 'canary', 'devnet'])
.default('mainnet'))
.addOption(new Option('--dry-run', 'Preview the transfer without executing'))
.action(async (deploymentId, newEditorAddress, options) => {
const parsed = parseDeploymentId(deploymentId);
if (!parsed)
return;
if (!newEditorAddress.match(/^5[a-km-zA-HJ-NP-Z1-9]{47}$/)) {
console.error('Invalid editor address. Please provide a valid AccountId32 address.');
return;
}
if (options.dryRun) {
console.log(`[DRY RUN] Would transfer editor permissions for deployment [${parsed.origin}, ${parsed.address}, ${parsed.number}] to: ${newEditorAddress}`);
return;
}
const wallet = await walletFromMnemonic(getEnv('ACURAST_MNEMONIC'), {
name: 'AcurastCli',
});
const rpcForUpdate = getRpcForNetwork(options.network);
filelogger.info(`Connecting to ${options.network} RPC: ${rpcForUpdate}`);
const acurast = new AcurastService(rpcForUpdate);
const spinner = ora.default(`Transferring editor permissions for deployment [${parsed.origin}, ${parsed.address}, ${parsed.number}]...`);
spinner.start();
try {
await acurast.connect();
if (!acurast.api) {
throw new Error('API not connected');
}
const txHash = await transferEditor(acurast.api, wallet, [MultiOrigin.Acurast, parsed.address, parsed.number], newEditorAddress);
spinner.succeed(`Editor permissions transferred successfully`);
console.log('Transaction ID:', txHash);
}
catch (error) {
spinner.fail(`Failed to transfer editor permissions: ${error}`);
}
finally {
await acurast.disconnect();
}
});
};
//# sourceMappingURL=deployments.js.map