141 lines
5.8 KiB
JavaScript
141 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 };
|
||
|
|
};
|