#!/usr/bin/env node /* Unlock the founder vault (ADR-014) after 2029-10-17 07:00 CEST. * * node tools/vault/unlock.mjs --to [--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
[--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())); }