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