UNPKG

@nfps.dev/cli

Version:

CLI for NFP development, inspection, and manipulation

254 lines 9.14 kB
import { access, constants, readFile, writeFile } from 'fs/promises'; import vm from 'vm'; import { base64_to_buffer, hex_to_buffer, oderac, ode, escape_regex } from '@blake.regalia/belt'; import { SecretApp } from '@solar-republic/neutrino'; import { query_contract_infer, SecretContract, Wallet, bech32_decode, exec_contract } from '@solar-republic/neutrino'; import { configDotenv } from 'dotenv'; import kleur from 'kleur'; import prompts from 'prompts'; import { H_DEFAULT_NETWORKS, X_GAS_PRICE_DEFAULT } from './constants.js'; let b_quiet = false; export function define_command(gc_cmd) { return { ...gc_cmd, ...gc_cmd.handler ? { async handler(g_argv) { // enable quiet mode if (g_argv['quiet']) b_quiet = true; // handle uncaught errors try { await gc_cmd.handler(g_argv); } catch (e_caught) { return exit(e_caught instanceof Error ? e_caught.message : e_caught + ''); } }, } : {}, }; } export const debug = (s_out) => ((b_quiet ? void 0 : process.stderr.write(s_out + '\n'), void 0)); // eslint-disable-next-line no-console export const result = (s_out) => console.log(s_out); // user-friendly output export const print = (s_header, h_fields) => debug(kleur.bold(s_header) + (h_fields ? '\n' + oderac(h_fields, (si_key, s_label) => ` ${kleur.gray(si_key)}: ${s_label}`).join('\n') + '\n' : '')); // die with error export const exit = (s_error) => { console.error(kleur.red(s_error)); process.exit(1); }; export async function load(g_argv, // Record<keyof typeof H_OPTS_EXEC, Nilable<boolean | number | string>>, a_reqs = [] // eslint-disable-line @typescript-eslint/naming-convention ) { // load environment variables configDotenv(); const h_env = process.env; // destructure env vars const { NFP_WALLET_PRIVATE_KEY: sh_sk, NFP_VIEWING_KEY: sh_vk, NFP_SELF_CHAIN: si_chain, NFP_WEB_LCDS: s_lcds, NFP_WEB_RPCS: s_rpcs, NFP_SELF_CONTRACT: sa_contract, NFP_SELF_TOKEN: si_token, } = h_env; // missing chain id if (!si_chain) return exit('Missing NFP_SELF_CHAIN variable in .env file'); // no private key if (!sh_sk) return exit('Missing NFP_WALLET_PRIVATE_KEY variable in .env file'); // no viewing key if (a_reqs.includes('vk') && !sh_vk) { return exit([ 'Missing viewing key in config. You can run either:', ' nfp set-vk <key> # to set a new one on the contract', ' nfp config vk <key> # to add an existing one to your config', ].join('\n')); } // missing token id if (a_reqs.includes('token-id') && !si_chain) return exit('Missing NFP_SELF_TOKEN variable in .env file'); // decode private key let atu8_sk; if (64 === sh_sk.length) atu8_sk = hex_to_buffer(sh_sk); else atu8_sk = atu8_sk = base64_to_buffer(sh_sk); const a_lcds = s_lcds?.split(',') || []; const a_rpcs = s_rpcs?.split(',') || []; const p_lcd = a_lcds[0] || null; const p_rpc = a_rpcs[0] || null; // create wallet const k_wallet = await Wallet(atu8_sk, si_chain, p_lcd, p_rpc); // connect to the contract const k_contract = await SecretContract(p_lcd, sa_contract); return { sh_sk, sh_vk: sh_vk, si_chain, a_lcds, a_rpcs, sa_contract: sa_contract, si_token: si_token, k_contract, k_wallet, k_service: SecretApp(k_wallet, k_contract, 0.125), }; } /** * parses a cli option value as JSON or simplified key-value * e.g., --entry "key: 'value'" */ export function cli_entries(sx_entry) { try { return JSON.parse(sx_entry); } catch (e_parse) { // normalize sx_entry = sx_entry.trim(); // wrap in object notation if ('{' !== sx_entry[0]) sx_entry = `{${sx_entry}}`; // attempt to parse as ecmascript const d_script = new vm.Script(`(${sx_entry})`); try { return d_script.runInNewContext({}); } catch (e_eval) { throw new Error(`Failed to evaluate ECMAScript: ${e_eval.stack}`, { cause: e_eval, }); } } } export async function env_exists() { try { await access('.env', constants.F_OK); return true; } catch (e_accessible) { return false; } } export async function check_writable_env() { try { await access('.env', constants.W_OK); } catch (e_writable) { return exit('The existing .env file in this directory appears to be write-protected'); } } // | ((s_prev: string, ...a_args: any[]) => string) export async function mutate_env(h_replacements) { if (!await env_exists()) { return exit(`The .env file was deleted while waiting for input`); } await check_writable_env(); // load env into string let sx_env = await readFile('.env', 'utf-8'); // apply replacements for (const [si_key, s_replace] of ode(h_replacements)) { const r_find = new RegExp(`((?:^|\\n)[ \\t]*?${escape_regex(si_key)}=)("[^\\n]*")([ \\t]*#[^\\n]*)?[ \\t]*(?:$|\\n)`); // key exists in env if (r_find.test(sx_env)) { sx_env = sx_env.replace(r_find, `$1${JSON.stringify(s_replace)}$3\n`); } // not yet defined; append to file else { sx_env += `\n${si_key}=${JSON.stringify(s_replace)}\n`; } } // // write to disk await writeFile('.env', sx_env); // verbose debug(`${kleur.green('✓')} ${kleur.bold('Updated config saved to .env file')}`); } export async function cli_query_contract(g_argv, si_query, h_args = {}, z_auth) { const { sh_vk, si_chain, k_contract, k_wallet, } = await load(g_argv, ['vk']); if ('undefined' === typeof z_auth) z_auth = [sh_vk, k_wallet.addr]; // verbose print('Querying contract', { chain: si_chain, auth: z_auth ? ['*'.repeat(6), z_auth[1]] : null, contract: k_contract.addr, query: JSON.stringify({ [si_query]: h_args, }), }); // query const a_response = await query_contract_infer(k_contract, si_query, h_args || {}, z_auth); const [g_response, xc_code, s_err] = a_response; // error if (xc_code) return exit(s_err); // results print('Response:'); result(JSON.stringify(g_response)); // return return a_response; } export async function cli_exec_contract(g_argv, g_msg, xg_limit, r_debug) { const { si_chain, k_contract, k_wallet, } = await load(g_argv); // resolve price const x_price = g_argv.price || H_DEFAULT_NETWORKS[si_chain || '']?.price || X_GAS_PRICE_DEFAULT; // override gas limit if (g_argv.gas) xg_limit = BigInt(g_argv.gas); // prep fee const a_fees = [[`${BigInt(Math.ceil(Number(xg_limit) * x_price))}`, 'uscrt']]; // verbose print('Executing contract', { chain: si_chain, from: k_wallet.addr, contract: k_contract.addr, fee: a_fees.map(a_slim => a_slim.join(' ')).join(' + '), message: JSON.stringify(g_msg), }); // request broadcast confirmation if (!g_argv.yes) { // confirm const { broadcast: b_broadcast } = await prompts({ type: 'confirm', name: 'broadcast', message: 'Broadcast transaction?', }); // pad prompt debug(''); // cancel if (!b_broadcast) return exit('Broadcast cancelled'); } // execute const a_response = await exec_contract(k_contract, k_wallet, g_msg, a_fees, `${xg_limit}`); // destructure const [xc_code, s_res, g_tx_res, si_txn] = a_response; // error if (xc_code) { // possible remedy let s_ammend = ''; if (r_debug?.test(s_res)) { s_ammend = [ '', 'You can use secretcli to retry from an authorized account:', ` $ secretcli tx compute execute ${k_contract.addr} '${JSON.stringify(g_msg)}' --from $ACCOUNT`, ].join('\n'); } // allow caller to catch throw new Error(`Error code ${xc_code}: ${s_res}${s_ammend}`); } // log tx details print('Success', { 'height': g_tx_res.height, 'tx hash': si_txn, 'gas used/spent': `${g_tx_res.result.gas_used}/${g_tx_res.result.gas_wanted}`, }); // forward to caller return a_response; } export function validate_bech32(sa_input) { // attempt to parse try { return sa_input.startsWith('secret1') ? 20 === bech32_decode(sa_input).length || 'Address is wrong length: ' + bech32_decode(sa_input).length : 'Address must be in bech32 format and start with `secret1` human-readable part'; } catch (e_decode) { return `Invalid bech32 address: ${e_decode.message}`; } } //# sourceMappingURL=common.js.map