theseus/bundled-addons/bchwallet/lib/chain-sia.js
Local Dev 118de0ef5c feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit)
Aegis now covers four coins across two-step coin+network picks: BCH
(mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet).

- Sia (SC): pulled the standalone siawallet's lib into
  bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing
  the common adapter shape. The very first SC wallet the user adds in
  Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry
  over automatically; subsequent SC sub-accounts start at
  "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL
  shows a "Point Aegis at a walletd node" gate in the panel.
- Vault-derive gate now honors a manifest-declared `absorbs` list, so
  Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive()
  guard accepts paths under either the current id or the absorbed one —
  the mechanism a superseding add-on uses to inherit an older add-on's
  keyspace without orphaning funds.
- DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the
  SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit
  (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32.
  ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool).
  BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no
  FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked
  against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address
  for "abandon×11 about, m/84'/20'/0'/0/0" is
  dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39.
- Panel: SVG coin logos for SC (green disc with S) and DGB (blue
  octagon with D) alongside the BCH/TRX marks. Chain-specific settings
  block per coin (walletd URL for SC; derivation path for DGB). Balance
  render uses BigInt-safe arithmetic so 24-decimal SC amounts don't
  lose precision on the way through the panel; amount input on SC
  returns a hastings string.
- Every chain adapter's snapshot fits the panel's shared shape
  (address/balance/history/etc.), so future chains only need a new
  chain-<x>.js file, a COINS registry entry, a matching case in
  mountWallet, and an SVG logo.

Standalone siawallet addon stays as-is on disk; users can delete it once
they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00

182 lines
7.4 KiB
JavaScript

// 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 };
};