""" Blockchain-related models for TON network integration. Handles transaction records, wallet management, and smart contract interactions. """ from datetime import datetime from decimal import Decimal from typing import Dict, List, Optional, Any from uuid import UUID import sqlalchemy as sa from sqlalchemy import Column, String, Integer, DateTime, Boolean, Text, JSON, ForeignKey, Index from sqlalchemy.orm import relationship, validates from sqlalchemy.dialects.postgresql import UUID as PostgreSQLUUID from app.core.models.base import Base, TimestampMixin, UUIDMixin class BlockchainTransaction(Base, UUIDMixin, TimestampMixin): """Model for storing blockchain transaction records.""" __tablename__ = "blockchain_transactions" # User relationship user_id = Column(PostgreSQLUUID(as_uuid=True), ForeignKey("users.id"), nullable=False) user = relationship("User", back_populates="blockchain_transactions") # Transaction details transaction_hash = Column(String(64), unique=True, nullable=False, index=True) transaction_type = Column(String(20), nullable=False) # transfer, mint, burn, stake, etc. status = Column(String(20), nullable=False, default="pending") # pending, confirmed, failed # Amount and fees amount = Column(sa.BIGINT, nullable=False, default=0) # Amount in nanotons network_fee = Column(sa.BIGINT, nullable=False, default=0) # Network fee in nanotons # Addresses sender_address = Column(String(48), nullable=True, index=True) recipient_address = Column(String(48), nullable=True, index=True) # Message and metadata message = Column(Text, nullable=True) metadata = Column(JSON, nullable=True) # Blockchain specific fields block_hash = Column(String(64), nullable=True) logical_time = Column(sa.BIGINT, nullable=True) # TON logical time confirmations = Column(Integer, nullable=False, default=0) # Timing confirmed_at = Column(DateTime, nullable=True) failed_at = Column(DateTime, nullable=True) # Smart contract interaction contract_address = Column(String(48), nullable=True) contract_method = Column(String(100), nullable=True) contract_data = Column(JSON, nullable=True) # Internal tracking retry_count = Column(Integer, nullable=False, default=0) last_retry_at = Column(DateTime, nullable=True) error_message = Column(Text, nullable=True) # Indexes for performance __table_args__ = ( Index("idx_blockchain_tx_user_status", "user_id", "status"), Index("idx_blockchain_tx_hash", "transaction_hash"), Index("idx_blockchain_tx_addresses", "sender_address", "recipient_address"), Index("idx_blockchain_tx_created", "created_at"), Index("idx_blockchain_tx_type_status", "transaction_type", "status"), ) @validates('transaction_type') def validate_transaction_type(self, key, transaction_type): """Validate transaction type.""" allowed_types = { 'transfer', 'mint', 'burn', 'stake', 'unstake', 'contract_call', 'deploy', 'withdraw', 'deposit' } if transaction_type not in allowed_types: raise ValueError(f"Invalid transaction type: {transaction_type}") return transaction_type @validates('status') def validate_status(self, key, status): """Validate transaction status.""" allowed_statuses = {'pending', 'confirmed', 'failed', 'cancelled'} if status not in allowed_statuses: raise ValueError(f"Invalid status: {status}") return status @property def amount_tons(self) -> Decimal: """Convert nanotons to TON.""" return Decimal(self.amount) / Decimal("1000000000") @property def fee_tons(self) -> Decimal: """Convert fee nanotons to TON.""" return Decimal(self.network_fee) / Decimal("1000000000") @property def is_incoming(self) -> bool: """Check if transaction is incoming to user's wallet.""" return self.transaction_type in {'transfer', 'mint', 'deposit'} and self.recipient_address @property def is_outgoing(self) -> bool: """Check if transaction is outgoing from user's wallet.""" return self.transaction_type in {'transfer', 'burn', 'withdraw'} and self.sender_address def to_dict(self) -> Dict[str, Any]: """Convert transaction to dictionary.""" return { "id": str(self.id), "hash": self.transaction_hash, "type": self.transaction_type, "status": self.status, "amount": self.amount, "amount_tons": str(self.amount_tons), "fee": self.network_fee, "fee_tons": str(self.fee_tons), "sender": self.sender_address, "recipient": self.recipient_address, "message": self.message, "block_hash": self.block_hash, "confirmations": self.confirmations, "created_at": self.created_at.isoformat() if self.created_at else None, "confirmed_at": self.confirmed_at.isoformat() if self.confirmed_at else None, "is_incoming": self.is_incoming, "is_outgoing": self.is_outgoing } class SmartContract(Base, UUIDMixin, TimestampMixin): """Model for smart contract management.""" __tablename__ = "smart_contracts" # Contract details address = Column(String(48), unique=True, nullable=False, index=True) name = Column(String(100), nullable=False) description = Column(Text, nullable=True) contract_type = Column(String(50), nullable=False) # nft, token, defi, etc. # Contract metadata abi = Column(JSON, nullable=True) # Contract ABI if available source_code = Column(Text, nullable=True) compiler_version = Column(String(20), nullable=True) # Deployment info deployer_address = Column(String(48), nullable=True) deployment_tx_hash = Column(String(64), nullable=True) deployment_block = Column(sa.BIGINT, nullable=True) # Status and verification is_verified = Column(Boolean, nullable=False, default=False) is_active = Column(Boolean, nullable=False, default=True) verification_date = Column(DateTime, nullable=True) # Usage statistics interaction_count = Column(Integer, nullable=False, default=0) last_interaction_at = Column(DateTime, nullable=True) # Relationships transactions = relationship( "BlockchainTransaction", foreign_keys="BlockchainTransaction.contract_address", primaryjoin="SmartContract.address == BlockchainTransaction.contract_address", back_populates=None ) __table_args__ = ( Index("idx_smart_contract_address", "address"), Index("idx_smart_contract_type", "contract_type"), Index("idx_smart_contract_active", "is_active"), ) @validates('contract_type') def validate_contract_type(self, key, contract_type): """Validate contract type.""" allowed_types = { 'nft', 'token', 'defi', 'game', 'dao', 'bridge', 'oracle', 'multisig', 'custom' } if contract_type not in allowed_types: raise ValueError(f"Invalid contract type: {contract_type}") return contract_type class TokenBalance(Base, UUIDMixin, TimestampMixin): """Model for tracking user token balances.""" __tablename__ = "token_balances" # User relationship user_id = Column(PostgreSQLUUID(as_uuid=True), ForeignKey("users.id"), nullable=False) user = relationship("User", back_populates="token_balances") # Token details token_address = Column(String(48), nullable=False, index=True) token_name = Column(String(100), nullable=True) token_symbol = Column(String(10), nullable=True) token_decimals = Column(Integer, nullable=False, default=9) # Balance information balance = Column(sa.BIGINT, nullable=False, default=0) # Raw balance locked_balance = Column(sa.BIGINT, nullable=False, default=0) # Locked in contracts # Metadata last_update_block = Column(sa.BIGINT, nullable=True) last_update_tx = Column(String(64), nullable=True) # Unique constraint __table_args__ = ( sa.UniqueConstraint("user_id", "token_address", name="uq_user_token"), Index("idx_token_balance_user", "user_id"), Index("idx_token_balance_token", "token_address"), Index("idx_token_balance_updated", "updated_at"), ) @property def available_balance(self) -> int: """Get available (unlocked) balance.""" return max(0, self.balance - self.locked_balance) @property def formatted_balance(self) -> Decimal: """Get balance formatted with decimals.""" return Decimal(self.balance) / Decimal(10 ** self.token_decimals) @property def formatted_available_balance(self) -> Decimal: """Get available balance formatted with decimals.""" return Decimal(self.available_balance) / Decimal(10 ** self.token_decimals) class StakingPosition(Base, UUIDMixin, TimestampMixin): """Model for staking positions.""" __tablename__ = "staking_positions" # User relationship user_id = Column(PostgreSQLUUID(as_uuid=True), ForeignKey("users.id"), nullable=False) user = relationship("User", back_populates="staking_positions") # Staking details validator_address = Column(String(48), nullable=False, index=True) pool_address = Column(String(48), nullable=True) # Amount and timing staked_amount = Column(sa.BIGINT, nullable=False) # Amount in nanotons stake_tx_hash = Column(String(64), nullable=False) stake_block = Column(sa.BIGINT, nullable=True) # Status status = Column(String(20), nullable=False, default="active") # active, unstaking, withdrawn unstake_tx_hash = Column(String(64), nullable=True) unstake_requested_at = Column(DateTime, nullable=True) withdrawn_at = Column(DateTime, nullable=True) # Rewards rewards_earned = Column(sa.BIGINT, nullable=False, default=0) last_reward_claim = Column(DateTime, nullable=True) last_reward_tx = Column(String(64), nullable=True) # Lock period lock_period_days = Column(Integer, nullable=False, default=0) unlock_date = Column(DateTime, nullable=True) __table_args__ = ( Index("idx_staking_user_status", "user_id", "status"), Index("idx_staking_validator", "validator_address"), Index("idx_staking_unlock", "unlock_date"), ) @validates('status') def validate_status(self, key, status): """Validate staking status.""" allowed_statuses = {'active', 'unstaking', 'withdrawn', 'slashed'} if status not in allowed_statuses: raise ValueError(f"Invalid staking status: {status}") return status @property def staked_tons(self) -> Decimal: """Get staked amount in TON.""" return Decimal(self.staked_amount) / Decimal("1000000000") @property def rewards_tons(self) -> Decimal: """Get rewards amount in TON.""" return Decimal(self.rewards_earned) / Decimal("1000000000") @property def is_locked(self) -> bool: """Check if staking position is still locked.""" if not self.unlock_date: return False return datetime.utcnow() < self.unlock_date class NFTCollection(Base, UUIDMixin, TimestampMixin): """Model for NFT collections.""" __tablename__ = "nft_collections" # Collection details contract_address = Column(String(48), unique=True, nullable=False, index=True) name = Column(String(100), nullable=False) description = Column(Text, nullable=True) symbol = Column(String(10), nullable=True) # Creator and metadata creator_address = Column(String(48), nullable=False) metadata_uri = Column(String(500), nullable=True) base_uri = Column(String(500), nullable=True) # Collection stats total_supply = Column(Integer, nullable=False, default=0) max_supply = Column(Integer, nullable=True) floor_price = Column(sa.BIGINT, nullable=True) # In nanotons # Status is_verified = Column(Boolean, nullable=False, default=False) is_active = Column(Boolean, nullable=False, default=True) # Relationships nfts = relationship("NFTToken", back_populates="collection") __table_args__ = ( Index("idx_nft_collection_address", "contract_address"), Index("idx_nft_collection_creator", "creator_address"), Index("idx_nft_collection_verified", "is_verified"), ) class NFTToken(Base, UUIDMixin, TimestampMixin): """Model for individual NFT tokens.""" __tablename__ = "nft_tokens" # Token identification collection_id = Column(PostgreSQLUUID(as_uuid=True), ForeignKey("nft_collections.id"), nullable=False) collection = relationship("NFTCollection", back_populates="nfts") token_id = Column(String(100), nullable=False) # Token ID within collection token_address = Column(String(48), unique=True, nullable=False, index=True) # Ownership owner_address = Column(String(48), nullable=False, index=True) # Metadata name = Column(String(200), nullable=True) description = Column(Text, nullable=True) image_uri = Column(String(500), nullable=True) metadata_uri = Column(String(500), nullable=True) attributes = Column(JSON, nullable=True) # Trading last_sale_price = Column(sa.BIGINT, nullable=True) # In nanotons last_sale_tx = Column(String(64), nullable=True) last_sale_date = Column(DateTime, nullable=True) # Status is_burned = Column(Boolean, nullable=False, default=False) burned_at = Column(DateTime, nullable=True) __table_args__ = ( sa.UniqueConstraint("collection_id", "token_id", name="uq_collection_token"), Index("idx_nft_token_address", "token_address"), Index("idx_nft_token_owner", "owner_address"), Index("idx_nft_token_collection", "collection_id"), ) @property def last_sale_tons(self) -> Optional[Decimal]: """Get last sale price in TON.""" if self.last_sale_price is None: return None return Decimal(self.last_sale_price) / Decimal("1000000000") class DeFiPosition(Base, UUIDMixin, TimestampMixin): """Model for DeFi protocol positions.""" __tablename__ = "defi_positions" # User relationship user_id = Column(PostgreSQLUUID(as_uuid=True), ForeignKey("users.id"), nullable=False) user = relationship("User", back_populates="defi_positions") # Protocol details protocol_name = Column(String(100), nullable=False) protocol_address = Column(String(48), nullable=False) position_type = Column(String(50), nullable=False) # liquidity, lending, borrowing, etc. # Position details token_a_address = Column(String(48), nullable=True) token_a_amount = Column(sa.BIGINT, nullable=False, default=0) token_b_address = Column(String(48), nullable=True) token_b_amount = Column(sa.BIGINT, nullable=False, default=0) # Value tracking initial_value = Column(sa.BIGINT, nullable=False, default=0) # In nanotons current_value = Column(sa.BIGINT, nullable=False, default=0) last_value_update = Column(DateTime, nullable=True) # Rewards and fees rewards_earned = Column(sa.BIGINT, nullable=False, default=0) fees_paid = Column(sa.BIGINT, nullable=False, default=0) # Status status = Column(String(20), nullable=False, default="active") # active, closed, liquidated opened_tx = Column(String(64), nullable=False) closed_tx = Column(String(64), nullable=True) closed_at = Column(DateTime, nullable=True) __table_args__ = ( Index("idx_defi_user_protocol", "user_id", "protocol_name"), Index("idx_defi_position_type", "position_type"), Index("idx_defi_status", "status"), ) @validates('position_type') def validate_position_type(self, key, position_type): """Validate position type.""" allowed_types = { 'liquidity', 'lending', 'borrowing', 'farming', 'staking', 'options', 'futures', 'insurance' } if position_type not in allowed_types: raise ValueError(f"Invalid position type: {position_type}") return position_type @validates('status') def validate_status(self, key, status): """Validate position status.""" allowed_statuses = {'active', 'closed', 'liquidated', 'expired'} if status not in allowed_statuses: raise ValueError(f"Invalid position status: {status}") return status @property def current_value_tons(self) -> Decimal: """Get current value in TON.""" return Decimal(self.current_value) / Decimal("1000000000") @property def pnl_tons(self) -> Decimal: """Get profit/loss in TON.""" return Decimal(self.current_value - self.initial_value) / Decimal("1000000000") @property def pnl_percentage(self) -> Decimal: """Get profit/loss percentage.""" if self.initial_value == 0: return Decimal("0") return (Decimal(self.current_value - self.initial_value) / Decimal(self.initial_value)) * 100