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/server/index.js
CHANGED
|
@@ -370,6 +370,63 @@ var CHAINS = {
|
|
|
370
370
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
371
371
|
avgBlockTime: 0.5
|
|
372
372
|
// ~500ms finality
|
|
373
|
+
},
|
|
374
|
+
// ============ BNB Chain Testnet ============
|
|
375
|
+
bnb_testnet: {
|
|
376
|
+
name: "BNB Testnet",
|
|
377
|
+
chainId: 97,
|
|
378
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
379
|
+
tokens: {
|
|
380
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
381
|
+
// Using official Binance-Peg testnet tokens
|
|
382
|
+
USDC: {
|
|
383
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
384
|
+
// Testnet USDC
|
|
385
|
+
decimals: 18,
|
|
386
|
+
symbol: "USDC",
|
|
387
|
+
eip712Name: "USD Coin"
|
|
388
|
+
},
|
|
389
|
+
USDT: {
|
|
390
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
391
|
+
// Testnet USDT
|
|
392
|
+
decimals: 18,
|
|
393
|
+
symbol: "USDT",
|
|
394
|
+
eip712Name: "Tether USD"
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
398
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
399
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
400
|
+
avgBlockTime: 3,
|
|
401
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
402
|
+
requiresApproval: true
|
|
403
|
+
},
|
|
404
|
+
// ============ BNB Chain Mainnet ============
|
|
405
|
+
bnb: {
|
|
406
|
+
name: "BNB Smart Chain",
|
|
407
|
+
chainId: 56,
|
|
408
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
409
|
+
tokens: {
|
|
410
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
411
|
+
USDC: {
|
|
412
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
413
|
+
decimals: 18,
|
|
414
|
+
symbol: "USDC",
|
|
415
|
+
eip712Name: "USD Coin"
|
|
416
|
+
},
|
|
417
|
+
USDT: {
|
|
418
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
419
|
+
decimals: 18,
|
|
420
|
+
symbol: "USDT",
|
|
421
|
+
eip712Name: "Tether USD"
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
425
|
+
explorer: "https://bscscan.com/address/",
|
|
426
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
427
|
+
avgBlockTime: 3,
|
|
428
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
429
|
+
requiresApproval: true
|
|
373
430
|
}
|
|
374
431
|
};
|
|
375
432
|
|
|
@@ -493,7 +550,528 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
493
550
|
}
|
|
494
551
|
};
|
|
495
552
|
|
|
553
|
+
// src/facilitators/bnb.ts
|
|
554
|
+
var import_accounts = require("viem/accounts");
|
|
555
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
556
|
+
var EIP712_DOMAIN = {
|
|
557
|
+
name: "MoltsPay",
|
|
558
|
+
version: "1"
|
|
559
|
+
};
|
|
560
|
+
var INTENT_TYPES = {
|
|
561
|
+
PaymentIntent: [
|
|
562
|
+
{ name: "from", type: "address" },
|
|
563
|
+
{ name: "to", type: "address" },
|
|
564
|
+
{ name: "amount", type: "uint256" },
|
|
565
|
+
{ name: "token", type: "address" },
|
|
566
|
+
{ name: "service", type: "string" },
|
|
567
|
+
{ name: "nonce", type: "uint256" },
|
|
568
|
+
{ name: "deadline", type: "uint256" }
|
|
569
|
+
]
|
|
570
|
+
};
|
|
571
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
572
|
+
name = "bnb";
|
|
573
|
+
displayName = "BNB Smart Chain";
|
|
574
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
575
|
+
// Mainnet + Testnet
|
|
576
|
+
serverPrivateKey;
|
|
577
|
+
spenderAddress = null;
|
|
578
|
+
chainConfigs;
|
|
579
|
+
constructor(serverPrivateKey) {
|
|
580
|
+
super();
|
|
581
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
582
|
+
if (this.serverPrivateKey) {
|
|
583
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
584
|
+
const account = (0, import_accounts.privateKeyToAccount)(key);
|
|
585
|
+
this.spenderAddress = account.address;
|
|
586
|
+
}
|
|
587
|
+
this.chainConfigs = {
|
|
588
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
589
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
async healthCheck() {
|
|
593
|
+
const start = Date.now();
|
|
594
|
+
try {
|
|
595
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
596
|
+
method: "POST",
|
|
597
|
+
headers: { "Content-Type": "application/json" },
|
|
598
|
+
body: JSON.stringify({
|
|
599
|
+
jsonrpc: "2.0",
|
|
600
|
+
method: "eth_chainId",
|
|
601
|
+
params: [],
|
|
602
|
+
id: 1
|
|
603
|
+
})
|
|
604
|
+
});
|
|
605
|
+
const data = await response.json();
|
|
606
|
+
const chainId = parseInt(data.result, 16);
|
|
607
|
+
if (chainId !== 56) {
|
|
608
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
609
|
+
}
|
|
610
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
611
|
+
} catch (error) {
|
|
612
|
+
return { healthy: false, error: String(error) };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Verify a payment intent signature (before service execution)
|
|
617
|
+
*
|
|
618
|
+
* This verifies:
|
|
619
|
+
* 1. Signature is valid for the intent
|
|
620
|
+
* 2. Client has approved server wallet
|
|
621
|
+
* 3. Client has sufficient balance
|
|
622
|
+
* 4. Intent hasn't expired
|
|
623
|
+
*/
|
|
624
|
+
async verify(paymentPayload, requirements) {
|
|
625
|
+
try {
|
|
626
|
+
const bnbPayload = paymentPayload.payload;
|
|
627
|
+
if (!bnbPayload?.intent) {
|
|
628
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
629
|
+
}
|
|
630
|
+
const { intent, chainId } = bnbPayload;
|
|
631
|
+
const config = this.chainConfigs[chainId];
|
|
632
|
+
if (!config) {
|
|
633
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
634
|
+
}
|
|
635
|
+
if (intent.deadline < Date.now()) {
|
|
636
|
+
return { valid: false, error: "Intent expired" };
|
|
637
|
+
}
|
|
638
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
639
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
640
|
+
return { valid: false, error: "Invalid signature" };
|
|
641
|
+
}
|
|
642
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
643
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
644
|
+
}
|
|
645
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
646
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
647
|
+
}
|
|
648
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
649
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
650
|
+
}
|
|
651
|
+
const serverAddress = await this.getServerAddress();
|
|
652
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
653
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
654
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
655
|
+
}
|
|
656
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
657
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
658
|
+
return { valid: false, error: "Insufficient balance" };
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
valid: true,
|
|
662
|
+
details: {
|
|
663
|
+
from: intent.from,
|
|
664
|
+
to: intent.to,
|
|
665
|
+
amount: intent.amount,
|
|
666
|
+
token: intent.token,
|
|
667
|
+
service: intent.service,
|
|
668
|
+
nonce: intent.nonce,
|
|
669
|
+
deadline: intent.deadline
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Settle a payment by executing transferFrom
|
|
678
|
+
*
|
|
679
|
+
* This is called AFTER the service has been successfully delivered.
|
|
680
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
681
|
+
*/
|
|
682
|
+
async settle(paymentPayload, requirements) {
|
|
683
|
+
if (!this.serverPrivateKey) {
|
|
684
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
688
|
+
if (!verifyResult.valid) {
|
|
689
|
+
return { success: false, error: verifyResult.error };
|
|
690
|
+
}
|
|
691
|
+
const bnbPayload = paymentPayload.payload;
|
|
692
|
+
const { intent, chainId } = bnbPayload;
|
|
693
|
+
const config = this.chainConfigs[chainId];
|
|
694
|
+
const txHash = await this.executeTransferFrom(
|
|
695
|
+
intent.from,
|
|
696
|
+
intent.to,
|
|
697
|
+
intent.amount,
|
|
698
|
+
intent.token,
|
|
699
|
+
config.rpc
|
|
700
|
+
);
|
|
701
|
+
return {
|
|
702
|
+
success: true,
|
|
703
|
+
transaction: txHash,
|
|
704
|
+
status: "settled"
|
|
705
|
+
};
|
|
706
|
+
} catch (error) {
|
|
707
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Check if client has approved the server wallet
|
|
712
|
+
*/
|
|
713
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
714
|
+
const config = this.chainConfigs[chainId];
|
|
715
|
+
if (!config) {
|
|
716
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
717
|
+
}
|
|
718
|
+
const serverAddress = await this.getServerAddress();
|
|
719
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
720
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
721
|
+
return {
|
|
722
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
723
|
+
allowance
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Verify a completed transaction (for checking past payments)
|
|
728
|
+
*/
|
|
729
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
730
|
+
const config = this.chainConfigs[chainId];
|
|
731
|
+
if (!config) {
|
|
732
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
736
|
+
if (!receipt) {
|
|
737
|
+
return { valid: false, error: "Transaction not found" };
|
|
738
|
+
}
|
|
739
|
+
if (receipt.status !== "0x1") {
|
|
740
|
+
return { valid: false, error: "Transaction failed" };
|
|
741
|
+
}
|
|
742
|
+
const transferLog = receipt.logs.find(
|
|
743
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
744
|
+
);
|
|
745
|
+
if (!transferLog) {
|
|
746
|
+
return { valid: false, error: "No Transfer event found" };
|
|
747
|
+
}
|
|
748
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
749
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
750
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
751
|
+
}
|
|
752
|
+
const amount = BigInt(transferLog.data);
|
|
753
|
+
if (amount < BigInt(expected.amount)) {
|
|
754
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
valid: true,
|
|
758
|
+
details: {
|
|
759
|
+
txHash,
|
|
760
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
761
|
+
to: toAddress,
|
|
762
|
+
amount: amount.toString(),
|
|
763
|
+
token: transferLog.address
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
} catch (error) {
|
|
767
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// ==================== Private Methods ====================
|
|
771
|
+
/**
|
|
772
|
+
* Get the server's spender address (public, for 402 responses)
|
|
773
|
+
* Returns cached value computed at construction time.
|
|
774
|
+
*/
|
|
775
|
+
getSpenderAddress() {
|
|
776
|
+
return this.spenderAddress;
|
|
777
|
+
}
|
|
778
|
+
async getServerAddress() {
|
|
779
|
+
const { ethers } = await import("ethers");
|
|
780
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey);
|
|
781
|
+
return wallet.address;
|
|
782
|
+
}
|
|
783
|
+
async recoverIntentSigner(intent, chainId) {
|
|
784
|
+
const { ethers } = await import("ethers");
|
|
785
|
+
const domain = {
|
|
786
|
+
...EIP712_DOMAIN,
|
|
787
|
+
chainId
|
|
788
|
+
};
|
|
789
|
+
const message = {
|
|
790
|
+
from: intent.from,
|
|
791
|
+
to: intent.to,
|
|
792
|
+
amount: intent.amount,
|
|
793
|
+
token: intent.token,
|
|
794
|
+
service: intent.service,
|
|
795
|
+
nonce: intent.nonce,
|
|
796
|
+
deadline: intent.deadline
|
|
797
|
+
};
|
|
798
|
+
const recoveredAddress = ethers.verifyTypedData(
|
|
799
|
+
domain,
|
|
800
|
+
INTENT_TYPES,
|
|
801
|
+
message,
|
|
802
|
+
intent.signature
|
|
803
|
+
);
|
|
804
|
+
return recoveredAddress;
|
|
805
|
+
}
|
|
806
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
807
|
+
const selector = "0xdd62ed3e";
|
|
808
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
809
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
810
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
811
|
+
const response = await fetch(rpcUrl, {
|
|
812
|
+
method: "POST",
|
|
813
|
+
headers: { "Content-Type": "application/json" },
|
|
814
|
+
body: JSON.stringify({
|
|
815
|
+
jsonrpc: "2.0",
|
|
816
|
+
method: "eth_call",
|
|
817
|
+
params: [{ to: token, data }, "latest"],
|
|
818
|
+
id: 1
|
|
819
|
+
})
|
|
820
|
+
});
|
|
821
|
+
const result = await response.json();
|
|
822
|
+
return result.result || "0x0";
|
|
823
|
+
}
|
|
824
|
+
async getBalance(account, token, rpcUrl) {
|
|
825
|
+
const selector = "0x70a08231";
|
|
826
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
827
|
+
const data = selector + accountPadded;
|
|
828
|
+
const response = await fetch(rpcUrl, {
|
|
829
|
+
method: "POST",
|
|
830
|
+
headers: { "Content-Type": "application/json" },
|
|
831
|
+
body: JSON.stringify({
|
|
832
|
+
jsonrpc: "2.0",
|
|
833
|
+
method: "eth_call",
|
|
834
|
+
params: [{ to: token, data }, "latest"],
|
|
835
|
+
id: 1
|
|
836
|
+
})
|
|
837
|
+
});
|
|
838
|
+
const result = await response.json();
|
|
839
|
+
return result.result || "0x0";
|
|
840
|
+
}
|
|
841
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
842
|
+
const { ethers } = await import("ethers");
|
|
843
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
844
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
|
|
845
|
+
const tokenContract = new ethers.Contract(token, [
|
|
846
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
847
|
+
], wallet);
|
|
848
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
849
|
+
const receipt = await tx.wait();
|
|
850
|
+
return receipt.hash;
|
|
851
|
+
}
|
|
852
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
853
|
+
const response = await fetch(rpcUrl, {
|
|
854
|
+
method: "POST",
|
|
855
|
+
headers: { "Content-Type": "application/json" },
|
|
856
|
+
body: JSON.stringify({
|
|
857
|
+
jsonrpc: "2.0",
|
|
858
|
+
method: "eth_getTransactionReceipt",
|
|
859
|
+
params: [txHash],
|
|
860
|
+
id: 1
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
const data = await response.json();
|
|
864
|
+
return data.result;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// src/facilitators/solana.ts
|
|
869
|
+
var import_web32 = require("@solana/web3.js");
|
|
870
|
+
var import_spl_token = require("@solana/spl-token");
|
|
871
|
+
|
|
872
|
+
// src/chains/solana.ts
|
|
873
|
+
var import_web3 = require("@solana/web3.js");
|
|
874
|
+
var SOLANA_CHAINS = {
|
|
875
|
+
solana: {
|
|
876
|
+
name: "Solana Mainnet",
|
|
877
|
+
cluster: "mainnet-beta",
|
|
878
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
879
|
+
explorer: "https://solscan.io/account/",
|
|
880
|
+
explorerTx: "https://solscan.io/tx/",
|
|
881
|
+
tokens: {
|
|
882
|
+
USDC: {
|
|
883
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
884
|
+
// Circle official USDC
|
|
885
|
+
decimals: 6
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
solana_devnet: {
|
|
890
|
+
name: "Solana Devnet",
|
|
891
|
+
cluster: "devnet",
|
|
892
|
+
rpc: "https://api.devnet.solana.com",
|
|
893
|
+
explorer: "https://solscan.io/account/",
|
|
894
|
+
explorerTx: "https://solscan.io/tx/",
|
|
895
|
+
tokens: {
|
|
896
|
+
USDC: {
|
|
897
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
898
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
899
|
+
decimals: 6
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/facilitators/solana.ts
|
|
906
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
907
|
+
name = "solana";
|
|
908
|
+
displayName = "Solana Direct";
|
|
909
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
910
|
+
connections = /* @__PURE__ */ new Map();
|
|
911
|
+
feePayerKeypair;
|
|
912
|
+
constructor(config) {
|
|
913
|
+
super();
|
|
914
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
915
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
916
|
+
this.connections.set(
|
|
917
|
+
chain,
|
|
918
|
+
new import_web32.Connection(config2.rpc, "confirmed")
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (this.feePayerKeypair) {
|
|
922
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Get fee payer public key (for gasless transactions)
|
|
927
|
+
*/
|
|
928
|
+
getFeePayerPubkey() {
|
|
929
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
930
|
+
}
|
|
931
|
+
getConnection(chain) {
|
|
932
|
+
const conn = this.connections.get(chain);
|
|
933
|
+
if (!conn) {
|
|
934
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
935
|
+
}
|
|
936
|
+
return conn;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Convert our chain name to network identifier
|
|
940
|
+
*/
|
|
941
|
+
static chainToNetwork(chain) {
|
|
942
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Convert network identifier to chain name
|
|
946
|
+
*/
|
|
947
|
+
static networkToChain(network) {
|
|
948
|
+
if (network === "solana:mainnet") return "solana";
|
|
949
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
async healthCheck() {
|
|
953
|
+
const start = Date.now();
|
|
954
|
+
try {
|
|
955
|
+
const conn = this.getConnection("solana_devnet");
|
|
956
|
+
await conn.getSlot();
|
|
957
|
+
return {
|
|
958
|
+
healthy: true,
|
|
959
|
+
latencyMs: Date.now() - start
|
|
960
|
+
};
|
|
961
|
+
} catch (error) {
|
|
962
|
+
return {
|
|
963
|
+
healthy: false,
|
|
964
|
+
error: error.message
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Verify a Solana payment
|
|
970
|
+
*
|
|
971
|
+
* Checks:
|
|
972
|
+
* 1. Transaction is valid and properly signed
|
|
973
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
974
|
+
*/
|
|
975
|
+
async verify(paymentPayload, requirements) {
|
|
976
|
+
try {
|
|
977
|
+
const solanaPayload = paymentPayload.payload;
|
|
978
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
979
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
980
|
+
}
|
|
981
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
982
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
983
|
+
if (!chainConfig) {
|
|
984
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
985
|
+
}
|
|
986
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
987
|
+
let tx;
|
|
988
|
+
try {
|
|
989
|
+
tx = import_web32.Transaction.from(txBuffer);
|
|
990
|
+
} catch {
|
|
991
|
+
tx = import_web32.VersionedTransaction.deserialize(txBuffer);
|
|
992
|
+
}
|
|
993
|
+
if (tx instanceof import_web32.Transaction) {
|
|
994
|
+
const hasAnySignature = tx.signatures.some(
|
|
995
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
996
|
+
);
|
|
997
|
+
if (!hasAnySignature) {
|
|
998
|
+
return { valid: false, error: "Transaction not signed" };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1002
|
+
const expectedRecipient = new import_web32.PublicKey(requirements.payTo);
|
|
1003
|
+
return {
|
|
1004
|
+
valid: true,
|
|
1005
|
+
details: {
|
|
1006
|
+
chain,
|
|
1007
|
+
sender: solanaPayload.sender,
|
|
1008
|
+
recipient: requirements.payTo,
|
|
1009
|
+
amount: requirements.amount
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
return { valid: false, error: error.message };
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Settle a Solana payment
|
|
1018
|
+
*
|
|
1019
|
+
* Submits the signed transaction to the network.
|
|
1020
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
1021
|
+
*/
|
|
1022
|
+
async settle(paymentPayload, requirements) {
|
|
1023
|
+
try {
|
|
1024
|
+
const solanaPayload = paymentPayload.payload;
|
|
1025
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1026
|
+
return { success: false, error: "Missing signed transaction" };
|
|
1027
|
+
}
|
|
1028
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1029
|
+
const connection = this.getConnection(chain);
|
|
1030
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1031
|
+
let txToSend;
|
|
1032
|
+
try {
|
|
1033
|
+
const tx = import_web32.Transaction.from(txBuffer);
|
|
1034
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
1035
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
1036
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
1037
|
+
if (txFeePayer === feePayerPubkey) {
|
|
1038
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
1039
|
+
tx.partialSign(this.feePayerKeypair);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
txToSend = tx.serialize();
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
txToSend = txBuffer;
|
|
1045
|
+
}
|
|
1046
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
1047
|
+
skipPreflight: false,
|
|
1048
|
+
preflightCommitment: "confirmed"
|
|
1049
|
+
});
|
|
1050
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
1051
|
+
if (confirmation.value.err) {
|
|
1052
|
+
return {
|
|
1053
|
+
success: false,
|
|
1054
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
1055
|
+
transaction: signature
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
success: true,
|
|
1060
|
+
transaction: signature,
|
|
1061
|
+
status: "confirmed"
|
|
1062
|
+
};
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
return { success: false, error: error.message };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
supportsNetwork(network) {
|
|
1068
|
+
return this.supportedNetworks.includes(network);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
496
1072
|
// src/facilitators/registry.ts
|
|
1073
|
+
var import_web33 = require("@solana/web3.js");
|
|
1074
|
+
var import_bs58 = __toESM(require("bs58"));
|
|
497
1075
|
var FacilitatorRegistry = class {
|
|
498
1076
|
factories = /* @__PURE__ */ new Map();
|
|
499
1077
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -502,7 +1080,20 @@ var FacilitatorRegistry = class {
|
|
|
502
1080
|
constructor(selection) {
|
|
503
1081
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
504
1082
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
505
|
-
this.
|
|
1083
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
1084
|
+
this.registerFactory("solana", (config) => {
|
|
1085
|
+
let feePayerKeypair;
|
|
1086
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
1087
|
+
if (feePayerKey) {
|
|
1088
|
+
try {
|
|
1089
|
+
feePayerKeypair = import_web33.Keypair.fromSecretKey(import_bs58.default.decode(feePayerKey));
|
|
1090
|
+
} catch (e) {
|
|
1091
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
1095
|
+
});
|
|
1096
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
506
1097
|
}
|
|
507
1098
|
/**
|
|
508
1099
|
* Register a new facilitator factory
|
|
@@ -746,14 +1337,40 @@ var TOKEN_ADDRESSES = {
|
|
|
746
1337
|
// pathUSD
|
|
747
1338
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
748
1339
|
// alphaUSD
|
|
1340
|
+
},
|
|
1341
|
+
// BNB Smart Chain mainnet
|
|
1342
|
+
"eip155:56": {
|
|
1343
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
1344
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
1345
|
+
},
|
|
1346
|
+
// BNB Smart Chain testnet
|
|
1347
|
+
"eip155:97": {
|
|
1348
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
1349
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
1350
|
+
},
|
|
1351
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
1352
|
+
"solana:mainnet": {
|
|
1353
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
1354
|
+
// Circle USDC
|
|
1355
|
+
},
|
|
1356
|
+
"solana:devnet": {
|
|
1357
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
1358
|
+
// Devnet USDC
|
|
749
1359
|
}
|
|
750
1360
|
};
|
|
751
1361
|
var CHAIN_TO_NETWORK = {
|
|
752
1362
|
"base": "eip155:8453",
|
|
753
1363
|
"base_sepolia": "eip155:84532",
|
|
754
1364
|
"polygon": "eip155:137",
|
|
755
|
-
"tempo_moderato": "eip155:42431"
|
|
1365
|
+
"tempo_moderato": "eip155:42431",
|
|
1366
|
+
"bnb": "eip155:56",
|
|
1367
|
+
"bnb_testnet": "eip155:97",
|
|
1368
|
+
"solana": "solana:mainnet",
|
|
1369
|
+
"solana_devnet": "solana:devnet"
|
|
756
1370
|
};
|
|
1371
|
+
function isSolanaNetwork(network) {
|
|
1372
|
+
return network.startsWith("solana:");
|
|
1373
|
+
}
|
|
757
1374
|
var TOKEN_DOMAINS = {
|
|
758
1375
|
// Base mainnet
|
|
759
1376
|
"eip155:8453": {
|
|
@@ -775,6 +1392,16 @@ var TOKEN_DOMAINS = {
|
|
|
775
1392
|
"eip155:42431": {
|
|
776
1393
|
USDC: { name: "pathUSD", version: "1" },
|
|
777
1394
|
USDT: { name: "alphaUSD", version: "1" }
|
|
1395
|
+
},
|
|
1396
|
+
// BNB Smart Chain mainnet
|
|
1397
|
+
"eip155:56": {
|
|
1398
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1399
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1400
|
+
},
|
|
1401
|
+
// BNB Smart Chain testnet
|
|
1402
|
+
"eip155:97": {
|
|
1403
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1404
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
778
1405
|
}
|
|
779
1406
|
};
|
|
780
1407
|
function getTokenDomain(network, token) {
|
|
@@ -832,7 +1459,7 @@ var MoltsPayServer = class {
|
|
|
832
1459
|
};
|
|
833
1460
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
834
1461
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
835
|
-
const defaultFallback = ["tempo"];
|
|
1462
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
836
1463
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
837
1464
|
const facilitatorConfig = options.facilitators || {
|
|
838
1465
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -875,12 +1502,20 @@ var MoltsPayServer = class {
|
|
|
875
1502
|
*/
|
|
876
1503
|
getProviderChains() {
|
|
877
1504
|
const provider = this.manifest.provider;
|
|
1505
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
1506
|
+
if (explicitWallet) return explicitWallet;
|
|
1507
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
1508
|
+
return provider.solana_wallet;
|
|
1509
|
+
}
|
|
1510
|
+
return provider.wallet;
|
|
1511
|
+
};
|
|
878
1512
|
if (provider.chains && provider.chains.length > 0) {
|
|
879
1513
|
return provider.chains.map((c) => {
|
|
880
1514
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
1515
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
881
1516
|
return {
|
|
882
1517
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
883
|
-
wallet: (
|
|
1518
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
884
1519
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
885
1520
|
};
|
|
886
1521
|
});
|
|
@@ -889,7 +1524,7 @@ var MoltsPayServer = class {
|
|
|
889
1524
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
890
1525
|
return [{
|
|
891
1526
|
network,
|
|
892
|
-
wallet:
|
|
1527
|
+
wallet: getWalletForChain(chain),
|
|
893
1528
|
tokens: ["USDC"]
|
|
894
1529
|
}];
|
|
895
1530
|
}
|
|
@@ -960,7 +1595,8 @@ var MoltsPayServer = class {
|
|
|
960
1595
|
}
|
|
961
1596
|
const body = await this.readBody(req);
|
|
962
1597
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
963
|
-
|
|
1598
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1599
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
964
1600
|
}
|
|
965
1601
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
966
1602
|
const skill = this.skills.get(servicePath);
|
|
@@ -997,7 +1633,9 @@ var MoltsPayServer = class {
|
|
|
997
1633
|
name: this.manifest.provider.name,
|
|
998
1634
|
description: this.manifest.provider.description,
|
|
999
1635
|
wallet: this.manifest.provider.wallet,
|
|
1000
|
-
chain: this.manifest.provider.chain || "base"
|
|
1636
|
+
chain: this.manifest.provider.chain || "base",
|
|
1637
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
1638
|
+
chains: this.manifest.provider.chains
|
|
1001
1639
|
},
|
|
1002
1640
|
services,
|
|
1003
1641
|
endpoints: {
|
|
@@ -1110,6 +1748,21 @@ var MoltsPayServer = class {
|
|
|
1110
1748
|
});
|
|
1111
1749
|
}
|
|
1112
1750
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
1751
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
1752
|
+
let settlement = null;
|
|
1753
|
+
if (isSolana) {
|
|
1754
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
1755
|
+
try {
|
|
1756
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1757
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
1760
|
+
return this.sendJson(res, 402, {
|
|
1761
|
+
error: "Payment settlement failed",
|
|
1762
|
+
message: err.message
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1113
1766
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1114
1767
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1115
1768
|
let result;
|
|
@@ -1124,16 +1777,19 @@ var MoltsPayServer = class {
|
|
|
1124
1777
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1125
1778
|
return this.sendJson(res, 500, {
|
|
1126
1779
|
error: "Service execution failed",
|
|
1127
|
-
message: err.message
|
|
1780
|
+
message: err.message,
|
|
1781
|
+
paymentSettled: isSolana ? true : false,
|
|
1782
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1128
1783
|
});
|
|
1129
1784
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1785
|
+
if (!isSolana) {
|
|
1786
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
1787
|
+
try {
|
|
1788
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1789
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
1792
|
+
}
|
|
1137
1793
|
}
|
|
1138
1794
|
const responseHeaders = {};
|
|
1139
1795
|
if (settlement?.success) {
|
|
@@ -1409,7 +2065,7 @@ var MoltsPayServer = class {
|
|
|
1409
2065
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
1410
2066
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1411
2067
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
1412
|
-
|
|
2068
|
+
const requirements = {
|
|
1413
2069
|
scheme: "exact",
|
|
1414
2070
|
network: selectedNetwork,
|
|
1415
2071
|
asset: tokenAddress,
|
|
@@ -1418,6 +2074,27 @@ var MoltsPayServer = class {
|
|
|
1418
2074
|
maxTimeoutSeconds: 300,
|
|
1419
2075
|
extra: tokenDomain
|
|
1420
2076
|
};
|
|
2077
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
2078
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
2079
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
2080
|
+
if (feePayerPubkey) {
|
|
2081
|
+
requirements.extra = {
|
|
2082
|
+
...requirements.extra || {},
|
|
2083
|
+
solanaFeePayer: feePayerPubkey
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
2088
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2089
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2090
|
+
if (spenderAddress) {
|
|
2091
|
+
requirements.extra = {
|
|
2092
|
+
...requirements.extra || {},
|
|
2093
|
+
bnbSpender: spenderAddress
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return requirements;
|
|
1421
2098
|
}
|
|
1422
2099
|
/**
|
|
1423
2100
|
* Detect which token is being used in the payment
|
|
@@ -1483,31 +2160,42 @@ var MoltsPayServer = class {
|
|
|
1483
2160
|
/**
|
|
1484
2161
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1485
2162
|
*
|
|
1486
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
2163
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1487
2164
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1488
2165
|
*
|
|
1489
2166
|
* Request body:
|
|
1490
2167
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1491
2168
|
*
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
2169
|
+
* For x402 (base, polygon, base_sepolia):
|
|
2170
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
2171
|
+
* With X-Payment header: verifies payment via CDP
|
|
2172
|
+
*
|
|
2173
|
+
* For MPP (tempo_moderato):
|
|
2174
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
2175
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1494
2176
|
*/
|
|
1495
|
-
async handleProxy(body, paymentHeader, res) {
|
|
2177
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1496
2178
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1497
2179
|
if (!wallet || !amount) {
|
|
1498
2180
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1499
2181
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
2182
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2183
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
2184
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2185
|
+
}
|
|
2186
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
2187
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
2188
|
+
const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
2189
|
+
if (isSolanaChain && !isValidSolanaAddress) {
|
|
2190
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
2191
|
+
}
|
|
2192
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
2193
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1502
2194
|
}
|
|
1503
2195
|
const amountNum = parseFloat(amount);
|
|
1504
2196
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1505
2197
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1506
2198
|
}
|
|
1507
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1508
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
1509
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
1510
|
-
}
|
|
1511
2199
|
const proxyConfig = {
|
|
1512
2200
|
id: serviceId || "proxy",
|
|
1513
2201
|
name: description || "Proxy Payment",
|
|
@@ -1519,6 +2207,9 @@ var MoltsPayServer = class {
|
|
|
1519
2207
|
input: {},
|
|
1520
2208
|
output: {}
|
|
1521
2209
|
};
|
|
2210
|
+
if (chain === "tempo_moderato") {
|
|
2211
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
2212
|
+
}
|
|
1522
2213
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1523
2214
|
if (!paymentHeader) {
|
|
1524
2215
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1554,7 +2245,6 @@ var MoltsPayServer = class {
|
|
|
1554
2245
|
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
1555
2246
|
const { execute, service, params } = body;
|
|
1556
2247
|
if (execute && service) {
|
|
1557
|
-
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
1558
2248
|
const skill = this.skills.get(service);
|
|
1559
2249
|
if (!skill) {
|
|
1560
2250
|
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
@@ -1564,6 +2254,32 @@ var MoltsPayServer = class {
|
|
|
1564
2254
|
error: `Service not found: ${service}`
|
|
1565
2255
|
});
|
|
1566
2256
|
}
|
|
2257
|
+
const isSolana = isSolanaNetwork(network);
|
|
2258
|
+
let settlement2 = null;
|
|
2259
|
+
if (isSolana) {
|
|
2260
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
2261
|
+
try {
|
|
2262
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2263
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2264
|
+
if (!settlement2.success) {
|
|
2265
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
2266
|
+
return this.sendJson(res, 402, {
|
|
2267
|
+
success: false,
|
|
2268
|
+
paymentSettled: false,
|
|
2269
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
2274
|
+
return this.sendJson(res, 402, {
|
|
2275
|
+
success: false,
|
|
2276
|
+
paymentSettled: false,
|
|
2277
|
+
error: `Payment settlement failed: ${err.message}`
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
} else {
|
|
2281
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
2282
|
+
}
|
|
1567
2283
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1568
2284
|
let result;
|
|
1569
2285
|
try {
|
|
@@ -1573,34 +2289,36 @@ var MoltsPayServer = class {
|
|
|
1573
2289
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1574
2290
|
)
|
|
1575
2291
|
]);
|
|
1576
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded
|
|
2292
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
1577
2293
|
} catch (err) {
|
|
1578
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
2294
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
1579
2295
|
return this.sendJson(res, 500, {
|
|
1580
2296
|
success: false,
|
|
1581
|
-
paymentSettled: false,
|
|
1582
|
-
error: `Service execution failed: ${err.message}
|
|
2297
|
+
paymentSettled: isSolana ? true : false,
|
|
2298
|
+
error: `Service execution failed: ${err.message}`,
|
|
2299
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1583
2300
|
});
|
|
1584
2301
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
2302
|
+
if (!isSolana) {
|
|
2303
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
2304
|
+
try {
|
|
2305
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2306
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2307
|
+
} catch (err) {
|
|
2308
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2309
|
+
return this.sendJson(res, 200, {
|
|
2310
|
+
success: true,
|
|
2311
|
+
verified: true,
|
|
2312
|
+
settled: false,
|
|
2313
|
+
settlementError: err.message,
|
|
2314
|
+
from: payment.payload?.authorization?.from,
|
|
2315
|
+
paidTo: wallet,
|
|
2316
|
+
amount: amountNum,
|
|
2317
|
+
currency: currency || "USDC",
|
|
2318
|
+
memo,
|
|
2319
|
+
result
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
1604
2322
|
}
|
|
1605
2323
|
return this.sendJson(res, 200, {
|
|
1606
2324
|
success: true,
|
|
@@ -1608,7 +2326,6 @@ var MoltsPayServer = class {
|
|
|
1608
2326
|
settled: settlement2?.success || false,
|
|
1609
2327
|
txHash: settlement2?.transaction,
|
|
1610
2328
|
from: payment.payload?.authorization?.from,
|
|
1611
|
-
// Buyer's wallet address
|
|
1612
2329
|
paidTo: wallet,
|
|
1613
2330
|
amount: amountNum,
|
|
1614
2331
|
currency: currency || "USDC",
|
|
@@ -1643,6 +2360,131 @@ var MoltsPayServer = class {
|
|
|
1643
2360
|
memo
|
|
1644
2361
|
});
|
|
1645
2362
|
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
2365
|
+
*/
|
|
2366
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
2367
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
2368
|
+
const amountNum = parseFloat(amount);
|
|
2369
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
2370
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
2371
|
+
const challengeId = this.generateChallengeId();
|
|
2372
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
2373
|
+
const mppRequest = {
|
|
2374
|
+
amount: amountInUnits,
|
|
2375
|
+
currency: tokenAddress,
|
|
2376
|
+
methodDetails: {
|
|
2377
|
+
chainId: 42431,
|
|
2378
|
+
feePayer: true
|
|
2379
|
+
},
|
|
2380
|
+
recipient: wallet
|
|
2381
|
+
};
|
|
2382
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2383
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2384
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2385
|
+
res.writeHead(402, {
|
|
2386
|
+
"Content-Type": "application/problem+json",
|
|
2387
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
2388
|
+
});
|
|
2389
|
+
res.end(JSON.stringify({
|
|
2390
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
2391
|
+
title: "Payment Required",
|
|
2392
|
+
status: 402,
|
|
2393
|
+
detail: `Payment is required (${config.name}).`,
|
|
2394
|
+
service: serviceId || "proxy",
|
|
2395
|
+
price: amountNum,
|
|
2396
|
+
currency: "USDC"
|
|
2397
|
+
}, null, 2));
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2401
|
+
if (!credentialMatch) {
|
|
2402
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2403
|
+
}
|
|
2404
|
+
let mppCredential;
|
|
2405
|
+
try {
|
|
2406
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2407
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2408
|
+
mppCredential = JSON.parse(decoded);
|
|
2409
|
+
} catch (err) {
|
|
2410
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
2411
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2412
|
+
}
|
|
2413
|
+
let txHash;
|
|
2414
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2415
|
+
txHash = mppCredential.payload.hash;
|
|
2416
|
+
} else {
|
|
2417
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2418
|
+
}
|
|
2419
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
2420
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
2421
|
+
const paymentPayload = {
|
|
2422
|
+
x402Version: X402_VERSION2,
|
|
2423
|
+
scheme: "exact",
|
|
2424
|
+
network: "eip155:42431",
|
|
2425
|
+
payload: { txHash, chainId: 42431 }
|
|
2426
|
+
};
|
|
2427
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
2428
|
+
if (!verification.valid) {
|
|
2429
|
+
return this.sendJson(res, 402, {
|
|
2430
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2434
|
+
const { execute, service, params } = body;
|
|
2435
|
+
if (execute && service) {
|
|
2436
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2437
|
+
const skill = this.skills.get(service);
|
|
2438
|
+
if (!skill) {
|
|
2439
|
+
return this.sendJson(res, 404, {
|
|
2440
|
+
success: false,
|
|
2441
|
+
paymentSettled: true,
|
|
2442
|
+
// Payment already happened on Tempo
|
|
2443
|
+
error: `Service not found: ${service}`
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2447
|
+
let result;
|
|
2448
|
+
try {
|
|
2449
|
+
result = await Promise.race([
|
|
2450
|
+
skill.handler(params || {}),
|
|
2451
|
+
new Promise(
|
|
2452
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2453
|
+
)
|
|
2454
|
+
]);
|
|
2455
|
+
} catch (err) {
|
|
2456
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2457
|
+
return this.sendJson(res, 500, {
|
|
2458
|
+
success: false,
|
|
2459
|
+
paymentSettled: true,
|
|
2460
|
+
error: `Service execution failed: ${err.message}`
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
return this.sendJson(res, 200, {
|
|
2464
|
+
success: true,
|
|
2465
|
+
verified: true,
|
|
2466
|
+
txHash,
|
|
2467
|
+
chain: "tempo_moderato",
|
|
2468
|
+
paidTo: wallet,
|
|
2469
|
+
amount: amountNum,
|
|
2470
|
+
currency: "USDC",
|
|
2471
|
+
facilitator: verification.facilitator,
|
|
2472
|
+
memo,
|
|
2473
|
+
result
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
this.sendJson(res, 200, {
|
|
2477
|
+
success: true,
|
|
2478
|
+
verified: true,
|
|
2479
|
+
txHash,
|
|
2480
|
+
chain: "tempo_moderato",
|
|
2481
|
+
paidTo: wallet,
|
|
2482
|
+
amount: amountNum,
|
|
2483
|
+
currency: "USDC",
|
|
2484
|
+
facilitator: verification.facilitator,
|
|
2485
|
+
memo
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
1646
2488
|
/**
|
|
1647
2489
|
* Build payment requirements for proxy endpoint (uses provided wallet)
|
|
1648
2490
|
*/
|
|
@@ -1654,7 +2496,7 @@ var MoltsPayServer = class {
|
|
|
1654
2496
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1655
2497
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1656
2498
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1657
|
-
|
|
2499
|
+
const requirements = {
|
|
1658
2500
|
scheme: "exact",
|
|
1659
2501
|
network: networkId,
|
|
1660
2502
|
asset: tokenAddress,
|
|
@@ -1664,6 +2506,17 @@ var MoltsPayServer = class {
|
|
|
1664
2506
|
maxTimeoutSeconds: 300,
|
|
1665
2507
|
extra: tokenDomain
|
|
1666
2508
|
};
|
|
2509
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
2510
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2511
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2512
|
+
if (spenderAddress) {
|
|
2513
|
+
requirements.extra = {
|
|
2514
|
+
...requirements.extra || {},
|
|
2515
|
+
bnbSpender: spenderAddress
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return requirements;
|
|
1667
2520
|
}
|
|
1668
2521
|
/**
|
|
1669
2522
|
* Return 402 with x402 payment requirements for proxy endpoint
|