805 lines
32 KiB
Python
805 lines
32 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Montana Protocol — Ascending Auction System
|
||
Аукцион нарастания для всех сервисов Montana.
|
||
|
||
Модель ценообразования:
|
||
- 1-й сервис: 1 Ɉ
|
||
- 2-й сервис: 2 Ɉ
|
||
- 3-й сервис: 3 Ɉ
|
||
- N-й сервис: N Ɉ (шаг 1 Ɉ)
|
||
|
||
Применяется к:
|
||
- Domains (alice@efir.org)
|
||
- VPN subscriptions
|
||
- Всем будущим сервисам
|
||
"""
|
||
|
||
import json
|
||
import threading
|
||
import fcntl
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Dict, Optional, List
|
||
from datetime import datetime
|
||
import logging
|
||
|
||
log = logging.getLogger("montana_auction")
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# AUCTION SERVICE TYPES
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
class ServiceType:
|
||
"""Типы сервисов Montana Protocol"""
|
||
# Базовые сервисы
|
||
DOMAIN = "domain" # alice@efir.org
|
||
VPN = "vpn" # WireGuard VPN subscription
|
||
STORAGE = "storage" # Future: distributed storage
|
||
COMPUTE = "compute" # Future: compute credits
|
||
|
||
# Телефония и коммуникации
|
||
PHONE_NUMBER = "phone_number" # Виртуальный номер +montana-123456
|
||
AUDIO_SECOND = "audio_second" # Цена 1 секунды аудио звонка
|
||
VIDEO_SECOND = "video_second" # Цена 1 секунды видео звонка
|
||
|
||
@classmethod
|
||
def all(cls) -> List[str]:
|
||
return [
|
||
cls.DOMAIN,
|
||
cls.VPN,
|
||
cls.STORAGE,
|
||
cls.COMPUTE,
|
||
cls.PHONE_NUMBER,
|
||
cls.AUDIO_SECOND,
|
||
cls.VIDEO_SECOND
|
||
]
|
||
|
||
@classmethod
|
||
def communication_services(cls) -> List[str]:
|
||
"""Коммуникационные сервисы (телефония, звонки)"""
|
||
return [cls.PHONE_NUMBER, cls.AUDIO_SECOND, cls.VIDEO_SECOND]
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# AUCTION COUNTER REGISTRY
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
class AuctionRegistry:
|
||
"""
|
||
Реестр счетчиков аукциона для каждого типа сервиса.
|
||
Консенсус между 3 узлами Montana Network.
|
||
"""
|
||
|
||
def __init__(self, data_dir: Path):
|
||
self.data_dir = data_dir
|
||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||
self.auction_file = self.data_dir / "auction_counters.json"
|
||
self.purchases_file = self.data_dir / "auction_purchases.json"
|
||
self._lock = threading.Lock()
|
||
self._ensure_files()
|
||
|
||
def _ensure_files(self):
|
||
"""Создать файлы аукциона если не существуют"""
|
||
if not self.auction_file.exists():
|
||
self._save_counters({service: 0 for service in ServiceType.all()})
|
||
if not self.purchases_file.exists():
|
||
self._save_purchases([])
|
||
|
||
def _load_counters(self) -> Dict[str, int]:
|
||
"""Загрузить счетчики аукциона (thread-safe)"""
|
||
with open(self.auction_file, 'r') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
|
||
try:
|
||
data = json.load(f)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
return data
|
||
|
||
def _save_counters(self, counters: Dict[str, int]):
|
||
"""Сохранить счетчики аукциона (thread-safe)"""
|
||
with open(self.auction_file, 'w') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||
try:
|
||
json.dump(counters, f, indent=2)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
|
||
def _load_purchases(self) -> List[Dict]:
|
||
"""Загрузить историю покупок"""
|
||
with open(self.purchases_file, 'r') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
|
||
try:
|
||
data = json.load(f)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
return data
|
||
|
||
def _save_purchases(self, purchases: List[Dict]):
|
||
"""Сохранить историю покупок"""
|
||
with open(self.purchases_file, 'w') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||
try:
|
||
json.dump(purchases, f, indent=2)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
|
||
def get_current_price(self, service_type: str) -> int:
|
||
"""
|
||
Получить текущую цену для следующей покупки.
|
||
|
||
Args:
|
||
service_type: Тип сервиса (domain, vpn, etc.)
|
||
|
||
Returns:
|
||
Цена в Ɉ (монетах времени)
|
||
"""
|
||
if service_type not in ServiceType.all():
|
||
raise ValueError(f"Unknown service type: {service_type}")
|
||
|
||
with self._lock:
|
||
counters = self._load_counters()
|
||
count = counters.get(service_type, 0)
|
||
# Следующая цена = текущий счетчик + 1
|
||
return count + 1
|
||
|
||
def get_total_sold(self, service_type: str) -> int:
|
||
"""
|
||
Получить количество проданных сервисов.
|
||
|
||
Args:
|
||
service_type: Тип сервиса
|
||
|
||
Returns:
|
||
Количество проданных сервисов
|
||
"""
|
||
if service_type not in ServiceType.all():
|
||
raise ValueError(f"Unknown service type: {service_type}")
|
||
|
||
with self._lock:
|
||
counters = self._load_counters()
|
||
return counters.get(service_type, 0)
|
||
|
||
def purchase(
|
||
self,
|
||
service_type: str,
|
||
buyer_address: str,
|
||
service_id: str,
|
||
amount_paid: int
|
||
) -> Dict:
|
||
"""
|
||
Зарегистрировать покупку сервиса.
|
||
|
||
Args:
|
||
service_type: Тип сервиса (domain, vpn, etc.)
|
||
buyer_address: Montana адрес покупателя
|
||
service_id: Идентификатор сервиса (например, "alice" для alice@efir.org)
|
||
amount_paid: Сумма оплаченная в Ɉ
|
||
|
||
Returns:
|
||
Dict с результатом покупки
|
||
|
||
Raises:
|
||
ValueError: Если цена не совпадает или недостаточно монет
|
||
"""
|
||
if service_type not in ServiceType.all():
|
||
raise ValueError(f"Unknown service type: {service_type}")
|
||
|
||
with self._lock:
|
||
counters = self._load_counters()
|
||
current_count = counters.get(service_type, 0)
|
||
expected_price = current_count + 1
|
||
|
||
if amount_paid < expected_price:
|
||
raise ValueError(
|
||
f"Insufficient payment: {amount_paid} Ɉ, expected {expected_price} Ɉ"
|
||
)
|
||
|
||
# Инкремент счетчика
|
||
counters[service_type] = current_count + 1
|
||
self._save_counters(counters)
|
||
|
||
# Записать в историю покупок
|
||
purchases = self._load_purchases()
|
||
purchase = {
|
||
"service_type": service_type,
|
||
"service_id": service_id,
|
||
"buyer_address": buyer_address,
|
||
"price_paid": amount_paid,
|
||
"purchase_number": current_count + 1,
|
||
"timestamp": datetime.utcnow().isoformat() + "Z"
|
||
}
|
||
purchases.append(purchase)
|
||
self._save_purchases(purchases)
|
||
|
||
log.info(
|
||
f"Auction purchase: {service_type} #{current_count + 1} "
|
||
f"'{service_id}' by {buyer_address[:10]}... for {amount_paid} Ɉ"
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"service_type": service_type,
|
||
"service_id": service_id,
|
||
"price_paid": amount_paid,
|
||
"purchase_number": current_count + 1,
|
||
"next_price": current_count + 2
|
||
}
|
||
|
||
def get_purchase_history(
|
||
self,
|
||
service_type: Optional[str] = None,
|
||
limit: int = 100
|
||
) -> List[Dict]:
|
||
"""
|
||
Получить историю покупок.
|
||
|
||
Args:
|
||
service_type: Фильтр по типу сервиса (None = все)
|
||
limit: Максимум записей
|
||
|
||
Returns:
|
||
Список покупок (последние первыми)
|
||
"""
|
||
with self._lock:
|
||
purchases = self._load_purchases()
|
||
|
||
if service_type:
|
||
purchases = [p for p in purchases if p.get("service_type") == service_type]
|
||
|
||
# Последние первыми
|
||
purchases.reverse()
|
||
return purchases[:limit]
|
||
|
||
def get_stats(self) -> Dict:
|
||
"""
|
||
Получить статистику аукциона по всем сервисам.
|
||
|
||
Returns:
|
||
Dict со статистикой
|
||
"""
|
||
with self._lock:
|
||
counters = self._load_counters()
|
||
purchases = self._load_purchases()
|
||
|
||
total_revenue = sum(p.get("price_paid", 0) for p in purchases)
|
||
|
||
stats = {
|
||
"total_services_sold": sum(counters.values()),
|
||
"total_revenue": total_revenue,
|
||
"services": {}
|
||
}
|
||
|
||
for service_type in ServiceType.all():
|
||
count = counters.get(service_type, 0)
|
||
next_price = count + 1
|
||
service_purchases = [p for p in purchases if p.get("service_type") == service_type]
|
||
revenue = sum(p.get("price_paid", 0) for p in service_purchases)
|
||
|
||
stats["services"][service_type] = {
|
||
"total_sold": count,
|
||
"next_price": next_price,
|
||
"revenue": revenue,
|
||
"latest_purchase": service_purchases[-1] if service_purchases else None
|
||
}
|
||
|
||
return stats
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# GLOBAL INSTANCE
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
_auction_registry: Optional[AuctionRegistry] = None
|
||
|
||
def get_auction_registry(data_dir: Path = None) -> AuctionRegistry:
|
||
"""Получить глобальный экземпляр AuctionRegistry (singleton)"""
|
||
global _auction_registry
|
||
if _auction_registry is None:
|
||
if data_dir is None:
|
||
data_dir = Path(__file__).parent / "data"
|
||
_auction_registry = AuctionRegistry(data_dir)
|
||
return _auction_registry
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# DOMAIN SERVICE (Montana Name Service)
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
class DomainService:
|
||
"""
|
||
Montana Name Service (MNS) — доменный слой поверх Montana Protocol.
|
||
|
||
Домены вида: alice@efir.org → mt1234...5678
|
||
"""
|
||
|
||
def __init__(self, data_dir: Path):
|
||
self.data_dir = data_dir
|
||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||
self.domains_file = self.data_dir / "domains.json"
|
||
self._lock = threading.Lock()
|
||
self._ensure_file()
|
||
|
||
def _ensure_file(self):
|
||
"""Создать файл доменов если не существует"""
|
||
if not self.domains_file.exists():
|
||
self._save_domains({})
|
||
|
||
def _load_domains(self) -> Dict[str, Dict]:
|
||
"""Загрузить реестр доменов"""
|
||
with open(self.domains_file, 'r') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
|
||
try:
|
||
data = json.load(f)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
return data
|
||
|
||
def _save_domains(self, domains: Dict[str, Dict]):
|
||
"""Сохранить реестр доменов"""
|
||
with open(self.domains_file, 'w') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||
try:
|
||
json.dump(domains, f, indent=2)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
|
||
def is_available(self, domain: str) -> bool:
|
||
"""Проверить доступность домена"""
|
||
with self._lock:
|
||
domains = self._load_domains()
|
||
return domain.lower() not in domains
|
||
|
||
def register(
|
||
self,
|
||
domain: str,
|
||
owner_address: str,
|
||
price_paid: int
|
||
) -> Dict:
|
||
"""
|
||
Зарегистрировать домен через аукцион.
|
||
|
||
Args:
|
||
domain: Имя домена (например, "alice")
|
||
owner_address: Montana адрес владельца
|
||
price_paid: Цена оплаченная в Ɉ
|
||
|
||
Returns:
|
||
Dict с результатом регистрации
|
||
"""
|
||
domain = domain.lower()
|
||
|
||
# Валидация домена
|
||
if not self._validate_domain(domain):
|
||
raise ValueError(f"Invalid domain name: {domain}")
|
||
|
||
with self._lock:
|
||
domains = self._load_domains()
|
||
|
||
if domain in domains:
|
||
raise ValueError(f"Domain already registered: {domain}@efir.org")
|
||
|
||
# Регистрация через аукцион
|
||
auction = get_auction_registry(self.data_dir.parent / "data")
|
||
result = auction.purchase(
|
||
service_type=ServiceType.DOMAIN,
|
||
buyer_address=owner_address,
|
||
service_id=domain,
|
||
amount_paid=price_paid
|
||
)
|
||
|
||
# Сохранить в реестре доменов
|
||
domains[domain] = {
|
||
"owner": owner_address,
|
||
"registered": datetime.utcnow().isoformat() + "Z",
|
||
"purchase_number": result["purchase_number"],
|
||
"price_paid": price_paid
|
||
}
|
||
self._save_domains(domains)
|
||
|
||
log.info(f"Domain registered: {domain}@efir.org → {owner_address[:10]}...")
|
||
|
||
return {
|
||
"success": True,
|
||
"domain": f"{domain}@efir.org",
|
||
"owner": owner_address,
|
||
"price_paid": price_paid,
|
||
"purchase_number": result["purchase_number"]
|
||
}
|
||
|
||
def lookup(self, domain: str) -> Optional[Dict]:
|
||
"""
|
||
Найти владельца домена.
|
||
|
||
Args:
|
||
domain: Имя домена (alice или alice@efir.org)
|
||
|
||
Returns:
|
||
Dict с информацией о домене или None
|
||
"""
|
||
# Убрать @efir.org если есть
|
||
domain = domain.lower().replace("@efir.org", "")
|
||
|
||
with self._lock:
|
||
domains = self._load_domains()
|
||
if domain in domains:
|
||
info = domains[domain].copy()
|
||
info["domain"] = f"{domain}@efir.org"
|
||
return info
|
||
return None
|
||
|
||
def _validate_domain(self, domain: str) -> bool:
|
||
"""
|
||
Валидация имени домена.
|
||
|
||
Правила:
|
||
- 3-32 символа
|
||
- Только a-z, 0-9, дефис
|
||
- Не начинается и не заканчивается дефисом
|
||
"""
|
||
if not domain:
|
||
return False
|
||
if len(domain) < 3 or len(domain) > 32:
|
||
return False
|
||
if not domain[0].isalnum() or not domain[-1].isalnum():
|
||
return False
|
||
if not all(c.isalnum() or c == '-' for c in domain):
|
||
return False
|
||
return True
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# GLOBAL DOMAIN SERVICE
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
_domain_service: Optional[DomainService] = None
|
||
|
||
def get_domain_service(data_dir: Path = None) -> DomainService:
|
||
"""Получить глобальный экземпляр DomainService (singleton)"""
|
||
global _domain_service
|
||
if _domain_service is None:
|
||
if data_dir is None:
|
||
data_dir = Path(__file__).parent / "data"
|
||
_domain_service = DomainService(data_dir)
|
||
return _domain_service
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# PHONE SERVICE — Virtual Phone Numbers
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
class PhoneService:
|
||
"""
|
||
Montana Phone Service — виртуальные телефонные номера для аудио/видео звонков.
|
||
|
||
Формат номера: +montana-NNNNNN (где N = sequential number)
|
||
Пример: +montana-000001, +montana-000042
|
||
|
||
Возможности:
|
||
- P2P аудио звонки (WebRTC)
|
||
- P2P видео звонки (WebRTC)
|
||
- Оплата за секунды через аукцион
|
||
"""
|
||
|
||
def __init__(self, data_dir: Path):
|
||
self.data_dir = data_dir
|
||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||
self.phone_file = self.data_dir / "phone_numbers.json"
|
||
self._lock = threading.Lock()
|
||
self._ensure_file()
|
||
|
||
def _ensure_file(self):
|
||
"""Создать файл номеров если не существует"""
|
||
if not self.phone_file.exists():
|
||
self._save_numbers({})
|
||
|
||
def _load_numbers(self) -> Dict[str, Dict]:
|
||
"""Загрузить реестр номеров"""
|
||
with open(self.phone_file, 'r') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
|
||
try:
|
||
data = json.load(f)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
return data
|
||
|
||
def _save_numbers(self, numbers: Dict[str, Dict]):
|
||
"""Сохранить реестр номеров"""
|
||
with open(self.phone_file, 'w') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||
try:
|
||
json.dump(numbers, f, indent=2)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
|
||
def format_number(self, number: int) -> str:
|
||
"""Форматировать номер: 42 → +montana-000042"""
|
||
return f"+montana-{number:06d}"
|
||
|
||
def is_available(self, number: int) -> bool:
|
||
"""Проверить доступность номера"""
|
||
with self._lock:
|
||
numbers = self._load_numbers()
|
||
key = self.format_number(number)
|
||
return key not in numbers
|
||
|
||
def register(
|
||
self,
|
||
number: int,
|
||
owner_address: str,
|
||
price_paid: int
|
||
) -> Dict:
|
||
"""
|
||
Зарегистрировать виртуальный номер через аукцион.
|
||
|
||
Args:
|
||
number: Номер (например, 42 для +montana-000042)
|
||
owner_address: Montana адрес владельца
|
||
price_paid: Цена оплаченная в Ɉ
|
||
|
||
Returns:
|
||
Dict с результатом регистрации
|
||
"""
|
||
formatted = self.format_number(number)
|
||
|
||
with self._lock:
|
||
numbers = self._load_numbers()
|
||
|
||
if formatted in numbers:
|
||
raise ValueError(f"Phone number already registered: {formatted}")
|
||
|
||
# Регистрация через аукцион
|
||
auction = get_auction_registry(self.data_dir.parent / "data")
|
||
result = auction.purchase(
|
||
service_type=ServiceType.PHONE_NUMBER,
|
||
buyer_address=owner_address,
|
||
service_id=formatted,
|
||
amount_paid=price_paid
|
||
)
|
||
|
||
# Сохранить в реестре номеров
|
||
numbers[formatted] = {
|
||
"owner": owner_address,
|
||
"number": number,
|
||
"registered": datetime.utcnow().isoformat() + "Z",
|
||
"purchase_number": result["purchase_number"],
|
||
"price_paid": price_paid,
|
||
"audio_seconds_used": 0,
|
||
"video_seconds_used": 0
|
||
}
|
||
self._save_numbers(numbers)
|
||
|
||
log.info(f"Phone number registered: {formatted} → {owner_address[:10]}...")
|
||
|
||
return {
|
||
"success": True,
|
||
"phone_number": formatted,
|
||
"owner": owner_address,
|
||
"price_paid": price_paid,
|
||
"purchase_number": result["purchase_number"]
|
||
}
|
||
|
||
def lookup(self, number: int) -> Optional[Dict]:
|
||
"""
|
||
Найти владельца номера.
|
||
|
||
Args:
|
||
number: Номер (42 или "+montana-000042")
|
||
|
||
Returns:
|
||
Dict с информацией о номере или None
|
||
"""
|
||
# Поддержка обоих форматов
|
||
if isinstance(number, str) and number.startswith("+montana-"):
|
||
formatted = number
|
||
else:
|
||
formatted = self.format_number(int(number))
|
||
|
||
with self._lock:
|
||
numbers = self._load_numbers()
|
||
if formatted in numbers:
|
||
return numbers[formatted].copy()
|
||
return None
|
||
|
||
def record_call(
|
||
self,
|
||
phone_number: str,
|
||
call_type: str,
|
||
duration_seconds: int,
|
||
price_per_second: int
|
||
) -> Dict:
|
||
"""
|
||
Записать использование звонка.
|
||
|
||
Args:
|
||
phone_number: Номер телефона
|
||
call_type: "audio" или "video"
|
||
duration_seconds: Длительность в секундах
|
||
price_per_second: Цена за секунду в Ɉ
|
||
|
||
Returns:
|
||
Dict с информацией о звонке
|
||
"""
|
||
total_cost = duration_seconds * price_per_second
|
||
|
||
with self._lock:
|
||
numbers = self._load_numbers()
|
||
|
||
if phone_number not in numbers:
|
||
raise ValueError(f"Phone number not found: {phone_number}")
|
||
|
||
if call_type == "audio":
|
||
numbers[phone_number]["audio_seconds_used"] += duration_seconds
|
||
elif call_type == "video":
|
||
numbers[phone_number]["video_seconds_used"] += duration_seconds
|
||
else:
|
||
raise ValueError(f"Invalid call type: {call_type}")
|
||
|
||
self._save_numbers(numbers)
|
||
|
||
log.info(
|
||
f"Call recorded: {phone_number} {call_type} {duration_seconds}s "
|
||
f"({total_cost} Ɉ)"
|
||
)
|
||
|
||
return {
|
||
"phone_number": phone_number,
|
||
"call_type": call_type,
|
||
"duration_seconds": duration_seconds,
|
||
"price_per_second": price_per_second,
|
||
"total_cost": total_cost
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# GLOBAL PHONE SERVICE
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
_phone_service: Optional[PhoneService] = None
|
||
|
||
def get_phone_service(data_dir: Path = None) -> PhoneService:
|
||
"""Получить глобальный экземпляр PhoneService (singleton)"""
|
||
global _phone_service
|
||
if _phone_service is None:
|
||
if data_dir is None:
|
||
data_dir = Path(__file__).parent / "data"
|
||
_phone_service = PhoneService(data_dir)
|
||
return _phone_service
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# CALL PRICING — Audio & Video Second Pricing
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
class CallPricingService:
|
||
"""
|
||
Аукционная модель цены за секунду аудио/видео звонков.
|
||
|
||
Принцип: N-я секунда всех звонков стоит N Ɉ
|
||
- 1-я секунда аудио в истории Montana = 1 Ɉ
|
||
- 2-я секунда аудио = 2 Ɉ
|
||
- N-я секунда аудио = N Ɉ
|
||
|
||
Аналогично для видео (отдельный счетчик).
|
||
"""
|
||
|
||
def __init__(self, data_dir: Path):
|
||
self.data_dir = data_dir
|
||
self.auction = get_auction_registry(data_dir)
|
||
|
||
def get_current_audio_price(self) -> int:
|
||
"""Получить текущую цену за 1 секунду аудио"""
|
||
return self.auction.get_current_price(ServiceType.AUDIO_SECOND)
|
||
|
||
def get_current_video_price(self) -> int:
|
||
"""Получить текущую цену за 1 секунду видео"""
|
||
return self.auction.get_current_price(ServiceType.VIDEO_SECOND)
|
||
|
||
def calculate_audio_call_cost(self, duration_seconds: int) -> int:
|
||
"""
|
||
Рассчитать стоимость аудио звонка.
|
||
|
||
Args:
|
||
duration_seconds: Длительность звонка в секундах
|
||
|
||
Returns:
|
||
Общая стоимость в Ɉ
|
||
"""
|
||
base_price = self.get_current_audio_price()
|
||
# Формула: сумма арифметической прогрессии
|
||
# cost = base_price + (base_price+1) + ... + (base_price+duration-1)
|
||
# = duration * base_price + (0+1+2+...+(duration-1))
|
||
# = duration * base_price + duration*(duration-1)/2
|
||
cost = duration_seconds * base_price + (duration_seconds * (duration_seconds - 1)) // 2
|
||
return cost
|
||
|
||
def calculate_video_call_cost(self, duration_seconds: int) -> int:
|
||
"""
|
||
Рассчитать стоимость видео звонка.
|
||
|
||
Args:
|
||
duration_seconds: Длительность звонка в секундах
|
||
|
||
Returns:
|
||
Общая стоимость в Ɉ
|
||
"""
|
||
base_price = self.get_current_video_price()
|
||
cost = duration_seconds * base_price + (duration_seconds * (duration_seconds - 1)) // 2
|
||
return cost
|
||
|
||
def register_audio_seconds(self, seconds: int, caller_address: str) -> Dict:
|
||
"""
|
||
Зарегистрировать использование N секунд аудио.
|
||
|
||
Args:
|
||
seconds: Количество секунд
|
||
caller_address: Montana адрес звонящего
|
||
|
||
Returns:
|
||
Dict с результатом регистрации
|
||
"""
|
||
total_cost = self.calculate_audio_call_cost(seconds)
|
||
|
||
# Зарегистрировать каждую секунду в аукционе
|
||
for i in range(seconds):
|
||
current_price = self.auction.get_current_price(ServiceType.AUDIO_SECOND)
|
||
self.auction.purchase(
|
||
service_type=ServiceType.AUDIO_SECOND,
|
||
buyer_address=caller_address,
|
||
service_id=f"audio_{self.auction.get_total_sold(ServiceType.AUDIO_SECOND) + 1}",
|
||
amount_paid=current_price
|
||
)
|
||
|
||
return {
|
||
"service_type": "audio",
|
||
"seconds": seconds,
|
||
"total_cost": total_cost,
|
||
"caller": caller_address
|
||
}
|
||
|
||
def register_video_seconds(self, seconds: int, caller_address: str) -> Dict:
|
||
"""
|
||
Зарегистрировать использование N секунд видео.
|
||
|
||
Args:
|
||
seconds: Количество секунд
|
||
caller_address: Montana адрес звонящего
|
||
|
||
Returns:
|
||
Dict с результатом регистрации
|
||
"""
|
||
total_cost = self.calculate_video_call_cost(seconds)
|
||
|
||
# Зарегистрировать каждую секунду в аукционе
|
||
for i in range(seconds):
|
||
current_price = self.auction.get_current_price(ServiceType.VIDEO_SECOND)
|
||
self.auction.purchase(
|
||
service_type=ServiceType.VIDEO_SECOND,
|
||
buyer_address=caller_address,
|
||
service_id=f"video_{self.auction.get_total_sold(ServiceType.VIDEO_SECOND) + 1}",
|
||
amount_paid=current_price
|
||
)
|
||
|
||
return {
|
||
"service_type": "video",
|
||
"seconds": seconds,
|
||
"total_cost": total_cost,
|
||
"caller": caller_address
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# GLOBAL CALL PRICING SERVICE
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
_call_pricing_service: Optional[CallPricingService] = None
|
||
|
||
def get_call_pricing_service(data_dir: Path = None) -> CallPricingService:
|
||
"""Получить глобальный экземпляр CallPricingService (singleton)"""
|
||
global _call_pricing_service
|
||
if _call_pricing_service is None:
|
||
if data_dir is None:
|
||
data_dir = Path(__file__).parent / "data"
|
||
_call_pricing_service = CallPricingService(data_dir)
|
||
return _call_pricing_service
|