@pafi-dev/issuer 0.16.0 → 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.d.cts CHANGED
@@ -621,6 +621,14 @@ declare class RelayService {
621
621
  * issuer-signed `BurnRequest` path.
622
622
  */
623
623
  prepareBurn(params: PrepareBurnParams): Promise<PartialUserOperation>;
624
+ /**
625
+ * Build a dummy `PartialUserOperation` for the mint scenario, suitable
626
+ * for `feeManager.estimateGasFee({ partialUserOp, ... })`. NO signing —
627
+ * uses a 65-byte zero signature in place of the real minter sig.
628
+ */
629
+ previewMintUserOp(params: PreviewMintParams): PartialUserOperation;
630
+ /** Burn-side mirror of `previewMintUserOp`. */
631
+ previewBurnUserOp(params: PreviewBurnParams): PartialUserOperation;
624
632
  }
625
633
  /**
626
634
  * Sig-gated `PointToken.mint(to, amount, deadline, minterSig)`.
@@ -707,19 +715,131 @@ interface PrepareBurnParams {
707
715
  verificationGasLimit?: bigint;
708
716
  preVerificationGas?: bigint;
709
717
  }
718
+ /**
719
+ * Inputs for `previewMintUserOp` — strict subset of `PrepareMintParams`
720
+ * that excludes the signing wallet, EIP-712 domain, and on-chain nonce.
721
+ * These don't affect calldata shape so the bundler estimate doesn't
722
+ * need them.
723
+ */
724
+ interface PreviewMintParams {
725
+ userAddress: Address;
726
+ aaNonce: bigint;
727
+ pointTokenAddress: Address;
728
+ amount: bigint;
729
+ deadline: bigint;
730
+ mintFeeWrapperAddress?: Address;
731
+ }
732
+ interface PreviewBurnParams {
733
+ userAddress: Address;
734
+ aaNonce: bigint;
735
+ pointTokenAddress: Address;
736
+ amount: bigint;
737
+ deadline: bigint;
738
+ }
739
+
740
+ /**
741
+ * In-memory cache for bundler-estimated gas units, keyed by
742
+ * `(scenario, contractCodehash, paymaster)`.
743
+ *
744
+ * Why these key components:
745
+ * - `scenario` — distinguishes mint vs burn vs swap; their UserOps
746
+ * differ in calldata shape so estimates differ.
747
+ * - `contractCodehash` — fetched from the SC address via `eth_getCode`,
748
+ * cached separately for 1h. When the SC is upgraded
749
+ * the codehash changes → the gas key auto-invalidates
750
+ * (no manual purge needed).
751
+ * - `paymaster` — paymaster contract version affects postOp gas;
752
+ * rotating paymaster naturally invalidates.
753
+ *
754
+ * Bound: max `maxEntries` to defend against unbounded growth on
755
+ * long-running issuer backends; eldest evicted first (LRU-by-insertion).
756
+ */
757
+ interface GasUnitsCacheConfig {
758
+ /** Gas-units cache TTL in ms. Default 5 minutes. */
759
+ ttlMs?: number;
760
+ /** Codehash cache TTL in ms. Default 1 hour. */
761
+ codehashTtlMs?: number;
762
+ /** Max entries before LRU eviction. Default 100. */
763
+ maxEntries?: number;
764
+ }
765
+ declare class GasUnitsCache {
766
+ private readonly entries;
767
+ private readonly codehashEntries;
768
+ private readonly ttlMs;
769
+ private readonly codehashTtlMs;
770
+ private readonly maxEntries;
771
+ constructor(config?: GasUnitsCacheConfig);
772
+ buildKey(params: {
773
+ scenario: string;
774
+ contractAddress: Address;
775
+ paymasterAddress?: Address;
776
+ provider: PublicClient;
777
+ }): Promise<string>;
778
+ get(key: string, now?: number): bigint | null;
779
+ set(key: string, gasUnits: bigint, now?: number): void;
780
+ invalidate(): void;
781
+ size(): number;
782
+ private getCodehash;
783
+ }
710
784
 
