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
@@ -336,6 +336,63 @@ var CHAINS = {
336
336
  explorerTx: "https://explore.testnet.tempo.xyz/tx/",
337
337
  avgBlockTime: 0.5
338
338
  // ~500ms finality
339
+ },
340
+ // ============ BNB Chain Testnet ============
341
+ bnb_testnet: {
342
+ name: "BNB Testnet",
343
+ chainId: 97,
344
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
345
+ tokens: {
346
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
347
+ // Using official Binance-Peg testnet tokens
348
+ USDC: {
349
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
350
+ // Testnet USDC
351
+ decimals: 18,
352
+ symbol: "USDC",
353
+ eip712Name: "USD Coin"
354
+ },
355
+ USDT: {
356
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
357
+ // Testnet USDT
358
+ decimals: 18,
359
+ symbol: "USDT",
360
+ eip712Name: "Tether USD"
361
+ }
362
+ },
363
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
364
+ explorer: "https://testnet.bscscan.com/address/",
365
+ explorerTx: "https://testnet.bscscan.com/tx/",
366
+ avgBlockTime: 3,
367
+ // BNB-specific: requires approval for pay-for-success flow
368
+ requiresApproval: true
369
+ },
370
+ // ============ BNB Chain Mainnet ============
371
+ bnb: {
372
+ name: "BNB Smart Chain",
373
+ chainId: 56,
374
+ rpc: "https://bsc-dataseed.binance.org",
375
+ tokens: {
376
+ // Note: BNB uses 18 decimals for stablecoins
377
+ USDC: {
378
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
379
+ decimals: 18,
380
+ symbol: "USDC",
381
+ eip712Name: "USD Coin"
382
+ },
383
+ USDT: {
384
+ address: "0x55d398326f99059fF775485246999027B3197955",
385
+ decimals: 18,
386
+ symbol: "USDT",
387
+ eip712Name: "Tether USD"
388
+ }
389
+ },
390
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
391
+ explorer: "https://bscscan.com/address/",
392
+ explorerTx: "https://bscscan.com/tx/",
393
+ avgBlockTime: 3,
394
+ // BNB-specific: requires approval for pay-for-success flow
395
+ requiresApproval: true
339
396
  }
340
397
  };
341
398
 
@@ -459,7 +516,538 @@ var TempoFacilitator = class extends BaseFacilitator {
459
516
  }
460
517
  };
461
518
 
