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:
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
data/
|
||||||
|
*.log
|
||||||
@@ -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"]
|
||||||
@@ -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
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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:
|
||||||
@@ -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"}
|
||||||
|
}
|
||||||
@@ -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, $ };
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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) {}
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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)}}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
})();
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 & 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>
|
||||||
@@ -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}`));
|
||||||
Vendored
+1230
File diff suppressed because it is too large
Load Diff
Vendored
+662
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user