@wafermapxyz/cli 0.4.6 → 0.4.8

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.8 (5 Nov 2026)
4
+
5
+ **Two more payment networks: Arc and Soneium.**
6
+
7
+ - `--network arc` pays in USDC on Arc (eip155:5042, the ERC-20 face of Arc's native USDC at `0x3600…0000`, 6 decimals) and `--network soneium` in bridged USDC.e on Soneium (eip155:1868). Both are EIP-3009 transfers, gasless for you; Wafermap settles them through the Elsa facilitator. The EIP-712 domains (`"USDC", "2"` and `"Bridged USDC (Soneium)", "2"`) were checked against each token's `DOMAIN_SEPARATOR()`.
8
+ - `balance`, `withdraw` and `history --verify` cover both. RPCs: `WAFERMAP_RPC_ARC`, `WAFERMAP_RPC_SONEIUM` (public ones by default). On Arc the gas token is USDC itself; on Soneium it is ETH.
9
+ - The MCP server's `network` option takes `arc` and `soneium`.
10
+ - LLM resources can stream: pass `accept: text/event-stream` to `x402fetch` and read `delta`, `receipt` and `done` events (https://wafermap.xyz/docs#streaming). Payment works as before and settles once the model finished.
11
+
12
+ ## 0.4.7 (4 Nov 2026)
13
+
14
+ **`history --verify` checks Solana payments.** Until now they showed `unknown`.
15
+
16
+ - Each Solana payment now logs what it can be found by later: the wallet's own signature (the second one in the transaction; the first, which names it on explorers, is the facilitator's), the memo, the blockhash and the slot it was read at.
17
+ - `wafermap history --verify` reads a Solana RPC, the public one by default (`WAFERMAP_RPC_SOLANA` for another), never Wafermap's, and takes no number from the server that was paid:
18
+ - The payment is anchored to the block its blockhash names: the block at the logged slot must carry exactly that hash, or the check stops with `error`. The payment can land only after that block, and only until the chain is 150 blocks past it.
19
+ - `used`: a transaction carrying our signature landed and succeeded (the one the server named, or one found in the wallet's USDC account history).
20
+ - `unused`: the finalized chain is past the blockhash's last valid height (read first), and the history, read from a node at least that far along and back to the blockhash's block, has no transaction with our signature. Such a payment can never execute. A transaction that landed with our signature and failed moved nothing: also `unused`.
21
+ - `pending`: not found, and still able to land.
22
+ - `error`: anything the RPC could not establish for sure (a block or history it cannot return, a time-out). Never `unused` on a doubtful read.
23
+ - Solana lines logged before 0.4.7 have no signature: they are `used` when the server's transaction shows this wallet signing and its USDC going down by exactly the signed amount (and no other line was given the same transaction), otherwise `unknown`.
24
+ - Reviewed adversarially twice before release. The fixes that came out of it: the expiry is read before the search; the cut-off is a block checked on-chain, not the local clock or a slot from Wafermap's RPC; a fresh blockhash the finalized chain has not reached yet reads `pending`, not `unused`.
25
+ - Solana RPC reads have a 15-second limit and back off on rate limits.
26
+ - SDK: `checkSolanaPayment()` and `SOLANA_PUBLIC_RPC` are exported; `checkPayment()` routes Solana lines to it. `SolanaPayer` gains an optional `blockhashInfo()`; `blockhash()` still returns a string.
27
+
3
28
  ## 0.4.6 (3 Nov 2026)
4
29
 
5
30
  **`history --verify` tells an executed payment from a cancelled one.** Thank you @GBLIN_Protocol for the catch.
package/README.md CHANGED
@@ -6,7 +6,7 @@ The CLI, SDK and MCP server behind [Wafermap](https://wafermap.xyz): an x402 wal
6
6
  - **discover** third-party x402 services in the open x402 index and **pay** them
7
7
  - **swap** tokens through the KyberSwap aggregator
8
8
 
9
- Every payment is checked against a per-payment cap and a daily cap before anything is signed. Swaps have their own per-swap cap. Payments settle in USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or Polygon and are gasless; swaps need a little ETH for gas.
9
+ Every payment is checked against a per-payment cap and a daily cap before anything is signed. Swaps have their own per-swap cap. Payments settle in USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium and are gasless; swaps need a little ETH for gas.
10
10
 
11
11
  ## Install
12
12
 
@@ -35,7 +35,7 @@ wafermap swap 0.01 ETH USDG
35
35
  wafermap history
36
36
  ```
37
37
 
38
- Add `--yes` to skip the confirmation prompt (caps still apply), `--network base`, `arbitrum`, `polygon` or `solana` to pay in USDC on that network (swaps run on `robinhood` or `base`). Set `WAFERMAP_PASSWORD` to avoid the password prompt in scripts.
38
+ Add `--yes` to skip the confirmation prompt (caps still apply), `--network base`, `arbitrum`, `polygon`, `arc`, `soneium` or `solana` to pay in USDC on that network (swaps run on `robinhood` or `base`). Set `WAFERMAP_PASSWORD` to avoid the password prompt in scripts.
39
39
 
40
40
  ## MCP server
41
41
 
@@ -82,7 +82,7 @@ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `le
82
82
 
83
83
  ## $WAFERMAP holder discount
84
84
 
85
- A wallet that holds at least 100,000 $WAFERMAP on Robinhood Chain pays 50% less for Wafermap's own resources when it pays on an EVM network (Robinhood Chain, Base, Arbitrum, Polygon). Nothing to set up: `wafermap call` names the wallet to Wafermap and the quote comes back discounted; the server re-checks the balance of the wallet that signs. Marketplace listings and Solana payments keep the regular price. Check a wallet: https://wafermap.xyz/api/holder?address=0x…, or run `wafermap holder`.
85
+ A wallet that holds at least 100,000 $WAFERMAP on Robinhood Chain pays 50% less for Wafermap's own resources when it pays on an EVM network (Robinhood Chain, Base, Arbitrum, Polygon, Arc, Soneium). Nothing to set up: `wafermap call` names the wallet to Wafermap and the quote comes back discounted; the server re-checks the balance of the wallet that signs. Marketplace listings and Solana payments keep the regular price. Check a wallet: https://wafermap.xyz/api/holder?address=0x…, or run `wafermap holder`.
86
86
 
87
87
  Wafermap's own resources can also be **paid in $WAFERMAP at 50% off, and the tokens are burned**: `wafermap approve-wafermap` once (a Permit2 approval, one Robinhood Chain transaction), then `wafermap call <id> --pay wafermap`. Each payment is a Permit2 signature for one exact amount, usable for two minutes only by Wafermap's relayer (fixed in the client), checked against a market price the client reads itself.
88
88
 
@@ -94,7 +94,7 @@ The caps sit with the key, on your machine. Before the wallet signs anything it
94
94
 
95
95
  **The daily cap counts what the wallet signed, not what a server says it charged.** Every payment is written to `spend.jsonl` at its full signed amount the moment it is signed, before it is sent, and stays counted whatever the server answers: an error, a receipt saying $0, no receipt, or no answer at all. Check, sign and record run one payment at a time within a process, so calls started together cannot share the same remaining budget. Across processes on the same wallet (two MCP clients, or the CLI next to the MCP server), the wallet re-reads the shared log right after recording a signed payment and, if the cap is now passed, drops that payment before it is sent. (Before 0.3.3 the cap trusted the server's receipt; see [CHANGELOG.md](CHANGELOG.md).)
96
96
 
97
- **What the wallet cannot enforce.** An x402 `exact` payment is a signed authorization (EIP-3009 on EVM chains, a partially signed transaction on Solana). Whoever receives it can execute it once until it expires. The wallet signs EVM authorizations for at most 90 seconds, or less when the server asks for less (300 before 0.4.0). "A failed call costs $0" is therefore a promise of the server you pay: Wafermap's server settles only after the resource worked and otherwise lets the authorization expire unused. You do not have to take that on trust: `wafermap history --verify` asks each token contract (`authorizationState`) whether every signed payment was executed, and shows the ones that expired unused (`error` means the RPC did not answer). Because `authorizationState` is also true after `cancelAuthorization`, a payment only shows as `used` once the contract's `AuthorizationUsed` event for that nonce is found (in the settlement transaction, or in the blocks between signing and expiry); a nonce that is spent but never executed shows as `cancelled`. The search needs `eth_getLogs`, which Base's public RPC refuses; set `WAFERMAP_RPC_BASE` to your own RPC if a Base payment without a transaction on record shows `error`. Solana payments are not checked yet. The caps bound the worst case either way.
97
+ **What the wallet cannot enforce.** An x402 `exact` payment is a signed authorization (EIP-3009 on EVM chains, a partially signed transaction on Solana). Whoever receives it can execute it once until it expires. The wallet signs EVM authorizations for at most 90 seconds, or less when the server asks for less (300 before 0.4.0). "A failed call costs $0" is therefore a promise of the server you pay: Wafermap's server settles only after the resource worked and otherwise lets the authorization expire unused. You do not have to take that on trust: `wafermap history --verify` asks each token contract (`authorizationState`) whether every signed payment was executed, and shows the ones that expired unused (`error` means the RPC did not answer). Because `authorizationState` is also true after `cancelAuthorization`, a payment only shows as `used` once the contract's `AuthorizationUsed` event for that nonce is found (in the settlement transaction, or in the blocks between signing and expiry); a nonce that is spent but never executed shows as `cancelled`. The search needs `eth_getLogs`, which Base's public RPC refuses; set `WAFERMAP_RPC_BASE` to your own RPC if a Base payment without a transaction on record shows `error`. Solana payments are checked too, on the public Solana RPC by default (`WAFERMAP_RPC_SOLANA` to use another), not on Wafermap's: from 0.4.7 the wallet logs its own signature, the memo, the blockhash and its slot for each Solana payment. The check anchors the payment to the block its blockhash names (verified on-chain), and shows `used` when a transaction carrying the wallet's signature landed and succeeded, `unused` when the chain is past the blockhash's last valid height and the wallet's USDC history back to that block has no such transaction, and `pending` otherwise. Anything the RPC cannot establish for sure shows `error`, never `unused`. Solana payments logged before 0.4.7 can only be confirmed through the transaction the server named; otherwise they show `unknown`. The caps bound the worst case either way.
98
98
 
99
99
  **Time limits.** Requests to paid resources and services, to Wafermap's API, to the swap API and to Solana RPC give up after 60 seconds (`timeoutMs` in the SDK); EVM RPC calls keep viem's own time-outs. `wafermap resources` uses the same limit. Set `WAFERMAP_TIMEOUT_MS` to change it for the CLI and the MCP server. A paid call that times out after the payment was signed says so: the payment stays counted, and for EVM payments the message says until when it can be executed.
100
100
 
package/dist/cli.js CHANGED
@@ -202,7 +202,7 @@ function ataOf(owner, mint = SOLANA_USDC) {
202
202
  }
203
203
  throw new Error("No associated token account address.");
204
204
  }
205
- async function solanaPayment(payer, q) {
205
+ async function solanaPaymentSigned(payer, q) {
206
206
  if (q.asset !== SOLANA_USDC) throw new Error("Only USDC is paid on Solana.");
207
207
  if (!isSolanaAddress(q.feePayer) || !isSolanaAddress(q.payTo)) throw new Error("The quote has no valid Solana fee payer or recipient.");
208
208
  if (q.feePayer === payer.address) throw new Error("The fee payer cannot be the paying wallet.");
@@ -219,19 +219,26 @@ async function solanaPayment(payer, q) {
219
219
  { program: TOKEN_PROGRAM, accounts: [source, q.asset, dest, payer.address], data: concat(Uint8Array.of(12), le(BigInt(q.amount), 8), Uint8Array.of(USDC_DECIMALS)) },
220
220
  { program: MEMO_PROGRAM, accounts: [], data: memo }
221
221
  ];
222
+ const bh = payer.blockhashInfo ? await payer.blockhashInfo() : { blockhash: await payer.blockhash() };
222
223
  const message = concat(
223
224
  Uint8Array.of(128, 2, 1, 4),
224
225
  // v0; 2 signatures, 1 of them readonly; 4 readonly non-signers
225
226
  shortvec(keys.length),
226
227
  ...keys.map((k) => base58.decode(k)),
227
- base58.decode(await payer.blockhash()),
228
+ base58.decode(bh.blockhash),
228
229
  shortvec(ixs.length),
229
230
  ...ixs.map((ix) => concat(Uint8Array.of(at(ix.program)), shortvec(ix.accounts.length), Uint8Array.from(ix.accounts.map(at)), shortvec(ix.data.length), ix.data)),
230
231
  shortvec(0)
231
232
  // no address lookup tables
232
233
  );
233
234
  const signature = await payer.sign(message);
234
- return base64.encode(concat(shortvec(2), new Uint8Array(64), signature, message));
235
+ return {
236
+ transaction: base64.encode(concat(shortvec(2), new Uint8Array(64), signature, message)),
237
+ signature: base58.encode(signature),
238
+ memo: new TextDecoder().decode(memo),
239
+ blockhash: bh.blockhash,
240
+ ..."slot" in bh && Number.isFinite(bh.slot) ? { slot: bh.slot } : {}
241
+ };
235
242
  }
236
243
  async function solanaRpc(url, method, params, f = fetch) {
237
244
  const r = await f(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }) });
@@ -240,6 +247,70 @@ async function solanaRpc(url, method, params, f = fetch) {
240
247
  if (j.error) throw new Error(j.error.message ?? "Solana RPC error.");
241
248
  return j.result;
242
249
  }
250
+ async function solanaRead(url, method, params, f = fetch) {
251
+ const timed = (input, init) => f(input, { ...init, signal: AbortSignal.timeout(READ_TIMEOUT_MS) });
252
+ for (let attempt = 0; ; attempt++) {
253
+ try {
254
+ return await solanaRpc(url, method, params, timed);
255
+ } catch (e) {
256
+ if (attempt >= 6 || !/429|rate|too many/i.test(String(e?.message))) throw e;
257
+ await new Promise((r) => setTimeout(r, 1e3 * 2 ** attempt * (0.75 + Math.random() / 2)));
258
+ }
259
+ }
260
+ }
261
+ async function txOutcome(url, tx, e, f) {
262
+ const t = await solanaRead(url, "getTransaction", [tx, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: "confirmed" }], f);
263
+ if (!t) return null;
264
+ const sigs = t.transaction?.signatures ?? [];
265
+ const keys = t.transaction?.message?.accountKeys ?? [];
266
+ if (e.sig ? !sigs.includes(e.sig) : keys[1] !== e.payer) return "other";
267
+ if (t.meta?.err) return "failed";
268
+ if (!e.sig) {
269
+ const bal = (list) => BigInt(list?.find((b) => b.owner === e.payer && b.mint === SOLANA_USDC)?.uiTokenAmount?.amount ?? "0");
270
+ const drop = bal(t.meta?.preTokenBalances) - bal(t.meta?.postTokenBalances);
271
+ if (drop <= 0n || e.usd !== void 0 && drop !== BigInt(Math.round(e.usd * 1e6))) return "other";
272
+ }
273
+ return "ok";
274
+ }
275
+ async function checkSolanaPayment(e, url = SOLANA_PUBLIC_RPC, f) {
276
+ if (e.tx) {
277
+ const o = await txOutcome(url, e.tx, e, f).catch(() => null);
278
+ if (o === "ok") return "used";
279
+ if (o === "failed" && e.sig) return "unused";
280
+ }
281
+ if (!e.sig || !e.blockhash || !Number.isSafeInteger(e.slot) || e.slot <= 0) return "unknown";
282
+ const slot = e.slot;
283
+ const block = await solanaRead(url, "getBlock", [slot, { commitment: "confirmed", transactionDetails: "none", rewards: false, maxSupportedTransactionVersion: 0 }], f);
284
+ if (!block || block.blockhash !== e.blockhash || !Number.isSafeInteger(block.blockHeight)) throw new Error("The chain has no block with this payment's blockhash at the logged slot.");
285
+ const lastValidHeight = block.blockHeight + BLOCKHASH_MAX_AGE;
286
+ const fin = await solanaRead(url, "getEpochInfo", [{ commitment: "finalized" }], f);
287
+ const S = Number(fin?.absoluteSlot);
288
+ const H = Number(fin?.blockHeight);
289
+ if (!Number.isSafeInteger(S) || !Number.isSafeInteger(H)) throw new Error("The RPC did not give the finalized height.");
290
+ const expired = H > lastValidHeight;
291
+ let before;
292
+ let reached = false;
293
+ search: for (let page = 0; page < 20; page++) {
294
+ const list = await solanaRead(url, "getSignaturesForAddress", [ataOf(e.payer), { limit: 1e3, commitment: "confirmed", minContextSlot: S, ...before ? { before } : {} }], f);
295
+ if (!Array.isArray(list)) throw new Error("The RPC did not return the wallet's history.");
296
+ for (const x of list) {
297
+ if (!Number.isSafeInteger(x.slot)) throw new Error("The RPC returned history without slots.");
298
+ if (x.slot <= slot) {
299
+ reached = true;
300
+ break search;
301
+ }
302
+ if (e.memo && typeof x.memo === "string" && x.memo && !x.memo.includes(e.memo)) continue;
303
+ const o = await txOutcome(url, x.signature, e, f);
304
+ if (o === "ok") return "used";
305
+ if (o === "failed") return "unused";
306
+ if (o === null) throw new Error("The RPC lists a transaction it cannot return.");
307
+ }
308
+ if (list.length < 1e3) break;
309
+ before = list[list.length - 1].signature;
310
+ }
311
+ if (!reached) throw new Error("Could not read the wallet's history back to this payment.");
312
+ return expired ? "unused" : "pending";
313
+ }
243
314
  async function solanaBalances(url, owner, f) {
244
315
  const [usdc, lamports] = await Promise.all([
245
316
  solanaRpc(url, "getAccountInfo", [ataOf(owner), { commitment: "confirmed", encoding: "jsonParsed" }], f).then((r) => String(r.value?.data?.parsed?.info?.tokenAmount?.uiAmountString ?? "0")),
@@ -247,7 +318,7 @@ async function solanaBalances(url, owner, f) {
247
318
  ]);
248
319
  return { usdc, sol: String(lamports / 1e9) };
249
320
  }
250
- var SOLANA_CAIP, SOLANA_USDC, USDC_DECIMALS, TOKEN_PROGRAM, ATA_PROGRAM, COMPUTE_BUDGET, MEMO_PROGRAM, CU_LIMIT, CU_PRICE_MICROLAMPORTS, MAX_MEMO_BYTES, utf8, concat, hexBytes, le, shortvec, onCurve, latestBlockhash;
321
+ var SOLANA_CAIP, SOLANA_USDC, USDC_DECIMALS, TOKEN_PROGRAM, ATA_PROGRAM, COMPUTE_BUDGET, MEMO_PROGRAM, CU_LIMIT, CU_PRICE_MICROLAMPORTS, MAX_MEMO_BYTES, utf8, concat, hexBytes, le, shortvec, onCurve, latestBlockhash, latestBlockhashInfo, SOLANA_PUBLIC_RPC, READ_TIMEOUT_MS, BLOCKHASH_MAX_AGE;
251
322
  var init_solana = __esm({
252
323
  "shared/solana.ts"() {
253
324
  "use strict";
@@ -291,7 +362,14 @@ var init_solana = __esm({
291
362
  return false;
292
363
  }
293
364
  };
294
- latestBlockhash = async (url, f) => (await solanaRpc(url, "getLatestBlockhash", [{ commitment: "confirmed" }], f)).value.blockhash;
365
+ latestBlockhash = async (url, f) => (await latestBlockhashInfo(url, f)).blockhash;
366
+ latestBlockhashInfo = async (url, f) => {
367
+ const r = await solanaRpc(url, "getLatestBlockhash", [{ commitment: "confirmed" }], f);
368
+ return { blockhash: r.value.blockhash, slot: Number(r.context?.slot) };
369
+ };
370
+ SOLANA_PUBLIC_RPC = "https://api.mainnet-beta.solana.com";
371
+ READ_TIMEOUT_MS = 15e3;
372
+ BLOCKHASH_MAX_AGE = 150;
295
373
  }
296
374
  });
297
375
 
@@ -369,20 +447,23 @@ function choose(q, prefer = CHAIN_KEYS) {
369
447
  const r = q.accepts.find((a) => a.chain.key === k);
370
448
  if (r) return r;
371
449
  }
372
- throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or Polygon.` : "The service offered no payment option.");
450
+ throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium.` : "The service offered no payment option.");
373
451
  }
