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.
This commit is contained in:
parent
8055d39a9d
commit
2e54bf5e5a
61 changed files with 1 additions and 1342 deletions
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"id": "siawallet",
|
||||
"name": "Siacoin Wallet",
|
||||
"version": "0.1.0",
|
||||
"description": "Send and receive Siacoin with keys derived from your Theseus vault. Talks to any walletd node you point it at; dapps on .x sites can request payments through window.siacoin.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "Ⓢ",
|
||||
"main": "index.js",
|
||||
"capabilities": ["sidebar-panel", "vault-derive", "page-inject", "approval-modal"],
|
||||
"page-inject": {
|
||||
"preload": "wallet-inject.js",
|
||||
"origins": ["https://*.x/*"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
// Siacoin Wallet — bundled Theseus add-on. activate() runs in the main
|
||||
// process; keys are re-derived from the password vault on every launch and
|
||||
// live only in memory. Chain access goes through a walletd node the user
|
||||
// configures (Settings); no default endpoint ships with the add-on.
|
||||
const NETWORK = "mainnet";
|
||||
const PURPOSE = "siawallet/mainnet/0";
|
||||
const EXPLORER_TX = "https://siascan.com/tx/";
|
||||
const EXPLORER_ADDR = "https://siascan.com/address/";
|
||||
const SC = 10n ** 24n;
|
||||
const ALLOWANCES = [100n * SC, 1000n * SC, 10000n * SC];
|
||||
|
||||
let ctx = null; // { api, d, keys, wallet, client, root, phase, error }
|
||||
|
||||
async function deps(api) {
|
||||
const { ed25519 } = await api.import("@noble/curves/ed25519.js");
|
||||
const { blake2b } = await api.import("@noble/hashes/blake2.js");
|
||||
const sia = require("./lib/sia.js")({ ed25519, blake2b });
|
||||
const keysLib = require("./lib/keys.js")({ sia });
|
||||
const walletd = require("./lib/walletd.js")({ log: (...a) => api.log("walletd", ...a) });
|
||||
return { sia, keysLib, walletd };
|
||||
}
|
||||
|
||||
const walletdUrl = (api) => String(api.storage.get("walletdUrl", "") || "").trim();
|
||||
const fmt = (h) => ctx.d.sia.formatSC(h);
|
||||
|
||||
function snapshot() {
|
||||
const c = ctx;
|
||||
const base = {
|
||||
network: NETWORK, phase: c.phase, error: c.error,
|
||||
server: c.client ? c.client.displayUrl : null,
|
||||
hasUrl: !!walletdUrl(c.api),
|
||||
explorerTx: EXPLORER_TX, explorerAddr: EXPLORER_ADDR,
|
||||
};
|
||||
if (c.wallet) Object.assign(base, c.wallet.snapshot());
|
||||
return base;
|
||||
}
|
||||
function emitState() { try { ctx.api.emit("state", snapshot()); } catch {} }
|
||||
function setPhase(phase, error = null) { ctx.phase = phase; ctx.error = error; emitState(); }
|
||||
|
||||
function buildWallet() {
|
||||
const c = ctx;
|
||||
if (c.wallet) { c.wallet.dispose(); c.wallet = null; }
|
||||
if (!c.keys) c.keys = new c.d.keysLib.WalletKeys(c.root);
|
||||
const url = walletdUrl(c.api);
|
||||
if (!url) { setPhase("nourl"); return; }
|
||||
if (!c.client) c.client = new c.d.walletd.Client(url); else c.client.setBase(url);
|
||||
c.wallet = require("./lib/wallet.js")({
|
||||
client: c.client, keys: c.keys, sia: c.d.sia, storage: c.api.storage,
|
||||
log: (...a) => c.api.log("wallet", ...a), onChange: emitState,
|
||||
});
|
||||
c.api.log("wallet ready, receive address", c.keys.entry(0).address, "via", c.client.displayUrl);
|
||||
setPhase("ready");
|
||||
c.wallet.refresh(true).then(() => c.wallet && c.wallet.startPolling());
|
||||
}
|
||||
|
||||
async function deriveAndStart() {
|
||||
const c = ctx;
|
||||
setPhase("locked");
|
||||
try { c.root = await c.api.vault.derive(PURPOSE); }
|
||||
catch (e) {
|
||||
const msg = e?.message || String(e);
|
||||
setPhase(/not set up/i.test(msg) ? "nosetup" : "error", msg);
|
||||
return;
|
||||
}
|
||||
if (ctx !== c) return;
|
||||
try { buildWallet(); }
|
||||
catch (e) { setPhase("error", e?.message || String(e)); c.api.log("wallet build failed:", e?.message); }
|
||||
}
|
||||
|
||||
function requireReady() {
|
||||
if (!ctx || ctx.phase !== "ready" || !ctx.wallet) throw new Error(ctx && ctx.phase === "nourl" ? "no walletd URL configured" : "wallet is not ready (vault locked?)");
|
||||
return ctx;
|
||||
}
|
||||
const fromPanel = (m) => { if (!m || m.from !== "panel") throw new Error("panel-only message"); };
|
||||
function planFrom(p) {
|
||||
const c = requireReady();
|
||||
const spec = p || {};
|
||||
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) }];
|
||||
return c.wallet.plan({ targets, feeMultiplier: spec.feeMultiplier, sendMax: !!spec.sendMax });
|
||||
}
|
||||
// Amounts arrive as decimal hastings strings (or numbers for small values).
|
||||
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);
|
||||
}
|
||||
function describePlan(plan) {
|
||||
const sent = plan.recipients.reduce((a, r) => a + BigInt(r.value), 0n);
|
||||
return { recipients: plan.recipients, fee: plan.fee.toString(), feePerByte: plan.feePerByte.toString(), inputs: plan.tx.inputs.length, change: plan.change.toString(), total: (sent + plan.fee).toString() };
|
||||
}
|
||||
|
||||
function registerPanelMessages(api) {
|
||||
api.onMessage("state", (_p, m) => { fromPanel(m); return snapshot(); });
|
||||
api.onMessage("refresh", async (_p, m) => { fromPanel(m); const c = requireReady(); await c.wallet.refresh(true); return snapshot(); });
|
||||
api.onMessage("nextAddress", (_p, m) => { fromPanel(m); const c = requireReady(); c.wallet.nextUnusedAddress(); return snapshot(); });
|
||||
api.onMessage("openUrl", (p, m) => { fromPanel(m); api.openTab(String(p && p.url || "")); return true; });
|
||||
api.onMessage("settings", (_p, m) => { fromPanel(m); return { walletdUrl: walletdUrl(api) }; });
|
||||
api.onMessage("setSettings", (p, m) => {
|
||||
fromPanel(m);
|
||||
const patch = p || {};
|
||||
if ("walletdUrl" in patch) {
|
||||
const v = String(patch.walletdUrl || "").trim();
|
||||
if (v && !/^https?:\/\/[^\s]+$/i.test(v)) throw new Error("walletd URL must start with http:// or https://");
|
||||
api.storage.set("walletdUrl", v);
|
||||
}
|
||||
if (ctx && ctx.root) buildWallet();
|
||||
return snapshot();
|
||||
});
|
||||
api.onMessage("planSend", (p, m) => { fromPanel(m); return describePlan(planFrom(p)); });
|
||||
api.onMessage("send", async (p, m) => {
|
||||
fromPanel(m);
|
||||
const c = requireReady();
|
||||
const plan = planFrom(p);
|
||||
const d = describePlan(plan);
|
||||
const pick = await api.approvalModal({
|
||||
title: "Send Siacoin?",
|
||||
origin: "Theseus wallet panel",
|
||||
rows: [
|
||||
{ label: "To", value: d.recipients[0].to, mono: true },
|
||||
{ label: "Amount", value: fmt(d.recipients[0].value) + " SC", strong: true },
|
||||
{ label: "Fee", value: fmt(d.fee) + " SC" },
|
||||
{ label: "Total", value: fmt(d.total) + " SC" },
|
||||
],
|
||||
actions: [{ id: "send", label: "Send", primary: true }],
|
||||
});
|
||||
if (pick !== "send") throw new Error("cancelled");
|
||||
return c.wallet.signAndBroadcast(plan);
|
||||
});
|
||||
// Recovery: the 32-byte wallet seed (walletd KeyFromSeed scheme), only
|
||||
// after an explicit confirmation in the approval overlay.
|
||||
api.onMessage("recovery", async (p, m) => {
|
||||
fromPanel(m);
|
||||
const c = requireReady();
|
||||
const out = { purpose: PURPOSE, scheme: "walletd KeyFromSeed(seed, index); address = standard unlock hash", firstAddress: c.keys.entry(0).address };
|
||||
if (p && p.reveal) {
|
||||
const pick = await api.approvalModal({
|
||||
title: "Reveal the wallet seed?",
|
||||
origin: "Theseus wallet panel",
|
||||
body: "Anyone holding this seed can spend every coin in this wallet. It stays on screen until you close the Settings tab.",
|
||||
actions: [{ id: "reveal", label: "Reveal", danger: true }],
|
||||
});
|
||||
if (pick === "reveal") out.seedHex = c.keys.seedHex;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
// ---- dapp bridge (window.siacoin) ------------------------------------------
|
||||
// permissions: { [origin]: { readAddress: true, sendTx: { cap, used, grantedAt } } }
|
||||
// cap/used are hastings as decimal strings. No unlimited option; signing
|
||||
// always asks.
|
||||
const pendingByOrigin = new Set();
|
||||
function permissions(api) { const p = api.storage.get("permissions", {}); return p && typeof p === "object" ? p : {}; }
|
||||
const fromPage = (m) => { if (!m || m.from !== "page" || !m.origin) throw new Error("page-only message"); return m.origin; };
|
||||
async function withOriginLock(origin, fn) {
|
||||
if (pendingByOrigin.has(origin)) throw new Error("a wallet request from this site is already waiting for approval");
|
||||
pendingByOrigin.add(origin);
|
||||
try { return await fn(); } finally { pendingByOrigin.delete(origin); }
|
||||
}
|
||||
|
||||
function registerPageMessages(api) {
|
||||
api.onMessage("getAddress", async (_p, m) => {
|
||||
const origin = fromPage(m);
|
||||
const c = requireReady();
|
||||
const perms = permissions(api);
|
||||
if (perms[origin] && perms[origin].readAddress) return c.wallet.current().address;
|
||||
return withOriginLock(origin, async () => {
|
||||
const pick = await api.approvalModal({
|
||||
title: "Share your Siacoin address?",
|
||||
origin,
|
||||
body: "The site will see your current receiving address and can look up its balance and history on the public chain.",
|
||||
rows: [{ label: "Address", value: c.wallet.current().address, mono: true }],
|
||||
actions: [{ id: "allow", label: "Share", primary: true }],
|
||||
checkbox: { id: "always", label: "Always allow this site to see my address" },
|
||||
});
|
||||
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
||||
if (pick === "allow+always") { perms[origin] = { ...(perms[origin] || {}), readAddress: true }; api.storage.set("permissions", perms); emitState(); }
|
||||
return c.wallet.current().address;
|
||||
});
|
||||
});
|
||||
api.onMessage("signAndSend", async (p, m) => {
|
||||
const origin = fromPage(m);
|
||||
const c = requireReady();
|
||||
return withOriginLock(origin, async () => {
|
||||
let plan;
|
||||
try { plan = planFrom(p); }
|
||||
catch (e) { throw new Error(/insufficient funds|balance/i.test(e?.message) ? "insufficient funds" : e?.message || String(e)); }
|
||||
const d = describePlan(plan);
|
||||
if (d.recipients.length > 8) throw new Error("too many outputs");
|
||||
const perms = permissions(api);
|
||||
const budget = perms[origin] && perms[origin].sendTx;
|
||||
const remaining = budget ? BigInt(budget.cap) - BigInt(budget.used || "0") : 0n;
|
||||
const total = BigInt(d.total);
|
||||
if (budget && total <= remaining) {
|
||||
const r = await c.wallet.signAndBroadcast(plan);
|
||||
budget.used = (BigInt(budget.used || "0") + total).toString();
|
||||
api.storage.set("permissions", perms);
|
||||
emitState();
|
||||
api.log(`silent send ${fmt(total)} SC for ${origin}, ${fmt(remaining - total)} SC of allowance left`);
|
||||
return { txid: r.txid };
|
||||
}
|
||||
const rows = d.recipients.map((r, i) => ({ label: d.recipients.length > 1 ? `To #${i + 1}` : "To", value: r.to, mono: true }));
|
||||
rows.push({ label: "Amount", value: fmt(d.recipients.reduce((a, r) => a + BigInt(r.value), 0n)) + " SC", strong: true });
|
||||
rows.push({ label: "Fee", value: fmt(d.fee) + " SC" });
|
||||
rows.push({ label: "Total", value: fmt(d.total) + " SC" });
|
||||
const pick = await api.approvalModal({
|
||||
title: "Send Siacoin?",
|
||||
origin,
|
||||
body: budget
|
||||
? `This payment is over what is left of the site's allowance (${fmt(remaining)} SC). Check the address and amount.`
|
||||
: "This site is asking your wallet to pay. Check the address and amount.",
|
||||
rows,
|
||||
actions: [{ id: "send", label: "Send", primary: true }],
|
||||
select: {
|
||||
id: "cap", label: "Afterwards",
|
||||
options: [{ value: "", label: "ask every time" }, ...ALLOWANCES.map((s) => ({ value: s.toString(), label: `allow up to ${fmt(s)} SC more without asking` }))],
|
||||
},
|
||||
});
|
||||
const [action, ...flags] = pick.split("+");
|
||||
if (action !== "send") throw new Error("user rejected");
|
||||
const cap = flags.find((f) => f.startsWith("cap="));
|
||||
const capValue = cap ? cap.slice(4) : "";
|
||||
if (ALLOWANCES.some((a) => a.toString() === capValue)) {
|
||||
perms[origin] = { ...(perms[origin] || {}), sendTx: { cap: capValue, used: "0", grantedAt: Date.now() } };
|
||||
api.storage.set("permissions", perms);
|
||||
} else if (budget) {
|
||||
delete perms[origin].sendTx;
|
||||
api.storage.set("permissions", perms);
|
||||
}
|
||||
emitState();
|
||||
const r = await c.wallet.signAndBroadcast(plan);
|
||||
return { txid: r.txid };
|
||||
});
|
||||
});
|
||||
api.onMessage("signMessage", async (p, m) => {
|
||||
const origin = fromPage(m);
|
||||
const c = requireReady();
|
||||
const message = String(p && p.message != null ? p.message : "");
|
||||
if (message.length > 4096) throw new Error("message too long");
|
||||
return withOriginLock(origin, async () => {
|
||||
const entry = c.wallet.current();
|
||||
const pick = await api.approvalModal({
|
||||
title: "Sign a message?",
|
||||
origin,
|
||||
body: "Signing proves you control the address below. It moves no coins.",
|
||||
rows: [{ label: "Message", value: message.length > 400 ? message.slice(0, 400) + "…" : message, mono: true }, { label: "Address", value: entry.address, mono: true }],
|
||||
actions: [{ id: "sign", label: "Sign", primary: true }],
|
||||
});
|
||||
if (pick !== "sign") throw new Error("user rejected");
|
||||
const digest = c.d.sia.b256(new TextEncoder().encode(message));
|
||||
const sig = c.keys.sign(entry, digest);
|
||||
return { address: entry.address, publicKey: "ed25519:" + c.d.sia.toHex(entry.pub), signature: c.d.sia.toHex(sig) };
|
||||
});
|
||||
});
|
||||
api.onMessage("permissions", (_p, m) => { fromPanel(m); return permissions(api); });
|
||||
api.onMessage("revoke", (p, m) => {
|
||||
fromPanel(m);
|
||||
const perms = permissions(api);
|
||||
delete perms[String(p && p.origin || "")];
|
||||
api.storage.set("permissions", perms);
|
||||
return perms;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
activate(api) {
|
||||
api.registerSidebarPanel({ id: "main", title: "Sia", icon: "Ⓢ", page: "panel.html" });
|
||||
const c = ctx = { api, d: null, keys: null, wallet: null, client: null, root: null, phase: "locked", error: null };
|
||||
registerPanelMessages(api);
|
||||
registerPageMessages(api);
|
||||
deps(api).then((d) => { if (ctx !== c) return; c.d = d; return deriveAndStart(); })
|
||||
.catch((e) => { if (ctx !== c) return; setPhase("error", e?.message || String(e)); api.log("startup failed:", e?.message); });
|
||||
},
|
||||
deactivate() {
|
||||
const c = ctx; ctx = null;
|
||||
if (!c) return;
|
||||
try { c.wallet && c.wallet.dispose(); } catch {}
|
||||
try { c.keys && c.keys.wipe(); } catch {}
|
||||
if (c.root) c.root.fill(0);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// Key tree for the Sia wallet: index i -> ed25519 key via walletd's
|
||||
// KeyFromSeed(root, i), address = standard unlock hash of the public key.
|
||||
// Private keys stay inside this module; sign() is the only way out.
|
||||
module.exports = function makeKeys({ sia }) {
|
||||
class WalletKeys {
|
||||
constructor(root32) {
|
||||
this._root = Uint8Array.from(root32);
|
||||
this._cache = new Map();
|
||||
}
|
||||
entry(index) {
|
||||
let e = this._cache.get(index);
|
||||
if (!e) {
|
||||
const k = sia.keyFromSeed(this._root, index);
|
||||
e = { index, pub: k.pub, address32: k.address32, address: k.address, _priv: k.priv };
|
||||
this._cache.set(index, e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
sign(entry, msg) { return sia.sign(entry._priv, msg); }
|
||||
// Revealed only on explicit user action in Settings.
|
||||
get seedHex() { return sia.toHex(this._root); }
|
||||
wipe() {
|
||||
for (const e of this._cache.values()) e._priv.fill(0);
|
||||
this._cache.clear();
|
||||
this._root.fill(0);
|
||||
}
|
||||
}
|
||||
return { WalletKeys };
|
||||
};
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
// 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 };
|
||||
};
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
// Sia wallet state: address discovery, balance, history and v2 sends over a
|
||||
// walletd client. Keys are the addon's derived key tree; nothing here touches
|
||||
// UI or IPC. Amounts are BigInt hastings throughout.
|
||||
module.exports = function makeWallet({ client, keys, sia, storage, log = () => {}, onChange = () => {} }) {
|
||||
const GAP = 10;
|
||||
const HISTORY_LIMIT = 25;
|
||||
const state = {
|
||||
used: new Set(), // indexes with any event
|
||||
height: 0,
|
||||
balance: { confirmed: 0n, immature: 0n },
|
||||
outputs: [], // spendable SiacoinElements with { index }
|
||||
basis: null,
|
||||
history: [],
|
||||
receiveIndex: 0,
|
||||
scanning: false,
|
||||
error: null,
|
||||
feePerByte: 0n,
|
||||
};
|
||||
// Outputs we just spent stay hidden until walletd stops listing them.
|
||||
const pendingSpent = new Map(); // id -> timestamp
|
||||
|
||||
async function isUsed(entry) {
|
||||
const ev = await client.events(entry.address, 1, 0);
|
||||
return Array.isArray(ev) && ev.length > 0;
|
||||
}
|
||||
async function scan() {
|
||||
const cursor = Number(storage.get("receiveCursor", 0)) || 0;
|
||||
let gap = 0, i = 0;
|
||||
while (gap < GAP || i < cursor + GAP) {
|
||||
const e = keys.entry(i);
|
||||
if (await isUsed(e)) { state.used.add(i); gap = 0; } else gap++;
|
||||
i++;
|
||||
}
|
||||
let r = cursor;
|
||||
while (state.used.has(r)) r++;
|
||||
state.receiveIndex = r;
|
||||
}
|
||||
function watched() {
|
||||
const idx = new Set(state.used); idx.add(state.receiveIndex);
|
||||
return [...idx].map((i) => keys.entry(i));
|
||||
}
|
||||
|
||||
async function loadOutputs() {
|
||||
const tip = await client.tip();
|
||||
state.height = tip.height || 0;
|
||||
let confirmed = 0n, immature = 0n; const outs = []; let basis = null;
|
||||
for (const e of watched()) {
|
||||
for (let offset = 0; ; offset += 100) {
|
||||
const r = await client.outputs(e.address, 100, offset);
|
||||
basis = r.basis || basis;
|
||||
const list = Array.isArray(r.outputs) ? r.outputs : [];
|
||||
for (const o of list) {
|
||||
const v = BigInt(o.siacoinOutput.value);
|
||||
if (o.maturityHeight > state.height) { immature += v; continue; }
|
||||
if (pendingSpent.has(o.id)) continue;
|
||||
confirmed += v;
|
||||
outs.push({ element: o, id: o.id, value: v, entry: e });
|
||||
}
|
||||
if (list.length < 100) break;
|
||||
}
|
||||
}
|
||||
for (const [id, t] of pendingSpent) if (Date.now() - t > 20 * 60 * 1000) pendingSpent.delete(id);
|
||||
state.outputs = outs; state.basis = basis;
|
||||
state.balance = { confirmed, immature };
|
||||
try { state.feePerByte = await client.feePerByte(); } catch (e) { log("fee lookup failed:", e?.message); }
|
||||
}
|
||||
|
||||
// One row per event: net change for our addresses, type, confirmations.
|
||||
async function loadHistory() {
|
||||
const ours = new Set(watched().map((e) => e.address));
|
||||
const seen = new Map();
|
||||
for (const e of watched()) {
|
||||
if (!state.used.has(e.index)) continue;
|
||||
const evs = await client.events(e.address, HISTORY_LIMIT, 0);
|
||||
for (const ev of Array.isArray(evs) ? evs : []) if (!seen.has(ev.id)) seen.set(ev.id, ev);
|
||||
}
|
||||
const rows = [];
|
||||
for (const ev of seen.values()) {
|
||||
let received = 0n, spent = 0n, to = null;
|
||||
const d = ev.data || {};
|
||||
const type = String(ev.type || "").toLowerCase();
|
||||
if (type === "v2transaction" && d.transaction) {
|
||||
for (const i of d.transaction.siacoinInputs || []) if (ours.has(i.parent.siacoinOutput.address)) spent += BigInt(i.parent.siacoinOutput.value);
|
||||
for (const o of d.transaction.siacoinOutputs || []) { if (ours.has(o.address)) received += BigInt(o.value); else if (!to) to = o.address; }
|
||||
} else if (type === "v1transaction" && d.transaction) {
|
||||
for (const s of d.spentSiacoinElements || []) if (ours.has(s.siacoinOutput.address)) spent += BigInt(s.siacoinOutput.value);
|
||||
for (const o of d.transaction.siacoinOutputs || []) { if (ours.has(o.address)) received += BigInt(o.value); else if (!to) to = o.address; }
|
||||
} else if (d.siacoinElement) {
|
||||
if (ours.has(d.siacoinElement.siacoinOutput.address)) received += BigInt(d.siacoinElement.siacoinOutput.value);
|
||||
}
|
||||
rows.push({
|
||||
id: ev.id, type: ev.type, height: ev.index ? ev.index.height : 0, confirmations: ev.confirmations || 0,
|
||||
time: ev.timestamp ? Math.floor(Date.parse(ev.timestamp) / 1000) : 0,
|
||||
delta: (received - spent).toString(), to: spent > received ? to : null,
|
||||
maturityHeight: ev.maturityHeight || 0,
|
||||
});
|
||||
}
|
||||
rows.sort((a, b) => (b.height || Infinity) - (a.height || Infinity) || b.time - a.time);
|
||||
state.history = rows.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
async function refresh(full = false) {
|
||||
if (state.scanning) return;
|
||||
state.scanning = true; state.error = null; onChange();
|
||||
try {
|
||||
if (full || !state.used.size && state.receiveIndex === 0) await scan();
|
||||
else { let r = Number(storage.get("receiveCursor", 0)) || 0; while (state.used.has(r)) r++; state.receiveIndex = r; }
|
||||
await loadOutputs();
|
||||
await loadHistory();
|
||||
for (const o of state.outputs) state.used.add(o.entry.index);
|
||||
let r = Number(storage.get("receiveCursor", 0)) || 0;
|
||||
while (state.used.has(r)) r++;
|
||||
state.receiveIndex = r;
|
||||
} catch (e) {
|
||||
state.error = e?.message || String(e);
|
||||
log("refresh failed:", state.error);
|
||||
} finally { state.scanning = false; onChange(); }
|
||||
}
|
||||
let pollTimer = null;
|
||||
function startPolling(ms = 60000) { stopPolling(); pollTimer = setInterval(() => refresh(false), ms); }
|
||||
function stopPolling() { clearInterval(pollTimer); pollTimer = null; }
|
||||
|
||||
function current() { return keys.entry(state.receiveIndex); }
|
||||
function nextUnusedAddress() {
|
||||
let r = state.receiveIndex + 1;
|
||||
while (state.used.has(r)) r++;
|
||||
storage.set("receiveCursor", r);
|
||||
state.receiveIndex = r;
|
||||
onChange();
|
||||
return current();
|
||||
}
|
||||
|
||||
// targets: [{ to, value: BigInt }]; feeMultiplier 1-3 over walletd's rate.
|
||||
function plan({ targets, feeMultiplier = 1, sendMax = false }) {
|
||||
const mult = BigInt(Math.min(3, Math.max(1, Math.round(Number(feeMultiplier) || 1))));
|
||||
const rate = state.feePerByte > 0n ? state.feePerByte * mult : 10n ** 19n * mult;
|
||||
const outs = targets.map((t) => {
|
||||
const a = sia.parseAddress(t.to);
|
||||
return { value: BigInt(t.value || 0), address32: a.bytes, to: a.address };
|
||||
});
|
||||
const sorted = state.outputs.slice().sort((a, b) => (b.value > a.value ? 1 : b.value < a.value ? -1 : 0));
|
||||
const total = sorted.reduce((a, o) => a + o.value, 0n);
|
||||
const change = current();
|
||||
const txOf = (inputs, outputs, fee) => ({
|
||||
inputs: inputs.map((o) => ({
|
||||
parentId: o.id, element: o.element, value: o.value, address32: o.entry.address32, pub: o.entry.pub,
|
||||
leafIndex: o.element.stateElement.leafIndex, merkleProof: o.element.stateElement.merkleProof || [], maturityHeight: o.element.maturityHeight || 0, entry: o.entry,
|
||||
})),
|
||||
outputs, minerFee: fee,
|
||||
});
|
||||
if (sendMax) {
|
||||
if (outs.length !== 1) throw new Error("send max needs exactly one recipient");
|
||||
if (!sorted.length) throw new Error("no spendable balance");
|
||||
let fee = 0n;
|
||||
for (let k = 0; k < 3; k++) fee = rate * BigInt(sia.weight(txOf(sorted, [{ value: total - fee, address32: outs[0].address32 }], fee)));
|
||||
if (total <= fee) throw new Error("balance does not cover the fee");
|
||||
const tx = txOf(sorted, [{ value: total - fee, address32: outs[0].address32 }], fee);
|
||||
return { tx, fee, recipients: [{ to: outs[0].to, value: (total - fee).toString() }], change: 0n, feePerByte: rate };
|
||||
}
|
||||
const want = outs.reduce((a, o) => a + o.value, 0n);
|
||||
for (const o of outs) if (o.value <= 0n) throw new Error("amount must be positive");
|
||||
const chosen = []; let sum = 0n;
|
||||
for (const o of sorted) {
|
||||
chosen.push(o); sum += o.value;
|
||||
const withChange = [...outs, { value: 0n, address32: change.address32 }];
|
||||
const fee = rate * BigInt(sia.weight(txOf(chosen, withChange, 1n)));
|
||||
if (sum >= want + fee) {
|
||||
const rest = sum - want - fee;
|
||||
const outputs = rest > 0n ? [...outs, { value: rest, address32: change.address32 }] : outs.slice();
|
||||
const tx = txOf(chosen, outputs, fee);
|
||||
return { tx, fee, recipients: outs.map((o) => ({ to: o.to, value: o.value.toString() })), change: rest, feePerByte: rate };
|
||||
}
|
||||
}
|
||||
throw new Error("insufficient funds");
|
||||
}
|
||||
|
||||
async function signAndBroadcast(p) {
|
||||
const h = sia.inputSigHash(p.tx);
|
||||
const sigs = p.tx.inputs.map((i) => keys.sign(i.entry, h));
|
||||
const json = sia.toJson(p.tx, sigs);
|
||||
if (!state.basis) throw new Error("no chain basis for the outputs; refresh first");
|
||||
const r = await client.broadcast(state.basis, json);
|
||||
const txid = r && r.v2transactions && r.v2transactions[0] && r.v2transactions[0].id;
|
||||
for (const i of p.tx.inputs) pendingSpent.set(i.parentId, Date.now());
|
||||
log("broadcast", txid || "(no id returned)");
|
||||
setTimeout(() => refresh(false), 3000);
|
||||
return { txid: txid || null, fee: p.fee.toString() };
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
const cur = current();
|
||||
return {
|
||||
address: cur.address, addressIndex: state.receiveIndex,
|
||||
balance: { confirmed: state.balance.confirmed.toString(), immature: state.balance.immature.toString() },
|
||||
height: state.height, history: state.history, outputCount: state.outputs.length,
|
||||
feePerByte: state.feePerByte.toString(), scanning: state.scanning, error: state.error,
|
||||
};
|
||||
}
|
||||
function dispose() { stopPolling(); }
|
||||
return { refresh, snapshot, nextUnusedAddress, current, plan, signAndBroadcast, startPolling, dispose, state };
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
// Thin client for the walletd HTTP API (go.sia.tech/walletd, index mode
|
||||
// "full"). Only address-scoped reads plus txpool fee/broadcast are used, so
|
||||
// any public or self-hosted walletd works; the URL is a user setting.
|
||||
module.exports = function makeWalletd({ log = () => {} }) {
|
||||
class Client {
|
||||
constructor(baseUrl) { this.setBase(baseUrl); }
|
||||
setBase(baseUrl) {
|
||||
const u = String(baseUrl || "").trim().replace(/\/+$/, "");
|
||||
this.base = u ? (u.endsWith("/api") ? u : u + "/api") : "";
|
||||
}
|
||||
// Everything after the host is the node's business; only the origin is
|
||||
// ever logged or shown, since hosted providers key access on the path.
|
||||
get displayUrl() { try { return new URL(this.base).origin; } catch { return this.base; } }
|
||||
async _req(method, path, body) {
|
||||
if (!this.base) throw new Error("no walletd URL configured");
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 25000);
|
||||
try {
|
||||
const res = await fetch(this.base + path, {
|
||||
method, signal: ctrl.signal,
|
||||
headers: body !== undefined ? { "content-type": "application/json" } : {},
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) throw new Error(`walletd ${res.status}: ${text.slice(0, 200).trim()}`);
|
||||
try { return JSON.parse(text); } catch { return text; }
|
||||
} finally { clearTimeout(timer); }
|
||||
}
|
||||
get(path) { return this._req("GET", path); }
|
||||
post(path, body) { return this._req("POST", path, body); }
|
||||
|
||||
tip() { return this.get("/consensus/tip"); }
|
||||
// Recommended fee in hastings per byte (JSON string).
|
||||
async feePerByte() { return BigInt(String(await this.get("/txpool/fee")).replace(/"/g, "")); }
|
||||
balance(addr) { return this.get(`/addresses/${addr}/balance`); }
|
||||
// { basis, outputs: [SiacoinElement] } — proofs are valid at `basis`.
|
||||
outputs(addr, limit = 100, offset = 0) { return this.get(`/addresses/${addr}/outputs/siacoin?limit=${limit}&offset=${offset}`); }
|
||||
events(addr, limit = 25, offset = 0) { return this.get(`/addresses/${addr}/events?limit=${limit}&offset=${offset}`); }
|
||||
broadcast(basis, v2tx) { return this.post("/txpool/broadcast", { basis, transactions: [], v2transactions: [v2tx] }); }
|
||||
}
|
||||
return { Client };
|
||||
};
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Siacoin Wallet</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark;
|
||||
--bg:#0e131c; --panel:#141a24; --card:#0f1621; --line:rgba(255,255,255,.09);
|
||||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; --danger:#f6768a; --ok:#5ad38a;
|
||||
--sia:#20ee82; }
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg:#f8faff; --panel:#ffffff; --card:#f1f4fa; --line:rgba(0,0,0,.10);
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --sia:#1abc6a;
|
||||
/* Darker acid on light backgrounds. */
|
||||
--acid: #0AC18E; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
body { background: var(--bg); color: var(--ink);
|
||||
font: 13.5px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
display: flex; flex-direction: column; }
|
||||
header { padding: 12px 14px 10px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
||||
header .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
header .t { font-weight: 600; display: flex; gap: 8px; align-items: center; }
|
||||
header .t .coin { color: var(--sia); font-size: 16px; }
|
||||
.net { display: flex; align-items: center; gap: 6px; color: var(--dim); font-size: 11.5px; white-space: nowrap; }
|
||||
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dim); }
|
||||
.dot.on { background: var(--ok); } .dot.busy { background: #e0b341; }
|
||||
.bal { margin-top: 8px; font-variant-numeric: tabular-nums; }
|
||||
.bal .big { font-size: 22px; font-weight: 650; letter-spacing: .2px; }
|
||||
.bal .big small { font-size: 13px; color: var(--mut); font-weight: 500; margin-left: 4px; }
|
||||
.bal .sub { color: var(--dim); font-size: 11.5px; }
|
||||
nav { display: flex; border-bottom: 1px solid var(--line); background: var(--panel); }
|
||||
nav button { flex: 1; padding: 9px 0 8px; border: 0; background: transparent; color: var(--mut); cursor: pointer;
|
||||
font: inherit; font-size: 12.5px; border-bottom: 2px solid transparent; }
|
||||
nav button.on { color: var(--acid); border-bottom-color: var(--acid); }
|
||||
nav button:hover { color: var(--ink); }
|
||||
main { flex: 1; overflow: auto; padding: 14px; }
|
||||
section[hidden] { display: none; }
|
||||
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 12px; }
|
||||
.mono { font: 12.5px/1.45 ui-monospace, "Cascadia Code", Consolas, monospace; word-break: break-all; }
|
||||
.lbl { color: var(--dim); font-size: 11.5px; margin-bottom: 4px; }
|
||||
.btn { padding: 7px 12px; border-radius: 7px; border: 1px solid var(--line); background: var(--panel); color: var(--ink);
|
||||
cursor: pointer; font: inherit; font-size: 12.5px; }
|
||||
.btn:hover { border-color: rgb(from var(--acid) r g b / .4); }
|
||||
.btn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; font-weight: 650; }
|
||||
.btn.primary:disabled { opacity: .45; cursor: default; }
|
||||
.btn.danger { color: var(--danger); }
|
||||
.btn.sm { padding: 4px 9px; font-size: 12px; }
|
||||
.actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
||||
.qrwrap { display: grid; place-items: center; padding: 12px; background: #fff; border-radius: 10px; margin-bottom: 12px; }
|
||||
canvas { image-rendering: pixelated; }
|
||||
input[type=text], textarea { width: 100%; padding: 7px 9px; border-radius: 7px; background: var(--panel);
|
||||
border: 1px solid var(--line); color: var(--ink); font: inherit; font-size: 13px; outline: none; }
|
||||
input:focus, textarea:focus { border-color: rgb(from var(--acid) r g b / .5); }
|
||||
.field { margin-bottom: 12px; }
|
||||
.hint { color: var(--dim); font-size: 11.5px; margin-top: 4px; }
|
||||
.amt { display: flex; gap: 6px; }
|
||||
.amt input { flex: 1; }
|
||||
.fee { display: flex; align-items: center; gap: 10px; }
|
||||
.fee input[type=range] { flex: 1; accent-color: var(--acid); }
|
||||
.fee .v { color: var(--mut); font-size: 12px; min-width: 70px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.summary { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 12.5px; margin-top: 10px; }
|
||||
.summary .k { color: var(--dim); } .summary .v { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.msg { margin-top: 10px; font-size: 12.5px; padding: 8px 10px; border-radius: 7px; }
|
||||
.msg.err { background: rgba(246,118,138,.12); color: var(--danger); }
|
||||
.msg.ok { background: rgba(90,211,138,.12); color: var(--ok); }
|
||||
.tx { display: grid; grid-template-columns: 22px 1fr auto; gap: 2px 10px; padding: 9px 4px; border-bottom: 1px solid var(--line); cursor: pointer; }
|
||||
.tx:hover { background: rgba(255,255,255,.03); }
|
||||
.tx .ic { font-size: 15px; line-height: 1.3; }
|
||||
.tx .ic.in { color: var(--ok); } .tx .ic.out { color: var(--danger); }
|
||||
.tx .what { font-size: 12.5px; }
|
||||
.tx .when { color: var(--dim); font-size: 11.5px; grid-column: 2; }
|
||||
.tx .amt2 { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; font-size: 12.5px; }
|
||||
.tx .amt2.in { color: var(--ok); }
|
||||
.tx .conf { grid-column: 3; text-align: right; color: var(--dim); font-size: 11px; }
|
||||
.tx .conf.pending { color: #e0b341; }
|
||||
.empty { color: var(--dim); text-align: center; padding: 30px 10px; font-size: 12.5px; }
|
||||
.gate { padding: 40px 18px; text-align: center; color: var(--mut); }
|
||||
.gate .big { font-size: 28px; margin-bottom: 8px; }
|
||||
.gate b { color: var(--ink); }
|
||||
.gate input { margin-top: 14px; text-align: left; }
|
||||
.kv { margin-top: 10px; }
|
||||
.kv .lbl { margin-top: 8px; }
|
||||
a.link { color: var(--acid); text-decoration: none; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="row">
|
||||
<div class="t"><span class="coin">Ⓢ</span><span>Siacoin</span></div>
|
||||
<div class="net"><span class="dot" id="dot"></span><span id="netlbl">connecting…</span></div>
|
||||
</div>
|
||||
<div class="bal">
|
||||
<div class="big"><span id="balSc">—</span><small>SC</small></div>
|
||||
<div class="sub" id="balSub">mainnet</div>
|
||||
</div>
|
||||
</header>
|
||||
<nav>
|
||||
<button data-tab="receive" class="on">Receive</button>
|
||||
<button data-tab="send">Send</button>
|
||||
<button data-tab="history">History</button>
|
||||
<button data-tab="settings">Settings</button>
|
||||
</nav>
|
||||
<main>
|
||||
<div id="gate" class="gate" hidden></div>
|
||||
<div id="tabs">
|
||||
<section id="tab-receive">
|
||||
<div class="qrwrap"><canvas id="qr" width="200" height="200"></canvas></div>
|
||||
<div class="card">
|
||||
<div class="lbl">Receiving address <span id="addrMeta"></span></div>
|
||||
<div class="mono" id="addr">—</div>
|
||||
<div class="actions">
|
||||
<button class="btn sm" id="copyAddr">Copy</button>
|
||||
<button class="btn sm" id="nextAddr">Next unused address</button>
|
||||
<button class="btn sm" id="viewAddr">Explorer</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="tab-send" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">Recipient</div>
|
||||
<input type="text" id="sendTo" spellcheck="false" autocomplete="off" placeholder="76-character Sia address">
|
||||
<div class="hint" id="sendToHint"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Amount (SC)</div>
|
||||
<div class="amt">
|
||||
<input type="text" id="sendAmt" inputmode="decimal" autocomplete="off" placeholder="0.00">
|
||||
<button class="btn sm" id="sendMax" type="button">Max</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Fee</div>
|
||||
<div class="fee"><input type="range" id="feeMult" min="1" max="3" step="1" value="1"><span class="v" id="feeLbl">normal</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="summary">
|
||||
<div class="k">Amount</div><div class="v" id="sumAmt">—</div>
|
||||
<div class="k">Network fee</div><div class="v" id="sumFee">—</div>
|
||||
<div class="k">Total</div><div class="v" id="sumTotal">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions"><button class="btn primary" id="sendBtn" disabled>Send</button></div>
|
||||
<div class="msg" id="sendMsg" hidden></div>
|
||||
</section>
|
||||
<section id="tab-history" hidden>
|
||||
<div id="txlist"></div>
|
||||
</section>
|
||||
<section id="tab-settings" hidden>
|
||||
<div class="field">
|
||||
<div class="lbl">walletd node URL</div>
|
||||
<input type="text" id="setUrl" spellcheck="false" placeholder="https://host[:port]">
|
||||
<div class="hint" id="urlHint">Any walletd (v2) with index mode "full". Hosted providers put your access key in the path; it is stored locally only.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="applySettings">Apply</button>
|
||||
<button class="btn" id="refreshBtn">Rescan</button>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Recovery info</div>
|
||||
<div class="hint">Keys come from your Theseus password vault under <span class="mono">silentmode/addons/siawallet/mainnet/0</span>, using walletd's per-index key scheme. The same vault seed on another machine recreates this wallet.</div>
|
||||
<div class="kv" id="recovery"></div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="showInfo">Show derivation</button>
|
||||
<button class="btn danger" id="showSeed">Show wallet seed</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Connected sites</div>
|
||||
<div class="hint">Sites allowed to read your address, and sites with a payment allowance they can spend without asking. Message signing always asks.</div>
|
||||
<div id="sites" class="kv"></div>
|
||||
</div>
|
||||
<div class="msg err" id="settingsMsg" hidden></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script src="qr.js"></script>
|
||||
<script src="panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
// Sia wallet panel. State comes from activate() via window.silentmode; this
|
||||
// file renders and collects input. Amounts cross the bridge as hastings
|
||||
// strings and are formatted here with BigInt.
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const S = window.silentmode;
|
||||
let state = null;
|
||||
const H = 10n ** 24n;
|
||||
|
||||
function fmtSC(h, decimals = 6) {
|
||||
const v = BigInt(h || 0); const neg = v < 0n; const a = neg ? -v : v;
|
||||
let frac = (a % H).toString().padStart(24, "0").slice(0, decimals).replace(/0+$/, "");
|
||||
if (frac.length < 2) frac = frac.padEnd(2, "0");
|
||||
return (neg ? "-" : "") + (a / H).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") * H + BigInt((f + "0".repeat(24)).slice(0, 24));
|
||||
}
|
||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
||||
const openUrl = (url) => S.invoke("openUrl", { url }).catch(() => {});
|
||||
const cleanErr = (e) => String(e?.message || e).replace(/^Error invoking remote method '[^']+': Error: /, "");
|
||||
|
||||
// ---- tabs ------------------------------------------------------------------
|
||||
document.querySelectorAll("nav button").forEach((b) => b.addEventListener("click", () => showTab(b.dataset.tab)));
|
||||
function showTab(name) {
|
||||
document.querySelectorAll("nav button").forEach((b) => b.classList.toggle("on", b.dataset.tab === name));
|
||||
document.querySelectorAll("main section").forEach((s) => { s.hidden = s.id !== "tab-" + name; });
|
||||
if (name !== "settings") $("recovery").innerHTML = "";
|
||||
if (name === "settings") fillSettings();
|
||||
}
|
||||
|
||||
// ---- render ----------------------------------------------------------------
|
||||
function render() {
|
||||
if (!state) return;
|
||||
const ready = state.phase === "ready";
|
||||
const gate = $("gate");
|
||||
$("tabs").hidden = !ready;
|
||||
gate.hidden = ready;
|
||||
if (!ready) {
|
||||
const copy = {
|
||||
locked: ["🔒", "Unlock your password vault to open the wallet.", "Settings › Passwords. The wallet keys derive from the vault seed."],
|
||||
nosetup: ["🗝", "Set up a password vault to create your wallet.", "Settings › Passwords › Set up. With a recovery phrase this wallet can be recreated on any machine."],
|
||||
nourl: ["🌐", "Point the wallet at a walletd node.", "Paste the URL of a walletd (v2, full index) node. Hosted providers give you a URL with your key in it; it stays on this machine."],
|
||||
error: ["⚠", "The wallet could not start.", state.error || ""],
|
||||
}[state.phase] || ["…", "Starting…", ""];
|
||||
gate.innerHTML = `<div class="big">${copy[0]}</div><div><b>${esc(copy[1])}</b></div><div class="hint" style="margin-top:8px">${esc(copy[2])}</div>`
|
||||
+ (state.phase === "nourl" ? `<input type="text" id="gateUrl" placeholder="https://host[:port]" spellcheck="false"><div class="actions" style="justify-content:center"><button class="btn primary" id="gateApply">Connect</button></div><div class="msg err" id="gateMsg" hidden></div>` : "");
|
||||
if (state.phase === "nourl") $("gateApply").addEventListener("click", () => applyUrl($("gateUrl").value, $("gateMsg")));
|
||||
}
|
||||
const dot = $("dot");
|
||||
dot.className = "dot " + (ready && state.server ? (state.scanning ? "busy" : "on") : "");
|
||||
$("netlbl").textContent = ready && state.server ? state.server.replace(/^https?:\/\//, "") + (state.scanning ? " · syncing" : "") : "mainnet";
|
||||
if (ready) {
|
||||
$("balSc").textContent = fmtSC(state.balance?.confirmed || 0);
|
||||
const parts = [];
|
||||
if (state.balance?.immature && state.balance.immature !== "0") parts.push(fmtSC(state.balance.immature) + " SC maturing");
|
||||
if (state.height) parts.push("block " + Number(state.height).toLocaleString("en-US"));
|
||||
if (state.feePerByte && state.feePerByte !== "0") parts.push("fee " + fmtSC(BigInt(state.feePerByte) * 1000n, 4) + " SC/kB");
|
||||
$("balSub").textContent = state.error || parts.join(" · ") || "mainnet";
|
||||
} else { $("balSc").textContent = "—"; $("balSub").textContent = "mainnet"; }
|
||||
if (!ready) return;
|
||||
if (state.address && $("addr").textContent !== state.address) {
|
||||
$("addr").textContent = state.address;
|
||||
drawQr(state.address);
|
||||
}
|
||||
$("addrMeta").textContent = `· #${state.addressIndex}`;
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function drawQr(text) {
|
||||
const cv = $("qr"); const g = cv.getContext("2d");
|
||||
let q;
|
||||
try { q = window.QR.build(text); } catch { g.clearRect(0, 0, cv.width, cv.height); return; }
|
||||
const scale = Math.max(2, Math.floor(200 / (q.size + 2)));
|
||||
const px = (q.size + 2) * scale;
|
||||
cv.width = cv.height = px; cv.style.width = cv.style.height = px + "px";
|
||||
g.fillStyle = "#fff"; g.fillRect(0, 0, px, px); g.fillStyle = "#000";
|
||||
for (let r = 0; r < q.size; r++) for (let c = 0; c < q.size; c++) if (q.modules[r][c]) g.fillRect((c + 1) * scale, (r + 1) * scale, scale, scale);
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const list = state.history || [];
|
||||
const el = $("txlist");
|
||||
if (!list.length) { el.innerHTML = `<div class="empty">${state.scanning ? "Syncing…" : "No transactions yet."}</div>`; return; }
|
||||
el.innerHTML = list.map((t) => {
|
||||
const delta = BigInt(t.delta || 0);
|
||||
const inc = delta >= 0n;
|
||||
const when = t.time ? new Date(t.time * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }) : "pending";
|
||||
const kind = { miner: "Mining reward", foundation: "Foundation subsidy", siafundclaim: "Siafund claim" }[String(t.type).toLowerCase()];
|
||||
const what = kind || (inc ? "Received" : "Sent" + (t.to ? " to " + esc(t.to.slice(0, 10)) + "…" : ""));
|
||||
const immature = t.maturityHeight && state.height && t.maturityHeight > state.height;
|
||||
const conf = immature ? "matures at " + t.maturityHeight : t.confirmations > 0 ? (t.confirmations >= 6 ? "confirmed" : t.confirmations + " conf") : "unconfirmed";
|
||||
return `<div class="tx" data-id="${esc(t.id)}" title="${esc(t.id)}">
|
||||
<div class="ic ${inc ? "in" : "out"}">${inc ? "↓" : "↑"}</div>
|
||||
<div class="what">${what}</div>
|
||||
<div class="amt2 ${inc ? "in" : ""}">${inc ? "+" : "−"}${fmtSC(inc ? delta : -delta)}</div>
|
||||
<div class="when">${esc(when)}</div>
|
||||
<div class="conf ${t.confirmations > 0 && !immature ? "" : "pending"}">${esc(conf)}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(state.explorerTx + row.dataset.id)));
|
||||
}
|
||||
|
||||
// ---- receive ---------------------------------------------------------------
|
||||
$("copyAddr").addEventListener("click", async () => { try { await navigator.clipboard.writeText(state.address); flash($("copyAddr"), "Copied"); } catch {} });
|
||||
$("nextAddr").addEventListener("click", async () => { try { state = await S.invoke("nextAddress"); render(); } catch { flash($("nextAddr"), "Failed"); } });
|
||||
$("viewAddr").addEventListener("click", () => openUrl(state.explorerAddr + state.address));
|
||||
function flash(btn, text) { const old = btn.textContent; btn.textContent = text; setTimeout(() => { btn.textContent = old; }, 1200); }
|
||||
|
||||
// ---- send ------------------------------------------------------------------
|
||||
let sendMax = false, planTimer = null, lastPlan = null;
|
||||
const FEE_LABELS = { 1: "normal", 2: "2× fee", 3: "3× fee" };
|
||||
$("sendMax").addEventListener("click", () => {
|
||||
sendMax = !sendMax;
|
||||
$("sendMax").classList.toggle("primary", sendMax);
|
||||
$("sendAmt").disabled = sendMax;
|
||||
if (!sendMax) $("sendAmt").value = "";
|
||||
schedulePlan();
|
||||
});
|
||||
$("feeMult").addEventListener("input", () => { $("feeLbl").textContent = FEE_LABELS[$("feeMult").value]; schedulePlan(); });
|
||||
["sendTo", "sendAmt"].forEach((id) => $(id).addEventListener("input", () => { if (id === "sendAmt" && sendMax) return; schedulePlan(); }));
|
||||
function schedulePlan() { clearTimeout(planTimer); planTimer = setTimeout(updatePlan, 250); }
|
||||
function sendSpec() {
|
||||
const amount = sendMax ? "0" : parseSC($("sendAmt").value).toString();
|
||||
return { to: $("sendTo").value.trim(), amount, feeMultiplier: Number($("feeMult").value), sendMax };
|
||||
}
|
||||
async function updatePlan() {
|
||||
const msg = $("sendMsg"); msg.hidden = true;
|
||||
lastPlan = null; $("sendBtn").disabled = true;
|
||||
$("sumAmt").textContent = $("sumFee").textContent = $("sumTotal").textContent = "—";
|
||||
if (!$("sendTo").value.trim() || (!sendMax && !$("sendAmt").value.trim())) return;
|
||||
try {
|
||||
const p = await S.invoke("planSend", sendSpec());
|
||||
lastPlan = p;
|
||||
$("sumAmt").textContent = fmtSC(p.recipients[0].value) + " SC";
|
||||
$("sumFee").textContent = fmtSC(p.fee) + " SC";
|
||||
$("sumTotal").textContent = fmtSC(p.total) + " SC";
|
||||
if (sendMax) $("sendAmt").value = fmtSC(p.recipients[0].value, 24).replace(/0+$/, "").replace(/\.$/, ".0");
|
||||
$("sendBtn").disabled = false;
|
||||
} catch (e) { msg.className = "msg err"; msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||
}
|
||||
$("sendBtn").addEventListener("click", async () => {
|
||||
if (!lastPlan) return;
|
||||
const msg = $("sendMsg"); msg.hidden = true;
|
||||
$("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…";
|
||||
try {
|
||||
const r = await S.invoke("send", sendSpec());
|
||||
msg.className = "msg ok";
|
||||
msg.innerHTML = r.txid ? `Sent. <a class="link">${esc(r.txid.slice(0, 16))}…</a>` : "Sent.";
|
||||
if (r.txid) msg.querySelector("a").addEventListener("click", () => openUrl(state.explorerTx + r.txid));
|
||||
msg.hidden = false;
|
||||
$("sendTo").value = ""; $("sendAmt").value = ""; sendMax = false; $("sendMax").classList.remove("primary"); $("sendAmt").disabled = false;
|
||||
lastPlan = null;
|
||||
} catch (e) {
|
||||
const t = cleanErr(e);
|
||||
if (t !== "cancelled") { msg.className = "msg err"; msg.textContent = t; msg.hidden = false; }
|
||||
$("sendBtn").disabled = !lastPlan;
|
||||
} finally { $("sendBtn").textContent = "Send"; }
|
||||
});
|
||||
|
||||
// ---- settings --------------------------------------------------------------
|
||||
async function fillSettings() {
|
||||
try { const s = await S.invoke("settings"); if (document.activeElement !== $("setUrl")) $("setUrl").value = s.walletdUrl || ""; } catch {}
|
||||
renderSites();
|
||||
}
|
||||
async function applyUrl(url, msgEl) {
|
||||
msgEl.hidden = true;
|
||||
try { state = await S.invoke("setSettings", { walletdUrl: url }); render(); }
|
||||
catch (e) { msgEl.textContent = cleanErr(e); msgEl.hidden = false; }
|
||||
}
|
||||
$("applySettings").addEventListener("click", () => applyUrl($("setUrl").value, $("settingsMsg")));
|
||||
$("refreshBtn").addEventListener("click", async () => { try { state = await S.invoke("refresh"); render(); } catch (e) { $("settingsMsg").textContent = cleanErr(e); $("settingsMsg").hidden = false; } });
|
||||
$("showInfo").addEventListener("click", async () => { try { $("recovery").innerHTML = recoveryHtml(await S.invoke("recovery", {})); } catch (e) { $("recovery").textContent = cleanErr(e); } });
|
||||
$("showSeed").addEventListener("click", async () => { try { $("recovery").innerHTML = recoveryHtml(await S.invoke("recovery", { reveal: true })); } catch (e) { $("recovery").textContent = cleanErr(e); } });
|
||||
function recoveryHtml(r) {
|
||||
let h = `<div class="lbl">Scheme</div><div class="mono">${esc(r.scheme)}</div><div class="lbl">First address</div><div class="mono">${esc(r.firstAddress)}</div>`;
|
||||
if (r.seedHex) h += `<div class="lbl">Wallet seed (32 bytes, hex)</div><div class="mono" style="color:var(--danger)">${esc(r.seedHex)}</div>`;
|
||||
return h;
|
||||
}
|
||||
async function renderSites() {
|
||||
let perms = {};
|
||||
try { perms = await S.invoke("permissions"); } catch {}
|
||||
const origins = Object.keys(perms).filter((o) => perms[o] && (perms[o].readAddress || perms[o].sendTx));
|
||||
const el = $("sites");
|
||||
if (!origins.length) { el.innerHTML = `<div class="hint">None yet.</div>`; return; }
|
||||
el.innerHTML = origins.map((o) => {
|
||||
const p = perms[o]; const what = [];
|
||||
if (p.readAddress) what.push("address");
|
||||
if (p.sendTx) { const left = BigInt(p.sendTx.cap) - BigInt(p.sendTx.used || 0); what.push(`payments: ${fmtSC(left < 0n ? 0n : left)} of ${fmtSC(p.sendTx.cap)} SC left`); }
|
||||
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default"><div><div class="mono">${esc(o)}</div><div class="hint">${esc(what.join(" · "))}</div></div><button class="btn sm" data-origin="${esc(o)}">Revoke</button></div>`;
|
||||
}).join("");
|
||||
el.querySelectorAll("button[data-origin]").forEach((b) => b.addEventListener("click", async () => { try { await S.invoke("revoke", { origin: b.dataset.origin }); renderSites(); } catch {} }));
|
||||
}
|
||||
|
||||
// ---- boot ------------------------------------------------------------------
|
||||
S.on("state", (s) => { state = s; render(); });
|
||||
(async () => {
|
||||
try { state = await S.invoke("state"); render(); }
|
||||
catch (e) { $("gate").hidden = false; $("gate").innerHTML = `<div class="big">⚠</div><div>${esc(cleanErr(e))}</div>`; }
|
||||
})();
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
// Minimal QR encoder for the receive tab: byte mode, versions 1-10, error
|
||||
// correction M (falls back to L when M won't fit). Returns a boolean matrix.
|
||||
// Loaded as a plain script in panel.html and as a CommonJS module in tests.
|
||||
(function (root) {
|
||||
const EC_LEVELS = { L: 1, M: 0 };
|
||||
// Per version: [totalCodewords, {L:[ecPerBlock, [[blocks, dataCw], ...]], M:[...]}]
|
||||
const TABLE = {
|
||||
1: [26, { L: [7, [[1, 19]]], M: [10, [[1, 16]]] }],
|
||||
2: [44, { L: [10, [[1, 34]]], M: [16, [[1, 28]]] }],
|
||||
3: [70, { L: [15, [[1, 55]]], M: [26, [[1, 44]]] }],
|
||||
4: [100, { L: [20, [[1, 80]]], M: [18, [[2, 32]]] }],
|
||||
5: [134, { L: [26, [[1, 108]]], M: [24, [[2, 43]]] }],
|
||||
6: [172, { L: [18, [[2, 68]]], M: [16, [[4, 27]]] }],
|
||||
7: [196, { L: [20, [[2, 78]]], M: [18, [[4, 31]]] }],
|
||||
8: [242, { L: [24, [[2, 97]]], M: [22, [[2, 38], [2, 39]]] }],
|
||||
9: [292, { L: [30, [[2, 116]]], M: [22, [[3, 36], [2, 37]]] }],
|
||||
10: [346, { L: [18, [[2, 68], [2, 69]]], M: [26, [[4, 43], [1, 44]]] }],
|
||||
};
|
||||
const ALIGN = { 1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30], 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50] };
|
||||
const REMAINDER = { 1: 0, 2: 7, 3: 7, 4: 7, 5: 7, 6: 7, 7: 0, 8: 0, 9: 0, 10: 0 };
|
||||
|
||||
// GF(256) with the QR polynomial 0x11d.
|
||||
const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
|
||||
(function () {
|
||||
let x = 1;
|
||||
for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; }
|
||||
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
|
||||
})();
|
||||
const gmul = (a, b) => (a && b) ? EXP[LOG[a] + LOG[b]] : 0;
|
||||
function generator(n) {
|
||||
let g = [1];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const next = new Array(g.length + 1).fill(0);
|
||||
for (let j = 0; j < g.length; j++) { next[j] ^= g[j]; next[j + 1] ^= gmul(g[j], EXP[i]); }
|
||||
g = next;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
function rsEncode(data, nEc) {
|
||||
const g = generator(nEc);
|
||||
const out = new Uint8Array(data.length + nEc);
|
||||
out.set(data);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const c = out[i];
|
||||
if (c) for (let j = 1; j < g.length; j++) out[i + j] ^= gmul(g[j], c);
|
||||
}
|
||||
return out.slice(data.length);
|
||||
}
|
||||
|
||||
function dataCapacity(v, ec) { return TABLE[v][1][ec][1].reduce((a, [b, d]) => a + b * d, 0); }
|
||||
function pickVersion(len) {
|
||||
for (const ec of ["M", "L"]) {
|
||||
for (let v = 1; v <= 10; v++) {
|
||||
const bitsNeeded = 4 + (v <= 9 ? 8 : 16) + len * 8;
|
||||
if (bitsNeeded <= dataCapacity(v, ec) * 8) return { v, ec };
|
||||
}
|
||||
}
|
||||
throw new Error("qr: text too long");
|
||||
}
|
||||
|
||||
function encodeData(bytes, v, ec) {
|
||||
const cap = dataCapacity(v, ec);
|
||||
const bits = [];
|
||||
const push = (val, n) => { for (let i = n - 1; i >= 0; i--) bits.push((val >> i) & 1); };
|
||||
push(0b0100, 4);
|
||||
push(bytes.length, v <= 9 ? 8 : 16);
|
||||
for (const b of bytes) push(b, 8);
|
||||
for (let i = 0; i < 4 && bits.length < cap * 8; i++) bits.push(0);
|
||||
while (bits.length % 8) bits.push(0);
|
||||
const data = new Uint8Array(cap);
|
||||
for (let i = 0; i < bits.length / 8; i++) data[i] = parseInt(bits.slice(i * 8, i * 8 + 8).join(""), 2);
|
||||
for (let i = bits.length / 8, k = 0; i < cap; i++, k++) data[i] = k % 2 ? 0x11 : 0xec;
|
||||
return data;
|
||||
}
|
||||
|
||||
function interleave(data, v, ec) {
|
||||
const [nEc, groups] = TABLE[v][1][ec];
|
||||
const blocks = []; let o = 0;
|
||||
for (const [count, size] of groups) for (let i = 0; i < count; i++) { blocks.push(data.slice(o, o + size)); o += size; }
|
||||
const ecBlocks = blocks.map((b) => rsEncode(b, nEc));
|
||||
const out = [];
|
||||
const maxLen = Math.max(...blocks.map((b) => b.length));
|
||||
for (let i = 0; i < maxLen; i++) for (const b of blocks) if (i < b.length) out.push(b[i]);
|
||||
for (let i = 0; i < nEc; i++) for (const b of ecBlocks) out.push(b[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Remainder of value·x^degree modulo the generator poly (BCH error correction
|
||||
// for the format / version fields).
|
||||
function bch(value, poly, degree) {
|
||||
let v = value << degree;
|
||||
for (let i = 31 - Math.clz32(v); i >= degree; i--) if ((v >> i) & 1) v ^= poly << (i - degree);
|
||||
return v;
|
||||
}
|
||||
const formatBits = (ec, mask) => { const d = (EC_LEVELS[ec] << 3) | mask; return ((d << 10) | bch(d, 0x537, 10)) ^ 0x5412; };
|
||||
const versionBits = (v) => (v << 12) | bch(v, 0x1f25, 12);
|
||||
|
||||
function build(text) {
|
||||
const bytes = typeof text === "string" ? new TextEncoder().encode(text) : Uint8Array.from(text);
|
||||
const { v, ec } = pickVersion(bytes.length);
|
||||
const size = v * 4 + 17;
|
||||
const codewords = interleave(encodeData(bytes, v, ec), v, ec);
|
||||
const grid = Array.from({ length: size }, () => new Uint8Array(size)); // 1 dark, 0 light
|
||||
const fixed = Array.from({ length: size }, () => new Uint8Array(size)); // function pattern / reserved
|
||||
const set = (r, c, val) => { grid[r][c] = val ? 1 : 0; fixed[r][c] = 1; };
|
||||
const finder = (r0, c0) => {
|
||||
for (let r = -1; r <= 7; r++) for (let c = -1; c <= 7; c++) {
|
||||
const rr = r0 + r, cc = c0 + c;
|
||||
if (rr < 0 || cc < 0 || rr >= size || cc >= size) continue;
|
||||
const inner = r >= 0 && r <= 6 && c >= 0 && c <= 6;
|
||||
const dark = inner && (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4));
|
||||
set(rr, cc, dark);
|
||||
}
|
||||
};
|
||||
finder(0, 0); finder(0, size - 7); finder(size - 7, 0);
|
||||
for (let i = 8; i < size - 8; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
|
||||
const al = ALIGN[v], last = al.length - 1;
|
||||
al.forEach((r, ri) => al.forEach((c, ci) => {
|
||||
// The three corners that would sit on a finder pattern are omitted.
|
||||
if ((ri === 0 && ci === 0) || (ri === 0 && ci === last) || (ri === last && ci === 0)) return;
|
||||
for (let dr = -2; dr <= 2; dr++) for (let dc = -2; dc <= 2; dc++) {
|
||||
set(r + dr, c + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1);
|
||||
}
|
||||
}));
|
||||
set(size - 8, 8, 1); // dark module
|
||||
// Reserve format areas (filled per mask below) and version areas.
|
||||
for (let i = 0; i < 9; i++) { fixed[8][i] = 1; fixed[i][8] = 1; }
|
||||
for (let i = 0; i < 8; i++) { fixed[8][size - 1 - i] = 1; fixed[size - 1 - i][8] = 1; }
|
||||
if (v >= 7) {
|
||||
const vb = versionBits(v);
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const bit = (vb >> i) & 1;
|
||||
set(Math.floor(i / 3), size - 11 + (i % 3), bit);
|
||||
set(size - 11 + (i % 3), Math.floor(i / 3), bit);
|
||||
}
|
||||
}
|
||||
// Zigzag data placement.
|
||||
const bits = [];
|
||||
for (const cw of codewords) for (let i = 7; i >= 0; i--) bits.push((cw >> i) & 1);
|
||||
for (let i = 0; i < REMAINDER[v]; i++) bits.push(0);
|
||||
let bi = 0, up = true;
|
||||
for (let col = size - 1; col > 0; col -= 2) {
|
||||
if (col === 6) col--;
|
||||
for (let k = 0; k < size; k++) {
|
||||
const r = up ? size - 1 - k : k;
|
||||
for (const c of [col, col - 1]) {
|
||||
if (fixed[r][c]) continue;
|
||||
grid[r][c] = bi < bits.length ? bits[bi] : 0;
|
||||
bi++;
|
||||
}
|
||||
}
|
||||
up = !up;
|
||||
}
|
||||
// Try every mask, keep the lowest penalty.
|
||||
const MASKS = [
|
||||
(i, j) => (i + j) % 2 === 0, (i) => i % 2 === 0, (_i, j) => j % 3 === 0, (i, j) => (i + j) % 3 === 0,
|
||||
(i, j) => (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0, (i, j) => ((i * j) % 2) + ((i * j) % 3) === 0,
|
||||
(i, j) => (((i * j) % 2) + ((i * j) % 3)) % 2 === 0, (i, j) => (((i + j) % 2) + ((i * j) % 3)) % 2 === 0,
|
||||
];
|
||||
let best = null;
|
||||
for (let m = 0; m < 8; m++) {
|
||||
const g = grid.map((row) => Uint8Array.from(row));
|
||||
for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) if (!fixed[r][c] && MASKS[m](r, c)) g[r][c] ^= 1;
|
||||
const fb = formatBits(ec, m);
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const bit = (fb >> i) & 1;
|
||||
// Copy 1: down column 8 (bits 0-7, skipping the timing row), then
|
||||
// left along row 8 (bits 8-14). Copy 2: right end of row 8, bottom
|
||||
// of column 8.
|
||||
if (i < 6) g[i][8] = bit; else if (i === 6) g[7][8] = bit; else if (i === 7) g[8][8] = bit;
|
||||
else if (i === 8) g[8][7] = bit; else g[8][14 - i] = bit;
|
||||
if (i < 8) g[8][size - 1 - i] = bit; else g[size - 15 + i][8] = bit;
|
||||
}
|
||||
const p = penalty(g, size);
|
||||
if (!best || p < best.p) best = { g, p, m };
|
||||
}
|
||||
return { size, version: v, ec, mask: best.m, modules: best.g.map((row) => Array.from(row, (x) => !!x)) };
|
||||
}
|
||||
|
||||
function penalty(g, n) {
|
||||
let p = 0;
|
||||
const runs = (get) => {
|
||||
for (let a = 0; a < n; a++) {
|
||||
let run = 1;
|
||||
for (let b = 1; b <= n; b++) {
|
||||
if (b < n && get(a, b) === get(a, b - 1)) run++;
|
||||
else { if (run >= 5) p += 3 + run - 5; run = 1; }
|
||||
}
|
||||
}
|
||||
};
|
||||
runs((a, b) => g[a][b]); runs((a, b) => g[b][a]);
|
||||
for (let r = 0; r < n - 1; r++) for (let c = 0; c < n - 1; c++) {
|
||||
const s = g[r][c] + g[r][c + 1] + g[r + 1][c] + g[r + 1][c + 1];
|
||||
if (s === 0 || s === 4) p += 3;
|
||||
}
|
||||
const pat = [1, 0, 1, 1, 1, 0, 1];
|
||||
const finderLike = (get, a) => {
|
||||
for (let b = 0; b <= n - 7; b++) {
|
||||
let ok = true;
|
||||
for (let k = 0; k < 7; k++) if (get(a, b + k) !== pat[k]) { ok = false; break; }
|
||||
if (!ok) continue;
|
||||
const before = b >= 4 && [0, 1, 2, 3].every((k) => get(a, b - 1 - k) === 0);
|
||||
const after = b + 10 < n && [0, 1, 2, 3].every((k) => get(a, b + 7 + k) === 0);
|
||||
if (before || after) p += 40;
|
||||
}
|
||||
};
|
||||
for (let a = 0; a < n; a++) { finderLike((x, y) => g[x][y], a); finderLike((x, y) => g[y][x], a); }
|
||||
let dark = 0;
|
||||
for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) dark += g[r][c];
|
||||
const pct = (dark * 100) / (n * n);
|
||||
p += Math.floor(Math.abs(pct - 50) / 5) * 10;
|
||||
return p;
|
||||
}
|
||||
|
||||
const api = { build };
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
else root.QR = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this);
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
// Dapp bridge for Sia, run in the isolated world of pages matching
|
||||
// addon.json "page-inject".origins. Exposes window.siacoin; every call goes
|
||||
// through the wallet's activate() context in main, which shows the approval
|
||||
// overlay and enforces per-origin permissions.
|
||||
//
|
||||
// `theseus` is provided by the host: { id, origin, contextBridge, invoke }.
|
||||
const call = (msg, payload) =>
|
||||
theseus.invoke(msg, payload).catch((e) => {
|
||||
const text = String(e && e.message || e).replace(/^Error invoking remote method '[^']+': (Error: )?/, "");
|
||||
throw new Error(text);
|
||||
});
|
||||
|
||||
theseus.contextBridge.exposeInMainWorld("siacoin", {
|
||||
isTheseus: true,
|
||||
version: "0.1.0",
|
||||
network: "mainnet",
|
||||
// Current receiving address (76-hex). First call per origin asks the user;
|
||||
// "always allow" makes later calls silent.
|
||||
getAddress: () => call("getAddress"),
|
||||
// txSpec: { to, amount } or { outputs: [{ to, amount }] } — amounts are
|
||||
// hastings as decimal strings (1 SC = 1e24). Always approval-gated;
|
||||
// resolves { txid }.
|
||||
signAndSend: (txSpec) => call("signAndSend", txSpec && typeof txSpec === "object" ? txSpec : {}),
|
||||
// ed25519 signature over blake2b-256(message) with the current address's
|
||||
// key. Always approval-gated; resolves { address, publicKey, signature }.
|
||||
signMessage: (message) => call("signMessage", { message: String(message ?? "") }),
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.27",
|
||||
"version": "0.3.28",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
"main": "main.js",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue