theseus/bundled-addons/aegis/lib/chain-sia.js
Local Dev 2e54bf5e5a Ship Theseus 0.3.28 5d15508b (Aegis update card + DevTools in tab sidebar + real favicons)
Setup    5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643
Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4

Bundled since 0.3.27:

32d4b75 - Aegis (bchwallet) gains its own update card in Settings >
General beside Ariadne. Check for updates hits the same signed OTA
endpoint the boot timer uses; Restart to apply appears when a signed
newer version is staged. Uses the existing addons-check-updates + a
new app-restart IPC. New Aegis versions ship without a Theseus release.

32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to
the right of the tab (mode: 'right') instead of a detached window.
Matches stock Chrome. Users who prefer detached can drag out via the
DevTools own toolbar.

b71c925 - Search-engine favicons in Settings > Search now use Google's
/s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough
hosts (Brave, Bing, Yandex, etc.) that half the list was falling
through to the emoji placeholder.

Deployed. Verified LIVE 0.3.28.
2026-09-08 18:17: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 };
};