Built-in transaction viewer: every ledger row verifiable at /tx/<hash>, explorer link when configured
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -229,4 +229,4 @@ function init(opts) {
|
||||
}
|
||||
|
||||
module.exports = { init, getConfig, reloadConfig, memberIdByAccount, memberCount, member,
|
||||
product, productCount, quoteWei, creditBalance, catalog, recentEvents, totals, rpc };
|
||||
product, productCount, quoteWei, creditBalance, catalog, recentEvents, totals, rpc, decodeLog };
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ FACTS:
|
||||
- Every purchase is split by an immutable smart contract in the same transaction: 50% direct sponsor, 20% level 2, 10% level 3, 20% platform. No withdrawals exist; money lands in members' own wallets instantly.
|
||||
- Qualification: level 1 open to all; 2 buyers of $20+ unlock level 2; 5 unlock level 3. Unqualified shares pass up the sponsor line, checking up to 25 positions, else the platform receives them. Qualification cannot be bought and never expires.
|
||||
- Every member gets a share link immediately (site code, /join/<code>); the contract locks a buyer to their sponsor at the buyer's FIRST purchase, so members should switch on payouts before their referrals buy.
|
||||
- Transparency: live ledger at /ledger streams every chain event with verify links; plain-language contract review at /contract; the verified source is public.
|
||||
- Transparency: live ledger at /ledger streams every chain event, and EVERY transaction row carries a clickable "verify" link — to the public block explorer when the chain has one, or to the built-in transaction viewer at /tx/<hash> (block, time, from/to, value, gas, decoded events, read live from the node). Plain-language contract review at /contract; the verified source is public.
|
||||
- The contract cannot be paused, upgraded, or drained. The operator can only manage the ad catalog ($1-$500 bounds, 24h price timelock) and rotate keys.
|
||||
|
||||
RULES:
|
||||
|
||||
@@ -92,7 +92,7 @@ window.IAP = (function () {
|
||||
div.innerHTML = '<span>' + describeEvent(ev, c) + '</span>'
|
||||
+ (c.explorer
|
||||
? '<span class="tx"><a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + ev.tx + '">verify ↗</a></span>'
|
||||
: '<span class="tx mono">' + ev.tx.slice(0, 12) + '…</span>');
|
||||
: '<span class="tx"><a href="/tx/' + ev.tx + '">verify ↗</a></span>'); // built-in viewer when the chain has no public explorer
|
||||
return div;
|
||||
}
|
||||
// Render one served ad into #<elId>. Silent if no inventory.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
(async function () {
|
||||
await IAP.renderNav('ledger');
|
||||
const c = await IAP.getConfig();
|
||||
IAP.$('contractLink').href = c.explorer + '/address/' + c.contract;
|
||||
IAP.$('contractLink').href = c.explorer ? c.explorer + '/address/' + c.contract : '/contract';
|
||||
const feed = IAP.$('feed');
|
||||
|
||||
const { events } = await (await fetch('/api/feed?n=150')).json();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Transaction viewer: renders one tx from /api/tx/<hash> (the server-side RPC
|
||||
// relay, so the browser never talks to the chain directly and CSP stays 'self').
|
||||
// This is the "verify it yourself" page for chains without a public explorer;
|
||||
// when the config carries an explorer URL, feed links point there instead.
|
||||
(async function () {
|
||||
await IAP.renderNav('ledger');
|
||||
const c = await IAP.getConfig();
|
||||
const hash = location.pathname.split('/').pop();
|
||||
IAP.$('txHash').textContent = hash;
|
||||
IAP.$('txChain').textContent = 'on ' + (c.chainName || 'the settlement chain');
|
||||
let r = null;
|
||||
try { r = await (await fetch('/api/tx/' + hash)).json(); } catch (e) {}
|
||||
const stat = IAP.$('txStatus');
|
||||
if (!r || !r.found) {
|
||||
stat.textContent = 'not found';
|
||||
stat.className = 'badge amber';
|
||||
IAP.$('txHint').hidden = false;
|
||||
return;
|
||||
}
|
||||
const ok = r.status === '0x1';
|
||||
stat.textContent = ok ? '✓ confirmed' : '✗ reverted';
|
||||
stat.className = 'badge' + (ok ? '' : ' amber');
|
||||
const hx = v => { try { return parseInt(v, 16); } catch (e) { return 0; } };
|
||||
const rows = [
|
||||
['Block', '#' + hx(r.blockNumber).toLocaleString()],
|
||||
['Time', r.ts ? new Date(hx(r.ts) * 1000).toLocaleString() : 'pending'],
|
||||
['From', r.from || ''],
|
||||
['To (contract)', r.to || ''],
|
||||
['Value sent', IAP.fmtPol(r.valueWei || '0') + ' POL'],
|
||||
['Gas used', hx(r.gasUsed).toLocaleString()]
|
||||
];
|
||||
const tb = IAP.$('txTable');
|
||||
tb.hidden = false;
|
||||
tb.querySelector('tbody').innerHTML = rows.map(x =>
|
||||
'<tr><td class="muted small" style="white-space:nowrap">' + x[0] + '</td>'
|
||||
+ '<td class="mono small" style="overflow-wrap:anywhere">' + String(x[1]).replace(/[&<>]/g, '') + '</td></tr>').join('');
|
||||
if (r.events && r.events.length) {
|
||||
IAP.$('evCard').hidden = false;
|
||||
const feed = IAP.$('evFeed');
|
||||
for (const ev of r.events) feed.appendChild(IAP.feedRow(ev, c));
|
||||
}
|
||||
})();
|
||||
@@ -5,7 +5,7 @@
|
||||
<title>The contract | InstantAdPay</title>
|
||||
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905j">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905r">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
@@ -129,8 +129,8 @@
|
||||
<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?v=20260905j"></script>
|
||||
<script src="/assets/contract.js?v=20260905j"></script>
|
||||
<script src="/assets/chat.js?v=20260905j"></script>
|
||||
<script src="/assets/common.js?v=20260905r"></script>
|
||||
<script src="/assets/contract.js?v=20260905r"></script>
|
||||
<script src="/assets/chat.js?v=20260905r"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@
|
||||
<title>InstantAdPay: advertise and earn, locked in code</title>
|
||||
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905j">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905r">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -409,9 +409,9 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/assets/common.js?v=20260905j"></script>
|
||||
<script src="/assets/wallet.js?v=20260905j"></script>
|
||||
<script src="/assets/home.js?v=20260905j"></script>
|
||||
<script src="/assets/chat.js?v=20260905j"></script>
|
||||
<script src="/assets/common.js?v=20260905r"></script>
|
||||
<script src="/assets/wallet.js?v=20260905r"></script>
|
||||
<script src="/assets/home.js?v=20260905r"></script>
|
||||
<script src="/assets/chat.js?v=20260905r"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@
|
||||
<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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905j">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905r">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
@@ -25,8 +25,8 @@
|
||||
<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?v=20260905j"></script>
|
||||
<script src="/assets/ledger.js?v=20260905j"></script>
|
||||
<script src="/assets/chat.js?v=20260905j"></script>
|
||||
<script src="/assets/common.js?v=20260905r"></script>
|
||||
<script src="/assets/ledger.js?v=20260905r"></script>
|
||||
<script src="/assets/chat.js?v=20260905r"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Member area | InstantAdPay</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905q">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905r">
|
||||
</head>
|
||||
<body class="bo-body">
|
||||
|
||||
@@ -399,9 +399,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/common.js?v=20260905q"></script>
|
||||
<script src="/assets/wallet.js?v=20260905q"></script>
|
||||
<script src="/assets/my.js?v=20260905q"></script>
|
||||
<script src="/assets/chat.js?v=20260905q"></script>
|
||||
<script src="/assets/common.js?v=20260905r"></script>
|
||||
<script src="/assets/wallet.js?v=20260905r"></script>
|
||||
<script src="/assets/my.js?v=20260905r"></script>
|
||||
<script src="/assets/chat.js?v=20260905r"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Transaction | InstantAdPay</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905r">
|
||||
</head>
|
||||
<body>
|
||||
<div id="nav"></div>
|
||||
<section>
|
||||
<div class="wrap" style="max-width:860px">
|
||||
<div class="sectionhead" style="text-align:left">
|
||||
<h2>One transaction, <em>fully public</em></h2>
|
||||
<p>Pulled straight from the chain this site settles on. Every field below is read from the node,
|
||||
not from our database.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<p><span class="badge" id="txStatus">looking it up…</span>
|
||||
<span class="small muted" id="txChain"></span></p>
|
||||
<p class="mono small" id="txHash" style="overflow-wrap:anywhere"></p>
|
||||
<div class="tablewrap"><table id="txTable" hidden>
|
||||
<tbody></tbody>
|
||||
</table></div>
|
||||
<p class="small muted" id="txHint" hidden>Nothing under that hash on this chain yet. If the
|
||||
transaction just happened, give the next block a moment and refresh.</p>
|
||||
</div>
|
||||
<div class="card" id="evCard" hidden>
|
||||
<h3>What this transaction did</h3>
|
||||
<p class="muted small">The contract events inside it, decoded to plain language.</p>
|
||||
<div class="feed" id="evFeed"></div>
|
||||
</div>
|
||||
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
|
||||
</div>
|
||||
</section>
|
||||
<script src="/assets/common.js?v=20260905r"></script>
|
||||
<script src="/assets/tx.js?v=20260905r"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Viewing ad — InstantAdPay</title>
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905q">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905r">
|
||||
<style>
|
||||
html,body{height:100%;margin:0;overflow:hidden}
|
||||
.vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
|
||||
@@ -43,6 +43,6 @@
|
||||
</div>
|
||||
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
|
||||
</div>
|
||||
<script src="/assets/view.js?v=20260905q"></script>
|
||||
<script src="/assets/view.js?v=20260905r"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -262,10 +262,26 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
m = /^\/api\/tx\/(0x[0-9a-fA-F]{64})$/.exec(p);
|
||||
if (m && req.method === 'GET') {
|
||||
// receipt relay so the browser never talks to the RPC directly (CSP stays 'self')
|
||||
// tx relay so the browser never talks to the RPC directly (CSP stays 'self').
|
||||
// Serves both the wallet's waitTx poll (found/status) and the built-in
|
||||
// /tx/<hash> viewer (full details + decoded events).
|
||||
try {
|
||||
const r = await chain.rpc('eth_getTransactionReceipt', [m[1]]);
|
||||
return json(res, 200, r ? { found: true, status: r.status, blockNumber: r.blockNumber } : { found: false });
|
||||
if (!r) return json(res, 200, { found: false });
|
||||
const out = { found: true, status: r.status, blockNumber: r.blockNumber, gasUsed: r.gasUsed };
|
||||
try {
|
||||
const t = await chain.rpc('eth_getTransactionByHash', [m[1]]);
|
||||
if (t) { out.from = t.from; out.to = t.to; out.valueWei = BigInt(t.value || '0x0').toString(); }
|
||||
} catch (e) {}
|
||||
try {
|
||||
const blk = await chain.rpc('eth_getBlockByNumber', [r.blockNumber, false]);
|
||||
if (blk) out.ts = blk.timestamp;
|
||||
} catch (e) {}
|
||||
try {
|
||||
out.events = await attachNames((r.logs || []).map(chain.decodeLog).filter(Boolean)
|
||||
.map(ev => Object.assign(ev, { tx: m[1] })));
|
||||
} catch (e) { out.events = []; }
|
||||
return json(res, 200, out);
|
||||
} catch (e) { return json(res, 200, { found: false, rpcError: true }); }
|
||||
}
|
||||
if (p === '/api/sponsor' && req.method === 'GET') {
|
||||
@@ -681,6 +697,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
|
||||
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
|
||||
if (m) return sendFile(res, path.join(UPLOADS_DIR, m[1]));
|
||||
if (/^\/tx\/0x[0-9a-fA-F]{64}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'tx.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);
|
||||
|
||||
Reference in New Issue
Block a user