@pafi-dev/issuer 0.3.0-beta.10 → 0.3.0-beta.11
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 +164 -292
- package/dist/index.cjs +336 -310
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -121
- package/dist/index.d.ts +100 -121
- package/dist/index.js +312 -286
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,201 +1,3 @@
|
|
|
1
|
-
// src/ledger/memoryLedger.ts
|
|
2
|
-
import { getAddress } from "viem";
|
|
3
|
-
var MemoryPointLedger = class {
|
|
4
|
-
balances = /* @__PURE__ */ new Map();
|
|
5
|
-
locks = /* @__PURE__ */ new Map();
|
|
6
|
-
nextLockId = 1;
|
|
7
|
-
now;
|
|
8
|
-
constructor(opts = {}) {
|
|
9
|
-
this.now = opts.now ?? (() => Date.now());
|
|
10
|
-
}
|
|
11
|
-
// -------------------------------------------------------------------------
|
|
12
|
-
// Read
|
|
13
|
-
// -------------------------------------------------------------------------
|
|
14
|
-
async getBalance(userAddress, tokenAddress) {
|
|
15
|
-
const user = getAddress(userAddress);
|
|
16
|
-
const token = normalizeToken(tokenAddress);
|
|
17
|
-
this.purgeExpired();
|
|
18
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
19
|
-
const locked = this.lockedTotalFor(user, token);
|
|
20
|
-
return total - locked;
|
|
21
|
-
}
|
|
22
|
-
async getLockedRequests(userAddress, tokenAddress) {
|
|
23
|
-
const user = getAddress(userAddress);
|
|
24
|
-
const token = normalizeToken(tokenAddress);
|
|
25
|
-
this.purgeExpired();
|
|
26
|
-
const out = [];
|
|
27
|
-
for (const lock of this.locks.values()) {
|
|
28
|
-
if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
29
|
-
out.push({ ...lock });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
// -------------------------------------------------------------------------
|
|
35
|
-
// Write
|
|
36
|
-
// -------------------------------------------------------------------------
|
|
37
|
-
async creditBalance(userAddress, amount, _reason, tokenAddress) {
|
|
38
|
-
if (amount <= 0n) {
|
|
39
|
-
throw new Error("MemoryPointLedger: credit amount must be positive");
|
|
40
|
-
}
|
|
41
|
-
const user = getAddress(userAddress);
|
|
42
|
-
const token = normalizeToken(tokenAddress);
|
|
43
|
-
const key = balanceKey(user, token);
|
|
44
|
-
const current = this.balances.get(key) ?? 0n;
|
|
45
|
-
this.balances.set(key, current + amount);
|
|
46
|
-
}
|
|
47
|
-
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
48
|
-
if (amount <= 0n) {
|
|
49
|
-
throw new Error("MemoryPointLedger: lock amount must be positive");
|
|
50
|
-
}
|
|
51
|
-
if (lockDurationMs <= 0) {
|
|
52
|
-
throw new Error("MemoryPointLedger: lockDurationMs must be positive");
|
|
53
|
-
}
|
|
54
|
-
const user = getAddress(userAddress);
|
|
55
|
-
const token = normalizeToken(tokenAddress);
|
|
56
|
-
this.purgeExpired();
|
|
57
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
58
|
-
const alreadyLocked = this.lockedTotalFor(user, token);
|
|
59
|
-
const available = total - alreadyLocked;
|
|
60
|
-
if (available < amount) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const lockId = `lock-${this.nextLockId++}`;
|
|
66
|
-
const now = this.now();
|
|
67
|
-
const lock = {
|
|
68
|
-
lockId,
|
|
69
|
-
userAddress: user,
|
|
70
|
-
amount,
|
|
71
|
-
status: "PENDING",
|
|
72
|
-
createdAt: now,
|
|
73
|
-
expiresAt: now + lockDurationMs
|
|
74
|
-
};
|
|
75
|
-
if (tokenAddress !== void 0) {
|
|
76
|
-
lock.tokenAddress = getAddress(tokenAddress);
|
|
77
|
-
}
|
|
78
|
-
this.locks.set(lockId, lock);
|
|
79
|
-
return lockId;
|
|
80
|
-
}
|
|
81
|
-
async releaseLock(lockId) {
|
|
82
|
-
const lock = this.locks.get(lockId);
|
|
83
|
-
if (!lock) return;
|
|
84
|
-
if (lock.status === "PENDING") {
|
|
85
|
-
this.locks.delete(lockId);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
89
|
-
if (amount <= 0n) {
|
|
90
|
-
throw new Error("MemoryPointLedger: deduct amount must be positive");
|
|
91
|
-
}
|
|
92
|
-
const user = getAddress(userAddress);
|
|
93
|
-
const token = normalizeToken(tokenAddress);
|
|
94
|
-
const key = balanceKey(user, token);
|
|
95
|
-
const current = this.balances.get(key) ?? 0n;
|
|
96
|
-
if (current < amount) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
this.balances.set(key, current - amount);
|
|
102
|
-
for (const lock of this.locks.values()) {
|
|
103
|
-
if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
104
|
-
lock.status = "MINTED";
|
|
105
|
-
lock.txHash = txHash;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async updateMintStatus(lockId, status, txHash) {
|
|
111
|
-
const lock = this.locks.get(lockId);
|
|
112
|
-
if (!lock) {
|
|
113
|
-
throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
|
|
114
|
-
}
|
|
115
|
-
lock.status = status;
|
|
116
|
-
if (txHash) lock.txHash = txHash;
|
|
117
|
-
}
|
|
118
|
-
// -------------------------------------------------------------------------
|
|
119
|
-
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
120
|
-
// -------------------------------------------------------------------------
|
|
121
|
-
pendingCredits = /* @__PURE__ */ new Map();
|
|
122
|
-
nextCreditId = 1;
|
|
123
|
-
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
124
|
-
if (amount <= 0n) {
|
|
125
|
-
throw new Error(
|
|
126
|
-
"MemoryPointLedger: pending credit amount must be positive"
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
if (durationMs <= 0) {
|
|
130
|
-
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
131
|
-
}
|
|
132
|
-
const user = getAddress(userAddress);
|
|
133
|
-
const lockId = `credit-${this.nextCreditId++}`;
|
|
134
|
-
const now = this.now();
|
|
135
|
-
this.pendingCredits.set(lockId, {
|
|
136
|
-
lockId,
|
|
137
|
-
userAddress: user,
|
|
138
|
-
amount,
|
|
139
|
-
tokenAddress: tokenAddress !== void 0 ? getAddress(tokenAddress) : void 0,
|
|
140
|
-
createdAt: now,
|
|
141
|
-
expiresAt: now + durationMs,
|
|
142
|
-
status: "PENDING"
|
|
143
|
-
});
|
|
144
|
-
return lockId;
|
|
145
|
-
}
|
|
146
|
-
async resolveCreditByBurnTx(lockId, txHash) {
|
|
147
|
-
const credit = this.pendingCredits.get(lockId);
|
|
148
|
-
if (!credit) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
if (credit.status === "RESOLVED") {
|
|
154
|
-
if (credit.txHash === txHash) return;
|
|
155
|
-
throw new Error(
|
|
156
|
-
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
const token = normalizeToken(credit.tokenAddress);
|
|
160
|
-
const key = balanceKey(credit.userAddress, token);
|
|
161
|
-
const current = this.balances.get(key) ?? 0n;
|
|
162
|
-
this.balances.set(key, current + credit.amount);
|
|
163
|
-
credit.status = "RESOLVED";
|
|
164
|
-
credit.txHash = txHash;
|
|
165
|
-
}
|
|
166
|
-
// -------------------------------------------------------------------------
|
|
167
|
-
// Internal helpers
|
|
168
|
-
// -------------------------------------------------------------------------
|
|
169
|
-
/**
|
|
170
|
-
* Auto-expire any PENDING lock past its expiry. Called lazily on every
|
|
171
|
-
* read/write so the in-memory state stays self-cleaning without a timer.
|
|
172
|
-
*/
|
|
173
|
-
purgeExpired() {
|
|
174
|
-
const now = this.now();
|
|
175
|
-
for (const lock of this.locks.values()) {
|
|
176
|
-
if (lock.status === "PENDING" && lock.expiresAt <= now) {
|
|
177
|
-
lock.status = "EXPIRED";
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
lockedTotalFor(userAddress, tokenKey) {
|
|
182
|
-
let total = 0n;
|
|
183
|
-
for (const lock of this.locks.values()) {
|
|
184
|
-
if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
|
|
185
|
-
total += lock.amount;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return total;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
var DEFAULT_TOKEN_KEY = "default";
|
|
192
|
-
function normalizeToken(tokenAddress) {
|
|
193
|
-
return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : getAddress(tokenAddress);
|
|
194
|
-
}
|
|
195
|
-
function balanceKey(user, tokenKey) {
|
|
196
|
-
return `${user}|${tokenKey}`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
1
|
// src/policy/defaultPolicy.ts
|
|
200
2
|
var DefaultPolicyEngine = class {
|
|
201
3
|
ledger;
|
|
@@ -211,6 +13,13 @@ var DefaultPolicyEngine = class {
|
|
|
211
13
|
}
|
|
212
14
|
if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
|
|
213
15
|
if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
|
|
16
|
+
if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
|
|
17
|
+
if (process.env.NODE_ENV === "production") {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
214
23
|
}
|
|
215
24
|
async evaluate(request) {
|
|
216
25
|
if (request.amount <= 0n) {
|
|
@@ -247,34 +56,9 @@ var DefaultPolicyEngine = class {
|
|
|
247
56
|
}
|
|
248
57
|
};
|
|
249
58
|
|
|
250
|
-
// src/signer/privateKeySigner.ts
|
|
251
|
-
import { createWalletClient, http } from "viem";
|
|
252
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
253
|
-
import {
|
|
254
|
-
signMintRequest
|
|
255
|
-
} from "@pafi-dev/core";
|
|
256
|
-
var PrivateKeySigner = class {
|
|
257
|
-
account;
|
|
258
|
-
walletClient;
|
|
259
|
-
constructor(opts) {
|
|
260
|
-
this.account = privateKeyToAccount(opts.privateKey);
|
|
261
|
-
this.walletClient = createWalletClient({
|
|
262
|
-
account: this.account,
|
|
263
|
-
chain: opts.chain,
|
|
264
|
-
transport: http(opts.rpcUrl)
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
async signMintRequest(domain, message) {
|
|
268
|
-
return signMintRequest(this.walletClient, domain, message);
|
|
269
|
-
}
|
|
270
|
-
async getAddress() {
|
|
271
|
-
return this.account.address;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
59
|
// src/auth/memorySessionStore.ts
|
|
276
60
|
import { randomBytes } from "crypto";
|
|
277
|
-
import { getAddress
|
|
61
|
+
import { getAddress } from "viem";
|
|
278
62
|
var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
279
63
|
var MemorySessionStore = class {
|
|
280
64
|
nonces = /* @__PURE__ */ new Map();
|
|
@@ -284,6 +68,11 @@ var MemorySessionStore = class {
|
|
|
284
68
|
nonceTtlMs;
|
|
285
69
|
now;
|
|
286
70
|
constructor(opts = {}) {
|
|
71
|
+
if (process.env.NODE_ENV === "production") {
|
|
72
|
+
console.error(
|
|
73
|
+
"[PAFI] MemorySessionStore is not safe for multi-process/K8s deployments. Session revocations are NOT propagated across pods. Use a Redis-backed session store in production."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
287
76
|
this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
|
|
288
77
|
this.now = opts.now ?? (() => Date.now());
|
|
289
78
|
}
|
|
@@ -310,7 +99,7 @@ var MemorySessionStore = class {
|
|
|
310
99
|
this.purgeExpiredSessions();
|
|
311
100
|
const normalized = {
|
|
312
101
|
...session,
|
|
313
|
-
userAddress:
|
|
102
|
+
userAddress: getAddress(session.userAddress)
|
|
314
103
|
};
|
|
315
104
|
this.sessions.set(session.tokenId, normalized);
|
|
316
105
|
}
|
|
@@ -328,7 +117,7 @@ var MemorySessionStore = class {
|
|
|
328
117
|
this.sessions.delete(tokenId);
|
|
329
118
|
}
|
|
330
119
|
async revokeAllSessions(userAddress) {
|
|
331
|
-
const key =
|
|
120
|
+
const key = getAddress(userAddress);
|
|
332
121
|
for (const [tokenId, session] of this.sessions.entries()) {
|
|
333
122
|
if (session.userAddress === key) {
|
|
334
123
|
this.sessions.delete(tokenId);
|
|
@@ -374,7 +163,7 @@ var NonceManager = class {
|
|
|
374
163
|
// src/auth/loginVerifier.ts
|
|
375
164
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
376
165
|
import { SignJWT, jwtVerify, errors as joseErrors } from "jose";
|
|
377
|
-
import { getAddress as
|
|
166
|
+
import { getAddress as getAddress2 } from "viem";
|
|
378
167
|
import { parseLoginMessage, verifyLoginMessage } from "@pafi-dev/core";
|
|
379
168
|
|
|
380
169
|
// src/auth/errors.ts
|
|
@@ -398,8 +187,8 @@ var AuthService = class {
|
|
|
398
187
|
nonceManager;
|
|
399
188
|
now;
|
|
400
189
|
constructor(config) {
|
|
401
|
-
if (!config.jwtSecret || config.jwtSecret.length <
|
|
402
|
-
throw new Error("AuthService: jwtSecret must be at least
|
|
190
|
+
if (!config.jwtSecret || config.jwtSecret.length < 32) {
|
|
191
|
+
throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
|
|
403
192
|
}
|
|
404
193
|
this.sessionStore = config.sessionStore;
|
|
405
194
|
this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
|
|
@@ -428,6 +217,12 @@ var AuthService = class {
|
|
|
428
217
|
const msg = err instanceof Error ? err.message : String(err);
|
|
429
218
|
throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
|
|
430
219
|
}
|
|
220
|
+
if (parsed.expirationTime == null) {
|
|
221
|
+
throw new AuthError(
|
|
222
|
+
"INVALID_MESSAGE",
|
|
223
|
+
"login message must include expirationTime"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
431
226
|
if (parsed.domain !== this.domain) {
|
|
432
227
|
throw new AuthError(
|
|
433
228
|
"DOMAIN_MISMATCH",
|
|
@@ -464,7 +259,7 @@ var AuthService = class {
|
|
|
464
259
|
"Nonce is unknown, expired, or already used"
|
|
465
260
|
);
|
|
466
261
|
}
|
|
467
|
-
const userAddress =
|
|
262
|
+
const userAddress = getAddress2(verifyResult.address);
|
|
468
263
|
const tokenId = randomBytes2(16).toString("hex");
|
|
469
264
|
const issuedAt = now;
|
|
470
265
|
const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
|
|
@@ -492,7 +287,11 @@ var AuthService = class {
|
|
|
492
287
|
if (payload.jti) {
|
|
493
288
|
await this.sessionStore.revokeSession(payload.jti);
|
|
494
289
|
}
|
|
495
|
-
} catch {
|
|
290
|
+
} catch (err) {
|
|
291
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
292
|
+
if (!msg.includes("not found") && !msg.includes("expired")) {
|
|
293
|
+
console.error("[PAFI] AuthService logout: session store error", err);
|
|
294
|
+
}
|
|
496
295
|
}
|
|
497
296
|
}
|
|
498
297
|
/**
|
|
@@ -531,7 +330,7 @@ var AuthService = class {
|
|
|
531
330
|
throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
|
|
532
331
|
}
|
|
533
332
|
return {
|
|
534
|
-
userAddress:
|
|
333
|
+
userAddress: getAddress2(userAddress),
|
|
535
334
|
chainId,
|
|
536
335
|
tokenId
|
|
537
336
|
};
|
|
@@ -589,7 +388,7 @@ import {
|
|
|
589
388
|
import {
|
|
590
389
|
POINT_TOKEN_V2_ABI,
|
|
591
390
|
buildPartialUserOperation,
|
|
592
|
-
signMintRequest
|
|
391
|
+
signMintRequest
|
|
593
392
|
} from "@pafi-dev/core";
|
|
594
393
|
var RelayService = class {
|
|
595
394
|
/**
|
|
@@ -624,9 +423,20 @@ var RelayService = class {
|
|
|
624
423
|
if (params.deadline <= 0n) {
|
|
625
424
|
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
|
|
626
425
|
}
|
|
426
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
427
|
+
if (params.deadline <= nowSecs) {
|
|
428
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
|
|
429
|
+
}
|
|
430
|
+
const MAX_DEADLINE_WINDOW = 3600n;
|
|
431
|
+
if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
|
|
432
|
+
throw new RelayError(
|
|
433
|
+
"ENCODE_FAILED",
|
|
434
|
+
"prepareMint: deadline exceeds maximum allowed window (1 hour)"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
627
437
|
let minterSig;
|
|
628
438
|
try {
|
|
629
|
-
const sig = await
|
|
439
|
+
const sig = await signMintRequest(
|
|
630
440
|
params.issuerSignerWallet,
|
|
631
441
|
params.domain,
|
|
632
442
|
{
|
|
@@ -672,6 +482,12 @@ var RelayService = class {
|
|
|
672
482
|
"prepareMint: feeRecipient required when feeAmount > 0"
|
|
673
483
|
);
|
|
674
484
|
}
|
|
485
|
+
if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
|
|
486
|
+
throw new RelayError(
|
|
487
|
+
"ENCODE_FAILED",
|
|
488
|
+
"prepareMint: feeRecipient must not be zero address"
|
|
489
|
+
);
|
|
490
|
+
}
|
|
675
491
|
operations.push({
|
|
676
492
|
target: params.pointTokenAddress,
|
|
677
493
|
value: 0n,
|
|
@@ -770,11 +586,14 @@ function errorMessage(err) {
|
|
|
770
586
|
// src/relay/feeManager.ts
|
|
771
587
|
var DEFAULT_GAS_UNITS = 500000n;
|
|
772
588
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
773
|
-
var FeeManager = class {
|
|
589
|
+
var FeeManager = class _FeeManager {
|
|
774
590
|
provider;
|
|
775
591
|
gasUnits;
|
|
776
592
|
gasPremiumBps;
|
|
777
593
|
quoteNativeToFee;
|
|
594
|
+
cachedFee = null;
|
|
595
|
+
cacheExpiresAt = 0;
|
|
596
|
+
static CACHE_TTL_MS = 1e4;
|
|
778
597
|
constructor(config) {
|
|
779
598
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
780
599
|
if (!config.quoteNativeToFee)
|
|
@@ -797,10 +616,17 @@ var FeeManager = class {
|
|
|
797
616
|
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
798
617
|
*/
|
|
799
618
|
async estimateGasFee() {
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
if (this.cachedFee !== null && now < this.cacheExpiresAt) {
|
|
621
|
+
return this.cachedFee;
|
|
622
|
+
}
|
|
800
623
|
const gasPrice = await this.provider.getGasPrice();
|
|
801
624
|
const nativeCost = gasPrice * this.gasUnits;
|
|
802
625
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
803
|
-
|
|
626
|
+
const fee = await this.quoteNativeToFee(withPremium);
|
|
627
|
+
this.cachedFee = fee;
|
|
628
|
+
this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
|
|
629
|
+
return fee;
|
|
804
630
|
}
|
|
805
631
|
};
|
|
806
632
|
|
|
@@ -816,7 +642,7 @@ var InMemoryCursorStore = class {
|
|
|
816
642
|
};
|
|
817
643
|
|
|
818
644
|
// src/indexer/pointIndexer.ts
|
|
819
|
-
import { getAddress as
|
|
645
|
+
import { getAddress as getAddress3, parseAbiItem } from "viem";
|
|
820
646
|
var TRANSFER_EVENT = parseAbiItem(
|
|
821
647
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
822
648
|
);
|
|
@@ -887,7 +713,8 @@ var PointIndexer = class {
|
|
|
887
713
|
return;
|
|
888
714
|
}
|
|
889
715
|
await this.processBlockRange(from, safeHead);
|
|
890
|
-
} catch {
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error("[PAFI] PointIndexer tick error:", err);
|
|
891
718
|
}
|
|
892
719
|
this.scheduleNext();
|
|
893
720
|
}
|
|
@@ -937,10 +764,10 @@ var PointIndexer = class {
|
|
|
937
764
|
for (const log of logs) {
|
|
938
765
|
const args = log.args;
|
|
939
766
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
940
|
-
if (
|
|
767
|
+
if (getAddress3(args.from) !== ZERO_ADDRESS) continue;
|
|
941
768
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
942
769
|
out.push({
|
|
943
|
-
to:
|
|
770
|
+
to: getAddress3(args.to),
|
|
944
771
|
amount: args.value,
|
|
945
772
|
blockNumber: log.blockNumber,
|
|
946
773
|
txHash: log.transactionHash,
|
|
@@ -996,7 +823,7 @@ function pickMatchingLock(locks, amount) {
|
|
|
996
823
|
}
|
|
997
824
|
|
|
998
825
|
// src/indexer/burnIndexer.ts
|
|
999
|
-
import { getAddress as
|
|
826
|
+
import { getAddress as getAddress4, parseAbiItem as parseAbiItem2 } from "viem";
|
|
1000
827
|
var TRANSFER_EVENT2 = parseAbiItem2(
|
|
1001
828
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1002
829
|
);
|
|
@@ -1013,18 +840,7 @@ var BurnIndexer = class {
|
|
|
1013
840
|
confirmations;
|
|
1014
841
|
batchSize;
|
|
1015
842
|
pollIntervalMs;
|
|
1016
|
-
|
|
1017
|
-
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1018
|
-
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1019
|
-
* ledger's query path.
|
|
1020
|
-
*
|
|
1021
|
-
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1022
|
-
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1023
|
-
* incrementing IDs so callers with the memory ledger must provide a
|
|
1024
|
-
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1025
|
-
* their `pending_credits` table.
|
|
1026
|
-
*/
|
|
1027
|
-
matchLockId = async () => void 0;
|
|
843
|
+
matchLockId;
|
|
1028
844
|
running = false;
|
|
1029
845
|
timer;
|
|
1030
846
|
constructor(config) {
|
|
@@ -1042,6 +858,12 @@ var BurnIndexer = class {
|
|
|
1042
858
|
);
|
|
1043
859
|
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1044
860
|
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
861
|
+
if (!config.matchLockId) {
|
|
862
|
+
throw new Error(
|
|
863
|
+
"BurnIndexer: matchLockId is required. Provide a function that maps a burn event to its pending credit lockId. Without it, no on-chain burns will ever grant off-chain credits."
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
this.matchLockId = config.matchLockId;
|
|
1045
867
|
}
|
|
1046
868
|
start() {
|
|
1047
869
|
if (this.running) return;
|
|
@@ -1071,7 +893,8 @@ var BurnIndexer = class {
|
|
|
1071
893
|
return;
|
|
1072
894
|
}
|
|
1073
895
|
await this.processBlockRange(from, safeHead);
|
|
1074
|
-
} catch {
|
|
896
|
+
} catch (err) {
|
|
897
|
+
console.error("[PAFI] BurnIndexer tick error:", err);
|
|
1075
898
|
}
|
|
1076
899
|
this.scheduleNext();
|
|
1077
900
|
}
|
|
@@ -1116,10 +939,10 @@ var BurnIndexer = class {
|
|
|
1116
939
|
for (const log of logs) {
|
|
1117
940
|
const args = log.args;
|
|
1118
941
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1119
|
-
if (
|
|
942
|
+
if (getAddress4(args.to) !== ZERO_ADDRESS2) continue;
|
|
1120
943
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1121
944
|
out.push({
|
|
1122
|
-
from:
|
|
945
|
+
from: getAddress4(args.from),
|
|
1123
946
|
amount: args.value,
|
|
1124
947
|
blockNumber: log.blockNumber,
|
|
1125
948
|
txHash: log.transactionHash,
|
|
@@ -1134,20 +957,27 @@ var BurnIndexer = class {
|
|
|
1134
957
|
* log + skip.
|
|
1135
958
|
*/
|
|
1136
959
|
async finalize(evt) {
|
|
960
|
+
const txHash = evt.txHash;
|
|
1137
961
|
const lockId = await this.matchLockId(evt);
|
|
1138
|
-
if (
|
|
962
|
+
if (lockId === void 0) {
|
|
963
|
+
console.warn(
|
|
964
|
+
"[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
|
|
965
|
+
);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
1139
968
|
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1140
969
|
return;
|
|
1141
970
|
}
|
|
1142
971
|
try {
|
|
1143
972
|
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1144
|
-
} catch {
|
|
973
|
+
} catch (err) {
|
|
974
|
+
console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
|
|
1145
975
|
}
|
|
1146
976
|
}
|
|
1147
977
|
};
|
|
1148
978
|
|
|
1149
979
|
// src/api/handlers.ts
|
|
1150
|
-
import { getAddress as
|
|
980
|
+
import { getAddress as getAddress5 } from "viem";
|
|
1151
981
|
import {
|
|
1152
982
|
getMintRequestNonce,
|
|
1153
983
|
getPointTokenBalance,
|
|
@@ -1165,15 +995,12 @@ var IssuerApiHandlers = class {
|
|
|
1165
995
|
* validate the request's `pointTokenAddress` against this set.
|
|
1166
996
|
*/
|
|
1167
997
|
supportedTokens;
|
|
1168
|
-
/** First supported token — used as default when a handler doesn't
|
|
1169
|
-
* receive a `pointTokenAddress` in the request (shouldn't happen in
|
|
1170
|
-
* practice, but keeps type-narrowing happy). */
|
|
1171
|
-
defaultToken;
|
|
1172
998
|
chainId;
|
|
1173
999
|
contracts;
|
|
1174
1000
|
pafiWebUrl;
|
|
1175
1001
|
feeManager;
|
|
1176
1002
|
poolsProvider;
|
|
1003
|
+
claim;
|
|
1177
1004
|
constructor(config) {
|
|
1178
1005
|
this.authService = config.authService;
|
|
1179
1006
|
this.ledger = config.ledger;
|
|
@@ -1184,14 +1011,14 @@ var IssuerApiHandlers = class {
|
|
|
1184
1011
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1185
1012
|
);
|
|
1186
1013
|
}
|
|
1187
|
-
const normalized = raw.map((a) =>
|
|
1014
|
+
const normalized = raw.map((a) => getAddress5(a));
|
|
1188
1015
|
this.supportedTokens = new Set(normalized);
|
|
1189
|
-
this.defaultToken = normalized[0];
|
|
1190
1016
|
this.chainId = config.chainId;
|
|
1191
1017
|
this.contracts = config.contracts;
|
|
1192
1018
|
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1193
1019
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1194
1020
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1021
|
+
if (config.claim) this.claim = config.claim;
|
|
1195
1022
|
}
|
|
1196
1023
|
// =========================================================================
|
|
1197
1024
|
// Public handlers (no auth required)
|
|
@@ -1206,6 +1033,12 @@ var IssuerApiHandlers = class {
|
|
|
1206
1033
|
if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
|
|
1207
1034
|
throw new Error("handleLogin: message and signature are required");
|
|
1208
1035
|
}
|
|
1036
|
+
if (body.message.length > 4096) {
|
|
1037
|
+
throw new Error("message too long");
|
|
1038
|
+
}
|
|
1039
|
+
if (body.signature.length > 260) {
|
|
1040
|
+
throw new Error("signature too long");
|
|
1041
|
+
}
|
|
1209
1042
|
const result = await this.authService.login(body.message, body.signature);
|
|
1210
1043
|
return {
|
|
1211
1044
|
token: result.token,
|
|
@@ -1220,9 +1053,12 @@ var IssuerApiHandlers = class {
|
|
|
1220
1053
|
* needs to build EIP-712 messages and interact with on-chain.
|
|
1221
1054
|
*/
|
|
1222
1055
|
async handleConfig(chainId) {
|
|
1056
|
+
if (!Number.isInteger(chainId) || chainId <= 0) {
|
|
1057
|
+
throw new Error("invalid chainId");
|
|
1058
|
+
}
|
|
1223
1059
|
if (chainId !== this.chainId) {
|
|
1224
1060
|
throw new Error(
|
|
1225
|
-
`handleConfig: unsupported chainId ${chainId}
|
|
1061
|
+
`handleConfig: unsupported chainId ${chainId}`
|
|
1226
1062
|
);
|
|
1227
1063
|
}
|
|
1228
1064
|
const contracts = {
|
|
@@ -1285,14 +1121,14 @@ var IssuerApiHandlers = class {
|
|
|
1285
1121
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1286
1122
|
);
|
|
1287
1123
|
}
|
|
1288
|
-
const normalizedAuthed =
|
|
1289
|
-
const normalizedRequest =
|
|
1124
|
+
const normalizedAuthed = getAddress5(userAddress);
|
|
1125
|
+
const normalizedRequest = getAddress5(request.userAddress);
|
|
1290
1126
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1291
1127
|
throw new Error(
|
|
1292
1128
|
"handleUser: request userAddress must match authenticated user"
|
|
1293
1129
|
);
|
|
1294
1130
|
}
|
|
1295
|
-
const pointToken =
|
|
1131
|
+
const pointToken = getAddress5(request.pointTokenAddress);
|
|
1296
1132
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1297
1133
|
throw new Error(
|
|
1298
1134
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
@@ -1335,19 +1171,32 @@ var IssuerApiHandlers = class {
|
|
|
1335
1171
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1336
1172
|
);
|
|
1337
1173
|
}
|
|
1338
|
-
const pointToken =
|
|
1174
|
+
const pointToken = getAddress5(request.pointTokenAddress);
|
|
1339
1175
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1340
1176
|
throw new Error(
|
|
1341
1177
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
1342
1178
|
);
|
|
1343
1179
|
}
|
|
1180
|
+
const consent = request.receiverConsent;
|
|
1181
|
+
if (getAddress5(consent.originalReceiver) !== getAddress5(userAddress)) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
"handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
if (consent.amount <= 0n) {
|
|
1187
|
+
throw new Error("handleBuildConsentTypedData: amount must be positive");
|
|
1188
|
+
}
|
|
1189
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1190
|
+
if (consent.deadline <= nowSecs) {
|
|
1191
|
+
throw new Error("handleBuildConsentTypedData: deadline is in the past");
|
|
1192
|
+
}
|
|
1344
1193
|
const name = await getTokenName(this.provider, pointToken);
|
|
1345
1194
|
const domain = {
|
|
1346
1195
|
name,
|
|
1347
1196
|
verifyingContract: pointToken,
|
|
1348
1197
|
chainId: this.chainId
|
|
1349
1198
|
};
|
|
1350
|
-
const typedData = buildReceiverConsentTypedData(domain,
|
|
1199
|
+
const typedData = buildReceiverConsentTypedData(domain, consent);
|
|
1351
1200
|
return {
|
|
1352
1201
|
typedData: {
|
|
1353
1202
|
domain: typedData.domain,
|
|
@@ -1357,11 +1206,96 @@ var IssuerApiHandlers = class {
|
|
|
1357
1206
|
}
|
|
1358
1207
|
};
|
|
1359
1208
|
}
|
|
1209
|
+
/**
|
|
1210
|
+
* `POST /claim`
|
|
1211
|
+
*
|
|
1212
|
+
* Policy gate + ledger lock + MintRequest signing in a single atomic
|
|
1213
|
+
* step. Returns an unsigned UserOp the frontend attaches paymaster data
|
|
1214
|
+
* to and submits via EIP-7702 + Bundler.
|
|
1215
|
+
*
|
|
1216
|
+
* Order of operations:
|
|
1217
|
+
* 1. Validate request fields.
|
|
1218
|
+
* 2. policy.evaluate() — throws if denied; cannot be bypassed.
|
|
1219
|
+
* 3. ledger.lockForMinting() — reserves the balance.
|
|
1220
|
+
* 4. Read on-chain mintRequestNonce + token name in parallel.
|
|
1221
|
+
* 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
|
|
1222
|
+
* 6. On any error after step 3, release the lock before re-throwing.
|
|
1223
|
+
*/
|
|
1224
|
+
async handleClaim(userAddress, request) {
|
|
1225
|
+
if (!this.claim) {
|
|
1226
|
+
throw new Error("handleClaim: claim is not configured on this issuer");
|
|
1227
|
+
}
|
|
1228
|
+
if (request.chainId !== this.chainId) {
|
|
1229
|
+
throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
|
|
1230
|
+
}
|
|
1231
|
+
const pointToken = getAddress5(request.pointTokenAddress);
|
|
1232
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1233
|
+
throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
|
|
1234
|
+
}
|
|
1235
|
+
if (request.amount <= 0n) {
|
|
1236
|
+
throw new Error("handleClaim: amount must be positive");
|
|
1237
|
+
}
|
|
1238
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1239
|
+
if (request.deadline <= nowSecs) {
|
|
1240
|
+
throw new Error("handleClaim: deadline is in the past");
|
|
1241
|
+
}
|
|
1242
|
+
const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
|
|
1243
|
+
const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
|
|
1244
|
+
const normalizedUser = getAddress5(userAddress);
|
|
1245
|
+
const decision = await policy.evaluate({
|
|
1246
|
+
userAddress: normalizedUser,
|
|
1247
|
+
amount: request.amount,
|
|
1248
|
+
pointTokenAddress: pointToken,
|
|
1249
|
+
chainId: this.chainId
|
|
1250
|
+
});
|
|
1251
|
+
if (!decision.approved) {
|
|
1252
|
+
throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
|
|
1253
|
+
}
|
|
1254
|
+
const lockId = await this.ledger.lockForMinting(
|
|
1255
|
+
normalizedUser,
|
|
1256
|
+
request.amount,
|
|
1257
|
+
lockDurationMs,
|
|
1258
|
+
pointToken
|
|
1259
|
+
);
|
|
1260
|
+
try {
|
|
1261
|
+
const [mintRequestNonce, tokenName] = await Promise.all([
|
|
1262
|
+
getMintRequestNonce(this.provider, pointToken, normalizedUser),
|
|
1263
|
+
getTokenName(this.provider, pointToken)
|
|
1264
|
+
]);
|
|
1265
|
+
const domain = {
|
|
1266
|
+
name: tokenName,
|
|
1267
|
+
verifyingContract: pointToken,
|
|
1268
|
+
chainId: this.chainId
|
|
1269
|
+
};
|
|
1270
|
+
const userOp = await relayService.prepareMint({
|
|
1271
|
+
userAddress: normalizedUser,
|
|
1272
|
+
aaNonce: request.aaNonce,
|
|
1273
|
+
batchExecutorAddress,
|
|
1274
|
+
pointTokenAddress: pointToken,
|
|
1275
|
+
amount: request.amount,
|
|
1276
|
+
issuerSignerWallet,
|
|
1277
|
+
domain,
|
|
1278
|
+
mintRequestNonce,
|
|
1279
|
+
deadline: request.deadline,
|
|
1280
|
+
feeAmount: request.feeAmount,
|
|
1281
|
+
feeRecipient: request.feeRecipient
|
|
1282
|
+
});
|
|
1283
|
+
return {
|
|
1284
|
+
lockId,
|
|
1285
|
+
userOp,
|
|
1286
|
+
expiresInSeconds: Math.floor(lockDurationMs / 1e3)
|
|
1287
|
+
};
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
await this.ledger.releaseLock(lockId).catch(() => {
|
|
1290
|
+
});
|
|
1291
|
+
throw err;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1360
1294
|
};
|
|
1361
1295
|
|
|
1362
1296
|
// src/api/handlers/ptRedeemHandler.ts
|
|
1363
|
-
import { getAddress as
|
|
1364
|
-
import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2 } from "@pafi-dev/core";
|
|
1297
|
+
import { getAddress as getAddress6 } from "viem";
|
|
1298
|
+
import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2, getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
|
|
1365
1299
|
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1366
1300
|
var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
|
|
1367
1301
|
var PTRedeemError = class extends Error {
|
|
@@ -1400,16 +1334,25 @@ var PTRedeemHandler = class {
|
|
|
1400
1334
|
this.ledger = config.ledger;
|
|
1401
1335
|
this.relayService = config.relayService;
|
|
1402
1336
|
this.provider = config.provider;
|
|
1403
|
-
this.pointTokenAddress =
|
|
1404
|
-
this.batchExecutorAddress =
|
|
1337
|
+
this.pointTokenAddress = getAddress6(config.pointTokenAddress);
|
|
1338
|
+
this.batchExecutorAddress = getAddress6(config.batchExecutorAddress);
|
|
1405
1339
|
this.chainId = config.chainId;
|
|
1406
1340
|
this.domain = config.domain;
|
|
1407
1341
|
this.burnerSignerWallet = config.burnerSignerWallet;
|
|
1342
|
+
if (this.burnerSignerWallet?.account?.type === "local") {
|
|
1343
|
+
console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
|
|
1344
|
+
}
|
|
1408
1345
|
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1409
1346
|
this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
|
|
1410
1347
|
this.now = config.now ?? (() => Date.now());
|
|
1411
1348
|
}
|
|
1412
1349
|
async handle(request) {
|
|
1350
|
+
if (getAddress6(request.authenticatedAddress) !== getAddress6(request.userAddress)) {
|
|
1351
|
+
throw new PTRedeemError(
|
|
1352
|
+
"UNAUTHORIZED",
|
|
1353
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1413
1356
|
if (request.amount <= 0n) {
|
|
1414
1357
|
throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
|
|
1415
1358
|
}
|
|
@@ -1427,6 +1370,17 @@ var PTRedeemHandler = class {
|
|
|
1427
1370
|
`failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
|
|
1428
1371
|
);
|
|
1429
1372
|
}
|
|
1373
|
+
const onChainBalance = await getPointTokenBalance2(
|
|
1374
|
+
this.provider,
|
|
1375
|
+
this.pointTokenAddress,
|
|
1376
|
+
request.userAddress
|
|
1377
|
+
);
|
|
1378
|
+
if (onChainBalance < request.amount) {
|
|
1379
|
+
throw new PTRedeemError(
|
|
1380
|
+
"INVALID_AMOUNT",
|
|
1381
|
+
`insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1430
1384
|
const deadline = BigInt(
|
|
1431
1385
|
Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
|
|
1432
1386
|
);
|
|
@@ -1480,8 +1434,8 @@ var PTRedeemHandler = class {
|
|
|
1480
1434
|
};
|
|
1481
1435
|
|
|
1482
1436
|
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1483
|
-
import { getAddress as
|
|
1484
|
-
import { getPointTokenBalance as
|
|
1437
|
+
import { getAddress as getAddress7 } from "viem";
|
|
1438
|
+
import { getPointTokenBalance as getPointTokenBalance3 } from "@pafi-dev/core";
|
|
1485
1439
|
var TopUpRedemptionError = class extends Error {
|
|
1486
1440
|
constructor(code, message) {
|
|
1487
1441
|
super(message);
|
|
@@ -1499,9 +1453,15 @@ var TopUpRedemptionHandler = class {
|
|
|
1499
1453
|
this.ledger = config.ledger;
|
|
1500
1454
|
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1501
1455
|
this.provider = config.provider;
|
|
1502
|
-
this.pointTokenAddress =
|
|
1456
|
+
this.pointTokenAddress = getAddress7(config.pointTokenAddress);
|
|
1503
1457
|
}
|
|
1504
1458
|
async handle(request) {
|
|
1459
|
+
if (getAddress7(request.authenticatedAddress) !== getAddress7(request.userAddress)) {
|
|
1460
|
+
throw new TopUpRedemptionError(
|
|
1461
|
+
"UNAUTHORIZED",
|
|
1462
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1505
1465
|
const offChainBalance = await this.ledger.getBalance(
|
|
1506
1466
|
request.userAddress,
|
|
1507
1467
|
this.pointTokenAddress
|
|
@@ -1510,7 +1470,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1510
1470
|
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1511
1471
|
}
|
|
1512
1472
|
const shortfall = request.requiredAmount - offChainBalance;
|
|
1513
|
-
const onChainBalance = await
|
|
1473
|
+
const onChainBalance = await getPointTokenBalance3(
|
|
1514
1474
|
this.provider,
|
|
1515
1475
|
this.pointTokenAddress,
|
|
1516
1476
|
request.userAddress
|
|
@@ -1524,6 +1484,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1524
1484
|
};
|
|
1525
1485
|
}
|
|
1526
1486
|
const redeem = await this.ptRedeemHandler.handle({
|
|
1487
|
+
authenticatedAddress: request.authenticatedAddress,
|
|
1527
1488
|
userAddress: request.userAddress,
|
|
1528
1489
|
amount: shortfall,
|
|
1529
1490
|
aaNonce: request.aaNonce
|
|
@@ -1537,6 +1498,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1537
1498
|
};
|
|
1538
1499
|
|
|
1539
1500
|
// src/pools/subgraphPoolsProvider.ts
|
|
1501
|
+
import { isAddress } from "viem";
|
|
1540
1502
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1541
1503
|
var POOL_QUERY = `
|
|
1542
1504
|
query GetPoolForPointToken($id: ID!) {
|
|
@@ -1559,6 +1521,19 @@ function createSubgraphPoolsProvider(config) {
|
|
|
1559
1521
|
"createSubgraphPoolsProvider: subgraphUrl is required"
|
|
1560
1522
|
);
|
|
1561
1523
|
}
|
|
1524
|
+
try {
|
|
1525
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1526
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1527
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1528
|
+
}
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
if (err instanceof TypeError) {
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
throw err;
|
|
1536
|
+
}
|
|
1562
1537
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
1563
1538
|
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1564
1539
|
const now = config.now ?? (() => Date.now());
|
|
@@ -1627,6 +1602,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
|
|
|
1627
1602
|
return [];
|
|
1628
1603
|
}
|
|
1629
1604
|
const { pool } = token;
|
|
1605
|
+
if (!isAddress(pool.hooks)) {
|
|
1606
|
+
console.error(
|
|
1607
|
+
"[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
|
|
1608
|
+
pool.hooks,
|
|
1609
|
+
"\u2014 skipping pool"
|
|
1610
|
+
);
|
|
1611
|
+
return [];
|
|
1612
|
+
}
|
|
1613
|
+
if (!isAddress(pool.token0.id) || !isAddress(pool.token1.id)) {
|
|
1614
|
+
console.error(
|
|
1615
|
+
"[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
|
|
1616
|
+
);
|
|
1617
|
+
return [];
|
|
1618
|
+
}
|
|
1619
|
+
if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
|
|
1620
|
+
console.error(
|
|
1621
|
+
"[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
|
|
1622
|
+
);
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
1630
1625
|
const [currency0, currency1] = sortCurrencies(
|
|
1631
1626
|
pool.token0.id,
|
|
1632
1627
|
pool.token1.id
|
|
@@ -1662,6 +1657,19 @@ function createSubgraphNativeUsdtQuoter(config) {
|
|
|
1662
1657
|
"createSubgraphNativeUsdtQuoter: subgraphUrl is required"
|
|
1663
1658
|
);
|
|
1664
1659
|
}
|
|
1660
|
+
try {
|
|
1661
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1662
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1663
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1664
|
+
}
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
if (err instanceof TypeError) {
|
|
1667
|
+
throw new Error(
|
|
1668
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
throw err;
|
|
1672
|
+
}
|
|
1665
1673
|
const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
|
|
1666
1674
|
const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
|
|
1667
1675
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
|
|
@@ -1738,6 +1746,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
|
|
|
1738
1746
|
);
|
|
1739
1747
|
return null;
|
|
1740
1748
|
}
|
|
1749
|
+
const MIN_REASONABLE_ETH_PRICE = 100;
|
|
1750
|
+
const MAX_REASONABLE_ETH_PRICE = 1e5;
|
|
1751
|
+
if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
|
|
1752
|
+
console.warn(
|
|
1753
|
+
`[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
|
|
1754
|
+
);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1741
1757
|
return parsed;
|
|
1742
1758
|
}
|
|
1743
1759
|
function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
@@ -1748,7 +1764,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1748
1764
|
}
|
|
1749
1765
|
|
|
1750
1766
|
// src/balance/balanceAggregator.ts
|
|
1751
|
-
import { getPointTokenBalance as
|
|
1767
|
+
import { getPointTokenBalance as getPointTokenBalance4 } from "@pafi-dev/core";
|
|
1752
1768
|
var BalanceAggregator = class {
|
|
1753
1769
|
provider;
|
|
1754
1770
|
ledger;
|
|
@@ -1769,7 +1785,7 @@ var BalanceAggregator = class {
|
|
|
1769
1785
|
async getCombinedBalance(user, pointToken) {
|
|
1770
1786
|
const [offChain, onChain] = await Promise.all([
|
|
1771
1787
|
this.ledger.getBalance(user, pointToken),
|
|
1772
|
-
|
|
1788
|
+
getPointTokenBalance4(this.provider, pointToken, user)
|
|
1773
1789
|
]);
|
|
1774
1790
|
return {
|
|
1775
1791
|
offChain,
|
|
@@ -1829,11 +1845,14 @@ var PafiBackendError = class extends Error {
|
|
|
1829
1845
|
};
|
|
1830
1846
|
|
|
1831
1847
|
// src/config.ts
|
|
1832
|
-
import { getAddress as
|
|
1848
|
+
import { getAddress as getAddress8 } from "viem";
|
|
1833
1849
|
function createIssuerService(config) {
|
|
1834
1850
|
if (!config.provider) {
|
|
1835
1851
|
throw new Error("createIssuerService: provider is required");
|
|
1836
1852
|
}
|
|
1853
|
+
if (!config.ledger) {
|
|
1854
|
+
throw new Error("createIssuerService: ledger is required");
|
|
1855
|
+
}
|
|
1837
1856
|
if (!config.auth?.jwtSecret) {
|
|
1838
1857
|
throw new Error("createIssuerService: auth.jwtSecret is required");
|
|
1839
1858
|
}
|
|
@@ -1846,8 +1865,8 @@ function createIssuerService(config) {
|
|
|
1846
1865
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
1847
1866
|
);
|
|
1848
1867
|
}
|
|
1849
|
-
const tokenAddresses = rawAddresses.map((a) =>
|
|
1850
|
-
const ledger = config.ledger
|
|
1868
|
+
const tokenAddresses = rawAddresses.map((a) => getAddress8(a));
|
|
1869
|
+
const ledger = config.ledger;
|
|
1851
1870
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1852
1871
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
1853
1872
|
const authServiceConfig = {
|
|
@@ -1903,6 +1922,15 @@ function createIssuerService(config) {
|
|
|
1903
1922
|
};
|
|
1904
1923
|
if (feeManager) handlersConfig.feeManager = feeManager;
|
|
1905
1924
|
if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
|
|
1925
|
+
if (config.claim) {
|
|
1926
|
+
handlersConfig.claim = {
|
|
1927
|
+
policy,
|
|
1928
|
+
relayService,
|
|
1929
|
+
issuerSignerWallet: config.claim.issuerSignerWallet,
|
|
1930
|
+
batchExecutorAddress: config.claim.batchExecutorAddress,
|
|
1931
|
+
lockDurationMs: config.claim.lockDurationMs
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1906
1934
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
1907
1935
|
if (config.indexer?.autoStart) {
|
|
1908
1936
|
for (const idx of indexers.values()) {
|
|
@@ -1933,7 +1961,6 @@ export {
|
|
|
1933
1961
|
FeeManager,
|
|
1934
1962
|
InMemoryCursorStore,
|
|
1935
1963
|
IssuerApiHandlers,
|
|
1936
|
-
MemoryPointLedger,
|
|
1937
1964
|
MemorySessionStore,
|
|
1938
1965
|
NonceManager,
|
|
1939
1966
|
PAFI_ISSUER_SDK_VERSION,
|
|
@@ -1941,7 +1968,6 @@ export {
|
|
|
1941
1968
|
PTRedeemHandler,
|
|
1942
1969
|
PafiBackendError,
|
|
1943
1970
|
PointIndexer,
|
|
1944
|
-
PrivateKeySigner,
|
|
1945
1971
|
RelayError,
|
|
1946
1972
|
RelayService,
|
|
1947
1973
|
TopUpRedemptionError,
|