@pafi-dev/issuer 0.15.2 → 0.18.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/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  DEFAULT_REDEMPTION_POLICY: () => DEFAULT_REDEMPTION_POLICY,
31
31
  DefaultPolicyEngine: () => DefaultPolicyEngine,
32
32
  FeeManager: () => FeeManager,
33
+ GasUnitsCache: () => GasUnitsCache,
33
34
  InMemoryCursorStore: () => InMemoryCursorStore,
34
35
  IssuerApiAdapter: () => IssuerApiAdapter,
35
36
  IssuerApiHandlers: () => IssuerApiHandlers,
@@ -1093,22 +1094,172 @@ var RelayService = class {
1093
1094
  }
1094
1095
  });
1095
1096
  }
1097
+ // =========================================================================
1098
+ // Preview methods — produce bundler-ready partial UserOps WITHOUT signing.
1099
+ //
1100
+ // These exist so callers can compute an accurate gas estimate via
1101
+ // `bundlerClient.estimateUserOperationGas(...)` BEFORE committing to a
1102
+ // signed MintRequest / BurnRequest (signing is HSM-backed and expensive
1103
+ // in prod). The returned `callData` matches the SHAPE of the real call;
1104
+ // the EIP-712 signature bytes are placeholder. Bundler simulation
1105
+ // doesn't validate the signature, so the gas units come back accurate.
1106
+ //
1107
+ // Cache-wise: same SC version + same scenario → same calldata shape →
1108
+ // same bundler-returned gas units. The first call seeds the cache; the
1109
+ // rest hit it.
1110
+ // =========================================================================
1111
+ /**
1112
+ * Build a dummy `PartialUserOperation` for the mint scenario, suitable
1113
+ * for `feeManager.estimateGasFee({ partialUserOp, ... })`. NO signing —
1114
+ * uses a 65-byte zero signature in place of the real minter sig.
1115
+ */
1116
+ previewMintUserOp(params) {
1117
+ const useWrapper = params.mintFeeWrapperAddress !== void 0;
1118
+ let mintCallData;
1119
+ let mintTarget;
1120
+ if (useWrapper) {
1121
+ mintCallData = (0, import_viem3.encodeFunctionData)({
1122
+ abi: import_core5.mintFeeWrapperAbi,
1123
+ functionName: "mintWithFee",
1124
+ args: [
1125
+ params.pointTokenAddress,
1126
+ params.userAddress,
1127
+ params.amount,
1128
+ params.deadline,
1129
+ PLACEHOLDER_SIG_65
1130
+ ]
1131
+ });
1132
+ mintTarget = params.mintFeeWrapperAddress;
1133
+ } else {
1134
+ mintCallData = (0, import_viem3.encodeFunctionData)({
1135
+ abi: import_core5.POINT_TOKEN_ABI,
1136
+ functionName: "mint",
1137
+ args: [
1138
+ params.userAddress,
1139
+ params.amount,
1140
+ params.deadline,
1141
+ PLACEHOLDER_SIG_65
1142
+ ]
1143
+ });
1144
+ mintTarget = params.pointTokenAddress;
1145
+ }
1146
+ return (0, import_core5.buildPartialUserOperation)({
1147
+ sender: params.userAddress,
1148
+ nonce: params.aaNonce,
1149
+ operations: [{ target: mintTarget, value: 0n, data: mintCallData }],
1150
+ // Gas limits ignored by bundler estimate — it computes them.
1151
+ gasLimits: { callGasLimit: 1n, verificationGasLimit: 1n, preVerificationGas: 1n }
1152
+ });
1153
+ }
1154
+ /** Burn-side mirror of `previewMintUserOp`. */
1155
+ previewBurnUserOp(params) {
1156
+ const burnCallData = (0, import_viem3.encodeFunctionData)({
1157
+ abi: import_core5.POINT_TOKEN_ABI,
1158
+ functionName: "burn",
1159
+ args: [
1160
+ params.userAddress,
1161
+ params.amount,
1162
+ params.deadline,
1163
+ PLACEHOLDER_SIG_65
1164
+ ]
1165
+ });
1166
+ return (0, import_core5.buildPartialUserOperation)({
1167
+ sender: params.userAddress,
1168
+ nonce: params.aaNonce,
1169
+ operations: [
1170
+ { target: params.pointTokenAddress, value: 0n, data: burnCallData }
1171
+ ],
1172
+ gasLimits: { callGasLimit: 1n, verificationGasLimit: 1n, preVerificationGas: 1n }
1173
+ });
1174
+ }
1096
1175
  };
1176
+ var PLACEHOLDER_SIG_65 = `0x${"00".repeat(65)}`;
1097
1177
  function errorMessage(err) {
1098
1178
  return err instanceof Error ? err.message : String(err);
1099
1179
  }
1100
1180
 
