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
@@ -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
@@ -1483,31 +2160,42 @@ var MoltsPayServer = class {
1483
2160
  /**
1484
2161
  * POST /proxy - Handle payment for external services (moltspay-creators)
1485
2162
  *
1486
- * This endpoint allows other services to delegate x402 payment handling.
2163
+ * This endpoint allows other services to delegate x402/MPP payment handling.
1487
2164
  * It does NOT execute any skill - just handles payment verification/settlement.
1488
2165
  *
1489
2166
  * Request body:
1490
2167
  * { wallet, amount, currency, chain, memo, serviceId, description }
1491
2168
  *
1492
- * Without X-Payment header: returns 402 with payment requirements
1493
- * With X-Payment header: verifies payment and returns result
2169
+ * For x402 (base, polygon, base_sepolia):
2170
+ * Without X-Payment header: returns 402 with X-Payment-Required
2171
+ * With X-Payment header: verifies payment via CDP
2172
+ *
2173
+ * For MPP (tempo_moderato):
2174
+ * Without Authorization header: returns 402 with WWW-Authenticate
2175
+ * With Authorization: Payment header: verifies tx on Tempo chain
1494
2176
  */
1495
- async handleProxy(body, paymentHeader, res) {
2177
+ async handleProxy(body, paymentHeader, authHeader, res) {
1496
2178
  const { wallet, amount, currency, chain, memo, serviceId, description } = body;
1497
2179
  if (!wallet || !amount) {
1498
2180
  return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
1499
2181
  }
1500
- if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) {
1501
- return this.sendJson(res, 400, { error: "Invalid wallet address format" });
2182
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
2183
+ if (chain && !supportedChains.includes(chain)) {
2184
+ return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
2185
+ }
2186
+ const isSolanaChain = chain === "solana" || chain === "solana_devnet";
2187
+ const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
2188
+ const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
2189
+ if (isSolanaChain && !isValidSolanaAddress) {
2190
+ return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
2191
+ }
2192
+ if (!isSolanaChain && !isValidEvmAddress) {
2193
+ return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
1502
2194
  }
1503
2195
  const amountNum = parseFloat(amount);
1504
2196
  if (isNaN(amountNum) || amountNum <= 0) {
1505
2197
  return this.sendJson(res, 400, { error: "Invalid amount" });
1506
2198
  }
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
2199
  const proxyConfig = {
1512
2200
  id: serviceId || "proxy",
1513
2201
  name: description || "Proxy Payment",
@@ -1519,6 +2207,9 @@ var MoltsPayServer = class {
1519
2207
  input: {},
1520
2208
  output: {}
1521
2209
  };
2210
+ if (chain === "tempo_moderato") {
2211
+ return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
2212
+ }
1522
2213
  const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
1523
2214
  if (!paymentHeader) {
1524
2215
  return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
@@ -1554,7 +2245,6 @@ var MoltsPayServer = class {
1554
2245
  console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
1555
2246
  const { execute, service, params } = body;
1556
2247
  if (execute && service) {
1557
- console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
1558
2248
  const skill = this.skills.get(service);
1559
2249
  if (!skill) {
1560
2250
  console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
@@ -1564,6 +2254,32 @@ var MoltsPayServer = class {
1564
2254
  error: `Service not found: ${service}`
1565
2255
  });
1566
2256
  }
2257
+ const isSolana = isSolanaNetwork(network);
2258
+ let settlement2 = null;
2259
+ if (isSolana) {
2260
+ console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
2261
+ try {
2262
+ settlement2 = await this.registry.settle(payment, requirements);
2263
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
2264
+ if (!settlement2.success) {
2265
+ console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
2266
+ return this.sendJson(res, 402, {
2267
+ success: false,
2268
+ paymentSettled: false,
2269
+ error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
2270
+ });
2271
+ }
2272
+ } catch (err) {
2273
+ console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
2274
+ return this.sendJson(res, 402, {
2275
+ success: false,
2276
+ paymentSettled: false,
2277
+ error: `Payment settlement failed: ${err.message}`
2278
+ });
2279
+ }
2280
+ } else {
2281
+ console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
2282
+ }
1567
2283
  const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
1568
2284
  let result;
1569
2285
  try {
@@ -1573,34 +2289,36 @@ var MoltsPayServer = class {
1573
2289
  (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
1574
2290
  )
1575
2291
  ]);
1576
- console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
2292
+ console.log(`[MoltsPay] /proxy: Skill succeeded`);
1577
2293
  } catch (err) {
1578
- console.error(`[MoltsPay] /proxy: Skill failed: ${err.message} - NOT settling`);
2294
+ console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
1579
2295
  return this.sendJson(res, 500, {
1580
2296
  success: false,
1581
- paymentSettled: false,
1582
- error: `Service execution failed: ${err.message}`
2297
+ paymentSettled: isSolana ? true : false,
2298
+ error: `Service execution failed: ${err.message}`,
2299
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
1583
2300
  });
1584
2301
  }
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
- });
2302
+ if (!isSolana) {
2303
+ console.log(`[MoltsPay] /proxy: Settling payment...`);
2304
+ try {
2305
+ settlement2 = await this.registry.settle(payment, requirements);
2306
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
2307
+ } catch (err) {
2308
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
2309
+ return this.sendJson(res, 200, {
2310
+ success: true,
2311
+ verified: true,
2312
+ settled: false,
2313
+ settlementError: err.message,
2314
+ from: payment.payload?.authorization?.from,
2315
+ paidTo: wallet,
2316
+ amount: amountNum,
2317
+ currency: currency || "USDC",
2318
+ memo,
2319
+ result
2320
+ });
2321
+ }
1604
2322
  }
1605
2323
  return this.sendJson(res, 200, {
1606
2324
  success: true,
@@ -1608,7 +2326,6 @@ var MoltsPayServer = class {
1608
2326
  settled: settlement2?.success || false,
1609
2327
  txHash: settlement2?.transaction,
1610
2328
  from: payment.payload?.authorization?.from,
1611
- // Buyer's wallet address
1612
2329
  paidTo: wallet,
1613
2330
  amount: amountNum,
1614
2331
  currency: currency || "USDC",
@@ -1643,6 +2360,131 @@ var MoltsPayServer = class {
1643
2360
  memo
1644
2361
  });
1645
2362
  }
2363
+ /**
2364
+ * Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
2365
+ */
2366
+ async handleProxyMPP(body, config, authHeader, res) {
2367
+ const { wallet, amount, memo, serviceId } = body;
2368
+ const amountNum = parseFloat(amount);
2369
+ const amountInUnits = Math.floor(amountNum * 1e6).toString();
2370
+ if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
2371
+ const challengeId = this.generateChallengeId();
2372
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
2373
+ const mppRequest = {
2374
+ amount: amountInUnits,
2375
+ currency: tokenAddress,
2376
+ methodDetails: {
2377
+ chainId: 42431,
2378
+ feePayer: true
2379
+ },
2380
+ recipient: wallet
2381
+ };
2382
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
2383
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
2384
+ const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
2385
+ res.writeHead(402, {
2386
+ "Content-Type": "application/problem+json",
2387
+ [MPP_WWW_AUTH_HEADER]: wwwAuth
2388
+ });
2389
+ res.end(JSON.stringify({
2390
+ type: "https://paymentauth.org/problems/payment-required",
2391
+ title: "Payment Required",
2392
+ status: 402,
2393
+ detail: `Payment is required (${config.name}).`,
2394
+ service: serviceId || "proxy",
2395
+ price: amountNum,
2396
+ currency: "USDC"
2397
+ }, null, 2));
2398
+ return;
2399
+ }
2400
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
2401
+ if (!credentialMatch) {
2402
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
2403
+ }
2404
+ let mppCredential;
2405
+ try {
2406
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
2407
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
2408
+ mppCredential = JSON.parse(decoded);
2409
+ } catch (err) {
2410
+ console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
2411
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
2412
+ }
2413
+ let txHash;
2414
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
2415
+ txHash = mppCredential.payload.hash;
2416
+ } else {
2417
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
2418
+ }
2419
+ console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
2420
+ const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
2421
+ const paymentPayload = {
2422
+ x402Version: X402_VERSION2,
2423
+ scheme: "exact",
2424
+ network: "eip155:42431",
2425
+ payload: { txHash, chainId: 42431 }
2426
+ };
2427
+ const verification = await this.registry.verify(paymentPayload, requirements);
2428
+ if (!verification.valid) {
2429
+ return this.sendJson(res, 402, {
2430
+ error: `Payment verification failed: ${verification.error}`
2431
+ });
2432
+ }
2433
+ console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
2434
+ const { execute, service, params } = body;
2435
+ if (execute && service) {
2436
+ console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
2437
+ const skill = this.skills.get(service);
2438
+ if (!skill) {
2439
+ return this.sendJson(res, 404, {
2440
+ success: false,
2441
+ paymentSettled: true,
2442
+ // Payment already happened on Tempo
2443
+ error: `Service not found: ${service}`
2444
+ });
2445
+ }
2446
+ const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
2447
+ let result;
2448
+ try {
2449
+ result = await Promise.race([
2450
+ skill.handler(params || {}),
2451
+ new Promise(
2452
+ (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
2453
+ )
2454
+ ]);
2455
+ } catch (err) {
2456
+ console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
2457
+ return this.sendJson(res, 500, {
2458
+ success: false,
2459
+ paymentSettled: true,
2460
+ error: `Service execution failed: ${err.message}`
2461
+ });
2462
+ }
2463
+ return this.sendJson(res, 200, {
2464
+ success: true,
2465
+ verified: true,
2466
+ txHash,
2467
+ chain: "tempo_moderato",
2468
+ paidTo: wallet,
2469
+ amount: amountNum,
2470
+ currency: "USDC",
2471
+ facilitator: verification.facilitator,
2472
+ memo,
2473
+ result
2474
+ });
2475
+ }
2476
+ this.sendJson(res, 200, {
2477
+ success: true,
2478
+ verified: true,
2479
+ txHash,
2480
+ chain: "tempo_moderato",
2481
+ paidTo: wallet,
2482
+ amount: amountNum,
2483
+ currency: "USDC",
2484
+ facilitator: verification.facilitator,
2485
+ memo
2486
+ });
2487
+ }
1646
2488
  /**
1647
2489
  * Build payment requirements for proxy endpoint (uses provided wallet)
1648
2490
  */
@@ -1654,7 +2496,7 @@ var MoltsPayServer = class {
1654
2496
  const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
1655
2497
  const tokenAddress = tokenAddresses[selectedToken];
1656
2498
  const tokenDomain = getTokenDomain(networkId, selectedToken);
1657
- return {
2499
+ const requirements = {
1658
2500
  scheme: "exact",
1659
2501
  network: networkId,
1660
2502
  asset: tokenAddress,
@@ -1664,6 +2506,17 @@ var MoltsPayServer = class {
1664
2506
  maxTimeoutSeconds: 300,
1665
2507
  extra: tokenDomain
1666
2508
  };
2509
+ if (networkId === "eip155:56" || networkId === "eip155:97") {
2510
+ const bnbFacilitator = this.registry.get("bnb");
2511
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
2512
+ if (spenderAddress) {
2513
+ requirements.extra = {
2514
+ ...requirements.extra || {},
2515
+ bnbSpender: spenderAddress
2516
+ };
2517
+ }
2518
+ }
2519
+ return requirements;
1667
2520
  }
1668
2521
  /**
1669
2522
  * Return 402 with x402 payment requirements for proxy endpoint