duly / scripts/chain.mjs · raw
#!/usr/bin/env node// Operator helper: deploy, init and call contracts with a key file. Keys are read// from the path given and never printed.//   chain.mjs deploy <net> <keyfile> <wasm> [initMethod initJson]//   chain.mjs call   <net> <keyfile> <contract> <method> <json> [tgas] [depositYocto]//   chain.mjs addfnkey <net> <keyfile> <publicKey> <receiver> <m1,m2> <allowanceYocto>//   chain.mjs view   <net> <contract> <method> <json>//   chain.mjs subaccount <net> <parent keyfile> <new keyfile> <depositNEAR>//   chain.mjs transfer <net> <keyfile> <to> <amountNEAR>//   chain.mjs exists <net> <account>//   chain.mjs lock <net> <keyfile> <expected code hash>   removes every key. Cannot be undone.import { readFileSync } from 'node:fs';import {  accessKeyNonce,  accountExists,  b58decode,  b64encode,  encodeSignedTransaction,  encodeTransaction,  failureText,  outcomeFailure,  outcomeValue,  rpc,  sha256,  TGAS,  toYocto,  viewCall,} from '../web/lib/near.js';import { NETWORKS } from '../web/lib/config.js';import { signAsync } from '../web/vendor/ed25519.js';const [cmd, netName, ...rest] = process.argv.slice(2);const net = NETWORKS[netName];if (!net) throw new Error('network?');function loadKey(file) {  const j = JSON.parse(readFileSync(file, 'utf8'));  const bytes = b58decode(j.private_key.replace(/^ed25519:/, ''));  return { accountId: j.account_id, seed: bytes.slice(0, 32), publicKey: bytes.slice(32), publicKeyStr: j.public_key };}async function send(key, receiverId, actions) {  const ak = await accessKeyNonce(net.rpc, key.accountId, key.publicKeyStr);  if (!ak) throw new Error('key not found on account');  const body = encodeTransaction({    signerId: key.accountId,    publicKey: key.publicKey,    nonce: ak.nonce + 1n,    receiverId,    blockHash: b58decode(ak.blockHash),    actions,  });  const sig = await signAsync(await sha256(body), key.seed);  const out = await rpc(net.rpc, 'send_tx', { signed_tx_base64: b64encode(encodeSignedTransaction(body, sig)), wait_until: 'FINAL' }, { timeoutMs: 60000 });  const failure = outcomeFailure(out);  console.log(JSON.stringify({ tx: out.transaction?.hash, ok: !failure, error: failure ? failureText(failure) : undefined, value: outcomeValue(out) }));  if (failure) process.exitCode = 1;  return out;}const call = (method, args, tgas = 100n, deposit = 0n) => ({  type: 'FunctionCall',  method,  args: new TextEncoder().encode(JSON.stringify(args)),  gas: tgas * TGAS,  deposit,});if (cmd === 'deploy') {  const [keyfile, wasm, initMethod, initJson] = rest;  const key = loadKey(keyfile);  const code = readFileSync(wasm);  const actions = [{ type: 'DeployContract', code: new Uint8Array(code) }];  if (initMethod) actions.push(call(initMethod, JSON.parse(initJson || '{}')));  await send(key, key.accountId, actions);} else if (cmd === 'call') {  const [keyfile, contract, method, json, tgas, deposit] = rest;  await send(loadKey(keyfile), contract, [call(method, JSON.parse(json || '{}'), BigInt(tgas || 100), BigInt(deposit || 0))]);} else if (cmd === 'addfnkey') {  const [keyfile, pub, receiver, methods, allowance] = rest;  const key = loadKey(keyfile);  await send(key, key.accountId, [    {      type: 'AddFunctionCallKey',      publicKey: b58decode(pub.replace(/^ed25519:/, '')),      allowance: BigInt(allowance),      receiverId: receiver,      methods: methods.split(','),    },  ]);} else if (cmd === 'subaccount') {  const [parentFile, newFile, near] = rest;  const parent = loadKey(parentFile);  const child = loadKey(newFile);  if (!child.accountId.endsWith(`.${parent.accountId}`)) throw new Error('not a subaccount of the parent');  await send(parent, child.accountId, [    { type: 'CreateAccount' },    { type: 'Transfer', deposit: toYocto(near) },    { type: 'AddFullAccessKey', publicKey: child.publicKey },  ]);} else if (cmd === 'transfer') {  const [keyfile, to, near] = rest;  await send(loadKey(keyfile), to, [{ type: 'Transfer', deposit: toYocto(near) }]);} else if (cmd === 'lock') {  const [keyfile, expected] = rest;  const key = loadKey(keyfile);  const query = (request_type) => rpc(net.rpc, 'query', { request_type, finality: 'final', account_id: key.accountId });  const account = await query('view_account');  if (!expected || account.code_hash !== expected) throw new Error('deployed code does not match the expected hash');  const { keys } = await query('view_access_key_list');  const others = keys.map((k) => k.public_key).filter((k) => k !== key.publicKeyStr);  const decode = (k) => b58decode(k.replace(/^ed25519:/, ''));  await send(key, key.accountId, [...others, key.publicKeyStr].map((k) => ({ type: 'DeleteKey', publicKey: decode(k) })));  const after = await query('view_access_key_list');  console.log(after.keys.length === 0 ? 'locked: no keys remain' : `NOT LOCKED: ${after.keys.length} keys remain`);} else if (cmd === 'exists') {  console.log((await accountExists(net.rpc, rest[0])) ? 'yes' : 'no');} else if (cmd === 'view') {  const [contract, method, json] = rest;  console.log(JSON.stringify(await viewCall(net.rpc, contract, method, JSON.parse(json || '{}')), null, 1));} else {  throw new Error('unknown command');}