theseus/bundled-addons/aegis/lib/chain-btc.js
Local Dev 884f3948b8 Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).

- ETH nonce. The pending count from a load-balanced RPC often misses a
  transaction this wallet sent seconds ago, so two sends in a row shared a
  nonce and the second failed or replaced the first. The nonce is taken at
  signing, from the RPC or from what the wallet itself last broadcast,
  whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
  transaction; they rejected the type-2 envelope, so a network added by a
  dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
  estimate instead of a constant, size each output from its real script
  (a taproot destination was undercounted), and round the size up before
  pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
  sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
  server dropped. The client reconnects with backoff, pings to catch dead
  sockets, times out a silent connect, and hands the replayed subscription
  answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
  balance.
- Removing a wallet retired its derivation index for good. Add wallet now
  takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
  setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
  page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00

564 lines
25 KiB
JavaScript

// Bitcoin (BTC) chain adapter — mainnet + testnet3. BIP84 native SegWit,
// bitcoinjs-lib for the tx/PSBT primitives, Aegis's own ElectrumX transport
// for the network side. Very close in shape to chain-dgb.js; the two could
// share a "bip84-electrum" helper later, but for now a distinct file keeps
// the chain-specific tuning (electrum pool, network object) visible.
//
// Address family: BIP84 only in this rev — bc1q… (mainnet) / tb1q… (testnet).
// BIP44 (1…) and BIP49 (3…) are trivially reachable by editing accountPath
// to m/44'/0'/0' or m/49'/0'/0' respectively; the PSBT layer already
// supports the resulting scripts because bitcoinjs-lib does. An explicit
// address-family picker like DGB's is a follow-up.
const NETWORKS = {
mainnet: {
id: "mainnet", label: "Mainnet",
hrp: "bc", coinType: 0,
defaultAccountPath: "m/84'/0'/0'",
explorerTx: "https://mempool.space/tx/",
explorerAddr: "https://mempool.space/address/",
defaultServers: [
"wss://electrum.blockstream.info:50004",
"wss://bitcoin.lu.ke:50004",
"wss://fulcrum.grey.pw:50004",
],
faucet: null,
},
testnet: {
id: "testnet", label: "Testnet3",
hrp: "tb", coinType: 1,
defaultAccountPath: "m/84'/1'/0'",
explorerTx: "https://mempool.space/testnet/tx/",
explorerAddr: "https://mempool.space/testnet/address/",
defaultServers: [
"wss://testnet.aranguren.org:51004",
"wss://blockstream.info:993",
],
faucet: "https://coinfaucet.eu/en/btc-testnet/",
},
signet: {
// Signet (BIP-325) shares testnet's address format and SLIP-44 coin
// type (1), so bitcoinjs-lib's `networks.testnet` handles address
// derivation unchanged. The chain itself is a separate, permissioned
// testnet with its own genesis + signer-signed blocks; from a wallet's
// point of view, the only differences are the electrum pool serving
// it and the explorer URL for tx lookups.
id: "signet", label: "Signet",
hrp: "tb", coinType: 1,
defaultAccountPath: "m/84'/1'/0'",
explorerTx: "https://mempool.space/signet/tx/",
explorerAddr: "https://mempool.space/signet/address/",
defaultServers: [
"wss://signet.aranguren.org:51102",
"wss://signet-electrumx.wakiyamap.dev:50003",
],
faucet: "https://signetfaucet.com/",
},
};
module.exports = function makeBtcAdapter({
bitcoinjs, bip32Factory, ecpairFactory, ecc, sha256, electrum,
}) {
if (!bitcoinjs || !bip32Factory || !ecpairFactory || !ecc || !electrum) {
throw new Error("chain-btc: missing dep");
}
const { payments, Psbt, networks: bjsNetworks } = bitcoinjs;
const bip32 = bip32Factory(ecc);
const ECPair = ecpairFactory(ecc);
// Taproot (p2tr) address derivation needs bitcoinjs-lib's schnorr backend
// wired to a curve implementation — @bitcoinerlab/secp256k1 provides both
// ECDSA and schnorr, so initEccLib once at load makes p2tr resolve.
try { bitcoinjs.initEccLib && bitcoinjs.initEccLib(ecc); } catch {}
// Map our network id → bitcoinjs-lib Network object. Signet shares
// testnet's address prefixes + magic (BIP-325 defines only new consensus
// rules; the p2p / address layer stays testnet-compatible).
function bjsNetworkFor(id) {
if (id === "mainnet") return bjsNetworks.bitcoin;
if (id === "testnet" || id === "signet") return bjsNetworks.testnet;
throw new Error("chain-btc: unknown network " + id);
}
const toHex = (b) => Buffer.from(b).toString("hex");
const scripthashOf = (scriptBuf) => Buffer.from(sha256(scriptBuf)).reverse().toString("hex");
function scopedStorage(storage, keyPrefix) {
const k = (key) => keyPrefix + key;
return {
get: (key, fallback = null) => storage.get(k(key), fallback),
set: (key, value) => storage.set(k(key), value),
};
}
// Address-family shape from the derivation-path purpose. Every field the
// PSBT layer might need for signing an input funded by this family is
// captured here so signAndBroadcast has one code path per family.
function paymentFor(purpose, node, network) {
const pubkey = Buffer.from(node.publicKey);
if (purpose === 44) {
// Legacy P2PKH. Signing needs the full previous transaction
// (nonWitnessUtxo) — one extra electrum call per input at send time.
const p = payments.p2pkh({ pubkey, network });
return { family: "bip44", address: p.address, output: Buffer.from(p.output), send: "p2pkh", needsPrevTx: true };
}
if (purpose === 49) {
// P2SH-wrapped SegWit. PSBT needs the redeem script (the inner p2wpkh
// output) alongside the witnessUtxo.
const redeem = payments.p2wpkh({ pubkey, network });
const p = payments.p2sh({ redeem, network });
return { family: "bip49", address: p.address, output: Buffer.from(p.output), redeem: Buffer.from(redeem.output), send: "p2sh-p2wpkh" };
}
if (purpose === 86) {
// BIP86 Taproot key-path. Signing goes through a tap-tweaked ECPair
// (see signerFor + signAndBroadcast); the internal 32-byte x-only
// pubkey is captured here so the PSBT input can carry it.
const internalPubkey = Buffer.from(pubkey.subarray(1, 33));
const p = payments.p2tr({ internalPubkey, network });
return { family: "bip86", address: p.address, output: Buffer.from(p.output), internalPubkey, send: "p2tr" };
}
// Default: BIP84 native SegWit.
const p = payments.p2wpkh({ pubkey, network });
return { family: "bip84", address: p.address, output: Buffer.from(p.output), send: "p2wpkh" };
}
function purposeOfPath(accountPath) {
const m = /^m\/(\d+)'\//.exec(String(accountPath || ""));
return m ? Number(m[1]) : 84;
}
class WalletKeys {
constructor(root32, accountPath, bjsNetwork) {
this._accountPath = /^m(\/\d+'?)+$/.test(accountPath) ? accountPath : "m/84'/0'/0'";
this._purpose = purposeOfPath(this._accountPath);
this._network = bjsNetwork;
this._root = bip32.fromSeed(Buffer.from(root32), bjsNetwork);
this._account = this._root.derivePath(this._accountPath);
this._branch = [this._account.derive(0), this._account.derive(1)];
this._cache = new Map();
}
get xpub() { return this._account.neutered().toBase58(); }
get xprv() { return this._account.toBase58(); }
get accountPath() { return this._accountPath; }
get purpose() { return this._purpose; }
entry(branch, index) {
const k = branch + "/" + index;
let e = this._cache.get(k);
if (!e) {
const node = this._branch[branch].derive(index);
const pay = paymentFor(this._purpose, node, this._network);
e = {
branch, index, path: this._accountPath + "/" + branch + "/" + index,
publicKey: Buffer.from(node.publicKey),
family: pay.family, sendKind: pay.send,
script: pay.output, scriptHex: pay.output.toString("hex"),
scripthash: scripthashOf(pay.output),
address: pay.address,
redeemScript: pay.redeem || null,
tapInternalKey: pay.internalPubkey || null,
_node: node,
};
this._cache.set(k, e);
}
return e;
}
signerFor(entry) {
return ECPair.fromPrivateKey(Buffer.from(entry._node.privateKey), { network: this._network });
}
wipe() {
for (const e of this._cache.values()) e._node = null;
this._cache.clear();
this._branch = null;
this._account = null;
this._root = null;
}
}
// Fee vsize model per family. Values are rounded vsize contributions from
// standard tx-size tables; the estimator is pessimistic enough to cover a
// real broadcast without underpaying.
const OVERHEAD_VB = 10.5;
const OUTPUT_VB = 31; // P2WPKH / P2SH / P2PKH outputs are all ~31 vB give or take
const INPUT_VB = {
p2pkh: 148, // (32+4)+1+107+4 legacy input
"p2sh-p2wpkh": 91, // 40 base + ~205/4 witness
p2wpkh: 68, // 41 base + 108/4 witness
p2tr: 58, // 41 base + 66/4 witness (key-path)
};
const feeVb = (kind, nIn, nOut, feePerVb) => Math.ceil((OVERHEAD_VB + nIn * (INPUT_VB[kind] || 68) + nOut * OUTPUT_VB) * feePerVb);
// Same, with each output sized from its real script (8 value + 1 length +
// script). A flat 31 vB undercounts P2TR / P2WSH destinations (43 vB), so
// a send to a bc1p… address paid below the rate it asked for.
const feeFor = (kind, nIn, outScripts, feePerVb) =>
// Round the size up before pricing it: a real transaction is a whole
// number of vbytes, and paying for 152.5 of 153 lands under the rate.
Math.ceil(Math.ceil(OVERHEAD_VB + nIn * (INPUT_VB[kind] || 68) + outScripts.reduce((a, s) => a + 9 + s.length, 0)) * feePerVb);
const DEFAULT_RATE = 5; // sat/vB when the server has no estimate
const RBF_SEQUENCE = 0xfffffffd; // BIP125: opt in to replace-by-fee
class BtcWallet {
constructor(root32, networkId, {
walletId, storage, log = () => {}, onChange = () => {}, servers,
accountPath,
} = {}) {
if (!walletId) throw new Error("chain-btc: walletId required");
const net = NETWORKS[networkId];
if (!net) throw new Error("chain-btc: unknown network " + networkId);
this.walletId = walletId;
this.chain = "btc";
this.network = net.id;
this._net = net;
this._bjsNet = bjsNetworkFor(net.id);
this.log = log;
this.onChange = onChange;
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
this._servers = Array.isArray(servers) && servers.length ? servers : net.defaultServers.slice();
const wantPath = accountPath || net.defaultAccountPath;
this._keys = new WalletKeys(root32, wantPath, this._bjsNet);
this._root = new Uint8Array(root32);
this._client = new electrum.Client(this._servers);
this._client.onServer = () => this._emit();
this._state = {
used: new Set(),
watched: new Map(),
height: 0,
balance: { confirmed: 0, unconfirmed: 0 },
utxos: [],
history: [],
receiveIndex: 0,
scanning: false,
error: null,
};
this._refreshTimer = null;
this._subscribedHeaders = false;
this._client.onNotify = (method, params) => {
if (method === "blockchain.headers.subscribe") {
const h = params && params[0] && params[0].height;
if (h) { this._state.height = h; this._scheduleRefresh(1500); }
} else if (method === "blockchain.scripthash.subscribe") {
this._scheduleRefresh(800);
}
};
}
_emit() { try { this.onChange(); } catch {} }
async _historyOf(entry) {
const h = await this._client.call("blockchain.scripthash.get_history", [entry.scripthash]);
return Array.isArray(h) ? h : [];
}
async _scan() {
const cursor = Number(this.storage.get("receiveCursor", 0)) || 0;
const GAP = 20;
for (const branch of [0, 1]) {
let gap = 0, i = 0;
const minIndex = branch === 0 ? cursor + 1 : 0;
while (gap < GAP || i < minIndex + GAP) {
const batch = [];
for (let k = 0; k < 10; k++) batch.push(this._keys.entry(branch, i + k));
const results = await Promise.all(batch.map((e) => this._historyOf(e)));
for (let k = 0; k < batch.length; k++) {
const e = batch[k];
this._state.watched.set(e.scripthash, e);
if (results[k].length) { this._state.used.add(branch + "/" + e.index); gap = 0; } else gap++;
i++;
if (gap >= GAP && i >= minIndex + GAP) break;
}
}
}
let r = cursor;
while (this._state.used.has("0/" + r)) r++;
this._state.receiveIndex = r;
this._state.watched.set(this._keys.entry(0, r).scripthash, this._keys.entry(0, r));
}
async _subscribeAll() {
if (!this._subscribedHeaders) {
this._subscribedHeaders = true;
const tip = await this._client.subscribe("blockchain.headers.subscribe", []);
if (tip && tip.height) this._state.height = tip.height;
}
await Promise.all([...this._state.watched.values()].map((e) =>
this._client.subscribe("blockchain.scripthash.subscribe", [e.scripthash]).catch(() => {})));
}
async _loadUtxos() {
const lists = await Promise.all([...this._state.watched.values()].map(async (e) => {
const u = await this._client.call("blockchain.scripthash.listunspent", [e.scripthash]);
return (Array.isArray(u) ? u : []).map((x) => ({ txid: x.tx_hash, vout: x.tx_pos, value: x.value, height: x.height, entry: e }));
}));
this._state.utxos = lists.flat();
let confirmed = 0, unconfirmed = 0;
for (const u of this._state.utxos) { if (u.height > 0) confirmed += u.value; else unconfirmed += u.value; }
this._state.balance = { confirmed, unconfirmed };
}
async _loadHistory() {
const entries = [...this._state.watched.values()].filter((e) => this._state.used.has(e.branch + "/" + e.index));
const merged = new Map();
const lists = await Promise.all(entries.map((e) => this._historyOf(e)));
for (const list of lists) for (const h of list) {
const prev = merged.get(h.tx_hash);
if (!prev || (h.height > 0 && prev.height <= 0)) merged.set(h.tx_hash, { txid: h.tx_hash, height: h.height });
}
const ordered = [...merged.values()].sort((a, b) => {
const ha = a.height > 0 ? a.height : Infinity, hb = b.height > 0 ? b.height : Infinity;
return hb - ha;
}).slice(0, 25);
const ours = new Set([...this._state.watched.values()].map((e) => e.scriptHex));
const out = [];
for (const h of ordered) {
let received = 0, spent = 0;
try {
const t = await this._client.call("blockchain.transaction.get", [h.txid, true]);
for (const o of t.vout || []) {
const hex = o.scriptPubKey && o.scriptPubKey.hex;
if (hex && ours.has(hex)) received += Math.round(Number(o.value || 0) * 1e8);
}
for (const i of t.vin || []) {
if (!i.txid) continue;
try {
const p = await this._client.call("blockchain.transaction.get", [i.txid, true]);
const po = p.vout && p.vout[i.vout];
const hex = po && po.scriptPubKey && po.scriptPubKey.hex;
if (hex && ours.has(hex)) spent += Math.round(Number(po.value || 0) * 1e8);
} catch {}
}
out.push({
txid: h.txid, height: h.height, confirmations: t.confirmations || 0,
time: t.blocktime || t.time || 0,
delta: received - spent, fee: null, to: null,
status: (t.confirmations || 0) > 0 ? "confirmed" : "pending",
kind: "transfer",
});
} catch {
out.push({ txid: h.txid, height: h.height, confirmations: 0, time: 0, delta: 0, fee: null, to: null, status: "pending", kind: "transfer" });
}
}
this._state.history = out;
}
async refresh(full = false) {
if (this._state.scanning) return;
this._state.scanning = true; this._state.error = null; this._emit();
try {
if (full || !this._state.watched.size) await this._scan();
else {
let r = Number(this.storage.get("receiveCursor", 0)) || 0;
while (this._state.used.has("0/" + r)) r++;
this._state.receiveIndex = r;
this._state.watched.set(this._keys.entry(0, r).scripthash, this._keys.entry(0, r));
}
await this._loadUtxos();
await this._loadHistory();
await this._subscribeAll();
for (const u of this._state.utxos) this._state.used.add(u.entry.branch + "/" + u.entry.index);
let r = Number(this.storage.get("receiveCursor", 0)) || 0;
while (this._state.used.has("0/" + r)) r++;
this._state.receiveIndex = r;
} catch (e) {
this._state.error = e?.message || String(e);
this.log("refresh failed:", this._state.error);
} finally {
this._state.scanning = false;
this._emit();
}
}
_scheduleRefresh(ms = 800) {
clearTimeout(this._refreshTimer);
this._refreshTimer = setTimeout(() => this.refresh(false), ms);
}
current() { return this._keys.entry(0, this._state.receiveIndex); }
nextAddress() {
let r = this._state.receiveIndex + 1;
while (this._state.used.has("0/" + r)) r++;
this.storage.set("receiveCursor", r);
this._state.receiveIndex = r;
const e = this._keys.entry(0, r);
this._state.watched.set(e.scripthash, e);
this._client.subscribe("blockchain.scripthash.subscribe", [e.scripthash]).catch(() => {});
this._emit();
return this.current();
}
_changeEntry() {
let i = 0;
while (this._state.used.has("1/" + i)) i++;
return this._keys.entry(1, i);
}
setServers(list) {
this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice();
this._client.setServers(this._servers);
}
// sat/vB for confirmation within ~6 blocks, asked of the Electrum server
// and remembered for a minute. The rate used to be a constant 5, which
// overpays on a quiet day and strands the payment on a busy one.
async _estimateRate() {
if (this._feeEst && Date.now() - this._feeEstAt < 60_000) return this._feeEst;
try {
const perKb = Number(await Promise.race([
this._client.call("blockchain.estimatefee", [6]),
new Promise((_, rej) => setTimeout(() => rej(new Error("estimatefee timeout")), 4000)),
]));
if (Number.isFinite(perKb) && perKb > 0) { // coin per kB; -1 = no estimate
this._feeEst = Math.min(500, Math.max(1, Math.ceil(perKb * 1e5)));
this._feeEstAt = Date.now();
return this._feeEst;
}
} catch {}
return DEFAULT_RATE;
}
async plan({ to, amount, feeRate, sendMax = false }) {
const rate = feeRate != null && Number(feeRate) > 0
? Math.min(500, Math.max(1, Number(feeRate)))
: await this._estimateRate();
const dest = String(to || "");
let destScript;
try { destScript = bitcoinjs.address.toOutputScript(dest, this._bjsNet); }
catch (e) { throw new Error(`bad Bitcoin address: ${e?.message || dest}`); }
const cur = this.current();
if (!cur.sendKind) throw new Error(`no sender for ${cur.family} — registry bug`);
const spendable = this._state.utxos.slice().sort((a, b) => (b.height > 0) - (a.height > 0));
const change = this._changeEntry();
const kind = cur.sendKind;
if (sendMax) {
const chosen = spendable;
const sum = chosen.reduce((a, u) => a + u.value, 0);
const fee = feeFor(kind, chosen.length, [destScript], rate);
if (sum <= fee) throw new Error("balance does not cover the fee");
return {
_chosen: chosen, _rate: rate, _to: dest, _sendMax: true, _change: change, _kind: kind,
recipients: [{ to: dest, value: sum - fee }],
fee, feeRate: rate, change: 0,
total: sum,
};
}
const value = Math.round(Number(amount) || 0);
if (!(value > 0)) throw new Error("amount must be > 0");
let sum = 0; const chosen = [];
for (const u of spendable) {
chosen.push(u); sum += u.value;
const withChange = feeFor(kind, chosen.length, [destScript, change.script], rate);
if (sum >= value + withChange) {
const changeVal = sum - value - withChange;
const fee = changeVal > 546 ? withChange : sum - value;
return {
_chosen: chosen, _rate: rate, _to: dest, _value: value,
_change: change, _changeVal: changeVal > 546 ? changeVal : 0, _kind: kind,
recipients: [{ to: dest, value }],
fee, feeRate: rate,
change: changeVal > 546 ? changeVal : 0,
total: value + fee,
};
}
}
throw new Error("insufficient funds");
}
async signAndBroadcast(plan) {
// BIP44 inputs need the whole previous transaction (nonWitnessUtxo)
// so PSBT can compute a legacy sighash; fetch each one in parallel
// before assembling the PSBT.
const needsPrev = plan._chosen.filter((u) => u.entry.family === "bip44");
const prevHex = new Map();
if (needsPrev.length) {
const results = await Promise.all(needsPrev.map((u) =>
this._client.call("blockchain.transaction.get", [u.txid, false])
));
needsPrev.forEach((u, i) => prevHex.set(u.txid, String(results[i])));
}
const psbt = new Psbt({ network: this._bjsNet });
for (const u of plan._chosen) {
// Signal replace-by-fee, so a payment sent at too low a rate can be
// re-issued at a higher one instead of sitting unconfirmed for days.
const inp = { hash: u.txid, index: u.vout, sequence: RBF_SEQUENCE };
const fam = u.entry.family;
if (fam === "bip44") {
inp.nonWitnessUtxo = Buffer.from(prevHex.get(u.txid), "hex");
} else {
inp.witnessUtxo = { script: u.entry.script, value: u.value };
if (fam === "bip49" && u.entry.redeemScript) inp.redeemScript = u.entry.redeemScript;
if (fam === "bip86" && u.entry.tapInternalKey) inp.tapInternalKey = u.entry.tapInternalKey;
}
psbt.addInput(inp);
}
const outputs = [{ address: plan._to, value: plan._sendMax ? plan.recipients[0].value : plan._value }];
if (!plan._sendMax && plan._changeVal > 0) {
outputs.push({ address: plan._change.address, value: plan._changeVal });
}
for (const o of outputs) psbt.addOutput(o);
for (let i = 0; i < plan._chosen.length; i++) {
const entry = plan._chosen[i].entry;
// Taproot key-path: bitcoinjs-lib matches the signer's publicKey
// against the tweaked output key, so the signer has to be the
// internal ECPair tweaked with sha256("TapTweak" || internalPubkey).
// ECPair.tweak() from the ecpair package does exactly that (its
// internal state becomes the tap-tweaked keypair) and its
// signSchnorr is what PSBT calls for a key-path spend.
if (entry.family === "bip86") {
const raw = ECPair.fromPrivateKey(Buffer.from(entry._node.privateKey), { network: this._bjsNet });
const tweak = bitcoinjs.crypto.taggedHash("TapTweak", entry.tapInternalKey);
const tweaked = raw.tweak(tweak);
psbt.signInput(i, tweaked);
} else {
psbt.signInput(i, this._keys.signerFor(entry));
}
}
psbt.finalizeAllInputs();
const tx = psbt.extractTransaction();
const hex = tx.toHex();
const txid = await this._client.call("blockchain.transaction.broadcast", [hex]);
if (typeof txid !== "string" || txid.length !== 64) throw new Error("broadcast rejected: " + JSON.stringify(txid));
this.log("broadcast", txid);
this._scheduleRefresh(1200);
return { txid, hex, fee: plan.fee };
}
// BIP-137 recoverable over sha256d("Bitcoin Signed Message:\n" || msg).
signMessage(message) {
const enc = new TextEncoder();
const varstr = (s) => { const b = enc.encode(s); if (b.length >= 0xfd) throw new Error("too long"); return Uint8Array.from([b.length, ...b]); };
const MAGIC = "Bitcoin Signed Message:\n";
const payload = Uint8Array.from([...varstr(MAGIC), ...varstr(String(message))]);
const digest = sha256(sha256(payload));
const entry = this.current();
const signer = this._keys.signerFor(entry);
const sig = ecc.signRecoverable(Buffer.from(digest), signer.privateKey);
const out = Buffer.alloc(65);
out[0] = 27 + sig.recoveryId + 4; // +4 = compressed
Buffer.from(sig.signature).copy(out, 1);
return { address: entry.address, signature: out.toString("base64") };
}
recovery() {
return { accountPath: this._keys.accountPath, xpub: this._keys.xpub, xprv: this._keys.xprv };
}
snapshot() {
const cur = this.current();
return {
chain: "btc", network: this._net.id, ticker: "BTC", decimals: 8,
address: cur.address, addressIndex: this._state.receiveIndex,
addressPath: cur.path,
balance: this._state.balance,
height: this._state.height,
history: this._state.history,
scanning: this._state.scanning,
error: this._state.error,
server: this._client.url || null,
servers: this._servers,
accountPath: this._keys.accountPath,
xpub: this._keys.xpub,
explorerTx: this._net.explorerTx,
explorerAddr: this._net.explorerAddr,
faucet: this._net.faucet,
};
}
dispose() {
clearTimeout(this._refreshTimer);
try { this._keys.wipe(); } catch {}
try { this._client.dispose ? this._client.dispose() : this._client.disconnect(); } catch {}
if (this._root) this._root.fill(0);
}
}
return { BtcWallet, NETWORKS };
};