374
452
  async function signPayment(signer, req, resource, solana) {
375
453
  if (req.chain.vm === "svm") {
376
454
  if (!solana) throw new X402Error("no_solana_wallet", "This payment is on Solana and no Solana wallet was given.");
377
455
  let transaction;
456
+ let sol;
378
457
  try {
379
- transaction = await solanaPayment(solana, { amount: req.amount, payTo: req.payTo, asset: req.asset, feePayer: req.extra.feePayer, memo: req.extra.memo });
458
+ const s = await solanaPaymentSigned(solana, { amount: req.amount, payTo: req.payTo, asset: req.asset, feePayer: req.extra.feePayer, memo: req.extra.memo });
459
+ transaction = s.transaction;
460
+ sol = { sig: s.signature, memo: s.memo, blockhash: s.blockhash, ...s.slot !== void 0 ? { slot: s.slot } : {} };
380
461
  } catch (e) {
381
462
  if (e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError") throw e;
382
463
  throw new X402Error("sign_failed", e?.message ?? "Could not build the Solana payment.");
383
464
  }
384
- if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })) };
385
- return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })) };
465
+ if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })), sol };
466
+ return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })), sol };
386
467
  }
387
468
  const nonce = randomNonce();
388
469
  const validBefore = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_VALIDITY_SECONDS));