785
+ /**
786
+ * Minimal duck-typed shape of an ERC-4337 bundler client supporting
787
+ * `eth_estimateUserOperationGas`. Compatible with `permissionless`'s
788
+ * `createBundlerClient` and any spec-compliant bundler (Pimlico, Alchemy,
789
+ * StackUp, Biconomy). The SDK does NOT add `permissionless` as a hard
790
+ * dependency — callers pass whatever client they already use.
791
+ */
792
+ interface BundlerEstimatorClient {
793
+ estimateUserOperationGas(args: {
794
+ sender: Address;
795
+ nonce: bigint;
796
+ callData: Hex;
797
+ signature?: Hex;
798
+ paymaster?: Address;
799
+ paymasterData?: Hex;
800
+ }): Promise<{
801
+ callGasLimit: bigint;
802
+ verificationGasLimit: bigint;
803
+ preVerificationGas: bigint;
804
+ paymasterVerificationGasLimit?: bigint;
805
+ paymasterPostOpGasLimit?: bigint;
806
+ }>;
807
+ }
808
+ type GasFeeSource = "cache" | "bundler" | "fallback";
809
+ /**
810
+ * Hooks called by `FeeManager` so callers can wire their own
811
+ * observability (Pino logger, Prometheus counters, ...). All hooks are
812
+ * synchronous and best-effort — they MUST NOT throw.
813
+ */
814
+ interface FeeManagerMetrics {
815
+ onEstimate?: (info: {
816
+ source: GasFeeSource;
817
+ scenario?: string;
818
+ gasUnits: bigint;
819
+ latencyMs: number;
820
+ }) => void;
821
+ onBundlerError?: (info: {
822
+ scenario?: string;
823
+ reason: string;
824
+ }) => void;
825
+ }
711
826
  interface FeeManagerConfig {
712
- /** Provider used for gas price reads. */
827
+ /** Provider used for gas price reads + codehash lookup. */
713
828
  provider: PublicClient;
714
829
  /**
715
- * Typical gas used by a single sponsored UserOp. Default: 500_000.
716
- * The manager multiplies this by current gas price to get native
717
- * cost, then converts via the injected `quoteNativeToFee`.
830
+ * Hardcoded fallback gas units, used when (a) `bundlerClient` is not
831
+ * configured OR (b) the bundler call fails. Default: 500_000.
718
832
  */
719
833
  gasUnits?: bigint;
720
834
  /**
721
- * Safety margin applied before charging the user, as basis points.
722
- * 12_000 = 120%. Default: 12_000.
835
+ * Safety margin applied on top of the bundler/fallback estimate, in
836
+ * basis points. 10_000 = 100% (no extra). Default: 10_000.
837
+ *
838
+ * Pimlico's `eth_estimateUserOperationGas` already adds ~10-15% safety
839
+ * margin internally, so a second SDK-side premium just pads padding
840
+ * and over-charges users. We dropped the historical 12_000 (120%)
841
+ * default after measuring `actualGasUsed / estimate ≈ 0.92` across
842
+ * 100 dev mints.
723
843
  */
724
844
  gasPremiumBps?: number;
725
845
  /**
@@ -732,6 +852,60 @@ interface FeeManagerConfig {
732
852
  * or an oracle feed.
733
853
  */
734
854
  quoteNativeToFee: (amountNative: bigint) => Promise<bigint>;
855
+ /**
856
+ * Optional ERC-4337 bundler client. When set, `estimateGasFee({
857
+ * partialUserOp, ... })` will call `eth_estimateUserOperationGas`
858
+ * (cached) for per-UserOp accuracy. Omit to keep the legacy hardcoded
859
+ * path.
860
+ */
861
+ bundlerClient?: BundlerEstimatorClient;
862
+ /** Gas-units cache config — see `GasUnitsCache`. */
863
+ cache?: GasUnitsCacheConfig;
864
+ /**
865
+ * Fixed overhead added to bundler estimates to account for paymaster
866
+ * verification + postOp gas, since we estimate WITHOUT paymaster
867
+ * context (chicken-and-egg: paymaster data depends on gas limits).
868
+ * Default: 80_000 — covers Pimlico singleton paymaster v0.7 postOp.
869
+ */
870
+ paymasterOverheadGas?: bigint;
871
+ /** Optional observability hooks. */
872
+ metrics?: FeeManagerMetrics;
873
+ }
874
+ /**
875
+ * Parameters for a single bundler-driven `estimateGasFee` call.
876
+ *
877
+ * All fields optional for backwards compat: when `partialUserOp` is
878
+ * omitted, FeeManager falls back to the legacy `gasUnits × gasPrice`
879
+ * path.
880
+ */
881
+ interface EstimateGasFeeOptions {
882
+ /**
883
+ * Partial UserOperation to simulate. The bundler runs the calldata in
884
+ * a virtual EVM and returns gas limits. Pass `signature` as a 65-byte
885
+ * dummy when the user hasn't signed yet.
886
+ *
887
+ * Estimate WITHOUT paymaster fields (paymaster/paymasterData omitted)
888
+ * to avoid the chicken-and-egg with paymasterData generation —
889
+ * `paymasterOverheadGas` is added afterwards.
890
+ */
891
+ partialUserOp?: {
892
+ sender: Address;
893
+ nonce: bigint;
894
+ callData: Hex;
895
+ signature?: Hex;
896
+ };
897
+ /**
898
+ * Contract address being called — used for cache key (codehash
899
+ * lookup). Typically the PointToken address for mint/burn.
900
+ */
901
+ contractAddress?: Address;
902
+ /** Scenario label used in the cache key (e.g. 'mint', 'burn'). */
903
+ scenario?: string;
904
+ /**
905
+ * Paymaster address used in the cache key. When omitted, '0x0' is
906
+ * used — fine if a single paymaster is used across the deployment.
907
+ */
908
+ paymasterAddress?: Address;
735
909
  }
