moltspay 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.env.example +14 -0
  2. package/README.md +319 -89
  3. package/dist/cdp/index.d.mts +4 -4
  4. package/dist/cdp/index.d.ts +4 -4
  5. package/dist/cdp/index.js +57 -0
  6. package/dist/cdp/index.js.map +1 -1
  7. package/dist/cdp/index.mjs +57 -0
  8. package/dist/cdp/index.mjs.map +1 -1
  9. package/dist/chains/index.d.mts +9 -8
  10. package/dist/chains/index.d.ts +9 -8
  11. package/dist/chains/index.js +57 -0
  12. package/dist/chains/index.js.map +1 -1
  13. package/dist/chains/index.mjs +57 -0
  14. package/dist/chains/index.mjs.map +1 -1
  15. package/dist/cli/index.js +2021 -285
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/index.mjs +2023 -277
  18. package/dist/cli/index.mjs.map +1 -1
  19. package/dist/client/index.d.mts +39 -3
  20. package/dist/client/index.d.ts +39 -3
  21. package/dist/client/index.js +563 -37
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/client/index.mjs +571 -35
  24. package/dist/client/index.mjs.map +1 -1
  25. package/dist/facilitators/index.d.mts +220 -1
  26. package/dist/facilitators/index.d.ts +220 -1
  27. package/dist/facilitators/index.js +664 -1
  28. package/dist/facilitators/index.js.map +1 -1
  29. package/dist/facilitators/index.mjs +670 -1
  30. package/dist/facilitators/index.mjs.map +1 -1
  31. package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
  32. package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
  33. package/dist/index.d.mts +2 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.js +1440 -153
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +1448 -151
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/server/index.d.mts +13 -3
  40. package/dist/server/index.d.ts +13 -3
  41. package/dist/server/index.js +909 -54
  42. package/dist/server/index.js.map +1 -1
  43. package/dist/server/index.mjs +919 -54
  44. package/dist/server/index.mjs.map +1 -1
  45. package/dist/verify/index.d.mts +1 -1
  46. package/dist/verify/index.d.ts +1 -1
  47. package/dist/verify/index.js +57 -0
  48. package/dist/verify/index.js.map +1 -1
  49. package/dist/verify/index.mjs +57 -0
  50. package/dist/verify/index.mjs.map +1 -1
  51. package/dist/wallet/index.d.mts +3 -3
  52. package/dist/wallet/index.d.ts +3 -3
  53. package/dist/wallet/index.js +57 -0
  54. package/dist/wallet/index.js.map +1 -1
  55. package/dist/wallet/index.mjs +57 -0
  56. package/dist/wallet/index.mjs.map +1 -1
  57. package/package.json +5 -2
  58. package/schemas/moltspay.services.schema.json +27 -132
@@ -331,6 +331,63 @@ var CHAINS = {
331
331
  explorerTx: "https://explore.testnet.tempo.xyz/tx/",
332
332
  avgBlockTime: 0.5
333
333
  // ~500ms finality
334
+ },
335
+ // ============ BNB Chain Testnet ============
336
+ bnb_testnet: {
337
+ name: "BNB Testnet",
338
+ chainId: 97,
339
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
340
+ tokens: {
341
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
342
+ // Using official Binance-Peg testnet tokens
343
+ USDC: {
344
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
345
+ // Testnet USDC
346
+ decimals: 18,
347
+ symbol: "USDC",
348
+ eip712Name: "USD Coin"
349
+ },
350
+ USDT: {
351
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
352
+ // Testnet USDT
353
+ decimals: 18,
354
+ symbol: "USDT",
355
+ eip712Name: "Tether USD"
356
+ }
357
+ },
358
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
359
+ explorer: "https://testnet.bscscan.com/address/",
360
+ explorerTx: "https://testnet.bscscan.com/tx/",
361
+ avgBlockTime: 3,
362
+ // BNB-specific: requires approval for pay-for-success flow
363
+ requiresApproval: true
364
+ },
365
+ // ============ BNB Chain Mainnet ============
366
+ bnb: {
367
+ name: "BNB Smart Chain",
368
+ chainId: 56,
369
+ rpc: "https://bsc-dataseed.binance.org",
370
+ tokens: {
371
+ // Note: BNB uses 18 decimals for stablecoins
372
+ USDC: {
373
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
374
+ decimals: 18,
375
+ symbol: "USDC",
376
+ eip712Name: "USD Coin"
377
+ },
378
+ USDT: {
379
+ address: "0x55d398326f99059fF775485246999027B3197955",
380
+ decimals: 18,
381
+ symbol: "USDT",
382
+ eip712Name: "Tether USD"
383
+ }
384
+ },
385
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
386
+ explorer: "https://bscscan.com/address/",
387
+ explorerTx: "https://bscscan.com/tx/",
388
+ avgBlockTime: 3,
389
+ // BNB-specific: requires approval for pay-for-success flow
390
+ requiresApproval: true
334
391
  }
