@nfps.dev/cli
Version:
CLI for NFP development, inspection, and manipulation
254 lines • 9.14 kB
JavaScript
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