33
44This module provides a clean abstraction for transaction signing, allowing EVM tools
55to work with either local private keys (via web3.py) or Turnkey's secure API.
6+
7+ Priority order for auto-detection:
8+ 1. Plain private key from environment variable (not encrypted)
9+ 2. Encrypted private key from SecretVault (ENC:v2 decrypted)
10+ 3. Turnkey remote signing
611"""
712
813import os
1722
1823logger = logging .getLogger (__name__ )
1924
25+ # Environment variable keys
26+ ENV_PRIVATE_KEY = "EVM_PRIVATE_KEY"
27+ ENV_TURNKEY_SIGN_WITH = "TURNKEY_SIGN_WITH"
28+ ENV_TURNKEY_ADDRESS = "TURNKEY_ADDRESS"
29+
30+
31+ def _is_encrypted (value : str ) -> bool :
32+ """Check if a value is an encrypted secret (ENC: prefix)."""
33+ return value .startswith ("ENC:" )
34+
35+
36+ def _get_from_vault (key : str ) -> Optional [str ]:
37+ """Try to retrieve a decrypted secret from SecretVault."""
38+ try :
39+ from spoon_ai .wallet .vault import get_vault
40+ vault = get_vault ()
41+ if vault .exists (key ):
42+ raw = vault .get_raw (key )
43+ if raw :
44+ return raw .decode ("utf-8" )
45+ except ImportError :
46+ pass
47+ except Exception as e :
48+ logger .debug ("Failed to get %s from vault: %s" , key , e )
49+ return None
50+
51+
52+ def _auto_decrypt_to_vault (env_key : str ) -> bool :
53+ """
54+ Auto-decrypt an encrypted env var and store in vault.
55+
56+ Returns True if decryption succeeded, False otherwise.
57+ """
58+ enc_value = os .getenv (env_key )
59+ if not enc_value or not _is_encrypted (enc_value ):
60+ return False
61+
62+ try :
63+ from spoon_ai .wallet .vault import get_vault
64+ from spoon_ai .wallet .security import decrypt_and_store
65+
66+ vault = get_vault ()
67+
68+ # Already decrypted?
69+ if vault .exists (env_key ):
70+ return True
71+
72+ # Get master password
73+ password = os .getenv ("SPOON_MASTER_PWD" )
74+ if not password :
75+ import sys
76+ import getpass
77+ try :
78+ if sys .stdin .isatty ():
79+ password = getpass .getpass (
80+ f"Enter password to decrypt { env_key } : "
81+ )
82+ except Exception :
83+ pass
84+
85+ if not password :
86+ logger .warning (
87+ f"Encrypted { env_key } found but no password available. "
88+ f"Set SPOON_MASTER_PWD or run interactively."
89+ )
90+ return False
91+
92+ # Decrypt and store
93+ decrypt_and_store (enc_value , password , env_key , vault = vault )
94+ logger .info (f"Decrypted { env_key } and stored in vault." )
95+ return True
96+
97+ except ImportError as e :
98+ logger .warning (f"Cannot decrypt { env_key } : { e } " )
99+ return False
100+ except Exception as e :
101+ logger .error (f"Failed to decrypt { env_key } : { e } " )
102+ return False
103+
104+
105+ def _get_private_key_from_vault () -> Optional [str ]:
106+ """
107+ Get decrypted private key from SecretVault.
108+
109+ If an encrypted key exists in env but not in vault, auto-decrypt it first.
110+ """
111+ # Check if already in vault
112+ value = _get_from_vault (ENV_PRIVATE_KEY )
113+ if value :
114+ return value
115+
116+ # Try auto-decrypt if encrypted in env
117+ env_value = os .getenv (ENV_PRIVATE_KEY )
118+ if env_value and _is_encrypted (env_value ):
119+ if _auto_decrypt_to_vault (ENV_PRIVATE_KEY ):
120+ return _get_from_vault (ENV_PRIVATE_KEY )
121+
122+ return None
123+
20124
21125class SignerError (Exception ):
22126 """Exception raised for signing-related errors."""
@@ -257,6 +361,11 @@ def create_signer(
257361 """
258362 Create a signer based on configuration.
259363
364+ Priority order for auto-detection:
365+ 1. Plain private key from env (not encrypted)
366+ 2. Encrypted private key from SecretVault
367+ 3. Turnkey remote signing
368+
260369 Args:
261370 signer_type: 'local', 'turnkey', or 'auto'
262371 private_key: Private key for local signing
@@ -268,39 +377,70 @@ def create_signer(
268377 """
269378 # Auto-detect signer type
270379 if signer_type == "auto" :
271- if turnkey_sign_with :
272- signer_type = "turnkey"
273- elif private_key :
380+ # Check explicit parameters first
381+ if private_key :
274382 signer_type = "local"
383+ elif turnkey_sign_with :
384+ signer_type = "turnkey"
275385 else :
276- # Check environment variables
277- if os .getenv ("TURNKEY_SIGN_WITH" ):
278- signer_type = "turnkey"
279- elif os .getenv ("EVM_PRIVATE_KEY" ):
386+ # Priority: plain env -> vault -> turnkey
387+ env_key = os .getenv (ENV_PRIVATE_KEY )
388+
389+ # 1. Plain private key from env (not encrypted)
390+ if env_key and not _is_encrypted (env_key ):
391+ signer_type = "local"
392+
393+ # 2. Encrypted private key from SecretVault (auto-decrypt if needed)
394+ elif _get_private_key_from_vault ():
280395 signer_type = "local"
396+
397+ # 3. Turnkey remote signing
398+ elif os .getenv (ENV_TURNKEY_SIGN_WITH ):
399+ signer_type = "turnkey"
400+
281401 else :
282- raise ValueError ("Cannot auto-detect signer type, please specify signer_type or provide credentials" )
402+ raise ValueError (
403+ "Cannot auto-detect signer type. Options:\n "
404+ f"1. Set { ENV_PRIVATE_KEY } with plain private key\n "
405+ f"2. Set { ENV_PRIVATE_KEY } with ENC:v2 encrypted key and decrypt to vault\n "
406+ f"3. Set { ENV_TURNKEY_SIGN_WITH } for Turnkey signing"
407+ )
283408
284409 if signer_type == "local" :
285- key = private_key or os .getenv ("EVM_PRIVATE_KEY" )
410+ # Try sources in priority order: param -> plain env -> vault (auto-decrypt)
411+ key = private_key
412+ if not key :
413+ env_key = os .getenv (ENV_PRIVATE_KEY )
414+ if env_key and not _is_encrypted (env_key ):
415+ key = env_key
286416 if not key :
287- raise ValueError ("Private key required for local signing" )
417+ key = _get_private_key_from_vault ()
418+
419+ if not key :
420+ raise ValueError (
421+ f"Private key required for local signing. "
422+ f"Set { ENV_PRIVATE_KEY } or decrypt encrypted key to vault."
423+ )
424+
288425 # Ensure private key has 0x prefix
289426 key = key .strip ()
290427 if not key .startswith ("0x" ):
291428 key = "0x" + key
292429 return LocalSigner (key )
293430
294431 elif signer_type == "turnkey" :
295- sign_with = turnkey_sign_with or os .getenv ("TURNKEY_SIGN_WITH" )
432+ sign_with = turnkey_sign_with or os .getenv (ENV_TURNKEY_SIGN_WITH )
296433 if not sign_with :
297- raise ValueError ("turnkey_sign_with required for Turnkey signing" )
434+ raise ValueError (
435+ f"turnkey_sign_with required for Turnkey signing. "
436+ f"Set { ENV_TURNKEY_SIGN_WITH } env var."
437+ )
298438
299439 signer = TurnkeySigner (sign_with )
300440 if turnkey_address :
301441 signer ._cached_address = turnkey_address
302- elif os .getenv ("TURNKEY_ADDRESS" ):
303- signer ._cached_address = os .getenv ("TURNKEY_ADDRESS" )
442+ elif os .getenv (ENV_TURNKEY_ADDRESS ):
443+ signer ._cached_address = os .getenv (ENV_TURNKEY_ADDRESS )
304444
305445 return signer
306446
0 commit comments