@wafermapxyz/cli 0.4.6 → 0.4.8
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 +25 -0
- package/README.md +4 -4
- package/dist/cli.js +120 -29
- package/dist/index.js +102 -15
- package/dist/types/shared/solana.d.ts +52 -1
- package/dist/types/shared/x402client.d.ts +11 -1
- package/dist/types/src/core.d.ts +10 -4
- package/dist/types/src/index.d.ts +3 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.8 (5 Nov 2026)
|
|
4
|
+
|
|
5
|
+
**Two more payment networks: Arc and Soneium.**
|
|
6
|
+
|
|
7
|
+
- `--network arc` pays in USDC on Arc (eip155:5042, the ERC-20 face of Arc's native USDC at `0x3600…0000`, 6 decimals) and `--network soneium` in bridged USDC.e on Soneium (eip155:1868). Both are EIP-3009 transfers, gasless for you; Wafermap settles them through the Elsa facilitator. The EIP-712 domains (`"USDC", "2"` and `"Bridged USDC (Soneium)", "2"`) were checked against each token's `DOMAIN_SEPARATOR()`.
|
|
8
|
+
- `balance`, `withdraw` and `history --verify` cover both. RPCs: `WAFERMAP_RPC_ARC`, `WAFERMAP_RPC_SONEIUM` (public ones by default). On Arc the gas token is USDC itself; on Soneium it is ETH.
|
|
9
|
+
- The MCP server's `network` option takes `arc` and `soneium`.
|
|
10
|
+
- LLM resources can stream: pass `accept: text/event-stream` to `x402fetch` and read `delta`, `receipt` and `done` events (https://wafermap.xyz/docs#streaming). Payment works as before and settles once the model finished.
|
|
11
|
+
|
|
12
|
+
## 0.4.7 (4 Nov 2026)
|
|
13
|
+
|
|
14
|
+
**`history --verify` checks Solana payments.** Until now they showed `unknown`.
|
|
15
|
+
|
|
16
|
+
- Each Solana payment now logs what it can be found by later: the wallet's own signature (the second one in the transaction; the first, which names it on explorers, is the facilitator's), the memo, the blockhash and the slot it was read at.
|
|
17
|
+
- `wafermap history --verify` reads a Solana RPC, the public one by default (`WAFERMAP_RPC_SOLANA` for another), never Wafermap's, and takes no number from the server that was paid:
|
|
18
|
+
- The payment is anchored to the block its blockhash names: the block at the logged slot must carry exactly that hash, or the check stops with `error`. The payment can land only after that block, and only until the chain is 150 blocks past it.
|
|
19
|
+
- `used`: a transaction carrying our signature landed and succeeded (the one the server named, or one found in the wallet's USDC account history).
|
|
20
|
+
- `unused`: the finalized chain is past the blockhash's last valid height (read first), and the history, read from a node at least that far along and back to the blockhash's block, has no transaction with our signature. Such a payment can never execute. A transaction that landed with our signature and failed moved nothing: also `unused`.
|
|
21
|
+
- `pending`: not found, and still able to land.
|
|
22
|
+
- `error`: anything the RPC could not establish for sure (a block or history it cannot return, a time-out). Never `unused` on a doubtful read.
|
|
23
|
+
- Solana lines logged before 0.4.7 have no signature: they are `used` when the server's transaction shows this wallet signing and its USDC going down by exactly the signed amount (and no other line was given the same transaction), otherwise `unknown`.
|
|
24
|
+
- Reviewed adversarially twice before release. The fixes that came out of it: the expiry is read before the search; the cut-off is a block checked on-chain, not the local clock or a slot from Wafermap's RPC; a fresh blockhash the finalized chain has not reached yet reads `pending`, not `unused`.
|
|
25
|
+
- Solana RPC reads have a 15-second limit and back off on rate limits.
|
|
26
|
+
- SDK: `checkSolanaPayment()` and `SOLANA_PUBLIC_RPC` are exported; `checkPayment()` routes Solana lines to it. `SolanaPayer` gains an optional `blockhashInfo()`; `blockhash()` still returns a string.
|
|
27
|
+
|
|
3
28
|
## 0.4.6 (3 Nov 2026)
|
|
4
29
|
|
|
5
30
|
**`history --verify` tells an executed payment from a cancelled one.** Thank you @GBLIN_Protocol for the catch.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The CLI, SDK and MCP server behind [Wafermap](https://wafermap.xyz): an x402 wal
|
|
|
6
6
|
- **discover** third-party x402 services in the open x402 index and **pay** them
|
|
7
7
|
- **swap** tokens through the KyberSwap aggregator
|
|
8
8
|
|
|
9
|
-
Every payment is checked against a per-payment cap and a daily cap before anything is signed. Swaps have their own per-swap cap. Payments settle in USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or
|
|
9
|
+
Every payment is checked against a per-payment cap and a daily cap before anything is signed. Swaps have their own per-swap cap. Payments settle in USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium and are gasless; swaps need a little ETH for gas.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -35,7 +35,7 @@ wafermap swap 0.01 ETH USDG
|
|
|
35
35
|
wafermap history
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Add `--yes` to skip the confirmation prompt (caps still apply), `--network base`, `arbitrum`, `polygon` or `solana` to pay in USDC on that network (swaps run on `robinhood` or `base`). Set `WAFERMAP_PASSWORD` to avoid the password prompt in scripts.
|
|
38
|
+
Add `--yes` to skip the confirmation prompt (caps still apply), `--network base`, `arbitrum`, `polygon`, `arc`, `soneium` or `solana` to pay in USDC on that network (swaps run on `robinhood` or `base`). Set `WAFERMAP_PASSWORD` to avoid the password prompt in scripts.
|
|
39
39
|
|
|
40
40
|
## MCP server
|
|
41
41
|
|
|
@@ -82,7 +82,7 @@ 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…, or run `wafermap holder`.
|
|
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, Arc, Soneium). Nothing to set up: `wafermap call` names the wallet to Wafermap and the quote comes back discounted; the server re-checks the balance of the wallet that signs. Marketplace listings and Solana payments keep the regular price. Check a wallet: https://wafermap.xyz/api/holder?address=0x…, or run `wafermap holder`.
|
|
86
86
|
|
|
87
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
88
|
|
|
@@ -94,7 +94,7 @@ The caps sit with the key, on your machine. Before the wallet signs anything it
|
|
|
94
94
|
|
|
95
95
|
**The daily cap counts what the wallet signed, not what a server says it charged.** Every payment is written to `spend.jsonl` at its full signed amount the moment it is signed, before it is sent, and stays counted whatever the server answers: an error, a receipt saying $0, no receipt, or no answer at all. Check, sign and record run one payment at a time within a process, so calls started together cannot share the same remaining budget. Across processes on the same wallet (two MCP clients, or the CLI next to the MCP server), the wallet re-reads the shared log right after recording a signed payment and, if the cap is now passed, drops that payment before it is sent. (Before 0.3.3 the cap trusted the server's receipt; see [CHANGELOG.md](CHANGELOG.md).)
|
|
96
96
|
|
|
97
|
-
**What the wallet cannot enforce.** An x402 `exact` payment is a signed authorization (EIP-3009 on EVM chains, a partially signed transaction on Solana). Whoever receives it can execute it once until it expires. The wallet signs EVM authorizations for at most 90 seconds, or less when the server asks for less (300 before 0.4.0). "A failed call costs $0" is therefore a promise of the server you pay: Wafermap's server settles only after the resource worked and otherwise lets the authorization expire unused. You do not have to take that on trust: `wafermap history --verify` asks each token contract (`authorizationState`) whether every signed payment was executed, and shows the ones that expired unused (`error` means the RPC did not answer). 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
|
|
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 checked too, on the public Solana RPC by default (`WAFERMAP_RPC_SOLANA` to use another), not on Wafermap's: from 0.4.7 the wallet logs its own signature, the memo, the blockhash and its slot for each Solana payment. The check anchors the payment to the block its blockhash names (verified on-chain), and shows `used` when a transaction carrying the wallet's signature landed and succeeded, `unused` when the chain is past the blockhash's last valid height and the wallet's USDC history back to that block has no such transaction, and `pending` otherwise. Anything the RPC cannot establish for sure shows `error`, never `unused`. Solana payments logged before 0.4.7 can only be confirmed through the transaction the server named; otherwise they show `unknown`. The caps bound the worst case either way.
|
|
98
98
|
|
|
99
99
|
**Time limits.** Requests to paid resources and services, to Wafermap's API, to the swap API and to Solana RPC give up after 60 seconds (`timeoutMs` in the SDK); EVM RPC calls keep viem's own time-outs. `wafermap resources` uses the same limit. Set `WAFERMAP_TIMEOUT_MS` to change it for the CLI and the MCP server. A paid call that times out after the payment was signed says so: the payment stays counted, and for EVM payments the message says until when it can be executed.
|
|
100
100
|
|
package/dist/cli.js
CHANGED
|
@@ -202,7 +202,7 @@ function ataOf(owner, mint = SOLANA_USDC) {
|
|
|
202
202
|
}
|
|
203
203
|
throw new Error("No associated token account address.");
|
|
204
204
|
}
|
|
205
|
-
async function
|
|
205
|
+
async function solanaPaymentSigned(payer, q) {
|
|
206
206
|
if (q.asset !== SOLANA_USDC) throw new Error("Only USDC is paid on Solana.");
|
|
207
207
|
if (!isSolanaAddress(q.feePayer) || !isSolanaAddress(q.payTo)) throw new Error("The quote has no valid Solana fee payer or recipient.");
|
|
208
208
|
if (q.feePayer === payer.address) throw new Error("The fee payer cannot be the paying wallet.");
|
|
@@ -219,19 +219,26 @@ async function solanaPayment(payer, q) {
|
|
|
219
219
|
{ program: TOKEN_PROGRAM, accounts: [source, q.asset, dest, payer.address], data: concat(Uint8Array.of(12), le(BigInt(q.amount), 8), Uint8Array.of(USDC_DECIMALS)) },
|
|
220
220
|
{ program: MEMO_PROGRAM, accounts: [], data: memo }
|
|
221
221
|
];
|
|
222
|
+
const bh = payer.blockhashInfo ? await payer.blockhashInfo() : { blockhash: await payer.blockhash() };
|
|
222
223
|
const message = concat(
|
|
223
224
|
Uint8Array.of(128, 2, 1, 4),
|
|
224
225
|
// v0; 2 signatures, 1 of them readonly; 4 readonly non-signers
|
|
225
226
|
shortvec(keys.length),
|
|
226
227
|
...keys.map((k) => base58.decode(k)),
|
|
227
|
-
base58.decode(
|
|
228
|
+
base58.decode(bh.blockhash),
|
|
228
229
|
shortvec(ixs.length),
|
|
229
230
|
...ixs.map((ix) => concat(Uint8Array.of(at(ix.program)), shortvec(ix.accounts.length), Uint8Array.from(ix.accounts.map(at)), shortvec(ix.data.length), ix.data)),
|
|
230
231
|
shortvec(0)
|
|
231
232
|
// no address lookup tables
|
|
232
233
|
);
|
|
233
234
|
const signature = await payer.sign(message);
|
|
234
|
-
return
|
|
235
|
+
return {
|
|
236
|
+
transaction: base64.encode(concat(shortvec(2), new Uint8Array(64), signature, message)),
|
|
237
|
+
signature: base58.encode(signature),
|
|
238
|
+
memo: new TextDecoder().decode(memo),
|
|
239
|
+
blockhash: bh.blockhash,
|
|
240
|
+
..."slot" in bh && Number.isFinite(bh.slot) ? { slot: bh.slot } : {}
|
|
241
|
+
};
|
|
235
242
|
}
|
|
236
243
|
async function solanaRpc(url, method, params, f = fetch) {
|
|
237
244
|
const r = await f(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }) });
|
|
@@ -240,6 +247,70 @@ async function solanaRpc(url, method, params, f = fetch) {
|
|
|
240
247
|
if (j.error) throw new Error(j.error.message ?? "Solana RPC error.");
|
|
241
248
|
return j.result;
|
|
242
249
|
}
|
|
250
|
+
async function solanaRead(url, method, params, f = fetch) {
|
|
251
|
+
const timed = (input, init) => f(input, { ...init, signal: AbortSignal.timeout(READ_TIMEOUT_MS) });
|
|
252
|
+
for (let attempt = 0; ; attempt++) {
|
|
253
|
+
try {
|
|
254
|
+
return await solanaRpc(url, method, params, timed);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
if (attempt >= 6 || !/429|rate|too many/i.test(String(e?.message))) throw e;
|
|
257
|
+
await new Promise((r) => setTimeout(r, 1e3 * 2 ** attempt * (0.75 + Math.random() / 2)));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function txOutcome(url, tx, e, f) {
|
|
262
|
+
const t = await solanaRead(url, "getTransaction", [tx, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: "confirmed" }], f);
|
|
263
|
+
if (!t) return null;
|
|
264
|
+
const sigs = t.transaction?.signatures ?? [];
|
|
265
|
+
const keys = t.transaction?.message?.accountKeys ?? [];
|
|
266
|
+
if (e.sig ? !sigs.includes(e.sig) : keys[1] !== e.payer) return "other";
|
|
267
|
+
if (t.meta?.err) return "failed";
|
|
268
|
+
if (!e.sig) {
|
|
269
|
+
const bal = (list) => BigInt(list?.find((b) => b.owner === e.payer && b.mint === SOLANA_USDC)?.uiTokenAmount?.amount ?? "0");
|
|
270
|
+
const drop = bal(t.meta?.preTokenBalances) - bal(t.meta?.postTokenBalances);
|
|
271
|
+
if (drop <= 0n || e.usd !== void 0 && drop !== BigInt(Math.round(e.usd * 1e6))) return "other";
|
|
272
|
+
}
|
|
273
|
+
return "ok";
|
|
274
|
+
}
|
|
275
|
+
async function checkSolanaPayment(e, url = SOLANA_PUBLIC_RPC, f) {
|
|
276
|
+
if (e.tx) {
|
|
277
|
+
const o = await txOutcome(url, e.tx, e, f).catch(() => null);
|
|
278
|
+
if (o === "ok") return "used";
|
|
279
|
+
if (o === "failed" && e.sig) return "unused";
|
|
280
|
+
}
|
|
281
|
+
if (!e.sig || !e.blockhash || !Number.isSafeInteger(e.slot) || e.slot <= 0) return "unknown";
|
|
282
|
+
const slot = e.slot;
|
|
283
|
+
const block = await solanaRead(url, "getBlock", [slot, { commitment: "confirmed", transactionDetails: "none", rewards: false, maxSupportedTransactionVersion: 0 }], f);
|
|
284
|
+
if (!block || block.blockhash !== e.blockhash || !Number.isSafeInteger(block.blockHeight)) throw new Error("The chain has no block with this payment's blockhash at the logged slot.");
|
|
285
|
+
const lastValidHeight = block.blockHeight + BLOCKHASH_MAX_AGE;
|
|
286
|
+
const fin = await solanaRead(url, "getEpochInfo", [{ commitment: "finalized" }], f);
|
|
287
|
+
const S = Number(fin?.absoluteSlot);
|
|
288
|
+
const H = Number(fin?.blockHeight);
|
|
289
|
+
if (!Number.isSafeInteger(S) || !Number.isSafeInteger(H)) throw new Error("The RPC did not give the finalized height.");
|
|
290
|
+
const expired = H > lastValidHeight;
|
|
291
|
+
let before;
|
|
292
|
+
let reached = false;
|
|
293
|
+
search: for (let page = 0; page < 20; page++) {
|
|
294
|
+
const list = await solanaRead(url, "getSignaturesForAddress", [ataOf(e.payer), { limit: 1e3, commitment: "confirmed", minContextSlot: S, ...before ? { before } : {} }], f);
|
|
295
|
+
if (!Array.isArray(list)) throw new Error("The RPC did not return the wallet's history.");
|
|
296
|
+
for (const x of list) {
|
|
297
|
+
if (!Number.isSafeInteger(x.slot)) throw new Error("The RPC returned history without slots.");
|
|
298
|
+
if (x.slot <= slot) {
|
|
299
|
+
reached = true;
|
|
300
|
+
break search;
|
|
301
|
+
}
|
|
302
|
+
if (e.memo && typeof x.memo === "string" && x.memo && !x.memo.includes(e.memo)) continue;
|
|
303
|
+
const o = await txOutcome(url, x.signature, e, f);
|
|
304
|
+
if (o === "ok") return "used";
|
|
305
|
+
if (o === "failed") return "unused";
|
|
306
|
+
if (o === null) throw new Error("The RPC lists a transaction it cannot return.");
|
|
307
|
+
}
|
|
308
|
+
if (list.length < 1e3) break;
|
|
309
|
+
before = list[list.length - 1].signature;
|
|
310
|
+
}
|
|
311
|
+
if (!reached) throw new Error("Could not read the wallet's history back to this payment.");
|
|
312
|
+
return expired ? "unused" : "pending";
|
|
313
|
+
}
|
|
243
314
|
async function solanaBalances(url, owner, f) {
|
|
244
315
|
const [usdc, lamports] = await Promise.all([
|
|
245
316
|
solanaRpc(url, "getAccountInfo", [ataOf(owner), { commitment: "confirmed", encoding: "jsonParsed" }], f).then((r) => String(r.value?.data?.parsed?.info?.tokenAmount?.uiAmountString ?? "0")),
|
|
@@ -247,7 +318,7 @@ async function solanaBalances(url, owner, f) {
|
|
|
247
318
|
]);
|
|
248
319
|
return { usdc, sol: String(lamports / 1e9) };
|
|
249
320
|
}
|
|
250
|
-
var SOLANA_CAIP, SOLANA_USDC, USDC_DECIMALS, TOKEN_PROGRAM, ATA_PROGRAM, COMPUTE_BUDGET, MEMO_PROGRAM, CU_LIMIT, CU_PRICE_MICROLAMPORTS, MAX_MEMO_BYTES, utf8, concat, hexBytes, le, shortvec, onCurve, latestBlockhash;
|
|
321
|
+
var SOLANA_CAIP, SOLANA_USDC, USDC_DECIMALS, TOKEN_PROGRAM, ATA_PROGRAM, COMPUTE_BUDGET, MEMO_PROGRAM, CU_LIMIT, CU_PRICE_MICROLAMPORTS, MAX_MEMO_BYTES, utf8, concat, hexBytes, le, shortvec, onCurve, latestBlockhash, latestBlockhashInfo, SOLANA_PUBLIC_RPC, READ_TIMEOUT_MS, BLOCKHASH_MAX_AGE;
|
|
251
322
|
var init_solana = __esm({
|
|
252
323
|
"shared/solana.ts"() {
|
|
253
324
|
"use strict";
|
|
@@ -291,7 +362,14 @@ var init_solana = __esm({
|
|
|
291
362
|
return false;
|
|
292
363
|
}
|
|
293
364
|
};
|
|
294
|
-
latestBlockhash = async (url, f) => (await
|
|
365
|
+
latestBlockhash = async (url, f) => (await latestBlockhashInfo(url, f)).blockhash;
|
|
366
|
+
latestBlockhashInfo = async (url, f) => {
|
|
367
|
+
const r = await solanaRpc(url, "getLatestBlockhash", [{ commitment: "confirmed" }], f);
|
|
368
|
+
return { blockhash: r.value.blockhash, slot: Number(r.context?.slot) };
|
|
369
|
+
};
|
|
370
|
+
SOLANA_PUBLIC_RPC = "https://api.mainnet-beta.solana.com";
|
|
371
|
+
READ_TIMEOUT_MS = 15e3;
|
|
372
|
+
BLOCKHASH_MAX_AGE = 150;
|
|
295
373
|
}
|
|
296
374
|
});
|
|
297
375
|
|
|
@@ -369,20 +447,23 @@ function choose(q, prefer = CHAIN_KEYS) {
|
|
|
369
447
|
const r = q.accepts.find((a) => a.chain.key === k);
|
|
370
448
|
if (r) return r;
|
|
371
449
|
}
|
|
372
|
-
throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or
|
|
450
|
+
throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium.` : "The service offered no payment option.");
|
|
373
451
|
}
|
|
374
452
|
async function signPayment(signer, req, resource, solana) {
|
|
375
453
|
if (req.chain.vm === "svm") {
|
|
376
454
|
if (!solana) throw new X402Error("no_solana_wallet", "This payment is on Solana and no Solana wallet was given.");
|
|
377
455
|
let transaction;
|
|
456
|
+
let sol;
|
|
378
457
|
try {
|
|
379
|
-
|
|
458
|
+
const s = await solanaPaymentSigned(solana, { amount: req.amount, payTo: req.payTo, asset: req.asset, feePayer: req.extra.feePayer, memo: req.extra.memo });
|
|
459
|
+
transaction = s.transaction;
|
|
460
|
+
sol = { sig: s.signature, memo: s.memo, blockhash: s.blockhash, ...s.slot !== void 0 ? { slot: s.slot } : {} };
|
|
380
461
|
} catch (e) {
|
|
381
462
|
if (e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError") throw e;
|
|
382
463
|
throw new X402Error("sign_failed", e?.message ?? "Could not build the Solana payment.");
|
|
383
464
|
}
|
|
384
|
-
if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })) };
|
|
385
|
-
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })) };
|
|
465
|
+
if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })), sol };
|
|
466
|
+
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })), sol };
|
|
386
467
|
}
|
|
387
468
|
const nonce = randomNonce();
|
|
388
469
|
const validBefore = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_VALIDITY_SECONDS));
|
|
@@ -436,7 +517,7 @@ async function x402fetch(url, init, opts) {
|
|
|
436
517
|
const svm = req.chain.vm === "svm";
|
|
437
518
|
const signer = svm ? null : typeof opts.signer === "function" ? await opts.signer() : opts.signer;
|
|
438
519
|
const header = await signPayment(signer, req, quote.resource, opts.solana);
|
|
439
|
-
const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {} };
|
|
520
|
+
const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {}, ...header.sol ? { sol: header.sol } : {} };
|
|
440
521
|
opts.onSigned?.(signed);
|
|
441
522
|
const headers = new Headers(init.headers);
|
|
442
523
|
headers.set(header.name, header.value);
|
|
@@ -454,7 +535,10 @@ var init_x402client = __esm({
|
|
|
454
535
|
{ key: "solana", vm: "svm", id: 0, caip: SOLANA_CAIP, v1: "solana", stable: SOLANA_USDC, symbol: "USDC", name: "Solana" },
|
|
455
536
|
{ key: "base", vm: "evm", id: 8453, caip: "eip155:8453", v1: "base", stable: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol: "USDC", name: "Base", domain: USDC_DOMAIN },
|
|
456
537
|
{ key: "arbitrum", vm: "evm", id: 42161, caip: "eip155:42161", v1: "arbitrum", stable: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", symbol: "USDC", name: "Arbitrum", domain: USDC_DOMAIN },
|
|
457
|
-
{ key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN }
|
|
538
|
+
{ key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN },
|
|
539
|
+
// EIP-712 domains read from the token contracts (name(), version()) and checked against DOMAIN_SEPARATOR().
|
|
540
|
+
{ key: "arc", vm: "evm", id: 5042, caip: "eip155:5042", v1: "arc", stable: "0x3600000000000000000000000000000000000000", symbol: "USDC", name: "Arc", domain: { name: "USDC", version: "2" } },
|
|
541
|
+
{ key: "soneium", vm: "evm", id: 1868, caip: "eip155:1868", v1: "soneium", stable: "0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369", symbol: "USDC", name: "Soneium", domain: { name: "Bridged USDC (Soneium)", version: "2" } }
|
|
458
542
|
];
|
|
459
543
|
EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
|
|
460
544
|
CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
|
|
@@ -562,7 +646,7 @@ __export(core_exports, {
|
|
|
562
646
|
});
|
|
563
647
|
import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http, pad, parseAbiItem, toEventSelector } from "viem";
|
|
564
648
|
import { privateKeyToAccount } from "viem/accounts";
|
|
565
|
-
import { arbitrum, base, polygon, robinhood } from "viem/chains";
|
|
649
|
+
import { arbitrum, arc, base, polygon, robinhood, soneium } from "viem/chains";
|
|
566
650
|
function mergeLog(entries) {
|
|
567
651
|
const outcomes = new Map(entries.filter((e) => e.of).map((e) => [e.of, e]));
|
|
568
652
|
return entries.filter((e) => !e.of).map((e) => {
|
|
@@ -592,7 +676,8 @@ async function blockAt(client, ts, side, latest) {
|
|
|
592
676
|
return side === "from" ? lo.number : hi.number;
|
|
593
677
|
}
|
|
594
678
|
async function checkPayment(e, rpc) {
|
|
595
|
-
if (e.network === "solana"
|
|
679
|
+
if (e.network === "solana") return checkSolanaPayment(e, rpc).catch(() => "error");
|
|
680
|
+
if (!e.nonce || !(e.network in CHAINS)) return "unknown";
|
|
596
681
|
const chain = PAY_CHAINS.find((c) => c.key === e.network);
|
|
597
682
|
try {
|
|
598
683
|
const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
|
|
@@ -666,7 +751,7 @@ var init_core = __esm({
|
|
|
666
751
|
}
|
|
667
752
|
code;
|
|
668
753
|
};
|
|
669
|
-
CHAINS = { robinhood, base, arbitrum, polygon };
|
|
754
|
+
CHAINS = { robinhood, base, arbitrum, polygon, arc, soneium };
|
|
670
755
|
AUTH_STATE_ABI = [{ type: "function", name: "authorizationState", stateMutability: "view", inputs: [{ name: "authorizer", type: "address" }, { name: "nonce", type: "bytes32" }], outputs: [{ type: "bool" }] }];
|
|
671
756
|
AUTH_USED = parseAbiItem("event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)");
|
|
672
757
|
DEFAULT_LIMITS = { perPayment: 0.1, perDay: 1, perSwap: 25 };
|
|
@@ -703,7 +788,7 @@ var init_core = __esm({
|
|
|
703
788
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
704
789
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
705
790
|
const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
|
|
706
|
-
this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch) } : null;
|
|
791
|
+
this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch), blockhashInfo: () => latestBlockhashInfo(solRpc, this.fetch) } : null;
|
|
707
792
|
this.solanaAddress = sol?.address ?? o.solanaAddress ?? null;
|
|
708
793
|
}
|
|
709
794
|
get address() {
|
|
@@ -742,7 +827,7 @@ var init_core = __esm({
|
|
|
742
827
|
/** Write the signed payment to the ledger before it leaves the machine. */
|
|
743
828
|
booked(kind, what, s) {
|
|
744
829
|
const id = crypto.randomUUID();
|
|
745
|
-
this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {} });
|
|
830
|
+
this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {}, ...s.sol ? { sig: s.sol.sig, memo: s.sol.memo, blockhash: s.sol.blockhash, ...s.sol.slot !== void 0 ? { slot: s.sol.slot } : {} } : {} });
|
|
746
831
|
return id;
|
|
747
832
|
}
|
|
748
833
|
/** The outcome line for a booked payment. It adds nothing to the daily total. */
|
|
@@ -1144,7 +1229,7 @@ function openWallet(password2, approve) {
|
|
|
1144
1229
|
limits: c.limits,
|
|
1145
1230
|
ledger: { entries: spendLog, record: appendSpend },
|
|
1146
1231
|
approve,
|
|
1147
|
-
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, solana: process.env.WAFERMAP_RPC_SOLANA }
|
|
1232
|
+
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, arc: process.env.WAFERMAP_RPC_ARC, soneium: process.env.WAFERMAP_RPC_SONEIUM, solana: process.env.WAFERMAP_RPC_SOLANA }
|
|
1148
1233
|
});
|
|
1149
1234
|
if (wm.solanaAddress) rememberSolana(wm.solanaAddress);
|
|
1150
1235
|
return wm;
|
|
@@ -1162,7 +1247,7 @@ var VERSION;
|
|
|
1162
1247
|
var init_version = __esm({
|
|
1163
1248
|
"src/version.ts"() {
|
|
1164
1249
|
"use strict";
|
|
1165
|
-
VERSION = "0.4.
|
|
1250
|
+
VERSION = "0.4.8";
|
|
1166
1251
|
}
|
|
1167
1252
|
});
|
|
1168
1253
|
|
|
@@ -1281,7 +1366,7 @@ async function runMcp() {
|
|
|
1281
1366
|
"wafermap_discover",
|
|
1282
1367
|
{
|
|
1283
1368
|
title: "Discover x402 services",
|
|
1284
|
-
description: "Search the open x402 index for third-party paid APIs (weather, search, enrichment, data\u2026). Services marked payable accept USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or
|
|
1369
|
+
description: "Search the open x402 index for third-party paid APIs (weather, search, enrichment, data\u2026). Services marked payable accept USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium and can be called with wafermap_pay.",
|
|
1285
1370
|
inputSchema: { query: z.string().min(2).describe("What you need, in a few words."), limit: z.number().int().min(1).max(20).optional() }
|
|
1286
1371
|
},
|
|
1287
1372
|
async ({ query, limit }) => {
|
|
@@ -1359,7 +1444,7 @@ var init_mcp = __esm({
|
|
|
1359
1444
|
init_local();
|
|
1360
1445
|
init_store();
|
|
1361
1446
|
init_version();
|
|
1362
|
-
NET = z.enum(["robinhood", "solana", "base", "arbitrum", "polygon"]).optional().describe("Payment network: robinhood (USDG), or solana, base, arbitrum or
|
|
1447
|
+
NET = z.enum(["robinhood", "solana", "base", "arbitrum", "polygon", "arc", "soneium"]).optional().describe("Payment network: robinhood (USDG), or solana, base, arbitrum, polygon, arc or soneium (USDC). Defaults to your configured network.");
|
|
1363
1448
|
SWAP_NET = z.enum(["robinhood", "base"]).optional().describe("Swap network: robinhood or base. Defaults to your configured network when it is one of these, else robinhood.");
|
|
1364
1449
|
}
|
|
1365
1450
|
});
|
|
@@ -1693,7 +1778,9 @@ var PAY_NETWORKS = {
|
|
|
1693
1778
|
solana: { id: 0, caip: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", name: "Solana", asset: "USDC", explorer: "https://solscan.io", vm: "svm" },
|
|
1694
1779
|
base: { ...NETWORKS.base, vm: "evm" },
|
|
1695
1780
|
arbitrum: { id: 42161, caip: "eip155:42161", name: "Arbitrum", asset: "USDC", explorer: "https://arbiscan.io", vm: "evm" },
|
|
1696
|
-
polygon: { id: 137, caip: "eip155:137", name: "Polygon", asset: "USDC", explorer: "https://polygonscan.com", vm: "evm" }
|
|
1781
|
+
polygon: { id: 137, caip: "eip155:137", name: "Polygon", asset: "USDC", explorer: "https://polygonscan.com", vm: "evm" },
|
|
1782
|
+
arc: { id: 5042, caip: "eip155:5042", name: "Arc", asset: "USDC", explorer: "https://explorer.arc.io", vm: "evm" },
|
|
1783
|
+
soneium: { id: 1868, caip: "eip155:1868", name: "Soneium", asset: "USDC", explorer: "https://soneium.blockscout.com", vm: "evm" }
|
|
1697
1784
|
};
|
|
1698
1785
|
function payNetworkOf(v) {
|
|
1699
1786
|
const s = String(v ?? "").toLowerCase();
|
|
@@ -1708,7 +1795,7 @@ init_local();
|
|
|
1708
1795
|
init_solana();
|
|
1709
1796
|
init_store();
|
|
1710
1797
|
init_version();
|
|
1711
|
-
var HELP = `wafermap ${VERSION}: an x402 wallet for agents (Robinhood Chain, Solana, Base, Arbitrum, Polygon)
|
|
1798
|
+
var HELP = `wafermap ${VERSION}: an x402 wallet for agents (Robinhood Chain, Solana, Base, Arbitrum, Polygon, Arc, Soneium)
|
|
1712
1799
|
|
|
1713
1800
|
Wallet
|
|
1714
1801
|
wafermap init [--import] create (or import) the agent wallet, encrypted with a password
|
|
@@ -1735,7 +1822,7 @@ Trade
|
|
|
1735
1822
|
Agents
|
|
1736
1823
|
wafermap mcp run as an MCP server (stdio); password from \`wafermap keychain on\` or WAFERMAP_PASSWORD
|
|
1737
1824
|
|
|
1738
|
-
Common flags: --network robinhood|solana|base|arbitrum|polygon --yes (skip the confirmation)
|
|
1825
|
+
Common flags: --network robinhood|solana|base|arbitrum|polygon|arc|soneium --yes (skip the confirmation)
|
|
1739
1826
|
Files live in ${HOME}. Docs: https://wafermap.xyz/docs#cli`;
|
|
1740
1827
|
function parse(argv) {
|
|
1741
1828
|
const out = { _: [], flags: {} };
|
|
@@ -1837,7 +1924,7 @@ async function main() {
|
|
|
1837
1924
|
console.log(`\u2713 Agent wallet ${acct.address}
|
|
1838
1925
|
Solana address ${sol} (derived from the same key)
|
|
1839
1926
|
Key encrypted in ${HOME}/keystore.json. Forget the password and the key is gone: run \`wafermap export-key\` to back it up.
|
|
1840
|
-
Fund it with a few dollars of USDC on Solana (Solana address), or USDG on Robinhood Chain or USDC on Base, Arbitrum or
|
|
1927
|
+
Fund it with a few dollars of USDC on Solana (Solana address), or USDG on Robinhood Chain or USDC on Base, Arbitrum, Polygon, Arc or Soneium (EVM address). Payments are gasless; swaps need a little ETH.
|
|
1841
1928
|
Caps: $0.10 per payment, $1 per day, $25 per swap. Change with \`wafermap limits\`.`);
|
|
1842
1929
|
return;
|
|
1843
1930
|
}
|
|
@@ -1912,14 +1999,18 @@ async function main() {
|
|
|
1912
1999
|
const rows = mergeLog(spendLog()).filter((e) => e.at >= since).reverse();
|
|
1913
2000
|
if (!rows.length) return console.log(`Nothing in the last ${days} days.`);
|
|
1914
2001
|
const verify = a.flags.verify !== void 0;
|
|
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 };
|
|
2002
|
+
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, arc: process.env.WAFERMAP_RPC_ARC, soneium: process.env.WAFERMAP_RPC_SONEIUM, solana: process.env.WAFERMAP_RPC_SOLANA };
|
|
1916
2003
|
const chain = [];
|
|
1917
2004
|
if (verify) {
|
|
2005
|
+
const txCount = /* @__PURE__ */ new Map();
|
|
2006
|
+
for (const e of rows) if (e.network === "solana" && e.tx) txCount.set(e.tx, (txCount.get(e.tx) ?? 0) + 1);
|
|
1918
2007
|
let next = 0;
|
|
1919
2008
|
await Promise.all(Array.from({ length: 4 }, async () => {
|
|
1920
2009
|
while (next < rows.length) {
|
|
1921
2010
|
const i = next++;
|
|
1922
|
-
|
|
2011
|
+
const e = rows[i];
|
|
2012
|
+
const shared = e.network === "solana" && !e.sig && !!e.tx && (txCount.get(e.tx) ?? 0) > 1;
|
|
2013
|
+
chain[i] = e.kind === "swap" ? null : await checkPayment(shared ? { ...e, tx: null } : e, RPC[e.network]);
|
|
1923
2014
|
}
|
|
1924
2015
|
}));
|
|
1925
2016
|
}
|
|
@@ -1934,7 +2025,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
1934
2025
|
if (verify) {
|
|
1935
2026
|
const never = rows.filter((e, i) => e.kind !== "swap" && chain[i] === "unused").reduce((s, e) => s + e.usd, 0);
|
|
1936
2027
|
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" =
|
|
2028
|
+
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" = an older log line with nothing to check by; "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).`);
|
|
1938
2029
|
} else console.log("Add --verify to check on-chain which signed payments were executed.");
|
|
1939
2030
|
return;
|
|
1940
2031
|
}
|
|
@@ -1953,7 +2044,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
1953
2044
|
${s.description.slice(0, 140)}${s.input ? `
|
|
1954
2045
|
input: ${JSON.stringify(s.input).slice(0, 140)}` : ""}`);
|
|
1955
2046
|
return console.log(`
|
|
1956
|
-
\u25CF payable with USDG (Robinhood Chain) or USDC (Solana, Base, Arbitrum, Polygon) \u25CB other networks only`);
|
|
2047
|
+
\u25CF payable with USDG (Robinhood Chain) or USDC (Solana, Base, Arbitrum, Polygon, Arc, Soneium) \u25CB other networks only`);
|
|
1957
2048
|
}
|
|
1958
2049
|
case "call": {
|
|
1959
2050
|
const id = a._[0] ?? die("Which resource? e.g. wafermap call gas-now network=base");
|
|
@@ -1977,7 +2068,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
1977
2068
|
case "balance": {
|
|
1978
2069
|
const addr = keystoreAddress() ?? die("No wallet yet. Run `wafermap init`.");
|
|
1979
2070
|
const { Wafermap: Wafermap2 } = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
1980
|
-
const wm = new Wafermap2({ account: { ...privateKeyToAccount2(generatePrivateKey()), address: addr }, solanaAddress: keystoreSolana() ?? void 0, baseUrl: process.env.WAFERMAP_URL, 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, solana: process.env.WAFERMAP_RPC_SOLANA } });
|
|
2071
|
+
const wm = new Wafermap2({ account: { ...privateKeyToAccount2(generatePrivateKey()), address: addr }, solanaAddress: keystoreSolana() ?? void 0, baseUrl: process.env.WAFERMAP_URL, 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, arc: process.env.WAFERMAP_RPC_ARC, soneium: process.env.WAFERMAP_RPC_SONEIUM, solana: process.env.WAFERMAP_RPC_SOLANA } });
|
|
1981
2072
|
const b = await wm.balances();
|
|
1982
2073
|
console.log(addr);
|
|
1983
2074
|
for (const n of NETS) console.log(` ${n.padEnd(10)} ${b[n] ? `${Number(b[n].stable).toFixed(4)} ${b[n].symbol} ${Number(b[n].eth).toPrecision(4)} ${b[n].gas}` : n === "solana" && !wm.solanaAddress ? "run `wafermap address` once with WAFERMAP_PASSWORD set" : "unreadable (RPC did not answer)"}`);
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
2
|
import { createPublicClient, createWalletClient, erc20Abi as erc20Abi2, formatUnits as formatUnits2, http, pad, parseAbiItem, toEventSelector } from "viem";
|
|
3
3
|
import { privateKeyToAccount } from "viem/accounts";
|
|
4
|
-
import { arbitrum, base, polygon, robinhood } from "viem/chains";
|
|
4
|
+
import { arbitrum, arc, base, polygon, robinhood, soneium } from "viem/chains";
|
|
5
5
|
|
|
6
6
|
// shared/swap.ts
|
|
7
7
|
import { decodeFunctionData, erc20Abi, formatUnits, getAddress, isAddress, parseAbi, parseUnits } from "viem";
|
|
@@ -225,7 +225,7 @@ function ataOf(owner, mint = SOLANA_USDC) {
|
|
|
225
225
|
}
|
|
226
226
|
throw new Error("No associated token account address.");
|
|
227
227
|
}
|
|
228
|
-
async function
|
|
228
|
+
async function solanaPaymentSigned(payer, q) {
|
|
229
229
|
if (q.asset !== SOLANA_USDC) throw new Error("Only USDC is paid on Solana.");
|
|
230
230
|
if (!isSolanaAddress(q.feePayer) || !isSolanaAddress(q.payTo)) throw new Error("The quote has no valid Solana fee payer or recipient.");
|
|
231
231
|
if (q.feePayer === payer.address) throw new Error("The fee payer cannot be the paying wallet.");
|
|
@@ -242,19 +242,26 @@ async function solanaPayment(payer, q) {
|
|
|
242
242
|
{ program: TOKEN_PROGRAM, accounts: [source, q.asset, dest, payer.address], data: concat(Uint8Array.of(12), le(BigInt(q.amount), 8), Uint8Array.of(USDC_DECIMALS)) },
|
|
243
243
|
{ program: MEMO_PROGRAM, accounts: [], data: memo }
|
|
244
244
|
];
|
|
245
|
+
const bh = payer.blockhashInfo ? await payer.blockhashInfo() : { blockhash: await payer.blockhash() };
|
|
245
246
|
const message = concat(
|
|
246
247
|
Uint8Array.of(128, 2, 1, 4),
|
|
247
248
|
// v0; 2 signatures, 1 of them readonly; 4 readonly non-signers
|
|
248
249
|
shortvec(keys.length),
|
|
249
250
|
...keys.map((k) => base58.decode(k)),
|
|
250
|
-
base58.decode(
|
|
251
|
+
base58.decode(bh.blockhash),
|
|
251
252
|
shortvec(ixs.length),
|
|
252
253
|
...ixs.map((ix) => concat(Uint8Array.of(at(ix.program)), shortvec(ix.accounts.length), Uint8Array.from(ix.accounts.map(at)), shortvec(ix.data.length), ix.data)),
|
|
253
254
|
shortvec(0)
|
|
254
255
|
// no address lookup tables
|
|
255
256
|
);
|
|
256
257
|
const signature = await payer.sign(message);
|
|
257
|
-
return
|
|
258
|
+
return {
|
|
259
|
+
transaction: base64.encode(concat(shortvec(2), new Uint8Array(64), signature, message)),
|
|
260
|
+
signature: base58.encode(signature),
|
|
261
|
+
memo: new TextDecoder().decode(memo),
|
|
262
|
+
blockhash: bh.blockhash,
|
|
263
|
+
..."slot" in bh && Number.isFinite(bh.slot) ? { slot: bh.slot } : {}
|
|
264
|
+
};
|
|
258
265
|
}
|
|
259
266
|
async function solanaRpc(url, method, params, f = fetch) {
|
|
260
267
|
const r = await f(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }) });
|
|
@@ -263,7 +270,78 @@ async function solanaRpc(url, method, params, f = fetch) {
|
|
|
263
270
|
if (j.error) throw new Error(j.error.message ?? "Solana RPC error.");
|
|
264
271
|
return j.result;
|
|
265
272
|
}
|
|
266
|
-
var latestBlockhash = async (url, f) => (await
|
|
273
|
+
var latestBlockhash = async (url, f) => (await latestBlockhashInfo(url, f)).blockhash;
|
|
274
|
+
var latestBlockhashInfo = async (url, f) => {
|
|
275
|
+
const r = await solanaRpc(url, "getLatestBlockhash", [{ commitment: "confirmed" }], f);
|
|
276
|
+
return { blockhash: r.value.blockhash, slot: Number(r.context?.slot) };
|
|
277
|
+
};
|
|
278
|
+
var SOLANA_PUBLIC_RPC = "https://api.mainnet-beta.solana.com";
|
|
279
|
+
var READ_TIMEOUT_MS = 15e3;
|
|
280
|
+
async function solanaRead(url, method, params, f = fetch) {
|
|
281
|
+
const timed = (input, init) => f(input, { ...init, signal: AbortSignal.timeout(READ_TIMEOUT_MS) });
|
|
282
|
+
for (let attempt = 0; ; attempt++) {
|
|
283
|
+
try {
|
|
284
|
+
return await solanaRpc(url, method, params, timed);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
if (attempt >= 6 || !/429|rate|too many/i.test(String(e?.message))) throw e;
|
|
287
|
+
await new Promise((r) => setTimeout(r, 1e3 * 2 ** attempt * (0.75 + Math.random() / 2)));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function txOutcome(url, tx, e, f) {
|
|
292
|
+
const t = await solanaRead(url, "getTransaction", [tx, { encoding: "json", maxSupportedTransactionVersion: 0, commitment: "confirmed" }], f);
|
|
293
|
+
if (!t) return null;
|
|
294
|
+
const sigs = t.transaction?.signatures ?? [];
|
|
295
|
+
const keys = t.transaction?.message?.accountKeys ?? [];
|
|
296
|
+
if (e.sig ? !sigs.includes(e.sig) : keys[1] !== e.payer) return "other";
|
|
297
|
+
if (t.meta?.err) return "failed";
|
|
298
|
+
if (!e.sig) {
|
|
299
|
+
const bal = (list) => BigInt(list?.find((b) => b.owner === e.payer && b.mint === SOLANA_USDC)?.uiTokenAmount?.amount ?? "0");
|
|
300
|
+
const drop = bal(t.meta?.preTokenBalances) - bal(t.meta?.postTokenBalances);
|
|
301
|
+
if (drop <= 0n || e.usd !== void 0 && drop !== BigInt(Math.round(e.usd * 1e6))) return "other";
|
|
302
|
+
}
|
|
303
|
+
return "ok";
|
|
304
|
+
}
|
|
305
|
+
var BLOCKHASH_MAX_AGE = 150;
|
|
306
|
+
async function checkSolanaPayment(e, url = SOLANA_PUBLIC_RPC, f) {
|
|
307
|
+
if (e.tx) {
|
|
308
|
+
const o = await txOutcome(url, e.tx, e, f).catch(() => null);
|
|
309
|
+
if (o === "ok") return "used";
|
|
310
|
+
if (o === "failed" && e.sig) return "unused";
|
|
311
|
+
}
|
|
312
|
+
if (!e.sig || !e.blockhash || !Number.isSafeInteger(e.slot) || e.slot <= 0) return "unknown";
|
|
313
|
+
const slot = e.slot;
|
|
314
|
+
const block = await solanaRead(url, "getBlock", [slot, { commitment: "confirmed", transactionDetails: "none", rewards: false, maxSupportedTransactionVersion: 0 }], f);
|
|
315
|
+
if (!block || block.blockhash !== e.blockhash || !Number.isSafeInteger(block.blockHeight)) throw new Error("The chain has no block with this payment's blockhash at the logged slot.");
|
|
316
|
+
const lastValidHeight = block.blockHeight + BLOCKHASH_MAX_AGE;
|
|
317
|
+
const fin = await solanaRead(url, "getEpochInfo", [{ commitment: "finalized" }], f);
|
|
318
|
+
const S = Number(fin?.absoluteSlot);
|
|
319
|
+
const H = Number(fin?.blockHeight);
|
|
320
|
+
if (!Number.isSafeInteger(S) || !Number.isSafeInteger(H)) throw new Error("The RPC did not give the finalized height.");
|
|
321
|
+
const expired = H > lastValidHeight;
|
|
322
|
+
let before;
|
|
323
|
+
let reached = false;
|
|
324
|
+
search: for (let page = 0; page < 20; page++) {
|
|
325
|
+
const list = await solanaRead(url, "getSignaturesForAddress", [ataOf(e.payer), { limit: 1e3, commitment: "confirmed", minContextSlot: S, ...before ? { before } : {} }], f);
|
|
326
|
+
if (!Array.isArray(list)) throw new Error("The RPC did not return the wallet's history.");
|
|
327
|
+
for (const x of list) {
|
|
328
|
+
if (!Number.isSafeInteger(x.slot)) throw new Error("The RPC returned history without slots.");
|
|
329
|
+
if (x.slot <= slot) {
|
|
330
|
+
reached = true;
|
|
331
|
+
break search;
|
|
332
|
+
}
|
|
333
|
+
if (e.memo && typeof x.memo === "string" && x.memo && !x.memo.includes(e.memo)) continue;
|
|
334
|
+
const o = await txOutcome(url, x.signature, e, f);
|
|
335
|
+
if (o === "ok") return "used";
|
|
336
|
+
if (o === "failed") return "unused";
|
|
337
|
+
if (o === null) throw new Error("The RPC lists a transaction it cannot return.");
|
|
338
|
+
}
|
|
339
|
+
if (list.length < 1e3) break;
|
|
340
|
+
before = list[list.length - 1].signature;
|
|
341
|
+
}
|
|
342
|
+
if (!reached) throw new Error("Could not read the wallet's history back to this payment.");
|
|
343
|
+
return expired ? "unused" : "pending";
|
|
344
|
+
}
|
|
267
345
|
async function solanaBalances(url, owner, f) {
|
|
268
346
|
const [usdc, lamports] = await Promise.all([
|
|
269
347
|
solanaRpc(url, "getAccountInfo", [ataOf(owner), { commitment: "confirmed", encoding: "jsonParsed" }], f).then((r) => String(r.value?.data?.parsed?.info?.tokenAmount?.uiAmountString ?? "0")),
|
|
@@ -279,7 +357,10 @@ var PAY_CHAINS = [
|
|
|
279
357
|
{ key: "solana", vm: "svm", id: 0, caip: SOLANA_CAIP, v1: "solana", stable: SOLANA_USDC, symbol: "USDC", name: "Solana" },
|
|
280
358
|
{ key: "base", vm: "evm", id: 8453, caip: "eip155:8453", v1: "base", stable: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol: "USDC", name: "Base", domain: USDC_DOMAIN },
|
|
281
359
|
{ key: "arbitrum", vm: "evm", id: 42161, caip: "eip155:42161", v1: "arbitrum", stable: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", symbol: "USDC", name: "Arbitrum", domain: USDC_DOMAIN },
|
|
282
|
-
{ key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN }
|
|
360
|
+
{ key: "polygon", vm: "evm", id: 137, caip: "eip155:137", v1: "polygon", stable: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", symbol: "USDC", name: "Polygon", domain: USDC_DOMAIN },
|
|
361
|
+
// EIP-712 domains read from the token contracts (name(), version()) and checked against DOMAIN_SEPARATOR().
|
|
362
|
+
{ key: "arc", vm: "evm", id: 5042, caip: "eip155:5042", v1: "arc", stable: "0x3600000000000000000000000000000000000000", symbol: "USDC", name: "Arc", domain: { name: "USDC", version: "2" } },
|
|
363
|
+
{ key: "soneium", vm: "evm", id: 1868, caip: "eip155:1868", v1: "soneium", stable: "0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369", symbol: "USDC", name: "Soneium", domain: { name: "Bridged USDC (Soneium)", version: "2" } }
|
|
283
364
|
];
|
|
284
365
|
var EVM_CHAINS = PAY_CHAINS.filter((c) => c.vm === "evm");
|
|
285
366
|
var CHAIN_KEYS = PAY_CHAINS.map((c) => c.key);
|
|
@@ -388,7 +469,7 @@ function choose(q, prefer = CHAIN_KEYS) {
|
|
|
388
469
|
const r = q.accepts.find((a) => a.chain.key === k);
|
|
389
470
|
if (r) return r;
|
|
390
471
|
}
|
|
391
|
-
throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum or
|
|
472
|
+
throw new X402Error("no_supported_payment", q.offered.length ? `This service only accepts ${[...new Set(q.offered)].join(", ")}. Wafermap pays USDG on Robinhood Chain or USDC on Solana, Base, Arbitrum, Polygon, Arc or Soneium.` : "The service offered no payment option.");
|
|
392
473
|
}
|
|
393
474
|
var TYPES = {
|
|
394
475
|
TransferWithAuthorization: [
|
|
@@ -406,14 +487,17 @@ async function signPayment(signer, req, resource, solana) {
|
|
|
406
487
|
if (req.chain.vm === "svm") {
|
|
407
488
|
if (!solana) throw new X402Error("no_solana_wallet", "This payment is on Solana and no Solana wallet was given.");
|
|
408
489
|
let transaction;
|
|
490
|
+
let sol;
|
|
409
491
|
try {
|
|
410
|
-
|
|
492
|
+
const s = await solanaPaymentSigned(solana, { amount: req.amount, payTo: req.payTo, asset: req.asset, feePayer: req.extra.feePayer, memo: req.extra.memo });
|
|
493
|
+
transaction = s.transaction;
|
|
494
|
+
sol = { sig: s.signature, memo: s.memo, blockhash: s.blockhash, ...s.slot !== void 0 ? { slot: s.slot } : {} };
|
|
411
495
|
} catch (e) {
|
|
412
496
|
if (e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError") throw e;
|
|
413
497
|
throw new X402Error("sign_failed", e?.message ?? "Could not build the Solana payment.");
|
|
414
498
|
}
|
|
415
|
-
if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })) };
|
|
416
|
-
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })) };
|
|
499
|
+
if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })), sol };
|
|
500
|
+
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })), sol };
|
|
417
501
|
}
|
|
418
502
|
const nonce = randomNonce();
|
|
419
503
|
const validBefore = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_VALIDITY_SECONDS));
|
|
@@ -467,7 +551,7 @@ async function x402fetch(url, init, opts) {
|
|
|
467
551
|
const svm = req.chain.vm === "svm";
|
|
468
552
|
const signer = svm ? null : typeof opts.signer === "function" ? await opts.signer() : opts.signer;
|
|
469
553
|
const header = await signPayment(signer, req, quote.resource, opts.solana);
|
|
470
|
-
const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {} };
|
|
554
|
+
const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {}, ...header.sol ? { sol: header.sol } : {} };
|
|
471
555
|
opts.onSigned?.(signed);
|
|
472
556
|
const headers = new Headers(init.headers);
|
|
473
557
|
headers.set(header.name, header.value);
|
|
@@ -520,7 +604,7 @@ var WafermapError = class extends Error {
|
|
|
520
604
|
}
|
|
521
605
|
code;
|
|
522
606
|
};
|
|
523
|
-
var CHAINS = { robinhood, base, arbitrum, polygon };
|
|
607
|
+
var CHAINS = { robinhood, base, arbitrum, polygon, arc, soneium };
|
|
524
608
|
var AUTH_STATE_ABI = [{ type: "function", name: "authorizationState", stateMutability: "view", inputs: [{ name: "authorizer", type: "address" }, { name: "nonce", type: "bytes32" }], outputs: [{ type: "bool" }] }];
|
|
525
609
|
function mergeLog(entries) {
|
|
526
610
|
const outcomes = new Map(entries.filter((e) => e.of).map((e) => [e.of, e]));
|
|
@@ -552,7 +636,8 @@ async function blockAt(client, ts, side, latest) {
|
|
|
552
636
|
}
|
|
553
637
|
var AUTH_USED = parseAbiItem("event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)");
|
|
554
638
|
async function checkPayment(e, rpc) {
|
|
555
|
-
if (e.network === "solana"
|
|
639
|
+
if (e.network === "solana") return checkSolanaPayment(e, rpc).catch(() => "error");
|
|
640
|
+
if (!e.nonce || !(e.network in CHAINS)) return "unknown";
|
|
556
641
|
const chain = PAY_CHAINS.find((c) => c.key === e.network);
|
|
557
642
|
try {
|
|
558
643
|
const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
|
|
@@ -642,7 +727,7 @@ var Wafermap = class {
|
|
|
642
727
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
643
728
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
644
729
|
const solRpc = this.rpc.solana ?? `${this.baseUrl}/api/solana`;
|
|
645
|
-
this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch) } : null;
|
|
730
|
+
this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch), blockhashInfo: () => latestBlockhashInfo(solRpc, this.fetch) } : null;
|
|
646
731
|
this.solanaAddress = sol?.address ?? o.solanaAddress ?? null;
|
|
647
732
|
}
|
|
648
733
|
get address() {
|
|
@@ -681,7 +766,7 @@ var Wafermap = class {
|
|
|
681
766
|
/** Write the signed payment to the ledger before it leaves the machine. */
|
|
682
767
|
booked(kind, what, s) {
|
|
683
768
|
const id = crypto.randomUUID();
|
|
684
|
-
this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {} });
|
|
769
|
+
this.ledger.record({ at: Date.now(), kind, what, usd: s.req.usd, network: s.req.chain.key, tx: null, payer: s.from, id, status: "signed", ...s.auth ? { nonce: s.auth.nonce, validBefore: s.auth.validBefore } : {}, ...s.sol ? { sig: s.sol.sig, memo: s.sol.memo, blockhash: s.sol.blockhash, ...s.sol.slot !== void 0 ? { slot: s.sol.slot } : {} } : {} });
|
|
685
770
|
return id;
|
|
686
771
|
}
|
|
687
772
|
/** The outcome line for a booked payment. It adds nothing to the daily total. */
|
|
@@ -908,6 +993,7 @@ export {
|
|
|
908
993
|
PASS_HEADER,
|
|
909
994
|
PAY_CHAINS,
|
|
910
995
|
PERMIT2,
|
|
996
|
+
SOLANA_PUBLIC_RPC,
|
|
911
997
|
SwapError,
|
|
912
998
|
WAFERMAP_RELAYERS,
|
|
913
999
|
WAFERMAP_TOKEN,
|
|
@@ -915,6 +1001,7 @@ export {
|
|
|
915
1001
|
WafermapError,
|
|
916
1002
|
X402Error,
|
|
917
1003
|
checkPayment,
|
|
1004
|
+
checkSolanaPayment,
|
|
918
1005
|
choose,
|
|
919
1006
|
decodePass,
|
|
920
1007
|
decodeSwap,
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export declare const SOLANA_CAIP = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
2
2
|
export declare const SOLANA_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
3
|
-
/** Signs as one Solana address. `blockhash` returns a recent blockhash from any mainnet RPC.
|
|
3
|
+
/** Signs as one Solana address. `blockhash` returns a recent blockhash from any mainnet RPC. When
|
|
4
|
+
* `blockhashInfo` is given it is used instead, and the slot the blockhash was read at is kept with
|
|
5
|
+
* the payment: the payment cannot land before it, which bounds the search for it later. */
|
|
4
6
|
export type SolanaPayer = {
|
|
5
7
|
address: string;
|
|
6
8
|
sign: (message: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
7
9
|
blockhash: () => Promise<string>;
|
|
10
|
+
blockhashInfo?: () => Promise<{
|
|
11
|
+
blockhash: string;
|
|
12
|
+
slot: number;
|
|
13
|
+
}>;
|
|
8
14
|
};
|
|
9
15
|
export declare function isSolanaAddress(v: string): boolean;
|
|
10
16
|
/** The agent's Solana keypair, derived from its EVM private key. */
|
|
@@ -25,8 +31,53 @@ export type SolanaQuote = {
|
|
|
25
31
|
};
|
|
26
32
|
/** The base64 wire transaction for an x402 `exact` payment on Solana, signed by `payer` only. */
|
|
27
33
|
export declare function solanaPayment(payer: SolanaPayer, q: SolanaQuote): Promise<string>;
|
|
34
|
+
/** What a signed Solana payment leaves to check it by later: our signature (the second one in the
|
|
35
|
+
* transaction; the first, which names it on explorers, is the facilitator's and unknown until it
|
|
36
|
+
* submits), its memo, its blockhash (once that is no longer valid the payment can never execute)
|
|
37
|
+
* and, when known, the slot the blockhash was read at. */
|
|
38
|
+
export type SolanaSigned = {
|
|
39
|
+
transaction: string;
|
|
40
|
+
signature: string;
|
|
41
|
+
memo: string;
|
|
42
|
+
blockhash: string;
|
|
43
|
+
slot?: number;
|
|
44
|
+
};
|
|
45
|
+
export declare function solanaPaymentSigned(payer: SolanaPayer, q: SolanaQuote): Promise<SolanaSigned>;
|
|
28
46
|
export declare function solanaRpc(url: string, method: string, params: unknown[], f?: typeof fetch): Promise<any>;
|
|
29
47
|
export declare const latestBlockhash: (url: string, f?: typeof fetch) => Promise<string>;
|
|
48
|
+
export declare const latestBlockhashInfo: (url: string, f?: typeof fetch) => Promise<{
|
|
49
|
+
blockhash: string;
|
|
50
|
+
slot: number;
|
|
51
|
+
}>;
|
|
52
|
+
/** The public Solana RPC that `checkSolanaPayment` reads by default: not Wafermap's, so the answer
|
|
53
|
+
* does not depend on the server that was paid. */
|
|
54
|
+
export declare const SOLANA_PUBLIC_RPC = "https://api.mainnet-beta.solana.com";
|
|
55
|
+
export type SolanaCheck = {
|
|
56
|
+
payer: string;
|
|
57
|
+
usd?: number;
|
|
58
|
+
tx?: string | null;
|
|
59
|
+
sig?: string;
|
|
60
|
+
memo?: string;
|
|
61
|
+
blockhash?: string;
|
|
62
|
+
slot?: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Was a signed Solana payment ever executed? Read from a Solana RPC (by default the public one, not
|
|
66
|
+
* Wafermap's), and every number it rests on is checked on that RPC, not taken from the server
|
|
67
|
+
* that was paid.
|
|
68
|
+
* - The transaction the server named, if it carries our signature: "used" when it succeeded.
|
|
69
|
+
* - Otherwise the payment is anchored to the block its blockhash names: the block at the logged
|
|
70
|
+
* slot must have exactly that hash. The payment can only land after that slot, and only until
|
|
71
|
+
* the chain is BLOCKHASH_MAX_AGE blocks past it. Then, with the finalized height H at slot S:
|
|
72
|
+
* the wallet's USDC account history, read from a node at slot S or later, back to that slot,
|
|
73
|
+
* is searched for our signature. Found: "used" (or "unused" if it landed and failed: nothing
|
|
74
|
+
* moved, and it cannot land again). Not found: "unused" when H is past the last valid height
|
|
75
|
+
* (any landing would then be final and in that history), else "pending".
|
|
76
|
+
* - Anything that cannot be established that way throws (the CLI shows "error"); it is never
|
|
77
|
+
* "unused". Lines from before 0.4.7 have no signature: only the server's transaction can prove
|
|
78
|
+
* them ("used"), else "unknown".
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkSolanaPayment(e: SolanaCheck, url?: string, f?: typeof fetch): Promise<"used" | "unused" | "pending" | "unknown">;
|
|
30
81
|
/** USDC (6 decimals) and SOL balances of a wallet. A wallet that never held USDC has no token
|
|
31
82
|
* account: 0. Read with getAccountInfo, which free public RPCs serve (getTokenAccountBalance is
|
|
32
83
|
* an "indexed" call some of them reserve for paid keys). */
|
|
@@ -10,7 +10,7 @@ export type Signer = {
|
|
|
10
10
|
message: any;
|
|
11
11
|
}) => Promise<Hex>;
|
|
12
12
|
};
|
|
13
|
-
export type ChainKey = "robinhood" | "base" | "arbitrum" | "polygon" | "solana";
|
|
13
|
+
export type ChainKey = "robinhood" | "base" | "arbitrum" | "polygon" | "arc" | "soneium" | "solana";
|
|
14
14
|
export type Chain = {
|
|
15
15
|
key: ChainKey;
|
|
16
16
|
vm: "evm" | "svm";
|
|
@@ -98,6 +98,7 @@ export declare function signPayment(signer: Signer, req: Requirement, resource:
|
|
|
98
98
|
name: string;
|
|
99
99
|
value: string;
|
|
100
100
|
auth?: SignedAuth;
|
|
101
|
+
sol?: SignedSol;
|
|
101
102
|
}>;
|
|
102
103
|
/** Settlement details a server returns after a paid call, when it sends them. */
|
|
103
104
|
export declare function readSettlement(res: Response): {
|
|
@@ -111,10 +112,19 @@ export type SignedAuth = {
|
|
|
111
112
|
nonce: Hex;
|
|
112
113
|
validBefore: number;
|
|
113
114
|
};
|
|
115
|
+
/** What a Solana payment can be found by on-chain later (see checkSolanaPayment): our signature, its
|
|
116
|
+
* memo, its blockhash and the slot that blockhash was read at. */
|
|
117
|
+
export type SignedSol = {
|
|
118
|
+
sig: string;
|
|
119
|
+
memo: string;
|
|
120
|
+
blockhash: string;
|
|
121
|
+
slot?: number;
|
|
122
|
+
};
|
|
114
123
|
export type Signed = {
|
|
115
124
|
req: Requirement;
|
|
116
125
|
from: string;
|
|
117
126
|
auth?: SignedAuth;
|
|
127
|
+
sol?: SignedSol;
|
|
118
128
|
token?: {
|
|
119
129
|
amount: string;
|
|
120
130
|
};
|
package/dist/types/src/core.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export type SpendEntry = {
|
|
|
31
31
|
/** EVM payments: the EIP-3009 nonce and expiry, to check on-chain whether it was ever used. */
|
|
32
32
|
nonce?: string;
|
|
33
33
|
validBefore?: number;
|
|
34
|
+
/** Solana payments (from 0.4.7): our signature, the memo, the blockhash and the slot it was read at. */
|
|
35
|
+
sig?: string;
|
|
36
|
+
memo?: string;
|
|
37
|
+
blockhash?: string;
|
|
38
|
+
slot?: number;
|
|
34
39
|
/** What the server said it charged, for the record only. */
|
|
35
40
|
reported?: number | null;
|
|
36
41
|
};
|
|
@@ -56,7 +61,7 @@ export type WafermapOptions = {
|
|
|
56
61
|
solanaAddress?: string;
|
|
57
62
|
/** Default https://wafermap.xyz */
|
|
58
63
|
baseUrl?: string;
|
|
59
|
-
/** Preferred payment network. Default robinhood (USDG); base, arbitrum, polygon and solana pay in USDC. */
|
|
64
|
+
/** Preferred payment network. Default robinhood (USDG); base, arbitrum, polygon, arc, soneium and solana pay in USDC. */
|
|
60
65
|
network?: Net;
|
|
61
66
|
limits?: Partial<Limits>;
|
|
62
67
|
/** Where spending is remembered for the daily cap. Default: in memory. */
|
|
@@ -87,10 +92,11 @@ export declare function mergeLog(entries: SpendEntry[]): SpendEntry[];
|
|
|
87
92
|
* can never be charged. authorizationState is also true after cancelAuthorization, so a true is
|
|
88
93
|
* confirmed by the AuthorizationUsed event: first in the receipt of the transaction the server
|
|
89
94
|
* 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
|
|
91
|
-
*
|
|
95
|
+
* None there means the nonce was cancelled, never executed ("cancelled"). Solana payments are
|
|
96
|
+
* checked by checkSolanaPayment (shared/solana.ts), by default on the public Solana RPC. Old log
|
|
97
|
+
* lines with nothing to check by come back "unknown"; an RPC failure comes back "error".
|
|
92
98
|
*/
|
|
93
|
-
export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore"> & {
|
|
99
|
+
export declare function checkPayment(e: Pick<SpendEntry, "network" | "payer" | "nonce" | "validBefore"> & Partial<Pick<SpendEntry, "sig" | "memo" | "blockhash" | "slot" | "usd">> & {
|
|
94
100
|
at?: number;
|
|
95
101
|
tx?: string | null;
|
|
96
102
|
}, rpc?: string): Promise<"used" | "cancelled" | "unused" | "pending" | "unknown" | "error">;
|
|
@@ -3,4 +3,6 @@ export { PASS_HEADER, passMessage, encodePass, decodePass, makePass } from "../s
|
|
|
3
3
|
export type { Approval, Limits, Net, PaymentStatus, SpendEntry, SwapQuote, Token, WafermapOptions } from "./core.js";
|
|
4
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
|
-
export type { Requirement, Quote, Signed, SignedAuth, Signer } from "../shared/x402client";
|
|
6
|
+
export type { Requirement, Quote, Signed, SignedAuth, SignedSol, Signer } from "../shared/x402client";
|
|
7
|
+
export { checkSolanaPayment, SOLANA_PUBLIC_RPC } from "../shared/solana";
|
|
8
|
+
export type { SolanaCheck } from "../shared/solana";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wafermapxyz/cli",
|
|
3
|
-
"version": "0.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.",
|
|
3
|
+
"version": "0.4.8",
|
|
4
|
+
"description": "Wafermap for agents: an encrypted local wallet that pays x402 resources, discovers and pays third-party x402 services (USDG on Robinhood Chain; USDC on Solana, Base, Arbitrum, Polygon, Arc, Soneium), and swaps on Robinhood Chain and Base. CLI, SDK and MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"wafermap": "dist/cli.js"
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"base",
|
|
37
37
|
"arbitrum",
|
|
38
38
|
"polygon",
|
|
39
|
+
"arc",
|
|
40
|
+
"soneium",
|
|
39
41
|
"solana",
|
|
40
42
|
"usdg",
|
|
41
43
|
"usdc"
|