736
910
  /**
737
911
  * Computes how much fee to collect from the user to cover the gas cost
@@ -740,34 +914,55 @@ interface FeeManagerConfig {
740
914
  * The fee is expressed in the **fee currency** chosen by the caller
741
915
  * (PT for mint/burn, USDT for swap/perp_deposit) — not hardcoded.
742
916
  *
917
+ * Two paths:
918
+ * 1. Legacy (no `bundlerClient`): gasUnits × gasPrice × premium
919
+ * 2. Bundler (recommended): bundler.estimateUserOperationGas →
920
+ * sum gas limits → × gasPrice × premium
921
+ *
922
+ * The bundler path caches by `(scenario, contractCodehash, paymaster)`
923
+ * for ~99% hit rate after warm-up; cache invalidates automatically on
924
+ * SC upgrade via codehash change.
925
+ *
743
926
  * **No operator rebalancing.** The operator does not hold ETH directly;
744
- * gas is paid by PAFI sponsor-relayer via the paymaster-proxy (see
745
- * [SPONSORED_PATH_FLOW.md]). The fee collected here is an
746
- * application-level ERC-20 transfer inside the same UserOp batch, not a
747
- * reimbursement to a wallet that needs topping up.
927
+ * gas is paid by PAFI sponsor-relayer via the paymaster-proxy. The fee
928
+ * collected here is an application-level ERC-20 transfer inside the same
929
+ * UserOp batch, not a reimbursement to a wallet that needs topping up.
748
930
  */
749
931
  declare class FeeManager {
750
932
  private readonly provider;
751
933
  private readonly gasUnits;
752
934
  private readonly gasPremiumBps;
753
935
  private readonly quoteNativeToFee;
936
+ private readonly bundlerClient;
937
+ private readonly cache;
938
+ private readonly paymasterOverheadGas;
939
+ private readonly metrics;
754
940
  private cachedFee;
755
941
  private cacheExpiresAt;
756
- private static readonly CACHE_TTL_MS;
942
+ private static readonly FEE_CACHE_TTL_MS;
757
943
  constructor(config: FeeManagerConfig);
758
944
  /**
759
945
  * Estimate the fee (in the caller's fee currency) to charge for the
760
- * next sponsored UserOp:
946
+ * next sponsored UserOp.
761
947
  *
948
+ * gasUnits = bundler-estimated (cached) or hardcoded fallback
762
949
  * nativeCost = gasUnits × gasPrice
763
950
  * withPremium = nativeCost × premiumBps / 10_000
764
951
  * fee = quoteNativeToFee(withPremium)
765
952
  *
766
- * For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
767
- * from the response, the name `estimateGasFee` is kept — but the
768
- * currency depends on how the caller wired `quoteNativeToFee`.
953
+ * When `opts.partialUserOp` is omitted, behaves exactly like v0.16.x:
954
+ * uses the hardcoded `gasUnits` default.
955
+ */
956
+ estimateGasFee(opts?: EstimateGasFeeOptions): Promise<bigint>;
957
+ /**
958
+ * Manually purge the per-scenario gas-units cache. Useful after an SC
959
+ * upgrade when ops wants the next estimate to refresh immediately
960
+ * (the codehash check would catch it on the NEXT call anyway, but
961
+ * this forces it now).
769
962
  */
770
- estimateGasFee(): Promise<bigint>;
963
+ invalidateCache(): void;
964
+ private resolveGasUnits;
965
+ private safeEmit;
771
966
  }
772
967
 
773
968
  /** Decoded Transfer(from=0x0 → to) event used to finalize a mint. */