@@ -436,7 +517,7 @@ async function x402fetch(url, init, opts) {
436
517
  const svm = req.chain.vm === "svm";
437
518
  const signer = svm ? null : typeof opts.signer === "function" ? await opts.signer() : opts.signer;
438
519
  const header = await signPayment(signer, req, quote.resource, opts.solana);
439
- const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {} };
520
+ const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {}, ...header.sol ? { sol: header.sol } : {} };
440
521
  opts.onSigned?.(signed);
441
522
  const headers = new Headers(init.headers);
442
523
  headers.set(header.name, header.value);
@@ -454,7 +535,10 @@ var init_x402client = __esm({
454
535
  { key: "solana", vm: "svm", id: 0, caip: SOLANA_CAIP, v1: "solana", stable: SOLANA_USDC, symbol: "USDC", name: "Solana" },
455
536
  { key: "base", vm: "evm", id: 8453, caip: "eip155:8453", v1: "base", stable: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol: "USDC", name: "Base", domain: USDC_DOMAIN },
456
537
  { key: "arbitrum", vm: "evm", id: 42161, caip: "eip155:42161", v1: "arbitrum", stable: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", symbol: "USDC", name: "Arbitrum", domain: USDC_DOMAIN },
457
- { key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN }
538
+ { key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN },
539
+ // EIP-712 domains read from the token contracts (name(), version()) and checked against DOMAIN_SEPARATOR().
540
+ { key: "arc", vm: "evm", id: 5042, caip: "eip155:5042", v1: "arc", stable: "0x3600000000000000000000000000000000000000", symbol: "USDC", name: "Arc", domain: { name: "USDC", version: "2" } },
541
+ { key: "soneium", vm: "evm", id: 1868, caip: "eip155:1868", v1: "soneium", stable: "0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369", symbol: "USDC", name: "Soneium", domain: { name: "Bridged USDC (Soneium)", version: "2" } }
458
542
  ];
459
543
  EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
460
544
  CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
@@ -562,7 +646,7 @@ __export(core_exports, {
562
646
  });
563
647
  import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http, pad, parseAbiItem, toEventSelector } from "viem";
564
648
  import { privateKeyToAccount } from "viem/accounts";
565
- import { arbitrum, base, polygon, robinhood } from "viem/chains";
649
+ import { arbitrum, arc, base, polygon, robinhood, soneium } from "viem/chains";
566
650
  function mergeLog(entries) {
567
651
  const outcomes = new Map(entries.filter((e) => e.of).map((e) => [e.of, e]));
568
652
  return entries.filter((e) => !e.of).map((e) => {
@@ -592,7 +676,8 @@ async function blockAt(client, ts, side, latest) {
592
676
  return side === "from" ? lo.number : hi.number;
593
677
  }
594
678
  async function checkPayment(e, rpc) {
595
- if (e.network === "solana" || !e.nonce || !(e.network in CHAINS)) return "unknown";
679
+ if (e.network === "solana") return checkSolanaPayment(e, rpc).catch(() => "error");
680
+ if (!e.nonce || !(e.network in CHAINS)) return "unknown";
596
681
  const chain = PAY_CHAINS.find((c) => c.key === e.network);
597
682
  try {
598
683
  const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
@@ -666,7 +751,7 @@ var init_core = __esm({
666
751
  }
667
752
  code;
668
753
  };
669
- CHAINS = { robinhood, base, arbitrum, polygon };
754
+ CHAINS = { robinhood, base, arbitrum, polygon, arc, soneium };
670
755
  AUTH_STATE_ABI = [{ type: "function", name: "authorizationState", stateMutability: "view", inputs: [{ name: "authorizer", type: "address" }, { name: "nonce", type: "bytes32" }], outputs: [{ type: "bool" }] }];
671
756
  AUTH_USED = parseAbiItem("event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)");
672
757
  DEFAULT_LIMITS = { perPayment: 0.1, perDay: 1, perSwap: 25 };
@@ -703,7 +788,7 @@ var init_core = __esm({
703
788
  this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
704
789
  const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
705
790
  const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
706
- this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch) } : null;
791
+ this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch), blockhashInfo: () => latestBlockhashInfo(solRpc, this.fetch) } : null;
707
792
  this.solanaAddress = sol?.address ?? o.solanaAddress ?? null;
708
793
  }
709
794
  get address() {
@@ -742,7 +827,7 @@ var init_core = __esm({
742
827
  /** Write the signed payment to the ledger before it leaves the machine. */
743
828
  booked(kind, what, s) {
744
829
  const id = crypto.randomUUID();
745
- this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {} });
830
+ this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {}, ...s.sol ? { sig: s.sol.sig, memo: s.sol.memo, blockhash: s.sol.blockhash, ...s.sol.slot !== void 0 ? { slot: s.sol.slot } : {} } : {} });
746
831
  return id;
747
832
  }
748
833
  /** The outcome line for a booked payment. It adds nothing to the daily total. */
@@ -1144,7 +1229,7 @@ function openWallet(password2, approve) {
1144
1229
  limits: c.limits,
1145
1230
  ledger: { entries: spendLog, record: appendSpend },
1146
1231
  approve,
1147
- rpc: { robinhood: process.env.WAFERMAP_RPC_ROBINHOOD, base: process.env.WAFERMAP_RPC_BASE, arbitrum: process.env.WAFERMAP_RPC_ARBITRUM, polygon: process.env.WAFERMAP_RPC_POLYGON, solana: process.env.WAFERMAP_RPC_SOLANA }
1232
+ rpc: { robinhood: process.env.WAFERMAP_RPC_ROBINHOOD, base: process.env.WAFERMAP_RPC_BASE, arbitrum: process.env.WAFERMAP_RPC_ARBITRUM, polygon: process.env.WAFERMAP_RPC_POLYGON, arc: process.env.WAFERMAP_RPC_ARC, soneium: process.env.WAFERMAP_RPC_SONEIUM, solana: process.env.WAFERMAP_RPC_SOLANA }
1148
1233
  });
1149
1234
  if (wm.solanaAddress) rememberSolana(wm.solanaAddress);
1150
1235
  return wm;
@@ -1162,7 +1247,7 @@ var VERSION;
1162
1247
  var init_version = __esm({
1163
1248
  "src/version.ts"() {
1164
1249
  "use strict";
1165
- VERSION = "0.4.6";
1250
+ VERSION = "0.4.8";
1166
1251
  }
1167
1252
  });
1168
1253
 
@@ -1281,7 +1366,7 @@ async function runMcp() {
1281
1366
  "wafermap_discover",
1282
1367
  {
1283
1368
  title: "Discover x402 services",
1284
- description: "Search the open x402 index for third-party paid APIs (weather, search, enrichment, data\u2026). Services marked payable accept USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or Polygon and can be called with wafermap_pay.",
1369
+ description: "Search the open x402 index for third-party paid APIs (weather, search, enrichment, data\u2026). Services marked payable accept USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium and can be called with wafermap_pay.",
1285
1370
  inputSchema: { query: z.string().min(2).describe("What you need, in a few words."), limit: z.number().int().min(1).max(20).optional() }
1286
1371
  },
1287
1372
  async ({ query, limit }) => {
@@ -1359,7 +1444,7 @@ var init_mcp = __esm({
1359
1444
  init_local();
1360
1445
  init_store();
1361
1446
  init_version();
1362
- NET = z.enum(["robinhood", "solana", "base", "arbitrum", "polygon"]).optional().describe("Payment network: robinhood (USDG), or solana, base, arbitrum or polygon (USDC). Defaults to your configured network.");
1447
+ NET = z.enum(["robinhood", "solana", "base", "arbitrum", "polygon", "arc", "soneium"]).optional().describe("Payment network: robinhood (USDG), or solana, base, arbitrum, polygon, arc or soneium (USDC). Defaults to your configured network.");
1363
1448
  SWAP_NET = z.enum(["robinhood", "base"]).optional().describe("Swap network: robinhood or base. Defaults to your configured network when it is one of these, else robinhood.");
1364
1449
  }
1365
1450
  });
@@ -1693,7 +1778,9 @@ var PAY_NETWORKS = {
1693
1778
  solana: { id: 0, caip: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", name: "Solana", asset: "USDC", explorer: "https://solscan.io", vm: "svm" },
1694
1779
  base: { ...NETWORKS.base, vm: "evm" },
1695
1780
  arbitrum: { id: 42161, caip: "eip155:42161", name: "Arbitrum", asset: "USDC", explorer: "https://arbiscan.io", vm: "evm" },
1696
- polygon: { id: 137, caip: "eip155:137", name: "Polygon", asset: "USDC", explorer: "https://polygonscan.com", vm: "evm" }
1781
+ polygon: { id: 137, caip: "eip155:137", name: "Polygon", asset: "USDC", explorer: "https://polygonscan.com", vm: "evm" },
1782
+ arc: { id: 5042, caip: "eip155:5042", name: "Arc", asset: "USDC", explorer: "https://explorer.arc.io", vm: "evm" },
1783
+ soneium: { id: 1868, caip: "eip155:1868", name: "Soneium", asset: "USDC", explorer: "https://soneium.blockscout.com", vm: "evm" }
1697
1784
  };
1698
1785
  function payNetworkOf(v) {
1699
1786
  const s = String(v ?? "").toLowerCase();
@@ -1708,7 +1795,7 @@ init_local();
1708
1795
  init_solana();
1709
1796
  init_store();
1710
1797
  init_version();
1711
- var HELP = `wafermap ${VERSION}: an x402 wallet for agents (Robinhood Chain, Solana, Base, Arbitrum, Polygon)
1798
+ var HELP = `wafermap ${VERSION}: an x402 wallet for agents (Robinhood Chain, Solana, Base, Arbitrum, Polygon, Arc, Soneium)
1712
1799
 
1713
1800
  Wallet
1714
1801
  wafermap init [--import] create (or import) the agent wallet, encrypted with a password
@@ -1735,7 +1822,7 @@ Trade
1735
1822
  Agents
1736
1823
  wafermap mcp run as an MCP server (stdio); password from \`wafermap keychain on\` or WAFERMAP_PASSWORD
1737
1824
 
1738
- Common flags: --network robinhood|solana|base|arbitrum|polygon --yes (skip the confirmation)
1825
+ Common flags: --network robinhood|solana|base|arbitrum|polygon|arc|soneium --yes (skip the confirmation)
1739
1826
  Files live in ${HOME}. Docs: https://wafermap.xyz/docs#cli`;
1740
1827
  function parse(argv) {
1741
1828
  const out = { _: [], flags: {} };
@@ -1837,7 +1924,7 @@ async function main() {
1837
1924
  console.log(`\u2713 Agent wallet ${acct.address}
1838
1925
  Solana address ${sol} (derived from the same key)
1839
1926
  Key encrypted in ${HOME}/keystore.json. Forget the password and the key is gone: run \`wafermap export-key\` to back it up.
1840
- Fund it with a few dollars of USDC on Solana (Solana address), or USDG on Robinhood Chain or USDC on Base, Arbitrum or Polygon (EVM address). Payments are gasless; swaps need a little ETH.
1927
+ Fund it with a few dollars of USDC on Solana (Solana address), or USDG on Robinhood Chain or USDC on Base, Arbitrum, Polygon, Arc or Soneium (EVM address). Payments are gasless; swaps need a little ETH.
1841
1928
  Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits\`.`);
1842
1929
  return;
1843
1930
  }
@@ -1912,14 +1999,18 @@ async function main() {
1912
1999
  const rows = mergeLog(spendLog()).filter((e) => e.at >= since).reverse();
1913
2000
  if (!rows.length) return console.log(`Nothing in the last ${days} days.`);
1914
2001
  const verify = a.flags.verify !== void 0;
1915
- const RPC = { robinhood: process.env.WAFERMAP_RPC_ROBINHOOD, base: process.env.WAFERMAP_RPC_BASE, arbitrum: process.env.WAFERMAP_RPC_ARBITRUM, polygon: process.env.WAFERMAP_RPC_POLYGON };
2002
+ const RPC = { robinhood: process.env.WAFERMAP_RPC_ROBINHOOD, base: process.env.WAFERMAP_RPC_BASE, arbitrum: process.env.WAFERMAP_RPC_ARBITRUM, polygon: process.env.WAFERMAP_RPC_POLYGON, arc: process.env.WAFERMAP_RPC_ARC, soneium: process.env.WAFERMAP_RPC_SONEIUM, solana: process.env.WAFERMAP_RPC_SOLANA };
1916
2003
  const chain = [];
1917
2004
  if (verify) {
2005
+ const txCount = /* @__PURE__ */ new Map();
2006
+ for (const e of rows) if (e.network === "solana" && e.tx) txCount.set(e.tx, (txCount.get(e.tx) ?? 0) + 1);
1918
2007
  let next = 0;
1919
2008
  await Promise.all(Array.from({ length: 4 }, async () => {
1920
2009
  while (next < rows.length) {
1921
2010
  const i = next++;
1922
- chain[i] = rows[i].kind === "swap" ? null : await checkPayment(rows[i], RPC[rows[i].network]);
2011
+ const e = rows[i];
2012
+ const shared = e.network === "solana" && !e.sig && !!e.tx && (txCount.get(e.tx) ?? 0) > 1;
2013
+ chain[i] = e.kind === "swap" ? null : await checkPayment(shared ? { ...e, tx: null } : e, RPC[e.network]);
1923
2014
  }
1924
2015
  }));
1925
2016
  }
@@ -1934,7 +2025,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
1934
2025
  if (verify) {
1935
2026
  const never = rows.filter((e, i) => e.kind !== "swap" && chain[i] === "unused").reduce((s, e) => s + e.usd, 0);
1936
2027
  const cancelled = rows.filter((e, i) => e.kind !== "swap" && chain[i] === "cancelled").reduce((s, e) => s + e.usd, 0);
1937
- console.log(`On-chain: $${never.toFixed(4)} of that was signed but never executed and has expired${cancelled ? `, and $${cancelled.toFixed(4)} was cancelled by this wallet before it executed` : ""}. "used" = the token contract logged the transfer (AuthorizationUsed); "cancelled" = the nonce is spent but never executed (cancelAuthorization); "unknown" = Solana or an older log line; "error" = the RPC did not answer, try again (Base's public RPC refuses log searches: for a payment with no transaction on record, set WAFERMAP_RPC_BASE to your own RPC).`);
2028
+ console.log(`On-chain: $${never.toFixed(4)} of that was signed but never executed and has expired${cancelled ? `, and $${cancelled.toFixed(4)} was cancelled by this wallet before it executed` : ""}. "used" = the token contract logged the transfer (AuthorizationUsed); "cancelled" = the nonce is spent but never executed (cancelAuthorization); "unknown" = an older log line with nothing to check by; "error" = the RPC did not answer, try again (Base's public RPC refuses log searches: for a payment with no transaction on record, set WAFERMAP_RPC_BASE to your own RPC).`);
1938
2029
  } else console.log("Add --verify to check on-chain which signed payments were executed.");
1939
2030
  return;
1940
2031
  }
@@ -1953,7 +2044,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
1953
2044
  ${s.description.slice(0, 140)}${s.input ? `
1954
2045
  input: ${JSON.stringify(s.input).slice(0, 140)}` : ""}`);
1955
2046
  return console.log(`
1956
- \u25CF payable with USDG (Robinhood Chain) or USDC (Solana, Base, Arbitrum, Polygon) \u25CB other networks only`);
2047
+ \u25CF payable with USDG (Robinhood Chain) or USDC (Solana, Base, Arbitrum, Polygon, Arc, Soneium) \u25CB other networks only`);
1957
2048
  }
1958
2049
  case "call": {
1959
2050
  const id = a._[0] ?? die("Which resource? e.g. wafermap call gas-now network=base");
@@ -1977,7 +2068,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
1977
2068
  case "balance": {
1978
2069
  const addr = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
1979
2070
  const { Wafermap: Wafermap2 } = await Promise.resolve().then(() => (init_core(), core_exports));
1980
- const wm = new Wafermap2({ account: { ...privateKeyToAccount2(generatePrivateKey()), address: addr }, solanaAddress: keystoreSolana() ?? void 0, baseUrl: process.env.WAFERMAP_URL, rpc: { robinhood: process.env.WAFERMAP_RPC_ROBINHOOD, base: process.env.WAFERMAP_RPC_BASE, arbitrum: process.env.WAFERMAP_RPC_ARBITRUM, polygon: process.env.WAFERMAP_RPC_POLYGON, solana: process.env.WAFERMAP_RPC_SOLANA } });
2071
+ const wm = new Wafermap2({ account: { ...privateKeyToAccount2(generatePrivateKey()), address: addr }, solanaAddress: keystoreSolana() ?? void 0, baseUrl: process.env.WAFERMAP_URL, rpc: { robinhood: process.env.WAFERMAP_RPC_ROBINHOOD, base: process.env.WAFERMAP_RPC_BASE, arbitrum: process.env.WAFERMAP_RPC_ARBITRUM, polygon: process.env.WAFERMAP_RPC_POLYGON, arc: process.env.WAFERMAP_RPC_ARC, soneium: process.env.WAFERMAP_RPC_SONEIUM, solana: process.env.WAFERMAP_RPC_SOLANA } });
1981
2072
  const b = await wm.balances();
1982
2073
  console.log(addr);
1983
2074
  for (const n of NETS) console.log(` ${n.padEnd(10)} ${b[n] ? `${Number(b[n].stable).toFixed(4)} ${b[n].symbol} ${Number(b[n].eth).toPrecision(4)} ${b[n].gas}` : n === "solana" && !wm.solanaAddress ? "run `wafermap address` once with WAFERMAP_PASSWORD set" : "unreadable (RPC did not answer)"}`);
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/core.ts
2
2
  import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http, pad, parseAbiItem, toEventSelector } from "viem";
3
3
  import { privateKeyToAccount } from "viem/accounts";
4
- import { arbitrum, base, polygon, robinhood } from "viem/chains";
4
+ import { arbitrum, arc, base, polygon, robinhood, soneium } from "viem/chains";
5
5
 
6
6
  // shared/swap.ts
7
7
  import { decodeFunctionData, erc20Abi, formatUnits, getAddress, isAddress, parseAbi, parseUnits } from "viem";
@@ -225,7 +225,7 @@ function ataOf(owner, mint = SOLANA_USDC) {
225
225
  }
226
226
  throw new Error("No associated token account address.");
227
227
  }
228
- async function solanaPayment(payer, q) {
228
+ async function solanaPaymentSigned(payer, q) {
229
229
  if (q.asset !== SOLANA_USDC) throw new Error("Only USDC is paid on Solana.");
230
230
  if (!isSolanaAddress(q.feePayer) || !isSolanaAddress(q.payTo)) throw new Error("The quote has no valid Solana fee payer or recipient.");
231
231
  if (q.feePayer === payer.address) throw new Error("The fee payer cannot be the paying wallet.");
@@ -242,19 +242,26 @@ async function solanaPayment(payer, q) {
242
242
  { program: TOKEN_PROGRAM, accounts: [source, q.asset, dest, payer.address], data: concat(Uint8Array.of(12), le(BigInt(q.amount), 8), Uint8Array.of(USDC_DECIMALS)) },
243
243
  { program: MEMO_PROGRAM, accounts: [], data: memo }
244
244
  ];
245
+ const bh = payer.blockhashInfo ? await payer.blockhashInfo() : { blockhash: await payer.blockhash() };
245
246
  const message = concat(
246
247
  Uint8Array.of(128, 2, 1, 4),
247
248
  // v0; 2 signatures, 1 of them readonly; 4 readonly non-signers
248
249
  shortvec(keys.length),
249
250
  ...keys.map((k) => base58.decode(k)),
250
- base58.decode(await payer.blockhash()),
251
+ base58.decode(bh.blockhash),
251
252
  shortvec(ixs.length),
252
253
  ...ixs.map((ix) => concat(Uint8Array.of(at(ix.program)), shortvec(ix.accounts.length), Uint8Array.from(ix.accounts.map(at)), shortvec(ix.data.length), ix.data)),
253
254
  shortvec(0)
254
255
  // no address lookup tables
255
256
  );
256
257
  const signature = await payer.sign(message);
257
- return base64.encode(concat(shortvec(2), new Uint8Array(64), signature, message));
258
+ return {
259
+ transaction: base64.encode(concat(shortvec(2), new Uint8Array(64), signature, message)),
260
+ signature: base58.encode(signature),
261
+ memo: new TextDecoder().decode(memo),
262
+ blockhash: bh.blockhash,
263
+ ..."slot" in bh && Number.isFinite(bh.slot) ? { slot: bh.slot } : {}
264
+ };
258
265
  }
259
266
  async function solanaRpc(url, method, params, f = fetch) {
260
267
  const r = await f(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }) });
@@ -263,7 +270,78 @@ async function solanaRpc(url, method, params, f = fetch) {
263
270
  if (j.error) throw new Error(j.error.message ?? "Solana RPC error.");
264
271
  return j.result;
265
272
  }
266
- var latestBlockhash = async (url, f) => (await solanaRpc(url, "getLatestBlockhash", [{ commitment: "confirmed" }], f)).value.blockhash;
273
+ var latestBlockhash = async (url, f) => (await latestBlockhashInfo(url, f)).blockhash;
274
+ var latestBlockhashInfo = async (url, f) => {
275
+ const r = await solanaRpc(url, "getLatestBlockhash", [{ commitment: "confirmed" }], f);
276
+ return { blockhash: r.value.blockhash, slot: Number(r.context?.slot) };
277
+ };
278
+ var SOLANA_PUBLIC_RPC = "https://api.mainnet-beta.solana.com";
279
+ var READ_TIMEOUT_MS = 15e3;
280
+ async function solanaRead(url, method, params, f = fetch) {
281
+ const timed = (input, init) => f(input, { ...init, signal: AbortSignal.timeout(READ_TIMEOUT_MS) });
282
+ for (let attempt = 0; ; attempt++) {
283
+ try {
284
+ return await solanaRpc(url, method, params, timed);
285
+ } catch (e) {
286
+ if (attempt >= 6 || !/429|rate|too many/i.test(String(e?.message))) throw e;
287
+ await new Promise((r) => setTimeout(r, 1e3 * 2 ** attempt * (0.75 + Math.random() / 2)));
288
+ }
289
+ }
290
+ }
291
+ async function txOutcome(url, tx, e, f) {
292
+ const t = await solanaRead(url, "getTransaction", [tx, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: "confirmed" }], f);
293
+ if (!t) return null;
294
+ const sigs = t.transaction?.signatures ?? [];
295
+ const keys = t.transaction?.message?.accountKeys ?? [];
296
+ if (e.sig ? !sigs.includes(e.sig) : keys[1] !== e.payer) return "other";
297
+ if (t.meta?.err) return "failed";
298
+ if (!e.sig) {
299
+ const bal = (list) => BigInt(list?.find((b) => b.owner === e.payer && b.mint === SOLANA_USDC)?.uiTokenAmount?.amount ?? "0");
300
+ const drop = bal(t.meta?.preTokenBalances) - bal(t.meta?.postTokenBalances);
301
+ if (drop <= 0n || e.usd !== void 0 && drop !== BigInt(Math.round(e.usd * 1e6))) return "other";
302
+ }
303
+ return "ok";
304
+ }
305
+ var BLOCKHASH_MAX_AGE = 150;
306
+ async function checkSolanaPayment(e, url = SOLANA_PUBLIC_RPC, f) {
307
+ if (e.tx) {
308
+ const o = await txOutcome(url, e.tx, e, f).catch(() => null);
309
+ if (o === "ok") return "used";
310
+ if (o === "failed" && e.sig) return "unused";
311
+ }
312
+ if (!e.sig || !e.blockhash || !Number.isSafeInteger(e.slot) || e.slot <= 0) return "unknown";
313
+ const slot = e.slot;
314
+ const block = await solanaRead(url, "getBlock", [slot, { commitment: "confirmed", transactionDetails: "none", rewards: false, maxSupportedTransactionVersion: 0 }], f);
315
+ if (!block || block.blockhash !== e.blockhash || !Number.isSafeInteger(block.blockHeight)) throw new Error("The chain has no block with this payment's blockhash at the logged slot.");
316
+ const lastValidHeight = block.blockHeight + BLOCKHASH_MAX_AGE;
317
+ const fin = await solanaRead(url, "getEpochInfo", [{ commitment: "finalized" }], f);
318
+ const S = Number(fin?.absoluteSlot);
319
+ const H = Number(fin?.blockHeight);
320
+ if (!Number.isSafeInteger(S) || !Number.isSafeInteger(H)) throw new Error("The RPC did not give the finalized height.");
321
+ const expired = H > lastValidHeight;
322
+ let before;
323
+ let reached = false;
324
+ search: for (let page = 0; page < 20; page++) {
325
+ const list = await solanaRead(url, "getSignaturesForAddress", [ataOf(e.payer), { limit: 1e3, commitment: "confirmed", minContextSlot: S, ...before ? { before } : {} }], f);
326
+ if (!Array.isArray(list)) throw new Error("The RPC did not return the wallet's history.");
327
+ for (const x of list) {
328
+ if (!Number.isSafeInteger(x.slot)) throw new Error("The RPC returned history without slots.");
329
+ if (x.slot <= slot) {
330
+ reached = true;
331
+ break search;
332
+ }
333
+ if (e.memo && typeof x.memo === "string" && x.memo && !x.memo.includes(e.memo)) continue;
334
+ const o = await txOutcome(url, x.signature, e, f);
335
+ if (o === "ok") return "used";
336
+ if (o === "failed") return "unused";
337
+ if (o === null) throw new Error("The RPC lists a transaction it cannot return.");
338
+ }
339
+ if (list.length < 1e3) break;
340
+ before = list[list.length - 1].signature;
341
+ }
342
+ if (!reached) throw new Error("Could not read the wallet's history back to this payment.");
343
+ return expired ? "unused" : "pending";
344
+ }
267
345
  async function solanaBalances(url, owner, f) {
268
346
  const [usdc, lamports] = await Promise.all([
269
347
  solanaRpc(url, "getAccountInfo", [ataOf(owner), { commitment: "confirmed", encoding: "jsonParsed" }], f).then((r) => String(r.value?.data?.parsed?.info?.tokenAmount?.uiAmountString ?? "0")),
@@ -279,7 +357,10 @@ var PAY_CHAINS = [
279
357
  { key: "solana", vm: "svm", id: 0, caip: SOLANA_CAIP, v1: "solana", stable: SOLANA_USDC, symbol: "USDC", name: "Solana" },
280
358
  { key: "base", vm: "evm", id: 8453, caip: "eip155:8453", v1: "base", stable: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol: "USDC", name: "Base", domain: USDC_DOMAIN },
281
359
  { key: "arbitrum", vm: "evm", id: 42161, caip: "eip155:42161", v1: "arbitrum", stable: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", symbol: "USDC", name: "Arbitrum", domain: USDC_DOMAIN },
282
- { key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN }
360
+ { key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN },
361
+ // EIP-712 domains read from the token contracts (name(), version()) and checked against DOMAIN_SEPARATOR().
362
+ { key: "arc", vm: "evm", id: 5042, caip: "eip155:5042", v1: "arc", stable: "0x3600000000000000000000000000000000000000", symbol: "USDC", name: "Arc", domain: { name: "USDC", version: "2" } },
363
+ { key: "soneium", vm: "evm", id: 1868, caip: "eip155:1868", v1: "soneium", stable: "0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369", symbol: "USDC", name: "Soneium", domain: { name: "Bridged USDC (Soneium)", version: "2" } }
283
364
  ];
284
365
  var EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
285
366
  var CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
@@ -388,7 +469,7 @@ function choose(q, prefer = CHAIN_KEYS) {
388
469
  const r = q.accepts.find((a) => a.chain.key === k);
389
470
  if (r) return r;
390
471
  }
391
- throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or Polygon.` : "The service offered no payment option.");
472
+ throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium.` : "The service offered no payment option.");
392
473
  }
393
474
  var TYPES = {
394
475
  TransferWithAuthorization: [
@@ -406,14 +487,17 @@ async function signPayment(signer, req, resource, solana) {
406
487
  if (req.chain.vm === "svm") {
407
488
  if (!solana) throw new X402Error("no_solana_wallet", "This payment is on Solana and no Solana wallet was given.");
408
489
  let transaction;
490
+ let sol;
409
491
  try {
410
- transaction = await solanaPayment(solana, { amount: req.amount, payTo: req.payTo, asset: req.asset, feePayer: req.extra.feePayer, memo: req.extra.memo });
492
+ const s = await solanaPaymentSigned(solana, { amount: req.amount, payTo: req.payTo, asset: req.asset, feePayer: req.extra.feePayer, memo: req.extra.memo });
493
+ transaction = s.transaction;
494
+ sol = { sig: s.signature, memo: s.memo, blockhash: s.blockhash, ...s.slot !== void 0 ? { slot: s.slot } : {} };
411
495
  } catch (e) {
412
496
  if (e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError") throw e;
413
497
  throw new X402Error("sign_failed", e?.message ?? "Could not build the Solana payment.");
414
498
  }
415
- if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })) };
416
- return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })) };
499
+ if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })), sol };
500
+ return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })), sol };
417
501
  }
