moltspay 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +221 -38
  2. package/dist/cdp/index.d.mts +4 -4
  3. package/dist/cdp/index.d.ts +4 -4
  4. package/dist/cdp/index.js +57 -0
  5. package/dist/cdp/index.js.map +1 -1
  6. package/dist/cdp/index.mjs +57 -0
  7. package/dist/cdp/index.mjs.map +1 -1
  8. package/dist/chains/index.d.mts +9 -8
  9. package/dist/chains/index.d.ts +9 -8
  10. package/dist/chains/index.js +57 -0
  11. package/dist/chains/index.js.map +1 -1
  12. package/dist/chains/index.mjs +57 -0
  13. package/dist/chains/index.mjs.map +1 -1
  14. package/dist/cli/index.js +1975 -273
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/index.mjs +1977 -265
  17. package/dist/cli/index.mjs.map +1 -1
  18. package/dist/client/index.d.mts +36 -3
  19. package/dist/client/index.d.ts +36 -3
  20. package/dist/client/index.js +540 -32
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/index.mjs +548 -30
  23. package/dist/client/index.mjs.map +1 -1
  24. package/dist/facilitators/index.d.mts +220 -1
  25. package/dist/facilitators/index.d.ts +220 -1
  26. package/dist/facilitators/index.js +664 -1
  27. package/dist/facilitators/index.js.map +1 -1
  28. package/dist/facilitators/index.mjs +670 -1
  29. package/dist/facilitators/index.mjs.map +1 -1
  30. package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
  31. package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
  32. package/dist/index.d.mts +2 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +1413 -146
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1421 -144
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/server/index.d.mts +13 -3
  39. package/dist/server/index.d.ts +13 -3
  40. package/dist/server/index.js +905 -52
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/server/index.mjs +915 -52
  43. package/dist/server/index.mjs.map +1 -1
  44. package/dist/verify/index.d.mts +1 -1
  45. package/dist/verify/index.d.ts +1 -1
  46. package/dist/verify/index.js +57 -0
  47. package/dist/verify/index.js.map +1 -1
  48. package/dist/verify/index.mjs +57 -0
  49. package/dist/verify/index.mjs.map +1 -1
  50. package/dist/wallet/index.d.mts +3 -3
  51. package/dist/wallet/index.d.ts +3 -3
  52. package/dist/wallet/index.js +57 -0
  53. package/dist/wallet/index.js.map +1 -1
  54. package/dist/wallet/index.mjs +57 -0
  55. package/dist/wallet/index.mjs.map +1 -1
  56. package/package.json +4 -1
  57. 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.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
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: (typeof c === "object" ? c.wallet : null) || provider.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: provider.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
- return await this.handleProxy(body, paymentHeader, res);
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
- console.log(`[MoltsPay] Skill succeeded, settling payment...`);
1140
- let settlement = null;
1141
- try {
1142
- settlement = await this.registry.settle(payment, requirements);
1143
- console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1144
- } catch (err) {
1145
- console.error("[MoltsPay] Settlement failed:", err.message);
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
- return {
2132
+ const requirements = {
1422
2133
  scheme: "exact",
1423
2134
  network: selectedNetwork,
1424
2135
  asset: tokenAddress,
@@ -1427,6 +2138,27 @@ var MoltsPayServer = class {
1427
2138
  maxTimeoutSeconds: 300,
1428
2139
  extra: tokenDomain
1429
2140
  };
2141
+ if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
2142
+ const solanaFacilitator = this.registry.get("solana");
2143
+ const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
2144
+ if (feePayerPubkey) {
2145
+ requirements.extra = {
2146
+ ...requirements.extra || {},
2147
+ solanaFeePayer: feePayerPubkey
2148
+ };
2149
+ }
2150
+ }
2151
+ if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
2152
+ const bnbFacilitator = this.registry.get("bnb");
2153
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
2154
+ if (spenderAddress) {
2155
+ requirements.extra = {
2156
+ ...requirements.extra || {},
2157
+ bnbSpender: spenderAddress
2158
+ };
2159
+ }
2160
+ }
2161
+ return requirements;
1430
2162
  }
1431
2163
  /**
1432
2164
  * Detect which token is being used in the payment
@@ -1492,31 +2224,42 @@ var MoltsPayServer = class {
1492
2224
  /**
1493
2225
  * POST /proxy - Handle payment for external services (moltspay-creators)
1494
2226
  *
1495
- * This endpoint allows other services to delegate x402 payment handling.
2227
+ * This endpoint allows other services to delegate x402/MPP payment handling.
1496
2228
  * It does NOT execute any skill - just handles payment verification/settlement.
1497
2229
  *
1498
2230
  * Request body:
1499
2231
  * { wallet, amount, currency, chain, memo, serviceId, description }
1500
2232
  *
1501
- * Without X-Payment header: returns 402 with payment requirements
1502
- * With X-Payment header: verifies payment and returns result
2233
+ * For x402 (base, polygon, base_sepolia):
2234
+ * Without X-Payment header: returns 402 with X-Payment-Required
2235
+ * With X-Payment header: verifies payment via CDP
2236
+ *
2237
+ * For MPP (tempo_moderato):
2238
+ * Without Authorization header: returns 402 with WWW-Authenticate
2239
+ * With Authorization: Payment header: verifies tx on Tempo chain
1503
2240
  */
