@pafi-dev/issuer 0.3.0-beta.0 → 0.3.0-beta.10

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.js CHANGED
@@ -582,209 +582,103 @@ var RelayError = class extends Error {
582
582
  };
583
583
 
584
584
  // src/relay/relayService.ts
585
- import { encodeFunctionData } from "viem";
586
585
  import {
587
- relayAbi,
588
- encodeMintAndSwap,
589
- simulateMintAndSwap as coreSimulateMintAndSwap,
590
- SimulationError,
591
- RELAYER_V2_ABI,
586
+ encodeFunctionData,
587
+ erc20Abi
588
+ } from "viem";
589
+ import {
592
590
  POINT_TOKEN_V2_ABI,
593
- buildPartialUserOperation
591
+ buildPartialUserOperation,
592
+ signMintRequest as signMintRequest2
594
593
  } from "@pafi-dev/core";
595
- var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
596
594
  var RelayService = class {
597
- relayAddress;
598
- operatorWallet;
599
- provider;
600
- confirmationTimeoutMs;
601
- simulateBeforeSubmit;
602
- constructor(config) {
603
- if (!config.relayAddress) {
604
- throw new Error("RelayService: relayAddress is required");
605
- }
606
- if (!config.operatorWallet) {
607
- throw new Error("RelayService: operatorWallet is required");
608
- }
609
- this.relayAddress = config.relayAddress;
610
- this.operatorWallet = config.operatorWallet;
611
- if (config.provider) this.provider = config.provider;
612
- this.confirmationTimeoutMs = config.confirmationTimeoutMs ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
613
- this.simulateBeforeSubmit = config.simulateBeforeSubmit ?? config.provider !== void 0;
614
- }
615
- /** Address the operator wallet is broadcasting from (for logging). */
616
- operatorAddress() {
617
- return this.operatorWallet.account?.address;
618
- }
619
595
  /**
620
- * Build calldata for the Relay `mintAndSwap` function. Kept public so
621
- * callers (e.g. the MintingGateway) can log or persist the encoded call
622
- * for audit before broadcasting.
596
+ * Build an unsigned UserOp for Scenario 1 (Mint) sig-gated
597
+ * `PointToken.mint(to, amount, deadline, minterSig)`.
623
598
  */
624
- encodeCall(params) {
625
- try {
626
- return encodeMintAndSwap(params.mint, params.swap);
627
- } catch (err) {
599
+ async prepareMint(params) {
600
+ if (!params.batchExecutorAddress) {
628
601
  throw new RelayError(
629
602
  "ENCODE_FAILED",
630
- `Failed to encode mintAndSwap calldata: ${errorMessage(err)}`,
631
- err
603
+ "prepareMint: batchExecutorAddress required"
632
604
  );
633
605
  }
634
- }
635
- /**
636
- * Submit a `mintAndSwap` transaction. Flow:
637
- *
638
- * 1. (optional) pre-flight simulate via provider
639
- * 2. writeContract through the operator wallet
640
- * 3. (optional) wait for the receipt and surface gasUsed / status
641
- *
642
- * Throws a typed `RelayError` on any failure so the MintingGateway can
643
- * decide whether to release the ledger lock (`SUBMIT_FAILED` and
644
- * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
645
- * need manual review because the tx may still land).
646
- *
647
- * @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
648
- * `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
649
- * still needs to finalize Relayer v2 ABI before the replacements
650
- * can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
651
- */
652
- async submitMintAndSwap(params) {
653
- if (this.simulateBeforeSubmit && this.provider) {
654
- const operatorAddr = this.operatorWallet.account?.address;
655
- if (operatorAddr) {
656
- try {
657
- await coreSimulateMintAndSwap(
658
- this.provider,
659
- this.relayAddress,
660
- params.mint,
661
- params.swap,
662
- operatorAddr
663
- );
664
- } catch (err) {
665
- const reason = err instanceof SimulationError ? err.reason : errorMessage(err);
666
- throw new RelayError(
667
- "SIMULATION_FAILED",
668
- `mintAndSwap would revert: ${reason}`,
669
- err
670
- );
671
- }
672
- }
606
+ if (!params.userAddress) {
607
+ throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
673
608
  }
674
- let txHash;
675
- try {
676
- txHash = await this.operatorWallet.writeContract({
677
- address: this.relayAddress,
678
- abi: relayAbi,
679
- functionName: "mintAndSwap",
680
- args: [params.mint, params.swap],
681
- ...this.operatorWallet.account ? { account: this.operatorWallet.account } : {}
682
- });
683
- } catch (err) {
609
+ if (!params.pointTokenAddress) {
684
610
  throw new RelayError(
685
- "SUBMIT_FAILED",
686
- `Failed to broadcast mintAndSwap: ${errorMessage(err)}`,
687
- err
611
+ "ENCODE_FAILED",
612
+ "prepareMint: pointTokenAddress required"
688
613
  );
689
614
  }
690
- if (!this.provider) {
691
- return { txHash };
615
+ if (params.amount <= 0n) {
616
+ throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
692
617
  }
693
- try {
694
- const receipt = await this.provider.waitForTransactionReceipt({
695
- hash: txHash,
696
- timeout: this.confirmationTimeoutMs
697
- });
698
- if (receipt.status !== "success") {
699
- throw new RelayError(
700
- "TX_REVERTED",
701
- `mintAndSwap reverted on-chain (tx=${txHash})`
702
- );
703
- }
704
- return {
705
- txHash,
706
- blockNumber: receipt.blockNumber,
707
- gasUsed: receipt.gasUsed,
708
- status: receipt.status
709
- };
710
- } catch (err) {
711
- if (err instanceof RelayError) throw err;
618
+ if (!params.issuerSignerWallet) {
712
619
  throw new RelayError(
713
- "TIMEOUT",
714
- `Timed out waiting for mintAndSwap receipt (tx=${txHash}): ${errorMessage(err)}`,
715
- err
620
+ "ENCODE_FAILED",
621
+ "prepareMint: issuerSignerWallet required (for MintRequest EIP-712 signature)"
716
622
  );
717
623
  }
718
- }
719
- // ==========================================================================
720
- // v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
721
- // ==========================================================================
722
- //
723
- // These two methods build unsigned `PartialUserOperation` payloads for
724
- // the Frontend to sign (via Privy) and submit to the Bundler. The
725
- // Issuer Backend no longer broadcasts — that's the Frontend's job.
726
- //
727
- // Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
728
- // When SC delivers real ABIs, the imports swap but these method bodies
729
- // stay the same (calldata encoder is ABI-driven).
730
- // ==========================================================================
731
- /**
732
- * Build an unsigned UserOp for Scenario 1 (Mint).
733
- *
734
- * Flow:
735
- * 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
736
- * 2. Optionally append a PT fee transfer from user → feeRecipient
737
- * (fee recovery happens on-chain via BatchExecutor, not via an
738
- * operator wallet)
739
- * 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
740
- * 4. Return a `PartialUserOperation` ready for:
741
- * - gas estimation (Bundler)
742
- * - paymaster sponsorship (PAFI Backend)
743
- * - user signature (Privy)
744
- */
745
- prepareMint(params) {
746
- if (!params.relayerAddress) {
747
- throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
624
+ if (params.deadline <= 0n) {
625
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
748
626
  }
749
- if (!params.batchExecutorAddress) {
627
+ let minterSig;
628
+ try {
629
+ const sig = await signMintRequest2(
630
+ params.issuerSignerWallet,
631
+ params.domain,
632
+ {
633
+ to: params.userAddress,
634
+ amount: params.amount,
635
+ nonce: params.mintRequestNonce,
636
+ deadline: params.deadline
637
+ }
638
+ );
639
+ minterSig = sig.serialized;
640
+ } catch (err) {
750
641
  throw new RelayError(
751
642
  "ENCODE_FAILED",
752
- "prepareMint: batchExecutorAddress required"
643
+ `prepareMint: failed to sign MintRequest: ${errorMessage(err)}`,
644
+ err
753
645
  );
754
646
  }
755
- if (!params.userAddress) {
756
- throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
757
- }
758
647
  let mintCallData;
759
648
  try {
760
649
  mintCallData = encodeFunctionData({
761
- abi: RELAYER_V2_ABI,
650
+ abi: POINT_TOKEN_V2_ABI,
762
651
  functionName: "mint",
763
- args: [params.mintRequest, params.userSignature, params.issuerSignature]
652
+ args: [params.userAddress, params.amount, params.deadline, minterSig]
764
653
  });
765
654
  } catch (err) {
766
655
  throw new RelayError(
767
656
  "ENCODE_FAILED",
768
- `prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
657
+ `prepareMint: failed to encode PointToken.mint: ${errorMessage(err)}`,
769
658
  err
770
659
  );
771
660
  }
772
661
  const operations = [
773
662
  {
774
- target: params.relayerAddress,
663
+ target: params.pointTokenAddress,
775
664
  value: 0n,
776
665
  data: mintCallData
777
666
  }
778
667
  ];
779
- if (params.mintRequest.feeAmount > 0n) {
668
+ if (params.feeAmount && params.feeAmount > 0n) {
669
+ if (!params.feeRecipient) {
670
+ throw new RelayError(
671
+ "ENCODE_FAILED",
672
+ "prepareMint: feeRecipient required when feeAmount > 0"
673
+ );
674
+ }
780
675
  operations.push({
781
676
  target: params.pointTokenAddress,
782
677
  value: 0n,
783
678
  data: encodeFunctionData({
784
- abi: POINT_TOKEN_V2_ABI,
785
- functionName: "balanceOf",
786
- // placeholder — real impl uses transfer
787
- args: [params.mintRequest.feeRecipient]
679
+ abi: erc20Abi,
680
+ functionName: "transfer",
681
+ args: [params.feeRecipient, params.feeAmount]
788
682
  })
789
683
  });
790
684
  }
@@ -793,7 +687,7 @@ var RelayService = class {
793
687
  nonce: params.aaNonce,
794
688
  operations,
795
689
  gasLimits: {
796
- callGasLimit: params.callGasLimit ?? 500000n,
690
+ callGasLimit: params.callGasLimit ?? 300000n,
797
691
  verificationGasLimit: params.verificationGasLimit ?? 150000n,
798
692
  preVerificationGas: params.preVerificationGas ?? 50000n
799
693
  }
@@ -803,13 +697,12 @@ var RelayService = class {
803
697
  * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
804
698
  *
805
699
  * Two modes:
806
- * - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
807
- * via EIP-7702 delegation is the user, so no signature needed
808
- * on-chain (the BurnConsent was already verified off-chain by
809
- * the issuer backend before we got here)
810
- * - `mode: 'burnWithSig'` `PointToken.burnWithSig(consent, sig)`;
811
- * used when the issuer hasn't verified the consent and the
812
- * contract has to do it on-chain
700
+ * - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
701
+ * usable if the caller (via EIP-7702) is whitelisted as a burner.
702
+ * Rare in v1.4; kept for admin/operator tools.
703
+ * - `mode: 'burnWithSig'` `PointToken.burn(from, amount, deadline,
704
+ * burnerSig)`. Caller provides a pre-signed `BurnRequest` + sig
705
+ * bytes (typically from `PTRedeemHandler`).
813
706
  */
814
707
  prepareBurn(params) {
815
708
  if (!params.pointTokenAddress) {
@@ -824,19 +717,24 @@ var RelayService = class {
824
717
  let burnCallData;
825
718
  try {
826
719
  if (params.mode === "burnWithSig") {
827
- if (!params.burnConsent || !params.consentSignature) {
828
- throw new Error("burnWithSig requires burnConsent + consentSignature");
720
+ if (!params.burnRequest || !params.burnerSignature) {
721
+ throw new Error("burnWithSig requires burnRequest + burnerSignature");
829
722
  }
830
723
  burnCallData = encodeFunctionData({
831
724
  abi: POINT_TOKEN_V2_ABI,
832
- functionName: "burnWithSig",
833
- args: [params.burnConsent, params.consentSignature]
725
+ functionName: "burn",
726
+ args: [
727
+ params.burnRequest.from,
728
+ params.burnRequest.amount,
729
+ params.burnRequest.deadline,
730
+ params.burnerSignature
731
+ ]
834
732
  });
835
733
  } else {
836
734
  burnCallData = encodeFunctionData({
837
735
  abi: POINT_TOKEN_V2_ABI,
838
736
  functionName: "burn",
839
- args: [params.amount]
737
+ args: [params.userAddress, params.amount]
840
738
  });
841
739
  }
842
740
  } catch (err) {
@@ -906,255 +804,6 @@ var FeeManager = class {
906
804
  }
907
805
  };
908
806
 
909
- // src/gateway/types.ts
910
- var MintingGatewayError = class extends Error {
911
- code;
912
- /**
913
- * True if the ledger lock was released before this error was thrown,
914
- * meaning the user can safely retry. False means the funds are still
915
- * locked (e.g. tx may still land on-chain) and retry would double-spend.
916
- */
917
- safeToRetry;
918
- cause;
919
- constructor(code, message, opts) {
920
- super(message);
921
- this.name = "MintingGatewayError";
922
- this.code = code;
923
- this.safeToRetry = opts.safeToRetry;
924
- if (opts.cause !== void 0) this.cause = opts.cause;
925
- }
926
- };
927
-
928
- // src/gateway/mintingGateway.ts
929
- import {
930
- verifyReceiverConsent,
931
- encodeExtData
932
- } from "@pafi-dev/core";
933
- var DEFAULT_LOCK_BUFFER_MS = 6e4;
934
- var MintingGateway = class {
935
- ledger;
936
- policy;
937
- signer;
938
- relayService;
939
- now;
940
- defaultLockBufferMs;
941
- constructor(config) {
942
- if (!config.ledger) throw new Error("MintingGateway: ledger required");
943
- if (!config.policy) throw new Error("MintingGateway: policy required");
944
- if (!config.signer) throw new Error("MintingGateway: signer required");
945
- if (!config.relayService)
946
- throw new Error("MintingGateway: relayService required");
947
- this.ledger = config.ledger;
948
- this.policy = config.policy;
949
- this.signer = config.signer;
950
- this.relayService = config.relayService;
951
- this.now = config.now ?? (() => Date.now());
952
- this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
953
- }
954
- /**
955
- * @deprecated Since 0.3.0 — will be renamed to `processMint()` once
956
- * the SC team finalizes Relayer v2 ABI. The new flow drops the
957
- * swap steps entirely (no more single-call mint+swap); users swap
958
- * separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
959
- */
960
- async processMintAndCashOut(request) {
961
- const { receiverConsent, receiverSignature } = request;
962
- if (!receiverConsent || !receiverSignature) {
963
- throw new MintingGatewayError(
964
- "INVALID_REQUEST",
965
- "receiverConsent and receiverSignature are required",
966
- { safeToRetry: true }
967
- );
968
- }
969
- if (receiverConsent.amount <= 0n) {
970
- throw new MintingGatewayError(
971
- "INVALID_REQUEST",
972
- "consent amount must be positive",
973
- { safeToRetry: true }
974
- );
975
- }
976
- if (receiverConsent.originalReceiver !== request.userAddress) {
977
- throw new MintingGatewayError(
978
- "INVALID_REQUEST",
979
- "consent.originalReceiver must equal request.userAddress",
980
- { safeToRetry: true }
981
- );
982
- }
983
- const nowSec = BigInt(Math.floor(this.now() / 1e3));
984
- if (receiverConsent.deadline <= nowSec) {
985
- throw new MintingGatewayError(
986
- "CONSENT_EXPIRED",
987
- "ReceiverConsent deadline has already passed",
988
- { safeToRetry: true }
989
- );
990
- }
991
- const consentResult = await verifyReceiverConsent(
992
- request.domain,
993
- receiverConsent,
994
- receiverSignature,
995
- request.userAddress
996
- );
997
- if (!consentResult.isValid) {
998
- throw new MintingGatewayError(
999
- "INVALID_CONSENT_SIGNATURE",
1000
- `ReceiverConsent signature did not recover to ${request.userAddress}`,
1001
- { safeToRetry: true }
1002
- );
1003
- }
1004
- const policyDecision = await this.policy.evaluate({
1005
- userAddress: request.userAddress,
1006
- amount: receiverConsent.amount,
1007
- pointTokenAddress: request.pointTokenAddress,
1008
- chainId: request.chainId
1009
- });
1010
- if (!policyDecision.approved) {
1011
- const code = policyDecision.reason?.toLowerCase().includes("insufficient") ? "INSUFFICIENT_BALANCE" : "POLICY_REJECTED";
1012
- throw new MintingGatewayError(
1013
- code,
1014
- policyDecision.reason ?? "Minting request rejected by policy engine",
1015
- { safeToRetry: true }
1016
- );
1017
- }
1018
- const lockDurationMs = request.lockDurationMs ?? this.computeLockDurationMs(receiverConsent.deadline);
1019
- let lockId;
1020
- try {
1021
- lockId = await this.ledger.lockForMinting(
1022
- request.userAddress,
1023
- receiverConsent.amount,
1024
- lockDurationMs,
1025
- request.pointTokenAddress
1026
- );
1027
- } catch (err) {
1028
- throw new MintingGatewayError(
1029
- "INSUFFICIENT_BALANCE",
1030
- `Failed to lock ledger balance: ${errorMessage2(err)}`,
1031
- { safeToRetry: true, cause: err }
1032
- );
1033
- }
1034
- try {
1035
- let minterSignature;
1036
- try {
1037
- minterSignature = await this.signer.signMintRequest(request.domain, {
1038
- to: request.userAddress,
1039
- amount: receiverConsent.amount,
1040
- nonce: receiverConsent.nonce,
1041
- deadline: receiverConsent.deadline
1042
- });
1043
- } catch (err) {
1044
- await this.releaseLockSafely(lockId);
1045
- throw new MintingGatewayError(
1046
- "SIGNER_FAILED",
1047
- `Issuer signer failed: ${errorMessage2(err)}`,
1048
- { safeToRetry: true, cause: err }
1049
- );
1050
- }
1051
- const mintParams = {
1052
- pointToken: request.pointTokenAddress,
1053
- receiver: request.userAddress,
1054
- amount: receiverConsent.amount,
1055
- deadline: receiverConsent.deadline,
1056
- minterSig: minterSignature.serialized,
1057
- receiverSig: receiverSignature,
1058
- extData: receiverConsent.extData
1059
- };
1060
- const swapParams = {
1061
- path: request.swapPath,
1062
- deadline: request.swapDeadline
1063
- };
1064
- let relayResult;
1065
- try {
1066
- relayResult = await this.relayService.submitMintAndSwap({
1067
- mint: mintParams,
1068
- swap: swapParams
1069
- });
1070
- } catch (err) {
1071
- await this.handleRelayFailure(err, lockId);
1072
- }
1073
- const result = {
1074
- txHash: relayResult.txHash,
1075
- lockId
1076
- };
1077
- if (relayResult.blockNumber !== void 0) {
1078
- result.blockNumber = relayResult.blockNumber;
1079
- }
1080
- if (relayResult.gasUsed !== void 0) {
1081
- result.gasUsed = relayResult.gasUsed;
1082
- }
1083
- return result;
1084
- } catch (err) {
1085
- if (err instanceof MintingGatewayError) throw err;
1086
- await this.releaseLockSafely(lockId);
1087
- throw new MintingGatewayError(
1088
- "RELAY_SUBMIT_FAILED",
1089
- `Unexpected error: ${errorMessage2(err)}`,
1090
- { safeToRetry: true, cause: err }
1091
- );
1092
- }
1093
- }
1094
- // ---------------------------------------------------------------------------
1095
- // Internals
1096
- // ---------------------------------------------------------------------------
1097
- computeLockDurationMs(consentDeadlineSec) {
1098
- const nowMs = this.now();
1099
- const deadlineMs = Number(consentDeadlineSec) * 1e3;
1100
- const remaining = Math.max(0, deadlineMs - nowMs);
1101
- return remaining + this.defaultLockBufferMs;
1102
- }
1103
- /**
1104
- * Map a RelayError to a MintingGatewayError, releasing the lock only
1105
- * when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
1106
- * leave the lock in place because the tx may still be in the mempool
1107
- * or already mined — releasing would enable a double-spend on retry.
1108
- * Always throws.
1109
- */
1110
- async handleRelayFailure(err, lockId) {
1111
- if (err instanceof RelayError) {
1112
- switch (err.code) {
1113
- case "ENCODE_FAILED":
1114
- case "SIMULATION_FAILED":
1115
- case "SUBMIT_FAILED":
1116
- case "NOT_CONFIGURED":
1117
- await this.releaseLockSafely(lockId);
1118
- throw new MintingGatewayError(
1119
- err.code === "SIMULATION_FAILED" ? "RELAY_SIMULATION_FAILED" : "RELAY_SUBMIT_FAILED",
1120
- err.message,
1121
- { safeToRetry: true, cause: err }
1122
- );
1123
- case "TX_REVERTED":
1124
- throw new MintingGatewayError("RELAY_REVERTED", err.message, {
1125
- safeToRetry: false,
1126
- cause: err
1127
- });
1128
- case "TIMEOUT":
1129
- throw new MintingGatewayError("RELAY_TIMEOUT", err.message, {
1130
- safeToRetry: false,
1131
- cause: err
1132
- });
1133
- }
1134
- }
1135
- await this.releaseLockSafely(lockId);
1136
- throw new MintingGatewayError(
1137
- "RELAY_SUBMIT_FAILED",
1138
- `Unexpected relay error: ${errorMessage2(err)}`,
1139
- { safeToRetry: true, cause: err }
1140
- );
1141
- }
1142
- /**
1143
- * Release a lock, swallowing any secondary error. We never want a lock
1144
- * release failure to mask the original error — the lock will auto-expire
1145
- * via TTL anyway.
1146
- */
1147
- async releaseLockSafely(lockId) {
1148
- try {
1149
- await this.ledger.releaseLock(lockId);
1150
- } catch {
1151
- }
1152
- }
1153
- };
1154
- function errorMessage2(err) {
1155
- return err instanceof Error ? err.message : String(err);
1156
- }
1157
-
1158
807
  // src/indexer/types.ts
1159
808
  var InMemoryCursorStore = class {
1160
809
  cursor;
@@ -1509,7 +1158,6 @@ import {
1509
1158
  } from "@pafi-dev/core";
1510
1159
  var IssuerApiHandlers = class {
1511
1160
  authService;
1512
- gateway;
1513
1161
  ledger;
1514
1162
  provider;
1515
1163
  /**
@@ -1528,7 +1176,6 @@ var IssuerApiHandlers = class {
1528
1176
  poolsProvider;
1529
1177
  constructor(config) {
1530
1178
  this.authService = config.authService;
1531
- this.gateway = config.gateway;
1532
1179
  this.ledger = config.ledger;
1533
1180
  this.provider = config.provider;
1534
1181
  const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
@@ -1710,55 +1357,13 @@ var IssuerApiHandlers = class {
1710
1357
  }
1711
1358
  };
1712
1359
  }
1713
- /**
1714
- * `POST /claim-and-swap`
1715
- *
1716
- * @deprecated Since 0.3.0 — the single-call mint-then-swap flow is
1717
- * retired in v1.4. Use the new `handleClaim()` (mint only) and let
1718
- * the user swap separately on PAFI Web. See
1719
- * [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
1720
- * removed in 2.0.
1721
- *
1722
- * Legacy behavior: the terminal handler forwards the verified
1723
- * consent to the MintingGateway, which runs the 11-step flow.
1724
- */
1725
- async handleClaimAndSwap(userAddress, request) {
1726
- if (request.chainId !== this.chainId) {
1727
- throw new Error(
1728
- `handleClaimAndSwap: unsupported chainId ${request.chainId}`
1729
- );
1730
- }
1731
- const pointToken = getAddress6(request.pointTokenAddress);
1732
- if (!this.supportedTokens.has(pointToken)) {
1733
- throw new Error(
1734
- `handleClaimAndSwap: unsupported pointToken ${pointToken}`
1735
- );
1736
- }
1737
- const result = await this.gateway.processMintAndCashOut({
1738
- userAddress: getAddress6(userAddress),
1739
- pointTokenAddress: pointToken,
1740
- chainId: request.chainId,
1741
- domain: request.domain,
1742
- receiverConsent: request.receiverConsent,
1743
- receiverSignature: request.receiverSignature,
1744
- swapPath: request.swapPath,
1745
- swapDeadline: request.swapDeadline
1746
- });
1747
- const response = {
1748
- txHash: result.txHash,
1749
- lockId: result.lockId
1750
- };
1751
- if (result.blockNumber !== void 0)
1752
- response.blockNumber = result.blockNumber;
1753
- if (result.gasUsed !== void 0) response.gasUsed = result.gasUsed;
1754
- return response;
1755
- }
1756
1360
  };
1757
1361
 
1758
1362
  // src/api/handlers/ptRedeemHandler.ts
1759
1363
  import { getAddress as getAddress7 } from "viem";
1760
- import { verifyBurnConsent } from "@pafi-dev/core";
1364
+ import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2 } from "@pafi-dev/core";
1761
1365
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1366
+ var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1762
1367
  var PTRedeemError = class extends Error {
1763
1368
  constructor(code, message) {
1764
1369
  super(message);
@@ -1770,11 +1375,14 @@ var PTRedeemError = class extends Error {
1770
1375
  var PTRedeemHandler = class {
1771
1376
  ledger;
1772
1377
  relayService;
1378
+ provider;
1773
1379
  pointTokenAddress;
1774
1380
  batchExecutorAddress;
1775
1381
  chainId;
1776
1382
  domain;
1383
+ burnerSignerWallet;
1777
1384
  redeemLockDurationMs;
1385
+ signatureDeadlineSeconds;
1778
1386
  now;
1779
1387
  constructor(config) {
1780
1388
  if (!config.ledger.reservePendingCredit) {
@@ -1783,46 +1391,68 @@ var PTRedeemHandler = class {
1783
1391
  "PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
1784
1392
  );
1785
1393
  }
1394
+ if (!config.burnerSignerWallet) {
1395
+ throw new PTRedeemError(
1396
+ "SIGNING_FAILED",
1397
+ "PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
1398
+ );
1399
+ }
1786
1400
  this.ledger = config.ledger;
1787
1401
  this.relayService = config.relayService;
1402
+ this.provider = config.provider;
1788
1403
  this.pointTokenAddress = getAddress7(config.pointTokenAddress);
1789
1404
  this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
1790
1405
  this.chainId = config.chainId;
1791
1406
  this.domain = config.domain;
1407
+ this.burnerSignerWallet = config.burnerSignerWallet;
1792
1408
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1409
+ this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1793
1410
  this.now = config.now ?? (() => Date.now());
1794
1411
  }
1795
1412
  async handle(request) {
1796
1413
  if (request.amount <= 0n) {
1797
- throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
1798
- }
1799
- if (request.consent.amount !== request.amount) {
1800
- throw new PTRedeemError(
1801
- "AMOUNT_MISMATCH",
1802
- `consent.amount (${request.consent.amount}) must match request.amount (${request.amount})`
1803
- );
1414
+ throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1804
1415
  }
1805
- const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
1806
- if (request.consent.deadline <= nowSeconds) {
1416
+ let burnNonce;
1417
+ try {
1418
+ burnNonce = await this.provider.readContract({
1419
+ address: this.pointTokenAddress,
1420
+ abi: POINT_TOKEN_V2_ABI2,
1421
+ functionName: "burnRequestNonces",
1422
+ args: [request.userAddress]
1423
+ });
1424
+ } catch (err) {
1807
1425
  throw new PTRedeemError(
1808
- "EXPIRED_CONSENT",
1809
- `consent deadline (${request.consent.deadline}) already passed`
1426
+ "NONCE_READ_FAILED",
1427
+ `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1810
1428
  );
1811
1429
  }
1812
- const verification = await verifyBurnConsent(
1813
- {
1814
- name: this.domain.name,
1815
- chainId: this.chainId,
1816
- verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1817
- },
1818
- request.consent,
1819
- request.consentSignature,
1820
- request.userAddress
1430
+ const deadline = BigInt(
1431
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1821
1432
  );
1822
- if (!verification.isValid) {
1433
+ const domain = {
1434
+ name: this.domain.name,
1435
+ chainId: this.chainId,
1436
+ verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1437
+ };
1438
+ const burnRequest = {
1439
+ from: request.userAddress,
1440
+ amount: request.amount,
1441
+ nonce: burnNonce,
1442
+ deadline
1443
+ };
1444
+ let burnerSignature;
1445
+ try {
1446
+ const sig = await signBurnRequest(
1447
+ this.burnerSignerWallet,
1448
+ domain,
1449
+ burnRequest
1450
+ );
1451
+ burnerSignature = sig.serialized;
1452
+ } catch (err) {
1823
1453
  throw new PTRedeemError(
1824
- "SIGNATURE_MISMATCH",
1825
- `signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
1454
+ "SIGNING_FAILED",
1455
+ `failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
1826
1456
  );
1827
1457
  }
1828
1458
  const lockId = await this.ledger.reservePendingCredit(
@@ -1837,29 +1467,17 @@ var PTRedeemHandler = class {
1837
1467
  aaNonce: request.aaNonce,
1838
1468
  pointTokenAddress: this.pointTokenAddress,
1839
1469
  batchExecutorAddress: this.batchExecutorAddress,
1840
- burnConsent: request.consent,
1841
- consentSignature: parseSigStruct(request.consentSignature)
1470
+ burnRequest,
1471
+ burnerSignature
1842
1472
  });
1843
1473
  return {
1844
1474
  lockId,
1845
1475
  userOp,
1846
- expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
1476
+ expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
1477
+ signatureDeadline: deadline
1847
1478
  };
1848
1479
  }
1849
1480
  };
1850
- function parseSigStruct(serialized) {
1851
- const raw = serialized.slice(2);
1852
- if (raw.length !== 130) {
1853
- throw new PTRedeemError(
1854
- "INVALID_CONSENT",
1855
- `signature must be 65 bytes, got ${raw.length / 2}`
1856
- );
1857
- }
1858
- const r = `0x${raw.slice(0, 64)}`;
1859
- const s = `0x${raw.slice(64, 128)}`;
1860
- const v = parseInt(raw.slice(128, 130), 16);
1861
- return { v, r, s };
1862
- }
1863
1481
 
1864
1482
  // src/api/handlers/topUpRedemptionHandler.ts
1865
1483
  import { getAddress as getAddress8 } from "viem";
@@ -1905,24 +1523,10 @@ var TopUpRedemptionHandler = class {
1905
1523
  shortfall
1906
1524
  };
1907
1525
  }
1908
- if (request.redeemRequest.consent.amount < shortfall) {
1909
- throw new TopUpRedemptionError(
1910
- "CONSENT_AMOUNT_TOO_LOW",
1911
- `consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
1912
- );
1913
- }
1914
- if (request.redeemRequest.consent.amount !== shortfall) {
1915
- throw new TopUpRedemptionError(
1916
- "CONSENT_AMOUNT_TOO_LOW",
1917
- `consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
1918
- );
1919
- }
1920
1526
  const redeem = await this.ptRedeemHandler.handle({
1921
1527
  userAddress: request.userAddress,
1922
1528
  amount: shortfall,
1923
- consent: request.redeemRequest.consent,
1924
- consentSignature: request.redeemRequest.consentSignature,
1925
- aaNonce: request.redeemRequest.aaNonce
1529
+ aaNonce: request.aaNonce
1926
1530
  });
1927
1531
  return {
1928
1532
  action: "TOP_UP_STARTED",
@@ -2203,28 +1807,11 @@ var PafiBackendError = class extends Error {
2203
1807
  code;
2204
1808
  httpStatus;
2205
1809
  details;
2206
- /**
2207
- * Seconds to wait before retry. Populated from the server body
2208
- * (e.g. rate limit returns the number of seconds until UTC midnight).
2209
- */
2210
1810
  retryAfter;
2211
- /**
2212
- * `safeToRetry` as reported by the server body. Prefer this over the
2213
- * code-based heuristic when available — the server knows more about
2214
- * whether the same request will succeed on retry.
2215
- */
2216
1811
  serverSafeToRetry;
2217
- /**
2218
- * Whether the caller can safely retry the same request.
2219
- *
2220
- * If the server provided `safeToRetry` in the body, trust that.
2221
- * Otherwise fall back to a code-based heuristic.
2222
- */
2223
1812
  get safeToRetry() {
2224
1813
  if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
2225
1814
  switch (this.code) {
2226
- case "PAYMASTER_UNAVAILABLE":
2227
- case "PAYMASTER_TIMEOUT":
2228
1815
  case "RATE_LIMITER_UNAVAILABLE":
2229
1816
  case "INTERNAL_ERROR":
2230
1817
  case "TIMEOUT":
@@ -2233,197 +1820,20 @@ var PafiBackendError = class extends Error {
2233
1820
  case "RATE_LIMIT_EXCEEDED":
2234
1821
  case "RATE_LIMIT_EXCEEDED_DAILY":
2235
1822
  case "RATE_LIMIT_EXCEEDED_PER_USER":
1823
+ case "ISSUER_BUDGET_EXCEEDED":
2236
1824
  return true;
2237
- // after retryAfter
2238
1825
  default:
2239
1826
  return false;
2240
1827
  }
2241
1828
  }
2242
1829
  };
2243
1830
 
2244
- // src/pafi-backend/pafiBackendClient.ts
2245
- var DEFAULT_TIMEOUT_MS = 1e4;
2246
- var RETRY_DEFAULTS = {
2247
- maxAttempts: 1,
2248
- initialDelayMs: 500,
2249
- maxDelayMs: 1e4,
2250
- maxRetryAfterMs: 3e4
2251
- };
2252
- var PafiBackendClient = class {
2253
- url;
2254
- issuerId;
2255
- apiKey;
2256
- fetchImpl;
2257
- timeoutMs;
2258
- retry;
2259
- constructor(config) {
2260
- if (!config.url) {
2261
- throw new Error("PafiBackendClient: url is required");
2262
- }
2263
- if (!config.issuerId) {
2264
- throw new Error("PafiBackendClient: issuerId is required");
2265
- }
2266
- if (!config.apiKey) {
2267
- throw new Error("PafiBackendClient: apiKey is required");
2268
- }
2269
- this.url = config.url.replace(/\/+$/, "");
2270
- this.issuerId = config.issuerId;
2271
- this.apiKey = config.apiKey;
2272
- this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
2273
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2274
- this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
2275
- if (!this.fetchImpl) {
2276
- throw new Error(
2277
- "PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
2278
- );
2279
- }
2280
- if (this.retry.maxAttempts < 1) {
2281
- throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
2282
- }
2283
- }
2284
- /**
2285
- * Request paymaster sponsorship for a pre-built UserOperation.
2286
- * See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
2287
- *
2288
- * Retries automatically on transient failures (5xx, timeouts, network
2289
- * errors, and errors the server flags with `safeToRetry: true`) up to
2290
- * `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
2291
- *
2292
- * @throws PafiBackendError on final failure after exhausting retries
2293
- */
2294
- async requestSponsorship(req) {
2295
- return this.postWithRetry(
2296
- "/paymaster/sponsor",
2297
- req
2298
- );
2299
- }
2300
- // -------------------------------------------------------------------------
2301
- // Internals
2302
- // -------------------------------------------------------------------------
2303
- async postWithRetry(path, body) {
2304
- let lastError;
2305
- for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
2306
- try {
2307
- return await this.post(path, body);
2308
- } catch (err) {
2309
- if (!(err instanceof PafiBackendError)) throw err;
2310
- lastError = err;
2311
- const isLastAttempt = attempt >= this.retry.maxAttempts;
2312
- if (isLastAttempt || !err.safeToRetry) throw err;
2313
- const delay = this.computeBackoff(attempt, err.retryAfter);
2314
- if (delay === null) throw err;
2315
- await this.sleep(delay);
2316
- }
2317
- }
2318
- throw lastError;
2319
- }
2320
- /**
2321
- * Pick the delay before the next retry.
2322
- * - If the server sent `retryAfter` (seconds), honor it (capped by
2323
- * `maxRetryAfterMs`) — returns null if the server wait exceeds the
2324
- * cap, signalling the caller should give up.
2325
- * - Otherwise: exponential backoff with ±20% jitter, capped at
2326
- * `maxDelayMs`.
2327
- */
2328
- computeBackoff(attempt, retryAfter) {
2329
- if (retryAfter !== void 0) {
2330
- const serverMs = retryAfter * 1e3;
2331
- if (serverMs > this.retry.maxRetryAfterMs) return null;
2332
- return serverMs;
2333
- }
2334
- const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
2335
- const capped = Math.min(exp, this.retry.maxDelayMs);
2336
- const jitter = capped * (0.8 + Math.random() * 0.4);
2337
- return Math.round(jitter);
2338
- }
2339
- sleep(ms) {
2340
- return new Promise((resolve) => setTimeout(resolve, ms));
2341
- }
2342
- async post(path, body) {
2343
- const controller = new AbortController();
2344
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2345
- let response;
2346
- try {
2347
- response = await this.fetchImpl(`${this.url}${path}`, {
2348
- method: "POST",
2349
- headers: {
2350
- "Content-Type": "application/json",
2351
- "Authorization": `Bearer ${this.apiKey}`,
2352
- "X-Issuer-Id": this.issuerId
2353
- },
2354
- body: JSON.stringify(body, this.bigintReplacer),
2355
- signal: controller.signal
2356
- });
2357
- } catch (err) {
2358
- if (err.name === "AbortError") {
2359
- throw new PafiBackendError(
2360
- "TIMEOUT",
2361
- `PAFI Backend request timed out after ${this.timeoutMs}ms`,
2362
- 0
2363
- );
2364
- }
2365
- throw new PafiBackendError(
2366
- "NETWORK_ERROR",
2367
- `PAFI Backend unreachable: ${err.message}`,
2368
- 0
2369
- );
2370
- } finally {
2371
- clearTimeout(timeoutId);
2372
- }
2373
- const text = await response.text();
2374
- if (!response.ok) {
2375
- let code = "INTERNAL_ERROR";
2376
- let message = text || response.statusText;
2377
- let details;
2378
- let retryAfter;
2379
- let serverSafeToRetry;
2380
- try {
2381
- const parsed = JSON.parse(text);
2382
- code = parsed.code ?? code;
2383
- message = parsed.message ?? message;
2384
- details = parsed.details;
2385
- if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
2386
- if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
2387
- } catch {
2388
- }
2389
- throw new PafiBackendError(code, message, response.status, details, {
2390
- ...retryAfter !== void 0 ? { retryAfter } : {},
2391
- ...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
2392
- });
2393
- }
2394
- return JSON.parse(text, this.bigintReviver);
2395
- }
2396
- /** JSON replacer that stringifies bigints. Paired with bigintReviver. */
2397
- bigintReplacer = (_key, value) => {
2398
- return typeof value === "bigint" ? value.toString() : value;
2399
- };
2400
- /**
2401
- * JSON reviver that coerces specific numeric-string fields back to
2402
- * bigint. The server must send these fields as decimal strings.
2403
- */
2404
- bigintReviver = (key, value) => {
2405
- if (typeof value === "string" && (key.endsWith("GasLimit") || key === "nonce" || key === "callGasLimit" || key === "verificationGasLimit" || key === "preVerificationGas" || key === "maxFeePerGas" || key === "maxPriorityFeePerGas" || key === "paymasterVerificationGasLimit" || key === "paymasterPostOpGasLimit") && /^\d+$/.test(value)) {
2406
- return BigInt(value);
2407
- }
2408
- return value;
2409
- };
2410
- };
2411
-
2412
1831
  // src/config.ts
2413
1832
  import { getAddress as getAddress9 } from "viem";
2414
1833
  function createIssuerService(config) {
2415
1834
  if (!config.provider) {
2416
1835
  throw new Error("createIssuerService: provider is required");
2417
1836
  }
2418
- if (!config.operatorWallet) {
2419
- throw new Error("createIssuerService: operatorWallet is required");
2420
- }
2421
- if (!config.signer) {
2422
- throw new Error("createIssuerService: signer is required");
2423
- }
2424
- if (!config.relayAddress) {
2425
- throw new Error("createIssuerService: relayAddress is required");
2426
- }
2427
1837
  if (!config.auth?.jwtSecret) {
2428
1838
  throw new Error("createIssuerService: auth.jwtSecret is required");
2429
1839
  }
@@ -2450,18 +1860,7 @@ function createIssuerService(config) {
2450
1860
  authServiceConfig.jwtExpiresIn = config.auth.jwtExpiresIn;
2451
1861
  }
2452
1862
  const authService = new AuthService(authServiceConfig);
2453
- const relayServiceConfig = {
2454
- relayAddress: config.relayAddress,
2455
- operatorWallet: config.operatorWallet,
2456
- provider: config.provider
2457
- };
2458
- if (config.relay?.simulateBeforeSubmit !== void 0) {
2459
- relayServiceConfig.simulateBeforeSubmit = config.relay.simulateBeforeSubmit;
2460
- }
2461
- if (config.relay?.confirmationTimeoutMs !== void 0) {
2462
- relayServiceConfig.confirmationTimeoutMs = config.relay.confirmationTimeoutMs;
2463
- }
2464
- const relayService = new RelayService(relayServiceConfig);
1863
+ const relayService = new RelayService();
2465
1864
  let feeManager;
2466
1865
  if (config.fee) {
2467
1866
  feeManager = new FeeManager({
@@ -2469,16 +1868,6 @@ function createIssuerService(config) {
2469
1868
  provider: config.provider
2470
1869
  });
2471
1870
  }
2472
- const gatewayConfig = {
2473
- ledger,
2474
- policy,
2475
- signer: config.signer,
2476
- relayService
2477
- };
2478
- if (config.gateway?.defaultLockBufferMs !== void 0) {
2479
- gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
2480
- }
2481
- const gateway = new MintingGateway(gatewayConfig);
2482
1871
  const indexers = /* @__PURE__ */ new Map();
2483
1872
  for (const tokenAddress of tokenAddresses) {
2484
1873
  const indexerConfig = {
@@ -2506,7 +1895,6 @@ function createIssuerService(config) {
2506
1895
  const firstIndexer = indexers.get(tokenAddresses[0]);
2507
1896
  const handlersConfig = {
2508
1897
  authService,
2509
- gateway,
2510
1898
  ledger,
2511
1899
  provider: config.provider,
2512
1900
  pointTokenAddresses: tokenAddresses,
@@ -2526,10 +1914,8 @@ function createIssuerService(config) {
2526
1914
  sessionStore,
2527
1915
  ledger,
2528
1916
  policy,
2529
- signer: config.signer,
2530
1917
  relayService,
2531
1918
  feeManager,
2532
- gateway,
2533
1919
  indexers,
2534
1920
  indexer: firstIndexer,
2535
1921
  handlers
@@ -2549,13 +1935,10 @@ export {
2549
1935
  IssuerApiHandlers,
2550
1936
  MemoryPointLedger,
2551
1937
  MemorySessionStore,
2552
- MintingGateway,
2553
- MintingGatewayError,
2554
1938
  NonceManager,
2555
1939
  PAFI_ISSUER_SDK_VERSION,
2556
1940
  PTRedeemError,
2557
1941
  PTRedeemHandler,
2558
- PafiBackendClient,
2559
1942
  PafiBackendError,
2560
1943
  PointIndexer,
2561
1944
  PrivateKeySigner,
@@ -2566,7 +1949,6 @@ export {
2566
1949
  authenticateRequest,
2567
1950
  createIssuerService,
2568
1951
  createSubgraphNativeUsdtQuoter,
2569
- createSubgraphPoolsProvider,
2570
- encodeExtData
1952
+ createSubgraphPoolsProvider
2571
1953
  };
2572
1954
  //# sourceMappingURL=index.js.map