git / moonbite-network / tools/vault/unlock.mjs · raw

#!/usr/bin/env node/* Unlock the founder vault (ADR-014) after 2029-10-17 07:00 CEST. * *   node tools/vault/unlock.mjs --to <moon1 address> [--api https://moonbite.org] [--broadcast] * * Asks for the founder wallet's 12 words (and extra password, if one was set) * without echoing them, reads the vault's coins from the API, signs locally and * prints the transaction. It only sends it with --broadcast. The words never * leave this machine; run it offline and broadcast the printed hex elsewhere * if you prefer. Before the unlock time every node refuses the transaction * ("non-final"), whoever signs it. */import { readFileSync } from 'node:fs';import { fileURLToPath } from 'node:url';import { dirname, join } from 'node:path';import readline from 'node:readline';import { vault, buildUnlock, segwitDecode } from './moonbite-vault.mjs';const args = Object.fromEntries(process.argv.slice(2).reduce((a, x, i, all) => (x.startsWith('--') ? a.concat([[x.slice(2), all[i + 1] && !all[i + 1].startsWith('--') ? all[i + 1] : true]]) : a), []));const cfg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), args.config || 'founder-vault.json'), 'utf8'));const api = String(args.api || 'https://moonbite.org').replace(/\/$/, '');if (!args.to) { console.error('usage: node tools/vault/unlock.mjs --to <address> [--api URL] [--broadcast]'); process.exit(2); }segwitDecode(args.to);const v = await vault({ pubkeyHash: cfg.pubkeyHash, unlockTime: cfg.unlockTime, hrp: cfg.address.slice(0, cfg.address.lastIndexOf('1')) });if (v.address !== cfg.address || v.witnessScript !== cfg.witnessScript) throw new Error('founder-vault.json does not match its own script');function ask(q, hidden) {  return new Promise(res => {    const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });    if (hidden) rl._writeToOutput = s => { if (!rl._muted) rl.output.write(s); };    rl.question(q, a => { rl.close(); if (hidden) process.stdout.write('\n'); res(a); });    rl._muted = !!hidden;  });}const now = Math.floor(Date.now() / 1000);if (now < cfg.unlockTime) console.log(`Note: the vault unlocks ${cfg.unlock_local}; until then the network will refuse this.`);const r = await fetch(`${api}/api/chain/address/${encodeURIComponent(cfg.address)}`);const j = await r.json();if (j.status !== 'success') throw new Error('could not read the vault: ' + (j.message || r.status));const utxos = (j.utxos || []).map(u => ({ txid: u.txid, vout: u.vout, value: u.value }));console.log(`Vault ${cfg.address}: ${utxos.length} spendable coins, ${(j.balance_units / 1e8).toFixed(8)} MBITE (${((j.immature_units || 0) / 1e8).toFixed(8)} still maturing).`);const secret = (await ask('12 words: ', true)).trim();const pass = await ask('Extra password (Enter if none): ', true);const tx = await buildUnlock({ secret, passphrase: pass, vaultInfo: v, utxos, toAddress: args.to });console.log(`Sends ${(tx.amount / 1e8).toFixed(8)} MBITE to ${args.to}, fee ${(tx.fee / 1e8).toFixed(8)}, txid ${tx.txid}`);console.log(tx.rawHex);if (args.broadcast) {  const b = await fetch(`${api}/api/chain/broadcast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rawtx: tx.rawHex }) });  console.log('broadcast:', JSON.stringify(await b.json()));}