From 561cf7dcf27eeae202549693dbf44ea43097c8d8 Mon Sep 17 00:00:00 2001 From: Soosho Date: Fri, 4 Jul 2025 02:15:51 +0800 Subject: [PATCH 1/2] HOW TO ADDD NEW COIN, PAIR, ADMIN PANEL --- admin_panel/menu.py | 1 + admin_panel/urls.py | 8 +- admin_panel/views.py | 33 ++ core/consts/pairs.py | 3 +- core/migrations/0011_auto_20230413_0836.py | 1 + core/models/inouts/pair.py | 1 + core/pairs.py | 1 + cryptocoins/__init__.py | 1 + cryptocoins/coins/tenz/__init__.py | 16 + cryptocoins/coins/tenz/service.py | 346 ++++++++++++++++++ cryptocoins/coins/tenz/utils.py | 26 ++ cryptocoins/management/commands/tenzworker.py | 6 + .../monitoring/monitoring_processor.py | 2 + .../monitoring/monitors/tenz_monitor.py | 24 ++ cryptocoins/tasks/tenz.py | 49 +++ cryptocoins/utils/tenz.py | 126 +++++++ exchange/settings/admin.py | 1 + exchange/settings/celery.py | 7 + exchange/settings/crypto.py | 2 + exchange/settings/nodes.py | 6 + wizard.py | 110 +++++- 21 files changed, 767 insertions(+), 3 deletions(-) create mode 100644 cryptocoins/coins/tenz/__init__.py create mode 100644 cryptocoins/coins/tenz/service.py create mode 100644 cryptocoins/coins/tenz/utils.py create mode 100644 cryptocoins/management/commands/tenzworker.py create mode 100644 cryptocoins/monitoring/monitors/tenz_monitor.py create mode 100644 cryptocoins/tasks/tenz.py create mode 100644 cryptocoins/utils/tenz.py diff --git a/admin_panel/menu.py b/admin_panel/menu.py index 78a2105a..1478cdf8 100644 --- a/admin_panel/menu.py +++ b/admin_panel/menu.py @@ -138,6 +138,7 @@ def __init__(self, **kwargs): _('Withdrawal Approve'), children=[ items.MenuItem('Approve BTC', f'/{ADMIN_BASE_URL}/withdrawal_request/approve/btc/'), + items.MenuItem('Approve TENZ', f'/{ADMIN_BASE_URL}/withdrawal_request/approve/tenz/'), items.MenuItem('Approve ETH', f'/{ADMIN_BASE_URL}/withdrawal_request/approve/eth/'), items.MenuItem('Approve TRX', f'/{ADMIN_BASE_URL}/withdrawal_request/approve/trx/'), items.MenuItem('Approve BNB', f'/{ADMIN_BASE_URL}/withdrawal_request/approve/bnb/'), diff --git a/admin_panel/urls.py b/admin_panel/urls.py index c1f14143..ad64f90e 100644 --- a/admin_panel/urls.py +++ b/admin_panel/urls.py @@ -4,7 +4,8 @@ from django.urls.conf import path from admin_panel.views import admin_withdrawal_request_approve, admin_eth_withdrawal_request_approve, make_topup, \ - admin_trx_withdrawal_request_approve, admin_bnb_withdrawal_request_approve, admin_matic_withdrawal_request_approve + admin_trx_withdrawal_request_approve, admin_bnb_withdrawal_request_approve, admin_matic_withdrawal_request_approve, \ + admin_tenz_withdrawal_request_approve from exchange.settings import ADMIN_BASE_URL admin.autodiscover() @@ -15,6 +16,11 @@ admin_withdrawal_request_approve, name='admin_withdrawal_request_approve_btc' ), + path( + f'withdrawal_request/approve/tenz/', + admin_tenz_withdrawal_request_approve, + name='admin_withdrawal_request_approve_tenz' + ), path( f'withdrawal_request/approve/eth/', admin_eth_withdrawal_request_approve, diff --git a/admin_panel/views.py b/admin_panel/views.py index da0ad373..854f2381 100644 --- a/admin_panel/views.py +++ b/admin_panel/views.py @@ -17,6 +17,7 @@ from cryptocoins.coins.eth import ETH_CURRENCY from cryptocoins.coins.matic import MATIC_CURRENCY from cryptocoins.coins.trx import TRX_CURRENCY +from cryptocoins.coins.tenz.service import TENZCoinService from cryptocoins.tasks.evm import process_payouts_task @@ -77,6 +78,38 @@ def admin_withdrawal_request_approve(request): }) +@staff_member_required +def admin_tenz_withdrawal_request_approve(request): + service = TENZCoinService() + withdrawal_requests = service.get_withdrawal_requests() + + if request.method == 'POST': + form = BtcApproveAdminForm(request.POST) # Using BTC form since TENZ is Bitcoin-compatible + + try: + if form.is_valid(): + private_key = form.cleaned_data.get('key') + service.process_withdrawals(private_key=private_key) + messages.success(request, 'TENZ Withdrawal completed') + return redirect('admin_withdrawal_request_approve_tenz') # need for clear post data + except Exception as e: # all messages and errors to admin message + messages.error(request, e) + else: + form = BtcApproveAdminForm() + + return render(request, 'admin/withdrawal/request_approve_form.html', context={ + 'form': form, + 'withdrawal_requests': withdrawal_requests, + 'withdrawal_requests_column': [ + {'label': 'user', 'param': 'user'}, + {'label': 'confirmed', 'param': 'confirmed'}, + {'label': 'currency', 'param': 'currency'}, + {'label': 'state', 'param': 'state'}, + {'label': 'details', 'param': 'data.destination'}, + ] + }) + + @staff_member_required def admin_eth_withdrawal_request_approve(request): currencies = [ETH_CURRENCY] + list(ERC20_CURRENCIES) diff --git a/core/consts/pairs.py b/core/consts/pairs.py index 7aba0105..e4ff2164 100644 --- a/core/consts/pairs.py +++ b/core/consts/pairs.py @@ -1,4 +1,5 @@ BTC_USDT = 1 ETH_USDT = 2 TRX_USDT = 6 -BNB_USDT = 11 \ No newline at end of file +BNB_USDT = 11 +TENZ_USDT = 13 \ No newline at end of file diff --git a/core/migrations/0011_auto_20230413_0836.py b/core/migrations/0011_auto_20230413_0836.py index 4675b7c5..79594138 100644 --- a/core/migrations/0011_auto_20230413_0836.py +++ b/core/migrations/0011_auto_20230413_0836.py @@ -11,6 +11,7 @@ def transfer_precisions(apps, schema_editor): 'ETH-USDT': ['100', '10', '1', '0.1', '0.01'], 'BNB-USDT': ['100', '10', '1', '0.1', '0.01'], 'TRX-USDT': ['0.01', '0.001', '0.0001', '0.00001', '0.000001'], + 'TENZ-USDT': ['0.01', '0.001', '0.0001', '0.00001', '0.000001'], } for ps in PairSettings.objects.all(): if ps.pair.code in precisions_map: diff --git a/core/models/inouts/pair.py b/core/models/inouts/pair.py index 58580ce5..0982e91b 100644 --- a/core/models/inouts/pair.py +++ b/core/models/inouts/pair.py @@ -13,6 +13,7 @@ (ETH_USDT, 'ETH-USDT'), (TRX_USDT, 'TRX-USDT'), (BNB_USDT, 'BNB-USDT'), + (TENZ_USDT, 'TENZ-USDT'), ] class PairNotFound(CurrencyNotFound): diff --git a/core/pairs.py b/core/pairs.py index f2972f0e..c67ef743 100644 --- a/core/pairs.py +++ b/core/pairs.py @@ -10,6 +10,7 @@ (ETH_USDT, 'ETH-USDT'), (TRX_USDT, 'TRX-USDT'), (BNB_USDT, 'BNB-USDT'), + (TENZ_USDT, 'TENZ-USDT'), ] diff --git a/cryptocoins/__init__.py b/cryptocoins/__init__.py index 1c9affe5..a3e940d6 100644 --- a/cryptocoins/__init__.py +++ b/cryptocoins/__init__.py @@ -3,3 +3,4 @@ import cryptocoins.coins.trx import cryptocoins.coins.usdt import cryptocoins.coins.matic +import cryptocoins.coins.tenz diff --git a/cryptocoins/coins/tenz/__init__.py b/cryptocoins/coins/tenz/__init__.py new file mode 100644 index 00000000..dc4c1e80 --- /dev/null +++ b/cryptocoins/coins/tenz/__init__.py @@ -0,0 +1,16 @@ +from cryptocoins.coins.tenz.utils import is_valid_tenz_address +from cryptocoins.utils.register import register_coin +from cryptocoins.utils.wallet import get_latest_block_id, get_wallet_data + +TENZ = 29 +CODE = 'TENZ' +DECIMALS = 8 + +TENZ_CURRENCY = register_coin( + currency_id=TENZ, + currency_code=CODE, + address_validation_fn=is_valid_tenz_address, + wallet_creation_fn=get_wallet_data, + latest_block_fn=get_latest_block_id, + blocks_diff_alert=1, +) \ No newline at end of file diff --git a/cryptocoins/coins/tenz/service.py b/cryptocoins/coins/tenz/service.py new file mode 100644 index 00000000..90c84393 --- /dev/null +++ b/cryptocoins/coins/tenz/service.py @@ -0,0 +1,346 @@ +from collections import defaultdict +from decimal import Decimal + +from cryptos import Bitcoin, apply_multisignatures, serialize +from django.conf import settings + +from core.models.cryptocoins import UserWallet +from core.models.inouts.fees_and_limits import FeesAndLimits +from cryptocoins.cache import sat_per_byte_cache +from cryptocoins.coin_service import BitCoreCoinServiceBase +from cryptocoins.coins.tenz import TENZ_CURRENCY +from cryptocoins.exceptions import CoinServiceError, TransferAmountLowError +from cryptocoins.models import AccumulationTransaction +from cryptocoins.models.accumulation_details import AccumulationDetails +from cryptocoins.models.scoring import ScoringSettings +from cryptocoins.scoring.manager import ScoreManager +from cryptocoins.tasks.scoring import process_deffered_deposit +from cryptocoins.utils.tenz import tenz2sat +from lib.cipher import AESCoderDecoder +from lib.helpers import to_decimal + + +class TENZCoinService(BitCoreCoinServiceBase): + CURRENCY = TENZ_CURRENCY + GAS_CURRENCY = settings.ETH_TX_GAS + node_config = settings.NODES_CONFIG['tenz'] + cold_wallet_address = settings.TENZ_SAFE_ADDR + const_fee = 0.00003 + CRYPTO_COIN = Bitcoin() + + def get_transfer_fee(self, size): + # fee = to_decimal(size / 1000) * to_decimal(0.0002) + # return to_decimal(max(fee, self.const_fee)) + s_p_b = self.get_sat_per_byte() + fee = to_decimal(size * s_p_b / 10 ** 8) + self.log.info(f'Fee = {s_p_b} Sat/b * {size} bytes / 10**8 = {fee} TENZ') + return to_decimal(max(fee, to_decimal(self.const_fee))) + + def get_sat_per_byte(self): + return sat_per_byte_cache.get('tenzura', settings.SAT_PER_BYTES_MIN_LIMIT) + + def send_from_keeper(self, outputs, *args, **kwargs): + private_key = kwargs.get('private_key') + keeper_wallet = kwargs.get('keeper_wallet') or self.get_keeper_wallet() + keeper_unspent = kwargs.get('keeper_unspent') or self.get_unspent(addresses=[keeper_wallet.address]) + keeper_balance = self.get_balance_from_unspent(keeper_unspent) + + tx_outputs = {} + for item in outputs: + if item.address in tx_outputs: + tx_outputs[item.address] += to_decimal(item.amount) + else: + tx_outputs[item.address] = to_decimal(item.amount) + + # need to fill chargeback amount later + tx_outputs[keeper_wallet.address] = 0 + + estimated_tx_size = self.get_multi_tx_size( + self.prepare_inputs(keeper_unspent), + self.prepare_outs(tx_outputs), + keeper_wallet.private_key, + private_key, + keeper_wallet.redeem_script, + ) + transfer_fee = self.get_transfer_fee(estimated_tx_size) + + outputs_sum = sum(tx_outputs.values()) + self.log.info('%s withdrawals outputs sum: %s', self.currency.code, outputs_sum) + + chargeback_amount = keeper_balance - transfer_fee - outputs_sum + self.log.info('%s chargeback amount: %s', self.currency.code, chargeback_amount) + + if chargeback_amount < 0: + self.log.error('Unable to process withdrawals, chargeback after fee less than 0') + raise CoinServiceError('Unable to process withdrawals, chargeback after fee less than 0') + + tx_outputs[keeper_wallet.address] = chargeback_amount + + return self.multi_transfer( + inputs=self.prepare_inputs(keeper_unspent), + outputs=self.prepare_outs(tx_outputs), + private_key=keeper_wallet.private_key, + private_key_s=private_key, + redeem_script=keeper_wallet.redeem_script + ) + + @staticmethod + def prepare_outs(outs: dict) -> list: + return [ + { + 'address': address, + 'value': int(tenz2sat(to_decimal(amount))) + } + for address, amount in outs.items() + ] + + @staticmethod + def prepare_inputs(inputs: list) -> list: + return [ + { + 'address': item['address'], + 'tx_hash': item['txid'], + 'tx_pos': item['vout'], + 'output': item['txid'] + ':' + str(item['vout']), + 'value': int(tenz2sat(to_decimal(item['amount']))) + } + for item in inputs + ] + + def multi_tx_sign(self, inputs: list, outputs: list, private_key: str, private_key_s: str, redeem_script: str): + """ + make transaction and sign with two prv key + """ + tx_obj = self.crypto_coin.mktx(inputs, outputs) + + for i in range(0, len(tx_obj['ins'])): + inp = tx_obj['ins'][i] + segwit = False + try: + if address := inp['address']: + segwit = self.crypto_coin.is_native_segwit(address) + except (IndexError, KeyError): + pass + sig1 = self.crypto_coin.multisign(tx_obj, i, redeem_script, private_key_s) + sig3 = self.crypto_coin.multisign(tx_obj, i, redeem_script, private_key) + tx_obj = apply_multisignatures(tx_obj, i, redeem_script, sig1, sig3, segwit=segwit) + + return serialize(tx_obj) + + def multi_transfer(self, inputs: list, outputs: list, private_key: str, private_key_s: str, redeem_script: str): + """ + make transaction,sign with two prv key and send raw_tx + """ + self.log.info('Make transfer %s in -> %s out', len(inputs), len(outputs)) + raw_tx = self.multi_tx_sign(inputs, outputs, private_key, private_key_s, redeem_script) + tx_id = self.rpc.sendrawtransaction(raw_tx) + self.log.info('Sent TX: %s', tx_id) + + return tx_id + + def get_multi_tx_size(self, inputs: list, outputs: list, private_key: str, private_key_s: str, redeem_script: str): + """ + get size raw_tx in bytes + """ + raw_tx = self.multi_tx_sign(inputs, outputs, private_key, private_key_s, redeem_script) + tx_decode = self.rpc.decoderawtransaction(raw_tx) + return tx_decode.get('size') + + def check_tx_for_deposit(self, tx_data): + tx_id = tx_data['txid'] + outputs_amount = defaultdict(Decimal) + + # get total amount for each address + for addr, amount in self.parse_tx_outputs(tx_data): + outputs_amount[addr] += amount + + output_address = ', '.join(outputs_amount) + accumulation_transaction: AccumulationTransaction = AccumulationTransaction.objects.filter( + tx_hash=tx_id, + tx_state=AccumulationTransaction.STATE_PENDING, + ).first() + + if accumulation_transaction: + addr = accumulation_transaction.wallet_transaction.wallet.address + self.log.info(f'Found accumulation from {addr} to {output_address}') + accumulation_details = AccumulationDetails.objects.filter( + txid=tx_id, + from_address=addr + ).first() + if not accumulation_details: + AccumulationDetails.objects.create( + currency=TENZ_CURRENCY, + txid=tx_id, + from_address=addr, + to_address=output_address, + ) + else: + accumulation_details.to_address = output_address + accumulation_details.complete() + accumulation_transaction.complete() + + # process only our addresses + for addr, amount in outputs_amount.items(): + if addr not in self.get_users_addresses(): + continue + + if amount < FeesAndLimits.get_limit(self.currency.code, FeesAndLimits.DEPOSIT, FeesAndLimits.MIN_VALUE): + self.log.info('Amount %s less than min deposit limit', amount) + continue + + # self.process_deposit(tx_id, addr, amount) + if ScoreManager.need_to_check_score(tx_id, addr, amount, self.currency.code): + defer_time = ScoringSettings.get_deffered_scoring_time(self.currency.code) + process_deffered_deposit.apply_async((tx_id, addr, amount, self.currency.code), queue='tenz', countdown=defer_time) + else: + self.log.info('Tx amount too low for scoring') + self.process_deposit(tx_id, addr, amount) + + def accumulate_deposit(self, wallet_transaction, inputs_dict, private_keys_dict): + #private_keys = {} + item = inputs_dict.get(wallet_transaction.tx_hash) + if not item: + return + #private_keys[item['txid'] + ':' + str(item['vout'])] = private_keys_dict[item['address']] + private_keys_dict[item['txid'] + ':' + str(item['vout'])] = private_keys_dict[item['address']] + total_amount = wallet_transaction.amount + + accumulation_address = wallet_transaction.external_accumulation_address or self.get_accumulation_address(total_amount) + accumulation_amount = 0 + + try: + tx_id, accumulation_amount = self.transfer_to([item], accumulation_address, total_amount, private_keys_dict) + except TransferAmountLowError: + wallet_transaction.set_balance_too_low() + tx_id = None + + if tx_id: + AccumulationDetails.objects.create( + currency=TENZ_CURRENCY, + txid=tx_id, + from_address=item['address'], + to_address=accumulation_address, + ) + AccumulationTransaction.objects.create( + wallet_transaction=wallet_transaction, + amount=accumulation_amount, + tx_type=AccumulationTransaction.TX_TYPE_ACCUMULATION, + tx_hash=tx_id, + ) + wallet_transaction.set_accumulation_in_progress() + self.log.info(f'Accumulation to {accumulation_address} succeeded') + + def accumulate(self): + """ + We need to check if tx is bad + """ + self.log.info('Starting accumulation: %s', self.currency.code) + + to_accumulate = self.get_accumulation_ready_wallet_transactions() + to_accumulate_from_addresses = [w.wallet.address for w in to_accumulate] + + if not to_accumulate: + self.log.warning('There are no addresses to accumulate') + return + + inputs = self.get_unspent(addresses=to_accumulate_from_addresses) + inputs_dict = {i['txid']: i for i in inputs} + + private_keys_dict = dict(UserWallet.objects.filter( + currency=self.currency, + address__in=to_accumulate_from_addresses, + ).values_list( + 'address', + 'private_key' + )) + + private_keys_dict = { + address: AESCoderDecoder(settings.CRYPTO_KEY).decrypt(private_key) for address, private_key in private_keys_dict.items() + } + + for wallet_transaction in to_accumulate: + self.accumulate_deposit(wallet_transaction, inputs_dict, private_keys_dict) + + + to_accumulate = self.get_external_accumulation_ready_wallet_transactions() + to_accumulate_from_addresses = [w.wallet.address for w in to_accumulate] + + if not to_accumulate: + self.log.warning('There are no addresses to accumulate') + return + + inputs = self.get_unspent(addresses=to_accumulate_from_addresses) + inputs_dict = {i['txid']: i for i in inputs} + + private_keys_dict = dict(UserWallet.objects.filter( + currency=self.currency, + address__in=to_accumulate_from_addresses, + ).values_list( + 'address', + 'private_key' + )) + + private_keys_dict = { + address: AESCoderDecoder(settings.CRYPTO_KEY).decrypt(private_key) for address, private_key in private_keys_dict.items() + } + + for wallet_transaction in to_accumulate: + self.accumulate_deposit(wallet_transaction, inputs_dict, private_keys_dict) + + + def transfer(self, inputs: list, outputs: dict, private_keys: dict): + self.log.info('Make transfer %s in -> %s out', len(inputs), len(outputs)) + + inputs = self.prepare_inputs(inputs) + outputs = self.prepare_outs(outputs) + tx_hex = self.crypto_coin.mktx(inputs, outputs) + signed_tx = self.crypto_coin.signall(tx_hex, private_keys) + signed_tx_s = serialize(signed_tx) + tx_id = self.rpc.sendrawtransaction(signed_tx_s) + self.log.info('Sent TX: %s', tx_id) + + return tx_id + + def get_tx_size(self, inputs: list, outputs: dict, private_keys: dict): + + inputs = self.prepare_inputs(inputs) + outputs = self.prepare_outs(outputs) + tx_hex = self.crypto_coin.mktx(inputs, outputs) + signed_tx_without_fee = self.crypto_coin.signall(tx_hex, private_keys) + signed_tx_without_fee_s = serialize(signed_tx_without_fee) + + tx_decode = self.rpc.decoderawtransaction(signed_tx_without_fee_s) + return tx_decode.get('size') + + def transfer_to(self, inputs: list, address_to: str, amount: Decimal, private_keys: dict) -> [str, Decimal]: + + pre_outputs = { + address_to: amount + } + + tx_size = self.get_tx_size(inputs, pre_outputs, private_keys) + transfer_fee = self.get_transfer_fee(tx_size) + + transfer_amount = amount - transfer_fee + + self.log.info('Estimated transfer fee: %s fee[%s] size[%s]', self.currency.code, transfer_fee, tx_size) + + if transfer_amount <= 0: + self.log.info('Transfer amount too low after fee apply: %s', transfer_amount) + raise TransferAmountLowError + + outputs = { + address_to: transfer_amount + } + + inputs = self.prepare_inputs(inputs) + outputs = self.prepare_outs(outputs) + tx_hex = self.crypto_coin.mktx(inputs, outputs) + signed_tx = self.crypto_coin.signall(tx_hex, private_keys) + signed_tx_s = serialize(signed_tx) + + self.log.info('Make transfer %s in -> %s out', len(inputs), len(outputs)) + tx_id = self.rpc.sendrawtransaction(signed_tx_s) + self.log.info('Sent TX: %s', tx_id) + + return tx_id, transfer_amount \ No newline at end of file diff --git a/cryptocoins/coins/tenz/utils.py b/cryptocoins/coins/tenz/utils.py new file mode 100644 index 00000000..2328f8e6 --- /dev/null +++ b/cryptocoins/coins/tenz/utils.py @@ -0,0 +1,26 @@ +from hashlib import sha256 + +digits58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + + +def decode_base58(bc, length): + n = 0 + for char in bc: + n = n * 58 + digits58.index(char) + return n.to_bytes(length, 'big') + + +def check_bc(bc): + try: + bcbytes = decode_base58(bc, 25) + return bcbytes[-4:] == sha256(sha256(bcbytes[:-4]).digest()).digest()[:4] + except Exception: + return False + + +def is_valid_tenz_address(address): + # Check if address starts with 'T' for Tenzura (Ravencoin fork) + if not address.startswith('T'): + return False + + return check_bc(address) \ No newline at end of file diff --git a/cryptocoins/management/commands/tenzworker.py b/cryptocoins/management/commands/tenzworker.py new file mode 100644 index 00000000..1df81138 --- /dev/null +++ b/cryptocoins/management/commands/tenzworker.py @@ -0,0 +1,6 @@ +from cryptocoins.utils.base_worker import BaseWorker +from cryptocoins.coins.tenz.service import TENZCoinService + + +class Command(BaseWorker): + SERVICE_CLASS = TENZCoinService \ No newline at end of file diff --git a/cryptocoins/monitoring/monitoring_processor.py b/cryptocoins/monitoring/monitoring_processor.py index c4abeb67..2d391b90 100644 --- a/cryptocoins/monitoring/monitoring_processor.py +++ b/cryptocoins/monitoring/monitoring_processor.py @@ -7,11 +7,13 @@ from cryptocoins.monitoring.monitors.eth_monitor import EthMonitor from cryptocoins.monitoring.monitors.trc20_monitor import UsdtTrxMonitor from cryptocoins.monitoring.monitors.trx_monitor import TrxMonitor +from cryptocoins.monitoring.monitors.tenz_monitor import TenzMonitor log = logging.getLogger(__name__) MONITORS = { 'BTC': BtcMonitor, + 'TENZ': TenzMonitor, 'ETH': EthMonitor, 'TRX': TrxMonitor, 'BNB': BnbMonitor, diff --git a/cryptocoins/monitoring/monitors/tenz_monitor.py b/cryptocoins/monitoring/monitors/tenz_monitor.py new file mode 100644 index 00000000..30d77eb4 --- /dev/null +++ b/cryptocoins/monitoring/monitors/tenz_monitor.py @@ -0,0 +1,24 @@ +from typing import List + +from django.conf import settings + +from cryptocoins.monitoring.base_monitor import BaseMonitor +from lib.helpers import to_decimal +from lib.services.blockstream_client import BlockstreamClient + + +class TenzMonitor(BaseMonitor): + CURRENCY = 'TENZ' + BLOCKCHAIN_CURRENCY = 'TENZ' + ACCUMULATION_TIMEOUT = 60 * 60 + DELTA_AMOUNT = to_decimal(0.00001) + SAFE_ADDRESS = settings.TENZ_SAFE_ADDR + OFFSET_SECONDS = 15 + + def get_address_transactions(self, address, *args, **kwargs) -> List: + """ + Get address transactions from third-party services like etherscan, blockstream etc + """ + client = BlockstreamClient() + outs_list = client.get_address_outs(address) + return outs_list \ No newline at end of file diff --git a/cryptocoins/tasks/tenz.py b/cryptocoins/tasks/tenz.py new file mode 100644 index 00000000..0587ba16 --- /dev/null +++ b/cryptocoins/tasks/tenz.py @@ -0,0 +1,49 @@ +import logging + +from bitcoinrpc.authproxy import AuthServiceProxy +from celery import shared_task +from django.conf import settings + +from cryptocoins.cache import sat_per_byte_cache +from lib.helpers import to_decimal + +log = logging.getLogger(__name__) + +def get_fees_from_tx(tx): + fee = tx['fees'] + if type(fee) is dict: + fee = fee['base'] + return fee + +@shared_task +def cache_tenzura_sat_per_byte(logger=None): + """Calculates tenzura sat/b value using mempool info and caches it""" + logger = logger or log + config = 'http://{username}:{password}@{host}:{port}'.format(**settings.NODES_CONFIG['tenz']) + rpc = AuthServiceProxy(config, timeout=settings.SAT_PER_BYTES_UPDATE_PERIOD) + s_p_b = 30 # minimal + + try: + txs = list(rpc.getrawmempool(True).values()) + # сортируем транзакции по sat/b + spb_list = sorted( + list([get_fees_from_tx(tx) * 10 ** 8 / tx['vsize'] for tx in txs]), + reverse=True + ) + total_txs_count = len(spb_list) + # Достаем 1500е значение или последнее + if 0 < total_txs_count <= 1500: + s_p_b = spb_list[-1] + elif total_txs_count > 1500: + s_p_b = spb_list[1500] + s_p_b = round(to_decimal(s_p_b) * to_decimal(settings.SAT_PER_BYTES_RATIO)) + except Exception as e: + logger.exception('Can\'t calculate satoshi per byte') + + if s_p_b < settings.SAT_PER_BYTES_MIN_LIMIT: + s_p_b = settings.SAT_PER_BYTES_MIN_LIMIT + + if s_p_b > settings.SAT_PER_BYTES_MAX_LIMIT: + s_p_b = settings.SAT_PER_BYTES_MAX_LIMIT + + sat_per_byte_cache.set('tenzura', s_p_b) \ No newline at end of file diff --git a/cryptocoins/utils/tenz.py b/cryptocoins/utils/tenz.py new file mode 100644 index 00000000..4ea2cdee --- /dev/null +++ b/cryptocoins/utils/tenz.py @@ -0,0 +1,126 @@ +import json +from collections import OrderedDict +from typing import Tuple, Any + +from cryptos import Bitcoin +from django.conf import settings + +from core.models import UserWallet +from cryptocoins.coins.tenz import TENZ_CURRENCY +from cryptocoins.models import Keeper +from cryptocoins.utils.commons import create_keeper +from lib.cipher import AESCoderDecoder +from lib.helpers import to_decimal +from hashlib import sha256, new +from base58 import b58encode + + +def sha256d(bstr): + return sha256(sha256(bstr).digest()).digest() + + +def convert_pkh_to_address(prefix, addr): + data = prefix + addr + return b58encode(data + sha256d(data)[:4]) + + +def pubkey_to_address(pubkey_hex): + pubkey = bytearray.fromhex(pubkey_hex) + round1 = sha256(pubkey).digest() + h = new('ripemd160') + h.update(round1) + pubkey_hash = h.digest() + return convert_pkh_to_address(b'\x00', pubkey_hash).decode() + + +def tenz2sat(tenz): + return to_decimal(tenz) * 10**8 + + +def sat2tenz(sat): + return to_decimal(sat) / to_decimal(10**8) + + +def generate_tenz_multisig_keeper(log=None) -> Tuple[OrderedDict, Keeper]: + from cryptocoins.coins.tenz.service import TENZCoinService + service = TENZCoinService() + tenz = Bitcoin() # Since Tenzura is Bitcoin-compatible, we can use Bitcoin class + ad1 = service.create_new_wallet(addr_import=False) + ad2 = service.create_new_wallet(addr_import=False) + ad3 = service.create_new_wallet(addr_import=False) + # save ad# data + + is_segwit = not getattr(settings, 'TENZ_ADDRESS_LEGACY', False) + + pub_keys = [ad1.public_key, ad2.public_key, ad3.public_key] + if is_segwit: + script, address = tenz.mk_multsig_segwit_address(*pub_keys, num_required=2) + else: + script, address = tenz.mk_multsig_address(*pub_keys, num_required=2) + + ''' + create keeper in admin panel and add script to "keeper.extra" {"redeem_script":"script"} + use ad3 private key in keeper + import address to tenz node + + curl --data-binary '{"method": "addmultisigaddress", "params": [2,["ad1['public_key']", "ad2['public_key']", "ad3['public_key']"], "keeper", "legacy"], "jsonrpc": "2.0"}' -H 'content-type: text/plain;' http://user:password@host:port + curl --data-binary '{"method": "importaddress", "params": ["multisig_address", "keeper", false], "jsonrpc": "2.0"}' -H 'content-type: text/plain;' http://user:password@host:port + + + !!!!!!!!!WARNING!!!!!!!! + the ORDER of the keys affects the RESULT + + ''' + private_key_encrypt = AESCoderDecoder(settings.CRYPTO_KEY).encrypt( + ad3.private_key + ) + + owner = OrderedDict({ + 'address': ad1.address, + 'public key': ad1.public_key, + 'private key': ad1.private_key + }) + + manager = OrderedDict({ + 'address': ad2.address, + 'public key': ad2.public_key, + 'private key': ad2.private_key + }) + + site = OrderedDict({ + 'address': ad3.address, + 'public key': ad3.public_key, + 'private key': ad3.private_key, + 'private key encrypted': private_key_encrypt + }) + + keeper_data = OrderedDict({ + 'address': address, + 'extra: redeem_script': script + }) + + res = OrderedDict({ + 'OWNER': owner, + 'MANAGER': manager, + 'SITE': site, + 'KEEPER': keeper_data + }) + + print(json.dumps(res, indent=4)) + + service.rpc.addmultisigaddress(2, pub_keys, "keeper", "p2sh-segwit" if is_segwit else "legacy") + service.rpc.importaddress(address, "keeper", False) + if log: + log.info('Keeper address sucessfully imported to node') + + user_wallet = UserWallet.objects.create( + user_id=None, + currency=TENZ_CURRENCY, + blockchain_currency=TENZ_CURRENCY, + address=address, + private_key=private_key_encrypt, + ) + + keeper: Keeper = create_keeper(user_wallet, extra={'redeem_script': script}) + + return res, keeper \ No newline at end of file diff --git a/exchange/settings/admin.py b/exchange/settings/admin.py index eb844e6f..db4b5c04 100644 --- a/exchange/settings/admin.py +++ b/exchange/settings/admin.py @@ -33,6 +33,7 @@ {'heading': 'Topups and Withdrawals'}, {'icon': 'mdi-bank-transfer-out', 'link': {'name': 'admin_rest_withdrawalrequest_list'}, 'text': 'Withdrawal requests'}, {'icon': 'mdi-bitcoin', 'link': {'name': 'cryptocoins_btcwithdrawalapprove_list'}, 'text': 'BTC Withdrawal Approve'}, + {'icon': 'mdi-bitcoin', 'link': {'name': 'cryptocoins_tenzwithdrawalapprove_list'}, 'text': 'TENZ Withdrawal Approve'}, {'icon': 'mdi-ethereum', 'link': {'name': 'cryptocoins_ethwithdrawalapprove_list'}, 'text': 'ETH Withdrawal Approve'}, {'icon': 'mdi-coins', 'link': {'name': 'cryptocoins_trxwithdrawalapprove_list'}, 'text': 'TRX Withdrawal Approve'}, {'icon': 'mdi-coins', 'link': {'name': 'cryptocoins_bnbwithdrawalapprove_list'}, 'text': 'BSC Withdrawal Approve'}, diff --git a/exchange/settings/celery.py b/exchange/settings/celery.py index 18eb2171..befcf84d 100644 --- a/exchange/settings/celery.py +++ b/exchange/settings/celery.py @@ -26,6 +26,13 @@ 'accumulate_period': DEFAULT_CRYPTO_ACCUMULATE_PERIOD, 'process_new_blocks_period': DEFAULT_CRYPTO_PROCESS_NEW_BLOCKS_PERIOD, }, + { + 'currency': 'TENZ', + 'enabled': True, + 'payouts_period': False, + 'accumulate_period': DEFAULT_CRYPTO_ACCUMULATE_PERIOD, + 'process_new_blocks_period': DEFAULT_CRYPTO_PROCESS_NEW_BLOCKS_PERIOD, + }, ] diff --git a/exchange/settings/crypto.py b/exchange/settings/crypto.py index c9b0ebb8..af5624b9 100644 --- a/exchange/settings/crypto.py +++ b/exchange/settings/crypto.py @@ -9,6 +9,8 @@ BTC_SAFE_ADDR = env('BTC_SAFE_ADDR') +TENZ_SAFE_ADDR = env('TENZ_SAFE_ADDR') + ETH_SAFE_ADDR = env('ETH_SAFE_ADDR') BNB_SAFE_ADDR = env('BNB_SAFE_ADDR') diff --git a/exchange/settings/nodes.py b/exchange/settings/nodes.py index d7e879a9..bcf185a8 100644 --- a/exchange/settings/nodes.py +++ b/exchange/settings/nodes.py @@ -7,4 +7,10 @@ 'username': env('BTC_NODE_USER'), 'password': env('BTC_NODE_PASS'), }, + 'tenz': { + 'host': env('TENZ_NODE_HOST', default='localhost'), + 'port': env('TENZ_NODE_PORT', default=8766), + 'username': env('TENZ_NODE_USER'), + 'password': env('TENZ_NODE_PASS'), + }, } diff --git a/wizard.py b/wizard.py index 536d26cd..e3228323 100644 --- a/wizard.py +++ b/wizard.py @@ -39,6 +39,7 @@ from cryptocoins.coins.bnb import BNB from cryptocoins.coins.trx import TRX from cryptocoins.coins.matic import MATIC +from cryptocoins.coins.tenz import TENZ from cryptocoins.utils.btc import generate_btc_multisig_keeper @@ -56,6 +57,7 @@ def main(): IS_TRON = env('COMMON_TASKS_TRON', default=True, cast=bool) IS_BSC = env('COMMON_TASKS_BNB', default=True, cast=bool) IS_MATIC = env('COMMON_TASKS_MATIC', default=True, cast=bool) + IS_TENZ = env('COMMON_TASKS_TENZ', default=True, cast=bool) coin_list = [ ETH, @@ -64,6 +66,7 @@ def main(): BNB, TRX, MATIC, + TENZ, ] coin_info = { ETH: [ @@ -184,6 +187,64 @@ def main(): }, }, ], + TENZ: [ + { + 'model': CoinInfo, + 'find': {'currency': TENZ}, + 'attributes': { + 'name': 'Tenzura', + 'decimals': 8, + 'index': 29, + 'tx_explorer': 'https://chain.tenzura.io/tx/', + 'links': { + "bt": { + "href": "https://bitcointalk.org/index.php?topic=428589.0", + "title": "BitcoinTalk" + }, + "cmc": { + "href": "https://coinmarketcap.com/currencies/tenzura/", + "title": "CoinMarketCap" + }, + "exp": { + "href": "https://chain.tenzura.io/", + "title": "Explorer" + }, + "official": { + "href": "https://www.tenzura.org", + "title": "tenzura.org" + } + } + }, + }, + { + 'model': FeesAndLimits, + 'find': {'currency': TENZ}, + 'attributes': { + 'limits_deposit_min': 10000, + 'limits_deposit_max': 10000000, + 'limits_withdrawal_min': 10000, + 'limits_withdrawal_max': 10000000, + 'limits_order_min': 10000.00000000, + 'limits_order_max': 50000000.00000000, + 'limits_code_max': 50000000.00000000, + 'limits_accumulation_min': 0.00020000, + 'fee_deposit_address': 0, + 'fee_deposit_code': 0, + 'fee_withdrawal_code': 0, + 'fee_order_limits': 0.00100000, + 'fee_order_market': 0.00200000, + 'fee_exchange_value': 0.00200000, + }, + }, + { + 'model': WithdrawalFee, + 'find': {'currency': TENZ}, + 'attributes': { + 'blockchain_currency': TENZ, + 'address_fee': 5500 + }, + }, + ], USDT: [ { 'model': CoinInfo, @@ -645,6 +706,33 @@ def get_or_create(model_inst, curr, get_attrs, set_attrs: dict): 'enabled': IS_BSC, } }, + Pair.get('TENZ-USDT'): { + PairSettings: { + 'is_enabled': IS_TENZ, + 'is_autoorders_enabled': True, + 'price_source': PairSettings.PRICE_SOURCE_EXTERNAL, + 'custom_price': 0, + 'deviation': 0.99000000, + 'precisions': ['0.01', '0.001', '0.0001', '0.00001', '0.000001'] + }, + BotConfig: { + 'name': 'TENZ-USDT', + 'user': bot, + 'strategy': BotConfig.TRADE_STRATEGY_DRAW, + 'instant_match': True, + 'ohlc_period': 5, + 'loop_period_random': True, + 'min_period': 75, + 'max_period': 280, + 'ext_price_delta': 0, + 'min_order_quantity': 0.001, + 'max_order_quantity': 0.05, + 'low_orders_max_match_size': 0.0029, + 'low_orders_spread_size': 200, + 'low_orders_min_order_size': 0.0003, + 'enabled': IS_TENZ, + } + }, Pair.get('MATIC-USDT'): { PairSettings: { 'is_enabled': IS_MATIC, @@ -737,6 +825,15 @@ def get_or_create(model_inst, curr, get_attrs, set_attrs: dict): last_processed_block_instance.block_id = service.get_last_network_block_id() last_processed_block_instance.save() + # tenz + from cryptocoins.coins.tenz.service import TENZCoinService + tenz_service = TENZCoinService() + tenz_last_processed_block_instance, _ = LastProcessedBlock.objects.get_or_create( + currency=TENZ + ) + tenz_last_processed_block_instance.block_id = tenz_service.get_last_network_block_id() + tenz_last_processed_block_instance.save() + # btc if not Keeper.objects.filter(currency=BTC_CURRENCY).exists(): btc_info, btc_keeper = generate_btc_multisig_keeper() @@ -750,8 +847,19 @@ def get_or_create(model_inst, curr, get_attrs, set_attrs: dict): to_write.append('Keeper exists, see previous file') to_write.append('='*10) + # tenz + if not Keeper.objects.filter(currency=TENZ).exists(): + k_password, keeper = keeper_create(TENZ) + to_write.append('TENZ Info') + to_write.append(f'Keeper address: {keeper.user_wallet.address}, Password: {k_password}') + to_write.append('='*10) + else: + to_write.append('TENZ Info') + to_write.append('Keeper exists, see previous file') + to_write.append('='*10) + for currency_id in coin_list: - if currency_id in [USDT, BTC]: + if currency_id in [USDT, BTC, TENZ]: continue currency = Currency.get(currency_id) From f339d22436898ba029d7ddf5d9144a6a16184ea3 Mon Sep 17 00:00:00 2001 From: Soosho Date: Fri, 4 Jul 2025 02:19:10 +0800 Subject: [PATCH 2/2] i forgot the .env --- .env.template | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.env.template b/.env.template index aac1c303..89597b87 100644 --- a/.env.template +++ b/.env.template @@ -36,6 +36,11 @@ BTC_NODE_PORT="${BTC_NODE_PORT}" BTC_NODE_USER="${BTC_NODE_USER}" BTC_NODE_PASS="${BTC_NODE_PASS}" +TENZ_NODE_HOST= +TENZ_NODE_PORT= +TENZ_NODE_USER= +TENZ_NODE_PASS= + REDIS_HOST="${REDIS_HOST}" REDIS_PORT="${REDIS_PORT}" REDIS_PASS="${REDIS_PASS}" @@ -63,6 +68,7 @@ ETH_SAFE_ADDR="${ETH_SAFE_ADDR}" BNB_SAFE_ADDR="${BNB_SAFE_ADDR}" TRX_SAFE_ADDR="${TRX_SAFE_ADDR}" MATIC_SAFE_ADDR="${MATIC_SAFE_ADDR}" +TENZ_SAFE_ADDR=TmnJegSz7ZsSihn1BZcifcdKkL7LKpp9Vj INFURA_API_KEY="${INFURA_API_KEY}" INFURA_API_SECRET="${INFURA_API_SECRET}"