@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.cjs
CHANGED
|
@@ -28,7 +28,6 @@ __export(index_exports, {
|
|
|
28
28
|
FeeManager: () => FeeManager,
|
|
29
29
|
InMemoryCursorStore: () => InMemoryCursorStore,
|
|
30
30
|
IssuerApiHandlers: () => IssuerApiHandlers,
|
|
31
|
-
MemoryPointLedger: () => MemoryPointLedger,
|
|
32
31
|
MemorySessionStore: () => MemorySessionStore,
|
|
33
32
|
NonceManager: () => NonceManager,
|
|
34
33
|
PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
|
|
@@ -36,7 +35,6 @@ __export(index_exports, {
|
|
|
36
35
|
PTRedeemHandler: () => PTRedeemHandler,
|
|
37
36
|
PafiBackendError: () => PafiBackendError,
|
|
38
37
|
PointIndexer: () => PointIndexer,
|
|
39
|
-
PrivateKeySigner: () => PrivateKeySigner,
|
|
40
38
|
RelayError: () => RelayError,
|
|
41
39
|
RelayService: () => RelayService,
|
|
42
40
|
TopUpRedemptionError: () => TopUpRedemptionError,
|
|
@@ -48,204 +46,6 @@ __export(index_exports, {
|
|
|
48
46
|
});
|
|
49
47
|
module.exports = __toCommonJS(index_exports);
|
|
50
48
|
|
|
51
|
-
// src/ledger/memoryLedger.ts
|
|
52
|
-
var import_viem = require("viem");
|
|
53
|
-
var MemoryPointLedger = class {
|
|
54
|
-
balances = /* @__PURE__ */ new Map();
|
|
55
|
-
locks = /* @__PURE__ */ new Map();
|
|
56
|
-
nextLockId = 1;
|
|
57
|
-
now;
|
|
58
|
-
constructor(opts = {}) {
|
|
59
|
-
this.now = opts.now ?? (() => Date.now());
|
|
60
|
-
}
|
|
61
|
-
// -------------------------------------------------------------------------
|
|
62
|
-
// Read
|
|
63
|
-
// -------------------------------------------------------------------------
|
|
64
|
-
async getBalance(userAddress, tokenAddress) {
|
|
65
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
66
|
-
const token = normalizeToken(tokenAddress);
|
|
67
|
-
this.purgeExpired();
|
|
68
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
69
|
-
const locked = this.lockedTotalFor(user, token);
|
|
70
|
-
return total - locked;
|
|
71
|
-
}
|
|
72
|
-
async getLockedRequests(userAddress, tokenAddress) {
|
|
73
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
74
|
-
const token = normalizeToken(tokenAddress);
|
|
75
|
-
this.purgeExpired();
|
|
76
|
-
const out = [];
|
|
77
|
-
for (const lock of this.locks.values()) {
|
|
78
|
-
if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
79
|
-
out.push({ ...lock });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
// -------------------------------------------------------------------------
|
|
85
|
-
// Write
|
|
86
|
-
// -------------------------------------------------------------------------
|
|
87
|
-
async creditBalance(userAddress, amount, _reason, tokenAddress) {
|
|
88
|
-
if (amount <= 0n) {
|
|
89
|
-
throw new Error("MemoryPointLedger: credit amount must be positive");
|
|
90
|
-
}
|
|
91
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
92
|
-
const token = normalizeToken(tokenAddress);
|
|
93
|
-
const key = balanceKey(user, token);
|
|
94
|
-
const current = this.balances.get(key) ?? 0n;
|
|
95
|
-
this.balances.set(key, current + amount);
|
|
96
|
-
}
|
|
97
|
-
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
98
|
-
if (amount <= 0n) {
|
|
99
|
-
throw new Error("MemoryPointLedger: lock amount must be positive");
|
|
100
|
-
}
|
|
101
|
-
if (lockDurationMs <= 0) {
|
|
102
|
-
throw new Error("MemoryPointLedger: lockDurationMs must be positive");
|
|
103
|
-
}
|
|
104
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
105
|
-
const token = normalizeToken(tokenAddress);
|
|
106
|
-
this.purgeExpired();
|
|
107
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
108
|
-
const alreadyLocked = this.lockedTotalFor(user, token);
|
|
109
|
-
const available = total - alreadyLocked;
|
|
110
|
-
if (available < amount) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
const lockId = `lock-${this.nextLockId++}`;
|
|
116
|
-
const now = this.now();
|
|
117
|
-
const lock = {
|
|
118
|
-
lockId,
|
|
119
|
-
userAddress: user,
|
|
120
|
-
amount,
|
|
121
|
-
status: "PENDING",
|
|
122
|
-
createdAt: now,
|
|
123
|
-
expiresAt: now + lockDurationMs
|
|
124
|
-
};
|
|
125
|
-
if (tokenAddress !== void 0) {
|
|
126
|
-
lock.tokenAddress = (0, import_viem.getAddress)(tokenAddress);
|
|
127
|
-
}
|
|
128
|
-
this.locks.set(lockId, lock);
|
|
129
|
-
return lockId;
|
|
130
|
-
}
|
|
131
|
-
async releaseLock(lockId) {
|
|
132
|
-
const lock = this.locks.get(lockId);
|
|
133
|
-
if (!lock) return;
|
|
134
|
-
if (lock.status === "PENDING") {
|
|
135
|
-
this.locks.delete(lockId);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
139
|
-
if (amount <= 0n) {
|
|
140
|
-
throw new Error("MemoryPointLedger: deduct amount must be positive");
|
|
141
|
-
}
|
|
142
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
143
|
-
const token = normalizeToken(tokenAddress);
|
|
144
|
-
const key = balanceKey(user, token);
|
|
145
|
-
const current = this.balances.get(key) ?? 0n;
|
|
146
|
-
if (current < amount) {
|
|
147
|
-
throw new Error(
|
|
148
|
-
`MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
this.balances.set(key, current - amount);
|
|
152
|
-
for (const lock of this.locks.values()) {
|
|
153
|
-
if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
154
|
-
lock.status = "MINTED";
|
|
155
|
-
lock.txHash = txHash;
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
async updateMintStatus(lockId, status, txHash) {
|
|
161
|
-
const lock = this.locks.get(lockId);
|
|
162
|
-
if (!lock) {
|
|
163
|
-
throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
|
|
164
|
-
}
|
|
165
|
-
lock.status = status;
|
|
166
|
-
if (txHash) lock.txHash = txHash;
|
|
167
|
-
}
|
|
168
|
-
// -------------------------------------------------------------------------
|
|
169
|
-
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
170
|
-
// -------------------------------------------------------------------------
|
|
171
|
-
pendingCredits = /* @__PURE__ */ new Map();
|
|
172
|
-
nextCreditId = 1;
|
|
173
|
-
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
174
|
-
if (amount <= 0n) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
"MemoryPointLedger: pending credit amount must be positive"
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
if (durationMs <= 0) {
|
|
180
|
-
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
181
|
-
}
|
|
182
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
183
|
-
const lockId = `credit-${this.nextCreditId++}`;
|
|
184
|
-
const now = this.now();
|
|
185
|
-
this.pendingCredits.set(lockId, {
|
|
186
|
-
lockId,
|
|
187
|
-
userAddress: user,
|
|
188
|
-
amount,
|
|
189
|
-
tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
|
|
190
|
-
createdAt: now,
|
|
191
|
-
expiresAt: now + durationMs,
|
|
192
|
-
status: "PENDING"
|
|
193
|
-
});
|
|
194
|
-
return lockId;
|
|
195
|
-
}
|
|
196
|
-
async resolveCreditByBurnTx(lockId, txHash) {
|
|
197
|
-
const credit = this.pendingCredits.get(lockId);
|
|
198
|
-
if (!credit) {
|
|
199
|
-
throw new Error(
|
|
200
|
-
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
if (credit.status === "RESOLVED") {
|
|
204
|
-
if (credit.txHash === txHash) return;
|
|
205
|
-
throw new Error(
|
|
206
|
-
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
const token = normalizeToken(credit.tokenAddress);
|
|
210
|
-
const key = balanceKey(credit.userAddress, token);
|
|
211
|
-
const current = this.balances.get(key) ?? 0n;
|
|
212
|
-
this.balances.set(key, current + credit.amount);
|
|
213
|
-
credit.status = "RESOLVED";
|
|
214
|
-
credit.txHash = txHash;
|
|
215
|
-
}
|
|
216
|
-
// -------------------------------------------------------------------------
|
|
217
|
-
// Internal helpers
|
|
218
|
-
// -------------------------------------------------------------------------
|
|
219
|
-
/**
|
|
220
|
-
* Auto-expire any PENDING lock past its expiry. Called lazily on every
|
|
221
|
-
* read/write so the in-memory state stays self-cleaning without a timer.
|
|
222
|
-
*/
|
|
223
|
-
purgeExpired() {
|
|
224
|
-
const now = this.now();
|
|
225
|
-
for (const lock of this.locks.values()) {
|
|
226
|
-
if (lock.status === "PENDING" && lock.expiresAt <= now) {
|
|
227
|
-
lock.status = "EXPIRED";
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
lockedTotalFor(userAddress, tokenKey) {
|
|
232
|
-
let total = 0n;
|
|
233
|
-
for (const lock of this.locks.values()) {
|
|
234
|
-
if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
|
|
235
|
-
total += lock.amount;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return total;
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
var DEFAULT_TOKEN_KEY = "default";
|
|
242
|
-
function normalizeToken(tokenAddress) {
|
|
243
|
-
return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : (0, import_viem.getAddress)(tokenAddress);
|
|
244
|
-
}
|
|
245
|
-
function balanceKey(user, tokenKey) {
|
|
246
|
-
return `${user}|${tokenKey}`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
49
|
// src/policy/defaultPolicy.ts
|
|
250
50
|
var DefaultPolicyEngine = class {
|
|
251
51
|
ledger;
|
|
@@ -261,6 +61,13 @@ var DefaultPolicyEngine = class {
|
|
|
261
61
|
}
|
|
262
62
|
if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
|
|
263
63
|
if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
|
|
64
|
+
if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
|
|
65
|
+
if (process.env.NODE_ENV === "production") {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
264
71
|
}
|
|
265
72
|
async evaluate(request) {
|
|
266
73
|
if (request.amount <= 0n) {
|
|
@@ -297,32 +104,9 @@ var DefaultPolicyEngine = class {
|
|
|
297
104
|
}
|
|
298
105
|
};
|
|
299
106
|
|
|
300
|
-
// src/signer/privateKeySigner.ts
|
|
301
|
-
var import_viem2 = require("viem");
|
|
302
|
-
var import_accounts = require("viem/accounts");
|
|
303
|
-
var import_core = require("@pafi-dev/core");
|
|
304
|
-
var PrivateKeySigner = class {
|
|
305
|
-
account;
|
|
306
|
-
walletClient;
|
|
307
|
-
constructor(opts) {
|
|
308
|
-
this.account = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
|
|
309
|
-
this.walletClient = (0, import_viem2.createWalletClient)({
|
|
310
|
-
account: this.account,
|
|
311
|
-
chain: opts.chain,
|
|
312
|
-
transport: (0, import_viem2.http)(opts.rpcUrl)
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
async signMintRequest(domain, message) {
|
|
316
|
-
return (0, import_core.signMintRequest)(this.walletClient, domain, message);
|
|
317
|
-
}
|
|
318
|
-
async getAddress() {
|
|
319
|
-
return this.account.address;
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
107
|
// src/auth/memorySessionStore.ts
|
|
324
108
|
var import_node_crypto = require("crypto");
|
|
325
|
-
var
|
|
109
|
+
var import_viem = require("viem");
|
|
326
110
|
var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
327
111
|
var MemorySessionStore = class {
|
|
328
112
|
nonces = /* @__PURE__ */ new Map();
|
|
@@ -332,6 +116,11 @@ var MemorySessionStore = class {
|
|
|
332
116
|
nonceTtlMs;
|
|
333
117
|
now;
|
|
334
118
|
constructor(opts = {}) {
|
|
119
|
+
if (process.env.NODE_ENV === "production") {
|
|
120
|
+
console.error(
|
|
121
|
+
"[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."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
335
124
|
this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
|
|
336
125
|
this.now = opts.now ?? (() => Date.now());
|
|
337
126
|
}
|
|
@@ -358,7 +147,7 @@ var MemorySessionStore = class {
|
|
|
358
147
|
this.purgeExpiredSessions();
|
|
359
148
|
const normalized = {
|
|
360
149
|
...session,
|
|
361
|
-
userAddress: (0,
|
|
150
|
+
userAddress: (0, import_viem.getAddress)(session.userAddress)
|
|
362
151
|
};
|
|
363
152
|
this.sessions.set(session.tokenId, normalized);
|
|
364
153
|
}
|
|
@@ -376,7 +165,7 @@ var MemorySessionStore = class {
|
|
|
376
165
|
this.sessions.delete(tokenId);
|
|
377
166
|
}
|
|
378
167
|
async revokeAllSessions(userAddress) {
|
|
379
|
-
const key = (0,
|
|
168
|
+
const key = (0, import_viem.getAddress)(userAddress);
|
|
380
169
|
for (const [tokenId, session] of this.sessions.entries()) {
|
|
381
170
|
if (session.userAddress === key) {
|
|
382
171
|
this.sessions.delete(tokenId);
|
|
@@ -422,8 +211,8 @@ var NonceManager = class {
|
|
|
422
211
|
// src/auth/loginVerifier.ts
|
|
423
212
|
var import_node_crypto2 = require("crypto");
|
|
424
213
|
var import_jose = require("jose");
|
|
425
|
-
var
|
|
426
|
-
var
|
|
214
|
+
var import_viem2 = require("viem");
|
|
215
|
+
var import_core = require("@pafi-dev/core");
|
|
427
216
|
|
|
428
217
|
// src/auth/errors.ts
|
|
429
218
|
var AuthError = class extends Error {
|
|
@@ -446,8 +235,8 @@ var AuthService = class {
|
|
|
446
235
|
nonceManager;
|
|
447
236
|
now;
|
|
448
237
|
constructor(config) {
|
|
449
|
-
if (!config.jwtSecret || config.jwtSecret.length <
|
|
450
|
-
throw new Error("AuthService: jwtSecret must be at least
|
|
238
|
+
if (!config.jwtSecret || config.jwtSecret.length < 32) {
|
|
239
|
+
throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
|
|
451
240
|
}
|
|
452
241
|
this.sessionStore = config.sessionStore;
|
|
453
242
|
this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
|
|
@@ -471,11 +260,17 @@ var AuthService = class {
|
|
|
471
260
|
async login(message, signature) {
|
|
472
261
|
let parsed;
|
|
473
262
|
try {
|
|
474
|
-
parsed = (0,
|
|
263
|
+
parsed = (0, import_core.parseLoginMessage)(message);
|
|
475
264
|
} catch (err) {
|
|
476
265
|
const msg = err instanceof Error ? err.message : String(err);
|
|
477
266
|
throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
|
|
478
267
|
}
|
|
268
|
+
if (parsed.expirationTime == null) {
|
|
269
|
+
throw new AuthError(
|
|
270
|
+
"INVALID_MESSAGE",
|
|
271
|
+
"login message must include expirationTime"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
479
274
|
if (parsed.domain !== this.domain) {
|
|
480
275
|
throw new AuthError(
|
|
481
276
|
"DOMAIN_MISMATCH",
|
|
@@ -498,7 +293,7 @@ var AuthService = class {
|
|
|
498
293
|
if (parsed.expirationTime && parsed.expirationTime.getTime() <= now.getTime()) {
|
|
499
294
|
throw new AuthError("MESSAGE_EXPIRED", "Login message has expired");
|
|
500
295
|
}
|
|
501
|
-
const verifyResult = await (0,
|
|
296
|
+
const verifyResult = await (0, import_core.verifyLoginMessage)(message, signature);
|
|
502
297
|
if (!verifyResult.valid) {
|
|
503
298
|
throw new AuthError(
|
|
504
299
|
"SIGNATURE_INVALID",
|
|
@@ -512,7 +307,7 @@ var AuthService = class {
|
|
|
512
307
|
"Nonce is unknown, expired, or already used"
|
|
513
308
|
);
|
|
514
309
|
}
|
|
515
|
-
const userAddress = (0,
|
|
310
|
+
const userAddress = (0, import_viem2.getAddress)(verifyResult.address);
|
|
516
311
|
const tokenId = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
517
312
|
const issuedAt = now;
|
|
518
313
|
const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
|
|
@@ -540,7 +335,11 @@ var AuthService = class {
|
|
|
540
335
|
if (payload.jti) {
|
|
541
336
|
await this.sessionStore.revokeSession(payload.jti);
|
|
542
337
|
}
|
|
543
|
-
} catch {
|
|
338
|
+
} catch (err) {
|
|
339
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
340
|
+
if (!msg.includes("not found") && !msg.includes("expired")) {
|
|
341
|
+
console.error("[PAFI] AuthService logout: session store error", err);
|
|
342
|
+
}
|
|
544
343
|
}
|
|
545
344
|
}
|
|
546
345
|
/**
|
|
@@ -579,7 +378,7 @@ var AuthService = class {
|
|
|
579
378
|
throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
|
|
580
379
|
}
|
|
581
380
|
return {
|
|
582
|
-
userAddress: (0,
|
|
381
|
+
userAddress: (0, import_viem2.getAddress)(userAddress),
|
|
583
382
|
chainId,
|
|
584
383
|
tokenId
|
|
585
384
|
};
|
|
@@ -630,8 +429,8 @@ var RelayError = class extends Error {
|
|
|
630
429
|
};
|
|
631
430
|
|
|
632
431
|
// src/relay/relayService.ts
|
|
633
|
-
var
|
|
634
|
-
var
|
|
432
|
+
var import_viem3 = require("viem");
|
|
433
|
+
var import_core2 = require("@pafi-dev/core");
|
|
635
434
|
var RelayService = class {
|
|
636
435
|
/**
|
|
637
436
|
* Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
|
|
@@ -665,9 +464,20 @@ var RelayService = class {
|
|
|
665
464
|
if (params.deadline <= 0n) {
|
|
666
465
|
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
|
|
667
466
|
}
|
|
467
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
468
|
+
if (params.deadline <= nowSecs) {
|
|
469
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
|
|
470
|
+
}
|
|
471
|
+
const MAX_DEADLINE_WINDOW = 3600n;
|
|
472
|
+
if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
|
|
473
|
+
throw new RelayError(
|
|
474
|
+
"ENCODE_FAILED",
|
|
475
|
+
"prepareMint: deadline exceeds maximum allowed window (1 hour)"
|
|
476
|
+
);
|
|
477
|
+
}
|
|
668
478
|
let minterSig;
|
|
669
479
|
try {
|
|
670
|
-
const sig = await (0,
|
|
480
|
+
const sig = await (0, import_core2.signMintRequest)(
|
|
671
481
|
params.issuerSignerWallet,
|
|
672
482
|
params.domain,
|
|
673
483
|
{
|
|
@@ -687,8 +497,8 @@ var RelayService = class {
|
|
|
687
497
|
}
|
|
688
498
|
let mintCallData;
|
|
689
499
|
try {
|
|
690
|
-
mintCallData = (0,
|
|
691
|
-
abi:
|
|
500
|
+
mintCallData = (0, import_viem3.encodeFunctionData)({
|
|
501
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
692
502
|
functionName: "mint",
|
|
693
503
|
args: [params.userAddress, params.amount, params.deadline, minterSig]
|
|
694
504
|
});
|
|
@@ -713,17 +523,23 @@ var RelayService = class {
|
|
|
713
523
|
"prepareMint: feeRecipient required when feeAmount > 0"
|
|
714
524
|
);
|
|
715
525
|
}
|
|
526
|
+
if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
|
|
527
|
+
throw new RelayError(
|
|
528
|
+
"ENCODE_FAILED",
|
|
529
|
+
"prepareMint: feeRecipient must not be zero address"
|
|
530
|
+
);
|
|
531
|
+
}
|
|
716
532
|
operations.push({
|
|
717
533
|
target: params.pointTokenAddress,
|
|
718
534
|
value: 0n,
|
|
719
|
-
data: (0,
|
|
720
|
-
abi:
|
|
535
|
+
data: (0, import_viem3.encodeFunctionData)({
|
|
536
|
+
abi: import_viem3.erc20Abi,
|
|
721
537
|
functionName: "transfer",
|
|
722
538
|
args: [params.feeRecipient, params.feeAmount]
|
|
723
539
|
})
|
|
724
540
|
});
|
|
725
541
|
}
|
|
726
|
-
return (0,
|
|
542
|
+
return (0, import_core2.buildPartialUserOperation)({
|
|
727
543
|
sender: params.userAddress,
|
|
728
544
|
nonce: params.aaNonce,
|
|
729
545
|
operations,
|
|
@@ -761,8 +577,8 @@ var RelayService = class {
|
|
|
761
577
|
if (!params.burnRequest || !params.burnerSignature) {
|
|
762
578
|
throw new Error("burnWithSig requires burnRequest + burnerSignature");
|
|
763
579
|
}
|
|
764
|
-
burnCallData = (0,
|
|
765
|
-
abi:
|
|
580
|
+
burnCallData = (0, import_viem3.encodeFunctionData)({
|
|
581
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
766
582
|
functionName: "burn",
|
|
767
583
|
args: [
|
|
768
584
|
params.burnRequest.from,
|
|
@@ -772,8 +588,8 @@ var RelayService = class {
|
|
|
772
588
|
]
|
|
773
589
|
});
|
|
774
590
|
} else {
|
|
775
|
-
burnCallData = (0,
|
|
776
|
-
abi:
|
|
591
|
+
burnCallData = (0, import_viem3.encodeFunctionData)({
|
|
592
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
777
593
|
functionName: "burn",
|
|
778
594
|
args: [params.userAddress, params.amount]
|
|
779
595
|
});
|
|
@@ -792,7 +608,7 @@ var RelayService = class {
|
|
|
792
608
|
data: burnCallData
|
|
793
609
|
}
|
|
794
610
|
];
|
|
795
|
-
return (0,
|
|
611
|
+
return (0, import_core2.buildPartialUserOperation)({
|
|
796
612
|
sender: params.userAddress,
|
|
797
613
|
nonce: params.aaNonce,
|
|
798
614
|
operations,
|
|
@@ -811,11 +627,14 @@ function errorMessage(err) {
|
|
|
811
627
|
// src/relay/feeManager.ts
|
|
812
628
|
var DEFAULT_GAS_UNITS = 500000n;
|
|
813
629
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
814
|
-
var FeeManager = class {
|
|
630
|
+
var FeeManager = class _FeeManager {
|
|
815
631
|
provider;
|
|
816
632
|
gasUnits;
|
|
817
633
|
gasPremiumBps;
|
|
818
634
|
quoteNativeToFee;
|
|
635
|
+
cachedFee = null;
|
|
636
|
+
cacheExpiresAt = 0;
|
|
637
|
+
static CACHE_TTL_MS = 1e4;
|
|
819
638
|
constructor(config) {
|
|
820
639
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
821
640
|
if (!config.quoteNativeToFee)
|
|
@@ -838,10 +657,17 @@ var FeeManager = class {
|
|
|
838
657
|
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
839
658
|
*/
|
|
840
659
|
async estimateGasFee() {
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
if (this.cachedFee !== null && now < this.cacheExpiresAt) {
|
|
662
|
+
return this.cachedFee;
|
|
663
|
+
}
|
|
841
664
|
const gasPrice = await this.provider.getGasPrice();
|
|
842
665
|
const nativeCost = gasPrice * this.gasUnits;
|
|
843
666
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
844
|
-
|
|
667
|
+
const fee = await this.quoteNativeToFee(withPremium);
|
|
668
|
+
this.cachedFee = fee;
|
|
669
|
+
this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
|
|
670
|
+
return fee;
|
|
845
671
|
}
|
|
846
672
|
};
|
|
847
673
|
|
|
@@ -857,8 +683,8 @@ var InMemoryCursorStore = class {
|
|
|
857
683
|
};
|
|
858
684
|
|
|
859
685
|
// src/indexer/pointIndexer.ts
|
|
860
|
-
var
|
|
861
|
-
var TRANSFER_EVENT = (0,
|
|
686
|
+
var import_viem4 = require("viem");
|
|
687
|
+
var TRANSFER_EVENT = (0, import_viem4.parseAbiItem)(
|
|
862
688
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
863
689
|
);
|
|
864
690
|
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
@@ -928,7 +754,8 @@ var PointIndexer = class {
|
|
|
928
754
|
return;
|
|
929
755
|
}
|
|
930
756
|
await this.processBlockRange(from, safeHead);
|
|
931
|
-
} catch {
|
|
757
|
+
} catch (err) {
|
|
758
|
+
console.error("[PAFI] PointIndexer tick error:", err);
|
|
932
759
|
}
|
|
933
760
|
this.scheduleNext();
|
|
934
761
|
}
|
|
@@ -978,10 +805,10 @@ var PointIndexer = class {
|
|
|
978
805
|
for (const log of logs) {
|
|
979
806
|
const args = log.args;
|
|
980
807
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
981
|
-
if ((0,
|
|
808
|
+
if ((0, import_viem4.getAddress)(args.from) !== ZERO_ADDRESS) continue;
|
|
982
809
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
983
810
|
out.push({
|
|
984
|
-
to: (0,
|
|
811
|
+
to: (0, import_viem4.getAddress)(args.to),
|
|
985
812
|
amount: args.value,
|
|
986
813
|
blockNumber: log.blockNumber,
|
|
987
814
|
txHash: log.transactionHash,
|
|
@@ -1037,8 +864,8 @@ function pickMatchingLock(locks, amount) {
|
|
|
1037
864
|
}
|
|
1038
865
|
|
|
1039
866
|
// src/indexer/burnIndexer.ts
|
|
1040
|
-
var
|
|
1041
|
-
var TRANSFER_EVENT2 = (0,
|
|
867
|
+
var import_viem5 = require("viem");
|
|
868
|
+
var TRANSFER_EVENT2 = (0, import_viem5.parseAbiItem)(
|
|
1042
869
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1043
870
|
);
|
|
1044
871
|
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
@@ -1054,18 +881,7 @@ var BurnIndexer = class {
|
|
|
1054
881
|
confirmations;
|
|
1055
882
|
batchSize;
|
|
1056
883
|
pollIntervalMs;
|
|
1057
|
-
|
|
1058
|
-
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1059
|
-
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1060
|
-
* ledger's query path.
|
|
1061
|
-
*
|
|
1062
|
-
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1063
|
-
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1064
|
-
* incrementing IDs so callers with the memory ledger must provide a
|
|
1065
|
-
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1066
|
-
* their `pending_credits` table.
|
|
1067
|
-
*/
|
|
1068
|
-
matchLockId = async () => void 0;
|
|
884
|
+
matchLockId;
|
|
1069
885
|
running = false;
|
|
1070
886
|
timer;
|
|
1071
887
|
constructor(config) {
|
|
@@ -1083,6 +899,12 @@ var BurnIndexer = class {
|
|
|
1083
899
|
);
|
|
1084
900
|
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1085
901
|
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
902
|
+
if (!config.matchLockId) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
"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."
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
this.matchLockId = config.matchLockId;
|
|
1086
908
|
}
|
|
1087
909
|
start() {
|
|
1088
910
|
if (this.running) return;
|
|
@@ -1112,7 +934,8 @@ var BurnIndexer = class {
|
|
|
1112
934
|
return;
|
|
1113
935
|
}
|
|
1114
936
|
await this.processBlockRange(from, safeHead);
|
|
1115
|
-
} catch {
|
|
937
|
+
} catch (err) {
|
|
938
|
+
console.error("[PAFI] BurnIndexer tick error:", err);
|
|
1116
939
|
}
|
|
1117
940
|
this.scheduleNext();
|
|
1118
941
|
}
|
|
@@ -1157,10 +980,10 @@ var BurnIndexer = class {
|
|
|
1157
980
|
for (const log of logs) {
|
|
1158
981
|
const args = log.args;
|
|
1159
982
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1160
|
-
if ((0,
|
|
983
|
+
if ((0, import_viem5.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
|
|
1161
984
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1162
985
|
out.push({
|
|
1163
|
-
from: (0,
|
|
986
|
+
from: (0, import_viem5.getAddress)(args.from),
|
|
1164
987
|
amount: args.value,
|
|
1165
988
|
blockNumber: log.blockNumber,
|
|
1166
989
|
txHash: log.transactionHash,
|
|
@@ -1175,21 +998,28 @@ var BurnIndexer = class {
|
|
|
1175
998
|
* log + skip.
|
|
1176
999
|
*/
|
|
1177
1000
|
async finalize(evt) {
|
|
1001
|
+
const txHash = evt.txHash;
|
|
1178
1002
|
const lockId = await this.matchLockId(evt);
|
|
1179
|
-
if (
|
|
1003
|
+
if (lockId === void 0) {
|
|
1004
|
+
console.warn(
|
|
1005
|
+
"[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
|
|
1006
|
+
);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1180
1009
|
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1181
1010
|
return;
|
|
1182
1011
|
}
|
|
1183
1012
|
try {
|
|
1184
1013
|
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1185
|
-
} catch {
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
|
|
1186
1016
|
}
|
|
1187
1017
|
}
|
|
1188
1018
|
};
|
|
1189
1019
|
|
|
1190
1020
|
// src/api/handlers.ts
|
|
1191
|
-
var
|
|
1192
|
-
var
|
|
1021
|
+
var import_viem6 = require("viem");
|
|
1022
|
+
var import_core3 = require("@pafi-dev/core");
|
|
1193
1023
|
var IssuerApiHandlers = class {
|
|
1194
1024
|
authService;
|
|
1195
1025
|
ledger;
|
|
@@ -1199,15 +1029,12 @@ var IssuerApiHandlers = class {
|
|
|
1199
1029
|
* validate the request's `pointTokenAddress` against this set.
|
|
1200
1030
|
*/
|
|
1201
1031
|
supportedTokens;
|
|
1202
|
-
/** First supported token — used as default when a handler doesn't
|
|
1203
|
-
* receive a `pointTokenAddress` in the request (shouldn't happen in
|
|
1204
|
-
* practice, but keeps type-narrowing happy). */
|
|
1205
|
-
defaultToken;
|
|
1206
1032
|
chainId;
|
|
1207
1033
|
contracts;
|
|
1208
1034
|
pafiWebUrl;
|
|
1209
1035
|
feeManager;
|
|
1210
1036
|
poolsProvider;
|
|
1037
|
+
claim;
|
|
1211
1038
|
constructor(config) {
|
|
1212
1039
|
this.authService = config.authService;
|
|
1213
1040
|
this.ledger = config.ledger;
|
|
@@ -1218,14 +1045,14 @@ var IssuerApiHandlers = class {
|
|
|
1218
1045
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1219
1046
|
);
|
|
1220
1047
|
}
|
|
1221
|
-
const normalized = raw.map((a) => (0,
|
|
1048
|
+
const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
|
|
1222
1049
|
this.supportedTokens = new Set(normalized);
|
|
1223
|
-
this.defaultToken = normalized[0];
|
|
1224
1050
|
this.chainId = config.chainId;
|
|
1225
1051
|
this.contracts = config.contracts;
|
|
1226
1052
|
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1227
1053
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1228
1054
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1055
|
+
if (config.claim) this.claim = config.claim;
|
|
1229
1056
|
}
|
|
1230
1057
|
// =========================================================================
|
|
1231
1058
|
// Public handlers (no auth required)
|
|
@@ -1240,6 +1067,12 @@ var IssuerApiHandlers = class {
|
|
|
1240
1067
|
if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
|
|
1241
1068
|
throw new Error("handleLogin: message and signature are required");
|
|
1242
1069
|
}
|
|
1070
|
+
if (body.message.length > 4096) {
|
|
1071
|
+
throw new Error("message too long");
|
|
1072
|
+
}
|
|
1073
|
+
if (body.signature.length > 260) {
|
|
1074
|
+
throw new Error("signature too long");
|
|
1075
|
+
}
|
|
1243
1076
|
const result = await this.authService.login(body.message, body.signature);
|
|
1244
1077
|
return {
|
|
1245
1078
|
token: result.token,
|
|
@@ -1254,9 +1087,12 @@ var IssuerApiHandlers = class {
|
|
|
1254
1087
|
* needs to build EIP-712 messages and interact with on-chain.
|
|
1255
1088
|
*/
|
|
1256
1089
|
async handleConfig(chainId) {
|
|
1090
|
+
if (!Number.isInteger(chainId) || chainId <= 0) {
|
|
1091
|
+
throw new Error("invalid chainId");
|
|
1092
|
+
}
|
|
1257
1093
|
if (chainId !== this.chainId) {
|
|
1258
1094
|
throw new Error(
|
|
1259
|
-
`handleConfig: unsupported chainId ${chainId}
|
|
1095
|
+
`handleConfig: unsupported chainId ${chainId}`
|
|
1260
1096
|
);
|
|
1261
1097
|
}
|
|
1262
1098
|
const contracts = {
|
|
@@ -1319,25 +1155,25 @@ var IssuerApiHandlers = class {
|
|
|
1319
1155
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1320
1156
|
);
|
|
1321
1157
|
}
|
|
1322
|
-
const normalizedAuthed = (0,
|
|
1323
|
-
const normalizedRequest = (0,
|
|
1158
|
+
const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
|
|
1159
|
+
const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
|
|
1324
1160
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1325
1161
|
throw new Error(
|
|
1326
1162
|
"handleUser: request userAddress must match authenticated user"
|
|
1327
1163
|
);
|
|
1328
1164
|
}
|
|
1329
|
-
const pointToken = (0,
|
|
1165
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1330
1166
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1331
1167
|
throw new Error(
|
|
1332
1168
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
1333
1169
|
);
|
|
1334
1170
|
}
|
|
1335
1171
|
const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
|
|
1336
|
-
(0,
|
|
1337
|
-
(0,
|
|
1172
|
+
(0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1173
|
+
(0, import_core3.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1338
1174
|
this.ledger.getBalance(normalizedAuthed, pointToken),
|
|
1339
|
-
(0,
|
|
1340
|
-
(0,
|
|
1175
|
+
(0, import_core3.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
|
|
1176
|
+
(0, import_core3.isMinter)(this.provider, pointToken, normalizedAuthed)
|
|
1341
1177
|
]);
|
|
1342
1178
|
return {
|
|
1343
1179
|
mintRequestNonce,
|
|
@@ -1369,19 +1205,32 @@ var IssuerApiHandlers = class {
|
|
|
1369
1205
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1370
1206
|
);
|
|
1371
1207
|
}
|
|
1372
|
-
const pointToken = (0,
|
|
1208
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1373
1209
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1374
1210
|
throw new Error(
|
|
1375
1211
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
1376
1212
|
);
|
|
1377
1213
|
}
|
|
1378
|
-
const
|
|
1214
|
+
const consent = request.receiverConsent;
|
|
1215
|
+
if ((0, import_viem6.getAddress)(consent.originalReceiver) !== (0, import_viem6.getAddress)(userAddress)) {
|
|
1216
|
+
throw new Error(
|
|
1217
|
+
"handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (consent.amount <= 0n) {
|
|
1221
|
+
throw new Error("handleBuildConsentTypedData: amount must be positive");
|
|
1222
|
+
}
|
|
1223
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1224
|
+
if (consent.deadline <= nowSecs) {
|
|
1225
|
+
throw new Error("handleBuildConsentTypedData: deadline is in the past");
|
|
1226
|
+
}
|
|
1227
|
+
const name = await (0, import_core3.getTokenName)(this.provider, pointToken);
|
|
1379
1228
|
const domain = {
|
|
1380
1229
|
name,
|
|
1381
1230
|
verifyingContract: pointToken,
|
|
1382
1231
|
chainId: this.chainId
|
|
1383
1232
|
};
|
|
1384
|
-
const typedData = (0,
|
|
1233
|
+
const typedData = (0, import_core3.buildReceiverConsentTypedData)(domain, consent);
|
|
1385
1234
|
return {
|
|
1386
1235
|
typedData: {
|
|
1387
1236
|
domain: typedData.domain,
|
|
@@ -1391,11 +1240,96 @@ var IssuerApiHandlers = class {
|
|
|
1391
1240
|
}
|
|
1392
1241
|
};
|
|
1393
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* `POST /claim`
|
|
1245
|
+
*
|
|
1246
|
+
* Policy gate + ledger lock + MintRequest signing in a single atomic
|
|
1247
|
+
* step. Returns an unsigned UserOp the frontend attaches paymaster data
|
|
1248
|
+
* to and submits via EIP-7702 + Bundler.
|
|
1249
|
+
*
|
|
1250
|
+
* Order of operations:
|
|
1251
|
+
* 1. Validate request fields.
|
|
1252
|
+
* 2. policy.evaluate() — throws if denied; cannot be bypassed.
|
|
1253
|
+
* 3. ledger.lockForMinting() — reserves the balance.
|
|
1254
|
+
* 4. Read on-chain mintRequestNonce + token name in parallel.
|
|
1255
|
+
* 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
|
|
1256
|
+
* 6. On any error after step 3, release the lock before re-throwing.
|
|
1257
|
+
*/
|
|
1258
|
+
async handleClaim(userAddress, request) {
|
|
1259
|
+
if (!this.claim) {
|
|
1260
|
+
throw new Error("handleClaim: claim is not configured on this issuer");
|
|
1261
|
+
}
|
|
1262
|
+
if (request.chainId !== this.chainId) {
|
|
1263
|
+
throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
|
|
1264
|
+
}
|
|
1265
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1266
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1267
|
+
throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
|
|
1268
|
+
}
|
|
1269
|
+
if (request.amount <= 0n) {
|
|
1270
|
+
throw new Error("handleClaim: amount must be positive");
|
|
1271
|
+
}
|
|
1272
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1273
|
+
if (request.deadline <= nowSecs) {
|
|
1274
|
+
throw new Error("handleClaim: deadline is in the past");
|
|
1275
|
+
}
|
|
1276
|
+
const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
|
|
1277
|
+
const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
|
|
1278
|
+
const normalizedUser = (0, import_viem6.getAddress)(userAddress);
|
|
1279
|
+
const decision = await policy.evaluate({
|
|
1280
|
+
userAddress: normalizedUser,
|
|
1281
|
+
amount: request.amount,
|
|
1282
|
+
pointTokenAddress: pointToken,
|
|
1283
|
+
chainId: this.chainId
|
|
1284
|
+
});
|
|
1285
|
+
if (!decision.approved) {
|
|
1286
|
+
throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
|
|
1287
|
+
}
|
|
1288
|
+
const lockId = await this.ledger.lockForMinting(
|
|
1289
|
+
normalizedUser,
|
|
1290
|
+
request.amount,
|
|
1291
|
+
lockDurationMs,
|
|
1292
|
+
pointToken
|
|
1293
|
+
);
|
|
1294
|
+
try {
|
|
1295
|
+
const [mintRequestNonce, tokenName] = await Promise.all([
|
|
1296
|
+
(0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedUser),
|
|
1297
|
+
(0, import_core3.getTokenName)(this.provider, pointToken)
|
|
1298
|
+
]);
|
|
1299
|
+
const domain = {
|
|
1300
|
+
name: tokenName,
|
|
1301
|
+
verifyingContract: pointToken,
|
|
1302
|
+
chainId: this.chainId
|
|
1303
|
+
};
|
|
1304
|
+
const userOp = await relayService.prepareMint({
|
|
1305
|
+
userAddress: normalizedUser,
|
|
1306
|
+
aaNonce: request.aaNonce,
|
|
1307
|
+
batchExecutorAddress,
|
|
1308
|
+
pointTokenAddress: pointToken,
|
|
1309
|
+
amount: request.amount,
|
|
1310
|
+
issuerSignerWallet,
|
|
1311
|
+
domain,
|
|
1312
|
+
mintRequestNonce,
|
|
1313
|
+
deadline: request.deadline,
|
|
1314
|
+
feeAmount: request.feeAmount,
|
|
1315
|
+
feeRecipient: request.feeRecipient
|
|
1316
|
+
});
|
|
1317
|
+
return {
|
|
1318
|
+
lockId,
|
|
1319
|
+
userOp,
|
|
1320
|
+
expiresInSeconds: Math.floor(lockDurationMs / 1e3)
|
|
1321
|
+
};
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
await this.ledger.releaseLock(lockId).catch(() => {
|
|
1324
|
+
});
|
|
1325
|
+
throw err;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1394
1328
|
};
|
|
1395
1329
|
|
|
1396
1330
|
// src/api/handlers/ptRedeemHandler.ts
|
|
1397
|
-
var
|
|
1398
|
-
var
|
|
1331
|
+
var import_viem7 = require("viem");
|
|
1332
|
+
var import_core4 = require("@pafi-dev/core");
|
|
1399
1333
|
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1400
1334
|
var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
|
|
1401
1335
|
var PTRedeemError = class extends Error {
|
|
@@ -1434,16 +1368,25 @@ var PTRedeemHandler = class {
|
|
|
1434
1368
|
this.ledger = config.ledger;
|
|
1435
1369
|
this.relayService = config.relayService;
|
|
1436
1370
|
this.provider = config.provider;
|
|
1437
|
-
this.pointTokenAddress = (0,
|
|
1438
|
-
this.batchExecutorAddress = (0,
|
|
1371
|
+
this.pointTokenAddress = (0, import_viem7.getAddress)(config.pointTokenAddress);
|
|
1372
|
+
this.batchExecutorAddress = (0, import_viem7.getAddress)(config.batchExecutorAddress);
|
|
1439
1373
|
this.chainId = config.chainId;
|
|
1440
1374
|
this.domain = config.domain;
|
|
1441
1375
|
this.burnerSignerWallet = config.burnerSignerWallet;
|
|
1376
|
+
if (this.burnerSignerWallet?.account?.type === "local") {
|
|
1377
|
+
console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
|
|
1378
|
+
}
|
|
1442
1379
|
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1443
1380
|
this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
|
|
1444
1381
|
this.now = config.now ?? (() => Date.now());
|
|
1445
1382
|
}
|
|
1446
1383
|
async handle(request) {
|
|
1384
|
+
if ((0, import_viem7.getAddress)(request.authenticatedAddress) !== (0, import_viem7.getAddress)(request.userAddress)) {
|
|
1385
|
+
throw new PTRedeemError(
|
|
1386
|
+
"UNAUTHORIZED",
|
|
1387
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1447
1390
|
if (request.amount <= 0n) {
|
|
1448
1391
|
throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
|
|
1449
1392
|
}
|
|
@@ -1451,7 +1394,7 @@ var PTRedeemHandler = class {
|
|
|
1451
1394
|
try {
|
|
1452
1395
|
burnNonce = await this.provider.readContract({
|
|
1453
1396
|
address: this.pointTokenAddress,
|
|
1454
|
-
abi:
|
|
1397
|
+
abi: import_core4.POINT_TOKEN_V2_ABI,
|
|
1455
1398
|
functionName: "burnRequestNonces",
|
|
1456
1399
|
args: [request.userAddress]
|
|
1457
1400
|
});
|
|
@@ -1461,6 +1404,17 @@ var PTRedeemHandler = class {
|
|
|
1461
1404
|
`failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
|
|
1462
1405
|
);
|
|
1463
1406
|
}
|
|
1407
|
+
const onChainBalance = await (0, import_core4.getPointTokenBalance)(
|
|
1408
|
+
this.provider,
|
|
1409
|
+
this.pointTokenAddress,
|
|
1410
|
+
request.userAddress
|
|
1411
|
+
);
|
|
1412
|
+
if (onChainBalance < request.amount) {
|
|
1413
|
+
throw new PTRedeemError(
|
|
1414
|
+
"INVALID_AMOUNT",
|
|
1415
|
+
`insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1464
1418
|
const deadline = BigInt(
|
|
1465
1419
|
Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
|
|
1466
1420
|
);
|
|
@@ -1477,7 +1431,7 @@ var PTRedeemHandler = class {
|
|
|
1477
1431
|
};
|
|
1478
1432
|
let burnerSignature;
|
|
1479
1433
|
try {
|
|
1480
|
-
const sig = await (0,
|
|
1434
|
+
const sig = await (0, import_core4.signBurnRequest)(
|
|
1481
1435
|
this.burnerSignerWallet,
|
|
1482
1436
|
domain,
|
|
1483
1437
|
burnRequest
|
|
@@ -1514,8 +1468,8 @@ var PTRedeemHandler = class {
|
|
|
1514
1468
|
};
|
|
1515
1469
|
|
|
1516
1470
|
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1517
|
-
var
|
|
1518
|
-
var
|
|
1471
|
+
var import_viem8 = require("viem");
|
|
1472
|
+
var import_core5 = require("@pafi-dev/core");
|
|
1519
1473
|
var TopUpRedemptionError = class extends Error {
|
|
1520
1474
|
constructor(code, message) {
|
|
1521
1475
|
super(message);
|
|
@@ -1533,9 +1487,15 @@ var TopUpRedemptionHandler = class {
|
|
|
1533
1487
|
this.ledger = config.ledger;
|
|
1534
1488
|
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1535
1489
|
this.provider = config.provider;
|
|
1536
|
-
this.pointTokenAddress = (0,
|
|
1490
|
+
this.pointTokenAddress = (0, import_viem8.getAddress)(config.pointTokenAddress);
|
|
1537
1491
|
}
|
|
1538
1492
|
async handle(request) {
|
|
1493
|
+
if ((0, import_viem8.getAddress)(request.authenticatedAddress) !== (0, import_viem8.getAddress)(request.userAddress)) {
|
|
1494
|
+
throw new TopUpRedemptionError(
|
|
1495
|
+
"UNAUTHORIZED",
|
|
1496
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1539
1499
|
const offChainBalance = await this.ledger.getBalance(
|
|
1540
1500
|
request.userAddress,
|
|
1541
1501
|
this.pointTokenAddress
|
|
@@ -1544,7 +1504,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1544
1504
|
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1545
1505
|
}
|
|
1546
1506
|
const shortfall = request.requiredAmount - offChainBalance;
|
|
1547
|
-
const onChainBalance = await (0,
|
|
1507
|
+
const onChainBalance = await (0, import_core5.getPointTokenBalance)(
|
|
1548
1508
|
this.provider,
|
|
1549
1509
|
this.pointTokenAddress,
|
|
1550
1510
|
request.userAddress
|
|
@@ -1558,6 +1518,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1558
1518
|
};
|
|
1559
1519
|
}
|
|
1560
1520
|
const redeem = await this.ptRedeemHandler.handle({
|
|
1521
|
+
authenticatedAddress: request.authenticatedAddress,
|
|
1561
1522
|
userAddress: request.userAddress,
|
|
1562
1523
|
amount: shortfall,
|
|
1563
1524
|
aaNonce: request.aaNonce
|
|
@@ -1571,6 +1532,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1571
1532
|
};
|
|
1572
1533
|
|
|
1573
1534
|
// src/pools/subgraphPoolsProvider.ts
|
|
1535
|
+
var import_viem9 = require("viem");
|
|
1574
1536
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1575
1537
|
var POOL_QUERY = `
|
|
1576
1538
|
query GetPoolForPointToken($id: ID!) {
|
|
@@ -1593,6 +1555,19 @@ function createSubgraphPoolsProvider(config) {
|
|
|
1593
1555
|
"createSubgraphPoolsProvider: subgraphUrl is required"
|
|
1594
1556
|
);
|
|
1595
1557
|
}
|
|
1558
|
+
try {
|
|
1559
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1560
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1561
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1562
|
+
}
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
if (err instanceof TypeError) {
|
|
1565
|
+
throw new Error(
|
|
1566
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
throw err;
|
|
1570
|
+
}
|
|
1596
1571
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
1597
1572
|
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1598
1573
|
const now = config.now ?? (() => Date.now());
|
|
@@ -1661,6 +1636,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
|
|
|
1661
1636
|
return [];
|
|
1662
1637
|
}
|
|
1663
1638
|
const { pool } = token;
|
|
1639
|
+
if (!(0, import_viem9.isAddress)(pool.hooks)) {
|
|
1640
|
+
console.error(
|
|
1641
|
+
"[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
|
|
1642
|
+
pool.hooks,
|
|
1643
|
+
"\u2014 skipping pool"
|
|
1644
|
+
);
|
|
1645
|
+
return [];
|
|
1646
|
+
}
|
|
1647
|
+
if (!(0, import_viem9.isAddress)(pool.token0.id) || !(0, import_viem9.isAddress)(pool.token1.id)) {
|
|
1648
|
+
console.error(
|
|
1649
|
+
"[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
|
|
1650
|
+
);
|
|
1651
|
+
return [];
|
|
1652
|
+
}
|
|
1653
|
+
if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
|
|
1654
|
+
console.error(
|
|
1655
|
+
"[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
|
|
1656
|
+
);
|
|
1657
|
+
return [];
|
|
1658
|
+
}
|
|
1664
1659
|
const [currency0, currency1] = sortCurrencies(
|
|
1665
1660
|
pool.token0.id,
|
|
1666
1661
|
pool.token1.id
|
|
@@ -1696,6 +1691,19 @@ function createSubgraphNativeUsdtQuoter(config) {
|
|
|
1696
1691
|
"createSubgraphNativeUsdtQuoter: subgraphUrl is required"
|
|
1697
1692
|
);
|
|
1698
1693
|
}
|
|
1694
|
+
try {
|
|
1695
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1696
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1697
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1698
|
+
}
|
|
1699
|
+
} catch (err) {
|
|
1700
|
+
if (err instanceof TypeError) {
|
|
1701
|
+
throw new Error(
|
|
1702
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
throw err;
|
|
1706
|
+
}
|
|
1699
1707
|
const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
|
|
1700
1708
|
const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
|
|
1701
1709
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
|
|
@@ -1772,6 +1780,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
|
|
|
1772
1780
|
);
|
|
1773
1781
|
return null;
|
|
1774
1782
|
}
|
|
1783
|
+
const MIN_REASONABLE_ETH_PRICE = 100;
|
|
1784
|
+
const MAX_REASONABLE_ETH_PRICE = 1e5;
|
|
1785
|
+
if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
|
|
1786
|
+
console.warn(
|
|
1787
|
+
`[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
|
|
1788
|
+
);
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1775
1791
|
return parsed;
|
|
1776
1792
|
}
|
|
1777
1793
|
function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
@@ -1782,7 +1798,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1782
1798
|
}
|
|
1783
1799
|
|
|
1784
1800
|
// src/balance/balanceAggregator.ts
|
|
1785
|
-
var
|
|
1801
|
+
var import_core6 = require("@pafi-dev/core");
|
|
1786
1802
|
var BalanceAggregator = class {
|
|
1787
1803
|
provider;
|
|
1788
1804
|
ledger;
|
|
@@ -1803,7 +1819,7 @@ var BalanceAggregator = class {
|
|
|
1803
1819
|
async getCombinedBalance(user, pointToken) {
|
|
1804
1820
|
const [offChain, onChain] = await Promise.all([
|
|
1805
1821
|
this.ledger.getBalance(user, pointToken),
|
|
1806
|
-
(0,
|
|
1822
|
+
(0, import_core6.getPointTokenBalance)(this.provider, pointToken, user)
|
|
1807
1823
|
]);
|
|
1808
1824
|
return {
|
|
1809
1825
|
offChain,
|
|
@@ -1863,11 +1879,14 @@ var PafiBackendError = class extends Error {
|
|
|
1863
1879
|
};
|
|
1864
1880
|
|
|
1865
1881
|
// src/config.ts
|
|
1866
|
-
var
|
|
1882
|
+
var import_viem10 = require("viem");
|
|
1867
1883
|
function createIssuerService(config) {
|
|
1868
1884
|
if (!config.provider) {
|
|
1869
1885
|
throw new Error("createIssuerService: provider is required");
|
|
1870
1886
|
}
|
|
1887
|
+
if (!config.ledger) {
|
|
1888
|
+
throw new Error("createIssuerService: ledger is required");
|
|
1889
|
+
}
|
|
1871
1890
|
if (!config.auth?.jwtSecret) {
|
|
1872
1891
|
throw new Error("createIssuerService: auth.jwtSecret is required");
|
|
1873
1892
|
}
|
|
@@ -1880,8 +1899,8 @@ function createIssuerService(config) {
|
|
|
1880
1899
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
1881
1900
|
);
|
|
1882
1901
|
}
|
|
1883
|
-
const tokenAddresses = rawAddresses.map((a) => (0,
|
|
1884
|
-
const ledger = config.ledger
|
|
1902
|
+
const tokenAddresses = rawAddresses.map((a) => (0, import_viem10.getAddress)(a));
|
|
1903
|
+
const ledger = config.ledger;
|
|
1885
1904
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1886
1905
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
1887
1906
|
const authServiceConfig = {
|
|
@@ -1937,6 +1956,15 @@ function createIssuerService(config) {
|
|
|
1937
1956
|
};
|
|
1938
1957
|
if (feeManager) handlersConfig.feeManager = feeManager;
|
|
1939
1958
|
if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
|
|
1959
|
+
if (config.claim) {
|
|
1960
|
+
handlersConfig.claim = {
|
|
1961
|
+
policy,
|
|
1962
|
+
relayService,
|
|
1963
|
+
issuerSignerWallet: config.claim.issuerSignerWallet,
|
|
1964
|
+
batchExecutorAddress: config.claim.batchExecutorAddress,
|
|
1965
|
+
lockDurationMs: config.claim.lockDurationMs
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1940
1968
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
1941
1969
|
if (config.indexer?.autoStart) {
|
|
1942
1970
|
for (const idx of indexers.values()) {
|
|
@@ -1968,7 +1996,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1968
1996
|
FeeManager,
|
|
1969
1997
|
InMemoryCursorStore,
|
|
1970
1998
|
IssuerApiHandlers,
|
|
1971
|
-
MemoryPointLedger,
|
|
1972
1999
|
MemorySessionStore,
|
|
1973
2000
|
NonceManager,
|
|
1974
2001
|
PAFI_ISSUER_SDK_VERSION,
|
|
@@ -1976,7 +2003,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1976
2003
|
PTRedeemHandler,
|
|
1977
2004
|
PafiBackendError,
|
|
1978
2005
|
PointIndexer,
|
|
1979
|
-
PrivateKeySigner,
|
|
1980
2006
|
RelayError,
|
|
1981
2007
|
RelayService,
|
|
1982
2008
|
TopUpRedemptionError,
|