moltspay 1.3.0 → 1.4.1
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/.env.example +14 -0
- package/README.md +319 -89
- 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 +2021 -285
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2023 -277
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +39 -3
- package/dist/client/index.d.ts +39 -3
- package/dist/client/index.js +563 -37
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +571 -35
- 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 +1440 -153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1448 -151
- 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 +909 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +919 -54
- 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 +5 -2
- 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
|
|
@@ -1531,8 +2253,10 @@ var MoltsPayServer = class {
|
|
|
1531
2253
|
isProxyAllowed(clientIP) {
|
|
1532
2254
|
const allowedIPs = process.env.PROXY_ALLOWED_IPS?.split(",").map((ip) => ip.trim()) || [];
|
|
1533
2255
|
if (allowedIPs.length === 0) {
|
|
1534
|
-
|
|
1535
|
-
|
|
2256
|
+
return true;
|
|
2257
|
+
}
|
|
2258
|
+
if (allowedIPs.includes("*")) {
|
|
2259
|
+
return true;
|
|
1536
2260
|
}
|
|
1537
2261
|
const normalizedIP = clientIP === "::1" ? "127.0.0.1" : clientIP.replace("::ffff:", "");
|
|
1538
2262
|
const allowed = allowedIPs.includes(normalizedIP) || allowedIPs.includes(clientIP);
|
|
@@ -1544,31 +2268,42 @@ var MoltsPayServer = class {
|
|
|
1544
2268
|
/**
|
|
1545
2269
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1546
2270
|
*
|
|
1547
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
2271
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1548
2272
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1549
2273
|
*
|
|
1550
2274
|
* Request body:
|
|
1551
2275
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1552
2276
|
*
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
2277
|
+
* For x402 (base, polygon, base_sepolia):
|
|
2278
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
2279
|
+
* With X-Payment header: verifies payment via CDP
|
|
2280
|
+
*
|
|
2281
|
+
* For MPP (tempo_moderato):
|
|
2282
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
2283
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1555
2284
|
*/
|
|
1556
|
-
async handleProxy(body, paymentHeader, res) {
|
|
2285
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1557
2286
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1558
2287
|
if (!wallet || !amount) {
|
|
1559
2288
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1560
2289
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
2290
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2291
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
2292
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2293
|
+
}
|
|
2294
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
2295
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
2296
|
+
const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
2297
|
+
if (isSolanaChain && !isValidSolanaAddress) {
|
|
2298
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
2299
|
+
}
|
|
2300
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
2301
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1563
2302
|
}
|
|
1564
2303
|
const amountNum = parseFloat(amount);
|
|
1565
2304
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1566
2305
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1567
2306
|
}
|
|
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
2307
|
const proxyConfig = {
|
|
1573
2308
|
id: serviceId || "proxy",
|
|
1574
2309
|
name: description || "Proxy Payment",
|
|
@@ -1580,6 +2315,9 @@ var MoltsPayServer = class {
|
|
|
1580
2315
|
input: {},
|
|
1581
2316
|
output: {}
|
|
1582
2317
|
};
|
|
2318
|
+
if (chain === "tempo_moderato") {
|
|
2319
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
2320
|
+
}
|
|
1583
2321
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1584
2322
|
if (!paymentHeader) {
|
|
1585
2323
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1591,37 +2329,225 @@ var MoltsPayServer = class {
|
|
|
1591
2329
|
} catch {
|
|
1592
2330
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
1593
2331
|
}
|
|
1594
|
-
if (payment.x402Version !== X402_VERSION2) {
|
|
1595
|
-
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
2332
|
+
if (payment.x402Version !== X402_VERSION2) {
|
|
2333
|
+
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
2334
|
+
}
|
|
2335
|
+
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2336
|
+
const network = payment.accepted?.network || payment.network;
|
|
2337
|
+
if (scheme !== "exact") {
|
|
2338
|
+
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
2339
|
+
}
|
|
2340
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
2341
|
+
if (network !== expectedNetwork) {
|
|
2342
|
+
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
2343
|
+
}
|
|
2344
|
+
console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
|
|
2345
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
2346
|
+
if (!verifyResult.valid) {
|
|
2347
|
+
return this.sendJson(res, 402, {
|
|
2348
|
+
success: false,
|
|
2349
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2350
|
+
facilitator: verifyResult.facilitator
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
2354
|
+
const { execute, service, params } = body;
|
|
2355
|
+
if (execute && service) {
|
|
2356
|
+
const skill = this.skills.get(service);
|
|
2357
|
+
if (!skill) {
|
|
2358
|
+
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
2359
|
+
return this.sendJson(res, 404, {
|
|
2360
|
+
success: false,
|
|
2361
|
+
paymentSettled: false,
|
|
2362
|
+
error: `Service not found: ${service}`
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
const isSolana = isSolanaNetwork(network);
|
|
2366
|
+
let settlement2 = null;
|
|
2367
|
+
if (isSolana) {
|
|
2368
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
2369
|
+
try {
|
|
2370
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2371
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2372
|
+
if (!settlement2.success) {
|
|
2373
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
2374
|
+
return this.sendJson(res, 402, {
|
|
2375
|
+
success: false,
|
|
2376
|
+
paymentSettled: false,
|
|
2377
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
} catch (err) {
|
|
2381
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
2382
|
+
return this.sendJson(res, 402, {
|
|
2383
|
+
success: false,
|
|
2384
|
+
paymentSettled: false,
|
|
2385
|
+
error: `Payment settlement failed: ${err.message}`
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
} else {
|
|
2389
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
2390
|
+
}
|
|
2391
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2392
|
+
let result;
|
|
2393
|
+
try {
|
|
2394
|
+
result = await Promise.race([
|
|
2395
|
+
skill.handler(params || {}),
|
|
2396
|
+
new Promise(
|
|
2397
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2398
|
+
)
|
|
2399
|
+
]);
|
|
2400
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
2401
|
+
} catch (err) {
|
|
2402
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
2403
|
+
return this.sendJson(res, 500, {
|
|
2404
|
+
success: false,
|
|
2405
|
+
paymentSettled: isSolana ? true : false,
|
|
2406
|
+
error: `Service execution failed: ${err.message}`,
|
|
2407
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
if (!isSolana) {
|
|
2411
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
2412
|
+
try {
|
|
2413
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2414
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2415
|
+
} catch (err) {
|
|
2416
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2417
|
+
return this.sendJson(res, 200, {
|
|
2418
|
+
success: true,
|
|
2419
|
+
verified: true,
|
|
2420
|
+
settled: false,
|
|
2421
|
+
settlementError: err.message,
|
|
2422
|
+
from: payment.payload?.authorization?.from,
|
|
2423
|
+
paidTo: wallet,
|
|
2424
|
+
amount: amountNum,
|
|
2425
|
+
currency: currency || "USDC",
|
|
2426
|
+
memo,
|
|
2427
|
+
result
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return this.sendJson(res, 200, {
|
|
2432
|
+
success: true,
|
|
2433
|
+
verified: true,
|
|
2434
|
+
settled: settlement2?.success || false,
|
|
2435
|
+
txHash: settlement2?.transaction,
|
|
2436
|
+
from: payment.payload?.authorization?.from,
|
|
2437
|
+
paidTo: wallet,
|
|
2438
|
+
amount: amountNum,
|
|
2439
|
+
currency: currency || "USDC",
|
|
2440
|
+
facilitator: settlement2?.facilitator,
|
|
2441
|
+
memo,
|
|
2442
|
+
result
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
2446
|
+
let settlement = null;
|
|
2447
|
+
try {
|
|
2448
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2449
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2450
|
+
} catch (err) {
|
|
2451
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2452
|
+
return this.sendJson(res, 500, {
|
|
2453
|
+
success: false,
|
|
2454
|
+
error: `Settlement failed: ${err.message}`
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
this.sendJson(res, 200, {
|
|
2458
|
+
success: true,
|
|
2459
|
+
verified: true,
|
|
2460
|
+
settled: settlement?.success || false,
|
|
2461
|
+
txHash: settlement?.transaction,
|
|
2462
|
+
from: payment.payload?.authorization?.from,
|
|
2463
|
+
// Buyer's wallet address
|
|
2464
|
+
paidTo: wallet,
|
|
2465
|
+
amount: amountNum,
|
|
2466
|
+
currency: currency || "USDC",
|
|
2467
|
+
facilitator: settlement?.facilitator,
|
|
2468
|
+
memo
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
2473
|
+
*/
|
|
2474
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
2475
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
2476
|
+
const amountNum = parseFloat(amount);
|
|
2477
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
2478
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
2479
|
+
const challengeId = this.generateChallengeId();
|
|
2480
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
2481
|
+
const mppRequest = {
|
|
2482
|
+
amount: amountInUnits,
|
|
2483
|
+
currency: tokenAddress,
|
|
2484
|
+
methodDetails: {
|
|
2485
|
+
chainId: 42431,
|
|
2486
|
+
feePayer: true
|
|
2487
|
+
},
|
|
2488
|
+
recipient: wallet
|
|
2489
|
+
};
|
|
2490
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2491
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2492
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2493
|
+
res.writeHead(402, {
|
|
2494
|
+
"Content-Type": "application/problem+json",
|
|
2495
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
2496
|
+
});
|
|
2497
|
+
res.end(JSON.stringify({
|
|
2498
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
2499
|
+
title: "Payment Required",
|
|
2500
|
+
status: 402,
|
|
2501
|
+
detail: `Payment is required (${config.name}).`,
|
|
2502
|
+
service: serviceId || "proxy",
|
|
2503
|
+
price: amountNum,
|
|
2504
|
+
currency: "USDC"
|
|
2505
|
+
}, null, 2));
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2509
|
+
if (!credentialMatch) {
|
|
2510
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
1596
2511
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
2512
|
+
let mppCredential;
|
|
2513
|
+
try {
|
|
2514
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2515
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2516
|
+
mppCredential = JSON.parse(decoded);
|
|
2517
|
+
} catch (err) {
|
|
2518
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
2519
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
1601
2520
|
}
|
|
1602
|
-
|
|
1603
|
-
if (
|
|
1604
|
-
|
|
2521
|
+
let txHash;
|
|
2522
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2523
|
+
txHash = mppCredential.payload.hash;
|
|
2524
|
+
} else {
|
|
2525
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
1605
2526
|
}
|
|
1606
|
-
console.log(`[MoltsPay] /proxy: Verifying
|
|
1607
|
-
const
|
|
1608
|
-
|
|
2527
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
2528
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
2529
|
+
const paymentPayload = {
|
|
2530
|
+
x402Version: X402_VERSION2,
|
|
2531
|
+
scheme: "exact",
|
|
2532
|
+
network: "eip155:42431",
|
|
2533
|
+
payload: { txHash, chainId: 42431 }
|
|
2534
|
+
};
|
|
2535
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
2536
|
+
if (!verification.valid) {
|
|
1609
2537
|
return this.sendJson(res, 402, {
|
|
1610
|
-
|
|
1611
|
-
error: `Payment verification failed: ${verifyResult.error}`,
|
|
1612
|
-
facilitator: verifyResult.facilitator
|
|
2538
|
+
error: `Payment verification failed: ${verification.error}`
|
|
1613
2539
|
});
|
|
1614
2540
|
}
|
|
1615
|
-
console.log(`[MoltsPay] /proxy:
|
|
2541
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
1616
2542
|
const { execute, service, params } = body;
|
|
1617
2543
|
if (execute && service) {
|
|
1618
|
-
console.log(`[MoltsPay] /proxy: Executing skill
|
|
2544
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
1619
2545
|
const skill = this.skills.get(service);
|
|
1620
2546
|
if (!skill) {
|
|
1621
|
-
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
1622
2547
|
return this.sendJson(res, 404, {
|
|
1623
2548
|
success: false,
|
|
1624
|
-
paymentSettled:
|
|
2549
|
+
paymentSettled: true,
|
|
2550
|
+
// Payment already happened on Tempo
|
|
1625
2551
|
error: `Service not found: ${service}`
|
|
1626
2552
|
});
|
|
1627
2553
|
}
|
|
@@ -1634,73 +2560,36 @@ var MoltsPayServer = class {
|
|
|
1634
2560
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1635
2561
|
)
|
|
1636
2562
|
]);
|
|
1637
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
|
|
1638
2563
|
} catch (err) {
|
|
1639
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
2564
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
1640
2565
|
return this.sendJson(res, 500, {
|
|
1641
2566
|
success: false,
|
|
1642
|
-
paymentSettled:
|
|
2567
|
+
paymentSettled: true,
|
|
1643
2568
|
error: `Service execution failed: ${err.message}`
|
|
1644
2569
|
});
|
|
1645
2570
|
}
|
|
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
2571
|
return this.sendJson(res, 200, {
|
|
1667
2572
|
success: true,
|
|
1668
2573
|
verified: true,
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
from: payment.payload?.authorization?.from,
|
|
1672
|
-
// Buyer's wallet address
|
|
2574
|
+
txHash,
|
|
2575
|
+
chain: "tempo_moderato",
|
|
1673
2576
|
paidTo: wallet,
|
|
1674
2577
|
amount: amountNum,
|
|
1675
|
-
currency:
|
|
1676
|
-
facilitator:
|
|
2578
|
+
currency: "USDC",
|
|
2579
|
+
facilitator: verification.facilitator,
|
|
1677
2580
|
memo,
|
|
1678
2581
|
result
|
|
1679
2582
|
});
|
|
1680
2583
|
}
|
|
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
2584
|
this.sendJson(res, 200, {
|
|
1694
2585
|
success: true,
|
|
1695
2586
|
verified: true,
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
from: payment.payload?.authorization?.from,
|
|
1699
|
-
// Buyer's wallet address
|
|
2587
|
+
txHash,
|
|
2588
|
+
chain: "tempo_moderato",
|
|
1700
2589
|
paidTo: wallet,
|
|
1701
2590
|
amount: amountNum,
|
|
1702
|
-
currency:
|
|
1703
|
-
facilitator:
|
|
2591
|
+
currency: "USDC",
|
|
2592
|
+
facilitator: verification.facilitator,
|
|
1704
2593
|
memo
|
|
1705
2594
|
});
|
|
1706
2595
|
}
|
|
@@ -1715,7 +2604,7 @@ var MoltsPayServer = class {
|
|
|
1715
2604
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1716
2605
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1717
2606
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1718
|
-
|
|
2607
|
+
const requirements = {
|
|
1719
2608
|
scheme: "exact",
|
|
1720
2609
|
network: networkId,
|
|
1721
2610
|
asset: tokenAddress,
|
|
@@ -1725,6 +2614,17 @@ var MoltsPayServer = class {
|
|
|
1725
2614
|
maxTimeoutSeconds: 300,
|
|
1726
2615
|
extra: tokenDomain
|
|
1727
2616
|
};
|
|
2617
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
2618
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2619
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2620
|
+
if (spenderAddress) {
|
|
2621
|
+
requirements.extra = {
|
|
2622
|
+
...requirements.extra || {},
|
|
2623
|
+
bnbSpender: spenderAddress
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
return requirements;
|
|
1728
2628
|
}
|
|
1729
2629
|
/**
|
|
1730
2630
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -1755,10 +2655,40 @@ var MoltsPayServer = class {
|
|
|
1755
2655
|
};
|
|
1756
2656
|
|
|
1757
2657
|
// src/client/index.ts
|
|
2658
|
+
var import_fs4 = require("fs");
|
|
2659
|
+
var import_os2 = require("os");
|
|
2660
|
+
var import_path2 = require("path");
|
|
2661
|
+
var import_ethers = require("ethers");
|
|
2662
|
+
|
|
2663
|
+
// src/wallet/solana.ts
|
|
2664
|
+
var import_web34 = require("@solana/web3.js");
|
|
2665
|
+
var import_spl_token2 = require("@solana/spl-token");
|
|
1758
2666
|
var import_fs3 = require("fs");
|
|
1759
|
-
var import_os = require("os");
|
|
1760
2667
|
var import_path = require("path");
|
|
1761
|
-
var
|
|
2668
|
+
var import_os = require("os");
|
|
2669
|
+
var import_bs582 = __toESM(require("bs58"));
|
|
2670
|
+
var DEFAULT_CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
|
|
2671
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
2672
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
2673
|
+
return (0, import_path.join)(configDir, SOLANA_WALLET_FILE);
|
|
2674
|
+
}
|
|
2675
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
2676
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
2677
|
+
if (!(0, import_fs3.existsSync)(walletPath)) {
|
|
2678
|
+
return null;
|
|
2679
|
+
}
|
|
2680
|
+
try {
|
|
2681
|
+
const data = JSON.parse((0, import_fs3.readFileSync)(walletPath, "utf-8"));
|
|
2682
|
+
const secretKey = import_bs582.default.decode(data.secretKey);
|
|
2683
|
+
return import_web34.Keypair.fromSecretKey(secretKey);
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
console.error("Failed to load Solana wallet:", error);
|
|
2686
|
+
return null;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// src/client/index.ts
|
|
2691
|
+
var import_web35 = require("@solana/web3.js");
|
|
1762
2692
|
var X402_VERSION3 = 2;
|
|
1763
2693
|
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
1764
2694
|
var PAYMENT_HEADER2 = "x-payment";
|
|
@@ -1777,7 +2707,7 @@ var MoltsPayClient = class {
|
|
|
1777
2707
|
todaySpending = 0;
|
|
1778
2708
|
lastSpendingReset = 0;
|
|
1779
2709
|
constructor(options = {}) {
|
|
1780
|
-
this.configDir = options.configDir || (0,
|
|
2710
|
+
this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
1781
2711
|
this.config = this.loadConfig();
|
|
1782
2712
|
this.walletData = this.loadWallet();
|
|
1783
2713
|
this.loadSpending();
|
|
@@ -1797,6 +2727,12 @@ var MoltsPayClient = class {
|
|
|
1797
2727
|
get address() {
|
|
1798
2728
|
return this.wallet?.address || null;
|
|
1799
2729
|
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Get wallet instance (for direct operations like approvals)
|
|
2732
|
+
*/
|
|
2733
|
+
getWallet() {
|
|
2734
|
+
return this.wallet;
|
|
2735
|
+
}
|
|
1800
2736
|
/**
|
|
1801
2737
|
* Get current config
|
|
1802
2738
|
*/
|
|
@@ -1850,11 +2786,26 @@ var MoltsPayClient = class {
|
|
|
1850
2786
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
1851
2787
|
}
|
|
1852
2788
|
console.log(`[MoltsPay] Requesting service: ${service}`);
|
|
1853
|
-
|
|
2789
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
2790
|
+
try {
|
|
2791
|
+
const services = await this.getServices(serverUrl);
|
|
2792
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
2793
|
+
if (svc?.endpoint) {
|
|
2794
|
+
executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
2795
|
+
console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
|
|
2796
|
+
}
|
|
2797
|
+
} catch {
|
|
2798
|
+
}
|
|
2799
|
+
let requestBody;
|
|
2800
|
+
if (options.rawData) {
|
|
2801
|
+
requestBody = { service, ...params };
|
|
2802
|
+
} else {
|
|
2803
|
+
requestBody = { service, params };
|
|
2804
|
+
}
|
|
1854
2805
|
if (options.chain) {
|
|
1855
2806
|
requestBody.chain = options.chain;
|
|
1856
2807
|
}
|
|
1857
|
-
const initialRes = await fetch(
|
|
2808
|
+
const initialRes = await fetch(executeUrl, {
|
|
1858
2809
|
method: "POST",
|
|
1859
2810
|
headers: { "Content-Type": "application/json" },
|
|
1860
2811
|
body: JSON.stringify(requestBody)
|
|
@@ -1866,9 +2817,14 @@ var MoltsPayClient = class {
|
|
|
1866
2817
|
}
|
|
1867
2818
|
throw new Error(data.error || "Unexpected response");
|
|
1868
2819
|
}
|
|
2820
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
1869
2821
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER2);
|
|
2822
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
2823
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
2824
|
+
return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
|
|
2825
|
+
}
|
|
1870
2826
|
if (!paymentRequiredHeader) {
|
|
1871
|
-
throw new Error("Missing x-payment-required
|
|
2827
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
1872
2828
|
}
|
|
1873
2829
|
let requirements;
|
|
1874
2830
|
try {
|
|
@@ -1885,17 +2841,22 @@ var MoltsPayClient = class {
|
|
|
1885
2841
|
throw new Error("Invalid x-payment-required header");
|
|
1886
2842
|
}
|
|
1887
2843
|
const networkToChainName = (network2) => {
|
|
2844
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
2845
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
1888
2846
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
1889
2847
|
if (!match) return null;
|
|
1890
2848
|
const chainId = parseInt(match[1]);
|
|
1891
2849
|
if (chainId === 8453) return "base";
|
|
1892
2850
|
if (chainId === 137) return "polygon";
|
|
1893
2851
|
if (chainId === 84532) return "base_sepolia";
|
|
2852
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
2853
|
+
if (chainId === 56) return "bnb";
|
|
2854
|
+
if (chainId === 97) return "bnb_testnet";
|
|
1894
2855
|
return null;
|
|
1895
2856
|
};
|
|
1896
2857
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
1897
|
-
let chainName;
|
|
1898
2858
|
const userSpecifiedChain = options.chain;
|
|
2859
|
+
let selectedChain;
|
|
1899
2860
|
if (userSpecifiedChain) {
|
|
1900
2861
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
1901
2862
|
throw new Error(
|
|
@@ -1903,17 +2864,27 @@ var MoltsPayClient = class {
|
|
|
1903
2864
|
Server accepts: ${serverChains.join(", ")}`
|
|
1904
2865
|
);
|
|
1905
2866
|
}
|
|
1906
|
-
|
|
2867
|
+
selectedChain = userSpecifiedChain;
|
|
1907
2868
|
} else {
|
|
1908
2869
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
1909
|
-
|
|
2870
|
+
selectedChain = "base";
|
|
1910
2871
|
} else {
|
|
1911
2872
|
throw new Error(
|
|
1912
2873
|
`Server accepts: ${serverChains.join(", ")}
|
|
1913
|
-
Please specify: --chain
|
|
2874
|
+
Please specify: --chain <chain_name>`
|
|
1914
2875
|
);
|
|
1915
2876
|
}
|
|
1916
2877
|
}
|
|
2878
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
2879
|
+
const solanaChain = selectedChain;
|
|
2880
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
2881
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
2882
|
+
if (!req2) {
|
|
2883
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
2884
|
+
}
|
|
2885
|
+
return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
|
|
2886
|
+
}
|
|
2887
|
+
const chainName = selectedChain;
|
|
1917
2888
|
const chain = getChain(chainName);
|
|
1918
2889
|
const network = `eip155:${chain.chainId}`;
|
|
1919
2890
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -1948,6 +2919,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
1948
2919
|
} else {
|
|
1949
2920
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
1950
2921
|
}
|
|
2922
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
2923
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
2924
|
+
const payTo2 = req.payTo || req.resource;
|
|
2925
|
+
if (!payTo2) {
|
|
2926
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
2927
|
+
}
|
|
2928
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
2929
|
+
if (!bnbSpender) {
|
|
2930
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
2931
|
+
}
|
|
2932
|
+
return await this.handleBNBPayment(executeUrl, service, params, {
|
|
2933
|
+
to: payTo2,
|
|
2934
|
+
amount,
|
|
2935
|
+
token,
|
|
2936
|
+
chainName,
|
|
2937
|
+
chain,
|
|
2938
|
+
spender: bnbSpender
|
|
2939
|
+
}, options);
|
|
2940
|
+
}
|
|
1951
2941
|
const payTo = req.payTo || req.resource;
|
|
1952
2942
|
if (!payTo) {
|
|
1953
2943
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -1977,11 +2967,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
1977
2967
|
};
|
|
1978
2968
|
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1979
2969
|
console.log(`[MoltsPay] Sending request with payment...`);
|
|
1980
|
-
const paidRequestBody = { service, params };
|
|
2970
|
+
const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
|
|
1981
2971
|
if (options.chain) {
|
|
1982
2972
|
paidRequestBody.chain = options.chain;
|
|
1983
2973
|
}
|
|
1984
|
-
const paidRes = await fetch(
|
|
2974
|
+
const paidRes = await fetch(executeUrl, {
|
|
1985
2975
|
method: "POST",
|
|
1986
2976
|
headers: {
|
|
1987
2977
|
"Content-Type": "application/json",
|
|
@@ -1995,7 +2985,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
1995
2985
|
}
|
|
1996
2986
|
this.recordSpending(amount);
|
|
1997
2987
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
1998
|
-
return result.result;
|
|
2988
|
+
return result.result || result;
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
2992
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
2993
|
+
*/
|
|
2994
|
+
async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
|
|
2995
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
2996
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
2997
|
+
const { tempoModerato } = await import("viem/chains");
|
|
2998
|
+
const { Actions } = await import("viem/tempo");
|
|
2999
|
+
const privateKey = this.walletData.privateKey;
|
|
3000
|
+
const account = privateKeyToAccount2(privateKey);
|
|
3001
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
3002
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
3003
|
+
const parseAuthParam = (header, key) => {
|
|
3004
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
3005
|
+
return match ? match[1] : null;
|
|
3006
|
+
};
|
|
3007
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
3008
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
3009
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
3010
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
3011
|
+
if (method !== "tempo") {
|
|
3012
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
3013
|
+
}
|
|
3014
|
+
if (!requestB64) {
|
|
3015
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
3016
|
+
}
|
|
3017
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
3018
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
3019
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
3020
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
3021
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
3022
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
3023
|
+
this.checkLimits(amountDisplay);
|
|
3024
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
3025
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
3026
|
+
const publicClient = createPublicClient({
|
|
3027
|
+
chain: tempoChain,
|
|
3028
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
3029
|
+
});
|
|
3030
|
+
const walletClient = createWalletClient({
|
|
3031
|
+
account,
|
|
3032
|
+
chain: tempoChain,
|
|
3033
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
3034
|
+
});
|
|
3035
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
3036
|
+
to: recipient,
|
|
3037
|
+
amount: BigInt(amount),
|
|
3038
|
+
token: currency
|
|
3039
|
+
});
|
|
3040
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
3041
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
3042
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
3043
|
+
const credential = {
|
|
3044
|
+
challenge: {
|
|
3045
|
+
id: challengeId,
|
|
3046
|
+
realm,
|
|
3047
|
+
method: "tempo",
|
|
3048
|
+
intent: "charge",
|
|
3049
|
+
request: paymentRequest
|
|
3050
|
+
},
|
|
3051
|
+
payload: { hash: txHash, type: "hash" },
|
|
3052
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
3053
|
+
};
|
|
3054
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3055
|
+
const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
|
|
3056
|
+
const paidRes = await fetch(executeUrl, {
|
|
3057
|
+
method: "POST",
|
|
3058
|
+
headers: {
|
|
3059
|
+
"Content-Type": "application/json",
|
|
3060
|
+
"Authorization": `Payment ${credentialB64}`
|
|
3061
|
+
},
|
|
3062
|
+
body: JSON.stringify(retryBody)
|
|
3063
|
+
});
|
|
3064
|
+
const result = await paidRes.json();
|
|
3065
|
+
if (!paidRes.ok) {
|
|
3066
|
+
throw new Error(result.error || "Payment verification failed");
|
|
3067
|
+
}
|
|
3068
|
+
this.recordSpending(amountDisplay);
|
|
3069
|
+
console.log(`[MoltsPay] Success!`);
|
|
3070
|
+
return result.result || result;
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
3074
|
+
*
|
|
3075
|
+
* Flow:
|
|
3076
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
3077
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
3078
|
+
* 3. Send intent to server
|
|
3079
|
+
* 4. Server executes service
|
|
3080
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
3081
|
+
*/
|
|
3082
|
+
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
3083
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
3084
|
+
const tokenConfig = chain.tokens[token];
|
|
3085
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
3086
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
3087
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
3088
|
+
if (allowance < amountWeiCheck) {
|
|
3089
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
3090
|
+
const minGasBalance = import_ethers.ethers.parseEther("0.0005");
|
|
3091
|
+
if (nativeBalance < minGasBalance) {
|
|
3092
|
+
const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
3093
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
3094
|
+
if (isTestnet) {
|
|
3095
|
+
throw new Error(
|
|
3096
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
3097
|
+
|
|
3098
|
+
Current tBNB: ${nativeBNB}
|
|
3099
|
+
Required: ~0.001 tBNB
|
|
3100
|
+
|
|
3101
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
3102
|
+
(Gives USDC + tBNB for gas)`
|
|
3103
|
+
);
|
|
3104
|
+
} else {
|
|
3105
|
+
throw new Error(
|
|
3106
|
+
`\u274C Insufficient BNB for approval transaction
|
|
3107
|
+
|
|
3108
|
+
Current BNB: ${nativeBNB}
|
|
3109
|
+
Required: ~0.001 BNB (~$0.60)
|
|
3110
|
+
|
|
3111
|
+
To get BNB:
|
|
3112
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
3113
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
3114
|
+
|
|
3115
|
+
After funding, run:
|
|
3116
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
throw new Error(
|
|
3121
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
3122
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
3126
|
+
const intent = {
|
|
3127
|
+
from: this.wallet.address,
|
|
3128
|
+
to,
|
|
3129
|
+
amount: amountWei,
|
|
3130
|
+
token: tokenConfig.address,
|
|
3131
|
+
service,
|
|
3132
|
+
nonce: Date.now(),
|
|
3133
|
+
// Use timestamp as nonce for simplicity
|
|
3134
|
+
deadline: Date.now() + 36e5
|
|
3135
|
+
// 1 hour
|
|
3136
|
+
};
|
|
3137
|
+
const domain = {
|
|
3138
|
+
name: "MoltsPay",
|
|
3139
|
+
version: "1",
|
|
3140
|
+
chainId: chain.chainId
|
|
3141
|
+
};
|
|
3142
|
+
const types = {
|
|
3143
|
+
PaymentIntent: [
|
|
3144
|
+
{ name: "from", type: "address" },
|
|
3145
|
+
{ name: "to", type: "address" },
|
|
3146
|
+
{ name: "amount", type: "uint256" },
|
|
3147
|
+
{ name: "token", type: "address" },
|
|
3148
|
+
{ name: "service", type: "string" },
|
|
3149
|
+
{ name: "nonce", type: "uint256" },
|
|
3150
|
+
{ name: "deadline", type: "uint256" }
|
|
3151
|
+
]
|
|
3152
|
+
};
|
|
3153
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
3154
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
3155
|
+
const network = `eip155:${chain.chainId}`;
|
|
3156
|
+
const payload = {
|
|
3157
|
+
x402Version: 2,
|
|
3158
|
+
scheme: "exact",
|
|
3159
|
+
network,
|
|
3160
|
+
payload: {
|
|
3161
|
+
intent: {
|
|
3162
|
+
...intent,
|
|
3163
|
+
signature
|
|
3164
|
+
},
|
|
3165
|
+
chainId: chain.chainId
|
|
3166
|
+
},
|
|
3167
|
+
accepted: {
|
|
3168
|
+
scheme: "exact",
|
|
3169
|
+
network,
|
|
3170
|
+
asset: tokenConfig.address,
|
|
3171
|
+
amount: amountWei,
|
|
3172
|
+
payTo: to,
|
|
3173
|
+
maxTimeoutSeconds: 300
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
3177
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
3178
|
+
const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
|
|
3179
|
+
const paidRes = await fetch(executeUrl, {
|
|
3180
|
+
method: "POST",
|
|
3181
|
+
headers: {
|
|
3182
|
+
"Content-Type": "application/json",
|
|
3183
|
+
"X-Payment": paymentHeader
|
|
3184
|
+
},
|
|
3185
|
+
body: JSON.stringify(bnbRequestBody)
|
|
3186
|
+
});
|
|
3187
|
+
const result = await paidRes.json();
|
|
3188
|
+
if (!paidRes.ok) {
|
|
3189
|
+
throw new Error(result.error || "BNB payment failed");
|
|
3190
|
+
}
|
|
3191
|
+
this.recordSpending(amount);
|
|
3192
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
3193
|
+
return result.result || result;
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Handle Solana payment flow
|
|
3197
|
+
*
|
|
3198
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
3199
|
+
* 1. Client creates and signs a transfer transaction
|
|
3200
|
+
* 2. Server submits the transaction after service completes
|
|
3201
|
+
*/
|
|
3202
|
+
async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
|
|
3203
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
3204
|
+
if (!solanaWallet) {
|
|
3205
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
3206
|
+
}
|
|
3207
|
+
const amount = Number(requirements.amount);
|
|
3208
|
+
const amountUSDC = amount / 1e6;
|
|
3209
|
+
this.checkLimits(amountUSDC);
|
|
3210
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
3211
|
+
if (!requirements.payTo) {
|
|
3212
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
3213
|
+
}
|
|
3214
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
3215
|
+
const feePayerPubkey = solanaFeePayer ? new import_web35.PublicKey(solanaFeePayer) : void 0;
|
|
3216
|
+
if (feePayerPubkey) {
|
|
3217
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
3218
|
+
}
|
|
3219
|
+
const recipientPubkey = new import_web35.PublicKey(requirements.payTo);
|
|
3220
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
3221
|
+
solanaWallet.publicKey,
|
|
3222
|
+
recipientPubkey,
|
|
3223
|
+
BigInt(amount),
|
|
3224
|
+
chain,
|
|
3225
|
+
feePayerPubkey
|
|
3226
|
+
// Optional fee payer for gasless mode
|
|
3227
|
+
);
|
|
3228
|
+
if (feePayerPubkey) {
|
|
3229
|
+
transaction.partialSign(solanaWallet);
|
|
3230
|
+
} else {
|
|
3231
|
+
transaction.sign(solanaWallet);
|
|
3232
|
+
}
|
|
3233
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
3234
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
3235
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
3236
|
+
const payload = {
|
|
3237
|
+
x402Version: 2,
|
|
3238
|
+
scheme: "exact",
|
|
3239
|
+
network,
|
|
3240
|
+
payload: {
|
|
3241
|
+
signedTransaction: signedTx,
|
|
3242
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
3243
|
+
chain
|
|
3244
|
+
},
|
|
3245
|
+
accepted: {
|
|
3246
|
+
scheme: "exact",
|
|
3247
|
+
network,
|
|
3248
|
+
asset: requirements.asset,
|
|
3249
|
+
amount: requirements.amount,
|
|
3250
|
+
payTo: requirements.payTo,
|
|
3251
|
+
maxTimeoutSeconds: 300
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
3255
|
+
const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
|
|
3256
|
+
const paidRes = await fetch(executeUrl, {
|
|
3257
|
+
method: "POST",
|
|
3258
|
+
headers: {
|
|
3259
|
+
"Content-Type": "application/json",
|
|
3260
|
+
"X-Payment": paymentHeader
|
|
3261
|
+
},
|
|
3262
|
+
body: JSON.stringify(solanaRequestBody)
|
|
3263
|
+
});
|
|
3264
|
+
const result = await paidRes.json();
|
|
3265
|
+
if (!paidRes.ok) {
|
|
3266
|
+
throw new Error(result.error || "Solana payment failed");
|
|
3267
|
+
}
|
|
3268
|
+
this.recordSpending(amountUSDC);
|
|
3269
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
3270
|
+
if (result.payment?.transaction) {
|
|
3271
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
3272
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
3273
|
+
}
|
|
3274
|
+
return result.result || result;
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Check ERC20 allowance for a spender
|
|
3278
|
+
*/
|
|
3279
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
3280
|
+
const contract = new import_ethers.ethers.Contract(
|
|
3281
|
+
tokenAddress,
|
|
3282
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
3283
|
+
provider
|
|
3284
|
+
);
|
|
3285
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
1999
3286
|
}
|
|
2000
3287
|
/**
|
|
2001
3288
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
@@ -2067,26 +3354,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2067
3354
|
}
|
|
2068
3355
|
// --- Config & Wallet Management ---
|
|
2069
3356
|
loadConfig() {
|
|
2070
|
-
const configPath = (0,
|
|
2071
|
-
if ((0,
|
|
2072
|
-
const content = (0,
|
|
3357
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
3358
|
+
if ((0, import_fs4.existsSync)(configPath)) {
|
|
3359
|
+
const content = (0, import_fs4.readFileSync)(configPath, "utf-8");
|
|
2073
3360
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
2074
3361
|
}
|
|
2075
3362
|
return { ...DEFAULT_CONFIG };
|
|
2076
3363
|
}
|
|
2077
3364
|
saveConfig() {
|
|
2078
|
-
(0,
|
|
2079
|
-
const configPath = (0,
|
|
2080
|
-
(0,
|
|
3365
|
+
(0, import_fs4.mkdirSync)(this.configDir, { recursive: true });
|
|
3366
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
3367
|
+
(0, import_fs4.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
2081
3368
|
}
|
|
2082
3369
|
/**
|
|
2083
3370
|
* Load spending data from disk
|
|
2084
3371
|
*/
|
|
2085
3372
|
loadSpending() {
|
|
2086
|
-
const spendingPath = (0,
|
|
2087
|
-
if ((0,
|
|
3373
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
3374
|
+
if ((0, import_fs4.existsSync)(spendingPath)) {
|
|
2088
3375
|
try {
|
|
2089
|
-
const data = JSON.parse((0,
|
|
3376
|
+
const data = JSON.parse((0, import_fs4.readFileSync)(spendingPath, "utf-8"));
|
|
2090
3377
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
2091
3378
|
if (data.date && data.date === today) {
|
|
2092
3379
|
this.todaySpending = data.amount || 0;
|
|
@@ -2105,29 +3392,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2105
3392
|
* Save spending data to disk
|
|
2106
3393
|
*/
|
|
2107
3394
|
saveSpending() {
|
|
2108
|
-
(0,
|
|
2109
|
-
const spendingPath = (0,
|
|
3395
|
+
(0, import_fs4.mkdirSync)(this.configDir, { recursive: true });
|
|
3396
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
2110
3397
|
const data = {
|
|
2111
3398
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
2112
3399
|
amount: this.todaySpending,
|
|
2113
3400
|
updatedAt: Date.now()
|
|
2114
3401
|
};
|
|
2115
|
-
(0,
|
|
3402
|
+
(0, import_fs4.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
2116
3403
|
}
|
|
2117
3404
|
loadWallet() {
|
|
2118
|
-
const walletPath = (0,
|
|
2119
|
-
if ((0,
|
|
3405
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
3406
|
+
if ((0, import_fs4.existsSync)(walletPath)) {
|
|
2120
3407
|
try {
|
|
2121
|
-
const stats = (0,
|
|
3408
|
+
const stats = (0, import_fs4.statSync)(walletPath);
|
|
2122
3409
|
const mode = stats.mode & 511;
|
|
2123
3410
|
if (mode !== 384) {
|
|
2124
3411
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
2125
3412
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
2126
|
-
(0,
|
|
3413
|
+
(0, import_fs4.chmodSync)(walletPath, 384);
|
|
2127
3414
|
}
|
|
2128
3415
|
} catch (err) {
|
|
2129
3416
|
}
|
|
2130
|
-
const content = (0,
|
|
3417
|
+
const content = (0, import_fs4.readFileSync)(walletPath, "utf-8");
|
|
2131
3418
|
return JSON.parse(content);
|
|
2132
3419
|
}
|
|
2133
3420
|
return null;
|
|
@@ -2136,15 +3423,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2136
3423
|
* Initialize a new wallet (called by CLI)
|
|
2137
3424
|
*/
|
|
2138
3425
|
static init(configDir, options) {
|
|
2139
|
-
(0,
|
|
3426
|
+
(0, import_fs4.mkdirSync)(configDir, { recursive: true });
|
|
2140
3427
|
const wallet = import_ethers.Wallet.createRandom();
|
|
2141
3428
|
const walletData = {
|
|
2142
3429
|
address: wallet.address,
|
|
2143
3430
|
privateKey: wallet.privateKey,
|
|
2144
3431
|
createdAt: Date.now()
|
|
2145
3432
|
};
|
|
2146
|
-
const walletPath = (0,
|
|
2147
|
-
(0,
|
|
3433
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
3434
|
+
(0, import_fs4.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
2148
3435
|
const config = {
|
|
2149
3436
|
chain: options.chain,
|
|
2150
3437
|
limits: {
|
|
@@ -2152,8 +3439,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2152
3439
|
maxPerDay: options.maxPerDay
|
|
2153
3440
|
}
|
|
2154
3441
|
};
|
|
2155
|
-
const configPath = (0,
|
|
2156
|
-
(0,
|
|
3442
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
3443
|
+
(0, import_fs4.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
2157
3444
|
return { address: wallet.address, configDir };
|
|
2158
3445
|
}
|
|
2159
3446
|
/**
|
|
@@ -2189,7 +3476,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2189
3476
|
if (!this.wallet) {
|
|
2190
3477
|
throw new Error("Client not initialized");
|
|
2191
3478
|
}
|
|
2192
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
3479
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
2193
3480
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
2194
3481
|
const results = {};
|
|
2195
3482
|
const tempoTokens = {
|
|
@@ -2260,12 +3547,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
2260
3547
|
if (!this.wallet || !this.walletData) {
|
|
2261
3548
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
2262
3549
|
}
|
|
2263
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
3550
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
2264
3551
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
2265
3552
|
const { tempoModerato } = await import("viem/chains");
|
|
2266
3553
|
const { Actions } = await import("viem/tempo");
|
|
2267
3554
|
const privateKey = this.walletData.privateKey;
|
|
2268
|
-
const account =
|
|
3555
|
+
const account = privateKeyToAccount2(privateKey);
|
|
2269
3556
|
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
2270
3557
|
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
2271
3558
|
const initResponse = await fetch(url, {
|
|
@@ -2365,10 +3652,10 @@ var import_ethers2 = require("ethers");
|
|
|
2365
3652
|
|
|
2366
3653
|
// src/wallet/createWallet.ts
|
|
2367
3654
|
var import_ethers3 = require("ethers");
|
|
2368
|
-
var
|
|
2369
|
-
var
|
|
3655
|
+
var import_fs5 = require("fs");
|
|
3656
|
+
var import_path3 = require("path");
|
|
2370
3657
|
var import_crypto = require("crypto");
|
|
2371
|
-
var DEFAULT_STORAGE_DIR = (0,
|
|
3658
|
+
var DEFAULT_STORAGE_DIR = (0, import_path3.join)(process.env.HOME || "~", ".moltspay");
|
|
2372
3659
|
var DEFAULT_STORAGE_FILE = "wallet.json";
|
|
2373
3660
|
function encryptPrivateKey(privateKey, password) {
|
|
2374
3661
|
const salt = (0, import_crypto.randomBytes)(16);
|
|
@@ -2391,10 +3678,10 @@ function decryptPrivateKey(encrypted, password, iv, salt) {
|
|
|
2391
3678
|
return decrypted;
|
|
2392
3679
|
}
|
|
2393
3680
|
function createWallet(options = {}) {
|
|
2394
|
-
const storagePath = options.storagePath || (0,
|
|
2395
|
-
if ((0,
|
|
3681
|
+
const storagePath = options.storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3682
|
+
if ((0, import_fs5.existsSync)(storagePath) && !options.overwrite) {
|
|
2396
3683
|
try {
|
|
2397
|
-
const existing = JSON.parse((0,
|
|
3684
|
+
const existing = JSON.parse((0, import_fs5.readFileSync)(storagePath, "utf8"));
|
|
2398
3685
|
return {
|
|
2399
3686
|
success: true,
|
|
2400
3687
|
address: existing.address,
|
|
@@ -2425,11 +3712,11 @@ function createWallet(options = {}) {
|
|
|
2425
3712
|
} else {
|
|
2426
3713
|
walletData.privateKey = wallet.privateKey;
|
|
2427
3714
|
}
|
|
2428
|
-
const dir = (0,
|
|
2429
|
-
if (!(0,
|
|
2430
|
-
(0,
|
|
3715
|
+
const dir = (0, import_path3.dirname)(storagePath);
|
|
3716
|
+
if (!(0, import_fs5.existsSync)(dir)) {
|
|
3717
|
+
(0, import_fs5.mkdirSync)(dir, { recursive: true });
|
|
2431
3718
|
}
|
|
2432
|
-
(0,
|
|
3719
|
+
(0, import_fs5.writeFileSync)(storagePath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
2433
3720
|
return {
|
|
2434
3721
|
success: true,
|
|
2435
3722
|
address: wallet.address,
|
|
@@ -2444,12 +3731,12 @@ function createWallet(options = {}) {
|
|
|
2444
3731
|
}
|
|
2445
3732
|
}
|
|
2446
3733
|
function loadWallet(options = {}) {
|
|
2447
|
-
const storagePath = options.storagePath || (0,
|
|
2448
|
-
if (!(0,
|
|
3734
|
+
const storagePath = options.storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3735
|
+
if (!(0, import_fs5.existsSync)(storagePath)) {
|
|
2449
3736
|
return { success: false, error: "Wallet not found. Run createWallet() first." };
|
|
2450
3737
|
}
|
|
2451
3738
|
try {
|
|
2452
|
-
const data = JSON.parse((0,
|
|
3739
|
+
const data = JSON.parse((0, import_fs5.readFileSync)(storagePath, "utf8"));
|
|
2453
3740
|
if (data.encrypted) {
|
|
2454
3741
|
if (!options.password) {
|
|
2455
3742
|
return { success: false, error: "Wallet is encrypted. Password required." };
|
|
@@ -2464,25 +3751,25 @@ function loadWallet(options = {}) {
|
|
|
2464
3751
|
}
|
|
2465
3752
|
}
|
|
2466
3753
|
function getWalletAddress(storagePath) {
|
|
2467
|
-
const path4 = storagePath || (0,
|
|
2468
|
-
if (!(0,
|
|
3754
|
+
const path4 = storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3755
|
+
if (!(0, import_fs5.existsSync)(path4)) {
|
|
2469
3756
|
return null;
|
|
2470
3757
|
}
|
|
2471
3758
|
try {
|
|
2472
|
-
const data = JSON.parse((0,
|
|
3759
|
+
const data = JSON.parse((0, import_fs5.readFileSync)(path4, "utf8"));
|
|
2473
3760
|
return data.address;
|
|
2474
3761
|
} catch {
|
|
2475
3762
|
return null;
|
|
2476
3763
|
}
|
|
2477
3764
|
}
|
|
2478
3765
|
function walletExists(storagePath) {
|
|
2479
|
-
const path4 = storagePath || (0,
|
|
2480
|
-
return (0,
|
|
3766
|
+
const path4 = storagePath || (0, import_path3.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3767
|
+
return (0, import_fs5.existsSync)(path4);
|
|
2481
3768
|
}
|
|
2482
3769
|
|
|
2483
3770
|
// src/verify/index.ts
|
|
2484
3771
|
var import_ethers4 = require("ethers");
|
|
2485
|
-
var
|
|
3772
|
+
var TRANSFER_EVENT_TOPIC3 = import_ethers4.ethers.id("Transfer(address,address,uint256)");
|
|
2486
3773
|
async function verifyPayment(params) {
|
|
2487
3774
|
const { txHash, expectedAmount, expectedTo, expectedToken } = params;
|
|
2488
3775
|
let chain;
|
|
@@ -2523,7 +3810,7 @@ async function verifyPayment(params) {
|
|
|
2523
3810
|
if (!detectedToken) {
|
|
2524
3811
|
continue;
|
|
2525
3812
|
}
|
|
2526
|
-
if (log.topics.length < 3 || log.topics[0] !==
|
|
3813
|
+
if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC3) {
|
|
2527
3814
|
continue;
|
|
2528
3815
|
}
|
|
2529
3816
|
const from = "0x" + log.topics[1].slice(-40);
|