@arcenpay/node 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,3317 @@
1
+ import {
2
+ TablelandService,
3
+ __esm,
4
+ __export,
5
+ __toCommonJS
6
+ } from "./chunk-SKFD6TSD.mjs";
7
+
8
+ // src/services/nonce-store.ts
9
+ var nonce_store_exports = {};
10
+ __export(nonce_store_exports, {
11
+ InMemoryNonceStore: () => InMemoryNonceStore,
12
+ RedisNonceStore: () => RedisNonceStore
13
+ });
14
+ var InMemoryNonceStore, RedisNonceStore;
15
+ var init_nonce_store = __esm({
16
+ "src/services/nonce-store.ts"() {
17
+ "use strict";
18
+ InMemoryNonceStore = class {
19
+ seen = /* @__PURE__ */ new Map();
20
+ cleanupTimer;
21
+ constructor(cleanupIntervalMs = 6e4) {
22
+ this.cleanupTimer = setInterval(() => {
23
+ const now = Date.now();
24
+ for (const [key, expiresAt] of this.seen) {
25
+ if (now >= expiresAt) {
26
+ this.seen.delete(key);
27
+ }
28
+ }
29
+ }, cleanupIntervalMs);
30
+ if (typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
31
+ this.cleanupTimer.unref();
32
+ }
33
+ }
34
+ async markUsed(key, ttlMs) {
35
+ if (this.seen.has(key)) {
36
+ const expiresAt = this.seen.get(key);
37
+ if (Date.now() < expiresAt) {
38
+ return false;
39
+ }
40
+ }
41
+ this.seen.set(key, Date.now() + ttlMs);
42
+ return true;
43
+ }
44
+ async has(key) {
45
+ if (!this.seen.has(key)) return false;
46
+ const expiresAt = this.seen.get(key);
47
+ if (Date.now() >= expiresAt) {
48
+ this.seen.delete(key);
49
+ return false;
50
+ }
51
+ return true;
52
+ }
53
+ stop() {
54
+ clearInterval(this.cleanupTimer);
55
+ }
56
+ };
57
+ RedisNonceStore = class {
58
+ redis;
59
+ prefix;
60
+ constructor(redis, prefix = "meap:nonce:") {
61
+ this.redis = redis;
62
+ this.prefix = prefix;
63
+ }
64
+ async markUsed(key, ttlMs) {
65
+ const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
66
+ const result = await this.redis.set(
67
+ `${this.prefix}${key}`,
68
+ "1",
69
+ "EX",
70
+ ttlSeconds,
71
+ "NX"
72
+ );
73
+ return result === "OK";
74
+ }
75
+ async has(key) {
76
+ const result = await this.redis.get(`${this.prefix}${key}`);
77
+ return result !== null;
78
+ }
79
+ stop() {
80
+ }
81
+ };
82
+ }
83
+ });
84
+
85
+ // src/client.ts
86
+ import { resolveArcenPayBaseUrl } from "@arcenpay/sdk";
87
+ var ArcenClient = class {
88
+ apiKey;
89
+ baseUrl;
90
+ timeoutMs;
91
+ session = null;
92
+ constructor(config) {
93
+ if (!config.apiKey.startsWith("api_")) {
94
+ throw new Error('ArcenClient: apiKey must start with "api_"');
95
+ }
96
+ this.apiKey = config.apiKey;
97
+ this.baseUrl = resolveArcenPayBaseUrl({ explicit: config.baseUrl });
98
+ this.timeoutMs = config.timeoutMs ?? 1e4;
99
+ }
100
+ buildAuthHeader() {
101
+ return this.session ? `Bearer ${this.session.token}` : `Bearer ${this.apiKey}`;
102
+ }
103
+ async request(method, path2, options = {}) {
104
+ const url = new URL(`${this.baseUrl}${path2}`);
105
+ if (options.query) {
106
+ for (const [k, v] of Object.entries(options.query)) {
107
+ url.searchParams.set(k, v);
108
+ }
109
+ }
110
+ const headers = {
111
+ Authorization: options.useApiKey ? `Bearer ${this.apiKey}` : this.buildAuthHeader(),
112
+ "Content-Type": "application/json"
113
+ };
114
+ if (!this.session || options.useApiKey) {
115
+ if (options.companyKeys) {
116
+ const parts = [];
117
+ if (options.companyKeys.id) parts.push(`id=${options.companyKeys.id}`);
118
+ if (options.companyKeys.wallet)
119
+ parts.push(`wallet=${options.companyKeys.wallet}`);
120
+ if (options.companyKeys.email)
121
+ parts.push(`email=${options.companyKeys.email}`);
122
+ if (parts.length > 0) headers["X-Arcen-Company-Keys"] = parts.join(",");
123
+ }
124
+ if (options.userKeys) {
125
+ const parts = [];
126
+ if (options.userKeys.id) parts.push(`id=${options.userKeys.id}`);
127
+ if (options.userKeys.clerkUserId)
128
+ parts.push(`clerk_user_id=${options.userKeys.clerkUserId}`);
129
+ if (options.userKeys.wallet)
130
+ parts.push(`wallet=${options.userKeys.wallet}`);
131
+ if (parts.length > 0) headers["X-Arcen-User-Keys"] = parts.join(",");
132
+ }
133
+ }
134
+ const controller = new AbortController();
135
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
136
+ try {
137
+ const res = await fetch(url.toString(), {
138
+ method,
139
+ headers,
140
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
141
+ signal: controller.signal
142
+ });
143
+ let json;
144
+ try {
145
+ json = await res.json();
146
+ } catch {
147
+ throw new ArcenApiError(
148
+ `Invalid JSON response from ${method} ${path2}`,
149
+ res.status
150
+ );
151
+ }
152
+ if (!res.ok) {
153
+ throw new ArcenApiError(json.error ?? `HTTP ${res.status}`, res.status);
154
+ }
155
+ return json;
156
+ } catch (err) {
157
+ if (err instanceof ArcenApiError) throw err;
158
+ if (err instanceof DOMException && err.name === "AbortError") {
159
+ throw new ArcenApiError(
160
+ `Request timed out after ${this.timeoutMs}ms`,
161
+ 408
162
+ );
163
+ }
164
+ throw new ArcenApiError(
165
+ err instanceof Error ? err.message : "Network request failed",
166
+ 0
167
+ );
168
+ } finally {
169
+ clearTimeout(timer);
170
+ }
171
+ }
172
+ async identify(input) {
173
+ const res = await this.request(
174
+ "POST",
175
+ "/api/v1/access-tokens",
176
+ {
177
+ useApiKey: true,
178
+ body: {
179
+ company: input.company,
180
+ user: input.user ? {
181
+ id: input.user.id,
182
+ clerk_user_id: input.user.clerkUserId,
183
+ wallet: input.user.wallet,
184
+ name: input.user.name,
185
+ email: input.user.email
186
+ } : void 0,
187
+ expires_in: input.expiresIn ?? 3600
188
+ }
189
+ }
190
+ );
191
+ const data = res.data;
192
+ this.session = {
193
+ token: data.token,
194
+ companyId: data.company_id,
195
+ userId: data.user_id
196
+ };
197
+ return {
198
+ token: data.token,
199
+ companyId: data.company_id,
200
+ userId: data.user_id,
201
+ expiresAt: data.expires_at
202
+ };
203
+ }
204
+ async track(event) {
205
+ const input = typeof event === "string" ? { name: event } : event;
206
+ await this.request("POST", "/api/v1/events", {
207
+ useApiKey: this.session === null,
208
+ body: {
209
+ event_type: "track",
210
+ name: input.name,
211
+ ...input.company ? { company: input.company } : {},
212
+ ...input.user ? { user: input.user } : {},
213
+ traits: input.traits,
214
+ ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}
215
+ }
216
+ });
217
+ }
218
+ async consumeEntitlement(input) {
219
+ const normalized = typeof input === "string" ? { featureKey: input } : input;
220
+ return this.request(
221
+ "POST",
222
+ "/api/v1/usage/consume",
223
+ {
224
+ useApiKey: this.session === null,
225
+ companyKeys: normalized.company,
226
+ userKeys: normalized.user,
227
+ body: {
228
+ featureKey: normalized.featureKey,
229
+ traits: normalized.traits,
230
+ ...normalized.idempotencyKey ? { idempotencyKey: normalized.idempotencyKey } : {}
231
+ }
232
+ }
233
+ );
234
+ }
235
+ async checkFlag(key, companyKeys, userKeys) {
236
+ return this.request("GET", "/api/v1/check", {
237
+ query: { key },
238
+ companyKeys,
239
+ userKeys
240
+ });
241
+ }
242
+ async checkEntitlement(key, companyKeys, userKeys) {
243
+ return this.request("GET", "/api/v1/check", {
244
+ query: { key },
245
+ companyKeys,
246
+ userKeys
247
+ });
248
+ }
249
+ async checkFlags(featureKeys, companyKeys, userKeys) {
250
+ const results = await Promise.all(
251
+ featureKeys.map((key) => this.checkFlag(key, companyKeys, userKeys))
252
+ );
253
+ return Object.fromEntries(results.map((r) => [r.key, r]));
254
+ }
255
+ async listCompanies(options = {}) {
256
+ const query = {};
257
+ if (options.limit) query.limit = String(options.limit);
258
+ if (options.search) query.search = options.search;
259
+ if (options.cursor) query.cursor = options.cursor;
260
+ return this.request("GET", "/api/v1/companies", { useApiKey: true, query });
261
+ }
262
+ async getCompany(id) {
263
+ return this.request("GET", `/api/v1/companies/${id}`, { useApiKey: true });
264
+ }
265
+ async createCompany(data) {
266
+ return this.request("POST", "/api/v1/companies", {
267
+ useApiKey: true,
268
+ body: data
269
+ });
270
+ }
271
+ async updateCompany(id, data) {
272
+ return this.request("PATCH", `/api/v1/companies/${id}`, {
273
+ useApiKey: true,
274
+ body: data
275
+ });
276
+ }
277
+ async setCompanyOverride(companyId, featureKey, value, reason) {
278
+ await this.request("POST", `/api/v1/companies/${companyId}/overrides`, {
279
+ useApiKey: true,
280
+ body: { featureKey, value, reason }
281
+ });
282
+ }
283
+ async activateSubscription(input) {
284
+ const res = await this.request(
285
+ "POST",
286
+ "/api/v1/subscriptions/activate",
287
+ {
288
+ useApiKey: true,
289
+ body: {
290
+ planId: typeof input.planId === "bigint" ? input.planId.toString() : String(input.planId),
291
+ paymentAccount: input.paymentAccount,
292
+ company: input.company,
293
+ ...input.subscriberEmail ? { subscriberEmail: input.subscriberEmail } : {}
294
+ }
295
+ }
296
+ );
297
+ return res.data;
298
+ }
299
+ /** @deprecated Use identify() instead */
300
+ async createAccessToken(companyKeys, userKeys, expiresIn = 3600) {
301
+ const res = await this.request(
302
+ "POST",
303
+ "/api/v1/access-tokens",
304
+ {
305
+ useApiKey: true,
306
+ body: {
307
+ company: companyKeys,
308
+ user: userKeys ? {
309
+ id: userKeys.id,
310
+ clerk_user_id: userKeys.clerkUserId,
311
+ wallet: userKeys.wallet
312
+ } : void 0,
313
+ expires_in: expiresIn
314
+ }
315
+ }
316
+ );
317
+ return {
318
+ token: res.data.token,
319
+ expiresAt: res.data.expires_at
320
+ };
321
+ }
322
+ };
323
+ var ArcenApiError = class extends Error {
324
+ constructor(message, statusCode) {
325
+ super(message);
326
+ this.statusCode = statusCode;
327
+ this.name = "ArcenApiError";
328
+ }
329
+ };
330
+
331
+ // src/middleware/x402.ts
332
+ import {
333
+ X402_VERSION,
334
+ X402_SCHEME,
335
+ SessionVaultABI,
336
+ getChainEnvironment
337
+ } from "@arcenpay/sdk";
338
+ import {
339
+ createPublicClient,
340
+ http,
341
+ recoverTypedDataAddress,
342
+ isAddress,
343
+ parseUnits
344
+ } from "viem";
345
+ import { sepolia, baseSepolia, base } from "viem/chains";
346
+ var PAYMENT_DOMAIN = {
347
+ name: "MEAP x402 Payment",
348
+ version: "1"
349
+ };
350
+ var PAYMENT_TYPES = {
351
+ Payment: [
352
+ { name: "from", type: "address" },
353
+ { name: "amount", type: "uint256" },
354
+ { name: "resource", type: "string" },
355
+ { name: "sessionId", type: "string" },
356
+ { name: "nonce", type: "uint256" },
357
+ { name: "timestamp", type: "uint256" }
358
+ ]
359
+ };
360
+ var CHAIN_MAP = {
361
+ 11155111: sepolia,
362
+ 84532: baseSepolia,
363
+ 8453: base
364
+ };
365
+ var CHAIN_ID_TO_NETWORK = {
366
+ 11155111: "ethereum-sepolia",
367
+ 84532: "base-sepolia",
368
+ 8453: "base-mainnet"
369
+ };
370
+ function coerceBigInt(value, fallback) {
371
+ try {
372
+ if (typeof value === "bigint") return value;
373
+ if (typeof value === "number") {
374
+ if (!Number.isFinite(value) || value < 0) return fallback;
375
+ return BigInt(Math.trunc(value));
376
+ }
377
+ if (typeof value === "string" && value.trim()) return BigInt(value.trim());
378
+ return fallback;
379
+ } catch {
380
+ return fallback;
381
+ }
382
+ }
383
+ function normalizeSessionId(value) {
384
+ if (typeof value !== "string") return "default";
385
+ const trimmed = value.trim();
386
+ return trimmed.length > 0 ? trimmed : "default";
387
+ }
388
+ function normalizeAmount(value, fallback) {
389
+ return coerceBigInt(value, fallback);
390
+ }
391
+ function normalizeNonce(value) {
392
+ return coerceBigInt(value, 0n);
393
+ }
394
+ function normalizeTimestamp(value) {
395
+ return coerceBigInt(value, BigInt(Date.now()));
396
+ }
397
+ function normalizePaymentTimestampMs(timestamp) {
398
+ if (timestamp < 1000000000000n) {
399
+ return timestamp * 1000n;
400
+ }
401
+ return timestamp;
402
+ }
403
+ function normalizeResourceUrl(value) {
404
+ try {
405
+ const parsed = new URL(value);
406
+ return `${parsed.origin}${parsed.pathname}${parsed.search}`;
407
+ } catch {
408
+ return null;
409
+ }
410
+ }
411
+ function buildRequestResource(req) {
412
+ return `${req.protocol}://${req.get("host")}${req.originalUrl}`;
413
+ }
414
+ function sendRejection(res, reason, message, details) {
415
+ res.setHeader("x-meap-rejection-reason", reason);
416
+ res.status(402).json({
417
+ ok: false,
418
+ rejectionReason: reason,
419
+ message,
420
+ ...details ? { details } : {}
421
+ });
422
+ }
423
+ function x402Middleware(options) {
424
+ const {
425
+ planId,
426
+ ratePerCall,
427
+ chainId = 84532,
428
+ network,
429
+ rpcUrl,
430
+ payTo,
431
+ settlePayment,
432
+ failOpenOnBalanceCheck = false,
433
+ maxPaymentAgeMs = 3e5,
434
+ // 5 minutes
435
+ nonceTtlMs = 6e5,
436
+ // 10 minutes
437
+ nonceStore: externalNonceStore
438
+ } = options;
439
+ const inferredNetwork = CHAIN_ID_TO_NETWORK[chainId];
440
+ if (network && inferredNetwork && network !== inferredNetwork) {
441
+ throw new Error(
442
+ `[x402] network "${network}" does not match chainId ${chainId} (${inferredNetwork})`
443
+ );
444
+ }
445
+ const resolvedNetwork = network ?? inferredNetwork ?? "ethereum-sepolia";
446
+ let nonceStore;
447
+ if (externalNonceStore) {
448
+ nonceStore = externalNonceStore;
449
+ } else {
450
+ if (process.env.NODE_ENV === "production") {
451
+ console.warn(
452
+ "[x402] WARNING: Using InMemoryNonceStore in production. This does not survive process restarts and is unsafe for multi-instance deployments. Pass a RedisNonceStore (or compatible NonceStore) via the `nonceStore` option."
453
+ );
454
+ }
455
+ const { InMemoryNonceStore: InMemoryNonceStore2 } = (init_nonce_store(), __toCommonJS(nonce_store_exports));
456
+ nonceStore = new InMemoryNonceStore2();
457
+ }
458
+ const rateAtomicUnits = parseUnits(ratePerCall, 6);
459
+ const chainEnv = getChainEnvironment(chainId);
460
+ const contracts = chainEnv.contracts;
461
+ const payToAddress = payTo || contracts.feeCollector || "0x0000000000000000000000000000000000000000";
462
+ const chain = CHAIN_MAP[chainId] || sepolia;
463
+ const publicClient = createPublicClient({
464
+ chain,
465
+ transport: http(rpcUrl || chainEnv.services.rpcUrl || chain.rpcUrls.default.http[0])
466
+ });
467
+ return async (req, res, next) => {
468
+ const paymentHeader = req.headers["x-payment"];
469
+ if (!paymentHeader) {
470
+ const x402Response = {
471
+ ok: false,
472
+ version: X402_VERSION,
473
+ accepts: [
474
+ {
475
+ scheme: X402_SCHEME,
476
+ network: resolvedNetwork,
477
+ maxAmountRequired: rateAtomicUnits.toString(),
478
+ resource: `${req.protocol}://${req.get("host")}${req.originalUrl}`,
479
+ description: `Premium API access \u2014 Plan: ${planId}`,
480
+ mimeType: "application/json",
481
+ payTo: payToAddress
482
+ }
483
+ ],
484
+ error: "Payment required for this resource",
485
+ rejectionReason: "MISSING_PAYMENT_HEADER"
486
+ };
487
+ res.status(402).json(x402Response);
488
+ return;
489
+ }
490
+ try {
491
+ let paymentData;
492
+ try {
493
+ const normalizedHeader = paymentHeader.trim();
494
+ if (!/^[A-Za-z0-9+/=]+$/.test(normalizedHeader)) {
495
+ sendRejection(
496
+ res,
497
+ "INVALID_PAYMENT_HEADER_ENCODING",
498
+ "X-PAYMENT header must be valid base64-encoded JSON"
499
+ );
500
+ return;
501
+ }
502
+ const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
503
+ paymentData = JSON.parse(decoded);
504
+ } catch {
505
+ sendRejection(
506
+ res,
507
+ "INVALID_PAYMENT_HEADER_FORMAT",
508
+ "X-PAYMENT header must decode to a valid JSON object"
509
+ );
510
+ return;
511
+ }
512
+ const { signature, payload: payloadRaw } = paymentData;
513
+ if (typeof signature !== "string" || !signature.startsWith("0x") || !payloadRaw) {
514
+ sendRejection(
515
+ res,
516
+ "MALFORMED_PAYMENT_PAYLOAD",
517
+ "Payment must include valid signature and payload fields"
518
+ );
519
+ return;
520
+ }
521
+ let payload;
522
+ try {
523
+ const parsedPayload = typeof payloadRaw === "string" ? JSON.parse(payloadRaw) : payloadRaw;
524
+ if (!parsedPayload || typeof parsedPayload !== "object" || Array.isArray(parsedPayload)) {
525
+ throw new Error("payload must be an object");
526
+ }
527
+ payload = parsedPayload;
528
+ } catch {
529
+ sendRejection(
530
+ res,
531
+ "MALFORMED_PAYMENT_PAYLOAD",
532
+ "Payment payload must be a valid JSON object"
533
+ );
534
+ return;
535
+ }
536
+ const signerClaimed = typeof payload.from === "string" ? payload.from : "";
537
+ if (!isAddress(signerClaimed)) {
538
+ sendRejection(
539
+ res,
540
+ "MALFORMED_PAYMENT_PAYLOAD",
541
+ 'Payment payload is missing a valid "from" address'
542
+ );
543
+ return;
544
+ }
545
+ const requestedAmount = normalizeAmount(payload.amount, rateAtomicUnits);
546
+ const sessionId = normalizeSessionId(payload.sessionId);
547
+ const expectedResource = normalizeResourceUrl(buildRequestResource(req));
548
+ const paymentResourceRaw = typeof payload.resource === "string" ? payload.resource : "";
549
+ const paymentResource = normalizeResourceUrl(paymentResourceRaw);
550
+ if (!expectedResource || !paymentResource || paymentResource !== expectedResource) {
551
+ sendRejection(
552
+ res,
553
+ "RESOURCE_MISMATCH",
554
+ "Payment resource does not match the requested endpoint",
555
+ {
556
+ paymentResource: paymentResourceRaw || null,
557
+ expectedResource: expectedResource || null
558
+ }
559
+ );
560
+ return;
561
+ }
562
+ if (requestedAmount < rateAtomicUnits) {
563
+ sendRejection(
564
+ res,
565
+ "PAYMENT_AMOUNT_TOO_LOW",
566
+ "Payment amount is below the required endpoint rate",
567
+ {
568
+ providedAmount: requestedAmount.toString(),
569
+ requiredAmount: rateAtomicUnits.toString()
570
+ }
571
+ );
572
+ return;
573
+ }
574
+ let recoveredSigner;
575
+ try {
576
+ recoveredSigner = await recoverTypedDataAddress({
577
+ domain: { ...PAYMENT_DOMAIN, chainId: BigInt(chainId) },
578
+ types: PAYMENT_TYPES,
579
+ primaryType: "Payment",
580
+ message: {
581
+ from: signerClaimed,
582
+ amount: requestedAmount,
583
+ resource: String(payload.resource || ""),
584
+ sessionId,
585
+ nonce: normalizeNonce(payload.nonce),
586
+ timestamp: normalizeTimestamp(payload.timestamp)
587
+ },
588
+ signature
589
+ });
590
+ } catch {
591
+ sendRejection(
592
+ res,
593
+ "INVALID_SIGNATURE",
594
+ "Unable to validate the payment signature"
595
+ );
596
+ return;
597
+ }
598
+ if (recoveredSigner.toLowerCase() !== signerClaimed.toLowerCase()) {
599
+ sendRejection(
600
+ res,
601
+ "SIGNER_MISMATCH",
602
+ "Recovered signer does not match the payment sender"
603
+ );
604
+ return;
605
+ }
606
+ const verifiedSigner = recoveredSigner;
607
+ const rawPaymentTimestamp = normalizeTimestamp(payload.timestamp);
608
+ const paymentTimestamp = normalizePaymentTimestampMs(rawPaymentTimestamp);
609
+ const paymentAgeMs = Date.now() - Number(paymentTimestamp);
610
+ if (paymentAgeMs > maxPaymentAgeMs || paymentAgeMs < -6e4) {
611
+ sendRejection(
612
+ res,
613
+ "PAYMENT_EXPIRED",
614
+ `Payment timestamp is outside the acceptable window (${Math.round(maxPaymentAgeMs / 1e3)}s)`,
615
+ {
616
+ rawPaymentTimestamp: rawPaymentTimestamp.toString(),
617
+ paymentTimestamp: paymentTimestamp.toString(),
618
+ serverTime: Date.now().toString()
619
+ }
620
+ );
621
+ return;
622
+ }
623
+ const nonce = normalizeNonce(payload.nonce);
624
+ const nonceKey = `${verifiedSigner.toLowerCase()}:${nonce.toString()}`;
625
+ const isFresh = await nonceStore.markUsed(nonceKey, nonceTtlMs);
626
+ if (!isFresh) {
627
+ sendRejection(
628
+ res,
629
+ "NONCE_ALREADY_USED",
630
+ "This payment nonce has already been used. Use a fresh nonce for each request."
631
+ );
632
+ return;
633
+ }
634
+ if (verifiedSigner && contracts.sessionVault) {
635
+ try {
636
+ const balance = await publicClient.readContract({
637
+ address: contracts.sessionVault,
638
+ abi: SessionVaultABI,
639
+ functionName: "getAgentBalance",
640
+ args: [verifiedSigner]
641
+ });
642
+ if (balance < requestedAmount) {
643
+ sendRejection(
644
+ res,
645
+ "INSUFFICIENT_SESSION_BALANCE",
646
+ "Session balance is below the required amount",
647
+ {
648
+ balance: balance.toString(),
649
+ required: requestedAmount.toString()
650
+ }
651
+ );
652
+ return;
653
+ }
654
+ } catch (err) {
655
+ if (failOpenOnBalanceCheck) {
656
+ console.warn(
657
+ "[x402] Session vault balance check failed (fail-open enabled):",
658
+ err
659
+ );
660
+ } else {
661
+ sendRejection(
662
+ res,
663
+ "BALANCE_CHECK_FAILED",
664
+ "Unable to verify session balance on-chain"
665
+ );
666
+ return;
667
+ }
668
+ }
669
+ }
670
+ let settledAmount = requestedAmount;
671
+ let settlementTxHash = null;
672
+ if (settlePayment) {
673
+ try {
674
+ const settlement = await settlePayment({
675
+ signer: verifiedSigner,
676
+ amount: requestedAmount,
677
+ sessionId,
678
+ planId,
679
+ paymentHeader,
680
+ payload,
681
+ request: req
682
+ });
683
+ settledAmount = coerceBigInt(
684
+ settlement.settledAmount,
685
+ requestedAmount
686
+ );
687
+ settlementTxHash = settlement.settlementTxHash || null;
688
+ } catch (err) {
689
+ sendRejection(
690
+ res,
691
+ "SETTLEMENT_FAILED",
692
+ err?.message || "Payment verification succeeded but settlement failed"
693
+ );
694
+ return;
695
+ }
696
+ }
697
+ const verifiedContext = {
698
+ verified: true,
699
+ signer: verifiedSigner,
700
+ planId,
701
+ ratePerCall,
702
+ amount: requestedAmount.toString(),
703
+ settledAmount: settledAmount.toString(),
704
+ settlementTxHash,
705
+ sessionId,
706
+ paymentHeader,
707
+ timestamp: Date.now(),
708
+ rejectionReason: null
709
+ };
710
+ req.meapPayment = verifiedContext;
711
+ next();
712
+ } catch (error) {
713
+ console.error("[x402] Payment processing error:", error);
714
+ sendRejection(
715
+ res,
716
+ "PAYMENT_PROCESSING_FAILED",
717
+ error.message || "Payment processing failed"
718
+ );
719
+ }
720
+ };
721
+ }
722
+
723
+ // src/services/usage.ts
724
+ import { createHash } from "crypto";
725
+ import fs from "fs";
726
+ import path from "path";
727
+ import { pathToFileURL } from "url";
728
+ import { createPublicClient as createPublicClient2, createWalletClient, http as http2 } from "viem";
729
+ import { privateKeyToAccount } from "viem/accounts";
730
+ import { sepolia as sepolia2, baseSepolia as baseSepolia2, base as base2 } from "viem/chains";
731
+ import {
732
+ DEFAULT_CHAIN_ID,
733
+ ZKUsageVerifierABI,
734
+ getContractAddresses
735
+ } from "@arcenpay/sdk";
736
+ var UsageProofErrorCodes = {
737
+ TOOLING_UNAVAILABLE: "TOOLING_UNAVAILABLE",
738
+ ARTIFACTS_MISSING: "ARTIFACTS_MISSING",
739
+ ARTIFACT_VERSION_MISMATCH: "ARTIFACT_VERSION_MISMATCH",
740
+ INVALID_VERIFICATION_KEY: "INVALID_VERIFICATION_KEY",
741
+ INVALID_INPUT: "INVALID_INPUT",
742
+ PROOF_GENERATION_FAILED: "PROOF_GENERATION_FAILED",
743
+ PROOF_SIGNAL_MISMATCH: "PROOF_SIGNAL_MISMATCH"
744
+ };
745
+ var UsageProofError = class extends Error {
746
+ code;
747
+ cause;
748
+ constructor(code, message, cause) {
749
+ super(message);
750
+ this.name = "UsageProofError";
751
+ this.code = code;
752
+ this.cause = cause;
753
+ }
754
+ };
755
+ var CHAIN_MAP2 = {
756
+ 11155111: sepolia2,
757
+ 84532: baseSepolia2,
758
+ 8453: base2
759
+ };
760
+ var MAX_ENTRIES = 128;
761
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
762
+ var UsageService = class {
763
+ logs = /* @__PURE__ */ new Map();
764
+ config;
765
+ signerAccount;
766
+ publicClient;
767
+ walletClient;
768
+ runtimeValidated = false;
769
+ constructor(config = {}) {
770
+ const chainId = config.chainId ?? DEFAULT_CHAIN_ID;
771
+ const chain = CHAIN_MAP2[chainId] || baseSepolia2;
772
+ const rpcUrl = config.rpcUrl || chain.rpcUrls.default.http[0];
773
+ const proofMode = config.proofMode ?? (process.env.NODE_ENV === "development" ? "dev" : "strict");
774
+ const circuitsRootDir = this.resolveCircuitsRoot(config.circuitsRootDir);
775
+ const buildDir = config.circuitBuildDir || path.join(circuitsRootDir, "build");
776
+ this.config = {
777
+ chainId,
778
+ rpcUrl,
779
+ proofMode,
780
+ circuitsRootDir,
781
+ circuitBuildDir: buildDir,
782
+ circuitWasmPath: config.circuitWasmPath || path.join(buildDir, "UsageBilling_js", "UsageBilling.wasm"),
783
+ zkeyPath: config.zkeyPath || path.join(buildDir, "circuit_final.zkey"),
784
+ verificationKeyPath: config.verificationKeyPath || path.join(buildDir, "verification_key.json"),
785
+ proveScriptPath: config.proveScriptPath || path.join(circuitsRootDir, "scripts", "prove.js"),
786
+ artifactVersion: config.artifactVersion || process.env.MEAP_PROOF_ARTIFACT_VERSION || "",
787
+ artifactManifestPath: config.artifactManifestPath || process.env.MEAP_PROOF_ARTIFACT_MANIFEST_PATH || path.join(circuitsRootDir, "artifact-manifest.json"),
788
+ signerPrivateKey: config.signerPrivateKey
789
+ };
790
+ if (config.signerPrivateKey) {
791
+ const key = config.signerPrivateKey.startsWith("0x") ? config.signerPrivateKey : `0x${config.signerPrivateKey}`;
792
+ this.signerAccount = privateKeyToAccount(key);
793
+ } else {
794
+ this.signerAccount = null;
795
+ }
796
+ this.publicClient = createPublicClient2({
797
+ chain,
798
+ transport: http2(rpcUrl)
799
+ });
800
+ if (this.signerAccount) {
801
+ this.walletClient = createWalletClient({
802
+ account: this.signerAccount,
803
+ chain,
804
+ transport: http2(rpcUrl)
805
+ });
806
+ } else {
807
+ this.walletClient = null;
808
+ }
809
+ if (this.config.proofMode === "strict") {
810
+ this.assertProofRuntimeReady();
811
+ }
812
+ }
813
+ getProofMode() {
814
+ return this.config.proofMode;
815
+ }
816
+ getProofRuntimeConfig() {
817
+ return {
818
+ proofMode: this.config.proofMode,
819
+ proveScriptPath: this.config.proveScriptPath,
820
+ circuitWasmPath: this.config.circuitWasmPath,
821
+ zkeyPath: this.config.zkeyPath,
822
+ verificationKeyPath: this.config.verificationKeyPath,
823
+ artifactVersion: this.config.artifactVersion,
824
+ artifactManifestPath: this.config.artifactManifestPath
825
+ };
826
+ }
827
+ assertProofRuntimeReady() {
828
+ this.assertProofTooling();
829
+ this.assertProofArtifacts();
830
+ this.assertVerificationKey();
831
+ this.runtimeValidated = true;
832
+ }
833
+ async logUsage(sessionId, callData) {
834
+ const message = JSON.stringify({
835
+ sessionId,
836
+ endpoint: callData.endpoint,
837
+ method: callData.method,
838
+ timestamp: callData.timestamp,
839
+ responseSize: callData.responseSize ?? 0
840
+ });
841
+ let signature = "";
842
+ if (this.signerAccount) {
843
+ try {
844
+ signature = await this.signerAccount.signMessage({ message });
845
+ } catch (err) {
846
+ console.error("[UsageService] Failed to sign log entry:", err);
847
+ signature = createHash("sha256").update(message).digest("hex");
848
+ }
849
+ } else {
850
+ signature = createHash("sha256").update(message).digest("hex");
851
+ }
852
+ const entry = { sessionId, callData, signature };
853
+ const existing = this.logs.get(sessionId) || [];
854
+ existing.push(entry);
855
+ this.logs.set(sessionId, existing);
856
+ }
857
+ getUsageCount(sessionId) {
858
+ return (this.logs.get(sessionId) || []).length;
859
+ }
860
+ getAllSessions() {
861
+ return Array.from(this.logs.keys());
862
+ }
863
+ aggregateUsage(sessionId, windowEnd) {
864
+ const logs = this.logs.get(sessionId) || [];
865
+ const windowLogs = logs.filter((entry) => entry.callData.timestamp <= windowEnd);
866
+ const windowStart = windowLogs.length > 0 ? Math.min(...windowLogs.map((entry) => entry.callData.timestamp)) : 0;
867
+ return {
868
+ sessionId,
869
+ callCount: windowLogs.length,
870
+ windowStart,
871
+ windowEnd,
872
+ logs: windowLogs
873
+ };
874
+ }
875
+ async generateProof(sessionId, windowEnd) {
876
+ const aggregation = this.aggregateUsage(sessionId, windowEnd);
877
+ if (aggregation.callCount <= 0) {
878
+ throw new UsageProofError(
879
+ UsageProofErrorCodes.INVALID_INPUT,
880
+ `No usage entries available for session ${sessionId}.`
881
+ );
882
+ }
883
+ if (aggregation.callCount > MAX_ENTRIES) {
884
+ throw new UsageProofError(
885
+ UsageProofErrorCodes.INVALID_INPUT,
886
+ `Usage batch too large (${aggregation.callCount} > ${MAX_ENTRIES}).`
887
+ );
888
+ }
889
+ if (aggregation.windowEnd <= aggregation.windowStart) {
890
+ throw new UsageProofError(
891
+ UsageProofErrorCodes.INVALID_INPUT,
892
+ "Billing window end must be greater than window start."
893
+ );
894
+ }
895
+ const sessionIdBytes32 = this.normalizeBytes32(sessionId);
896
+ const agentAddress = this.signerAccount?.address || ZERO_ADDRESS;
897
+ const agentAddressBytes32 = this.addressToBytes32(agentAddress);
898
+ const logHashes = aggregation.logs.map((log) => this.hashUsageLog(log));
899
+ const logTimestamps = aggregation.logs.map(
900
+ (log) => String(log.callData.timestamp)
901
+ );
902
+ const circuitInput = {
903
+ agentAddress,
904
+ sessionId: sessionIdBytes32,
905
+ callCount: aggregation.callCount,
906
+ windowStart: aggregation.windowStart,
907
+ windowEnd: aggregation.windowEnd,
908
+ logHashes,
909
+ logTimestamps
910
+ };
911
+ let proofResult = null;
912
+ let publicSignals = null;
913
+ try {
914
+ this.assertProofRuntimeReady();
915
+ const prover = await this.loadCircuitProver();
916
+ proofResult = await prover.prove(circuitInput);
917
+ publicSignals = proofResult.publicSignals;
918
+ this.assertPublicSignals(publicSignals, aggregation, agentAddress, sessionIdBytes32, prover);
919
+ } catch (err) {
920
+ if (this.config.proofMode === "strict") {
921
+ throw this.wrapProofError(err);
922
+ }
923
+ console.warn(
924
+ "[UsageService] Proof generation unavailable in dev mode, using zero-proof envelope:",
925
+ err
926
+ );
927
+ }
928
+ const proof = proofResult ? {
929
+ a: [
930
+ BigInt(proofResult.proof.pi_a[0]),
931
+ BigInt(proofResult.proof.pi_a[1])
932
+ ],
933
+ b: [
934
+ [
935
+ BigInt(proofResult.proof.pi_b[0][1]),
936
+ BigInt(proofResult.proof.pi_b[0][0])
937
+ ],
938
+ [
939
+ BigInt(proofResult.proof.pi_b[1][1]),
940
+ BigInt(proofResult.proof.pi_b[1][0])
941
+ ]
942
+ ],
943
+ c: [
944
+ BigInt(proofResult.proof.pi_c[0]),
945
+ BigInt(proofResult.proof.pi_c[1])
946
+ ]
947
+ } : {
948
+ a: [0n, 0n],
949
+ b: [
950
+ [0n, 0n],
951
+ [0n, 0n]
952
+ ],
953
+ c: [0n, 0n]
954
+ };
955
+ const fallbackMerkleRoot = this.normalizeBytes32(logHashes.join(","));
956
+ const fallbackNullifier = this.normalizeBytes32(
957
+ `${sessionIdBytes32}:${aggregation.windowEnd}`
958
+ );
959
+ const publicInputs = publicSignals ? {
960
+ agentAddress: this.fieldToBytes32(publicSignals[0]),
961
+ sessionId: this.fieldToBytes32(publicSignals[1]),
962
+ callCount: BigInt(publicSignals[2]),
963
+ windowStart: BigInt(publicSignals[3]),
964
+ windowEnd: BigInt(publicSignals[4]),
965
+ merkleRoot: this.fieldToBytes32(publicSignals[5]),
966
+ nullifier: this.fieldToBytes32(publicSignals[6])
967
+ } : {
968
+ agentAddress: agentAddressBytes32,
969
+ sessionId: sessionIdBytes32,
970
+ callCount: BigInt(aggregation.callCount),
971
+ windowStart: BigInt(aggregation.windowStart),
972
+ windowEnd: BigInt(aggregation.windowEnd),
973
+ merkleRoot: fallbackMerkleRoot,
974
+ nullifier: fallbackNullifier
975
+ };
976
+ return { proof, publicInputs };
977
+ }
978
+ async submitProof(proof, publicInputs) {
979
+ if (!this.walletClient) {
980
+ throw new Error(
981
+ "UsageService: Cannot submit proof \u2014 no signer private key configured. Set signerPrivateKey in UsageServiceConfig."
982
+ );
983
+ }
984
+ const chainId = this.config.chainId ?? DEFAULT_CHAIN_ID;
985
+ const contracts = getContractAddresses(chainId);
986
+ const verifierAddress = contracts.zkUsageVerifier;
987
+ if (!verifierAddress || verifierAddress === "0x0000000000000000000000000000000000000000") {
988
+ throw new Error(
989
+ "UsageService: ZKUsageVerifier contract address not configured for this chain"
990
+ );
991
+ }
992
+ const onChainProof = {
993
+ a: proof.a,
994
+ b: proof.b,
995
+ c: proof.c
996
+ };
997
+ const onChainInputs = {
998
+ agentAddress: publicInputs.agentAddress,
999
+ sessionId: publicInputs.sessionId,
1000
+ callCount: publicInputs.callCount,
1001
+ windowStart: publicInputs.windowStart,
1002
+ windowEnd: publicInputs.windowEnd,
1003
+ merkleRoot: publicInputs.merkleRoot,
1004
+ nullifier: publicInputs.nullifier
1005
+ };
1006
+ const txHash = await this.walletClient.writeContract({
1007
+ address: verifierAddress,
1008
+ abi: ZKUsageVerifierABI,
1009
+ functionName: "submitProof",
1010
+ args: [onChainProof, onChainInputs]
1011
+ });
1012
+ const receipt = await this.publicClient.waitForTransactionReceipt({
1013
+ hash: txHash
1014
+ });
1015
+ return {
1016
+ sessionId: publicInputs.sessionId,
1017
+ agentAddress: publicInputs.agentAddress,
1018
+ callCount: publicInputs.callCount,
1019
+ settlementAmount: 0n,
1020
+ windowStart: publicInputs.windowStart,
1021
+ windowEnd: publicInputs.windowEnd,
1022
+ transactionHash: receipt.transactionHash
1023
+ };
1024
+ }
1025
+ clearLogs(sessionId, windowEnd) {
1026
+ const logs = this.logs.get(sessionId) || [];
1027
+ this.logs.set(
1028
+ sessionId,
1029
+ logs.filter((entry) => entry.callData.timestamp > windowEnd)
1030
+ );
1031
+ }
1032
+ resolveCircuitsRoot(explicitRoot) {
1033
+ if (explicitRoot) return path.resolve(explicitRoot);
1034
+ const candidates = [
1035
+ process.env.MEAP_CIRCUITS_DIR,
1036
+ path.resolve(process.cwd(), "circuits"),
1037
+ path.resolve(process.cwd(), "..", "..", "circuits"),
1038
+ path.resolve(__dirname, "../../../../../circuits")
1039
+ ].filter(Boolean);
1040
+ for (const candidate of candidates) {
1041
+ if (fs.existsSync(path.join(candidate, "UsageBilling.circom"))) {
1042
+ return candidate;
1043
+ }
1044
+ }
1045
+ return path.resolve(process.cwd(), "circuits");
1046
+ }
1047
+ assertProofTooling() {
1048
+ if (!fs.existsSync(this.config.proveScriptPath)) {
1049
+ throw new UsageProofError(
1050
+ UsageProofErrorCodes.TOOLING_UNAVAILABLE,
1051
+ `Circuit prover script missing: ${this.config.proveScriptPath}`
1052
+ );
1053
+ }
1054
+ }
1055
+ assertProofArtifacts() {
1056
+ const required = [
1057
+ this.config.circuitWasmPath,
1058
+ this.config.zkeyPath,
1059
+ this.config.verificationKeyPath
1060
+ ];
1061
+ const missing = required.filter((artifactPath) => !fs.existsSync(artifactPath));
1062
+ if (missing.length > 0) {
1063
+ throw new UsageProofError(
1064
+ UsageProofErrorCodes.ARTIFACTS_MISSING,
1065
+ `Missing proof artifact(s): ${missing.join(", ")}`
1066
+ );
1067
+ }
1068
+ this.assertArtifactVersion();
1069
+ }
1070
+ assertArtifactVersion() {
1071
+ const expectedVersion = this.config.artifactVersion?.trim();
1072
+ const manifestPath = this.config.artifactManifestPath;
1073
+ if (!fs.existsSync(manifestPath)) {
1074
+ if (expectedVersion && this.config.proofMode === "strict") {
1075
+ throw new UsageProofError(
1076
+ UsageProofErrorCodes.ARTIFACTS_MISSING,
1077
+ `Artifact version set (${expectedVersion}) but manifest missing: ${manifestPath}`
1078
+ );
1079
+ }
1080
+ return;
1081
+ }
1082
+ let manifestVersion = "";
1083
+ try {
1084
+ const raw = fs.readFileSync(manifestPath, "utf8");
1085
+ const parsed = JSON.parse(raw);
1086
+ manifestVersion = String(parsed.version || "").trim();
1087
+ } catch (err) {
1088
+ throw new UsageProofError(
1089
+ UsageProofErrorCodes.ARTIFACTS_MISSING,
1090
+ `Invalid artifact manifest JSON: ${manifestPath}`,
1091
+ err
1092
+ );
1093
+ }
1094
+ if (!expectedVersion) return;
1095
+ if (!manifestVersion || manifestVersion !== expectedVersion) {
1096
+ throw new UsageProofError(
1097
+ UsageProofErrorCodes.ARTIFACT_VERSION_MISMATCH,
1098
+ `Artifact version mismatch (expected ${expectedVersion}, got ${manifestVersion || "missing"})`
1099
+ );
1100
+ }
1101
+ }
1102
+ assertVerificationKey() {
1103
+ try {
1104
+ const raw = fs.readFileSync(this.config.verificationKeyPath, "utf8");
1105
+ const vkey = JSON.parse(raw);
1106
+ if (vkey.protocol !== "groth16") {
1107
+ throw new Error(`Unsupported protocol: ${String(vkey.protocol || "unknown")}`);
1108
+ }
1109
+ if (typeof vkey.nPublic === "number" && vkey.nPublic !== 7) {
1110
+ throw new Error(`Expected 7 public inputs, received ${vkey.nPublic}`);
1111
+ }
1112
+ } catch (err) {
1113
+ throw new UsageProofError(
1114
+ UsageProofErrorCodes.INVALID_VERIFICATION_KEY,
1115
+ `Invalid verification key: ${this.config.verificationKeyPath}`,
1116
+ err
1117
+ );
1118
+ }
1119
+ }
1120
+ wrapProofError(err) {
1121
+ if (err instanceof UsageProofError) {
1122
+ return err;
1123
+ }
1124
+ return new UsageProofError(
1125
+ UsageProofErrorCodes.PROOF_GENERATION_FAILED,
1126
+ "Failed to generate Groth16 proof.",
1127
+ err
1128
+ );
1129
+ }
1130
+ async loadCircuitProver() {
1131
+ const moduleUrl = pathToFileURL(this.config.proveScriptPath).href;
1132
+ const mod = await import(moduleUrl);
1133
+ if (typeof mod.prove !== "function") {
1134
+ throw new UsageProofError(
1135
+ UsageProofErrorCodes.TOOLING_UNAVAILABLE,
1136
+ "Circuit prover module does not export `prove()`."
1137
+ );
1138
+ }
1139
+ return mod;
1140
+ }
1141
+ assertPublicSignals(actual, aggregation, agentAddress, sessionIdBytes32, prover) {
1142
+ if (actual.length !== 7) {
1143
+ throw new UsageProofError(
1144
+ UsageProofErrorCodes.PROOF_SIGNAL_MISMATCH,
1145
+ `Public signal length mismatch (${actual.length} !== 7).`
1146
+ );
1147
+ }
1148
+ const expectedAgentField = BigInt(agentAddress).toString();
1149
+ const expectedSessionField = typeof prover.sessionIdToField === "function" ? prover.sessionIdToField(sessionIdBytes32) : this.toFieldElement(sessionIdBytes32);
1150
+ if (BigInt(actual[0]) !== BigInt(expectedAgentField)) {
1151
+ throw new UsageProofError(
1152
+ UsageProofErrorCodes.PROOF_SIGNAL_MISMATCH,
1153
+ "Public signal mismatch at index 0 (agentAddress)."
1154
+ );
1155
+ }
1156
+ if (BigInt(actual[1]) !== BigInt(expectedSessionField)) {
1157
+ throw new UsageProofError(
1158
+ UsageProofErrorCodes.PROOF_SIGNAL_MISMATCH,
1159
+ "Public signal mismatch at index 1 (sessionId)."
1160
+ );
1161
+ }
1162
+ if (BigInt(actual[2]) !== BigInt(aggregation.callCount)) {
1163
+ throw new UsageProofError(
1164
+ UsageProofErrorCodes.PROOF_SIGNAL_MISMATCH,
1165
+ "Public signal mismatch at index 2 (callCount)."
1166
+ );
1167
+ }
1168
+ if (BigInt(actual[3]) !== BigInt(aggregation.windowStart)) {
1169
+ throw new UsageProofError(
1170
+ UsageProofErrorCodes.PROOF_SIGNAL_MISMATCH,
1171
+ "Public signal mismatch at index 3 (windowStart)."
1172
+ );
1173
+ }
1174
+ if (BigInt(actual[4]) !== BigInt(aggregation.windowEnd)) {
1175
+ throw new UsageProofError(
1176
+ UsageProofErrorCodes.PROOF_SIGNAL_MISMATCH,
1177
+ "Public signal mismatch at index 4 (windowEnd)."
1178
+ );
1179
+ }
1180
+ }
1181
+ hashUsageLog(log) {
1182
+ const digest = createHash("sha256").update(JSON.stringify({ ...log.callData, sig: log.signature })).digest("hex");
1183
+ return `0x${digest}`;
1184
+ }
1185
+ normalizeBytes32(value) {
1186
+ if (/^0x[a-fA-F0-9]{64}$/.test(value)) {
1187
+ return value.toLowerCase();
1188
+ }
1189
+ const hash = createHash("sha256").update(value).digest("hex");
1190
+ return `0x${hash}`;
1191
+ }
1192
+ addressToBytes32(address) {
1193
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
1194
+ throw new UsageProofError(
1195
+ UsageProofErrorCodes.INVALID_INPUT,
1196
+ `Invalid address: ${address}`
1197
+ );
1198
+ }
1199
+ return `0x${address.slice(2).padStart(64, "0")}`.toLowerCase();
1200
+ }
1201
+ fieldToBytes32(field) {
1202
+ const value = BigInt(field);
1203
+ if (value < 0n || value >= 2n ** 256n) {
1204
+ throw new UsageProofError(
1205
+ UsageProofErrorCodes.INVALID_INPUT,
1206
+ `Field value out of bytes32 range: ${field}`
1207
+ );
1208
+ }
1209
+ return `0x${value.toString(16).padStart(64, "0")}`.toLowerCase();
1210
+ }
1211
+ toFieldElement(input) {
1212
+ const hex = Buffer.isBuffer(input) ? input.toString("hex") : input.replace(/^0x/, "");
1213
+ const normalized = hex.length === 0 ? "0" : hex;
1214
+ const truncated = normalized.slice(0, 62);
1215
+ return BigInt(`0x${truncated || "0"}`).toString();
1216
+ }
1217
+ };
1218
+
1219
+ // src/services/settlement.ts
1220
+ import { createPublicClient as createPublicClient3, http as http3 } from "viem";
1221
+ import { sepolia as sepolia3, baseSepolia as baseSepolia3, base as base3 } from "viem/chains";
1222
+ import {
1223
+ DEFAULT_CHAIN_ID as DEFAULT_CHAIN_ID2,
1224
+ SessionVaultABI as SessionVaultABI2,
1225
+ getContractAddresses as getContractAddresses2
1226
+ } from "@arcenpay/sdk";
1227
+ var CHAIN_MAP3 = {
1228
+ 11155111: sepolia3,
1229
+ 84532: baseSepolia3,
1230
+ 8453: base3
1231
+ };
1232
+ var SettlementService = class {
1233
+ rpcUrl;
1234
+ chainId;
1235
+ zkVerifierAddress;
1236
+ sessionVaultAddress;
1237
+ watchers = [];
1238
+ pollInterval = null;
1239
+ lastBlock = 0n;
1240
+ publicClient;
1241
+ constructor(config) {
1242
+ this.rpcUrl = config.rpcUrl;
1243
+ this.chainId = config.chainId ?? DEFAULT_CHAIN_ID2;
1244
+ const contracts = getContractAddresses2(this.chainId);
1245
+ this.zkVerifierAddress = config.zkVerifierAddress || contracts.zkUsageVerifier;
1246
+ this.sessionVaultAddress = config.sessionVaultAddress || contracts.sessionVault;
1247
+ const chain = CHAIN_MAP3[this.chainId] || baseSepolia3;
1248
+ this.publicClient = createPublicClient3({
1249
+ chain,
1250
+ transport: http3(this.rpcUrl)
1251
+ });
1252
+ }
1253
+ /**
1254
+ * Queries the SessionVault for remaining balance
1255
+ */
1256
+ async getSessionBalance(agentAddress) {
1257
+ try {
1258
+ const balance = await this.publicClient.readContract({
1259
+ address: this.sessionVaultAddress,
1260
+ abi: SessionVaultABI2,
1261
+ functionName: "getAgentBalance",
1262
+ args: [agentAddress]
1263
+ });
1264
+ return balance;
1265
+ } catch (err) {
1266
+ console.error("[SettlementService] Balance query failed:", err);
1267
+ return 0n;
1268
+ }
1269
+ }
1270
+ /**
1271
+ * Gets details for a specific session
1272
+ */
1273
+ async getSessionDetails(sessionId) {
1274
+ try {
1275
+ const session = await this.publicClient.readContract({
1276
+ address: this.sessionVaultAddress,
1277
+ abi: SessionVaultABI2,
1278
+ functionName: "getSession",
1279
+ args: [sessionId]
1280
+ });
1281
+ return session;
1282
+ } catch (err) {
1283
+ console.error("[SettlementService] Session query failed:", err);
1284
+ return null;
1285
+ }
1286
+ }
1287
+ /**
1288
+ * Subscribes to on-chain BillingSettled events
1289
+ */
1290
+ watchSettlements(callback, intervalMs = 1e4) {
1291
+ this.watchers.push(callback);
1292
+ if (!this.pollInterval) {
1293
+ this.initBlockNumber().then(() => {
1294
+ this.pollInterval = setInterval(async () => {
1295
+ await this.pollSettlements();
1296
+ }, intervalMs);
1297
+ });
1298
+ }
1299
+ return () => {
1300
+ this.watchers = this.watchers.filter((w) => w !== callback);
1301
+ if (this.watchers.length === 0 && this.pollInterval) {
1302
+ clearInterval(this.pollInterval);
1303
+ this.pollInterval = null;
1304
+ }
1305
+ };
1306
+ }
1307
+ /**
1308
+ * Initialize the starting block number for event polling
1309
+ */
1310
+ async initBlockNumber() {
1311
+ try {
1312
+ this.lastBlock = await this.publicClient.getBlockNumber();
1313
+ } catch {
1314
+ this.lastBlock = 0n;
1315
+ }
1316
+ }
1317
+ /**
1318
+ * Polls for new BillingSettled events from ZKUsageVerifier
1319
+ */
1320
+ async pollSettlements() {
1321
+ try {
1322
+ const currentBlock = await this.publicClient.getBlockNumber();
1323
+ if (currentBlock <= this.lastBlock) return;
1324
+ const logs = await this.publicClient.getLogs({
1325
+ address: this.zkVerifierAddress,
1326
+ event: {
1327
+ type: "event",
1328
+ name: "BillingSettled",
1329
+ inputs: [
1330
+ { name: "sessionId", type: "bytes32", indexed: true },
1331
+ { name: "agentAddress", type: "bytes32", indexed: true },
1332
+ { name: "callCount", type: "uint256", indexed: false },
1333
+ { name: "settlementAmount", type: "uint256", indexed: false },
1334
+ { name: "windowStart", type: "uint64", indexed: false },
1335
+ { name: "windowEnd", type: "uint64", indexed: false }
1336
+ ]
1337
+ },
1338
+ fromBlock: this.lastBlock + 1n,
1339
+ toBlock: currentBlock
1340
+ });
1341
+ for (const log of logs) {
1342
+ const settlement = {
1343
+ sessionId: log.args.sessionId,
1344
+ agentAddress: log.args.agentAddress,
1345
+ callCount: log.args.callCount,
1346
+ settlementAmount: log.args.settlementAmount,
1347
+ windowStart: log.args.windowStart,
1348
+ windowEnd: log.args.windowEnd,
1349
+ transactionHash: log.transactionHash
1350
+ };
1351
+ this.watchers.forEach((cb) => cb(settlement));
1352
+ }
1353
+ this.lastBlock = currentBlock;
1354
+ } catch (err) {
1355
+ console.error("[SettlementService] Polling error:", err);
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Stops all settlement watchers
1360
+ */
1361
+ stop() {
1362
+ if (this.pollInterval) {
1363
+ clearInterval(this.pollInterval);
1364
+ this.pollInterval = null;
1365
+ }
1366
+ this.watchers = [];
1367
+ }
1368
+ };
1369
+
1370
+ // src/services/settlement-writer.ts
1371
+ import { createPublicClient as createPublicClient4, createWalletClient as createWalletClient2, http as http4, isAddress as isAddress2 } from "viem";
1372
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
1373
+ import { sepolia as sepolia4, baseSepolia as baseSepolia4, base as base4 } from "viem/chains";
1374
+ import {
1375
+ DEFAULT_CHAIN_ID as DEFAULT_CHAIN_ID3,
1376
+ SessionVaultABI as SessionVaultABI3,
1377
+ FeeCollectorABI,
1378
+ getContractAddresses as getContractAddresses3
1379
+ } from "@arcenpay/sdk";
1380
+ var CHAIN_MAP4 = {
1381
+ 11155111: sepolia4,
1382
+ 84532: baseSepolia4,
1383
+ 8453: base4
1384
+ };
1385
+ function isBytes32(value) {
1386
+ return /^0x[a-fA-F0-9]{64}$/.test(value);
1387
+ }
1388
+ function normalizeSession(raw) {
1389
+ const value = raw;
1390
+ const agent = value?.agent ?? value?.[0] ?? "0x0000000000000000000000000000000000000000";
1391
+ const token = value?.token ?? value?.[1] ?? "0x0000000000000000000000000000000000000000";
1392
+ const balance = BigInt(value?.balance ?? value?.[2] ?? 0n);
1393
+ const totalFunded = BigInt(value?.totalFunded ?? value?.[3] ?? 0n);
1394
+ const totalSettled = BigInt(value?.totalSettled ?? value?.[4] ?? 0n);
1395
+ const active = Boolean(value?.active ?? value?.[5] ?? false);
1396
+ const createdAt = BigInt(value?.createdAt ?? value?.[6] ?? 0n);
1397
+ return {
1398
+ agent,
1399
+ token,
1400
+ balance,
1401
+ totalFunded,
1402
+ totalSettled,
1403
+ active,
1404
+ createdAt
1405
+ };
1406
+ }
1407
+ var SettlementWriterService = class {
1408
+ chainId;
1409
+ sessionVaultAddress;
1410
+ feeCollectorAddress;
1411
+ providerAddress;
1412
+ signerAddress;
1413
+ waitForReceipt;
1414
+ publicClient;
1415
+ walletClient;
1416
+ constructor(config) {
1417
+ this.chainId = config.chainId ?? DEFAULT_CHAIN_ID3;
1418
+ const chain = CHAIN_MAP4[this.chainId] || baseSepolia4;
1419
+ const contracts = getContractAddresses3(this.chainId);
1420
+ this.sessionVaultAddress = config.sessionVaultAddress || contracts.sessionVault;
1421
+ this.feeCollectorAddress = config.feeCollectorAddress || contracts.feeCollector;
1422
+ const account = privateKeyToAccount2(config.privateKey);
1423
+ this.signerAddress = account.address;
1424
+ this.providerAddress = config.providerAddress || account.address;
1425
+ this.waitForReceipt = config.waitForReceipt ?? true;
1426
+ if (!isAddress2(this.sessionVaultAddress)) {
1427
+ throw new Error("Invalid SessionVault address for SettlementWriterService");
1428
+ }
1429
+ if (!isAddress2(this.providerAddress)) {
1430
+ throw new Error("Invalid provider address for SettlementWriterService");
1431
+ }
1432
+ this.publicClient = createPublicClient4({
1433
+ chain,
1434
+ transport: http4(config.rpcUrl || chain.rpcUrls.default.http[0])
1435
+ });
1436
+ this.walletClient = createWalletClient2({
1437
+ account,
1438
+ chain,
1439
+ transport: http4(config.rpcUrl || chain.rpcUrls.default.http[0])
1440
+ });
1441
+ }
1442
+ getSignerAddress() {
1443
+ return this.signerAddress;
1444
+ }
1445
+ getSessionVaultAddress() {
1446
+ return this.sessionVaultAddress;
1447
+ }
1448
+ async isSignerAuthorized(address = this.signerAddress) {
1449
+ if (!isAddress2(address)) {
1450
+ return false;
1451
+ }
1452
+ const authorized = await this.publicClient.readContract({
1453
+ address: this.sessionVaultAddress,
1454
+ abi: SessionVaultABI3,
1455
+ functionName: "authorizedSettlers",
1456
+ args: [address]
1457
+ });
1458
+ return Boolean(authorized);
1459
+ }
1460
+ async getSession(sessionId) {
1461
+ const rawSession = await this.publicClient.readContract({
1462
+ address: this.sessionVaultAddress,
1463
+ abi: SessionVaultABI3,
1464
+ functionName: "getSession",
1465
+ args: [sessionId]
1466
+ });
1467
+ return normalizeSession(rawSession);
1468
+ }
1469
+ async resolveSessionId(sessionIdHint, signer) {
1470
+ const trimmedHint = sessionIdHint.trim();
1471
+ if (isBytes32(trimmedHint)) {
1472
+ return trimmedHint;
1473
+ }
1474
+ const sessionIds = await this.publicClient.readContract({
1475
+ address: this.sessionVaultAddress,
1476
+ abi: SessionVaultABI3,
1477
+ functionName: "getAgentSessions",
1478
+ args: [signer]
1479
+ });
1480
+ if (!sessionIds.length) {
1481
+ throw new Error("No billing session found for signer");
1482
+ }
1483
+ for (let idx = sessionIds.length - 1; idx >= 0; idx -= 1) {
1484
+ const candidate = sessionIds[idx];
1485
+ const session = await this.getSession(candidate);
1486
+ if (session.active && session.balance > 0n) {
1487
+ return candidate;
1488
+ }
1489
+ }
1490
+ throw new Error("No active billing session found for signer");
1491
+ }
1492
+ async settleForSigner(input) {
1493
+ if (!isAddress2(input.signer)) {
1494
+ throw new Error("Invalid signer address for settlement");
1495
+ }
1496
+ if (input.amount <= 0n) {
1497
+ throw new Error("Settlement amount must be greater than zero");
1498
+ }
1499
+ const resolvedSessionId = await this.resolveSessionId(input.sessionIdHint, input.signer);
1500
+ const session = await this.getSession(resolvedSessionId);
1501
+ if (!session.active) {
1502
+ throw new Error("Session is not active");
1503
+ }
1504
+ if (session.agent.toLowerCase() !== input.signer.toLowerCase()) {
1505
+ throw new Error("Session owner does not match payment signer");
1506
+ }
1507
+ if (session.balance < input.amount) {
1508
+ throw new Error(
1509
+ `Session balance too low for settlement (balance=${session.balance.toString()}, required=${input.amount.toString()})`
1510
+ );
1511
+ }
1512
+ const providerAddress = input.providerAddress || this.providerAddress;
1513
+ if (!isAddress2(providerAddress)) {
1514
+ throw new Error("Invalid settlement provider address");
1515
+ }
1516
+ const txHash = await this.walletClient.writeContract({
1517
+ address: this.sessionVaultAddress,
1518
+ abi: SessionVaultABI3,
1519
+ functionName: "settle",
1520
+ args: [resolvedSessionId, input.amount, providerAddress]
1521
+ });
1522
+ if (this.waitForReceipt) {
1523
+ await this.publicClient.waitForTransactionReceipt({ hash: txHash });
1524
+ }
1525
+ if (isAddress2(this.feeCollectorAddress)) {
1526
+ try {
1527
+ const feeTxHash = await this.walletClient.writeContract({
1528
+ address: this.feeCollectorAddress,
1529
+ abi: FeeCollectorABI,
1530
+ functionName: "collectSettlementFeeFor",
1531
+ args: [input.amount, providerAddress]
1532
+ });
1533
+ if (this.waitForReceipt) {
1534
+ await this.publicClient.waitForTransactionReceipt({ hash: feeTxHash });
1535
+ }
1536
+ } catch (err) {
1537
+ console.warn("[SettlementWriter] Fee collection failed (non-fatal):", err);
1538
+ }
1539
+ }
1540
+ return {
1541
+ transactionHash: txHash,
1542
+ settledAmount: input.amount,
1543
+ sessionId: resolvedSessionId,
1544
+ providerAddress
1545
+ };
1546
+ }
1547
+ };
1548
+
1549
+ // src/services/events.ts
1550
+ import { createPublicClient as createPublicClient5, http as http5 } from "viem";
1551
+ import { sepolia as sepolia5, baseSepolia as baseSepolia5, base as base5 } from "viem/chains";
1552
+ import { DEFAULT_CHAIN_ID as DEFAULT_CHAIN_ID4, getContractAddresses as getContractAddresses4 } from "@arcenpay/sdk";
1553
+ var CHAIN_MAP5 = {
1554
+ 11155111: sepolia5,
1555
+ 84532: baseSepolia5,
1556
+ 8453: base5
1557
+ };
1558
+ var EventListenerService = class {
1559
+ publicClient;
1560
+ registryAddress;
1561
+ autopayModuleAddress;
1562
+ callbacks = /* @__PURE__ */ new Map();
1563
+ pollInterval = null;
1564
+ lastBlock = 0n;
1565
+ constructor(config) {
1566
+ const chainId = config.chainId ?? DEFAULT_CHAIN_ID4;
1567
+ const chain = CHAIN_MAP5[chainId] || baseSepolia5;
1568
+ const contracts = getContractAddresses4(chainId);
1569
+ this.registryAddress = config.registryAddress || contracts.subscriptionRegistry;
1570
+ this.autopayModuleAddress = contracts.autopayModule;
1571
+ this.publicClient = createPublicClient5({
1572
+ chain,
1573
+ transport: http5(config.rpcUrl)
1574
+ });
1575
+ }
1576
+ /**
1577
+ * Register a callback for a specific event type (or '*' for all events)
1578
+ */
1579
+ on(eventType, callback) {
1580
+ const existing = this.callbacks.get(eventType) || [];
1581
+ existing.push(callback);
1582
+ this.callbacks.set(eventType, existing);
1583
+ return () => {
1584
+ const cbs = this.callbacks.get(eventType) || [];
1585
+ this.callbacks.set(eventType, cbs.filter((cb) => cb !== callback));
1586
+ };
1587
+ }
1588
+ /**
1589
+ * Start polling for events at the given interval
1590
+ */
1591
+ async start(intervalMs = 12e3) {
1592
+ try {
1593
+ this.lastBlock = await this.publicClient.getBlockNumber();
1594
+ } catch {
1595
+ this.lastBlock = 0n;
1596
+ }
1597
+ console.log(`[EventListener] Watching SubscriptionRegistry at ${this.registryAddress} from block ${this.lastBlock}`);
1598
+ this.pollInterval = setInterval(async () => {
1599
+ await this.poll();
1600
+ }, intervalMs);
1601
+ }
1602
+ /**
1603
+ * Stop polling
1604
+ */
1605
+ stop() {
1606
+ if (this.pollInterval) {
1607
+ clearInterval(this.pollInterval);
1608
+ this.pollInterval = null;
1609
+ }
1610
+ console.log("[EventListener] Stopped");
1611
+ }
1612
+ /**
1613
+ * Poll for new events since lastBlock
1614
+ */
1615
+ async poll() {
1616
+ try {
1617
+ const currentBlock = await this.publicClient.getBlockNumber();
1618
+ if (currentBlock <= this.lastBlock) return;
1619
+ const addr = this.registryAddress;
1620
+ const from = this.lastBlock + 1n;
1621
+ const [mintedLogs, renewedLogs, cancelledLogs, planChangedLogs, billingLogs] = await Promise.all([
1622
+ this.publicClient.getLogs({
1623
+ address: addr,
1624
+ event: {
1625
+ type: "event",
1626
+ name: "SubscriptionMinted",
1627
+ inputs: [
1628
+ { name: "subscriber", type: "address", indexed: true },
1629
+ { name: "tokenId", type: "uint256", indexed: true },
1630
+ { name: "planId", type: "uint256", indexed: true },
1631
+ { name: "expiration", type: "uint64", indexed: false }
1632
+ ]
1633
+ },
1634
+ fromBlock: from,
1635
+ toBlock: currentBlock
1636
+ }),
1637
+ this.publicClient.getLogs({
1638
+ address: addr,
1639
+ event: {
1640
+ type: "event",
1641
+ name: "SubscriptionRenewed",
1642
+ inputs: [
1643
+ { name: "tokenId", type: "uint256", indexed: true },
1644
+ { name: "newExpiration", type: "uint64", indexed: false },
1645
+ { name: "amountPaid", type: "uint256", indexed: false }
1646
+ ]
1647
+ },
1648
+ fromBlock: from,
1649
+ toBlock: currentBlock
1650
+ }),
1651
+ this.publicClient.getLogs({
1652
+ address: addr,
1653
+ event: {
1654
+ type: "event",
1655
+ name: "SubscriptionCancelled",
1656
+ inputs: [
1657
+ { name: "tokenId", type: "uint256", indexed: true }
1658
+ ]
1659
+ },
1660
+ fromBlock: from,
1661
+ toBlock: currentBlock
1662
+ }),
1663
+ this.publicClient.getLogs({
1664
+ address: addr,
1665
+ event: {
1666
+ type: "event",
1667
+ name: "SubscriptionPlanChanged",
1668
+ inputs: [
1669
+ { name: "tokenId", type: "uint256", indexed: true },
1670
+ { name: "previousPlanId", type: "uint256", indexed: true },
1671
+ { name: "newPlanId", type: "uint256", indexed: true },
1672
+ { name: "expiration", type: "uint64", indexed: false }
1673
+ ]
1674
+ },
1675
+ fromBlock: from,
1676
+ toBlock: currentBlock
1677
+ }),
1678
+ this.publicClient.getLogs({
1679
+ address: this.autopayModuleAddress,
1680
+ event: {
1681
+ type: "event",
1682
+ name: "BillingExecuted",
1683
+ inputs: [
1684
+ { name: "account", type: "address", indexed: true },
1685
+ { name: "merchant", type: "address", indexed: true },
1686
+ { name: "tokenId", type: "uint256", indexed: true },
1687
+ { name: "planId", type: "uint256", indexed: false },
1688
+ { name: "reason", type: "uint8", indexed: false },
1689
+ { name: "grossAmount", type: "uint256", indexed: false },
1690
+ { name: "merchantAmount", type: "uint256", indexed: false },
1691
+ { name: "protocolFee", type: "uint256", indexed: false },
1692
+ { name: "timestamp", type: "uint256", indexed: false }
1693
+ ]
1694
+ },
1695
+ fromBlock: from,
1696
+ toBlock: currentBlock
1697
+ })
1698
+ ]);
1699
+ for (const log of mintedLogs) {
1700
+ await this.emit({
1701
+ type: "minted",
1702
+ tokenId: log.args.tokenId,
1703
+ subscriber: log.args.subscriber,
1704
+ planId: log.args.planId,
1705
+ expiration: log.args.expiration,
1706
+ transactionHash: log.transactionHash,
1707
+ blockNumber: log.blockNumber,
1708
+ timestamp: Date.now()
1709
+ });
1710
+ }
1711
+ for (const log of renewedLogs) {
1712
+ await this.emit({
1713
+ type: "renewed",
1714
+ tokenId: log.args.tokenId,
1715
+ expiration: log.args.newExpiration,
1716
+ amountPaid: log.args.amountPaid,
1717
+ transactionHash: log.transactionHash,
1718
+ blockNumber: log.blockNumber,
1719
+ timestamp: Date.now()
1720
+ });
1721
+ }
1722
+ for (const log of cancelledLogs) {
1723
+ await this.emit({
1724
+ type: "cancelled",
1725
+ tokenId: log.args.tokenId,
1726
+ transactionHash: log.transactionHash,
1727
+ blockNumber: log.blockNumber,
1728
+ timestamp: Date.now()
1729
+ });
1730
+ }
1731
+ for (const log of planChangedLogs) {
1732
+ await this.emit({
1733
+ type: "plan_changed",
1734
+ tokenId: log.args.tokenId,
1735
+ planId: log.args.newPlanId,
1736
+ previousPlanId: log.args.previousPlanId,
1737
+ expiration: log.args.expiration,
1738
+ transactionHash: log.transactionHash,
1739
+ blockNumber: log.blockNumber,
1740
+ timestamp: Date.now()
1741
+ });
1742
+ }
1743
+ for (const log of billingLogs) {
1744
+ await this.emit({
1745
+ type: "billing_executed",
1746
+ tokenId: log.args.tokenId,
1747
+ account: log.args.account,
1748
+ merchant: log.args.merchant,
1749
+ planId: log.args.planId,
1750
+ billingReason: log.args.reason,
1751
+ amountPaid: log.args.grossAmount,
1752
+ merchantAmount: log.args.merchantAmount,
1753
+ protocolFee: log.args.protocolFee,
1754
+ transactionHash: log.transactionHash,
1755
+ blockNumber: log.blockNumber,
1756
+ timestamp: Number(log.args.timestamp ?? Date.now())
1757
+ });
1758
+ }
1759
+ this.lastBlock = currentBlock;
1760
+ } catch (err) {
1761
+ console.error("[EventListener] Polling error:", err);
1762
+ }
1763
+ }
1764
+ /**
1765
+ * Dispatch event to registered callbacks
1766
+ */
1767
+ async emit(event) {
1768
+ console.log(`[EventListener] ${event.type} \u2014 tokenId=${event.tokenId} tx=${event.transactionHash}`);
1769
+ const typeCallbacks = this.callbacks.get(event.type) || [];
1770
+ const wildcardCallbacks = this.callbacks.get("*") || [];
1771
+ for (const cb of [...typeCallbacks, ...wildcardCallbacks]) {
1772
+ try {
1773
+ await cb(event);
1774
+ } catch (err) {
1775
+ console.error(`[EventListener] Callback error for ${event.type}:`, err);
1776
+ }
1777
+ }
1778
+ }
1779
+ };
1780
+
1781
+ // src/services/webhooks.ts
1782
+ import { createHmac } from "crypto";
1783
+ var DEFAULT_RETRY_DELAYS = [1e3, 5e3, 3e4, 3e5, 18e5];
1784
+ var WebhookService = class {
1785
+ config;
1786
+ deliveryLog = [];
1787
+ pendingRetries = /* @__PURE__ */ new Map();
1788
+ constructor(config) {
1789
+ this.config = {
1790
+ maxRetries: 5,
1791
+ retryDelays: DEFAULT_RETRY_DELAYS,
1792
+ ...config
1793
+ };
1794
+ }
1795
+ /**
1796
+ * Deliver a webhook for a subscription event
1797
+ */
1798
+ async deliverEvent(event) {
1799
+ const payload = {
1800
+ event: `subscription.${event.type}`,
1801
+ timestamp: event.timestamp,
1802
+ data: {
1803
+ tokenId: event.tokenId?.toString(),
1804
+ subscriber: event.subscriber,
1805
+ planId: event.planId?.toString(),
1806
+ expiration: event.expiration?.toString(),
1807
+ amountPaid: event.amountPaid?.toString(),
1808
+ txHash: event.transactionHash,
1809
+ blockNumber: event.blockNumber?.toString()
1810
+ }
1811
+ };
1812
+ await this.deliver(payload);
1813
+ }
1814
+ /**
1815
+ * Deliver an arbitrary webhook payload
1816
+ */
1817
+ async deliver(payload, attempt = 1) {
1818
+ const id = `${payload.event}-${payload.timestamp}-${attempt}`;
1819
+ const body = JSON.stringify(payload);
1820
+ const signature = this.sign(body);
1821
+ try {
1822
+ const response = await fetch(this.config.url, {
1823
+ method: "POST",
1824
+ headers: {
1825
+ "Content-Type": "application/json",
1826
+ "X-MEAP-Signature": signature,
1827
+ "X-MEAP-Delivery": id,
1828
+ "User-Agent": "MEAP-Webhook/1.0"
1829
+ },
1830
+ body,
1831
+ signal: AbortSignal.timeout(1e4)
1832
+ });
1833
+ this.log({
1834
+ id,
1835
+ event: payload.event,
1836
+ url: this.config.url,
1837
+ statusCode: response.status,
1838
+ attemptNumber: attempt,
1839
+ deliveredAt: Date.now(),
1840
+ success: response.ok
1841
+ });
1842
+ await this.persistDelivery(this.deliveryLog[this.deliveryLog.length - 1], payload);
1843
+ if (!response.ok && attempt < this.config.maxRetries) {
1844
+ await this.scheduleRetry(payload, attempt);
1845
+ }
1846
+ } catch (err) {
1847
+ this.log({
1848
+ id,
1849
+ event: payload.event,
1850
+ url: this.config.url,
1851
+ statusCode: null,
1852
+ attemptNumber: attempt,
1853
+ deliveredAt: Date.now(),
1854
+ success: false,
1855
+ error: err.message
1856
+ });
1857
+ await this.persistDelivery(this.deliveryLog[this.deliveryLog.length - 1], payload);
1858
+ if (attempt < this.config.maxRetries) {
1859
+ await this.scheduleRetry(payload, attempt);
1860
+ }
1861
+ }
1862
+ }
1863
+ /**
1864
+ * HMAC-SHA256 signature of the payload body
1865
+ */
1866
+ sign(body) {
1867
+ return createHmac("sha256", this.config.secret).update(body).digest("hex");
1868
+ }
1869
+ /**
1870
+ * Schedule a retry with exponential backoff
1871
+ */
1872
+ async scheduleRetry(payload, currentAttempt) {
1873
+ const delay = this.config.retryDelays[currentAttempt - 1] || 18e5;
1874
+ const retryId = `retry-${payload.event}-${currentAttempt}`;
1875
+ console.log(`[Webhook] Scheduling retry ${currentAttempt + 1} in ${delay}ms for ${payload.event}`);
1876
+ const timer = setTimeout(() => {
1877
+ this.deliver(payload, currentAttempt + 1);
1878
+ this.pendingRetries.delete(retryId);
1879
+ }, delay);
1880
+ this.pendingRetries.set(retryId, timer);
1881
+ }
1882
+ /**
1883
+ * Log a delivery attempt
1884
+ */
1885
+ log(entry) {
1886
+ this.deliveryLog.push(entry);
1887
+ if (this.deliveryLog.length > 1e3) {
1888
+ this.deliveryLog = this.deliveryLog.slice(-500);
1889
+ }
1890
+ console.log(`[Webhook] ${entry.success ? "\u2705" : "\u274C"} ${entry.event} \u2192 ${entry.statusCode} (attempt ${entry.attemptNumber})`);
1891
+ }
1892
+ async persistDelivery(entry, payload) {
1893
+ if (!entry || !this.config.onDelivery) return;
1894
+ try {
1895
+ await this.config.onDelivery(entry, payload);
1896
+ } catch (err) {
1897
+ console.error("[Webhook] onDelivery callback failed:", err);
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Get the delivery log for dashboard display
1902
+ */
1903
+ getDeliveryLog(limit = 50) {
1904
+ return this.deliveryLog.slice(-limit).reverse();
1905
+ }
1906
+ /**
1907
+ * Verify an incoming webhook signature (for consumers).
1908
+ *
1909
+ * @example
1910
+ * ```ts
1911
+ * import { verifyWebhookSignature } from '@arcenpay/node';
1912
+ *
1913
+ * app.post('/webhooks/meap', (req, res) => {
1914
+ * const raw = JSON.stringify(req.body);
1915
+ * const sig = req.headers['x-meap-signature'] as string;
1916
+ * if (!verifyWebhookSignature(raw, sig, process.env.MEAP_WEBHOOK_SECRET!)) {
1917
+ * return res.status(401).json({ error: 'Invalid signature' });
1918
+ * }
1919
+ * });
1920
+ * ```
1921
+ */
1922
+ static verifySignature(body, signature, secret) {
1923
+ const expected = createHmac("sha256", secret).update(body).digest("hex");
1924
+ return expected === signature;
1925
+ }
1926
+ /**
1927
+ * Cancel all pending retries and clean up
1928
+ */
1929
+ stop() {
1930
+ for (const timer of this.pendingRetries.values()) {
1931
+ clearTimeout(timer);
1932
+ }
1933
+ this.pendingRetries.clear();
1934
+ }
1935
+ };
1936
+ function verifyWebhookSignature(body, signature, secret) {
1937
+ return WebhookService.verifySignature(body, signature, secret);
1938
+ }
1939
+
1940
+ // src/services/email.ts
1941
+ var EmailService = class {
1942
+ apiKey;
1943
+ fromAddress;
1944
+ brandName;
1945
+ constructor(config) {
1946
+ this.apiKey = config.apiKey;
1947
+ this.fromAddress = config.fromAddress;
1948
+ this.brandName = config.brandName || "ArcenPay";
1949
+ }
1950
+ /**
1951
+ * Send an email using a pre-defined template
1952
+ */
1953
+ async sendTemplate(template, recipient, data) {
1954
+ const payload = this.buildEmail(template, recipient, data);
1955
+ try {
1956
+ const response = await fetch("https://api.resend.com/emails", {
1957
+ method: "POST",
1958
+ headers: {
1959
+ Authorization: `Bearer ${this.apiKey}`,
1960
+ "Content-Type": "application/json"
1961
+ },
1962
+ body: JSON.stringify({
1963
+ from: `${this.brandName} <${this.fromAddress}>`,
1964
+ to: [payload.to],
1965
+ subject: payload.subject,
1966
+ html: payload.html
1967
+ })
1968
+ });
1969
+ if (!response.ok) {
1970
+ const err = await response.text();
1971
+ console.error(`[Email] Failed to send ${template}:`, err);
1972
+ return { success: false, error: err };
1973
+ }
1974
+ const result = await response.json();
1975
+ console.log(`[Email] \u2705 Sent ${template} to ${recipient.email} (${result.id})`);
1976
+ return { success: true, messageId: result.id };
1977
+ } catch (err) {
1978
+ console.error(`[Email] Error sending ${template}:`, err.message);
1979
+ return { success: false, error: err.message };
1980
+ }
1981
+ }
1982
+ /**
1983
+ * Build the email payload for a given template
1984
+ */
1985
+ buildEmail(template, recipient, data) {
1986
+ const shortAddr = `${data.walletAddress?.slice(0, 6)}...${data.walletAddress?.slice(-4)}`;
1987
+ const templates = {
1988
+ welcome: {
1989
+ subject: `Welcome to ${this.brandName}!`,
1990
+ body: `
1991
+ <h2>Welcome to ${this.brandName}! \u{1F389}</h2>
1992
+ <p>Your subscription is now active.</p>
1993
+ <table style="border-collapse:collapse;width:100%;max-width:400px">
1994
+ <tr><td style="padding:8px;border-bottom:1px solid #eee"><strong>Plan</strong></td><td style="padding:8px;border-bottom:1px solid #eee">${data.planTier || "Pro"}</td></tr>
1995
+ <tr><td style="padding:8px;border-bottom:1px solid #eee"><strong>Wallet</strong></td><td style="padding:8px;border-bottom:1px solid #eee"><code>${shortAddr}</code></td></tr>
1996
+ <tr><td style="padding:8px;border-bottom:1px solid #eee"><strong>Expires</strong></td><td style="padding:8px;border-bottom:1px solid #eee">${data.expiresAt || "N/A"}</td></tr>
1997
+ <tr><td style="padding:8px"><strong>Token ID</strong></td><td style="padding:8px">#${data.tokenId || "\u2014"}</td></tr>
1998
+ </table>
1999
+ <p style="margin-top:16px;color:#888">Transaction: <a href="${data.explorerUrl || "#"}">${data.txHash?.slice(0, 16) || ""}...</a></p>
2000
+ `
2001
+ },
2002
+ renewal_receipt: {
2003
+ subject: `${this.brandName} \u2014 Subscription Renewed`,
2004
+ body: `
2005
+ <h2>Subscription Renewed \u2705</h2>
2006
+ <p>Your subscription has been automatically renewed.</p>
2007
+ <table style="border-collapse:collapse;width:100%;max-width:400px">
2008
+ <tr><td style="padding:8px;border-bottom:1px solid #eee"><strong>Amount</strong></td><td style="padding:8px;border-bottom:1px solid #eee">${data.amount || "0"} USDC</td></tr>
2009
+ <tr><td style="padding:8px;border-bottom:1px solid #eee"><strong>New Expiry</strong></td><td style="padding:8px;border-bottom:1px solid #eee">${data.expiresAt || "N/A"}</td></tr>
2010
+ <tr><td style="padding:8px"><strong>Wallet</strong></td><td style="padding:8px"><code>${shortAddr}</code></td></tr>
2011
+ </table>
2012
+ <p style="margin-top:16px;color:#888">Transaction: <a href="${data.explorerUrl || "#"}">${data.txHash?.slice(0, 16) || ""}...</a></p>
2013
+ `
2014
+ },
2015
+ payment_failed: {
2016
+ subject: `\u26A0\uFE0F ${this.brandName} \u2014 Payment Failed`,
2017
+ body: `
2018
+ <h2>Payment Failed \u26A0\uFE0F</h2>
2019
+ <p>We were unable to process your subscription renewal.</p>
2020
+ <p><strong>Reason:</strong> ${data.reason || "Insufficient balance"}</p>
2021
+ <p><strong>Wallet:</strong> <code>${shortAddr}</code></p>
2022
+ <p>Please ensure your smart account has sufficient USDC balance to avoid service interruption.</p>
2023
+ `
2024
+ },
2025
+ subscription_expiring: {
2026
+ subject: `${this.brandName} \u2014 Subscription Expiring Soon`,
2027
+ body: `
2028
+ <h2>Subscription Expiring Soon \u23F0</h2>
2029
+ <p>Your subscription will expire on <strong>${data.expiresAt || "soon"}</strong>.</p>
2030
+ <p>Please ensure your wallet has sufficient balance for automatic renewal, or renew manually.</p>
2031
+ <p><strong>Wallet:</strong> <code>${shortAddr}</code></p>
2032
+ `
2033
+ },
2034
+ access_revoked: {
2035
+ subject: `${this.brandName} \u2014 Access Revoked`,
2036
+ body: `
2037
+ <h2>Access Revoked</h2>
2038
+ <p>Your subscription has been cancelled and access has been revoked.</p>
2039
+ <p><strong>Token ID:</strong> #${data.tokenId || "\u2014"}</p>
2040
+ <p><strong>Wallet:</strong> <code>${shortAddr}</code></p>
2041
+ <p>You can re-subscribe at any time to restore access.</p>
2042
+ `
2043
+ },
2044
+ low_session_balance: {
2045
+ subject: `\u26A0\uFE0F ${this.brandName} \u2014 Low Session Balance`,
2046
+ body: `
2047
+ <h2>Low Session Balance \u26A0\uFE0F</h2>
2048
+ <p>Your ZKVUB session vault balance is running low.</p>
2049
+ <table style="border-collapse:collapse;width:100%;max-width:400px">
2050
+ <tr><td style="padding:8px;border-bottom:1px solid #eee"><strong>Remaining</strong></td><td style="padding:8px;border-bottom:1px solid #eee">${data.remaining || "0"} USDC</td></tr>
2051
+ <tr><td style="padding:8px"><strong>Session ID</strong></td><td style="padding:8px"><code>${data.sessionId?.slice(0, 16) || ""}...</code></td></tr>
2052
+ </table>
2053
+ <p>Please deposit additional USDC to avoid service interruption.</p>
2054
+ `
2055
+ }
2056
+ };
2057
+ const tpl = templates[template];
2058
+ return {
2059
+ to: recipient.email,
2060
+ subject: tpl.subject,
2061
+ html: this.wrapInLayout(tpl.body)
2062
+ };
2063
+ }
2064
+ /**
2065
+ * Wrap template body in a consistent email layout
2066
+ */
2067
+ wrapInLayout(body) {
2068
+ return `
2069
+ <!DOCTYPE html>
2070
+ <html>
2071
+ <head><meta charset="utf-8"></head>
2072
+ <body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1a1a2e;background:#fafafa">
2073
+ <div style="background:#fff;border-radius:12px;padding:32px;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
2074
+ ${body}
2075
+ </div>
2076
+ <div style="text-align:center;margin-top:24px;font-size:12px;color:#999">
2077
+ <p>${this.brandName} \u2014 MEAP + ZKVUB Protocol</p>
2078
+ <p>Powered by on-chain entitlements</p>
2079
+ </div>
2080
+ </body>
2081
+ </html>
2082
+ `;
2083
+ }
2084
+ };
2085
+
2086
+ // src/services/lit.ts
2087
+ import { LIT_NETWORK } from "@lit-protocol/constants";
2088
+ import { decryptToString, encryptString } from "@lit-protocol/encryption";
2089
+ import { LitNodeClient } from "@lit-protocol/lit-node-client";
2090
+ var LitProtocolService = class {
2091
+ config;
2092
+ litClient = null;
2093
+ constructor(config) {
2094
+ this.config = config;
2095
+ }
2096
+ async connect() {
2097
+ const litNetwork = (() => {
2098
+ switch (this.config.network) {
2099
+ case "habanero":
2100
+ case "datil":
2101
+ return LIT_NETWORK.Datil;
2102
+ case "datil-test":
2103
+ case "manzano":
2104
+ return LIT_NETWORK.DatilTest;
2105
+ case "datil-dev":
2106
+ default:
2107
+ return LIT_NETWORK.DatilDev;
2108
+ }
2109
+ })();
2110
+ this.litClient = new LitNodeClient({
2111
+ litNetwork,
2112
+ debug: false
2113
+ });
2114
+ await this.litClient.connect();
2115
+ console.log(`[LitProtocol] Connected to ${litNetwork}`);
2116
+ }
2117
+ buildSubscriptionACC(_walletAddress) {
2118
+ return [
2119
+ {
2120
+ contractAddress: this.config.registryAddress,
2121
+ standardContractType: "Custom",
2122
+ chain: this.config.chain,
2123
+ method: "hasActiveSubscription",
2124
+ parameters: [":userAddress"],
2125
+ returnValueTest: {
2126
+ comparator: "=",
2127
+ value: "true"
2128
+ }
2129
+ }
2130
+ ];
2131
+ }
2132
+ buildTierACC(minimumTier) {
2133
+ const tierOrder = { starter: 1, pro: 2, enterprise: 3 };
2134
+ const minValue = tierOrder[minimumTier] || 1;
2135
+ return [
2136
+ {
2137
+ contractAddress: this.config.registryAddress,
2138
+ standardContractType: "Custom",
2139
+ chain: this.config.chain,
2140
+ method: "hasActiveSubscription",
2141
+ parameters: [":userAddress"],
2142
+ returnValueTest: {
2143
+ comparator: "=",
2144
+ value: "true"
2145
+ }
2146
+ },
2147
+ {
2148
+ contractAddress: this.config.registryAddress,
2149
+ standardContractType: "Custom",
2150
+ chain: this.config.chain,
2151
+ method: "getWalletSubscription",
2152
+ parameters: [":userAddress"],
2153
+ returnValueTest: {
2154
+ comparator: ">=",
2155
+ value: String(minValue > 0 ? 1 : 0)
2156
+ }
2157
+ }
2158
+ ];
2159
+ }
2160
+ async encrypt(content, accessConditions) {
2161
+ this.ensureConnected();
2162
+ const result = await encryptString(
2163
+ {
2164
+ accessControlConditions: accessConditions,
2165
+ chain: this.config.chain,
2166
+ dataToEncrypt: content
2167
+ },
2168
+ this.litClient
2169
+ );
2170
+ return {
2171
+ ciphertext: result.ciphertext,
2172
+ dataToEncryptHash: result.dataToEncryptHash
2173
+ };
2174
+ }
2175
+ async decrypt(ciphertext, dataToEncryptHash, accessConditions, auth) {
2176
+ this.ensureConnected();
2177
+ if (!auth?.authSig && !auth?.sessionSigs) {
2178
+ throw new Error(
2179
+ "[LitProtocol] Missing auth input. Provide authSig or sessionSigs for decryption."
2180
+ );
2181
+ }
2182
+ const decrypted = await decryptToString(
2183
+ {
2184
+ accessControlConditions: accessConditions,
2185
+ chain: this.config.chain,
2186
+ ciphertext,
2187
+ dataToEncryptHash,
2188
+ ...auth.authSig ? { authSig: auth.authSig } : {},
2189
+ ...auth.sessionSigs ? { sessionSigs: auth.sessionSigs } : {}
2190
+ },
2191
+ this.litClient
2192
+ );
2193
+ return decrypted;
2194
+ }
2195
+ async disconnect() {
2196
+ if (this.litClient) {
2197
+ await this.litClient.disconnect();
2198
+ this.litClient = null;
2199
+ }
2200
+ console.log("[LitProtocol] Disconnected");
2201
+ }
2202
+ ensureConnected() {
2203
+ if (!this.litClient) {
2204
+ throw new Error("[LitProtocol] Not connected. Call connect() first.");
2205
+ }
2206
+ }
2207
+ };
2208
+
2209
+ // src/services/aggregator.ts
2210
+ import { createHash as createHash2 } from "crypto";
2211
+ var AggregatorService = class {
2212
+ logs = /* @__PURE__ */ new Map();
2213
+ merkleRoots = /* @__PURE__ */ new Map();
2214
+ settlementThreshold;
2215
+ constructor(config) {
2216
+ this.settlementThreshold = config?.settlementThreshold || 100;
2217
+ }
2218
+ /**
2219
+ * Append a signed usage entry
2220
+ */
2221
+ addEntry(entry) {
2222
+ const existing = this.logs.get(entry.sessionId) || [];
2223
+ existing.push(entry);
2224
+ this.logs.set(entry.sessionId, existing);
2225
+ const thresholdReached = existing.length >= this.settlementThreshold;
2226
+ if (thresholdReached) {
2227
+ console.log(`[Aggregator] Threshold reached for session ${entry.sessionId} (${existing.length} entries)`);
2228
+ }
2229
+ return { totalEntries: existing.length, thresholdReached };
2230
+ }
2231
+ /**
2232
+ * Build a Merkle tree from session's usage logs
2233
+ * Returns the Merkle root hash
2234
+ */
2235
+ buildMerkleTree(sessionId) {
2236
+ const entries = this.logs.get(sessionId) || [];
2237
+ if (entries.length === 0) return "0x" + "0".repeat(64);
2238
+ const leaves = entries.map(
2239
+ (entry) => this.hashEntry(entry)
2240
+ );
2241
+ const root = this.computeMerkleRoot(leaves);
2242
+ this.merkleRoots.set(sessionId, root);
2243
+ console.log(`[Aggregator] Merkle root for ${sessionId}: ${root} (${entries.length} leaves)`);
2244
+ return root;
2245
+ }
2246
+ /**
2247
+ * Get circuit inputs for zk-SNARK proving
2248
+ * These inputs are passed to the Circom circuit
2249
+ */
2250
+ getCircuitInputs(sessionId) {
2251
+ const entries = this.logs.get(sessionId) || [];
2252
+ const merkleRoot = this.merkleRoots.get(sessionId) || this.buildMerkleTree(sessionId);
2253
+ const timestamps = entries.map((e) => e.timestamp);
2254
+ const windowStart = timestamps.length > 0 ? Math.min(...timestamps) : 0;
2255
+ const windowEnd = timestamps.length > 0 ? Math.max(...timestamps) : 0;
2256
+ return {
2257
+ callCount: entries.length,
2258
+ merkleRoot,
2259
+ windowStart,
2260
+ windowEnd,
2261
+ logHashes: entries.map((e) => this.hashEntry(e))
2262
+ };
2263
+ }
2264
+ /**
2265
+ * Clear entries for a settled session window
2266
+ */
2267
+ clearSettledEntries(sessionId, windowEnd) {
2268
+ const entries = this.logs.get(sessionId) || [];
2269
+ this.logs.set(
2270
+ sessionId,
2271
+ entries.filter((e) => e.timestamp > windowEnd)
2272
+ );
2273
+ this.merkleRoots.delete(sessionId);
2274
+ console.log(`[Aggregator] Cleared entries for ${sessionId} before ${windowEnd}`);
2275
+ }
2276
+ /**
2277
+ * Get aggregation stats for monitoring
2278
+ */
2279
+ getStats() {
2280
+ let totalEntries = 0;
2281
+ let pendingSettlements = 0;
2282
+ for (const [, entries] of this.logs) {
2283
+ totalEntries += entries.length;
2284
+ if (entries.length >= this.settlementThreshold) {
2285
+ pendingSettlements++;
2286
+ }
2287
+ }
2288
+ return {
2289
+ sessions: this.logs.size,
2290
+ totalEntries,
2291
+ pendingSettlements
2292
+ };
2293
+ }
2294
+ /**
2295
+ * Hash a single usage entry (leaf in the Merkle tree)
2296
+ */
2297
+ hashEntry(entry) {
2298
+ const data = `${entry.sessionId}|${entry.endpoint}|${entry.method}|${entry.timestamp}|${entry.responseSize}|${entry.signature}`;
2299
+ return "0x" + createHash2("sha256").update(data).digest("hex");
2300
+ }
2301
+ /**
2302
+ * Compute Merkle root from leaf hashes
2303
+ */
2304
+ computeMerkleRoot(leaves) {
2305
+ if (leaves.length === 0) return "0x" + "0".repeat(64);
2306
+ if (leaves.length === 1) return leaves[0];
2307
+ while (leaves.length & leaves.length - 1) {
2308
+ leaves.push("0x" + "0".repeat(64));
2309
+ }
2310
+ let currentLevel = leaves;
2311
+ while (currentLevel.length > 1) {
2312
+ const nextLevel = [];
2313
+ for (let i = 0; i < currentLevel.length; i += 2) {
2314
+ const combined = currentLevel[i] + currentLevel[i + 1].slice(2);
2315
+ nextLevel.push("0x" + createHash2("sha256").update(combined).digest("hex"));
2316
+ }
2317
+ currentLevel = nextLevel;
2318
+ }
2319
+ return currentLevel[0];
2320
+ }
2321
+ };
2322
+
2323
+ // src/services/redis-usage.ts
2324
+ import { createClient } from "redis";
2325
+ function sessionKey(prefix, sessionId) {
2326
+ return `${prefix}:usage:${sessionId}`;
2327
+ }
2328
+ function settlementKey(prefix) {
2329
+ return `${prefix}:settlement:latest`;
2330
+ }
2331
+ var RedisUsageStore = class {
2332
+ config;
2333
+ client;
2334
+ connected = false;
2335
+ constructor(config) {
2336
+ this.config = {
2337
+ keyPrefix: "meap",
2338
+ ttlSeconds: 7 * 24 * 60 * 60,
2339
+ ...config
2340
+ };
2341
+ this.client = createClient({ url: this.config.url });
2342
+ this.client.on("error", (err) => {
2343
+ console.error("[RedisUsageStore] Redis error:", err);
2344
+ });
2345
+ }
2346
+ async connect() {
2347
+ if (this.connected) return;
2348
+ await this.client.connect();
2349
+ this.connected = true;
2350
+ }
2351
+ async appendUsage(entry) {
2352
+ if (!this.connected) {
2353
+ await this.connect();
2354
+ }
2355
+ const key = sessionKey(this.config.keyPrefix, entry.sessionId);
2356
+ await this.client.rPush(key, JSON.stringify(entry));
2357
+ await this.client.expire(key, this.config.ttlSeconds);
2358
+ }
2359
+ async getUsageCount(sessionId) {
2360
+ if (!this.connected) {
2361
+ await this.connect();
2362
+ }
2363
+ return this.client.lLen(sessionKey(this.config.keyPrefix, sessionId));
2364
+ }
2365
+ async getUsageEntries(sessionId, limit = 100) {
2366
+ if (!this.connected) {
2367
+ await this.connect();
2368
+ }
2369
+ const upper = Math.max(0, limit - 1);
2370
+ const values = await this.client.lRange(sessionKey(this.config.keyPrefix, sessionId), 0, upper);
2371
+ return values.map((value) => {
2372
+ try {
2373
+ return JSON.parse(value);
2374
+ } catch {
2375
+ return null;
2376
+ }
2377
+ }).filter((value) => value !== null);
2378
+ }
2379
+ async setLatestSettlementTx(sessionId, txHash) {
2380
+ if (!this.connected) {
2381
+ await this.connect();
2382
+ }
2383
+ await this.client.hSet(settlementKey(this.config.keyPrefix), sessionId, txHash);
2384
+ await this.client.expire(settlementKey(this.config.keyPrefix), this.config.ttlSeconds);
2385
+ }
2386
+ async getLatestSettlementTx(sessionId) {
2387
+ if (!this.connected) {
2388
+ await this.connect();
2389
+ }
2390
+ const txHash = await this.client.hGet(settlementKey(this.config.keyPrefix), sessionId);
2391
+ return txHash ?? null;
2392
+ }
2393
+ async stop() {
2394
+ if (this.connected) {
2395
+ await this.client.quit();
2396
+ this.connected = false;
2397
+ }
2398
+ }
2399
+ };
2400
+
2401
+ // src/services/proof-orchestrator.ts
2402
+ var ProofOrchestrator = class {
2403
+ usageService;
2404
+ config;
2405
+ /**
2406
+ * Nullifiers that have been submitted — prevents double-settlement.
2407
+ * Key: nullifier hash, Value: settlement timestamp
2408
+ */
2409
+ settledNullifiers = /* @__PURE__ */ new Map();
2410
+ /** Active proof jobs indexed by `sessionId:windowEnd` */
2411
+ activeJobs = /* @__PURE__ */ new Map();
2412
+ /** Completed job history (last 200) */
2413
+ jobHistory = [];
2414
+ /** Interval for auto-submit checks */
2415
+ autoCheckInterval = null;
2416
+ constructor(config = {}) {
2417
+ this.usageService = new UsageService(config);
2418
+ this.config = {
2419
+ maxRetries: config.maxRetries ?? 3,
2420
+ retryDelays: config.retryDelays ?? [2e3, 5e3, 15e3],
2421
+ autoSubmitThreshold: config.autoSubmitThreshold ?? 50,
2422
+ windowDurationSeconds: config.windowDurationSeconds ?? 3600
2423
+ };
2424
+ }
2425
+ /**
2426
+ * Records usage and auto-triggers proof+submission when threshold is reached
2427
+ */
2428
+ async recordUsage(sessionId, callData) {
2429
+ await this.usageService.logUsage(sessionId, callData);
2430
+ const count = this.usageService.getUsageCount(sessionId);
2431
+ if (count >= this.config.autoSubmitThreshold) {
2432
+ void this.generateAndSubmit(sessionId).catch(
2433
+ (err) => console.error(
2434
+ `[ProofOrchestrator] Auto-submit failed for ${sessionId}:`,
2435
+ err
2436
+ )
2437
+ );
2438
+ }
2439
+ }
2440
+ /**
2441
+ * Manually triggers proof generation + submission for a session
2442
+ */
2443
+ async generateAndSubmit(sessionId) {
2444
+ const windowEnd = Math.floor(Date.now() / 1e3);
2445
+ const jobKey = `${sessionId}:${windowEnd}`;
2446
+ const existingJob = this.activeJobs.get(jobKey);
2447
+ if (existingJob && existingJob.status !== "failed") {
2448
+ return existingJob;
2449
+ }
2450
+ const job = {
2451
+ sessionId,
2452
+ windowEnd,
2453
+ windowStart: null,
2454
+ callCount: null,
2455
+ settlementAmount: null,
2456
+ status: "pending",
2457
+ attempts: 0,
2458
+ nullifier: null,
2459
+ txHash: null,
2460
+ error: null,
2461
+ createdAt: Date.now(),
2462
+ lastAttemptAt: 0
2463
+ };
2464
+ this.activeJobs.set(jobKey, job);
2465
+ return this.executeJob(job);
2466
+ }
2467
+ /**
2468
+ * Execute a proof job with deterministic retry
2469
+ */
2470
+ async executeJob(job) {
2471
+ const jobKey = `${job.sessionId}:${job.windowEnd}`;
2472
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
2473
+ job.attempts = attempt + 1;
2474
+ job.lastAttemptAt = Date.now();
2475
+ if (attempt > 0) {
2476
+ const delay = this.config.retryDelays[attempt - 1] ?? 15e3;
2477
+ console.log(
2478
+ `[ProofOrchestrator] Retry ${attempt}/${this.config.maxRetries} in ${delay}ms for ${job.sessionId}`
2479
+ );
2480
+ await new Promise((resolve) => setTimeout(resolve, delay));
2481
+ }
2482
+ try {
2483
+ job.status = "generating";
2484
+ const { proof, publicInputs } = await this.usageService.generateProof(
2485
+ job.sessionId,
2486
+ job.windowEnd
2487
+ );
2488
+ job.windowStart = Number(publicInputs.windowStart);
2489
+ job.callCount = publicInputs.callCount.toString();
2490
+ const nullifierKey = publicInputs.nullifier;
2491
+ if (this.settledNullifiers.has(nullifierKey)) {
2492
+ console.warn(
2493
+ `[ProofOrchestrator] Nullifier already settled: ${nullifierKey}`
2494
+ );
2495
+ job.status = "settled";
2496
+ job.nullifier = nullifierKey;
2497
+ job.error = "Nullifier already settled (idempotent skip)";
2498
+ this.archiveJob(jobKey, job);
2499
+ return job;
2500
+ }
2501
+ job.status = "submitting";
2502
+ const settlement = await this.usageService.submitProof(
2503
+ proof,
2504
+ publicInputs
2505
+ );
2506
+ job.status = "settled";
2507
+ job.nullifier = nullifierKey;
2508
+ job.txHash = settlement.transactionHash;
2509
+ job.settlementAmount = settlement.settlementAmount.toString();
2510
+ job.error = null;
2511
+ this.settledNullifiers.set(nullifierKey, Date.now());
2512
+ this.usageService.clearLogs(job.sessionId, job.windowEnd);
2513
+ console.log(
2514
+ `[ProofOrchestrator] \u2705 Settled ${job.sessionId} \u2192 ${settlement.transactionHash}`
2515
+ );
2516
+ this.archiveJob(jobKey, job);
2517
+ return job;
2518
+ } catch (err) {
2519
+ job.error = err?.message || String(err);
2520
+ job.status = "failed";
2521
+ console.error(
2522
+ `[ProofOrchestrator] Attempt ${attempt + 1} failed for ${job.sessionId}:`,
2523
+ err?.message
2524
+ );
2525
+ }
2526
+ }
2527
+ console.error(
2528
+ `[ProofOrchestrator] \u274C All ${this.config.maxRetries + 1} attempts failed for ${job.sessionId}`
2529
+ );
2530
+ this.archiveJob(jobKey, job);
2531
+ return job;
2532
+ }
2533
+ /**
2534
+ * Move a job from active to history
2535
+ */
2536
+ archiveJob(jobKey, job) {
2537
+ this.activeJobs.delete(jobKey);
2538
+ this.jobHistory.push({ ...job });
2539
+ if (this.jobHistory.length > 200) {
2540
+ this.jobHistory.splice(0, this.jobHistory.length - 200);
2541
+ }
2542
+ }
2543
+ /**
2544
+ * Start auto-submit monitoring (polls every 30s)
2545
+ */
2546
+ startAutoSubmit() {
2547
+ if (this.autoCheckInterval) return;
2548
+ console.log(
2549
+ `[ProofOrchestrator] Auto-submit started (threshold: ${this.config.autoSubmitThreshold} calls)`
2550
+ );
2551
+ this.autoCheckInterval = setInterval(async () => {
2552
+ try {
2553
+ const sessions = this.usageService.getAllSessions();
2554
+ for (const sessionId of sessions) {
2555
+ const count = this.usageService.getUsageCount(sessionId);
2556
+ if (count >= this.config.autoSubmitThreshold) {
2557
+ const jobKey = Array.from(this.activeJobs.keys()).find(
2558
+ (k) => k.startsWith(`${sessionId}:`)
2559
+ );
2560
+ if (!jobKey) {
2561
+ console.log(
2562
+ `[ProofOrchestrator] Auto-submit triggered for ${sessionId} (${count} calls)`
2563
+ );
2564
+ void this.generateAndSubmit(sessionId).catch(
2565
+ (err) => console.error(
2566
+ `[ProofOrchestrator] Auto-submit failed for ${sessionId}:`,
2567
+ err
2568
+ )
2569
+ );
2570
+ }
2571
+ }
2572
+ }
2573
+ } catch (err) {
2574
+ console.error("[ProofOrchestrator] Auto-submit poll error:", err);
2575
+ }
2576
+ }, 3e4);
2577
+ if (typeof this.autoCheckInterval === "object" && "unref" in this.autoCheckInterval) {
2578
+ this.autoCheckInterval.unref();
2579
+ }
2580
+ }
2581
+ /**
2582
+ * Stop auto-submit monitoring
2583
+ */
2584
+ stop() {
2585
+ if (this.autoCheckInterval) {
2586
+ clearInterval(this.autoCheckInterval);
2587
+ this.autoCheckInterval = null;
2588
+ }
2589
+ }
2590
+ /**
2591
+ * Get current active proof jobs
2592
+ */
2593
+ getActiveJobs() {
2594
+ return Array.from(this.activeJobs.values());
2595
+ }
2596
+ /**
2597
+ * Get completed job history
2598
+ */
2599
+ getJobHistory(limit = 50) {
2600
+ return this.jobHistory.slice(-limit).reverse();
2601
+ }
2602
+ /**
2603
+ * Check if a nullifier has already been settled
2604
+ */
2605
+ isNullifierSettled(nullifier) {
2606
+ return this.settledNullifiers.has(nullifier);
2607
+ }
2608
+ /**
2609
+ * Get the underlying UsageService for direct access
2610
+ */
2611
+ getUsageService() {
2612
+ return this.usageService;
2613
+ }
2614
+ };
2615
+
2616
+ // src/services/transport-axelar.ts
2617
+ import {
2618
+ createPublicClient as createPublicClient6,
2619
+ createWalletClient as createWalletClient3,
2620
+ http as http6
2621
+ } from "viem";
2622
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
2623
+ import { sepolia as sepolia6, baseSepolia as baseSepolia6, base as base6 } from "viem/chains";
2624
+ import { DEFAULT_CHAIN_ID as DEFAULT_CHAIN_ID5 } from "@arcenpay/sdk";
2625
+ var CHAIN_MAP6 = {
2626
+ 11155111: sepolia6,
2627
+ 84532: baseSepolia6,
2628
+ 8453: base6
2629
+ };
2630
+ var AXELAR_CHAIN_NAMES = {
2631
+ 1: "ethereum",
2632
+ 11155111: "ethereum-sepolia",
2633
+ 137: "polygon",
2634
+ 42161: "arbitrum",
2635
+ 10: "optimism",
2636
+ 8453: "base",
2637
+ 84532: "base-sepolia",
2638
+ 43114: "avalanche"
2639
+ };
2640
+ var AXELAR_GATEWAY_ABI = [
2641
+ {
2642
+ name: "callContract",
2643
+ type: "function",
2644
+ stateMutability: "nonpayable",
2645
+ inputs: [
2646
+ { name: "destinationChain", type: "string" },
2647
+ { name: "contractAddress", type: "string" },
2648
+ { name: "payload", type: "bytes" }
2649
+ ],
2650
+ outputs: []
2651
+ }
2652
+ ];
2653
+ var AXELAR_GAS_SERVICE_ABI = [
2654
+ {
2655
+ name: "payNativeGasForContractCall",
2656
+ type: "function",
2657
+ stateMutability: "payable",
2658
+ inputs: [
2659
+ { name: "sender", type: "address" },
2660
+ { name: "destinationChain", type: "string" },
2661
+ { name: "destinationAddress", type: "string" },
2662
+ { name: "payload", type: "bytes" },
2663
+ { name: "refundAddress", type: "address" }
2664
+ ],
2665
+ outputs: []
2666
+ },
2667
+ {
2668
+ name: "estimateGasFee",
2669
+ type: "function",
2670
+ stateMutability: "view",
2671
+ inputs: [
2672
+ { name: "destinationChain", type: "string" },
2673
+ { name: "destinationAddress", type: "string" },
2674
+ { name: "payload", type: "bytes" }
2675
+ ],
2676
+ outputs: [{ name: "", type: "uint256" }]
2677
+ }
2678
+ ];
2679
+ var AxelarTransport = class {
2680
+ config;
2681
+ chainId;
2682
+ publicClient;
2683
+ walletClient;
2684
+ signerAddress;
2685
+ constructor(config) {
2686
+ this.config = config;
2687
+ this.chainId = config.chainId ?? DEFAULT_CHAIN_ID5;
2688
+ const chain = CHAIN_MAP6[this.chainId] || baseSepolia6;
2689
+ const account = privateKeyToAccount3(config.privateKey);
2690
+ this.signerAddress = account.address;
2691
+ this.publicClient = createPublicClient6({
2692
+ chain,
2693
+ transport: http6(config.rpcUrl)
2694
+ });
2695
+ this.walletClient = createWalletClient3({
2696
+ account,
2697
+ chain,
2698
+ transport: http6(config.rpcUrl)
2699
+ });
2700
+ }
2701
+ /**
2702
+ * Dispatch a cross-chain mirror update via Axelar GMP
2703
+ */
2704
+ async dispatch(destinationChainId, payload, refundAddress, destinationAddressOverride) {
2705
+ const destChainName = AXELAR_CHAIN_NAMES[destinationChainId];
2706
+ if (!destChainName) {
2707
+ throw new Error(
2708
+ `Unsupported destination chain ID: ${destinationChainId}`
2709
+ );
2710
+ }
2711
+ const destAddress = destinationAddressOverride || this.config.mirrorRegistryAddress;
2712
+ let gasEstimate = 0n;
2713
+ try {
2714
+ gasEstimate = await this.publicClient.readContract({
2715
+ address: this.config.gasServiceAddress,
2716
+ abi: AXELAR_GAS_SERVICE_ABI,
2717
+ functionName: "estimateGasFee",
2718
+ args: [destChainName, destAddress, payload]
2719
+ });
2720
+ } catch {
2721
+ gasEstimate = 2000000000000000n;
2722
+ }
2723
+ const gasBudget = gasEstimate * 120n / 100n;
2724
+ const gasPayTx = await this.walletClient.writeContract({
2725
+ address: this.config.gasServiceAddress,
2726
+ abi: AXELAR_GAS_SERVICE_ABI,
2727
+ functionName: "payNativeGasForContractCall",
2728
+ args: [
2729
+ this.signerAddress,
2730
+ destChainName,
2731
+ destAddress,
2732
+ payload,
2733
+ refundAddress
2734
+ ],
2735
+ value: gasBudget
2736
+ });
2737
+ await this.publicClient.waitForTransactionReceipt({ hash: gasPayTx });
2738
+ const dispatchTx = await this.walletClient.writeContract({
2739
+ address: this.config.gatewayAddress,
2740
+ abi: AXELAR_GATEWAY_ABI,
2741
+ functionName: "callContract",
2742
+ args: [destChainName, destAddress, payload]
2743
+ });
2744
+ const receipt = await this.publicClient.waitForTransactionReceipt({
2745
+ hash: dispatchTx
2746
+ });
2747
+ console.log(
2748
+ `[AxelarTransport] \u2705 Dispatched to ${destChainName} \u2192 tx: ${dispatchTx}`
2749
+ );
2750
+ return {
2751
+ transactionHash: dispatchTx,
2752
+ messageId: `axelar-${dispatchTx}`,
2753
+ route: "axelar",
2754
+ destinationChain: destChainName,
2755
+ gasEstimate: gasBudget
2756
+ };
2757
+ }
2758
+ /**
2759
+ * Get the estimated gas cost for a dispatch
2760
+ */
2761
+ async estimateGas(destinationChainId, payload, destinationAddressOverride) {
2762
+ const destChainName = AXELAR_CHAIN_NAMES[destinationChainId];
2763
+ if (!destChainName) return 0n;
2764
+ const destAddress = destinationAddressOverride || this.config.mirrorRegistryAddress;
2765
+ try {
2766
+ return await this.publicClient.readContract({
2767
+ address: this.config.gasServiceAddress,
2768
+ abi: AXELAR_GAS_SERVICE_ABI,
2769
+ functionName: "estimateGasFee",
2770
+ args: [destChainName, destAddress, payload]
2771
+ });
2772
+ } catch {
2773
+ return 2000000000000000n;
2774
+ }
2775
+ }
2776
+ /**
2777
+ * Get signer's gas balance (for alert monitoring)
2778
+ */
2779
+ async getSignerBalance() {
2780
+ return this.publicClient.getBalance({ address: this.signerAddress });
2781
+ }
2782
+ };
2783
+
2784
+ // src/services/transport-ccip.ts
2785
+ import { createPublicClient as createPublicClient7, createWalletClient as createWalletClient4, http as http7 } from "viem";
2786
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2787
+ import { sepolia as sepolia7, baseSepolia as baseSepolia7, base as base7 } from "viem/chains";
2788
+ import { DEFAULT_CHAIN_ID as DEFAULT_CHAIN_ID6 } from "@arcenpay/sdk";
2789
+ var CHAIN_MAP7 = {
2790
+ 11155111: sepolia7,
2791
+ 84532: baseSepolia7,
2792
+ 8453: base7
2793
+ };
2794
+ var CCIP_CHAIN_SELECTORS = {
2795
+ 1: 5009297550715157269n,
2796
+ // Ethereum Mainnet
2797
+ 11155111: 16015286601757825753n,
2798
+ // Sepolia
2799
+ 137: 4051577828743386545n,
2800
+ // Polygon
2801
+ 42161: 4949039107694359620n,
2802
+ // Arbitrum
2803
+ 10: 3734403246176062136n,
2804
+ // Optimism
2805
+ 8453: 15971525489660198786n,
2806
+ // Base
2807
+ 84532: 10344971235874465080n,
2808
+ // Base Sepolia
2809
+ 43114: 6433500567565415381n
2810
+ // Avalanche
2811
+ };
2812
+ var CCIP_ROUTER_ABI = [
2813
+ {
2814
+ name: "ccipSend",
2815
+ type: "function",
2816
+ stateMutability: "payable",
2817
+ inputs: [
2818
+ { name: "destinationChainSelector", type: "uint64" },
2819
+ {
2820
+ name: "message",
2821
+ type: "tuple",
2822
+ components: [
2823
+ { name: "receiver", type: "bytes" },
2824
+ { name: "data", type: "bytes" },
2825
+ {
2826
+ name: "tokenAmounts",
2827
+ type: "tuple[]",
2828
+ components: [
2829
+ { name: "token", type: "address" },
2830
+ { name: "amount", type: "uint256" }
2831
+ ]
2832
+ },
2833
+ { name: "feeToken", type: "address" },
2834
+ { name: "extraArgs", type: "bytes" }
2835
+ ]
2836
+ }
2837
+ ],
2838
+ outputs: [{ name: "messageId", type: "bytes32" }]
2839
+ },
2840
+ {
2841
+ name: "getFee",
2842
+ type: "function",
2843
+ stateMutability: "view",
2844
+ inputs: [
2845
+ { name: "destinationChainSelector", type: "uint64" },
2846
+ {
2847
+ name: "message",
2848
+ type: "tuple",
2849
+ components: [
2850
+ { name: "receiver", type: "bytes" },
2851
+ { name: "data", type: "bytes" },
2852
+ {
2853
+ name: "tokenAmounts",
2854
+ type: "tuple[]",
2855
+ components: [
2856
+ { name: "token", type: "address" },
2857
+ { name: "amount", type: "uint256" }
2858
+ ]
2859
+ },
2860
+ { name: "feeToken", type: "address" },
2861
+ { name: "extraArgs", type: "bytes" }
2862
+ ]
2863
+ }
2864
+ ],
2865
+ outputs: [{ name: "fee", type: "uint256" }]
2866
+ }
2867
+ ];
2868
+ var CCIPTransport = class {
2869
+ config;
2870
+ chainId;
2871
+ publicClient;
2872
+ walletClient;
2873
+ signerAddress;
2874
+ gasLimit;
2875
+ constructor(config) {
2876
+ this.config = config;
2877
+ this.chainId = config.chainId ?? DEFAULT_CHAIN_ID6;
2878
+ this.gasLimit = config.gasLimit || 200000n;
2879
+ const chain = CHAIN_MAP7[this.chainId] || baseSepolia7;
2880
+ const account = privateKeyToAccount4(config.privateKey);
2881
+ this.signerAddress = account.address;
2882
+ this.publicClient = createPublicClient7({
2883
+ chain,
2884
+ transport: http7(config.rpcUrl)
2885
+ });
2886
+ this.walletClient = createWalletClient4({
2887
+ account,
2888
+ chain,
2889
+ transport: http7(config.rpcUrl)
2890
+ });
2891
+ }
2892
+ /**
2893
+ * Dispatch a cross-chain mirror update via Chainlink CCIP
2894
+ */
2895
+ async dispatch(destinationChainId, payload, refundAddress, destinationAddressOverride) {
2896
+ const destSelector = CCIP_CHAIN_SELECTORS[destinationChainId];
2897
+ if (!destSelector) {
2898
+ throw new Error(
2899
+ `Unsupported CCIP destination chain ID: ${destinationChainId}`
2900
+ );
2901
+ }
2902
+ const message = this.buildMessage(payload, destinationAddressOverride);
2903
+ let fee = 0n;
2904
+ try {
2905
+ fee = await this.publicClient.readContract({
2906
+ address: this.config.routerAddress,
2907
+ abi: CCIP_ROUTER_ABI,
2908
+ functionName: "getFee",
2909
+ args: [destSelector, message]
2910
+ });
2911
+ } catch {
2912
+ fee = 5000000000000000n;
2913
+ }
2914
+ const feeBudget = fee * 115n / 100n;
2915
+ const txHash = await this.walletClient.writeContract({
2916
+ address: this.config.routerAddress,
2917
+ abi: CCIP_ROUTER_ABI,
2918
+ functionName: "ccipSend",
2919
+ args: [destSelector, message],
2920
+ value: feeBudget
2921
+ });
2922
+ const receipt = await this.publicClient.waitForTransactionReceipt({
2923
+ hash: txHash
2924
+ });
2925
+ console.log(
2926
+ `[CCIPTransport] \u2705 Dispatched to selector ${destSelector} \u2192 tx: ${txHash}`
2927
+ );
2928
+ return {
2929
+ transactionHash: txHash,
2930
+ messageId: `ccip-${txHash}`,
2931
+ route: "ccip",
2932
+ destinationChainSelector: destSelector,
2933
+ fee: feeBudget
2934
+ };
2935
+ }
2936
+ /**
2937
+ * Build a CCIP EVM2AnyMessage
2938
+ */
2939
+ buildMessage(payload, destinationAddressOverride) {
2940
+ const extraArgs = `0x97a657c9${this.gasLimit.toString(16).padStart(64, "0")}`;
2941
+ return {
2942
+ receiver: destinationAddressOverride || this.config.mirrorRegistryAddress,
2943
+ data: payload,
2944
+ tokenAmounts: [],
2945
+ feeToken: "0x0000000000000000000000000000000000000000",
2946
+ // Native gas
2947
+ extraArgs
2948
+ };
2949
+ }
2950
+ /**
2951
+ * Estimate the CCIP fee for a dispatch
2952
+ */
2953
+ async estimateFee(destinationChainId, payload, destinationAddressOverride) {
2954
+ const destSelector = CCIP_CHAIN_SELECTORS[destinationChainId];
2955
+ if (!destSelector) return 0n;
2956
+ try {
2957
+ return await this.publicClient.readContract({
2958
+ address: this.config.routerAddress,
2959
+ abi: CCIP_ROUTER_ABI,
2960
+ functionName: "getFee",
2961
+ args: [destSelector, this.buildMessage(payload, destinationAddressOverride)]
2962
+ });
2963
+ } catch {
2964
+ return 5000000000000000n;
2965
+ }
2966
+ }
2967
+ /**
2968
+ * Get signer's gas balance (for alert monitoring)
2969
+ */
2970
+ async getSignerBalance() {
2971
+ return this.publicClient.getBalance({ address: this.signerAddress });
2972
+ }
2973
+ };
2974
+
2975
+ // src/services/keeper.ts
2976
+ import { createPublicClient as createPublicClient8, createWalletClient as createWalletClient5, http as http8 } from "viem";
2977
+ import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
2978
+ import { sepolia as sepolia8, baseSepolia as baseSepolia8, base as base8 } from "viem/chains";
2979
+ import {
2980
+ DEFAULT_CHAIN_ID as DEFAULT_CHAIN_ID7,
2981
+ SubscriptionRegistryABI,
2982
+ ERC7579AutopayModuleABI,
2983
+ getContractAddresses as getContractAddresses5
2984
+ } from "@arcenpay/sdk";
2985
+ var CHAIN_MAP8 = {
2986
+ 11155111: sepolia8,
2987
+ 84532: baseSepolia8,
2988
+ 8453: base8
2989
+ };
2990
+ var BillingKeeper = class {
2991
+ chainId;
2992
+ publicClient;
2993
+ walletClient;
2994
+ signerAddress;
2995
+ intervalMs;
2996
+ batchSize;
2997
+ renewalBufferSeconds;
2998
+ registryAddress;
2999
+ autopayModuleAddress;
3000
+ pollTimer = null;
3001
+ processing = false;
3002
+ renewalLog = [];
3003
+ callbacks = [];
3004
+ constructor(config) {
3005
+ this.chainId = config.chainId ?? DEFAULT_CHAIN_ID7;
3006
+ const chain = CHAIN_MAP8[this.chainId] || baseSepolia8;
3007
+ const contracts = getContractAddresses5(this.chainId);
3008
+ this.registryAddress = contracts.subscriptionRegistry;
3009
+ this.autopayModuleAddress = contracts.autopayModule;
3010
+ this.intervalMs = config.intervalMs || 6e4;
3011
+ this.batchSize = config.batchSize || 20;
3012
+ this.renewalBufferSeconds = config.renewalBufferSeconds || 86400;
3013
+ const account = privateKeyToAccount5(config.privateKey);
3014
+ this.signerAddress = account.address;
3015
+ this.publicClient = createPublicClient8({
3016
+ chain,
3017
+ transport: http8(config.rpcUrl)
3018
+ });
3019
+ this.walletClient = createWalletClient5({
3020
+ account,
3021
+ chain,
3022
+ transport: http8(config.rpcUrl)
3023
+ });
3024
+ }
3025
+ /**
3026
+ * Register a callback for renewal events
3027
+ */
3028
+ onRenewal(callback) {
3029
+ this.callbacks.push(callback);
3030
+ return () => {
3031
+ this.callbacks = this.callbacks.filter((cb) => cb !== callback);
3032
+ };
3033
+ }
3034
+ /**
3035
+ * Start the keeper polling loop
3036
+ */
3037
+ start() {
3038
+ if (this.pollTimer) return;
3039
+ console.log(
3040
+ `[BillingKeeper] Started \u2014 polling every ${this.intervalMs / 1e3}s, renewal buffer ${this.renewalBufferSeconds}s, batch size ${this.batchSize}`
3041
+ );
3042
+ void this.poll();
3043
+ this.pollTimer = setInterval(() => {
3044
+ void this.poll();
3045
+ }, this.intervalMs);
3046
+ }
3047
+ /**
3048
+ * Stop the keeper
3049
+ */
3050
+ stop() {
3051
+ if (this.pollTimer) {
3052
+ clearInterval(this.pollTimer);
3053
+ this.pollTimer = null;
3054
+ }
3055
+ console.log("[BillingKeeper] Stopped");
3056
+ }
3057
+ /**
3058
+ * Get renewal history
3059
+ */
3060
+ getRenewalLog(limit = 50) {
3061
+ return this.renewalLog.slice(-limit).reverse();
3062
+ }
3063
+ /**
3064
+ * Get keeper stats
3065
+ */
3066
+ getStats() {
3067
+ const successful = this.renewalLog.filter((r) => r.success).length;
3068
+ return {
3069
+ running: this.pollTimer !== null,
3070
+ totalRenewals: this.renewalLog.length,
3071
+ successfulRenewals: successful,
3072
+ failedRenewals: this.renewalLog.length - successful
3073
+ };
3074
+ }
3075
+ /**
3076
+ * Poll for subscriptions needing renewal
3077
+ */
3078
+ async poll() {
3079
+ if (this.processing) return;
3080
+ this.processing = true;
3081
+ try {
3082
+ const now = Math.floor(Date.now() / 1e3);
3083
+ const renewalDeadline = now + this.renewalBufferSeconds;
3084
+ const expiringTokenIds = await this.findExpiringSubscriptions(
3085
+ BigInt(now),
3086
+ BigInt(renewalDeadline)
3087
+ );
3088
+ if (expiringTokenIds.length === 0) {
3089
+ this.processing = false;
3090
+ return;
3091
+ }
3092
+ console.log(
3093
+ `[BillingKeeper] Found ${expiringTokenIds.length} subscriptions nearing expiry`
3094
+ );
3095
+ const batch = expiringTokenIds.slice(0, this.batchSize);
3096
+ for (const tokenId of batch) {
3097
+ await this.processRenewal(tokenId);
3098
+ }
3099
+ } catch (err) {
3100
+ console.error("[BillingKeeper] Poll error:", err);
3101
+ } finally {
3102
+ this.processing = false;
3103
+ }
3104
+ }
3105
+ /**
3106
+ * Find subscriptions expiring between now and the renewal deadline
3107
+ */
3108
+ async findExpiringSubscriptions(now, deadline) {
3109
+ try {
3110
+ const totalSupply = await this.publicClient.readContract({
3111
+ address: this.registryAddress,
3112
+ abi: SubscriptionRegistryABI,
3113
+ functionName: "totalSupply"
3114
+ });
3115
+ const tokenIds = [];
3116
+ for (let index = 0n; index < totalSupply; index += 1n) {
3117
+ try {
3118
+ const tokenId = await this.publicClient.readContract({
3119
+ address: this.registryAddress,
3120
+ abi: SubscriptionRegistryABI,
3121
+ functionName: "tokenByIndex",
3122
+ args: [index]
3123
+ });
3124
+ const expiresAt = await this.publicClient.readContract({
3125
+ address: this.registryAddress,
3126
+ abi: SubscriptionRegistryABI,
3127
+ functionName: "expiresAt",
3128
+ args: [tokenId]
3129
+ });
3130
+ if (expiresAt > now && expiresAt <= deadline) {
3131
+ tokenIds.push(tokenId);
3132
+ }
3133
+ } catch (err) {
3134
+ console.warn(
3135
+ `[BillingKeeper] Skipping token index ${index.toString()} while scanning expiring subscriptions:`,
3136
+ err
3137
+ );
3138
+ }
3139
+ }
3140
+ return tokenIds;
3141
+ } catch (err) {
3142
+ console.error("[BillingKeeper] Error finding expiring subscriptions:", err);
3143
+ return [];
3144
+ }
3145
+ }
3146
+ /**
3147
+ * Process a single subscription renewal
3148
+ */
3149
+ async processRenewal(tokenId) {
3150
+ try {
3151
+ const subscriber = await this.publicClient.readContract({
3152
+ address: this.registryAddress,
3153
+ abi: SubscriptionRegistryABI,
3154
+ functionName: "ownerOf",
3155
+ args: [tokenId]
3156
+ });
3157
+ const isInstalled = await this.checkAutopayInstalled(subscriber);
3158
+ if (!isInstalled) {
3159
+ console.log(
3160
+ `[BillingKeeper] Skipping tokenId=${tokenId} \u2014 no AutopayModule installed`
3161
+ );
3162
+ return;
3163
+ }
3164
+ const config = await this.publicClient.readContract({
3165
+ address: this.autopayModuleAddress,
3166
+ abi: ERC7579AutopayModuleABI,
3167
+ functionName: "getConfig",
3168
+ args: [subscriber]
3169
+ });
3170
+ const merchant = String(config?.merchant ?? config?.[0] ?? "").toLowerCase();
3171
+ const amount = BigInt(config?.maxAmount ?? config?.[1] ?? 0);
3172
+ if (amount <= 0n) {
3173
+ console.log(
3174
+ `[BillingKeeper] Skipping tokenId=${tokenId} \u2014 autopay maxAmount is not configured`
3175
+ );
3176
+ return;
3177
+ }
3178
+ if (merchant !== this.signerAddress.toLowerCase()) {
3179
+ throw new Error(
3180
+ `Keeper signer ${this.signerAddress} does not match autopay merchant ${merchant || "unknown"}. Renewals must be executed by the configured merchant wallet.`
3181
+ );
3182
+ }
3183
+ const txHash = await this.walletClient.writeContract({
3184
+ address: this.autopayModuleAddress,
3185
+ abi: ERC7579AutopayModuleABI,
3186
+ functionName: "execute",
3187
+ args: [subscriber, amount]
3188
+ });
3189
+ await this.publicClient.waitForTransactionReceipt({ hash: txHash });
3190
+ const result = {
3191
+ tokenId,
3192
+ subscriber,
3193
+ txHash,
3194
+ success: true
3195
+ };
3196
+ this.logRenewal(result);
3197
+ console.log(
3198
+ `[BillingKeeper] \u2705 Renewed tokenId=${tokenId} for ${subscriber} \u2014 tx: ${txHash}`
3199
+ );
3200
+ } catch (err) {
3201
+ const result = {
3202
+ tokenId,
3203
+ subscriber: "unknown",
3204
+ txHash: "0x",
3205
+ success: false,
3206
+ error: err?.message || String(err)
3207
+ };
3208
+ this.logRenewal(result);
3209
+ console.error(
3210
+ `[BillingKeeper] \u274C Failed to renew tokenId=${tokenId}:`,
3211
+ err?.message
3212
+ );
3213
+ }
3214
+ }
3215
+ /**
3216
+ * Check if AutopayModule is installed for a subscriber
3217
+ */
3218
+ async checkAutopayInstalled(subscriber) {
3219
+ try {
3220
+ const config = await this.publicClient.readContract({
3221
+ address: this.autopayModuleAddress,
3222
+ abi: ERC7579AutopayModuleABI,
3223
+ functionName: "getConfig",
3224
+ args: [subscriber]
3225
+ });
3226
+ const maxAmount = BigInt(config?.maxAmount ?? config?.[0] ?? 0);
3227
+ return maxAmount > 0n;
3228
+ } catch {
3229
+ return false;
3230
+ }
3231
+ }
3232
+ /**
3233
+ * Log a renewal result
3234
+ */
3235
+ logRenewal(result) {
3236
+ this.renewalLog.push(result);
3237
+ if (this.renewalLog.length > 1e3) {
3238
+ this.renewalLog = this.renewalLog.slice(-500);
3239
+ }
3240
+ for (const cb of this.callbacks) {
3241
+ try {
3242
+ cb(result);
3243
+ } catch (err) {
3244
+ console.error("[BillingKeeper] Callback error:", err);
3245
+ }
3246
+ }
3247
+ }
3248
+ };
3249
+
3250
+ // src/index.ts
3251
+ init_nonce_store();
3252
+
3253
+ // src/services/event-contracts.ts
3254
+ var PROTOCOL_EVENT_SCHEMA_VERSION = "2026-03-09.1";
3255
+ function normalizePart(value) {
3256
+ return value.trim().replace(/\s+/g, "_").replace(/[:/]/g, "-");
3257
+ }
3258
+ function buildEventIdempotencyKey(name, uniqueParts) {
3259
+ const suffix = uniqueParts.map((part) => normalizePart(String(part))).filter(Boolean).join(":");
3260
+ return suffix ? `${name}:${suffix}` : name;
3261
+ }
3262
+ function createProtocolEvent(name, payload, options) {
3263
+ const occurred = options.occurredAt instanceof Date ? options.occurredAt.toISOString() : typeof options.occurredAt === "number" ? new Date(options.occurredAt).toISOString() : options.occurredAt || (/* @__PURE__ */ new Date()).toISOString();
3264
+ return {
3265
+ schemaVersion: PROTOCOL_EVENT_SCHEMA_VERSION,
3266
+ name,
3267
+ source: options.source,
3268
+ idempotencyKey: options.idempotencyKey,
3269
+ chainId: options.chainId,
3270
+ occurredAt: occurred,
3271
+ payload
3272
+ };
3273
+ }
3274
+ function mapProtocolEventToBillingType(name) {
3275
+ switch (name) {
3276
+ case "subscription.minted":
3277
+ return "SUBSCRIPTION_MINTED";
3278
+ case "subscription.renewed":
3279
+ return "SUBSCRIPTION_RENEWED";
3280
+ case "subscription.renewal_failed":
3281
+ return "PAYMENT_FAILED";
3282
+ case "subscription.cancelled":
3283
+ return "SUBSCRIPTION_CANCELLED";
3284
+ default:
3285
+ throw new Error(
3286
+ `[mapProtocolEventToBillingType] Unsupported event name for billing bridge: ${name}`
3287
+ );
3288
+ }
3289
+ }
3290
+ export {
3291
+ AggregatorService,
3292
+ ArcenApiError,
3293
+ ArcenClient,
3294
+ AxelarTransport,
3295
+ BillingKeeper,
3296
+ CCIPTransport,
3297
+ EmailService,
3298
+ EventListenerService,
3299
+ InMemoryNonceStore,
3300
+ LitProtocolService,
3301
+ PROTOCOL_EVENT_SCHEMA_VERSION,
3302
+ ProofOrchestrator,
3303
+ RedisNonceStore,
3304
+ RedisUsageStore,
3305
+ SettlementService,
3306
+ SettlementWriterService,
3307
+ TablelandService,
3308
+ UsageProofError,
3309
+ UsageProofErrorCodes,
3310
+ UsageService,
3311
+ WebhookService,
3312
+ buildEventIdempotencyKey,
3313
+ createProtocolEvent,
3314
+ mapProtocolEventToBillingType,
3315
+ verifyWebhookSignature,
3316
+ x402Middleware
3317
+ };