@pafi-dev/issuer 0.2.0 → 0.3.0-beta.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.cjs CHANGED
@@ -22,6 +22,8 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthError: () => AuthError,
24
24
  AuthService: () => AuthService,
25
+ BalanceAggregator: () => BalanceAggregator,
26
+ BurnIndexer: () => BurnIndexer,
25
27
  DefaultPolicyEngine: () => DefaultPolicyEngine,
26
28
  FeeManager: () => FeeManager,
27
29
  InMemoryCursorStore: () => InMemoryCursorStore,
@@ -32,10 +34,16 @@ __export(index_exports, {
32
34
  MintingGatewayError: () => MintingGatewayError,
33
35
  NonceManager: () => NonceManager,
34
36
  PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
37
+ PTRedeemError: () => PTRedeemError,
38
+ PTRedeemHandler: () => PTRedeemHandler,
39
+ PafiBackendClient: () => PafiBackendClient,
40
+ PafiBackendError: () => PafiBackendError,
35
41
  PointIndexer: () => PointIndexer,
36
42
  PrivateKeySigner: () => PrivateKeySigner,
37
43
  RelayError: () => RelayError,
38
44
  RelayService: () => RelayService,
45
+ TopUpRedemptionError: () => TopUpRedemptionError,
46
+ TopUpRedemptionHandler: () => TopUpRedemptionHandler,
39
47
  authenticateRequest: () => authenticateRequest,
40
48
  createIssuerService: () => createIssuerService,
41
49
  createSubgraphNativeUsdtQuoter: () => createSubgraphNativeUsdtQuoter,
@@ -162,6 +170,54 @@ var MemoryPointLedger = class {
162
170
  if (txHash) lock.txHash = txHash;
163
171
  }
164
172
  // -------------------------------------------------------------------------
173
+ // v1.4 — Reverse flow (PT burn → off-chain credit)
174
+ // -------------------------------------------------------------------------
175
+ pendingCredits = /* @__PURE__ */ new Map();
176
+ nextCreditId = 1;
177
+ async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
178
+ if (amount <= 0n) {
179
+ throw new Error(
180
+ "MemoryPointLedger: pending credit amount must be positive"
181
+ );
182
+ }
183
+ if (durationMs <= 0) {
184
+ throw new Error("MemoryPointLedger: durationMs must be positive");
185
+ }
186
+ const user = (0, import_viem.getAddress)(userAddress);
187
+ const lockId = `credit-${this.nextCreditId++}`;
188
+ const now = this.now();
189
+ this.pendingCredits.set(lockId, {
190
+ lockId,
191
+ userAddress: user,
192
+ amount,
193
+ tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
194
+ createdAt: now,
195
+ expiresAt: now + durationMs,
196
+ status: "PENDING"
197
+ });
198
+ return lockId;
199
+ }
200
+ async resolveCreditByBurnTx(lockId, txHash) {
201
+ const credit = this.pendingCredits.get(lockId);
202
+ if (!credit) {
203
+ throw new Error(
204
+ `MemoryPointLedger: unknown pending credit lockId ${lockId}`
205
+ );
206
+ }
207
+ if (credit.status === "RESOLVED") {
208
+ if (credit.txHash === txHash) return;
209
+ throw new Error(
210
+ `MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
211
+ );
212
+ }
213
+ const token = normalizeToken(credit.tokenAddress);
214
+ const key = balanceKey(credit.userAddress, token);
215
+ const current = this.balances.get(key) ?? 0n;
216
+ this.balances.set(key, current + credit.amount);
217
+ credit.status = "RESOLVED";
218
+ credit.txHash = txHash;
219
+ }
220
+ // -------------------------------------------------------------------------
165
221
  // Internal helpers
166
222
  // -------------------------------------------------------------------------
167
223
  /**
@@ -578,6 +634,7 @@ var RelayError = class extends Error {
578
634
  };
579
635
 
580
636
  // src/relay/relayService.ts
637
+ var import_viem5 = require("viem");
581
638
  var import_core3 = require("@pafi-dev/core");
582
639
  var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
583
640
  var RelayService = class {
@@ -630,6 +687,11 @@ var RelayService = class {
630
687
  * decide whether to release the ledger lock (`SUBMIT_FAILED` and
631
688
  * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
632
689
  * need manual review because the tx may still land).
690
+ *
691
+ * @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
692
+ * `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
693
+ * still needs to finalize Relayer v2 ABI before the replacements
694
+ * can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
633
695
  */
634
696
  async submitMintAndSwap(params) {
635
697
  if (this.simulateBeforeSubmit && this.provider) {
@@ -698,6 +760,154 @@ var RelayService = class {
698
760
  );
699
761
  }
700
762
  }
763
+ // ==========================================================================
764
+ // v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
765
+ // ==========================================================================
766
+ //
767
+ // These two methods build unsigned `PartialUserOperation` payloads for
768
+ // the Frontend to sign (via Privy) and submit to the Bundler. The
769
+ // Issuer Backend no longer broadcasts — that's the Frontend's job.
770
+ //
771
+ // Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
772
+ // When SC delivers real ABIs, the imports swap but these method bodies
773
+ // stay the same (calldata encoder is ABI-driven).
774
+ // ==========================================================================
775
+ /**
776
+ * Build an unsigned UserOp for Scenario 1 (Mint).
777
+ *
778
+ * Flow:
779
+ * 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
780
+ * 2. Optionally append a PT fee transfer from user → feeRecipient
781
+ * (fee recovery happens on-chain via BatchExecutor, not via an
782
+ * operator wallet)
783
+ * 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
784
+ * 4. Return a `PartialUserOperation` ready for:
785
+ * - gas estimation (Bundler)
786
+ * - paymaster sponsorship (PAFI Backend)
787
+ * - user signature (Privy)
788
+ */
789
+ prepareMint(params) {
790
+ if (!params.relayerAddress) {
791
+ throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
792
+ }
793
+ if (!params.batchExecutorAddress) {
794
+ throw new RelayError(
795
+ "ENCODE_FAILED",
796
+ "prepareMint: batchExecutorAddress required"
797
+ );
798
+ }
799
+ if (!params.userAddress) {
800
+ throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
801
+ }
802
+ let mintCallData;
803
+ try {
804
+ mintCallData = (0, import_viem5.encodeFunctionData)({
805
+ abi: import_core3.RELAYER_V2_ABI,
806
+ functionName: "mint",
807
+ args: [params.mintRequest, params.userSignature, params.issuerSignature]
808
+ });
809
+ } catch (err) {
810
+ throw new RelayError(
811
+ "ENCODE_FAILED",
812
+ `prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
813
+ err
814
+ );
815
+ }
816
+ const operations = [
817
+ {
818
+ target: params.relayerAddress,
819
+ value: 0n,
820
+ data: mintCallData
821
+ }
822
+ ];
823
+ if (params.mintRequest.feeAmount > 0n) {
824
+ operations.push({
825
+ target: params.pointTokenAddress,
826
+ value: 0n,
827
+ data: (0, import_viem5.encodeFunctionData)({
828
+ abi: import_core3.POINT_TOKEN_V2_ABI,
829
+ functionName: "balanceOf",
830
+ // placeholder — real impl uses transfer
831
+ args: [params.mintRequest.feeRecipient]
832
+ })
833
+ });
834
+ }
835
+ return (0, import_core3.buildPartialUserOperation)({
836
+ sender: params.userAddress,
837
+ nonce: params.aaNonce,
838
+ operations,
839
+ gasLimits: {
840
+ callGasLimit: params.callGasLimit ?? 500000n,
841
+ verificationGasLimit: params.verificationGasLimit ?? 150000n,
842
+ preVerificationGas: params.preVerificationGas ?? 50000n
843
+ }
844
+ });
845
+ }
846
+ /**
847
+ * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
848
+ *
849
+ * Two modes:
850
+ * - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
851
+ * via EIP-7702 delegation is the user, so no signature needed
852
+ * on-chain (the BurnConsent was already verified off-chain by
853
+ * the issuer backend before we got here)
854
+ * - `mode: 'burnWithSig'` — `PointToken.burnWithSig(consent, sig)`;
855
+ * used when the issuer hasn't verified the consent and the
856
+ * contract has to do it on-chain
857
+ */
858
+ prepareBurn(params) {
859
+ if (!params.pointTokenAddress) {
860
+ throw new RelayError("ENCODE_FAILED", "prepareBurn: pointTokenAddress required");
861
+ }
862
+ if (!params.batchExecutorAddress) {
863
+ throw new RelayError(
864
+ "ENCODE_FAILED",
865
+ "prepareBurn: batchExecutorAddress required"
866
+ );
867
+ }
868
+ let burnCallData;
869
+ try {
870
+ if (params.mode === "burnWithSig") {
871
+ if (!params.burnConsent || !params.consentSignature) {
872
+ throw new Error("burnWithSig requires burnConsent + consentSignature");
873
+ }
874
+ burnCallData = (0, import_viem5.encodeFunctionData)({
875
+ abi: import_core3.POINT_TOKEN_V2_ABI,
876
+ functionName: "burnWithSig",
877
+ args: [params.burnConsent, params.consentSignature]
878
+ });
879
+ } else {
880
+ burnCallData = (0, import_viem5.encodeFunctionData)({
881
+ abi: import_core3.POINT_TOKEN_V2_ABI,
882
+ functionName: "burn",
883
+ args: [params.amount]
884
+ });
885
+ }
886
+ } catch (err) {
887
+ throw new RelayError(
888
+ "ENCODE_FAILED",
889
+ `prepareBurn: failed to encode burn call: ${errorMessage(err)}`,
890
+ err
891
+ );
892
+ }
893
+ const operations = [
894
+ {
895
+ target: params.pointTokenAddress,
896
+ value: 0n,
897
+ data: burnCallData
898
+ }
899
+ ];
900
+ return (0, import_core3.buildPartialUserOperation)({
901
+ sender: params.userAddress,
902
+ nonce: params.aaNonce,
903
+ operations,
904
+ gasLimits: {
905
+ callGasLimit: params.callGasLimit ?? 300000n,
906
+ verificationGasLimit: params.verificationGasLimit ?? 150000n,
907
+ preVerificationGas: params.preVerificationGas ?? 50000n
908
+ }
909
+ });
910
+ }
701
911
  };
702
912
  function errorMessage(err) {
703
913
  return err instanceof Error ? err.message : String(err);
@@ -708,84 +918,35 @@ var DEFAULT_GAS_UNITS = 500000n;
708
918
  var DEFAULT_PREMIUM_BPS = 12e3;
709
919
  var FeeManager = class {
710
920
  provider;
711
- operatorWallet;
712
- mintAndSwapGasUnits;
921
+ gasUnits;
713
922
  gasPremiumBps;
714
- quoteNativeToUsdt;
715
- rebalanceThresholdWei;
716
- rebalanceUsdtAmount;
717
- swapUsdtToNative;
923
+ quoteNativeToFee;
718
924
  constructor(config) {
719
925
  if (!config.provider) throw new Error("FeeManager: provider required");
720
- if (!config.operatorWallet)
721
- throw new Error("FeeManager: operatorWallet required");
722
- if (!config.quoteNativeToUsdt)
723
- throw new Error("FeeManager: quoteNativeToUsdt required");
926
+ if (!config.quoteNativeToFee)
927
+ throw new Error("FeeManager: quoteNativeToFee required");
724
928
  this.provider = config.provider;
725
- this.operatorWallet = config.operatorWallet;
726
- this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
929
+ this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
727
930
  this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
728
- this.quoteNativeToUsdt = config.quoteNativeToUsdt;
729
- if (config.rebalanceThresholdWei !== void 0) {
730
- this.rebalanceThresholdWei = config.rebalanceThresholdWei;
731
- }
732
- if (config.rebalanceUsdtAmount !== void 0) {
733
- this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
734
- }
735
- if (config.swapUsdtToNative) {
736
- this.swapUsdtToNative = config.swapUsdtToNative;
737
- }
738
- const rebalanceFields = [
739
- config.rebalanceThresholdWei,
740
- config.rebalanceUsdtAmount,
741
- config.swapUsdtToNative
742
- ];
743
- const someSet = rebalanceFields.some((v) => v !== void 0);
744
- const allSet = rebalanceFields.every((v) => v !== void 0);
745
- if (someSet && !allSet) {
746
- throw new Error(
747
- "FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
748
- );
749
- }
931
+ this.quoteNativeToFee = config.quoteNativeToFee;
750
932
  }
751
933
  /**
752
- * Estimate the USDT fee the operator should charge for a single
753
- * `mintAndSwap`. Computed as:
934
+ * Estimate the fee (in the caller's fee currency) to charge for the
935
+ * next sponsored UserOp:
936
+ *
937
+ * nativeCost = gasUnits × gasPrice
938
+ * withPremium = nativeCost × premiumBps / 10_000
939
+ * fee = quoteNativeToFee(withPremium)
754
940
  *
755
- * nativeCost = gasUnits × gasPrice
756
- * premiumNativeCost = nativeCost × premiumBps / 10_000
757
- * usdtFee = quoteNativeToUsdt(premiumNativeCost)
941
+ * For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
942
+ * from the response, the name `estimateGasFee` is kept — but the
943
+ * currency depends on how the caller wired `quoteNativeToFee`.
758
944
  */
759
945
  async estimateGasFee() {
760
946
  const gasPrice = await this.provider.getGasPrice();
761
- const nativeCost = gasPrice * this.mintAndSwapGasUnits;
947
+ const nativeCost = gasPrice * this.gasUnits;
762
948
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
763
- return this.quoteNativeToUsdt(withPremium);
764
- }
765
- /**
766
- * Check the operator's native balance and, if it has dropped below the
767
- * configured threshold, trigger a USDT→native rebalance via the injected
768
- * `swapUsdtToNative` function.
769
- *
770
- * Returns `true` if a rebalance was performed, `false` otherwise.
771
- * Silently no-ops when rebalance is not configured.
772
- */
773
- async rebalanceIfNeeded() {
774
- if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
775
- return false;
776
- }
777
- const operatorAddress = this.operatorWallet.account?.address;
778
- if (!operatorAddress) {
779
- throw new Error(
780
- "FeeManager: operator wallet has no account attached \u2014 cannot read balance"
781
- );
782
- }
783
- const balance = await this.provider.getBalance({ address: operatorAddress });
784
- if (balance >= this.rebalanceThresholdWei) {
785
- return false;
786
- }
787
- await this.swapUsdtToNative(this.rebalanceUsdtAmount);
788
- return true;
949
+ return this.quoteNativeToFee(withPremium);
789
950
  }
790
951
  };
791
952
 
@@ -831,6 +992,12 @@ var MintingGateway = class {
831
992
  this.now = config.now ?? (() => Date.now());
832
993
  this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
833
994
  }
995
+ /**
996
+ * @deprecated Since 0.3.0 — will be renamed to `processMint()` once
997
+ * the SC team finalizes Relayer v2 ABI. The new flow drops the
998
+ * swap steps entirely (no more single-call mint+swap); users swap
999
+ * separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
1000
+ */
834
1001
  async processMintAndCashOut(request) {
835
1002
  const { receiverConsent, receiverSignature } = request;
836
1003
  if (!receiverConsent || !receiverSignature) {
@@ -1041,8 +1208,8 @@ var InMemoryCursorStore = class {
1041
1208
  };
1042
1209
 
1043
1210
  // src/indexer/pointIndexer.ts
1044
- var import_viem5 = require("viem");
1045
- var TRANSFER_EVENT = (0, import_viem5.parseAbiItem)(
1211
+ var import_viem6 = require("viem");
1212
+ var TRANSFER_EVENT = (0, import_viem6.parseAbiItem)(
1046
1213
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1047
1214
  );
1048
1215
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
@@ -1162,10 +1329,10 @@ var PointIndexer = class {
1162
1329
  for (const log of logs) {
1163
1330
  const args = log.args;
1164
1331
  if (!args.from || !args.to || args.value === void 0) continue;
1165
- if ((0, import_viem5.getAddress)(args.from) !== ZERO_ADDRESS) continue;
1332
+ if ((0, import_viem6.getAddress)(args.from) !== ZERO_ADDRESS) continue;
1166
1333
  if (log.blockNumber === null || log.transactionHash === null) continue;
1167
1334
  out.push({
1168
- to: (0, import_viem5.getAddress)(args.to),
1335
+ to: (0, import_viem6.getAddress)(args.to),
1169
1336
  amount: args.value,
1170
1337
  blockNumber: log.blockNumber,
1171
1338
  txHash: log.transactionHash,
@@ -1220,8 +1387,159 @@ function pickMatchingLock(locks, amount) {
1220
1387
  return best;
1221
1388
  }
1222
1389
 
1390
+ // src/indexer/burnIndexer.ts
1391
+ var import_viem7 = require("viem");
1392
+ var TRANSFER_EVENT2 = (0, import_viem7.parseAbiItem)(
1393
+ "event Transfer(address indexed from, address indexed to, uint256 value)"
1394
+ );
1395
+ var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
1396
+ var DEFAULT_CONFIRMATIONS2 = 3;
1397
+ var DEFAULT_BATCH_SIZE2 = 2000n;
1398
+ var DEFAULT_POLL_INTERVAL_MS2 = 5e3;
1399
+ var BurnIndexer = class {
1400
+ provider;
1401
+ pointTokenAddress;
1402
+ ledger;
1403
+ cursorStore;
1404
+ startBlock;
1405
+ confirmations;
1406
+ batchSize;
1407
+ pollIntervalMs;
1408
+ /**
1409
+ * Caller-supplied matcher. Return the lockId to resolve for a given
1410
+ * burn event, or `undefined` to skip. Runs synchronously via the
1411
+ * ledger's query path.
1412
+ *
1413
+ * Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
1414
+ * lock id `burn-${from}-${amount}` — the in-memory ledger assigns
1415
+ * incrementing IDs so callers with the memory ledger must provide a
1416
+ * custom matcher. Real DB-backed ledgers override this to JOIN on
1417
+ * their `pending_credits` table.
1418
+ */
1419
+ matchLockId = async () => void 0;
1420
+ running = false;
1421
+ timer;
1422
+ constructor(config) {
1423
+ if (!config.provider) throw new Error("BurnIndexer: provider required");
1424
+ if (!config.pointTokenAddress)
1425
+ throw new Error("BurnIndexer: pointTokenAddress required");
1426
+ if (!config.ledger) throw new Error("BurnIndexer: ledger required");
1427
+ this.provider = config.provider;
1428
+ this.pointTokenAddress = config.pointTokenAddress;
1429
+ this.ledger = config.ledger;
1430
+ this.cursorStore = config.cursorStore ?? new InMemoryCursorStore();
1431
+ this.startBlock = config.fromBlock ?? 0n;
1432
+ this.confirmations = BigInt(
1433
+ config.confirmations ?? DEFAULT_CONFIRMATIONS2
1434
+ );
1435
+ this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
1436
+ this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
1437
+ }
1438
+ start() {
1439
+ if (this.running) return;
1440
+ this.running = true;
1441
+ void this.tick();
1442
+ }
1443
+ stop() {
1444
+ this.running = false;
1445
+ if (this.timer) {
1446
+ clearTimeout(this.timer);
1447
+ this.timer = void 0;
1448
+ }
1449
+ }
1450
+ async tick() {
1451
+ if (!this.running) return;
1452
+ try {
1453
+ const latest = await this.provider.getBlockNumber();
1454
+ const safeHead = latest - this.confirmations;
1455
+ if (safeHead < 0n) {
1456
+ this.scheduleNext();
1457
+ return;
1458
+ }
1459
+ const stored = await this.cursorStore.load();
1460
+ const from = stored ?? this.startBlock;
1461
+ if (from > safeHead) {
1462
+ this.scheduleNext();
1463
+ return;
1464
+ }
1465
+ await this.processBlockRange(from, safeHead);
1466
+ } catch {
1467
+ }
1468
+ this.scheduleNext();
1469
+ }
1470
+ scheduleNext() {
1471
+ if (!this.running) return;
1472
+ this.timer = setTimeout(() => void this.tick(), this.pollIntervalMs);
1473
+ }
1474
+ /**
1475
+ * Scan `[from, to]` inclusive for burn events. Callers can drive this
1476
+ * directly to backfill a specific range without `start()`. Cursor is
1477
+ * advanced to `to + 1` on completion.
1478
+ */
1479
+ async processBlockRange(from, to) {
1480
+ if (from > to) return;
1481
+ let cursor = from;
1482
+ while (cursor <= to) {
1483
+ const chunkEnd = cursor + this.batchSize - 1n > to ? to : cursor + this.batchSize - 1n;
1484
+ const logs = await this.provider.getLogs({
1485
+ address: this.pointTokenAddress,
1486
+ event: TRANSFER_EVENT2,
1487
+ args: { to: ZERO_ADDRESS2 },
1488
+ // filter: burn = transfer to zero
1489
+ fromBlock: cursor,
1490
+ toBlock: chunkEnd
1491
+ });
1492
+ const events = this.decodeBurnEvents(logs);
1493
+ events.sort((a, b) => {
1494
+ if (a.blockNumber !== b.blockNumber) {
1495
+ return a.blockNumber < b.blockNumber ? -1 : 1;
1496
+ }
1497
+ return a.logIndex - b.logIndex;
1498
+ });
1499
+ for (const evt of events) {
1500
+ await this.finalize(evt);
1501
+ }
1502
+ await this.cursorStore.save(chunkEnd + 1n);
1503
+ cursor = chunkEnd + 1n;
1504
+ }
1505
+ }
1506
+ decodeBurnEvents(logs) {
1507
+ const out = [];
1508
+ for (const log of logs) {
1509
+ const args = log.args;
1510
+ if (!args.from || !args.to || args.value === void 0) continue;
1511
+ if ((0, import_viem7.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
1512
+ if (log.blockNumber === null || log.transactionHash === null) continue;
1513
+ out.push({
1514
+ from: (0, import_viem7.getAddress)(args.from),
1515
+ amount: args.value,
1516
+ blockNumber: log.blockNumber,
1517
+ txHash: log.transactionHash,
1518
+ logIndex: log.logIndex ?? 0
1519
+ });
1520
+ }
1521
+ return out;
1522
+ }
1523
+ /**
1524
+ * Resolve a matching pending credit for this burn event and call
1525
+ * `ledger.resolveCreditByBurnTx(lockId, txHash)`. If no match found,
1526
+ * log + skip.
1527
+ */
1528
+ async finalize(evt) {
1529
+ const lockId = await this.matchLockId(evt);
1530
+ if (!lockId) return;
1531
+ if (!this.ledger.resolveCreditByBurnTx) {
1532
+ return;
1533
+ }
1534
+ try {
1535
+ await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
1536
+ } catch {
1537
+ }
1538
+ }
1539
+ };
1540
+
1223
1541
  // src/api/handlers.ts
1224
- var import_viem6 = require("viem");
1542
+ var import_viem8 = require("viem");
1225
1543
  var import_core5 = require("@pafi-dev/core");
1226
1544
  var IssuerApiHandlers = class {
1227
1545
  authService;
@@ -1239,6 +1557,7 @@ var IssuerApiHandlers = class {
1239
1557
  defaultToken;
1240
1558
  chainId;
1241
1559
  contracts;
1560
+ pafiWebUrl;
1242
1561
  feeManager;
1243
1562
  poolsProvider;
1244
1563
  constructor(config) {
@@ -1252,11 +1571,12 @@ var IssuerApiHandlers = class {
1252
1571
  "IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
1253
1572
  );
1254
1573
  }
1255
- const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
1574
+ const normalized = raw.map((a) => (0, import_viem8.getAddress)(a));
1256
1575
  this.supportedTokens = new Set(normalized);
1257
1576
  this.defaultToken = normalized[0];
1258
1577
  this.chainId = config.chainId;
1259
1578
  this.contracts = config.contracts;
1579
+ if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
1260
1580
  if (config.feeManager) this.feeManager = config.feeManager;
1261
1581
  if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
1262
1582
  }
@@ -1292,7 +1612,16 @@ var IssuerApiHandlers = class {
1292
1612
  `handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
1293
1613
  );
1294
1614
  }
1295
- return { chainId: this.chainId, contracts: { ...this.contracts } };
1615
+ const contracts = {
1616
+ ...this.contracts,
1617
+ pointTokens: Array.from(this.supportedTokens)
1618
+ };
1619
+ const response = {
1620
+ chainId: this.chainId,
1621
+ contracts
1622
+ };
1623
+ if (this.pafiWebUrl) response.pafiWebUrl = this.pafiWebUrl;
1624
+ return response;
1296
1625
  }
1297
1626
  /** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
1298
1627
  async handleGasFee() {
@@ -1343,14 +1672,14 @@ var IssuerApiHandlers = class {
1343
1672
  `handleUser: unsupported chainId ${request.chainId}`
1344
1673
  );
1345
1674
  }
1346
- const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
1347
- const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
1675
+ const normalizedAuthed = (0, import_viem8.getAddress)(userAddress);
1676
+ const normalizedRequest = (0, import_viem8.getAddress)(request.userAddress);
1348
1677
  if (normalizedAuthed !== normalizedRequest) {
1349
1678
  throw new Error(
1350
1679
  "handleUser: request userAddress must match authenticated user"
1351
1680
  );
1352
1681
  }
1353
- const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1682
+ const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1354
1683
  if (!this.supportedTokens.has(pointToken)) {
1355
1684
  throw new Error(
1356
1685
  `handleUser: unsupported pointToken ${pointToken}`
@@ -1393,7 +1722,7 @@ var IssuerApiHandlers = class {
1393
1722
  `handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
1394
1723
  );
1395
1724
  }
1396
- const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1725
+ const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1397
1726
  if (!this.supportedTokens.has(pointToken)) {
1398
1727
  throw new Error(
1399
1728
  `handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
@@ -1418,8 +1747,14 @@ var IssuerApiHandlers = class {
1418
1747
  /**
1419
1748
  * `POST /claim-and-swap`
1420
1749
  *
1421
- * The terminal handler: forwards the verified consent to the
1422
- * MintingGateway, which runs the 11-step flow.
1750
+ * @deprecated Since 0.3.0 the single-call mint-then-swap flow is
1751
+ * retired in v1.4. Use the new `handleClaim()` (mint only) and let
1752
+ * the user swap separately on PAFI Web. See
1753
+ * [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
1754
+ * removed in 2.0.
1755
+ *
1756
+ * Legacy behavior: the terminal handler forwards the verified
1757
+ * consent to the MintingGateway, which runs the 11-step flow.
1423
1758
  */
1424
1759
  async handleClaimAndSwap(userAddress, request) {
1425
1760
  if (request.chainId !== this.chainId) {
@@ -1427,14 +1762,14 @@ var IssuerApiHandlers = class {
1427
1762
  `handleClaimAndSwap: unsupported chainId ${request.chainId}`
1428
1763
  );
1429
1764
  }
1430
- const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1765
+ const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1431
1766
  if (!this.supportedTokens.has(pointToken)) {
1432
1767
  throw new Error(
1433
1768
  `handleClaimAndSwap: unsupported pointToken ${pointToken}`
1434
1769
  );
1435
1770
  }
1436
1771
  const result = await this.gateway.processMintAndCashOut({
1437
- userAddress: (0, import_viem6.getAddress)(userAddress),
1772
+ userAddress: (0, import_viem8.getAddress)(userAddress),
1438
1773
  pointTokenAddress: pointToken,
1439
1774
  chainId: request.chainId,
1440
1775
  domain: request.domain,
@@ -1454,6 +1789,183 @@ var IssuerApiHandlers = class {
1454
1789
  }
1455
1790
  };
1456
1791
 
1792
+ // src/api/handlers/ptRedeemHandler.ts
1793
+ var import_viem9 = require("viem");
1794
+ var import_core6 = require("@pafi-dev/core");
1795
+ var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1796
+ var PTRedeemError = class extends Error {
1797
+ constructor(code, message) {
1798
+ super(message);
1799
+ this.code = code;
1800
+ this.name = "PTRedeemError";
1801
+ }
1802
+ code;
1803
+ };
1804
+ var PTRedeemHandler = class {
1805
+ ledger;
1806
+ relayService;
1807
+ pointTokenAddress;
1808
+ batchExecutorAddress;
1809
+ chainId;
1810
+ domain;
1811
+ redeemLockDurationMs;
1812
+ now;
1813
+ constructor(config) {
1814
+ if (!config.ledger.reservePendingCredit) {
1815
+ throw new PTRedeemError(
1816
+ "LEDGER_NOT_SUPPORTED",
1817
+ "PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
1818
+ );
1819
+ }
1820
+ this.ledger = config.ledger;
1821
+ this.relayService = config.relayService;
1822
+ this.pointTokenAddress = (0, import_viem9.getAddress)(config.pointTokenAddress);
1823
+ this.batchExecutorAddress = (0, import_viem9.getAddress)(config.batchExecutorAddress);
1824
+ this.chainId = config.chainId;
1825
+ this.domain = config.domain;
1826
+ this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1827
+ this.now = config.now ?? (() => Date.now());
1828
+ }
1829
+ async handle(request) {
1830
+ if (request.amount <= 0n) {
1831
+ throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
1832
+ }
1833
+ if (request.consent.amount !== request.amount) {
1834
+ throw new PTRedeemError(
1835
+ "AMOUNT_MISMATCH",
1836
+ `consent.amount (${request.consent.amount}) must match request.amount (${request.amount})`
1837
+ );
1838
+ }
1839
+ const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
1840
+ if (request.consent.deadline <= nowSeconds) {
1841
+ throw new PTRedeemError(
1842
+ "EXPIRED_CONSENT",
1843
+ `consent deadline (${request.consent.deadline}) already passed`
1844
+ );
1845
+ }
1846
+ const verification = await (0, import_core6.verifyBurnConsent)(
1847
+ {
1848
+ name: this.domain.name,
1849
+ chainId: this.chainId,
1850
+ verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1851
+ },
1852
+ request.consent,
1853
+ request.consentSignature,
1854
+ request.userAddress
1855
+ );
1856
+ if (!verification.isValid) {
1857
+ throw new PTRedeemError(
1858
+ "SIGNATURE_MISMATCH",
1859
+ `signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
1860
+ );
1861
+ }
1862
+ const lockId = await this.ledger.reservePendingCredit(
1863
+ request.userAddress,
1864
+ request.amount,
1865
+ this.redeemLockDurationMs,
1866
+ this.pointTokenAddress
1867
+ );
1868
+ const userOp = this.relayService.prepareBurn({
1869
+ mode: "burnWithSig",
1870
+ userAddress: request.userAddress,
1871
+ aaNonce: request.aaNonce,
1872
+ pointTokenAddress: this.pointTokenAddress,
1873
+ batchExecutorAddress: this.batchExecutorAddress,
1874
+ burnConsent: request.consent,
1875
+ consentSignature: parseSigStruct(request.consentSignature)
1876
+ });
1877
+ return {
1878
+ lockId,
1879
+ userOp,
1880
+ expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
1881
+ };
1882
+ }
1883
+ };
1884
+ function parseSigStruct(serialized) {
1885
+ const raw = serialized.slice(2);
1886
+ if (raw.length !== 130) {
1887
+ throw new PTRedeemError(
1888
+ "INVALID_CONSENT",
1889
+ `signature must be 65 bytes, got ${raw.length / 2}`
1890
+ );
1891
+ }
1892
+ const r = `0x${raw.slice(0, 64)}`;
1893
+ const s = `0x${raw.slice(64, 128)}`;
1894
+ const v = parseInt(raw.slice(128, 130), 16);
1895
+ return { v, r, s };
1896
+ }
1897
+
1898
+ // src/api/handlers/topUpRedemptionHandler.ts
1899
+ var import_viem10 = require("viem");
1900
+ var import_core7 = require("@pafi-dev/core");
1901
+ var TopUpRedemptionError = class extends Error {
1902
+ constructor(code, message) {
1903
+ super(message);
1904
+ this.code = code;
1905
+ this.name = "TopUpRedemptionError";
1906
+ }
1907
+ code;
1908
+ };
1909
+ var TopUpRedemptionHandler = class {
1910
+ ledger;
1911
+ ptRedeemHandler;
1912
+ provider;
1913
+ pointTokenAddress;
1914
+ constructor(config) {
1915
+ this.ledger = config.ledger;
1916
+ this.ptRedeemHandler = config.ptRedeemHandler;
1917
+ this.provider = config.provider;
1918
+ this.pointTokenAddress = (0, import_viem10.getAddress)(config.pointTokenAddress);
1919
+ }
1920
+ async handle(request) {
1921
+ const offChainBalance = await this.ledger.getBalance(
1922
+ request.userAddress,
1923
+ this.pointTokenAddress
1924
+ );
1925
+ if (offChainBalance >= request.requiredAmount) {
1926
+ return { action: "NO_TOP_UP_NEEDED", offChainBalance };
1927
+ }
1928
+ const shortfall = request.requiredAmount - offChainBalance;
1929
+ const onChainBalance = await (0, import_core7.getPointTokenBalance)(
1930
+ this.provider,
1931
+ this.pointTokenAddress,
1932
+ request.userAddress
1933
+ );
1934
+ if (onChainBalance < shortfall) {
1935
+ return {
1936
+ action: "INSUFFICIENT_ONCHAIN",
1937
+ offChainBalance,
1938
+ onChainBalance,
1939
+ shortfall
1940
+ };
1941
+ }
1942
+ if (request.redeemRequest.consent.amount < shortfall) {
1943
+ throw new TopUpRedemptionError(
1944
+ "CONSENT_AMOUNT_TOO_LOW",
1945
+ `consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
1946
+ );
1947
+ }
1948
+ if (request.redeemRequest.consent.amount !== shortfall) {
1949
+ throw new TopUpRedemptionError(
1950
+ "CONSENT_AMOUNT_TOO_LOW",
1951
+ `consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
1952
+ );
1953
+ }
1954
+ const redeem = await this.ptRedeemHandler.handle({
1955
+ userAddress: request.userAddress,
1956
+ amount: shortfall,
1957
+ consent: request.redeemRequest.consent,
1958
+ consentSignature: request.redeemRequest.consentSignature,
1959
+ aaNonce: request.redeemRequest.aaNonce
1960
+ });
1961
+ return {
1962
+ action: "TOP_UP_STARTED",
1963
+ shortfall,
1964
+ redeem
1965
+ };
1966
+ }
1967
+ };
1968
+
1457
1969
  // src/pools/subgraphPoolsProvider.ts
1458
1970
  var DEFAULT_CACHE_TTL_MS = 3e4;
1459
1971
  var POOL_QUERY = `
@@ -1665,8 +2177,274 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
1665
2177
  return BigInt(whole + padded);
1666
2178
  }
1667
2179
 
2180
+ // src/balance/balanceAggregator.ts
2181
+ var import_core8 = require("@pafi-dev/core");
2182
+ var BalanceAggregator = class {
2183
+ provider;
2184
+ ledger;
2185
+ constructor(config) {
2186
+ if (!config.provider) {
2187
+ throw new Error("BalanceAggregator: provider is required");
2188
+ }
2189
+ if (!config.ledger) {
2190
+ throw new Error("BalanceAggregator: ledger is required");
2191
+ }
2192
+ this.provider = config.provider;
2193
+ this.ledger = config.ledger;
2194
+ }
2195
+ /**
2196
+ * Combined balance for a single (user, token) pair. Fetches off-chain
2197
+ * + on-chain in parallel.
2198
+ */
2199
+ async getCombinedBalance(user, pointToken) {
2200
+ const [offChain, onChain] = await Promise.all([
2201
+ this.ledger.getBalance(user, pointToken),
2202
+ (0, import_core8.getPointTokenBalance)(this.provider, pointToken, user)
2203
+ ]);
2204
+ return {
2205
+ offChain,
2206
+ onChain,
2207
+ total: offChain + onChain
2208
+ };
2209
+ }
2210
+ /**
2211
+ * Combined balance for multiple tokens owned by the same user. Runs
2212
+ * all lookups in parallel. Returns a Map keyed by the token address
2213
+ * (same casing as supplied — caller should normalize if needed).
2214
+ */
2215
+ async getCombinedBalanceMulti(user, pointTokens) {
2216
+ const entries = await Promise.all(
2217
+ pointTokens.map(async (token) => {
2218
+ const balance = await this.getCombinedBalance(user, token);
2219
+ return [token, balance];
2220
+ })
2221
+ );
2222
+ return new Map(entries);
2223
+ }
2224
+ };
2225
+
2226
+ // src/pafi-backend/types.ts
2227
+ var PafiBackendError = class extends Error {
2228
+ constructor(code, message, httpStatus, details, opts) {
2229
+ super(message);
2230
+ this.code = code;
2231
+ this.httpStatus = httpStatus;
2232
+ this.details = details;
2233
+ this.name = "PafiBackendError";
2234
+ if (opts?.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
2235
+ if (opts?.safeToRetry !== void 0) this.serverSafeToRetry = opts.safeToRetry;
2236
+ }
2237
+ code;
2238
+ httpStatus;
2239
+ details;
2240
+ /**
2241
+ * Seconds to wait before retry. Populated from the server body
2242
+ * (e.g. rate limit returns the number of seconds until UTC midnight).
2243
+ */
2244
+ retryAfter;
2245
+ /**
2246
+ * `safeToRetry` as reported by the server body. Prefer this over the
2247
+ * code-based heuristic when available — the server knows more about
2248
+ * whether the same request will succeed on retry.
2249
+ */
2250
+ serverSafeToRetry;
2251
+ /**
2252
+ * Whether the caller can safely retry the same request.
2253
+ *
2254
+ * If the server provided `safeToRetry` in the body, trust that.
2255
+ * Otherwise fall back to a code-based heuristic.
2256
+ */
2257
+ get safeToRetry() {
2258
+ if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
2259
+ switch (this.code) {
2260
+ case "PAYMASTER_UNAVAILABLE":
2261
+ case "PAYMASTER_TIMEOUT":
2262
+ case "RATE_LIMITER_UNAVAILABLE":
2263
+ case "INTERNAL_ERROR":
2264
+ case "TIMEOUT":
2265
+ case "NETWORK_ERROR":
2266
+ return true;
2267
+ case "RATE_LIMIT_EXCEEDED":
2268
+ case "RATE_LIMIT_EXCEEDED_DAILY":
2269
+ case "RATE_LIMIT_EXCEEDED_PER_USER":
2270
+ return true;
2271
+ // after retryAfter
2272
+ default:
2273
+ return false;
2274
+ }
2275
+ }
2276
+ };
2277
+
2278
+ // src/pafi-backend/pafiBackendClient.ts
2279
+ var DEFAULT_TIMEOUT_MS = 1e4;
2280
+ var RETRY_DEFAULTS = {
2281
+ maxAttempts: 1,
2282
+ initialDelayMs: 500,
2283
+ maxDelayMs: 1e4,
2284
+ maxRetryAfterMs: 3e4
2285
+ };
2286
+ var PafiBackendClient = class {
2287
+ url;
2288
+ issuerId;
2289
+ apiKey;
2290
+ fetchImpl;
2291
+ timeoutMs;
2292
+ retry;
2293
+ constructor(config) {
2294
+ if (!config.url) {
2295
+ throw new Error("PafiBackendClient: url is required");
2296
+ }
2297
+ if (!config.issuerId) {
2298
+ throw new Error("PafiBackendClient: issuerId is required");
2299
+ }
2300
+ if (!config.apiKey) {
2301
+ throw new Error("PafiBackendClient: apiKey is required");
2302
+ }
2303
+ this.url = config.url.replace(/\/+$/, "");
2304
+ this.issuerId = config.issuerId;
2305
+ this.apiKey = config.apiKey;
2306
+ this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
2307
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2308
+ this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
2309
+ if (!this.fetchImpl) {
2310
+ throw new Error(
2311
+ "PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
2312
+ );
2313
+ }
2314
+ if (this.retry.maxAttempts < 1) {
2315
+ throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
2316
+ }
2317
+ }
2318
+ /**
2319
+ * Request paymaster sponsorship for a pre-built UserOperation.
2320
+ * See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
2321
+ *
2322
+ * Retries automatically on transient failures (5xx, timeouts, network
2323
+ * errors, and errors the server flags with `safeToRetry: true`) up to
2324
+ * `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
2325
+ *
2326
+ * @throws PafiBackendError on final failure after exhausting retries
2327
+ */
2328
+ async requestSponsorship(req) {
2329
+ return this.postWithRetry(
2330
+ "/paymaster/sponsor",
2331
+ req
2332
+ );
2333
+ }
2334
+ // -------------------------------------------------------------------------
2335
+ // Internals
2336
+ // -------------------------------------------------------------------------
2337
+ async postWithRetry(path, body) {
2338
+ let lastError;
2339
+ for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
2340
+ try {
2341
+ return await this.post(path, body);
2342
+ } catch (err) {
2343
+ if (!(err instanceof PafiBackendError)) throw err;
2344
+ lastError = err;
2345
+ const isLastAttempt = attempt >= this.retry.maxAttempts;
2346
+ if (isLastAttempt || !err.safeToRetry) throw err;
2347
+ const delay = this.computeBackoff(attempt, err.retryAfter);
2348
+ if (delay === null) throw err;
2349
+ await this.sleep(delay);
2350
+ }
2351
+ }
2352
+ throw lastError;
2353
+ }
2354
+ /**
2355
+ * Pick the delay before the next retry.
2356
+ * - If the server sent `retryAfter` (seconds), honor it (capped by
2357
+ * `maxRetryAfterMs`) — returns null if the server wait exceeds the
2358
+ * cap, signalling the caller should give up.
2359
+ * - Otherwise: exponential backoff with ±20% jitter, capped at
2360
+ * `maxDelayMs`.
2361
+ */
2362
+ computeBackoff(attempt, retryAfter) {
2363
+ if (retryAfter !== void 0) {
2364
+ const serverMs = retryAfter * 1e3;
2365
+ if (serverMs > this.retry.maxRetryAfterMs) return null;
2366
+ return serverMs;
2367
+ }
2368
+ const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
2369
+ const capped = Math.min(exp, this.retry.maxDelayMs);
2370
+ const jitter = capped * (0.8 + Math.random() * 0.4);
2371
+ return Math.round(jitter);
2372
+ }
2373
+ sleep(ms) {
2374
+ return new Promise((resolve) => setTimeout(resolve, ms));
2375
+ }
2376
+ async post(path, body) {
2377
+ const controller = new AbortController();
2378
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2379
+ let response;
2380
+ try {
2381
+ response = await this.fetchImpl(`${this.url}${path}`, {
2382
+ method: "POST",
2383
+ headers: {
2384
+ "Content-Type": "application/json",
2385
+ "Authorization": `Bearer ${this.apiKey}`,
2386
+ "X-Issuer-Id": this.issuerId
2387
+ },
2388
+ body: JSON.stringify(body, this.bigintReplacer),
2389
+ signal: controller.signal
2390
+ });
2391
+ } catch (err) {
2392
+ if (err.name === "AbortError") {
2393
+ throw new PafiBackendError(
2394
+ "TIMEOUT",
2395
+ `PAFI Backend request timed out after ${this.timeoutMs}ms`,
2396
+ 0
2397
+ );
2398
+ }
2399
+ throw new PafiBackendError(
2400
+ "NETWORK_ERROR",
2401
+ `PAFI Backend unreachable: ${err.message}`,
2402
+ 0
2403
+ );
2404
+ } finally {
2405
+ clearTimeout(timeoutId);
2406
+ }
2407
+ const text = await response.text();
2408
+ if (!response.ok) {
2409
+ let code = "INTERNAL_ERROR";
2410
+ let message = text || response.statusText;
2411
+ let details;
2412
+ let retryAfter;
2413
+ let serverSafeToRetry;
2414
+ try {
2415
+ const parsed = JSON.parse(text);
2416
+ code = parsed.code ?? code;
2417
+ message = parsed.message ?? message;
2418
+ details = parsed.details;
2419
+ if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
2420
+ if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
2421
+ } catch {
2422
+ }
2423
+ throw new PafiBackendError(code, message, response.status, details, {
2424
+ ...retryAfter !== void 0 ? { retryAfter } : {},
2425
+ ...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
2426
+ });
2427
+ }
2428
+ return JSON.parse(text, this.bigintReviver);
2429
+ }
2430
+ /** JSON replacer that stringifies bigints. Paired with bigintReviver. */
2431
+ bigintReplacer = (_key, value) => {
2432
+ return typeof value === "bigint" ? value.toString() : value;
2433
+ };
2434
+ /**
2435
+ * JSON reviver that coerces specific numeric-string fields back to
2436
+ * bigint. The server must send these fields as decimal strings.
2437
+ */
2438
+ bigintReviver = (key, value) => {
2439
+ 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)) {
2440
+ return BigInt(value);
2441
+ }
2442
+ return value;
2443
+ };
2444
+ };
2445
+
1668
2446
  // src/config.ts
1669
- var import_viem7 = require("viem");
2447
+ var import_viem11 = require("viem");
1670
2448
  function createIssuerService(config) {
1671
2449
  if (!config.provider) {
1672
2450
  throw new Error("createIssuerService: provider is required");
@@ -1692,7 +2470,7 @@ function createIssuerService(config) {
1692
2470
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
1693
2471
  );
1694
2472
  }
1695
- const tokenAddresses = rawAddresses.map((a) => (0, import_viem7.getAddress)(a));
2473
+ const tokenAddresses = rawAddresses.map((a) => (0, import_viem11.getAddress)(a));
1696
2474
  const ledger = config.ledger ?? new MemoryPointLedger();
1697
2475
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
1698
2476
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
@@ -1722,8 +2500,7 @@ function createIssuerService(config) {
1722
2500
  if (config.fee) {
1723
2501
  feeManager = new FeeManager({
1724
2502
  ...config.fee,
1725
- provider: config.provider,
1726
- operatorWallet: config.operatorWallet
2503
+ provider: config.provider
1727
2504
  });
1728
2505
  }
1729
2506
  const gatewayConfig = {
@@ -1799,6 +2576,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
1799
2576
  0 && (module.exports = {
1800
2577
  AuthError,
1801
2578
  AuthService,
2579
+ BalanceAggregator,
2580
+ BurnIndexer,
1802
2581
  DefaultPolicyEngine,
1803
2582
  FeeManager,
1804
2583
  InMemoryCursorStore,
@@ -1809,10 +2588,16 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
1809
2588
  MintingGatewayError,
1810
2589
  NonceManager,
1811
2590
  PAFI_ISSUER_SDK_VERSION,
2591
+ PTRedeemError,
2592
+ PTRedeemHandler,
2593
+ PafiBackendClient,
2594
+ PafiBackendError,
1812
2595
  PointIndexer,
1813
2596
  PrivateKeySigner,
1814
2597
  RelayError,
1815
2598
  RelayService,
2599
+ TopUpRedemptionError,
2600
+ TopUpRedemptionHandler,
1816
2601
  authenticateRequest,
1817
2602
  createIssuerService,
1818
2603
  createSubgraphNativeUsdtQuoter,