// 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 };