@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.js
CHANGED
|
@@ -1,201 +1,3 @@
|
|
|
1
|
-
// src/ledger/memoryLedger.ts
|
|
2
|
-
import { getAddress } from "viem";
|
|
3
|
-
var MemoryPointLedger = class {
|
|
4
|
-
balances = /* @__PURE__ */ new Map();
|
|
5
|
-
locks = /* @__PURE__ */ new Map();
|
|
6
|
-
nextLockId = 1;
|
|
7
|
-
now;
|
|
8
|
-
constructor(opts = {}) {
|
|
9
|
-
this.now = opts.now ?? (() => Date.now());
|
|
10
|
-
}
|
|
11
|
-
// -------------------------------------------------------------------------
|
|
12
|
-
// Read
|
|
13
|
-
// -------------------------------------------------------------------------
|
|
14
|
-
async getBalance(userAddress, tokenAddress) {
|
|
15
|
-
const user = getAddress(userAddress);
|
|
16
|
-
const token = normalizeToken(tokenAddress);
|
|
17
|
-
this.purgeExpired();
|
|
18
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
19
|
-
const locked = this.lockedTotalFor(user, token);
|
|
20
|
-
return total - locked;
|
|
21
|
-
}
|
|
22
|
-
async getLockedRequests(userAddress, tokenAddress) {
|
|
23
|
-
const user = getAddress(userAddress);
|
|
24
|
-
const token = normalizeToken(tokenAddress);
|
|
25
|
-
this.purgeExpired();
|
|
26
|
-
const out = [];
|
|
27
|
-
for (const lock of this.locks.values()) {
|
|
28
|
-
if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
29
|
-
out.push({ ...lock });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
// -------------------------------------------------------------------------
|
|
35
|
-
// Write
|
|
36
|
-
// -------------------------------------------------------------------------
|
|
37
|
-
async creditBalance(userAddress, amount, _reason, tokenAddress) {
|
|
38
|
-
if (amount <= 0n) {
|
|
39
|
-
throw new Error("MemoryPointLedger: credit amount must be positive");
|
|
40
|
-
}
|
|
41
|
-
const user = getAddress(userAddress);
|
|
42
|
-
const token = normalizeToken(tokenAddress);
|
|
43
|
-
const key = balanceKey(user, token);
|
|
44
|
-
const current = this.balances.get(key) ?? 0n;
|
|
45
|
-
this.balances.set(key, current + amount);
|
|
46
|
-
}
|
|
47
|
-
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
48
|
-
if (amount <= 0n) {
|
|
49
|
-
throw new Error("MemoryPointLedger: lock amount must be positive");
|
|
50
|
-
}
|
|
51
|
-
if (lockDurationMs <= 0) {
|
|
52
|
-
throw new Error("MemoryPointLedger: lockDurationMs must be positive");
|
|
53
|
-
}
|
|
54
|
-
const user = getAddress(userAddress);
|
|
55
|
-
const token = normalizeToken(tokenAddress);
|
|
56
|
-
this.purgeExpired();
|
|
57
|
-
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
58
|
-
const alreadyLocked = this.lockedTotalFor(user, token);
|
|
59
|
-
const available = total - alreadyLocked;
|
|
60
|
-
if (available < amount) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const lockId = `lock-${this.nextLockId++}`;
|
|
66
|
-
const now = this.now();
|
|
67
|
-
const lock = {
|
|
68
|
-
lockId,
|
|
69
|
-
userAddress: user,
|
|
70
|
-
amount,
|
|
71
|
-
status: "PENDING",
|
|
72
|
-
createdAt: now,
|
|
73
|
-
expiresAt: now + lockDurationMs
|
|
74
|
-
};
|
|
75
|
-
if (tokenAddress !== void 0) {
|
|
76
|
-
lock.tokenAddress = getAddress(tokenAddress);
|
|
77
|
-
}
|
|
78
|
-
this.locks.set(lockId, lock);
|
|
79
|
-
return lockId;
|
|
80
|
-
}
|
|
81
|
-
async releaseLock(lockId) {
|
|
82
|
-
const lock = this.locks.get(lockId);
|
|
83
|
-
if (!lock) return;
|
|
84
|
-
if (lock.status === "PENDING") {
|
|
85
|
-
this.locks.delete(lockId);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
89
|
-
if (amount <= 0n) {
|
|
90
|
-
throw new Error("MemoryPointLedger: deduct amount must be positive");
|
|
91
|
-
}
|
|
92
|
-
const user = getAddress(userAddress);
|
|
93
|
-
const token = normalizeToken(tokenAddress);
|
|
94
|
-
const key = balanceKey(user, token);
|
|
95
|
-
const current = this.balances.get(key) ?? 0n;
|
|
96
|
-
if (current < amount) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
this.balances.set(key, current - amount);
|
|
102
|
-
for (const lock of this.locks.values()) {
|
|
103
|
-
if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
104
|
-
lock.status = "MINTED";
|
|
105
|
-
lock.txHash = txHash;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async updateMintStatus(lockId, status, txHash) {
|
|
111
|
-
const lock = this.locks.get(lockId);
|
|
112
|
-
if (!lock) {
|
|
113
|
-
throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
|
|
114
|
-
}
|
|
115
|
-
lock.status = status;
|
|
116
|
-
if (txHash) lock.txHash = txHash;
|
|
117
|
-
}
|
|
118
|
-
// -------------------------------------------------------------------------
|
|
119
|
-
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
120
|
-
// -------------------------------------------------------------------------
|
|
121
|
-
pendingCredits = /* @__PURE__ */ new Map();
|
|
122
|
-
nextCreditId = 1;
|
|
123
|
-
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
124
|
-
if (amount <= 0n) {
|
|
125
|
-
throw new Error(
|
|
126
|
-
"MemoryPointLedger: pending credit amount must be positive"
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
if (durationMs <= 0) {
|
|
130
|
-
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
131
|
-
}
|
|
132
|
-
const user = getAddress(userAddress);
|
|
133
|
-
const lockId = `credit-${this.nextCreditId++}`;
|
|
134
|
-
const now = this.now();
|
|
135
|
-
this.pendingCredits.set(lockId, {
|
|
136
|
-
lockId,
|
|
137
|
-
userAddress: user,
|
|
138
|
-
amount,
|
|
139
|
-
tokenAddress: tokenAddress !== void 0 ? getAddress(tokenAddress) : void 0,
|
|
140
|
-
createdAt: now,
|
|
141
|
-
expiresAt: now + durationMs,
|
|
142
|
-
status: "PENDING"
|
|
143
|
-
});
|
|
144
|
-
return lockId;
|
|
145
|
-
}
|
|
146
|
-
async resolveCreditByBurnTx(lockId, txHash) {
|
|
147
|
-
const credit = this.pendingCredits.get(lockId);
|
|
148
|
-
if (!credit) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
if (credit.status === "RESOLVED") {
|
|
154
|
-
if (credit.txHash === txHash) return;
|
|
155
|
-
throw new Error(
|
|
156
|
-
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
const token = normalizeToken(credit.tokenAddress);
|
|
160
|
-
const key = balanceKey(credit.userAddress, token);
|
|
161
|
-
const current = this.balances.get(key) ?? 0n;
|
|
162
|
-
this.balances.set(key, current + credit.amount);
|
|
163
|
-
credit.status = "RESOLVED";
|
|
164
|
-
credit.txHash = txHash;
|
|
165
|
-
}
|
|
166
|
-
// -------------------------------------------------------------------------
|
|
167
|
-
// Internal helpers
|
|
168
|
-
// -------------------------------------------------------------------------
|
|
169
|
-
/**
|
|
170
|
-
* Auto-expire any PENDING lock past its expiry. Called lazily on every
|
|
171
|
-
* read/write so the in-memory state stays self-cleaning without a timer.
|
|
172
|
-
*/
|
|
173
|
-
purgeExpired() {
|
|
174
|
-
const now = this.now();
|
|
175
|
-
for (const lock of this.locks.values()) {
|
|
176
|
-
if (lock.status === "PENDING" && lock.expiresAt <= now) {
|
|
177
|
-
lock.status = "EXPIRED";
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
lockedTotalFor(userAddress, tokenKey) {
|
|
182
|
-
let total = 0n;
|
|
183
|
-
for (const lock of this.locks.values()) {
|
|
184
|
-
if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
|
|
185
|
-
total += lock.amount;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return total;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
var DEFAULT_TOKEN_KEY = "default";
|
|
192
|
-
function normalizeToken(tokenAddress) {
|
|
193
|
-
return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : getAddress(tokenAddress);
|
|
194
|
-
}
|
|
195
|
-
function balanceKey(user, tokenKey) {
|
|
196
|
-
return `${user}|${tokenKey}`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
1
|
// src/policy/defaultPolicy.ts
|
|
200
2
|
var DefaultPolicyEngine = class {
|
|
201
3
|
ledger;
|
|
@@ -211,6 +13,13 @@ var DefaultPolicyEngine = class {
|
|
|
211
13
|
}
|
|
212
14
|
if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
|
|
213
15
|
if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
|
|
16
|
+
if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
|
|
17
|
+
if (process.env.NODE_ENV === "production") {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
214
23
|
}
|
|
215
24
|
async evaluate(request) {
|
|
216
25
|
if (request.amount <= 0n) {
|
|
@@ -247,34 +56,9 @@ var DefaultPolicyEngine = class {
|
|
|
247
56
|
}
|
|
248
57
|
};
|
|
249
58
|
|
|
250
|
-
// src/signer/privateKeySigner.ts
|
|
251
|
-
import { createWalletClient, http } from "viem";
|
|
252
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
253
|
-
import {
|
|
254
|
-
signMintRequest
|
|
255
|
-
} from "@pafi-dev/core";
|
|
256
|
-
var PrivateKeySigner = class {
|
|
257
|
-
account;
|
|
258
|
-
walletClient;
|
|
259
|
-
constructor(opts) {
|
|
260
|
-
this.account = privateKeyToAccount(opts.privateKey);
|
|
261
|
-
this.walletClient = createWalletClient({
|
|
262
|
-
account: this.account,
|
|
263
|
-
chain: opts.chain,
|
|
264
|
-
transport: http(opts.rpcUrl)
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
async signMintRequest(domain, message) {
|
|
268
|
-
return signMintRequest(this.walletClient, domain, message);
|
|
269
|
-
}
|
|
270
|
-
async getAddress() {
|
|
271
|
-
return this.account.address;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
59
|
// src/auth/memorySessionStore.ts
|
|
276
60
|
import { randomBytes } from "crypto";
|
|
277
|
-
import { getAddress
|
|
61
|
+
import { getAddress } from "viem";
|
|
278
62
|
var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
279
63
|
var MemorySessionStore = class {
|
|
280
64
|
nonces = /* @__PURE__ */ new Map();
|
|
@@ -284,6 +68,11 @@ var MemorySessionStore = class {
|
|
|
284
68
|
nonceTtlMs;
|
|
285
69
|
now;
|
|
286
70
|
constructor(opts = {}) {
|
|
71
|
+
if (process.env.NODE_ENV === "production") {
|
|
72
|
+
console.error(
|
|
73
|
+
"[PAFI] MemorySessionStore is not safe for multi-process/K8s deployments. Session revocations are NOT propagated across pods. Use a Redis-backed session store in production."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
287
76
|
this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
|
|
288
77
|
this.now = opts.now ?? (() => Date.now());
|
|
289
78
|
}
|
|
@@ -310,7 +99,7 @@ var MemorySessionStore = class {
|
|
|
310
99
|
this.purgeExpiredSessions();
|
|
311
100
|
const normalized = {
|
|
312
101
|
...session,
|
|
313
|
-
userAddress:
|
|
102
|
+
userAddress: getAddress(session.userAddress)
|
|
314
103
|
};
|
|
315
104
|
this.sessions.set(session.tokenId, normalized);
|
|
316
105
|
}
|
|
@@ -328,7 +117,7 @@ var MemorySessionStore = class {
|
|
|
328
117
|
this.sessions.delete(tokenId);
|
|
329
118
|
}
|
|
330
119
|
async revokeAllSessions(userAddress) {
|
|
331
|
-
const key =
|
|
120
|
+
const key = getAddress(userAddress);
|
|
332
121
|
for (const [tokenId, session] of this.sessions.entries()) {
|
|
333
122
|
if (session.userAddress === key) {
|
|
334
123
|
this.sessions.delete(tokenId);
|
|
@@ -374,7 +163,7 @@ var NonceManager = class {
|
|
|
374
163
|
// src/auth/loginVerifier.ts
|
|
375
164
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
376
165
|
import { SignJWT, jwtVerify, errors as joseErrors } from "jose";
|
|
377
|
-
import { getAddress as
|
|
166
|
+
import { getAddress as getAddress2 } from "viem";
|
|
378
167
|
import { parseLoginMessage, verifyLoginMessage } from "@pafi-dev/core";
|
|
379
168
|
|
|
380
169
|
// src/auth/errors.ts
|
|
@@ -398,8 +187,8 @@ var AuthService = class {
|
|
|
398
187
|
nonceManager;
|
|
399
188
|
now;
|
|
400
189
|
constructor(config) {
|
|
401
|
-
if (!config.jwtSecret || config.jwtSecret.length <
|
|
402
|
-
throw new Error("AuthService: jwtSecret must be at least
|
|
190
|
+
if (!config.jwtSecret || config.jwtSecret.length < 32) {
|
|
191
|
+
throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
|
|
403
192
|
}
|
|
404
193
|
this.sessionStore = config.sessionStore;
|
|
405
194
|
this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
|
|
@@ -428,6 +217,12 @@ var AuthService = class {
|
|
|
428
217
|
const msg = err instanceof Error ? err.message : String(err);
|
|
429
218
|
throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
|
|
430
219
|
}
|
|
220
|
+
if (parsed.expirationTime == null) {
|
|
221
|
+
throw new AuthError(
|
|
222
|
+
"INVALID_MESSAGE",
|
|
223
|
+
"login message must include expirationTime"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
431
226
|
if (parsed.domain !== this.domain) {
|
|
432
227
|
throw new AuthError(
|
|
433
228
|
"DOMAIN_MISMATCH",
|
|
@@ -464,7 +259,7 @@ var AuthService = class {
|
|
|
464
259
|
"Nonce is unknown, expired, or already used"
|
|
465
260
|
);
|
|
466
261
|
}
|
|
467
|
-
const userAddress =
|
|
262
|
+
const userAddress = getAddress2(verifyResult.address);
|
|
468
263
|
const tokenId = randomBytes2(16).toString("hex");
|
|
469
264
|
const issuedAt = now;
|
|
470
265
|
const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
|
|
@@ -492,7 +287,11 @@ var AuthService = class {
|
|
|
492
287
|
if (payload.jti) {
|
|
493
288
|
await this.sessionStore.revokeSession(payload.jti);
|
|
494
289
|
}
|
|
495
|
-
} catch {
|
|
290
|
+
} catch (err) {
|
|
291
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
292
|
+
if (!msg.includes("not found") && !msg.includes("expired")) {
|
|
293
|
+
console.error("[PAFI] AuthService logout: session store error", err);
|
|
294
|
+
}
|
|
496
295
|
}
|
|
497
296
|
}
|
|
498
297
|
/**
|
|
@@ -531,7 +330,7 @@ var AuthService = class {
|
|
|
531
330
|
throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
|
|
532
331
|
}
|
|
533
332
|
return {
|
|
534
|
-
userAddress:
|
|
333
|
+
userAddress: getAddress2(userAddress),
|
|
535
334
|
chainId,
|
|
536
335
|
tokenId
|
|
537
336
|
};
|
|
@@ -582,209 +381,120 @@ var RelayError = class extends Error {
|
|
|
582
381
|
};
|
|
583
382
|
|
|
584
383
|
// src/relay/relayService.ts
|
|
585
|
-
import { encodeFunctionData } from "viem";
|
|
586
384
|
import {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
RELAYER_V2_ABI,
|
|
385
|
+
encodeFunctionData,
|
|
386
|
+
erc20Abi
|
|
387
|
+
} from "viem";
|
|
388
|
+
import {
|
|
592
389
|
POINT_TOKEN_V2_ABI,
|
|
593
|
-
buildPartialUserOperation
|
|
390
|
+
buildPartialUserOperation,
|
|
391
|
+
signMintRequest
|
|
594
392
|
} from "@pafi-dev/core";
|
|
595
|
-
var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
596
393
|
var RelayService = class {
|
|
597
|
-
relayAddress;
|
|
598
|
-
operatorWallet;
|
|
599
|
-
provider;
|
|
600
|
-
confirmationTimeoutMs;
|
|
601
|
-
simulateBeforeSubmit;
|
|
602
|
-
constructor(config) {
|
|
603
|
-
if (!config.relayAddress) {
|
|
604
|
-
throw new Error("RelayService: relayAddress is required");
|
|
605
|
-
}
|
|
606
|
-
if (!config.operatorWallet) {
|
|
607
|
-
throw new Error("RelayService: operatorWallet is required");
|
|
608
|
-
}
|
|
609
|
-
this.relayAddress = config.relayAddress;
|
|
610
|
-
this.operatorWallet = config.operatorWallet;
|
|
611
|
-
if (config.provider) this.provider = config.provider;
|
|
612
|
-
this.confirmationTimeoutMs = config.confirmationTimeoutMs ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
|
|
613
|
-
this.simulateBeforeSubmit = config.simulateBeforeSubmit ?? config.provider !== void 0;
|
|
614
|
-
}
|
|
615
|
-
/** Address the operator wallet is broadcasting from (for logging). */
|
|
616
|
-
operatorAddress() {
|
|
617
|
-
return this.operatorWallet.account?.address;
|
|
618
|
-
}
|
|
619
394
|
/**
|
|
620
|
-
* Build
|
|
621
|
-
*
|
|
622
|
-
* for audit before broadcasting.
|
|
395
|
+
* Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
|
|
396
|
+
* `PointToken.mint(to, amount, deadline, minterSig)`.
|
|
623
397
|
*/
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return encodeMintAndSwap(params.mint, params.swap);
|
|
627
|
-
} catch (err) {
|
|
398
|
+
async prepareMint(params) {
|
|
399
|
+
if (!params.batchExecutorAddress) {
|
|
628
400
|
throw new RelayError(
|
|
629
401
|
"ENCODE_FAILED",
|
|
630
|
-
|
|
631
|
-
err
|
|
402
|
+
"prepareMint: batchExecutorAddress required"
|
|
632
403
|
);
|
|
633
404
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
* Submit a `mintAndSwap` transaction. Flow:
|
|
637
|
-
*
|
|
638
|
-
* 1. (optional) pre-flight simulate via provider
|
|
639
|
-
* 2. writeContract through the operator wallet
|
|
640
|
-
* 3. (optional) wait for the receipt and surface gasUsed / status
|
|
641
|
-
*
|
|
642
|
-
* Throws a typed `RelayError` on any failure so the MintingGateway can
|
|
643
|
-
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
644
|
-
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
645
|
-
* need manual review because the tx may still land).
|
|
646
|
-
*
|
|
647
|
-
* @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
|
|
648
|
-
* `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
|
|
649
|
-
* still needs to finalize Relayer v2 ABI before the replacements
|
|
650
|
-
* can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
|
|
651
|
-
*/
|
|
652
|
-
async submitMintAndSwap(params) {
|
|
653
|
-
if (this.simulateBeforeSubmit && this.provider) {
|
|
654
|
-
const operatorAddr = this.operatorWallet.account?.address;
|
|
655
|
-
if (operatorAddr) {
|
|
656
|
-
try {
|
|
657
|
-
await coreSimulateMintAndSwap(
|
|
658
|
-
this.provider,
|
|
659
|
-
this.relayAddress,
|
|
660
|
-
params.mint,
|
|
661
|
-
params.swap,
|
|
662
|
-
operatorAddr
|
|
663
|
-
);
|
|
664
|
-
} catch (err) {
|
|
665
|
-
const reason = err instanceof SimulationError ? err.reason : errorMessage(err);
|
|
666
|
-
throw new RelayError(
|
|
667
|
-
"SIMULATION_FAILED",
|
|
668
|
-
`mintAndSwap would revert: ${reason}`,
|
|
669
|
-
err
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
405
|
+
if (!params.userAddress) {
|
|
406
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
|
|
673
407
|
}
|
|
674
|
-
|
|
675
|
-
try {
|
|
676
|
-
txHash = await this.operatorWallet.writeContract({
|
|
677
|
-
address: this.relayAddress,
|
|
678
|
-
abi: relayAbi,
|
|
679
|
-
functionName: "mintAndSwap",
|
|
680
|
-
args: [params.mint, params.swap],
|
|
681
|
-
...this.operatorWallet.account ? { account: this.operatorWallet.account } : {}
|
|
682
|
-
});
|
|
683
|
-
} catch (err) {
|
|
408
|
+
if (!params.pointTokenAddress) {
|
|
684
409
|
throw new RelayError(
|
|
685
|
-
"
|
|
686
|
-
|
|
687
|
-
err
|
|
410
|
+
"ENCODE_FAILED",
|
|
411
|
+
"prepareMint: pointTokenAddress required"
|
|
688
412
|
);
|
|
689
413
|
}
|
|
690
|
-
if (
|
|
691
|
-
|
|
414
|
+
if (params.amount <= 0n) {
|
|
415
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
|
|
692
416
|
}
|
|
693
|
-
|
|
694
|
-
const receipt = await this.provider.waitForTransactionReceipt({
|
|
695
|
-
hash: txHash,
|
|
696
|
-
timeout: this.confirmationTimeoutMs
|
|
697
|
-
});
|
|
698
|
-
if (receipt.status !== "success") {
|
|
699
|
-
throw new RelayError(
|
|
700
|
-
"TX_REVERTED",
|
|
701
|
-
`mintAndSwap reverted on-chain (tx=${txHash})`
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
return {
|
|
705
|
-
txHash,
|
|
706
|
-
blockNumber: receipt.blockNumber,
|
|
707
|
-
gasUsed: receipt.gasUsed,
|
|
708
|
-
status: receipt.status
|
|
709
|
-
};
|
|
710
|
-
} catch (err) {
|
|
711
|
-
if (err instanceof RelayError) throw err;
|
|
417
|
+
if (!params.issuerSignerWallet) {
|
|
712
418
|
throw new RelayError(
|
|
713
|
-
"
|
|
714
|
-
|
|
715
|
-
err
|
|
419
|
+
"ENCODE_FAILED",
|
|
420
|
+
"prepareMint: issuerSignerWallet required (for MintRequest EIP-712 signature)"
|
|
716
421
|
);
|
|
717
422
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
// v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
|
|
721
|
-
// ==========================================================================
|
|
722
|
-
//
|
|
723
|
-
// These two methods build unsigned `PartialUserOperation` payloads for
|
|
724
|
-
// the Frontend to sign (via Privy) and submit to the Bundler. The
|
|
725
|
-
// Issuer Backend no longer broadcasts — that's the Frontend's job.
|
|
726
|
-
//
|
|
727
|
-
// Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
|
|
728
|
-
// When SC delivers real ABIs, the imports swap but these method bodies
|
|
729
|
-
// stay the same (calldata encoder is ABI-driven).
|
|
730
|
-
// ==========================================================================
|
|
731
|
-
/**
|
|
732
|
-
* Build an unsigned UserOp for Scenario 1 (Mint).
|
|
733
|
-
*
|
|
734
|
-
* Flow:
|
|
735
|
-
* 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
|
|
736
|
-
* 2. Optionally append a PT fee transfer from user → feeRecipient
|
|
737
|
-
* (fee recovery happens on-chain via BatchExecutor, not via an
|
|
738
|
-
* operator wallet)
|
|
739
|
-
* 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
|
|
740
|
-
* 4. Return a `PartialUserOperation` ready for:
|
|
741
|
-
* - gas estimation (Bundler)
|
|
742
|
-
* - paymaster sponsorship (PAFI Backend)
|
|
743
|
-
* - user signature (Privy)
|
|
744
|
-
*/
|
|
745
|
-
prepareMint(params) {
|
|
746
|
-
if (!params.relayerAddress) {
|
|
747
|
-
throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
|
|
423
|
+
if (params.deadline <= 0n) {
|
|
424
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
|
|
748
425
|
}
|
|
749
|
-
|
|
426
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
427
|
+
if (params.deadline <= nowSecs) {
|
|
428
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
|
|
429
|
+
}
|
|
430
|
+
const MAX_DEADLINE_WINDOW = 3600n;
|
|
431
|
+
if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
|
|
750
432
|
throw new RelayError(
|
|
751
433
|
"ENCODE_FAILED",
|
|
752
|
-
"prepareMint:
|
|
434
|
+
"prepareMint: deadline exceeds maximum allowed window (1 hour)"
|
|
753
435
|
);
|
|
754
436
|
}
|
|
755
|
-
|
|
756
|
-
|
|
437
|
+
let minterSig;
|
|
438
|
+
try {
|
|
439
|
+
const sig = await signMintRequest(
|
|
440
|
+
params.issuerSignerWallet,
|
|
441
|
+
params.domain,
|
|
442
|
+
{
|
|
443
|
+
to: params.userAddress,
|
|
444
|
+
amount: params.amount,
|
|
445
|
+
nonce: params.mintRequestNonce,
|
|
446
|
+
deadline: params.deadline
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
minterSig = sig.serialized;
|
|
450
|
+
} catch (err) {
|
|
451
|
+
throw new RelayError(
|
|
452
|
+
"ENCODE_FAILED",
|
|
453
|
+
`prepareMint: failed to sign MintRequest: ${errorMessage(err)}`,
|
|
454
|
+
err
|
|
455
|
+
);
|
|
757
456
|
}
|
|
758
457
|
let mintCallData;
|
|
759
458
|
try {
|
|
760
459
|
mintCallData = encodeFunctionData({
|
|
761
|
-
abi:
|
|
460
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
762
461
|
functionName: "mint",
|
|
763
|
-
args: [params.
|
|
462
|
+
args: [params.userAddress, params.amount, params.deadline, minterSig]
|
|
764
463
|
});
|
|
765
464
|
} catch (err) {
|
|
766
465
|
throw new RelayError(
|
|
767
466
|
"ENCODE_FAILED",
|
|
768
|
-
`prepareMint: failed to encode
|
|
467
|
+
`prepareMint: failed to encode PointToken.mint: ${errorMessage(err)}`,
|
|
769
468
|
err
|
|
770
469
|
);
|
|
771
470
|
}
|
|
772
471
|
const operations = [
|
|
773
472
|
{
|
|
774
|
-
target: params.
|
|
473
|
+
target: params.pointTokenAddress,
|
|
775
474
|
value: 0n,
|
|
776
475
|
data: mintCallData
|
|
777
476
|
}
|
|
778
477
|
];
|
|
779
|
-
if (params.
|
|
478
|
+
if (params.feeAmount && params.feeAmount > 0n) {
|
|
479
|
+
if (!params.feeRecipient) {
|
|
480
|
+
throw new RelayError(
|
|
481
|
+
"ENCODE_FAILED",
|
|
482
|
+
"prepareMint: feeRecipient required when feeAmount > 0"
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
|
|
486
|
+
throw new RelayError(
|
|
487
|
+
"ENCODE_FAILED",
|
|
488
|
+
"prepareMint: feeRecipient must not be zero address"
|
|
489
|
+
);
|
|
490
|
+
}
|
|
780
491
|
operations.push({
|
|
781
492
|
target: params.pointTokenAddress,
|
|
782
493
|
value: 0n,
|
|
783
494
|
data: encodeFunctionData({
|
|
784
|
-
abi:
|
|
785
|
-
functionName: "
|
|
786
|
-
|
|
787
|
-
args: [params.mintRequest.feeRecipient]
|
|
495
|
+
abi: erc20Abi,
|
|
496
|
+
functionName: "transfer",
|
|
497
|
+
args: [params.feeRecipient, params.feeAmount]
|
|
788
498
|
})
|
|
789
499
|
});
|
|
790
500
|
}
|
|
@@ -793,7 +503,7 @@ var RelayService = class {
|
|
|
793
503
|
nonce: params.aaNonce,
|
|
794
504
|
operations,
|
|
795
505
|
gasLimits: {
|
|
796
|
-
callGasLimit: params.callGasLimit ??
|
|
506
|
+
callGasLimit: params.callGasLimit ?? 300000n,
|
|
797
507
|
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
798
508
|
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
799
509
|
}
|
|
@@ -803,13 +513,12 @@ var RelayService = class {
|
|
|
803
513
|
* Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
|
|
804
514
|
*
|
|
805
515
|
* Two modes:
|
|
806
|
-
* - `mode: 'burn'` — direct `PointToken.burn(amount)`;
|
|
807
|
-
* via EIP-7702
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
* contract has to do it on-chain
|
|
516
|
+
* - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
|
|
517
|
+
* usable if the caller (via EIP-7702) is whitelisted as a burner.
|
|
518
|
+
* Rare in v1.4; kept for admin/operator tools.
|
|
519
|
+
* - `mode: 'burnWithSig'` — `PointToken.burn(from, amount, deadline,
|
|
520
|
+
* burnerSig)`. Caller provides a pre-signed `BurnRequest` + sig
|
|
521
|
+
* bytes (typically from `PTRedeemHandler`).
|
|
813
522
|
*/
|
|
814
523
|
prepareBurn(params) {
|
|
815
524
|
if (!params.pointTokenAddress) {
|
|
@@ -824,19 +533,24 @@ var RelayService = class {
|
|
|
824
533
|
let burnCallData;
|
|
825
534
|
try {
|
|
826
535
|
if (params.mode === "burnWithSig") {
|
|
827
|
-
if (!params.
|
|
828
|
-
throw new Error("burnWithSig requires
|
|
536
|
+
if (!params.burnRequest || !params.burnerSignature) {
|
|
537
|
+
throw new Error("burnWithSig requires burnRequest + burnerSignature");
|
|
829
538
|
}
|
|
830
539
|
burnCallData = encodeFunctionData({
|
|
831
540
|
abi: POINT_TOKEN_V2_ABI,
|
|
832
|
-
functionName: "
|
|
833
|
-
args: [
|
|
541
|
+
functionName: "burn",
|
|
542
|
+
args: [
|
|
543
|
+
params.burnRequest.from,
|
|
544
|
+
params.burnRequest.amount,
|
|
545
|
+
params.burnRequest.deadline,
|
|
546
|
+
params.burnerSignature
|
|
547
|
+
]
|
|
834
548
|
});
|
|
835
549
|
} else {
|
|
836
550
|
burnCallData = encodeFunctionData({
|
|
837
551
|
abi: POINT_TOKEN_V2_ABI,
|
|
838
552
|
functionName: "burn",
|
|
839
|
-
args: [params.amount]
|
|
553
|
+
args: [params.userAddress, params.amount]
|
|
840
554
|
});
|
|
841
555
|
}
|
|
842
556
|
} catch (err) {
|
|
@@ -872,11 +586,14 @@ function errorMessage(err) {
|
|
|
872
586
|
// src/relay/feeManager.ts
|
|
873
587
|
var DEFAULT_GAS_UNITS = 500000n;
|
|
874
588
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
875
|
-
var FeeManager = class {
|
|
589
|
+
var FeeManager = class _FeeManager {
|
|
876
590
|
provider;
|
|
877
591
|
gasUnits;
|
|
878
592
|
gasPremiumBps;
|
|
879
593
|
quoteNativeToFee;
|
|
594
|
+
cachedFee = null;
|
|
595
|
+
cacheExpiresAt = 0;
|
|
596
|
+
static CACHE_TTL_MS = 1e4;
|
|
880
597
|
constructor(config) {
|
|
881
598
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
882
599
|
if (!config.quoteNativeToFee)
|
|
@@ -899,262 +616,20 @@ var FeeManager = class {
|
|
|
899
616
|
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
900
617
|
*/
|
|
901
618
|
async estimateGasFee() {
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
if (this.cachedFee !== null && now < this.cacheExpiresAt) {
|
|
621
|
+
return this.cachedFee;
|
|
622
|
+
}
|
|
902
623
|
const gasPrice = await this.provider.getGasPrice();
|
|
903
624
|
const nativeCost = gasPrice * this.gasUnits;
|
|
904
625
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
905
|
-
|
|
626
|
+
const fee = await this.quoteNativeToFee(withPremium);
|
|
627
|
+
this.cachedFee = fee;
|
|
628
|
+
this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
|
|
629
|
+
return fee;
|
|
906
630
|
}
|
|
907
631
|
};
|
|
908
632
|
|
|
909
|
-
// src/gateway/types.ts
|
|
910
|
-
var MintingGatewayError = class extends Error {
|
|
911
|
-
code;
|
|
912
|
-
/**
|
|
913
|
-
* True if the ledger lock was released before this error was thrown,
|
|
914
|
-
* meaning the user can safely retry. False means the funds are still
|
|
915
|
-
* locked (e.g. tx may still land on-chain) and retry would double-spend.
|
|
916
|
-
*/
|
|
917
|
-
safeToRetry;
|
|
918
|
-
cause;
|
|
919
|
-
constructor(code, message, opts) {
|
|
920
|
-
super(message);
|
|
921
|
-
this.name = "MintingGatewayError";
|
|
922
|
-
this.code = code;
|
|
923
|
-
this.safeToRetry = opts.safeToRetry;
|
|
924
|
-
if (opts.cause !== void 0) this.cause = opts.cause;
|
|
925
|
-
}
|
|
926
|
-
};
|
|
927
|
-
|
|
928
|
-
// src/gateway/mintingGateway.ts
|
|
929
|
-
import {
|
|
930
|
-
verifyReceiverConsent,
|
|
931
|
-
encodeExtData
|
|
932
|
-
} from "@pafi-dev/core";
|
|
933
|
-
var DEFAULT_LOCK_BUFFER_MS = 6e4;
|
|
934
|
-
var MintingGateway = class {
|
|
935
|
-
ledger;
|
|
936
|
-
policy;
|
|
937
|
-
signer;
|
|
938
|
-
relayService;
|
|
939
|
-
now;
|
|
940
|
-
defaultLockBufferMs;
|
|
941
|
-
constructor(config) {
|
|
942
|
-
if (!config.ledger) throw new Error("MintingGateway: ledger required");
|
|
943
|
-
if (!config.policy) throw new Error("MintingGateway: policy required");
|
|
944
|
-
if (!config.signer) throw new Error("MintingGateway: signer required");
|
|
945
|
-
if (!config.relayService)
|
|
946
|
-
throw new Error("MintingGateway: relayService required");
|
|
947
|
-
this.ledger = config.ledger;
|
|
948
|
-
this.policy = config.policy;
|
|
949
|
-
this.signer = config.signer;
|
|
950
|
-
this.relayService = config.relayService;
|
|
951
|
-
this.now = config.now ?? (() => Date.now());
|
|
952
|
-
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* @deprecated Since 0.3.0 — will be renamed to `processMint()` once
|
|
956
|
-
* the SC team finalizes Relayer v2 ABI. The new flow drops the
|
|
957
|
-
* swap steps entirely (no more single-call mint+swap); users swap
|
|
958
|
-
* separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
|
|
959
|
-
*/
|
|
960
|
-
async processMintAndCashOut(request) {
|
|
961
|
-
const { receiverConsent, receiverSignature } = request;
|
|
962
|
-
if (!receiverConsent || !receiverSignature) {
|
|
963
|
-
throw new MintingGatewayError(
|
|
964
|
-
"INVALID_REQUEST",
|
|
965
|
-
"receiverConsent and receiverSignature are required",
|
|
966
|
-
{ safeToRetry: true }
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
if (receiverConsent.amount <= 0n) {
|
|
970
|
-
throw new MintingGatewayError(
|
|
971
|
-
"INVALID_REQUEST",
|
|
972
|
-
"consent amount must be positive",
|
|
973
|
-
{ safeToRetry: true }
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
if (receiverConsent.originalReceiver !== request.userAddress) {
|
|
977
|
-
throw new MintingGatewayError(
|
|
978
|
-
"INVALID_REQUEST",
|
|
979
|
-
"consent.originalReceiver must equal request.userAddress",
|
|
980
|
-
{ safeToRetry: true }
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
const nowSec = BigInt(Math.floor(this.now() / 1e3));
|
|
984
|
-
if (receiverConsent.deadline <= nowSec) {
|
|
985
|
-
throw new MintingGatewayError(
|
|
986
|
-
"CONSENT_EXPIRED",
|
|
987
|
-
"ReceiverConsent deadline has already passed",
|
|
988
|
-
{ safeToRetry: true }
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
const consentResult = await verifyReceiverConsent(
|
|
992
|
-
request.domain,
|
|
993
|
-
receiverConsent,
|
|
994
|
-
receiverSignature,
|
|
995
|
-
request.userAddress
|
|
996
|
-
);
|
|
997
|
-
if (!consentResult.isValid) {
|
|
998
|
-
throw new MintingGatewayError(
|
|
999
|
-
"INVALID_CONSENT_SIGNATURE",
|
|
1000
|
-
`ReceiverConsent signature did not recover to ${request.userAddress}`,
|
|
1001
|
-
{ safeToRetry: true }
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
const policyDecision = await this.policy.evaluate({
|
|
1005
|
-
userAddress: request.userAddress,
|
|
1006
|
-
amount: receiverConsent.amount,
|
|
1007
|
-
pointTokenAddress: request.pointTokenAddress,
|
|
1008
|
-
chainId: request.chainId
|
|
1009
|
-
});
|
|
1010
|
-
if (!policyDecision.approved) {
|
|
1011
|
-
const code = policyDecision.reason?.toLowerCase().includes("insufficient") ? "INSUFFICIENT_BALANCE" : "POLICY_REJECTED";
|
|
1012
|
-
throw new MintingGatewayError(
|
|
1013
|
-
code,
|
|
1014
|
-
policyDecision.reason ?? "Minting request rejected by policy engine",
|
|
1015
|
-
{ safeToRetry: true }
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
const lockDurationMs = request.lockDurationMs ?? this.computeLockDurationMs(receiverConsent.deadline);
|
|
1019
|
-
let lockId;
|
|
1020
|
-
try {
|
|
1021
|
-
lockId = await this.ledger.lockForMinting(
|
|
1022
|
-
request.userAddress,
|
|
1023
|
-
receiverConsent.amount,
|
|
1024
|
-
lockDurationMs,
|
|
1025
|
-
request.pointTokenAddress
|
|
1026
|
-
);
|
|
1027
|
-
} catch (err) {
|
|
1028
|
-
throw new MintingGatewayError(
|
|
1029
|
-
"INSUFFICIENT_BALANCE",
|
|
1030
|
-
`Failed to lock ledger balance: ${errorMessage2(err)}`,
|
|
1031
|
-
{ safeToRetry: true, cause: err }
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
try {
|
|
1035
|
-
let minterSignature;
|
|
1036
|
-
try {
|
|
1037
|
-
minterSignature = await this.signer.signMintRequest(request.domain, {
|
|
1038
|
-
to: request.userAddress,
|
|
1039
|
-
amount: receiverConsent.amount,
|
|
1040
|
-
nonce: receiverConsent.nonce,
|
|
1041
|
-
deadline: receiverConsent.deadline
|
|
1042
|
-
});
|
|
1043
|
-
} catch (err) {
|
|
1044
|
-
await this.releaseLockSafely(lockId);
|
|
1045
|
-
throw new MintingGatewayError(
|
|
1046
|
-
"SIGNER_FAILED",
|
|
1047
|
-
`Issuer signer failed: ${errorMessage2(err)}`,
|
|
1048
|
-
{ safeToRetry: true, cause: err }
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
const mintParams = {
|
|
1052
|
-
pointToken: request.pointTokenAddress,
|
|
1053
|
-
receiver: request.userAddress,
|
|
1054
|
-
amount: receiverConsent.amount,
|
|
1055
|
-
deadline: receiverConsent.deadline,
|
|
1056
|
-
minterSig: minterSignature.serialized,
|
|
1057
|
-
receiverSig: receiverSignature,
|
|
1058
|
-
extData: receiverConsent.extData
|
|
1059
|
-
};
|
|
1060
|
-
const swapParams = {
|
|
1061
|
-
path: request.swapPath,
|
|
1062
|
-
deadline: request.swapDeadline
|
|
1063
|
-
};
|
|
1064
|
-
let relayResult;
|
|
1065
|
-
try {
|
|
1066
|
-
relayResult = await this.relayService.submitMintAndSwap({
|
|
1067
|
-
mint: mintParams,
|
|
1068
|
-
swap: swapParams
|
|
1069
|
-
});
|
|
1070
|
-
} catch (err) {
|
|
1071
|
-
await this.handleRelayFailure(err, lockId);
|
|
1072
|
-
}
|
|
1073
|
-
const result = {
|
|
1074
|
-
txHash: relayResult.txHash,
|
|
1075
|
-
lockId
|
|
1076
|
-
};
|
|
1077
|
-
if (relayResult.blockNumber !== void 0) {
|
|
1078
|
-
result.blockNumber = relayResult.blockNumber;
|
|
1079
|
-
}
|
|
1080
|
-
if (relayResult.gasUsed !== void 0) {
|
|
1081
|
-
result.gasUsed = relayResult.gasUsed;
|
|
1082
|
-
}
|
|
1083
|
-
return result;
|
|
1084
|
-
} catch (err) {
|
|
1085
|
-
if (err instanceof MintingGatewayError) throw err;
|
|
1086
|
-
await this.releaseLockSafely(lockId);
|
|
1087
|
-
throw new MintingGatewayError(
|
|
1088
|
-
"RELAY_SUBMIT_FAILED",
|
|
1089
|
-
`Unexpected error: ${errorMessage2(err)}`,
|
|
1090
|
-
{ safeToRetry: true, cause: err }
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
// ---------------------------------------------------------------------------
|
|
1095
|
-
// Internals
|
|
1096
|
-
// ---------------------------------------------------------------------------
|
|
1097
|
-
computeLockDurationMs(consentDeadlineSec) {
|
|
1098
|
-
const nowMs = this.now();
|
|
1099
|
-
const deadlineMs = Number(consentDeadlineSec) * 1e3;
|
|
1100
|
-
const remaining = Math.max(0, deadlineMs - nowMs);
|
|
1101
|
-
return remaining + this.defaultLockBufferMs;
|
|
1102
|
-
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Map a RelayError to a MintingGatewayError, releasing the lock only
|
|
1105
|
-
* when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
|
|
1106
|
-
* leave the lock in place because the tx may still be in the mempool
|
|
1107
|
-
* or already mined — releasing would enable a double-spend on retry.
|
|
1108
|
-
* Always throws.
|
|
1109
|
-
*/
|
|
1110
|
-
async handleRelayFailure(err, lockId) {
|
|
1111
|
-
if (err instanceof RelayError) {
|
|
1112
|
-
switch (err.code) {
|
|
1113
|
-
case "ENCODE_FAILED":
|
|
1114
|
-
case "SIMULATION_FAILED":
|
|
1115
|
-
case "SUBMIT_FAILED":
|
|
1116
|
-
case "NOT_CONFIGURED":
|
|
1117
|
-
await this.releaseLockSafely(lockId);
|
|
1118
|
-
throw new MintingGatewayError(
|
|
1119
|
-
err.code === "SIMULATION_FAILED" ? "RELAY_SIMULATION_FAILED" : "RELAY_SUBMIT_FAILED",
|
|
1120
|
-
err.message,
|
|
1121
|
-
{ safeToRetry: true, cause: err }
|
|
1122
|
-
);
|
|
1123
|
-
case "TX_REVERTED":
|
|
1124
|
-
throw new MintingGatewayError("RELAY_REVERTED", err.message, {
|
|
1125
|
-
safeToRetry: false,
|
|
1126
|
-
cause: err
|
|
1127
|
-
});
|
|
1128
|
-
case "TIMEOUT":
|
|
1129
|
-
throw new MintingGatewayError("RELAY_TIMEOUT", err.message, {
|
|
1130
|
-
safeToRetry: false,
|
|
1131
|
-
cause: err
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
await this.releaseLockSafely(lockId);
|
|
1136
|
-
throw new MintingGatewayError(
|
|
1137
|
-
"RELAY_SUBMIT_FAILED",
|
|
1138
|
-
`Unexpected relay error: ${errorMessage2(err)}`,
|
|
1139
|
-
{ safeToRetry: true, cause: err }
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Release a lock, swallowing any secondary error. We never want a lock
|
|
1144
|
-
* release failure to mask the original error — the lock will auto-expire
|
|
1145
|
-
* via TTL anyway.
|
|
1146
|
-
*/
|
|
1147
|
-
async releaseLockSafely(lockId) {
|
|
1148
|
-
try {
|
|
1149
|
-
await this.ledger.releaseLock(lockId);
|
|
1150
|
-
} catch {
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
};
|
|
1154
|
-
function errorMessage2(err) {
|
|
1155
|
-
return err instanceof Error ? err.message : String(err);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
633
|
// src/indexer/types.ts
|
|
1159
634
|
var InMemoryCursorStore = class {
|
|
1160
635
|
cursor;
|
|
@@ -1167,7 +642,7 @@ var InMemoryCursorStore = class {
|
|
|
1167
642
|
};
|
|
1168
643
|
|
|
1169
644
|
// src/indexer/pointIndexer.ts
|
|
1170
|
-
import { getAddress as
|
|
645
|
+
import { getAddress as getAddress3, parseAbiItem } from "viem";
|
|
1171
646
|
var TRANSFER_EVENT = parseAbiItem(
|
|
1172
647
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1173
648
|
);
|
|
@@ -1238,7 +713,8 @@ var PointIndexer = class {
|
|
|
1238
713
|
return;
|
|
1239
714
|
}
|
|
1240
715
|
await this.processBlockRange(from, safeHead);
|
|
1241
|
-
} catch {
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error("[PAFI] PointIndexer tick error:", err);
|
|
1242
718
|
}
|
|
1243
719
|
this.scheduleNext();
|
|
1244
720
|
}
|
|
@@ -1288,10 +764,10 @@ var PointIndexer = class {
|
|
|
1288
764
|
for (const log of logs) {
|
|
1289
765
|
const args = log.args;
|
|
1290
766
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1291
|
-
if (
|
|
767
|
+
if (getAddress3(args.from) !== ZERO_ADDRESS) continue;
|
|
1292
768
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1293
769
|
out.push({
|
|
1294
|
-
to:
|
|
770
|
+
to: getAddress3(args.to),
|
|
1295
771
|
amount: args.value,
|
|
1296
772
|
blockNumber: log.blockNumber,
|
|
1297
773
|
txHash: log.transactionHash,
|
|
@@ -1347,7 +823,7 @@ function pickMatchingLock(locks, amount) {
|
|
|
1347
823
|
}
|
|
1348
824
|
|
|
1349
825
|
// src/indexer/burnIndexer.ts
|
|
1350
|
-
import { getAddress as
|
|
826
|
+
import { getAddress as getAddress4, parseAbiItem as parseAbiItem2 } from "viem";
|
|
1351
827
|
var TRANSFER_EVENT2 = parseAbiItem2(
|
|
1352
828
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1353
829
|
);
|
|
@@ -1364,18 +840,7 @@ var BurnIndexer = class {
|
|
|
1364
840
|
confirmations;
|
|
1365
841
|
batchSize;
|
|
1366
842
|
pollIntervalMs;
|
|
1367
|
-
|
|
1368
|
-
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1369
|
-
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1370
|
-
* ledger's query path.
|
|
1371
|
-
*
|
|
1372
|
-
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1373
|
-
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1374
|
-
* incrementing IDs so callers with the memory ledger must provide a
|
|
1375
|
-
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1376
|
-
* their `pending_credits` table.
|
|
1377
|
-
*/
|
|
1378
|
-
matchLockId = async () => void 0;
|
|
843
|
+
matchLockId;
|
|
1379
844
|
running = false;
|
|
1380
845
|
timer;
|
|
1381
846
|
constructor(config) {
|
|
@@ -1393,6 +858,12 @@ var BurnIndexer = class {
|
|
|
1393
858
|
);
|
|
1394
859
|
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1395
860
|
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
861
|
+
if (!config.matchLockId) {
|
|
862
|
+
throw new Error(
|
|
863
|
+
"BurnIndexer: matchLockId is required. Provide a function that maps a burn event to its pending credit lockId. Without it, no on-chain burns will ever grant off-chain credits."
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
this.matchLockId = config.matchLockId;
|
|
1396
867
|
}
|
|
1397
868
|
start() {
|
|
1398
869
|
if (this.running) return;
|
|
@@ -1422,7 +893,8 @@ var BurnIndexer = class {
|
|
|
1422
893
|
return;
|
|
1423
894
|
}
|
|
1424
895
|
await this.processBlockRange(from, safeHead);
|
|
1425
|
-
} catch {
|
|
896
|
+
} catch (err) {
|
|
897
|
+
console.error("[PAFI] BurnIndexer tick error:", err);
|
|
1426
898
|
}
|
|
1427
899
|
this.scheduleNext();
|
|
1428
900
|
}
|
|
@@ -1467,10 +939,10 @@ var BurnIndexer = class {
|
|
|
1467
939
|
for (const log of logs) {
|
|
1468
940
|
const args = log.args;
|
|
1469
941
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1470
|
-
if (
|
|
942
|
+
if (getAddress4(args.to) !== ZERO_ADDRESS2) continue;
|
|
1471
943
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1472
944
|
out.push({
|
|
1473
|
-
from:
|
|
945
|
+
from: getAddress4(args.from),
|
|
1474
946
|
amount: args.value,
|
|
1475
947
|
blockNumber: log.blockNumber,
|
|
1476
948
|
txHash: log.transactionHash,
|
|
@@ -1485,20 +957,27 @@ var BurnIndexer = class {
|
|
|
1485
957
|
* log + skip.
|
|
1486
958
|
*/
|
|
1487
959
|
async finalize(evt) {
|
|
960
|
+
const txHash = evt.txHash;
|
|
1488
961
|
const lockId = await this.matchLockId(evt);
|
|
1489
|
-
if (
|
|
962
|
+
if (lockId === void 0) {
|
|
963
|
+
console.warn(
|
|
964
|
+
"[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
|
|
965
|
+
);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
1490
968
|
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1491
969
|
return;
|
|
1492
970
|
}
|
|
1493
971
|
try {
|
|
1494
972
|
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1495
|
-
} catch {
|
|
973
|
+
} catch (err) {
|
|
974
|
+
console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
|
|
1496
975
|
}
|
|
1497
976
|
}
|
|
1498
977
|
};
|
|
1499
978
|
|
|
1500
979
|
// src/api/handlers.ts
|
|
1501
|
-
import { getAddress as
|
|
980
|
+
import { getAddress as getAddress5 } from "viem";
|
|
1502
981
|
import {
|
|
1503
982
|
getMintRequestNonce,
|
|
1504
983
|
getPointTokenBalance,
|
|
@@ -1509,7 +988,6 @@ import {
|
|
|
1509
988
|
} from "@pafi-dev/core";
|
|
1510
989
|
var IssuerApiHandlers = class {
|
|
1511
990
|
authService;
|
|
1512
|
-
gateway;
|
|
1513
991
|
ledger;
|
|
1514
992
|
provider;
|
|
1515
993
|
/**
|
|
@@ -1517,18 +995,14 @@ var IssuerApiHandlers = class {
|
|
|
1517
995
|
* validate the request's `pointTokenAddress` against this set.
|
|
1518
996
|
*/
|
|
1519
997
|
supportedTokens;
|
|
1520
|
-
/** First supported token — used as default when a handler doesn't
|
|
1521
|
-
* receive a `pointTokenAddress` in the request (shouldn't happen in
|
|
1522
|
-
* practice, but keeps type-narrowing happy). */
|
|
1523
|
-
defaultToken;
|
|
1524
998
|
chainId;
|
|
1525
999
|
contracts;
|
|
1526
1000
|
pafiWebUrl;
|
|
1527
1001
|
feeManager;
|
|
1528
1002
|
poolsProvider;
|
|
1003
|
+
claim;
|
|
1529
1004
|
constructor(config) {
|
|
1530
1005
|
this.authService = config.authService;
|
|
1531
|
-
this.gateway = config.gateway;
|
|
1532
1006
|
this.ledger = config.ledger;
|
|
1533
1007
|
this.provider = config.provider;
|
|
1534
1008
|
const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
@@ -1537,14 +1011,14 @@ var IssuerApiHandlers = class {
|
|
|
1537
1011
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1538
1012
|
);
|
|
1539
1013
|
}
|
|
1540
|
-
const normalized = raw.map((a) =>
|
|
1014
|
+
const normalized = raw.map((a) => getAddress5(a));
|
|
1541
1015
|
this.supportedTokens = new Set(normalized);
|
|
1542
|
-
this.defaultToken = normalized[0];
|
|
1543
1016
|
this.chainId = config.chainId;
|
|
1544
1017
|
this.contracts = config.contracts;
|
|
1545
1018
|
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1546
1019
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1547
1020
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1021
|
+
if (config.claim) this.claim = config.claim;
|
|
1548
1022
|
}
|
|
1549
1023
|
// =========================================================================
|
|
1550
1024
|
// Public handlers (no auth required)
|
|
@@ -1559,6 +1033,12 @@ var IssuerApiHandlers = class {
|
|
|
1559
1033
|
if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
|
|
1560
1034
|
throw new Error("handleLogin: message and signature are required");
|
|
1561
1035
|
}
|
|
1036
|
+
if (body.message.length > 4096) {
|
|
1037
|
+
throw new Error("message too long");
|
|
1038
|
+
}
|
|
1039
|
+
if (body.signature.length > 260) {
|
|
1040
|
+
throw new Error("signature too long");
|
|
1041
|
+
}
|
|
1562
1042
|
const result = await this.authService.login(body.message, body.signature);
|
|
1563
1043
|
return {
|
|
1564
1044
|
token: result.token,
|
|
@@ -1573,9 +1053,12 @@ var IssuerApiHandlers = class {
|
|
|
1573
1053
|
* needs to build EIP-712 messages and interact with on-chain.
|
|
1574
1054
|
*/
|
|
1575
1055
|
async handleConfig(chainId) {
|
|
1056
|
+
if (!Number.isInteger(chainId) || chainId <= 0) {
|
|
1057
|
+
throw new Error("invalid chainId");
|
|
1058
|
+
}
|
|
1576
1059
|
if (chainId !== this.chainId) {
|
|
1577
1060
|
throw new Error(
|
|
1578
|
-
`handleConfig: unsupported chainId ${chainId}
|
|
1061
|
+
`handleConfig: unsupported chainId ${chainId}`
|
|
1579
1062
|
);
|
|
1580
1063
|
}
|
|
1581
1064
|
const contracts = {
|
|
@@ -1638,14 +1121,14 @@ var IssuerApiHandlers = class {
|
|
|
1638
1121
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1639
1122
|
);
|
|
1640
1123
|
}
|
|
1641
|
-
const normalizedAuthed =
|
|
1642
|
-
const normalizedRequest =
|
|
1124
|
+
const normalizedAuthed = getAddress5(userAddress);
|
|
1125
|
+
const normalizedRequest = getAddress5(request.userAddress);
|
|
1643
1126
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1644
1127
|
throw new Error(
|
|
1645
1128
|
"handleUser: request userAddress must match authenticated user"
|
|
1646
1129
|
);
|
|
1647
1130
|
}
|
|
1648
|
-
const pointToken =
|
|
1131
|
+
const pointToken = getAddress5(request.pointTokenAddress);
|
|
1649
1132
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1650
1133
|
throw new Error(
|
|
1651
1134
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
@@ -1688,19 +1171,32 @@ var IssuerApiHandlers = class {
|
|
|
1688
1171
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1689
1172
|
);
|
|
1690
1173
|
}
|
|
1691
|
-
const pointToken =
|
|
1174
|
+
const pointToken = getAddress5(request.pointTokenAddress);
|
|
1692
1175
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1693
1176
|
throw new Error(
|
|
1694
1177
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
1695
1178
|
);
|
|
1696
1179
|
}
|
|
1180
|
+
const consent = request.receiverConsent;
|
|
1181
|
+
if (getAddress5(consent.originalReceiver) !== getAddress5(userAddress)) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
"handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
if (consent.amount <= 0n) {
|
|
1187
|
+
throw new Error("handleBuildConsentTypedData: amount must be positive");
|
|
1188
|
+
}
|
|
1189
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1190
|
+
if (consent.deadline <= nowSecs) {
|
|
1191
|
+
throw new Error("handleBuildConsentTypedData: deadline is in the past");
|
|
1192
|
+
}
|
|
1697
1193
|
const name = await getTokenName(this.provider, pointToken);
|
|
1698
1194
|
const domain = {
|
|
1699
1195
|
name,
|
|
1700
1196
|
verifyingContract: pointToken,
|
|
1701
1197
|
chainId: this.chainId
|
|
1702
1198
|
};
|
|
1703
|
-
const typedData = buildReceiverConsentTypedData(domain,
|
|
1199
|
+
const typedData = buildReceiverConsentTypedData(domain, consent);
|
|
1704
1200
|
return {
|
|
1705
1201
|
typedData: {
|
|
1706
1202
|
domain: typedData.domain,
|
|
@@ -1711,54 +1207,97 @@ var IssuerApiHandlers = class {
|
|
|
1711
1207
|
};
|
|
1712
1208
|
}
|
|
1713
1209
|
/**
|
|
1714
|
-
* `POST /claim
|
|
1210
|
+
* `POST /claim`
|
|
1715
1211
|
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
* [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
|
|
1720
|
-
* removed in 2.0.
|
|
1212
|
+
* Policy gate + ledger lock + MintRequest signing in a single atomic
|
|
1213
|
+
* step. Returns an unsigned UserOp the frontend attaches paymaster data
|
|
1214
|
+
* to and submits via EIP-7702 + Bundler.
|
|
1721
1215
|
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1216
|
+
* Order of operations:
|
|
1217
|
+
* 1. Validate request fields.
|
|
1218
|
+
* 2. policy.evaluate() — throws if denied; cannot be bypassed.
|
|
1219
|
+
* 3. ledger.lockForMinting() — reserves the balance.
|
|
1220
|
+
* 4. Read on-chain mintRequestNonce + token name in parallel.
|
|
1221
|
+
* 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
|
|
1222
|
+
* 6. On any error after step 3, release the lock before re-throwing.
|
|
1724
1223
|
*/
|
|
1725
|
-
async
|
|
1224
|
+
async handleClaim(userAddress, request) {
|
|
1225
|
+
if (!this.claim) {
|
|
1226
|
+
throw new Error("handleClaim: claim is not configured on this issuer");
|
|
1227
|
+
}
|
|
1726
1228
|
if (request.chainId !== this.chainId) {
|
|
1727
|
-
throw new Error(
|
|
1728
|
-
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1729
|
-
);
|
|
1229
|
+
throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
|
|
1730
1230
|
}
|
|
1731
|
-
const pointToken =
|
|
1231
|
+
const pointToken = getAddress5(request.pointTokenAddress);
|
|
1732
1232
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1733
|
-
throw new Error(
|
|
1734
|
-
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1735
|
-
);
|
|
1233
|
+
throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
|
|
1736
1234
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1235
|
+
if (request.amount <= 0n) {
|
|
1236
|
+
throw new Error("handleClaim: amount must be positive");
|
|
1237
|
+
}
|
|
1238
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
1239
|
+
if (request.deadline <= nowSecs) {
|
|
1240
|
+
throw new Error("handleClaim: deadline is in the past");
|
|
1241
|
+
}
|
|
1242
|
+
const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
|
|
1243
|
+
const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
|
|
1244
|
+
const normalizedUser = getAddress5(userAddress);
|
|
1245
|
+
const decision = await policy.evaluate({
|
|
1246
|
+
userAddress: normalizedUser,
|
|
1247
|
+
amount: request.amount,
|
|
1739
1248
|
pointTokenAddress: pointToken,
|
|
1740
|
-
chainId:
|
|
1741
|
-
domain: request.domain,
|
|
1742
|
-
receiverConsent: request.receiverConsent,
|
|
1743
|
-
receiverSignature: request.receiverSignature,
|
|
1744
|
-
swapPath: request.swapPath,
|
|
1745
|
-
swapDeadline: request.swapDeadline
|
|
1249
|
+
chainId: this.chainId
|
|
1746
1250
|
});
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1251
|
+
if (!decision.approved) {
|
|
1252
|
+
throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
|
|
1253
|
+
}
|
|
1254
|
+
const lockId = await this.ledger.lockForMinting(
|
|
1255
|
+
normalizedUser,
|
|
1256
|
+
request.amount,
|
|
1257
|
+
lockDurationMs,
|
|
1258
|
+
pointToken
|
|
1259
|
+
);
|
|
1260
|
+
try {
|
|
1261
|
+
const [mintRequestNonce, tokenName] = await Promise.all([
|
|
1262
|
+
getMintRequestNonce(this.provider, pointToken, normalizedUser),
|
|
1263
|
+
getTokenName(this.provider, pointToken)
|
|
1264
|
+
]);
|
|
1265
|
+
const domain = {
|
|
1266
|
+
name: tokenName,
|
|
1267
|
+
verifyingContract: pointToken,
|
|
1268
|
+
chainId: this.chainId
|
|
1269
|
+
};
|
|
1270
|
+
const userOp = await relayService.prepareMint({
|
|
1271
|
+
userAddress: normalizedUser,
|
|
1272
|
+
aaNonce: request.aaNonce,
|
|
1273
|
+
batchExecutorAddress,
|
|
1274
|
+
pointTokenAddress: pointToken,
|
|
1275
|
+
amount: request.amount,
|
|
1276
|
+
issuerSignerWallet,
|
|
1277
|
+
domain,
|
|
1278
|
+
mintRequestNonce,
|
|
1279
|
+
deadline: request.deadline,
|
|
1280
|
+
feeAmount: request.feeAmount,
|
|
1281
|
+
feeRecipient: request.feeRecipient
|
|
1282
|
+
});
|
|
1283
|
+
return {
|
|
1284
|
+
lockId,
|
|
1285
|
+
userOp,
|
|
1286
|
+
expiresInSeconds: Math.floor(lockDurationMs / 1e3)
|
|
1287
|
+
};
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
await this.ledger.releaseLock(lockId).catch(() => {
|
|
1290
|
+
});
|
|
1291
|
+
throw err;
|
|
1292
|
+
}
|
|
1755
1293
|
}
|
|
1756
1294
|
};
|
|
1757
1295
|
|
|
1758
1296
|
// src/api/handlers/ptRedeemHandler.ts
|
|
1759
|
-
import { getAddress as
|
|
1760
|
-
import {
|
|
1297
|
+
import { getAddress as getAddress6 } from "viem";
|
|
1298
|
+
import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2, getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
|
|
1761
1299
|
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1300
|
+
var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
|
|
1762
1301
|
var PTRedeemError = class extends Error {
|
|
1763
1302
|
constructor(code, message) {
|
|
1764
1303
|
super(message);
|
|
@@ -1770,11 +1309,14 @@ var PTRedeemError = class extends Error {
|
|
|
1770
1309
|
var PTRedeemHandler = class {
|
|
1771
1310
|
ledger;
|
|
1772
1311
|
relayService;
|
|
1312
|
+
provider;
|
|
1773
1313
|
pointTokenAddress;
|
|
1774
1314
|
batchExecutorAddress;
|
|
1775
1315
|
chainId;
|
|
1776
1316
|
domain;
|
|
1317
|
+
burnerSignerWallet;
|
|
1777
1318
|
redeemLockDurationMs;
|
|
1319
|
+
signatureDeadlineSeconds;
|
|
1778
1320
|
now;
|
|
1779
1321
|
constructor(config) {
|
|
1780
1322
|
if (!config.ledger.reservePendingCredit) {
|
|
@@ -1783,46 +1325,88 @@ var PTRedeemHandler = class {
|
|
|
1783
1325
|
"PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
|
|
1784
1326
|
);
|
|
1785
1327
|
}
|
|
1328
|
+
if (!config.burnerSignerWallet) {
|
|
1329
|
+
throw new PTRedeemError(
|
|
1330
|
+
"SIGNING_FAILED",
|
|
1331
|
+
"PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1786
1334
|
this.ledger = config.ledger;
|
|
1787
1335
|
this.relayService = config.relayService;
|
|
1788
|
-
this.
|
|
1789
|
-
this.
|
|
1336
|
+
this.provider = config.provider;
|
|
1337
|
+
this.pointTokenAddress = getAddress6(config.pointTokenAddress);
|
|
1338
|
+
this.batchExecutorAddress = getAddress6(config.batchExecutorAddress);
|
|
1790
1339
|
this.chainId = config.chainId;
|
|
1791
1340
|
this.domain = config.domain;
|
|
1341
|
+
this.burnerSignerWallet = config.burnerSignerWallet;
|
|
1342
|
+
if (this.burnerSignerWallet?.account?.type === "local") {
|
|
1343
|
+
console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
|
|
1344
|
+
}
|
|
1792
1345
|
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1346
|
+
this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
|
|
1793
1347
|
this.now = config.now ?? (() => Date.now());
|
|
1794
1348
|
}
|
|
1795
1349
|
async handle(request) {
|
|
1350
|
+
if (getAddress6(request.authenticatedAddress) !== getAddress6(request.userAddress)) {
|
|
1351
|
+
throw new PTRedeemError(
|
|
1352
|
+
"UNAUTHORIZED",
|
|
1353
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1796
1356
|
if (request.amount <= 0n) {
|
|
1797
|
-
throw new PTRedeemError("
|
|
1357
|
+
throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
|
|
1798
1358
|
}
|
|
1799
|
-
|
|
1359
|
+
let burnNonce;
|
|
1360
|
+
try {
|
|
1361
|
+
burnNonce = await this.provider.readContract({
|
|
1362
|
+
address: this.pointTokenAddress,
|
|
1363
|
+
abi: POINT_TOKEN_V2_ABI2,
|
|
1364
|
+
functionName: "burnRequestNonces",
|
|
1365
|
+
args: [request.userAddress]
|
|
1366
|
+
});
|
|
1367
|
+
} catch (err) {
|
|
1800
1368
|
throw new PTRedeemError(
|
|
1801
|
-
"
|
|
1802
|
-
`
|
|
1369
|
+
"NONCE_READ_FAILED",
|
|
1370
|
+
`failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
|
|
1803
1371
|
);
|
|
1804
1372
|
}
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1373
|
+
const onChainBalance = await getPointTokenBalance2(
|
|
1374
|
+
this.provider,
|
|
1375
|
+
this.pointTokenAddress,
|
|
1376
|
+
request.userAddress
|
|
1377
|
+
);
|
|
1378
|
+
if (onChainBalance < request.amount) {
|
|
1807
1379
|
throw new PTRedeemError(
|
|
1808
|
-
"
|
|
1809
|
-
`
|
|
1380
|
+
"INVALID_AMOUNT",
|
|
1381
|
+
`insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
|
|
1810
1382
|
);
|
|
1811
1383
|
}
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
name: this.domain.name,
|
|
1815
|
-
chainId: this.chainId,
|
|
1816
|
-
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1817
|
-
},
|
|
1818
|
-
request.consent,
|
|
1819
|
-
request.consentSignature,
|
|
1820
|
-
request.userAddress
|
|
1384
|
+
const deadline = BigInt(
|
|
1385
|
+
Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
|
|
1821
1386
|
);
|
|
1822
|
-
|
|
1387
|
+
const domain = {
|
|
1388
|
+
name: this.domain.name,
|
|
1389
|
+
chainId: this.chainId,
|
|
1390
|
+
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1391
|
+
};
|
|
1392
|
+
const burnRequest = {
|
|
1393
|
+
from: request.userAddress,
|
|
1394
|
+
amount: request.amount,
|
|
1395
|
+
nonce: burnNonce,
|
|
1396
|
+
deadline
|
|
1397
|
+
};
|
|
1398
|
+
let burnerSignature;
|
|
1399
|
+
try {
|
|
1400
|
+
const sig = await signBurnRequest(
|
|
1401
|
+
this.burnerSignerWallet,
|
|
1402
|
+
domain,
|
|
1403
|
+
burnRequest
|
|
1404
|
+
);
|
|
1405
|
+
burnerSignature = sig.serialized;
|
|
1406
|
+
} catch (err) {
|
|
1823
1407
|
throw new PTRedeemError(
|
|
1824
|
-
"
|
|
1825
|
-
`
|
|
1408
|
+
"SIGNING_FAILED",
|
|
1409
|
+
`failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
|
|
1826
1410
|
);
|
|
1827
1411
|
}
|
|
1828
1412
|
const lockId = await this.ledger.reservePendingCredit(
|
|
@@ -1837,33 +1421,21 @@ var PTRedeemHandler = class {
|
|
|
1837
1421
|
aaNonce: request.aaNonce,
|
|
1838
1422
|
pointTokenAddress: this.pointTokenAddress,
|
|
1839
1423
|
batchExecutorAddress: this.batchExecutorAddress,
|
|
1840
|
-
|
|
1841
|
-
|
|
1424
|
+
burnRequest,
|
|
1425
|
+
burnerSignature
|
|
1842
1426
|
});
|
|
1843
1427
|
return {
|
|
1844
1428
|
lockId,
|
|
1845
1429
|
userOp,
|
|
1846
|
-
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
|
|
1430
|
+
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
|
|
1431
|
+
signatureDeadline: deadline
|
|
1847
1432
|
};
|
|
1848
1433
|
}
|
|
1849
1434
|
};
|
|
1850
|
-
function parseSigStruct(serialized) {
|
|
1851
|
-
const raw = serialized.slice(2);
|
|
1852
|
-
if (raw.length !== 130) {
|
|
1853
|
-
throw new PTRedeemError(
|
|
1854
|
-
"INVALID_CONSENT",
|
|
1855
|
-
`signature must be 65 bytes, got ${raw.length / 2}`
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
|
-
const r = `0x${raw.slice(0, 64)}`;
|
|
1859
|
-
const s = `0x${raw.slice(64, 128)}`;
|
|
1860
|
-
const v = parseInt(raw.slice(128, 130), 16);
|
|
1861
|
-
return { v, r, s };
|
|
1862
|
-
}
|
|
1863
1435
|
|
|
1864
1436
|
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1865
|
-
import { getAddress as
|
|
1866
|
-
import { getPointTokenBalance as
|
|
1437
|
+
import { getAddress as getAddress7 } from "viem";
|
|
1438
|
+
import { getPointTokenBalance as getPointTokenBalance3 } from "@pafi-dev/core";
|
|
1867
1439
|
var TopUpRedemptionError = class extends Error {
|
|
1868
1440
|
constructor(code, message) {
|
|
1869
1441
|
super(message);
|
|
@@ -1881,9 +1453,15 @@ var TopUpRedemptionHandler = class {
|
|
|
1881
1453
|
this.ledger = config.ledger;
|
|
1882
1454
|
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1883
1455
|
this.provider = config.provider;
|
|
1884
|
-
this.pointTokenAddress =
|
|
1456
|
+
this.pointTokenAddress = getAddress7(config.pointTokenAddress);
|
|
1885
1457
|
}
|
|
1886
1458
|
async handle(request) {
|
|
1459
|
+
if (getAddress7(request.authenticatedAddress) !== getAddress7(request.userAddress)) {
|
|
1460
|
+
throw new TopUpRedemptionError(
|
|
1461
|
+
"UNAUTHORIZED",
|
|
1462
|
+
`userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1887
1465
|
const offChainBalance = await this.ledger.getBalance(
|
|
1888
1466
|
request.userAddress,
|
|
1889
1467
|
this.pointTokenAddress
|
|
@@ -1892,7 +1470,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1892
1470
|
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1893
1471
|
}
|
|
1894
1472
|
const shortfall = request.requiredAmount - offChainBalance;
|
|
1895
|
-
const onChainBalance = await
|
|
1473
|
+
const onChainBalance = await getPointTokenBalance3(
|
|
1896
1474
|
this.provider,
|
|
1897
1475
|
this.pointTokenAddress,
|
|
1898
1476
|
request.userAddress
|
|
@@ -1905,24 +1483,11 @@ var TopUpRedemptionHandler = class {
|
|
|
1905
1483
|
shortfall
|
|
1906
1484
|
};
|
|
1907
1485
|
}
|
|
1908
|
-
if (request.redeemRequest.consent.amount < shortfall) {
|
|
1909
|
-
throw new TopUpRedemptionError(
|
|
1910
|
-
"CONSENT_AMOUNT_TOO_LOW",
|
|
1911
|
-
`consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
|
|
1912
|
-
);
|
|
1913
|
-
}
|
|
1914
|
-
if (request.redeemRequest.consent.amount !== shortfall) {
|
|
1915
|
-
throw new TopUpRedemptionError(
|
|
1916
|
-
"CONSENT_AMOUNT_TOO_LOW",
|
|
1917
|
-
`consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
|
|
1918
|
-
);
|
|
1919
|
-
}
|
|
1920
1486
|
const redeem = await this.ptRedeemHandler.handle({
|
|
1487
|
+
authenticatedAddress: request.authenticatedAddress,
|
|
1921
1488
|
userAddress: request.userAddress,
|
|
1922
1489
|
amount: shortfall,
|
|
1923
|
-
|
|
1924
|
-
consentSignature: request.redeemRequest.consentSignature,
|
|
1925
|
-
aaNonce: request.redeemRequest.aaNonce
|
|
1490
|
+
aaNonce: request.aaNonce
|
|
1926
1491
|
});
|
|
1927
1492
|
return {
|
|
1928
1493
|
action: "TOP_UP_STARTED",
|
|
@@ -1933,6 +1498,7 @@ var TopUpRedemptionHandler = class {
|
|
|
1933
1498
|
};
|
|
1934
1499
|
|
|
1935
1500
|
// src/pools/subgraphPoolsProvider.ts
|
|
1501
|
+
import { isAddress } from "viem";
|
|
1936
1502
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1937
1503
|
var POOL_QUERY = `
|
|
1938
1504
|
query GetPoolForPointToken($id: ID!) {
|
|
@@ -1955,6 +1521,19 @@ function createSubgraphPoolsProvider(config) {
|
|
|
1955
1521
|
"createSubgraphPoolsProvider: subgraphUrl is required"
|
|
1956
1522
|
);
|
|
1957
1523
|
}
|
|
1524
|
+
try {
|
|
1525
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1526
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1527
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1528
|
+
}
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
if (err instanceof TypeError) {
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
throw err;
|
|
1536
|
+
}
|
|
1958
1537
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
1959
1538
|
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1960
1539
|
const now = config.now ?? (() => Date.now());
|
|
@@ -2023,6 +1602,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
|
|
|
2023
1602
|
return [];
|
|
2024
1603
|
}
|
|
2025
1604
|
const { pool } = token;
|
|
1605
|
+
if (!isAddress(pool.hooks)) {
|
|
1606
|
+
console.error(
|
|
1607
|
+
"[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
|
|
1608
|
+
pool.hooks,
|
|
1609
|
+
"\u2014 skipping pool"
|
|
1610
|
+
);
|
|
1611
|
+
return [];
|
|
1612
|
+
}
|
|
1613
|
+
if (!isAddress(pool.token0.id) || !isAddress(pool.token1.id)) {
|
|
1614
|
+
console.error(
|
|
1615
|
+
"[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
|
|
1616
|
+
);
|
|
1617
|
+
return [];
|
|
1618
|
+
}
|
|
1619
|
+
if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
|
|
1620
|
+
console.error(
|
|
1621
|
+
"[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
|
|
1622
|
+
);
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
2026
1625
|
const [currency0, currency1] = sortCurrencies(
|
|
2027
1626
|
pool.token0.id,
|
|
2028
1627
|
pool.token1.id
|
|
@@ -2058,6 +1657,19 @@ function createSubgraphNativeUsdtQuoter(config) {
|
|
|
2058
1657
|
"createSubgraphNativeUsdtQuoter: subgraphUrl is required"
|
|
2059
1658
|
);
|
|
2060
1659
|
}
|
|
1660
|
+
try {
|
|
1661
|
+
const parsed = new URL(config.subgraphUrl);
|
|
1662
|
+
if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
|
|
1663
|
+
throw new Error("subgraphUrl must use HTTPS in production");
|
|
1664
|
+
}
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
if (err instanceof TypeError) {
|
|
1667
|
+
throw new Error(
|
|
1668
|
+
`subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
throw err;
|
|
1672
|
+
}
|
|
2061
1673
|
const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
|
|
2062
1674
|
const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
|
|
2063
1675
|
const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
|
|
@@ -2134,6 +1746,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
|
|
|
2134
1746
|
);
|
|
2135
1747
|
return null;
|
|
2136
1748
|
}
|
|
1749
|
+
const MIN_REASONABLE_ETH_PRICE = 100;
|
|
1750
|
+
const MAX_REASONABLE_ETH_PRICE = 1e5;
|
|
1751
|
+
if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
|
|
1752
|
+
console.warn(
|
|
1753
|
+
`[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
|
|
1754
|
+
);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
2137
1757
|
return parsed;
|
|
2138
1758
|
}
|
|
2139
1759
|
function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
@@ -2144,7 +1764,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
2144
1764
|
}
|
|
2145
1765
|
|
|
2146
1766
|
// src/balance/balanceAggregator.ts
|
|
2147
|
-
import { getPointTokenBalance as
|
|
1767
|
+
import { getPointTokenBalance as getPointTokenBalance4 } from "@pafi-dev/core";
|
|
2148
1768
|
var BalanceAggregator = class {
|
|
2149
1769
|
provider;
|
|
2150
1770
|
ledger;
|
|
@@ -2165,7 +1785,7 @@ var BalanceAggregator = class {
|
|
|
2165
1785
|
async getCombinedBalance(user, pointToken) {
|
|
2166
1786
|
const [offChain, onChain] = await Promise.all([
|
|
2167
1787
|
this.ledger.getBalance(user, pointToken),
|
|
2168
|
-
|
|
1788
|
+
getPointTokenBalance4(this.provider, pointToken, user)
|
|
2169
1789
|
]);
|
|
2170
1790
|
return {
|
|
2171
1791
|
offChain,
|
|
@@ -2203,28 +1823,11 @@ var PafiBackendError = class extends Error {
|
|
|
2203
1823
|
code;
|
|
2204
1824
|
httpStatus;
|
|
2205
1825
|
details;
|
|
2206
|
-
/**
|
|
2207
|
-
* Seconds to wait before retry. Populated from the server body
|
|
2208
|
-
* (e.g. rate limit returns the number of seconds until UTC midnight).
|
|
2209
|
-
*/
|
|
2210
1826
|
retryAfter;
|
|
2211
|
-
/**
|
|
2212
|
-
* `safeToRetry` as reported by the server body. Prefer this over the
|
|
2213
|
-
* code-based heuristic when available — the server knows more about
|
|
2214
|
-
* whether the same request will succeed on retry.
|
|
2215
|
-
*/
|
|
2216
1827
|
serverSafeToRetry;
|
|
2217
|
-
/**
|
|
2218
|
-
* Whether the caller can safely retry the same request.
|
|
2219
|
-
*
|
|
2220
|
-
* If the server provided `safeToRetry` in the body, trust that.
|
|
2221
|
-
* Otherwise fall back to a code-based heuristic.
|
|
2222
|
-
*/
|
|
2223
1828
|
get safeToRetry() {
|
|
2224
1829
|
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
2225
1830
|
switch (this.code) {
|
|
2226
|
-
case "PAYMASTER_UNAVAILABLE":
|
|
2227
|
-
case "PAYMASTER_TIMEOUT":
|
|
2228
1831
|
case "RATE_LIMITER_UNAVAILABLE":
|
|
2229
1832
|
case "INTERNAL_ERROR":
|
|
2230
1833
|
case "TIMEOUT":
|
|
@@ -2233,196 +1836,22 @@ var PafiBackendError = class extends Error {
|
|
|
2233
1836
|
case "RATE_LIMIT_EXCEEDED":
|
|
2234
1837
|
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
2235
1838
|
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
1839
|
+
case "ISSUER_BUDGET_EXCEEDED":
|
|
2236
1840
|
return true;
|
|
2237
|
-
// after retryAfter
|
|
2238
1841
|
default:
|
|
2239
1842
|
return false;
|
|
2240
1843
|
}
|
|
2241
1844
|
}
|
|
2242
1845
|
};
|
|
2243
1846
|
|
|
2244
|
-
// src/pafi-backend/pafiBackendClient.ts
|
|
2245
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2246
|
-
var RETRY_DEFAULTS = {
|
|
2247
|
-
maxAttempts: 1,
|
|
2248
|
-
initialDelayMs: 500,
|
|
2249
|
-
maxDelayMs: 1e4,
|
|
2250
|
-
maxRetryAfterMs: 3e4
|
|
2251
|
-
};
|
|
2252
|
-
var PafiBackendClient = class {
|
|
2253
|
-
url;
|
|
2254
|
-
issuerId;
|
|
2255
|
-
apiKey;
|
|
2256
|
-
fetchImpl;
|
|
2257
|
-
timeoutMs;
|
|
2258
|
-
retry;
|
|
2259
|
-
constructor(config) {
|
|
2260
|
-
if (!config.url) {
|
|
2261
|
-
throw new Error("PafiBackendClient: url is required");
|
|
2262
|
-
}
|
|
2263
|
-
if (!config.issuerId) {
|
|
2264
|
-
throw new Error("PafiBackendClient: issuerId is required");
|
|
2265
|
-
}
|
|
2266
|
-
if (!config.apiKey) {
|
|
2267
|
-
throw new Error("PafiBackendClient: apiKey is required");
|
|
2268
|
-
}
|
|
2269
|
-
this.url = config.url.replace(/\/+$/, "");
|
|
2270
|
-
this.issuerId = config.issuerId;
|
|
2271
|
-
this.apiKey = config.apiKey;
|
|
2272
|
-
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
2273
|
-
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2274
|
-
this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
|
|
2275
|
-
if (!this.fetchImpl) {
|
|
2276
|
-
throw new Error(
|
|
2277
|
-
"PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
|
|
2278
|
-
);
|
|
2279
|
-
}
|
|
2280
|
-
if (this.retry.maxAttempts < 1) {
|
|
2281
|
-
throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
/**
|
|
2285
|
-
* Request paymaster sponsorship for a pre-built UserOperation.
|
|
2286
|
-
* See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
|
|
2287
|
-
*
|
|
2288
|
-
* Retries automatically on transient failures (5xx, timeouts, network
|
|
2289
|
-
* errors, and errors the server flags with `safeToRetry: true`) up to
|
|
2290
|
-
* `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
|
|
2291
|
-
*
|
|
2292
|
-
* @throws PafiBackendError on final failure after exhausting retries
|
|
2293
|
-
*/
|
|
2294
|
-
async requestSponsorship(req) {
|
|
2295
|
-
return this.postWithRetry(
|
|
2296
|
-
"/paymaster/sponsor",
|
|
2297
|
-
req
|
|
2298
|
-
);
|
|
2299
|
-
}
|
|
2300
|
-
// -------------------------------------------------------------------------
|
|
2301
|
-
// Internals
|
|
2302
|
-
// -------------------------------------------------------------------------
|
|
2303
|
-
async postWithRetry(path, body) {
|
|
2304
|
-
let lastError;
|
|
2305
|
-
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
2306
|
-
try {
|
|
2307
|
-
return await this.post(path, body);
|
|
2308
|
-
} catch (err) {
|
|
2309
|
-
if (!(err instanceof PafiBackendError)) throw err;
|
|
2310
|
-
lastError = err;
|
|
2311
|
-
const isLastAttempt = attempt >= this.retry.maxAttempts;
|
|
2312
|
-
if (isLastAttempt || !err.safeToRetry) throw err;
|
|
2313
|
-
const delay = this.computeBackoff(attempt, err.retryAfter);
|
|
2314
|
-
if (delay === null) throw err;
|
|
2315
|
-
await this.sleep(delay);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
throw lastError;
|
|
2319
|
-
}
|
|
2320
|
-
/**
|
|
2321
|
-
* Pick the delay before the next retry.
|
|
2322
|
-
* - If the server sent `retryAfter` (seconds), honor it (capped by
|
|
2323
|
-
* `maxRetryAfterMs`) — returns null if the server wait exceeds the
|
|
2324
|
-
* cap, signalling the caller should give up.
|
|
2325
|
-
* - Otherwise: exponential backoff with ±20% jitter, capped at
|
|
2326
|
-
* `maxDelayMs`.
|
|
2327
|
-
*/
|
|
2328
|
-
computeBackoff(attempt, retryAfter) {
|
|
2329
|
-
if (retryAfter !== void 0) {
|
|
2330
|
-
const serverMs = retryAfter * 1e3;
|
|
2331
|
-
if (serverMs > this.retry.maxRetryAfterMs) return null;
|
|
2332
|
-
return serverMs;
|
|
2333
|
-
}
|
|
2334
|
-
const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
|
|
2335
|
-
const capped = Math.min(exp, this.retry.maxDelayMs);
|
|
2336
|
-
const jitter = capped * (0.8 + Math.random() * 0.4);
|
|
2337
|
-
return Math.round(jitter);
|
|
2338
|
-
}
|
|
2339
|
-
sleep(ms) {
|
|
2340
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2341
|
-
}
|
|
2342
|
-
async post(path, body) {
|
|
2343
|
-
const controller = new AbortController();
|
|
2344
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2345
|
-
let response;
|
|
2346
|
-
try {
|
|
2347
|
-
response = await this.fetchImpl(`${this.url}${path}`, {
|
|
2348
|
-
method: "POST",
|
|
2349
|
-
headers: {
|
|
2350
|
-
"Content-Type": "application/json",
|
|
2351
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
2352
|
-
"X-Issuer-Id": this.issuerId
|
|
2353
|
-
},
|
|
2354
|
-
body: JSON.stringify(body, this.bigintReplacer),
|
|
2355
|
-
signal: controller.signal
|
|
2356
|
-
});
|
|
2357
|
-
} catch (err) {
|
|
2358
|
-
if (err.name === "AbortError") {
|
|
2359
|
-
throw new PafiBackendError(
|
|
2360
|
-
"TIMEOUT",
|
|
2361
|
-
`PAFI Backend request timed out after ${this.timeoutMs}ms`,
|
|
2362
|
-
0
|
|
2363
|
-
);
|
|
2364
|
-
}
|
|
2365
|
-
throw new PafiBackendError(
|
|
2366
|
-
"NETWORK_ERROR",
|
|
2367
|
-
`PAFI Backend unreachable: ${err.message}`,
|
|
2368
|
-
0
|
|
2369
|
-
);
|
|
2370
|
-
} finally {
|
|
2371
|
-
clearTimeout(timeoutId);
|
|
2372
|
-
}
|
|
2373
|
-
const text = await response.text();
|
|
2374
|
-
if (!response.ok) {
|
|
2375
|
-
let code = "INTERNAL_ERROR";
|
|
2376
|
-
let message = text || response.statusText;
|
|
2377
|
-
let details;
|
|
2378
|
-
let retryAfter;
|
|
2379
|
-
let serverSafeToRetry;
|
|
2380
|
-
try {
|
|
2381
|
-
const parsed = JSON.parse(text);
|
|
2382
|
-
code = parsed.code ?? code;
|
|
2383
|
-
message = parsed.message ?? message;
|
|
2384
|
-
details = parsed.details;
|
|
2385
|
-
if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
|
|
2386
|
-
if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
|
|
2387
|
-
} catch {
|
|
2388
|
-
}
|
|
2389
|
-
throw new PafiBackendError(code, message, response.status, details, {
|
|
2390
|
-
...retryAfter !== void 0 ? { retryAfter } : {},
|
|
2391
|
-
...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
|
|
2392
|
-
});
|
|
2393
|
-
}
|
|
2394
|
-
return JSON.parse(text, this.bigintReviver);
|
|
2395
|
-
}
|
|
2396
|
-
/** JSON replacer that stringifies bigints. Paired with bigintReviver. */
|
|
2397
|
-
bigintReplacer = (_key, value) => {
|
|
2398
|
-
return typeof value === "bigint" ? value.toString() : value;
|
|
2399
|
-
};
|
|
2400
|
-
/**
|
|
2401
|
-
* JSON reviver that coerces specific numeric-string fields back to
|
|
2402
|
-
* bigint. The server must send these fields as decimal strings.
|
|
2403
|
-
*/
|
|
2404
|
-
bigintReviver = (key, value) => {
|
|
2405
|
-
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)) {
|
|
2406
|
-
return BigInt(value);
|
|
2407
|
-
}
|
|
2408
|
-
return value;
|
|
2409
|
-
};
|
|
2410
|
-
};
|
|
2411
|
-
|
|
2412
1847
|
// src/config.ts
|
|
2413
|
-
import { getAddress as
|
|
1848
|
+
import { getAddress as getAddress8 } from "viem";
|
|
2414
1849
|
function createIssuerService(config) {
|
|
2415
1850
|
if (!config.provider) {
|
|
2416
1851
|
throw new Error("createIssuerService: provider is required");
|
|
2417
1852
|
}
|
|
2418
|
-
if (!config.
|
|
2419
|
-
throw new Error("createIssuerService:
|
|
2420
|
-
}
|
|
2421
|
-
if (!config.signer) {
|
|
2422
|
-
throw new Error("createIssuerService: signer is required");
|
|
2423
|
-
}
|
|
2424
|
-
if (!config.relayAddress) {
|
|
2425
|
-
throw new Error("createIssuerService: relayAddress is required");
|
|
1853
|
+
if (!config.ledger) {
|
|
1854
|
+
throw new Error("createIssuerService: ledger is required");
|
|
2426
1855
|
}
|
|
2427
1856
|
if (!config.auth?.jwtSecret) {
|
|
2428
1857
|
throw new Error("createIssuerService: auth.jwtSecret is required");
|
|
@@ -2436,8 +1865,8 @@ function createIssuerService(config) {
|
|
|
2436
1865
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
2437
1866
|
);
|
|
2438
1867
|
}
|
|
2439
|
-
const tokenAddresses = rawAddresses.map((a) =>
|
|
2440
|
-
const ledger = config.ledger
|
|
1868
|
+
const tokenAddresses = rawAddresses.map((a) => getAddress8(a));
|
|
1869
|
+
const ledger = config.ledger;
|
|
2441
1870
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
2442
1871
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
2443
1872
|
const authServiceConfig = {
|
|
@@ -2450,18 +1879,7 @@ function createIssuerService(config) {
|
|
|
2450
1879
|
authServiceConfig.jwtExpiresIn = config.auth.jwtExpiresIn;
|
|
2451
1880
|
}
|
|
2452
1881
|
const authService = new AuthService(authServiceConfig);
|
|
2453
|
-
const
|
|
2454
|
-
relayAddress: config.relayAddress,
|
|
2455
|
-
operatorWallet: config.operatorWallet,
|
|
2456
|
-
provider: config.provider
|
|
2457
|
-
};
|
|
2458
|
-
if (config.relay?.simulateBeforeSubmit !== void 0) {
|
|
2459
|
-
relayServiceConfig.simulateBeforeSubmit = config.relay.simulateBeforeSubmit;
|
|
2460
|
-
}
|
|
2461
|
-
if (config.relay?.confirmationTimeoutMs !== void 0) {
|
|
2462
|
-
relayServiceConfig.confirmationTimeoutMs = config.relay.confirmationTimeoutMs;
|
|
2463
|
-
}
|
|
2464
|
-
const relayService = new RelayService(relayServiceConfig);
|
|
1882
|
+
const relayService = new RelayService();
|
|
2465
1883
|
let feeManager;
|
|
2466
1884
|
if (config.fee) {
|
|
2467
1885
|
feeManager = new FeeManager({
|
|
@@ -2469,16 +1887,6 @@ function createIssuerService(config) {
|
|
|
2469
1887
|
provider: config.provider
|
|
2470
1888
|
});
|
|
2471
1889
|
}
|
|
2472
|
-
const gatewayConfig = {
|
|
2473
|
-
ledger,
|
|
2474
|
-
policy,
|
|
2475
|
-
signer: config.signer,
|
|
2476
|
-
relayService
|
|
2477
|
-
};
|
|
2478
|
-
if (config.gateway?.defaultLockBufferMs !== void 0) {
|
|
2479
|
-
gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
|
|
2480
|
-
}
|
|
2481
|
-
const gateway = new MintingGateway(gatewayConfig);
|
|
2482
1890
|
const indexers = /* @__PURE__ */ new Map();
|
|
2483
1891
|
for (const tokenAddress of tokenAddresses) {
|
|
2484
1892
|
const indexerConfig = {
|
|
@@ -2506,7 +1914,6 @@ function createIssuerService(config) {
|
|
|
2506
1914
|
const firstIndexer = indexers.get(tokenAddresses[0]);
|
|
2507
1915
|
const handlersConfig = {
|
|
2508
1916
|
authService,
|
|
2509
|
-
gateway,
|
|
2510
1917
|
ledger,
|
|
2511
1918
|
provider: config.provider,
|
|
2512
1919
|
pointTokenAddresses: tokenAddresses,
|
|
@@ -2515,6 +1922,15 @@ function createIssuerService(config) {
|
|
|
2515
1922
|
};
|
|
2516
1923
|
if (feeManager) handlersConfig.feeManager = feeManager;
|
|
2517
1924
|
if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
|
|
1925
|
+
if (config.claim) {
|
|
1926
|
+
handlersConfig.claim = {
|
|
1927
|
+
policy,
|
|
1928
|
+
relayService,
|
|
1929
|
+
issuerSignerWallet: config.claim.issuerSignerWallet,
|
|
1930
|
+
batchExecutorAddress: config.claim.batchExecutorAddress,
|
|
1931
|
+
lockDurationMs: config.claim.lockDurationMs
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
2518
1934
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
2519
1935
|
if (config.indexer?.autoStart) {
|
|
2520
1936
|
for (const idx of indexers.values()) {
|
|
@@ -2526,10 +1942,8 @@ function createIssuerService(config) {
|
|
|
2526
1942
|
sessionStore,
|
|
2527
1943
|
ledger,
|
|
2528
1944
|
policy,
|
|
2529
|
-
signer: config.signer,
|
|
2530
1945
|
relayService,
|
|
2531
1946
|
feeManager,
|
|
2532
|
-
gateway,
|
|
2533
1947
|
indexers,
|
|
2534
1948
|
indexer: firstIndexer,
|
|
2535
1949
|
handlers
|
|
@@ -2547,18 +1961,13 @@ export {
|
|
|
2547
1961
|
FeeManager,
|
|
2548
1962
|
InMemoryCursorStore,
|
|
2549
1963
|
IssuerApiHandlers,
|
|
2550
|
-
MemoryPointLedger,
|
|
2551
1964
|
MemorySessionStore,
|
|
2552
|
-
MintingGateway,
|
|
2553
|
-
MintingGatewayError,
|
|
2554
1965
|
NonceManager,
|
|
2555
1966
|
PAFI_ISSUER_SDK_VERSION,
|
|
2556
1967
|
PTRedeemError,
|
|
2557
1968
|
PTRedeemHandler,
|
|
2558
|
-
PafiBackendClient,
|
|
2559
1969
|
PafiBackendError,
|
|
2560
1970
|
PointIndexer,
|
|
2561
|
-
PrivateKeySigner,
|
|
2562
1971
|
RelayError,
|
|
2563
1972
|
RelayService,
|
|
2564
1973
|
TopUpRedemptionError,
|
|
@@ -2566,7 +1975,6 @@ export {
|
|
|
2566
1975
|
authenticateRequest,
|
|
2567
1976
|
createIssuerService,
|
|
2568
1977
|
createSubgraphNativeUsdtQuoter,
|
|
2569
|
-
createSubgraphPoolsProvider
|
|
2570
|
-
encodeExtData
|
|
1978
|
+
createSubgraphPoolsProvider
|
|
2571
1979
|
};
|
|
2572
1980
|
//# sourceMappingURL=index.js.map
|