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.
- package/README.md +187 -0
- package/dist/cli/index.js +486 -152
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +483 -149
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +5 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +245 -116
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +241 -114
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +418 -0
- package/dist/client/web/index.mjs +1289 -0
- package/dist/client/web/index.mjs.map +1 -0
- package/dist/facilitators/index.d.mts +24 -2
- package/dist/facilitators/index.d.ts +24 -2
- package/dist/facilitators/index.js +127 -13
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +127 -13
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.js +463 -149
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +460 -146
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.d.mts +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1623 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/index.mjs +1617 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/server/index.d.mts +43 -1
- package/dist/server/index.d.ts +43 -1
- package/dist/server/index.js +205 -18
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +205 -18
- package/dist/server/index.mjs.map +1 -1
- package/package.json +19 -4
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
844
|
-
const wallet = new
|
|
845
|
-
const tokenContract = new
|
|
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: "
|
|
1394
|
-
USDT: { name: "
|
|
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
|
-
*
|
|
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
|
-
|
|
1566
|
-
|
|
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(
|
|
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
|
|
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;
|