@pafi-dev/issuer 0.19.0 → 0.20.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,58 +862,265 @@ 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 fetch an accurate gas estimate from PAFI's
869
+ // `/v1/estimate-gas-fee` BEFORE committing to a signed MintRequest /
870
+ // BurnRequest (HSM/KMS signing is expensive in production). The
871
+ // returned `callData` matches the SHAPE of the real call; the EIP-712
872
+ // signature bytes are a placeholder. Bundler simulation doesn't
873
+ // validate the signature, so the gas estimate comes back accurate.
874
+ //
875
+ // Cache-wise: same SC version + same scenario produce identical
876
+ // calldata shape → identical bundler-returned gas units. The first
877
+ // call seeds the cache; subsequent ones 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 placeholder.
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: {
920
+ callGasLimit: 1n,
921
+ verificationGasLimit: 1n,
922
+ preVerificationGas: 1n
923
+ }
924
+ });
925
+ }
926
+ /** Burn-side mirror of `previewMintUserOp`. */
927
+ previewBurnUserOp(params) {
928
+ const burnCallData = encodeFunctionData({
929
+ abi: POINT_TOKEN_ABI,
930
+ functionName: "burn",
931
+ args: [
932
+ params.userAddress,
933
+ params.amount,
934
+ params.deadline,
935
+ PLACEHOLDER_SIG_65
936
+ ]
937
+ });
938
+ return buildPartialUserOperation({
939
+ sender: params.userAddress,
940
+ nonce: params.aaNonce,
941
+ operations: [
942
+ { target: params.pointTokenAddress, value: 0n, data: burnCallData }
943
+ ],
944
+ gasLimits: {
945
+ callGasLimit: 1n,
946
+ verificationGasLimit: 1n,
947
+ preVerificationGas: 1n
948
+ }
949
+ });
950
+ }
865
951
  };
952
+ var PLACEHOLDER_SIG_65 = `0x${"00".repeat(65)}`;
866
953
  function errorMessage(err) {
867
954
  return err instanceof Error ? err.message : String(err);
868
955
  }
869
956
 
870
957
  // src/relay/feeManager.ts
871
958
  var DEFAULT_GAS_UNITS = 500000n;
