From f053c1befa6abaecc3f967f00c7c02ba16ce1007 Mon Sep 17 00:00:00 2001 From: martbost Date: Fri, 4 Sep 2026 12:25:34 -0500 Subject: [PATCH] InstantAdPay site skeleton: SIWE auth, live chain ledger, join links, buy flow Zero-dependency Node server on the RM Circle pattern. Chain config lives in the volume so the same code runs the Amoy dress rehearsal and mainnet. Co-Authored-By: Claude Fable 5 --- .gitignore | 3 + Dockerfile | 8 + README.md | 32 + accounts.js | 49 ++ auth.js | 120 ++++ chain.js | 216 +++++++ docker-compose.yml | 15 + package.json | 9 + public/assets/common.js | 87 +++ public/assets/home.js | 44 ++ public/assets/ledger.js | 28 + public/assets/my.js | 67 +++ public/assets/site.css | 82 +++ public/assets/wallet.js | 75 +++ public/index.html | 88 +++ public/ledger.html | 29 + public/my.html | 62 ++ server.js | 210 +++++++ vendor/secp256k1.js | 1230 +++++++++++++++++++++++++++++++++++++++ vendor/sha3.js | 662 +++++++++++++++++++++ 20 files changed, 3116 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 accounts.js create mode 100644 auth.js create mode 100644 chain.js create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 public/assets/common.js create mode 100644 public/assets/home.js create mode 100644 public/assets/ledger.js create mode 100644 public/assets/my.js create mode 100644 public/assets/site.css create mode 100644 public/assets/wallet.js create mode 100644 public/index.html create mode 100644 public/ledger.html create mode 100644 public/my.html create mode 100644 server.js create mode 100644 vendor/secp256k1.js create mode 100644 vendor/sha3.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7af0d92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +data/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7057669 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-alpine +WORKDIR /app +COPY . . +RUN mkdir -p /app/data +ENV NODE_ENV=production +ENV PORT=3000 +EXPOSE 3000 +CMD ["node","server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b4a9e9 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# InstantAdPay — site + +Membership advertising with immutable on-chain settlement. Zero-dependency +Node server (RM Circle pattern): static pages + JSON API + SSE ledger. + +## Architecture + +- `server.js` — http server: pages, `/api/*`, `/join/` sponsor links, SSE feed +- `chain.js` — contract reader + persistent event indexer (free public RPCs) +- `auth.js` — SIWE wallet sign-in (EIP-4361), sessions in the volume +- `accounts.js` — site-side records only (free members, first-touch sponsor + attribution, handles). The CHAIN is the source of truth for money/credits. +- `public/` — landing, live ledger, member area; no client libraries + +## Chain flip (rehearsal → mainnet) + +Everything chain-specific lives in `data/config.json` (volume): +`{contract, chainId, chainName, explorer, rpcs, deployBlock}`. +Defaults point at the **Amoy rehearsal** deployment. Launch = deploy the +mainnet contract, wipe `accounts.json`/`sessions.json`/`chain-index.json`, +PATCH `/api/admin/chain` with the mainnet values, set `rehearsal:false` via +`/api/admin/site`. Same code, different config. + +## Run + +``` +PORT=3100 node server.js # DATA_DIR defaults to ./data +``` + +Admin API auth: `Authorization: Bearer $ADMIN_PASSWORD`. + +Spec: `../CONTRACT-SPEC.md` (v1.0.3-frozen). Contracts: `../contracts/`. diff --git a/accounts.js b/accounts.js new file mode 100644 index 0000000..eff6a44 --- /dev/null +++ b/accounts.js @@ -0,0 +1,49 @@ +// Site-side member records for InstantAdPay. +// The chain is the source of truth for money, credits, and qualification; +// this module holds only what the chain doesn't: free members who haven't +// touched the chain yet, sponsor attribution before first purchase (spec §4), +// display handles, and join stats. Wiping this file = the clean reset between +// the Amoy dress rehearsal and mainnet launch. +const fs = require('fs'); +const path = require('path'); + +let DATA_DIR = null; +const FILE = () => path.join(DATA_DIR, 'accounts.json'); +let db = { v: 1, byAddress: {}, joins: 0 }; + +function load() { + try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {} + if (!db || db.v !== 1) db = { v: 1, byAddress: {}, joins: 0 }; +} +function save() { + try { + const tmp = FILE() + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(db)); + fs.renameSync(tmp, FILE()); + } catch (e) { console.error('accounts save failed', e.message); } +} +function init(opts) { DATA_DIR = opts.dataDir; load(); } + +function get(address) { return db.byAddress[(address || '').toLowerCase()] || null; } +function upsert(address, fields) { + const a = (address || '').toLowerCase(); + if (!/^0x[0-9a-f]{40}$/.test(a)) return null; + const cur = db.byAddress[a] || { created: Date.now() }; + db.byAddress[a] = Object.assign(cur, fields || {}); + save(); + return db.byAddress[a]; +} +// Sponsor attribution: first touch wins, written on-chain at the member's +// first purchase/activation and permanent from then on. +function attributeSponsor(address, sponsorId) { + const a = (address || '').toLowerCase(); + const cur = get(a); + if (cur && cur.sponsorId) return cur.sponsorId; // first touch already set + const id = Number(sponsorId) || 0; + upsert(a, { sponsorId: id }); + db.joins += 1; save(); + return id; +} +function count() { return Object.keys(db.byAddress).length; } + +module.exports = { init, get, upsert, attributeSponsor, count }; diff --git a/auth.js b/auth.js new file mode 100644 index 0000000..dba2d2f --- /dev/null +++ b/auth.js @@ -0,0 +1,120 @@ +// Wallet sign-in (SIWE / EIP-4361) for InstantAdPay. +// Pattern lifted from the RM Circle messages.js implementation (proven with +// MetaMask's friendly sign-in UI). One free signature, cannot move funds. +// +// Difference from RM Circle: a wallet WITHOUT an on-chain member id still gets +// a session — free members exist site-side only until their payout activation +// or first purchase writes them on-chain (spec §4). +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { keccak256 } = require('./vendor/sha3'); +const secp = require('./vendor/secp256k1'); + +let DATA_DIR = null; +let chain = null; +let IS_PROD = false; +let SITE = 'instantadpay.com'; + +const CHALLENGE_TTL = 10 * 60 * 1000; +const SESSION_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days +const challenges = new Map(); // addressLower -> {message, exp} +let sessions = new Map(); // token -> {address, memberId, expires} + +const SESS_FILE = () => path.join(DATA_DIR, 'sessions.json'); +function loadSessions() { + try { + const o = JSON.parse(fs.readFileSync(SESS_FILE(), 'utf8')); + sessions = new Map(Object.entries(o).filter(([, s]) => s.expires > Date.now())); + } catch (e) { sessions = new Map(); } +} +function saveSessions() { + try { + const tmp = SESS_FILE() + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(Object.fromEntries(sessions)), { mode: 0o600 }); + fs.renameSync(tmp, SESS_FILE()); + } catch (e) { console.error('session save failed', e.message); } +} + +function init(opts) { + DATA_DIR = opts.dataDir; chain = opts.chain; IS_PROD = !!opts.isProd; + if (opts.site) SITE = opts.site; + loadSessions(); +} + +// ---- crypto ---- +function personalDigest(msg) { + const m = Buffer.from(msg, 'utf8'); + const pre = Buffer.from('\x19Ethereum Signed Message:\n' + m.length, 'utf8'); + return Buffer.from(keccak256(Buffer.concat([pre, m])), 'hex'); +} +function recoverAddress(msg, signature) { + const raw = Buffer.from(String(signature).replace(/^0x/, ''), 'hex'); + if (raw.length !== 65) throw new Error('Bad signature length'); + let v = raw[64]; if (v >= 27) v -= 27; + if (v !== 0 && v !== 1) throw new Error('Bad signature recovery byte'); + const pub = secp.recoverPublicKey(personalDigest(msg), raw.slice(0, 64), v, false); + return '0x' + keccak256(Buffer.from(pub.slice(1))).slice(-40); +} +const ADDR_RE = /^0x[0-9a-fA-F]{40}$/; +function checksumAddress(address) { + const a = address.toLowerCase().replace(/^0x/, ''); + const h = keccak256(a); + let out = '0x'; + for (let i = 0; i < a.length; i++) out += parseInt(h[i], 16) >= 8 ? a[i].toUpperCase() : a[i]; + return out; +} + +// ---- auth flow ---- +function makeChallenge(address) { + if (!ADDR_RE.test(address || '')) return { error: 'Bad address' }; + const a = address.toLowerCase(); + const nonce = crypto.randomBytes(16).toString('hex'); + const chainId = chain.getConfig().chainId; + const message = `${SITE} wants you to sign in with your Ethereum account:\n${checksumAddress(a)}\n\nInstantAdPay member sign-in. This signature is free and cannot move funds or approve anything.\n\nURI: https://${SITE}\nVersion: 1\nChain ID: ${chainId}\nNonce: ${nonce}\nIssued At: ${new Date().toISOString()}`; + challenges.set(a, { message, exp: Date.now() + CHALLENGE_TTL }); + return { message }; +} +async function verifyChallenge(address, signature) { + const a = (address || '').toLowerCase(); + const ch = challenges.get(a); + if (!ch || ch.exp < Date.now()) return { error: 'Challenge expired - tap sign-in again.' }; + let rec; + try { rec = recoverAddress(ch.message, signature); } catch (e) { return { error: 'Invalid signature: ' + e.message }; } + if (rec !== a) return { error: 'Your wallet signed with a different account than the page is using (' + + rec.slice(0, 6) + '…' + rec.slice(-4) + '). Switch accounts and tap sign-in again.' }; + challenges.delete(a); + let memberId = 0; + try { memberId = await chain.memberIdByAccount(a); } catch (e) { /* chain read down: session still valid */ } + const token = crypto.randomBytes(32).toString('hex'); + sessions.set(token, { address: a, memberId, expires: Date.now() + SESSION_TTL }); + saveSessions(); + return { token, address: a, memberId }; +} +function sessionCookie(token) { + return `iap.sid=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL / 1000}${IS_PROD ? '; Secure' : ''}`; +} +function clearCookie() { return 'iap.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'; } +function fromRequest(req) { + const m = /(?:^|;\s*)iap\.sid=([^;]+)/.exec(req.headers.cookie || ''); + if (!m) return null; + const token = decodeURIComponent(m[1]); + const s = sessions.get(token); + if (!s || s.expires < Date.now()) return null; + return Object.assign({ token }, s); +} +async function refreshMemberId(sess) { + // called after an on-chain action so the session learns its new member id + try { + const id = await chain.memberIdByAccount(sess.address); + if (id && id !== sess.memberId) { sess.memberId = id; sessions.set(sess.token, { + address: sess.address, memberId: id, expires: sess.expires }); saveSessions(); } + return id; + } catch (e) { return sess.memberId; } +} +function logout(req) { + const s = fromRequest(req); + if (s) { sessions.delete(s.token); saveSessions(); } +} + +module.exports = { init, makeChallenge, verifyChallenge, sessionCookie, clearCookie, fromRequest, refreshMemberId, logout }; diff --git a/chain.js b/chain.js new file mode 100644 index 0000000..d52b2e1 --- /dev/null +++ b/chain.js @@ -0,0 +1,216 @@ +// On-chain reader + indexer for the InstantAdPay contract. +// Free public RPCs only, zero npm dependencies (RM Circle pattern). +// +// Two jobs: +// 1. READS: member/product/quote lookups via eth_call. +// 2. LIVE TAIL: eth_getLogs over the recent window feeds the public +// transparency ledger. State persists in DATA_DIR across redeploys. +// +// The contract address + chain live in data/config.json so the SAME code +// runs the Amoy dress rehearsal and, later, mainnet (flip config, wipe DB). +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data'); +const CONFIG_FILE = path.join(DATA_DIR, 'config.json'); +const STATE_FILE = path.join(DATA_DIR, 'chain-index.json'); + +// keccak-256 topic hashes, precomputed with cast 2026-09-04 +const TOPICS = { + '0x271ca08b7d4244a2c931d4d329c113aab66e22de98163d90ed791a667735864e': 'MemberActivated', + '0x11dc109cafd0f24c81621f745383ec82be7f2947c650355791e27b56ac83bc8c': 'Purchase', + '0x5bbc207bba1439ff320c25b90f91258bfaeafb156b09481f148da52be4d7ce8e': 'TierPaid', + '0x628405f02369b6b9fc70c1c84e675613626898bcfa536383fdb33695cfd68f7d': 'PassedUp', + '0x3999769fc9743f7d4fe9e264d6e9575e8613d17ca29ea7d887b341e17238e0d1': 'AdminPaid', + '0x980b1d1cb448ce10b9e9f6f41af3fe5610e4084378c81fd0e0a7f5bbf9360bf8': 'BuyerCounted', + '0x97f58994fda6236f3659a1d723c3d42e81841551eae9243477a2948eac6aec46': 'CreditsConsumed', + '0x756a68c7a9e11c294245f97f39923944a5d81db3794102f15ca600934e88ae23': 'AwardPaid', + '0x1f044acf816321a559e13e967225bcf188d869107e381d2b793a469f122c0f69': 'ProductAdded', + '0xafb990f51e0a69f1a1c2ae42a5dc54543e9fafc12ba1c95d130737502070b783': 'PriceChangeQueued', + '0xfa5bbf62287a1aea9b1e3ed371e906f83229f094c5fe8a39c73036f557189580': 'PriceChanged', + '0xc361eff50f1c2ee869e4ddb66dede88a03a9471fb738c680d55fe6ac37f8d459': 'ProductRetired', + '0x3b00801a940479d5435f6ef82acfc5071c5e9bdcddac5150ed53f299366ddd51': 'ProductReactivated', + '0xc46f23bfe0653cac1e97856ba6f31cc9efb436822e835bd6331789bfd574d0e5': 'PriceCached', + '0xf1dfddc73b1fe570da41b81e839cbefa121a067726d2f770d17faa01d0146fb7': 'FallbackPriceUsed' +}; +const SEL = { + quoteWei: '0x7de85694', // quoteWei(uint32) + memberId: '0x39106821', // memberId(address) + members: '0x5f59bb40', // members(uint32) + creditBalance: '0x3a1d5b5c', // creditBalance(uint32,uint8) + products: '0xbf712fd6', // products(uint32) + productCount: '0xe0f6ef87', // productCount() + memberCount: '0x11aee380' // memberCount() +}; + +const CHUNK = 9000; +const POLL_MS = 30000; +const KEEP_EVENTS = 600; + +let cfg = null; +let state = null; +let busy = false; +let onEvent = null; + +function getConfig() { + if (!cfg) { + let saved = {}; + try { saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (e) {} + cfg = Object.assign({ + // Amoy rehearsal defaults; mainnet flips these in the volume config + contract: '0x07786E664AAfc0641eEB5297766935763dbA2288', + chainId: 80002, + chainName: 'Polygon Amoy (testnet rehearsal)', + explorer: 'https://amoy.polygonscan.com', + rpcs: ['https://polygon-amoy-bor-rpc.publicnode.com', 'https://polygon-amoy.drpc.org'], + deployBlock: 46717266 // feed deploy block on Amoy (exact, from broadcast receipts) + }, saved); + } + return cfg; +} +function reloadConfig() { cfg = null; return getConfig(); } + +// ---- JSON-RPC with fallback rotation ---- +let rpcIdx = 0; +function rpcOnce(url, method, params) { + return new Promise((resolve, reject) => { + const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }); + const u = new URL(url); + const req = https.request({ hostname: u.hostname, path: u.pathname + u.search, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 15000 }, + res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { + try { const j = JSON.parse(d); if (j.error) reject(new Error(j.error.message)); else resolve(j.result); } + catch (e) { reject(e); } }); }); + req.on('error', reject); req.on('timeout', () => { req.destroy(new Error('rpc timeout')); }); + req.end(body); + }); +} +async function rpc(method, params) { + const urls = getConfig().rpcs; + let last; + for (let i = 0; i < urls.length; i++) { + const url = urls[(rpcIdx + i) % urls.length]; + try { const r = await rpcOnce(url, method, params); rpcIdx = (rpcIdx + i) % urls.length; return r; } + catch (e) { last = e; } + } + throw last || new Error('all RPCs failed'); +} + +// ---- ABI helpers ---- +const strip = h => (h || '').replace(/^0x/, ''); +const word = (data, i) => strip(data).slice(i * 64, i * 64 + 64); +const toBig = h => BigInt('0x' + (strip(h) || '0')); +const toNum = h => Number(toBig(h)); +const toAddr = h => '0x' + strip(h).slice(-40); +const pad = (v, bits) => BigInt(v).toString(16).padStart(64, '0'); +function decodeString(data, wordIdx) { + try { + const off = toNum(word(data, wordIdx)) / 32; + const len = toNum(word(data, off)); + return Buffer.from(strip(data).slice((off + 1) * 64, (off + 1) * 64 + len * 2), 'hex').toString('utf8'); + } catch (e) { return ''; } +} +async function call(sel, args) { + const data = sel + (args || []).map(a => pad(a)).join(''); + return rpc('eth_call', [{ to: getConfig().contract, data }, 'latest']); +} + +// ---- reads ---- +async function memberIdByAccount(addr) { + const r = await call(SEL.memberId, [BigInt(addr)]); + return toNum(word(r, 0)); +} +async function memberCount() { return toNum(word(await call(SEL.memberCount), 0)); } +async function productCount() { return toNum(word(await call(SEL.productCount), 0)); } +async function member(id) { + const r = await call(SEL.members, [id]); + return { account: toAddr(word(r, 0)), sponsorId: toNum(word(r, 1)), buyerCount: toNum(word(r, 2)), + activated: toNum(word(r, 3)) === 1, countedAsBuyer: toNum(word(r, 4)) === 1 }; +} +async function product(id) { + const r = await call(SEL.products, [id]); + return { id, priceCents: toNum(word(r, 0)), creditType: toNum(word(r, 1)), + creditAmount: Number(toBig(word(r, 2))), active: toNum(word(r, 3)) === 1, exists: toNum(word(r, 4)) === 1 }; +} +async function quoteWei(id) { try { return toBig(word(await call(SEL.quoteWei, [id]), 0)); } catch (e) { return null; } } +async function creditBalance(id, type) { return Number(toBig(word(await call(SEL.creditBalance, [id, type || 0]), 0))); } +async function catalog() { + const n = await productCount(); + const out = []; + for (let i = 1; i <= n; i++) { + const p = await product(i); + if (!p.exists || !p.active) continue; + const q = await quoteWei(i); + out.push(Object.assign(p, { costWei: q === null ? null : q.toString() })); + } + return out; +} + +// ---- event decode ---- +function decodeLog(log) { + const name = TOPICS[log.topics[0]]; + if (!name) return null; + const t = i => log.topics[i]; + const d = log.data; + const base = { type: name, block: parseInt(log.blockNumber, 16), tx: log.transactionHash, li: parseInt(log.logIndex, 16) }; + switch (name) { + case 'MemberActivated': return Object.assign(base, { id: toNum(t(1)), account: toAddr(t(2)), sponsorId: toNum(word(d, 0)) }); + case 'Purchase': return Object.assign(base, { buyerId: toNum(t(1)), productId: toNum(t(2)), + paidWei: toBig(word(d, 0)).toString(), priceCents: toNum(word(d, 1)), creditType: toNum(word(d, 2)), creditAmount: Number(toBig(word(d, 3))) }); + case 'TierPaid': return Object.assign(base, { buyerId: toNum(t(1)), recipientId: toNum(t(2)), + tier: toNum(word(d, 0)), amountWei: toBig(word(d, 1)).toString(), hops: toNum(word(d, 2)) }); + case 'PassedUp': return Object.assign(base, { buyerId: toNum(t(1)), tier: toNum(word(d, 0)), skippedId: toNum(word(d, 1)), reason: decodeString(d, 2) }); + case 'AdminPaid': return Object.assign(base, { buyerId: toNum(t(1)), amountWei: toBig(word(d, 0)).toString() }); + case 'BuyerCounted': return Object.assign(base, { sponsorId: toNum(t(1)), newBuyerId: toNum(t(2)), newCount: toNum(word(d, 0)) }); + case 'CreditsConsumed': return Object.assign(base, { memberId: toNum(t(1)), creditType: toNum(word(d, 0)), amount: Number(toBig(word(d, 1))), ref: word(d, 2) }); + case 'AwardPaid': return Object.assign(base, { from: toAddr(t(1)), toId: toNum(t(2)), amountWei: toBig(word(d, 0)).toString() }); + default: return base; // catalog/oracle events: type + tx is enough for the feed + } +} + +// ---- persistent live tail ---- +function loadState() { + try { state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch (e) { state = null; } + if (!state || state.v !== 1) state = { v: 1, lastBlock: getConfig().deployBlock - 1, events: [] }; +} +function saveState() { + try { + const tmp = STATE_FILE + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(state)); + fs.renameSync(tmp, STATE_FILE); + } catch (e) { console.error('chain state save failed', e.message); } +} +async function tail() { + if (busy) return; busy = true; + try { + const latest = parseInt(await rpc('eth_blockNumber', []), 16); + while (state.lastBlock < latest) { + const from = state.lastBlock + 1; + const to = Math.min(from + CHUNK - 1, latest); + const logs = await rpc('eth_getLogs', [{ address: getConfig().contract, + fromBlock: '0x' + from.toString(16), toBlock: '0x' + to.toString(16) }]); + for (const lg of logs) { + const ev = decodeLog(lg); + if (!ev) continue; + state.events.push(ev); + if (onEvent) { try { onEvent(ev); } catch (e) { console.error('chain onEvent', e.message); } } + } + if (state.events.length > KEEP_EVENTS) state.events = state.events.slice(-KEEP_EVENTS); + state.lastBlock = to; + } + saveState(); + } catch (e) { console.error('chain tail', e.message); } + busy = false; +} +function recentEvents(n) { return state ? state.events.slice(-(n || 100)).reverse() : []; } + +function init(opts) { + if (opts && opts.onEvent) onEvent = opts.onEvent; + getConfig(); loadState(); + tail(); + setInterval(tail, POLL_MS); +} + +module.exports = { init, getConfig, reloadConfig, memberIdByAccount, memberCount, member, + product, productCount, quoteWei, creditBalance, catalog, recentEvents, rpc }; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..25d3935 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + instantadpay: + build: . + restart: unless-stopped + environment: + NODE_ENV: production + PORT: 3000 + ADMIN_PASSWORD: ${ADMIN_PASSWORD} + DATA_DIR: /app/data + volumes: + - instantadpay-data:/app/data + ports: + - "3000:3000" +volumes: + instantadpay-data: diff --git a/package.json b/package.json new file mode 100644 index 0000000..59a1914 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "instantadpay-site", + "version": "0.1.0", + "private": true, + "description": "InstantAdPay membership advertising site - immutable on-chain settlement, transparent ledger.", + "main": "server.js", + "scripts": {"start": "node server.js", "dev": "node --watch server.js"}, + "engines": {"node": ">=20"} +} diff --git a/public/assets/common.js b/public/assets/common.js new file mode 100644 index 0000000..c88424c --- /dev/null +++ b/public/assets/common.js @@ -0,0 +1,87 @@ +// Shared page runtime: site config, nav, formatting. Zero dependencies. +window.IAP = (function () { + let config = null; + const $ = id => document.getElementById(id); + + async function getConfig() { + if (!config) config = await (await fetch('/api/config')).json(); + return config; + } + function fmtPol(wei) { + const s = BigInt(wei).toString().padStart(19, '0'); + const whole = s.slice(0, -18) || '0'; + const frac = s.slice(-18, -12).replace(/0+$/, ''); + return whole + (frac ? '.' + frac : ''); + } + const fmtUsd = cents => '$' + (cents / 100).toFixed(2); + + function status(msg, cls) { + let el = $('status'); + if (!el) { el = document.createElement('div'); el.id = 'status'; document.body.appendChild(el); } + el.textContent = msg; el.className = cls || ''; el.hidden = false; + clearTimeout(status._t); + if (cls === 'ok') status._t = setTimeout(() => { el.hidden = true; }, 6000); + } + + async function renderNav(active) { + const c = await getConfig(); + const nav = document.createElement('nav'); + nav.innerHTML = '
' + + '' + + '' + + 'How it works' + + 'Live ledger' + + 'My account' + + '…
'; + document.body.prepend(nav); + if (c.rehearsal) { + const b = document.createElement('div'); + b.className = 'rehearsal'; + b.innerHTML = 'Testnet rehearsal — running on ' + c.chainName + '. Purchases use valueless test POL while we prove every payout in public.'; + document.body.prepend(b); + } + const a = nav.querySelector('[data-p="' + active + '"]'); + if (a) a.style.color = 'var(--ink)'; + refreshNavWallet(); + } + async function refreshNavWallet() { + try { + const me = await (await fetch('/api/me')).json(); + const el = $('navWallet'); + if (!el) return; + if (me.signedIn) { + el.innerHTML = (me.memberId ? 'member #' + me.memberId + ' ' : '') + + '' + me.address.slice(0, 6) + '…' + me.address.slice(-4) + ''; + } else { + el.innerHTML = 'Sign in'; + } + return me; + } catch (e) { return null; } + } + function describeEvent(ev, c) { + const pol = w => fmtPol(w) + ' POL'; + switch (ev.type) { + case 'Purchase': return '🧾 member #' + ev.buyerId + ' bought package #' + ev.productId + + ' (' + fmtUsd(ev.priceCents) + ') for ' + pol(ev.paidWei) + ' → +' + ev.creditAmount.toLocaleString() + ' credits'; + case 'TierPaid': return '💸 level ' + ev.tier + ' payout → member #' + ev.recipientId + ': ' + pol(ev.amountWei) + + (ev.hops ? ' (passed up ' + ev.hops + ')' : ''); + case 'PassedUp': return '↷ level ' + ev.tier + ' passed over #' + ev.skippedId + ' (' + ev.reason + ')'; + case 'AdminPaid': return '🏛 platform fee settled: ' + pol(ev.amountWei); + case 'BuyerCounted': return '⭐ member #' + ev.sponsorId + ' now has ' + ev.newCount + ' qualifying buyer(s)'; + case 'MemberActivated': return '👤 member #' + ev.id + ' activated a payout wallet'; + case 'AwardPaid': return '🎁 award: ' + pol(ev.amountWei) + ' → member #' + ev.toId; + case 'CreditsConsumed': return '📣 member #' + ev.memberId + ' ran ads: −' + ev.amount.toLocaleString() + ' credits'; + case 'PriceCached': return '🔮 oracle price refreshed'; + case 'FallbackPriceUsed': return '🔮 cached price bridged an oracle gap'; + default: return '· ' + ev.type; + } + } + function feedRow(ev, c) { + const div = document.createElement('div'); + div.className = 'row t-' + ev.type; + div.innerHTML = '' + describeEvent(ev, c) + '' + + 'verify ↗'; + return div; + } + return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, $ }; +})(); diff --git a/public/assets/home.js b/public/assets/home.js new file mode 100644 index 0000000..59570b2 --- /dev/null +++ b/public/assets/home.js @@ -0,0 +1,44 @@ +// Landing page: live ladder, buy buttons, sponsor attribution line. +(async function () { + await IAP.renderNav('home'); + const c = await IAP.getConfig(); + IAP.$('contractLink').href = c.explorer + '/address/' + c.contract; + + const sp = await (await fetch('/api/sponsor')).json(); + if (sp.sponsorId) { + const el = IAP.$('sponsorLine'); + el.hidden = false; + el.textContent = 'You were invited by member #' + sp.sponsorId + ' — your purchases pay their team, and your own link will do the same for you.'; + } + + async function loadLadder() { + const { products } = await (await fetch('/api/catalog')).json(); + const tb = document.querySelector('#ladder tbody'); + tb.innerHTML = ''; + const NAMES = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' }; + for (const p of products) { + const tr = document.createElement('tr'); + tr.innerHTML = '' + (NAMES[p.id] || 'Package ' + p.id) + '' + + '' + IAP.fmtUsd(p.priceCents) + '' + + '' + p.creditAmount.toLocaleString() + '' + + '' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL' : 'paused') + '' + + ''; + tb.appendChild(tr); + } + tb.querySelectorAll('button[data-id]').forEach(b => b.addEventListener('click', () => buyPack(b))); + } + async function buyPack(btn) { + try { + btn.disabled = true; + IAP.status('Confirm the purchase in your wallet…'); + const r = await IAPWallet.buy(Number(btn.dataset.id), sp.sponsorId || 0, btn.dataset.cost); + if (r.receipt.status !== '0x1') throw new Error('Transaction reverted — see the explorer.'); + IAP.status('Purchase settled on-chain — credits are yours, payouts delivered. Watch it on the ledger.', 'ok'); + IAP.refreshNavWallet(); + } catch (e) { + IAP.status('Purchase failed: ' + (e.message || e), 'bad'); + } finally { btn.disabled = false; } + } + loadLadder(); +})(); diff --git a/public/assets/ledger.js b/public/assets/ledger.js new file mode 100644 index 0000000..db27772 --- /dev/null +++ b/public/assets/ledger.js @@ -0,0 +1,28 @@ +// Live ledger: recent history + SSE stream of new chain events. +(async function () { + await IAP.renderNav('ledger'); + const c = await IAP.getConfig(); + IAP.$('contractLink').href = c.explorer + '/address/' + c.contract; + const feed = IAP.$('feed'); + + const { events } = await (await fetch('/api/feed?n=150')).json(); + feed.innerHTML = ''; + if (!events.length) feed.innerHTML = '
No activity yet — the first purchase will appear here the moment it lands.
'; + for (const ev of events) feed.appendChild(IAP.feedRow(ev, c)); + + try { + const stats = await (await fetch('/api/stats')).json(); + IAP.$('statLine').textContent = stats.onchainMembers + ' on-chain member(s)'; + } catch (e) {} + + const es = new EventSource('/api/feed/live'); + es.onopen = () => { const b = IAP.$('liveBadge'); b.textContent = '● live'; }; + es.onerror = () => { const b = IAP.$('liveBadge'); b.textContent = 'reconnecting…'; }; + es.onmessage = m => { + try { + const ev = JSON.parse(m.data); + feed.prepend(IAP.feedRow(ev, c)); + while (feed.children.length > 200) feed.removeChild(feed.lastChild); + } catch (e) {} + }; +})(); diff --git a/public/assets/my.js b/public/assets/my.js new file mode 100644 index 0000000..3233828 --- /dev/null +++ b/public/assets/my.js @@ -0,0 +1,67 @@ +// My account: SIWE sign-in, member state, free activation, invite link. +(async function () { + await IAP.renderNav('my'); + const $ = IAP.$; + + async function render() { + const me = await IAP.refreshNavWallet(); + if (!me || !me.signedIn) { $('signinCard').hidden = false; $('memberArea').hidden = true; return; } + $('signinCard').hidden = true; + $('memberArea').hidden = false; + + if (me.memberId) { + $('posLine').innerHTML = 'On-chain member #' + me.memberId + '
wallet ' + + me.address.slice(0, 8) + '…' + me.address.slice(-6) + '' + + (me.onchainSponsorId ? '
sponsored by member #' + me.onchainSponsorId : '
no sponsor (house line)'); + $('creditLine').textContent = (me.credits || 0).toLocaleString(); + const bc = me.buyerCount || 0; + $('qualLine').innerHTML = '' + bc + ' qualifying buyer(s) referred
' + + (bc >= 5 ? 'Level 3 unlocked — full three-level earnings' + : bc >= 2 ? 'Level 2 unlocked · ' + (5 - bc) + ' more for level 3' + : (2 - bc) + ' more ≥$20 buyer(s) unlock level 2'); + $('activateCard').hidden = true; + $('inviteLine').textContent = location.origin + '/join/' + me.memberId; + $('copyInvite').hidden = false; + } else { + $('posLine').innerHTML = 'Signed in as ' + me.address.slice(0, 8) + '…' + me.address.slice(-6) + + '
free member — not on-chain yet' + + (me.sponsorId ? '
invited by member #' + me.sponsorId : ''); + $('creditLine').textContent = '0'; + $('qualLine').textContent = 'Activate your payout wallet (or buy any package) to start; referrals who buy ≥$20 packages qualify you.'; + $('activateCard').hidden = false; + $('inviteLine').textContent = 'Your link appears after your free on-chain activation.'; + $('copyInvite').hidden = true; + } + } + + $('signinBtn').addEventListener('click', async () => { + try { + $('signinBtn').disabled = true; + IAP.status('Check your wallet for the free sign-in signature…'); + await IAPWallet.signIn(); + IAP.status('Signed in.', 'ok'); + await render(); + } catch (e) { IAP.status((e && e.message) || String(e), 'bad'); } + finally { $('signinBtn').disabled = false; } + }); + + $('activateBtn').addEventListener('click', async () => { + try { + $('activateBtn').disabled = true; + const me = await (await fetch('/api/me')).json(); + IAP.status('Confirm the free activation in your wallet…'); + const r = await IAPWallet.activate(me.sponsorId || 0); + if (r.receipt.status !== '0x1') throw new Error('Transaction reverted — see the explorer.'); + IAP.status('Payout wallet activated — your invite link is live.', 'ok'); + await render(); + } catch (e) { IAP.status('Activation failed: ' + ((e && e.message) || e), 'bad'); } + finally { $('activateBtn').disabled = false; } + }); + + $('copyInvite').addEventListener('click', async () => { + try { await navigator.clipboard.writeText($('inviteLine').textContent); IAP.status('Link copied.', 'ok'); } + catch (e) { IAP.status('Copy failed — select and copy the link text.', 'bad'); } + }); + + render(); +})(); diff --git a/public/assets/site.css b/public/assets/site.css new file mode 100644 index 0000000..07cf4d9 --- /dev/null +++ b/public/assets/site.css @@ -0,0 +1,82 @@ +/* InstantAdPay — site-wide styles. + Identity: "wire-transfer receipt meets neon ledger" — dark bank-slate ground, + electric mint for money-in-motion, warm amber for calls to action. */ +:root{ + --ground:#0d1420; --panel:#141d2e; --panel2:#1a2538; --line:#26344d; + --ink:#e8eef7; --muted:#8fa1bb; --mint:#3ee6a8; --mint-soft:#10362b; + --amber:#f5b83d; --amber-ink:#0d1420; --bad:#ff8f7d; --mono:"Consolas","JetBrains Mono",monospace; +} +*{box-sizing:border-box} +body{margin:0;background:var(--ground);color:var(--ink);font:16px/1.55 "Segoe UI",system-ui,sans-serif} +a{color:var(--mint);text-decoration:none} +a:hover{text-decoration:underline} +.wrap{max-width:1020px;margin:0 auto;padding:0 18px} +/* nav */ +nav{border-bottom:1px solid var(--line);background:rgba(13,20,32,.92);position:sticky;top:0;z-index:10} +nav .wrap{display:flex;align-items:center;gap:22px;height:58px} +.logo{font-weight:800;font-size:19px;color:var(--ink)} +.logo b{color:var(--amber)} +nav .links{display:flex;gap:18px;font-size:14.5px;flex:1} +nav a{color:var(--muted)} +nav a:hover{color:var(--ink);text-decoration:none} +#navWallet{font-size:13.5px} +.rehearsal{background:#3d2a52;color:#c9a9f7;text-align:center;font-size:13px;padding:6px 10px} +.rehearsal b{color:#e6d5ff} +/* buttons */ +.btn{display:inline-block;background:var(--amber);color:var(--amber-ink);border:0;border-radius:10px; + padding:12px 22px;font-weight:800;font-size:15.5px;cursor:pointer;font-family:inherit} +.btn.sec{background:transparent;color:var(--mint);border:2px solid var(--mint);padding:10px 20px} +.btn.small{padding:8px 14px;font-size:13.5px} +.btn:disabled{opacity:.45;cursor:default} +/* layout blocks */ +.hero{padding:64px 0 40px} +.hero h1{font-size:clamp(30px,5.5vw,50px);line-height:1.08;margin:0 0 14px;max-width:640px} +.hero h1 em{color:var(--mint);font-style:normal} +.hero p.lead{color:var(--muted);font-size:18px;max-width:560px;margin:0 0 26px} +.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;margin:0 0 16px} +.grid{display:grid;gap:16px} +@media(min-width:760px){.grid.c3{grid-template-columns:1fr 1fr 1fr}.grid.c2{grid-template-columns:1fr 1fr}} +h2{font-size:24px;margin:36px 0 14px} +h3{font-size:17px;margin:0 0 8px} +.muted{color:var(--muted)} +.small{font-size:13.5px} +.mono{font-family:var(--mono)} +/* ladder table */ +table{width:100%;border-collapse:collapse;font-size:15px} +th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:middle} +th{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:1px} +td.num,th.num{font-variant-numeric:tabular-nums} +tr:last-child td{border-bottom:0} +.tablewrap{overflow-x:auto} +/* ledger feed */ +.feed{font-family:var(--mono);font-size:13.5px;line-height:1.7} +.feed .row{padding:7px 10px;border-bottom:1px solid var(--line);display:flex;gap:10px;align-items:baseline;flex-wrap:wrap} +.feed .row:first-child{background:var(--mint-soft)} +.feed .t-Purchase{color:var(--amber)} +.feed .t-TierPaid{color:var(--mint)} +.feed .t-AdminPaid{color:var(--muted)} +.feed .t-AwardPaid{color:var(--amber)} +.feed .tx a{color:var(--muted);font-size:12px} +.badge{display:inline-block;background:var(--mint-soft);color:var(--mint);border-radius:10px;padding:2px 10px;font-size:12.5px;font-weight:700} +.badge.amber{background:#3c2f10;color:var(--amber)} +/* split diagram strip */ +.split{display:flex;gap:8px;margin:14px 0} +.split div{border-radius:8px;padding:10px 6px;text-align:center;font-size:12.5px;font-weight:700} +.split .s50{flex:5;background:var(--mint-soft);color:var(--mint)} +.split .s20{flex:2;background:#173347;color:#6cc4ee} +.split .s10{flex:1;background:#2c2440;color:#b39df1} +.split .sa{flex:2;background:#33290f;color:var(--amber)} +/* status line + toasts */ +#status{position:fixed;left:50%;transform:translateX(-50%);bottom:22px;background:var(--panel2); + border:1px solid var(--line);border-radius:12px;padding:12px 20px;font-size:14px;max-width:90vw; + box-shadow:0 8px 30px rgba(0,0,0,.5)} +#status.ok{border-color:var(--mint)} +#status.bad{border-color:var(--bad)} +footer{border-top:1px solid var(--line);margin-top:60px;padding:26px 0;color:var(--muted);font-size:13.5px} +input,select{background:var(--ground);border:1px solid var(--line);color:var(--ink);border-radius:8px; + padding:10px 12px;font-size:14.5px;font-family:inherit} +:focus-visible{outline:2px solid var(--mint);outline-offset:2px} +@media(prefers-reduced-motion:no-preference){ + .feed .row:first-child{animation:landed .9s ease} + @keyframes landed{from{background:#1d5c44}to{background:var(--mint-soft)}} +} diff --git a/public/assets/wallet.js b/public/assets/wallet.js new file mode 100644 index 0000000..8fddf45 --- /dev/null +++ b/public/assets/wallet.js @@ -0,0 +1,75 @@ +// Wallet plumbing: EIP-1193 connect, chain add/switch, SIWE sign-in, and raw +// calldata builders for the contract's tx functions (no library needed). +window.IAPWallet = (function () { + const SEL_BUY = '0xfd095e97'; // buy(uint32,uint32) + const SEL_ACTIVATE = '0x1a93ec95'; // activate(uint32) + const pad = v => BigInt(v).toString(16).padStart(64, '0'); + + function eth() { + if (!window.ethereum) throw new Error('No wallet found. Open this page in a browser with MetaMask (or a wallet browser).'); + return window.ethereum; + } + async function ensureChain(c) { + const want = '0x' + Number(c.chainId).toString(16); + const cur = await eth().request({ method: 'eth_chainId' }); + if (cur === want) return; + try { + await eth().request({ method: 'wallet_switchEthereumChain', params: [{ chainId: want }] }); + } catch (e) { + if (e.code !== 4902) throw e; + await eth().request({ method: 'wallet_addEthereumChain', params: [{ + chainId: want, chainName: c.chainName, nativeCurrency: { name: 'POL', symbol: 'POL', decimals: 18 }, + rpcUrls: [c.rpc], blockExplorerUrls: [c.explorer] }] }); + } + } + async function connect() { + const c = await IAP.getConfig(); + const [addr] = await eth().request({ method: 'eth_requestAccounts' }); + await ensureChain(c); + return addr; + } + // SIWE: challenge -> personal_sign -> verify (server sets the session cookie) + async function signIn() { + const addr = await connect(); + const ch = await (await fetch('/api/auth/challenge', { method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr }) })).json(); + if (ch.error) throw new Error(ch.error); + const sig = await eth().request({ method: 'personal_sign', params: [ch.message, addr] }); + const r = await (await fetch('/api/auth/verify', { method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr, signature: sig }) })).json(); + if (r.error) throw new Error(r.error); + return r; // {address, memberId, sponsorId} + } + async function sendTx(data, valueWei) { + const c = await IAP.getConfig(); + const [addr] = await eth().request({ method: 'eth_requestAccounts' }); + await ensureChain(c); + const tx = { from: addr, to: c.contract, data }; + if (valueWei) tx.value = '0x' + BigInt(valueWei).toString(16); + return eth().request({ method: 'eth_sendTransaction', params: [tx] }); + } + async function waitTx(hash) { + const c = await IAP.getConfig(); + for (let i = 0; i < 60; i++) { + const r = await (await fetch(c.rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionReceipt', params: [hash] }) })).json(); + if (r.result) return r.result; + await new Promise(res => setTimeout(res, 2500)); + } + throw new Error('Timed out waiting for the transaction — check the explorer.'); + } + // buy: quote is read live server-side; pad 2% for oracle drift, contract + // refunds every wei of excess in the same transaction. + async function buy(productId, sponsorId, costWei) { + const value = BigInt(costWei) + BigInt(costWei) / 50n; + const data = SEL_BUY + pad(productId) + pad(sponsorId || 0); + const hash = await sendTx(data, value); + return { hash, receipt: await waitTx(hash) }; + } + async function activate(sponsorId) { + const data = SEL_ACTIVATE + pad(sponsorId || 0); + const hash = await sendTx(data, null); + return { hash, receipt: await waitTx(hash) }; + } + return { connect, signIn, buy, activate, waitTx }; +})(); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b5ba621 --- /dev/null +++ b/public/index.html @@ -0,0 +1,88 @@ + + + + +InstantAdPay — advertising that pays instantly, on-chain + + + + +
+
+

Advertising that pays the people who build it — instantly, on-chain.

+

Free to join. Real ad inventory. And when anyone buys an ad package, + the payment splits to their sponsors in the same blockchain transaction — + no balances, no withdrawal requests, no company holding your money. Ever.

+

+ Join free + Watch payments land live +

+ +
+ +

Where every dollar goes — enforced by code, not promises

+
+
+
50%
direct sponsor
+
20%
level 2
+
10%
level 3
+
20%
platform
+
+

These percentages are constants in an immutable smart contract — there is no + function to change them, pause payouts, or hold funds. The contract's balance is zero after every + sale because everything is delivered the moment it arrives. Don't take our word for it: + every payment is public on the live ledger with a verify link to the blockchain.

+
+ +
+

🆓 Join free, earn from day one

+

Membership costs nothing. Share your link and you earn 50% of every ad package + your referrals ever buy — not once, every time. Payment arrives in your own wallet within seconds + of their purchase.

+

📣 Real advertising, on real audiences

+

Packages buy ad credits delivered across our owned network — banner, text, and + login placements seen by active members. Credits are recorded on-chain and only ever spent by + your own campaigns.

+

🔎 Qualification by performance

+

Deeper earning levels unlock by referring real buyers — 2 buyers unlock level 2, + 5 unlock level 3. No buying your way in, no timers, no demotions. When someone in your line isn't + qualified, their share passes up to the next person who is.

+
+ +

The ad packages

+
+
+ + + +
PackagePriceAd creditsCost right now
Loading live prices from the contract…
+
+

Prices are set in dollars and settled in POL at the live exchange rate the + moment you buy (Chainlink oracle). Overpayment from rate movement is refunded in the same transaction. + Packages of $20 or more count toward your sponsor's qualification.

+
+ +

Why this is different

+
+

No trust required

+

Most affiliate platforms ask you to trust their dashboard number and their + payout schedule. Here there is no dashboard number to trust — your earnings arrive as blockchain + transactions to your own wallet, and the contract that sends them cannot be modified by anyone, + including us.

+

Honest about what it is

+

This is advertising with a referral program, not an investment. Nobody earns + without real ad purchases happening, and no income is guaranteed. What we guarantee is the part + code can guarantee: if a purchase happens in your line, your share reaches your wallet — instantly, + or it visibly passes to someone qualified.

+
+ +
+
InstantAdPay · every payment verifiable on-chain · live ledger · view the contract ↗
+
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible — never spend what you cannot afford.
+
+
+ + + + + diff --git a/public/ledger.html b/public/ledger.html new file mode 100644 index 0000000..3ee74a2 --- /dev/null +++ b/public/ledger.html @@ -0,0 +1,29 @@ + + + + +Live ledger — InstantAdPay + + + + +
+
+

The live ledger

+

This feed is not our database — it is the blockchain itself, decoded. + Every line has a verify link that opens the raw transaction. If it's not here, it didn't happen; + if it is here, nobody can undo it.

+

connecting… +

+
+
+
Loading recent history…
+
+ +
+ + + + diff --git a/public/my.html b/public/my.html new file mode 100644 index 0000000..80f52b1 --- /dev/null +++ b/public/my.html @@ -0,0 +1,62 @@ + + + + +My account — InstantAdPay + + + +
+
+

My account

+

One free wallet signature signs you in — it cannot move funds or approve anything. + No email, no password.

+
+ +
+

Sign in with your wallet

+

New here? The same button creates your free membership. Your earnings always go + straight to this wallet — we never hold them.

+ +
+ + + + +
+ + + + + diff --git a/server.js b/server.js new file mode 100644 index 0000000..9123cb5 --- /dev/null +++ b/server.js @@ -0,0 +1,210 @@ +// InstantAdPay — membership advertising with immutable on-chain settlement. +// Zero-dependency Node server (RM Circle pattern): static pages + JSON API, +// wallet sign-in (SIWE), live contract ledger, sponsor join links. +// +// Chain selection (Amoy rehearsal vs mainnet) lives in data/config.json — +// see chain.js. Wipe the volume's accounts/sessions + flip config = launch. +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const { URL } = require('url'); +const chain = require('./chain'); +const auth = require('./auth'); +const accounts = require('./accounts'); + +const PORT = Number(process.env.PORT || 3000); +const ROOT = __dirname; +const PUBLIC_DIR = path.join(ROOT, 'public'); +const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data'); +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme'; +const IS_PROD = process.env.NODE_ENV === 'production'; +const SITE_FILE = path.join(DATA_DIR, 'site.json'); + +fs.mkdirSync(DATA_DIR, { recursive: true }); +chain.init({ onEvent: ev => pushFeed(ev) }); +auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' }); +accounts.init({ dataDir: DATA_DIR }); + +function siteConfig() { + let saved = {}; + try { saved = JSON.parse(fs.readFileSync(SITE_FILE, 'utf8')); } catch (e) {} + return Object.assign({ + siteName: 'InstantAdPay', + tagline: 'Advertising that pays the people who build it — instantly, on-chain.', + rehearsal: true // shows the testnet banner; flipped off at mainnet launch + }, saved); +} + +// ---- helpers ---- +const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', + '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp', + '.ico': 'image/x-icon', '.json': 'application/json', '.mp4': 'video/mp4', '.woff2': 'font/woff2' }; +const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'"; +function baseHeaders(extra) { + return Object.assign({ 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff', + 'Referrer-Policy': 'strict-origin-when-cross-origin' }, extra || {}); +} +function json(res, code, obj, extra) { + const body = JSON.stringify(obj); + res.writeHead(code, baseHeaders(Object.assign({ 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }, extra))); + res.end(body); +} +function sendFile(res, file) { + fs.readFile(file, (err, data) => { + if (err) { res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' })); return res.end('Not found'); } + const ext = path.extname(file).toLowerCase(); + res.writeHead(200, baseHeaders({ 'Content-Type': MIME[ext] || 'application/octet-stream', + 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600' })); + res.end(data); + }); +} +function readBody(req) { + return new Promise((resolve, reject) => { + let d = ''; let n = 0; + req.on('data', c => { n += c.length; if (n > 64 * 1024) { req.destroy(); reject(new Error('too big')); } d += c; }); + req.on('end', () => { try { resolve(d ? JSON.parse(d) : {}); } catch (e) { reject(e); } }); + req.on('error', reject); + }); +} +function parseCookies(req) { + const out = {}; + for (const p of (req.headers.cookie || '').split(';')) { + const i = p.indexOf('='); if (i > 0) out[p.slice(0, i).trim()] = decodeURIComponent(p.slice(i + 1).trim()); + } + return out; +} +function isAdmin(req) { + const h = req.headers.authorization || ''; + return h === 'Bearer ' + ADMIN_PASSWORD; +} + +// ---- live feed (SSE) ---- +const feedClients = new Set(); +function pushFeed(ev) { + const line = 'data: ' + JSON.stringify(ev) + '\n\n'; + for (const res of feedClients) { try { res.write(line); } catch (e) { feedClients.delete(res); } } +} + +// ---- server ---- +const server = http.createServer(async (req, res) => { + try { + const u = new URL(req.url, 'http://x'); + const p = u.pathname; + + // -- join links: /join/ — first-touch attribution cookie + let m = /^\/join\/(\d{1,9})$/.exec(p); + if (m && req.method === 'GET') { + const sid = Number(m[1]); + const cookies = parseCookies(req); + const headers = { Location: '/' }; + if (!cookies['iap.sponsor']) { + headers['Set-Cookie'] = `iap.sponsor=${sid}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; + } + res.writeHead(302, baseHeaders(headers)); + return res.end(); + } + + // -- public API + if (p === '/api/config' && req.method === 'GET') { + const c = chain.getConfig(); + return json(res, 200, Object.assign({ contract: c.contract, chainId: c.chainId, + chainName: c.chainName, explorer: c.explorer, rpc: c.rpcs[0] }, siteConfig())); + } + if (p === '/api/catalog' && req.method === 'GET') { + return json(res, 200, { products: await chain.catalog() }); + } + if (p === '/api/feed' && req.method === 'GET') { + return json(res, 200, { events: chain.recentEvents(Number(u.searchParams.get('n')) || 100) }); + } + if (p === '/api/feed/live' && req.method === 'GET') { + res.writeHead(200, baseHeaders({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', Connection: 'keep-alive' })); + res.write(': connected\n\n'); + feedClients.add(res); + req.on('close', () => feedClients.delete(res)); + return; + } + if (p === '/api/sponsor' && req.method === 'GET') { + const sid = Number(parseCookies(req)['iap.sponsor']) || 0; + let sponsor = null; + if (sid) { try { const mm = await chain.member(sid); if (mm.account !== '0x' + '0'.repeat(40)) sponsor = { id: sid }; } catch (e) {} } + return json(res, 200, { sponsorId: sponsor ? sid : 0 }); + } + if (p === '/api/stats' && req.method === 'GET') { + let members = 0; try { members = await chain.memberCount(); } catch (e) {} + return json(res, 200, { onchainMembers: members, siteAccounts: accounts.count() }); + } + + // -- auth + if (p === '/api/auth/challenge' && req.method === 'POST') { + const b = await readBody(req); + const r = auth.makeChallenge(b.address); + return json(res, r.error ? 400 : 200, r); + } + if (p === '/api/auth/verify' && req.method === 'POST') { + const b = await readBody(req); + const r = await auth.verifyChallenge(b.address, b.signature); + if (r.error) return json(res, 400, r); + // bind the visitor's sponsor cookie to this wallet, first touch wins + const sid = Number(parseCookies(req)['iap.sponsor']) || 0; + const attributed = accounts.attributeSponsor(r.address, sid); + accounts.upsert(r.address, { lastSeen: Date.now() }); + return json(res, 200, { ok: true, address: r.address, memberId: r.memberId, sponsorId: attributed }, + { 'Set-Cookie': auth.sessionCookie(r.token) }); + } + if (p === '/api/auth/logout' && req.method === 'POST') { + auth.logout(req); + return json(res, 200, { ok: true }, { 'Set-Cookie': auth.clearCookie() }); + } + if (p === '/api/me' && req.method === 'GET') { + const s = auth.fromRequest(req); + if (!s) return json(res, 200, { signedIn: false }); + const memberId = await auth.refreshMemberId(s); + const acct = accounts.get(s.address) || {}; + const out = { signedIn: true, address: s.address, memberId, sponsorId: acct.sponsorId || 0 }; + if (memberId) { + try { + const mm = await chain.member(memberId); + out.buyerCount = mm.buyerCount; + out.onchainSponsorId = mm.sponsorId; + out.credits = await chain.creditBalance(memberId, 0); + } catch (e) { out.chainReadError = true; } + } + return json(res, 200, out); + } + + // -- admin (Bearer ADMIN_PASSWORD) + if (p === '/api/admin/site' && req.method === 'PATCH') { + if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); + const b = await readBody(req); + const cur = siteConfig(); + fs.writeFileSync(SITE_FILE, JSON.stringify(Object.assign(cur, b), null, 2)); + return json(res, 200, { ok: true, site: siteConfig() }); + } + if (p === '/api/admin/chain' && req.method === 'PATCH') { + if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); + const b = await readBody(req); + const file = path.join(DATA_DIR, 'config.json'); + let cur = {}; try { cur = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) {} + fs.writeFileSync(file, JSON.stringify(Object.assign(cur, b), null, 2)); + chain.reloadConfig(); + return json(res, 200, { ok: true, config: chain.getConfig() }); + } + + // -- pages + if (req.method === 'GET') { + if (p === '/') return sendFile(res, path.join(PUBLIC_DIR, 'index.html')); + if (p === '/ledger') return sendFile(res, path.join(PUBLIC_DIR, 'ledger.html')); + if (p === '/my') return sendFile(res, path.join(PUBLIC_DIR, 'my.html')); + const safe = path.normalize(p).replace(/^([.\\/])+/, ''); + const file = path.join(PUBLIC_DIR, safe); + if (file.startsWith(PUBLIC_DIR) && fs.existsSync(file) && fs.statSync(file).isFile()) return sendFile(res, file); + } + + res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' })); + res.end('Not found'); + } catch (e) { + console.error('request error', req.url, e.message); + try { json(res, 500, { error: 'server error' }); } catch (_) {} + } +}); +server.listen(PORT, () => console.log(`InstantAdPay site on :${PORT} — chain: ${chain.getConfig().chainName}`)); diff --git a/vendor/secp256k1.js b/vendor/secp256k1.js new file mode 100644 index 0000000..33a0843 --- /dev/null +++ b/vendor/secp256k1.js @@ -0,0 +1,1230 @@ +"use strict"; +/*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.utils = exports.schnorr = exports.verify = exports.signSync = exports.sign = exports.getSharedSecret = exports.recoverPublicKey = exports.getPublicKey = exports.Signature = exports.Point = exports.CURVE = void 0; +const nodeCrypto = require("crypto"); +const _0n = BigInt(0); +const _1n = BigInt(1); +const _2n = BigInt(2); +const _3n = BigInt(3); +const _8n = BigInt(8); +const CURVE = Object.freeze({ + a: _0n, + b: BigInt(7), + P: BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), + n: BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'), + h: _1n, + Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'), + Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'), + beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), +}); +exports.CURVE = CURVE; +const divNearest = (a, b) => (a + b / _2n) / b; +const endo = { + beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), + splitScalar(k) { + const { n } = CURVE; + const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15'); + const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3'); + const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8'); + const b2 = a1; + const POW_2_128 = BigInt('0x100000000000000000000000000000000'); + const c1 = divNearest(b2 * k, n); + const c2 = divNearest(-b1 * k, n); + let k1 = mod(k - c1 * a1 - c2 * a2, n); + let k2 = mod(-c1 * b1 - c2 * b2, n); + const k1neg = k1 > POW_2_128; + const k2neg = k2 > POW_2_128; + if (k1neg) + k1 = n - k1; + if (k2neg) + k2 = n - k2; + if (k1 > POW_2_128 || k2 > POW_2_128) { + throw new Error('splitScalarEndo: Endomorphism failed, k=' + k); + } + return { k1neg, k1, k2neg, k2 }; + }, +}; +const fieldLen = 32; +const groupLen = 32; +const hashLen = 32; +const compressedLen = fieldLen + 1; +const uncompressedLen = 2 * fieldLen + 1; +function weierstrass(x) { + const { a, b } = CURVE; + const x2 = mod(x * x); + const x3 = mod(x2 * x); + return mod(x3 + a * x + b); +} +const USE_ENDOMORPHISM = CURVE.a === _0n; +class ShaError extends Error { + constructor(message) { + super(message); + } +} +function assertJacPoint(other) { + if (!(other instanceof JacobianPoint)) + throw new TypeError('JacobianPoint expected'); +} +class JacobianPoint { + constructor(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + static fromAffine(p) { + if (!(p instanceof Point)) { + throw new TypeError('JacobianPoint#fromAffine: expected Point'); + } + if (p.equals(Point.ZERO)) + return JacobianPoint.ZERO; + return new JacobianPoint(p.x, p.y, _1n); + } + static toAffineBatch(points) { + const toInv = invertBatch(points.map((p) => p.z)); + return points.map((p, i) => p.toAffine(toInv[i])); + } + static normalizeZ(points) { + return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine); + } + equals(other) { + assertJacPoint(other); + const { x: X1, y: Y1, z: Z1 } = this; + const { x: X2, y: Y2, z: Z2 } = other; + const Z1Z1 = mod(Z1 * Z1); + const Z2Z2 = mod(Z2 * Z2); + const U1 = mod(X1 * Z2Z2); + const U2 = mod(X2 * Z1Z1); + const S1 = mod(mod(Y1 * Z2) * Z2Z2); + const S2 = mod(mod(Y2 * Z1) * Z1Z1); + return U1 === U2 && S1 === S2; + } + negate() { + return new JacobianPoint(this.x, mod(-this.y), this.z); + } + double() { + const { x: X1, y: Y1, z: Z1 } = this; + const A = mod(X1 * X1); + const B = mod(Y1 * Y1); + const C = mod(B * B); + const x1b = X1 + B; + const D = mod(_2n * (mod(x1b * x1b) - A - C)); + const E = mod(_3n * A); + const F = mod(E * E); + const X3 = mod(F - _2n * D); + const Y3 = mod(E * (D - X3) - _8n * C); + const Z3 = mod(_2n * Y1 * Z1); + return new JacobianPoint(X3, Y3, Z3); + } + add(other) { + assertJacPoint(other); + const { x: X1, y: Y1, z: Z1 } = this; + const { x: X2, y: Y2, z: Z2 } = other; + if (X2 === _0n || Y2 === _0n) + return this; + if (X1 === _0n || Y1 === _0n) + return other; + const Z1Z1 = mod(Z1 * Z1); + const Z2Z2 = mod(Z2 * Z2); + const U1 = mod(X1 * Z2Z2); + const U2 = mod(X2 * Z1Z1); + const S1 = mod(mod(Y1 * Z2) * Z2Z2); + const S2 = mod(mod(Y2 * Z1) * Z1Z1); + const H = mod(U2 - U1); + const r = mod(S2 - S1); + if (H === _0n) { + if (r === _0n) { + return this.double(); + } + else { + return JacobianPoint.ZERO; + } + } + const HH = mod(H * H); + const HHH = mod(H * HH); + const V = mod(U1 * HH); + const X3 = mod(r * r - HHH - _2n * V); + const Y3 = mod(r * (V - X3) - S1 * HHH); + const Z3 = mod(Z1 * Z2 * H); + return new JacobianPoint(X3, Y3, Z3); + } + subtract(other) { + return this.add(other.negate()); + } + multiplyUnsafe(scalar) { + const P0 = JacobianPoint.ZERO; + if (typeof scalar === 'bigint' && scalar === _0n) + return P0; + let n = normalizeScalar(scalar); + if (n === _1n) + return this; + if (!USE_ENDOMORPHISM) { + let p = P0; + let d = this; + while (n > _0n) { + if (n & _1n) + p = p.add(d); + d = d.double(); + n >>= _1n; + } + return p; + } + let { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); + let k1p = P0; + let k2p = P0; + let d = this; + while (k1 > _0n || k2 > _0n) { + if (k1 & _1n) + k1p = k1p.add(d); + if (k2 & _1n) + k2p = k2p.add(d); + d = d.double(); + k1 >>= _1n; + k2 >>= _1n; + } + if (k1neg) + k1p = k1p.negate(); + if (k2neg) + k2p = k2p.negate(); + k2p = new JacobianPoint(mod(k2p.x * endo.beta), k2p.y, k2p.z); + return k1p.add(k2p); + } + precomputeWindow(W) { + const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1; + const points = []; + let p = this; + let base = p; + for (let window = 0; window < windows; window++) { + base = p; + points.push(base); + for (let i = 1; i < 2 ** (W - 1); i++) { + base = base.add(p); + points.push(base); + } + p = base.double(); + } + return points; + } + wNAF(n, affinePoint) { + if (!affinePoint && this.equals(JacobianPoint.BASE)) + affinePoint = Point.BASE; + const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1; + if (256 % W) { + throw new Error('Point#wNAF: Invalid precomputation window, must be power of 2'); + } + let precomputes = affinePoint && pointPrecomputes.get(affinePoint); + if (!precomputes) { + precomputes = this.precomputeWindow(W); + if (affinePoint && W !== 1) { + precomputes = JacobianPoint.normalizeZ(precomputes); + pointPrecomputes.set(affinePoint, precomputes); + } + } + let p = JacobianPoint.ZERO; + let f = JacobianPoint.BASE; + const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W); + const windowSize = 2 ** (W - 1); + const mask = BigInt(2 ** W - 1); + const maxNumber = 2 ** W; + const shiftBy = BigInt(W); + for (let window = 0; window < windows; window++) { + const offset = window * windowSize; + let wbits = Number(n & mask); + n >>= shiftBy; + if (wbits > windowSize) { + wbits -= maxNumber; + n += _1n; + } + const offset1 = offset; + const offset2 = offset + Math.abs(wbits) - 1; + const cond1 = window % 2 !== 0; + const cond2 = wbits < 0; + if (wbits === 0) { + f = f.add(constTimeNegate(cond1, precomputes[offset1])); + } + else { + p = p.add(constTimeNegate(cond2, precomputes[offset2])); + } + } + return { p, f }; + } + multiply(scalar, affinePoint) { + let n = normalizeScalar(scalar); + let point; + let fake; + if (USE_ENDOMORPHISM) { + const { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); + let { p: k1p, f: f1p } = this.wNAF(k1, affinePoint); + let { p: k2p, f: f2p } = this.wNAF(k2, affinePoint); + k1p = constTimeNegate(k1neg, k1p); + k2p = constTimeNegate(k2neg, k2p); + k2p = new JacobianPoint(mod(k2p.x * endo.beta), k2p.y, k2p.z); + point = k1p.add(k2p); + fake = f1p.add(f2p); + } + else { + const { p, f } = this.wNAF(n, affinePoint); + point = p; + fake = f; + } + return JacobianPoint.normalizeZ([point, fake])[0]; + } + toAffine(invZ) { + const { x, y, z } = this; + const is0 = this.equals(JacobianPoint.ZERO); + if (invZ == null) + invZ = is0 ? _8n : invert(z); + const iz1 = invZ; + const iz2 = mod(iz1 * iz1); + const iz3 = mod(iz2 * iz1); + const ax = mod(x * iz2); + const ay = mod(y * iz3); + const zz = mod(z * iz1); + if (is0) + return Point.ZERO; + if (zz !== _1n) + throw new Error('invZ was invalid'); + return new Point(ax, ay); + } +} +JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n); +JacobianPoint.ZERO = new JacobianPoint(_0n, _1n, _0n); +function constTimeNegate(condition, item) { + const neg = item.negate(); + return condition ? neg : item; +} +const pointPrecomputes = new WeakMap(); +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + _setWindowSize(windowSize) { + this._WINDOW_SIZE = windowSize; + pointPrecomputes.delete(this); + } + hasEvenY() { + return this.y % _2n === _0n; + } + static fromCompressedHex(bytes) { + const isShort = bytes.length === 32; + const x = bytesToNumber(isShort ? bytes : bytes.subarray(1)); + if (!isValidFieldElement(x)) + throw new Error('Point is not on curve'); + const y2 = weierstrass(x); + let y = sqrtMod(y2); + const isYOdd = (y & _1n) === _1n; + if (isShort) { + if (isYOdd) + y = mod(-y); + } + else { + const isFirstByteOdd = (bytes[0] & 1) === 1; + if (isFirstByteOdd !== isYOdd) + y = mod(-y); + } + const point = new Point(x, y); + point.assertValidity(); + return point; + } + static fromUncompressedHex(bytes) { + const x = bytesToNumber(bytes.subarray(1, fieldLen + 1)); + const y = bytesToNumber(bytes.subarray(fieldLen + 1, fieldLen * 2 + 1)); + const point = new Point(x, y); + point.assertValidity(); + return point; + } + static fromHex(hex) { + const bytes = ensureBytes(hex); + const len = bytes.length; + const header = bytes[0]; + if (len === fieldLen) + return this.fromCompressedHex(bytes); + if (len === compressedLen && (header === 0x02 || header === 0x03)) { + return this.fromCompressedHex(bytes); + } + if (len === uncompressedLen && header === 0x04) + return this.fromUncompressedHex(bytes); + throw new Error(`Point.fromHex: received invalid point. Expected 32-${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes, not ${len}`); + } + static fromPrivateKey(privateKey) { + return Point.BASE.multiply(normalizePrivateKey(privateKey)); + } + static fromSignature(msgHash, signature, recovery) { + const { r, s } = normalizeSignature(signature); + if (![0, 1, 2, 3].includes(recovery)) + throw new Error('Cannot recover: invalid recovery bit'); + const h = truncateHash(ensureBytes(msgHash)); + const { n } = CURVE; + const radj = recovery === 2 || recovery === 3 ? r + n : r; + const rinv = invert(radj, n); + const u1 = mod(-h * rinv, n); + const u2 = mod(s * rinv, n); + const prefix = recovery & 1 ? '03' : '02'; + const R = Point.fromHex(prefix + numTo32bStr(radj)); + const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); + if (!Q) + throw new Error('Cannot recover signature: point at infinify'); + Q.assertValidity(); + return Q; + } + toRawBytes(isCompressed = false) { + return hexToBytes(this.toHex(isCompressed)); + } + toHex(isCompressed = false) { + const x = numTo32bStr(this.x); + if (isCompressed) { + const prefix = this.hasEvenY() ? '02' : '03'; + return `${prefix}${x}`; + } + else { + return `04${x}${numTo32bStr(this.y)}`; + } + } + toHexX() { + return this.toHex(true).slice(2); + } + toRawX() { + return this.toRawBytes(true).slice(1); + } + assertValidity() { + const msg = 'Point is not on elliptic curve'; + const { x, y } = this; + if (!isValidFieldElement(x) || !isValidFieldElement(y)) + throw new Error(msg); + const left = mod(y * y); + const right = weierstrass(x); + if (mod(left - right) !== _0n) + throw new Error(msg); + } + equals(other) { + return this.x === other.x && this.y === other.y; + } + negate() { + return new Point(this.x, mod(-this.y)); + } + double() { + return JacobianPoint.fromAffine(this).double().toAffine(); + } + add(other) { + return JacobianPoint.fromAffine(this).add(JacobianPoint.fromAffine(other)).toAffine(); + } + subtract(other) { + return this.add(other.negate()); + } + multiply(scalar) { + return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine(); + } + multiplyAndAddUnsafe(Q, a, b) { + const P = JacobianPoint.fromAffine(this); + const aP = a === _0n || a === _1n || this !== Point.BASE ? P.multiplyUnsafe(a) : P.multiply(a); + const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b); + const sum = aP.add(bQ); + return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine(); + } +} +exports.Point = Point; +Point.BASE = new Point(CURVE.Gx, CURVE.Gy); +Point.ZERO = new Point(_0n, _0n); +function sliceDER(s) { + return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s; +} +function parseDERInt(data) { + if (data.length < 2 || data[0] !== 0x02) { + throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`); + } + const len = data[1]; + const res = data.subarray(2, len + 2); + if (!len || res.length !== len) { + throw new Error(`Invalid signature integer: wrong length`); + } + if (res[0] === 0x00 && res[1] <= 0x7f) { + throw new Error('Invalid signature integer: trailing length'); + } + return { data: bytesToNumber(res), left: data.subarray(len + 2) }; +} +function parseDERSignature(data) { + if (data.length < 2 || data[0] != 0x30) { + throw new Error(`Invalid signature tag: ${bytesToHex(data)}`); + } + if (data[1] !== data.length - 2) { + throw new Error('Invalid signature: incorrect length'); + } + const { data: r, left: sBytes } = parseDERInt(data.subarray(2)); + const { data: s, left: rBytesLeft } = parseDERInt(sBytes); + if (rBytesLeft.length) { + throw new Error(`Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`); + } + return { r, s }; +} +class Signature { + constructor(r, s) { + this.r = r; + this.s = s; + this.assertValidity(); + } + static fromCompact(hex) { + const arr = hex instanceof Uint8Array; + const name = 'Signature.fromCompact'; + if (typeof hex !== 'string' && !arr) + throw new TypeError(`${name}: Expected string or Uint8Array`); + const str = arr ? bytesToHex(hex) : hex; + if (str.length !== 128) + throw new Error(`${name}: Expected 64-byte hex`); + return new Signature(hexToNumber(str.slice(0, 64)), hexToNumber(str.slice(64, 128))); + } + static fromDER(hex) { + const arr = hex instanceof Uint8Array; + if (typeof hex !== 'string' && !arr) + throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`); + const { r, s } = parseDERSignature(arr ? hex : hexToBytes(hex)); + return new Signature(r, s); + } + static fromHex(hex) { + return this.fromDER(hex); + } + assertValidity() { + const { r, s } = this; + if (!isWithinCurveOrder(r)) + throw new Error('Invalid Signature: r must be 0 < r < n'); + if (!isWithinCurveOrder(s)) + throw new Error('Invalid Signature: s must be 0 < s < n'); + } + hasHighS() { + const HALF = CURVE.n >> _1n; + return this.s > HALF; + } + normalizeS() { + return this.hasHighS() ? new Signature(this.r, mod(-this.s, CURVE.n)) : this; + } + toDERRawBytes() { + return hexToBytes(this.toDERHex()); + } + toDERHex() { + const sHex = sliceDER(numberToHexUnpadded(this.s)); + const rHex = sliceDER(numberToHexUnpadded(this.r)); + const sHexL = sHex.length / 2; + const rHexL = rHex.length / 2; + const sLen = numberToHexUnpadded(sHexL); + const rLen = numberToHexUnpadded(rHexL); + const length = numberToHexUnpadded(rHexL + sHexL + 4); + return `30${length}02${rLen}${rHex}02${sLen}${sHex}`; + } + toRawBytes() { + return this.toDERRawBytes(); + } + toHex() { + return this.toDERHex(); + } + toCompactRawBytes() { + return hexToBytes(this.toCompactHex()); + } + toCompactHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s); + } +} +exports.Signature = Signature; +function concatBytes(...arrays) { + if (!arrays.every((b) => b instanceof Uint8Array)) + throw new Error('Uint8Array list expected'); + if (arrays.length === 1) + return arrays[0]; + const length = arrays.reduce((a, arr) => a + arr.length, 0); + const result = new Uint8Array(length); + for (let i = 0, pad = 0; i < arrays.length; i++) { + const arr = arrays[i]; + result.set(arr, pad); + pad += arr.length; + } + return result; +} +const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0')); +function bytesToHex(uint8a) { + if (!(uint8a instanceof Uint8Array)) + throw new Error('Expected Uint8Array'); + let hex = ''; + for (let i = 0; i < uint8a.length; i++) { + hex += hexes[uint8a[i]]; + } + return hex; +} +const POW_2_256 = BigInt('0x10000000000000000000000000000000000000000000000000000000000000000'); +function numTo32bStr(num) { + if (typeof num !== 'bigint') + throw new Error('Expected bigint'); + if (!(_0n <= num && num < POW_2_256)) + throw new Error('Expected number 0 <= n < 2^256'); + return num.toString(16).padStart(64, '0'); +} +function numTo32b(num) { + const b = hexToBytes(numTo32bStr(num)); + if (b.length !== 32) + throw new Error('Error: expected 32 bytes'); + return b; +} +function numberToHexUnpadded(num) { + const hex = num.toString(16); + return hex.length & 1 ? `0${hex}` : hex; +} +function hexToNumber(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToNumber: expected string, got ' + typeof hex); + } + return BigInt(`0x${hex}`); +} +function hexToBytes(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToBytes: expected string, got ' + typeof hex); + } + if (hex.length % 2) + throw new Error('hexToBytes: received invalid unpadded hex' + hex.length); + const array = new Uint8Array(hex.length / 2); + for (let i = 0; i < array.length; i++) { + const j = i * 2; + const hexByte = hex.slice(j, j + 2); + const byte = Number.parseInt(hexByte, 16); + if (Number.isNaN(byte) || byte < 0) + throw new Error('Invalid byte sequence'); + array[i] = byte; + } + return array; +} +function bytesToNumber(bytes) { + return hexToNumber(bytesToHex(bytes)); +} +function ensureBytes(hex) { + return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex); +} +function normalizeScalar(num) { + if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) + return BigInt(num); + if (typeof num === 'bigint' && isWithinCurveOrder(num)) + return num; + throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n'); +} +function mod(a, b = CURVE.P) { + const result = a % b; + return result >= _0n ? result : b + result; +} +function pow2(x, power) { + const { P } = CURVE; + let res = x; + while (power-- > _0n) { + res *= res; + res %= P; + } + return res; +} +function sqrtMod(x) { + const { P } = CURVE; + const _6n = BigInt(6); + const _11n = BigInt(11); + const _22n = BigInt(22); + const _23n = BigInt(23); + const _44n = BigInt(44); + const _88n = BigInt(88); + const b2 = (x * x * x) % P; + const b3 = (b2 * b2 * x) % P; + const b6 = (pow2(b3, _3n) * b3) % P; + const b9 = (pow2(b6, _3n) * b3) % P; + const b11 = (pow2(b9, _2n) * b2) % P; + const b22 = (pow2(b11, _11n) * b11) % P; + const b44 = (pow2(b22, _22n) * b22) % P; + const b88 = (pow2(b44, _44n) * b44) % P; + const b176 = (pow2(b88, _88n) * b88) % P; + const b220 = (pow2(b176, _44n) * b44) % P; + const b223 = (pow2(b220, _3n) * b3) % P; + const t1 = (pow2(b223, _23n) * b22) % P; + const t2 = (pow2(t1, _6n) * b2) % P; + const rt = pow2(t2, _2n); + const xc = (rt * rt) % P; + if (xc !== x) + throw new Error('Cannot find square root'); + return rt; +} +function invert(number, modulo = CURVE.P) { + if (number === _0n || modulo <= _0n) { + throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`); + } + let a = mod(number, modulo); + let b = modulo; + let x = _0n, y = _1n, u = _1n, v = _0n; + while (a !== _0n) { + const q = b / a; + const r = b % a; + const m = x - u * q; + const n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + const gcd = b; + if (gcd !== _1n) + throw new Error('invert: does not exist'); + return mod(x, modulo); +} +function invertBatch(nums, p = CURVE.P) { + const scratch = new Array(nums.length); + const lastMultiplied = nums.reduce((acc, num, i) => { + if (num === _0n) + return acc; + scratch[i] = acc; + return mod(acc * num, p); + }, _1n); + const inverted = invert(lastMultiplied, p); + nums.reduceRight((acc, num, i) => { + if (num === _0n) + return acc; + scratch[i] = mod(acc * scratch[i], p); + return mod(acc * num, p); + }, inverted); + return scratch; +} +function bits2int_2(bytes) { + const delta = bytes.length * 8 - groupLen * 8; + const num = bytesToNumber(bytes); + return delta > 0 ? num >> BigInt(delta) : num; +} +function truncateHash(hash, truncateOnly = false) { + const h = bits2int_2(hash); + if (truncateOnly) + return h; + const { n } = CURVE; + return h >= n ? h - n : h; +} +let _sha256Sync; +let _hmacSha256Sync; +class HmacDrbg { + constructor(hashLen, qByteLen) { + this.hashLen = hashLen; + this.qByteLen = qByteLen; + if (typeof hashLen !== 'number' || hashLen < 2) + throw new Error('hashLen must be a number'); + if (typeof qByteLen !== 'number' || qByteLen < 2) + throw new Error('qByteLen must be a number'); + this.v = new Uint8Array(hashLen).fill(1); + this.k = new Uint8Array(hashLen).fill(0); + this.counter = 0; + } + hmac(...values) { + return exports.utils.hmacSha256(this.k, ...values); + } + hmacSync(...values) { + return _hmacSha256Sync(this.k, ...values); + } + checkSync() { + if (typeof _hmacSha256Sync !== 'function') + throw new ShaError('hmacSha256Sync needs to be set'); + } + incr() { + if (this.counter >= 1000) + throw new Error('Tried 1,000 k values for sign(), all were invalid'); + this.counter += 1; + } + async reseed(seed = new Uint8Array()) { + this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed); + this.v = await this.hmac(this.v); + if (seed.length === 0) + return; + this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed); + this.v = await this.hmac(this.v); + } + reseedSync(seed = new Uint8Array()) { + this.checkSync(); + this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed); + this.v = this.hmacSync(this.v); + if (seed.length === 0) + return; + this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed); + this.v = this.hmacSync(this.v); + } + async generate() { + this.incr(); + let len = 0; + const out = []; + while (len < this.qByteLen) { + this.v = await this.hmac(this.v); + const sl = this.v.slice(); + out.push(sl); + len += this.v.length; + } + return concatBytes(...out); + } + generateSync() { + this.checkSync(); + this.incr(); + let len = 0; + const out = []; + while (len < this.qByteLen) { + this.v = this.hmacSync(this.v); + const sl = this.v.slice(); + out.push(sl); + len += this.v.length; + } + return concatBytes(...out); + } +} +function isWithinCurveOrder(num) { + return _0n < num && num < CURVE.n; +} +function isValidFieldElement(num) { + return _0n < num && num < CURVE.P; +} +function kmdToSig(kBytes, m, d, lowS = true) { + const { n } = CURVE; + const k = truncateHash(kBytes, true); + if (!isWithinCurveOrder(k)) + return; + const kinv = invert(k, n); + const q = Point.BASE.multiply(k); + const r = mod(q.x, n); + if (r === _0n) + return; + const s = mod(kinv * mod(m + d * r, n), n); + if (s === _0n) + return; + let sig = new Signature(r, s); + let recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n); + if (lowS && sig.hasHighS()) { + sig = sig.normalizeS(); + recovery ^= 1; + } + return { sig, recovery }; +} +function normalizePrivateKey(key) { + let num; + if (typeof key === 'bigint') { + num = key; + } + else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) { + num = BigInt(key); + } + else if (typeof key === 'string') { + if (key.length !== 2 * groupLen) + throw new Error('Expected 32 bytes of private key'); + num = hexToNumber(key); + } + else if (key instanceof Uint8Array) { + if (key.length !== groupLen) + throw new Error('Expected 32 bytes of private key'); + num = bytesToNumber(key); + } + else { + throw new TypeError('Expected valid private key'); + } + if (!isWithinCurveOrder(num)) + throw new Error('Expected private key: 0 < key < n'); + return num; +} +function normalizePublicKey(publicKey) { + if (publicKey instanceof Point) { + publicKey.assertValidity(); + return publicKey; + } + else { + return Point.fromHex(publicKey); + } +} +function normalizeSignature(signature) { + if (signature instanceof Signature) { + signature.assertValidity(); + return signature; + } + try { + return Signature.fromDER(signature); + } + catch (error) { + return Signature.fromCompact(signature); + } +} +function getPublicKey(privateKey, isCompressed = false) { + return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed); +} +exports.getPublicKey = getPublicKey; +function recoverPublicKey(msgHash, signature, recovery, isCompressed = false) { + return Point.fromSignature(msgHash, signature, recovery).toRawBytes(isCompressed); +} +exports.recoverPublicKey = recoverPublicKey; +function isProbPub(item) { + const arr = item instanceof Uint8Array; + const str = typeof item === 'string'; + const len = (arr || str) && item.length; + if (arr) + return len === compressedLen || len === uncompressedLen; + if (str) + return len === compressedLen * 2 || len === uncompressedLen * 2; + if (item instanceof Point) + return true; + return false; +} +function getSharedSecret(privateA, publicB, isCompressed = false) { + if (isProbPub(privateA)) + throw new TypeError('getSharedSecret: first arg must be private key'); + if (!isProbPub(publicB)) + throw new TypeError('getSharedSecret: second arg must be public key'); + const b = normalizePublicKey(publicB); + b.assertValidity(); + return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed); +} +exports.getSharedSecret = getSharedSecret; +function bits2int(bytes) { + const slice = bytes.length > fieldLen ? bytes.slice(0, fieldLen) : bytes; + return bytesToNumber(slice); +} +function bits2octets(bytes) { + const z1 = bits2int(bytes); + const z2 = mod(z1, CURVE.n); + return int2octets(z2 < _0n ? z1 : z2); +} +function int2octets(num) { + return numTo32b(num); +} +function initSigArgs(msgHash, privateKey, extraEntropy) { + if (msgHash == null) + throw new Error(`sign: expected valid message hash, not "${msgHash}"`); + const h1 = ensureBytes(msgHash); + const d = normalizePrivateKey(privateKey); + const seedArgs = [int2octets(d), bits2octets(h1)]; + if (extraEntropy != null) { + if (extraEntropy === true) + extraEntropy = exports.utils.randomBytes(fieldLen); + const e = ensureBytes(extraEntropy); + if (e.length !== fieldLen) + throw new Error(`sign: Expected ${fieldLen} bytes of extra data`); + seedArgs.push(e); + } + const seed = concatBytes(...seedArgs); + const m = bits2int(h1); + return { seed, m, d }; +} +function finalizeSig(recSig, opts) { + const { sig, recovery } = recSig; + const { der, recovered } = Object.assign({ canonical: true, der: true }, opts); + const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes(); + return recovered ? [hashed, recovery] : hashed; +} +async function sign(msgHash, privKey, opts = {}) { + const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy); + const drbg = new HmacDrbg(hashLen, groupLen); + await drbg.reseed(seed); + let sig; + while (!(sig = kmdToSig(await drbg.generate(), m, d, opts.canonical))) + await drbg.reseed(); + return finalizeSig(sig, opts); +} +exports.sign = sign; +function signSync(msgHash, privKey, opts = {}) { + const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy); + const drbg = new HmacDrbg(hashLen, groupLen); + drbg.reseedSync(seed); + let sig; + while (!(sig = kmdToSig(drbg.generateSync(), m, d, opts.canonical))) + drbg.reseedSync(); + return finalizeSig(sig, opts); +} +exports.signSync = signSync; +const vopts = { strict: true }; +function verify(signature, msgHash, publicKey, opts = vopts) { + let sig; + try { + sig = normalizeSignature(signature); + msgHash = ensureBytes(msgHash); + } + catch (error) { + return false; + } + const { r, s } = sig; + if (opts.strict && sig.hasHighS()) + return false; + const h = truncateHash(msgHash); + let P; + try { + P = normalizePublicKey(publicKey); + } + catch (error) { + return false; + } + const { n } = CURVE; + const sinv = invert(s, n); + const u1 = mod(h * sinv, n); + const u2 = mod(r * sinv, n); + const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2); + if (!R) + return false; + const v = mod(R.x, n); + return v === r; +} +exports.verify = verify; +function schnorrChallengeFinalize(ch) { + return mod(bytesToNumber(ch), CURVE.n); +} +class SchnorrSignature { + constructor(r, s) { + this.r = r; + this.s = s; + this.assertValidity(); + } + static fromHex(hex) { + const bytes = ensureBytes(hex); + if (bytes.length !== 64) + throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`); + const r = bytesToNumber(bytes.subarray(0, 32)); + const s = bytesToNumber(bytes.subarray(32, 64)); + return new SchnorrSignature(r, s); + } + assertValidity() { + const { r, s } = this; + if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) + throw new Error('Invalid signature'); + } + toHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s); + } + toRawBytes() { + return hexToBytes(this.toHex()); + } +} +function schnorrGetPublicKey(privateKey) { + return Point.fromPrivateKey(privateKey).toRawX(); +} +class InternalSchnorrSignature { + constructor(message, privateKey, auxRand = exports.utils.randomBytes()) { + if (message == null) + throw new TypeError(`sign: Expected valid message, not "${message}"`); + this.m = ensureBytes(message); + const { x, scalar } = this.getScalar(normalizePrivateKey(privateKey)); + this.px = x; + this.d = scalar; + this.rand = ensureBytes(auxRand); + if (this.rand.length !== 32) + throw new TypeError('sign: Expected 32 bytes of aux randomness'); + } + getScalar(priv) { + const point = Point.fromPrivateKey(priv); + const scalar = point.hasEvenY() ? priv : CURVE.n - priv; + return { point, scalar, x: point.toRawX() }; + } + initNonce(d, t0h) { + return numTo32b(d ^ bytesToNumber(t0h)); + } + finalizeNonce(k0h) { + const k0 = mod(bytesToNumber(k0h), CURVE.n); + if (k0 === _0n) + throw new Error('sign: Creation of signature failed. k is zero'); + const { point: R, x: rx, scalar: k } = this.getScalar(k0); + return { R, rx, k }; + } + finalizeSig(R, k, e, d) { + return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes(); + } + error() { + throw new Error('sign: Invalid signature produced'); + } + async calc() { + const { m, d, px, rand } = this; + const tag = exports.utils.taggedHash; + const t = this.initNonce(d, await tag(TAGS.aux, rand)); + const { R, rx, k } = this.finalizeNonce(await tag(TAGS.nonce, t, px, m)); + const e = schnorrChallengeFinalize(await tag(TAGS.challenge, rx, px, m)); + const sig = this.finalizeSig(R, k, e, d); + if (!(await schnorrVerify(sig, m, px))) + this.error(); + return sig; + } + calcSync() { + const { m, d, px, rand } = this; + const tag = exports.utils.taggedHashSync; + const t = this.initNonce(d, tag(TAGS.aux, rand)); + const { R, rx, k } = this.finalizeNonce(tag(TAGS.nonce, t, px, m)); + const e = schnorrChallengeFinalize(tag(TAGS.challenge, rx, px, m)); + const sig = this.finalizeSig(R, k, e, d); + if (!schnorrVerifySync(sig, m, px)) + this.error(); + return sig; + } +} +async function schnorrSign(msg, privKey, auxRand) { + return new InternalSchnorrSignature(msg, privKey, auxRand).calc(); +} +function schnorrSignSync(msg, privKey, auxRand) { + return new InternalSchnorrSignature(msg, privKey, auxRand).calcSync(); +} +function initSchnorrVerify(signature, message, publicKey) { + const raw = signature instanceof SchnorrSignature; + const sig = raw ? signature : SchnorrSignature.fromHex(signature); + if (raw) + sig.assertValidity(); + return { + ...sig, + m: ensureBytes(message), + P: normalizePublicKey(publicKey), + }; +} +function finalizeSchnorrVerify(r, P, s, e) { + const R = Point.BASE.multiplyAndAddUnsafe(P, normalizePrivateKey(s), mod(-e, CURVE.n)); + if (!R || !R.hasEvenY() || R.x !== r) + return false; + return true; +} +async function schnorrVerify(signature, message, publicKey) { + try { + const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey); + const e = schnorrChallengeFinalize(await exports.utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m)); + return finalizeSchnorrVerify(r, P, s, e); + } + catch (error) { + return false; + } +} +function schnorrVerifySync(signature, message, publicKey) { + try { + const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey); + const e = schnorrChallengeFinalize(exports.utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m)); + return finalizeSchnorrVerify(r, P, s, e); + } + catch (error) { + if (error instanceof ShaError) + throw error; + return false; + } +} +exports.schnorr = { + Signature: SchnorrSignature, + getPublicKey: schnorrGetPublicKey, + sign: schnorrSign, + verify: schnorrVerify, + signSync: schnorrSignSync, + verifySync: schnorrVerifySync, +}; +Point.BASE._setWindowSize(8); +const crypto = { + node: nodeCrypto, + web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined, +}; +const TAGS = { + challenge: 'BIP0340/challenge', + aux: 'BIP0340/aux', + nonce: 'BIP0340/nonce', +}; +const TAGGED_HASH_PREFIXES = {}; +exports.utils = { + bytesToHex, + hexToBytes, + concatBytes, + mod, + invert, + isValidPrivateKey(privateKey) { + try { + normalizePrivateKey(privateKey); + return true; + } + catch (error) { + return false; + } + }, + _bigintTo32Bytes: numTo32b, + _normalizePrivateKey: normalizePrivateKey, + hashToPrivateKey: (hash) => { + hash = ensureBytes(hash); + const minLen = groupLen + 8; + if (hash.length < minLen || hash.length > 1024) { + throw new Error(`Expected valid bytes of private key as per FIPS 186`); + } + const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n; + return numTo32b(num); + }, + randomBytes: (bytesLength = 32) => { + if (crypto.web) { + return crypto.web.getRandomValues(new Uint8Array(bytesLength)); + } + else if (crypto.node) { + const { randomBytes } = crypto.node; + return Uint8Array.from(randomBytes(bytesLength)); + } + else { + throw new Error("The environment doesn't have randomBytes function"); + } + }, + randomPrivateKey: () => exports.utils.hashToPrivateKey(exports.utils.randomBytes(groupLen + 8)), + precompute(windowSize = 8, point = Point.BASE) { + const cached = point === Point.BASE ? point : new Point(point.x, point.y); + cached._setWindowSize(windowSize); + cached.multiply(_3n); + return cached; + }, + sha256: async (...messages) => { + if (crypto.web) { + const buffer = await crypto.web.subtle.digest('SHA-256', concatBytes(...messages)); + return new Uint8Array(buffer); + } + else if (crypto.node) { + const { createHash } = crypto.node; + const hash = createHash('sha256'); + messages.forEach((m) => hash.update(m)); + return Uint8Array.from(hash.digest()); + } + else { + throw new Error("The environment doesn't have sha256 function"); + } + }, + hmacSha256: async (key, ...messages) => { + if (crypto.web) { + const ckey = await crypto.web.subtle.importKey('raw', key, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']); + const message = concatBytes(...messages); + const buffer = await crypto.web.subtle.sign('HMAC', ckey, message); + return new Uint8Array(buffer); + } + else if (crypto.node) { + const { createHmac } = crypto.node; + const hash = createHmac('sha256', key); + messages.forEach((m) => hash.update(m)); + return Uint8Array.from(hash.digest()); + } + else { + throw new Error("The environment doesn't have hmac-sha256 function"); + } + }, + sha256Sync: undefined, + hmacSha256Sync: undefined, + taggedHash: async (tag, ...messages) => { + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === undefined) { + const tagH = await exports.utils.sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0))); + tagP = concatBytes(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return exports.utils.sha256(tagP, ...messages); + }, + taggedHashSync: (tag, ...messages) => { + if (typeof _sha256Sync !== 'function') + throw new ShaError('sha256Sync is undefined, you need to set it'); + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === undefined) { + const tagH = _sha256Sync(Uint8Array.from(tag, (c) => c.charCodeAt(0))); + tagP = concatBytes(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return _sha256Sync(tagP, ...messages); + }, + _JacobianPoint: JacobianPoint, +}; +Object.defineProperties(exports.utils, { + sha256Sync: { + configurable: false, + get() { + return _sha256Sync; + }, + set(val) { + if (!_sha256Sync) + _sha256Sync = val; + }, + }, + hmacSha256Sync: { + configurable: false, + get() { + return _hmacSha256Sync; + }, + set(val) { + if (!_hmacSha256Sync) + _hmacSha256Sync = val; + }, + }, +}); diff --git a/vendor/sha3.js b/vendor/sha3.js new file mode 100644 index 0000000..52c9334 --- /dev/null +++ b/vendor/sha3.js @@ -0,0 +1,662 @@ +/** + * [js-sha3]{@link https://github.com/emn178/js-sha3} + * + * @version 0.9.3 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2015-2023 + * @license MIT + */ +/*jslint bitwise: true */ +(function () { + 'use strict'; + + var INPUT_ERROR = 'input is invalid type'; + var FINALIZE_ERROR = 'finalize already called'; + var WINDOW = typeof window === 'object'; + var root = WINDOW ? window : {}; + if (root.JS_SHA3_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = !root.JS_SHA3_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA3_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA3_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var SHAKE_PADDING = [31, 7936, 2031616, 520093696]; + var CSHAKE_PADDING = [4, 1024, 262144, 67108864]; + var KECCAK_PADDING = [1, 256, 65536, 16777216]; + var PADDING = [6, 1536, 393216, 100663296]; + var SHIFT = [0, 8, 16, 24]; + var RC = [1, 0, 32898, 0, 32906, 2147483648, 2147516416, 2147483648, 32907, 0, 2147483649, + 0, 2147516545, 2147483648, 32777, 2147483648, 138, 0, 136, 0, 2147516425, 0, + 2147483658, 0, 2147516555, 0, 139, 2147483648, 32905, 2147483648, 32771, + 2147483648, 32770, 2147483648, 128, 2147483648, 32778, 0, 2147483658, 2147483648, + 2147516545, 2147483648, 32896, 2147483648, 2147483649, 0, 2147516424, 2147483648]; + var BITS = [224, 256, 384, 512]; + var SHAKE_BITS = [128, 256]; + var OUTPUT_TYPES = ['hex', 'buffer', 'arrayBuffer', 'array', 'digest']; + var CSHAKE_BYTEPAD = { + '128': 168, + '256': 136 + }; + + + var isArray = root.JS_SHA3_NO_NODE_JS || !Array.isArray + ? function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + } + : Array.isArray; + + var isView = (ARRAY_BUFFER && (root.JS_SHA3_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) + ? function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + } + : ArrayBuffer.isView; + + // [message: string, isString: bool] + var formatMessage = function (message) { + var type = typeof message; + if (type === 'string') { + return [message, true]; + } + if (type !== 'object' || message === null) { + throw new Error(INPUT_ERROR); + } + if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + return [new Uint8Array(message), false]; + } + if (!isArray(message) && !isView(message)) { + throw new Error(INPUT_ERROR); + } + return [message, false]; + } + + var empty = function (message) { + return formatMessage(message)[0].length === 0; + }; + + var cloneArray = function (array) { + var newArray = []; + for (var i = 0; i < array.length; ++i) { + newArray[i] = array[i]; + } + return newArray; + } + + var createOutputMethod = function (bits, padding, outputType) { + return function (message) { + return new Keccak(bits, padding, bits).update(message)[outputType](); + }; + }; + + var createShakeOutputMethod = function (bits, padding, outputType) { + return function (message, outputBits) { + return new Keccak(bits, padding, outputBits).update(message)[outputType](); + }; + }; + + var createCshakeOutputMethod = function (bits, padding, outputType) { + return function (message, outputBits, n, s) { + return methods['cshake' + bits].update(message, outputBits, n, s)[outputType](); + }; + }; + + var createKmacOutputMethod = function (bits, padding, outputType) { + return function (key, message, outputBits, s) { + return methods['kmac' + bits].update(key, message, outputBits, s)[outputType](); + }; + }; + + var createOutputMethods = function (method, createMethod, bits, padding) { + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createMethod(bits, padding, type); + } + return method; + }; + + var createMethod = function (bits, padding) { + var method = createOutputMethod(bits, padding, 'hex'); + method.create = function () { + return new Keccak(bits, padding, bits); + }; + method.update = function (message) { + return method.create().update(message); + }; + return createOutputMethods(method, createOutputMethod, bits, padding); + }; + + var createShakeMethod = function (bits, padding) { + var method = createShakeOutputMethod(bits, padding, 'hex'); + method.create = function (outputBits) { + return new Keccak(bits, padding, outputBits); + }; + method.update = function (message, outputBits) { + return method.create(outputBits).update(message); + }; + return createOutputMethods(method, createShakeOutputMethod, bits, padding); + }; + + var createCshakeMethod = function (bits, padding) { + var w = CSHAKE_BYTEPAD[bits]; + var method = createCshakeOutputMethod(bits, padding, 'hex'); + method.create = function (outputBits, n, s) { + if (empty(n) && empty(s)) { + return methods['shake' + bits].create(outputBits); + } else { + return new Keccak(bits, padding, outputBits).bytepad([n, s], w); + } + }; + method.update = function (message, outputBits, n, s) { + return method.create(outputBits, n, s).update(message); + }; + return createOutputMethods(method, createCshakeOutputMethod, bits, padding); + }; + + var createKmacMethod = function (bits, padding) { + var w = CSHAKE_BYTEPAD[bits]; + var method = createKmacOutputMethod(bits, padding, 'hex'); + method.create = function (key, outputBits, s) { + return new Kmac(bits, padding, outputBits).bytepad(['KMAC', s], w).bytepad([key], w); + }; + method.update = function (key, message, outputBits, s) { + return method.create(key, outputBits, s).update(message); + }; + return createOutputMethods(method, createKmacOutputMethod, bits, padding); + }; + + var algorithms = [ + { name: 'keccak', padding: KECCAK_PADDING, bits: BITS, createMethod: createMethod }, + { name: 'sha3', padding: PADDING, bits: BITS, createMethod: createMethod }, + { name: 'shake', padding: SHAKE_PADDING, bits: SHAKE_BITS, createMethod: createShakeMethod }, + { name: 'cshake', padding: CSHAKE_PADDING, bits: SHAKE_BITS, createMethod: createCshakeMethod }, + { name: 'kmac', padding: CSHAKE_PADDING, bits: SHAKE_BITS, createMethod: createKmacMethod } + ]; + + var methods = {}, methodNames = []; + + for (var i = 0; i < algorithms.length; ++i) { + var algorithm = algorithms[i]; + var bits = algorithm.bits; + for (var j = 0; j < bits.length; ++j) { + var methodName = algorithm.name + '_' + bits[j]; + methodNames.push(methodName); + methods[methodName] = algorithm.createMethod(bits[j], algorithm.padding); + if (algorithm.name !== 'sha3') { + var newMethodName = algorithm.name + bits[j]; + methodNames.push(newMethodName); + methods[newMethodName] = methods[methodName]; + } + } + } + + function Keccak(bits, padding, outputBits) { + this.blocks = []; + this.s = []; + this.padding = padding; + this.outputBits = outputBits; + this.reset = true; + this.finalized = false; + this.block = 0; + this.start = 0; + this.blockCount = (1600 - (bits << 1)) >> 5; + this.byteCount = this.blockCount << 2; + this.outputBlocks = outputBits >> 5; + this.extraBytes = (outputBits & 31) >> 3; + + for (var i = 0; i < 50; ++i) { + this.s[i] = 0; + } + } + + Keccak.prototype.update = function (message) { + if (this.finalized) { + throw new Error(FINALIZE_ERROR); + } + var result = formatMessage(message); + message = result[0]; + var isString = result[1]; + var blocks = this.blocks, byteCount = this.byteCount, length = message.length, + blockCount = this.blockCount, index = 0, s = this.s, i, code; + + while (index < length) { + if (this.reset) { + this.reset = false; + blocks[0] = this.block; + for (i = 1; i < blockCount + 1; ++i) { + blocks[i] = 0; + } + } + if (isString) { + for (i = this.start; index < length && i < byteCount; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } else { + for (i = this.start; index < length && i < byteCount; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } + this.lastByteIndex = i; + if (i >= byteCount) { + this.start = i - byteCount; + this.block = blocks[blockCount]; + for (i = 0; i < blockCount; ++i) { + s[i] ^= blocks[i]; + } + f(s); + this.reset = true; + } else { + this.start = i; + } + } + return this; + }; + + Keccak.prototype.encode = function (x, right) { + var o = x & 255, n = 1; + var bytes = [o]; + x = x >> 8; + o = x & 255; + while (o > 0) { + bytes.unshift(o); + x = x >> 8; + o = x & 255; + ++n; + } + if (right) { + bytes.push(n); + } else { + bytes.unshift(n); + } + this.update(bytes); + return bytes.length; + }; + + Keccak.prototype.encodeString = function (str) { + var result = formatMessage(str); + str = result[0]; + var isString = result[1]; + var bytes = 0, length = str.length; + if (isString) { + for (var i = 0; i < str.length; ++i) { + var code = str.charCodeAt(i); + if (code < 0x80) { + bytes += 1; + } else if (code < 0x800) { + bytes += 2; + } else if (code < 0xd800 || code >= 0xe000) { + bytes += 3; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (str.charCodeAt(++i) & 0x3ff)); + bytes += 4; + } + } + } else { + bytes = length; + } + bytes += this.encode(bytes * 8); + this.update(str); + return bytes; + }; + + Keccak.prototype.bytepad = function (strs, w) { + var bytes = this.encode(w); + for (var i = 0; i < strs.length; ++i) { + bytes += this.encodeString(strs[i]); + } + var paddingBytes = (w - bytes % w) % w; + var zeros = []; + zeros.length = paddingBytes; + this.update(zeros); + return this; + }; + + Keccak.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex, blockCount = this.blockCount, s = this.s; + blocks[i >> 2] |= this.padding[i & 3]; + if (this.lastByteIndex === this.byteCount) { + blocks[0] = blocks[blockCount]; + for (i = 1; i < blockCount + 1; ++i) { + blocks[i] = 0; + } + } + blocks[blockCount - 1] |= 0x80000000; + for (i = 0; i < blockCount; ++i) { + s[i] ^= blocks[i]; + } + f(s); + }; + + Keccak.prototype.toString = Keccak.prototype.hex = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var hex = '', block; + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + block = s[i]; + hex += HEX_CHARS[(block >> 4) & 0x0F] + HEX_CHARS[block & 0x0F] + + HEX_CHARS[(block >> 12) & 0x0F] + HEX_CHARS[(block >> 8) & 0x0F] + + HEX_CHARS[(block >> 20) & 0x0F] + HEX_CHARS[(block >> 16) & 0x0F] + + HEX_CHARS[(block >> 28) & 0x0F] + HEX_CHARS[(block >> 24) & 0x0F]; + } + if (j % blockCount === 0) { + s = cloneArray(s); + f(s); + i = 0; + } + } + if (extraBytes) { + block = s[i]; + hex += HEX_CHARS[(block >> 4) & 0x0F] + HEX_CHARS[block & 0x0F]; + if (extraBytes > 1) { + hex += HEX_CHARS[(block >> 12) & 0x0F] + HEX_CHARS[(block >> 8) & 0x0F]; + } + if (extraBytes > 2) { + hex += HEX_CHARS[(block >> 20) & 0x0F] + HEX_CHARS[(block >> 16) & 0x0F]; + } + } + return hex; + }; + + Keccak.prototype.arrayBuffer = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var bytes = this.outputBits >> 3; + var buffer; + if (extraBytes) { + buffer = new ArrayBuffer((outputBlocks + 1) << 2); + } else { + buffer = new ArrayBuffer(bytes); + } + var array = new Uint32Array(buffer); + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + array[j] = s[i]; + } + if (j % blockCount === 0) { + s = cloneArray(s); + f(s); + } + } + if (extraBytes) { + array[j] = s[i]; + buffer = buffer.slice(0, bytes); + } + return buffer; + }; + + Keccak.prototype.buffer = Keccak.prototype.arrayBuffer; + + Keccak.prototype.digest = Keccak.prototype.array = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var array = [], offset, block; + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + offset = j << 2; + block = s[i]; + array[offset] = block & 0xFF; + array[offset + 1] = (block >> 8) & 0xFF; + array[offset + 2] = (block >> 16) & 0xFF; + array[offset + 3] = (block >> 24) & 0xFF; + } + if (j % blockCount === 0) { + s = cloneArray(s); + f(s); + } + } + if (extraBytes) { + offset = j << 2; + block = s[i]; + array[offset] = block & 0xFF; + if (extraBytes > 1) { + array[offset + 1] = (block >> 8) & 0xFF; + } + if (extraBytes > 2) { + array[offset + 2] = (block >> 16) & 0xFF; + } + } + return array; + }; + + function Kmac(bits, padding, outputBits) { + Keccak.call(this, bits, padding, outputBits); + } + + Kmac.prototype = new Keccak(); + + Kmac.prototype.finalize = function () { + this.encode(this.outputBits, true); + return Keccak.prototype.finalize.call(this); + }; + + var f = function (s) { + var h, l, n, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, + b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, + b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31, b32, b33, + b34, b35, b36, b37, b38, b39, b40, b41, b42, b43, b44, b45, b46, b47, b48, b49; + for (n = 0; n < 48; n += 2) { + c0 = s[0] ^ s[10] ^ s[20] ^ s[30] ^ s[40]; + c1 = s[1] ^ s[11] ^ s[21] ^ s[31] ^ s[41]; + c2 = s[2] ^ s[12] ^ s[22] ^ s[32] ^ s[42]; + c3 = s[3] ^ s[13] ^ s[23] ^ s[33] ^ s[43]; + c4 = s[4] ^ s[14] ^ s[24] ^ s[34] ^ s[44]; + c5 = s[5] ^ s[15] ^ s[25] ^ s[35] ^ s[45]; + c6 = s[6] ^ s[16] ^ s[26] ^ s[36] ^ s[46]; + c7 = s[7] ^ s[17] ^ s[27] ^ s[37] ^ s[47]; + c8 = s[8] ^ s[18] ^ s[28] ^ s[38] ^ s[48]; + c9 = s[9] ^ s[19] ^ s[29] ^ s[39] ^ s[49]; + + h = c8 ^ ((c2 << 1) | (c3 >>> 31)); + l = c9 ^ ((c3 << 1) | (c2 >>> 31)); + s[0] ^= h; + s[1] ^= l; + s[10] ^= h; + s[11] ^= l; + s[20] ^= h; + s[21] ^= l; + s[30] ^= h; + s[31] ^= l; + s[40] ^= h; + s[41] ^= l; + h = c0 ^ ((c4 << 1) | (c5 >>> 31)); + l = c1 ^ ((c5 << 1) | (c4 >>> 31)); + s[2] ^= h; + s[3] ^= l; + s[12] ^= h; + s[13] ^= l; + s[22] ^= h; + s[23] ^= l; + s[32] ^= h; + s[33] ^= l; + s[42] ^= h; + s[43] ^= l; + h = c2 ^ ((c6 << 1) | (c7 >>> 31)); + l = c3 ^ ((c7 << 1) | (c6 >>> 31)); + s[4] ^= h; + s[5] ^= l; + s[14] ^= h; + s[15] ^= l; + s[24] ^= h; + s[25] ^= l; + s[34] ^= h; + s[35] ^= l; + s[44] ^= h; + s[45] ^= l; + h = c4 ^ ((c8 << 1) | (c9 >>> 31)); + l = c5 ^ ((c9 << 1) | (c8 >>> 31)); + s[6] ^= h; + s[7] ^= l; + s[16] ^= h; + s[17] ^= l; + s[26] ^= h; + s[27] ^= l; + s[36] ^= h; + s[37] ^= l; + s[46] ^= h; + s[47] ^= l; + h = c6 ^ ((c0 << 1) | (c1 >>> 31)); + l = c7 ^ ((c1 << 1) | (c0 >>> 31)); + s[8] ^= h; + s[9] ^= l; + s[18] ^= h; + s[19] ^= l; + s[28] ^= h; + s[29] ^= l; + s[38] ^= h; + s[39] ^= l; + s[48] ^= h; + s[49] ^= l; + + b0 = s[0]; + b1 = s[1]; + b32 = (s[11] << 4) | (s[10] >>> 28); + b33 = (s[10] << 4) | (s[11] >>> 28); + b14 = (s[20] << 3) | (s[21] >>> 29); + b15 = (s[21] << 3) | (s[20] >>> 29); + b46 = (s[31] << 9) | (s[30] >>> 23); + b47 = (s[30] << 9) | (s[31] >>> 23); + b28 = (s[40] << 18) | (s[41] >>> 14); + b29 = (s[41] << 18) | (s[40] >>> 14); + b20 = (s[2] << 1) | (s[3] >>> 31); + b21 = (s[3] << 1) | (s[2] >>> 31); + b2 = (s[13] << 12) | (s[12] >>> 20); + b3 = (s[12] << 12) | (s[13] >>> 20); + b34 = (s[22] << 10) | (s[23] >>> 22); + b35 = (s[23] << 10) | (s[22] >>> 22); + b16 = (s[33] << 13) | (s[32] >>> 19); + b17 = (s[32] << 13) | (s[33] >>> 19); + b48 = (s[42] << 2) | (s[43] >>> 30); + b49 = (s[43] << 2) | (s[42] >>> 30); + b40 = (s[5] << 30) | (s[4] >>> 2); + b41 = (s[4] << 30) | (s[5] >>> 2); + b22 = (s[14] << 6) | (s[15] >>> 26); + b23 = (s[15] << 6) | (s[14] >>> 26); + b4 = (s[25] << 11) | (s[24] >>> 21); + b5 = (s[24] << 11) | (s[25] >>> 21); + b36 = (s[34] << 15) | (s[35] >>> 17); + b37 = (s[35] << 15) | (s[34] >>> 17); + b18 = (s[45] << 29) | (s[44] >>> 3); + b19 = (s[44] << 29) | (s[45] >>> 3); + b10 = (s[6] << 28) | (s[7] >>> 4); + b11 = (s[7] << 28) | (s[6] >>> 4); + b42 = (s[17] << 23) | (s[16] >>> 9); + b43 = (s[16] << 23) | (s[17] >>> 9); + b24 = (s[26] << 25) | (s[27] >>> 7); + b25 = (s[27] << 25) | (s[26] >>> 7); + b6 = (s[36] << 21) | (s[37] >>> 11); + b7 = (s[37] << 21) | (s[36] >>> 11); + b38 = (s[47] << 24) | (s[46] >>> 8); + b39 = (s[46] << 24) | (s[47] >>> 8); + b30 = (s[8] << 27) | (s[9] >>> 5); + b31 = (s[9] << 27) | (s[8] >>> 5); + b12 = (s[18] << 20) | (s[19] >>> 12); + b13 = (s[19] << 20) | (s[18] >>> 12); + b44 = (s[29] << 7) | (s[28] >>> 25); + b45 = (s[28] << 7) | (s[29] >>> 25); + b26 = (s[38] << 8) | (s[39] >>> 24); + b27 = (s[39] << 8) | (s[38] >>> 24); + b8 = (s[48] << 14) | (s[49] >>> 18); + b9 = (s[49] << 14) | (s[48] >>> 18); + + s[0] = b0 ^ (~b2 & b4); + s[1] = b1 ^ (~b3 & b5); + s[10] = b10 ^ (~b12 & b14); + s[11] = b11 ^ (~b13 & b15); + s[20] = b20 ^ (~b22 & b24); + s[21] = b21 ^ (~b23 & b25); + s[30] = b30 ^ (~b32 & b34); + s[31] = b31 ^ (~b33 & b35); + s[40] = b40 ^ (~b42 & b44); + s[41] = b41 ^ (~b43 & b45); + s[2] = b2 ^ (~b4 & b6); + s[3] = b3 ^ (~b5 & b7); + s[12] = b12 ^ (~b14 & b16); + s[13] = b13 ^ (~b15 & b17); + s[22] = b22 ^ (~b24 & b26); + s[23] = b23 ^ (~b25 & b27); + s[32] = b32 ^ (~b34 & b36); + s[33] = b33 ^ (~b35 & b37); + s[42] = b42 ^ (~b44 & b46); + s[43] = b43 ^ (~b45 & b47); + s[4] = b4 ^ (~b6 & b8); + s[5] = b5 ^ (~b7 & b9); + s[14] = b14 ^ (~b16 & b18); + s[15] = b15 ^ (~b17 & b19); + s[24] = b24 ^ (~b26 & b28); + s[25] = b25 ^ (~b27 & b29); + s[34] = b34 ^ (~b36 & b38); + s[35] = b35 ^ (~b37 & b39); + s[44] = b44 ^ (~b46 & b48); + s[45] = b45 ^ (~b47 & b49); + s[6] = b6 ^ (~b8 & b0); + s[7] = b7 ^ (~b9 & b1); + s[16] = b16 ^ (~b18 & b10); + s[17] = b17 ^ (~b19 & b11); + s[26] = b26 ^ (~b28 & b20); + s[27] = b27 ^ (~b29 & b21); + s[36] = b36 ^ (~b38 & b30); + s[37] = b37 ^ (~b39 & b31); + s[46] = b46 ^ (~b48 & b40); + s[47] = b47 ^ (~b49 & b41); + s[8] = b8 ^ (~b0 & b2); + s[9] = b9 ^ (~b1 & b3); + s[18] = b18 ^ (~b10 & b12); + s[19] = b19 ^ (~b11 & b13); + s[28] = b28 ^ (~b20 & b22); + s[29] = b29 ^ (~b21 & b23); + s[38] = b38 ^ (~b30 & b32); + s[39] = b39 ^ (~b31 & b33); + s[48] = b48 ^ (~b40 & b42); + s[49] = b49 ^ (~b41 & b43); + + s[0] ^= RC[n]; + s[1] ^= RC[n + 1]; + } + }; + + if (COMMON_JS) { + module.exports = methods; + } else { + for (i = 0; i < methodNames.length; ++i) { + root[methodNames[i]] = methods[methodNames[i]]; + } + if (AMD) { + define(function () { + return methods; + }); + } + } +})();