@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.js CHANGED
@@ -862,22 +862,172 @@ var RelayService = class {
862
862
  }
863
863
  });
864
864
  }
865
+ // =========================================================================
866
+ // Preview methods — produce bundler-ready partial UserOps WITHOUT signing.
867
+ //
868
+ // These exist so callers can compute an accurate gas estimate via
869
+ // `bundlerClient.estimateUserOperationGas(...)` BEFORE committing to a
870
+ // signed MintRequest / BurnRequest (signing is HSM-backed and expensive
871
+ // in prod). The returned `callData` matches the SHAPE of the real call;
872
+ // the EIP-712 signature bytes are placeholder. Bundler simulation
873
+ // doesn't validate the signature, so the gas units come back accurate.
874
+ //
875
+ // Cache-wise: same SC version + same scenario → same calldata shape →
876
+ // same bundler-returned gas units. The first call seeds the cache; the
877
+ // rest hit it.
878
+ // =========================================================================
879
+ /**
880
+ * Build a dummy `PartialUserOperation` for the mint scenario, suitable
881
+ * for `feeManager.estimateGasFee({ partialUserOp, ... })`. NO signing —
882
+ * uses a 65-byte zero signature in place of the real minter sig.
883
+ */
884
+ previewMintUserOp(params) {
885
+ const useWrapper = params.mintFeeWrapperAddress !== void 0;
886
+ let mintCallData;
887
+ let mintTarget;
888
+ if (useWrapper) {
889
+ mintCallData = encodeFunctionData({
890
+ abi: mintFeeWrapperAbi,
891
+ functionName: "mintWithFee",
892
+ args: [
893
+ params.pointTokenAddress,
894
+ params.userAddress,
895
+ params.amount,
896
+ params.deadline,
897
+ PLACEHOLDER_SIG_65
898
+ ]
899
+ });
900
+ mintTarget = params.mintFeeWrapperAddress;
901
+ } else {
902
+ mintCallData = encodeFunctionData({
903
+ abi: POINT_TOKEN_ABI,
904
+ functionName: "mint",
905
+ args: [
906
+ params.userAddress,
907
+ params.amount,
908
+ params.deadline,
909
+ PLACEHOLDER_SIG_65
910
+ ]
911
+ });
912
+ mintTarget = params.pointTokenAddress;
913
+ }
914
+ return buildPartialUserOperation({
915
+ sender: params.userAddress,
916
+ nonce: params.aaNonce,
917
+ operations: [{ target: mintTarget, value: 0n, data: mintCallData }],
918
+ // Gas limits ignored by bundler estimate — it computes them.
919
+ gasLimits: { callGasLimit: 1n, verificationGasLimit: 1n, preVerificationGas: 1n }
920
+ });
921
+ }
922
+ /** Burn-side mirror of `previewMintUserOp`. */
923
+ previewBurnUserOp(params) {
924
+ const burnCallData = encodeFunctionData({
925
+ abi: POINT_TOKEN_ABI,
926
+ functionName: "burn",
927
+ args: [
928
+ params.userAddress,
929
+ params.amount,
930
+ params.deadline,
931
+ PLACEHOLDER_SIG_65
932
+ ]
933
+ });
934
+ return buildPartialUserOperation({
935
+ sender: params.userAddress,
936
+ nonce: params.aaNonce,
937
+ operations: [
938
+ { target: params.pointTokenAddress, value: 0n, data: burnCallData }
939
+ ],
940
+ gasLimits: { callGasLimit: 1n, verificationGasLimit: 1n, preVerificationGas: 1n }
941
+ });
942
+ }
865
943
  };
944
+ var PLACEHOLDER_SIG_65 = `0x${"00".repeat(65)}`;
866
945
  function errorMessage(err) {
867
946
  return err instanceof Error ? err.message : String(err);
868
947
  }
869
948
 
