445 lines
17 KiB
Python
445 lines
17 KiB
Python
"""
|
|
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 |