moltspay 1.4.1 → 1.6.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.
@@ -263,6 +263,9 @@ var CDPFacilitator = class extends BaseFacilitator {
263
263
  }
264
264
  };
265
265
 
266
+ // src/facilitators/tempo.ts
267
+ var import_ethers = require("ethers");
268
+
266
269
  // src/chains/index.ts
267
270
  var CHAINS = {
268
271
  // ============ Mainnet ============
@@ -432,15 +435,38 @@ var CHAINS = {
432
435
 
433
436
  // src/facilitators/tempo.ts
434
437
  var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
438
+ var TIP20_PERMIT_ABI = [
439
+ "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
440
+ "function transferFrom(address from, address to, uint256 value) returns (bool)"
441
+ ];
435
442
  var TempoFacilitator = class extends BaseFacilitator {
436
443
  name = "tempo";
437
444
  displayName = "Tempo Testnet";
438
445
  supportedNetworks = ["eip155:42431"];
439
446
  // Tempo Moderato
440
447
  rpcUrl;
448
+ settlerWallet = null;
441
449
  constructor() {
442
450
  super();
443
451
  this.rpcUrl = CHAINS.tempo_moderato.rpc;
452
+ const settlerKey = process.env.TEMPO_SETTLER_KEY;
453
+ if (settlerKey) {
454
+ try {
455
+ const provider = new import_ethers.ethers.JsonRpcProvider(this.rpcUrl);
456
+ this.settlerWallet = new import_ethers.ethers.Wallet(settlerKey, provider);
457
+ } catch (err) {
458
+ console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
459
+ this.settlerWallet = null;
460
+ }
461
+ }
462
+ }
463
+ /**
464
+ * Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
465
+ * Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
466
+ * Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
467
+ */
468
+ getSpenderAddress() {
469
+ return this.settlerWallet?.address ?? null;
444
470
  }
445
471
  async healthCheck() {
446
472
  const start = Date.now();
@@ -466,6 +492,44 @@ var TempoFacilitator = class extends BaseFacilitator {
466
492
  }
467
493
  }
468
494
  async verify(paymentPayload, requirements) {
495
+ const inner = paymentPayload.payload;
496
+ if (inner && "permit" in inner && inner.permit) {
497
+ return this.verifyPermit(inner, requirements);
498
+ }
499
+ return this.verifyTxHash(paymentPayload, requirements);
500
+ }
501
+ /**
502
+ * Structural validation of an EIP-2612 permit payload. Does NOT submit
503
+ * anything on-chain — actual submission happens in settlePermit().
504
+ */
505
+ async verifyPermit(payload, requirements) {
506
+ if (!this.settlerWallet) {
507
+ return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
508
+ }
509
+ const p = payload.permit;
510
+ if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
511
+ return { valid: false, error: "Invalid permit payload: missing fields" };
512
+ }
513
+ if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
514
+ return {
515
+ valid: false,
516
+ error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
517
+ };
518
+ }
519
+ const deadline = BigInt(p.deadline);
520
+ const now = BigInt(Math.floor(Date.now() / 1e3));
521
+ if (deadline <= now) {
522
+ return { valid: false, error: "Permit deadline has expired" };
523
+ }
524
+ if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
525
+ return {
526
+ valid: false,
527
+ error: `Permit value ${p.value} is less than required ${requirements.amount}`
528
+ };
529
+ }
530
+ return { valid: true, details: { scheme: "permit", owner: p.owner } };
531
+ }
532
+ async verifyTxHash(paymentPayload, requirements) {
469
533
  try {
470
534
  const tempoPayload = paymentPayload.payload;
471
535
  if (!tempoPayload?.txHash) {
@@ -523,7 +587,11 @@ var TempoFacilitator = class extends BaseFacilitator {
523
587
  }
524
588
  }
525
589
  async settle(paymentPayload, requirements) {
526
- const verifyResult = await this.verify(paymentPayload, requirements);
590
+ const inner = paymentPayload.payload;
591
+ if (inner && "permit" in inner && inner.permit) {
592
+ return this.settlePermit(inner, requirements);
593
+ }
594
+ const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
527
595
  if (!verifyResult.valid) {
528
596
  return { success: false, error: verifyResult.error };
529
597
  }
@@ -534,6 +602,52 @@ var TempoFacilitator = class extends BaseFacilitator {
534
602
  status: "settled"
535
603
  };
536
604
  }
605
+ /**
606
+ * EIP-2612 permit settlement path. Submits two transactions on Tempo:
607
+ * 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
608
+ * 2. pathUSD.transferFrom(owner, payTo, value)
609
+ *
610
+ * The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
611
+ * native tTEMPO required; any held TIP-20 token balance covers fees).
612
+ */
613
+ async settlePermit(payload, requirements) {
614
+ if (!this.settlerWallet) {
615
+ return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
616
+ }
617
+ if (!requirements.asset || !requirements.payTo) {
618
+ return { success: false, error: "Missing asset or payTo in requirements" };
619
+ }
620
+ const verifyResult = await this.verifyPermit(payload, requirements);
621
+ if (!verifyResult.valid) {
622
+ return { success: false, error: verifyResult.error };
623
+ }
624
+ const token = new import_ethers.ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
625
+ const p = payload.permit;
626
+ try {
627
+ const permitTx = await token.permit(
628
+ p.owner,
629
+ p.spender,
630
+ p.value,
631
+ p.deadline,
632
+ p.v,
633
+ p.r,
634
+ p.s
635
+ );
636
+ await permitTx.wait();
637
+ const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
638
+ await transferTx.wait();
639
+ return {
640
+ success: true,
641
+ transaction: transferTx.hash,
642
+ status: "settled"
643
+ };
644
+ } catch (err) {
645
+ return {
646
+ success: false,
647
+ error: `Tempo permit settlement failed: ${err.message}`
648
+ };
649
+ }
650
+ }
537
651
  async getTransactionReceipt(txHash) {
538
652
  const response = await fetch(this.rpcUrl, {
539
653
  method: "POST",
@@ -776,12 +890,12 @@ var BNBFacilitator = class extends BaseFacilitator {
776
890
  return this.spenderAddress;
777
891
  }
778
892
  async getServerAddress() {
779
- const { ethers } = await import("ethers");
780
- const wallet = new ethers.Wallet(this.serverPrivateKey);
893
+ const { ethers: ethers2 } = await import("ethers");
894
+ const wallet = new ethers2.Wallet(this.serverPrivateKey);
781
895
  return wallet.address;
782
896
  }
783
897
  async recoverIntentSigner(intent, chainId) {
784
- const { ethers } = await import("ethers");
898
+ const { ethers: ethers2 } = await import("ethers");
785
899
  const domain = {
786
900
  ...EIP712_DOMAIN,
787
901
  chainId
@@ -795,7 +909,7 @@ var BNBFacilitator = class extends BaseFacilitator {
795
909
  nonce: intent.nonce,
796
910
  deadline: intent.deadline
797
911
  };
798
- const recoveredAddress = ethers.verifyTypedData(
912
+ const recoveredAddress = ethers2.verifyTypedData(
799
913
  domain,
800
914
  INTENT_TYPES,
801
915
  message,
@@ -839,10 +953,10 @@ var BNBFacilitator = class extends BaseFacilitator {
839
953
  return result.result || "0x0";
840
954
  }
841
955
  async executeTransferFrom(from, to, amount, token, rpcUrl) {
842
- const { ethers } = await import("ethers");
843
- const provider = new ethers.JsonRpcProvider(rpcUrl);
844
- const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
845
- const tokenContract = new ethers.Contract(token, [
956
+ const { ethers: ethers2 } = await import("ethers");
957
+ const provider = new ethers2.JsonRpcProvider(rpcUrl);
958
+ const wallet = new ethers2.Wallet(this.serverPrivateKey, provider);
959
+ const tokenContract = new ethers2.Contract(token, [
846
960
  "function transferFrom(address from, address to, uint256 amount) returns (bool)"
847
961
  ], wallet);
848
962
  const tx = await tokenContract.transferFrom(from, to, amount);
@@ -1389,9 +1503,13 @@ var TOKEN_DOMAINS = {
1389
1503
  USDT: { name: "(PoS) Tether USD", version: "2" }
1390
1504
  },
1391
1505
  // Tempo Moderato testnet - TIP-20 stablecoins
1506
+ // Domain names verified against on-chain DOMAIN_SEPARATOR values on 2026-04-21.
1507
+ // See docs/TEMPO-WEB-SUPPORT.md Section 2 and test/server/tempo-domain.test.ts.
1508
+ // All 4 Tempo TIP-20 tokens (pathUSD / AlphaUSD / BetaUSD / ThetaUSD) use
1509
+ // the token symbol with first letter capitalized + version "1".
1392
1510
  "eip155:42431": {
1393
- USDC: { name: "pathUSD", version: "1" },
1394
- USDT: { name: "alphaUSD", version: "1" }
1511
+ USDC: { name: "PathUSD", version: "1" },
1512
+ USDT: { name: "AlphaUSD", version: "1" }
1395
1513
  },
1396
1514
  // BNB Smart Chain mainnet
1397
1515
  "eip155:56": {
@@ -1560,13 +1678,62 @@ var MoltsPayServer = class {
1560
1678
  });
1561
1679
  }
1562
1680
  /**
1563
- * Handle incoming request
1681
+ * Apply CORS response headers according to the `cors` option.
1682
+ *
1683
+ * Default (`cors` unset or `true`): `Access-Control-Allow-Origin: *`. Matches 1.5.x behavior
1684
+ * and works for every browser client whose origin does not need to send cookies.
1685
+ *
1686
+ * `cors: false`: emit no CORS headers. Same-origin only.
1687
+ * `cors: string[]`: origin allowlist — echo the origin back iff it matches.
1688
+ * `cors: CorsOptions`: full control (allowlist + credentials + maxAge).
1689
+ *
1690
+ * The required-for-Web response headers are always exposed when CORS is active:
1691
+ * `X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt`.
1564
1692
  */
1565
- async handleRequest(req, res) {
1566
- res.setHeader("Access-Control-Allow-Origin", "*");
1693
+ applyCorsHeaders(req, res) {
1694
+ const cors = this.options.cors;
1695
+ if (cors === false) {
1696
+ return;
1697
+ }
1698
+ const requestOrigin = req.headers.origin ?? "*";
1699
+ if (cors === void 0 || cors === true) {
1700
+ this.writeCorsHeaders(res, "*");
1701
+ return;
1702
+ }
1703
+ if (Array.isArray(cors)) {
1704
+ if (cors.includes(requestOrigin)) {
1705
+ this.writeCorsHeaders(res, requestOrigin);
1706
+ res.setHeader("Vary", "Origin");
1707
+ }
1708
+ return;
1709
+ }
1710
+ const opt = cors;
1711
+ const isAllowed = typeof opt.origins === "function" ? opt.origins(requestOrigin) : opt.origins.includes(requestOrigin);
1712
+ if (!isAllowed) {
1713
+ return;
1714
+ }
1715
+ this.writeCorsHeaders(res, requestOrigin);
1716
+ res.setHeader("Vary", "Origin");
1717
+ if (opt.credentials) {
1718
+ res.setHeader("Access-Control-Allow-Credentials", "true");
1719
+ }
1720
+ const maxAge = opt.maxAge ?? 600;
1721
+ res.setHeader("Access-Control-Max-Age", String(maxAge));
1722
+ }
1723
+ writeCorsHeaders(res, origin) {
1724
+ res.setHeader("Access-Control-Allow-Origin", origin);
1567
1725
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1568
1726
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
1569
- res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
1727
+ res.setHeader(
1728
+ "Access-Control-Expose-Headers",
1729
+ "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt"
1730
+ );
1731
+ }
1732
+ /**
1733
+ * Handle incoming request
1734
+ */
1735
+ async handleRequest(req, res) {
1736
+ this.applyCorsHeaders(req, res);
1570
1737
  if (req.method === "OPTIONS") {
1571
1738
  res.writeHead(204);
1572
1739
  res.end();
@@ -1789,6 +1956,14 @@ var MoltsPayServer = class {
1789
1956
  console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1790
1957
  } catch (err) {
1791
1958
  console.error("[MoltsPay] Settlement failed:", err.message);
1959
+ settlement = { success: false, error: err.message, facilitator: "none" };
1960
+ }
1961
+ if (!settlement?.success) {
1962
+ return this.sendJson(res, 402, {
1963
+ error: "Payment settlement failed",
1964
+ message: settlement?.error || "Settlement returned no success state",
1965
+ facilitator: settlement?.facilitator
1966
+ });
1792
1967
  }
1793
1968
  }
1794
1969
  const responseHeaders = {};
@@ -2043,7 +2218,7 @@ var MoltsPayServer = class {
2043
2218
  }
2044
2219
  const scheme = payment.accepted?.scheme || payment.scheme;
2045
2220
  const network = payment.accepted?.network || payment.network || this.networkId;
2046
- if (scheme !== "exact") {
2221
+ if (scheme !== "exact" && scheme !== "permit") {
2047
2222
  return { valid: false, error: `Unsupported scheme: ${scheme}` };
2048
2223
  }
2049
2224
  if (!this.isNetworkAccepted(network)) {
@@ -2065,8 +2240,10 @@ var MoltsPayServer = class {
2065
2240
  const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
2066
2241
  const tokenAddress = tokenAddresses[selectedToken];
2067
2242
  const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
2243
+ const isTempo = selectedNetwork === "eip155:42431";
2244
+ const scheme = isTempo ? "permit" : "exact";
2068
2245
  const requirements = {
2069
- scheme: "exact",
2246
+ scheme,
2070
2247
  network: selectedNetwork,
2071
2248
  asset: tokenAddress,
2072
2249
  amount: amountInUnits,
@@ -2094,6 +2271,16 @@ var MoltsPayServer = class {
2094
2271
  };
2095
2272
  }
2096
2273
  }
2274
+ if (isTempo) {
2275
+ const tempoFacilitator = this.registry.get("tempo");
2276
+ const tempoSpender = tempoFacilitator?.getSpenderAddress?.();
2277
+ if (tempoSpender) {
2278
+ requirements.extra = {
2279
+ ...requirements.extra || {},
2280
+ tempoSpender
2281
+ };
2282
+ }
2283
+ }
2097
2284
  return requirements;
2098
2285
  }
2099
2286
  /**
@@ -2228,7 +2415,7 @@ var MoltsPayServer = class {
2228
2415
  }
2229
2416
  const scheme = payment.accepted?.scheme || payment.scheme;
2230
2417
  const network = payment.accepted?.network || payment.network;
2231
- if (scheme !== "exact") {
2418
+ if (scheme !== "exact" && scheme !== "permit") {
2232
2419
  return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
2233
2420
  }
2234
2421
  const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;