@pafi-dev/issuer 0.2.0 → 0.3.0-alpha.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
@@ -1,5 +1,5 @@
1
1
  import { Address, Hex, PublicClient, Chain } from 'viem';
2
- import { PointTokenDomainConfig, MintRequest, EIP712Signature, MintParams, SwapParams, ReceiverConsent, PathKey, PoolKey } from '@pafi-dev/core';
2
+ import { PointTokenDomainConfig, MintRequest, EIP712Signature, MintParams, SwapParams, ReceiverConsent, PathKey, PoolKey, SponsorshipScenario, PartialUserOperation } from '@pafi-dev/core';
3
3
  export { encodeExtData } from '@pafi-dev/core';
4
4
 
5
5
  /**
@@ -78,6 +78,24 @@ interface IPointLedger {
78
78
  * supplied when the status is `MINTED`.
79
79
  */
80
80
  updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void>;
81
+ /**
82
+ * Reserve a pending off-chain credit before the burn tx is submitted.
83
+ *
84
+ * Returns a lockId that the burn indexer uses to correlate the
85
+ * on-chain burn event back to this credit request.
86
+ *
87
+ * Throws if the ledger doesn't support the reverse flow (legacy
88
+ * implementations) — callers should catch and fall back.
89
+ */
90
+ reservePendingCredit?(userAddress: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string>;
91
+ /**
92
+ * Finalize a reserved credit when the on-chain Burn event is seen by
93
+ * the BurnIndexer. Idempotent — safe to call multiple times with the
94
+ * same txHash (no double-credit).
95
+ *
96
+ * Throws if the lockId is unknown or already resolved.
97
+ */
98
+ resolveCreditByBurnTx?(lockId: string, txHash: Hex): Promise<void>;
81
99
  }
82
100
 
83
101
  /**
@@ -109,6 +127,10 @@ declare class MemoryPointLedger implements IPointLedger {
109
127
  releaseLock(lockId: string): Promise<void>;
110
128
  deductBalance(userAddress: Address, amount: bigint, txHash: Hex, tokenAddress?: Address): Promise<void>;
111
129
  updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void>;
130
+ private pendingCredits;
131
+ private nextCreditId;
132
+ reservePendingCredit(userAddress: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string>;
133
+ resolveCreditByBurnTx(lockId: string, txHash: Hex): Promise<void>;
112
134
  /**
113
135
  * Auto-expire any PENDING lock past its expiry. Called lazily on every
114
136
  * read/write so the in-memory state stays self-cleaning without a timer.
@@ -536,93 +558,76 @@ declare class RelayService {
536
558
  * decide whether to release the ledger lock (`SUBMIT_FAILED` and
537
559
  * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
538
560
  * need manual review because the tx may still land).
561
+ *
562
+ * @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
563
+ * `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
564
+ * still needs to finalize Relayer v2 ABI before the replacements
565
+ * can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
539
566
  */
540
567
  submitMintAndSwap(params: SubmitMintAndSwapParams): Promise<RelayResult>;
541
568
  }
542
569
 
