@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.cjs CHANGED
@@ -677,21 +677,11 @@ var RelayService = class {
677
677
  }
678
678
  }
679
679
  /**
680
- * Submit a `mintAndSwap` transaction. Flow:
680
+ * Submit a `mintAndSwap` transaction (legacy v0.2 `/claim-and-swap`).
681
681
  *
682
- * 1. (optional) pre-flight simulate via provider
683
- * 2. writeContract through the operator wallet
684
- * 3. (optional) wait for the receipt and surface gasUsed / status
685
- *
686
- * Throws a typed `RelayError` on any failure so the MintingGateway can
687
- * decide whether to release the ledger lock (`SUBMIT_FAILED` and
688
- * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
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.
682
+ * @deprecated Since 0.3.0 replaced by `prepareMint` / `prepareBurn`
683
+ * in the v1.4 sponsored-UserOp flow. Kept for v0.2.x consumers;
684
+ * scheduled removal in 2.0.
695
685
  */
696
686
  async submitMintAndSwap(params) {
697
687
  if (this.simulateBeforeSubmit && this.provider) {
@@ -761,35 +751,24 @@ var RelayService = class {
761
751
  }
762
752
  }
763
753
  // ==========================================================================
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).
754
+ // v1.4 — Sponsored UserOp preparation (sig-gated mint + burn)
774
755
  // ==========================================================================
775
756
  /**
776
- * Build an unsigned UserOp for Scenario 1 (Mint).
757
+ * Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
758
+ * `PointToken.mint(to, amount, deadline, minterSig)`.
777
759
  *
778
760
  * 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)
761
+ * 1. Issuer backend signs `MintRequest(to=user, amount, nonce, deadline)`
762
+ * with its minter signer (HSM/KMS)`minterSig`.
763
+ * 2. Encode `PointToken.mint(user, amount, deadline, minterSig)`.
764
+ * On-chain, `msg.sender` must equal `to` — satisfied by EIP-7702
765
+ * delegating the user EOA to BatchExecutor.
766
+ * 3. Optional PT fee transfer appended after mint (application-level
767
+ * fee recovery since Relayer v2 no longer exists).
768
+ * 4. Return `PartialUserOperation` ready for Bundler gas estimate +
769
+ * Paymaster sponsorship + user signature.
788
770
  */
789
- prepareMint(params) {
790
- if (!params.relayerAddress) {
791
- throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
792
- }
771
+ async prepareMint(params) {
793
772
  if (!params.batchExecutorAddress) {
794
773
  throw new RelayError(
795
774
  "ENCODE_FAILED",
@@ -799,36 +778,79 @@ var RelayService = class {
799
778
  if (!params.userAddress) {
800
779
  throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
801
780
  }
781
+ if (!params.pointTokenAddress) {
782
+ throw new RelayError(
783
+ "ENCODE_FAILED",
784
+ "prepareMint: pointTokenAddress required"
785
+ );
786
+ }
787
+ if (params.amount <= 0n) {
788
+ throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
789
+ }
790
+ if (!params.issuerSignerWallet) {
791
+ throw new RelayError(
792
+ "ENCODE_FAILED",
793
+ "prepareMint: issuerSignerWallet required (for MintRequest EIP-712 signature)"
794
+ );
795
+ }
796
+ if (params.deadline <= 0n) {
797
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
798
+ }
799
+ let minterSig;
800
+ try {
801
+ const sig = await (0, import_core3.signMintRequest)(
802
+ params.issuerSignerWallet,
803
+ params.domain,
804
+ {
805
+ to: params.userAddress,
806
+ amount: params.amount,
807
+ nonce: params.mintRequestNonce,
808
+ deadline: params.deadline
809
+ }
810
+ );
811
+ minterSig = sig.serialized;
812
+ } catch (err) {
813
+ throw new RelayError(
814
+ "ENCODE_FAILED",
815
+ `prepareMint: failed to sign MintRequest: ${errorMessage(err)}`,
816
+ err
817
+ );
818
+ }
802
819
  let mintCallData;
803
820
  try {
804
821
  mintCallData = (0, import_viem5.encodeFunctionData)({
805
- abi: import_core3.RELAYER_V2_ABI,
822
+ abi: import_core3.POINT_TOKEN_V2_ABI,
806
823
  functionName: "mint",
807
- args: [params.mintRequest, params.userSignature, params.issuerSignature]
824
+ args: [params.userAddress, params.amount, params.deadline, minterSig]
808
825
  });
809
826
  } catch (err) {
810
827
  throw new RelayError(
811
828
  "ENCODE_FAILED",
812
- `prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
829
+ `prepareMint: failed to encode PointToken.mint: ${errorMessage(err)}`,
813
830
  err
814
831
  );
815
832
  }
816
833
  const operations = [
817
834
  {
818
- target: params.relayerAddress,
835
+ target: params.pointTokenAddress,
819
836
  value: 0n,
820
837
  data: mintCallData
821
838
  }
822
839
  ];
823
- if (params.mintRequest.feeAmount > 0n) {
840
+ if (params.feeAmount && params.feeAmount > 0n) {
841
+ if (!params.feeRecipient) {
842
+ throw new RelayError(
843
+ "ENCODE_FAILED",
844
+ "prepareMint: feeRecipient required when feeAmount > 0"
845
+ );
846
+ }
824
847
  operations.push({
825
848
  target: params.pointTokenAddress,
826
849
  value: 0n,
827
850
  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]
851
+ abi: import_viem5.erc20Abi,
852
+ functionName: "transfer",
853
+ args: [params.feeRecipient, params.feeAmount]
832
854
  })
833
855
  });
834
856
  }
@@ -837,7 +859,7 @@ var RelayService = class {
837
859
  nonce: params.aaNonce,
838
860
  operations,
839
861
  gasLimits: {
840
- callGasLimit: params.callGasLimit ?? 500000n,
862
+ callGasLimit: params.callGasLimit ?? 300000n,
841
863
  verificationGasLimit: params.verificationGasLimit ?? 150000n,
842
864
  preVerificationGas: params.preVerificationGas ?? 50000n
843
865
  }
@@ -847,13 +869,13 @@ var RelayService = class {
847
869
  * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
848
870
  *
849
871
  * 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
872
+ * - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
873
+ * usable if the user is a whitelisted burner. Not the typical
874
+ * v1.4 path (users aren't burners); kept for admin/operator tools.
875
+ * - `mode: 'burnWithSig'` `PointToken.burn(from, amount, deadline,
876
+ * burnerSig)`. Issuer signs `BurnRequest` off-chain; user submits
877
+ * via EIP-7702. `msg.sender == from` enforced on-chain. This is
878
+ * the user-initiated redeem path in v1.4.
857
879
  */
858
880
  prepareBurn(params) {
859
881
  if (!params.pointTokenAddress) {
@@ -868,19 +890,24 @@ var RelayService = class {
868
890
  let burnCallData;
869
891
  try {
870
892
  if (params.mode === "burnWithSig") {
871
- if (!params.burnConsent || !params.consentSignature) {
872
- throw new Error("burnWithSig requires burnConsent + consentSignature");
893
+ if (!params.burnRequest || !params.burnerSignature) {
894
+ throw new Error("burnWithSig requires burnRequest + burnerSignature");
873
895
  }
874
896
  burnCallData = (0, import_viem5.encodeFunctionData)({
875
897
  abi: import_core3.POINT_TOKEN_V2_ABI,
876
- functionName: "burnWithSig",
877
- args: [params.burnConsent, params.consentSignature]
898
+ functionName: "burn",
899
+ args: [
900
+ params.burnRequest.from,
901
+ params.burnRequest.amount,
902
+ params.burnRequest.deadline,
903
+ params.burnerSignature
904
+ ]
878
905
  });
879
906
  } else {
880
907
  burnCallData = (0, import_viem5.encodeFunctionData)({
881
908
  abi: import_core3.POINT_TOKEN_V2_ABI,
882
909
  functionName: "burn",
883
- args: [params.amount]
910
+ args: [params.userAddress, params.amount]
884
911
  });
885
912
  }
886
913
  } catch (err) {
@@ -1793,6 +1820,7 @@ var IssuerApiHandlers = class {
1793
1820
  var import_viem9 = require("viem");
1794
1821
  var import_core6 = require("@pafi-dev/core");
1795
1822
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1823
+ var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1796
1824
  var PTRedeemError = class extends Error {
1797
1825
  constructor(code, message) {
1798
1826
  super(message);
@@ -1804,11 +1832,14 @@ var PTRedeemError = class extends Error {
1804
1832
  var PTRedeemHandler = class {
1805
1833
  ledger;
1806
1834
  relayService;
1835
+ provider;
1807
1836
  pointTokenAddress;
1808
1837
  batchExecutorAddress;
1809
1838
  chainId;
1810
1839
  domain;
1840
+ burnerSignerWallet;
1811
1841
  redeemLockDurationMs;
1842
+ signatureDeadlineSeconds;
1812
1843
  now;
1813
1844
  constructor(config) {
1814
1845
  if (!config.ledger.reservePendingCredit) {
@@ -1817,46 +1848,68 @@ var PTRedeemHandler = class {
1817
1848
  "PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
1818
1849
  );
1819
1850
  }
1851
+ if (!config.burnerSignerWallet) {
1852
+ throw new PTRedeemError(
1853
+ "SIGNING_FAILED",
1854
+ "PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
1855
+ );
1856
+ }
1820
1857
  this.ledger = config.ledger;
1821
1858
  this.relayService = config.relayService;
1859
+ this.provider = config.provider;
1822
1860
  this.pointTokenAddress = (0, import_viem9.getAddress)(config.pointTokenAddress);
1823
1861
  this.batchExecutorAddress = (0, import_viem9.getAddress)(config.batchExecutorAddress);
1824
1862
  this.chainId = config.chainId;
1825
1863
  this.domain = config.domain;
1864
+ this.burnerSignerWallet = config.burnerSignerWallet;
1826
1865
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1866
+ this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1827
1867
  this.now = config.now ?? (() => Date.now());
1828
1868
  }
1829
1869
  async handle(request) {
1830
1870
  if (request.amount <= 0n) {
1831
- throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
1871
+ throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1832
1872
  }
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) {
1873
+ let burnNonce;
1874
+ try {
1875
+ burnNonce = await this.provider.readContract({
1876
+ address: this.pointTokenAddress,
1877
+ abi: import_core6.POINT_TOKEN_V2_ABI,
1878
+ functionName: "burnRequestNonces",
1879
+ args: [request.userAddress]
1880
+ });
1881
+ } catch (err) {
1841
1882
  throw new PTRedeemError(
1842
- "EXPIRED_CONSENT",
1843
- `consent deadline (${request.consent.deadline}) already passed`
1883
+ "NONCE_READ_FAILED",
1884
+ `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1844
1885
  );
1845
1886
  }
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
1887
+ const deadline = BigInt(
1888
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1855
1889
  );
1856
- if (!verification.isValid) {
1890
+ const domain = {
1891
+ name: this.domain.name,
1892
+ chainId: this.chainId,
1893
+ verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1894
+ };
1895
+ const burnRequest = {
1896
+ from: request.userAddress,
1897
+ amount: request.amount,
1898
+ nonce: burnNonce,
1899
+ deadline
1900
+ };
1901
+ let burnerSignature;
1902
+ try {
1903
+ const sig = await (0, import_core6.signBurnRequest)(
1904
+ this.burnerSignerWallet,
1905
+ domain,
1906
+ burnRequest
1907
+ );
1908
+ burnerSignature = sig.serialized;
1909
+ } catch (err) {
1857
1910
  throw new PTRedeemError(
1858
- "SIGNATURE_MISMATCH",
1859
- `signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
1911
+ "SIGNING_FAILED",
1912
+ `failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
1860
1913
  );
1861
1914
  }
1862
1915
  const lockId = await this.ledger.reservePendingCredit(
@@ -1871,29 +1924,17 @@ var PTRedeemHandler = class {
1871
1924
  aaNonce: request.aaNonce,
1872
1925
  pointTokenAddress: this.pointTokenAddress,
1873
1926
  batchExecutorAddress: this.batchExecutorAddress,
1874
- burnConsent: request.consent,
1875
- consentSignature: parseSigStruct(request.consentSignature)
1927
+ burnRequest,
1928
+ burnerSignature
1876
1929
  });
1877
1930
  return {
1878
1931
  lockId,
1879
1932
  userOp,
1880
- expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
1933
+ expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
1934
+ signatureDeadline: deadline
1881
1935
  };
1882
1936
  }
1883
1937
  };
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
1938
 
1898
1939
  // src/api/handlers/topUpRedemptionHandler.ts
1899
1940
  var import_viem10 = require("viem");
@@ -1939,24 +1980,10 @@ var TopUpRedemptionHandler = class {
1939
1980
  shortfall
1940
1981
  };
1941
1982
  }
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
1983
  const redeem = await this.ptRedeemHandler.handle({
1955
1984
  userAddress: request.userAddress,
1956
1985
  amount: shortfall,
1957
- consent: request.redeemRequest.consent,
1958
- consentSignature: request.redeemRequest.consentSignature,
1959
- aaNonce: request.redeemRequest.aaNonce
1986
+ aaNonce: request.aaNonce
1960
1987
  });
1961
1988
  return {
1962
1989
  action: "TOP_UP_STARTED",