@pafi-dev/issuer 0.3.0-beta.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -292
- package/dist/index.cjs +394 -489
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +132 -308
- package/dist/index.d.ts +132 -308
- package/dist/index.js +370 -465
- 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,
|
|
@@ -37,7 +36,6 @@ __export(index_exports, {
|
|
|
37
36
|
PafiBackendClient: () => PafiBackendClient,
|
|
38
37
|
PafiBackendError: () => PafiBackendError,
|
|
39
38
|
PointIndexer: () => PointIndexer,
|
|
40
|
-
PrivateKeySigner: () => PrivateKeySigner,
|
|
41
39
|
RelayError: () => RelayError,
|
|
42
40
|
RelayService: () => RelayService,
|
|
43
41
|
TopUpRedemptionError: () => TopUpRedemptionError,
|
|
@@ -49,204 +47,6 @@ __export(index_exports, {
|
|
|
49
47
|
});
|
|
50
48
|
module.exports = __toCommonJS(index_exports);
|
|
51
49
|
|
|
52
|
-
// src/ledger/memoryLedger.ts
|
|
53
|
-
var import_viem = require("viem");
|
|
54
|
-
var MemoryPointLedger = class {
|
|
55
|
-
balances = /* @__PURE__ */ new Map();
|
|
56
|
-
locks = /* @__PURE__ */ new Map();
|
|
57
|
-
nextLockId = 1;
|
|
58
|
-
now;
|
|
59
|
-
constructor(opts = {}) {
|
|
60
|
-
this.now = opts.now ?? (() => Date.now());
|
|
61
|
-
}
|
|
62
|
-
// -------------------------------------------------------------------------
|
|
63
|
-
// Read
|
|
64
|
-
// -------------------------------------------------------------------------
|
|
65
|
-
async getBalance(userAddress, tokenAddress) {
|
|
66
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
67
|
-
const token = normalizeToken(tokenAddress);
|
|
68
|
-
this.purgeExpired();
|
|
69
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
70
|
-
const locked = this.lockedTotalFor(user, token);
|
|
71
|
-
return total - locked;
|
|
72
|
-
}
|
|
73
|
-
async getLockedRequests(userAddress, tokenAddress) {
|
|
74
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
75
|
-
const token = normalizeToken(tokenAddress);
|
|
76
|
-
this.purgeExpired();
|
|
77
|
-
const out = [];
|
|
78
|
-
for (const lock of this.locks.values()) {
|
|
79
|
-
if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
80
|
-
out.push({ ...lock });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return out;
|
|
84
|
-
}
|
|
85
|
-
// -------------------------------------------------------------------------
|
|
86
|
-
// Write
|
|
87
|
-
// -------------------------------------------------------------------------
|
|
88
|
-
async creditBalance(userAddress, amount, _reason, tokenAddress) {
|
|
89
|
-
if (amount <= 0n) {
|
|
90
|
-
throw new Error("MemoryPointLedger: credit amount must be positive");
|
|
91
|
-
}
|
|
92
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
93
|
-
const token = normalizeToken(tokenAddress);
|
|
94
|
-
const key = balanceKey(user, token);
|
|
95
|
-
const current = this.balances.get(key) ?? 0n;
|
|
96
|
-
this.balances.set(key, current + amount);
|
|
97
|
-
}
|
|
98
|
-
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
99
|
-
if (amount <= 0n) {
|
|
100
|
-
throw new Error("MemoryPointLedger: lock amount must be positive");
|
|
101
|
-
}
|
|
102
|
-
if (lockDurationMs <= 0) {
|
|
103
|
-
throw new Error("MemoryPointLedger: lockDurationMs must be positive");
|
|
104
|
-
}
|
|
105
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
106
|
-
const token = normalizeToken(tokenAddress);
|
|
107
|
-
this.purgeExpired();
|
|
108
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
109
|
-
const alreadyLocked = this.lockedTotalFor(user, token);
|
|
110
|
-
const available = total - alreadyLocked;
|
|
111
|
-
if (available < amount) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
const lockId = `lock-${this.nextLockId++}`;
|
|
117
|
-
const now = this.now();
|
|
118
|
-
const lock = {
|
|
119
|
-
lockId,
|
|
120
|
-
userAddress: user,
|
|
121
|
-
amount,
|
|
122
|
-
status: "PENDING",
|
|
123
|
-
createdAt: now,
|
|
124
|
-
expiresAt: now + lockDurationMs
|
|
125
|
-
};
|
|
126
|
-
if (tokenAddress !== void 0) {
|
|
127
|
-
lock.tokenAddress = (0, import_viem.getAddress)(tokenAddress);
|
|
128
|
-
}
|
|
129
|
-
this.locks.set(lockId, lock);
|
|
130
|
-
return lockId;
|
|
131
|
-
}
|
|
132
|
-
async releaseLock(lockId) {
|
|
133
|
-
const lock = this.locks.get(lockId);
|
|
134
|
-
if (!lock) return;
|
|
135
|
-
if (lock.status === "PENDING") {
|
|
136
|
-
this.locks.delete(lockId);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
140
|
-
if (amount <= 0n) {
|
|
141
|
-
throw new Error("MemoryPointLedger: deduct amount must be positive");
|
|
142
|
-
}
|
|
143
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
144
|
-
const token = normalizeToken(tokenAddress);
|
|
145
|
-
const key = balanceKey(user, token);
|
|
146
|
-
const current = this.balances.get(key) ?? 0n;
|
|
147
|
-
if (current < amount) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
this.balances.set(key, current - amount);
|
|
153
|
-
for (const lock of this.locks.values()) {
|
|
154
|
-
if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
155
|
-
lock.status = "MINTED";
|
|
156
|
-
lock.txHash = txHash;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
async updateMintStatus(lockId, status, txHash) {
|
|
162
|
-
const lock = this.locks.get(lockId);
|
|
163
|
-
if (!lock) {
|
|
164
|
-
throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
|
|
165
|
-
}
|
|
166
|
-
lock.status = status;
|
|
167
|
-
if (txHash) lock.txHash = txHash;
|
|
168
|
-
}
|
|
169
|
-
// -------------------------------------------------------------------------
|
|
170
|
-
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
171
|
-
// -------------------------------------------------------------------------
|
|
172
|
-
pendingCredits = /* @__PURE__ */ new Map();
|
|
173
|
-
nextCreditId = 1;
|
|
174
|
-
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
175
|
-
if (amount <= 0n) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
"MemoryPointLedger: pending credit amount must be positive"
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
if (durationMs <= 0) {
|
|
181
|
-
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
182
|
-
}
|
|
183
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
184
|
-
const lockId = `credit-${this.nextCreditId++}`;
|
|
185
|
-
const now = this.now();
|
|
186
|
-
this.pendingCredits.set(lockId, {
|
|
187
|
-
lockId,
|
|
188
|
-
userAddress: user,
|
|
189
|
-
amount,
|
|
190
|
-
tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
|
|
191
|
-
createdAt: now,
|
|
192
|
-
expiresAt: now + durationMs,
|
|
193
|
-
status: "PENDING"
|
|
194
|
-
});
|
|
195
|
-
return lockId;
|
|
196
|
-
}
|
|
197
|
-
async resolveCreditByBurnTx(lockId, txHash) {
|
|
198
|
-
const credit = this.pendingCredits.get(lockId);
|
|
199
|
-
if (!credit) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
if (credit.status === "RESOLVED") {
|
|
205
|
-
if (credit.txHash === txHash) return;
|
|
206
|
-
throw new Error(
|
|
207
|
-
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
const token = normalizeToken(credit.tokenAddress);
|
|
211
|
-
const key = balanceKey(credit.userAddress, token);
|
|
212
|
-
const current = this.balances.get(key) ?? 0n;
|
|
213
|
-
this.balances.set(key, current + credit.amount);
|
|
214
|
-
credit.status = "RESOLVED";
|
|
215
|
-
credit.txHash = txHash;
|
|
216
|
-
}
|
|
217
|
-
// -------------------------------------------------------------------------
|
|
218
|
-
// Internal helpers
|
|
219
|
-
// -------------------------------------------------------------------------
|
|
220
|
-
/**
|
|
221
|
-
* Auto-expire any PENDING lock past its expiry. Called lazily on every
|
|
222
|
-
* read/write so the in-memory state stays self-cleaning without a timer.
|
|
223
|
-
*/
|
|
224
|
-
purgeExpired() {
|
|
225
|
-
const now = this.now();
|
|
226
|
-
for (const lock of this.locks.values()) {
|
|
227
|
-
if (lock.status === "PENDING" && lock.expiresAt <= now) {
|
|
228
|
-
lock.status = "EXPIRED";
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
lockedTotalFor(userAddress, tokenKey) {
|
|
233
|
-
let total = 0n;
|
|
234
|
-
for (const lock of this.locks.values()) {
|
|
235
|
-
if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
|
|
236
|
-
total += lock.amount;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return total;
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
var DEFAULT_TOKEN_KEY = "default";
|
|
243
|
-
function normalizeToken(tokenAddress) {
|
|
244
|
-
return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : (0, import_viem.getAddress)(tokenAddress);
|
|
245
|
-
}
|
|
246
|
-
function balanceKey(user, tokenKey) {
|
|
247
|
-
return `${user}|${tokenKey}`;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
50
|
// src/policy/defaultPolicy.ts
|
|
251
51
|
var DefaultPolicyEngine = class {
|
|
252
52
|
ledger;
|
|
@@ -262,6 +62,13 @@ var DefaultPolicyEngine = class {
|
|
|
262
62
|
}
|
|
263
63
|
if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
|
|
264
64
|
if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
|
|
65
|
+
if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
|
|
66
|
+
if (process.env.NODE_ENV === "production") {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
265
72
|
}
|
|
266
73
|
async evaluate(request) {
|
|
267
74
|
if (request.amount <= 0n) {
|
|
@@ -298,32 +105,9 @@ var DefaultPolicyEngine = class {
|
|
|
298
105
|
}
|
|
299
106
|
};
|
|
300
107
|
|
|
301
|
-
// src/signer/privateKeySigner.ts
|
|
302
|
-
var import_viem2 = require("viem");
|
|
303
|
-
var import_accounts = require("viem/accounts");
|
|
304
|
-
var import_core = require("@pafi-dev/core");
|
|
305
|
-
var PrivateKeySigner = class {
|
|
306
|
-
account;
|
|
307
|
-
walletClient;
|
|
308
|
-
constructor(opts) {
|
|
309
|
-
this.account = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
|
|
310
|
-
this.walletClient = (0, import_viem2.createWalletClient)({
|
|
311
|
-
account: this.account,
|
|
312
|
-
chain: opts.chain,
|
|
313
|
-
transport: (0, import_viem2.http)(opts.rpcUrl)
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
async signMintRequest(domain, message) {
|
|
317
|
-
return (0, import_core.signMintRequest)(this.walletClient, domain, message);
|
|
318
|
-
}
|
|
319
|
-
async getAddress() {
|
|
320
|
-
return this.account.address;
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
108
|
// src/auth/memorySessionStore.ts
|
|
325
109
|
var import_node_crypto = require("crypto");
|
|
326
|
-
var
|
|
110
|
+
var import_viem = require("viem");
|
|
327
111
|
var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
328
112
|
var MemorySessionStore = class {
|
|
329
113
|
nonces = /* @__PURE__ */ new Map();
|
|
@@ -333,6 +117,11 @@ var MemorySessionStore = class {
|
|
|
333
117
|
nonceTtlMs;
|
|
334
118
|
now;
|
|
335
119
|
constructor(opts = {}) {
|
|
120
|
+
if (process.env.NODE_ENV === "production") {
|
|
121
|
+
console.error(
|
|
122
|
+
"[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."
|
|
123
|
+
);
|
|
124
|
+
}
|
|
336
125
|
this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
|
|
337
126
|
this.now = opts.now ?? (() => Date.now());
|
|
338
127
|
}
|
|
@@ -359,7 +148,7 @@ var MemorySessionStore = class {
|
|
|
359
148
|
this.purgeExpiredSessions();
|
|
360
149
|
const normalized = {
|
|
361
150
|
...session,
|
|
362
|
-
userAddress: (0,
|
|
151
|
+
userAddress: (0, import_viem.getAddress)(session.userAddress)
|
|
363
152
|
};
|
|
364
153
|
this.sessions.set(session.tokenId, normalized);
|
|
365
154
|
}
|
|
@@ -377,7 +166,7 @@ var MemorySessionStore = class {
|
|
|
377
166
|
this.sessions.delete(tokenId);
|
|
378
167
|
}
|
|
379
168
|
async revokeAllSessions(userAddress) {
|
|
380
|
-
const key = (0,
|
|
169
|
+
const key = (0, import_viem.getAddress)(userAddress);
|
|
381
170
|
for (const [tokenId, session] of this.sessions.entries()) {
|
|
382
171
|
if (session.userAddress === key) {
|
|
383
172
|
this.sessions.delete(tokenId);
|
|
@@ -423,8 +212,8 @@ var NonceManager = class {
|
|
|
423
212
|
// src/auth/loginVerifier.ts
|
|
424
213
|
var import_node_crypto2 = require("crypto");
|
|
425
214
|
var import_jose = require("jose");
|
|
426
|
-
var
|
|
427
|
-
var
|
|
215
|
+
var import_viem2 = require("viem");
|
|
216
|
+
var import_core = require("@pafi-dev/core");
|
|
428
217
|
|
|
429
218
|
// src/auth/errors.ts
|
|
430
219
|
var AuthError = class extends Error {
|
|
@@ -447,8 +236,8 @@ var AuthService = class {
|
|
|
447
236
|
nonceManager;
|
|
448
237
|
now;
|
|
449
238
|
constructor(config) {
|
|
450
|
-
if (!config.jwtSecret || config.jwtSecret.length <
|
|
451
|
-
throw new Error("AuthService: jwtSecret must be at least
|
|
239
|
+
if (!config.jwtSecret || config.jwtSecret.length < 32) {
|
|
240
|
+
throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
|
|
452
241
|
}
|
|
453
242
|
this.sessionStore = config.sessionStore;
|
|
454
243
|
this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
|
|
@@ -472,11 +261,17 @@ var AuthService = class {
|
|
|
472
261
|
async login(message, signature) {
|
|
473
262
|
let parsed;
|
|
474
263
|
try {
|
|
475
|
-
parsed = (0,
|
|
264
|
+
parsed = (0, import_core.parseLoginMessage)(message);
|
|
476
265
|
} catch (err) {
|
|
477
266
|
const msg = err instanceof Error ? err.message : String(err);
|
|
478
267
|
throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
|
|
479
268
|
}
|
|
269
|
+
if (parsed.expirationTime == null) {
|
|
270
|
+
throw new AuthError(
|
|
271
|
+
"INVALID_MESSAGE",
|
|
272
|
+
"login message must include expirationTime"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
480
275
|
if (parsed.domain !== this.domain) {
|
|
481
276
|
throw new AuthError(
|
|
482
277
|
"DOMAIN_MISMATCH",
|
|
@@ -499,7 +294,7 @@ var AuthService = class {
|
|
|
499
294
|
if (parsed.expirationTime && parsed.expirationTime.getTime() <= now.getTime()) {
|
|
500
295
|
throw new AuthError("MESSAGE_EXPIRED", "Login message has expired");
|
|
501
296
|
}
|
|
502
|
-
const verifyResult = await (0,
|
|
297
|
+
const verifyResult = await (0, import_core.verifyLoginMessage)(message, signature);
|
|
503
298
|
if (!verifyResult.valid) {
|
|
504
299
|
throw new AuthError(
|
|
505
300
|
"SIGNATURE_INVALID",
|
|
@@ -513,7 +308,7 @@ var AuthService = class {
|
|
|
513
308
|
"Nonce is unknown, expired, or already used"
|
|
514
309
|
);
|
|
515
310
|
}
|
|
516
|
-
const userAddress = (0,
|
|
311
|
+
const userAddress = (0, import_viem2.getAddress)(verifyResult.address);
|
|
517
312
|
const tokenId = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
518
313
|
const issuedAt = now;
|
|
519
314
|
const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
|
|
@@ -541,7 +336,11 @@ var AuthService = class {
|
|
|
541
336
|
if (payload.jti) {
|
|
542
337
|
await this.sessionStore.revokeSession(payload.jti);
|
|
543
338
|
}
|
|
544
|
-
} catch {
|
|
339
|
+
} catch (err) {
|
|
340
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
341
|
+
if (!msg.includes("not found") && !msg.includes("expired")) {
|
|
342
|
+
console.error("[PAFI] AuthService logout: session store error", err);
|
|
343
|
+
}
|
|
545
344
|
}
|
|
546
345
|
}
|
|
547
346
|
/**
|
|
@@ -580,7 +379,7 @@ var AuthService = class {
|
|
|
580
379
|
throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
|
|
581
380
|
}
|
|
582
381
|
return {
|
|
583
|
-
userAddress: (0,
|
|
382
|
+
userAddress: (0, import_viem2.getAddress)(userAddress),
|
|
584
383
|
chainId,
|
|
585
384
|
tokenId
|
|
586
385
|
};
|
|
@@ -631,8 +430,8 @@ var RelayError = class extends Error {
|
|
|
631
430
|
};
|
|
632
431
|
|
|
633
432
|
// src/relay/relayService.ts
|
|
634
|
-
var
|
|
635
|
-
var
|
|
433
|
+
var import_viem3 = require("viem");
|
|
434
|
+
var import_core2 = require("@pafi-dev/core");
|
|
636
435
|
var RelayService = class {
|
|
637
436
|
/**
|
|
638
437
|
* Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
|
|
@@ -666,9 +465,20 @@ var RelayService = class {
|
|
|
666
465
|
if (params.deadline <= 0n) {
|
|
667
466
|
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
|
|
668
467
|
}
|
|
468
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
469
|
+
if (params.deadline <= nowSecs) {
|
|
470
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
|
|
471
|
+
}
|
|
472
|
+
const MAX_DEADLINE_WINDOW = 3600n;
|
|
473
|
+
if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
|
|
474
|
+
throw new RelayError(
|
|
475
|
+
"ENCODE_FAILED",
|
|
476
|
+
"prepareMint: deadline exceeds maximum allowed window (1 hour)"
|
|
477
|
+
);
|
|
478
|
+
}
|
|
669
479
|
let minterSig;
|
|
670
480
|
try {
|
|
671
|
-
const sig = await (0,
|
|
481
|
+
const sig = await (0, import_core2.signMintRequest)(
|
|
672
482
|
params.issuerSignerWallet,
|
|
673
483
|
params.domain,
|
|
674
484
|
{
|
|
@@ -688,8 +498,8 @@ var RelayService = class {
|
|
|
688
498
|
}
|
|
689
499
|
let mintCallData;
|
|
690
500
|
try {
|
|
691
|
-
mintCallData = (0,
|
|
692
|
-
abi:
|
|
501
|
+
mintCallData = (0, import_viem3.encodeFunctionData)({
|
|
502
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
693
503
|
functionName: "mint",
|
|
694
504
|
args: [params.userAddress, params.amount, params.deadline, minterSig]
|
|
695
505
|
});
|
|
@@ -714,17 +524,23 @@ var RelayService = class {
|
|
|
714
524
|
"prepareMint: feeRecipient required when feeAmount > 0"
|
|
715
525
|
);
|
|
716
526
|
}
|
|
527
|
+
if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
|
|
528
|
+
throw new RelayError(
|
|
529
|
+
"ENCODE_FAILED",
|
|
530
|
+
"prepareMint: feeRecipient must not be zero address"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
717
533
|
operations.push({
|
|
718
534
|
target: params.pointTokenAddress,
|
|
719
535
|
value: 0n,
|
|
720
|
-
data: (0,
|
|
721
|
-
abi:
|
|
536
|
+
data: (0, import_viem3.encodeFunctionData)({
|
|
537
|
+
abi: import_viem3.erc20Abi,
|
|
722
538
|
functionName: "transfer",
|
|
723
539
|
args: [params.feeRecipient, params.feeAmount]
|
|
724
540
|
})
|
|
725
541
|
});
|
|
726
542
|
}
|
|
727
|
-
return (0,
|
|
543
|
+
return (0, import_core2.buildPartialUserOperation)({
|
|
728
544
|
sender: params.userAddress,
|
|
729
545
|
nonce: params.aaNonce,
|
|
730
546
|
operations,
|
|
@@ -762,8 +578,8 @@ var RelayService = class {
|
|
|
762
578
|
if (!params.burnRequest || !params.burnerSignature) {
|
|
763
579
|
throw new Error("burnWithSig requires burnRequest + burnerSignature");
|
|
764
580
|
}
|
|
765
|
-
burnCallData = (0,
|
|
766
|
-
abi:
|
|
581
|
+
burnCallData = (0, import_viem3.encodeFunctionData)({
|
|
582
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
767
583
|
functionName: "burn",
|
|
768
584
|
args: [
|
|
769
585
|
params.burnRequest.from,
|
|
@@ -773,8 +589,8 @@ var RelayService = class {
|
|
|
773
589
|
]
|
|
774
590
|
});
|
|
775
591
|
} else {
|
|
776
|
-
burnCallData = (0,
|
|
777
|
-
abi:
|
|
592
|
+
burnCallData = (0, import_viem3.encodeFunctionData)({
|
|
593
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
778
594
|
functionName: "burn",
|
|
779
595
|
args: [params.userAddress, params.amount]
|
|
780
596
|
});
|
|
@@ -793,7 +609,7 @@ var RelayService = class {
|
|
|
793
609
|
data: burnCallData
|
|
794
610
|
}
|
|
795
611
|
];
|
|
796
|
-
return (0,
|
|
612
|
+
return (0, import_core2.buildPartialUserOperation)({
|
|
797
613
|
sender: params.userAddress,
|
|
798
614
|
nonce: params.aaNonce,
|
|
799
615
|
operations,
|
|
@@ -812,11 +628,14 @@ function errorMessage(err) {
|
|
|
812
628
|
// src/relay/feeManager.ts
|
|
813
629
|
var DEFAULT_GAS_UNITS = 500000n;
|
|
814
630
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
815
|
-
var FeeManager = class {
|
|
631
|
+
var FeeManager = class _FeeManager {
|
|
816
632
|
provider;
|
|
817
633
|
gasUnits;
|
|
818
634
|
gasPremiumBps;
|
|
819
635
|
quoteNativeToFee;
|
|
636
|
+
cachedFee = null;
|
|
637
|
+
cacheExpiresAt = 0;
|
|
638
|
+
static CACHE_TTL_MS = 1e4;
|
|
820
639
|
constructor(config) {
|
|
821
640
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
822
641
|
if (!config.quoteNativeToFee)
|
|
@@ -839,10 +658,17 @@ var FeeManager = class {
|
|
|
839
658
|
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
840
659
|
*/
|
|
841
660
|
async estimateGasFee() {
|
|
661
|
+
const now = Date.now();
|
|
662
|
+
if (this.cachedFee !== null && now < this.cacheExpiresAt) {
|
|
663
|
+
return this.cachedFee;
|
|
664
|
+
}
|
|
842
665
|
const gasPrice = await this.provider.getGasPrice();
|
|
843
666
|
const nativeCost = gasPrice * this.gasUnits;
|
|
844
667
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
845
|
-
|
|
668
|
+
const fee = await this.quoteNativeToFee(withPremium);
|
|
669
|
+
this.cachedFee = fee;
|
|
670
|
+
this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
|
|
671
|
+
return fee;
|
|
846
672
|
}
|
|
847
673
|
};
|
|
848
674
|
|
|
@@ -858,8 +684,8 @@ var InMemoryCursorStore = class {
|
|
|
858
684
|
};
|
|
859
685
|
|
|
860
686
|
// src/indexer/pointIndexer.ts
|
|
861
|
-
var
|
|
862
|
-
var TRANSFER_EVENT = (0,
|
|
687
|
+
var import_viem4 = require("viem");
|
|
688
|
+
var TRANSFER_EVENT = (0, import_viem4.parseAbiItem)(
|
|
863
689
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
864
690
|
);
|
|
865
691
|
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
@@ -929,7 +755,8 @@ var PointIndexer = class {
|
|
|
929
755
|
return;
|
|
930
756
|
}
|
|
931
757
|
await this.processBlockRange(from, safeHead);
|
|
932
|
-
} catch {
|
|
758
|
+
} catch (err) {
|
|
759
|
+
console.error("[PAFI] PointIndexer tick error:", err);
|
|
933
760
|
}
|
|
934
761
|
this.scheduleNext();
|
|
935
762
|
}
|
|
@@ -979,10 +806,10 @@ var PointIndexer = class {
|
|
|
979
806
|
for (const log of logs) {
|
|
980
807
|
const args = log.args;
|
|
981
808
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
982
|
-
if ((0,
|
|
809
|
+
if ((0, import_viem4.getAddress)(args.from) !== ZERO_ADDRESS) continue;
|
|
983
810
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
984
811
|
out.push({
|
|
985
|
-
to: (0,
|
|
812
|
+
to: (0, import_viem4.getAddress)(args.to),
|
|
986
813
|
amount: args.value,
|
|
987
814
|
blockNumber: log.blockNumber,
|
|
988
815
|
txHash: log.transactionHash,
|
|
@@ -1038,8 +865,8 @@ function pickMatchingLock(locks, amount) {
|
|
|
1038
865
|
}
|
|
1039
866
|
|
|
1040
867
|
// src/indexer/burnIndexer.ts
|
|
1041
|
-
var
|
|
1042
|
-
var TRANSFER_EVENT2 = (0,
|
|
868
|
+
var import_viem5 = require("viem");
|
|
869
|
+
var TRANSFER_EVENT2 = (0, import_viem5.parseAbiItem)(
|
|
1043
870
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1044
871
|
);
|
|
1045
872
|
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
@@ -1055,18 +882,7 @@ var BurnIndexer = class {
|
|
|
1055
882
|
confirmations;
|
|
1056
883
|
batchSize;
|
|
1057
884
|
pollIntervalMs;
|
|
1058
|
-
|
|
1059
|
-
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1060
|
-
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1061
|
-
* ledger's query path.
|
|
1062
|
-
*
|
|
1063
|
-
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1064
|
-
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1065
|
-
* incrementing IDs so callers with the memory ledger must provide a
|
|
1066
|
-
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1067
|
-
* their `pending_credits` table.
|
|
1068
|
-
*/
|
|
1069
|
-
matchLockId = async () => void 0;
|
|
885
|
+
matchLockId;
|
|
1070
886
|
running = false;
|
|
1071
887
|
timer;
|
|
1072
888
|
constructor(config) {
|
|
@@ -1084,6 +900,12 @@ var BurnIndexer = class {
|
|
|
1084
900
|
);
|
|
1085
901
|
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1086
902
|
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
903
|
+
if (!config.matchLockId) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
"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."
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
this.matchLockId = config.matchLockId;
|
|
1087
909
|
}
|
|
1088
910
|
start() {
|
|
1089
911
|
if (this.running) return;
|
|
@@ -1113,7 +935,8 @@ var BurnIndexer = class {
|
|
|
1113
935
|
return;
|
|
1114
936
|
}
|
|
1115
937
|
await this.processBlockRange(from, safeHead);
|
|
1116
|
-
} catch {
|
|
938
|
+
} catch (err) {
|
|
939
|
+
console.error("[PAFI] BurnIndexer tick error:", err);
|
|
1117
940
|
}
|
|
1118
941
|
this.scheduleNext();
|
|
1119
942
|
}
|
|
@@ -1158,10 +981,10 @@ var BurnIndexer = class {
|
|
|
1158
981
|
for (const log of logs) {
|
|
1159
982
|
const args = log.args;
|
|
1160
983
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1161
|
-
if ((0,
|
|
984
|
+
if ((0, import_viem5.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
|
|
1162
985
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1163
986
|
out.push({
|
|
1164
|
-
from: (0,
|
|
987
|
+
from: (0, import_viem5.getAddress)(args.from),
|
|
1165
988
|
amount: args.value,
|
|
1166
989
|
blockNumber: log.blockNumber,
|
|
1167
990
|
txHash: log.transactionHash,
|
|
@@ -1176,21 +999,28 @@ var BurnIndexer = class {
|
|
|
1176
999
|
* log + skip.
|
|
1177
1000
|
*/
|
|
1178
1001
|
async finalize(evt) {
|
|
1002
|
+
const txHash = evt.txHash;
|
|
1179
1003
|
const lockId = await this.matchLockId(evt);
|
|
1180
|
-
if (
|
|
1004
|
+
if (lockId === void 0) {
|
|
1005
|
+
console.warn(
|
|
1006
|
+
"[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
|
|
1007
|
+
);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1181
1010
|
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1182
1011
|
return;
|
|
1183
1012
|
}
|
|
1184
1013
|
try {
|
|
1185
1014
|
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1186
|
-
} catch {
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
|
|
1187
1017
|
}
|
|
1188
1018
|
}
|
|
1189
1019
|
};
|
|
1190
1020
|
|
|
1191
1021
|
// src/api/handlers.ts
|
|
1192
|
-
var
|
|
1193
|
-
var
|
|
1022
|
+
var import_viem6 = require("viem");
|
|
1023
|
+
var import_core3 = require("@pafi-dev/core");
|
|
1194
1024
|
var IssuerApiHandlers = class {
|
|
1195
1025
|
authService;
|
|
1196
1026
|
ledger;
|
|
@@ -1200,15 +1030,12 @@ var IssuerApiHandlers = class {
|
|
|
1200
1030
|
* validate the request's `pointTokenAddress` against this set.
|
|
1201
1031
|
*/
|
|
1202
1032
|
supportedTokens;
|
|
1203
|
-
/** First supported token — used as default when a handler doesn't
|
|
1204
|
-
* receive a `pointTokenAddress` in the request (shouldn't happen in
|
|
1205
|
-
* practice, but keeps type-narrowing happy). */
|
|
1206
|
-
defaultToken;
|
|
1207
1033
|
chainId;
|
|
1208
1034
|
contracts;
|
|
1209
1035
|
pafiWebUrl;
|
|
1210
1036
|
feeManager;
|
|
1211
1037
|
poolsProvider;
|
|
1038
|
+
claim;
|
|
1212
1039
|
constructor(config) {
|
|
1213
1040
|
this.authService = config.authService;
|
|
1214
1041
|
this.ledger = config.ledger;
|
|
@@ -1219,14 +1046,14 @@ var IssuerApiHandlers = class {
|
|
|
1219
1046
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1220
1047
|
);
|
|
1221
1048
|
}
|
|
1222
|
-
const normalized = raw.map((a) => (0,
|
|
1049
|
+
const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
|
|
1223
1050
|
this.supportedTokens = new Set(normalized);
|
|
1224
|
-
this.defaultToken = normalized[0];
|
|
1225
1051
|
this.chainId = config.chainId;
|
|
1226
1052
|
this.contracts = config.contracts;
|
|
1227
1053
|
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1228
1054
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1229
1055
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1056
|
+
if (config.claim) this.claim = config.claim;
|
|
1230
1057
|
}
|
|
1231
1058
|
// =========================================================================
|
|
1232
1059
|
// Public handlers (no auth required)
|
|
@@ -1241,6 +1068,12 @@ var IssuerApiHandlers = class {
|
|
|
1241
1068
|
if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
|
|
1242
1069
|
throw new Error("handleLogin: message and signature are required");
|
|
1243
1070
|
}
|
|
1071
|
+
if (body.message.length > 4096) {
|
|
1072
|
+
throw new Error("message too long");
|
|
1073
|
+
}
|
|
1074
|
+
if (body.signature.length > 260) {
|
|
1075
|
+
throw new Error("signature too long");
|
|
1076
|
+
}
|
|
1244
1077
|
const result = await this.authService.login(body.message, body.signature);
|
|
1245
1078
|
return {
|
|
1246
1079
|
token: result.token,
|
|
@@ -1255,9 +1088,12 @@ var IssuerApiHandlers = class {
|
|
|
1255
1088
|
* needs to build EIP-712 messages and interact with on-chain.
|
|
1256
1089
|
*/
|
|
1257
1090
|
async handleConfig(chainId) {
|
|
1091
|
+
if (!Number.isInteger(chainId) || chainId <= 0) {
|
|
1092
|
+
throw new Error("invalid chainId");
|
|
1093
|
+
}
|
|
1258
1094
|
if (chainId !== this.chainId) {
|
|
1259
1095
|
throw new Error(
|
|
1260
|
-
`handleConfig: unsupported chainId ${chainId}
|
|
1096
|
+
`handleConfig: unsupported chainId ${chainId}`
|
|
1261
1097
|
);
|
|
1262
1098
|
}
|
|
1263
1099
|
const contracts = {
|
|
@@ -1320,25 +1156,25 @@ var IssuerApiHandlers = class {
|
|
|
1320
1156
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1321
1157
|
);
|
|
1322
1158
|
}
|
|
1323
|
-
const normalizedAuthed = (0,
|
|
1324
|
-
const normalizedRequest = (0,
|
|
1159
|
+
const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
|
|
1160
|
+
const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
|
|
1325
1161
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1326
1162
|
throw new Error(
|
|
1327
1163
|
"handleUser: request userAddress must match authenticated user"
|
|
1328
1164
|
);
|
|
1329
1165
|
}
|
|
1330
|
-
const pointToken = (0,
|
|
1166
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1331
1167
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1332
1168
|
throw new Error(
|
|
1333
1169
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
1334
1170
|
);
|
|
1335
1171
|
}
|
|
1336
1172
|
const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
|
|
1337
|
-
(0,
|
|
1338
|
-
(0,
|
|
1173
|
+
(0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1174
|
+
(0, import_core3.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1339
1175
|
this.ledger.getBalance(normalizedAuthed, pointToken),
|
|
1340
|
-
(0,
|
|
1341
|
-
(0,
|
|
1176
|
+
(0, import_core3.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
|
|
1177
|
+
(0, import_core3.isMinter)(this.provider, pointToken, normalizedAuthed)
|
|
1342
1178
|
]);
|
|
1343
1179
|
return {
|
|
1344
1180
|
mintRequestNonce,
|
|
@@ -1370,19 +1206,32 @@ var IssuerApiHandlers = class {
|
|
|
1370
1206
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1371
1207
|
);
|
|
1372
1208
|
}
|
|
1373
|
-
const pointToken = (0,
|
|
1209
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1374
1210
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1375
1211
|
throw new Error(
|
|
1376
1212
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
1377
1213
|
);
|
|
1378
1214
|
}
|
|
1379
|
-
const
|
|
1215
|
+
const consent = request.receiverConsent;
|
|
1216
|
+
if ((0, import_viem6.getAddress)(consent.originalReceiver) !== (0, import_viem6.getAddress)(userAddress)) {
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
"handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
if (consent.amount <= 0n) {
|
|
1222
|
+
throw new Error("handleBuildConsentTypedData: amount must be positive");
|
|
1223
|
+
}
|
|
1224
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1225
|
+
if (consent.deadline <= nowSecs) {
|
|
1226
|
+
throw new Error("handleBuildConsentTypedData: deadline is in the past");
|
|
1227
|
+
}
|
|
1228
|
+
const name = await (0, import_core3.getTokenName)(this.provider, pointToken);
|
|
1380
1229
|
const domain = {
|
|
1381
1230
|
name,
|
|
1382
1231
|
verifyingContract: pointToken,
|
|
1383
1232
|
chainId: this.chainId
|
|
1384
1233
|
};
|
|
1385
|
-
const typedData = (0,
|
|
1234
|
+
const typedData = (0, import_core3.buildReceiverConsentTypedData)(domain, consent);
|
|
1386
1235
|
return {
|
|
1387
1236
|
typedData: {
|
|
1388
1237
|
domain: typedData.domain,
|
|
@@ -1392,11 +1241,96 @@ var IssuerApiHandlers = class {
|
|
|
1392
1241
|
}
|
|
1393
1242
|
};
|
|
1394
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* `POST /claim`
|
|
1246
|
+
*
|
|
1247
|
+
* Policy gate + ledger lock + MintRequest signing in a single atomic
|
|
1248
|
+
* step. Returns an unsigned UserOp the frontend attaches paymaster data
|
|
1249
|
+
* to and submits via EIP-7702 + Bundler.
|
|
1250
|
+
*
|
|
1251
|
+
* Order of operations:
|
|
1252
|
+
* 1. Validate request fields.
|
|
1253
|
+
* 2. policy.evaluate() — throws if denied; cannot be bypassed.
|
|
1254
|
+
* 3. ledger.lockForMinting() — reserves the balance.
|
|
1255
|
+
* 4. Read on-chain mintRequestNonce + token name in parallel.
|
|
1256
|
+
* 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
|
|
1257
|
+
* 6. On any error after step 3, release the lock before re-throwing.
|
|
1258
|
+
*/
|
|
1259
|
+
async handleClaim(userAddress, request) {
|
|
1260
|
+
if (!this.claim) {
|
|
1261
|
+
throw new Error("handleClaim: claim is not configured on this issuer");
|
|
1262
|
+
}
|
|
1263
|
+
if (request.chainId !== this.chainId) {
|
|
1264
|
+
throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
|
|
1265
|
+
}
|
|
1266
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1267
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1268
|
+
throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
|
|
1269
|
+
}
|
|
1270
|
+
if (request.amount <= 0n) {
|
|
1271
|
+
throw new Error("handleClaim: amount must be positive");
|
|
1272
|
+
}
|
|
1273
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1274
|
+
if (request.deadline <= nowSecs) {
|
|
1275
|
+
throw new Error("handleClaim: deadline is in the past");
|
|
1276
|
+
}
|
|
1277
|
+
const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
|
|
1278
|
+
const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
|
|
1279
|
+
const normalizedUser = (0, import_viem6.getAddress)(userAddress);
|
|
1280
|
+
const decision = await policy.evaluate({
|
|
1281
|
+
userAddress: normalizedUser,
|
|
1282
|
+
amount: request.amount,
|
|
1283
|
+
pointTokenAddress: pointToken,
|
|
1284
|
+
chainId: this.chainId
|
|
1285
|
+
});
|
|
1286
|
+
if (!decision.approved) {
|
|
1287
|
+
throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
|
|
1288
|
+
}
|
|
1289
|
+
const lockId = await this.ledger.lockForMinting(
|
|
1290
|
+
normalizedUser,
|
|
1291
|
+
request.amount,
|
|
1292
|
+
lockDurationMs,
|
|
1293
|
+
pointToken
|
|
1294
|
+
);
|
|
1295
|
+
try {
|
|
1296
|
+
const [mintRequestNonce, tokenName] = await Promise.all([
|
|
1297
|
+
(0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedUser),
|
|
1298
|
+
(0, import_core3.getTokenName)(this.provider, pointToken)
|
|
1299
|
+
]);
|
|
1300
|
+
const domain = {
|
|
1301
|
+
name: tokenName,
|
|
1302
|
+
verifyingContract: pointToken,
|
|
1303
|
+
chainId: this.chainId
|
|
1304
|
+
};
|
|
1305
|
+
const userOp = await relayService.prepareMint({
|
|
1306
|
+
userAddress: normalizedUser,
|
|
1307
|
+
aaNonce: request.aaNonce,
|
|
1308
|
+
batchExecutorAddress,
|
|
1309
|
+
pointTokenAddress: pointToken,
|
|
1310
|
+
amount: request.amount,
|
|
1311
|
+
issuerSignerWallet,
|
|
1312
|
+
domain,
|
|
1313
|
+
mintRequestNonce,
|
|
1314
|
+
deadline: request.deadline,
|
|
1315
|
+
feeAmount: request.feeAmount,
|
|
1316
|
+
feeRecipient: request.feeRecipient
|
|
1317
|
+
});
|
|
1318
|
+
return {
|
|
1319
|
+
lockId,
|
|
1320
|
+
userOp,
|
|
1321
|
+
expiresInSeconds: Math.floor(lockDurationMs / 1e3)
|
|
1322
|
+
};
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
await this.ledger.releaseLock(lockId).catch(() => {
|
|
1325
|
+
});
|
|
1326
|
+
throw err;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1395
1329
|
};
|
|
1396
1330
|
|
|
1397
1331
|
// src/api/handlers/ptRedeemHandler.ts
|
|
1398
|
-
var
|
|
1399
|
-
var
|
|
1332
|
+
var import_viem7 = require("viem");
|
|
1333
|
+
var import_core4 = require("@pafi-dev/core");
|
|
1400
1334
|
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1401
1335
|
var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
|
|
1402
1336
|
var PTRedeemError = class extends Error {
|
|
@@ -1435,16 +1369,25 @@ var PTRedeemHandler = class {
|
|
|
1435
1369
|
this.ledger = config.ledger;
|
|
1436
1370
|
this.relayService = config.relayService;
|
|
1437
1371
|
this.provider = config.provider;
|
|
1438
|
-
this.pointTokenAddress = (0,
|
|
1439
|
-
this.batchExecutorAddress = (0,
|
|
1372
|
+
this.pointTokenAddress = (0, import_viem7.getAddress)(config.pointTokenAddress);
|
|
1373
|
+
this.batchExecutorAddress = (0, import_viem7.getAddress)(config.batchExecutorAddress);
|
|
1440
1374
|
this.chainId = config.chainId;
|
|
1441
1375
|
this.domain = config.domain;
|
|
1442
1376
|
this.burnerSignerWallet = config.burnerSignerWallet;
|
|
1377
|
+
if (this.burnerSignerWallet?.account?.type === "local") {
|
|
1378
|
+
console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
|
|
1379
|
+
}
|
|
1443
1380
|
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1444
1381
|
this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
|
|
1445
1382
|
this.now = config.now ?? (() => Date.now());
|
|
1446
1383
|
}
|
|
1447
1384
|
async handle(request) {
|
|
1385
|
+
if ((0, import_viem7.getAddress)(request.authenticatedAddress) !== (0, import_viem7.getAddress)(request.userAddress)) {
|
|
1386
|
+
throw new PTRedeemError(
|
|
1387
|
+
"UNAUTHORIZED",
|
|
1388
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1448
1391
|
if (request.amount <= 0n) {
|
|
1449
1392
|
throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
|
|
1450
1393
|
}
|
|
@@ -1452,7 +1395,7 @@ var PTRedeemHandler = class {
|
|
|
1452
1395
|
try {
|
|
1453
1396
|
burnNonce = await this.provider.readContract({
|
|
1454
1397
|
address: this.pointTokenAddress,
|
|
1455
|
-
abi:
|
|
1398
|
+
abi: import_core4.POINT_TOKEN_V2_ABI,
|
|
1456
1399
|
functionName: "burnRequestNonces",
|
|
1457
1400
|
args: [request.userAddress]
|
|
1458
1401
|
});
|
|
@@ -1462,6 +1405,17 @@ var PTRedeemHandler = class {
|
|
|
1462
1405
|
`failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
|
|
1463
1406
|
);
|
|
1464
1407
|
}
|
|
1408
|
+
const onChainBalance = await (0, import_core4.getPointTokenBalance)(
|
|
1409
|
+
this.provider,
|
|
1410
|
+
this.pointTokenAddress,
|
|
1411
|
+
request.userAddress
|
|
1412
|
+
);
|
|
1413
|
+
if (onChainBalance < request.amount) {
|
|
1414
|
+
throw new PTRedeemError(
|
|
1415
|
+
"INVALID_AMOUNT",
|
|
1416
|
+
`insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1465
1419
|
const deadline = BigInt(
|
|
1466
1420
|
Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
|
|
1467
1421
|
);
|
|
@@ -1478,7 +1432,7 @@ var PTRedeemHandler = class {
|
|
|
1478
1432
|
};
|
|
1479
1433
|
let burnerSignature;
|
|
1480
1434
|
try {
|
|
1481
|
-
const sig = await (0,
|
|
1435
|
+
const sig = await (0, import_core4.signBurnRequest)(
|
|
1482
1436
|
this.burnerSignerWallet,
|
|
1483
1437
|
domain,
|
|
1484
1438
|
burnRequest
|
|
@@ -1515,8 +1469,8 @@ var PTRedeemHandler = class {
|
|
|
1515
1469
|
};
|
|
1516
1470
|
|
|
1517
1471
|
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1518
|
-
var
|
|
1519
|
-
var
|
|
1472
|
+
var import_viem8 = require("viem");
|
|
1473
|
+
var import_core5 = require("@pafi-dev/core");
|
|
1520
1474
|
var TopUpRedemptionError = class extends Error {
|
|
1521
1475
|
constructor(code, message) {
|
|
1522
1476
|
super(message);
|
|
@@ -1534,9 +1488,15 @@ var TopUpRedemptionHandler = class {
|
|
|
1534
1488
|
this.ledger = config.ledger;
|
|
1535
1489
|
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1536
1490
|
this.provider = config.provider;
|
|
1537
|
-
this.pointTokenAddress = (0,
|
|
1491
|
+
this.pointTokenAddress = (0, import_viem8.getAddress)(config.pointTokenAddress);
|
|
1538
1492
|
}
|
|
1539
1493
|
async handle(request) {
|
|
1494
|
+
if ((0, import_viem8.getAddress)(request.authenticatedAddress) !== (0, import_viem8.getAddress)(request.userAddress)) {
|
|
1495
|
+
throw new TopUpRedemptionError(
|
|
1496
|
+
"UNAUTHORIZED",
|
|
1497
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1540
1500
|
const offChainBalance = await this.ledger.getBalance(
|
|
1541
1501
|
request.userAddress,
|
|
1542
1502
|
this.pointTokenAddress
|
|
@@ -1545,7 +1505,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1545
1505
|
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1546
1506
|
}
|
|
1547
1507
|
const shortfall = request.requiredAmount - offChainBalance;
|
|
1548
|
-
const onChainBalance = await (0,
|
|
1508
|
+
const onChainBalance = await (0, import_core5.getPointTokenBalance)(
|
|
1549
1509
|
this.provider,
|
|
1550
1510
|
this.pointTokenAddress,
|
|
1551
1511
|
request.userAddress
|
|
@@ -1559,6 +1519,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1559
1519
|
};
|
|
1560
1520
|
}
|
|
1561
1521
|
const redeem = await this.ptRedeemHandler.handle({
|
|
1522
|
+
authenticatedAddress: request.authenticatedAddress,
|
|
1562
1523
|
userAddress: request.userAddress,
|
|
1563
1524
|
amount: shortfall,
|
|
1564
1525
|
aaNonce: request.aaNonce
|
|
@@ -1572,6 +1533,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1572
1533
|
};
|
|
1573
1534
|
|
|
1574
1535
|
// src/pools/subgraphPoolsProvider.ts
|
|
1536
|
+
var import_viem9 = require("viem");
|
|
1575
1537
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1576
1538
|
var POOL_QUERY = `
|
|
1577
1539
|
query GetPoolForPointToken($id: ID!) {
|
|
@@ -1594,6 +1556,19 @@ function createSubgraphPoolsProvider(config) {
|
|
|
1594
1556
|
"createSubgraphPoolsProvider: subgraphUrl is required"
|
|
1595
1557
|
);
|
|
1596
1558
|
}
|
|
1559
|
+
try {
|
|
1560
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1561
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1562
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1563
|
+
}
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
if (err instanceof TypeError) {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
throw err;
|
|
1571
|
+
}
|
|
1597
1572
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
1598
1573
|
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1599
1574
|
const now = config.now ?? (() => Date.now());
|
|
@@ -1662,6 +1637,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
|
|
|
1662
1637
|
return [];
|
|
1663
1638
|
}
|
|
1664
1639
|
const { pool } = token;
|
|
1640
|
+
if (!(0, import_viem9.isAddress)(pool.hooks)) {
|
|
1641
|
+
console.error(
|
|
1642
|
+
"[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
|
|
1643
|
+
pool.hooks,
|
|
1644
|
+
"\u2014 skipping pool"
|
|
1645
|
+
);
|
|
1646
|
+
return [];
|
|
1647
|
+
}
|
|
1648
|
+
if (!(0, import_viem9.isAddress)(pool.token0.id) || !(0, import_viem9.isAddress)(pool.token1.id)) {
|
|
1649
|
+
console.error(
|
|
1650
|
+
"[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
|
|
1651
|
+
);
|
|
1652
|
+
return [];
|
|
1653
|
+
}
|
|
1654
|
+
if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
|
|
1655
|
+
console.error(
|
|
1656
|
+
"[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
|
|
1657
|
+
);
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1665
1660
|
const [currency0, currency1] = sortCurrencies(
|
|
1666
1661
|
pool.token0.id,
|
|
1667
1662
|
pool.token1.id
|
|
@@ -1697,6 +1692,19 @@ function createSubgraphNativeUsdtQuoter(config) {
|
|
|
1697
1692
|
"createSubgraphNativeUsdtQuoter: subgraphUrl is required"
|
|
1698
1693
|
);
|
|
1699
1694
|
}
|
|
1695
|
+
try {
|
|
1696
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1697
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1698
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1699
|
+
}
|
|
1700
|
+
} catch (err) {
|
|
1701
|
+
if (err instanceof TypeError) {
|
|
1702
|
+
throw new Error(
|
|
1703
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
throw err;
|
|
1707
|
+
}
|
|
1700
1708
|
const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
|
|
1701
1709
|
const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
|
|
1702
1710
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
|
|
@@ -1773,6 +1781,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
|
|
|
1773
1781
|
);
|
|
1774
1782
|
return null;
|
|
1775
1783
|
}
|
|
1784
|
+
const MIN_REASONABLE_ETH_PRICE = 100;
|
|
1785
|
+
const MAX_REASONABLE_ETH_PRICE = 1e5;
|
|
1786
|
+
if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
|
|
1787
|
+
console.warn(
|
|
1788
|
+
`[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
|
|
1789
|
+
);
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1776
1792
|
return parsed;
|
|
1777
1793
|
}
|
|
1778
1794
|
function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
@@ -1783,7 +1799,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1783
1799
|
}
|
|
1784
1800
|
|
|
1785
1801
|
// src/balance/balanceAggregator.ts
|
|
1786
|
-
var
|
|
1802
|
+
var import_core6 = require("@pafi-dev/core");
|
|
1787
1803
|
var BalanceAggregator = class {
|
|
1788
1804
|
provider;
|
|
1789
1805
|
ledger;
|
|
@@ -1804,7 +1820,7 @@ var BalanceAggregator = class {
|
|
|
1804
1820
|
async getCombinedBalance(user, pointToken) {
|
|
1805
1821
|
const [offChain, onChain] = await Promise.all([
|
|
1806
1822
|
this.ledger.getBalance(user, pointToken),
|
|
1807
|
-
(0,
|
|
1823
|
+
(0, import_core6.getPointTokenBalance)(this.provider, pointToken, user)
|
|
1808
1824
|
]);
|
|
1809
1825
|
return {
|
|
1810
1826
|
offChain,
|
|
@@ -1842,37 +1858,16 @@ var PafiBackendError = class extends Error {
|
|
|
1842
1858
|
code;
|
|
1843
1859
|
httpStatus;
|
|
1844
1860
|
details;
|
|
1845
|
-
/**
|
|
1846
|
-
* Seconds to wait before retry. Populated from the server body
|
|
1847
|
-
* (e.g. rate limit returns the number of seconds until UTC midnight).
|
|
1848
|
-
*/
|
|
1849
1861
|
retryAfter;
|
|
1850
|
-
/**
|
|
1851
|
-
* `safeToRetry` as reported by the server body. Prefer this over the
|
|
1852
|
-
* code-based heuristic when available — the server knows more about
|
|
1853
|
-
* whether the same request will succeed on retry.
|
|
1854
|
-
*/
|
|
1855
1862
|
serverSafeToRetry;
|
|
1856
|
-
/**
|
|
1857
|
-
* Whether the caller can safely retry the same request.
|
|
1858
|
-
*
|
|
1859
|
-
* If the server provided `safeToRetry` in the body, trust that.
|
|
1860
|
-
* Otherwise fall back to a code-based heuristic.
|
|
1861
|
-
*/
|
|
1862
1863
|
get safeToRetry() {
|
|
1863
1864
|
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
1864
1865
|
switch (this.code) {
|
|
1865
|
-
// Transient infra
|
|
1866
|
-
case "PAYMASTER_UNAVAILABLE":
|
|
1867
|
-
case "PAYMASTER_TIMEOUT":
|
|
1868
1866
|
case "RATE_LIMITER_UNAVAILABLE":
|
|
1869
|
-
case "KMS_UNAVAILABLE":
|
|
1870
|
-
case "SPONSOR_AUTH_SIGNING_FAILED":
|
|
1871
1867
|
case "INTERNAL_ERROR":
|
|
1872
1868
|
case "TIMEOUT":
|
|
1873
1869
|
case "NETWORK_ERROR":
|
|
1874
1870
|
return true;
|
|
1875
|
-
// Rate-limited — safe to retry after retryAfter window
|
|
1876
1871
|
case "RATE_LIMIT_EXCEEDED":
|
|
1877
1872
|
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
1878
1873
|
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
@@ -1884,201 +1879,104 @@ var PafiBackendError = class extends Error {
|
|
|
1884
1879
|
}
|
|
1885
1880
|
};
|
|
1886
1881
|
|
|
1887
|
-
// src/pafi-backend/
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
};
|
|
1882
|
+
// src/pafi-backend/client.ts
|
|
1883
|
+
function serializeBigInt(_key, value) {
|
|
1884
|
+
return typeof value === "bigint" ? value.toString(10) : value;
|
|
1885
|
+
}
|
|
1886
|
+
function sleep(ms) {
|
|
1887
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1888
|
+
}
|
|
1895
1889
|
var PafiBackendClient = class {
|
|
1896
|
-
|
|
1897
|
-
issuerId;
|
|
1898
|
-
apiKey;
|
|
1899
|
-
fetchImpl;
|
|
1900
|
-
timeoutMs;
|
|
1901
|
-
retry;
|
|
1890
|
+
config;
|
|
1902
1891
|
constructor(config) {
|
|
1903
|
-
if (!config.url)
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
this.url = config.url.replace(/\/+$/, "");
|
|
1913
|
-
this.issuerId = config.issuerId;
|
|
1914
|
-
this.apiKey = config.apiKey;
|
|
1915
|
-
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1916
|
-
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1917
|
-
this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
|
|
1918
|
-
if (!this.fetchImpl) {
|
|
1919
|
-
throw new Error(
|
|
1920
|
-
"PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
1923
|
-
if (this.retry.maxAttempts < 1) {
|
|
1924
|
-
throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
/**
|
|
1928
|
-
* Request a SponsorAuth signature from PAFI sponsor-relayer (beta.8+).
|
|
1929
|
-
*
|
|
1930
|
-
* The relayer:
|
|
1931
|
-
* 1. Authenticates user (JWT) + issuer (API key)
|
|
1932
|
-
* 2. Per-(user, scenario) rate limit + per-issuer daily budget
|
|
1933
|
-
* 3. Scenario-specific intent validation (mint cap, KYC, etc.)
|
|
1934
|
-
* 4. Allocates nonce + signs SponsorAuth payload via KMS PAFI key
|
|
1935
|
-
* 5. Returns `{ sponsorAuth, payload }` for the FE to forward to
|
|
1936
|
-
* Privy's `signUserOperation({ sponsorAuth, payload })`.
|
|
1937
|
-
*
|
|
1938
|
-
* Retries on transient failures (5xx, timeouts, KMS unavailable,
|
|
1939
|
-
* rate-limit-with-retryAfter). 4xx that are not `safeToRetry` fail fast.
|
|
1940
|
-
*
|
|
1941
|
-
* See `pafi-backend/docs/SPONSOR_AUTH_DESIGN.md` for the full spec.
|
|
1942
|
-
*
|
|
1943
|
-
* @throws PafiBackendError on final failure after exhausting retries
|
|
1944
|
-
*/
|
|
1945
|
-
async requestSponsorAuth(req) {
|
|
1946
|
-
return this.postWithRetry(
|
|
1947
|
-
"/sponsor-auth",
|
|
1948
|
-
req
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1951
|
-
/**
|
|
1952
|
-
* @deprecated Coinbase paymaster path — replaced by `requestSponsorAuth`
|
|
1953
|
-
* in beta.8. Will be removed in 1.0. Migrate by:
|
|
1954
|
-
* 1. Switch to `requestSponsorAuth` returning `{ sponsorAuth, payload }`
|
|
1955
|
-
* 2. Pass both to Privy `signUserOperation` instead of merging
|
|
1956
|
-
* paymasterData into the UserOp callData
|
|
1957
|
-
*/
|
|
1958
|
-
async requestSponsorship(req) {
|
|
1959
|
-
return this.postWithRetry(
|
|
1960
|
-
"/paymaster/sponsor",
|
|
1961
|
-
req
|
|
1962
|
-
);
|
|
1963
|
-
}
|
|
1964
|
-
// -------------------------------------------------------------------------
|
|
1965
|
-
// Internals
|
|
1966
|
-
// -------------------------------------------------------------------------
|
|
1967
|
-
async postWithRetry(path, body) {
|
|
1892
|
+
if (!config.url) throw new Error("PafiBackendClient: url is required");
|
|
1893
|
+
if (!config.issuerId) throw new Error("PafiBackendClient: issuerId is required");
|
|
1894
|
+
this.config = config;
|
|
1895
|
+
}
|
|
1896
|
+
async requestSponsorship(request) {
|
|
1897
|
+
const maxAttempts = this.config.retry?.maxAttempts ?? 1;
|
|
1898
|
+
const initialDelayMs = this.config.retry?.initialDelayMs ?? 100;
|
|
1899
|
+
const maxDelayMs = this.config.retry?.maxDelayMs ?? 1e4;
|
|
1900
|
+
const maxRetryAfterMs = this.config.retry?.maxRetryAfterMs;
|
|
1968
1901
|
let lastError;
|
|
1969
|
-
|
|
1902
|
+
let delay = initialDelayMs;
|
|
1903
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1970
1904
|
try {
|
|
1971
|
-
return await this.
|
|
1905
|
+
return await this._doRequest(request);
|
|
1972
1906
|
} catch (err) {
|
|
1973
1907
|
if (!(err instanceof PafiBackendError)) throw err;
|
|
1974
1908
|
lastError = err;
|
|
1975
|
-
|
|
1976
|
-
if (
|
|
1977
|
-
const
|
|
1978
|
-
if (
|
|
1979
|
-
|
|
1909
|
+
if (attempt >= maxAttempts) break;
|
|
1910
|
+
if (!err.safeToRetry) break;
|
|
1911
|
+
const retryAfterMs = err.retryAfter !== void 0 ? err.retryAfter * 1e3 : void 0;
|
|
1912
|
+
if (maxRetryAfterMs !== void 0 && retryAfterMs !== void 0 && retryAfterMs > maxRetryAfterMs) {
|
|
1913
|
+
break;
|
|
1914
|
+
}
|
|
1915
|
+
await sleep(retryAfterMs ?? delay);
|
|
1916
|
+
delay = Math.min(delay * 2, maxDelayMs);
|
|
1980
1917
|
}
|
|
1981
1918
|
}
|
|
1982
1919
|
throw lastError;
|
|
1983
1920
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
* cap, signalling the caller should give up.
|
|
1989
|
-
* - Otherwise: exponential backoff with ±20% jitter, capped at
|
|
1990
|
-
* `maxDelayMs`.
|
|
1991
|
-
*/
|
|
1992
|
-
computeBackoff(attempt, retryAfter) {
|
|
1993
|
-
if (retryAfter !== void 0) {
|
|
1994
|
-
const serverMs = retryAfter * 1e3;
|
|
1995
|
-
if (serverMs > this.retry.maxRetryAfterMs) return null;
|
|
1996
|
-
return serverMs;
|
|
1997
|
-
}
|
|
1998
|
-
const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
|
|
1999
|
-
const capped = Math.min(exp, this.retry.maxDelayMs);
|
|
2000
|
-
const jitter = capped * (0.8 + Math.random() * 0.4);
|
|
2001
|
-
return Math.round(jitter);
|
|
2002
|
-
}
|
|
2003
|
-
sleep(ms) {
|
|
2004
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2005
|
-
}
|
|
2006
|
-
async post(path, body) {
|
|
2007
|
-
const controller = new AbortController();
|
|
2008
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1921
|
+
async _doRequest(request) {
|
|
1922
|
+
const fetchFn = this.config.fetchImpl ?? fetch;
|
|
1923
|
+
const url = `${this.config.url}/paymaster/sponsor`;
|
|
1924
|
+
const body = JSON.stringify(request, serializeBigInt);
|
|
2009
1925
|
let response;
|
|
2010
1926
|
try {
|
|
2011
|
-
response = await
|
|
1927
|
+
response = await fetchFn(url, {
|
|
2012
1928
|
method: "POST",
|
|
2013
1929
|
headers: {
|
|
2014
1930
|
"Content-Type": "application/json",
|
|
2015
|
-
|
|
2016
|
-
"X-Issuer-Id": this.issuerId
|
|
1931
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
1932
|
+
"X-Issuer-Id": this.config.issuerId
|
|
2017
1933
|
},
|
|
2018
|
-
body
|
|
2019
|
-
signal: controller.signal
|
|
1934
|
+
body
|
|
2020
1935
|
});
|
|
2021
1936
|
} catch (err) {
|
|
2022
|
-
if (err.name === "AbortError") {
|
|
2023
|
-
throw new PafiBackendError(
|
|
2024
|
-
"TIMEOUT",
|
|
2025
|
-
`PAFI Backend request timed out after ${this.timeoutMs}ms`,
|
|
2026
|
-
0
|
|
2027
|
-
);
|
|
2028
|
-
}
|
|
2029
1937
|
throw new PafiBackendError(
|
|
2030
1938
|
"NETWORK_ERROR",
|
|
2031
|
-
`
|
|
1939
|
+
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
2032
1940
|
0
|
|
2033
1941
|
);
|
|
2034
|
-
} finally {
|
|
2035
|
-
clearTimeout(timeoutId);
|
|
2036
1942
|
}
|
|
2037
1943
|
const text = await response.text();
|
|
1944
|
+
let json = {};
|
|
1945
|
+
try {
|
|
1946
|
+
json = JSON.parse(text);
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
2038
1949
|
if (!response.ok) {
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
code = parsed.code ?? code;
|
|
2047
|
-
message = parsed.message ?? message;
|
|
2048
|
-
details = parsed.details;
|
|
2049
|
-
if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
|
|
2050
|
-
if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
|
|
2051
|
-
} catch {
|
|
2052
|
-
}
|
|
2053
|
-
throw new PafiBackendError(code, message, response.status, details, {
|
|
2054
|
-
...retryAfter !== void 0 ? { retryAfter } : {},
|
|
2055
|
-
...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
|
|
1950
|
+
const code = json.code ?? "INTERNAL_ERROR";
|
|
1951
|
+
const message = json.message ?? `HTTP ${response.status}`;
|
|
1952
|
+
const retryAfter = typeof json.retryAfter === "number" ? json.retryAfter : void 0;
|
|
1953
|
+
const safeToRetry = typeof json.safeToRetry === "boolean" ? json.safeToRetry : void 0;
|
|
1954
|
+
throw new PafiBackendError(code, message, response.status, json, {
|
|
1955
|
+
retryAfter,
|
|
1956
|
+
safeToRetry
|
|
2056
1957
|
});
|
|
2057
1958
|
}
|
|
2058
|
-
return
|
|
1959
|
+
return {
|
|
1960
|
+
paymaster: json.paymaster,
|
|
1961
|
+
paymasterData: json.paymasterData,
|
|
1962
|
+
paymasterVerificationGasLimit: BigInt(
|
|
1963
|
+
json.paymasterVerificationGasLimit
|
|
1964
|
+
),
|
|
1965
|
+
paymasterPostOpGasLimit: BigInt(json.paymasterPostOpGasLimit),
|
|
1966
|
+
expiresAt: json.expiresAt
|
|
1967
|
+
};
|
|
2059
1968
|
}
|
|
2060
|
-
/** JSON replacer that stringifies bigints. Paired with bigintReviver. */
|
|
2061
|
-
bigintReplacer = (_key, value) => {
|
|
2062
|
-
return typeof value === "bigint" ? value.toString() : value;
|
|
2063
|
-
};
|
|
2064
|
-
/**
|
|
2065
|
-
* JSON reviver that coerces specific numeric-string fields back to
|
|
2066
|
-
* bigint. The server must send these fields as decimal strings.
|
|
2067
|
-
*/
|
|
2068
|
-
bigintReviver = (key, value) => {
|
|
2069
|
-
if (typeof value === "string" && (key.endsWith("GasLimit") || key === "nonce" || key === "callGasLimit" || key === "verificationGasLimit" || key === "preVerificationGas" || key === "maxFeePerGas" || key === "maxPriorityFeePerGas" || key === "paymasterVerificationGasLimit" || key === "paymasterPostOpGasLimit") && /^\d+$/.test(value)) {
|
|
2070
|
-
return BigInt(value);
|
|
2071
|
-
}
|
|
2072
|
-
return value;
|
|
2073
|
-
};
|
|
2074
1969
|
};
|
|
2075
1970
|
|
|
2076
1971
|
// src/config.ts
|
|
2077
|
-
var
|
|
1972
|
+
var import_viem10 = require("viem");
|
|
2078
1973
|
function createIssuerService(config) {
|
|
2079
1974
|
if (!config.provider) {
|
|
2080
1975
|
throw new Error("createIssuerService: provider is required");
|
|
2081
1976
|
}
|
|
1977
|
+
if (!config.ledger) {
|
|
1978
|
+
throw new Error("createIssuerService: ledger is required");
|
|
1979
|
+
}
|
|
2082
1980
|
if (!config.auth?.jwtSecret) {
|
|
2083
1981
|
throw new Error("createIssuerService: auth.jwtSecret is required");
|
|
2084
1982
|
}
|
|
@@ -2091,8 +1989,8 @@ function createIssuerService(config) {
|
|
|
2091
1989
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
2092
1990
|
);
|
|
2093
1991
|
}
|
|
2094
|
-
const tokenAddresses = rawAddresses.map((a) => (0,
|
|
2095
|
-
const ledger = config.ledger
|
|
1992
|
+
const tokenAddresses = rawAddresses.map((a) => (0, import_viem10.getAddress)(a));
|
|
1993
|
+
const ledger = config.ledger;
|
|
2096
1994
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
2097
1995
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
2098
1996
|
const authServiceConfig = {
|
|
@@ -2148,6 +2046,15 @@ function createIssuerService(config) {
|
|
|
2148
2046
|
};
|
|
2149
2047
|
if (feeManager) handlersConfig.feeManager = feeManager;
|
|
2150
2048
|
if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
|
|
2049
|
+
if (config.claim) {
|
|
2050
|
+
handlersConfig.claim = {
|
|
2051
|
+
policy,
|
|
2052
|
+
relayService,
|
|
2053
|
+
issuerSignerWallet: config.claim.issuerSignerWallet,
|
|
2054
|
+
batchExecutorAddress: config.claim.batchExecutorAddress,
|
|
2055
|
+
lockDurationMs: config.claim.lockDurationMs
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2151
2058
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
2152
2059
|
if (config.indexer?.autoStart) {
|
|
2153
2060
|
for (const idx of indexers.values()) {
|
|
@@ -2179,7 +2086,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
2179
2086
|
FeeManager,
|
|
2180
2087
|
InMemoryCursorStore,
|
|
2181
2088
|
IssuerApiHandlers,
|
|
2182
|
-
MemoryPointLedger,
|
|
2183
2089
|
MemorySessionStore,
|
|
2184
2090
|
NonceManager,
|
|
2185
2091
|
PAFI_ISSUER_SDK_VERSION,
|
|
@@ -2188,7 +2094,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
2188
2094
|
PafiBackendClient,
|
|
2189
2095
|
PafiBackendError,
|
|
2190
2096
|
PointIndexer,
|
|
2191
|
-
PrivateKeySigner,
|
|
2192
2097
|
RelayError,
|
|
2193
2098
|
RelayService,
|
|
2194
2099
|
TopUpRedemptionError,
|