543
570
  interface FeeManagerConfig {
544
- /** Provider used for gas price + balance reads. */
571
+ /** Provider used for gas price reads. */
545
572
  provider: PublicClient;
546
- /** Operator wallet whose native balance the manager monitors. */
547
- operatorWallet: OperatorWalletLike;
548
- /** USDT token address on the target chain (used for rebalance swaps). */
549
- usdtAddress: Address;
550
- /** Wrapped-native token address (WETH on Base/Ethereum, WMATIC, etc). */
551
- nativeWrappedAddress: Address;
552
573
  /**
553
- * Typical gas used by a `mintAndSwap` transaction. Default: 500_000. The
554
- * manager multiplies this by current gas price to get the native cost,
555
- * then converts to USDT via the injected `quoteNativeToUsdt`.
574
+ * Typical gas used by a single sponsored UserOp. Default: 500_000.
575
+ * The manager multiplies this by current gas price to get native
576
+ * cost, then converts via the injected `quoteNativeToFee`.
556
577
  */
557
- mintAndSwapGasUnits?: bigint;
578
+ gasUnits?: bigint;
558
579
  /**
559
- * Safety margin applied to the gas estimate before charging the user.
560
- * Expressed as a basis-point multiplier, e.g. 12_000 = 120%. Default 12_000.
580
+ * Safety margin applied before charging the user, as basis points.
581
+ * 12_000 = 120%. Default: 12_000.
561
582
  */
562
583
  gasPremiumBps?: number;
563
584
  /**
564
- * Price conversion: given an amount of native token (wei), return the
565
- * equivalent amount of USDT. Injected so the manager is chain-agnostic
566
- * production wires this to `@pafi/core` V4 quoting or an oracle feed.
567
- */
568
- quoteNativeToUsdt: (amountNative: bigint) => Promise<bigint>;
569
- /**
570
- * Rebalance trigger: when the operator's native balance falls below
571
- * `rebalanceThresholdWei`, `rebalanceIfNeeded()` swaps `rebalanceUsdtAmount`
572
- * worth of USDT into native. Both optional — omit to disable rebalancing.
573
- */
574
- rebalanceThresholdWei?: bigint;
575
- rebalanceUsdtAmount?: bigint;
576
- /**
577
- * Actual swap executor — the manager calls this when a rebalance is
578
- * triggered. Injected so the SDK does not hard-code a DEX choice; the
579
- * issuer wires it to the UniversalRouter (via `@pafi/core swap/`) or
580
- * whatever liquidity venue they trust. Required iff the rebalance
581
- * fields above are set.
585
+ * Quote function given an amount of native wei, return the
586
+ * equivalent amount in the fee currency (PT raw units in v1.4,
587
+ * USDT 6-decimal in legacy v1.2 flows).
588
+ *
589
+ * Injected so the manager stays chain- and token-agnostic. Issuers
590
+ * wire it to `@pafi-dev/core` V4 quoting, a subgraph query, or an
591
+ * oracle feed.
582
592
  */
583
- swapUsdtToNative?: (amountUsdt: bigint) => Promise<void>;
593
+ quoteNativeToFee: (amountNative: bigint) => Promise<bigint>;
584
594
  }
585
595
  /**
586
- * Manages the operator wallet's economics:
596
+ * Computes how much fee to collect from the user to cover the gas cost
597
+ * of a sponsored UserOp.
598
+ *
599
+ * ## v1.4 scope change
587
600
  *
588
- * 1. `estimateGasFee()` how many USDT to deduct from the swap proceeds
589
- * to cover the operator's gas cost for the upcoming `mintAndSwap`.
590
- * 2. `rebalanceIfNeeded()` — when the operator's native balance gets
591
- * low, swap some of the accumulated USDT fee back into native gas
592
- * token so the operator never runs dry.
601
+ * The fee is now expressed in the **fee currency** chosen by the caller
602
+ * (PT for mint/burn, USDT for swap/perp_deposit) not hardcoded to USDT.
593
603
  *
594
- * Both calculations are intentionally injection-based: gas estimation and
595
- * USDT→native swapping both depend on DEX state, which the SDK deliberately
596
- * does not own. Issuers supply the conversion + swap functions.
604
+ * **Operator rebalancing is gone.** In v1.4 the operator no longer holds
605
+ * ETH directly gas is paid by Coinbase Paymaster via the paymaster-proxy
606
+ * (see [SPONSORED_PATH_FLOW.md]). The fee collected here is an
607
+ * application-level ERC-20 transfer inside the same UserOp batch, not a
608
+ * reimbursement to a wallet that needs topping up.
609
+ *
610
+ * `rebalanceIfNeeded()` and `swapUsdtToNative` were removed in 0.3.0.
597
611
  */
598
612
  declare class FeeManager {
599
613
  private readonly provider;
600
- private readonly operatorWallet;
601
- private readonly mintAndSwapGasUnits;
614
+ private readonly gasUnits;
602
615
  private readonly gasPremiumBps;
603
- private readonly quoteNativeToUsdt;
604
- private readonly rebalanceThresholdWei?;
605
- private readonly rebalanceUsdtAmount?;
606
- private readonly swapUsdtToNative?;
616
+ private readonly quoteNativeToFee;
607
617
  constructor(config: FeeManagerConfig);
608
618
  /**
609
- * Estimate the USDT fee the operator should charge for a single
610
- * `mintAndSwap`. Computed as:
619
+ * Estimate the fee (in the caller's fee currency) to charge for the
620
+ * next sponsored UserOp:
611
621
  *
612
- * nativeCost = gasUnits × gasPrice
613
- * premiumNativeCost = nativeCost × premiumBps / 10_000
614
- * usdtFee = quoteNativeToUsdt(premiumNativeCost)
615
- */
616
- estimateGasFee(): Promise<bigint>;
617
- /**
618
- * Check the operator's native balance and, if it has dropped below the
619
- * configured threshold, trigger a USDT→native rebalance via the injected
620
- * `swapUsdtToNative` function.
622
+ * nativeCost = gasUnits × gasPrice
623
+ * withPremium = nativeCost × premiumBps / 10_000
624
+ * fee = quoteNativeToFee(withPremium)
621
625
  *
622
- * Returns `true` if a rebalance was performed, `false` otherwise.
623
- * Silently no-ops when rebalance is not configured.
626
+ * For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
627
+ * from the response, the name `estimateGasFee` is kept — but the
628
+ * currency depends on how the caller wired `quoteNativeToFee`.
624
629
  */
625
- rebalanceIfNeeded(): Promise<boolean>;
630
+ estimateGasFee(): Promise<bigint>;
626
631
  }
