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