418
502
  const nonce = randomNonce();
419
503
  const validBefore = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_VALIDITY_SECONDS));
@@ -467,7 +551,7 @@ async function x402fetch(url, init, opts) {
467
551
  const svm = req.chain.vm === "svm";
468
552
  const signer = svm ? null : typeof opts.signer === "function" ? await opts.signer() : opts.signer;
469
553
  const header = await signPayment(signer, req, quote.resource, opts.solana);
470
- const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {} };
554
+ const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {}, ...header.sol ? { sol: header.sol } : {} };
471
555
  opts.onSigned?.(signed);
472
556
  const headers = new Headers(init.headers);
473
557
  headers.set(header.name, header.value);
@@ -520,7 +604,7 @@ var WafermapError = class extends Error {
520
604
  }
521
605
  code;
522
606
  };
523
- var CHAINS = { robinhood, base, arbitrum, polygon };
607
+ var CHAINS = { robinhood, base, arbitrum, polygon, arc, soneium };
524
608
  var AUTH_STATE_ABI = [{ type: "function", name: "authorizationState", stateMutability: "view", inputs: [{ name: "authorizer", type: "address" }, { name: "nonce", type: "bytes32" }], outputs: [{ type: "bool" }] }];
525
609
  function mergeLog(entries) {
526
610
  const outcomes = new Map(entries.filter((e) => e.of).map((e) => [e.of, e]));
@@ -552,7 +636,8 @@ async function blockAt(client, ts, side, latest) {
552
636
  }
553
637
  var AUTH_USED = parseAbiItem("event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)");
554
638
  async function checkPayment(e, rpc) {
555
- if (e.network === "solana" || !e.nonce || !(e.network in CHAINS)) return "unknown";
639
+ if (e.network === "solana") return checkSolanaPayment(e, rpc).catch(() => "error");
640
+ if (!e.nonce || !(e.network in CHAINS)) return "unknown";
556
641
  const chain = PAY_CHAINS.find((c) => c.key === e.network);
557
642
  try {
558
643
  const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
@@ -642,7 +727,7 @@ var Wafermap = class {
642
727
  this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
643
728
  const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
644
729
  const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
645
- this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch) } : null;
730
+ this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch), blockhashInfo: () => latestBlockhashInfo(solRpc, this.fetch) } : null;
646
731
  this.solanaAddress = sol?.address ?? o.solanaAddress ?? null;
