@wafermapxyz/cli 0.4.10 → 0.4.12
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 +19 -0
- package/README.md +4 -0
- package/dist/cli.js +277 -11
- package/dist/index.js +199 -4
- package/dist/types/shared/x402client.d.ts +34 -0
- package/dist/types/src/core.d.ts +10 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/statement.d.ts +58 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.12 (16 Nov 2026)
|
|
4
|
+
|
|
5
|
+
**`wafermap call <llm> --metered`: pay only the tokens the model used.**
|
|
6
|
+
|
|
7
|
+
- Signs one ceiling (x402 scheme `upto`, a Permit2 transfer of at most that much) instead of the flat price. After the model finishes, Wafermap settles only the input and output tokens it reported, never more than the ceiling and never less than $0.001. The confirmation shows the ceiling; the receipt line shows the token counts, what was charged and what was signed.
|
|
8
|
+
- USDC on `base`, `arbitrum` or `polygon` (pick one with `--network`). A wallet that never approved Permit2 for USDC signs an EIP-2612 permit with the payment, so no gas and no extra transaction is needed.
|
|
9
|
+
- The signature can only be spent by the x402 upto proxy (its address is fixed in the client), only to the quote's recipient and only through the facilitator named in the quote, and it expires after at most 5 minutes. Spending caps count the full ceiling, because that is what was signed.
|
|
10
|
+
- Works with `--stream`. Cannot be combined with `--pay wafermap`.
|
|
11
|
+
- SDK: `call(id, input, { metered: true })`; the receipt carries `metered` (`inputTokens`, `outputTokens`, `estimated`, `ceilingUsd`, `chargedUsd`). MCP: `wafermap_call` takes `metered`.
|
|
12
|
+
|
|
13
|
+
## 0.4.11 (15 Nov 2026)
|
|
14
|
+
|
|
15
|
+
**`wafermap statement`: a monthly statement you can check against the chains.**
|
|
16
|
+
|
|
17
|
+
- `wafermap statement --month 2026-11 --out statement.json` downloads one month of this wallet's payments to Wafermap and to marketplace sellers (EVM and Solana sides together), from https://wafermap.xyz/api/statement. `--csv` gives the accounting version; without `--out` it prints the lines and the total.
|
|
18
|
+
- `wafermap statement --verify statement.json` reads every transaction from its own chain, not from Wafermap: an EVM line passes only if the receipt holds a Transfer of exactly that amount of that token from the payer to the recipient; a Solana line only if the transaction succeeded and moved exactly that amount between those two owners. It also checks that the totals add up to the lines, and exits with code 2 on any mismatch, failed or missing transaction. Public RPCs by default; set `WAFERMAP_RPC_<NETWORK>` to use your own.
|
|
19
|
+
- Works on statements downloaded anywhere: the Wallet page, a team owner's statement from /teams, or the API.
|
|
20
|
+
- SDK: `fetchStatement`, `parseStatement`, `verifyStatement`, `verifyLine`.
|
|
21
|
+
|
|
3
22
|
## 0.4.10 (12 Nov 2026)
|
|
4
23
|
|
|
5
24
|
**`wafermap discover` shows the price on every network a service accepts.**
|
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ wafermap discover weather
|
|
|
33
33
|
wafermap pay https://x402.agentindex.world/weather city=Jakarta
|
|
34
34
|
wafermap swap 0.01 ETH USDG
|
|
35
35
|
wafermap history
|
|
36
|
+
wafermap statement --month 2026-11 --out nov.json
|
|
37
|
+
wafermap statement --verify nov.json
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
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.
|
|
@@ -84,6 +86,8 @@ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `le
|
|
|
84
86
|
|
|
85
87
|
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
88
|
|
|
89
|
+
LLM resources can also be **metered**: `wafermap call llama-4-scout prompt="hi" --metered --network base` signs a ceiling (shown before you confirm) and pays only the tokens the model used, at least $0.001, in USDC on Base, Arbitrum or Polygon. No gas needed. Caps count the ceiling. Details: https://wafermap.xyz/docs#metered
|
|
90
|
+
|
|
87
91
|
LLM resources can **stream**: `wafermap call gemma-4 prompt="hi" --stream` prints the answer as it arrives, and the payment settles only once the model finishes.
|
|
88
92
|
|
|
89
93
|
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.
|
package/dist/cli.js
CHANGED
|
@@ -374,6 +374,36 @@ var init_solana = __esm({
|
|
|
374
374
|
});
|
|
375
375
|
|
|
376
376
|
// shared/x402client.ts
|
|
377
|
+
async function signUptoPayment(signer, req, resource, opts = {}) {
|
|
378
|
+
const fac = String(req.raw?.extra?.facilitatorAddress ?? "");
|
|
379
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(fac)) throw new X402Error("bad_quote", "The metered quote names no facilitator. Nothing was signed.");
|
|
380
|
+
const nonce = BigInt(randomNonce()) >> 8n;
|
|
381
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_METERED_SECONDS));
|
|
382
|
+
const max = BigInt(req.amount);
|
|
383
|
+
const witness = { to: req.payTo, facilitator: fac, validAfter: 0n };
|
|
384
|
+
let signature;
|
|
385
|
+
try {
|
|
386
|
+
signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: req.chain.id, verifyingContract: PERMIT2 }, types: UPTO_TYPES, primaryType: "PermitWitnessTransferFrom", message: { permitted: { token: req.asset, amount: max }, spender: UPTO_PROXY, nonce, deadline, witness } });
|
|
387
|
+
} catch (e) {
|
|
388
|
+
throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
|
|
389
|
+
}
|
|
390
|
+
let extensions;
|
|
391
|
+
if (opts.gasless && opts.reader) {
|
|
392
|
+
const allowance = await opts.reader.allowance(req.chain, signer.address, req.asset).catch(() => 0n);
|
|
393
|
+
if (allowance < max) {
|
|
394
|
+
const pn = await opts.reader.permitNonce(req.chain, signer.address, req.asset);
|
|
395
|
+
let ps;
|
|
396
|
+
try {
|
|
397
|
+
ps = await signer.signTypedData({ domain: { name: req.extra.name, version: req.extra.version, chainId: req.chain.id, verifyingContract: req.asset }, types: EIP2612_TYPES, primaryType: "Permit", message: { owner: signer.address, spender: PERMIT2, value: max, nonce: pn, deadline } });
|
|
398
|
+
} catch (e) {
|
|
399
|
+
throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
|
|
400
|
+
}
|
|
401
|
+
extensions = { eip2612GasSponsoring: { info: { from: signer.address, asset: req.asset, spender: PERMIT2, amount: max.toString(), nonce: pn.toString(), deadline: deadline.toString(), signature: ps, version: "1" } } };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const permit2Authorization = { from: signer.address, permitted: { token: req.asset, amount: req.amount }, spender: UPTO_PROXY, nonce: nonce.toString(), deadline: deadline.toString(), witness: { to: req.payTo, facilitator: fac, validAfter: "0" } };
|
|
405
|
+
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { signature, permit2Authorization }, ...extensions ? { extensions } : {} })), deadline: Number(deadline) };
|
|
406
|
+
}
|
|
377
407
|
async function wafermapPriceUsd(f = fetch) {
|
|
378
408
|
const res = await f(`https://api.geckoterminal.com/api/v2/simple/networks/robinhood/token_price/${WAFERMAP_TOKEN}`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15e3) });
|
|
379
409
|
const j = await res.json().catch(() => null);
|
|
@@ -421,12 +451,19 @@ async function readQuote(res) {
|
|
|
421
451
|
const accepts = [];
|
|
422
452
|
const offered = [];
|
|
423
453
|
let token;
|
|
454
|
+
const metered = [];
|
|
424
455
|
for (const r of body.accepts) {
|
|
425
456
|
offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
|
|
426
457
|
if (r?.scheme === TOKEN_SCHEME) {
|
|
427
458
|
token ??= tokenOption(r);
|
|
428
459
|
continue;
|
|
429
460
|
}
|
|
461
|
+
if (r?.scheme === "upto" && version === 2) {
|
|
462
|
+
const ch = chainOf(r.network);
|
|
463
|
+
if (ch && ch.vm === "evm" && String(r.asset ?? "").toLowerCase() === ch.stable.toLowerCase() && /^\d+$/.test(String(r.amount)) && /^0x[0-9a-fA-F]{40}$/.test(String(r.extra?.facilitatorAddress ?? "")) && /^0x[0-9a-fA-F]{40}$/.test(String(r.payTo ?? "")))
|
|
464
|
+
metered.push({ version, chain: ch, amount: String(r.amount), usd: Number(r.amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra: r.extra?.name && r.extra?.version ? { name: String(r.extra.name), version: String(r.extra.version) } : ch.domain, raw: r });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
430
467
|
const chain = chainOf(r.network);
|
|
431
468
|
if (!chain || r.scheme !== "exact") continue;
|
|
432
469
|
if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
|
|
@@ -440,7 +477,10 @@ async function readQuote(res) {
|
|
|
440
477
|
accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
|
|
441
478
|
}
|
|
442
479
|
const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
|
|
443
|
-
|
|
480
|
+
const m = body.metered;
|
|
481
|
+
const meterInfo = m && Number.isFinite(Number(m.ceilingUsd)) ? { inputPer1kTokensUsd: Number(m.inputPer1kTokensUsd), outputPer1kTokensUsd: Number(m.outputPer1kTokensUsd), ceilingUsd: Number(m.ceilingUsd), minimumUsd: Number(m.minimumUsd) } : void 0;
|
|
482
|
+
const gasless = !!body.extensions?.eip2612GasSponsoring;
|
|
483
|
+
return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {}, ...metered.length ? { metered: metered.map((q) => ({ ...q, gasless })) } : {}, ...meterInfo ? { meterInfo } : {} };
|
|
444
484
|
}
|
|
445
485
|
function choose(q, prefer = CHAIN_KEYS) {
|
|
446
486
|
for (const k of prefer) {
|
|
@@ -512,6 +552,20 @@ async function x402fetch(url, init, opts) {
|
|
|
512
552
|
const res2 = await f(url, { ...init, headers: headers2 });
|
|
513
553
|
return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
|
|
514
554
|
}
|
|
555
|
+
if (opts.meter) {
|
|
556
|
+
const pref = (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana");
|
|
557
|
+
const req2 = pref.map((k) => quote.metered?.find((q) => q.chain.key === k)).find(Boolean);
|
|
558
|
+
if (!req2) throw new X402Error("metered_not_offered", quote.metered?.length ? `Metered calls are offered on ${quote.metered.map((q) => q.chain.name).join(", ")}; pick one of those networks.` : "This call is not offered as a metered (pay per token) call. Pay the flat price instead.");
|
|
559
|
+
if (opts.approve) await opts.approve(req2);
|
|
560
|
+
const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
|
|
561
|
+
const header2 = await signUptoPayment(signer2, req2, quote.resource, { reader: opts.reader, gasless: !!req2.gasless });
|
|
562
|
+
const signed2 = { req: req2, from: signer2.address, metered: { ceiling: req2.amount, deadline: header2.deadline } };
|
|
563
|
+
opts.onSigned?.(signed2);
|
|
564
|
+
const headers2 = new Headers(init.headers);
|
|
565
|
+
headers2.set(header2.name, header2.value);
|
|
566
|
+
const res2 = await f(url, { ...init, headers: headers2 });
|
|
567
|
+
return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
|
|
568
|
+
}
|
|
515
569
|
const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
|
|
516
570
|
if (opts.approve) await opts.approve(req);
|
|
517
571
|
const svm = req.chain.vm === "svm";
|
|
@@ -524,7 +578,7 @@ async function x402fetch(url, init, opts) {
|
|
|
524
578
|
const res = await f(url, { ...init, headers });
|
|
525
579
|
return { res, paid: { ...signed, tx: readSettlement(res)?.tx ?? null } };
|
|
526
580
|
}
|
|
527
|
-
var USDC_DOMAIN, PAY_CHAINS, EVM_CHAINS, CHAIN_KEYS, WAFERMAP_TOKEN, PERMIT2, DEAD, TOKEN_SCHEME, WAFERMAP_RELAYERS, TOKEN_VALUE_SLACK, PERMIT_TYPES, X402Error, b64encode, b64decode, TYPES, MAX_VALIDITY_SECONDS, randomNonce;
|
|
581
|
+
var USDC_DOMAIN, PAY_CHAINS, EVM_CHAINS, CHAIN_KEYS, UPTO_PROXY, MAX_METERED_SECONDS, UPTO_TYPES, EIP2612_TYPES, WAFERMAP_TOKEN, PERMIT2, DEAD, TOKEN_SCHEME, WAFERMAP_RELAYERS, TOKEN_VALUE_SLACK, PERMIT_TYPES, X402Error, b64encode, b64decode, TYPES, MAX_VALIDITY_SECONDS, randomNonce;
|
|
528
582
|
var init_x402client = __esm({
|
|
529
583
|
"shared/x402client.ts"() {
|
|
530
584
|
"use strict";
|
|
@@ -542,6 +596,35 @@ var init_x402client = __esm({
|
|
|
542
596
|
];
|
|
543
597
|
EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
|
|
544
598
|
CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
|
|
599
|
+
UPTO_PROXY = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002";
|
|
600
|
+
MAX_METERED_SECONDS = 300;
|
|
601
|
+
UPTO_TYPES = {
|
|
602
|
+
PermitWitnessTransferFrom: [
|
|
603
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
604
|
+
{ name: "spender", type: "address" },
|
|
605
|
+
{ name: "nonce", type: "uint256" },
|
|
606
|
+
{ name: "deadline", type: "uint256" },
|
|
607
|
+
{ name: "witness", type: "Witness" }
|
|
608
|
+
],
|
|
609
|
+
TokenPermissions: [
|
|
610
|
+
{ name: "token", type: "address" },
|
|
611
|
+
{ name: "amount", type: "uint256" }
|
|
612
|
+
],
|
|
613
|
+
Witness: [
|
|
614
|
+
{ name: "to", type: "address" },
|
|
615
|
+
{ name: "facilitator", type: "address" },
|
|
616
|
+
{ name: "validAfter", type: "uint256" }
|
|
617
|
+
]
|
|
618
|
+
};
|
|
619
|
+
EIP2612_TYPES = {
|
|
620
|
+
Permit: [
|
|
621
|
+
{ name: "owner", type: "address" },
|
|
622
|
+
{ name: "spender", type: "address" },
|
|
623
|
+
{ name: "value", type: "uint256" },
|
|
624
|
+
{ name: "nonce", type: "uint256" },
|
|
625
|
+
{ name: "deadline", type: "uint256" }
|
|
626
|
+
]
|
|
627
|
+
};
|
|
545
628
|
WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
|
|
546
629
|
PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
547
630
|
DEAD = "0x000000000000000000000000000000000000dEaD";
|
|
@@ -858,7 +941,15 @@ var init_core = __esm({
|
|
|
858
941
|
this.queue = prev.then(() => held);
|
|
859
942
|
return prev.then(() => release);
|
|
860
943
|
}
|
|
861
|
-
|
|
944
|
+
/** Chain reads for a metered payment's EIP-2612 permit: Permit2 allowance and the token's permit nonce. */
|
|
945
|
+
reader() {
|
|
946
|
+
const nonces = [{ type: "function", name: "nonces", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] }];
|
|
947
|
+
return {
|
|
948
|
+
allowance: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: erc20Abi2, functionName: "allowance", args: [owner, PERMIT2] }),
|
|
949
|
+
permitNonce: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: nonces, functionName: "nonces", args: [owner] })
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
async paidFetch(kind, what, url, init, network, payIn, meter) {
|
|
862
953
|
let id = null;
|
|
863
954
|
let release = null;
|
|
864
955
|
const unlock = () => {
|
|
@@ -872,10 +963,12 @@ var init_core = __esm({
|
|
|
872
963
|
prefer: this.prefer(network),
|
|
873
964
|
fetch: this.fetch,
|
|
874
965
|
payIn,
|
|
966
|
+
meter,
|
|
967
|
+
reader: meter ? this.reader() : void 0,
|
|
875
968
|
tokenPriceUsd: this.tokenPriceUsd,
|
|
876
969
|
approve: async (req) => {
|
|
877
970
|
release = await this.acquire();
|
|
878
|
-
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : `${req.chain.symbol} to ${req.payTo}` });
|
|
971
|
+
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : meter ? `at most ${req.chain.symbol} ${req.usd}, charged for the tokens used, to ${req.payTo}` : `${req.chain.symbol} to ${req.payTo}` });
|
|
879
972
|
},
|
|
880
973
|
onSigned: (s) => {
|
|
881
974
|
id = this.booked(kind, what, s);
|
|
@@ -963,11 +1056,12 @@ var init_core = __esm({
|
|
|
963
1056
|
}
|
|
964
1057
|
async call(id, input = {}, o = {}) {
|
|
965
1058
|
const inToken = o.payIn === "wafermap";
|
|
1059
|
+
if (o.metered && inToken) throw new WafermapError("bad_options", "A metered call is paid in USDC; it cannot also be paid in $WAFERMAP.");
|
|
966
1060
|
const stream = !!o.onDelta && !inToken;
|
|
967
1061
|
if (inToken && id.startsWith("m/")) throw new WafermapError("token_not_offered", "Marketplace listings are paid in USDG or USDC to their seller, not in $WAFERMAP.");
|
|
968
1062
|
if (inToken && !await this.wafermapApproved().catch(() => true)) throw new WafermapError("permit2_not_approved", "Paying in $WAFERMAP needs a one-time approval of Permit2. Run `wafermap approve-wafermap` (one transaction on Robinhood Chain, paid in ETH), then call again.");
|
|
969
1063
|
const pass = id.startsWith("m/") ? null : await this.holderPass();
|
|
970
|
-
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
|
|
1064
|
+
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0, o.metered);
|
|
971
1065
|
const settle = (r2, out2) => {
|
|
972
1066
|
if (!r2.paid || !r2.id) return;
|
|
973
1067
|
const status = r2.res.status === 402 ? "refused" : r2.res.ok && !out2?.streamError ? "ok" : "failed";
|
|
@@ -1288,7 +1382,119 @@ var VERSION;
|
|
|
1288
1382
|
var init_version = __esm({
|
|
1289
1383
|
"src/version.ts"() {
|
|
1290
1384
|
"use strict";
|
|
1291
|
-
VERSION = "0.4.
|
|
1385
|
+
VERSION = "0.4.12";
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// src/statement.ts
|
|
1390
|
+
var statement_exports = {};
|
|
1391
|
+
__export(statement_exports, {
|
|
1392
|
+
defaultClients: () => defaultClients,
|
|
1393
|
+
fetchStatement: () => fetchStatement,
|
|
1394
|
+
parseStatement: () => parseStatement,
|
|
1395
|
+
verifyLine: () => verifyLine,
|
|
1396
|
+
verifyStatement: () => verifyStatement
|
|
1397
|
+
});
|
|
1398
|
+
import { createPublicClient as createPublicClient2, getAddress as getAddress3, http as http2, isAddress as isAddress3, parseAbiItem as parseAbiItem2, toEventSelector as toEventSelector2 } from "viem";
|
|
1399
|
+
import { arbitrum as arbitrum2, arc as arc2, base as base2, polygon as polygon2, robinhood as robinhood2, soneium as soneium2 } from "viem/chains";
|
|
1400
|
+
async function fetchStatement(wallets, month, opts = {}) {
|
|
1401
|
+
const url = `${(opts.baseUrl ?? "https://wafermap.xyz").replace(/\/$/, "")}/api/statement?wallet=${encodeURIComponent(wallets.join(","))}&month=${encodeURIComponent(month)}${opts.csv ? "&format=csv" : ""}`;
|
|
1402
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 6e4) });
|
|
1403
|
+
if (!r.ok) {
|
|
1404
|
+
const j = await r.json().catch(() => null);
|
|
1405
|
+
throw new Error(j?.error?.message ?? `Statement request failed (${r.status}).`);
|
|
1406
|
+
}
|
|
1407
|
+
return opts.csv ? r.text() : await r.json();
|
|
1408
|
+
}
|
|
1409
|
+
function parseStatement(text2) {
|
|
1410
|
+
let s;
|
|
1411
|
+
try {
|
|
1412
|
+
s = JSON.parse(text2);
|
|
1413
|
+
} catch {
|
|
1414
|
+
throw new Error("That file is not JSON. Download the JSON statement (not the CSV) to verify it.");
|
|
1415
|
+
}
|
|
1416
|
+
if (s?.kind !== "wafermap-statement" || !Array.isArray(s.lines)) throw new Error("That file is not a Wafermap statement.");
|
|
1417
|
+
return s;
|
|
1418
|
+
}
|
|
1419
|
+
function defaultClients(env = process.env) {
|
|
1420
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1421
|
+
return {
|
|
1422
|
+
evm: (network) => {
|
|
1423
|
+
const chain = EVM[network];
|
|
1424
|
+
if (!chain) return null;
|
|
1425
|
+
if (!cache.has(network)) cache.set(network, createPublicClient2({ chain, transport: http2(env[ENV_RPC[network]] || void 0, { timeout: 3e4 }) }));
|
|
1426
|
+
return cache.get(network);
|
|
1427
|
+
},
|
|
1428
|
+
solana: (method, params) => solanaRpc(env.WAFERMAP_RPC_SOLANA || SOLANA_PUBLIC_RPC, method, params)
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
async function verifyLine(l, clients) {
|
|
1432
|
+
try {
|
|
1433
|
+
if (!/^\d+$/.test(l.amountUnits)) return "mismatch";
|
|
1434
|
+
const want = BigInt(l.amountUnits);
|
|
1435
|
+
if (l.network.startsWith("solana:")) {
|
|
1436
|
+
const tx = await clients.solana("getTransaction", [l.tx, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0, commitment: "finalized" }]);
|
|
1437
|
+
if (!tx) return "not_found";
|
|
1438
|
+
if (tx.meta?.err) return "failed";
|
|
1439
|
+
const delta = (owner) => {
|
|
1440
|
+
const sum = (xs) => (xs ?? []).filter((b) => b.owner === owner && b.mint === l.token).reduce((a, b) => a + BigInt(b.uiTokenAmount?.amount ?? "0"), 0n);
|
|
1441
|
+
return sum(tx.meta?.postTokenBalances) - sum(tx.meta?.preTokenBalances);
|
|
1442
|
+
};
|
|
1443
|
+
return delta(l.from) === -want && delta(l.to) === want ? "ok" : "mismatch";
|
|
1444
|
+
}
|
|
1445
|
+
const client = clients.evm(l.network);
|
|
1446
|
+
if (!client) return "unsupported";
|
|
1447
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(l.tx) || !isAddress3(l.token) || !isAddress3(l.from) || !isAddress3(l.to)) return "mismatch";
|
|
1448
|
+
const receipt = await client.getTransactionReceipt({ hash: l.tx }).catch((e) => /not.*found|could not be found/i.test(String(e?.message)) ? null : Promise.reject(e));
|
|
1449
|
+
if (!receipt) return "not_found";
|
|
1450
|
+
if (receipt.status !== "success") return "failed";
|
|
1451
|
+
const hit = receipt.logs.some(
|
|
1452
|
+
(g) => g.address.toLowerCase() === l.token.toLowerCase() && g.topics[0] === TRANSFER && topicAddr(g.topics[1]) === getAddress3(l.from) && topicAddr(g.topics[2]) === getAddress3(l.to) && BigInt(g.data) === want
|
|
1453
|
+
);
|
|
1454
|
+
return hit ? "ok" : "mismatch";
|
|
1455
|
+
} catch {
|
|
1456
|
+
return "error";
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async function verifyStatement(s, clients = defaultClients(), onLine) {
|
|
1460
|
+
const results = new Array(s.lines.length);
|
|
1461
|
+
let next = 0;
|
|
1462
|
+
await Promise.all(
|
|
1463
|
+
Array.from({ length: 4 }, async () => {
|
|
1464
|
+
while (next < s.lines.length) {
|
|
1465
|
+
const i = next++;
|
|
1466
|
+
results[i] = await verifyLine(s.lines[i], clients);
|
|
1467
|
+
onLine?.(i, results[i]);
|
|
1468
|
+
}
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
const usd = (pick) => Math.round(s.lines.reduce((a, l, i) => a + (pick(results[i]) ? Number(l.amountUsd) : 0), 0) * 1e6) / 1e6;
|
|
1472
|
+
const count = (r) => results.filter((x) => x === r).length;
|
|
1473
|
+
const linesUsd = usd(() => true);
|
|
1474
|
+
return {
|
|
1475
|
+
results,
|
|
1476
|
+
lines: s.lines.length,
|
|
1477
|
+
ok: count("ok"),
|
|
1478
|
+
mismatch: count("mismatch"),
|
|
1479
|
+
failed: count("failed"),
|
|
1480
|
+
notFound: count("not_found"),
|
|
1481
|
+
unsupported: count("unsupported"),
|
|
1482
|
+
error: count("error"),
|
|
1483
|
+
confirmedUsd: usd((r) => r === "ok"),
|
|
1484
|
+
linesUsd,
|
|
1485
|
+
// The totals block must add up to the lines it sits next to.
|
|
1486
|
+
totalsMatch: s.totals?.calls === s.lines.length && Math.abs((s.totals?.usd ?? NaN) - linesUsd) < 1e-6
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
var EVM, ENV_RPC, TRANSFER, topicAddr;
|
|
1490
|
+
var init_statement = __esm({
|
|
1491
|
+
"src/statement.ts"() {
|
|
1492
|
+
"use strict";
|
|
1493
|
+
init_solana();
|
|
1494
|
+
EVM = { "eip155:4663": robinhood2, "eip155:8453": base2, "eip155:42161": arbitrum2, "eip155:137": polygon2, "eip155:5042": arc2, "eip155:1868": soneium2 };
|
|
1495
|
+
ENV_RPC = { "eip155:4663": "WAFERMAP_RPC_ROBINHOOD", "eip155:8453": "WAFERMAP_RPC_BASE", "eip155:42161": "WAFERMAP_RPC_ARBITRUM", "eip155:137": "WAFERMAP_RPC_POLYGON", "eip155:5042": "WAFERMAP_RPC_ARC", "eip155:1868": "WAFERMAP_RPC_SONEIUM", solana: "WAFERMAP_RPC_SOLANA" };
|
|
1496
|
+
TRANSFER = toEventSelector2(parseAbiItem2("event Transfer(address indexed from, address indexed to, uint256 value)"));
|
|
1497
|
+
topicAddr = (t) => t ? getAddress3(`0x${t.slice(-40)}`) : null;
|
|
1292
1498
|
}
|
|
1293
1499
|
});
|
|
1294
1500
|
|
|
@@ -1392,12 +1598,13 @@ async function runMcp() {
|
|
|
1392
1598
|
resource: z.string().describe("Resource id, e.g. market-brief, prediction-markets, stock-signal, flux-2-dev."),
|
|
1393
1599
|
input: z.record(z.string(), z.any()).optional().describe("Input fields for the resource."),
|
|
1394
1600
|
network: NET,
|
|
1395
|
-
payIn: z.enum(["stablecoin", "wafermap"]).optional().describe("wafermap: pay Wafermap's own resources in $WAFERMAP at 50% off (the tokens are burned; needs `wafermap approve-wafermap` once). Default: USDG/USDC.")
|
|
1601
|
+
payIn: z.enum(["stablecoin", "wafermap"]).optional().describe("wafermap: pay Wafermap's own resources in $WAFERMAP at 50% off (the tokens are burned; needs `wafermap approve-wafermap` once). Default: USDG/USDC."),
|
|
1602
|
+
metered: z.boolean().optional().describe("LLM resources only: sign a ceiling and pay only the tokens the model used (USDC on base, arbitrum or polygon; no gas needed). Caps count the ceiling.")
|
|
1396
1603
|
}
|
|
1397
1604
|
},
|
|
1398
|
-
async ({ resource, input, network, payIn }) => {
|
|
1605
|
+
async ({ resource, input, network, payIn, metered }) => {
|
|
1399
1606
|
try {
|
|
1400
|
-
return result(await wm.call(resource, input ?? {}, { network, payIn }));
|
|
1607
|
+
return result(await wm.call(resource, input ?? {}, { network, payIn, metered }));
|
|
1401
1608
|
} catch (e) {
|
|
1402
1609
|
return failure(e);
|
|
1403
1610
|
}
|
|
@@ -1844,6 +2051,8 @@ Wallet
|
|
|
1844
2051
|
wafermap balance USDG / USDC and gas on every payment network
|
|
1845
2052
|
wafermap limits [--per-payment 0.1] [--per-day 1] [--per-swap 25] [--network <net>] [--mcp-swaps on|off] [--mcp-confirm on|off]
|
|
1846
2053
|
wafermap history [--days 7] [--verify] payments and swaps from this machine; --verify checks on-chain
|
|
2054
|
+
wafermap statement [--month 2026-11] [--csv] [--out file] a month of payments to Wafermap and its sellers
|
|
2055
|
+
wafermap statement --verify <file.json> check every line of a statement against the chain
|
|
1847
2056
|
wafermap export-key [--solana] print the private key, or the Solana key for Phantom/Solflare
|
|
1848
2057
|
wafermap keychain [on|off] keep the password for \`wafermap mcp\` in the OS keychain, not in plain text
|
|
1849
2058
|
|
|
@@ -1851,6 +2060,7 @@ Pay
|
|
|
1851
2060
|
wafermap resources [words] Wafermap's pay-per-call catalog
|
|
1852
2061
|
wafermap call <id> [field=value ...] run a resource and pay its price (--json '{...}' for complex input)
|
|
1853
2062
|
wafermap call <id> ... --pay wafermap pay a Wafermap resource in $WAFERMAP at 50% off (the tokens are burned)
|
|
2063
|
+
wafermap call <llm> ... --metered pay only the tokens used, up to a ceiling you sign (USDC on base, arbitrum, polygon)
|
|
1854
2064
|
wafermap call <llm> ... --stream print the answer as it arrives; you pay only once the model finishes
|
|
1855
2065
|
wafermap approve-wafermap one-time approval so the wallet can pay in $WAFERMAP (Robinhood Chain, ETH gas)
|
|
1856
2066
|
wafermap holder what this wallet's $WAFERMAP unlocks: holder price, Pro free calls left today
|
|
@@ -1873,7 +2083,7 @@ function parse(argv) {
|
|
|
1873
2083
|
if (a.startsWith("--")) {
|
|
1874
2084
|
const [k, v] = a.slice(2).split("=", 2);
|
|
1875
2085
|
if (v !== void 0) out.flags[k] = v;
|
|
1876
|
-
else if (argv[i + 1] && !argv[i + 1].startsWith("--") && !["yes", "post", "import", "stream"].includes(k)) out.flags[k] = argv[++i];
|
|
2086
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith("--") && !["yes", "post", "import", "stream", "metered"].includes(k)) out.flags[k] = argv[++i];
|
|
1877
2087
|
else out.flags[k] = true;
|
|
1878
2088
|
} else out._.push(a);
|
|
1879
2089
|
}
|
|
@@ -2080,6 +2290,60 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
2080
2290
|
} else console.log("Add --verify to check on-chain which signed payments were executed.");
|
|
2081
2291
|
return;
|
|
2082
2292
|
}
|
|
2293
|
+
case "statement": {
|
|
2294
|
+
const { fetchStatement: fetchStatement2, parseStatement: parseStatement2, verifyStatement: verifyStatement2 } = await Promise.resolve().then(() => (init_statement(), statement_exports));
|
|
2295
|
+
const { readFileSync: readFileSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
2296
|
+
if (a.flags.verify !== void 0) {
|
|
2297
|
+
const file2 = typeof a.flags.verify === "string" ? a.flags.verify : a._[0];
|
|
2298
|
+
if (!file2) die("Which file? wafermap statement --verify wafermap-statement-2026-11.json");
|
|
2299
|
+
let text2;
|
|
2300
|
+
try {
|
|
2301
|
+
text2 = readFileSync2(file2, "utf8");
|
|
2302
|
+
} catch {
|
|
2303
|
+
die(`Cannot read ${file2}.`);
|
|
2304
|
+
}
|
|
2305
|
+
let st;
|
|
2306
|
+
try {
|
|
2307
|
+
st = parseStatement2(text2);
|
|
2308
|
+
} catch (e) {
|
|
2309
|
+
die(e.message);
|
|
2310
|
+
}
|
|
2311
|
+
console.error(`Checking ${st.lines.length} payments from ${st.month} on-chain\u2026`);
|
|
2312
|
+
const r = await verifyStatement2(st);
|
|
2313
|
+
st.lines.forEach((l, i) => {
|
|
2314
|
+
if (r.results[i] !== "ok") console.log(`${r.results[i].toUpperCase().padEnd(11)} ${l.date.slice(0, 16).replace("T", " ")} $${l.amountUsd.padEnd(8)} ${l.resource} ${l.explorer ?? l.tx}`);
|
|
2315
|
+
});
|
|
2316
|
+
console.log(`
|
|
2317
|
+
${r.ok} of ${r.lines} payments confirmed on-chain, $${r.confirmedUsd} of $${r.linesUsd}.${r.totalsMatch ? " The totals match the lines." : " The totals block does NOT match the lines."}${st.truncated ? " This statement was cut at 10,000 lines." : ""}`);
|
|
2318
|
+
if (r.ok !== r.lines)
|
|
2319
|
+
console.log(`ok = the transaction moved exactly that amount from the payer to the recipient \xB7 mismatch = it did not \xB7 failed = the transaction reverted \xB7 not_found = no such transaction \xB7 error = the RPC did not answer, try again (set WAFERMAP_RPC_<NETWORK> to your own RPC) \xB7 unsupported = a network this CLI cannot read.`);
|
|
2320
|
+
if (r.mismatch || r.failed || r.notFound || !r.totalsMatch) process.exitCode = 2;
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const evm = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`, or check a downloaded file with --verify.");
|
|
2324
|
+
const wallets = [evm, ...keystoreSolana() ? [keystoreSolana()] : []];
|
|
2325
|
+
const month = typeof a.flags.month === "string" ? a.flags.month : (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
2326
|
+
if (!/^20\d\d-(0[1-9]|1[0-2])$/.test(month)) die("--month must be YYYY-MM, for example 2026-11.");
|
|
2327
|
+
const csv = a.flags.csv === true;
|
|
2328
|
+
const out = await fetchStatement2(wallets, month, { baseUrl: process.env.WAFERMAP_URL, csv, timeoutMs: Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0 });
|
|
2329
|
+
const file = typeof a.flags.out === "string" ? a.flags.out : null;
|
|
2330
|
+
if (typeof out === "string") {
|
|
2331
|
+
if (file) writeFileSync2(file, out);
|
|
2332
|
+
else process.stdout.write(out);
|
|
2333
|
+
if (file) console.error(`Saved ${file}.`);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
if (file) {
|
|
2337
|
+
writeFileSync2(file, JSON.stringify(out, null, 2) + "\n");
|
|
2338
|
+
console.error(`Saved ${file}. Check it any time with: wafermap statement --verify ${file}`);
|
|
2339
|
+
}
|
|
2340
|
+
if (!file || a.flags.print !== void 0) {
|
|
2341
|
+
for (const l of out.lines) console.log(`${l.date.slice(0, 16).replace("T", " ")} $${l.amountUsd.padEnd(8)} ${l.network.padEnd(14).slice(0, 14)} ${l.resource} ${l.explorer ?? l.tx}`);
|
|
2342
|
+
console.log(`
|
|
2343
|
+
${month}: ${out.totals.calls} paid calls, $${out.totals.usd}.${out.lines.length ? " Save it with --out statement.json, then check it with --verify." : ""}`);
|
|
2344
|
+
}
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2083
2347
|
case "resources": {
|
|
2084
2348
|
const q = a._.join(" ").toLowerCase();
|
|
2085
2349
|
const list = await listResources(process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
|
|
@@ -2103,9 +2367,11 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
2103
2367
|
const payIn = a.flags.pay === "wafermap" ? "wafermap" : a.flags.pay === void 0 || a.flags.pay === "usd" ? void 0 : die("--pay must be wafermap (or leave it out to pay in USDG/USDC)");
|
|
2104
2368
|
let streamed = false;
|
|
2105
2369
|
const onDelta = a.flags.stream === true ? (t) => (streamed = true, process.stdout.write(t)) : void 0;
|
|
2106
|
-
const
|
|
2370
|
+
const metered = a.flags.metered === true;
|
|
2371
|
+
const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network), payIn, onDelta, metered });
|
|
2107
2372
|
if (streamed) process.stdout.write("\n");
|
|
2108
2373
|
if (out.receipt?.asset === "WAFERMAP") console.error(`\u2713 Paid ${out.receipt.amountTokens} $WAFERMAP (\u2248 $${out.receipt.amountUsd}), burned \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
|
|
2374
|
+
else if (out.receipt?.metered) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} for ${out.receipt.metered.inputTokens} in + ${out.receipt.metered.outputTokens} out tokens${out.receipt.metered.estimated ? " (estimated)" : ""}, signed up to $${out.receipt.metered.ceilingUsd} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
|
|
2109
2375
|
else if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
|
|
2110
2376
|
else if (out.free) console.error(`\u2713 Free call ($WAFERMAP Pro) \xB7 ${out.free.left} of ${out.free.perDay} left today. Nothing was paid.`);
|
|
2111
2377
|
if (typeof a.flags.out === "string") await saveMedia(out.result, a.flags.out);
|
package/dist/index.js
CHANGED
|
@@ -364,6 +364,65 @@ var PAY_CHAINS = [
|
|
|
364
364
|
];
|
|
365
365
|
var EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
|
|
366
366
|
var CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
|
|
367
|
+
var UPTO_PROXY = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002";
|
|
368
|
+
var MAX_METERED_SECONDS = 300;
|
|
369
|
+
var UPTO_TYPES = {
|
|
370
|
+
PermitWitnessTransferFrom: [
|
|
371
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
372
|
+
{ name: "spender", type: "address" },
|
|
373
|
+
{ name: "nonce", type: "uint256" },
|
|
374
|
+
{ name: "deadline", type: "uint256" },
|
|
375
|
+
{ name: "witness", type: "Witness" }
|
|
376
|
+
],
|
|
377
|
+
TokenPermissions: [
|
|
378
|
+
{ name: "token", type: "address" },
|
|
379
|
+
{ name: "amount", type: "uint256" }
|
|
380
|
+
],
|
|
381
|
+
Witness: [
|
|
382
|
+
{ name: "to", type: "address" },
|
|
383
|
+
{ name: "facilitator", type: "address" },
|
|
384
|
+
{ name: "validAfter", type: "uint256" }
|
|
385
|
+
]
|
|
386
|
+
};
|
|
387
|
+
var EIP2612_TYPES = {
|
|
388
|
+
Permit: [
|
|
389
|
+
{ name: "owner", type: "address" },
|
|
390
|
+
{ name: "spender", type: "address" },
|
|
391
|
+
{ name: "value", type: "uint256" },
|
|
392
|
+
{ name: "nonce", type: "uint256" },
|
|
393
|
+
{ name: "deadline", type: "uint256" }
|
|
394
|
+
]
|
|
395
|
+
};
|
|
396
|
+
async function signUptoPayment(signer, req, resource, opts = {}) {
|
|
397
|
+
const fac = String(req.raw?.extra?.facilitatorAddress ?? "");
|
|
398
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(fac)) throw new X402Error("bad_quote", "The metered quote names no facilitator. Nothing was signed.");
|
|
399
|
+
const nonce = BigInt(randomNonce()) >> 8n;
|
|
400
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_METERED_SECONDS));
|
|
401
|
+
const max = BigInt(req.amount);
|
|
402
|
+
const witness = { to: req.payTo, facilitator: fac, validAfter: 0n };
|
|
403
|
+
let signature;
|
|
404
|
+
try {
|
|
405
|
+
signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: req.chain.id, verifyingContract: PERMIT2 }, types: UPTO_TYPES, primaryType: "PermitWitnessTransferFrom", message: { permitted: { token: req.asset, amount: max }, spender: UPTO_PROXY, nonce, deadline, witness } });
|
|
406
|
+
} catch (e) {
|
|
407
|
+
throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
|
|
408
|
+
}
|
|
409
|
+
let extensions;
|
|
410
|
+
if (opts.gasless && opts.reader) {
|
|
411
|
+
const allowance = await opts.reader.allowance(req.chain, signer.address, req.asset).catch(() => 0n);
|
|
412
|
+
if (allowance < max) {
|
|
413
|
+
const pn = await opts.reader.permitNonce(req.chain, signer.address, req.asset);
|
|
414
|
+
let ps;
|
|
415
|
+
try {
|
|
416
|
+
ps = await signer.signTypedData({ domain: { name: req.extra.name, version: req.extra.version, chainId: req.chain.id, verifyingContract: req.asset }, types: EIP2612_TYPES, primaryType: "Permit", message: { owner: signer.address, spender: PERMIT2, value: max, nonce: pn, deadline } });
|
|
417
|
+
} catch (e) {
|
|
418
|
+
throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
|
|
419
|
+
}
|
|
420
|
+
extensions = { eip2612GasSponsoring: { info: { from: signer.address, asset: req.asset, spender: PERMIT2, amount: max.toString(), nonce: pn.toString(), deadline: deadline.toString(), signature: ps, version: "1" } } };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const permit2Authorization = { from: signer.address, permitted: { token: req.asset, amount: req.amount }, spender: UPTO_PROXY, nonce: nonce.toString(), deadline: deadline.toString(), witness: { to: req.payTo, facilitator: fac, validAfter: "0" } };
|
|
424
|
+
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { signature, permit2Authorization }, ...extensions ? { extensions } : {} })), deadline: Number(deadline) };
|
|
425
|
+
}
|
|
367
426
|
var WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
|
|
368
427
|
var PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
369
428
|
var DEAD = "0x000000000000000000000000000000000000dEaD";
|
|
@@ -443,12 +502,19 @@ async function readQuote(res) {
|
|
|
443
502
|
const accepts = [];
|
|
444
503
|
const offered = [];
|
|
445
504
|
let token;
|
|
505
|
+
const metered = [];
|
|
446
506
|
for (const r of body.accepts) {
|
|
447
507
|
offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
|
|
448
508
|
if (r?.scheme === TOKEN_SCHEME) {
|
|
449
509
|
token ??= tokenOption(r);
|
|
450
510
|
continue;
|
|
451
511
|
}
|
|
512
|
+
if (r?.scheme === "upto" && version === 2) {
|
|
513
|
+
const ch = chainOf(r.network);
|
|
514
|
+
if (ch && ch.vm === "evm" && String(r.asset ?? "").toLowerCase() === ch.stable.toLowerCase() && /^\d+$/.test(String(r.amount)) && /^0x[0-9a-fA-F]{40}$/.test(String(r.extra?.facilitatorAddress ?? "")) && /^0x[0-9a-fA-F]{40}$/.test(String(r.payTo ?? "")))
|
|
515
|
+
metered.push({ version, chain: ch, amount: String(r.amount), usd: Number(r.amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra: r.extra?.name && r.extra?.version ? { name: String(r.extra.name), version: String(r.extra.version) } : ch.domain, raw: r });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
452
518
|
const chain = chainOf(r.network);
|
|
453
519
|
if (!chain || r.scheme !== "exact") continue;
|
|
454
520
|
if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
|
|
@@ -462,7 +528,10 @@ async function readQuote(res) {
|
|
|
462
528
|
accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
|
|
463
529
|
}
|
|
464
530
|
const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
|
|
465
|
-
|
|
531
|
+
const m = body.metered;
|
|
532
|
+
const meterInfo = m && Number.isFinite(Number(m.ceilingUsd)) ? { inputPer1kTokensUsd: Number(m.inputPer1kTokensUsd), outputPer1kTokensUsd: Number(m.outputPer1kTokensUsd), ceilingUsd: Number(m.ceilingUsd), minimumUsd: Number(m.minimumUsd) } : void 0;
|
|
533
|
+
const gasless = !!body.extensions?.eip2612GasSponsoring;
|
|
534
|
+
return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {}, ...metered.length ? { metered: metered.map((q) => ({ ...q, gasless })) } : {}, ...meterInfo ? { meterInfo } : {} };
|
|
466
535
|
}
|
|
467
536
|
function choose(q, prefer = CHAIN_KEYS) {
|
|
468
537
|
for (const k of prefer) {
|
|
@@ -546,6 +615,20 @@ async function x402fetch(url, init, opts) {
|
|
|
546
615
|
const res2 = await f(url, { ...init, headers: headers2 });
|
|
547
616
|
return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
|
|
548
617
|
}
|
|
618
|
+
if (opts.meter) {
|
|
619
|
+
const pref = (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana");
|
|
620
|
+
const req2 = pref.map((k) => quote.metered?.find((q) => q.chain.key === k)).find(Boolean);
|
|
621
|
+
if (!req2) throw new X402Error("metered_not_offered", quote.metered?.length ? `Metered calls are offered on ${quote.metered.map((q) => q.chain.name).join(", ")}; pick one of those networks.` : "This call is not offered as a metered (pay per token) call. Pay the flat price instead.");
|
|
622
|
+
if (opts.approve) await opts.approve(req2);
|
|
623
|
+
const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
|
|
624
|
+
const header2 = await signUptoPayment(signer2, req2, quote.resource, { reader: opts.reader, gasless: !!req2.gasless });
|
|
625
|
+
const signed2 = { req: req2, from: signer2.address, metered: { ceiling: req2.amount, deadline: header2.deadline } };
|
|
626
|
+
opts.onSigned?.(signed2);
|
|
627
|
+
const headers2 = new Headers(init.headers);
|
|
628
|
+
headers2.set(header2.name, header2.value);
|
|
629
|
+
const res2 = await f(url, { ...init, headers: headers2 });
|
|
630
|
+
return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
|
|
631
|
+
}
|
|
549
632
|
const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
|
|
550
633
|
if (opts.approve) await opts.approve(req);
|
|
551
634
|
const svm = req.chain.vm === "svm";
|
|
@@ -797,7 +880,15 @@ var Wafermap = class {
|
|
|
797
880
|
this.queue = prev.then(() => held);
|
|
798
881
|
return prev.then(() => release);
|
|
799
882
|
}
|
|
800
|
-
|
|
883
|
+
/** Chain reads for a metered payment's EIP-2612 permit: Permit2 allowance and the token's permit nonce. */
|
|
884
|
+
reader() {
|
|
885
|
+
const nonces = [{ type: "function", name: "nonces", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] }];
|
|
886
|
+
return {
|
|
887
|
+
allowance: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: erc20Abi2, functionName: "allowance", args: [owner, PERMIT2] }),
|
|
888
|
+
permitNonce: (chain, owner, token) => this.client(chain.key).readContract({ address: token, abi: nonces, functionName: "nonces", args: [owner] })
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
async paidFetch(kind, what, url, init, network, payIn, meter) {
|
|
801
892
|
let id = null;
|
|
802
893
|
let release = null;
|
|
803
894
|
const unlock = () => {
|
|
@@ -811,10 +902,12 @@ var Wafermap = class {
|
|
|
811
902
|
prefer: this.prefer(network),
|
|
812
903
|
fetch: this.fetch,
|
|
813
904
|
payIn,
|
|
905
|
+
meter,
|
|
906
|
+
reader: meter ? this.reader() : void 0,
|
|
814
907
|
tokenPriceUsd: this.tokenPriceUsd,
|
|
815
908
|
approve: async (req) => {
|
|
816
909
|
release = await this.acquire();
|
|
817
|
-
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : `${req.chain.symbol} to ${req.payTo}` });
|
|
910
|
+
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: req.asset === WAFERMAP_TOKEN ? "in $WAFERMAP at 50% off, burned" : meter ? `at most ${req.chain.symbol} ${req.usd}, charged for the tokens used, to ${req.payTo}` : `${req.chain.symbol} to ${req.payTo}` });
|
|
818
911
|
},
|
|
819
912
|
onSigned: (s) => {
|
|
820
913
|
id = this.booked(kind, what, s);
|
|
@@ -902,11 +995,12 @@ var Wafermap = class {
|
|
|
902
995
|
}
|
|
903
996
|
async call(id, input = {}, o = {}) {
|
|
904
997
|
const inToken = o.payIn === "wafermap";
|
|
998
|
+
if (o.metered && inToken) throw new WafermapError("bad_options", "A metered call is paid in USDC; it cannot also be paid in $WAFERMAP.");
|
|
905
999
|
const stream = !!o.onDelta && !inToken;
|
|
906
1000
|
if (inToken && id.startsWith("m/")) throw new WafermapError("token_not_offered", "Marketplace listings are paid in USDG or USDC to their seller, not in $WAFERMAP.");
|
|
907
1001
|
if (inToken && !await this.wafermapApproved().catch(() => true)) throw new WafermapError("permit2_not_approved", "Paying in $WAFERMAP needs a one-time approval of Permit2. Run `wafermap approve-wafermap` (one transaction on Robinhood Chain, paid in ETH), then call again.");
|
|
908
1002
|
const pass = id.startsWith("m/") ? null : await this.holderPass();
|
|
909
|
-
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
|
|
1003
|
+
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...stream ? { accept: "text/event-stream" } : {}, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0, o.metered);
|
|
910
1004
|
const settle = (r2, out2) => {
|
|
911
1005
|
if (!r2.paid || !r2.id) return;
|
|
912
1006
|
const status = r2.res.status === 402 ? "refused" : r2.res.ok && !out2?.streamError ? "ok" : "failed";
|
|
@@ -1026,6 +1120,103 @@ var Wafermap = class {
|
|
|
1026
1120
|
}
|
|
1027
1121
|
}
|
|
1028
1122
|
};
|
|
1123
|
+
|
|
1124
|
+
// src/statement.ts
|
|
1125
|
+
import { createPublicClient as createPublicClient2, getAddress as getAddress3, http as http2, isAddress as isAddress3, parseAbiItem as parseAbiItem2, toEventSelector as toEventSelector2 } from "viem";
|
|
1126
|
+
import { arbitrum as arbitrum2, arc as arc2, base as base2, polygon as polygon2, robinhood as robinhood2, soneium as soneium2 } from "viem/chains";
|
|
1127
|
+
var EVM = { "eip155:4663": robinhood2, "eip155:8453": base2, "eip155:42161": arbitrum2, "eip155:137": polygon2, "eip155:5042": arc2, "eip155:1868": soneium2 };
|
|
1128
|
+
var ENV_RPC = { "eip155:4663": "WAFERMAP_RPC_ROBINHOOD", "eip155:8453": "WAFERMAP_RPC_BASE", "eip155:42161": "WAFERMAP_RPC_ARBITRUM", "eip155:137": "WAFERMAP_RPC_POLYGON", "eip155:5042": "WAFERMAP_RPC_ARC", "eip155:1868": "WAFERMAP_RPC_SONEIUM", solana: "WAFERMAP_RPC_SOLANA" };
|
|
1129
|
+
var TRANSFER = toEventSelector2(parseAbiItem2("event Transfer(address indexed from, address indexed to, uint256 value)"));
|
|
1130
|
+
var topicAddr = (t) => t ? getAddress3(`0x${t.slice(-40)}`) : null;
|
|
1131
|
+
async function fetchStatement(wallets, month, opts = {}) {
|
|
1132
|
+
const url = `${(opts.baseUrl ?? "https://wafermap.xyz").replace(/\/$/, "")}/api/statement?wallet=${encodeURIComponent(wallets.join(","))}&month=${encodeURIComponent(month)}${opts.csv ? "&format=csv" : ""}`;
|
|
1133
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 6e4) });
|
|
1134
|
+
if (!r.ok) {
|
|
1135
|
+
const j = await r.json().catch(() => null);
|
|
1136
|
+
throw new Error(j?.error?.message ?? `Statement request failed (${r.status}).`);
|
|
1137
|
+
}
|
|
1138
|
+
return opts.csv ? r.text() : await r.json();
|
|
1139
|
+
}
|
|
1140
|
+
function parseStatement(text) {
|
|
1141
|
+
let s;
|
|
1142
|
+
try {
|
|
1143
|
+
s = JSON.parse(text);
|
|
1144
|
+
} catch {
|
|
1145
|
+
throw new Error("That file is not JSON. Download the JSON statement (not the CSV) to verify it.");
|
|
1146
|
+
}
|
|
1147
|
+
if (s?.kind !== "wafermap-statement" || !Array.isArray(s.lines)) throw new Error("That file is not a Wafermap statement.");
|
|
1148
|
+
return s;
|
|
1149
|
+
}
|
|
1150
|
+
function defaultClients(env = process.env) {
|
|
1151
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1152
|
+
return {
|
|
1153
|
+
evm: (network) => {
|
|
1154
|
+
const chain = EVM[network];
|
|
1155
|
+
if (!chain) return null;
|
|
1156
|
+
if (!cache.has(network)) cache.set(network, createPublicClient2({ chain, transport: http2(env[ENV_RPC[network]] || void 0, { timeout: 3e4 }) }));
|
|
1157
|
+
return cache.get(network);
|
|
1158
|
+
},
|
|
1159
|
+
solana: (method, params) => solanaRpc(env.WAFERMAP_RPC_SOLANA || SOLANA_PUBLIC_RPC, method, params)
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
async function verifyLine(l, clients) {
|
|
1163
|
+
try {
|
|
1164
|
+
if (!/^\d+$/.test(l.amountUnits)) return "mismatch";
|
|
1165
|
+
const want = BigInt(l.amountUnits);
|
|
1166
|
+
if (l.network.startsWith("solana:")) {
|
|
1167
|
+
const tx = await clients.solana("getTransaction", [l.tx, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0, commitment: "finalized" }]);
|
|
1168
|
+
if (!tx) return "not_found";
|
|
1169
|
+
if (tx.meta?.err) return "failed";
|
|
1170
|
+
const delta = (owner) => {
|
|
1171
|
+
const sum = (xs) => (xs ?? []).filter((b) => b.owner === owner && b.mint === l.token).reduce((a, b) => a + BigInt(b.uiTokenAmount?.amount ?? "0"), 0n);
|
|
1172
|
+
return sum(tx.meta?.postTokenBalances) - sum(tx.meta?.preTokenBalances);
|
|
1173
|
+
};
|
|
1174
|
+
return delta(l.from) === -want && delta(l.to) === want ? "ok" : "mismatch";
|
|
1175
|
+
}
|
|
1176
|
+
const client = clients.evm(l.network);
|
|
1177
|
+
if (!client) return "unsupported";
|
|
1178
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(l.tx) || !isAddress3(l.token) || !isAddress3(l.from) || !isAddress3(l.to)) return "mismatch";
|
|
1179
|
+
const receipt = await client.getTransactionReceipt({ hash: l.tx }).catch((e) => /not.*found|could not be found/i.test(String(e?.message)) ? null : Promise.reject(e));
|
|
1180
|
+
if (!receipt) return "not_found";
|
|
1181
|
+
if (receipt.status !== "success") return "failed";
|
|
1182
|
+
const hit = receipt.logs.some(
|
|
1183
|
+
(g) => g.address.toLowerCase() === l.token.toLowerCase() && g.topics[0] === TRANSFER && topicAddr(g.topics[1]) === getAddress3(l.from) && topicAddr(g.topics[2]) === getAddress3(l.to) && BigInt(g.data) === want
|
|
1184
|
+
);
|
|
1185
|
+
return hit ? "ok" : "mismatch";
|
|
1186
|
+
} catch {
|
|
1187
|
+
return "error";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function verifyStatement(s, clients = defaultClients(), onLine) {
|
|
1191
|
+
const results = new Array(s.lines.length);
|
|
1192
|
+
let next = 0;
|
|
1193
|
+
await Promise.all(
|
|
1194
|
+
Array.from({ length: 4 }, async () => {
|
|
1195
|
+
while (next < s.lines.length) {
|
|
1196
|
+
const i = next++;
|
|
1197
|
+
results[i] = await verifyLine(s.lines[i], clients);
|
|
1198
|
+
onLine?.(i, results[i]);
|
|
1199
|
+
}
|
|
1200
|
+
})
|
|
1201
|
+
);
|
|
1202
|
+
const usd = (pick) => Math.round(s.lines.reduce((a, l, i) => a + (pick(results[i]) ? Number(l.amountUsd) : 0), 0) * 1e6) / 1e6;
|
|
1203
|
+
const count = (r) => results.filter((x) => x === r).length;
|
|
1204
|
+
const linesUsd = usd(() => true);
|
|
1205
|
+
return {
|
|
1206
|
+
results,
|
|
1207
|
+
lines: s.lines.length,
|
|
1208
|
+
ok: count("ok"),
|
|
1209
|
+
mismatch: count("mismatch"),
|
|
1210
|
+
failed: count("failed"),
|
|
1211
|
+
notFound: count("not_found"),
|
|
1212
|
+
unsupported: count("unsupported"),
|
|
1213
|
+
error: count("error"),
|
|
1214
|
+
confirmedUsd: usd((r) => r === "ok"),
|
|
1215
|
+
linesUsd,
|
|
1216
|
+
// The totals block must add up to the lines it sits next to.
|
|
1217
|
+
totalsMatch: s.totals?.calls === s.lines.length && Math.abs((s.totals?.usd ?? NaN) - linesUsd) < 1e-6
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1029
1220
|
export {
|
|
1030
1221
|
DEFAULT_LIMITS,
|
|
1031
1222
|
DEFAULT_TIMEOUT_MS,
|
|
@@ -1048,15 +1239,19 @@ export {
|
|
|
1048
1239
|
decodeSwap,
|
|
1049
1240
|
encodePass,
|
|
1050
1241
|
executeSwap,
|
|
1242
|
+
fetchStatement,
|
|
1051
1243
|
holderInfo,
|
|
1052
1244
|
listResources,
|
|
1053
1245
|
makePass,
|
|
1054
1246
|
mergeLog,
|
|
1247
|
+
parseStatement,
|
|
1055
1248
|
passMessage,
|
|
1056
1249
|
quoteSwap,
|
|
1057
1250
|
readQuote,
|
|
1058
1251
|
signPayment,
|
|
1059
1252
|
signTokenPayment,
|
|
1253
|
+
verifyLine,
|
|
1254
|
+
verifyStatement,
|
|
1060
1255
|
wafermapPriceUsd,
|
|
1061
1256
|
x402fetch
|
|
1062
1257
|
};
|
|
@@ -46,6 +46,8 @@ export type Requirement = {
|
|
|
46
46
|
};
|
|
47
47
|
/** The requirement exactly as the server sent it (echoed back in the payment). */
|
|
48
48
|
raw: any;
|
|
49
|
+
/** Metered quotes only: the server announced eip2612GasSponsoring, so a missing Permit2 approval can be signed. */
|
|
50
|
+
gasless?: boolean;
|
|
49
51
|
};
|
|
50
52
|
export type Quote = {
|
|
51
53
|
version: 1 | 2;
|
|
@@ -57,7 +59,33 @@ export type Quote = {
|
|
|
57
59
|
message: string;
|
|
58
60
|
};
|
|
59
61
|
token?: TokenRequirement;
|
|
62
|
+
metered?: Requirement[];
|
|
63
|
+
meterInfo?: MeterInfo;
|
|
60
64
|
};
|
|
65
|
+
export declare const UPTO_PROXY = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002";
|
|
66
|
+
/** The longest a metered authorization stays valid: long answers settle after the model finishes. */
|
|
67
|
+
export declare const MAX_METERED_SECONDS = 300;
|
|
68
|
+
export type MeterInfo = {
|
|
69
|
+
inputPer1kTokensUsd: number;
|
|
70
|
+
outputPer1kTokensUsd: number;
|
|
71
|
+
ceilingUsd: number;
|
|
72
|
+
minimumUsd: number;
|
|
73
|
+
};
|
|
74
|
+
/** Reads the client needs for an EIP-2612 permit: the owner's Permit2 allowance and its permit nonce on the token. */
|
|
75
|
+
export type ChainReader = {
|
|
76
|
+
allowance: (chain: Chain, owner: Hex, token: Hex) => Promise<bigint>;
|
|
77
|
+
permitNonce: (chain: Chain, owner: Hex, token: Hex) => Promise<bigint>;
|
|
78
|
+
};
|
|
79
|
+
/** Signs a metered payment for at most `req.amount` (the ceiling). Adds an EIP-2612 permit when the Permit2
|
|
80
|
+
* allowance is short and the server announced eip2612GasSponsoring. */
|
|
81
|
+
export declare function signUptoPayment(signer: Signer, req: Requirement, resource: any, opts?: {
|
|
82
|
+
reader?: ChainReader;
|
|
83
|
+
gasless?: boolean;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
name: string;
|
|
86
|
+
value: string;
|
|
87
|
+
deadline: number;
|
|
88
|
+
}>;
|
|
61
89
|
export declare const WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
|
|
62
90
|
export declare const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
63
91
|
export declare const DEAD = "0x000000000000000000000000000000000000dEaD";
|
|
@@ -123,6 +151,10 @@ export type SignedSol = {
|
|
|
123
151
|
export type Signed = {
|
|
124
152
|
req: Requirement;
|
|
125
153
|
from: string;
|
|
154
|
+
metered?: {
|
|
155
|
+
ceiling: string;
|
|
156
|
+
deadline: number;
|
|
157
|
+
};
|
|
126
158
|
auth?: SignedAuth;
|
|
127
159
|
sol?: SignedSol;
|
|
128
160
|
token?: {
|
|
@@ -151,4 +183,6 @@ export declare function x402fetch(url: string, init: RequestInit, opts: {
|
|
|
151
183
|
fetch?: typeof fetch;
|
|
152
184
|
payIn?: "stablecoin" | "wafermap";
|
|
153
185
|
tokenPriceUsd?: () => Promise<number>;
|
|
186
|
+
meter?: boolean;
|
|
187
|
+
reader?: ChainReader;
|
|
154
188
|
}): Promise<PaidCall>;
|
package/dist/types/src/core.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ export declare class Wafermap {
|
|
|
142
142
|
private body;
|
|
143
143
|
private queue;
|
|
144
144
|
private acquire;
|
|
145
|
+
/** Chain reads for a metered payment's EIP-2612 permit: Permit2 allowance and the token's permit nonce. */
|
|
146
|
+
private reader;
|
|
145
147
|
private paidFetch;
|
|
146
148
|
resources(): Promise<any[]>;
|
|
147
149
|
private passCache;
|
|
@@ -161,6 +163,7 @@ export declare class Wafermap {
|
|
|
161
163
|
network?: Net;
|
|
162
164
|
payIn?: "stablecoin" | "wafermap";
|
|
163
165
|
onDelta?: (text: string) => void;
|
|
166
|
+
metered?: boolean;
|
|
164
167
|
}): Promise<{
|
|
165
168
|
resource: string;
|
|
166
169
|
result: any;
|
|
@@ -173,6 +176,13 @@ export declare class Wafermap {
|
|
|
173
176
|
payer: string;
|
|
174
177
|
tx: string;
|
|
175
178
|
ms: number;
|
|
179
|
+
metered?: {
|
|
180
|
+
inputTokens: number;
|
|
181
|
+
outputTokens: number;
|
|
182
|
+
estimated: boolean;
|
|
183
|
+
ceilingUsd: number;
|
|
184
|
+
chargedUsd: number;
|
|
185
|
+
};
|
|
176
186
|
} | null;
|
|
177
187
|
free?: {
|
|
178
188
|
wallet: string;
|
|
@@ -6,3 +6,5 @@ export { quoteSwap, executeSwap, decodeSwap, KYBER_ROUTER, SwapError } from "../
|
|
|
6
6
|
export type { Requirement, Quote, Signed, SignedAuth, SignedSol, Signer } from "../shared/x402client";
|
|
7
7
|
export { checkSolanaPayment, SOLANA_PUBLIC_RPC } from "../shared/solana";
|
|
8
8
|
export type { SolanaCheck } from "../shared/solana";
|
|
9
|
+
export { fetchStatement, parseStatement, verifyStatement, verifyLine } from "./statement.js";
|
|
10
|
+
export type { Statement, StatementLine, LineCheck } from "./statement.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type PublicClient } from "viem";
|
|
2
|
+
export type StatementLine = {
|
|
3
|
+
date: string;
|
|
4
|
+
resource: string;
|
|
5
|
+
network: string;
|
|
6
|
+
asset: string;
|
|
7
|
+
amountUsd: string;
|
|
8
|
+
token: string;
|
|
9
|
+
amountUnits: string;
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
tx: string;
|
|
13
|
+
explorer?: string | null;
|
|
14
|
+
};
|
|
15
|
+
export type Statement = {
|
|
16
|
+
kind: "wafermap-statement";
|
|
17
|
+
version: number;
|
|
18
|
+
month: string;
|
|
19
|
+
wallets: string[];
|
|
20
|
+
totals: {
|
|
21
|
+
calls: number;
|
|
22
|
+
usd: number;
|
|
23
|
+
};
|
|
24
|
+
lines: StatementLine[];
|
|
25
|
+
truncated?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type LineCheck = "ok" | "mismatch" | "failed" | "not_found" | "unsupported" | "error";
|
|
28
|
+
/** Downloads a statement. `csv` returns the CSV text, otherwise the parsed JSON. */
|
|
29
|
+
export declare function fetchStatement(wallets: string[], month: string, opts?: {
|
|
30
|
+
baseUrl?: string;
|
|
31
|
+
csv?: boolean;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
}): Promise<Statement | string>;
|
|
34
|
+
/** Reads and sanity-checks a statement file's content. Throws on anything that is not a statement. */
|
|
35
|
+
export declare function parseStatement(text: string): Statement;
|
|
36
|
+
type Clients = {
|
|
37
|
+
evm: (network: string) => Pick<PublicClient, "getTransactionReceipt"> | null;
|
|
38
|
+
solana: (method: string, params: unknown[]) => Promise<any>;
|
|
39
|
+
};
|
|
40
|
+
/** Clients from env RPCs (WAFERMAP_RPC_<NET>) or the chains' public RPCs. */
|
|
41
|
+
export declare function defaultClients(env?: Record<string, string | undefined>): Clients;
|
|
42
|
+
/** Checks one line against its chain. */
|
|
43
|
+
export declare function verifyLine(l: StatementLine, clients: Clients): Promise<LineCheck>;
|
|
44
|
+
/** Checks every line, four at a time (public RPCs refuse bursts), and adds up what the chain confirms. */
|
|
45
|
+
export declare function verifyStatement(s: Statement, clients?: Clients, onLine?: (i: number, r: LineCheck) => void): Promise<{
|
|
46
|
+
results: LineCheck[];
|
|
47
|
+
lines: number;
|
|
48
|
+
ok: number;
|
|
49
|
+
mismatch: number;
|
|
50
|
+
failed: number;
|
|
51
|
+
notFound: number;
|
|
52
|
+
unsupported: number;
|
|
53
|
+
error: number;
|
|
54
|
+
confirmedUsd: number;
|
|
55
|
+
linesUsd: number;
|
|
56
|
+
totalsMatch: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wafermapxyz/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
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, Arc, Soneium), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|