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