659 lines
23 KiB
Python
659 lines
23 KiB
Python
"""
|
|
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
|