519
+ // src/facilitators/bnb.ts
520
+ import { privateKeyToAccount } from "viem/accounts";
521
+ var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
522
+ var EIP712_DOMAIN = {
523
+ name: "MoltsPay",
524
+ version: "1"
525
+ };
526
+ var INTENT_TYPES = {
527
+ PaymentIntent: [
528
+ { name: "from", type: "address" },
529
+ { name: "to", type: "address" },
530
+ { name: "amount", type: "uint256" },
531
+ { name: "token", type: "address" },
532
+ { name: "service", type: "string" },
533
+ { name: "nonce", type: "uint256" },
534
+ { name: "deadline", type: "uint256" }
535
+ ]
536
+ };
537
+ var BNBFacilitator = class extends BaseFacilitator {
538
+ name = "bnb";
539
+ displayName = "BNB Smart Chain";
540
+ supportedNetworks = ["eip155:56", "eip155:97"];
541
+ // Mainnet + Testnet
542
+ serverPrivateKey;
543
+ spenderAddress = null;
544
+ chainConfigs;
545
+ constructor(serverPrivateKey) {
546
+ super();
547
+ this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
548
+ if (this.serverPrivateKey) {
549
+ const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
550
+ const account = privateKeyToAccount(key);
551
+ this.spenderAddress = account.address;
552
+ }
553
+ this.chainConfigs = {
554
+ 56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
555
+ 97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
556
+ };
557
+ }
558
+ async healthCheck() {
559
+ const start = Date.now();
560
+ try {
561
+ const response = await fetch(this.chainConfigs[56].rpc, {
562
+ method: "POST",
563
+ headers: { "Content-Type": "application/json" },
564
+ body: JSON.stringify({
565
+ jsonrpc: "2.0",
566
+ method: "eth_chainId",
567
+ params: [],
568
+ id: 1
569
+ })
570
+ });
571
+ const data = await response.json();
572
+ const chainId = parseInt(data.result, 16);
573
+ if (chainId !== 56) {
574
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
575
+ }
576
+ return { healthy: true, latencyMs: Date.now() - start };
577
+ } catch (error) {
578
+ return { healthy: false, error: String(error) };
579
+ }
580
+ }
581
+ /**
582
+ * Verify a payment intent signature (before service execution)
583
+ *
584
+ * This verifies:
585
+ * 1. Signature is valid for the intent
586
+ * 2. Client has approved server wallet
587
+ * 3. Client has sufficient balance
588
+ * 4. Intent hasn't expired
589
+ */
590
+ async verify(paymentPayload, requirements) {
591
+ try {
592
+ const bnbPayload = paymentPayload.payload;
593
+ if (!bnbPayload?.intent) {
594
+ return { valid: false, error: "Missing intent in payment payload" };
595
+ }
596
+ const { intent, chainId } = bnbPayload;
597
+ const config = this.chainConfigs[chainId];
598
+ if (!config) {
599
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
600
+ }
601
+ if (intent.deadline < Date.now()) {
602
+ return { valid: false, error: "Intent expired" };
603
+ }
604
+ const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
605
+ if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
606
+ return { valid: false, error: "Invalid signature" };
607
+ }
608
+ if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
609
+ return { valid: false, error: `Wrong recipient: ${intent.to}` };
610
+ }
611
+ if (BigInt(intent.amount) < BigInt(requirements.amount)) {
612
+ return { valid: false, error: `Insufficient amount: ${intent.amount}` };
613
+ }
614
+ if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
615
+ return { valid: false, error: `Wrong token: ${intent.token}` };
616
+ }
617
+ const serverAddress = await this.getServerAddress();
618
+ const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
619
+ if (BigInt(allowance) < BigInt(intent.amount)) {
620
+ return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
621
+ }
622
+ const balance = await this.getBalance(intent.from, intent.token, config.rpc);
623
+ if (BigInt(balance) < BigInt(intent.amount)) {
624
+ return { valid: false, error: "Insufficient balance" };
625
+ }
626
+ return {
627
+ valid: true,
628
+ details: {
629
+ from: intent.from,
630
+ to: intent.to,
631
+ amount: intent.amount,
632
+ token: intent.token,
633
+ service: intent.service,
634
+ nonce: intent.nonce,
635
+ deadline: intent.deadline
636
+ }
637
+ };
638
+ } catch (error) {
639
+ return { valid: false, error: `Verification failed: ${error}` };
640
+ }
641
+ }
642
+ /**
643
+ * Settle a payment by executing transferFrom
644
+ *
645
+ * This is called AFTER the service has been successfully delivered.
646
+ * Server pays gas, transfers tokens from client to provider.
647
+ */
648
+ async settle(paymentPayload, requirements) {
649
+ if (!this.serverPrivateKey) {
650
+ return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
651
+ }
652
+ try {
653
+ const verifyResult = await this.verify(paymentPayload, requirements);
654
+ if (!verifyResult.valid) {
655
+ return { success: false, error: verifyResult.error };
656
+ }
657
+ const bnbPayload = paymentPayload.payload;
658
+ const { intent, chainId } = bnbPayload;
659
+ const config = this.chainConfigs[chainId];
660
+ const txHash = await this.executeTransferFrom(
661
+ intent.from,
662
+ intent.to,
663
+ intent.amount,
664
+ intent.token,
665
+ config.rpc
666
+ );
667
+ return {
668
+ success: true,
669
+ transaction: txHash,
670
+ status: "settled"
671
+ };
672
+ } catch (error) {
673
+ return { success: false, error: `Settlement failed: ${error}` };
674
+ }
675
+ }
676
+ /**
677
+ * Check if client has approved the server wallet
678
+ */
679
+ async checkApproval(clientAddress, token, chainId) {
680
+ const config = this.chainConfigs[chainId];
681
+ if (!config) {
682
+ throw new Error(`Unsupported chainId: ${chainId}`);
683
+ }
684
+ const serverAddress = await this.getServerAddress();
685
+ const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
686
+ const minAllowance = BigInt("1000000000000000000000");
687
+ return {
688
+ approved: BigInt(allowance) >= minAllowance,
689
+ allowance
690
+ };
691
+ }
692
+ /**
693
+ * Verify a completed transaction (for checking past payments)
694
+ */
695
+ async verifyTransaction(txHash, expected, chainId) {
696
+ const config = this.chainConfigs[chainId];
697
+ if (!config) {
698
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
699
+ }
700
+ try {
701
+ const receipt = await this.getTransactionReceipt(txHash, config.rpc);
702
+ if (!receipt) {
703
+ return { valid: false, error: "Transaction not found" };
704
+ }
705
+ if (receipt.status !== "0x1") {
706
+ return { valid: false, error: "Transaction failed" };
707
+ }
708
+ const transferLog = receipt.logs.find(
709
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
710
+ );
711
+ if (!transferLog) {
712
+ return { valid: false, error: "No Transfer event found" };
713
+ }
714
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
715
+ if (toAddress !== expected.to.toLowerCase()) {
716
+ return { valid: false, error: `Wrong recipient: ${toAddress}` };
717
+ }
718
+ const amount = BigInt(transferLog.data);
719
+ if (amount < BigInt(expected.amount)) {
720
+ return { valid: false, error: `Insufficient amount: ${amount}` };
721
+ }
722
+ return {
723
+ valid: true,
724
+ details: {
725
+ txHash,
726
+ from: "0x" + transferLog.topics[1].slice(26),
727
+ to: toAddress,
728
+ amount: amount.toString(),
729
+ token: transferLog.address
730
+ }
731
+ };
732
+ } catch (error) {
733
+ return { valid: false, error: `Verification failed: ${error}` };
734
+ }
735
+ }
736
+ // ==================== Private Methods ====================
737
+ /**
738
+ * Get the server's spender address (public, for 402 responses)
739
+ * Returns cached value computed at construction time.
740
+ */
741
+ getSpenderAddress() {
742
+ return this.spenderAddress;
743
+ }
744
+ async getServerAddress() {
745
+ const { ethers } = await import("ethers");
746
+ const wallet = new ethers.Wallet(this.serverPrivateKey);
747
+ return wallet.address;
748
+ }
749
+ async recoverIntentSigner(intent, chainId) {
750
+ const { ethers } = await import("ethers");
751
+ const domain = {
752
+ ...EIP712_DOMAIN,
753
+ chainId
754
+ };
755
+ const message = {
756
+ from: intent.from,
757
+ to: intent.to,
758
+ amount: intent.amount,
759
+ token: intent.token,
760
+ service: intent.service,
761
+ nonce: intent.nonce,
762
+ deadline: intent.deadline
763
+ };
764
+ const recoveredAddress = ethers.verifyTypedData(
765
+ domain,
766
+ INTENT_TYPES,
767
+ message,
768
+ intent.signature
769
+ );
770
+ return recoveredAddress;
771
+ }
772
+ async getAllowance(owner, spender, token, rpcUrl) {
773
+ const selector = "0xdd62ed3e";
774
+ const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
775
+ const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
776
+ const data = selector + ownerPadded + spenderPadded;
777
+ const response = await fetch(rpcUrl, {
778
+ method: "POST",
779
+ headers: { "Content-Type": "application/json" },
780
+ body: JSON.stringify({
781
+ jsonrpc: "2.0",
782
+ method: "eth_call",
783
+ params: [{ to: token, data }, "latest"],
784
+ id: 1
785
+ })
786
+ });
787
+ const result = await response.json();
788
+ return result.result || "0x0";
789
+ }
790
+ async getBalance(account, token, rpcUrl) {
791
+ const selector = "0x70a08231";
792
+ const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
793
+ const data = selector + accountPadded;
794
+ const response = await fetch(rpcUrl, {
795
+ method: "POST",
796
+ headers: { "Content-Type": "application/json" },
797
+ body: JSON.stringify({
798
+ jsonrpc: "2.0",
799
+ method: "eth_call",
800
+ params: [{ to: token, data }, "latest"],
801
+ id: 1
802
+ })
803
+ });
804
+ const result = await response.json();
805
+ return result.result || "0x0";
806
+ }
807
+ async executeTransferFrom(from, to, amount, token, rpcUrl) {
808
+ const { ethers } = await import("ethers");
809
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
810
+ const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
811
+ const tokenContract = new ethers.Contract(token, [
812
+ "function transferFrom(address from, address to, uint256 amount) returns (bool)"
813
+ ], wallet);
814
+ const tx = await tokenContract.transferFrom(from, to, amount);
815
+ const receipt = await tx.wait();
816
+ return receipt.hash;
817
+ }
818
+ async getTransactionReceipt(txHash, rpcUrl) {
819
+ const response = await fetch(rpcUrl, {
820
+ method: "POST",
821
+ headers: { "Content-Type": "application/json" },
822
+ body: JSON.stringify({
823
+ jsonrpc: "2.0",
824
+ method: "eth_getTransactionReceipt",
825
+ params: [txHash],
826
+ id: 1
827
+ })
828
+ });
829
+ const data = await response.json();
830
+ return data.result;
831
+ }
832
+ };
833
+
834
+ // src/facilitators/solana.ts
835
+ import {
836
+ Connection as Connection2,
837
+ PublicKey as PublicKey2,
838
+ Transaction,
839
+ VersionedTransaction
840
+ } from "@solana/web3.js";
841
+ import {
842
+ getAssociatedTokenAddress,
843
+ createTransferCheckedInstruction,
844
+ getAccount,
845
+ createAssociatedTokenAccountInstruction
846
+ } from "@solana/spl-token";
847
+
848
+ // src/chains/solana.ts
849
+ import { Connection, PublicKey } from "@solana/web3.js";
850
+ var SOLANA_CHAINS = {
851
+ solana: {
852
+ name: "Solana Mainnet",
853
+ cluster: "mainnet-beta",
854
+ rpc: "https://api.mainnet-beta.solana.com",
855
+ explorer: "https://solscan.io/account/",
856
+ explorerTx: "https://solscan.io/tx/",
857
+ tokens: {
858
+ USDC: {
859
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
860
+ // Circle official USDC
861
+ decimals: 6
862
+ }
863
+ }
864
+ },
865
+ solana_devnet: {
866
+ name: "Solana Devnet",
867
+ cluster: "devnet",
868
+ rpc: "https://api.devnet.solana.com",
869
+ explorer: "https://solscan.io/account/",
870
+ explorerTx: "https://solscan.io/tx/",
871
+ tokens: {
872
+ USDC: {
873
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
874
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
875
+ decimals: 6
876
+ }
877
+ }
878
+ }
879
+ };
880
+
881
+ // src/facilitators/solana.ts
882
+ var SolanaFacilitator = class extends BaseFacilitator {
883
+ name = "solana";
884
+ displayName = "Solana Direct";
885
+ supportedNetworks = ["solana:mainnet", "solana:devnet"];
886
+ connections = /* @__PURE__ */ new Map();
887
+ feePayerKeypair;
888
+ constructor(config) {
889
+ super();
890
+ this.feePayerKeypair = config?.feePayerKeypair;
891
+ for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
892
+ this.connections.set(
893
+ chain,
894
+ new Connection2(config2.rpc, "confirmed")
895
+ );
896
+ }
897
+ if (this.feePayerKeypair) {
898
+ console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
899
+ }
900
+ }
901
+ /**
902
+ * Get fee payer public key (for gasless transactions)
903
+ */
904
+ getFeePayerPubkey() {
905
+ return this.feePayerKeypair?.publicKey.toBase58() || null;
906
+ }
907
+ getConnection(chain) {
908
+ const conn = this.connections.get(chain);
909
+ if (!conn) {
910
+ throw new Error(`No connection for chain: ${chain}`);
911
+ }
912
+ return conn;
913
+ }
914
+ /**
915
+ * Convert our chain name to network identifier
916
+ */
917
+ static chainToNetwork(chain) {
918
+ return chain === "solana" ? "solana:mainnet" : "solana:devnet";
919
+ }
920
+ /**
921
+ * Convert network identifier to chain name
922
+ */
923
+ static networkToChain(network) {
924
+ if (network === "solana:mainnet") return "solana";
925
+ if (network === "solana:devnet") return "solana_devnet";
926
+ return null;
927
+ }
928
+ async healthCheck() {
929
+ const start = Date.now();
930
+ try {
931
+ const conn = this.getConnection("solana_devnet");
932
+ await conn.getSlot();
933
+ return {
934
+ healthy: true,
935
+ latencyMs: Date.now() - start
936
+ };
937
+ } catch (error) {
938
+ return {
939
+ healthy: false,
940
+ error: error.message
941
+ };
942
+ }
943
+ }
944
+ /**
945
+ * Verify a Solana payment
946
+ *
947
+ * Checks:
948
+ * 1. Transaction is valid and properly signed
949
+ * 2. Transfer instruction matches expected amount and recipient
950
+ */
951
+ async verify(paymentPayload, requirements) {
952
+ try {
953
+ const solanaPayload = paymentPayload.payload;
954
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
955
+ return { valid: false, error: "Missing signed transaction" };
956
+ }
957
+ const chain = solanaPayload.chain || "solana_devnet";
958
+ const chainConfig = SOLANA_CHAINS[chain];
959
+ if (!chainConfig) {
960
+ return { valid: false, error: `Invalid chain: ${chain}` };
961
+ }
962
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
963
+ let tx;
964
+ try {
965
+ tx = Transaction.from(txBuffer);
966
+ } catch {
967
+ tx = VersionedTransaction.deserialize(txBuffer);
968
+ }
969
+ if (tx instanceof Transaction) {
970
+ const hasAnySignature = tx.signatures.some(
971
+ (sig) => sig.signature && !sig.signature.every((b) => b === 0)
972
+ );
973
+ if (!hasAnySignature) {
974
+ return { valid: false, error: "Transaction not signed" };
975
+ }
976
+ }
977
+ const expectedAmount = BigInt(requirements.amount);
978
+ const expectedRecipient = new PublicKey2(requirements.payTo);
979
+ return {
980
+ valid: true,
981
+ details: {
982
+ chain,
983
+ sender: solanaPayload.sender,
984
+ recipient: requirements.payTo,
985
+ amount: requirements.amount
986
+ }
987
+ };
988
+ } catch (error) {
989
+ return { valid: false, error: error.message };
990
+ }
991
+ }
992
+ /**
993
+ * Settle a Solana payment
994
+ *
995
+ * Submits the signed transaction to the network.
996
+ * In gasless mode, adds fee payer signature before submitting.
997
+ */
998
+ async settle(paymentPayload, requirements) {
999
+ try {
1000
+ const solanaPayload = paymentPayload.payload;
1001
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
1002
+ return { success: false, error: "Missing signed transaction" };
1003
+ }
1004
+ const chain = solanaPayload.chain || "solana_devnet";
1005
+ const connection = this.getConnection(chain);
1006
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
1007
+ let txToSend;
1008
+ try {
1009
+ const tx = Transaction.from(txBuffer);
1010
+ if (this.feePayerKeypair && tx.feePayer) {
1011
+ const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
1012
+ const txFeePayer = tx.feePayer.toBase58();
1013
+ if (txFeePayer === feePayerPubkey) {
1014
+ console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
1015
+ tx.partialSign(this.feePayerKeypair);
1016
+ }
1017
+ }
1018
+ txToSend = tx.serialize();
1019
+ } catch (e) {
1020
+ txToSend = txBuffer;
1021
+ }
1022
+ const signature = await connection.sendRawTransaction(txToSend, {
1023
+ skipPreflight: false,
1024
+ preflightCommitment: "confirmed"
1025
+ });
1026
+ const confirmation = await connection.confirmTransaction(signature, "confirmed");
1027
+ if (confirmation.value.err) {
1028
+ return {
1029
+ success: false,
1030
+ error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
1031
+ transaction: signature
1032
+ };
1033
+ }
1034
+ return {
1035
+ success: true,
1036
+ transaction: signature,
1037
+ status: "confirmed"
1038
+ };
1039
+ } catch (error) {
1040
+ return { success: false, error: error.message };
1041
+ }
1042
+ }
1043
+ supportsNetwork(network) {
1044
+ return this.supportedNetworks.includes(network);
1045
+ }
1046
+ };
1047
+
462
1048
  // src/facilitators/registry.ts
