@pafi-dev/issuer 0.3.0-beta.2 → 0.3.0-beta.4

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,15 +582,18 @@ var RelayError = class extends Error {
582
582
  };
583
583
 
584
584
  // src/relay/relayService.ts
585
- import { encodeFunctionData } from "viem";
585
+ import {
586
+ encodeFunctionData,
587
+ erc20Abi
588
+ } from "viem";
586
589
  import {
587
590
  relayAbi,
588
591
  encodeMintAndSwap,
589
592
  simulateMintAndSwap as coreSimulateMintAndSwap,
590
593
  SimulationError,
591
- RELAYER_V2_ABI,
592
594
  POINT_TOKEN_V2_ABI,
593
- buildPartialUserOperation
595
+ buildPartialUserOperation,
596
+ signMintRequest as signMintRequest2
594
597
  } from "@pafi-dev/core";
595
598
  var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
596
599
  var RelayService = class {
@@ -633,21 +636,11 @@ var RelayService = class {
633
636
  }
634
637
  }
635
638
  /**
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
639
+ * Submit a `mintAndSwap` transaction (legacy v0.2 `/claim-and-swap`).
641
640
  *
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.
641
+ * @deprecated Since 0.3.0 replaced by `prepareMint` / `prepareBurn`
642
+ * in the v1.4 sponsored-UserOp flow. Kept for v0.2.x consumers;
643
+ * scheduled removal in 2.0.
651
644
  */
652
645
  async submitMintAndSwap(params) {
653
646
  if (this.simulateBeforeSubmit && this.provider) {
@@ -717,35 +710,24 @@ var RelayService = class {
717
710
  }
718
711
  }
719
712
  // ==========================================================================
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).
713
+ // v1.4 — Sponsored UserOp preparation (sig-gated mint + burn)
730
714
  // ==========================================================================
731
715
  /**
732
- * Build an unsigned UserOp for Scenario 1 (Mint).
716
+ * Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
717
+ * `PointToken.mint(to, amount, deadline, minterSig)`.
733
718
  *
734
719
  * 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)
720
+ * 1. Issuer backend signs `MintRequest(to=user, amount, nonce, deadline)`
721
+ * with its minter signer (HSM/KMS)`minterSig`.
722
+ * 2. Encode `PointToken.mint(user, amount, deadline, minterSig)`.
723
+ * On-chain, `msg.sender` must equal `to` — satisfied by EIP-7702
724
+ * delegating the user EOA to BatchExecutor.
725
+ * 3. Optional PT fee transfer appended after mint (application-level
726
+ * fee recovery since Relayer v2 no longer exists).
727
+ * 4. Return `PartialUserOperation` ready for Bundler gas estimate +
728
+ * Paymaster sponsorship + user signature.
744
729
  */
