@dvmkit/sdk 0.1.0-rc.2 → 0.1.0-rc.3

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.
Files changed (42) hide show
  1. package/README.md +10 -2
  2. package/dist/{revenue-reporter-GB4WKLDC.js → chunk-2ABMGUDS.js} +1 -0
  3. package/dist/chunk-5SO7ZVOH.js +375 -0
  4. package/dist/chunk-66HGCPBU.js +25 -0
  5. package/dist/{chunk-KQAJVVZT.js → chunk-AZBXSXQT.js} +27 -287
  6. package/dist/chunk-C2DC4FKR.js +21424 -0
  7. package/dist/{chunk-H25M54MI.js → chunk-C3MTFLC6.js} +16 -0
  8. package/dist/{chunk-OJ5WFIB2.js → chunk-EXHBXA4U.js} +1 -1
  9. package/dist/chunk-FT7IM66W.js +1557 -0
  10. package/dist/{chunk-KXWROQGK.js → chunk-FUJ36YDV.js} +1 -24
  11. package/dist/{tempo-charge-store-6GJEMNUU.js → chunk-JZWELPFH.js} +1 -0
  12. package/dist/{chunk-365P52XQ.js → chunk-LWUR4CGG.js} +2 -1
  13. package/dist/chunk-RHP3BRTH.js +1090 -0
  14. package/dist/{tempo-session-store-FTEEGZXA.js → chunk-RU7SXHLO.js} +2 -1
  15. package/dist/{chunk-7IH5SG2A.js → chunk-TKA6ZP4M.js} +62 -41
  16. package/dist/chunk-TVI4V7GF.js +283 -0
  17. package/dist/{payout-reporter-4TNWRS5F.js → chunk-X3IKFWJA.js} +3 -2
  18. package/dist/{chunk-DCNT4PJS.js → chunk-XY5Y5REG.js} +6 -258
  19. package/dist/chunk-XYTSDAPH.js +232 -0
  20. package/dist/chunk-YD3TZNXV.js +1042 -0
  21. package/dist/{credit-ledger-RO4FGSHG.js → credit-ledger-ED6JXKVD.js} +2 -2
  22. package/dist/credit-menu-DONAtGVf.d.ts +5076 -0
  23. package/dist/{ssrf-DZi-xJyn.d.ts → fx-Bq4cvn16.d.ts} +37 -119
  24. package/dist/index.d.ts +8 -66
  25. package/dist/index.js +11 -219
  26. package/dist/internal/index.d.ts +5745 -0
  27. package/dist/internal/index.js +6379 -0
  28. package/dist/{job-store-6gR4pZRP.d.ts → job-store-m2pYmvbr.d.ts} +1493 -12
  29. package/dist/{memory-credit-ledger-I2G64DDK.js → memory-credit-ledger-XJ5VQEVP.js} +3 -3
  30. package/dist/payout-reporter-3UB5WRCV.js +13 -0
  31. package/dist/revenue-reporter-JIKUPXOK.js +7 -0
  32. package/dist/server/index.d.ts +14 -3590
  33. package/dist/server/index.js +309 -20796
  34. package/dist/ssrf-DbFkpDv0.d.ts +118 -0
  35. package/dist/tempo-charge-store-RIFTALZK.js +8 -0
  36. package/dist/tempo-session-store-DALMRIWN.js +11 -0
  37. package/dist/testing/index.d.ts +3 -2
  38. package/dist/testing/index.js +3 -2
  39. package/dist/usd-DjVAPMlf.d.ts +97 -0
  40. package/dist/x402-5EVIUSEP.js +81 -0
  41. package/package.json +6 -2
  42. package/dist/x402-35VLYFKZ.js +0 -1272
