Skip to content

Commit 5a4fd4e

Browse files
authored
Merge pull request #58 from helloissariel/grace/crypto
add neo/solana Turnkey signers
2 parents 024abee + b8d7bdd commit 5a4fd4e

File tree

5 files changed

+1516
-16
lines changed

5 files changed

+1516
-16
lines changed

spoon_toolkits/crypto/evm/signers.py

Lines changed: 154 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
44
This module provides a clean abstraction for transaction signing, allowing EVM tools
55
to 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

813
import os
@@ -17,6 +22,105 @@
1722

1823
logger = 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

21125
class 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

Comments
 (0)