@pafi-dev/issuer 0.3.0-beta.1 → 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 +437 -1029
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +234 -616
- package/dist/index.d.ts +234 -616
- package/dist/index.js +421 -1013
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -28,18 +28,13 @@ __export(index_exports, {
|
|
|
28
28
|
FeeManager: () => FeeManager,
|
|
29
29
|
InMemoryCursorStore: () => InMemoryCursorStore,
|
|
30
30
|
IssuerApiHandlers: () => IssuerApiHandlers,
|
|
31
|
-
MemoryPointLedger: () => MemoryPointLedger,
|
|
32
31
|
MemorySessionStore: () => MemorySessionStore,
|
|
33
|
-
MintingGateway: () => MintingGateway,
|
|
34
|
-
MintingGatewayError: () => MintingGatewayError,
|
|
35
32
|
NonceManager: () => NonceManager,
|
|
36
33
|
PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
|
|
37
34
|
PTRedeemError: () => PTRedeemError,
|
|
38
35
|
PTRedeemHandler: () => PTRedeemHandler,
|
|
39
|
-
PafiBackendClient: () => PafiBackendClient,
|
|
40
36
|
PafiBackendError: () => PafiBackendError,
|
|
41
37
|
PointIndexer: () => PointIndexer,
|
|
42
|
-
PrivateKeySigner: () => PrivateKeySigner,
|
|
43
38
|
RelayError: () => RelayError,
|
|
44
39
|
RelayService: () => RelayService,
|
|
45
40
|
TopUpRedemptionError: () => TopUpRedemptionError,
|
|
@@ -47,209 +42,10 @@ __export(index_exports, {
|
|
|
47
42
|
authenticateRequest: () => authenticateRequest,
|
|
48
43
|
createIssuerService: () => createIssuerService,
|
|
49
44
|
createSubgraphNativeUsdtQuoter: () => createSubgraphNativeUsdtQuoter,
|
|
50
|
-
createSubgraphPoolsProvider: () => createSubgraphPoolsProvider
|
|
51
|
-
encodeExtData: () => import_core4.encodeExtData
|
|
45
|
+
createSubgraphPoolsProvider: () => createSubgraphPoolsProvider
|
|
52
46
|
});
|
|
53
47
|
module.exports = __toCommonJS(index_exports);
|
|
54
48
|
|
|
55
|
-
// src/ledger/memoryLedger.ts
|
|
56
|
-
var import_viem = require("viem");
|
|
57
|
-
var MemoryPointLedger = class {
|
|
58
|
-
balances = /* @__PURE__ */ new Map();
|
|
59
|
-
locks = /* @__PURE__ */ new Map();
|
|
60
|
-
nextLockId = 1;
|
|
61
|
-
now;
|
|
62
|
-
constructor(opts = {}) {
|
|
63
|
-
this.now = opts.now ?? (() => Date.now());
|
|
64
|
-
}
|
|
65
|
-
// -------------------------------------------------------------------------
|
|
66
|
-
// Read
|
|
67
|
-
// -------------------------------------------------------------------------
|
|
68
|
-
async getBalance(userAddress, tokenAddress) {
|
|
69
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
70
|
-
const token = normalizeToken(tokenAddress);
|
|
71
|
-
this.purgeExpired();
|
|
72
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
73
|
-
const locked = this.lockedTotalFor(user, token);
|
|
74
|
-
return total - locked;
|
|
75
|
-
}
|
|
76
|
-
async getLockedRequests(userAddress, tokenAddress) {
|
|
77
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
78
|
-
const token = normalizeToken(tokenAddress);
|
|
79
|
-
this.purgeExpired();
|
|
80
|
-
const out = [];
|
|
81
|
-
for (const lock of this.locks.values()) {
|
|
82
|
-
if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
83
|
-
out.push({ ...lock });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
// -------------------------------------------------------------------------
|
|
89
|
-
// Write
|
|
90
|
-
// -------------------------------------------------------------------------
|
|
91
|
-
async creditBalance(userAddress, amount, _reason, tokenAddress) {
|
|
92
|
-
if (amount <= 0n) {
|
|
93
|
-
throw new Error("MemoryPointLedger: credit amount must be positive");
|
|
94
|
-
}
|
|
95
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
96
|
-
const token = normalizeToken(tokenAddress);
|
|
97
|
-
const key = balanceKey(user, token);
|
|
98
|
-
const current = this.balances.get(key) ?? 0n;
|
|
99
|
-
this.balances.set(key, current + amount);
|
|
100
|
-
}
|
|
101
|
-
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
102
|
-
if (amount <= 0n) {
|
|
103
|
-
throw new Error("MemoryPointLedger: lock amount must be positive");
|
|
104
|
-
}
|
|
105
|
-
if (lockDurationMs <= 0) {
|
|
106
|
-
throw new Error("MemoryPointLedger: lockDurationMs must be positive");
|
|
107
|
-
}
|
|
108
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
109
|
-
const token = normalizeToken(tokenAddress);
|
|
110
|
-
this.purgeExpired();
|
|
111
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
112
|
-
const alreadyLocked = this.lockedTotalFor(user, token);
|
|
113
|
-
const available = total - alreadyLocked;
|
|
114
|
-
if (available < amount) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
const lockId = `lock-${this.nextLockId++}`;
|
|
120
|
-
const now = this.now();
|
|
121
|
-
const lock = {
|
|
122
|
-
lockId,
|
|
123
|
-
userAddress: user,
|
|
124
|
-
amount,
|
|
125
|
-
status: "PENDING",
|
|
126
|
-
createdAt: now,
|
|
127
|
-
expiresAt: now + lockDurationMs
|
|
128
|
-
};
|
|
129
|
-
if (tokenAddress !== void 0) {
|
|
130
|
-
lock.tokenAddress = (0, import_viem.getAddress)(tokenAddress);
|
|
131
|
-
}
|
|
132
|
-
this.locks.set(lockId, lock);
|
|
133
|
-
return lockId;
|
|
134
|
-
}
|
|
135
|
-
async releaseLock(lockId) {
|
|
136
|
-
const lock = this.locks.get(lockId);
|
|
137
|
-
if (!lock) return;
|
|
138
|
-
if (lock.status === "PENDING") {
|
|
139
|
-
this.locks.delete(lockId);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
143
|
-
if (amount <= 0n) {
|
|
144
|
-
throw new Error("MemoryPointLedger: deduct amount must be positive");
|
|
145
|
-
}
|
|
146
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
147
|
-
const token = normalizeToken(tokenAddress);
|
|
148
|
-
const key = balanceKey(user, token);
|
|
149
|
-
const current = this.balances.get(key) ?? 0n;
|
|
150
|
-
if (current < amount) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
this.balances.set(key, current - amount);
|
|
156
|
-
for (const lock of this.locks.values()) {
|
|
157
|
-
if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
158
|
-
lock.status = "MINTED";
|
|
159
|
-
lock.txHash = txHash;
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
async updateMintStatus(lockId, status, txHash) {
|
|
165
|
-
const lock = this.locks.get(lockId);
|
|
166
|
-
if (!lock) {
|
|
167
|
-
throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
|
|
168
|
-
}
|
|
169
|
-
lock.status = status;
|
|
170
|
-
if (txHash) lock.txHash = txHash;
|
|
171
|
-
}
|
|
172
|
-
// -------------------------------------------------------------------------
|
|
173
|
-
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
174
|
-
// -------------------------------------------------------------------------
|
|
175
|
-
pendingCredits = /* @__PURE__ */ new Map();
|
|
176
|
-
nextCreditId = 1;
|
|
177
|
-
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
178
|
-
if (amount <= 0n) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
"MemoryPointLedger: pending credit amount must be positive"
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (durationMs <= 0) {
|
|
184
|
-
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
185
|
-
}
|
|
186
|
-
const user = (0, import_viem.getAddress)(userAddress);
|
|
187
|
-
const lockId = `credit-${this.nextCreditId++}`;
|
|
188
|
-
const now = this.now();
|
|
189
|
-
this.pendingCredits.set(lockId, {
|
|
190
|
-
lockId,
|
|
191
|
-
userAddress: user,
|
|
192
|
-
amount,
|
|
193
|
-
tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
|
|
194
|
-
createdAt: now,
|
|
195
|
-
expiresAt: now + durationMs,
|
|
196
|
-
status: "PENDING"
|
|
197
|
-
});
|
|
198
|
-
return lockId;
|
|
199
|
-
}
|
|
200
|
-
async resolveCreditByBurnTx(lockId, txHash) {
|
|
201
|
-
const credit = this.pendingCredits.get(lockId);
|
|
202
|
-
if (!credit) {
|
|
203
|
-
throw new Error(
|
|
204
|
-
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
if (credit.status === "RESOLVED") {
|
|
208
|
-
if (credit.txHash === txHash) return;
|
|
209
|
-
throw new Error(
|
|
210
|
-
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
const token = normalizeToken(credit.tokenAddress);
|
|
214
|
-
const key = balanceKey(credit.userAddress, token);
|
|
215
|
-
const current = this.balances.get(key) ?? 0n;
|
|
216
|
-
this.balances.set(key, current + credit.amount);
|
|
217
|
-
credit.status = "RESOLVED";
|
|
218
|
-
credit.txHash = txHash;
|
|
219
|
-
}
|
|
220
|
-
// -------------------------------------------------------------------------
|
|
221
|
-
// Internal helpers
|
|
222
|
-
// -------------------------------------------------------------------------
|
|
223
|
-
/**
|
|
224
|
-
* Auto-expire any PENDING lock past its expiry. Called lazily on every
|
|
225
|
-
* read/write so the in-memory state stays self-cleaning without a timer.
|
|
226
|
-
*/
|
|
227
|
-
purgeExpired() {
|
|
228
|
-
const now = this.now();
|
|
229
|
-
for (const lock of this.locks.values()) {
|
|
230
|
-
if (lock.status === "PENDING" && lock.expiresAt <= now) {
|
|
231
|
-
lock.status = "EXPIRED";
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
lockedTotalFor(userAddress, tokenKey) {
|
|
236
|
-
let total = 0n;
|
|
237
|
-
for (const lock of this.locks.values()) {
|
|
238
|
-
if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
|
|
239
|
-
total += lock.amount;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return total;
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
var DEFAULT_TOKEN_KEY = "default";
|
|
246
|
-
function normalizeToken(tokenAddress) {
|
|
247
|
-
return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : (0, import_viem.getAddress)(tokenAddress);
|
|
248
|
-
}
|
|
249
|
-
function balanceKey(user, tokenKey) {
|
|
250
|
-
return `${user}|${tokenKey}`;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
49
|
// src/policy/defaultPolicy.ts
|
|
254
50
|
var DefaultPolicyEngine = class {
|
|
255
51
|
ledger;
|
|
@@ -265,6 +61,13 @@ var DefaultPolicyEngine = class {
|
|
|
265
61
|
}
|
|
266
62
|
if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
|
|
267
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
|
+
}
|
|
268
71
|
}
|
|
269
72
|
async evaluate(request) {
|
|
270
73
|
if (request.amount <= 0n) {
|
|
@@ -301,32 +104,9 @@ var DefaultPolicyEngine = class {
|
|
|
301
104
|
}
|
|
302
105
|
};
|
|
303
106
|
|
|
304
|
-
// src/signer/privateKeySigner.ts
|
|
305
|
-
var import_viem2 = require("viem");
|
|
306
|
-
var import_accounts = require("viem/accounts");
|
|
307
|
-
var import_core = require("@pafi-dev/core");
|
|
308
|
-
var PrivateKeySigner = class {
|
|
309
|
-
account;
|
|
310
|
-
walletClient;
|
|
311
|
-
constructor(opts) {
|
|
312
|
-
this.account = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
|
|
313
|
-
this.walletClient = (0, import_viem2.createWalletClient)({
|
|
314
|
-
account: this.account,
|
|
315
|
-
chain: opts.chain,
|
|
316
|
-
transport: (0, import_viem2.http)(opts.rpcUrl)
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
async signMintRequest(domain, message) {
|
|
320
|
-
return (0, import_core.signMintRequest)(this.walletClient, domain, message);
|
|
321
|
-
}
|
|
322
|
-
async getAddress() {
|
|
323
|
-
return this.account.address;
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
|
|
327
107
|
// src/auth/memorySessionStore.ts
|
|
328
108
|
var import_node_crypto = require("crypto");
|
|
329
|
-
var
|
|
109
|
+
var import_viem = require("viem");
|
|
330
110
|
var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
331
111
|
var MemorySessionStore = class {
|
|
332
112
|
nonces = /* @__PURE__ */ new Map();
|
|
@@ -336,6 +116,11 @@ var MemorySessionStore = class {
|
|
|
336
116
|
nonceTtlMs;
|
|
337
117
|
now;
|
|
338
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
|
+
}
|
|
339
124
|
this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
|
|
340
125
|
this.now = opts.now ?? (() => Date.now());
|
|
341
126
|
}
|
|
@@ -362,7 +147,7 @@ var MemorySessionStore = class {
|
|
|
362
147
|
this.purgeExpiredSessions();
|
|
363
148
|
const normalized = {
|
|
364
149
|
...session,
|
|
365
|
-
userAddress: (0,
|
|
150
|
+
userAddress: (0, import_viem.getAddress)(session.userAddress)
|
|
366
151
|
};
|
|
367
152
|
this.sessions.set(session.tokenId, normalized);
|
|
368
153
|
}
|
|
@@ -380,7 +165,7 @@ var MemorySessionStore = class {
|
|
|
380
165
|
this.sessions.delete(tokenId);
|
|
381
166
|
}
|
|
382
167
|
async revokeAllSessions(userAddress) {
|
|
383
|
-
const key = (0,
|
|
168
|
+
const key = (0, import_viem.getAddress)(userAddress);
|
|
384
169
|
for (const [tokenId, session] of this.sessions.entries()) {
|
|
385
170
|
if (session.userAddress === key) {
|
|
386
171
|
this.sessions.delete(tokenId);
|
|
@@ -426,8 +211,8 @@ var NonceManager = class {
|
|
|
426
211
|
// src/auth/loginVerifier.ts
|
|
427
212
|
var import_node_crypto2 = require("crypto");
|
|
428
213
|
var import_jose = require("jose");
|
|
429
|
-
var
|
|
430
|
-
var
|
|
214
|
+
var import_viem2 = require("viem");
|
|
215
|
+
var import_core = require("@pafi-dev/core");
|
|
431
216
|
|
|
432
217
|
// src/auth/errors.ts
|
|
433
218
|
var AuthError = class extends Error {
|
|
@@ -450,8 +235,8 @@ var AuthService = class {
|
|
|
450
235
|
nonceManager;
|
|
451
236
|
now;
|
|
452
237
|
constructor(config) {
|
|
453
|
-
if (!config.jwtSecret || config.jwtSecret.length <
|
|
454
|
-
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");
|
|
455
240
|
}
|
|
456
241
|
this.sessionStore = config.sessionStore;
|
|
457
242
|
this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
|
|
@@ -475,11 +260,17 @@ var AuthService = class {
|
|
|
475
260
|
async login(message, signature) {
|
|
476
261
|
let parsed;
|
|
477
262
|
try {
|
|
478
|
-
parsed = (0,
|
|
263
|
+
parsed = (0, import_core.parseLoginMessage)(message);
|
|
479
264
|
} catch (err) {
|
|
480
265
|
const msg = err instanceof Error ? err.message : String(err);
|
|
481
266
|
throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
|
|
482
267
|
}
|
|
268
|
+
if (parsed.expirationTime == null) {
|
|
269
|
+
throw new AuthError(
|
|
270
|
+
"INVALID_MESSAGE",
|
|
271
|
+
"login message must include expirationTime"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
483
274
|
if (parsed.domain !== this.domain) {
|
|
484
275
|
throw new AuthError(
|
|
485
276
|
"DOMAIN_MISMATCH",
|
|
@@ -502,7 +293,7 @@ var AuthService = class {
|
|
|
502
293
|
if (parsed.expirationTime && parsed.expirationTime.getTime() <= now.getTime()) {
|
|
503
294
|
throw new AuthError("MESSAGE_EXPIRED", "Login message has expired");
|
|
504
295
|
}
|
|
505
|
-
const verifyResult = await (0,
|
|
296
|
+
const verifyResult = await (0, import_core.verifyLoginMessage)(message, signature);
|
|
506
297
|
if (!verifyResult.valid) {
|
|
507
298
|
throw new AuthError(
|
|
508
299
|
"SIGNATURE_INVALID",
|
|
@@ -516,7 +307,7 @@ var AuthService = class {
|
|
|
516
307
|
"Nonce is unknown, expired, or already used"
|
|
517
308
|
);
|
|
518
309
|
}
|
|
519
|
-
const userAddress = (0,
|
|
310
|
+
const userAddress = (0, import_viem2.getAddress)(verifyResult.address);
|
|
520
311
|
const tokenId = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
521
312
|
const issuedAt = now;
|
|
522
313
|
const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
|
|
@@ -544,7 +335,11 @@ var AuthService = class {
|
|
|
544
335
|
if (payload.jti) {
|
|
545
336
|
await this.sessionStore.revokeSession(payload.jti);
|
|
546
337
|
}
|
|
547
|
-
} 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
|
+
}
|
|
548
343
|
}
|
|
549
344
|
}
|
|
550
345
|
/**
|
|
@@ -583,7 +378,7 @@ var AuthService = class {
|
|
|
583
378
|
throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
|
|
584
379
|
}
|
|
585
380
|
return {
|
|
586
|
-
userAddress: (0,
|
|
381
|
+
userAddress: (0, import_viem2.getAddress)(userAddress),
|
|
587
382
|
chainId,
|
|
588
383
|
tokenId
|
|
589
384
|
};
|
|
@@ -634,210 +429,122 @@ var RelayError = class extends Error {
|
|
|
634
429
|
};
|
|
635
430
|
|
|
636
431
|
// src/relay/relayService.ts
|
|
637
|
-
var
|
|
638
|
-
var
|
|
639
|
-
var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
432
|
+
var import_viem3 = require("viem");
|
|
433
|
+
var import_core2 = require("@pafi-dev/core");
|
|
640
434
|
var RelayService = class {
|
|
641
|
-
relayAddress;
|
|
642
|
-
operatorWallet;
|
|
643
|
-
provider;
|
|
644
|
-
confirmationTimeoutMs;
|
|
645
|
-
simulateBeforeSubmit;
|
|
646
|
-
constructor(config) {
|
|
647
|
-
if (!config.relayAddress) {
|
|
648
|
-
throw new Error("RelayService: relayAddress is required");
|
|
649
|
-
}
|
|
650
|
-
if (!config.operatorWallet) {
|
|
651
|
-
throw new Error("RelayService: operatorWallet is required");
|
|
652
|
-
}
|
|
653
|
-
this.relayAddress = config.relayAddress;
|
|
654
|
-
this.operatorWallet = config.operatorWallet;
|
|
655
|
-
if (config.provider) this.provider = config.provider;
|
|
656
|
-
this.confirmationTimeoutMs = config.confirmationTimeoutMs ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
|
|
657
|
-
this.simulateBeforeSubmit = config.simulateBeforeSubmit ?? config.provider !== void 0;
|
|
658
|
-
}
|
|
659
|
-
/** Address the operator wallet is broadcasting from (for logging). */
|
|
660
|
-
operatorAddress() {
|
|
661
|
-
return this.operatorWallet.account?.address;
|
|
662
|
-
}
|
|
663
435
|
/**
|
|
664
|
-
* Build
|
|
665
|
-
*
|
|
666
|
-
* for audit before broadcasting.
|
|
436
|
+
* Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
|
|
437
|
+
* `PointToken.mint(to, amount, deadline, minterSig)`.
|
|
667
438
|
*/
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
return (0, import_core3.encodeMintAndSwap)(params.mint, params.swap);
|
|
671
|
-
} catch (err) {
|
|
439
|
+
async prepareMint(params) {
|
|
440
|
+
if (!params.batchExecutorAddress) {
|
|
672
441
|
throw new RelayError(
|
|
673
442
|
"ENCODE_FAILED",
|
|
674
|
-
|
|
675
|
-
err
|
|
443
|
+
"prepareMint: batchExecutorAddress required"
|
|
676
444
|
);
|
|
677
445
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
* Submit a `mintAndSwap` transaction. Flow:
|
|
681
|
-
*
|
|
682
|
-
* 1. (optional) pre-flight simulate via provider
|
|
683
|
-
* 2. writeContract through the operator wallet
|
|
684
|
-
* 3. (optional) wait for the receipt and surface gasUsed / status
|
|
685
|
-
*
|
|
686
|
-
* Throws a typed `RelayError` on any failure so the MintingGateway can
|
|
687
|
-
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
688
|
-
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
689
|
-
* need manual review because the tx may still land).
|
|
690
|
-
*
|
|
691
|
-
* @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
|
|
692
|
-
* `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
|
|
693
|
-
* still needs to finalize Relayer v2 ABI before the replacements
|
|
694
|
-
* can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
|
|
695
|
-
*/
|
|
696
|
-
async submitMintAndSwap(params) {
|
|
697
|
-
if (this.simulateBeforeSubmit && this.provider) {
|
|
698
|
-
const operatorAddr = this.operatorWallet.account?.address;
|
|
699
|
-
if (operatorAddr) {
|
|
700
|
-
try {
|
|
701
|
-
await (0, import_core3.simulateMintAndSwap)(
|
|
702
|
-
this.provider,
|
|
703
|
-
this.relayAddress,
|
|
704
|
-
params.mint,
|
|
705
|
-
params.swap,
|
|
706
|
-
operatorAddr
|
|
707
|
-
);
|
|
708
|
-
} catch (err) {
|
|
709
|
-
const reason = err instanceof import_core3.SimulationError ? err.reason : errorMessage(err);
|
|
710
|
-
throw new RelayError(
|
|
711
|
-
"SIMULATION_FAILED",
|
|
712
|
-
`mintAndSwap would revert: ${reason}`,
|
|
713
|
-
err
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
446
|
+
if (!params.userAddress) {
|
|
447
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
|
|
717
448
|
}
|
|
718
|
-
|
|
719
|
-
try {
|
|
720
|
-
txHash = await this.operatorWallet.writeContract({
|
|
721
|
-
address: this.relayAddress,
|
|
722
|
-
abi: import_core3.relayAbi,
|
|
723
|
-
functionName: "mintAndSwap",
|
|
724
|
-
args: [params.mint, params.swap],
|
|
725
|
-
...this.operatorWallet.account ? { account: this.operatorWallet.account } : {}
|
|
726
|
-
});
|
|
727
|
-
} catch (err) {
|
|
449
|
+
if (!params.pointTokenAddress) {
|
|
728
450
|
throw new RelayError(
|
|
729
|
-
"
|
|
730
|
-
|
|
731
|
-
err
|
|
451
|
+
"ENCODE_FAILED",
|
|
452
|
+
"prepareMint: pointTokenAddress required"
|
|
732
453
|
);
|
|
733
454
|
}
|
|
734
|
-
if (
|
|
735
|
-
|
|
455
|
+
if (params.amount <= 0n) {
|
|
456
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
|
|
736
457
|
}
|
|
737
|
-
|
|
738
|
-
const receipt = await this.provider.waitForTransactionReceipt({
|
|
739
|
-
hash: txHash,
|
|
740
|
-
timeout: this.confirmationTimeoutMs
|
|
741
|
-
});
|
|
742
|
-
if (receipt.status !== "success") {
|
|
743
|
-
throw new RelayError(
|
|
744
|
-
"TX_REVERTED",
|
|
745
|
-
`mintAndSwap reverted on-chain (tx=${txHash})`
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
return {
|
|
749
|
-
txHash,
|
|
750
|
-
blockNumber: receipt.blockNumber,
|
|
751
|
-
gasUsed: receipt.gasUsed,
|
|
752
|
-
status: receipt.status
|
|
753
|
-
};
|
|
754
|
-
} catch (err) {
|
|
755
|
-
if (err instanceof RelayError) throw err;
|
|
458
|
+
if (!params.issuerSignerWallet) {
|
|
756
459
|
throw new RelayError(
|
|
757
|
-
"
|
|
758
|
-
|
|
759
|
-
err
|
|
460
|
+
"ENCODE_FAILED",
|
|
461
|
+
"prepareMint: issuerSignerWallet required (for MintRequest EIP-712 signature)"
|
|
760
462
|
);
|
|
761
463
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
// v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
|
|
765
|
-
// ==========================================================================
|
|
766
|
-
//
|
|
767
|
-
// These two methods build unsigned `PartialUserOperation` payloads for
|
|
768
|
-
// the Frontend to sign (via Privy) and submit to the Bundler. The
|
|
769
|
-
// Issuer Backend no longer broadcasts — that's the Frontend's job.
|
|
770
|
-
//
|
|
771
|
-
// Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
|
|
772
|
-
// When SC delivers real ABIs, the imports swap but these method bodies
|
|
773
|
-
// stay the same (calldata encoder is ABI-driven).
|
|
774
|
-
// ==========================================================================
|
|
775
|
-
/**
|
|
776
|
-
* Build an unsigned UserOp for Scenario 1 (Mint).
|
|
777
|
-
*
|
|
778
|
-
* Flow:
|
|
779
|
-
* 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
|
|
780
|
-
* 2. Optionally append a PT fee transfer from user → feeRecipient
|
|
781
|
-
* (fee recovery happens on-chain via BatchExecutor, not via an
|
|
782
|
-
* operator wallet)
|
|
783
|
-
* 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
|
|
784
|
-
* 4. Return a `PartialUserOperation` ready for:
|
|
785
|
-
* - gas estimation (Bundler)
|
|
786
|
-
* - paymaster sponsorship (PAFI Backend)
|
|
787
|
-
* - user signature (Privy)
|
|
788
|
-
*/
|
|
789
|
-
prepareMint(params) {
|
|
790
|
-
if (!params.relayerAddress) {
|
|
791
|
-
throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
|
|
464
|
+
if (params.deadline <= 0n) {
|
|
465
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
|
|
792
466
|
}
|
|
793
|
-
|
|
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) {
|
|
794
473
|
throw new RelayError(
|
|
795
474
|
"ENCODE_FAILED",
|
|
796
|
-
"prepareMint:
|
|
475
|
+
"prepareMint: deadline exceeds maximum allowed window (1 hour)"
|
|
797
476
|
);
|
|
798
477
|
}
|
|
799
|
-
|
|
800
|
-
|
|
478
|
+
let minterSig;
|
|
479
|
+
try {
|
|
480
|
+
const sig = await (0, import_core2.signMintRequest)(
|
|
481
|
+
params.issuerSignerWallet,
|
|
482
|
+
params.domain,
|
|
483
|
+
{
|
|
484
|
+
to: params.userAddress,
|
|
485
|
+
amount: params.amount,
|
|
486
|
+
nonce: params.mintRequestNonce,
|
|
487
|
+
deadline: params.deadline
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
minterSig = sig.serialized;
|
|
491
|
+
} catch (err) {
|
|
492
|
+
throw new RelayError(
|
|
493
|
+
"ENCODE_FAILED",
|
|
494
|
+
`prepareMint: failed to sign MintRequest: ${errorMessage(err)}`,
|
|
495
|
+
err
|
|
496
|
+
);
|
|
801
497
|
}
|
|
802
498
|
let mintCallData;
|
|
803
499
|
try {
|
|
804
|
-
mintCallData = (0,
|
|
805
|
-
abi:
|
|
500
|
+
mintCallData = (0, import_viem3.encodeFunctionData)({
|
|
501
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
806
502
|
functionName: "mint",
|
|
807
|
-
args: [params.
|
|
503
|
+
args: [params.userAddress, params.amount, params.deadline, minterSig]
|
|
808
504
|
});
|
|
809
505
|
} catch (err) {
|
|
810
506
|
throw new RelayError(
|
|
811
507
|
"ENCODE_FAILED",
|
|
812
|
-
`prepareMint: failed to encode
|
|
508
|
+
`prepareMint: failed to encode PointToken.mint: ${errorMessage(err)}`,
|
|
813
509
|
err
|
|
814
510
|
);
|
|
815
511
|
}
|
|
816
512
|
const operations = [
|
|
817
513
|
{
|
|
818
|
-
target: params.
|
|
514
|
+
target: params.pointTokenAddress,
|
|
819
515
|
value: 0n,
|
|
820
516
|
data: mintCallData
|
|
821
517
|
}
|
|
822
518
|
];
|
|
823
|
-
if (params.
|
|
519
|
+
if (params.feeAmount && params.feeAmount > 0n) {
|
|
520
|
+
if (!params.feeRecipient) {
|
|
521
|
+
throw new RelayError(
|
|
522
|
+
"ENCODE_FAILED",
|
|
523
|
+
"prepareMint: feeRecipient required when feeAmount > 0"
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
|
|
527
|
+
throw new RelayError(
|
|
528
|
+
"ENCODE_FAILED",
|
|
529
|
+
"prepareMint: feeRecipient must not be zero address"
|
|
530
|
+
);
|
|
531
|
+
}
|
|
824
532
|
operations.push({
|
|
825
533
|
target: params.pointTokenAddress,
|
|
826
534
|
value: 0n,
|
|
827
|
-
data: (0,
|
|
828
|
-
abi:
|
|
829
|
-
functionName: "
|
|
830
|
-
|
|
831
|
-
args: [params.mintRequest.feeRecipient]
|
|
535
|
+
data: (0, import_viem3.encodeFunctionData)({
|
|
536
|
+
abi: import_viem3.erc20Abi,
|
|
537
|
+
functionName: "transfer",
|
|
538
|
+
args: [params.feeRecipient, params.feeAmount]
|
|
832
539
|
})
|
|
833
540
|
});
|
|
834
541
|
}
|
|
835
|
-
return (0,
|
|
542
|
+
return (0, import_core2.buildPartialUserOperation)({
|
|
836
543
|
sender: params.userAddress,
|
|
837
544
|
nonce: params.aaNonce,
|
|
838
545
|
operations,
|
|
839
546
|
gasLimits: {
|
|
840
|
-
callGasLimit: params.callGasLimit ??
|
|
547
|
+
callGasLimit: params.callGasLimit ?? 300000n,
|
|
841
548
|
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
842
549
|
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
843
550
|
}
|
|
@@ -847,13 +554,12 @@ var RelayService = class {
|
|
|
847
554
|
* Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
|
|
848
555
|
*
|
|
849
556
|
* Two modes:
|
|
850
|
-
* - `mode: 'burn'` — direct `PointToken.burn(amount)`;
|
|
851
|
-
* via EIP-7702
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* contract has to do it on-chain
|
|
557
|
+
* - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
|
|
558
|
+
* usable if the caller (via EIP-7702) is whitelisted as a burner.
|
|
559
|
+
* Rare in v1.4; kept for admin/operator tools.
|
|
560
|
+
* - `mode: 'burnWithSig'` — `PointToken.burn(from, amount, deadline,
|
|
561
|
+
* burnerSig)`. Caller provides a pre-signed `BurnRequest` + sig
|
|
562
|
+
* bytes (typically from `PTRedeemHandler`).
|
|
857
563
|
*/
|
|
858
564
|
prepareBurn(params) {
|
|
859
565
|
if (!params.pointTokenAddress) {
|
|
@@ -868,19 +574,24 @@ var RelayService = class {
|
|
|
868
574
|
let burnCallData;
|
|
869
575
|
try {
|
|
870
576
|
if (params.mode === "burnWithSig") {
|
|
871
|
-
if (!params.
|
|
872
|
-
throw new Error("burnWithSig requires
|
|
577
|
+
if (!params.burnRequest || !params.burnerSignature) {
|
|
578
|
+
throw new Error("burnWithSig requires burnRequest + burnerSignature");
|
|
873
579
|
}
|
|
874
|
-
burnCallData = (0,
|
|
875
|
-
abi:
|
|
876
|
-
functionName: "
|
|
877
|
-
args: [
|
|
580
|
+
burnCallData = (0, import_viem3.encodeFunctionData)({
|
|
581
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
582
|
+
functionName: "burn",
|
|
583
|
+
args: [
|
|
584
|
+
params.burnRequest.from,
|
|
585
|
+
params.burnRequest.amount,
|
|
586
|
+
params.burnRequest.deadline,
|
|
587
|
+
params.burnerSignature
|
|
588
|
+
]
|
|
878
589
|
});
|
|
879
590
|
} else {
|
|
880
|
-
burnCallData = (0,
|
|
881
|
-
abi:
|
|
591
|
+
burnCallData = (0, import_viem3.encodeFunctionData)({
|
|
592
|
+
abi: import_core2.POINT_TOKEN_V2_ABI,
|
|
882
593
|
functionName: "burn",
|
|
883
|
-
args: [params.amount]
|
|
594
|
+
args: [params.userAddress, params.amount]
|
|
884
595
|
});
|
|
885
596
|
}
|
|
886
597
|
} catch (err) {
|
|
@@ -897,7 +608,7 @@ var RelayService = class {
|
|
|
897
608
|
data: burnCallData
|
|
898
609
|
}
|
|
899
610
|
];
|
|
900
|
-
return (0,
|
|
611
|
+
return (0, import_core2.buildPartialUserOperation)({
|
|
901
612
|
sender: params.userAddress,
|
|
902
613
|
nonce: params.aaNonce,
|
|
903
614
|
operations,
|
|
@@ -916,11 +627,14 @@ function errorMessage(err) {
|
|
|
916
627
|
// src/relay/feeManager.ts
|
|
917
628
|
var DEFAULT_GAS_UNITS = 500000n;
|
|
918
629
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
919
|
-
var FeeManager = class {
|
|
630
|
+
var FeeManager = class _FeeManager {
|
|
920
631
|
provider;
|
|
921
632
|
gasUnits;
|
|
922
633
|
gasPremiumBps;
|
|
923
634
|
quoteNativeToFee;
|
|
635
|
+
cachedFee = null;
|
|
636
|
+
cacheExpiresAt = 0;
|
|
637
|
+
static CACHE_TTL_MS = 1e4;
|
|
924
638
|
constructor(config) {
|
|
925
639
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
926
640
|
if (!config.quoteNativeToFee)
|
|
@@ -943,259 +657,20 @@ var FeeManager = class {
|
|
|
943
657
|
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
944
658
|
*/
|
|
945
659
|
async estimateGasFee() {
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
if (this.cachedFee !== null && now < this.cacheExpiresAt) {
|
|
662
|
+
return this.cachedFee;
|
|
663
|
+
}
|
|
946
664
|
const gasPrice = await this.provider.getGasPrice();
|
|
947
665
|
const nativeCost = gasPrice * this.gasUnits;
|
|
948
666
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
// src/gateway/types.ts
|
|
954
|
-
var MintingGatewayError = class extends Error {
|
|
955
|
-
code;
|
|
956
|
-
/**
|
|
957
|
-
* True if the ledger lock was released before this error was thrown,
|
|
958
|
-
* meaning the user can safely retry. False means the funds are still
|
|
959
|
-
* locked (e.g. tx may still land on-chain) and retry would double-spend.
|
|
960
|
-
*/
|
|
961
|
-
safeToRetry;
|
|
962
|
-
cause;
|
|
963
|
-
constructor(code, message, opts) {
|
|
964
|
-
super(message);
|
|
965
|
-
this.name = "MintingGatewayError";
|
|
966
|
-
this.code = code;
|
|
967
|
-
this.safeToRetry = opts.safeToRetry;
|
|
968
|
-
if (opts.cause !== void 0) this.cause = opts.cause;
|
|
667
|
+
const fee = await this.quoteNativeToFee(withPremium);
|
|
668
|
+
this.cachedFee = fee;
|
|
669
|
+
this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
|
|
670
|
+
return fee;
|
|
969
671
|
}
|
|
970
672
|
};
|
|
971
673
|
|
|
972
|
-
// src/gateway/mintingGateway.ts
|
|
973
|
-
var import_core4 = require("@pafi-dev/core");
|
|
974
|
-
var DEFAULT_LOCK_BUFFER_MS = 6e4;
|
|
975
|
-
var MintingGateway = class {
|
|
976
|
-
ledger;
|
|
977
|
-
policy;
|
|
978
|
-
signer;
|
|
979
|
-
relayService;
|
|
980
|
-
now;
|
|
981
|
-
defaultLockBufferMs;
|
|
982
|
-
constructor(config) {
|
|
983
|
-
if (!config.ledger) throw new Error("MintingGateway: ledger required");
|
|
984
|
-
if (!config.policy) throw new Error("MintingGateway: policy required");
|
|
985
|
-
if (!config.signer) throw new Error("MintingGateway: signer required");
|
|
986
|
-
if (!config.relayService)
|
|
987
|
-
throw new Error("MintingGateway: relayService required");
|
|
988
|
-
this.ledger = config.ledger;
|
|
989
|
-
this.policy = config.policy;
|
|
990
|
-
this.signer = config.signer;
|
|
991
|
-
this.relayService = config.relayService;
|
|
992
|
-
this.now = config.now ?? (() => Date.now());
|
|
993
|
-
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
994
|
-
}
|
|
995
|
-
/**
|
|
996
|
-
* @deprecated Since 0.3.0 — will be renamed to `processMint()` once
|
|
997
|
-
* the SC team finalizes Relayer v2 ABI. The new flow drops the
|
|
998
|
-
* swap steps entirely (no more single-call mint+swap); users swap
|
|
999
|
-
* separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
|
|
1000
|
-
*/
|
|
1001
|
-
async processMintAndCashOut(request) {
|
|
1002
|
-
const { receiverConsent, receiverSignature } = request;
|
|
1003
|
-
if (!receiverConsent || !receiverSignature) {
|
|
1004
|
-
throw new MintingGatewayError(
|
|
1005
|
-
"INVALID_REQUEST",
|
|
1006
|
-
"receiverConsent and receiverSignature are required",
|
|
1007
|
-
{ safeToRetry: true }
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
if (receiverConsent.amount <= 0n) {
|
|
1011
|
-
throw new MintingGatewayError(
|
|
1012
|
-
"INVALID_REQUEST",
|
|
1013
|
-
"consent amount must be positive",
|
|
1014
|
-
{ safeToRetry: true }
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
if (receiverConsent.originalReceiver !== request.userAddress) {
|
|
1018
|
-
throw new MintingGatewayError(
|
|
1019
|
-
"INVALID_REQUEST",
|
|
1020
|
-
"consent.originalReceiver must equal request.userAddress",
|
|
1021
|
-
{ safeToRetry: true }
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1024
|
-
const nowSec = BigInt(Math.floor(this.now() / 1e3));
|
|
1025
|
-
if (receiverConsent.deadline <= nowSec) {
|
|
1026
|
-
throw new MintingGatewayError(
|
|
1027
|
-
"CONSENT_EXPIRED",
|
|
1028
|
-
"ReceiverConsent deadline has already passed",
|
|
1029
|
-
{ safeToRetry: true }
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
const consentResult = await (0, import_core4.verifyReceiverConsent)(
|
|
1033
|
-
request.domain,
|
|
1034
|
-
receiverConsent,
|
|
1035
|
-
receiverSignature,
|
|
1036
|
-
request.userAddress
|
|
1037
|
-
);
|
|
1038
|
-
if (!consentResult.isValid) {
|
|
1039
|
-
throw new MintingGatewayError(
|
|
1040
|
-
"INVALID_CONSENT_SIGNATURE",
|
|
1041
|
-
`ReceiverConsent signature did not recover to ${request.userAddress}`,
|
|
1042
|
-
{ safeToRetry: true }
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
|
-
const policyDecision = await this.policy.evaluate({
|
|
1046
|
-
userAddress: request.userAddress,
|
|
1047
|
-
amount: receiverConsent.amount,
|
|
1048
|
-
pointTokenAddress: request.pointTokenAddress,
|
|
1049
|
-
chainId: request.chainId
|
|
1050
|
-
});
|
|
1051
|
-
if (!policyDecision.approved) {
|
|
1052
|
-
const code = policyDecision.reason?.toLowerCase().includes("insufficient") ? "INSUFFICIENT_BALANCE" : "POLICY_REJECTED";
|
|
1053
|
-
throw new MintingGatewayError(
|
|
1054
|
-
code,
|
|
1055
|
-
policyDecision.reason ?? "Minting request rejected by policy engine",
|
|
1056
|
-
{ safeToRetry: true }
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
const lockDurationMs = request.lockDurationMs ?? this.computeLockDurationMs(receiverConsent.deadline);
|
|
1060
|
-
let lockId;
|
|
1061
|
-
try {
|
|
1062
|
-
lockId = await this.ledger.lockForMinting(
|
|
1063
|
-
request.userAddress,
|
|
1064
|
-
receiverConsent.amount,
|
|
1065
|
-
lockDurationMs,
|
|
1066
|
-
request.pointTokenAddress
|
|
1067
|
-
);
|
|
1068
|
-
} catch (err) {
|
|
1069
|
-
throw new MintingGatewayError(
|
|
1070
|
-
"INSUFFICIENT_BALANCE",
|
|
1071
|
-
`Failed to lock ledger balance: ${errorMessage2(err)}`,
|
|
1072
|
-
{ safeToRetry: true, cause: err }
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
try {
|
|
1076
|
-
let minterSignature;
|
|
1077
|
-
try {
|
|
1078
|
-
minterSignature = await this.signer.signMintRequest(request.domain, {
|
|
1079
|
-
to: request.userAddress,
|
|
1080
|
-
amount: receiverConsent.amount,
|
|
1081
|
-
nonce: receiverConsent.nonce,
|
|
1082
|
-
deadline: receiverConsent.deadline
|
|
1083
|
-
});
|
|
1084
|
-
} catch (err) {
|
|
1085
|
-
await this.releaseLockSafely(lockId);
|
|
1086
|
-
throw new MintingGatewayError(
|
|
1087
|
-
"SIGNER_FAILED",
|
|
1088
|
-
`Issuer signer failed: ${errorMessage2(err)}`,
|
|
1089
|
-
{ safeToRetry: true, cause: err }
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
const mintParams = {
|
|
1093
|
-
pointToken: request.pointTokenAddress,
|
|
1094
|
-
receiver: request.userAddress,
|
|
1095
|
-
amount: receiverConsent.amount,
|
|
1096
|
-
deadline: receiverConsent.deadline,
|
|
1097
|
-
minterSig: minterSignature.serialized,
|
|
1098
|
-
receiverSig: receiverSignature,
|
|
1099
|
-
extData: receiverConsent.extData
|
|
1100
|
-
};
|
|
1101
|
-
const swapParams = {
|
|
1102
|
-
path: request.swapPath,
|
|
1103
|
-
deadline: request.swapDeadline
|
|
1104
|
-
};
|
|
1105
|
-
let relayResult;
|
|
1106
|
-
try {
|
|
1107
|
-
relayResult = await this.relayService.submitMintAndSwap({
|
|
1108
|
-
mint: mintParams,
|
|
1109
|
-
swap: swapParams
|
|
1110
|
-
});
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
await this.handleRelayFailure(err, lockId);
|
|
1113
|
-
}
|
|
1114
|
-
const result = {
|
|
1115
|
-
txHash: relayResult.txHash,
|
|
1116
|
-
lockId
|
|
1117
|
-
};
|
|
1118
|
-
if (relayResult.blockNumber !== void 0) {
|
|
1119
|
-
result.blockNumber = relayResult.blockNumber;
|
|
1120
|
-
}
|
|
1121
|
-
if (relayResult.gasUsed !== void 0) {
|
|
1122
|
-
result.gasUsed = relayResult.gasUsed;
|
|
1123
|
-
}
|
|
1124
|
-
return result;
|
|
1125
|
-
} catch (err) {
|
|
1126
|
-
if (err instanceof MintingGatewayError) throw err;
|
|
1127
|
-
await this.releaseLockSafely(lockId);
|
|
1128
|
-
throw new MintingGatewayError(
|
|
1129
|
-
"RELAY_SUBMIT_FAILED",
|
|
1130
|
-
`Unexpected error: ${errorMessage2(err)}`,
|
|
1131
|
-
{ safeToRetry: true, cause: err }
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
// ---------------------------------------------------------------------------
|
|
1136
|
-
// Internals
|
|
1137
|
-
// ---------------------------------------------------------------------------
|
|
1138
|
-
computeLockDurationMs(consentDeadlineSec) {
|
|
1139
|
-
const nowMs = this.now();
|
|
1140
|
-
const deadlineMs = Number(consentDeadlineSec) * 1e3;
|
|
1141
|
-
const remaining = Math.max(0, deadlineMs - nowMs);
|
|
1142
|
-
return remaining + this.defaultLockBufferMs;
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Map a RelayError to a MintingGatewayError, releasing the lock only
|
|
1146
|
-
* when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
|
|
1147
|
-
* leave the lock in place because the tx may still be in the mempool
|
|
1148
|
-
* or already mined — releasing would enable a double-spend on retry.
|
|
1149
|
-
* Always throws.
|
|
1150
|
-
*/
|
|
1151
|
-
async handleRelayFailure(err, lockId) {
|
|
1152
|
-
if (err instanceof RelayError) {
|
|
1153
|
-
switch (err.code) {
|
|
1154
|
-
case "ENCODE_FAILED":
|
|
1155
|
-
case "SIMULATION_FAILED":
|
|
1156
|
-
case "SUBMIT_FAILED":
|
|
1157
|
-
case "NOT_CONFIGURED":
|
|
1158
|
-
await this.releaseLockSafely(lockId);
|
|
1159
|
-
throw new MintingGatewayError(
|
|
1160
|
-
err.code === "SIMULATION_FAILED" ? "RELAY_SIMULATION_FAILED" : "RELAY_SUBMIT_FAILED",
|
|
1161
|
-
err.message,
|
|
1162
|
-
{ safeToRetry: true, cause: err }
|
|
1163
|
-
);
|
|
1164
|
-
case "TX_REVERTED":
|
|
1165
|
-
throw new MintingGatewayError("RELAY_REVERTED", err.message, {
|
|
1166
|
-
safeToRetry: false,
|
|
1167
|
-
cause: err
|
|
1168
|
-
});
|
|
1169
|
-
case "TIMEOUT":
|
|
1170
|
-
throw new MintingGatewayError("RELAY_TIMEOUT", err.message, {
|
|
1171
|
-
safeToRetry: false,
|
|
1172
|
-
cause: err
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
await this.releaseLockSafely(lockId);
|
|
1177
|
-
throw new MintingGatewayError(
|
|
1178
|
-
"RELAY_SUBMIT_FAILED",
|
|
1179
|
-
`Unexpected relay error: ${errorMessage2(err)}`,
|
|
1180
|
-
{ safeToRetry: true, cause: err }
|
|
1181
|
-
);
|
|
1182
|
-
}
|
|
1183
|
-
/**
|
|
1184
|
-
* Release a lock, swallowing any secondary error. We never want a lock
|
|
1185
|
-
* release failure to mask the original error — the lock will auto-expire
|
|
1186
|
-
* via TTL anyway.
|
|
1187
|
-
*/
|
|
1188
|
-
async releaseLockSafely(lockId) {
|
|
1189
|
-
try {
|
|
1190
|
-
await this.ledger.releaseLock(lockId);
|
|
1191
|
-
} catch {
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
function errorMessage2(err) {
|
|
1196
|
-
return err instanceof Error ? err.message : String(err);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
674
|
// src/indexer/types.ts
|
|
1200
675
|
var InMemoryCursorStore = class {
|
|
1201
676
|
cursor;
|
|
@@ -1208,8 +683,8 @@ var InMemoryCursorStore = class {
|
|
|
1208
683
|
};
|
|
1209
684
|
|
|
1210
685
|
// src/indexer/pointIndexer.ts
|
|
1211
|
-
var
|
|
1212
|
-
var TRANSFER_EVENT = (0,
|
|
686
|
+
var import_viem4 = require("viem");
|
|
687
|
+
var TRANSFER_EVENT = (0, import_viem4.parseAbiItem)(
|
|
1213
688
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1214
689
|
);
|
|
1215
690
|
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
@@ -1279,7 +754,8 @@ var PointIndexer = class {
|
|
|
1279
754
|
return;
|
|
1280
755
|
}
|
|
1281
756
|
await this.processBlockRange(from, safeHead);
|
|
1282
|
-
} catch {
|
|
757
|
+
} catch (err) {
|
|
758
|
+
console.error("[PAFI] PointIndexer tick error:", err);
|
|
1283
759
|
}
|
|
1284
760
|
this.scheduleNext();
|
|
1285
761
|
}
|
|
@@ -1329,10 +805,10 @@ var PointIndexer = class {
|
|
|
1329
805
|
for (const log of logs) {
|
|
1330
806
|
const args = log.args;
|
|
1331
807
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1332
|
-
if ((0,
|
|
808
|
+
if ((0, import_viem4.getAddress)(args.from) !== ZERO_ADDRESS) continue;
|
|
1333
809
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1334
810
|
out.push({
|
|
1335
|
-
to: (0,
|
|
811
|
+
to: (0, import_viem4.getAddress)(args.to),
|
|
1336
812
|
amount: args.value,
|
|
1337
813
|
blockNumber: log.blockNumber,
|
|
1338
814
|
txHash: log.transactionHash,
|
|
@@ -1388,8 +864,8 @@ function pickMatchingLock(locks, amount) {
|
|
|
1388
864
|
}
|
|
1389
865
|
|
|
1390
866
|
// src/indexer/burnIndexer.ts
|
|
1391
|
-
var
|
|
1392
|
-
var TRANSFER_EVENT2 = (0,
|
|
867
|
+
var import_viem5 = require("viem");
|
|
868
|
+
var TRANSFER_EVENT2 = (0, import_viem5.parseAbiItem)(
|
|
1393
869
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1394
870
|
);
|
|
1395
871
|
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
@@ -1405,18 +881,7 @@ var BurnIndexer = class {
|
|
|
1405
881
|
confirmations;
|
|
1406
882
|
batchSize;
|
|
1407
883
|
pollIntervalMs;
|
|
1408
|
-
|
|
1409
|
-
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1410
|
-
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1411
|
-
* ledger's query path.
|
|
1412
|
-
*
|
|
1413
|
-
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1414
|
-
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1415
|
-
* incrementing IDs so callers with the memory ledger must provide a
|
|
1416
|
-
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1417
|
-
* their `pending_credits` table.
|
|
1418
|
-
*/
|
|
1419
|
-
matchLockId = async () => void 0;
|
|
884
|
+
matchLockId;
|
|
1420
885
|
running = false;
|
|
1421
886
|
timer;
|
|
1422
887
|
constructor(config) {
|
|
@@ -1434,6 +899,12 @@ var BurnIndexer = class {
|
|
|
1434
899
|
);
|
|
1435
900
|
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1436
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;
|
|
1437
908
|
}
|
|
1438
909
|
start() {
|
|
1439
910
|
if (this.running) return;
|
|
@@ -1463,7 +934,8 @@ var BurnIndexer = class {
|
|
|
1463
934
|
return;
|
|
1464
935
|
}
|
|
1465
936
|
await this.processBlockRange(from, safeHead);
|
|
1466
|
-
} catch {
|
|
937
|
+
} catch (err) {
|
|
938
|
+
console.error("[PAFI] BurnIndexer tick error:", err);
|
|
1467
939
|
}
|
|
1468
940
|
this.scheduleNext();
|
|
1469
941
|
}
|
|
@@ -1508,10 +980,10 @@ var BurnIndexer = class {
|
|
|
1508
980
|
for (const log of logs) {
|
|
1509
981
|
const args = log.args;
|
|
1510
982
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1511
|
-
if ((0,
|
|
983
|
+
if ((0, import_viem5.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
|
|
1512
984
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1513
985
|
out.push({
|
|
1514
|
-
from: (0,
|
|
986
|
+
from: (0, import_viem5.getAddress)(args.from),
|
|
1515
987
|
amount: args.value,
|
|
1516
988
|
blockNumber: log.blockNumber,
|
|
1517
989
|
txHash: log.transactionHash,
|
|
@@ -1526,24 +998,30 @@ var BurnIndexer = class {
|
|
|
1526
998
|
* log + skip.
|
|
1527
999
|
*/
|
|
1528
1000
|
async finalize(evt) {
|
|
1001
|
+
const txHash = evt.txHash;
|
|
1529
1002
|
const lockId = await this.matchLockId(evt);
|
|
1530
|
-
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
|
+
}
|
|
1531
1009
|
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1532
1010
|
return;
|
|
1533
1011
|
}
|
|
1534
1012
|
try {
|
|
1535
1013
|
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1536
|
-
} catch {
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
|
|
1537
1016
|
}
|
|
1538
1017
|
}
|
|
1539
1018
|
};
|
|
1540
1019
|
|
|
1541
1020
|
// src/api/handlers.ts
|
|
1542
|
-
var
|
|
1543
|
-
var
|
|
1021
|
+
var import_viem6 = require("viem");
|
|
1022
|
+
var import_core3 = require("@pafi-dev/core");
|
|
1544
1023
|
var IssuerApiHandlers = class {
|
|
1545
1024
|
authService;
|
|
1546
|
-
gateway;
|
|
1547
1025
|
ledger;
|
|
1548
1026
|
provider;
|
|
1549
1027
|
/**
|
|
@@ -1551,18 +1029,14 @@ var IssuerApiHandlers = class {
|
|
|
1551
1029
|
* validate the request's `pointTokenAddress` against this set.
|
|
1552
1030
|
*/
|
|
1553
1031
|
supportedTokens;
|
|
1554
|
-
/** First supported token — used as default when a handler doesn't
|
|
1555
|
-
* receive a `pointTokenAddress` in the request (shouldn't happen in
|
|
1556
|
-
* practice, but keeps type-narrowing happy). */
|
|
1557
|
-
defaultToken;
|
|
1558
1032
|
chainId;
|
|
1559
1033
|
contracts;
|
|
1560
1034
|
pafiWebUrl;
|
|
1561
1035
|
feeManager;
|
|
1562
1036
|
poolsProvider;
|
|
1037
|
+
claim;
|
|
1563
1038
|
constructor(config) {
|
|
1564
1039
|
this.authService = config.authService;
|
|
1565
|
-
this.gateway = config.gateway;
|
|
1566
1040
|
this.ledger = config.ledger;
|
|
1567
1041
|
this.provider = config.provider;
|
|
1568
1042
|
const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
@@ -1571,14 +1045,14 @@ var IssuerApiHandlers = class {
|
|
|
1571
1045
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1572
1046
|
);
|
|
1573
1047
|
}
|
|
1574
|
-
const normalized = raw.map((a) => (0,
|
|
1048
|
+
const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
|
|
1575
1049
|
this.supportedTokens = new Set(normalized);
|
|
1576
|
-
this.defaultToken = normalized[0];
|
|
1577
1050
|
this.chainId = config.chainId;
|
|
1578
1051
|
this.contracts = config.contracts;
|
|
1579
1052
|
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1580
1053
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1581
1054
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1055
|
+
if (config.claim) this.claim = config.claim;
|
|
1582
1056
|
}
|
|
1583
1057
|
// =========================================================================
|
|
1584
1058
|
// Public handlers (no auth required)
|
|
@@ -1593,6 +1067,12 @@ var IssuerApiHandlers = class {
|
|
|
1593
1067
|
if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
|
|
1594
1068
|
throw new Error("handleLogin: message and signature are required");
|
|
1595
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
|
+
}
|
|
1596
1076
|
const result = await this.authService.login(body.message, body.signature);
|
|
1597
1077
|
return {
|
|
1598
1078
|
token: result.token,
|
|
@@ -1607,9 +1087,12 @@ var IssuerApiHandlers = class {
|
|
|
1607
1087
|
* needs to build EIP-712 messages and interact with on-chain.
|
|
1608
1088
|
*/
|
|
1609
1089
|
async handleConfig(chainId) {
|
|
1090
|
+
if (!Number.isInteger(chainId) || chainId <= 0) {
|
|
1091
|
+
throw new Error("invalid chainId");
|
|
1092
|
+
}
|
|
1610
1093
|
if (chainId !== this.chainId) {
|
|
1611
1094
|
throw new Error(
|
|
1612
|
-
`handleConfig: unsupported chainId ${chainId}
|
|
1095
|
+
`handleConfig: unsupported chainId ${chainId}`
|
|
1613
1096
|
);
|
|
1614
1097
|
}
|
|
1615
1098
|
const contracts = {
|
|
@@ -1672,25 +1155,25 @@ var IssuerApiHandlers = class {
|
|
|
1672
1155
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1673
1156
|
);
|
|
1674
1157
|
}
|
|
1675
|
-
const normalizedAuthed = (0,
|
|
1676
|
-
const normalizedRequest = (0,
|
|
1158
|
+
const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
|
|
1159
|
+
const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
|
|
1677
1160
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1678
1161
|
throw new Error(
|
|
1679
1162
|
"handleUser: request userAddress must match authenticated user"
|
|
1680
1163
|
);
|
|
1681
1164
|
}
|
|
1682
|
-
const pointToken = (0,
|
|
1165
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1683
1166
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1684
1167
|
throw new Error(
|
|
1685
1168
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
1686
1169
|
);
|
|
1687
1170
|
}
|
|
1688
1171
|
const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
|
|
1689
|
-
(0,
|
|
1690
|
-
(0,
|
|
1172
|
+
(0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1173
|
+
(0, import_core3.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1691
1174
|
this.ledger.getBalance(normalizedAuthed, pointToken),
|
|
1692
|
-
(0,
|
|
1693
|
-
(0,
|
|
1175
|
+
(0, import_core3.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
|
|
1176
|
+
(0, import_core3.isMinter)(this.provider, pointToken, normalizedAuthed)
|
|
1694
1177
|
]);
|
|
1695
1178
|
return {
|
|
1696
1179
|
mintRequestNonce,
|
|
@@ -1722,19 +1205,32 @@ var IssuerApiHandlers = class {
|
|
|
1722
1205
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1723
1206
|
);
|
|
1724
1207
|
}
|
|
1725
|
-
const pointToken = (0,
|
|
1208
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1726
1209
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1727
1210
|
throw new Error(
|
|
1728
1211
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
1729
1212
|
);
|
|
1730
1213
|
}
|
|
1731
|
-
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);
|
|
1732
1228
|
const domain = {
|
|
1733
1229
|
name,
|
|
1734
1230
|
verifyingContract: pointToken,
|
|
1735
1231
|
chainId: this.chainId
|
|
1736
1232
|
};
|
|
1737
|
-
const typedData = (0,
|
|
1233
|
+
const typedData = (0, import_core3.buildReceiverConsentTypedData)(domain, consent);
|
|
1738
1234
|
return {
|
|
1739
1235
|
typedData: {
|
|
1740
1236
|
domain: typedData.domain,
|
|
@@ -1745,54 +1241,97 @@ var IssuerApiHandlers = class {
|
|
|
1745
1241
|
};
|
|
1746
1242
|
}
|
|
1747
1243
|
/**
|
|
1748
|
-
* `POST /claim
|
|
1244
|
+
* `POST /claim`
|
|
1749
1245
|
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
* [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
|
|
1754
|
-
* removed in 2.0.
|
|
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.
|
|
1755
1249
|
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
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.
|
|
1758
1257
|
*/
|
|
1759
|
-
async
|
|
1258
|
+
async handleClaim(userAddress, request) {
|
|
1259
|
+
if (!this.claim) {
|
|
1260
|
+
throw new Error("handleClaim: claim is not configured on this issuer");
|
|
1261
|
+
}
|
|
1760
1262
|
if (request.chainId !== this.chainId) {
|
|
1761
|
-
throw new Error(
|
|
1762
|
-
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1763
|
-
);
|
|
1263
|
+
throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
|
|
1764
1264
|
}
|
|
1765
|
-
const pointToken = (0,
|
|
1265
|
+
const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
|
|
1766
1266
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1767
|
-
throw new Error(
|
|
1768
|
-
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1769
|
-
);
|
|
1267
|
+
throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
|
|
1770
1268
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
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,
|
|
1773
1282
|
pointTokenAddress: pointToken,
|
|
1774
|
-
chainId:
|
|
1775
|
-
domain: request.domain,
|
|
1776
|
-
receiverConsent: request.receiverConsent,
|
|
1777
|
-
receiverSignature: request.receiverSignature,
|
|
1778
|
-
swapPath: request.swapPath,
|
|
1779
|
-
swapDeadline: request.swapDeadline
|
|
1283
|
+
chainId: this.chainId
|
|
1780
1284
|
});
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
+
}
|
|
1789
1327
|
}
|
|
1790
1328
|
};
|
|
1791
1329
|
|
|
1792
1330
|
// src/api/handlers/ptRedeemHandler.ts
|
|
1793
|
-
var
|
|
1794
|
-
var
|
|
1331
|
+
var import_viem7 = require("viem");
|
|
1332
|
+
var import_core4 = require("@pafi-dev/core");
|
|
1795
1333
|
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1334
|
+
var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
|
|
1796
1335
|
var PTRedeemError = class extends Error {
|
|
1797
1336
|
constructor(code, message) {
|
|
1798
1337
|
super(message);
|
|
@@ -1804,11 +1343,14 @@ var PTRedeemError = class extends Error {
|
|
|
1804
1343
|
var PTRedeemHandler = class {
|
|
1805
1344
|
ledger;
|
|
1806
1345
|
relayService;
|
|
1346
|
+
provider;
|
|
1807
1347
|
pointTokenAddress;
|
|
1808
1348
|
batchExecutorAddress;
|
|
1809
1349
|
chainId;
|
|
1810
1350
|
domain;
|
|
1351
|
+
burnerSignerWallet;
|
|
1811
1352
|
redeemLockDurationMs;
|
|
1353
|
+
signatureDeadlineSeconds;
|
|
1812
1354
|
now;
|
|
1813
1355
|
constructor(config) {
|
|
1814
1356
|
if (!config.ledger.reservePendingCredit) {
|
|
@@ -1817,46 +1359,88 @@ var PTRedeemHandler = class {
|
|
|
1817
1359
|
"PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
|
|
1818
1360
|
);
|
|
1819
1361
|
}
|
|
1362
|
+
if (!config.burnerSignerWallet) {
|
|
1363
|
+
throw new PTRedeemError(
|
|
1364
|
+
"SIGNING_FAILED",
|
|
1365
|
+
"PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1820
1368
|
this.ledger = config.ledger;
|
|
1821
1369
|
this.relayService = config.relayService;
|
|
1822
|
-
this.
|
|
1823
|
-
this.
|
|
1370
|
+
this.provider = config.provider;
|
|
1371
|
+
this.pointTokenAddress = (0, import_viem7.getAddress)(config.pointTokenAddress);
|
|
1372
|
+
this.batchExecutorAddress = (0, import_viem7.getAddress)(config.batchExecutorAddress);
|
|
1824
1373
|
this.chainId = config.chainId;
|
|
1825
1374
|
this.domain = config.domain;
|
|
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
|
+
}
|
|
1826
1379
|
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1380
|
+
this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
|
|
1827
1381
|
this.now = config.now ?? (() => Date.now());
|
|
1828
1382
|
}
|
|
1829
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
|
+
}
|
|
1830
1390
|
if (request.amount <= 0n) {
|
|
1831
|
-
throw new PTRedeemError("
|
|
1391
|
+
throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
|
|
1832
1392
|
}
|
|
1833
|
-
|
|
1393
|
+
let burnNonce;
|
|
1394
|
+
try {
|
|
1395
|
+
burnNonce = await this.provider.readContract({
|
|
1396
|
+
address: this.pointTokenAddress,
|
|
1397
|
+
abi: import_core4.POINT_TOKEN_V2_ABI,
|
|
1398
|
+
functionName: "burnRequestNonces",
|
|
1399
|
+
args: [request.userAddress]
|
|
1400
|
+
});
|
|
1401
|
+
} catch (err) {
|
|
1834
1402
|
throw new PTRedeemError(
|
|
1835
|
-
"
|
|
1836
|
-
`
|
|
1403
|
+
"NONCE_READ_FAILED",
|
|
1404
|
+
`failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
|
|
1837
1405
|
);
|
|
1838
1406
|
}
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1407
|
+
const onChainBalance = await (0, import_core4.getPointTokenBalance)(
|
|
1408
|
+
this.provider,
|
|
1409
|
+
this.pointTokenAddress,
|
|
1410
|
+
request.userAddress
|
|
1411
|
+
);
|
|
1412
|
+
if (onChainBalance < request.amount) {
|
|
1841
1413
|
throw new PTRedeemError(
|
|
1842
|
-
"
|
|
1843
|
-
`
|
|
1414
|
+
"INVALID_AMOUNT",
|
|
1415
|
+
`insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
|
|
1844
1416
|
);
|
|
1845
1417
|
}
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
name: this.domain.name,
|
|
1849
|
-
chainId: this.chainId,
|
|
1850
|
-
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1851
|
-
},
|
|
1852
|
-
request.consent,
|
|
1853
|
-
request.consentSignature,
|
|
1854
|
-
request.userAddress
|
|
1418
|
+
const deadline = BigInt(
|
|
1419
|
+
Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
|
|
1855
1420
|
);
|
|
1856
|
-
|
|
1421
|
+
const domain = {
|
|
1422
|
+
name: this.domain.name,
|
|
1423
|
+
chainId: this.chainId,
|
|
1424
|
+
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1425
|
+
};
|
|
1426
|
+
const burnRequest = {
|
|
1427
|
+
from: request.userAddress,
|
|
1428
|
+
amount: request.amount,
|
|
1429
|
+
nonce: burnNonce,
|
|
1430
|
+
deadline
|
|
1431
|
+
};
|
|
1432
|
+
let burnerSignature;
|
|
1433
|
+
try {
|
|
1434
|
+
const sig = await (0, import_core4.signBurnRequest)(
|
|
1435
|
+
this.burnerSignerWallet,
|
|
1436
|
+
domain,
|
|
1437
|
+
burnRequest
|
|
1438
|
+
);
|
|
1439
|
+
burnerSignature = sig.serialized;
|
|
1440
|
+
} catch (err) {
|
|
1857
1441
|
throw new PTRedeemError(
|
|
1858
|
-
"
|
|
1859
|
-
`
|
|
1442
|
+
"SIGNING_FAILED",
|
|
1443
|
+
`failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
|
|
1860
1444
|
);
|
|
1861
1445
|
}
|
|
1862
1446
|
const lockId = await this.ledger.reservePendingCredit(
|
|
@@ -1871,33 +1455,21 @@ var PTRedeemHandler = class {
|
|
|
1871
1455
|
aaNonce: request.aaNonce,
|
|
1872
1456
|
pointTokenAddress: this.pointTokenAddress,
|
|
1873
1457
|
batchExecutorAddress: this.batchExecutorAddress,
|
|
1874
|
-
|
|
1875
|
-
|
|
1458
|
+
burnRequest,
|
|
1459
|
+
burnerSignature
|
|
1876
1460
|
});
|
|
1877
1461
|
return {
|
|
1878
1462
|
lockId,
|
|
1879
1463
|
userOp,
|
|
1880
|
-
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
|
|
1464
|
+
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
|
|
1465
|
+
signatureDeadline: deadline
|
|
1881
1466
|
};
|
|
1882
1467
|
}
|
|
1883
1468
|
};
|
|
1884
|
-
function parseSigStruct(serialized) {
|
|
1885
|
-
const raw = serialized.slice(2);
|
|
1886
|
-
if (raw.length !== 130) {
|
|
1887
|
-
throw new PTRedeemError(
|
|
1888
|
-
"INVALID_CONSENT",
|
|
1889
|
-
`signature must be 65 bytes, got ${raw.length / 2}`
|
|
1890
|
-
);
|
|
1891
|
-
}
|
|
1892
|
-
const r = `0x${raw.slice(0, 64)}`;
|
|
1893
|
-
const s = `0x${raw.slice(64, 128)}`;
|
|
1894
|
-
const v = parseInt(raw.slice(128, 130), 16);
|
|
1895
|
-
return { v, r, s };
|
|
1896
|
-
}
|
|
1897
1469
|
|
|
1898
1470
|
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1899
|
-
var
|
|
1900
|
-
var
|
|
1471
|
+
var import_viem8 = require("viem");
|
|
1472
|
+
var import_core5 = require("@pafi-dev/core");
|
|
1901
1473
|
var TopUpRedemptionError = class extends Error {
|
|
1902
1474
|
constructor(code, message) {
|
|
1903
1475
|
super(message);
|
|
@@ -1915,9 +1487,15 @@ var TopUpRedemptionHandler = class {
|
|
|
1915
1487
|
this.ledger = config.ledger;
|
|
1916
1488
|
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1917
1489
|
this.provider = config.provider;
|
|
1918
|
-
this.pointTokenAddress = (0,
|
|
1490
|
+
this.pointTokenAddress = (0, import_viem8.getAddress)(config.pointTokenAddress);
|
|
1919
1491
|
}
|
|
1920
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
|
+
}
|
|
1921
1499
|
const offChainBalance = await this.ledger.getBalance(
|
|
1922
1500
|
request.userAddress,
|
|
1923
1501
|
this.pointTokenAddress
|
|
@@ -1926,7 +1504,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1926
1504
|
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1927
1505
|
}
|
|
1928
1506
|
const shortfall = request.requiredAmount - offChainBalance;
|
|
1929
|
-
const onChainBalance = await (0,
|
|
1507
|
+
const onChainBalance = await (0, import_core5.getPointTokenBalance)(
|
|
1930
1508
|
this.provider,
|
|
1931
1509
|
this.pointTokenAddress,
|
|
1932
1510
|
request.userAddress
|
|
@@ -1939,24 +1517,11 @@ var TopUpRedemptionHandler = class {
|
|
|
1939
1517
|
shortfall
|
|
1940
1518
|
};
|
|
1941
1519
|
}
|
|
1942
|
-
if (request.redeemRequest.consent.amount < shortfall) {
|
|
1943
|
-
throw new TopUpRedemptionError(
|
|
1944
|
-
"CONSENT_AMOUNT_TOO_LOW",
|
|
1945
|
-
`consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
|
|
1946
|
-
);
|
|
1947
|
-
}
|
|
1948
|
-
if (request.redeemRequest.consent.amount !== shortfall) {
|
|
1949
|
-
throw new TopUpRedemptionError(
|
|
1950
|
-
"CONSENT_AMOUNT_TOO_LOW",
|
|
1951
|
-
`consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
|
|
1952
|
-
);
|
|
1953
|
-
}
|
|
1954
1520
|
const redeem = await this.ptRedeemHandler.handle({
|
|
1521
|
+
authenticatedAddress: request.authenticatedAddress,
|
|
1955
1522
|
userAddress: request.userAddress,
|
|
1956
1523
|
amount: shortfall,
|
|
1957
|
-
|
|
1958
|
-
consentSignature: request.redeemRequest.consentSignature,
|
|
1959
|
-
aaNonce: request.redeemRequest.aaNonce
|
|
1524
|
+
aaNonce: request.aaNonce
|
|
1960
1525
|
});
|
|
1961
1526
|
return {
|
|
1962
1527
|
action: "TOP_UP_STARTED",
|
|
@@ -1967,6 +1532,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1967
1532
|
};
|
|
1968
1533
|
|
|
1969
1534
|
// src/pools/subgraphPoolsProvider.ts
|
|
1535
|
+
var import_viem9 = require("viem");
|
|
1970
1536
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1971
1537
|
var POOL_QUERY = `
|
|
1972
1538
|
query GetPoolForPointToken($id: ID!) {
|
|
@@ -1989,6 +1555,19 @@ function createSubgraphPoolsProvider(config) {
|
|
|
1989
1555
|
"createSubgraphPoolsProvider: subgraphUrl is required"
|
|
1990
1556
|
);
|
|
1991
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
|
+
}
|
|
1992
1571
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
1993
1572
|
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1994
1573
|
const now = config.now ?? (() => Date.now());
|
|
@@ -2057,6 +1636,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
|
|
|
2057
1636
|
return [];
|
|
2058
1637
|
}
|
|
2059
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
|
+
}
|
|
2060
1659
|
const [currency0, currency1] = sortCurrencies(
|
|
2061
1660
|
pool.token0.id,
|
|
2062
1661
|
pool.token1.id
|
|
@@ -2092,6 +1691,19 @@ function createSubgraphNativeUsdtQuoter(config) {
|
|
|
2092
1691
|
"createSubgraphNativeUsdtQuoter: subgraphUrl is required"
|
|
2093
1692
|
);
|
|
2094
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
|
+
}
|
|
2095
1707
|
const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
|
|
2096
1708
|
const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
|
|
2097
1709
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
|
|
@@ -2168,6 +1780,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
|
|
|
2168
1780
|
);
|
|
2169
1781
|
return null;
|
|
2170
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
|
+
}
|
|
2171
1791
|
return parsed;
|
|
2172
1792
|
}
|
|
2173
1793
|
function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
@@ -2178,7 +1798,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
2178
1798
|
}
|
|
2179
1799
|
|
|
2180
1800
|
// src/balance/balanceAggregator.ts
|
|
2181
|
-
var
|
|
1801
|
+
var import_core6 = require("@pafi-dev/core");
|
|
2182
1802
|
var BalanceAggregator = class {
|
|
2183
1803
|
provider;
|
|
2184
1804
|
ledger;
|
|
@@ -2199,7 +1819,7 @@ var BalanceAggregator = class {
|
|
|
2199
1819
|
async getCombinedBalance(user, pointToken) {
|
|
2200
1820
|
const [offChain, onChain] = await Promise.all([
|
|
2201
1821
|
this.ledger.getBalance(user, pointToken),
|
|
2202
|
-
(0,
|
|
1822
|
+
(0, import_core6.getPointTokenBalance)(this.provider, pointToken, user)
|
|
2203
1823
|
]);
|
|
2204
1824
|
return {
|
|
2205
1825
|
offChain,
|
|
@@ -2237,28 +1857,11 @@ var PafiBackendError = class extends Error {
|
|
|
2237
1857
|
code;
|
|
2238
1858
|
httpStatus;
|
|
2239
1859
|
details;
|
|
2240
|
-
/**
|
|
2241
|
-
* Seconds to wait before retry. Populated from the server body
|
|
2242
|
-
* (e.g. rate limit returns the number of seconds until UTC midnight).
|
|
2243
|
-
*/
|
|
2244
1860
|
retryAfter;
|
|
2245
|
-
/**
|
|
2246
|
-
* `safeToRetry` as reported by the server body. Prefer this over the
|
|
2247
|
-
* code-based heuristic when available — the server knows more about
|
|
2248
|
-
* whether the same request will succeed on retry.
|
|
2249
|
-
*/
|
|
2250
1861
|
serverSafeToRetry;
|
|
2251
|
-
/**
|
|
2252
|
-
* Whether the caller can safely retry the same request.
|
|
2253
|
-
*
|
|
2254
|
-
* If the server provided `safeToRetry` in the body, trust that.
|
|
2255
|
-
* Otherwise fall back to a code-based heuristic.
|
|
2256
|
-
*/
|
|
2257
1862
|
get safeToRetry() {
|
|
2258
1863
|
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
2259
1864
|
switch (this.code) {
|
|
2260
|
-
case "PAYMASTER_UNAVAILABLE":
|
|
2261
|
-
case "PAYMASTER_TIMEOUT":
|
|
2262
1865
|
case "RATE_LIMITER_UNAVAILABLE":
|
|
2263
1866
|
case "INTERNAL_ERROR":
|
|
2264
1867
|
case "TIMEOUT":
|
|
@@ -2267,196 +1870,22 @@ var PafiBackendError = class extends Error {
|
|
|
2267
1870
|
case "RATE_LIMIT_EXCEEDED":
|
|
2268
1871
|
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
2269
1872
|
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
1873
|
+
case "ISSUER_BUDGET_EXCEEDED":
|
|
2270
1874
|
return true;
|
|
2271
|
-
// after retryAfter
|
|
2272
1875
|
default:
|
|
2273
1876
|
return false;
|
|
2274
1877
|
}
|
|
2275
1878
|
}
|
|
2276
1879
|
};
|
|
2277
1880
|
|
|
2278
|
-
// src/pafi-backend/pafiBackendClient.ts
|
|
2279
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2280
|
-
var RETRY_DEFAULTS = {
|
|
2281
|
-
maxAttempts: 1,
|
|
2282
|
-
initialDelayMs: 500,
|
|
2283
|
-
maxDelayMs: 1e4,
|
|
2284
|
-
maxRetryAfterMs: 3e4
|
|
2285
|
-
};
|
|
2286
|
-
var PafiBackendClient = class {
|
|
2287
|
-
url;
|
|
2288
|
-
issuerId;
|
|
2289
|
-
apiKey;
|
|
2290
|
-
fetchImpl;
|
|
2291
|
-
timeoutMs;
|
|
2292
|
-
retry;
|
|
2293
|
-
constructor(config) {
|
|
2294
|
-
if (!config.url) {
|
|
2295
|
-
throw new Error("PafiBackendClient: url is required");
|
|
2296
|
-
}
|
|
2297
|
-
if (!config.issuerId) {
|
|
2298
|
-
throw new Error("PafiBackendClient: issuerId is required");
|
|
2299
|
-
}
|
|
2300
|
-
if (!config.apiKey) {
|
|
2301
|
-
throw new Error("PafiBackendClient: apiKey is required");
|
|
2302
|
-
}
|
|
2303
|
-
this.url = config.url.replace(/\/+$/, "");
|
|
2304
|
-
this.issuerId = config.issuerId;
|
|
2305
|
-
this.apiKey = config.apiKey;
|
|
2306
|
-
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
2307
|
-
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2308
|
-
this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
|
|
2309
|
-
if (!this.fetchImpl) {
|
|
2310
|
-
throw new Error(
|
|
2311
|
-
"PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
|
|
2312
|
-
);
|
|
2313
|
-
}
|
|
2314
|
-
if (this.retry.maxAttempts < 1) {
|
|
2315
|
-
throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
/**
|
|
2319
|
-
* Request paymaster sponsorship for a pre-built UserOperation.
|
|
2320
|
-
* See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
|
|
2321
|
-
*
|
|
2322
|
-
* Retries automatically on transient failures (5xx, timeouts, network
|
|
2323
|
-
* errors, and errors the server flags with `safeToRetry: true`) up to
|
|
2324
|
-
* `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
|
|
2325
|
-
*
|
|
2326
|
-
* @throws PafiBackendError on final failure after exhausting retries
|
|
2327
|
-
*/
|
|
2328
|
-
async requestSponsorship(req) {
|
|
2329
|
-
return this.postWithRetry(
|
|
2330
|
-
"/paymaster/sponsor",
|
|
2331
|
-
req
|
|
2332
|
-
);
|
|
2333
|
-
}
|
|
2334
|
-
// -------------------------------------------------------------------------
|
|
2335
|
-
// Internals
|
|
2336
|
-
// -------------------------------------------------------------------------
|
|
2337
|
-
async postWithRetry(path, body) {
|
|
2338
|
-
let lastError;
|
|
2339
|
-
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
2340
|
-
try {
|
|
2341
|
-
return await this.post(path, body);
|
|
2342
|
-
} catch (err) {
|
|
2343
|
-
if (!(err instanceof PafiBackendError)) throw err;
|
|
2344
|
-
lastError = err;
|
|
2345
|
-
const isLastAttempt = attempt >= this.retry.maxAttempts;
|
|
2346
|
-
if (isLastAttempt || !err.safeToRetry) throw err;
|
|
2347
|
-
const delay = this.computeBackoff(attempt, err.retryAfter);
|
|
2348
|
-
if (delay === null) throw err;
|
|
2349
|
-
await this.sleep(delay);
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
throw lastError;
|
|
2353
|
-
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Pick the delay before the next retry.
|
|
2356
|
-
* - If the server sent `retryAfter` (seconds), honor it (capped by
|
|
2357
|
-
* `maxRetryAfterMs`) — returns null if the server wait exceeds the
|
|
2358
|
-
* cap, signalling the caller should give up.
|
|
2359
|
-
* - Otherwise: exponential backoff with ±20% jitter, capped at
|
|
2360
|
-
* `maxDelayMs`.
|
|
2361
|
-
*/
|
|
2362
|
-
computeBackoff(attempt, retryAfter) {
|
|
2363
|
-
if (retryAfter !== void 0) {
|
|
2364
|
-
const serverMs = retryAfter * 1e3;
|
|
2365
|
-
if (serverMs > this.retry.maxRetryAfterMs) return null;
|
|
2366
|
-
return serverMs;
|
|
2367
|
-
}
|
|
2368
|
-
const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
|
|
2369
|
-
const capped = Math.min(exp, this.retry.maxDelayMs);
|
|
2370
|
-
const jitter = capped * (0.8 + Math.random() * 0.4);
|
|
2371
|
-
return Math.round(jitter);
|
|
2372
|
-
}
|
|
2373
|
-
sleep(ms) {
|
|
2374
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2375
|
-
}
|
|
2376
|
-
async post(path, body) {
|
|
2377
|
-
const controller = new AbortController();
|
|
2378
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2379
|
-
let response;
|
|
2380
|
-
try {
|
|
2381
|
-
response = await this.fetchImpl(`${this.url}${path}`, {
|
|
2382
|
-
method: "POST",
|
|
2383
|
-
headers: {
|
|
2384
|
-
"Content-Type": "application/json",
|
|
2385
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
2386
|
-
"X-Issuer-Id": this.issuerId
|
|
2387
|
-
},
|
|
2388
|
-
body: JSON.stringify(body, this.bigintReplacer),
|
|
2389
|
-
signal: controller.signal
|
|
2390
|
-
});
|
|
2391
|
-
} catch (err) {
|
|
2392
|
-
if (err.name === "AbortError") {
|
|
2393
|
-
throw new PafiBackendError(
|
|
2394
|
-
"TIMEOUT",
|
|
2395
|
-
`PAFI Backend request timed out after ${this.timeoutMs}ms`,
|
|
2396
|
-
0
|
|
2397
|
-
);
|
|
2398
|
-
}
|
|
2399
|
-
throw new PafiBackendError(
|
|
2400
|
-
"NETWORK_ERROR",
|
|
2401
|
-
`PAFI Backend unreachable: ${err.message}`,
|
|
2402
|
-
0
|
|
2403
|
-
);
|
|
2404
|
-
} finally {
|
|
2405
|
-
clearTimeout(timeoutId);
|
|
2406
|
-
}
|
|
2407
|
-
const text = await response.text();
|
|
2408
|
-
if (!response.ok) {
|
|
2409
|
-
let code = "INTERNAL_ERROR";
|
|
2410
|
-
let message = text || response.statusText;
|
|
2411
|
-
let details;
|
|
2412
|
-
let retryAfter;
|
|
2413
|
-
let serverSafeToRetry;
|
|
2414
|
-
try {
|
|
2415
|
-
const parsed = JSON.parse(text);
|
|
2416
|
-
code = parsed.code ?? code;
|
|
2417
|
-
message = parsed.message ?? message;
|
|
2418
|
-
details = parsed.details;
|
|
2419
|
-
if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
|
|
2420
|
-
if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
|
|
2421
|
-
} catch {
|
|
2422
|
-
}
|
|
2423
|
-
throw new PafiBackendError(code, message, response.status, details, {
|
|
2424
|
-
...retryAfter !== void 0 ? { retryAfter } : {},
|
|
2425
|
-
...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
|
|
2426
|
-
});
|
|
2427
|
-
}
|
|
2428
|
-
return JSON.parse(text, this.bigintReviver);
|
|
2429
|
-
}
|
|
2430
|
-
/** JSON replacer that stringifies bigints. Paired with bigintReviver. */
|
|
2431
|
-
bigintReplacer = (_key, value) => {
|
|
2432
|
-
return typeof value === "bigint" ? value.toString() : value;
|
|
2433
|
-
};
|
|
2434
|
-
/**
|
|
2435
|
-
* JSON reviver that coerces specific numeric-string fields back to
|
|
2436
|
-
* bigint. The server must send these fields as decimal strings.
|
|
2437
|
-
*/
|
|
2438
|
-
bigintReviver = (key, value) => {
|
|
2439
|
-
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)) {
|
|
2440
|
-
return BigInt(value);
|
|
2441
|
-
}
|
|
2442
|
-
return value;
|
|
2443
|
-
};
|
|
2444
|
-
};
|
|
2445
|
-
|
|
2446
1881
|
// src/config.ts
|
|
2447
|
-
var
|
|
1882
|
+
var import_viem10 = require("viem");
|
|
2448
1883
|
function createIssuerService(config) {
|
|
2449
1884
|
if (!config.provider) {
|
|
2450
1885
|
throw new Error("createIssuerService: provider is required");
|
|
2451
1886
|
}
|
|
2452
|
-
if (!config.
|
|
2453
|
-
throw new Error("createIssuerService:
|
|
2454
|
-
}
|
|
2455
|
-
if (!config.signer) {
|
|
2456
|
-
throw new Error("createIssuerService: signer is required");
|
|
2457
|
-
}
|
|
2458
|
-
if (!config.relayAddress) {
|
|
2459
|
-
throw new Error("createIssuerService: relayAddress is required");
|
|
1887
|
+
if (!config.ledger) {
|
|
1888
|
+
throw new Error("createIssuerService: ledger is required");
|
|
2460
1889
|
}
|
|
2461
1890
|
if (!config.auth?.jwtSecret) {
|
|
2462
1891
|
throw new Error("createIssuerService: auth.jwtSecret is required");
|
|
@@ -2470,8 +1899,8 @@ function createIssuerService(config) {
|
|
|
2470
1899
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
2471
1900
|
);
|
|
2472
1901
|
}
|
|
2473
|
-
const tokenAddresses = rawAddresses.map((a) => (0,
|
|
2474
|
-
const ledger = config.ledger
|
|
1902
|
+
const tokenAddresses = rawAddresses.map((a) => (0, import_viem10.getAddress)(a));
|
|
1903
|
+
const ledger = config.ledger;
|
|
2475
1904
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
2476
1905
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
2477
1906
|
const authServiceConfig = {
|
|
@@ -2484,18 +1913,7 @@ function createIssuerService(config) {
|
|
|
2484
1913
|
authServiceConfig.jwtExpiresIn = config.auth.jwtExpiresIn;
|
|
2485
1914
|
}
|
|
2486
1915
|
const authService = new AuthService(authServiceConfig);
|
|
2487
|
-
const
|
|
2488
|
-
relayAddress: config.relayAddress,
|
|
2489
|
-
operatorWallet: config.operatorWallet,
|
|
2490
|
-
provider: config.provider
|
|
2491
|
-
};
|
|
2492
|
-
if (config.relay?.simulateBeforeSubmit !== void 0) {
|
|
2493
|
-
relayServiceConfig.simulateBeforeSubmit = config.relay.simulateBeforeSubmit;
|
|
2494
|
-
}
|
|
2495
|
-
if (config.relay?.confirmationTimeoutMs !== void 0) {
|
|
2496
|
-
relayServiceConfig.confirmationTimeoutMs = config.relay.confirmationTimeoutMs;
|
|
2497
|
-
}
|
|
2498
|
-
const relayService = new RelayService(relayServiceConfig);
|
|
1916
|
+
const relayService = new RelayService();
|
|
2499
1917
|
let feeManager;
|
|
2500
1918
|
if (config.fee) {
|
|
2501
1919
|
feeManager = new FeeManager({
|
|
@@ -2503,16 +1921,6 @@ function createIssuerService(config) {
|
|
|
2503
1921
|
provider: config.provider
|
|
2504
1922
|
});
|
|
2505
1923
|
}
|
|
2506
|
-
const gatewayConfig = {
|
|
2507
|
-
ledger,
|
|
2508
|
-
policy,
|
|
2509
|
-
signer: config.signer,
|
|
2510
|
-
relayService
|
|
2511
|
-
};
|
|
2512
|
-
if (config.gateway?.defaultLockBufferMs !== void 0) {
|
|
2513
|
-
gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
|
|
2514
|
-
}
|
|
2515
|
-
const gateway = new MintingGateway(gatewayConfig);
|
|
2516
1924
|
const indexers = /* @__PURE__ */ new Map();
|
|
2517
1925
|
for (const tokenAddress of tokenAddresses) {
|
|
2518
1926
|
const indexerConfig = {
|
|
@@ -2540,7 +1948,6 @@ function createIssuerService(config) {
|
|
|
2540
1948
|
const firstIndexer = indexers.get(tokenAddresses[0]);
|
|
2541
1949
|
const handlersConfig = {
|
|
2542
1950
|
authService,
|
|
2543
|
-
gateway,
|
|
2544
1951
|
ledger,
|
|
2545
1952
|
provider: config.provider,
|
|
2546
1953
|
pointTokenAddresses: tokenAddresses,
|
|
@@ -2549,6 +1956,15 @@ function createIssuerService(config) {
|
|
|
2549
1956
|
};
|
|
2550
1957
|
if (feeManager) handlersConfig.feeManager = feeManager;
|
|
2551
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
|
+
}
|
|
2552
1968
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
2553
1969
|
if (config.indexer?.autoStart) {
|
|
2554
1970
|
for (const idx of indexers.values()) {
|
|
@@ -2560,10 +1976,8 @@ function createIssuerService(config) {
|
|
|
2560
1976
|
sessionStore,
|
|
2561
1977
|
ledger,
|
|
2562
1978
|
policy,
|
|
2563
|
-
signer: config.signer,
|
|
2564
1979
|
relayService,
|
|
2565
1980
|
feeManager,
|
|
2566
|
-
gateway,
|
|
2567
1981
|
indexers,
|
|
2568
1982
|
indexer: firstIndexer,
|
|
2569
1983
|
handlers
|
|
@@ -2582,18 +1996,13 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
2582
1996
|
FeeManager,
|
|
2583
1997
|
InMemoryCursorStore,
|
|
2584
1998
|
IssuerApiHandlers,
|
|
2585
|
-
MemoryPointLedger,
|
|
2586
1999
|
MemorySessionStore,
|
|
2587
|
-
MintingGateway,
|
|
2588
|
-
MintingGatewayError,
|
|
2589
2000
|
NonceManager,
|
|
2590
2001
|
PAFI_ISSUER_SDK_VERSION,
|
|
2591
2002
|
PTRedeemError,
|
|
2592
2003
|
PTRedeemHandler,
|
|
2593
|
-
PafiBackendClient,
|
|
2594
2004
|
PafiBackendError,
|
|
2595
2005
|
PointIndexer,
|
|
2596
|
-
PrivateKeySigner,
|
|
2597
2006
|
RelayError,
|
|
2598
2007
|
RelayService,
|
|
2599
2008
|
TopUpRedemptionError,
|
|
@@ -2601,7 +2010,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
2601
2010
|
authenticateRequest,
|
|
2602
2011
|
createIssuerService,
|
|
2603
2012
|
createSubgraphNativeUsdtQuoter,
|
|
2604
|
-
createSubgraphPoolsProvider
|
|
2605
|
-
encodeExtData
|
|
2013
|
+
createSubgraphPoolsProvider
|
|
2606
2014
|
});
|
|
2607
2015
|
//# sourceMappingURL=index.cjs.map
|