@@ -0,0 +1,1090 @@
1
+ import {
2
+ authorizeProtectedRemoval,
3
+ fundingReachableAt,
4
+ info,
5
+ isValidEvmPrivateKey,
6
+ listCredits,
7
+ loadConfig,
8
+ maxAdvertisedMicro,
9
+ updateConfig,
10
+ withinAdvertisedTolerance
11
+ } from "./chunk-FT7IM66W.js";
12
+ import {
13
+ X402_DEFAULT_NETWORK,
14
+ X402_V1_VERSION,
15
+ X402_VERSION,
16
+ assertSupportedX402Network,
17
+ buildPaymentRequirements,
18
+ chainIdFromCaip2,
19
+ createX402FacilitatorAuthHeaders,
20
+ decodePayment,
21
+ encodePayment,
22
+ encodeSettleResponseHeader,
23
+ exactEvmAuthorization,
24
+ settleWithFacilitator,
25
+ usdcContractFor,
26
+ usdcDomainNameFor,
27
+ usdcDomainVersionFor,
28
+ verifyWithFacilitator,
29
+ x402NetworkByCaip2,
30
+ x402NetworkToCaip2,
31
+ x402SupportedNetworksHint
32
+ } from "./chunk-XY5Y5REG.js";
33
+ import {
34
+ DvmError,
35
+ X402_CHANNELS_FILE,
36
+ costAnchor,
37
+ ensureConfigDir,
38
+ fetchBtcUsdRate,
39
+ formatStaleness,
40
+ formatUsd,
41
+ getLastKnownBtcRate,
42
+ msatsToUsdc,
43
+ resolveRpcHttpTransportOptions,
44
+ resolveRpcOverride,
45
+ usdcToMsats
46
+ } from "./chunk-5SO7ZVOH.js";
47
+ import {
48
+ callerLoggers
49
+ } from "./chunk-66HGCPBU.js";
50
+ import {
51
+ redactUrl,
52
+ redactUrlsInText
53
+ } from "./chunk-FUJ36YDV.js";
54
+
55
+ // src/lib/x402/asset.ts
56
+ function resolveX402Asset(network = X402_DEFAULT_NETWORK) {
57
+ const spec = x402NetworkByCaip2(network);
58
+ if (!spec) return null;
59
+ return {
60
+ address: spec.usdc,
61
+ symbol: "USDC",
62
+ // "native" is the load-bearing word: every contract in the table is
63
+ // Circle's own USDC, and the dollars that read as zero here are the
64
+ // bridged ones.
65
+ label: "native USDC",
66
+ unit: "USDC",
67
+ networkLabel: spec.label,
68
+ confusable: spec.confusable
69
+ };
70
+ }
71
+ function x402WrongAssetHint(asset) {
72
+ const wrongToken = asset.confusable ? `Another dollar on ${asset.networkLabel} \u2014 ${asset.confusable}, the legacy bridged dollar, or any other stablecoin \u2014 is a different contract and will never show up here, however much of it your wallet says you hold.` : `Another dollar on ${asset.networkLabel} is a different contract and will never show up here, however much of it your wallet says you hold.`;
73
+ return `This rail reads only ${asset.label} (${asset.address}) on ${asset.networkLabel}. ${wrongToken} USDC on a different chain is invisible here too \u2014 this wallet reads ${asset.networkLabel} alone. If you funded the wrong one, swap it to ${asset.label} on ${asset.networkLabel}, or bridge the funds across to ${asset.networkLabel}; either way they land where this rail can spend them.`;
74
+ }
75
+
76
+ // src/lib/x402/client.ts
77
+ import { randomBytes } from "crypto";
78
+
79
+ // src/lib/btc-rate.ts
80
+ async function resolveBtcUsdRate(purpose) {
81
+ try {
82
+ return { rate: await fetchBtcUsdRate(), stale: false };
83
+ } catch {
84
+ const cached = getLastKnownBtcRate();
85
+ if (cached) {
86
+ warnStaleRate(cached.cachedAt);
87
+ return { rate: cached.rate, stale: true, cachedAt: cached.cachedAt };
88
+ }
89
+ throw rateUnavailable(purpose);
90
+ }
91
+ }
92
+ async function tryResolveBtcUsdRate() {
93
+ try {
94
+ return await resolveBtcUsdRate("conversion");
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ var warnedStale = false;
100
+ function warnStaleRate(cachedAt) {
101
+ if (warnedStale) return;
102
+ warnedStale = true;
103
+ info(
104
+ `Warning: the BTC/USD rate provider is unreachable \u2014 using the last rate this machine saw (cached ${formatStaleness(cachedAt)}). Dollar figures may have drifted.`
105
+ );
106
+ }
107
+ function rateUnavailable(purpose) {
108
+ const outcome = purpose === "payment" ? "nothing was paid" : "no conversion was performed";
109
+ const noun = purpose === "payment" ? "payment" : "conversion";
110
+ const capped = outcome.charAt(0).toUpperCase() + outcome.slice(1);
111
+ return new DvmError(
112
+ "rate_unavailable",
113
+ `Could not price this ${noun}: the BTC/USD rate provider is unreachable and no cached rate is available.`,
114
+ `${capped}. The rate provider (CoinGecko) is rate-limiting or down, and this machine has never cached a rate. Retry in a minute; if it keeps failing, check network access to api.coingecko.com.${purpose === "payment" ? " Budgets are denominated in dollars, so a payment cannot be authorised without a rate." : ""}`,
115
+ {
116
+ display: `BTC/USD rate unavailable \u2014 ${outcome}.`,
117
+ anchor: "rate_unavailable",
118
+ purpose,
119
+ provider: "coingecko",
120
+ retryable: true
121
+ }
122
+ );
123
+ }
124
+
125
+ // src/lib/payment-authorization-bounds.ts
126
+ var X402_EXACT_AUTHORIZATION_TTL_SECONDS = 600;
127
+ var X402_REFUSAL_TRANSACTION_DISCOVERY_WINDOW_MS = 15 * 6e4;
128
+
129
+ // src/lib/x402/channel-store.ts
130
+ import { AsyncLocalStorage } from "async_hooks";
131
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
132
+ import lockfile from "proper-lockfile";
133
+ function x402ExactFundingDead(pending, now = Date.now()) {
134
+ const seconds = pending.authorizationValidBefore ? Number(pending.authorizationValidBefore) : NaN;
135
+ return Number.isFinite(seconds) ? now >= seconds * 1e3 : now >= pending.requestedAt + X402_EXACT_AUTHORIZATION_TTL_SECONDS * 1e3;
136
+ }
137
+ function recordPendingX402ExactFunding(pending) {
138
+ const { file, credits } = loadFundingFile();
139
+ file.exact = [
140
+ ...file.exact.filter((entry) => !sameFundingCredit(entry, pending, credits)),
141
+ pending
142
+ ];
143
+ saveFile(file);
144
+ }
145
+ function loadResumableX402ExactFunding(args) {
146
+ const { file, credits } = loadFundingFile();
147
+ return file.exact.find(
148
+ (entry) => sameFundingCredit(entry, args, credits) && !x402ExactFundingDead(entry, args.now)
149
+ );
150
+ }
151
+ function listResumableX402ExactFundings(filter = {}) {
152
+ const endpoint = filter.endpoint ? normalizeEndpoint(filter.endpoint) : void 0;
153
+ const { file, credits } = loadFundingFile();
154
+ return file.exact.filter(
155
+ (entry) => (!endpoint || fundingReachableAt(entry, endpoint, credits)) && (!filter.callerPubkey || entry.callerPubkey === filter.callerPubkey) && !x402ExactFundingDead(entry, filter.now)
156
+ );
157
+ }
158
+ function clearPendingX402ExactFunding(pending) {
159
+ const { file } = loadFundingFile();
160
+ const key = pendingKey(pending);
161
+ file.exact = file.exact.filter(
162
+ (entry) => pendingKey(entry) !== key || entry.fundId !== pending.fundId
163
+ );
164
+ saveFile(file);
165
+ }
166
+ var X402_PENDING_STALE_MS = 24 * 60 * 60 * 1e3;
167
+ function x402PendingStale(pending, now = Date.now()) {
168
+ return now - pending.requestedAt >= X402_PENDING_STALE_MS;
169
+ }
170
+ function x402PendingRetired(pending) {
171
+ return pending.refusedAt !== void 0 || pending.supersededAt !== void 0;
172
+ }
173
+ function x402PendingBlockExpiresAt(pending) {
174
+ return x402PendingDepositExpiresAt(pending) ?? pending.requestedAt + X402_PENDING_STALE_MS;
175
+ }
176
+ function x402PendingDepositExpiresAt(pending) {
177
+ const seconds = pending.authorizationValidBefore ? Number(pending.authorizationValidBefore) : NaN;
178
+ return Number.isFinite(seconds) ? seconds * 1e3 : void 0;
179
+ }
180
+ function x402PendingUnsettled(pending, now = Date.now()) {
181
+ return !x402PendingRetired(pending) || now <= x402PendingBlockExpiresAt(pending);
182
+ }
183
+ function x402PendingFundingJson(entry) {
184
+ return {
185
+ credit_id: entry.creditId,
186
+ fund_id: entry.fundId,
187
+ endpoint: entry.endpoint,
188
+ channel_id: entry.channelId,
189
+ amount_micro: entry.amountMicro,
190
+ // What the ERC-3009 authorization can still pull, which is the figure a
191
+ // `--force` retirement is really deciding about: a deposit's authorized
192
+ // value can exceed the amount funded (internal-review), so `amount_micro` alone
193
+ // understates it. Null on a voucher-only payload, which authorizes no
194
+ // deposit at all.
195
+ deposit_micro: entry.depositMicro ?? null,
196
+ signed_max_claimable_micro: entry.maxClaimableMicro ?? null,
197
+ requested_at: new Date(entry.requestedAt).toISOString(),
198
+ stale: x402PendingStale(entry),
199
+ ...entry.authorizationValidBefore ? {
200
+ authorization_valid_before: new Date(
201
+ Number(entry.authorizationValidBefore) * 1e3
202
+ ).toISOString()
203
+ } : {},
204
+ ...entry.refusedAt !== void 0 ? {
205
+ refused_at: new Date(entry.refusedAt).toISOString(),
206
+ funding_blocked_until: new Date(x402PendingBlockExpiresAt(entry)).toISOString()
207
+ } : {},
208
+ ...entry.supersededAt !== void 0 ? {
209
+ superseded_at: new Date(entry.supersededAt).toISOString(),
210
+ funding_blocked_until: new Date(x402PendingBlockExpiresAt(entry)).toISOString()
211
+ } : {}
212
+ };
213
+ }
214
+ var EMPTY = { version: 1, channels: {}, pending: [], exact: [] };
215
+ var x402LockOwner = new AsyncLocalStorage();
216
+ async function withX402ChannelLock(operation) {
217
+ if (x402LockOwner.getStore()?.active) return operation();
218
+ ensureX402ChannelFile();
219
+ const release = await lockfile.lock(X402_CHANNELS_FILE, {
220
+ realpath: false,
221
+ retries: { retries: 20, minTimeout: 25, maxTimeout: 250 }
222
+ });
223
+ const owner = { active: true };
224
+ try {
225
+ return await x402LockOwner.run(owner, operation);
226
+ } finally {
227
+ owner.active = false;
228
+ await release();
229
+ }
230
+ }
231
+ var DvmX402ChannelStorage = class {
232
+ nextBalanceProvenance;
233
+ get(key) {
234
+ return fromSynchronousStore(() => loadFile().channels[canonicalChannelId(key)]?.context);
235
+ }
236
+ /** Read the highest cumulative amount a DVM has acknowledged for this channel. */
237
+ getAcknowledgedCumulativeAmount(key) {
238
+ return fromSynchronousStore(
239
+ () => readAcknowledgedCumulativeAmount(loadFile().channels[canonicalChannelId(key)])
240
+ );
241
+ }
242
+ /**
243
+ * Upstream's ordinary write. Its chain-recovery paths are authoritative and
244
+ * clear the unreconciled stamp. DVM-response paths scope this same write to
245
+ * persist their weaker provenance atomically with the balance (internal-review).
246
+ */
247
+ set(key, context) {
248
+ return fromSynchronousStore(() => {
249
+ const provenance = this.nextBalanceProvenance?.provenance ?? "observed";
250
+ this.nextBalanceProvenance = void 0;
251
+ writeContext(key, context, provenance, "advance");
252
+ });
253
+ }
254
+ /**
255
+ * Apply non-chain provenance to the next upstream `set` on this instance.
256
+ * Returns a cleanup for paths where upstream rejects or deletes instead, so a
257
+ * later chain recovery cannot inherit a stale provenance decision.
258
+ */
259
+ stageNextUnobservedBalanceProvenance(provenance) {
260
+ const staged = { provenance };
261
+ this.nextBalanceProvenance = staged;
262
+ return () => {
263
+ if (this.nextBalanceProvenance === staged) this.nextBalanceProvenance = void 0;
264
+ };
265
+ }
266
+ /**
267
+ * Write a context whose `balance` carries a deposit this machine added on its
268
+ * own authority, and stamp the record as unreconciled (internal-review).
269
+ *
270
+ * Separate from {@link set} because the two differ in exactly one thing that
271
+ * matters later: where the figure came from. Nothing else about the write
272
+ * differs.
273
+ */
274
+ setLocallyAppliedDeposit(key, context) {
275
+ return fromSynchronousStore(() => {
276
+ writeContext(key, context, "unreconciled", "advance");
277
+ });
278
+ }
279
+ /**
280
+ * Write a context whose `balance` is byte-for-byte the one already stored,
281
+ * leaving the unreconciled stamp exactly as it found it (internal-review).
282
+ *
283
+ * The replay acknowledgement's idempotent second pass is what needs this: it
284
+ * restates a cumulative base without adding to the balance, so it observes
285
+ * nothing and must not certify the local sum it left alone. The caller owes
286
+ * the precondition in the name — a write that does move `balance` belongs on
287
+ * one of the other two.
288
+ */
289
+ setUnchangedBalance(key, context) {
290
+ return fromSynchronousStore(() => {
291
+ writeContext(key, context, "unchanged", "advance");
292
+ });
293
+ }
294
+ /**
295
+ * Seed upstream's cooperative-refund probe without changing the acknowledged
296
+ * base. Records written before internal-review have only the upstream context, so
297
+ * `hold` adopts its acknowledged value rather than losing that base to the
298
+ * chain floor this seeds. The seed itself is a `channels()` read, so it is
299
+ * `observed` and settles the unreconciled question like any other.
300
+ */
301
+ setRefundProbe(key, context) {
302
+ return fromSynchronousStore(() => {
303
+ writeContext(key, context, "observed", "hold");
304
+ });
305
+ }
306
+ /**
307
+ * Replace the stored projection with a successful `channels()` read.
308
+ *
309
+ * Only `balance` and `totalClaimed` move. The highest DVM-acknowledged
310
+ * cumulative base stays fail-closed, and the chain observation clears the
311
+ * unreconciled stamp.
312
+ */
313
+ reconcileChainState(key, state) {
314
+ return fromSynchronousStore(() => {
315
+ const file = loadFile();
316
+ const id = canonicalChannelId(key);
317
+ const record = file.channels[id] ?? {};
318
+ file.channels[id] = channelRecordWithContext(
319
+ record,
320
+ { ...record.context ?? {}, ...state },
321
+ "observed",
322
+ "hold"
323
+ );
324
+ saveFile(file);
325
+ });
326
+ }
327
+ delete(key) {
328
+ return fromSynchronousStore(() => {
329
+ const file = loadFile();
330
+ const id = canonicalChannelId(key);
331
+ const record = file.channels[id];
332
+ if (!record) return;
333
+ if (record.metadata) file.channels[id] = { metadata: record.metadata };
334
+ else {
335
+ const { [id]: _removed, ...channels } = file.channels;
336
+ file.channels = channels;
337
+ }
338
+ saveFile(file);
339
+ });
340
+ }
341
+ };
342
+ function recordX402ChannelMetadata(metadata) {
343
+ const file = loadFile();
344
+ const id = canonicalChannelId(metadata.channelId);
345
+ const acknowledged = file.channels[id]?.metadata;
346
+ const sameOwner = acknowledged?.endpoint === metadata.endpoint && acknowledged.callerPubkey === metadata.callerPubkey && acknowledged.creditId === metadata.creditId;
347
+ file.channels[id] = {
348
+ ...file.channels[id],
349
+ metadata: !acknowledged || sameOwner ? { ...metadata, channelId: id } : { ...acknowledged, channelId: id }
350
+ };
351
+ saveFile(file);
352
+ }
353
+ function acknowledgeX402ChannelOwner(owner) {
354
+ const file = loadFile();
355
+ const id = canonicalChannelId(owner.channelId);
356
+ const record = file.channels[id];
357
+ if (!record?.metadata) {
358
+ throw new Error(
359
+ `Cannot acknowledge credit ${owner.creditId}: x402 channel ${id} has no metadata`
360
+ );
361
+ }
362
+ file.channels[id] = {
363
+ ...record,
364
+ metadata: {
365
+ ...record.metadata,
366
+ endpoint: owner.endpoint,
367
+ callerPubkey: owner.callerPubkey,
368
+ creditId: owner.creditId,
369
+ updatedAt: Date.now()
370
+ }
371
+ };
372
+ saveFile(file);
373
+ }
374
+ function listX402Channels(filter = {}) {
375
+ const payer = filter.payer?.toLowerCase();
376
+ return allChannelMetadata().filter((entry) => !payer || entry.config.payer.toLowerCase() === payer).sort((a, b) => b.updatedAt - a.updatedAt);
377
+ }
378
+ function findX402Channel(args) {
379
+ const channels = allChannelMetadata();
380
+ if (args.channelId) {
381
+ const channelId = canonicalChannelId(args.channelId);
382
+ return channels.find((entry) => entry.channelId === channelId);
383
+ }
384
+ const endpoint = args.endpoint ? normalizeEndpoint(args.endpoint) : void 0;
385
+ return channels.filter(
386
+ (entry) => (!endpoint || entry.endpoint === endpoint) && (!args.callerPubkey || entry.callerPubkey === args.callerPubkey) && (!args.creditId || entry.creditId === args.creditId)
387
+ ).sort((a, b) => b.updatedAt - a.updatedAt)[0];
388
+ }
389
+ function x402ChannelBalanceUnreconciledAt(channelId) {
390
+ return loadFile().channels[canonicalChannelId(channelId)]?.unreconciledBalanceAt;
391
+ }
392
+ function recordPendingX402Funding(pending) {
393
+ const { file, credits } = loadFundingFile();
394
+ const pendingOrigin = normalizeEndpoint(pending.endpoint);
395
+ file.pending = [
396
+ ...file.pending.flatMap((entry) => {
397
+ if (!sameFundingCredit(entry, pending, credits) || x402PendingRetired(entry)) return [entry];
398
+ if (normalizeEndpoint(entry.endpoint) === pendingOrigin) return [];
399
+ return [{ ...entry, supersededAt: pending.requestedAt }];
400
+ }),
401
+ pending
402
+ ];
403
+ saveFile(file);
404
+ }
405
+ function loadResumableX402Funding(args) {
406
+ const { file, credits } = loadFundingFile();
407
+ return file.pending.find(
408
+ (entry) => sameFundingCredit(entry, args, credits) && !x402PendingRetired(entry)
409
+ );
410
+ }
411
+ function listPendingX402Fundings(filter = {}) {
412
+ const endpoint = filter.endpoint ? normalizeEndpoint(filter.endpoint) : void 0;
413
+ const { file, credits } = loadFundingFile();
414
+ return file.pending.filter(
415
+ (entry) => (!endpoint || fundingReachableAt(entry, endpoint, credits)) && (!filter.callerPubkey || entry.callerPubkey === filter.callerPubkey)
416
+ );
417
+ }
418
+ function listResumableX402Fundings(filter = {}) {
419
+ return listPendingX402Fundings(filter).filter((entry) => !x402PendingRetired(entry));
420
+ }
421
+ function listPendingX402FundingsForChannel(channelId) {
422
+ const canonical = canonicalChannelId(channelId);
423
+ return loadFundingFile().file.pending.filter(
424
+ (entry) => canonicalChannelId(entry.channelId) === canonical
425
+ );
426
+ }
427
+ function retirePendingX402Funding(pending) {
428
+ const { file } = loadFundingFile();
429
+ const key = pendingKey(pending);
430
+ const target = file.pending.find(
431
+ (entry) => pendingKey(entry) === key && !x402PendingRetired(entry)
432
+ );
433
+ if (!target) return;
434
+ const refusedAt = Date.now();
435
+ file.pending = file.pending.map((entry) => entry === target ? { ...entry, refusedAt } : entry);
436
+ saveFile(file);
437
+ }
438
+ function clearPendingX402Funding(pending) {
439
+ const { file } = loadFundingFile();
440
+ const next = file.pending.filter((entry) => !sameJournalEntry(entry, pending));
441
+ if (next.length === file.pending.length) return;
442
+ file.pending = next;
443
+ saveFile(file);
444
+ }
445
+ function writeContext(key, context, provenance, base) {
446
+ const file = loadFile();
447
+ const id = canonicalChannelId(key);
448
+ const record = file.channels[id] ?? {};
449
+ file.channels[id] = channelRecordWithContext(record, context, provenance, base);
450
+ saveFile(file);
451
+ }
452
+ function channelRecordWithContext(record, context, provenance, base) {
453
+ const { unreconciledBalanceAt: previousStamp, ...kept } = record;
454
+ const stamp = provenance === "unreconciled" ? Date.now() : provenance === "unchanged" ? previousStamp : void 0;
455
+ const stored = readAcknowledgedCumulativeAmount(record);
456
+ const acknowledgedCumulativeAmount = base === "advance" ? highestCumulativeAmount(stored, context.chargedCumulativeAmount) : stored;
457
+ return {
458
+ ...kept,
459
+ context,
460
+ ...acknowledgedCumulativeAmount !== void 0 ? { acknowledgedCumulativeAmount } : {},
461
+ ...stamp !== void 0 ? { unreconciledBalanceAt: stamp } : {}
462
+ };
463
+ }
464
+ function allChannelMetadata() {
465
+ return Object.values(loadFile().channels).filter((record) => record !== void 0).map((record) => record.metadata).filter((entry) => entry !== void 0);
466
+ }
467
+ function pendingKey(entry) {
468
+ return `${normalizeEndpoint(entry.endpoint)}\0${entry.callerPubkey}\0${entry.creditId}`;
469
+ }
470
+ function sameFundingCredit(entry, target, credits) {
471
+ return entry.callerPubkey === target.callerPubkey && entry.creditId === target.creditId && fundingReachableAt(entry, normalizeEndpoint(target.endpoint), credits);
472
+ }
473
+ function loadFundingFile() {
474
+ const snapshot = collapseFundingFile();
475
+ if (!snapshot.changed) return snapshot;
476
+ const persisted = tryWithX402ChannelLockSync(() => {
477
+ const current = collapseFundingFile();
478
+ if (current.changed) saveFile(current.file);
479
+ return current;
480
+ });
481
+ return persisted ?? snapshot;
482
+ }
483
+ function collapseFundingFile() {
484
+ const file = loadFile();
485
+ const credits = listCredits();
486
+ const pending = dedupeX402FundingCredits(file.pending, credits);
487
+ const exact = dedupeFundingCredits(file.exact, credits);
488
+ if (pending.changed) file.pending = pending.entries;
489
+ if (exact.changed) file.exact = exact.entries;
490
+ return { file, credits, changed: pending.changed || exact.changed };
491
+ }
492
+ function dedupeX402FundingCredits(entries, credits) {
493
+ const kept = [];
494
+ let changed = false;
495
+ for (const entry of entries) {
496
+ if (x402PendingRetired(entry)) {
497
+ kept.push(entry);
498
+ continue;
499
+ }
500
+ const index = kept.findIndex(
501
+ (candidate) => !x402PendingRetired(candidate) && sameFundingCredit(candidate, entry, credits)
502
+ );
503
+ if (index === -1) {
504
+ kept.push(entry);
505
+ continue;
506
+ }
507
+ const seen = kept[index];
508
+ const seenAt = Number.isFinite(seen.requestedAt) ? seen.requestedAt : 0;
509
+ const entryAt = Number.isFinite(entry.requestedAt) ? entry.requestedAt : 0;
510
+ const sameOrigin = normalizeEndpoint(seen.endpoint) === normalizeEndpoint(entry.endpoint);
511
+ if (entryAt > seenAt) {
512
+ kept[index] = entry;
513
+ if (!sameOrigin) kept.push({ ...seen, supersededAt: entry.requestedAt });
514
+ } else if (!sameOrigin) {
515
+ kept.push({ ...entry, supersededAt: seen.requestedAt });
516
+ }
517
+ changed = true;
518
+ }
519
+ return { entries: kept, changed };
520
+ }
521
+ function dedupeFundingCredits(entries, credits) {
522
+ const kept = [];
523
+ let changed = false;
524
+ for (const entry of entries) {
525
+ const index = kept.findIndex((candidate) => sameFundingCredit(candidate, entry, credits));
526
+ if (index === -1) {
527
+ kept.push(entry);
528
+ continue;
529
+ }
530
+ const seenAt = Number.isFinite(kept[index].requestedAt) ? kept[index].requestedAt : 0;
531
+ const entryAt = Number.isFinite(entry.requestedAt) ? entry.requestedAt : 0;
532
+ if (entryAt > seenAt) kept[index] = entry;
533
+ changed = true;
534
+ }
535
+ return { entries: kept, changed };
536
+ }
537
+ function sameJournalEntry(entry, target) {
538
+ return pendingKey(entry) === pendingKey(target) && entry.fundId === target.fundId && x402PendingRetired(entry) === x402PendingRetired(target);
539
+ }
540
+ function fromSynchronousStore(operation) {
541
+ try {
542
+ return Promise.resolve(operation());
543
+ } catch (error) {
544
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
545
+ }
546
+ }
547
+ function highestCumulativeAmount(previous, next) {
548
+ if (previous === void 0) return next;
549
+ if (next === void 0) return previous;
550
+ return BigInt(next) > BigInt(previous) ? next : previous;
551
+ }
552
+ function readAcknowledgedCumulativeAmount(record) {
553
+ return record?.acknowledgedCumulativeAmount ?? record?.context?.chargedCumulativeAmount;
554
+ }
555
+ function canonicalChannelId(value) {
556
+ return value.toLowerCase();
557
+ }
558
+ function normalizeEndpoint(endpoint) {
559
+ try {
560
+ return new URL(endpoint).origin;
561
+ } catch {
562
+ return endpoint.replace(/\/+$/, "");
563
+ }
564
+ }
565
+ function tryWithX402ChannelLockSync(operation) {
566
+ if (x402LockOwner.getStore()?.active) return operation();
567
+ ensureX402ChannelFile();
568
+ let release;
569
+ try {
570
+ release = lockfile.lockSync(X402_CHANNELS_FILE, { realpath: false });
571
+ } catch (error) {
572
+ if (error.code === "ELOCKED") return void 0;
573
+ throw error;
574
+ }
575
+ const owner = { active: true };
576
+ try {
577
+ return x402LockOwner.run(owner, operation);
578
+ } finally {
579
+ owner.active = false;
580
+ release();
581
+ }
582
+ }
583
+ function ensureX402ChannelFile() {
584
+ ensureConfigDir();
585
+ if (existsSync(X402_CHANNELS_FILE)) return;
586
+ try {
587
+ writeFileSync(X402_CHANNELS_FILE, `${JSON.stringify(EMPTY, null, 2)}
588
+ `, {
589
+ mode: 384,
590
+ flag: "wx"
591
+ });
592
+ } catch {
593
+ }
594
+ }
595
+ function loadFile() {
596
+ if (!existsSync(X402_CHANNELS_FILE)) return { ...EMPTY, channels: {}, pending: [], exact: [] };
597
+ try {
598
+ const raw = JSON.parse(readFileSync(X402_CHANNELS_FILE, "utf8"));
599
+ if (raw.version !== 1 || !raw.channels || typeof raw.channels !== "object" || Array.isArray(raw.channels) || !Array.isArray(raw.pending)) {
600
+ throw new Error("unsupported x402 channel store shape");
601
+ }
602
+ return {
603
+ version: 1,
604
+ channels: raw.channels,
605
+ pending: raw.pending,
606
+ // Tolerated rather than required, unlike its two siblings: every file
607
+ // written before internal-review lacks the key, and failing closed on it would
608
+ // read as a corrupt journal on a store that is perfectly intact.
609
+ exact: Array.isArray(raw.exact) ? raw.exact : []
610
+ };
611
+ } catch (cause) {
612
+ throw new DvmError(
613
+ "x402_channel_store_invalid",
614
+ "The saved x402 channel journal cannot be read safely.",
615
+ `Do not delete ${X402_CHANNELS_FILE}; restore or repair it before signing another channel voucher.`,
616
+ { cause: cause instanceof Error ? cause.message : String(cause) }
617
+ );
618
+ }
619
+ }
620
+ function saveFile(file) {
621
+ ensureConfigDir();
622
+ const tmp = `${X402_CHANNELS_FILE}.${String(process.pid)}.tmp`;
623
+ writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
624
+ `, { mode: 384 });
625
+ renameSync(tmp, X402_CHANNELS_FILE);
626
+ }
627
+
628
+ // src/lib/x402/wallet.ts
629
+ function loadX402Wallet() {
630
+ const config = loadConfig();
631
+ const x402 = config?.x402;
632
+ const privateKey = x402?.privateKey;
633
+ if (!privateKey || !isValidEvmPrivateKey(privateKey)) return null;
634
+ return {
635
+ privateKey,
636
+ address: "",
637
+ network: x402.network ?? X402_DEFAULT_NETWORK
638
+ };
639
+ }
640
+ async function ensureAddress(wallet) {
641
+ if (!isValidEvmPrivateKey(wallet.privateKey)) throw invalidX402PrivateKey();
642
+ if (wallet.address) return wallet;
643
+ const { privateKeyToAccount } = await import("viem/accounts");
644
+ const account = privateKeyToAccount(wallet.privateKey);
645
+ return { ...wallet, address: account.address };
646
+ }
647
+ async function connectedX402Payer() {
648
+ const wallet = loadX402Wallet();
649
+ if (!wallet) return null;
650
+ return (await ensureAddress(wallet)).address;
651
+ }
652
+ async function generateX402PrivateKey() {
653
+ const { generatePrivateKey } = await import("viem/accounts");
654
+ return generatePrivateKey();
655
+ }
656
+ async function repinX402WalletNetwork(network) {
657
+ assertSupportedX402Network(network);
658
+ return withX402ChannelLock(async () => {
659
+ const { previous } = await updateConfig({
660
+ reason: "x402_wallet_repin",
661
+ patch: (current) => {
662
+ if (!current) throw new DvmError("no_config", "No config found.", "Run: dvm init");
663
+ const privateKey = current.x402?.privateKey;
664
+ if (privateKey === void 0) throw x402WalletMissingError();
665
+ if (!isValidEvmPrivateKey(privateKey)) throw invalidX402PrivateKey();
666
+ return { set: { x402: { privateKey, network } } };
667
+ }
668
+ });
669
+ const stored = previous?.x402;
670
+ if (stored === void 0) throw x402WalletMissingError();
671
+ if (!isValidEvmPrivateKey(stored.privateKey)) throw invalidX402PrivateKey();
672
+ const { privateKeyToAccount } = await import("viem/accounts");
673
+ return {
674
+ address: privateKeyToAccount(stored.privateKey).address,
675
+ previousNetwork: stored.network ?? X402_DEFAULT_NETWORK,
676
+ network
677
+ };
678
+ });
679
+ }
680
+ async function saveX402Wallet(privateKey, network) {
681
+ if (network !== void 0) assertSupportedX402Network(network);
682
+ if (!isValidEvmPrivateKey(privateKey)) throw invalidX402PrivateKey();
683
+ const { privateKeyToAccount } = await import("viem/accounts");
684
+ const account = privateKeyToAccount(privateKey);
685
+ let resolved;
686
+ await updateConfig({
687
+ reason: "x402_wallet_connect",
688
+ patch: (current) => {
689
+ resolved = network ?? current?.x402?.network;
690
+ return {
691
+ set: { x402: resolved === void 0 ? { privateKey } : { privateKey, network: resolved } }
692
+ };
693
+ }
694
+ });
695
+ return { address: account.address, network: resolved ?? X402_DEFAULT_NETWORK };
696
+ }
697
+ async function removeX402Wallet() {
698
+ await updateConfig({
699
+ reason: "x402_wallet_disconnect",
700
+ authorize: authorizeProtectedRemoval("x402"),
701
+ patch: () => ({ unset: ["x402"] })
702
+ });
703
+ }
704
+ function invalidX402PrivateKey() {
705
+ return new DvmError(
706
+ "invalid_key",
707
+ "Invalid secp256k1 private key.",
708
+ "Reconnect with `dvm wallet x402-connect --key-file <path>`, set DVM_X402_KEY, or pipe a valid 0x-prefixed 32-byte secp256k1 private key through stdin."
709
+ );
710
+ }
711
+ function x402WalletMissingError() {
712
+ return new DvmError(
713
+ "x402_wallet_missing",
714
+ "No x402 wallet is connected.",
715
+ "Run 'dvm wallet x402-connect' to create one, or add --key-file <path> to import a key you already hold."
716
+ );
717
+ }
718
+ async function getX402Balance(wallet) {
719
+ const w = await ensureAddress(wallet);
720
+ const spec = x402NetworkByCaip2(w.network);
721
+ if (!spec) {
722
+ throw new DvmError(
723
+ "x402_unsupported_network",
724
+ `The x402 wallet is configured for ${w.network}, a chain this rail can't settle on.`,
725
+ `${x402SupportedNetworksHint()} Re-pin the connected wallet without handling its key: 'dvm wallet x402-network ${X402_DEFAULT_NETWORK}'.`
726
+ );
727
+ }
728
+ const chains = await import("viem/chains");
729
+ const { createPublicClient, http, erc20Abi } = await import("viem");
730
+ const chain = chains[spec.viemChain];
731
+ const { url: rpcUrl, source: rpcSource } = resolveRpcOverride(
732
+ process.env.DVM_X402_RPC_URL,
733
+ chain?.rpcUrls.default.http[0]
734
+ );
735
+ try {
736
+ const transportOptions = resolveRpcHttpTransportOptions(
737
+ rpcSource,
738
+ process.env.DVM_X402_RPC_HEADERS_JSON
739
+ );
740
+ const client = createPublicClient({
741
+ chain,
742
+ transport: transportOptions ? http(rpcUrl, transportOptions) : http(rpcUrl)
743
+ });
744
+ const balance = await client.readContract({
745
+ address: spec.usdc,
746
+ abi: erc20Abi,
747
+ functionName: "balanceOf",
748
+ args: [w.address]
749
+ });
750
+ return {
751
+ address: w.address,
752
+ usdc: Number(balance) / 1e6,
753
+ network: w.network,
754
+ tokenAddress: spec.usdc,
755
+ // internal-review: redacted here rather than at a display surface, so the clear
756
+ // endpoint never leaves this function on the success path either.
757
+ rpcUrlRedacted: rpcUrl ? redactUrl(rpcUrl) : void 0,
758
+ rpcSource
759
+ };
760
+ } catch (err) {
761
+ const cause = redactUrlsInText(err instanceof Error ? err.message : String(err));
762
+ throw new DvmError(
763
+ "x402_rpc_error",
764
+ `x402 USDC balance lookup on ${spec.label} failed: ${cause}`,
765
+ // A caller who set the override doesn't need to be told to set it. Which
766
+ // knob is theirs to turn depends on whose endpoint this was.
767
+ rpcSource === "override" ? `DVM_X402_RPC_URL sent this read to ${redactUrl(rpcUrl)}. Correct it, or unset it to fall back to ${spec.label}'s bundled endpoint.` : `Verify connectivity to ${spec.label}'s bundled endpoint${rpcUrl ? ` (${redactUrl(rpcUrl)})` : ""}, or send the read somewhere else with DVM_X402_RPC_URL.`
768
+ );
769
+ }
770
+ }
771
+
772
+ // src/lib/x402/client.ts
773
+ async function tryX402Payment(request, wallet) {
774
+ const selection = selectPaymentRequirement(request);
775
+ let networkSlug;
776
+ let networkCaip2;
777
+ let chainId;
778
+ let usdc;
779
+ try {
780
+ networkCaip2 = x402NetworkToCaip2(selection.network);
781
+ const mapped = x402NetworkByCaip2(networkCaip2);
782
+ networkSlug = mapped?.slug ?? selection.network;
783
+ chainId = chainIdFromCaip2(networkCaip2);
784
+ usdc = usdcContractFor(networkSlug);
785
+ } catch (err) {
786
+ throw new DvmError(
787
+ "x402_build_failed",
788
+ `Could not resolve x402 network from '${selection.network}': ${err instanceof Error ? err.message : String(err)}`,
789
+ "Check that the DVM's advertised x402.network is a CAIP-2 chain id (e.g. 'eip155:8453' for Base)."
790
+ );
791
+ }
792
+ if (!usdc) return null;
793
+ const rate = await priceableRate(request.btcRate);
794
+ if (rate === null) return null;
795
+ const localUsdcMicro = BigInt(msatsToUsdc(request.amountMsats, rate));
796
+ const requiredUsdcMicro = resolveRequiredUsdcMicro(selection.amount, localUsdcMicro);
797
+ const signedAmountMsats = usdcToMsats(Number(requiredUsdcMicro) / 1e6, rate);
798
+ try {
799
+ const w = await ensureAddress(wallet);
800
+ const validAfter = 0n;
801
+ const validBefore = BigInt(
802
+ Math.floor(Date.now() / 1e3) + X402_EXACT_AUTHORIZATION_TTL_SECONDS
803
+ );
804
+ const nonce = request.nonce ?? `0x${randomBytes(32).toString("hex")}`;
805
+ const domainName = selectedDomainField(selection.accepted?.extra?.name, "name") ?? usdcDomainNameFor(networkSlug) ?? "USD Coin";
806
+ const domainVersion = selectedDomainField(selection.accepted?.extra?.version, "version") ?? usdcDomainVersionFor(networkSlug) ?? "2";
807
+ const { signTypedData } = await import("viem/accounts");
808
+ const signature = await signTypedData({
809
+ privateKey: w.privateKey,
810
+ domain: {
811
+ name: domainName,
812
+ version: domainVersion,
813
+ chainId,
814
+ verifyingContract: usdc
815
+ },
816
+ types: {
817
+ TransferWithAuthorization: [
818
+ { name: "from", type: "address" },
819
+ { name: "to", type: "address" },
820
+ { name: "value", type: "uint256" },
821
+ { name: "validAfter", type: "uint256" },
822
+ { name: "validBefore", type: "uint256" },
823
+ { name: "nonce", type: "bytes32" }
824
+ ]
825
+ },
826
+ primaryType: "TransferWithAuthorization",
827
+ message: {
828
+ from: w.address,
829
+ to: selection.payTo,
830
+ value: requiredUsdcMicro,
831
+ validAfter,
832
+ validBefore,
833
+ nonce
834
+ }
835
+ });
836
+ const exactPayload = {
837
+ signature,
838
+ payload: {
839
+ authorization: {
840
+ from: w.address,
841
+ to: selection.payTo,
842
+ value: requiredUsdcMicro.toString(),
843
+ validAfter: validAfter.toString(),
844
+ validBefore: validBefore.toString(),
845
+ nonce
846
+ }
847
+ }
848
+ };
849
+ const payload = selection.version === X402_V1_VERSION ? {
850
+ x402Version: X402_V1_VERSION,
851
+ scheme: "exact",
852
+ network: networkSlug,
853
+ payload: { signature: exactPayload.signature, ...exactPayload.payload }
854
+ } : {
855
+ x402Version: X402_VERSION,
856
+ ...selection.resource ? { resource: selection.resource } : {},
857
+ accepted: selection.accepted ?? buildPaymentRequirements({
858
+ x402Config: {
859
+ payTo: selection.payTo,
860
+ network: networkCaip2,
861
+ ...selection.asset ? { asset: selection.asset } : {}
862
+ },
863
+ requiredUsdcMicro,
864
+ resource: selection.resource?.url ?? "",
865
+ version: X402_VERSION
866
+ }),
867
+ payload: { signature: exactPayload.signature, ...exactPayload.payload },
868
+ extensions: selection.extensions ?? {}
869
+ };
870
+ return {
871
+ paymentHeader: encodePayment(payload),
872
+ x402Version: selection.version,
873
+ amountUsdcMicro: requiredUsdcMicro.toString(),
874
+ amountMsats: signedAmountMsats,
875
+ btcRate: rate
876
+ };
877
+ } catch (err) {
878
+ throw new DvmError(
879
+ "x402_build_failed",
880
+ `Failed to build x402 payment: ${err instanceof Error ? err.message : String(err)}`,
881
+ "Check the configured x402 private key and RPC reachability for the target chain."
882
+ );
883
+ }
884
+ }
885
+ function isSignableExactRequirement(requirement) {
886
+ const transferMethod = requirement.extra?.assetTransferMethod;
887
+ return requirement.scheme === "exact" && (transferMethod === void 0 || transferMethod === "eip3009");
888
+ }
889
+ function selectPaymentRequirement(request) {
890
+ const challenge = request.paymentRequired;
891
+ if (challenge?.x402Version === X402_VERSION) {
892
+ const accepted = challenge.accepts.find(isSignableExactRequirement);
893
+ if (!accepted) {
894
+ throw invalidChallenge("The DVM's v2 x402 challenge has no exact EIP-3009 requirement.");
895
+ }
896
+ return {
897
+ version: X402_VERSION,
898
+ network: accepted.network,
899
+ amount: accepted.amount,
900
+ payTo: accepted.payTo,
901
+ asset: accepted.asset,
902
+ accepted,
903
+ resource: challenge.resource,
904
+ extensions: challenge.extensions
905
+ };
906
+ }
907
+ if (challenge?.x402Version === X402_V1_VERSION) {
908
+ const accepted = challenge.accepts.find((candidate) => candidate.scheme === "exact");
909
+ if (!accepted) throw invalidChallenge("The DVM's v1 x402 challenge has no exact requirement.");
910
+ return {
911
+ version: X402_V1_VERSION,
912
+ network: accepted.network,
913
+ amount: accepted.maxAmountRequired,
914
+ payTo: accepted.payTo,
915
+ asset: accepted.asset
916
+ };
917
+ }
918
+ return {
919
+ version: request.x402Version ?? X402_V1_VERSION,
920
+ network: request.network,
921
+ amount: request.requiredUsdcMicro,
922
+ payTo: request.payTo,
923
+ asset: request.asset,
924
+ resource: request.resource
925
+ };
926
+ }
927
+ function invalidChallenge(message) {
928
+ return new DvmError(
929
+ "x402_build_failed",
930
+ message,
931
+ "Ask the DVM operator to advertise an exact x402 payment requirement."
932
+ );
933
+ }
934
+ function selectedDomainField(value, field) {
935
+ if (value === void 0 || value === null) return void 0;
936
+ if (typeof value === "string" && value.length > 0) return value;
937
+ throw invalidChallenge(`The DVM advertised an invalid x402 ${field}.`);
938
+ }
939
+ function resolveRequiredUsdcMicro(advertised, localUsdcMicro) {
940
+ if (advertised === void 0) return localUsdcMicro;
941
+ if (!/^[1-9]\d*$/.test(advertised)) {
942
+ throw new DvmError(
943
+ "x402_amount_invalid",
944
+ `The DVM advertised an invalid x402 maxAmountRequired: '${advertised}'.`,
945
+ "Do not sign this payment. Ask the DVM operator to return a positive integer number of USDC micro-units."
946
+ );
947
+ }
948
+ const required = BigInt(advertised);
949
+ if (withinAdvertisedTolerance(required, localUsdcMicro)) return required;
950
+ const advertisedUsd = Number(required) / 1e6;
951
+ const localUsd = Number(localUsdcMicro) / 1e6;
952
+ throw new DvmError(
953
+ "x402_amount_exceeds_limit",
954
+ `The DVM's x402 charge ($${formatUsd(advertisedUsd)} USDC) is more than 10% above the caller's local price ($${formatUsd(localUsd)} USDC).`,
955
+ "The payment was not signed. Refresh the quote or ask the DVM operator to check its BTC/USD rate.",
956
+ {
957
+ advertised_usdc_microunits: required.toString(),
958
+ local_usdc_microunits: localUsdcMicro.toString(),
959
+ max_usdc_microunits: maxAdvertisedMicro(localUsdcMicro).toString(),
960
+ anchor: costAnchor(advertisedUsd),
961
+ display: `Payment refused: $${formatUsd(advertisedUsd)} USDC exceeds the 10% safety margin over the caller's $${formatUsd(localUsd)} USDC estimate.`
962
+ }
963
+ );
964
+ }
965
+ async function priceableRate(btcRate) {
966
+ if (btcRate && btcRate > 0) return btcRate;
967
+ try {
968
+ return (await resolveBtcUsdRate("payment")).rate;
969
+ } catch (err) {
970
+ if (err instanceof DvmError && err.code === "rate_unavailable") {
971
+ info(
972
+ "Skipping the x402 rail: it prices in USDC and the BTC/USD rate is unavailable (no cached rate on this machine). Trying the remaining rails."
973
+ );
974
+ return null;
975
+ }
976
+ throw err;
977
+ }
978
+ }
979
+
980
+ // src/lib/x402/verify.ts
981
+ async function verifyX402Payment(paymentHeader, config, requiredUsdcMicro, resource, btcUsdRate, exactVersions = { v1: true, v2: true }) {
982
+ return callerLoggers.x402.span(
983
+ "x402.verify_payment",
984
+ {
985
+ resource,
986
+ required_usdc_micro: Number(requiredUsdcMicro),
987
+ facilitator_url: config.facilitator
988
+ },
989
+ async () => {
990
+ let payload;
991
+ try {
992
+ payload = decodePayment(paymentHeader);
993
+ } catch {
994
+ return { verified: false, amountMsats: 0, invalidReason: "decode_failed" };
995
+ }
996
+ if (payload.x402Version === 1 && !exactVersions.v1 || payload.x402Version === 2 && !exactVersions.v2) {
997
+ return { verified: false, amountMsats: 0, invalidReason: "unsupported_version" };
998
+ }
999
+ const requirementOpts = { x402Config: config, requiredUsdcMicro, resource };
1000
+ const requirements = payload.x402Version === 1 ? buildPaymentRequirements({ ...requirementOpts, version: 1 }) : buildPaymentRequirements({ ...requirementOpts, version: 2 });
1001
+ const createAuthHeaders = createX402FacilitatorAuthHeaders(config);
1002
+ const verifyRes = await verifyWithFacilitator(
1003
+ payload,
1004
+ requirements,
1005
+ config,
1006
+ createAuthHeaders
1007
+ );
1008
+ if (!verifyRes.isValid) {
1009
+ return {
1010
+ verified: false,
1011
+ amountMsats: 0,
1012
+ invalidReason: verifyRes.invalidReason ?? "verify_failed"
1013
+ };
1014
+ }
1015
+ const settleRes = await settleWithFacilitator(
1016
+ payload,
1017
+ requirements,
1018
+ config,
1019
+ createAuthHeaders
1020
+ );
1021
+ if (!settleRes.success) {
1022
+ return {
1023
+ verified: false,
1024
+ amountMsats: 0,
1025
+ invalidReason: settleRes.errorReason ?? "settle_failed"
1026
+ };
1027
+ }
1028
+ const amountUsdcMicro = Number.parseInt(exactEvmAuthorization(payload).value, 10);
1029
+ const amountMsats = usdcToMsats(amountUsdcMicro / 1e6, btcUsdRate);
1030
+ callerLoggers.x402.info("x402.verify_payment.result", {
1031
+ verified: true,
1032
+ tx_hash: settleRes.transaction,
1033
+ amount_msats: amountMsats
1034
+ });
1035
+ return {
1036
+ verified: true,
1037
+ amountMsats,
1038
+ amountUsdcMicro,
1039
+ txHash: settleRes.transaction,
1040
+ settleResponseHeader: encodeSettleResponseHeader(settleRes)
1041
+ };
1042
+ }
1043
+ );
1044
+ }
1045
+
1046
+ export {
1047
+ resolveX402Asset,
1048
+ x402WrongAssetHint,
1049
+ resolveBtcUsdRate,
1050
+ tryResolveBtcUsdRate,
1051
+ X402_EXACT_AUTHORIZATION_TTL_SECONDS,
1052
+ X402_REFUSAL_TRANSACTION_DISCOVERY_WINDOW_MS,
1053
+ x402ExactFundingDead,
1054
+ recordPendingX402ExactFunding,
1055
+ loadResumableX402ExactFunding,
1056
+ listResumableX402ExactFundings,
1057
+ clearPendingX402ExactFunding,
1058
+ X402_PENDING_STALE_MS,
1059
+ x402PendingStale,
1060
+ x402PendingRetired,
1061
+ x402PendingBlockExpiresAt,
1062
+ x402PendingDepositExpiresAt,
1063
+ x402PendingUnsettled,
1064
+ x402PendingFundingJson,
1065
+ withX402ChannelLock,
1066
+ DvmX402ChannelStorage,
1067
+ recordX402ChannelMetadata,
1068
+ acknowledgeX402ChannelOwner,
1069
+ listX402Channels,
1070
+ findX402Channel,
1071
+ x402ChannelBalanceUnreconciledAt,
1072
+ recordPendingX402Funding,
1073
+ loadResumableX402Funding,
1074
+ listPendingX402Fundings,
1075
+ listResumableX402Fundings,
1076
+ listPendingX402FundingsForChannel,
1077
+ retirePendingX402Funding,
1078
+ clearPendingX402Funding,
1079
+ loadX402Wallet,
1080
+ ensureAddress,
1081
+ connectedX402Payer,
1082
+ generateX402PrivateKey,
1083
+ repinX402WalletNetwork,
1084
+ saveX402Wallet,
1085
+ removeX402Wallet,
1086
+ getX402Balance,
1087
+ tryX402Payment,
1088
+ isSignableExactRequirement,
1089
+ verifyX402Payment
1090
+ };