@payai/x402-evm 2.4.0 → 2.4.2

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 (150) hide show
  1. package/dist/cjs/batch-settlement/client/file-storage.d.ts +47 -0
  2. package/dist/cjs/batch-settlement/client/file-storage.js +116 -0
  3. package/dist/cjs/batch-settlement/client/file-storage.js.map +1 -0
  4. package/dist/cjs/batch-settlement/client/index.d.ts +111 -0
  5. package/dist/cjs/batch-settlement/client/index.js +1565 -0
  6. package/dist/cjs/batch-settlement/client/index.js.map +1 -0
  7. package/dist/cjs/batch-settlement/facilitator/index.d.ts +71 -0
  8. package/dist/cjs/batch-settlement/facilitator/index.js +2032 -0
  9. package/dist/cjs/batch-settlement/facilitator/index.js.map +1 -0
  10. package/dist/cjs/batch-settlement/server/file-storage.d.ts +53 -0
  11. package/dist/cjs/batch-settlement/server/file-storage.js +181 -0
  12. package/dist/cjs/batch-settlement/server/file-storage.js.map +1 -0
  13. package/dist/cjs/batch-settlement/server/index.d.ts +491 -0
  14. package/dist/cjs/batch-settlement/server/index.js +1960 -0
  15. package/dist/cjs/batch-settlement/server/index.js.map +1 -0
  16. package/dist/cjs/batch-settlement/server/redis-storage.d.ts +87 -0
  17. package/dist/cjs/batch-settlement/server/redis-storage.js +181 -0
  18. package/dist/cjs/batch-settlement/server/redis-storage.js.map +1 -0
  19. package/dist/cjs/exact/client/index.d.ts +6 -4
  20. package/dist/cjs/exact/client/index.js +7 -5
  21. package/dist/cjs/exact/client/index.js.map +1 -1
  22. package/dist/cjs/exact/facilitator/index.d.ts +16 -9
  23. package/dist/cjs/exact/facilitator/index.js +56 -9
  24. package/dist/cjs/exact/facilitator/index.js.map +1 -1
  25. package/dist/cjs/exact/server/index.d.ts +0 -8
  26. package/dist/cjs/exact/server/index.js +53 -19
  27. package/dist/cjs/exact/server/index.js.map +1 -1
  28. package/dist/cjs/exact/v1/client/index.d.ts +2 -1
  29. package/dist/cjs/exact/v1/client/index.js.map +1 -1
  30. package/dist/cjs/exact/v1/facilitator/index.d.ts +11 -5
  31. package/dist/cjs/exact/v1/facilitator/index.js +16 -2
  32. package/dist/cjs/exact/v1/facilitator/index.js.map +1 -1
  33. package/dist/cjs/index.d.ts +113 -7
  34. package/dist/cjs/index.js +1353 -5
  35. package/dist/cjs/index.js.map +1 -1
  36. package/dist/{esm/permit2-CyZxwngN.d.mts → cjs/permit2-DhJRUcgY.d.ts} +1 -13
  37. package/dist/cjs/rpc-DULZzRne.d.ts +13 -0
  38. package/dist/cjs/scheme-CvkPJXBD.d.ts +307 -0
  39. package/dist/{esm/scheme-DCR7hsa3.d.mts → cjs/scheme-DTQFE9xp.d.ts} +2 -2
  40. package/dist/{esm/signer-D912R4mq.d.mts → cjs/signer-tYS6Y46X.d.ts} +3 -0
  41. package/dist/cjs/storage-6W5MO46W.d.ts +50 -0
  42. package/dist/cjs/storage-Bl6aD0Xg.d.ts +81 -0
  43. package/dist/cjs/types-CF8P2-NM.d.ts +180 -0
  44. package/dist/cjs/upto/client/index.d.ts +5 -3
  45. package/dist/cjs/upto/client/index.js +7 -5
  46. package/dist/cjs/upto/client/index.js.map +1 -1
  47. package/dist/cjs/upto/facilitator/index.d.ts +2 -1
  48. package/dist/cjs/upto/facilitator/index.js +2 -1
  49. package/dist/cjs/upto/facilitator/index.js.map +1 -1
  50. package/dist/cjs/upto/server/index.d.ts +0 -8
  51. package/dist/cjs/upto/server/index.js +51 -19
  52. package/dist/cjs/upto/server/index.js.map +1 -1
  53. package/dist/cjs/v1/index.d.ts +2 -1
  54. package/dist/cjs/v1/index.js.map +1 -1
  55. package/dist/esm/batch-settlement/client/file-storage.d.mts +47 -0
  56. package/dist/esm/batch-settlement/client/file-storage.mjs +63 -0
  57. package/dist/esm/batch-settlement/client/file-storage.mjs.map +1 -0
  58. package/dist/esm/batch-settlement/client/index.d.mts +111 -0
  59. package/dist/esm/batch-settlement/client/index.mjs +59 -0
  60. package/dist/esm/batch-settlement/client/index.mjs.map +1 -0
  61. package/dist/esm/batch-settlement/facilitator/index.d.mts +71 -0
  62. package/dist/esm/batch-settlement/facilitator/index.mjs +1235 -0
  63. package/dist/esm/batch-settlement/facilitator/index.mjs.map +1 -0
  64. package/dist/esm/batch-settlement/server/file-storage.d.mts +53 -0
  65. package/dist/esm/batch-settlement/server/file-storage.mjs +128 -0
  66. package/dist/esm/batch-settlement/server/file-storage.mjs.map +1 -0
  67. package/dist/esm/batch-settlement/server/index.d.mts +491 -0
  68. package/dist/esm/batch-settlement/server/index.mjs +1645 -0
  69. package/dist/esm/batch-settlement/server/index.mjs.map +1 -0
  70. package/dist/esm/batch-settlement/server/redis-storage.d.mts +87 -0
  71. package/dist/esm/batch-settlement/server/redis-storage.mjs +156 -0
  72. package/dist/esm/batch-settlement/server/redis-storage.mjs.map +1 -0
  73. package/dist/esm/chunk-2EUQTNJO.mjs +38 -0
  74. package/dist/esm/chunk-2EUQTNJO.mjs.map +1 -0
  75. package/dist/esm/chunk-53USC5VE.mjs +47 -0
  76. package/dist/esm/chunk-53USC5VE.mjs.map +1 -0
  77. package/dist/esm/{chunk-GJ57SZGI.mjs → chunk-6WQOGWBE.mjs} +7 -5
  78. package/dist/esm/{chunk-GJ57SZGI.mjs.map → chunk-6WQOGWBE.mjs.map} +1 -1
  79. package/dist/esm/{chunk-NSFLAANF.mjs → chunk-BTYNCDNS.mjs} +51 -2
  80. package/dist/esm/chunk-BTYNCDNS.mjs.map +1 -0
  81. package/dist/esm/{chunk-RYT6M3PA.mjs → chunk-CSQS7ZON.mjs} +47 -7
  82. package/dist/esm/chunk-CSQS7ZON.mjs.map +1 -0
  83. package/dist/esm/chunk-GD4MKCN7.mjs +57 -0
  84. package/dist/esm/chunk-GD4MKCN7.mjs.map +1 -0
  85. package/dist/esm/chunk-HYABYUBD.mjs +432 -0
  86. package/dist/esm/chunk-HYABYUBD.mjs.map +1 -0
  87. package/dist/esm/chunk-IN5YIT5C.mjs +159 -0
  88. package/dist/esm/chunk-IN5YIT5C.mjs.map +1 -0
  89. package/dist/esm/{chunk-JII456TS.mjs → chunk-JK7SLLF7.mjs} +1 -1
  90. package/dist/esm/chunk-JK7SLLF7.mjs.map +1 -0
  91. package/dist/esm/{chunk-C4ZQMS77.mjs → chunk-MACPBXCT.mjs} +2 -216
  92. package/dist/esm/chunk-MACPBXCT.mjs.map +1 -0
  93. package/dist/esm/chunk-NKYVYGRA.mjs +911 -0
  94. package/dist/esm/chunk-NKYVYGRA.mjs.map +1 -0
  95. package/dist/esm/{chunk-D6RXZXOS.mjs → chunk-R7I3RZFF.mjs} +10 -6
  96. package/dist/esm/{chunk-D6RXZXOS.mjs.map → chunk-R7I3RZFF.mjs.map} +1 -1
  97. package/dist/esm/{chunk-CRT6YNY5.mjs → chunk-RWLVVO3B.mjs} +21 -61
  98. package/dist/esm/chunk-RWLVVO3B.mjs.map +1 -0
  99. package/dist/esm/chunk-TGFAVNUD.mjs +111 -0
  100. package/dist/esm/chunk-TGFAVNUD.mjs.map +1 -0
  101. package/dist/esm/chunk-TW7Z65AO.mjs +34 -0
  102. package/dist/esm/chunk-TW7Z65AO.mjs.map +1 -0
  103. package/dist/esm/chunk-U4HCGTLU.mjs +35 -0
  104. package/dist/esm/chunk-U4HCGTLU.mjs.map +1 -0
  105. package/dist/esm/chunk-VS3RYAYE.mjs +80 -0
  106. package/dist/esm/chunk-VS3RYAYE.mjs.map +1 -0
  107. package/dist/esm/chunk-W6ON4LG2.mjs +39 -0
  108. package/dist/esm/chunk-W6ON4LG2.mjs.map +1 -0
  109. package/dist/esm/{chunk-WKBC5YMI.mjs → chunk-YMQCTKDU.mjs} +23 -55
  110. package/dist/esm/chunk-YMQCTKDU.mjs.map +1 -0
  111. package/dist/esm/exact/client/index.d.mts +6 -4
  112. package/dist/esm/exact/client/index.mjs +10 -5
  113. package/dist/esm/exact/facilitator/index.d.mts +16 -9
  114. package/dist/esm/exact/facilitator/index.mjs +39 -16
  115. package/dist/esm/exact/facilitator/index.mjs.map +1 -1
  116. package/dist/esm/exact/server/index.d.mts +0 -8
  117. package/dist/esm/exact/server/index.mjs +3 -19
  118. package/dist/esm/exact/server/index.mjs.map +1 -1
  119. package/dist/esm/exact/v1/client/index.d.mts +2 -1
  120. package/dist/esm/exact/v1/client/index.mjs +5 -2
  121. package/dist/esm/exact/v1/facilitator/index.d.mts +11 -5
  122. package/dist/esm/exact/v1/facilitator/index.mjs +5 -2
  123. package/dist/esm/index.d.mts +113 -7
  124. package/dist/esm/index.mjs +53 -7
  125. package/dist/esm/index.mjs.map +1 -1
  126. package/dist/esm/permit2-DhJRUcgY.d.mts +729 -0
  127. package/dist/esm/rpc-DULZzRne.d.mts +13 -0
  128. package/dist/esm/scheme-DtbSS4Fk.d.mts +307 -0
  129. package/dist/esm/scheme-gtqAIYPJ.d.mts +47 -0
  130. package/dist/esm/signer-tYS6Y46X.d.mts +170 -0
  131. package/dist/esm/storage-6W5MO46W.d.mts +50 -0
  132. package/dist/esm/storage-sZ1CDS4P.d.mts +81 -0
  133. package/dist/esm/types-CF8P2-NM.d.mts +180 -0
  134. package/dist/esm/upto/client/index.d.mts +5 -3
  135. package/dist/esm/upto/client/index.mjs +9 -4
  136. package/dist/esm/upto/facilitator/index.d.mts +2 -1
  137. package/dist/esm/upto/facilitator/index.mjs +17 -9
  138. package/dist/esm/upto/facilitator/index.mjs.map +1 -1
  139. package/dist/esm/upto/server/index.d.mts +0 -8
  140. package/dist/esm/upto/server/index.mjs +3 -19
  141. package/dist/esm/upto/server/index.mjs.map +1 -1
  142. package/dist/esm/v1/index.d.mts +2 -1
  143. package/dist/esm/v1/index.mjs +5 -2
  144. package/package.json +5 -5
  145. package/dist/esm/chunk-C4ZQMS77.mjs.map +0 -1
  146. package/dist/esm/chunk-CRT6YNY5.mjs.map +0 -1
  147. package/dist/esm/chunk-JII456TS.mjs.map +0 -1
  148. package/dist/esm/chunk-NSFLAANF.mjs.map +0 -1
  149. package/dist/esm/chunk-RYT6M3PA.mjs.map +0 -1
  150. package/dist/esm/chunk-WKBC5YMI.mjs.map +0 -1
