@pafi-dev/issuer 0.5.34 → 0.5.35

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, WalletClient } from 'viem';
2
- import { PointTokenDomainConfig, PartialUserOperation, BurnRequest, PoolKey, ENTRY_POINT_V08, UserOpTypedData } from '@pafi-dev/core';
2
+ import { PointTokenDomainConfig, PartialUserOperation, BurnRequest, PoolKey, UserOpTypedData, decodeBatchExecuteCalls, BROKER_HASHES, ENTRY_POINT_V08 } from '@pafi-dev/core';
3
3
  export { PAFI_SUBGRAPH_URL } from '@pafi-dev/core';
4
4
 
5
5
  /**
@@ -1528,745 +1528,1060 @@ declare function handleClaimStatus(params: MintStatusParams): Promise<MintStatus
1528
1528
  declare function handleRedeemStatus(params: BurnStatusParams): Promise<BurnStatusResponse>;
1529
1529
 
1530
1530
  /**
1531
- * Config for `createSubgraphPoolsProvider`.
1531
+ * A pending UserOp serialized for persistent storage (Redis, Postgres, memory).
1532
+ *
1533
+ * All bigint fields are stored as decimal strings so the entry can be
1534
+ * JSON-serialized without precision loss. Convert back to bigint before
1535
+ * calling `computeUserOpHash` or `serializeUserOpToJsonRpc`.
1532
1536
  */
