@pafi-dev/issuer 0.3.0-beta.3 → 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
@@ -592,7 +592,8 @@ import {
592
592
  simulateMintAndSwap as coreSimulateMintAndSwap,
593
593
  SimulationError,
594
594
  POINT_TOKEN_V2_ABI,
595
- buildPartialUserOperation
595
+ buildPartialUserOperation,
596
+ signMintRequest as signMintRequest2
596
597
  } from "@pafi-dev/core";
597
598
  var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
598
599
  var RelayService = class {
@@ -635,21 +636,11 @@ var RelayService = class {
635
636
  }
636
637
  }
637
638
  /**
638
- * Submit a `mintAndSwap` transaction. Flow:
639
+ * Submit a `mintAndSwap` transaction (legacy v0.2 `/claim-and-swap`).
639
640
  *
640
- * 1. (optional) pre-flight simulate via provider
641
- * 2. writeContract through the operator wallet
642
- * 3. (optional) wait for the receipt and surface gasUsed / status
643
- *
644
- * Throws a typed `RelayError` on any failure so the MintingGateway can
645
- * decide whether to release the ledger lock (`SUBMIT_FAILED` and
646
- * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
647
- * need manual review because the tx may still land).
648
- *
649
- * @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
650
- * `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
651
- * still needs to finalize Relayer v2 ABI before the replacements
652
- * 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.
653
644
  */
654
645
  async submitMintAndSwap(params) {
655
646
  if (this.simulateBeforeSubmit && this.provider) {
@@ -719,45 +710,24 @@ var RelayService = class {
719
710
  }
720
711
  }
721
712
  // ==========================================================================
722
- // v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
723
- // ==========================================================================
724
- //
725
- // These two methods build unsigned `PartialUserOperation` payloads for
726
- // the Frontend to sign (via Privy) and submit to the Bundler. The
727
- // Issuer Backend no longer broadcasts — that's the Frontend's job.
728
- //
729
- // Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
730
- // When SC delivers real ABIs, the imports swap but these method bodies
731
- // stay the same (calldata encoder is ABI-driven).
713
+ // v1.4 — Sponsored UserOp preparation (sig-gated mint + burn)
732
714
  // ==========================================================================
733
715
  /**
734
- * 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)`.
735
718
  *
736
719
  * Flow:
737
- * 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
738
- * 2. Optionally append a PT fee transfer from user feeRecipient
739
- * (fee recovery happens on-chain via BatchExecutor, not via an
740
- * operator wallet)
741
- * 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
742
- * 4. Return a `PartialUserOperation` ready for:
743
- * - gas estimation (Bundler)
744
- * - paymaster sponsorship (PAFI Backend)
745
- * - user signature (Privy)
746
- */
747
- /**
748
- * Build an unsigned UserOp for Scenario 1 (Mint) — direct
749
- * `PointToken.mint(amount)` flow (v1.4 post-Relayer-removal).
750
- *
751
- * `msg.sender` inside the batch = user EOA via EIP-7702 delegation.
752
- * `PointToken.mint` checks the caller is on its `minters` allowlist
753
- * (gg56 BE pre-validates this off-chain to avoid wasted gas).
754
- *
755
- * Optional PT fee transfer is appended after the mint when
756
- * `feeAmount > 0` — this is application-level fee recovery (no
757
- * Relayer doing it for us). The user must end up with `amount - fee`
758
- * net PT after the batch executes.
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.
759
729
  */
760
- prepareMint(params) {
730
+ async prepareMint(params) {
761
731
  if (!params.batchExecutorAddress) {
762
732
  throw new RelayError(
763
733
  "ENCODE_FAILED",
@@ -776,12 +746,41 @@ var RelayService = class {
776
746
  if (params.amount <= 0n) {
777
747
  throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
778
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
+ }
779
778
  let mintCallData;
780
779
  try {
781
780
  mintCallData = encodeFunctionData({
782
781
  abi: POINT_TOKEN_V2_ABI,
783
782
  functionName: "mint",
784
- args: [params.amount]
783
+ args: [params.userAddress, params.amount, params.deadline, minterSig]
785
784
  });
786
785
  } catch (err) {
787
786
  throw new RelayError(
@@ -829,13 +828,13 @@ var RelayService = class {
829
828
  * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
830
829
  *
831
830
  * Two modes:
832
- * - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
833
- * via EIP-7702 delegation is the user, so no signature needed
834
- * on-chain (the BurnConsent was already verified off-chain by
835
- * the issuer backend before we got here)
836
- * - `mode: 'burnWithSig'` `PointToken.burnWithSig(consent, sig)`;
837
- * used when the issuer hasn't verified the consent and the
838
- * 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.
839
838
  */
840
839
  prepareBurn(params) {
841
840
  if (!params.pointTokenAddress) {
@@ -850,19 +849,24 @@ var RelayService = class {
850
849
  let burnCallData;
851
850
  try {
852
851
  if (params.mode === "burnWithSig") {
853
- if (!params.burnConsent || !params.consentSignature) {
854
- throw new Error("burnWithSig requires burnConsent + consentSignature");
852
+ if (!params.burnRequest || !params.burnerSignature) {
853
+ throw new Error("burnWithSig requires burnRequest + burnerSignature");
855
854
  }
856
855
  burnCallData = encodeFunctionData({
857
856
  abi: POINT_TOKEN_V2_ABI,
858
- functionName: "burnWithSig",
859
- 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
+ ]
860
864
  });
861
865
  } else {
862
866
  burnCallData = encodeFunctionData({
863
867
  abi: POINT_TOKEN_V2_ABI,
864
868
  functionName: "burn",
865
- args: [params.amount]
869
+ args: [params.userAddress, params.amount]
866
870
  });
867
871
  }
868
872
  } catch (err) {
@@ -1783,8 +1787,9 @@ var IssuerApiHandlers = class {
1783
1787
 
1784
1788
  // src/api/handlers/ptRedeemHandler.ts
1785
1789
  import { getAddress as getAddress7 } from "viem";
1786
- import { verifyBurnConsent } from "@pafi-dev/core";
1790
+ import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2 } from "@pafi-dev/core";
1787
1791
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1792
+ var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1788
1793
  var PTRedeemError = class extends Error {
1789
1794
  constructor(code, message) {
1790
1795
  super(message);
@@ -1796,11 +1801,14 @@ var PTRedeemError = class extends Error {
1796
1801
  var PTRedeemHandler = class {
1797
1802
  ledger;
1798
1803
  relayService;
1804
+ provider;
1799
1805
  pointTokenAddress;
1800
1806
  batchExecutorAddress;
1801
1807
  chainId;
1802
1808
  domain;
1809
+ burnerSignerWallet;
1803
1810
  redeemLockDurationMs;
1811
+ signatureDeadlineSeconds;
1804
1812
  now;
1805
1813
  constructor(config) {
1806
1814
  if (!config.ledger.reservePendingCredit) {
@@ -1809,46 +1817,68 @@ var PTRedeemHandler = class {
1809
1817
  "PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
1810
1818
  );
1811
1819
  }
1820
+ if (!config.burnerSignerWallet) {
1821
+ throw new PTRedeemError(
1822
+ "SIGNING_FAILED",
1823
+ "PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
1824
+ );
1825
+ }
1812
1826
  this.ledger = config.ledger;
1813
1827
  this.relayService = config.relayService;
1828
+ this.provider = config.provider;
1814
1829
  this.pointTokenAddress = getAddress7(config.pointTokenAddress);
1815
1830
  this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
1816
1831
  this.chainId = config.chainId;
1817
1832
  this.domain = config.domain;
1833
+ this.burnerSignerWallet = config.burnerSignerWallet;
1818
1834
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1835
+ this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1819
1836
  this.now = config.now ?? (() => Date.now());
1820
1837
  }
1821
1838
  async handle(request) {
1822
1839
  if (request.amount <= 0n) {
1823
- throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
1840
+ throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1824
1841
  }
1825
- if (request.consent.amount !== request.amount) {
1826
- throw new PTRedeemError(
1827
- "AMOUNT_MISMATCH",
1828
- `consent.amount (${request.consent.amount}) must match request.amount (${request.amount})`
1829
- );
1830
- }
1831
- const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
1832
- 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) {
1833
1851
  throw new PTRedeemError(
1834
- "EXPIRED_CONSENT",
1835
- `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)}`
1836
1854
  );
1837
1855
  }
1838
- const verification = await verifyBurnConsent(
1839
- {
1840
- name: this.domain.name,
1841
- chainId: this.chainId,
1842
- verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1843
- },
1844
- request.consent,
1845
- request.consentSignature,
1846
- request.userAddress
1856
+ const deadline = BigInt(
1857
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1847
1858
  );
1848
- 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) {
1849
1879
  throw new PTRedeemError(
1850
- "SIGNATURE_MISMATCH",
1851
- `signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
1880
+ "SIGNING_FAILED",
1881
+ `failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
1852
1882
  );
1853
1883
  }
1854
1884
  const lockId = await this.ledger.reservePendingCredit(
@@ -1863,29 +1893,17 @@ var PTRedeemHandler = class {
1863
1893
  aaNonce: request.aaNonce,
1864
1894
  pointTokenAddress: this.pointTokenAddress,
1865
1895
  batchExecutorAddress: this.batchExecutorAddress,
1866
- burnConsent: request.consent,
1867
- consentSignature: parseSigStruct(request.consentSignature)
1896
+ burnRequest,
1897
+ burnerSignature
1868
1898
  });
1869
1899
  return {
1870
1900
  lockId,
1871
1901
  userOp,
1872
- expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
1902
+ expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
1903
+ signatureDeadline: deadline
1873
1904
  };
1874
1905
  }
1875
1906
  };
1876
- function parseSigStruct(serialized) {
1877
- const raw = serialized.slice(2);
1878
- if (raw.length !== 130) {
1879
- throw new PTRedeemError(
1880
- "INVALID_CONSENT",
1881
- `signature must be 65 bytes, got ${raw.length / 2}`
1882
- );
1883
- }
1884
- const r = `0x${raw.slice(0, 64)}`;
1885
- const s = `0x${raw.slice(64, 128)}`;
1886
- const v = parseInt(raw.slice(128, 130), 16);
1887
- return { v, r, s };
1888
- }
1889
1907
 
1890
1908
  // src/api/handlers/topUpRedemptionHandler.ts
1891
1909
  import { getAddress as getAddress8 } from "viem";
@@ -1931,24 +1949,10 @@ var TopUpRedemptionHandler = class {
1931
1949
  shortfall
1932
1950
  };
1933
1951
  }
1934
- if (request.redeemRequest.consent.amount < shortfall) {
1935
- throw new TopUpRedemptionError(
1936
- "CONSENT_AMOUNT_TOO_LOW",
1937
- `consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
1938
- );
1939
- }
1940
- if (request.redeemRequest.consent.amount !== shortfall) {
1941
- throw new TopUpRedemptionError(
1942
- "CONSENT_AMOUNT_TOO_LOW",
1943
- `consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
1944
- );
1945
- }
1946
1952
  const redeem = await this.ptRedeemHandler.handle({
1947
1953
  userAddress: request.userAddress,
1948
1954
  amount: shortfall,
1949
- consent: request.redeemRequest.consent,
1950
- consentSignature: request.redeemRequest.consentSignature,
1951
- aaNonce: request.redeemRequest.aaNonce
1955
+ aaNonce: request.aaNonce
1952
1956
  });
1953
1957
  return {
1954
1958
  action: "TOP_UP_STARTED",