@@ -0,0 +1,1960 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/batch-settlement/server/index.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ BatchSettlementChannelManager: () => BatchSettlementChannelManager,
24
+ BatchSettlementEvmScheme: () => BatchSettlementEvmScheme,
25
+ InMemoryChannelStorage: () => InMemoryChannelStorage
26
+ });
27
+ module.exports = __toCommonJS(server_exports);
28
+
29
+ // src/batch-settlement/server/scheme.ts
30
+ var import_utils12 = require("@payai/x402/utils");
31
+ var import_viem7 = require("viem");
32
+
33
+ // src/batch-settlement/utils.ts
34
+ var import_viem3 = require("viem");
35
+
36
+ // src/batch-settlement/constants.ts
37
+ var import_viem = require("viem");
38
+ var BATCH_SETTLEMENT_SCHEME = "batch-settlement";
39
+ var BATCH_SETTLEMENT_ADDRESS = "0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003";
40
+ var MIN_WITHDRAW_DELAY = 900;
41
+ var MAX_WITHDRAW_DELAY = 2592e3;
42
+ var BATCH_SETTLEMENT_DOMAIN = {
43
+ name: "x402 Batch Settlement",
44
+ version: "1"
45
+ };
46
+ var CHANNEL_CONFIG_TYPEHASH = (0, import_viem.keccak256)(
47
+ (0, import_viem.toBytes)(
48
+ "ChannelConfig(address payer,address payerAuthorizer,address receiver,address receiverAuthorizer,address token,uint40 withdrawDelay,bytes32 salt)"
49
+ )
50
+ );
51
+ var channelConfigTypes = {
52
+ ChannelConfig: [
53
+ { name: "payer", type: "address" },
54
+ { name: "payerAuthorizer", type: "address" },
55
+ { name: "receiver", type: "address" },
56
+ { name: "receiverAuthorizer", type: "address" },
57
+ { name: "token", type: "address" },
58
+ { name: "withdrawDelay", type: "uint40" },
59
+ { name: "salt", type: "bytes32" }
60
+ ]
61
+ };
62
+ var voucherTypes = {
63
+ Voucher: [
64
+ { name: "channelId", type: "bytes32" },
65
+ { name: "maxClaimableAmount", type: "uint128" }
66
+ ]
67
+ };
68
+ var refundTypes = {
69
+ Refund: [
70
+ { name: "channelId", type: "bytes32" },
71
+ { name: "nonce", type: "uint256" },
72
+ { name: "amount", type: "uint128" }
73
+ ]
74
+ };
75
+ var claimBatchTypes = {
76
+ ClaimBatch: [{ name: "claims", type: "ClaimEntry[]" }],
77
+ ClaimEntry: [
78
+ { name: "channelId", type: "bytes32" },
79
+ { name: "maxClaimableAmount", type: "uint128" },
80
+ { name: "totalClaimed", type: "uint128" }
81
+ ]
82
+ };
83
+
84
+ // src/utils.ts
85
+ var import_viem2 = require("viem");
86
+ function getEvmChainId(network) {
87
+ if (network.startsWith("eip155:")) {
88
+ const idStr = network.split(":")[1];
89
+ const chainId = parseInt(idStr, 10);
90
+ if (isNaN(chainId)) {
91
+ throw new Error(`Invalid CAIP-2 chain ID: ${network}`);
92
+ }
93
+ return chainId;
94
+ }
95
+ throw new Error(`Unsupported network format: ${network} (expected eip155:CHAIN_ID)`);
96
+ }
97
+ function getCrypto() {
98
+ const cryptoObj = globalThis.crypto;
99
+ if (!cryptoObj) {
100
+ throw new Error("Crypto API not available");
101
+ }
102
+ return cryptoObj;
103
+ }
104
+ function createNonce() {
105
+ return (0, import_viem2.toHex)(getCrypto().getRandomValues(new Uint8Array(32)));
106
+ }
107
+
108
+ // src/batch-settlement/utils.ts
109
+ function computeChannelId(config, networkOrChainId) {
110
+ const chainId = typeof networkOrChainId === "number" ? networkOrChainId : getEvmChainId(networkOrChainId);
111
+ return (0, import_viem3.hashTypedData)({
112
+ domain: getBatchSettlementEip712Domain(chainId),
113
+ types: channelConfigTypes,
114
+ primaryType: "ChannelConfig",
115
+ message: {
116
+ payer: config.payer,
117
+ payerAuthorizer: config.payerAuthorizer,
118
+ receiver: config.receiver,
119
+ receiverAuthorizer: config.receiverAuthorizer,
120
+ token: config.token,
121
+ withdrawDelay: config.withdrawDelay,
122
+ salt: config.salt
123
+ }
124
+ });
125
+ }
126
+ function getBatchSettlementEip712Domain(chainId) {
127
+ return {
128
+ ...BATCH_SETTLEMENT_DOMAIN,
129
+ chainId,
130
+ verifyingContract: (0, import_viem3.getAddress)(BATCH_SETTLEMENT_ADDRESS)
131
+ };
132
+ }
133
+
134
+ // src/batch-settlement/authorizerSigner.ts
135
+ async function signClaimBatch(signer, claims, network) {
136
+ const chainId = getEvmChainId(network);
137
+ const claimEntries = claims.map((c) => ({
138
+ channelId: computeChannelId(c.voucher.channel, chainId),
139
+ maxClaimableAmount: BigInt(c.voucher.maxClaimableAmount),
140
+ totalClaimed: BigInt(c.totalClaimed)
141
+ }));
142
+ return signer.signTypedData({
143
+ domain: getBatchSettlementEip712Domain(chainId),
144
+ types: claimBatchTypes,
145
+ primaryType: "ClaimBatch",
146
+ message: {
147
+ claims: claimEntries
148
+ }
149
+ });
150
+ }
151
+ async function signRefund(signer, channelId, amount, nonce, network) {
152
+ const chainId = getEvmChainId(network);
153
+ return signer.signTypedData({
154
+ domain: getBatchSettlementEip712Domain(chainId),
155
+ types: refundTypes,
156
+ primaryType: "Refund",
157
+ message: {
158
+ channelId,
159
+ nonce: BigInt(nonce),
160
+ amount: BigInt(amount)
161
+ }
162
+ });
163
+ }
164
+
165
+ // src/batch-settlement/server/channelManager.ts
166
+ var AUTO_JOB_PRIORITY = ["claim", "settle", "refund"];
167
+ function formatFacilitatorFailure(operation, response) {
168
+ return `${operation} failed: ${response.errorReason ?? "unknown"} \u2014 ${response.errorMessage ?? ""}`;
169
+ }
170
+ function hasLivePendingRequest(channel, now = Date.now()) {
171
+ return channel.pendingRequest !== void 0 && channel.pendingRequest.expiresAt > now;
172
+ }
173
+ var BatchSettlementChannelManager = class {
174
+ /**
175
+ * Creates a new channel manager.
176
+ *
177
+ * @param config - Manager configuration: scheme, facilitator, receiver, token, network.
178
+ */
179
+ constructor(config) {
180
+ this.timers = {};
181
+ this.lastClaimTime = 0;
182
+ this.lastSettleTime = 0;
183
+ this.pendingSettle = false;
184
+ this.running = false;
185
+ this.pendingJobs = /* @__PURE__ */ new Set();
186
+ this.drainingJobs = false;
187
+ this.autoSettleConfig = {};
188
+ this.scheme = config.scheme;
189
+ this.facilitator = config.facilitator;
190
+ this.receiver = config.receiver;
191
+ this.token = config.token;
192
+ this.network = config.network;
193
+ }
194
+ /**
195
+ * Collects claimable vouchers and submits them in batches to the facilitator via `claim()`.
196
+ *
197
+ * @param opts - Optional claim execution and target selection options.
198
+ * @param opts.maxClaimsPerBatch - Max vouchers per facilitator `claim` batch.
199
+ * @param opts.idleSecs - When set, only include channels idle for at least this many seconds.
200
+ * @param opts.selectClaimChannels - Optional selector for choosing channels before claimability checks.
201
+ * @returns Array of claim results (one per batch).
202
+ */
203
+ async claim(opts) {
204
+ const channels = await this.selectClaimTargets(opts);
205
+ return this.claimFromChannels(channels, {
206
+ maxClaimsPerBatch: opts?.maxClaimsPerBatch ?? 100,
207
+ ...opts?.idleSecs !== void 0 ? { idleSecs: opts.idleSecs } : {}
208
+ });
209
+ }
210
+ /**
211
+ * Transfers claimed (but unsettled) funds to the receiver by calling `settle(receiver, token)`.
212
+ *
213
+ * @returns Settle result with the transaction hash.
214
+ */
215
+ async settle() {
216
+ const paymentPayload = this.buildSettlePaymentPayload();
217
+ const requirements = this.buildPaymentRequirements();
218
+ const response = await this.facilitator.settle(paymentPayload, requirements);
219
+ if (!response.success) {
220
+ throw new Error(formatFacilitatorFailure("Settle", response));
221
+ }
222
+ this.pendingSettle = false;
223
+ return { transaction: response.transaction };
224
+ }
225
+ /**
226
+ * Convenience: claims all eligible vouchers then settles in one call.
227
+ *
228
+ * @param opts - Optional claim execution and target selection options.
229
+ * @param opts.maxClaimsPerBatch - Max vouchers per claim batch before settling.
230
+ * @param opts.idleSecs - When set, only include channels idle for at least this many seconds.
231
+ * @param opts.selectClaimChannels - Optional selector for choosing channels before claimability checks.
232
+ * @returns Combined claim and settle results.
233
+ */
234
+ async claimAndSettle(opts) {
235
+ const claims = await this.claim(opts);
236
+ let settleResult;
237
+ if (claims.length > 0) {
238
+ settleResult = await this.settle();
239
+ }
240
+ return { claims, settle: settleResult };
241
+ }
242
+ /**
243
+ * Initiates cooperative refunds for one or more channels.
244
+ *
245
+ * @param channelIds - Specific channels to refund; defaults to all sessions.
246
+ * @returns One result per successfully refunded channel.
247
+ */
248
+ async refund(channelIds) {
249
+ const storage = this.scheme.getStorage();
250
+ const channels = await storage.list();
251
+ const now = Date.now();
252
+ const targets = (channelIds ? channels.filter(
253
+ (s) => channelIds.some((id) => id.toLowerCase() === s.channelId.toLowerCase())
254
+ ) : channels).filter((channel) => !hasLivePendingRequest(channel, now));
255
+ if (targets.length === 0) {
256
+ return [];
257
+ }
258
+ return this.refundChannels(targets);
259
+ }
260
+ /**
261
+ * Refunds idle channels with non-zero balances.
262
+ *
263
+ * @param opts - Idle refund options.
264
+ * @param opts.idleSecs - Minimum seconds since the last request.
265
+ * @returns One result per successfully refunded channel.
266
+ */
267
+ async refundIdleChannels(opts) {
268
+ const channels = await this.getIdleChannelsForRefund(opts.idleSecs);
269
+ return this.refundChannels(channels);
270
+ }
271
+ /**
272
+ * Collects vouchers that are eligible for onchain claiming.
273
+ *
274
+ * A voucher is claimable when its `chargedCumulativeAmount` exceeds what has already
275
+ * been claimed onchain. An optional idle filter skips sessions that received a
276
+ * request within the last `idleSecs` seconds.
277
+ *
278
+ * @param opts - Optional filtering: `idleSecs` to only return idle channels.
279
+ * @param opts.idleSecs - Minimum seconds since last request for a channel to be included.
280
+ * @returns Array of {@link BatchSettlementVoucherClaim} entries for batch submission.
281
+ */
282
+ async getClaimableVouchers(opts) {
283
+ const channels = await this.scheme.getStorage().list();
284
+ return this.getClaimableVouchersFromChannels(channels, opts);
285
+ }
286
+ /**
287
+ * Returns channels that have a pending payer-initiated withdrawal.
288
+ *
289
+ * @returns All stored channel records with `withdrawRequestedAt` set.
290
+ */
291
+ async getWithdrawalPendingSessions() {
292
+ const channels = await this.scheme.getStorage().list();
293
+ return channels.filter((s) => s.withdrawRequestedAt > 0);
294
+ }
295
+ /**
296
+ * Starts auto-settlement jobs for configured claim, settle, and refund intervals.
297
+ *
298
+ * @param config - Auto-settlement policy configuration.
299
+ */
300
+ start(config = {}) {
301
+ if (this.running) {
302
+ return;
303
+ }
304
+ const now = Date.now();
305
+ this.lastClaimTime = now;
306
+ this.lastSettleTime = now;
307
+ this.running = true;
308
+ this.autoSettleConfig = config;
309
+ this.startAutoTimer("claim", config.claimIntervalSecs);
310
+ this.startAutoTimer("settle", config.settleIntervalSecs);
311
+ this.startAutoTimer("refund", config.refundIntervalSecs);
312
+ }
313
+ /**
314
+ * Stops the auto-settlement loop.
315
+ *
316
+ * @param opts - Stop options.
317
+ * @param opts.flush - When true, run `claimAndSettle` before stopping.
318
+ * @returns Resolves when the loop is stopped (and flush work completes, if requested).
319
+ */
320
+ async stop(opts) {
321
+ this.running = false;
322
+ for (const timer of Object.values(this.timers)) {
323
+ clearInterval(timer);
324
+ }
325
+ this.timers = {};
326
+ this.pendingJobs.clear();
327
+ if (opts?.flush) {
328
+ await this.claimAndSettle({
329
+ maxClaimsPerBatch: this.autoSettleConfig.maxClaimsPerBatch,
330
+ selectClaimChannels: this.autoSettleConfig.selectClaimChannels
331
+ });
332
+ }
333
+ }
334
+ /**
335
+ * Refunds a single channel and removes it from storage after success.
336
+ *
337
+ * @param target - Channel to refund.
338
+ * @returns Successful refund transaction.
339
+ */
340
+ async refundChannel(target) {
341
+ const authorizerSigner = this.scheme.getReceiverAuthorizerSigner();
342
+ const claims = this.buildRefundClaims(target);
343
+ const refundAmount = (BigInt(target.balance) - BigInt(target.chargedCumulativeAmount)).toString();
344
+ const nonce = String(target.refundNonce ?? 0);
345
+ const refundAuthorizerSignature = authorizerSigner ? await signRefund(
346
+ authorizerSigner,
347
+ target.channelId,
348
+ refundAmount,
349
+ nonce,
350
+ this.network
351
+ ) : void 0;
352
+ const claimAuthorizerSignature = authorizerSigner && claims.length > 0 ? await signClaimBatch(authorizerSigner, claims, this.network) : void 0;
353
+ const paymentPayload = {
354
+ x402Version: 2,
355
+ accepted: this.buildPaymentRequirements(),
356
+ payload: {
357
+ type: "refund",
358
+ channelConfig: target.channelConfig,
359
+ voucher: {
360
+ channelId: target.channelId,
361
+ maxClaimableAmount: target.signedMaxClaimable,
362
+ signature: target.signature
363
+ },
364
+ amount: refundAmount,
365
+ refundNonce: nonce,
366
+ claims,
367
+ ...refundAuthorizerSignature ? { refundAuthorizerSignature } : {},
368
+ ...claimAuthorizerSignature ? { claimAuthorizerSignature } : {}
369
+ }
370
+ };
371
+ const response = await this.facilitator.settle(paymentPayload, this.buildPaymentRequirements());
372
+ if (!response.success) {
373
+ throw new Error(formatFacilitatorFailure("Refund", response));
374
+ }
375
+ await this.scheme.getStorage().updateChannel(
376
+ target.channelId,
377
+ (current) => current && !hasLivePendingRequest(current) ? void 0 : current
378
+ );
379
+ return {
380
+ channel: target.channelId,
381
+ transaction: response.transaction
382
+ };
383
+ }
384
+ /**
385
+ * Starts a recurring timer for one auto job.
386
+ *
387
+ * @param job - Job to enqueue when the interval fires.
388
+ * @param intervalSecs - Timer interval in seconds.
389
+ */
390
+ startAutoTimer(job, intervalSecs) {
391
+ if (intervalSecs === void 0) {
392
+ return;
393
+ }
394
+ this.timers[job] = setInterval(() => {
395
+ this.enqueueJob(job);
396
+ }, intervalSecs * 1e3);
397
+ }
398
+ /**
399
+ * Adds an auto job to the coalescing queue.
400
+ *
401
+ * @param job - Job to run.
402
+ */
403
+ enqueueJob(job) {
404
+ if (!this.running) {
405
+ return;
406
+ }
407
+ this.pendingJobs.add(job);
408
+ if (!this.drainingJobs) {
409
+ void this.drainJobs();
410
+ }
411
+ }
412
+ /**
413
+ * Drains queued auto jobs in priority order.
414
+ */
415
+ async drainJobs() {
416
+ if (this.drainingJobs) {
417
+ return;
418
+ }
419
+ this.drainingJobs = true;
420
+ try {
421
+ while (this.running && this.pendingJobs.size > 0) {
422
+ const job = this.nextPendingJob();
423
+ if (!job) {
424
+ return;
425
+ }
426
+ this.pendingJobs.delete(job);
427
+ await this.runAutoJob(job);
428
+ }
429
+ } finally {
430
+ this.drainingJobs = false;
431
+ }
432
+ }
433
+ /**
434
+ * Returns the highest-priority queued auto job.
435
+ *
436
+ * @returns Next job to run.
437
+ */
438
+ nextPendingJob() {
439
+ return AUTO_JOB_PRIORITY.find((job) => this.pendingJobs.has(job));
440
+ }
441
+ /**
442
+ * Runs one auto job.
443
+ *
444
+ * @param job - Job to run.
445
+ */
446
+ async runAutoJob(job) {
447
+ switch (job) {
448
+ case "claim":
449
+ await this.runClaimJob();
450
+ return;
451
+ case "settle":
452
+ await this.runSettleJob();
453
+ return;
454
+ case "refund":
455
+ await this.runRefundJob();
456
+ return;
457
+ }
458
+ }
459
+ /**
460
+ * Runs the claim auto job.
461
+ */
462
+ async runClaimJob() {
463
+ const cfg = this.autoSettleConfig;
464
+ try {
465
+ const targets = await this.selectClaimTargets({
466
+ selectClaimChannels: cfg.selectClaimChannels
467
+ });
468
+ const results = await this.claimFromChannels(targets, {
469
+ maxClaimsPerBatch: cfg.maxClaimsPerBatch ?? 100
470
+ });
471
+ this.lastClaimTime = Date.now();
472
+ for (const result of results) {
473
+ cfg.onClaim?.(result);
474
+ }
475
+ } catch (err) {
476
+ cfg.onError?.(err);
477
+ }
478
+ }
479
+ /**
480
+ * Runs the settlement auto job.
481
+ */
482
+ async runSettleJob() {
483
+ const cfg = this.autoSettleConfig;
484
+ const context = this.buildAutoSettlementContext(Date.now());
485
+ if (!context.pendingSettle) {
486
+ return;
487
+ }
488
+ try {
489
+ if (cfg.shouldSettle && !await cfg.shouldSettle(context)) {
490
+ return;
491
+ }
492
+ const result = await this.settle();
493
+ this.lastSettleTime = Date.now();
494
+ cfg.onSettle?.(result);
495
+ } catch (err) {
496
+ cfg.onError?.(err);
497
+ }
498
+ }
499
+ /**
500
+ * Runs the refund auto job.
501
+ */
502
+ async runRefundJob() {
503
+ const cfg = this.autoSettleConfig;
504
+ if (!cfg.selectRefundChannels) {
505
+ return;
506
+ }
507
+ try {
508
+ const context = this.buildAutoSettlementContext(Date.now());
509
+ const channels = await this.scheme.getStorage().list();
510
+ const targets = await cfg.selectRefundChannels(channels, context);
511
+ for (const result of await this.refundChannels(targets)) {
512
+ cfg.onRefund?.(result);
513
+ }
514
+ } catch (err) {
515
+ cfg.onError?.(err);
516
+ }
517
+ }
518
+ /**
519
+ * Claims vouchers from a provided channel snapshot.
520
+ *
521
+ * @param channels - Channels to inspect for claimable vouchers.
522
+ * @param opts - Claim batching and filtering options.
523
+ * @param opts.maxClaimsPerBatch - Max vouchers per facilitator claim transaction.
524
+ * @param opts.idleSecs - Optional idle filter.
525
+ * @returns Claim results, one per submitted batch.
526
+ */
527
+ async claimFromChannels(channels, opts) {
528
+ const allClaims = this.getClaimableVouchersFromChannels(
529
+ channels,
530
+ opts.idleSecs !== void 0 ? { idleSecs: opts.idleSecs } : void 0
531
+ );
532
+ if (allClaims.length === 0) {
533
+ return [];
534
+ }
535
+ const results = [];
536
+ for (let i = 0; i < allClaims.length; i += opts.maxClaimsPerBatch) {
537
+ const batch = allClaims.slice(i, i + opts.maxClaimsPerBatch);
538
+ const result = await this.submitClaim(batch);
539
+ results.push(result);
540
+ await this.updateClaimedSessions(batch);
541
+ }
542
+ if (results.length > 0) {
543
+ this.pendingSettle = true;
544
+ }
545
+ return results;
546
+ }
547
+ /**
548
+ * Loads stored channels and applies the configured claim selector, if any.
549
+ *
550
+ * @param opts - Claim options containing an optional target selector.
551
+ * @returns The channel snapshot that should be inspected for claimable vouchers.
552
+ */
553
+ async selectClaimTargets(opts) {
554
+ const channels = await this.scheme.getStorage().list();
555
+ if (!opts?.selectClaimChannels) {
556
+ return channels;
557
+ }
558
+ const context = this.buildAutoSettlementContext(Date.now());
559
+ return opts.selectClaimChannels(channels, context);
560
+ }
561
+ /**
562
+ * Refunds each eligible channel independently.
563
+ *
564
+ * @param channels - Channels to refund.
565
+ * @returns Successful refund results.
566
+ */
567
+ async refundChannels(channels) {
568
+ const results = [];
569
+ for (const channel of channels) {
570
+ if (hasLivePendingRequest(channel)) {
571
+ continue;
572
+ }
573
+ results.push(await this.refundChannel(channel));
574
+ }
575
+ return results;
576
+ }
577
+ /**
578
+ * Builds an outstanding voucher claim for a refund payload.
579
+ *
580
+ * @param channel - Channel being refunded.
581
+ * @returns Claim payloads needed before refunding unclaimed balance.
582
+ */
583
+ buildRefundClaims(channel) {
584
+ if (BigInt(channel.chargedCumulativeAmount) <= BigInt(channel.totalClaimed)) {
585
+ return [];
586
+ }
587
+ return [
588
+ {
589
+ voucher: {
590
+ channel: channel.channelConfig,
591
+ maxClaimableAmount: channel.signedMaxClaimable
592
+ },
593
+ signature: channel.signature,
594
+ totalClaimed: channel.chargedCumulativeAmount
595
+ }
596
+ ];
597
+ }
598
+ /**
599
+ * Builds the policy context passed to interval hooks.
600
+ *
601
+ * @param now - Current wall-clock time in milliseconds.
602
+ * @returns Auto-settlement policy context.
603
+ */
604
+ buildAutoSettlementContext(now) {
605
+ return {
606
+ now,
607
+ lastClaimTime: this.lastClaimTime,
608
+ lastSettleTime: this.lastSettleTime,
609
+ pendingSettle: this.pendingSettle
610
+ };
611
+ }
612
+ /**
613
+ * Collects claimable vouchers from a provided channel snapshot.
614
+ *
615
+ * @param channels - Channels to inspect.
616
+ * @param opts - Optional idle filter.
617
+ * @param opts.idleSecs - Minimum seconds since last request.
618
+ * @returns Claimable voucher payloads.
619
+ */
620
+ getClaimableVouchersFromChannels(channels, opts) {
621
+ const now = Date.now();
622
+ const claims = [];
623
+ for (const c of channels) {
624
+ if (BigInt(c.chargedCumulativeAmount) <= BigInt(c.totalClaimed)) {
625
+ continue;
626
+ }
627
+ if (opts?.idleSecs !== void 0) {
628
+ const idleMs = now - c.lastRequestTimestamp;
629
+ if (idleMs < opts.idleSecs * 1e3) {
630
+ continue;
631
+ }
632
+ }
633
+ claims.push({
634
+ voucher: {
635
+ channel: c.channelConfig,
636
+ maxClaimableAmount: c.signedMaxClaimable
637
+ },
638
+ signature: c.signature,
639
+ totalClaimed: c.chargedCumulativeAmount
640
+ });
641
+ }
642
+ return claims;
643
+ }
644
+ /**
645
+ * Filters idle channels that can be cooperatively refunded.
646
+ *
647
+ * @param channels - Channels to inspect.
648
+ * @param idleSecs - Minimum seconds since the last request.
649
+ * @returns Idle refundable channels.
650
+ */
651
+ getIdleChannelsForRefundFromChannels(channels, idleSecs) {
652
+ const now = Date.now();
653
+ const idleMs = idleSecs * 1e3;
654
+ return channels.filter((c) => {
655
+ if (BigInt(c.balance) === 0n) return false;
656
+ if (hasLivePendingRequest(c, now)) return false;
657
+ return now - c.lastRequestTimestamp >= idleMs;
658
+ });
659
+ }
660
+ /**
661
+ * Returns channels that have been idle longer than `idleSecs` and still have
662
+ * a non-zero balance (candidates for cooperative refund).
663
+ *
664
+ * @param idleSecs - Minimum seconds since last request for a session to count as idle.
665
+ * @returns Channels meeting the idle and balance criteria.
666
+ */
667
+ async getIdleChannelsForRefund(idleSecs) {
668
+ const channels = await this.scheme.getStorage().list();
669
+ return this.getIdleChannelsForRefundFromChannels(channels, idleSecs);
670
+ }
671
+ /**
672
+ * Submits a batch of voucher claims to the facilitator.
673
+ *
674
+ * @param claims - Voucher claims to send in one `type: "claim"` payload.
675
+ * @returns Per-batch claim summary (count and transaction hash).
676
+ */
677
+ async submitClaim(claims) {
678
+ const authorizerSigner = this.scheme.getReceiverAuthorizerSigner();
679
+ const claimAuthorizerSignature = authorizerSigner ? await signClaimBatch(authorizerSigner, claims, this.network) : void 0;
680
+ const paymentPayload = {
681
+ x402Version: 2,
682
+ accepted: this.buildPaymentRequirements(),
683
+ payload: {
684
+ type: "claim",
685
+ claims,
686
+ ...claimAuthorizerSignature ? { claimAuthorizerSignature } : {}
687
+ }
688
+ };
689
+ const response = await this.facilitator.settle(
690
+ paymentPayload,
691
+ this.buildPaymentRequirements()
692
+ );
693
+ if (!response.success) {
694
+ throw new Error(formatFacilitatorFailure("Claim", response));
695
+ }
696
+ return { vouchers: claims.length, transaction: response.transaction };
697
+ }
698
+ /**
699
+ * Builds a settlement payment payload for `settle(receiver, token)`.
700
+ *
701
+ * @returns Payload with `type: "settle"` and receiver/token fields.
702
+ */
703
+ buildSettlePaymentPayload() {
704
+ return {
705
+ x402Version: 2,
706
+ accepted: this.buildPaymentRequirements(),
707
+ payload: {
708
+ type: "settle",
709
+ receiver: this.receiver,
710
+ token: this.token
711
+ }
712
+ };
713
+ }
714
+ /**
715
+ * Builds a minimal {@link PaymentRequirements} for channel manager operations.
716
+ *
717
+ * @returns Requirements describing batched operations for this manager.
718
+ */
719
+ buildPaymentRequirements() {
720
+ return {
721
+ scheme: BATCH_SETTLEMENT_SCHEME,
722
+ network: this.network,
723
+ asset: this.token,
724
+ amount: "0",
725
+ payTo: this.receiver,
726
+ maxTimeoutSeconds: 0,
727
+ extra: {}
728
+ };
729
+ }
730
+ /**
731
+ * Updates session records after a successful claim submission so that
732
+ * `getClaimableVouchers` no longer returns already-claimed vouchers.
733
+ *
734
+ * @param claims - Voucher claims that were included in the submitted settlement transaction.
735
+ */
736
+ async updateClaimedSessions(claims) {
737
+ const storage = this.scheme.getStorage();
738
+ for (const claim of claims) {
739
+ const channelId = computeChannelId(claim.voucher.channel, this.network);
740
+ const channel = await storage.get(channelId);
741
+ if (!channel) {
742
+ continue;
743
+ }
744
+ const claimedAmount = BigInt(claim.totalClaimed);
745
+ if (claimedAmount <= BigInt(channel.totalClaimed)) {
746
+ continue;
747
+ }
748
+ await storage.updateChannel(channelId, (current) => {
749
+ if (!current || claimedAmount <= BigInt(current.totalClaimed)) {
750
+ return current;
751
+ }
752
+ return {
753
+ ...current,
754
+ totalClaimed: claimedAmount.toString()
755
+ };
756
+ });
757
+ }
758
+ }
759
+ };
760
+
761
+ // src/shared/defaultAssets.ts
762
+ var DEFAULT_STABLECOINS = {
763
+ "eip155:8453": {
764
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
765
+ name: "USD Coin",
766
+ version: "2",
767
+ decimals: 6
768
+ },
769
+ // Base mainnet USDC
770
+ "eip155:84532": {
771
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
772
+ name: "USDC",
773
+ version: "2",
774
+ decimals: 6
775
+ },
776
+ // Base Sepolia USDC
777
+ "eip155:4326": {
778
+ address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7",
779
+ name: "MegaUSD",
780
+ version: "1",
781
+ decimals: 18,
782
+ assetTransferMethod: "permit2",
783
+ supportsEip2612: true
784
+ },
785
+ // MegaETH mainnet MegaUSD (no EIP-3009, supports EIP-2612)
786
+ "eip155:143": {
787
+ address: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
788
+ name: "USD Coin",
789
+ version: "2",
790
+ decimals: 6
791
+ },
792
+ // Monad mainnet USDC
793
+ "eip155:988": {
794
+ address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
795
+ name: "USDT0",
796
+ version: "1",
797
+ decimals: 6
798
+ },
799
+ // Stable mainnet USDT0
800
+ "eip155:2201": {
801
+ address: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
802
+ name: "USDT0",
803
+ version: "1",
804
+ decimals: 6
805
+ },
806
+ // Stable testnet USDT0
807
+ "eip155:137": {
808
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
809
+ name: "USD Coin",
810
+ version: "2",
811
+ decimals: 6
812
+ },
813
+ // Polygon mainnet USDC
814
+ "eip155:42161": {
815
+ address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
816
+ name: "USD Coin",
817
+ version: "2",
818
+ decimals: 6
819
+ },
820
+ // Arbitrum One USDC
821
+ "eip155:421614": {
822
+ address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
823
+ name: "USD Coin",
824
+ version: "2",
825
+ decimals: 6
826
+ },
827
+ // Arbitrum Sepolia USDC
828
+ "eip155:31611": {
829
+ address: "0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503",
830
+ name: "Mezo USD",
831
+ version: "1",
832
+ decimals: 18,
833
+ assetTransferMethod: "permit2",
834
+ supportsEip2612: true
835
+ },
836
+ // Mezo Testnet mUSD (no EIP-3009, supports EIP-2612)
837
+ "eip155:723487": {
838
+ address: "0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb",
839
+ name: "Stable Coin",
840
+ version: "1",
841
+ decimals: 6,
842
+ assetTransferMethod: "permit2",
843
+ supportsEip2612: true
844
+ },
845
+ // Radius Network SBC (no EIP-3009, supports EIP-2612)
846
+ "eip155:72344": {
847
+ address: "0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb",
848
+ name: "Stable Coin",
849
+ version: "1",
850
+ decimals: 6,
851
+ assetTransferMethod: "permit2",
852
+ supportsEip2612: true
853
+ },
854
+ // Radius Testnet SBC (no EIP-3009, supports EIP-2612)
855
+ "eip155:36900": {
856
+ address: "0x9cb8142aEBBcdc60AF7c97Af897A67A8f3CA71C2",
857
+ name: "USDC.e",
858
+ version: "2",
859
+ decimals: 6
860
+ },
861
+ // ADI Chain USDC.e (EIP-3009 supported)
862
+ "eip155:190415": {
863
+ address: "0x401eCb1D350407f13ba348573E5630B83638E30D",
864
+ name: "Bridged USDC",
865
+ version: "2",
866
+ decimals: 6
867
+ },
868
+ // HPP mainnet USDC.e
869
+ "eip155:181228": {
870
+ address: "0x401eCb1D350407f13ba348573E5630B83638E30D",
871
+ name: "Bridged USDC",
872
+ version: "2",
873
+ decimals: 6
874
+ }
875
+ // HPP Sepolia USDC.e
876
+ };
877
+ function getDefaultAsset(network) {
878
+ const info = DEFAULT_STABLECOINS[network];
879
+ if (!info) {
880
+ throw new Error(`No default asset configured for network ${network}`);
881
+ }
882
+ return info;
883
+ }
884
+
885
+ // src/batch-settlement/server/storage.ts
886
+ var InMemoryChannelStorage = class {
887
+ constructor() {
888
+ this.channels = /* @__PURE__ */ new Map();
889
+ this.channelLocks = /* @__PURE__ */ new Map();
890
+ }
891
+ /**
892
+ * Returns the channel record for a channel, if present.
893
+ *
894
+ * @param channelId - The channel identifier.
895
+ * @returns The channel record or undefined when not found.
896
+ */
897
+ async get(channelId) {
898
+ return this.channels.get(channelId.toLowerCase());
899
+ }
900
+ /**
901
+ * Lists all stored channel records.
902
+ *
903
+ * @returns All channel records in storage.
904
+ */
905
+ async list() {
906
+ return [...this.channels.values()];
907
+ }
908
+ /**
909
+ * Atomically inspects and mutates a channel record while holding a per-channel lock.
910
+ *
911
+ * @param channelId - The channel identifier.
912
+ * @param update - Mutation callback. Return `undefined` to delete, or `current` to leave unchanged.
913
+ * @returns The final stored channel and whether storage updated, stayed unchanged, or deleted.
914
+ */
915
+ async updateChannel(channelId, update) {
916
+ const key = channelId.toLowerCase();
917
+ return this.withChannelLock(key, async () => {
918
+ const current = this.channels.get(key);
919
+ const next = update(current);
920
+ if (next === current) {
921
+ return { channel: current, status: "unchanged" };
922
+ }
923
+ if (!next) {
924
+ this.channels.delete(key);
925
+ return { channel: void 0, status: current ? "deleted" : "unchanged" };
926
+ }
927
+ this.channels.set(key, next);
928
+ return { channel: next, status: "updated" };
929
+ });
930
+ }
931
+ /**
932
+ * Runs `fn` after any prior locked work for the same channel key has finished.
933
+ *
934
+ * @param key - Lowercased channel id used as the lock key.
935
+ * @param fn - Async work to run while holding the logical per-channel lock.
936
+ * @returns The resolved result of `fn`.
937
+ */
938
+ async withChannelLock(key, fn) {
939
+ const previous = this.channelLocks.get(key) ?? Promise.resolve();
940
+ let release;
941
+ const current = new Promise((resolve) => {
942
+ release = resolve;
943
+ });
944
+ const next = previous.catch(() => {
945
+ }).then(() => current);
946
+ this.channelLocks.set(key, next);
947
+ await previous.catch(() => {
948
+ });
949
+ try {
950
+ return await fn();
951
+ } finally {
952
+ release();
953
+ if (this.channelLocks.get(key) === next) {
954
+ this.channelLocks.delete(key);
955
+ }
956
+ }
957
+ }
958
+ };
959
+
960
+ // src/batch-settlement/server/verify.ts
961
+ var import_viem6 = require("viem");
962
+
963
+ // src/batch-settlement/types.ts
964
+ function isObject(payload) {
965
+ return typeof payload === "object" && payload !== null;
966
+ }
967
+ function isVoucherFields(payload) {
968
+ return isObject(payload) && "channelId" in payload && "maxClaimableAmount" in payload && "signature" in payload;
969
+ }
970
+ function isBatchSettlementDepositPayload(payload) {
971
+ return isObject(payload) && payload.type === "deposit" && "channelConfig" in payload && isVoucherFields(payload.voucher) && isObject(payload.deposit) && typeof payload.deposit.amount === "string" && isObject(payload.deposit.authorization);
972
+ }
973
+ function isBatchSettlementVoucherPayload(payload) {
974
+ return isObject(payload) && payload.type === "voucher" && "channelConfig" in payload && isVoucherFields(payload.voucher);
975
+ }
976
+ function isBatchSettlementRefundPayload(payload) {
977
+ return isObject(payload) && payload.type === "refund" && "channelConfig" in payload && isVoucherFields(payload.voucher);
978
+ }
979
+
980
+ // src/batch-settlement/facilitator/utils.ts
981
+ var import_viem5 = require("viem");
982
+
983
+ // src/multicall.ts
984
+ var import_viem4 = require("viem");
985
+
986
+ // src/batch-settlement/errors.ts
987
+ var ErrTokenMismatch = "invalid_batch_settlement_evm_token_mismatch";
988
+ var ErrInvalidVoucherSignature = "invalid_batch_settlement_evm_voucher_signature";
989
+ var ErrCumulativeExceedsBalance = "invalid_batch_settlement_evm_cumulative_exceeds_balance";
990
+ var ErrCumulativeAmountBelowClaimed = "invalid_batch_settlement_evm_cumulative_below_claimed";
991
+ var ErrWithdrawDelayOutOfRange = "invalid_batch_settlement_evm_withdraw_delay_out_of_range";
992
+ var ErrChannelIdMismatch = "invalid_batch_settlement_evm_channel_id_mismatch";
993
+ var ErrReceiverMismatch = "invalid_batch_settlement_evm_receiver_mismatch";
994
+ var ErrReceiverAuthorizerMismatch = "invalid_batch_settlement_evm_receiver_authorizer_mismatch";
995
+ var ErrWithdrawDelayMismatch = "invalid_batch_settlement_evm_withdraw_delay_mismatch";
996
+ var ErrRefundPayload = "invalid_batch_settlement_evm_refund_payload";
997
+ var ErrCumulativeAmountMismatch = "invalid_batch_settlement_evm_cumulative_amount_mismatch";
998
+ var ErrChannelBusy = "invalid_batch_settlement_evm_channel_busy";
999
+ var ErrChargeExceedsSignedCumulative = "invalid_batch_settlement_evm_charge_exceeds_signed_cumulative";
1000
+ var ErrMissingChannel = "invalid_batch_settlement_evm_missing_channel";
1001
+ var ErrRefundNoBalance = "invalid_batch_settlement_evm_refund_no_balance";
1002
+ var ErrRefundAmountInvalid = "invalid_batch_settlement_evm_refund_amount_invalid";
1003
+
1004
+ // src/batch-settlement/facilitator/utils.ts
1005
+ function validateChannelConfig(config, channelId, requirements) {
1006
+ const computedId = computeChannelId(config, requirements.network);
1007
+ if (computedId.toLowerCase() !== channelId.toLowerCase()) {
1008
+ return ErrChannelIdMismatch;
1009
+ }
1010
+ if ((0, import_viem5.getAddress)(config.receiver) !== (0, import_viem5.getAddress)(requirements.payTo)) {
1011
+ return ErrReceiverMismatch;
1012
+ }
1013
+ const extra = requirements.extra;
1014
+ const requiredReceiverAuthorizer = extra?.receiverAuthorizer;
1015
+ if (!requiredReceiverAuthorizer || (0, import_viem5.getAddress)(requiredReceiverAuthorizer) === "0x0000000000000000000000000000000000000000" || (0, import_viem5.getAddress)(config.receiverAuthorizer) !== (0, import_viem5.getAddress)(requiredReceiverAuthorizer)) {
1016
+ return ErrReceiverAuthorizerMismatch;
1017
+ }
1018
+ if ((0, import_viem5.getAddress)(config.token) !== (0, import_viem5.getAddress)(requirements.asset)) {
1019
+ return ErrTokenMismatch;
1020
+ }
1021
+ if (extra?.withdrawDelay !== void 0 && config.withdrawDelay !== Number(extra.withdrawDelay)) {
1022
+ return ErrWithdrawDelayMismatch;
1023
+ }
1024
+ if (config.withdrawDelay < MIN_WITHDRAW_DELAY || config.withdrawDelay > MAX_WITHDRAW_DELAY) {
1025
+ return ErrWithdrawDelayOutOfRange;
1026
+ }
1027
+ return void 0;
1028
+ }
1029
+
1030
+ // src/batch-settlement/server/utils.ts
1031
+ function readChannelStateExtra(extra) {
1032
+ const value = extra?.channelState;
1033
+ if (typeof value !== "object" || value === null) {
1034
+ return void 0;
1035
+ }
1036
+ return value;
1037
+ }
1038
+ function readExtraString(extra, key, fallback) {
1039
+ const value = extra?.[key];
1040
+ if (typeof value === "string") return value;
1041
+ if (typeof value === "number") return String(value);
1042
+ return fallback;
1043
+ }
1044
+ function readExtraNumber(extra, key, fallback) {
1045
+ const value = extra?.[key];
1046
+ if (typeof value === "number") return value;
1047
+ if (typeof value === "string") return parseInt(value, 10) || fallback;
1048
+ return fallback;
1049
+ }
1050
+ function parseRefundSettlementSnapshot(extra) {
1051
+ const channelState = readChannelStateExtra(extra);
1052
+ return {
1053
+ balance: parseUintStringExtra(channelState, "balance"),
1054
+ totalClaimed: parseUintStringExtra(channelState, "totalClaimed"),
1055
+ withdrawRequestedAt: parseUintNumberExtra(channelState, "withdrawRequestedAt"),
1056
+ refundNonce: parseUintNumberExtra(channelState, "refundNonce")
1057
+ };
1058
+ }
1059
+ function parseUintStringExtra(extra, key) {
1060
+ const value = extra?.[key];
1061
+ if (typeof value === "string" && /^\d+$/.test(value)) return value;
1062
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return String(value);
1063
+ throw new Error(ErrRefundPayload);
1064
+ }
1065
+ function parseUintNumberExtra(extra, key) {
1066
+ const value = extra?.[key];
1067
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
1068
+ if (typeof value === "string" && /^\d+$/.test(value)) {
1069
+ const parsed = parseInt(value, 10);
1070
+ if (!Number.isNaN(parsed)) return parsed;
1071
+ }
1072
+ throw new Error(ErrRefundPayload);
1073
+ }
1074
+
1075
+ // src/batch-settlement/server/verify.ts
1076
+ var MIN_PENDING_TTL_MS = 5e3;
1077
+ var MAX_PENDING_TTL_MS = 10 * 60 * 1e3;
1078
+ function pendingExpiresAt(maxTimeoutSeconds, now) {
1079
+ const requestedMs = Math.max(0, maxTimeoutSeconds ?? 0) * 1e3;
1080
+ const ttlMs = Math.min(MAX_PENDING_TTL_MS, Math.max(MIN_PENDING_TTL_MS, requestedMs));
1081
+ return now + ttlMs;
1082
+ }
1083
+ function isPendingLive(pending, now) {
1084
+ return pending !== void 0 && pending.expiresAt > now;
1085
+ }
1086
+ async function handleBeforeVerify(scheme, ctx) {
1087
+ const { paymentPayload, requirements } = ctx;
1088
+ const raw = paymentPayload.payload;
1089
+ const isPaidPayload = isBatchSettlementVoucherPayload(raw) || isBatchSettlementDepositPayload(raw);
1090
+ const isZeroChargePayload = isBatchSettlementRefundPayload(raw);
1091
+ if (!isPaidPayload && !isZeroChargePayload) {
1092
+ return;
1093
+ }
1094
+ const channelId = raw.voucher.channelId;
1095
+ const now = Date.now();
1096
+ const pendingId = createNonce();
1097
+ let outcome;
1098
+ await scheme.getStorage().updateChannel(channelId, (current) => {
1099
+ if (isPendingLive(current?.pendingRequest, now)) {
1100
+ outcome = { status: "busy" };
1101
+ return current;
1102
+ }
1103
+ const chargedCumulativeAmount = current?.chargedCumulativeAmount ?? inferMissingLocalChargedAmount(
1104
+ raw.voucher.maxClaimableAmount,
1105
+ requirements.amount,
1106
+ isPaidPayload
1107
+ );
1108
+ const expectedMaxClaimable = isZeroChargePayload ? BigInt(chargedCumulativeAmount) : BigInt(chargedCumulativeAmount) + BigInt(requirements.amount);
1109
+ if (BigInt(raw.voucher.maxClaimableAmount) !== expectedMaxClaimable) {
1110
+ if (current) {
1111
+ outcome = { status: "mismatch", channel: current };
1112
+ } else {
1113
+ outcome = {
1114
+ status: "mismatch",
1115
+ channel: buildProvisionalChannel(raw, chargedCumulativeAmount)
1116
+ };
1117
+ }
1118
+ return current;
1119
+ }
1120
+ const pendingRequest = {
1121
+ pendingId,
1122
+ signedMaxClaimable: raw.voucher.maxClaimableAmount,
1123
+ expiresAt: pendingExpiresAt(requirements.maxTimeoutSeconds, now)
1124
+ };
1125
+ outcome = { status: "reserved", channelSnapshot: current };
1126
+ return {
1127
+ ...current ?? buildProvisionalChannel(raw, chargedCumulativeAmount),
1128
+ pendingRequest,
1129
+ lastRequestTimestamp: now
1130
+ };
1131
+ });
1132
+ if (outcome?.status === "busy") {
1133
+ return {
1134
+ abort: true,
1135
+ reason: ErrChannelBusy,
1136
+ message: "Channel is already processing a request"
1137
+ };
1138
+ }
1139
+ if (outcome?.status === "mismatch") {
1140
+ scheme.rememberChannelSnapshot(paymentPayload, outcome.channel);
1141
+ return {
1142
+ abort: true,
1143
+ reason: ErrCumulativeAmountMismatch,
1144
+ message: "Client voucher base does not match server state"
1145
+ };
1146
+ }
1147
+ if (outcome?.status === "reserved") {
1148
+ scheme.mergeRequestContext(paymentPayload, {
1149
+ channelId,
1150
+ pendingId,
1151
+ channelSnapshot: outcome.channelSnapshot
1152
+ });
1153
+ if (isBatchSettlementVoucherPayload(raw)) {
1154
+ const localResult = await verifyVoucherLocally(
1155
+ scheme,
1156
+ raw,
1157
+ requirements,
1158
+ outcome.channelSnapshot,
1159
+ now
1160
+ );
1161
+ if (localResult) {
1162
+ scheme.mergeRequestContext(paymentPayload, { localVerify: true });
1163
+ return { skip: true, result: localResult };
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ async function handleEnrichPaymentRequiredResponse(scheme, ctx) {
1169
+ if (ctx.error !== ErrCumulativeAmountMismatch) {
1170
+ return;
1171
+ }
1172
+ const { paymentPayload } = ctx;
1173
+ if (!paymentPayload) {
1174
+ return;
1175
+ }
1176
+ const raw = paymentPayload.payload;
1177
+ if (!isBatchSettlementVoucherPayload(raw) && !isBatchSettlementDepositPayload(raw) && !isBatchSettlementRefundPayload(raw)) {
1178
+ return;
1179
+ }
1180
+ const channel = scheme.takeChannelSnapshot(paymentPayload) ?? await scheme.getStorage().get(raw.voucher.channelId);
1181
+ if (!channel) {
1182
+ return;
1183
+ }
1184
+ const accept = ctx.requirements.find(
1185
+ (req) => req.scheme === BATCH_SETTLEMENT_SCHEME && req.network === paymentPayload.accepted.network
1186
+ );
1187
+ if (!accept) {
1188
+ return;
1189
+ }
1190
+ accept.extra = {
1191
+ ...accept.extra,
1192
+ channelState: {
1193
+ channelId: channel.channelId,
1194
+ balance: channel.balance,
1195
+ totalClaimed: channel.totalClaimed,
1196
+ withdrawRequestedAt: channel.withdrawRequestedAt,
1197
+ refundNonce: String(channel.refundNonce),
1198
+ chargedCumulativeAmount: channel.chargedCumulativeAmount
1199
+ },
1200
+ voucherState: {
1201
+ signedMaxClaimable: channel.signedMaxClaimable,
1202
+ signature: channel.signature
1203
+ }
1204
+ };
1205
+ }
1206
+ async function handleAfterVerify(scheme, ctx) {
1207
+ const { paymentPayload, result } = ctx;
1208
+ if (!result.isValid || !result.payer) {
1209
+ await scheme.clearPendingRequest(paymentPayload);
1210
+ return;
1211
+ }
1212
+ const raw = paymentPayload.payload;
1213
+ let channelId;
1214
+ let signedMaxClaimable;
1215
+ let signature;
1216
+ let channelConfig;
1217
+ let isRefundVoucher = false;
1218
+ if (isBatchSettlementDepositPayload(raw)) {
1219
+ channelId = raw.voucher.channelId;
1220
+ signedMaxClaimable = raw.voucher.maxClaimableAmount;
1221
+ signature = raw.voucher.signature;
1222
+ channelConfig = raw.channelConfig;
1223
+ } else if (isBatchSettlementVoucherPayload(raw)) {
1224
+ channelId = raw.voucher.channelId;
1225
+ signedMaxClaimable = raw.voucher.maxClaimableAmount;
1226
+ signature = raw.voucher.signature;
1227
+ channelConfig = raw.channelConfig;
1228
+ } else if (isBatchSettlementRefundPayload(raw)) {
1229
+ channelId = raw.voucher.channelId;
1230
+ signedMaxClaimable = raw.voucher.maxClaimableAmount;
1231
+ signature = raw.voucher.signature;
1232
+ channelConfig = raw.channelConfig;
1233
+ isRefundVoucher = true;
1234
+ } else {
1235
+ return;
1236
+ }
1237
+ const ex = result.extra ?? {};
1238
+ const balance = readExtraString(ex, "balance", "0");
1239
+ const totalClaimed = readExtraString(ex, "totalClaimed", "0");
1240
+ const withdrawRequestedAt = readExtraNumber(ex, "withdrawRequestedAt", 0);
1241
+ const refundNonce = readExtraNumber(ex, "refundNonce", 0);
1242
+ const now = Date.now();
1243
+ const storage = scheme.getStorage();
1244
+ const requestContext = scheme.readRequestContext(paymentPayload);
1245
+ if (!requestContext?.pendingId) {
1246
+ return;
1247
+ }
1248
+ if (requestContext.localVerify && isBatchSettlementVoucherPayload(raw)) {
1249
+ return;
1250
+ }
1251
+ const updateResult = await storage.updateChannel(channelId, (current) => {
1252
+ if (!current || current.pendingRequest?.pendingId !== requestContext.pendingId) {
1253
+ return current;
1254
+ }
1255
+ const channel = {
1256
+ channelId,
1257
+ channelConfig,
1258
+ chargedCumulativeAmount: current.chargedCumulativeAmount,
1259
+ signedMaxClaimable,
1260
+ signature,
1261
+ balance,
1262
+ totalClaimed,
1263
+ withdrawRequestedAt,
1264
+ refundNonce,
1265
+ onchainSyncedAt: requestContext.localVerify ? current.onchainSyncedAt : now,
1266
+ lastRequestTimestamp: now,
1267
+ pendingRequest: current.pendingRequest
1268
+ };
1269
+ return channel;
1270
+ });
1271
+ if (updateResult.status === "updated" && updateResult.channel) {
1272
+ scheme.rememberChannelSnapshot(paymentPayload, updateResult.channel);
1273
+ }
1274
+ if (isRefundVoucher && updateResult.status === "updated") {
1275
+ return {
1276
+ skipHandler: true,
1277
+ response: {
1278
+ contentType: "application/json",
1279
+ body: { message: "Refund acknowledged", channelId }
1280
+ }
1281
+ };
1282
+ }
1283
+ }
1284
+ async function handleVerifyFailure(scheme, ctx) {
1285
+ await scheme.clearPendingRequest(ctx.paymentPayload);
1286
+ }
1287
+ async function handleVerifiedPaymentCanceled(scheme, ctx) {
1288
+ if (ctx.reason !== "handler_threw" && ctx.reason !== "handler_failed") {
1289
+ return;
1290
+ }
1291
+ await scheme.clearPendingRequest(ctx.paymentPayload);
1292
+ }
1293
+ async function verifyVoucherLocally(scheme, raw, requirements, channel, now) {
1294
+ if (!channel || !isOnchainStateFresh(channel, scheme.getOnchainStateTtlMs(), now)) {
1295
+ return;
1296
+ }
1297
+ if (raw.channelConfig.payerAuthorizer === "0x0000000000000000000000000000000000000000") {
1298
+ return;
1299
+ }
1300
+ const payer = raw.channelConfig.payer;
1301
+ const configErr = validateChannelConfig(
1302
+ raw.channelConfig,
1303
+ raw.voucher.channelId,
1304
+ requirements
1305
+ );
1306
+ if (configErr) {
1307
+ return invalidVerifyResponse(payer, configErr);
1308
+ }
1309
+ if (computeChannelId(raw.channelConfig, requirements.network).toLowerCase() !== channel.channelId.toLowerCase()) {
1310
+ return invalidVerifyResponse(payer, ErrChannelIdMismatch);
1311
+ }
1312
+ const signatureOk = await verifyLocalVoucherSignature(raw, requirements.network);
1313
+ if (!signatureOk) {
1314
+ return invalidVerifyResponse(payer, ErrInvalidVoucherSignature);
1315
+ }
1316
+ const maxClaimableAmount = BigInt(raw.voucher.maxClaimableAmount);
1317
+ if (maxClaimableAmount > BigInt(channel.balance)) {
1318
+ return invalidVerifyResponse(payer, ErrCumulativeExceedsBalance);
1319
+ }
1320
+ if (maxClaimableAmount <= BigInt(channel.totalClaimed)) {
1321
+ return invalidVerifyResponse(payer, ErrCumulativeAmountBelowClaimed);
1322
+ }
1323
+ return {
1324
+ isValid: true,
1325
+ payer,
1326
+ extra: {
1327
+ channelId: raw.voucher.channelId,
1328
+ balance: channel.balance,
1329
+ totalClaimed: channel.totalClaimed,
1330
+ withdrawRequestedAt: channel.withdrawRequestedAt,
1331
+ refundNonce: channel.refundNonce.toString()
1332
+ }
1333
+ };
1334
+ }
1335
+ function isOnchainStateFresh(channel, ttlMs, now) {
1336
+ return channel.onchainSyncedAt !== void 0 && now - channel.onchainSyncedAt <= ttlMs;
1337
+ }
1338
+ async function verifyLocalVoucherSignature(raw, network) {
1339
+ try {
1340
+ return await (0, import_viem6.verifyTypedData)({
1341
+ address: (0, import_viem6.getAddress)(raw.channelConfig.payerAuthorizer),
1342
+ domain: getBatchSettlementEip712Domain(getEvmChainId(network)),
1343
+ types: voucherTypes,
1344
+ primaryType: "Voucher",
1345
+ message: {
1346
+ channelId: raw.voucher.channelId,
1347
+ maxClaimableAmount: BigInt(raw.voucher.maxClaimableAmount)
1348
+ },
1349
+ signature: raw.voucher.signature
1350
+ });
1351
+ } catch {
1352
+ return false;
1353
+ }
1354
+ }
1355
+ function invalidVerifyResponse(payer, invalidReason) {
1356
+ return { isValid: false, invalidReason, payer };
1357
+ }
1358
+ function buildProvisionalChannel(raw, chargedCumulativeAmount) {
1359
+ return {
1360
+ channelId: raw.voucher.channelId,
1361
+ channelConfig: raw.channelConfig,
1362
+ chargedCumulativeAmount,
1363
+ signedMaxClaimable: raw.voucher.maxClaimableAmount,
1364
+ signature: raw.voucher.signature,
1365
+ balance: "0",
1366
+ totalClaimed: "0",
1367
+ withdrawRequestedAt: 0,
1368
+ refundNonce: 0,
1369
+ lastRequestTimestamp: Date.now()
1370
+ };
1371
+ }
1372
+ function inferMissingLocalChargedAmount(signedMaxClaimable, price, isPaidPayload) {
1373
+ if (!isPaidPayload) {
1374
+ return signedMaxClaimable;
1375
+ }
1376
+ const signed = BigInt(signedMaxClaimable);
1377
+ const amount = BigInt(price);
1378
+ if (signed < amount) {
1379
+ return "0";
1380
+ }
1381
+ return (signed - amount).toString();
1382
+ }
1383
+
1384
+ // src/batch-settlement/server/settle.ts
1385
+ function channelStateExtra(channel, chargedCumulativeAmount) {
1386
+ return {
1387
+ channelId: channel.channelId,
1388
+ balance: channel.balance,
1389
+ totalClaimed: channel.totalClaimed,
1390
+ withdrawRequestedAt: channel.withdrawRequestedAt,
1391
+ refundNonce: String(channel.refundNonce),
1392
+ ...chargedCumulativeAmount !== void 0 ? { chargedCumulativeAmount } : {}
1393
+ };
1394
+ }
1395
+ async function handleBeforeSettle(scheme, ctx) {
1396
+ const { paymentPayload, requirements } = ctx;
1397
+ const raw = paymentPayload.payload;
1398
+ const storage = scheme.getStorage();
1399
+ if (!isBatchSettlementVoucherPayload(raw)) {
1400
+ return;
1401
+ }
1402
+ const { voucher } = raw;
1403
+ const channelId = voucher.channelId;
1404
+ const pendingId = scheme.readRequestContext(paymentPayload)?.pendingId;
1405
+ const increment = BigInt(requirements.amount);
1406
+ const signedCap = BigInt(voucher.maxClaimableAmount);
1407
+ let outcome;
1408
+ const updateResult = await storage.updateChannel(channelId, (current) => {
1409
+ if (!current) {
1410
+ outcome = { status: "missing" };
1411
+ return current;
1412
+ }
1413
+ if (!pendingId || current.pendingRequest?.pendingId !== pendingId) {
1414
+ outcome = { status: "pending_mismatch" };
1415
+ return current;
1416
+ }
1417
+ const newCharged = BigInt(current.chargedCumulativeAmount) + increment;
1418
+ if (newCharged > signedCap) {
1419
+ outcome = { status: "cap_exceeded", charged: newCharged.toString() };
1420
+ return {
1421
+ ...current,
1422
+ pendingRequest: void 0
1423
+ };
1424
+ }
1425
+ const updatedChannel = {
1426
+ ...current,
1427
+ chargedCumulativeAmount: newCharged.toString(),
1428
+ signedMaxClaimable: voucher.maxClaimableAmount,
1429
+ signature: voucher.signature,
1430
+ lastRequestTimestamp: Date.now(),
1431
+ pendingRequest: void 0
1432
+ };
1433
+ outcome = { status: "committed", previous: current, current: updatedChannel };
1434
+ return updatedChannel;
1435
+ });
1436
+ if (outcome?.status === "missing") {
1437
+ scheme.takeRequestContext(paymentPayload);
1438
+ return {
1439
+ abort: true,
1440
+ reason: ErrMissingChannel,
1441
+ message: "No channel record"
1442
+ };
1443
+ }
1444
+ if (outcome?.status === "cap_exceeded") {
1445
+ scheme.takeRequestContext(paymentPayload);
1446
+ return {
1447
+ abort: true,
1448
+ reason: ErrChargeExceedsSignedCumulative,
1449
+ message: `Charged ${outcome.charged} exceeds signed max ${signedCap.toString()}`
1450
+ };
1451
+ }
1452
+ if (updateResult.status !== "updated" || outcome?.status !== "committed") {
1453
+ scheme.takeRequestContext(paymentPayload);
1454
+ return {
1455
+ abort: true,
1456
+ reason: ErrChannelBusy,
1457
+ message: "Concurrent request modified channel state"
1458
+ };
1459
+ }
1460
+ scheme.takeRequestContext(paymentPayload);
1461
+ const skipExtra = {
1462
+ channelState: channelStateExtra(outcome.previous, outcome.current.chargedCumulativeAmount),
1463
+ chargedAmount: requirements.amount
1464
+ };
1465
+ return {
1466
+ skip: true,
1467
+ result: {
1468
+ success: true,
1469
+ payer: outcome.previous.channelConfig.payer.toLowerCase(),
1470
+ transaction: "",
1471
+ network: requirements.network,
1472
+ amount: "",
1473
+ extra: skipExtra
1474
+ }
1475
+ };
1476
+ }
1477
+ async function handleEnrichSettlementPayload(scheme, ctx) {
1478
+ const { paymentPayload, requirements } = ctx;
1479
+ const raw = paymentPayload.payload;
1480
+ if (!isBatchSettlementRefundPayload(raw)) {
1481
+ return;
1482
+ }
1483
+ const channelId = computeChannelId(raw.channelConfig, requirements.network);
1484
+ if (raw.voucher.channelId !== channelId) {
1485
+ throw new Error("refund channelId does not match channelConfig");
1486
+ }
1487
+ const channel = await scheme.getStorage().get(channelId);
1488
+ if (!channel) {
1489
+ throw new Error(ErrMissingChannel);
1490
+ }
1491
+ const pendingId = scheme.readRequestContext(paymentPayload)?.pendingId;
1492
+ if (!pendingId || channel.pendingRequest?.pendingId !== pendingId) {
1493
+ throw new Error(ErrChannelBusy);
1494
+ }
1495
+ if (BigInt(raw.voucher.maxClaimableAmount) !== BigInt(channel.chargedCumulativeAmount)) {
1496
+ throw new Error(ErrCumulativeAmountMismatch);
1497
+ }
1498
+ if (raw.voucher.signature !== channel.signature) {
1499
+ throw new Error(ErrInvalidVoucherSignature);
1500
+ }
1501
+ const config = raw.channelConfig;
1502
+ const claimEntry = {
1503
+ voucher: {
1504
+ channel: config,
1505
+ maxClaimableAmount: raw.voucher.maxClaimableAmount
1506
+ },
1507
+ signature: raw.voucher.signature,
1508
+ totalClaimed: channel.chargedCumulativeAmount
1509
+ };
1510
+ const remainder = BigInt(channel.balance) - BigInt(channel.chargedCumulativeAmount);
1511
+ if (remainder <= 0n) {
1512
+ throw new Error(ErrRefundNoBalance);
1513
+ }
1514
+ let refundAmountBig = remainder;
1515
+ if (raw.amount !== void 0) {
1516
+ if (!/^\d+$/.test(raw.amount)) {
1517
+ throw new Error(ErrRefundAmountInvalid);
1518
+ }
1519
+ const requested = BigInt(raw.amount);
1520
+ if (requested <= 0n) {
1521
+ throw new Error(ErrRefundAmountInvalid);
1522
+ }
1523
+ refundAmountBig = requested;
1524
+ }
1525
+ const refundAmount = refundAmountBig.toString();
1526
+ const nonce = String(channel.refundNonce ?? 0);
1527
+ const receiverAuthorizerSigner = scheme.getReceiverAuthorizerSigner();
1528
+ const refundAuthorizerSignature = receiverAuthorizerSigner ? await signRefund(
1529
+ receiverAuthorizerSigner,
1530
+ channelId,
1531
+ refundAmount,
1532
+ nonce,
1533
+ requirements.network
1534
+ ) : void 0;
1535
+ const claimAuthorizerSignature = receiverAuthorizerSigner ? await signClaimBatch(receiverAuthorizerSigner, [claimEntry], requirements.network) : void 0;
1536
+ scheme.rememberChannelSnapshot(paymentPayload, channel);
1537
+ return {
1538
+ ...raw.amount === void 0 ? { amount: refundAmount } : {},
1539
+ refundNonce: nonce,
1540
+ claims: [claimEntry],
1541
+ refundAuthorizerSignature,
1542
+ claimAuthorizerSignature
1543
+ };
1544
+ }
1545
+ async function handleAfterSettle(scheme, ctx) {
1546
+ const { paymentPayload, requirements, result } = ctx;
1547
+ if (!result.success) {
1548
+ return;
1549
+ }
1550
+ const raw = paymentPayload.payload;
1551
+ const storage = scheme.getStorage();
1552
+ if (isBatchSettlementRefundPayload(raw)) {
1553
+ const channelId = computeChannelId(raw.channelConfig, requirements.network);
1554
+ const pendingId = scheme.readRequestContext(paymentPayload)?.pendingId;
1555
+ const now = Date.now();
1556
+ const snapshot = parseRefundSettlementSnapshot(result.extra);
1557
+ const updateResult = await storage.updateChannel(channelId, (current) => {
1558
+ if (!current) {
1559
+ return current;
1560
+ }
1561
+ if (!pendingId || current.pendingRequest?.pendingId !== pendingId) {
1562
+ return current;
1563
+ }
1564
+ if (BigInt(snapshot.balance) <= BigInt(current.chargedCumulativeAmount)) {
1565
+ return void 0;
1566
+ }
1567
+ return {
1568
+ ...current,
1569
+ ...snapshot,
1570
+ onchainSyncedAt: now,
1571
+ lastRequestTimestamp: now,
1572
+ pendingRequest: void 0
1573
+ };
1574
+ });
1575
+ if (updateResult.status === "unchanged") {
1576
+ throw new Error(ErrChannelBusy);
1577
+ }
1578
+ if (!updateResult.channel) {
1579
+ return;
1580
+ }
1581
+ return;
1582
+ }
1583
+ if (isBatchSettlementVoucherPayload(raw)) {
1584
+ return;
1585
+ }
1586
+ if (isBatchSettlementDepositPayload(raw)) {
1587
+ const channelId = raw.voucher.channelId;
1588
+ const pendingId = scheme.readRequestContext(paymentPayload)?.pendingId;
1589
+ const ex = result.extra ?? {};
1590
+ const channelState = readChannelStateExtra(ex);
1591
+ const config = raw.channelConfig;
1592
+ const signedMaxClaimable = raw.voucher.maxClaimableAmount;
1593
+ const now = Date.now();
1594
+ const updateResult = await storage.updateChannel(channelId, (current) => {
1595
+ if (!current) {
1596
+ return current;
1597
+ }
1598
+ if (!pendingId || current.pendingRequest?.pendingId !== pendingId) {
1599
+ return current;
1600
+ }
1601
+ const chargedActual = (BigInt(current.chargedCumulativeAmount) + BigInt(requirements.amount)).toString();
1602
+ return {
1603
+ channelId,
1604
+ channelConfig: config,
1605
+ chargedCumulativeAmount: chargedActual,
1606
+ signedMaxClaimable,
1607
+ signature: raw.voucher.signature,
1608
+ balance: readExtraString(channelState, "balance", current.balance),
1609
+ totalClaimed: readExtraString(channelState, "totalClaimed", current.totalClaimed),
1610
+ withdrawRequestedAt: readExtraNumber(
1611
+ channelState,
1612
+ "withdrawRequestedAt",
1613
+ current.withdrawRequestedAt
1614
+ ),
1615
+ refundNonce: readExtraNumber(channelState, "refundNonce", current.refundNonce),
1616
+ onchainSyncedAt: now,
1617
+ lastRequestTimestamp: now
1618
+ };
1619
+ });
1620
+ if (updateResult.status === "updated" && updateResult.channel) {
1621
+ scheme.rememberChannelSnapshot(paymentPayload, updateResult.channel);
1622
+ return;
1623
+ }
1624
+ scheme.takeRequestContext(paymentPayload);
1625
+ throw new Error(ErrChannelBusy);
1626
+ }
1627
+ }
1628
+ async function handleSettleFailure(scheme, ctx) {
1629
+ await scheme.clearPendingRequest(ctx.paymentPayload);
1630
+ }
1631
+ async function handleEnrichSettlementResponse(scheme, ctx) {
1632
+ const raw = ctx.paymentPayload.payload;
1633
+ if (isBatchSettlementVoucherPayload(raw)) {
1634
+ return;
1635
+ }
1636
+ const channel = scheme.takeChannelSnapshot(ctx.paymentPayload);
1637
+ if (!channel) {
1638
+ return;
1639
+ }
1640
+ if (isBatchSettlementRefundPayload(raw)) {
1641
+ return {
1642
+ channelState: {
1643
+ chargedCumulativeAmount: channel.chargedCumulativeAmount
1644
+ }
1645
+ };
1646
+ }
1647
+ if (isBatchSettlementDepositPayload(raw)) {
1648
+ return {
1649
+ channelState: {
1650
+ chargedCumulativeAmount: channel.chargedCumulativeAmount
1651
+ },
1652
+ chargedAmount: ctx.requirements.amount
1653
+ };
1654
+ }
1655
+ return {
1656
+ channelState: {
1657
+ chargedCumulativeAmount: channel.chargedCumulativeAmount
1658
+ }
1659
+ };
1660
+ }
1661
+
1662
+ // src/batch-settlement/server/scheme.ts
1663
+ var BatchSettlementEvmScheme = class {
1664
+ /**
1665
+ * Constructs a batched server scheme.
1666
+ *
1667
+ * @param receiverAddress - The server's receiver address (payTo).
1668
+ * @param config - Optional configuration for storage, receiver-authorizer signer, and withdraw delay.
1669
+ */
1670
+ constructor(receiverAddress, config) {
1671
+ this.scheme = BATCH_SETTLEMENT_SCHEME;
1672
+ this.requestContexts = /* @__PURE__ */ new WeakMap();
1673
+ this.moneyParsers = [];
1674
+ /**
1675
+ * Adds server-owned settlement fields before facilitator settlement.
1676
+ *
1677
+ * @param ctx - Settlement context for the current payment.
1678
+ * @returns Additive payload fields, or nothing when no enrichment is needed.
1679
+ */
1680
+ this.enrichSettlementPayload = (ctx) => handleEnrichSettlementPayload(this, ctx);
1681
+ /**
1682
+ * Adds corrective channel state to payment-required responses when available.
1683
+ *
1684
+ * @param ctx - Payment-required response context for the current request.
1685
+ * @returns Updated payment requirements, or nothing when no enrichment is needed.
1686
+ */
1687
+ this.enrichPaymentRequiredResponse = (ctx) => handleEnrichPaymentRequiredResponse(this, ctx);
1688
+ /**
1689
+ * Adds server-owned extra fields after facilitator settlement.
1690
+ *
1691
+ * @param ctx - Settlement result context for the current payment.
1692
+ * @returns Additive response extra fields, or nothing when no enrichment is needed.
1693
+ */
1694
+ this.enrichSettlementResponse = (ctx) => handleEnrichSettlementResponse(this, ctx);
1695
+ this.receiverAddress = receiverAddress;
1696
+ this.storage = config?.storage ?? new InMemoryChannelStorage();
1697
+ this.receiverAuthorizerSigner = config?.receiverAuthorizerSigner;
1698
+ this.withdrawDelay = config?.withdrawDelay ?? MIN_WITHDRAW_DELAY;
1699
+ this.onchainStateTtlMs = config?.onchainStateTtlMs ?? defaultOnchainStateTtlMs(this.withdrawDelay);
1700
+ this.schemeHooks = {
1701
+ onBeforeVerify: (ctx) => handleBeforeVerify(this, ctx),
1702
+ onAfterVerify: (ctx) => handleAfterVerify(this, ctx),
1703
+ onBeforeSettle: (ctx) => handleBeforeSettle(this, ctx),
1704
+ onAfterSettle: (ctx) => handleAfterSettle(this, ctx),
1705
+ onVerifyFailure: (ctx) => handleVerifyFailure(this, ctx),
1706
+ onSettleFailure: (ctx) => handleSettleFailure(this, ctx),
1707
+ onVerifiedPaymentCanceled: (ctx) => handleVerifiedPaymentCanceled(this, ctx)
1708
+ };
1709
+ }
1710
+ /**
1711
+ * Merges batch-settlement state into the current request context.
1712
+ *
1713
+ * @param payload - Request-scoped payment payload object.
1714
+ * @param context - Partial context fields to merge.
1715
+ */
1716
+ mergeRequestContext(payload, context) {
1717
+ this.requestContexts.set(payload, {
1718
+ ...this.requestContexts.get(payload),
1719
+ ...context
1720
+ });
1721
+ }
1722
+ /**
1723
+ * Reads batch-settlement state for the current request without clearing it.
1724
+ *
1725
+ * @param payload - Request-scoped payment payload object.
1726
+ * @returns Request context, if one was recorded.
1727
+ */
1728
+ readRequestContext(payload) {
1729
+ return this.requestContexts.get(payload);
1730
+ }
1731
+ /**
1732
+ * Reads and clears batch-settlement state for the current request.
1733
+ *
1734
+ * @param payload - Request-scoped payment payload object.
1735
+ * @returns Request context, if one was recorded.
1736
+ */
1737
+ takeRequestContext(payload) {
1738
+ const context = this.requestContexts.get(payload);
1739
+ this.requestContexts.delete(payload);
1740
+ return context;
1741
+ }
1742
+ /**
1743
+ * Stores a channel snapshot for the current settlement request.
1744
+ *
1745
+ * @param payload - Request-scoped payment payload object.
1746
+ * @param channel - Channel state to use during response enrichment.
1747
+ */
1748
+ rememberChannelSnapshot(payload, channel) {
1749
+ this.mergeRequestContext(payload, {
1750
+ channelId: channel.channelId,
1751
+ channelSnapshot: channel
1752
+ });
1753
+ }
1754
+ /**
1755
+ * Reads and clears a channel snapshot for the current settlement request.
1756
+ *
1757
+ * @param payload - Request-scoped payment payload object.
1758
+ * @returns Stored channel state, if one was recorded.
1759
+ */
1760
+ takeChannelSnapshot(payload) {
1761
+ return this.takeRequestContext(payload)?.channelSnapshot;
1762
+ }
1763
+ /**
1764
+ * Clears this request's pending reservation without touching newer reservations.
1765
+ *
1766
+ * @param payload - Request-scoped payment payload object.
1767
+ */
1768
+ async clearPendingRequest(payload) {
1769
+ const context = this.takeRequestContext(payload);
1770
+ if (!context?.channelId || !context.pendingId) {
1771
+ return;
1772
+ }
1773
+ await this.storage.updateChannel(context.channelId, (current) => {
1774
+ if (!current || current.pendingRequest?.pendingId !== context.pendingId) {
1775
+ return current;
1776
+ }
1777
+ if (!context.channelSnapshot) {
1778
+ return void 0;
1779
+ }
1780
+ return {
1781
+ ...current,
1782
+ pendingRequest: void 0
1783
+ };
1784
+ });
1785
+ }
1786
+ /**
1787
+ * Registers a custom money parser for converting price strings to token amounts.
1788
+ *
1789
+ * @param parser - A parser function to try before the default USD→token conversion.
1790
+ * @returns `this` for chaining.
1791
+ */
1792
+ registerMoneyParser(parser) {
1793
+ this.moneyParsers.push(parser);
1794
+ return this;
1795
+ }
1796
+ /**
1797
+ * Resolves a human-readable price (e.g. `"$0.01"`) into an onchain token amount.
1798
+ *
1799
+ * @param price - A price string, number, or explicit {@link AssetAmount}.
1800
+ * @param network - CAIP-2 network identifier for looking up the default asset.
1801
+ * @returns Token amount with asset address and metadata.
1802
+ */
1803
+ async parsePrice(price, network) {
1804
+ if (typeof price === "object" && price !== null && "amount" in price) {
1805
+ if (!price.asset) {
1806
+ throw new Error(`Asset address must be specified for AssetAmount on network ${network}`);
1807
+ }
1808
+ return {
1809
+ amount: price.amount,
1810
+ asset: price.asset,
1811
+ extra: price.extra || {}
1812
+ };
1813
+ }
1814
+ const amount = this.parseMoneyToDecimal(price);
1815
+ for (const parser of this.moneyParsers) {
1816
+ const result = await parser(amount, network);
1817
+ if (result !== null) {
1818
+ return result;
1819
+ }
1820
+ }
1821
+ return this.defaultMoneyConversion(amount, network);
1822
+ }
1823
+ /**
1824
+ * Injects batched-specific fields into the payment requirements returned to
1825
+ * the client (receiverAuthorizer, withdrawDelay, EIP-712 domain info).
1826
+ *
1827
+ * @param paymentRequirements - Base payment requirements from the middleware.
1828
+ * @param supportedKind - Matched scheme/network kind (extra may contain overrides).
1829
+ * @param supportedKind.x402Version - Protocol version from the matched kind.
1830
+ * @param supportedKind.scheme - Scheme name from the matched kind.
1831
+ * @param supportedKind.network - Network identifier from the matched kind.
1832
+ * @param supportedKind.extra - Optional extra fields on the matched kind.
1833
+ * @param _extensionKeys - Extension keys (unused).
1834
+ * @returns Enhanced payment requirements with batched fields in `extra`.
1835
+ */
1836
+ async enhancePaymentRequirements(paymentRequirements, supportedKind, _extensionKeys) {
1837
+ void _extensionKeys;
1838
+ const assetInfo = getDefaultAsset(paymentRequirements.network);
1839
+ const receiverAuthorizer = this.receiverAuthorizerSigner?.address ?? (typeof supportedKind.extra?.receiverAuthorizer === "string" ? supportedKind.extra.receiverAuthorizer : void 0);
1840
+ if (!receiverAuthorizer || (0, import_viem7.getAddress)(receiverAuthorizer) === "0x0000000000000000000000000000000000000000") {
1841
+ throw new Error("Payment requirements must include a non-zero extra.receiverAuthorizer");
1842
+ }
1843
+ return {
1844
+ ...paymentRequirements,
1845
+ extra: {
1846
+ ...paymentRequirements.extra,
1847
+ receiverAuthorizer: (0, import_viem7.getAddress)(receiverAuthorizer),
1848
+ withdrawDelay: this.withdrawDelay,
1849
+ name: assetInfo.name,
1850
+ version: assetInfo.version,
1851
+ assetTransferMethod: paymentRequirements.extra?.assetTransferMethod ?? assetInfo.assetTransferMethod
1852
+ }
1853
+ };
1854
+ }
1855
+ /**
1856
+ * Returns the underlying channel storage instance.
1857
+ *
1858
+ * @returns The configured {@link ChannelStorage} backend.
1859
+ */
1860
+ getStorage() {
1861
+ return this.storage;
1862
+ }
1863
+ /**
1864
+ * Returns the server's receiver address.
1865
+ *
1866
+ * @returns Receiver wallet address for the payment channel.
1867
+ */
1868
+ getReceiverAddress() {
1869
+ return this.receiverAddress;
1870
+ }
1871
+ /**
1872
+ * Returns the configured withdraw delay (seconds).
1873
+ *
1874
+ * @returns Withdraw delay in seconds before uncooperative withdrawal is allowed.
1875
+ */
1876
+ getWithdrawDelay() {
1877
+ return this.withdrawDelay;
1878
+ }
1879
+ /**
1880
+ * Returns how long mirrored onchain channel state is trusted for local voucher verification.
1881
+ *
1882
+ * @returns Freshness window in milliseconds.
1883
+ */
1884
+ getOnchainStateTtlMs() {
1885
+ return this.onchainStateTtlMs;
1886
+ }
1887
+ /**
1888
+ * Returns the receiver-authorizer signer, if configured.
1889
+ *
1890
+ * @returns Receiver-authorizer signer, or `undefined` when not set.
1891
+ */
1892
+ getReceiverAuthorizerSigner() {
1893
+ return this.receiverAuthorizerSigner;
1894
+ }
1895
+ /**
1896
+ * Creates a {@link BatchSettlementChannelManager} pre-configured with this scheme's
1897
+ * receiver, default token for the given network, and the provided facilitator.
1898
+ *
1899
+ * @param facilitator - Facilitator client for submitting onchain claims/settlements.
1900
+ * @param network - CAIP-2 network identifier (e.g. `"eip155:84532"`).
1901
+ * @returns A ready-to-use channel manager.
1902
+ */
1903
+ createChannelManager(facilitator, network) {
1904
+ const token = getDefaultAsset(network).address;
1905
+ return new BatchSettlementChannelManager({
1906
+ scheme: this,
1907
+ facilitator,
1908
+ receiver: this.receiverAddress,
1909
+ token,
1910
+ network
1911
+ });
1912
+ }
1913
+ /**
1914
+ * Parses a human-readable money string (e.g. `"$1.50"`) into a decimal number.
1915
+ *
1916
+ * @param money - Money string (may include `$`) or numeric amount.
1917
+ * @returns Parsed finite number.
1918
+ */
1919
+ parseMoneyToDecimal(money) {
1920
+ if (typeof money === "number") {
1921
+ return money;
1922
+ }
1923
+ const cleanMoney = money.replace(/^\$/, "").trim();
1924
+ const amount = parseFloat(cleanMoney);
1925
+ if (isNaN(amount)) {
1926
+ throw new Error(`Invalid money format: ${money}`);
1927
+ }
1928
+ return amount;
1929
+ }
1930
+ /**
1931
+ * Converts a decimal dollar amount to the network's default token amount.
1932
+ *
1933
+ * @param amount - Decimal amount in display units.
1934
+ * @param network - Target chain/network for default asset resolution.
1935
+ * @returns {@link AssetAmount} with integer token amount, contract address, and metadata.
1936
+ */
1937
+ defaultMoneyConversion(amount, network) {
1938
+ const assetInfo = getDefaultAsset(network);
1939
+ const tokenAmount = (0, import_utils12.convertToTokenAmount)((0, import_utils12.numberToDecimalString)(amount), assetInfo.decimals);
1940
+ return {
1941
+ amount: tokenAmount,
1942
+ asset: assetInfo.address,
1943
+ extra: {
1944
+ name: assetInfo.name,
1945
+ version: assetInfo.version
1946
+ }
1947
+ };
1948
+ }
1949
+ };
1950
+ function defaultOnchainStateTtlMs(withdrawDelaySeconds) {
1951
+ const withdrawDelayMs = Math.max(0, withdrawDelaySeconds) * 1e3;
1952
+ return Math.min(5 * 60 * 1e3, Math.max(30 * 1e3, Math.floor(withdrawDelayMs / 3)));
1953
+ }
1954
+ // Annotate the CommonJS export names for ESM import in node:
1955
+ 0 && (module.exports = {
1956
+ BatchSettlementChannelManager,
1957
+ BatchSettlementEvmScheme,
1958
+ InMemoryChannelStorage
1959
+ });
1960
+ //# sourceMappingURL=index.js.map