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 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 12:25:34 -05:00
commit f053c1befa
20 changed files with 3116 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
data/
*.log
+8
View File
@@ -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"]
+32
View File
@@ -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/<id>` 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/`.
+49
View File
@@ -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 };
+120
View File
@@ -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 };
+216
View File
@@ -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 };
+15
View File
@@ -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:
+9
View File
@@ -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"}
}
+87
View File
@@ -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 = '<div class="wrap">'
+ '<a class="logo" href="/">Instant<b>AdPay</b></a>'
+ '<span class="links">'
+ '<a href="/" data-p="home">How it works</a>'
+ '<a href="/ledger" data-p="ledger">Live ledger</a>'
+ '<a href="/my" data-p="my">My account</a>'
+ '</span><span id="navWallet" class="muted">…</span></div>';
document.body.prepend(nav);
if (c.rehearsal) {
const b = document.createElement('div');
b.className = 'rehearsal';
b.innerHTML = '<b>Testnet rehearsal</b> — 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 ? '<span class="badge">member #' + me.memberId + '</span> ' : '')
+ '<span class="mono">' + me.address.slice(0, 6) + '…' + me.address.slice(-4) + '</span>';
} else {
el.innerHTML = '<a href="/my">Sign in</a>';
}
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 = '<span>' + describeEvent(ev, c) + '</span>'
+ '<span class="tx"><a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + ev.tx + '">verify ↗</a></span>';
return div;
}
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, $ };
})();
+44
View File
@@ -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 = '<td><b>' + (NAMES[p.id] || 'Package ' + p.id) + '</b></td>'
+ '<td class="num">' + IAP.fmtUsd(p.priceCents) + '</td>'
+ '<td class="num">' + p.creditAmount.toLocaleString() + '</td>'
+ '<td class="num mono">' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL' : 'paused') + '</td>'
+ '<td><button class="btn small" data-id="' + p.id + '" data-cost="' + (p.costWei || '') + '"'
+ (p.costWei ? '' : ' disabled') + '>Buy</button></td>';
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();
})();
+28
View File
@@ -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 = '<div class="row muted">No activity yet — the first purchase will appear here the moment it lands.</div>';
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) {}
};
})();
+67
View File
@@ -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 <b>member #' + me.memberId + '</b><br>wallet <span class="mono">'
+ me.address.slice(0, 8) + '…' + me.address.slice(-6) + '</span>'
+ (me.onchainSponsorId ? '<br>sponsored by member #' + me.onchainSponsorId : '<br>no sponsor (house line)');
$('creditLine').textContent = (me.credits || 0).toLocaleString();
const bc = me.buyerCount || 0;
$('qualLine').innerHTML = '<b>' + bc + '</b> qualifying buyer(s) referred<br>'
+ (bc >= 5 ? '<span class="badge">Level 3 unlocked — full three-level earnings</span>'
: bc >= 2 ? '<span class="badge">Level 2 unlocked</span> · ' + (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 <span class="mono">' + me.address.slice(0, 8) + '…' + me.address.slice(-6)
+ '</span><br>free member — not on-chain yet'
+ (me.sponsorId ? '<br>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();
})();
+82
View File
@@ -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)}}
}
+75
View File
@@ -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 };
})();
+88
View File
@@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>InstantAdPay — advertising that pays instantly, on-chain</title>
<meta name="description" content="Members-only advertising with immutable on-chain settlement. Every package purchase pays the people who built the audience — in the same transaction, verifiable by anyone.">
<link rel="stylesheet" href="/assets/site.css">
</head>
<body>
<div class="wrap">
<section class="hero">
<h1>Advertising that pays the people who build it — <em>instantly, on-chain</em>.</h1>
<p class="lead">Free to join. Real ad inventory. And when anyone buys an ad package,
the payment splits to their sponsors <b>in the same blockchain transaction</b>
no balances, no withdrawal requests, no company holding your money. Ever.</p>
<p>
<a class="btn" href="/my">Join free</a>
<a class="btn sec" href="/ledger">Watch payments land live</a>
</p>
<p class="small muted" id="sponsorLine" hidden></p>
</section>
<h2>Where every dollar goes — enforced by code, not promises</h2>
<div class="card">
<div class="split">
<div class="s50">50%<br>direct sponsor</div>
<div class="s20">20%<br>level 2</div>
<div class="s10">10%<br>level 3</div>
<div class="sa">20%<br>platform</div>
</div>
<p class="muted">These percentages are <b>constants in an immutable smart contract</b> — 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 <a href="/ledger">live ledger</a> with a verify link to the blockchain.</p>
</div>
<div class="grid c3">
<div class="card"><h3>🆓 Join free, earn from day one</h3>
<p class="muted small">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.</p></div>
<div class="card"><h3>📣 Real advertising, on real audiences</h3>
<p class="muted small">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.</p></div>
<div class="card"><h3>🔎 Qualification by performance</h3>
<p class="muted small">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.</p></div>
</div>
<h2>The ad packages</h2>
<div class="card">
<div class="tablewrap">
<table id="ladder">
<thead><tr><th>Package</th><th class="num">Price</th><th class="num">Ad credits</th><th class="num">Cost right now</th><th></th></tr></thead>
<tbody><tr><td colspan="5" class="muted">Loading live prices from the contract…</td></tr></tbody>
</table>
</div>
<p class="small muted">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.</p>
</div>
<h2>Why this is different</h2>
<div class="grid c2">
<div class="card"><h3>No trust required</h3>
<p class="muted small">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.</p></div>
<div class="card"><h3>Honest about what it is</h3>
<p class="muted small">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.</p></div>
</div>
<footer>
<div>InstantAdPay · every payment verifiable on-chain · <a href="/ledger">live ledger</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">view the contract ↗</a></div>
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible — never spend what you cannot afford.</div>
</footer>
</div>
<script src="/assets/common.js"></script>
<script src="/assets/wallet.js"></script>
<script src="/assets/home.js"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Live ledger — InstantAdPay</title>
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
<link rel="stylesheet" href="/assets/site.css">
</head>
<body>
<div class="wrap">
<section class="hero" style="padding-bottom:20px">
<h1>The <em>live ledger</em></h1>
<p class="lead">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.</p>
<p><span class="badge" id="liveBadge">connecting…</span>
<span class="small muted" id="statLine"></span></p>
</section>
<div class="card" style="padding:0">
<div class="feed" id="feed"><div class="row muted">Loading recent history…</div></div>
</div>
<footer>
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
</footer>
</div>
<script src="/assets/common.js"></script>
<script src="/assets/ledger.js"></script>
</body>
</html>
+62
View File
@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>My account — InstantAdPay</title>
<link rel="stylesheet" href="/assets/site.css">
</head>
<body>
<div class="wrap">
<section class="hero" style="padding-bottom:16px">
<h1>My <em>account</em></h1>
<p class="lead" id="introLead">One free wallet signature signs you in — it cannot move funds or approve anything.
No email, no password.</p>
</section>
<div class="card" id="signinCard">
<h3>Sign in with your wallet</h3>
<p class="muted small">New here? The same button creates your free membership. Your earnings always go
straight to this wallet — we never hold them.</p>
<button class="btn" id="signinBtn">Connect &amp; sign in</button>
</div>
<div id="memberArea" hidden>
<div class="grid c3">
<div class="card"><h3>Your position</h3>
<p id="posLine" class="muted small"></p></div>
<div class="card"><h3>Ad credits</h3>
<p class="mono" style="font-size:26px;margin:0" id="creditLine"></p>
<p class="muted small">1 credit = 1¢ of delivery across the network. Recorded on-chain; only your campaigns can spend them.</p></div>
<div class="card"><h3>Qualification</h3>
<p id="qualLine" class="muted small"></p></div>
</div>
<div class="card" id="activateCard" hidden>
<h3>Activate your payout wallet — free</h3>
<p class="muted small">One free transaction registers this wallet on-chain so commissions can reach it.
Buying any package does this automatically, so you can also just start with a package below.</p>
<button class="btn sec" id="activateBtn">Activate payout wallet</button>
</div>
<div class="card">
<h3>Your invite link</h3>
<p class="muted small">Share it anywhere. Everyone who joins through it becomes part of your line —
you earn 50% of every ad package they ever buy, level 2 and 3 of their teams' buys as you qualify.</p>
<p class="mono" id="inviteLine">Sign in to get your link.</p>
<button class="btn small sec" id="copyInvite" hidden>Copy link</button>
</div>
<div class="card">
<h3>Buy ad packages</h3>
<p class="muted small">The full ladder with live pricing is on the <a href="/">home page</a>
purchases from this wallet automatically credit this account.</p>
</div>
</div>
<footer><div>InstantAdPay · <a href="/ledger">live ledger</a></div></footer>
</div>
<script src="/assets/common.js"></script>
<script src="/assets/wallet.js"></script>
<script src="/assets/my.js"></script>
</body>
</html>
+210
View File
@@ -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/<sponsorId> — 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}`));
+1230
View File
File diff suppressed because it is too large Load Diff
+662
View File
@@ -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;
});
}
}
})();