@pafi-dev/issuer 0.3.0-beta.1 → 0.3.0-beta.11

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.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { Address, Hex, PublicClient, Chain } from 'viem';
2
- import { PointTokenDomainConfig, MintRequest, EIP712Signature, MintParams, SwapParams, MintRequestV2, SignatureStruct, PartialUserOperation, BurnConsent, ReceiverConsent, PathKey, PoolKey, SponsorshipScenario } from '@pafi-dev/core';
3
- export { encodeExtData } from '@pafi-dev/core';
1
+ import { Address, Hex, PublicClient, WalletClient } from 'viem';
2
+ import { PointTokenDomainConfig, PartialUserOperation, BurnRequest, ReceiverConsent, PathKey, PoolKey } from '@pafi-dev/core';
4
3
 
5
4
  /**
6
5
  * Lifecycle of a minting request as tracked by the issuer's point ledger.
@@ -63,11 +62,7 @@ interface IPointLedger {
63
62
  lockForMinting(userAddress: Address, amount: bigint, lockDurationMs: number, tokenAddress?: Address): Promise<string>;
64
63
  /** Release a previously created lock (e.g. on tx failure / cancel). */
65
64
  releaseLock(lockId: string): Promise<void>;
66
- /**
67
- * Permanently deduct an amount from a user's balance after the on-chain
68
- * mint has been observed by the indexer. Should also resolve any matching
69
- * lock so the funds aren't double-counted.
70
- */
65
+ /** Deduct balance after a confirmed on-chain mint. Idempotent on `txHash`. */
71
66
  deductBalance(userAddress: Address, amount: bigint, txHash: Hex, tokenAddress?: Address): Promise<void>;
72
67
  /** Credit points to a user's balance (e.g. from merchant activity). */
73
68
  creditBalance(userAddress: Address, amount: bigint, reason: string, tokenAddress?: Address): Promise<void>;
@@ -98,47 +93,6 @@ interface IPointLedger {
98
93
  resolveCreditByBurnTx?(lockId: string, txHash: Hex): Promise<void>;
99
94
  }
100
95
 
101
- /**
102
- * In-memory IPointLedger implementation for development and tests.
103
- *
104
- * NOT for production — state is lost on restart. Issuers should ship their
105
- * own database-backed implementation.
106
- *
107
- * Concurrency model: single-process, single-threaded (Node.js event loop).
108
- * The lock check + insert is atomic within a tick because no awaits sit
109
- * between balance read and lock write.
110
- *
111
- * **Multi-token (0.2.0):** Balances are keyed by `(user, token)`. If callers
112
- * omit `tokenAddress`, the literal string "default" is used — that keeps
113
- * single-token usage working exactly like 0.1.x.
114
- */
115
- declare class MemoryPointLedger implements IPointLedger {
116
- private balances;
117
- private locks;
118
- private nextLockId;
119
- private now;
120
- constructor(opts?: {
121
- now?: () => number;
122
- });
123
- getBalance(userAddress: Address, tokenAddress?: Address): Promise<bigint>;
124
- getLockedRequests(userAddress: Address, tokenAddress?: Address): Promise<LockedMintRequest[]>;
125
- creditBalance(userAddress: Address, amount: bigint, _reason: string, tokenAddress?: Address): Promise<void>;
126
- lockForMinting(userAddress: Address, amount: bigint, lockDurationMs: number, tokenAddress?: Address): Promise<string>;
127
- releaseLock(lockId: string): Promise<void>;
128
- deductBalance(userAddress: Address, amount: bigint, txHash: Hex, tokenAddress?: Address): Promise<void>;
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>;
134
- /**
135
- * Auto-expire any PENDING lock past its expiry. Called lazily on every
136
- * read/write so the in-memory state stays self-cleaning without a timer.
137
- */
138
- private purgeExpired;
139
- private lockedTotalFor;
140
- }
141
-
142
96
  /**
143
97
  * Input to a policy evaluation. Policy engines use this to decide whether
144
98
  * a user's mint request should be approved.
@@ -214,56 +168,6 @@ declare class DefaultPolicyEngine implements IPolicyEngine {
214
168
  evaluate(request: PolicyEvalRequest): Promise<PolicyDecision>;
215
169
  }
216
170
 
217
- /**
218
- * Issuer signer — produces the MintRequest EIP-712 signature that the Relay
219
- * contract verifies against the issuer's on-chain authorized minter.
220
- *
221
- * This is a trust boundary: the default `PrivateKeySigner` holds the key in
222
- * process memory and is intended for local development only. Production
223
- * issuers replace this with an HSM/KMS/MPC integration.
224
- */
225
- interface IIssuerSigner {
226
- /**
227
- * Sign a `MintRequest` message against the point token's EIP-712 domain.
228
- * The returned signature is what the Relay contract passes to
229
- * `PointToken.verify` during `mintAndSwap`.
230
- */
231
- signMintRequest(domain: PointTokenDomainConfig, message: MintRequest): Promise<EIP712Signature>;
232
- /** Get the signer's on-chain address (used for verification / logging). */
233
- getAddress(): Promise<Address>;
234
- }
235
-
236
- interface PrivateKeySignerOptions {
237
- /** 0x-prefixed 32-byte hex private key */
238
- privateKey: Hex;
239
- /**
240
- * Chain metadata for the viem WalletClient. Only the chain id is actually
241
- * used for signing; a minimal stub is acceptable (it does not need to
242
- * match the deployed chain config beyond id).
243
- */
244
- chain: Chain;
245
- /**
246
- * Optional RPC URL. `signTypedData` is offline, so this can usually be
247
- * left unset — viem only requires a transport to construct the client.
248
- */
249
- rpcUrl?: string;
250
- }
251
- /**
252
- * Local-key implementation of `IIssuerSigner`. Wraps viem's `signTypedData`
253
- * via the shared `@pafi/core` `signMintRequest` helper.
254
- *
255
- * ⚠️ **NOT for production use.** The private key lives in process memory
256
- * and is trivially extractable from a compromised host. Replace with an
257
- * HSM/KMS/MPC-backed `IIssuerSigner` before deployment.
258
- */
259
- declare class PrivateKeySigner implements IIssuerSigner {
260
- private readonly account;
261
- private readonly walletClient;
262
- constructor(opts: PrivateKeySignerOptions);
263
- signMintRequest(domain: PointTokenDomainConfig, message: MintRequest): Promise<EIP712Signature>;
264
- getAddress(): Promise<Address>;
265
- }
266
-
267
171
  /**
268
172
  * A server-issued session created after a successful wallet login. The
269
173
  * token id is embedded in the JWT so sessions can be revoked without
@@ -449,168 +353,93 @@ declare class AuthError extends Error {
449
353
  }
450
354
 
451
355
  /**
452
- * Parameters for a single `mintAndSwap` relay submission. This is the
453
- * exact shape the Relay contract expects, with the two structs the
454
- * `@pafi/core` calldata helpers build. The RelayService wires these
455
- * through viem's `writeContract`.
456
- */
457
- interface SubmitMintAndSwapParams {
458
- mint: MintParams;
459
- swap: SwapParams;
460
- }
461
- /**
462
- * Result of a relay submission. `txHash` is returned immediately after
463
- * the tx is broadcast; `receipt` is present only when the caller opted to
464
- * wait for confirmation.
465
- */
466
- interface RelayResult {
467
- txHash: Hex;
468
- blockNumber?: bigint;
469
- gasUsed?: bigint;
470
- status?: "success" | "reverted";
471
- }
472
- /**
473
- * Errors raised by RelayService carry a `code` so the MintingGateway (or
474
- * any caller) can decide whether to release the ledger lock.
356
+ * Errors raised by RelayService carry a `code` so callers (handlers /
357
+ * gateway-less HTTP wrappers) can decide how to map them to HTTP status.
358
+ *
359
+ * v1.4 trimmed the error space to just encoding failures — the service
360
+ * no longer broadcasts transactions, so `SUBMIT_FAILED`, `TX_REVERTED`,
361
+ * `SIMULATION_FAILED`, and `TIMEOUT` all went away with the operator
362
+ * wallet. Paymaster/Bundler errors surface out-of-band on the FE.
475
363
  */
