- Inline SVG logos for BCH (green disc + ₿) and TRX (red disc + geometric T) replace the 🟨/🔴/🔵 emoji in the sidebar header and wallet picker rows. The approval overlay stays text-only ("Wallet: <name> — BCH · Mainnet") because that surface renders plain rows, not HTML. - Wallet picker's "Add wallet" is now two-step: click a coin to expand its networks, then click a network to create the wallet. The flat list is gone. - BCH Chipnet is a real chain option now: bchtest cashaddr prefix, m/44'/1'/0' derivation (BIP44 testnet coin type), bundled Chipnet electrum defaults, chipnet.imaginary.cash explorer, tbch.googol.cash faucet link in Receive. The shared electrum-servers setting stays mainnet-only in this rev; Chipnet uses adapter-embedded defaults. - lib/chain-bch.js gained a BCH_NETWORKS table so mainnet vs chipnet differences (prefix, path, servers, explorer, faucet) live in one place. - Registry is grouped by coin ({networks:{…}}) instead of a flat chain:network map — snapshot exposes coins[] for the panel and adds coinLabel/networkLabel/testnet fields per wallet. - Testnet wallets get a small "TEST" tag next to the network name so the user can never mistake a chipnet or Nile balance for real money. Legacy BCH mainnet index 0 derivation unchanged (bchwallet/mainnet/0 → m/44'/145'/0' → bitcoincash prefix); the network parameter defaults to "mainnet" and BCH_NETWORKS.mainnet reproduces the pre-change constants.
156 lines
6 KiB
JavaScript
156 lines
6 KiB
JavaScript
// BCH chain adapter — wraps the existing keys.js / wallet.js / tx.js /
|
|
// electrum.js / cashaddr.js code with the common adapter shape that the
|
|
// multi-wallet manager talks to. Every BCH wallet is one BIP32 account
|
|
// derived from its own 32-byte root (from api.vault.derive).
|
|
//
|
|
// Deps are handed in so index.js loads @noble/* once and shares them across
|
|
// every wallet, rather than each adapter dynamic-importing on its own.
|
|
|
|
const BCH_NETWORKS = {
|
|
mainnet: {
|
|
id: "mainnet",
|
|
label: "Mainnet",
|
|
prefix: "bitcoincash",
|
|
defaultAccountPath: "m/44'/145'/0'",
|
|
explorerTx: "https://blockchair.com/bitcoin-cash/transaction/",
|
|
explorerAddr: "https://blockchair.com/bitcoin-cash/address/",
|
|
defaultServers: [
|
|
"wss://bch.imaginary.cash:50004",
|
|
"wss://cashnode.bch.ninja:50004",
|
|
"wss://electroncash.dk:50004",
|
|
"wss://fulcrum.jettscythe.xyz:50004",
|
|
],
|
|
faucet: null,
|
|
},
|
|
chipnet: {
|
|
id: "chipnet",
|
|
label: "Chipnet testnet",
|
|
prefix: "bchtest",
|
|
// BIP44 testnet coin type is 1 across every chain; the account is still 0.
|
|
defaultAccountPath: "m/44'/1'/0'",
|
|
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/",
|
|
},
|
|
};
|
|
|
|
module.exports = function makeBchAdapter({
|
|
HDKey, secp256k1, sha256, ripemd160, cashaddr,
|
|
keysLib, tx, electrum, WebSocket,
|
|
}) {
|
|
|
|
// storage is the FULL api.storage. keyPrefix scopes every read/write under
|
|
// "wallets/<walletId>/…" so multiple BCH wallets don't stomp each other.
|
|
function scopedStorage(storage, keyPrefix) {
|
|
const k = (key) => keyPrefix + key;
|
|
return {
|
|
get: (key, fallback = null) => storage.get(k(key), fallback),
|
|
set: (key, value) => storage.set(k(key), value),
|
|
};
|
|
}
|
|
|
|
class BchWallet {
|
|
constructor(root32, {
|
|
walletId, storage, log = () => {}, onChange = () => {}, servers,
|
|
network = "mainnet", accountPath,
|
|
} = {}) {
|
|
if (!walletId) throw new Error("chain-bch: walletId required");
|
|
const net = BCH_NETWORKS[network];
|
|
if (!net) throw new Error(`chain-bch: unknown network ${network}`);
|
|
this.walletId = walletId;
|
|
this.chain = "bch";
|
|
this.network = net.id;
|
|
this._net = net;
|
|
this.log = log;
|
|
this.onChange = onChange;
|
|
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
|
// Servers: caller-provided override → user-set custom list (handled by
|
|
// index.js already, this is a fallback path) → the network's built-in
|
|
// defaults so a wallet always has somewhere to connect.
|
|
this._servers = Array.isArray(servers) && servers.length ? servers : net.defaultServers.slice();
|
|
const wantPath = accountPath || net.defaultAccountPath;
|
|
this._accountPath = /^m(\/\d+'?)+$/.test(wantPath) ? wantPath : net.defaultAccountPath;
|
|
this._client = new electrum.Client(this._servers);
|
|
this._client.onServer = () => this._emit();
|
|
this._keys = new keysLib.WalletKeys(root32, this._accountPath, net.prefix);
|
|
this._root = new Uint8Array(root32);
|
|
const walletFactory = require("./wallet.js");
|
|
this._wallet = walletFactory({
|
|
client: this._client, keys: this._keys, tx, cashaddr, sha256,
|
|
storage: this.storage,
|
|
log: (...a) => this.log(...a),
|
|
onChange: () => this._emit(),
|
|
});
|
|
}
|
|
|
|
setServers(list) {
|
|
this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice();
|
|
this._client.setServers(this._servers);
|
|
}
|
|
|
|
_emit() { try { this.onChange(); } catch {} }
|
|
|
|
snapshot() {
|
|
const w = this._wallet.snapshot();
|
|
return {
|
|
chain: "bch",
|
|
network: this._net.id,
|
|
ticker: "BCH",
|
|
decimals: 8,
|
|
address: w.address,
|
|
addressIndex: w.addressIndex,
|
|
addressPath: w.addressPath,
|
|
balance: w.balance,
|
|
height: w.height,
|
|
history: w.history,
|
|
scanning: w.scanning,
|
|
error: w.error,
|
|
server: this._client.url || null,
|
|
servers: this._servers,
|
|
accountPath: this._accountPath,
|
|
xpub: this._keys.xpub,
|
|
explorerTx: this._net.explorerTx,
|
|
explorerAddr: this._net.explorerAddr,
|
|
faucet: this._net.faucet,
|
|
};
|
|
}
|
|
|
|
async refresh(full) { return this._wallet.refresh(!!full); }
|
|
nextAddress() { return this._wallet.nextUnusedAddress(); }
|
|
current() { return this._wallet.current(); }
|
|
plan(spec) {
|
|
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 }];
|
|
return this._wallet.plan({ targets, feeRate: spec.feeRate, sendMax: !!spec.sendMax });
|
|
}
|
|
async signAndBroadcast(plan) { return this._wallet.signAndBroadcast(plan); }
|
|
// 65-byte BIP-137 recoverable signature — the format Electron Cash and
|
|
// most BCH tooling verify against.
|
|
signMessage(message) {
|
|
const enc = new TextEncoder();
|
|
const varstr = (s) => { const b = enc.encode(s); if (b.length >= 0xfd) throw new Error("too long"); return Uint8Array.from([b.length, ...b]); };
|
|
const MAGIC = "Bitcoin Signed Message:\n";
|
|
const payload = Uint8Array.from([...varstr(MAGIC), ...varstr(String(message))]);
|
|
const digest = sha256(sha256(payload));
|
|
const entry = this.current();
|
|
const sig = this._keys.signRecoverable(entry, digest);
|
|
return { address: entry.address, signature: Buffer.from(sig).toString("base64") };
|
|
}
|
|
recovery() {
|
|
return { accountPath: this._accountPath, xpub: this._keys.xpub, xprv: this._keys.xprv };
|
|
}
|
|
dispose() {
|
|
try { this._wallet.dispose(); } catch {}
|
|
try { this._keys.wipe(); } catch {}
|
|
try { this._client.disconnect(); } catch {}
|
|
if (this._root) this._root.fill(0);
|
|
}
|
|
}
|
|
|
|
return { BchWallet, BCH_NETWORKS };
|
|
};
|