@wafermapxyz/cli 0.4.3 → 0.4.5
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 +5 -1
- package/dist/cli.js +210 -11
- package/dist/index.js +166 -4
- package/dist/types/shared/pass.d.ts +21 -0
- package/dist/types/shared/x402client.d.ts +28 -0
- package/dist/types/src/core.d.ts +25 -0
- package/dist/types/src/index.d.ts +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.5 (2 Nov 2026)
|
|
4
|
+
|
|
5
|
+
**Pay Wafermap in $WAFERMAP, at 50% off. The tokens are burned.**
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
- `wafermap approve-wafermap`: the one-time Permit2 approval (one Robinhood Chain transaction, paid in ETH).
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
## 0.4.4 (2 Nov 2026)
|
|
13
|
+
|
|
14
|
+
**$WAFERMAP holder tiers.**
|
|
15
|
+
|
|
16
|
+
- **Pro free calls.** A wallet holding 1,000,000 $WAFERMAP on Robinhood Chain gets 10 free calls a day (UTC) to Wafermap resources listed at $0.01 or less. `call()` (CLI `wafermap call`, MCP `wafermap_call`) sends a **holder pass** to Wafermap with each call; when the call is free, the result comes back with `receipt: null` and `free: {left, perDay}`, no payment is signed and nothing is recorded as spending. Otherwise the call is quoted and paid as before.
|
|
17
|
+
- **Holder pass.** A message the wallet signs (`personal_sign`: no transaction, no cost) that proves it holds the wallet, valid for 24 hours and reused until its last 10 minutes (`Wafermap.holderPass()`). It goes to Wafermap only: never with `pay()` to third-party services, and never to marketplace listings (`m/...`). The SDK exports `PASS_HEADER`, `passMessage`, `encodePass`, `decodePass`, `makePass`.
|
|
18
|
+
- **`wafermap holder`** shows what the wallet's $WAFERMAP unlocks: holder price, tier, free calls left today, x402 Tracker Pro. Needs no password (`holderInfo()` in the SDK).
|
|
19
|
+
- **`wafermap pass`** prints a pass for scripts, e.g. the x402 Tracker Pro API (`/api/x402/pro/movers`, `/api/x402/pro/export`, for wallets holding 100,000 $WAFERMAP).
|
|
20
|
+
|
|
3
21
|
## 0.4.3 (2 Nov 2026)
|
|
4
22
|
|
|
5
23
|
The two open points from the third public review by @whoiskn0why (8/10), thank you.
|
package/README.md
CHANGED
|
@@ -82,7 +82,11 @@ Pass `approve: (a) => boolean` to confirm each payment or swap yourself, and `le
|
|
|
82
82
|
|
|
83
83
|
## $WAFERMAP holder discount
|
|
84
84
|
|
|
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
|
|
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
|
+
|
|
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
|
+
|
|
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
|
|
86
90
|
|
|
87
91
|
## Where the limits live
|
|
88
92
|
|
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);
|
|
@@ -437,6 +505,47 @@ var init_x402client = __esm({
|
|
|
437
505
|
}
|
|
438
506
|
});
|
|
439
507
|
|
|
508
|
+
// shared/pass.ts
|
|
509
|
+
function passMessage(address, iat, exp) {
|
|
510
|
+
return [
|
|
511
|
+
"wafermap.xyz holder pass",
|
|
512
|
+
"",
|
|
513
|
+
`Wallet: ${address}`,
|
|
514
|
+
`Issued: ${new Date(iat * 1e3).toISOString()}`,
|
|
515
|
+
`Expires: ${new Date(exp * 1e3).toISOString()}`,
|
|
516
|
+
"",
|
|
517
|
+
"Proves you hold this wallet, so Wafermap can apply what your $WAFERMAP unlocks. It costs nothing and moves no funds."
|
|
518
|
+
].join("\n");
|
|
519
|
+
}
|
|
520
|
+
function decodePass(s) {
|
|
521
|
+
if (!s || s.length > 1e3 || !/^[A-Za-z0-9_-]+$/.test(s)) return null;
|
|
522
|
+
try {
|
|
523
|
+
const p = JSON.parse(unb64url(s));
|
|
524
|
+
if (p?.v !== 1 || typeof p.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(p.address)) return null;
|
|
525
|
+
if (!Number.isInteger(p.iat) || !Number.isInteger(p.exp) || typeof p.sig !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(p.sig)) return null;
|
|
526
|
+
return { v: 1, address: p.address, iat: p.iat, exp: p.exp, sig: p.sig };
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function makePass(signer, seconds = PASS_MAX_SECONDS, now = Math.floor(Date.now() / 1e3)) {
|
|
532
|
+
const iat = now;
|
|
533
|
+
const exp = now + Math.min(Math.max(60, seconds), PASS_MAX_SECONDS);
|
|
534
|
+
const sig = await signer.signMessage({ message: passMessage(signer.address, iat, exp) });
|
|
535
|
+
return { v: 1, address: signer.address, iat, exp, sig };
|
|
536
|
+
}
|
|
537
|
+
var PASS_HEADER, PASS_MAX_SECONDS, b64url, unb64url, encodePass;
|
|
538
|
+
var init_pass = __esm({
|
|
539
|
+
"shared/pass.ts"() {
|
|
540
|
+
"use strict";
|
|
541
|
+
PASS_HEADER = "x-wafermap-pass";
|
|
542
|
+
PASS_MAX_SECONDS = 86400;
|
|
543
|
+
b64url = (s) => btoa(unescape(encodeURIComponent(s))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
544
|
+
unb64url = (s) => decodeURIComponent(escape(atob(s.replace(/-/g, "+").replace(/_/g, "/"))));
|
|
545
|
+
encodePass = (p) => b64url(JSON.stringify(p));
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
440
549
|
// src/core.ts
|
|
441
550
|
var core_exports = {};
|
|
442
551
|
__export(core_exports, {
|
|
@@ -446,6 +555,7 @@ __export(core_exports, {
|
|
|
446
555
|
Wafermap: () => Wafermap,
|
|
447
556
|
WafermapError: () => WafermapError,
|
|
448
557
|
checkPayment: () => checkPayment,
|
|
558
|
+
holderInfo: () => holderInfo,
|
|
449
559
|
isNet: () => isNet,
|
|
450
560
|
listResources: () => listResources,
|
|
451
561
|
mergeLog: () => mergeLog
|
|
@@ -473,6 +583,15 @@ async function checkPayment(e, rpc) {
|
|
|
473
583
|
return "error";
|
|
474
584
|
}
|
|
475
585
|
}
|
|
586
|
+
async function holderInfo(address, baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
587
|
+
const host = new URL(baseUrl).host;
|
|
588
|
+
const r = await fetch(`${baseUrl.replace(/\/$/, "")}/api/holder?address=${encodeURIComponent(address)}`, { signal: AbortSignal.timeout(timeoutMs) }).catch((e) => {
|
|
589
|
+
throw timedOut(e) ? new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s.`) : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
|
|
590
|
+
});
|
|
591
|
+
const j = await r.json().catch(() => null);
|
|
592
|
+
if (!r.ok || !j) throw new WafermapError(j?.error?.code ?? "network_error", j?.error?.message ?? `${host} answered HTTP ${r.status}.`);
|
|
593
|
+
return j;
|
|
594
|
+
}
|
|
476
595
|
async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
477
596
|
const host = new URL(baseUrl).host;
|
|
478
597
|
const gaveUp = () => new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s. Nothing was paid.`);
|
|
@@ -494,6 +613,7 @@ var init_core = __esm({
|
|
|
494
613
|
init_swap();
|
|
495
614
|
init_solana();
|
|
496
615
|
init_x402client();
|
|
616
|
+
init_pass();
|
|
497
617
|
NETS = CHAIN_KEYS;
|
|
498
618
|
isNet = (v) => NETS.includes(v);
|
|
499
619
|
isSwapNet = (v) => v === "robinhood" || v === "base";
|
|
@@ -523,6 +643,7 @@ var init_core = __esm({
|
|
|
523
643
|
timeoutMs;
|
|
524
644
|
/** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
|
|
525
645
|
fetch;
|
|
646
|
+
tokenPriceUsd;
|
|
526
647
|
constructor(o) {
|
|
527
648
|
const account = o.account ?? (o.privateKey ? privateKeyToAccount(o.privateKey) : null);
|
|
528
649
|
if (!account) throw new WafermapError("no_account", "Pass privateKey or account.");
|
|
@@ -535,6 +656,7 @@ var init_core = __esm({
|
|
|
535
656
|
this.approveFn = o.approve;
|
|
536
657
|
this.rpc = o.rpc ?? {};
|
|
537
658
|
this.timeoutMs = o.timeoutMs && o.timeoutMs > 0 ? o.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
659
|
+
this.tokenPriceUsd = o.tokenPriceUsd;
|
|
538
660
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
539
661
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
540
662
|
const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
|
|
@@ -608,7 +730,7 @@ var init_core = __esm({
|
|
|
608
730
|
this.queue = prev.then(() => held);
|
|
609
731
|
return prev.then(() => release);
|
|
610
732
|
}
|
|
611
|
-
async paidFetch(kind, what, url, init, network) {
|
|
733
|
+
async paidFetch(kind, what, url, init, network, payIn) {
|
|
612
734
|
let id = null;
|
|
613
735
|
let release = null;
|
|
614
736
|
const unlock = () => {
|
|
@@ -621,9 +743,11 @@ var init_core = __esm({
|
|
|
621
743
|
solana: this.solana ?? void 0,
|
|
622
744
|
prefer: this.prefer(network),
|
|
623
745
|
fetch: this.fetch,
|
|
746
|
+
payIn,
|
|
747
|
+
tokenPriceUsd: this.tokenPriceUsd,
|
|
624
748
|
approve: async (req) => {
|
|
625
749
|
release = await this.acquire();
|
|
626
|
-
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: `${req.chain.symbol} to ${req.payTo}` });
|
|
750
|
+
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}` });
|
|
627
751
|
},
|
|
628
752
|
onSigned: (s) => {
|
|
629
753
|
id = this.booked(kind, what, s);
|
|
@@ -649,8 +773,43 @@ var init_core = __esm({
|
|
|
649
773
|
async resources() {
|
|
650
774
|
return listResources(this.baseUrl, this.timeoutMs);
|
|
651
775
|
}
|
|
776
|
+
passCache = null;
|
|
777
|
+
/** A holder pass (shared/pass.ts) signed by this wallet, valid 24 h and reused until its last 10 minutes. It
|
|
778
|
+
* proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
|
|
779
|
+
* a message moves no funds. */
|
|
780
|
+
async holderPass() {
|
|
781
|
+
const p = decodePass(this.passCache);
|
|
782
|
+
if (this.passCache && p && p.exp - 600 > Date.now() / 1e3) return this.passCache;
|
|
783
|
+
this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
|
|
784
|
+
return this.passCache;
|
|
785
|
+
}
|
|
786
|
+
// ---- Paying in $WAFERMAP (Permit2; the tokens are burned) ----
|
|
787
|
+
/** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
|
|
788
|
+
async wafermapApproved() {
|
|
789
|
+
const a = await this.client("robinhood").readContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "allowance", args: [this.address, PERMIT2] });
|
|
790
|
+
return a >= 10n ** 30n;
|
|
791
|
+
}
|
|
792
|
+
/** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
|
|
793
|
+
* transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
|
|
794
|
+
* spendable only by Wafermap's relayer for two minutes. */
|
|
795
|
+
async approveWafermap() {
|
|
796
|
+
const wallet = createWalletClient({ account: this.account, chain: CHAINS.robinhood, transport: http(this.rpc.robinhood) });
|
|
797
|
+
try {
|
|
798
|
+
const tx = await wallet.writeContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "approve", args: [PERMIT2, 2n ** 256n - 1n] });
|
|
799
|
+
const rc = await this.client("robinhood").waitForTransactionReceipt({ hash: tx, timeout: 6e4 });
|
|
800
|
+
if (rc.status !== "success") throw new WafermapError("approve_failed", `The approval reverted: ${tx}`);
|
|
801
|
+
return tx;
|
|
802
|
+
} catch (e) {
|
|
803
|
+
if (e instanceof WafermapError) throw e;
|
|
804
|
+
throw new WafermapError("approve_failed", e?.shortMessage ?? e?.message ?? "The approval failed.");
|
|
805
|
+
}
|
|
806
|
+
}
|
|
652
807
|
async call(id, input = {}, o = {}) {
|
|
653
|
-
const
|
|
808
|
+
const inToken = o.payIn === "wafermap";
|
|
809
|
+
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.");
|
|
810
|
+
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.");
|
|
811
|
+
const pass = id.startsWith("m/") ? null : await this.holderPass();
|
|
812
|
+
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);
|
|
654
813
|
const settle = (r2, out2) => {
|
|
655
814
|
if (!r2.paid || !r2.id) return;
|
|
656
815
|
const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
|
|
@@ -960,7 +1119,7 @@ var VERSION;
|
|
|
960
1119
|
var init_version = __esm({
|
|
961
1120
|
"src/version.ts"() {
|
|
962
1121
|
"use strict";
|
|
963
|
-
VERSION = "0.4.
|
|
1122
|
+
VERSION = "0.4.5";
|
|
964
1123
|
}
|
|
965
1124
|
});
|
|
966
1125
|
|
|
@@ -1060,11 +1219,16 @@ async function runMcp() {
|
|
|
1060
1219
|
{
|
|
1061
1220
|
title: "Call a Wafermap resource",
|
|
1062
1221
|
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.",
|
|
1063
|
-
inputSchema: {
|
|
1222
|
+
inputSchema: {
|
|
1223
|
+
resource: z.string().describe("Resource id, e.g. market-brief, prediction-markets, stock-signal, flux-2-dev."),
|
|
1224
|
+
input: z.record(z.string(), z.any()).optional().describe("Input fields for the resource."),
|
|
1225
|
+
network: NET,
|
|
1226
|
+
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.")
|
|
1227
|
+
}
|
|
1064
1228
|
},
|
|
1065
|
-
async ({ resource, input, network }) => {
|
|
1229
|
+
async ({ resource, input, network, payIn }) => {
|
|
1066
1230
|
try {
|
|
1067
|
-
return result(await wm.call(resource, input ?? {}, { network }));
|
|
1231
|
+
return result(await wm.call(resource, input ?? {}, { network, payIn }));
|
|
1068
1232
|
} catch (e) {
|
|
1069
1233
|
return failure(e);
|
|
1070
1234
|
}
|
|
@@ -1515,6 +1679,10 @@ Wallet
|
|
|
1515
1679
|
Pay
|
|
1516
1680
|
wafermap resources [words] Wafermap's pay-per-call catalog
|
|
1517
1681
|
wafermap call <id> [field=value ...] run a resource and pay its price (--json '{...}' for complex input)
|
|
1682
|
+
wafermap call <id> ... --pay wafermap pay a Wafermap resource in $WAFERMAP at 50% off (the tokens are burned)
|
|
1683
|
+
wafermap approve-wafermap one-time approval so the wallet can pay in $WAFERMAP (Robinhood Chain, ETH gas)
|
|
1684
|
+
wafermap holder what this wallet's $WAFERMAP unlocks: holder price, Pro free calls left today
|
|
1685
|
+
wafermap pass print a holder pass (X-Wafermap-Pass) for scripts and x402 Tracker Pro
|
|
1518
1686
|
wafermap discover <words> search third-party x402 services
|
|
1519
1687
|
wafermap pay <url> [field=value ...] call any x402 URL and pay it (--post to send a JSON body)
|
|
1520
1688
|
|
|
@@ -1630,6 +1798,34 @@ async function main() {
|
|
|
1630
1798
|
Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits\`.`);
|
|
1631
1799
|
return;
|
|
1632
1800
|
}
|
|
1801
|
+
case "approve-wafermap": {
|
|
1802
|
+
const wm = openWallet(await password());
|
|
1803
|
+
if (await wm.wafermapApproved()) return console.log("\u2713 Already approved: this wallet can pay Wafermap in $WAFERMAP (wafermap call <id> --pay wafermap).");
|
|
1804
|
+
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.`);
|
|
1805
|
+
if (!yes && !/^y(es)?$/i.test((await ask("Send the approval? [y/N] ")).trim())) die("Cancelled. Nothing was sent.");
|
|
1806
|
+
const tx = await wm.approveWafermap();
|
|
1807
|
+
return console.log(`\u2713 Approved \xB7 ${EXPLORER("robinhood")}/tx/${tx}`);
|
|
1808
|
+
}
|
|
1809
|
+
case "holder": {
|
|
1810
|
+
const addr = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
|
|
1811
|
+
const j = await holderInfo(addr, process.env.WAFERMAP_URL, Number(process.env.WAFERMAP_TIMEOUT_MS) || void 0);
|
|
1812
|
+
if (!j.active) return console.log("The $WAFERMAP holder program is off.");
|
|
1813
|
+
const n = (v) => Math.floor(v).toLocaleString("en-US");
|
|
1814
|
+
const k = j.perks;
|
|
1815
|
+
console.log(`${addr}
|
|
1816
|
+
holds ${n(j.balance)} $WAFERMAP on Robinhood Chain`);
|
|
1817
|
+
console.log(` tier ${j.tier === "pro" ? "Pro" : j.tier === "holder" ? "Holder" : "none"}`);
|
|
1818
|
+
console.log(` price ${j.holder ? `${j.percent}% off Wafermap resources on EVM networks` : `regular (hold ${n(j.minTokens)} for ${j.percent}% off)`}`);
|
|
1819
|
+
if (k) console.log(` free ${j.tier === "pro" ? `${k.freeLeftToday ?? "?"} of ${k.freeCallsPerDay} free calls left today (resources up to $${k.freeMaxUsd}); \`wafermap call\` uses them first` : `hold ${n(k.proMinTokens)} for ${k.freeCallsPerDay} free calls a day`}`);
|
|
1820
|
+
return console.log(` tracker ${j.holder ? "x402 Tracker Pro unlocked: wafermap.xyz/x402 (or `wafermap pass` for the API)" : `x402 Tracker Pro needs ${n(j.minTokens)}`}`);
|
|
1821
|
+
}
|
|
1822
|
+
case "pass": {
|
|
1823
|
+
const wm = openWallet(await password());
|
|
1824
|
+
const v = await wm.holderPass();
|
|
1825
|
+
console.error(`Holder pass for ${wm.address}, valid 24 hours. It proves you hold the wallet and moves no funds; anyone who has it can use your free calls until it expires, so keep it private. Send it as X-Wafermap-Pass:`);
|
|
1826
|
+
console.error(` curl -H "X-Wafermap-Pass: $(wafermap pass)" https://wafermap.xyz/api/x402/pro/movers`);
|
|
1827
|
+
return console.log(v);
|
|
1828
|
+
}
|
|
1633
1829
|
case "address": {
|
|
1634
1830
|
const evm = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
|
|
1635
1831
|
let sol = keystoreSolana();
|
|
@@ -1709,8 +1905,11 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
1709
1905
|
case "call": {
|
|
1710
1906
|
const id = a._[0] ?? die("Which resource? e.g. wafermap call gas-now network=base");
|
|
1711
1907
|
const wm = openWallet(await password(), approver(yes));
|
|
1712
|
-
const
|
|
1713
|
-
|
|
1908
|
+
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)");
|
|
1909
|
+
const out = await wm.call(id, inputOf(a, 1), { network: net(a.flags.network), payIn });
|
|
1910
|
+
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}`);
|
|
1911
|
+
else if (out.receipt) console.error(`\u2713 Paid $${out.receipt.amountUsd} ${out.receipt.asset} \xB7 ${EXPLORER(out.receipt.network)}/tx/${out.receipt.tx}`);
|
|
1912
|
+
else if (out.free) console.error(`\u2713 Free call ($WAFERMAP Pro) \xB7 ${out.free.left} of ${out.free.perDay} left today. Nothing was paid.`);
|
|
1714
1913
|
if (typeof a.flags.out === "string") await saveMedia(out.result, a.flags.out);
|
|
1715
1914
|
return printResult(out.result);
|
|
1716
1915
|
}
|
package/dist/index.js
CHANGED
|
@@ -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";
|
|
@@ -407,6 +475,41 @@ async function x402fetch(url, init, opts) {
|
|
|
407
475
|
return { res, paid: { ...signed, tx: readSettlement(res)?.tx ?? null } };
|
|
408
476
|
}
|
|
409
477
|
|
|
478
|
+
// shared/pass.ts
|
|
479
|
+
var PASS_HEADER = "x-wafermap-pass";
|
|
480
|
+
var PASS_MAX_SECONDS = 86400;
|
|
481
|
+
function passMessage(address, iat, exp) {
|
|
482
|
+
return [
|
|
483
|
+
"wafermap.xyz holder pass",
|
|
484
|
+
"",
|
|
485
|
+
`Wallet: ${address}`,
|
|
486
|
+
`Issued: ${new Date(iat * 1e3).toISOString()}`,
|
|
487
|
+
`Expires: ${new Date(exp * 1e3).toISOString()}`,
|
|
488
|
+
"",
|
|
489
|
+
"Proves you hold this wallet, so Wafermap can apply what your $WAFERMAP unlocks. It costs nothing and moves no funds."
|
|
490
|
+
].join("\n");
|
|
491
|
+
}
|
|
492
|
+
var b64url = (s) => btoa(unescape(encodeURIComponent(s))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
493
|
+
var unb64url = (s) => decodeURIComponent(escape(atob(s.replace(/-/g, "+").replace(/_/g, "/"))));
|
|
494
|
+
var encodePass = (p) => b64url(JSON.stringify(p));
|
|
495
|
+
function decodePass(s) {
|
|
496
|
+
if (!s || s.length > 1e3 || !/^[A-Za-z0-9_-]+$/.test(s)) return null;
|
|
497
|
+
try {
|
|
498
|
+
const p = JSON.parse(unb64url(s));
|
|
499
|
+
if (p?.v !== 1 || typeof p.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(p.address)) return null;
|
|
500
|
+
if (!Number.isInteger(p.iat) || !Number.isInteger(p.exp) || typeof p.sig !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(p.sig)) return null;
|
|
501
|
+
return { v: 1, address: p.address, iat: p.iat, exp: p.exp, sig: p.sig };
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function makePass(signer, seconds = PASS_MAX_SECONDS, now = Math.floor(Date.now() / 1e3)) {
|
|
507
|
+
const iat = now;
|
|
508
|
+
const exp = now + Math.min(Math.max(60, seconds), PASS_MAX_SECONDS);
|
|
509
|
+
const sig = await signer.signMessage({ message: passMessage(signer.address, iat, exp) });
|
|
510
|
+
return { v: 1, address: signer.address, iat, exp, sig };
|
|
511
|
+
}
|
|
512
|
+
|
|
410
513
|
// src/core.ts
|
|
411
514
|
var NETS = CHAIN_KEYS;
|
|
412
515
|
var isSwapNet = (v) => v === "robinhood" || v === "base";
|
|
@@ -442,6 +545,15 @@ async function checkPayment(e, rpc) {
|
|
|
442
545
|
var DEFAULT_LIMITS = { perPayment: 0.1, perDay: 1, perSwap: 25 };
|
|
443
546
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
444
547
|
var timedOut = (e) => e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError";
|
|
548
|
+
async function holderInfo(address, baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
549
|
+
const host = new URL(baseUrl).host;
|
|
550
|
+
const r = await fetch(`${baseUrl.replace(/\/$/, "")}/api/holder?address=${encodeURIComponent(address)}`, { signal: AbortSignal.timeout(timeoutMs) }).catch((e) => {
|
|
551
|
+
throw timedOut(e) ? new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s.`) : new WafermapError("network_error", `${host}: ${e?.message ?? "request failed"}`);
|
|
552
|
+
});
|
|
553
|
+
const j = await r.json().catch(() => null);
|
|
554
|
+
if (!r.ok || !j) throw new WafermapError(j?.error?.code ?? "network_error", j?.error?.message ?? `${host} answered HTTP ${r.status}.`);
|
|
555
|
+
return j;
|
|
556
|
+
}
|
|
445
557
|
async function listResources(baseUrl = "https://wafermap.xyz", timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
446
558
|
const host = new URL(baseUrl).host;
|
|
447
559
|
const gaveUp = () => new WafermapError("timeout", `${host} did not answer within ${Math.round(timeoutMs / 1e3)} s. Nothing was paid.`);
|
|
@@ -470,6 +582,7 @@ var Wafermap = class {
|
|
|
470
582
|
timeoutMs;
|
|
471
583
|
/** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
|
|
472
584
|
fetch;
|
|
585
|
+
tokenPriceUsd;
|
|
473
586
|
constructor(o) {
|
|
474
587
|
const account = o.account ?? (o.privateKey ? privateKeyToAccount(o.privateKey) : null);
|
|
475
588
|
if (!account) throw new WafermapError("no_account", "Pass privateKey or account.");
|
|
@@ -482,6 +595,7 @@ var Wafermap = class {
|
|
|
482
595
|
this.approveFn = o.approve;
|
|
483
596
|
this.rpc = o.rpc ?? {};
|
|
484
597
|
this.timeoutMs = o.timeoutMs && o.timeoutMs > 0 ? o.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
598
|
+
this.tokenPriceUsd = o.tokenPriceUsd;
|
|
485
599
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
486
600
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
487
601
|
const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
|
|
@@ -555,7 +669,7 @@ var Wafermap = class {
|
|
|
555
669
|
this.queue = prev.then(() => held);
|
|
556
670
|
return prev.then(() => release);
|
|
557
671
|
}
|
|
558
|
-
async paidFetch(kind, what, url, init, network) {
|
|
672
|
+
async paidFetch(kind, what, url, init, network, payIn) {
|
|
559
673
|
let id = null;
|
|
560
674
|
let release = null;
|
|
561
675
|
const unlock = () => {
|
|
@@ -568,9 +682,11 @@ var Wafermap = class {
|
|
|
568
682
|
solana: this.solana ?? void 0,
|
|
569
683
|
prefer: this.prefer(network),
|
|
570
684
|
fetch: this.fetch,
|
|
685
|
+
payIn,
|
|
686
|
+
tokenPriceUsd: this.tokenPriceUsd,
|
|
571
687
|
approve: async (req) => {
|
|
572
688
|
release = await this.acquire();
|
|
573
|
-
await this.gate({ kind, what, usd: req.usd, network: req.chain.key, detail: `${req.chain.symbol} to ${req.payTo}` });
|
|
689
|
+
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}` });
|
|
574
690
|
},
|
|
575
691
|
onSigned: (s) => {
|
|
576
692
|
id = this.booked(kind, what, s);
|
|
@@ -596,8 +712,43 @@ var Wafermap = class {
|
|
|
596
712
|
async resources() {
|
|
597
713
|
return listResources(this.baseUrl, this.timeoutMs);
|
|
598
714
|
}
|
|
715
|
+
passCache = null;
|
|
716
|
+
/** A holder pass (shared/pass.ts) signed by this wallet, valid 24 h and reused until its last 10 minutes. It
|
|
717
|
+
* proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
|
|
718
|
+
* a message moves no funds. */
|
|
719
|
+
async holderPass() {
|
|
720
|
+
const p = decodePass(this.passCache);
|
|
721
|
+
if (this.passCache && p && p.exp - 600 > Date.now() / 1e3) return this.passCache;
|
|
722
|
+
this.passCache = encodePass(await makePass({ address: this.account.address, signMessage: (a) => this.account.signMessage(a) }));
|
|
723
|
+
return this.passCache;
|
|
724
|
+
}
|
|
725
|
+
// ---- Paying in $WAFERMAP (Permit2; the tokens are burned) ----
|
|
726
|
+
/** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
|
|
727
|
+
async wafermapApproved() {
|
|
728
|
+
const a = await this.client("robinhood").readContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "allowance", args: [this.address, PERMIT2] });
|
|
729
|
+
return a >= 10n ** 30n;
|
|
730
|
+
}
|
|
731
|
+
/** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
|
|
732
|
+
* transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
|
|
733
|
+
* spendable only by Wafermap's relayer for two minutes. */
|
|
734
|
+
async approveWafermap() {
|
|
735
|
+
const wallet = createWalletClient({ account: this.account, chain: CHAINS.robinhood, transport: http(this.rpc.robinhood) });
|
|
736
|
+
try {
|
|
737
|
+
const tx = await wallet.writeContract({ address: WAFERMAP_TOKEN, abi: erc20Abi2, functionName: "approve", args: [PERMIT2, 2n ** 256n - 1n] });
|
|
738
|
+
const rc = await this.client("robinhood").waitForTransactionReceipt({ hash: tx, timeout: 6e4 });
|
|
739
|
+
if (rc.status !== "success") throw new WafermapError("approve_failed", `The approval reverted: ${tx}`);
|
|
740
|
+
return tx;
|
|
741
|
+
} catch (e) {
|
|
742
|
+
if (e instanceof WafermapError) throw e;
|
|
743
|
+
throw new WafermapError("approve_failed", e?.shortMessage ?? e?.message ?? "The approval failed.");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
599
746
|
async call(id, input = {}, o = {}) {
|
|
600
|
-
const
|
|
747
|
+
const inToken = o.payIn === "wafermap";
|
|
748
|
+
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.");
|
|
749
|
+
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.");
|
|
750
|
+
const pass = id.startsWith("m/") ? null : await this.holderPass();
|
|
751
|
+
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);
|
|
601
752
|
const settle = (r2, out2) => {
|
|
602
753
|
if (!r2.paid || !r2.id) return;
|
|
603
754
|
const status = r2.res.status === 402 ? "refused" : r2.res.ok ? "ok" : "failed";
|
|
@@ -711,19 +862,30 @@ export {
|
|
|
711
862
|
DEFAULT_TIMEOUT_MS,
|
|
712
863
|
KYBER_ROUTER,
|
|
713
864
|
MAX_VALIDITY_SECONDS,
|
|
865
|
+
PASS_HEADER,
|
|
714
866
|
PAY_CHAINS,
|
|
867
|
+
PERMIT2,
|
|
715
868
|
SwapError,
|
|
869
|
+
WAFERMAP_RELAYERS,
|
|
870
|
+
WAFERMAP_TOKEN,
|
|
716
871
|
Wafermap,
|
|
717
872
|
WafermapError,
|
|
718
873
|
X402Error,
|
|
719
874
|
checkPayment,
|
|
720
875
|
choose,
|
|
876
|
+
decodePass,
|
|
721
877
|
decodeSwap,
|
|
878
|
+
encodePass,
|
|
722
879
|
executeSwap,
|
|
880
|
+
holderInfo,
|
|
723
881
|
listResources,
|
|
882
|
+
makePass,
|
|
724
883
|
mergeLog,
|
|
884
|
+
passMessage,
|
|
725
885
|
quoteSwap,
|
|
726
886
|
readQuote,
|
|
727
887
|
signPayment,
|
|
888
|
+
signTokenPayment,
|
|
889
|
+
wafermapPriceUsd,
|
|
728
890
|
x402fetch
|
|
729
891
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const PASS_HEADER = "x-wafermap-pass";
|
|
2
|
+
export declare const PASS_MAX_SECONDS = 86400;
|
|
3
|
+
export type Pass = {
|
|
4
|
+
v: 1;
|
|
5
|
+
address: string;
|
|
6
|
+
iat: number;
|
|
7
|
+
exp: number;
|
|
8
|
+
sig: string;
|
|
9
|
+
};
|
|
10
|
+
/** The exact text the wallet signs. */
|
|
11
|
+
export declare function passMessage(address: string, iat: number, exp: number): string;
|
|
12
|
+
export declare const encodePass: (p: Pass) => string;
|
|
13
|
+
/** The pass's fields, or null when it is not a well-formed pass (the signature is not checked here). */
|
|
14
|
+
export declare function decodePass(s: string | undefined | null): Pass | null;
|
|
15
|
+
/** Signs a new pass with any signer that can personal_sign. Valid for `seconds` (at most a day). */
|
|
16
|
+
export declare function makePass(signer: {
|
|
17
|
+
address: string;
|
|
18
|
+
signMessage: (a: {
|
|
19
|
+
message: string;
|
|
20
|
+
}) => Promise<string>;
|
|
21
|
+
}, seconds?: number, now?: number): Promise<Pass>;
|
|
@@ -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;
|
|
@@ -87,6 +90,8 @@ export declare function mergeLog(entries: SpendEntry[]): SpendEntry[];
|
|
|
87
90
|
export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore">, rpc?: string): Promise<"used" | "unused" | "pending" | "unknown" | "error">;
|
|
88
91
|
export declare const DEFAULT_LIMITS: Limits;
|
|
89
92
|
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
93
|
+
/** What a wallet's $WAFERMAP unlocks (holder price, Pro free calls left today), from /api/holder. Needs no key. */
|
|
94
|
+
export declare function holderInfo(address: string, baseUrl?: string, timeoutMs?: number): Promise<any>;
|
|
90
95
|
/** Wafermap's resource catalog. Needs no wallet; gives up after `timeoutMs` like every other request. */
|
|
91
96
|
export declare function listResources(baseUrl?: string, timeoutMs?: number): Promise<any[]>;
|
|
92
97
|
export declare class Wafermap {
|
|
@@ -103,6 +108,7 @@ export declare class Wafermap {
|
|
|
103
108
|
readonly timeoutMs: number;
|
|
104
109
|
/** fetch with this wallet's time limit: a server that never answers cannot hold a call forever. */
|
|
105
110
|
readonly fetch: typeof fetch;
|
|
111
|
+
private readonly tokenPriceUsd?;
|
|
106
112
|
constructor(o: WafermapOptions);
|
|
107
113
|
get address(): `0x${string}`;
|
|
108
114
|
client(n: EvmNet): PublicClient;
|
|
@@ -126,8 +132,20 @@ export declare class Wafermap {
|
|
|
126
132
|
private acquire;
|
|
127
133
|
private paidFetch;
|
|
128
134
|
resources(): Promise<any[]>;
|
|
135
|
+
private passCache;
|
|
136
|
+
/** A holder pass (shared/pass.ts) signed by this wallet, valid 24 h and reused until its last 10 minutes. It
|
|
137
|
+
* proves the wallet to Wafermap for what its $WAFERMAP unlocks (Pro free calls, x402 Tracker Pro). Signing
|
|
138
|
+
* a message moves no funds. */
|
|
139
|
+
holderPass(): Promise<string>;
|
|
140
|
+
/** Whether this wallet has approved Permit2 to move its $WAFERMAP (needed once before paying in $WAFERMAP). */
|
|
141
|
+
wafermapApproved(): Promise<boolean>;
|
|
142
|
+
/** Approves Permit2 (Uniswap's, at its canonical address) to move this wallet's $WAFERMAP: one on-chain
|
|
143
|
+
* transaction on Robinhood Chain, paid in ETH. Each payment after that is a signature for one exact amount,
|
|
144
|
+
* spendable only by Wafermap's relayer for two minutes. */
|
|
145
|
+
approveWafermap(): Promise<string>;
|
|
129
146
|
call(id: string, input?: Record<string, unknown>, o?: {
|
|
130
147
|
network?: Net;
|
|
148
|
+
payIn?: "stablecoin" | "wafermap";
|
|
131
149
|
}): Promise<{
|
|
132
150
|
resource: string;
|
|
133
151
|
result: any;
|
|
@@ -135,10 +153,17 @@ export declare class Wafermap {
|
|
|
135
153
|
network: string;
|
|
136
154
|
asset: string;
|
|
137
155
|
amountUsd: number;
|
|
156
|
+
amountTokens?: number;
|
|
157
|
+
burned?: boolean;
|
|
138
158
|
payer: string;
|
|
139
159
|
tx: string;
|
|
140
160
|
ms: number;
|
|
141
161
|
} | null;
|
|
162
|
+
free?: {
|
|
163
|
+
wallet: string;
|
|
164
|
+
left: number;
|
|
165
|
+
perDay: number;
|
|
166
|
+
};
|
|
142
167
|
}>;
|
|
143
168
|
discover(query: string, limit?: number): Promise<{
|
|
144
169
|
url: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, listResources, mergeLog } from "./core.js";
|
|
1
|
+
export { Wafermap, WafermapError, DEFAULT_LIMITS, DEFAULT_TIMEOUT_MS, checkPayment, holderInfo, listResources, mergeLog } from "./core.js";
|
|
2
|
+
export { PASS_HEADER, passMessage, encodePass, decodePass, makePass } from "../shared/pass";
|
|
2
3
|
export type { Approval, Limits, Net, PaymentStatus, SpendEntry, SwapQuote, Token, WafermapOptions } from "./core.js";
|
|
3
|
-
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";
|
|
4
5
|
export { quoteSwap, executeSwap, decodeSwap, KYBER_ROUTER, SwapError } from "../shared/swap";
|
|
5
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.5",
|
|
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": {
|