2026-09-14 02:30:51 +02:00
|
|
|
// Generic single-address read-only imported adapter for UTXO chains
|
|
|
|
|
// (BTC + DGB). Uses electrum for balance + history, bitcoinjs to convert
|
|
|
|
|
// the address back into a locking script for the scripthash.
|
|
|
|
|
//
|
|
|
|
|
// M.1b will add spending; for now these wallets show as read-only,
|
|
|
|
|
// matching chain-bch-imported.js's stance.
|
|
|
|
|
|
|
|
|
|
module.exports = function makeUtxoImportedAdapter({ sha256, bitcoinjs, dgbCore, electrum, WebSocket }) {
|
|
|
|
|
|
|
|
|
|
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
|
|
|
const scripthashOf = (script) => toHex(sha256(script).slice().reverse());
|
|
|
|
|
|
|
|
|
|
const NETWORKS = {
|
|
|
|
|
btc: {
|
|
|
|
|
ticker: "BTC", decimals: 8,
|
|
|
|
|
networks: {
|
|
|
|
|
mainnet: {
|
|
|
|
|
id: "mainnet", label: "Mainnet",
|
|
|
|
|
bitcoinjsNet: bitcoinjs.networks.bitcoin,
|
|
|
|
|
servers: ["wss://electrum.blockstream.info:50004", "wss://fulcrum.sethforprivacy.com:50002"],
|
|
|
|
|
explorerAddr: "https://mempool.space/address/", explorerTx: "https://mempool.space/tx/",
|
|
|
|
|
},
|
|
|
|
|
testnet3: {
|
|
|
|
|
id: "testnet3", label: "Testnet3",
|
|
|
|
|
bitcoinjsNet: bitcoinjs.networks.testnet,
|
|
|
|
|
servers: ["wss://electrumx.tomasi.name:50004"],
|
|
|
|
|
explorerAddr: "https://mempool.space/testnet/address/", explorerTx: "https://mempool.space/testnet/tx/",
|
|
|
|
|
testnet: true, faucet: "https://coinfaucet.eu/en/btc-testnet/",
|
|
|
|
|
},
|
|
|
|
|
signet: {
|
|
|
|
|
id: "signet", label: "Signet",
|
|
|
|
|
bitcoinjsNet: bitcoinjs.networks.testnet,
|
|
|
|
|
servers: ["wss://signet-electrumx.wakiyamap.dev:50004"],
|
|
|
|
|
explorerAddr: "https://mempool.space/signet/address/", explorerTx: "https://mempool.space/signet/tx/",
|
|
|
|
|
testnet: true, faucet: "https://signet.bc-2.jp/",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
dgb: {
|
|
|
|
|
ticker: "DGB", decimals: 8,
|
|
|
|
|
networks: {
|
|
|
|
|
mainnet: {
|
|
|
|
|
id: "mainnet", label: "Mainnet",
|
|
|
|
|
bitcoinjsNet: dgbCore ? dgbCore.digibyte : null,
|
|
|
|
|
servers: ["wss://electrum1.cipig.net:20063", "wss://electrum2.cipig.net:20063"],
|
|
|
|
|
explorerAddr: "https://chainz.cryptoid.info/dgb/address.dws?", explorerTx: "https://chainz.cryptoid.info/dgb/tx.dws?",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class UtxoImportedWallet {
|
|
|
|
|
constructor({ chain, network, address, log = () => {}, onChange = () => {} } = {}) {
|
|
|
|
|
const cfg = NETWORKS[chain]; if (!cfg) throw new Error(`chain-utxo-imported: unknown chain ${chain}`);
|
|
|
|
|
const net = cfg.networks[network]; if (!net) throw new Error(`chain-utxo-imported: ${chain} has no network ${network}`);
|
|
|
|
|
if (!address) throw new Error("address required");
|
|
|
|
|
if (!net.bitcoinjsNet) throw new Error(`chain-utxo-imported: ${chain}/${network} missing bitcoinjs network params`);
|
|
|
|
|
this.chain = chain;
|
|
|
|
|
this.network = network;
|
|
|
|
|
this._cfg = cfg;
|
|
|
|
|
this._net = net;
|
|
|
|
|
this.log = log;
|
|
|
|
|
this.onChange = onChange;
|
|
|
|
|
this._address = address;
|
|
|
|
|
try {
|
|
|
|
|
this._script = bitcoinjs.address.toOutputScript(address, net.bitcoinjsNet);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new Error(`invalid ${chain} address for ${network}: ${e?.message || e}`);
|
|
|
|
|
}
|
|
|
|
|
this._scripthash = scripthashOf(this._script);
|
|
|
|
|
this._client = new electrum.Client(net.servers.slice());
|
|
|
|
|
this._client.onServer = () => this._emit();
|
|
|
|
|
this._state = {
|
|
|
|
|
balance: { confirmed: 0, unconfirmed: 0 },
|
|
|
|
|
history: [],
|
|
|
|
|
height: 0,
|
|
|
|
|
scanning: false,
|
|
|
|
|
error: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setServers(list) { this._client.setServers(list && list.length ? list : this._net.servers.slice()); }
|
|
|
|
|
schedulePoll(ms) {
|
|
|
|
|
clearTimeout(this._pollTimer);
|
|
|
|
|
this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms);
|
|
|
|
|
}
|
|
|
|
|
_emit() { try { this.onChange(); } catch {} }
|
|
|
|
|
|
|
|
|
|
snapshot() {
|
|
|
|
|
return {
|
|
|
|
|
chain: this.chain, network: this.network,
|
|
|
|
|
ticker: this._cfg.ticker, decimals: this._cfg.decimals,
|
|
|
|
|
address: this._address,
|
|
|
|
|
addressIndex: 0,
|
|
|
|
|
addressPath: null,
|
|
|
|
|
balance: this._state.balance,
|
|
|
|
|
history: this._state.history,
|
|
|
|
|
height: this._state.height,
|
|
|
|
|
scanning: this._state.scanning,
|
|
|
|
|
error: this._state.error,
|
|
|
|
|
server: this._client.url || null,
|
|
|
|
|
imported: true,
|
|
|
|
|
explorerAddr: this._net.explorerAddr,
|
|
|
|
|
explorerTx: this._net.explorerTx,
|
|
|
|
|
faucet: this._net.faucet || null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async refresh(full) {
|
|
|
|
|
this._state.scanning = true; this._emit();
|
|
|
|
|
try {
|
chore(aegis): 0.8.2 — sectioned Settings tab
Settings grew tall enough that the user had to scroll past a dozen
cards to reach Prices, Sites or About. Split it into six named
sections (Security, Session, Wallet, Prices, Sites, About) with a
chip nav row at the top; only one section is visible at a time and
the choice persists across restarts.
Adds an About card that names the wallet, the aegis.x front-door
site and the silentmode.st umbrella, so support triage has a
one-click way to reach either from within the panel.
Includes the accumulated 0.7.x-0.8.1 wallet work that was already
shipping on OTA (CashTokens/BCMR, imported-wallet spend, siascan
integration, consolidate, currency picker, footer update chip).
2026-09-22 20:37:28 +02:00
|
|
|
const bal = await this._client.call("blockchain.scripthash.get_balance", [this._scripthash]);
|
2026-09-14 02:30:51 +02:00
|
|
|
this._state.balance = { confirmed: Number(bal?.confirmed || 0), unconfirmed: Number(bal?.unconfirmed || 0) };
|
|
|
|
|
if (full) {
|
chore(aegis): 0.8.2 — sectioned Settings tab
Settings grew tall enough that the user had to scroll past a dozen
cards to reach Prices, Sites or About. Split it into six named
sections (Security, Session, Wallet, Prices, Sites, About) with a
chip nav row at the top; only one section is visible at a time and
the choice persists across restarts.
Adds an About card that names the wallet, the aegis.x front-door
site and the silentmode.st umbrella, so support triage has a
one-click way to reach either from within the panel.
Includes the accumulated 0.7.x-0.8.1 wallet work that was already
shipping on OTA (CashTokens/BCMR, imported-wallet spend, siascan
integration, consolidate, currency picker, footer update chip).
2026-09-22 20:37:28 +02:00
|
|
|
const hist = await this._client.call("blockchain.scripthash.get_history", [this._scripthash]);
|
2026-09-14 02:30:51 +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 }; }
|
|
|
|
|
|
|
|
|
|
plan() { throw new Error(`Imported ${this.chain.toUpperCase()} wallets are read-only in this build.`); }
|
|
|
|
|
signAndBroadcast() { throw new Error("read-only"); }
|
|
|
|
|
signMessage() { throw new Error("read-only"); }
|
|
|
|
|
recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import." }; }
|
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
|
|
|
dispose() { clearTimeout(this._pollTimer); try { this._client.dispose ? this._client.dispose() : this._client.disconnect(); } catch {} }
|
2026-09-14 02:30:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { UtxoImportedWallet, NETWORKS };
|
|
|
|
|
};
|