872
- var DEFAULT_PREMIUM_BPS = 12e3;
959
+ var DEFAULT_PREMIUM_BPS = 1e4;
873
960
  var FeeManager = class _FeeManager {
874
961
  provider;
875
- gasUnits;
962
+ fallbackGasUnits;
876
963
  gasPremiumBps;
877
964
  quoteNativeToFee;
965
+ bundlerClient;
966
+ metrics;
967
+ // Short-lived fee-value cache. Distinct from the estimator's cache:
968
+ // this absorbs burst calls (e.g. 5 user requests in 5s all hit the
969
+ // /gas-fee endpoint) by remembering the COMPUTED PT amount, not the
970
+ // gas units. Only used by the no-opts legacy path; estimator path
971
+ // gets its caching from the PAFI side instead.
878
972
  cachedFee = null;
879
973
  cacheExpiresAt = 0;
880
- static CACHE_TTL_MS = 1e4;
974
+ static FEE_CACHE_TTL_MS = 1e4;
881
975
  constructor(config) {
882
976
  if (!config.provider) throw new Error("FeeManager: provider required");
883
977
  if (!config.quoteNativeToFee)
884
978
  throw new Error("FeeManager: quoteNativeToFee required");
885
979
  this.provider = config.provider;
886
- this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
980
+ this.fallbackGasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
887
981
  this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
888
982
  this.quoteNativeToFee = config.quoteNativeToFee;
983
+ this.bundlerClient = config.bundlerClient;
984
+ this.metrics = config.metrics;
889
985
  }
890
986
  /**
891
- * Estimate the fee (in the caller's fee currency) to charge for the
892
- * next sponsored UserOp:
987
+ * Estimate the operator fee for the next sponsored UserOp.
893
988
  *
894
- * nativeCost = gasUnits × gasPrice
895
- * withPremium = nativeCost × premiumBps / 10_000
896
- * fee = quoteNativeToFee(withPremium)
989
+ * Without `opts` → legacy path: `gasUnits × gasPrice × premium →
990
+ * quoteNativeToFee`. Cached for 10 s to absorb bursts.
897
991
  *
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`.
992
+ * With `opts` AND `bundlerClient` → estimator path. Each call may
993
+ * hit a different bundler-cached result; the SDK does NOT add its
994
+ * own value cache because the estimator's cache TTL is the source
995
+ * of truth for "how long is this estimate good for".
901
996
  */
902
- async estimateGasFee() {
997
+ async estimateGasFee(opts = {}) {
998
+ const isLegacyCall = !opts.partialUserOp && !opts.scenario && !opts.contractAddress;
903
999
  const now = Date.now();
904
- if (this.cachedFee !== null && now < this.cacheExpiresAt) {
1000
+ if (isLegacyCall && this.cachedFee !== null && now < this.cacheExpiresAt) {
905
1001
  return this.cachedFee;
906
1002
  }
1003
+ const t0 = Date.now();
1004
+ const { gasUnits, source } = await this.resolveGasUnits(opts);
1005
+ const latencyMs = Date.now() - t0;
1006
+ this.safeEmit(
1007
+ () => this.metrics?.onEstimate?.({
1008
+ source,
1009
+ scenario: opts.scenario,
1010
+ gasUnits,
1011
+ latencyMs
1012
+ })
1013
+ );
907
1014
  const gasPrice = await this.provider.getGasPrice();
908
- const nativeCost = gasPrice * this.gasUnits;
1015
+ const nativeCost = gasPrice * gasUnits;
909
1016
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
910
1017
  const fee = await this.quoteNativeToFee(withPremium);
911
- this.cachedFee = fee;
912
- this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
1018
+ if (isLegacyCall) {
1019
+ this.cachedFee = fee;
1020
+ this.cacheExpiresAt = now + _FeeManager.FEE_CACHE_TTL_MS;
1021
+ }
913
1022
  return fee;
914
1023
  }
1024
+ /** Manually purge the legacy 10s fee cache. */
1025
+ invalidateCache() {
1026
+ this.cachedFee = null;
1027
+ this.cacheExpiresAt = 0;
1028
+ }
1029
+ async resolveGasUnits(opts) {
1030
+ if (!this.bundlerClient || !opts.partialUserOp || !opts.scenario || !opts.contractAddress) {
1031
+ return { gasUnits: this.fallbackGasUnits, source: "fallback" };
1032
+ }
1033
+ try {
1034
+ const result = await this.bundlerClient.getGasUnits({
1035
+ scenario: opts.scenario,
1036
+ contractAddress: opts.contractAddress,
1037
+ paymasterAddress: opts.paymasterAddress,
1038
+ partialUserOp: opts.partialUserOp
1039
+ });
1040
+ return { gasUnits: result.gasUnits, source: "estimator" };
1041
+ } catch (err) {
1042
+ const reason = err instanceof Error ? err.message : String(err);
1043
+ this.safeEmit(
1044
+ () => this.metrics?.onEstimatorError?.({
1045
+ scenario: opts.scenario,
1046
+ reason
1047
+ })
1048
+ );
1049
+ return { gasUnits: this.fallbackGasUnits, source: "fallback" };
1050
+ }
1051
+ }
1052
+ safeEmit(fn) {
1053
+ try {
1054
+ fn();
1055
+ } catch {
1056
+ }
1057
+ }
915
1058
  };
916
1059
 
1060
+ // src/relay/bundlerEstimator.ts
1061
+ var PafiEstimatorHttpError = class extends Error {
1062
+ status;
1063
+ body;
1064
+ constructor(status, body, message) {
1065
+ super(message ?? `PAFI estimator HTTP ${status}`);
1066
+ this.status = status;
1067
+ this.body = body;
1068
+ }
1069
+ };
1070
+ function createPafiEstimatorClient(config) {
1071
+ const { baseUrl, apiKey, issuerId } = config;
1072
+ if (!baseUrl) throw new Error("createPafiEstimatorClient: baseUrl required");
1073
+ if (!apiKey) throw new Error("createPafiEstimatorClient: apiKey required");
1074
+ if (!issuerId) throw new Error("createPafiEstimatorClient: issuerId required");
1075
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1076
+ if (!fetchImpl) {
1077
+ throw new Error(
1078
+ "createPafiEstimatorClient: no fetch implementation available \u2014 pass `fetchImpl`"
1079
+ );
1080
+ }
1081
+ const url = `${baseUrl.replace(/\/$/, "")}/v1/estimate-gas-fee`;
1082
+ return {
1083
+ async getGasUnits(input) {
1084
+ const body = JSON.stringify({
1085
+ partialUserOp: {
1086
+ sender: input.partialUserOp.sender,
1087
+ // Hex-encode bigint for JSON safety. Sponsor-relayer parses
1088
+ // back to bigint at the DTO layer.
1089
+ nonce: `0x${input.partialUserOp.nonce.toString(16)}`,
1090
+ callData: input.partialUserOp.callData,
1091
+ signature: input.partialUserOp.signature
1092
+ },
1093
+ scenario: input.scenario,
1094
+ contractAddress: input.contractAddress,
1095
+ paymasterAddress: input.paymasterAddress
1096
+ });
1097
+ const res = await fetchImpl(url, {
1098
+ method: "POST",
1099
+ headers: {
1100
+ "content-type": "application/json",
1101
+ authorization: `Bearer ${apiKey}`,
1102
+ "x-issuer-id": issuerId
1103
+ },
1104
+ body
1105
+ });
1106
+ if (!res.ok) {
1107
+ let errBody = null;
1108
+ try {
1109
+ errBody = await res.json();
1110
+ } catch {
1111
+ }
1112
+ throw new PafiEstimatorHttpError(res.status, errBody);
1113
+ }
1114
+ const json = await res.json();
1115
+ return {
1116
+ gasUnits: BigInt(json.gasUnits),
1117
+ source: json.source,
1118
+ expiresAt: json.expiresAt
1119
+ };
1120
+ }
1121
+ };
1122
+ }
1123
+
917
1124
  // src/indexer/types.ts
918
1125
  var InMemoryCursorStore = class {
919
1126
  cursor;
@@ -1850,11 +2057,29 @@ var PTRedeemHandler = class {
1850
2057
  }
1851
2058
  }
1852
2059
  async _handleAfterNonceLock(request, burnNonce) {
2060
+ const previewDeadline = BigInt(
2061
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
2062
+ );
1853
2063
  let fee;
1854
2064
  if (request.feeAmount !== void 0) {
1855
2065
  fee = request.feeAmount > 0n ? request.feeAmount : 0n;
1856
2066
  } else if (this.feeService) {
1857
- fee = await this.feeService.estimateGasFee();
2067
+ const previewUserOp = this.relayService.previewBurnUserOp({
2068
+ userAddress: request.userAddress,
2069
+ aaNonce: burnNonce,
2070
+ pointTokenAddress: this.pointTokenAddress,
2071
+ amount: request.amount,
2072
+ deadline: previewDeadline
2073
+ });
2074
+ fee = await this.feeService.estimateGasFee({
2075
+ scenario: "burn",
2076
+ contractAddress: this.pointTokenAddress,
2077
+ partialUserOp: {
2078
+ sender: previewUserOp.sender,
2079
+ nonce: previewUserOp.nonce,
2080
+ callData: previewUserOp.callData
2081
+ }
2082
+ });
1858
2083
  } else {
1859
2084
  fee = 0n;
1860
2085
  }
@@ -1876,9 +2101,7 @@ var PTRedeemHandler = class {
1876
2101
  `insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
1877
2102
  );
1878
2103
  }
1879
- const deadline = BigInt(
1880
- Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1881
- );
2104
+ const deadline = previewDeadline;
1882
2105
  const domain = {
1883
2106
  name: this.domain.name,
1884
2107
  chainId: this.chainId,
@@ -2550,7 +2773,23 @@ var PTClaimHandler = class {
2550
2773
  const signatureDeadline = BigInt(
2551
2774
  Math.floor(this.cfg.now() / 1e3) + this.cfg.signatureDeadlineSeconds
2552
2775
  );
2553
- const feeAmount = this.cfg.feeService ? await this.cfg.feeService.estimateGasFee() : 0n;
2776
+ const previewUserOp = this.cfg.relayService.previewMintUserOp({
2777
+ userAddress: request.userAddress,
2778
+ aaNonce: request.aaNonce,
2779
+ pointTokenAddress: request.pointTokenAddress,
2780
+ amount: request.amount,
2781
+ deadline: signatureDeadline,
2782
+ mintFeeWrapperAddress: resolvedWrapper
2783
+ });
2784
+ const feeAmount = this.cfg.feeService ? await this.cfg.feeService.estimateGasFee({
2785
+ scenario: resolvedWrapper ? "mint-wrapped" : "mint",
2786
+ contractAddress: request.pointTokenAddress,
2787
+ partialUserOp: {
2788
+ sender: previewUserOp.sender,
2789
+ nonce: previewUserOp.nonce,
2790
+ callData: previewUserOp.callData
2791
+ }
2792
+ }) : 0n;
2554
2793
  const domain = {
2555
2794
  name: this.cfg.pointTokenDomainName,
2556
2795
  chainId: request.chainId,
@@ -4517,7 +4756,7 @@ var MemoryRedemptionHistoryStore = class {
4517
4756
  };
4518
4757
 
4519
4758
  // src/index.ts
4520
- var PAFI_ISSUER_SDK_VERSION = true ? "0.19.0" : "dev";
4759
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.20.0" : "dev";
4521
4760
  export {
4522
4761
  AdapterMisconfiguredError,
4523
4762
  AuthError,
@@ -4549,6 +4788,7 @@ export {
4549
4788
  PTRedeemHandler,
4550
4789
  PafiBackendClient,
4551
4790
  PafiBackendError,
4791
+ PafiEstimatorHttpError,
4552
4792
  PafiSdkError,
4553
4793
  PendingUserOpForbiddenError,
4554
4794
  PendingUserOpNotFoundError,
@@ -4569,6 +4809,7 @@ export {
4569
4809
  buildSdkErrorBody,
4570
4810
  createIssuerService,
4571
4811
  createNativePtQuoter,
4812
+ createPafiEstimatorClient,
4572
4813
  createSdkErrorMapper,
4573
4814
  createSubgraphNativeUsdtQuoter,
4574
4815
  createSubgraphPoolsProvider,