1
1
import asyncio
2
+ import json
2
3
import logging
3
4
from pathlib import Path
4
- from typing import Optional , Type , TypeVar
5
+ from typing import Dict , List , Optional , Type , TypeVar , Union , overload
6
+
7
+ import base58
8
+ from aleph_message .models import Chain
5
9
6
10
from aleph .sdk .chains .common import get_fallback_private_key
7
11
from aleph .sdk .chains .ethereum import ETHAccount
8
12
from aleph .sdk .chains .remote import RemoteAccount
13
+ from aleph .sdk .chains .solana import SOLAccount
9
14
from aleph .sdk .conf import settings
10
15
from aleph .sdk .types import AccountFromPrivateKey
16
+ from aleph .sdk .utils import parse_solana_private_key , solana_private_key_from_bytes
11
17
12
18
logger = logging .getLogger (__name__ )
13
19
14
20
T = TypeVar ("T" , bound = AccountFromPrivateKey )
15
21
22
+ CHAIN_TO_ACCOUNT_MAP : Dict [Chain , Type [AccountFromPrivateKey ]] = {
23
+ Chain .ETH : ETHAccount ,
24
+ Chain .AVAX : ETHAccount ,
25
+ Chain .SOL : SOLAccount ,
26
+ Chain .BASE : ETHAccount ,
27
+ }
28
+
29
+
30
+ def detect_chain_from_private_key (private_key : Union [str , List [int ], bytes ]) -> Chain :
31
+ """
32
+ Detect the blockchain chain based on the private key format.
33
+ - Chain.ETH for Ethereum (EVM) private keys
34
+ - Chain.SOL for Solana private keys (base58 or uint8 format).
35
+
36
+ Raises:
37
+ ValueError: If the private key format is invalid or not recognized.
38
+ """
39
+ if isinstance (private_key , (str , bytes )) and is_valid_private_key (
40
+ private_key , ETHAccount
41
+ ):
42
+ return Chain .ETH
43
+
44
+ elif is_valid_private_key (private_key , SOLAccount ):
45
+ return Chain .SOL
46
+
47
+ else :
48
+ raise ValueError ("Unsupported private key format. Unable to detect chain." )
49
+
50
+
51
+ @overload
52
+ def is_valid_private_key (
53
+ private_key : Union [str , bytes ], account_type : Type [ETHAccount ]
54
+ ) -> bool : ...
55
+
56
+
57
+ @overload
58
+ def is_valid_private_key (
59
+ private_key : Union [str , List [int ], bytes ], account_type : Type [SOLAccount ]
60
+ ) -> bool : ...
61
+
62
+
63
+ def is_valid_private_key (
64
+ private_key : Union [str , List [int ], bytes ], account_type : Type [T ]
65
+ ) -> bool :
66
+ """
67
+ Check if the private key is valid for either Ethereum or Solana based on the account type.
68
+ """
69
+ try :
70
+ if account_type == ETHAccount :
71
+ # Handle Ethereum private key validation
72
+ if isinstance (private_key , str ):
73
+ if private_key .startswith ("0x" ):
74
+ private_key = private_key [2 :]
75
+ private_key = bytes .fromhex (private_key )
76
+ elif isinstance (private_key , list ):
77
+ raise ValueError ("Ethereum keys cannot be a list of integers" )
78
+
79
+ account_type (private_key )
80
+
81
+ elif account_type == SOLAccount :
82
+ # Handle Solana private key validation
83
+ if isinstance (private_key , bytes ):
84
+ return len (private_key ) == 64
85
+ elif isinstance (private_key , str ):
86
+ decoded_key = base58 .b58decode (private_key )
87
+ return len (decoded_key ) == 64
88
+ elif isinstance (private_key , list ):
89
+ return len (private_key ) == 64 and all (
90
+ isinstance (i , int ) and 0 <= i <= 255 for i in private_key
91
+ )
92
+
93
+ return True
94
+ except Exception :
95
+ return False
96
+
16
97
17
98
def account_from_hex_string (private_key_str : str , account_type : Type [T ]) -> T :
18
99
if private_key_str .startswith ("0x" ):
@@ -22,6 +103,11 @@ def account_from_hex_string(private_key_str: str, account_type: Type[T]) -> T:
22
103
23
104
def account_from_file (private_key_path : Path , account_type : Type [T ]) -> T :
24
105
private_key = private_key_path .read_bytes ()
106
+ if account_type == SOLAccount :
107
+ private_key = parse_solana_private_key (
108
+ solana_private_key_from_bytes (private_key )
109
+ )
110
+
25
111
return account_type (private_key )
26
112
27
113
@@ -33,10 +119,59 @@ def _load_account(
33
119
"""Load private key from a string or a file. takes the string argument in priority"""
34
120
35
121
if private_key_str :
122
+ # Check Account type based on private-key string format (base58 / uint for solana)
123
+ private_key_chain = detect_chain_from_private_key (private_key = private_key_str )
124
+ if private_key_chain == Chain .SOL :
125
+ account_type = SOLAccount
126
+ logger .debug ("Solana private key is detected" )
127
+ parsed_key = parse_solana_private_key (private_key_str )
128
+ return account_type (parsed_key )
36
129
logger .debug ("Using account from string" )
37
130
return account_from_hex_string (private_key_str , account_type )
38
131
elif private_key_path and private_key_path .is_file ():
39
- logger .debug ("Using account from file" )
132
+ if private_key_path :
133
+ try :
134
+ # Look for the account by private_key_path in CHAINS_CONFIG_FILE
135
+ with open (settings .CHAINS_CONFIG_FILE , "r" ) as file :
136
+ accounts = json .load (file )
137
+
138
+ matching_account = next (
139
+ (
140
+ account
141
+ for account in accounts
142
+ if account ["path" ] == str (private_key_path )
143
+ ),
144
+ None ,
145
+ )
146
+
147
+ if matching_account :
148
+ chain = Chain (matching_account ["chain" ])
149
+ account_type = CHAIN_TO_ACCOUNT_MAP .get (chain , ETHAccount )
150
+ if account_type is None :
151
+ account_type = ETHAccount
152
+ logger .debug (
153
+ f"Detected { chain } account for path { private_key_path } "
154
+ )
155
+ else :
156
+ logger .warning (
157
+ f"No matching account found for path { private_key_path } , defaulting to { account_type .__name__ } "
158
+ )
159
+
160
+ except FileNotFoundError :
161
+ logger .warning (
162
+ f"CHAINS_CONFIG_FILE not found, using default account type { account_type .__name__ } "
163
+ )
164
+ except json .JSONDecodeError :
165
+ logger .error (
166
+ f"Invalid format in CHAINS_CONFIG_FILE, unable to load account info."
167
+ )
168
+ raise ValueError (f"Invalid format in { settings .CHAINS_CONFIG_FILE } ." )
169
+ except Exception as e :
170
+ logger .error (f"Error loading accounts from config: { e } " )
171
+ raise ValueError (
172
+ f"Could not find matching account for path { private_key_path } ."
173
+ )
174
+
40
175
return account_from_file (private_key_path , account_type )
41
176
elif settings .REMOTE_CRYPTO_HOST :
42
177
logger .debug ("Using remote account" )
0 commit comments