Second bundled wallet, same shape as bchwallet:
- keys: api.vault.derive("siawallet/mainnet/0") as the seed for walletd's
KeyFromSeed(seed, index) (blake2b(seed||index) -> ed25519); addresses are
standard unlock hashes, so a future walletd seed import yields the same
addresses. Seed and keys live in memory only.
- lib/sia.js: Sia binary encoder, StandardUnlockHash, address checksum,
v2 InputSigHash ("sia/sig/input|" + replay byte 2 + transaction
semantics), transaction weight, walletd JSON. Address hashing and the
sighash were verified against real mainnet v2 transactions (signatures
from block 591853 verify under this implementation).
- lib/walletd.js: address-scoped walletd HTTP client (tip, fee, balance,
outputs with proofs, events, broadcast). The node URL is a user setting
with no default; hosted providers embed the access key in the path, so
only the origin is ever displayed or logged.
- lib/wallet.js: gap-limit discovery via events, mature/immature balance,
history deltas from v1/v2/foundation/miner events, largest-first
selection with change to the current address, fee = walletd rate x
weight x 1-3 multiplier, broadcast with the outputs' basis. A signed tx
built here was accepted structurally by a live walletd (rejected only
for the stub key not owning the parent).
- panel: Receive (QR), Send, History, Settings (node URL, derivation info,
seed reveal behind approval, connected sites); gates for locked vault,
no vault, no node URL.
- window.siacoin dapp bridge: getAddress (rememberable), signAndSend with
100/1,000/10,000 SC allowances, signMessage (ed25519 over blake2b-256 of
the message) — same approval and permission rules as the BCH wallet.
142 lines
7 KiB
JavaScript
142 lines
7 KiB
JavaScript
// 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 };
|
|
};
|