theseus/bundled-addons/aegis/lib/chain-bch-imported.js

340 lines
16 KiB
JavaScript
Raw Normal View History

feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
// Imported BCH wallet — single-address, key material lives in Theseus's
// wallet-imports.enc (design §3.2). This adapter mirrors chain-bch.js's
// public shape (snapshot, refresh, plan, signAndBroadcast, signMessage,
// dispose) but does NOT go through vault.derive + HKDF: derivation is
// direct from the seed+path or WIF that the user imported.
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
//
// 0.6.36+: spend path enabled. plan() builds a P2PKH tx off the wallet's
// single scripthash UTXO set; signAndBroadcast() pulls the signer material
// from api.vault.imports.signer(importId), decodes the WIF or derives the
// mnemonic/path into a 32-byte priv key, and signs every input in RAM.
// The private key never lands in adapter state — signAndBroadcast fetches
// it fresh per broadcast and drops it before returning.
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
module.exports = function makeImportedBchAdapter({
sha256, ripemd160, cashaddr, electrum, WebSocket, tx,
HDKey, secp256k1, base58check, vaultImports,
}) {
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
const p2pkhScript = (h160) => Uint8Array.from([0x76, 0xa9, 0x14, ...h160, 0x88, 0xac]);
const scripthashOf = (script) => toHex(sha256(script).slice().reverse());
const hash160 = (b) => ripemd160(sha256(b));
const IMPORTED_BCH_NETWORKS = {
mainnet: {
id: "mainnet", label: "Mainnet", prefix: "bitcoincash", wifVersion: 0x80,
explorerTx: "https://bchexplorer.cash/tx/",
explorerAddr: "https://bchexplorer.cash/address/",
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
defaultServers: [
"wss://bch.imaginary.cash:50004",
"wss://cashnode.bch.ninja:50004",
"wss://electroncash.dk:50004",
"wss://fulcrum.jettscythe.xyz:50004",
],
},
chipnet: {
id: "chipnet", label: "Chipnet testnet", prefix: "bchtest", wifVersion: 0xef,
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
explorerTx: "https://chipnet.imaginary.cash/tx/",
explorerAddr: "https://chipnet.imaginary.cash/address/",
defaultServers: [
"wss://chipnet.imaginary.cash:50004",
"wss://chipnet.bch.ninja:50004",
],
faucet: "https://tbch.googol.cash/",
},
};
function h160OfCashaddr(addr) {
const clean = String(addr || "").replace(/^bitcoincash:|^bchtest:/, "");
const { type, hash } = cashaddr.decode(addr.includes(":") ? addr : "bitcoincash:" + clean);
if (type !== 0) throw new Error(`imported wallet must be P2PKH (got type ${type})`);
return hash;
}
// Decode a WIF-encoded private key. Accepts both mainnet (0x80) and
// testnet (0xef) version bytes and both compressed and uncompressed
// forms; returns { priv (32 bytes), compressed (bool) }.
function decodeWif(wif, versionByte) {
const bytes = base58check.decodeCheck(String(wif).trim());
if (!(bytes[0] === versionByte || bytes[0] === 0x80 || bytes[0] === 0xef)) {
throw new Error(`unexpected WIF version 0x${bytes[0].toString(16)}`);
}
const compressed = bytes.length === 34 && bytes[33] === 0x01;
const priv = bytes.slice(1, 33);
if (priv.length !== 32) throw new Error("WIF payload is not 32 bytes");
return { priv, compressed };
}
// Derive a P2PKH signer (32-byte priv + 33-byte compressed pubkey) from
// whatever vault.imports.signer returned. Two shapes today:
// { kind: "seed", seed: hex, path: "m/…" } — BIP32 derivation
// { kind: "wif", wif: base58check } — direct decode
// Anything else (or a missing signer) throws with a clear message so
// the panel can surface it rather than the broadcast returning garbage.
function signerToKey(signerBlob, net) {
if (!signerBlob) throw new Error("no signer material for this wallet");
if (signerBlob.kind === "seed") {
const seed = signerBlob.seed;
if (!/^[0-9a-f]+$/i.test(seed)) throw new Error("seed material must be hex");
const seedBytes = Uint8Array.from(seed.match(/../g).map((x) => parseInt(x, 16)));
const node = HDKey.fromMasterSeed(seedBytes).derive(signerBlob.path || "m");
return { priv: node.privateKey, pub: node.publicKey };
}
if (signerBlob.kind === "wif") {
const { priv } = decodeWif(signerBlob.wif, net.wifVersion);
const pub = secp256k1.getPublicKey(priv, true);
return { priv, pub };
}
throw new Error(`unknown signer kind: ${signerBlob.kind}`);
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
class ImportedBchWallet {
constructor({
walletId, storage, log = () => {}, onChange = () => {},
network = "mainnet", cashaddr: address, servers, importId,
} = {}) {
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
const net = IMPORTED_BCH_NETWORKS[network];
if (!net) throw new Error(`chain-bch-imported: unknown network ${network}`);
if (!address) throw new Error("chain-bch-imported: cashaddr required");
this.walletId = walletId;
// importId is the vault-side id used to fetch the signer at
// sign-time. Optional here so a mount without spend capability
// still works (read-only surface unaffected).
this._importId = importId || null;
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
this.chain = "bch";
this.network = net.id;
this._net = net;
this.log = log;
this.onChange = onChange;
this._address = address;
this._h160 = h160OfCashaddr(address);
this._script = p2pkhScript(this._h160);
this._scripthash = scripthashOf(this._script);
this._scriptHex = toHex(this._script);
this._servers = Array.isArray(servers) && servers.length ? servers : net.defaultServers.slice();
this._client = new electrum.Client(this._servers);
this._client.onServer = () => this._emit();
this._state = {
balance: { confirmed: 0, unconfirmed: 0 },
history: [],
utxos: [],
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
height: 0,
fix(aegis): 0.15.0 — ask Electrum for protocol 1.5, which is where the tokens were Aegis negotiated a flat protocol "1.4". Measured against Fulcrum 2.1.0 on chipnet, for a wallet holding CashTokens: asked "1.4" -> negotiated 1.4 -> 39 utxos, 0 with token_data asked ["1.4","1.5.3"] -> negotiated 1.5.3 -> 130 utxos, 91 with token_data So 1.4 did not merely omit the `token_data` field — Fulcrum left the token-bearing outputs out of listunspent altogether. Ninety-one UTXOs were invisible to the wallet, along with the BCH sitting in them. That is a balance-correctness bug, not only a missing Assets card, and it applied to the HD path too, since lib/wallet.js reads the same listunspent. Now a [min, max] range: a modern server picks 1.5.3, an older one still settles on 1.4, so nothing that worked before stops working. The negotiated version is recorded on the client for diagnosis. Second half: the imported BCH adapter had no CashToken code at all — it never set tokenBalances and snapshot() never exposed it, so the panel's Assets card was hidden for every WIF import however many tokens the address held. A chipnet test wallet with 46 categories showed nothing. It now aggregates from the server's own token_data, which costs one call for the whole set rather than the per-UTXO transaction fetch the HD path uses, and emits the same serialised shape the panel already reads. Verified against that wallet: 130 UTXOs, 46 categories, 17 fungible, 32 with NFTs, JSON-clean. Found because the user said their asset "uses a different asset category" and suggested checking with the explorer. It is ordinary CashTokens; the wallet simply could not see them. My earlier conclusion that the empty Assets card was correct came from probing a single address that genuinely holds no tokens and generalising from it.
2026-09-28 22:52:15 +02:00
tokenBalances: null, // { <categoryHex>: {fungible, nfts, utxoCount} }
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
scanning: false,
error: null,
};
}
setServers(list) {
this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice();
this._client.setServers(this._servers);
}
schedulePoll(ms) {
clearTimeout(this._pollTimer);
this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms);
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
_emit() { try { this.onChange(); } catch {} }
snapshot() {
return {
chain: "bch",
network: this._net.id,
ticker: "BCH",
decimals: 8,
address: this._address,
addressIndex: 0,
addressPath: null,
balance: this._state.balance,
height: this._state.height,
history: this._state.history,
fix(aegis): 0.15.0 — ask Electrum for protocol 1.5, which is where the tokens were Aegis negotiated a flat protocol "1.4". Measured against Fulcrum 2.1.0 on chipnet, for a wallet holding CashTokens: asked "1.4" -> negotiated 1.4 -> 39 utxos, 0 with token_data asked ["1.4","1.5.3"] -> negotiated 1.5.3 -> 130 utxos, 91 with token_data So 1.4 did not merely omit the `token_data` field — Fulcrum left the token-bearing outputs out of listunspent altogether. Ninety-one UTXOs were invisible to the wallet, along with the BCH sitting in them. That is a balance-correctness bug, not only a missing Assets card, and it applied to the HD path too, since lib/wallet.js reads the same listunspent. Now a [min, max] range: a modern server picks 1.5.3, an older one still settles on 1.4, so nothing that worked before stops working. The negotiated version is recorded on the client for diagnosis. Second half: the imported BCH adapter had no CashToken code at all — it never set tokenBalances and snapshot() never exposed it, so the panel's Assets card was hidden for every WIF import however many tokens the address held. A chipnet test wallet with 46 categories showed nothing. It now aggregates from the server's own token_data, which costs one call for the whole set rather than the per-UTXO transaction fetch the HD path uses, and emits the same serialised shape the panel already reads. Verified against that wallet: 130 UTXOs, 46 categories, 17 fungible, 32 with NFTs, JSON-clean. Found because the user said their asset "uses a different asset category" and suggested checking with the explorer. It is ordinary CashTokens; the wallet simply could not see them. My earlier conclusion that the empty Assets card was correct came from probing a single address that genuinely holds no tokens and generalising from it.
2026-09-28 22:52:15 +02:00
// Without this the panel's Assets card has nothing to read and stays
// hidden however many tokens refresh() found.
tokenBalances: this._state.tokenBalances || null,
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
scanning: this._state.scanning,
error: this._state.error,
server: this._client.url || null,
servers: this._servers,
imported: true,
// 0.6.36+: imported wallets can spend when the vault signer is
// reachable (i.e. Theseus is unlocked). canSpend reflects that so
// the panel can enable the Send tab without probing.
canSpend: !!this._importId,
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
explorerTx: this._net.explorerTx,
explorerAddr: this._net.explorerAddr,
faucet: this._net.faucet,
};
}
async refresh(full) {
this._state.scanning = true; this._emit();
try {
const bal = await this._client.call("blockchain.scripthash.get_balance", [this._scripthash]);
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
this._state.balance = { confirmed: Number(bal?.confirmed || 0), unconfirmed: Number(bal?.unconfirmed || 0) };
// Always pull UTXOs so spend / send-max work off fresh state.
const utxos = await this._client.call("blockchain.scripthash.listunspent", [this._scripthash]);
fix(aegis): 0.15.0 — ask Electrum for protocol 1.5, which is where the tokens were Aegis negotiated a flat protocol "1.4". Measured against Fulcrum 2.1.0 on chipnet, for a wallet holding CashTokens: asked "1.4" -> negotiated 1.4 -> 39 utxos, 0 with token_data asked ["1.4","1.5.3"] -> negotiated 1.5.3 -> 130 utxos, 91 with token_data So 1.4 did not merely omit the `token_data` field — Fulcrum left the token-bearing outputs out of listunspent altogether. Ninety-one UTXOs were invisible to the wallet, along with the BCH sitting in them. That is a balance-correctness bug, not only a missing Assets card, and it applied to the HD path too, since lib/wallet.js reads the same listunspent. Now a [min, max] range: a modern server picks 1.5.3, an older one still settles on 1.4, so nothing that worked before stops working. The negotiated version is recorded on the client for diagnosis. Second half: the imported BCH adapter had no CashToken code at all — it never set tokenBalances and snapshot() never exposed it, so the panel's Assets card was hidden for every WIF import however many tokens the address held. A chipnet test wallet with 46 categories showed nothing. It now aggregates from the server's own token_data, which costs one call for the whole set rather than the per-UTXO transaction fetch the HD path uses, and emits the same serialised shape the panel already reads. Verified against that wallet: 130 UTXOs, 46 categories, 17 fungible, 32 with NFTs, JSON-clean. Found because the user said their asset "uses a different asset category" and suggested checking with the explorer. It is ordinary CashTokens; the wallet simply could not see them. My earlier conclusion that the empty Assets card was correct came from probing a single address that genuinely holds no tokens and generalising from it.
2026-09-28 22:52:15 +02:00
const list = Array.isArray(utxos) ? utxos : [];
this._state.utxos = list.map((u) => ({
txid: u.tx_hash, vout: u.tx_pos, value: Number(u.value), height: Number(u.height || 0),
fix(aegis): 0.15.0 — ask Electrum for protocol 1.5, which is where the tokens were Aegis negotiated a flat protocol "1.4". Measured against Fulcrum 2.1.0 on chipnet, for a wallet holding CashTokens: asked "1.4" -> negotiated 1.4 -> 39 utxos, 0 with token_data asked ["1.4","1.5.3"] -> negotiated 1.5.3 -> 130 utxos, 91 with token_data So 1.4 did not merely omit the `token_data` field — Fulcrum left the token-bearing outputs out of listunspent altogether. Ninety-one UTXOs were invisible to the wallet, along with the BCH sitting in them. That is a balance-correctness bug, not only a missing Assets card, and it applied to the HD path too, since lib/wallet.js reads the same listunspent. Now a [min, max] range: a modern server picks 1.5.3, an older one still settles on 1.4, so nothing that worked before stops working. The negotiated version is recorded on the client for diagnosis. Second half: the imported BCH adapter had no CashToken code at all — it never set tokenBalances and snapshot() never exposed it, so the panel's Assets card was hidden for every WIF import however many tokens the address held. A chipnet test wallet with 46 categories showed nothing. It now aggregates from the server's own token_data, which costs one call for the whole set rather than the per-UTXO transaction fetch the HD path uses, and emits the same serialised shape the panel already reads. Verified against that wallet: 130 UTXOs, 46 categories, 17 fungible, 32 with NFTs, JSON-clean. Found because the user said their asset "uses a different asset category" and suggested checking with the explorer. It is ordinary CashTokens; the wallet simply could not see them. My earlier conclusion that the empty Assets card was correct came from probing a single address that genuinely holds no tokens and generalising from it.
2026-09-28 22:52:15 +02:00
token: u.token_data || null,
}));
fix(aegis): 0.15.0 — ask Electrum for protocol 1.5, which is where the tokens were Aegis negotiated a flat protocol "1.4". Measured against Fulcrum 2.1.0 on chipnet, for a wallet holding CashTokens: asked "1.4" -> negotiated 1.4 -> 39 utxos, 0 with token_data asked ["1.4","1.5.3"] -> negotiated 1.5.3 -> 130 utxos, 91 with token_data So 1.4 did not merely omit the `token_data` field — Fulcrum left the token-bearing outputs out of listunspent altogether. Ninety-one UTXOs were invisible to the wallet, along with the BCH sitting in them. That is a balance-correctness bug, not only a missing Assets card, and it applied to the HD path too, since lib/wallet.js reads the same listunspent. Now a [min, max] range: a modern server picks 1.5.3, an older one still settles on 1.4, so nothing that worked before stops working. The negotiated version is recorded on the client for diagnosis. Second half: the imported BCH adapter had no CashToken code at all — it never set tokenBalances and snapshot() never exposed it, so the panel's Assets card was hidden for every WIF import however many tokens the address held. A chipnet test wallet with 46 categories showed nothing. It now aggregates from the server's own token_data, which costs one call for the whole set rather than the per-UTXO transaction fetch the HD path uses, and emits the same serialised shape the panel already reads. Verified against that wallet: 130 UTXOs, 46 categories, 17 fungible, 32 with NFTs, JSON-clean. Found because the user said their asset "uses a different asset category" and suggested checking with the explorer. It is ordinary CashTokens; the wallet simply could not see them. My earlier conclusion that the empty Assets card was correct came from probing a single address that genuinely holds no tokens and generalising from it.
2026-09-28 22:52:15 +02:00
// CashTokens. This adapter never looked for them, so a WIF-imported
// wallet holding tokens reported none and the panel hid its Assets
// card — a chipnet test wallet with 46 categories showed nothing.
//
// The HD path (lib/wallet.js) decodes the token prefix off each
// UTXO's scriptPubKey, which costs one transaction fetch per UTXO.
// Here we take the server's own `token_data` instead: one call for
// the whole set. That arrives only when protocol >= 1.5 was
// negotiated (see electrum.js), and when it does not, tokens are
// simply absent — the same as before this change, never wrong.
const tokenBalances = {};
for (const u of list) {
const t = u.token_data;
if (!t || !t.category) continue;
const cat = String(t.category);
if (!tokenBalances[cat]) tokenBalances[cat] = { fungible: 0n, nfts: [], utxoCount: 0 };
const b = tokenBalances[cat];
b.utxoCount++;
if (t.amount) { try { b.fungible += BigInt(t.amount); } catch (_e) {} }
if (t.nft) {
b.nfts.push({
utxoId: `${u.tx_hash}:${u.tx_pos}`,
commitmentHex: t.nft.commitment || "",
capability: t.nft.capability || "none",
capabilityLabel: t.nft.capability || "none",
});
}
}
// Same serialised shape the HD wallet emits — fungible as a decimal
// string, since JSON.stringify cannot carry a BigInt.
const serialised = {};
for (const [cat, b] of Object.entries(tokenBalances)) {
serialised[cat] = { fungible: b.fungible.toString(), nfts: b.nfts, utxoCount: b.utxoCount };
}
this._state.tokenBalances = serialised;
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
if (full) {
const hist = await this._client.call("blockchain.scripthash.get_history", [this._scripthash]);
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
this._state.history = (hist || []).slice(-50).map((h) => ({
txid: h.tx_hash, time: 0, delta: 0, confirmations: h.height > 0 ? 1 : 0,
}));
}
this._state.error = null;
} catch (e) {
this._state.error = e?.message || String(e);
} finally {
this._state.scanning = false;
this._emit();
}
}
nextAddress() { return { address: this._address, index: 0 }; }
current() {
return {
address: this._address, index: 0, branch: 0, path: null,
h160: this._h160, script: this._script, scripthash: this._scripthash, scriptHex: this._scriptHex,
};
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
// 0.6.36 spend path. Builds an unsigned P2PKH plan against the wallet's
// own UTXO set. Signing happens in signAndBroadcast, which fetches the
// key material from Theseus's vault at broadcast time — nothing key-
// bearing lives in the plan itself, so a plan can round-trip through
// the approval overlay without leaking secrets.
plan(spec) {
if (!this._state.utxos.length) throw new Error("wallet has no unspent outputs to spend from");
const targets = Array.isArray(spec?.outputs) && spec.outputs.length
? spec.outputs.map((o) => ({ to: o.to, value: o.amount ?? o.value }))
: [{ to: spec?.to, value: spec?.amount ?? spec?.value }];
const outs = targets.map((t) => {
const a = cashaddr.parseAny(t.to, sha256, this._net.prefix);
const script = a.type === 0
? Uint8Array.from([0x76, 0xa9, 0x14, ...a.hash, 0x88, 0xac])
: Uint8Array.from([0xa9, 0x14, ...a.hash, 0x87]);
return { value: Math.round(Number(t.value) || 0), script, to: a.cashaddr };
});
if (spec?.memo) outs.push({ value: 0, script: tx.memoScript(String(spec.memo)), data: true, memo: String(spec.memo) });
const rate = Math.min(10, Math.max(1, Number(spec?.feeRate) || 1));
// Imported wallets have exactly one address, so change goes back to
// itself — no need to derive a fresh change entry from an HD tree.
const changeScript = this._script;
const spendable = this._state.utxos.slice().sort((a, b) => (b.height > 0) - (a.height > 0));
const sel = tx.select(spendable, outs, rate, changeScript, { sendMax: !!spec?.sendMax });
const nonData = sel.outputs.filter((o) => !o.data);
const total = sel.outputs.reduce((a, o) => a + o.value, 0);
return {
...sel,
feeRate: rate,
recipients: nonData
.filter((_, i) => outs[i] && !outs[i].data)
.map((o, i) => ({ to: outs[i].to, value: o.value })),
memo: spec?.memo || null,
total,
};
}
async signAndBroadcast(plan) {
if (!this._importId) throw new Error("this wallet has no signer registered");
if (!vaultImports || typeof vaultImports.signer !== "function") throw new Error("vault.imports.signer unavailable");
const signerBlob = await vaultImports.signer(this._importId);
let key;
try {
key = signerToKey(signerBlob, this._net);
// Belt-and-braces: the signer must match the wallet's own address.
// Catches vault-side corruption and any accidental cross-mount.
const derivedH160 = hash160(key.pub);
const same = derivedH160.length === this._h160.length && derivedH160.every((b, i) => b === this._h160[i]);
if (!same) throw new Error("signer material does not match this wallet's address");
const inputs = plan.inputs.map((u) => ({ ...u, script: this._script }));
const t = { inputs, outputs: plan.outputs };
const signed = tx.sign(t, (inp, _i, digest) => ({
sig: secp256k1.sign(digest, key.priv, { prehash: false, lowS: true, format: "der" }),
publicKey: key.pub,
}));
const txid = await this._client.call("blockchain.transaction.broadcast", [signed.hex]);
if (typeof txid !== "string" || txid.length !== 64) throw new Error("broadcast rejected: " + JSON.stringify(txid));
this.log("broadcast", txid);
setTimeout(() => this.refresh(false).catch(() => {}), 1500);
return { txid, hex: signed.hex, fee: plan.fee };
} finally {
// Wipe the private material before returning. Not perfect (JS can
// still relocate the underlying buffer during GC) but it minimises
// the window in which the raw key sits in this frame.
if (key && key.priv && key.priv.fill) { try { key.priv.fill(0); } catch {} }
}
}
// BIP-137 message signing from the imported key. Same MAGIC / double-
// sha256 payload as chain-bch.js so the resulting sig verifies through
// Electron Cash and every other BCH tool.
async signMessage(message) {
if (!this._importId) throw new Error("this wallet has no signer registered");
if (!vaultImports || typeof vaultImports.signer !== "function") throw new Error("vault.imports.signer unavailable");
const signerBlob = await vaultImports.signer(this._importId);
let key;
try {
key = signerToKey(signerBlob, this._net);
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 sig = secp256k1.sign(digest, key.priv, { prehash: false, lowS: true, format: "recovered" });
const out = new Uint8Array(65);
out[0] = 27 + sig[0] + 4;
out.set(sig.subarray(1), 1);
return { address: this._address, signature: Buffer.from(out).toString("base64") };
} finally {
if (key && key.priv && key.priv.fill) { try { key.priv.fill(0); } catch {} }
}
}
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import (Deviant keystore or wherever you got the seed/WIF from)." }; }
dispose() { clearTimeout(this._pollTimer); try { this._client.disconnect(); } catch {} }
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
}
return { ImportedBchWallet, IMPORTED_BCH_NETWORKS };
};