@wafermapxyz/cli 0.4.4 → 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 +18 -0
- package/README.md +3 -1
- package/dist/cli.js +183 -17
- package/dist/index.js +151 -7
- package/dist/types/shared/x402client.d.ts +28 -0
- package/dist/types/src/core.d.ts +22 -3
- package/dist/types/src/index.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.4.5 (2 Nov 2026)
|
|
13
|
+
|
|
14
|
+
**Pay Wafermap in $WAFERMAP, at 50% off. The tokens are burned.**
|
|
15
|
+
|
|
16
|
+
- `wafermap call <id> --pay wafermap` (MCP `wafermap_call` with `payIn: "wafermap"`, SDK `call(id, input, { payIn: "wafermap" })`) pays one of Wafermap's own resources in $WAFERMAP instead of USDG/USDC. $WAFERMAP has no EIP-3009, so the payment is a Uniswap Permit2 signature for one exact amount, spendable for two minutes only by Wafermap's relayer, which sends it to the dead address once the call succeeded. A failed call moves nothing.
|
|
17
|
+
- `wafermap approve-wafermap`: the one-time Permit2 approval (one Robinhood Chain transaction, paid in ETH).
|
|
18
|
+
- What the client checks before it signs, trusting the server for none of it: the spender is Wafermap's relayer as fixed in this release (`WAFERMAP_RELAYERS`); the option names the canonical Permit2, the $WAFERMAP token and the dead address; the amount is valued at a price this client reads itself (GeckoTerminal) and refused when it is worth more than the quote states. The caps count the higher of the two values.
|
|
19
|
+
- Only `call()` to Wafermap ever signs a $WAFERMAP permit. `pay()` to third-party services never does, whatever they offer, because a Permit2 spender chooses where the tokens go.
|
|
20
|
+
|
|
3
21
|
## 0.4.4 (2 Nov 2026)
|
|
4
22
|
|
|
5
23
|
**$WAFERMAP holder tiers.**
|
package/README.md
CHANGED
|
@@ -84,6 +84,8 @@ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `le
|
|
|
84
84
|
|
|
85
85
|
A wallet that holds at least 100,000 $WAFERMAP on Robinhood Chain pays 50% less for Wafermap's own resources when it pays on an EVM network (Robinhood Chain, Base, Arbitrum, Polygon). Nothing to set up: `wafermap call` names the wallet to Wafermap and the quote comes back discounted; the server re-checks the balance of the wallet that signs. Marketplace listings and Solana payments keep the regular price. Check a wallet: https://wafermap.xyz/api/holder?address=0x…, or run `wafermap holder`.
|
|
86
86
|
|
|
87
|
+
Wafermap's own resources can also be **paid in $WAFERMAP at 50% off, and the tokens are burned**: `wafermap approve-wafermap` once (a Permit2 approval, one Robinhood Chain transaction), then `wafermap call <id> --pay wafermap`. Each payment is a Permit2 signature for one exact amount, usable for two minutes only by Wafermap's relayer (fixed in the client), checked against a market price the client reads itself.
|
|
88
|
+
|
|
87
89
|
With 1,000,000 $WAFERMAP the wallet is **Pro**: 10 free calls a day (UTC) to resources listed at $0.01 or less. `wafermap call` sends a holder pass (a signed message, no transaction, valid 24 hours) to Wafermap only; when a call is free, nothing is signed for payment and it prints `✓ Free call`. Holders of 100,000 also unlock x402 Tracker Pro: `wafermap pass` prints a pass for its API. Details: https://wafermap.xyz/docs#holders
|
|
88
90
|
|
|
89
91
|
## Where the limits live
|
|
@@ -92,7 +94,7 @@ The caps sit with the key, on your machine. Before the wallet signs anything it
|
|
|
92
94
|
|
|
93
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).)
|
|
94
96
|
|
|
95
|
-
**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.
|
|
96
98
|
|
|
97
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.
|
|
98
100
|
|
package/dist/cli.js
CHANGED
|
@@ -296,6 +296,34 @@ var init_solana = __esm({
|
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
// shared/x402client.ts
|
|
299
|
+
async function wafermapPriceUsd(f = fetch) {
|
|
300
|
+
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) });
|
|
301
|
+
const j = await res.json().catch(() => null);
|
|
302
|
+
const p = Number(j?.data?.attributes?.token_prices?.[WAFERMAP_TOKEN]);
|
|
303
|
+
if (!res.ok || !(p > 0)) throw new X402Error("price_unavailable", "The $WAFERMAP market price could not be read, so the amount cannot be checked. Nothing was signed; pay in USDG or USDC.");
|
|
304
|
+
return p;
|
|
305
|
+
}
|
|
306
|
+
function tokenOption(r) {
|
|
307
|
+
const e = r?.extra ?? {};
|
|
308
|
+
const same = (a, b) => String(a ?? "").toLowerCase() === b.toLowerCase();
|
|
309
|
+
if (r?.scheme !== TOKEN_SCHEME || r.network !== "eip155:4663" || !same(r.asset, WAFERMAP_TOKEN) || !same(r.payTo, DEAD) || !same(e.permit2, PERMIT2)) return;
|
|
310
|
+
if (!/^\d{1,40}$/.test(String(r.amount)) || !/^0x[0-9a-fA-F]{40}$/.test(String(e.spender)) || !(Number(e.usd) > 0)) return;
|
|
311
|
+
if (!WAFERMAP_RELAYERS.some((a) => same(e.spender, a))) return;
|
|
312
|
+
return { amount: String(r.amount), usd: Number(e.usd), spender: e.spender, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, raw: r };
|
|
313
|
+
}
|
|
314
|
+
async function signTokenPayment(signer, t, resource) {
|
|
315
|
+
const nonce = BigInt(randomNonce()) >> 8n;
|
|
316
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(t.maxTimeoutSeconds, 120));
|
|
317
|
+
const permitted = { token: WAFERMAP_TOKEN, amount: BigInt(t.amount) };
|
|
318
|
+
let signature;
|
|
319
|
+
try {
|
|
320
|
+
signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: 4663, verifyingContract: PERMIT2 }, types: PERMIT_TYPES, primaryType: "PermitTransferFrom", message: { permitted, spender: t.spender, nonce, deadline } });
|
|
321
|
+
} catch (e) {
|
|
322
|
+
throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
|
|
323
|
+
}
|
|
324
|
+
const payload = { owner: signer.address, permit: { permitted: { token: WAFERMAP_TOKEN, amount: t.amount }, nonce: nonce.toString(), deadline: deadline.toString() }, signature };
|
|
325
|
+
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: t.raw, payload })), deadline: Number(deadline) };
|
|
326
|
+
}
|
|
299
327
|
function chainOf(network) {
|
|
300
328
|
const n = String(network ?? "").toLowerCase();
|
|
301
329
|
return PAY_CHAINS.find((c) => c.caip.toLowerCase() === n || c.v1 === n || c.id > 0 && String(c.id) === n);
|
|
@@ -314,8 +342,13 @@ async function readQuote(res) {
|
|
|
314
342
|
const version = Number(body.x402Version) === 1 ? 1 : 2;
|
|
315
343
|
const accepts = [];
|
|
316
344
|
const offered = [];
|
|
345
|
+
let token;
|
|
317
346
|
for (const r of body.accepts) {
|
|
318
347
|
offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
|
|
348
|
+
if (r?.scheme === TOKEN_SCHEME) {
|
|
349
|
+
token ??= tokenOption(r);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
319
352
|
const chain = chainOf(r.network);
|
|
320
353
|
if (!chain || r.scheme !== "exact") continue;
|
|
321
354
|
if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
|
|
@@ -329,7 +362,7 @@ async function readQuote(res) {
|
|
|
329
362
|
accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
|
|
330
363
|
}
|
|
331
364
|
const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
|
|
332
|
-
return { version, resource: body.resource ?? null, accepts, offered, inputError };
|
|
365
|
+
return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {} };
|
|
333
366
|
}
|
|
334
367
|
function choose(q, prefer = CHAIN_KEYS) {
|
|
335
368
|
for (const k of prefer) {
|
|
@@ -381,6 +414,23 @@ async function x402fetch(url, init, opts) {
|
|
|
381
414
|
if (first.status !== 402) return { res: first, paid: null };
|
|
382
415
|
const quote = await readQuote(first);
|
|
383
416
|
if (quote.inputError) throw new X402Error(quote.inputError.code, quote.inputError.message);
|
|
417
|
+
if (opts.payIn === "wafermap") {
|
|
418
|
+
const t = quote.token;
|
|
419
|
+
if (!t) throw new X402Error("token_not_offered", "This call is not offered in $WAFERMAP right now (or not to a known Wafermap relayer). Pay in USDG or USDC.");
|
|
420
|
+
const value = Number(BigInt(t.amount) / 10n ** 12n) / 1e6 * await (opts.tokenPriceUsd ?? (() => wafermapPriceUsd(fetch)))();
|
|
421
|
+
if (value > t.usd * TOKEN_VALUE_SLACK + 5e-4) throw new X402Error("price_mismatch", `The quote asks for $WAFERMAP worth about $${value.toFixed(4)} at the market price, more than the $${t.usd} it states. Nothing was signed.`);
|
|
422
|
+
const usd = Math.max(t.usd, Math.round(value * 1e6) / 1e6);
|
|
423
|
+
const req2 = { version: 2, chain: PAY_CHAINS[0], amount: String(Math.round(usd * 1e6)), usd, payTo: DEAD, asset: WAFERMAP_TOKEN, maxTimeoutSeconds: t.maxTimeoutSeconds, extra: {}, raw: t.raw };
|
|
424
|
+
if (opts.approve) await opts.approve(req2);
|
|
425
|
+
const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
|
|
426
|
+
const header2 = await signTokenPayment(signer2, t, quote.resource);
|
|
427
|
+
const signed2 = { req: req2, from: signer2.address, token: { amount: t.amount } };
|
|
428
|
+
opts.onSigned?.(signed2);
|
|
429
|
+
const headers2 = new Headers(init.headers);
|
|
430
|
+
headers2.set(header2.name, header2.value);
|
|
431
|
+
const res2 = await f(url, { ...init, headers: headers2 });
|
|
432
|
+
return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
|
|
433
|
+
}
|
|
384
434
|
const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
|
|
385
435
|
if (opts.approve) await opts.approve(req);
|
|
386
436
|
const svm = req.chain.vm === "svm";
|
|
@@ -393,7 +443,7 @@ async function x402fetch(url, init, opts) {
|
|
|
393
443
|
const res = await f(url, { ...init, headers });
|
|
394
444
|
return { res, paid: { ...signed, tx: readSettlement(res)?.tx ?? null } };
|
|
395
445
|
}
|
|
396
|
-
var USDC_DOMAIN, PAY_CHAINS, EVM_CHAINS, CHAIN_KEYS, X402Error, b64encode, b64decode, TYPES, MAX_VALIDITY_SECONDS, randomNonce;
|
|
446
|
+
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;
|
|
397
447
|
var init_x402client = __esm({
|
|
398
448
|
"shared/x402client.ts"() {
|
|
399
449
|
"use strict";
|
|
@@ -408,6 +458,24 @@ var init_x402client = __esm({
|
|
|
408
458
|
];
|
|
409
459
|
EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
|
|
410
460
|
CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
|
|
461
|
+
WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
|
|
462
|
+
PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
463
|
+
DEAD = "0x000000000000000000000000000000000000dEaD";
|
|
464
|
+
TOKEN_SCHEME = "wafermap-permit2";
|
|
465
|
+
WAFERMAP_RELAYERS = ["0xD21547a387a9CfFb747211983c91f457742Ef00D"];
|
|
466
|
+
TOKEN_VALUE_SLACK = 1.25;
|
|
467
|
+
PERMIT_TYPES = {
|
|
468
|
+
PermitTransferFrom: [
|
|
469
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
470
|
+
{ name: "spender", type: "address" },
|
|
471
|
+
{ name: "nonce", type: "uint256" },
|
|
472
|
+
{ name: "deadline", type: "uint256" }
|
|
473
|
+
],
|
|
474
|
+
TokenPermissions: [
|
|
475
|
+
{ name: "token", type: "address" },
|
|
476
|
+
{ name: "amount", type: "uint256" }
|
|
477
|
+
]
|
|
478
|
+
};
|
|
411
479
|
X402Error = class extends Error {
|
|
412
480
|
constructor(code, message) {
|
|
413
481
|
super(message);
|
|
@@ -492,7 +560,7 @@ __export(core_exports, {
|
|
|
492
560
|
listResources: () => listResources,
|
|
493
561
|
mergeLog: () => mergeLog
|
|
494
562
|
});
|
|
495
|
-
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";
|
|
496
564
|
import { privateKeyToAccount } from "viem/accounts";
|
|
497
565
|
import { arbitrum, base, polygon, robinhood } from "viem/chains";
|
|
498
566
|
function mergeLog(entries) {
|
|
@@ -503,14 +571,56 @@ function mergeLog(entries) {
|
|
|
503
571
|
return { ...e, status: o.status, tx: o.tx ?? e.tx, reported: o.reported ?? null, ...o.status === "aborted" ? { usd: 0 } : {} };
|
|
504
572
|
});
|
|
505
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
|
+
}
|
|
506
594
|
async function checkPayment(e, rpc) {
|
|
507
595
|
if (e.network === "solana" || !e.nonce || !(e.network in CHAINS)) return "unknown";
|
|
508
596
|
const chain = PAY_CHAINS.find((c) => c.key === e.network);
|
|
509
597
|
try {
|
|
510
598
|
const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
|
|
511
599
|
const used = await client.readContract({ address: chain.stable, abi: AUTH_STATE_ABI, functionName: "authorizationState", args: [e.payer, e.nonce] });
|
|
512
|
-
if (used) return "
|
|
513
|
-
|
|
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";
|
|
514
624
|
} catch {
|
|
515
625
|
return "error";
|
|
516
626
|
}
|
|
@@ -538,7 +648,7 @@ async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAU
|
|
|
538
648
|
if (!r.ok || !Array.isArray(j?.data)) throw new WafermapError("network_error", `${host} returned no catalog (HTTP ${r.status}).`);
|
|
539
649
|
return j.data;
|
|
540
650
|
}
|
|
541
|
-
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;
|
|
542
652
|
var init_core = __esm({
|
|
543
653
|
"src/core.ts"() {
|
|
544
654
|
"use strict";
|
|
@@ -558,6 +668,7 @@ var init_core = __esm({
|
|
|
558
668
|
};
|
|
559
669
|
CHAINS = { robinhood, base, arbitrum, polygon };
|
|
560
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)");
|
|
561
672
|
DEFAULT_LIMITS = { perPayment: 0.1, perDay: 1, perSwap: 25 };
|
|
562
673
|
DEFAULT_TIMEOUT_MS = 6e4;
|
|
563
674
|
timedOut = (e) => e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError";
|
|
@@ -575,6 +686,7 @@ var init_core = __esm({
|
|
|
575
686
|
timeoutMs;
|
|
576
687
|
/** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
|
|
577
688
|
fetch;
|
|
689
|
+
tokenPriceUsd;
|
|
578
690
|
constructor(o) {
|
|
579
691
|
const account = o.account ?? (o.privateKey ? privateKeyToAccount(o.privateKey) : null);
|
|
580
692
|
if (!account) throw new WafermapError("no_account", "Pass privateKey or account.");
|
|
@@ -587,6 +699,7 @@ var init_core = __esm({
|
|
|
587
699
|
this.approveFn = o.approve;
|
|
588
700
|
this.rpc = o.rpc ?? {};
|
|
589
701
|
this.timeoutMs = o.timeoutMs && o.timeoutMs > 0 ? o.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
702
|
+
this.tokenPriceUsd = o.tokenPriceUsd;
|
|
590
703
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
591
704
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
592
705
|
const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
|
|
@@ -660,7 +773,7 @@ var init_core = __esm({
|
|
|
660
773
|
this.queue = prev.then(() => held);
|
|
661
774
|
return prev.then(() => release);
|
|
662
775
|
}
|
|
663
|
-
async paidFetch(kind, what, url, init, network) {
|
|
776
|
+
async paidFetch(kind, what, url, init, network, payIn) {
|
|
664
777
|
let id = null;
|
|
665
778
|
let release = null;
|
|
666
779
|
const unlock = () => {
|
|
@@ -673,9 +786,11 @@ var init_core = __esm({
|
|
|
673
786
|
solana: this.solana ?? void 0,
|
|
674
787
|
prefer: this.prefer(network),
|
|
675
788
|
fetch: this.fetch,
|
|
789
|
+
payIn,
|
|
790
|
+
tokenPriceUsd: this.tokenPriceUsd,
|
|
676
791
|
approve: async (req) => {
|
|
677
792
|
release = await this.acquire();
|
|
678
|
-
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: `${req.chain.symbol} to ${req.payTo}` });
|
|
793
|
+
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}` });
|
|
679
794
|
},
|
|
680
795
|
onSigned: (s) => {
|
|
681
796
|
id = this.booked(kind, what, s);
|
|
@@ -711,9 +826,33 @@ var init_core = __esm({
|
|
|
711
826
|
this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
|
|
712
827
|
return this.passCache;
|
|
713
828
|
}
|
|
829
|
+
// ---- Paying in $WAFERMAP (Permit2; the tokens are burned) ----
|
|
830
|
+
/** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
|
|
831
|
+
async wafermapApproved() {
|
|
832
|
+
const a = await this.client("robinhood").readContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "allowance", args: [this.address, PERMIT2] });
|
|
833
|
+
return a >= 10n ** 30n;
|
|
834
|
+
}
|
|
835
|
+
/** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
|
|
836
|
+
* transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
|
|
837
|
+
* spendable only by Wafermap's relayer for two minutes. */
|
|
838
|
+
async approveWafermap() {
|
|
839
|
+
const wallet = createWalletClient({ account: this.account, chain: CHAINS.robinhood, transport: http(this.rpc.robinhood) });
|
|
840
|
+
try {
|
|
841
|
+
const tx = await wallet.writeContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "approve", args: [PERMIT2, 2n ** 256n - 1n] });
|
|
842
|
+
const rc = await this.client("robinhood").waitForTransactionReceipt({ hash: tx, timeout: 6e4 });
|
|
843
|
+
if (rc.status !== "success") throw new WafermapError("approve_failed", `The approval reverted: ${tx}`);
|
|
844
|
+
return tx;
|
|
845
|
+
} catch (e) {
|
|
846
|
+
if (e instanceof WafermapError) throw e;
|
|
847
|
+
throw new WafermapError("approve_failed", e?.shortMessage ?? e?.message ?? "The approval failed.");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
714
850
|
async call(id, input = {}, o = {}) {
|
|
851
|
+
const inToken = o.payIn === "wafermap";
|
|
852
|
+
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.");
|
|
853
|
+
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.");
|
|
715
854
|
const pass = id.startsWith("m/") ? null : await this.holderPass();
|
|
716
|
-
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network);
|
|
855
|
+
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
|
|
717
856
|
const settle = (r2, out2) => {
|
|
718
857
|
if (!r2.paid || !r2.id) return;
|
|
719
858
|
const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
|
|
@@ -1023,7 +1162,7 @@ var VERSION;
|
|
|
1023
1162
|
var init_version = __esm({
|
|
1024
1163
|
"src/version.ts"() {
|
|
1025
1164
|
"use strict";
|
|
1026
|
-
VERSION = "0.4.
|
|
1165
|
+
VERSION = "0.4.6";
|
|
1027
1166
|
}
|
|
1028
1167
|
});
|
|
1029
1168
|
|
|
@@ -1123,11 +1262,16 @@ async function runMcp() {
|
|
|
1123
1262
|
{
|
|
1124
1263
|
title: "Call a Wafermap resource",
|
|
1125
1264
|
description: "Run one Wafermap resource and pay its exact price over x402 from the local wallet, within the per-payment and daily caps. Failed calls are not charged.",
|
|
1126
|
-
inputSchema: {
|
|
1265
|
+
inputSchema: {
|
|
1266
|
+
resource: z.string().describe("Resource id, e.g. market-brief, prediction-markets, stock-signal, flux-2-dev."),
|
|
1267
|
+
input: z.record(z.string(), z.any()).optional().describe("Input fields for the resource."),
|
|
1268
|
+
network: NET,
|
|
1269
|
+
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.")
|
|
1270
|
+
}
|
|
1127
1271
|
},
|
|
1128
|
-
async ({ resource, input, network }) => {
|
|
1272
|
+
async ({ resource, input, network, payIn }) => {
|
|
1129
1273
|
try {
|
|
1130
|
-
return result(await wm.call(resource, input ?? {}, { network }));
|
|
1274
|
+
return result(await wm.call(resource, input ?? {}, { network, payIn }));
|
|
1131
1275
|
} catch (e) {
|
|
1132
1276
|
return failure(e);
|
|
1133
1277
|
}
|
|
@@ -1578,6 +1722,8 @@ Wallet
|
|
|
1578
1722
|
Pay
|
|
1579
1723
|
wafermap resources [words] Wafermap's pay-per-call catalog
|
|
1580
1724
|
wafermap call <id> [field=value ...] run a resource and pay its price (--json '{...}' for complex input)
|
|
1725
|
+
wafermap call <id> ... --pay wafermap pay a Wafermap resource in $WAFERMAP at 50% off (the tokens are burned)
|
|
1726
|
+
wafermap approve-wafermap one-time approval so the wallet can pay in $WAFERMAP (Robinhood Chain, ETH gas)
|
|
1581
1727
|
wafermap holder what this wallet's $WAFERMAP unlocks: holder price, Pro free calls left today
|
|
1582
1728
|
wafermap pass print a holder pass (X-Wafermap-Pass) for scripts and x402 Tracker Pro
|
|
1583
1729
|
wafermap discover <words> search third-party x402 services
|
|
@@ -1695,6 +1841,14 @@ async function main() {
|
|
|
1695
1841
|
Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits\`.`);
|
|
1696
1842
|
return;
|
|
1697
1843
|
}
|
|
1844
|
+
case "approve-wafermap": {
|
|
1845
|
+
const wm = openWallet(await password());
|
|
1846
|
+
if (await wm.wafermapApproved()) return console.log("\u2713 Already approved: this wallet can pay Wafermap in $WAFERMAP (wafermap call <id> --pay wafermap).");
|
|
1847
|
+
console.error(`This sends one transaction on Robinhood Chain from ${wm.address}, paid in ETH: approve Permit2 (Uniswap, 0x000000000022D473030F116dDEE9F6B43aC78BA3) to move this wallet's $WAFERMAP. Each payment after that is a separate signature for one exact amount, usable only by Wafermap's relayer for two minutes.`);
|
|
1848
|
+
if (!yes && !/^y(es)?$/i.test((await ask("Send the approval? [y/N] ")).trim())) die("Cancelled. Nothing was sent.");
|
|
1849
|
+
const tx = await wm.approveWafermap();
|
|
1850
|
+
return console.log(`\u2713 Approved \xB7 ${EXPLORER("robinhood")}/tx/${tx}`);
|
|
1851
|
+
}
|
|
1698
1852
|
case "holder": {
|
|
1699
1853
|
const addr = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
|
|
1700
1854
|
const j = await holderInfo(addr, process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
|
|
@@ -1759,7 +1913,16 @@ async function main() {
|
|
|
1759
1913
|
if (!rows.length) return console.log(`Nothing in the last ${days} days.`);
|
|
1760
1914
|
const verify = a.flags.verify !== void 0;
|
|
1761
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 };
|
|
1762
|
-
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
|
+
}
|
|
1763
1926
|
rows.forEach((e, i) => {
|
|
1764
1927
|
const state = [e.status, chain[i] ? `on-chain: ${chain[i]}` : null].filter(Boolean).join(", ");
|
|
1765
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}` : ""}`);
|
|
@@ -1770,7 +1933,8 @@ async function main() {
|
|
|
1770
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.`);
|
|
1771
1934
|
if (verify) {
|
|
1772
1935
|
const never = rows.filter((e, i) => e.kind !== "swap" && chain[i] === "unused").reduce((s, e) => s + e.usd, 0);
|
|
1773
|
-
|
|
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).`);
|
|
1774
1938
|
} else console.log("Add --verify to check on-chain which signed payments were executed.");
|
|
1775
1939
|
return;
|
|
1776
1940
|
}
|
|
@@ -1794,8 +1958,10 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
1794
1958
|
case "call": {
|
|
1795
1959
|
const id = a._[0] ?? die("Which resource? e.g. wafermap call gas-now network=base");
|
|
1796
1960
|
const wm = openWallet(await password(), approver(yes));
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1961
|
+
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)");
|
|
1962
|
+
const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network), payIn });
|
|
1963
|
+
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}`);
|
|
1964
|
+
else if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
|
|
1799
1965
|
else if (out.free) console.error(`\u2713 Free call ($WAFERMAP Pro) \xB7 ${out.free.left} of ${out.free.perDay} left today. Nothing was paid.`);
|
|
1800
1966
|
if (typeof a.flags.out === "string") await saveMedia(out.result, a.flags.out);
|
|
1801
1967
|
return printResult(out.result);
|
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
|
|
|
@@ -283,6 +283,52 @@ var PAY_CHAINS = [
|
|
|
283
283
|
];
|
|
284
284
|
var EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
|
|
285
285
|
var CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
|
|
286
|
+
var WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
|
|
287
|
+
var PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
288
|
+
var DEAD = "0x000000000000000000000000000000000000dEaD";
|
|
289
|
+
var TOKEN_SCHEME = "wafermap-permit2";
|
|
290
|
+
var WAFERMAP_RELAYERS = ["0xD21547a387a9CfFb747211983c91f457742Ef00D"];
|
|
291
|
+
var TOKEN_VALUE_SLACK = 1.25;
|
|
292
|
+
async function wafermapPriceUsd(f = fetch) {
|
|
293
|
+
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) });
|
|
294
|
+
const j = await res.json().catch(() => null);
|
|
295
|
+
const p = Number(j?.data?.attributes?.token_prices?.[WAFERMAP_TOKEN]);
|
|
296
|
+
if (!res.ok || !(p > 0)) throw new X402Error("price_unavailable", "The $WAFERMAP market price could not be read, so the amount cannot be checked. Nothing was signed; pay in USDG or USDC.");
|
|
297
|
+
return p;
|
|
298
|
+
}
|
|
299
|
+
var PERMIT_TYPES = {
|
|
300
|
+
PermitTransferFrom: [
|
|
301
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
302
|
+
{ name: "spender", type: "address" },
|
|
303
|
+
{ name: "nonce", type: "uint256" },
|
|
304
|
+
{ name: "deadline", type: "uint256" }
|
|
305
|
+
],
|
|
306
|
+
TokenPermissions: [
|
|
307
|
+
{ name: "token", type: "address" },
|
|
308
|
+
{ name: "amount", type: "uint256" }
|
|
309
|
+
]
|
|
310
|
+
};
|
|
311
|
+
function tokenOption(r) {
|
|
312
|
+
const e = r?.extra ?? {};
|
|
313
|
+
const same = (a, b) => String(a ?? "").toLowerCase() === b.toLowerCase();
|
|
314
|
+
if (r?.scheme !== TOKEN_SCHEME || r.network !== "eip155:4663" || !same(r.asset, WAFERMAP_TOKEN) || !same(r.payTo, DEAD) || !same(e.permit2, PERMIT2)) return;
|
|
315
|
+
if (!/^\d{1,40}$/.test(String(r.amount)) || !/^0x[0-9a-fA-F]{40}$/.test(String(e.spender)) || !(Number(e.usd) > 0)) return;
|
|
316
|
+
if (!WAFERMAP_RELAYERS.some((a) => same(e.spender, a))) return;
|
|
317
|
+
return { amount: String(r.amount), usd: Number(e.usd), spender: e.spender, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, raw: r };
|
|
318
|
+
}
|
|
319
|
+
async function signTokenPayment(signer, t, resource) {
|
|
320
|
+
const nonce = BigInt(randomNonce()) >> 8n;
|
|
321
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + Math.min(t.maxTimeoutSeconds, 120));
|
|
322
|
+
const permitted = { token: WAFERMAP_TOKEN, amount: BigInt(t.amount) };
|
|
323
|
+
let signature;
|
|
324
|
+
try {
|
|
325
|
+
signature = await signer.signTypedData({ domain: { name: "Permit2", chainId: 4663, verifyingContract: PERMIT2 }, types: PERMIT_TYPES, primaryType: "PermitTransferFrom", message: { permitted, spender: t.spender, nonce, deadline } });
|
|
326
|
+
} catch (e) {
|
|
327
|
+
throw new X402Error("sign_rejected", e?.shortMessage ?? e?.message ?? "Signature rejected.");
|
|
328
|
+
}
|
|
329
|
+
const payload = { owner: signer.address, permit: { permitted: { token: WAFERMAP_TOKEN, amount: t.amount }, nonce: nonce.toString(), deadline: deadline.toString() }, signature };
|
|
330
|
+
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: t.raw, payload })), deadline: Number(deadline) };
|
|
331
|
+
}
|
|
286
332
|
var X402Error = class extends Error {
|
|
287
333
|
constructor(code, message) {
|
|
288
334
|
super(message);
|
|
@@ -315,8 +361,13 @@ async function readQuote(res) {
|
|
|
315
361
|
const version = Number(body.x402Version) === 1 ? 1 : 2;
|
|
316
362
|
const accepts = [];
|
|
317
363
|
const offered = [];
|
|
364
|
+
let token;
|
|
318
365
|
for (const r of body.accepts) {
|
|
319
366
|
offered.push(`${r.scheme ?? "?"}/${r.network ?? "?"}`);
|
|
367
|
+
if (r?.scheme === TOKEN_SCHEME) {
|
|
368
|
+
token ??= tokenOption(r);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
320
371
|
const chain = chainOf(r.network);
|
|
321
372
|
if (!chain || r.scheme !== "exact") continue;
|
|
322
373
|
if (chain.vm === "evm" ? String(r.asset ?? "").toLowerCase() !== chain.stable.toLowerCase() : r.asset !== chain.stable) continue;
|
|
@@ -330,7 +381,7 @@ async function readQuote(res) {
|
|
|
330
381
|
accepts.push({ version, chain, amount, usd: Number(amount) / 1e6, payTo: r.payTo, asset: r.asset, maxTimeoutSeconds: Number(r.maxTimeoutSeconds) || 120, extra, raw: r });
|
|
331
382
|
}
|
|
332
383
|
const inputError = body.inputError?.message ? { code: String(body.inputError.code ?? "invalid_input"), message: String(body.inputError.message) } : void 0;
|
|
333
|
-
return { version, resource: body.resource ?? null, accepts, offered, inputError };
|
|
384
|
+
return { version, resource: body.resource ?? null, accepts, offered, inputError, ...token ? { token } : {} };
|
|
334
385
|
}
|
|
335
386
|
function choose(q, prefer = CHAIN_KEYS) {
|
|
336
387
|
for (const k of prefer) {
|
|
@@ -394,6 +445,23 @@ async function x402fetch(url, init, opts) {
|
|
|
394
445
|
if (first.status !== 402) return { res: first, paid: null };
|
|
395
446
|
const quote = await readQuote(first);
|
|
396
447
|
if (quote.inputError) throw new X402Error(quote.inputError.code, quote.inputError.message);
|
|
448
|
+
if (opts.payIn === "wafermap") {
|
|
449
|
+
const t = quote.token;
|
|
450
|
+
if (!t) throw new X402Error("token_not_offered", "This call is not offered in $WAFERMAP right now (or not to a known Wafermap relayer). Pay in USDG or USDC.");
|
|
451
|
+
const value = Number(BigInt(t.amount) / 10n ** 12n) / 1e6 * await (opts.tokenPriceUsd ?? (() => wafermapPriceUsd(fetch)))();
|
|
452
|
+
if (value > t.usd * TOKEN_VALUE_SLACK + 5e-4) throw new X402Error("price_mismatch", `The quote asks for $WAFERMAP worth about $${value.toFixed(4)} at the market price, more than the $${t.usd} it states. Nothing was signed.`);
|
|
453
|
+
const usd = Math.max(t.usd, Math.round(value * 1e6) / 1e6);
|
|
454
|
+
const req2 = { version: 2, chain: PAY_CHAINS[0], amount: String(Math.round(usd * 1e6)), usd, payTo: DEAD, asset: WAFERMAP_TOKEN, maxTimeoutSeconds: t.maxTimeoutSeconds, extra: {}, raw: t.raw };
|
|
455
|
+
if (opts.approve) await opts.approve(req2);
|
|
456
|
+
const signer2 = typeof opts.signer === "function" ? await opts.signer() : opts.signer;
|
|
457
|
+
const header2 = await signTokenPayment(signer2, t, quote.resource);
|
|
458
|
+
const signed2 = { req: req2, from: signer2.address, token: { amount: t.amount } };
|
|
459
|
+
opts.onSigned?.(signed2);
|
|
460
|
+
const headers2 = new Headers(init.headers);
|
|
461
|
+
headers2.set(header2.name, header2.value);
|
|
462
|
+
const res2 = await f(url, { ...init, headers: headers2 });
|
|
463
|
+
return { res: res2, paid: { ...signed2, tx: readSettlement(res2)?.tx ?? null } };
|
|
464
|
+
}
|
|
397
465
|
const req = choose(quote, (opts.prefer ?? CHAIN_KEYS).filter((k) => k !== "solana" || opts.solana));
|
|
398
466
|
if (opts.approve) await opts.approve(req);
|
|
399
467
|
const svm = req.chain.vm === "svm";
|
|
@@ -462,14 +530,57 @@ function mergeLog(entries) {
|
|
|
462
530
|
return { ...e, status: o.status, tx: o.tx ?? e.tx, reported: o.reported ?? null, ...o.status === "aborted" ? { usd: 0 } : {} };
|
|
463
531
|
});
|
|
464
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)");
|
|
465
554
|
async function checkPayment(e, rpc) {
|
|
466
555
|
if (e.network === "solana" || !e.nonce || !(e.network in CHAINS)) return "unknown";
|
|
467
556
|
const chain = PAY_CHAINS.find((c) => c.key === e.network);
|
|
468
557
|
try {
|
|
469
558
|
const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
|
|
470
559
|
const used = await client.readContract({ address: chain.stable, abi: AUTH_STATE_ABI, functionName: "authorizationState", args: [e.payer, e.nonce] });
|
|
471
|
-
if (used) return "
|
|
472
|
-
|
|
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";
|
|
473
584
|
} catch {
|
|
474
585
|
return "error";
|
|
475
586
|
}
|
|
@@ -514,6 +625,7 @@ var Wafermap = class {
|
|
|
514
625
|
timeoutMs;
|
|
515
626
|
/** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
|
|
516
627
|
fetch;
|
|
628
|
+
tokenPriceUsd;
|
|
517
629
|
constructor(o) {
|
|
518
630
|
const account = o.account ?? (o.privateKey ? privateKeyToAccount(o.privateKey) : null);
|
|
519
631
|
if (!account) throw new WafermapError("no_account", "Pass privateKey or account.");
|
|
@@ -526,6 +638,7 @@ var Wafermap = class {
|
|
|
526
638
|
this.approveFn = o.approve;
|
|
527
639
|
this.rpc = o.rpc ?? {};
|
|
528
640
|
this.timeoutMs = o.timeoutMs && o.timeoutMs > 0 ? o.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
641
|
+
this.tokenPriceUsd = o.tokenPriceUsd;
|
|
529
642
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
530
643
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
531
644
|
const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
|
|
@@ -599,7 +712,7 @@ var Wafermap = class {
|
|
|
599
712
|
this.queue = prev.then(() => held);
|
|
600
713
|
return prev.then(() => release);
|
|
601
714
|
}
|
|
602
|
-
async paidFetch(kind, what, url, init, network) {
|
|
715
|
+
async paidFetch(kind, what, url, init, network, payIn) {
|
|
603
716
|
let id = null;
|
|
604
717
|
let release = null;
|
|
605
718
|
const unlock = () => {
|
|
@@ -612,9 +725,11 @@ var Wafermap = class {
|
|
|
612
725
|
solana: this.solana ?? void 0,
|
|
613
726
|
prefer: this.prefer(network),
|
|
614
727
|
fetch: this.fetch,
|
|
728
|
+
payIn,
|
|
729
|
+
tokenPriceUsd: this.tokenPriceUsd,
|
|
615
730
|
approve: async (req) => {
|
|
616
731
|
release = await this.acquire();
|
|
617
|
-
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: `${req.chain.symbol} to ${req.payTo}` });
|
|
732
|
+
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}` });
|
|
618
733
|
},
|
|
619
734
|
onSigned: (s) => {
|
|
620
735
|
id = this.booked(kind, what, s);
|
|
@@ -650,9 +765,33 @@ var Wafermap = class {
|
|
|
650
765
|
this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
|
|
651
766
|
return this.passCache;
|
|
652
767
|
}
|
|
768
|
+
// ---- Paying in $WAFERMAP (Permit2; the tokens are burned) ----
|
|
769
|
+
/** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
|
|
770
|
+
async wafermapApproved() {
|
|
771
|
+
const a = await this.client("robinhood").readContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "allowance", args: [this.address, PERMIT2] });
|
|
772
|
+
return a >= 10n ** 30n;
|
|
773
|
+
}
|
|
774
|
+
/** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
|
|
775
|
+
* transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
|
|
776
|
+
* spendable only by Wafermap's relayer for two minutes. */
|
|
777
|
+
async approveWafermap() {
|
|
778
|
+
const wallet = createWalletClient({ account: this.account, chain: CHAINS.robinhood, transport: http(this.rpc.robinhood) });
|
|
779
|
+
try {
|
|
780
|
+
const tx = await wallet.writeContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "approve", args: [PERMIT2, 2n ** 256n - 1n] });
|
|
781
|
+
const rc = await this.client("robinhood").waitForTransactionReceipt({ hash: tx, timeout: 6e4 });
|
|
782
|
+
if (rc.status !== "success") throw new WafermapError("approve_failed", `The approval reverted: ${tx}`);
|
|
783
|
+
return tx;
|
|
784
|
+
} catch (e) {
|
|
785
|
+
if (e instanceof WafermapError) throw e;
|
|
786
|
+
throw new WafermapError("approve_failed", e?.shortMessage ?? e?.message ?? "The approval failed.");
|
|
787
|
+
}
|
|
788
|
+
}
|
|
653
789
|
async call(id, input = {}, o = {}) {
|
|
790
|
+
const inToken = o.payIn === "wafermap";
|
|
791
|
+
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.");
|
|
792
|
+
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.");
|
|
654
793
|
const pass = id.startsWith("m/") ? null : await this.holderPass();
|
|
655
|
-
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network);
|
|
794
|
+
const go = () => this.paidFetch("resource", id, `${this.baseUrl}/v1/${id}`, { method: "POST", headers: { "content-type": "application/json", "x-wafermap-payer": this.address, ...pass ? { [PASS_HEADER]: pass } : {} }, body: JSON.stringify(input) }, o.network, inToken ? "wafermap" : void 0);
|
|
656
795
|
const settle = (r2, out2) => {
|
|
657
796
|
if (!r2.paid || !r2.id) return;
|
|
658
797
|
const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
|
|
@@ -768,7 +907,10 @@ export {
|
|
|
768
907
|
MAX_VALIDITY_SECONDS,
|
|
769
908
|
PASS_HEADER,
|
|
770
909
|
PAY_CHAINS,
|
|
910
|
+
PERMIT2,
|
|
771
911
|
SwapError,
|
|
912
|
+
WAFERMAP_RELAYERS,
|
|
913
|
+
WAFERMAP_TOKEN,
|
|
772
914
|
Wafermap,
|
|
773
915
|
WafermapError,
|
|
774
916
|
X402Error,
|
|
@@ -786,5 +928,7 @@ export {
|
|
|
786
928
|
quoteSwap,
|
|
787
929
|
readQuote,
|
|
788
930
|
signPayment,
|
|
931
|
+
signTokenPayment,
|
|
932
|
+
wafermapPriceUsd,
|
|
789
933
|
x402fetch
|
|
790
934
|
};
|
|
@@ -56,7 +56,30 @@ export type Quote = {
|
|
|
56
56
|
code: string;
|
|
57
57
|
message: string;
|
|
58
58
|
};
|
|
59
|
+
token?: TokenRequirement;
|
|
59
60
|
};
|
|
61
|
+
export declare const WAFERMAP_TOKEN = "0xfda080f64d84c989cfc50ff18253542cfe502db3";
|
|
62
|
+
export declare const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
63
|
+
export declare const DEAD = "0x000000000000000000000000000000000000dEaD";
|
|
64
|
+
export declare const TOKEN_SCHEME = "wafermap-permit2";
|
|
65
|
+
/** Wafermap's relayer wallets, the only spenders this client signs a $WAFERMAP permit for. Empty: paying in
|
|
66
|
+
* $WAFERMAP is off in this build. A new relayer address ships as a new client release. */
|
|
67
|
+
export declare const WAFERMAP_RELAYERS: string[];
|
|
68
|
+
/** $WAFERMAP in USD, read by the client itself from GeckoTerminal. Throws when it cannot be read. */
|
|
69
|
+
export declare function wafermapPriceUsd(f?: typeof fetch): Promise<number>;
|
|
70
|
+
export type TokenRequirement = {
|
|
71
|
+
amount: string;
|
|
72
|
+
usd: number;
|
|
73
|
+
spender: Hex;
|
|
74
|
+
maxTimeoutSeconds: number;
|
|
75
|
+
raw: any;
|
|
76
|
+
};
|
|
77
|
+
/** Signs a Permit2 transfer of exactly the quoted $WAFERMAP to Wafermap's relayer, valid for at most 120 s. */
|
|
78
|
+
export declare function signTokenPayment(signer: Signer, t: TokenRequirement, resource: any): Promise<{
|
|
79
|
+
name: string;
|
|
80
|
+
value: string;
|
|
81
|
+
deadline: number;
|
|
82
|
+
}>;
|
|
60
83
|
export declare class X402Error extends Error {
|
|
61
84
|
code: string;
|
|
62
85
|
constructor(code: string, message: string);
|
|
@@ -92,6 +115,9 @@ export type Signed = {
|
|
|
92
115
|
req: Requirement;
|
|
93
116
|
from: string;
|
|
94
117
|
auth?: SignedAuth;
|
|
118
|
+
token?: {
|
|
119
|
+
amount: string;
|
|
120
|
+
};
|
|
95
121
|
};
|
|
96
122
|
export type PaidCall = {
|
|
97
123
|
res: Response;
|
|
@@ -113,4 +139,6 @@ export declare function x402fetch(url: string, init: RequestInit, opts: {
|
|
|
113
139
|
approve?: (req: Requirement) => Promise<void> | void;
|
|
114
140
|
onSigned?: (s: Signed) => void;
|
|
115
141
|
fetch?: typeof fetch;
|
|
142
|
+
payIn?: "stablecoin" | "wafermap";
|
|
143
|
+
tokenPriceUsd?: () => Promise<number>;
|
|
116
144
|
}): Promise<PaidCall>;
|
package/dist/types/src/core.d.ts
CHANGED
|
@@ -71,6 +71,9 @@ export type WafermapOptions = {
|
|
|
71
71
|
/** Give up on any HTTP request (quote, paid call, discovery, swap API) after this long. Default 60000 ms.
|
|
72
72
|
* A paid call that times out after signing stays counted until its authorization expires. */
|
|
73
73
|
timeoutMs?: number;
|
|
74
|
+
/** Where the $WAFERMAP price comes from when paying in $WAFERMAP (default: GeckoTerminal, read by this client).
|
|
75
|
+
* The signed amount is valued at this price and refused when it is worth more than the quote states. */
|
|
76
|
+
tokenPriceUsd?: () => Promise<number>;
|
|
74
77
|
};
|
|
75
78
|
export declare class WafermapError extends Error {
|
|
76
79
|
code: string;
|
|
@@ -81,10 +84,16 @@ export declare function mergeLog(entries: SpendEntry[]): SpendEntry[];
|
|
|
81
84
|
/**
|
|
82
85
|
* Was a signed payment ever executed? Reads the token contract (EIP-3009 authorizationState),
|
|
83
86
|
* so the answer does not depend on any server and needs no key. "unused" after expiry means it
|
|
84
|
-
* can never be charged.
|
|
85
|
-
*
|
|
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".
|
|
86
92
|
*/
|
|
87
|
-
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">;
|
|
88
97
|
export declare const DEFAULT_LIMITS: Limits;
|
|
89
98
|
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
90
99
|
/** What a wallet's $WAFERMAP unlocks (holder price, Pro free calls left today), from /api/holder. Needs no key. */
|
|
@@ -105,6 +114,7 @@ export declare class Wafermap {
|
|
|
105
114
|
readonly timeoutMs: number;
|
|
106
115
|
/** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
|
|
107
116
|
readonly fetch: typeof fetch;
|
|
117
|
+
private readonly tokenPriceUsd?;
|
|
108
118
|
constructor(o: WafermapOptions);
|
|
109
119
|
get address(): `0x${string}`;
|
|
110
120
|
client(n: EvmNet): PublicClient;
|
|
@@ -133,8 +143,15 @@ export declare class Wafermap {
|
|
|
133
143
|
* proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
|
|
134
144
|
* a message moves no funds. */
|
|
135
145
|
holderPass(): Promise<string>;
|
|
146
|
+
/** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
|
|
147
|
+
wafermapApproved(): Promise<boolean>;
|
|
148
|
+
/** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
|
|
149
|
+
* transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
|
|
150
|
+
* spendable only by Wafermap's relayer for two minutes. */
|
|
151
|
+
approveWafermap(): Promise<string>;
|
|
136
152
|
call(id: string, input?: Record<string, unknown>, o?: {
|
|
137
153
|
network?: Net;
|
|
154
|
+
payIn?: "stablecoin" | "wafermap";
|
|
138
155
|
}): Promise<{
|
|
139
156
|
resource: string;
|
|
140
157
|
result: any;
|
|
@@ -142,6 +159,8 @@ export declare class Wafermap {
|
|
|
142
159
|
network: string;
|
|
143
160
|
asset: string;
|
|
144
161
|
amountUsd: number;
|
|
162
|
+
amountTokens?: number;
|
|
163
|
+
burned?: boolean;
|
|
145
164
|
payer: string;
|
|
146
165
|
tx: string;
|
|
147
166
|
ms: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, holderInfo, listResources, mergeLog } from "./core.js";
|
|
2
2
|
export { PASS_HEADER, passMessage, encodePass, decodePass, makePass } from "../shared/pass";
|
|
3
3
|
export type { Approval, Limits, Net, PaymentStatus, SpendEntry, SwapQuote, Token, WafermapOptions } from "./core.js";
|
|
4
|
-
export { x402fetch, readQuote, choose, signPayment, PAY_CHAINS, MAX_VALIDITY_SECONDS, X402Error } from "../shared/x402client";
|
|
4
|
+
export { x402fetch, readQuote, choose, signPayment, signTokenPayment, wafermapPriceUsd, PAY_CHAINS, MAX_VALIDITY_SECONDS, WAFERMAP_RELAYERS, WAFERMAP_TOKEN, PERMIT2, X402Error } from "../shared/x402client";
|
|
5
5
|
export { quoteSwap, executeSwap, decodeSwap, KYBER_ROUTER, SwapError } from "../shared/swap";
|
|
6
6
|
export type { Requirement, Quote, Signed, SignedAuth, Signer } from "../shared/x402client";
|
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": {
|