@pafi-dev/issuer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1759 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AuthError: () => AuthError,
24
+ AuthService: () => AuthService,
25
+ DefaultPolicyEngine: () => DefaultPolicyEngine,
26
+ FeeManager: () => FeeManager,
27
+ InMemoryCursorStore: () => InMemoryCursorStore,
28
+ IssuerApiHandlers: () => IssuerApiHandlers,
29
+ MemoryPointLedger: () => MemoryPointLedger,
30
+ MemorySessionStore: () => MemorySessionStore,
31
+ MintingGateway: () => MintingGateway,
32
+ MintingGatewayError: () => MintingGatewayError,
33
+ NonceManager: () => NonceManager,
34
+ PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
35
+ PointIndexer: () => PointIndexer,
36
+ PrivateKeySigner: () => PrivateKeySigner,
37
+ RelayError: () => RelayError,
38
+ RelayService: () => RelayService,
39
+ authenticateRequest: () => authenticateRequest,
40
+ createIssuerService: () => createIssuerService,
41
+ createSubgraphNativeUsdtQuoter: () => createSubgraphNativeUsdtQuoter,
42
+ createSubgraphPoolsProvider: () => createSubgraphPoolsProvider,
43
+ encodeExtData: () => import_core4.encodeExtData
44
+ });
45
+ module.exports = __toCommonJS(index_exports);
46
+
47
+ // src/ledger/memoryLedger.ts
48
+ var import_viem = require("viem");
49
+ var MemoryPointLedger = class {
50
+ balances = /* @__PURE__ */ new Map();
51
+ locks = /* @__PURE__ */ new Map();
52
+ nextLockId = 1;
53
+ now;
54
+ constructor(opts = {}) {
55
+ this.now = opts.now ?? (() => Date.now());
56
+ }
57
+ // -------------------------------------------------------------------------
58
+ // Read
59
+ // -------------------------------------------------------------------------
60
+ async getBalance(userAddress) {
61
+ const key = (0, import_viem.getAddress)(userAddress);
62
+ this.purgeExpired();
63
+ const total = this.balances.get(key) ?? 0n;
64
+ const locked = this.lockedTotalFor(key);
65
+ return total - locked;
66
+ }
67
+ async getLockedRequests(userAddress) {
68
+ const key = (0, import_viem.getAddress)(userAddress);
69
+ this.purgeExpired();
70
+ const out = [];
71
+ for (const lock of this.locks.values()) {
72
+ if (lock.userAddress === key && lock.status === "PENDING") {
73
+ out.push({ ...lock });
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+ // -------------------------------------------------------------------------
79
+ // Write
80
+ // -------------------------------------------------------------------------
81
+ async creditBalance(userAddress, amount, _reason) {
82
+ if (amount <= 0n) {
83
+ throw new Error("MemoryPointLedger: credit amount must be positive");
84
+ }
85
+ const key = (0, import_viem.getAddress)(userAddress);
86
+ const current = this.balances.get(key) ?? 0n;
87
+ this.balances.set(key, current + amount);
88
+ }
89
+ async lockForMinting(userAddress, amount, lockDurationMs) {
90
+ if (amount <= 0n) {
91
+ throw new Error("MemoryPointLedger: lock amount must be positive");
92
+ }
93
+ if (lockDurationMs <= 0) {
94
+ throw new Error("MemoryPointLedger: lockDurationMs must be positive");
95
+ }
96
+ const key = (0, import_viem.getAddress)(userAddress);
97
+ this.purgeExpired();
98
+ const total = this.balances.get(key) ?? 0n;
99
+ const alreadyLocked = this.lockedTotalFor(key);
100
+ const available = total - alreadyLocked;
101
+ if (available < amount) {
102
+ throw new Error(
103
+ `MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
104
+ );
105
+ }
106
+ const lockId = `lock-${this.nextLockId++}`;
107
+ const now = this.now();
108
+ this.locks.set(lockId, {
109
+ lockId,
110
+ userAddress: key,
111
+ amount,
112
+ status: "PENDING",
113
+ createdAt: now,
114
+ expiresAt: now + lockDurationMs
115
+ });
116
+ return lockId;
117
+ }
118
+ async releaseLock(lockId) {
119
+ const lock = this.locks.get(lockId);
120
+ if (!lock) return;
121
+ if (lock.status === "PENDING") {
122
+ this.locks.delete(lockId);
123
+ }
124
+ }
125
+ async deductBalance(userAddress, amount, txHash) {
126
+ if (amount <= 0n) {
127
+ throw new Error("MemoryPointLedger: deduct amount must be positive");
128
+ }
129
+ const key = (0, import_viem.getAddress)(userAddress);
130
+ const current = this.balances.get(key) ?? 0n;
131
+ if (current < amount) {
132
+ throw new Error(
133
+ `MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
134
+ );
135
+ }
136
+ this.balances.set(key, current - amount);
137
+ for (const lock of this.locks.values()) {
138
+ if (lock.userAddress === key && lock.status === "PENDING" && lock.amount === amount) {
139
+ lock.status = "MINTED";
140
+ lock.txHash = txHash;
141
+ return;
142
+ }
143
+ }
144
+ }
145
+ async updateMintStatus(lockId, status, txHash) {
146
+ const lock = this.locks.get(lockId);
147
+ if (!lock) {
148
+ throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
149
+ }
150
+ lock.status = status;
151
+ if (txHash) lock.txHash = txHash;
152
+ }
153
+ // -------------------------------------------------------------------------
154
+ // Internal helpers
155
+ // -------------------------------------------------------------------------
156
+ /**
157
+ * Auto-expire any PENDING lock past its expiry. Called lazily on every
158
+ * read/write so the in-memory state stays self-cleaning without a timer.
159
+ */
160
+ purgeExpired() {
161
+ const now = this.now();
162
+ for (const lock of this.locks.values()) {
163
+ if (lock.status === "PENDING" && lock.expiresAt <= now) {
164
+ lock.status = "EXPIRED";
165
+ }
166
+ }
167
+ }
168
+ lockedTotalFor(userAddress) {
169
+ let total = 0n;
170
+ for (const lock of this.locks.values()) {
171
+ if (lock.userAddress === userAddress && lock.status === "PENDING") {
172
+ total += lock.amount;
173
+ }
174
+ }
175
+ return total;
176
+ }
177
+ };
178
+
179
+ // src/policy/defaultPolicy.ts
180
+ var DefaultPolicyEngine = class {
181
+ ledger;
182
+ provider;
183
+ mintingOracleAddress;
184
+ verifyMintCap;
185
+ resolveIssuer;
186
+ constructor(opts) {
187
+ this.ledger = opts.ledger;
188
+ if (opts.provider) this.provider = opts.provider;
189
+ if (opts.mintingOracleAddress) {
190
+ this.mintingOracleAddress = opts.mintingOracleAddress;
191
+ }
192
+ if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
193
+ if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
194
+ }
195
+ async evaluate(request) {
196
+ if (request.amount <= 0n) {
197
+ return { approved: false, reason: "Amount must be positive" };
198
+ }
199
+ const available = await this.ledger.getBalance(request.userAddress);
200
+ if (available < request.amount) {
201
+ return {
202
+ approved: false,
203
+ reason: `Insufficient balance (available=${available}, requested=${request.amount})`
204
+ };
205
+ }
206
+ if (this.mintingOracleAddress && this.provider && this.verifyMintCap && this.resolveIssuer) {
207
+ try {
208
+ const issuer = await this.resolveIssuer(request.pointTokenAddress);
209
+ await this.verifyMintCap(
210
+ this.provider,
211
+ this.mintingOracleAddress,
212
+ issuer,
213
+ request.amount
214
+ );
215
+ } catch (err) {
216
+ const msg = err instanceof Error ? err.message : String(err);
217
+ return {
218
+ approved: false,
219
+ reason: `Minting cap check failed: ${msg}`
220
+ };
221
+ }
222
+ }
223
+ return { approved: true };
224
+ }
225
+ };
226
+
227
+ // src/signer/privateKeySigner.ts
228
+ var import_viem2 = require("viem");
229
+ var import_accounts = require("viem/accounts");
230
+ var import_core = require("@pafi-dev/core");
231
+ var PrivateKeySigner = class {
232
+ account;
233
+ walletClient;
234
+ constructor(opts) {
235
+ this.account = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
236
+ this.walletClient = (0, import_viem2.createWalletClient)({
237
+ account: this.account,
238
+ chain: opts.chain,
239
+ transport: (0, import_viem2.http)(opts.rpcUrl)
240
+ });
241
+ }
242
+ async signMintRequest(domain, message) {
243
+ return (0, import_core.signMintRequest)(this.walletClient, domain, message);
244
+ }
245
+ async getAddress() {
246
+ return this.account.address;
247
+ }
248
+ };
249
+
250
+ // src/auth/memorySessionStore.ts
251
+ var import_node_crypto = require("crypto");
252
+ var import_viem3 = require("viem");
253
+ var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
254
+ var MemorySessionStore = class {
255
+ nonces = /* @__PURE__ */ new Map();
256
+ // nonce → expiresAt
257
+ sessions = /* @__PURE__ */ new Map();
258
+ // tokenId → session
259
+ nonceTtlMs;
260
+ now;
261
+ constructor(opts = {}) {
262
+ this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
263
+ this.now = opts.now ?? (() => Date.now());
264
+ }
265
+ // -------------------------------------------------------------------------
266
+ // Nonces
267
+ // -------------------------------------------------------------------------
268
+ async createNonce() {
269
+ this.purgeExpiredNonces();
270
+ const nonce = (0, import_node_crypto.randomBytes)(16).toString("hex");
271
+ this.nonces.set(nonce, this.now() + this.nonceTtlMs);
272
+ return nonce;
273
+ }
274
+ async consumeNonce(nonce) {
275
+ this.purgeExpiredNonces();
276
+ const expiresAt = this.nonces.get(nonce);
277
+ if (expiresAt === void 0) return false;
278
+ this.nonces.delete(nonce);
279
+ return expiresAt > this.now();
280
+ }
281
+ // -------------------------------------------------------------------------
282
+ // Sessions
283
+ // -------------------------------------------------------------------------
284
+ async createSession(session) {
285
+ this.purgeExpiredSessions();
286
+ const normalized = {
287
+ ...session,
288
+ userAddress: (0, import_viem3.getAddress)(session.userAddress)
289
+ };
290
+ this.sessions.set(session.tokenId, normalized);
291
+ }
292
+ async getSession(tokenId) {
293
+ this.purgeExpiredSessions();
294
+ const session = this.sessions.get(tokenId);
295
+ if (!session) return null;
296
+ if (session.expiresAt.getTime() <= this.now()) {
297
+ this.sessions.delete(tokenId);
298
+ return null;
299
+ }
300
+ return { ...session };
301
+ }
302
+ async revokeSession(tokenId) {
303
+ this.sessions.delete(tokenId);
304
+ }
305
+ async revokeAllSessions(userAddress) {
306
+ const key = (0, import_viem3.getAddress)(userAddress);
307
+ for (const [tokenId, session] of this.sessions.entries()) {
308
+ if (session.userAddress === key) {
309
+ this.sessions.delete(tokenId);
310
+ }
311
+ }
312
+ }
313
+ // -------------------------------------------------------------------------
314
+ // Housekeeping
315
+ // -------------------------------------------------------------------------
316
+ purgeExpiredNonces() {
317
+ const now = this.now();
318
+ for (const [nonce, expiresAt] of this.nonces.entries()) {
319
+ if (expiresAt <= now) this.nonces.delete(nonce);
320
+ }
321
+ }
322
+ purgeExpiredSessions() {
323
+ const now = this.now();
324
+ for (const [tokenId, session] of this.sessions.entries()) {
325
+ if (session.expiresAt.getTime() <= now) this.sessions.delete(tokenId);
326
+ }
327
+ }
328
+ };
329
+
330
+ // src/auth/nonceManager.ts
331
+ var NonceManager = class {
332
+ constructor(store) {
333
+ this.store = store;
334
+ }
335
+ store;
336
+ /** Generate a fresh login nonce. The store is responsible for TTL. */
337
+ async generate() {
338
+ return this.store.createNonce();
339
+ }
340
+ /**
341
+ * Atomically validate + consume a nonce. Returns `true` iff the nonce
342
+ * was known and unexpired (and is now removed from the store).
343
+ */
344
+ async consume(nonce) {
345
+ return this.store.consumeNonce(nonce);
346
+ }
347
+ };
348
+
349
+ // src/auth/loginVerifier.ts
350
+ var import_node_crypto2 = require("crypto");
351
+ var import_jose = require("jose");
352
+ var import_viem4 = require("viem");
353
+ var import_core2 = require("@pafi-dev/core");
354
+
355
+ // src/auth/errors.ts
356
+ var AuthError = class extends Error {
357
+ code;
358
+ constructor(code, message) {
359
+ super(message);
360
+ this.name = "AuthError";
361
+ this.code = code;
362
+ }
363
+ };
364
+
365
+ // src/auth/loginVerifier.ts
366
+ var DEFAULT_EXPIRES_IN = "24h";
367
+ var AuthService = class {
368
+ sessionStore;
369
+ jwtSecret;
370
+ jwtExpiresIn;
371
+ domain;
372
+ chainId;
373
+ nonceManager;
374
+ now;
375
+ constructor(config) {
376
+ if (!config.jwtSecret || config.jwtSecret.length < 16) {
377
+ throw new Error("AuthService: jwtSecret must be at least 16 chars");
378
+ }
379
+ this.sessionStore = config.sessionStore;
380
+ this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
381
+ this.jwtExpiresIn = config.jwtExpiresIn ?? DEFAULT_EXPIRES_IN;
382
+ this.domain = config.domain;
383
+ this.chainId = config.chainId;
384
+ this.nonceManager = new NonceManager(config.sessionStore);
385
+ this.now = config.now ?? (() => /* @__PURE__ */ new Date());
386
+ }
387
+ // -------------------------------------------------------------------------
388
+ // Public API
389
+ // -------------------------------------------------------------------------
390
+ /** Generate a fresh login nonce. */
391
+ async getNonce() {
392
+ return this.nonceManager.generate();
393
+ }
394
+ /**
395
+ * Verify a signed login message and issue a JWT on success.
396
+ * Throws an `AuthError` on any validation failure.
397
+ */
398
+ async login(message, signature) {
399
+ let parsed;
400
+ try {
401
+ parsed = (0, import_core2.parseLoginMessage)(message);
402
+ } catch (err) {
403
+ const msg = err instanceof Error ? err.message : String(err);
404
+ throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
405
+ }
406
+ if (parsed.domain !== this.domain) {
407
+ throw new AuthError(
408
+ "DOMAIN_MISMATCH",
409
+ `Expected domain "${this.domain}", got "${parsed.domain}"`
410
+ );
411
+ }
412
+ if (parsed.chainId !== this.chainId) {
413
+ throw new AuthError(
414
+ "CHAIN_MISMATCH",
415
+ `Expected chainId ${this.chainId}, got ${parsed.chainId}`
416
+ );
417
+ }
418
+ const now = this.now();
419
+ if (parsed.notBefore && parsed.notBefore.getTime() > now.getTime()) {
420
+ throw new AuthError(
421
+ "MESSAGE_NOT_YET_VALID",
422
+ "Login message is not yet valid"
423
+ );
424
+ }
425
+ if (parsed.expirationTime && parsed.expirationTime.getTime() <= now.getTime()) {
426
+ throw new AuthError("MESSAGE_EXPIRED", "Login message has expired");
427
+ }
428
+ const verifyResult = await (0, import_core2.verifyLoginMessage)(message, signature);
429
+ if (!verifyResult.valid) {
430
+ throw new AuthError(
431
+ "SIGNATURE_INVALID",
432
+ "Signature does not match the address in the login message"
433
+ );
434
+ }
435
+ const nonceOk = await this.nonceManager.consume(parsed.nonce);
436
+ if (!nonceOk) {
437
+ throw new AuthError(
438
+ "NONCE_INVALID",
439
+ "Nonce is unknown, expired, or already used"
440
+ );
441
+ }
442
+ const userAddress = (0, import_viem4.getAddress)(verifyResult.address);
443
+ const tokenId = (0, import_node_crypto2.randomBytes)(16).toString("hex");
444
+ const issuedAt = now;
445
+ const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
446
+ const session = {
447
+ tokenId,
448
+ userAddress,
449
+ chainId: this.chainId,
450
+ issuedAt,
451
+ expiresAt
452
+ };
453
+ await this.sessionStore.createSession(session);
454
+ const token = await new import_jose.SignJWT({
455
+ userAddress,
456
+ chainId: this.chainId
457
+ }).setProtectedHeader({ alg: "HS256" }).setJti(tokenId).setIssuedAt(Math.floor(issuedAt.getTime() / 1e3)).setExpirationTime(Math.floor(expiresAt.getTime() / 1e3)).sign(this.jwtSecret);
458
+ return { token, userAddress, tokenId, expiresAt };
459
+ }
460
+ /** Revoke the session backing the given JWT (logout). */
461
+ async logout(token) {
462
+ try {
463
+ const { payload } = await (0, import_jose.jwtVerify)(token, this.jwtSecret, {
464
+ clockTolerance: 60
465
+ // allow logout right after expiry
466
+ });
467
+ if (payload.jti) {
468
+ await this.sessionStore.revokeSession(payload.jti);
469
+ }
470
+ } catch {
471
+ }
472
+ }
473
+ /**
474
+ * Verify a JWT and return the authenticated user context. Throws an
475
+ * `AuthError` if the token is missing, malformed, expired, revoked, or
476
+ * signed by a different key.
477
+ */
478
+ async verifyToken(token) {
479
+ let payload;
480
+ try {
481
+ const result = await (0, import_jose.jwtVerify)(token, this.jwtSecret);
482
+ payload = result.payload;
483
+ } catch (err) {
484
+ if (err instanceof import_jose.errors.JWTExpired) {
485
+ throw new AuthError("TOKEN_EXPIRED", "JWT has expired");
486
+ }
487
+ if (err instanceof import_jose.errors.JWTInvalid || err instanceof import_jose.errors.JWSInvalid || err instanceof import_jose.errors.JWSSignatureVerificationFailed) {
488
+ throw new AuthError("TOKEN_INVALID", "JWT is invalid");
489
+ }
490
+ throw new AuthError("TOKEN_INVALID", "JWT verification failed");
491
+ }
492
+ const tokenId = payload.jti;
493
+ if (!tokenId) {
494
+ throw new AuthError("TOKEN_INVALID", "JWT is missing jti claim");
495
+ }
496
+ const session = await this.sessionStore.getSession(tokenId);
497
+ if (!session) {
498
+ throw new AuthError(
499
+ "SESSION_REVOKED",
500
+ "Session is no longer active (revoked or expired)"
501
+ );
502
+ }
503
+ const userAddress = payload.userAddress;
504
+ const chainId = payload.chainId;
505
+ if (!userAddress || typeof chainId !== "number") {
506
+ throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
507
+ }
508
+ return {
509
+ userAddress: (0, import_viem4.getAddress)(userAddress),
510
+ chainId,
511
+ tokenId
512
+ };
513
+ }
514
+ };
515
+ function parseExpiry(from, expiresIn) {
516
+ const match = expiresIn.match(/^(\d+)\s*(s|m|h|d)$/i);
517
+ if (!match) {
518
+ throw new Error(
519
+ `AuthService: unsupported jwtExpiresIn "${expiresIn}" \u2014 use e.g. "24h"`
520
+ );
521
+ }
522
+ const n = Number(match[1]);
523
+ const unit = match[2].toLowerCase();
524
+ const multiplier = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
525
+ return new Date(from.getTime() + n * multiplier);
526
+ }
527
+
528
+ // src/auth/jwtMiddleware.ts
529
+ async function authenticateRequest(authHeader, authService) {
530
+ if (!authHeader) {
531
+ throw new AuthError(
532
+ "MISSING_TOKEN",
533
+ "Authorization header is required"
534
+ );
535
+ }
536
+ const match = authHeader.match(/^Bearer\s+(\S+)\s*$/i);
537
+ if (!match) {
538
+ throw new AuthError(
539
+ "MALFORMED_TOKEN",
540
+ "Authorization header must be in the form 'Bearer <token>'"
541
+ );
542
+ }
543
+ const token = match[1];
544
+ return authService.verifyToken(token);
545
+ }
546
+
547
+ // src/relay/types.ts
548
+ var RelayError = class extends Error {
549
+ code;
550
+ cause;
551
+ constructor(code, message, cause) {
552
+ super(message);
553
+ this.name = "RelayError";
554
+ this.code = code;
555
+ if (cause !== void 0) this.cause = cause;
556
+ }
557
+ };
558
+
559
+ // src/relay/relayService.ts
560
+ var import_core3 = require("@pafi-dev/core");
561
+ var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
562
+ var RelayService = class {
563
+ relayAddress;
564
+ operatorWallet;
565
+ provider;
566
+ confirmationTimeoutMs;
567
+ simulateBeforeSubmit;
568
+ constructor(config) {
569
+ if (!config.relayAddress) {
570
+ throw new Error("RelayService: relayAddress is required");
571
+ }
572
+ if (!config.operatorWallet) {
573
+ throw new Error("RelayService: operatorWallet is required");
574
+ }
575
+ this.relayAddress = config.relayAddress;
576
+ this.operatorWallet = config.operatorWallet;
577
+ if (config.provider) this.provider = config.provider;
578
+ this.confirmationTimeoutMs = config.confirmationTimeoutMs ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
579
+ this.simulateBeforeSubmit = config.simulateBeforeSubmit ?? config.provider !== void 0;
580
+ }
581
+ /** Address the operator wallet is broadcasting from (for logging). */
582
+ operatorAddress() {
583
+ return this.operatorWallet.account?.address;
584
+ }
585
+ /**
586
+ * Build calldata for the Relay `mintAndSwap` function. Kept public so
587
+ * callers (e.g. the MintingGateway) can log or persist the encoded call
588
+ * for audit before broadcasting.
589
+ */
590
+ encodeCall(params) {
591
+ try {
592
+ return (0, import_core3.encodeMintAndSwap)(params.mint, params.swap);
593
+ } catch (err) {
594
+ throw new RelayError(
595
+ "ENCODE_FAILED",
596
+ `Failed to encode mintAndSwap calldata: ${errorMessage(err)}`,
597
+ err
598
+ );
599
+ }
600
+ }
601
+ /**
602
+ * Submit a `mintAndSwap` transaction. Flow:
603
+ *
604
+ * 1. (optional) pre-flight simulate via provider
605
+ * 2. writeContract through the operator wallet
606
+ * 3. (optional) wait for the receipt and surface gasUsed / status
607
+ *
608
+ * Throws a typed `RelayError` on any failure so the MintingGateway can
609
+ * decide whether to release the ledger lock (`SUBMIT_FAILED` and
610
+ * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
611
+ * need manual review because the tx may still land).
612
+ */
613
+ async submitMintAndSwap(params) {
614
+ if (this.simulateBeforeSubmit && this.provider) {
615
+ const operatorAddr = this.operatorWallet.account?.address;
616
+ if (operatorAddr) {
617
+ try {
618
+ await (0, import_core3.simulateMintAndSwap)(
619
+ this.provider,
620
+ this.relayAddress,
621
+ params.mint,
622
+ params.swap,
623
+ operatorAddr
624
+ );
625
+ } catch (err) {
626
+ const reason = err instanceof import_core3.SimulationError ? err.reason : errorMessage(err);
627
+ throw new RelayError(
628
+ "SIMULATION_FAILED",
629
+ `mintAndSwap would revert: ${reason}`,
630
+ err
631
+ );
632
+ }
633
+ }
634
+ }
635
+ let txHash;
636
+ try {
637
+ txHash = await this.operatorWallet.writeContract({
638
+ address: this.relayAddress,
639
+ abi: import_core3.relayAbi,
640
+ functionName: "mintAndSwap",
641
+ args: [params.mint, params.swap],
642
+ ...this.operatorWallet.account ? { account: this.operatorWallet.account } : {}
643
+ });
644
+ } catch (err) {
645
+ throw new RelayError(
646
+ "SUBMIT_FAILED",
647
+ `Failed to broadcast mintAndSwap: ${errorMessage(err)}`,
648
+ err
649
+ );
650
+ }
651
+ if (!this.provider) {
652
+ return { txHash };
653
+ }
654
+ try {
655
+ const receipt = await this.provider.waitForTransactionReceipt({
656
+ hash: txHash,
657
+ timeout: this.confirmationTimeoutMs
658
+ });
659
+ if (receipt.status !== "success") {
660
+ throw new RelayError(
661
+ "TX_REVERTED",
662
+ `mintAndSwap reverted on-chain (tx=${txHash})`
663
+ );
664
+ }
665
+ return {
666
+ txHash,
667
+ blockNumber: receipt.blockNumber,
668
+ gasUsed: receipt.gasUsed,
669
+ status: receipt.status
670
+ };
671
+ } catch (err) {
672
+ if (err instanceof RelayError) throw err;
673
+ throw new RelayError(
674
+ "TIMEOUT",
675
+ `Timed out waiting for mintAndSwap receipt (tx=${txHash}): ${errorMessage(err)}`,
676
+ err
677
+ );
678
+ }
679
+ }
680
+ };
681
+ function errorMessage(err) {
682
+ return err instanceof Error ? err.message : String(err);
683
+ }
684
+
685
+ // src/relay/feeManager.ts
686
+ var DEFAULT_GAS_UNITS = 500000n;
687
+ var DEFAULT_PREMIUM_BPS = 12e3;
688
+ var FeeManager = class {
689
+ provider;
690
+ operatorWallet;
691
+ mintAndSwapGasUnits;
692
+ gasPremiumBps;
693
+ quoteNativeToUsdt;
694
+ rebalanceThresholdWei;
695
+ rebalanceUsdtAmount;
696
+ swapUsdtToNative;
697
+ constructor(config) {
698
+ if (!config.provider) throw new Error("FeeManager: provider required");
699
+ if (!config.operatorWallet)
700
+ throw new Error("FeeManager: operatorWallet required");
701
+ if (!config.quoteNativeToUsdt)
702
+ throw new Error("FeeManager: quoteNativeToUsdt required");
703
+ this.provider = config.provider;
704
+ this.operatorWallet = config.operatorWallet;
705
+ this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
706
+ this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
707
+ this.quoteNativeToUsdt = config.quoteNativeToUsdt;
708
+ if (config.rebalanceThresholdWei !== void 0) {
709
+ this.rebalanceThresholdWei = config.rebalanceThresholdWei;
710
+ }
711
+ if (config.rebalanceUsdtAmount !== void 0) {
712
+ this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
713
+ }
714
+ if (config.swapUsdtToNative) {
715
+ this.swapUsdtToNative = config.swapUsdtToNative;
716
+ }
717
+ const rebalanceFields = [
718
+ config.rebalanceThresholdWei,
719
+ config.rebalanceUsdtAmount,
720
+ config.swapUsdtToNative
721
+ ];
722
+ const someSet = rebalanceFields.some((v) => v !== void 0);
723
+ const allSet = rebalanceFields.every((v) => v !== void 0);
724
+ if (someSet && !allSet) {
725
+ throw new Error(
726
+ "FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
727
+ );
728
+ }
729
+ }
730
+ /**
731
+ * Estimate the USDT fee the operator should charge for a single
732
+ * `mintAndSwap`. Computed as:
733
+ *
734
+ * nativeCost = gasUnits × gasPrice
735
+ * premiumNativeCost = nativeCost × premiumBps / 10_000
736
+ * usdtFee = quoteNativeToUsdt(premiumNativeCost)
737
+ */
738
+ async estimateGasFee() {
739
+ const gasPrice = await this.provider.getGasPrice();
740
+ const nativeCost = gasPrice * this.mintAndSwapGasUnits;
741
+ const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
742
+ return this.quoteNativeToUsdt(withPremium);
743
+ }
744
+ /**
745
+ * Check the operator's native balance and, if it has dropped below the
746
+ * configured threshold, trigger a USDT→native rebalance via the injected
747
+ * `swapUsdtToNative` function.
748
+ *
749
+ * Returns `true` if a rebalance was performed, `false` otherwise.
750
+ * Silently no-ops when rebalance is not configured.
751
+ */
752
+ async rebalanceIfNeeded() {
753
+ if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
754
+ return false;
755
+ }
756
+ const operatorAddress = this.operatorWallet.account?.address;
757
+ if (!operatorAddress) {
758
+ throw new Error(
759
+ "FeeManager: operator wallet has no account attached \u2014 cannot read balance"
760
+ );
761
+ }
762
+ const balance = await this.provider.getBalance({ address: operatorAddress });
763
+ if (balance >= this.rebalanceThresholdWei) {
764
+ return false;
765
+ }
766
+ await this.swapUsdtToNative(this.rebalanceUsdtAmount);
767
+ return true;
768
+ }
769
+ };
770
+
771
+ // src/gateway/types.ts
772
+ var MintingGatewayError = class extends Error {
773
+ code;
774
+ /**
775
+ * True if the ledger lock was released before this error was thrown,
776
+ * meaning the user can safely retry. False means the funds are still
777
+ * locked (e.g. tx may still land on-chain) and retry would double-spend.
778
+ */
779
+ safeToRetry;
780
+ cause;
781
+ constructor(code, message, opts) {
782
+ super(message);
783
+ this.name = "MintingGatewayError";
784
+ this.code = code;
785
+ this.safeToRetry = opts.safeToRetry;
786
+ if (opts.cause !== void 0) this.cause = opts.cause;
787
+ }
788
+ };
789
+
790
+ // src/gateway/mintingGateway.ts
791
+ var import_core4 = require("@pafi-dev/core");
792
+ var DEFAULT_LOCK_BUFFER_MS = 6e4;
793
+ var MintingGateway = class {
794
+ ledger;
795
+ policy;
796
+ signer;
797
+ relayService;
798
+ now;
799
+ defaultLockBufferMs;
800
+ constructor(config) {
801
+ if (!config.ledger) throw new Error("MintingGateway: ledger required");
802
+ if (!config.policy) throw new Error("MintingGateway: policy required");
803
+ if (!config.signer) throw new Error("MintingGateway: signer required");
804
+ if (!config.relayService)
805
+ throw new Error("MintingGateway: relayService required");
806
+ this.ledger = config.ledger;
807
+ this.policy = config.policy;
808
+ this.signer = config.signer;
809
+ this.relayService = config.relayService;
810
+ this.now = config.now ?? (() => Date.now());
811
+ this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
812
+ }
813
+ async processMintAndCashOut(request) {
814
+ const { receiverConsent, receiverSignature } = request;
815
+ if (!receiverConsent || !receiverSignature) {
816
+ throw new MintingGatewayError(
817
+ "INVALID_REQUEST",
818
+ "receiverConsent and receiverSignature are required",
819
+ { safeToRetry: true }
820
+ );
821
+ }
822
+ if (receiverConsent.amount <= 0n) {
823
+ throw new MintingGatewayError(
824
+ "INVALID_REQUEST",
825
+ "consent amount must be positive",
826
+ { safeToRetry: true }
827
+ );
828
+ }
829
+ if (receiverConsent.originalReceiver !== request.userAddress) {
830
+ throw new MintingGatewayError(
831
+ "INVALID_REQUEST",
832
+ "consent.originalReceiver must equal request.userAddress",
833
+ { safeToRetry: true }
834
+ );
835
+ }
836
+ const nowSec = BigInt(Math.floor(this.now() / 1e3));
837
+ if (receiverConsent.deadline <= nowSec) {
838
+ throw new MintingGatewayError(
839
+ "CONSENT_EXPIRED",
840
+ "ReceiverConsent deadline has already passed",
841
+ { safeToRetry: true }
842
+ );
843
+ }
844
+ const consentResult = await (0, import_core4.verifyReceiverConsent)(
845
+ request.domain,
846
+ receiverConsent,
847
+ receiverSignature,
848
+ request.userAddress
849
+ );
850
+ if (!consentResult.isValid) {
851
+ throw new MintingGatewayError(
852
+ "INVALID_CONSENT_SIGNATURE",
853
+ `ReceiverConsent signature did not recover to ${request.userAddress}`,
854
+ { safeToRetry: true }
855
+ );
856
+ }
857
+ const policyDecision = await this.policy.evaluate({
858
+ userAddress: request.userAddress,
859
+ amount: receiverConsent.amount,
860
+ pointTokenAddress: request.pointTokenAddress,
861
+ chainId: request.chainId
862
+ });
863
+ if (!policyDecision.approved) {
864
+ const code = policyDecision.reason?.toLowerCase().includes("insufficient") ? "INSUFFICIENT_BALANCE" : "POLICY_REJECTED";
865
+ throw new MintingGatewayError(
866
+ code,
867
+ policyDecision.reason ?? "Minting request rejected by policy engine",
868
+ { safeToRetry: true }
869
+ );
870
+ }
871
+ const lockDurationMs = request.lockDurationMs ?? this.computeLockDurationMs(receiverConsent.deadline);
872
+ let lockId;
873
+ try {
874
+ lockId = await this.ledger.lockForMinting(
875
+ request.userAddress,
876
+ receiverConsent.amount,
877
+ lockDurationMs
878
+ );
879
+ } catch (err) {
880
+ throw new MintingGatewayError(
881
+ "INSUFFICIENT_BALANCE",
882
+ `Failed to lock ledger balance: ${errorMessage2(err)}`,
883
+ { safeToRetry: true, cause: err }
884
+ );
885
+ }
886
+ try {
887
+ let minterSignature;
888
+ try {
889
+ minterSignature = await this.signer.signMintRequest(request.domain, {
890
+ to: request.userAddress,
891
+ amount: receiverConsent.amount,
892
+ nonce: receiverConsent.nonce,
893
+ deadline: receiverConsent.deadline
894
+ });
895
+ } catch (err) {
896
+ await this.releaseLockSafely(lockId);
897
+ throw new MintingGatewayError(
898
+ "SIGNER_FAILED",
899
+ `Issuer signer failed: ${errorMessage2(err)}`,
900
+ { safeToRetry: true, cause: err }
901
+ );
902
+ }
903
+ const mintParams = {
904
+ pointToken: request.pointTokenAddress,
905
+ receiver: request.userAddress,
906
+ amount: receiverConsent.amount,
907
+ deadline: receiverConsent.deadline,
908
+ minterSig: minterSignature.serialized,
909
+ receiverSig: receiverSignature,
910
+ extData: receiverConsent.extData
911
+ };
912
+ const swapParams = {
913
+ path: request.swapPath,
914
+ deadline: request.swapDeadline
915
+ };
916
+ let relayResult;
917
+ try {
918
+ relayResult = await this.relayService.submitMintAndSwap({
919
+ mint: mintParams,
920
+ swap: swapParams
921
+ });
922
+ } catch (err) {
923
+ await this.handleRelayFailure(err, lockId);
924
+ }
925
+ const result = {
926
+ txHash: relayResult.txHash,
927
+ lockId
928
+ };
929
+ if (relayResult.blockNumber !== void 0) {
930
+ result.blockNumber = relayResult.blockNumber;
931
+ }
932
+ if (relayResult.gasUsed !== void 0) {
933
+ result.gasUsed = relayResult.gasUsed;
934
+ }
935
+ return result;
936
+ } catch (err) {
937
+ if (err instanceof MintingGatewayError) throw err;
938
+ await this.releaseLockSafely(lockId);
939
+ throw new MintingGatewayError(
940
+ "RELAY_SUBMIT_FAILED",
941
+ `Unexpected error: ${errorMessage2(err)}`,
942
+ { safeToRetry: true, cause: err }
943
+ );
944
+ }
945
+ }
946
+ // ---------------------------------------------------------------------------
947
+ // Internals
948
+ // ---------------------------------------------------------------------------
949
+ computeLockDurationMs(consentDeadlineSec) {
950
+ const nowMs = this.now();
951
+ const deadlineMs = Number(consentDeadlineSec) * 1e3;
952
+ const remaining = Math.max(0, deadlineMs - nowMs);
953
+ return remaining + this.defaultLockBufferMs;
954
+ }
955
+ /**
956
+ * Map a RelayError to a MintingGatewayError, releasing the lock only
957
+ * when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
958
+ * leave the lock in place because the tx may still be in the mempool
959
+ * or already mined — releasing would enable a double-spend on retry.
960
+ * Always throws.
961
+ */
962
+ async handleRelayFailure(err, lockId) {
963
+ if (err instanceof RelayError) {
964
+ switch (err.code) {
965
+ case "ENCODE_FAILED":
966
+ case "SIMULATION_FAILED":
967
+ case "SUBMIT_FAILED":
968
+ case "NOT_CONFIGURED":
969
+ await this.releaseLockSafely(lockId);
970
+ throw new MintingGatewayError(
971
+ err.code === "SIMULATION_FAILED" ? "RELAY_SIMULATION_FAILED" : "RELAY_SUBMIT_FAILED",
972
+ err.message,
973
+ { safeToRetry: true, cause: err }
974
+ );
975
+ case "TX_REVERTED":
976
+ throw new MintingGatewayError("RELAY_REVERTED", err.message, {
977
+ safeToRetry: false,
978
+ cause: err
979
+ });
980
+ case "TIMEOUT":
981
+ throw new MintingGatewayError("RELAY_TIMEOUT", err.message, {
982
+ safeToRetry: false,
983
+ cause: err
984
+ });
985
+ }
986
+ }
987
+ await this.releaseLockSafely(lockId);
988
+ throw new MintingGatewayError(
989
+ "RELAY_SUBMIT_FAILED",
990
+ `Unexpected relay error: ${errorMessage2(err)}`,
991
+ { safeToRetry: true, cause: err }
992
+ );
993
+ }
994
+ /**
995
+ * Release a lock, swallowing any secondary error. We never want a lock
996
+ * release failure to mask the original error — the lock will auto-expire
997
+ * via TTL anyway.
998
+ */
999
+ async releaseLockSafely(lockId) {
1000
+ try {
1001
+ await this.ledger.releaseLock(lockId);
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ };
1006
+ function errorMessage2(err) {
1007
+ return err instanceof Error ? err.message : String(err);
1008
+ }
1009
+
1010
+ // src/indexer/types.ts
1011
+ var InMemoryCursorStore = class {
1012
+ cursor;
1013
+ async load() {
1014
+ return this.cursor;
1015
+ }
1016
+ async save(blockNumber) {
1017
+ this.cursor = blockNumber;
1018
+ }
1019
+ };
1020
+
1021
+ // src/indexer/pointIndexer.ts
1022
+ var import_viem5 = require("viem");
1023
+ var TRANSFER_EVENT = (0, import_viem5.parseAbiItem)(
1024
+ "event Transfer(address indexed from, address indexed to, uint256 value)"
1025
+ );
1026
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
1027
+ var DEFAULT_CONFIRMATIONS = 3;
1028
+ var DEFAULT_BATCH_SIZE = 2000n;
1029
+ var DEFAULT_POLL_INTERVAL_MS = 5e3;
1030
+ var PointIndexer = class {
1031
+ provider;
1032
+ pointTokenAddress;
1033
+ ledger;
1034
+ cursorStore;
1035
+ startBlock;
1036
+ confirmations;
1037
+ batchSize;
1038
+ pollIntervalMs;
1039
+ running = false;
1040
+ timer;
1041
+ constructor(config) {
1042
+ if (!config.provider) throw new Error("PointIndexer: provider required");
1043
+ if (!config.pointTokenAddress)
1044
+ throw new Error("PointIndexer: pointTokenAddress required");
1045
+ if (!config.ledger) throw new Error("PointIndexer: ledger required");
1046
+ this.provider = config.provider;
1047
+ this.pointTokenAddress = config.pointTokenAddress;
1048
+ this.ledger = config.ledger;
1049
+ this.cursorStore = config.cursorStore ?? new InMemoryCursorStore();
1050
+ this.startBlock = config.fromBlock ?? 0n;
1051
+ this.confirmations = BigInt(config.confirmations ?? DEFAULT_CONFIRMATIONS);
1052
+ this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE));
1053
+ this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1054
+ }
1055
+ // -------------------------------------------------------------------------
1056
+ // Lifecycle
1057
+ // -------------------------------------------------------------------------
1058
+ /** Begin polling. Schedules `tick()` on a loop. */
1059
+ start() {
1060
+ if (this.running) return;
1061
+ this.running = true;
1062
+ void this.tick();
1063
+ }
1064
+ /** Stop polling. Safe to call multiple times. */
1065
+ stop() {
1066
+ this.running = false;
1067
+ if (this.timer) {
1068
+ clearTimeout(this.timer);
1069
+ this.timer = void 0;
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Run one poll cycle: load cursor → scan [cursor, safeHead] in
1074
+ * `batchSize` chunks → persist new cursor. Swallows any error and
1075
+ * schedules the next tick. Visible for test harnesses via a public name.
1076
+ */
1077
+ async tick() {
1078
+ if (!this.running) return;
1079
+ try {
1080
+ const latest = await this.provider.getBlockNumber();
1081
+ const safeHead = latest - this.confirmations;
1082
+ if (safeHead < 0n) {
1083
+ this.scheduleNext();
1084
+ return;
1085
+ }
1086
+ const stored = await this.cursorStore.load();
1087
+ const from = stored ?? this.startBlock;
1088
+ if (from > safeHead) {
1089
+ this.scheduleNext();
1090
+ return;
1091
+ }
1092
+ await this.processBlockRange(from, safeHead);
1093
+ } catch {
1094
+ }
1095
+ this.scheduleNext();
1096
+ }
1097
+ scheduleNext() {
1098
+ if (!this.running) return;
1099
+ this.timer = setTimeout(() => void this.tick(), this.pollIntervalMs);
1100
+ }
1101
+ // -------------------------------------------------------------------------
1102
+ // Block scanning
1103
+ // -------------------------------------------------------------------------
1104
+ /**
1105
+ * Scan `[from, to]` inclusive for mint events in `batchSize` chunks.
1106
+ * Callers can use this directly to backfill a specific range without
1107
+ * engaging `start()`. On completion, the cursor is advanced to `to + 1`.
1108
+ */
1109
+ async processBlockRange(from, to) {
1110
+ if (from > to) return;
1111
+ let cursor = from;
1112
+ while (cursor <= to) {
1113
+ const chunkEnd = cursor + this.batchSize - 1n > to ? to : cursor + this.batchSize - 1n;
1114
+ const logs = await this.provider.getLogs({
1115
+ address: this.pointTokenAddress,
1116
+ event: TRANSFER_EVENT,
1117
+ args: { from: ZERO_ADDRESS },
1118
+ fromBlock: cursor,
1119
+ toBlock: chunkEnd
1120
+ });
1121
+ const events = this.decodeMintEvents(logs);
1122
+ events.sort((a, b) => {
1123
+ if (a.blockNumber !== b.blockNumber) {
1124
+ return a.blockNumber < b.blockNumber ? -1 : 1;
1125
+ }
1126
+ return a.logIndex - b.logIndex;
1127
+ });
1128
+ for (const evt of events) {
1129
+ await this.finalize(evt);
1130
+ }
1131
+ await this.cursorStore.save(chunkEnd + 1n);
1132
+ cursor = chunkEnd + 1n;
1133
+ }
1134
+ }
1135
+ // -------------------------------------------------------------------------
1136
+ // Internals
1137
+ // -------------------------------------------------------------------------
1138
+ decodeMintEvents(logs) {
1139
+ const out = [];
1140
+ for (const log of logs) {
1141
+ const args = log.args;
1142
+ if (!args.from || !args.to || args.value === void 0) continue;
1143
+ if ((0, import_viem5.getAddress)(args.from) !== ZERO_ADDRESS) continue;
1144
+ if (log.blockNumber === null || log.transactionHash === null) continue;
1145
+ out.push({
1146
+ to: (0, import_viem5.getAddress)(args.to),
1147
+ amount: args.value,
1148
+ blockNumber: log.blockNumber,
1149
+ txHash: log.transactionHash,
1150
+ logIndex: log.logIndex ?? 0
1151
+ });
1152
+ }
1153
+ return out;
1154
+ }
1155
+ /**
1156
+ * Finalize a single mint event: match it to a PENDING lock in the
1157
+ * ledger, then call `deductBalance` (which also resolves the lock in
1158
+ * the default `MemoryPointLedger`).
1159
+ *
1160
+ * No-matching-lock is a valid state: it means either the lock already
1161
+ * expired, or the mint was authorized out-of-band (e.g. a direct
1162
+ * `PointToken.mint()` from an EOA minter for testing). In that case we
1163
+ * do NOT touch the ledger — crediting here would silently allow the
1164
+ * issuer to mint without going through the gateway.
1165
+ */
1166
+ async finalize(evt) {
1167
+ const locks = await this.ledger.getLockedRequests(evt.to);
1168
+ const match = pickMatchingLock(locks, evt.amount);
1169
+ if (!match) return;
1170
+ try {
1171
+ await this.ledger.deductBalance(evt.to, evt.amount, evt.txHash);
1172
+ } catch {
1173
+ return;
1174
+ }
1175
+ try {
1176
+ await this.ledger.updateMintStatus(match.lockId, "MINTED", evt.txHash);
1177
+ } catch {
1178
+ }
1179
+ }
1180
+ };
1181
+ function pickMatchingLock(locks, amount) {
1182
+ let best;
1183
+ for (const lock of locks) {
1184
+ if (lock.status !== "PENDING") continue;
1185
+ if (lock.amount !== amount) continue;
1186
+ if (!best || lock.createdAt < best.createdAt) {
1187
+ best = lock;
1188
+ }
1189
+ }
1190
+ return best;
1191
+ }
1192
+
1193
+ // src/api/handlers.ts
1194
+ var import_viem6 = require("viem");
1195
+ var import_core5 = require("@pafi-dev/core");
1196
+ var IssuerApiHandlers = class {
1197
+ authService;
1198
+ gateway;
1199
+ ledger;
1200
+ provider;
1201
+ pointTokenAddress;
1202
+ chainId;
1203
+ contracts;
1204
+ feeManager;
1205
+ poolsProvider;
1206
+ constructor(config) {
1207
+ this.authService = config.authService;
1208
+ this.gateway = config.gateway;
1209
+ this.ledger = config.ledger;
1210
+ this.provider = config.provider;
1211
+ this.pointTokenAddress = config.pointTokenAddress;
1212
+ this.chainId = config.chainId;
1213
+ this.contracts = config.contracts;
1214
+ if (config.feeManager) this.feeManager = config.feeManager;
1215
+ if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
1216
+ }
1217
+ // =========================================================================
1218
+ // Public handlers (no auth required)
1219
+ // =========================================================================
1220
+ /** `GET /auth/nonce` */
1221
+ async handleGetNonce() {
1222
+ const nonce = await this.authService.getNonce();
1223
+ return { nonce };
1224
+ }
1225
+ /** `POST /auth/login` */
1226
+ async handleLogin(body) {
1227
+ if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
1228
+ throw new Error("handleLogin: message and signature are required");
1229
+ }
1230
+ const result = await this.authService.login(body.message, body.signature);
1231
+ return {
1232
+ token: result.token,
1233
+ userAddress: result.userAddress,
1234
+ expiresAt: result.expiresAt.getTime()
1235
+ };
1236
+ }
1237
+ /**
1238
+ * `GET /config?chainId=<id>`
1239
+ *
1240
+ * Returns the contract addresses and chain id that the frontend SDK
1241
+ * needs to build EIP-712 messages and interact with on-chain.
1242
+ */
1243
+ async handleConfig(chainId) {
1244
+ if (chainId !== this.chainId) {
1245
+ throw new Error(
1246
+ `handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
1247
+ );
1248
+ }
1249
+ return { chainId: this.chainId, contracts: { ...this.contracts } };
1250
+ }
1251
+ /** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
1252
+ async handleGasFee() {
1253
+ if (!this.feeManager) {
1254
+ throw new Error(
1255
+ "handleGasFee: feeManager is not configured on this issuer"
1256
+ );
1257
+ }
1258
+ const gasFeeUsdt = await this.feeManager.estimateGasFee();
1259
+ return { gasFeeUsdt };
1260
+ }
1261
+ // =========================================================================
1262
+ // Protected handlers (JWT required — userAddress extracted by middleware)
1263
+ // =========================================================================
1264
+ /** `POST /auth/logout` */
1265
+ async handleLogout(token) {
1266
+ await this.authService.logout(token);
1267
+ }
1268
+ /**
1269
+ * `GET /pools?chainId=<id>&pointToken=<addr>`
1270
+ *
1271
+ * Delegates to the injected `PoolsProvider`. The handler itself does
1272
+ * not know where pools come from — that's an issuer decision.
1273
+ */
1274
+ async handlePools(_userAddress, request) {
1275
+ if (!this.poolsProvider) {
1276
+ throw new Error(
1277
+ "handlePools: poolsProvider is not configured on this issuer"
1278
+ );
1279
+ }
1280
+ if (request.chainId !== this.chainId) {
1281
+ throw new Error(
1282
+ `handlePools: unsupported chainId ${request.chainId}`
1283
+ );
1284
+ }
1285
+ return this.poolsProvider(request);
1286
+ }
1287
+ /**
1288
+ * `GET /user?chainId=<id>&user=<addr>&pointToken=<addr>`
1289
+ *
1290
+ * Returns per-user state the frontend needs to build a fresh
1291
+ * `ReceiverConsent`: on-chain nonces + minter status + off-chain
1292
+ * balance.
1293
+ */
1294
+ async handleUser(userAddress, request) {
1295
+ if (request.chainId !== this.chainId) {
1296
+ throw new Error(
1297
+ `handleUser: unsupported chainId ${request.chainId}`
1298
+ );
1299
+ }
1300
+ const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
1301
+ const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
1302
+ if (normalizedAuthed !== normalizedRequest) {
1303
+ throw new Error(
1304
+ "handleUser: request userAddress must match authenticated user"
1305
+ );
1306
+ }
1307
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1308
+ if (pointToken !== this.pointTokenAddress) {
1309
+ throw new Error(
1310
+ `handleUser: unsupported pointToken ${pointToken}`
1311
+ );
1312
+ }
1313
+ const [mintRequestNonce, receiverConsentNonce, balance, minter] = await Promise.all([
1314
+ (0, import_core5.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
1315
+ (0, import_core5.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
1316
+ this.ledger.getBalance(normalizedAuthed),
1317
+ (0, import_core5.isMinter)(this.provider, pointToken, normalizedAuthed)
1318
+ ]);
1319
+ return {
1320
+ mintRequestNonce,
1321
+ receiverConsentNonce,
1322
+ balance,
1323
+ isMinter: minter
1324
+ };
1325
+ }
1326
+ /**
1327
+ * `POST /build-consent-typed-data`
1328
+ *
1329
+ * Backend builds the full EIP-712 typed data payload for a
1330
+ * ReceiverConsent message. The domain (name, version, chainId,
1331
+ * verifyingContract) is read from the PointToken contract — mobile
1332
+ * never needs to know or hardcode these values. Forward the
1333
+ * response directly to `wallet.signTypedData()`.
1334
+ *
1335
+ * This ensures a single source of truth for domain + types, and
1336
+ * makes contract upgrades (domain version bump) transparent to
1337
+ * mobile apps — no app store review needed.
1338
+ */
1339
+ async handleBuildConsentTypedData(userAddress, request) {
1340
+ if (request.chainId !== this.chainId) {
1341
+ throw new Error(
1342
+ `handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
1343
+ );
1344
+ }
1345
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1346
+ if (pointToken !== this.pointTokenAddress) {
1347
+ throw new Error(
1348
+ `handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
1349
+ );
1350
+ }
1351
+ const name = await (0, import_core5.getTokenName)(this.provider, pointToken);
1352
+ const domain = {
1353
+ name,
1354
+ verifyingContract: pointToken,
1355
+ chainId: this.chainId
1356
+ };
1357
+ const typedData = (0, import_core5.buildReceiverConsentTypedData)(domain, request.receiverConsent);
1358
+ return {
1359
+ typedData: {
1360
+ domain: typedData.domain,
1361
+ types: typedData.types,
1362
+ primaryType: typedData.primaryType,
1363
+ message: typedData.message
1364
+ }
1365
+ };
1366
+ }
1367
+ /**
1368
+ * `POST /claim-and-swap`
1369
+ *
1370
+ * The terminal handler: forwards the verified consent to the
1371
+ * MintingGateway, which runs the 11-step flow.
1372
+ */
1373
+ async handleClaimAndSwap(userAddress, request) {
1374
+ if (request.chainId !== this.chainId) {
1375
+ throw new Error(
1376
+ `handleClaimAndSwap: unsupported chainId ${request.chainId}`
1377
+ );
1378
+ }
1379
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1380
+ if (pointToken !== this.pointTokenAddress) {
1381
+ throw new Error(
1382
+ `handleClaimAndSwap: unsupported pointToken ${pointToken}`
1383
+ );
1384
+ }
1385
+ const result = await this.gateway.processMintAndCashOut({
1386
+ userAddress: (0, import_viem6.getAddress)(userAddress),
1387
+ pointTokenAddress: pointToken,
1388
+ chainId: request.chainId,
1389
+ domain: request.domain,
1390
+ receiverConsent: request.receiverConsent,
1391
+ receiverSignature: request.receiverSignature,
1392
+ swapPath: request.swapPath,
1393
+ swapDeadline: request.swapDeadline
1394
+ });
1395
+ const response = {
1396
+ txHash: result.txHash,
1397
+ lockId: result.lockId
1398
+ };
1399
+ if (result.blockNumber !== void 0)
1400
+ response.blockNumber = result.blockNumber;
1401
+ if (result.gasUsed !== void 0) response.gasUsed = result.gasUsed;
1402
+ return response;
1403
+ }
1404
+ };
1405
+
1406
+ // src/pools/subgraphPoolsProvider.ts
1407
+ var DEFAULT_CACHE_TTL_MS = 3e4;
1408
+ var POOL_QUERY = `
1409
+ query GetPoolForPointToken($id: ID!) {
1410
+ pafiToken(id: $id) {
1411
+ id
1412
+ pool {
1413
+ id
1414
+ feeTier
1415
+ tickSpacing
1416
+ hooks
1417
+ token0 { id }
1418
+ token1 { id }
1419
+ }
1420
+ }
1421
+ }
1422
+ `;
1423
+ function createSubgraphPoolsProvider(config) {
1424
+ if (!config.subgraphUrl) {
1425
+ throw new Error(
1426
+ "createSubgraphPoolsProvider: subgraphUrl is required"
1427
+ );
1428
+ }
1429
+ const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1430
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1431
+ const now = config.now ?? (() => Date.now());
1432
+ const cache = /* @__PURE__ */ new Map();
1433
+ if (!fetchImpl) {
1434
+ throw new Error(
1435
+ "createSubgraphPoolsProvider: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
1436
+ );
1437
+ }
1438
+ return async (request) => {
1439
+ const cacheKey = `${request.chainId}:${request.pointTokenAddress.toLowerCase()}`;
1440
+ if (cacheTtl > 0) {
1441
+ const cached = cache.get(cacheKey);
1442
+ if (cached && cached.expiresAt > now()) {
1443
+ return { pools: cached.pools };
1444
+ }
1445
+ }
1446
+ const pools = await fetchPoolsFromSubgraph(
1447
+ fetchImpl,
1448
+ config.subgraphUrl,
1449
+ request.pointTokenAddress
1450
+ );
1451
+ if (cacheTtl > 0) {
1452
+ cache.set(cacheKey, {
1453
+ expiresAt: now() + cacheTtl,
1454
+ pools
1455
+ });
1456
+ }
1457
+ return { pools };
1458
+ };
1459
+ }
1460
+ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress) {
1461
+ let response;
1462
+ try {
1463
+ response = await fetchImpl(subgraphUrl, {
1464
+ method: "POST",
1465
+ headers: { "Content-Type": "application/json" },
1466
+ body: JSON.stringify({
1467
+ query: POOL_QUERY,
1468
+ variables: { id: pointTokenAddress.toLowerCase() }
1469
+ })
1470
+ });
1471
+ } catch (err) {
1472
+ console.warn(
1473
+ "[subgraphPoolsProvider] subgraph unreachable:",
1474
+ err.message
1475
+ );
1476
+ return [];
1477
+ }
1478
+ if (!response.ok) {
1479
+ console.warn(
1480
+ `[subgraphPoolsProvider] subgraph returned ${response.status}`
1481
+ );
1482
+ return [];
1483
+ }
1484
+ const json = await response.json();
1485
+ if (json.errors && json.errors.length > 0) {
1486
+ console.warn(
1487
+ "[subgraphPoolsProvider] subgraph errors:",
1488
+ json.errors.map((e) => e.message).join("; ")
1489
+ );
1490
+ return [];
1491
+ }
1492
+ const token = json.data?.pafiToken;
1493
+ if (!token || !token.pool) {
1494
+ return [];
1495
+ }
1496
+ const { pool } = token;
1497
+ const [currency0, currency1] = sortCurrencies(
1498
+ pool.token0.id,
1499
+ pool.token1.id
1500
+ );
1501
+ const poolKey = {
1502
+ currency0,
1503
+ currency1,
1504
+ fee: Number(pool.feeTier),
1505
+ tickSpacing: Number(pool.tickSpacing),
1506
+ hooks: pool.hooks
1507
+ };
1508
+ return [poolKey];
1509
+ }
1510
+ function sortCurrencies(a, b) {
1511
+ return a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a];
1512
+ }
1513
+
1514
+ // src/pools/subgraphNativeUsdtQuoter.ts
1515
+ var DEFAULT_CACHE_TTL_MS2 = 3e4;
1516
+ var DEFAULT_FALLBACK_PRICE = 3e3;
1517
+ var DEFAULT_USDT_DECIMALS = 6;
1518
+ var DEFAULT_NATIVE_DECIMALS = 18;
1519
+ var PRICE_QUERY = `
1520
+ query GetEthPrice {
1521
+ bundle(id: "1") {
1522
+ ethPriceUSD
1523
+ }
1524
+ }
1525
+ `;
1526
+ function createSubgraphNativeUsdtQuoter(config) {
1527
+ if (!config.subgraphUrl) {
1528
+ throw new Error(
1529
+ "createSubgraphNativeUsdtQuoter: subgraphUrl is required"
1530
+ );
1531
+ }
1532
+ const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
1533
+ const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
1534
+ const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
1535
+ const fallbackPrice = config.fallbackEthPriceUsd ?? DEFAULT_FALLBACK_PRICE;
1536
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1537
+ const now = config.now ?? (() => Date.now());
1538
+ if (!fetchImpl) {
1539
+ throw new Error(
1540
+ "createSubgraphNativeUsdtQuoter: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
1541
+ );
1542
+ }
1543
+ let cached;
1544
+ async function getUsdtPerNative() {
1545
+ if (cacheTtl > 0 && cached && cached.expiresAt > now()) {
1546
+ return cached.usdtPerNative;
1547
+ }
1548
+ const price = await fetchEthPriceFromSubgraph(
1549
+ fetchImpl,
1550
+ config.subgraphUrl
1551
+ );
1552
+ const usdtPerNative = toUsdtPerNative(
1553
+ price ?? fallbackPrice,
1554
+ usdtDecimals
1555
+ );
1556
+ if (cacheTtl > 0) {
1557
+ cached = {
1558
+ usdtPerNative,
1559
+ expiresAt: now() + cacheTtl
1560
+ };
1561
+ }
1562
+ return usdtPerNative;
1563
+ }
1564
+ return async (amountNative) => {
1565
+ if (amountNative === 0n) return 0n;
1566
+ const usdtPerNative = await getUsdtPerNative();
1567
+ return amountNative * usdtPerNative / 10n ** BigInt(nativeDecimals);
1568
+ };
1569
+ }
1570
+ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
1571
+ let response;
1572
+ try {
1573
+ response = await fetchImpl(subgraphUrl, {
1574
+ method: "POST",
1575
+ headers: { "Content-Type": "application/json" },
1576
+ body: JSON.stringify({ query: PRICE_QUERY })
1577
+ });
1578
+ } catch (err) {
1579
+ console.warn(
1580
+ "[subgraphNativeUsdtQuoter] subgraph unreachable:",
1581
+ err.message
1582
+ );
1583
+ return null;
1584
+ }
1585
+ if (!response.ok) {
1586
+ console.warn(
1587
+ `[subgraphNativeUsdtQuoter] subgraph returned ${response.status}`
1588
+ );
1589
+ return null;
1590
+ }
1591
+ const json = await response.json();
1592
+ if (json.errors && json.errors.length > 0) {
1593
+ console.warn(
1594
+ "[subgraphNativeUsdtQuoter] subgraph errors:",
1595
+ json.errors.map((e) => e.message).join("; ")
1596
+ );
1597
+ return null;
1598
+ }
1599
+ const raw = json.data?.bundle?.ethPriceUSD;
1600
+ if (!raw) return null;
1601
+ const parsed = Number(raw);
1602
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1603
+ console.warn(
1604
+ `[subgraphNativeUsdtQuoter] invalid ethPriceUSD from subgraph: ${raw}`
1605
+ );
1606
+ return null;
1607
+ }
1608
+ return parsed;
1609
+ }
1610
+ function toUsdtPerNative(priceFloat, usdtDecimals) {
1611
+ const fixed = priceFloat.toFixed(usdtDecimals);
1612
+ const [whole, fraction = ""] = fixed.split(".");
1613
+ const padded = (fraction + "0".repeat(usdtDecimals)).slice(0, usdtDecimals);
1614
+ return BigInt(whole + padded);
1615
+ }
1616
+
1617
+ // src/config.ts
1618
+ function createIssuerService(config) {
1619
+ if (!config.provider) {
1620
+ throw new Error("createIssuerService: provider is required");
1621
+ }
1622
+ if (!config.operatorWallet) {
1623
+ throw new Error("createIssuerService: operatorWallet is required");
1624
+ }
1625
+ if (!config.signer) {
1626
+ throw new Error("createIssuerService: signer is required");
1627
+ }
1628
+ if (!config.pointTokenAddress) {
1629
+ throw new Error("createIssuerService: pointTokenAddress is required");
1630
+ }
1631
+ if (!config.relayAddress) {
1632
+ throw new Error("createIssuerService: relayAddress is required");
1633
+ }
1634
+ if (!config.auth?.jwtSecret) {
1635
+ throw new Error("createIssuerService: auth.jwtSecret is required");
1636
+ }
1637
+ if (!config.auth?.domain) {
1638
+ throw new Error("createIssuerService: auth.domain is required");
1639
+ }
1640
+ const ledger = config.ledger ?? new MemoryPointLedger();
1641
+ const sessionStore = config.sessionStore ?? new MemorySessionStore();
1642
+ const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
1643
+ const authServiceConfig = {
1644
+ sessionStore,
1645
+ jwtSecret: config.auth.jwtSecret,
1646
+ domain: config.auth.domain,
1647
+ chainId: config.chainId
1648
+ };
1649
+ if (config.auth.jwtExpiresIn) {
1650
+ authServiceConfig.jwtExpiresIn = config.auth.jwtExpiresIn;
1651
+ }
1652
+ const authService = new AuthService(authServiceConfig);
1653
+ const relayServiceConfig = {
1654
+ relayAddress: config.relayAddress,
1655
+ operatorWallet: config.operatorWallet,
1656
+ provider: config.provider
1657
+ };
1658
+ if (config.relay?.simulateBeforeSubmit !== void 0) {
1659
+ relayServiceConfig.simulateBeforeSubmit = config.relay.simulateBeforeSubmit;
1660
+ }
1661
+ if (config.relay?.confirmationTimeoutMs !== void 0) {
1662
+ relayServiceConfig.confirmationTimeoutMs = config.relay.confirmationTimeoutMs;
1663
+ }
1664
+ const relayService = new RelayService(relayServiceConfig);
1665
+ let feeManager;
1666
+ if (config.fee) {
1667
+ feeManager = new FeeManager({
1668
+ ...config.fee,
1669
+ provider: config.provider,
1670
+ operatorWallet: config.operatorWallet
1671
+ });
1672
+ }
1673
+ const gatewayConfig = {
1674
+ ledger,
1675
+ policy,
1676
+ signer: config.signer,
1677
+ relayService
1678
+ };
1679
+ if (config.gateway?.defaultLockBufferMs !== void 0) {
1680
+ gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
1681
+ }
1682
+ const gateway = new MintingGateway(gatewayConfig);
1683
+ const indexerConfig = {
1684
+ provider: config.provider,
1685
+ pointTokenAddress: config.pointTokenAddress,
1686
+ ledger
1687
+ };
1688
+ if (config.indexer?.fromBlock !== void 0) {
1689
+ indexerConfig.fromBlock = config.indexer.fromBlock;
1690
+ }
1691
+ if (config.indexer?.cursorStore) {
1692
+ indexerConfig.cursorStore = config.indexer.cursorStore;
1693
+ }
1694
+ if (config.indexer?.confirmations !== void 0) {
1695
+ indexerConfig.confirmations = config.indexer.confirmations;
1696
+ }
1697
+ if (config.indexer?.batchSize !== void 0) {
1698
+ indexerConfig.batchSize = config.indexer.batchSize;
1699
+ }
1700
+ if (config.indexer?.pollIntervalMs !== void 0) {
1701
+ indexerConfig.pollIntervalMs = config.indexer.pollIntervalMs;
1702
+ }
1703
+ const indexer = new PointIndexer(indexerConfig);
1704
+ const handlersConfig = {
1705
+ authService,
1706
+ gateway,
1707
+ ledger,
1708
+ provider: config.provider,
1709
+ pointTokenAddress: config.pointTokenAddress,
1710
+ chainId: config.chainId,
1711
+ contracts: config.contracts
1712
+ };
1713
+ if (feeManager) handlersConfig.feeManager = feeManager;
1714
+ if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
1715
+ const handlers = new IssuerApiHandlers(handlersConfig);
1716
+ if (config.indexer?.autoStart) {
1717
+ indexer.start();
1718
+ }
1719
+ return {
1720
+ authService,
1721
+ sessionStore,
1722
+ ledger,
1723
+ policy,
1724
+ signer: config.signer,
1725
+ relayService,
1726
+ feeManager,
1727
+ gateway,
1728
+ indexer,
1729
+ handlers
1730
+ };
1731
+ }
1732
+
1733
+ // src/index.ts
1734
+ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
1735
+ // Annotate the CommonJS export names for ESM import in node:
1736
+ 0 && (module.exports = {
1737
+ AuthError,
1738
+ AuthService,
1739
+ DefaultPolicyEngine,
1740
+ FeeManager,
1741
+ InMemoryCursorStore,
1742
+ IssuerApiHandlers,
1743
+ MemoryPointLedger,
1744
+ MemorySessionStore,
1745
+ MintingGateway,
1746
+ MintingGatewayError,
1747
+ NonceManager,
1748
+ PAFI_ISSUER_SDK_VERSION,
1749
+ PointIndexer,
1750
+ PrivateKeySigner,
1751
+ RelayError,
1752
+ RelayService,
1753
+ authenticateRequest,
1754
+ createIssuerService,
1755
+ createSubgraphNativeUsdtQuoter,
1756
+ createSubgraphPoolsProvider,
1757
+ encodeExtData
1758
+ });
1759
+ //# sourceMappingURL=index.cjs.map