745
- prepareMint(params) {
746
- if (!params.relayerAddress) {
747
- throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
748
- }
730
+ async prepareMint(params) {
749
731
  if (!params.batchExecutorAddress) {
750
732
  throw new RelayError(
751
733
  "ENCODE_FAILED",
@@ -755,36 +737,79 @@ var RelayService = class {
755
737
  if (!params.userAddress) {
756
738
  throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
757
739
  }
740
+ if (!params.pointTokenAddress) {
741
+ throw new RelayError(
742
+ "ENCODE_FAILED",
743
+ "prepareMint: pointTokenAddress required"
744
+ );
745
+ }
746
+ if (params.amount <= 0n) {
747
+ throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
748
+ }
749
+ if (!params.issuerSignerWallet) {
750
+ throw new RelayError(
751
+ "ENCODE_FAILED",
752
+ "prepareMint: issuerSignerWallet required (for MintRequest EIP-712 signature)"
753
+ );
754
+ }
755
+ if (params.deadline <= 0n) {
756
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
757
+ }
758
+ let minterSig;
759
+ try {
760
+ const sig = await signMintRequest2(
761
+ params.issuerSignerWallet,
762
+ params.domain,
763
+ {
764
+ to: params.userAddress,
765
+ amount: params.amount,
766
+ nonce: params.mintRequestNonce,
767
+ deadline: params.deadline
768
+ }
769
+ );
770
+ minterSig = sig.serialized;
771
+ } catch (err) {
772
+ throw new RelayError(
773
+ "ENCODE_FAILED",
774
+ `prepareMint: failed to sign MintRequest: ${errorMessage(err)}`,
775
+ err
776
+ );
777
+ }
758
778
  let mintCallData;
759
779
  try {
760
780
  mintCallData = encodeFunctionData({
761
- abi: RELAYER_V2_ABI,
781
+ abi: POINT_TOKEN_V2_ABI,
762
782
  functionName: "mint",
763
- args: [params.mintRequest, params.userSignature, params.issuerSignature]
783
+ args: [params.userAddress, params.amount, params.deadline, minterSig]
764
784
  });
765
785
  } catch (err) {
766
786
  throw new RelayError(
767
787
  "ENCODE_FAILED",
768
- `prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
788
+ `prepareMint: failed to encode PointToken.mint: ${errorMessage(err)}`,
769
789
  err
770
790
  );
771
791
  }
772
792
  const operations = [
773
793
  {
774
- target: params.relayerAddress,
794
+ target: params.pointTokenAddress,
775
795
  value: 0n,
776
796
  data: mintCallData
777
797
  }
778
798
  ];
779
- if (params.mintRequest.feeAmount > 0n) {
799
+ if (params.feeAmount && params.feeAmount > 0n) {
800
+ if (!params.feeRecipient) {
801
+ throw new RelayError(
802
+ "ENCODE_FAILED",
803
+ "prepareMint: feeRecipient required when feeAmount > 0"
804
+ );
805
+ }
780
806
  operations.push({
781
807
  target: params.pointTokenAddress,
782
808
  value: 0n,
783
809
  data: encodeFunctionData({
784
- abi: POINT_TOKEN_V2_ABI,
785
- functionName: "balanceOf",
786
- // placeholder — real impl uses transfer
787
- args: [params.mintRequest.feeRecipient]
810
+ abi: erc20Abi,
811
+ functionName: "transfer",
812
+ args: [params.feeRecipient, params.feeAmount]
788
813
  })
789
814
  });
790
815
  }
@@ -793,7 +818,7 @@ var RelayService = class {
793
818
  nonce: params.aaNonce,
794
819
  operations,
795
820
  gasLimits: {
796
- callGasLimit: params.callGasLimit ?? 500000n,
821
+ callGasLimit: params.callGasLimit ?? 300000n,
797
822
  verificationGasLimit: params.verificationGasLimit ?? 150000n,
798
823
  preVerificationGas: params.preVerificationGas ?? 50000n
799
824
  }
@@ -803,13 +828,13 @@ var RelayService = class {
803
828
  * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
804
829
  *
805
830
  * 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
831
+ * - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
832
+ * usable if the user is a whitelisted burner. Not the typical
833
+ * v1.4 path (users aren't burners); kept for admin/operator tools.
834
+ * - `mode: 'burnWithSig'` `PointToken.burn(from, amount, deadline,
835
+ * burnerSig)`. Issuer signs `BurnRequest` off-chain; user submits
836
+ * via EIP-7702. `msg.sender == from` enforced on-chain. This is
837
+ * the user-initiated redeem path in v1.4.
813
838
  */
814
839
  prepareBurn(params) {
815
840
  if (!params.pointTokenAddress) {
@@ -824,19 +849,24 @@ var RelayService = class {
824
849
  let burnCallData;
825
850
  try {
826
851
  if (params.mode === "burnWithSig") {
827
- if (!params.burnConsent || !params.consentSignature) {
828
- throw new Error("burnWithSig requires burnConsent + consentSignature");
852
+ if (!params.burnRequest || !params.burnerSignature) {
853
+ throw new Error("burnWithSig requires burnRequest + burnerSignature");
829
854
  }
830
855
  burnCallData = encodeFunctionData({
831
856
  abi: POINT_TOKEN_V2_ABI,
832
- functionName: "burnWithSig",
833
- args: [params.burnConsent, params.consentSignature]
857
+ functionName: "burn",
858
+ args: [
859
+ params.burnRequest.from,
860
+ params.burnRequest.amount,
861
+ params.burnRequest.deadline,
862
+ params.burnerSignature
863
+ ]
834
864
  });
835
865
  } else {
836
866
  burnCallData = encodeFunctionData({
837
867
  abi: POINT_TOKEN_V2_ABI,
838
868
  functionName: "burn",
839
- args: [params.amount]
869
+ args: [params.userAddress, params.amount]
840
870
  });
841
871
  }
842
872
  } catch (err) {
@@ -1757,8 +1787,9 @@ var IssuerApiHandlers = class {
1757
1787
 
1758
1788
  // src/api/handlers/ptRedeemHandler.ts
1759
1789
  import { getAddress as getAddress7 } from "viem";
1760
- import { verifyBurnConsent } from "@pafi-dev/core";
1790
+ import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2 } from "@pafi-dev/core";
1761
1791
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1792
+ var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1762
1793
  var PTRedeemError = class extends Error {
1763
1794
  constructor(code, message) {
1764
1795
  super(message);
@@ -1770,11 +1801,14 @@ var PTRedeemError = class extends Error {
1770
1801
  var PTRedeemHandler = class {
1771
1802
  ledger;
1772
1803
  relayService;
1804
+ provider;
1773
1805
  pointTokenAddress;
1774
1806
  batchExecutorAddress;
1775
1807
  chainId;
1776
1808
  domain;
1809
+ burnerSignerWallet;
1777
1810
  redeemLockDurationMs;
1811
+ signatureDeadlineSeconds;
1778
1812
  now;
1779
1813
  constructor(config) {
1780
1814
  if (!config.ledger.reservePendingCredit) {
@@ -1783,46 +1817,68 @@ var PTRedeemHandler = class {
1783
1817
  "PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
1784
1818
  );
1785
1819
  }
1820
+ if (!config.burnerSignerWallet) {
1821
+ throw new PTRedeemError(
1822
+ "SIGNING_FAILED",
1823
+ "PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
1824
+ );
1825
+ }
1786
1826
  this.ledger = config.ledger;
1787
1827
  this.relayService = config.relayService;
1828
+ this.provider = config.provider;
1788
1829
  this.pointTokenAddress = getAddress7(config.pointTokenAddress);
1789
1830
  this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
1790
1831
  this.chainId = config.chainId;
1791
1832
  this.domain = config.domain;
1833
+ this.burnerSignerWallet = config.burnerSignerWallet;
1792
1834
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1835
+ this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1793
1836
  this.now = config.now ?? (() => Date.now());
1794
1837
  }
1795
1838
  async handle(request) {
1796
1839
  if (request.amount <= 0n) {
1797
- throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
1840
+ throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1798
1841
  }
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) {
1842
+ let burnNonce;
1843
+ try {
1844
+ burnNonce = await this.provider.readContract({
1845
+ address: this.pointTokenAddress,
1846
+ abi: POINT_TOKEN_V2_ABI2,
1847
+ functionName: "burnRequestNonces",
1848
+ args: [request.userAddress]
1849
+ });
1850
+ } catch (err) {
1807
1851
  throw new PTRedeemError(
1808
- "EXPIRED_CONSENT",
1809
- `consent deadline (${request.consent.deadline}) already passed`
1852
+ "NONCE_READ_FAILED",
1853
+ `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1810
1854
  );
1811
1855
  }
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
1856
+ const deadline = BigInt(
1857
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1821
1858
  );
1822
- if (!verification.isValid) {
1859
+ const domain = {
1860
+ name: this.domain.name,
1861
+ chainId: this.chainId,
1862
+ verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1863
+ };
1864
+ const burnRequest = {
1865
+ from: request.userAddress,
1866
+ amount: request.amount,
1867
+ nonce: burnNonce,
1868
+ deadline
1869
+ };
1870
+ let burnerSignature;
1871
+ try {
1872
+ const sig = await signBurnRequest(
1873
+ this.burnerSignerWallet,
1874
+ domain,
1875
+ burnRequest
1876
+ );
1877
+ burnerSignature = sig.serialized;
1878
+ } catch (err) {
1823
1879
  throw new PTRedeemError(
1824
- "SIGNATURE_MISMATCH",
1825
- `signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
1880
+ "SIGNING_FAILED",
1881
+ `failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
1826
1882
  );
1827
1883
  }
1828
1884
  const lockId = await this.ledger.reservePendingCredit(
@@ -1837,29 +1893,17 @@ var PTRedeemHandler = class {
1837
1893
  aaNonce: request.aaNonce,
1838
1894
  pointTokenAddress: this.pointTokenAddress,
1839
1895
  batchExecutorAddress: this.batchExecutorAddress,
1840
- burnConsent: request.consent,
1841
- consentSignature: parseSigStruct(request.consentSignature)
1896
+ burnRequest,
1897
+ burnerSignature
1842
1898
  });
1843
1899
  return {
1844
1900
  lockId,
1845
1901
  userOp,
1846
- expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
1902
+ expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
1903
+ signatureDeadline: deadline
1847
1904
  };
1848
1905
  }
1849
1906
  };
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
1907
 
1864
1908
  // src/api/handlers/topUpRedemptionHandler.ts
1865
1909
  import { getAddress as getAddress8 } from "viem";
@@ -1905,24 +1949,10 @@ var TopUpRedemptionHandler = class {
1905
1949
  shortfall
1906
1950
  };
1907
1951
  }
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
1952
  const redeem = await this.ptRedeemHandler.handle({
1921
1953
  userAddress: request.userAddress,
1922
1954
  amount: shortfall,
1923
- consent: request.redeemRequest.consent,
1924
- consentSignature: request.redeemRequest.consentSignature,
1925
- aaNonce: request.redeemRequest.aaNonce
1955
+ aaNonce: request.aaNonce
1926
1956
  });
1927
1957
  return {
1928
1958
  action: "TOP_UP_STARTED",