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.
102 lines
4.7 KiB
JavaScript
102 lines
4.7 KiB
JavaScript
// CashAddr (BCH address format) encode/decode + legacy Base58Check decode, so
|
|
// the send form accepts whatever the user pastes but the UI only ever shows
|
|
// cashaddr. Pure JS, no deps — the polymod is tiny.
|
|
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
const CHARSET_REV = Object.fromEntries([...CHARSET].map((c, i) => [c, i]));
|
|
|
|
function polymod(values) {
|
|
let c = 1n;
|
|
for (const d of values) {
|
|
const c0 = c >> 35n;
|
|
c = ((c & 0x07ffffffffn) << 5n) ^ BigInt(d);
|
|
if (c0 & 0x01n) c ^= 0x98f2bc8e61n;
|
|
if (c0 & 0x02n) c ^= 0x79b76d99e2n;
|
|
if (c0 & 0x04n) c ^= 0xf33e5fb3c4n;
|
|
if (c0 & 0x08n) c ^= 0xae2eabe2a8n;
|
|
if (c0 & 0x10n) c ^= 0x1e4f43e470n;
|
|
}
|
|
return c ^ 1n;
|
|
}
|
|
const prefixExpand = (prefix) => [...prefix].map((c) => c.charCodeAt(0) & 0x1f).concat([0]);
|
|
|
|
function convertBits(data, from, to, pad) {
|
|
let acc = 0, bits = 0; const out = [];
|
|
const maxv = (1 << to) - 1;
|
|
for (const v of data) {
|
|
acc = (acc << from) | v; bits += from;
|
|
while (bits >= to) { bits -= to; out.push((acc >> bits) & maxv); }
|
|
}
|
|
if (pad) { if (bits > 0) out.push((acc << (to - bits)) & maxv); }
|
|
else if (bits >= from || ((acc << (to - bits)) & maxv)) throw new Error("cashaddr: bad padding");
|
|
return out;
|
|
}
|
|
|
|
// type: 0 = P2PKH, 1 = P2SH. hash: 20 bytes (the only size we emit).
|
|
function encode(prefix, type, hash) {
|
|
if (hash.length !== 20) throw new Error("cashaddr: only 160-bit hashes supported");
|
|
const versionByte = (type << 3) | 0; // size bits 000 = 160
|
|
const payload = convertBits([versionByte, ...hash], 8, 5, true);
|
|
const mod = polymod([...prefixExpand(prefix), ...payload, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
const checksum = [];
|
|
for (let i = 0; i < 8; i++) checksum.push(Number((mod >> BigInt(5 * (7 - i))) & 0x1fn));
|
|
return prefix + ":" + [...payload, ...checksum].map((v) => CHARSET[v]).join("");
|
|
}
|
|
|
|
// Accepts "prefix:payload" or a bare payload (assumes defaultPrefix).
|
|
function decode(address, defaultPrefix = "bitcoincash") {
|
|
const raw = String(address).trim();
|
|
if (raw !== raw.toLowerCase() && raw !== raw.toUpperCase()) throw new Error("cashaddr: mixed case");
|
|
const s = raw.toLowerCase();
|
|
const i = s.lastIndexOf(":");
|
|
const prefix = i >= 0 ? s.slice(0, i) : defaultPrefix;
|
|
const payloadStr = i >= 0 ? s.slice(i + 1) : s;
|
|
if (!/^[a-z0-9]+$/.test(prefix) || payloadStr.length < 8) throw new Error("cashaddr: malformed");
|
|
const values = [...payloadStr].map((c) => {
|
|
if (!(c in CHARSET_REV)) throw new Error("cashaddr: bad character");
|
|
return CHARSET_REV[c];
|
|
});
|
|
if (polymod([...prefixExpand(prefix), ...values]) !== 0n) throw new Error("cashaddr: bad checksum");
|
|
const data = convertBits(values.slice(0, -8), 5, 8, false);
|
|
const versionByte = data[0];
|
|
const type = (versionByte >> 3) & 0x0f;
|
|
const size = [20, 24, 28, 32, 40, 48, 56, 64][versionByte & 0x07];
|
|
const hash = Uint8Array.from(data.slice(1));
|
|
if (hash.length !== size) throw new Error("cashaddr: hash size mismatch");
|
|
return { prefix, type, hash };
|
|
}
|
|
|
|
// Legacy Base58Check (1... / 3... on mainnet). sha256 is injected so this
|
|
// file stays dependency-free.
|
|
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
function decodeLegacy(address, sha256) {
|
|
const s = String(address).trim();
|
|
let n = 0n;
|
|
for (const c of s) {
|
|
const v = B58.indexOf(c);
|
|
if (v < 0) throw new Error("base58: bad character");
|
|
n = n * 58n + BigInt(v);
|
|
}
|
|
const bytes = [];
|
|
while (n > 0n) { bytes.unshift(Number(n & 0xffn)); n >>= 8n; }
|
|
for (const c of s) { if (c !== "1") break; bytes.unshift(0); }
|
|
if (bytes.length !== 25) throw new Error("base58: wrong length");
|
|
const body = Uint8Array.from(bytes.slice(0, 21));
|
|
const check = sha256(sha256(body)).slice(0, 4);
|
|
for (let k = 0; k < 4; k++) if (check[k] !== bytes[21 + k]) throw new Error("base58: bad checksum");
|
|
const type = body[0] === 0x00 ? 0 : body[0] === 0x05 ? 1 : null;
|
|
if (type == null) throw new Error("base58: not a mainnet address");
|
|
return { prefix: "bitcoincash", type, hash: body.slice(1) };
|
|
}
|
|
|
|
// Anything a user might paste -> { type, hash, cashaddr }. Rejects other
|
|
// prefixes so a chipnet address can never be paid on mainnet by accident.
|
|
function parseAny(input, sha256, prefix = "bitcoincash") {
|
|
const s = String(input || "").trim().replace(/^bitcoincash:\/\//i, "bitcoincash:");
|
|
if (!s) throw new Error("empty address");
|
|
const r = /^[13][1-9A-HJ-NP-Za-km-z]{25,34}$/.test(s) ? decodeLegacy(s, sha256) : decode(s, prefix);
|
|
if (r.prefix !== prefix) throw new Error(`address is for "${r.prefix}", expected "${prefix}"`);
|
|
if (r.type !== 0 && r.type !== 1) throw new Error("unsupported address type");
|
|
return { type: r.type, hash: r.hash, cashaddr: encode(prefix, r.type, r.hash) };
|
|
}
|
|
|
|
module.exports = { encode, decode, decodeLegacy, parseAny };
|