diff --git a/.env b/.env index c0fec24f..3d69fb48 100644 --- a/.env +++ b/.env @@ -8,4 +8,5 @@ DB_NAME = "" NGROK_TOKEN = "" PAGE_ENTRIES = "" LANGUAGE = "" -MULTIBOT = "" \ No newline at end of file +MULTIBOT = "" +ETHPLORER_API_KEY = "" \ No newline at end of file diff --git a/config.py b/config.py index 135aee14..6141f230 100644 --- a/config.py +++ b/config.py @@ -18,3 +18,4 @@ PAGE_ENTRIES = int(os.environ.get("PAGE_ENTRIES")) LANGUAGE = os.environ.get("LANGUAGE") MULTIBOT = os.environ.get("MULTIBOT", False) == 'true' +ETHPLORER_API_KEY = os.environ.get("ETHPLORER_API_KEY") diff --git a/crypto_api/CryptoApiManager.py b/crypto_api/CryptoApiManager.py index 93b3d723..c34626f7 100644 --- a/crypto_api/CryptoApiManager.py +++ b/crypto_api/CryptoApiManager.py @@ -1,53 +1,152 @@ -from typing import Any +from datetime import datetime, timedelta +import aiohttp import grequests +import config +from services.deposit import DepositService + class CryptoApiManager: - def __init__(self, btc_address, ltc_address, trx_address): + def __init__(self, btc_address, ltc_address, trx_address, eth_address, user_id): self.btc_address = btc_address.strip() self.ltc_address = ltc_address.strip() self.trx_address = trx_address.strip() + self.eth_address = eth_address.strip().lower() + self.user_id = user_id + self.min_timestamp = int((datetime.now() - timedelta(hours=24)).timestamp()) * 1000 + + @staticmethod + async def fetch_api_request(url: str) -> dict: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data = await response.json() + return data + + async def get_btc_balance(self, deposits) -> float: + url = f'https://mempool.space/api/address/{self.btc_address}/utxo' + data = await self.fetch_api_request(url) + deposits = [deposit.tx_id for deposit in deposits if deposit.network == "BTC"] + deposit_sum = 0.0 + for deposit in data: + if deposit["txid"] not in deposits and deposit['status']['confirmed']: + await DepositService.create(deposit['txid'], self.user_id, "BTC", None, + deposit["value"], deposit['vout']) + deposit_sum += float(deposit["value"]) / 100_000_000 + return deposit_sum + + async def get_ltc_balance(self, deposits) -> float: + url = f"https://api.blockcypher.com/v1/ltc/main/addrs/{self.ltc_address}?unspentOnly=true" + data = await self.fetch_api_request(url) + deposits = [deposit.tx_id for deposit in deposits if deposit.network == "LTC"] + deposits_sum = 0.0 + if data['n_tx'] > 0: + for deposit in data['txrefs']: + if deposit["confirmations"] > 0 and deposit['tx_hash'] not in deposits: + await DepositService.create(deposit['tx_hash'], self.user_id, "LTC", None, + deposit["value"], deposit['tx_output_n']) + deposits_sum += float(deposit['value']) / 100_000_000 + return deposits_sum + + async def get_usdt_trc20_balance(self, deposits) -> float: + url = f"https://api.trongrid.io/v1/accounts/{self.trx_address}/transactions/trc20?only_confirmed=true&min_timestamp={self.min_timestamp}&contract_address=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t&only_to=true" + data = await self.fetch_api_request(url) + deposits = [deposit.tx_id for deposit in deposits if + deposit.network == "TRX" and deposit.token_name == "USDT_TRC20"] + deposits_sum = 0.0 + for deposit in data['data']: + if deposit['transaction_id'] not in deposits: + await DepositService.create(deposit['transaction_id'], self.user_id, "TRX", + "USDT_TRC20", deposit['value']) + deposits_sum += float(deposit['value']) / pow(10, deposit['token_info']['decimals']) + return deposits_sum + + async def get_usdd_trc20_balance(self, deposits) -> float: + url = f"https://api.trongrid.io/v1/accounts/{self.trx_address}/transactions/trc20?only_confirmed=true&min_timestamp={self.min_timestamp}&contract_address=TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn&only_to=true" + data = await self.fetch_api_request(url) + deposits = [deposit.tx_id for deposit in deposits if + deposit.network == "TRX" and deposit.token_name == "USDD_TRC20"] + deposits_sum = 0.0 + for deposit in data['data']: + if deposit['transaction_id'] not in deposits: + await DepositService.create(deposit['transaction_id'], self.user_id, "TRX", + "USDD_TRC20", deposit['value']) + deposits_sum += float(deposit['value']) / pow(10, deposit['token_info']['decimals']) + return deposits_sum + + async def get_usdt_erc20_balance(self, deposits) -> float: + url = f'https://api.ethplorer.io/getAddressHistory/{self.eth_address}?type=transfer&token=0xdAC17F958D2ee523a2206206994597C13D831ec7&apiKey={config.ETHPLORER_API_KEY}&limit=1000' + data = await self.fetch_api_request(url) + deposits = [deposit.tx_id for deposit in deposits if + deposit.network == "ETH" and deposit.token_name == "USDT_ERC20"] + deposits_sum = 0.0 + for deposit in data['operations']: + if deposit['transactionHash'] not in deposits and deposit['to'] == self.eth_address: + await DepositService.create(deposit['transactionHash'], self.user_id, "ETH", "USDT_ERC20", + deposit['value']) + deposits_sum += float(deposit['value']) / pow(10, 6) + return deposits_sum + + async def get_usdc_erc20_balance(self, deposits): + url = f'https://api.ethplorer.io/getAddressHistory/{self.eth_address}?type=transfer&token=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&apiKey={config.ETHPLORER_API_KEY}&limit=1000' + data = await self.fetch_api_request(url) + deposits = [deposit.tx_id for deposit in deposits if + deposit.network == "ETH" and deposit.token_name == "USDC_ERC20"] + deposits_sum = 0.0 + for deposit in data['operations']: + if deposit['transactionHash'] not in deposits and deposit['to'] == self.eth_address: + await DepositService.create(deposit['transactionHash'], self.user_id, "ETH", "USDC_ERC20", + deposit['value']) + deposits_sum += float(deposit['value']) / pow(10, 6) + return deposits_sum async def get_top_ups(self): - urls = { - "btc_balance": f'https://blockchain.info/rawaddr/{self.btc_address}', - "usdt_balance": f'https://apilist.tronscan.org/api/account?address={self.trx_address}&includeToken=true', - "ltc_balance": f'https://api.blockcypher.com/v1/ltc/main/addrs/{self.ltc_address}' + user_deposits = await DepositService.get_by_user_id(self.user_id) + balances = {"btc__deposit": await self.get_btc_balance(user_deposits), + "ltc__deposit": await self.get_ltc_balance(user_deposits), + "usdt_trc20_deposit": await self.get_usdt_trc20_balance(user_deposits), + "usdd_trc20_deposit": await self.get_usdd_trc20_balance(user_deposits), + "usdt_erc20_deposit": await self.get_usdt_erc20_balance(user_deposits), + "usdc_erc20_deposit": await self.get_usdc_erc20_balance(user_deposits)} + return balances + + async def get_top_up_by_crypto_name(self, crypto_name: str): + user_deposits = await DepositService.get_by_user_id(self.user_id) + + crypto_functions = { + "BTC": ("btc_deposit", self.get_btc_balance), + "LTC": ("ltc_deposit", self.get_ltc_balance), + "TRX_USDT": ("usdt_trc20_deposit", self.get_usdt_trc20_balance), + "TRX_USDD": ("usdd_trc20_deposit", self.get_usdd_trc20_balance), + "ETH_USDT": ("usdt_erc20_deposit", self.get_usdt_erc20_balance), + "ETH_USDC": ("usdc_erc20_deposit", self.get_usdc_erc20_balance), } - balances = {} - rs = (grequests.get(url) for url in urls.values()) - data_list = grequests.map(rs) - - for symbol, data in zip(urls.keys(), data_list): - response_code = data.status_code - if response_code != 200: - balances[symbol] = 0 - else: - data = data.json() - if 'total_received' in data: - balance = float(data['total_received']) / 100000000 - balances[symbol] = balance - else: - usdt_balance = None - for token in data['trc20token_balances']: - if token['tokenName'] == 'Tether USD': - usdt_balance = round(float(token['balance']) * pow(10, -token['tokenDecimal']), 6) - break - if usdt_balance is not None: - balances[symbol] = usdt_balance - else: - balances[symbol] = 0.0 - return balances + if "_" in crypto_name: + base, token = crypto_name.split('_') + else: + base, token = crypto_name, None + + key = f"{base}_{token}" if token else base + deposit_name, balance_func = crypto_functions.get(key, (None, None)) + + if deposit_name and balance_func: + return {deposit_name: await balance_func(user_deposits)} + + raise ValueError(f"Unsupported crypto name: {crypto_name}") @staticmethod async def get_crypto_prices() -> dict[str, float]: + # TODO("NEED API FOR USDD-TRC-20") usd_crypto_prices = {} urls = { "btc": 'https://api.kraken.com/0/public/Ticker?pair=BTCUSDT', "usdt": 'https://api.kraken.com/0/public/Ticker?pair=USDTUSD', - "ltc": 'https://api.kraken.com/0/public/Ticker?pair=LTCUSD' + "usdc": "https://api.kraken.com/0/public/Ticker?pair=USDCUSD", + "ltc": 'https://api.kraken.com/0/public/Ticker?pair=LTCUSD', + "eth": 'https://api.kraken.com/0/public/Ticker?pair=ETHUSD', + "trx": "https://api.kraken.com/0/public/Ticker?pair=TRXUSD" } responses = (grequests.get(url) for url in urls.values()) datas = grequests.map(responses) @@ -55,5 +154,5 @@ async def get_crypto_prices() -> dict[str, float]: data = data.json() price = float(next(iter(data['result'].values()))['l'][1]) usd_crypto_prices[symbol] = price + usd_crypto_prices["usdd"] = 1.0 # 1USDD=1USD return usd_crypto_prices - diff --git a/db.py b/db.py index 2cc3505a..b991d2cb 100644 --- a/db.py +++ b/db.py @@ -15,6 +15,7 @@ from models.buyItem import BuyItem from models.category import Category from models.subcategory import Subcategory +from models.deposit import Deposit url = f"sqlite+aiosqlite:///data/{DB_NAME}" data_folder = Path("data") diff --git a/docker-compose.yml b/docker-compose.yml index a613cd14..0dce889e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: PAGE_ENTRIES: 20 # Items per page LANGUAGE: "en" # The name of your file from the l10n folder without the .json suffix MULTIBOT: "false" # Allows the use of a multibot + ETHPLORER_API_KEY: "" # API key from Ethplorer ports: - "4040:4040" - "5000:5000" # ${WEBAPP_PORT}:${WEBAPP_PORT} diff --git a/handlers/user/my_profile.py b/handlers/user/my_profile.py index cd56dc85..03ed4e3d 100644 --- a/handlers/user/my_profile.py +++ b/handlers/user/my_profile.py @@ -43,12 +43,18 @@ class MyProfileConstants: async def get_my_profile_message(telegram_id: int): user = await UserService.get_by_tgid(telegram_id) btc_balance = user.btc_balance - usdt_balance = user.usdt_balance + usdt_trc20_balance = user.usdt_trc20_balance + usdd_trc20_balance = user.usdd_trc20_balance + usdt_erc20_balance = user.usdt_erc20_balance + usdc_erc20_balance = user.usdc_erc20_balance ltc_balance = user.ltc_balance usd_balance = round(user.top_up_amount - user.consume_records, 2) return Localizator.get_text_from_key("my_profile_msg").format(telegram_id=telegram_id, btc_balance=btc_balance, - usdt_balance=usdt_balance, + usdt_trc20_balance=usdt_trc20_balance, + usdd_trc20_balance=usdd_trc20_balance, + usdt_erc20_balance=usdt_erc20_balance, + usdc_erc20_balance=usdc_erc20_balance, ltc_balance=ltc_balance, usd_balance=usd_balance) @@ -58,13 +64,10 @@ async def my_profile(message: Union[Message, CallbackQuery]): top_up_button = types.InlineKeyboardButton(text=Localizator.get_text_from_key("top_up_balance_button"), callback_data=create_callback_profile(current_level + 1, "top_up")) purchase_history_button = types.InlineKeyboardButton(text=Localizator.get_text_from_key("purchase_history_button"), - callback_data=create_callback_profile(current_level + 2, + callback_data=create_callback_profile(current_level + 4, "purchase_history")) - update_balance = types.InlineKeyboardButton(text=Localizator.get_text_from_key("refresh_balance_button"), - callback_data=create_callback_profile(current_level + 3, - "refresh_balance")) my_profile_builder = InlineKeyboardBuilder() - my_profile_builder.add(top_up_button, purchase_history_button, update_balance) + my_profile_builder.add(top_up_button, purchase_history_button) my_profile_builder.adjust(2) my_profile_markup = my_profile_builder.as_markup() @@ -84,25 +87,34 @@ async def my_profile(message: Union[Message, CallbackQuery]): async def top_up_balance(callback: CallbackQuery): - telegram_id = callback.message.chat.id - user = await UserService.get_by_tgid(telegram_id) current_level = 1 - btc_address = user.btc_address - trx_address = user.trx_address - ltc_address = user.ltc_address back_to_profile_button = types.InlineKeyboardButton(text=Localizator.get_text_from_key("admin_back_button"), callback_data=create_callback_profile(current_level - 1)) - back_button_builder = InlineKeyboardBuilder() - back_button_builder.add(back_to_profile_button) - back_button_markup = back_button_builder.as_markup() - bot_entity = await callback.bot.get_me() + top_up_methods_builder = InlineKeyboardBuilder() + top_up_methods_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("btc_top_up"), + callback_data=create_callback_profile(current_level + 1, + args_for_action="BTC"))) + top_up_methods_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("ltc_top_up"), + callback_data=create_callback_profile(current_level + 1, + args_for_action="LTC"))) + top_up_methods_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("usdt_trc20_top_up"), + callback_data=create_callback_profile(current_level + 1, + args_for_action="TRX_USDT"))) + top_up_methods_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("usdd_trc20_top_up"), + callback_data=create_callback_profile(current_level + 1, + args_for_action="TRX_USDD"))) + top_up_methods_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("usdt_erc20_top_up"), + callback_data=create_callback_profile(current_level + 1, + args_for_action="ETH_USDT"))) + top_up_methods_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("usdc_trc20_top_up"), + callback_data=create_callback_profile(current_level + 1, + args_for_action="ETH_USDC"))) + top_up_methods_builder.row(back_to_profile_button) + await callback.message.edit_text( - text=Localizator.get_text_from_key("top_up_balance_msg").format(bot_name=bot_entity.first_name, - btc_address=btc_address, - trx_address=trx_address, - ltc_address=ltc_address), + text=Localizator.get_text_from_key("choose_top_up_method"), parse_mode=ParseMode.HTML, - reply_markup=back_button_markup) + reply_markup=top_up_methods_builder.as_markup()) await callback.answer() @@ -115,7 +127,7 @@ async def create_purchase_history_keyboard_builder(page: int, user_id: int): buy_id = order.id buy_item = await BuyItemService.get_buy_item_by_buy_id(buy_id) item = await ItemService.get_by_primary_key(buy_item.item_id) - item_from_history_callback = create_callback_profile(4, action="get_order", + item_from_history_callback = create_callback_profile(5, action="get_order", args_for_action=str(buy_id)) order_inline = types.InlineKeyboardButton( text=Localizator.get_text_from_key("purchase_history_item").format(subcategory_name=item.subcategory.name, @@ -141,32 +153,33 @@ async def purchase_history(callback: CallbackQuery): reply_markup=orders_markup_builder.as_markup(), parse_mode=ParseMode.HTML) else: - await callback.message.edit_text(Localizator.get_text_from_key("purchases"), reply_markup=orders_markup_builder.as_markup(), + await callback.message.edit_text(Localizator.get_text_from_key("purchases"), + reply_markup=orders_markup_builder.as_markup(), parse_mode=ParseMode.HTML) await callback.answer() async def refresh_balance(callback: CallbackQuery): telegram_id = callback.from_user.id + unpacked_cb = MyProfileCallback.unpack(callback.data) + crypto_info = unpacked_cb.args_for_action if await UserService.can_refresh_balance(telegram_id): await callback.answer(Localizator.get_text_from_key("balance_refreshing")) - old_crypto_balances = await UserService.get_balances(telegram_id) await UserService.create_last_balance_refresh_data(telegram_id) + user = await UserService.get_by_tgid(telegram_id) addresses = await UserService.get_addresses(telegram_id) - new_crypto_balances = await CryptoApiManager(**addresses).get_top_ups() + new_crypto_deposits = await CryptoApiManager(**addresses, user_id=user.id).get_top_up_by_crypto_name(crypto_info) crypto_prices = await CryptoApiManager.get_crypto_prices() deposit_usd_amount = 0.0 bot_obj = callback.bot - if sum(new_crypto_balances.values()) > sum(old_crypto_balances.values()): - merged_deposit = {key: new_crypto_balances[key] - old_crypto_balances[key] for key in - new_crypto_balances.keys()} - for balance_key, balance in merged_deposit.items(): + if sum(new_crypto_deposits.values()) > 0: + for balance_key, balance in new_crypto_deposits.items(): balance_key = balance_key.split('_')[0] crypto_balance_in_usd = balance * crypto_prices[balance_key] deposit_usd_amount += crypto_balance_in_usd - await UserService.update_crypto_balances(telegram_id, new_crypto_balances) + await UserService.update_crypto_balances(telegram_id, new_crypto_deposits) await UserService.update_top_up_amount(telegram_id, deposit_usd_amount * 0.95) - await NotificationManager.new_deposit(old_crypto_balances, new_crypto_balances, deposit_usd_amount, + await NotificationManager.new_deposit(new_crypto_deposits, deposit_usd_amount, telegram_id, bot_obj) await my_profile(callback) else: @@ -174,17 +187,46 @@ async def refresh_balance(callback: CallbackQuery): async def get_order_from_history(callback: CallbackQuery): - current_level = 4 + current_level = 5 buy_id = MyProfileCallback.unpack(callback.data).args_for_action items = await ItemService.get_items_by_buy_id(buy_id) message = await create_message_with_bought_items(items) back_builder = InlineKeyboardBuilder() back_button = types.InlineKeyboardButton(text=Localizator.get_text_from_key("admin_back_button"), - callback_data=create_callback_profile(level=current_level - 2)) + callback_data=create_callback_profile(level=current_level - 1)) back_builder.add(back_button) await callback.message.edit_text(text=message, parse_mode=ParseMode.HTML, reply_markup=back_builder.as_markup()) +async def top_up_by_method(callback: CallbackQuery): + unpacked_cb = MyProfileCallback.unpack(callback.data) + current_level = unpacked_cb.level + payment_method = unpacked_cb.args_for_action + addr = "" + user = await UserService.get_by_tgid(callback.from_user.id) + bot = await callback.bot.get_me() + if payment_method == "BTC": + addr = user.btc_address + elif payment_method == "LTC": + addr = user.ltc_address + elif "ETH" in payment_method : + addr = user.eth_address + elif "TRX" in payment_method: + addr = user.trx_address + msg = Localizator.get_text_from_key("top_up_balance_msg").format(bot_name=bot.first_name, + crypto_name=payment_method.split("_")[0], + addr=addr) + refresh_balance_builder = InlineKeyboardBuilder() + refresh_balance_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("refresh_balance_button"), + callback_data=create_callback_profile(current_level + 1, + args_for_action=payment_method))) + refresh_balance_builder.row(types.InlineKeyboardButton(text=Localizator.get_text_from_key("admin_back_button"), + callback_data=create_callback_profile( + level=current_level - 1))) + await callback.message.edit_text(text=msg, parse_mode=ParseMode.HTML, + reply_markup=refresh_balance_builder.as_markup()) + + @my_profile_router.callback_query(MyProfileCallback.filter(), IsUserExistFilter()) async def navigate(callback: CallbackQuery, callback_data: MyProfileCallback): current_level = callback_data.level @@ -192,9 +234,10 @@ async def navigate(callback: CallbackQuery, callback_data: MyProfileCallback): levels = { 0: my_profile, 1: top_up_balance, - 2: purchase_history, + 2: top_up_by_method, 3: refresh_balance, - 4: get_order_from_history + 4: purchase_history, + 5: get_order_from_history } current_level_function = levels[current_level] diff --git a/l10n/en.json b/l10n/en.json index 76f87742..0d9633da 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -62,11 +62,11 @@ "out_of_stock": "Out of stock!", "purchased_item": "Item#{count}\nData:{private_data}\n", "back_to_my_profile": "⤵\uFE0FBack my profile", - "my_profile_msg": "Your profile\nID: {telegram_id}\n\nYour BTC balance:\n{btc_balance}\nYour USDT balance:\n{usdt_balance}\nYour LTC balance:\n{ltc_balance}\nYour balance in USD:\n{usd_balance}$", + "my_profile_msg": "Your profile\nID: {telegram_id}\n\nYour BTC balance:\n{btc_balance}\nYour USDT TRC-20 balance:\n{usdt_trc20_balance}\nYour USDD TRC-20 balance:\n{usdd_trc20_balance}\nYour USDT ERC-20 balance:\n{usdt_erc20_balance}\nYour USDC ERC-20 balance:\n{usdc_erc20_balance}\nYour LTC balance:\n{ltc_balance}\nYour balance in USD:\n{usd_balance}$", "top_up_balance_button": "Top Up balance", "purchase_history_button": "Purchase history", "refresh_balance_button": "Refresh balance", - "top_up_balance_msg": "Deposit to the address the amount you want to top up the {bot_name} \n\nImportant\nA unique BTC/LTC/USDT addresses is given for each deposit\nThe top up takes place within 5 minutes after the transfer\n\nYour BTC address\n{btc_address}\nYour USDT TRC-20 address\n{trx_address}\nYour LTC address\n{ltc_address}\n", + "top_up_balance_msg": "Deposit to the address the amount you want to top up the {bot_name} \n\nImportant\nA unique BTC/LTC/TRX/ETH addresses is given for each user\nThe top up takes place within 5 minutes after the transfer.\n\nAfter a successful balance refresh, the balance must change in your profile.\n\nYour {crypto_name} address\n{addr}", "purchase_history_item": "{subcategory_name} | Total Price: {total_price}$ | Quantity: {quantity} pcs", "no_purchases": "You haven't had any purchases yet", "purchases": "Your purchases:", @@ -75,12 +75,18 @@ "user_notification_refund": "You have been refunded ${total_price} for the purchase of {quantity} pieces of {subcategory}", "admin_notification_new_deposit_username": "New deposit by user with username @{username} for ${deposit_amount_usd} with ", "admin_notification_new_deposit_id": "New deposit by user with ID {telegram_id} for ${deposit_amount_usd} with ", - "usdt_deposit_notification_part": "{value} {crypto_name}\nTRX address:{trx_address}\n", "crypto_deposit_notification_part": "{value} {crypto_name}\n{crypto_name} address:{crypto_address}\n", "seed_notification_part": "Seed: {seed}", "new_purchase_notification_with_tgid": "A new purchase by user @{username} for the amount of ${total_price} for the purchase of a {quantity} pcs {subcategory_name}.", "new_purchase_notification_with_username": "A new purchase by user with ID:{telegram_id} for the amount of ${total_price} for the purchase of a {quantity} pcs {subcategory_name}.", "new_items_message_update": "\uD83D\uDCC5 Update {update_data}\n", "new_items_message_category": "\n\uD83D\uDCC1 Category {category}\n\n", - "new_items_message_subcategory": "\uD83D\uDCC4 Subcategory {subcategory_name} {items_len} pcs\n" + "new_items_message_subcategory": "\uD83D\uDCC4 Subcategory {subcategory_name} {items_len} pcs\n", + "usdt_trc20_top_up":"USDT TRC-20", + "usdd_trc20_top_up":"USDD TRC-20", + "usdt_erc20_top_up":"USDT ERC-20", + "usdc_trc20_top_up":"USDC ERC-20", + "btc_top_up":"BTC", + "ltc_top_up":"LTC", + "choose_top_up_method": "Choose a top-up method:" } \ No newline at end of file diff --git a/models/deposit.py b/models/deposit.py new file mode 100644 index 00000000..afd38df7 --- /dev/null +++ b/models/deposit.py @@ -0,0 +1,16 @@ +from sqlalchemy import Integer, Column, String, ForeignKey, Boolean, BigInteger, DateTime, func + +from models.base import Base + + +class Deposit(Base): + __tablename__ = 'deposits' + id = Column(Integer, primary_key=True) + tx_id = Column(String, nullable=False, unique=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + network = Column(String, nullable=False) + token_name = Column(String, nullable=True) + amount = Column(BigInteger, nullable=False) + is_withdrawn = Column(Boolean, default=False) + vout = Column(Integer, nullable=True) + deposit_datetime = Column(DateTime, default=func.now()) diff --git a/models/user.py b/models/user.py index 7a7779f7..84a1bf95 100644 --- a/models/user.py +++ b/models/user.py @@ -1,4 +1,5 @@ -from sqlalchemy import Column, Integer, DateTime, String, Boolean, Float, func +from sqlalchemy import Column, Integer, DateTime, String, Boolean, Float, func, ForeignKey +from sqlalchemy.orm import relationship, backref from models.base import Base @@ -12,12 +13,16 @@ class User(Base): btc_address = Column(String, nullable=False, unique=True) ltc_address = Column(String, nullable=False, unique=True) trx_address = Column(String, nullable=False, unique=True) + eth_address = Column(String, nullable=False, unique=True) last_balance_refresh = Column(DateTime) top_up_amount = Column(Float, default=0.0) consume_records = Column(Float, default=0.0) btc_balance = Column(Float, nullable=False, default=0.0) ltc_balance = Column(Float, nullable=False, default=0.0) - usdt_balance = Column(Float, nullable=False, default=0.0) + usdt_trc20_balance = Column(Float, nullable=False, default=0.0) + usdd_trc20_balance = Column(Float, nullable=False, default=0.0) + usdt_erc20_balance = Column(Float, nullable=False, default=0.0) + usdc_erc20_balance = Column(Float, nullable=False, default=0.0) registered_at = Column(DateTime, default=func.now()) seed = Column(String, nullable=False, unique=True) can_receive_messages = Column(Boolean, default=True) diff --git a/services/deposit.py b/services/deposit.py new file mode 100644 index 00000000..1044d807 --- /dev/null +++ b/services/deposit.py @@ -0,0 +1,41 @@ +from sqlalchemy import select + +from db import async_session_maker +from models.deposit import Deposit + + +class DepositService: + + @staticmethod + async def get_next_user_id() -> int: + async with async_session_maker() as session: + query = select(Deposit.id).order_by(Deposit.id.desc()).limit(1) + last_user_id = await session.execute(query) + last_user_id = last_user_id.scalar() + if last_user_id is None: + return 0 + else: + return int(last_user_id) + 1 + + @staticmethod + async def create(tx_id, user_id, network, token_name, amount, vout=None): + async with async_session_maker() as session: + next_deposit_id = await DepositService.get_next_user_id() + dep = Deposit(id=next_deposit_id, + user_id=user_id, + tx_id=tx_id, + network=network, + token_name=token_name, + amount=amount, + vout=vout) + session.add(dep) + await session.commit() + return next_deposit_id + + @staticmethod + async def get_by_user_id(user_id): + async with async_session_maker() as session: + stmt = select(Deposit).where(Deposit.user_id == user_id) + deposits = await session.execute(stmt) + deposits = deposits.scalars().all() + return deposits diff --git a/services/user.py b/services/user.py index d5fa8d4d..0843c756 100644 --- a/services/user.py +++ b/services/user.py @@ -1,9 +1,7 @@ import datetime -import logging import math from sqlalchemy import select, update, func - import config from db import async_session_maker @@ -34,10 +32,10 @@ async def get_next_user_id() -> int: @staticmethod async def create(telegram_id: int, telegram_username: str): + crypto_addr_gen = CryptoAddressGenerator() + crypto_addresses = crypto_addr_gen.get_addresses(i=0) async with async_session_maker() as session: next_user_id = await UserService.get_next_user_id() - crypto_addr_gen = CryptoAddressGenerator() - crypto_addresses = crypto_addr_gen.get_addresses(i=0) new_user = User( id=next_user_id, telegram_username=telegram_username, @@ -45,6 +43,7 @@ async def create(telegram_id: int, telegram_username: str): btc_address=crypto_addresses['btc'], ltc_address=crypto_addresses['ltc'], trx_address=crypto_addresses['trx'], + eth_address=crypto_addresses['eth'], seed=crypto_addr_gen.mnemonic_str ) session.add(new_user) @@ -91,33 +90,55 @@ async def create_last_balance_refresh_data(telegram_id: int): @staticmethod async def get_balances(telegram_id: int) -> dict: async with async_session_maker() as session: - stmt = select(User.btc_balance, User.ltc_balance, User.usdt_balance).where(User.telegram_id == telegram_id) + stmt = select(User).where(User.telegram_id == telegram_id) user_balances = await session.execute(stmt) - user_balances = user_balances.fetchone() - keys = ["btc_balance", "ltc_balance", "usdt_balance"] + user_balances = user_balances.scalar() + user_balances = [user_balances.btc_balance, user_balances.ltc_balance, + user_balances.usdt_trc20_balance, user_balances.usdd_trc20_balance, + user_balances.usdt_erc20_balance, user_balances.usdc_erc20_balance] + keys = ["btc_balance", "ltc_balance", "trc_20_usdt_balance", "trc_20_usdd_balance", "erc_20_usdt_balance", + "erc_20_usdc_balance"] user_balances = dict(zip(keys, user_balances)) return user_balances @staticmethod async def get_addresses(telegram_id: int) -> dict: - async with async_session_maker() as session: - stmt = select(User.btc_address, User.ltc_address, User.trx_address).where(User.telegram_id == telegram_id) + async with (async_session_maker() as session): + stmt = select(User).where(User.telegram_id == telegram_id) user_addresses = await session.execute(stmt) - user_addresses = user_addresses.fetchone() - keys = ["btc_address", "ltc_address", "trx_address"] + user_addresses = user_addresses.scalar() + user_addresses = [user_addresses.btc_address, user_addresses.ltc_address, + user_addresses.trx_address, user_addresses.eth_address] + keys = ["btc_address", "ltc_address", "trx_address", "eth_address"] user_addresses = dict(zip(keys, user_addresses)) return user_addresses @staticmethod async def update_crypto_balances(telegram_id: int, new_crypto_balances: dict): async with async_session_maker() as session: - stmt = update(User).where(User.telegram_id == telegram_id).values( - btc_balance=new_crypto_balances["btc_balance"], - ltc_balance=new_crypto_balances["ltc_balance"], - usdt_balance=new_crypto_balances["usdt_balance"], - ) - await session.execute(stmt) - await session.commit() + get_old_values_stmt = select(User).where(User.telegram_id == telegram_id) + result = await session.execute(get_old_values_stmt) + user = result.scalar() + balance_fields_map = { + "btc_deposit": "btc_balance", + "ltc_deposit": "ltc_balance", + "usdt_trc20_deposit": "usdt_trc20_balance", + "usdd_trc20_deposit": "usdd_trc20_balance", + "usdd_erc20_deposit": "usdd_erc20_balance", + "usdc_erc20_deposit": "usdc_erc20_balance", + } + update_values = {} + + for key, value in new_crypto_balances.items(): + if key in balance_fields_map: + field_name = balance_fields_map[key] + current_balance = getattr(user, field_name) + update_values[field_name] = current_balance + value + + if update_values: + stmt = update(User).where(User.telegram_id == telegram_id).values(**update_values) + await session.execute(stmt) + await session.commit() @staticmethod async def update_top_up_amount(telegram_id, deposit_amount): diff --git a/typesDTO/itemDTO.py b/types/dto/itemDTO.py similarity index 100% rename from typesDTO/itemDTO.py rename to types/dto/itemDTO.py diff --git a/utils/CryptoAddressGenerator.py b/utils/CryptoAddressGenerator.py index 97cf67e7..09b4b8c8 100644 --- a/utils/CryptoAddressGenerator.py +++ b/utils/CryptoAddressGenerator.py @@ -3,33 +3,51 @@ class CryptoAddressGenerator: - def __init__(self): - mnemonic_gen = Bip39MnemonicGenerator().FromWordsNumber(Bip39WordsNum.WORDS_NUM_12) - self.mnemonic_str = mnemonic_gen.ToStr() - self.seed_bytes = Bip39SeedGenerator(self.mnemonic_str).Generate() + def __init__(self, seed_str: str = None): + if seed_str is not None: + self.mnemonic_str = seed_str + self.seed_bytes = Bip39SeedGenerator(self.mnemonic_str).Generate() + else: + mnemonic_gen = Bip39MnemonicGenerator().FromWordsNumber(Bip39WordsNum.WORDS_NUM_12) + self.mnemonic_str = mnemonic_gen.ToStr() + self.seed_bytes = Bip39SeedGenerator(self.mnemonic_str).Generate() - def __generate_btc_pair(self, i: int) -> str: + def __generate_btc_pair(self, i: int) -> tuple: bip84_mst_ctx = Bip84.FromSeed(self.seed_bytes, Bip84Coins.BITCOIN) bip84_acc_ctx = bip84_mst_ctx.Purpose().Coin().Account(0) bip84_chg_ctx = bip84_acc_ctx.Change(Bip44Changes.CHAIN_EXT) bip84_addr_ctx = bip84_chg_ctx.AddressIndex(i).PublicKey().ToAddress() - return bip84_addr_ctx + return bip84_addr_ctx, bip84_chg_ctx.AddressIndex(i).PrivateKey().ToWif() - def __generate_ltc_pair(self, i: int) -> str: + def __generate_ltc_pair(self, i: int) -> tuple: bip84_mst_ctx = Bip84.FromSeed(self.seed_bytes, Bip84Coins.LITECOIN) bip84_acc_ctx = bip84_mst_ctx.Purpose().Coin().Account(0) bip84_chg_ctx = bip84_acc_ctx.Change(Bip44Changes.CHAIN_EXT) bip84_addr_ctx = bip84_chg_ctx.AddressIndex(i).PublicKey().ToAddress() - return bip84_addr_ctx + return bip84_addr_ctx, bip84_chg_ctx.AddressIndex(i).PrivateKey().ToWif() - def __generate_trx_pair(self, i: int) -> str: + def __generate_trx_pair(self, i: int) -> tuple: bip44_mst_ctx = Bip44.FromSeed(self.seed_bytes, Bip44Coins.TRON) bip44_acc_ctx = bip44_mst_ctx.Purpose().Coin().Account(0) bip44_chg_ctx = bip44_acc_ctx.Change(Bip44Changes.CHAIN_EXT) bip44_addr_ctx = bip44_chg_ctx.AddressIndex(i).PublicKey().ToAddress() - return bip44_addr_ctx + return bip44_addr_ctx, bip44_chg_ctx.AddressIndex(i).PrivateKey().ToWif() - def get_addresses(self, i): - return {'btc': self.__generate_btc_pair(i), - 'ltc': self.__generate_ltc_pair(i), - 'trx': self.__generate_trx_pair(i)} + def __generate_eth_pair(self, i: int) -> tuple: + bip44_mst_ctx = Bip44.FromSeed(self.seed_bytes, Bip44Coins.ETHEREUM) + bip44_acc_ctx = bip44_mst_ctx.Purpose().Coin().Account(0) + bip44_chg_ctx = bip44_acc_ctx.Change(Bip44Changes.CHAIN_EXT) + bip44_addr_ctx = bip44_chg_ctx.AddressIndex(i).PublicKey().ToAddress() + return bip44_addr_ctx, bip44_chg_ctx.AddressIndex(i).PrivateKey().ToWif() + + def get_private_keys(self, i: int) -> dict: + return {'btc': self.__generate_btc_pair(i)[1], + 'ltc': self.__generate_ltc_pair(i)[1], + 'trx': self.__generate_trx_pair(i)[1], + 'eth': self.__generate_eth_pair(i)[1]} + + def get_addresses(self, i: int): + return {'btc': self.__generate_btc_pair(i)[0], + 'ltc': self.__generate_ltc_pair(i)[0], + 'trx': self.__generate_trx_pair(i)[0], + 'eth': self.__generate_eth_pair(i)[0]} diff --git a/utils/new_items_generator.py b/utils/new_items_generator.py index a048582b..eb00ea9c 100644 --- a/utils/new_items_generator.py +++ b/utils/new_items_generator.py @@ -1,6 +1,6 @@ import json -from typesDTO.itemDTO import ItemDTO +from typesDTO.dto.itemDTO import ItemDTO from dataclasses import asdict diff --git a/utils/notification_manager.py b/utils/notification_manager.py index a27dfab8..5db2cd8b 100644 --- a/utils/notification_manager.py +++ b/utils/notification_manager.py @@ -3,7 +3,6 @@ from aiogram import types from aiogram.utils.keyboard import InlineKeyboardBuilder - from services.subcategory import SubcategoryService from services.user import UserService from config import ADMIN_ID_LIST @@ -40,41 +39,41 @@ async def make_user_button(username: Union[str, None]): return user_button_builder.as_markup() @staticmethod - async def new_deposit(old_crypto_balances: dict, new_crypto_balances: dict, deposit_amount_usd, telegram_id: int, - bot): + async def new_deposit(new_crypto_balances: dict, deposit_amount_usd, telegram_id: int, bot): deposit_amount_usd = round(deposit_amount_usd, 2) - merged_crypto_balances = [new_balance - old_balance for (new_balance, old_balance) in - zip(new_crypto_balances.values(), - old_crypto_balances.values())] - merged_crypto_balances_keys = [key.split('_')[0] for key in new_crypto_balances.keys()] - merged_crypto_balances = zip(merged_crypto_balances_keys, merged_crypto_balances) + merged_crypto_balances = { + key.replace('_deposit', "").replace('_', ' ').upper(): value + for key, value in new_crypto_balances.items() + } + user = await UserService.get_by_tgid(telegram_id) - user = user.__dict__ - username = user['telegram_username'] - user_button = await NotificationManager.make_user_button(username) - if username: + user_button = await NotificationManager.make_user_button(user.telegram_username) + address_map = { + "TRC": user.trx_address, + "ERC": user.eth_address, + "BTC": user.btc_address, + "LTC": user.ltc_address + } + crypto_key = list(merged_crypto_balances.keys())[0] + addr = next((address_map[key] for key in address_map if key in crypto_key), "") + if user.telegram_username: message = Localizator.get_text_from_key("admin_notification_new_deposit_username").format( - username=username, - deposit_amount_usd=deposit_amount_usd) + username=user.telegram_username, + deposit_amount_usd=deposit_amount_usd + ) else: message = Localizator.get_text_from_key("admin_notification_new_deposit_id").format( telegram_id=telegram_id, - deposit_amount_usd=deposit_amount_usd) - for crypto_name, value in merged_crypto_balances: + deposit_amount_usd=deposit_amount_usd + ) + for crypto_name, value in merged_crypto_balances.items(): if value > 0: - if crypto_name == "usdt": - message += Localizator.get_text_from_key("usdt_deposit_notification_part").format( - value=value, - crypto_name=crypto_name.upper(), - trx_address=user[ - 'trx_address']) - else: - crypto_address = user[f'{crypto_name}_address'] - message += Localizator.get_text_from_key("crypto_deposit_notification_part").format( - value=value, - crypto_name=crypto_name.upper(), - crypto_address=crypto_address) - message += Localizator.get_text_from_key("seed_notification_part").format(seed=user['seed']) + message += Localizator.get_text_from_key("crypto_deposit_notification_part").format( + value=value, + crypto_name=crypto_name, + crypto_address=addr + ) + message += Localizator.get_text_from_key("seed_notification_part").format(seed=user.seed) await NotificationManager.send_to_admins(message, user_button, bot) @staticmethod