montana/Русский/Бот/montana_db.py

671 lines
26 KiB
Python

#!/usr/bin/env python3
"""
Montana Protocol Database — Standalone SQLite Database
Fully independent, no Telegram dependencies.
Primary identifier: Montana address (mt + 40 hex chars)
Tables:
- wallets: Montana addresses and balances
- transactions: Transfer history
- presence: Time Bank presence records
"""
import sqlite3
import json
import time
import logging
import threading
import secrets
import hashlib
from pathlib import Path
from datetime import datetime, timezone
from typing import Optional, Dict, List, Any, Tuple
from contextlib import contextmanager
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("montana_db")
# Database path
DB_DIR = Path(__file__).parent / "data"
DB_PATH = DB_DIR / "montana.db"
class MontanaDB:
"""
Montana Protocol Database.
Primary identifier: Montana address (mt...)
All operations use Montana addresses, not external IDs.
"""
def __init__(self, db_path: Path = DB_PATH):
DB_DIR.mkdir(parents=True, exist_ok=True)
self.db_path = db_path
self._local = threading.local()
self._init_db()
@property
def conn(self) -> sqlite3.Connection:
"""Thread-safe connection"""
if not hasattr(self._local, 'conn') or self._local.conn is None:
self._local.conn = sqlite3.connect(
str(self.db_path),
check_same_thread=False,
timeout=30.0
)
self._local.conn.row_factory = sqlite3.Row
self._local.conn.execute("PRAGMA journal_mode=WAL")
self._local.conn.execute("PRAGMA foreign_keys=ON")
return self._local.conn
@contextmanager
def transaction(self):
"""Context manager for transactions"""
try:
yield self.conn
self.conn.commit()
except Exception as e:
self.conn.rollback()
raise e
def _init_db(self):
"""Initialize database schema"""
with self.transaction():
self.conn.executescript("""
-- Wallets: Montana addresses and keys
CREATE TABLE IF NOT EXISTS wallets (
address TEXT PRIMARY KEY,
public_key TEXT,
balance REAL DEFAULT 0.0,
alias TEXT UNIQUE,
phone TEXT UNIQUE,
created_at TEXT NOT NULL,
updated_at TEXT
);
-- Transactions: Transfer history
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tx_id TEXT UNIQUE NOT NULL,
from_address TEXT NOT NULL,
to_address TEXT NOT NULL,
amount REAL NOT NULL,
timestamp TEXT NOT NULL,
signature TEXT,
status TEXT DEFAULT 'confirmed'
);
-- Presence: Time Bank activity records
CREATE TABLE IF NOT EXISTS presence (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL,
timestamp TEXT NOT NULL,
seconds INTEGER DEFAULT 1,
coins_earned REAL DEFAULT 0.0,
signature TEXT
);
-- Sessions: Active presence sessions
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL,
started_at TEXT NOT NULL,
last_activity REAL NOT NULL,
ended_at TEXT,
is_active INTEGER DEFAULT 1,
presence_seconds INTEGER DEFAULT 0,
coins_earned REAL DEFAULT 0.0
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_transactions_from ON transactions(from_address);
CREATE INDEX IF NOT EXISTS idx_transactions_to ON transactions(to_address);
CREATE INDEX IF NOT EXISTS idx_transactions_timestamp ON transactions(timestamp);
CREATE INDEX IF NOT EXISTS idx_presence_address ON presence(address);
CREATE INDEX IF NOT EXISTS idx_sessions_address ON sessions(address);
CREATE INDEX IF NOT EXISTS idx_wallets_alias ON wallets(alias);
CREATE INDEX IF NOT EXISTS idx_wallets_phone ON wallets(phone);
""")
# ═══════════════════════════════════════════════════════════════════════════════
# WALLET OPERATIONS
# ═══════════════════════════════════════════════════════════════════════════════
def create_wallet(self, address: str, public_key: str = None) -> Dict[str, Any]:
"""Create or get existing wallet (race-condition safe)."""
if not address.startswith('mt') or len(address) != 42:
raise ValueError(f"Invalid address format: {address}")
now = datetime.utcnow().isoformat()
with self.transaction():
# INSERT OR IGNORE prevents UNIQUE constraint failures on concurrent access
self.conn.execute("""
INSERT OR IGNORE INTO wallets (address, public_key, balance, created_at)
VALUES (?, ?, 0.0, ?)
""", (address, public_key, now))
cursor = self.conn.execute(
"SELECT * FROM wallets WHERE address = ?",
(address,)
)
row = cursor.fetchone()
if row:
return dict(row)
logger.info(f"🔐 Wallet created: {address[:16]}...")
return {
"address": address,
"public_key": public_key,
"balance": 0.0,
"alias": None,
"phone": None,
"created_at": now,
"updated_at": None
}
def get_wallet(self, address: str) -> Optional[Dict[str, Any]]:
"""Get wallet by address"""
cursor = self.conn.execute(
"SELECT * FROM wallets WHERE address = ?",
(address,)
)
row = cursor.fetchone()
return dict(row) if row else None
def get_balance(self, address: str) -> float:
"""Get balance for address"""
cursor = self.conn.execute(
"SELECT balance FROM wallets WHERE address = ?",
(address,)
)
row = cursor.fetchone()
return row["balance"] if row else 0.0
def update_balance(self, address: str, delta: float) -> float:
"""Update balance by delta. Returns new balance."""
now = datetime.utcnow().isoformat()
with self.transaction():
self.create_wallet(address)
self.conn.execute("""
UPDATE wallets
SET balance = balance + ?, updated_at = ?
WHERE address = ?
""", (delta, now, address))
return self.get_balance(address)
def set_balance(self, address: str, balance: float) -> float:
"""Set absolute balance"""
now = datetime.utcnow().isoformat()
with self.transaction():
self.create_wallet(address)
self.conn.execute("""
UPDATE wallets
SET balance = ?, updated_at = ?
WHERE address = ?
""", (balance, now, address))
return balance
def set_alias(self, address: str, alias: str) -> Tuple[bool, str]:
"""Set human-readable alias for address."""
alias = alias.lower().strip()
if not alias.isalnum() or len(alias) < 3 or len(alias) > 32:
return False, "Alias must be 3-32 alphanumeric characters"
now = datetime.utcnow().isoformat()
try:
with self.transaction():
self.create_wallet(address)
self.conn.execute("""
UPDATE wallets
SET alias = ?, updated_at = ?
WHERE address = ?
""", (alias, now, address))
logger.info(f"🏷️ Alias set: {address[:16]}... → {alias}.montana")
return True, alias
except sqlite3.IntegrityError:
return False, "Alias already taken"
def set_phone(self, address: str, phone: str) -> Tuple[bool, str]:
"""Link phone number to address"""
now = datetime.utcnow().isoformat()
try:
with self.transaction():
self.create_wallet(address)
self.conn.execute("""
UPDATE wallets
SET phone = ?, updated_at = ?
WHERE address = ?
""", (phone, now, address))
logger.info(f"📱 Phone linked: {address[:16]}... → {phone}")
return True, phone
except sqlite3.IntegrityError:
return False, "Phone already linked to another address"
def resolve_address(self, identifier: str) -> Optional[str]:
"""Resolve any identifier to Montana address."""
if identifier.startswith('mt') and len(identifier) == 42:
return identifier
alias = identifier.lower().replace('.montana', '').strip()
cursor = self.conn.execute(
"SELECT address FROM wallets WHERE alias = ?",
(alias,)
)
row = cursor.fetchone()
if row:
return row["address"]
if identifier.startswith('+'):
cursor = self.conn.execute(
"SELECT address FROM wallets WHERE phone = ?",
(identifier,)
)
row = cursor.fetchone()
if row:
return row["address"]
return None
# ═══════════════════════════════════════════════════════════════════════════════
# TRANSACTIONS
# ═══════════════════════════════════════════════════════════════════════════════
def transfer(
self,
from_address: str,
to_address: str,
amount: float,
signature: str = None
) -> Dict[str, Any]:
"""Transfer Ɉ between addresses."""
if amount <= 0:
raise ValueError("Amount must be positive")
sender_balance = self.get_balance(from_address)
if sender_balance < amount:
raise ValueError(f"Insufficient funds: {sender_balance} < {amount}")
now = datetime.utcnow().isoformat()
tx_id = hashlib.sha256(
f"{from_address}{to_address}{amount}{now}".encode()
).hexdigest()[:16]
with self.transaction():
self.create_wallet(from_address)
self.create_wallet(to_address)
self.conn.execute("""
UPDATE wallets SET balance = balance - ?, updated_at = ?
WHERE address = ?
""", (amount, now, from_address))
self.conn.execute("""
UPDATE wallets SET balance = balance + ?, updated_at = ?
WHERE address = ?
""", (amount, now, to_address))
self.conn.execute("""
INSERT INTO transactions
(tx_id, from_address, to_address, amount, timestamp, signature, status)
VALUES (?, ?, ?, ?, ?, ?, 'confirmed')
""", (tx_id, from_address, to_address, amount, now, signature))
logger.info(f"💸 Transfer: {from_address[:10]}{to_address[:10]}: {amount} Ɉ")
return {
"tx_id": tx_id,
"from": from_address,
"to": to_address,
"amount": amount,
"timestamp": now,
"status": "confirmed"
}
def get_transactions(self, address: str, limit: int = 50) -> List[Dict[str, Any]]:
"""Get transactions for address"""
cursor = self.conn.execute("""
SELECT * FROM transactions
WHERE from_address = ? OR to_address = ?
ORDER BY id DESC LIMIT ?
""", (address, address, limit))
return [dict(row) for row in cursor.fetchall()]
def record_transaction(
self,
address: str,
event: str,
amount: float,
source: str = "system"
) -> Dict[str, Any]:
"""Record a system transaction (emission, reward, etc.)"""
now = datetime.utcnow().isoformat()
tx_id = hashlib.sha256(
f"{address}{event}{amount}{now}".encode()
).hexdigest()[:16]
with self.transaction():
self.create_wallet(address)
new_balance = self.update_balance(address, amount)
self.conn.execute("""
INSERT INTO transactions
(tx_id, from_address, to_address, amount, timestamp, status)
VALUES (?, 'SYSTEM', ?, ?, ?, 'confirmed')
""", (tx_id, address, amount, now))
logger.info(f"💰 {event}: {address[:10]}... +{amount} Ɉ (balance: {new_balance})")
return {
"tx_id": tx_id,
"event": event,
"address": address,
"amount": amount,
"balance": new_balance,
"timestamp": now
}
# ═══════════════════════════════════════════════════════════════════════════════
# PRESENCE / TIME BANK
# ═══════════════════════════════════════════════════════════════════════════════
def start_session(self, address: str) -> int:
"""Start presence session for address"""
now = datetime.utcnow().isoformat()
with self.transaction():
self.create_wallet(address)
self.conn.execute("""
UPDATE sessions
SET is_active = 0, ended_at = ?
WHERE address = ? AND is_active = 1
""", (now, address))
cursor = self.conn.execute("""
INSERT INTO sessions (address, started_at, last_activity, is_active)
VALUES (?, ?, ?, 1)
""", (address, now, time.time()))
session_id = cursor.lastrowid
logger.info(f"📍 Session #{session_id} started: {address[:16]}...")
return session_id
def get_active_session(self, address: str) -> Optional[Dict[str, Any]]:
"""Get active session for address"""
cursor = self.conn.execute("""
SELECT * FROM sessions
WHERE address = ? AND is_active = 1
ORDER BY id DESC LIMIT 1
""", (address,))
row = cursor.fetchone()
return dict(row) if row else None
def update_presence(self, address: str, seconds: int = 1) -> Dict[str, Any]:
"""Record presence activity"""
now = datetime.utcnow().isoformat()
with self.transaction():
session = self.get_active_session(address)
if not session:
self.start_session(address)
session = self.get_active_session(address)
self.conn.execute("""
UPDATE sessions
SET last_activity = ?, presence_seconds = presence_seconds + ?
WHERE id = ?
""", (time.time(), seconds, session["id"]))
self.conn.execute("""
INSERT INTO presence (address, timestamp, seconds)
VALUES (?, ?, ?)
""", (address, now, seconds))
return {
"address": address,
"session_id": session["id"],
"presence_seconds": session["presence_seconds"] + seconds
}
def end_session(self, address: str) -> Optional[Dict[str, Any]]:
"""End active session"""
session = self.get_active_session(address)
if not session:
return None
now = datetime.utcnow().isoformat()
with self.transaction():
self.conn.execute("""
UPDATE sessions
SET is_active = 0, ended_at = ?
WHERE id = ?
""", (now, session["id"]))
logger.info(f"📍 Session #{session['id']} ended: {address[:16]}... ({session['presence_seconds']}s)")
return {
"session_id": session["id"],
"address": address,
"presence_seconds": session["presence_seconds"],
"coins_earned": session["coins_earned"]
}
def get_total_presence(self, address: str) -> int:
"""Get total presence seconds for address"""
cursor = self.conn.execute("""
SELECT COALESCE(SUM(presence_seconds), 0) as total
FROM sessions WHERE address = ?
""", (address,))
row = cursor.fetchone()
return row["total"] if row else 0
# ═══════════════════════════════════════════════════════════════════════════════
# TIME_BANK COMPATIBILITY API
# ═══════════════════════════════════════════════════════════════════════════════
def wallet(self, address: str, addr_type: str = "unknown") -> Dict[str, Any]:
"""Create/get wallet (TIME_BANK compatibility)"""
return self.create_wallet(address)
def credit(self, address: str, amount: float, addr_type: str = "unknown") -> float:
"""Credit balance (TIME_BANK compatibility)"""
return self.update_balance(address, amount)
def balance(self, address: str) -> float:
"""Get balance (TIME_BANK compatibility — alias for get_balance)"""
return self.get_balance(address)
def send(self, from_addr: str, to_addr: str, amount: float) -> Dict[str, Any]:
"""Send coins (TIME_BANK compatibility)"""
return self.transfer(from_addr, to_addr, amount)
def tx_feed(self, limit: int = 50) -> List[Dict[str, Any]]:
"""Transaction feed (TIME_BANK compatibility)"""
cursor = self.conn.execute(
"SELECT * FROM transactions ORDER BY id DESC LIMIT ?",
(limit,)
)
return [dict(row) for row in cursor.fetchall()]
def tx_verify(self, proof: Dict) -> bool:
"""Verify transaction (TIME_BANK compatibility)"""
tx_id = proof.get("tx_id", "")
if not tx_id:
return False
cursor = self.conn.execute(
"SELECT * FROM transactions WHERE tx_id = ?",
(tx_id,)
)
return cursor.fetchone() is not None
def my_txs(self, address: str, limit: int = 50) -> List[Dict[str, Any]]:
"""User's transactions (TIME_BANK compatibility)"""
return self.get_transactions(address, limit)
def wallets(self, addr_type: str = None) -> List[Dict[str, Any]]:
"""List wallets (TIME_BANK compatibility)"""
cursor = self.conn.execute(
"SELECT * FROM wallets ORDER BY balance DESC LIMIT 1000"
)
return [dict(row) for row in cursor.fetchall()]
# ═══════════════════════════════════════════════════════════════════════════════
# STATISTICS
# ═══════════════════════════════════════════════════════════════════════════════
def get_stats(self) -> Dict[str, Any]:
"""Get database statistics"""
stats = {}
cursor = self.conn.execute("SELECT COUNT(*) as count FROM wallets")
stats["total_wallets"] = cursor.fetchone()["count"]
cursor = self.conn.execute("SELECT COALESCE(SUM(balance), 0) as total FROM wallets")
stats["total_supply"] = cursor.fetchone()["total"]
cursor = self.conn.execute("SELECT COUNT(*) as count FROM transactions")
stats["total_transactions"] = cursor.fetchone()["count"]
cursor = self.conn.execute(
"SELECT COALESCE(SUM(presence_seconds), 0) as total FROM sessions"
)
stats["total_presence_seconds"] = cursor.fetchone()["total"]
cursor = self.conn.execute(
"SELECT COUNT(*) as count FROM sessions WHERE is_active = 1"
)
stats["active_sessions"] = cursor.fetchone()["count"]
cursor = self.conn.execute("""
SELECT address, balance, alias
FROM wallets
ORDER BY balance DESC
LIMIT 10
""")
stats["top_balances"] = [dict(row) for row in cursor.fetchall()]
return stats
def get_leaderboard(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get balance leaderboard"""
cursor = self.conn.execute("""
SELECT address, balance, alias,
(SELECT COALESCE(SUM(presence_seconds), 0) FROM sessions s WHERE s.address = w.address) as presence
FROM wallets w
WHERE balance > 0
ORDER BY balance DESC
LIMIT ?
""", (limit,))
return [dict(row) for row in cursor.fetchall()]
# ═══════════════════════════════════════════════════════════════════════════════
# SINGLETON
# ═══════════════════════════════════════════════════════════════════════════════
_db_instance: Optional[MontanaDB] = None
_db_lock = threading.Lock()
def get_db() -> MontanaDB:
"""Get singleton database instance (thread-safe)"""
global _db_instance
if _db_instance is None:
with _db_lock:
if _db_instance is None:
_db_instance = MontanaDB()
return _db_instance
# ═══════════════════════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
import sys
db = MontanaDB()
if len(sys.argv) < 2:
print("""
Montana Database CLI
Usage:
python montana_db.py stats - Database statistics
python montana_db.py balance <address> - Get balance
python montana_db.py wallet <address> - Get wallet info
python montana_db.py leaderboard - Top balances
python montana_db.py tx <address> - Transaction history
""")
sys.exit(0)
cmd = sys.argv[1]
if cmd == "stats":
stats = db.get_stats()
print("\n📊 Montana Database Statistics:")
print(f" Wallets: {stats['total_wallets']}")
print(f" Total Supply: {stats['total_supply']:.2f} Ɉ")
print(f" Transactions: {stats['total_transactions']}")
print(f" Presence: {stats['total_presence_seconds']} seconds")
print(f" Active: {stats['active_sessions']} sessions")
elif cmd == "balance" and len(sys.argv) > 2:
address = sys.argv[2]
balance = db.get_balance(address)
print(f"💰 Balance {address[:16]}...: {balance} Ɉ")
elif cmd == "wallet" and len(sys.argv) > 2:
address = sys.argv[2]
wallet = db.get_wallet(address)
if wallet:
print(f"\n🔐 Wallet: {wallet['address']}")
print(f" Balance: {wallet['balance']} Ɉ")
print(f" Alias: {wallet['alias'] or '-'}")
print(f" Phone: {wallet['phone'] or '-'}")
print(f" Created: {wallet['created_at']}")
else:
print(f"Wallet not found: {address}")
elif cmd == "leaderboard":
leaders = db.get_leaderboard()
print("\n🏆 Leaderboard:")
for i, w in enumerate(leaders, 1):
name = w.get("alias") or w["address"][:16] + "..."
print(f" {i}. {name}: {w['balance']:.2f} Ɉ")
elif cmd == "tx" and len(sys.argv) > 2:
address = sys.argv[2]
txs = db.get_transactions(address, limit=10)
print(f"\n📜 Transactions for {address[:16]}...:")
for tx in txs:
direction = "" if tx["from_address"] == address else ""
other = tx["to_address"] if direction == "" else tx["from_address"]
print(f" {direction} {tx['amount']} Ɉ ({other[:10]}...) [{tx['timestamp'][:10]}]")
else:
print("Unknown command. Run without arguments for help.")