t402 1.6.0__py3-none-any.whl → 1.6.1__py3-none-any.whl

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.
t402/__init__.py CHANGED
@@ -64,11 +64,15 @@ from t402.tron import (
64
64
  is_testnet as is_tron_testnet,
65
65
  )
66
66
  from t402.svm import (
67
+ # Constants
67
68
  SOLANA_MAINNET,
68
69
  SOLANA_DEVNET,
69
70
  SOLANA_TESTNET,
70
71
  USDC_MAINNET_ADDRESS as SVM_USDC_MAINNET_ADDRESS,
71
72
  USDC_DEVNET_ADDRESS as SVM_USDC_DEVNET_ADDRESS,
73
+ TOKEN_PROGRAM_ADDRESS as SVM_TOKEN_PROGRAM_ADDRESS,
74
+ TOKEN_2022_PROGRAM_ADDRESS as SVM_TOKEN_2022_PROGRAM_ADDRESS,
75
+ # Address/Network utilities
72
76
  validate_svm_address,
73
77
  get_usdc_address as get_svm_usdc_address,
74
78
  get_network_config as get_svm_network_config,
@@ -80,6 +84,34 @@ from t402.svm import (
80
84
  validate_transaction as validate_svm_transaction,
81
85
  normalize_network as normalize_svm_network,
82
86
  get_rpc_url as get_svm_rpc_url,
87
+ # Transaction utilities
88
+ decode_transaction as decode_svm_transaction,
89
+ decode_versioned_transaction,
90
+ encode_transaction as encode_svm_transaction,
91
+ get_transaction_fee_payer as get_svm_fee_payer,
92
+ get_token_payer_from_transaction as get_svm_token_payer,
93
+ parse_transfer_checked_instruction,
94
+ TransferDetails as SvmTransferDetails,
95
+ # Signer interfaces and implementations
96
+ ClientSvmSigner,
97
+ FacilitatorSvmSigner,
98
+ KeypairSvmSigner,
99
+ RpcSvmSigner,
100
+ # Scheme implementations
101
+ ExactSvmClientScheme,
102
+ ExactSvmServerScheme,
103
+ ExactSvmFacilitatorScheme,
104
+ # Factory functions
105
+ create_client_scheme as create_svm_client_scheme,
106
+ create_server_scheme as create_svm_server_scheme,
107
+ create_facilitator_scheme as create_svm_facilitator_scheme,
108
+ check_solana_available,
109
+ # Types
110
+ SvmAuthorization,
111
+ SvmPaymentPayload,
112
+ SvmVerifyMessageResult,
113
+ SvmTransactionConfirmation,
114
+ ExactSvmPayloadV2,
83
115
  )
84
116
  from t402.paywall import (
85
117
  get_paywall_html,
@@ -235,12 +267,15 @@ __all__ = [
235
267
  "parse_tron_amount",
236
268
  "format_tron_amount",
237
269
  "is_tron_testnet",
238
- # SVM (Solana) utilities
270
+ # SVM (Solana) utilities - Constants
239
271
  "SOLANA_MAINNET",
240
272
  "SOLANA_DEVNET",
241
273
  "SOLANA_TESTNET",
242
274
  "SVM_USDC_MAINNET_ADDRESS",
243
275
  "SVM_USDC_DEVNET_ADDRESS",
276
+ "SVM_TOKEN_PROGRAM_ADDRESS",
277
+ "SVM_TOKEN_2022_PROGRAM_ADDRESS",
278
+ # SVM - Address/Network utilities
244
279
  "validate_svm_address",
245
280
  "get_svm_usdc_address",
246
281
  "get_svm_network_config",
@@ -252,6 +287,34 @@ __all__ = [
252
287
  "validate_svm_transaction",
253
288
  "normalize_svm_network",
254
289
  "get_svm_rpc_url",
290
+ # SVM - Transaction utilities
291
+ "decode_svm_transaction",
292
+ "decode_versioned_transaction",
293
+ "encode_svm_transaction",
294
+ "get_svm_fee_payer",
295
+ "get_svm_token_payer",
296
+ "parse_transfer_checked_instruction",
297
+ "SvmTransferDetails",
298
+ # SVM - Signer interfaces
299
+ "ClientSvmSigner",
300
+ "FacilitatorSvmSigner",
301
+ "KeypairSvmSigner",
302
+ "RpcSvmSigner",
303
+ # SVM - Scheme implementations
304
+ "ExactSvmClientScheme",
305
+ "ExactSvmServerScheme",
306
+ "ExactSvmFacilitatorScheme",
307
+ # SVM - Factory functions
308
+ "create_svm_client_scheme",
309
+ "create_svm_server_scheme",
310
+ "create_svm_facilitator_scheme",
311
+ "check_solana_available",
312
+ # SVM - Types
313
+ "SvmAuthorization",
314
+ "SvmPaymentPayload",
315
+ "SvmVerifyMessageResult",
316
+ "SvmTransactionConfirmation",
317
+ "ExactSvmPayloadV2",
255
318
  # Paywall
256
319
  "get_paywall_html",
257
320
  "get_paywall_template",
t402/svm.py CHANGED
@@ -3,6 +3,13 @@ Solana SVM blockchain support for t402 protocol.
3
3
 
4
4
  This module provides types and utilities for Solana payments
5
5
  using SPL token transfers.
6
+
7
+ Features:
8
+ - Client scheme for creating payment payloads
9
+ - Server scheme for parsing prices and requirements
10
+ - Facilitator scheme for verifying and settling payments
11
+ - Signer interfaces for key management
12
+ - Transaction utilities for validation and parsing
6
13
  """
7
14
 
8
15
  from __future__ import annotations
@@ -10,12 +17,35 @@ from __future__ import annotations
10
17
  import re
11
18
  import time
12
19
  import base64
13
- from typing import Any, Dict, Optional, List
20
+ from typing import Any, Dict, Optional, List, Callable, Awaitable, Protocol, runtime_checkable
14
21
  from typing_extensions import TypedDict
15
22
 
16
23
  from pydantic import BaseModel, ConfigDict, Field, field_validator
17
24
  from pydantic.alias_generators import to_camel
18
25
 
26
+ # Optional solana imports - only required for actual blockchain operations
27
+ try:
28
+ from solders.keypair import Keypair
29
+ from solders.pubkey import Pubkey
30
+ from solders.transaction import VersionedTransaction
31
+ from solders.message import MessageV0
32
+ from solders.signature import Signature
33
+ from solders.instruction import CompiledInstruction
34
+ from solana.rpc.async_api import AsyncClient
35
+ from solana.rpc.commitment import Commitment, Confirmed
36
+ SOLANA_AVAILABLE = True
37
+ except ImportError:
38
+ SOLANA_AVAILABLE = False
39
+ Keypair = None
40
+ Pubkey = None
41
+ VersionedTransaction = None
42
+ MessageV0 = None
43
+ Signature = None
44
+ CompiledInstruction = None
45
+ AsyncClient = None
46
+ Commitment = None
47
+ Confirmed = None
48
+
19
49
 
20
50
  # Constants
21
51
  SCHEME_EXACT = "exact"
@@ -564,3 +594,969 @@ def get_known_tokens(network: str) -> List[TokenConfig]:
564
594
  if not config:
565
595
  return []
566
596
  return list(config["supported_assets"].values())
597
+
598
+
599
+ # =============================================================================
600
+ # Transaction Utilities
601
+ # =============================================================================
602
+
603
+
604
+ def decode_transaction(tx_base64: str) -> bytes:
605
+ """
606
+ Decode a base64 encoded transaction.
607
+
608
+ Args:
609
+ tx_base64: Base64 encoded transaction string
610
+
611
+ Returns:
612
+ Transaction bytes
613
+
614
+ Raises:
615
+ ValueError: If transaction cannot be decoded
616
+ """
617
+ try:
618
+ return base64.b64decode(tx_base64)
619
+ except Exception as e:
620
+ raise ValueError(f"Failed to decode transaction: {e}")
621
+
622
+
623
+ def decode_versioned_transaction(tx_base64: str) -> "VersionedTransaction":
624
+ """
625
+ Decode a base64 encoded Solana versioned transaction.
626
+
627
+ Requires solana/solders packages to be installed.
628
+
629
+ Args:
630
+ tx_base64: Base64 encoded transaction string
631
+
632
+ Returns:
633
+ VersionedTransaction object
634
+
635
+ Raises:
636
+ ImportError: If solana packages not installed
637
+ ValueError: If transaction cannot be decoded
638
+ """
639
+ if not SOLANA_AVAILABLE:
640
+ raise ImportError(
641
+ "solana and solders packages required for transaction decoding. "
642
+ "Install with: pip install t402[svm]"
643
+ )
644
+
645
+ try:
646
+ tx_bytes = decode_transaction(tx_base64)
647
+ return VersionedTransaction.from_bytes(tx_bytes)
648
+ except Exception as e:
649
+ raise ValueError(f"Failed to decode versioned transaction: {e}")
650
+
651
+
652
+ def encode_transaction(tx: "VersionedTransaction") -> str:
653
+ """
654
+ Encode a versioned transaction to base64.
655
+
656
+ Args:
657
+ tx: VersionedTransaction object
658
+
659
+ Returns:
660
+ Base64 encoded transaction string
661
+ """
662
+ return base64.b64encode(bytes(tx)).decode("utf-8")
663
+
664
+
665
+ def get_transaction_fee_payer(tx_base64: str) -> Optional[str]:
666
+ """
667
+ Extract the fee payer address from a transaction.
668
+
669
+ Args:
670
+ tx_base64: Base64 encoded transaction string
671
+
672
+ Returns:
673
+ Fee payer address or None if cannot be extracted
674
+ """
675
+ if not SOLANA_AVAILABLE:
676
+ return None
677
+
678
+ try:
679
+ tx = decode_versioned_transaction(tx_base64)
680
+ message = tx.message
681
+ if hasattr(message, "account_keys") and len(message.account_keys) > 0:
682
+ return str(message.account_keys[0])
683
+ return None
684
+ except Exception:
685
+ return None
686
+
687
+
688
+ def get_token_payer_from_transaction(tx_base64: str) -> Optional[str]:
689
+ """
690
+ Extract the token transfer authority (payer) from a transaction.
691
+
692
+ This looks for the authority account in TransferChecked instructions.
693
+
694
+ Args:
695
+ tx_base64: Base64 encoded transaction string
696
+
697
+ Returns:
698
+ Token payer address or None if cannot be extracted
699
+ """
700
+ if not SOLANA_AVAILABLE:
701
+ return None
702
+
703
+ try:
704
+ tx = decode_versioned_transaction(tx_base64)
705
+ message = tx.message
706
+ account_keys = list(message.account_keys) if hasattr(message, "account_keys") else []
707
+
708
+ # Look for token program instructions
709
+ for ix in message.instructions:
710
+ program_idx = ix.program_id_index
711
+ if program_idx >= len(account_keys):
712
+ continue
713
+
714
+ program_id = str(account_keys[program_idx])
715
+
716
+ # Check if it's a token program
717
+ if program_id in (TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS):
718
+ # TransferChecked has authority at index 2
719
+ accounts = list(ix.accounts)
720
+ if len(accounts) >= 3:
721
+ authority_idx = accounts[2]
722
+ if authority_idx < len(account_keys):
723
+ return str(account_keys[authority_idx])
724
+
725
+ return None
726
+ except Exception:
727
+ return None
728
+
729
+
730
+ class TransferDetails(TypedDict):
731
+ """Details of a token transfer instruction."""
732
+ source: str
733
+ mint: str
734
+ destination: str
735
+ authority: str
736
+ amount: int
737
+ decimals: int
738
+
739
+
740
+ def parse_transfer_checked_instruction(
741
+ tx_base64: str,
742
+ ) -> Optional[TransferDetails]:
743
+ """
744
+ Parse a TransferChecked instruction from a transaction.
745
+
746
+ Args:
747
+ tx_base64: Base64 encoded transaction string
748
+
749
+ Returns:
750
+ TransferDetails or None if no transfer found
751
+ """
752
+ if not SOLANA_AVAILABLE:
753
+ return None
754
+
755
+ try:
756
+ tx = decode_versioned_transaction(tx_base64)
757
+ message = tx.message
758
+ account_keys = list(message.account_keys) if hasattr(message, "account_keys") else []
759
+
760
+ for ix in message.instructions:
761
+ program_idx = ix.program_id_index
762
+ if program_idx >= len(account_keys):
763
+ continue
764
+
765
+ program_id = str(account_keys[program_idx])
766
+
767
+ # Check if it's a token program
768
+ if program_id not in (TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS):
769
+ continue
770
+
771
+ # Check instruction discriminator for TransferChecked (12)
772
+ if not ix.data or ix.data[0] != 12:
773
+ continue
774
+
775
+ accounts = list(ix.accounts)
776
+ if len(accounts) < 4:
777
+ continue
778
+
779
+ # Parse TransferChecked data: [discriminator(1), amount(8), decimals(1)]
780
+ if len(ix.data) < 10:
781
+ continue
782
+
783
+ amount = int.from_bytes(ix.data[1:9], "little")
784
+ decimals = ix.data[9]
785
+
786
+ source_idx = accounts[0]
787
+ mint_idx = accounts[1]
788
+ dest_idx = accounts[2]
789
+ authority_idx = accounts[3]
790
+
791
+ return {
792
+ "source": str(account_keys[source_idx]) if source_idx < len(account_keys) else "",
793
+ "mint": str(account_keys[mint_idx]) if mint_idx < len(account_keys) else "",
794
+ "destination": str(account_keys[dest_idx]) if dest_idx < len(account_keys) else "",
795
+ "authority": str(account_keys[authority_idx]) if authority_idx < len(account_keys) else "",
796
+ "amount": amount,
797
+ "decimals": decimals,
798
+ }
799
+
800
+ return None
801
+ except Exception:
802
+ return None
803
+
804
+
805
+ # =============================================================================
806
+ # Signer Interfaces
807
+ # =============================================================================
808
+
809
+
810
+ @runtime_checkable
811
+ class ClientSvmSigner(Protocol):
812
+ """
813
+ Interface for client-side SVM signing operations.
814
+
815
+ Implementations should provide methods to:
816
+ - Get the signer's public address
817
+ - Sign transactions
818
+ - Optionally get token balances
819
+ """
820
+
821
+ def get_address(self) -> str:
822
+ """Get the signer's Solana address."""
823
+ ...
824
+
825
+ async def sign_transaction(
826
+ self,
827
+ tx_base64: str,
828
+ network: str,
829
+ ) -> str:
830
+ """
831
+ Sign a transaction.
832
+
833
+ Args:
834
+ tx_base64: Base64 encoded unsigned transaction
835
+ network: Network identifier
836
+
837
+ Returns:
838
+ Base64 encoded signed transaction
839
+ """
840
+ ...
841
+
842
+
843
+ @runtime_checkable
844
+ class FacilitatorSvmSigner(Protocol):
845
+ """
846
+ Interface for facilitator-side SVM operations.
847
+
848
+ Extends client signer with RPC capabilities for:
849
+ - Signing transactions (as fee payer)
850
+ - Simulating transactions
851
+ - Sending and confirming transactions
852
+ """
853
+
854
+ def get_addresses(self) -> List[str]:
855
+ """Get all available fee payer addresses."""
856
+ ...
857
+
858
+ async def sign_transaction(
859
+ self,
860
+ tx_base64: str,
861
+ fee_payer: str,
862
+ network: str,
863
+ ) -> str:
864
+ """
865
+ Sign a transaction as fee payer.
866
+
867
+ Args:
868
+ tx_base64: Base64 encoded transaction
869
+ fee_payer: Fee payer address to use
870
+ network: Network identifier
871
+
872
+ Returns:
873
+ Base64 encoded fully signed transaction
874
+ """
875
+ ...
876
+
877
+ async def simulate_transaction(
878
+ self,
879
+ tx_base64: str,
880
+ network: str,
881
+ ) -> bool:
882
+ """
883
+ Simulate a transaction to verify it will succeed.
884
+
885
+ Args:
886
+ tx_base64: Base64 encoded signed transaction
887
+ network: Network identifier
888
+
889
+ Returns:
890
+ True if simulation succeeds
891
+
892
+ Raises:
893
+ Exception: If simulation fails
894
+ """
895
+ ...
896
+
897
+ async def send_transaction(
898
+ self,
899
+ tx_base64: str,
900
+ network: str,
901
+ ) -> str:
902
+ """
903
+ Send a signed transaction to the network.
904
+
905
+ Args:
906
+ tx_base64: Base64 encoded signed transaction
907
+ network: Network identifier
908
+
909
+ Returns:
910
+ Transaction signature
911
+ """
912
+ ...
913
+
914
+ async def confirm_transaction(
915
+ self,
916
+ signature: str,
917
+ network: str,
918
+ ) -> bool:
919
+ """
920
+ Wait for transaction confirmation.
921
+
922
+ Args:
923
+ signature: Transaction signature
924
+ network: Network identifier
925
+
926
+ Returns:
927
+ True if confirmed
928
+ """
929
+ ...
930
+
931
+
932
+ class KeypairSvmSigner:
933
+ """
934
+ Simple SVM signer using a Keypair.
935
+
936
+ Suitable for client-side signing operations.
937
+ """
938
+
939
+ def __init__(self, keypair: "Keypair"):
940
+ """
941
+ Initialize with a Keypair.
942
+
943
+ Args:
944
+ keypair: Solders Keypair object
945
+ """
946
+ if not SOLANA_AVAILABLE:
947
+ raise ImportError(
948
+ "solana and solders packages required. "
949
+ "Install with: pip install t402[svm]"
950
+ )
951
+ self._keypair = keypair
952
+
953
+ @classmethod
954
+ def from_secret_key(cls, secret_key: bytes) -> "KeypairSvmSigner":
955
+ """Create signer from a 64-byte secret key."""
956
+ if not SOLANA_AVAILABLE:
957
+ raise ImportError(
958
+ "solana and solders packages required. "
959
+ "Install with: pip install t402[svm]"
960
+ )
961
+ return cls(Keypair.from_bytes(secret_key))
962
+
963
+ @classmethod
964
+ def from_base58(cls, base58_key: str) -> "KeypairSvmSigner":
965
+ """Create signer from a base58 encoded secret key."""
966
+ if not SOLANA_AVAILABLE:
967
+ raise ImportError(
968
+ "solana and solders packages required. "
969
+ "Install with: pip install t402[svm]"
970
+ )
971
+ return cls(Keypair.from_base58_string(base58_key))
972
+
973
+ def get_address(self) -> str:
974
+ """Get the signer's public address."""
975
+ return str(self._keypair.pubkey())
976
+
977
+ async def sign_transaction(
978
+ self,
979
+ tx_base64: str,
980
+ network: str,
981
+ ) -> str:
982
+ """Sign a transaction."""
983
+ tx = decode_versioned_transaction(tx_base64)
984
+
985
+ # Sign the transaction
986
+ tx.sign([self._keypair])
987
+
988
+ return encode_transaction(tx)
989
+
990
+
991
+ class RpcSvmSigner:
992
+ """
993
+ Facilitator SVM signer with RPC capabilities.
994
+
995
+ Manages multiple keypairs and provides RPC operations
996
+ for transaction simulation, sending, and confirmation.
997
+ """
998
+
999
+ def __init__(
1000
+ self,
1001
+ keypairs: List["Keypair"],
1002
+ rpc_urls: Optional[Dict[str, str]] = None,
1003
+ ):
1004
+ """
1005
+ Initialize with keypairs and optional custom RPC URLs.
1006
+
1007
+ Args:
1008
+ keypairs: List of Keypair objects for signing
1009
+ rpc_urls: Optional map of network -> RPC URL overrides
1010
+ """
1011
+ if not SOLANA_AVAILABLE:
1012
+ raise ImportError(
1013
+ "solana and solders packages required. "
1014
+ "Install with: pip install t402[svm]"
1015
+ )
1016
+
1017
+ self._keypairs = {str(kp.pubkey()): kp for kp in keypairs}
1018
+ self._rpc_urls = rpc_urls or {}
1019
+
1020
+ def get_addresses(self) -> List[str]:
1021
+ """Get all available fee payer addresses."""
1022
+ return list(self._keypairs.keys())
1023
+
1024
+ def _get_rpc_url(self, network: str) -> str:
1025
+ """Get RPC URL for a network."""
1026
+ if network in self._rpc_urls:
1027
+ return self._rpc_urls[network]
1028
+ return get_rpc_url(network)
1029
+
1030
+ async def sign_transaction(
1031
+ self,
1032
+ tx_base64: str,
1033
+ fee_payer: str,
1034
+ network: str,
1035
+ ) -> str:
1036
+ """Sign a transaction as fee payer."""
1037
+ if fee_payer not in self._keypairs:
1038
+ raise ValueError(f"Fee payer {fee_payer} not found in managed keypairs")
1039
+
1040
+ tx = decode_versioned_transaction(tx_base64)
1041
+ keypair = self._keypairs[fee_payer]
1042
+
1043
+ # Sign the transaction
1044
+ tx.sign([keypair])
1045
+
1046
+ return encode_transaction(tx)
1047
+
1048
+ async def simulate_transaction(
1049
+ self,
1050
+ tx_base64: str,
1051
+ network: str,
1052
+ ) -> bool:
1053
+ """Simulate a transaction."""
1054
+ rpc_url = self._get_rpc_url(network)
1055
+
1056
+ async with AsyncClient(rpc_url) as client:
1057
+ tx = decode_versioned_transaction(tx_base64)
1058
+ result = await client.simulate_transaction(tx)
1059
+
1060
+ if result.value.err:
1061
+ raise Exception(f"Simulation failed: {result.value.err}")
1062
+
1063
+ return True
1064
+
1065
+ async def send_transaction(
1066
+ self,
1067
+ tx_base64: str,
1068
+ network: str,
1069
+ ) -> str:
1070
+ """Send a signed transaction."""
1071
+ rpc_url = self._get_rpc_url(network)
1072
+
1073
+ async with AsyncClient(rpc_url) as client:
1074
+ tx = decode_versioned_transaction(tx_base64)
1075
+ result = await client.send_transaction(tx)
1076
+ return str(result.value)
1077
+
1078
+ async def confirm_transaction(
1079
+ self,
1080
+ signature: str,
1081
+ network: str,
1082
+ timeout_seconds: int = 30,
1083
+ ) -> bool:
1084
+ """Wait for transaction confirmation."""
1085
+ rpc_url = self._get_rpc_url(network)
1086
+
1087
+ async with AsyncClient(rpc_url) as client:
1088
+ sig = Signature.from_string(signature)
1089
+ # Wait for confirmation with timeout
1090
+ result = await client.confirm_transaction(
1091
+ sig,
1092
+ commitment=Confirmed,
1093
+ )
1094
+ return result.value[0].confirmation_status is not None
1095
+
1096
+
1097
+ # =============================================================================
1098
+ # Scheme Implementations
1099
+ # =============================================================================
1100
+
1101
+
1102
+ class ExactSvmPayloadV2(TypedDict):
1103
+ """Exact SVM payment payload (V2 format)."""
1104
+ transaction: str
1105
+ authorization: Optional[Dict[str, Any]]
1106
+
1107
+
1108
+ class SvmPaymentRequirementsExtra(TypedDict, total=False):
1109
+ """Extra fields for SVM payment requirements."""
1110
+ feePayer: str
1111
+
1112
+
1113
+ class ExactSvmClientScheme:
1114
+ """
1115
+ Client scheme for creating SVM payment payloads.
1116
+
1117
+ Handles creation of SPL token transfer transactions
1118
+ for the exact payment scheme.
1119
+ """
1120
+
1121
+ scheme = SCHEME_EXACT
1122
+ caip_family = "solana:*"
1123
+
1124
+ def __init__(self, signer: ClientSvmSigner):
1125
+ """
1126
+ Initialize with a client signer.
1127
+
1128
+ Args:
1129
+ signer: ClientSvmSigner implementation
1130
+ """
1131
+ self._signer = signer
1132
+
1133
+ async def create_payment_payload(
1134
+ self,
1135
+ requirements: Dict[str, Any],
1136
+ build_transaction: Callable[[], Awaitable[str]],
1137
+ ) -> Dict[str, Any]:
1138
+ """
1139
+ Create a payment payload for the given requirements.
1140
+
1141
+ Args:
1142
+ requirements: Payment requirements dict
1143
+ build_transaction: Async function that builds and returns
1144
+ a base64 encoded unsigned transaction
1145
+
1146
+ Returns:
1147
+ Payment payload dict ready for header encoding
1148
+ """
1149
+ # Build the transaction
1150
+ unsigned_tx = await build_transaction()
1151
+
1152
+ # Sign the transaction
1153
+ signed_tx = await self._signer.sign_transaction(
1154
+ unsigned_tx,
1155
+ requirements.get("network", SOLANA_MAINNET),
1156
+ )
1157
+
1158
+ # Extract transfer details for authorization
1159
+ transfer = parse_transfer_checked_instruction(signed_tx)
1160
+
1161
+ now = int(time.time())
1162
+ valid_until = now + requirements.get("maxTimeoutSeconds", DEFAULT_VALIDITY_DURATION)
1163
+
1164
+ authorization = None
1165
+ if transfer:
1166
+ authorization = {
1167
+ "from": self._signer.get_address(),
1168
+ "to": requirements.get("payTo", ""),
1169
+ "mint": transfer["mint"],
1170
+ "amount": str(transfer["amount"]),
1171
+ "validUntil": valid_until,
1172
+ "feePayer": requirements.get("extra", {}).get("feePayer"),
1173
+ }
1174
+
1175
+ return {
1176
+ "t402Version": requirements.get("t402Version", 1),
1177
+ "scheme": SCHEME_EXACT,
1178
+ "network": requirements.get("network", SOLANA_MAINNET),
1179
+ "payload": {
1180
+ "transaction": signed_tx,
1181
+ "authorization": authorization,
1182
+ },
1183
+ }
1184
+
1185
+
1186
+ class ExactSvmServerScheme:
1187
+ """
1188
+ Server scheme for SVM payment processing.
1189
+
1190
+ Handles parsing prices and enhancing payment requirements
1191
+ with SVM-specific details.
1192
+ """
1193
+
1194
+ scheme = SCHEME_EXACT
1195
+ caip_family = "solana:*"
1196
+
1197
+ def parse_price(
1198
+ self,
1199
+ price: str,
1200
+ network: str,
1201
+ ) -> Dict[str, Any]:
1202
+ """
1203
+ Parse a price string into amount and asset info.
1204
+
1205
+ Args:
1206
+ price: Price string (e.g., "1.50" or "1500000")
1207
+ network: Network identifier
1208
+
1209
+ Returns:
1210
+ Dict with amount (in atomic units) and asset info
1211
+ """
1212
+ default_asset = get_default_asset(network)
1213
+ if not default_asset:
1214
+ raise ValueError(f"Unsupported network: {network}")
1215
+
1216
+ decimals = default_asset["decimals"]
1217
+
1218
+ # Check if price is already in atomic units (no decimal)
1219
+ if "." in price:
1220
+ amount = parse_amount(price, decimals)
1221
+ else:
1222
+ amount = int(price)
1223
+
1224
+ return {
1225
+ "amount": str(amount),
1226
+ "asset": default_asset["mint_address"],
1227
+ "decimals": decimals,
1228
+ "symbol": default_asset["symbol"],
1229
+ }
1230
+
1231
+ def enhance_payment_requirements(
1232
+ self,
1233
+ requirements: Dict[str, Any],
1234
+ fee_payer: Optional[str] = None,
1235
+ ) -> Dict[str, Any]:
1236
+ """
1237
+ Enhance payment requirements with SVM-specific details.
1238
+
1239
+ Args:
1240
+ requirements: Base payment requirements
1241
+ fee_payer: Optional fee payer address from facilitator
1242
+
1243
+ Returns:
1244
+ Enhanced requirements dict
1245
+ """
1246
+ enhanced = dict(requirements)
1247
+
1248
+ # Normalize network to CAIP-2 format
1249
+ network = enhanced.get("network", SOLANA_MAINNET)
1250
+ enhanced["network"] = normalize_network(network)
1251
+
1252
+ # Add fee payer if provided
1253
+ if fee_payer:
1254
+ extra = enhanced.get("extra", {})
1255
+ extra["feePayer"] = fee_payer
1256
+ enhanced["extra"] = extra
1257
+
1258
+ return enhanced
1259
+
1260
+
1261
+ class ExactSvmFacilitatorScheme:
1262
+ """
1263
+ Facilitator scheme for SVM payment verification and settlement.
1264
+
1265
+ Handles transaction validation, simulation, and settlement
1266
+ for the exact payment scheme.
1267
+ """
1268
+
1269
+ scheme = SCHEME_EXACT
1270
+ caip_family = "solana:*"
1271
+
1272
+ def __init__(self, signer: FacilitatorSvmSigner):
1273
+ """
1274
+ Initialize with a facilitator signer.
1275
+
1276
+ Args:
1277
+ signer: FacilitatorSvmSigner implementation
1278
+ """
1279
+ self._signer = signer
1280
+
1281
+ def get_extra(self, network: str) -> Optional[Dict[str, Any]]:
1282
+ """
1283
+ Get mechanism-specific extra data for supported kinds.
1284
+
1285
+ Returns a randomly selected fee payer address to distribute load.
1286
+
1287
+ Args:
1288
+ network: Network identifier (unused for SVM)
1289
+
1290
+ Returns:
1291
+ Dict with feePayer address
1292
+ """
1293
+ import random
1294
+
1295
+ addresses = self._signer.get_addresses()
1296
+ if not addresses:
1297
+ return None
1298
+
1299
+ return {
1300
+ "feePayer": random.choice(addresses),
1301
+ }
1302
+
1303
+ def get_signers(self, network: str) -> List[str]:
1304
+ """
1305
+ Get all signer addresses for this facilitator.
1306
+
1307
+ Args:
1308
+ network: Network identifier (unused for SVM)
1309
+
1310
+ Returns:
1311
+ List of fee payer addresses
1312
+ """
1313
+ return self._signer.get_addresses()
1314
+
1315
+ async def verify(
1316
+ self,
1317
+ payload: Dict[str, Any],
1318
+ requirements: Dict[str, Any],
1319
+ ) -> Dict[str, Any]:
1320
+ """
1321
+ Verify a payment payload.
1322
+
1323
+ Args:
1324
+ payload: Payment payload dict
1325
+ requirements: Payment requirements dict
1326
+
1327
+ Returns:
1328
+ Verification result dict with isValid, invalidReason, payer
1329
+ """
1330
+ svm_payload = payload.get("payload", {})
1331
+ tx_base64 = svm_payload.get("transaction")
1332
+
1333
+ # Validate payload structure
1334
+ if not tx_base64:
1335
+ return {
1336
+ "isValid": False,
1337
+ "invalidReason": "invalid_payload_structure",
1338
+ "payer": "",
1339
+ }
1340
+
1341
+ # Validate scheme
1342
+ if payload.get("scheme") != SCHEME_EXACT or requirements.get("scheme") != SCHEME_EXACT:
1343
+ return {
1344
+ "isValid": False,
1345
+ "invalidReason": "unsupported_scheme",
1346
+ "payer": "",
1347
+ }
1348
+
1349
+ # Validate network
1350
+ accepted_network = payload.get("network", "")
1351
+ required_network = requirements.get("network", "")
1352
+ if normalize_network(accepted_network) != normalize_network(required_network):
1353
+ return {
1354
+ "isValid": False,
1355
+ "invalidReason": "network_mismatch",
1356
+ "payer": "",
1357
+ }
1358
+
1359
+ # Validate fee payer
1360
+ extra = requirements.get("extra", {})
1361
+ fee_payer = extra.get("feePayer")
1362
+ if not fee_payer:
1363
+ return {
1364
+ "isValid": False,
1365
+ "invalidReason": "invalid_exact_svm_payload_missing_fee_payer",
1366
+ "payer": "",
1367
+ }
1368
+
1369
+ # Verify fee payer is managed by this facilitator
1370
+ signer_addresses = self._signer.get_addresses()
1371
+ if fee_payer not in signer_addresses:
1372
+ return {
1373
+ "isValid": False,
1374
+ "invalidReason": "fee_payer_not_managed_by_facilitator",
1375
+ "payer": "",
1376
+ }
1377
+
1378
+ # Get token payer from transaction
1379
+ payer = get_token_payer_from_transaction(tx_base64)
1380
+ if not payer:
1381
+ return {
1382
+ "isValid": False,
1383
+ "invalidReason": "invalid_exact_svm_payload_no_transfer_instruction",
1384
+ "payer": "",
1385
+ }
1386
+
1387
+ # Parse and validate transfer instruction
1388
+ transfer = parse_transfer_checked_instruction(tx_base64)
1389
+ if not transfer:
1390
+ return {
1391
+ "isValid": False,
1392
+ "invalidReason": "invalid_exact_svm_payload_no_transfer_instruction",
1393
+ "payer": payer,
1394
+ }
1395
+
1396
+ # Security: Verify facilitator's signers are not transferring their own funds
1397
+ if transfer["authority"] in signer_addresses:
1398
+ return {
1399
+ "isValid": False,
1400
+ "invalidReason": "invalid_exact_svm_payload_transaction_fee_payer_transferring_funds",
1401
+ "payer": payer,
1402
+ }
1403
+
1404
+ # Verify mint matches requirements
1405
+ if transfer["mint"] != requirements.get("asset"):
1406
+ return {
1407
+ "isValid": False,
1408
+ "invalidReason": "invalid_exact_svm_payload_mint_mismatch",
1409
+ "payer": payer,
1410
+ }
1411
+
1412
+ # Verify amount meets requirements
1413
+ required_amount = int(requirements.get("maxAmountRequired", "0"))
1414
+ if transfer["amount"] < required_amount:
1415
+ return {
1416
+ "isValid": False,
1417
+ "invalidReason": "invalid_exact_svm_payload_amount_insufficient",
1418
+ "payer": payer,
1419
+ }
1420
+
1421
+ # Sign and simulate transaction
1422
+ try:
1423
+ signed_tx = await self._signer.sign_transaction(
1424
+ tx_base64,
1425
+ fee_payer,
1426
+ required_network,
1427
+ )
1428
+ await self._signer.simulate_transaction(signed_tx, required_network)
1429
+ except Exception as e:
1430
+ return {
1431
+ "isValid": False,
1432
+ "invalidReason": f"transaction_simulation_failed: {str(e)}",
1433
+ "payer": payer,
1434
+ }
1435
+
1436
+ return {
1437
+ "isValid": True,
1438
+ "invalidReason": None,
1439
+ "payer": payer,
1440
+ }
1441
+
1442
+ async def settle(
1443
+ self,
1444
+ payload: Dict[str, Any],
1445
+ requirements: Dict[str, Any],
1446
+ ) -> Dict[str, Any]:
1447
+ """
1448
+ Settle a payment by submitting the transaction.
1449
+
1450
+ Args:
1451
+ payload: Payment payload dict
1452
+ requirements: Payment requirements dict
1453
+
1454
+ Returns:
1455
+ Settlement result dict
1456
+ """
1457
+ network = payload.get("network", "")
1458
+ svm_payload = payload.get("payload", {})
1459
+ tx_base64 = svm_payload.get("transaction")
1460
+
1461
+ if not tx_base64:
1462
+ return {
1463
+ "success": False,
1464
+ "network": network,
1465
+ "transaction": "",
1466
+ "errorReason": "invalid_payload_structure",
1467
+ "payer": "",
1468
+ }
1469
+
1470
+ # Verify first
1471
+ verify_result = await self.verify(payload, requirements)
1472
+ if not verify_result.get("isValid"):
1473
+ return {
1474
+ "success": False,
1475
+ "network": network,
1476
+ "transaction": "",
1477
+ "errorReason": verify_result.get("invalidReason", "verification_failed"),
1478
+ "payer": verify_result.get("payer", ""),
1479
+ }
1480
+
1481
+ try:
1482
+ fee_payer = requirements.get("extra", {}).get("feePayer")
1483
+ required_network = requirements.get("network", network)
1484
+
1485
+ # Sign transaction
1486
+ signed_tx = await self._signer.sign_transaction(
1487
+ tx_base64,
1488
+ fee_payer,
1489
+ required_network,
1490
+ )
1491
+
1492
+ # Send transaction
1493
+ signature = await self._signer.send_transaction(signed_tx, required_network)
1494
+
1495
+ # Wait for confirmation
1496
+ await self._signer.confirm_transaction(signature, required_network)
1497
+
1498
+ return {
1499
+ "success": True,
1500
+ "transaction": signature,
1501
+ "network": network,
1502
+ "payer": verify_result.get("payer", ""),
1503
+ }
1504
+ except Exception as e:
1505
+ return {
1506
+ "success": False,
1507
+ "errorReason": f"transaction_failed: {str(e)}",
1508
+ "transaction": "",
1509
+ "network": network,
1510
+ "payer": verify_result.get("payer", ""),
1511
+ }
1512
+
1513
+
1514
+ # =============================================================================
1515
+ # Helper Functions
1516
+ # =============================================================================
1517
+
1518
+
1519
+ def create_client_scheme(signer: ClientSvmSigner) -> ExactSvmClientScheme:
1520
+ """
1521
+ Create a client scheme for SVM payments.
1522
+
1523
+ Args:
1524
+ signer: ClientSvmSigner implementation
1525
+
1526
+ Returns:
1527
+ ExactSvmClientScheme instance
1528
+ """
1529
+ return ExactSvmClientScheme(signer)
1530
+
1531
+
1532
+ def create_server_scheme() -> ExactSvmServerScheme:
1533
+ """
1534
+ Create a server scheme for SVM payments.
1535
+
1536
+ Returns:
1537
+ ExactSvmServerScheme instance
1538
+ """
1539
+ return ExactSvmServerScheme()
1540
+
1541
+
1542
+ def create_facilitator_scheme(signer: FacilitatorSvmSigner) -> ExactSvmFacilitatorScheme:
1543
+ """
1544
+ Create a facilitator scheme for SVM payments.
1545
+
1546
+ Args:
1547
+ signer: FacilitatorSvmSigner implementation
1548
+
1549
+ Returns:
1550
+ ExactSvmFacilitatorScheme instance
1551
+ """
1552
+ return ExactSvmFacilitatorScheme(signer)
1553
+
1554
+
1555
+ def check_solana_available() -> bool:
1556
+ """
1557
+ Check if Solana packages are available.
1558
+
1559
+ Returns:
1560
+ True if solana/solders packages are installed
1561
+ """
1562
+ return SOLANA_AVAILABLE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t402
3
- Version: 1.6.0
3
+ Version: 1.6.1
4
4
  Summary: t402: An internet native payments protocol
5
5
  Author-email: T402 Team <dev@t402.io>
6
6
  License: Apache-2.0
@@ -1,4 +1,4 @@
1
- t402/__init__.py,sha256=yara_fUhws5F34SNkO8QCDr4yWH6xq01dDdcwDGeYPw,8153
1
+ t402/__init__.py,sha256=ijh3QHwocnPewrlYYAmeW04XaJO4DuFfNYEEKJoJVSw,10225
2
2
  t402/chains.py,sha256=Hyn3bfubNpiWE1fqkyJulkonzMu3lncSt-_RCgagdro,2832
3
3
  t402/cli.py,sha256=UiGuXNHUp67P7S2nD2j67JJ1M2KWalXk5qjE3LfXK_4,9732
4
4
  t402/common.py,sha256=cRiu_MsMVldCOZH9XCzynSAtF4Xkcvhc63jbIlKhe3c,5851
@@ -10,7 +10,7 @@ t402/networks.py,sha256=WZzQ_azmlLwD4sp3RCcU0BMMZ26FGyQSiGeNjaxX340,3766
10
10
  t402/path.py,sha256=G3oxm12FS1OsxXK_BPopS4bVCFrei5MOMnQAvDbgZsY,1311
11
11
  t402/paywall.py,sha256=lSpGlDBC4slPU5rw7aDq5iBrBJXxLHRe3lgddFMKZ-Q,4119
12
12
  t402/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- t402/svm.py,sha256=IaiFy-xRpVrVe8AC2r2_Ht1R45A_6B9JrrkZhQLApiA,14672
13
+ t402/svm.py,sha256=cN_AtbYF29yVbZFGb9Hb4N3luakFkmHNTJf2GsdBF3Q,43339
14
14
  t402/svm_paywall_template.py,sha256=pmueO0Qgcwl-uyci8lVI1eiery2vggHGl_b1tnxcB_E,827953
15
15
  t402/ton.py,sha256=ggh_6nvZ55c-iYe6bNVejPJIWoFRSf85-DQmsvu-auI,11919
16
16
  t402/ton_paywall_template.py,sha256=kRddMyZk51dDd1b4-r_MlSGCc-DLRHskIxA-LN9l9vw,7343
@@ -45,7 +45,7 @@ t402/wdk/chains.py,sha256=UlkOjBeV8HhUis61YceMevHgGoPLvDJ9VzL8IskO2V8,6885
45
45
  t402/wdk/errors.py,sha256=Sz_qNoO1K8Tx1sGs3KcG9rMoKJXkIvhSkoEZyPaAyd0,6066
46
46
  t402/wdk/signer.py,sha256=cEjBTdWNVqrDZCHsuy6IC7fV81o_dX2mpKqLEhxtLGI,20234
47
47
  t402/wdk/types.py,sha256=dkuLK7hNNUTrIs15obZTYiniwT0vF87qoEMyOJkOmms,2542
48
- t402-1.6.0.dist-info/METADATA,sha256=NbhisTU-SnGKoBgFAF2fDcpHP7PysM8IOEu4_-VVsuA,11317
49
- t402-1.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
50
- t402-1.6.0.dist-info/entry_points.txt,sha256=Lx-ftLGax72kwtKHBUcyIbwOPT9tjMLc5_bxbRts-G4,39
51
- t402-1.6.0.dist-info/RECORD,,
48
+ t402-1.6.1.dist-info/METADATA,sha256=X_OS1aDoRyl9WNrPqNv9rZbVdy_swHqUGgJnasJWOHU,11317
49
+ t402-1.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
50
+ t402-1.6.1.dist-info/entry_points.txt,sha256=Lx-ftLGax72kwtKHBUcyIbwOPT9tjMLc5_bxbRts-G4,39
51
+ t402-1.6.1.dist-info/RECORD,,
File without changes