@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 +9 -0
- package/README.md +1 -1
- package/dist/cli.js +60 -7
- package/dist/index.js +46 -3
- package/dist/types/src/core.d.ts +9 -3
- package/package.json +1 -1
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 "
|
|
581
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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 "
|
|
540
|
-
|
|
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
|
}
|
package/dist/types/src/core.d.ts
CHANGED
|
@@ -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.
|
|
88
|
-
*
|
|
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"
|
|
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.
|
|
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": {
|