Fix: relay tx receipts through the server (CSP blocked direct RPC polls)

After a wallet sent a transaction, waitTx polled the public RPC straight
from the browser, which connect-src 'self' blocks (Firefox NetworkError,
reported by Marty on Activate; the activation itself landed on-chain).
New /api/tx/<hash> relays eth_getTransactionReceipt via the server's RPC
pool, so the browser only ever talks to us and the mainnet flip needs no
CSP changes. Assets v=20260905a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-05 05:40:56 -05:00
parent f848112e2d
commit 013a88b7d0
6 changed files with 29 additions and 22 deletions
+3 -4
View File
@@ -49,11 +49,10 @@ window.IAPWallet = (function () {
return eth().request({ method: 'eth_sendTransaction', params: [tx] }); return eth().request({ method: 'eth_sendTransaction', params: [tx] });
} }
async function waitTx(hash) { async function waitTx(hash) {
const c = await IAP.getConfig(); // poll our own server (CSP-friendly); it relays the receipt from the RPC
for (let i = 0; i < 60; i++) { for (let i = 0; i < 60; i++) {
const r = await (await fetch(c.rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' }, const r = await (await fetch('/api/tx/' + hash)).json();
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionReceipt', params: [hash] }) })).json(); if (r.found) return { status: r.status, blockNumber: r.blockNumber };
if (r.result) return r.result;
await new Promise(res => setTimeout(res, 2500)); await new Promise(res => setTimeout(res, 2500));
} }
throw new Error('Timed out waiting for the transaction. Check the explorer.'); throw new Error('Timed out waiting for the transaction. Check the explorer.');
+4 -4
View File
@@ -5,7 +5,7 @@
<title>The contract | InstantAdPay</title> <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."> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260904r"> <link rel="stylesheet" href="/assets/site.css?v=20260905a">
</head> </head>
<body> <body>
<div class="wrap"> <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> <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> </footer>
</div> </div>
<script src="/assets/common.js?v=20260904r"></script> <script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/contract.js?v=20260904r"></script> <script src="/assets/contract.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260904r"></script> <script src="/assets/chat.js?v=20260905a"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -5,7 +5,7 @@
<title>InstantAdPay: advertise and earn, locked in code</title> <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."> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260904r"> <link rel="stylesheet" href="/assets/site.css?v=20260905a">
</head> </head>
<body> <body>
@@ -398,9 +398,9 @@
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260904r"></script> <script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/wallet.js?v=20260904r"></script> <script src="/assets/wallet.js?v=20260905a"></script>
<script src="/assets/home.js?v=20260904r"></script> <script src="/assets/home.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260904r"></script> <script src="/assets/chat.js?v=20260905a"></script>
</body> </body>
</html> </html>
+4 -4
View File
@@ -5,7 +5,7 @@
<title>Live ledger | InstantAdPay</title> <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."> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260904r"> <link rel="stylesheet" href="/assets/site.css?v=20260905a">
</head> </head>
<body> <body>
<div class="wrap"> <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> <div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
</footer> </footer>
</div> </div>
<script src="/assets/common.js?v=20260904r"></script> <script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/ledger.js?v=20260904r"></script> <script src="/assets/ledger.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260904r"></script> <script src="/assets/chat.js?v=20260905a"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Member area | InstantAdPay</title> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260904r"> <link rel="stylesheet" href="/assets/site.css?v=20260905a">
</head> </head>
<body class="bo-body"> <body class="bo-body">
@@ -208,9 +208,9 @@
</div> </div>
</div> </div>
<script src="/assets/common.js?v=20260904r"></script> <script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/wallet.js?v=20260904r"></script> <script src="/assets/wallet.js?v=20260905a"></script>
<script src="/assets/my.js?v=20260904r"></script> <script src="/assets/my.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260904r"></script> <script src="/assets/chat.js?v=20260905a"></script>
</body> </body>
</html> </html>
+8
View File
@@ -173,6 +173,14 @@ const server = http.createServer(async (req, res) => {
req.on('close', () => feedClients.delete(res)); req.on('close', () => feedClients.delete(res));
return; return;
} }
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')
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 });
} catch (e) { return json(res, 200, { found: false, rpcError: true }); }
}
if (p === '/api/sponsor' && req.method === 'GET') { if (p === '/api/sponsor' && req.method === 'GET') {
const tok = parseCookies(req)['iap.sponsor'] || ''; const tok = parseCookies(req)['iap.sponsor'] || '';
const sponsorId = await resolveSponsorToken(tok); const sponsorId = await resolveSponsorToken(tok);