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