647
732
  }
648
733
  get address() {
@@ -681,7 +766,7 @@ var Wafermap = class {
681
766
  /** Write the signed payment to the ledger before it leaves the machine. */
682
767
  booked(kind, what, s) {
683
768
  const id = crypto.randomUUID();
684
- this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {} });
769
+ this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {}, ...s.sol ? { sig: s.sol.sig, memo: s.sol.memo, blockhash: s.sol.blockhash, ...s.sol.slot !== void 0 ? { slot: s.sol.slot } : {} } : {} });
685
770
  return id;
686
771
  }
687
772
  /** The outcome line for a booked payment. It adds nothing to the daily total. */
@@ -908,6 +993,7 @@ export {
908
993
  PASS_HEADER,
909
994
  PAY_CHAINS,
910
995
  PERMIT2,
996
+ SOLANA_PUBLIC_RPC,
911
997
  SwapError,
912
998
  WAFERMAP_RELAYERS,
913
999
  WAFERMAP_TOKEN,
@@ -915,6 +1001,7 @@ export {
915
1001
  WafermapError,
916
1002
  X402Error,
917
1003
  checkPayment,
1004
+ checkSolanaPayment,
918
1005
  choose,
919
1006
  decodePass,
920
1007
  decodeSwap,
@@ -1,10 +1,16 @@
1
1
  export declare const SOLANA_CAIP = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
2
2
  export declare const SOLANA_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
3
- /** Signs as one Solana address. `blockhash` returns a recent blockhash from any mainnet RPC. */
3
+ /** Signs as one Solana address. `blockhash` returns a recent blockhash from any mainnet RPC. When
4
+ * `blockhashInfo` is given it is used instead, and the slot the blockhash was read at is kept with
5
+ * the payment: the payment cannot land before it, which bounds the search for it later. */
4
6
  export type SolanaPayer = {
5
7
  address: string;
6
8
  sign: (message: Uint8Array) => Uint8Array | Promise<Uint8Array>;
7
9
  blockhash: () => Promise<string>;
10
+ blockhashInfo?: () => Promise<{
11
+ blockhash: string;
12
+ slot: number;
13
+ }>;
8
14
  };
9
15
  export declare function isSolanaAddress(v: string): boolean;
10
16
  /** The agent's Solana keypair, derived from its EVM private key. */
@@ -25,8 +31,53 @@ export type SolanaQuote = {
25
31
  };
26
32
  /** The base64 wire transaction for an x402 `exact` payment on Solana, signed by `payer` only. */
27
33
  export declare function solanaPayment(payer: SolanaPayer, q: SolanaQuote): Promise<string>;
34
+ /** What a signed Solana payment leaves to check it by later: our signature (the second one in the
35
+ * transaction; the first, which names it on explorers, is the facilitator's and unknown until it
36
+ * submits), its memo, its blockhash (once that is no longer valid the payment can never execute)
37
+ * and, when known, the slot the blockhash was read at. */
38
+ export type SolanaSigned = {
39
+ transaction: string;
40
+ signature: string;
41
+ memo: string;
42
+ blockhash: string;
43
+ slot?: number;
44
+ };
45
+ export declare function solanaPaymentSigned(payer: SolanaPayer, q: SolanaQuote): Promise<SolanaSigned>;
28
46
  export declare function solanaRpc(url: string, method: string, params: unknown[], f?: typeof fetch): Promise<any>;
29
47
  export declare const latestBlockhash: (url: string, f?: typeof fetch) => Promise<string>;
48
+ export declare const latestBlockhashInfo: (url: string, f?: typeof fetch) => Promise<{
49
+ blockhash: string;
50
+ slot: number;
51
+ }>;
52
+ /** The public Solana RPC that `checkSolanaPayment` reads by default: not Wafermap's, so the answer
53
+ * does not depend on the server that was paid. */
54
+ export declare const SOLANA_PUBLIC_RPC = "https://api.mainnet-beta.solana.com";
55
+ export type SolanaCheck = {
56
+ payer: string;
57
+ usd?: number;
58
+ tx?: string | null;
59
+ sig?: string;
60
+ memo?: string;
61
+ blockhash?: string;
62
+ slot?: number;
63
+ };
64
+ /**
65
+ * Was a signed Solana payment ever executed? Read from a Solana RPC (by default the public one, not
66
+ * Wafermap's), and every number it rests on is checked on that RPC, not taken from the server
67
+ * that was paid.
68
+ * - The transaction the server named, if it carries our signature: "used" when it succeeded.
69
+ * - Otherwise the payment is anchored to the block its blockhash names: the block at the logged
70
+ * slot must have exactly that hash. The payment can only land after that slot, and only until
71
+ * the chain is BLOCKHASH_MAX_AGE blocks past it. Then, with the finalized height H at slot S:
72
+ * the wallet's USDC account history, read from a node at slot S or later, back to that slot,
73
+ * is searched for our signature. Found: "used" (or "unused" if it landed and failed: nothing
74
+ * moved, and it cannot land again). Not found: "unused" when H is past the last valid height
75
+ * (any landing would then be final and in that history), else "pending".
76
+ * - Anything that cannot be established that way throws (the CLI shows "error"); it is never
77
+ * "unused". Lines from before 0.4.7 have no signature: only the server's transaction can prove
78
+ * them ("used"), else "unknown".
79
+ */
80
+ export declare function checkSolanaPayment(e: SolanaCheck, url?: string, f?: typeof fetch): Promise<"used" | "unused" | "pending" | "unknown">;
30
81
  /** USDC (6 decimals) and SOL balances of a wallet. A wallet that never held USDC has no token
31
82
  * account: 0. Read with getAccountInfo, which free public RPCs serve (getTokenAccountBalance is
32
83
  * an "indexed" call some of them reserve for paid keys). */
@@ -10,7 +10,7 @@ export type Signer = {
10
10
  message: any;
11
11
  }) => Promise<Hex>;
12
12
  };
