@pafi-dev/issuer 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1884,6 +1884,16 @@ interface PTRedeemHandlerConfig {
1884
1884
  * provisioning time. Typically HSM/KMS-backed in prod.
1885
1885
  */
1886
1886
  burnerSignerWallet: WalletClient;
1887
+ /**
1888
+ * Issuer's allow-listed PointToken contracts (checksummed EIP-55).
1889
+ * Every `handle()` call validates the request's `pointTokenAddress`
1890
+ * against this set BEFORE any chain read, signer call, or pending
1891
+ * credit reservation.
1892
+ * Pass the SAME set used to construct `IssuerApiHandlers` and
1893
+ * `PTClaimHandler` so the read, claim, and redeem paths agree on
1894
+ * what this issuer is willing to sign for.
1895
+ */
1896
+ supportedTokens: ReadonlySet<Address>;
1887
1897
  /**
1888
1898
  * Optional — when wired, used to estimate the PT gas-reimbursement
1889
1899
  * fee. The handler self-computes `feeAmount` + `feeRecipient` so the
@@ -1977,7 +1987,7 @@ interface PTRedeemResponse {
1977
1987
  /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1978
1988
  signatureDeadline: bigint;
1979
1989
  }
1980
- type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED";
1990
+ type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "UNSUPPORTED_POINT_TOKEN";
1981
1991
  declare class PTRedeemError extends PafiSdkError {
1982
1992
  readonly httpStatus: "unprocessable";
1983
1993
  readonly code: PTRedeemErrorCode;
@@ -1995,6 +2005,7 @@ declare class PTRedeemHandler {
1995
2005
  private readonly chainId;
1996
2006
  private readonly domainResolver;
1997
2007
  private readonly burnerSignerWallet;
2008
+ private readonly supportedTokens;
1998
2009
  private readonly redeemLockDurationMs;
1999
2010
  private readonly signatureDeadlineSeconds;
2000
2011
  private readonly now;
@@ -2273,6 +2284,23 @@ interface PendingUserOpEntry {
2273
2284
  verificationGasLimit: string;
2274
2285
  preVerificationGas: string;
2275
2286
  userOpHash: Hex;
2287
+ /**
2288
+ * Lock id (PendingCredit on redeem, LockedMint on claim) reserved
2289
+ * for the FALLBACK path — distinct from the outer entry's
2290
+ * sponsored lock because the on-chain amount the user burns/mints
2291
+ * differs between variants.
2292
+ *
2293
+ * Audit PACI5-21 — pre-fix `handleMobileSubmit.bindUserOpHash`
2294
+ * always bound the submitted userOpHash to the SPONSORED lockId,
2295
+ * even when the user submitted the fallback variant. The
2296
+ * bundler-receipt fallback in `handleRedeemStatus` then resolved
2297
+ * the sponsored credit (`amount - fee`) against the on-chain
2298
+ * burn of the FULL `amount` — every fee-bearing fallback redeem
2299
+ * permanently under-credited the user by exactly the fee. Surface
2300
+ * the fallback lockId here so submit can route the bind to the
2301
+ * correct ledger row.
2302
+ */
2303
+ lockId?: string;
2276
2304
  };
2277
2305
  /**
2278
2306
  * EIP-7702 authorization tuple — present only on the `delegate`
@@ -2493,6 +2521,21 @@ interface PrepareMobileUserOpParams {
2493
2521
  maxFeePerGas?: bigint;
2494
2522
  maxPriorityFeePerGas?: bigint;
2495
2523
  };
2524
+ /**
2525
+ * Optional separate lock id for the FALLBACK path — required when
2526
+ * the upstream handler reserves a DIFFERENT ledger row for the
2527
+ * fallback variant (PTRedeemHandler reserves
2528
+ * `amount` for fallback vs `amount - fee` for sponsored).
2529
+ *
2530
+ * Audit PACI5-21 — when omitted, `handleMobileSubmit` binds the
2531
+ * fallback userOpHash to the outer sponsored `lockId`, which made
2532
+ * `handleRedeemStatus`'s bundler-receipt fallback resolve the
2533
+ * SMALLER sponsored credit against the on-chain burn of the FULL
2534
+ * amount. Every fee-bearing fallback redeem permanently
2535
+ * under-credited the user by exactly the fee. Pass the fallback
2536
+ * lockId here so submit can route the bind to the correct row.
2537
+ */
2538
+ lockIdFallback?: string;
2496
2539
  /** Paymaster sponsorship response, or `undefined` if PAFI declined. */