627
632
 
628
633
  /**
@@ -751,6 +756,12 @@ declare class MintingGateway {
751
756
  private readonly now;
752
757
  private readonly defaultLockBufferMs;
753
758
  constructor(config: MintingGatewayConfig);
759
+ /**
760
+ * @deprecated Since 0.3.0 — will be renamed to `processMint()` once
761
+ * the SC team finalizes Relayer v2 ABI. The new flow drops the
762
+ * swap steps entirely (no more single-call mint+swap); users swap
763
+ * separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
764
+ */
754
765
  processMintAndCashOut(request: MintAndCashOutRequest): Promise<MintAndCashOutResponse>;
755
766
  private computeLockDurationMs;
756
767
  /**
@@ -782,6 +793,16 @@ interface MintEvent {
782
793
  /** Log index within the tx, for deterministic ordering */
783
794
  logIndex: number;
784
795
  }
796
+ /** Decoded Transfer(from=user → 0x0) event used to finalize a burn-for-credit. */
797
+ interface BurnEvent {
798
+ /** The burner — user whose PT was burned. */
799
+ from: Address;
800
+ /** Amount burned. */
801
+ amount: bigint;
802
+ blockNumber: bigint;
803
+ txHash: Hex;
804
+ logIndex: number;
805
+ }
785
806
  /**
786
807
  * Cursor persistence interface — the indexer reports the next block
787
808
  * number it is about to process so the caller can write it to Redis /
@@ -901,10 +922,102 @@ declare class PointIndexer {
901
922
  private finalize;
902
923
  }
903
924
 
925
+ interface BurnIndexerConfig {
926
+ provider: PublicClient;
927
+ pointTokenAddress: Address;
928
+ ledger: IPointLedger;
929
+ /** Block to start from on first run. Ignored if cursor store has value. */
930
+ fromBlock?: bigint;
931
+ cursorStore?: IIndexerCursorStore;
932
+ /**
933
+ * Reorg safety — only treat events as final after this many
934
+ * confirmations. Default: 3.
935
+ */
936
+ confirmations?: number;
937
+ /** Blocks per getLogs call. Default: 2000. */
938
+ batchSize?: number;
939
+ /** Polling interval (ms). Default: 5000. */
940
+ pollIntervalMs?: number;
941
+ now?: () => number;
942
+ }
943
+ /**
944
+ * Mirror of `PointIndexer` for the reverse direction — watches
945
+ * `Transfer(user → 0x0)` events (ERC-20 burns) on the PointToken
946
+ * contract and finalizes pending off-chain credits.
947
+ *
948
+ * Finalization flow:
949
+ * 1. For each Burn event at `{from, amount, txHash}`:
950
+ * 2. Call `ledger.resolveCreditByBurnTx(lockId, txHash)` where `lockId`
951
+ * is resolved by the caller's `onMatchCredit` hook or a
952
+ * ledger-specific lookup. The SDK does not prescribe the matching
953
+ * strategy — issuers with a Postgres ledger can JOIN by
954
+ * `(from, amount, status=PENDING)`; the in-memory ledger matches
955
+ * by `lockId` supplied out-of-band.
956
+ *
957
+ * When no pending credit matches an observed Burn event, the indexer
958
+ * logs + skips — **it never credits off-chain** from a Burn that was
959
+ * not first reserved via `reservePendingCredit()`. This prevents
960
+ * spurious credits from one-off admin burns or direct burn calls
961
+ * outside the issuer SDK.
962
+ */
963
+ declare class BurnIndexer {
964
+ private readonly provider;
965
+ private readonly pointTokenAddress;
966
+ private readonly ledger;
967
+ private readonly cursorStore;
968
+ private readonly startBlock;
969
+ private readonly confirmations;
970
+ private readonly batchSize;
971
+ private readonly pollIntervalMs;
972
+ /**
973
+ * Caller-supplied matcher. Return the lockId to resolve for a given
974
+ * burn event, or `undefined` to skip. Runs synchronously via the
975
+ * ledger's query path.
976
+ *
977
+ * Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
978
+ * lock id `burn-${from}-${amount}` — the in-memory ledger assigns
979
+ * incrementing IDs so callers with the memory ledger must provide a
980
+ * custom matcher. Real DB-backed ledgers override this to JOIN on
981
+ * their `pending_credits` table.
982
+ */
983
+ matchLockId: (event: BurnEvent) => Promise<string | undefined>;
984
+ private running;
985
+ private timer;
986
+ constructor(config: BurnIndexerConfig);
987
+ start(): void;
988
+ stop(): void;
989
+ tick(): Promise<void>;
990
+ private scheduleNext;
991
+ /**
992
+ * Scan `[from, to]` inclusive for burn events. Callers can drive this
993
+ * directly to backfill a specific range without `start()`. Cursor is
994
+ * advanced to `to + 1` on completion.
995
+ */
996
+ processBlockRange(from: bigint, to: bigint): Promise<void>;
997
+ private decodeBurnEvents;
998
+ /**
999
+ * Resolve a matching pending credit for this burn event and call
1000
+ * `ledger.resolveCreditByBurnTx(lockId, txHash)`. If no match found,
1001
+ * log + skip.
1002
+ */
1003
+ private finalize;
1004
+ }
1005
+
904
1006
  interface ApiConfigResponse {
905
1007
  chainId: number;
906
1008
  contracts: {
1009
+ /**
1010
+ * Legacy single-token field — kept for backward compat with v0.1.x
1011
+ * frontends. Prefer `pointTokens` for multi-token issuers.
1012
+ */
907
1013
  pointToken?: Address;
1014
+ /**
1015
+ * All supported PointToken addresses (v0.2.0+). Single-token issuers
1016
+ * will have one entry that matches `pointToken`. Multi-token
1017
+ * issuers expose the full list here so the frontend can render a
1018
+ * token picker.
1019
+ */
1020
+ pointTokens?: Address[];
908
1021
  relay?: Address;
909
1022
  issuerRegistry?: Address;
910
1023
  pointTokenFactory?: Address;
@@ -912,6 +1025,17 @@ interface ApiConfigResponse {
912
1025
  poolManager?: Address;
913
1026
  usdt?: Address;
914
1027
  };
1028
+ /**
1029
+ * Absolute URL that the Issuer App opens after a successful claim to
1030
+ * let the user swap PT → USDT or deposit into the perp DEX on PAFI
1031
+ * Web. Mobile opens this in an in-app browser
1032
+ * (SFSafariViewController / Chrome Custom Tabs). Desktop opens in a
1033
+ * popup. See [MOBILE_SDK_INTEGRATION.md] "PAFI Web Handoff" section.
1034
+ *
1035
+ * Optional — if omitted, the Issuer App should hide the "Open PAFI"
1036
+ * button.
1037
+ */
1038
+ pafiWebUrl?: string;
915
1039
  }
916
1040
  interface ApiNonceResponse {
917
1041
  nonce: string;
@@ -967,6 +1091,7 @@ interface ApiUserResponse {
967
1091
  balance: bigint;
968
1092
  isMinter: boolean;
969
1093
  }
1094
+ /** @deprecated Since 0.3.0 — use `ApiClaimRequest` (mint-only) instead. Removed in 2.0. */
970
1095
  interface ApiClaimAndSwapRequest {
971
1096
  chainId: number;
972
1097
  pointTokenAddress: Address;
@@ -985,6 +1110,7 @@ interface ApiClaimAndSwapRequest {
985
1110
  /** Unix seconds. */
986
1111
  swapDeadline: bigint;
987
1112
  }
1113
+ /** @deprecated Since 0.3.0 — use `ApiClaimResponse` instead. Removed in 2.0. */
988
1114
  interface ApiClaimAndSwapResponse {
989
1115
  txHash: Hex;
990
1116
  lockId: string;
@@ -1032,6 +1158,12 @@ interface IssuerApiHandlersConfig {
1032
1158
  pointTokenAddresses?: Address[];
1033
1159
  chainId: number;
1034
1160
  contracts: ApiConfigResponse["contracts"];
1161
+ /**
1162
+ * Optional — URL that the Issuer App opens for PT→USDT swap or perp
1163
+ * deposit after a successful claim. Surfaced in `/config` response.
1164
+ * See [MOBILE_SDK_INTEGRATION.md] "PAFI Web Handoff".
1165
+ */
1166
+ pafiWebUrl?: string;
1035
1167
  /** Required by `handleGasFee`; omit to disable the endpoint. */
1036
1168
  feeManager?: FeeManager;
1037
1169
  /** Required by `handlePools`; omit to disable the endpoint. */
@@ -1065,6 +1197,7 @@ declare class IssuerApiHandlers {
1065
1197
  private readonly defaultToken;
1066
1198
  private readonly chainId;
1067
1199
  private readonly contracts;
1200
+ private readonly pafiWebUrl?;
1068
1201
  private readonly feeManager?;
1069
1202
  private readonly poolsProvider?;
1070
1203
  constructor(config: IssuerApiHandlersConfig);
@@ -1115,8 +1248,14 @@ declare class IssuerApiHandlers {
1115
1248
  /**
1116
1249
  * `POST /claim-and-swap`
1117
1250
  *
1118
- * The terminal handler: forwards the verified consent to the
1119
- * MintingGateway, which runs the 11-step flow.
1251
+ * @deprecated Since 0.3.0 the single-call mint-then-swap flow is
1252
+ * retired in v1.4. Use the new `handleClaim()` (mint only) and let
1253
+ * the user swap separately on PAFI Web. See
1254
+ * [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
1255
+ * removed in 2.0.
1256
+ *
1257
+ * Legacy behavior: the terminal handler forwards the verified
1258
+ * consent to the MintingGateway, which runs the 11-step flow.
1120
1259
  */
1121
1260
  handleClaimAndSwap(userAddress: Address, request: ApiClaimAndSwapRequest): Promise<ApiClaimAndSwapResponse>;
1122
1261
  }
@@ -1219,26 +1358,29 @@ interface SubgraphNativeUsdtQuoterConfig {
1219
1358
  now?: () => number;
1220
1359
  }
1221
1360
  /**
1222
- * Create a `quoteNativeToUsdt` function backed by the PAFI subgraph's
1223
- * `Bundle.ethPriceUSD`.
1361
+ * Create a native→USDT quoter backed by the PAFI subgraph's
1362
+ * `Bundle.ethPriceUSD`. The returned function has the shape
1363
+ * `(amountNative: bigint) => Promise<bigint>` and can be passed as
1364
+ * `quoteNativeToFee` to `FeeManager` — in v1.4 the fee currency
1365
+ * is configured at the integration layer, not hardcoded here.
1224
1366
  *
1225
- * Used by `FeeManager.estimateGasFee()` to convert the operator's native
1226
- * gas cost into the USDT amount deducted from the user's cashout. Price
1227
- * precision is not critical here — a 1-2% drift is acceptable since
1228
- * the operator already takes a `gasPremiumBps` buffer.
1367
+ * Used by `FeeManager.estimateGasFee()` to convert the gas cost into
1368
+ * an ERC-20 amount charged as part of the sponsored UserOp batch.
1369
+ * Price precision is not critical — a 1-2% drift is acceptable since
1370
+ * the fee manager applies a `gasPremiumBps` buffer.
1229
1371
  *
1230
- * The result is cached in-process with a short TTL (default 30s). If the
1231
- * subgraph is unreachable, falls back to `fallbackEthPriceUsd` so gas
1232
- * estimation doesn't block cashouts during a subgraph outage.
1372
+ * The result is cached in-process with a short TTL (default 30s). If
1373
+ * the subgraph is unreachable, falls back to `fallbackEthPriceUsd` so
1374
+ * gas estimation doesn't block user flow during a subgraph outage.
1233
1375
  *
1234
1376
  * @example
1235
1377
  * ```ts
1236
- * import { createSubgraphNativeUsdtQuoter, createIssuerService } from "@pafi/issuer";
1378
+ * import { createSubgraphNativeUsdtQuoter, createIssuerService } from "@pafi-dev/issuer";
1237
1379
  *
1238
1380
  * const service = createIssuerService({
1239
1381
  * // ...other config
1240
1382
  * fee: {
1241
- * quoteNativeToUsdt: createSubgraphNativeUsdtQuoter({
1383
+ * quoteNativeToFee: createSubgraphNativeUsdtQuoter({
1242
1384
  * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
1243
1385
  * }),
1244
1386
  * },
@@ -1247,6 +1389,204 @@ interface SubgraphNativeUsdtQuoterConfig {
1247
1389
  */
1248
1390
  declare function createSubgraphNativeUsdtQuoter(config: SubgraphNativeUsdtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
1249
1391
 
1392
+ /**
1393
+ * Combined off-chain + on-chain balance for a single user / token pair.
1394
+ *
1395
+ * - `offChain` — the issuer's ledger balance (available, excluding locks)
1396
+ * - `onChain` — the user's ERC-20 balance from `PointToken.balanceOf`
1397
+ * - `total` — `offChain + onChain` (what the Issuer App displays)
1398
+ */
1399
+ interface CombinedBalance {
1400
+ offChain: bigint;
1401
+ onChain: bigint;
1402
+ total: bigint;
1403
+ }
1404
+ interface BalanceAggregatorConfig {
1405
+ provider: PublicClient;
1406
+ ledger: IPointLedger;
1407
+ }
1408
+ /**
1409
+ * v1.4 utility — aggregates off-chain + on-chain point balances into a
1410
+ * single view for the "combined balance" UI in the Issuer App.
1411
+ *
1412
+ * The `/user` API handler uses this internally; the helper is exposed
1413
+ * publicly so Issuer Apps can call it directly without going through
1414
+ * the HTTP layer (e.g., for server-rendered pages or admin dashboards).
1415
+ *
1416
+ * See [REQUIREMENTS_V2.md] §1 — "The Issuer App displays a combined
1417
+ * balance (off-chain points + on-chain PT) and does not surface USDT."
1418
+ */
1419
+ declare class BalanceAggregator {
1420
+ private readonly provider;
1421
+ private readonly ledger;
1422
+ constructor(config: BalanceAggregatorConfig);
1423
+ /**
1424
+ * Combined balance for a single (user, token) pair. Fetches off-chain
1425
+ * + on-chain in parallel.
1426
+ */
1427
+ getCombinedBalance(user: Address, pointToken: Address): Promise<CombinedBalance>;
1428
+ /**
1429
+ * Combined balance for multiple tokens owned by the same user. Runs
1430
+ * all lookups in parallel. Returns a Map keyed by the token address
1431
+ * (same casing as supplied — caller should normalize if needed).
1432
+ */
1433
+ getCombinedBalanceMulti(user: Address, pointTokens: Address[]): Promise<Map<Address, CombinedBalance>>;
1434
+ }
1435
+
1436
+ interface RetryConfig {
1437
+ /**
1438
+ * Max total attempts including the first try. Default: 1 (no retry).
1439
+ * Set to 3 to get 2 retries after the initial call.
1440
+ *
1441
+ * Only applies when the server error body carries `safeToRetry: true`
1442
+ * or the failure is a transient network/timeout error.
1443
+ */
1444
+ maxAttempts?: number;
1445
+ /**
1446
+ * Initial backoff delay in ms. Default: 500. Each subsequent retry
1447
+ * doubles this (exponential backoff) and adds ±20% jitter.
1448
+ */
1449
+ initialDelayMs?: number;
1450
+ /**
1451
+ * Hard ceiling for a single backoff (ms). Default: 10_000.
1452
+ */
1453
+ maxDelayMs?: number;
1454
+ /**
1455
+ * Upper bound on `retryAfter` from the server. If the server asks us
1456
+ * to wait longer than this (e.g. rate limit until UTC midnight), the
1457
+ * client gives up rather than blocking. Default: 30_000.
1458
+ */
1459
+ maxRetryAfterMs?: number;
1460
+ }
1461
+ interface PafiBackendConfig {
1462
+ /**
1463
+ * PAFI Backend API base URL. Example:
1464
+ * https://api.pacificfinance.org
1465
+ * https://staging-api.pacificfinance.org
1466
+ */
1467
+ url: string;
1468
+ /** PAFI-assigned issuer ID (e.g., "gg56"). Sent in X-Issuer-Id header. */
1469
+ issuerId: string;
1470
+ /** Per-issuer API key (or JWT) for the Authorization header. */
1471
+ apiKey: string;
1472
+ /** Optional fetch override for tests. */
1473
+ fetchImpl?: typeof fetch;
1474
+ /**
1475
+ * Timeout (ms) for each request. Default: 10_000. PAFI Backend should
1476
+ * respond in <1s for the happy path; this is just the sanity bound.
1477
+ */
1478
+ timeoutMs?: number;
1479
+ /**
1480
+ * Retry policy for transient failures (5xx, 429, timeouts, network).
1481
+ * Omit or pass `{ maxAttempts: 1 }` to disable retry entirely.
1482
+ */
1483
+ retry?: RetryConfig;
1484
+ }
1485
+ /** Paired with `POST /paymaster/sponsor`. See SPONSORED_PATH_FLOW.md §4.1 */
1486
+ interface SponsorshipRequest {
1487
+ chainId: number;
1488
+ scenario: SponsorshipScenario;
1489
+ userOp: PartialUserOperation;
1490
+ target: {
1491
+ /** The allowlisted contract this batch call targets. */
1492
+ contract: Address;
1493
+ /** Function selector / name — validated against allowlist. */
1494
+ function: string;
1495
+ /** The PointToken involved (for scenario context). */
1496
+ pointToken?: Address;
1497
+ };
1498
+ }
1499
+ interface SponsorshipResponse {
1500
+ paymaster: Address;
1501
+ paymasterData: Hex;
1502
+ paymasterVerificationGasLimit: bigint;
1503
+ paymasterPostOpGasLimit: bigint;
1504
+ /** Unix seconds when this sponsorship expires. Re-request after. */
1505
+ expiresAt: number;
1506
+ }
1507
+ /**
1508
+ * Machine-readable error codes returned by PAFI Backend.
1509
+ *
1510
+ * Source of truth: `apps/paymaster-proxy` `CalldataValidationError`,
1511
+ * `RateLimitError`, `CoinbaseClientError`. Keep in sync.
1512
+ */
1513
+ type PafiBackendErrorCode = "MISSING_ISSUER_ID" | "MISSING_API_KEY" | "ISSUER_UNAUTHORIZED" | "CALLDATA_INVALID" | "CALLDATA_EMPTY_BATCH" | "TARGET_NOT_ALLOWLISTED" | "FUNCTION_NOT_ALLOWED" | "SCENARIO_MISMATCH" | "SCENARIO_DISABLED" | "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_EXCEEDED_DAILY" | "RATE_LIMIT_EXCEEDED_PER_USER" | "RATE_LIMITER_UNAVAILABLE" | "PAYMASTER_REJECTED" | "PAYMASTER_UNAVAILABLE" | "PAYMASTER_TIMEOUT" | "BAD_REQUEST" | "INTERNAL_ERROR" | "TIMEOUT" | "NETWORK_ERROR";
1514
+ declare class PafiBackendError extends Error {
1515
+ code: PafiBackendErrorCode;
1516
+ httpStatus: number;
1517
+ details?: unknown | undefined;
1518
+ /**
1519
+ * Seconds to wait before retry. Populated from the server body
1520
+ * (e.g. rate limit returns the number of seconds until UTC midnight).
1521
+ */
1522
+ readonly retryAfter?: number;
1523
+ /**
1524
+ * `safeToRetry` as reported by the server body. Prefer this over the
1525
+ * code-based heuristic when available — the server knows more about
1526
+ * whether the same request will succeed on retry.
1527
+ */
1528
+ private readonly serverSafeToRetry?;
1529
+ constructor(code: PafiBackendErrorCode, message: string, httpStatus: number, details?: unknown | undefined, opts?: {
1530
+ retryAfter?: number;
1531
+ safeToRetry?: boolean;
1532
+ });
1533
+ /**
1534
+ * Whether the caller can safely retry the same request.
1535
+ *
1536
+ * If the server provided `safeToRetry` in the body, trust that.
1537
+ * Otherwise fall back to a code-based heuristic.
1538
+ */
1539
+ get safeToRetry(): boolean;
1540
+ }
1541
+
1542
+ /**
1543
+ * HTTP client for the PAFI Backend paymaster proxy service. See
1544
+ * [SPONSORED_PATH_FLOW.md] for the full flow + API contract.
1545
+ *
1546
+ * This client sits between `@pafi/issuer`'s RelayService and the
1547
+ * PAFI Backend. It does NOT talk to Coinbase Paymaster directly —
1548
+ * PAFI Backend holds that integration.
1549
+ */
1550
+ declare class PafiBackendClient {
1551
+ private readonly url;
1552
+ private readonly issuerId;
1553
+ private readonly apiKey;
1554
+ private readonly fetchImpl;
1555
+ private readonly timeoutMs;
1556
+ private readonly retry;
1557
+ constructor(config: PafiBackendConfig);
1558
+ /**
1559
+ * Request paymaster sponsorship for a pre-built UserOperation.
1560
+ * See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
1561
+ *
1562
+ * Retries automatically on transient failures (5xx, timeouts, network
1563
+ * errors, and errors the server flags with `safeToRetry: true`) up to
1564
+ * `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
1565
+ *
1566
+ * @throws PafiBackendError on final failure after exhausting retries
1567
+ */
1568
+ requestSponsorship(req: SponsorshipRequest): Promise<SponsorshipResponse>;
1569
+ private postWithRetry;
1570
+ /**
1571
+ * Pick the delay before the next retry.
1572
+ * - If the server sent `retryAfter` (seconds), honor it (capped by
1573
+ * `maxRetryAfterMs`) — returns null if the server wait exceeds the
1574
+ * cap, signalling the caller should give up.
1575
+ * - Otherwise: exponential backoff with ±20% jitter, capped at
1576
+ * `maxDelayMs`.
1577
+ */
1578
+ private computeBackoff;
1579
+ private sleep;
1580
+ private post;
1581
+ /** JSON replacer that stringifies bigints. Paired with bigintReviver. */
1582
+ private bigintReplacer;
1583
+ /**
1584
+ * JSON reviver that coerces specific numeric-string fields back to
1585
+ * bigint. The server must send these fields as decimal strings.
1586
+ */
1587
+ private bigintReviver;
1588
+ }
1589
+
1250
1590
  /**
1251
1591
  * Top-level configuration for `createIssuerService`. Everything except
1252
1592
  * the chain metadata, wallets, auth secret, and `signer` is optional and
@@ -1304,10 +1644,9 @@ interface IssuerServiceConfig {
1304
1644
  /**
1305
1645
  * Fee management config. If omitted the `handleGasFee` endpoint will
1306
1646
  * throw "not configured" at request time. Pass any subset of fields
1307
- * to opt in — provider + operatorWallet are inherited from the outer
1308
- * config automatically.
1647
+ * to opt in — `provider` is inherited from the outer config.
1309
1648
  */
1310
- fee?: Omit<FeeManagerConfig, "provider" | "operatorWallet">;
1649
+ fee?: Omit<FeeManagerConfig, "provider">;
1311
1650
  /**
1312
1651
  * Pool discovery function for `handlePools`. If omitted the endpoint
1313
1652
  * throws "not configured" at request time.
@@ -1377,4 +1716,4 @@ declare function createIssuerService(config: IssuerServiceConfig): IssuerService
1377
1716
  /** SDK package version — bumped on every release */
1378
1717
  declare const PAFI_ISSUER_SDK_VERSION = "0.1.0";
1379
1718
 
1380
- export { type ApiBuildConsentTypedDataRequest, type ApiBuildConsentTypedDataResponse, type ApiClaimAndSwapRequest, type ApiClaimAndSwapResponse, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, DefaultPolicyEngine, type DefaultPolicyEngineOptions, FeeManager, type FeeManagerConfig, type IIndexerCursorStore, type IIssuerSigner, type IPointLedger, type IPolicyEngine, type ISessionStore, InMemoryCursorStore, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerService, type IssuerServiceConfig, type LockedMintRequest, type LoginResult, MemoryPointLedger, MemorySessionStore, type MemorySessionStoreOptions, type MintAndCashOutRequest, type MintAndCashOutResponse, type MintEvent, MintingGateway, type MintingGatewayConfig, MintingGatewayError, type MintingGatewayErrorCode, type MintingStatus, NonceManager, type OperatorWalletLike, PAFI_ISSUER_SDK_VERSION, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, type PoolsProvider, PrivateKeySigner, type PrivateKeySignerOptions, RelayError, type RelayErrorCode, type RelayResult, RelayService, type RelayServiceConfig, type Session, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type SubmitMintAndSwapParams, authenticateRequest, createIssuerService, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider };
1719
+ export { type ApiBuildConsentTypedDataRequest, type ApiBuildConsentTypedDataResponse, type ApiClaimAndSwapRequest, type ApiClaimAndSwapResponse, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, BalanceAggregator, type BalanceAggregatorConfig, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type CombinedBalance, DefaultPolicyEngine, type DefaultPolicyEngineOptions, FeeManager, type FeeManagerConfig, type IIndexerCursorStore, type IIssuerSigner, type IPointLedger, type IPolicyEngine, type ISessionStore, InMemoryCursorStore, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerService, type IssuerServiceConfig, type LockedMintRequest, type LoginResult, MemoryPointLedger, MemorySessionStore, type MemorySessionStoreOptions, type MintAndCashOutRequest, type MintAndCashOutResponse, type MintEvent, MintingGateway, type MintingGatewayConfig, MintingGatewayError, type MintingGatewayErrorCode, type MintingStatus, NonceManager, type OperatorWalletLike, PAFI_ISSUER_SDK_VERSION, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, type PoolsProvider, PrivateKeySigner, type PrivateKeySignerOptions, RelayError, type RelayErrorCode, type RelayResult, RelayService, type RelayServiceConfig, type Session, type SponsorshipRequest, type SponsorshipResponse, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type SubmitMintAndSwapParams, authenticateRequest, createIssuerService, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider };