moltspay 1.3.0 → 1.4.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 +221 -38
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +57 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +57 -0
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +57 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1975 -273
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1977 -265
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +36 -3
- package/dist/client/index.d.ts +36 -3
- package/dist/client/index.js +540 -32
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +548 -30
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -1
- package/dist/facilitators/index.d.ts +220 -1
- package/dist/facilitators/index.js +664 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +670 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1413 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1421 -144
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +905 -52
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +915 -52
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +57 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +57 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +57 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +57 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +4 -1
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/index.mjs
CHANGED
|
@@ -343,6 +343,63 @@ var CHAINS = {
|
|
|
343
343
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
344
344
|
avgBlockTime: 0.5
|
|
345
345
|
// ~500ms finality
|
|
346
|
+
},
|
|
347
|
+
// ============ BNB Chain Testnet ============
|
|
348
|
+
bnb_testnet: {
|
|
349
|
+
name: "BNB Testnet",
|
|
350
|
+
chainId: 97,
|
|
351
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
352
|
+
tokens: {
|
|
353
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
354
|
+
// Using official Binance-Peg testnet tokens
|
|
355
|
+
USDC: {
|
|
356
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
357
|
+
// Testnet USDC
|
|
358
|
+
decimals: 18,
|
|
359
|
+
symbol: "USDC",
|
|
360
|
+
eip712Name: "USD Coin"
|
|
361
|
+
},
|
|
362
|
+
USDT: {
|
|
363
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
364
|
+
// Testnet USDT
|
|
365
|
+
decimals: 18,
|
|
366
|
+
symbol: "USDT",
|
|
367
|
+
eip712Name: "Tether USD"
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
371
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
372
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
373
|
+
avgBlockTime: 3,
|
|
374
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
375
|
+
requiresApproval: true
|
|
376
|
+
},
|
|
377
|
+
// ============ BNB Chain Mainnet ============
|
|
378
|
+
bnb: {
|
|
379
|
+
name: "BNB Smart Chain",
|
|
380
|
+
chainId: 56,
|
|
381
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
382
|
+
tokens: {
|
|
383
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
384
|
+
USDC: {
|
|
385
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
386
|
+
decimals: 18,
|
|
387
|
+
symbol: "USDC",
|
|
388
|
+
eip712Name: "USD Coin"
|
|
389
|
+
},
|
|
390
|
+
USDT: {
|
|
391
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
392
|
+
decimals: 18,
|
|
393
|
+
symbol: "USDT",
|
|
394
|
+
eip712Name: "Tether USD"
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
398
|
+
explorer: "https://bscscan.com/address/",
|
|
399
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
400
|
+
avgBlockTime: 3,
|
|
401
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
402
|
+
requiresApproval: true
|
|
346
403
|
}
|
|
347
404
|
};
|
|
348
405
|
function getChain(name) {
|
|
@@ -492,7 +549,583 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
492
549
|
}
|
|
493
550
|
};
|
|
494
551
|
|
|
552
|
+
// src/facilitators/bnb.ts
|
|
553
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
554
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
555
|
+
var EIP712_DOMAIN = {
|
|
556
|
+
name: "MoltsPay",
|
|
557
|
+
version: "1"
|
|
558
|
+
};
|
|
559
|
+
var INTENT_TYPES = {
|
|
560
|
+
PaymentIntent: [
|
|
561
|
+
{ name: "from", type: "address" },
|
|
562
|
+
{ name: "to", type: "address" },
|
|
563
|
+
{ name: "amount", type: "uint256" },
|
|
564
|
+
{ name: "token", type: "address" },
|
|
565
|
+
{ name: "service", type: "string" },
|
|
566
|
+
{ name: "nonce", type: "uint256" },
|
|
567
|
+
{ name: "deadline", type: "uint256" }
|
|
568
|
+
]
|
|
569
|
+
};
|
|
570
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
571
|
+
name = "bnb";
|
|
572
|
+
displayName = "BNB Smart Chain";
|
|
573
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
574
|
+
// Mainnet + Testnet
|
|
575
|
+
serverPrivateKey;
|
|
576
|
+
spenderAddress = null;
|
|
577
|
+
chainConfigs;
|
|
578
|
+
constructor(serverPrivateKey) {
|
|
579
|
+
super();
|
|
580
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
581
|
+
if (this.serverPrivateKey) {
|
|
582
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
583
|
+
const account = privateKeyToAccount(key);
|
|
584
|
+
this.spenderAddress = account.address;
|
|
585
|
+
}
|
|
586
|
+
this.chainConfigs = {
|
|
587
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
588
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async healthCheck() {
|
|
592
|
+
const start = Date.now();
|
|
593
|
+
try {
|
|
594
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
595
|
+
method: "POST",
|
|
596
|
+
headers: { "Content-Type": "application/json" },
|
|
597
|
+
body: JSON.stringify({
|
|
598
|
+
jsonrpc: "2.0",
|
|
599
|
+
method: "eth_chainId",
|
|
600
|
+
params: [],
|
|
601
|
+
id: 1
|
|
602
|
+
})
|
|
603
|
+
});
|
|
604
|
+
const data = await response.json();
|
|
605
|
+
const chainId = parseInt(data.result, 16);
|
|
606
|
+
if (chainId !== 56) {
|
|
607
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
608
|
+
}
|
|
609
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
610
|
+
} catch (error) {
|
|
611
|
+
return { healthy: false, error: String(error) };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Verify a payment intent signature (before service execution)
|
|
616
|
+
*
|
|
617
|
+
* This verifies:
|
|
618
|
+
* 1. Signature is valid for the intent
|
|
619
|
+
* 2. Client has approved server wallet
|
|
620
|
+
* 3. Client has sufficient balance
|
|
621
|
+
* 4. Intent hasn't expired
|
|
622
|
+
*/
|
|
623
|
+
async verify(paymentPayload, requirements) {
|
|
624
|
+
try {
|
|
625
|
+
const bnbPayload = paymentPayload.payload;
|
|
626
|
+
if (!bnbPayload?.intent) {
|
|
627
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
628
|
+
}
|
|
629
|
+
const { intent, chainId } = bnbPayload;
|
|
630
|
+
const config = this.chainConfigs[chainId];
|
|
631
|
+
if (!config) {
|
|
632
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
633
|
+
}
|
|
634
|
+
if (intent.deadline < Date.now()) {
|
|
635
|
+
return { valid: false, error: "Intent expired" };
|
|
636
|
+
}
|
|
637
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
638
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
639
|
+
return { valid: false, error: "Invalid signature" };
|
|
640
|
+
}
|
|
641
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
642
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
643
|
+
}
|
|
644
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
645
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
646
|
+
}
|
|
647
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
648
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
649
|
+
}
|
|
650
|
+
const serverAddress = await this.getServerAddress();
|
|
651
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
652
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
653
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
654
|
+
}
|
|
655
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
656
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
657
|
+
return { valid: false, error: "Insufficient balance" };
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
valid: true,
|
|
661
|
+
details: {
|
|
662
|
+
from: intent.from,
|
|
663
|
+
to: intent.to,
|
|
664
|
+
amount: intent.amount,
|
|
665
|
+
token: intent.token,
|
|
666
|
+
service: intent.service,
|
|
667
|
+
nonce: intent.nonce,
|
|
668
|
+
deadline: intent.deadline
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
} catch (error) {
|
|
672
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Settle a payment by executing transferFrom
|
|
677
|
+
*
|
|
678
|
+
* This is called AFTER the service has been successfully delivered.
|
|
679
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
680
|
+
*/
|
|
681
|
+
async settle(paymentPayload, requirements) {
|
|
682
|
+
if (!this.serverPrivateKey) {
|
|
683
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
687
|
+
if (!verifyResult.valid) {
|
|
688
|
+
return { success: false, error: verifyResult.error };
|
|
689
|
+
}
|
|
690
|
+
const bnbPayload = paymentPayload.payload;
|
|
691
|
+
const { intent, chainId } = bnbPayload;
|
|
692
|
+
const config = this.chainConfigs[chainId];
|
|
693
|
+
const txHash = await this.executeTransferFrom(
|
|
694
|
+
intent.from,
|
|
695
|
+
intent.to,
|
|
696
|
+
intent.amount,
|
|
697
|
+
intent.token,
|
|
698
|
+
config.rpc
|
|
699
|
+
);
|
|
700
|
+
return {
|
|
701
|
+
success: true,
|
|
702
|
+
transaction: txHash,
|
|
703
|
+
status: "settled"
|
|
704
|
+
};
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Check if client has approved the server wallet
|
|
711
|
+
*/
|
|
712
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
713
|
+
const config = this.chainConfigs[chainId];
|
|
714
|
+
if (!config) {
|
|
715
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
716
|
+
}
|
|
717
|
+
const serverAddress = await this.getServerAddress();
|
|
718
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
719
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
720
|
+
return {
|
|
721
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
722
|
+
allowance
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Verify a completed transaction (for checking past payments)
|
|
727
|
+
*/
|
|
728
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
729
|
+
const config = this.chainConfigs[chainId];
|
|
730
|
+
if (!config) {
|
|
731
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
735
|
+
if (!receipt) {
|
|
736
|
+
return { valid: false, error: "Transaction not found" };
|
|
737
|
+
}
|
|
738
|
+
if (receipt.status !== "0x1") {
|
|
739
|
+
return { valid: false, error: "Transaction failed" };
|
|
740
|
+
}
|
|
741
|
+
const transferLog = receipt.logs.find(
|
|
742
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
743
|
+
);
|
|
744
|
+
if (!transferLog) {
|
|
745
|
+
return { valid: false, error: "No Transfer event found" };
|
|
746
|
+
}
|
|
747
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
748
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
749
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
750
|
+
}
|
|
751
|
+
const amount = BigInt(transferLog.data);
|
|
752
|
+
if (amount < BigInt(expected.amount)) {
|
|
753
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
valid: true,
|
|
757
|
+
details: {
|
|
758
|
+
txHash,
|
|
759
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
760
|
+
to: toAddress,
|
|
761
|
+
amount: amount.toString(),
|
|
762
|
+
token: transferLog.address
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// ==================== Private Methods ====================
|
|
770
|
+
/**
|
|
771
|
+
* Get the server's spender address (public, for 402 responses)
|
|
772
|
+
* Returns cached value computed at construction time.
|
|
773
|
+
*/
|
|
774
|
+
getSpenderAddress() {
|
|
775
|
+
return this.spenderAddress;
|
|
776
|
+
}
|
|
777
|
+
async getServerAddress() {
|
|
778
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
779
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey);
|
|
780
|
+
return wallet.address;
|
|
781
|
+
}
|
|
782
|
+
async recoverIntentSigner(intent, chainId) {
|
|
783
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
784
|
+
const domain = {
|
|
785
|
+
...EIP712_DOMAIN,
|
|
786
|
+
chainId
|
|
787
|
+
};
|
|
788
|
+
const message = {
|
|
789
|
+
from: intent.from,
|
|
790
|
+
to: intent.to,
|
|
791
|
+
amount: intent.amount,
|
|
792
|
+
token: intent.token,
|
|
793
|
+
service: intent.service,
|
|
794
|
+
nonce: intent.nonce,
|
|
795
|
+
deadline: intent.deadline
|
|
796
|
+
};
|
|
797
|
+
const recoveredAddress = ethers5.verifyTypedData(
|
|
798
|
+
domain,
|
|
799
|
+
INTENT_TYPES,
|
|
800
|
+
message,
|
|
801
|
+
intent.signature
|
|
802
|
+
);
|
|
803
|
+
return recoveredAddress;
|
|
804
|
+
}
|
|
805
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
806
|
+
const selector = "0xdd62ed3e";
|
|
807
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
808
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
809
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
810
|
+
const response = await fetch(rpcUrl, {
|
|
811
|
+
method: "POST",
|
|
812
|
+
headers: { "Content-Type": "application/json" },
|
|
813
|
+
body: JSON.stringify({
|
|
814
|
+
jsonrpc: "2.0",
|
|
815
|
+
method: "eth_call",
|
|
816
|
+
params: [{ to: token, data }, "latest"],
|
|
817
|
+
id: 1
|
|
818
|
+
})
|
|
819
|
+
});
|
|
820
|
+
const result = await response.json();
|
|
821
|
+
return result.result || "0x0";
|
|
822
|
+
}
|
|
823
|
+
async getBalance(account, token, rpcUrl) {
|
|
824
|
+
const selector = "0x70a08231";
|
|
825
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
826
|
+
const data = selector + accountPadded;
|
|
827
|
+
const response = await fetch(rpcUrl, {
|
|
828
|
+
method: "POST",
|
|
829
|
+
headers: { "Content-Type": "application/json" },
|
|
830
|
+
body: JSON.stringify({
|
|
831
|
+
jsonrpc: "2.0",
|
|
832
|
+
method: "eth_call",
|
|
833
|
+
params: [{ to: token, data }, "latest"],
|
|
834
|
+
id: 1
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
const result = await response.json();
|
|
838
|
+
return result.result || "0x0";
|
|
839
|
+
}
|
|
840
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
841
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
842
|
+
const provider = new ethers5.JsonRpcProvider(rpcUrl);
|
|
843
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey, provider);
|
|
844
|
+
const tokenContract = new ethers5.Contract(token, [
|
|
845
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
846
|
+
], wallet);
|
|
847
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
848
|
+
const receipt = await tx.wait();
|
|
849
|
+
return receipt.hash;
|
|
850
|
+
}
|
|
851
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
852
|
+
const response = await fetch(rpcUrl, {
|
|
853
|
+
method: "POST",
|
|
854
|
+
headers: { "Content-Type": "application/json" },
|
|
855
|
+
body: JSON.stringify({
|
|
856
|
+
jsonrpc: "2.0",
|
|
857
|
+
method: "eth_getTransactionReceipt",
|
|
858
|
+
params: [txHash],
|
|
859
|
+
id: 1
|
|
860
|
+
})
|
|
861
|
+
});
|
|
862
|
+
const data = await response.json();
|
|
863
|
+
return data.result;
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// src/facilitators/solana.ts
|
|
868
|
+
import {
|
|
869
|
+
Connection as Connection2,
|
|
870
|
+
PublicKey as PublicKey2,
|
|
871
|
+
Transaction,
|
|
872
|
+
VersionedTransaction
|
|
873
|
+
} from "@solana/web3.js";
|
|
874
|
+
import {
|
|
875
|
+
getAssociatedTokenAddress,
|
|
876
|
+
createTransferCheckedInstruction,
|
|
877
|
+
getAccount,
|
|
878
|
+
createAssociatedTokenAccountInstruction
|
|
879
|
+
} from "@solana/spl-token";
|
|
880
|
+
|
|
881
|
+
// src/chains/solana.ts
|
|
882
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
883
|
+
var SOLANA_CHAINS = {
|
|
884
|
+
solana: {
|
|
885
|
+
name: "Solana Mainnet",
|
|
886
|
+
cluster: "mainnet-beta",
|
|
887
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
888
|
+
explorer: "https://solscan.io/account/",
|
|
889
|
+
explorerTx: "https://solscan.io/tx/",
|
|
890
|
+
tokens: {
|
|
891
|
+
USDC: {
|
|
892
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
893
|
+
// Circle official USDC
|
|
894
|
+
decimals: 6
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
solana_devnet: {
|
|
899
|
+
name: "Solana Devnet",
|
|
900
|
+
cluster: "devnet",
|
|
901
|
+
rpc: "https://api.devnet.solana.com",
|
|
902
|
+
explorer: "https://solscan.io/account/",
|
|
903
|
+
explorerTx: "https://solscan.io/tx/",
|
|
904
|
+
tokens: {
|
|
905
|
+
USDC: {
|
|
906
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
907
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
908
|
+
decimals: 6
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// src/facilitators/solana.ts
|
|
915
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
916
|
+
name = "solana";
|
|
917
|
+
displayName = "Solana Direct";
|
|
918
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
919
|
+
connections = /* @__PURE__ */ new Map();
|
|
920
|
+
feePayerKeypair;
|
|
921
|
+
constructor(config) {
|
|
922
|
+
super();
|
|
923
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
924
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
925
|
+
this.connections.set(
|
|
926
|
+
chain,
|
|
927
|
+
new Connection2(config2.rpc, "confirmed")
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (this.feePayerKeypair) {
|
|
931
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Get fee payer public key (for gasless transactions)
|
|
936
|
+
*/
|
|
937
|
+
getFeePayerPubkey() {
|
|
938
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
939
|
+
}
|
|
940
|
+
getConnection(chain) {
|
|
941
|
+
const conn = this.connections.get(chain);
|
|
942
|
+
if (!conn) {
|
|
943
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
944
|
+
}
|
|
945
|
+
return conn;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Convert our chain name to network identifier
|
|
949
|
+
*/
|
|
950
|
+
static chainToNetwork(chain) {
|
|
951
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Convert network identifier to chain name
|
|
955
|
+
*/
|
|
956
|
+
static networkToChain(network) {
|
|
957
|
+
if (network === "solana:mainnet") return "solana";
|
|
958
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
async healthCheck() {
|
|
962
|
+
const start = Date.now();
|
|
963
|
+
try {
|
|
964
|
+
const conn = this.getConnection("solana_devnet");
|
|
965
|
+
await conn.getSlot();
|
|
966
|
+
return {
|
|
967
|
+
healthy: true,
|
|
968
|
+
latencyMs: Date.now() - start
|
|
969
|
+
};
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return {
|
|
972
|
+
healthy: false,
|
|
973
|
+
error: error.message
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Verify a Solana payment
|
|
979
|
+
*
|
|
980
|
+
* Checks:
|
|
981
|
+
* 1. Transaction is valid and properly signed
|
|
982
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
983
|
+
*/
|
|
984
|
+
async verify(paymentPayload, requirements) {
|
|
985
|
+
try {
|
|
986
|
+
const solanaPayload = paymentPayload.payload;
|
|
987
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
988
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
989
|
+
}
|
|
990
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
991
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
992
|
+
if (!chainConfig) {
|
|
993
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
994
|
+
}
|
|
995
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
996
|
+
let tx;
|
|
997
|
+
try {
|
|
998
|
+
tx = Transaction.from(txBuffer);
|
|
999
|
+
} catch {
|
|
1000
|
+
tx = VersionedTransaction.deserialize(txBuffer);
|
|
1001
|
+
}
|
|
1002
|
+
if (tx instanceof Transaction) {
|
|
1003
|
+
const hasAnySignature = tx.signatures.some(
|
|
1004
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
1005
|
+
);
|
|
1006
|
+
if (!hasAnySignature) {
|
|
1007
|
+
return { valid: false, error: "Transaction not signed" };
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1011
|
+
const expectedRecipient = new PublicKey2(requirements.payTo);
|
|
1012
|
+
return {
|
|
1013
|
+
valid: true,
|
|
1014
|
+
details: {
|
|
1015
|
+
chain,
|
|
1016
|
+
sender: solanaPayload.sender,
|
|
1017
|
+
recipient: requirements.payTo,
|
|
1018
|
+
amount: requirements.amount
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
return { valid: false, error: error.message };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Settle a Solana payment
|
|
1027
|
+
*
|
|
1028
|
+
* Submits the signed transaction to the network.
|
|
1029
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
1030
|
+
*/
|
|
1031
|
+
async settle(paymentPayload, requirements) {
|
|
1032
|
+
try {
|
|
1033
|
+
const solanaPayload = paymentPayload.payload;
|
|
1034
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1035
|
+
return { success: false, error: "Missing signed transaction" };
|
|
1036
|
+
}
|
|
1037
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1038
|
+
const connection = this.getConnection(chain);
|
|
1039
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1040
|
+
let txToSend;
|
|
1041
|
+
try {
|
|
1042
|
+
const tx = Transaction.from(txBuffer);
|
|
1043
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
1044
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
1045
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
1046
|
+
if (txFeePayer === feePayerPubkey) {
|
|
1047
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
1048
|
+
tx.partialSign(this.feePayerKeypair);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
txToSend = tx.serialize();
|
|
1052
|
+
} catch (e) {
|
|
1053
|
+
txToSend = txBuffer;
|
|
1054
|
+
}
|
|
1055
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
1056
|
+
skipPreflight: false,
|
|
1057
|
+
preflightCommitment: "confirmed"
|
|
1058
|
+
});
|
|
1059
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
1060
|
+
if (confirmation.value.err) {
|
|
1061
|
+
return {
|
|
1062
|
+
success: false,
|
|
1063
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
1064
|
+
transaction: signature
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
success: true,
|
|
1069
|
+
transaction: signature,
|
|
1070
|
+
status: "confirmed"
|
|
1071
|
+
};
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
return { success: false, error: error.message };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
supportsNetwork(network) {
|
|
1077
|
+
return this.supportedNetworks.includes(network);
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
1081
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
1082
|
+
const connection = new Connection2(chainConfig.rpc, "confirmed");
|
|
1083
|
+
const mint = new PublicKey2(chainConfig.tokens.USDC.mint);
|
|
1084
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
1085
|
+
const senderATA = await getAssociatedTokenAddress(mint, senderPubkey);
|
|
1086
|
+
const recipientATA = await getAssociatedTokenAddress(mint, recipientPubkey);
|
|
1087
|
+
const transaction = new Transaction();
|
|
1088
|
+
try {
|
|
1089
|
+
await getAccount(connection, recipientATA);
|
|
1090
|
+
} catch {
|
|
1091
|
+
transaction.add(
|
|
1092
|
+
createAssociatedTokenAccountInstruction(
|
|
1093
|
+
actualFeePayer,
|
|
1094
|
+
// payer (fee payer in gasless mode)
|
|
1095
|
+
recipientATA,
|
|
1096
|
+
// ata to create
|
|
1097
|
+
recipientPubkey,
|
|
1098
|
+
// owner
|
|
1099
|
+
mint
|
|
1100
|
+
// mint
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
transaction.add(
|
|
1105
|
+
createTransferCheckedInstruction(
|
|
1106
|
+
senderATA,
|
|
1107
|
+
// source
|
|
1108
|
+
mint,
|
|
1109
|
+
// mint
|
|
1110
|
+
recipientATA,
|
|
1111
|
+
// destination
|
|
1112
|
+
senderPubkey,
|
|
1113
|
+
// owner (sender still authorizes the transfer)
|
|
1114
|
+
amount,
|
|
1115
|
+
// amount
|
|
1116
|
+
chainConfig.tokens.USDC.decimals
|
|
1117
|
+
// decimals
|
|
1118
|
+
)
|
|
1119
|
+
);
|
|
1120
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
1121
|
+
transaction.recentBlockhash = blockhash;
|
|
1122
|
+
transaction.feePayer = actualFeePayer;
|
|
1123
|
+
return transaction;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
495
1126
|
// src/facilitators/registry.ts
|
|
1127
|
+
import { Keypair as Keypair2 } from "@solana/web3.js";
|
|
1128
|
+
import bs58 from "bs58";
|
|
496
1129
|
var FacilitatorRegistry = class {
|
|
497
1130
|
factories = /* @__PURE__ */ new Map();
|
|
498
1131
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -501,7 +1134,20 @@ var FacilitatorRegistry = class {
|
|
|
501
1134
|
constructor(selection) {
|
|
502
1135
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
503
1136
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
504
|
-
this.
|
|
1137
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
1138
|
+
this.registerFactory("solana", (config) => {
|
|
1139
|
+
let feePayerKeypair;
|
|
1140
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
1141
|
+
if (feePayerKey) {
|
|
1142
|
+
try {
|
|
1143
|
+
feePayerKeypair = Keypair2.fromSecretKey(bs58.decode(feePayerKey));
|
|
1144
|
+
} catch (e) {
|
|
1145
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
1149
|
+
});
|
|
1150
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
505
1151
|
}
|
|
506
1152
|
/**
|
|
507
1153
|
* Register a new facilitator factory
|
|
@@ -755,14 +1401,40 @@ var TOKEN_ADDRESSES = {
|
|
|
755
1401
|
// pathUSD
|
|
756
1402
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
757
1403
|
// alphaUSD
|
|
1404
|
+
},
|
|
1405
|
+
// BNB Smart Chain mainnet
|
|
1406
|
+
"eip155:56": {
|
|
1407
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
1408
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
1409
|
+
},
|
|
1410
|
+
// BNB Smart Chain testnet
|
|
1411
|
+
"eip155:97": {
|
|
1412
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
1413
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
1414
|
+
},
|
|
1415
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
1416
|
+
"solana:mainnet": {
|
|
1417
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
1418
|
+
// Circle USDC
|
|
1419
|
+
},
|
|
1420
|
+
"solana:devnet": {
|
|
1421
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
1422
|
+
// Devnet USDC
|
|
758
1423
|
}
|
|
759
1424
|
};
|
|
760
1425
|
var CHAIN_TO_NETWORK = {
|
|
761
1426
|
"base": "eip155:8453",
|
|
762
1427
|
"base_sepolia": "eip155:84532",
|
|
763
1428
|
"polygon": "eip155:137",
|
|
764
|
-
"tempo_moderato": "eip155:42431"
|
|
1429
|
+
"tempo_moderato": "eip155:42431",
|
|
1430
|
+
"bnb": "eip155:56",
|
|
1431
|
+
"bnb_testnet": "eip155:97",
|
|
1432
|
+
"solana": "solana:mainnet",
|
|
1433
|
+
"solana_devnet": "solana:devnet"
|
|
765
1434
|
};
|
|
1435
|
+
function isSolanaNetwork(network) {
|
|
1436
|
+
return network.startsWith("solana:");
|
|
1437
|
+
}
|
|
766
1438
|
var TOKEN_DOMAINS = {
|
|
767
1439
|
// Base mainnet
|
|
768
1440
|
"eip155:8453": {
|
|
@@ -784,6 +1456,16 @@ var TOKEN_DOMAINS = {
|
|
|
784
1456
|
"eip155:42431": {
|
|
785
1457
|
USDC: { name: "pathUSD", version: "1" },
|
|
786
1458
|
USDT: { name: "alphaUSD", version: "1" }
|
|
1459
|
+
},
|
|
1460
|
+
// BNB Smart Chain mainnet
|
|
1461
|
+
"eip155:56": {
|
|
1462
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1463
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1464
|
+
},
|
|
1465
|
+
// BNB Smart Chain testnet
|
|
1466
|
+
"eip155:97": {
|
|
1467
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1468
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
787
1469
|
}
|
|
788
1470
|
};
|
|
789
1471
|
function getTokenDomain(network, token) {
|
|
@@ -841,7 +1523,7 @@ var MoltsPayServer = class {
|
|
|
841
1523
|
};
|
|
842
1524
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
843
1525
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
844
|
-
const defaultFallback = ["tempo"];
|
|
1526
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
845
1527
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
846
1528
|
const facilitatorConfig = options.facilitators || {
|
|
847
1529
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -884,12 +1566,20 @@ var MoltsPayServer = class {
|
|
|
884
1566
|
*/
|
|
885
1567
|
getProviderChains() {
|
|
886
1568
|
const provider = this.manifest.provider;
|
|
1569
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
1570
|
+
if (explicitWallet) return explicitWallet;
|
|
1571
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
1572
|
+
return provider.solana_wallet;
|
|
1573
|
+
}
|
|
1574
|
+
return provider.wallet;
|
|
1575
|
+
};
|
|
887
1576
|
if (provider.chains && provider.chains.length > 0) {
|
|
888
1577
|
return provider.chains.map((c) => {
|
|
889
1578
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
1579
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
890
1580
|
return {
|
|
891
1581
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
892
|
-
wallet: (
|
|
1582
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
893
1583
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
894
1584
|
};
|
|
895
1585
|
});
|
|
@@ -898,7 +1588,7 @@ var MoltsPayServer = class {
|
|
|
898
1588
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
899
1589
|
return [{
|
|
900
1590
|
network,
|
|
901
|
-
wallet:
|
|
1591
|
+
wallet: getWalletForChain(chain),
|
|
902
1592
|
tokens: ["USDC"]
|
|
903
1593
|
}];
|
|
904
1594
|
}
|
|
@@ -969,7 +1659,8 @@ var MoltsPayServer = class {
|
|
|
969
1659
|
}
|
|
970
1660
|
const body = await this.readBody(req);
|
|
971
1661
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
972
|
-
|
|
1662
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1663
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
973
1664
|
}
|
|
974
1665
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
975
1666
|
const skill = this.skills.get(servicePath);
|
|
@@ -1006,7 +1697,9 @@ var MoltsPayServer = class {
|
|
|
1006
1697
|
name: this.manifest.provider.name,
|
|
1007
1698
|
description: this.manifest.provider.description,
|
|
1008
1699
|
wallet: this.manifest.provider.wallet,
|
|
1009
|
-
chain: this.manifest.provider.chain || "base"
|
|
1700
|
+
chain: this.manifest.provider.chain || "base",
|
|
1701
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
1702
|
+
chains: this.manifest.provider.chains
|
|
1010
1703
|
},
|
|
1011
1704
|
services,
|
|
1012
1705
|
endpoints: {
|
|
@@ -1119,6 +1812,21 @@ var MoltsPayServer = class {
|
|
|
1119
1812
|
});
|
|
1120
1813
|
}
|
|
1121
1814
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
1815
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
1816
|
+
let settlement = null;
|
|
1817
|
+
if (isSolana) {
|
|
1818
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
1819
|
+
try {
|
|
1820
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1821
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
1824
|
+
return this.sendJson(res, 402, {
|
|
1825
|
+
error: "Payment settlement failed",
|
|
1826
|
+
message: err.message
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1122
1830
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1123
1831
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1124
1832
|
let result;
|
|
@@ -1133,16 +1841,19 @@ var MoltsPayServer = class {
|
|
|
1133
1841
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1134
1842
|
return this.sendJson(res, 500, {
|
|
1135
1843
|
error: "Service execution failed",
|
|
1136
|
-
message: err.message
|
|
1844
|
+
message: err.message,
|
|
1845
|
+
paymentSettled: isSolana ? true : false,
|
|
1846
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1137
1847
|
});
|
|
1138
1848
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1849
|
+
if (!isSolana) {
|
|
1850
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
1851
|
+
try {
|
|
1852
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1853
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
1856
|
+
}
|
|
1146
1857
|
}
|
|
1147
1858
|
const responseHeaders = {};
|
|
1148
1859
|
if (settlement?.success) {
|
|
@@ -1418,7 +2129,7 @@ var MoltsPayServer = class {
|
|
|
1418
2129
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
1419
2130
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1420
2131
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
1421
|
-
|
|
2132
|
+
const requirements = {
|
|
1422
2133
|
scheme: "exact",
|
|
1423
2134
|
network: selectedNetwork,
|
|
1424
2135
|
asset: tokenAddress,
|
|
@@ -1427,6 +2138,27 @@ var MoltsPayServer = class {
|
|
|
1427
2138
|
maxTimeoutSeconds: 300,
|
|
1428
2139
|
extra: tokenDomain
|
|
1429
2140
|
};
|
|
2141
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
2142
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
2143
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
2144
|
+
if (feePayerPubkey) {
|
|
2145
|
+
requirements.extra = {
|
|
2146
|
+
...requirements.extra || {},
|
|
2147
|
+
solanaFeePayer: feePayerPubkey
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
2152
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2153
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2154
|
+
if (spenderAddress) {
|
|
2155
|
+
requirements.extra = {
|
|
2156
|
+
...requirements.extra || {},
|
|
2157
|
+
bnbSpender: spenderAddress
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return requirements;
|
|
1430
2162
|
}
|
|
1431
2163
|
/**
|
|
1432
2164
|
* Detect which token is being used in the payment
|
|
@@ -1492,31 +2224,42 @@ var MoltsPayServer = class {
|
|
|
1492
2224
|
/**
|
|
1493
2225
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1494
2226
|
*
|
|
1495
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
2227
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1496
2228
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1497
2229
|
*
|
|
1498
2230
|
* Request body:
|
|
1499
2231
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1500
2232
|
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
2233
|
+
* For x402 (base, polygon, base_sepolia):
|
|
2234
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
2235
|
+
* With X-Payment header: verifies payment via CDP
|
|
2236
|
+
*
|
|
2237
|
+
* For MPP (tempo_moderato):
|
|
2238
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
2239
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1503
2240
|
*/
|
|
1504
|
-
async handleProxy(body, paymentHeader, res) {
|
|
2241
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1505
2242
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1506
2243
|
if (!wallet || !amount) {
|
|
1507
2244
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1508
2245
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
2246
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2247
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
2248
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2249
|
+
}
|
|
2250
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
2251
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
2252
|
+
const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
2253
|
+
if (isSolanaChain && !isValidSolanaAddress) {
|
|
2254
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
2255
|
+
}
|
|
2256
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
2257
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1511
2258
|
}
|
|
1512
2259
|
const amountNum = parseFloat(amount);
|
|
1513
2260
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1514
2261
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1515
2262
|
}
|
|
1516
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1517
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
1518
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
1519
|
-
}
|
|
1520
2263
|
const proxyConfig = {
|
|
1521
2264
|
id: serviceId || "proxy",
|
|
1522
2265
|
name: description || "Proxy Payment",
|
|
@@ -1528,6 +2271,9 @@ var MoltsPayServer = class {
|
|
|
1528
2271
|
input: {},
|
|
1529
2272
|
output: {}
|
|
1530
2273
|
};
|
|
2274
|
+
if (chain === "tempo_moderato") {
|
|
2275
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
2276
|
+
}
|
|
1531
2277
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1532
2278
|
if (!paymentHeader) {
|
|
1533
2279
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1539,37 +2285,225 @@ var MoltsPayServer = class {
|
|
|
1539
2285
|
} catch {
|
|
1540
2286
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
1541
2287
|
}
|
|
1542
|
-
if (payment.x402Version !== X402_VERSION2) {
|
|
1543
|
-
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
2288
|
+
if (payment.x402Version !== X402_VERSION2) {
|
|
2289
|
+
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
2290
|
+
}
|
|
2291
|
+
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2292
|
+
const network = payment.accepted?.network || payment.network;
|
|
2293
|
+
if (scheme !== "exact") {
|
|
2294
|
+
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
2295
|
+
}
|
|
2296
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
2297
|
+
if (network !== expectedNetwork) {
|
|
2298
|
+
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
2299
|
+
}
|
|
2300
|
+
console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
|
|
2301
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
2302
|
+
if (!verifyResult.valid) {
|
|
2303
|
+
return this.sendJson(res, 402, {
|
|
2304
|
+
success: false,
|
|
2305
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2306
|
+
facilitator: verifyResult.facilitator
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
2310
|
+
const { execute, service, params } = body;
|
|
2311
|
+
if (execute && service) {
|
|
2312
|
+
const skill = this.skills.get(service);
|
|
2313
|
+
if (!skill) {
|
|
2314
|
+
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
2315
|
+
return this.sendJson(res, 404, {
|
|
2316
|
+
success: false,
|
|
2317
|
+
paymentSettled: false,
|
|
2318
|
+
error: `Service not found: ${service}`
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
const isSolana = isSolanaNetwork(network);
|
|
2322
|
+
let settlement2 = null;
|
|
2323
|
+
if (isSolana) {
|
|
2324
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
2325
|
+
try {
|
|
2326
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2327
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2328
|
+
if (!settlement2.success) {
|
|
2329
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
2330
|
+
return this.sendJson(res, 402, {
|
|
2331
|
+
success: false,
|
|
2332
|
+
paymentSettled: false,
|
|
2333
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
} catch (err) {
|
|
2337
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
2338
|
+
return this.sendJson(res, 402, {
|
|
2339
|
+
success: false,
|
|
2340
|
+
paymentSettled: false,
|
|
2341
|
+
error: `Payment settlement failed: ${err.message}`
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
} else {
|
|
2345
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
2346
|
+
}
|
|
2347
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2348
|
+
let result;
|
|
2349
|
+
try {
|
|
2350
|
+
result = await Promise.race([
|
|
2351
|
+
skill.handler(params || {}),
|
|
2352
|
+
new Promise(
|
|
2353
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2354
|
+
)
|
|
2355
|
+
]);
|
|
2356
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
2357
|
+
} catch (err) {
|
|
2358
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
2359
|
+
return this.sendJson(res, 500, {
|
|
2360
|
+
success: false,
|
|
2361
|
+
paymentSettled: isSolana ? true : false,
|
|
2362
|
+
error: `Service execution failed: ${err.message}`,
|
|
2363
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
if (!isSolana) {
|
|
2367
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
2368
|
+
try {
|
|
2369
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2370
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2371
|
+
} catch (err) {
|
|
2372
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2373
|
+
return this.sendJson(res, 200, {
|
|
2374
|
+
success: true,
|
|
2375
|
+
verified: true,
|
|
2376
|
+
settled: false,
|
|
2377
|
+
settlementError: err.message,
|
|
2378
|
+
from: payment.payload?.authorization?.from,
|
|
2379
|
+
paidTo: wallet,
|
|
2380
|
+
amount: amountNum,
|
|
2381
|
+
currency: currency || "USDC",
|
|
2382
|
+
memo,
|
|
2383
|
+
result
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return this.sendJson(res, 200, {
|
|
2388
|
+
success: true,
|
|
2389
|
+
verified: true,
|
|
2390
|
+
settled: settlement2?.success || false,
|
|
2391
|
+
txHash: settlement2?.transaction,
|
|
2392
|
+
from: payment.payload?.authorization?.from,
|
|
2393
|
+
paidTo: wallet,
|
|
2394
|
+
amount: amountNum,
|
|
2395
|
+
currency: currency || "USDC",
|
|
2396
|
+
facilitator: settlement2?.facilitator,
|
|
2397
|
+
memo,
|
|
2398
|
+
result
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
2402
|
+
let settlement = null;
|
|
2403
|
+
try {
|
|
2404
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2405
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2406
|
+
} catch (err) {
|
|
2407
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2408
|
+
return this.sendJson(res, 500, {
|
|
2409
|
+
success: false,
|
|
2410
|
+
error: `Settlement failed: ${err.message}`
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
this.sendJson(res, 200, {
|
|
2414
|
+
success: true,
|
|
2415
|
+
verified: true,
|
|
2416
|
+
settled: settlement?.success || false,
|
|
2417
|
+
txHash: settlement?.transaction,
|
|
2418
|
+
from: payment.payload?.authorization?.from,
|
|
2419
|
+
// Buyer's wallet address
|
|
2420
|
+
paidTo: wallet,
|
|
2421
|
+
amount: amountNum,
|
|
2422
|
+
currency: currency || "USDC",
|
|
2423
|
+
facilitator: settlement?.facilitator,
|
|
2424
|
+
memo
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
2429
|
+
*/
|
|
2430
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
2431
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
2432
|
+
const amountNum = parseFloat(amount);
|
|
2433
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
2434
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
2435
|
+
const challengeId = this.generateChallengeId();
|
|
2436
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
2437
|
+
const mppRequest = {
|
|
2438
|
+
amount: amountInUnits,
|
|
2439
|
+
currency: tokenAddress,
|
|
2440
|
+
methodDetails: {
|
|
2441
|
+
chainId: 42431,
|
|
2442
|
+
feePayer: true
|
|
2443
|
+
},
|
|
2444
|
+
recipient: wallet
|
|
2445
|
+
};
|
|
2446
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2447
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2448
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2449
|
+
res.writeHead(402, {
|
|
2450
|
+
"Content-Type": "application/problem+json",
|
|
2451
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
2452
|
+
});
|
|
2453
|
+
res.end(JSON.stringify({
|
|
2454
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
2455
|
+
title: "Payment Required",
|
|
2456
|
+
status: 402,
|
|
2457
|
+
detail: `Payment is required (${config.name}).`,
|
|
2458
|
+
service: serviceId || "proxy",
|
|
2459
|
+
price: amountNum,
|
|
2460
|
+
currency: "USDC"
|
|
2461
|
+
}, null, 2));
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2465
|
+
if (!credentialMatch) {
|
|
2466
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
1544
2467
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2468
|
+
let mppCredential;
|
|
2469
|
+
try {
|
|
2470
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2471
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2472
|
+
mppCredential = JSON.parse(decoded);
|
|
2473
|
+
} catch (err) {
|
|
2474
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
2475
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
1549
2476
|
}
|
|
1550
|
-
|
|
1551
|
-
if (
|
|
1552
|
-
|
|
2477
|
+
let txHash;
|
|
2478
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2479
|
+
txHash = mppCredential.payload.hash;
|
|
2480
|
+
} else {
|
|
2481
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
1553
2482
|
}
|
|
1554
|
-
console.log(`[MoltsPay] /proxy: Verifying
|
|
1555
|
-
const
|
|
1556
|
-
|
|
2483
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
2484
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
2485
|
+
const paymentPayload = {
|
|
2486
|
+
x402Version: X402_VERSION2,
|
|
2487
|
+
scheme: "exact",
|
|
2488
|
+
network: "eip155:42431",
|
|
2489
|
+
payload: { txHash, chainId: 42431 }
|
|
2490
|
+
};
|
|
2491
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
2492
|
+
if (!verification.valid) {
|
|
1557
2493
|
return this.sendJson(res, 402, {
|
|
1558
|
-
|
|
1559
|
-
error: `Payment verification failed: ${verifyResult.error}`,
|
|
1560
|
-
facilitator: verifyResult.facilitator
|
|
2494
|
+
error: `Payment verification failed: ${verification.error}`
|
|
1561
2495
|
});
|
|
1562
2496
|
}
|
|
1563
|
-
console.log(`[MoltsPay] /proxy:
|
|
2497
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
1564
2498
|
const { execute, service, params } = body;
|
|
1565
2499
|
if (execute && service) {
|
|
1566
|
-
console.log(`[MoltsPay] /proxy: Executing skill
|
|
2500
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
1567
2501
|
const skill = this.skills.get(service);
|
|
1568
2502
|
if (!skill) {
|
|
1569
|
-
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
1570
2503
|
return this.sendJson(res, 404, {
|
|
1571
2504
|
success: false,
|
|
1572
|
-
paymentSettled:
|
|
2505
|
+
paymentSettled: true,
|
|
2506
|
+
// Payment already happened on Tempo
|
|
1573
2507
|
error: `Service not found: ${service}`
|
|
1574
2508
|
});
|
|
1575
2509
|
}
|
|
@@ -1582,73 +2516,36 @@ var MoltsPayServer = class {
|
|
|
1582
2516
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1583
2517
|
)
|
|
1584
2518
|
]);
|
|
1585
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
|
|
1586
2519
|
} catch (err) {
|
|
1587
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
2520
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
1588
2521
|
return this.sendJson(res, 500, {
|
|
1589
2522
|
success: false,
|
|
1590
|
-
paymentSettled:
|
|
2523
|
+
paymentSettled: true,
|
|
1591
2524
|
error: `Service execution failed: ${err.message}`
|
|
1592
2525
|
});
|
|
1593
2526
|
}
|
|
1594
|
-
let settlement2 = null;
|
|
1595
|
-
try {
|
|
1596
|
-
settlement2 = await this.registry.settle(payment, requirements);
|
|
1597
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
1598
|
-
} catch (err) {
|
|
1599
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
1600
|
-
return this.sendJson(res, 200, {
|
|
1601
|
-
success: true,
|
|
1602
|
-
verified: true,
|
|
1603
|
-
settled: false,
|
|
1604
|
-
settlementError: err.message,
|
|
1605
|
-
from: payment.payload?.authorization?.from,
|
|
1606
|
-
// Buyer's wallet address
|
|
1607
|
-
paidTo: wallet,
|
|
1608
|
-
amount: amountNum,
|
|
1609
|
-
currency: currency || "USDC",
|
|
1610
|
-
memo,
|
|
1611
|
-
result
|
|
1612
|
-
});
|
|
1613
|
-
}
|
|
1614
2527
|
return this.sendJson(res, 200, {
|
|
1615
2528
|
success: true,
|
|
1616
2529
|
verified: true,
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
from: payment.payload?.authorization?.from,
|
|
1620
|
-
// Buyer's wallet address
|
|
2530
|
+
txHash,
|
|
2531
|
+
chain: "tempo_moderato",
|
|
1621
2532
|
paidTo: wallet,
|
|
1622
2533
|
amount: amountNum,
|
|
1623
|
-
currency:
|
|
1624
|
-
facilitator:
|
|
2534
|
+
currency: "USDC",
|
|
2535
|
+
facilitator: verification.facilitator,
|
|
1625
2536
|
memo,
|
|
1626
2537
|
result
|
|
1627
2538
|
});
|
|
1628
2539
|
}
|
|
1629
|
-
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
1630
|
-
let settlement = null;
|
|
1631
|
-
try {
|
|
1632
|
-
settlement = await this.registry.settle(payment, requirements);
|
|
1633
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1634
|
-
} catch (err) {
|
|
1635
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
1636
|
-
return this.sendJson(res, 500, {
|
|
1637
|
-
success: false,
|
|
1638
|
-
error: `Settlement failed: ${err.message}`
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
2540
|
this.sendJson(res, 200, {
|
|
1642
2541
|
success: true,
|
|
1643
2542
|
verified: true,
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
from: payment.payload?.authorization?.from,
|
|
1647
|
-
// Buyer's wallet address
|
|
2543
|
+
txHash,
|
|
2544
|
+
chain: "tempo_moderato",
|
|
1648
2545
|
paidTo: wallet,
|
|
1649
2546
|
amount: amountNum,
|
|
1650
|
-
currency:
|
|
1651
|
-
facilitator:
|
|
2547
|
+
currency: "USDC",
|
|
2548
|
+
facilitator: verification.facilitator,
|
|
1652
2549
|
memo
|
|
1653
2550
|
});
|
|
1654
2551
|
}
|
|
@@ -1663,7 +2560,7 @@ var MoltsPayServer = class {
|
|
|
1663
2560
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1664
2561
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1665
2562
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1666
|
-
|
|
2563
|
+
const requirements = {
|
|
1667
2564
|
scheme: "exact",
|
|
1668
2565
|
network: networkId,
|
|
1669
2566
|
asset: tokenAddress,
|
|
@@ -1673,6 +2570,17 @@ var MoltsPayServer = class {
|
|
|
1673
2570
|
maxTimeoutSeconds: 300,
|
|
1674
2571
|
extra: tokenDomain
|
|
1675
2572
|
};
|
|
2573
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
2574
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2575
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2576
|
+
if (spenderAddress) {
|
|
2577
|
+
requirements.extra = {
|
|
2578
|
+
...requirements.extra || {},
|
|
2579
|
+
bnbSpender: spenderAddress
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return requirements;
|
|
1676
2584
|
}
|
|
1677
2585
|
/**
|
|
1678
2586
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -1703,10 +2611,40 @@ var MoltsPayServer = class {
|
|
|
1703
2611
|
};
|
|
1704
2612
|
|
|
1705
2613
|
// src/client/index.ts
|
|
1706
|
-
import { existsSync as
|
|
1707
|
-
import { homedir } from "os";
|
|
1708
|
-
import { join as
|
|
2614
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
|
|
2615
|
+
import { homedir as homedir2 } from "os";
|
|
2616
|
+
import { join as join4 } from "path";
|
|
1709
2617
|
import { Wallet, ethers } from "ethers";
|
|
2618
|
+
|
|
2619
|
+
// src/wallet/solana.ts
|
|
2620
|
+
import { Keypair as Keypair3, PublicKey as PublicKey3, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
|
2621
|
+
import { getAssociatedTokenAddress as getAssociatedTokenAddress2, getAccount as getAccount2 } from "@solana/spl-token";
|
|
2622
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync } from "fs";
|
|
2623
|
+
import { join as join3 } from "path";
|
|
2624
|
+
import { homedir } from "os";
|
|
2625
|
+
import bs582 from "bs58";
|
|
2626
|
+
var DEFAULT_CONFIG_DIR = join3(homedir(), ".moltspay");
|
|
2627
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
2628
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
2629
|
+
return join3(configDir, SOLANA_WALLET_FILE);
|
|
2630
|
+
}
|
|
2631
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
2632
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
2633
|
+
if (!existsSync3(walletPath)) {
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
try {
|
|
2637
|
+
const data = JSON.parse(readFileSync3(walletPath, "utf-8"));
|
|
2638
|
+
const secretKey = bs582.decode(data.secretKey);
|
|
2639
|
+
return Keypair3.fromSecretKey(secretKey);
|
|
2640
|
+
} catch (error) {
|
|
2641
|
+
console.error("Failed to load Solana wallet:", error);
|
|
2642
|
+
return null;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/client/index.ts
|
|
2647
|
+
import { PublicKey as PublicKey4 } from "@solana/web3.js";
|
|
1710
2648
|
var X402_VERSION3 = 2;
|
|
1711
2649
|
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
1712
2650
|
var PAYMENT_HEADER2 = "x-payment";
|
|
@@ -1725,7 +2663,7 @@ var MoltsPayClient = class {
|
|
|
1725
2663
|
todaySpending = 0;
|
|
1726
2664
|
lastSpendingReset = 0;
|
|
1727
2665
|
constructor(options = {}) {
|
|
1728
|
-
this.configDir = options.configDir ||
|
|
2666
|
+
this.configDir = options.configDir || join4(homedir2(), ".moltspay");
|
|
1729
2667
|
this.config = this.loadConfig();
|
|
1730
2668
|
this.walletData = this.loadWallet();
|
|
1731
2669
|
this.loadSpending();
|
|
@@ -1745,6 +2683,12 @@ var MoltsPayClient = class {
|
|
|
1745
2683
|
get address() {
|
|
1746
2684
|
return this.wallet?.address || null;
|
|
1747
2685
|
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Get wallet instance (for direct operations like approvals)
|
|
2688
|
+
*/
|
|
2689
|
+
getWallet() {
|
|
2690
|
+
return this.wallet;
|
|
2691
|
+
}
|
|
1748
2692
|
/**
|
|
1749
2693
|
* Get current config
|
|
1750
2694
|
*/
|
|
@@ -1814,9 +2758,14 @@ var MoltsPayClient = class {
|
|
|
1814
2758
|
}
|
|
1815
2759
|
throw new Error(data.error || "Unexpected response");
|
|
1816
2760
|
}
|
|
2761
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
1817
2762
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER2);
|
|
2763
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
2764
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
2765
|
+
return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
|
|
2766
|
+
}
|
|
1818
2767
|
if (!paymentRequiredHeader) {
|
|
1819
|
-
throw new Error("Missing x-payment-required
|
|
2768
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
1820
2769
|
}
|
|
1821
2770
|
let requirements;
|
|
1822
2771
|
try {
|
|
@@ -1833,17 +2782,22 @@ var MoltsPayClient = class {
|
|
|
1833
2782
|
throw new Error("Invalid x-payment-required header");
|
|
1834
2783
|
}
|
|
1835
2784
|
const networkToChainName = (network2) => {
|
|
2785
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
2786
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
1836
2787
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
1837
2788
|
if (!match) return null;
|
|
1838
2789
|
const chainId = parseInt(match[1]);
|
|
1839
2790
|
if (chainId === 8453) return "base";
|
|
1840
2791
|
if (chainId === 137) return "polygon";
|
|
1841
2792
|
if (chainId === 84532) return "base_sepolia";
|
|
2793
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
2794
|
+
if (chainId === 56) return "bnb";
|
|
2795
|
+
if (chainId === 97) return "bnb_testnet";
|
|
1842
2796
|
return null;
|
|
1843
2797
|
};
|
|
1844
2798
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
1845
|
-
let chainName;
|
|
1846
2799
|
const userSpecifiedChain = options.chain;
|
|
2800
|
+
let selectedChain;
|
|
1847
2801
|
if (userSpecifiedChain) {
|
|
1848
2802
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
1849
2803
|
throw new Error(
|
|
@@ -1851,17 +2805,27 @@ var MoltsPayClient = class {
|
|
|
1851
2805
|
Server accepts: ${serverChains.join(", ")}`
|
|
1852
2806
|
);
|
|
1853
2807
|
}
|
|
1854
|
-
|
|
2808
|
+
selectedChain = userSpecifiedChain;
|
|
1855
2809
|
} else {
|
|
1856
2810
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
1857
|
-
|
|
2811
|
+
selectedChain = "base";
|
|
1858
2812
|
} else {
|
|
1859
2813
|
throw new Error(
|
|
1860
2814
|
`Server accepts: ${serverChains.join(", ")}
|
|
1861
|
-
Please specify: --chain
|
|
2815
|
+
Please specify: --chain <chain_name>`
|
|
1862
2816
|
);
|
|
1863
2817
|
}
|
|
1864
2818
|
}
|
|
2819
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
2820
|
+
const solanaChain = selectedChain;
|
|
2821
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
2822
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
2823
|
+
if (!req2) {
|
|
2824
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
2825
|
+
}
|
|
2826
|
+
return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
|
|
2827
|
+
}
|
|
2828
|
+
const chainName = selectedChain;
|
|
1865
2829
|
const chain = getChain(chainName);
|
|
1866
2830
|
const network = `eip155:${chain.chainId}`;
|
|
1867
2831
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -1896,6 +2860,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
1896
2860
|
} else {
|
|
1897
2861
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
1898
2862
|
}
|
|
2863
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
2864
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
2865
|
+
const payTo2 = req.payTo || req.resource;
|
|
2866
|
+
if (!payTo2) {
|
|
2867
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
2868
|
+
}
|
|
2869
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
2870
|
+
if (!bnbSpender) {
|
|
2871
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
2872
|
+
}
|
|
2873
|
+
return await this.handleBNBPayment(serverUrl, service, params, {
|
|
2874
|
+
to: payTo2,
|
|
2875
|
+
amount,
|
|
2876
|
+
token,
|
|
2877
|
+
chainName,
|
|
2878
|
+
chain,
|
|
2879
|
+
spender: bnbSpender
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
1899
2882
|
const payTo = req.payTo || req.resource;
|
|
1900
2883
|
if (!payTo) {
|
|
1901
2884
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -1945,6 +2928,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
1945
2928
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
1946
2929
|
return result.result;
|
|
1947
2930
|
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
2933
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
2934
|
+
*/
|
|
2935
|
+
async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
|
|
2936
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
2937
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
2938
|
+
const { tempoModerato } = await import("viem/chains");
|
|
2939
|
+
const { Actions } = await import("viem/tempo");
|
|
2940
|
+
const privateKey = this.walletData.privateKey;
|
|
2941
|
+
const account = privateKeyToAccount2(privateKey);
|
|
2942
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
2943
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
2944
|
+
const parseAuthParam = (header, key) => {
|
|
2945
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
2946
|
+
return match ? match[1] : null;
|
|
2947
|
+
};
|
|
2948
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
2949
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
2950
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
2951
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
2952
|
+
if (method !== "tempo") {
|
|
2953
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
2954
|
+
}
|
|
2955
|
+
if (!requestB64) {
|
|
2956
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
2957
|
+
}
|
|
2958
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
2959
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
2960
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
2961
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
2962
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
2963
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
2964
|
+
this.checkLimits(amountDisplay);
|
|
2965
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
2966
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
2967
|
+
const publicClient = createPublicClient({
|
|
2968
|
+
chain: tempoChain,
|
|
2969
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
2970
|
+
});
|
|
2971
|
+
const walletClient = createWalletClient({
|
|
2972
|
+
account,
|
|
2973
|
+
chain: tempoChain,
|
|
2974
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
2975
|
+
});
|
|
2976
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
2977
|
+
to: recipient,
|
|
2978
|
+
amount: BigInt(amount),
|
|
2979
|
+
token: currency
|
|
2980
|
+
});
|
|
2981
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
2982
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
2983
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
2984
|
+
const credential = {
|
|
2985
|
+
challenge: {
|
|
2986
|
+
id: challengeId,
|
|
2987
|
+
realm,
|
|
2988
|
+
method: "tempo",
|
|
2989
|
+
intent: "charge",
|
|
2990
|
+
request: paymentRequest
|
|
2991
|
+
},
|
|
2992
|
+
payload: { hash: txHash, type: "hash" },
|
|
2993
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
2994
|
+
};
|
|
2995
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2996
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
2997
|
+
method: "POST",
|
|
2998
|
+
headers: {
|
|
2999
|
+
"Content-Type": "application/json",
|
|
3000
|
+
"Authorization": `Payment ${credentialB64}`
|
|
3001
|
+
},
|
|
3002
|
+
body: JSON.stringify({ service, params, chain: "tempo_moderato" })
|
|
3003
|
+
});
|
|
3004
|
+
const result = await paidRes.json();
|
|
3005
|
+
if (!paidRes.ok) {
|
|
3006
|
+
throw new Error(result.error || "Payment verification failed");
|
|
3007
|
+
}
|
|
3008
|
+
this.recordSpending(amountDisplay);
|
|
3009
|
+
console.log(`[MoltsPay] Success!`);
|
|
3010
|
+
return result.result || result;
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
3014
|
+
*
|
|
3015
|
+
* Flow:
|
|
3016
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
3017
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
3018
|
+
* 3. Send intent to server
|
|
3019
|
+
* 4. Server executes service
|
|
3020
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
3021
|
+
*/
|
|
3022
|
+
async handleBNBPayment(serverUrl, service, params, paymentDetails) {
|
|
3023
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
3024
|
+
const tokenConfig = chain.tokens[token];
|
|
3025
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
3026
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
3027
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
3028
|
+
if (allowance < amountWeiCheck) {
|
|
3029
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
3030
|
+
const minGasBalance = ethers.parseEther("0.0005");
|
|
3031
|
+
if (nativeBalance < minGasBalance) {
|
|
3032
|
+
const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
|
|
3033
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
3034
|
+
if (isTestnet) {
|
|
3035
|
+
throw new Error(
|
|
3036
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
3037
|
+
|
|
3038
|
+
Current tBNB: ${nativeBNB}
|
|
3039
|
+
Required: ~0.001 tBNB
|
|
3040
|
+
|
|
3041
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
3042
|
+
(Gives USDC + tBNB for gas)`
|
|
3043
|
+
);
|
|
3044
|
+
} else {
|
|
3045
|
+
throw new Error(
|
|
3046
|
+
`\u274C Insufficient BNB for approval transaction
|
|
3047
|
+
|
|
3048
|
+
Current BNB: ${nativeBNB}
|
|
3049
|
+
Required: ~0.001 BNB (~$0.60)
|
|
3050
|
+
|
|
3051
|
+
To get BNB:
|
|
3052
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
3053
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
3054
|
+
|
|
3055
|
+
After funding, run:
|
|
3056
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
throw new Error(
|
|
3061
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
3062
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
3066
|
+
const intent = {
|
|
3067
|
+
from: this.wallet.address,
|
|
3068
|
+
to,
|
|
3069
|
+
amount: amountWei,
|
|
3070
|
+
token: tokenConfig.address,
|
|
3071
|
+
service,
|
|
3072
|
+
nonce: Date.now(),
|
|
3073
|
+
// Use timestamp as nonce for simplicity
|
|
3074
|
+
deadline: Date.now() + 36e5
|
|
3075
|
+
// 1 hour
|
|
3076
|
+
};
|
|
3077
|
+
const domain = {
|
|
3078
|
+
name: "MoltsPay",
|
|
3079
|
+
version: "1",
|
|
3080
|
+
chainId: chain.chainId
|
|
3081
|
+
};
|
|
3082
|
+
const types = {
|
|
3083
|
+
PaymentIntent: [
|
|
3084
|
+
{ name: "from", type: "address" },
|
|
3085
|
+
{ name: "to", type: "address" },
|
|
3086
|
+
{ name: "amount", type: "uint256" },
|
|
3087
|
+
{ name: "token", type: "address" },
|
|
3088
|
+
{ name: "service", type: "string" },
|
|
3089
|
+
{ name: "nonce", type: "uint256" },
|
|
3090
|
+
{ name: "deadline", type: "uint256" }
|
|
3091
|
+
]
|
|
3092
|
+
};
|
|
3093
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
3094
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
3095
|
+
const network = `eip155:${chain.chainId}`;
|
|
3096
|
+
const payload = {
|
|
3097
|
+
x402Version: 2,
|
|
3098
|
+
scheme: "exact",
|
|
3099
|
+
network,
|
|
3100
|
+
payload: {
|
|
3101
|
+
intent: {
|
|
3102
|
+
...intent,
|
|
3103
|
+
signature
|
|
3104
|
+
},
|
|
3105
|
+
chainId: chain.chainId
|
|
3106
|
+
},
|
|
3107
|
+
accepted: {
|
|
3108
|
+
scheme: "exact",
|
|
3109
|
+
network,
|
|
3110
|
+
asset: tokenConfig.address,
|
|
3111
|
+
amount: amountWei,
|
|
3112
|
+
payTo: to,
|
|
3113
|
+
maxTimeoutSeconds: 300
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
3116
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
3117
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
3118
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
3119
|
+
method: "POST",
|
|
3120
|
+
headers: {
|
|
3121
|
+
"Content-Type": "application/json",
|
|
3122
|
+
"X-Payment": paymentHeader
|
|
3123
|
+
},
|
|
3124
|
+
body: JSON.stringify({ service, params, chain: chainName })
|
|
3125
|
+
});
|
|
3126
|
+
const result = await paidRes.json();
|
|
3127
|
+
if (!paidRes.ok) {
|
|
3128
|
+
throw new Error(result.error || "BNB payment failed");
|
|
3129
|
+
}
|
|
3130
|
+
this.recordSpending(amount);
|
|
3131
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
3132
|
+
return result.result || result;
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Handle Solana payment flow
|
|
3136
|
+
*
|
|
3137
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
3138
|
+
* 1. Client creates and signs a transfer transaction
|
|
3139
|
+
* 2. Server submits the transaction after service completes
|
|
3140
|
+
*/
|
|
3141
|
+
async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
|
|
3142
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
3143
|
+
if (!solanaWallet) {
|
|
3144
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
3145
|
+
}
|
|
3146
|
+
const amount = Number(requirements.amount);
|
|
3147
|
+
const amountUSDC = amount / 1e6;
|
|
3148
|
+
this.checkLimits(amountUSDC);
|
|
3149
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
3150
|
+
if (!requirements.payTo) {
|
|
3151
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
3152
|
+
}
|
|
3153
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
3154
|
+
const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
|
|
3155
|
+
if (feePayerPubkey) {
|
|
3156
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
3157
|
+
}
|
|
3158
|
+
const recipientPubkey = new PublicKey4(requirements.payTo);
|
|
3159
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
3160
|
+
solanaWallet.publicKey,
|
|
3161
|
+
recipientPubkey,
|
|
3162
|
+
BigInt(amount),
|
|
3163
|
+
chain,
|
|
3164
|
+
feePayerPubkey
|
|
3165
|
+
// Optional fee payer for gasless mode
|
|
3166
|
+
);
|
|
3167
|
+
if (feePayerPubkey) {
|
|
3168
|
+
transaction.partialSign(solanaWallet);
|
|
3169
|
+
} else {
|
|
3170
|
+
transaction.sign(solanaWallet);
|
|
3171
|
+
}
|
|
3172
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
3173
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
3174
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
3175
|
+
const payload = {
|
|
3176
|
+
x402Version: 2,
|
|
3177
|
+
scheme: "exact",
|
|
3178
|
+
network,
|
|
3179
|
+
payload: {
|
|
3180
|
+
signedTransaction: signedTx,
|
|
3181
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
3182
|
+
chain
|
|
3183
|
+
},
|
|
3184
|
+
accepted: {
|
|
3185
|
+
scheme: "exact",
|
|
3186
|
+
network,
|
|
3187
|
+
asset: requirements.asset,
|
|
3188
|
+
amount: requirements.amount,
|
|
3189
|
+
payTo: requirements.payTo,
|
|
3190
|
+
maxTimeoutSeconds: 300
|
|
3191
|
+
}
|
|
3192
|
+
};
|
|
3193
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
3194
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
3195
|
+
method: "POST",
|
|
3196
|
+
headers: {
|
|
3197
|
+
"Content-Type": "application/json",
|
|
3198
|
+
"X-Payment": paymentHeader
|
|
3199
|
+
},
|
|
3200
|
+
body: JSON.stringify({ service, params, chain })
|
|
3201
|
+
});
|
|
3202
|
+
const result = await paidRes.json();
|
|
3203
|
+
if (!paidRes.ok) {
|
|
3204
|
+
throw new Error(result.error || "Solana payment failed");
|
|
3205
|
+
}
|
|
3206
|
+
this.recordSpending(amountUSDC);
|
|
3207
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
3208
|
+
if (result.payment?.transaction) {
|
|
3209
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
3210
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
3211
|
+
}
|
|
3212
|
+
return result.result || result;
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Check ERC20 allowance for a spender
|
|
3216
|
+
*/
|
|
3217
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
3218
|
+
const contract = new ethers.Contract(
|
|
3219
|
+
tokenAddress,
|
|
3220
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
3221
|
+
provider
|
|
3222
|
+
);
|
|
3223
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
3224
|
+
}
|
|
1948
3225
|
/**
|
|
1949
3226
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
1950
3227
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -2015,26 +3292,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2015
3292
|
}
|
|
2016
3293
|
// --- Config & Wallet Management ---
|
|
2017
3294
|
loadConfig() {
|
|
2018
|
-
const configPath =
|
|
2019
|
-
if (
|
|
2020
|
-
const content =
|
|
3295
|
+
const configPath = join4(this.configDir, "config.json");
|
|
3296
|
+
if (existsSync4(configPath)) {
|
|
3297
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
2021
3298
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
2022
3299
|
}
|
|
2023
3300
|
return { ...DEFAULT_CONFIG };
|
|
2024
3301
|
}
|
|
2025
3302
|
saveConfig() {
|
|
2026
|
-
|
|
2027
|
-
const configPath =
|
|
2028
|
-
|
|
3303
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
3304
|
+
const configPath = join4(this.configDir, "config.json");
|
|
3305
|
+
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
2029
3306
|
}
|
|
2030
3307
|
/**
|
|
2031
3308
|
* Load spending data from disk
|
|
2032
3309
|
*/
|
|
2033
3310
|
loadSpending() {
|
|
2034
|
-
const spendingPath =
|
|
2035
|
-
if (
|
|
3311
|
+
const spendingPath = join4(this.configDir, "spending.json");
|
|
3312
|
+
if (existsSync4(spendingPath)) {
|
|
2036
3313
|
try {
|
|
2037
|
-
const data = JSON.parse(
|
|
3314
|
+
const data = JSON.parse(readFileSync4(spendingPath, "utf-8"));
|
|
2038
3315
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
2039
3316
|
if (data.date && data.date === today) {
|
|
2040
3317
|
this.todaySpending = data.amount || 0;
|
|
@@ -2053,18 +3330,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2053
3330
|
* Save spending data to disk
|
|
2054
3331
|
*/
|
|
2055
3332
|
saveSpending() {
|
|
2056
|
-
|
|
2057
|
-
const spendingPath =
|
|
3333
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
3334
|
+
const spendingPath = join4(this.configDir, "spending.json");
|
|
2058
3335
|
const data = {
|
|
2059
3336
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
2060
3337
|
amount: this.todaySpending,
|
|
2061
3338
|
updatedAt: Date.now()
|
|
2062
3339
|
};
|
|
2063
|
-
|
|
3340
|
+
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
2064
3341
|
}
|
|
2065
3342
|
loadWallet() {
|
|
2066
|
-
const walletPath =
|
|
2067
|
-
if (
|
|
3343
|
+
const walletPath = join4(this.configDir, "wallet.json");
|
|
3344
|
+
if (existsSync4(walletPath)) {
|
|
2068
3345
|
try {
|
|
2069
3346
|
const stats = statSync(walletPath);
|
|
2070
3347
|
const mode = stats.mode & 511;
|
|
@@ -2075,7 +3352,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2075
3352
|
}
|
|
2076
3353
|
} catch (err) {
|
|
2077
3354
|
}
|
|
2078
|
-
const content =
|
|
3355
|
+
const content = readFileSync4(walletPath, "utf-8");
|
|
2079
3356
|
return JSON.parse(content);
|
|
2080
3357
|
}
|
|
2081
3358
|
return null;
|
|
@@ -2084,15 +3361,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2084
3361
|
* Initialize a new wallet (called by CLI)
|
|
2085
3362
|
*/
|
|
2086
3363
|
static init(configDir, options) {
|
|
2087
|
-
|
|
3364
|
+
mkdirSync2(configDir, { recursive: true });
|
|
2088
3365
|
const wallet = Wallet.createRandom();
|
|
2089
3366
|
const walletData = {
|
|
2090
3367
|
address: wallet.address,
|
|
2091
3368
|
privateKey: wallet.privateKey,
|
|
2092
3369
|
createdAt: Date.now()
|
|
2093
3370
|
};
|
|
2094
|
-
const walletPath =
|
|
2095
|
-
|
|
3371
|
+
const walletPath = join4(configDir, "wallet.json");
|
|
3372
|
+
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
2096
3373
|
const config = {
|
|
2097
3374
|
chain: options.chain,
|
|
2098
3375
|
limits: {
|
|
@@ -2100,8 +3377,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2100
3377
|
maxPerDay: options.maxPerDay
|
|
2101
3378
|
}
|
|
2102
3379
|
};
|
|
2103
|
-
const configPath =
|
|
2104
|
-
|
|
3380
|
+
const configPath = join4(configDir, "config.json");
|
|
3381
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
2105
3382
|
return { address: wallet.address, configDir };
|
|
2106
3383
|
}
|
|
2107
3384
|
/**
|
|
@@ -2137,7 +3414,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2137
3414
|
if (!this.wallet) {
|
|
2138
3415
|
throw new Error("Client not initialized");
|
|
2139
3416
|
}
|
|
2140
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
3417
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
2141
3418
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
2142
3419
|
const results = {};
|
|
2143
3420
|
const tempoTokens = {
|
|
@@ -2208,12 +3485,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2208
3485
|
if (!this.wallet || !this.walletData) {
|
|
2209
3486
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
2210
3487
|
}
|
|
2211
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
3488
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
2212
3489
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
2213
3490
|
const { tempoModerato } = await import("viem/chains");
|
|
2214
3491
|
const { Actions } = await import("viem/tempo");
|
|
2215
3492
|
const privateKey = this.walletData.privateKey;
|
|
2216
|
-
const account =
|
|
3493
|
+
const account = privateKeyToAccount2(privateKey);
|
|
2217
3494
|
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
2218
3495
|
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
2219
3496
|
const initResponse = await fetch(url, {
|
|
@@ -2313,10 +3590,10 @@ import { ethers as ethers2 } from "ethers";
|
|
|
2313
3590
|
|
|
2314
3591
|
// src/wallet/createWallet.ts
|
|
2315
3592
|
import { ethers as ethers3 } from "ethers";
|
|
2316
|
-
import { writeFileSync as
|
|
2317
|
-
import { join as
|
|
3593
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
3594
|
+
import { join as join5, dirname } from "path";
|
|
2318
3595
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
2319
|
-
var DEFAULT_STORAGE_DIR =
|
|
3596
|
+
var DEFAULT_STORAGE_DIR = join5(process.env.HOME || "~", ".moltspay");
|
|
2320
3597
|
var DEFAULT_STORAGE_FILE = "wallet.json";
|
|
2321
3598
|
function encryptPrivateKey(privateKey, password) {
|
|
2322
3599
|
const salt = randomBytes(16);
|
|
@@ -2339,10 +3616,10 @@ function decryptPrivateKey(encrypted, password, iv, salt) {
|
|
|
2339
3616
|
return decrypted;
|
|
2340
3617
|
}
|
|
2341
3618
|
function createWallet(options = {}) {
|
|
2342
|
-
const storagePath = options.storagePath ||
|
|
2343
|
-
if (
|
|
3619
|
+
const storagePath = options.storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3620
|
+
if (existsSync5(storagePath) && !options.overwrite) {
|
|
2344
3621
|
try {
|
|
2345
|
-
const existing = JSON.parse(
|
|
3622
|
+
const existing = JSON.parse(readFileSync5(storagePath, "utf8"));
|
|
2346
3623
|
return {
|
|
2347
3624
|
success: true,
|
|
2348
3625
|
address: existing.address,
|
|
@@ -2374,10 +3651,10 @@ function createWallet(options = {}) {
|
|
|
2374
3651
|
walletData.privateKey = wallet.privateKey;
|
|
2375
3652
|
}
|
|
2376
3653
|
const dir = dirname(storagePath);
|
|
2377
|
-
if (!
|
|
2378
|
-
|
|
3654
|
+
if (!existsSync5(dir)) {
|
|
3655
|
+
mkdirSync3(dir, { recursive: true });
|
|
2379
3656
|
}
|
|
2380
|
-
|
|
3657
|
+
writeFileSync3(storagePath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
2381
3658
|
return {
|
|
2382
3659
|
success: true,
|
|
2383
3660
|
address: wallet.address,
|
|
@@ -2392,12 +3669,12 @@ function createWallet(options = {}) {
|
|
|
2392
3669
|
}
|
|
2393
3670
|
}
|
|
2394
3671
|
function loadWallet(options = {}) {
|
|
2395
|
-
const storagePath = options.storagePath ||
|
|
2396
|
-
if (!
|
|
3672
|
+
const storagePath = options.storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3673
|
+
if (!existsSync5(storagePath)) {
|
|
2397
3674
|
return { success: false, error: "Wallet not found. Run createWallet() first." };
|
|
2398
3675
|
}
|
|
2399
3676
|
try {
|
|
2400
|
-
const data = JSON.parse(
|
|
3677
|
+
const data = JSON.parse(readFileSync5(storagePath, "utf8"));
|
|
2401
3678
|
if (data.encrypted) {
|
|
2402
3679
|
if (!options.password) {
|
|
2403
3680
|
return { success: false, error: "Wallet is encrypted. Password required." };
|
|
@@ -2412,25 +3689,25 @@ function loadWallet(options = {}) {
|
|
|
2412
3689
|
}
|
|
2413
3690
|
}
|
|
2414
3691
|
function getWalletAddress(storagePath) {
|
|
2415
|
-
const path4 = storagePath ||
|
|
2416
|
-
if (!
|
|
3692
|
+
const path4 = storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3693
|
+
if (!existsSync5(path4)) {
|
|
2417
3694
|
return null;
|
|
2418
3695
|
}
|
|
2419
3696
|
try {
|
|
2420
|
-
const data = JSON.parse(
|
|
3697
|
+
const data = JSON.parse(readFileSync5(path4, "utf8"));
|
|
2421
3698
|
return data.address;
|
|
2422
3699
|
} catch {
|
|
2423
3700
|
return null;
|
|
2424
3701
|
}
|
|
2425
3702
|
}
|
|
2426
3703
|
function walletExists(storagePath) {
|
|
2427
|
-
const path4 = storagePath ||
|
|
2428
|
-
return
|
|
3704
|
+
const path4 = storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3705
|
+
return existsSync5(path4);
|
|
2429
3706
|
}
|
|
2430
3707
|
|
|
2431
3708
|
// src/verify/index.ts
|
|
2432
3709
|
import { ethers as ethers4 } from "ethers";
|
|
2433
|
-
var
|
|
3710
|
+
var TRANSFER_EVENT_TOPIC3 = ethers4.id("Transfer(address,address,uint256)");
|
|
2434
3711
|
async function verifyPayment(params) {
|
|
2435
3712
|
const { txHash, expectedAmount, expectedTo, expectedToken } = params;
|
|
2436
3713
|
let chain;
|
|
@@ -2471,7 +3748,7 @@ async function verifyPayment(params) {
|
|
|
2471
3748
|
if (!detectedToken) {
|
|
2472
3749
|
continue;
|
|
2473
3750
|
}
|
|
2474
|
-
if (log.topics.length < 3 || log.topics[0] !==
|
|
3751
|
+
if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC3) {
|
|
2475
3752
|
continue;
|
|
2476
3753
|
}
|
|
2477
3754
|
const from = "0x" + log.topics[1].slice(-40);
|