@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/README.md +117 -0
- package/dist/chunk-SKFD6TSD.mjs +266 -0
- package/dist/index.d.mts +1083 -0
- package/dist/index.d.ts +1083 -0
- package/dist/index.js +3581 -0
- package/dist/index.mjs +3317 -0
- package/dist/tableland.d.mts +46 -0
- package/dist/tableland.d.ts +46 -0
- package/dist/tableland.js +268 -0
- package/dist/tableland.mjs +6 -0
- package/package.json +74 -0
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
|
+
});
|