1049
+ import { Keypair as Keypair2 } from "@solana/web3.js";
1050
+ import bs58 from "bs58";
463
1051
  var FacilitatorRegistry = class {
464
1052
  factories = /* @__PURE__ */ new Map();
465
1053
  instances = /* @__PURE__ */ new Map();
@@ -468,7 +1056,20 @@ var FacilitatorRegistry = class {
468
1056
  constructor(selection) {
469
1057
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
470
1058
  this.registerFactory("tempo", () => new TempoFacilitator());
471
- this.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
1059
+ this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
1060
+ this.registerFactory("solana", (config) => {
1061
+ let feePayerKeypair;
1062
+ const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
1063
+ if (feePayerKey) {
1064
+ try {
1065
+ feePayerKeypair = Keypair2.fromSecretKey(bs58.decode(feePayerKey));
1066
+ } catch (e) {
1067
+ console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
1068
+ }
1069
+ }
1070
+ return new SolanaFacilitator({ feePayerKeypair });
1071
+ });
1072
+ this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
472
1073
  }
473
1074
  /**
474
1075
  * Register a new facilitator factory
@@ -712,14 +1313,40 @@ var TOKEN_ADDRESSES = {
712
1313
  // pathUSD
713
1314
  USDT: "0x20c0000000000000000000000000000000000001"
714
1315
  // alphaUSD
1316
+ },
1317
+ // BNB Smart Chain mainnet
1318
+ "eip155:56": {
1319
+ USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
1320
+ USDT: "0x55d398326f99059fF775485246999027B3197955"
1321
+ },
1322
+ // BNB Smart Chain testnet
1323
+ "eip155:97": {
1324
+ USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
1325
+ USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
1326
+ },
1327
+ // Solana networks use mint addresses (SPL tokens)
1328
+ "solana:mainnet": {
1329
+ USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
1330
+ // Circle USDC
1331
+ },
1332
+ "solana:devnet": {
1333
+ USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
1334
+ // Devnet USDC
715
1335
  }
716
1336
  };
717
1337
  var CHAIN_TO_NETWORK = {
718
1338
  "base": "eip155:8453",
719
1339
  "base_sepolia": "eip155:84532",
720
1340
  "polygon": "eip155:137",
721
- "tempo_moderato": "eip155:42431"
1341
+ "tempo_moderato": "eip155:42431",
1342
+ "bnb": "eip155:56",
1343
+ "bnb_testnet": "eip155:97",
1344
+ "solana": "solana:mainnet",
1345
+ "solana_devnet": "solana:devnet"
722
1346
  };
1347
+ function isSolanaNetwork(network) {
1348
+ return network.startsWith("solana:");
1349
+ }
723
1350
  var TOKEN_DOMAINS = {
724
1351
  // Base mainnet
725
1352
  "eip155:8453": {
@@ -741,6 +1368,16 @@ var TOKEN_DOMAINS = {
741
1368
  "eip155:42431": {
742
1369
  USDC: { name: "pathUSD", version: "1" },
743
1370
  USDT: { name: "alphaUSD", version: "1" }
1371
+ },
1372
+ // BNB Smart Chain mainnet
1373
+ "eip155:56": {
1374
+ USDC: { name: "USD Coin", version: "1" },
1375
+ USDT: { name: "Tether USD", version: "1" }
1376
+ },
1377
+ // BNB Smart Chain testnet
1378
+ "eip155:97": {
1379
+ USDC: { name: "USD Coin", version: "1" },
1380
+ USDT: { name: "Tether USD", version: "1" }
744
1381
  }
745
1382
  };
746
1383
  function getTokenDomain(network, token) {
@@ -798,7 +1435,7 @@ var MoltsPayServer = class {
798
1435
  };
799
1436
  this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
800
1437
  this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
801
- const defaultFallback = ["tempo"];
1438
+ const defaultFallback = ["tempo", "bnb", "solana"];
802
1439
  const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
803
1440
  const facilitatorConfig = options.facilitators || {
804
1441
  primary: process.env.FACILITATOR_PRIMARY || "cdp",
@@ -841,12 +1478,20 @@ var MoltsPayServer = class {
841
1478
  */
842
1479
  getProviderChains() {
843
1480
  const provider = this.manifest.provider;
1481
+ const getWalletForChain = (chainName, explicitWallet) => {
1482
+ if (explicitWallet) return explicitWallet;
1483
+ if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
1484
+ return provider.solana_wallet;
1485
+ }
1486
+ return provider.wallet;
1487
+ };
844
1488
  if (provider.chains && provider.chains.length > 0) {
845
1489
  return provider.chains.map((c) => {
846
1490
  const chainName = typeof c === "string" ? c : c.chain;
1491
+ const explicitWallet = typeof c === "object" ? c.wallet : null;
847
1492
  return {
848
1493
  network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
849
- wallet: (typeof c === "object" ? c.wallet : null) || provider.wallet,
1494
+ wallet: getWalletForChain(chainName, explicitWallet || void 0),
850
1495
  tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
851
1496
  };
852
1497
  });
@@ -855,7 +1500,7 @@ var MoltsPayServer = class {
855
1500
  const network = CHAIN_TO_NETWORK[chain] || this.networkId;
856
1501
  return [{
857
1502
  network,
858
- wallet: provider.wallet,
1503
+ wallet: getWalletForChain(chain),
859
1504
  tokens: ["USDC"]
860
1505
  }];
861
1506
  }
@@ -926,7 +1571,8 @@ var MoltsPayServer = class {
926
1571
  }
927
1572
  const body = await this.readBody(req);
928
1573
  const paymentHeader = req.headers[PAYMENT_HEADER];
929
- return await this.handleProxy(body, paymentHeader, res);
1574
+ const authHeader = req.headers[MPP_AUTH_HEADER];
1575
+ return await this.handleProxy(body, paymentHeader, authHeader, res);
930
1576
  }
931
1577
  const servicePath = url.pathname.replace(/^\//, "");
932
1578
  const skill = this.skills.get(servicePath);
@@ -963,7 +1609,9 @@ var MoltsPayServer = class {
963
1609
  name: this.manifest.provider.name,
964
1610
  description: this.manifest.provider.description,
965
1611
  wallet: this.manifest.provider.wallet,
966
- chain: this.manifest.provider.chain || "base"
1612
+ chain: this.manifest.provider.chain || "base",
1613
+ solana_wallet: this.manifest.provider.solana_wallet,
1614
+ chains: this.manifest.provider.chains
967
1615
  },
968
1616
  services,
969
1617
  endpoints: {
@@ -1076,6 +1724,21 @@ var MoltsPayServer = class {
1076
1724
  });
1077
1725
  }
1078
1726
  console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
1727
+ const isSolana = isSolanaNetwork(paymentNetwork);
1728
+ let settlement = null;
1729
+ if (isSolana) {
1730
+ console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
1731
+ try {
1732
+ settlement = await this.registry.settle(payment, requirements);
1733
+ console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1734
+ } catch (err) {
1735
+ console.error("[MoltsPay] Solana settlement failed:", err.message);
1736
+ return this.sendJson(res, 402, {
1737
+ error: "Payment settlement failed",
1738
+ message: err.message
1739
+ });
1740
+ }
1741
+ }
1079
1742
  const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
1080
1743
  console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
1081
1744
  let result;
@@ -1090,16 +1753,19 @@ var MoltsPayServer = class {
1090
1753
  console.error("[MoltsPay] Skill execution failed:", err.message);
1091
1754
  return this.sendJson(res, 500, {
1092
1755
  error: "Service execution failed",
1093
- message: err.message
1756
+ message: err.message,
1757
+ paymentSettled: isSolana ? true : false,
1758
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
1094
1759
  });
1095
1760
  }
1096
- console.log(`[MoltsPay] Skill succeeded, settling payment...`);
1097
- let settlement = null;
1098
- try {
1099
- settlement = await this.registry.settle(payment, requirements);
1100
- console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1101
- } catch (err) {
1102
- console.error("[MoltsPay] Settlement failed:", err.message);
1761
+ if (!isSolana) {
1762
+ console.log(`[MoltsPay] Skill succeeded, settling payment...`);
1763
+ try {
1764
+ settlement = await this.registry.settle(payment, requirements);
1765
+ console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1766
+ } catch (err) {
1767
+ console.error("[MoltsPay] Settlement failed:", err.message);
1768
+ }
1103
1769
  }
1104
1770
  const responseHeaders = {};
1105
1771
  if (settlement?.success) {
@@ -1375,7 +2041,7 @@ var MoltsPayServer = class {
1375
2041
  const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
1376
2042
  const tokenAddress = tokenAddresses[selectedToken];
1377
2043
  const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
1378
- return {
2044
+ const requirements = {
1379
2045
  scheme: "exact",
1380
2046
  network: selectedNetwork,
1381
2047
  asset: tokenAddress,
@@ -1384,6 +2050,27 @@ var MoltsPayServer = class {
1384
2050
  maxTimeoutSeconds: 300,
1385
2051
  extra: tokenDomain
1386
2052
  };
2053
+ if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
2054
+ const solanaFacilitator = this.registry.get("solana");
2055
+ const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
2056
+ if (feePayerPubkey) {
2057
+ requirements.extra = {
2058
+ ...requirements.extra || {},
2059
+ solanaFeePayer: feePayerPubkey
2060
+ };
2061
+ }
2062
+ }
2063
+ if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
2064
+ const bnbFacilitator = this.registry.get("bnb");
2065
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
2066
+ if (spenderAddress) {
2067
+ requirements.extra = {
2068
+ ...requirements.extra || {},
2069
+ bnbSpender: spenderAddress
2070
+ };
2071
+ }
2072
+ }
2073
+ return requirements;
1387
2074
  }
1388
2075
  /**
1389
2076
  * Detect which token is being used in the payment
@@ -1449,31 +2136,42 @@ var MoltsPayServer = class {
1449
2136
  /**
1450
2137
  * POST /proxy - Handle payment for external services (moltspay-creators)
1451
2138
  *
1452
- * This endpoint allows other services to delegate x402 payment handling.
2139
+ * This endpoint allows other services to delegate x402/MPP payment handling.
1453
2140
  * It does NOT execute any skill - just handles payment verification/settlement.
1454
2141
  *
1455
2142
  * Request body:
1456
2143
  * { wallet, amount, currency, chain, memo, serviceId, description }
1457
2144
  *
1458
- * Without X-Payment header: returns 402 with payment requirements
1459
- * With X-Payment header: verifies payment and returns result
2145
+ * For x402 (base, polygon, base_sepolia):
2146
+ * Without X-Payment header: returns 402 with X-Payment-Required
2147
+ * With X-Payment header: verifies payment via CDP
2148
+ *
2149
+ * For MPP (tempo_moderato):
2150
+ * Without Authorization header: returns 402 with WWW-Authenticate
2151
+ * With Authorization: Payment header: verifies tx on Tempo chain
1460
2152
  */
1461
- async handleProxy(body, paymentHeader, res) {
2153
+ async handleProxy(body, paymentHeader, authHeader, res) {
1462
2154
  const { wallet, amount, currency, chain, memo, serviceId, description } = body;
1463
2155
  if (!wallet || !amount) {
1464
2156
  return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
1465
2157
  }
1466
- if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) {
1467
- return this.sendJson(res, 400, { error: "Invalid wallet address format" });
2158
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
2159
+ if (chain && !supportedChains.includes(chain)) {
2160
+ return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
2161
+ }
2162
+ const isSolanaChain = chain === "solana" || chain === "solana_devnet";
2163
+ const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
2164
+ const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
2165
+ if (isSolanaChain && !isValidSolanaAddress) {
2166
+ return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
2167
+ }
2168
+ if (!isSolanaChain && !isValidEvmAddress) {
2169
+ return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
1468
2170
  }
1469
2171
  const amountNum = parseFloat(amount);
1470
2172
  if (isNaN(amountNum) || amountNum <= 0) {
1471
2173
  return this.sendJson(res, 400, { error: "Invalid amount" });
1472
2174
  }
1473
- const supportedChains = ["base", "polygon", "base_sepolia"];
1474
- if (chain && !supportedChains.includes(chain)) {
1475
- return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
1476
- }
1477
2175
  const proxyConfig = {
1478
2176
  id: serviceId || "proxy",
1479
2177
  name: description || "Proxy Payment",
@@ -1485,6 +2183,9 @@ var MoltsPayServer = class {
1485
2183
  input: {},
1486
2184
  output: {}
1487
2185
  };
2186
+ if (chain === "tempo_moderato") {
2187
+ return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
2188
+ }
1488
2189
  const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
1489
2190
  if (!paymentHeader) {
1490
2191
  return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
@@ -1520,7 +2221,6 @@ var MoltsPayServer = class {
1520
2221
  console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
1521
2222
  const { execute, service, params } = body;
1522
2223
  if (execute && service) {
1523
- console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
1524
2224
  const skill = this.skills.get(service);
1525
2225
  if (!skill) {
1526
2226
  console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
@@ -1530,6 +2230,32 @@ var MoltsPayServer = class {
1530
2230
  error: `Service not found: ${service}`
1531
2231
  });
1532
2232
  }
2233
+ const isSolana = isSolanaNetwork(network);
2234
+ let settlement2 = null;
2235
+ if (isSolana) {
2236
+ console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
2237
+ try {
2238
+ settlement2 = await this.registry.settle(payment, requirements);
2239
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
2240
+ if (!settlement2.success) {
2241
+ console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
2242
+ return this.sendJson(res, 402, {
2243
+ success: false,
2244
+ paymentSettled: false,
2245
+ error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
2246
+ });
2247
+ }
2248
+ } catch (err) {
2249
+ console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
2250
+ return this.sendJson(res, 402, {
2251
+ success: false,
2252
+ paymentSettled: false,
2253
+ error: `Payment settlement failed: ${err.message}`
2254
+ });
2255
+ }
2256
+ } else {
2257
+ console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
2258
+ }
1533
2259
  const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
1534
2260
  let result;
1535
2261
  try {
@@ -1539,34 +2265,36 @@ var MoltsPayServer = class {
1539
2265
  (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
1540
2266
  )
1541
2267
  ]);
1542
- console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
2268
+ console.log(`[MoltsPay] /proxy: Skill succeeded`);
1543
2269
  } catch (err) {
1544
- console.error(`[MoltsPay] /proxy: Skill failed: ${err.message} - NOT settling`);
2270
+ console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
1545
2271
  return this.sendJson(res, 500, {
1546
2272
  success: false,
1547
- paymentSettled: false,
1548
- error: `Service execution failed: ${err.message}`
2273
+ paymentSettled: isSolana ? true : false,
2274
+ error: `Service execution failed: ${err.message}`,
2275
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
1549
2276
  });
1550
2277
  }
1551
- let settlement2 = null;
1552
- try {
1553
- settlement2 = await this.registry.settle(payment, requirements);
1554
- console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
1555
- } catch (err) {
1556
- console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
1557
- return this.sendJson(res, 200, {
1558
- success: true,
1559
- verified: true,
1560
- settled: false,
1561
- settlementError: err.message,
1562
- from: payment.payload?.authorization?.from,
1563
- // Buyer's wallet address
1564
- paidTo: wallet,
1565
- amount: amountNum,
1566
- currency: currency || "USDC",
1567
- memo,
1568
- result
1569
- });
2278
+ if (!isSolana) {
2279
+ console.log(`[MoltsPay] /proxy: Settling payment...`);
2280
+ try {
2281
+ settlement2 = await this.registry.settle(payment, requirements);
2282
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
2283
+ } catch (err) {
2284
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
2285
+ return this.sendJson(res, 200, {
2286
+ success: true,
2287
+ verified: true,
2288
+ settled: false,
2289
+ settlementError: err.message,
2290
+ from: payment.payload?.authorization?.from,
2291
+ paidTo: wallet,
2292
+ amount: amountNum,
2293
+ currency: currency || "USDC",
2294
+ memo,
2295
+ result
2296
+ });
2297
+ }
1570
2298
  }
1571
2299
  return this.sendJson(res, 200, {
1572
2300
  success: true,
@@ -1574,7 +2302,6 @@ var MoltsPayServer = class {
1574
2302
  settled: settlement2?.success || false,
1575
2303
  txHash: settlement2?.transaction,
1576
2304
  from: payment.payload?.authorization?.from,
1577
- // Buyer's wallet address
1578
2305
  paidTo: wallet,
1579
2306
  amount: amountNum,
1580
2307
  currency: currency || "USDC",
@@ -1609,6 +2336,131 @@ var MoltsPayServer = class {
1609
2336
  memo
1610
2337
  });
1611
2338
  }
2339
+ /**
2340
+ * Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
2341
+ */
2342
+ async handleProxyMPP(body, config, authHeader, res) {
2343
+ const { wallet, amount, memo, serviceId } = body;
2344
+ const amountNum = parseFloat(amount);
2345
+ const amountInUnits = Math.floor(amountNum * 1e6).toString();
2346
+ if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
2347
+ const challengeId = this.generateChallengeId();
2348
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
2349
+ const mppRequest = {
2350
+ amount: amountInUnits,
2351
+ currency: tokenAddress,
2352
+ methodDetails: {
2353
+ chainId: 42431,
2354
+ feePayer: true
2355
+ },
2356
+ recipient: wallet
2357
+ };
2358
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
2359
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
2360
+ const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
2361
+ res.writeHead(402, {
2362
+ "Content-Type": "application/problem+json",
2363
+ [MPP_WWW_AUTH_HEADER]: wwwAuth
2364
+ });
2365
+ res.end(JSON.stringify({
2366
+ type: "https://paymentauth.org/problems/payment-required",
2367
+ title: "Payment Required",
2368
+ status: 402,
2369
+ detail: `Payment is required (${config.name}).`,
2370
+ service: serviceId || "proxy",
2371
+ price: amountNum,
2372
+ currency: "USDC"
2373
+ }, null, 2));
2374
+ return;
2375
+ }
2376
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
2377
+ if (!credentialMatch) {
2378
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
2379
+ }
2380
+ let mppCredential;
2381
+ try {
2382
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
2383
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
2384
+ mppCredential = JSON.parse(decoded);
2385
+ } catch (err) {
2386
+ console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
2387
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
2388
+ }
2389
+ let txHash;
2390
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
2391
+ txHash = mppCredential.payload.hash;
2392
+ } else {
2393
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
2394
+ }
2395
+ console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
2396
+ const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
2397
+ const paymentPayload = {
2398
+ x402Version: X402_VERSION2,
2399
+ scheme: "exact",
2400
+ network: "eip155:42431",
2401
+ payload: { txHash, chainId: 42431 }
2402
+ };
2403
+ const verification = await this.registry.verify(paymentPayload, requirements);
2404
+ if (!verification.valid) {
2405
+ return this.sendJson(res, 402, {
2406
+ error: `Payment verification failed: ${verification.error}`
2407
+ });
2408
+ }
2409
+ console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
2410
+ const { execute, service, params } = body;
2411
+ if (execute && service) {
2412
+ console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
2413
+ const skill = this.skills.get(service);
2414
+ if (!skill) {
2415
+ return this.sendJson(res, 404, {
2416
+ success: false,
2417
+ paymentSettled: true,
2418
+ // Payment already happened on Tempo
2419
+ error: `Service not found: ${service}`
2420
+ });
2421
+ }
2422
+ const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
2423
+ let result;
2424
+ try {
2425
+ result = await Promise.race([
2426
+ skill.handler(params || {}),
2427
+ new Promise(
2428
+ (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
2429
+ )
2430
+ ]);
2431
+ } catch (err) {
2432
+ console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
2433
+ return this.sendJson(res, 500, {
2434
+ success: false,
2435
+ paymentSettled: true,
2436
+ error: `Service execution failed: ${err.message}`
2437
+ });
2438
+ }
2439
+ return this.sendJson(res, 200, {
2440
+ success: true,
2441
+ verified: true,
2442
+ txHash,
2443
+ chain: "tempo_moderato",
2444
+ paidTo: wallet,
2445
+ amount: amountNum,
2446
+ currency: "USDC",
2447
+ facilitator: verification.facilitator,
2448
+ memo,
2449
+ result
2450
+ });
2451
+ }
2452
+ this.sendJson(res, 200, {
2453
+ success: true,
2454
+ verified: true,
2455
+ txHash,
2456
+ chain: "tempo_moderato",
2457
+ paidTo: wallet,
2458
+ amount: amountNum,
2459
+ currency: "USDC",
2460
+ facilitator: verification.facilitator,
2461
+ memo
2462
+ });
2463
+ }
1612
2464
  /**
1613
2465
  * Build payment requirements for proxy endpoint (uses provided wallet)
1614
2466
  */
@@ -1620,7 +2472,7 @@ var MoltsPayServer = class {
1620
2472
  const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
1621
2473
  const tokenAddress = tokenAddresses[selectedToken];
1622
2474
  const tokenDomain = getTokenDomain(networkId, selectedToken);
1623
- return {
2475
+ const requirements = {
1624
2476
  scheme: "exact",
1625
2477
  network: networkId,
1626
2478
  asset: tokenAddress,
@@ -1630,6 +2482,17 @@ var MoltsPayServer = class {
1630
2482
  maxTimeoutSeconds: 300,
1631
2483
  extra: tokenDomain
1632
2484
  };
2485
+ if (networkId === "eip155:56" || networkId === "eip155:97") {
2486
+ const bnbFacilitator = this.registry.get("bnb");
2487
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
2488
+ if (spenderAddress) {
2489
+ requirements.extra = {
2490
+ ...requirements.extra || {},
2491
+ bnbSpender: spenderAddress
2492
+ };
2493
+ }
2494
+ }
2495
+ return requirements;
1633
2496
  }
1634
2497
  /**
1635
2498
  * Return 402 with x402 payment requirements for proxy endpoint