openzoo 0.50.15 → 0.50.17

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/lib/x402.js +55 -6
  2. package/package.json +1 -1
package/lib/x402.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  ComputeBudgetProgram,
4
4
  Connection,
5
5
  PublicKey,
6
+ SystemProgram,
6
7
  Transaction,
7
8
  TransactionInstruction,
8
9
  } from '@solana/web3.js';
@@ -271,7 +272,7 @@ export const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqX
271
272
  * them in one window under 10 workers). The spec now puts uniqueness in the
272
273
  * memo, so the CU limit is a plain constant and the nonce is the memo body.
273
274
  */
274
- export function buildPayment({ accept, decimals, programId, recentBlockhash, keypair }) {
275
+ export function buildPayment({ accept, decimals, programId, recentBlockhash, keypair, nonce = null }) {
275
276
  const mint = new PublicKey(accept.asset);
276
277
  const payTo = new PublicKey(accept.payTo);
277
278
  const feePayer = new PublicKey(accept.extra.feePayer);
@@ -281,6 +282,25 @@ export function buildPayment({ accept, decimals, programId, recentBlockhash, key
281
282
  const dest = getAssociatedTokenAddressSync(mint, payTo, true, programId);
282
283
 
283
284
  const tx = new Transaction({ feePayer, recentBlockhash });
285
+ // A DURABLE NONCE, WHEN THE FACILITATOR OFFERS ONE — and `nonceAdvance` MUST
286
+ // be the first instruction, which is why this precedes the compute budget.
287
+ //
288
+ // `recentBlockhash` is inside the signed message and expires after ~150 slots
289
+ // (~60s). x402 signs here and settles over there, so a slow uplink or a large
290
+ // body kills a valid payment and nobody downstream can refresh the hash.
291
+ // MEASURED 2026-08-28: every settle from one client failed for forty minutes
292
+ // with `blockhash … valid=false`, funded fee payer, 526 bytes — nothing wrong
293
+ // with it but age. With a nonce in that slot the transaction never expires.
294
+ //
295
+ // The authority is a dedicated facilitator key, NOT the fee payer:
296
+ // scheme_exact_svm forbids the fee payer from appearing in any instruction's
297
+ // accounts, and an advance names its authority as a signer.
298
+ if (nonce?.account && nonce?.authority) {
299
+ tx.add(SystemProgram.nonceAdvance({
300
+ noncePubkey: new PublicKey(nonce.account),
301
+ authorizedPubkey: new PublicKey(nonce.authority),
302
+ }));
303
+ }
284
304
  tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }));
285
305
  tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1 }));
286
306
  tx.add(createTransferCheckedInstruction(
@@ -294,8 +314,8 @@ export function buildPayment({ accept, decimals, programId, recentBlockhash, key
294
314
  programId: MEMO_PROGRAM_ID,
295
315
  data: Buffer.from(accept.extra?.memo || crypto.randomBytes(16).toString('hex'), 'utf8'),
296
316
  }));
297
- if (tx.instructions.length < 3 || tx.instructions.length > 6) {
298
- throw new Error(`x402 svm payment has ${tx.instructions.length} instructions, outside the required 3..6`);
317
+ if (tx.instructions.length < 3 || tx.instructions.length > 7) {
318
+ throw new Error(`x402 svm payment has ${tx.instructions.length} instructions, outside the required 3..7`);
299
319
  }
300
320
  // THE FEE PAYER MUST NOT APPEAR IN ANY INSTRUCTION'S ACCOUNTS. It is the
301
321
  // facilitator's gas signer; letting it in means their signature can be made
@@ -345,9 +365,38 @@ export function buildPayment({ accept, decimals, programId, recentBlockhash, key
345
365
  */
346
366
  export async function buildPaymentOnline(connection, keypair, accept) {
347
367
  const { programId, decimals } = await getMintInfo(connection, accept.asset);
348
- const commitment = process.env.OPENZOO_BLOCKHASH_COMMITMENT || 'finalized';
349
- const { blockhash } = await connection.getLatestBlockhash(commitment);
350
- return buildPayment({ accept, decimals, programId, recentBlockhash: blockhash, keypair });
368
+
369
+ // PREFER A DURABLE NONCE. The facilitator leases one from a pool; with it in
370
+ // the blockhash slot the signed payment never expires, which removes the
371
+ // whole class of "valid=false, no logs" failures. Best-effort by design: no
372
+ // nonce, a slow facilitator, or an exhausted pool all fall through to a
373
+ // recent blockhash, because a 60-second window beats no payment at all.
374
+ let nonce = null;
375
+ const facilitator = accept?.extra?.facilitator;
376
+ // ON BY DEFAULT — PROVEN ON CHAIN 2026-08-28.
377
+ //
378
+ // It shipped opt-in first because the fallback below covers a missing, slow
379
+ // or exhausted nonce but NOT a malformed nonce transaction: a wrong
380
+ // instruction order or account meta only surfaces at settle time, after the
381
+ // fallback is long past. That risk is now retired by a real settlement —
382
+ // tx 3pJbRH2u…mMWAZ3, instruction 0 `advanceNonceAccount`, err None.
383
+ // OPENZOO_NONCE=0 is the kill switch back to recent blockhashes.
384
+ if (facilitator && process.env.OPENZOO_NONCE !== '0') {
385
+ try {
386
+ const r = await fetch(`${facilitator}/nonce?network=${encodeURIComponent(accept.network)}`, {
387
+ signal: AbortSignal.timeout(4000),
388
+ });
389
+ if (r.ok) {
390
+ const j = await r.json();
391
+ if (j?.account && j?.nonce && j?.authority) nonce = j;
392
+ }
393
+ } catch { /* fall through to a blockhash */ }
394
+ }
395
+
396
+ const recentBlockhash = nonce
397
+ ? nonce.nonce
398
+ : (await connection.getLatestBlockhash(process.env.OPENZOO_BLOCKHASH_COMMITMENT || 'finalized')).blockhash;
399
+ return buildPayment({ accept, decimals, programId, recentBlockhash, keypair, nonce });
351
400
  }
352
401
 
353
402
  /** Token balance of our ATA for a mint. Returns { raw: bigint, ui: number|null }. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.15",
3
+ "version": "0.50.17",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",