13
- export type ChainKey = "robinhood" | "base" | "arbitrum" | "polygon" | "solana";
13
+ export type ChainKey = "robinhood" | "base" | "arbitrum" | "polygon" | "arc" | "soneium" | "solana";
14
14
  export type Chain = {
15
15
  key: ChainKey;
16
16
  vm: "evm" | "svm";
@@ -98,6 +98,7 @@ export declare function signPayment(signer: Signer, req: Requirement, resource:
98
98
  name: string;
99
99
  value: string;
100
100
  auth?: SignedAuth;
101
+ sol?: SignedSol;
101
102
  }>;
102
103
  /** Settlement details a server returns after a paid call, when it sends them. */
103
104
  export declare function readSettlement(res: Response): {
@@ -111,10 +112,19 @@ export type SignedAuth = {
111
112
  nonce: Hex;
112
113
  validBefore: number;
113
114
  };
115
+ /** What a Solana payment can be found by on-chain later (see checkSolanaPayment): our signature, its
116
+ * memo, its blockhash and the slot that blockhash was read at. */
117
+ export type SignedSol = {
118
+ sig: string;
119
+ memo: string;
120
+ blockhash: string;
121
+ slot?: number;
122
+ };
114
123
  export type Signed = {
115
124
  req: Requirement;
116
125
  from: string;
117
126
  auth?: SignedAuth;
127
+ sol?: SignedSol;
118
128
  token?: {
119
129
  amount: string;
120
130
  };
@@ -31,6 +31,11 @@ export type SpendEntry = {
31
31
  /** EVM payments: the EIP-3009 nonce and expiry, to check on-chain whether it was ever used. */
32
32
  nonce?: string;
33
33
  validBefore?: number;
34
+ /** Solana payments (from 0.4.7): our signature, the memo, the blockhash and the slot it was read at. */
35
+ sig?: string;
36
+ memo?: string;
37
+ blockhash?: string;
38
+ slot?: number;
34
39
  /** What the server said it charged, for the record only. */
35
40
  reported?: number | null;
36
41
  };
@@ -56,7 +61,7 @@ export type WafermapOptions = {
56
61
  solanaAddress?: string;
57
62
  /** Default https://wafermap.xyz */
58
63
  baseUrl?: string;
59
- /** Preferred payment network. Default robinhood (USDG); base, arbitrum, polygon and solana pay in USDC. */
64
+ /** Preferred payment network. Default robinhood (USDG); base, arbitrum, polygon, arc, soneium and solana pay in USDC. */
60
65
  network?: Net;
61
66
  limits?: Partial<Limits>;
62
67
  /** Where spending is remembered for the daily cap. Default: in memory. */
@@ -87,10 +92,11 @@ export declare function mergeLog(entries: SpendEntry[]): SpendEntry[];
87
92
  * can never be charged. authorizationState is also true after cancelAuthorization, so a true is
88
93
  * confirmed by the AuthorizationUsed event: first in the receipt of the transaction the server
89
94
  * named, else in the blocks between signing and expiry, the only time it could have executed.
90
- * None there means the nonce was cancelled, never executed ("cancelled"). Solana payments and old
91
- * log lines without a nonce come back "unknown"; an RPC failure comes back "error".
95
+ * None there means the nonce was cancelled, never executed ("cancelled"). Solana payments are
96
+ * checked by checkSolanaPayment (shared/solana.ts), by default on the public Solana RPC. Old log
97
+ * lines with nothing to check by come back "unknown"; an RPC failure comes back "error".
92
98
  */
93
- export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore"> & {
99
+ export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore"> & Partial<Pick<SpendEntry, "sig" | "memo" | "blockhash" | "slot" | "usd">> & {
94
100
  at?: number;
95
101
  tx?: string | null;
96
102
  }, rpc?: string): Promise<"used" | "cancelled" | "unused" | "pending" | "unknown" | "error">;
@@ -3,4 +3,6 @@ export { PASS_HEADER, passMessage, encodePass, decodePass, makePass } from "../s
3
3
  export type { Approval, Limits, Net, PaymentStatus, SpendEntry, SwapQuote, Token, WafermapOptions } from "./core.js";
4
4
  export { x402fetch, readQuote, choose, signPayment, signTokenPayment, wafermapPriceUsd, PAY_CHAINS, MAX_VALIDITY_SECONDS, WAFERMAP_RELAYERS, WAFERMAP_TOKEN, PERMIT2, X402Error } from "../shared/x402client";
5
5
  export { quoteSwap, executeSwap, decodeSwap, KYBER_ROUTER, SwapError } from "../shared/swap";
6
- export type { Requirement, Quote, Signed, SignedAuth, Signer } from "../shared/x402client";
6
+ export type { Requirement, Quote, Signed, SignedAuth, SignedSol, Signer } from "../shared/x402client";
7
+ export { checkSolanaPayment, SOLANA_PUBLIC_RPC } from "../shared/solana";
8
+ export type { SolanaCheck } from "../shared/solana";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wafermapxyz/cli",
3
- "version": "0.4.6",
4
- "description": "Wafermap for agents: an encrypted local wallet that pays x402 resources, discovers and pays third-party x402 services (USDG on Robinhood Chain; USDC on Solana, Base, Arbitrum, Polygon), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
3
+ "version": "0.4.8",
4
+ "description": "Wafermap for agents: an encrypted local wallet that pays x402 resources, discovers and pays third-party x402 services (USDG on Robinhood Chain; USDC on Solana, Base, Arbitrum, Polygon, Arc, Soneium), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "wafermap": "dist/cli.js"
@@ -36,6 +36,8 @@
36
36
  "base",
37
37
  "arbitrum",
38
38
  "polygon",
39
+ "arc",
40
+ "soneium",
39
41
  "solana",
40
42
  "usdg",
41
43
  "usdc"