@pafi-dev/issuer 0.3.0-alpha.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.js CHANGED
@@ -582,11 +582,15 @@ var RelayError = class extends Error {
582
582
  };
583
583
 
584
584
  // src/relay/relayService.ts
585
+ import { encodeFunctionData } from "viem";
585
586
  import {
586
587
  relayAbi,
587
588
  encodeMintAndSwap,
588
589
  simulateMintAndSwap as coreSimulateMintAndSwap,
589
- SimulationError
590
+ SimulationError,
591
+ RELAYER_V2_ABI,
592
+ POINT_TOKEN_V2_ABI,
593
+ buildPartialUserOperation
590
594
  } from "@pafi-dev/core";
591
595
  var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
592
596
  var RelayService = class {
@@ -712,6 +716,154 @@ var RelayService = class {
712
716
  );
713
717
  }
714
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");
748
+ }
749
+ if (!params.batchExecutorAddress) {
750
+ throw new RelayError(
751
+ "ENCODE_FAILED",
752
+ "prepareMint: batchExecutorAddress required"
753
+ );
754
+ }
755
+ if (!params.userAddress) {
756
+ throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
757
+ }
758
+ let mintCallData;
759
+ try {
760
+ mintCallData = encodeFunctionData({
761
+ abi: RELAYER_V2_ABI,
762
+ functionName: "mint",
763
+ args: [params.mintRequest, params.userSignature, params.issuerSignature]
764
+ });
765
+ } catch (err) {
766
+ throw new RelayError(
767
+ "ENCODE_FAILED",
768
+ `prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
769
+ err
770
+ );
771
+ }
772
+ const operations = [
773
+ {
774
+ target: params.relayerAddress,
775
+ value: 0n,
776
+ data: mintCallData
777
+ }
778
+ ];
779
+ if (params.mintRequest.feeAmount > 0n) {
780
+ operations.push({
781
+ target: params.pointTokenAddress,
782
+ value: 0n,
783
+ data: encodeFunctionData({
784
+ abi: POINT_TOKEN_V2_ABI,
785
+ functionName: "balanceOf",
786
+ // placeholder — real impl uses transfer
787
+ args: [params.mintRequest.feeRecipient]
788
+ })
789
+ });
790
+ }
791
+ return buildPartialUserOperation({
792
+ sender: params.userAddress,
793
+ nonce: params.aaNonce,
794
+ operations,
795
+ gasLimits: {
796
+ callGasLimit: params.callGasLimit ?? 500000n,
797
+ verificationGasLimit: params.verificationGasLimit ?? 150000n,
798
+ preVerificationGas: params.preVerificationGas ?? 50000n
799
+ }
800
+ });
801
+ }
802
+ /**
803
+ * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
804
+ *
805
+ * 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
813
+ */
814
+ prepareBurn(params) {
815
+ if (!params.pointTokenAddress) {
816
+ throw new RelayError("ENCODE_FAILED", "prepareBurn: pointTokenAddress required");
817
+ }
818
+ if (!params.batchExecutorAddress) {
819
+ throw new RelayError(
820
+ "ENCODE_FAILED",
821
+ "prepareBurn: batchExecutorAddress required"
822
+ );
823
+ }
824
+ let burnCallData;
825
+ try {
826
+ if (params.mode === "burnWithSig") {
827
+ if (!params.burnConsent || !params.consentSignature) {
828
+ throw new Error("burnWithSig requires burnConsent + consentSignature");
829
+ }
830
+ burnCallData = encodeFunctionData({
831
+ abi: POINT_TOKEN_V2_ABI,
832
+ functionName: "burnWithSig",
833
+ args: [params.burnConsent, params.consentSignature]
834
+ });
835
+ } else {
836
+ burnCallData = encodeFunctionData({
837
+ abi: POINT_TOKEN_V2_ABI,
838
+ functionName: "burn",
839
+ args: [params.amount]
840
+ });
841
+ }
842
+ } catch (err) {
843
+ throw new RelayError(
844
+ "ENCODE_FAILED",
845
+ `prepareBurn: failed to encode burn call: ${errorMessage(err)}`,
846
+ err
847
+ );
848
+ }
849
+ const operations = [
850
+ {
851
+ target: params.pointTokenAddress,
852
+ value: 0n,
853
+ data: burnCallData
854
+ }
855
+ ];
856
+ return buildPartialUserOperation({
857
+ sender: params.userAddress,
858
+ nonce: params.aaNonce,
859
+ operations,
860
+ gasLimits: {
861
+ callGasLimit: params.callGasLimit ?? 300000n,
862
+ verificationGasLimit: params.verificationGasLimit ?? 150000n,
863
+ preVerificationGas: params.preVerificationGas ?? 50000n
864
+ }
865
+ });
866
+ }
715
867
  };
716
868
  function errorMessage(err) {
717
869
  return err instanceof Error ? err.message : String(err);
@@ -1603,6 +1755,183 @@ var IssuerApiHandlers = class {
1603
1755
  }
1604
1756
  };
1605
1757
 
1758
+ // src/api/handlers/ptRedeemHandler.ts
1759
+ import { getAddress as getAddress7 } from "viem";
1760
+ import { verifyBurnConsent } from "@pafi-dev/core";
1761
+ var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1762
+ var PTRedeemError = class extends Error {
1763
+ constructor(code, message) {
1764
+ super(message);
1765
+ this.code = code;
1766
+ this.name = "PTRedeemError";
1767
+ }
1768
+ code;
1769
+ };
1770
+ var PTRedeemHandler = class {
1771
+ ledger;
1772
+ relayService;
1773
+ pointTokenAddress;
1774
+ batchExecutorAddress;
1775
+ chainId;
1776
+ domain;
1777
+ redeemLockDurationMs;
1778
+ now;
1779
+ constructor(config) {
1780
+ if (!config.ledger.reservePendingCredit) {
1781
+ throw new PTRedeemError(
1782
+ "LEDGER_NOT_SUPPORTED",
1783
+ "PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
1784
+ );
1785
+ }
1786
+ this.ledger = config.ledger;
1787
+ this.relayService = config.relayService;
1788
+ this.pointTokenAddress = getAddress7(config.pointTokenAddress);
1789
+ this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
1790
+ this.chainId = config.chainId;
1791
+ this.domain = config.domain;
1792
+ this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1793
+ this.now = config.now ?? (() => Date.now());
1794
+ }
1795
+ async handle(request) {
1796
+ 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
+ );
1804
+ }
1805
+ const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
1806
+ if (request.consent.deadline <= nowSeconds) {
1807
+ throw new PTRedeemError(
1808
+ "EXPIRED_CONSENT",
1809
+ `consent deadline (${request.consent.deadline}) already passed`
1810
+ );
1811
+ }
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
1821
+ );
1822
+ if (!verification.isValid) {
1823
+ throw new PTRedeemError(
1824
+ "SIGNATURE_MISMATCH",
1825
+ `signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
1826
+ );
1827
+ }
1828
+ const lockId = await this.ledger.reservePendingCredit(
1829
+ request.userAddress,
1830
+ request.amount,
1831
+ this.redeemLockDurationMs,
1832
+ this.pointTokenAddress
1833
+ );
1834
+ const userOp = this.relayService.prepareBurn({
1835
+ mode: "burnWithSig",
1836
+ userAddress: request.userAddress,
1837
+ aaNonce: request.aaNonce,
1838
+ pointTokenAddress: this.pointTokenAddress,
1839
+ batchExecutorAddress: this.batchExecutorAddress,
1840
+ burnConsent: request.consent,
1841
+ consentSignature: parseSigStruct(request.consentSignature)
1842
+ });
1843
+ return {
1844
+ lockId,
1845
+ userOp,
1846
+ expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
1847
+ };
1848
+ }
1849
+ };
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
+
1864
+ // src/api/handlers/topUpRedemptionHandler.ts
1865
+ import { getAddress as getAddress8 } from "viem";
1866
+ import { getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
1867
+ var TopUpRedemptionError = class extends Error {
1868
+ constructor(code, message) {
1869
+ super(message);
1870
+ this.code = code;
1871
+ this.name = "TopUpRedemptionError";
1872
+ }
1873
+ code;
1874
+ };
1875
+ var TopUpRedemptionHandler = class {
1876
+ ledger;
1877
+ ptRedeemHandler;
1878
+ provider;
1879
+ pointTokenAddress;
1880
+ constructor(config) {
1881
+ this.ledger = config.ledger;
1882
+ this.ptRedeemHandler = config.ptRedeemHandler;
1883
+ this.provider = config.provider;
1884
+ this.pointTokenAddress = getAddress8(config.pointTokenAddress);
1885
+ }
1886
+ async handle(request) {
1887
+ const offChainBalance = await this.ledger.getBalance(
1888
+ request.userAddress,
1889
+ this.pointTokenAddress
1890
+ );
1891
+ if (offChainBalance >= request.requiredAmount) {
1892
+ return { action: "NO_TOP_UP_NEEDED", offChainBalance };
1893
+ }
1894
+ const shortfall = request.requiredAmount - offChainBalance;
1895
+ const onChainBalance = await getPointTokenBalance2(
1896
+ this.provider,
1897
+ this.pointTokenAddress,
1898
+ request.userAddress
1899
+ );
1900
+ if (onChainBalance < shortfall) {
1901
+ return {
1902
+ action: "INSUFFICIENT_ONCHAIN",
1903
+ offChainBalance,
1904
+ onChainBalance,
1905
+ shortfall
1906
+ };
1907
+ }
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
+ const redeem = await this.ptRedeemHandler.handle({
1921
+ userAddress: request.userAddress,
1922
+ amount: shortfall,
1923
+ consent: request.redeemRequest.consent,
1924
+ consentSignature: request.redeemRequest.consentSignature,
1925
+ aaNonce: request.redeemRequest.aaNonce
1926
+ });
1927
+ return {
1928
+ action: "TOP_UP_STARTED",
1929
+ shortfall,
1930
+ redeem
1931
+ };
1932
+ }
1933
+ };
1934
+
1606
1935
  // src/pools/subgraphPoolsProvider.ts
1607
1936
  var DEFAULT_CACHE_TTL_MS = 3e4;
1608
1937
  var POOL_QUERY = `
@@ -1815,7 +2144,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
1815
2144
  }
1816
2145
 
1817
2146
  // src/balance/balanceAggregator.ts
1818
- import { getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
2147
+ import { getPointTokenBalance as getPointTokenBalance3 } from "@pafi-dev/core";
1819
2148
  var BalanceAggregator = class {
1820
2149
  provider;
1821
2150
  ledger;
@@ -1836,7 +2165,7 @@ var BalanceAggregator = class {
1836
2165
  async getCombinedBalance(user, pointToken) {
1837
2166
  const [offChain, onChain] = await Promise.all([
1838
2167
  this.ledger.getBalance(user, pointToken),
1839
- getPointTokenBalance2(this.provider, pointToken, user)
2168
+ getPointTokenBalance3(this.provider, pointToken, user)
1840
2169
  ]);
1841
2170
  return {
1842
2171
  offChain,
@@ -2081,7 +2410,7 @@ var PafiBackendClient = class {
2081
2410
  };
2082
2411
 
2083
2412
  // src/config.ts
2084
- import { getAddress as getAddress7 } from "viem";
2413
+ import { getAddress as getAddress9 } from "viem";
2085
2414
  function createIssuerService(config) {
2086
2415
  if (!config.provider) {
2087
2416
  throw new Error("createIssuerService: provider is required");
@@ -2107,7 +2436,7 @@ function createIssuerService(config) {
2107
2436
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
2108
2437
  );
2109
2438
  }
2110
- const tokenAddresses = rawAddresses.map((a) => getAddress7(a));
2439
+ const tokenAddresses = rawAddresses.map((a) => getAddress9(a));
2111
2440
  const ledger = config.ledger ?? new MemoryPointLedger();
2112
2441
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
2113
2442
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
@@ -2224,12 +2553,16 @@ export {
2224
2553
  MintingGatewayError,
2225
2554
  NonceManager,
2226
2555
  PAFI_ISSUER_SDK_VERSION,
2556
+ PTRedeemError,
2557
+ PTRedeemHandler,
2227
2558
  PafiBackendClient,
2228
2559
  PafiBackendError,
2229
2560
  PointIndexer,
2230
2561
  PrivateKeySigner,
2231
2562
  RelayError,
2232
2563
  RelayService,
2564
+ TopUpRedemptionError,
2565
+ TopUpRedemptionHandler,
2233
2566
  authenticateRequest,
2234
2567
  createIssuerService,
2235
2568
  createSubgraphNativeUsdtQuoter,