2497
2540
  paymasterFields?: {
2498
2541
  paymaster: Address;
@@ -2606,6 +2649,13 @@ interface HandleMobilePrepareParams {
2606
2649
  partialUserOp: PartialUserOperation;
2607
2650
  /** Optional fee-stripped fallback variant. */
2608
2651
  partialUserOpFallback?: PartialUserOperation;
2652
+ /**
2653
+ * Optional separate lock id for the fallback path. Required when the
2654
+ * upstream handler (e.g. PTRedeemHandler) reserves a DIFFERENT
2655
+ * ledger row for the fallback variant. See audit PACI5-21 +
2656
+ * `prepareMobileUserOp` for the full rationale.
2657
+ */
2658
+ lockIdFallback?: string;
2609
2659
  /**
2610
2660
  * Scenario tag — passed to `requestSponsorship` so the relayer can
2611
2661
  * apply per-scenario L1 enforcement (`mint`, `burn`, etc.).
@@ -2732,7 +2782,7 @@ interface IssuerStateValidatorLike {
2732
2782
  * own composer — gg56 uses a timestamp-key 2D nonce). Caller layers
2733
2783
  * paymaster sponsorship + sponsorAuth on top of the returned UserOps.
2734
2784
  */
2735
- type PTClaimErrorCode = "INVALID_AMOUNT" | "VALIDATION_FAILED" | "BUILD_FAILED" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT";
2785
+ type PTClaimErrorCode = "INVALID_AMOUNT" | "VALIDATION_FAILED" | "BUILD_FAILED" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "UNSUPPORTED_POINT_TOKEN";
2736
2786
  declare class PTClaimError extends PafiSdkError {
2737
2787
  readonly httpStatus: "unprocessable";
2738
2788
  readonly code: PTClaimErrorCode;
@@ -2754,6 +2804,23 @@ interface PTClaimHandlerConfig {
2754
2804
  * `chainId` + `pointTokenAddress` respectively.
2755
2805
  */
2756
2806
  domainResolver: PointTokenDomainResolver;
2807
+ /**
2808
+ * Issuer's allow-listed PointToken contracts (checksummed EIP-55).
2809
+ * Every `handle()` call validates the request's `pointTokenAddress`
2810
+ * against this set BEFORE any chain read or signer call.
2811
+ *
2812
+ * write surface (claim/redeem) previously skipped it. The asymmetry
2813
+ * meant an issuer signer whitelisted as minter on PointTokens
2814
+ * outside the configured indexer set could be coerced into signing
2815
+ * a valid `MintForRequest` for an off-set token — the on-chain
2816
+ * mint would succeed, but no PointIndexer was watching to debit the
2817
+ * off-chain ledger. Silent supply-invariant violation.
2818
+ *
2819
+ * Pass the SAME set used to construct `IssuerApiHandlers` so the
2820
+ * read and write paths agree on what this issuer is willing to
2821
+ * sign for.
2822
+ */
2823
+ supportedTokens: ReadonlySet<Address>;
2757
2824
  /** Optional — when wired, used to estimate the PT gas-reimbursement fee. */
2758
2825
  feeService?: FeeManager;
2759
2826
  /** Optional — pre-validates issuer status + cap before locking balance. */
@@ -3283,6 +3350,18 @@ interface MobilePrepareDto {
3283
3350
  interface RedeemPrepareDto extends MobilePrepareDto {
3284
3351
  netCreditAmount: string;
3285
3352
  netCreditAmountFallback?: string;
3353
+ /**
3354
+ * Lock id reserved for the FALLBACK redeem path (= full `amount`).
3355
+ * Mobile FE polls `/redeem/status/:lockIdFallback` when it submits
3356
+ * the fallback variant (`variant: 'fallback'` on `/redeem/submit`).
3357
+ *
3358
+ * Audit PACI5-21 — pre-fix the adapter exposed only the sponsored
3359
+ * `lockId` (= `amount - fee`), so the bundler-receipt fallback in
3360
+ * `handleRedeemStatus` resolved the smaller credit against the
3361
+ * on-chain burn of the full amount → user under-credited by exactly
3362
+ * the fee on every fallback redeem.
3363
+ */
3364
+ lockIdFallback?: string;
3286
3365
  }
3287
3366
  interface MobileSubmitDto {
3288
3367
  userOpHash: Hex;
package/dist/index.d.ts CHANGED
@@ -1884,6 +1884,16 @@ interface PTRedeemHandlerConfig {
1884
1884
  * provisioning time. Typically HSM/KMS-backed in prod.
1885
1885
  */
1886
1886
  burnerSignerWallet: WalletClient;
1887
+ /**
1888
+ * Issuer's allow-listed PointToken contracts (checksummed EIP-55).
1889
+ * Every `handle()` call validates the request's `pointTokenAddress`
1890
+ * against this set BEFORE any chain read, signer call, or pending
1891
+ * credit reservation.
1892
+ * Pass the SAME set used to construct `IssuerApiHandlers` and
1893
+ * `PTClaimHandler` so the read, claim, and redeem paths agree on
1894
+ * what this issuer is willing to sign for.
1895
+ */
1896
+ supportedTokens: ReadonlySet<Address>;
1887
1897
  /**
1888
1898
  * Optional — when wired, used to estimate the PT gas-reimbursement
1889
1899
  * fee. The handler self-computes `feeAmount` + `feeRecipient` so the
@@ -1977,7 +1987,7 @@ interface PTRedeemResponse {
1977
1987
  /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1978
1988
  signatureDeadline: bigint;
1979
1989
  }
1980
- type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED";
1990
+ type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "UNSUPPORTED_POINT_TOKEN";
1981
1991
  declare class PTRedeemError extends PafiSdkError {
1982
1992
  readonly httpStatus: "unprocessable";
1983
1993
  readonly code: PTRedeemErrorCode;
@@ -1995,6 +2005,7 @@ declare class PTRedeemHandler {
1995
2005
  private readonly chainId;
1996
2006
  private readonly domainResolver;
1997
2007
  private readonly burnerSignerWallet;
2008
+ private readonly supportedTokens;
1998
2009
  private readonly redeemLockDurationMs;
1999
2010
  private readonly signatureDeadlineSeconds;
2000
2011
  private readonly now;
@@ -2273,6 +2284,23 @@ interface PendingUserOpEntry {
2273
2284
  verificationGasLimit: string;
2274
2285
  preVerificationGas: string;
2275
2286
  userOpHash: Hex;
2287
+ /**
2288
+ * Lock id (PendingCredit on redeem, LockedMint on claim) reserved
2289
+ * for the FALLBACK path — distinct from the outer entry's
2290
+ * sponsored lock because the on-chain amount the user burns/mints
2291
+ * differs between variants.
2292
+ *
2293
+ * Audit PACI5-21 — pre-fix `handleMobileSubmit.bindUserOpHash`
2294
+ * always bound the submitted userOpHash to the SPONSORED lockId,
2295
+ * even when the user submitted the fallback variant. The
2296
+ * bundler-receipt fallback in `handleRedeemStatus` then resolved
2297
+ * the sponsored credit (`amount - fee`) against the on-chain
2298
+ * burn of the FULL `amount` — every fee-bearing fallback redeem
2299
+ * permanently under-credited the user by exactly the fee. Surface
2300
+ * the fallback lockId here so submit can route the bind to the
2301
+ * correct ledger row.
2302
+ */
2303
+ lockId?: string;
2276
2304
  };
2277
2305
  /**
2278
2306
  * EIP-7702 authorization tuple — present only on the `delegate`
@@ -2493,6 +2521,21 @@ interface PrepareMobileUserOpParams {
2493
2521
  maxFeePerGas?: bigint;
2494
2522
  maxPriorityFeePerGas?: bigint;
2495
2523
  };
2524
+ /**
2525
+ * Optional separate lock id for the FALLBACK path — required when
2526
+ * the upstream handler reserves a DIFFERENT ledger row for the
2527
+ * fallback variant (PTRedeemHandler reserves
2528
+ * `amount` for fallback vs `amount - fee` for sponsored).
2529
+ *
2530
+ * Audit PACI5-21 — when omitted, `handleMobileSubmit` binds the
2531
+ * fallback userOpHash to the outer sponsored `lockId`, which made
2532
+ * `handleRedeemStatus`'s bundler-receipt fallback resolve the
2533
+ * SMALLER sponsored credit against the on-chain burn of the FULL
2534
+ * amount. Every fee-bearing fallback redeem permanently
2535
+ * under-credited the user by exactly the fee. Pass the fallback
2536
+ * lockId here so submit can route the bind to the correct row.
2537
+ */
2538
+ lockIdFallback?: string;
2496
2539
  /** Paymaster sponsorship response, or `undefined` if PAFI declined. */
2497
2540
  paymasterFields?: {
2498
2541
  paymaster: Address;
@@ -2606,6 +2649,13 @@ interface HandleMobilePrepareParams {
2606
2649
  partialUserOp: PartialUserOperation;
2607
2650
  /** Optional fee-stripped fallback variant. */
2608
2651
  partialUserOpFallback?: PartialUserOperation;
2652
+ /**
2653
+ * Optional separate lock id for the fallback path. Required when the
2654
+ * upstream handler (e.g. PTRedeemHandler) reserves a DIFFERENT
2655
+ * ledger row for the fallback variant. See audit PACI5-21 +
2656
+ * `prepareMobileUserOp` for the full rationale.
2657
+ */
2658
+ lockIdFallback?: string;
2609
2659
  /**
2610
2660
  * Scenario tag — passed to `requestSponsorship` so the relayer can
2611
2661
  * apply per-scenario L1 enforcement (`mint`, `burn`, etc.).
@@ -2732,7 +2782,7 @@ interface IssuerStateValidatorLike {
2732
2782
  * own composer — gg56 uses a timestamp-key 2D nonce). Caller layers
2733
2783
  * paymaster sponsorship + sponsorAuth on top of the returned UserOps.
2734
2784
  */
2735
- type PTClaimErrorCode = "INVALID_AMOUNT" | "VALIDATION_FAILED" | "BUILD_FAILED" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT";
2785
+ type PTClaimErrorCode = "INVALID_AMOUNT" | "VALIDATION_FAILED" | "BUILD_FAILED" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "UNSUPPORTED_POINT_TOKEN";
2736
2786
  declare class PTClaimError extends PafiSdkError {
2737
2787
  readonly httpStatus: "unprocessable";
2738
2788
  readonly code: PTClaimErrorCode;
@@ -2754,6 +2804,23 @@ interface PTClaimHandlerConfig {
2754
2804
  * `chainId` + `pointTokenAddress` respectively.
2755
2805
  */
2756
2806
  domainResolver: PointTokenDomainResolver;
2807
+ /**
2808
+ * Issuer's allow-listed PointToken contracts (checksummed EIP-55).
2809
+ * Every `handle()` call validates the request's `pointTokenAddress`
2810
+ * against this set BEFORE any chain read or signer call.
2811
+ *
2812
+ * write surface (claim/redeem) previously skipped it. The asymmetry
2813
+ * meant an issuer signer whitelisted as minter on PointTokens
2814
+ * outside the configured indexer set could be coerced into signing
2815
+ * a valid `MintForRequest` for an off-set token — the on-chain
2816
+ * mint would succeed, but no PointIndexer was watching to debit the
2817
+ * off-chain ledger. Silent supply-invariant violation.
2818
+ *
2819
+ * Pass the SAME set used to construct `IssuerApiHandlers` so the
2820
+ * read and write paths agree on what this issuer is willing to
2821
+ * sign for.
2822
+ */
2823
+ supportedTokens: ReadonlySet<Address>;
2757
2824
  /** Optional — when wired, used to estimate the PT gas-reimbursement fee. */
2758
2825
  feeService?: FeeManager;
2759
2826
  /** Optional — pre-validates issuer status + cap before locking balance. */
@@ -3283,6 +3350,18 @@ interface MobilePrepareDto {
3283
3350
  interface RedeemPrepareDto extends MobilePrepareDto {
3284
3351
  netCreditAmount: string;
3285
3352
  netCreditAmountFallback?: string;
3353
+ /**
3354
+ * Lock id reserved for the FALLBACK redeem path (= full `amount`).
3355
+ * Mobile FE polls `/redeem/status/:lockIdFallback` when it submits
3356
+ * the fallback variant (`variant: 'fallback'` on `/redeem/submit`).
3357
+ *
3358
+ * Audit PACI5-21 — pre-fix the adapter exposed only the sponsored
3359
+ * `lockId` (= `amount - fee`), so the bundler-receipt fallback in
3360
+ * `handleRedeemStatus` resolved the smaller credit against the
3361
+ * on-chain burn of the full amount → user under-credited by exactly
3362
+ * the fee on every fallback redeem.
3363
+ */
3364
+ lockIdFallback?: string;
3286
3365
  }
3287
3366
  interface MobileSubmitDto {
3288
3367
  userOpHash: Hex;
package/dist/index.js CHANGED
@@ -2108,6 +2108,7 @@ var PTRedeemHandler = class {
2108
2108
  chainId;
2109
2109
  domainResolver;
2110
2110
  burnerSignerWallet;
2111
+ supportedTokens;
2111
2112
  redeemLockDurationMs;
2112
2113
  signatureDeadlineSeconds;
2113
2114
  now;
@@ -2147,6 +2148,13 @@ var PTRedeemHandler = class {
2147
2148
  this.chainId = config.chainId;
2148
2149
  this.domainResolver = config.domainResolver;
2149
2150
  this.burnerSignerWallet = config.burnerSignerWallet;
2151
+ if (!config.supportedTokens) {
2152
+ throw new PTRedeemError(
2153
+ "UNSUPPORTED_POINT_TOKEN",
2154
+ "PTRedeemHandler requires `supportedTokens` (issuer's allow-listed PointToken contracts). See audit PACI5-18."
2155
+ );
2156
+ }
2157
+ this.supportedTokens = config.supportedTokens;
2150
2158
  if (this.burnerSignerWallet?.account?.type === "local") {
2151
2159
  console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
2152
2160
  }
@@ -2175,6 +2183,12 @@ var PTRedeemHandler = class {
2175
2183
  throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
2176
2184
  }
2177
2185
  const pointTokenAddress = getAddress7(request.pointTokenAddress);
2186
+ if (!this.supportedTokens.has(pointTokenAddress)) {
2187
+ throw new PTRedeemError(
2188
+ "UNSUPPORTED_POINT_TOKEN",
2189
+ `redeem: pointTokenAddress ${pointTokenAddress} is not in the issuer's supported-token allowlist. Check IssuerApiHandlers.supportedTokens and PTRedeemHandler.config.supportedTokens point at the same set.`
2190
+ );
2191
+ }
2178
2192
  if (this.redemptionService) {
2179
2193
  const decision = await this.redemptionService.evaluate(
2180
2194
  request.userAddress,
@@ -2671,7 +2685,10 @@ async function prepareMobileUserOp(params) {
2671
2685
  callGasLimit: fallback.userOp.callGasLimit.toString(),
2672
2686
  verificationGasLimit: fallback.userOp.verificationGasLimit.toString(),
2673
2687
  preVerificationGas: fallback.userOp.preVerificationGas.toString(),
2674
- userOpHash: fallback.userOpHash
2688
+ userOpHash: fallback.userOpHash,
2689
+ // Audit PACI5-21 — carry the fallback-specific lockId so submit
2690
+ // can bind the fallback userOpHash to the correct ledger row.
2691
+ lockId: params.lockIdFallback
2675
2692
  };
2676
2693
  }
2677
2694
  const entry = {
@@ -2864,6 +2881,7 @@ async function handleMobilePrepare(params) {
2864
2881
  lockId: params.lockId,
2865
2882
  partialUserOp: sponsoredOp,
2866
2883
  partialUserOpFallback: params.partialUserOpFallback,
2884
+ lockIdFallback: params.lockIdFallback,
2867
2885
  paymasterFields,
2868
2886
  chainId: params.chainId,
2869
2887
  store: params.store,
@@ -2892,7 +2910,8 @@ async function handleMobileSubmit(params) {
2892
2910
  entryPoint: params.entryPoint ?? ENTRY_POINT_V08,
2893
2911
  eip7702Auth: entry.eip7702Auth
2894
2912
  });
2895
- await params.bindUserOpHash(params.lockId, result.userOpHash);
2913
+ const targetLockId = variant === "fallback" && entry.fallback?.lockId ? entry.fallback.lockId : params.lockId;
2914
+ await params.bindUserOpHash(targetLockId, result.userOpHash);
2896
2915
  await params.store.delete(params.lockId);
2897
2916
  return { userOpHash: result.userOpHash };
2898
2917
  }
@@ -2942,6 +2961,12 @@ var PTClaimHandler = class {
2942
2961
  cfg;
2943
2962
  inFlightNonces = /* @__PURE__ */ new Map();
2944
2963
  constructor(config) {
2964
+ if (!config.supportedTokens) {
2965
+ throw new PTClaimError(
2966
+ "UNSUPPORTED_POINT_TOKEN",
2967
+ "PTClaimHandler requires `supportedTokens` (issuer's allow-listed PointToken contracts). See audit PACI5-18."
2968
+ );
2969
+ }
2945
2970
  const lockDurationMs = config.lockDurationMs ?? DEFAULT_LOCK_MS;
2946
2971
  const signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC2;
2947
2972
  const maxAllowedSignatureMs = lockDurationMs - M11_SAFETY_MARGIN_MS2;
@@ -2973,6 +2998,14 @@ var PTClaimHandler = class {
2973
2998
  if (request.amount <= 0n) {
2974
2999
  throw new PTClaimError("INVALID_AMOUNT", "claim amount must be positive");
2975
3000
  }
3001
+ const pointTokenAddress = getAddress9(request.pointTokenAddress);
3002
+ if (!this.cfg.supportedTokens.has(pointTokenAddress)) {
3003
+ throw new PTClaimError(
3004
+ "UNSUPPORTED_POINT_TOKEN",
3005
+ `claim: pointTokenAddress ${pointTokenAddress} is not in the issuer's supported-token allowlist. Check IssuerApiHandlers.supportedTokens and PTClaimHandler.config.supportedTokens point at the same set.`,
3006
+ { requested: pointTokenAddress }
3007
+ );
3008
+ }
2976
3009
  if (this.cfg.issuerStateValidator) {
2977
3010
  try {
2978
3011
  await this.cfg.issuerStateValidator.preValidateMint(
@@ -3648,10 +3681,16 @@ var IssuerApiAdapter = class {
3648
3681
  "burn",
3649
3682
  pointTokenAddress,
3650
3683
  redeemResponse.expiresInSeconds,
3651
- input.eip7702Auth
3684
+ input.eip7702Auth,
3685
+ // Audit PACI5-21 — fallback path reserves a separate
3686
+ // PendingCredit row for the full `amount`. Surface its lockId so
3687
+ // mobile FE can poll the correct row + `handleMobileSubmit`
3688
+ // routes the userOpHash bind to it on fallback submit.
3689
+ redeemResponse.fallback?.lockId
3652
3690
  );
3653
3691
  return {
3654
3692
  lockId: redeemResponse.lockId,
3693
+ lockIdFallback: redeemResponse.fallback?.lockId,
3655
3694
  userOpHash: prepared.sponsored.userOpHash,
3656
3695
  typedData: prepared.sponsored.typedData,
3657
3696
  userOpHashFallback: prepared.fallback?.userOpHash,
@@ -3782,13 +3821,14 @@ var IssuerApiAdapter = class {
3782
3821
  issuerSignerWallet: this.cfg.issuerSignerWallet
3783
3822
  });
3784
3823
  }
3785
- async runMobilePrepare(authenticatedAddress, chainId, lockId, partialUserOp, partialUserOpFallback, scenario, pointTokenAddress, ttlSeconds, eip7702Auth) {
3824
+ async runMobilePrepare(authenticatedAddress, chainId, lockId, partialUserOp, partialUserOpFallback, scenario, pointTokenAddress, ttlSeconds, eip7702Auth, lockIdFallback) {
3786
3825
  return await handleMobilePrepare({
3787
3826
  userAddress: authenticatedAddress,
3788
3827
  chainId,
3789
3828
  lockId,
3790
3829
  partialUserOp,
3791
3830
  partialUserOpFallback,
3831
+ lockIdFallback,
3792
3832
  scenario,
3793
3833
  pointTokenAddress,
3794
3834
  ttlSeconds,
@@ -5059,7 +5099,7 @@ var MemoryRedemptionHistoryStore = class {
5059
5099
  };
5060
5100
 
5061
5101
  // src/index.ts
5062
- var PAFI_ISSUER_SDK_VERSION = true ? "0.28.1" : "dev";
5102
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.31.0" : "dev";
5063
5103
  export {
5064
5104
  AdapterMisconfiguredError,
5065
5105
  AuthError,