fix: harden wallet balances and service startup
This commit is contained in:
@@ -65,6 +65,8 @@ class Settings(BaseSettings):
|
|||||||
CSRF_COOKIE_PATH: str = '/'
|
CSRF_COOKIE_PATH: str = '/'
|
||||||
CSRF_COOKIE_DOMAIN: str | None = None
|
CSRF_COOKIE_DOMAIN: str | None = None
|
||||||
|
|
||||||
|
CORS_ALLOW_ORIGIN_REGEX: str = r'https?://([a-z0-9-]+\.)*elcsa\.ru(:\d+)?$'
|
||||||
|
|
||||||
DOCS_USERNAME: str = 'admin'
|
DOCS_USERNAME: str = 'admin'
|
||||||
DOCS_PASSWORD: str = 'admin'
|
DOCS_PASSWORD: str = 'admin'
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -19,6 +20,8 @@ from src.infrastructure.wallet_balances.token_registry import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
TIMEOUT_SECONDS = 15
|
TIMEOUT_SECONDS = 15
|
||||||
|
RETRY_ATTEMPTS = 2
|
||||||
|
PRICE_CACHE_SECONDS = 30
|
||||||
BLOCKSTREAM_URL = 'https://blockstream.info/api'
|
BLOCKSTREAM_URL = 'https://blockstream.info/api'
|
||||||
TRONGRID_URL = 'https://api.trongrid.io'
|
TRONGRID_URL = 'https://api.trongrid.io'
|
||||||
COINGECKO_URL = 'https://api.coingecko.com/api/v3/simple/price'
|
COINGECKO_URL = 'https://api.coingecko.com/api/v3/simple/price'
|
||||||
@@ -49,6 +52,10 @@ class WalletBalance:
|
|||||||
|
|
||||||
|
|
||||||
class WalletBalanceService:
|
class WalletBalanceService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._prices_cache: dict[tuple[tuple[str,str],...],tuple[float,dict[str,Decimal | None]]] = {}
|
||||||
|
|
||||||
|
|
||||||
async def get_wallet_balances(self, wallets) -> list[WalletBalance]:
|
async def get_wallet_balances(self, wallets) -> list[WalletBalance]:
|
||||||
prices = await self._get_prices(wallets)
|
prices = await self._get_prices(wallets)
|
||||||
results = await asyncio.gather(
|
results = await asyncio.gather(
|
||||||
@@ -193,10 +200,17 @@ class WalletBalanceService:
|
|||||||
for token in self._tokens_for_chain(chain):
|
for token in self._tokens_for_chain(chain):
|
||||||
coin_id = get_coingecko_id(chain, token['symbol'])
|
coin_id = get_coingecko_id(chain, token['symbol'])
|
||||||
if coin_id:
|
if coin_id:
|
||||||
pairs[f'{chain}:{token['symbol']}'] = coin_id
|
symbol = token['symbol']
|
||||||
|
pairs[f'{chain}:{symbol}'] = coin_id
|
||||||
if not pairs:
|
if not pairs:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
cache_key = tuple(sorted(pairs.items()))
|
||||||
|
cached = self._prices_cache.get(cache_key)
|
||||||
|
now = time.monotonic()
|
||||||
|
if cached is not None and now - cached[0] < PRICE_CACHE_SECONDS:
|
||||||
|
return dict(cached[1])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self._get_json(
|
response = await self._get_json(
|
||||||
COINGECKO_URL,
|
COINGECKO_URL,
|
||||||
@@ -210,10 +224,12 @@ class WalletBalanceService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return {key: None for key in pairs}
|
return {key: None for key in pairs}
|
||||||
|
|
||||||
return {
|
prices = {
|
||||||
key: self._decimal_or_none((response.get(coin_id) or {}).get('usd'))
|
key: self._decimal_or_none((response.get(coin_id) or {}).get('usd'))
|
||||||
for key, coin_id in pairs.items()
|
for key, coin_id in pairs.items()
|
||||||
}
|
}
|
||||||
|
self._prices_cache[cache_key] = (now,prices)
|
||||||
|
return prices
|
||||||
|
|
||||||
def _token_amounts(
|
def _token_amounts(
|
||||||
self,
|
self,
|
||||||
@@ -287,9 +303,16 @@ class WalletBalanceService:
|
|||||||
timeout: int = TIMEOUT_SECONDS,
|
timeout: int = TIMEOUT_SECONDS,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
response = await client.get(url, params=params, headers=headers)
|
for attempt in range(RETRY_ATTEMPTS + 1):
|
||||||
response.raise_for_status()
|
try:
|
||||||
return response.json()
|
response = await client.get(url, params=params, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except (httpx.RequestError,httpx.HTTPStatusError,ValueError):
|
||||||
|
if attempt >= RETRY_ATTEMPTS:
|
||||||
|
raise
|
||||||
|
await asyncio.sleep(0.2 * (attempt + 1))
|
||||||
|
raise RuntimeError('HTTP GET failed')
|
||||||
|
|
||||||
async def _post_json(
|
async def _post_json(
|
||||||
self,
|
self,
|
||||||
@@ -300,9 +323,16 @@ class WalletBalanceService:
|
|||||||
timeout: int = TIMEOUT_SECONDS,
|
timeout: int = TIMEOUT_SECONDS,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
response = await client.post(url, json=payload, headers=headers)
|
for attempt in range(RETRY_ATTEMPTS + 1):
|
||||||
response.raise_for_status()
|
try:
|
||||||
return response.json()
|
response = await client.post(url, json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except (httpx.RequestError,httpx.HTTPStatusError,ValueError):
|
||||||
|
if attempt >= RETRY_ATTEMPTS:
|
||||||
|
raise
|
||||||
|
await asyncio.sleep(0.2 * (attempt + 1))
|
||||||
|
raise RuntimeError('HTTP POST failed')
|
||||||
|
|
||||||
def _tokens_for_chain(self, chain: str) -> list[dict]:
|
def _tokens_for_chain(self, chain: str) -> list[dict]:
|
||||||
if chain in {'ETH', 'BSC'}:
|
if chain in {'ETH', 'BSC'}:
|
||||||
|
|||||||
11
src/main.py
11
src/main.py
@@ -74,8 +74,13 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
path=settings.VAULT_CRYPTO_MASTER_KEY_PATH,
|
path=settings.VAULT_CRYPTO_MASTER_KEY_PATH,
|
||||||
)
|
)
|
||||||
logger.info('Crypto master key loaded from Vault')
|
logger.info('Crypto master key loaded from Vault')
|
||||||
yield
|
try:
|
||||||
logger.info(f'B2B service instance ended with id {instance_id}')
|
yield
|
||||||
|
finally:
|
||||||
|
sched = getattr(app.state,'jwt_keys_scheduler',None)
|
||||||
|
if sched:
|
||||||
|
sched.shutdown(wait=False)
|
||||||
|
logger.info(f'B2B service instance ended with id {instance_id}')
|
||||||
|
|
||||||
|
|
||||||
app: FastAPI = FastAPI(
|
app: FastAPI = FastAPI(
|
||||||
@@ -111,7 +116,7 @@ app.add_middleware(
|
|||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=[],
|
allow_origins=[],
|
||||||
allow_origin_regex='.*',
|
allow_origin_regex=settings.CORS_ALLOW_ORIGIN_REGEX,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=['*'],
|
allow_methods=['*'],
|
||||||
allow_headers=['*'],
|
allow_headers=['*'],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from functools import lru_cache
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.commands import (
|
from src.application.commands import (
|
||||||
@@ -54,13 +55,18 @@ def get_list_organization_wallets_command(
|
|||||||
return ListOrganizationWalletsCommand(unit_of_work=unit_of_work, logger=logger)
|
return ListOrganizationWalletsCommand(unit_of_work=unit_of_work, logger=logger)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _wallet_balance_service() -> WalletBalanceService:
|
||||||
|
return WalletBalanceService()
|
||||||
|
|
||||||
|
|
||||||
def get_get_organization_wallet_balances_command(
|
def get_get_organization_wallet_balances_command(
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
) -> GetOrganizationWalletBalancesCommand:
|
) -> GetOrganizationWalletBalancesCommand:
|
||||||
return GetOrganizationWalletBalancesCommand(
|
return GetOrganizationWalletBalancesCommand(
|
||||||
unit_of_work=unit_of_work,
|
unit_of_work=unit_of_work,
|
||||||
balance_service=WalletBalanceService(),
|
balance_service=_wallet_balance_service(),
|
||||||
logger=logger,
|
logger=logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user