1181
+ // src/relay/gasUnitsCache.ts
1182
+ var import_viem4 = require("viem");
1183
+ var DEFAULT_TTL_MS = 5 * 6e4;
1184
+ var DEFAULT_CODEHASH_TTL_MS = 60 * 6e4;
1185
+ var DEFAULT_MAX_ENTRIES = 100;
1186
+ var GasUnitsCache = class {
1187
+ entries = /* @__PURE__ */ new Map();
1188
+ codehashEntries = /* @__PURE__ */ new Map();
1189
+ ttlMs;
1190
+ codehashTtlMs;
1191
+ maxEntries;
1192
+ constructor(config = {}) {
1193
+ this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
1194
+ this.codehashTtlMs = config.codehashTtlMs ?? DEFAULT_CODEHASH_TTL_MS;
1195
+ this.maxEntries = config.maxEntries ?? DEFAULT_MAX_ENTRIES;
1196
+ }
1197
+ async buildKey(params) {
1198
+ const codehash = await this.getCodehash(
1199
+ params.provider,
1200
+ params.contractAddress
1201
+ );
1202
+ const pm = params.paymasterAddress?.toLowerCase() ?? "0x0";
1203
+ return `${params.scenario}:${codehash}:${pm}`;
1204
+ }
1205
+ get(key, now = Date.now()) {
1206
+ const entry = this.entries.get(key);
1207
+ if (!entry) return null;
1208
+ if (entry.expiresAt <= now) {
1209
+ this.entries.delete(key);
1210
+ return null;
1211
+ }
1212
+ return entry.gasUnits;
1213
+ }
1214
+ set(key, gasUnits, now = Date.now()) {
1215
+ if (this.entries.size >= this.maxEntries && !this.entries.has(key)) {
1216
+ const eldest = this.entries.keys().next().value;
1217
+ if (eldest !== void 0) this.entries.delete(eldest);
1218
+ }
1219
+ this.entries.set(key, { gasUnits, expiresAt: now + this.ttlMs });
1220
+ }
1221
+ invalidate() {
1222
+ this.entries.clear();
1223
+ this.codehashEntries.clear();
1224
+ }
1225
+ size() {
1226
+ return this.entries.size;
1227
+ }
1228
+ async getCodehash(provider, address) {
1229
+ const lower = address.toLowerCase();
1230
+ const now = Date.now();
1231
+ const cached = this.codehashEntries.get(lower);
1232
+ if (cached && cached.expiresAt > now) return cached.codehash;
1233
+ const code = await provider.getCode({ address });
1234
+ const codehash = code ? (0, import_viem4.keccak256)(code) : "0x0";
1235
+ this.codehashEntries.set(lower, {
1236
+ codehash,
1237
+ expiresAt: now + this.codehashTtlMs
1238
+ });
1239
+ return codehash;
1240
+ }
1241
+ };
1242
+
1101
1243
  // src/relay/feeManager.ts
1102
1244
  var DEFAULT_GAS_UNITS = 500000n;
