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.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
810
|
-
const wallet = new
|
|
811
|
-
const tokenContract = new
|
|
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: "
|
|
1370
|
-
USDT: { name: "
|
|
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
|
-
*
|
|
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
|
-
|
|
1542
|
-
|
|
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(
|
|
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
|
|
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;
|