""" TON Blockchain service for wallet operations, transaction management, and smart contract interactions. Provides async operations with connection pooling, caching, and comprehensive error handling. """ import asyncio import json from datetime import datetime, timedelta from decimal import Decimal from typing import Dict, List, Optional, Any, Tuple from uuid import UUID import httpx from sqlalchemy import select, update, and_ from app.core.config import get_settings from app.core.database import get_async_session, get_cache_manager from app.core.logging import get_logger from app.core.security import decrypt_data, encrypt_data logger = get_logger(__name__) settings = get_settings() class TONService: """ Comprehensive TON blockchain service with async operations. Handles wallet management, transactions, and smart contract interactions. """ def __init__(self): self.api_endpoint = settings.TON_API_ENDPOINT self.testnet = settings.TON_TESTNET self.api_key = settings.TON_API_KEY self.timeout = 30 # HTTP client for API requests self.client = httpx.AsyncClient( timeout=self.timeout, headers={ "Authorization": f"Bearer {self.api_key}" if self.api_key else None, "Content-Type": "application/json" } ) self.cache_manager = get_cache_manager() async def close(self): """Close HTTP client and cleanup resources.""" if self.client: await self.client.aclose() async def create_wallet(self) -> Dict[str, Any]: """ Create new TON wallet with mnemonic generation. Returns: Dict: Wallet creation result with address and private key """ try: # Generate mnemonic phrase mnemonic_response = await self.client.post( f"{self.api_endpoint}/wallet/generate", json={"testnet": self.testnet} ) if mnemonic_response.status_code != 200: error_msg = f"Failed to generate wallet: {mnemonic_response.text}" await logger.aerror("Wallet generation failed", error=error_msg) return {"error": error_msg} mnemonic_data = mnemonic_response.json() # Create wallet from mnemonic wallet_response = await self.client.post( f"{self.api_endpoint}/wallet/create", json={ "mnemonic": mnemonic_data["mnemonic"], "testnet": self.testnet } ) if wallet_response.status_code != 200: error_msg = f"Failed to create wallet: {wallet_response.text}" await logger.aerror("Wallet creation failed", error=error_msg) return {"error": error_msg} wallet_data = wallet_response.json() await logger.ainfo( "Wallet created successfully", address=wallet_data.get("address"), testnet=self.testnet ) return { "address": wallet_data["address"], "private_key": wallet_data["private_key"], "mnemonic": mnemonic_data["mnemonic"], "testnet": self.testnet } except httpx.TimeoutException: error_msg = "Wallet creation timeout" await logger.aerror(error_msg) return {"error": error_msg} except Exception as e: error_msg = f"Wallet creation error: {str(e)}" await logger.aerror("Wallet creation exception", error=str(e)) return {"error": error_msg} async def get_wallet_balance(self, address: str) -> Dict[str, Any]: """ Get wallet balance with caching for performance. Args: address: TON wallet address Returns: Dict: Balance information """ try: # Check cache first cache_key = f"ton_balance:{address}" cached_balance = await self.cache_manager.get(cache_key) if cached_balance: return cached_balance # Fetch from blockchain balance_response = await self.client.get( f"{self.api_endpoint}/wallet/{address}/balance" ) if balance_response.status_code != 200: error_msg = f"Failed to get balance: {balance_response.text}" return {"error": error_msg} balance_data = balance_response.json() result = { "balance": int(balance_data.get("balance", 0)), # nanotons "last_transaction_lt": balance_data.get("last_transaction_lt"), "account_state": balance_data.get("account_state", "unknown"), "updated_at": datetime.utcnow().isoformat() } # Cache for 30 seconds await self.cache_manager.set(cache_key, result, ttl=30) return result except httpx.TimeoutException: return {"error": "Balance fetch timeout"} except Exception as e: await logger.aerror("Balance fetch error", address=address, error=str(e)) return {"error": f"Balance fetch error: {str(e)}"} async def get_wallet_transactions( self, address: str, limit: int = 20, offset: int = 0 ) -> Dict[str, Any]: """ Get wallet transaction history with pagination. Args: address: TON wallet address limit: Number of transactions to fetch offset: Pagination offset Returns: Dict: Transaction history """ try: # Check cache cache_key = f"ton_transactions:{address}:{limit}:{offset}" cached_transactions = await self.cache_manager.get(cache_key) if cached_transactions: return cached_transactions transactions_response = await self.client.get( f"{self.api_endpoint}/wallet/{address}/transactions", params={"limit": limit, "offset": offset} ) if transactions_response.status_code != 200: error_msg = f"Failed to get transactions: {transactions_response.text}" return {"error": error_msg} transactions_data = transactions_response.json() result = { "transactions": transactions_data.get("transactions", []), "total": transactions_data.get("total", 0), "limit": limit, "offset": offset, "updated_at": datetime.utcnow().isoformat() } # Cache for 1 minute await self.cache_manager.set(cache_key, result, ttl=60) return result except httpx.TimeoutException: return {"error": "Transaction fetch timeout"} except Exception as e: await logger.aerror( "Transaction fetch error", address=address, error=str(e) ) return {"error": f"Transaction fetch error: {str(e)}"} async def send_transaction( self, private_key: str, recipient_address: str, amount: int, message: str = "", **kwargs ) -> Dict[str, Any]: """ Send TON transaction with validation and monitoring. Args: private_key: Encrypted private key recipient_address: Recipient wallet address amount: Amount in nanotons message: Optional message **kwargs: Additional transaction parameters Returns: Dict: Transaction result """ try: # Validate inputs if amount <= 0: return {"error": "Amount must be positive"} if len(recipient_address) != 48: return {"error": "Invalid recipient address format"} # Decrypt private key try: decrypted_key = decrypt_data(private_key, context="wallet") if isinstance(decrypted_key, bytes): decrypted_key = decrypted_key.decode('utf-8') except Exception as e: await logger.aerror("Private key decryption failed", error=str(e)) return {"error": "Invalid private key"} # Prepare transaction transaction_data = { "private_key": decrypted_key, "recipient": recipient_address, "amount": str(amount), "message": message, "testnet": self.testnet } # Send transaction tx_response = await self.client.post( f"{self.api_endpoint}/transaction/send", json=transaction_data ) if tx_response.status_code != 200: error_msg = f"Transaction failed: {tx_response.text}" await logger.aerror( "Transaction submission failed", recipient=recipient_address, amount=amount, error=error_msg ) return {"error": error_msg} tx_data = tx_response.json() result = { "hash": tx_data["hash"], "lt": tx_data.get("lt"), "fee": tx_data.get("fee", 0), "block_hash": tx_data.get("block_hash"), "timestamp": datetime.utcnow().isoformat() } await logger.ainfo( "Transaction sent successfully", hash=result["hash"], recipient=recipient_address, amount=amount ) return result except httpx.TimeoutException: return {"error": "Transaction timeout"} except Exception as e: await logger.aerror( "Transaction send error", recipient=recipient_address, amount=amount, error=str(e) ) return {"error": f"Transaction error: {str(e)}"} async def get_transaction_status(self, tx_hash: str) -> Dict[str, Any]: """ Get transaction status and confirmation details. Args: tx_hash: Transaction hash Returns: Dict: Transaction status information """ try: # Check cache cache_key = f"ton_tx_status:{tx_hash}" cached_status = await self.cache_manager.get(cache_key) if cached_status and cached_status.get("confirmed"): return cached_status status_response = await self.client.get( f"{self.api_endpoint}/transaction/{tx_hash}/status" ) if status_response.status_code != 200: return {"error": f"Failed to get status: {status_response.text}"} status_data = status_response.json() result = { "hash": tx_hash, "confirmed": status_data.get("confirmed", False), "failed": status_data.get("failed", False), "confirmations": status_data.get("confirmations", 0), "block_hash": status_data.get("block_hash"), "block_time": status_data.get("block_time"), "fee": status_data.get("fee"), "confirmed_at": status_data.get("confirmed_at"), "updated_at": datetime.utcnow().isoformat() } # Cache confirmed/failed transactions longer cache_ttl = 3600 if result["confirmed"] or result["failed"] else 30 await self.cache_manager.set(cache_key, result, ttl=cache_ttl) return result except httpx.TimeoutException: return {"error": "Status check timeout"} except Exception as e: await logger.aerror("Status check error", tx_hash=tx_hash, error=str(e)) return {"error": f"Status check error: {str(e)}"} async def validate_address(self, address: str) -> Dict[str, Any]: """ Validate TON address format and existence. Args: address: TON address to validate Returns: Dict: Validation result """ try: # Basic format validation if len(address) != 48: return {"valid": False, "error": "Invalid address length"} # Check against blockchain validation_response = await self.client.post( f"{self.api_endpoint}/address/validate", json={"address": address} ) if validation_response.status_code != 200: return {"valid": False, "error": "Validation service error"} validation_data = validation_response.json() return { "valid": validation_data.get("valid", False), "exists": validation_data.get("exists", False), "account_type": validation_data.get("account_type"), "error": validation_data.get("error") } except Exception as e: await logger.aerror("Address validation error", address=address, error=str(e)) return {"valid": False, "error": f"Validation error: {str(e)}"} async def get_network_info(self) -> Dict[str, Any]: """ Get TON network information and statistics. Returns: Dict: Network information """ try: cache_key = "ton_network_info" cached_info = await self.cache_manager.get(cache_key) if cached_info: return cached_info network_response = await self.client.get( f"{self.api_endpoint}/network/info" ) if network_response.status_code != 200: return {"error": f"Failed to get network info: {network_response.text}"} network_data = network_response.json() result = { "network": "testnet" if self.testnet else "mainnet", "last_block": network_data.get("last_block"), "last_block_time": network_data.get("last_block_time"), "total_accounts": network_data.get("total_accounts"), "total_transactions": network_data.get("total_transactions"), "tps": network_data.get("tps"), # Transactions per second "updated_at": datetime.utcnow().isoformat() } # Cache for 5 minutes await self.cache_manager.set(cache_key, result, ttl=300) return result except Exception as e: await logger.aerror("Network info error", error=str(e)) return {"error": f"Network info error: {str(e)}"} async def estimate_transaction_fee( self, sender_address: str, recipient_address: str, amount: int, message: str = "" ) -> Dict[str, Any]: """ Estimate transaction fee before sending. Args: sender_address: Sender wallet address recipient_address: Recipient wallet address amount: Amount in nanotons message: Optional message Returns: Dict: Fee estimation """ try: fee_response = await self.client.post( f"{self.api_endpoint}/transaction/estimate-fee", json={ "sender": sender_address, "recipient": recipient_address, "amount": str(amount), "message": message } ) if fee_response.status_code != 200: return {"error": f"Fee estimation failed: {fee_response.text}"} fee_data = fee_response.json() return { "estimated_fee": fee_data.get("fee", 0), "estimated_fee_tons": str(Decimal(fee_data.get("fee", 0)) / Decimal("1000000000")), "gas_used": fee_data.get("gas_used"), "message_size": len(message.encode('utf-8')), "updated_at": datetime.utcnow().isoformat() } except Exception as e: await logger.aerror("Fee estimation error", error=str(e)) return {"error": f"Fee estimation error: {str(e)}"} async def monitor_transaction(self, tx_hash: str, max_wait_time: int = 300) -> Dict[str, Any]: """ Monitor transaction until confirmation or timeout. Args: tx_hash: Transaction hash to monitor max_wait_time: Maximum wait time in seconds Returns: Dict: Final transaction status """ start_time = datetime.utcnow() check_interval = 5 # Check every 5 seconds while (datetime.utcnow() - start_time).seconds < max_wait_time: status = await self.get_transaction_status(tx_hash) if status.get("error"): return status if status.get("confirmed") or status.get("failed"): await logger.ainfo( "Transaction monitoring completed", tx_hash=tx_hash, confirmed=status.get("confirmed"), failed=status.get("failed"), duration=(datetime.utcnow() - start_time).seconds ) return status await asyncio.sleep(check_interval) # Timeout reached await logger.awarning( "Transaction monitoring timeout", tx_hash=tx_hash, max_wait_time=max_wait_time ) return { "hash": tx_hash, "confirmed": False, "timeout": True, "error": "Monitoring timeout reached" } async def get_smart_contract_info(self, address: str) -> Dict[str, Any]: """ Get smart contract information and ABI. Args: address: Smart contract address Returns: Dict: Contract information """ try: cache_key = f"ton_contract:{address}" cached_info = await self.cache_manager.get(cache_key) if cached_info: return cached_info contract_response = await self.client.get( f"{self.api_endpoint}/contract/{address}/info" ) if contract_response.status_code != 200: return {"error": f"Failed to get contract info: {contract_response.text}"} contract_data = contract_response.json() result = { "address": address, "contract_type": contract_data.get("contract_type"), "is_verified": contract_data.get("is_verified", False), "abi": contract_data.get("abi"), "source_code": contract_data.get("source_code"), "compiler_version": contract_data.get("compiler_version"), "deployment_block": contract_data.get("deployment_block"), "updated_at": datetime.utcnow().isoformat() } # Cache for 1 hour await self.cache_manager.set(cache_key, result, ttl=3600) return result except Exception as e: await logger.aerror("Contract info error", address=address, error=str(e)) return {"error": f"Contract info error: {str(e)}"} async def call_smart_contract( self, contract_address: str, method: str, params: Dict[str, Any], private_key: Optional[str] = None ) -> Dict[str, Any]: """ Call smart contract method. Args: contract_address: Contract address method: Method name to call params: Method parameters private_key: Private key for write operations Returns: Dict: Contract call result """ try: call_data = { "contract": contract_address, "method": method, "params": params } # Add private key for write operations if private_key: try: decrypted_key = decrypt_data(private_key, context="wallet") if isinstance(decrypted_key, bytes): decrypted_key = decrypted_key.decode('utf-8') call_data["private_key"] = decrypted_key except Exception as e: return {"error": "Invalid private key"} contract_response = await self.client.post( f"{self.api_endpoint}/contract/call", json=call_data ) if contract_response.status_code != 200: return {"error": f"Contract call failed: {contract_response.text}"} call_result = contract_response.json() await logger.ainfo( "Smart contract called", contract=contract_address, method=method, success=call_result.get("success", False) ) return call_result except Exception as e: await logger.aerror( "Contract call error", contract=contract_address, method=method, error=str(e) ) return {"error": f"Contract call error: {str(e)}"} # Global TON service instance _ton_service = None async def get_ton_service() -> TONService: """Get or create global TON service instance.""" global _ton_service if _ton_service is None: _ton_service = TONService() return _ton_service async def cleanup_ton_service(): """Cleanup global TON service instance.""" global _ton_service if _ton_service: await _ton_service.close() _ton_service = None