1504
- async handleProxy(body, paymentHeader, res) {
2241
+ async handleProxy(body, paymentHeader, authHeader, res) {
1505
2242
  const { wallet, amount, currency, chain, memo, serviceId, description } = body;
1506
2243
  if (!wallet || !amount) {
1507
2244
  return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
1508
2245
  }
1509
- if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) {
1510
- return this.sendJson(res, 400, { error: "Invalid wallet address format" });
2246
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
2247
+ if (chain && !supportedChains.includes(chain)) {
2248
+ return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
2249
+ }
2250
+ const isSolanaChain = chain === "solana" || chain === "solana_devnet";
2251
+ const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
2252
+ const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
2253
+ if (isSolanaChain && !isValidSolanaAddress) {
2254
+ return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
2255
+ }
2256
+ if (!isSolanaChain && !isValidEvmAddress) {
2257
+ return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
1511
2258
  }
1512
2259
  const amountNum = parseFloat(amount);
1513
2260
  if (isNaN(amountNum) || amountNum <= 0) {
1514
2261
  return this.sendJson(res, 400, { error: "Invalid amount" });
1515
2262
  }
1516
- const supportedChains = ["base", "polygon", "base_sepolia"];
1517
- if (chain && !supportedChains.includes(chain)) {
1518
- return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
1519
- }
1520
2263
  const proxyConfig = {
1521
2264
  id: serviceId || "proxy",
1522
2265
  name: description || "Proxy Payment",
@@ -1528,6 +2271,9 @@ var MoltsPayServer = class {
1528
2271
  input: {},
1529
2272
  output: {}
1530
2273
  };
2274
+ if (chain === "tempo_moderato") {
2275
+ return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
2276
+ }
1531
2277
  const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
1532
2278
  if (!paymentHeader) {
1533
2279
  return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
@@ -1539,37 +2285,225 @@ var MoltsPayServer = class {
1539
2285
  } catch {
1540
2286
  return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
1541
2287
  }
1542
- if (payment.x402Version !== X402_VERSION2) {
1543
- return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
2288
+ if (payment.x402Version !== X402_VERSION2) {
2289
+ return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
2290
+ }
2291
+ const scheme = payment.accepted?.scheme || payment.scheme;
2292
+ const network = payment.accepted?.network || payment.network;
2293
+ if (scheme !== "exact") {
2294
+ return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
2295
+ }
2296
+ const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
2297
+ if (network !== expectedNetwork) {
2298
+ return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
2299
+ }
2300
+ console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
2301
+ const verifyResult = await this.registry.verify(payment, requirements);
2302
+ if (!verifyResult.valid) {
2303
+ return this.sendJson(res, 402, {
2304
+ success: false,
2305
+ error: `Payment verification failed: ${verifyResult.error}`,
2306
+ facilitator: verifyResult.facilitator
2307
+ });
2308
+ }
2309
+ console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
2310
+ const { execute, service, params } = body;
2311
+ if (execute && service) {
2312
+ const skill = this.skills.get(service);
2313
+ if (!skill) {
2314
+ console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
2315
+ return this.sendJson(res, 404, {
2316
+ success: false,
2317
+ paymentSettled: false,
2318
+ error: `Service not found: ${service}`
2319
+ });
2320
+ }
2321
+ const isSolana = isSolanaNetwork(network);
2322
+ let settlement2 = null;
2323
+ if (isSolana) {
2324
+ console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
2325
+ try {
2326
+ settlement2 = await this.registry.settle(payment, requirements);
2327
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
2328
+ if (!settlement2.success) {
2329
+ console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
2330
+ return this.sendJson(res, 402, {
2331
+ success: false,
2332
+ paymentSettled: false,
2333
+ error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
2334
+ });
2335
+ }
2336
+ } catch (err) {
2337
+ console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
2338
+ return this.sendJson(res, 402, {
2339
+ success: false,
2340
+ paymentSettled: false,
2341
+ error: `Payment settlement failed: ${err.message}`
2342
+ });
2343
+ }
2344
+ } else {
2345
+ console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
2346
+ }
2347
+ const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
2348
+ let result;
2349
+ try {
2350
+ result = await Promise.race([
2351
+ skill.handler(params || {}),
2352
+ new Promise(
2353
+ (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
2354
+ )
2355
+ ]);
2356
+ console.log(`[MoltsPay] /proxy: Skill succeeded`);
2357
+ } catch (err) {
2358
+ console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
2359
+ return this.sendJson(res, 500, {
2360
+ success: false,
2361
+ paymentSettled: isSolana ? true : false,
2362
+ error: `Service execution failed: ${err.message}`,
2363
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
2364
+ });
2365
+ }
2366
+ if (!isSolana) {
2367
+ console.log(`[MoltsPay] /proxy: Settling payment...`);
2368
+ try {
2369
+ settlement2 = await this.registry.settle(payment, requirements);
2370
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
2371
+ } catch (err) {
2372
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
2373
+ return this.sendJson(res, 200, {
2374
+ success: true,
2375
+ verified: true,
2376
+ settled: false,
2377
+ settlementError: err.message,
2378
+ from: payment.payload?.authorization?.from,
2379
+ paidTo: wallet,
2380
+ amount: amountNum,
2381
+ currency: currency || "USDC",
2382
+ memo,
2383
+ result
2384
+ });
2385
+ }
2386
+ }
2387
+ return this.sendJson(res, 200, {
2388
+ success: true,
2389
+ verified: true,
2390
+ settled: settlement2?.success || false,
2391
+ txHash: settlement2?.transaction,
2392
+ from: payment.payload?.authorization?.from,
2393
+ paidTo: wallet,
2394
+ amount: amountNum,
2395
+ currency: currency || "USDC",
2396
+ facilitator: settlement2?.facilitator,
2397
+ memo,
2398
+ result
2399
+ });
2400
+ }
2401
+ console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
2402
+ let settlement = null;
2403
+ try {
2404
+ settlement = await this.registry.settle(payment, requirements);
2405
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
2406
+ } catch (err) {
2407
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
2408
+ return this.sendJson(res, 500, {
2409
+ success: false,
2410
+ error: `Settlement failed: ${err.message}`
2411
+ });
2412
+ }
2413
+ this.sendJson(res, 200, {
2414
+ success: true,
2415
+ verified: true,
2416
+ settled: settlement?.success || false,
2417
+ txHash: settlement?.transaction,
2418
+ from: payment.payload?.authorization?.from,
2419
+ // Buyer's wallet address
2420
+ paidTo: wallet,
2421
+ amount: amountNum,
2422
+ currency: currency || "USDC",
2423
+ facilitator: settlement?.facilitator,
2424
+ memo
2425
+ });
2426
+ }
2427
+ /**
2428
+ * Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
2429
+ */
2430
+ async handleProxyMPP(body, config, authHeader, res) {
2431
+ const { wallet, amount, memo, serviceId } = body;
2432
+ const amountNum = parseFloat(amount);
2433
+ const amountInUnits = Math.floor(amountNum * 1e6).toString();
2434
+ if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
2435
+ const challengeId = this.generateChallengeId();
2436
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
2437
+ const mppRequest = {
2438
+ amount: amountInUnits,
2439
+ currency: tokenAddress,
2440
+ methodDetails: {
2441
+ chainId: 42431,
2442
+ feePayer: true
2443
+ },
2444
+ recipient: wallet
2445
+ };
2446
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
2447
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
2448
+ const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
2449
+ res.writeHead(402, {
2450
+ "Content-Type": "application/problem+json",
2451
+ [MPP_WWW_AUTH_HEADER]: wwwAuth
2452
+ });
2453
+ res.end(JSON.stringify({
2454
+ type: "https://paymentauth.org/problems/payment-required",
2455
+ title: "Payment Required",
2456
+ status: 402,
2457
+ detail: `Payment is required (${config.name}).`,
2458
+ service: serviceId || "proxy",
2459
+ price: amountNum,
2460
+ currency: "USDC"
2461
+ }, null, 2));
2462
+ return;
2463
+ }
2464
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
2465
+ if (!credentialMatch) {
2466
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
1544
2467
  }
1545
- const scheme = payment.accepted?.scheme || payment.scheme;
1546
- const network = payment.accepted?.network || payment.network;
1547
- if (scheme !== "exact") {
1548
- return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
2468
+ let mppCredential;
2469
+ try {
2470
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
2471
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
2472
+ mppCredential = JSON.parse(decoded);
2473
+ } catch (err) {
2474
+ console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
2475
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
1549
2476
  }
1550
- const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
1551
- if (network !== expectedNetwork) {
1552
- return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
2477
+ let txHash;
2478
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
2479
+ txHash = mppCredential.payload.hash;
2480
+ } else {
2481
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
1553
2482
  }
1554
- console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
1555
- const verifyResult = await this.registry.verify(payment, requirements);
1556
- if (!verifyResult.valid) {
2483
+ console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
2484
+ const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
2485
+ const paymentPayload = {
2486
+ x402Version: X402_VERSION2,
2487
+ scheme: "exact",
2488
+ network: "eip155:42431",
2489
+ payload: { txHash, chainId: 42431 }
2490
+ };
2491
+ const verification = await this.registry.verify(paymentPayload, requirements);
2492
+ if (!verification.valid) {
1557
2493
  return this.sendJson(res, 402, {
1558
- success: false,
1559
- error: `Payment verification failed: ${verifyResult.error}`,
1560
- facilitator: verifyResult.facilitator
2494
+ error: `Payment verification failed: ${verification.error}`
1561
2495
  });
1562
2496
  }
1563
- console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
2497
+ console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
1564
2498
  const { execute, service, params } = body;
1565
2499
  if (execute && service) {
1566
- console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
2500
+ console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
1567
2501
  const skill = this.skills.get(service);
1568
2502
  if (!skill) {
1569
- console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
1570
2503
  return this.sendJson(res, 404, {
1571
2504
  success: false,
1572
- paymentSettled: false,
2505
+ paymentSettled: true,
2506
+ // Payment already happened on Tempo
1573
2507
  error: `Service not found: ${service}`
1574
2508
  });
1575
2509
  }
@@ -1582,73 +2516,36 @@ var MoltsPayServer = class {
1582
2516
  (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
1583
2517
  )
1584
2518
  ]);
1585
- console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
1586
2519
  } catch (err) {
1587
- console.error(`[MoltsPay] /proxy: Skill failed: ${err.message} - NOT settling`);
2520
+ console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
1588
2521
  return this.sendJson(res, 500, {
1589
2522
  success: false,
1590
- paymentSettled: false,
2523
+ paymentSettled: true,
1591
2524
  error: `Service execution failed: ${err.message}`
1592
2525
  });
1593
2526
  }
1594
- let settlement2 = null;
1595
- try {
1596
- settlement2 = await this.registry.settle(payment, requirements);
1597
- console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
1598
- } catch (err) {
1599
- console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
1600
- return this.sendJson(res, 200, {
1601
- success: true,
1602
- verified: true,
1603
- settled: false,
1604
- settlementError: err.message,
1605
- from: payment.payload?.authorization?.from,
1606
- // Buyer's wallet address
1607
- paidTo: wallet,
1608
- amount: amountNum,
1609
- currency: currency || "USDC",
1610
- memo,
1611
- result
1612
- });
1613
- }
1614
2527
  return this.sendJson(res, 200, {
1615
2528
  success: true,
1616
2529
  verified: true,
1617
- settled: settlement2?.success || false,
1618
- txHash: settlement2?.transaction,
1619
- from: payment.payload?.authorization?.from,
1620
- // Buyer's wallet address
2530
+ txHash,
2531
+ chain: "tempo_moderato",
1621
2532
  paidTo: wallet,
1622
2533
  amount: amountNum,
1623
- currency: currency || "USDC",
1624
- facilitator: settlement2?.facilitator,
2534
+ currency: "USDC",
2535
+ facilitator: verification.facilitator,
1625
2536
  memo,
1626
2537
  result
1627
2538
  });
1628
2539
  }
1629
- console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
1630
- let settlement = null;
1631
- try {
1632
- settlement = await this.registry.settle(payment, requirements);
1633
- console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1634
- } catch (err) {
1635
- console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
1636
- return this.sendJson(res, 500, {
1637
- success: false,
1638
- error: `Settlement failed: ${err.message}`
1639
- });
1640
- }
1641
2540
  this.sendJson(res, 200, {
1642
2541
  success: true,
1643
2542
  verified: true,
1644
- settled: settlement?.success || false,
1645
- txHash: settlement?.transaction,
1646
- from: payment.payload?.authorization?.from,
1647
- // Buyer's wallet address
2543
+ txHash,
2544
+ chain: "tempo_moderato",
1648
2545
  paidTo: wallet,
1649
2546
  amount: amountNum,
1650
- currency: currency || "USDC",
1651
- facilitator: settlement?.facilitator,
2547
+ currency: "USDC",
2548
+ facilitator: verification.facilitator,
1652
2549
  memo
1653
2550
  });
1654
2551
  }
@@ -1663,7 +2560,7 @@ var MoltsPayServer = class {
1663
2560
  const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
1664
2561
  const tokenAddress = tokenAddresses[selectedToken];
1665
2562
  const tokenDomain = getTokenDomain(networkId, selectedToken);
1666
- return {
2563
+ const requirements = {
1667
2564
  scheme: "exact",
1668
2565
  network: networkId,
1669
2566
  asset: tokenAddress,
@@ -1673,6 +2570,17 @@ var MoltsPayServer = class {
1673
2570
  maxTimeoutSeconds: 300,
1674
2571
  extra: tokenDomain
1675
2572
  };
2573
+ if (networkId === "eip155:56" || networkId === "eip155:97") {
2574
+ const bnbFacilitator = this.registry.get("bnb");
2575
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
2576
+ if (spenderAddress) {
2577
+ requirements.extra = {
2578
+ ...requirements.extra || {},
2579
+ bnbSpender: spenderAddress
2580
+ };
2581
+ }
2582
+ }
2583
+ return requirements;
1676
2584
  }
1677
2585
  /**
1678
2586
  * Return 402 with x402 payment requirements for proxy endpoint
@@ -1703,10 +2611,40 @@ var MoltsPayServer = class {
1703
2611
  };
1704
2612
 
1705
2613
  // src/client/index.ts
1706
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync, statSync, chmodSync } from "fs";
1707
- import { homedir } from "os";
1708
- import { join as join3 } from "path";
2614
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
2615
+ import { homedir as homedir2 } from "os";
2616
+ import { join as join4 } from "path";
1709
2617
  import { Wallet, ethers } from "ethers";
2618
+
2619
+ // src/wallet/solana.ts
2620
+ import { Keypair as Keypair3, PublicKey as PublicKey3, LAMPORTS_PER_SOL } from "@solana/web3.js";
2621
+ import { getAssociatedTokenAddress as getAssociatedTokenAddress2, getAccount as getAccount2 } from "@solana/spl-token";
2622
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync } from "fs";
2623
+ import { join as join3 } from "path";
2624
+ import { homedir } from "os";
2625
+ import bs582 from "bs58";
2626
+ var DEFAULT_CONFIG_DIR = join3(homedir(), ".moltspay");
2627
+ var SOLANA_WALLET_FILE = "wallet-solana.json";
2628
+ function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
2629
+ return join3(configDir, SOLANA_WALLET_FILE);
2630
+ }
2631
+ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
2632
+ const walletPath = getSolanaWalletPath(configDir);
2633
+ if (!existsSync3(walletPath)) {
2634
+ return null;
2635
+ }
2636
+ try {
2637
+ const data = JSON.parse(readFileSync3(walletPath, "utf-8"));
2638
+ const secretKey = bs582.decode(data.secretKey);
2639
+ return Keypair3.fromSecretKey(secretKey);
2640
+ } catch (error) {
2641
+ console.error("Failed to load Solana wallet:", error);
2642
+ return null;
2643
+ }
2644
+ }
2645
+
2646
+ // src/client/index.ts
2647
+ import { PublicKey as PublicKey4 } from "@solana/web3.js";
1710
2648
  var X402_VERSION3 = 2;
1711
2649
  var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
1712
2650
  var PAYMENT_HEADER2 = "x-payment";
@@ -1725,7 +2663,7 @@ var MoltsPayClient = class {
1725
2663
  todaySpending = 0;
1726
2664
  lastSpendingReset = 0;
1727
2665
  constructor(options = {}) {
1728
- this.configDir = options.configDir || join3(homedir(), ".moltspay");
2666
+ this.configDir = options.configDir || join4(homedir2(), ".moltspay");
1729
2667
  this.config = this.loadConfig();
1730
2668
  this.walletData = this.loadWallet();
1731
2669
  this.loadSpending();
@@ -1745,6 +2683,12 @@ var MoltsPayClient = class {
1745
2683
  get address() {
1746
2684
  return this.wallet?.address || null;
1747
2685
  }
2686
+ /**
2687
+ * Get wallet instance (for direct operations like approvals)
2688
+ */
2689
+ getWallet() {
2690
+ return this.wallet;
2691
+ }
1748
2692
  /**
1749
2693
  * Get current config
1750
2694
  */
@@ -1814,9 +2758,14 @@ var MoltsPayClient = class {
1814
2758
  }
1815
2759
  throw new Error(data.error || "Unexpected response");
1816
2760
  }
2761
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
1817
2762
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER2);
2763
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
2764
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
2765
+ return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
2766
+ }
1818
2767
  if (!paymentRequiredHeader) {
1819
- throw new Error("Missing x-payment-required header");
2768
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
1820
2769
  }
1821
2770
  let requirements;
1822
2771
  try {
@@ -1833,17 +2782,22 @@ var MoltsPayClient = class {
1833
2782
  throw new Error("Invalid x-payment-required header");
1834
2783
  }
1835
2784
  const networkToChainName = (network2) => {
2785
+ if (network2 === "solana:mainnet") return "solana";
2786
+ if (network2 === "solana:devnet") return "solana_devnet";
1836
2787
  const match = network2.match(/^eip155:(\d+)$/);
1837
2788
  if (!match) return null;
1838
2789
  const chainId = parseInt(match[1]);
1839
2790
  if (chainId === 8453) return "base";
1840
2791
  if (chainId === 137) return "polygon";
1841
2792
  if (chainId === 84532) return "base_sepolia";
2793
+ if (chainId === 42431) return "tempo_moderato";
2794
+ if (chainId === 56) return "bnb";
2795
+ if (chainId === 97) return "bnb_testnet";
1842
2796
  return null;
1843
2797
  };
1844
2798
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
1845
- let chainName;
1846
2799
  const userSpecifiedChain = options.chain;
2800
+ let selectedChain;
1847
2801
  if (userSpecifiedChain) {
1848
2802
  if (!serverChains.includes(userSpecifiedChain)) {
1849
2803
  throw new Error(
@@ -1851,17 +2805,27 @@ var MoltsPayClient = class {
1851
2805
  Server accepts: ${serverChains.join(", ")}`
1852
2806
  );
1853
2807
  }
1854
- chainName = userSpecifiedChain;
2808
+ selectedChain = userSpecifiedChain;
1855
2809
  } else {
1856
2810
  if (serverChains.length === 1 && serverChains[0] === "base") {
1857
- chainName = "base";
2811
+ selectedChain = "base";
1858
2812
  } else {
1859
2813
  throw new Error(
1860
2814
  `Server accepts: ${serverChains.join(", ")}
1861
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2815
+ Please specify: --chain <chain_name>`
1862
2816
  );
1863
2817
  }
1864
2818
  }
2819
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
2820
+ const solanaChain = selectedChain;
2821
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
2822
+ const req2 = requirements.find((r) => r.network === network2);
2823
+ if (!req2) {
2824
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
2825
+ }
2826
+ return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
2827
+ }
2828
+ const chainName = selectedChain;
1865
2829
  const chain = getChain(chainName);
1866
2830
  const network = `eip155:${chain.chainId}`;
1867
2831
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -1896,6 +2860,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
1896
2860
  } else {
1897
2861
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
1898
2862
  }
2863
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
2864
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
2865
+ const payTo2 = req.payTo || req.resource;
2866
+ if (!payTo2) {
2867
+ throw new Error("Missing payTo address in payment requirements");
2868
+ }
2869
+ const bnbSpender = req.extra?.bnbSpender;
2870
+ if (!bnbSpender) {
2871
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
2872
+ }
2873
+ return await this.handleBNBPayment(serverUrl, service, params, {
2874
+ to: payTo2,
2875
+ amount,
2876
+ token,
2877
+ chainName,
2878
+ chain,
2879
+ spender: bnbSpender
2880
+ });
2881
+ }
1899
2882
  const payTo = req.payTo || req.resource;
1900
2883
  if (!payTo) {
1901
2884
  throw new Error("Missing payTo address in payment requirements");
@@ -1945,6 +2928,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
1945
2928
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
1946
2929
  return result.result;
1947
2930
  }
2931
+ /**
2932
+ * Handle MPP (Machine Payments Protocol) payment flow
2933
+ * Called when pay() detects WWW-Authenticate header in 402 response
2934
+ */
2935
+ async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
2936
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
2937
+ const { createWalletClient, createPublicClient, http } = await import("viem");
2938
+ const { tempoModerato } = await import("viem/chains");
2939
+ const { Actions } = await import("viem/tempo");
2940
+ const privateKey = this.walletData.privateKey;
2941
+ const account = privateKeyToAccount2(privateKey);
2942
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
2943
+ console.log(`[MoltsPay] Account: ${account.address}`);
2944
+ const parseAuthParam = (header, key) => {
2945
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
2946
+ return match ? match[1] : null;
2947
+ };
2948
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
2949
+ const method = parseAuthParam(wwwAuthHeader, "method");
2950
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
2951
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
2952
+ if (method !== "tempo") {
2953
+ throw new Error(`Unsupported payment method: ${method}`);
2954
+ }
2955
+ if (!requestB64) {
2956
+ throw new Error("Missing request in WWW-Authenticate");
2957
+ }
2958
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
2959
+ const paymentRequest = JSON.parse(requestJson);
2960
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
2961
+ const chainId = methodDetails?.chainId || 42431;
2962
+ const amountDisplay = Number(amount) / 1e6;
2963
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
2964
+ this.checkLimits(amountDisplay);
2965
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
2966
+ const tempoChain = { ...tempoModerato, feeToken: currency };
2967
+ const publicClient = createPublicClient({
2968
+ chain: tempoChain,
2969
+ transport: http("https://rpc.moderato.tempo.xyz")
2970
+ });
2971
+ const walletClient = createWalletClient({
2972
+ account,
2973
+ chain: tempoChain,
2974
+ transport: http("https://rpc.moderato.tempo.xyz")
2975
+ });
2976
+ const txHash = await Actions.token.transfer(walletClient, {
2977
+ to: recipient,
2978
+ amount: BigInt(amount),
2979
+ token: currency
2980
+ });
2981
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
2982
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
2983
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
2984
+ const credential = {
2985
+ challenge: {
2986
+ id: challengeId,
2987
+ realm,
2988
+ method: "tempo",
2989
+ intent: "charge",
2990
+ request: paymentRequest
2991
+ },
2992
+ payload: { hash: txHash, type: "hash" },
2993
+ source: `did:pkh:eip155:${chainId}:${account.address}`
2994
+ };
2995
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2996
+ const paidRes = await fetch(`${serverUrl}/execute`, {
2997
+ method: "POST",
2998
+ headers: {
2999
+ "Content-Type": "application/json",
3000
+ "Authorization": `Payment ${credentialB64}`
3001
+ },
3002
+ body: JSON.stringify({ service, params, chain: "tempo_moderato" })
3003
+ });
3004
+ const result = await paidRes.json();
3005
+ if (!paidRes.ok) {
3006
+ throw new Error(result.error || "Payment verification failed");
3007
+ }
3008
+ this.recordSpending(amountDisplay);
3009
+ console.log(`[MoltsPay] Success!`);
3010
+ return result.result || result;
3011
+ }
3012
+ /**
3013
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
3014
+ *
3015
+ * Flow:
3016
+ * 1. Check client has approved server wallet (done via `moltspay init`)
3017
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
3018
+ * 3. Send intent to server
3019
+ * 4. Server executes service
3020
+ * 5. Server calls transferFrom if successful (pay-for-success)
3021
+ */
3022
+ async handleBNBPayment(serverUrl, service, params, paymentDetails) {
3023
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
3024
+ const tokenConfig = chain.tokens[token];
3025
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
3026
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
3027
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
3028
+ if (allowance < amountWeiCheck) {
3029
+ const nativeBalance = await provider.getBalance(this.wallet.address);
3030
+ const minGasBalance = ethers.parseEther("0.0005");
3031
+ if (nativeBalance < minGasBalance) {
3032
+ const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
3033
+ const isTestnet = chainName === "bnb_testnet";
3034
+ if (isTestnet) {
3035
+ throw new Error(
3036
+ `\u274C Insufficient tBNB for approval transaction
3037
+
3038
+ Current tBNB: ${nativeBNB}
3039
+ Required: ~0.001 tBNB
3040
+
3041
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
3042
+ (Gives USDC + tBNB for gas)`
3043
+ );
3044
+ } else {
3045
+ throw new Error(
3046
+ `\u274C Insufficient BNB for approval transaction
3047
+
3048
+ Current BNB: ${nativeBNB}
3049
+ Required: ~0.001 BNB (~$0.60)
3050
+
3051
+ To get BNB:
3052
+ \u2022 Withdraw from Binance/exchange to your wallet
3053
+ \u2022 Most exchanges include BNB dust with withdrawals
3054
+
3055
+ After funding, run:
3056
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
3057
+ );
3058
+ }
3059
+ }
3060
+ throw new Error(
3061
+ `Insufficient allowance for ${spender.slice(0, 10)}...
3062
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
3063
+ );
3064
+ }
3065
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
3066
+ const intent = {
3067
+ from: this.wallet.address,
3068
+ to,
3069
+ amount: amountWei,
3070
+ token: tokenConfig.address,
3071
+ service,
3072
+ nonce: Date.now(),
3073
+ // Use timestamp as nonce for simplicity
3074
+ deadline: Date.now() + 36e5
3075
+ // 1 hour
3076
+ };
3077
+ const domain = {
3078
+ name: "MoltsPay",
3079
+ version: "1",
3080
+ chainId: chain.chainId
3081
+ };
3082
+ const types = {
3083
+ PaymentIntent: [
3084
+ { name: "from", type: "address" },
3085
+ { name: "to", type: "address" },
3086
+ { name: "amount", type: "uint256" },
3087
+ { name: "token", type: "address" },
3088
+ { name: "service", type: "string" },
3089
+ { name: "nonce", type: "uint256" },
3090
+ { name: "deadline", type: "uint256" }
3091
+ ]
3092
+ };
3093
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
3094
+ const signature = await this.wallet.signTypedData(domain, types, intent);
3095
+ const network = `eip155:${chain.chainId}`;
3096
+ const payload = {
3097
+ x402Version: 2,
3098
+ scheme: "exact",
3099
+ network,
3100
+ payload: {
3101
+ intent: {
3102
+ ...intent,
3103
+ signature
3104
+ },
3105
+ chainId: chain.chainId
3106
+ },
3107
+ accepted: {
3108
+ scheme: "exact",
3109
+ network,
3110
+ asset: tokenConfig.address,
3111
+ amount: amountWei,
3112
+ payTo: to,
3113
+ maxTimeoutSeconds: 300
3114
+ }
3115
+ };
3116
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
3117
+ console.log(`[MoltsPay] Sending BNB payment request...`);
3118
+ const paidRes = await fetch(`${serverUrl}/execute`, {
3119
+ method: "POST",
3120
+ headers: {
3121
+ "Content-Type": "application/json",
3122
+ "X-Payment": paymentHeader
3123
+ },
3124
+ body: JSON.stringify({ service, params, chain: chainName })
3125
+ });
3126
+ const result = await paidRes.json();
3127
+ if (!paidRes.ok) {
3128
+ throw new Error(result.error || "BNB payment failed");
3129
+ }
3130
+ this.recordSpending(amount);
3131
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
3132
+ return result.result || result;
3133
+ }
3134
+ /**
3135
+ * Handle Solana payment flow
3136
+ *
3137
+ * Solana uses SPL token transfers with pay-for-success model:
3138
+ * 1. Client creates and signs a transfer transaction
3139
+ * 2. Server submits the transaction after service completes
3140
+ */
3141
+ async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
3142
+ const solanaWallet = loadSolanaWallet(this.configDir);
3143
+ if (!solanaWallet) {
3144
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
3145
+ }
3146
+ const amount = Number(requirements.amount);
3147
+ const amountUSDC = amount / 1e6;
3148
+ this.checkLimits(amountUSDC);
3149
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
3150
+ if (!requirements.payTo) {
3151
+ throw new Error("Missing payTo address in payment requirements");
3152
+ }
3153
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
3154
+ const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
3155
+ if (feePayerPubkey) {
3156
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
3157
+ }
3158
+ const recipientPubkey = new PublicKey4(requirements.payTo);
3159
+ const transaction = await createSolanaPaymentTransaction(
3160
+ solanaWallet.publicKey,
3161
+ recipientPubkey,
3162
+ BigInt(amount),
3163
+ chain,
3164
+ feePayerPubkey
3165
+ // Optional fee payer for gasless mode
3166
+ );
3167
+ if (feePayerPubkey) {
3168
+ transaction.partialSign(solanaWallet);
3169
+ } else {
3170
+ transaction.sign(solanaWallet);
3171
+ }
3172
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
3173
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
3174
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
3175
+ const payload = {
3176
+ x402Version: 2,
3177
+ scheme: "exact",
3178
+ network,
3179
+ payload: {
3180
+ signedTransaction: signedTx,
3181
+ sender: solanaWallet.publicKey.toBase58(),
3182
+ chain
3183
+ },
3184
+ accepted: {
3185
+ scheme: "exact",
3186
+ network,
3187
+ asset: requirements.asset,
3188
+ amount: requirements.amount,
3189
+ payTo: requirements.payTo,
3190
+ maxTimeoutSeconds: 300
3191
+ }
3192
+ };
3193
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
3194
+ const paidRes = await fetch(`${serverUrl}/execute`, {
3195
+ method: "POST",
3196
+ headers: {
3197
+ "Content-Type": "application/json",
3198
+ "X-Payment": paymentHeader
3199
+ },
3200
+ body: JSON.stringify({ service, params, chain })
3201
+ });
3202
+ const result = await paidRes.json();
3203
+ if (!paidRes.ok) {
3204
+ throw new Error(result.error || "Solana payment failed");
3205
+ }
3206
+ this.recordSpending(amountUSDC);
3207
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
3208
+ if (result.payment?.transaction) {
3209
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
3210
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
3211
+ }
3212
+ return result.result || result;
3213
+ }
3214
+ /**
3215
+ * Check ERC20 allowance for a spender
3216
+ */
3217
+ async checkAllowance(tokenAddress, spender, provider) {
3218
+ const contract = new ethers.Contract(
3219
+ tokenAddress,
3220
+ ["function allowance(address owner, address spender) view returns (uint256)"],
3221
+ provider
3222
+ );
3223
+ return await contract.allowance(this.wallet.address, spender);
3224
+ }
1948
3225
  /**
1949
3226
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
1950
3227
  * This only signs - no on-chain transaction, no gas needed.
@@ -2015,26 +3292,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2015
3292
  }
2016
3293
  // --- Config & Wallet Management ---
2017
3294
  loadConfig() {
2018
- const configPath = join3(this.configDir, "config.json");
2019
- if (existsSync3(configPath)) {
2020
- const content = readFileSync3(configPath, "utf-8");
3295
+ const configPath = join4(this.configDir, "config.json");
3296
+ if (existsSync4(configPath)) {
3297
+ const content = readFileSync4(configPath, "utf-8");
2021
3298
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
2022
3299
  }
2023
3300
  return { ...DEFAULT_CONFIG };
2024
3301
  }
2025
3302
  saveConfig() {
2026
- mkdirSync(this.configDir, { recursive: true });
2027
- const configPath = join3(this.configDir, "config.json");
2028
- writeFileSync(configPath, JSON.stringify(this.config, null, 2));
3303
+ mkdirSync2(this.configDir, { recursive: true });
3304
+ const configPath = join4(this.configDir, "config.json");
3305
+ writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
2029
3306
  }
2030
3307
  /**
2031
3308
  * Load spending data from disk
2032
3309
  */
2033
3310
  loadSpending() {
2034
- const spendingPath = join3(this.configDir, "spending.json");
2035
- if (existsSync3(spendingPath)) {
3311
+ const spendingPath = join4(this.configDir, "spending.json");
3312
+ if (existsSync4(spendingPath)) {
2036
3313
  try {
2037
- const data = JSON.parse(readFileSync3(spendingPath, "utf-8"));
3314
+ const data = JSON.parse(readFileSync4(spendingPath, "utf-8"));
2038
3315
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
2039
3316
  if (data.date && data.date === today) {
2040
3317
  this.todaySpending = data.amount || 0;
@@ -2053,18 +3330,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2053
3330
  * Save spending data to disk
2054
3331
  */
2055
3332
  saveSpending() {
2056
- mkdirSync(this.configDir, { recursive: true });
2057
- const spendingPath = join3(this.configDir, "spending.json");
3333
+ mkdirSync2(this.configDir, { recursive: true });
3334
+ const spendingPath = join4(this.configDir, "spending.json");
2058
3335
  const data = {
2059
3336
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
2060
3337
  amount: this.todaySpending,
2061
3338
  updatedAt: Date.now()
2062
3339
  };
2063
- writeFileSync(spendingPath, JSON.stringify(data, null, 2));
3340
+ writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
2064
3341
  }
2065
3342
  loadWallet() {
2066
- const walletPath = join3(this.configDir, "wallet.json");
2067
- if (existsSync3(walletPath)) {
3343
+ const walletPath = join4(this.configDir, "wallet.json");
3344
+ if (existsSync4(walletPath)) {
2068
3345
  try {
2069
3346
  const stats = statSync(walletPath);
2070
3347
  const mode = stats.mode & 511;
@@ -2075,7 +3352,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2075
3352
  }
2076
3353
  } catch (err) {
2077
3354
  }
2078
- const content = readFileSync3(walletPath, "utf-8");
3355
+ const content = readFileSync4(walletPath, "utf-8");
2079
3356
  return JSON.parse(content);
2080
3357
  }
2081
3358
  return null;
@@ -2084,15 +3361,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2084
3361
  * Initialize a new wallet (called by CLI)
2085
3362
  */
2086
3363
  static init(configDir, options) {
2087
- mkdirSync(configDir, { recursive: true });
3364
+ mkdirSync2(configDir, { recursive: true });
2088
3365
  const wallet = Wallet.createRandom();
2089
3366
  const walletData = {
2090
3367
  address: wallet.address,
2091
3368
  privateKey: wallet.privateKey,
2092
3369
  createdAt: Date.now()
2093
3370
  };
2094
- const walletPath = join3(configDir, "wallet.json");
2095
- writeFileSync(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
3371
+ const walletPath = join4(configDir, "wallet.json");
3372
+ writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
2096
3373
  const config = {
2097
3374
  chain: options.chain,
2098
3375
  limits: {
@@ -2100,8 +3377,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2100
3377
  maxPerDay: options.maxPerDay
2101
3378
  }
2102
3379
  };
2103
- const configPath = join3(configDir, "config.json");
2104
- writeFileSync(configPath, JSON.stringify(config, null, 2));
3380
+ const configPath = join4(configDir, "config.json");
3381
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
2105
3382
  return { address: wallet.address, configDir };
2106
3383
  }
2107
3384
  /**
@@ -2137,7 +3414,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2137
3414
  if (!this.wallet) {
2138
3415
  throw new Error("Client not initialized");
2139
3416
  }
2140
- const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
3417
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
2141
3418
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
2142
3419
  const results = {};
2143
3420
  const tempoTokens = {
@@ -2208,12 +3485,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
2208
3485
  if (!this.wallet || !this.walletData) {
2209
3486
  throw new Error("Client not initialized. Run: npx moltspay init");
2210
3487
  }
2211
- const { privateKeyToAccount } = await import("viem/accounts");
3488
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
2212
3489
  const { createWalletClient, createPublicClient, http } = await import("viem");
2213
3490
  const { tempoModerato } = await import("viem/chains");
2214
3491
  const { Actions } = await import("viem/tempo");
2215
3492
  const privateKey = this.walletData.privateKey;
2216
- const account = privateKeyToAccount(privateKey);
3493
+ const account = privateKeyToAccount2(privateKey);
2217
3494
  console.log(`[MoltsPay] Making MPP request to: ${url}`);
2218
3495
  console.log(`[MoltsPay] Using account: ${account.address}`);
2219
3496
  const initResponse = await fetch(url, {
@@ -2313,10 +3590,10 @@ import { ethers as ethers2 } from "ethers";
2313
3590
 
2314
3591
  // src/wallet/createWallet.ts
2315
3592
  import { ethers as ethers3 } from "ethers";
2316
- import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
2317
- import { join as join4, dirname } from "path";
3593
+ import { writeFileSync as writeFileSync3, readFileSync as readFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
3594
+ import { join as join5, dirname } from "path";
2318
3595
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
2319
- var DEFAULT_STORAGE_DIR = join4(process.env.HOME || "~", ".moltspay");
3596
+ var DEFAULT_STORAGE_DIR = join5(process.env.HOME || "~", ".moltspay");
2320
3597
  var DEFAULT_STORAGE_FILE = "wallet.json";
2321
3598
  function encryptPrivateKey(privateKey, password) {
2322
3599
  const salt = randomBytes(16);
@@ -2339,10 +3616,10 @@ function decryptPrivateKey(encrypted, password, iv, salt) {
2339
3616
  return decrypted;
2340
3617
  }
2341
3618
  function createWallet(options = {}) {
2342
- const storagePath = options.storagePath || join4(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
2343
- if (existsSync4(storagePath) && !options.overwrite) {
3619
+ const storagePath = options.storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
3620
+ if (existsSync5(storagePath) && !options.overwrite) {
2344
3621
  try {
2345
- const existing = JSON.parse(readFileSync4(storagePath, "utf8"));
3622
+ const existing = JSON.parse(readFileSync5(storagePath, "utf8"));
2346
3623
  return {
2347
3624
  success: true,
2348
3625
  address: existing.address,
@@ -2374,10 +3651,10 @@ function createWallet(options = {}) {
2374
3651
  walletData.privateKey = wallet.privateKey;
2375
3652
  }
2376
3653
  const dir = dirname(storagePath);
2377
- if (!existsSync4(dir)) {
2378
- mkdirSync2(dir, { recursive: true });
3654
+ if (!existsSync5(dir)) {
3655
+ mkdirSync3(dir, { recursive: true });
2379
3656
  }
2380
- writeFileSync2(storagePath, JSON.stringify(walletData, null, 2), { mode: 384 });
3657
+ writeFileSync3(storagePath, JSON.stringify(walletData, null, 2), { mode: 384 });
2381
3658
  return {
2382
3659
  success: true,
2383
3660
  address: wallet.address,
@@ -2392,12 +3669,12 @@ function createWallet(options = {}) {
2392
3669
  }
2393
3670
  }
2394
3671
  function loadWallet(options = {}) {
2395
- const storagePath = options.storagePath || join4(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
2396
- if (!existsSync4(storagePath)) {
3672
+ const storagePath = options.storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
3673
+ if (!existsSync5(storagePath)) {
2397
3674
  return { success: false, error: "Wallet not found. Run createWallet() first." };
2398
3675
  }
2399
3676
  try {
2400
- const data = JSON.parse(readFileSync4(storagePath, "utf8"));
3677
+ const data = JSON.parse(readFileSync5(storagePath, "utf8"));
2401
3678
  if (data.encrypted) {
2402
3679
  if (!options.password) {
2403
3680
  return { success: false, error: "Wallet is encrypted. Password required." };
@@ -2412,25 +3689,25 @@ function loadWallet(options = {}) {
2412
3689
  }
2413
3690
  }
2414
3691
  function getWalletAddress(storagePath) {
2415
- const path4 = storagePath || join4(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
2416
- if (!existsSync4(path4)) {
3692
+ const path4 = storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
3693
+ if (!existsSync5(path4)) {
2417
3694
  return null;
2418
3695
  }
2419
3696
  try {
2420
- const data = JSON.parse(readFileSync4(path4, "utf8"));
3697
+ const data = JSON.parse(readFileSync5(path4, "utf8"));
2421
3698
  return data.address;
2422
3699
  } catch {
2423
3700
  return null;
2424
3701
  }
2425
3702
  }
2426
3703
  function walletExists(storagePath) {
2427
- const path4 = storagePath || join4(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
2428
- return existsSync4(path4);
3704
+ const path4 = storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
3705
+ return existsSync5(path4);
2429
3706
  }
2430
3707
 
2431
3708
  // src/verify/index.ts
2432
3709
  import { ethers as ethers4 } from "ethers";
2433
- var TRANSFER_EVENT_TOPIC2 = ethers4.id("Transfer(address,address,uint256)");
3710
+ var TRANSFER_EVENT_TOPIC3 = ethers4.id("Transfer(address,address,uint256)");
2434
3711
  async function verifyPayment(params) {
2435
3712
  const { txHash, expectedAmount, expectedTo, expectedToken } = params;
2436
3713
  let chain;
@@ -2471,7 +3748,7 @@ async function verifyPayment(params) {
2471
3748
  if (!detectedToken) {
2472
3749
  continue;
2473
3750
  }
2474
- if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC2) {
3751
+ if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC3) {
2475
3752
  continue;
2476
3753
  }
2477
3754
  const from = "0x" + log.topics[1].slice(-40);