335
392
  };
336
393
 
@@ -454,7 +511,602 @@ var TempoFacilitator = class extends BaseFacilitator {
454
511
  }
455
512
  };
456
513
 
514
+ // src/facilitators/bnb.ts
515
+ import { privateKeyToAccount } from "viem/accounts";
516
+ var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
517
+ var EIP712_DOMAIN = {
518
+ name: "MoltsPay",
519
+ version: "1"
520
+ };
521
+ var INTENT_TYPES = {
522
+ PaymentIntent: [
523
+ { name: "from", type: "address" },
524
+ { name: "to", type: "address" },
525
+ { name: "amount", type: "uint256" },
526
+ { name: "token", type: "address" },
527
+ { name: "service", type: "string" },
528
+ { name: "nonce", type: "uint256" },
529
+ { name: "deadline", type: "uint256" }
530
+ ]
531
+ };
532
+ var BNBFacilitator = class extends BaseFacilitator {
533
+ name = "bnb";
534
+ displayName = "BNB Smart Chain";
535
+ supportedNetworks = ["eip155:56", "eip155:97"];
536
+ // Mainnet + Testnet
537
+ serverPrivateKey;
538
+ spenderAddress = null;
539
+ chainConfigs;
540
+ constructor(serverPrivateKey) {
541
+ super();
542
+ this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
543
+ if (this.serverPrivateKey) {
544
+ const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
545
+ const account = privateKeyToAccount(key);
546
+ this.spenderAddress = account.address;
547
+ }
548
+ this.chainConfigs = {
549
+ 56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
550
+ 97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
551
+ };
552
+ }
553
+ async healthCheck() {
554
+ const start = Date.now();
555
+ try {
556
+ const response = await fetch(this.chainConfigs[56].rpc, {
557
+ method: "POST",
558
+ headers: { "Content-Type": "application/json" },
559
+ body: JSON.stringify({
560
+ jsonrpc: "2.0",
561
+ method: "eth_chainId",
562
+ params: [],
563
+ id: 1
564
+ })
565
+ });
566
+ const data = await response.json();
567
+ const chainId = parseInt(data.result, 16);
568
+ if (chainId !== 56) {
569
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
570
+ }
571
+ return { healthy: true, latencyMs: Date.now() - start };
572
+ } catch (error) {
573
+ return { healthy: false, error: String(error) };
574
+ }
575
+ }
576
+ /**
577
+ * Verify a payment intent signature (before service execution)
578
+ *
579
+ * This verifies:
580
+ * 1. Signature is valid for the intent
581
+ * 2. Client has approved server wallet
582
+ * 3. Client has sufficient balance
583
+ * 4. Intent hasn't expired
584
+ */
585
+ async verify(paymentPayload, requirements) {
586
+ try {
587
+ const bnbPayload = paymentPayload.payload;
588
+ if (!bnbPayload?.intent) {
589
+ return { valid: false, error: "Missing intent in payment payload" };
590
+ }
591
+ const { intent, chainId } = bnbPayload;
592
+ const config = this.chainConfigs[chainId];
593
+ if (!config) {
594
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
595
+ }
596
+ if (intent.deadline < Date.now()) {
597
+ return { valid: false, error: "Intent expired" };
598
+ }
599
+ const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
600
+ if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
601
+ return { valid: false, error: "Invalid signature" };
602
+ }
603
+ if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
604
+ return { valid: false, error: `Wrong recipient: ${intent.to}` };
605
+ }
606
+ if (BigInt(intent.amount) < BigInt(requirements.amount)) {
607
+ return { valid: false, error: `Insufficient amount: ${intent.amount}` };
608
+ }
609
+ if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
610
+ return { valid: false, error: `Wrong token: ${intent.token}` };
611
+ }
612
+ const serverAddress = await this.getServerAddress();
613
+ const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
614
+ if (BigInt(allowance) < BigInt(intent.amount)) {
615
+ return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
616
+ }
617
+ const balance = await this.getBalance(intent.from, intent.token, config.rpc);
618
+ if (BigInt(balance) < BigInt(intent.amount)) {
619
+ return { valid: false, error: "Insufficient balance" };
620
+ }
621
+ return {
622
+ valid: true,
623
+ details: {
624
+ from: intent.from,
625
+ to: intent.to,
626
+ amount: intent.amount,
627
+ token: intent.token,
628
+ service: intent.service,
629
+ nonce: intent.nonce,
630
+ deadline: intent.deadline
631
+ }
632
+ };
633
+ } catch (error) {
634
+ return { valid: false, error: `Verification failed: ${error}` };
635
+ }
636
+ }
637
+ /**
638
+ * Settle a payment by executing transferFrom
639
+ *
640
+ * This is called AFTER the service has been successfully delivered.
641
+ * Server pays gas, transfers tokens from client to provider.
642
+ */
643
+ async settle(paymentPayload, requirements) {
644
+ if (!this.serverPrivateKey) {
645
+ return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
646
+ }
647
+ try {
648
+ const verifyResult = await this.verify(paymentPayload, requirements);
649
+ if (!verifyResult.valid) {
650
+ return { success: false, error: verifyResult.error };
651
+ }
652
+ const bnbPayload = paymentPayload.payload;
653
+ const { intent, chainId } = bnbPayload;
654
+ const config = this.chainConfigs[chainId];
655
+ const txHash = await this.executeTransferFrom(
656
+ intent.from,
657
+ intent.to,
658
+ intent.amount,
659
+ intent.token,
660
+ config.rpc
661
+ );
662
+ return {
663
+ success: true,
664
+ transaction: txHash,
665
+ status: "settled"
666
+ };
667
+ } catch (error) {
668
+ return { success: false, error: `Settlement failed: ${error}` };
669
+ }
670
+ }
671
+ /**
672
+ * Check if client has approved the server wallet
673
+ */
674
+ async checkApproval(clientAddress, token, chainId) {
675
+ const config = this.chainConfigs[chainId];
676
+ if (!config) {
677
+ throw new Error(`Unsupported chainId: ${chainId}`);
678
+ }
679
+ const serverAddress = await this.getServerAddress();
680
+ const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
681
+ const minAllowance = BigInt("1000000000000000000000");
682
+ return {
683
+ approved: BigInt(allowance) >= minAllowance,
684
+ allowance
685
+ };
686
+ }
687
+ /**
688
+ * Verify a completed transaction (for checking past payments)
689
+ */
690
+ async verifyTransaction(txHash, expected, chainId) {
691
+ const config = this.chainConfigs[chainId];
692
+ if (!config) {
693
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
694
+ }
695
+ try {
696
+ const receipt = await this.getTransactionReceipt(txHash, config.rpc);
697
+ if (!receipt) {
698
+ return { valid: false, error: "Transaction not found" };
699
+ }
700
+ if (receipt.status !== "0x1") {
701
+ return { valid: false, error: "Transaction failed" };
702
+ }
703
+ const transferLog = receipt.logs.find(
704
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
705
+ );
706
+ if (!transferLog) {
707
+ return { valid: false, error: "No Transfer event found" };
708
+ }
709
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
710
+ if (toAddress !== expected.to.toLowerCase()) {
711
+ return { valid: false, error: `Wrong recipient: ${toAddress}` };
712
+ }
713
+ const amount = BigInt(transferLog.data);
714
+ if (amount < BigInt(expected.amount)) {
715
+ return { valid: false, error: `Insufficient amount: ${amount}` };
716
+ }
717
+ return {
718
+ valid: true,
719
+ details: {
720
+ txHash,
721
+ from: "0x" + transferLog.topics[1].slice(26),
722
+ to: toAddress,
723
+ amount: amount.toString(),
724
+ token: transferLog.address
725
+ }
726
+ };
727
+ } catch (error) {
728
+ return { valid: false, error: `Verification failed: ${error}` };
729
+ }
730
+ }
731
+ // ==================== Private Methods ====================
732
+ /**
733
+ * Get the server's spender address (public, for 402 responses)
734
+ * Returns cached value computed at construction time.
735
+ */
736
+ getSpenderAddress() {
737
+ return this.spenderAddress;
738
+ }
739
+ async getServerAddress() {
740
+ const { ethers } = await import("ethers");
741
+ const wallet = new ethers.Wallet(this.serverPrivateKey);
742
+ return wallet.address;
743
+ }
744
+ async recoverIntentSigner(intent, chainId) {
745
+ const { ethers } = await import("ethers");
746
+ const domain = {
747
+ ...EIP712_DOMAIN,
748
+ chainId
749
+ };
750
+ const message = {
751
+ from: intent.from,
752
+ to: intent.to,
753
+ amount: intent.amount,
754
+ token: intent.token,
755
+ service: intent.service,
756
+ nonce: intent.nonce,
757
+ deadline: intent.deadline
758
+ };
759
+ const recoveredAddress = ethers.verifyTypedData(
760
+ domain,
761
+ INTENT_TYPES,
762
+ message,
763
+ intent.signature
764
+ );
765
+ return recoveredAddress;
766
+ }
767
+ async getAllowance(owner, spender, token, rpcUrl) {
768
+ const selector = "0xdd62ed3e";
769
+ const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
770
+ const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
771
+ const data = selector + ownerPadded + spenderPadded;
772
+ const response = await fetch(rpcUrl, {
773
+ method: "POST",
774
+ headers: { "Content-Type": "application/json" },
775
+ body: JSON.stringify({
776
+ jsonrpc: "2.0",
777
+ method: "eth_call",
778
+ params: [{ to: token, data }, "latest"],
779
+ id: 1
780
+ })
781
+ });
782
+ const result = await response.json();
783
+ return result.result || "0x0";
784
+ }
785
+ async getBalance(account, token, rpcUrl) {
786
+ const selector = "0x70a08231";
787
+ const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
788
+ const data = selector + accountPadded;
789
+ const response = await fetch(rpcUrl, {
790
+ method: "POST",
791
+ headers: { "Content-Type": "application/json" },
792
+ body: JSON.stringify({
793
+ jsonrpc: "2.0",
794
+ method: "eth_call",
795
+ params: [{ to: token, data }, "latest"],
796
+ id: 1
797
+ })
798
+ });
799
+ const result = await response.json();
800
+ return result.result || "0x0";
801
+ }
802
+ async executeTransferFrom(from, to, amount, token, rpcUrl) {
803
+ const { ethers } = await import("ethers");
804
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
805
+ const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
806
+ const tokenContract = new ethers.Contract(token, [
807
+ "function transferFrom(address from, address to, uint256 amount) returns (bool)"
808
+ ], wallet);
809
+ const tx = await tokenContract.transferFrom(from, to, amount);
810
+ const receipt = await tx.wait();
811
+ return receipt.hash;
812
+ }
813
+ async getTransactionReceipt(txHash, rpcUrl) {
814
+ const response = await fetch(rpcUrl, {
815
+ method: "POST",
816
+ headers: { "Content-Type": "application/json" },
817
+ body: JSON.stringify({
818
+ jsonrpc: "2.0",
819
+ method: "eth_getTransactionReceipt",
820
+ params: [txHash],
821
+ id: 1
822
+ })
823
+ });
824
+ const data = await response.json();
825
+ return data.result;
826
+ }
827
+ };
828
+ function createIntentTypedData(intent, chainId) {
829
+ return {
830
+ domain: {
831
+ ...EIP712_DOMAIN,
832
+ chainId
833
+ },
834
+ types: INTENT_TYPES,
835
+ primaryType: "PaymentIntent",
836
+ message: {
837
+ from: intent.from,
838
+ to: intent.to,
839
+ amount: intent.amount,
840
+ token: intent.token,
841
+ service: intent.service,
842
+ nonce: intent.nonce,
843
+ deadline: intent.deadline
844
+ }
845
+ };
846
+ }
847
+
848
+ // src/facilitators/solana.ts
849
+ import {
850
+ Connection as Connection2,
851
+ PublicKey as PublicKey2,
852
+ Transaction,
853
+ VersionedTransaction
854
+ } from "@solana/web3.js";
855
+ import {
856
+ getAssociatedTokenAddress,
857
+ createTransferCheckedInstruction,
858
+ getAccount,
859
+ createAssociatedTokenAccountInstruction
860
+ } from "@solana/spl-token";
861
+
862
+ // src/chains/solana.ts
863
+ import { Connection, PublicKey } from "@solana/web3.js";
864
+ var SOLANA_CHAINS = {
865
+ solana: {
866
+ name: "Solana Mainnet",
867
+ cluster: "mainnet-beta",
868
+ rpc: "https://api.mainnet-beta.solana.com",
869
+ explorer: "https://solscan.io/account/",
870
+ explorerTx: "https://solscan.io/tx/",
871
+ tokens: {
872
+ USDC: {
873
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
874
+ // Circle official USDC
875
+ decimals: 6
876
+ }
877
+ }
878
+ },
879
+ solana_devnet: {
880
+ name: "Solana Devnet",
881
+ cluster: "devnet",
882
+ rpc: "https://api.devnet.solana.com",
883
+ explorer: "https://solscan.io/account/",
884
+ explorerTx: "https://solscan.io/tx/",
885
+ tokens: {
886
+ USDC: {
887
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
888
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
889
+ decimals: 6
890
+ }
891
+ }
892
+ }
893
+ };
894
+
895
+ // src/facilitators/solana.ts
896
+ var SolanaFacilitator = class extends BaseFacilitator {
897
+ name = "solana";
898
+ displayName = "Solana Direct";
899
+ supportedNetworks = ["solana:mainnet", "solana:devnet"];
900
+ connections = /* @__PURE__ */ new Map();
901
+ feePayerKeypair;
902
+ constructor(config) {
903
+ super();
904
+ this.feePayerKeypair = config?.feePayerKeypair;
905
+ for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
906
+ this.connections.set(
907
+ chain,
908
+ new Connection2(config2.rpc, "confirmed")
909
+ );
910
+ }
911
+ if (this.feePayerKeypair) {
912
+ console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
913
+ }
914
+ }
915
+ /**
916
+ * Get fee payer public key (for gasless transactions)
917
+ */
918
+ getFeePayerPubkey() {
919
+ return this.feePayerKeypair?.publicKey.toBase58() || null;
920
+ }
921
+ getConnection(chain) {
922
+ const conn = this.connections.get(chain);
923
+ if (!conn) {
924
+ throw new Error(`No connection for chain: ${chain}`);
925
+ }
926
+ return conn;
927
+ }
928
+ /**
929
+ * Convert our chain name to network identifier
930
+ */
931
+ static chainToNetwork(chain) {
932
+ return chain === "solana" ? "solana:mainnet" : "solana:devnet";
933
+ }
934
+ /**
935
+ * Convert network identifier to chain name
936
+ */
937
+ static networkToChain(network) {
938
+ if (network === "solana:mainnet") return "solana";
939
+ if (network === "solana:devnet") return "solana_devnet";
940
+ return null;
941
+ }
942
+ async healthCheck() {
943
+ const start = Date.now();
944
+ try {
945
+ const conn = this.getConnection("solana_devnet");
946
+ await conn.getSlot();
947
+ return {
948
+ healthy: true,
949
+ latencyMs: Date.now() - start
950
+ };
951
+ } catch (error) {
952
+ return {
953
+ healthy: false,
954
+ error: error.message
955
+ };
956
+ }
957
+ }
958
+ /**
959
+ * Verify a Solana payment
960
+ *
961
+ * Checks:
962
+ * 1. Transaction is valid and properly signed
963
+ * 2. Transfer instruction matches expected amount and recipient
964
+ */
965
+ async verify(paymentPayload, requirements) {
966
+ try {
967
+ const solanaPayload = paymentPayload.payload;
968
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
969
+ return { valid: false, error: "Missing signed transaction" };
970
+ }
971
+ const chain = solanaPayload.chain || "solana_devnet";
972
+ const chainConfig = SOLANA_CHAINS[chain];
973
+ if (!chainConfig) {
974
+ return { valid: false, error: `Invalid chain: ${chain}` };
975
+ }
976
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
977
+ let tx;
978
+ try {
979
+ tx = Transaction.from(txBuffer);
980
+ } catch {
981
+ tx = VersionedTransaction.deserialize(txBuffer);
982
+ }
983
+ if (tx instanceof Transaction) {
984
+ const hasAnySignature = tx.signatures.some(
985
+ (sig) => sig.signature && !sig.signature.every((b) => b === 0)
986
+ );
987
+ if (!hasAnySignature) {
988
+ return { valid: false, error: "Transaction not signed" };
989
+ }
990
+ }
991
+ const expectedAmount = BigInt(requirements.amount);
992
+ const expectedRecipient = new PublicKey2(requirements.payTo);
993
+ return {
994
+ valid: true,
995
+ details: {
996
+ chain,
997
+ sender: solanaPayload.sender,
998
+ recipient: requirements.payTo,
999
+ amount: requirements.amount
1000
+ }
1001
+ };
1002
+ } catch (error) {
1003
+ return { valid: false, error: error.message };
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Settle a Solana payment
1008
+ *
1009
+ * Submits the signed transaction to the network.
1010
+ * In gasless mode, adds fee payer signature before submitting.
1011
+ */
1012
+ async settle(paymentPayload, requirements) {
1013
+ try {
1014
+ const solanaPayload = paymentPayload.payload;
1015
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
1016
+ return { success: false, error: "Missing signed transaction" };
1017
+ }
1018
+ const chain = solanaPayload.chain || "solana_devnet";
1019
+ const connection = this.getConnection(chain);
1020
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
1021
+ let txToSend;
1022
+ try {
1023
+ const tx = Transaction.from(txBuffer);
1024
+ if (this.feePayerKeypair && tx.feePayer) {
1025
+ const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
1026
+ const txFeePayer = tx.feePayer.toBase58();
1027
+ if (txFeePayer === feePayerPubkey) {
1028
+ console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
1029
+ tx.partialSign(this.feePayerKeypair);
1030
+ }
1031
+ }
1032
+ txToSend = tx.serialize();
1033
+ } catch (e) {
1034
+ txToSend = txBuffer;
1035
+ }
1036
+ const signature = await connection.sendRawTransaction(txToSend, {
1037
+ skipPreflight: false,
1038
+ preflightCommitment: "confirmed"
1039
+ });
1040
+ const confirmation = await connection.confirmTransaction(signature, "confirmed");
1041
+ if (confirmation.value.err) {
1042
+ return {
1043
+ success: false,
1044
+ error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
1045
+ transaction: signature
1046
+ };
1047
+ }
1048
+ return {
1049
+ success: true,
1050
+ transaction: signature,
1051
+ status: "confirmed"
1052
+ };
1053
+ } catch (error) {
1054
+ return { success: false, error: error.message };
1055
+ }
1056
+ }
1057
+ supportsNetwork(network) {
1058
+ return this.supportedNetworks.includes(network);
1059
+ }
1060
+ };
1061
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
1062
+ const chainConfig = SOLANA_CHAINS[chain];
1063
+ const connection = new Connection2(chainConfig.rpc, "confirmed");
1064
+ const mint = new PublicKey2(chainConfig.tokens.USDC.mint);
1065
+ const actualFeePayer = feePayerPubkey || senderPubkey;
1066
+ const senderATA = await getAssociatedTokenAddress(mint, senderPubkey);
1067
+ const recipientATA = await getAssociatedTokenAddress(mint, recipientPubkey);
1068
+ const transaction = new Transaction();
1069
+ try {
1070
+ await getAccount(connection, recipientATA);
1071
+ } catch {
1072
+ transaction.add(
1073
+ createAssociatedTokenAccountInstruction(
1074
+ actualFeePayer,
1075
+ // payer (fee payer in gasless mode)
1076
+ recipientATA,
1077
+ // ata to create
1078
+ recipientPubkey,
1079
+ // owner
1080
+ mint
1081
+ // mint
1082
+ )
1083
+ );
1084
+ }
1085
+ transaction.add(
1086
+ createTransferCheckedInstruction(
1087
+ senderATA,
1088
+ // source
1089
+ mint,
1090
+ // mint
1091
+ recipientATA,
1092
+ // destination
1093
+ senderPubkey,
1094
+ // owner (sender still authorizes the transfer)
1095
+ amount,
1096
+ // amount
1097
+ chainConfig.tokens.USDC.decimals
1098
+ // decimals
1099
+ )
1100
+ );
1101
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
1102
+ transaction.recentBlockhash = blockhash;
1103
+ transaction.feePayer = actualFeePayer;
1104
+ return transaction;
1105
+ }
1106
+
457
1107
  // src/facilitators/registry.ts
