diff --git a/spoon_toolkits/crypto/evm/signers.py b/spoon_toolkits/crypto/evm/signers.py index ad46b6d..1f0be18 100644 --- a/spoon_toolkits/crypto/evm/signers.py +++ b/spoon_toolkits/crypto/evm/signers.py @@ -26,6 +26,39 @@ ENV_PRIVATE_KEY = "EVM_PRIVATE_KEY" ENV_TURNKEY_SIGN_WITH = "TURNKEY_SIGN_WITH" ENV_TURNKEY_ADDRESS = "TURNKEY_ADDRESS" +ENV_TURNKEY_API_PRIVATE_KEY = "TURNKEY_API_PRIVATE_KEY" + + +def _get_turnkey_api_private_key_from_vault() -> Optional[str]: + """ + Get decrypted Turnkey API private key from SecretVault. + If an encrypted key exists in env but not in vault, auto-decrypt it first. + """ + value = _get_from_vault(ENV_TURNKEY_API_PRIVATE_KEY) + if value: + logger.debug(f"Retrieved {ENV_TURNKEY_API_PRIVATE_KEY} from vault (already decrypted)") + return value + + env_value = os.getenv(ENV_TURNKEY_API_PRIVATE_KEY) + if env_value: + if _is_encrypted(env_value): + logger.info(f"Found encrypted {ENV_TURNKEY_API_PRIVATE_KEY} in environment, attempting decryption...") + if _auto_decrypt_to_vault(ENV_TURNKEY_API_PRIVATE_KEY): + value = _get_from_vault(ENV_TURNKEY_API_PRIVATE_KEY) + if value: + logger.info(f"Successfully decrypted {ENV_TURNKEY_API_PRIVATE_KEY} and retrieved from vault") + return value + + # CRITICAL: If it's encrypted but decryption failed, we must NOT return None + # because that would cause the client to fallback to the encrypted string. + raise SignerError( + f"Failed to decrypt {ENV_TURNKEY_API_PRIVATE_KEY}. " + "The password in SPOON_MASTER_PWD is likely incorrect or missing." + ) + else: + logger.debug(f"{ENV_TURNKEY_API_PRIVATE_KEY} in environment is plaintext") + return env_value + return None def _is_encrypted(value: str) -> bool: @@ -111,13 +144,29 @@ def _get_private_key_from_vault() -> Optional[str]: # Check if already in vault value = _get_from_vault(ENV_PRIVATE_KEY) if value: + logger.debug(f"Retrieved {ENV_PRIVATE_KEY} from vault (already decrypted)") return value # Try auto-decrypt if encrypted in env env_value = os.getenv(ENV_PRIVATE_KEY) - if env_value and _is_encrypted(env_value): - if _auto_decrypt_to_vault(ENV_PRIVATE_KEY): - return _get_from_vault(ENV_PRIVATE_KEY) + if env_value: + if _is_encrypted(env_value): + logger.info(f"Found encrypted {ENV_PRIVATE_KEY} in environment, attempting decryption...") + if _auto_decrypt_to_vault(ENV_PRIVATE_KEY): + value = _get_from_vault(ENV_PRIVATE_KEY) + if value: + logger.info(f"Successfully decrypted {ENV_PRIVATE_KEY} and retrieved from vault") + return value + + # CRITICAL: If it's encrypted but decryption failed, we must NOT return None + # because that would cause the client to fallback to the encrypted string. + raise SignerError( + f"Failed to decrypt {ENV_PRIVATE_KEY}. " + "The password in SPOON_MASTER_PWD is likely incorrect or missing." + ) + else: + logger.debug(f"{ENV_PRIVATE_KEY} in environment is plaintext") + return env_value return None @@ -222,7 +271,9 @@ def _get_turnkey_client(self): if self._turnkey is None: try: from spoon_ai.turnkey import Turnkey - self._turnkey = Turnkey() + # Get decrypted API private key from vault + api_private_key = _get_turnkey_api_private_key_from_vault() + self._turnkey = Turnkey(api_private_key=api_private_key) except Exception as e: raise SignerError(f"Failed to initialize Turnkey client: {str(e)}") return self._turnkey @@ -233,7 +284,7 @@ async def sign_transaction(self, tx_dict: Dict[str, Any], rpc_url: str) -> str: from web3 import Web3 import rlp - w3 = Web3(HTTPProvider(rpc_url)) if rpc_url else Web3() + w3 = Web3(HTTPProvider(rpc_url)) if rpc_url else None # Helper function to convert int to bytes def int_to_bytes(value: int) -> bytes: @@ -245,7 +296,16 @@ def int_to_bytes(value: int) -> bytes: # Check if it's EIP-1559 (has maxFeePerGas) or legacy (has gasPrice) if "maxFeePerGas" in tx_dict or "maxPriorityFeePerGas" in tx_dict: # EIP-1559 transaction (type 2) - chain_id = tx_dict.get("chainId", w3.eth.chain_id if rpc_url else 1) + chain_id = tx_dict.get("chainId") + if chain_id is None: + if w3 and rpc_url: + try: + chain_id = w3.eth.chain_id + except Exception: + chain_id = 1 + else: + chain_id = 1 + nonce = tx_dict.get("nonce", 0) max_priority_fee_per_gas = tx_dict.get("maxPriorityFeePerGas", 0) max_fee_per_gas = tx_dict.get("maxFeePerGas", 0) @@ -279,7 +339,16 @@ def int_to_bytes(value: int) -> bytes: else: # Legacy transaction (type 0) - convert to EIP-1559 format for Turnkey # Turnkey prefers EIP-1559 format, so we'll convert legacy tx to EIP-1559 - chain_id = tx_dict.get("chainId", w3.eth.chain_id if rpc_url else 1) + chain_id = tx_dict.get("chainId") + if chain_id is None: + if w3 and rpc_url: + try: + chain_id = w3.eth.chain_id + except Exception: + chain_id = 1 + else: + chain_id = 1 + nonce = tx_dict.get("nonce", 0) gas_price = tx_dict.get("gasPrice", 0) gas_limit = tx_dict.get("gas", tx_dict.get("gasLimit", 21000)) @@ -379,7 +448,23 @@ def create_signer( if signer_type == "auto": # Check explicit parameters first if private_key: - signer_type = "local" + # If encrypted, check if we can decrypt before committing to local + if _is_encrypted(private_key): + password = os.getenv("SPOON_MASTER_PWD") + if password: + signer_type = "local" + elif turnkey_sign_with or os.getenv(ENV_TURNKEY_SIGN_WITH): + # Can't decrypt, but Turnkey is available - use Turnkey + logger.warning( + "Encrypted private_key provided but SPOON_MASTER_PWD not set. " + "Falling back to Turnkey signing." + ) + signer_type = "turnkey" + else: + # No Turnkey fallback, will fail later with helpful error + signer_type = "local" + else: + signer_type = "local" elif turnkey_sign_with: signer_type = "turnkey" else: @@ -395,7 +480,7 @@ def create_signer( signer_type = "local" # 3. Turnkey remote signing - elif os.getenv(ENV_TURNKEY_SIGN_WITH): + elif os.getenv(ENV_TURNKEY_SIGN_WITH) or _get_turnkey_api_private_key_from_vault(): signer_type = "turnkey" else: @@ -407,25 +492,64 @@ def create_signer( ) if signer_type == "local": - # Try sources in priority order: param -> plain env -> vault (auto-decrypt) - key = private_key + # Try sources in priority order: param -> env -> vault (auto-decrypt) + key = None + + # 1. Check parameter first + if private_key: + if _is_encrypted(private_key): + logger.info("Found encrypted private_key parameter, decrypting to vault...") + password = os.getenv("SPOON_MASTER_PWD") + if not password: + raise SignerError( + "Found encrypted private key but SPOON_MASTER_PWD is not set. " + "Please export SPOON_MASTER_PWD to decrypt the key." + ) + try: + from spoon_ai.wallet.security import decrypt_and_store + from spoon_ai.wallet.vault import get_vault + vault = get_vault() + param_vault_key = f"{ENV_PRIVATE_KEY}_PARAM" + decrypt_and_store(private_key, password, param_vault_key, vault=vault) + key = _get_from_vault(param_vault_key) + if key: + logger.info("Successfully decrypted provided private key and stored in vault.") + except Exception as e: + raise SignerError( + f"Failed to decrypt provided private key: {str(e)}. " + "Check if SPOON_MASTER_PWD is correct." + ) + else: + logger.debug("Using plaintext private_key parameter") + key = private_key + + # 2. Check environment variable if not key: env_key = os.getenv(ENV_PRIVATE_KEY) - if env_key and not _is_encrypted(env_key): - key = env_key + if env_key: + if _is_encrypted(env_key): + logger.info(f"Found encrypted {ENV_PRIVATE_KEY} in environment, will decrypt via vault...") + key = _get_private_key_from_vault() + else: + logger.debug(f"Using plaintext {ENV_PRIVATE_KEY} from environment") + key = env_key + + # 3. Check vault (for already decrypted keys or fallback) if not key: + logger.debug("Checking vault for decrypted private key...") key = _get_private_key_from_vault() if not key: raise ValueError( f"Private key required for local signing. " - f"Set {ENV_PRIVATE_KEY} or decrypt encrypted key to vault." + f"Set {ENV_PRIVATE_KEY} or provide a private_key parameter." ) # Ensure private key has 0x prefix key = key.strip() if not key.startswith("0x"): key = "0x" + key + logger.debug("Private key retrieved successfully, creating LocalSigner") return LocalSigner(key) elif signer_type == "turnkey": diff --git a/spoon_toolkits/crypto/neo/signers.py b/spoon_toolkits/crypto/neo/signers.py index 6735101..38ae0e7 100644 --- a/spoon_toolkits/crypto/neo/signers.py +++ b/spoon_toolkits/crypto/neo/signers.py @@ -203,7 +203,7 @@ def _ensure_initialized(self): return try: - from neo3.wallet import Account + from neo3.wallet.account import Account from neo3.core import types # Try WIF format first, then hex @@ -217,14 +217,20 @@ def _ensure_initialized(self): key_hex = self._private_key # Convert hex to bytes and create account - key_bytes = bytes.fromhex(key_hex) + try: + key_bytes = bytes.fromhex(key_hex) + except ValueError as e: + raise SignerError( + f"Invalid hex private key format: {e}. " + f"Expected a valid hexadecimal string (WIF or hex format)." + ) self._account = Account.from_private_key(key_bytes) self._initialized = True except ImportError: raise SignerError( - "neo3 library not installed. Install with: pip install neo3" + "neo-mamba library not installed. Install with: pip install neo-mamba" ) except Exception as e: raise SignerError(f"Failed to initialize Neo account: {e}") @@ -388,12 +394,25 @@ def _parse_signature(self, result: Dict[str, Any]) -> bytes: # Check for direct signature signature_hex = result.get("signature") or result.get("signatureHex") if signature_hex: - return bytes.fromhex(signature_hex.replace("0x", "")) + if not isinstance(signature_hex, str): + raise SignerError( + f"Invalid signature type: expected str, got {type(signature_hex).__name__}" + ) + try: + return bytes.fromhex(signature_hex.replace("0x", "")) + except ValueError as e: + raise SignerError(f"Invalid signature hex format: {e}") # Reconstruct from r and s r_hex = result.get("r", "") s_hex = result.get("s", "") + # Type check for r and s + if not isinstance(r_hex, str) or not isinstance(s_hex, str): + raise SignerError( + f"Invalid r or s type: expected str, got r={type(r_hex).__name__}, s={type(s_hex).__name__}" + ) + if not r_hex or not s_hex: raise SignerError("Turnkey signature missing r or s components") @@ -401,8 +420,11 @@ def _parse_signature(self, result: Dict[str, Any]) -> bytes: s_hex = s_hex.replace("0x", "") # Pad to 32 bytes each for P-256 curve - r_bytes = bytes.fromhex(r_hex.zfill(64)) - s_bytes = bytes.fromhex(s_hex.zfill(64)) + try: + r_bytes = bytes.fromhex(r_hex.zfill(64)) + s_bytes = bytes.fromhex(s_hex.zfill(64)) + except ValueError as e: + raise SignerError(f"Invalid r or s hex format: {e}") return r_bytes + s_bytes @@ -451,14 +473,14 @@ async def get_script_hash(self) -> str: try: from neo3.core import types - from neo3.wallet import Account + from neo3.wallet.account import Account # Convert Neo address to script hash script_hash = Account.address_to_script_hash(address) self._cached_script_hash = f"0x{script_hash}" return self._cached_script_hash except ImportError: - raise SignerError("neo3 library required for address conversion") + raise SignerError("neo-mamba library required for address conversion. Install with: pip install neo-mamba") except Exception as e: raise SignerError(f"Failed to convert address to script hash: {e}") @@ -502,7 +524,7 @@ def create_signer( elif turnkey_sign_with: signer_type = "turnkey" else: - # Priority: plain env -> vault -> turnkey + # Priority: plain env -> vault -> auto-decrypt -> turnkey env_key = os.getenv(ENV_PRIVATE_KEY) # 1. Plain private key from env (not encrypted) @@ -513,6 +535,11 @@ def create_signer( elif _get_from_vault(ENV_PRIVATE_KEY): signer_type = "local" + # 2b. Try auto-decrypt if encrypted in env + elif env_key and _is_encrypted(env_key): + if _auto_decrypt_to_vault(ENV_PRIVATE_KEY): + signer_type = "local" + # 3. Turnkey remote signing elif os.getenv(ENV_TURNKEY_SIGN_WITH): signer_type = "turnkey" @@ -526,14 +553,15 @@ def create_signer( ) if signer_type == "local": - # Try sources in priority order: param -> plain env -> vault + # Try sources in priority order: param -> plain env -> vault (with auto-decrypt) key = private_key if not key: env_key = os.getenv(ENV_PRIVATE_KEY) if env_key and not _is_encrypted(env_key): key = env_key if not key: - key = _get_from_vault(ENV_PRIVATE_KEY) + # Use _get_private_key_from_vault which handles auto-decryption + key = _get_private_key_from_vault() if not key: raise ValueError( diff --git a/spoon_toolkits/crypto/solana/swap.py b/spoon_toolkits/crypto/solana/swap.py index ba08b6c..1fe5ad7 100644 --- a/spoon_toolkits/crypto/solana/swap.py +++ b/spoon_toolkits/crypto/solana/swap.py @@ -13,7 +13,7 @@ parse_transaction_error, validate_solana_address, ) -from .keypairUtils import get_wallet_key +from .signers import SignerManager, SolanaSigner from .constants import ( JUPITER_QUOTE_ENDPOINT, JUPITER_SWAP_ENDPOINT, @@ -28,9 +28,13 @@ logger = logging.getLogger(__name__) class SolanaSwapTool(BaseTool): + """Swap tokens on Solana using Jupiter aggregator. + + Supports both local private key and Turnkey secure signing. + """ name: str = "solana_swap" - description: str = "Swap tokens on Solana using Jupiter aggregator" + description: str = "Swap tokens on Solana using Jupiter aggregator. Supports local and Turnkey secure signing." parameters: dict = { "type": "object", "properties": { @@ -38,9 +42,19 @@ class SolanaSwapTool(BaseTool): "type": "string", "description": "Solana RPC endpoint URL. Defaults to SOLANA_RPC_URL env var." }, + "signer_type": { + "type": "string", + "enum": ["local", "turnkey", "auto"], + "description": "Signing method: 'local' (private key), 'turnkey' (secure API), or 'auto' (detect from env)", + "default": "auto", + }, "private_key": { "type": "string", - "description": "Wallet private key. Defaults to SOLANA_PRIVATE_KEY env var." + "description": "Wallet private key. Required for local signing. Defaults to SOLANA_PRIVATE_KEY env var." + }, + "turnkey_sign_with": { + "type": "string", + "description": "Turnkey signing identity (Solana address). Required for Turnkey signing. Defaults to TURNKEY_SOLANA_ADDRESS env var.", }, "input_token": { "type": "string", @@ -69,7 +83,9 @@ class SolanaSwapTool(BaseTool): } rpc_url: Optional[str] = Field(default=None) + signer_type: str = Field(default="auto") private_key: Optional[str] = Field(default=None) + turnkey_sign_with: Optional[str] = Field(default=None) input_token: Optional[str] = Field(default=None) output_token: Optional[str] = Field(default=None) amount: Optional[Union[str, float]] = Field(default=None) @@ -79,7 +95,9 @@ class SolanaSwapTool(BaseTool): async def execute( self, rpc_url: Optional[str] = None, + signer_type: Optional[str] = None, private_key: Optional[str] = None, + turnkey_sign_with: Optional[str] = None, input_token: Optional[str] = None, output_token: Optional[str] = None, amount: Optional[Union[str, float]] = None, @@ -91,16 +109,18 @@ async def execute( try: # Resolve parameters rpc_url = rpc_url or self.rpc_url or get_rpc_url() + signer_type = signer_type or self.signer_type private_key = private_key or self.private_key + turnkey_sign_with = turnkey_sign_with or self.turnkey_sign_with input_token = input_token or self.input_token output_token = output_token or self.output_token amount = amount or self.amount slippage_bps = slippage_bps if slippage_bps is not None else self.slippage_bps priority_level = priority_level or self.priority_level - + # Log received parameters for debugging logger.debug(f"SolanaSwapTool.execute called with: input_token={input_token}, output_token={output_token}, amount={amount}") - + # Validate required parameters early if not input_token: return ToolResult(error="input_token parameter is required. Please provide 'SOL' for native SOL or a token mint address.") @@ -109,12 +129,17 @@ async def execute( if amount is None: return ToolResult(error="amount parameter is required.") - # Get wallet keypair with dynamic private key support - keypair_result = get_wallet_key(require_private_key=True, private_key=private_key) - if not keypair_result.keypair: - return ToolResult(error="Failed to get wallet keypair") - wallet_keypair = keypair_result.keypair - wallet_pubkey = str(wallet_keypair.pubkey()) + # Create signer using SignerManager (supports local and Turnkey) + try: + signer = SignerManager.create_signer( + signer_type=signer_type, + private_key=private_key, + turnkey_sign_with=turnkey_sign_with, + ) + except Exception as e: + return ToolResult(error=f"Failed to create signer: {str(e)}") + + wallet_pubkey = signer.get_address() portfolio = await self._load_wallet_portfolio(runtime, rpc_url, wallet_pubkey) @@ -173,7 +198,7 @@ async def execute( # Execute swap execute_result = await self._execute_swap_transaction( - rpc_url, wallet_keypair, swap_result["swap_transaction"] + rpc_url, signer, swap_result["swap_transaction"] ) if not execute_result["success"]: @@ -194,7 +219,8 @@ async def execute( "price_impact": float(quote.get("priceImpactPct", 0)), "slippage_bps": quote_context["slippage_bps"], "route_plan": quote.get("routePlan", []), - "fees": execute_result.get("fees", {}) + "fees": execute_result.get("fees", {}), + "signer_type": signer.signer_type, }) except Exception as e: @@ -451,17 +477,34 @@ async def _get_jupiter_swap_transaction( async def _execute_swap_transaction( self, rpc_url: str, - wallet_keypair, + signer: SolanaSigner, swap_transaction_base64: str ) -> Dict[str, Any]: """Execute the swap transaction.""" try: from solders.transaction import VersionedTransaction + from solders.signature import Signature import base64 transaction_bytes = base64.b64decode(swap_transaction_base64) transaction = VersionedTransaction.from_bytes(transaction_bytes) - transaction.sign([wallet_keypair]) + + # Sign the transaction using the signer + # For Turnkey, we need to sign the message and reconstruct the transaction + if signer.signer_type == "turnkey": + # Get the message bytes for signing + message_bytes = bytes(transaction.message) + # Sign using Turnkey + signature_bytes = signer.sign_transaction(message_bytes) + signature = Signature.from_bytes(signature_bytes) + # Reconstruct transaction with new signature + # The transaction already has a placeholder signature, replace it + signatures = list(transaction.signatures) + signatures[0] = signature + transaction = VersionedTransaction.populate(transaction.message, signatures) + else: + # Local signer - use the keypair directly + transaction.sign([signer.keypair]) return await self._submit_signed_transaction(rpc_url, transaction) diff --git a/spoon_toolkits/crypto/solana/transfer.py b/spoon_toolkits/crypto/solana/transfer.py index c1ed62a..8309ccb 100644 --- a/spoon_toolkits/crypto/solana/transfer.py +++ b/spoon_toolkits/crypto/solana/transfer.py @@ -4,7 +4,7 @@ from spoon_ai.tools.base import BaseTool, ToolResult from .service import get_rpc_url, get_associated_token_address, is_native_sol -from .keypairUtils import get_wallet_key +from .signers import SignerManager, SolanaSigner from .constants import TOKEN_PROGRAM_ID from solana.rpc.async_api import AsyncClient @@ -19,8 +19,13 @@ class SolanaTransferTool(BaseTool): + """Transfer SOL or SPL tokens on Solana. + + Supports both local private key and Turnkey secure signing. + """ + name: str = "solana_transfer" - description: str = "Transfer SOL or SPL tokens to another address on Solana" + description: str = "Transfer SOL or SPL tokens to another address on Solana. Supports local and Turnkey secure signing." parameters: dict = { "type": "object", "properties": { @@ -28,9 +33,19 @@ class SolanaTransferTool(BaseTool): "type": "string", "description": "Solana RPC endpoint URL. Defaults to SOLANA_RPC_URL env var." }, + "signer_type": { + "type": "string", + "enum": ["local", "turnkey", "auto"], + "description": "Signing method: 'local' (private key), 'turnkey' (secure API), or 'auto' (detect from env)", + "default": "auto", + }, "private_key": { "type": "string", - "description": "Sender private key. Defaults to SOLANA_PRIVATE_KEY env var." + "description": "Sender private key. Required for local signing. Defaults to SOLANA_PRIVATE_KEY env var." + }, + "turnkey_sign_with": { + "type": "string", + "description": "Turnkey signing identity (Solana address). Required for Turnkey signing. Defaults to TURNKEY_SOLANA_ADDRESS env var.", }, "recipient": { "type": "string", @@ -49,7 +64,9 @@ class SolanaTransferTool(BaseTool): } rpc_url: Optional[str] = Field(default=None) + signer_type: str = Field(default="auto") private_key: Optional[str] = Field(default=None) + turnkey_sign_with: Optional[str] = Field(default=None) recipient: Optional[str] = Field(default=None) amount: Optional[str] = Field(default=None) token_address: Optional[str] = Field(default=None) @@ -57,7 +74,9 @@ class SolanaTransferTool(BaseTool): async def execute( self, rpc_url: Optional[str] = None, + signer_type: Optional[str] = None, private_key: Optional[str] = None, + turnkey_sign_with: Optional[str] = None, recipient: Optional[str] = None, amount: Optional[str] = None, token_address: Optional[str] = None @@ -66,7 +85,9 @@ async def execute( try: # Resolve parameters rpc_url = rpc_url or self.rpc_url or get_rpc_url() + signer_type = signer_type or self.signer_type private_key = private_key or self.private_key + turnkey_sign_with = turnkey_sign_with or self.turnkey_sign_with recipient = recipient or self.recipient amount = amount or self.amount token_address = token_address or self.token_address @@ -81,20 +102,24 @@ async def execute( amount_float = float(amount) display_amount = amount - # Get wallet keypair with dynamic private key support - keypair_result = get_wallet_key(require_private_key=True, private_key=private_key) - if not keypair_result.keypair: - return ToolResult(error="Failed to get wallet keypair") - sender_keypair = keypair_result.keypair + # Create signer using SignerManager (supports local and Turnkey) + try: + signer = SignerManager.create_signer( + signer_type=signer_type, + private_key=private_key, + turnkey_sign_with=turnkey_sign_with, + ) + except Exception as e: + return ToolResult(error=f"Failed to create signer: {str(e)}") # Execute transfer if is_native: result = await self._transfer_sol( - rpc_url, sender_keypair, recipient, amount_float, display_amount + rpc_url, signer, recipient, amount_float, display_amount ) else: result = await self._transfer_spl_token( - rpc_url, sender_keypair, recipient, amount_float, + rpc_url, signer, recipient, amount_float, token_address, display_amount ) @@ -107,7 +132,7 @@ async def execute( async def _transfer_sol( self, rpc_url: str, - sender_keypair, + signer: SolanaSigner, recipient: str, amount: float, display_amount @@ -121,7 +146,7 @@ async def _transfer_sol( transfer_ix = system_transfer( { - "from_pubkey": sender_keypair.pubkey(), + "from_pubkey": signer.pubkey, "to_pubkey": recipient_pubkey, "lamports": lamports } @@ -132,13 +157,13 @@ async def _transfer_sol( recent_blockhash = recent_blockhash_resp.value.blockhash message = MessageV0.try_compile( - payer=sender_keypair.pubkey(), + payer=signer.pubkey, instructions=instructions, address_lookup_table_accounts=[], recent_blockhash=recent_blockhash, ) - transaction = VersionedTransaction(message, [sender_keypair]) + transaction = signer.sign_versioned_transaction(message) response = await client.send_transaction(transaction) signature = str(response.value) @@ -148,12 +173,13 @@ async def _transfer_sol( "signature": signature, "amount": display_amount, "recipient": recipient, + "signer_type": signer.signer_type, }) async def _transfer_spl_token( self, rpc_url: str, - sender_keypair, + signer: SolanaSigner, recipient: str, amount: float, token_address: str, @@ -175,7 +201,7 @@ async def _transfer_spl_token( token_amount = int(amount * (10 ** decimals)) sender_ata = Pubkey.from_string( - get_associated_token_address(token_address, str(sender_keypair.pubkey())) + get_associated_token_address(token_address, signer.get_address()) ) recipient_ata = Pubkey.from_string( get_associated_token_address(token_address, str(recipient_pubkey)) @@ -186,7 +212,7 @@ async def _transfer_spl_token( recipient_ata_info = await client.get_account_info(recipient_ata) if not recipient_ata_info.value: create_ata_ix = create_associated_token_account( - payer=sender_keypair.pubkey(), + payer=signer.pubkey, owner=recipient_pubkey, mint=mint_pubkey, ) @@ -197,7 +223,7 @@ async def _transfer_spl_token( "program_id": Pubkey.from_string(TOKEN_PROGRAM_ID), "source": sender_ata, "dest": recipient_ata, - "owner": sender_keypair.pubkey(), + "owner": signer.pubkey, "amount": token_amount, } ) @@ -207,13 +233,13 @@ async def _transfer_spl_token( recent_blockhash = recent_blockhash_resp.value.blockhash message = MessageV0.try_compile( - payer=sender_keypair.pubkey(), + payer=signer.pubkey, instructions=instructions, address_lookup_table_accounts=[], recent_blockhash=recent_blockhash, ) - transaction = VersionedTransaction(message, [sender_keypair]) + transaction = signer.sign_versioned_transaction(message) response = await client.send_transaction(transaction) signature = str(response.value) @@ -223,4 +249,5 @@ async def _transfer_spl_token( "signature": signature, "amount": display_amount, "recipient": recipient, + "signer_type": signer.signer_type, })