mtok-verify 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/onchain.js +20 -16
  2. package/package.json +1 -1
package/onchain.js CHANGED
@@ -98,6 +98,7 @@ export function createOnchainVerifier({ rpcUrl, rpcUrls, usdcAddress, expectedCh
98
98
  // a few times before giving up. Bounded so a genuinely-missing tx still fails fast.
99
99
  receiptRetries = 3, receiptRetryMs = 700, sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)),
100
100
  nowMs = () => Date.now(), // injectable clock for the optional draw-age guard (verifyDrawPaid maxPaidAgeMs).
101
+ rpcTimeoutMs = 5000,
101
102
  } = {}) {
102
103
  // Accept one URL, a comma-separated list, or an array — rpc() rotates across them
103
104
  // so a single rate-limited or down RPC can't strand a settlement verification (#108).
@@ -110,6 +111,7 @@ export function createOnchainVerifier({ rpcUrl, rpcUrls, usdcAddress, expectedCh
110
111
  method: 'POST',
111
112
  headers: { 'content-type': 'application/json' },
112
113
  body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
114
+ signal: AbortSignal.timeout(rpcTimeoutMs),
113
115
  });
114
116
  if (!res.ok) throw apiError(502, 'rpc_error', `chain RPC returned ${res.status}`);
115
117
  const body = await res.json();
@@ -145,12 +147,14 @@ export function createOnchainVerifier({ rpcUrl, rpcUrls, usdcAddress, expectedCh
145
147
  async function assertChain() {
146
148
  if (!chainPinned) return true; // no (valid) expected chain configured -> today's behavior
147
149
  if (chainOkUrls.size) return true; // already found at least one on-chain url (cached)
150
+ let timeout;
148
151
  for (const url of urls) {
149
152
  try {
150
153
  const hex = await rpcOn(url, 'eth_chainId', []);
151
154
  if (typeof hex === 'string' && parseInt(hex, 16) === expChain) chainOkUrls.add(url);
152
- } catch { /* transient / wrong-chain: leave it out, don't cache a failure */ }
155
+ } catch (error) { if (error.name === 'TimeoutError') timeout = error; }
153
156
  }
157
+ if (!chainOkUrls.size && timeout) throw timeout;
154
158
  return chainOkUrls.size > 0;
155
159
  }
156
160
 
@@ -270,30 +274,30 @@ export function createOnchainVerifier({ rpcUrl, rpcUrls, usdcAddress, expectedCh
270
274
  if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: 'request_hash_mismatch' };
271
275
  if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: 'amount_too_low' };
272
276
 
273
- // #580 draw-age guard: refuse a payment older than maxPaidAgeMs so a stale replay
274
- // can't buy a fresh serve after the relay's local redemption record lapsed or was
275
- // lost. This is DEFENSE-IN-DEPTH on top of the relay's served.has() one-serve guard,
276
- // so it only REJECTS when it actually reads an age past the bound. If the paid block
277
- // can't be read (missing blockNumber, or a transient RPC failure even after retries),
278
- // it SKIPS the freshness check rather than reject: the payment was already fully
279
- // verified above, and rejecting would make the SDK auto-DISPUTE a good draw over an RPC
280
- // blip, burning an honest buyer's USDC and busting an innocent seller's reputation.
281
- // Opt-in: unset => no extra RPC, exact current behavior.
277
+ // Once redemption records expire, verified payment age is the replay boundary.
278
+ // An unreadable timestamp is retryable uncertainty, never permission to spend.
282
279
  const maxAge = Number(maxPaidAgeMs);
280
+ let paidAtMs = null;
283
281
  if (Number.isFinite(maxAge) && maxAge > 0) {
284
- const bn = matchedLog?.blockNumber;
285
- let paidAtMs = null;
282
+ const bn = matchedLog?.blockNumber ?? got.receipt.blockNumber;
286
283
  if (bn != null) {
287
- const blockTag = (typeof bn === 'string' && bn.startsWith('0x')) ? bn : '0x' + BigInt(bn).toString(16);
288
284
  for (let i = 0; i <= receiptRetries; i++) {
289
285
  try {
286
+ const blockTag = '0x' + BigInt(bn).toString(16);
290
287
  const block = await rpc('eth_getBlockByNumber', [blockTag, false]);
291
- if (block?.timestamp != null) { paidAtMs = Number(BigInt(block.timestamp)) * 1000; break; }
288
+ if (block?.timestamp != null) {
289
+ const timestamp = Number(BigInt(block.timestamp)) * 1000;
290
+ if (Number.isSafeInteger(timestamp) && timestamp >= 0 && timestamp <= nowMs() + 60_000) {
291
+ paidAtMs = timestamp;
292
+ break;
293
+ }
294
+ }
292
295
  } catch { /* transient: fall through to retry */ }
293
296
  if (i < receiptRetries) await sleepImpl(receiptRetryMs);
294
297
  }
295
298
  }
296
- if (paidAtMs != null && nowMs() - paidAtMs > maxAge) return { ok: false, reason: 'payment_too_old' };
299
+ if (paidAtMs == null) return { ok: false, reason: 'payment_age_unavailable' };
300
+ if (nowMs() - paidAtMs > maxAge) return { ok: false, reason: 'payment_too_old' };
297
301
  }
298
302
 
299
303
  // #(fable review): bind each leg's `from` to the pay-time buyer (event.buyer,
@@ -313,7 +317,7 @@ export function createOnchainVerifier({ rpcUrl, rpcUrls, usdcAddress, expectedCh
313
317
  if (!feeTransfer.ok) return { ok: false, reason: 'fee_transfer_' + feeTransfer.reason };
314
318
  }
315
319
 
316
- return { ok: true, event, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
320
+ return { ok: true, event, paidAtMs, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
317
321
  },
318
322
  };
319
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-verify",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The on-chain verify core for mtok.market sellers: decode + verify a MtokDripLedger DrawPaid receipt (and its USDC transfer legs) against a paid draw, with the canonical DrawPaid topic set. Dependency-free, runs anywhere fetch runs (node, a CF Worker). The SSOT the platform + reference relay + house seller all share.",
5
5
  "type": "module",
6
6
  "exports": {