theseus/bundled-addons/aegis/lib/chain-utxo-imported.js
Local Dev f46e9112b7 chore(theseus): 0.3.47 — plug-in category + panel-driven addon self-update, aegis 0.6.31
Theseus core:
- addons-host: manifest.category ("plugin") propagates through snapshot(); new
  addon API surface checkAndStageSelfUpdate() + restartApp() so a plug-in
  can offer in-panel "update now → restart to apply" without pushing the
  user to Settings.
- main.js: wires the two new hooks into the AddonHost constructor.
- settings.html: Extensions listing filters out category==="plugin"; those
  add-ons live in Plug-ins instead, single source of truth.

Aegis 0.6.31:
- BTC picker trimmed to Signet only; testnet3 hidden (adapter kept so any
  existing wallet still loads).
- Wallet strip groups by chain, not chain:network; ticker gets a ▾ chevron
  and a dropdown listing every subnetwork with its own totals. Mainnet
  reads as the plain ticker; testnets carry a small Chipnet/Signet/Sepolia
  pill inline.
- Per-unit price sits directly under the ticker; amount + fiat mirror on
  the right — one glance covers name/price/holding/value.
- + Add and ⋯ More promoted from the strip into the header's action row,
  next to the new ✎ chip (was the redundant top ⋯). Duplicate "Manage
  current wallet" entry removed from the More menu.
- Footer update chip is a two-step flow via the new API: stage → restart.
  Falls back to opening Settings on any Theseus that lacks the hooks.
- Manifest declares "category": "plugin".
2026-09-14 02:30:51 +02:00

140 lines
5.8 KiB
JavaScript

// 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 {
const bal = await this._client.request("blockchain.scripthash.get_balance", [this._scripthash]);
this._state.balance = { confirmed: Number(bal?.confirmed || 0), unconfirmed: Number(bal?.unconfirmed || 0) };
if (full) {
const hist = await this._client.request("blockchain.scripthash.get_history", [this._scripthash]);
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." }; }
dispose() { clearTimeout(this._pollTimer); try { this._client.disconnect(); } catch {} }
}
return { UtxoImportedWallet, NETWORKS };
};