1108
+ import { Keypair as Keypair2 } from "@solana/web3.js";
1109
+ import bs58 from "bs58";
458
1110
  var FacilitatorRegistry = class {
459
1111
  factories = /* @__PURE__ */ new Map();
460
1112
  instances = /* @__PURE__ */ new Map();
@@ -463,7 +1115,20 @@ var FacilitatorRegistry = class {
463
1115
  constructor(selection) {
464
1116
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
465
1117
  this.registerFactory("tempo", () => new TempoFacilitator());
466
- this.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
1118
+ this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
1119
+ this.registerFactory("solana", (config) => {
1120
+ let feePayerKeypair;
1121
+ const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
1122
+ if (feePayerKey) {
1123
+ try {
1124
+ feePayerKeypair = Keypair2.fromSecretKey(bs58.decode(feePayerKey));
1125
+ } catch (e) {
1126
+ console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
1127
+ }
1128
+ }
1129
+ return new SolanaFacilitator({ feePayerKeypair });
1130
+ });
1131
+ this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
467
1132
  }
468
1133
  /**
469
1134
  * Register a new facilitator factory
@@ -689,11 +1354,15 @@ function createRegistry(selection) {
689
1354
  return new FacilitatorRegistry(selection);
690
1355
  }
691
1356
  export {
1357
+ BNBFacilitator,
692
1358
  BaseFacilitator,
693
1359
  CDPFacilitator,
694
1360
  FacilitatorRegistry,
1361
+ SolanaFacilitator,
695
1362
  TempoFacilitator,
1363
+ createIntentTypedData,
696
1364
  createRegistry,
1365
+ createSolanaPaymentTransaction,
697
1366
  getDefaultRegistry
698
1367
  };
699
1368
  //# sourceMappingURL=index.mjs.map