476
- type RelayErrorCode = "NOT_CONFIGURED" | "ENCODE_FAILED" | "SIMULATION_FAILED" | "SUBMIT_FAILED" | "TX_REVERTED" | "TIMEOUT";
364
+ type RelayErrorCode = "NOT_CONFIGURED" | "ENCODE_FAILED";
477
365
  declare class RelayError extends Error {
478
366
  readonly code: RelayErrorCode;
479
367
  readonly cause?: unknown;
480
368
  constructor(code: RelayErrorCode, message: string, cause?: unknown);
481
369
  }
482
- /**
483
- * Interface an operator wallet must satisfy for the RelayService. Matches
484
- * the subset of viem's `WalletClient` we actually call, so tests can pass
485
- * a minimal mock instead of a full client.
486
- */
487
- interface OperatorWalletLike {
488
- writeContract: (args: {
489
- address: Address;
490
- abi: readonly unknown[];
491
- functionName: string;
492
- args: readonly unknown[];
493
- account?: {
494
- address: Address;
495
- };
496
- }) => Promise<Hex>;
497
- account?: {
498
- address: Address;
499
- } | undefined;
500
- }
501
370
 
502
- interface RelayServiceConfig {
503
- /** Address of the deployed Relay contract (chain-specific). */
504
- relayAddress: Address;
505
- /** Operator wallet that pays gas and receives the operator fee. */
506
- operatorWallet: OperatorWalletLike;
507
- /**
508
- * Provider used for pre-flight simulation and receipt waiting. Optional —
509
- * if omitted, the service still broadcasts but returns only `txHash`
510
- * (caller is responsible for confirmation tracking).
511
- */
512
- provider?: PublicClient;
513
- /**
514
- * If a provider is supplied, wait up to this many milliseconds for a
515
- * receipt before timing out. Default: 60_000 (one minute).
516
- */
517
- confirmationTimeoutMs?: number;
518
- /**
519
- * Whether to run `simulateContract` before submitting. Catches most
520
- * reverts locally without wasting gas. Default: true (when provider is
521
- * supplied).
522
- */
523
- simulateBeforeSubmit?: boolean;
524
- }
525
371
  /**
526
- * Submits `mintAndSwap` transactions to the Relay contract on behalf of
527
- * the issuer. This is the single place the operator wallet is touched; the
528
- * MintingGateway calls into `submitMintAndSwap()` after it has signed the
529
- * MintRequest and verified the ReceiverConsent.
372
+ * Builds unsigned `PartialUserOperation` payloads for the v1.4 sponsored
373
+ * flow. The service is stateless and HTTP-client-free:
530
374
  *
531
- * The service is intentionally thin: calldata encoding stays in `@pafi/core`
532
- * (`encodeMintAndSwap`), so on-chain ABI changes only ripple through one
533
- * package.
375
+ * - `prepareMint` signs a `MintRequest` EIP-712 with the caller-supplied
376
+ * issuer signer wallet, then encodes `PointToken.mint(to, amount,
377
+ * deadline, minterSig)` into a UserOp the frontend submits via
378
+ * EIP-7702 + Paymaster.
379
+ * - `prepareBurn` mirrors the above on the burn side using a
380
+ * pre-signed `BurnRequest` + `PointToken.burn(from, amount,
381
+ * deadline, burnerSig)`.
382
+ *
383
+ * There is no broadcasting, no operator wallet, no simulation — those
384
+ * concerns moved to the Bundler + Paymaster in v1.4.
534
385
  */
535
386
  declare class RelayService {
536
- private readonly relayAddress;
537
- private readonly operatorWallet;
538
- private readonly provider?;
539
- private readonly confirmationTimeoutMs;
540
- private readonly simulateBeforeSubmit;
541
- constructor(config: RelayServiceConfig);
542
- /** Address the operator wallet is broadcasting from (for logging). */
543
- operatorAddress(): Address | undefined;
544
- /**
545
- * Build calldata for the Relay `mintAndSwap` function. Kept public so
546
- * callers (e.g. the MintingGateway) can log or persist the encoded call
547
- * for audit before broadcasting.
548
- */
549
- encodeCall(params: SubmitMintAndSwapParams): Hex;
550
387
  /**
551
- * Submit a `mintAndSwap` transaction. Flow:
552
- *
553
- * 1. (optional) pre-flight simulate via provider
554
- * 2. writeContract through the operator wallet
555
- * 3. (optional) wait for the receipt and surface gasUsed / status
556
- *
557
- * Throws a typed `RelayError` on any failure so the MintingGateway can
558
- * decide whether to release the ledger lock (`SUBMIT_FAILED` and
559
- * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
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.
388
+ * Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
389
+ * `PointToken.mint(to, amount, deadline, minterSig)`.
566
390
  */
567
- submitMintAndSwap(params: SubmitMintAndSwapParams): Promise<RelayResult>;
568
- /**
569
- * Build an unsigned UserOp for Scenario 1 (Mint).
570
- *
571
- * Flow:
572
- * 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
573
- * 2. Optionally append a PT fee transfer from user → feeRecipient
574
- * (fee recovery happens on-chain via BatchExecutor, not via an
575
- * operator wallet)
576
- * 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
577
- * 4. Return a `PartialUserOperation` ready for:
578
- * - gas estimation (Bundler)
579
- * - paymaster sponsorship (PAFI Backend)
580
- * - user signature (Privy)
581
- */
582
- prepareMint(params: PrepareMintParams): PartialUserOperation;
391
+ prepareMint(params: PrepareMintParams): Promise<PartialUserOperation>;
583
392
  /**
584
393
  * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
585
394
  *
586
395
  * Two modes:
587
- * - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
588
- * via EIP-7702 delegation is the user, so no signature needed
589
- * on-chain (the BurnConsent was already verified off-chain by
590
- * the issuer backend before we got here)
591
- * - `mode: 'burnWithSig'` `PointToken.burnWithSig(consent, sig)`;
592
- * used when the issuer hasn't verified the consent and the
593
- * contract has to do it on-chain
396
+ * - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
397
+ * usable if the caller (via EIP-7702) is whitelisted as a burner.
398
+ * Rare in v1.4; kept for admin/operator tools.
399
+ * - `mode: 'burnWithSig'` `PointToken.burn(from, amount, deadline,
400
+ * burnerSig)`. Caller provides a pre-signed `BurnRequest` + sig
401
+ * bytes (typically from `PTRedeemHandler`).
594
402
  */
595
403
  prepareBurn(params: PrepareBurnParams): PartialUserOperation;
596
404
  }
