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,4 +1,4 @@
|
|
|
1
|
-
t402/__init__.py,sha256=
|
|
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=
|
|
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.
|
|
49
|
-
t402-1.6.
|
|
50
|
-
t402-1.6.
|
|
51
|
-
t402-1.6.
|
|
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
|
|
File without changes
|