fix(theseus/light): light-mode acid → BCH-teal #088A66 (brand-family, AA on white)
Prior light-mode --acid was #3a5c00 (dark olive-green) — legible but off-brand. The Bitcoin Cash brand primary is #0AC18E (a teal-leaning green already used in bchwallet's --bch variable). Darken it a step to #088A66 for AA text contrast on white (~5:1) while staying in the BCH family — the light-mode accent now reads as "Bitcoin Cash green, darkened for legibility" instead of an arbitrary olive. Applied across every chrome page + addon panel that carries the light override (chrome / settings / error / home / approval / messages / bchwallet / siawallet / screenshot editor). Dark mode's #d6ff3d untouched.
This commit is contained in:
commit
9d81c29656
50 changed files with 5212 additions and 140 deletions
|
|
@ -136,7 +136,19 @@ function validateManifest(raw, folderName) {
|
|||
items: cleanItems,
|
||||
};
|
||||
}
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu };
|
||||
// `absorbs`: legacy add-on ids whose vault-derive namespace this add-on
|
||||
// inherits. Set on a superseding add-on (e.g. aegis absorbs siawallet) so
|
||||
// funds derived under the old id's paths stay reachable through the new
|
||||
// one. Each entry is validated as an id itself and gates vault.derive by
|
||||
// (own id OR one of these) in makeApi below.
|
||||
const absorbs = Array.isArray(m.absorbs) ? m.absorbs.map(String).filter(Boolean) : [];
|
||||
for (const a of absorbs) {
|
||||
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(a)) {
|
||||
throw new Error(`addon "${id}": absorbs entry "${a}" is not a valid add-on id`);
|
||||
}
|
||||
if (a === id) throw new Error(`addon "${id}": absorbs cannot list its own id`);
|
||||
}
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, absorbs };
|
||||
}
|
||||
|
||||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||
|
|
@ -377,9 +389,11 @@ class AddonHost {
|
|||
if (this._emitToPanel) this._emitToPanel(manifest.id, String(msg), payload);
|
||||
},
|
||||
// vault-derive: a 32-byte HKDF child of the password vault's root,
|
||||
// keyed by a path that MUST start with this add-on's id so one add-on
|
||||
// can never ask for another's material. Resolves only once the user
|
||||
// has unlocked the vault (main polls; the await can be long).
|
||||
// keyed by a path that MUST start with this add-on's id — or one of
|
||||
// the ids it declared under `absorbs` in addon.json, so a superseding
|
||||
// add-on can keep deriving the same keys as the add-on it replaced
|
||||
// (funds stay reachable across the transition). Resolves only once
|
||||
// the user has unlocked the vault (main polls; the await can be long).
|
||||
vault: {
|
||||
derive: async (purposePath) => {
|
||||
if (!manifest.capabilities.includes("vault-derive")) {
|
||||
|
|
@ -387,9 +401,16 @@ class AddonHost {
|
|||
}
|
||||
if (!this._vaultDerive) throw new Error(`vault.derive unavailable (host not wired)`);
|
||||
const p = String(purposePath || "");
|
||||
if (!p.startsWith(manifest.id + "/") || /[^a-z0-9/._-]/i.test(p) || p.includes("..")) {
|
||||
if (/[^a-z0-9/._-]/i.test(p) || p.includes("..")) {
|
||||
throw new Error(`vault.derive: purposePath must look like "${manifest.id}/<name>"`);
|
||||
}
|
||||
const allowed = [manifest.id, ...(manifest.absorbs || [])];
|
||||
if (!allowed.some((prefix) => p.startsWith(prefix + "/"))) {
|
||||
const list = allowed.length > 1
|
||||
? `one of "${allowed.join('", "')}"`
|
||||
: `"${manifest.id}"`;
|
||||
throw new Error(`vault.derive: purposePath must start with ${list} + "/"`);
|
||||
}
|
||||
return this._vaultDerive(p, manifest.id);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
:root { --surface:#ffffff; --surface2:#f1f4fa; --line:rgba(0,0,0,.12);
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5;
|
||||
/* Darker acid for light backgrounds — ~5.5:1 on white. */
|
||||
--acid: #3a5c00; }
|
||||
--acid: #088A66; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; background: transparent; }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"id": "bchwallet",
|
||||
"name": "Aegis Wallet",
|
||||
"version": "0.2.0",
|
||||
"description": "Multi-chain wallet (BCH, Tron mainnet, Tron Nile testnet) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites and window.tronWeb / window.tronLink on any https page.",
|
||||
"version": "0.3.0",
|
||||
"description": "Multi-chain wallet (BCH mainnet/chipnet, Tron mainnet/Nile, Siacoin, DigiByte) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites and window.tronWeb / window.tronLink on any https page.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "🛡",
|
||||
"main": "index.js",
|
||||
"capabilities": ["sidebar-panel", "vault-derive", "page-inject", "approval-modal"],
|
||||
"absorbs": ["siawallet"],
|
||||
"page-inject": {
|
||||
"preload": "wallet-inject.js",
|
||||
"origins": ["https://*/*"]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,15 +6,42 @@
|
|||
// 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,
|
||||
}) {
|
||||
const NETWORK = "mainnet";
|
||||
const PREFIX = "bitcoincash";
|
||||
const DEFAULT_ACCOUNT_PATH = "m/44'/145'/0'";
|
||||
const EXPLORER_TX = "https://blockchair.com/bitcoin-cash/transaction/";
|
||||
const EXPLORER_ADDR = "https://blockchair.com/bitcoin-cash/address/";
|
||||
|
||||
// storage is the FULL api.storage. keyPrefix scopes every read/write under
|
||||
// "wallets/<walletId>/…" so multiple BCH wallets don't stomp each other.
|
||||
|
|
@ -29,20 +56,27 @@ module.exports = function makeBchAdapter({
|
|||
class BchWallet {
|
||||
constructor(root32, {
|
||||
walletId, storage, log = () => {}, onChange = () => {}, servers,
|
||||
accountPath = DEFAULT_ACCOUNT_PATH,
|
||||
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 = NETWORK;
|
||||
this.network = net.id;
|
||||
this._net = net;
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
||||
this._servers = Array.isArray(servers) && servers.length ? servers : [];
|
||||
this._accountPath = /^m(\/\d+'?)+$/.test(accountPath) ? accountPath : DEFAULT_ACCOUNT_PATH;
|
||||
// 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, PREFIX);
|
||||
this._keys = new keysLib.WalletKeys(root32, this._accountPath, net.prefix);
|
||||
this._root = new Uint8Array(root32);
|
||||
const walletFactory = require("./wallet.js");
|
||||
this._wallet = walletFactory({
|
||||
|
|
@ -54,7 +88,7 @@ module.exports = function makeBchAdapter({
|
|||
}
|
||||
|
||||
setServers(list) {
|
||||
this._servers = Array.isArray(list) && list.length ? list : [];
|
||||
this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice();
|
||||
this._client.setServers(this._servers);
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +98,7 @@ module.exports = function makeBchAdapter({
|
|||
const w = this._wallet.snapshot();
|
||||
return {
|
||||
chain: "bch",
|
||||
network: NETWORK,
|
||||
network: this._net.id,
|
||||
ticker: "BCH",
|
||||
decimals: 8,
|
||||
address: w.address,
|
||||
|
|
@ -79,9 +113,9 @@ module.exports = function makeBchAdapter({
|
|||
servers: this._servers,
|
||||
accountPath: this._accountPath,
|
||||
xpub: this._keys.xpub,
|
||||
explorerTx: EXPLORER_TX,
|
||||
explorerAddr: EXPLORER_ADDR,
|
||||
faucet: null,
|
||||
explorerTx: this._net.explorerTx,
|
||||
explorerAddr: this._net.explorerAddr,
|
||||
faucet: this._net.faucet,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -118,5 +152,5 @@ module.exports = function makeBchAdapter({
|
|||
}
|
||||
}
|
||||
|
||||
return { BchWallet, DEFAULT_ACCOUNT_PATH, PREFIX, EXPLORER_TX, EXPLORER_ADDR };
|
||||
return { BchWallet, BCH_NETWORKS };
|
||||
};
|
||||
|
|
|
|||
531
bundled-addons/bchwallet/lib/chain-btc.js
Normal file
531
bundled-addons/bchwallet/lib/chain-btc.js
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
// Bitcoin (BTC) chain adapter — mainnet + testnet3. BIP84 native SegWit,
|
||||
// bitcoinjs-lib for the tx/PSBT primitives, Aegis's own ElectrumX transport
|
||||
// for the network side. Very close in shape to chain-dgb.js; the two could
|
||||
// share a "bip84-electrum" helper later, but for now a distinct file keeps
|
||||
// the chain-specific tuning (electrum pool, network object) visible.
|
||||
//
|
||||
// Address family: BIP84 only in this rev — bc1q… (mainnet) / tb1q… (testnet).
|
||||
// BIP44 (1…) and BIP49 (3…) are trivially reachable by editing accountPath
|
||||
// to m/44'/0'/0' or m/49'/0'/0' respectively; the PSBT layer already
|
||||
// supports the resulting scripts because bitcoinjs-lib does. An explicit
|
||||
// address-family picker like DGB's is a follow-up.
|
||||
|
||||
const NETWORKS = {
|
||||
mainnet: {
|
||||
id: "mainnet", label: "Mainnet",
|
||||
hrp: "bc", coinType: 0,
|
||||
defaultAccountPath: "m/84'/0'/0'",
|
||||
explorerTx: "https://mempool.space/tx/",
|
||||
explorerAddr: "https://mempool.space/address/",
|
||||
defaultServers: [
|
||||
"wss://electrum.blockstream.info:50004",
|
||||
"wss://bitcoin.lu.ke:50004",
|
||||
"wss://fulcrum.grey.pw:50004",
|
||||
],
|
||||
faucet: null,
|
||||
},
|
||||
testnet: {
|
||||
id: "testnet", label: "Testnet3",
|
||||
hrp: "tb", coinType: 1,
|
||||
defaultAccountPath: "m/84'/1'/0'",
|
||||
explorerTx: "https://mempool.space/testnet/tx/",
|
||||
explorerAddr: "https://mempool.space/testnet/address/",
|
||||
defaultServers: [
|
||||
"wss://testnet.aranguren.org:51004",
|
||||
"wss://blockstream.info:993",
|
||||
],
|
||||
faucet: "https://coinfaucet.eu/en/btc-testnet/",
|
||||
},
|
||||
signet: {
|
||||
// Signet (BIP-325) shares testnet's address format and SLIP-44 coin
|
||||
// type (1), so bitcoinjs-lib's `networks.testnet` handles address
|
||||
// derivation unchanged. The chain itself is a separate, permissioned
|
||||
// testnet with its own genesis + signer-signed blocks; from a wallet's
|
||||
// point of view, the only differences are the electrum pool serving
|
||||
// it and the explorer URL for tx lookups.
|
||||
id: "signet", label: "Signet",
|
||||
hrp: "tb", coinType: 1,
|
||||
defaultAccountPath: "m/84'/1'/0'",
|
||||
explorerTx: "https://mempool.space/signet/tx/",
|
||||
explorerAddr: "https://mempool.space/signet/address/",
|
||||
defaultServers: [
|
||||
"wss://signet.aranguren.org:51102",
|
||||
"wss://signet-electrumx.wakiyamap.dev:50003",
|
||||
],
|
||||
faucet: "https://signetfaucet.com/",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = function makeBtcAdapter({
|
||||
bitcoinjs, bip32Factory, ecpairFactory, ecc, sha256, electrum,
|
||||
}) {
|
||||
if (!bitcoinjs || !bip32Factory || !ecpairFactory || !ecc || !electrum) {
|
||||
throw new Error("chain-btc: missing dep");
|
||||
}
|
||||
const { payments, Psbt, networks: bjsNetworks } = bitcoinjs;
|
||||
const bip32 = bip32Factory(ecc);
|
||||
const ECPair = ecpairFactory(ecc);
|
||||
// Taproot (p2tr) address derivation needs bitcoinjs-lib's schnorr backend
|
||||
// wired to a curve implementation — @bitcoinerlab/secp256k1 provides both
|
||||
// ECDSA and schnorr, so initEccLib once at load makes p2tr resolve.
|
||||
try { bitcoinjs.initEccLib && bitcoinjs.initEccLib(ecc); } catch {}
|
||||
|
||||
// Map our network id → bitcoinjs-lib Network object. Signet shares
|
||||
// testnet's address prefixes + magic (BIP-325 defines only new consensus
|
||||
// rules; the p2p / address layer stays testnet-compatible).
|
||||
function bjsNetworkFor(id) {
|
||||
if (id === "mainnet") return bjsNetworks.bitcoin;
|
||||
if (id === "testnet" || id === "signet") return bjsNetworks.testnet;
|
||||
throw new Error("chain-btc: unknown network " + id);
|
||||
}
|
||||
|
||||
const toHex = (b) => Buffer.from(b).toString("hex");
|
||||
const scripthashOf = (scriptBuf) => Buffer.from(sha256(scriptBuf)).reverse().toString("hex");
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
// Address-family shape from the derivation-path purpose. Every field the
|
||||
// PSBT layer might need for signing an input funded by this family is
|
||||
// captured here so signAndBroadcast has one code path per family.
|
||||
function paymentFor(purpose, node, network) {
|
||||
const pubkey = Buffer.from(node.publicKey);
|
||||
if (purpose === 44) {
|
||||
// Legacy P2PKH. Signing needs the full previous transaction
|
||||
// (nonWitnessUtxo) — one extra electrum call per input at send time.
|
||||
const p = payments.p2pkh({ pubkey, network });
|
||||
return { family: "bip44", address: p.address, output: Buffer.from(p.output), send: "p2pkh", needsPrevTx: true };
|
||||
}
|
||||
if (purpose === 49) {
|
||||
// P2SH-wrapped SegWit. PSBT needs the redeem script (the inner p2wpkh
|
||||
// output) alongside the witnessUtxo.
|
||||
const redeem = payments.p2wpkh({ pubkey, network });
|
||||
const p = payments.p2sh({ redeem, network });
|
||||
return { family: "bip49", address: p.address, output: Buffer.from(p.output), redeem: Buffer.from(redeem.output), send: "p2sh-p2wpkh" };
|
||||
}
|
||||
if (purpose === 86) {
|
||||
// BIP86 Taproot key-path. Signing goes through a tap-tweaked ECPair
|
||||
// (see signerFor + signAndBroadcast); the internal 32-byte x-only
|
||||
// pubkey is captured here so the PSBT input can carry it.
|
||||
const internalPubkey = Buffer.from(pubkey.subarray(1, 33));
|
||||
const p = payments.p2tr({ internalPubkey, network });
|
||||
return { family: "bip86", address: p.address, output: Buffer.from(p.output), internalPubkey, send: "p2tr" };
|
||||
}
|
||||
// Default: BIP84 native SegWit.
|
||||
const p = payments.p2wpkh({ pubkey, network });
|
||||
return { family: "bip84", address: p.address, output: Buffer.from(p.output), send: "p2wpkh" };
|
||||
}
|
||||
function purposeOfPath(accountPath) {
|
||||
const m = /^m\/(\d+)'\//.exec(String(accountPath || ""));
|
||||
return m ? Number(m[1]) : 84;
|
||||
}
|
||||
|
||||
class WalletKeys {
|
||||
constructor(root32, accountPath, bjsNetwork) {
|
||||
this._accountPath = /^m(\/\d+'?)+$/.test(accountPath) ? accountPath : "m/84'/0'/0'";
|
||||
this._purpose = purposeOfPath(this._accountPath);
|
||||
this._network = bjsNetwork;
|
||||
this._root = bip32.fromSeed(Buffer.from(root32), bjsNetwork);
|
||||
this._account = this._root.derivePath(this._accountPath);
|
||||
this._branch = [this._account.derive(0), this._account.derive(1)];
|
||||
this._cache = new Map();
|
||||
}
|
||||
get xpub() { return this._account.neutered().toBase58(); }
|
||||
get xprv() { return this._account.toBase58(); }
|
||||
get accountPath() { return this._accountPath; }
|
||||
get purpose() { return this._purpose; }
|
||||
entry(branch, index) {
|
||||
const k = branch + "/" + index;
|
||||
let e = this._cache.get(k);
|
||||
if (!e) {
|
||||
const node = this._branch[branch].derive(index);
|
||||
const pay = paymentFor(this._purpose, node, this._network);
|
||||
e = {
|
||||
branch, index, path: this._accountPath + "/" + branch + "/" + index,
|
||||
publicKey: Buffer.from(node.publicKey),
|
||||
family: pay.family, sendKind: pay.send,
|
||||
script: pay.output, scriptHex: pay.output.toString("hex"),
|
||||
scripthash: scripthashOf(pay.output),
|
||||
address: pay.address,
|
||||
redeemScript: pay.redeem || null,
|
||||
tapInternalKey: pay.internalPubkey || null,
|
||||
_node: node,
|
||||
};
|
||||
this._cache.set(k, e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
signerFor(entry) {
|
||||
return ECPair.fromPrivateKey(Buffer.from(entry._node.privateKey), { network: this._network });
|
||||
}
|
||||
wipe() {
|
||||
for (const e of this._cache.values()) e._node = null;
|
||||
this._cache.clear();
|
||||
this._branch = null;
|
||||
this._account = null;
|
||||
this._root = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fee vsize model per family. Values are rounded vsize contributions from
|
||||
// standard tx-size tables; the estimator is pessimistic enough to cover a
|
||||
// real broadcast without underpaying.
|
||||
const OVERHEAD_VB = 10.5;
|
||||
const OUTPUT_VB = 31; // P2WPKH / P2SH / P2PKH outputs are all ~31 vB give or take
|
||||
const INPUT_VB = {
|
||||
p2pkh: 148, // (32+4)+1+107+4 legacy input
|
||||
"p2sh-p2wpkh": 91, // 40 base + ~205/4 witness
|
||||
p2wpkh: 68, // 41 base + 108/4 witness
|
||||
p2tr: 58, // 41 base + 66/4 witness (key-path)
|
||||
};
|
||||
const feeVb = (kind, nIn, nOut, feePerVb) => Math.ceil((OVERHEAD_VB + nIn * (INPUT_VB[kind] || 68) + nOut * OUTPUT_VB) * feePerVb);
|
||||
|
||||
class BtcWallet {
|
||||
constructor(root32, networkId, {
|
||||
walletId, storage, log = () => {}, onChange = () => {}, servers,
|
||||
accountPath,
|
||||
} = {}) {
|
||||
if (!walletId) throw new Error("chain-btc: walletId required");
|
||||
const net = NETWORKS[networkId];
|
||||
if (!net) throw new Error("chain-btc: unknown network " + networkId);
|
||||
this.walletId = walletId;
|
||||
this.chain = "btc";
|
||||
this.network = net.id;
|
||||
this._net = net;
|
||||
this._bjsNet = bjsNetworkFor(net.id);
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
||||
this._servers = Array.isArray(servers) && servers.length ? servers : net.defaultServers.slice();
|
||||
const wantPath = accountPath || net.defaultAccountPath;
|
||||
this._keys = new WalletKeys(root32, wantPath, this._bjsNet);
|
||||
this._root = new Uint8Array(root32);
|
||||
this._client = new electrum.Client(this._servers);
|
||||
this._client.onServer = () => this._emit();
|
||||
this._state = {
|
||||
used: new Set(),
|
||||
watched: new Map(),
|
||||
height: 0,
|
||||
balance: { confirmed: 0, unconfirmed: 0 },
|
||||
utxos: [],
|
||||
history: [],
|
||||
receiveIndex: 0,
|
||||
scanning: false,
|
||||
error: null,
|
||||
};
|
||||
this._refreshTimer = null;
|
||||
this._subscribedHeaders = false;
|
||||
this._client.onNotify = (method, params) => {
|
||||
if (method === "blockchain.headers.subscribe") {
|
||||
const h = params && params[0] && params[0].height;
|
||||
if (h) { this._state.height = h; this._scheduleRefresh(1500); }
|
||||
} else if (method === "blockchain.scripthash.subscribe") {
|
||||
this._scheduleRefresh(800);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_emit() { try { this.onChange(); } catch {} }
|
||||
|
||||
async _historyOf(entry) {
|
||||
const h = await this._client.call("blockchain.scripthash.get_history", [entry.scripthash]);
|
||||
return Array.isArray(h) ? h : [];
|
||||
}
|
||||
async _scan() {
|
||||
const cursor = Number(this.storage.get("receiveCursor", 0)) || 0;
|
||||
const GAP = 20;
|
||||
for (const branch of [0, 1]) {
|
||||
let gap = 0, i = 0;
|
||||
const minIndex = branch === 0 ? cursor + 1 : 0;
|
||||
while (gap < GAP || i < minIndex + GAP) {
|
||||
const batch = [];
|
||||
for (let k = 0; k < 10; k++) batch.push(this._keys.entry(branch, i + k));
|
||||
const results = await Promise.all(batch.map((e) => this._historyOf(e)));
|
||||
for (let k = 0; k < batch.length; k++) {
|
||||
const e = batch[k];
|
||||
this._state.watched.set(e.scripthash, e);
|
||||
if (results[k].length) { this._state.used.add(branch + "/" + e.index); gap = 0; } else gap++;
|
||||
i++;
|
||||
if (gap >= GAP && i >= minIndex + GAP) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let r = cursor;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this._state.receiveIndex = r;
|
||||
this._state.watched.set(this._keys.entry(0, r).scripthash, this._keys.entry(0, r));
|
||||
}
|
||||
async _subscribeAll() {
|
||||
if (!this._subscribedHeaders) {
|
||||
this._subscribedHeaders = true;
|
||||
const tip = await this._client.subscribe("blockchain.headers.subscribe", []);
|
||||
if (tip && tip.height) this._state.height = tip.height;
|
||||
}
|
||||
await Promise.all([...this._state.watched.values()].map((e) =>
|
||||
this._client.subscribe("blockchain.scripthash.subscribe", [e.scripthash]).catch(() => {})));
|
||||
}
|
||||
async _loadUtxos() {
|
||||
const lists = await Promise.all([...this._state.watched.values()].map(async (e) => {
|
||||
const u = await this._client.call("blockchain.scripthash.listunspent", [e.scripthash]);
|
||||
return (Array.isArray(u) ? u : []).map((x) => ({ txid: x.tx_hash, vout: x.tx_pos, value: x.value, height: x.height, entry: e }));
|
||||
}));
|
||||
this._state.utxos = lists.flat();
|
||||
let confirmed = 0, unconfirmed = 0;
|
||||
for (const u of this._state.utxos) { if (u.height > 0) confirmed += u.value; else unconfirmed += u.value; }
|
||||
this._state.balance = { confirmed, unconfirmed };
|
||||
}
|
||||
async _loadHistory() {
|
||||
const entries = [...this._state.watched.values()].filter((e) => this._state.used.has(e.branch + "/" + e.index));
|
||||
const merged = new Map();
|
||||
const lists = await Promise.all(entries.map((e) => this._historyOf(e)));
|
||||
for (const list of lists) for (const h of list) {
|
||||
const prev = merged.get(h.tx_hash);
|
||||
if (!prev || (h.height > 0 && prev.height <= 0)) merged.set(h.tx_hash, { txid: h.tx_hash, height: h.height });
|
||||
}
|
||||
const ordered = [...merged.values()].sort((a, b) => {
|
||||
const ha = a.height > 0 ? a.height : Infinity, hb = b.height > 0 ? b.height : Infinity;
|
||||
return hb - ha;
|
||||
}).slice(0, 25);
|
||||
const ours = new Set([...this._state.watched.values()].map((e) => e.scriptHex));
|
||||
const out = [];
|
||||
for (const h of ordered) {
|
||||
let received = 0, spent = 0;
|
||||
try {
|
||||
const t = await this._client.call("blockchain.transaction.get", [h.txid, true]);
|
||||
for (const o of t.vout || []) {
|
||||
const hex = o.scriptPubKey && o.scriptPubKey.hex;
|
||||
if (hex && ours.has(hex)) received += Math.round(Number(o.value || 0) * 1e8);
|
||||
}
|
||||
for (const i of t.vin || []) {
|
||||
if (!i.txid) continue;
|
||||
try {
|
||||
const p = await this._client.call("blockchain.transaction.get", [i.txid, true]);
|
||||
const po = p.vout && p.vout[i.vout];
|
||||
const hex = po && po.scriptPubKey && po.scriptPubKey.hex;
|
||||
if (hex && ours.has(hex)) spent += Math.round(Number(po.value || 0) * 1e8);
|
||||
} catch {}
|
||||
}
|
||||
out.push({
|
||||
txid: h.txid, height: h.height, confirmations: t.confirmations || 0,
|
||||
time: t.blocktime || t.time || 0,
|
||||
delta: received - spent, fee: null, to: null,
|
||||
status: (t.confirmations || 0) > 0 ? "confirmed" : "pending",
|
||||
kind: "transfer",
|
||||
});
|
||||
} catch {
|
||||
out.push({ txid: h.txid, height: h.height, confirmations: 0, time: 0, delta: 0, fee: null, to: null, status: "pending", kind: "transfer" });
|
||||
}
|
||||
}
|
||||
this._state.history = out;
|
||||
}
|
||||
async refresh(full = false) {
|
||||
if (this._state.scanning) return;
|
||||
this._state.scanning = true; this._state.error = null; this._emit();
|
||||
try {
|
||||
if (full || !this._state.watched.size) await this._scan();
|
||||
else {
|
||||
let r = Number(this.storage.get("receiveCursor", 0)) || 0;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this._state.receiveIndex = r;
|
||||
this._state.watched.set(this._keys.entry(0, r).scripthash, this._keys.entry(0, r));
|
||||
}
|
||||
await this._loadUtxos();
|
||||
await this._loadHistory();
|
||||
await this._subscribeAll();
|
||||
for (const u of this._state.utxos) this._state.used.add(u.entry.branch + "/" + u.entry.index);
|
||||
let r = Number(this.storage.get("receiveCursor", 0)) || 0;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this._state.receiveIndex = r;
|
||||
} catch (e) {
|
||||
this._state.error = e?.message || String(e);
|
||||
this.log("refresh failed:", this._state.error);
|
||||
} finally {
|
||||
this._state.scanning = false;
|
||||
this._emit();
|
||||
}
|
||||
}
|
||||
_scheduleRefresh(ms = 800) {
|
||||
clearTimeout(this._refreshTimer);
|
||||
this._refreshTimer = setTimeout(() => this.refresh(false), ms);
|
||||
}
|
||||
|
||||
current() { return this._keys.entry(0, this._state.receiveIndex); }
|
||||
nextAddress() {
|
||||
let r = this._state.receiveIndex + 1;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this.storage.set("receiveCursor", r);
|
||||
this._state.receiveIndex = r;
|
||||
const e = this._keys.entry(0, r);
|
||||
this._state.watched.set(e.scripthash, e);
|
||||
this._client.subscribe("blockchain.scripthash.subscribe", [e.scripthash]).catch(() => {});
|
||||
this._emit();
|
||||
return this.current();
|
||||
}
|
||||
_changeEntry() {
|
||||
let i = 0;
|
||||
while (this._state.used.has("1/" + i)) i++;
|
||||
return this._keys.entry(1, i);
|
||||
}
|
||||
|
||||
setServers(list) {
|
||||
this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice();
|
||||
this._client.setServers(this._servers);
|
||||
}
|
||||
|
||||
plan({ to, amount, feeRate = 5, sendMax = false }) {
|
||||
const rate = Math.min(500, Math.max(1, Number(feeRate) || 5));
|
||||
const dest = String(to || "");
|
||||
try { bitcoinjs.address.toOutputScript(dest, this._bjsNet); }
|
||||
catch (e) { throw new Error(`bad Bitcoin address: ${e?.message || dest}`); }
|
||||
const cur = this.current();
|
||||
if (!cur.sendKind) throw new Error(`no sender for ${cur.family} — registry bug`);
|
||||
const spendable = this._state.utxos.slice().sort((a, b) => (b.height > 0) - (a.height > 0));
|
||||
const change = this._changeEntry();
|
||||
const kind = cur.sendKind;
|
||||
if (sendMax) {
|
||||
const chosen = spendable;
|
||||
const sum = chosen.reduce((a, u) => a + u.value, 0);
|
||||
const fee = feeVb(kind, chosen.length, 1, rate);
|
||||
if (sum <= fee) throw new Error("balance does not cover the fee");
|
||||
return {
|
||||
_chosen: chosen, _rate: rate, _to: dest, _sendMax: true, _change: change, _kind: kind,
|
||||
recipients: [{ to: dest, value: sum - fee }],
|
||||
fee, feeRate: rate, change: 0,
|
||||
total: sum,
|
||||
};
|
||||
}
|
||||
const value = Math.round(Number(amount) || 0);
|
||||
if (!(value > 0)) throw new Error("amount must be > 0");
|
||||
let sum = 0; const chosen = [];
|
||||
for (const u of spendable) {
|
||||
chosen.push(u); sum += u.value;
|
||||
const withChange = feeVb(kind, chosen.length, 2, rate);
|
||||
if (sum >= value + withChange) {
|
||||
const changeVal = sum - value - withChange;
|
||||
const fee = changeVal > 546 ? withChange : sum - value;
|
||||
return {
|
||||
_chosen: chosen, _rate: rate, _to: dest, _value: value,
|
||||
_change: change, _changeVal: changeVal > 546 ? changeVal : 0, _kind: kind,
|
||||
recipients: [{ to: dest, value }],
|
||||
fee, feeRate: rate,
|
||||
change: changeVal > 546 ? changeVal : 0,
|
||||
total: value + fee,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error("insufficient funds");
|
||||
}
|
||||
|
||||
async signAndBroadcast(plan) {
|
||||
// BIP44 inputs need the whole previous transaction (nonWitnessUtxo)
|
||||
// so PSBT can compute a legacy sighash; fetch each one in parallel
|
||||
// before assembling the PSBT.
|
||||
const needsPrev = plan._chosen.filter((u) => u.entry.family === "bip44");
|
||||
const prevHex = new Map();
|
||||
if (needsPrev.length) {
|
||||
const results = await Promise.all(needsPrev.map((u) =>
|
||||
this._client.call("blockchain.transaction.get", [u.txid, false])
|
||||
));
|
||||
needsPrev.forEach((u, i) => prevHex.set(u.txid, String(results[i])));
|
||||
}
|
||||
const psbt = new Psbt({ network: this._bjsNet });
|
||||
for (const u of plan._chosen) {
|
||||
const inp = { hash: u.txid, index: u.vout };
|
||||
const fam = u.entry.family;
|
||||
if (fam === "bip44") {
|
||||
inp.nonWitnessUtxo = Buffer.from(prevHex.get(u.txid), "hex");
|
||||
} else {
|
||||
inp.witnessUtxo = { script: u.entry.script, value: u.value };
|
||||
if (fam === "bip49" && u.entry.redeemScript) inp.redeemScript = u.entry.redeemScript;
|
||||
if (fam === "bip86" && u.entry.tapInternalKey) inp.tapInternalKey = u.entry.tapInternalKey;
|
||||
}
|
||||
psbt.addInput(inp);
|
||||
}
|
||||
const outputs = [{ address: plan._to, value: plan._sendMax ? plan.recipients[0].value : plan._value }];
|
||||
if (!plan._sendMax && plan._changeVal > 0) {
|
||||
outputs.push({ address: plan._change.address, value: plan._changeVal });
|
||||
}
|
||||
for (const o of outputs) psbt.addOutput(o);
|
||||
for (let i = 0; i < plan._chosen.length; i++) {
|
||||
const entry = plan._chosen[i].entry;
|
||||
// Taproot key-path: bitcoinjs-lib matches the signer's publicKey
|
||||
// against the tweaked output key, so the signer has to be the
|
||||
// internal ECPair tweaked with sha256("TapTweak" || internalPubkey).
|
||||
// ECPair.tweak() from the ecpair package does exactly that (its
|
||||
// internal state becomes the tap-tweaked keypair) and its
|
||||
// signSchnorr is what PSBT calls for a key-path spend.
|
||||
if (entry.family === "bip86") {
|
||||
const raw = ECPair.fromPrivateKey(Buffer.from(entry._node.privateKey), { network: this._bjsNet });
|
||||
const tweak = bitcoinjs.crypto.taggedHash("TapTweak", entry.tapInternalKey);
|
||||
const tweaked = raw.tweak(tweak);
|
||||
psbt.signInput(i, tweaked);
|
||||
} else {
|
||||
psbt.signInput(i, this._keys.signerFor(entry));
|
||||
}
|
||||
}
|
||||
psbt.finalizeAllInputs();
|
||||
const tx = psbt.extractTransaction();
|
||||
const hex = tx.toHex();
|
||||
const txid = await this._client.call("blockchain.transaction.broadcast", [hex]);
|
||||
if (typeof txid !== "string" || txid.length !== 64) throw new Error("broadcast rejected: " + JSON.stringify(txid));
|
||||
this.log("broadcast", txid);
|
||||
this._scheduleRefresh(1200);
|
||||
return { txid, hex, fee: plan.fee };
|
||||
}
|
||||
|
||||
// BIP-137 recoverable over sha256d("Bitcoin Signed Message:\n" || msg).
|
||||
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 signer = this._keys.signerFor(entry);
|
||||
const sig = ecc.signRecoverable(Buffer.from(digest), signer.privateKey);
|
||||
const out = Buffer.alloc(65);
|
||||
out[0] = 27 + sig.recoveryId + 4; // +4 = compressed
|
||||
Buffer.from(sig.signature).copy(out, 1);
|
||||
return { address: entry.address, signature: out.toString("base64") };
|
||||
}
|
||||
|
||||
recovery() {
|
||||
return { accountPath: this._keys.accountPath, xpub: this._keys.xpub, xprv: this._keys.xprv };
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
const cur = this.current();
|
||||
return {
|
||||
chain: "btc", network: this._net.id, ticker: "BTC", decimals: 8,
|
||||
address: cur.address, addressIndex: this._state.receiveIndex,
|
||||
addressPath: cur.path,
|
||||
balance: this._state.balance,
|
||||
height: this._state.height,
|
||||
history: this._state.history,
|
||||
scanning: this._state.scanning,
|
||||
error: this._state.error,
|
||||
server: this._client.url || null,
|
||||
servers: this._servers,
|
||||
accountPath: this._keys.accountPath,
|
||||
xpub: this._keys.xpub,
|
||||
explorerTx: this._net.explorerTx,
|
||||
explorerAddr: this._net.explorerAddr,
|
||||
faucet: this._net.faucet,
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
clearTimeout(this._refreshTimer);
|
||||
try { this._keys.wipe(); } catch {}
|
||||
try { this._client.disconnect(); } catch {}
|
||||
if (this._root) this._root.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
return { BtcWallet, NETWORKS };
|
||||
};
|
||||
451
bundled-addons/bchwallet/lib/chain-dgb.js
Normal file
451
bundled-addons/bchwallet/lib/chain-dgb.js
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
// DigiByte (DGB) chain adapter — thin bridge over the @dgb-wallet/* packages
|
||||
// vendored under lib/dgb/ from D:\Dev\SilentCode\Digibyte\packages\{core,psbt}.
|
||||
// Address derivation, network params and PSBT construction come from the
|
||||
// upstream design; Aegis provides the runtime shell (ElectrumX transport,
|
||||
// gap-limit scanning, wallet-manager plumbing).
|
||||
//
|
||||
// Backend: DGB ElectrumX pool via Theseus's existing lib/electrum.js — same
|
||||
// TCP-over-wss stack the BCH wallet uses, no separate protocol adapter.
|
||||
// Optional Blockbook mode is on the roadmap; ElectrumX is the default
|
||||
// because it matches Aegis's transport shape and needs no per-server keys.
|
||||
//
|
||||
// Only BIP84 (m/84'/20'/0'/0/x → dgb1q…) is exposed in this rev; the
|
||||
// vendored core also supports BIP44 (D…) and BIP49 (S…) — plumb them by
|
||||
// switching the purpose passed to accountNode(). Address recovery from any
|
||||
// BIP39 tool at coin type 20 is guaranteed by bitcoinjs-lib's Network
|
||||
// object, so a seed exported here can be restored on iancoleman.io/bip39
|
||||
// or the SilentCode Digibyte web-wallet with matching addresses.
|
||||
|
||||
const NETWORK = "mainnet";
|
||||
const DEFAULT_PURPOSE = 84;
|
||||
const EXPLORER_TX = "https://digiexplorer.info/tx/";
|
||||
const EXPLORER_ADDR = "https://digiexplorer.info/address/";
|
||||
const DEFAULT_SERVERS = [
|
||||
"wss://electrum1.cyberbits.eu:50022",
|
||||
"wss://electrum3.cyberbits.eu:50022",
|
||||
"wss://electrum1.digibyteblockexplorer.com:50022",
|
||||
];
|
||||
|
||||
module.exports = function makeDgbAdapter({
|
||||
dgbCore, // ESM namespace of @dgb-wallet/core (vendored)
|
||||
dgbPsbt, // ESM namespace of @dgb-wallet/psbt (vendored)
|
||||
bitcoinjs, // require("bitcoinjs-lib")
|
||||
bip32Factory, // require("bip32").BIP32Factory
|
||||
ecpairFactory, // require("ecpair").ECPairFactory
|
||||
ecc, // require("@bitcoinerlab/secp256k1")
|
||||
sha256, // @noble/hashes/sha2 (only used for scripthash reversal)
|
||||
electrum,
|
||||
}) {
|
||||
if (!dgbCore || !dgbPsbt || !bitcoinjs || !bip32Factory || !ecpairFactory || !ecc || !electrum) {
|
||||
throw new Error("chain-dgb: missing dep");
|
||||
}
|
||||
const { rootFromSeed, accountNode, addressNode, p2wpkhAddress, digibyte, DGB_COIN_TYPE } = dgbCore;
|
||||
const { buildPsbt, signAllInputs, finalizeAndExtract, feeSats } = dgbPsbt;
|
||||
const { payments, Psbt } = bitcoinjs;
|
||||
const bip32 = bip32Factory(ecc);
|
||||
const ECPair = ecpairFactory(ecc);
|
||||
|
||||
const toHex = (b) => Buffer.from(b).toString("hex");
|
||||
// Electrum scripthash: sha256(scriptPubKey), byte-reversed, hex.
|
||||
function scripthashOf(scriptBuf) {
|
||||
const h = sha256(scriptBuf);
|
||||
const rev = Buffer.from(h).reverse();
|
||||
return rev.toString("hex");
|
||||
}
|
||||
function scriptPubKeyBuf(pubkeyBuf) {
|
||||
return payments.p2wpkh({ pubkey: pubkeyBuf, network: digibyte }).output;
|
||||
}
|
||||
|
||||
// ---- keys --------------------------------------------------------------
|
||||
// The HD node is the sole owner of the private key material; every derived
|
||||
// entry keeps a reference so PSBT.signInput(index, node) can sign each
|
||||
// input under its own key.
|
||||
class WalletKeys {
|
||||
constructor(root32, accountPath) {
|
||||
const purpose = parsePurposeFromPath(accountPath) || DEFAULT_PURPOSE;
|
||||
this._purpose = purpose;
|
||||
// bip32.fromSeed uses bitcoinjs-lib's Network object — pass DGB's so
|
||||
// extended keys serialize with the right BIP32 magic (0x0488B21E).
|
||||
this._root = bip32.fromSeed(Buffer.from(root32), digibyte);
|
||||
this._account = this._root.derivePath(`m/${purpose}'/${DGB_COIN_TYPE}'/0'`);
|
||||
this._accountPath = `m/${purpose}'/${DGB_COIN_TYPE}'/0'`;
|
||||
this._branch = [this._account.derive(0), this._account.derive(1)];
|
||||
this._cache = new Map();
|
||||
}
|
||||
get xpub() { return this._account.neutered().toBase58(); }
|
||||
get xprv() { return this._account.toBase58(); }
|
||||
get accountPath() { return this._accountPath; }
|
||||
entry(branch, index) {
|
||||
const k = branch + "/" + index;
|
||||
let e = this._cache.get(k);
|
||||
if (!e) {
|
||||
const node = this._branch[branch].derive(index);
|
||||
const pubkey = Buffer.from(node.publicKey);
|
||||
const address = p2wpkhAddress(node, digibyte);
|
||||
const script = Buffer.from(scriptPubKeyBuf(pubkey));
|
||||
e = {
|
||||
branch, index, path: this._accountPath + "/" + branch + "/" + index,
|
||||
publicKey: pubkey, script, scriptHex: script.toString("hex"),
|
||||
scripthash: scripthashOf(script),
|
||||
address,
|
||||
_node: node,
|
||||
};
|
||||
this._cache.set(k, e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
signerFor(entry) {
|
||||
// bitcoinjs-lib's PSBT accepts anything with .publicKey + .sign(hash).
|
||||
// BIP32Interface fits, but ECPair.fromPrivateKey gives a plain signer
|
||||
// that matches what the DGB web-wallet uses — pick that for parity.
|
||||
return ECPair.fromPrivateKey(Buffer.from(entry._node.privateKey), { network: digibyte });
|
||||
}
|
||||
wipe() {
|
||||
// BIP32Interface holds Buffers; drop references so GC picks them up.
|
||||
for (const e of this._cache.values()) e._node = null;
|
||||
this._cache.clear();
|
||||
this._branch = null;
|
||||
this._account = null;
|
||||
this._root = null;
|
||||
}
|
||||
}
|
||||
function parsePurposeFromPath(p) {
|
||||
const m = /^m\/(\d+)'\/\d+'\/\d+'$/.exec(String(p || ""));
|
||||
return m ? Number(m[1]) : null;
|
||||
}
|
||||
|
||||
// ---- vsize model (fee estimation ahead of PSBT.getFee) -----------------
|
||||
const OVERHEAD_VB = 10.5;
|
||||
const P2WPKH_INPUT_VB = 68;
|
||||
const P2WPKH_OUTPUT_VB = 31;
|
||||
const feeVb = (nIn, nOut, feePerVb) => Math.ceil((OVERHEAD_VB + nIn * P2WPKH_INPUT_VB + nOut * P2WPKH_OUTPUT_VB) * feePerVb);
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- wallet ------------------------------------------------------------
|
||||
class DgbWallet {
|
||||
constructor(root32, {
|
||||
walletId, storage, log = () => {}, onChange = () => {}, servers,
|
||||
accountPath,
|
||||
} = {}) {
|
||||
if (!walletId) throw new Error("chain-dgb: walletId required");
|
||||
this.walletId = walletId;
|
||||
this.chain = "dgb";
|
||||
this.network = NETWORK;
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
||||
this._servers = Array.isArray(servers) && servers.length ? servers : DEFAULT_SERVERS.slice();
|
||||
this._keys = new WalletKeys(root32, accountPath);
|
||||
this._root = new Uint8Array(root32);
|
||||
this._client = new electrum.Client(this._servers);
|
||||
this._client.onServer = () => this._emit();
|
||||
this._state = {
|
||||
used: new Set(),
|
||||
watched: new Map(),
|
||||
height: 0,
|
||||
balance: { confirmed: 0, unconfirmed: 0 },
|
||||
utxos: [],
|
||||
history: [],
|
||||
receiveIndex: 0,
|
||||
scanning: false,
|
||||
error: null,
|
||||
};
|
||||
this._refreshTimer = null;
|
||||
this._subscribedHeaders = false;
|
||||
this._client.onNotify = (method, params) => {
|
||||
if (method === "blockchain.headers.subscribe") {
|
||||
const h = params && params[0] && params[0].height;
|
||||
if (h) { this._state.height = h; this._scheduleRefresh(1500); }
|
||||
} else if (method === "blockchain.scripthash.subscribe") {
|
||||
this._scheduleRefresh(800);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_emit() { try { this.onChange(); } catch {} }
|
||||
|
||||
// ---- discovery (gap-limit) -----------------------------------------
|
||||
async _historyOf(entry) {
|
||||
const h = await this._client.call("blockchain.scripthash.get_history", [entry.scripthash]);
|
||||
return Array.isArray(h) ? h : [];
|
||||
}
|
||||
async _scan() {
|
||||
const cursor = Number(this.storage.get("receiveCursor", 0)) || 0;
|
||||
const GAP = 20;
|
||||
for (const branch of [0, 1]) {
|
||||
let gap = 0, i = 0;
|
||||
const minIndex = branch === 0 ? cursor + 1 : 0;
|
||||
while (gap < GAP || i < minIndex + GAP) {
|
||||
const batch = [];
|
||||
for (let k = 0; k < 10; k++) batch.push(this._keys.entry(branch, i + k));
|
||||
const results = await Promise.all(batch.map((e) => this._historyOf(e)));
|
||||
for (let k = 0; k < batch.length; k++) {
|
||||
const e = batch[k];
|
||||
this._state.watched.set(e.scripthash, e);
|
||||
if (results[k].length) { this._state.used.add(branch + "/" + e.index); gap = 0; } else gap++;
|
||||
i++;
|
||||
if (gap >= GAP && i >= minIndex + GAP) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let r = cursor;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this._state.receiveIndex = r;
|
||||
this._state.watched.set(this._keys.entry(0, r).scripthash, this._keys.entry(0, r));
|
||||
}
|
||||
async _subscribeAll() {
|
||||
if (!this._subscribedHeaders) {
|
||||
this._subscribedHeaders = true;
|
||||
const tip = await this._client.subscribe("blockchain.headers.subscribe", []);
|
||||
if (tip && tip.height) this._state.height = tip.height;
|
||||
}
|
||||
await Promise.all([...this._state.watched.values()].map((e) =>
|
||||
this._client.subscribe("blockchain.scripthash.subscribe", [e.scripthash]).catch(() => {})));
|
||||
}
|
||||
async _loadUtxos() {
|
||||
const lists = await Promise.all([...this._state.watched.values()].map(async (e) => {
|
||||
const u = await this._client.call("blockchain.scripthash.listunspent", [e.scripthash]);
|
||||
return (Array.isArray(u) ? u : []).map((x) => ({ txid: x.tx_hash, vout: x.tx_pos, value: x.value, height: x.height, entry: e }));
|
||||
}));
|
||||
this._state.utxos = lists.flat();
|
||||
let confirmed = 0, unconfirmed = 0;
|
||||
for (const u of this._state.utxos) { if (u.height > 0) confirmed += u.value; else unconfirmed += u.value; }
|
||||
this._state.balance = { confirmed, unconfirmed };
|
||||
}
|
||||
async _loadHistory() {
|
||||
// Best-effort tx-history summary: pull the transactions listed against
|
||||
// any used scripthash, sum ours-vs-not to get a delta per tx. Fine on
|
||||
// the DGB electrum pool (get_transaction verbose is supported).
|
||||
const entries = [...this._state.watched.values()].filter((e) => this._state.used.has(e.branch + "/" + e.index));
|
||||
const merged = new Map();
|
||||
const lists = await Promise.all(entries.map((e) => this._historyOf(e)));
|
||||
for (const list of lists) for (const h of list) {
|
||||
const prev = merged.get(h.tx_hash);
|
||||
if (!prev || (h.height > 0 && prev.height <= 0)) merged.set(h.tx_hash, { txid: h.tx_hash, height: h.height });
|
||||
}
|
||||
const ordered = [...merged.values()].sort((a, b) => {
|
||||
const ha = a.height > 0 ? a.height : Infinity, hb = b.height > 0 ? b.height : Infinity;
|
||||
return hb - ha;
|
||||
}).slice(0, 25);
|
||||
const ours = new Set([...this._state.watched.values()].map((e) => e.scriptHex));
|
||||
const out = [];
|
||||
for (const h of ordered) {
|
||||
let received = 0, spent = 0;
|
||||
try {
|
||||
const t = await this._client.call("blockchain.transaction.get", [h.txid, true]);
|
||||
for (const o of t.vout || []) {
|
||||
const hex = o.scriptPubKey && o.scriptPubKey.hex;
|
||||
if (hex && ours.has(hex)) received += Math.round(Number(o.value || 0) * 1e8);
|
||||
}
|
||||
for (const i of t.vin || []) {
|
||||
if (!i.txid) continue;
|
||||
try {
|
||||
const p = await this._client.call("blockchain.transaction.get", [i.txid, true]);
|
||||
const po = p.vout && p.vout[i.vout];
|
||||
const hex = po && po.scriptPubKey && po.scriptPubKey.hex;
|
||||
if (hex && ours.has(hex)) spent += Math.round(Number(po.value || 0) * 1e8);
|
||||
} catch {}
|
||||
}
|
||||
out.push({
|
||||
txid: h.txid, height: h.height, confirmations: t.confirmations || 0,
|
||||
time: t.blocktime || t.time || 0,
|
||||
delta: received - spent, fee: null, to: null,
|
||||
status: (t.confirmations || 0) > 0 ? "confirmed" : "pending",
|
||||
kind: "transfer",
|
||||
});
|
||||
} catch {
|
||||
out.push({ txid: h.txid, height: h.height, confirmations: 0, time: 0, delta: 0, fee: null, to: null, status: "pending", kind: "transfer" });
|
||||
}
|
||||
}
|
||||
this._state.history = out;
|
||||
}
|
||||
async refresh(full = false) {
|
||||
if (this._state.scanning) return;
|
||||
this._state.scanning = true; this._state.error = null; this._emit();
|
||||
try {
|
||||
if (full || !this._state.watched.size) await this._scan();
|
||||
else {
|
||||
let r = Number(this.storage.get("receiveCursor", 0)) || 0;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this._state.receiveIndex = r;
|
||||
this._state.watched.set(this._keys.entry(0, r).scripthash, this._keys.entry(0, r));
|
||||
}
|
||||
await this._loadUtxos();
|
||||
await this._loadHistory();
|
||||
await this._subscribeAll();
|
||||
for (const u of this._state.utxos) this._state.used.add(u.entry.branch + "/" + u.entry.index);
|
||||
let r = Number(this.storage.get("receiveCursor", 0)) || 0;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this._state.receiveIndex = r;
|
||||
} catch (e) {
|
||||
this._state.error = e?.message || String(e);
|
||||
this.log("refresh failed:", this._state.error);
|
||||
} finally {
|
||||
this._state.scanning = false;
|
||||
this._emit();
|
||||
}
|
||||
}
|
||||
_scheduleRefresh(ms = 800) {
|
||||
clearTimeout(this._refreshTimer);
|
||||
this._refreshTimer = setTimeout(() => this.refresh(false), ms);
|
||||
}
|
||||
|
||||
current() { return this._keys.entry(0, this._state.receiveIndex); }
|
||||
nextAddress() {
|
||||
let r = this._state.receiveIndex + 1;
|
||||
while (this._state.used.has("0/" + r)) r++;
|
||||
this.storage.set("receiveCursor", r);
|
||||
this._state.receiveIndex = r;
|
||||
const e = this._keys.entry(0, r);
|
||||
this._state.watched.set(e.scripthash, e);
|
||||
this._client.subscribe("blockchain.scripthash.subscribe", [e.scripthash]).catch(() => {});
|
||||
this._emit();
|
||||
return this.current();
|
||||
}
|
||||
_changeEntry() {
|
||||
let i = 0;
|
||||
while (this._state.used.has("1/" + i)) i++;
|
||||
return this._keys.entry(1, i);
|
||||
}
|
||||
|
||||
setServers(list) {
|
||||
this._servers = Array.isArray(list) && list.length ? list : DEFAULT_SERVERS.slice();
|
||||
this._client.setServers(this._servers);
|
||||
}
|
||||
|
||||
// ---- plan + sign (via @dgb-wallet/psbt) -----------------------------
|
||||
plan({ to, amount, feeRate = 20, sendMax = false }) {
|
||||
const rate = Math.min(500, Math.max(1, Number(feeRate) || 20));
|
||||
const dest = String(to || "");
|
||||
// The `payments` decoder will reject anything that isn't a valid
|
||||
// DGB address; catch and re-raise as a sane error.
|
||||
try { payments.address({ address: dest, network: digibyte }); }
|
||||
catch {
|
||||
// bitcoinjs-lib's address decoder is `address.toOutputScript`, not
|
||||
// payments.address; use it for validation.
|
||||
try { bitcoinjs.address.toOutputScript(dest, digibyte); }
|
||||
catch (e) { throw new Error(`bad DGB address: ${e?.message || dest}`); }
|
||||
}
|
||||
const spendable = this._state.utxos.slice().sort((a, b) => (b.height > 0) - (a.height > 0));
|
||||
const change = this._changeEntry();
|
||||
if (sendMax) {
|
||||
const chosen = spendable;
|
||||
const sum = chosen.reduce((a, u) => a + u.value, 0);
|
||||
const fee = feeVb(chosen.length, 1, rate);
|
||||
if (sum <= fee) throw new Error("balance does not cover the fee");
|
||||
return {
|
||||
_chosen: chosen, _rate: rate, _to: dest, _sendMax: true, _change: change,
|
||||
recipients: [{ to: dest, value: sum - fee }],
|
||||
fee, feeRate: rate, change: 0,
|
||||
total: sum,
|
||||
};
|
||||
}
|
||||
const value = Math.round(Number(amount) || 0);
|
||||
if (!(value > 0)) throw new Error("amount must be > 0");
|
||||
let sum = 0; const chosen = [];
|
||||
for (const u of spendable) {
|
||||
chosen.push(u); sum += u.value;
|
||||
const withChange = feeVb(chosen.length, 2, rate);
|
||||
if (sum >= value + withChange) {
|
||||
const changeVal = sum - value - withChange;
|
||||
const fee = changeVal > 546 ? withChange : sum - value;
|
||||
return {
|
||||
_chosen: chosen, _rate: rate, _to: dest, _value: value,
|
||||
_change: change, _changeVal: changeVal > 546 ? changeVal : 0,
|
||||
recipients: [{ to: dest, value }],
|
||||
fee, feeRate: rate,
|
||||
change: changeVal > 546 ? changeVal : 0,
|
||||
total: value + fee,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error("insufficient funds");
|
||||
}
|
||||
|
||||
async signAndBroadcast(plan) {
|
||||
const psbtInputs = plan._chosen.map((u) => ({
|
||||
txid: u.txid,
|
||||
vout: u.vout,
|
||||
witness: { scriptHex: u.entry.scriptHex, value: u.value },
|
||||
}));
|
||||
const psbtOutputs = [{ address: plan._to, value: plan._sendMax ? plan.recipients[0].value : plan._value }];
|
||||
if (!plan._sendMax && plan._changeVal > 0) {
|
||||
psbtOutputs.push({ address: plan._change.address, value: plan._changeVal });
|
||||
}
|
||||
const psbt = buildPsbt({ inputs: psbtInputs, outputs: psbtOutputs }, digibyte);
|
||||
// Sign per-input with the exact key that funded that UTXO. signAllInputs
|
||||
// would work when all inputs share a key, but each derived address
|
||||
// has its own key, so we go per-input.
|
||||
for (let i = 0; i < plan._chosen.length; i++) {
|
||||
const signer = this._keys.signerFor(plan._chosen[i].entry);
|
||||
psbt.signInput(i, signer);
|
||||
}
|
||||
const { hex, txid } = finalizeAndExtract(psbt);
|
||||
const broadcast = await this._client.call("blockchain.transaction.broadcast", [hex]);
|
||||
if (typeof broadcast !== "string" || broadcast.length !== 64) {
|
||||
throw new Error("broadcast rejected: " + JSON.stringify(broadcast));
|
||||
}
|
||||
this.log("broadcast", txid);
|
||||
this._scheduleRefresh(1200);
|
||||
return { txid, hex, fee: plan.fee };
|
||||
}
|
||||
|
||||
// BIP-137-style: 65-byte recoverable signature over sha256d(magic || msg).
|
||||
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 = "DigiByte Signed Message:\n";
|
||||
const payload = Uint8Array.from([...varstr(MAGIC), ...varstr(String(message))]);
|
||||
const digest = sha256(sha256(payload));
|
||||
const entry = this.current();
|
||||
const signer = this._keys.signerFor(entry);
|
||||
// ECPair's `signSchnorr` and `sign` don't emit recoverable sigs; fall
|
||||
// back to node's ecc.signRecoverable through @bitcoinerlab/secp256k1
|
||||
// (which the vendored @dgb-wallet/core already loaded).
|
||||
const sig = ecc.signRecoverable(Buffer.from(digest), signer.privateKey);
|
||||
const out = Buffer.alloc(65);
|
||||
out[0] = 27 + sig.recoveryId + 4; // +4 = compressed pubkey
|
||||
Buffer.from(sig.signature).copy(out, 1);
|
||||
return { address: entry.address, signature: out.toString("base64") };
|
||||
}
|
||||
|
||||
recovery() {
|
||||
return { accountPath: this._keys.accountPath, xpub: this._keys.xpub, xprv: this._keys.xprv };
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
const cur = this.current();
|
||||
return {
|
||||
chain: "dgb", network: NETWORK, ticker: "DGB", decimals: 8,
|
||||
address: cur.address, addressIndex: this._state.receiveIndex,
|
||||
addressPath: cur.path,
|
||||
balance: this._state.balance,
|
||||
height: this._state.height,
|
||||
history: this._state.history,
|
||||
scanning: this._state.scanning,
|
||||
error: this._state.error,
|
||||
server: this._client.url || null,
|
||||
servers: this._servers,
|
||||
accountPath: this._keys.accountPath,
|
||||
xpub: this._keys.xpub,
|
||||
explorerTx: EXPLORER_TX, explorerAddr: EXPLORER_ADDR, faucet: null,
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
clearTimeout(this._refreshTimer);
|
||||
try { this._keys.wipe(); } catch {}
|
||||
try { this._client.disconnect(); } catch {}
|
||||
if (this._root) this._root.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
return { DgbWallet, EXPLORER_TX, EXPLORER_ADDR };
|
||||
};
|
||||
361
bundled-addons/bchwallet/lib/chain-eth.js
Normal file
361
bundled-addons/bchwallet/lib/chain-eth.js
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
// Ethereum (EVM) chain adapter — mainnet + Sepolia testnet. One address per
|
||||
// wallet, the same "TronLink shape" as chain-tron.js: BIP44 derivation,
|
||||
// secp256k1 → keccak256 address, JSON-RPC backend, EIP-1559 send.
|
||||
//
|
||||
// Kept intentionally minimal:
|
||||
// - Native ETH only. ERC-20 token support is a follow-up: it needs a token
|
||||
// registry + eth_call for `balanceOf(address)` per token + a dedicated
|
||||
// Send flow that builds an ERC-20 `transfer(to, value)` calldata.
|
||||
// - No transaction history: without an indexer (Etherscan V2 / Alchemy)
|
||||
// the JSON-RPC alone can't answer "which txs touched this address".
|
||||
// The panel shows an empty history with a link to Etherscan.
|
||||
// - EIP-1559 only (type 0x02). Legacy type 0x00 works too but isn't
|
||||
// needed for mainnet or Sepolia in 2026.
|
||||
|
||||
const NETWORKS = {
|
||||
mainnet: {
|
||||
id: "mainnet", label: "Mainnet", chainId: 1,
|
||||
// Cloudflare's public Ethereum gateway — no key required, rate-limited
|
||||
// but adequate for a per-user wallet. User can override in settings.
|
||||
defaultRpc: "https://cloudflare-eth.com",
|
||||
explorerTx: "https://etherscan.io/tx/",
|
||||
explorerAddr: "https://etherscan.io/address/",
|
||||
faucet: null,
|
||||
},
|
||||
sepolia: {
|
||||
id: "sepolia", label: "Sepolia testnet", chainId: 11155111,
|
||||
defaultRpc: "https://ethereum-sepolia-rpc.publicnode.com",
|
||||
explorerTx: "https://sepolia.etherscan.io/tx/",
|
||||
explorerAddr: "https://sepolia.etherscan.io/address/",
|
||||
faucet: "https://sepoliafaucet.com/",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = function makeEthAdapter({ HDKey, secp256k1, keccak_256 }) {
|
||||
if (!HDKey || !secp256k1 || !keccak_256) throw new Error("chain-eth: missing dep");
|
||||
|
||||
const toHex = (b) => Buffer.from(b).toString("hex");
|
||||
const fromHex = (h) => Uint8Array.from(Buffer.from(String(h).replace(/^0x/i, ""), "hex"));
|
||||
const stripHex = (h) => String(h).replace(/^0x/i, "");
|
||||
const hexToBig = (h) => BigInt("0x" + (stripHex(h) || "0"));
|
||||
const bigToHex = (n) => "0x" + BigInt(n).toString(16);
|
||||
const zeroBig = 0n;
|
||||
|
||||
// ---- addresses -------------------------------------------------------
|
||||
// EIP-55 mixed-case checksum: lowercase hex, then flip case per keccak256
|
||||
// of the lowercase hex string (a-f digits get uppercased where the keccak
|
||||
// nibble is >= 8). Never needed for wire format (RPCs accept lowercase),
|
||||
// but it's what wallets show, so we return it that way.
|
||||
function eip55(addressLowerHex) {
|
||||
const lower = stripHex(addressLowerHex).toLowerCase();
|
||||
const hash = toHex(keccak_256(Buffer.from(lower, "utf8")));
|
||||
let out = "0x";
|
||||
for (let i = 0; i < lower.length; i++) {
|
||||
const c = lower[i];
|
||||
out += /[0-9]/.test(c) ? c : (parseInt(hash[i], 16) >= 8 ? c.toUpperCase() : c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function addressFromPubkey(uncompressed) {
|
||||
const inner = uncompressed.slice(1);
|
||||
const h = keccak_256(inner);
|
||||
const h20 = h.slice(h.length - 20);
|
||||
return eip55(toHex(h20));
|
||||
}
|
||||
function decodeAddress(str) {
|
||||
const s = String(str || "").trim();
|
||||
const hex = stripHex(s);
|
||||
if (!/^[0-9a-fA-F]{40}$/.test(hex)) throw new Error("bad Ethereum address");
|
||||
// Reject checksum mismatches on mixed-case inputs (all-lower and all-upper
|
||||
// pass unconditionally — that's the EIP-55 rule).
|
||||
const lower = hex.toLowerCase(), upper = hex.toUpperCase();
|
||||
if (hex !== lower && hex !== upper) {
|
||||
const want = stripHex(eip55(lower));
|
||||
if (hex !== want) throw new Error("EIP-55 checksum failed");
|
||||
}
|
||||
return "0x" + lower;
|
||||
}
|
||||
|
||||
// ---- RLP encode ------------------------------------------------------
|
||||
// Minimal encoder — enough for EIP-1559 tx encoding. Follows the RLP spec
|
||||
// (single byte < 0x80 → self; short string ≤ 55 → 0x80 + len + bytes;
|
||||
// long string → 0x80 + 55 + lenOfLen + lenBytes + bytes; lists similarly
|
||||
// with 0xc0/0xf7).
|
||||
function rlpEncodeBytes(bytes) {
|
||||
const b = Uint8Array.from(bytes);
|
||||
if (b.length === 1 && b[0] < 0x80) return b;
|
||||
if (b.length <= 55) return concat(Uint8Array.from([0x80 + b.length]), b);
|
||||
const lenBytes = encodeIntBE(b.length);
|
||||
return concat(Uint8Array.from([0xb7 + lenBytes.length]), lenBytes, b);
|
||||
}
|
||||
function rlpEncodeList(items) {
|
||||
const encoded = items.map(rlpEncode);
|
||||
const body = concat(...encoded);
|
||||
if (body.length <= 55) return concat(Uint8Array.from([0xc0 + body.length]), body);
|
||||
const lenBytes = encodeIntBE(body.length);
|
||||
return concat(Uint8Array.from([0xf7 + lenBytes.length]), lenBytes, body);
|
||||
}
|
||||
function rlpEncode(item) {
|
||||
if (item instanceof Uint8Array) return rlpEncodeBytes(item);
|
||||
if (Array.isArray(item)) return rlpEncodeList(item);
|
||||
if (typeof item === "bigint") return rlpEncodeBytes(bigToBytes(item));
|
||||
if (typeof item === "number") return rlpEncodeBytes(bigToBytes(BigInt(item)));
|
||||
if (typeof item === "string") return rlpEncodeBytes(item.startsWith("0x") ? fromHex(item) : Buffer.from(item, "utf8"));
|
||||
throw new Error("rlp: unsupported item type " + typeof item);
|
||||
}
|
||||
function bigToBytes(v) {
|
||||
if (v < 0n) throw new Error("negative bigint");
|
||||
if (v === 0n) return new Uint8Array(0);
|
||||
let hex = v.toString(16);
|
||||
if (hex.length % 2) hex = "0" + hex;
|
||||
return fromHex(hex);
|
||||
}
|
||||
function encodeIntBE(n) {
|
||||
let hex = n.toString(16);
|
||||
if (hex.length % 2) hex = "0" + hex;
|
||||
return fromHex(hex);
|
||||
}
|
||||
function concat(...ps) {
|
||||
const n = ps.reduce((a, p) => a + p.length, 0);
|
||||
const out = new Uint8Array(n); let k = 0;
|
||||
for (const p of ps) { out.set(p, k); k += p.length; }
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- signing ---------------------------------------------------------
|
||||
// EIP-1559 signed tx: 0x02 || RLP([chainId, nonce, maxPriorityFeePerGas,
|
||||
// maxFeePerGas, gasLimit, to, value, data, accessList,
|
||||
// yParity, r, s])
|
||||
// hash-to-sign: keccak256(0x02 || RLP([...same-without-sig-fields]))
|
||||
function signTxEip1559(unsignedFields, privKey) {
|
||||
const unsignedRlp = rlpEncodeList(unsignedFields);
|
||||
const preimage = concat(Uint8Array.from([0x02]), unsignedRlp);
|
||||
const hash = keccak_256(preimage);
|
||||
const sig = secp256k1.sign(hash, privKey, { prehash: false, lowS: true, format: "recovered" });
|
||||
// noble returns [recid || r(32) || s(32)]; EIP-1559 uses yParity as
|
||||
// 0 or 1 (recid directly, no +27 shift).
|
||||
const yParity = sig[0];
|
||||
const r = sig.subarray(1, 33);
|
||||
const s = sig.subarray(33, 65);
|
||||
const signedFields = [...unsignedFields, yParity, stripLeadingZeros(r), stripLeadingZeros(s)];
|
||||
const signedRlp = rlpEncodeList(signedFields);
|
||||
return "0x" + toHex(concat(Uint8Array.from([0x02]), signedRlp));
|
||||
}
|
||||
function stripLeadingZeros(bytes) {
|
||||
let i = 0;
|
||||
while (i < bytes.length - 1 && bytes[i] === 0) i++;
|
||||
return bytes.subarray(i);
|
||||
}
|
||||
|
||||
// ---- JSON-RPC client -------------------------------------------------
|
||||
function makeClient(rpcUrl) {
|
||||
let seq = 1;
|
||||
async function call(method, params = []) {
|
||||
const r = await fetch(rpcUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id: seq++, method, params }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`${method}: HTTP ${r.status}`);
|
||||
const j = await r.json();
|
||||
if (j.error) throw new Error(`${method}: ${j.error.message || JSON.stringify(j.error)}`);
|
||||
return j.result;
|
||||
}
|
||||
return { url: rpcUrl, call };
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- wallet ----------------------------------------------------------
|
||||
class EthWallet {
|
||||
constructor(root32, networkId, {
|
||||
walletId, storage, log = () => {}, onChange = () => {}, rpcUrl,
|
||||
customNetwork, // { id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker } for EIP-3085 chains
|
||||
} = {}) {
|
||||
if (!walletId) throw new Error("chain-eth: walletId required");
|
||||
const net = customNetwork || NETWORKS[networkId];
|
||||
if (!net) throw new Error(`chain-eth: unknown network ${networkId}`);
|
||||
this.walletId = walletId;
|
||||
this.chain = "eth";
|
||||
this.network = net.id;
|
||||
this._net = net;
|
||||
this._ticker = net.ticker || "ETH";
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
||||
const master = HDKey.fromMasterSeed(root32);
|
||||
// BIP44 for Ethereum: m/44'/60'/0'/0/0 is the canonical first address.
|
||||
const node = master.derive("m/44'/60'/0'/0/0");
|
||||
this._priv = node.privateKey;
|
||||
this._pubUncompressed = secp256k1.getPublicKey(this._priv, false);
|
||||
this.address = addressFromPubkey(this._pubUncompressed);
|
||||
this._root = new Uint8Array(root32);
|
||||
this._client = makeClient(String(rpcUrl || "").trim() || net.defaultRpc);
|
||||
this._state = {
|
||||
balance: { confirmed: "0", unconfirmed: "0" },
|
||||
history: [],
|
||||
height: 0,
|
||||
scanning: false,
|
||||
error: null,
|
||||
};
|
||||
this._pollTimer = null;
|
||||
}
|
||||
|
||||
setRpcUrl(url) {
|
||||
const v = String(url || "").trim() || this._net.defaultRpc;
|
||||
this._client = makeClient(v);
|
||||
this._emit();
|
||||
}
|
||||
_emit() { try { this.onChange(); } catch {} }
|
||||
|
||||
// Wei is 10^18 native units; the panel formats via decimals=18. The
|
||||
// ticker follows the chain's nativeCurrency (ETH on mainnet/Sepolia,
|
||||
// MATIC on Polygon, etc.) so the send-approval overlay reads correctly.
|
||||
snapshot() {
|
||||
return {
|
||||
chain: "eth", network: this._net.id, ticker: this._ticker, decimals: 18,
|
||||
address: this.address, addressIndex: 0,
|
||||
addressPath: "m/44'/60'/0'/0/0",
|
||||
balance: this._state.balance,
|
||||
height: this._state.height,
|
||||
history: this._state.history,
|
||||
scanning: this._state.scanning,
|
||||
error: this._state.error,
|
||||
server: this._client.url,
|
||||
rpcUrl: this._client.url,
|
||||
explorerTx: this._net.explorerTx,
|
||||
explorerAddr: this._net.explorerAddr,
|
||||
faucet: this._net.faucet,
|
||||
chainId: this._net.chainId,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (this._state.scanning) return;
|
||||
this._state.scanning = true; this._state.error = null; this._emit();
|
||||
try {
|
||||
const [bal, block] = await Promise.all([
|
||||
this._client.call("eth_getBalance", [this.address, "latest"]),
|
||||
this._client.call("eth_blockNumber", []),
|
||||
]);
|
||||
this._state.balance = { confirmed: hexToBig(bal).toString(), unconfirmed: "0" };
|
||||
this._state.height = Number(hexToBig(block));
|
||||
} catch (e) {
|
||||
this._state.error = e?.message || String(e);
|
||||
this.log("refresh failed:", this._state.error);
|
||||
} finally {
|
||||
this._state.scanning = false;
|
||||
this._emit();
|
||||
}
|
||||
}
|
||||
schedulePoll(ms = 20_000) {
|
||||
clearTimeout(this._pollTimer);
|
||||
this._pollTimer = setTimeout(() => this.refresh().finally(() => this.schedulePoll(ms)), ms);
|
||||
}
|
||||
|
||||
async plan({ to, amount, sendMax }) {
|
||||
const dest = decodeAddress(to);
|
||||
const from = this.address.toLowerCase();
|
||||
const [nonceHex, priorityHex, gasPriceHex, gasLimitHex] = await Promise.all([
|
||||
this._client.call("eth_getTransactionCount", [from, "pending"]),
|
||||
this._client.call("eth_maxPriorityFeePerGas", []).catch(() => "0x59682f00"), // fallback: 1.5 gwei
|
||||
this._client.call("eth_gasPrice", []),
|
||||
Promise.resolve("0x5208"), // 21000 for a plain ETH transfer
|
||||
]);
|
||||
const nonce = Number(hexToBig(nonceHex));
|
||||
const maxPriorityFeePerGas = hexToBig(priorityHex);
|
||||
// maxFeePerGas heuristic: 2 * base fee + priority tip. base fee ~=
|
||||
// gasPrice - priority tip on EIP-1559 chains; we approximate with the
|
||||
// reported gasPrice as an upper bound plus the priority.
|
||||
const baseGuess = hexToBig(gasPriceHex);
|
||||
const maxFeePerGas = baseGuess * 2n + maxPriorityFeePerGas;
|
||||
const gasLimit = hexToBig(gasLimitHex);
|
||||
const fee = gasLimit * maxFeePerGas;
|
||||
const bal = BigInt(this._state.balance.confirmed || "0");
|
||||
let value;
|
||||
if (sendMax) {
|
||||
if (bal <= fee) throw new Error("balance does not cover the gas fee");
|
||||
value = bal - fee;
|
||||
} else {
|
||||
value = BigInt(Math.round(Number(amount) || 0)); // wei
|
||||
if (value <= 0n) throw new Error("amount must be > 0 wei");
|
||||
if (value + fee > bal) throw new Error("insufficient funds");
|
||||
}
|
||||
return {
|
||||
_draft: {
|
||||
chainId: this._net.chainId, nonce, maxPriorityFeePerGas, maxFeePerGas,
|
||||
gasLimit, to: dest, value, data: "0x", accessList: [],
|
||||
},
|
||||
recipients: [{ to: dest, value: value.toString() }],
|
||||
fee: fee.toString(),
|
||||
feeRate: maxFeePerGas.toString(),
|
||||
inputs: [],
|
||||
change: "0",
|
||||
total: (value + fee).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
async signAndBroadcast(plan) {
|
||||
const d = plan && plan._draft;
|
||||
if (!d) throw new Error("bad plan");
|
||||
const unsignedFields = [
|
||||
d.chainId, d.nonce, d.maxPriorityFeePerGas, d.maxFeePerGas, d.gasLimit,
|
||||
fromHex(d.to.slice(2)), d.value, fromHex(""), [],
|
||||
];
|
||||
const rawTxHex = signTxEip1559(unsignedFields, this._priv);
|
||||
const txid = await this._client.call("eth_sendRawTransaction", [rawTxHex]);
|
||||
if (typeof txid !== "string" || !/^0x[0-9a-f]{64}$/i.test(txid)) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
|
||||
this.log("broadcast", txid);
|
||||
setTimeout(() => this.refresh(), 3000);
|
||||
return { txid };
|
||||
}
|
||||
|
||||
// EIP-712: sign a pre-computed typed-data digest with r||s||v (v = 27+recid).
|
||||
signTypedDataDigest(digest32) {
|
||||
const sig = secp256k1.sign(digest32, this._priv, { prehash: false, lowS: true, format: "recovered" });
|
||||
const out = new Uint8Array(65);
|
||||
out.set(sig.subarray(1), 0);
|
||||
out[64] = sig[0] + 27;
|
||||
return { address: this.address, signature: "0x" + toHex(out) };
|
||||
}
|
||||
// Ethereum personal_sign: keccak256("\x19Ethereum Signed Message:\n" + len + msg).
|
||||
signMessage(message) {
|
||||
const msg = String(message);
|
||||
const enc = new TextEncoder();
|
||||
const body = enc.encode(msg);
|
||||
const prefix = enc.encode("\x19Ethereum Signed Message:\n" + body.length);
|
||||
const buf = concat(prefix, body);
|
||||
const hash = keccak_256(buf);
|
||||
const sig = secp256k1.sign(hash, this._priv, { prehash: false, lowS: true, format: "recovered" });
|
||||
// personal_sign format: r || s || v where v = 27 + recid.
|
||||
const out = new Uint8Array(65);
|
||||
out.set(sig.subarray(1), 0);
|
||||
out[64] = sig[0] + 27;
|
||||
return { address: this.address, signature: "0x" + toHex(out) };
|
||||
}
|
||||
|
||||
recovery() {
|
||||
// Ethereum wallets typically expose the raw private key hex; we do too,
|
||||
// but only when the caller re-confirms in the approval overlay upstream.
|
||||
return {
|
||||
accountPath: "m/44'/60'/0'/0/0",
|
||||
xpub: "0x" + toHex(this._pubUncompressed),
|
||||
xprv: "0x" + toHex(this._priv),
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
clearTimeout(this._pollTimer);
|
||||
try { this._priv && this._priv.fill(0); } catch {}
|
||||
try { this._root && this._root.fill(0); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return { EthWallet, NETWORKS, addressFromPubkey, decodeAddress, eip55 };
|
||||
};
|
||||
182
bundled-addons/bchwallet/lib/chain-sia.js
Normal file
182
bundled-addons/bchwallet/lib/chain-sia.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Sia (SC) chain adapter — v2 walletd-backed. Constructs a wallet from a
|
||||
// 32-byte root and a user-supplied walletd URL. Amounts are hastings
|
||||
// (1 SC = 10^24 hastings) and cross the panel boundary as decimal strings
|
||||
// so the panel never has to touch BigInt precision.
|
||||
//
|
||||
// Deps: sia.js / keys.js / wallet.js / walletd.js — copied from the
|
||||
// standalone siawallet addon on 2026-09-07 when Aegis absorbed it, so the
|
||||
// key derivation + tx layout are byte-identical to the older addon.
|
||||
// Existing on-chain funds carry over via the vault-derive absorb (aegis's
|
||||
// manifest lists "siawallet" under absorbs, so the legacy purpose
|
||||
// "siawallet/mainnet/0" resolves to the same seed inside Aegis).
|
||||
|
||||
const SC = 10n ** 24n;
|
||||
const EXPLORER_TX = "https://siascan.com/tx/";
|
||||
const EXPLORER_ADDR = "https://siascan.com/address/";
|
||||
|
||||
module.exports = function makeSiaAdapter({ ed25519, blake2b }) {
|
||||
if (!ed25519 || !blake2b) throw new Error("chain-sia: missing dep");
|
||||
const sia = require("./sia/sia.js")({ ed25519, blake2b });
|
||||
const keysLib = require("./sia/keys.js")({ sia });
|
||||
const walletd = require("./sia/walletd.js")({ log: () => {} });
|
||||
const walletFactory = require("./sia/wallet.js");
|
||||
|
||||
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 SiaWallet {
|
||||
constructor(root32, {
|
||||
walletId, storage, log = () => {}, onChange = () => {},
|
||||
walletdUrl = "",
|
||||
} = {}) {
|
||||
if (!walletId) throw new Error("chain-sia: walletId required");
|
||||
this.walletId = walletId;
|
||||
this.chain = "sc";
|
||||
this.network = "mainnet";
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
||||
this._root = new Uint8Array(root32);
|
||||
this._keys = new keysLib.WalletKeys(root32);
|
||||
this._walletdUrl = String(walletdUrl || "").trim();
|
||||
this._client = null;
|
||||
this._wallet = null;
|
||||
if (this._walletdUrl) this._build();
|
||||
}
|
||||
|
||||
_build() {
|
||||
if (this._wallet) { try { this._wallet.dispose(); } catch {} this._wallet = null; }
|
||||
if (this._client) this._client.setBase(this._walletdUrl);
|
||||
else this._client = new walletd.Client(this._walletdUrl);
|
||||
this._wallet = walletFactory({
|
||||
client: this._client, keys: this._keys, sia,
|
||||
storage: this.storage,
|
||||
log: (...a) => this.log(...a),
|
||||
onChange: () => { try { this.onChange(); } catch {} },
|
||||
});
|
||||
}
|
||||
|
||||
setWalletdUrl(url) {
|
||||
const v = String(url || "").trim();
|
||||
if (v === this._walletdUrl) return;
|
||||
this._walletdUrl = v;
|
||||
if (v) this._build(); else { try { this._wallet && this._wallet.dispose(); } catch {} this._wallet = null; }
|
||||
}
|
||||
|
||||
// The panel treats Sia amounts as decimal strings of hastings; the
|
||||
// display layer picks how many SC-precision digits to show.
|
||||
snapshot() {
|
||||
const w = this._wallet && this._wallet.snapshot();
|
||||
const base = {
|
||||
chain: "sc", network: "mainnet", ticker: "SC", decimals: 24,
|
||||
address: null, addressIndex: 0, addressPath: `KeyFromSeed(seed, ${w?.addressIndex || 0})`,
|
||||
balance: { confirmed: "0", unconfirmed: "0" },
|
||||
height: 0, history: [], scanning: false, error: null,
|
||||
server: this._client ? this._client.displayUrl : null,
|
||||
walletdUrl: this._walletdUrl,
|
||||
needsWalletdUrl: !this._walletdUrl,
|
||||
explorerTx: EXPLORER_TX, explorerAddr: EXPLORER_ADDR, faucet: null,
|
||||
};
|
||||
if (w) {
|
||||
base.address = w.address;
|
||||
base.addressIndex = w.addressIndex;
|
||||
// wallet.js exposes confirmed / immature; the panel's shared shape
|
||||
// is confirmed / unconfirmed, so map immature → unconfirmed for
|
||||
// visual parity with BCH and TRX (small semantic bend, but the
|
||||
// number is the "not yet spendable" one either way).
|
||||
base.balance = { confirmed: w.balance.confirmed, unconfirmed: w.balance.immature };
|
||||
base.height = w.height;
|
||||
base.history = (w.history || []).map((r) => ({
|
||||
txid: r.id, delta: r.delta, to: r.to, from: null,
|
||||
fee: null, time: r.time || 0,
|
||||
confirmations: r.confirmations || 0,
|
||||
status: r.confirmations > 0 ? "confirmed" : "pending",
|
||||
kind: r.type,
|
||||
}));
|
||||
base.scanning = w.scanning;
|
||||
base.error = w.error;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
async refresh(full) {
|
||||
if (!this._wallet) return;
|
||||
return this._wallet.refresh(!!full);
|
||||
}
|
||||
nextAddress() {
|
||||
if (!this._wallet) throw new Error("no walletd URL configured");
|
||||
return this._wallet.nextUnusedAddress();
|
||||
}
|
||||
current() {
|
||||
if (!this._wallet) throw new Error("no walletd URL configured");
|
||||
return this._wallet.current();
|
||||
}
|
||||
plan(spec) {
|
||||
if (!this._wallet) throw new Error("no walletd URL configured");
|
||||
const targets = Array.isArray(spec.outputs) && spec.outputs.length
|
||||
? spec.outputs.map((o) => ({ to: o.to, value: toHastings(o.amount ?? o.value) }))
|
||||
: [{ to: spec.to, value: toHastings(spec.amount ?? spec.value) }];
|
||||
const p = this._wallet.plan({ targets, feeMultiplier: spec.feeMultiplier || spec.feeRate, sendMax: !!spec.sendMax });
|
||||
// Present the plan in the common panel shape: BigInts as decimal
|
||||
// strings, plus `total = sum(recipients) + fee`.
|
||||
const sent = p.recipients.reduce((a, r) => a + BigInt(r.value), 0n);
|
||||
return {
|
||||
_sia: p, // internal handle so signAndBroadcast doesn't re-plan
|
||||
recipients: p.recipients,
|
||||
fee: p.fee.toString(),
|
||||
feeRate: p.feePerByte.toString(),
|
||||
inputs: p.tx.inputs,
|
||||
change: p.change.toString(),
|
||||
total: (sent + p.fee).toString(),
|
||||
};
|
||||
}
|
||||
async signAndBroadcast(plan) {
|
||||
if (!this._wallet) throw new Error("no walletd URL configured");
|
||||
const inner = plan && plan._sia;
|
||||
if (!inner) throw new Error("bad plan");
|
||||
return this._wallet.signAndBroadcast(inner);
|
||||
}
|
||||
// Sia signature = ed25519 over blake2b256 of the raw message. Not a
|
||||
// BIP-137 style thing — dapps that want it should treat this as an
|
||||
// opaque {publicKey, signature} pair verified via ed25519.
|
||||
signMessage(message) {
|
||||
const entry = this.current();
|
||||
const digest = sia.b256(new TextEncoder().encode(String(message)));
|
||||
const sig = this._keys.sign(entry, digest);
|
||||
return {
|
||||
address: entry.address,
|
||||
publicKey: "ed25519:" + sia.toHex(entry.pub),
|
||||
signature: sia.toHex(sig),
|
||||
};
|
||||
}
|
||||
recovery() {
|
||||
// Sia has no xpub/xprv notion here; the scheme is "seed + index".
|
||||
return {
|
||||
accountPath: `KeyFromSeed(seed, i)`,
|
||||
xpub: sia.toHex(this._keys.entry(0).pub), // just the first pub for reference
|
||||
xprv: this._keys.seedHex,
|
||||
};
|
||||
}
|
||||
startPolling() { if (this._wallet) this._wallet.startPolling(60_000); }
|
||||
dispose() {
|
||||
try { this._wallet && this._wallet.dispose(); } catch {}
|
||||
try { this._keys && this._keys.wipe(); } catch {}
|
||||
if (this._root) this._root.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Amounts arrive as strings (hastings) or as small numbers.
|
||||
function toHastings(v) {
|
||||
if (typeof v === "bigint") return v;
|
||||
const s = String(v ?? "0").trim();
|
||||
if (!/^\d+$/.test(s)) throw new Error("amount must be an integer number of hastings");
|
||||
return BigInt(s);
|
||||
}
|
||||
|
||||
return { SiaWallet, EXPLORER_TX, EXPLORER_ADDR };
|
||||
};
|
||||
473
bundled-addons/bchwallet/lib/chain-sol.js
Normal file
473
bundled-addons/bchwallet/lib/chain-sol.js
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
// Solana (SOL) chain adapter — mainnet-beta + devnet. Ed25519 keypair per
|
||||
// SLIP-0010 (all-hardened path), base58 address, native SOL transfers via
|
||||
// the system program. Sits directly on the JSON-RPC endpoint; no @solana
|
||||
// SDK dep so the addon stays lean.
|
||||
//
|
||||
// Derivation: m/44'/501'/0'/0' — Phantom's default path. Every segment is
|
||||
// hardened per SLIP-0010 (ed25519 forbids non-hardened derivation because
|
||||
// the point-add trick that BIP32 uses on secp256k1 doesn't exist for
|
||||
// Curve25519). Multi-index accounts under one wallet aren't exposed here;
|
||||
// each Aegis "sub-account" gets its own vault-derive purpose instead.
|
||||
//
|
||||
// Not implemented in this rev:
|
||||
// - SPL token balances / transfers (needs Associated Token Account math
|
||||
// and the SPL Token program's transfer instruction).
|
||||
// - Transaction history (getSignaturesForAddress + getTransaction is
|
||||
// doable but heavy for a first cut; the panel links to Solana Explorer
|
||||
// for now).
|
||||
|
||||
const NETWORKS = {
|
||||
mainnet: {
|
||||
id: "mainnet", label: "Mainnet",
|
||||
defaultRpc: "https://api.mainnet-beta.solana.com",
|
||||
explorerTx: "https://explorer.solana.com/tx/",
|
||||
explorerAddr: "https://explorer.solana.com/address/",
|
||||
explorerCluster: "",
|
||||
faucet: null,
|
||||
},
|
||||
devnet: {
|
||||
id: "devnet", label: "Devnet",
|
||||
defaultRpc: "https://api.devnet.solana.com",
|
||||
explorerTx: "https://explorer.solana.com/tx/",
|
||||
explorerAddr: "https://explorer.solana.com/address/",
|
||||
explorerCluster: "?cluster=devnet",
|
||||
faucet: "https://faucet.solana.com/",
|
||||
},
|
||||
};
|
||||
|
||||
// System program's address is 32 bytes of zeros; base58 is "1111...1111".
|
||||
const SYSTEM_PROGRAM = new Uint8Array(32);
|
||||
|
||||
module.exports = function makeSolAdapter({ ed25519, base58, sha256 }) {
|
||||
if (!ed25519 || !base58 || !sha256) throw new Error("chain-sol: missing dep");
|
||||
const spl = require("./sol-spl.js")({ ed25519, sha256, base58 });
|
||||
|
||||
// ---- HMAC-SHA512 (for SLIP-0010) -------------------------------------
|
||||
// Not in @noble/hashes/sha2 as a direct helper for SHA-512; @noble/hashes
|
||||
// exports `hmac` in ./hmac. If unavailable, use Node's crypto — Electron
|
||||
// main is Node, so require("crypto") always works.
|
||||
const nodeCrypto = require("node:crypto");
|
||||
function hmacSha512(key, msg) {
|
||||
return new Uint8Array(nodeCrypto.createHmac("sha512", Buffer.from(key)).update(Buffer.from(msg)).digest());
|
||||
}
|
||||
|
||||
// ---- SLIP-0010 ed25519 derivation ------------------------------------
|
||||
const ED25519_MASTER_KEY = new TextEncoder().encode("ed25519 seed");
|
||||
function slip10Master(seed32) {
|
||||
const I = hmacSha512(ED25519_MASTER_KEY, seed32);
|
||||
return { key: I.slice(0, 32), chainCode: I.slice(32) };
|
||||
}
|
||||
function slip10Derive(parent, indexHardened) {
|
||||
// Data: 0x00 || parent.key || uint32BE(0x80000000 | index)
|
||||
const idx = 0x80000000 | (indexHardened & 0x7fffffff);
|
||||
const data = new Uint8Array(1 + 32 + 4);
|
||||
data[0] = 0x00;
|
||||
data.set(parent.key, 1);
|
||||
// Write index as big-endian u32; JS bitwise is signed so |0 masks correctly.
|
||||
data[33] = (idx >>> 24) & 0xff;
|
||||
data[34] = (idx >>> 16) & 0xff;
|
||||
data[35] = (idx >>> 8) & 0xff;
|
||||
data[36] = idx & 0xff;
|
||||
const I = hmacSha512(parent.chainCode, data);
|
||||
return { key: I.slice(0, 32), chainCode: I.slice(32) };
|
||||
}
|
||||
function derivePath(seed32, segments) {
|
||||
let node = slip10Master(seed32);
|
||||
for (const s of segments) node = slip10Derive(node, s);
|
||||
return node;
|
||||
}
|
||||
// "m/44'/501'/0'/0'" → [44, 501, 0, 0]. Every SLIP-0010 ed25519 segment
|
||||
// is hardened; the parser accepts either the standard "'" suffix or a
|
||||
// bare integer (both mean the same for this curve).
|
||||
function parseAllHardened(path) {
|
||||
const parts = String(path || "").trim().split("/").filter((p) => p && p !== "m");
|
||||
return parts.map((p) => {
|
||||
const m = /^(\d+)'?$/.exec(p);
|
||||
if (!m) throw new Error("bad SOL derivation path: " + path);
|
||||
return Number(m[1]);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Solana short-vec (compact-u16) ----------------------------------
|
||||
// Up to 3 bytes; 7 data bits per byte with continuation bit in position 7.
|
||||
function encodeCompactU16(n) {
|
||||
if (n < 0 || n > 0xffff) throw new Error("compact-u16 out of range");
|
||||
const out = [];
|
||||
let rem = n;
|
||||
while (true) {
|
||||
let byte = rem & 0x7f;
|
||||
rem >>= 7;
|
||||
if (rem === 0) { out.push(byte); break; }
|
||||
byte |= 0x80;
|
||||
out.push(byte);
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
}
|
||||
|
||||
const concat = (...ps) => {
|
||||
const n = ps.reduce((a, p) => a + p.length, 0);
|
||||
const o = new Uint8Array(n); let k = 0;
|
||||
for (const p of ps) { o.set(p, k); k += p.length; }
|
||||
return o;
|
||||
};
|
||||
const u64le = (n) => {
|
||||
let v = BigInt(n); const o = new Uint8Array(8);
|
||||
for (let i = 0; i < 8; i++) { o[i] = Number(v & 0xffn); v >>= 8n; }
|
||||
return o;
|
||||
};
|
||||
|
||||
// ---- transaction assembly --------------------------------------------
|
||||
// For a native SOL transfer between two addresses:
|
||||
// accounts (writable-signed | readonly-signed | writable-unsigned | readonly-unsigned):
|
||||
// [ from (WS), to (WU), systemProgram (RU) ]
|
||||
// header = [1 required-sig, 0 readonly-signed, 1 readonly-unsigned]
|
||||
// instructions = [{ programIdIndex: 2, accounts: [0, 1], data: u32(2) || u64(lamports) }]
|
||||
function buildSolTransferMessage({ fromPub, toPub, lamports, recentBlockhash }) {
|
||||
// account keys must be de-duplicated in the message; distinct here.
|
||||
const keys = [fromPub, toPub, SYSTEM_PROGRAM];
|
||||
const header = Uint8Array.from([1, 0, 1]);
|
||||
const keysSection = concat(
|
||||
encodeCompactU16(keys.length),
|
||||
...keys.map((k) => Uint8Array.from(k)),
|
||||
);
|
||||
// Instruction data: [2 (u32 LE = system Transfer discriminator), lamports (u64 LE)]
|
||||
const instrData = concat(Uint8Array.from([2, 0, 0, 0]), u64le(lamports));
|
||||
const instr = concat(
|
||||
Uint8Array.from([2]), // programIdIndex
|
||||
encodeCompactU16(2), // account key count
|
||||
Uint8Array.from([0, 1]), // account indexes (from, to)
|
||||
encodeCompactU16(instrData.length), // data length
|
||||
instrData,
|
||||
);
|
||||
const instrSection = concat(encodeCompactU16(1), instr);
|
||||
return concat(header, keysSection, recentBlockhash, instrSection);
|
||||
}
|
||||
|
||||
// ---- JSON-RPC --------------------------------------------------------
|
||||
function makeClient(rpcUrl) {
|
||||
let seq = 1;
|
||||
async function call(method, params = []) {
|
||||
const r = await fetch(rpcUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id: seq++, method, params }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`${method}: HTTP ${r.status}`);
|
||||
const j = await r.json();
|
||||
if (j.error) throw new Error(`${method}: ${j.error.message || JSON.stringify(j.error)}`);
|
||||
return j.result;
|
||||
}
|
||||
return { url: rpcUrl, call };
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- wallet ----------------------------------------------------------
|
||||
class SolWallet {
|
||||
constructor(root32, networkId, {
|
||||
walletId, storage, log = () => {}, onChange = () => {}, rpcUrl,
|
||||
derivationPath = "m/44'/501'/0'/0'",
|
||||
} = {}) {
|
||||
if (!walletId) throw new Error("chain-sol: walletId required");
|
||||
const net = NETWORKS[networkId];
|
||||
if (!net) throw new Error(`chain-sol: unknown network ${networkId}`);
|
||||
this.walletId = walletId;
|
||||
this.chain = "sol";
|
||||
this.network = net.id;
|
||||
this._net = net;
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
||||
this._path = derivationPath;
|
||||
const derived = derivePath(root32, parseAllHardened(derivationPath));
|
||||
this._priv = derived.key; // 32-byte ed25519 seed
|
||||
this._pub = ed25519.getPublicKey(this._priv); // 32 bytes
|
||||
this.address = base58.encode(this._pub);
|
||||
this._root = new Uint8Array(root32);
|
||||
this._client = makeClient(String(rpcUrl || "").trim() || net.defaultRpc);
|
||||
this._state = {
|
||||
balance: { confirmed: 0, unconfirmed: 0 },
|
||||
history: [],
|
||||
tokens: [], // [{mint, symbol, name, decimals, balance, tokenAccount, tokenProgram}]
|
||||
height: 0,
|
||||
scanning: false,
|
||||
error: null,
|
||||
};
|
||||
this._pollTimer = null;
|
||||
}
|
||||
|
||||
setRpcUrl(url) {
|
||||
const v = String(url || "").trim() || this._net.defaultRpc;
|
||||
this._client = makeClient(v);
|
||||
this._emit();
|
||||
}
|
||||
_emit() { try { this.onChange(); } catch {} }
|
||||
|
||||
snapshot() {
|
||||
return {
|
||||
chain: "sol", network: this._net.id, ticker: "SOL", decimals: 9,
|
||||
address: this.address, addressIndex: 0, addressPath: this._path,
|
||||
balance: this._state.balance,
|
||||
height: this._state.height,
|
||||
history: this._state.history,
|
||||
tokens: this._state.tokens,
|
||||
scanning: this._state.scanning,
|
||||
error: this._state.error,
|
||||
server: this._client.url,
|
||||
rpcUrl: this._client.url,
|
||||
explorerTx: this._net.explorerTx,
|
||||
explorerAddr: this._net.explorerAddr,
|
||||
explorerSuffix: this._net.explorerCluster,
|
||||
faucet: this._net.faucet,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (this._state.scanning) return;
|
||||
this._state.scanning = true; this._state.error = null; this._emit();
|
||||
try {
|
||||
const [balRes, slot, tokens] = await Promise.all([
|
||||
this._client.call("getBalance", [this.address]),
|
||||
this._client.call("getSlot", []),
|
||||
this._fetchTokens().catch((e) => { this.log("tokens fetch failed:", e?.message); return []; }),
|
||||
]);
|
||||
// getBalance response: { context, value: lamports }
|
||||
const lamports = balRes && typeof balRes === "object" ? Number(balRes.value || 0) : Number(balRes || 0);
|
||||
this._state.balance = { confirmed: lamports, unconfirmed: 0 };
|
||||
this._state.height = Number(slot || 0);
|
||||
this._state.tokens = tokens;
|
||||
} catch (e) {
|
||||
this._state.error = e?.message || String(e);
|
||||
this.log("refresh failed:", this._state.error);
|
||||
} finally {
|
||||
this._state.scanning = false;
|
||||
this._emit();
|
||||
}
|
||||
}
|
||||
// getTokenAccountsByOwner + parse. Result shape:
|
||||
// {context, value: [{ pubkey, account: {data: {parsed: {info:{mint, tokenAmount:{amount,decimals,uiAmountString}}}, program, space}, executable, ...} }]}
|
||||
// We ask for jsonParsed encoding so the RPC does the layout heavy-lift.
|
||||
async _fetchTokens() {
|
||||
const known = spl.KNOWN_TOKENS[this._net.id] || {};
|
||||
const call = (programB58) => this._client.call("getTokenAccountsByOwner", [
|
||||
this.address,
|
||||
{ programId: programB58 },
|
||||
{ encoding: "jsonParsed", commitment: "confirmed" },
|
||||
]);
|
||||
const results = await Promise.all([
|
||||
call(spl.TOKEN_PROGRAM_ID_B58).catch(() => ({ value: [] })),
|
||||
call(spl.TOKEN_2022_PROGRAM_ID_B58).catch(() => ({ value: [] })),
|
||||
]);
|
||||
const out = [];
|
||||
for (let p = 0; p < results.length; p++) {
|
||||
const list = results[p]?.value || [];
|
||||
const isTk22 = p === 1;
|
||||
for (const it of list) {
|
||||
const info = it?.account?.data?.parsed?.info;
|
||||
if (!info) continue;
|
||||
const mint = String(info.mint || "");
|
||||
const dec = Number(info.tokenAmount?.decimals || 0);
|
||||
const rawAmount = String(info.tokenAmount?.amount || "0");
|
||||
const meta = known[mint];
|
||||
out.push({
|
||||
mint, tokenAccount: String(it.pubkey || ""),
|
||||
tokenProgram: isTk22 ? spl.TOKEN_2022_PROGRAM_ID_B58 : spl.TOKEN_PROGRAM_ID_B58,
|
||||
symbol: meta?.symbol || mint.slice(0, 6) + "…",
|
||||
name: meta?.name || null,
|
||||
decimals: dec,
|
||||
balance: rawAmount, // string (u64) to preserve precision
|
||||
isKnown: !!meta,
|
||||
isToken2022: isTk22,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort known tokens first, then by balance desc.
|
||||
out.sort((a, b) => (b.isKnown - a.isKnown) || (BigInt(b.balance) > BigInt(a.balance) ? 1 : -1));
|
||||
return out;
|
||||
}
|
||||
schedulePoll(ms = 20_000) {
|
||||
clearTimeout(this._pollTimer);
|
||||
this._pollTimer = setTimeout(() => this.refresh().finally(() => this.schedulePoll(ms)), ms);
|
||||
}
|
||||
|
||||
// Solana fees are (usually) 5000 lamports per signature; reserve that
|
||||
// in max-mode. Real fee comes from the network on broadcast.
|
||||
async plan({ to, amount, sendMax }) {
|
||||
const toBytes = base58.decode(String(to || "").trim());
|
||||
if (!toBytes || toBytes.length !== 32) throw new Error("bad Solana address");
|
||||
const bal = this._state.balance.confirmed || 0;
|
||||
const FEE = 5000; // lamports per signature
|
||||
let lamports;
|
||||
if (sendMax) {
|
||||
if (bal <= FEE) throw new Error("balance does not cover the fee");
|
||||
lamports = bal - FEE;
|
||||
} else {
|
||||
lamports = Math.round(Number(amount) || 0);
|
||||
if (!(lamports > 0)) throw new Error("amount must be > 0 lamports");
|
||||
if (lamports + FEE > bal) throw new Error("insufficient funds");
|
||||
}
|
||||
// Fetch the fresh blockhash at plan time so signing can use it
|
||||
// immediately — Solana blockhashes expire quickly (~150 slots ≈ 60s).
|
||||
const { blockhash } = (await this._client.call("getLatestBlockhash", [])).value || {};
|
||||
if (!blockhash) throw new Error("could not fetch a recent blockhash");
|
||||
const recent = base58.decode(String(blockhash));
|
||||
return {
|
||||
_draft: { toBytes, lamports, recentBlockhash: recent },
|
||||
recipients: [{ to: base58.encode(toBytes), value: lamports }],
|
||||
fee: FEE, feeRate: FEE,
|
||||
inputs: [], change: 0,
|
||||
total: lamports + FEE,
|
||||
};
|
||||
}
|
||||
|
||||
async signAndBroadcast(plan) {
|
||||
const d = plan && plan._draft;
|
||||
if (!d) throw new Error("bad plan");
|
||||
const message = buildSolTransferMessage({
|
||||
fromPub: this._pub, toPub: d.toBytes, lamports: d.lamports,
|
||||
recentBlockhash: d.recentBlockhash,
|
||||
});
|
||||
const sig = ed25519.sign(message, this._priv); // 64 bytes
|
||||
// Full transaction wire format: sig-count || sigs... || message
|
||||
const sigCount = encodeCompactU16(1);
|
||||
const wire = concat(sigCount, sig, message);
|
||||
// Solana's sendTransaction accepts base58 (default) or base64 with
|
||||
// {encoding:"base64"} in the second arg; we use base58 for parity
|
||||
// with the rest of the ecosystem.
|
||||
const wireBase58 = base58.encode(wire);
|
||||
const txid = await this._client.call("sendTransaction", [wireBase58]);
|
||||
if (typeof txid !== "string" || !txid.length) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
|
||||
this.log("broadcast", txid);
|
||||
setTimeout(() => this.refresh(), 4000);
|
||||
return { txid };
|
||||
}
|
||||
|
||||
// ---- SPL token send -----------------------------------------------
|
||||
// Build a TransferChecked (+ optional CreateAssociatedTokenAccountIdempotent)
|
||||
// transaction moving `amount` (in raw token units) of a given mint to a
|
||||
// recipient's ATA. `mintB58` is the mint address as a base58 string;
|
||||
// decimals come from the sender's own token account (or the caller
|
||||
// passes them explicitly if the sender's ATA is empty).
|
||||
async planTokenTransfer({ mint, to, amount, decimals, tokenProgram }) {
|
||||
const mintB58 = String(mint || "");
|
||||
const mintBytes = base58.decode(mintB58);
|
||||
if (mintBytes.length !== 32) throw new Error("bad mint address");
|
||||
const recipientBytes = base58.decode(String(to || "").trim());
|
||||
if (recipientBytes.length !== 32) throw new Error("bad recipient address");
|
||||
// Resolve the token program for this mint from our own token list —
|
||||
// Token-2022 mints need the 2022 program in the transfer instruction.
|
||||
let tp = tokenProgram ? base58.decode(tokenProgram) : spl.TOKEN_PROGRAM_ID;
|
||||
let dec = decimals;
|
||||
const mine = (this._state.tokens || []).find((t) => t.mint === mintB58);
|
||||
if (mine) {
|
||||
tp = base58.decode(mine.tokenProgram);
|
||||
if (dec == null) dec = mine.decimals;
|
||||
}
|
||||
if (dec == null) throw new Error("token decimals unknown — no ATA on this wallet for that mint");
|
||||
const amt = BigInt(amount);
|
||||
if (amt <= 0n) throw new Error("amount must be > 0");
|
||||
if (mine && BigInt(mine.balance) < amt) throw new Error("insufficient token balance");
|
||||
const sourceATA = spl.associatedTokenAddress(this._pub, mintBytes, tp);
|
||||
const destATA = spl.associatedTokenAddress(recipientBytes, mintBytes, tp);
|
||||
// Ask the RPC whether the destination ATA already exists. If not,
|
||||
// prepend a CreateIdempotent instruction so the transfer succeeds
|
||||
// in one round-trip — the recipient never has to have interacted
|
||||
// with this mint before.
|
||||
const destATA_B58 = base58.encode(destATA);
|
||||
const info = await this._client.call("getAccountInfo", [destATA_B58, { encoding: "base64" }]);
|
||||
const destExists = !!(info && info.value);
|
||||
const instructions = [];
|
||||
if (!destExists) {
|
||||
instructions.push(spl.createATAIdempotentInstruction({
|
||||
payer: this._pub, ata: destATA, owner: recipientBytes, mint: mintBytes, tokenProgram: tp,
|
||||
}));
|
||||
}
|
||||
instructions.push(spl.transferCheckedInstruction({
|
||||
sourceATA, mint: mintBytes, destATA, owner: this._pub,
|
||||
amount: amt, decimals: dec, tokenProgram: tp,
|
||||
}));
|
||||
const { blockhash } = (await this._client.call("getLatestBlockhash", [])).value || {};
|
||||
if (!blockhash) throw new Error("could not fetch a recent blockhash");
|
||||
const recentBlockhash = base58.decode(String(blockhash));
|
||||
return {
|
||||
_spl: { instructions, recentBlockhash, destExists, sourceATA, destATA },
|
||||
recipients: [{ to: base58.encode(recipientBytes), value: amt.toString() }],
|
||||
// Fee estimate: 5000 lamports per signature + ~2039280 rent-exempt
|
||||
// if we're creating a new ATA. Real fee still comes from the network.
|
||||
fee: destExists ? 5000 : 5000 + 2039280,
|
||||
feeRate: 5000,
|
||||
inputs: [],
|
||||
change: 0,
|
||||
total: amt.toString(),
|
||||
mint: mintB58,
|
||||
decimals: dec,
|
||||
};
|
||||
}
|
||||
async signAndBroadcastToken(plan) {
|
||||
const sp = plan && plan._spl;
|
||||
if (!sp) throw new Error("bad token plan");
|
||||
const message = spl.buildMessage({
|
||||
feePayer: this._pub,
|
||||
instructions: sp.instructions,
|
||||
recentBlockhash: sp.recentBlockhash,
|
||||
});
|
||||
const sig = ed25519.sign(message, this._priv);
|
||||
const encodeCompactU16 = (n) => {
|
||||
const out = [];
|
||||
let rem = n;
|
||||
while (true) {
|
||||
let byte = rem & 0x7f; rem >>= 7;
|
||||
if (rem === 0) { out.push(byte); break; }
|
||||
byte |= 0x80; out.push(byte);
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
};
|
||||
const sigCount = encodeCompactU16(1);
|
||||
const wire = new Uint8Array(sigCount.length + 64 + message.length);
|
||||
wire.set(sigCount, 0);
|
||||
wire.set(sig, sigCount.length);
|
||||
wire.set(message, sigCount.length + 64);
|
||||
const wireB58 = base58.encode(wire);
|
||||
const txid = await this._client.call("sendTransaction", [wireB58]);
|
||||
if (typeof txid !== "string" || !txid.length) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
|
||||
this.log("SPL broadcast", txid);
|
||||
setTimeout(() => this.refresh().catch(() => {}), 4000);
|
||||
return { txid };
|
||||
}
|
||||
|
||||
// Solana's convention: ed25519 signature over the raw message bytes,
|
||||
// returned as {publicKey, signature} both base58. Dapps that follow
|
||||
// the wallet-adapter standard verify against these.
|
||||
signMessage(message) {
|
||||
const bytes = new TextEncoder().encode(String(message));
|
||||
const sig = ed25519.sign(bytes, this._priv);
|
||||
return {
|
||||
address: this.address,
|
||||
publicKey: base58.encode(this._pub),
|
||||
signature: base58.encode(sig),
|
||||
};
|
||||
}
|
||||
|
||||
recovery() {
|
||||
return {
|
||||
accountPath: this._path,
|
||||
xpub: base58.encode(this._pub),
|
||||
xprv: Buffer.from(this._priv).toString("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
clearTimeout(this._pollTimer);
|
||||
try { this._priv && this._priv.fill(0); } catch {}
|
||||
try { this._root && this._root.fill(0); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return { SolWallet, NETWORKS };
|
||||
};
|
||||
10
bundled-addons/bchwallet/lib/dgb/core/address.d.ts
vendored
Normal file
10
bundled-addons/bchwallet/lib/dgb/core/address.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { BIP32Interface } from 'bip32';
|
||||
import { type Network } from './network.js';
|
||||
export declare function p2pkhAddress(node: BIP32Interface, network?: Network): string;
|
||||
export declare function p2shP2wpkhAddress(node: BIP32Interface, network?: Network): string;
|
||||
export declare function p2shP2wpkhAddressesBoth(node: BIP32Interface): {
|
||||
modern: string;
|
||||
legacy: string;
|
||||
};
|
||||
export declare function p2wpkhAddress(node: BIP32Interface, network?: Network): string;
|
||||
export declare function p2trAddress(node: BIP32Interface, network?: Network): string;
|
||||
53
bundled-addons/bchwallet/lib/dgb/core/address.js
Normal file
53
bundled-addons/bchwallet/lib/dgb/core/address.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { payments, initEccLib } from 'bitcoinjs-lib';
|
||||
import * as ecc from '@bitcoinerlab/secp256k1';
|
||||
import { digibyte, digibyteLegacyP2SH } from './network.js';
|
||||
// Required for Taproot address derivation (P2TR).
|
||||
initEccLib(ecc);
|
||||
export function p2pkhAddress(node, network = digibyte) {
|
||||
const { address } = payments.p2pkh({
|
||||
pubkey: Buffer.from(node.publicKey),
|
||||
network,
|
||||
});
|
||||
if (!address)
|
||||
throw new Error('p2pkh derivation returned no address');
|
||||
return address;
|
||||
}
|
||||
export function p2shP2wpkhAddress(node, network = digibyte) {
|
||||
const redeem = payments.p2wpkh({
|
||||
pubkey: Buffer.from(node.publicKey),
|
||||
network,
|
||||
});
|
||||
const { address } = payments.p2sh({ redeem, network });
|
||||
if (!address)
|
||||
throw new Error('p2sh-p2wpkh derivation returned no address');
|
||||
return address;
|
||||
}
|
||||
// Derive the "S..." (current DGB, scriptHash 0x3f) AND "3..." (legacy
|
||||
// Bitcoin-compatible, scriptHash 0x05) BIP49 addresses for the same
|
||||
// key. Ian Coleman's BIP39 tool and several older wallets generate the
|
||||
// legacy "3..." variant, so any seed-recovery scan must check both.
|
||||
export function p2shP2wpkhAddressesBoth(node) {
|
||||
return {
|
||||
modern: p2shP2wpkhAddress(node, digibyte),
|
||||
legacy: p2shP2wpkhAddress(node, digibyteLegacyP2SH),
|
||||
};
|
||||
}
|
||||
export function p2wpkhAddress(node, network = digibyte) {
|
||||
const { address } = payments.p2wpkh({
|
||||
pubkey: Buffer.from(node.publicKey),
|
||||
network,
|
||||
});
|
||||
if (!address)
|
||||
throw new Error('p2wpkh derivation returned no address');
|
||||
return address;
|
||||
}
|
||||
// BIP86 Taproot address using the x-only pubkey with no script tree,
|
||||
// which applies the standard BIP86 tweak internally in bitcoinjs-lib.
|
||||
export function p2trAddress(node, network = digibyte) {
|
||||
const internalPubkey = Buffer.from(node.publicKey.subarray(1, 33));
|
||||
const { address } = payments.p2tr({ internalPubkey, network });
|
||||
if (!address)
|
||||
throw new Error('p2tr derivation returned no address');
|
||||
return address;
|
||||
}
|
||||
//# sourceMappingURL=address.js.map
|
||||
5
bundled-addons/bchwallet/lib/dgb/core/descriptor.d.ts
vendored
Normal file
5
bundled-addons/bchwallet/lib/dgb/core/descriptor.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { BIP32Interface } from 'bip32';
|
||||
import type { Purpose } from './hd.js';
|
||||
export declare function derivationPath(purpose: Purpose, account: number, change: 0 | 1, index: number): string;
|
||||
export declare function accountXpub(accountNode: BIP32Interface): string;
|
||||
export declare function accountXprv(accountNode: BIP32Interface): string;
|
||||
18
bundled-addons/bchwallet/lib/dgb/core/descriptor.js
Normal file
18
bundled-addons/bchwallet/lib/dgb/core/descriptor.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { DGB_COIN_TYPE } from './network.js';
|
||||
// Convenience helper: full derivation-path string for a specific
|
||||
// address, e.g. m/84'/20'/0'/0/5.
|
||||
export function derivationPath(purpose, account, change, index) {
|
||||
return `m/${purpose}'/${DGB_COIN_TYPE}'/${account}'/${change}/${index}`;
|
||||
}
|
||||
// xpub for an account, ready to be shared with a watch-only or
|
||||
// external indexer. Never share the corresponding xprv.
|
||||
export function accountXpub(accountNode) {
|
||||
return accountNode.neutered().toBase58();
|
||||
}
|
||||
export function accountXprv(accountNode) {
|
||||
if (!accountNode.privateKey) {
|
||||
throw new Error('Node has no private key; cannot export xprv');
|
||||
}
|
||||
return accountNode.toBase58();
|
||||
}
|
||||
//# sourceMappingURL=descriptor.js.map
|
||||
8
bundled-addons/bchwallet/lib/dgb/core/hd.d.ts
vendored
Normal file
8
bundled-addons/bchwallet/lib/dgb/core/hd.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { type BIP32Interface } from 'bip32';
|
||||
import { type Network } from './network.js';
|
||||
export type Purpose = 44 | 49 | 84 | 86;
|
||||
export declare const PURPOSE_LABEL: Record<Purpose, string>;
|
||||
export declare function rootFromSeed(seed: Buffer, network?: Network): BIP32Interface;
|
||||
export declare function accountNode(root: BIP32Interface, purpose: Purpose, account?: number): BIP32Interface;
|
||||
export declare function addressNode(account: BIP32Interface, change: 0 | 1, index: number): BIP32Interface;
|
||||
export type { BIP32Interface };
|
||||
24
bundled-addons/bchwallet/lib/dgb/core/hd.js
Normal file
24
bundled-addons/bchwallet/lib/dgb/core/hd.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { BIP32Factory } from 'bip32';
|
||||
import * as ecc from '@bitcoinerlab/secp256k1';
|
||||
import { digibyte, DGB_COIN_TYPE } from './network.js';
|
||||
const bip32 = BIP32Factory(ecc);
|
||||
export const PURPOSE_LABEL = {
|
||||
44: 'BIP44 legacy P2PKH',
|
||||
49: 'BIP49 P2SH-wrapped SegWit',
|
||||
84: 'BIP84 native SegWit v0',
|
||||
86: 'BIP86 Taproot (SegWit v1)',
|
||||
};
|
||||
export function rootFromSeed(seed, network = digibyte) {
|
||||
return bip32.fromSeed(seed, network);
|
||||
}
|
||||
// Standard account-level derivation: m/purpose'/coin'/account'.
|
||||
// account defaults to 0 (the first account).
|
||||
export function accountNode(root, purpose, account = 0) {
|
||||
return root.derivePath(`m/${purpose}'/${DGB_COIN_TYPE}'/${account}'`);
|
||||
}
|
||||
// Address-level derivation from an account node.
|
||||
// change = 0 for external (receive) addresses, 1 for internal (change).
|
||||
export function addressNode(account, change, index) {
|
||||
return account.derive(change).derive(index);
|
||||
}
|
||||
//# sourceMappingURL=hd.js.map
|
||||
6
bundled-addons/bchwallet/lib/dgb/core/index.d.ts
vendored
Normal file
6
bundled-addons/bchwallet/lib/dgb/core/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export * from './network.js';
|
||||
export * from './seed.js';
|
||||
export * from './hd.js';
|
||||
export * from './address.js';
|
||||
export * from './wif.js';
|
||||
export * from './descriptor.js';
|
||||
7
bundled-addons/bchwallet/lib/dgb/core/index.js
Normal file
7
bundled-addons/bchwallet/lib/dgb/core/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * from './network.js';
|
||||
export * from './seed.js';
|
||||
export * from './hd.js';
|
||||
export * from './address.js';
|
||||
export * from './wif.js';
|
||||
export * from './descriptor.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
13
bundled-addons/bchwallet/lib/dgb/core/network.d.ts
vendored
Normal file
13
bundled-addons/bchwallet/lib/dgb/core/network.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { networks } from 'bitcoinjs-lib';
|
||||
export type Network = (typeof networks)['bitcoin'];
|
||||
export declare const digibyte: Network;
|
||||
export declare const digibyteLegacyP2SH: Network;
|
||||
export declare const digibyteLegacyWIF: Network;
|
||||
export declare const digibyteTestnet: Network;
|
||||
export declare const DGB_COIN_TYPE = 20;
|
||||
export declare const DGB_P2P: {
|
||||
readonly magic: 3669410810;
|
||||
readonly defaultPort: 12024;
|
||||
readonly rpcPort: 14022;
|
||||
readonly dnsSeeds: readonly ["seed.digibyte.io", "seed.diginode.tools", "seed.digibyte.link", "seed.aroundtheblock.app", "seed.tuyul.cc"];
|
||||
};
|
||||
74
bundled-addons/bchwallet/lib/dgb/core/network.js
Normal file
74
bundled-addons/bchwallet/lib/dgb/core/network.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// DigiByte mainnet parameters. Sourced from
|
||||
// github.com/DigiByte-Core/digibyte src/kernel/chainparams.cpp CMainParams.
|
||||
//
|
||||
// Note on scriptHash: DGB Core defines BOTH SCRIPT_ADDRESS = 0x3f ("S..."
|
||||
// addresses, the current default) and SCRIPT_ADDRESS2 = 0x05 ("3..."
|
||||
// addresses, kept for Bitcoin compatibility). Older BIP39 tools and
|
||||
// wallets that forked bitcoinjs-lib params generate 3-addresses; current
|
||||
// DGB Core generates S-addresses. `digibyte` below uses the current
|
||||
// default; use `digibyteLegacyP2SH` when recovering from Ian Coleman's
|
||||
// BIP39 tool, older Atomic/Exodus vintages, or anything else that
|
||||
// inherited Bitcoin's 0x05 P2SH byte. Both are valid on-chain.
|
||||
export const digibyte = {
|
||||
messagePrefix: '\x19DigiByte Signed Message:\n',
|
||||
bech32: 'dgb',
|
||||
bip32: {
|
||||
public: 0x0488b21e,
|
||||
private: 0x0488ade4,
|
||||
},
|
||||
pubKeyHash: 0x1e,
|
||||
scriptHash: 0x3f,
|
||||
wif: 0x80,
|
||||
};
|
||||
// Same as `digibyte`, but with the legacy Bitcoin-compatible P2SH
|
||||
// version byte. Use for producing/scanning "3..."-style P2SH addresses
|
||||
// during seed recovery from tools that predate the 0x3f switch.
|
||||
// DGB Core keeps SCRIPT_ADDRESS_OLD = 5 for backward compatibility.
|
||||
export const digibyteLegacyP2SH = {
|
||||
...digibyte,
|
||||
scriptHash: 0x05,
|
||||
};
|
||||
// Same as `digibyte`, but with the legacy WIF version byte.
|
||||
// DGB Core keeps SECRET_KEY_OLD = 158 (0x9e) for backward compatibility.
|
||||
// Older DGB tools may have exported private keys with this prefix.
|
||||
// The wallet's WIF-import path should try both `digibyte` and this
|
||||
// variant before rejecting a key.
|
||||
export const digibyteLegacyWIF = {
|
||||
...digibyte,
|
||||
wif: 0x9e,
|
||||
};
|
||||
// DigiByte testnet parameters. From CTestNetParams in chainparams.cpp.
|
||||
export const digibyteTestnet = {
|
||||
messagePrefix: '\x19DigiByte Signed Message:\n',
|
||||
bech32: 'dgbt',
|
||||
bip32: {
|
||||
public: 0x043587cf,
|
||||
private: 0x04358394,
|
||||
},
|
||||
pubKeyHash: 0x7e,
|
||||
scriptHash: 0x8c,
|
||||
wif: 0xef,
|
||||
};
|
||||
// SLIP-0044 registered coin type.
|
||||
export const DGB_COIN_TYPE = 20;
|
||||
// P2P network constants (unused by @dgb-wallet/core itself, exposed for
|
||||
// the P2P client package that will consume them).
|
||||
export const DGB_P2P = {
|
||||
// pchMessageStart in DGB Core chainparams.cpp is the byte sequence
|
||||
// 0xFA 0xC3 0xB6 0xDA on the wire. `writeUInt32LE(magic)` writes
|
||||
// least-significant-byte first, so the integer value stored here must
|
||||
// be the LE-reading: 0xDAB6C3FA (byte 0 = 0xFA, byte 3 = 0xDA).
|
||||
// Getting this wrong means peers see wrong-network frames and close
|
||||
// immediately at handshake.
|
||||
magic: 0xdab6c3fa,
|
||||
defaultPort: 12024,
|
||||
rpcPort: 14022,
|
||||
dnsSeeds: [
|
||||
'seed.digibyte.io',
|
||||
'seed.diginode.tools',
|
||||
'seed.digibyte.link',
|
||||
'seed.aroundtheblock.app',
|
||||
'seed.tuyul.cc',
|
||||
],
|
||||
};
|
||||
//# sourceMappingURL=network.js.map
|
||||
1
bundled-addons/bchwallet/lib/dgb/core/package.json
Normal file
1
bundled-addons/bchwallet/lib/dgb/core/package.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"type":"module","main":"./index.js"}
|
||||
4
bundled-addons/bchwallet/lib/dgb/core/seed.d.ts
vendored
Normal file
4
bundled-addons/bchwallet/lib/dgb/core/seed.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export type SeedStrength = 128 | 160 | 192 | 224 | 256;
|
||||
export declare function generateSeedPhrase(strength?: SeedStrength): string;
|
||||
export declare function validateSeedPhrase(phrase: string, wordlist?: string[]): boolean;
|
||||
export declare function seedFromPhrase(phrase: string, passphrase?: string): Promise<Buffer>;
|
||||
17
bundled-addons/bchwallet/lib/dgb/core/seed.js
Normal file
17
bundled-addons/bchwallet/lib/dgb/core/seed.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { generateMnemonic, validateMnemonic, mnemonicToSeed, wordlists, } from 'bip39';
|
||||
// Word count → entropy strength for BIP39.
|
||||
// 12 → 128, 15 → 160, 18 → 192, 21 → 224, 24 → 256.
|
||||
export function generateSeedPhrase(strength = 128) {
|
||||
return generateMnemonic(strength);
|
||||
}
|
||||
// BIP39 checksum + wordlist validation. Returns false for typos, bad
|
||||
// word counts, and out-of-wordlist words.
|
||||
export function validateSeedPhrase(phrase, wordlist = wordlists.english) {
|
||||
return validateMnemonic(phrase.trim(), wordlist);
|
||||
}
|
||||
// BIP39 seed derivation. Passphrase is the optional "25th word";
|
||||
// changing it produces a different wallet from the same mnemonic.
|
||||
export async function seedFromPhrase(phrase, passphrase = '') {
|
||||
return mnemonicToSeed(phrase.trim(), passphrase);
|
||||
}
|
||||
//# sourceMappingURL=seed.js.map
|
||||
27
bundled-addons/bchwallet/lib/dgb/core/wif.d.ts
vendored
Normal file
27
bundled-addons/bchwallet/lib/dgb/core/wif.d.ts
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { type ECPairInterface } from 'ecpair';
|
||||
import { type Network } from './network.js';
|
||||
export type WifImportResult = {
|
||||
keyPair: ECPairInterface;
|
||||
variant: 'modern' | 'legacy';
|
||||
};
|
||||
export declare function importWif(wif: string): WifImportResult;
|
||||
export declare function importWifStrict(wif: string, network?: Network): ECPairInterface;
|
||||
export declare function exportWif(keyPair: ECPairInterface): string;
|
||||
export interface PubkeyAddresses {
|
||||
p2pkh: string;
|
||||
p2shP2wpkhModern: string;
|
||||
p2shP2wpkhLegacy: string;
|
||||
p2wpkh: string;
|
||||
p2tr: string;
|
||||
scripts: {
|
||||
p2pkh: string;
|
||||
p2shP2wpkh: string;
|
||||
p2wpkh: string;
|
||||
p2tr: string;
|
||||
};
|
||||
}
|
||||
export declare function addressesForPubkey(pubkey: Buffer, network?: Network): PubkeyAddresses;
|
||||
export interface WifImportWithAddresses extends WifImportResult, PubkeyAddresses {
|
||||
}
|
||||
export declare function importWifWithAddresses(wif: string): WifImportWithAddresses;
|
||||
export type { ECPairInterface };
|
||||
72
bundled-addons/bchwallet/lib/dgb/core/wif.js
Normal file
72
bundled-addons/bchwallet/lib/dgb/core/wif.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { ECPairFactory } from 'ecpair';
|
||||
import * as ecc from '@bitcoinerlab/secp256k1';
|
||||
import { payments, initEccLib } from 'bitcoinjs-lib';
|
||||
import { digibyte, digibyteLegacyP2SH, digibyteLegacyWIF } from './network.js';
|
||||
const ECPair = ECPairFactory(ecc);
|
||||
initEccLib(ecc);
|
||||
// Import a DigiByte private key in WIF (Wallet Import Format).
|
||||
// Tries the modern 0x80 prefix first, falls back to the legacy 0x9e
|
||||
// prefix that older DGB tools produced. Rejects anything else with a
|
||||
// clear message that identifies the network mismatch.
|
||||
export function importWif(wif) {
|
||||
const trimmed = wif.trim();
|
||||
try {
|
||||
return { keyPair: ECPair.fromWIF(trimmed, digibyte), variant: 'modern' };
|
||||
}
|
||||
catch {
|
||||
// fall through to legacy attempt
|
||||
}
|
||||
try {
|
||||
return { keyPair: ECPair.fromWIF(trimmed, digibyteLegacyWIF), variant: 'legacy' };
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Not a valid DigiByte WIF (tried both current 0x80 and legacy 0x9e prefixes). ` +
|
||||
`Underlying error: ${e.message}. Check the key is for DGB mainnet, not testnet or another chain.`);
|
||||
}
|
||||
}
|
||||
// Import against a specific network only (advanced / testing).
|
||||
export function importWifStrict(wif, network = digibyte) {
|
||||
return ECPair.fromWIF(wif.trim(), network);
|
||||
}
|
||||
export function exportWif(keyPair) {
|
||||
return keyPair.toWIF();
|
||||
}
|
||||
export function addressesForPubkey(pubkey, network = digibyte) {
|
||||
const p2pkh = payments.p2pkh({ pubkey, network });
|
||||
const wpkhRedeem = payments.p2wpkh({ pubkey, network });
|
||||
// bitcoinjs-lib enforces `redeem.network === outerNetwork` (identity
|
||||
// comparison, not shape). To render the legacy 3-prefix P2SH address
|
||||
// we need a fresh redeem whose .network property is the legacy variant
|
||||
// — same bytes on the wire, different object identity.
|
||||
const wpkhRedeemLegacy = payments.p2wpkh({ pubkey, network: digibyteLegacyP2SH });
|
||||
const p2shModern = payments.p2sh({ redeem: wpkhRedeem, network });
|
||||
const p2shLegacy = payments.p2sh({ redeem: wpkhRedeemLegacy, network: digibyteLegacyP2SH });
|
||||
const p2wpkh = payments.p2wpkh({ pubkey, network });
|
||||
const p2tr = payments.p2tr({ internalPubkey: pubkey.subarray(1, 33), network });
|
||||
if (!p2pkh.address || !p2shModern.address || !p2shLegacy.address || !p2wpkh.address || !p2tr.address) {
|
||||
throw new Error('bitcoinjs-lib returned an empty address for one of the payment types');
|
||||
}
|
||||
if (!p2pkh.output || !p2shModern.output || !p2wpkh.output || !p2tr.output) {
|
||||
throw new Error('bitcoinjs-lib returned an empty scriptPubKey for one of the payment types');
|
||||
}
|
||||
return {
|
||||
p2pkh: p2pkh.address,
|
||||
p2shP2wpkhModern: p2shModern.address,
|
||||
p2shP2wpkhLegacy: p2shLegacy.address,
|
||||
p2wpkh: p2wpkh.address,
|
||||
p2tr: p2tr.address,
|
||||
scripts: {
|
||||
p2pkh: Buffer.from(p2pkh.output).toString('hex'),
|
||||
p2shP2wpkh: Buffer.from(p2shModern.output).toString('hex'),
|
||||
p2wpkh: Buffer.from(p2wpkh.output).toString('hex'),
|
||||
p2tr: Buffer.from(p2tr.output).toString('hex'),
|
||||
},
|
||||
};
|
||||
}
|
||||
export function importWifWithAddresses(wif) {
|
||||
const imported = importWif(wif);
|
||||
const pubkey = Buffer.from(imported.keyPair.publicKey);
|
||||
const addrs = addressesForPubkey(pubkey);
|
||||
return { ...imported, ...addrs };
|
||||
}
|
||||
//# sourceMappingURL=wif.js.map
|
||||
4
bundled-addons/bchwallet/lib/dgb/psbt/build.d.ts
vendored
Normal file
4
bundled-addons/bchwallet/lib/dgb/psbt/build.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { Psbt } from 'bitcoinjs-lib';
|
||||
import { type Network } from '@dgb-wallet/core';
|
||||
import type { BuildParams } from './types.js';
|
||||
export declare function buildPsbt(params: BuildParams, network?: Network): Psbt;
|
||||
56
bundled-addons/bchwallet/lib/dgb/psbt/build.js
Normal file
56
bundled-addons/bchwallet/lib/dgb/psbt/build.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { Psbt } from 'bitcoinjs-lib';
|
||||
import { digibyte } from '../core/index.js';
|
||||
// Construct an unsigned PSBT from a set of UTXOs and destination outputs.
|
||||
// Does not add a change output — the caller decides change amount and
|
||||
// address. Does not compute fees — the caller must have already subtracted
|
||||
// fee from outputs.
|
||||
export function buildPsbt(params, network = digibyte) {
|
||||
const psbt = new Psbt({ network });
|
||||
const inputs = params.sortBip69 === false ? params.inputs : sortInputs(params.inputs);
|
||||
const outputs = params.sortBip69 === false ? params.outputs : sortOutputs(params.outputs);
|
||||
for (const u of inputs) {
|
||||
psbt.addInput(inputToPsbtInput(u));
|
||||
}
|
||||
for (const o of outputs) {
|
||||
psbt.addOutput({ address: o.address, value: o.value });
|
||||
}
|
||||
return psbt;
|
||||
}
|
||||
function inputToPsbtInput(u) {
|
||||
const input = {
|
||||
hash: u.txid,
|
||||
index: u.vout,
|
||||
};
|
||||
if (u.witness) {
|
||||
input.witnessUtxo = {
|
||||
script: Buffer.from(u.witness.scriptHex, 'hex'),
|
||||
value: u.witness.value,
|
||||
};
|
||||
}
|
||||
if (u.nonWitnessTxHex) {
|
||||
input.nonWitnessUtxo = Buffer.from(u.nonWitnessTxHex, 'hex');
|
||||
}
|
||||
if (u.redeemScriptHex) {
|
||||
input.redeemScript = Buffer.from(u.redeemScriptHex, 'hex');
|
||||
}
|
||||
if (u.tapInternalKeyHex) {
|
||||
input.tapInternalKey = Buffer.from(u.tapInternalKeyHex, 'hex');
|
||||
}
|
||||
return input;
|
||||
}
|
||||
// BIP69 lexicographic ordering. Improves privacy by not revealing input
|
||||
// selection order (which can hint at wallet coin-selection strategy).
|
||||
function sortInputs(inputs) {
|
||||
return [...inputs].sort((a, b) => {
|
||||
const cmp = a.txid.localeCompare(b.txid);
|
||||
return cmp !== 0 ? cmp : a.vout - b.vout;
|
||||
});
|
||||
}
|
||||
function sortOutputs(outputs) {
|
||||
return [...outputs].sort((a, b) => {
|
||||
if (a.value !== b.value)
|
||||
return a.value - b.value;
|
||||
return a.address.localeCompare(b.address);
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=build.js.map
|
||||
3
bundled-addons/bchwallet/lib/dgb/psbt/index.d.ts
vendored
Normal file
3
bundled-addons/bchwallet/lib/dgb/psbt/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './types.js';
|
||||
export * from './build.js';
|
||||
export * from './sign.js';
|
||||
4
bundled-addons/bchwallet/lib/dgb/psbt/index.js
Normal file
4
bundled-addons/bchwallet/lib/dgb/psbt/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from './types.js';
|
||||
export * from './build.js';
|
||||
export * from './sign.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
bundled-addons/bchwallet/lib/dgb/psbt/package.json
Normal file
1
bundled-addons/bchwallet/lib/dgb/psbt/package.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"type":"module","main":"./index.js"}
|
||||
9
bundled-addons/bchwallet/lib/dgb/psbt/sign.d.ts
vendored
Normal file
9
bundled-addons/bchwallet/lib/dgb/psbt/sign.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { Psbt } from 'bitcoinjs-lib';
|
||||
import type { ECPairInterface } from 'ecpair';
|
||||
export declare function signAllInputs(psbt: Psbt, keyPair: ECPairInterface): Psbt;
|
||||
export declare function finalizeAndExtract(psbt: Psbt): {
|
||||
hex: string;
|
||||
txid: string;
|
||||
};
|
||||
export declare function feeSats(psbt: Psbt): number;
|
||||
export declare function feeRateSatsPerByte(psbt: Psbt): number;
|
||||
25
bundled-addons/bchwallet/lib/dgb/psbt/sign.js
Normal file
25
bundled-addons/bchwallet/lib/dgb/psbt/sign.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Sign every input of a PSBT with the given key. Fails loudly if any
|
||||
// input can't be signed by this key (rather than silently leaving it
|
||||
// unsigned), so callers notice before broadcasting a half-signed tx.
|
||||
export function signAllInputs(psbt, keyPair) {
|
||||
psbt.signAllInputs(keyPair);
|
||||
return psbt;
|
||||
}
|
||||
// After all inputs are signed by all necessary parties, finalize and
|
||||
// extract the network-ready hex-encoded transaction.
|
||||
export function finalizeAndExtract(psbt) {
|
||||
psbt.finalizeAllInputs();
|
||||
const tx = psbt.extractTransaction();
|
||||
return { hex: tx.toHex(), txid: tx.getId() };
|
||||
}
|
||||
// Fee computed from the difference between total input value and total
|
||||
// output value. Requires all inputs to have witnessUtxo or
|
||||
// nonWitnessUtxo populated (which `buildPsbt` in this package
|
||||
// guarantees when the caller populates Utxo.value fields).
|
||||
export function feeSats(psbt) {
|
||||
return psbt.getFee();
|
||||
}
|
||||
export function feeRateSatsPerByte(psbt) {
|
||||
return psbt.getFeeRate();
|
||||
}
|
||||
//# sourceMappingURL=sign.js.map
|
||||
23
bundled-addons/bchwallet/lib/dgb/psbt/types.d.ts
vendored
Normal file
23
bundled-addons/bchwallet/lib/dgb/psbt/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export interface Utxo {
|
||||
txid: string;
|
||||
vout: number;
|
||||
value: number;
|
||||
address: string;
|
||||
scriptPubKey: string;
|
||||
witness?: {
|
||||
scriptHex: string;
|
||||
value: number;
|
||||
};
|
||||
nonWitnessTxHex?: string;
|
||||
redeemScriptHex?: string;
|
||||
tapInternalKeyHex?: string;
|
||||
}
|
||||
export interface Output {
|
||||
address: string;
|
||||
value: number;
|
||||
}
|
||||
export interface BuildParams {
|
||||
inputs: Utxo[];
|
||||
outputs: Output[];
|
||||
sortBip69?: boolean;
|
||||
}
|
||||
2
bundled-addons/bchwallet/lib/dgb/psbt/types.js
Normal file
2
bundled-addons/bchwallet/lib/dgb/psbt/types.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
143
bundled-addons/bchwallet/lib/eip712.js
Normal file
143
bundled-addons/bchwallet/lib/eip712.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// EIP-712 typed-data hashing (personal_sign's structured cousin). Produces
|
||||
// the 32-byte digest that eth_signTypedData_v4 signs with the wallet's
|
||||
// secp256k1 key.
|
||||
//
|
||||
// Reference: https://eips.ethereum.org/EIPS/eip-712
|
||||
// Digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct(primaryType, message))
|
||||
// - domainSeparator = hashStruct("EIP712Domain", typedData.domain)
|
||||
// - hashStruct(type, data) = keccak256(typeHash(type) || encodeData(type, data))
|
||||
// - typeHash(type) = keccak256(encodeType(type))
|
||||
// - encodeType is the canonical string form; sub-types are appended in
|
||||
// alphabetical order once, without recursion into themselves twice.
|
||||
//
|
||||
// This is enough for every mainstream EIP-712 payload — Permit / EIP-2612,
|
||||
// OpenSea order signatures, WalletConnect handshakes, Snapshot votes. Not
|
||||
// implemented: fixed-size arrays of atomic types wider than a byte (rare
|
||||
// enough that no shipping dapp we care about uses them).
|
||||
|
||||
module.exports = function makeEip712({ keccak_256 }) {
|
||||
const enc = new TextEncoder();
|
||||
const concat = (...ps) => {
|
||||
const n = ps.reduce((a, p) => a + p.length, 0);
|
||||
const out = new Uint8Array(n); let k = 0;
|
||||
for (const p of ps) { out.set(p, k); k += p.length; }
|
||||
return out;
|
||||
};
|
||||
const hex2bytes = (h) => {
|
||||
const s = String(h).replace(/^0x/i, "");
|
||||
if (s.length % 2) throw new Error("hex: odd length");
|
||||
const out = new Uint8Array(s.length / 2);
|
||||
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
||||
return out;
|
||||
};
|
||||
const bytesToBig = (b) => {
|
||||
let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v;
|
||||
};
|
||||
const bigToBe32 = (v, signed) => {
|
||||
let n = BigInt(v);
|
||||
if (n < 0n) {
|
||||
if (!signed) throw new Error("negative value for unsigned type");
|
||||
// two's complement to 256 bits
|
||||
n = (1n << 256n) + n;
|
||||
}
|
||||
const out = new Uint8Array(32);
|
||||
for (let i = 31; i >= 0; i--) { out[i] = Number(n & 0xffn); n >>= 8n; }
|
||||
return out;
|
||||
};
|
||||
|
||||
// encodeType walker — resolves the primary type + every struct it
|
||||
// transitively references, then emits "Primary(...)Sub1(...)Sub2(...)"
|
||||
// with sub-types in alphabetical order per the spec.
|
||||
function findDependencies(primaryType, types, found = new Set()) {
|
||||
if (found.has(primaryType) || !types[primaryType]) return found;
|
||||
found.add(primaryType);
|
||||
for (const f of types[primaryType]) {
|
||||
const base = f.type.replace(/\[.*\]$/, "");
|
||||
if (types[base]) findDependencies(base, types, found);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
function encodeType(primaryType, types) {
|
||||
const deps = [...findDependencies(primaryType, types)].filter((t) => t !== primaryType).sort();
|
||||
const all = [primaryType, ...deps];
|
||||
return all.map((t) => `${t}(${types[t].map((f) => `${f.type} ${f.name}`).join(",")})`).join("");
|
||||
}
|
||||
function typeHash(primaryType, types) {
|
||||
return keccak_256(enc.encode(encodeType(primaryType, types)));
|
||||
}
|
||||
|
||||
// Encode one field value per its declared type. Struct + array types
|
||||
// hash themselves to 32 bytes; atomics land in a 32-byte slot each.
|
||||
function encodeValue(type, value, types) {
|
||||
// Array types: `Type[]` (dynamic) or `Type[N]` (fixed) — both encode
|
||||
// as keccak256(concat(encodeValue(baseType, element)...)) per EIP-712.
|
||||
const arr = /^(.+)\[(\d*)\]$/.exec(type);
|
||||
if (arr) {
|
||||
const baseType = arr[1];
|
||||
const items = Array.isArray(value) ? value : [];
|
||||
const encoded = items.map((v) => encodeValue(baseType, v, types));
|
||||
return keccak_256(concat(...encoded));
|
||||
}
|
||||
// Struct types: hashStruct recursion.
|
||||
if (types[type]) return hashStruct(type, value, types);
|
||||
// Atomic types.
|
||||
if (type === "string") return keccak_256(enc.encode(String(value ?? "")));
|
||||
if (type === "bytes") {
|
||||
const b = typeof value === "string" ? hex2bytes(value) : Uint8Array.from(value || []);
|
||||
return keccak_256(b);
|
||||
}
|
||||
if (type === "address") {
|
||||
const h = hex2bytes(String(value || "0x0").replace(/^0x/, ""));
|
||||
if (h.length !== 20) throw new Error("address must be 20 bytes");
|
||||
const out = new Uint8Array(32);
|
||||
out.set(h, 12);
|
||||
return out;
|
||||
}
|
||||
if (type === "bool") {
|
||||
const out = new Uint8Array(32);
|
||||
out[31] = value ? 1 : 0;
|
||||
return out;
|
||||
}
|
||||
// bytesN (fixed): left-aligned in a 32-byte word.
|
||||
const bytesN = /^bytes(\d+)$/.exec(type);
|
||||
if (bytesN) {
|
||||
const n = Number(bytesN[1]);
|
||||
if (n < 1 || n > 32) throw new Error("bytesN out of range");
|
||||
const b = typeof value === "string" ? hex2bytes(value) : Uint8Array.from(value || []);
|
||||
if (b.length !== n) throw new Error(`${type} expects ${n} bytes, got ${b.length}`);
|
||||
const out = new Uint8Array(32);
|
||||
out.set(b, 0);
|
||||
return out;
|
||||
}
|
||||
// uint* / int*: encode as 32-byte big-endian.
|
||||
const uintM = /^uint(\d*)$/.exec(type);
|
||||
if (uintM) return bigToBe32(value, false);
|
||||
const intM = /^int(\d*)$/.exec(type);
|
||||
if (intM) return bigToBe32(value, true);
|
||||
throw new Error("unsupported EIP-712 type: " + type);
|
||||
}
|
||||
|
||||
function encodeData(primaryType, data, types) {
|
||||
const fields = types[primaryType];
|
||||
if (!fields) throw new Error("unknown type: " + primaryType);
|
||||
const encoded = fields.map((f) => encodeValue(f.type, data ? data[f.name] : undefined, types));
|
||||
return concat(...encoded);
|
||||
}
|
||||
function hashStruct(primaryType, data, types) {
|
||||
return keccak_256(concat(typeHash(primaryType, types), encodeData(primaryType, data, types)));
|
||||
}
|
||||
|
||||
// Full EIP-712 digest, ready for secp256k1.sign(digest, key).
|
||||
function digest(typedData) {
|
||||
const td = typedData && typeof typedData === "object" ? typedData : {};
|
||||
const types = td.types || {};
|
||||
if (!types.EIP712Domain) throw new Error("typedData.types.EIP712Domain missing");
|
||||
const primary = String(td.primaryType || "");
|
||||
if (!primary || !types[primary]) throw new Error(`typedData.primaryType "${primary}" not in types`);
|
||||
const domainSeparator = hashStruct("EIP712Domain", td.domain || {}, types);
|
||||
const messageHash = hashStruct(primary, td.message || {}, types);
|
||||
return keccak_256(concat(Uint8Array.from([0x19, 0x01]), domainSeparator, messageHash));
|
||||
}
|
||||
|
||||
return { digest, encodeType, typeHash, hashStruct };
|
||||
};
|
||||
29
bundled-addons/bchwallet/lib/sia/keys.js
Normal file
29
bundled-addons/bchwallet/lib/sia/keys.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Key tree for the Sia wallet: index i -> ed25519 key via walletd's
|
||||
// KeyFromSeed(root, i), address = standard unlock hash of the public key.
|
||||
// Private keys stay inside this module; sign() is the only way out.
|
||||
module.exports = function makeKeys({ sia }) {
|
||||
class WalletKeys {
|
||||
constructor(root32) {
|
||||
this._root = Uint8Array.from(root32);
|
||||
this._cache = new Map();
|
||||
}
|
||||
entry(index) {
|
||||
let e = this._cache.get(index);
|
||||
if (!e) {
|
||||
const k = sia.keyFromSeed(this._root, index);
|
||||
e = { index, pub: k.pub, address32: k.address32, address: k.address, _priv: k.priv };
|
||||
this._cache.set(index, e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
sign(entry, msg) { return sia.sign(entry._priv, msg); }
|
||||
// Revealed only on explicit user action in Settings.
|
||||
get seedHex() { return sia.toHex(this._root); }
|
||||
wipe() {
|
||||
for (const e of this._cache.values()) e._priv.fill(0);
|
||||
this._cache.clear();
|
||||
this._root.fill(0);
|
||||
}
|
||||
}
|
||||
return { WalletKeys };
|
||||
};
|
||||
142
bundled-addons/bchwallet/lib/sia/sia.js
Normal file
142
bundled-addons/bchwallet/lib/sia/sia.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// Sia (v2 era) primitives: the Sia binary encoder, standard unlock-hash
|
||||
// addresses, walletd's per-index key derivation, the v2 input signature hash
|
||||
// and transaction weight. Mirrors go.sia.tech/core/types; every encoding
|
||||
// here was checked against a real mainnet v2 transaction.
|
||||
module.exports = function makeSia({ ed25519, blake2b }) {
|
||||
const b256 = (data) => blake2b(data, { dkLen: 32 });
|
||||
const enc = new TextEncoder();
|
||||
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
||||
const fromHex = (h) => Uint8Array.from(String(h).replace(/^0x/, "").match(/../g) || [], (x) => parseInt(x, 16));
|
||||
|
||||
// ---- encoder ---------------------------------------------------------------
|
||||
class Encoder {
|
||||
constructor() { this.parts = []; this.length = 0; }
|
||||
write(b) { this.parts.push(b); this.length += b.length; return this; }
|
||||
u8(n) { return this.write(Uint8Array.from([n & 0xff])); }
|
||||
bool(v) { return this.u8(v ? 1 : 0); }
|
||||
u64(n) {
|
||||
let v = BigInt(n); const out = new Uint8Array(8);
|
||||
for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; }
|
||||
return this.write(out);
|
||||
}
|
||||
bytes(b) { return this.u64(b.length).write(b); }
|
||||
str(s) { return this.bytes(enc.encode(s)); }
|
||||
// Currency is a 128-bit little-endian pair (lo, hi).
|
||||
currency(hastings) {
|
||||
const v = BigInt(hastings);
|
||||
if (v < 0n || v >= (1n << 128n)) throw new Error("currency out of range");
|
||||
return this.u64(v & ((1n << 64n) - 1n)).u64(v >> 64n);
|
||||
}
|
||||
bytesOut() {
|
||||
const out = new Uint8Array(this.length); let o = 0;
|
||||
for (const p of this.parts) { out.set(p, o); o += p.length; }
|
||||
return out;
|
||||
}
|
||||
}
|
||||
const SPECIFIER_ED25519 = (() => { const s = new Uint8Array(16); s.set(enc.encode("ed25519")); return s; })();
|
||||
|
||||
// ---- addresses ---------------------------------------------------------------
|
||||
const LEAF = 0, NODE = 1;
|
||||
const sumPair = (a, b) => b256(Uint8Array.from([NODE, ...a, ...b]));
|
||||
const leaf = (bytes) => b256(Uint8Array.from([LEAF, ...bytes]));
|
||||
// Merkle root of the standard UnlockConditions {timelock 0, [pk], sigs 1}.
|
||||
function standardUnlockHash(pk) {
|
||||
const timelockHash = leaf(new Encoder().u64(0).bytesOut());
|
||||
const keyHash = leaf(new Encoder().write(SPECIFIER_ED25519).bytes(pk).bytesOut());
|
||||
const sigsHash = leaf(new Encoder().u64(1).bytesOut());
|
||||
return sumPair(sumPair(timelockHash, keyHash), sigsHash);
|
||||
}
|
||||
const addressString = (addr32) => toHex(addr32) + toHex(b256(addr32).slice(0, 6));
|
||||
function parseAddress(s) {
|
||||
const t = String(s || "").trim().toLowerCase().replace(/^addr:/, "");
|
||||
if (!/^[0-9a-f]{76}$/.test(t)) throw new Error("address must be 76 hex characters");
|
||||
const raw = fromHex(t);
|
||||
const body = raw.slice(0, 32);
|
||||
if (toHex(b256(body).slice(0, 6)) !== toHex(raw.slice(32))) throw new Error("address checksum is wrong");
|
||||
return { bytes: body, address: t };
|
||||
}
|
||||
|
||||
// ---- keys --------------------------------------------------------------------
|
||||
// walletd: key_i = ed25519 seed blake2b(seed32 || index u64le).
|
||||
function keyFromSeed(seed32, index) {
|
||||
const priv = b256(new Encoder().write(seed32).u64(index).bytesOut());
|
||||
const pub = ed25519.getPublicKey(priv);
|
||||
return { priv, pub, address32: standardUnlockHash(pub), address: addressString(standardUnlockHash(pub)) };
|
||||
}
|
||||
const sign = (priv, msg) => ed25519.sign(msg, priv);
|
||||
const verify = (pub, msg, sig) => ed25519.verify(sig, msg, pub);
|
||||
|
||||
// ---- v2 transactions ---------------------------------------------------------
|
||||
// tx: { inputs: [{ parentId(hex) }], outputs: [{ value(BigInt), address32 }], minerFee(BigInt) }
|
||||
// Sig hash = blake2b("sia/sig/input|" || 0x02 || V2TransactionSemantics).
|
||||
function inputSigHash(tx) {
|
||||
const e = new Encoder().write(enc.encode("sia/sig/input|")).u8(2);
|
||||
e.u64(tx.inputs.length);
|
||||
for (const i of tx.inputs) e.write(fromHex(i.parentId));
|
||||
e.u64(tx.outputs.length);
|
||||
for (const o of tx.outputs) e.currency(o.value).write(o.address32);
|
||||
e.u64(0).u64(0); // siafund inputs / outputs
|
||||
e.u64(0).u64(0).u64(0); // contracts, revisions, resolutions
|
||||
e.u64(0); // attestations
|
||||
e.bytes(new Uint8Array(0)); // arbitrary data
|
||||
e.bool(false); // new foundation address
|
||||
e.currency(tx.minerFee);
|
||||
return b256(e.bytesOut());
|
||||
}
|
||||
// Weight = length of the full V2Transaction encoding (fees are per byte).
|
||||
// Signed inputs carry the parent element with its Merkle proof, the policy
|
||||
// and one 64-byte signature.
|
||||
function weight(tx) {
|
||||
const e = new Encoder().u8(2);
|
||||
let fields = 0;
|
||||
if (tx.inputs.length) fields |= 1; if (tx.outputs.length) fields |= 2; if (tx.minerFee > 0n) fields |= 1 << 10;
|
||||
e.u64(fields);
|
||||
if (tx.inputs.length) {
|
||||
e.u64(tx.inputs.length);
|
||||
for (const i of tx.inputs) {
|
||||
e.u64(i.leafIndex || 0).u64((i.merkleProof || []).length);
|
||||
for (const p of i.merkleProof || []) e.write(fromHex(p));
|
||||
e.write(fromHex(i.parentId)).currency(i.value).write(i.address32).u64(i.maturityHeight || 0);
|
||||
// SatisfiedPolicy: version 1, op 7 (unlock conditions), uc, 1 sig, 0 preimages
|
||||
e.u8(1).u8(7).u64(0).u64(1).write(SPECIFIER_ED25519).bytes(i.pub).u64(1);
|
||||
e.u64(1).write(new Uint8Array(64)).u64(0);
|
||||
}
|
||||
}
|
||||
if (tx.outputs.length) { e.u64(tx.outputs.length); for (const o of tx.outputs) e.currency(o.value).write(o.address32); }
|
||||
if (tx.minerFee > 0n) e.currency(tx.minerFee);
|
||||
return e.length;
|
||||
}
|
||||
// walletd JSON for /api/txpool/broadcast.
|
||||
function toJson(tx, sigs) {
|
||||
return {
|
||||
siacoinInputs: tx.inputs.map((i, k) => ({
|
||||
parent: i.element,
|
||||
satisfiedPolicy: {
|
||||
policy: { type: "uc", policy: { timelock: 0, publicKeys: ["ed25519:" + toHex(i.pub)], signaturesRequired: 1 } },
|
||||
signatures: [toHex(sigs[k])],
|
||||
},
|
||||
})),
|
||||
siacoinOutputs: tx.outputs.map((o) => ({ value: o.value.toString(), address: addressString(o.address32) })),
|
||||
minerFee: tx.minerFee.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- units -------------------------------------------------------------------
|
||||
const HASTINGS_PER_SC = 10n ** 24n;
|
||||
function formatSC(hastings, decimals = 6) {
|
||||
const v = BigInt(hastings); const neg = v < 0n; const a = neg ? -v : v;
|
||||
const whole = a / HASTINGS_PER_SC;
|
||||
let frac = (a % HASTINGS_PER_SC).toString().padStart(24, "0").slice(0, decimals).replace(/0+$/, "");
|
||||
if (frac.length < 2) frac = frac.padEnd(2, "0");
|
||||
return (neg ? "-" : "") + whole.toString() + "." + frac;
|
||||
}
|
||||
function parseSC(text) {
|
||||
const s = String(text || "").trim().replace(/,/g, "");
|
||||
if (!/^\d*(\.\d*)?$/.test(s) || s === "" || s === ".") throw new Error("amount must be a number");
|
||||
const [w = "0", f = ""] = s.split(".");
|
||||
if (f.length > 24) throw new Error("too many decimals");
|
||||
return BigInt(w || "0") * HASTINGS_PER_SC + BigInt((f + "0".repeat(24)).slice(0, 24));
|
||||
}
|
||||
|
||||
return { Encoder, standardUnlockHash, addressString, parseAddress, keyFromSeed, sign, verify, inputSigHash, weight, toJson, formatSC, parseSC, HASTINGS_PER_SC, toHex, fromHex, b256 };
|
||||
};
|
||||
201
bundled-addons/bchwallet/lib/sia/wallet.js
Normal file
201
bundled-addons/bchwallet/lib/sia/wallet.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// Sia wallet state: address discovery, balance, history and v2 sends over a
|
||||
// walletd client. Keys are the addon's derived key tree; nothing here touches
|
||||
// UI or IPC. Amounts are BigInt hastings throughout.
|
||||
module.exports = function makeWallet({ client, keys, sia, storage, log = () => {}, onChange = () => {} }) {
|
||||
const GAP = 10;
|
||||
const HISTORY_LIMIT = 25;
|
||||
const state = {
|
||||
used: new Set(), // indexes with any event
|
||||
height: 0,
|
||||
balance: { confirmed: 0n, immature: 0n },
|
||||
outputs: [], // spendable SiacoinElements with { index }
|
||||
basis: null,
|
||||
history: [],
|
||||
receiveIndex: 0,
|
||||
scanning: false,
|
||||
error: null,
|
||||
feePerByte: 0n,
|
||||
};
|
||||
// Outputs we just spent stay hidden until walletd stops listing them.
|
||||
const pendingSpent = new Map(); // id -> timestamp
|
||||
|
||||
async function isUsed(entry) {
|
||||
const ev = await client.events(entry.address, 1, 0);
|
||||
return Array.isArray(ev) && ev.length > 0;
|
||||
}
|
||||
async function scan() {
|
||||
const cursor = Number(storage.get("receiveCursor", 0)) || 0;
|
||||
let gap = 0, i = 0;
|
||||
while (gap < GAP || i < cursor + GAP) {
|
||||
const e = keys.entry(i);
|
||||
if (await isUsed(e)) { state.used.add(i); gap = 0; } else gap++;
|
||||
i++;
|
||||
}
|
||||
let r = cursor;
|
||||
while (state.used.has(r)) r++;
|
||||
state.receiveIndex = r;
|
||||
}
|
||||
function watched() {
|
||||
const idx = new Set(state.used); idx.add(state.receiveIndex);
|
||||
return [...idx].map((i) => keys.entry(i));
|
||||
}
|
||||
|
||||
async function loadOutputs() {
|
||||
const tip = await client.tip();
|
||||
state.height = tip.height || 0;
|
||||
let confirmed = 0n, immature = 0n; const outs = []; let basis = null;
|
||||
for (const e of watched()) {
|
||||
for (let offset = 0; ; offset += 100) {
|
||||
const r = await client.outputs(e.address, 100, offset);
|
||||
basis = r.basis || basis;
|
||||
const list = Array.isArray(r.outputs) ? r.outputs : [];
|
||||
for (const o of list) {
|
||||
const v = BigInt(o.siacoinOutput.value);
|
||||
if (o.maturityHeight > state.height) { immature += v; continue; }
|
||||
if (pendingSpent.has(o.id)) continue;
|
||||
confirmed += v;
|
||||
outs.push({ element: o, id: o.id, value: v, entry: e });
|
||||
}
|
||||
if (list.length < 100) break;
|
||||
}
|
||||
}
|
||||
for (const [id, t] of pendingSpent) if (Date.now() - t > 20 * 60 * 1000) pendingSpent.delete(id);
|
||||
state.outputs = outs; state.basis = basis;
|
||||
state.balance = { confirmed, immature };
|
||||
try { state.feePerByte = await client.feePerByte(); } catch (e) { log("fee lookup failed:", e?.message); }
|
||||
}
|
||||
|
||||
// One row per event: net change for our addresses, type, confirmations.
|
||||
async function loadHistory() {
|
||||
const ours = new Set(watched().map((e) => e.address));
|
||||
const seen = new Map();
|
||||
for (const e of watched()) {
|
||||
if (!state.used.has(e.index)) continue;
|
||||
const evs = await client.events(e.address, HISTORY_LIMIT, 0);
|
||||
for (const ev of Array.isArray(evs) ? evs : []) if (!seen.has(ev.id)) seen.set(ev.id, ev);
|
||||
}
|
||||
const rows = [];
|
||||
for (const ev of seen.values()) {
|
||||
let received = 0n, spent = 0n, to = null;
|
||||
const d = ev.data || {};
|
||||
const type = String(ev.type || "").toLowerCase();
|
||||
if (type === "v2transaction" && d.transaction) {
|
||||
for (const i of d.transaction.siacoinInputs || []) if (ours.has(i.parent.siacoinOutput.address)) spent += BigInt(i.parent.siacoinOutput.value);
|
||||
for (const o of d.transaction.siacoinOutputs || []) { if (ours.has(o.address)) received += BigInt(o.value); else if (!to) to = o.address; }
|
||||
} else if (type === "v1transaction" && d.transaction) {
|
||||
for (const s of d.spentSiacoinElements || []) if (ours.has(s.siacoinOutput.address)) spent += BigInt(s.siacoinOutput.value);
|
||||
for (const o of d.transaction.siacoinOutputs || []) { if (ours.has(o.address)) received += BigInt(o.value); else if (!to) to = o.address; }
|
||||
} else if (d.siacoinElement) {
|
||||
if (ours.has(d.siacoinElement.siacoinOutput.address)) received += BigInt(d.siacoinElement.siacoinOutput.value);
|
||||
}
|
||||
rows.push({
|
||||
id: ev.id, type: ev.type, height: ev.index ? ev.index.height : 0, confirmations: ev.confirmations || 0,
|
||||
time: ev.timestamp ? Math.floor(Date.parse(ev.timestamp) / 1000) : 0,
|
||||
delta: (received - spent).toString(), to: spent > received ? to : null,
|
||||
maturityHeight: ev.maturityHeight || 0,
|
||||
});
|
||||
}
|
||||
rows.sort((a, b) => (b.height || Infinity) - (a.height || Infinity) || b.time - a.time);
|
||||
state.history = rows.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
async function refresh(full = false) {
|
||||
if (state.scanning) return;
|
||||
state.scanning = true; state.error = null; onChange();
|
||||
try {
|
||||
if (full || !state.used.size && state.receiveIndex === 0) await scan();
|
||||
else { let r = Number(storage.get("receiveCursor", 0)) || 0; while (state.used.has(r)) r++; state.receiveIndex = r; }
|
||||
await loadOutputs();
|
||||
await loadHistory();
|
||||
for (const o of state.outputs) state.used.add(o.entry.index);
|
||||
let r = Number(storage.get("receiveCursor", 0)) || 0;
|
||||
while (state.used.has(r)) r++;
|
||||
state.receiveIndex = r;
|
||||
} catch (e) {
|
||||
state.error = e?.message || String(e);
|
||||
log("refresh failed:", state.error);
|
||||
} finally { state.scanning = false; onChange(); }
|
||||
}
|
||||
let pollTimer = null;
|
||||
function startPolling(ms = 60000) { stopPolling(); pollTimer = setInterval(() => refresh(false), ms); }
|
||||
function stopPolling() { clearInterval(pollTimer); pollTimer = null; }
|
||||
|
||||
function current() { return keys.entry(state.receiveIndex); }
|
||||
function nextUnusedAddress() {
|
||||
let r = state.receiveIndex + 1;
|
||||
while (state.used.has(r)) r++;
|
||||
storage.set("receiveCursor", r);
|
||||
state.receiveIndex = r;
|
||||
onChange();
|
||||
return current();
|
||||
}
|
||||
|
||||
// targets: [{ to, value: BigInt }]; feeMultiplier 1-3 over walletd's rate.
|
||||
function plan({ targets, feeMultiplier = 1, sendMax = false }) {
|
||||
const mult = BigInt(Math.min(3, Math.max(1, Math.round(Number(feeMultiplier) || 1))));
|
||||
const rate = state.feePerByte > 0n ? state.feePerByte * mult : 10n ** 19n * mult;
|
||||
const outs = targets.map((t) => {
|
||||
const a = sia.parseAddress(t.to);
|
||||
return { value: BigInt(t.value || 0), address32: a.bytes, to: a.address };
|
||||
});
|
||||
const sorted = state.outputs.slice().sort((a, b) => (b.value > a.value ? 1 : b.value < a.value ? -1 : 0));
|
||||
const total = sorted.reduce((a, o) => a + o.value, 0n);
|
||||
const change = current();
|
||||
const txOf = (inputs, outputs, fee) => ({
|
||||
inputs: inputs.map((o) => ({
|
||||
parentId: o.id, element: o.element, value: o.value, address32: o.entry.address32, pub: o.entry.pub,
|
||||
leafIndex: o.element.stateElement.leafIndex, merkleProof: o.element.stateElement.merkleProof || [], maturityHeight: o.element.maturityHeight || 0, entry: o.entry,
|
||||
})),
|
||||
outputs, minerFee: fee,
|
||||
});
|
||||
if (sendMax) {
|
||||
if (outs.length !== 1) throw new Error("send max needs exactly one recipient");
|
||||
if (!sorted.length) throw new Error("no spendable balance");
|
||||
let fee = 0n;
|
||||
for (let k = 0; k < 3; k++) fee = rate * BigInt(sia.weight(txOf(sorted, [{ value: total - fee, address32: outs[0].address32 }], fee)));
|
||||
if (total <= fee) throw new Error("balance does not cover the fee");
|
||||
const tx = txOf(sorted, [{ value: total - fee, address32: outs[0].address32 }], fee);
|
||||
return { tx, fee, recipients: [{ to: outs[0].to, value: (total - fee).toString() }], change: 0n, feePerByte: rate };
|
||||
}
|
||||
const want = outs.reduce((a, o) => a + o.value, 0n);
|
||||
for (const o of outs) if (o.value <= 0n) throw new Error("amount must be positive");
|
||||
const chosen = []; let sum = 0n;
|
||||
for (const o of sorted) {
|
||||
chosen.push(o); sum += o.value;
|
||||
const withChange = [...outs, { value: 0n, address32: change.address32 }];
|
||||
const fee = rate * BigInt(sia.weight(txOf(chosen, withChange, 1n)));
|
||||
if (sum >= want + fee) {
|
||||
const rest = sum - want - fee;
|
||||
const outputs = rest > 0n ? [...outs, { value: rest, address32: change.address32 }] : outs.slice();
|
||||
const tx = txOf(chosen, outputs, fee);
|
||||
return { tx, fee, recipients: outs.map((o) => ({ to: o.to, value: o.value.toString() })), change: rest, feePerByte: rate };
|
||||
}
|
||||
}
|
||||
throw new Error("insufficient funds");
|
||||
}
|
||||
|
||||
async function signAndBroadcast(p) {
|
||||
const h = sia.inputSigHash(p.tx);
|
||||
const sigs = p.tx.inputs.map((i) => keys.sign(i.entry, h));
|
||||
const json = sia.toJson(p.tx, sigs);
|
||||
if (!state.basis) throw new Error("no chain basis for the outputs; refresh first");
|
||||
const r = await client.broadcast(state.basis, json);
|
||||
const txid = r && r.v2transactions && r.v2transactions[0] && r.v2transactions[0].id;
|
||||
for (const i of p.tx.inputs) pendingSpent.set(i.parentId, Date.now());
|
||||
log("broadcast", txid || "(no id returned)");
|
||||
setTimeout(() => refresh(false), 3000);
|
||||
return { txid: txid || null, fee: p.fee.toString() };
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
const cur = current();
|
||||
return {
|
||||
address: cur.address, addressIndex: state.receiveIndex,
|
||||
balance: { confirmed: state.balance.confirmed.toString(), immature: state.balance.immature.toString() },
|
||||
height: state.height, history: state.history, outputCount: state.outputs.length,
|
||||
feePerByte: state.feePerByte.toString(), scanning: state.scanning, error: state.error,
|
||||
};
|
||||
}
|
||||
function dispose() { stopPolling(); }
|
||||
return { refresh, snapshot, nextUnusedAddress, current, plan, signAndBroadcast, startPolling, dispose, state };
|
||||
};
|
||||
42
bundled-addons/bchwallet/lib/sia/walletd.js
Normal file
42
bundled-addons/bchwallet/lib/sia/walletd.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Thin client for the walletd HTTP API (go.sia.tech/walletd, index mode
|
||||
// "full"). Only address-scoped reads plus txpool fee/broadcast are used, so
|
||||
// any public or self-hosted walletd works; the URL is a user setting.
|
||||
module.exports = function makeWalletd({ log = () => {} }) {
|
||||
class Client {
|
||||
constructor(baseUrl) { this.setBase(baseUrl); }
|
||||
setBase(baseUrl) {
|
||||
const u = String(baseUrl || "").trim().replace(/\/+$/, "");
|
||||
this.base = u ? (u.endsWith("/api") ? u : u + "/api") : "";
|
||||
}
|
||||
// Everything after the host is the node's business; only the origin is
|
||||
// ever logged or shown, since hosted providers key access on the path.
|
||||
get displayUrl() { try { return new URL(this.base).origin; } catch { return this.base; } }
|
||||
async _req(method, path, body) {
|
||||
if (!this.base) throw new Error("no walletd URL configured");
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 25000);
|
||||
try {
|
||||
const res = await fetch(this.base + path, {
|
||||
method, signal: ctrl.signal,
|
||||
headers: body !== undefined ? { "content-type": "application/json" } : {},
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) throw new Error(`walletd ${res.status}: ${text.slice(0, 200).trim()}`);
|
||||
try { return JSON.parse(text); } catch { return text; }
|
||||
} finally { clearTimeout(timer); }
|
||||
}
|
||||
get(path) { return this._req("GET", path); }
|
||||
post(path, body) { return this._req("POST", path, body); }
|
||||
|
||||
tip() { return this.get("/consensus/tip"); }
|
||||
// Recommended fee in hastings per byte (JSON string).
|
||||
async feePerByte() { return BigInt(String(await this.get("/txpool/fee")).replace(/"/g, "")); }
|
||||
balance(addr) { return this.get(`/addresses/${addr}/balance`); }
|
||||
// { basis, outputs: [SiacoinElement] } — proofs are valid at `basis`.
|
||||
outputs(addr, limit = 100, offset = 0) { return this.get(`/addresses/${addr}/outputs/siacoin?limit=${limit}&offset=${offset}`); }
|
||||
events(addr, limit = 25, offset = 0) { return this.get(`/addresses/${addr}/events?limit=${limit}&offset=${offset}`); }
|
||||
broadcast(basis, v2tx) { return this.post("/txpool/broadcast", { basis, transactions: [], v2transactions: [v2tx] }); }
|
||||
}
|
||||
return { Client };
|
||||
};
|
||||
220
bundled-addons/bchwallet/lib/sol-spl.js
Normal file
220
bundled-addons/bchwallet/lib/sol-spl.js
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// Solana Program Library (SPL) token primitives — PDA derivation,
|
||||
// Associated Token Account math, and the two Token-program instructions
|
||||
// this wallet needs at the bytecode level: `TransferChecked` (send SPL
|
||||
// with a decimals sanity check) and `CreateAssociatedTokenAccountIdempotent`
|
||||
// (make the receiver's token account inline, so the user doesn't have to
|
||||
// pre-create it on any address they've never seen before).
|
||||
//
|
||||
// Every account address on Solana is a 32-byte ed25519 public key. A
|
||||
// Program-Derived Address (PDA) is a 32-byte value that is NOT on the
|
||||
// ed25519 curve — the runtime uses that fact as proof that no one holds
|
||||
// its private key, so only the owning program can spend from it. To
|
||||
// derive a PDA we sha256(seeds || programId || bump || "ProgramDerivedAddress")
|
||||
// for bump = 255…0 and pick the first value that isn't a valid curve
|
||||
// point. `isOnCurve` here defers to @noble/curves/ed25519's ExtendedPoint,
|
||||
// which throws on invalid points; everything that decodes is on-curve.
|
||||
|
||||
const PDA_MARKER = new TextEncoder().encode("ProgramDerivedAddress");
|
||||
|
||||
module.exports = function makeSolSpl({ ed25519, sha256, base58 }) {
|
||||
if (!ed25519 || !sha256 || !base58) throw new Error("sol-spl: missing dep");
|
||||
|
||||
// ---- constants -------------------------------------------------------
|
||||
const TOKEN_PROGRAM_ID_B58 = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
||||
const ASSOC_PROGRAM_ID_B58 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
||||
const TOKEN_2022_PROGRAM_ID_B58 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
||||
const TOKEN_PROGRAM_ID = base58.decode(TOKEN_PROGRAM_ID_B58);
|
||||
const ASSOC_PROGRAM_ID = base58.decode(ASSOC_PROGRAM_ID_B58);
|
||||
const TOKEN_2022_PROGRAM_ID = base58.decode(TOKEN_2022_PROGRAM_ID_B58);
|
||||
const SYSTEM_PROGRAM_ID = new Uint8Array(32);
|
||||
|
||||
// Known-token registry — just enough to give the panel a sensible label
|
||||
// for the tokens users actually see day-to-day. Everything else falls
|
||||
// back to the mint address itself (truncated in the UI).
|
||||
const KNOWN_TOKENS = {
|
||||
mainnet: {
|
||||
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": { symbol: "USDC", decimals: 6, name: "USD Coin" },
|
||||
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB": { symbol: "USDT", decimals: 6, name: "Tether USD" },
|
||||
"So11111111111111111111111111111111111111112": { symbol: "wSOL", decimals: 9, name: "Wrapped SOL" },
|
||||
},
|
||||
devnet: {
|
||||
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU": { symbol: "USDC", decimals: 6, name: "USD Coin (devnet)" },
|
||||
},
|
||||
};
|
||||
|
||||
// ---- ed25519 curve check --------------------------------------------
|
||||
// A 32-byte pubkey is "on curve" if it decompresses to a valid Edwards
|
||||
// point. @noble/curves v2 exposes Point.fromBytes(bytes) (throws on
|
||||
// invalid); older versions exposed ExtendedPoint.fromHex(hex-string)
|
||||
// — try each in order.
|
||||
function isOnCurve(pubkey32) {
|
||||
const P = ed25519.Point || ed25519.ExtendedPoint;
|
||||
if (!P) return true; // no curve access — treat every value as
|
||||
// on-curve (over-conservative; PDA loop falls
|
||||
// through more than it should but never gets
|
||||
// wrong).
|
||||
try {
|
||||
if (typeof P.fromBytes === "function") { P.fromBytes(pubkey32); return true; }
|
||||
if (typeof P.fromHex === "function") {
|
||||
const hex = Array.from(pubkey32, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
P.fromHex(hex); return true;
|
||||
}
|
||||
} catch { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
const concat = (...ps) => {
|
||||
const n = ps.reduce((a, p) => a + p.length, 0);
|
||||
const out = new Uint8Array(n); let k = 0;
|
||||
for (const p of ps) { out.set(p, k); k += p.length; }
|
||||
return out;
|
||||
};
|
||||
const u64le = (n) => {
|
||||
let v = BigInt(n); const o = new Uint8Array(8);
|
||||
for (let i = 0; i < 8; i++) { o[i] = Number(v & 0xffn); v >>= 8n; }
|
||||
return o;
|
||||
};
|
||||
const encodeCompactU16 = (n) => {
|
||||
const out = [];
|
||||
let rem = n;
|
||||
while (true) {
|
||||
let byte = rem & 0x7f; rem >>= 7;
|
||||
if (rem === 0) { out.push(byte); break; }
|
||||
byte |= 0x80; out.push(byte);
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
};
|
||||
|
||||
// ---- PDA / ATA -------------------------------------------------------
|
||||
function findProgramAddress(seeds, programId) {
|
||||
for (let bump = 255; bump >= 0; bump--) {
|
||||
const material = concat(
|
||||
...seeds.map((s) => Uint8Array.from(s)),
|
||||
Uint8Array.from([bump]),
|
||||
programId,
|
||||
PDA_MARKER,
|
||||
);
|
||||
const candidate = sha256(material);
|
||||
if (!isOnCurve(candidate)) return { address: candidate, bump };
|
||||
}
|
||||
throw new Error("no PDA found (unreachable)");
|
||||
}
|
||||
// Standard ATA = PDA under ASSOC_PROGRAM_ID with seeds
|
||||
// [ownerPubkey, TOKEN_PROGRAM_ID, mint].
|
||||
// We match the seed layout the @solana/spl-token library uses so
|
||||
// addresses agree with any wallet or block explorer.
|
||||
function associatedTokenAddress(owner, mint, tokenProgram = TOKEN_PROGRAM_ID) {
|
||||
return findProgramAddress([owner, tokenProgram, mint], ASSOC_PROGRAM_ID).address;
|
||||
}
|
||||
|
||||
// ---- instruction encoders -------------------------------------------
|
||||
// SPL Token: TransferChecked (discriminator 12) — asserts amount+decimals
|
||||
// against the mint so a UI bug can't move 1000× the intended value.
|
||||
// accounts: [sourceATA (writable), mint (readonly), destATA (writable), owner (signer)]
|
||||
// data: [12, amount:u64_le, decimals:u8]
|
||||
function transferCheckedInstruction({ sourceATA, mint, destATA, owner, amount, decimals, tokenProgram = TOKEN_PROGRAM_ID }) {
|
||||
return {
|
||||
programId: tokenProgram,
|
||||
keys: [
|
||||
{ pubkey: sourceATA, isSigner: false, isWritable: true },
|
||||
{ pubkey: mint, isSigner: false, isWritable: false },
|
||||
{ pubkey: destATA, isSigner: false, isWritable: true },
|
||||
{ pubkey: owner, isSigner: true, isWritable: false },
|
||||
],
|
||||
data: concat(Uint8Array.from([12]), u64le(amount), Uint8Array.from([decimals])),
|
||||
};
|
||||
}
|
||||
// Associated Token Account program: CreateIdempotent (discriminator 1)
|
||||
// accounts: [payer(signer,writable), ata(writable), owner(readonly),
|
||||
// mint(readonly), systemProgram(readonly), tokenProgram(readonly)]
|
||||
// data: [1] (idempotent variant — no-op if the account exists)
|
||||
function createATAIdempotentInstruction({ payer, ata, owner, mint, tokenProgram = TOKEN_PROGRAM_ID }) {
|
||||
return {
|
||||
programId: ASSOC_PROGRAM_ID,
|
||||
keys: [
|
||||
{ pubkey: payer, isSigner: true, isWritable: true },
|
||||
{ pubkey: ata, isSigner: false, isWritable: true },
|
||||
{ pubkey: owner, isSigner: false, isWritable: false },
|
||||
{ pubkey: mint, isSigner: false, isWritable: false },
|
||||
{ pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||
{ pubkey: tokenProgram, isSigner: false, isWritable: false },
|
||||
],
|
||||
data: Uint8Array.from([1]),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- transaction message builder ------------------------------------
|
||||
// Assembles the raw Solana message bytes for a single-fee-payer, single-
|
||||
// signer transaction that may carry multiple instructions. Account keys
|
||||
// are sorted per Solana's account-classification rules (writable-signed,
|
||||
// readonly-signed, writable-unsigned, readonly-unsigned).
|
||||
function buildMessage({ feePayer, instructions, recentBlockhash }) {
|
||||
// 1. Collect every unique pubkey mentioned across instructions +
|
||||
// include the fee payer + include each instruction's programId.
|
||||
const keys = new Map(); // b58 → { pubkey, isSigner, isWritable }
|
||||
const upsert = (pubkey, isSigner, isWritable) => {
|
||||
const k = base58.encode(pubkey);
|
||||
const cur = keys.get(k) || { pubkey, isSigner: false, isWritable: false };
|
||||
cur.isSigner = cur.isSigner || isSigner;
|
||||
cur.isWritable = cur.isWritable || isWritable;
|
||||
keys.set(k, cur);
|
||||
};
|
||||
upsert(feePayer, true, true);
|
||||
for (const ins of instructions) {
|
||||
for (const k of ins.keys) upsert(k.pubkey, k.isSigner, k.isWritable);
|
||||
upsert(ins.programId, false, false);
|
||||
}
|
||||
// 2. Classify + sort into the four buckets.
|
||||
const bucket = { ws: [], rs: [], wu: [], ru: [] };
|
||||
for (const v of keys.values()) {
|
||||
if (v.isSigner && v.isWritable) bucket.ws.push(v);
|
||||
else if (v.isSigner) bucket.rs.push(v);
|
||||
else if (v.isWritable) bucket.wu.push(v);
|
||||
else bucket.ru.push(v);
|
||||
}
|
||||
// Fee payer MUST be at index 0 (Solana requires the first signer to
|
||||
// be writable & to pay the fee).
|
||||
const payerB58 = base58.encode(feePayer);
|
||||
bucket.ws.sort((a, b) => (base58.encode(a.pubkey) === payerB58 ? -1 : base58.encode(b.pubkey) === payerB58 ? 1 : 0));
|
||||
const ordered = [...bucket.ws, ...bucket.rs, ...bucket.wu, ...bucket.ru];
|
||||
// 3. Encode the message.
|
||||
const header = Uint8Array.from([
|
||||
bucket.ws.length + bucket.rs.length, // numRequiredSignatures
|
||||
bucket.rs.length, // numReadonlySignedAccounts
|
||||
bucket.ru.length, // numReadonlyUnsignedAccounts
|
||||
]);
|
||||
const keysSection = concat(
|
||||
encodeCompactU16(ordered.length),
|
||||
...ordered.map((v) => Uint8Array.from(v.pubkey)),
|
||||
);
|
||||
const indexOf = new Map(ordered.map((v, i) => [base58.encode(v.pubkey), i]));
|
||||
const insSection = concat(
|
||||
encodeCompactU16(instructions.length),
|
||||
...instructions.map((ins) => {
|
||||
const programIndex = indexOf.get(base58.encode(ins.programId));
|
||||
const accountBytes = Uint8Array.from(ins.keys.map((k) => indexOf.get(base58.encode(k.pubkey))));
|
||||
return concat(
|
||||
Uint8Array.from([programIndex]),
|
||||
encodeCompactU16(accountBytes.length),
|
||||
accountBytes,
|
||||
encodeCompactU16(ins.data.length),
|
||||
ins.data,
|
||||
);
|
||||
}),
|
||||
);
|
||||
return concat(header, keysSection, recentBlockhash, insSection);
|
||||
}
|
||||
|
||||
return {
|
||||
// constants
|
||||
TOKEN_PROGRAM_ID, ASSOC_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, SYSTEM_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID_B58, ASSOC_PROGRAM_ID_B58,
|
||||
KNOWN_TOKENS,
|
||||
// helpers
|
||||
isOnCurve, findProgramAddress, associatedTokenAddress,
|
||||
// instruction encoders
|
||||
transferCheckedInstruction, createATAIdempotentInstruction,
|
||||
// tx assembly
|
||||
buildMessage,
|
||||
};
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Aegis Wallet</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D'http%3A//www.w3.org/2000/svg'%20viewBox%3D'0%200%2032%2032'%3E%3Cpolygon%20points%3D'16%2C2%2029%2C9%2029%2C23%2016%2C30%203%2C23%203%2C9'%20fill%3D'none'%20stroke%3D'%23d6ff3d'%20stroke-width%3D'2.5'%20stroke-linejoin%3D'round'/%3E%3Ccircle%20cx%3D'16'%20cy%3D'16'%20r%3D'4.3'%20fill%3D'none'%20stroke%3D'%23d6ff3d'%20stroke-width%3D'1.6'/%3E%3Ccircle%20cx%3D'16'%20cy%3D'16'%20r%3D'1.6'%20fill%3D'%23d6ff3d'/%3E%3Cpath%20d%3D'M16%2010.5%20v-2.5%20M16%2021.5%20v2.5%20M10.5%2016%20h-2.5%20M21.5%2016%20h2.5'%20stroke%3D'%23d6ff3d'%20stroke-width%3D'1.6'%20stroke-linecap%3D'round'/%3E%3C/svg%3E">
|
||||
<style>
|
||||
:root { color-scheme: light dark;
|
||||
--bg:#0e131c; --panel:#141a24; --card:#0f1621; --line:rgba(255,255,255,.09);
|
||||
|
|
@ -11,8 +12,8 @@
|
|||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg:#f8faff; --panel:#ffffff; --card:#f1f4fa; --line:rgba(0,0,0,.10);
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5;
|
||||
/* Darker acid on light backgrounds — ~7:1 on white. */
|
||||
--acid:#3a5c00; }
|
||||
/* Darker acid on light backgrounds — ~7:1 contrast on white. */
|
||||
--acid: #088A66; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
|
|
@ -35,23 +36,25 @@
|
|||
.bal .sub { color: var(--dim); font-size: 11.5px; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.bal .sub .netlbl { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#drop { position: absolute; left: 10px; right: 10px; top: 100%; background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; box-shadow: 0 8px 26px rgba(0,0,0,.3); z-index: 20; padding: 4px; margin-top: 4px; }
|
||||
border-radius: 10px; box-shadow: 0 8px 26px rgba(0,0,0,.3); z-index: 20; padding: 4px; margin-top: 4px; max-height: 60vh; overflow-y: auto; }
|
||||
#drop[hidden] { display: none; }
|
||||
#drop .row { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
|
||||
#drop .row:hover { background: rgba(255,255,255,.05); }
|
||||
#drop .row .b { font-size: 15px; line-height: 1; }
|
||||
#drop .row .m { flex: 1; min-width: 0; }
|
||||
#drop .row .m .l { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#drop .row .m .s { color: var(--dim); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#drop .row, #drop .coinrow { display: flex; align-items: center; gap: 9px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
|
||||
#drop .row:hover, #drop .coinrow:hover { background: rgba(255,255,255,.05); }
|
||||
#drop .row .m, #drop .coinrow .m { flex: 1; min-width: 0; }
|
||||
#drop .row .m .l, #drop .coinrow .m .l { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#drop .row .m .s, #drop .coinrow .m .s { color: var(--dim); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#drop .row .v { color: var(--mut); font-size: 12px; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
|
||||
#drop .row.on { background: rgba(214,255,61,.10); }
|
||||
#drop hr { border: 0; border-top: 1px solid var(--line); margin: 4px 0; }
|
||||
#drop .add { padding: 7px 8px; color: var(--acid); font-weight: 600; cursor: pointer; border-radius: 7px; }
|
||||
#drop .add:hover { background: rgba(214,255,61,.10); }
|
||||
#drop .netgroup { display: none; padding: 4px; border-radius: 7px; margin-top: 2px; }
|
||||
#drop .netgroup.on { display: block; background: rgba(255,255,255,.03); }
|
||||
#drop .coinrow .caret { color: var(--dim); font-size: 11px; }
|
||||
#drop hr { border: 0; border-top: 1px solid var(--line); margin: 6px 0; }
|
||||
#drop .addhdr { padding: 5px 8px 3px; color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
#drop .netgroup { padding: 2px 4px 6px 40px; }
|
||||
#drop .netgroup[hidden] { display: none; }
|
||||
#drop .netchoice { padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12.5px; color: var(--mut); }
|
||||
#drop .netchoice:hover { background: rgba(255,255,255,.05); color: var(--ink); }
|
||||
#drop .netchoice:hover { background: rgba(255,255,255,.06); color: var(--ink); }
|
||||
.ttag { display: inline-block; font-size: 9.5px; letter-spacing: .06em; padding: 1px 5px; border-radius: 3px;
|
||||
background: rgba(224,179,65,.18); color: #e0b341; font-weight: 700; vertical-align: middle; margin-left: 2px; }
|
||||
#hNet { color: var(--dim); font-size: 11px; margin-left: 4px; font-weight: 500; }
|
||||
nav { display: flex; border-bottom: 1px solid var(--line); background: var(--panel); }
|
||||
nav button { flex: 1; padding: 9px 0 8px; border: 0; background: transparent; color: var(--mut); cursor: pointer;
|
||||
font: inherit; font-size: 12.5px; border-bottom: 2px solid transparent; }
|
||||
|
|
@ -114,7 +117,11 @@
|
|||
<body>
|
||||
<header>
|
||||
<div class="picker" id="pickerBtn">
|
||||
<div class="t"><span class="badge" id="hBadge">🛡</span><span class="lbl" id="hLabel">Aegis Wallet</span></div>
|
||||
<div class="t">
|
||||
<span class="badge" id="hBadge"></span>
|
||||
<span class="lbl" id="hLabel">Aegis Wallet</span>
|
||||
<span id="hNet"></span>
|
||||
</div>
|
||||
<div class="caret">▾</div>
|
||||
</div>
|
||||
<div id="drop" hidden></div>
|
||||
|
|
@ -147,8 +154,18 @@
|
|||
<button class="btn sm" id="openFaucet" hidden>Faucet</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" id="tokensCard" hidden style="margin-top:12px">
|
||||
<div class="lbl">Tokens</div>
|
||||
<div id="tokensList" class="kv"></div>
|
||||
<div class="hint">SPL tokens held by this wallet. Send by picking one under the Send tab's Asset dropdown.</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="tab-send" hidden>
|
||||
<div class="field" id="sendAssetField" hidden>
|
||||
<div class="lbl">Asset</div>
|
||||
<select id="sendAsset"></select>
|
||||
<div class="hint">Pick the native coin or an SPL token in this wallet.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Recipient</div>
|
||||
<input type="text" id="sendTo" spellcheck="false" autocomplete="off" placeholder="…">
|
||||
|
|
@ -198,8 +215,8 @@
|
|||
<input type="text" id="setPath" spellcheck="false" placeholder="m/44'/145'/0'">
|
||||
<div class="hint">Changing this switches to a different set of addresses under the same wallet seed.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Electrum servers (shared across BCH wallets, one per line)</div>
|
||||
<div class="field" id="bchServersRow">
|
||||
<div class="lbl">Electrum servers (shared across BCH mainnet wallets, one per line)</div>
|
||||
<textarea id="setServers" spellcheck="false"></textarea>
|
||||
<div class="hint" id="serverHint"></div>
|
||||
</div>
|
||||
|
|
@ -225,6 +242,113 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="scSettings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">walletd URL</div>
|
||||
<input type="text" id="setWalletdUrl" spellcheck="false" placeholder="https://your-walletd.example/api">
|
||||
<div class="hint">Any public or self-hosted <span class="mono">go.sia.tech/walletd</span> in "full" index mode. The URL is per-wallet, so different Sia sub-accounts can point at different nodes. It is not shared with anyone else.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="applyWalletdUrl">Apply</button>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Recovery info</div>
|
||||
<div class="hint">Keys come from your Theseus password vault under <span class="mono" id="scPurpose"></span>. Address scheme: walletd <span class="mono">KeyFromSeed(seed, index)</span>.</div>
|
||||
<div class="kv" id="scRecovery"></div>
|
||||
<div class="actions">
|
||||
<button class="btn danger" id="showScSeed">Reveal wallet seed</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dgbSettings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">Address family</div>
|
||||
<select id="setDgbFamily"></select>
|
||||
<div class="hint">Picks the BIP purpose that shapes your DGB addresses. Switching rebuilds the wallet against a different set of addresses under the same seed — old funds don't move; they still live under the family they were received on.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Derivation path (account)</div>
|
||||
<input type="text" id="setDgbPath" spellcheck="false" placeholder="m/84'/20'/0'">
|
||||
<div class="hint">Auto-filled from the family above. Edit only if you need a non-default account.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="applyDgbPath">Apply</button>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Recovery info</div>
|
||||
<div class="hint">Keys come from your Theseus password vault under <span class="mono" id="dgbPurpose"></span>. Any BIP39 tool set to <span class="mono">DGB</span> (coin type 20) at the same purpose can reproduce this wallet.</div>
|
||||
<div class="kv" id="dgbRecovery"></div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="showDgbXpub">Show account xpub</button>
|
||||
<button class="btn danger" id="showDgbXprv">Show account private key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="btcSettings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">Address family</div>
|
||||
<select id="setBtcFamily"></select>
|
||||
<div class="hint">Picks the BIP purpose that shapes your Bitcoin addresses. Switching rebuilds the wallet against a different set of addresses under the same seed — old funds don't move; they still live under the family they were received on.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Derivation path (account)</div>
|
||||
<input type="text" id="setBtcPath" spellcheck="false" placeholder="m/84'/0'/0'">
|
||||
<div class="hint">Auto-filled from the family above. Edit only if you need a non-default account.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="applyBtcPath">Apply</button>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Recovery info</div>
|
||||
<div class="hint">Keys come from your Theseus password vault under <span class="mono" id="btcPurpose"></span>. Any BIP39 tool at coin type 0 (mainnet) / 1 (testnet) and the same purpose can reproduce this wallet.</div>
|
||||
<div class="kv" id="btcRecovery"></div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="showBtcXpub">Show account xpub</button>
|
||||
<button class="btn danger" id="showBtcXprv">Show account private key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ethSettings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">RPC URL</div>
|
||||
<input type="text" id="setEthRpcUrl" spellcheck="false" placeholder="https://cloudflare-eth.com">
|
||||
<div class="hint">JSON-RPC endpoint Aegis reads balances and broadcasts from. Any provider works — public gateways (Cloudflare, PublicNode), Alchemy, Infura, or your own node. Stored per-wallet; not shared.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="applyEthRpc">Apply</button>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Recovery info</div>
|
||||
<div class="hint">Keys come from your Theseus password vault under <span class="mono" id="ethPurpose"></span>. Import into MetaMask / any Ethereum wallet using the derivation path <span class="mono">m/44'/60'/0'/0/0</span> from the same vault seed.</div>
|
||||
<div class="kv" id="ethRecovery"></div>
|
||||
<div class="actions">
|
||||
<button class="btn danger" id="showEthKey">Reveal private key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="solSettings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">RPC URL</div>
|
||||
<input type="text" id="setSolRpcUrl" spellcheck="false" placeholder="https://api.mainnet-beta.solana.com">
|
||||
<div class="hint">JSON-RPC endpoint. The public Solana RPCs are heavily rate-limited — for real use point Aegis at Helius / QuickNode / your own node.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="applySolRpc">Apply</button>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Recovery info</div>
|
||||
<div class="hint">Keys come from your Theseus password vault under <span class="mono" id="solPurpose"></span>. Import into Phantom / Solflare with the derivation path <span class="mono">m/44'/501'/0'/0'</span> from the same vault seed.</div>
|
||||
<div class="kv" id="solRecovery"></div>
|
||||
<div class="actions">
|
||||
<button class="btn danger" id="showSolKey">Reveal wallet seed</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Connected sites</div>
|
||||
<div class="hint">Sites allowed to see your address, allowances for silent BCH payments, and Tron dapps you've connected. Message signing always asks.</div>
|
||||
|
|
|
|||
|
|
@ -9,28 +9,137 @@ let unit = null; // "big" | "small" — chain-dependent
|
|||
let sendMax = false;
|
||||
let planTimer = null;
|
||||
let lastPlan = null;
|
||||
let settingsFilled = false; // when true, we don't overwrite user edits
|
||||
let settingsFilled = false;
|
||||
// Selected asset for the Send tab. `null` = native coin. Otherwise a
|
||||
// { mint, symbol, decimals } picked from the SOL wallet's SPL token list.
|
||||
let sendAsset = null;
|
||||
|
||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
||||
const hostOf = (url) => { try { return new URL(url).host || url; } catch { return url; } };
|
||||
const openUrl = (url) => S.invoke("openUrl", { url }).catch(() => {});
|
||||
const cleanErr = (e) => String(e?.message || e).replace(/^Error invoking remote method '[^']+': Error: /, "");
|
||||
|
||||
// ---- coin logos ------------------------------------------------------------
|
||||
// Inline SVGs so the header, wallet picker and settings surface all render
|
||||
// the same mark. Sized by the container via width/height attributes.
|
||||
function logoSvg(logo, size) {
|
||||
const s = size || 20;
|
||||
if (logo === "bch") {
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Bitcoin Cash" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#0ac18e" stroke="#0a9a72" stroke-width=".8"/>
|
||||
<text x="16" y="22.4" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="20" font-weight="800" fill="#fff">₿</text>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "trx") {
|
||||
// Simplified from the official geometric Tron mark: triangle + tail line.
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Tron" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#ff060a" stroke="#c1050a" stroke-width=".8"/>
|
||||
<path d="M7.5 10.2 L24 12.5 L14.5 23.8 Z"
|
||||
fill="none" stroke="#fff" stroke-width="1.7" stroke-linejoin="round"/>
|
||||
<line x1="7.5" y1="10.2" x2="14.5" y2="23.8" stroke="#fff" stroke-width="1.7" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "sc") {
|
||||
// Sia's mark is a stylized S built from two mirrored crescents. Approximated
|
||||
// here with a plain S glyph on the brand green so it reads at 22px.
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Siacoin" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#20be82" stroke="#158e5f" stroke-width=".8"/>
|
||||
<text x="16" y="22.3" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="18" font-weight="800" fill="#fff">S</text>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "dgb") {
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="DigiByte" style="vertical-align:middle;flex:none">
|
||||
<polygon points="10,2 22,2 30,10 30,22 22,30 10,30 2,22 2,10" fill="#0066cc" stroke="#004a99" stroke-width=".8"/>
|
||||
<text x="16" y="22.4" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="18" font-weight="800" fill="#fff">D</text>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "btc") {
|
||||
// Orange disc with the Bitcoin sign — the widely-recognised BTC mark.
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Bitcoin" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#f7931a" stroke="#c76e0a" stroke-width=".8"/>
|
||||
<text x="16" y="22.4" text-anchor="middle" font-family="Segoe UI,Arial,sans-serif" font-size="20" font-weight="800" fill="#fff">₿</text>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "eth") {
|
||||
// Ethereum's mark is the two-triangle rhombus. Simplified to the
|
||||
// silhouette on a light-purple disc so it reads at 22px.
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Ethereum" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#627eea" stroke="#4058c0" stroke-width=".8"/>
|
||||
<polygon points="16,5 22,17 16,20 10,17" fill="#fff" opacity=".95"/>
|
||||
<polygon points="16,21 22,18 16,27 10,18" fill="#fff" opacity=".7"/>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "sol") {
|
||||
// Solana's three-slant mark. Purple → green gradient in the brand
|
||||
// spec; approximated with two solid parallelograms on a dark disc.
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Solana" style="vertical-align:middle;flex:none">
|
||||
<circle cx="16" cy="16" r="15.5" fill="#0f0f14" stroke="#2a2a3a" stroke-width=".8"/>
|
||||
<polygon points="9,11 22,11 20,14 7,14" fill="#9945ff"/>
|
||||
<polygon points="9,15 22,15 20,18 7,18" fill="#14f195"/>
|
||||
<polygon points="9,19 22,19 20,22 7,22" fill="#00d1ff"/>
|
||||
</svg>`;
|
||||
}
|
||||
if (logo === "aegis") {
|
||||
// Athena's aspis — hexagonal shield with a boss at center + four
|
||||
// spoke marks. Same silhouette as the aegis.x hero SVG so the wallet
|
||||
// and the marketing page read as one identity.
|
||||
return `<svg viewBox="0 0 32 32" width="${s}" height="${s}" aria-label="Aegis" style="vertical-align:middle;flex:none">
|
||||
<polygon points="16,2 29,9 29,23 16,30 3,23 3,9" fill="none" stroke="#d6ff3d" stroke-width="2" stroke-linejoin="round"/>
|
||||
<circle cx="16" cy="16" r="4.3" fill="none" stroke="#d6ff3d" stroke-width="1.4"/>
|
||||
<circle cx="16" cy="16" r="1.3" fill="#d6ff3d"/>
|
||||
<path d="M16 10.5 v-2.4 M16 21.5 v2.4 M10.5 16 h-2.4 M21.5 16 h2.4" stroke="#d6ff3d" stroke-width="1.4" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
// Fallback = Aegis shield (rather than a "?"), so an unrecognised
|
||||
// registry entry still looks intentional.
|
||||
return logoSvg("aegis", s);
|
||||
}
|
||||
function testnetTag() { return `<span class="ttag">TEST</span>`; }
|
||||
|
||||
// Selected wallet convenience.
|
||||
const sel = () => state && state.selected;
|
||||
const chain = () => sel()?.chain || "";
|
||||
const decimals = () => sel()?.meta?.decimals || 8;
|
||||
const ticker = () => sel()?.meta?.ticker || "";
|
||||
const badgeOf = (chainKey) => (state?.chains || []).find((c) => `${c.chain}:${c.network}` === chainKey)?.badge || "🧩";
|
||||
|
||||
// Amount formatting: n_units -> string trimmed to the coin's precision.
|
||||
// Numbers past ~9e15 lose precision as JS `Number`, and Sia amounts live at
|
||||
// 10^24-scale routinely. Use BigInt for anything that arrives as a string.
|
||||
function fmtBig(units, dec) {
|
||||
const d = dec != null ? dec : decimals();
|
||||
if (typeof units === "string" && /^-?\d+$/.test(units)) {
|
||||
const neg = units.startsWith("-");
|
||||
const raw = neg ? units.slice(1) : units;
|
||||
const bi = BigInt(raw || "0");
|
||||
const base = 10n ** BigInt(d);
|
||||
const whole = (bi / base).toString();
|
||||
let frac = (bi % base).toString().padStart(d, "0").replace(/0+$/, "");
|
||||
// Show 8-digit precision at most for very small units; keep 2 dp minimum.
|
||||
const cap = Math.min(d, 8);
|
||||
if (frac.length > cap) frac = frac.slice(0, cap);
|
||||
if (!frac) frac = "";
|
||||
return (neg ? "-" : "") + whole + (frac ? "." + frac : "");
|
||||
}
|
||||
const s = (Number(units || 0) / Math.pow(10, d)).toFixed(d);
|
||||
return s.replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1");
|
||||
}
|
||||
function fmtSmall(units) { return Number(units || 0).toLocaleString("en-US"); }
|
||||
function smallUnitLabel() { return chain() === "bch" ? "sat" : "sun"; }
|
||||
function fmtSmall(units) {
|
||||
if (typeof units === "string" && /^-?\d+$/.test(units)) return units.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return Number(units || 0).toLocaleString("en-US");
|
||||
}
|
||||
function smallUnitLabel() {
|
||||
const c = chain();
|
||||
if (c === "bch" || c === "dgb" || c === "btc") return "sat";
|
||||
if (c === "trx") return "sun";
|
||||
if (c === "sc") return "H";
|
||||
if (c === "eth") return "wei";
|
||||
if (c === "sol") return "lamports";
|
||||
return "u";
|
||||
}
|
||||
// Some chains (SOL) suffix explorer URLs to tell devnet from mainnet.
|
||||
function explorerHref(base, id) {
|
||||
const s = sel();
|
||||
return base + id + (s?.explorerSuffix || "");
|
||||
}
|
||||
function bigUnitLabel() { return ticker(); }
|
||||
|
||||
// ---- tabs ------------------------------------------------------------------
|
||||
|
|
@ -44,7 +153,7 @@ function showTab(name) {
|
|||
if (name === "send") applyUnitPicker();
|
||||
}
|
||||
|
||||
// ---- wallet picker ---------------------------------------------------------
|
||||
// ---- wallet picker (two-step add) ------------------------------------------
|
||||
|
||||
$("pickerBtn").addEventListener("click", () => {
|
||||
const d = $("drop");
|
||||
|
|
@ -57,25 +166,56 @@ document.addEventListener("click", (e) => {
|
|||
if (e.target.closest("#drop") || e.target.closest("#pickerBtn")) return;
|
||||
d.hidden = true;
|
||||
});
|
||||
|
||||
function fillPicker() {
|
||||
const d = $("drop");
|
||||
const wallets = state?.wallets || [];
|
||||
const chains = state?.chains || [];
|
||||
const rows = wallets.map((w) => {
|
||||
const coins = state?.coins || [];
|
||||
const rowsHtml = wallets.map((w) => {
|
||||
const on = w.id === state.selectedWalletId ? "on" : "";
|
||||
const bal = w.balance ? fmtBig(w.balance.confirmed || 0, w.decimals) + " " + w.ticker : "—";
|
||||
return `<div class="row ${on}" data-select="${esc(w.id)}"><div class="b">${esc(w.badge)}</div><div class="m"><div class="l">${esc(w.label)}</div><div class="s">${esc(w.short)} · ${w.phase === "ready" ? esc(w.address || "") : esc(w.phase)}</div></div><div class="v">${esc(bal)}</div></div>`;
|
||||
const sub = `${esc(w.coinLabel)} · ${esc(w.networkLabel)}${w.testnet ? " " + testnetTag() : ""}`;
|
||||
return `<div class="row ${on}" data-select="${esc(w.id)}">
|
||||
${logoSvg(w.logo, 22)}
|
||||
<div class="m"><div class="l">${esc(w.label)}</div><div class="s">${sub}</div></div>
|
||||
<div class="v">${esc(bal)}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
const chainList = chains.map((c) =>
|
||||
`<div class="netchoice" data-add="${esc(c.chain + ":" + c.network)}">${esc(c.badge)} ${esc(c.label)}</div>`
|
||||
).join("");
|
||||
d.innerHTML = rows + `<hr><div class="add" id="addToggle">+ Add wallet</div><div class="netgroup" id="netgroup">${chainList}</div>`;
|
||||
// "Add wallet" is a two-step flyout: first show coins, then that coin's
|
||||
// networks. Nothing is created until the user clicks a specific network.
|
||||
const coinRows = coins.map((c) => {
|
||||
const testCount = c.networks.filter((n) => n.testnet).length;
|
||||
const sub = c.networks.length > 1
|
||||
? c.networks.map((n) => n.label).join(" · ")
|
||||
: c.networks[0].label;
|
||||
return `<div class="coinrow" data-coin="${esc(c.chain)}">
|
||||
${logoSvg(c.logo, 22)}
|
||||
<div class="m"><div class="l">${esc(c.label)}</div><div class="s">${esc(sub)}</div></div>
|
||||
<div class="caret">▸</div>
|
||||
</div>
|
||||
<div class="netgroup" id="netgroup-${esc(c.chain)}" hidden>
|
||||
${c.networks.map((n) => `<div class="netchoice" data-add="${esc(c.chain + ":" + n.id)}">
|
||||
${esc(n.label)}${n.testnet ? " " + testnetTag() : ""}
|
||||
</div>`).join("")}
|
||||
</div>`;
|
||||
}).join("");
|
||||
d.innerHTML =
|
||||
rowsHtml +
|
||||
`<hr><div class="addhdr">+ Add wallet</div>${coinRows}`;
|
||||
|
||||
d.querySelectorAll("[data-select]").forEach((r) => r.addEventListener("click", async () => {
|
||||
d.hidden = true;
|
||||
try { state = await S.invoke("selectWallet", { id: r.dataset.select }); settingsFilled = false; render(); }
|
||||
catch (e) { showErr(cleanErr(e)); }
|
||||
}));
|
||||
$("addToggle").addEventListener("click", () => $("netgroup").classList.toggle("on"));
|
||||
d.querySelectorAll(".coinrow").forEach((r) => r.addEventListener("click", () => {
|
||||
// Collapse other coins' network groups; toggle this one.
|
||||
d.querySelectorAll(".netgroup").forEach((g) => { if (g.id !== "netgroup-" + r.dataset.coin) g.hidden = true; });
|
||||
d.querySelectorAll(".coinrow .caret").forEach((c) => { c.textContent = "▸"; });
|
||||
const group = d.querySelector("#netgroup-" + r.dataset.coin);
|
||||
group.hidden = !group.hidden;
|
||||
r.querySelector(".caret").textContent = group.hidden ? "▸" : "▾";
|
||||
}));
|
||||
d.querySelectorAll("[data-add]").forEach((r) => r.addEventListener("click", async () => {
|
||||
const [c, n] = r.dataset.add.split(":");
|
||||
d.hidden = true;
|
||||
|
|
@ -98,59 +238,139 @@ function render() {
|
|||
$("tabs").hidden = !ready;
|
||||
const gate = $("gate");
|
||||
gate.hidden = ready;
|
||||
// Header
|
||||
$("hBadge").textContent = s?.meta?.badge || "🛡";
|
||||
// Header: replace the badge slot with the coin's SVG and show
|
||||
// <wallet label> <coin · network + optional TEST tag>
|
||||
$("hBadge").innerHTML = s?.meta?.logo ? logoSvg(s.meta.logo, 22) : logoSvg(null, 22);
|
||||
$("hLabel").textContent = s?.label || "Aegis Wallet";
|
||||
$("hNet").innerHTML = s?.meta
|
||||
? `${esc(s.meta.coinLabel)} · ${esc(s.meta.networkLabel)}${s.meta.testnet ? " " + testnetTag() : ""}`
|
||||
: "";
|
||||
if (!ready) {
|
||||
const copy = {
|
||||
locked: ["🔒", "Unlock your password vault to open the wallet.", "Settings › Passwords. Aegis derives its keys from the vault seed, so there is nothing separate to unlock."],
|
||||
nosetup: ["🗝", "Set up a password vault to create your wallet.", "Settings › Passwords › Set up. Use a recovery phrase there and every wallet in Aegis can be recreated from it on any machine."],
|
||||
error: ["⚠", "This wallet could not start.", s?.error || ""],
|
||||
empty: ["🧩", "No wallets yet.", "Open the wallet picker at the top and pick a chain to create one."],
|
||||
empty: ["🧩", "No wallets yet.", "Open the wallet picker at the top and pick a coin, then a network to create one."],
|
||||
}[s?.phase || "locked"] || ["…", "Starting…", ""];
|
||||
gate.innerHTML = `<div class="big">${copy[0]}</div><div><b>${esc(copy[1])}</b></div><div class="hint" style="margin-top:8px">${esc(copy[2])}</div>`;
|
||||
}
|
||||
// Balance line
|
||||
const dot = $("dot");
|
||||
dot.className = "dot " + (s?.server ? (s?.scanning ? "busy" : "on") : "");
|
||||
$("netlbl").textContent = s?.server ? hostOf(s.server) + (s?.scanning ? " · syncing" : "") : (ready ? "connecting…" : (s?.network || ""));
|
||||
if (ready) {
|
||||
const total = (s.balance?.confirmed || 0) + (s.balance?.unconfirmed || 0);
|
||||
// Sia-specific gate: adapter is up, keys are derived, but no walletd URL
|
||||
// means no balance / history until the user configures one in Settings.
|
||||
if (chain() === "sc" && s.needsWalletdUrl) {
|
||||
$("balMain").textContent = "—"; $("balTicker").textContent = s.meta.ticker;
|
||||
$("netlbl").textContent = "point Aegis at a walletd node in Settings";
|
||||
$("tabs").hidden = true;
|
||||
gate.hidden = false;
|
||||
gate.innerHTML = `<div class="big">🗝</div><div><b>Point Aegis at a walletd node</b></div><div class="hint" style="margin-top:8px">Settings › Sia › walletd URL. Any public or self-hosted <span class="mono">go.sia.tech/walletd</span> in "full" index mode works.</div>`;
|
||||
return;
|
||||
}
|
||||
const total = balanceSum(s.balance);
|
||||
$("balMain").textContent = fmtBig(total);
|
||||
$("balTicker").textContent = s.meta.ticker;
|
||||
if (s.balance?.unconfirmed) $("netlbl").textContent += ` · ${fmtBig(s.balance.unconfirmed)} unconfirmed`;
|
||||
const uc = s.balance?.unconfirmed;
|
||||
if (uc && uc !== "0" && uc !== 0) $("netlbl").textContent += ` · ${fmtBig(uc)} unconfirmed`;
|
||||
} else {
|
||||
$("balMain").textContent = "—"; $("balTicker").textContent = "";
|
||||
}
|
||||
if (!ready) return;
|
||||
// Chain-specific header adjustments
|
||||
document.querySelector("nav [data-tab='settings']").hidden = false;
|
||||
// Receive tab
|
||||
const addr = s.address || "";
|
||||
if ($("addr").textContent !== addr) {
|
||||
$("addr").textContent = addr;
|
||||
drawQr(chain() === "bch" ? "bitcoincash:" + addr.replace(/^bitcoincash:/, "") : "tron:" + addr);
|
||||
drawQr(qrPayload(chain(), addr, sel()?.network));
|
||||
}
|
||||
$("addrMeta").textContent = s.addressPath ? "· " + s.addressPath : "";
|
||||
$("nextAddr").hidden = chain() !== "bch";
|
||||
$("openFaucet").hidden = !s.faucet;
|
||||
// Send tab: input placeholder + unit picker
|
||||
// Render SPL tokens list (SOL wallets only). Sending a token clicks
|
||||
// through to the Send tab with that asset pre-picked.
|
||||
renderTokens();
|
||||
applyUnitPicker();
|
||||
// Fee slider only meaningful for BCH
|
||||
$("feeField").hidden = chain() !== "bch";
|
||||
// History
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function renderTokens() {
|
||||
const s = sel();
|
||||
const tokens = (chain() === "sol" && s?.tokens) || [];
|
||||
const card = $("tokensCard");
|
||||
card.hidden = tokens.length === 0;
|
||||
if (!tokens.length) return;
|
||||
const el = $("tokensList");
|
||||
el.innerHTML = tokens.map((t) => {
|
||||
const dec = Number(t.decimals) || 0;
|
||||
const bal = fmtTokenAmount(t.balance, dec);
|
||||
return `<div class="tx" style="grid-template-columns:1fr auto auto;cursor:default">
|
||||
<div><div>${esc(t.symbol)}${t.name ? ' <span class="hint">' + esc(t.name) + '</span>' : ""}</div><div class="hint mono">${esc(t.mint.slice(0, 10))}…${esc(t.mint.slice(-6))}</div></div>
|
||||
<div class="amt2 in" style="align-self:center">${esc(bal)}</div>
|
||||
<button class="btn sm" data-mint="${esc(t.mint)}" data-symbol="${esc(t.symbol)}" data-decimals="${dec}" style="align-self:center">Send</button>
|
||||
</div>`;
|
||||
}).join("");
|
||||
el.querySelectorAll("button[data-mint]").forEach((b) => b.addEventListener("click", () => {
|
||||
sendAsset = { mint: b.dataset.mint, symbol: b.dataset.symbol, decimals: Number(b.dataset.decimals) };
|
||||
showTab("send");
|
||||
}));
|
||||
}
|
||||
// Same shape as index.js's fmtTokenAmount — string-safe for u64 SPL amounts.
|
||||
function fmtTokenAmount(rawStr, decimals) {
|
||||
const s = String(rawStr || "0");
|
||||
const neg = s.startsWith("-");
|
||||
const abs = neg ? s.slice(1) : s;
|
||||
const d = Number(decimals) || 0;
|
||||
if (d === 0) return (neg ? "-" : "") + abs;
|
||||
const pad = abs.padStart(d + 1, "0");
|
||||
const whole = pad.slice(0, pad.length - d);
|
||||
const frac = pad.slice(pad.length - d).replace(/0+$/, "");
|
||||
return (neg ? "-" : "") + whole + (frac ? "." + frac : "");
|
||||
}
|
||||
|
||||
function applyUnitPicker() {
|
||||
const s = sel(); if (!s) return;
|
||||
if (!unit) unit = "big";
|
||||
const big = bigUnitLabel(), small = smallUnitLabel();
|
||||
// ---- SPL asset picker (SOL wallets with tokens) ---------------------
|
||||
const tokens = (chain() === "sol" && s.tokens) || [];
|
||||
const assetField = $("sendAssetField");
|
||||
if (tokens.length) {
|
||||
assetField.hidden = false;
|
||||
const sel_ = $("sendAsset");
|
||||
// Rebuild whenever the asset set changes so a new token appears.
|
||||
const key = tokens.map((t) => t.mint).join("|");
|
||||
if (sel_.dataset.key !== key) {
|
||||
sel_.dataset.key = key;
|
||||
sel_.innerHTML = `<option value="">SOL — native</option>` + tokens.map((t) =>
|
||||
`<option value="${esc(t.mint)}" data-symbol="${esc(t.symbol)}" data-decimals="${Number(t.decimals) || 0}">${esc(t.symbol)}${t.name ? " · " + esc(t.name) : ""}</option>`
|
||||
).join("");
|
||||
sel_.onchange = () => {
|
||||
const opt = sel_.options[sel_.selectedIndex];
|
||||
sendAsset = opt && opt.value ? { mint: opt.value, symbol: opt.dataset.symbol, decimals: Number(opt.dataset.decimals) } : null;
|
||||
applyUnitPicker(); schedulePlan();
|
||||
};
|
||||
}
|
||||
// Reflect the current sendAsset back into the select.
|
||||
sel_.value = sendAsset ? sendAsset.mint : "";
|
||||
} else {
|
||||
assetField.hidden = true;
|
||||
sendAsset = null;
|
||||
}
|
||||
const isToken = sendAsset != null;
|
||||
const big = isToken ? sendAsset.symbol : bigUnitLabel();
|
||||
const small = isToken ? "raw" : smallUnitLabel();
|
||||
$("unitPicker").innerHTML =
|
||||
`<button data-u="big" class="${unit === "big" ? "on" : ""}" type="button">${esc(big)}</button>` +
|
||||
`<button data-u="small" class="${unit === "small" ? "on" : ""}" type="button">${esc(small)}</button>`;
|
||||
$("unitPicker").querySelectorAll("button").forEach((b) => b.addEventListener("click", () => setUnit(b.dataset.u)));
|
||||
$("sendTo").placeholder = chain() === "bch" ? "bitcoincash:q… or legacy 1…" : "T… (base58check, 34 chars)";
|
||||
$("sendTo").placeholder = ({
|
||||
bch: s.network === "chipnet" ? "bchtest:q… or legacy m…" : "bitcoincash:q… or legacy 1…",
|
||||
btc: s.network === "testnet" ? "tb1q… (or 2… / m…, n…)" : "bc1q… (or bc1p…, 3…, 1…)",
|
||||
trx: "T… (base58check, 34 chars)",
|
||||
sc: "addr1… (76-hex + checksum)",
|
||||
dgb: "dgb1q… (or D… / S… depending on family)",
|
||||
eth: "0x… (40 hex chars, EIP-55)",
|
||||
sol: "base58 public key (32 bytes)",
|
||||
})[chain()] || "recipient address";
|
||||
$("sendAmt").placeholder = unit === "big" ? "0.00" : "0";
|
||||
}
|
||||
function setUnit(u) {
|
||||
|
|
@ -163,12 +383,30 @@ function setUnit(u) {
|
|||
function amountUnits() {
|
||||
const raw = $("sendAmt").value.trim().replace(/,/g, "");
|
||||
if (!raw) return 0;
|
||||
if (unit === "small") return Math.round(Number(raw));
|
||||
const d = decimals();
|
||||
// For SPL tokens the amount is a raw u64 string in the token's own
|
||||
// smallest unit — same BigInt-safe path SC uses.
|
||||
const d = sendAsset ? Number(sendAsset.decimals) || 0 : decimals();
|
||||
const bigDecimals = sendAsset != null || d > 15;
|
||||
if (unit === "small") {
|
||||
if (bigDecimals) return raw.replace(/\D+/g, "") || "0";
|
||||
return Math.round(Number(raw));
|
||||
}
|
||||
const [w, f = ""] = raw.split(".");
|
||||
const frac = (f + "0".repeat(d)).slice(0, d);
|
||||
if (bigDecimals) {
|
||||
const total = (BigInt(w || "0") * (10n ** BigInt(d))) + BigInt(frac || "0");
|
||||
return total.toString();
|
||||
}
|
||||
return Number(w || 0) * Math.pow(10, d) + Number(frac || 0);
|
||||
}
|
||||
// Sum "confirmed + unconfirmed" BigInt-safely (strings for SC, numbers elsewhere).
|
||||
function balanceSum(b) {
|
||||
if (!b) return 0;
|
||||
if (typeof b.confirmed === "string" || typeof b.unconfirmed === "string") {
|
||||
return (BigInt(b.confirmed || "0") + BigInt(b.unconfirmed || "0")).toString();
|
||||
}
|
||||
return (b.confirmed || 0) + (b.unconfirmed || 0);
|
||||
}
|
||||
|
||||
// ---- history ---------------------------------------------------------------
|
||||
|
||||
|
|
@ -192,16 +430,29 @@ function renderHistory() {
|
|||
<div class="conf ${t.confirmations > 0 ? (t.status === "failed" ? "pending" : "") : "pending"}">${esc(conf)}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(sel().explorerTx + row.dataset.txid)));
|
||||
el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, row.dataset.txid))));
|
||||
}
|
||||
function shortAddr(a) {
|
||||
if (!a) return "";
|
||||
const s = String(a).replace(/^bitcoincash:/, "");
|
||||
const s = String(a).replace(/^bitcoincash:|^bchtest:/, "");
|
||||
return esc(s.slice(0, 10)) + "…" + esc(s.slice(-4));
|
||||
}
|
||||
|
||||
// ---- QR --------------------------------------------------------------------
|
||||
|
||||
// Coin-scheme URI so wallet apps that scan know which chain the payment is
|
||||
// for. Follows each chain's own convention (BIP21 for BTC-family, EIP-681
|
||||
// for ETH, Solana Pay for SOL, bare address for SC where no widely-agreed
|
||||
// URI scheme exists).
|
||||
function qrPayload(chain, address, network) {
|
||||
if (chain === "bch") return (network === "chipnet" ? "bchtest:" : "bitcoincash:") + String(address).replace(/^bitcoincash:|^bchtest:/, "");
|
||||
if (chain === "btc") return "bitcoin:" + address; // BIP21
|
||||
if (chain === "dgb") return "digibyte:" + address;
|
||||
if (chain === "eth") return "ethereum:" + address;
|
||||
if (chain === "sol") return "solana:" + address;
|
||||
if (chain === "trx") return "tron:" + address;
|
||||
return String(address);
|
||||
}
|
||||
function drawQr(text) {
|
||||
const cv = $("qr");
|
||||
const g = cv.getContext("2d");
|
||||
|
|
@ -225,7 +476,7 @@ $("nextAddr").addEventListener("click", async () => {
|
|||
try { const s = await S.invoke("nextAddress"); state.selected = { ...state.selected, ...s }; render(); }
|
||||
catch (e) { flash($("nextAddr"), "Failed"); }
|
||||
});
|
||||
$("viewAddr").addEventListener("click", () => openUrl(sel().explorerAddr + sel().address));
|
||||
$("viewAddr").addEventListener("click", () => openUrl(explorerHref(sel().explorerAddr, sel().address)));
|
||||
$("openFaucet").addEventListener("click", () => sel().faucet && openUrl(sel().faucet));
|
||||
function flash(btn, text) {
|
||||
const old = btn.textContent; btn.textContent = text;
|
||||
|
|
@ -252,6 +503,16 @@ async function updatePlan() {
|
|||
$("sendToHint").textContent = "";
|
||||
if (!to || (!sendMax && !amountUnits())) return;
|
||||
try {
|
||||
if (sendAsset) {
|
||||
// SPL token flow — amount is raw units of the token's decimals.
|
||||
const p = await S.invoke("planTokenSend", { mint: sendAsset.mint, to, amount: amountUnits() });
|
||||
lastPlan = { _token: true, ...p };
|
||||
$("sumAmt").textContent = fmtTokenAmount(p.recipients[0].value, sendAsset.decimals) + " " + sendAsset.symbol;
|
||||
$("sumFee").textContent = fmtBig(p.fee, decimals()) + " SOL";
|
||||
$("sumTotal").textContent = fmtTokenAmount(p.total, sendAsset.decimals) + " " + sendAsset.symbol;
|
||||
$("sendBtn").disabled = false;
|
||||
return;
|
||||
}
|
||||
const feeRate = chain() === "bch" ? Number($("feeRate").value) : undefined;
|
||||
const p = await S.invoke("planSend", { to, amount: amountUnits(), feeRate, sendMax });
|
||||
lastPlan = p;
|
||||
|
|
@ -272,11 +533,14 @@ $("sendBtn").addEventListener("click", async () => {
|
|||
const msg = $("sendMsg"); msg.hidden = true;
|
||||
$("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…";
|
||||
try {
|
||||
const isToken = sendAsset && lastPlan._token;
|
||||
const feeRate = chain() === "bch" ? Number($("feeRate").value) : undefined;
|
||||
const r = await S.invoke("send", { to: $("sendTo").value.trim(), amount: amountUnits(), feeRate, sendMax });
|
||||
const r = isToken
|
||||
? await S.invoke("sendToken", { mint: sendAsset.mint, to: $("sendTo").value.trim(), amount: amountUnits() })
|
||||
: await S.invoke("send", { to: $("sendTo").value.trim(), amount: amountUnits(), feeRate, sendMax });
|
||||
msg.className = "msg ok";
|
||||
msg.innerHTML = `Sent. <a class="link" data-tx="${esc(r.txid)}">${esc(r.txid.slice(0, 16))}…</a>`;
|
||||
msg.querySelector("a").addEventListener("click", () => openUrl(sel().explorerTx + r.txid));
|
||||
msg.querySelector("a").addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, r.txid)));
|
||||
msg.hidden = false;
|
||||
$("sendTo").value = ""; $("sendAmt").value = ""; sendMax = false;
|
||||
$("sendMax").classList.remove("primary"); $("sendAmt").disabled = false;
|
||||
|
|
@ -294,8 +558,15 @@ function fillSettings() {
|
|||
const s = sel(); if (!s) return;
|
||||
$("bchSettings").hidden = chain() !== "bch";
|
||||
$("trxSettings").hidden = chain() !== "trx";
|
||||
$("removeBtn").disabled = !!s.isLegacy;
|
||||
$("removeHint").textContent = s.isLegacy ? "The default BCH wallet cannot be removed (it protects legacy funds)." : "";
|
||||
$("scSettings").hidden = chain() !== "sc";
|
||||
$("dgbSettings").hidden = chain() !== "dgb";
|
||||
$("btcSettings").hidden = chain() !== "btc";
|
||||
$("ethSettings").hidden = chain() !== "eth";
|
||||
$("solSettings").hidden = chain() !== "sol";
|
||||
$("removeBtn").disabled = !!s.isLegacy && s.chain === "bch";
|
||||
$("removeHint").textContent = (s.isLegacy && s.chain === "bch")
|
||||
? "The default BCH wallet cannot be removed (it protects legacy funds)."
|
||||
: (s.isLegacy && s.chain === "sc" ? "Removing this wallet unlinks it from Aegis. Funds stay on-chain and reappear if you add a Siacoin wallet again with the legacy seed slot." : "");
|
||||
$("renameLabel").value = s.label || "";
|
||||
if (chain() === "bch") {
|
||||
if (!settingsFilled) {
|
||||
|
|
@ -303,10 +574,48 @@ function fillSettings() {
|
|||
$("setServers").value = (state.bchServers?.list || []).join("\n");
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("serverHint").textContent = (state.bchServers?.custom ? "Custom list." : "Bundled defaults.") + (s.server ? " Connected to " + hostOf(s.server) + "." : " Not connected.");
|
||||
$("bchServersRow").hidden = s.network !== "mainnet";
|
||||
$("serverHint").textContent = s.network !== "mainnet"
|
||||
? "Chipnet uses bundled defaults in this build."
|
||||
: (state.bchServers?.custom ? "Custom list." : "Bundled defaults.") + (s.server ? " Connected to " + hostOf(s.server) + "." : " Not connected.");
|
||||
$("purpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
} else if (chain() === "trx") {
|
||||
$("trxPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
} else if (chain() === "sc") {
|
||||
if (!settingsFilled) {
|
||||
$("setWalletdUrl").value = s.walletdUrl || "";
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("scPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("scRecovery").innerHTML = "";
|
||||
} else if (chain() === "dgb") {
|
||||
if (!settingsFilled) {
|
||||
fillFamilyPicker("Dgb", s);
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("dgbPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("dgbRecovery").innerHTML = "";
|
||||
} else if (chain() === "btc") {
|
||||
if (!settingsFilled) {
|
||||
fillFamilyPicker("Btc", s);
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("btcPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("btcRecovery").innerHTML = "";
|
||||
} else if (chain() === "eth") {
|
||||
if (!settingsFilled) {
|
||||
$("setEthRpcUrl").value = s.rpcUrl || "";
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("ethPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("ethRecovery").innerHTML = "";
|
||||
} else if (chain() === "sol") {
|
||||
if (!settingsFilled) {
|
||||
$("setSolRpcUrl").value = s.rpcUrl || "";
|
||||
settingsFilled = true;
|
||||
}
|
||||
$("solPurpose").textContent = "silentmode/addons/" + (s.purpose || "");
|
||||
$("solRecovery").innerHTML = "";
|
||||
}
|
||||
renderSites();
|
||||
}
|
||||
|
|
@ -336,14 +645,15 @@ $("applySettings").addEventListener("click", async () => {
|
|||
try {
|
||||
const path = $("setPath").value.trim();
|
||||
const servers = $("setServers").value.split(/\n+/).map((s) => s.trim()).filter(Boolean);
|
||||
// Apply path (per-wallet) and servers (shared) separately.
|
||||
if (path && path !== (sel().accountPath || "")) {
|
||||
state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: path });
|
||||
}
|
||||
if (sel().network === "mainnet") {
|
||||
const currentJoined = (state.bchServers?.list || []).join();
|
||||
if (state.bchServers?.custom || servers.join() !== currentJoined) {
|
||||
state = await S.invoke("setBchServers", { servers });
|
||||
}
|
||||
}
|
||||
settingsFilled = false; fillSettings(); render();
|
||||
flash($("applySettings"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
|
|
@ -360,7 +670,7 @@ $("renameBtn").addEventListener("click", async () => {
|
|||
});
|
||||
$("removeBtn").addEventListener("click", async () => {
|
||||
const s = sel(); if (!s || s.isLegacy) return;
|
||||
if (!confirm(`Remove the wallet "${s.label}"?\n\nThe on-chain address stays; the wallet is unlinked from Aegis. You can add it back later by creating a new wallet on the same chain.`)) return;
|
||||
if (!confirm(`Remove the wallet "${s.label}"?\n\nThe on-chain address stays; the wallet is unlinked from Aegis. You can add it back later by creating a new wallet on the same coin + network.`)) return;
|
||||
try { state = await S.invoke("removeWallet", { id: state.selectedWalletId }); settingsFilled = false; render(); }
|
||||
catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; }
|
||||
});
|
||||
|
|
@ -377,8 +687,122 @@ function recoveryHtml(r) {
|
|||
if (r.xprv) h += `<div class="lbl">Account private key (xprv)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>`;
|
||||
return h;
|
||||
}
|
||||
// Wipe a revealed key when the user leaves the Settings tab.
|
||||
document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => { if (b.dataset.tab !== "settings") $("recovery").innerHTML = ""; }));
|
||||
document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => {
|
||||
if (b.dataset.tab !== "settings") {
|
||||
$("recovery").innerHTML = "";
|
||||
$("scRecovery").innerHTML = "";
|
||||
$("dgbRecovery").innerHTML = "";
|
||||
$("btcRecovery").innerHTML = "";
|
||||
$("ethRecovery").innerHTML = "";
|
||||
$("solRecovery").innerHTML = "";
|
||||
}
|
||||
}));
|
||||
|
||||
// Sia-specific settings.
|
||||
$("applyWalletdUrl").addEventListener("click", async () => {
|
||||
const msg = $("settingsMsg"); msg.hidden = true;
|
||||
try {
|
||||
state = await S.invoke("setWalletdUrl", { id: state.selectedWalletId, walletdUrl: $("setWalletdUrl").value.trim() });
|
||||
settingsFilled = false; fillSettings(); render(); flash($("applyWalletdUrl"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
});
|
||||
$("showScSeed").addEventListener("click", async () => {
|
||||
try {
|
||||
const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true });
|
||||
$("scRecovery").innerHTML =
|
||||
`<div class="lbl">First address (index 0)</div><div class="mono">${esc(r.xpub || "")}</div>` +
|
||||
(r.xprv ? `<div class="lbl">Wallet seed (hex)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>` : "");
|
||||
} catch (e) { $("scRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
|
||||
// Family-picker helper used by both DGB and BTC. Prefix is "Dgb" or "Btc":
|
||||
// the DOM IDs are #set<Prefix>Family + #set<Prefix>Path.
|
||||
function fillFamilyPicker(prefix, s) {
|
||||
const families = s.meta?.addressFamilies || [];
|
||||
const current = String(s.accountPath || "");
|
||||
let currentId = families.find((f) => f.defaultAccountPath === current)?.id;
|
||||
if (!currentId) {
|
||||
const m = /^m\/(\d+)'/.exec(current);
|
||||
const purpose = m ? Number(m[1]) : null;
|
||||
currentId = families.find((f) => f.purpose === purpose)?.id || families[0]?.id;
|
||||
}
|
||||
$(`set${prefix}Path`).value = current || families[0]?.defaultAccountPath || "";
|
||||
$(`set${prefix}Family`).innerHTML = families.map((f) =>
|
||||
`<option value="${esc(f.id)}" data-path="${esc(f.defaultAccountPath)}" ${f.id === currentId ? "selected" : ""}>${esc(f.label)}</option>`
|
||||
).join("");
|
||||
}
|
||||
// Any family select → auto-fill the sibling path input.
|
||||
document.addEventListener("change", (e) => {
|
||||
const t = e.target;
|
||||
if (!t) return;
|
||||
const m = /^set(Dgb|Btc)Family$/.exec(t.id || "");
|
||||
if (!m) return;
|
||||
const opt = t.options[t.selectedIndex];
|
||||
if (opt && opt.dataset.path) $(`set${m[1]}Path`).value = opt.dataset.path;
|
||||
});
|
||||
$("applyDgbPath").addEventListener("click", async () => {
|
||||
const msg = $("settingsMsg"); msg.hidden = true;
|
||||
try {
|
||||
state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: $("setDgbPath").value.trim() });
|
||||
settingsFilled = false; fillSettings(); render(); flash($("applyDgbPath"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
});
|
||||
$("applyBtcPath").addEventListener("click", async () => {
|
||||
const msg = $("settingsMsg"); msg.hidden = true;
|
||||
try {
|
||||
state = await S.invoke("setAccountPath", { id: state.selectedWalletId, accountPath: $("setBtcPath").value.trim() });
|
||||
settingsFilled = false; fillSettings(); render(); flash($("applyBtcPath"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
});
|
||||
$("showBtcXpub").addEventListener("click", async () => {
|
||||
try { const r = await S.invoke("recovery", { id: state.selectedWalletId }); $("btcRecovery").innerHTML = recoveryHtml(r); }
|
||||
catch (e) { $("btcRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
$("showBtcXprv").addEventListener("click", async () => {
|
||||
try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("btcRecovery").innerHTML = recoveryHtml(r); }
|
||||
catch (e) { $("btcRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
|
||||
// ETH / SOL: RPC URL.
|
||||
$("applyEthRpc").addEventListener("click", async () => {
|
||||
const msg = $("settingsMsg"); msg.hidden = true;
|
||||
try {
|
||||
state = await S.invoke("setRpcUrl", { id: state.selectedWalletId, rpcUrl: $("setEthRpcUrl").value.trim() });
|
||||
settingsFilled = false; fillSettings(); render(); flash($("applyEthRpc"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
});
|
||||
$("applySolRpc").addEventListener("click", async () => {
|
||||
const msg = $("settingsMsg"); msg.hidden = true;
|
||||
try {
|
||||
state = await S.invoke("setRpcUrl", { id: state.selectedWalletId, rpcUrl: $("setSolRpcUrl").value.trim() });
|
||||
settingsFilled = false; fillSettings(); render(); flash($("applySolRpc"), "Applied");
|
||||
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
});
|
||||
$("showEthKey").addEventListener("click", async () => {
|
||||
try {
|
||||
const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true });
|
||||
$("ethRecovery").innerHTML =
|
||||
`<div class="lbl">Address</div><div class="mono">${esc(sel().address || "")}</div>` +
|
||||
`<div class="lbl">Public key (uncompressed hex)</div><div class="mono">${esc(r.xpub || "")}</div>` +
|
||||
(r.xprv ? `<div class="lbl">Private key (hex)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>` : "");
|
||||
} catch (e) { $("ethRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
$("showSolKey").addEventListener("click", async () => {
|
||||
try {
|
||||
const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true });
|
||||
$("solRecovery").innerHTML =
|
||||
`<div class="lbl">Address (public key, base58)</div><div class="mono">${esc(r.xpub || "")}</div>` +
|
||||
(r.xprv ? `<div class="lbl">Wallet seed (hex, 32 bytes)</div><div class="mono" style="color:var(--danger)">${esc(r.xprv)}</div>` : "");
|
||||
} catch (e) { $("solRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
$("showDgbXpub").addEventListener("click", async () => {
|
||||
try { const r = await S.invoke("recovery", { id: state.selectedWalletId }); $("dgbRecovery").innerHTML = recoveryHtml(r); }
|
||||
catch (e) { $("dgbRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
$("showDgbXprv").addEventListener("click", async () => {
|
||||
try { const r = await S.invoke("recovery", { id: state.selectedWalletId, reveal: true }); $("dgbRecovery").innerHTML = recoveryHtml(r); }
|
||||
catch (e) { $("dgbRecovery").textContent = cleanErr(e); }
|
||||
});
|
||||
|
||||
// ---- boot ------------------------------------------------------------------
|
||||
S.on("state", (s) => { state = s; render(); if (tab === "settings") fillSettings(); });
|
||||
|
|
|
|||
|
|
@ -140,3 +140,367 @@ const tronLink = {
|
|||
|
||||
theseus.contextBridge.exposeInMainWorld("tronWeb", tronWeb);
|
||||
theseus.contextBridge.exposeInMainWorld("tronLink", tronLink);
|
||||
|
||||
// -------- Main-world bridges (window.ethereum, window.solana) ---------------
|
||||
//
|
||||
// EIP-1193 (Ethereum) and the Solana wallet-adapter both expect the wallet
|
||||
// object to touch the dapp's own JS values — Solana in particular passes
|
||||
// `Transaction` instances whose `.serializeMessage()` method the wallet has
|
||||
// to call. Electron's contextBridge shallow-copies function arguments
|
||||
// between worlds and strips methods, so bridges that need to call methods
|
||||
// on dapp-side objects have to live in the main world. We inject a `<script>`
|
||||
// with the bridge source; it runs synchronously in the main world and talks
|
||||
// to us via `window.postMessage` on a namespaced envelope. This mirrors how
|
||||
// MetaMask and Phantom bridge extension code back to page code.
|
||||
|
||||
// Namespace used on the postMessage envelope. Includes the addon id so a
|
||||
// page that runs multiple dapp-wallet extensions doesn't misroute messages.
|
||||
const AEGIS_TAG = "aegis-" + theseus.id;
|
||||
const pendingCalls = new Map();
|
||||
window.addEventListener("message", async (e) => {
|
||||
const d = e && e.data;
|
||||
if (!d || d.aegisTag !== AEGIS_TAG) return;
|
||||
if (d.kind === "request") {
|
||||
// Forward main-world → isolated-world → addon.
|
||||
try {
|
||||
const result = await call(d.msg, d.payload);
|
||||
window.postMessage({ aegisTag: AEGIS_TAG, kind: "response", id: d.id, ok: true, result }, location.origin);
|
||||
} catch (err) {
|
||||
window.postMessage({ aegisTag: AEGIS_TAG, kind: "response", id: d.id, ok: false, error: String(err && err.message || err) }, location.origin);
|
||||
}
|
||||
}
|
||||
});
|
||||
function emitToMainWorld(event, data) {
|
||||
try { window.postMessage({ aegisTag: AEGIS_TAG, kind: "event", event, data }, location.origin); } catch {}
|
||||
}
|
||||
|
||||
// The main-world bridge — installed as a page-level `<script>` so it can
|
||||
// call methods on Transaction objects the dapp hands it, and so the globals
|
||||
// it defines look like ordinary page code to the dapp.
|
||||
const mainWorldSource = `(function () {
|
||||
if (window.__aegisBridge) return;
|
||||
window.__aegisBridge = true;
|
||||
const TAG = ${JSON.stringify(AEGIS_TAG)};
|
||||
const pending = new Map();
|
||||
let seq = 1;
|
||||
function invoke(msg, payload) {
|
||||
const id = "r" + (seq++);
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
window.postMessage({ aegisTag: TAG, kind: "request", id, msg, payload }, location.origin);
|
||||
});
|
||||
}
|
||||
window.addEventListener("message", (e) => {
|
||||
const d = e && e.data;
|
||||
if (!d || d.aegisTag !== TAG) return;
|
||||
if (d.kind === "response") {
|
||||
const p = pending.get(d.id);
|
||||
if (!p) return;
|
||||
pending.delete(d.id);
|
||||
d.ok ? p.resolve(d.result) : p.reject(new Error(d.error));
|
||||
} else if (d.kind === "event") {
|
||||
dispatchEvent(d.event, d.data);
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Ethereum (EIP-1193) ------------------------------------------------
|
||||
const ethListeners = { connect: [], disconnect: [], accountsChanged: [], chainChanged: [], message: [] };
|
||||
let ethState = { address: null, chainIdHex: "0x1", networkVersion: "1" };
|
||||
function ethEmit(event, data) {
|
||||
for (const fn of ethListeners[event] || []) { try { fn(data); } catch {} }
|
||||
}
|
||||
// After a chain switch or add, refresh the local chainId/address state
|
||||
// and fire the two events dapps expect (accountsChanged +
|
||||
// chainChanged). MetaMask does the same round-trip after switchChain.
|
||||
async function pullEthStateAndEmit() {
|
||||
try {
|
||||
const s = await invoke("eth.state");
|
||||
const oldChain = ethState.chainIdHex, oldAddr = ethState.address;
|
||||
ethState.address = s.address || null;
|
||||
ethState.chainIdHex = s.chainIdHex;
|
||||
ethState.networkVersion = s.networkVersion;
|
||||
window.ethereum.selectedAddress = ethState.address;
|
||||
window.ethereum.chainId = ethState.chainIdHex;
|
||||
window.ethereum.networkVersion = ethState.networkVersion;
|
||||
if (s.chainIdHex !== oldChain) ethEmit("chainChanged", s.chainIdHex);
|
||||
if ((s.address || null) !== oldAddr) ethEmit("accountsChanged", s.address ? [s.address] : []);
|
||||
} catch {}
|
||||
}
|
||||
async function ethHandle(method, params) {
|
||||
params = Array.isArray(params) ? params : (params ? [params] : []);
|
||||
switch (method) {
|
||||
case "eth_requestAccounts": {
|
||||
const r = await invoke("eth.requestAccounts");
|
||||
ethState.address = r.address;
|
||||
ethState.chainIdHex = r.chainIdHex;
|
||||
ethState.networkVersion = r.networkVersion;
|
||||
window.ethereum.selectedAddress = r.address;
|
||||
window.ethereum.chainId = r.chainIdHex;
|
||||
window.ethereum.networkVersion = r.networkVersion;
|
||||
ethEmit("accountsChanged", [r.address]);
|
||||
return [r.address];
|
||||
}
|
||||
case "eth_accounts":
|
||||
return ethState.address ? [ethState.address] : [];
|
||||
case "eth_chainId":
|
||||
return ethState.chainIdHex;
|
||||
case "net_version":
|
||||
return ethState.networkVersion;
|
||||
case "personal_sign": {
|
||||
// Both param orders are seen in the wild: [message, from] and [from, message].
|
||||
const [a, b] = params;
|
||||
const looksLikeAddr = (s) => typeof s === "string" && /^0x[0-9a-fA-F]{40}$/.test(s);
|
||||
const message = looksLikeAddr(a) ? b : a;
|
||||
return (await invoke("eth.personalSign", { message: String(message) })).signature;
|
||||
}
|
||||
case "eth_sign": {
|
||||
// Legacy method. Same shape as personal_sign for our purposes.
|
||||
const [_from, msg] = params;
|
||||
return (await invoke("eth.personalSign", { message: String(msg) })).signature;
|
||||
}
|
||||
case "eth_sendTransaction": {
|
||||
const tx = params[0] || {};
|
||||
return (await invoke("eth.sendTransaction", { tx })).txid;
|
||||
}
|
||||
case "eth_signTypedData_v4":
|
||||
case "eth_signTypedData":
|
||||
case "eth_signTypedData_v3": {
|
||||
// v3/v4 differ mostly in nested-struct support; the encoder handles
|
||||
// both. v1 is the flat "type[]" schema that Metamask deprecated —
|
||||
// reject it, dapps that still use v1 should upgrade.
|
||||
const [a, b] = params;
|
||||
const looksLikeAddr = (s) => typeof s === "string" && /^0x[0-9a-fA-F]{40}$/.test(s);
|
||||
const typedData = looksLikeAddr(a) ? b : a;
|
||||
return (await invoke("eth.signTypedData", { typedData })).signature;
|
||||
}
|
||||
case "wallet_switchEthereumChain": {
|
||||
const target = String((params[0] && params[0].chainId) || "").toLowerCase();
|
||||
try {
|
||||
const r = await invoke("eth.switchChain", { chainId: target });
|
||||
await pullEthStateAndEmit();
|
||||
return r;
|
||||
}
|
||||
catch (e) {
|
||||
// EIP-3326: preserve the 4902 signal the isolated-world handler
|
||||
// stamps on the Error so dapps fall through to addChain.
|
||||
if (/is not added/i.test(e.message || "")) { const err = new Error(e.message); err.code = 4902; throw err; }
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
case "wallet_addEthereumChain": {
|
||||
// EIP-3085. Approval overlay + persistence live in the addon.
|
||||
const r = await invoke("eth.addChain", { params: params[0] });
|
||||
await pullEthStateAndEmit();
|
||||
return r;
|
||||
}
|
||||
case "wallet_getPermissions":
|
||||
case "wallet_requestPermissions":
|
||||
// Minimum shape most dapps accept.
|
||||
return [{ parentCapability: "eth_accounts" }];
|
||||
default:
|
||||
// Read passthrough: eth_getBalance, eth_call, eth_blockNumber, etc.
|
||||
return invoke("eth.rpc", { method, params });
|
||||
}
|
||||
}
|
||||
const ethereum = {
|
||||
isAegis: true,
|
||||
isMetaMask: false,
|
||||
chainId: ethState.chainIdHex,
|
||||
networkVersion: ethState.networkVersion,
|
||||
selectedAddress: null,
|
||||
request: (opts) => ethHandle(String(opts && opts.method || ""), (opts && opts.params) || []),
|
||||
// EIP-1193 event surface.
|
||||
on: (event, fn) => { if (ethListeners[event]) ethListeners[event].push(fn); },
|
||||
removeListener: (event, fn) => {
|
||||
const arr = ethListeners[event]; if (!arr) return;
|
||||
const i = arr.indexOf(fn); if (i >= 0) arr.splice(i, 1);
|
||||
},
|
||||
// Legacy compat some old dapps still call.
|
||||
enable: () => ethereum.request({ method: "eth_requestAccounts" }),
|
||||
sendAsync: (payload, cb) => ethereum.request(payload).then((result) => cb(null, { id: payload.id, jsonrpc: "2.0", result }), (err) => cb(err)),
|
||||
send: (methodOrPayload, params) => ethereum.request(typeof methodOrPayload === "string" ? { method: methodOrPayload, params } : methodOrPayload),
|
||||
};
|
||||
|
||||
// ---- Solana (wallet-adapter shape) --------------------------------------
|
||||
const solListeners = { connect: [], disconnect: [], accountChanged: [] };
|
||||
let solState = { publicKey: null };
|
||||
function makePubkey(base58) {
|
||||
return {
|
||||
toString: () => base58,
|
||||
toBase58: () => base58,
|
||||
toBytes: () => base58ToBytes(base58),
|
||||
equals: (other) => other && other.toString && other.toString() === base58,
|
||||
_bn: null,
|
||||
};
|
||||
}
|
||||
// Local base58 decoder — needed to expose PublicKey.toBytes(). Alphabet
|
||||
// matches Bitcoin's (the only base58 flavor in real use).
|
||||
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
function base58ToBytes(s) {
|
||||
let zeros = 0;
|
||||
while (zeros < s.length && s[zeros] === B58[0]) zeros++;
|
||||
const out = new Uint8Array(Math.ceil(s.length * 733 / 1000 + 1));
|
||||
let length = 0;
|
||||
for (let i = zeros; i < s.length; i++) {
|
||||
const v = B58.indexOf(s[i]);
|
||||
if (v < 0) throw new Error("bad base58 char " + s[i]);
|
||||
let carry = v, j = 0;
|
||||
for (let k = out.length - 1; (carry !== 0 || j < length) && k >= 0; k--, j++) {
|
||||
carry += 58 * out[k];
|
||||
out[k] = carry & 0xff;
|
||||
carry >>>= 8;
|
||||
}
|
||||
length = j;
|
||||
}
|
||||
let it = out.length - length;
|
||||
while (it < out.length && out[it] === 0) it++;
|
||||
const total = zeros + (out.length - it);
|
||||
const dec = new Uint8Array(total);
|
||||
let p = zeros;
|
||||
while (it < out.length) dec[p++] = out[it++];
|
||||
return dec;
|
||||
}
|
||||
function bytesToBase58(b) {
|
||||
let zeros = 0;
|
||||
while (zeros < b.length && b[zeros] === 0) zeros++;
|
||||
const buf = new Uint8Array(Math.ceil(b.length * 138 / 100 + 1));
|
||||
let length = 0;
|
||||
for (let i = zeros; i < b.length; i++) {
|
||||
let carry = b[i], j = 0;
|
||||
for (let k = buf.length - 1; (carry !== 0 || j < length) && k >= 0; k--, j++) {
|
||||
carry += (buf[k] << 8) >>> 0;
|
||||
buf[k] = carry % 58;
|
||||
carry = (carry / 58) | 0;
|
||||
}
|
||||
length = j;
|
||||
}
|
||||
let it = buf.length - length;
|
||||
while (it < buf.length && buf[it] === 0) it++;
|
||||
let out = "";
|
||||
for (let i = 0; i < zeros; i++) out += B58[0];
|
||||
for (; it < buf.length; it++) out += B58[buf[it]];
|
||||
return out;
|
||||
}
|
||||
function u8ToBase64(u8) {
|
||||
let s = "";
|
||||
for (let i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]);
|
||||
return btoa(s);
|
||||
}
|
||||
function base64ToU8(s) {
|
||||
const bin = atob(s);
|
||||
const u8 = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
||||
return u8;
|
||||
}
|
||||
async function solConnect() {
|
||||
const r = await invoke("sol.connect");
|
||||
solState.publicKey = makePubkey(r.address);
|
||||
window.solana.publicKey = solState.publicKey;
|
||||
window.solana.isConnected = true;
|
||||
for (const fn of solListeners.connect) { try { fn(solState.publicKey); } catch {} }
|
||||
return { publicKey: solState.publicKey };
|
||||
}
|
||||
async function solDisconnect() {
|
||||
solState.publicKey = null;
|
||||
window.solana.publicKey = null;
|
||||
window.solana.isConnected = false;
|
||||
for (const fn of solListeners.disconnect) { try { fn(); } catch {} }
|
||||
}
|
||||
async function solSignMessage(u8) {
|
||||
if (!(u8 instanceof Uint8Array)) u8 = new Uint8Array(u8 || []);
|
||||
const r = await invoke("sol.signMessage", { messageB64: u8ToBase64(u8) });
|
||||
return { publicKey: solState.publicKey, signature: base58ToBytes(r.signature) };
|
||||
}
|
||||
// Solana Transaction objects have serializeMessage() → Uint8Array and
|
||||
// addSignature(publicKey, sig) — we live in the main world, so we can
|
||||
// call both. Wallets that live in an isolated content-script can't.
|
||||
async function solSignAndSendTransaction(tx, opts) {
|
||||
if (!tx || typeof tx.serialize !== "function") {
|
||||
throw new Error("Aegis: pass a @solana/web3.js Transaction (needs .serialize)");
|
||||
}
|
||||
// Serialize the FULL wire including any partial signatures the dapp
|
||||
// has already collected (multi-signer flows: session keys, escrows,
|
||||
// ephemeral co-signers). Aegis fills the wallet's own signature slot
|
||||
// in the addon and leaves the other slots untouched.
|
||||
const wire = tx.serialize({ requireAllSignatures: false, verifySignatures: false });
|
||||
const r = await invoke("sol.signAndSend", { wireB64: u8ToBase64(new Uint8Array(wire)) });
|
||||
return { signature: r.txid, publicKey: solState.publicKey };
|
||||
}
|
||||
async function solSignTransaction(tx) {
|
||||
if (!tx || typeof tx.serializeMessage !== "function" || typeof tx.addSignature !== "function") {
|
||||
throw new Error("Aegis: pass a @solana/web3.js Transaction");
|
||||
}
|
||||
const messageBytes = tx.serializeMessage();
|
||||
const r = await invoke("sol.signMessage", { messageB64: u8ToBase64(new Uint8Array(messageBytes)) });
|
||||
// r.signature is base58 of the 64-byte ed25519 sig.
|
||||
tx.addSignature(solState.publicKey, base58ToBytes(r.signature));
|
||||
return tx;
|
||||
}
|
||||
const solana = {
|
||||
isAegis: true,
|
||||
isPhantom: true, // set so dapps that gate on isPhantom pick us
|
||||
isConnected: false,
|
||||
publicKey: null,
|
||||
connect: async (opts) => solConnect(opts),
|
||||
disconnect: solDisconnect,
|
||||
signMessage: (u8) => solSignMessage(u8),
|
||||
signTransaction: (tx) => solSignTransaction(tx),
|
||||
signAllTransactions: async (txs) => {
|
||||
const out = [];
|
||||
for (const tx of txs) out.push(await solSignTransaction(tx));
|
||||
return out;
|
||||
},
|
||||
signAndSendTransaction: (tx, opts) => solSignAndSendTransaction(tx, opts),
|
||||
request: async (opts) => {
|
||||
const method = String(opts && opts.method || "");
|
||||
const params = opts && opts.params || {};
|
||||
if (method === "connect") return solConnect();
|
||||
if (method === "disconnect") return solDisconnect();
|
||||
if (method === "signMessage") return solSignMessage(params.message);
|
||||
if (method === "signTransaction") return solSignTransaction(params.transaction);
|
||||
if (method === "signAndSendTransaction") return solSignAndSendTransaction(params.transaction, params.options);
|
||||
throw new Error("Aegis: unsupported solana method " + method);
|
||||
},
|
||||
on: (event, fn) => { if (solListeners[event]) solListeners[event].push(fn); },
|
||||
off: (event, fn) => {
|
||||
const arr = solListeners[event]; if (!arr) return;
|
||||
const i = arr.indexOf(fn); if (i >= 0) arr.splice(i, 1);
|
||||
},
|
||||
removeAllListeners: () => { Object.keys(solListeners).forEach((k) => solListeners[k].length = 0); },
|
||||
};
|
||||
|
||||
function dispatchEvent(event, data) {
|
||||
if (event === "eth.accountsChanged") { ethState.address = data.address || null; ethereum.selectedAddress = ethState.address; ethEmit("accountsChanged", data.address ? [data.address] : []); }
|
||||
else if (event === "eth.chainChanged") { ethState.chainIdHex = data.chainIdHex; ethState.networkVersion = data.networkVersion; ethereum.chainId = data.chainIdHex; ethereum.networkVersion = data.networkVersion; ethEmit("chainChanged", data.chainIdHex); }
|
||||
else if (event === "sol.accountChanged") {
|
||||
const pk = data.address ? makePubkey(data.address) : null;
|
||||
solState.publicKey = pk; solana.publicKey = pk;
|
||||
for (const fn of solListeners.accountChanged || []) { try { fn(pk); } catch {} }
|
||||
}
|
||||
}
|
||||
|
||||
// Install the globals. Defined lazily via Object.defineProperty so we
|
||||
// survive dapps that check hasOwnProperty(window, "ethereum") after page
|
||||
// load — MetaMask does the same trick.
|
||||
try { Object.defineProperty(window, "ethereum", { value: ethereum, writable: false, configurable: false }); } catch { window.ethereum = ethereum; }
|
||||
try { Object.defineProperty(window, "solana", { value: solana, writable: false, configurable: false }); } catch { window.solana = solana; }
|
||||
// EIP-6963 provider announcement so wagmi / RainbowKit discover Aegis.
|
||||
try {
|
||||
const info = { uuid: crypto.randomUUID(), name: "Aegis", icon: "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpolygon points='16,2 29,9 29,23 16,30 3,23 3,9' fill='none' stroke='%23d6ff3d' stroke-width='2.5'/%3E%3Ccircle cx='16' cy='16' r='4.3' fill='none' stroke='%23d6ff3d' stroke-width='1.6'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23d6ff3d'/%3E%3C/svg%3E", rdns: "st.silentmode.aegis" };
|
||||
const announce = () => window.dispatchEvent(new CustomEvent("eip6963:announceProvider", { detail: Object.freeze({ info, provider: ethereum }) }));
|
||||
announce();
|
||||
window.addEventListener("eip6963:requestProvider", announce);
|
||||
} catch {}
|
||||
})();`;
|
||||
|
||||
// Actually push the script into the main world. Doing this at
|
||||
// document_start (which is when this preload runs) means the bridge is in
|
||||
// place before the dapp's own scripts execute.
|
||||
try {
|
||||
const s = document.createElement("script");
|
||||
s.textContent = mainWorldSource;
|
||||
(document.head || document.documentElement).appendChild(s);
|
||||
s.remove();
|
||||
} catch (e) {
|
||||
console.warn("[aegis] main-world bridge install failed:", e && e.message || e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
--danger:#ff5b5b; --board:#0a0d13; }
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#eff3fb; --line:rgba(0,0,0,.10);
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid:#3a5c00;
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid: #088A66;
|
||||
--board:#dde3ee; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
:root { --bg:#f8faff; --panel:#ffffff; --card:#f1f4fa; --line:rgba(0,0,0,.10);
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --sia:#1abc6a;
|
||||
/* Darker acid on light backgrounds. */
|
||||
--acid:#3a5c00; }
|
||||
--acid: #088A66; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
contrast on #ffffff so text and single-pixel accents stay
|
||||
readable. Tint fills (rgba(214,255,61,.08) etc.) stay as-is:
|
||||
at low alpha the specific hue barely matters. */
|
||||
--acid: #3a5c00;
|
||||
--acid: #088A66;
|
||||
}
|
||||
}
|
||||
[hidden] { display: none !important; }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
:root { --bg:#f6f8fb; --panel:#ffffff; --panel2:#f0f4f9; --line:rgba(0,0,0,.10);
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5;
|
||||
/* Darker acid for light backgrounds — ~5.5:1 on white. */
|
||||
--acid: #3a5c00; }
|
||||
--acid: #088A66; }
|
||||
}
|
||||
* { box-sizing: border-box }
|
||||
html, body { margin: 0; height: 100% }
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
--card: #ffffff; --card-hi: #f4f7fb; --line: rgba(0,0,0,.10); --line2: rgba(0,0,0,.14);
|
||||
--ink: #1a1f28; --mut: #4a5262; --dim: #8a93a2;
|
||||
/* Darker acid for light backgrounds — ~5.5:1 on white. */
|
||||
--acid: #3a5c00; }
|
||||
--acid: #088A66; }
|
||||
h1 .g { color: var(--acid); }
|
||||
.modal { background: #ffffff; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
--bg:#f6f8fb; --panel:#ffffff; --panel2:#f0f4f9; --line:rgba(0,0,0,.10);
|
||||
--ink:#1a1f28; --mut:#3c4453; --dim:#697280;
|
||||
/* Darker acid for light backgrounds — ~5.5:1 on white. */
|
||||
--acid: #3a5c00;
|
||||
--acid: #088A66;
|
||||
}
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
nativeTheme.themeSource, which drives prefers-color-scheme — so pinning
|
||||
color-scheme via that media query keeps the popup in sync automatically. */
|
||||
:root { color-scheme: dark; }
|
||||
@media (prefers-color-scheme: light) { :root { color-scheme: light; --acid: #3a5c00; } }
|
||||
@media (prefers-color-scheme: light) { :root { color-scheme: light; --acid: #088A66; } }
|
||||
/* Explicit option styling — Chromium respects it in the popup on Windows. */
|
||||
select option { background: #1b2330; color: var(--ink); }
|
||||
@media (prefers-color-scheme: light) { select option { background: #f1f3f7; color: #1a1f28; } }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue