theseus/bundled-addons/aegis/lib/chain-btc.js

560 lines
25 KiB
JavaScript
Raw Normal View History

feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
// 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 { verifyFunding, assertFee } = require("./utxo-verify.js");
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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/",
},
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
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/",
},
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
};
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);
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
// 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 {}
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
// 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).
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
function bjsNetworkFor(id) {
if (id === "mainnet") return bjsNetworks.bitcoin;
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
if (id === "testnet" || id === "signet") return bjsNetworks.testnet;
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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),
};
}
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
// 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 });
feat(theseus/aegis): BTC send from BIP44 + BIP86 addresses Aegis's Bitcoin adapter can now sign transactions from every BIP44/49/84/86 address it derives. Receive already worked on all four in the previous rev — this closes the send side. - BIP44 (legacy P2PKH, 1…): signAndBroadcast now fetches each spent UTXO's parent transaction via blockchain.transaction.get(txid, false) and hands the raw hex to PSBT as nonWitnessUtxo. Prev-tx calls fan out in parallel with Promise.all so a multi-input legacy send doesn't serialize the wait. - BIP86 (Taproot key-path, bc1p…): signInput now uses a tap-tweaked signer — the internal ECPair, tweaked with sha256("TapTweak" || internalPubkey) via ECPair.tweak(). bitcoinjs-lib matches the tweaked pubkey against the on-chain output key and signs with schnorr. The input carries tapInternalKey so the PSBT layer knows it's a key-path spend (no leaf script). - The plan-time "not yet in this rev" refusal is gone. paymentFor() returns send: "p2pkh" / "p2tr" for the two families; every path in the picker signs today. - Fee vsize model already covered p2pkh (148 vB per input) and p2tr (58 vB per input) — unchanged. - Verified in scratchpad/verify-btc-send.mjs: all four families produce a fully-finalized wire tx (bitcoinjs-lib refuses to finalize an invalid signature, so a valid extractTransaction() result is proof the signing path is correct). Vsize per family: BIP44 222 vB, BIP49 165 vB, BIP84 141 vB, BIP86 142 vB — all match the input-count/vsize model in this file's fee estimator.
2026-09-07 21:55:44 +02:00
return { family: "bip44", address: p.address, output: Buffer.from(p.output), send: "p2pkh", needsPrevTx: true };
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
}
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) {
feat(theseus/aegis): BTC send from BIP44 + BIP86 addresses Aegis's Bitcoin adapter can now sign transactions from every BIP44/49/84/86 address it derives. Receive already worked on all four in the previous rev — this closes the send side. - BIP44 (legacy P2PKH, 1…): signAndBroadcast now fetches each spent UTXO's parent transaction via blockchain.transaction.get(txid, false) and hands the raw hex to PSBT as nonWitnessUtxo. Prev-tx calls fan out in parallel with Promise.all so a multi-input legacy send doesn't serialize the wait. - BIP86 (Taproot key-path, bc1p…): signInput now uses a tap-tweaked signer — the internal ECPair, tweaked with sha256("TapTweak" || internalPubkey) via ECPair.tweak(). bitcoinjs-lib matches the tweaked pubkey against the on-chain output key and signs with schnorr. The input carries tapInternalKey so the PSBT layer knows it's a key-path spend (no leaf script). - The plan-time "not yet in this rev" refusal is gone. paymentFor() returns send: "p2pkh" / "p2tr" for the two families; every path in the picker signs today. - Fee vsize model already covered p2pkh (148 vB per input) and p2tr (58 vB per input) — unchanged. - Verified in scratchpad/verify-btc-send.mjs: all four families produce a fully-finalized wire tx (bitcoinjs-lib refuses to finalize an invalid signature, so a valid extractTransaction() result is proof the signing path is correct). Vsize per family: BIP44 222 vB, BIP49 165 vB, BIP84 141 vB, BIP86 142 vB — all match the input-count/vsize model in this file's fee estimator.
2026-09-07 21:55:44 +02:00
// 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));
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
const p = payments.p2tr({ internalPubkey, network });
feat(theseus/aegis): BTC send from BIP44 + BIP86 addresses Aegis's Bitcoin adapter can now sign transactions from every BIP44/49/84/86 address it derives. Receive already worked on all four in the previous rev — this closes the send side. - BIP44 (legacy P2PKH, 1…): signAndBroadcast now fetches each spent UTXO's parent transaction via blockchain.transaction.get(txid, false) and hands the raw hex to PSBT as nonWitnessUtxo. Prev-tx calls fan out in parallel with Promise.all so a multi-input legacy send doesn't serialize the wait. - BIP86 (Taproot key-path, bc1p…): signInput now uses a tap-tweaked signer — the internal ECPair, tweaked with sha256("TapTweak" || internalPubkey) via ECPair.tweak(). bitcoinjs-lib matches the tweaked pubkey against the on-chain output key and signs with schnorr. The input carries tapInternalKey so the PSBT layer knows it's a key-path spend (no leaf script). - The plan-time "not yet in this rev" refusal is gone. paymentFor() returns send: "p2pkh" / "p2tr" for the two families; every path in the picker signs today. - Fee vsize model already covered p2pkh (148 vB per input) and p2tr (58 vB per input) — unchanged. - Verified in scratchpad/verify-btc-send.mjs: all four families produce a fully-finalized wire tx (bitcoinjs-lib refuses to finalize an invalid signature, so a valid extractTransaction() result is proof the signing path is correct). Vsize per family: BIP44 222 vB, BIP49 165 vB, BIP84 141 vB, BIP86 142 vB — all match the input-count/vsize model in this file's fee estimator.
2026-09-07 21:55:44 +02:00
return { family: "bip86", address: p.address, output: Buffer.from(p.output), internalPubkey, send: "p2tr" };
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
}
// 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;
}
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
class WalletKeys {
constructor(root32, accountPath, bjsNetwork) {
this._accountPath = /^m(\/\d+'?)+$/.test(accountPath) ? accountPath : "m/84'/0'/0'";
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
this._purpose = purposeOfPath(this._accountPath);
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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; }
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
get purpose() { return this._purpose; }
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
entry(branch, index) {
const k = branch + "/" + index;
let e = this._cache.get(k);
if (!e) {
const node = this._branch[branch].derive(index);
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
const pay = paymentFor(this._purpose, node, this._network);
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
e = {
branch, index, path: this._accountPath + "/" + branch + "/" + index,
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
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,
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
_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;
}
}
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
// 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.
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
const OVERHEAD_VB = 10.5;
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
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);
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
// 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
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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);
}
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
// 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();
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
const dest = String(to || "");
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
let destScript;
try { destScript = bitcoinjs.address.toOutputScript(dest, this._bjsNet); }
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
catch (e) { throw new Error(`bad Bitcoin address: ${e?.message || dest}`); }
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
const cur = this.current();
feat(theseus/aegis): BTC send from BIP44 + BIP86 addresses Aegis's Bitcoin adapter can now sign transactions from every BIP44/49/84/86 address it derives. Receive already worked on all four in the previous rev — this closes the send side. - BIP44 (legacy P2PKH, 1…): signAndBroadcast now fetches each spent UTXO's parent transaction via blockchain.transaction.get(txid, false) and hands the raw hex to PSBT as nonWitnessUtxo. Prev-tx calls fan out in parallel with Promise.all so a multi-input legacy send doesn't serialize the wait. - BIP86 (Taproot key-path, bc1p…): signInput now uses a tap-tweaked signer — the internal ECPair, tweaked with sha256("TapTweak" || internalPubkey) via ECPair.tweak(). bitcoinjs-lib matches the tweaked pubkey against the on-chain output key and signs with schnorr. The input carries tapInternalKey so the PSBT layer knows it's a key-path spend (no leaf script). - The plan-time "not yet in this rev" refusal is gone. paymentFor() returns send: "p2pkh" / "p2tr" for the two families; every path in the picker signs today. - Fee vsize model already covered p2pkh (148 vB per input) and p2tr (58 vB per input) — unchanged. - Verified in scratchpad/verify-btc-send.mjs: all four families produce a fully-finalized wire tx (bitcoinjs-lib refuses to finalize an invalid signature, so a valid extractTransaction() result is proof the signing path is correct). Vsize per family: BIP44 222 vB, BIP49 165 vB, BIP84 141 vB, BIP86 142 vB — all match the input-count/vsize model in this file's fee estimator.
2026-09-07 21:55:44 +02:00
if (!cur.sendKind) throw new Error(`no sender for ${cur.family} — registry bug`);
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
const spendable = this._state.utxos.slice().sort((a, b) => (b.height > 0) - (a.height > 0));
const change = this._changeEntry();
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
const kind = cur.sendKind;
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
if (sendMax) {
const chosen = spendable;
const sum = chosen.reduce((a, u) => a + u.value, 0);
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
const fee = feeFor(kind, chosen.length, [destScript], rate);
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
if (sum <= fee) throw new Error("balance does not cover the fee");
return {
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
_chosen: chosen, _rate: rate, _to: dest, _sendMax: true, _change: change, _kind: kind,
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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;
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
const withChange = feeFor(kind, chosen.length, [destScript, change.script], rate);
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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,
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
_change: change, _changeVal: changeVal > 546 ? changeVal : 0, _kind: kind,
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
recipients: [{ to: dest, value }],
fee, feeRate: rate,
change: changeVal > 546 ? changeVal : 0,
total: value + fee,
};
}
}
throw new Error("insufficient funds");
}
async signAndBroadcast(plan) {
// Every non-taproot input's previous transaction is fetched and
// checked against the value the plan used (lib/utxo-verify.js); BIP44
// inputs also need it whole (nonWitnessUtxo) for the legacy sighash.
const prevHex = await verifyFunding({ chosen: plan._chosen, client: this._client, Transaction: bitcoinjs.Transaction });
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
const psbt = new Psbt({ network: this._bjsNet });
for (const u of plan._chosen) {
// Final sequence on every input: Aegis does not do replace-by-fee on
// any chain, so it must not advertise a transaction as replaceable
// (a recipient would then be right to distrust it until confirmed).
const inp = { hash: u.txid, index: u.vout, sequence: 0xffffffff };
feat(theseus/aegis): BTC send from BIP44 + BIP86 addresses Aegis's Bitcoin adapter can now sign transactions from every BIP44/49/84/86 address it derives. Receive already worked on all four in the previous rev — this closes the send side. - BIP44 (legacy P2PKH, 1…): signAndBroadcast now fetches each spent UTXO's parent transaction via blockchain.transaction.get(txid, false) and hands the raw hex to PSBT as nonWitnessUtxo. Prev-tx calls fan out in parallel with Promise.all so a multi-input legacy send doesn't serialize the wait. - BIP86 (Taproot key-path, bc1p…): signInput now uses a tap-tweaked signer — the internal ECPair, tweaked with sha256("TapTweak" || internalPubkey) via ECPair.tweak(). bitcoinjs-lib matches the tweaked pubkey against the on-chain output key and signs with schnorr. The input carries tapInternalKey so the PSBT layer knows it's a key-path spend (no leaf script). - The plan-time "not yet in this rev" refusal is gone. paymentFor() returns send: "p2pkh" / "p2tr" for the two families; every path in the picker signs today. - Fee vsize model already covered p2pkh (148 vB per input) and p2tr (58 vB per input) — unchanged. - Verified in scratchpad/verify-btc-send.mjs: all four families produce a fully-finalized wire tx (bitcoinjs-lib refuses to finalize an invalid signature, so a valid extractTransaction() result is proof the signing path is correct). Vsize per family: BIP44 222 vB, BIP49 165 vB, BIP84 141 vB, BIP86 142 vB — all match the input-count/vsize model in this file's fee estimator.
2026-09-07 21:55:44 +02:00
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;
}
feat(theseus/aegis): BTC address-family picker (BIP44/49/84/86 + Taproot) BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…), BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on mainnet or testnet3 (paths shift coin type 0 → 1 automatically). - lib/chain-btc.js: paymentFor(purpose, node, network) returns the right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr) keyed off the derivation path's purpose. WalletKeys.entry captures the family, redeem script (BIP49) and internal x-only pubkey (BIP86) alongside the standard script/address fields. bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves. - Registry: BTC + DGB address families are purpose-only now; a helper (addressFamiliesFor / defaultAccountPathFor) computes the concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type {mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta expands the list so the panel doesn't need per-chain knowledge. - Panel: #btcSettings block mirrors #dgbSettings (family select → path input auto-fill → Apply). The family-select listener + the fillFamilyPicker() helper are shared between DGB and BTC — the DOM prefix is the only per-chain input. - Send is wired for BIP84 (default) and BIP49 (adds redeemScript to the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and BIP86 (needs tap-tweaked signer) throw a clear "not yet in this rev — sweep to BIP84" error so users hit it at plan time, not at broadcast time. Receive works on all four families today. - Verified all four families derive the canonical BIP44/49/84/86 spec test vectors for the standard abandon×11 mnemonic — see scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
2026-09-07 21:23:15 +02:00
psbt.addInput(inp);
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
}
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++) {
feat(theseus/aegis): BTC send from BIP44 + BIP86 addresses Aegis's Bitcoin adapter can now sign transactions from every BIP44/49/84/86 address it derives. Receive already worked on all four in the previous rev — this closes the send side. - BIP44 (legacy P2PKH, 1…): signAndBroadcast now fetches each spent UTXO's parent transaction via blockchain.transaction.get(txid, false) and hands the raw hex to PSBT as nonWitnessUtxo. Prev-tx calls fan out in parallel with Promise.all so a multi-input legacy send doesn't serialize the wait. - BIP86 (Taproot key-path, bc1p…): signInput now uses a tap-tweaked signer — the internal ECPair, tweaked with sha256("TapTweak" || internalPubkey) via ECPair.tweak(). bitcoinjs-lib matches the tweaked pubkey against the on-chain output key and signs with schnorr. The input carries tapInternalKey so the PSBT layer knows it's a key-path spend (no leaf script). - The plan-time "not yet in this rev" refusal is gone. paymentFor() returns send: "p2pkh" / "p2tr" for the two families; every path in the picker signs today. - Fee vsize model already covered p2pkh (148 vB per input) and p2tr (58 vB per input) — unchanged. - Verified in scratchpad/verify-btc-send.mjs: all four families produce a fully-finalized wire tx (bitcoinjs-lib refuses to finalize an invalid signature, so a valid extractTransaction() result is proof the signing path is correct). Vsize per family: BIP44 222 vB, BIP49 165 vB, BIP84 141 vB, BIP86 142 vB — all match the input-count/vsize model in this file's fee estimator.
2026-09-07 21:55:44 +02:00
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));
}
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
}
psbt.finalizeAllInputs();
assertFee(psbt, plan);
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
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 {}
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
try { this._client.dispose ? this._client.dispose() : this._client.disconnect(); } catch {}
feat(theseus/aegis): Bitcoin adapter (mainnet + testnet3, BIP84 native SegWit) Seven coins across twelve networks now — BTC joins the shipping roster. - lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q… (mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same stack the DGB adapter already pulls in: bitcoinjs-lib for network params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign recoverable sigs. No new npm deps. - Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke, grey.pw) for mainnet and aranguren.org / blockstream.info:993 for testnet3. Send flow: PSBT build + per-input signInput + finalizeAllInputs + broadcast. BIP-137 recoverable message signing. - Registered as btc:mainnet + btc:testnet in COINS with the orange Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored, so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb as the DGB address-family selector, deferred to a follow-up). - Panel: sat as the small-unit label, bitcoin: BIP21 QR payload, chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet). - Verified: BIP84 spec test vector — abandon×11 mnemonic derives bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0 (byte-identical to the vector in the BIP text). Testnet variant produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0 (cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
2026-09-07 21:15:45 +02:00
if (this._root) this._root.fill(0);
}
}
return { BtcWallet, NETWORKS };
};