949
+ // src/relay/gasUnitsCache.ts
950
+ import { keccak256 } from "viem";
951
+ var DEFAULT_TTL_MS = 5 * 6e4;
952
+ var DEFAULT_CODEHASH_TTL_MS = 60 * 6e4;
953
+ var DEFAULT_MAX_ENTRIES = 100;
954
+ var GasUnitsCache = class {
955
+ entries = /* @__PURE__ */ new Map();
956
+ codehashEntries = /* @__PURE__ */ new Map();
957
+ ttlMs;
958
+ codehashTtlMs;
959
+ maxEntries;
960
+ constructor(config = {}) {
961
+ this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
962
+ this.codehashTtlMs = config.codehashTtlMs ?? DEFAULT_CODEHASH_TTL_MS;
963
+ this.maxEntries = config.maxEntries ?? DEFAULT_MAX_ENTRIES;
964
+ }
965
+ async buildKey(params) {
966
+ const codehash = await this.getCodehash(
967
+ params.provider,
968
+ params.contractAddress
969
+ );
970
+ const pm = params.paymasterAddress?.toLowerCase() ?? "0x0";
971
+ return `${params.scenario}:${codehash}:${pm}`;
972
+ }
973
+ get(key, now = Date.now()) {
974
+ const entry = this.entries.get(key);
975
+ if (!entry) return null;
976
+ if (entry.expiresAt <= now) {
977
+ this.entries.delete(key);
978
+ return null;
979
+ }
980
+ return entry.gasUnits;
981
+ }
982
+ set(key, gasUnits, now = Date.now()) {
983
+ if (this.entries.size >= this.maxEntries && !this.entries.has(key)) {
984
+ const eldest = this.entries.keys().next().value;
985
+ if (eldest !== void 0) this.entries.delete(eldest);
986
+ }
987
+ this.entries.set(key, { gasUnits, expiresAt: now + this.ttlMs });
988
+ }
989
+ invalidate() {
990
+ this.entries.clear();
991
+ this.codehashEntries.clear();
992
+ }
993
+ size() {
994
+ return this.entries.size;
995
+ }
996
+ async getCodehash(provider, address) {
997
+ const lower = address.toLowerCase();
998
+ const now = Date.now();
999
+ const cached = this.codehashEntries.get(lower);
1000
+ if (cached && cached.expiresAt > now) return cached.codehash;
1001
+ const code = await provider.getCode({ address });
1002
+ const codehash = code ? keccak256(code) : "0x0";
1003
+ this.codehashEntries.set(lower, {
1004
+ codehash,
1005
+ expiresAt: now + this.codehashTtlMs
1006
+ });
1007
+ return codehash;
1008
+ }
1009
+ };
1010
+
870
1011
  // src/relay/feeManager.ts
871
1012
  var DEFAULT_GAS_UNITS = 500000n;