1103
- var DEFAULT_PREMIUM_BPS = 12e3;
1245
+ var DEFAULT_PREMIUM_BPS = 1e4;
1246
+ var DEFAULT_PAYMASTER_OVERHEAD = 80000n;
1247
+ var DUMMY_SIGNATURE = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";
1104
1248
  var FeeManager = class _FeeManager {
1105
1249
  provider;
1106
1250
  gasUnits;
1107
1251
  gasPremiumBps;
1108
1252
  quoteNativeToFee;
1253
+ bundlerClient;
1254
+ cache;
1255
+ paymasterOverheadGas;
1256
+ metrics;
1257
+ // Short-lived in-flight fee cache (legacy behavior). Distinct from
1258
+ // `cache` — that one stores gasUnits per scenario; this one stores the
1259
+ // FULL computed fee value, valid for 10s to absorb burst calls.
1109
1260
  cachedFee = null;
1110
1261
  cacheExpiresAt = 0;
1111
- static CACHE_TTL_MS = 1e4;
1262
+ static FEE_CACHE_TTL_MS = 1e4;
1112
1263
  constructor(config) {
1113
1264
  if (!config.provider) throw new Error("FeeManager: provider required");
1114
1265
  if (!config.quoteNativeToFee)
@@ -1117,32 +1268,101 @@ var FeeManager = class _FeeManager {
1117
1268
  this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
1118
1269
  this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
1119
1270
  this.quoteNativeToFee = config.quoteNativeToFee;
1271
+ this.bundlerClient = config.bundlerClient;
1272
+ this.cache = new GasUnitsCache(config.cache);
1273
+ this.paymasterOverheadGas = config.paymasterOverheadGas ?? DEFAULT_PAYMASTER_OVERHEAD;
1274
+ this.metrics = config.metrics;
1120
1275
  }
1121
1276
  /**
1122
1277
  * Estimate the fee (in the caller's fee currency) to charge for the
1123
- * next sponsored UserOp:
1278
+ * next sponsored UserOp.
1124
1279
  *
1280
+ * gasUnits = bundler-estimated (cached) or hardcoded fallback
1125
1281
  * nativeCost = gasUnits × gasPrice
1126
1282
  * withPremium = nativeCost × premiumBps / 10_000
1127
1283
  * fee = quoteNativeToFee(withPremium)
1128
1284
  *
1129
- * For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
1130
- * from the response, the name `estimateGasFee` is kept — but the
1131
- * currency depends on how the caller wired `quoteNativeToFee`.
1285
+ * When `opts.partialUserOp` is omitted, behaves exactly like v0.16.x:
1286
+ * uses the hardcoded `gasUnits` default.
1132
1287
  */
1133
- async estimateGasFee() {
1288
+ async estimateGasFee(opts = {}) {
1134
1289
  const now = Date.now();
1135
- if (this.cachedFee !== null && now < this.cacheExpiresAt) {
1290
+ const isLegacyCall = !opts.partialUserOp && !opts.scenario && !opts.contractAddress;
1291
+ if (isLegacyCall && this.cachedFee !== null && now < this.cacheExpiresAt) {
1136
1292
  return this.cachedFee;
1137
1293
  }
1294
+ const t0 = Date.now();
1295
+ const { gasUnits, source } = await this.resolveGasUnits(opts);
1296
+ const latencyMs = Date.now() - t0;
1297
+ this.safeEmit(
1298
+ () => this.metrics?.onEstimate?.({
1299
+ source,
1300
+ scenario: opts.scenario,
1301
+ gasUnits,
1302
+ latencyMs
1303
+ })
1304
+ );
1138
1305
  const gasPrice = await this.provider.getGasPrice();
1139
- const nativeCost = gasPrice * this.gasUnits;
1306
+ const nativeCost = gasPrice * gasUnits;
1140
1307
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
1141
1308
  const fee = await this.quoteNativeToFee(withPremium);
1142
- this.cachedFee = fee;
1143
- this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
1309
+ if (isLegacyCall) {
1310
+ this.cachedFee = fee;
1311
+ this.cacheExpiresAt = now + _FeeManager.FEE_CACHE_TTL_MS;
1312
+ }
1144
1313
  return fee;
1145
1314
  }
1315
+ /**
1316
+ * Manually purge the per-scenario gas-units cache. Useful after an SC
1317
+ * upgrade when ops wants the next estimate to refresh immediately
1318
+ * (the codehash check would catch it on the NEXT call anyway, but
1319
+ * this forces it now).
1320
+ */
1321
+ invalidateCache() {
1322
+ this.cache.invalidate();
1323
+ this.cachedFee = null;
1324
+ this.cacheExpiresAt = 0;
1325
+ }
1326
+ async resolveGasUnits(opts) {
1327
+ if (!this.bundlerClient || !opts.partialUserOp || !opts.scenario || !opts.contractAddress) {
1328
+ return { gasUnits: this.gasUnits, source: "fallback" };
1329
+ }
1330
+ try {
1331
+ const cacheKey = await this.cache.buildKey({
1332
+ scenario: opts.scenario,
1333
+ contractAddress: opts.contractAddress,
1334
+ paymasterAddress: opts.paymasterAddress,
1335
+ provider: this.provider
1336
+ });
1337
+ const cached = this.cache.get(cacheKey);
1338
+ if (cached !== null) {
1339
+ return { gasUnits: cached, source: "cache" };
1340
+ }
1341
+ const estimate = await this.bundlerClient.estimateUserOperationGas({
1342
+ sender: opts.partialUserOp.sender,
1343
+ nonce: opts.partialUserOp.nonce,
1344
+ callData: opts.partialUserOp.callData,
1345
+ signature: opts.partialUserOp.signature ?? DUMMY_SIGNATURE
1346
+ // Intentionally NO paymaster fields — avoids chicken-and-egg
1347
+ // (paymasterData depends on gasLimits). Overhead added below.
1348
+ });
1349
+ const gasUnits = estimate.callGasLimit + estimate.verificationGasLimit + estimate.preVerificationGas + (estimate.paymasterVerificationGasLimit ?? 0n) + (estimate.paymasterPostOpGasLimit ?? 0n) + this.paymasterOverheadGas;
1350
+ this.cache.set(cacheKey, gasUnits);
1351
+ return { gasUnits, source: "bundler" };
1352
+ } catch (err) {
1353
+ const reason = err instanceof Error ? err.message : String(err);
1354
+ this.safeEmit(
1355
+ () => this.metrics?.onBundlerError?.({ scenario: opts.scenario, reason })
1356
+ );
1357
+ return { gasUnits: this.gasUnits, source: "fallback" };
1358
+ }
1359
+ }
1360
+ safeEmit(fn) {
1361
+ try {
1362
+ fn();
1363
+ } catch {
1364
+ }
1365
+ }
1146
1366
  };
1147
1367
 
1148
1368
  // src/indexer/types.ts
@@ -1157,11 +1377,11 @@ var InMemoryCursorStore = class {
1157
1377
  };
1158
1378
 
1159
1379
  // src/indexer/pointIndexer.ts
1160
- var import_viem4 = require("viem");
1161
- var TRANSFER_EVENT = (0, import_viem4.parseAbiItem)(
1380
+ var import_viem5 = require("viem");
1381
+ var TRANSFER_EVENT = (0, import_viem5.parseAbiItem)(
1162
1382
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1163
1383
  );
1164
- var MINT_WITH_FEE_EVENT = (0, import_viem4.parseAbiItem)(
1384
+ var MINT_WITH_FEE_EVENT = (0, import_viem5.parseAbiItem)(
1165
1385
  "event MintWithFee(address indexed pointToken, address indexed to, uint256 grossAmount, uint256 netAmount, uint256 feeAmount)"
1166
1386
  );
1167
1387
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
@@ -1171,7 +1391,7 @@ var DEFAULT_BATCH_SIZE = 2000n;
1171
1391
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
1172
1392
  function isNoWrapper(addr) {
1173
1393
  if (!addr) return true;
1174
- const checksummed = (0, import_viem4.getAddress)(addr);
1394
+ const checksummed = (0, import_viem5.getAddress)(addr);
1175
1395
  return checksummed === ZERO_ADDRESS || checksummed === DEAD_ADDRESS;
1176
1396
  }
1177
1397
  var PointIndexer = class {
@@ -1193,8 +1413,8 @@ var PointIndexer = class {
1193
1413
  throw new Error("PointIndexer: pointTokenAddress required");
1194
1414
  if (!config.ledger) throw new Error("PointIndexer: ledger required");
1195
1415
  this.provider = config.provider;
1196
- this.pointTokenAddress = (0, import_viem4.getAddress)(config.pointTokenAddress);
1197
- this.mintFeeWrapperAddress = isNoWrapper(config.mintFeeWrapperAddress) ? void 0 : (0, import_viem4.getAddress)(config.mintFeeWrapperAddress);
1416
+ this.pointTokenAddress = (0, import_viem5.getAddress)(config.pointTokenAddress);
1417
+ this.mintFeeWrapperAddress = isNoWrapper(config.mintFeeWrapperAddress) ? void 0 : (0, import_viem5.getAddress)(config.mintFeeWrapperAddress);
1198
1418
  this.ledger = config.ledger;
1199
1419
  this.cursorStore = config.cursorStore ?? new InMemoryCursorStore();
1200
1420
  this.startBlock = config.fromBlock ?? 0n;
@@ -1313,9 +1533,9 @@ var PointIndexer = class {
1313
1533
  if (!args.pointToken || !args.to || args.grossAmount === void 0 || log.blockNumber === null || log.transactionHash === null) {
1314
1534
  continue;
1315
1535
  }
1316
- if ((0, import_viem4.getAddress)(args.pointToken) !== this.pointTokenAddress) continue;
1536
+ if ((0, import_viem5.getAddress)(args.pointToken) !== this.pointTokenAddress) continue;
1317
1537
  out.push({
1318
- to: (0, import_viem4.getAddress)(args.to),
1538
+ to: (0, import_viem5.getAddress)(args.to),
1319
1539
  amount: args.grossAmount,
1320
1540
  blockNumber: log.blockNumber,
1321
1541
  txHash: log.transactionHash,
@@ -1343,10 +1563,10 @@ var PointIndexer = class {
1343
1563
  for (const log of logs) {
1344
1564
  const args = log.args;
1345
1565
  if (!args.from || !args.to || args.value === void 0) continue;
1346
- if ((0, import_viem4.getAddress)(args.from) !== ZERO_ADDRESS) continue;
1566
+ if ((0, import_viem5.getAddress)(args.from) !== ZERO_ADDRESS) continue;
1347
1567
  if (log.blockNumber === null || log.transactionHash === null) continue;
1348
1568
  out.push({
1349
- to: (0, import_viem4.getAddress)(args.to),
1569
+ to: (0, import_viem5.getAddress)(args.to),
1350
1570
  amount: args.value,
1351
1571
  blockNumber: log.blockNumber,
1352
1572
  txHash: log.transactionHash,
@@ -1405,8 +1625,8 @@ function pickMatchingLock(locks, amount) {
1405
1625
  }
1406
1626
 
1407
1627
  // src/indexer/burnIndexer.ts
1408
- var import_viem5 = require("viem");
1409
- var TRANSFER_EVENT2 = (0, import_viem5.parseAbiItem)(
1628
+ var import_viem6 = require("viem");
1629
+ var TRANSFER_EVENT2 = (0, import_viem6.parseAbiItem)(
1410
1630
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1411
1631
  );
1412
1632
  var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
@@ -1537,10 +1757,10 @@ var BurnIndexer = class {
1537
1757
  for (const log of logs) {
1538
1758
  const args = log.args;
1539
1759
  if (!args.from || !args.to || args.value === void 0) continue;
1540
- if ((0, import_viem5.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
1760
+ if ((0, import_viem6.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
1541
1761
  if (log.blockNumber === null || log.transactionHash === null) continue;
1542
1762
  out.push({
1543
- from: (0, import_viem5.getAddress)(args.from),
1763
+ from: (0, import_viem6.getAddress)(args.from),
1544
1764
  amount: args.value,
1545
1765
  blockNumber: log.blockNumber,
1546
1766
  txHash: log.transactionHash,
@@ -1575,7 +1795,7 @@ var BurnIndexer = class {
1575
1795
  };
1576
1796
 
1577
1797
  // src/api/handlers.ts
1578
- var import_viem6 = require("viem");
1798
+ var import_viem7 = require("viem");
1579
1799
  var import_core6 = require("@pafi-dev/core");
1580
1800
  var IssuerApiHandlers = class _IssuerApiHandlers {
1581
1801
  authService;
@@ -1612,7 +1832,7 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1612
1832
  "IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
1613
1833
  );
1614
1834
  }
1615
- const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
1835
+ const normalized = raw.map((a) => (0, import_viem7.getAddress)(a));
1616
1836
  this.supportedTokens = new Set(normalized);
1617
1837
  this.chainId = config.chainId;
1618
1838
  this.contracts = config.contracts;
@@ -1621,7 +1841,7 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1621
1841
  if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
1622
1842
  if (config.redemption) this.redemption = config.redemption;
1623
1843
  if (config.mintFeeWrapperAddress) {
1624
- this.mintFeeWrapperAddress = (0, import_viem6.getAddress)(config.mintFeeWrapperAddress);
1844
+ this.mintFeeWrapperAddress = (0, import_viem7.getAddress)(config.mintFeeWrapperAddress);
1625
1845
  }
1626
1846
  }
1627
1847
  // =========================================================================
@@ -1828,8 +2048,8 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1828
2048
  { requested: request.chainId, supported: this.chainId }
1829
2049
  );
1830
2050
  }
1831
- const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
1832
- const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
2051
+ const normalizedAuthed = (0, import_viem7.getAddress)(userAddress);
2052
+ const normalizedRequest = (0, import_viem7.getAddress)(request.userAddress);
1833
2053
  if (normalizedAuthed !== normalizedRequest) {
1834
2054
  throw new import_core3.ValidationError(
1835
2055
  "USER_ADDRESS_MISMATCH",
@@ -1837,7 +2057,7 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1837
2057
  { authenticated: normalizedAuthed, requested: normalizedRequest }
1838
2058
  );
1839
2059
  }
1840
- const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
2060
+ const pointToken = (0, import_viem7.getAddress)(request.pointTokenAddress);
1841
2061
  if (!this.supportedTokens.has(pointToken)) {
1842
2062
  throw new import_core3.ValidationError(
1843
2063
  "UNSUPPORTED_POINT_TOKEN",
@@ -1878,9 +2098,9 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1878
2098
  "handleRedemptionPreview: redemption is not configured on this issuer"
1879
2099
  );
1880
2100
  }
1881
- const tokenAddress = request.pointTokenAddress ? this.requireSupportedToken((0, import_viem6.getAddress)(request.pointTokenAddress), "handleRedemptionPreview") : void 0;
2101
+ const tokenAddress = request.pointTokenAddress ? this.requireSupportedToken((0, import_viem7.getAddress)(request.pointTokenAddress), "handleRedemptionPreview") : void 0;
1882
2102
  const preview = await this.redemption.preview(
1883
- (0, import_viem6.getAddress)(userAddress),
2103
+ (0, import_viem7.getAddress)(userAddress),
1884
2104
  tokenAddress
1885
2105
  );
1886
2106
  return preview;
@@ -1909,9 +2129,9 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1909
2129
  { amountPt: request.amountPt.toString() }
1910
2130
  );
1911
2131
  }
1912
- const tokenAddress = request.pointTokenAddress ? this.requireSupportedToken((0, import_viem6.getAddress)(request.pointTokenAddress), "handleRedemptionEvaluate") : void 0;
2132
+ const tokenAddress = request.pointTokenAddress ? this.requireSupportedToken((0, import_viem7.getAddress)(request.pointTokenAddress), "handleRedemptionEvaluate") : void 0;
1913
2133
  const decision = await this.redemption.evaluate(
1914
- (0, import_viem6.getAddress)(userAddress),
2134
+ (0, import_viem7.getAddress)(userAddress),
1915
2135
  request.amountPt,
1916
2136
  tokenAddress
1917
2137
  );
@@ -1935,7 +2155,7 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1935
2155
  };
1936
2156
 
1937
2157
  // src/api/handlers/ptRedeemHandler.ts
1938
- var import_viem7 = require("viem");
2158
+ var import_viem8 = require("viem");
1939
2159
  var import_core7 = require("@pafi-dev/core");
1940
2160
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1941
2161
  var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
@@ -1996,8 +2216,8 @@ var PTRedeemHandler = class {
1996
2216
  this.relayService = config.relayService;
1997
2217
  this.provider = config.provider;
1998
2218
  this.feeService = config.feeService;
1999
- this.pointTokenAddress = (0, import_viem7.getAddress)(config.pointTokenAddress);
2000
- this.batchExecutorAddress = (0, import_viem7.getAddress)(config.batchExecutorAddress);
2219
+ this.pointTokenAddress = (0, import_viem8.getAddress)(config.pointTokenAddress);
2220
+ this.batchExecutorAddress = (0, import_viem8.getAddress)(config.batchExecutorAddress);
2001
2221
  this.chainId = config.chainId;
2002
2222
  this.domain = config.domain;
2003
2223
  this.burnerSignerWallet = config.burnerSignerWallet;
@@ -2012,7 +2232,7 @@ var PTRedeemHandler = class {
2012
2232
  }
2013
2233
  }
2014
2234
  async handle(request) {
2015
- if ((0, import_viem7.getAddress)(request.authenticatedAddress) !== (0, import_viem7.getAddress)(request.userAddress)) {
2235
+ if ((0, import_viem8.getAddress)(request.authenticatedAddress) !== (0, import_viem8.getAddress)(request.userAddress)) {
2016
2236
  throw new PTRedeemError(
2017
2237
  "UNAUTHORIZED",
2018
2238
  `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
@@ -2050,7 +2270,7 @@ var PTRedeemHandler = class {
2050
2270
  `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
2051
2271
  );
2052
2272
  }
2053
- const userKey = (0, import_viem7.getAddress)(request.userAddress).toLowerCase();
2273
+ const userKey = (0, import_viem8.getAddress)(request.userAddress).toLowerCase();
2054
2274
  let userNonces = this.inFlightNonces.get(userKey);
2055
2275
  if (!userNonces) {
2056
2276
  userNonces = /* @__PURE__ */ new Set();
@@ -2071,11 +2291,29 @@ var PTRedeemHandler = class {
2071
2291
  }
2072
2292
  }
2073
2293
  async _handleAfterNonceLock(request, burnNonce) {
2294
+ const previewDeadline = BigInt(
2295
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
2296
+ );
2074
2297
  let fee;
2075
2298
  if (request.feeAmount !== void 0) {
2076
2299
  fee = request.feeAmount > 0n ? request.feeAmount : 0n;
2077
2300
  } else if (this.feeService) {
2078
- fee = await this.feeService.estimateGasFee();
2301
+ const previewUserOp = this.relayService.previewBurnUserOp({
2302
+ userAddress: request.userAddress,
2303
+ aaNonce: burnNonce,
2304
+ pointTokenAddress: this.pointTokenAddress,
2305
+ amount: request.amount,
2306
+ deadline: previewDeadline
2307
+ });
2308
+ fee = await this.feeService.estimateGasFee({
2309
+ scenario: "burn",
2310
+ contractAddress: this.pointTokenAddress,
2311
+ partialUserOp: {
2312
+ sender: previewUserOp.sender,
2313
+ nonce: previewUserOp.nonce,
2314
+ callData: previewUserOp.callData
2315
+ }
2316
+ });
2079
2317
  } else {
2080
2318
  fee = 0n;
2081
2319
  }
@@ -2097,9 +2335,7 @@ var PTRedeemHandler = class {
2097
2335
  `insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
2098
2336
  );
2099
2337
  }
2100
- const deadline = BigInt(
2101
- Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
2102
- );
2338
+ const deadline = previewDeadline;
2103
2339
  const domain = {
2104
2340
  name: this.domain.name,
2105
2341
  chainId: this.chainId,
@@ -2316,7 +2552,7 @@ async function handleRedeemStatus(params) {
2316
2552
  }
2317
2553
 
2318
2554
  // src/api/mobileHandlers.ts
2319
- var import_viem8 = require("viem");
2555
+ var import_viem9 = require("viem");
2320
2556
  var import_core10 = require("@pafi-dev/core");
2321
2557
 
2322
2558
  // src/userop-store/serialize.ts
@@ -2662,7 +2898,7 @@ async function handleMobileSubmit(params) {
2662
2898
  if (!entry) {
2663
2899
  throw new PendingUserOpNotFoundError(params.lockId);
2664
2900
  }
2665
- if ((0, import_viem8.getAddress)(entry.sender) !== (0, import_viem8.getAddress)(params.authenticatedAddress)) {
2901
+ if ((0, import_viem9.getAddress)(entry.sender) !== (0, import_viem9.getAddress)(params.authenticatedAddress)) {
2666
2902
  throw new PendingUserOpForbiddenError(params.lockId);
2667
2903
  }
2668
2904
  const variant = params.variant ?? "sponsored";
@@ -2678,7 +2914,7 @@ async function handleMobileSubmit(params) {
2678
2914
  }
2679
2915
 
2680
2916
  // src/api/handlers/ptClaimHandler.ts
2681
- var import_viem9 = require("viem");
2917
+ var import_viem10 = require("viem");
2682
2918
  var import_core11 = require("@pafi-dev/core");
2683
2919
 
2684
2920
  // src/issuer-state/types.ts
@@ -2724,7 +2960,7 @@ var PTClaimHandler = class {
2724
2960
  };
2725
2961
  }
2726
2962
  async handle(request) {
2727
- if ((0, import_viem9.getAddress)(request.authenticatedAddress) !== (0, import_viem9.getAddress)(request.userAddress)) {
2963
+ if ((0, import_viem10.getAddress)(request.authenticatedAddress) !== (0, import_viem10.getAddress)(request.userAddress)) {
2728
2964
  throw new PTClaimError(
2729
2965
  "VALIDATION_FAILED",
2730
2966
  `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
@@ -2762,7 +2998,23 @@ var PTClaimHandler = class {
2762
2998
  const signatureDeadline = BigInt(
2763
2999
  Math.floor(this.cfg.now() / 1e3) + this.cfg.signatureDeadlineSeconds
2764
3000
  );
2765
- const feeAmount = this.cfg.feeService ? await this.cfg.feeService.estimateGasFee() : 0n;
3001
+ const previewUserOp = this.cfg.relayService.previewMintUserOp({
3002
+ userAddress: request.userAddress,
3003
+ aaNonce: request.aaNonce,
3004
+ pointTokenAddress: request.pointTokenAddress,
3005
+ amount: request.amount,
3006
+ deadline: signatureDeadline,
3007
+ mintFeeWrapperAddress: resolvedWrapper
3008
+ });
3009
+ const feeAmount = this.cfg.feeService ? await this.cfg.feeService.estimateGasFee({
3010
+ scenario: resolvedWrapper ? "mint-wrapped" : "mint",
3011
+ contractAddress: request.pointTokenAddress,
3012
+ partialUserOp: {
3013
+ sender: previewUserOp.sender,
3014
+ nonce: previewUserOp.nonce,
3015
+ callData: previewUserOp.callData
3016
+ }
3017
+ }) : 0n;
2766
3018
  const domain = {
2767
3019
  name: this.cfg.pointTokenDomainName,
2768
3020
  chainId: request.chainId,
@@ -2962,7 +3214,7 @@ var PerpDepositHandler = class {
2962
3214
 
2963
3215
  // src/api/delegateHandler.ts
2964
3216
  var import_core13 = require("@pafi-dev/core");
2965
- var import_viem10 = require("viem");
3217
+ var import_viem11 = require("viem");
2966
3218
  var DEFAULT_DELEGATE_GAS = {
2967
3219
  callGasLimit: 100000n,
2968
3220
  verificationGasLimit: 150000n,
@@ -3049,7 +3301,7 @@ async function handleDelegateSubmit(params) {
3049
3301
  if (!entry) {
3050
3302
  throw new PendingUserOpNotFoundError(params.lockId);
3051
3303
  }
3052
- if ((0, import_viem10.getAddress)(entry.sender) !== (0, import_viem10.getAddress)(params.authenticatedAddress)) {
3304
+ if ((0, import_viem11.getAddress)(entry.sender) !== (0, import_viem11.getAddress)(params.authenticatedAddress)) {
3053
3305
  throw new PendingUserOpForbiddenError(params.lockId);
3054
3306
  }
3055
3307
  if (!entry.eip7702Auth) {
@@ -3070,7 +3322,7 @@ async function handleDelegateSubmit(params) {
3070
3322
 
3071
3323
  // src/api/issuerApiAdapter.ts
3072
3324
  var import_node_crypto3 = require("crypto");
3073
- var import_viem11 = require("viem");
3325
+ var import_viem12 = require("viem");
3074
3326
  var import_core14 = require("@pafi-dev/core");
3075
3327
  var AdapterMisconfiguredError = class extends Error {
3076
3328
  code = "ADAPTER_MISCONFIGURED";
@@ -3128,7 +3380,7 @@ var IssuerApiAdapter = class {
3128
3380
  async pools(authenticatedAddress, chainId, pointTokenAddress) {
3129
3381
  const result = await this.cfg.issuerService.api.handlePools(
3130
3382
  authenticatedAddress,
3131
- { chainId, pointTokenAddress: (0, import_viem11.getAddress)(pointTokenAddress) }
3383
+ { chainId, pointTokenAddress: (0, import_viem12.getAddress)(pointTokenAddress) }
3132
3384
  );
3133
3385
  return { pools: result.pools };
3134
3386
  }
@@ -3137,8 +3389,8 @@ var IssuerApiAdapter = class {
3137
3389
  authenticatedAddress,
3138
3390
  {
3139
3391
  chainId,
3140
- userAddress: (0, import_viem11.getAddress)(userAddress),
3141
- pointTokenAddress: (0, import_viem11.getAddress)(pointTokenAddress)
3392
+ userAddress: (0, import_viem12.getAddress)(userAddress),
3393
+ pointTokenAddress: (0, import_viem12.getAddress)(pointTokenAddress)
3142
3394
  }
3143
3395
  );
3144
3396
  return {
@@ -3159,7 +3411,7 @@ var IssuerApiAdapter = class {
3159
3411
  "ptClaimHandler",
3160
3412
  "claim"
3161
3413
  );
3162
- const pointTokenAddress = (0, import_viem11.getAddress)(input.pointTokenAddress);
3414
+ const pointTokenAddress = (0, import_viem12.getAddress)(input.pointTokenAddress);
3163
3415
  const result = await ptClaimHandler.handle({
3164
3416
  authenticatedAddress: input.authenticatedAddress,
3165
3417
  userAddress: input.authenticatedAddress,
@@ -3254,7 +3506,7 @@ var IssuerApiAdapter = class {
3254
3506
  "ptClaimHandler",
3255
3507
  "claimPrepare"
3256
3508
  );
3257
- const pointTokenAddress = (0, import_viem11.getAddress)(input.pointTokenAddress);
3509
+ const pointTokenAddress = (0, import_viem12.getAddress)(input.pointTokenAddress);
3258
3510
  const claimResult = await ptClaimHandler.handle({
3259
3511
  authenticatedAddress: input.authenticatedAddress,
3260
3512
  userAddress: input.authenticatedAddress,
@@ -3300,7 +3552,7 @@ var IssuerApiAdapter = class {
3300
3552
  }
3301
3553
  async redeemPrepare(input) {
3302
3554
  this.assertRedeemHandler();
3303
- const pointTokenAddress = (0, import_viem11.getAddress)(input.pointTokenAddress);
3555
+ const pointTokenAddress = (0, import_viem12.getAddress)(input.pointTokenAddress);
3304
3556
  const redeemResponse = await this.cfg.ptRedeemHandler.handle({
3305
3557
  userAddress: input.authenticatedAddress,
3306
3558
  authenticatedAddress: input.authenticatedAddress,
@@ -3490,7 +3742,7 @@ var IssuerApiAdapter = class {
3490
3742
  };
3491
3743
 
3492
3744
  // src/pools/subgraphPoolsProvider.ts
3493
- var import_viem12 = require("viem");
3745
+ var import_viem13 = require("viem");
3494
3746
  var import_core15 = require("@pafi-dev/core");
3495
3747
  var DEFAULT_CACHE_TTL_MS = 3e4;
3496
3748
  var MAX_REASONABLE_FEE_TIER = 1e6;
@@ -3613,7 +3865,7 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress,
3613
3865
  return [];
3614
3866
  }
3615
3867
  const { pool } = token;
3616
- if (!(0, import_viem12.isAddress)(pool.token0.id) || !(0, import_viem12.isAddress)(pool.token1.id)) {
3868
+ if (!(0, import_viem13.isAddress)(pool.token0.id) || !(0, import_viem13.isAddress)(pool.token1.id)) {
3617
3869
  const error = new Error(
3618
3870
  "[PAFI] SubgraphPoolsProvider: invalid token address in response"
3619
3871
  );
@@ -3766,8 +4018,8 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
3766
4018
  }
3767
4019
 
3768
4020
  // src/pools/nativePtQuoter.ts
3769
- var import_viem13 = require("viem");
3770
- var CHAINLINK_ABI = (0, import_viem13.parseAbi)([
4021
+ var import_viem14 = require("viem");
4022
+ var CHAINLINK_ABI = (0, import_viem14.parseAbi)([
3771
4023
  "function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)"
3772
4024
  ]);
3773
4025
  var CHAINLINK_MAX_AGE_S = 3600n;
@@ -4051,7 +4303,7 @@ var PafiBackendClient = class {
4051
4303
  };
4052
4304
 
4053
4305
  // src/config.ts
4054
- var import_viem14 = require("viem");
4306
+ var import_viem15 = require("viem");
4055
4307
  var import_core18 = require("@pafi-dev/core");
4056
4308
 
4057
4309
  // src/redemption/evaluator.ts
@@ -4378,7 +4630,7 @@ function createIssuerService(config) {
4378
4630
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
4379
4631
  );
4380
4632
  }
4381
- const tokenAddresses = rawAddresses.map((a) => (0, import_viem14.getAddress)(a));
4633
+ const tokenAddresses = rawAddresses.map((a) => (0, import_viem15.getAddress)(a));
4382
4634
  const ledger = config.ledger;
4383
4635
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
4384
4636
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
@@ -4497,7 +4749,7 @@ function createIssuerService(config) {
4497
4749
  }
4498
4750
 
4499
4751
  // src/issuer-state/validator.ts
4500
- var import_viem15 = require("viem");
4752
+ var import_viem16 = require("viem");
4501
4753
  var import_core19 = require("@pafi-dev/core");
4502
4754
  var ISSUER_RECORD_TTL_MS = 3e4;
4503
4755
  var IssuerStateValidator = class _IssuerStateValidator {
@@ -4524,7 +4776,7 @@ var IssuerStateValidator = class _IssuerStateValidator {
4524
4776
  */
4525
4777
  invalidate(pointToken) {
4526
4778
  if (pointToken) {
4527
- const key = (0, import_viem15.getAddress)(pointToken);
4779
+ const key = (0, import_viem16.getAddress)(pointToken);
4528
4780
  this.pointTokenIssuerCache.delete(key);
4529
4781
  this.stateCache.delete(key);
4530
4782
  this.inflight.delete(key);
@@ -4539,7 +4791,7 @@ var IssuerStateValidator = class _IssuerStateValidator {
4539
4791
  * The issuer field is set at `initialize()` and never changes.
4540
4792
  */
4541
4793
  async getIssuerAddressForPointToken(pointToken) {
4542
- const key = (0, import_viem15.getAddress)(pointToken);
4794
+ const key = (0, import_viem16.getAddress)(pointToken);
4543
4795
  const cached = this.pointTokenIssuerCache.get(key);
4544
4796
  if (cached) return cached;
4545
4797
  const issuer = await this.provider.readContract({
@@ -4547,15 +4799,15 @@ var IssuerStateValidator = class _IssuerStateValidator {
4547
4799
  abi: import_core19.POINT_TOKEN_ABI,
4548
4800
  functionName: "issuer"
4549
4801
  });
4550
- this.pointTokenIssuerCache.set(key, (0, import_viem15.getAddress)(issuer));
4551
- return (0, import_viem15.getAddress)(issuer);
4802
+ this.pointTokenIssuerCache.set(key, (0, import_viem16.getAddress)(issuer));
4803
+ return (0, import_viem16.getAddress)(issuer);
4552
4804
  }
4553
4805
  /**
4554
4806
  * Read registry record + totalSupply, with 30s cache and in-flight
4555
4807
  * deduplication. Does NOT throw on inactive/missing — returns raw state.
4556
4808
  */
4557
4809
  async getIssuerState(pointToken) {
4558
- const tokenAddr = (0, import_viem15.getAddress)(pointToken);
4810
+ const tokenAddr = (0, import_viem16.getAddress)(pointToken);
4559
4811
  const now = Date.now();
4560
4812
  const cached = this.stateCache.get(tokenAddr);
4561
4813
  if (cached && cached.expiresAt > now) return cached.value;
@@ -4698,7 +4950,7 @@ var MemoryRedemptionHistoryStore = class {
4698
4950
  };
4699
4951
 
4700
4952
  // src/index.ts
4701
- var PAFI_ISSUER_SDK_VERSION = true ? "0.15.1" : "dev";
4953
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.18.0" : "dev";
4702
4954
  // Annotate the CommonJS export names for ESM import in node:
4703
4955
  0 && (module.exports = {
4704
4956
  AdapterMisconfiguredError,
@@ -4711,6 +4963,7 @@ var PAFI_ISSUER_SDK_VERSION = true ? "0.15.1" : "dev";
4711
4963
  DEFAULT_REDEMPTION_POLICY,
4712
4964
  DefaultPolicyEngine,
4713
4965
  FeeManager,
4966
+ GasUnitsCache,
4714
4967
  InMemoryCursorStore,
4715
4968
  IssuerApiAdapter,
4716
4969
  IssuerApiHandlers,