@pafi-dev/issuer 0.7.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-R4FYJZ2N.js +1 -0
- package/dist/chunk-R4FYJZ2N.js.map +1 -0
- package/dist/chunk-U3WMORJG.js +230 -0
- package/dist/chunk-U3WMORJG.js.map +1 -0
- package/dist/http/index.cjs +169 -0
- package/dist/http/index.cjs.map +1 -0
- package/dist/http/index.d.cts +112 -0
- package/dist/http/index.d.ts +112 -0
- package/dist/http/index.js +14 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.cjs +927 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +502 -25
- package/dist/index.d.ts +502 -25
- package/dist/index.js +772 -74
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +314 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +54 -0
- package/dist/nestjs/index.d.ts +54 -0
- package/dist/nestjs/index.js +124 -0
- package/dist/nestjs/index.js.map +1 -0
- package/package.json +29 -2
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
BundlerRejectedError: () => BundlerRejectedError,
|
|
29
29
|
BurnIndexer: () => BurnIndexer,
|
|
30
30
|
ConfigurationError: () => ConfigurationError,
|
|
31
|
+
DEFAULT_REDEMPTION_POLICY: () => DEFAULT_REDEMPTION_POLICY,
|
|
31
32
|
DefaultPolicyEngine: () => DefaultPolicyEngine,
|
|
32
33
|
FeeManager: () => FeeManager,
|
|
33
34
|
InMemoryCursorStore: () => InMemoryCursorStore,
|
|
@@ -37,8 +38,11 @@ __export(index_exports, {
|
|
|
37
38
|
IssuerStateValidator: () => IssuerStateValidator,
|
|
38
39
|
LockNotFoundError: () => LockNotFoundError,
|
|
39
40
|
MemoryPendingUserOpStore: () => MemoryPendingUserOpStore,
|
|
41
|
+
MemoryRateLimiter: () => MemoryRateLimiter,
|
|
42
|
+
MemoryRedemptionHistoryStore: () => MemoryRedemptionHistoryStore,
|
|
40
43
|
MemorySessionStore: () => MemorySessionStore,
|
|
41
44
|
NonceManager: () => NonceManager,
|
|
45
|
+
NoopRateLimiter: () => NoopRateLimiter,
|
|
42
46
|
PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
|
|
43
47
|
PAFI_SUBGRAPH_URL: () => import_core15.PAFI_SUBGRAPH_URL,
|
|
44
48
|
PTClaimError: () => PTClaimError,
|
|
@@ -53,21 +57,34 @@ __export(index_exports, {
|
|
|
53
57
|
PerpDepositError: () => PerpDepositError,
|
|
54
58
|
PerpDepositHandler: () => PerpDepositHandler,
|
|
55
59
|
PointIndexer: () => PointIndexer,
|
|
60
|
+
PolicyProvider: () => PolicyProvider,
|
|
61
|
+
REDEMPTION_HISTORY_WINDOW_SEC: () => REDEMPTION_HISTORY_WINDOW_SEC,
|
|
62
|
+
RedemptionService: () => RedemptionService,
|
|
56
63
|
RelayError: () => RelayError,
|
|
57
64
|
RelayService: () => RelayService,
|
|
65
|
+
SDK_ERROR_HTTP_STATUS_CODE: () => import_core.SDK_ERROR_HTTP_STATUS_CODE,
|
|
66
|
+
SettlementClient: () => SettlementClient,
|
|
58
67
|
ValidationError: () => import_core3.ValidationError,
|
|
59
68
|
authenticateRequest: () => authenticateRequest,
|
|
69
|
+
buildErrorEnvelope: () => buildErrorEnvelope,
|
|
70
|
+
buildSdkErrorBody: () => buildSdkErrorBody,
|
|
60
71
|
createIssuerService: () => createIssuerService,
|
|
61
72
|
createNativePtQuoter: () => createNativePtQuoter,
|
|
62
73
|
createSdkErrorMapper: () => createSdkErrorMapper,
|
|
63
74
|
createSubgraphNativeUsdtQuoter: () => createSubgraphNativeUsdtQuoter,
|
|
64
75
|
createSubgraphPoolsProvider: () => createSubgraphPoolsProvider,
|
|
76
|
+
defaultErrorTypeForStatus: () => import_core.defaultErrorTypeForStatus,
|
|
77
|
+
defaultPolicyFor: () => defaultPolicyFor,
|
|
78
|
+
evaluateRedemption: () => evaluateRedemption,
|
|
65
79
|
handleClaimStatus: () => handleClaimStatus,
|
|
66
80
|
handleDelegateSubmit: () => handleDelegateSubmit,
|
|
67
81
|
handleMobilePrepare: () => handleMobilePrepare,
|
|
68
82
|
handleMobileSubmit: () => handleMobileSubmit,
|
|
69
83
|
handleRedeemStatus: () => handleRedeemStatus,
|
|
70
84
|
mergePaymasterFields: () => mergePaymasterFields,
|
|
85
|
+
payloadFromGenericError: () => payloadFromGenericError,
|
|
86
|
+
payloadFromHttpException: () => payloadFromHttpException,
|
|
87
|
+
payloadFromPafiSdkError: () => payloadFromPafiSdkError,
|
|
71
88
|
prepareMobileUserOp: () => prepareMobileUserOp,
|
|
72
89
|
relayUserOp: () => relayUserOp,
|
|
73
90
|
requestPaymaster: () => requestPaymaster,
|
|
@@ -91,6 +108,152 @@ var ConfigurationError = class extends import_core2.PafiSdkError {
|
|
|
91
108
|
}
|
|
92
109
|
};
|
|
93
110
|
|
|
111
|
+
// src/api/errorMapper.ts
|
|
112
|
+
function buildSdkErrorBody(err) {
|
|
113
|
+
const type = err.type ?? (0, import_core.defaultErrorTypeForStatus)(import_core.SDK_ERROR_HTTP_STATUS_CODE[err.httpStatus]);
|
|
114
|
+
const body = {
|
|
115
|
+
type,
|
|
116
|
+
code: err.code,
|
|
117
|
+
message: err.message,
|
|
118
|
+
safeToRetry: err.safeToRetry
|
|
119
|
+
};
|
|
120
|
+
if (err.param) body.param = err.param;
|
|
121
|
+
if (err.metadata) body.metadata = err.metadata;
|
|
122
|
+
if (err.details !== void 0) body.details = err.details;
|
|
123
|
+
return body;
|
|
124
|
+
}
|
|
125
|
+
function createSdkErrorMapper(factories) {
|
|
126
|
+
return (err) => {
|
|
127
|
+
if (!(err instanceof import_core.PafiSdkError)) {
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
const body = buildSdkErrorBody(err);
|
|
131
|
+
switch (err.httpStatus) {
|
|
132
|
+
case "not_found":
|
|
133
|
+
throw factories.notFound(body);
|
|
134
|
+
case "forbidden":
|
|
135
|
+
throw factories.forbidden(body);
|
|
136
|
+
case "unprocessable":
|
|
137
|
+
throw factories.unprocessable(body);
|
|
138
|
+
case "service_unavailable":
|
|
139
|
+
throw factories.serviceUnavailable(body);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/http/errorEnvelope.ts
|
|
145
|
+
function isValidationPipeBody(body) {
|
|
146
|
+
return Array.isArray(body["message"]) && typeof body["error"] === "string" && body["error"] === "Bad Request";
|
|
147
|
+
}
|
|
148
|
+
function extractParamFromValidatorMessage(message) {
|
|
149
|
+
const idx = message.indexOf(" ");
|
|
150
|
+
if (idx <= 0) return void 0;
|
|
151
|
+
const candidate = message.slice(0, idx);
|
|
152
|
+
return /^[A-Za-z_][\w.]*$/.test(candidate) ? candidate : void 0;
|
|
153
|
+
}
|
|
154
|
+
function normalizeValidationPipeBody(body) {
|
|
155
|
+
const fields = {};
|
|
156
|
+
for (const msg of body.message) {
|
|
157
|
+
const param2 = extractParamFromValidatorMessage(msg);
|
|
158
|
+
const key = param2 ?? "_";
|
|
159
|
+
(fields[key] ??= []).push(msg);
|
|
160
|
+
}
|
|
161
|
+
const fieldKeys = Object.keys(fields).filter((k) => k !== "_");
|
|
162
|
+
const param = fieldKeys.length === 1 ? fieldKeys[0] : void 0;
|
|
163
|
+
const payload = {
|
|
164
|
+
type: "validation_error",
|
|
165
|
+
code: "VALIDATION_FAILED",
|
|
166
|
+
message: body.message.join("; "),
|
|
167
|
+
safeToRetry: false,
|
|
168
|
+
metadata: { fieldErrors: fields }
|
|
169
|
+
};
|
|
170
|
+
if (param) payload.param = param;
|
|
171
|
+
return payload;
|
|
172
|
+
}
|
|
173
|
+
function normalizeHttpExceptionBody(desc) {
|
|
174
|
+
const { statusCode, responseBody, exceptionName, fallbackMessage } = desc;
|
|
175
|
+
const defaultType = (0, import_core.defaultErrorTypeForStatus)(statusCode);
|
|
176
|
+
if (typeof responseBody === "string") {
|
|
177
|
+
return {
|
|
178
|
+
type: defaultType,
|
|
179
|
+
code: exceptionName,
|
|
180
|
+
message: responseBody,
|
|
181
|
+
safeToRetry: false
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (responseBody && typeof responseBody === "object") {
|
|
185
|
+
const body = responseBody;
|
|
186
|
+
if (isValidationPipeBody(body)) {
|
|
187
|
+
return normalizeValidationPipeBody(body);
|
|
188
|
+
}
|
|
189
|
+
const code = typeof body["code"] === "string" && body["code"] || typeof body["error"] === "string" && body["error"] || exceptionName;
|
|
190
|
+
const message = typeof body["message"] === "string" && body["message"] || (Array.isArray(body["message"]) ? body["message"].join("; ") : "") || fallbackMessage;
|
|
191
|
+
const type = typeof body["type"] === "string" && body["type"] || defaultType;
|
|
192
|
+
const safeToRetry = typeof body["safeToRetry"] === "boolean" ? body["safeToRetry"] : false;
|
|
193
|
+
const payload = {
|
|
194
|
+
type,
|
|
195
|
+
code,
|
|
196
|
+
message,
|
|
197
|
+
safeToRetry
|
|
198
|
+
};
|
|
199
|
+
if (typeof body["param"] === "string") payload.param = body["param"];
|
|
200
|
+
if (body["metadata"] && typeof body["metadata"] === "object") {
|
|
201
|
+
payload.metadata = body["metadata"];
|
|
202
|
+
}
|
|
203
|
+
if (body["details"] !== void 0) payload.details = body["details"];
|
|
204
|
+
return payload;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
type: defaultType,
|
|
208
|
+
code: exceptionName || "INTERNAL_SERVER_ERROR",
|
|
209
|
+
message: fallbackMessage || "An unexpected error occurred",
|
|
210
|
+
safeToRetry: false
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function payloadFromPafiSdkError(err) {
|
|
214
|
+
const body = buildSdkErrorBody(err);
|
|
215
|
+
const payload = {
|
|
216
|
+
type: body.type,
|
|
217
|
+
code: body.code,
|
|
218
|
+
message: body.message,
|
|
219
|
+
safeToRetry: body.safeToRetry
|
|
220
|
+
};
|
|
221
|
+
if (body.param) payload.param = body.param;
|
|
222
|
+
if (body.metadata) payload.metadata = body.metadata;
|
|
223
|
+
if (body.details !== void 0) payload.details = body.details;
|
|
224
|
+
return payload;
|
|
225
|
+
}
|
|
226
|
+
function sanitizeDbErrorMessage(message) {
|
|
227
|
+
if (/^[A-Z_]+: /.test(message) && message.length < 256) return message;
|
|
228
|
+
return "Internal database error";
|
|
229
|
+
}
|
|
230
|
+
function buildErrorEnvelope(input) {
|
|
231
|
+
const now = (input.ctx.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
statusCode: input.status,
|
|
235
|
+
error: input.payload,
|
|
236
|
+
meta: {
|
|
237
|
+
timestamp: now.toISOString(),
|
|
238
|
+
requestId: input.ctx.requestId,
|
|
239
|
+
path: input.ctx.path
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function payloadFromHttpException(desc) {
|
|
244
|
+
return normalizeHttpExceptionBody(desc);
|
|
245
|
+
}
|
|
246
|
+
function payloadFromGenericError(err) {
|
|
247
|
+
const name = err.name || "INTERNAL_SERVER_ERROR";
|
|
248
|
+
const isDbError = name === "QueryFailedError" || name === "EntityNotFoundError";
|
|
249
|
+
return {
|
|
250
|
+
type: "server_error",
|
|
251
|
+
code: name,
|
|
252
|
+
message: isDbError ? sanitizeDbErrorMessage(err.message) : err.message || "An unexpected error occurred",
|
|
253
|
+
safeToRetry: false
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
94
257
|
// src/policy/defaultPolicy.ts
|
|
95
258
|
var DefaultPolicyEngine = class {
|
|
96
259
|
ledger;
|
|
@@ -161,9 +324,9 @@ var MemorySessionStore = class {
|
|
|
161
324
|
nonceTtlMs;
|
|
162
325
|
now;
|
|
163
326
|
constructor(opts = {}) {
|
|
164
|
-
if (process.env.NODE_ENV === "production") {
|
|
165
|
-
|
|
166
|
-
"[PAFI] MemorySessionStore
|
|
327
|
+
if (process.env.NODE_ENV === "production" && !opts.dangerouslyAllowMemoryStoreInProduction) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
"[PAFI] MemorySessionStore refuses to start in production (NODE_ENV=production). Multi-pod K8s deploys do not share Map state, so sessions are not revocable across replicas \u2014 `logout` on pod A leaves the token valid on pod B until expiry. Use a Redis-backed session store (see RedisSessionStoreService in gg56-backend or implement your own ISessionStore). To bypass for a single-pod deploy, pass `dangerouslyAllowMemoryStoreInProduction: true`."
|
|
167
330
|
);
|
|
168
331
|
}
|
|
169
332
|
this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
|
|
@@ -290,6 +453,64 @@ var AuthError = class extends import_core.PafiSdkError {
|
|
|
290
453
|
};
|
|
291
454
|
|
|
292
455
|
// src/auth/loginVerifier.ts
|
|
456
|
+
function assertJwtSecretStrength(secret) {
|
|
457
|
+
if (!secret) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
"AuthService: jwtSecret is required. Generate via `node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"`"
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (secret.length < 32) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`AuthService: jwtSecret too short (${secret.length} chars; need \u2265 32). HS256 brute-force becomes feasible below this threshold.`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const uniqueChars = new Set(secret).size;
|
|
468
|
+
if (uniqueChars < 16) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`AuthService: jwtSecret has only ${uniqueChars} unique characters; need \u2265 16. A trivially weak secret (e.g. "aaaaa...") will pass length but offer almost no entropy. Use \`crypto.randomBytes(32).toString('hex')\`.`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
const entropy = shannonEntropyBitsPerChar(secret);
|
|
474
|
+
if (entropy < 3.5) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`AuthService: jwtSecret entropy too low (${entropy.toFixed(2)} bits/char; need \u2265 3.5). Use \`crypto.randomBytes(32).toString('hex')\` (\u2248 4.0 bits/char).`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
for (let period = 1; period <= secret.length / 4; period++) {
|
|
480
|
+
if (secret.length % period !== 0) continue;
|
|
481
|
+
const head = secret.slice(0, period);
|
|
482
|
+
if (secret === head.repeat(secret.length / period)) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`AuthService: jwtSecret is a repeating pattern of period ${period}. Use \`crypto.randomBytes(32).toString('hex')\`.`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function shannonEntropyBitsPerChar(s) {
|
|
490
|
+
const counts = /* @__PURE__ */ new Map();
|
|
491
|
+
for (const c of s) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
492
|
+
const len = s.length;
|
|
493
|
+
let h = 0;
|
|
494
|
+
for (const n of counts.values()) {
|
|
495
|
+
const p = n / len;
|
|
496
|
+
h -= p * Math.log2(p);
|
|
497
|
+
}
|
|
498
|
+
return h;
|
|
499
|
+
}
|
|
500
|
+
function decodeExpiredJwtJti(token) {
|
|
501
|
+
try {
|
|
502
|
+
const parts = token.split(".");
|
|
503
|
+
if (parts.length !== 3) return {};
|
|
504
|
+
const payloadB64 = parts[1];
|
|
505
|
+
if (!payloadB64) return {};
|
|
506
|
+
const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((payloadB64.length + 3) % 4);
|
|
507
|
+
const json = Buffer.from(padded, "base64").toString("utf-8");
|
|
508
|
+
const claims = JSON.parse(json);
|
|
509
|
+
return typeof claims.jti === "string" ? { jti: claims.jti } : {};
|
|
510
|
+
} catch {
|
|
511
|
+
return {};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
293
514
|
var DEFAULT_EXPIRES_IN = "24h";
|
|
294
515
|
var AuthService = class {
|
|
295
516
|
sessionStore;
|
|
@@ -297,17 +518,19 @@ var AuthService = class {
|
|
|
297
518
|
jwtExpiresIn;
|
|
298
519
|
domain;
|
|
299
520
|
chainId;
|
|
521
|
+
issuer;
|
|
522
|
+
audience;
|
|
300
523
|
nonceManager;
|
|
301
524
|
now;
|
|
302
525
|
constructor(config) {
|
|
303
|
-
|
|
304
|
-
throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
|
|
305
|
-
}
|
|
526
|
+
assertJwtSecretStrength(config.jwtSecret);
|
|
306
527
|
this.sessionStore = config.sessionStore;
|
|
307
528
|
this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
|
|
308
529
|
this.jwtExpiresIn = config.jwtExpiresIn ?? DEFAULT_EXPIRES_IN;
|
|
309
530
|
this.domain = config.domain;
|
|
310
531
|
this.chainId = config.chainId;
|
|
532
|
+
this.issuer = config.issuer;
|
|
533
|
+
this.audience = config.audience;
|
|
311
534
|
this.nonceManager = new NonceManager(config.sessionStore);
|
|
312
535
|
this.now = config.now ?? (() => /* @__PURE__ */ new Date());
|
|
313
536
|
}
|
|
@@ -384,29 +607,59 @@ var AuthService = class {
|
|
|
384
607
|
expiresAt
|
|
385
608
|
};
|
|
386
609
|
await this.sessionStore.createSession(session);
|
|
387
|
-
|
|
610
|
+
let signer = new import_jose.SignJWT({
|
|
388
611
|
userAddress,
|
|
389
612
|
chainId: this.chainId
|
|
390
|
-
}).setProtectedHeader({ alg: "HS256" }).setJti(tokenId).setIssuedAt(Math.floor(issuedAt.getTime() / 1e3)).setExpirationTime(Math.floor(expiresAt.getTime() / 1e3))
|
|
613
|
+
}).setProtectedHeader({ alg: "HS256" }).setJti(tokenId).setIssuedAt(Math.floor(issuedAt.getTime() / 1e3)).setExpirationTime(Math.floor(expiresAt.getTime() / 1e3));
|
|
614
|
+
if (this.issuer) signer = signer.setIssuer(this.issuer);
|
|
615
|
+
if (this.audience) signer = signer.setAudience(this.audience);
|
|
616
|
+
const token = await signer.sign(this.jwtSecret);
|
|
391
617
|
return { token, userAddress, tokenId, expiresAt };
|
|
392
618
|
}
|
|
393
619
|
/** Revoke the session backing the given JWT (logout). */
|
|
394
620
|
async logout(token) {
|
|
621
|
+
let payload;
|
|
395
622
|
try {
|
|
396
|
-
const
|
|
397
|
-
clockTolerance: 60
|
|
623
|
+
const result = await (0, import_jose.jwtVerify)(token, this.jwtSecret, {
|
|
624
|
+
clockTolerance: 60,
|
|
398
625
|
// allow logout right after expiry
|
|
626
|
+
...this.issuer ? { issuer: this.issuer } : {},
|
|
627
|
+
...this.audience ? { audience: this.audience } : {}
|
|
399
628
|
});
|
|
400
|
-
|
|
401
|
-
await this.sessionStore.revokeSession(payload.jti);
|
|
402
|
-
}
|
|
629
|
+
payload = result.payload;
|
|
403
630
|
} catch (err) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
631
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
632
|
+
const decoded = decodeExpiredJwtJti(token);
|
|
633
|
+
if (decoded.jti) {
|
|
634
|
+
try {
|
|
635
|
+
await this.sessionStore.revokeSession(decoded.jti);
|
|
636
|
+
} catch (storeErr) {
|
|
637
|
+
this.logSessionStoreError(storeErr);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (err instanceof import_jose.errors.JWSSignatureVerificationFailed || err instanceof import_jose.errors.JWSInvalid || err instanceof import_jose.errors.JWTInvalid) {
|
|
643
|
+
throw new AuthError("TOKEN_INVALID", "JWT verification failed");
|
|
644
|
+
}
|
|
645
|
+
throw new AuthError(
|
|
646
|
+
"TOKEN_INVALID",
|
|
647
|
+
`JWT verification failed: ${err instanceof Error ? err.message : String(err)}`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (payload.jti) {
|
|
651
|
+
try {
|
|
652
|
+
await this.sessionStore.revokeSession(payload.jti);
|
|
653
|
+
} catch (storeErr) {
|
|
654
|
+
this.logSessionStoreError(storeErr);
|
|
407
655
|
}
|
|
408
656
|
}
|
|
409
657
|
}
|
|
658
|
+
logSessionStoreError(err) {
|
|
659
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
660
|
+
if (msg.includes("not found")) return;
|
|
661
|
+
console.error("[PAFI] AuthService logout: session store error", err);
|
|
662
|
+
}
|
|
410
663
|
/**
|
|
411
664
|
* Verify a JWT and return the authenticated user context. Throws an
|
|
412
665
|
* `AuthError` if the token is missing, malformed, expired, revoked, or
|
|
@@ -415,7 +668,10 @@ var AuthService = class {
|
|
|
415
668
|
async verifyToken(token) {
|
|
416
669
|
let payload;
|
|
417
670
|
try {
|
|
418
|
-
const result = await (0, import_jose.jwtVerify)(token, this.jwtSecret
|
|
671
|
+
const result = await (0, import_jose.jwtVerify)(token, this.jwtSecret, {
|
|
672
|
+
...this.issuer ? { issuer: this.issuer } : {},
|
|
673
|
+
...this.audience ? { audience: this.audience } : {}
|
|
674
|
+
});
|
|
419
675
|
payload = result.payload;
|
|
420
676
|
} catch (err) {
|
|
421
677
|
if (err instanceof import_jose.errors.JWTExpired) {
|
|
@@ -481,6 +737,70 @@ async function authenticateRequest(authHeader, authService) {
|
|
|
481
737
|
return authService.verifyToken(token);
|
|
482
738
|
}
|
|
483
739
|
|
|
740
|
+
// src/auth/rateLimiter.ts
|
|
741
|
+
var DEFAULT_LIMITS = {
|
|
742
|
+
auth_nonce: { max: 30, windowMs: 6e4 },
|
|
743
|
+
// 30 nonces/min ≈ 1 per 2s
|
|
744
|
+
auth_login: { max: 5, windowMs: 6e4 }
|
|
745
|
+
// 5 logins/min
|
|
746
|
+
};
|
|
747
|
+
var MemoryRateLimiter = class {
|
|
748
|
+
buckets = /* @__PURE__ */ new Map();
|
|
749
|
+
limits;
|
|
750
|
+
now;
|
|
751
|
+
constructor(config = {}) {
|
|
752
|
+
if (process.env.NODE_ENV === "production" && !process.env.PAFI_ALLOW_MEMORY_RATE_LIMITER_IN_PROD) {
|
|
753
|
+
console.warn(
|
|
754
|
+
"[PAFI] MemoryRateLimiter not safe for multi-pod K8s deploys \u2014 rate counters are NOT shared across replicas, allowing round-robin bypass. Use a Redis-backed IRateLimiter in production."
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
this.limits = {
|
|
758
|
+
...DEFAULT_LIMITS,
|
|
759
|
+
...config.limits ?? {}
|
|
760
|
+
};
|
|
761
|
+
this.now = config.now ?? (() => Date.now());
|
|
762
|
+
}
|
|
763
|
+
async consume(key, action) {
|
|
764
|
+
const limit = this.limits[action];
|
|
765
|
+
if (!limit) return { allowed: true };
|
|
766
|
+
const bucketKey = `${action}:${key}`;
|
|
767
|
+
const now = this.now();
|
|
768
|
+
const bucket = this.buckets.get(bucketKey);
|
|
769
|
+
if (!bucket || now - bucket.windowStartedAt >= limit.windowMs) {
|
|
770
|
+
this.buckets.set(bucketKey, { count: 1, windowStartedAt: now });
|
|
771
|
+
return { allowed: true };
|
|
772
|
+
}
|
|
773
|
+
if (bucket.count < limit.max) {
|
|
774
|
+
bucket.count += 1;
|
|
775
|
+
return { allowed: true };
|
|
776
|
+
}
|
|
777
|
+
const retryAfterMs = Math.max(
|
|
778
|
+
0,
|
|
779
|
+
bucket.windowStartedAt + limit.windowMs - now
|
|
780
|
+
);
|
|
781
|
+
return { allowed: false, retryAfterMs };
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Test helper — clear all buckets. Not part of `IRateLimiter`; only
|
|
785
|
+
* exposed on the in-memory impl for unit tests.
|
|
786
|
+
*/
|
|
787
|
+
reset() {
|
|
788
|
+
this.buckets.clear();
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
var NoopRateLimiter = class {
|
|
792
|
+
warned = false;
|
|
793
|
+
async consume() {
|
|
794
|
+
if (!this.warned && process.env.NODE_ENV === "production") {
|
|
795
|
+
console.warn(
|
|
796
|
+
"[PAFI] NoopRateLimiter active \u2014 `/auth/nonce` and `/auth/login` are NOT throttled. Wire a `MemoryRateLimiter` (dev) or Redis-backed impl (prod) via `IssuerApiHandlersConfig.rateLimiter`."
|
|
797
|
+
);
|
|
798
|
+
this.warned = true;
|
|
799
|
+
}
|
|
800
|
+
return { allowed: true };
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
484
804
|
// src/relay/types.ts
|
|
485
805
|
var RelayError = class extends import_core.PafiSdkError {
|
|
486
806
|
httpStatus = "unprocessable";
|
|
@@ -828,6 +1148,7 @@ var PointIndexer = class {
|
|
|
828
1148
|
confirmations;
|
|
829
1149
|
batchSize;
|
|
830
1150
|
pollIntervalMs;
|
|
1151
|
+
onTickError;
|
|
831
1152
|
running = false;
|
|
832
1153
|
timer;
|
|
833
1154
|
constructor(config) {
|
|
@@ -843,6 +1164,7 @@ var PointIndexer = class {
|
|
|
843
1164
|
this.confirmations = BigInt(config.confirmations ?? DEFAULT_CONFIRMATIONS);
|
|
844
1165
|
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE));
|
|
845
1166
|
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1167
|
+
if (config.onTickError) this.onTickError = config.onTickError;
|
|
846
1168
|
}
|
|
847
1169
|
// -------------------------------------------------------------------------
|
|
848
1170
|
// Lifecycle
|
|
@@ -851,7 +1173,7 @@ var PointIndexer = class {
|
|
|
851
1173
|
start() {
|
|
852
1174
|
if (this.running) return;
|
|
853
1175
|
this.running = true;
|
|
854
|
-
|
|
1176
|
+
this.tick().catch((err) => this.handleTickError(err));
|
|
855
1177
|
}
|
|
856
1178
|
/** Stop polling. Safe to call multiple times. */
|
|
857
1179
|
stop() {
|
|
@@ -883,13 +1205,27 @@ var PointIndexer = class {
|
|
|
883
1205
|
}
|
|
884
1206
|
await this.processBlockRange(from, safeHead);
|
|
885
1207
|
} catch (err) {
|
|
886
|
-
|
|
1208
|
+
this.handleTickError(err);
|
|
887
1209
|
}
|
|
888
1210
|
this.scheduleNext();
|
|
889
1211
|
}
|
|
1212
|
+
handleTickError(err) {
|
|
1213
|
+
if (this.onTickError) {
|
|
1214
|
+
try {
|
|
1215
|
+
this.onTickError(err);
|
|
1216
|
+
} catch {
|
|
1217
|
+
console.error("[PAFI] PointIndexer onTickError threw:", err);
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
console.error("[PAFI] PointIndexer tick error:", err);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
890
1223
|
scheduleNext() {
|
|
891
1224
|
if (!this.running) return;
|
|
892
|
-
this.timer = setTimeout(
|
|
1225
|
+
this.timer = setTimeout(
|
|
1226
|
+
() => this.tick().catch((err) => this.handleTickError(err)),
|
|
1227
|
+
this.pollIntervalMs
|
|
1228
|
+
);
|
|
893
1229
|
}
|
|
894
1230
|
// -------------------------------------------------------------------------
|
|
895
1231
|
// Block scanning
|
|
@@ -1009,6 +1345,7 @@ var BurnIndexer = class {
|
|
|
1009
1345
|
confirmations;
|
|
1010
1346
|
batchSize;
|
|
1011
1347
|
pollIntervalMs;
|
|
1348
|
+
onTickError;
|
|
1012
1349
|
matchLockId;
|
|
1013
1350
|
running = false;
|
|
1014
1351
|
timer;
|
|
@@ -1027,6 +1364,7 @@ var BurnIndexer = class {
|
|
|
1027
1364
|
);
|
|
1028
1365
|
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1029
1366
|
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
1367
|
+
if (config.onTickError) this.onTickError = config.onTickError;
|
|
1030
1368
|
if (!config.matchLockId) {
|
|
1031
1369
|
throw new Error(
|
|
1032
1370
|
"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."
|
|
@@ -1037,7 +1375,7 @@ var BurnIndexer = class {
|
|
|
1037
1375
|
start() {
|
|
1038
1376
|
if (this.running) return;
|
|
1039
1377
|
this.running = true;
|
|
1040
|
-
|
|
1378
|
+
this.tick().catch((err) => this.handleTickError(err));
|
|
1041
1379
|
}
|
|
1042
1380
|
stop() {
|
|
1043
1381
|
this.running = false;
|
|
@@ -1063,13 +1401,27 @@ var BurnIndexer = class {
|
|
|
1063
1401
|
}
|
|
1064
1402
|
await this.processBlockRange(from, safeHead);
|
|
1065
1403
|
} catch (err) {
|
|
1066
|
-
|
|
1404
|
+
this.handleTickError(err);
|
|
1067
1405
|
}
|
|
1068
1406
|
this.scheduleNext();
|
|
1069
1407
|
}
|
|
1408
|
+
handleTickError(err) {
|
|
1409
|
+
if (this.onTickError) {
|
|
1410
|
+
try {
|
|
1411
|
+
this.onTickError(err);
|
|
1412
|
+
} catch {
|
|
1413
|
+
console.error("[PAFI] BurnIndexer onTickError threw:", err);
|
|
1414
|
+
}
|
|
1415
|
+
} else {
|
|
1416
|
+
console.error("[PAFI] BurnIndexer tick error:", err);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1070
1419
|
scheduleNext() {
|
|
1071
1420
|
if (!this.running) return;
|
|
1072
|
-
this.timer = setTimeout(
|
|
1421
|
+
this.timer = setTimeout(
|
|
1422
|
+
() => this.tick().catch((err) => this.handleTickError(err)),
|
|
1423
|
+
this.pollIntervalMs
|
|
1424
|
+
);
|
|
1073
1425
|
}
|
|
1074
1426
|
/**
|
|
1075
1427
|
* Scan `[from, to]` inclusive for burn events. Callers can drive this
|
|
@@ -1162,10 +1514,13 @@ var IssuerApiHandlers = class {
|
|
|
1162
1514
|
pafiWebUrl;
|
|
1163
1515
|
feeManager;
|
|
1164
1516
|
poolsProvider;
|
|
1517
|
+
redemption;
|
|
1518
|
+
rateLimiter;
|
|
1165
1519
|
constructor(config) {
|
|
1166
1520
|
this.authService = config.authService;
|
|
1167
1521
|
this.ledger = config.ledger;
|
|
1168
1522
|
this.provider = config.provider;
|
|
1523
|
+
this.rateLimiter = config.rateLimiter ?? new NoopRateLimiter();
|
|
1169
1524
|
const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
1170
1525
|
if (raw.length === 0) {
|
|
1171
1526
|
throw new Error(
|
|
@@ -1179,17 +1534,64 @@ var IssuerApiHandlers = class {
|
|
|
1179
1534
|
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1180
1535
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1181
1536
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1537
|
+
if (config.redemption) this.redemption = config.redemption;
|
|
1182
1538
|
}
|
|
1183
1539
|
// =========================================================================
|
|
1184
1540
|
// Public handlers (no auth required)
|
|
1185
1541
|
// =========================================================================
|
|
1186
|
-
/**
|
|
1187
|
-
|
|
1542
|
+
/**
|
|
1543
|
+
* `GET /auth/nonce`
|
|
1544
|
+
*
|
|
1545
|
+
* @param rateLimitKey Caller-side rate-limit key (typically client IP).
|
|
1546
|
+
* The HTTP layer (controller/middleware) extracts
|
|
1547
|
+
* this from the request and passes it through.
|
|
1548
|
+
* When omitted, no rate limit applies — production
|
|
1549
|
+
* callers SHOULD always pass a key.
|
|
1550
|
+
*/
|
|
1551
|
+
async handleGetNonce(rateLimitKey) {
|
|
1552
|
+
if (rateLimitKey) {
|
|
1553
|
+
const result = await this.rateLimiter.consume(
|
|
1554
|
+
rateLimitKey,
|
|
1555
|
+
"auth_nonce"
|
|
1556
|
+
);
|
|
1557
|
+
if (!result.allowed) {
|
|
1558
|
+
throw new import_core3.ValidationError(
|
|
1559
|
+
"RATE_LIMIT_EXCEEDED",
|
|
1560
|
+
"handleGetNonce: too many requests",
|
|
1561
|
+
{
|
|
1562
|
+
retryAfterMs: result.retryAfterMs ?? 0,
|
|
1563
|
+
action: "auth_nonce"
|
|
1564
|
+
}
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1188
1568
|
const nonce = await this.authService.getNonce();
|
|
1189
1569
|
return { nonce };
|
|
1190
1570
|
}
|
|
1191
|
-
/**
|
|
1192
|
-
|
|
1571
|
+
/**
|
|
1572
|
+
* `POST /auth/login`
|
|
1573
|
+
*
|
|
1574
|
+
* @param body Login message + signature.
|
|
1575
|
+
* @param rateLimitKey Caller-side rate-limit key (typically client IP
|
|
1576
|
+
* or `body.userAddress` if known). See `handleGetNonce`.
|
|
1577
|
+
*/
|
|
1578
|
+
async handleLogin(body, rateLimitKey) {
|
|
1579
|
+
if (rateLimitKey) {
|
|
1580
|
+
const result2 = await this.rateLimiter.consume(
|
|
1581
|
+
rateLimitKey,
|
|
1582
|
+
"auth_login"
|
|
1583
|
+
);
|
|
1584
|
+
if (!result2.allowed) {
|
|
1585
|
+
throw new import_core3.ValidationError(
|
|
1586
|
+
"RATE_LIMIT_EXCEEDED",
|
|
1587
|
+
"handleLogin: too many requests",
|
|
1588
|
+
{
|
|
1589
|
+
retryAfterMs: result2.retryAfterMs ?? 0,
|
|
1590
|
+
action: "auth_login"
|
|
1591
|
+
}
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1193
1595
|
if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
|
|
1194
1596
|
throw new import_core3.ValidationError(
|
|
1195
1597
|
"INVALID_LOGIN_BODY",
|
|
@@ -1332,6 +1734,74 @@ var IssuerApiHandlers = class {
|
|
|
1332
1734
|
// Note: legacy `handleClaim` (sync sponsored-claim returning calls[]) was
|
|
1333
1735
|
// removed in 0.5.43 — callers should use `PTClaimHandler` directly or
|
|
1334
1736
|
// wire `IssuerApiAdapter.claim()` which composes the full flow.
|
|
1737
|
+
/**
|
|
1738
|
+
* `GET /redemption/preview?pointToken=<addr>`
|
|
1739
|
+
*
|
|
1740
|
+
* Returns the headroom currently available to `userAddress` under the
|
|
1741
|
+
* configured RedemptionPolicy. Pure read — does not record anything.
|
|
1742
|
+
* Use this for UI to render "X PT redeemable now / next available at …".
|
|
1743
|
+
*/
|
|
1744
|
+
async handleRedemptionPreview(userAddress, request) {
|
|
1745
|
+
if (!this.redemption) {
|
|
1746
|
+
throw new ConfigurationError(
|
|
1747
|
+
"REDEMPTION_NOT_CONFIGURED",
|
|
1748
|
+
"handleRedemptionPreview: redemption is not configured on this issuer"
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
const tokenAddress = request.pointTokenAddress ? this.requireSupportedToken((0, import_viem6.getAddress)(request.pointTokenAddress), "handleRedemptionPreview") : void 0;
|
|
1752
|
+
const preview = await this.redemption.preview(
|
|
1753
|
+
(0, import_viem6.getAddress)(userAddress),
|
|
1754
|
+
tokenAddress
|
|
1755
|
+
);
|
|
1756
|
+
return preview;
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* `POST /redemption/evaluate`
|
|
1760
|
+
*
|
|
1761
|
+
* Pre-flight check before the issuer signs a BurnRequest. Returns
|
|
1762
|
+
* { allowed, denial?, preview }. Caller (the burn-orchestrator) MUST
|
|
1763
|
+
* re-check on the actual initiate path — evaluate is read-only and a
|
|
1764
|
+
* caller could race two requests under the same headroom. The intended
|
|
1765
|
+
* write path is: evaluate → sign BurnRequest → reserve pending credit
|
|
1766
|
+
* → call `service.redemption.recordSuccessfulInitiate()`.
|
|
1767
|
+
*/
|
|
1768
|
+
async handleRedemptionEvaluate(userAddress, request) {
|
|
1769
|
+
if (!this.redemption) {
|
|
1770
|
+
throw new ConfigurationError(
|
|
1771
|
+
"REDEMPTION_NOT_CONFIGURED",
|
|
1772
|
+
"handleRedemptionEvaluate: redemption is not configured on this issuer"
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
if (request.amountPt <= 0n) {
|
|
1776
|
+
throw new import_core3.ValidationError(
|
|
1777
|
+
"INVALID_AMOUNT",
|
|
1778
|
+
"handleRedemptionEvaluate: amountPt must be positive",
|
|
1779
|
+
{ amountPt: request.amountPt.toString() }
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
const tokenAddress = request.pointTokenAddress ? this.requireSupportedToken((0, import_viem6.getAddress)(request.pointTokenAddress), "handleRedemptionEvaluate") : void 0;
|
|
1783
|
+
const decision = await this.redemption.evaluate(
|
|
1784
|
+
(0, import_viem6.getAddress)(userAddress),
|
|
1785
|
+
request.amountPt,
|
|
1786
|
+
tokenAddress
|
|
1787
|
+
);
|
|
1788
|
+
const response = {
|
|
1789
|
+
allowed: decision.allowed,
|
|
1790
|
+
preview: decision.preview
|
|
1791
|
+
};
|
|
1792
|
+
if (decision.denial) response.denial = decision.denial;
|
|
1793
|
+
return response;
|
|
1794
|
+
}
|
|
1795
|
+
requireSupportedToken(pointToken, handler) {
|
|
1796
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1797
|
+
throw new import_core3.ValidationError(
|
|
1798
|
+
"UNSUPPORTED_POINT_TOKEN",
|
|
1799
|
+
`${handler}: unsupported pointToken ${pointToken}`,
|
|
1800
|
+
{ requested: pointToken }
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
return pointToken;
|
|
1804
|
+
}
|
|
1335
1805
|
};
|
|
1336
1806
|
|
|
1337
1807
|
// src/api/handlers/ptRedeemHandler.ts
|
|
@@ -1342,9 +1812,13 @@ var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
|
|
|
1342
1812
|
var PTRedeemError = class extends import_core.PafiSdkError {
|
|
1343
1813
|
httpStatus = "unprocessable";
|
|
1344
1814
|
code;
|
|
1345
|
-
|
|
1815
|
+
policyDenialCode;
|
|
1816
|
+
constructor(code, message, options) {
|
|
1346
1817
|
super(message);
|
|
1347
1818
|
this.code = code;
|
|
1819
|
+
if (options?.policyDenialCode) {
|
|
1820
|
+
this.policyDenialCode = options.policyDenialCode;
|
|
1821
|
+
}
|
|
1348
1822
|
}
|
|
1349
1823
|
};
|
|
1350
1824
|
var PTRedeemHandler = class {
|
|
@@ -1360,6 +1834,7 @@ var PTRedeemHandler = class {
|
|
|
1360
1834
|
redeemLockDurationMs;
|
|
1361
1835
|
signatureDeadlineSeconds;
|
|
1362
1836
|
now;
|
|
1837
|
+
redemptionService;
|
|
1363
1838
|
/**
|
|
1364
1839
|
* Per-user in-flight nonce guard (single-process only).
|
|
1365
1840
|
*
|
|
@@ -1402,6 +1877,9 @@ var PTRedeemHandler = class {
|
|
|
1402
1877
|
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1403
1878
|
this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
|
|
1404
1879
|
this.now = config.now ?? (() => Date.now());
|
|
1880
|
+
if (config.redemptionService) {
|
|
1881
|
+
this.redemptionService = config.redemptionService;
|
|
1882
|
+
}
|
|
1405
1883
|
}
|
|
1406
1884
|
async handle(request) {
|
|
1407
1885
|
if ((0, import_viem7.getAddress)(request.authenticatedAddress) !== (0, import_viem7.getAddress)(request.userAddress)) {
|
|
@@ -1413,6 +1891,21 @@ var PTRedeemHandler = class {
|
|
|
1413
1891
|
if (request.amount <= 0n) {
|
|
1414
1892
|
throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
|
|
1415
1893
|
}
|
|
1894
|
+
if (this.redemptionService) {
|
|
1895
|
+
const decision = await this.redemptionService.evaluate(
|
|
1896
|
+
request.userAddress,
|
|
1897
|
+
request.amount,
|
|
1898
|
+
this.pointTokenAddress
|
|
1899
|
+
);
|
|
1900
|
+
if (!decision.allowed) {
|
|
1901
|
+
const denial = decision.denial;
|
|
1902
|
+
throw new PTRedeemError(
|
|
1903
|
+
"REDEMPTION_POLICY_DENIED",
|
|
1904
|
+
`redemption denied: ${denial.message}`,
|
|
1905
|
+
{ policyDenialCode: denial.code }
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1416
1909
|
let burnNonce;
|
|
1417
1910
|
try {
|
|
1418
1911
|
burnNonce = await this.provider.readContract({
|
|
@@ -1566,6 +2059,15 @@ var PTRedeemHandler = class {
|
|
|
1566
2059
|
netCreditAmount: request.amount
|
|
1567
2060
|
};
|
|
1568
2061
|
}
|
|
2062
|
+
if (this.redemptionService) {
|
|
2063
|
+
await this.redemptionService.recordSuccessfulInitiate({
|
|
2064
|
+
user: request.userAddress,
|
|
2065
|
+
amountPt: request.amount,
|
|
2066
|
+
pointTokenAddress: this.pointTokenAddress,
|
|
2067
|
+
reservationId: sponsoredLockId
|
|
2068
|
+
}).catch(() => {
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
1569
2071
|
return {
|
|
1570
2072
|
lockId: sponsoredLockId,
|
|
1571
2073
|
userOp: sponsoredUserOp,
|
|
@@ -2432,31 +2934,6 @@ async function handleDelegateSubmit(params) {
|
|
|
2432
2934
|
return { userOpHash: result.userOpHash };
|
|
2433
2935
|
}
|
|
2434
2936
|
|
|
2435
|
-
// src/api/errorMapper.ts
|
|
2436
|
-
function createSdkErrorMapper(factories) {
|
|
2437
|
-
return (err) => {
|
|
2438
|
-
if (!(err instanceof import_core.PafiSdkError)) {
|
|
2439
|
-
throw err;
|
|
2440
|
-
}
|
|
2441
|
-
const body = {
|
|
2442
|
-
code: err.code,
|
|
2443
|
-
message: err.message,
|
|
2444
|
-
details: err.details,
|
|
2445
|
-
safeToRetry: err.safeToRetry
|
|
2446
|
-
};
|
|
2447
|
-
switch (err.httpStatus) {
|
|
2448
|
-
case "not_found":
|
|
2449
|
-
throw factories.notFound(body);
|
|
2450
|
-
case "forbidden":
|
|
2451
|
-
throw factories.forbidden(body);
|
|
2452
|
-
case "unprocessable":
|
|
2453
|
-
throw factories.unprocessable(body);
|
|
2454
|
-
case "service_unavailable":
|
|
2455
|
-
throw factories.serviceUnavailable(body);
|
|
2456
|
-
}
|
|
2457
|
-
};
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
2937
|
// src/api/issuerApiAdapter.ts
|
|
2461
2938
|
var import_node_crypto3 = require("crypto");
|
|
2462
2939
|
var import_viem11 = require("viem");
|
|
@@ -3289,6 +3766,7 @@ var BalanceAggregator = class {
|
|
|
3289
3766
|
};
|
|
3290
3767
|
|
|
3291
3768
|
// src/pafi-backend/client.ts
|
|
3769
|
+
var import_core17 = require("@pafi-dev/core");
|
|
3292
3770
|
function serializeBigInt(_key, value) {
|
|
3293
3771
|
return typeof value === "bigint" ? value.toString(10) : value;
|
|
3294
3772
|
}
|
|
@@ -3297,10 +3775,13 @@ function sleep(ms) {
|
|
|
3297
3775
|
}
|
|
3298
3776
|
var PafiBackendClient = class {
|
|
3299
3777
|
config;
|
|
3778
|
+
baseUrl;
|
|
3300
3779
|
constructor(config) {
|
|
3301
|
-
if (!config.
|
|
3780
|
+
if (!config.chainId) throw new Error("PafiBackendClient: chainId is required");
|
|
3302
3781
|
if (!config.issuerId) throw new Error("PafiBackendClient: issuerId is required");
|
|
3782
|
+
if (!config.apiKey) throw new Error("PafiBackendClient: apiKey is required");
|
|
3303
3783
|
this.config = config;
|
|
3784
|
+
this.baseUrl = (0, import_core17.getPafiServiceUrls)(config.chainId).sponsorRelayer;
|
|
3304
3785
|
}
|
|
3305
3786
|
async requestSponsorship(request) {
|
|
3306
3787
|
const maxAttempts = this.config.retry?.maxAttempts ?? 1;
|
|
@@ -3335,7 +3816,7 @@ var PafiBackendClient = class {
|
|
|
3335
3816
|
*/
|
|
3336
3817
|
async getUserOpReceipt(userOpHash) {
|
|
3337
3818
|
const fetchFn = this.config.fetchImpl ?? fetch;
|
|
3338
|
-
const url = `${this.
|
|
3819
|
+
const url = `${this.baseUrl}/bundler/receipt`;
|
|
3339
3820
|
let response;
|
|
3340
3821
|
try {
|
|
3341
3822
|
response = await fetchFn(url, {
|
|
@@ -3374,7 +3855,7 @@ var PafiBackendClient = class {
|
|
|
3374
3855
|
}
|
|
3375
3856
|
async relayUserOperation(request) {
|
|
3376
3857
|
const fetchFn = this.config.fetchImpl ?? fetch;
|
|
3377
|
-
const url = `${this.
|
|
3858
|
+
const url = `${this.baseUrl}/bundler/relay`;
|
|
3378
3859
|
let response;
|
|
3379
3860
|
try {
|
|
3380
3861
|
response = await fetchFn(url, {
|
|
@@ -3408,7 +3889,7 @@ var PafiBackendClient = class {
|
|
|
3408
3889
|
}
|
|
3409
3890
|
async _doRequest(request) {
|
|
3410
3891
|
const fetchFn = this.config.fetchImpl ?? fetch;
|
|
3411
|
-
const url = `${this.
|
|
3892
|
+
const url = `${this.baseUrl}/paymaster/sponsor`;
|
|
3412
3893
|
const body = JSON.stringify(request, serializeBigInt);
|
|
3413
3894
|
let response;
|
|
3414
3895
|
try {
|
|
@@ -3463,7 +3944,313 @@ var PafiBackendClient = class {
|
|
|
3463
3944
|
|
|
3464
3945
|
// src/config.ts
|
|
3465
3946
|
var import_viem14 = require("viem");
|
|
3466
|
-
var
|
|
3947
|
+
var import_core19 = require("@pafi-dev/core");
|
|
3948
|
+
|
|
3949
|
+
// src/redemption/evaluator.ts
|
|
3950
|
+
var SECONDS_PER_DAY = 24 * 60 * 60;
|
|
3951
|
+
function evaluateRedemption(input) {
|
|
3952
|
+
const { policy, history, amountPt, nowUnixSec, policySource } = input;
|
|
3953
|
+
const dailyRemaining = policy.dailyLimitPt > history.redeemedLast24hPt ? policy.dailyLimitPt - history.redeemedLast24hPt : 0n;
|
|
3954
|
+
const cooldownUntilUnixSec = history.lastRedeemedAtUnixSec !== null ? history.lastRedeemedAtUnixSec + policy.cooldownSec : null;
|
|
3955
|
+
const inCooldown = cooldownUntilUnixSec !== null && cooldownUntilUnixSec > nowUnixSec;
|
|
3956
|
+
const activeBlackout = findActiveBlackout(policy.blackoutWindows, nowUnixSec);
|
|
3957
|
+
const nextBlackoutEnd = nextBlackoutEndAfter(
|
|
3958
|
+
policy.blackoutWindows,
|
|
3959
|
+
nowUnixSec
|
|
3960
|
+
);
|
|
3961
|
+
let availableAmountPt = 0n;
|
|
3962
|
+
if (!inCooldown && !activeBlackout) {
|
|
3963
|
+
const headroom = dailyRemaining < policy.perTxMaxPt ? dailyRemaining : policy.perTxMaxPt;
|
|
3964
|
+
availableAmountPt = headroom >= policy.perTxMinPt ? headroom : 0n;
|
|
3965
|
+
}
|
|
3966
|
+
const preview = {
|
|
3967
|
+
availableAmountPt,
|
|
3968
|
+
dailyRemainingPt: dailyRemaining,
|
|
3969
|
+
cooldownUntilUnixSec: inCooldown ? cooldownUntilUnixSec : null,
|
|
3970
|
+
nextBlackoutEndsAtUnixSec: activeBlackout ? activeBlackout.endUnixSec : nextBlackoutEnd,
|
|
3971
|
+
perTxMinPt: policy.perTxMinPt,
|
|
3972
|
+
perTxMaxPt: policy.perTxMaxPt,
|
|
3973
|
+
policyVersion: policy.version,
|
|
3974
|
+
policySource
|
|
3975
|
+
};
|
|
3976
|
+
if (amountPt <= 0n) {
|
|
3977
|
+
return { allowed: false, preview, denial: rejectAmountBelowMin(policy) };
|
|
3978
|
+
}
|
|
3979
|
+
const denial = firstDenial({
|
|
3980
|
+
amountPt,
|
|
3981
|
+
policy,
|
|
3982
|
+
dailyRemaining,
|
|
3983
|
+
inCooldown,
|
|
3984
|
+
cooldownUntilUnixSec,
|
|
3985
|
+
activeBlackout
|
|
3986
|
+
});
|
|
3987
|
+
if (denial) return { allowed: false, denial, preview };
|
|
3988
|
+
return { allowed: true, preview };
|
|
3989
|
+
}
|
|
3990
|
+
function firstDenial(args) {
|
|
3991
|
+
const { amountPt, policy, dailyRemaining, inCooldown, cooldownUntilUnixSec, activeBlackout } = args;
|
|
3992
|
+
if (activeBlackout) {
|
|
3993
|
+
return {
|
|
3994
|
+
code: "BLACKOUT_WINDOW",
|
|
3995
|
+
message: `Redemption is blocked until ${new Date(
|
|
3996
|
+
activeBlackout.endUnixSec * 1e3
|
|
3997
|
+
).toISOString()}${activeBlackout.reason ? ` (${activeBlackout.reason})` : ""}`
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
if (inCooldown && cooldownUntilUnixSec !== null) {
|
|
4001
|
+
return {
|
|
4002
|
+
code: "COOLDOWN_ACTIVE",
|
|
4003
|
+
message: `Cooldown active until ${new Date(
|
|
4004
|
+
cooldownUntilUnixSec * 1e3
|
|
4005
|
+
).toISOString()}`
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
if (amountPt < policy.perTxMinPt) {
|
|
4009
|
+
return {
|
|
4010
|
+
code: "AMOUNT_BELOW_MIN",
|
|
4011
|
+
message: `amount ${amountPt} below per-tx minimum ${policy.perTxMinPt}`
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
if (amountPt > policy.perTxMaxPt) {
|
|
4015
|
+
return {
|
|
4016
|
+
code: "AMOUNT_ABOVE_MAX",
|
|
4017
|
+
message: `amount ${amountPt} above per-tx maximum ${policy.perTxMaxPt}`
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
if (amountPt > dailyRemaining) {
|
|
4021
|
+
return {
|
|
4022
|
+
code: "DAILY_LIMIT_EXCEEDED",
|
|
4023
|
+
message: `amount ${amountPt} exceeds daily remaining ${dailyRemaining}`
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
return null;
|
|
4027
|
+
}
|
|
4028
|
+
function rejectAmountBelowMin(policy) {
|
|
4029
|
+
return {
|
|
4030
|
+
code: "AMOUNT_BELOW_MIN",
|
|
4031
|
+
message: `amount must be >= ${policy.perTxMinPt}`
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
function findActiveBlackout(windows, nowUnixSec) {
|
|
4035
|
+
for (const w of windows) {
|
|
4036
|
+
if (w.startUnixSec <= nowUnixSec && nowUnixSec < w.endUnixSec) return w;
|
|
4037
|
+
}
|
|
4038
|
+
return null;
|
|
4039
|
+
}
|
|
4040
|
+
function nextBlackoutEndAfter(windows, nowUnixSec) {
|
|
4041
|
+
let earliest = null;
|
|
4042
|
+
for (const w of windows) {
|
|
4043
|
+
if (w.endUnixSec > nowUnixSec) {
|
|
4044
|
+
if (earliest === null || w.endUnixSec < earliest) earliest = w.endUnixSec;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
return earliest;
|
|
4048
|
+
}
|
|
4049
|
+
var REDEMPTION_HISTORY_WINDOW_SEC = SECONDS_PER_DAY;
|
|
4050
|
+
|
|
4051
|
+
// src/redemption/settlementClient.ts
|
|
4052
|
+
var import_core18 = require("@pafi-dev/core");
|
|
4053
|
+
var DEFAULT_TIMEOUT_MS = 1e3;
|
|
4054
|
+
var SettlementClient = class {
|
|
4055
|
+
config;
|
|
4056
|
+
constructor(config) {
|
|
4057
|
+
if (!config.chainId) throw new Error("SettlementClient: chainId is required");
|
|
4058
|
+
if (!config.issuerId) throw new Error("SettlementClient: issuerId is required");
|
|
4059
|
+
if (!config.apiKey) throw new Error("SettlementClient: apiKey is required");
|
|
4060
|
+
this.config = {
|
|
4061
|
+
baseUrl: (0, import_core18.getPafiServiceUrls)(config.chainId).issuerApi.replace(/\/+$/, ""),
|
|
4062
|
+
issuerId: config.issuerId,
|
|
4063
|
+
apiKey: config.apiKey,
|
|
4064
|
+
fetchTimeoutMs: config.fetchTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
4065
|
+
fetchImpl: config.fetchImpl
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
async fetchPolicy() {
|
|
4069
|
+
const fetchFn = this.config.fetchImpl ?? fetch;
|
|
4070
|
+
const url = `${this.config.baseUrl}/issuers/${encodeURIComponent(this.config.issuerId)}/redemption-policy`;
|
|
4071
|
+
const controller = new AbortController();
|
|
4072
|
+
const timer = setTimeout(
|
|
4073
|
+
() => controller.abort(),
|
|
4074
|
+
this.config.fetchTimeoutMs
|
|
4075
|
+
);
|
|
4076
|
+
let response;
|
|
4077
|
+
try {
|
|
4078
|
+
response = await fetchFn(url, {
|
|
4079
|
+
method: "GET",
|
|
4080
|
+
headers: {
|
|
4081
|
+
"Content-Type": "application/json",
|
|
4082
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
4083
|
+
"X-Issuer-Id": this.config.issuerId
|
|
4084
|
+
},
|
|
4085
|
+
signal: controller.signal
|
|
4086
|
+
});
|
|
4087
|
+
} catch (err) {
|
|
4088
|
+
const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted|timeout/i.test(err.message ?? ""));
|
|
4089
|
+
return { ok: false, reason: isAbort ? "TIMEOUT" : "NETWORK" };
|
|
4090
|
+
} finally {
|
|
4091
|
+
clearTimeout(timer);
|
|
4092
|
+
}
|
|
4093
|
+
if (response.status === 404) {
|
|
4094
|
+
return { ok: false, reason: "NOT_FOUND", status: 404 };
|
|
4095
|
+
}
|
|
4096
|
+
if (response.status === 401 || response.status === 403) {
|
|
4097
|
+
return { ok: false, reason: "UNAUTHORIZED", status: response.status };
|
|
4098
|
+
}
|
|
4099
|
+
if (!response.ok) {
|
|
4100
|
+
return { ok: false, reason: "SERVER_ERROR", status: response.status };
|
|
4101
|
+
}
|
|
4102
|
+
let raw;
|
|
4103
|
+
try {
|
|
4104
|
+
raw = await response.json();
|
|
4105
|
+
} catch {
|
|
4106
|
+
return { ok: false, reason: "INVALID_RESPONSE", status: response.status };
|
|
4107
|
+
}
|
|
4108
|
+
const parsed = parsePolicyDto(raw);
|
|
4109
|
+
if (!parsed) {
|
|
4110
|
+
return { ok: false, reason: "INVALID_RESPONSE", status: response.status };
|
|
4111
|
+
}
|
|
4112
|
+
return { ok: true, policy: parsed };
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
function parsePolicyDto(raw) {
|
|
4116
|
+
if (!raw || typeof raw !== "object") return null;
|
|
4117
|
+
const dto = raw;
|
|
4118
|
+
if (typeof dto.issuerId !== "string" || typeof dto.dailyLimitPt !== "string" || typeof dto.cooldownSec !== "number" || typeof dto.perTxMinPt !== "string" || typeof dto.perTxMaxPt !== "string" || typeof dto.version !== "string" || !Array.isArray(dto.blackoutWindows)) {
|
|
4119
|
+
return null;
|
|
4120
|
+
}
|
|
4121
|
+
try {
|
|
4122
|
+
return {
|
|
4123
|
+
issuerId: dto.issuerId,
|
|
4124
|
+
dailyLimitPt: BigInt(dto.dailyLimitPt),
|
|
4125
|
+
cooldownSec: dto.cooldownSec,
|
|
4126
|
+
perTxMinPt: BigInt(dto.perTxMinPt),
|
|
4127
|
+
perTxMaxPt: BigInt(dto.perTxMaxPt),
|
|
4128
|
+
blackoutWindows: dto.blackoutWindows.map(normalizeBlackout).filter((b) => b !== null),
|
|
4129
|
+
version: dto.version
|
|
4130
|
+
};
|
|
4131
|
+
} catch {
|
|
4132
|
+
return null;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
function normalizeBlackout(raw) {
|
|
4136
|
+
if (!raw || typeof raw !== "object") return null;
|
|
4137
|
+
const win = raw;
|
|
4138
|
+
if (typeof win.startUnixSec !== "number" || typeof win.endUnixSec !== "number" || win.startUnixSec >= win.endUnixSec) {
|
|
4139
|
+
return null;
|
|
4140
|
+
}
|
|
4141
|
+
return {
|
|
4142
|
+
startUnixSec: win.startUnixSec,
|
|
4143
|
+
endUnixSec: win.endUnixSec,
|
|
4144
|
+
reason: typeof win.reason === "string" ? win.reason : void 0
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// src/redemption/defaults.ts
|
|
4149
|
+
var PT_DECIMALS = 10n ** 18n;
|
|
4150
|
+
var DEFAULT_REDEMPTION_POLICY = {
|
|
4151
|
+
issuerId: "default",
|
|
4152
|
+
dailyLimitPt: 1000n * PT_DECIMALS,
|
|
4153
|
+
cooldownSec: 60,
|
|
4154
|
+
perTxMinPt: 1n * PT_DECIMALS,
|
|
4155
|
+
perTxMaxPt: 500n * PT_DECIMALS,
|
|
4156
|
+
blackoutWindows: [],
|
|
4157
|
+
version: "default-v1"
|
|
4158
|
+
};
|
|
4159
|
+
function defaultPolicyFor(issuerId) {
|
|
4160
|
+
return { ...DEFAULT_REDEMPTION_POLICY, issuerId };
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
// src/redemption/policyProvider.ts
|
|
4164
|
+
var DEFAULT_CACHE_TTL_MS3 = 5 * 60 * 1e3;
|
|
4165
|
+
var PolicyProvider = class {
|
|
4166
|
+
client;
|
|
4167
|
+
issuerId;
|
|
4168
|
+
cacheTtlMs;
|
|
4169
|
+
now;
|
|
4170
|
+
cache = null;
|
|
4171
|
+
inflight = null;
|
|
4172
|
+
constructor(config) {
|
|
4173
|
+
this.client = new SettlementClient(config);
|
|
4174
|
+
this.issuerId = config.issuerId;
|
|
4175
|
+
this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS3;
|
|
4176
|
+
this.now = config.now ?? (() => Date.now());
|
|
4177
|
+
}
|
|
4178
|
+
async getPolicy() {
|
|
4179
|
+
const fresh = this.readCache();
|
|
4180
|
+
if (fresh) return { policy: fresh, source: "cache" };
|
|
4181
|
+
if (this.inflight) return this.inflight;
|
|
4182
|
+
this.inflight = this.fetchAndStore().finally(() => {
|
|
4183
|
+
this.inflight = null;
|
|
4184
|
+
});
|
|
4185
|
+
return this.inflight;
|
|
4186
|
+
}
|
|
4187
|
+
/** Drop cached policy. Next getPolicy() will refetch. */
|
|
4188
|
+
invalidate() {
|
|
4189
|
+
this.cache = null;
|
|
4190
|
+
}
|
|
4191
|
+
readCache() {
|
|
4192
|
+
if (!this.cache) return null;
|
|
4193
|
+
if (this.cache.expiresAtMs <= this.now()) {
|
|
4194
|
+
this.cache = null;
|
|
4195
|
+
return null;
|
|
4196
|
+
}
|
|
4197
|
+
return this.cache.policy;
|
|
4198
|
+
}
|
|
4199
|
+
async fetchAndStore() {
|
|
4200
|
+
const result = await this.client.fetchPolicy();
|
|
4201
|
+
if (result.ok) {
|
|
4202
|
+
this.cache = {
|
|
4203
|
+
policy: result.policy,
|
|
4204
|
+
expiresAtMs: this.now() + this.cacheTtlMs
|
|
4205
|
+
};
|
|
4206
|
+
return { policy: result.policy, source: "settlement" };
|
|
4207
|
+
}
|
|
4208
|
+
return { policy: defaultPolicyFor(this.issuerId), source: "default" };
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
|
|
4212
|
+
// src/redemption/service.ts
|
|
4213
|
+
var RedemptionService = class {
|
|
4214
|
+
policyProvider;
|
|
4215
|
+
historyStore;
|
|
4216
|
+
nowUnixSec;
|
|
4217
|
+
constructor(config) {
|
|
4218
|
+
this.policyProvider = config.policyProvider instanceof PolicyProvider ? config.policyProvider : new PolicyProvider(config.policyProvider);
|
|
4219
|
+
this.historyStore = config.historyStore;
|
|
4220
|
+
this.nowUnixSec = config.nowUnixSec ?? (() => Math.floor(Date.now() / 1e3));
|
|
4221
|
+
}
|
|
4222
|
+
async preview(user, pointTokenAddress) {
|
|
4223
|
+
const decision = await this.evaluate(user, 0n, pointTokenAddress);
|
|
4224
|
+
return decision.preview;
|
|
4225
|
+
}
|
|
4226
|
+
async evaluate(user, amountPt, pointTokenAddress) {
|
|
4227
|
+
const { policy, source } = await this.policyProvider.getPolicy();
|
|
4228
|
+
const now = this.nowUnixSec();
|
|
4229
|
+
const [redeemedLast24hPt, lastRedeemedAtUnixSec] = await Promise.all([
|
|
4230
|
+
this.historyStore.sumRedeemedSince(
|
|
4231
|
+
user,
|
|
4232
|
+
now - REDEMPTION_HISTORY_WINDOW_SEC,
|
|
4233
|
+
pointTokenAddress
|
|
4234
|
+
),
|
|
4235
|
+
this.historyStore.getLastRedeemedAtUnixSec(user, pointTokenAddress)
|
|
4236
|
+
]);
|
|
4237
|
+
return evaluateRedemption({
|
|
4238
|
+
policy,
|
|
4239
|
+
policySource: source,
|
|
4240
|
+
history: { redeemedLast24hPt, lastRedeemedAtUnixSec },
|
|
4241
|
+
amountPt,
|
|
4242
|
+
nowUnixSec: now
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
async recordSuccessfulInitiate(entry) {
|
|
4246
|
+
await this.historyStore.recordRedemption({
|
|
4247
|
+
...entry,
|
|
4248
|
+
unixSec: this.nowUnixSec()
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
};
|
|
4252
|
+
|
|
4253
|
+
// src/config.ts
|
|
3467
4254
|
function createIssuerService(config) {
|
|
3468
4255
|
if (!config.provider) {
|
|
3469
4256
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -3533,7 +4320,7 @@ function createIssuerService(config) {
|
|
|
3533
4320
|
indexers.set(tokenAddress, new PointIndexer(indexerConfig));
|
|
3534
4321
|
}
|
|
3535
4322
|
const firstIndexer = indexers.get(tokenAddresses[0]);
|
|
3536
|
-
const chainAddresses = (0,
|
|
4323
|
+
const chainAddresses = (0, import_core19.getContractAddresses)(config.chainId);
|
|
3537
4324
|
const resolvedContracts = {
|
|
3538
4325
|
batchExecutor: chainAddresses.batchExecutor,
|
|
3539
4326
|
usdt: chainAddresses.usdt,
|
|
@@ -3542,6 +4329,25 @@ function createIssuerService(config) {
|
|
|
3542
4329
|
pafiHook: chainAddresses.pafiHook,
|
|
3543
4330
|
...config.contracts
|
|
3544
4331
|
};
|
|
4332
|
+
let redemption;
|
|
4333
|
+
if (config.redemption) {
|
|
4334
|
+
const policyConfig = {
|
|
4335
|
+
chainId: config.chainId,
|
|
4336
|
+
issuerId: config.redemption.issuerId,
|
|
4337
|
+
apiKey: config.redemption.apiKey
|
|
4338
|
+
};
|
|
4339
|
+
if (config.redemption.fetchImpl) policyConfig.fetchImpl = config.redemption.fetchImpl;
|
|
4340
|
+
if (config.redemption.fetchTimeoutMs !== void 0) {
|
|
4341
|
+
policyConfig.fetchTimeoutMs = config.redemption.fetchTimeoutMs;
|
|
4342
|
+
}
|
|
4343
|
+
if (config.redemption.cacheTtlMs !== void 0) {
|
|
4344
|
+
policyConfig.cacheTtlMs = config.redemption.cacheTtlMs;
|
|
4345
|
+
}
|
|
4346
|
+
redemption = new RedemptionService({
|
|
4347
|
+
policyProvider: new PolicyProvider(policyConfig),
|
|
4348
|
+
historyStore: config.redemption.historyStore
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
3545
4351
|
const handlersConfig = {
|
|
3546
4352
|
authService,
|
|
3547
4353
|
ledger,
|
|
@@ -3552,6 +4358,7 @@ function createIssuerService(config) {
|
|
|
3552
4358
|
};
|
|
3553
4359
|
if (feeManager) handlersConfig.feeManager = feeManager;
|
|
3554
4360
|
if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
|
|
4361
|
+
if (redemption) handlersConfig.redemption = redemption;
|
|
3555
4362
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
3556
4363
|
if (config.indexer?.autoStart) {
|
|
3557
4364
|
for (const idx of indexers.values()) {
|
|
@@ -3567,13 +4374,14 @@ function createIssuerService(config) {
|
|
|
3567
4374
|
fee: feeManager,
|
|
3568
4375
|
indexers,
|
|
3569
4376
|
indexer: firstIndexer,
|
|
3570
|
-
api: handlers
|
|
4377
|
+
api: handlers,
|
|
4378
|
+
redemption
|
|
3571
4379
|
};
|
|
3572
4380
|
}
|
|
3573
4381
|
|
|
3574
4382
|
// src/issuer-state/validator.ts
|
|
3575
4383
|
var import_viem15 = require("viem");
|
|
3576
|
-
var
|
|
4384
|
+
var import_core20 = require("@pafi-dev/core");
|
|
3577
4385
|
var ISSUER_RECORD_TTL_MS = 3e4;
|
|
3578
4386
|
var IssuerStateValidator = class _IssuerStateValidator {
|
|
3579
4387
|
constructor(provider, registryAddress) {
|
|
@@ -3590,7 +4398,7 @@ var IssuerStateValidator = class _IssuerStateValidator {
|
|
|
3590
4398
|
* `CONTRACT_ADDRESSES` map for the given chain.
|
|
3591
4399
|
*/
|
|
3592
4400
|
static forChain(provider, chainId) {
|
|
3593
|
-
const { issuerRegistry } = (0,
|
|
4401
|
+
const { issuerRegistry } = (0, import_core20.getContractAddresses)(chainId);
|
|
3594
4402
|
return new _IssuerStateValidator(provider, issuerRegistry);
|
|
3595
4403
|
}
|
|
3596
4404
|
/**
|
|
@@ -3619,7 +4427,7 @@ var IssuerStateValidator = class _IssuerStateValidator {
|
|
|
3619
4427
|
if (cached) return cached;
|
|
3620
4428
|
const issuer = await this.provider.readContract({
|
|
3621
4429
|
address: key,
|
|
3622
|
-
abi:
|
|
4430
|
+
abi: import_core20.POINT_TOKEN_V2_ABI,
|
|
3623
4431
|
functionName: "issuer"
|
|
3624
4432
|
});
|
|
3625
4433
|
this.pointTokenIssuerCache.set(key, (0, import_viem15.getAddress)(issuer));
|
|
@@ -3700,13 +4508,13 @@ var IssuerStateValidator = class _IssuerStateValidator {
|
|
|
3700
4508
|
const [issuerTuple, totalSupply] = await Promise.all([
|
|
3701
4509
|
this.provider.readContract({
|
|
3702
4510
|
address: this.registryAddress,
|
|
3703
|
-
abi:
|
|
4511
|
+
abi: import_core20.issuerRegistryGetIssuerFlatAbi,
|
|
3704
4512
|
functionName: "getIssuer",
|
|
3705
4513
|
args: [issuerAddr]
|
|
3706
4514
|
}),
|
|
3707
4515
|
this.provider.readContract({
|
|
3708
4516
|
address: tokenAddr,
|
|
3709
|
-
abi:
|
|
4517
|
+
abi: import_core20.POINT_TOKEN_V2_ABI,
|
|
3710
4518
|
functionName: "totalSupply"
|
|
3711
4519
|
})
|
|
3712
4520
|
]);
|
|
@@ -3727,8 +4535,44 @@ var IssuerStateValidator = class _IssuerStateValidator {
|
|
|
3727
4535
|
}
|
|
3728
4536
|
};
|
|
3729
4537
|
|
|
4538
|
+
// src/redemption/memoryHistoryStore.ts
|
|
4539
|
+
var MemoryRedemptionHistoryStore = class {
|
|
4540
|
+
entries = [];
|
|
4541
|
+
async sumRedeemedSince(user, sinceUnixSec, pointTokenAddress) {
|
|
4542
|
+
const userKey = user.toLowerCase();
|
|
4543
|
+
const tokenKey = pointTokenAddress?.toLowerCase() ?? null;
|
|
4544
|
+
let total = 0n;
|
|
4545
|
+
for (const e of this.entries) {
|
|
4546
|
+
if (e.user !== userKey) continue;
|
|
4547
|
+
if (e.unixSec < sinceUnixSec) continue;
|
|
4548
|
+
if (tokenKey !== null && e.pointTokenAddress !== tokenKey) continue;
|
|
4549
|
+
total += e.amountPt;
|
|
4550
|
+
}
|
|
4551
|
+
return total;
|
|
4552
|
+
}
|
|
4553
|
+
async getLastRedeemedAtUnixSec(user, pointTokenAddress) {
|
|
4554
|
+
const userKey = user.toLowerCase();
|
|
4555
|
+
const tokenKey = pointTokenAddress?.toLowerCase() ?? null;
|
|
4556
|
+
let latest = null;
|
|
4557
|
+
for (const e of this.entries) {
|
|
4558
|
+
if (e.user !== userKey) continue;
|
|
4559
|
+
if (tokenKey !== null && e.pointTokenAddress !== tokenKey) continue;
|
|
4560
|
+
if (latest === null || e.unixSec > latest) latest = e.unixSec;
|
|
4561
|
+
}
|
|
4562
|
+
return latest;
|
|
4563
|
+
}
|
|
4564
|
+
async recordRedemption(entry) {
|
|
4565
|
+
this.entries.push({
|
|
4566
|
+
user: entry.user.toLowerCase(),
|
|
4567
|
+
amountPt: entry.amountPt,
|
|
4568
|
+
pointTokenAddress: entry.pointTokenAddress?.toLowerCase() ?? null,
|
|
4569
|
+
unixSec: entry.unixSec
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4573
|
+
|
|
3730
4574
|
// src/index.ts
|
|
3731
|
-
var PAFI_ISSUER_SDK_VERSION = true ? "0.
|
|
4575
|
+
var PAFI_ISSUER_SDK_VERSION = true ? "0.9.0" : "dev";
|
|
3732
4576
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3733
4577
|
0 && (module.exports = {
|
|
3734
4578
|
AdapterMisconfiguredError,
|
|
@@ -3739,6 +4583,7 @@ var PAFI_ISSUER_SDK_VERSION = true ? "0.7.8" : "dev";
|
|
|
3739
4583
|
BundlerRejectedError,
|
|
3740
4584
|
BurnIndexer,
|
|
3741
4585
|
ConfigurationError,
|
|
4586
|
+
DEFAULT_REDEMPTION_POLICY,
|
|
3742
4587
|
DefaultPolicyEngine,
|
|
3743
4588
|
FeeManager,
|
|
3744
4589
|
InMemoryCursorStore,
|
|
@@ -3748,8 +4593,11 @@ var PAFI_ISSUER_SDK_VERSION = true ? "0.7.8" : "dev";
|
|
|
3748
4593
|
IssuerStateValidator,
|
|
3749
4594
|
LockNotFoundError,
|
|
3750
4595
|
MemoryPendingUserOpStore,
|
|
4596
|
+
MemoryRateLimiter,
|
|
4597
|
+
MemoryRedemptionHistoryStore,
|
|
3751
4598
|
MemorySessionStore,
|
|
3752
4599
|
NonceManager,
|
|
4600
|
+
NoopRateLimiter,
|
|
3753
4601
|
PAFI_ISSUER_SDK_VERSION,
|
|
3754
4602
|
PAFI_SUBGRAPH_URL,
|
|
3755
4603
|
PTClaimError,
|
|
@@ -3764,21 +4612,34 @@ var PAFI_ISSUER_SDK_VERSION = true ? "0.7.8" : "dev";
|
|
|
3764
4612
|
PerpDepositError,
|
|
3765
4613
|
PerpDepositHandler,
|
|
3766
4614
|
PointIndexer,
|
|
4615
|
+
PolicyProvider,
|
|
4616
|
+
REDEMPTION_HISTORY_WINDOW_SEC,
|
|
4617
|
+
RedemptionService,
|
|
3767
4618
|
RelayError,
|
|
3768
4619
|
RelayService,
|
|
4620
|
+
SDK_ERROR_HTTP_STATUS_CODE,
|
|
4621
|
+
SettlementClient,
|
|
3769
4622
|
ValidationError,
|
|
3770
4623
|
authenticateRequest,
|
|
4624
|
+
buildErrorEnvelope,
|
|
4625
|
+
buildSdkErrorBody,
|
|
3771
4626
|
createIssuerService,
|
|
3772
4627
|
createNativePtQuoter,
|
|
3773
4628
|
createSdkErrorMapper,
|
|
3774
4629
|
createSubgraphNativeUsdtQuoter,
|
|
3775
4630
|
createSubgraphPoolsProvider,
|
|
4631
|
+
defaultErrorTypeForStatus,
|
|
4632
|
+
defaultPolicyFor,
|
|
4633
|
+
evaluateRedemption,
|
|
3776
4634
|
handleClaimStatus,
|
|
3777
4635
|
handleDelegateSubmit,
|
|
3778
4636
|
handleMobilePrepare,
|
|
3779
4637
|
handleMobileSubmit,
|
|
3780
4638
|
handleRedeemStatus,
|
|
3781
4639
|
mergePaymasterFields,
|
|
4640
|
+
payloadFromGenericError,
|
|
4641
|
+
payloadFromHttpException,
|
|
4642
|
+
payloadFromPafiSdkError,
|
|
3782
4643
|
prepareMobileUserOp,
|
|
3783
4644
|
relayUserOp,
|
|
3784
4645
|
requestPaymaster,
|