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.js
CHANGED
|
@@ -395,6 +395,63 @@ var CHAINS = {
|
|
|
395
395
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
396
396
|
avgBlockTime: 0.5
|
|
397
397
|
// ~500ms finality
|
|
398
|
+
},
|
|
399
|
+
// ============ BNB Chain Testnet ============
|
|
400
|
+
bnb_testnet: {
|
|
401
|
+
name: "BNB Testnet",
|
|
402
|
+
chainId: 97,
|
|
403
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
404
|
+
tokens: {
|
|
405
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
406
|
+
// Using official Binance-Peg testnet tokens
|
|
407
|
+
USDC: {
|
|
408
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
409
|
+
// Testnet USDC
|
|
410
|
+
decimals: 18,
|
|
411
|
+
symbol: "USDC",
|
|
412
|
+
eip712Name: "USD Coin"
|
|
413
|
+
},
|
|
414
|
+
USDT: {
|
|
415
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
416
|
+
// Testnet USDT
|
|
417
|
+
decimals: 18,
|
|
418
|
+
symbol: "USDT",
|
|
419
|
+
eip712Name: "Tether USD"
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
423
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
424
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
425
|
+
avgBlockTime: 3,
|
|
426
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
427
|
+
requiresApproval: true
|
|
428
|
+
},
|
|
429
|
+
// ============ BNB Chain Mainnet ============
|
|
430
|
+
bnb: {
|
|
431
|
+
name: "BNB Smart Chain",
|
|
432
|
+
chainId: 56,
|
|
433
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
434
|
+
tokens: {
|
|
435
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
436
|
+
USDC: {
|
|
437
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
438
|
+
decimals: 18,
|
|
439
|
+
symbol: "USDC",
|
|
440
|
+
eip712Name: "USD Coin"
|
|
441
|
+
},
|
|
442
|
+
USDT: {
|
|
443
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
444
|
+
decimals: 18,
|
|
445
|
+
symbol: "USDT",
|
|
446
|
+
eip712Name: "Tether USD"
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
450
|
+
explorer: "https://bscscan.com/address/",
|
|
451
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
452
|
+
avgBlockTime: 3,
|
|
453
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
454
|
+
requiresApproval: true
|
|
398
455
|
}
|
|
399
456
|
};
|
|
400
457
|
function getChain(name) {
|
|
@@ -544,7 +601,573 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
544
601
|
}
|
|
545
602
|
};
|
|
546
603
|
|
|
604
|
+
// src/facilitators/bnb.ts
|
|
605
|
+
var import_accounts = require("viem/accounts");
|
|
606
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
607
|
+
var EIP712_DOMAIN = {
|
|
608
|
+
name: "MoltsPay",
|
|
609
|
+
version: "1"
|
|
610
|
+
};
|
|
611
|
+
var INTENT_TYPES = {
|
|
612
|
+
PaymentIntent: [
|
|
613
|
+
{ name: "from", type: "address" },
|
|
614
|
+
{ name: "to", type: "address" },
|
|
615
|
+
{ name: "amount", type: "uint256" },
|
|
616
|
+
{ name: "token", type: "address" },
|
|
617
|
+
{ name: "service", type: "string" },
|
|
618
|
+
{ name: "nonce", type: "uint256" },
|
|
619
|
+
{ name: "deadline", type: "uint256" }
|
|
620
|
+
]
|
|
621
|
+
};
|
|
622
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
623
|
+
name = "bnb";
|
|
624
|
+
displayName = "BNB Smart Chain";
|
|
625
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
626
|
+
// Mainnet + Testnet
|
|
627
|
+
serverPrivateKey;
|
|
628
|
+
spenderAddress = null;
|
|
629
|
+
chainConfigs;
|
|
630
|
+
constructor(serverPrivateKey) {
|
|
631
|
+
super();
|
|
632
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
633
|
+
if (this.serverPrivateKey) {
|
|
634
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
635
|
+
const account = (0, import_accounts.privateKeyToAccount)(key);
|
|
636
|
+
this.spenderAddress = account.address;
|
|
637
|
+
}
|
|
638
|
+
this.chainConfigs = {
|
|
639
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
640
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async healthCheck() {
|
|
644
|
+
const start = Date.now();
|
|
645
|
+
try {
|
|
646
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
647
|
+
method: "POST",
|
|
648
|
+
headers: { "Content-Type": "application/json" },
|
|
649
|
+
body: JSON.stringify({
|
|
650
|
+
jsonrpc: "2.0",
|
|
651
|
+
method: "eth_chainId",
|
|
652
|
+
params: [],
|
|
653
|
+
id: 1
|
|
654
|
+
})
|
|
655
|
+
});
|
|
656
|
+
const data = await response.json();
|
|
657
|
+
const chainId = parseInt(data.result, 16);
|
|
658
|
+
if (chainId !== 56) {
|
|
659
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
660
|
+
}
|
|
661
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
662
|
+
} catch (error) {
|
|
663
|
+
return { healthy: false, error: String(error) };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Verify a payment intent signature (before service execution)
|
|
668
|
+
*
|
|
669
|
+
* This verifies:
|
|
670
|
+
* 1. Signature is valid for the intent
|
|
671
|
+
* 2. Client has approved server wallet
|
|
672
|
+
* 3. Client has sufficient balance
|
|
673
|
+
* 4. Intent hasn't expired
|
|
674
|
+
*/
|
|
675
|
+
async verify(paymentPayload, requirements) {
|
|
676
|
+
try {
|
|
677
|
+
const bnbPayload = paymentPayload.payload;
|
|
678
|
+
if (!bnbPayload?.intent) {
|
|
679
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
680
|
+
}
|
|
681
|
+
const { intent, chainId } = bnbPayload;
|
|
682
|
+
const config = this.chainConfigs[chainId];
|
|
683
|
+
if (!config) {
|
|
684
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
685
|
+
}
|
|
686
|
+
if (intent.deadline < Date.now()) {
|
|
687
|
+
return { valid: false, error: "Intent expired" };
|
|
688
|
+
}
|
|
689
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
690
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
691
|
+
return { valid: false, error: "Invalid signature" };
|
|
692
|
+
}
|
|
693
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
694
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
695
|
+
}
|
|
696
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
697
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
698
|
+
}
|
|
699
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
700
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
701
|
+
}
|
|
702
|
+
const serverAddress = await this.getServerAddress();
|
|
703
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
704
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
705
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
706
|
+
}
|
|
707
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
708
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
709
|
+
return { valid: false, error: "Insufficient balance" };
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
valid: true,
|
|
713
|
+
details: {
|
|
714
|
+
from: intent.from,
|
|
715
|
+
to: intent.to,
|
|
716
|
+
amount: intent.amount,
|
|
717
|
+
token: intent.token,
|
|
718
|
+
service: intent.service,
|
|
719
|
+
nonce: intent.nonce,
|
|
720
|
+
deadline: intent.deadline
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
} catch (error) {
|
|
724
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Settle a payment by executing transferFrom
|
|
729
|
+
*
|
|
730
|
+
* This is called AFTER the service has been successfully delivered.
|
|
731
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
732
|
+
*/
|
|
733
|
+
async settle(paymentPayload, requirements) {
|
|
734
|
+
if (!this.serverPrivateKey) {
|
|
735
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
739
|
+
if (!verifyResult.valid) {
|
|
740
|
+
return { success: false, error: verifyResult.error };
|
|
741
|
+
}
|
|
742
|
+
const bnbPayload = paymentPayload.payload;
|
|
743
|
+
const { intent, chainId } = bnbPayload;
|
|
744
|
+
const config = this.chainConfigs[chainId];
|
|
745
|
+
const txHash = await this.executeTransferFrom(
|
|
746
|
+
intent.from,
|
|
747
|
+
intent.to,
|
|
748
|
+
intent.amount,
|
|
749
|
+
intent.token,
|
|
750
|
+
config.rpc
|
|
751
|
+
);
|
|
752
|
+
return {
|
|
753
|
+
success: true,
|
|
754
|
+
transaction: txHash,
|
|
755
|
+
status: "settled"
|
|
756
|
+
};
|
|
757
|
+
} catch (error) {
|
|
758
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Check if client has approved the server wallet
|
|
763
|
+
*/
|
|
764
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
765
|
+
const config = this.chainConfigs[chainId];
|
|
766
|
+
if (!config) {
|
|
767
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
768
|
+
}
|
|
769
|
+
const serverAddress = await this.getServerAddress();
|
|
770
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
771
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
772
|
+
return {
|
|
773
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
774
|
+
allowance
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Verify a completed transaction (for checking past payments)
|
|
779
|
+
*/
|
|
780
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
781
|
+
const config = this.chainConfigs[chainId];
|
|
782
|
+
if (!config) {
|
|
783
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
787
|
+
if (!receipt) {
|
|
788
|
+
return { valid: false, error: "Transaction not found" };
|
|
789
|
+
}
|
|
790
|
+
if (receipt.status !== "0x1") {
|
|
791
|
+
return { valid: false, error: "Transaction failed" };
|
|
792
|
+
}
|
|
793
|
+
const transferLog = receipt.logs.find(
|
|
794
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
795
|
+
);
|
|
796
|
+
if (!transferLog) {
|
|
797
|
+
return { valid: false, error: "No Transfer event found" };
|
|
798
|
+
}
|
|
799
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
800
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
801
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
802
|
+
}
|
|
803
|
+
const amount = BigInt(transferLog.data);
|
|
804
|
+
if (amount < BigInt(expected.amount)) {
|
|
805
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
valid: true,
|
|
809
|
+
details: {
|
|
810
|
+
txHash,
|
|
811
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
812
|
+
to: toAddress,
|
|
813
|
+
amount: amount.toString(),
|
|
814
|
+
token: transferLog.address
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
} catch (error) {
|
|
818
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// ==================== Private Methods ====================
|
|
822
|
+
/**
|
|
823
|
+
* Get the server's spender address (public, for 402 responses)
|
|
824
|
+
* Returns cached value computed at construction time.
|
|
825
|
+
*/
|
|
826
|
+
getSpenderAddress() {
|
|
827
|
+
return this.spenderAddress;
|
|
828
|
+
}
|
|
829
|
+
async getServerAddress() {
|
|
830
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
831
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey);
|
|
832
|
+
return wallet.address;
|
|
833
|
+
}
|
|
834
|
+
async recoverIntentSigner(intent, chainId) {
|
|
835
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
836
|
+
const domain = {
|
|
837
|
+
...EIP712_DOMAIN,
|
|
838
|
+
chainId
|
|
839
|
+
};
|
|
840
|
+
const message = {
|
|
841
|
+
from: intent.from,
|
|
842
|
+
to: intent.to,
|
|
843
|
+
amount: intent.amount,
|
|
844
|
+
token: intent.token,
|
|
845
|
+
service: intent.service,
|
|
846
|
+
nonce: intent.nonce,
|
|
847
|
+
deadline: intent.deadline
|
|
848
|
+
};
|
|
849
|
+
const recoveredAddress = ethers5.verifyTypedData(
|
|
850
|
+
domain,
|
|
851
|
+
INTENT_TYPES,
|
|
852
|
+
message,
|
|
853
|
+
intent.signature
|
|
854
|
+
);
|
|
855
|
+
return recoveredAddress;
|
|
856
|
+
}
|
|
857
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
858
|
+
const selector = "0xdd62ed3e";
|
|
859
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
860
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
861
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
862
|
+
const response = await fetch(rpcUrl, {
|
|
863
|
+
method: "POST",
|
|
864
|
+
headers: { "Content-Type": "application/json" },
|
|
865
|
+
body: JSON.stringify({
|
|
866
|
+
jsonrpc: "2.0",
|
|
867
|
+
method: "eth_call",
|
|
868
|
+
params: [{ to: token, data }, "latest"],
|
|
869
|
+
id: 1
|
|
870
|
+
})
|
|
871
|
+
});
|
|
872
|
+
const result = await response.json();
|
|
873
|
+
return result.result || "0x0";
|
|
874
|
+
}
|
|
875
|
+
async getBalance(account, token, rpcUrl) {
|
|
876
|
+
const selector = "0x70a08231";
|
|
877
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
878
|
+
const data = selector + accountPadded;
|
|
879
|
+
const response = await fetch(rpcUrl, {
|
|
880
|
+
method: "POST",
|
|
881
|
+
headers: { "Content-Type": "application/json" },
|
|
882
|
+
body: JSON.stringify({
|
|
883
|
+
jsonrpc: "2.0",
|
|
884
|
+
method: "eth_call",
|
|
885
|
+
params: [{ to: token, data }, "latest"],
|
|
886
|
+
id: 1
|
|
887
|
+
})
|
|
888
|
+
});
|
|
889
|
+
const result = await response.json();
|
|
890
|
+
return result.result || "0x0";
|
|
891
|
+
}
|
|
892
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
893
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
894
|
+
const provider = new ethers5.JsonRpcProvider(rpcUrl);
|
|
895
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey, provider);
|
|
896
|
+
const tokenContract = new ethers5.Contract(token, [
|
|
897
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
898
|
+
], wallet);
|
|
899
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
900
|
+
const receipt = await tx.wait();
|
|
901
|
+
return receipt.hash;
|
|
902
|
+
}
|
|
903
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
904
|
+
const response = await fetch(rpcUrl, {
|
|
905
|
+
method: "POST",
|
|
906
|
+
headers: { "Content-Type": "application/json" },
|
|
907
|
+
body: JSON.stringify({
|
|
908
|
+
jsonrpc: "2.0",
|
|
909
|
+
method: "eth_getTransactionReceipt",
|
|
910
|
+
params: [txHash],
|
|
911
|
+
id: 1
|
|
912
|
+
})
|
|
913
|
+
});
|
|
914
|
+
const data = await response.json();
|
|
915
|
+
return data.result;
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/facilitators/solana.ts
|
|
920
|
+
var import_web32 = require("@solana/web3.js");
|
|
921
|
+
var import_spl_token = require("@solana/spl-token");
|
|
922
|
+
|
|
923
|
+
// src/chains/solana.ts
|
|
924
|
+
var import_web3 = require("@solana/web3.js");
|
|
925
|
+
var SOLANA_CHAINS = {
|
|
926
|
+
solana: {
|
|
927
|
+
name: "Solana Mainnet",
|
|
928
|
+
cluster: "mainnet-beta",
|
|
929
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
930
|
+
explorer: "https://solscan.io/account/",
|
|
931
|
+
explorerTx: "https://solscan.io/tx/",
|
|
932
|
+
tokens: {
|
|
933
|
+
USDC: {
|
|
934
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
935
|
+
// Circle official USDC
|
|
936
|
+
decimals: 6
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
solana_devnet: {
|
|
941
|
+
name: "Solana Devnet",
|
|
942
|
+
cluster: "devnet",
|
|
943
|
+
rpc: "https://api.devnet.solana.com",
|
|
944
|
+
explorer: "https://solscan.io/account/",
|
|
945
|
+
explorerTx: "https://solscan.io/tx/",
|
|
946
|
+
tokens: {
|
|
947
|
+
USDC: {
|
|
948
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
949
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
950
|
+
decimals: 6
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
// src/facilitators/solana.ts
|
|
957
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
958
|
+
name = "solana";
|
|
959
|
+
displayName = "Solana Direct";
|
|
960
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
961
|
+
connections = /* @__PURE__ */ new Map();
|
|
962
|
+
feePayerKeypair;
|
|
963
|
+
constructor(config) {
|
|
964
|
+
super();
|
|
965
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
966
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
967
|
+
this.connections.set(
|
|
968
|
+
chain,
|
|
969
|
+
new import_web32.Connection(config2.rpc, "confirmed")
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
if (this.feePayerKeypair) {
|
|
973
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Get fee payer public key (for gasless transactions)
|
|
978
|
+
*/
|
|
979
|
+
getFeePayerPubkey() {
|
|
980
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
981
|
+
}
|
|
982
|
+
getConnection(chain) {
|
|
983
|
+
const conn = this.connections.get(chain);
|
|
984
|
+
if (!conn) {
|
|
985
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
986
|
+
}
|
|
987
|
+
return conn;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Convert our chain name to network identifier
|
|
991
|
+
*/
|
|
992
|
+
static chainToNetwork(chain) {
|
|
993
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Convert network identifier to chain name
|
|
997
|
+
*/
|
|
998
|
+
static networkToChain(network) {
|
|
999
|
+
if (network === "solana:mainnet") return "solana";
|
|
1000
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
async healthCheck() {
|
|
1004
|
+
const start = Date.now();
|
|
1005
|
+
try {
|
|
1006
|
+
const conn = this.getConnection("solana_devnet");
|
|
1007
|
+
await conn.getSlot();
|
|
1008
|
+
return {
|
|
1009
|
+
healthy: true,
|
|
1010
|
+
latencyMs: Date.now() - start
|
|
1011
|
+
};
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
return {
|
|
1014
|
+
healthy: false,
|
|
1015
|
+
error: error.message
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Verify a Solana payment
|
|
1021
|
+
*
|
|
1022
|
+
* Checks:
|
|
1023
|
+
* 1. Transaction is valid and properly signed
|
|
1024
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
1025
|
+
*/
|
|
1026
|
+
async verify(paymentPayload, requirements) {
|
|
1027
|
+
try {
|
|
1028
|
+
const solanaPayload = paymentPayload.payload;
|
|
1029
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1030
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
1031
|
+
}
|
|
1032
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1033
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
1034
|
+
if (!chainConfig) {
|
|
1035
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
1036
|
+
}
|
|
1037
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1038
|
+
let tx;
|
|
1039
|
+
try {
|
|
1040
|
+
tx = import_web32.Transaction.from(txBuffer);
|
|
1041
|
+
} catch {
|
|
1042
|
+
tx = import_web32.VersionedTransaction.deserialize(txBuffer);
|
|
1043
|
+
}
|
|
1044
|
+
if (tx instanceof import_web32.Transaction) {
|
|
1045
|
+
const hasAnySignature = tx.signatures.some(
|
|
1046
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
1047
|
+
);
|
|
1048
|
+
if (!hasAnySignature) {
|
|
1049
|
+
return { valid: false, error: "Transaction not signed" };
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1053
|
+
const expectedRecipient = new import_web32.PublicKey(requirements.payTo);
|
|
1054
|
+
return {
|
|
1055
|
+
valid: true,
|
|
1056
|
+
details: {
|
|
1057
|
+
chain,
|
|
1058
|
+
sender: solanaPayload.sender,
|
|
1059
|
+
recipient: requirements.payTo,
|
|
1060
|
+
amount: requirements.amount
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
return { valid: false, error: error.message };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Settle a Solana payment
|
|
1069
|
+
*
|
|
1070
|
+
* Submits the signed transaction to the network.
|
|
1071
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
1072
|
+
*/
|
|
1073
|
+
async settle(paymentPayload, requirements) {
|
|
1074
|
+
try {
|
|
1075
|
+
const solanaPayload = paymentPayload.payload;
|
|
1076
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1077
|
+
return { success: false, error: "Missing signed transaction" };
|
|
1078
|
+
}
|
|
1079
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1080
|
+
const connection = this.getConnection(chain);
|
|
1081
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1082
|
+
let txToSend;
|
|
1083
|
+
try {
|
|
1084
|
+
const tx = import_web32.Transaction.from(txBuffer);
|
|
1085
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
1086
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
1087
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
1088
|
+
if (txFeePayer === feePayerPubkey) {
|
|
1089
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
1090
|
+
tx.partialSign(this.feePayerKeypair);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
txToSend = tx.serialize();
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
txToSend = txBuffer;
|
|
1096
|
+
}
|
|
1097
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
1098
|
+
skipPreflight: false,
|
|
1099
|
+
preflightCommitment: "confirmed"
|
|
1100
|
+
});
|
|
1101
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
1102
|
+
if (confirmation.value.err) {
|
|
1103
|
+
return {
|
|
1104
|
+
success: false,
|
|
1105
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
1106
|
+
transaction: signature
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
success: true,
|
|
1111
|
+
transaction: signature,
|
|
1112
|
+
status: "confirmed"
|
|
1113
|
+
};
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
return { success: false, error: error.message };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
supportsNetwork(network) {
|
|
1119
|
+
return this.supportedNetworks.includes(network);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
1123
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
1124
|
+
const connection = new import_web32.Connection(chainConfig.rpc, "confirmed");
|
|
1125
|
+
const mint = new import_web32.PublicKey(chainConfig.tokens.USDC.mint);
|
|
1126
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
1127
|
+
const senderATA = await (0, import_spl_token.getAssociatedTokenAddress)(mint, senderPubkey);
|
|
1128
|
+
const recipientATA = await (0, import_spl_token.getAssociatedTokenAddress)(mint, recipientPubkey);
|
|
1129
|
+
const transaction = new import_web32.Transaction();
|
|
1130
|
+
try {
|
|
1131
|
+
await (0, import_spl_token.getAccount)(connection, recipientATA);
|
|
1132
|
+
} catch {
|
|
1133
|
+
transaction.add(
|
|
1134
|
+
(0, import_spl_token.createAssociatedTokenAccountInstruction)(
|
|
1135
|
+
actualFeePayer,
|
|
1136
|
+
// payer (fee payer in gasless mode)
|
|
1137
|
+
recipientATA,
|
|
1138
|
+
// ata to create
|
|
1139
|
+
recipientPubkey,
|
|
1140
|
+
// owner
|
|
1141
|
+
mint
|
|
1142
|
+
// mint
|
|
1143
|
+
)
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
transaction.add(
|
|
1147
|
+
(0, import_spl_token.createTransferCheckedInstruction)(
|
|
1148
|
+
senderATA,
|
|
1149
|
+
// source
|
|
1150
|
+
mint,
|
|
1151
|
+
// mint
|
|
1152
|
+
recipientATA,
|
|
1153
|
+
// destination
|
|
1154
|
+
senderPubkey,
|
|
1155
|
+
// owner (sender still authorizes the transfer)
|
|
1156
|
+
amount,
|
|
1157
|
+
// amount
|
|
1158
|
+
chainConfig.tokens.USDC.decimals
|
|
1159
|
+
// decimals
|
|
1160
|
+
)
|
|
1161
|
+
);
|
|
1162
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
1163
|
+
transaction.recentBlockhash = blockhash;
|
|
1164
|
+
transaction.feePayer = actualFeePayer;
|
|
1165
|
+
return transaction;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
547
1168
|
// src/facilitators/registry.ts
|
|
1169
|
+
var import_web33 = require("@solana/web3.js");
|
|
1170
|
+
var import_bs58 = __toESM(require("bs58"));
|
|
548
1171
|
var FacilitatorRegistry = class {
|
|
549
1172
|
factories = /* @__PURE__ */ new Map();
|
|
550
1173
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -553,7 +1176,20 @@ var FacilitatorRegistry = class {
|
|
|
553
1176
|
constructor(selection) {
|
|
554
1177
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
555
1178
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
556
|
-
this.
|
|
1179
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
1180
|
+
this.registerFactory("solana", (config) => {
|
|
1181
|
+
let feePayerKeypair;
|
|
1182
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
1183
|
+
if (feePayerKey) {
|
|
1184
|
+
try {
|
|
1185
|
+
feePayerKeypair = import_web33.Keypair.fromSecretKey(import_bs58.default.decode(feePayerKey));
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
1191
|
+
});
|
|
1192
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
557
1193
|
}
|
|
558
1194
|
/**
|
|
559
1195
|
* Register a new facilitator factory
|
|
@@ -807,14 +1443,40 @@ var TOKEN_ADDRESSES = {
|
|
|
807
1443
|
// pathUSD
|
|
808
1444
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
809
1445
|
// alphaUSD
|
|
1446
|
+
},
|
|
1447
|
+
// BNB Smart Chain mainnet
|
|
1448
|
+
"eip155:56": {
|
|
1449
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
1450
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
1451
|
+
},
|
|
1452
|
+
// BNB Smart Chain testnet
|
|
1453
|
+
"eip155:97": {
|
|
1454
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
1455
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
1456
|
+
},
|
|
1457
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
1458
|
+
"solana:mainnet": {
|
|
1459
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
1460
|
+
// Circle USDC
|
|
1461
|
+
},
|
|
1462
|
+
"solana:devnet": {
|
|
1463
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
1464
|
+
// Devnet USDC
|
|
810
1465
|
}
|
|
811
1466
|
};
|
|
812
1467
|
var CHAIN_TO_NETWORK = {
|
|
813
1468
|
"base": "eip155:8453",
|
|
814
1469
|
"base_sepolia": "eip155:84532",
|
|
815
1470
|
"polygon": "eip155:137",
|
|
816
|
-
"tempo_moderato": "eip155:42431"
|
|
1471
|
+
"tempo_moderato": "eip155:42431",
|
|
1472
|
+
"bnb": "eip155:56",
|
|
1473
|
+
"bnb_testnet": "eip155:97",
|
|
1474
|
+
"solana": "solana:mainnet",
|
|
1475
|
+
"solana_devnet": "solana:devnet"
|
|
817
1476
|
};
|
|
1477
|
+
function isSolanaNetwork(network) {
|
|
1478
|
+
return network.startsWith("solana:");
|
|
1479
|
+
}
|
|
818
1480
|
var TOKEN_DOMAINS = {
|
|
819
1481
|
// Base mainnet
|
|
820
1482
|
"eip155:8453": {
|
|
@@ -836,6 +1498,16 @@ var TOKEN_DOMAINS = {
|
|
|
836
1498
|
"eip155:42431": {
|
|
837
1499
|
USDC: { name: "pathUSD", version: "1" },
|
|
838
1500
|
USDT: { name: "alphaUSD", version: "1" }
|
|
1501
|
+
},
|
|
1502
|
+
// BNB Smart Chain mainnet
|
|
1503
|
+
"eip155:56": {
|
|
1504
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1505
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1506
|
+
},
|
|
1507
|
+
// BNB Smart Chain testnet
|
|
1508
|
+
"eip155:97": {
|
|
1509
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1510
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
839
1511
|
}
|
|
840
1512
|
};
|
|
841
1513
|
function getTokenDomain(network, token) {
|
|
@@ -893,7 +1565,7 @@ var MoltsPayServer = class {
|
|
|
893
1565
|
};
|
|
894
1566
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
895
1567
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
896
|
-
const defaultFallback = ["tempo"];
|
|
1568
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
897
1569
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
898
1570
|
const facilitatorConfig = options.facilitators || {
|
|
899
1571
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -936,12 +1608,20 @@ var MoltsPayServer = class {
|
|
|
936
1608
|
*/
|
|
937
1609
|
getProviderChains() {
|
|
938
1610
|
const provider = this.manifest.provider;
|
|
1611
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
1612
|
+
if (explicitWallet) return explicitWallet;
|
|
1613
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
1614
|
+
return provider.solana_wallet;
|
|
1615
|
+
}
|
|
1616
|
+
return provider.wallet;
|
|
1617
|
+
};
|
|
939
1618
|
if (provider.chains && provider.chains.length > 0) {
|
|
940
1619
|
return provider.chains.map((c) => {
|
|
941
1620
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
1621
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
942
1622
|
return {
|
|
943
1623
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
944
|
-
wallet: (
|
|
1624
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
945
1625
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
946
1626
|
};
|
|
947
1627
|
});
|
|
@@ -950,7 +1630,7 @@ var MoltsPayServer = class {
|
|
|
950
1630
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
951
1631
|
return [{
|
|
952
1632
|
network,
|
|
953
|
-
wallet:
|
|
1633
|
+
wallet: getWalletForChain(chain),
|
|
954
1634
|
tokens: ["USDC"]
|
|
955
1635
|
}];
|
|
956
1636
|
}
|
|
@@ -1021,7 +1701,8 @@ var MoltsPayServer = class {
|
|
|
1021
1701
|
}
|
|
1022
1702
|
const body = await this.readBody(req);
|
|
1023
1703
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
1024
|
-
|
|
1704
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1705
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
1025
1706
|
}
|
|
1026
1707
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
1027
1708
|
const skill = this.skills.get(servicePath);
|
|
@@ -1058,7 +1739,9 @@ var MoltsPayServer = class {
|
|
|
1058
1739
|
name: this.manifest.provider.name,
|
|
1059
1740
|
description: this.manifest.provider.description,
|
|
1060
1741
|
wallet: this.manifest.provider.wallet,
|
|
1061
|
-
chain: this.manifest.provider.chain || "base"
|
|
1742
|
+
chain: this.manifest.provider.chain || "base",
|
|
1743
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
1744
|
+
chains: this.manifest.provider.chains
|
|
1062
1745
|
},
|
|
1063
1746
|
services,
|
|
1064
1747
|
endpoints: {
|
|
@@ -1171,6 +1854,21 @@ var MoltsPayServer = class {
|
|
|
1171
1854
|
});
|
|
1172
1855
|
}
|
|
1173
1856
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
1857
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
1858
|
+
let settlement = null;
|
|
1859
|
+
if (isSolana) {
|
|
1860
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
1861
|
+
try {
|
|
1862
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1863
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
1866
|
+
return this.sendJson(res, 402, {
|
|
1867
|
+
error: "Payment settlement failed",
|
|
1868
|
+
message: err.message
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1174
1872
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1175
1873
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1176
1874
|
let result;
|
|
@@ -1185,16 +1883,19 @@ var MoltsPayServer = class {
|
|
|
1185
1883
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1186
1884
|
return this.sendJson(res, 500, {
|
|
1187
1885
|
error: "Service execution failed",
|
|
1188
|
-
message: err.message
|
|
1886
|
+
message: err.message,
|
|
1887
|
+
paymentSettled: isSolana ? true : false,
|
|
1888
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1189
1889
|
});
|
|
1190
1890
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1891
|
+
if (!isSolana) {
|
|
1892
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
1893
|
+
try {
|
|
1894
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1895
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
1898
|
+
}
|
|
1198
1899
|
}
|
|
1199
1900
|
const responseHeaders = {};
|
|
1200
1901
|
if (settlement?.success) {
|
|
@@ -1470,7 +2171,7 @@ var MoltsPayServer = class {
|
|
|
1470
2171
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
1471
2172
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1472
2173
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
1473
|
-
|
|
2174
|
+
const requirements = {
|
|
1474
2175
|
scheme: "exact",
|
|
1475
2176
|
network: selectedNetwork,
|
|
1476
2177
|
asset: tokenAddress,
|
|
@@ -1479,6 +2180,27 @@ var MoltsPayServer = class {
|
|
|
1479
2180
|
maxTimeoutSeconds: 300,
|
|
1480
2181
|
extra: tokenDomain
|
|
1481
2182
|
};
|
|
2183
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
2184
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
2185
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
2186
|
+
if (feePayerPubkey) {
|
|
2187
|
+
requirements.extra = {
|
|
2188
|
+
...requirements.extra || {},
|
|
2189
|
+
solanaFeePayer: feePayerPubkey
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
2194
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2195
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2196
|
+
if (spenderAddress) {
|
|
2197
|
+
requirements.extra = {
|
|
2198
|
+
...requirements.extra || {},
|
|
2199
|
+
bnbSpender: spenderAddress
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return requirements;
|
|
1482
2204
|
}
|
|
1483
2205
|
/**
|
|
1484
2206
|
* Detect which token is being used in the payment
|
|
@@ -1544,31 +2266,42 @@ var MoltsPayServer = class {
|
|
|
1544
2266
|
/**
|
|
1545
2267
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1546
2268
|
*
|
|
1547
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
2269
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1548
2270
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1549
2271
|
*
|
|
1550
2272
|
* Request body:
|
|
1551
2273
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1552
2274
|
*
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
2275
|
+
* For x402 (base, polygon, base_sepolia):
|
|
2276
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
2277
|
+
* With X-Payment header: verifies payment via CDP
|
|
2278
|
+
*
|
|
2279
|
+
* For MPP (tempo_moderato):
|
|
2280
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
2281
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1555
2282
|
*/
|
|
1556
|
-
async handleProxy(body, paymentHeader, res) {
|
|
2283
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1557
2284
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1558
2285
|
if (!wallet || !amount) {
|
|
1559
2286
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1560
2287
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
2288
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2289
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
2290
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2291
|
+
}
|
|
2292
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
2293
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
2294
|
+
const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
2295
|
+
if (isSolanaChain && !isValidSolanaAddress) {
|
|
2296
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
2297
|
+
}
|
|
2298
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
2299
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1563
2300
|
}
|
|
1564
2301
|
const amountNum = parseFloat(amount);
|
|
1565
2302
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1566
2303
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1567
2304
|
}
|
|
1568
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1569
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
1570
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
1571
|
-
}
|
|
1572
2305
|
const proxyConfig = {
|
|
1573
2306
|
id: serviceId || "proxy",
|
|
1574
2307
|
name: description || "Proxy Payment",
|
|
@@ -1580,6 +2313,9 @@ var MoltsPayServer = class {
|
|
|
1580
2313
|
input: {},
|
|
1581
2314
|
output: {}
|
|
1582
2315
|
};
|
|
2316
|
+
if (chain === "tempo_moderato") {
|
|
2317
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
2318
|
+
}
|
|
1583
2319
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1584
2320
|
if (!paymentHeader) {
|
|
1585
2321
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1591,37 +2327,225 @@ var MoltsPayServer = class {
|
|
|
1591
2327
|
} catch {
|
|
1592
2328
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
1593
2329
|
}
|
|
1594
|
-
if (payment.x402Version !== X402_VERSION2) {
|
|
1595
|
-
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
2330
|
+
if (payment.x402Version !== X402_VERSION2) {
|
|
2331
|
+
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
2332
|
+
}
|
|
2333
|
+
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2334
|
+
const network = payment.accepted?.network || payment.network;
|
|
2335
|
+
if (scheme !== "exact") {
|
|
2336
|
+
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
2337
|
+
}
|
|
2338
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
2339
|
+
if (network !== expectedNetwork) {
|
|
2340
|
+
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
2341
|
+
}
|
|
2342
|
+
console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
|
|
2343
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
2344
|
+
if (!verifyResult.valid) {
|
|
2345
|
+
return this.sendJson(res, 402, {
|
|
2346
|
+
success: false,
|
|
2347
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2348
|
+
facilitator: verifyResult.facilitator
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
2352
|
+
const { execute, service, params } = body;
|
|
2353
|
+
if (execute && service) {
|
|
2354
|
+
const skill = this.skills.get(service);
|
|
2355
|
+
if (!skill) {
|
|
2356
|
+
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
2357
|
+
return this.sendJson(res, 404, {
|
|
2358
|
+
success: false,
|
|
2359
|
+
paymentSettled: false,
|
|
2360
|
+
error: `Service not found: ${service}`
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
const isSolana = isSolanaNetwork(network);
|
|
2364
|
+
let settlement2 = null;
|
|
2365
|
+
if (isSolana) {
|
|
2366
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
2367
|
+
try {
|
|
2368
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2369
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2370
|
+
if (!settlement2.success) {
|
|
2371
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
2372
|
+
return this.sendJson(res, 402, {
|
|
2373
|
+
success: false,
|
|
2374
|
+
paymentSettled: false,
|
|
2375
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
} catch (err) {
|
|
2379
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
2380
|
+
return this.sendJson(res, 402, {
|
|
2381
|
+
success: false,
|
|
2382
|
+
paymentSettled: false,
|
|
2383
|
+
error: `Payment settlement failed: ${err.message}`
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
} else {
|
|
2387
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
2388
|
+
}
|
|
2389
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2390
|
+
let result;
|
|
2391
|
+
try {
|
|
2392
|
+
result = await Promise.race([
|
|
2393
|
+
skill.handler(params || {}),
|
|
2394
|
+
new Promise(
|
|
2395
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2396
|
+
)
|
|
2397
|
+
]);
|
|
2398
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
2401
|
+
return this.sendJson(res, 500, {
|
|
2402
|
+
success: false,
|
|
2403
|
+
paymentSettled: isSolana ? true : false,
|
|
2404
|
+
error: `Service execution failed: ${err.message}`,
|
|
2405
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
if (!isSolana) {
|
|
2409
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
2410
|
+
try {
|
|
2411
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2412
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2415
|
+
return this.sendJson(res, 200, {
|
|
2416
|
+
success: true,
|
|
2417
|
+
verified: true,
|
|
2418
|
+
settled: false,
|
|
2419
|
+
settlementError: err.message,
|
|
2420
|
+
from: payment.payload?.authorization?.from,
|
|
2421
|
+
paidTo: wallet,
|
|
2422
|
+
amount: amountNum,
|
|
2423
|
+
currency: currency || "USDC",
|
|
2424
|
+
memo,
|
|
2425
|
+
result
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return this.sendJson(res, 200, {
|
|
2430
|
+
success: true,
|
|
2431
|
+
verified: true,
|
|
2432
|
+
settled: settlement2?.success || false,
|
|
2433
|
+
txHash: settlement2?.transaction,
|
|
2434
|
+
from: payment.payload?.authorization?.from,
|
|
2435
|
+
paidTo: wallet,
|
|
2436
|
+
amount: amountNum,
|
|
2437
|
+
currency: currency || "USDC",
|
|
2438
|
+
facilitator: settlement2?.facilitator,
|
|
2439
|
+
memo,
|
|
2440
|
+
result
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
2444
|
+
let settlement = null;
|
|
2445
|
+
try {
|
|
2446
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2447
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2450
|
+
return this.sendJson(res, 500, {
|
|
2451
|
+
success: false,
|
|
2452
|
+
error: `Settlement failed: ${err.message}`
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
this.sendJson(res, 200, {
|
|
2456
|
+
success: true,
|
|
2457
|
+
verified: true,
|
|
2458
|
+
settled: settlement?.success || false,
|
|
2459
|
+
txHash: settlement?.transaction,
|
|
2460
|
+
from: payment.payload?.authorization?.from,
|
|
2461
|
+
// Buyer's wallet address
|
|
2462
|
+
paidTo: wallet,
|
|
2463
|
+
amount: amountNum,
|
|
2464
|
+
currency: currency || "USDC",
|
|
2465
|
+
facilitator: settlement?.facilitator,
|
|
2466
|
+
memo
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
2471
|
+
*/
|
|
2472
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
2473
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
2474
|
+
const amountNum = parseFloat(amount);
|
|
2475
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
2476
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
2477
|
+
const challengeId = this.generateChallengeId();
|
|
2478
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
2479
|
+
const mppRequest = {
|
|
2480
|
+
amount: amountInUnits,
|
|
2481
|
+
currency: tokenAddress,
|
|
2482
|
+
methodDetails: {
|
|
2483
|
+
chainId: 42431,
|
|
2484
|
+
feePayer: true
|
|
2485
|
+
},
|
|
2486
|
+
recipient: wallet
|
|
2487
|
+
};
|
|
2488
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2489
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2490
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2491
|
+
res.writeHead(402, {
|
|
2492
|
+
"Content-Type": "application/problem+json",
|
|
2493
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
2494
|
+
});
|
|
2495
|
+
res.end(JSON.stringify({
|
|
2496
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
2497
|
+
title: "Payment Required",
|
|
2498
|
+
status: 402,
|
|
2499
|
+
detail: `Payment is required (${config.name}).`,
|
|
2500
|
+
service: serviceId || "proxy",
|
|
2501
|
+
price: amountNum,
|
|
2502
|
+
currency: "USDC"
|
|
2503
|
+
}, null, 2));
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2507
|
+
if (!credentialMatch) {
|
|
2508
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
1596
2509
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
2510
|
+
let mppCredential;
|
|
2511
|
+
try {
|
|
2512
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2513
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2514
|
+
mppCredential = JSON.parse(decoded);
|
|
2515
|
+
} catch (err) {
|
|
2516
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
2517
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
1601
2518
|
}
|
|
1602
|
-
|
|
1603
|
-
if (
|
|
1604
|
-
|
|
2519
|
+
let txHash;
|
|
2520
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2521
|
+
txHash = mppCredential.payload.hash;
|
|
2522
|
+
} else {
|
|
2523
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
1605
2524
|
}
|
|
1606
|
-
console.log(`[MoltsPay] /proxy: Verifying
|
|
1607
|
-
const
|
|
1608
|
-
|
|
2525
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
2526
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
2527
|
+
const paymentPayload = {
|
|
2528
|
+
x402Version: X402_VERSION2,
|
|
2529
|
+
scheme: "exact",
|
|
2530
|
+
network: "eip155:42431",
|
|
2531
|
+
payload: { txHash, chainId: 42431 }
|
|
2532
|
+
};
|
|
2533
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
2534
|
+
if (!verification.valid) {
|
|
1609
2535
|
return this.sendJson(res, 402, {
|
|
1610
|
-
|
|
1611
|
-
error: `Payment verification failed: ${verifyResult.error}`,
|
|
1612
|
-
facilitator: verifyResult.facilitator
|
|
2536
|
+
error: `Payment verification failed: ${verification.error}`
|
|
1613
2537
|
});
|
|
1614
2538
|
}
|
|
1615
|
-
console.log(`[MoltsPay] /proxy:
|
|
2539
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
1616
2540
|
const { execute, service, params } = body;
|
|
1617
2541
|
if (execute && service) {
|
|
1618
|
-
console.log(`[MoltsPay] /proxy: Executing skill
|
|
2542
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
1619
2543
|
const skill = this.skills.get(service);
|
|
1620
2544
|
if (!skill) {
|
|
1621
|
-
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
1622
2545
|
return this.sendJson(res, 404, {
|
|
1623
2546
|
success: false,
|
|
1624
|
-
paymentSettled:
|
|
2547
|
+
paymentSettled: true,
|
|
2548
|
+
// Payment already happened on Tempo
|
|
1625
2549
|
error: `Service not found: ${service}`
|
|
1626
2550
|
});
|
|
1627
2551
|
}
|
|
@@ -1634,73 +2558,36 @@ var MoltsPayServer = class {
|
|
|
1634
2558
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1635
2559
|
)
|
|
1636
2560
|
]);
|
|
1637
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
|
|
1638
2561
|
} catch (err) {
|
|
1639
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
2562
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
1640
2563
|
return this.sendJson(res, 500, {
|
|
1641
2564
|
success: false,
|
|
1642
|
-
paymentSettled:
|
|
2565
|
+
paymentSettled: true,
|
|
1643
2566
|
error: `Service execution failed: ${err.message}`
|
|
1644
2567
|
});
|
|
1645
2568
|
}
|
|
1646
|
-
let settlement2 = null;
|
|
1647
|
-
try {
|
|
1648
|
-
settlement2 = await this.registry.settle(payment, requirements);
|
|
1649
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
1650
|
-
} catch (err) {
|
|
1651
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
1652
|
-
return this.sendJson(res, 200, {
|
|
1653
|
-
success: true,
|
|
1654
|
-
verified: true,
|
|
1655
|
-
settled: false,
|
|
1656
|
-
settlementError: err.message,
|
|
1657
|
-
from: payment.payload?.authorization?.from,
|
|
1658
|
-
// Buyer's wallet address
|
|
1659
|
-
paidTo: wallet,
|
|
1660
|
-
amount: amountNum,
|
|
1661
|
-
currency: currency || "USDC",
|
|
1662
|
-
memo,
|
|
1663
|
-
result
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
2569
|
return this.sendJson(res, 200, {
|
|
1667
2570
|
success: true,
|
|
1668
2571
|
verified: true,
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
from: payment.payload?.authorization?.from,
|
|
1672
|
-
// Buyer's wallet address
|
|
2572
|
+
txHash,
|
|
2573
|
+
chain: "tempo_moderato",
|
|
1673
2574
|
paidTo: wallet,
|
|
1674
2575
|
amount: amountNum,
|
|
1675
|
-
currency:
|
|
1676
|
-
facilitator:
|
|
2576
|
+
currency: "USDC",
|
|
2577
|
+
facilitator: verification.facilitator,
|
|
1677
2578
|
memo,
|
|
1678
2579
|
result
|
|
1679
2580
|
});
|
|
1680
2581
|
}
|
|
1681
|
-
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
1682
|
-
let settlement = null;
|
|
1683
|
-
try {
|
|
1684
|
-
settlement = await this.registry.settle(payment, requirements);
|
|
1685
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1686
|
-
} catch (err) {
|
|
1687
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
1688
|
-
return this.sendJson(res, 500, {
|
|
1689
|
-
success: false,
|
|
1690
|
-
error: `Settlement failed: ${err.message}`
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
2582
|
this.sendJson(res, 200, {
|
|
1694
2583
|
success: true,
|
|
1695
2584
|
verified: true,
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
from: payment.payload?.authorization?.from,
|
|
1699
|
-
// Buyer's wallet address
|
|
2585
|
+
txHash,
|
|
2586
|
+
chain: "tempo_moderato",
|
|
1700
2587
|
paidTo: wallet,
|
|
1701
2588
|
amount: amountNum,
|
|
1702
|
-
currency:
|
|
1703
|
-
facilitator:
|
|
2589
|
+
currency: "USDC",
|
|
2590
|
+
facilitator: verification.facilitator,
|
|
1704
2591
|
memo
|
|
1705
2592
|
});
|
|
1706
2593
|
}
|
|
@@ -1715,7 +2602,7 @@ var MoltsPayServer = class {
|
|
|
1715
2602
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1716
2603
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1717
2604
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1718
|
-
|
|
2605
|
+
const requirements = {
|
|
1719
2606
|
scheme: "exact",
|
|
1720
2607
|
network: networkId,
|
|
1721
2608
|
asset: tokenAddress,
|
|
@@ -1725,6 +2612,17 @@ var MoltsPayServer = class {
|
|
|
1725
2612
|
maxTimeoutSeconds: 300,
|
|
1726
2613
|
extra: tokenDomain
|
|
1727
2614
|
};
|
|
2615
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
2616
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2617
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2618
|
+
if (spenderAddress) {
|
|
2619
|
+
requirements.extra = {
|
|
2620
|
+
...requirements.extra || {},
|
|
2621
|
+
bnbSpender: spenderAddress
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return requirements;
|
|
1728
2626
|
}
|
|
1729
2627
|
/**
|
|
1730
2628
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -1755,10 +2653,40 @@ var MoltsPayServer = class {
|
|
|
1755
2653
|
};
|
|
1756
2654
|
|
|
1757
2655
|
// src/client/index.ts
|
|
2656
|
+
var import_fs4 = require("fs");
|
|
2657
|
+
var import_os2 = require("os");
|
|
2658
|
+
var import_path2 = require("path");
|
|
2659
|
+
var import_ethers = require("ethers");
|
|
2660
|
+
|
|
2661
|
+
// src/wallet/solana.ts
|
|
2662
|
+
var import_web34 = require("@solana/web3.js");
|
|
2663
|
+
var import_spl_token2 = require("@solana/spl-token");
|
|
1758
2664
|
var import_fs3 = require("fs");
|
|
1759
|
-
var import_os = require("os");
|
|
1760
2665
|
var import_path = require("path");
|
|
1761
|
-
var
|
|
2666
|
+
var import_os = require("os");
|
|
2667
|
+
var import_bs582 = __toESM(require("bs58"));
|
|
2668
|
+
var DEFAULT_CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
|
|
2669
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
2670
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
2671
|
+
return (0, import_path.join)(configDir, SOLANA_WALLET_FILE);
|
|
2672
|
+
}
|
|
2673
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
2674
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
2675
|
+
if (!(0, import_fs3.existsSync)(walletPath)) {
|
|
2676
|
+
return null;
|
|
2677
|
+
}
|
|
2678
|
+
try {
|
|
2679
|
+
const data = JSON.parse((0, import_fs3.readFileSync)(walletPath, "utf-8"));
|
|
2680
|
+
const secretKey = import_bs582.default.decode(data.secretKey);
|
|
2681
|
+
return import_web34.Keypair.fromSecretKey(secretKey);
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
console.error("Failed to load Solana wallet:", error);
|
|
2684
|
+
return null;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// src/client/index.ts
|
|
2689
|
+
var import_web35 = require("@solana/web3.js");
|
|
1762
2690
|
var X402_VERSION3 = 2;
|
|
1763
2691
|
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
1764
2692
|
var PAYMENT_HEADER2 = "x-payment";
|
|
@@ -1777,7 +2705,7 @@ var MoltsPayClient = class {
|
|
|
1777
2705
|
todaySpending = 0;
|
|
1778
2706
|
lastSpendingReset = 0;
|
|
1779
2707
|
constructor(options = {}) {
|
|
1780
|
-
this.configDir = options.configDir || (0,
|
|
2708
|
+
this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
1781
2709
|
this.config = this.loadConfig();
|
|
1782
2710
|
this.walletData = this.loadWallet();
|
|
1783
2711
|
this.loadSpending();
|
|
@@ -1797,6 +2725,12 @@ var MoltsPayClient = class {
|
|
|
1797
2725
|
get address() {
|
|
1798
2726
|
return this.wallet?.address || null;
|
|
1799
2727
|
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Get wallet instance (for direct operations like approvals)
|
|
2730
|
+
*/
|
|
2731
|
+
getWallet() {
|
|
2732
|
+
return this.wallet;
|
|
2733
|
+
}
|
|
1800
2734
|
/**
|
|
1801
2735
|
* Get current config
|
|
1802
2736
|
*/
|
|
@@ -1866,9 +2800,14 @@ var MoltsPayClient = class {
|
|
|
1866
2800
|
}
|
|
1867
2801
|
throw new Error(data.error || "Unexpected response");
|
|
1868
2802
|
}
|
|
2803
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
1869
2804
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER2);
|
|
2805
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
2806
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
2807
|
+
return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
|
|
2808
|
+
}
|
|
1870
2809
|
if (!paymentRequiredHeader) {
|
|
1871
|
-
throw new Error("Missing x-payment-required
|
|
2810
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
1872
2811
|
}
|
|
1873
2812
|
let requirements;
|
|
1874
2813
|
try {
|
|
@@ -1885,17 +2824,22 @@ var MoltsPayClient = class {
|
|
|
1885
2824
|
throw new Error("Invalid x-payment-required header");
|
|
1886
2825
|
}
|
|
1887
2826
|
const networkToChainName = (network2) => {
|
|
2827
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
2828
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
1888
2829
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
1889
2830
|
if (!match) return null;
|
|
1890
2831
|
const chainId = parseInt(match[1]);
|
|
1891
2832
|
if (chainId === 8453) return "base";
|
|
1892
2833
|
if (chainId === 137) return "polygon";
|
|
1893
2834
|
if (chainId === 84532) return "base_sepolia";
|
|
2835
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
2836
|
+
if (chainId === 56) return "bnb";
|
|
2837
|
+
if (chainId === 97) return "bnb_testnet";
|
|
1894
2838
|
return null;
|
|
1895
2839
|
};
|
|
1896
2840
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
1897
|
-
let chainName;
|
|
1898
2841
|
const userSpecifiedChain = options.chain;
|
|
2842
|
+
let selectedChain;
|
|
1899
2843
|
if (userSpecifiedChain) {
|
|
1900
2844
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
1901
2845
|
throw new Error(
|
|
@@ -1903,17 +2847,27 @@ var MoltsPayClient = class {
|
|
|
1903
2847
|
Server accepts: ${serverChains.join(", ")}`
|
|
1904
2848
|
);
|
|
1905
2849
|
}
|
|
1906
|
-
|
|
2850
|
+
selectedChain = userSpecifiedChain;
|
|
1907
2851
|
} else {
|
|
1908
2852
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
1909
|
-
|
|
2853
|
+
selectedChain = "base";
|
|
1910
2854
|
} else {
|
|
1911
2855
|
throw new Error(
|
|
1912
2856
|
`Server accepts: ${serverChains.join(", ")}
|
|
1913
|
-
Please specify: --chain
|
|
2857
|
+
Please specify: --chain <chain_name>`
|
|
1914
2858
|
);
|
|
1915
2859
|
}
|
|
1916
2860
|
}
|
|
2861
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
2862
|
+
const solanaChain = selectedChain;
|
|
2863
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
2864
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
2865
|
+
if (!req2) {
|
|
2866
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
2867
|
+
}
|
|
2868
|
+
return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
|
|
2869
|
+
}
|
|
2870
|
+
const chainName = selectedChain;
|
|
1917
2871
|
const chain = getChain(chainName);
|
|
1918
2872
|
const network = `eip155:${chain.chainId}`;
|
|
1919
2873
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -1948,6 +2902,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
1948
2902
|
} else {
|
|
1949
2903
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
1950
2904
|
}
|
|
2905
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
2906
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
2907
|
+
const payTo2 = req.payTo || req.resource;
|
|
2908
|
+
if (!payTo2) {
|
|
2909
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
2910
|
+
}
|
|
2911
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
2912
|
+
if (!bnbSpender) {
|
|
2913
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
2914
|
+
}
|
|
2915
|
+
return await this.handleBNBPayment(serverUrl, service, params, {
|
|
2916
|
+
to: payTo2,
|
|
2917
|
+
amount,
|
|
2918
|
+
token,
|
|
2919
|
+
chainName,
|
|
2920
|
+
chain,
|
|
2921
|
+
spender: bnbSpender
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
1951
2924
|
const payTo = req.payTo || req.resource;
|
|
1952
2925
|
if (!payTo) {
|
|
1953
2926
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -1997,6 +2970,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
1997
2970
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
1998
2971
|
return result.result;
|
|
1999
2972
|
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
2975
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
2976
|
+
*/
|
|
2977
|
+
async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
|
|
2978
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
2979
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
2980
|
+
const { tempoModerato } = await import("viem/chains");
|
|
2981
|
+
const { Actions } = await import("viem/tempo");
|
|
2982
|
+
const privateKey = this.walletData.privateKey;
|
|
2983
|
+
const account = privateKeyToAccount2(privateKey);
|
|
2984
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
2985
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
2986
|
+
const parseAuthParam = (header, key) => {
|
|
2987
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
2988
|
+
return match ? match[1] : null;
|
|
2989
|
+
};
|
|
2990
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
2991
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
2992
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
2993
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
2994
|
+
if (method !== "tempo") {
|
|
2995
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
2996
|
+
}
|
|
2997
|
+
if (!requestB64) {
|
|
2998
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
2999
|
+
}
|
|
3000
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
3001
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
3002
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
3003
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
3004
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
3005
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
3006
|
+
this.checkLimits(amountDisplay);
|
|
3007
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
3008
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
3009
|
+
const publicClient = createPublicClient({
|
|
3010
|
+
chain: tempoChain,
|
|
3011
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
3012
|
+
});
|
|
3013
|
+
const walletClient = createWalletClient({
|
|
3014
|
+
account,
|
|
3015
|
+
chain: tempoChain,
|
|
3016
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
3017
|
+
});
|
|
3018
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
3019
|
+
to: recipient,
|
|
3020
|
+
amount: BigInt(amount),
|
|
3021
|
+
token: currency
|
|
3022
|
+
});
|
|
3023
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
3024
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
3025
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
3026
|
+
const credential = {
|
|
3027
|
+
challenge: {
|
|
3028
|
+
id: challengeId,
|
|
3029
|
+
realm,
|
|
3030
|
+
method: "tempo",
|
|
3031
|
+
intent: "charge",
|
|
3032
|
+
request: paymentRequest
|
|
3033
|
+
},
|
|
3034
|
+
payload: { hash: txHash, type: "hash" },
|
|
3035
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
3036
|
+
};
|
|
3037
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3038
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
3039
|
+
method: "POST",
|
|
3040
|
+
headers: {
|
|
3041
|
+
"Content-Type": "application/json",
|
|
3042
|
+
"Authorization": `Payment ${credentialB64}`
|
|
3043
|
+
},
|
|
3044
|
+
body: JSON.stringify({ service, params, chain: "tempo_moderato" })
|
|
3045
|
+
});
|
|
3046
|
+
const result = await paidRes.json();
|
|
3047
|
+
if (!paidRes.ok) {
|
|
3048
|
+
throw new Error(result.error || "Payment verification failed");
|
|
3049
|
+
}
|
|
3050
|
+
this.recordSpending(amountDisplay);
|
|
3051
|
+
console.log(`[MoltsPay] Success!`);
|
|
3052
|
+
return result.result || result;
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
3056
|
+
*
|
|
3057
|
+
* Flow:
|
|
3058
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
3059
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
3060
|
+
* 3. Send intent to server
|
|
3061
|
+
* 4. Server executes service
|
|
3062
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
3063
|
+
*/
|
|
3064
|
+
async handleBNBPayment(serverUrl, service, params, paymentDetails) {
|
|
3065
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
3066
|
+
const tokenConfig = chain.tokens[token];
|
|
3067
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
3068
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
3069
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
3070
|
+
if (allowance < amountWeiCheck) {
|
|
3071
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
3072
|
+
const minGasBalance = import_ethers.ethers.parseEther("0.0005");
|
|
3073
|
+
if (nativeBalance < minGasBalance) {
|
|
3074
|
+
const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
3075
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
3076
|
+
if (isTestnet) {
|
|
3077
|
+
throw new Error(
|
|
3078
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
3079
|
+
|
|
3080
|
+
Current tBNB: ${nativeBNB}
|
|
3081
|
+
Required: ~0.001 tBNB
|
|
3082
|
+
|
|
3083
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
3084
|
+
(Gives USDC + tBNB for gas)`
|
|
3085
|
+
);
|
|
3086
|
+
} else {
|
|
3087
|
+
throw new Error(
|
|
3088
|
+
`\u274C Insufficient BNB for approval transaction
|
|
3089
|
+
|
|
3090
|
+
Current BNB: ${nativeBNB}
|
|
3091
|
+
Required: ~0.001 BNB (~$0.60)
|
|
3092
|
+
|
|
3093
|
+
To get BNB:
|
|
3094
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
3095
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
3096
|
+
|
|
3097
|
+
After funding, run:
|
|
3098
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
throw new Error(
|
|
3103
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
3104
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
3107
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
3108
|
+
const intent = {
|
|
3109
|
+
from: this.wallet.address,
|
|
3110
|
+
to,
|
|
3111
|
+
amount: amountWei,
|
|
3112
|
+
token: tokenConfig.address,
|
|
3113
|
+
service,
|
|
3114
|
+
nonce: Date.now(),
|
|
3115
|
+
// Use timestamp as nonce for simplicity
|
|
3116
|
+
deadline: Date.now() + 36e5
|
|
3117
|
+
// 1 hour
|
|
3118
|
+
};
|
|
3119
|
+
const domain = {
|
|
3120
|
+
name: "MoltsPay",
|
|
3121
|
+
version: "1",
|
|
3122
|
+
chainId: chain.chainId
|
|
3123
|
+
};
|
|
3124
|
+
const types = {
|
|
3125
|
+
PaymentIntent: [
|
|
3126
|
+
{ name: "from", type: "address" },
|
|
3127
|
+
{ name: "to", type: "address" },
|
|
3128
|
+
{ name: "amount", type: "uint256" },
|
|
3129
|
+
{ name: "token", type: "address" },
|
|
3130
|
+
{ name: "service", type: "string" },
|
|
3131
|
+
{ name: "nonce", type: "uint256" },
|
|
3132
|
+
{ name: "deadline", type: "uint256" }
|
|
3133
|
+
]
|
|
3134
|
+
};
|
|
3135
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
3136
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
3137
|
+
const network = `eip155:${chain.chainId}`;
|
|
3138
|
+
const payload = {
|
|
3139
|
+
x402Version: 2,
|
|
3140
|
+
scheme: "exact",
|
|
3141
|
+
network,
|
|
3142
|
+
payload: {
|
|
3143
|
+
intent: {
|
|
3144
|
+
...intent,
|
|
3145
|
+
signature
|
|
3146
|
+
},
|
|
3147
|
+
chainId: chain.chainId
|
|
3148
|
+
},
|
|
3149
|
+
accepted: {
|
|
3150
|
+
scheme: "exact",
|
|
3151
|
+
network,
|
|
3152
|
+
asset: tokenConfig.address,
|
|
3153
|
+
amount: amountWei,
|
|
3154
|
+
payTo: to,
|
|
3155
|
+
maxTimeoutSeconds: 300
|
|
3156
|
+
}
|
|
3157
|
+
};
|
|
3158
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
3159
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
3160
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
3161
|
+
method: "POST",
|
|
3162
|
+
headers: {
|
|
3163
|
+
"Content-Type": "application/json",
|
|
3164
|
+
"X-Payment": paymentHeader
|
|
3165
|
+
},
|
|
3166
|
+
body: JSON.stringify({ service, params, chain: chainName })
|
|
3167
|
+
});
|
|
3168
|
+
const result = await paidRes.json();
|
|
3169
|
+
if (!paidRes.ok) {
|
|
3170
|
+
throw new Error(result.error || "BNB payment failed");
|
|
3171
|
+
}
|
|
3172
|
+
this.recordSpending(amount);
|
|
3173
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
3174
|
+
return result.result || result;
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Handle Solana payment flow
|
|
3178
|
+
*
|
|
3179
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
3180
|
+
* 1. Client creates and signs a transfer transaction
|
|
3181
|
+
* 2. Server submits the transaction after service completes
|
|
3182
|
+
*/
|
|
3183
|
+
async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
|
|
3184
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
3185
|
+
if (!solanaWallet) {
|
|
3186
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
3187
|
+
}
|
|
3188
|
+
const amount = Number(requirements.amount);
|
|
3189
|
+
const amountUSDC = amount / 1e6;
|
|
3190
|
+
this.checkLimits(amountUSDC);
|
|
3191
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
3192
|
+
if (!requirements.payTo) {
|
|
3193
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
3194
|
+
}
|
|
3195
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
3196
|
+
const feePayerPubkey = solanaFeePayer ? new import_web35.PublicKey(solanaFeePayer) : void 0;
|
|
3197
|
+
if (feePayerPubkey) {
|
|
3198
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
3199
|
+
}
|
|
3200
|
+
const recipientPubkey = new import_web35.PublicKey(requirements.payTo);
|
|
3201
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
3202
|
+
solanaWallet.publicKey,
|
|
3203
|
+
recipientPubkey,
|
|
3204
|
+
BigInt(amount),
|
|
3205
|
+
chain,
|
|
3206
|
+
feePayerPubkey
|
|
3207
|
+
// Optional fee payer for gasless mode
|
|
3208
|
+
);
|
|
3209
|
+
if (feePayerPubkey) {
|
|
3210
|
+
transaction.partialSign(solanaWallet);
|
|
3211
|
+
} else {
|
|
3212
|
+
transaction.sign(solanaWallet);
|
|
3213
|
+
}
|
|
3214
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
3215
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
3216
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
3217
|
+
const payload = {
|
|
3218
|
+
x402Version: 2,
|
|
3219
|
+
scheme: "exact",
|
|
3220
|
+
network,
|
|
3221
|
+
payload: {
|
|
3222
|
+
signedTransaction: signedTx,
|
|
3223
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
3224
|
+
chain
|
|
3225
|
+
},
|
|
3226
|
+
accepted: {
|
|
3227
|
+
scheme: "exact",
|
|
3228
|
+
network,
|
|
3229
|
+
asset: requirements.asset,
|
|
3230
|
+
amount: requirements.amount,
|
|
3231
|
+
payTo: requirements.payTo,
|
|
3232
|
+
maxTimeoutSeconds: 300
|
|
3233
|
+
}
|
|
3234
|
+
};
|
|
3235
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
3236
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
3237
|
+
method: "POST",
|
|
3238
|
+
headers: {
|
|
3239
|
+
"Content-Type": "application/json",
|
|
3240
|
+
"X-Payment": paymentHeader
|
|
3241
|
+
},
|
|
3242
|
+
body: JSON.stringify({ service, params, chain })
|
|
3243
|
+
});
|
|
3244
|
+
const result = await paidRes.json();
|
|
3245
|
+
if (!paidRes.ok) {
|
|
3246
|
+
throw new Error(result.error || "Solana payment failed");
|
|
3247
|
+
}
|
|
3248
|
+
this.recordSpending(amountUSDC);
|
|
3249
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
3250
|
+
if (result.payment?.transaction) {
|
|
3251
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
3252
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
3253
|
+
}
|
|
3254
|
+
return result.result || result;
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Check ERC20 allowance for a spender
|
|
3258
|
+
*/
|
|
3259
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
3260
|
+
const contract = new import_ethers.ethers.Contract(
|
|
3261
|
+
tokenAddress,
|
|
3262
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
3263
|
+
provider
|
|
3264
|
+
);
|
|
3265
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
3266
|
+
}
|
|
2000
3267
|
/**
|
|
2001
3268
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
2002
3269
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -2067,26 +3334,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2067
3334
|
}
|
|
2068
3335
|
// --- Config & Wallet Management ---
|
|
2069
3336
|
loadConfig() {
|
|
2070
|
-
const configPath = (0,
|
|
2071
|
-
if ((0,
|
|
2072
|
-
const content = (0,
|
|
3337
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
3338
|
+
if ((0, import_fs4.existsSync)(configPath)) {
|
|
3339
|
+
const content = (0, import_fs4.readFileSync)(configPath, "utf-8");
|
|
2073
3340
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
2074
3341
|
}
|
|
2075
3342
|
return { ...DEFAULT_CONFIG };
|
|
2076
3343
|
}
|
|
2077
3344
|
saveConfig() {
|
|
2078
|
-
(0,
|
|
2079
|
-
const configPath = (0,
|
|
2080
|
-
(0,
|
|
3345
|
+
(0, import_fs4.mkdirSync)(this.configDir, { recursive: true });
|
|
3346
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
3347
|
+
(0, import_fs4.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
2081
3348
|
}
|
|
2082
3349
|
/**
|
|
2083
3350
|
* Load spending data from disk
|
|
2084
3351
|
*/
|
|
2085
3352
|
loadSpending() {
|
|
2086
|
-
const spendingPath = (0,
|
|
2087
|
-
if ((0,
|
|
3353
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
3354
|
+
if ((0, import_fs4.existsSync)(spendingPath)) {
|
|
2088
3355
|
try {
|
|
2089
|
-
const data = JSON.parse((0,
|
|
3356
|
+
const data = JSON.parse((0, import_fs4.readFileSync)(spendingPath, "utf-8"));
|
|
2090
3357
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
2091
3358
|
if (data.date && data.date === today) {
|
|
2092
3359
|
this.todaySpending = data.amount || 0;
|
|
@@ -2105,29 +3372,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2105
3372
|
* Save spending data to disk
|
|
2106
3373
|
*/
|
|
2107
3374
|
saveSpending() {
|
|
2108
|
-
(0,
|
|
2109
|
-
const spendingPath = (0,
|
|
3375
|
+
(0, import_fs4.mkdirSync)(this.configDir, { recursive: true });
|
|
3376
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
2110
3377
|
const data = {
|
|
2111
3378
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
2112
3379
|
amount: this.todaySpending,
|
|
2113
3380
|
updatedAt: Date.now()
|
|
2114
3381
|
};
|
|
2115
|
-
(0,
|
|
3382
|
+
(0, import_fs4.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
2116
3383
|
}
|
|
2117
3384
|
loadWallet() {
|
|
2118
|
-
const walletPath = (0,
|
|
2119
|
-
if ((0,
|
|
3385
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
3386
|
+
if ((0, import_fs4.existsSync)(walletPath)) {
|
|
2120
3387
|
try {
|
|
2121
|
-
const stats = (0,
|
|
3388
|
+
const stats = (0, import_fs4.statSync)(walletPath);
|
|
2122
3389
|
const mode = stats.mode & 511;
|
|
2123
3390
|
if (mode !== 384) {
|
|
2124
3391
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
2125
3392
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
2126
|
-
(0,
|
|
3393
|
+
(0, import_fs4.chmodSync)(walletPath, 384);
|
|
2127
3394
|
}
|
|
2128
3395
|
} catch (err) {
|
|
2129
3396
|
}
|
|
2130
|
-
const content = (0,
|
|
3397
|
+
const content = (0, import_fs4.readFileSync)(walletPath, "utf-8");
|
|
2131
3398
|
return JSON.parse(content);
|
|
2132
3399
|
}
|
|
2133
3400
|
return null;
|
|
@@ -2136,15 +3403,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2136
3403
|
* Initialize a new wallet (called by CLI)
|
|
2137
3404
|
*/
|
|
2138
3405
|
static init(configDir, options) {
|
|
2139
|
-
(0,
|
|
3406
|
+
(0, import_fs4.mkdirSync)(configDir, { recursive: true });
|
|
2140
3407
|
const wallet = import_ethers.Wallet.createRandom();
|
|
2141
3408
|
const walletData = {
|
|
2142
3409
|
address: wallet.address,
|
|
2143
3410
|
privateKey: wallet.privateKey,
|
|
2144
3411
|
createdAt: Date.now()
|
|
2145
3412
|
};
|
|
2146
|
-
const walletPath = (0,
|
|
2147
|
-
(0,
|
|
3413
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
3414
|
+
(0, import_fs4.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
2148
3415
|
const config = {
|
|
2149
3416
|
chain: options.chain,
|
|
2150
3417
|
limits: {
|
|
@@ -2152,8 +3419,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2152
3419
|
maxPerDay: options.maxPerDay
|
|
2153
3420
|
}
|
|
2154
3421
|
};
|
|
2155
|
-
const configPath = (0,
|
|
2156
|
-
(0,
|
|
3422
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
3423
|
+
(0, import_fs4.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
2157
3424
|
return { address: wallet.address, configDir };
|
|
2158
3425
|
}
|
|
2159
3426
|
/**
|
|
@@ -2189,7 +3456,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2189
3456
|
if (!this.wallet) {
|
|
2190
3457
|
throw new Error("Client not initialized");
|
|
2191
3458
|
}
|
|
2192
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
3459
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
2193
3460
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
2194
3461
|
const results = {};
|
|
2195
3462
|
const tempoTokens = {
|
|
@@ -2260,12 +3527,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2260
3527
|
if (!this.wallet || !this.walletData) {
|
|
2261
3528
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
2262
3529
|
}
|
|
2263
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
3530
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
2264
3531
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
2265
3532
|
const { tempoModerato } = await import("viem/chains");
|
|
2266
3533
|
const { Actions } = await import("viem/tempo");
|
|
2267
3534
|
const privateKey = this.walletData.privateKey;
|
|
2268
|
-
const account =
|
|
3535
|
+
const account = privateKeyToAccount2(privateKey);
|
|
2269
3536
|
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
2270
3537
|
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
2271
3538
|
const initResponse = await fetch(url, {
|
|
@@ -2365,10 +3632,10 @@ var import_ethers2 = require("ethers");
|
|
|
2365
3632
|
|
|
2366
3633
|
// src/wallet/createWallet.ts
|
|
2367
3634
|
var import_ethers3 = require("ethers");
|
|
2368
|
-
var
|
|
2369
|
-
var
|
|
3635
|
+
var import_fs5 = require("fs");
|
|
3636
|
+
var import_path3 = require("path");
|
|
2370
3637
|
var import_crypto = require("crypto");
|
|
2371
|
-
var DEFAULT_STORAGE_DIR = (0,
|
|
3638
|
+
var DEFAULT_STORAGE_DIR = (0, import_path3.join)(process.env.HOME || "~", ".moltspay");
|
|
2372
3639
|
var DEFAULT_STORAGE_FILE = "wallet.json";
|
|
2373
3640
|
function encryptPrivateKey(privateKey, password) {
|
|
2374
3641
|
const salt = (0, import_crypto.randomBytes)(16);
|
|
@@ -2391,10 +3658,10 @@ function decryptPrivateKey(encrypted, password, iv, salt) {
|
|
|
2391
3658
|
return decrypted;
|
|
2392
3659
|
}
|
|
2393
3660
|
function createWallet(options = {}) {
|
|
2394
|
-
const storagePath = options.storagePath || (0,
|
|
2395
|
-
if ((0,
|
|
3661
|
+
const storagePath = options.storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3662
|
+
if ((0, import_fs5.existsSync)(storagePath) && !options.overwrite) {
|
|
2396
3663
|
try {
|
|
2397
|
-
const existing = JSON.parse((0,
|
|
3664
|
+
const existing = JSON.parse((0, import_fs5.readFileSync)(storagePath, "utf8"));
|
|
2398
3665
|
return {
|
|
2399
3666
|
success: true,
|
|
2400
3667
|
address: existing.address,
|
|
@@ -2425,11 +3692,11 @@ function createWallet(options = {}) {
|
|
|
2425
3692
|
} else {
|
|
2426
3693
|
walletData.privateKey = wallet.privateKey;
|
|
2427
3694
|
}
|
|
2428
|
-
const dir = (0,
|
|
2429
|
-
if (!(0,
|
|
2430
|
-
(0,
|
|
3695
|
+
const dir = (0, import_path3.dirname)(storagePath);
|
|
3696
|
+
if (!(0, import_fs5.existsSync)(dir)) {
|
|
3697
|
+
(0, import_fs5.mkdirSync)(dir, { recursive: true });
|
|
2431
3698
|
}
|
|
2432
|
-
(0,
|
|
3699
|
+
(0, import_fs5.writeFileSync)(storagePath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
2433
3700
|
return {
|
|
2434
3701
|
success: true,
|
|
2435
3702
|
address: wallet.address,
|
|
@@ -2444,12 +3711,12 @@ function createWallet(options = {}) {
|
|
|
2444
3711
|
}
|
|
2445
3712
|
}
|
|
2446
3713
|
function loadWallet(options = {}) {
|
|
2447
|
-
const storagePath = options.storagePath || (0,
|
|
2448
|
-
if (!(0,
|
|
3714
|
+
const storagePath = options.storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3715
|
+
if (!(0, import_fs5.existsSync)(storagePath)) {
|
|
2449
3716
|
return { success: false, error: "Wallet not found. Run createWallet() first." };
|
|
2450
3717
|
}
|
|
2451
3718
|
try {
|
|
2452
|
-
const data = JSON.parse((0,
|
|
3719
|
+
const data = JSON.parse((0, import_fs5.readFileSync)(storagePath, "utf8"));
|
|
2453
3720
|
if (data.encrypted) {
|
|
2454
3721
|
if (!options.password) {
|
|
2455
3722
|
return { success: false, error: "Wallet is encrypted. Password required." };
|
|
@@ -2464,25 +3731,25 @@ function loadWallet(options = {}) {
|
|
|
2464
3731
|
}
|
|
2465
3732
|
}
|
|
2466
3733
|
function getWalletAddress(storagePath) {
|
|
2467
|
-
const path4 = storagePath || (0,
|
|
2468
|
-
if (!(0,
|
|
3734
|
+
const path4 = storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3735
|
+
if (!(0, import_fs5.existsSync)(path4)) {
|
|
2469
3736
|
return null;
|
|
2470
3737
|
}
|
|
2471
3738
|
try {
|
|
2472
|
-
const data = JSON.parse((0,
|
|
3739
|
+
const data = JSON.parse((0, import_fs5.readFileSync)(path4, "utf8"));
|
|
2473
3740
|
return data.address;
|
|
2474
3741
|
} catch {
|
|
2475
3742
|
return null;
|
|
2476
3743
|
}
|
|
2477
3744
|
}
|
|
2478
3745
|
function walletExists(storagePath) {
|
|
2479
|
-
const path4 = storagePath || (0,
|
|
2480
|
-
return (0,
|
|
3746
|
+
const path4 = storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3747
|
+
return (0, import_fs5.existsSync)(path4);
|
|
2481
3748
|
}
|
|
2482
3749
|
|
|
2483
3750
|
// src/verify/index.ts
|
|
2484
3751
|
var import_ethers4 = require("ethers");
|
|
2485
|
-
var
|
|
3752
|
+
var TRANSFER_EVENT_TOPIC3 = import_ethers4.ethers.id("Transfer(address,address,uint256)");
|
|
2486
3753
|
async function verifyPayment(params) {
|
|
2487
3754
|
const { txHash, expectedAmount, expectedTo, expectedToken } = params;
|
|
2488
3755
|
let chain;
|
|
@@ -2523,7 +3790,7 @@ async function verifyPayment(params) {
|
|
|
2523
3790
|
if (!detectedToken) {
|
|
2524
3791
|
continue;
|
|
2525
3792
|
}
|
|
2526
|
-
if (log.topics.length < 3 || log.topics[0] !==
|
|
3793
|
+
if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC3) {
|
|
2527
3794
|
continue;
|
|
2528
3795
|
}
|
|
2529
3796
|
const from = "0x" + log.topics[1].slice(-40);
|