@wafermapxyz/cli 0.4.6 → 0.4.7
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 +16 -0
- package/README.md +1 -1
- package/dist/cli.js +102 -16
- package/dist/index.js +95 -11
- package/dist/types/shared/solana.d.ts +52 -1
- package/dist/types/shared/x402client.d.ts +10 -0
- package/dist/types/src/core.d.ts +9 -3
- package/dist/types/src/index.d.ts +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.7 (4 Nov 2026)
|
|
4
|
+
|
|
5
|
+
**`history --verify` checks Solana payments.** Until now they showed `unknown`.
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- `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:
|
|
9
|
+
- 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.
|
|
10
|
+
- `used`: a transaction carrying our signature landed and succeeded (the one the server named, or one found in the wallet's USDC account history).
|
|
11
|
+
- `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`.
|
|
12
|
+
- `pending`: not found, and still able to land.
|
|
13
|
+
- `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.
|
|
14
|
+
- 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`.
|
|
15
|
+
- 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`.
|
|
16
|
+
- Solana RPC reads have a 15-second limit and back off on rate limits.
|
|
17
|
+
- SDK: `checkSolanaPayment()` and `SOLANA_PUBLIC_RPC` are exported; `checkPayment()` routes Solana lines to it. `SolanaPayer` gains an optional `blockhashInfo()`; `blockhash()` still returns a string.
|
|
18
|
+
|
|
3
19
|
## 0.4.6 (3 Nov 2026)
|
|
4
20
|
|
|
5
21
|
**`history --verify` tells an executed payment from a cancelled one.** Thank you @GBLIN_Protocol for the catch.
|
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -375,14 +453,17 @@ 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);
|
|
@@ -592,7 +673,8 @@ async function blockAt(client, ts, side, latest) {
|
|
|
592
673
|
return side === "from" ? lo.number : hi.number;
|
|
593
674
|
}
|
|
594
675
|
async function checkPayment(e, rpc) {
|
|
595
|
-
if (e.network === "solana"
|
|
676
|
+
if (e.network === "solana") return checkSolanaPayment(e, rpc).catch(() => "error");
|
|
677
|
+
if (!e.nonce || !(e.network in CHAINS)) return "unknown";
|
|
596
678
|
const chain = PAY_CHAINS.find((c) => c.key === e.network);
|
|
597
679
|
try {
|
|
598
680
|
const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
|
|
@@ -703,7 +785,7 @@ var init_core = __esm({
|
|
|
703
785
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
704
786
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
705
787
|
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;
|
|
788
|
+
this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch), blockhashInfo: () => latestBlockhashInfo(solRpc, this.fetch) } : null;
|
|
707
789
|
this.solanaAddress = sol?.address ?? o.solanaAddress ?? null;
|
|
708
790
|
}
|
|
709
791
|
get address() {
|
|
@@ -742,7 +824,7 @@ var init_core = __esm({
|
|
|
742
824
|
/** Write the signed payment to the ledger before it leaves the machine. */
|
|
743
825
|
booked(kind, what, s) {
|
|
744
826
|
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 } : {} });
|
|
827
|
+
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
828
|
return id;
|
|
747
829
|
}
|
|
748
830
|
/** The outcome line for a booked payment. It adds nothing to the daily total. */
|
|
@@ -1162,7 +1244,7 @@ var VERSION;
|
|
|
1162
1244
|
var init_version = __esm({
|
|
1163
1245
|
"src/version.ts"() {
|
|
1164
1246
|
"use strict";
|
|
1165
|
-
VERSION = "0.4.
|
|
1247
|
+
VERSION = "0.4.7";
|
|
1166
1248
|
}
|
|
1167
1249
|
});
|
|
1168
1250
|
|
|
@@ -1912,14 +1994,18 @@ async function main() {
|
|
|
1912
1994
|
const rows = mergeLog(spendLog()).filter((e) => e.at >= since).reverse();
|
|
1913
1995
|
if (!rows.length) return console.log(`Nothing in the last ${days} days.`);
|
|
1914
1996
|
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 };
|
|
1997
|
+
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, solana: process.env.WAFERMAP_RPC_SOLANA };
|
|
1916
1998
|
const chain = [];
|
|
1917
1999
|
if (verify) {
|
|
2000
|
+
const txCount = /* @__PURE__ */ new Map();
|
|
2001
|
+
for (const e of rows) if (e.network === "solana" && e.tx) txCount.set(e.tx, (txCount.get(e.tx) ?? 0) + 1);
|
|
1918
2002
|
let next = 0;
|
|
1919
2003
|
await Promise.all(Array.from({ length: 4 }, async () => {
|
|
1920
2004
|
while (next < rows.length) {
|
|
1921
2005
|
const i = next++;
|
|
1922
|
-
|
|
2006
|
+
const e = rows[i];
|
|
2007
|
+
const shared = e.network === "solana" && !e.sig && !!e.tx && (txCount.get(e.tx) ?? 0) > 1;
|
|
2008
|
+
chain[i] = e.kind === "swap" ? null : await checkPayment(shared ? { ...e, tx: null } : e, RPC[e.network]);
|
|
1923
2009
|
}
|
|
1924
2010
|
}));
|
|
1925
2011
|
}
|
|
@@ -1934,7 +2020,7 @@ Total $${total.toFixed(4)} in ${pays.length} payments. From 0.3.3 every payment
|
|
|
1934
2020
|
if (verify) {
|
|
1935
2021
|
const never = rows.filter((e, i) => e.kind !== "swap" && chain[i] === "unused").reduce((s, e) => s + e.usd, 0);
|
|
1936
2022
|
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" =
|
|
2023
|
+
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
2024
|
} else console.log("Add --verify to check on-chain which signed payments were executed.");
|
|
1939
2025
|
return;
|
|
1940
2026
|
}
|
package/dist/index.js
CHANGED
|
@@ -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")),
|
|
@@ -406,14 +484,17 @@ async function signPayment(signer, req, resource, solana) {
|
|
|
406
484
|
if (req.chain.vm === "svm") {
|
|
407
485
|
if (!solana) throw new X402Error("no_solana_wallet", "This payment is on Solana and no Solana wallet was given.");
|
|
408
486
|
let transaction;
|
|
487
|
+
let sol;
|
|
409
488
|
try {
|
|
410
|
-
|
|
489
|
+
const s = await solanaPaymentSigned(solana, { amount: req.amount, payTo: req.payTo, asset: req.asset, feePayer: req.extra.feePayer, memo: req.extra.memo });
|
|
490
|
+
transaction = s.transaction;
|
|
491
|
+
sol = { sig: s.signature, memo: s.memo, blockhash: s.blockhash, ...s.slot !== void 0 ? { slot: s.slot } : {} };
|
|
411
492
|
} catch (e) {
|
|
412
493
|
if (e?.name === "TimeoutError" || e?.cause?.name === "TimeoutError") throw e;
|
|
413
494
|
throw new X402Error("sign_failed", e?.message ?? "Could not build the Solana payment.");
|
|
414
495
|
}
|
|
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 } })) };
|
|
496
|
+
if (req.version === 1) return { name: "X-PAYMENT", value: b64encode(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.raw.network, payload: { transaction } })), sol };
|
|
497
|
+
return { name: "PAYMENT-SIGNATURE", value: b64encode(JSON.stringify({ x402Version: 2, resource, accepted: req.raw, payload: { transaction } })), sol };
|
|
417
498
|
}
|
|
418
499
|
const nonce = randomNonce();
|
|
419
500
|
const validBefore = BigInt(Math.floor(Date.now() / 1e3) + Math.min(req.maxTimeoutSeconds, MAX_VALIDITY_SECONDS));
|
|
@@ -467,7 +548,7 @@ async function x402fetch(url, init, opts) {
|
|
|
467
548
|
const svm = req.chain.vm === "svm";
|
|
468
549
|
const signer = svm ? null : typeof opts.signer === "function" ? await opts.signer() : opts.signer;
|
|
469
550
|
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 } : {} };
|
|
551
|
+
const signed = { req, from: svm ? opts.solana.address : signer.address, ...header.auth ? { auth: header.auth } : {}, ...header.sol ? { sol: header.sol } : {} };
|
|
471
552
|
opts.onSigned?.(signed);
|
|
472
553
|
const headers = new Headers(init.headers);
|
|
473
554
|
headers.set(header.name, header.value);
|
|
@@ -552,7 +633,8 @@ async function blockAt(client, ts, side, latest) {
|
|
|
552
633
|
}
|
|
553
634
|
var AUTH_USED = parseAbiItem("event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)");
|
|
554
635
|
async function checkPayment(e, rpc) {
|
|
555
|
-
if (e.network === "solana"
|
|
636
|
+
if (e.network === "solana") return checkSolanaPayment(e, rpc).catch(() => "error");
|
|
637
|
+
if (!e.nonce || !(e.network in CHAINS)) return "unknown";
|
|
556
638
|
const chain = PAY_CHAINS.find((c) => c.key === e.network);
|
|
557
639
|
try {
|
|
558
640
|
const client = createPublicClient({ chain: CHAINS[e.network], transport: http(rpc) });
|
|
@@ -642,7 +724,7 @@ var Wafermap = class {
|
|
|
642
724
|
this.fetch = (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs) });
|
|
643
725
|
const sol = o.solana ?? (o.privateKey ? solanaKeyFromEvm(o.privateKey) : null);
|
|
644
726
|
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;
|
|
727
|
+
this.solana = sol ? { address: sol.address, sign: sol.sign, blockhash: () => latestBlockhash(solRpc, this.fetch), blockhashInfo: () => latestBlockhashInfo(solRpc, this.fetch) } : null;
|
|
646
728
|
this.solanaAddress = sol?.address ?? o.solanaAddress ?? null;
|
|
647
729
|
}
|
|
648
730
|
get address() {
|
|
@@ -681,7 +763,7 @@ var Wafermap = class {
|
|
|
681
763
|
/** Write the signed payment to the ledger before it leaves the machine. */
|
|
682
764
|
booked(kind, what, s) {
|
|
683
765
|
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 } : {} });
|
|
766
|
+
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
767
|
return id;
|
|
686
768
|
}
|
|
687
769
|
/** The outcome line for a booked payment. It adds nothing to the daily total. */
|
|
@@ -908,6 +990,7 @@ export {
|
|
|
908
990
|
PASS_HEADER,
|
|
909
991
|
PAY_CHAINS,
|
|
910
992
|
PERMIT2,
|
|
993
|
+
SOLANA_PUBLIC_RPC,
|
|
911
994
|
SwapError,
|
|
912
995
|
WAFERMAP_RELAYERS,
|
|
913
996
|
WAFERMAP_TOKEN,
|
|
@@ -915,6 +998,7 @@ export {
|
|
|
915
998
|
WafermapError,
|
|
916
999
|
X402Error,
|
|
917
1000
|
checkPayment,
|
|
1001
|
+
checkSolanaPayment,
|
|
918
1002
|
choose,
|
|
919
1003
|
decodePass,
|
|
920
1004
|
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). */
|
|
@@ -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
|
};
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wafermapxyz/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
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": {
|