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 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 });
|
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) {
|
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 });
|
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);
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plan({ to, amount, feeRate = 5, sendMax = false }) {
|
|
|
|
|
const rate = Math.min(500, Math.max(1, Number(feeRate) || 5));
|
|
|
|
|
const dest = String(to || "");
|
|
|
|
|
try { bitcoinjs.address.toOutputScript(dest, this._bjsNet); }
|
|
|
|
|
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();
|
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);
|
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 fee = feeVb(kind, chosen.length, 1, 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;
|
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 withChange = feeVb(kind, chosen.length, 2, 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) {
|
2026-09-07 21:55:44 +02:00
|
|
|
// BIP44 inputs need the whole previous transaction (nonWitnessUtxo)
|
|
|
|
|
// so PSBT can compute a legacy sighash; fetch each one in parallel
|
|
|
|
|
// before assembling the PSBT.
|
|
|
|
|
const needsPrev = plan._chosen.filter((u) => u.entry.family === "bip44");
|
|
|
|
|
const prevHex = new Map();
|
|
|
|
|
if (needsPrev.length) {
|
|
|
|
|
const results = await Promise.all(needsPrev.map((u) =>
|
|
|
|
|
this._client.call("blockchain.transaction.get", [u.txid, false])
|
|
|
|
|
));
|
|
|
|
|
needsPrev.forEach((u, i) => prevHex.set(u.txid, String(results[i])));
|
|
|
|
|
}
|
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) {
|
2026-09-07 21:55:44 +02:00
|
|
|
const inp = { hash: u.txid, index: u.vout };
|
|
|
|
|
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++) {
|
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();
|
|
|
|
|
const tx = psbt.extractTransaction();
|
|
|
|
|
const hex = tx.toHex();
|
|
|
|
|
const txid = await this._client.call("blockchain.transaction.broadcast", [hex]);
|
|
|
|
|
if (typeof txid !== "string" || txid.length !== 64) throw new Error("broadcast rejected: " + JSON.stringify(txid));
|
|
|
|
|
this.log("broadcast", txid);
|
|
|
|
|
this._scheduleRefresh(1200);
|
|
|
|
|
return { txid, hex, fee: plan.fee };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BIP-137 recoverable over sha256d("Bitcoin Signed Message:\n" || msg).
|
|
|
|
|
signMessage(message) {
|
|
|
|
|
const enc = new TextEncoder();
|
|
|
|
|
const varstr = (s) => { const b = enc.encode(s); if (b.length >= 0xfd) throw new Error("too long"); return Uint8Array.from([b.length, ...b]); };
|
|
|
|
|
const MAGIC = "Bitcoin Signed Message:\n";
|
|
|
|
|
const payload = Uint8Array.from([...varstr(MAGIC), ...varstr(String(message))]);
|
|
|
|
|
const digest = sha256(sha256(payload));
|
|
|
|
|
const entry = this.current();
|
|
|
|
|
const signer = this._keys.signerFor(entry);
|
|
|
|
|
const sig = ecc.signRecoverable(Buffer.from(digest), signer.privateKey);
|
|
|
|
|
const out = Buffer.alloc(65);
|
|
|
|
|
out[0] = 27 + sig.recoveryId + 4; // +4 = compressed
|
|
|
|
|
Buffer.from(sig.signature).copy(out, 1);
|
|
|
|
|
return { address: entry.address, signature: out.toString("base64") };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recovery() {
|
|
|
|
|
return { accountPath: this._keys.accountPath, xpub: this._keys.xpub, xprv: this._keys.xprv };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
snapshot() {
|
|
|
|
|
const cur = this.current();
|
|
|
|
|
return {
|
|
|
|
|
chain: "btc", network: this._net.id, ticker: "BTC", decimals: 8,
|
|
|
|
|
address: cur.address, addressIndex: this._state.receiveIndex,
|
|
|
|
|
addressPath: cur.path,
|
|
|
|
|
balance: this._state.balance,
|
|
|
|
|
height: this._state.height,
|
|
|
|
|
history: this._state.history,
|
|
|
|
|
scanning: this._state.scanning,
|
|
|
|
|
error: this._state.error,
|
|
|
|
|
server: this._client.url || null,
|
|
|
|
|
servers: this._servers,
|
|
|
|
|
accountPath: this._keys.accountPath,
|
|
|
|
|
xpub: this._keys.xpub,
|
|
|
|
|
explorerTx: this._net.explorerTx,
|
|
|
|
|
explorerAddr: this._net.explorerAddr,
|
|
|
|
|
faucet: this._net.faucet,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
|
clearTimeout(this._refreshTimer);
|
|
|
|
|
try { this._keys.wipe(); } catch {}
|
|
|
|
|
try { this._client.disconnect(); } catch {}
|
|
|
|
|
if (this._root) this._root.fill(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { BtcWallet, NETWORKS };
|
|
|
|
|
};
|