405
+ /**
406
+ * v1.4 — sig-gated `PointToken.mint(to, amount, deadline, minterSig)`.
407
+ *
408
+ * The issuer backend validates off-chain (balance, policy, KYC), signs
409
+ * a `MintRequest` EIP-712 with its minter signer, and packages the
410
+ * whole thing into a UserOp for the user to submit via EIP-7702 +
411
+ * Paymaster. On confirmation, PointIndexer watches `Transfer(0x0, user,
412
+ * amount)` and resolves the ledger lock.
413
+ */
597
414
  interface PrepareMintParams {
598
415
  /** User EOA that will send the UserOp (via EIP-7702 delegation). */
599
416
  userAddress: Address;
600
- /** ERC-4337 account nonce (not the MintRequest nonce — different namespace). */
417
+ /** ERC-4337 account nonce. Caller fetches from EntryPoint v0.7. */
601
418
  aaNonce: bigint;
602
- /** Deployed Relayer v2 contract address (chain-specific). */
603
- relayerAddress: Address;
604
419
  /** BatchExecutor delegation target (chain-specific). */
605
420
  batchExecutorAddress: Address;
606
- /** PointToken being minted used for optional fee transfer call. */
421
+ /** PointToken contractthe call target + EIP-712 verifying contract. */
607
422
  pointTokenAddress: Address;
608
- /** EIP-712-signed MintRequest fields. */
609
- mintRequest: MintRequestV2;
610
- /** User's EIP-712 signature over `mintRequest`. */
611
- userSignature: SignatureStruct;
612
- /** Issuer's EIP-712 signature over `mintRequest`. */
613
- issuerSignature: SignatureStruct;
423
+ /** PT amount to mint to `userAddress`. */
424
+ amount: bigint;
425
+ /**
426
+ * Issuer minter signer wallet — signs the `MintRequest` EIP-712.
427
+ * Must be added to `PointToken.minters[]` via `addMinter(signerAddr)`
428
+ * at provisioning time. Typically HSM/KMS-backed in prod.
429
+ */
430
+ issuerSignerWallet: WalletClient;
431
+ /** EIP-712 domain for MintRequest. */
432
+ domain: PointTokenDomainConfig;
433
+ /** Current `mintRequestNonces[userAddress]` — caller reads from contract. */
434
+ mintRequestNonce: bigint;
435
+ /** Unix timestamp after which the signature expires. */
436
+ deadline: bigint;
437
+ /**
438
+ * Optional — application-level fee transfer appended after mint.
439
+ * Set both `feeAmount` and `feeRecipient` together.
440
+ */
441
+ feeAmount?: bigint;
442
+ feeRecipient?: Address;
614
443
  /** Gas limits — defaults are conservative; caller can tighten. */
615
444
  callGasLimit?: bigint;
616
445
  verificationGasLimit?: bigint;
@@ -632,8 +461,10 @@ interface PrepareBurnDirectParams extends PrepareBurnCommonParams {
632
461
  }
633
462
  interface PrepareBurnWithSigParams extends PrepareBurnCommonParams {
634
463
  mode: "burnWithSig";
635
- burnConsent: BurnConsent;
636
- consentSignature: SignatureStruct;
464
+ /** BurnRequest message the issuer burner signer signed. */
465
+ burnRequest: BurnRequest;
466
+ /** Serialized EIP-712 signature (bytes) over `burnRequest`. */
467
+ burnerSignature: Hex;
637
468
  }
638
469
 
639
470
  interface FeeManagerConfig {
@@ -683,6 +514,9 @@ declare class FeeManager {
683
514
  private readonly gasUnits;
684
515
  private readonly gasPremiumBps;
685
516
  private readonly quoteNativeToFee;
517
+ private cachedFee;
518
+ private cacheExpiresAt;
519
+ private static readonly CACHE_TTL_MS;
686
520
  constructor(config: FeeManagerConfig);
687
521
  /**
688
522
  * Estimate the fee (in the caller's fee currency) to charge for the
@@ -699,156 +533,6 @@ declare class FeeManager {
699
533
  estimateGasFee(): Promise<bigint>;
700
534
  }
701
535
 
702
- /**
703
- * End-user request for a full "burn points → receive USDT" flow. The
704
- * receiver has already signed the `ReceiverConsent` EIP-712 message on
705
- * the frontend; the issuer backend runs everything else.
706
- */
707
- interface MintAndCashOutRequest {
708
- /** Address owning the off-chain points (== receiver in the consent). */
709
- userAddress: Address;
710
- /** Point token contract to mint. */
711
- pointTokenAddress: Address;
712
- /** Chain id the relay will submit on. */
713
- chainId: number;
714
- /**
715
- * EIP-712 domain for `pointTokenAddress`. The gateway passes this
716
- * straight through to `@pafi/core` `verifyReceiverConsent` and the
717
- * issuer signer. Callers typically fetch it once via
718
- * `PafiSDK.getDomain()` and cache it.
719
- */
720
- domain: PointTokenDomainConfig;
721
- /**
722
- * Receiver consent message + signature, pre-built by the frontend. The
723
- * message specifies `onBehalfOf = relay contract` and
724
- * `originalReceiver = userAddress`, plus the amount, nonce, deadline,
725
- * and encoded extData.
726
- */
727
- receiverConsent: ReceiverConsent;
728
- receiverSignature: Hex;
729
- /**
730
- * Swap path for the USDT output. Empty array = "no swap" (rare — only
731
- * useful for testing). Normally a single hop pointToken → USDT.
732
- */
733
- swapPath: PathKey[];
734
- /** Swap deadline (unix seconds). */
735
- swapDeadline: bigint;
736
- /**
737
- * Lock TTL (ms) to apply to the off-chain balance. The gateway computes
738
- * a safe default from `receiverConsent.deadline` if omitted.
739
- */
740
- lockDurationMs?: number;
741
- }
742
- /**
743
- * Result returned to the caller after a successful `processMintAndCashOut`.
744
- * The `lockId` is preserved so the indexer can correlate the on-chain
745
- * Mint event back to the ledger row (though that correlation is typically
746
- * done by `(userAddress, amount)` in the default `MemoryPointLedger`).
747
- */
748
- interface MintAndCashOutResponse {
749
- txHash: Hex;
750
- lockId: string;
751
- blockNumber?: bigint;
752
- gasUsed?: bigint;
753
- }
754
- /**
755
- * Error codes a MintingGateway can surface. Callers (API handlers) map
756
- * these to HTTP status codes. The `SAFE_TO_RETRY` field tells the caller
757
- * whether the underlying lock was released — if not, the user should
758
- * wait before retrying to avoid double-spend.
759
- */
760
- type MintingGatewayErrorCode = "INVALID_REQUEST" | "INVALID_CONSENT_SIGNATURE" | "CONSENT_EXPIRED" | "POLICY_REJECTED" | "INSUFFICIENT_BALANCE" | "SIGNER_FAILED" | "RELAY_SIMULATION_FAILED" | "RELAY_SUBMIT_FAILED" | "RELAY_REVERTED" | "RELAY_TIMEOUT";
761
- declare class MintingGatewayError extends Error {
762
- readonly code: MintingGatewayErrorCode;
763
- /**
764
- * True if the ledger lock was released before this error was thrown,
765
- * meaning the user can safely retry. False means the funds are still
766
- * locked (e.g. tx may still land on-chain) and retry would double-spend.
767
- */
768
- readonly safeToRetry: boolean;
769
- readonly cause?: unknown;
770
- constructor(code: MintingGatewayErrorCode, message: string, opts: {
771
- safeToRetry: boolean;
772
- cause?: unknown;
773
- });
774
- }
775
-
776
- interface MintingGatewayConfig {
777
- ledger: IPointLedger;
778
- policy: IPolicyEngine;
779
- signer: IIssuerSigner;
780
- relayService: RelayService;
781
- /**
782
- * Clock override for tests. Defaults to `() => Date.now()`. Used to
783
- * compute safe lock TTLs and to check consent deadlines.
784
- */
785
- now?: () => number;
786
- /**
787
- * Extra buffer (ms) added on top of `(consent.deadline - now)` when the
788
- * caller doesn't supply a `lockDurationMs`. Default: 60_000 (one minute
789
- * — roughly 2× the Base L2 block confirmation time).
790
- */
791
- defaultLockBufferMs?: number;
792
- }
793
- /**
794
- * The MintingGateway is the central orchestrator that turns a user's
795
- * signed ReceiverConsent into an on-chain `mintAndSwap` tx and a
796
- * consistent off-chain ledger update.
797
- *
798
- * 11-step flow (per `PAFI_ISSUER_SDK_SPEC.md` §4.3):
799
- *
800
- * 1. Field validation (cheap rejects before any crypto)
801
- * 2. Verify ReceiverConsent signature via `@pafi/core`
802
- * 3. Check off-chain balance via ledger
803
- * 4. Check locked requests via ledger
804
- * 5. Run policy engine (balance + on-chain cap + issuer rules)
805
- * 6. Lock the requested amount in the ledger
806
- * 7. Sign MintRequest as issuer
807
- * 8. Build MintParams + SwapParams
808
- * 9. Submit via RelayService (encode + simulate + broadcast + wait)
809
- * 10. Return { txHash, lockId, receipt fields }
810
- * 11. On any failure, release the lock IF it's safe to retry — i.e. we
811
- * know the tx cannot still land on-chain. If the tx may have made
812
- * it (`TX_REVERTED` or `TIMEOUT`), we leave the lock alone and
813
- * surface `safeToRetry: false` so the caller doesn't double-spend.
814
- *
815
- * The gateway deliberately does NOT deduct the balance here. Deduction
816
- * happens in the `PointIndexer` when the on-chain Mint event is observed,
817
- * which is what makes the system crash-safe: if the gateway dies between
818
- * broadcast and receipt, the indexer will still finalize the ledger.
819
- */
820
- declare class MintingGateway {
821
- private readonly ledger;
822
- private readonly policy;
823
- private readonly signer;
824
- private readonly relayService;
825
- private readonly now;
826
- private readonly defaultLockBufferMs;
827
- constructor(config: MintingGatewayConfig);
828
- /**
829
- * @deprecated Since 0.3.0 — will be renamed to `processMint()` once
830
- * the SC team finalizes Relayer v2 ABI. The new flow drops the
831
- * swap steps entirely (no more single-call mint+swap); users swap
832
- * separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
833
- */
834
- processMintAndCashOut(request: MintAndCashOutRequest): Promise<MintAndCashOutResponse>;
835
- private computeLockDurationMs;
836
- /**
837
- * Map a RelayError to a MintingGatewayError, releasing the lock only
838
- * when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
839
- * leave the lock in place because the tx may still be in the mempool
840
- * or already mined — releasing would enable a double-spend on retry.
841
- * Always throws.
842
- */
843
- private handleRelayFailure;
844
- /**
845
- * Release a lock, swallowing any secondary error. We never want a lock
846
- * release failure to mask the original error — the lock will auto-expire
847
- * via TTL anyway.
848
- */
849
- private releaseLockSafely;
850
- }
851
-
852
536
  /** Decoded Transfer(from=0x0 → to) event used to finalize a mint. */
853
537
  interface MintEvent {
854
538
  /** Destination address (the user who received the minted points) */
@@ -1008,6 +692,15 @@ interface BurnIndexerConfig {
1008
692
  /** Polling interval (ms). Default: 5000. */
1009
693
  pollIntervalMs?: number;
1010
694
  now?: () => number;
695
+ /**
696
+ * Map a burn event to the pending credit lockId that should be resolved.
697
+ * Return `undefined` to skip this burn event (no credit granted).
698
+ *
699
+ * REQUIRED — there is no default implementation. Issuers with a Postgres
700
+ * ledger typically JOIN on `(from, amount, status=PENDING)`. The in-memory
701
+ * ledger uses a lookup by lockId supplied out-of-band from the claim flow.
702
+ */
703
+ matchLockId: (event: BurnEvent) => Promise<string | undefined>;
1011
704
  }
1012
705
  /**
1013
706
  * Mirror of `PointIndexer` for the reverse direction — watches
@@ -1038,17 +731,6 @@ declare class BurnIndexer {
1038
731
  private readonly confirmations;
1039
732
  private readonly batchSize;
1040
733
  private readonly pollIntervalMs;
1041
- /**
1042
- * Caller-supplied matcher. Return the lockId to resolve for a given
1043
- * burn event, or `undefined` to skip. Runs synchronously via the
1044
- * ledger's query path.
1045
- *
1046
- * Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
1047
- * lock id `burn-${from}-${amount}` — the in-memory ledger assigns
1048
- * incrementing IDs so callers with the memory ledger must provide a
1049
- * custom matcher. Real DB-backed ledgers override this to JOIN on
1050
- * their `pending_credits` table.
1051
- */
1052
734
  matchLockId: (event: BurnEvent) => Promise<string | undefined>;
1053
735
  private running;
1054
736
  private timer;
@@ -1093,6 +775,18 @@ interface ApiConfigResponse {
1093
775
  mintingOracle?: Address;
1094
776
  poolManager?: Address;
1095
777
  usdt?: Address;
778
+ /**
779
+ * EIP-7702 delegation target — the single contract every user's
780
+ * EOA must delegate to before submitting sponsored UserOps. On
781
+ * Base mainnet this is Coinbase Smart Wallet v2.
782
+ */
783
+ batchExecutor?: Address;
784
+ /**
785
+ * Uniswap V4 hook that enforces the 10% fee on PT→USDT swaps
786
+ * (USDT→PT is free). FE uses this in `PoolKey.hooks` when building
787
+ * a swap; the pool is only discoverable when the hook matches.
788
+ */
789
+ pafiHook?: Address;
1096
790
  };
1097
791
  /**
1098
792
  * Absolute URL that the Issuer App opens after a successful claim to
@@ -1186,6 +880,27 @@ interface ApiClaimAndSwapResponse {
1186
880
  blockNumber?: bigint;
1187
881
  gasUsed?: bigint;
1188
882
  }
883
+ interface ApiClaimRequest {
884
+ chainId: number;
885
+ pointTokenAddress: Address;
886
+ /** PT amount to mint. */
887
+ amount: bigint;
888
+ /** ERC-4337 account nonce for the user's EOA (from EntryPoint). */
889
+ aaNonce: bigint;
890
+ /** Unix seconds — when the MintRequest signature expires. */
891
+ deadline: bigint;
892
+ /** Optional operator fee (PT) deducted inside the same UserOp batch. */
893
+ feeAmount?: bigint;
894
+ feeRecipient?: Address;
895
+ }
896
+ interface ApiClaimResponse {
897
+ /** Off-chain lock id — poll `/user` to track PENDING → MINTED. */
898
+ lockId: string;
899
+ /** Unsigned UserOp — attach paymaster data + user signature, then submit. */
900
+ userOp: PartialUserOperation;
901
+ /** Seconds until the off-chain lock expires if the UserOp is not submitted. */
902
+ expiresInSeconds: number;
903
+ }
1189
904
  interface ApiBuildConsentTypedDataRequest {
1190
905
  chainId: number;
1191
906
  pointTokenAddress: Address;
@@ -1211,7 +926,6 @@ type PoolsProvider = (request: ApiPoolsRequest) => Promise<ApiPoolsResponse>;
1211
926
 
1212
927
  interface IssuerApiHandlersConfig {
1213
928
  authService: AuthService;
1214
- gateway: MintingGateway;
1215
929
  ledger: IPointLedger;
1216
930
  /** Used by `handleUser` to read on-chain nonces and minter status. */
1217
931
  provider: PublicClient;
@@ -1237,13 +951,26 @@ interface IssuerApiHandlersConfig {
1237
951
  feeManager?: FeeManager;
1238
952
  /** Required by `handlePools`; omit to disable the endpoint. */
1239
953
  poolsProvider?: PoolsProvider;
954
+ /**
955
+ * Required by `handleClaim`; omit to disable the endpoint.
956
+ * Wires policy evaluation + ledger locking + MintRequest signing
957
+ * into a single atomic handler so callers cannot accidentally skip
958
+ * the policy check.
959
+ */
960
+ claim?: {
961
+ policy: IPolicyEngine;
962
+ relayService: RelayService;
963
+ issuerSignerWallet: WalletClient;
964
+ batchExecutorAddress: Address;
965
+ /** How long to hold the off-chain lock while the UserOp is in flight. Default: 15 min. */
966
+ lockDurationMs?: number;
967
+ };
1240
968
  }
1241
969
  /**
1242
970
  * Framework-agnostic HTTP handlers that match the endpoints a `PafiSDK`
1243
971
  * frontend expects to call. Issuers wrap these in Express / Fastify /
1244
972
  * Hono / whatever — the handlers take plain inputs and return plain
1245
- * outputs, with `AuthError` / `MintingGatewayError` surfaced as typed
1246
- * exceptions.
973
+ * outputs, with `AuthError` surfaced as typed exceptions.
1247
974
  *
1248
975
  * Every protected handler takes a pre-verified `userAddress` as its first
1249
976
  * argument. The issuer's middleware wraps `authenticateRequest()` from
@@ -1252,7 +979,6 @@ interface IssuerApiHandlersConfig {
1252
979
  */
1253
980
  declare class IssuerApiHandlers {
1254
981
  private readonly authService;
1255
- private readonly gateway;
1256
982
  private readonly ledger;
1257
983
  private readonly provider;
1258
984
  /**
@@ -1260,15 +986,12 @@ declare class IssuerApiHandlers {
1260
986
  * validate the request's `pointTokenAddress` against this set.
1261
987
  */
1262
988
  private readonly supportedTokens;
1263
- /** First supported token — used as default when a handler doesn't
1264
- * receive a `pointTokenAddress` in the request (shouldn't happen in
1265
- * practice, but keeps type-narrowing happy). */
1266
- private readonly defaultToken;
1267
989
  private readonly chainId;
1268
990
  private readonly contracts;
1269
991
  private readonly pafiWebUrl?;
1270
992
  private readonly feeManager?;
1271
993
  private readonly poolsProvider?;
994
+ private readonly claim?;
1272
995
  constructor(config: IssuerApiHandlersConfig);
1273
996
  /** `GET /auth/nonce` */
1274
997
  handleGetNonce(): Promise<ApiNonceResponse>;
@@ -1315,32 +1038,47 @@ declare class IssuerApiHandlers {
1315
1038
  */
1316
1039
  handleBuildConsentTypedData(userAddress: Address, request: ApiBuildConsentTypedDataRequest): Promise<ApiBuildConsentTypedDataResponse>;
1317
1040
  /**
1318
- * `POST /claim-and-swap`
1041
+ * `POST /claim`
1319
1042
  *
1320
- * @deprecated Since 0.3.0 the single-call mint-then-swap flow is
1321
- * retired in v1.4. Use the new `handleClaim()` (mint only) and let
1322
- * the user swap separately on PAFI Web. See
1323
- * [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
1324
- * removed in 2.0.
1043
+ * Policy gate + ledger lock + MintRequest signing in a single atomic
1044
+ * step. Returns an unsigned UserOp the frontend attaches paymaster data
1045
+ * to and submits via EIP-7702 + Bundler.
1325
1046
  *
1326
- * Legacy behavior: the terminal handler forwards the verified
1327
- * consent to the MintingGateway, which runs the 11-step flow.
1047
+ * Order of operations:
1048
+ * 1. Validate request fields.
1049
+ * 2. policy.evaluate() — throws if denied; cannot be bypassed.
1050
+ * 3. ledger.lockForMinting() — reserves the balance.
1051
+ * 4. Read on-chain mintRequestNonce + token name in parallel.
1052
+ * 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
1053
+ * 6. On any error after step 3, release the lock before re-throwing.
1328
1054
  */
1329
- handleClaimAndSwap(userAddress: Address, request: ApiClaimAndSwapRequest): Promise<ApiClaimAndSwapResponse>;
1055
+ handleClaim(userAddress: Address, request: ApiClaimRequest): Promise<ApiClaimResponse>;
1330
1056
  }
1331
1057
 
1332
1058
  /**
1333
- * v1.4 reverse flow — **Variant A**: user-initiated PT redeem.
1059
+ * v1.4 reverse flow — user-initiated PT redeem.
1334
1060
  *
1335
- * User has on-chain PT, wants to convert back to off-chain points. They
1336
- * sign a `BurnConsent`, the issuer backend verifies it, reserves an
1337
- * off-chain credit, and returns an unsigned UserOp that the frontend
1338
- * submits via the Bundler. When the burn lands, the `BurnIndexer`
1339
- * (elsewhere) resolves the credit.
1061
+ * User has on-chain PT, wants to convert back to off-chain points. The
1062
+ * issuer backend signs a `BurnRequest` with its burner signer, reserves
1063
+ * an off-chain pending credit, and returns an unsigned UserOp. The FE
1064
+ * submits the UserOp via EIP-7702 + Coinbase Paymaster. On confirmation,
1065
+ * `Transfer(user → 0x0)` is emitted; `BurnIndexer` resolves the pending
1066
+ * credit to a real off-chain credit.
1340
1067
  *
1341
- * **Mocked SC contracts**this handler compiles + wires end-to-end
1342
- * against `@pafi-dev/core/contracts` mock ABIs. When SC ships real
1343
- * ABIs, no changes here only `contracts/index.ts` re-export flips.
1068
+ * Contract path (mock ABI matches deployed PointToken):
1069
+ *
1070
+ * burn(address from, uint256 amount, uint256 deadline, bytes burnerSig)
1071
+ *
1072
+ * On-chain checks:
1073
+ * - msg.sender == from (enforced via EIP-7702 delegation on user EOA)
1074
+ * - burnerSig signer ∈ burners[]
1075
+ * - nonce == burnRequestNonces[from]
1076
+ * - now <= deadline
1077
+ *
1078
+ * The user never signs an EIP-712 message in this flow. Their only
1079
+ * signature is the ERC-4337 UserOp signature, which the AA wallet
1080
+ * handles. Consent is implicit: by submitting the UserOp they authorize
1081
+ * the burn.
1344
1082
  */
1345
1083
  interface PTRedeemHandlerConfig {
1346
1084
  ledger: IPointLedger;
@@ -1350,32 +1088,43 @@ interface PTRedeemHandlerConfig {
1350
1088
  pointTokenAddress: Address;
1351
1089
  /** BatchExecutor delegation target (chain-specific). */
1352
1090
  batchExecutorAddress: Address;
1353
- /** Chain id — used for domain separator when verifying BurnConsent. */
1091
+ /** Chain id — used for the BurnRequest EIP-712 domain. */
1354
1092
  chainId: number;
1355
1093
  /**
1356
1094
  * EIP-712 domain fields. Must match the on-chain PointToken's domain
1357
- * separator exactly, or signature verification fails. `name` is
1358
- * typically the PointToken's ERC-20 name (e.g. "PAFI Starbucks
1359
- * Points"). `verifyingContract` defaults to `pointTokenAddress`.
1095
+ * separator, or on-chain signature recovery fails. `name` is
1096
+ * typically the PointToken's ERC-20 name. `verifyingContract`
1097
+ * defaults to `pointTokenAddress`.
1360
1098
  */
1361
1099
  domain: {
1362
1100
  name: string;
1363
1101
  verifyingContract?: Address;
1364
1102
  };
1103
+ /**
1104
+ * Issuer burner signer wallet — signs the `BurnRequest` EIP-712.
1105
+ * Must be whitelisted via `PointToken.addBurner(signerAddr)` at
1106
+ * provisioning time. Typically HSM/KMS-backed in prod.
1107
+ */
1108
+ burnerSignerWallet: WalletClient;
1365
1109
  /**
1366
1110
  * How long the pending credit stays reserved if the burn never lands.
1367
1111
  * Default: 15 min — long enough for a bundler submission + confirmation.
1368
1112
  */
1369
1113
  redeemLockDurationMs?: number;
1114
+ /**
1115
+ * How far ahead of `now` to set the BurnRequest deadline. Default:
1116
+ * 15 min. Must be long enough for Bundler + EntryPoint to execute;
1117
+ * short enough to prevent replay if the UserOp is abandoned.
1118
+ */
1119
+ signatureDeadlineSeconds?: number;
1370
1120
  /** Clock injection for tests; defaults to `Date.now`. */
1371
1121
  now?: () => number;
1372
1122
  }
1373
1123
  interface PTRedeemRequest {
1124
+ /** Address extracted from the verified JWT — must match `userAddress`. */
1125
+ authenticatedAddress: Address;
1374
1126
  userAddress: Address;
1375
1127
  amount: bigint;
1376
- /** Serialized EIP-712 signature over the BurnConsent. */
1377
- consentSignature: Hex;
1378
- consent: BurnConsent;
1379
1128
  /** ERC-4337 account nonce for the user's EOA. */
1380
1129
  aaNonce: bigint;
1381
1130
  }
@@ -1386,19 +1135,24 @@ interface PTRedeemResponse {
1386
1135
  userOp: PartialUserOperation;
1387
1136
  /** Seconds until the lock expires if the burn doesn't land. */
1388
1137
  expiresInSeconds: number;
1138
+ /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1139
+ signatureDeadline: bigint;
1389
1140
  }
1390
1141
  declare class PTRedeemError extends Error {
1391
- code: "INVALID_CONSENT" | "SIGNATURE_MISMATCH" | "AMOUNT_MISMATCH" | "EXPIRED_CONSENT" | "LEDGER_NOT_SUPPORTED";
1392
- constructor(code: "INVALID_CONSENT" | "SIGNATURE_MISMATCH" | "AMOUNT_MISMATCH" | "EXPIRED_CONSENT" | "LEDGER_NOT_SUPPORTED", message: string);
1142
+ code: "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED";
1143
+ constructor(code: "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED", message: string);
1393
1144
  }
1394
1145
  declare class PTRedeemHandler {
1395
1146
  private readonly ledger;
1396
1147
  private readonly relayService;
1148
+ private readonly provider;
1397
1149
  private readonly pointTokenAddress;
1398
1150
  private readonly batchExecutorAddress;
1399
1151
  private readonly chainId;
1400
1152
  private readonly domain;
1153
+ private readonly burnerSignerWallet;
1401
1154
  private readonly redeemLockDurationMs;
1155
+ private readonly signatureDeadlineSeconds;
1402
1156
  private readonly now;
1403
1157
  constructor(config: PTRedeemHandlerConfig);
1404
1158
  handle(request: PTRedeemRequest): Promise<PTRedeemResponse>;
@@ -1419,8 +1173,12 @@ declare class PTRedeemHandler {
1419
1173
  * → burn 200 PT, credit 200 off-chain, voucher proceeds with 500
1420
1174
  *
1421
1175
  * Delegates the actual burn construction to {@link PTRedeemHandler}
1422
- * — this handler is pure business logic (calculating shortfall +
1423
- * checking on-chain balance) on top.
1176
+ * — this handler is pure business logic (shortfall math + on-chain
1177
+ * balance check) on top.
1178
+ *
1179
+ * v1.4 note: user no longer pre-signs a `BurnConsent`. The issuer
1180
+ * backend signs a `BurnRequest` itself (see `PTRedeemHandler`), so
1181
+ * this handler only needs `userAddress + requiredAmount + aaNonce`.
1424
1182
  */
1425
1183
  interface TopUpRedemptionHandlerConfig {
1426
1184
  ledger: IPointLedger;
@@ -1430,15 +1188,13 @@ interface TopUpRedemptionHandlerConfig {
1430
1188
  pointTokenAddress: Address;
1431
1189
  }
1432
1190
  interface TopUpRedemptionRequest {
1191
+ /** Address extracted from the verified JWT — must match `userAddress`. */
1192
+ authenticatedAddress: Address;
1433
1193
  userAddress: Address;
1434
1194
  /** Total points the voucher redemption requires off-chain. */
1435
1195
  requiredAmount: bigint;
1436
- /**
1437
- * The user's pre-signed `BurnConsent` + signature, prepared by the FE
1438
- * with amount set to a worst-case upper bound. Handler inspects the
1439
- * shortfall and uses the consent if the shortfall ≤ consent.amount.
1440
- */
1441
- redeemRequest: Pick<PTRedeemRequest, "consent" | "consentSignature" | "aaNonce">;
1196
+ /** ERC-4337 account nonce for the user's EOA. */
1197
+ aaNonce: bigint;
1442
1198
  }
1443
1199
  type TopUpRedemptionResponse = {
1444
1200
  action: "NO_TOP_UP_NEEDED";
@@ -1454,8 +1210,8 @@ type TopUpRedemptionResponse = {
1454
1210
  redeem: PTRedeemResponse;
1455
1211
  };
1456
1212
  declare class TopUpRedemptionError extends Error {
1457
- code: "INSUFFICIENT_ONCHAIN_BALANCE" | "CONSENT_AMOUNT_TOO_LOW" | "LEDGER_NOT_SUPPORTED";
1458
- constructor(code: "INSUFFICIENT_ONCHAIN_BALANCE" | "CONSENT_AMOUNT_TOO_LOW" | "LEDGER_NOT_SUPPORTED", message: string);
1213
+ code: "UNAUTHORIZED" | "INSUFFICIENT_ONCHAIN_BALANCE" | "LEDGER_NOT_SUPPORTED";
1214
+ constructor(code: "UNAUTHORIZED" | "INSUFFICIENT_ONCHAIN_BALANCE" | "LEDGER_NOT_SUPPORTED", message: string);
1459
1215
  }
1460
1216
  declare class TopUpRedemptionHandler {
1461
1217
  private readonly ledger;
@@ -1640,185 +1396,52 @@ declare class BalanceAggregator {
1640
1396
  }
1641
1397
 
1642
1398
  interface RetryConfig {
1643
- /**
1644
- * Max total attempts including the first try. Default: 1 (no retry).
1645
- * Set to 3 to get 2 retries after the initial call.
1646
- *
1647
- * Only applies when the server error body carries `safeToRetry: true`
1648
- * or the failure is a transient network/timeout error.
1649
- */
1650
1399
  maxAttempts?: number;
1651
- /**
1652
- * Initial backoff delay in ms. Default: 500. Each subsequent retry
1653
- * doubles this (exponential backoff) and adds ±20% jitter.
1654
- */
1655
1400
  initialDelayMs?: number;
1656
- /**
1657
- * Hard ceiling for a single backoff (ms). Default: 10_000.
1658
- */
1659
1401
  maxDelayMs?: number;
1660
- /**
1661
- * Upper bound on `retryAfter` from the server. If the server asks us
1662
- * to wait longer than this (e.g. rate limit until UTC midnight), the
1663
- * client gives up rather than blocking. Default: 30_000.
1664
- */
1665
1402
  maxRetryAfterMs?: number;
1666
1403
  }
1667
1404
  interface PafiBackendConfig {
1668
- /**
1669
- * PAFI Backend API base URL. Example:
1670
- * https://api.pacificfinance.org
1671
- * https://staging-api.pacificfinance.org
1672
- */
1673
1405
  url: string;
1674
- /** PAFI-assigned issuer ID (e.g., "gg56"). Sent in X-Issuer-Id header. */
1675
1406
  issuerId: string;
1676
- /** Per-issuer API key (or JWT) for the Authorization header. */
1677
1407
  apiKey: string;
1678
- /** Optional fetch override for tests. */
1679
1408
  fetchImpl?: typeof fetch;
1680
- /**
1681
- * Timeout (ms) for each request. Default: 10_000. PAFI Backend should
1682
- * respond in <1s for the happy path; this is just the sanity bound.
1683
- */
1684
1409
  timeoutMs?: number;
1685
- /**
1686
- * Retry policy for transient failures (5xx, 429, timeouts, network).
1687
- * Omit or pass `{ maxAttempts: 1 }` to disable retry entirely.
1688
- */
1689
1410
  retry?: RetryConfig;
1690
1411
  }
1691
- /** Paired with `POST /paymaster/sponsor`. See SPONSORED_PATH_FLOW.md §4.1 */
1692
- interface SponsorshipRequest {
1693
- chainId: number;
1694
- scenario: SponsorshipScenario;
1695
- userOp: PartialUserOperation;
1696
- target: {
1697
- /** The allowlisted contract this batch call targets. */
1698
- contract: Address;
1699
- /** Function selector / name — validated against allowlist. */
1700
- function: string;
1701
- /** The PointToken involved (for scenario context). */
1702
- pointToken?: Address;
1703
- };
1704
- }
1705
- interface SponsorshipResponse {
1706
- paymaster: Address;
1707
- paymasterData: Hex;
1708
- paymasterVerificationGasLimit: bigint;
1709
- paymasterPostOpGasLimit: bigint;
1710
- /** Unix seconds when this sponsorship expires. Re-request after. */
1711
- expiresAt: number;
1712
- }
1713
- /**
1714
- * Machine-readable error codes returned by PAFI Backend.
1715
- *
1716
- * Source of truth: `apps/paymaster-proxy` `CalldataValidationError`,
1717
- * `RateLimitError`, `CoinbaseClientError`. Keep in sync.
1718
- */
1719
- 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";
1412
+ type PafiBackendErrorCode = "MISSING_ISSUER_ID" | "MISSING_API_KEY" | "ISSUER_UNAUTHORIZED" | "USER_UNAUTHORIZED" | "INTENT_REJECTED" | "MINT_CAP_EXCEEDED" | "ISSUER_INACTIVE" | "BROKER_NOT_WHITELISTED" | "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_EXCEEDED_DAILY" | "RATE_LIMIT_EXCEEDED_PER_USER" | "ISSUER_BUDGET_EXCEEDED" | "RATE_LIMITER_UNAVAILABLE" | "BAD_REQUEST" | "INTERNAL_ERROR" | "TIMEOUT" | "NETWORK_ERROR";
1720
1413
  declare class PafiBackendError extends Error {
1721
1414
  code: PafiBackendErrorCode;
1722
1415
  httpStatus: number;
1723
1416
  details?: unknown | undefined;
1724
- /**
1725
- * Seconds to wait before retry. Populated from the server body
1726
- * (e.g. rate limit returns the number of seconds until UTC midnight).
1727
- */
1728
1417
  readonly retryAfter?: number;
1729
- /**
1730
- * `safeToRetry` as reported by the server body. Prefer this over the
1731
- * code-based heuristic when available — the server knows more about
1732
- * whether the same request will succeed on retry.
1733
- */
1734
1418
  private readonly serverSafeToRetry?;
1735
1419
  constructor(code: PafiBackendErrorCode, message: string, httpStatus: number, details?: unknown | undefined, opts?: {
1736
1420
  retryAfter?: number;
1737
1421
  safeToRetry?: boolean;
1738
1422
  });
1739
- /**
1740
- * Whether the caller can safely retry the same request.
1741
- *
1742
- * If the server provided `safeToRetry` in the body, trust that.
1743
- * Otherwise fall back to a code-based heuristic.
1744
- */
1745
1423
  get safeToRetry(): boolean;
1746
1424
  }
1747
1425
 
1748
1426
  /**
1749
- * HTTP client for the PAFI Backend paymaster proxy service. See
1750
- * [SPONSORED_PATH_FLOW.md] for the full flow + API contract.
1427
+ * Top-level configuration for `createIssuerService`.
1751
1428
  *
1752
- * This client sits between `@pafi/issuer`'s RelayService and the
1753
- * PAFI Backend. It does NOT talk to Coinbase Paymaster directly —
1754
- * PAFI Backend holds that integration.
1755
- */
1756
- declare class PafiBackendClient {
1757
- private readonly url;
1758
- private readonly issuerId;
1759
- private readonly apiKey;
1760
- private readonly fetchImpl;
1761
- private readonly timeoutMs;
1762
- private readonly retry;
1763
- constructor(config: PafiBackendConfig);
1764
- /**
1765
- * Request paymaster sponsorship for a pre-built UserOperation.
1766
- * See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
1767
- *
1768
- * Retries automatically on transient failures (5xx, timeouts, network
1769
- * errors, and errors the server flags with `safeToRetry: true`) up to
1770
- * `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
1771
- *
1772
- * @throws PafiBackendError on final failure after exhausting retries
1773
- */
1774
- requestSponsorship(req: SponsorshipRequest): Promise<SponsorshipResponse>;
1775
- private postWithRetry;
1776
- /**
1777
- * Pick the delay before the next retry.
1778
- * - If the server sent `retryAfter` (seconds), honor it (capped by
1779
- * `maxRetryAfterMs`) — returns null if the server wait exceeds the
1780
- * cap, signalling the caller should give up.
1781
- * - Otherwise: exponential backoff with ±20% jitter, capped at
1782
- * `maxDelayMs`.
1783
- */
1784
- private computeBackoff;
1785
- private sleep;
1786
- private post;
1787
- /** JSON replacer that stringifies bigints. Paired with bigintReviver. */
1788
- private bigintReplacer;
1789
- /**
1790
- * JSON reviver that coerces specific numeric-string fields back to
1791
- * bigint. The server must send these fields as decimal strings.
1792
- */
1793
- private bigintReviver;
1794
- }
1795
-
1796
- /**
1797
- * Top-level configuration for `createIssuerService`. Everything except
1798
- * the chain metadata, wallets, auth secret, and `signer` is optional and
1799
- * falls back to the in-memory dev defaults — that makes the happy path
1800
- * a single-call wire-up while still letting production issuers plug in
1801
- * their own ledger, session store, policy engine, and KMS signer.
1429
+ * In v1.4 the SDK is HTTP-client-free: it signs EIP-712 messages, reads
1430
+ * on-chain state, builds unsigned UserOperations, and maintains the
1431
+ * off-chain ledger. It never broadcasts transactions — that's the
1432
+ * frontend's responsibility via Bundler + Paymaster.
1802
1433
  *
1803
- * **Multi-token (0.2.0):** Pass `pointTokenAddresses: Address[]` to
1434
+ * **Multi-token (0.2.0+):** Pass `pointTokenAddresses: Address[]` to
1804
1435
  * support multiple PointTokens under a single issuer backend. Legacy
1805
1436
  * `pointTokenAddress: Address` still works for single-token deployments.
1806
1437
  * When both are provided, `pointTokenAddresses` takes precedence.
1807
1438
  */
1808
1439
  interface IssuerServiceConfig {
1809
1440
  chainId: number;
1810
- /**
1811
- * Address of the deployed PointToken. Legacy single-token shortcut;
1812
- * prefer `pointTokenAddresses` for multi-token issuers.
1813
- */
1441
+ /** Legacy single-token shortcut; prefer `pointTokenAddresses`. */
1814
1442
  pointTokenAddress?: Address;
1815
- /**
1816
- * All PointToken addresses this issuer supports. Takes precedence over
1817
- * `pointTokenAddress`. Factory creates one `PointIndexer` per address.
1818
- */
1443
+ /** All PointToken addresses this issuer supports. */
1819
1444
  pointTokenAddresses?: Address[];
1820
- /** Address of the deployed Relay contract. */
1821
- relayAddress: Address;
1822
1445
  /**
1823
1446
  * Full contract address bundle returned verbatim by `handleConfig` so
1824
1447
  * the frontend SDK can pick them up.
@@ -1829,8 +1452,6 @@ interface IssuerServiceConfig {
1829
1452
  * polling, and gas-price lookups. Must be pointed at the target chain.
1830
1453
  */
1831
1454
  provider: PublicClient;
1832
- /** Operator wallet — pays gas, receives the operator fee. */
1833
- operatorWallet: OperatorWalletLike;
1834
1455
  auth: {
1835
1456
  jwtSecret: string;
1836
1457
  /** SIWE-style login-message domain, e.g. `"app.example.com"`. */
@@ -1839,18 +1460,23 @@ interface IssuerServiceConfig {
1839
1460
  jwtExpiresIn?: string;
1840
1461
  };
1841
1462
  /**
1842
- * Issuer signer. No default production issuers MUST plug in an
1843
- * HSM/KMS-backed implementation. For local development use
1844
- * `PrivateKeySigner` directly.
1463
+ * Off-chain point ledgerthe source of truth for user balances and
1464
+ * in-flight minting locks. Every issuer provides their own database-backed
1465
+ * implementation (Postgres, Redis, etc.) that implements `IPointLedger`.
1466
+ * The SDK does not ship a production ledger; each issuer's data model and
1467
+ * infrastructure are different.
1468
+ */
1469
+ ledger: IPointLedger;
1470
+ /**
1471
+ * Policy engine — optional, defaults to `DefaultPolicyEngine` which checks
1472
+ * off-chain balance. Extend or replace to add KYC, volume caps, etc.
1845
1473
  */
1846
- signer: IIssuerSigner;
1847
- ledger?: IPointLedger;
1848
1474
  policy?: IPolicyEngine;
1475
+ /** Session store — optional, defaults to `MemorySessionStore` (dev/test only). */
1849
1476
  sessionStore?: ISessionStore;
1850
1477
  /**
1851
1478
  * Fee management config. If omitted the `handleGasFee` endpoint will
1852
- * throw "not configured" at request time. Pass any subset of fields
1853
- * to opt in — `provider` is inherited from the outer config.
1479
+ * throw "not configured" at request time.
1854
1480
  */
1855
1481
  fee?: Omit<FeeManagerConfig, "provider">;
1856
1482
  /**
@@ -1858,6 +1484,16 @@ interface IssuerServiceConfig {
1858
1484
  * throws "not configured" at request time.
1859
1485
  */
1860
1486
  poolsProvider?: PoolsProvider;
1487
+ /**
1488
+ * Enables `handleClaim`. The factory combines these with the shared
1489
+ * `policy` + `relayService` instances already wired by the factory.
1490
+ * Omit to disable the `/claim` endpoint.
1491
+ */
1492
+ claim?: {
1493
+ issuerSignerWallet: WalletClient;
1494
+ batchExecutorAddress: Address;
1495
+ lockDurationMs?: number;
1496
+ };
1861
1497
  indexer?: {
1862
1498
  fromBlock?: bigint;
1863
1499
  cursorStore?: IIndexerCursorStore;
@@ -1870,32 +1506,18 @@ interface IssuerServiceConfig {
1870
1506
  */
1871
1507
  autoStart?: boolean;
1872
1508
  };
1873
- relay?: {
1874
- simulateBeforeSubmit?: boolean;
1875
- confirmationTimeoutMs?: number;
1876
- };
1877
- gateway?: {
1878
- /** Extra lock TTL buffer beyond consent deadline. Default 60_000 ms. */
1879
- defaultLockBufferMs?: number;
1880
- };
1881
1509
  }
1882
1510
  interface IssuerService {
1883
1511
  authService: AuthService;
1884
1512
  sessionStore: ISessionStore;
1885
1513
  ledger: IPointLedger;
1886
1514
  policy: IPolicyEngine;
1887
- signer: IIssuerSigner;
1888
1515
  relayService: RelayService;
1889
1516
  feeManager: FeeManager | undefined;
1890
- gateway: MintingGateway;
1891
- /**
1892
- * All indexers keyed by PointToken address. For multi-token issuers there
1893
- * is one per configured token. Single-token issuers will find one entry.
1894
- */
1517
+ /** All indexers keyed by PointToken address. */
1895
1518
  indexers: Map<Address, PointIndexer>;
1896
1519
  /**
1897
- * First indexer. Kept for backward compat with 0.1.x callers that
1898
- * expected `service.indexer` to be a single instance.
1520
+ * First indexer. Kept for backward compat with 0.1.x callers.
1899
1521
  * @deprecated use `indexers.get(tokenAddress)` for multi-token.
1900
1522
  */
1901
1523
  indexer: PointIndexer;
@@ -1903,23 +1525,19 @@ interface IssuerService {
1903
1525
  }
1904
1526
  /**
1905
1527
  * Wire a fully-functional issuer service from a single config object.
1906
- * Returns every constructed collaborator so the caller can also use the
1907
- * indexer or relay service directly outside the HTTP layer.
1908
1528
  *
1909
1529
  * Defaults:
1910
- * - `ledger` → `MemoryPointLedger`
1911
- * - `sessionStore` → `MemorySessionStore`
1530
+ * - `sessionStore` → `MemorySessionStore` (dev/test only — replace in prod)
1912
1531
  * - `policy` → `DefaultPolicyEngine({ ledger })`
1913
1532
  * - `feeManager` → undefined (handleGasFee throws until configured)
1914
1533
  * - `poolsProvider` → undefined (handlePools throws until configured)
1915
1534
  * - `indexer.autoStart` → false
1916
1535
  *
1917
- * Throws synchronously if any required field (`signer`, `provider`,
1918
- * `operatorWallet`, `auth.jwtSecret`, at least one point token) is missing.
1536
+ * Throws synchronously if any required field is missing.
1919
1537
  */
1920
1538
  declare function createIssuerService(config: IssuerServiceConfig): IssuerService;
1921
1539
 
1922
1540
  /** SDK package version — bumped on every release */
1923
1541
  declare const PAFI_ISSUER_SDK_VERSION = "0.1.0";
1924
1542
 
1925
- 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, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, 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, TopUpRedemptionError, TopUpRedemptionHandler, type TopUpRedemptionHandlerConfig, type TopUpRedemptionRequest, type TopUpRedemptionResponse, authenticateRequest, createIssuerService, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider };
1543
+ export { type ApiBuildConsentTypedDataRequest, type ApiBuildConsentTypedDataResponse, type ApiClaimAndSwapRequest, type ApiClaimAndSwapResponse, type ApiClaimRequest, type ApiClaimResponse, 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 IPointLedger, type IPolicyEngine, type ISessionStore, InMemoryCursorStore, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerService, type IssuerServiceConfig, type LockedMintRequest, type LoginResult, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintingStatus, NonceManager, PAFI_ISSUER_SDK_VERSION, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, type PoolsProvider, type PrepareBurnDirectParams, type PrepareBurnParams, type PrepareBurnWithSigParams, type PrepareMintParams, RelayError, type RelayErrorCode, RelayService, type RetryConfig, type Session, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, TopUpRedemptionError, TopUpRedemptionHandler, type TopUpRedemptionHandlerConfig, type TopUpRedemptionRequest, type TopUpRedemptionResponse, authenticateRequest, createIssuerService, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider };