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.
@@ -229,6 +229,9 @@ var CDPFacilitator = class extends BaseFacilitator {
229
229
  }
230
230
  };
231
231
 
232
+ // src/facilitators/tempo.ts
233
+ import { ethers } from "ethers";
234
+
232
235
  // src/chains/index.ts
233
236
  var CHAINS = {
234
237
  // ============ Mainnet ============
@@ -398,15 +401,38 @@ var CHAINS = {
398
401
 
399
402
  // src/facilitators/tempo.ts
400
403
  var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
404
+ var TIP20_PERMIT_ABI = [
405
+ "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
406
+ "function transferFrom(address from, address to, uint256 value) returns (bool)"
407
+ ];
401
408
  var TempoFacilitator = class extends BaseFacilitator {
402
409
  name = "tempo";
403
410
  displayName = "Tempo Testnet";
404
411
  supportedNetworks = ["eip155:42431"];
405
412
  // Tempo Moderato
406
413
  rpcUrl;
414
+ settlerWallet = null;
407
415
  constructor() {
408
416
  super();
409
417
  this.rpcUrl = CHAINS.tempo_moderato.rpc;
418
+ const settlerKey = process.env.TEMPO_SETTLER_KEY;
419
+ if (settlerKey) {
420
+ try {
421
+ const provider = new ethers.JsonRpcProvider(this.rpcUrl);
422
+ this.settlerWallet = new ethers.Wallet(settlerKey, provider);
423
+ } catch (err) {
424
+ console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
425
+ this.settlerWallet = null;
426
+ }
427
+ }
428
+ }
429
+ /**
430
+ * Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
431
+ * Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
432
+ * Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
433
+ */
434
+ getSpenderAddress() {
435
+ return this.settlerWallet?.address ?? null;
410
436
  }
411
437
  async healthCheck() {
412
438
  const start = Date.now();
@@ -432,6 +458,44 @@ var TempoFacilitator = class extends BaseFacilitator {
432
458
  }
433
459
  }
434
460
  async verify(paymentPayload, requirements) {
461
+ const inner = paymentPayload.payload;
462
+ if (inner && "permit" in inner && inner.permit) {
463
+ return this.verifyPermit(inner, requirements);
464
+ }
465
+ return this.verifyTxHash(paymentPayload, requirements);
466
+ }
467
+ /**
468
+ * Structural validation of an EIP-2612 permit payload. Does NOT submit
469
+ * anything on-chain — actual submission happens in settlePermit().
470
+ */
471
+ async verifyPermit(payload, requirements) {
472
+ if (!this.settlerWallet) {
473
+ return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
474
+ }
475
+ const p = payload.permit;
476
+ if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
477
+ return { valid: false, error: "Invalid permit payload: missing fields" };
478
+ }
479
+ if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
480
+ return {
481
+ valid: false,
482
+ error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
483
+ };
484
+ }
485
+ const deadline = BigInt(p.deadline);
486
+ const now = BigInt(Math.floor(Date.now() / 1e3));
487
+ if (deadline <= now) {
488
+ return { valid: false, error: "Permit deadline has expired" };
489
+ }
490
+ if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
491
+ return {
492
+ valid: false,
493
+ error: `Permit value ${p.value} is less than required ${requirements.amount}`
494
+ };
495
+ }
496
+ return { valid: true, details: { scheme: "permit", owner: p.owner } };
497
+ }
498
+ async verifyTxHash(paymentPayload, requirements) {
435
499
  try {
436
500
  const tempoPayload = paymentPayload.payload;
437
501
  if (!tempoPayload?.txHash) {
@@ -489,7 +553,11 @@ var TempoFacilitator = class extends BaseFacilitator {
489
553
  }
490
554
  }
491
555
  async settle(paymentPayload, requirements) {
492
- const verifyResult = await this.verify(paymentPayload, requirements);
556
+ const inner = paymentPayload.payload;
557
+ if (inner && "permit" in inner && inner.permit) {
558
+ return this.settlePermit(inner, requirements);
559
+ }
560
+ const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
493
561
  if (!verifyResult.valid) {
494
562
  return { success: false, error: verifyResult.error };
495
563
  }
@@ -500,6 +568,52 @@ var TempoFacilitator = class extends BaseFacilitator {
500
568
  status: "settled"
501
569
  };
502
570
  }
571
+ /**
572
+ * EIP-2612 permit settlement path. Submits two transactions on Tempo:
573
+ * 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
574
+ * 2. pathUSD.transferFrom(owner, payTo, value)
575
+ *
576
+ * The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
577
+ * native tTEMPO required; any held TIP-20 token balance covers fees).
578
+ */
579
+ async settlePermit(payload, requirements) {
580
+ if (!this.settlerWallet) {
581
+ return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
582
+ }
583
+ if (!requirements.asset || !requirements.payTo) {
584
+ return { success: false, error: "Missing asset or payTo in requirements" };
585
+ }
586
+ const verifyResult = await this.verifyPermit(payload, requirements);
587
+ if (!verifyResult.valid) {
588
+ return { success: false, error: verifyResult.error };
589
+ }
590
+ const token = new ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
591
+ const p = payload.permit;
592
+ try {
593
+ const permitTx = await token.permit(
594
+ p.owner,
595
+ p.spender,
596
+ p.value,
597
+ p.deadline,
598
+ p.v,
599
+ p.r,
600
+ p.s
601
+ );
602
+ await permitTx.wait();
603
+ const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
604
+ await transferTx.wait();
605
+ return {
606
+ success: true,
607
+ transaction: transferTx.hash,
608
+ status: "settled"
609
+ };
610
+ } catch (err) {
611
+ return {
612
+ success: false,
613
+ error: `Tempo permit settlement failed: ${err.message}`
614
+ };
615
+ }
616
+ }
503
617
  async getTransactionReceipt(txHash) {
504
618
  const response = await fetch(this.rpcUrl, {
505
619
  method: "POST",
@@ -742,12 +856,12 @@ var BNBFacilitator = class extends BaseFacilitator {
742
856
  return this.spenderAddress;
743
857
  }
744
858
  async getServerAddress() {
745
- const { ethers } = await import("ethers");
746
- const wallet = new ethers.Wallet(this.serverPrivateKey);
859
+ const { ethers: ethers2 } = await import("ethers");
860
+ const wallet = new ethers2.Wallet(this.serverPrivateKey);
747
861
  return wallet.address;
748
862
  }
749
863
  async recoverIntentSigner(intent, chainId) {
750
- const { ethers } = await import("ethers");
864
+ const { ethers: ethers2 } = await import("ethers");
751
865
  const domain = {
752
866
  ...EIP712_DOMAIN,
753
867
  chainId
@@ -761,7 +875,7 @@ var BNBFacilitator = class extends BaseFacilitator {
761
875
  nonce: intent.nonce,
762
876
  deadline: intent.deadline
763
877
  };
764
- const recoveredAddress = ethers.verifyTypedData(
878
+ const recoveredAddress = ethers2.verifyTypedData(
765
879
  domain,
766
880
  INTENT_TYPES,
767
881
  message,
@@ -805,10 +919,10 @@ var BNBFacilitator = class extends BaseFacilitator {
805
919
  return result.result || "0x0";
806
920
  }
807
921
  async executeTransferFrom(from, to, amount, token, rpcUrl) {
808
- const { ethers } = await import("ethers");
809
- const provider = new ethers.JsonRpcProvider(rpcUrl);
810
- const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
811
- const tokenContract = new ethers.Contract(token, [
922
+ const { ethers: ethers2 } = await import("ethers");
923
+ const provider = new ethers2.JsonRpcProvider(rpcUrl);
924
+ const wallet = new ethers2.Wallet(this.serverPrivateKey, provider);
925
+ const tokenContract = new ethers2.Contract(token, [
812
926
  "function transferFrom(address from, address to, uint256 amount) returns (bool)"
813
927
  ], wallet);
814
928
  const tx = await tokenContract.transferFrom(from, to, amount);
@@ -1365,9 +1479,13 @@ var TOKEN_DOMAINS = {
1365
1479
  USDT: { name: "(PoS) Tether USD", version: "2" }
1366
1480
  },
1367
1481
  // Tempo Moderato testnet - TIP-20 stablecoins
1482
+ // Domain names verified against on-chain DOMAIN_SEPARATOR values on 2026-04-21.
1483
+ // See docs/TEMPO-WEB-SUPPORT.md Section 2 and test/server/tempo-domain.test.ts.
1484
+ // All 4 Tempo TIP-20 tokens (pathUSD / AlphaUSD / BetaUSD / ThetaUSD) use
1485
+ // the token symbol with first letter capitalized + version "1".
1368
1486
  "eip155:42431": {
1369
- USDC: { name: "pathUSD", version: "1" },
1370
- USDT: { name: "alphaUSD", version: "1" }
1487
+ USDC: { name: "PathUSD", version: "1" },
1488
+ USDT: { name: "AlphaUSD", version: "1" }
1371
1489
  },
1372
1490
  // BNB Smart Chain mainnet
1373
1491
  "eip155:56": {
@@ -1536,13 +1654,62 @@ var MoltsPayServer = class {
1536
1654
  });
1537
1655
  }
1538
1656
  /**
1539
- * Handle incoming request
1657
+ * Apply CORS response headers according to the `cors` option.
1658
+ *
1659
+ * Default (`cors` unset or `true`): `Access-Control-Allow-Origin: *`. Matches 1.5.x behavior
1660
+ * and works for every browser client whose origin does not need to send cookies.
1661
+ *
1662
+ * `cors: false`: emit no CORS headers. Same-origin only.
1663
+ * `cors: string[]`: origin allowlist — echo the origin back iff it matches.
1664
+ * `cors: CorsOptions`: full control (allowlist + credentials + maxAge).
1665
+ *
1666
+ * The required-for-Web response headers are always exposed when CORS is active:
1667
+ * `X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt`.
1540
1668
  */
1541
- async handleRequest(req, res) {
1542
- res.setHeader("Access-Control-Allow-Origin", "*");
1669
+ applyCorsHeaders(req, res) {
1670
+ const cors = this.options.cors;
1671
+ if (cors === false) {
1672
+ return;
1673
+ }
1674
+ const requestOrigin = req.headers.origin ?? "*";
1675
+ if (cors === void 0 || cors === true) {
1676
+ this.writeCorsHeaders(res, "*");
1677
+ return;
1678
+ }
1679
+ if (Array.isArray(cors)) {
1680
+ if (cors.includes(requestOrigin)) {
1681
+ this.writeCorsHeaders(res, requestOrigin);
1682
+ res.setHeader("Vary", "Origin");
1683
+ }
1684
+ return;
1685
+ }
1686
+ const opt = cors;
1687
+ const isAllowed = typeof opt.origins === "function" ? opt.origins(requestOrigin) : opt.origins.includes(requestOrigin);
1688
+ if (!isAllowed) {
1689
+ return;
1690
+ }
1691
+ this.writeCorsHeaders(res, requestOrigin);
1692
+ res.setHeader("Vary", "Origin");
1693
+ if (opt.credentials) {
1694
+ res.setHeader("Access-Control-Allow-Credentials", "true");
1695
+ }
1696
+ const maxAge = opt.maxAge ?? 600;
1697
+ res.setHeader("Access-Control-Max-Age", String(maxAge));
1698
+ }
1699
+ writeCorsHeaders(res, origin) {
1700
+ res.setHeader("Access-Control-Allow-Origin", origin);
1543
1701
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1544
1702
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
1545
- res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
1703
+ res.setHeader(
1704
+ "Access-Control-Expose-Headers",
1705
+ "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt"
1706
+ );
1707
+ }
1708
+ /**
1709
+ * Handle incoming request
1710
+ */
1711
+ async handleRequest(req, res) {
1712
+ this.applyCorsHeaders(req, res);
1546
1713
  if (req.method === "OPTIONS") {
1547
1714
  res.writeHead(204);
1548
1715
  res.end();
@@ -1765,6 +1932,14 @@ var MoltsPayServer = class {
1765
1932
  console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1766
1933
  } catch (err) {
1767
1934
  console.error("[MoltsPay] Settlement failed:", err.message);
1935
+ settlement = { success: false, error: err.message, facilitator: "none" };
1936
+ }
1937
+ if (!settlement?.success) {
1938
+ return this.sendJson(res, 402, {
1939
+ error: "Payment settlement failed",
1940
+ message: settlement?.error || "Settlement returned no success state",
1941
+ facilitator: settlement?.facilitator
1942
+ });
1768
1943
  }
1769
1944
  }
1770
1945
  const responseHeaders = {};
@@ -2019,7 +2194,7 @@ var MoltsPayServer = class {
2019
2194
  }
2020
2195
  const scheme = payment.accepted?.scheme || payment.scheme;
2021
2196
  const network = payment.accepted?.network || payment.network || this.networkId;
2022
- if (scheme !== "exact") {
2197
+ if (scheme !== "exact" && scheme !== "permit") {
2023
2198
  return { valid: false, error: `Unsupported scheme: ${scheme}` };
2024
2199
  }
2025
2200
  if (!this.isNetworkAccepted(network)) {
@@ -2041,8 +2216,10 @@ var MoltsPayServer = class {
2041
2216
  const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
2042
2217
  const tokenAddress = tokenAddresses[selectedToken];
2043
2218
  const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
2219
+ const isTempo = selectedNetwork === "eip155:42431";
2220
+ const scheme = isTempo ? "permit" : "exact";
2044
2221
  const requirements = {
2045
- scheme: "exact",
2222
+ scheme,
2046
2223
  network: selectedNetwork,
2047
2224
  asset: tokenAddress,
2048
2225
  amount: amountInUnits,
@@ -2070,6 +2247,16 @@ var MoltsPayServer = class {
2070
2247
  };
2071
2248
  }
2072
2249
  }
2250
+ if (isTempo) {
2251
+ const tempoFacilitator = this.registry.get("tempo");
2252
+ const tempoSpender = tempoFacilitator?.getSpenderAddress?.();
2253
+ if (tempoSpender) {
2254
+ requirements.extra = {
2255
+ ...requirements.extra || {},
2256
+ tempoSpender
2257
+ };
2258
+ }
2259
+ }
2073
2260
  return requirements;
2074
2261
  }
2075
2262
  /**
@@ -2204,7 +2391,7 @@ var MoltsPayServer = class {
2204
2391
  }
2205
2392
  const scheme = payment.accepted?.scheme || payment.scheme;
2206
2393
  const network = payment.accepted?.network || payment.network;
2207
- if (scheme !== "exact") {
2394
+ if (scheme !== "exact" && scheme !== "permit") {
2208
2395
  return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
2209
2396
  }
2210
2397
  const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;