872
- var DEFAULT_PREMIUM_BPS = 12e3;
1013
+ var DEFAULT_PREMIUM_BPS = 1e4;
1014
+ var DEFAULT_PAYMASTER_OVERHEAD = 80000n;
1015
+ var DUMMY_SIGNATURE = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";
873
1016
  var FeeManager = class _FeeManager {
874
1017
  provider;
875
1018
  gasUnits;
876
1019
  gasPremiumBps;
877
1020
  quoteNativeToFee;
1021
+ bundlerClient;
1022
+ cache;
1023
+ paymasterOverheadGas;
1024
+ metrics;
1025
+ // Short-lived in-flight fee cache (legacy behavior). Distinct from
1026
+ // `cache` — that one stores gasUnits per scenario; this one stores the
1027
+ // FULL computed fee value, valid for 10s to absorb burst calls.
878
1028
  cachedFee = null;
879
1029
  cacheExpiresAt = 0;
880
- static CACHE_TTL_MS = 1e4;
1030
+ static FEE_CACHE_TTL_MS = 1e4;
881
1031
  constructor(config) {
882
1032
  if (!config.provider) throw new Error("FeeManager: provider required");
883
1033
  if (!config.quoteNativeToFee)
@@ -886,32 +1036,101 @@ var FeeManager = class _FeeManager {
886
1036
  this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
887
1037
  this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
888
1038
  this.quoteNativeToFee = config.quoteNativeToFee;
1039
+ this.bundlerClient = config.bundlerClient;
1040
+ this.cache = new GasUnitsCache(config.cache);
1041
+ this.paymasterOverheadGas = config.paymasterOverheadGas ?? DEFAULT_PAYMASTER_OVERHEAD;
1042
+ this.metrics = config.metrics;
889
1043
  }
890
1044
  /**
891
1045
  * Estimate the fee (in the caller's fee currency) to charge for the
892
- * next sponsored UserOp:
1046
+ * next sponsored UserOp.
893
1047
  *
1048
+ * gasUnits = bundler-estimated (cached) or hardcoded fallback
894
1049
  * nativeCost = gasUnits × gasPrice
895
1050
  * withPremium = nativeCost × premiumBps / 10_000
896
1051
  * fee = quoteNativeToFee(withPremium)
897
1052
  *
898
- * For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
899
- * from the response, the name `estimateGasFee` is kept — but the
900
- * currency depends on how the caller wired `quoteNativeToFee`.
1053
+ * When `opts.partialUserOp` is omitted, behaves exactly like v0.16.x:
1054
+ * uses the hardcoded `gasUnits` default.
901
1055
  */
902
- async estimateGasFee() {
1056
+ async estimateGasFee(opts = {}) {
903
1057
  const now = Date.now();
904
- if (this.cachedFee !== null && now < this.cacheExpiresAt) {
1058
+ const isLegacyCall = !opts.partialUserOp && !opts.scenario && !opts.contractAddress;
1059
+ if (isLegacyCall && this.cachedFee !== null && now < this.cacheExpiresAt) {
905
1060
  return this.cachedFee;
906
1061
  }
1062
+ const t0 = Date.now();
1063
+ const { gasUnits, source } = await this.resolveGasUnits(opts);
1064
+ const latencyMs = Date.now() - t0;
1065
+ this.safeEmit(
1066
+ () => this.metrics?.onEstimate?.({
1067
+ source,
1068
+ scenario: opts.scenario,
1069
+ gasUnits,
1070
+ latencyMs
1071
+ })
1072
+ );
907
1073
  const gasPrice = await this.provider.getGasPrice();
908
- const nativeCost = gasPrice * this.gasUnits;
1074
+ const nativeCost = gasPrice * gasUnits;
909
1075
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
910
1076
  const fee = await this.quoteNativeToFee(withPremium);
911
- this.cachedFee = fee;
912
- this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
1077
+ if (isLegacyCall) {
1078
+ this.cachedFee = fee;
1079
+ this.cacheExpiresAt = now + _FeeManager.FEE_CACHE_TTL_MS;
1080
+ }
913
1081
  return fee;
914
1082
  }
1083
+ /**
1084
+ * Manually purge the per-scenario gas-units cache. Useful after an SC
1085
+ * upgrade when ops wants the next estimate to refresh immediately
1086
+ * (the codehash check would catch it on the NEXT call anyway, but
1087
+ * this forces it now).
1088
+ */
1089
+ invalidateCache() {
1090
+ this.cache.invalidate();
1091
+ this.cachedFee = null;
1092
+ this.cacheExpiresAt = 0;
1093
+ }
1094
+ async resolveGasUnits(opts) {
1095
+ if (!this.bundlerClient || !opts.partialUserOp || !opts.scenario || !opts.contractAddress) {
1096
+ return { gasUnits: this.gasUnits, source: "fallback" };
1097
+ }
1098
+ try {
1099
+ const cacheKey = await this.cache.buildKey({
1100
+ scenario: opts.scenario,
1101
+ contractAddress: opts.contractAddress,
1102
+ paymasterAddress: opts.paymasterAddress,
1103
+ provider: this.provider
1104
+ });
1105
+ const cached = this.cache.get(cacheKey);
1106
+ if (cached !== null) {
1107
+ return { gasUnits: cached, source: "cache" };
1108
+ }
1109
+ const estimate = await this.bundlerClient.estimateUserOperationGas({
1110
+ sender: opts.partialUserOp.sender,
1111
+ nonce: opts.partialUserOp.nonce,
1112
+ callData: opts.partialUserOp.callData,
1113
+ signature: opts.partialUserOp.signature ?? DUMMY_SIGNATURE
1114
+ // Intentionally NO paymaster fields — avoids chicken-and-egg
1115
+ // (paymasterData depends on gasLimits). Overhead added below.
1116
+ });
1117
+ const gasUnits = estimate.callGasLimit + estimate.verificationGasLimit + estimate.preVerificationGas + (estimate.paymasterVerificationGasLimit ?? 0n) + (estimate.paymasterPostOpGasLimit ?? 0n) + this.paymasterOverheadGas;
1118
+ this.cache.set(cacheKey, gasUnits);
1119
+ return { gasUnits, source: "bundler" };
1120
+ } catch (err) {
1121
+ const reason = err instanceof Error ? err.message : String(err);
1122
+ this.safeEmit(
1123
+ () => this.metrics?.onBundlerError?.({ scenario: opts.scenario, reason })
1124
+ );
1125
+ return { gasUnits: this.gasUnits, source: "fallback" };
1126
+ }
1127
+ }
1128
+ safeEmit(fn) {
1129
+ try {
1130
+ fn();
1131
+ } catch {
1132
+ }
1133
+ }
915
1134
  };
916
1135
 
917
1136
  // src/indexer/types.ts
@@ -1850,11 +2069,29 @@ var PTRedeemHandler = class {
1850
2069
  }
1851
2070
  }
1852
2071
  async _handleAfterNonceLock(request, burnNonce) {
2072
+ const previewDeadline = BigInt(
2073
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
2074
+ );
1853
2075
  let fee;
1854
2076
  if (request.feeAmount !== void 0) {
1855
2077
  fee = request.feeAmount > 0n ? request.feeAmount : 0n;
1856
2078
  } else if (this.feeService) {
1857
- fee = await this.feeService.estimateGasFee();
2079
+ const previewUserOp = this.relayService.previewBurnUserOp({
2080
+ userAddress: request.userAddress,
2081
+ aaNonce: burnNonce,
2082
+ pointTokenAddress: this.pointTokenAddress,
2083
+ amount: request.amount,
2084
+ deadline: previewDeadline
2085
+ });
2086
+ fee = await this.feeService.estimateGasFee({
2087
+ scenario: "burn",
2088
+ contractAddress: this.pointTokenAddress,
2089
+ partialUserOp: {
2090
+ sender: previewUserOp.sender,
2091
+ nonce: previewUserOp.nonce,
2092
+ callData: previewUserOp.callData
2093
+ }
2094
+ });
1858
2095
  } else {
1859
2096
  fee = 0n;
1860
2097
  }
@@ -1876,9 +2113,7 @@ var PTRedeemHandler = class {
1876
2113
  `insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
1877
2114
  );
1878
2115
  }
1879
- const deadline = BigInt(
1880
- Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1881
- );
2116
+ const deadline = previewDeadline;
1882
2117
  const domain = {
1883
2118
  name: this.domain.name,
1884
2119
  chainId: this.chainId,
@@ -2550,7 +2785,23 @@ var PTClaimHandler = class {
2550
2785
  const signatureDeadline = BigInt(
2551
2786
  Math.floor(this.cfg.now() / 1e3) + this.cfg.signatureDeadlineSeconds
2552
2787
  );
2553
- const feeAmount = this.cfg.feeService ? await this.cfg.feeService.estimateGasFee() : 0n;
2788
+ const previewUserOp = this.cfg.relayService.previewMintUserOp({
2789
+ userAddress: request.userAddress,
2790
+ aaNonce: request.aaNonce,
2791
+ pointTokenAddress: request.pointTokenAddress,
2792
+ amount: request.amount,
2793
+ deadline: signatureDeadline,
2794
+ mintFeeWrapperAddress: resolvedWrapper
2795
+ });
2796
+ const feeAmount = this.cfg.feeService ? await this.cfg.feeService.estimateGasFee({
2797
+ scenario: resolvedWrapper ? "mint-wrapped" : "mint",
2798
+ contractAddress: request.pointTokenAddress,
2799
+ partialUserOp: {
2800
+ sender: previewUserOp.sender,
2801
+ nonce: previewUserOp.nonce,
2802
+ callData: previewUserOp.callData
2803
+ }
2804
+ }) : 0n;
2554
2805
  const domain = {
2555
2806
  name: this.cfg.pointTokenDomainName,
2556
2807
  chainId: request.chainId,
@@ -4517,7 +4768,7 @@ var MemoryRedemptionHistoryStore = class {
4517
4768
  };
4518
4769
 
4519
4770
  // src/index.ts
4520
- var PAFI_ISSUER_SDK_VERSION = true ? "0.15.1" : "dev";
4771
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.18.0" : "dev";
4521
4772
  export {
4522
4773
  AdapterMisconfiguredError,
4523
4774
  AuthError,
@@ -4529,6 +4780,7 @@ export {
4529
4780
  DEFAULT_REDEMPTION_POLICY,
4530
4781
  DefaultPolicyEngine,
4531
4782
  FeeManager,
4783
+ GasUnitsCache,
4532
4784
  InMemoryCursorStore,
4533
4785
  IssuerApiAdapter,
4534
4786
  IssuerApiHandlers,