183 lines
7.4 KiB
JavaScript
183 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 };
|
||
|
|
};
|