feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
// Electrum (Fulcrum) JSON-RPC over WebSocket for the wallet. One live
|
|
|
|
|
// connection at a time, chosen by walking the server list in order; the
|
|
|
|
|
// caller gets a stable `call()` that reconnects transparently on the next
|
|
|
|
|
// request after a drop. Notifications (headers / scripthash subscriptions)
|
|
|
|
|
// fan out to `onNotify`.
|
|
|
|
|
module.exports = function makeElectrum({ WebSocket, log = () => {} }) {
|
|
|
|
|
const CALL_TIMEOUT_MS = 20000;
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
const CONNECT_TIMEOUT_MS = 10000;
|
|
|
|
|
const PING_EVERY_MS = 60000;
|
|
|
|
|
const RECONNECT_MIN_MS = 2000;
|
|
|
|
|
const RECONNECT_MAX_MS = 60000;
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
|
|
|
|
|
class Connection {
|
|
|
|
|
constructor(url) {
|
|
|
|
|
this.url = url;
|
|
|
|
|
this.id = 0;
|
|
|
|
|
this.pending = new Map();
|
|
|
|
|
this.buf = "";
|
|
|
|
|
this.closed = false;
|
|
|
|
|
this.onNotify = null;
|
|
|
|
|
this.onClose = null;
|
|
|
|
|
}
|
|
|
|
|
connect() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const ws = new WebSocket(this.url);
|
|
|
|
|
this.ws = ws;
|
|
|
|
|
const fail = (e) => { if (!this.closed) { this.closed = true; reject(e instanceof Error ? e : new Error("electrum ws error: " + this.url)); } };
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
// A server that accepts the TCP connection and then says nothing
|
|
|
|
|
// used to hold the whole server walk hostage; give up and move on.
|
|
|
|
|
const connectTimer = setTimeout(() => { fail(new Error("electrum connect timeout: " + this.url)); try { ws.close(); } catch {} }, CONNECT_TIMEOUT_MS);
|
|
|
|
|
ws.on("open", () => clearTimeout(connectTimer));
|
|
|
|
|
ws.on("close", () => clearTimeout(connectTimer));
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
ws.on("open", async () => {
|
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
|
|
|
// A RANGE, not a flat "1.4". Fulcrum only attaches `token_data` to
|
|
|
|
|
// listunspent results once protocol >= 1.5 is negotiated, and with
|
|
|
|
|
// a flat 1.4 it silently omits it — which is why imported BCH
|
|
|
|
|
// wallets showed no CashTokens at all. A [min, max] pair lets a
|
|
|
|
|
// modern server pick 1.5.3 while an older one still settles on 1.4,
|
|
|
|
|
// so nothing that worked before stops working.
|
|
|
|
|
try {
|
|
|
|
|
const v = await this.call("server.version", ["theseus-bchwallet", ["1.4", "1.5.3"]]);
|
|
|
|
|
this.serverVersion = Array.isArray(v) ? v[0] : null;
|
|
|
|
|
this.protocolVersion = Array.isArray(v) ? v[1] : null;
|
|
|
|
|
resolve(this);
|
|
|
|
|
}
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
catch (e) { fail(e); this.close(); }
|
|
|
|
|
});
|
|
|
|
|
ws.on("error", fail);
|
|
|
|
|
ws.on("message", (d) => this._onData(String(d)));
|
|
|
|
|
ws.on("close", () => {
|
|
|
|
|
this.closed = true;
|
|
|
|
|
for (const p of this.pending.values()) p.reject(new Error("electrum connection closed"));
|
|
|
|
|
this.pending.clear();
|
|
|
|
|
if (this.onClose) this.onClose();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
_onData(chunk) {
|
|
|
|
|
this.buf += chunk;
|
2026-10-04 04:09:45 +02:00
|
|
|
// A server that streams without newlines used to grow this buffer
|
|
|
|
|
// without bound on the Electron main thread. No legitimate reply is
|
|
|
|
|
// anywhere near this size.
|
|
|
|
|
if (this.buf.length > 8 * 1024 * 1024) {
|
|
|
|
|
this.buf = "";
|
|
|
|
|
try { this.ws && this.ws.close(); } catch {}
|
|
|
|
|
for (const p of this.pending.values()) p.reject(new Error("electrum server sent an oversized reply"));
|
|
|
|
|
this.pending.clear();
|
|
|
|
|
return;
|
|
|
|
|
}
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
let nl;
|
|
|
|
|
while ((nl = this.buf.indexOf("\n")) >= 0) {
|
|
|
|
|
const line = this.buf.slice(0, nl).trim();
|
|
|
|
|
this.buf = this.buf.slice(nl + 1);
|
|
|
|
|
if (line) this._handleLine(line);
|
|
|
|
|
}
|
2026-10-04 04:09:45 +02:00
|
|
|
// Re-parsing the whole tail on every chunk was quadratic; only try
|
|
|
|
|
// once it could be a complete JSON object.
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
const rest = this.buf.trim();
|
2026-10-04 04:09:45 +02:00
|
|
|
if (rest && rest.endsWith("}")) { try { JSON.parse(rest); this._handleLine(rest); this.buf = ""; } catch {} }
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
}
|
|
|
|
|
_handleLine(line) {
|
|
|
|
|
let msg;
|
|
|
|
|
try { msg = JSON.parse(line); } catch { return; }
|
|
|
|
|
if (msg.id != null && this.pending.has(msg.id)) {
|
|
|
|
|
const p = this.pending.get(msg.id);
|
|
|
|
|
this.pending.delete(msg.id);
|
|
|
|
|
clearTimeout(p.timer);
|
|
|
|
|
if (msg.error) p.reject(new Error(typeof msg.error === "object" ? (msg.error.message || JSON.stringify(msg.error)) : String(msg.error)));
|
|
|
|
|
else p.resolve(msg.result);
|
|
|
|
|
} else if (msg.method && this.onNotify) {
|
|
|
|
|
this.onNotify(msg.method, msg.params || []);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
call(method, params = []) {
|
|
|
|
|
if (this.closed) return Promise.reject(new Error("electrum connection closed"));
|
|
|
|
|
const id = ++this.id;
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`electrum timeout: ${method}`)); }
|
|
|
|
|
}, CALL_TIMEOUT_MS);
|
|
|
|
|
this.pending.set(id, { resolve, reject, timer });
|
|
|
|
|
try { this.ws.send(JSON.stringify({ id, method, params }) + "\n"); }
|
|
|
|
|
catch (e) { clearTimeout(timer); this.pending.delete(id); reject(e); }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
close() { this.closed = true; try { this.ws.close(); } catch {} }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Client {
|
|
|
|
|
constructor(servers) {
|
|
|
|
|
this.servers = servers.slice();
|
|
|
|
|
this.conn = null;
|
|
|
|
|
this.connecting = null;
|
|
|
|
|
this.subscriptions = new Map(); // method+key -> params (replayed on reconnect)
|
|
|
|
|
this.onNotify = null;
|
|
|
|
|
this.onServer = null; // (url|null) connection state for the UI
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
this.disposed = false;
|
|
|
|
|
this._reconnectTimer = null;
|
|
|
|
|
this._reconnectDelay = RECONNECT_MIN_MS;
|
|
|
|
|
this._pingTimer = null;
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
}
|
|
|
|
|
setServers(servers) {
|
|
|
|
|
this.servers = servers.slice();
|
|
|
|
|
this.disconnect();
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
this._scheduleReconnect(250); // keep the live feed going on the new list
|
|
|
|
|
}
|
|
|
|
|
// A wallet with live subscriptions used to go quiet for good when its
|
|
|
|
|
// server dropped: the connection was only re-opened by the next call(),
|
|
|
|
|
// and nothing calls while the user is just looking at a balance. Come
|
|
|
|
|
// back on our own, with backoff, for as long as anything is subscribed.
|
|
|
|
|
_scheduleReconnect(delay) {
|
|
|
|
|
if (this.disposed || !this.subscriptions.size || this._reconnectTimer) return;
|
|
|
|
|
const wait = delay != null ? delay : this._reconnectDelay;
|
|
|
|
|
this._reconnectTimer = setTimeout(() => {
|
|
|
|
|
this._reconnectTimer = null;
|
|
|
|
|
if (this.disposed || (this.conn && !this.conn.closed)) return;
|
|
|
|
|
this._ensure().then(
|
|
|
|
|
() => { this._reconnectDelay = RECONNECT_MIN_MS; },
|
|
|
|
|
() => { this._reconnectDelay = Math.min(RECONNECT_MAX_MS, this._reconnectDelay * 2); this._scheduleReconnect(); },
|
|
|
|
|
);
|
|
|
|
|
}, wait);
|
|
|
|
|
if (this._reconnectTimer.unref) this._reconnectTimer.unref();
|
|
|
|
|
}
|
|
|
|
|
// A half-dead socket (laptop slept, NAT dropped the flow) never fires
|
|
|
|
|
// "close". Ping it; a ping that fails or times out closes it, which
|
|
|
|
|
// lands in the reconnect path above.
|
|
|
|
|
_armPing(c) {
|
|
|
|
|
clearInterval(this._pingTimer);
|
|
|
|
|
this._pingTimer = setInterval(() => {
|
|
|
|
|
if (this.disposed || this.conn !== c || c.closed) { clearInterval(this._pingTimer); return; }
|
|
|
|
|
c.call("server.ping", []).catch(() => { try { c.close(); } catch {} });
|
|
|
|
|
}, PING_EVERY_MS);
|
|
|
|
|
if (this._pingTimer.unref) this._pingTimer.unref();
|
|
|
|
|
}
|
|
|
|
|
// Stop for good: no reconnects, no pings, and any later call() fails
|
|
|
|
|
// instead of quietly resurrecting the connection of a removed wallet.
|
|
|
|
|
dispose() {
|
|
|
|
|
this.disposed = true;
|
|
|
|
|
clearTimeout(this._reconnectTimer); this._reconnectTimer = null;
|
|
|
|
|
clearInterval(this._pingTimer); this._pingTimer = null;
|
|
|
|
|
this.subscriptions.clear();
|
|
|
|
|
this.disconnect();
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
}
|
|
|
|
|
get url() { return this.conn && !this.conn.closed ? this.conn.url : null; }
|
fix(aegis): 0.20.0 — the UTXO scan was 28k requests that always found nothing
Went looking for a performance fix and found a correctness bug underneath it.
getTx() slims each transaction on the way into the cache, and the slim shape
kept only { value, scriptHex } — dropping vout.tokenData. That field is where
the server reports CashTokens. It is NOT in scriptPubKey.hex, which Fulcrum
returns with the token prefix already stripped. So the classify pass fetched
one transaction per UTXO, looked for a prefix that was never there, and
concluded "no token" every single time.
Measured against a real chipnet wallet (130 UTXOs, 91 of them token-bearing):
the old pass cost 54 requests for that address and found 0 tokens; sampling
12 of those transactions, exactly 0 had a scriptPubKey starting with the
token prefix. On the faucet-fed address with 28,289 UTXOs it was ~28k
requests, still finding nothing — which is what made the wallet look hung.
So HD BCH wallets have never shown CashTokens. 0.15.0 fixed the imported
adapter, which reads token_data off listunspent, and I took the HD path's
silence for an empty wallet.
Now: when the server negotiated protocol >= 1.5, listunspent carries
token_data and a UTXO WITHOUT it is definitively not token-bearing, so the
whole set is classified from the one call we already make — 1 request instead
of 28,289. Below 1.5 the fallback fetches parents as before but reads
vout.tokenData (present even at 1.4) and only decodes a prefix as a last
resort, so it is correct now too.
Both routes are reconciled onto one shape. They speak different dialects:
the decoder yields a numeric capability (0/1/2) labelled
immutable/mutable/minting, while Electrum sends a string and calls 0 "none".
Left alone, an identical UTXO would have described itself differently
depending on which server answered. The decoder's vocabulary wins, and the
Certificates pane treats immutable as the quiet default so only capabilities
that change what the holder can do get a tag.
txCache is versioned and dropped once: entries written by the old shape carry
no token information, and an absent field cannot be told apart from "no
token", so a warm cache on a pre-1.5 server would have reported a token
wallet as empty.
Verified against the live wallet: one request, 91 token UTXOs, 46 categories,
17 assets, 51 certificates — matching what the panel reports — with
capability labels normalised (5 immutable, 28 mutable, 18 minting).
2026-09-29 00:56:06 +02:00
|
|
|
// The protocol the live connection settled on. Callers use it to decide
|
|
|
|
|
// whether listunspent will carry `token_data` (>= 1.5) or whether they
|
|
|
|
|
// have to classify tokens the expensive way, by fetching each UTXO's
|
|
|
|
|
// parent transaction.
|
|
|
|
|
get protocolVersion() { return this.conn && !this.conn.closed ? (this.conn.protocolVersion || null) : null; }
|
|
|
|
|
// True when the server will report CashTokens on listunspent itself.
|
|
|
|
|
get hasTokenData() {
|
|
|
|
|
const v = String(this.protocolVersion || "");
|
|
|
|
|
const m = /^(\d+)\.(\d+)/.exec(v);
|
|
|
|
|
if (!m) return false;
|
|
|
|
|
const major = Number(m[1]), minor = Number(m[2]);
|
|
|
|
|
return major > 1 || (major === 1 && minor >= 5);
|
|
|
|
|
}
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
async _ensure() {
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
if (this.disposed) throw new Error("electrum client disposed");
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
if (this.conn && !this.conn.closed) return this.conn;
|
|
|
|
|
if (this.connecting) return this.connecting;
|
|
|
|
|
this.connecting = (async () => {
|
|
|
|
|
let lastErr;
|
|
|
|
|
for (const url of this.servers) {
|
|
|
|
|
try {
|
|
|
|
|
const c = await new Connection(url).connect();
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
if (this.disposed) { c.close(); throw new Error("electrum client disposed"); }
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
c.onNotify = (m, p) => { if (this.onNotify) this.onNotify(m, p); };
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
c.onClose = () => {
|
|
|
|
|
if (this.conn !== c) return;
|
|
|
|
|
this.conn = null;
|
|
|
|
|
if (this.onServer) this.onServer(null);
|
|
|
|
|
this._scheduleReconnect();
|
|
|
|
|
};
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
this.conn = c;
|
|
|
|
|
log("connected", url);
|
|
|
|
|
if (this.onServer) this.onServer(url);
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
this._armPing(c);
|
|
|
|
|
// Re-arm subscriptions so a reconnect keeps the live feed, and
|
|
|
|
|
// hand each answer on as if it were a notification: whatever
|
|
|
|
|
// changed while we were away (a new tip, a payment) is in that
|
|
|
|
|
// answer, and the wallet only refreshes when it is told.
|
|
|
|
|
for (const [method, params] of this.subscriptions.values()) {
|
|
|
|
|
c.call(method, params).then((result) => {
|
|
|
|
|
if (!this.onNotify || this.conn !== c) return;
|
|
|
|
|
this.onNotify(method, method === "blockchain.headers.subscribe" ? [result] : [...params, result]);
|
|
|
|
|
}, () => {});
|
|
|
|
|
}
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
return c;
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
} catch (e) { lastErr = e; log("failed", url, e?.message); if (this.disposed) break; }
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
}
|
|
|
|
|
throw lastErr || new Error("no electrum server reachable");
|
|
|
|
|
})();
|
|
|
|
|
try { return await this.connecting; }
|
|
|
|
|
finally { this.connecting = null; }
|
|
|
|
|
}
|
|
|
|
|
async call(method, params = []) {
|
|
|
|
|
const c = await this._ensure();
|
|
|
|
|
return c.call(method, params);
|
|
|
|
|
}
|
|
|
|
|
// Remember a subscription so it survives reconnects.
|
|
|
|
|
async subscribe(method, params = []) {
|
|
|
|
|
this.subscriptions.set(method + ":" + JSON.stringify(params), [method, params]);
|
|
|
|
|
return this.call(method, params);
|
|
|
|
|
}
|
|
|
|
|
clearSubscriptions() { this.subscriptions.clear(); }
|
|
|
|
|
disconnect() {
|
Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
|
|
|
clearInterval(this._pingTimer); this._pingTimer = null;
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
if (this.conn) { const c = this.conn; this.conn = null; c.close(); }
|
|
|
|
|
if (this.onServer) this.onServer(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { Client };
|
|
|
|
|
};
|