#!/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 [initMethod initJson] // chain.mjs call [tgas] [depositYocto] // chain.mjs addfnkey // chain.mjs view // chain.mjs subaccount // chain.mjs transfer // chain.mjs exists // chain.mjs lock 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'); }