1533
- interface SubgraphPoolsProviderConfig {
1534
- /**
1535
- * Fully qualified subgraph GraphQL endpoint.
1536
- * Defaults to the PAFI-hosted subgraph (`PAFI_SUBGRAPH_URL`).
1537
- * Override only when pointing at a staging or custom deployment.
1538
- */
1539
- subgraphUrl?: string;
1540
- /**
1541
- * Cache TTL in milliseconds. Pool discovery is near-static — a 30s
1542
- * cache removes subgraph load without meaningfully delaying UX.
1543
- * Set to 0 to disable caching. Default: 30_000.
1544
- */
1545
- cacheTtlMs?: number;
1546
- /**
1547
- * Optional fetch override for test harnesses. Defaults to global `fetch`.
1548
- */
1549
- fetchImpl?: typeof fetch;
1537
+ interface PendingUserOpEntry {
1538
+ sender: Address;
1539
+ nonce: string;
1540
+ callData: Hex;
1541
+ callGasLimit: string;
1542
+ verificationGasLimit: string;
1543
+ preVerificationGas: string;
1544
+ maxFeePerGas: string;
1545
+ maxPriorityFeePerGas: string;
1546
+ paymaster?: Address;
1547
+ paymasterVerificationGasLimit?: string;
1548
+ paymasterPostOpGasLimit?: string;
1549
+ paymasterData?: Hex;
1550
+ chainId: number;
1551
+ /** Hex-encoded userOpHash the value the user signed via personal_sign. */
1552
+ userOpHash: Hex;
1550
1553
  /**
1551
- * Optional clock override for tests.
1554
+ * Fee-stripped fallback variant. Set by `/claim/prepare` and
1555
+ * `/redeem/prepare` when a PT operator fee is configured AND the
1556
+ * paymaster sponsorship attached successfully — i.e. the user might
1557
+ * still want to submit without paymaster (paying ETH gas), and in
1558
+ * that case shouldn't be charged the PT fee. `/claim/submit` reads
1559
+ * this branch when its request body specifies
1560
+ * `variant: "fallback"`.
1561
+ *
1562
+ * Has a different `callData` (no PT.transfer prepended) and
1563
+ * therefore a different `userOpHash`. Paymaster fields are NOT
1564
+ * present — the fallback is by definition unsponsored.
1552
1565
  */
1553
- now?: () => number;
1566
+ fallback?: {
1567
+ callData: Hex;
1568
+ callGasLimit: string;
1569
+ verificationGasLimit: string;
1570
+ preVerificationGas: string;
1571
+ userOpHash: Hex;
1572
+ };
1554
1573
  }
1555
1574
  /**
1556
- * Create a `PoolsProvider` backed by the PAFI subgraph.
1557
- *
1558
- * Queries the `pafiTokens` entity for the given `pointTokenAddress`,
1559
- * reads its linked `Pool`, and returns a single-element `PoolKey[]`.
1560
- * Multiple pools per token would require a subgraph schema change.
1561
- *
1562
- * The result is cached in-process with a short TTL (default 30s). Pool
1563
- * discovery is near-static so this avoids hammering the subgraph without
1564
- * blocking config changes for long.
1565
- *
1566
- * Returns `{ pools: [] }` if:
1567
- * - the token is not registered on PAFI yet (no PafiToken entity)
1568
- * - the token is registered but its pool has not been initialised
1569
- * - the subgraph is unreachable or returns an error (logs to console,
1570
- * does not throw — callers should handle empty pool list gracefully)
1575
+ * Storage backend for pending UserOps in the mobile prepare/submit pattern.
1571
1576
  *
1572
- * Assumes the PAFI subgraph schema. Issuers with a custom subgraph must
1573
- * implement `PoolsProvider` themselves instead of using this helper.
1577
+ * Implement this interface and wire it into your issuer backend:
1578
+ * - `save()` called by `POST /claim/prepare` and `POST /redeem/prepare`
1579
+ * - `get()` — called by `POST /claim/submit` and `POST /redeem/submit`
1580
+ * - `delete()` — called after successful submit or explicit cancellation
1574
1581
  *
1575
- * @example
1576
- * ```ts
1577
- * import { createSubgraphPoolsProvider, createIssuerService } from "@pafi/issuer";
1582
+ * The default implementation in the gg56 boilerplate uses Redis with
1583
+ * a short TTL matching the MintRequest / BurnRequest deadline.
1584
+ */
1585
+ interface IPendingUserOpStore {
1586
+ save(lockId: string, entry: PendingUserOpEntry, ttlSeconds: number): Promise<void>;
1587
+ get(lockId: string): Promise<PendingUserOpEntry | null>;
1588
+ delete(lockId: string): Promise<void>;
1589
+ }
1590
+
1591
+ /**
1592
+ * Convert a stored `PendingUserOpEntry` (decimal-string fields) plus a
1593
+ * signature into the JSON-RPC wire format for `eth_sendUserOperation`.
1578
1594
  *
1579
- * const service = createIssuerService({
1580
- * // ...other config
1581
- * poolsProvider: createSubgraphPoolsProvider({
1582
- * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
1583
- * }),
1584
- * });
1585
- * ```
1595
+ * Bridges the gap between the serialized storage format (decimal strings,
1596
+ * safe for JSON/Redis) and `serializeUserOpToJsonRpc` which expects bigints.
1586
1597
  */
1587
- declare function createSubgraphPoolsProvider(config?: SubgraphPoolsProviderConfig): PoolsProvider;
1598
+ declare function serializeEntryToJsonRpc(entry: PendingUserOpEntry, signature: Hex, variant?: "sponsored" | "fallback"): Record<string, string | null>;
1588
1599
 
1589
1600
  /**
1590
- * Config for `createSubgraphNativeUsdtQuoter`.
1601
+ * Re-shape `UserOpTypedData` so all `bigint` fields become hex strings —
1602
+ * required for JSON transport over HTTP. Mirrors the inverse of what
1603
+ * `walletClient.signTypedData` accepts on the client (it auto-coerces hex
1604
+ * strings back to bigints for `uint256` fields).
1591
1605
  */
1592
- interface SubgraphNativeUsdtQuoterConfig {
1593
- /**
1594
- * Fully qualified subgraph GraphQL endpoint.
1595
- * Defaults to the PAFI-hosted subgraph (`PAFI_SUBGRAPH_URL`).
1596
- * Override only when pointing at a staging or custom deployment.
1597
- */
1598
- subgraphUrl?: string;
1599
- /**
1600
- * Decimals of the USDT token. Defaults to 6 (standard USDT/USDC on
1601
- * Base, Ethereum, Polygon). Override for chains where USDT uses a
1602
- * different decimals value.
1603
- */
1604
- usdtDecimals?: number;
1605
- /**
1606
- * Decimals of the native token (ETH on Base/mainnet/Arbitrum/Optimism,
1607
- * MATIC on Polygon). Default: 18.
1608
- */
1609
- nativeDecimals?: number;
1610
- /**
1611
- * Cache TTL in milliseconds. ETH price drifts slowly relative to gas
1612
- * estimation needs — a 30s cache keeps fees stable across bursts of
1613
- * requests without letting them go stale during volatile markets.
1614
- * Set to 0 to disable caching. Default: 30_000.
1615
- */
1616
- cacheTtlMs?: number;
1617
- /**
1618
- * Fallback price (USDT per native token, human-readable float) used
1619
- * when the subgraph is unreachable. This keeps the backend operational
1620
- * during subgraph outages rather than bricking cashouts. The fee will
1621
- * be slightly off but the operator still gets paid. Default: 3000.
1622
- */
1623
- fallbackEthPriceUsd?: number;
1624
- /** Optional fetch override for test harnesses. */
1625
- fetchImpl?: typeof fetch;
1626
- /** Optional clock override for tests. */
1627
- now?: () => number;
1628
- }
1606
+ type SerializedUserOpTypedData = {
1607
+ domain: UserOpTypedData["domain"];
1608
+ types: UserOpTypedData["types"];
1609
+ primaryType: UserOpTypedData["primaryType"];
1610
+ message: {
1611
+ sender: Address;
1612
+ nonce: Hex;
1613
+ initCode: Hex;
1614
+ callData: Hex;
1615
+ accountGasLimits: Hex;
1616
+ preVerificationGas: Hex;
1617
+ gasFees: Hex;
1618
+ paymasterAndData: Hex;
1619
+ };
1620
+ };
1629
1621
  /**
1630
- * Create a native→USDT quoter backed by the PAFI subgraph's
1631
- * `Bundle.ethPriceUSD`. The returned function has the shape
1632
- * `(amountNative: bigint) => Promise<bigint>` and can be passed as
1633
- * `quoteNativeToFee` to `FeeManager` — in v1.4 the fee currency
1634
- * is configured at the integration layer, not hardcoded here.
1635
- *
1636
- * Used by `FeeManager.estimateGasFee()` to convert the gas cost into
1637
- * an ERC-20 amount charged as part of the sponsored UserOp batch.
1638
- * Price precision is not critical — a 1-2% drift is acceptable since
1639
- * the fee manager applies a `gasPremiumBps` buffer.
1622
+ * Convert a `UserOpTypedData` payload into the JSON-safe wire form
1623
+ * (bigint → hex string). The mobile client passes this directly to
1624
+ * `eth_signTypedData_v4` / viem's `signTypedData`.
1625
+ */
1626
+ declare function serializeUserOpTypedData(td: UserOpTypedData): SerializedUserOpTypedData;
1627
+ /**
1628
+ * Merge Pimlico's paymaster-sponsorship response into a UserOp
1629
+ * skeleton, applying only fields that are actually defined.
1640
1630
  *
1641
- * The result is cached in-process with a short TTL (default 30s). If
1642
- * the subgraph is unreachable, falls back to `fallbackEthPriceUsd` so
1643
- * gas estimation doesn't block user flow during a subgraph outage.
1631
+ * `pm_sponsorUserOperation` returns:
1632
+ * - `paymaster` / `paymasterData` required for the sponsored sig
1633
+ * - `paymasterVerificationGasLimit` / `paymasterPostOpGasLimit`
1634
+ * - **re-estimated** `callGasLimit` / `verificationGasLimit` /
1635
+ * `preVerificationGas` — the paymaster signature is computed over
1636
+ * these new values
1637
+ * - **bundler-required** `maxFeePerGas` / `maxPriorityFeePerGas` —
1638
+ * Base RPC's `eth_feeHistory` underestimates, bundler rejects
1644
1639
  *
1645
- * @example
1646
- * ```ts
1647
- * import { createSubgraphNativeUsdtQuoter, createIssuerService } from "@pafi-dev/issuer";
1640
+ * Callers MUST re-merge ALL of these into the userOp BEFORE computing
1641
+ * the EIP-712 userOpHash — otherwise both the paymaster signature
1642
+ * (AA34) and the user signature (AA24, recovered against a different
1643
+ * hash) fail at validation.
1648
1644
  *
1649
- * const service = createIssuerService({
1650
- * // ...other config
1651
- * fee: {
1652
- * quoteNativeToFee: createSubgraphNativeUsdtQuoter({
1653
- * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
1654
- * }),
1655
- * },
1656
- * });
1657
- * ```
1645
+ * Skips fields that are undefined so legacy paymaster responses
1646
+ * (without re-estimated gas) don't accidentally zero out the original
1647
+ * estimates.
1658
1648
  */
1659
- declare function createSubgraphNativeUsdtQuoter(config?: SubgraphNativeUsdtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
1660
-
1661
- interface NativePtQuoterConfig {
1662
- /** Viem PublicClient — used to call Chainlink on-chain. */
1663
- provider: PublicClient;
1664
- /** Address of the PointToken being traded. */
1665
- pointTokenAddress: Address;
1666
- /** Chainlink ETH/USD feed address. Defaults to Base mainnet feed. */
1667
- chainlinkFeedAddress?: Address;
1668
- /** PAFI subgraph GraphQL endpoint. */
1669
- subgraphUrl?: string;
1670
- /** Cache TTL in ms. Default: 30_000. */
1671
- cacheTtlMs?: number;
1672
- /** Fallback ETH price (USD) when Chainlink is unreachable. Default: 3000. */
1673
- fallbackEthPriceUsd?: number;
1674
- /** Fallback PT price (USDT per 1 PT) when subgraph is unreachable. Default: 0.1. */
1675
- fallbackPtPriceUsdt?: number;
1676
- fetchImpl?: typeof fetch;
1677
- now?: () => number;
1649
+ declare function mergePaymasterFields<T extends object>(userOp: T, paymasterFields: {
1650
+ paymaster: Address;
1651
+ paymasterData: Hex;
1652
+ paymasterVerificationGasLimit: bigint;
1653
+ paymasterPostOpGasLimit: bigint;
1654
+ callGasLimit?: bigint;
1655
+ verificationGasLimit?: bigint;
1656
+ preVerificationGas?: bigint;
1657
+ maxFeePerGas?: bigint;
1658
+ maxPriorityFeePerGas?: bigint;
1659
+ } | undefined): T;
1660
+ interface PreparedUserOp {
1661
+ /** The bundler-ready UserOp (with paymaster + Pimlico-quoted gas). */
1662
+ userOp: PartialUserOperation & {
1663
+ maxFeePerGas: bigint;
1664
+ maxPriorityFeePerGas: bigint;
1665
+ paymaster?: Address;
1666
+ paymasterData?: Hex;
1667
+ paymasterVerificationGasLimit?: bigint;
1668
+ paymasterPostOpGasLimit?: bigint;
1669
+ };
1670
+ /** Hex-encoded EIP-712 digest. Equals `EntryPoint.getUserOpHash`. */
1671
+ userOpHash: Hex;
1672
+ /** Typed-data payload — pass directly to `eth_signTypedData_v4`. */
1673
+ typedData: SerializedUserOpTypedData;
1678
1674
  }
1679
- /**
1680
- * Create a native→PT quoter for use as `FeeManager.quoteNativeToFee`.
1681
- *
1682
- * Converts ETH gas cost → USDT (via Chainlink ETH/USD) → PT (via subgraph
1683
- * pool price), returning the fee amount in PT raw units (18 decimals).
1684
- *
1685
- * Formula:
1686
- * feeInPT = amountNative × ethPrice_8dec × ptPerUsdt_18dec / 10^26
1675
+ interface PrepareMobileUserOpParams {
1676
+ /** Lock id (issuer-generated) keying both store entry + ledger row. */
1677
+ lockId: string;
1678
+ /**
1679
+ * Sponsored variant built with the PT operator-fee transfer
1680
+ * included. SDK or caller should set `partialUserOp.maxFeePerGas` /
1681
+ * `maxPriorityFeePerGas` from `provider.estimateFeesPerGas()` before
1682
+ * calling they get overridden by Pimlico's quote anyway, but
1683
+ * they must be valid bigints for the merge.
1684
+ */
1685
+ partialUserOp: PartialUserOperation & {
1686
+ maxFeePerGas: bigint;
1687
+ maxPriorityFeePerGas: bigint;
1688
+ };
1689
+ /**
1690
+ * Optional fee-stripped fallback variant — built with `gasFeePt: 0n`
1691
+ * (no PT operator-fee transfer). Submitted when paymaster refuses
1692
+ * sponsorship and the user pays ETH gas directly.
1693
+ */
1694
+ partialUserOpFallback?: PartialUserOperation & {
1695
+ maxFeePerGas?: bigint;
1696
+ maxPriorityFeePerGas?: bigint;
1697
+ };
1698
+ /** Paymaster sponsorship response, or `undefined` if PAFI declined. */
1699
+ paymasterFields?: {
1700
+ paymaster: Address;
1701
+ paymasterData: Hex;
1702
+ paymasterVerificationGasLimit: bigint;
1703
+ paymasterPostOpGasLimit: bigint;
1704
+ callGasLimit?: bigint;
1705
+ verificationGasLimit?: bigint;
1706
+ preVerificationGas?: bigint;
1707
+ maxFeePerGas?: bigint;
1708
+ maxPriorityFeePerGas?: bigint;
1709
+ };
1710
+ chainId: number;
1711
+ /** Pending-userop store implementation (Redis/Postgres/Memory). */
1712
+ store: IPendingUserOpStore;
1713
+ /** TTL the store entry should outlive — typically the MintRequest deadline. */
1714
+ ttlSeconds: number;
1715
+ }
1716
+ interface PrepareMobileUserOpResult {
1717
+ sponsored: PreparedUserOp;
1718
+ /**
1719
+ * Set when `partialUserOpFallback` was supplied AND the PT fee was
1720
+ * non-zero (i.e. sponsored ≠ fallback). Mobile client picks which
1721
+ * variant to sign + submit; SDK's `serializeEntryToJsonRpc` reads
1722
+ * the `variant` flag to dispatch.
1723
+ */
1724
+ fallback?: PreparedUserOp;
1725
+ /** What got persisted into the pending-userop store. */
1726
+ entry: PendingUserOpEntry;
1727
+ }
1728
+ /**
1729
+ * Build the sponsored UserOp + (optional) fee-stripped fallback for the
1730
+ * mobile prepare/submit flow.
1687
1731
  *
1688
- * Both prices are cached in-process (default 30s TTL).
1732
+ * What this does, end-to-end:
1733
+ * 1. Merge Pimlico's paymaster sponsorship + re-quoted gas into the
1734
+ * caller's `partialUserOp` skeleton.
1735
+ * 2. Compute the EIP-712 userOpHash + the typed-data payload (in
1736
+ * JSON-safe form for HTTP transport).
1737
+ * 3. (Optional) Repeat for the `partialUserOpFallback` skeleton with
1738
+ * no paymaster fields — produces a separate hash + typed-data so
1739
+ * the client can re-sign if it falls back.
1740
+ * 4. Persist a single store entry containing BOTH callData variants
1741
+ * keyed by lockId. `serializeEntryToJsonRpc` reads the `variant`
1742
+ * param at submit time.
1689
1743
  *
1690
- * @example
1691
- * ```ts
1692
- * fee: {
1693
- * quoteNativeToFee: createNativePtQuoter({
1694
- * provider,
1695
- * pointTokenAddress: "0x...",
1696
- * chainlinkFeedAddress: getContractAddresses(chainId).chainlinkEthUsd,
1697
- * }),
1698
- * }
1699
- * ```
1744
+ * Replaces ~100 LoC of glue per scenario in issuer controllers.
1700
1745
  */
1701
- declare function createNativePtQuoter(config: NativePtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
1746
+ declare function prepareMobileUserOp(params: PrepareMobileUserOpParams): Promise<PrepareMobileUserOpResult>;
1702
1747
 
1703
1748
  /**
1704
- * Combined off-chain + on-chain balance for a single user / token pair.
1749
+ * Mobile prepare/submit orchestrators abstract the duplicate glue
1750
+ * between `/claim/prepare`+`/claim/submit` and `/redeem/prepare`+
1751
+ * `/redeem/submit`. Both share the same shape:
1705
1752
  *
1706
- * - `offChain` the issuer's ledger balance (available, excluding locks)
1707
- * - `onChain` — the user's ERC-20 balance from `PointToken.balanceOf`
1708
- * - `total` — `offChain + onChain` (what the Issuer App displays)
1753
+ * prepare: fees + delegation check paymaster prepareMobileUserOp
1754
+ * submit: fetch entry serialize+sign relay bind hash → delete
1755
+ *
1756
+ * Issuer controllers shrink to ~30 LoC each — wire the handler that
1757
+ * produces `partialUserOp[+ fallback]`, hand off to these.
1709
1758
  */
1710
- interface CombinedBalance {
1711
- offChain: bigint;
1712
- onChain: bigint;
1713
- total: bigint;
1759
+ declare class PendingUserOpNotFoundError extends Error {
1760
+ readonly code = "PENDING_USEROP_NOT_FOUND";
1761
+ constructor(lockId: string);
1714
1762
  }
1715
- interface BalanceAggregatorConfig {
1763
+ interface HandleMobilePrepareParams {
1764
+ /** User EOA — used for the delegation check. */
1765
+ userAddress: Address;
1766
+ chainId: number;
1767
+ /** Lock id (issuer-generated) keying both store entry + ledger row. */
1768
+ lockId: string;
1769
+ /**
1770
+ * Partial UserOp from the upstream handler (PTClaimHandler /
1771
+ * PTRedeemHandler). The orchestrator will top up `maxFeePerGas` /
1772
+ * `maxPriorityFeePerGas` from `provider.estimateFeesPerGas()` if the
1773
+ * fields aren't already set, then request paymaster sponsorship.
1774
+ */
1775
+ partialUserOp: PartialUserOperation;
1776
+ /** Optional fee-stripped fallback variant. */
1777
+ partialUserOpFallback?: PartialUserOperation;
1778
+ /**
1779
+ * Scenario tag — passed to `requestSponsorship` so the relayer can
1780
+ * apply per-scenario L1 enforcement (`mint`, `burn`, etc.).
1781
+ */
1782
+ scenario: string;
1783
+ /** Target contract for the paymaster intent. */
1784
+ pointTokenAddress: Address;
1785
+ /** TTL the store entry should outlive. Typically the lock duration in seconds. */
1786
+ ttlSeconds: number;
1787
+ store: IPendingUserOpStore;
1716
1788
  provider: PublicClient;
1717
- ledger: IPointLedger;
1789
+ /** Optional — when omitted, paymaster is skipped and `sponsored: false` is returned. */
1790
+ pafiBackendClient?: PafiBackendClient | null;
1791
+ onWarning?: (msg: string) => void;
1792
+ }
1793
+ interface HandleMobilePrepareResult extends PrepareMobileUserOpResult {
1794
+ /**
1795
+ * True when paymaster sponsorship was applied to the sponsored variant.
1796
+ * (Renamed from `sponsored` to avoid clashing with
1797
+ * `PrepareMobileUserOpResult.sponsored` which is the PreparedUserOp object.)
1798
+ */
1799
+ isSponsored: boolean;
1800
+ /**
1801
+ * True when the user's EOA has no EIP-7702 delegation set on-chain.
1802
+ * The mobile client must run the `/delegate/*` flow first.
1803
+ */
1804
+ needsDelegation: boolean;
1718
1805
  }
1719
1806
  /**
1720
- * v1.4 utility aggregates off-chain + on-chain point balances into a
1721
- * single view for the "combined balance" UI in the Issuer App.
1807
+ * Build the mobile prepare response. End-to-end:
1808
+ *
1809
+ * 1. Pull `estimateFeesPerGas` + `getCode(user)` in parallel.
1810
+ * 2. Top up sponsored UserOp fees if the upstream handler left them
1811
+ * unset.
1812
+ * 3. `requestPaymaster` — non-fatal: if PAFI declines, the sponsored
1813
+ * variant still works, the FE just falls back to the unsponsored
1814
+ * response.
1815
+ * 4. `prepareMobileUserOp` — merge + hash + persist.
1816
+ */
1817
+ declare function handleMobilePrepare(params: HandleMobilePrepareParams): Promise<HandleMobilePrepareResult>;
1818
+ interface HandleMobileSubmitParams {
1819
+ lockId: string;
1820
+ /** User signature over `userOpHash` (or `userOpHashFallback`). */
1821
+ signature: Hex;
1822
+ /** Which variant the user actually signed. Defaults to `'sponsored'`. */
1823
+ variant?: "sponsored" | "fallback";
1824
+ store: IPendingUserOpStore;
1825
+ /**
1826
+ * Bind the bundler-returned hash to the lock so `claim/redeem status`
1827
+ * can fall back to the bundler receipt when the indexer's
1828
+ * amount-based match races and resolves a sibling. Different ledgers
1829
+ * use different methods (`bindMintUserOpHash` vs `bindCreditUserOpHash`),
1830
+ * so the caller passes the correct one.
1831
+ */
1832
+ bindUserOpHash: (lockId: string, userOpHash: Hex) => Promise<void>;
1833
+ pafiBackendClient?: PafiBackendClient | null;
1834
+ /** Defaults to `ENTRY_POINT_V08`. */
1835
+ entryPoint?: string;
1836
+ }
1837
+ /**
1838
+ * Submit a previously-prepared mobile UserOp to the bundler.
1722
1839
  *
1723
- * The `/user` API handler uses this internally; the helper is exposed
1724
- * publicly so Issuer Apps can call it directly without going through
1725
- * the HTTP layer (e.g., for server-rendered pages or admin dashboards).
1840
+ * Throws:
1841
+ * - `PendingUserOpNotFoundError` entry expired or already submitted.
1842
+ * Map to 404.
1843
+ * - `BundlerNotConfiguredError` — `pafiBackendClient` missing. Map to 503.
1844
+ * - `BundlerRejectedError` — bundler rejected the UserOp. Map to 422.
1845
+ */
1846
+ declare function handleMobileSubmit(params: HandleMobileSubmitParams): Promise<{
1847
+ userOpHash: Hex;
1848
+ }>;
1849
+
1850
+ interface IssuerRegistryRecord {
1851
+ issuerAddress: Address;
1852
+ signerAddress: Address;
1853
+ name: string;
1854
+ symbol: string;
1855
+ declaredTotalSupply: bigint;
1856
+ capBasisPoints: number;
1857
+ active: boolean;
1858
+ pointToken: Address;
1859
+ mintingOracle: Address;
1860
+ }
1861
+ interface PreValidateMintResult {
1862
+ /** Registry record read at pre-validation time. */
1863
+ issuer: IssuerRegistryRecord;
1864
+ /** Current on-chain PointToken.totalSupply(). */
1865
+ totalSupply: bigint;
1866
+ /** declaredTotalSupply × capBasisPoints / 10000. */
1867
+ hardCap: bigint;
1868
+ /** hardCap − totalSupply (clamped to 0). */
1869
+ remaining: bigint;
1870
+ }
1871
+ /**
1872
+ * Thrown by `IssuerStateValidator.preValidateMint()`.
1873
+ * `code` maps 1:1 to the HTTP error the issuer API surfaces to clients.
1874
+ */
1875
+ declare class IssuerStateError extends Error {
1876
+ readonly code: "ISSUER_NOT_REGISTERED" | "ISSUER_INACTIVE" | "MINT_CAP_EXCEEDED";
1877
+ readonly details?: Record<string, unknown> | undefined;
1878
+ constructor(code: "ISSUER_NOT_REGISTERED" | "ISSUER_INACTIVE" | "MINT_CAP_EXCEEDED", message: string, details?: Record<string, unknown> | undefined);
1879
+ }
1880
+
1881
+ /**
1882
+ * Pure (framework-agnostic) validator for issuer state.
1726
1883
  *
1727
- * See [REQUIREMENTS_V2.md] §1 "The Issuer App displays a combined
1728
- * balance (off-chain points + on-chain PT) and does not surface USDT."
1884
+ * Reads IssuerRegistry + PointToken on-chain state and pre-validates
1885
+ * mint requests before the user submits a UserOp. Catching these
1886
+ * off-chain lets issuers fail fast with a clear error rather than
1887
+ * wasting gas on a revert.
1888
+ *
1889
+ * Caching:
1890
+ * - `PointToken.issuer()` — memoized for the process lifetime (immutable)
1891
+ * - Full state (registry + totalSupply) — 30s TTL per PointToken
1892
+ * - Burst calls while a fetch is in-flight share the same Promise
1893
+ * (thundering-herd protection)
1894
+ *
1895
+ * Usage in NestJS: wrap this in an `@Injectable()` service; pass
1896
+ * `PublicClient` and `registryAddress` from your DI container.
1729
1897
  */
1730
- declare class BalanceAggregator {
1898
+ declare class IssuerStateValidator {
1731
1899
  private readonly provider;
1732
- private readonly ledger;
1733
- constructor(config: BalanceAggregatorConfig);
1900
+ private readonly registryAddress;
1901
+ private readonly pointTokenIssuerCache;
1902
+ private readonly stateCache;
1903
+ private readonly inflight;
1904
+ constructor(provider: PublicClient, registryAddress: Address);
1734
1905
  /**
1735
- * Combined balance for a single (user, token) pair. Fetches off-chain
1736
- * + on-chain in parallel.
1906
+ * Convenience factory reads `registryAddress` from the SDK
1907
+ * `CONTRACT_ADDRESSES` map for the given chain.
1737
1908
  */
1738
- getCombinedBalance(user: Address, pointToken: Address): Promise<CombinedBalance>;
1909
+ static forChain(provider: PublicClient, chainId: number): IssuerStateValidator;
1739
1910
  /**
1740
- * Combined balance for multiple tokens owned by the same user. Runs
1741
- * all lookups in parallel. Returns a Map keyed by the token address
1742
- * (same casing as supplied — caller should normalize if needed).
1911
+ * Invalidate cached state for one PointToken, or everything if omitted.
1912
+ * Call after admin txs that change registry or cap settings.
1743
1913
  */
1744
- getCombinedBalanceMulti(user: Address, pointTokens: Address[]): Promise<Map<Address, CombinedBalance>>;
1914
+ invalidate(pointToken?: Address): void;
1915
+ /**
1916
+ * Resolve `PointToken.issuer()` once per token and memoize.
1917
+ * The issuer field is set at `initialize()` and never changes.
1918
+ */
1919
+ getIssuerAddressForPointToken(pointToken: Address): Promise<Address>;
1920
+ /**
1921
+ * Read registry record + totalSupply, with 30s cache and in-flight
1922
+ * deduplication. Does NOT throw on inactive/missing — returns raw state.
1923
+ */
1924
+ getIssuerState(pointToken: Address): Promise<PreValidateMintResult>;
1925
+ /**
1926
+ * Validate that `amount` PT can be minted on `pointToken` right now.
1927
+ *
1928
+ * Throws `IssuerStateError` with:
1929
+ * - `ISSUER_NOT_REGISTERED` — registry has no record for this issuer
1930
+ * - `ISSUER_INACTIVE` — issuer.active is false
1931
+ * - `MINT_CAP_EXCEEDED` — totalSupply + amount would exceed hardCap
1932
+ *
1933
+ * Returns the fetched state on success so callers can log without a
1934
+ * second RPC round-trip.
1935
+ */
1936
+ preValidateMint(pointToken: Address, amount: bigint): Promise<PreValidateMintResult>;
1937
+ private fetchIssuerState;
1745
1938
  }
1746
1939
 
1940
+ type DecodedCall$2 = ReturnType<typeof decodeBatchExecuteCalls>[number];
1941
+
1747
1942
  /**
1748
- * Typed errors thrown by the helpers below issuer controllers map
1749
- * these to the appropriate HTTP status. We don't depend on @nestjs/common
1750
- * here because the SDK is framework-agnostic; the consuming controller
1751
- * wraps each one in its preferred exception class.
1943
+ * v1.4 sig-gated mint handler mirrors `PTRedeemHandler` on the mint side.
1944
+ *
1945
+ * Pre-validates against IssuerRegistry + on-chain totalSupply, locks the
1946
+ * off-chain balance, builds the sponsored UserOp (mint + PT fee
1947
+ * transfer) plus an optional fallback variant (mint only — for
1948
+ * paymaster-refused fallback).
1949
+ *
1950
+ * Caller fetches AA + mintRequest nonces (so issuers can plug in their
1951
+ * own composer — gg56 uses a timestamp-key 2D nonce). Caller layers
1952
+ * paymaster sponsorship + sponsorAuth on top of the returned UserOps.
1752
1953
  */
1753
- declare class BundlerNotConfiguredError extends Error {
1754
- readonly code = "BUNDLER_NOT_CONFIGURED";
1755
- constructor();
1954
+ declare class PTClaimError extends Error {
1955
+ code: "INVALID_AMOUNT" | "VALIDATION_FAILED" | "BUILD_FAILED";
1956
+ details?: Record<string, unknown> | undefined;
1957
+ constructor(code: "INVALID_AMOUNT" | "VALIDATION_FAILED" | "BUILD_FAILED", message: string, details?: Record<string, unknown> | undefined);
1756
1958
  }
1757
- declare class BundlerRejectedError extends Error {
1758
- readonly code = "BUNDLER_REJECTED";
1759
- readonly cause: unknown;
1760
- constructor(message: string, cause: unknown);
1959
+ interface PTClaimHandlerConfig {
1960
+ ledger: IPointLedger;
1961
+ relayService: RelayService;
1962
+ provider: PublicClient;
1963
+ /** Issuer minter signer wallet — passed through to RelayService.prepareMint. */
1964
+ issuerSignerWallet: WalletClient;
1965
+ /**
1966
+ * EIP-712 domain `name` for `MintRequest`. Typically the PointToken
1967
+ * ERC-20 name. RelayService will set chainId + verifyingContract from
1968
+ * the request.
1969
+ */
1970
+ pointTokenDomainName: string;
1971
+ /** Optional — when wired, used to estimate the PT gas-reimbursement fee. */
1972
+ feeService?: FeeManager;
1973
+ /** Optional — pre-validates issuer status + cap before locking balance. */
1974
+ issuerStateValidator?: IssuerStateValidator;
1975
+ /** How long the off-chain balance lock survives if the mint never lands. Default 15 min. */
1976
+ lockDurationMs?: number;
1977
+ /** How far ahead of `now` to set the MintRequest deadline. Default 15 min. */
1978
+ signatureDeadlineSeconds?: number;
1979
+ now?: () => number;
1761
1980
  }
1762
- interface RequestPaymasterParams {
1763
- /** PAFI backend client. When `null` / `undefined` → returns `undefined`. */
1764
- client: PafiBackendClient | null | undefined;
1765
- chainId: number;
1766
- scenario: string;
1767
- /** UserOp skeleton — must have all gas + fee fields set. */
1768
- userOp: {
1769
- sender: Address;
1770
- nonce: bigint;
1771
- callData: Hex;
1772
- callGasLimit: bigint;
1773
- verificationGasLimit: bigint;
1774
- preVerificationGas: bigint;
1775
- maxFeePerGas: bigint;
1776
- maxPriorityFeePerGas: bigint;
1777
- };
1778
- /** Target contract (typically the PointToken). */
1981
+ interface PTClaimRequest {
1982
+ authenticatedAddress: Address;
1983
+ userAddress: Address;
1984
+ amount: bigint;
1779
1985
  pointTokenAddress: Address;
1986
+ chainId: number;
1987
+ /** ERC-4337 account nonce for the user's EOA. */
1988
+ aaNonce: bigint;
1989
+ /** Current `mintRequestNonces[userAddress]` from PointToken. */
1990
+ mintRequestNonce: bigint;
1991
+ }
1992
+ interface PTClaimResponse {
1993
+ /** Sponsored UserOp — mint + PT fee transfer (when feeAmount > 0). */
1994
+ userOp: PartialUserOperation;
1780
1995
  /**
1781
- * Function name to surface in audit / paymaster context. Defaults to
1782
- * a per-scenario sensible value (`mint` / `burn` / `swap` / generic
1783
- * scenario name).
1996
+ * Fallback UserOp mint only, no PT fee transfer. Present only when
1997
+ * `feeAmount > 0`. User pays gas in ETH directly.
1784
1998
  */
1785
- functionName?: string;
1786
- /** Optional logger for the "sponsorship declined" warning. */
1787
- onWarning?: (msg: string) => void;
1999
+ fallback?: PartialUserOperation;
2000
+ lockId: string;
2001
+ feeAmount: bigint;
2002
+ signatureDeadline: bigint;
2003
+ expiresInSeconds: number;
2004
+ /** Decoded calls for the sponsored UserOp (convenience for FE-submit path). */
2005
+ calls: DecodedCall$2[];
2006
+ /** Decoded calls for the fallback UserOp (when present). */
2007
+ callsFallback?: DecodedCall$2[];
1788
2008
  }
1789
- /**
1790
- * Thin wrapper around `PafiBackendClient.requestSponsorship` with the
1791
- * "non-fatal on failure" semantics every issuer wants:
1792
- *
1793
- * - When the client is missing → returns `undefined` (the caller falls
1794
- * back to a self-funded UserOp).
1795
- * - When the network call throws OR PAFI declines (rate limit, intent
1796
- * rejection, paymaster outage) → returns `undefined` after logging,
1797
- * so the controller doesn't hard-fail. The caller's
1798
- * `prepareMobileUserOp` / `mergePaymasterFields` will gracefully
1799
- * produce the unsponsored variant.
1800
- *
1801
- * Replaces ~30 LoC of try/catch + scenario-to-function mapping every
1802
- * issuer would copy.
1803
- */
1804
- declare function requestPaymaster(params: RequestPaymasterParams): Promise<Awaited<ReturnType<PafiBackendClient["requestSponsorship"]>> | undefined>;
1805
- interface RelayUserOpParams {
1806
- client: PafiBackendClient | null | undefined;
1807
- /** EntryPoint address — typically `ENTRY_POINT_V08` from core. */
1808
- entryPoint: typeof ENTRY_POINT_V08 | string;
1809
- userOp: Record<string, string | null>;
1810
- /** EIP-7702 authorization (delegation UserOps only). */
1811
- eip7702Auth?: {
1812
- chainId: string;
1813
- address: string;
1814
- nonce: string;
1815
- r: string;
1816
- s: string;
1817
- yParity: string;
1818
- };
2009
+ declare class PTClaimHandler {
2010
+ private readonly cfg;
2011
+ constructor(config: PTClaimHandlerConfig);
2012
+ handle(request: PTClaimRequest): Promise<PTClaimResponse>;
1819
2013
  }
2014
+
2015
+ type DecodedCall$1 = ReturnType<typeof decodeBatchExecuteCalls>[number];
2016
+
1820
2017
  /**
1821
- * Submit a serialized UserOp to the Pimlico bundler via PAFI's
1822
- * sponsor-relayer. Handles the "client missing" / "bundler rejected"
1823
- * branches as typed errors so the controller can map to HTTP cleanly.
2018
+ * PT USDT swap handler.
1824
2019
  *
1825
- * Every issuer mobile flow has this exact wrapper — moved into SDK
1826
- * to drop ~30 LoC of try/catch + error-shape boilerplate per
1827
- * controller.
2020
+ * Quotes via V4 on-chain Quoter (`findBestQuote`), applies slippage,
2021
+ * computes the PT gas-reimbursement fee from `FeeManager`, and builds
2022
+ * two UserOps:
1828
2023
  *
1829
- * Throws:
1830
- * - `BundlerNotConfiguredError` — caller didn't configure
1831
- * `PafiBackendClient`. Map to 503.
1832
- * - `BundlerRejectedError` bundler returned an error. Map to 422
1833
- * (the FE can show the reason — usually `AA21` / `AA34` / etc.).
2024
+ * - **sponsored** — swap (amountIn − fee) + transfer(fee, PAFI). User
2025
+ * holds exactly `amountIn` PT.
2026
+ * - **fallback** — swap full `amountIn`, no PT fee transfer. Built
2027
+ * only when `feeAmount > 0`. User pays gas in ETH directly.
2028
+ *
2029
+ * Re-quotes the sponsored path on `(amountIn − feeAmount)` because the
2030
+ * sponsored path actually swaps that much; the fallback uses the
2031
+ * original quote.
2032
+ *
2033
+ * Caller (controller) layers `sponsorAuth` on top of the returned
2034
+ * userOp. No off-chain ledger state is touched — swap is purely
2035
+ * on-chain.
1834
2036
  */
1835
- declare function relayUserOp(params: RelayUserOpParams): Promise<{
1836
- userOpHash: Hex;
1837
- }>;
2037
+ declare class SwapError extends Error {
2038
+ code: "QUOTE_UNAVAILABLE" | "FEE_EXCEEDS_AMOUNT" | "INVALID_AMOUNT";
2039
+ constructor(code: "QUOTE_UNAVAILABLE" | "FEE_EXCEEDS_AMOUNT" | "INVALID_AMOUNT", message: string);
2040
+ }
2041
+ interface SwapHandlerConfig {
2042
+ provider: PublicClient;
2043
+ poolsProvider: PoolsProvider;
2044
+ /** Optional — when wired, used to estimate the PT gas-reimbursement fee. */
2045
+ feeService?: FeeManager;
2046
+ /**
2047
+ * Default slippage applied to the V4 quote when the request omits
2048
+ * `slippageBps`. 50 bps = 0.5%.
2049
+ */
2050
+ defaultSlippageBps?: number;
2051
+ /** Default deadline window in seconds applied when not passed in request. Default 300. */
2052
+ defaultSwapDeadlineSeconds?: number;
2053
+ now?: () => number;
2054
+ }
2055
+ interface SwapRequest {
2056
+ userAddress: Address;
2057
+ chainId: number;
2058
+ pointTokenAddress: Address;
2059
+ amountIn: bigint;
2060
+ /** ERC-4337 account nonce for the user's EOA. */
2061
+ aaNonce: bigint;
2062
+ /** Optional override; falls back to `defaultSlippageBps`. */
2063
+ slippageBps?: number;
2064
+ /** Optional override; falls back to `now() + defaultSwapDeadlineSeconds`. */
2065
+ deadline?: bigint;
2066
+ }
2067
+ interface SwapResponse {
2068
+ /** Sponsored UserOp — swap (amountIn − fee) + PT.transfer(fee). */
2069
+ userOp: PartialUserOperation;
2070
+ /** Fallback UserOp — swap full amountIn, no PT fee transfer. Present only when fee > 0. */
2071
+ fallback?: PartialUserOperation;
2072
+ feeAmount: bigint;
2073
+ /** Quote for the sponsored path (after fee deduction). */
2074
+ estimatedUsdtOut: bigint;
2075
+ minAmountOut: bigint;
2076
+ /** Quote for the fallback path (full amountIn). Present only when fee > 0. */
2077
+ estimatedUsdtOutFallback?: bigint;
2078
+ minAmountOutFallback?: bigint;
2079
+ deadline: bigint;
2080
+ calls: DecodedCall$1[];
2081
+ callsFallback?: DecodedCall$1[];
2082
+ }
2083
+ declare class SwapHandler {
2084
+ private readonly cfg;
2085
+ constructor(config: SwapHandlerConfig);
2086
+ handle(request: SwapRequest): Promise<SwapResponse>;
2087
+ }
2088
+
2089
+ type DecodedCall = ReturnType<typeof decodeBatchExecuteCalls>[number];
1838
2090
 
1839
2091
  /**
1840
- * Top-level configuration for `createIssuerService`.
2092
+ * Orderly perp-deposit handler builds the sponsored + fallback
2093
+ * UserOps for the PAFI Relay path.
1841
2094
  *
1842
- * In v1.4 the SDK is HTTP-client-free: it signs EIP-712 messages, reads
1843
- * on-chain state, builds unsigned UserOperations, and maintains the
1844
- * off-chain ledger. It never broadcasts transactions — that's the
1845
- * frontend's responsibility via Bundler + Paymaster.
2095
+ * Resolves USDC + verifies the broker is whitelisted on the Vault,
2096
+ * quotes the Relay's USDC fee (covers LayerZero msg.value out of the
2097
+ * Relay's ETH reserve), then builds two UserOps:
1846
2098
  *
1847
- * **Multi-token (0.2.0+):** Pass `pointTokenAddresses: Address[]` to
1848
- * support multiple PointTokens under a single issuer backend. Legacy
1849
- * `pointTokenAddress: Address` still works for single-token deployments.
1850
- * When both are provided, `pointTokenAddresses` takes precedence.
2099
+ * - **sponsored** — PT.transfer(fee, PAFI) + USDC.approve(relay,
2100
+ * total) + Relay.deposit(req). User reimburses gas in PT.
2101
+ * - **fallback** USDC.approve + Relay.deposit only. User pays
2102
+ * ERC-4337 gas in ETH. Built only when `feeAmount > 0`.
2103
+ *
2104
+ * `maxFee` is set to `quote × 1.5` — slippage cap on the Relay's
2105
+ * USD-pricing during the inclusion window. If the actual fee at
2106
+ * execution exceeds maxFee the Relay reverts (`FeeExceedsMax`).
1851
2107
  */
1852
- interface IssuerServiceConfig {
1853
- chainId: number;
1854
- /** Legacy single-token shortcut; prefer `pointTokenAddresses`. */
1855
- pointTokenAddress?: Address;
1856
- /** All PointToken addresses this issuer supports. */
1857
- pointTokenAddresses?: Address[];
1858
- /**
1859
- * Issuer-specific contract addresses merged into the `/config` response.
1860
- * PAFI-owned addresses (batchExecutor, usdt, issuerRegistry, mintingOracle,
1861
- * pafiHook) are auto-resolved from `getContractAddresses(chainId)` and
1862
- * can be omitted. Only `pointToken` / `pointTokens` must be provided.
1863
- */
1864
- contracts?: Pick<ApiConfigResponse["contracts"], "pointToken" | "pointTokens" | "relay">;
1865
- /**
1866
- * Shared `PublicClient` used for on-chain reads, simulation, indexer
1867
- * polling, and gas-price lookups. Must be pointed at the target chain.
1868
- */
2108
+ declare class PerpDepositError extends Error {
2109
+ code: "PERP_DEPOSIT_UNAVAILABLE" | "BROKER_NOT_WHITELISTED" | "RELAY_FEE_EXCEEDS_AMOUNT" | "FEE_EXCEEDS_AMOUNT" | "INVALID_AMOUNT";
2110
+ constructor(code: "PERP_DEPOSIT_UNAVAILABLE" | "BROKER_NOT_WHITELISTED" | "RELAY_FEE_EXCEEDS_AMOUNT" | "FEE_EXCEEDS_AMOUNT" | "INVALID_AMOUNT", message: string);
2111
+ }
2112
+ interface PerpDepositHandlerConfig {
1869
2113
  provider: PublicClient;
1870
- auth: {
1871
- jwtSecret: string;
1872
- /** SIWE-style login-message domain, e.g. `"app.example.com"`. */
1873
- domain: string;
1874
- /** Passed straight to `jose` (`"24h"`, `"7d"`, …). Default `"24h"`. */
1875
- jwtExpiresIn?: string;
1876
- };
1877
- /**
1878
- * Off-chain point ledger — the source of truth for user balances and
1879
- * in-flight minting locks. Every issuer provides their own database-backed
1880
- * implementation (Postgres, Redis, etc.) that implements `IPointLedger`.
1881
- * The SDK does not ship a production ledger; each issuer's data model and
1882
- * infrastructure are different.
1883
- */
1884
- ledger: IPointLedger;
2114
+ /** Optional — when wired, used to estimate the PT gas-reimbursement fee. */
2115
+ feeService?: FeeManager;
2116
+ /** PointToken address used for the sponsored PT.transfer. */
2117
+ pointTokenAddress: Address;
1885
2118
  /**
1886
- * Policy engine optional, defaults to `DefaultPolicyEngine` which checks
1887
- * off-chain balance. Extend or replace to add KYC, volume caps, etc.
2119
+ * Slippage premium applied on top of the Relay quote when computing
2120
+ * `maxFee`. Default 50% (factor 150). The Relay reverts if actual fee
2121
+ * exceeds `maxFee` so a generous cap reduces re-quote churn.
1888
2122
  */
1889
- policy?: IPolicyEngine;
1890
- /** Session store — optional, defaults to `MemorySessionStore` (dev/test only). */
1891
- sessionStore?: ISessionStore;
2123
+ maxFeePremiumBps?: number;
2124
+ }
2125
+ interface PerpDepositRequest {
2126
+ userAddress: Address;
2127
+ chainId: number;
2128
+ amount: bigint;
2129
+ brokerId: keyof typeof BROKER_HASHES;
2130
+ /** ERC-4337 account nonce. */
2131
+ aaNonce: bigint;
2132
+ }
2133
+ interface PerpDepositResponse {
2134
+ userOp: PartialUserOperation;
2135
+ fallback?: PartialUserOperation;
2136
+ feeAmount: bigint;
2137
+ relayTokenFee: bigint;
2138
+ maxFee: bigint;
2139
+ netDeposit: bigint;
2140
+ accountId: `0x${string}`;
2141
+ brokerHash: `0x${string}`;
2142
+ usdcAddress: Address;
2143
+ relayAddress: Address;
2144
+ calls: DecodedCall[];
2145
+ callsFallback?: DecodedCall[];
2146
+ }
2147
+ declare class PerpDepositHandler {
2148
+ private readonly cfg;
2149
+ constructor(config: PerpDepositHandlerConfig);
2150
+ handle(request: PerpDepositRequest): Promise<PerpDepositResponse>;
2151
+ }
2152
+
2153
+ /**
2154
+ * Config for `createSubgraphPoolsProvider`.
2155
+ */
2156
+ interface SubgraphPoolsProviderConfig {
1892
2157
  /**
1893
- * Fee management config. If omitted the `handleGasFee` endpoint will
1894
- * throw "not configured" at request time.
2158
+ * Fully qualified subgraph GraphQL endpoint.
2159
+ * Defaults to the PAFI-hosted subgraph (`PAFI_SUBGRAPH_URL`).
2160
+ * Override only when pointing at a staging or custom deployment.
1895
2161
  */
1896
- fee?: Omit<FeeManagerConfig, "provider">;
2162
+ subgraphUrl?: string;
1897
2163
  /**
1898
- * Pool discovery function for `handlePools`. If omitted the endpoint
1899
- * throws "not configured" at request time.
2164
+ * Cache TTL in milliseconds. Pool discovery is near-static a 30s
2165
+ * cache removes subgraph load without meaningfully delaying UX.
2166
+ * Set to 0 to disable caching. Default: 30_000.
1900
2167
  */
1901
- poolsProvider?: PoolsProvider;
2168
+ cacheTtlMs?: number;
1902
2169
  /**
1903
- * Enables `handleClaim`. The factory combines these with the shared
1904
- * `policy` + `relayService` instances already wired by the factory.
1905
- * Omit to disable the `/claim` endpoint.
2170
+ * Optional fetch override for test harnesses. Defaults to global `fetch`.
1906
2171
  */
1907
- claim?: {
1908
- issuerSignerWallet: WalletClient;
1909
- /** Defaults to the PAFI-deployed BatchExecutor for the target chain. */
1910
- batchExecutorAddress?: Address;
1911
- lockDurationMs?: number;
1912
- };
1913
- indexer?: {
1914
- fromBlock?: bigint;
1915
- cursorStore?: IIndexerCursorStore;
1916
- confirmations?: number;
1917
- batchSize?: number;
1918
- pollIntervalMs?: number;
1919
- /**
1920
- * If `true`, the factory calls `indexer.start()` before returning.
1921
- * Default: `false` — the caller decides when to begin polling.
1922
- */
1923
- autoStart?: boolean;
1924
- };
1925
- }
1926
- interface IssuerService {
1927
- /** AuthService — login, logout, nonce management. */
1928
- auth: AuthService;
1929
- /** Session store — nonce + JWT session persistence. */
1930
- session: ISessionStore;
1931
- ledger: IPointLedger;
1932
- policy: IPolicyEngine;
1933
- /** RelayService — prepareMint / prepareBurn UserOp builders. */
1934
- relay: RelayService;
1935
- /** FeeManager — gas fee estimation. Undefined if not configured. */
1936
- fee: FeeManager | undefined;
1937
- /** All indexers keyed by PointToken address. */
1938
- indexers: Map<Address, PointIndexer>;
2172
+ fetchImpl?: typeof fetch;
1939
2173
  /**
1940
- * First indexer. Kept for backward compat with 0.1.x callers.
1941
- * @deprecated use `indexers.get(tokenAddress)` for multi-token.
2174
+ * Optional clock override for tests.
1942
2175
  */
1943
- indexer: PointIndexer;
1944
- /** Framework-agnostic HTTP handlers — wire into Express / Fastify / Hono. */
1945
- api: IssuerApiHandlers;
2176
+ now?: () => number;
1946
2177
  }
1947
2178
  /**
1948
- * Wire a fully-functional issuer service from a single config object.
2179
+ * Create a `PoolsProvider` backed by the PAFI subgraph.
1949
2180
  *
1950
- * Defaults:
1951
- * - `sessionStore` → `MemorySessionStore` (dev/test only replace in prod)
1952
- * - `policy` → `DefaultPolicyEngine({ ledger })`
1953
- * - `feeManager` → undefined (handleGasFee throws until configured)
1954
- * - `poolsProvider` → undefined (handlePools throws until configured)
1955
- * - `indexer.autoStart` → false
2181
+ * Queries the `pafiTokens` entity for the given `pointTokenAddress`,
2182
+ * reads its linked `Pool`, and returns a single-element `PoolKey[]`.
2183
+ * Multiple pools per token would require a subgraph schema change.
1956
2184
  *
1957
- * Throws synchronously if any required field is missing.
2185
+ * The result is cached in-process with a short TTL (default 30s). Pool
2186
+ * discovery is near-static so this avoids hammering the subgraph without
2187
+ * blocking config changes for long.
2188
+ *
2189
+ * Returns `{ pools: [] }` if:
2190
+ * - the token is not registered on PAFI yet (no PafiToken entity)
2191
+ * - the token is registered but its pool has not been initialised
2192
+ * - the subgraph is unreachable or returns an error (logs to console,
2193
+ * does not throw — callers should handle empty pool list gracefully)
2194
+ *
2195
+ * Assumes the PAFI subgraph schema. Issuers with a custom subgraph must
2196
+ * implement `PoolsProvider` themselves instead of using this helper.
2197
+ *
2198
+ * @example
2199
+ * ```ts
2200
+ * import { createSubgraphPoolsProvider, createIssuerService } from "@pafi/issuer";
2201
+ *
2202
+ * const service = createIssuerService({
2203
+ * // ...other config
2204
+ * poolsProvider: createSubgraphPoolsProvider({
2205
+ * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
2206
+ * }),
2207
+ * });
2208
+ * ```
1958
2209
  */
1959
- declare function createIssuerService(config: IssuerServiceConfig): IssuerService;
2210
+ declare function createSubgraphPoolsProvider(config?: SubgraphPoolsProviderConfig): PoolsProvider;
1960
2211
 
1961
2212
  /**
1962
- * A pending UserOp serialized for persistent storage (Redis, Postgres, memory).
1963
- *
1964
- * All bigint fields are stored as decimal strings so the entry can be
1965
- * JSON-serialized without precision loss. Convert back to bigint before
1966
- * calling `computeUserOpHash` or `serializeUserOpToJsonRpc`.
2213
+ * Config for `createSubgraphNativeUsdtQuoter`.
1967
2214
  */
1968
- interface PendingUserOpEntry {
1969
- sender: Address;
1970
- nonce: string;
1971
- callData: Hex;
1972
- callGasLimit: string;
1973
- verificationGasLimit: string;
1974
- preVerificationGas: string;
1975
- maxFeePerGas: string;
1976
- maxPriorityFeePerGas: string;
1977
- paymaster?: Address;
1978
- paymasterVerificationGasLimit?: string;
1979
- paymasterPostOpGasLimit?: string;
1980
- paymasterData?: Hex;
1981
- chainId: number;
1982
- /** Hex-encoded userOpHash — the value the user signed via personal_sign. */
1983
- userOpHash: Hex;
2215
+ interface SubgraphNativeUsdtQuoterConfig {
1984
2216
  /**
1985
- * Fee-stripped fallback variant. Set by `/claim/prepare` and
1986
- * `/redeem/prepare` when a PT operator fee is configured AND the
1987
- * paymaster sponsorship attached successfully i.e. the user might
1988
- * still want to submit without paymaster (paying ETH gas), and in
1989
- * that case shouldn't be charged the PT fee. `/claim/submit` reads
1990
- * this branch when its request body specifies
1991
- * `variant: "fallback"`.
1992
- *
1993
- * Has a different `callData` (no PT.transfer prepended) and
1994
- * therefore a different `userOpHash`. Paymaster fields are NOT
1995
- * present — the fallback is by definition unsponsored.
2217
+ * Fully qualified subgraph GraphQL endpoint.
2218
+ * Defaults to the PAFI-hosted subgraph (`PAFI_SUBGRAPH_URL`).
2219
+ * Override only when pointing at a staging or custom deployment.
1996
2220
  */
1997
- fallback?: {
1998
- callData: Hex;
1999
- callGasLimit: string;
2000
- verificationGasLimit: string;
2001
- preVerificationGas: string;
2002
- userOpHash: Hex;
2003
- };
2221
+ subgraphUrl?: string;
2222
+ /**
2223
+ * Decimals of the USDT token. Defaults to 6 (standard USDT/USDC on
2224
+ * Base, Ethereum, Polygon). Override for chains where USDT uses a
2225
+ * different decimals value.
2226
+ */
2227
+ usdtDecimals?: number;
2228
+ /**
2229
+ * Decimals of the native token (ETH on Base/mainnet/Arbitrum/Optimism,
2230
+ * MATIC on Polygon). Default: 18.
2231
+ */
2232
+ nativeDecimals?: number;
2233
+ /**
2234
+ * Cache TTL in milliseconds. ETH price drifts slowly relative to gas
2235
+ * estimation needs — a 30s cache keeps fees stable across bursts of
2236
+ * requests without letting them go stale during volatile markets.
2237
+ * Set to 0 to disable caching. Default: 30_000.
2238
+ */
2239
+ cacheTtlMs?: number;
2240
+ /**
2241
+ * Fallback price (USDT per native token, human-readable float) used
2242
+ * when the subgraph is unreachable. This keeps the backend operational
2243
+ * during subgraph outages rather than bricking cashouts. The fee will
2244
+ * be slightly off but the operator still gets paid. Default: 3000.
2245
+ */
2246
+ fallbackEthPriceUsd?: number;
2247
+ /** Optional fetch override for test harnesses. */
2248
+ fetchImpl?: typeof fetch;
2249
+ /** Optional clock override for tests. */
2250
+ now?: () => number;
2004
2251
  }
2005
2252
  /**
2006
- * Storage backend for pending UserOps in the mobile prepare/submit pattern.
2253
+ * Create a native→USDT quoter backed by the PAFI subgraph's
2254
+ * `Bundle.ethPriceUSD`. The returned function has the shape
2255
+ * `(amountNative: bigint) => Promise<bigint>` and can be passed as
2256
+ * `quoteNativeToFee` to `FeeManager` — in v1.4 the fee currency
2257
+ * is configured at the integration layer, not hardcoded here.
2007
2258
  *
2008
- * Implement this interface and wire it into your issuer backend:
2009
- * - `save()` called by `POST /claim/prepare` and `POST /redeem/prepare`
2010
- * - `get()` called by `POST /claim/submit` and `POST /redeem/submit`
2011
- * - `delete()` called after successful submit or explicit cancellation
2259
+ * Used by `FeeManager.estimateGasFee()` to convert the gas cost into
2260
+ * an ERC-20 amount charged as part of the sponsored UserOp batch.
2261
+ * Price precision is not critical a 1-2% drift is acceptable since
2262
+ * the fee manager applies a `gasPremiumBps` buffer.
2012
2263
  *
2013
- * The default implementation in the gg56 boilerplate uses Redis with
2014
- * a short TTL matching the MintRequest / BurnRequest deadline.
2264
+ * The result is cached in-process with a short TTL (default 30s). If
2265
+ * the subgraph is unreachable, falls back to `fallbackEthPriceUsd` so
2266
+ * gas estimation doesn't block user flow during a subgraph outage.
2267
+ *
2268
+ * @example
2269
+ * ```ts
2270
+ * import { createSubgraphNativeUsdtQuoter, createIssuerService } from "@pafi-dev/issuer";
2271
+ *
2272
+ * const service = createIssuerService({
2273
+ * // ...other config
2274
+ * fee: {
2275
+ * quoteNativeToFee: createSubgraphNativeUsdtQuoter({
2276
+ * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
2277
+ * }),
2278
+ * },
2279
+ * });
2280
+ * ```
2015
2281
  */
2016
- interface IPendingUserOpStore {
2017
- save(lockId: string, entry: PendingUserOpEntry, ttlSeconds: number): Promise<void>;
2018
- get(lockId: string): Promise<PendingUserOpEntry | null>;
2019
- delete(lockId: string): Promise<void>;
2020
- }
2282
+ declare function createSubgraphNativeUsdtQuoter(config?: SubgraphNativeUsdtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
2021
2283
 
2284
+ interface NativePtQuoterConfig {
2285
+ /** Viem PublicClient — used to call Chainlink on-chain. */
2286
+ provider: PublicClient;
2287
+ /** Address of the PointToken being traded. */
2288
+ pointTokenAddress: Address;
2289
+ /** Chainlink ETH/USD feed address. Defaults to Base mainnet feed. */
2290
+ chainlinkFeedAddress?: Address;
2291
+ /** PAFI subgraph GraphQL endpoint. */
2292
+ subgraphUrl?: string;
2293
+ /** Cache TTL in ms. Default: 30_000. */
2294
+ cacheTtlMs?: number;
2295
+ /** Fallback ETH price (USD) when Chainlink is unreachable. Default: 3000. */
2296
+ fallbackEthPriceUsd?: number;
2297
+ /** Fallback PT price (USDT per 1 PT) when subgraph is unreachable. Default: 0.1. */
2298
+ fallbackPtPriceUsdt?: number;
2299
+ fetchImpl?: typeof fetch;
2300
+ now?: () => number;
2301
+ }
2022
2302
  /**
2023
- * Convert a stored `PendingUserOpEntry` (decimal-string fields) plus a
2024
- * signature into the JSON-RPC wire format for `eth_sendUserOperation`.
2303
+ * Create a native→PT quoter for use as `FeeManager.quoteNativeToFee`.
2025
2304
  *
2026
- * Bridges the gap between the serialized storage format (decimal strings,
2027
- * safe for JSON/Redis) and `serializeUserOpToJsonRpc` which expects bigints.
2305
+ * Converts ETH gas cost USDT (via Chainlink ETH/USD) → PT (via subgraph
2306
+ * pool price), returning the fee amount in PT raw units (18 decimals).
2307
+ *
2308
+ * Formula:
2309
+ * feeInPT = amountNative × ethPrice_8dec × ptPerUsdt_18dec / 10^26
2310
+ *
2311
+ * Both prices are cached in-process (default 30s TTL).
2312
+ *
2313
+ * @example
2314
+ * ```ts
2315
+ * fee: {
2316
+ * quoteNativeToFee: createNativePtQuoter({
2317
+ * provider,
2318
+ * pointTokenAddress: "0x...",
2319
+ * chainlinkFeedAddress: getContractAddresses(chainId).chainlinkEthUsd,
2320
+ * }),
2321
+ * }
2322
+ * ```
2028
2323
  */
2029
- declare function serializeEntryToJsonRpc(entry: PendingUserOpEntry, signature: Hex, variant?: "sponsored" | "fallback"): Record<string, string | null>;
2324
+ declare function createNativePtQuoter(config: NativePtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
2030
2325
 
2031
2326
  /**
2032
- * Re-shape `UserOpTypedData` so all `bigint` fields become hex strings
2033
- * required for JSON transport over HTTP. Mirrors the inverse of what
2034
- * `walletClient.signTypedData` accepts on the client (it auto-coerces hex
2035
- * strings back to bigints for `uint256` fields).
2036
- */
2037
- type SerializedUserOpTypedData = {
2038
- domain: UserOpTypedData["domain"];
2039
- types: UserOpTypedData["types"];
2040
- primaryType: UserOpTypedData["primaryType"];
2041
- message: {
2042
- sender: Address;
2043
- nonce: Hex;
2044
- initCode: Hex;
2045
- callData: Hex;
2046
- accountGasLimits: Hex;
2047
- preVerificationGas: Hex;
2048
- gasFees: Hex;
2049
- paymasterAndData: Hex;
2050
- };
2051
- };
2052
- /**
2053
- * Convert a `UserOpTypedData` payload into the JSON-safe wire form
2054
- * (bigint → hex string). The mobile client passes this directly to
2055
- * `eth_signTypedData_v4` / viem's `signTypedData`.
2327
+ * Combined off-chain + on-chain balance for a single user / token pair.
2328
+ *
2329
+ * - `offChain` the issuer's ledger balance (available, excluding locks)
2330
+ * - `onChain` — the user's ERC-20 balance from `PointToken.balanceOf`
2331
+ * - `total` — `offChain + onChain` (what the Issuer App displays)
2056
2332
  */
2057
- declare function serializeUserOpTypedData(td: UserOpTypedData): SerializedUserOpTypedData;
2333
+ interface CombinedBalance {
2334
+ offChain: bigint;
2335
+ onChain: bigint;
2336
+ total: bigint;
2337
+ }
2338
+ interface BalanceAggregatorConfig {
2339
+ provider: PublicClient;
2340
+ ledger: IPointLedger;
2341
+ }
2058
2342
  /**
2059
- * Merge Pimlico's paymaster-sponsorship response into a UserOp
2060
- * skeleton, applying only fields that are actually defined.
2061
- *
2062
- * `pm_sponsorUserOperation` returns:
2063
- * - `paymaster` / `paymasterData` — required for the sponsored sig
2064
- * - `paymasterVerificationGasLimit` / `paymasterPostOpGasLimit`
2065
- * - **re-estimated** `callGasLimit` / `verificationGasLimit` /
2066
- * `preVerificationGas` — the paymaster signature is computed over
2067
- * these new values
2068
- * - **bundler-required** `maxFeePerGas` / `maxPriorityFeePerGas` —
2069
- * Base RPC's `eth_feeHistory` underestimates, bundler rejects
2343
+ * v1.4 utility — aggregates off-chain + on-chain point balances into a
2344
+ * single view for the "combined balance" UI in the Issuer App.
2070
2345
  *
2071
- * Callers MUST re-merge ALL of these into the userOp BEFORE computing
2072
- * the EIP-712 userOpHash otherwise both the paymaster signature
2073
- * (AA34) and the user signature (AA24, recovered against a different
2074
- * hash) fail at validation.
2346
+ * The `/user` API handler uses this internally; the helper is exposed
2347
+ * publicly so Issuer Apps can call it directly without going through
2348
+ * the HTTP layer (e.g., for server-rendered pages or admin dashboards).
2075
2349
  *
2076
- * Skips fields that are undefined so legacy paymaster responses
2077
- * (without re-estimated gas) don't accidentally zero out the original
2078
- * estimates.
2350
+ * See [REQUIREMENTS_V2.md] §1 "The Issuer App displays a combined
2351
+ * balance (off-chain points + on-chain PT) and does not surface USDT."
2079
2352
  */
2080
- declare function mergePaymasterFields<T extends object>(userOp: T, paymasterFields: {
2081
- paymaster: Address;
2082
- paymasterData: Hex;
2083
- paymasterVerificationGasLimit: bigint;
2084
- paymasterPostOpGasLimit: bigint;
2085
- callGasLimit?: bigint;
2086
- verificationGasLimit?: bigint;
2087
- preVerificationGas?: bigint;
2088
- maxFeePerGas?: bigint;
2089
- maxPriorityFeePerGas?: bigint;
2090
- } | undefined): T;
2091
- interface PreparedUserOp {
2092
- /** The bundler-ready UserOp (with paymaster + Pimlico-quoted gas). */
2093
- userOp: PartialUserOperation & {
2094
- maxFeePerGas: bigint;
2095
- maxPriorityFeePerGas: bigint;
2096
- paymaster?: Address;
2097
- paymasterData?: Hex;
2098
- paymasterVerificationGasLimit?: bigint;
2099
- paymasterPostOpGasLimit?: bigint;
2100
- };
2101
- /** Hex-encoded EIP-712 digest. Equals `EntryPoint.getUserOpHash`. */
2102
- userOpHash: Hex;
2103
- /** Typed-data payload — pass directly to `eth_signTypedData_v4`. */
2104
- typedData: SerializedUserOpTypedData;
2105
- }
2106
- interface PrepareMobileUserOpParams {
2107
- /** Lock id (issuer-generated) keying both store entry + ledger row. */
2108
- lockId: string;
2353
+ declare class BalanceAggregator {
2354
+ private readonly provider;
2355
+ private readonly ledger;
2356
+ constructor(config: BalanceAggregatorConfig);
2109
2357
  /**
2110
- * Sponsored variant built with the PT operator-fee transfer
2111
- * included. SDK or caller should set `partialUserOp.maxFeePerGas` /
2112
- * `maxPriorityFeePerGas` from `provider.estimateFeesPerGas()` before
2113
- * calling — they get overridden by Pimlico's quote anyway, but
2114
- * they must be valid bigints for the merge.
2358
+ * Combined balance for a single (user, token) pair. Fetches off-chain
2359
+ * + on-chain in parallel.
2115
2360
  */
2116
- partialUserOp: PartialUserOperation & {
2117
- maxFeePerGas: bigint;
2118
- maxPriorityFeePerGas: bigint;
2119
- };
2361
+ getCombinedBalance(user: Address, pointToken: Address): Promise<CombinedBalance>;
2120
2362
  /**
2121
- * Optional fee-stripped fallback variant built with `gasFeePt: 0n`
2122
- * (no PT operator-fee transfer). Submitted when paymaster refuses
2123
- * sponsorship and the user pays ETH gas directly.
2363
+ * Combined balance for multiple tokens owned by the same user. Runs
2364
+ * all lookups in parallel. Returns a Map keyed by the token address
2365
+ * (same casing as supplied caller should normalize if needed).
2124
2366
  */
2125
- partialUserOpFallback?: PartialUserOperation & {
2126
- maxFeePerGas?: bigint;
2127
- maxPriorityFeePerGas?: bigint;
2128
- };
2129
- /** Paymaster sponsorship response, or `undefined` if PAFI declined. */
2130
- paymasterFields?: {
2131
- paymaster: Address;
2132
- paymasterData: Hex;
2133
- paymasterVerificationGasLimit: bigint;
2134
- paymasterPostOpGasLimit: bigint;
2135
- callGasLimit?: bigint;
2136
- verificationGasLimit?: bigint;
2137
- preVerificationGas?: bigint;
2138
- maxFeePerGas?: bigint;
2139
- maxPriorityFeePerGas?: bigint;
2140
- };
2141
- chainId: number;
2142
- /** Pending-userop store implementation (Redis/Postgres/Memory). */
2143
- store: IPendingUserOpStore;
2144
- /** TTL the store entry should outlive — typically the MintRequest deadline. */
2145
- ttlSeconds: number;
2367
+ getCombinedBalanceMulti(user: Address, pointTokens: Address[]): Promise<Map<Address, CombinedBalance>>;
2146
2368
  }
2147
- interface PrepareMobileUserOpResult {
2148
- sponsored: PreparedUserOp;
2369
+
2370
+ /**
2371
+ * Typed errors thrown by the helpers below — issuer controllers map
2372
+ * these to the appropriate HTTP status. We don't depend on @nestjs/common
2373
+ * here because the SDK is framework-agnostic; the consuming controller
2374
+ * wraps each one in its preferred exception class.
2375
+ */
2376
+ declare class BundlerNotConfiguredError extends Error {
2377
+ readonly code = "BUNDLER_NOT_CONFIGURED";
2378
+ constructor();
2379
+ }
2380
+ declare class BundlerRejectedError extends Error {
2381
+ readonly code = "BUNDLER_REJECTED";
2382
+ readonly cause: unknown;
2383
+ constructor(message: string, cause: unknown);
2384
+ }
2385
+ interface RequestPaymasterParams {
2386
+ /** PAFI backend client. When `null` / `undefined` → returns `undefined`. */
2387
+ client: PafiBackendClient | null | undefined;
2388
+ chainId: number;
2389
+ scenario: string;
2390
+ /** UserOp skeleton — must have all gas + fee fields set. */
2391
+ userOp: {
2392
+ sender: Address;
2393
+ nonce: bigint;
2394
+ callData: Hex;
2395
+ callGasLimit: bigint;
2396
+ verificationGasLimit: bigint;
2397
+ preVerificationGas: bigint;
2398
+ maxFeePerGas: bigint;
2399
+ maxPriorityFeePerGas: bigint;
2400
+ };
2401
+ /** Target contract (typically the PointToken). */
2402
+ pointTokenAddress: Address;
2149
2403
  /**
2150
- * Set when `partialUserOpFallback` was supplied AND the PT fee was
2151
- * non-zero (i.e. sponsored fallback). Mobile client picks which
2152
- * variant to sign + submit; SDK's `serializeEntryToJsonRpc` reads
2153
- * the `variant` flag to dispatch.
2404
+ * Function name to surface in audit / paymaster context. Defaults to
2405
+ * a per-scenario sensible value (`mint` / `burn` / `swap` / generic
2406
+ * scenario name).
2154
2407
  */
2155
- fallback?: PreparedUserOp;
2156
- /** What got persisted into the pending-userop store. */
2157
- entry: PendingUserOpEntry;
2408
+ functionName?: string;
2409
+ /** Optional logger for the "sponsorship declined" warning. */
2410
+ onWarning?: (msg: string) => void;
2158
2411
  }
2159
2412
  /**
2160
- * Build the sponsored UserOp + (optional) fee-stripped fallback for the
2161
- * mobile prepare/submit flow.
2413
+ * Thin wrapper around `PafiBackendClient.requestSponsorship` with the
2414
+ * "non-fatal on failure" semantics every issuer wants:
2162
2415
  *
2163
- * What this does, end-to-end:
2164
- * 1. Merge Pimlico's paymaster sponsorship + re-quoted gas into the
2165
- * caller's `partialUserOp` skeleton.
2166
- * 2. Compute the EIP-712 userOpHash + the typed-data payload (in
2167
- * JSON-safe form for HTTP transport).
2168
- * 3. (Optional) Repeat for the `partialUserOpFallback` skeleton with
2169
- * no paymaster fields — produces a separate hash + typed-data so
2170
- * the client can re-sign if it falls back.
2171
- * 4. Persist a single store entry containing BOTH callData variants
2172
- * keyed by lockId. `serializeEntryToJsonRpc` reads the `variant`
2173
- * param at submit time.
2416
+ * - When the client is missing → returns `undefined` (the caller falls
2417
+ * back to a self-funded UserOp).
2418
+ * - When the network call throws OR PAFI declines (rate limit, intent
2419
+ * rejection, paymaster outage) returns `undefined` after logging,
2420
+ * so the controller doesn't hard-fail. The caller's
2421
+ * `prepareMobileUserOp` / `mergePaymasterFields` will gracefully
2422
+ * produce the unsponsored variant.
2174
2423
  *
2175
- * Replaces ~100 LoC of glue per scenario in issuer controllers.
2424
+ * Replaces ~30 LoC of try/catch + scenario-to-function mapping every
2425
+ * issuer would copy.
2176
2426
  */
2177
- declare function prepareMobileUserOp(params: PrepareMobileUserOpParams): Promise<PrepareMobileUserOpResult>;
2178
-
2179
- interface IssuerRegistryRecord {
2180
- issuerAddress: Address;
2181
- signerAddress: Address;
2182
- name: string;
2183
- symbol: string;
2184
- declaredTotalSupply: bigint;
2185
- capBasisPoints: number;
2186
- active: boolean;
2187
- pointToken: Address;
2188
- mintingOracle: Address;
2189
- }
2190
- interface PreValidateMintResult {
2191
- /** Registry record read at pre-validation time. */
2192
- issuer: IssuerRegistryRecord;
2193
- /** Current on-chain PointToken.totalSupply(). */
2194
- totalSupply: bigint;
2195
- /** declaredTotalSupply × capBasisPoints / 10000. */
2196
- hardCap: bigint;
2197
- /** hardCap − totalSupply (clamped to 0). */
2198
- remaining: bigint;
2427
+ declare function requestPaymaster(params: RequestPaymasterParams): Promise<Awaited<ReturnType<PafiBackendClient["requestSponsorship"]>> | undefined>;
2428
+ interface RelayUserOpParams {
2429
+ client: PafiBackendClient | null | undefined;
2430
+ /** EntryPoint address — typically `ENTRY_POINT_V08` from core. */
2431
+ entryPoint: typeof ENTRY_POINT_V08 | string;
2432
+ userOp: Record<string, string | null>;
2433
+ /** EIP-7702 authorization (delegation UserOps only). */
2434
+ eip7702Auth?: {
2435
+ chainId: string;
2436
+ address: string;
2437
+ nonce: string;
2438
+ r: string;
2439
+ s: string;
2440
+ yParity: string;
2441
+ };
2199
2442
  }
2200
2443
  /**
2201
- * Thrown by `IssuerStateValidator.preValidateMint()`.
2202
- * `code` maps 1:1 to the HTTP error the issuer API surfaces to clients.
2444
+ * Submit a serialized UserOp to the Pimlico bundler via PAFI's
2445
+ * sponsor-relayer. Handles the "client missing" / "bundler rejected"
2446
+ * branches as typed errors so the controller can map to HTTP cleanly.
2447
+ *
2448
+ * Every issuer mobile flow has this exact wrapper — moved into SDK
2449
+ * to drop ~30 LoC of try/catch + error-shape boilerplate per
2450
+ * controller.
2451
+ *
2452
+ * Throws:
2453
+ * - `BundlerNotConfiguredError` — caller didn't configure
2454
+ * `PafiBackendClient`. Map to 503.
2455
+ * - `BundlerRejectedError` — bundler returned an error. Map to 422
2456
+ * (the FE can show the reason — usually `AA21` / `AA34` / etc.).
2203
2457
  */
2204
- declare class IssuerStateError extends Error {
2205
- readonly code: "ISSUER_NOT_REGISTERED" | "ISSUER_INACTIVE" | "MINT_CAP_EXCEEDED";
2206
- readonly details?: Record<string, unknown> | undefined;
2207
- constructor(code: "ISSUER_NOT_REGISTERED" | "ISSUER_INACTIVE" | "MINT_CAP_EXCEEDED", message: string, details?: Record<string, unknown> | undefined);
2208
- }
2458
+ declare function relayUserOp(params: RelayUserOpParams): Promise<{
2459
+ userOpHash: Hex;
2460
+ }>;
2209
2461
 
2210
2462
  /**
2211
- * Pure (framework-agnostic) validator for issuer state.
2212
- *
2213
- * Reads IssuerRegistry + PointToken on-chain state and pre-validates
2214
- * mint requests before the user submits a UserOp. Catching these
2215
- * off-chain lets issuers fail fast with a clear error rather than
2216
- * wasting gas on a revert.
2463
+ * Top-level configuration for `createIssuerService`.
2217
2464
  *
2218
- * Caching:
2219
- * - `PointToken.issuer()` memoized for the process lifetime (immutable)
2220
- * - Full state (registry + totalSupply)30s TTL per PointToken
2221
- * - Burst calls while a fetch is in-flight share the same Promise
2222
- * (thundering-herd protection)
2465
+ * In v1.4 the SDK is HTTP-client-free: it signs EIP-712 messages, reads
2466
+ * on-chain state, builds unsigned UserOperations, and maintains the
2467
+ * off-chain ledger. It never broadcasts transactionsthat's the
2468
+ * frontend's responsibility via Bundler + Paymaster.
2223
2469
  *
2224
- * Usage in NestJS: wrap this in an `@Injectable()` service; pass
2225
- * `PublicClient` and `registryAddress` from your DI container.
2470
+ * **Multi-token (0.2.0+):** Pass `pointTokenAddresses: Address[]` to
2471
+ * support multiple PointTokens under a single issuer backend. Legacy
2472
+ * `pointTokenAddress: Address` still works for single-token deployments.
2473
+ * When both are provided, `pointTokenAddresses` takes precedence.
2226
2474
  */
2227
- declare class IssuerStateValidator {
2228
- private readonly provider;
2229
- private readonly registryAddress;
2230
- private readonly pointTokenIssuerCache;
2231
- private readonly stateCache;
2232
- private readonly inflight;
2233
- constructor(provider: PublicClient, registryAddress: Address);
2475
+ interface IssuerServiceConfig {
2476
+ chainId: number;
2477
+ /** Legacy single-token shortcut; prefer `pointTokenAddresses`. */
2478
+ pointTokenAddress?: Address;
2479
+ /** All PointToken addresses this issuer supports. */
2480
+ pointTokenAddresses?: Address[];
2234
2481
  /**
2235
- * Convenience factory reads `registryAddress` from the SDK
2236
- * `CONTRACT_ADDRESSES` map for the given chain.
2482
+ * Issuer-specific contract addresses merged into the `/config` response.
2483
+ * PAFI-owned addresses (batchExecutor, usdt, issuerRegistry, mintingOracle,
2484
+ * pafiHook) are auto-resolved from `getContractAddresses(chainId)` and
2485
+ * can be omitted. Only `pointToken` / `pointTokens` must be provided.
2237
2486
  */
2238
- static forChain(provider: PublicClient, chainId: number): IssuerStateValidator;
2487
+ contracts?: Pick<ApiConfigResponse["contracts"], "pointToken" | "pointTokens" | "relay">;
2239
2488
  /**
2240
- * Invalidate cached state for one PointToken, or everything if omitted.
2241
- * Call after admin txs that change registry or cap settings.
2489
+ * Shared `PublicClient` used for on-chain reads, simulation, indexer
2490
+ * polling, and gas-price lookups. Must be pointed at the target chain.
2242
2491
  */
2243
- invalidate(pointToken?: Address): void;
2492
+ provider: PublicClient;
2493
+ auth: {
2494
+ jwtSecret: string;
2495
+ /** SIWE-style login-message domain, e.g. `"app.example.com"`. */
2496
+ domain: string;
2497
+ /** Passed straight to `jose` (`"24h"`, `"7d"`, …). Default `"24h"`. */
2498
+ jwtExpiresIn?: string;
2499
+ };
2244
2500
  /**
2245
- * Resolve `PointToken.issuer()` once per token and memoize.
2246
- * The issuer field is set at `initialize()` and never changes.
2501
+ * Off-chain point ledger the source of truth for user balances and
2502
+ * in-flight minting locks. Every issuer provides their own database-backed
2503
+ * implementation (Postgres, Redis, etc.) that implements `IPointLedger`.
2504
+ * The SDK does not ship a production ledger; each issuer's data model and
2505
+ * infrastructure are different.
2247
2506
  */
2248
- getIssuerAddressForPointToken(pointToken: Address): Promise<Address>;
2507
+ ledger: IPointLedger;
2249
2508
  /**
2250
- * Read registry record + totalSupply, with 30s cache and in-flight
2251
- * deduplication. Does NOT throw on inactive/missing returns raw state.
2509
+ * Policy engine optional, defaults to `DefaultPolicyEngine` which checks
2510
+ * off-chain balance. Extend or replace to add KYC, volume caps, etc.
2252
2511
  */
2253
- getIssuerState(pointToken: Address): Promise<PreValidateMintResult>;
2512
+ policy?: IPolicyEngine;
2513
+ /** Session store — optional, defaults to `MemorySessionStore` (dev/test only). */
2514
+ sessionStore?: ISessionStore;
2254
2515
  /**
2255
- * Validate that `amount` PT can be minted on `pointToken` right now.
2256
- *
2257
- * Throws `IssuerStateError` with:
2258
- * - `ISSUER_NOT_REGISTERED` — registry has no record for this issuer
2259
- * - `ISSUER_INACTIVE` — issuer.active is false
2260
- * - `MINT_CAP_EXCEEDED` — totalSupply + amount would exceed hardCap
2261
- *
2262
- * Returns the fetched state on success so callers can log without a
2263
- * second RPC round-trip.
2516
+ * Fee management config. If omitted the `handleGasFee` endpoint will
2517
+ * throw "not configured" at request time.
2264
2518
  */
2265
- preValidateMint(pointToken: Address, amount: bigint): Promise<PreValidateMintResult>;
2266
- private fetchIssuerState;
2519
+ fee?: Omit<FeeManagerConfig, "provider">;
2520
+ /**
2521
+ * Pool discovery function for `handlePools`. If omitted the endpoint
2522
+ * throws "not configured" at request time.
2523
+ */
2524
+ poolsProvider?: PoolsProvider;
2525
+ /**
2526
+ * Enables `handleClaim`. The factory combines these with the shared
2527
+ * `policy` + `relayService` instances already wired by the factory.
2528
+ * Omit to disable the `/claim` endpoint.
2529
+ */
2530
+ claim?: {
2531
+ issuerSignerWallet: WalletClient;
2532
+ /** Defaults to the PAFI-deployed BatchExecutor for the target chain. */
2533
+ batchExecutorAddress?: Address;
2534
+ lockDurationMs?: number;
2535
+ };
2536
+ indexer?: {
2537
+ fromBlock?: bigint;
2538
+ cursorStore?: IIndexerCursorStore;
2539
+ confirmations?: number;
2540
+ batchSize?: number;
2541
+ pollIntervalMs?: number;
2542
+ /**
2543
+ * If `true`, the factory calls `indexer.start()` before returning.
2544
+ * Default: `false` — the caller decides when to begin polling.
2545
+ */
2546
+ autoStart?: boolean;
2547
+ };
2548
+ }
2549
+ interface IssuerService {
2550
+ /** AuthService — login, logout, nonce management. */
2551
+ auth: AuthService;
2552
+ /** Session store — nonce + JWT session persistence. */
2553
+ session: ISessionStore;
2554
+ ledger: IPointLedger;
2555
+ policy: IPolicyEngine;
2556
+ /** RelayService — prepareMint / prepareBurn UserOp builders. */
2557
+ relay: RelayService;
2558
+ /** FeeManager — gas fee estimation. Undefined if not configured. */
2559
+ fee: FeeManager | undefined;
2560
+ /** All indexers keyed by PointToken address. */
2561
+ indexers: Map<Address, PointIndexer>;
2562
+ /**
2563
+ * First indexer. Kept for backward compat with 0.1.x callers.
2564
+ * @deprecated use `indexers.get(tokenAddress)` for multi-token.
2565
+ */
2566
+ indexer: PointIndexer;
2567
+ /** Framework-agnostic HTTP handlers — wire into Express / Fastify / Hono. */
2568
+ api: IssuerApiHandlers;
2267
2569
  }
2570
+ /**
2571
+ * Wire a fully-functional issuer service from a single config object.
2572
+ *
2573
+ * Defaults:
2574
+ * - `sessionStore` → `MemorySessionStore` (dev/test only — replace in prod)
2575
+ * - `policy` → `DefaultPolicyEngine({ ledger })`
2576
+ * - `feeManager` → undefined (handleGasFee throws until configured)
2577
+ * - `poolsProvider` → undefined (handlePools throws until configured)
2578
+ * - `indexer.autoStart` → false
2579
+ *
2580
+ * Throws synchronously if any required field is missing.
2581
+ */
2582
+ declare function createIssuerService(config: IssuerServiceConfig): IssuerService;
2268
2583
 
2269
2584
  /** SDK package version — bumped on every release */
2270
2585
  declare const PAFI_ISSUER_SDK_VERSION = "0.4.0";
2271
2586
 
2272
- export { type ApiClaimRequest, type ApiClaimResponse, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedeemRequest, type ApiRedeemResponse, type ApiTopUpRequest, type ApiTopUpResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, BalanceAggregator, type BalanceAggregatorConfig, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type CombinedBalance, DefaultPolicyEngine, type DefaultPolicyEngineOptions, FeeManager, type FeeManagerConfig, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type ISessionStore, InMemoryCursorStore, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type NativePtQuoterConfig, NonceManager, PAFI_ISSUER_SDK_VERSION, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PendingCredit, type PendingUserOpEntry, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, type PoolsProvider, type PreValidateMintResult, type PrepareBurnDirectParams, type PrepareBurnParams, type PrepareBurnWithSigParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type RetryConfig, type SerializedUserOpTypedData, type Session, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, TopUpRedemptionError, TopUpRedemptionHandler, type TopUpRedemptionHandlerConfig, type TopUpRedemptionRequest, type TopUpRedemptionResponse, authenticateRequest, createIssuerService, createNativePtQuoter, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, handleClaimStatus, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };
2587
+ export { type ApiClaimRequest, type ApiClaimResponse, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedeemRequest, type ApiRedeemResponse, type ApiTopUpRequest, type ApiTopUpResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, BalanceAggregator, type BalanceAggregatorConfig, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type CombinedBalance, DefaultPolicyEngine, type DefaultPolicyEngineOptions, FeeManager, type FeeManagerConfig, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type ISessionStore, InMemoryCursorStore, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type NativePtQuoterConfig, NonceManager, 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 PendingCredit, type PendingUserOpEntry, PendingUserOpNotFoundError, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, type PoolsProvider, type PreValidateMintResult, type PrepareBurnDirectParams, type PrepareBurnParams, type PrepareBurnWithSigParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type RetryConfig, type SerializedUserOpTypedData, type Session, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, SwapError, SwapHandler, type SwapHandlerConfig, type SwapRequest, type SwapResponse, TopUpRedemptionError, TopUpRedemptionHandler, type TopUpRedemptionHandlerConfig, type TopUpRedemptionRequest, type TopUpRedemptionResponse, authenticateRequest, createIssuerService, createNativePtQuoter, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, handleClaimStatus, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };