@wafermapxyz/cli 0.4.5 → 0.4.6

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.6 (3 Nov 2026)
4
+
5
+ **`history --verify` tells an executed payment from a cancelled one.** Thank you @GBLIN_Protocol for the catch.
6
+
7
+ - EIP-3009 `authorizationState` is true both after a payment is executed and after its nonce is cancelled (`cancelAuthorization`), so a cancelled nonce used to read as `used`. The wallet never cancels on its own, but a nonce cancelled by hand would have been shown as paid.
8
+ - A `true` is now confirmed by the token contract's `AuthorizationUsed` event for that payer and nonce: first in the receipt of the settlement transaction the server named, otherwise in the blocks between signing and expiry, the only time the payment could have executed. Found: `used`. Not found: `cancelled` (spent nonce, no transfer). If the RPC cannot answer, the result is `error`, never a guess. Base's public RPC refuses log searches, so a Base payment with no settlement transaction on record needs `WAFERMAP_RPC_BASE` set to another RPC.
9
+ - `history --verify` checks four payments at a time instead of all at once, so public RPCs stop refusing long histories.
10
+ - SDK: `checkPayment()` returns the new `"cancelled"` state and takes the entry's `at` and `tx` when given.
11
+
3
12
  ## 0.4.5 (2 Nov 2026)
4
13
 
5
14
  **Pay Wafermap in $WAFERMAP, at 50% off. The tokens are burned.**
package/README.md CHANGED
@@ -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). 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 not checked yet. 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
@@ -560,7 +560,7 @@ __export(core_exports, {
560
560
  listResources: () => listResources,
561
561
  mergeLog: () => mergeLog
562
562
  });
563
- import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http } from "viem";
563
+ import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http, pad, parseAbiItem, toEventSelector } from "viem";
564
564
  import { privateKeyToAccount } from "viem/accounts";
565
565
  import { arbitrum, base, polygon, robinhood } from "viem/chains";
566
566
  function mergeLog(entries) {
@@ -571,14 +571,56 @@ function mergeLog(entries) {
571
571
  return { ...e, status: o.status, tx: o.tx ?? e.tx, reported: o.reported ?? null, ...o.status === "aborted" ? { usd: 0 } : {} };
572
572
  });
573
573
  }
574
+ async function blockAt(client, ts, side, latest) {
575
+ if (ts >= Number(latest.timestamp)) return latest.number;
576
+ let hi = latest;
577
+ let lo = await client.getBlock({ blockNumber: latest.number > 100000n ? latest.number - 100000n : 0n });
578
+ while (Number(lo.timestamp) > ts && lo.number > 0n) {
579
+ const back = (hi.number - lo.number) * 4n;
580
+ hi = lo;
581
+ lo = await client.getBlock({ blockNumber: lo.number > back ? lo.number - back : 0n });
582
+ }
583
+ for (let i = 0; i < 20 && hi.number - lo.number > 20n; i++) {
584
+ const span = Number(hi.timestamp - lo.timestamp) || 1;
585
+ let guess = lo.number + BigInt(Math.floor(Number(hi.number - lo.number) * (ts - Number(lo.timestamp)) / span));
586
+ if (guess <= lo.number) guess = lo.number + 1n;
587
+ if (guess >= hi.number) guess = hi.number - 1n;
588
+ const b = await client.getBlock({ blockNumber: guess });
589
+ if (Number(b.timestamp) <= ts) lo = b;
590
+ else hi = b;
591
+ }
592
+ return side === "from" ? lo.number : hi.number;
593
+ }
574
594
  async function checkPayment(e, rpc) {
575
595
  if (e.network === "solana" || !e.nonce || !(e.network in CHAINS)) return "unknown";
576
596
  const chain = PAY_CHAINS.find((c) => c.key === e.network);
577
597
  try {
578
598
  const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
579
599
  const used = await client.readContract({ address: chain.stable, abi: AUTH_STATE_ABI, functionName: "authorizationState", args: [e.payer, e.nonce] });
580
- if (used) return "used";
581
- return e.validBefore && Date.now() / 1e3 > e.validBefore ? "unused" : "pending";
600
+ if (!used) return e.validBefore && Date.now() / 1e3 > e.validBefore ? "unused" : "pending";
601
+ const isOurs = (l) => l.address.toLowerCase() === chain.stable.toLowerCase() && l.topics[0] === toEventSelector(AUTH_USED) && l.topics[1]?.toLowerCase() === pad(e.payer).toLowerCase() && l.topics[2]?.toLowerCase() === e.nonce.toLowerCase();
602
+ if (e.tx && /^0x[0-9a-fA-F]{64}$/.test(e.tx)) {
603
+ const receipt = await client.getTransactionReceipt({ hash: e.tx }).catch(() => null);
604
+ if (receipt?.status === "success" && receipt.logs.some(isOurs)) return "used";
605
+ }
606
+ if (!e.validBefore || !e.at) return "error";
607
+ const latest = await client.getBlock();
608
+ const fromBlock = await blockAt(client, Math.floor(e.at / 1e3) - 60, "from", latest);
609
+ const toBlock = await blockAt(client, e.validBefore + 30, "to", latest);
610
+ for (let start = fromBlock; start <= toBlock; start += 50n) {
611
+ const end = start + 49n < toBlock ? start + 49n : toBlock;
612
+ for (let attempt = 0; ; attempt++) {
613
+ try {
614
+ const logs = await client.getLogs({ address: chain.stable, event: AUTH_USED, args: { authorizer: e.payer, nonce: e.nonce }, fromBlock: start, toBlock: end });
615
+ if (logs.length) return "used";
616
+ break;
617
+ } catch (err) {
618
+ if (attempt >= 3) throw err;
619
+ await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)));
620
+ }
621
+ }
622
+ }
623
+ return "cancelled";
582
624
  } catch {
583
625
  return "error";
584
626
  }
@@ -606,7 +648,7 @@ async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAU
606
648
  if (!r.ok || !Array.isArray(j?.data)) throw new WafermapError("network_error", `${host} returned no catalog (HTTP ${r.status}).`);
607
649
  return j.data;
608
650
  }
609
- var NETS, isNet, isSwapNet, WafermapError, CHAINS, AUTH_STATE_ABI, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, timedOut, Wafermap;
651
+ var NETS, isNet, isSwapNet, WafermapError, CHAINS, AUTH_STATE_ABI, AUTH_USED, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, timedOut, Wafermap;
610
652
  var init_core = __esm({
611
653
  "src/core.ts"() {
612
654
  "use strict";
@@ -626,6 +668,7 @@ var init_core = __esm({
626
668
  };
627
669
  CHAINS = { robinhood, base, arbitrum, polygon };
628
670
  AUTH_STATE_ABI = [{ type: "function", name: "authorizationState", stateMutability: "view", inputs: [{ name: "authorizer", type: "address" }, { name: "nonce", type: "bytes32" }], outputs: [{ type: "bool" }] }];
671
+ AUTH_USED = parseAbiItem("event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)");
629
672
  DEFAULT_LIMITS = { perPayment: 0.1, perDay: 1, perSwap: 25 };
630
673
  DEFAULT_TIMEOUT_MS = 6e4;
631
674
  timedOut = (e) => e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError";
@@ -1119,7 +1162,7 @@ var VERSION;
1119
1162
  var init_version = __esm({
1120
1163
  "src/version.ts"() {
1121
1164
  "use strict";
1122
- VERSION = "0.4.5";
1165
+ VERSION = "0.4.6";
1123
1166
  }
1124
1167
  });
1125
1168
 
@@ -1870,7 +1913,16 @@ async function main() {
1870
1913
  if (!rows.length) return console.log(`Nothing in the last ${days} days.`);
1871
1914
  const verify = a.flags.verify !== void 0;
1872
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 };
1873
- const chain = verify ? await Promise.all(rows.map((e) => e.kind === "swap" ? null : checkPayment(e, RPC[e.network]))) : [];
1916
+ const chain = [];
1917
+ if (verify) {
1918
+ let next = 0;
1919
+ await Promise.all(Array.from({ length: 4 }, async () => {
1920
+ while (next < rows.length) {
1921
+ const i = next++;
1922
+ chain[i] = rows[i].kind === "swap" ? null : await checkPayment(rows[i], RPC[rows[i].network]);
1923
+ }
1924
+ }));
1925
+ }
1874
1926
  rows.forEach((e, i) => {
1875
1927
  const state = [e.status, chain[i] ? `on-chain: ${chain[i]}` : null].filter(Boolean).join(", ");
1876
1928
  console.log(`${new Date(e.at).toISOString().slice(0, 16).replace("T", " ")} ${e.kind.padEnd(8)} ${e.kind === "swap" ? " " : `$${e.usd}`.padEnd(8)} ${e.network.padEnd(9)} ${e.what}${state ? ` [${state}]` : ""}${e.tx ? ` ${EXPLORER(e.network)}/tx/${e.tx}` : ""}`);
@@ -1881,7 +1933,8 @@ async function main() {
1881
1933
  Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment counts at its full signed amount; older lines show what the server reported.`);
1882
1934
  if (verify) {
1883
1935
  const never = rows.filter((e, i) => e.kind !== "swap" && chain[i] === "unused").reduce((s, e) => s + e.usd, 0);
1884
- console.log(`On-chain: $${never.toFixed(4)} of that was signed but never executed and has expired. "unknown" = Solana or an older log line; "error" = the RPC did not answer, try again.`);
1936
+ 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).`);
1885
1938
  } else console.log("Add --verify to check on-chain which signed payments were executed.");
1886
1939
  return;
1887
1940
  }
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/core.ts
2
- import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http } from "viem";
2
+ import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http, pad, parseAbiItem, toEventSelector } from "viem";
3
3
  import { privateKeyToAccount } from "viem/accounts";
4
4
  import { arbitrum, base, polygon, robinhood } from "viem/chains";
5
5
 
@@ -530,14 +530,57 @@ function mergeLog(entries) {
530
530
  return { ...e, status: o.status, tx: o.tx ?? e.tx, reported: o.reported ?? null, ...o.status === "aborted" ? { usd: 0 } : {} };
531
531
  });
532
532
  }
533
+ async function blockAt(client, ts, side, latest) {
534
+ if (ts >= Number(latest.timestamp)) return latest.number;
535
+ let hi = latest;
536
+ let lo = await client.getBlock({ blockNumber: latest.number > 100000n ? latest.number - 100000n : 0n });
537
+ while (Number(lo.timestamp) > ts && lo.number > 0n) {
538
+ const back = (hi.number - lo.number) * 4n;
539
+ hi = lo;
540
+ lo = await client.getBlock({ blockNumber: lo.number > back ? lo.number - back : 0n });
541
+ }
542
+ for (let i = 0; i < 20 && hi.number - lo.number > 20n; i++) {
543
+ const span = Number(hi.timestamp - lo.timestamp) || 1;
544
+ let guess = lo.number + BigInt(Math.floor(Number(hi.number - lo.number) * (ts - Number(lo.timestamp)) / span));
545
+ if (guess <= lo.number) guess = lo.number + 1n;
546
+ if (guess >= hi.number) guess = hi.number - 1n;
547
+ const b = await client.getBlock({ blockNumber: guess });
548
+ if (Number(b.timestamp) <= ts) lo = b;
549
+ else hi = b;
550
+ }
551
+ return side === "from" ? lo.number : hi.number;
552
+ }
553
+ var AUTH_USED = parseAbiItem("event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)");
533
554
  async function checkPayment(e, rpc) {
534
555
  if (e.network === "solana" || !e.nonce || !(e.network in CHAINS)) return "unknown";
535
556
  const chain = PAY_CHAINS.find((c) => c.key === e.network);
536
557
  try {
537
558
  const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
538
559
  const used = await client.readContract({ address: chain.stable, abi: AUTH_STATE_ABI, functionName: "authorizationState", args: [e.payer, e.nonce] });
539
- if (used) return "used";
540
- return e.validBefore && Date.now() / 1e3 > e.validBefore ? "unused" : "pending";
560
+ if (!used) return e.validBefore && Date.now() / 1e3 > e.validBefore ? "unused" : "pending";
561
+ const isOurs = (l) => l.address.toLowerCase() === chain.stable.toLowerCase() && l.topics[0] === toEventSelector(AUTH_USED) && l.topics[1]?.toLowerCase() === pad(e.payer).toLowerCase() && l.topics[2]?.toLowerCase() === e.nonce.toLowerCase();
562
+ if (e.tx && /^0x[0-9a-fA-F]{64}$/.test(e.tx)) {
563
+ const receipt = await client.getTransactionReceipt({ hash: e.tx }).catch(() => null);
564
+ if (receipt?.status === "success" && receipt.logs.some(isOurs)) return "used";
565
+ }
566
+ if (!e.validBefore || !e.at) return "error";
567
+ const latest = await client.getBlock();
568
+ const fromBlock = await blockAt(client, Math.floor(e.at / 1e3) - 60, "from", latest);
569
+ const toBlock = await blockAt(client, e.validBefore + 30, "to", latest);
570
+ for (let start = fromBlock; start <= toBlock; start += 50n) {
571
+ const end = start + 49n < toBlock ? start + 49n : toBlock;
572
+ for (let attempt = 0; ; attempt++) {
573
+ try {
574
+ const logs = await client.getLogs({ address: chain.stable, event: AUTH_USED, args: { authorizer: e.payer, nonce: e.nonce }, fromBlock: start, toBlock: end });
575
+ if (logs.length) return "used";
576
+ break;
577
+ } catch (err) {
578
+ if (attempt >= 3) throw err;
579
+ await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)));
580
+ }
581
+ }
582
+ }
583
+ return "cancelled";
541
584
  } catch {
542
585
  return "error";
543
586
  }
@@ -84,10 +84,16 @@ export declare function mergeLog(entries: SpendEntry[]): SpendEntry[];
84
84
  /**
85
85
  * Was a signed payment ever executed? Reads the token contract (EIP-3009 authorizationState),
86
86
  * so the answer does not depend on any server and needs no key. "unused" after expiry means it
87
- * can never be charged. Solana payments and old log lines without a nonce come back "unknown"; an RPC
88
- * failure comes back "error".
87
+ * can never be charged. authorizationState is also true after cancelAuthorization, so a true is
88
+ * confirmed by the AuthorizationUsed event: first in the receipt of the transaction the server
89
+ * 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".
89
92
  */
90
- export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore">, rpc?: string): Promise<"used" | "unused" | "pending" | "unknown" | "error">;
93
+ export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore"> & {
94
+ at?: number;
95
+ tx?: string | null;
96
+ }, rpc?: string): Promise<"used" | "cancelled" | "unused" | "pending" | "unknown" | "error">;
91
97
  export declare const DEFAULT_LIMITS: Limits;
92
98
  export declare const DEFAULT_TIMEOUT_MS = 60000;
93
99
  /** What a wallet's $WAFERMAP unlocks (holder price, Pro free calls left today), from /api/holder. Needs no key. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafermapxyz/cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
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.",
5
5
  "type": "module",
6
6
  "bin": {