@wafermapxyz/cli 0.4.9 → 0.4.11
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 +17 -0
- package/README.md +2 -0
- package/dist/cli.js +180 -3
- package/dist/index.js +101 -0
- package/dist/types/src/core.d.ts +6 -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,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.11 (15 Nov 2026)
|
|
4
|
+
|
|
5
|
+
**`wafermap statement`: a monthly statement you can check against the chains.**
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
- `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.
|
|
9
|
+
- Works on statements downloaded anywhere: the Wallet page, a team owner's statement from /teams, or the API.
|
|
10
|
+
- SDK: `fetchStatement`, `parseStatement`, `verifyStatement`, `verifyLine`.
|
|
11
|
+
|
|
12
|
+
## 0.4.10 (12 Nov 2026)
|
|
13
|
+
|
|
14
|
+
**`wafermap discover` shows the price on every network a service accepts.**
|
|
15
|
+
|
|
16
|
+
- Under each result, a `pays on:` line lists each network the service takes and its price there, for example `Base $0.002 · Polygon $0.002 · Solana $0.002`. Networks this wallet cannot pay on are marked `(not supported)`. Use the network you hold funds on with `wafermap pay <url> --network <net>`.
|
|
17
|
+
- SDK: `discover()` results carry `prices` (`network`, `chain`, `name`, `priceUsd`); `chain` is `null` for a network the wallet does not support. The MCP tool `wafermap_discover` returns the same field.
|
|
18
|
+
- `wafermap init` ends with a link to https://wafermap.xyz/start, the first paid call step by step.
|
|
19
|
+
|
|
3
20
|
## 0.4.9 (7 Nov 2026)
|
|
4
21
|
|
|
5
22
|
**`wafermap call <llm> --stream`: watch the answer arrive.**
|
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.
|
package/dist/cli.js
CHANGED
|
@@ -1288,7 +1288,119 @@ var VERSION;
|
|
|
1288
1288
|
var init_version = __esm({
|
|
1289
1289
|
"src/version.ts"() {
|
|
1290
1290
|
"use strict";
|
|
1291
|
-
VERSION = "0.4.
|
|
1291
|
+
VERSION = "0.4.11";
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// src/statement.ts
|
|
1296
|
+
var statement_exports = {};
|
|
1297
|
+
__export(statement_exports, {
|
|
1298
|
+
defaultClients: () => defaultClients,
|
|
1299
|
+
fetchStatement: () => fetchStatement,
|
|
1300
|
+
parseStatement: () => parseStatement,
|
|
1301
|
+
verifyLine: () => verifyLine,
|
|
1302
|
+
verifyStatement: () => verifyStatement
|
|
1303
|
+
});
|
|
1304
|
+
import { createPublicClient as createPublicClient2, getAddress as getAddress3, http as http2, isAddress as isAddress3, parseAbiItem as parseAbiItem2, toEventSelector as toEventSelector2 } from "viem";
|
|
1305
|
+
import { arbitrum as arbitrum2, arc as arc2, base as base2, polygon as polygon2, robinhood as robinhood2, soneium as soneium2 } from "viem/chains";
|
|
1306
|
+
async function fetchStatement(wallets, month, opts = {}) {
|
|
1307
|
+
const url = `${(opts.baseUrl ?? "https://wafermap.xyz").replace(/\/$/, "")}/api/statement?wallet=${encodeURIComponent(wallets.join(","))}&month=${encodeURIComponent(month)}${opts.csv ? "&format=csv" : ""}`;
|
|
1308
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 6e4) });
|
|
1309
|
+
if (!r.ok) {
|
|
1310
|
+
const j = await r.json().catch(() => null);
|
|
1311
|
+
throw new Error(j?.error?.message ?? `Statement request failed (${r.status}).`);
|
|
1312
|
+
}
|
|
1313
|
+
return opts.csv ? r.text() : await r.json();
|
|
1314
|
+
}
|
|
1315
|
+
function parseStatement(text2) {
|
|
1316
|
+
let s;
|
|
1317
|
+
try {
|
|
1318
|
+
s = JSON.parse(text2);
|
|
1319
|
+
} catch {
|
|
1320
|
+
throw new Error("That file is not JSON. Download the JSON statement (not the CSV) to verify it.");
|
|
1321
|
+
}
|
|
1322
|
+
if (s?.kind !== "wafermap-statement" || !Array.isArray(s.lines)) throw new Error("That file is not a Wafermap statement.");
|
|
1323
|
+
return s;
|
|
1324
|
+
}
|
|
1325
|
+
function defaultClients(env = process.env) {
|
|
1326
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1327
|
+
return {
|
|
1328
|
+
evm: (network) => {
|
|
1329
|
+
const chain = EVM[network];
|
|
1330
|
+
if (!chain) return null;
|
|
1331
|
+
if (!cache.has(network)) cache.set(network, createPublicClient2({ chain, transport: http2(env[ENV_RPC[network]] || void 0, { timeout: 3e4 }) }));
|
|
1332
|
+
return cache.get(network);
|
|
1333
|
+
},
|
|
1334
|
+
solana: (method, params) => solanaRpc(env.WAFERMAP_RPC_SOLANA || SOLANA_PUBLIC_RPC, method, params)
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
async function verifyLine(l, clients) {
|
|
1338
|
+
try {
|
|
1339
|
+
if (!/^\d+$/.test(l.amountUnits)) return "mismatch";
|
|
1340
|
+
const want = BigInt(l.amountUnits);
|
|
1341
|
+
if (l.network.startsWith("solana:")) {
|
|
1342
|
+
const tx = await clients.solana("getTransaction", [l.tx, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0, commitment: "finalized" }]);
|
|
1343
|
+
if (!tx) return "not_found";
|
|
1344
|
+
if (tx.meta?.err) return "failed";
|
|
1345
|
+
const delta = (owner) => {
|
|
1346
|
+
const sum = (xs) => (xs ?? []).filter((b) => b.owner === owner && b.mint === l.token).reduce((a, b) => a + BigInt(b.uiTokenAmount?.amount ?? "0"), 0n);
|
|
1347
|
+
return sum(tx.meta?.postTokenBalances) - sum(tx.meta?.preTokenBalances);
|
|
1348
|
+
};
|
|
1349
|
+
return delta(l.from) === -want && delta(l.to) === want ? "ok" : "mismatch";
|
|
1350
|
+
}
|
|
1351
|
+
const client = clients.evm(l.network);
|
|
1352
|
+
if (!client) return "unsupported";
|
|
1353
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(l.tx) || !isAddress3(l.token) || !isAddress3(l.from) || !isAddress3(l.to)) return "mismatch";
|
|
1354
|
+
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));
|
|
1355
|
+
if (!receipt) return "not_found";
|
|
1356
|
+
if (receipt.status !== "success") return "failed";
|
|
1357
|
+
const hit = receipt.logs.some(
|
|
1358
|
+
(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
|
|
1359
|
+
);
|
|
1360
|
+
return hit ? "ok" : "mismatch";
|
|
1361
|
+
} catch {
|
|
1362
|
+
return "error";
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async function verifyStatement(s, clients = defaultClients(), onLine) {
|
|
1366
|
+
const results = new Array(s.lines.length);
|
|
1367
|
+
let next = 0;
|
|
1368
|
+
await Promise.all(
|
|
1369
|
+
Array.from({ length: 4 }, async () => {
|
|
1370
|
+
while (next < s.lines.length) {
|
|
1371
|
+
const i = next++;
|
|
1372
|
+
results[i] = await verifyLine(s.lines[i], clients);
|
|
1373
|
+
onLine?.(i, results[i]);
|
|
1374
|
+
}
|
|
1375
|
+
})
|
|
1376
|
+
);
|
|
1377
|
+
const usd = (pick) => Math.round(s.lines.reduce((a, l, i) => a + (pick(results[i]) ? Number(l.amountUsd) : 0), 0) * 1e6) / 1e6;
|
|
1378
|
+
const count = (r) => results.filter((x) => x === r).length;
|
|
1379
|
+
const linesUsd = usd(() => true);
|
|
1380
|
+
return {
|
|
1381
|
+
results,
|
|
1382
|
+
lines: s.lines.length,
|
|
1383
|
+
ok: count("ok"),
|
|
1384
|
+
mismatch: count("mismatch"),
|
|
1385
|
+
failed: count("failed"),
|
|
1386
|
+
notFound: count("not_found"),
|
|
1387
|
+
unsupported: count("unsupported"),
|
|
1388
|
+
error: count("error"),
|
|
1389
|
+
confirmedUsd: usd((r) => r === "ok"),
|
|
1390
|
+
linesUsd,
|
|
1391
|
+
// The totals block must add up to the lines it sits next to.
|
|
1392
|
+
totalsMatch: s.totals?.calls === s.lines.length && Math.abs((s.totals?.usd ?? NaN) - linesUsd) < 1e-6
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
var EVM, ENV_RPC, TRANSFER, topicAddr;
|
|
1396
|
+
var init_statement = __esm({
|
|
1397
|
+
"src/statement.ts"() {
|
|
1398
|
+
"use strict";
|
|
1399
|
+
init_solana();
|
|
1400
|
+
EVM = { "eip155:4663": robinhood2, "eip155:8453": base2, "eip155:42161": arbitrum2, "eip155:137": polygon2, "eip155:5042": arc2, "eip155:1868": soneium2 };
|
|
1401
|
+
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" };
|
|
1402
|
+
TRANSFER = toEventSelector2(parseAbiItem2("event Transfer(address indexed from, address indexed to, uint256 value)"));
|
|
1403
|
+
topicAddr = (t) => t ? getAddress3(`0x${t.slice(-40)}`) : null;
|
|
1292
1404
|
}
|
|
1293
1405
|
});
|
|
1294
1406
|
|
|
@@ -1844,6 +1956,8 @@ Wallet
|
|
|
1844
1956
|
wafermap balance USDG / USDC and gas on every payment network
|
|
1845
1957
|
wafermap limits [--per-payment 0.1] [--per-day 1] [--per-swap 25] [--network <net>] [--mcp-swaps on|off] [--mcp-confirm on|off]
|
|
1846
1958
|
wafermap history [--days 7] [--verify] payments and swaps from this machine; --verify checks on-chain
|
|
1959
|
+
wafermap statement [--month 2026-11] [--csv] [--out file] a month of payments to Wafermap and its sellers
|
|
1960
|
+
wafermap statement --verify <file.json> check every line of a statement against the chain
|
|
1847
1961
|
wafermap export-key [--solana] print the private key, or the Solana key for Phantom/Solflare
|
|
1848
1962
|
wafermap keychain [on|off] keep the password for \`wafermap mcp\` in the OS keychain, not in plain text
|
|
1849
1963
|
|
|
@@ -1901,6 +2015,14 @@ function die(msg) {
|
|
|
1901
2015
|
console.error(`\u2715 ${msg}`);
|
|
1902
2016
|
process.exit(1);
|
|
1903
2017
|
}
|
|
2018
|
+
function pricesLine(prices) {
|
|
2019
|
+
if (!prices?.length) return "";
|
|
2020
|
+
const fmt = (p) => `${p.name} ${p.priceUsd !== null ? `$${p.priceUsd}` : "\u2014"}`;
|
|
2021
|
+
const ours = prices.filter((p) => p.chain).map(fmt);
|
|
2022
|
+
const other = prices.filter((p) => !p.chain).map((p) => `${fmt(p)} (not supported)`);
|
|
2023
|
+
return `
|
|
2024
|
+
pays on: ${[...ours, ...other].join(" \xB7 ")}`;
|
|
2025
|
+
}
|
|
1904
2026
|
function inputOf(a, from = 0) {
|
|
1905
2027
|
const input = {};
|
|
1906
2028
|
if (typeof a.flags.json === "string") {
|
|
@@ -1967,7 +2089,8 @@ async function main() {
|
|
|
1967
2089
|
Solana address ${sol} (derived from the same key)
|
|
1968
2090
|
Key encrypted in ${HOME}/keystore.json. Forget the password and the key is gone: run \`wafermap export-key\` to back it up.
|
|
1969
2091
|
Fund it with a few dollars of USDC on Solana (Solana address), or USDG on Robinhood Chain or USDC on Base, Arbitrum, Polygon, Arc or Soneium (EVM address). Payments are gasless; swaps need a little ETH.
|
|
1970
|
-
Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits
|
|
2092
|
+
Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits\`.
|
|
2093
|
+
New here? Your first paid call, step by step: https://wafermap.xyz/start`);
|
|
1971
2094
|
return;
|
|
1972
2095
|
}
|
|
1973
2096
|
case "approve-wafermap": {
|
|
@@ -2071,6 +2194,60 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
2071
2194
|
} else console.log("Add --verify to check on-chain which signed payments were executed.");
|
|
2072
2195
|
return;
|
|
2073
2196
|
}
|
|
2197
|
+
case "statement": {
|
|
2198
|
+
const { fetchStatement: fetchStatement2, parseStatement: parseStatement2, verifyStatement: verifyStatement2 } = await Promise.resolve().then(() => (init_statement(), statement_exports));
|
|
2199
|
+
const { readFileSync: readFileSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
2200
|
+
if (a.flags.verify !== void 0) {
|
|
2201
|
+
const file2 = typeof a.flags.verify === "string" ? a.flags.verify : a._[0];
|
|
2202
|
+
if (!file2) die("Which file? wafermap statement --verify wafermap-statement-2026-11.json");
|
|
2203
|
+
let text2;
|
|
2204
|
+
try {
|
|
2205
|
+
text2 = readFileSync2(file2, "utf8");
|
|
2206
|
+
} catch {
|
|
2207
|
+
die(`Cannot read ${file2}.`);
|
|
2208
|
+
}
|
|
2209
|
+
let st;
|
|
2210
|
+
try {
|
|
2211
|
+
st = parseStatement2(text2);
|
|
2212
|
+
} catch (e) {
|
|
2213
|
+
die(e.message);
|
|
2214
|
+
}
|
|
2215
|
+
console.error(`Checking ${st.lines.length} payments from ${st.month} on-chain\u2026`);
|
|
2216
|
+
const r = await verifyStatement2(st);
|
|
2217
|
+
st.lines.forEach((l, i) => {
|
|
2218
|
+
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}`);
|
|
2219
|
+
});
|
|
2220
|
+
console.log(`
|
|
2221
|
+
${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." : ""}`);
|
|
2222
|
+
if (r.ok !== r.lines)
|
|
2223
|
+
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.`);
|
|
2224
|
+
if (r.mismatch || r.failed || r.notFound || !r.totalsMatch) process.exitCode = 2;
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const evm = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`, or check a downloaded file with --verify.");
|
|
2228
|
+
const wallets = [evm, ...keystoreSolana() ? [keystoreSolana()] : []];
|
|
2229
|
+
const month = typeof a.flags.month === "string" ? a.flags.month : (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
2230
|
+
if (!/^20\d\d-(0[1-9]|1[0-2])$/.test(month)) die("--month must be YYYY-MM, for example 2026-11.");
|
|
2231
|
+
const csv = a.flags.csv === true;
|
|
2232
|
+
const out = await fetchStatement2(wallets, month, { baseUrl: process.env.WAFERMAP_URL, csv, timeoutMs: Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0 });
|
|
2233
|
+
const file = typeof a.flags.out === "string" ? a.flags.out : null;
|
|
2234
|
+
if (typeof out === "string") {
|
|
2235
|
+
if (file) writeFileSync2(file, out);
|
|
2236
|
+
else process.stdout.write(out);
|
|
2237
|
+
if (file) console.error(`Saved ${file}.`);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (file) {
|
|
2241
|
+
writeFileSync2(file, JSON.stringify(out, null, 2) + "\n");
|
|
2242
|
+
console.error(`Saved ${file}. Check it any time with: wafermap statement --verify ${file}`);
|
|
2243
|
+
}
|
|
2244
|
+
if (!file || a.flags.print !== void 0) {
|
|
2245
|
+
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}`);
|
|
2246
|
+
console.log(`
|
|
2247
|
+
${month}: ${out.totals.calls} paid calls, $${out.totals.usd}.${out.lines.length ? " Save it with --out statement.json, then check it with --verify." : ""}`);
|
|
2248
|
+
}
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2074
2251
|
case "resources": {
|
|
2075
2252
|
const q = a._.join(" ").toLowerCase();
|
|
2076
2253
|
const list = await listResources(process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
|
|
@@ -2083,7 +2260,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
2083
2260
|
const wm = openWallet(await password());
|
|
2084
2261
|
const list = await wm.discover(q, Number(a.flags.limit ?? 10));
|
|
2085
2262
|
for (const s of list) console.log(`${s.payable ? "\u25CF" : "\u25CB"} ${s.priceUsd !== null ? `$${s.priceUsd}`.padEnd(8) : "\u2014".padEnd(8)} ${s.method.padEnd(4)} ${s.url}
|
|
2086
|
-
${s.description.slice(0, 140)}${s.input ? `
|
|
2263
|
+
${s.description.slice(0, 140)}${pricesLine(s.prices)}${s.input ? `
|
|
2087
2264
|
input: ${JSON.stringify(s.input).slice(0, 140)}` : ""}`);
|
|
2088
2265
|
return console.log(`
|
|
2089
2266
|
\u25CF payable with USDG (Robinhood Chain) or USDC (Solana, Base, Arbitrum, Polygon, Arc, Soneium) \u25CB other networks only`);
|
package/dist/index.js
CHANGED
|
@@ -1026,6 +1026,103 @@ var Wafermap = class {
|
|
|
1026
1026
|
}
|
|
1027
1027
|
}
|
|
1028
1028
|
};
|
|
1029
|
+
|
|
1030
|
+
// src/statement.ts
|
|
1031
|
+
import { createPublicClient as createPublicClient2, getAddress as getAddress3, http as http2, isAddress as isAddress3, parseAbiItem as parseAbiItem2, toEventSelector as toEventSelector2 } from "viem";
|
|
1032
|
+
import { arbitrum as arbitrum2, arc as arc2, base as base2, polygon as polygon2, robinhood as robinhood2, soneium as soneium2 } from "viem/chains";
|
|
1033
|
+
var EVM = { "eip155:4663": robinhood2, "eip155:8453": base2, "eip155:42161": arbitrum2, "eip155:137": polygon2, "eip155:5042": arc2, "eip155:1868": soneium2 };
|
|
1034
|
+
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" };
|
|
1035
|
+
var TRANSFER = toEventSelector2(parseAbiItem2("event Transfer(address indexed from, address indexed to, uint256 value)"));
|
|
1036
|
+
var topicAddr = (t) => t ? getAddress3(`0x${t.slice(-40)}`) : null;
|
|
1037
|
+
async function fetchStatement(wallets, month, opts = {}) {
|
|
1038
|
+
const url = `${(opts.baseUrl ?? "https://wafermap.xyz").replace(/\/$/, "")}/api/statement?wallet=${encodeURIComponent(wallets.join(","))}&month=${encodeURIComponent(month)}${opts.csv ? "&format=csv" : ""}`;
|
|
1039
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 6e4) });
|
|
1040
|
+
if (!r.ok) {
|
|
1041
|
+
const j = await r.json().catch(() => null);
|
|
1042
|
+
throw new Error(j?.error?.message ?? `Statement request failed (${r.status}).`);
|
|
1043
|
+
}
|
|
1044
|
+
return opts.csv ? r.text() : await r.json();
|
|
1045
|
+
}
|
|
1046
|
+
function parseStatement(text) {
|
|
1047
|
+
let s;
|
|
1048
|
+
try {
|
|
1049
|
+
s = JSON.parse(text);
|
|
1050
|
+
} catch {
|
|
1051
|
+
throw new Error("That file is not JSON. Download the JSON statement (not the CSV) to verify it.");
|
|
1052
|
+
}
|
|
1053
|
+
if (s?.kind !== "wafermap-statement" || !Array.isArray(s.lines)) throw new Error("That file is not a Wafermap statement.");
|
|
1054
|
+
return s;
|
|
1055
|
+
}
|
|
1056
|
+
function defaultClients(env = process.env) {
|
|
1057
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1058
|
+
return {
|
|
1059
|
+
evm: (network) => {
|
|
1060
|
+
const chain = EVM[network];
|
|
1061
|
+
if (!chain) return null;
|
|
1062
|
+
if (!cache.has(network)) cache.set(network, createPublicClient2({ chain, transport: http2(env[ENV_RPC[network]] || void 0, { timeout: 3e4 }) }));
|
|
1063
|
+
return cache.get(network);
|
|
1064
|
+
},
|
|
1065
|
+
solana: (method, params) => solanaRpc(env.WAFERMAP_RPC_SOLANA || SOLANA_PUBLIC_RPC, method, params)
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async function verifyLine(l, clients) {
|
|
1069
|
+
try {
|
|
1070
|
+
if (!/^\d+$/.test(l.amountUnits)) return "mismatch";
|
|
1071
|
+
const want = BigInt(l.amountUnits);
|
|
1072
|
+
if (l.network.startsWith("solana:")) {
|
|
1073
|
+
const tx = await clients.solana("getTransaction", [l.tx, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0, commitment: "finalized" }]);
|
|
1074
|
+
if (!tx) return "not_found";
|
|
1075
|
+
if (tx.meta?.err) return "failed";
|
|
1076
|
+
const delta = (owner) => {
|
|
1077
|
+
const sum = (xs) => (xs ?? []).filter((b) => b.owner === owner && b.mint === l.token).reduce((a, b) => a + BigInt(b.uiTokenAmount?.amount ?? "0"), 0n);
|
|
1078
|
+
return sum(tx.meta?.postTokenBalances) - sum(tx.meta?.preTokenBalances);
|
|
1079
|
+
};
|
|
1080
|
+
return delta(l.from) === -want && delta(l.to) === want ? "ok" : "mismatch";
|
|
1081
|
+
}
|
|
1082
|
+
const client = clients.evm(l.network);
|
|
1083
|
+
if (!client) return "unsupported";
|
|
1084
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(l.tx) || !isAddress3(l.token) || !isAddress3(l.from) || !isAddress3(l.to)) return "mismatch";
|
|
1085
|
+
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));
|
|
1086
|
+
if (!receipt) return "not_found";
|
|
1087
|
+
if (receipt.status !== "success") return "failed";
|
|
1088
|
+
const hit = receipt.logs.some(
|
|
1089
|
+
(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
|
|
1090
|
+
);
|
|
1091
|
+
return hit ? "ok" : "mismatch";
|
|
1092
|
+
} catch {
|
|
1093
|
+
return "error";
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async function verifyStatement(s, clients = defaultClients(), onLine) {
|
|
1097
|
+
const results = new Array(s.lines.length);
|
|
1098
|
+
let next = 0;
|
|
1099
|
+
await Promise.all(
|
|
1100
|
+
Array.from({ length: 4 }, async () => {
|
|
1101
|
+
while (next < s.lines.length) {
|
|
1102
|
+
const i = next++;
|
|
1103
|
+
results[i] = await verifyLine(s.lines[i], clients);
|
|
1104
|
+
onLine?.(i, results[i]);
|
|
1105
|
+
}
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
const usd = (pick) => Math.round(s.lines.reduce((a, l, i) => a + (pick(results[i]) ? Number(l.amountUsd) : 0), 0) * 1e6) / 1e6;
|
|
1109
|
+
const count = (r) => results.filter((x) => x === r).length;
|
|
1110
|
+
const linesUsd = usd(() => true);
|
|
1111
|
+
return {
|
|
1112
|
+
results,
|
|
1113
|
+
lines: s.lines.length,
|
|
1114
|
+
ok: count("ok"),
|
|
1115
|
+
mismatch: count("mismatch"),
|
|
1116
|
+
failed: count("failed"),
|
|
1117
|
+
notFound: count("not_found"),
|
|
1118
|
+
unsupported: count("unsupported"),
|
|
1119
|
+
error: count("error"),
|
|
1120
|
+
confirmedUsd: usd((r) => r === "ok"),
|
|
1121
|
+
linesUsd,
|
|
1122
|
+
// The totals block must add up to the lines it sits next to.
|
|
1123
|
+
totalsMatch: s.totals?.calls === s.lines.length && Math.abs((s.totals?.usd ?? NaN) - linesUsd) < 1e-6
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1029
1126
|
export {
|
|
1030
1127
|
DEFAULT_LIMITS,
|
|
1031
1128
|
DEFAULT_TIMEOUT_MS,
|
|
@@ -1048,15 +1145,19 @@ export {
|
|
|
1048
1145
|
decodeSwap,
|
|
1049
1146
|
encodePass,
|
|
1050
1147
|
executeSwap,
|
|
1148
|
+
fetchStatement,
|
|
1051
1149
|
holderInfo,
|
|
1052
1150
|
listResources,
|
|
1053
1151
|
makePass,
|
|
1054
1152
|
mergeLog,
|
|
1153
|
+
parseStatement,
|
|
1055
1154
|
passMessage,
|
|
1056
1155
|
quoteSwap,
|
|
1057
1156
|
readQuote,
|
|
1058
1157
|
signPayment,
|
|
1059
1158
|
signTokenPayment,
|
|
1159
|
+
verifyLine,
|
|
1160
|
+
verifyStatement,
|
|
1060
1161
|
wafermapPriceUsd,
|
|
1061
1162
|
x402fetch
|
|
1062
1163
|
};
|
package/dist/types/src/core.d.ts
CHANGED
|
@@ -186,6 +186,12 @@ export declare class Wafermap {
|
|
|
186
186
|
name: string | null;
|
|
187
187
|
description: string;
|
|
188
188
|
priceUsd: number | null;
|
|
189
|
+
prices?: {
|
|
190
|
+
network: string;
|
|
191
|
+
chain: string | null;
|
|
192
|
+
name: string;
|
|
193
|
+
priceUsd: number | null;
|
|
194
|
+
}[];
|
|
189
195
|
networks: string[];
|
|
190
196
|
payable: boolean;
|
|
191
197
|
input: unknown;
|
|
@@ -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.11",
|
|
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": {
|