@@ -3487,4 +3682,4 @@ declare class MemoryRedemptionHistoryStore implements IRedemptionHistoryStore {
3487
3682
 
3488
3683
  declare const PAFI_ISSUER_SDK_VERSION: string;
3489
3684
 
3490
- export { AdapterMisconfiguredError, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedemptionEvaluateRequest, type ApiRedemptionEvaluateResponse, type ApiRedemptionPreviewRequest, type ApiRedemptionPreviewResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type ClaimDto, type ConfigDto, ConfigurationError, DEFAULT_REDEMPTION_POLICY, type DecodedCallDto, DefaultPolicyEngine, type DefaultPolicyEngineOptions, type DelegatePrepareDto, type DelegateStatusDto, type EvaluateInput, FeeManager, type FeeManagerConfig, type FetchFailureReason, type FetchResult, type GasFeeDto, type HandleDelegateSubmitParams, type HandleDelegateSubmitResult, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type IRateLimiter, type IRedemptionHistoryStore, type ISessionStore, InMemoryCursorStore, IssuerApiAdapter, type IssuerApiAdapterConfig, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemoryPendingUserOpStore, MemoryRateLimiter, MemoryRedemptionHistoryStore, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type MobilePrepareDto, type MobileSubmitDto, type NativePtQuoterConfig, NonceManager, NoopRateLimiter, PAFI_ISSUER_SDK_VERSION, PTClaimError, PTClaimHandler, type PTClaimHandlerConfig, type PTClaimRequest, type PTClaimResponse, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PaymasterGasEstimates, type PendingCredit, type PendingUserOpEntry, PendingUserOpForbiddenError, PendingUserOpNotFoundError, type PerpDepositDto, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, PolicyProvider, type PolicyProviderConfig, type PoolsDto, type PoolsProvider, type PreValidateMintResult, type PrepareBurnParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, REDEMPTION_HISTORY_WINDOW_SEC, type RateLimitAction, type RateLimiterConfig, type RedeemDto, type RedeemPrepareDto, RedemptionService, type RedemptionServiceConfig, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type ResolvedPolicy, type RetryConfig, type SdkErrorBody, type SdkErrorMapperFactories, type SdkErrorStatus, type SerializedUserOpTypedData, type Session, SettlementClient, type SettlementClientConfig, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type UserDto, type UserHistory, applyPaymasterGasEstimates, authenticateRequest, buildSdkErrorBody, createIssuerService, createNativePtQuoter, createSdkErrorMapper, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, defaultPolicyFor, evaluateRedemption, handleClaimStatus, handleDelegateSubmit, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };
3685
+ export { AdapterMisconfiguredError, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedemptionEvaluateRequest, type ApiRedemptionEvaluateResponse, type ApiRedemptionPreviewRequest, type ApiRedemptionPreviewResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, type BundlerEstimatorClient, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type ClaimDto, type ConfigDto, ConfigurationError, DEFAULT_REDEMPTION_POLICY, type DecodedCallDto, DefaultPolicyEngine, type DefaultPolicyEngineOptions, type DelegatePrepareDto, type DelegateStatusDto, type EstimateGasFeeOptions, type EvaluateInput, FeeManager, type FeeManagerConfig, type FeeManagerMetrics, type FetchFailureReason, type FetchResult, type GasFeeDto, type GasFeeSource, GasUnitsCache, type GasUnitsCacheConfig, type HandleDelegateSubmitParams, type HandleDelegateSubmitResult, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type IRateLimiter, type IRedemptionHistoryStore, type ISessionStore, InMemoryCursorStore, IssuerApiAdapter, type IssuerApiAdapterConfig, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemoryPendingUserOpStore, MemoryRateLimiter, MemoryRedemptionHistoryStore, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type MobilePrepareDto, type MobileSubmitDto, type NativePtQuoterConfig, NonceManager, NoopRateLimiter, PAFI_ISSUER_SDK_VERSION, PTClaimError, PTClaimHandler, type PTClaimHandlerConfig, type PTClaimRequest, type PTClaimResponse, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PaymasterGasEstimates, type PendingCredit, type PendingUserOpEntry, PendingUserOpForbiddenError, PendingUserOpNotFoundError, type PerpDepositDto, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, PolicyProvider, type PolicyProviderConfig, type PoolsDto, type PoolsProvider, type PreValidateMintResult, type PrepareBurnParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, type PreviewBurnParams, type PreviewMintParams, REDEMPTION_HISTORY_WINDOW_SEC, type RateLimitAction, type RateLimiterConfig, type RedeemDto, type RedeemPrepareDto, RedemptionService, type RedemptionServiceConfig, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type ResolvedPolicy, type RetryConfig, type SdkErrorBody, type SdkErrorMapperFactories, type SdkErrorStatus, type SerializedUserOpTypedData, type Session, SettlementClient, type SettlementClientConfig, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type UserDto, type UserHistory, applyPaymasterGasEstimates, authenticateRequest, buildSdkErrorBody, createIssuerService, createNativePtQuoter, createSdkErrorMapper, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, defaultPolicyFor, evaluateRedemption, handleClaimStatus, handleDelegateSubmit, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };
package/dist/index.d.ts CHANGED
@@ -621,6 +621,14 @@ declare class RelayService {
621
621
  * issuer-signed `BurnRequest` path.
622
622
  */
623
623
  prepareBurn(params: PrepareBurnParams): Promise<PartialUserOperation>;
624
+ /**
625
+ * Build a dummy `PartialUserOperation` for the mint scenario, suitable
626
+ * for `feeManager.estimateGasFee({ partialUserOp, ... })`. NO signing —
627
+ * uses a 65-byte zero signature in place of the real minter sig.
628
+ */
629
+ previewMintUserOp(params: PreviewMintParams): PartialUserOperation;
630
+ /** Burn-side mirror of `previewMintUserOp`. */
631
+ previewBurnUserOp(params: PreviewBurnParams): PartialUserOperation;
624
632
  }
625
633
  /**
626
634
  * Sig-gated `PointToken.mint(to, amount, deadline, minterSig)`.
@@ -707,19 +715,131 @@ interface PrepareBurnParams {
707
715
  verificationGasLimit?: bigint;
708
716
  preVerificationGas?: bigint;
709
717
  }
718
+ /**
719
+ * Inputs for `previewMintUserOp` — strict subset of `PrepareMintParams`
720
+ * that excludes the signing wallet, EIP-712 domain, and on-chain nonce.
721
+ * These don't affect calldata shape so the bundler estimate doesn't
722
+ * need them.
723
+ */
724
+ interface PreviewMintParams {
725
+ userAddress: Address;
726
+ aaNonce: bigint;
727
+ pointTokenAddress: Address;
728
+ amount: bigint;
729
+ deadline: bigint;
730
+ mintFeeWrapperAddress?: Address;
731
+ }
732
+ interface PreviewBurnParams {
733
+ userAddress: Address;
734
+ aaNonce: bigint;
735
+ pointTokenAddress: Address;
736
+ amount: bigint;
737
+ deadline: bigint;
738
+ }
739
+
740
+ /**
741
+ * In-memory cache for bundler-estimated gas units, keyed by
742
+ * `(scenario, contractCodehash, paymaster)`.
743
+ *
744
+ * Why these key components:
745
+ * - `scenario` — distinguishes mint vs burn vs swap; their UserOps
746
+ * differ in calldata shape so estimates differ.
747
+ * - `contractCodehash` — fetched from the SC address via `eth_getCode`,
748
+ * cached separately for 1h. When the SC is upgraded
749
+ * the codehash changes → the gas key auto-invalidates
750
+ * (no manual purge needed).
751
+ * - `paymaster` — paymaster contract version affects postOp gas;
752
+ * rotating paymaster naturally invalidates.
753
+ *
754
+ * Bound: max `maxEntries` to defend against unbounded growth on
755
+ * long-running issuer backends; eldest evicted first (LRU-by-insertion).
756
+ */
757
+ interface GasUnitsCacheConfig {
758
+ /** Gas-units cache TTL in ms. Default 5 minutes. */
759
+ ttlMs?: number;
760
+ /** Codehash cache TTL in ms. Default 1 hour. */
761
+ codehashTtlMs?: number;
762
+ /** Max entries before LRU eviction. Default 100. */
763
+ maxEntries?: number;
764
+ }
765
+ declare class GasUnitsCache {
766
+ private readonly entries;
767
+ private readonly codehashEntries;
768
+ private readonly ttlMs;
769
+ private readonly codehashTtlMs;
770
+ private readonly maxEntries;
771
+ constructor(config?: GasUnitsCacheConfig);
772
+ buildKey(params: {
773
+ scenario: string;
774
+ contractAddress: Address;
775
+ paymasterAddress?: Address;
776
+ provider: PublicClient;
777
+ }): Promise<string>;
778
+ get(key: string, now?: number): bigint | null;
779
+ set(key: string, gasUnits: bigint, now?: number): void;
780
+ invalidate(): void;
781
+ size(): number;
782
+ private getCodehash;
783
+ }
710
784
 
785
+ /**
786
+ * Minimal duck-typed shape of an ERC-4337 bundler client supporting
787
+ * `eth_estimateUserOperationGas`. Compatible with `permissionless`'s
788
+ * `createBundlerClient` and any spec-compliant bundler (Pimlico, Alchemy,
789
+ * StackUp, Biconomy). The SDK does NOT add `permissionless` as a hard
790
+ * dependency — callers pass whatever client they already use.
791
+ */
792
+ interface BundlerEstimatorClient {
793
+ estimateUserOperationGas(args: {
794
+ sender: Address;
795
+ nonce: bigint;
796
+ callData: Hex;
797
+ signature?: Hex;
798
+ paymaster?: Address;
799
+ paymasterData?: Hex;
800
+ }): Promise<{
801
+ callGasLimit: bigint;
802
+ verificationGasLimit: bigint;
803
+ preVerificationGas: bigint;
804
+ paymasterVerificationGasLimit?: bigint;
805
+ paymasterPostOpGasLimit?: bigint;
806
+ }>;
807
+ }
808
+ type GasFeeSource = "cache" | "bundler" | "fallback";
809
+ /**
810
+ * Hooks called by `FeeManager` so callers can wire their own
811
+ * observability (Pino logger, Prometheus counters, ...). All hooks are
812
+ * synchronous and best-effort — they MUST NOT throw.
813
+ */
814
+ interface FeeManagerMetrics {
815
+ onEstimate?: (info: {
816
+ source: GasFeeSource;
817
+ scenario?: string;
818
+ gasUnits: bigint;
819
+ latencyMs: number;
820
+ }) => void;
821
+ onBundlerError?: (info: {
822
+ scenario?: string;
823
+ reason: string;
824
+ }) => void;
825
+ }
711
826
  interface FeeManagerConfig {
712
- /** Provider used for gas price reads. */
827
+ /** Provider used for gas price reads + codehash lookup. */
713
828
  provider: PublicClient;
714
829
  /**
715
- * Typical gas used by a single sponsored UserOp. Default: 500_000.
716
- * The manager multiplies this by current gas price to get native
717
- * cost, then converts via the injected `quoteNativeToFee`.
830
+ * Hardcoded fallback gas units, used when (a) `bundlerClient` is not
831
+ * configured OR (b) the bundler call fails. Default: 500_000.
718
832
  */
719
833
  gasUnits?: bigint;
720
834
  /**
721
- * Safety margin applied before charging the user, as basis points.
722
- * 12_000 = 120%. Default: 12_000.
835
+ * Safety margin applied on top of the bundler/fallback estimate, in
836
+ * basis points. 10_000 = 100% (no extra). Default: 10_000.
837
+ *
838
+ * Pimlico's `eth_estimateUserOperationGas` already adds ~10-15% safety
839
+ * margin internally, so a second SDK-side premium just pads padding
840
+ * and over-charges users. We dropped the historical 12_000 (120%)
841
+ * default after measuring `actualGasUsed / estimate ≈ 0.92` across
842
+ * 100 dev mints.
723
843
  */
724
844
  gasPremiumBps?: number;
725
845
  /**
@@ -732,6 +852,60 @@ interface FeeManagerConfig {
732
852
  * or an oracle feed.
733
853
  */
734
854
  quoteNativeToFee: (amountNative: bigint) => Promise<bigint>;
855
+ /**
856
+ * Optional ERC-4337 bundler client. When set, `estimateGasFee({
857
+ * partialUserOp, ... })` will call `eth_estimateUserOperationGas`
858
+ * (cached) for per-UserOp accuracy. Omit to keep the legacy hardcoded
859
+ * path.
860
+ */
861
+ bundlerClient?: BundlerEstimatorClient;
862
+ /** Gas-units cache config — see `GasUnitsCache`. */
863
+ cache?: GasUnitsCacheConfig;
864
+ /**
865
+ * Fixed overhead added to bundler estimates to account for paymaster
866
+ * verification + postOp gas, since we estimate WITHOUT paymaster
867
+ * context (chicken-and-egg: paymaster data depends on gas limits).
868
+ * Default: 80_000 — covers Pimlico singleton paymaster v0.7 postOp.
869
+ */
870
+ paymasterOverheadGas?: bigint;
871
+ /** Optional observability hooks. */
872
+ metrics?: FeeManagerMetrics;
873
+ }
874
+ /**
875
+ * Parameters for a single bundler-driven `estimateGasFee` call.
876
+ *
877
+ * All fields optional for backwards compat: when `partialUserOp` is
878
+ * omitted, FeeManager falls back to the legacy `gasUnits × gasPrice`
879
+ * path.
880
+ */
881
+ interface EstimateGasFeeOptions {
882
+ /**
883
+ * Partial UserOperation to simulate. The bundler runs the calldata in
884
+ * a virtual EVM and returns gas limits. Pass `signature` as a 65-byte
885
+ * dummy when the user hasn't signed yet.
886
+ *
887
+ * Estimate WITHOUT paymaster fields (paymaster/paymasterData omitted)
888
+ * to avoid the chicken-and-egg with paymasterData generation —
889
+ * `paymasterOverheadGas` is added afterwards.
890
+ */
891
+ partialUserOp?: {
892
+ sender: Address;
893
+ nonce: bigint;
894
+ callData: Hex;
895
+ signature?: Hex;
896
+ };
897
+ /**
898
+ * Contract address being called — used for cache key (codehash
899
+ * lookup). Typically the PointToken address for mint/burn.
900
+ */
901
+ contractAddress?: Address;
902
+ /** Scenario label used in the cache key (e.g. 'mint', 'burn'). */
903
+ scenario?: string;
904
+ /**
905
+ * Paymaster address used in the cache key. When omitted, '0x0' is
906
+ * used — fine if a single paymaster is used across the deployment.
907
+ */
908
+ paymasterAddress?: Address;
735
909
  }
736
910
  /**
737
911
  * Computes how much fee to collect from the user to cover the gas cost
@@ -740,34 +914,55 @@ interface FeeManagerConfig {
740
914
  * The fee is expressed in the **fee currency** chosen by the caller
741
915
  * (PT for mint/burn, USDT for swap/perp_deposit) — not hardcoded.
742
916
  *
917
+ * Two paths:
918
+ * 1. Legacy (no `bundlerClient`): gasUnits × gasPrice × premium
919
+ * 2. Bundler (recommended): bundler.estimateUserOperationGas →
920
+ * sum gas limits → × gasPrice × premium
921
+ *
922
+ * The bundler path caches by `(scenario, contractCodehash, paymaster)`
923
+ * for ~99% hit rate after warm-up; cache invalidates automatically on
924
+ * SC upgrade via codehash change.
925
+ *
743
926
  * **No operator rebalancing.** The operator does not hold ETH directly;
744
- * gas is paid by PAFI sponsor-relayer via the paymaster-proxy (see
745
- * [SPONSORED_PATH_FLOW.md]). The fee collected here is an
746
- * application-level ERC-20 transfer inside the same UserOp batch, not a
747
- * reimbursement to a wallet that needs topping up.
927
+ * gas is paid by PAFI sponsor-relayer via the paymaster-proxy. The fee
928
+ * collected here is an application-level ERC-20 transfer inside the same
929
+ * UserOp batch, not a reimbursement to a wallet that needs topping up.
748
930
  */
749
931
  declare class FeeManager {
750
932
  private readonly provider;
751
933
  private readonly gasUnits;
752
934
  private readonly gasPremiumBps;
753
935
  private readonly quoteNativeToFee;
936
+ private readonly bundlerClient;
937
+ private readonly cache;
938
+ private readonly paymasterOverheadGas;
939
+ private readonly metrics;
754
940
  private cachedFee;
755
941
  private cacheExpiresAt;
756
- private static readonly CACHE_TTL_MS;
942
+ private static readonly FEE_CACHE_TTL_MS;
757
943
  constructor(config: FeeManagerConfig);
758
944
  /**
759
945
  * Estimate the fee (in the caller's fee currency) to charge for the
760
- * next sponsored UserOp:
946
+ * next sponsored UserOp.
761
947
  *
948
+ * gasUnits = bundler-estimated (cached) or hardcoded fallback
762
949
  * nativeCost = gasUnits × gasPrice
763
950
  * withPremium = nativeCost × premiumBps / 10_000
764
951
  * fee = quoteNativeToFee(withPremium)
765
952
  *
766
- * For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
767
- * from the response, the name `estimateGasFee` is kept — but the
768
- * currency depends on how the caller wired `quoteNativeToFee`.
953
+ * When `opts.partialUserOp` is omitted, behaves exactly like v0.16.x:
954
+ * uses the hardcoded `gasUnits` default.
955
+ */
956
+ estimateGasFee(opts?: EstimateGasFeeOptions): Promise<bigint>;
957
+ /**
958
+ * Manually purge the per-scenario gas-units cache. Useful after an SC
959
+ * upgrade when ops wants the next estimate to refresh immediately
960
+ * (the codehash check would catch it on the NEXT call anyway, but
961
+ * this forces it now).
769
962
  */
770
- estimateGasFee(): Promise<bigint>;
963
+ invalidateCache(): void;
964
+ private resolveGasUnits;
965
+ private safeEmit;
771
966
  }
772
967
 
773
968
  /** Decoded Transfer(from=0x0 → to) event used to finalize a mint. */
@@ -3487,4 +3682,4 @@ declare class MemoryRedemptionHistoryStore implements IRedemptionHistoryStore {
3487
3682
 
3488
3683
  declare const PAFI_ISSUER_SDK_VERSION: string;
3489
3684
 
3490
- export { AdapterMisconfiguredError, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedemptionEvaluateRequest, type ApiRedemptionEvaluateResponse, type ApiRedemptionPreviewRequest, type ApiRedemptionPreviewResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type ClaimDto, type ConfigDto, ConfigurationError, DEFAULT_REDEMPTION_POLICY, type DecodedCallDto, DefaultPolicyEngine, type DefaultPolicyEngineOptions, type DelegatePrepareDto, type DelegateStatusDto, type EvaluateInput, FeeManager, type FeeManagerConfig, type FetchFailureReason, type FetchResult, type GasFeeDto, type HandleDelegateSubmitParams, type HandleDelegateSubmitResult, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type IRateLimiter, type IRedemptionHistoryStore, type ISessionStore, InMemoryCursorStore, IssuerApiAdapter, type IssuerApiAdapterConfig, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemoryPendingUserOpStore, MemoryRateLimiter, MemoryRedemptionHistoryStore, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type MobilePrepareDto, type MobileSubmitDto, type NativePtQuoterConfig, NonceManager, NoopRateLimiter, PAFI_ISSUER_SDK_VERSION, PTClaimError, PTClaimHandler, type PTClaimHandlerConfig, type PTClaimRequest, type PTClaimResponse, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PaymasterGasEstimates, type PendingCredit, type PendingUserOpEntry, PendingUserOpForbiddenError, PendingUserOpNotFoundError, type PerpDepositDto, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, PolicyProvider, type PolicyProviderConfig, type PoolsDto, type PoolsProvider, type PreValidateMintResult, type PrepareBurnParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, REDEMPTION_HISTORY_WINDOW_SEC, type RateLimitAction, type RateLimiterConfig, type RedeemDto, type RedeemPrepareDto, RedemptionService, type RedemptionServiceConfig, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type ResolvedPolicy, type RetryConfig, type SdkErrorBody, type SdkErrorMapperFactories, type SdkErrorStatus, type SerializedUserOpTypedData, type Session, SettlementClient, type SettlementClientConfig, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type UserDto, type UserHistory, applyPaymasterGasEstimates, authenticateRequest, buildSdkErrorBody, createIssuerService, createNativePtQuoter, createSdkErrorMapper, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, defaultPolicyFor, evaluateRedemption, handleClaimStatus, handleDelegateSubmit, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };
3685
+ export { AdapterMisconfiguredError, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedemptionEvaluateRequest, type ApiRedemptionEvaluateResponse, type ApiRedemptionPreviewRequest, type ApiRedemptionPreviewResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, type BundlerEstimatorClient, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type ClaimDto, type ConfigDto, ConfigurationError, DEFAULT_REDEMPTION_POLICY, type DecodedCallDto, DefaultPolicyEngine, type DefaultPolicyEngineOptions, type DelegatePrepareDto, type DelegateStatusDto, type EstimateGasFeeOptions, type EvaluateInput, FeeManager, type FeeManagerConfig, type FeeManagerMetrics, type FetchFailureReason, type FetchResult, type GasFeeDto, type GasFeeSource, GasUnitsCache, type GasUnitsCacheConfig, type HandleDelegateSubmitParams, type HandleDelegateSubmitResult, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type IRateLimiter, type IRedemptionHistoryStore, type ISessionStore, InMemoryCursorStore, IssuerApiAdapter, type IssuerApiAdapterConfig, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemoryPendingUserOpStore, MemoryRateLimiter, MemoryRedemptionHistoryStore, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type MobilePrepareDto, type MobileSubmitDto, type NativePtQuoterConfig, NonceManager, NoopRateLimiter, PAFI_ISSUER_SDK_VERSION, PTClaimError, PTClaimHandler, type PTClaimHandlerConfig, type PTClaimRequest, type PTClaimResponse, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PaymasterGasEstimates, type PendingCredit, type PendingUserOpEntry, PendingUserOpForbiddenError, PendingUserOpNotFoundError, type PerpDepositDto, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, PolicyProvider, type PolicyProviderConfig, type PoolsDto, type PoolsProvider, type PreValidateMintResult, type PrepareBurnParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, type PreviewBurnParams, type PreviewMintParams, REDEMPTION_HISTORY_WINDOW_SEC, type RateLimitAction, type RateLimiterConfig, type RedeemDto, type RedeemPrepareDto, RedemptionService, type RedemptionServiceConfig, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type ResolvedPolicy, type RetryConfig, type SdkErrorBody, type SdkErrorMapperFactories, type SdkErrorStatus, type SerializedUserOpTypedData, type Session, SettlementClient, type SettlementClientConfig, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type UserDto, type UserHistory, applyPaymasterGasEstimates, authenticateRequest, buildSdkErrorBody, createIssuerService, createNativePtQuoter, createSdkErrorMapper, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, defaultPolicyFor, evaluateRedemption, handleClaimStatus, handleDelegateSubmit, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };