def check_src_address_balance(self, **kwargs) -> bool: token_name = kwargs['token_name'] from_addr = kwargs['from_addr'] # to_addr = kwargs['to_addr'] amount = kwargs['amount'] # 特别注意 amount = round_down(amount) pro_id = kwargs['pro_id'] project = self.session.query(Project).filter_by(pro_id=pro_id).first() assert isinstance(project, Project), f'not found pro_id:{pro_id} in project' btc_withdraw_cfg = self.session.query(WithdrawConfig).filter_by( pro_id=pro_id, token_name='BTC').first() assert isinstance( btc_withdraw_cfg, WithdrawConfig), f'not found pro_id:{pro_id} in withdraw_cfg' sms_content = '' sms_template = '【shbao】 尊敬的管理员,余额预警。{0}出币地址{1}余额为{2},请立即充值{3}。{4},{5}' + f',{project.pro_name}' try: assert self.proxy.ping() == True, 'bitcoind rpc is gone' # 获取余额(包含未确认的收币, 减去未确认的出币) balance_in_satoshi = self.proxy.get_balance(address=from_addr, mem_spent=True, mem_recv=True) balance_in_btc = round_down( Decimal(balance_in_satoshi) / Decimal(10**8)) decim_balance = balance_in_btc if decim_balance < amount + BTC_TX_FEE: self.logger.error( f'BTC balance {decim_balance} < {amount + BTC_TX_FEE}') sms_content = sms_template.format('BTC', 'BTC', decim_balance, 'BTC', str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) if decim_balance < btc_withdraw_cfg.balance_threshold_to_sms: sms_content = sms_template.format('BTC', 'BTC', decim_balance, 'BTC', str(datetime.now()), ENV_NAME.upper()) except BadConnectionError as e: self.logger.error(f'connection BTC API server error: {e}') raise HttpConnectionError(e) except BalanceNotEnoughException as e: tel_no = project.tel_no self.send_sms_queue(sms_content=sms_content, tel_no=tel_no) raise e # 发送余额预警短信 if len(sms_content) != 0: tel_no = project.tel_no self.send_sms_queue(sms_content=sms_content, tel_no=tel_no) return True
def search_utxo(self, addrs: list, total_amount: Decimal) -> (bool, Dict): ret_utxos_map = dict() sum = Decimal(0) is_enough = False for addr in addrs: if is_enough: break utxos = self.get_utxo(address=addr, include_mem=True) utxos.sort(key=lambda item: item['value'], reverse=False) #按照金额升序排序 utxos.sort(key=lambda item: item['status']['confirmed'], reverse=True) #按照确认状态排序, 已确认的靠前 for utxo in utxos: value_in_satoshi = utxo['value'] if value_in_satoshi < MIN_AVAILABLE_UTXO_VALUE_IN_SATOSHI: continue #金额太小, 小于 10000satoshi, 不要 sum += round_down(Decimal(value_in_satoshi) / Decimal(10**8)) if addr not in ret_utxos_map: ret_utxos_map[addr] = [] ret_utxos_map[addr].append(utxo) if sum >= total_amount: is_enough = True break return is_enough, ret_utxos_map
def getHRC20TokenBalance(self, contract_addr: str, address: str) -> str: # 123.56.71.141:1317/hs/contract/htdf1nkkc48lfchy92ahg50akj2384v4yfqpm4hsq6y/70a0823100000000000000000000000067ee5cbe5cb9ae6794ca1437186f12066b0196af # assert self.cointype.upper() == 'HTDF' rlp_data = '70a08231' # from cosmos.my_bech32 import Bech32AddrToHex hex_addr = Bech32AddrToHex(addr=address) hex_addr_fmt = '0' * (32 * 2 - len(hex_addr)) + hex_addr rlp_data += hex_addr_fmt assert len(rlp_data) == (4 * 2 + 32 * 2) urlpath = f'/hs/contract/{contract_addr}/{rlp_data}' rsp = self._get_url_call(urlpath) data = rsp.replace('"', '') assert len(data) == 32 * 2 ndecimals = self.getHRC20_decimals(contract_addr=contract_addr) assert 1 < ndecimals <= 18 # balance = int(data, 16) * 1.0 / 10**(ndecimals) #会四舍五入!! 有问题 balance = Decimal(int(data, 16)) / Decimal(10**(ndecimals)) if balance < 0.00010000: return '0.00000000' # balance.quantize(Decimal("0.00000000"), getattr(decimal, 'ROUND_DOWN')) strfmt_balance = str(round_down(Decimal(str(balance)))) # strfmt_balance = '%.8f' % balance return strfmt_balance
def process_withdraw_order(self, **kwargs): """ 处理提币 :param kwargs: :return: """ logger.info(f'kwargs: { json.dumps(kwargs)}') orderid = str(kwargs['order_id']).strip() from_addr = str(kwargs['from_address']).strip() to_addr = str(kwargs['to_address']).strip() amount = round_down(Decimal(kwargs['amount'])) token_name = str(kwargs['token_name']).strip() challback_url = kwargs['callback_url'] pro_id = kwargs['pro_id'] memo = '' if 'memo' not in kwargs else kwargs['memo'] if not is_valid_addr(address=from_addr, token_name=token_name): raise Exception('`from` is invalid address') if not is_valid_addr(address=to_addr, token_name=token_name): raise Exception('`to` is invalid address') self.check_valid_params(pro_id=pro_id, token_name=token_name, from_addr=from_addr, amount=amount) #处理, 如果有异常, 则抛出异常 #根据订单号查询是否存在该订单 if self.is_order_exists(orderid, pro_id): logging.info(f"order already exists, order_id : {orderid}") #订单重复咋处理? 直接抛异常 raise Exception("order already exists") #生成唯一的流水号 serial_id = self.generate_serial_id() # 保存订单 self.insert_order(orderid, serial_id, token_name, from_addr, to_addr, amount, challback_url, pro_id, memo) # 发送消息到mq msg = {"serial_id": serial_id} logger.info('start send MQ...') #TODO 如果发送失败, 应该从数据库里面删除刚刚插入订单那数据 self.send_msg_to_mq(msg, token_name) logger.info('send MQ sucessed') #如果一切顺利, 返回 ret_info = {'serial_id': serial_id, 'order_id': orderid} return ret_info
def btc_collect(): # 1) 查询 tb_active_address_balances 表获取符合归集条件的地址 engine = create_engine(MYSQL_CONNECT_INFO, max_overflow=0, pool_size=5) Session = sessionmaker(bind=engine, autoflush=False, autocommit=True) # 增删改操作 需要手动flush, session = Session() query_sets = session.query(CollectionConfig, ActiveAddressBalance, Address) \ .filter(ActiveAddressBalance.token_name == 'BTC', CollectionConfig.token_name == 'BTC') \ .filter(CollectionConfig.pro_id == ActiveAddressBalance.pro_id) \ .filter(Address.address == ActiveAddressBalance.address) \ .filter(ActiveAddressBalance.balance >= CollectionConfig.min_amount_to_collect) \ .all() tx_hash_set = set() # 保存本次广播的tx_hash 用于后面监控 if not (query_sets is None or len(query_sets) == 0): logger.info(f'query_sets size is {len(query_sets)} ') proxy = BTCProxy(host=BTC_API_HOST, port=BTC_API_PORT) btcutil = BTCTransferUitl(host=BTC_API_HOST, port=BTC_API_PORT, net_type='mainnet' if g_IS_MAINNET else 'testnet') # 2) 遍历地址列表进行转账操作, 并插入归集记录表 class CLC_ADDR_INFO: pro_id = 0xffffff src_address = '' dst_address = '' clc_amount_in_satoshi = 0 priv_key = '' txhash = '' addr_info_map = dict() for clc_cfg, act_addr, addr in query_sets: assert isinstance(clc_cfg, CollectionConfig) assert isinstance(act_addr, ActiveAddressBalance) assert isinstance(addr, Address) if not is_valid_addr(clc_cfg.collection_dst_addr, token_name='BTC'): logger.error(f'invalid collection dst address : {clc_cfg.collection_dst_addr}') continue from_addr = addr.address balance_in_satoshi = proxy.get_balance(address=from_addr) #默认是保守方式的余额 balance_in_btc = round_down(Decimal(balance_in_satoshi) / Decimal(10 ** 8)) # 判断地址的(链上)余额是否满足归集条件 if balance_in_btc < Decimal(clc_cfg.min_amount_to_collect): logger.info(f'{from_addr}, BTC balance:{balance_in_btc} is less than min_amount_to_collect') continue logger.info(f'active address: {act_addr}') nettype = 'mainnet' if g_IS_MAINNET else 'testnet' priv_key, sub_addr = gen_bip44_subprivkey_from_mnemonic(mnemonic=g_MNEMONIC, coin_type='BTC', account_index=addr.pro_id, address_index=addr.address_index, nettype=nettype) if not sub_addr == addr.address: #大小写敏感, 不能转为小写 # 致命错误, 不可恢复 raise Exception(f'ADDRESS NOT MATCH! { sub_addr } != { addr.address}') pass if clc_cfg.pro_id not in addr_info_map: addr_info_map[clc_cfg.pro_id] = [] addr_info = CLC_ADDR_INFO() addr_info.pro_id = clc_cfg.pro_id addr_info.src_address = from_addr addr_info.priv_key = priv_key addr_info.txhash = '' addr_info.clc_amount_in_satoshi = balance_in_satoshi addr_info.dst_address = clc_cfg.collection_dst_addr addr_info_map[clc_cfg.pro_id].append(addr_info) for pro_id, addr_infos in addr_info_map.items(): if len(addr_infos) == 0: continue sum_amount_in_satoshi = 0 tmp_addrs = [] dst_addr = addr_infos[0].dst_address src_addrs_key_map = OrderedDict() for addr_info in addr_infos: assert pro_id == addr_info.pro_id , 'pro_id not match!' assert dst_addr == addr_info.dst_address, 'dst_addr not match!' if addr_info.src_address not in tmp_addrs: #去重 tmp_addrs.append(addr_info.src_address) sum_amount_in_satoshi += addr_info.clc_amount_in_satoshi src_addrs_key_map[addr_info.src_address] = addr_info.priv_key logger.info(f'to collect sub-address is: {tmp_addrs} ') # tmp_amount_in_btc = round_down(Decimal(sum_amount_in_satoshi) / Decimal(10 ** 8)) is_enough, founded_utxos, sum_utxo_satoshi = btcutil.search_utxo(addrs=tmp_addrs, total_amount=Decimal('1000.0'), #为了获取所有的UTXO(包括尚未确认的utxo) min_utxo_value=1000) if sum_utxo_satoshi > sum_amount_in_satoshi: logger.info('sum_utxo_satoshi > sum_amount_in_satoshi, will collect unconfirmed utxo') elif sum_utxo_satoshi == sum_amount_in_satoshi: logger.info('sum_utxo_satoshi == sum_amount_in_satoshi, not exist unconfirmed utxo, everything is perfect! ') else: logger.info('sum_utxo_satoshi < sum_amount_in_satoshi, maybe exsit some dusty utxo those less than 1000 satoshi') # 计算输入的utxo的数量, 用于计算手续费 fee_rate = 20 utxo_count = 0 for addr, utxos in founded_utxos.items(): utxo_count += len(utxos) txfee = round_down( Decimal(str((148 * utxo_count + 34 * 1 + 10))) * Decimal(fee_rate) / Decimal(10 ** 8) )# Decimal(str((148 * nIn + 34 * nOut + 10))) * Decimal(rate) txfee = txfee if txfee <= Decimal('0.0002') else Decimal('0.0002') # 防止手续费给太多 #本次归集的金额, 以搜索到的所有utxo的总金额为准 total_clc_amount_satoshi = sum_utxo_satoshi - int(txfee * Decimal(10**8)) - 1 #减去手续费, 因为 transfer中真实到账金额,不包括手续费 dst_addrs_amount_map = OrderedDict() dst_addrs_amount_map[dst_addr] = round_down(Decimal(total_clc_amount_satoshi) / Decimal(10 ** 8)) try: assert proxy.ping() == True, 'bitcoind rpc is gone' # 测试 bitcoind的 rpc服务是否还在 txid = btcutil.transfer(src_addrs_key_map=src_addrs_key_map, dst_addrs_amount_map=dst_addrs_amount_map, txfee=txfee, auto_calc_pay_back=False, pay_back_index=0xfffffff, ensure_one_txout=True) logger.info(f'txid: {txid}') if not session.is_active: session = Session() for addr, utxos in founded_utxos.items(): collect_amount_in_satoshi = 0 # 当前地址归集了的金额 for utxo in utxos: collect_amount_in_satoshi += utxo['value'] clc_records = CollectionRecords() clc_records.tx_hash = txid clc_records.pro_id = pro_id clc_records.token_name = 'BTC' clc_records.amount = round_down(Decimal(collect_amount_in_satoshi) / Decimal(10 ** 8)) clc_records.transaction_status = WithdrawStatus.transaction_status.PENDING clc_records.from_address = addr clc_records.collection_type = CollectionType.AUTO clc_records.to_address = dst_addr clc_records.complete_time = datetime.now() session.add(clc_records) session.flush() # 稍后要查询交易状态 tx_hash_set.add(txid) except Exception as e: logger.error(f'error: {e}') time.sleep(2) pass # 3) 监控发出的交易, 获取区块高度等信息, 并修改 交易状态 logger.info('waiting 5s for tx confirmed') time.sleep(5) # 4) 获取数据库中所有 all_pending_txs = session.query(CollectionRecords.tx_hash) \ .filter(CollectionRecords.transaction_status == WithdrawStatus.transaction_status.PENDING, CollectionRecords.token_name == 'BTC') \ .all() for item in all_pending_txs: tx_hash_set.add( item.tx_hash) #自动去重 #因为BTC是批量归集, 所以存在多笔归集记录的tx_hash相同, 更新数据库的交易状态时, 根据tx_hash进行查找即可 for tx_hash in tx_hash_set: try: tx_status = btc_get_transaction_status(tx_hash=tx_hash) if tx_status.transaction_status == WithdrawStatus.transaction_status.SUCCESS: session.query(CollectionRecords) \ .filter_by(tx_hash=tx_hash) \ .update({ 'block_height': tx_status.block_height, 'transaction_status': tx_status.transaction_status, 'block_time': tx_status.block_time, 'tx_confirmations': tx_status.confirmations, 'complete_time': datetime.now() }) elif tx_status.transaction_status == WithdrawStatus.transaction_status.FAIL: session.query(CollectionRecords) \ .filter_by(tx_hash=tx_hash) \ .update({ 'block_height': tx_status.block_height, 'transaction_status': tx_status.transaction_status, 'block_time': tx_status.block_time, # 'confirmations': tx_status.confirmations, 'complete_time': datetime.now() }) else: logger.info(f'{tx_hash} is still pending....') pass except Exception as e: logger.error(f'error: {e}') time.sleep(5) pass
def check_src_address_balance(self, **kwargs) -> bool: token_name = kwargs['token_name'] from_addr = kwargs['from_addr'] # to_addr = kwargs['to_addr'] amount = kwargs['amount'] # 特别注意 amount = round_down(amount) pro_id = kwargs['pro_id'] project = self.session.query(Project).filter_by(pro_id=pro_id).first() assert isinstance(project, Project), f'not found pro_id:{pro_id} in project' htdf_withdraw_cfg = self.session.query(WithdrawConfig).filter_by( pro_id=pro_id, token_name='HTDF').first() assert isinstance( htdf_withdraw_cfg, WithdrawConfig), f'not found pro_id:{pro_id} in withdraw_cfg' rpc = CosmosProxy(host=HTDF_NODE_RPC_HOST, port=HTDF_NODE_RPC_PORT, cointype=token_name) sms_content = '' sms_template = '【shbao】 尊敬的管理员,余额预警。{0}出币地址{1}余额为{2},请立即充值{3}。{4},{5}' + f',{project.pro_name}' try: if token_name == 'HTDF': strbalance = rpc.getBalance(from_addr) # 获取余额 decim_balance = Decimal(strbalance) if decim_balance < amount + Decimal('0.1'): self.logger.error( f'HTDF balance {decim_balance} < {amount + Decimal("0.1")}' ) sms_content = sms_template.format('HTDF', 'HTDF', decim_balance, 'HTDF', str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) if decim_balance < htdf_withdraw_cfg.balance_threshold_to_sms: sms_content = sms_template.format('HTDF', 'HTDF', decim_balance, 'HTDF', str(datetime.now()), ENV_NAME.upper()) else: #HRC20 hrc20_contract = '' hrc20_decimals = 18 for con_addr, sym_info in HRC20_CONTRACT_MAP.items(): if sym_info['symbol'] == token_name: hrc20_contract = con_addr hrc20_decimals = sym_info['decimal'] assert len(hrc20_contract) == 43, 'hrc20_contract is illegal' assert hrc20_decimals == 18, 'hrc20_deciaml not equal 18' hrc20_btu_withdraw_cfg = self.session.query(WithdrawConfig)\ .filter_by(pro_id=pro_id, token_name=token_name).first() assert isinstance( hrc20_btu_withdraw_cfg, WithdrawConfig ), f'not found pro_id:{pro_id} in withdraw_cfg' htdf_balance = rpc.getBalance(from_addr) # 获取余额 htdf_decim_balance = Decimal(htdf_balance) if htdf_decim_balance < Decimal('0.3'): str_balance = ( '%.8f' % htdf_decim_balance ) if htdf_decim_balance > Decimal('0.00000001') else '0' self.logger.error(f'HTDF fee balance {str_balance} < 0.01') sms_content = sms_template.format(token_name, 'HTDF手续费', str_balance, 'HTDF', str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) strbalance = rpc.getHRC20TokenBalance( contract_addr=hrc20_contract, address=from_addr) token_balance = round_down(Decimal(strbalance)) if token_balance < amount + Decimal('0.1'): self.logger.error( f'token balance {token_balance} < {amount + Decimal("0.1")}' ) str_balance = ( '%.8f' % token_balance ) if token_balance > Decimal('0.00000001') else '0' sms_content = sms_template.format(token_name, token_name, str_balance, token_name, str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) # 如果当前余额小于设定的预警值 则发送短信通知项目方 if token_balance < hrc20_btu_withdraw_cfg.balance_threshold_to_sms: sms_content = sms_template.format(token_name, token_name, token_balance, token_name, str(datetime.now()), ENV_NAME.upper()) except BadConnectionError as e: self.logger.error(f'connection HTDF node error: {e}') raise HttpConnectionError(e) except BalanceNotEnoughException as e: tel_no = project.tel_no self.send_sms_queue(sms_content=sms_content, tel_no=tel_no) raise e # 发送余额预警短信 if len(sms_content) != 0: tel_no = project.tel_no self.send_sms_queue(sms_content=sms_content, tel_no=tel_no) return True
def check_src_address_balance(self, **kwargs) -> bool: token_name = kwargs['token_name'] from_addr = kwargs['from_addr'] # to_addr = kwargs['to_addr'] amount = kwargs['amount'] # 特别注意: 以 ETH 为单位, 不要转为 wei !!! # priv_key = kwargs['priv_key'] pro_id = kwargs['pro_id'] project = self.session.query(Project).filter_by(pro_id=pro_id).first() assert isinstance(project, Project), f'not found pro_id:{pro_id} in project' eth_withdraw_cfg = self.session.query(WithdrawConfig).filter_by( pro_id=pro_id, token_name='ETH').first() assert isinstance( eth_withdraw_cfg, WithdrawConfig), f'not found pro_id:{pro_id} in withdraw_cfg' amount = round_down(amount) block_identifier = HexStr('latest') # 不能用pending myweb3 = Web3(provider=HTTPProvider( endpoint_uri=URI(ETH_FULL_NODE_RPC_URL))) nbalance = myweb3.eth.getBalance( account=to_checksum_address(from_addr), block_identifier=block_identifier) ether_balance = myweb3.fromWei(nbalance, 'ether') # ETH 余额 decim_eth_balance = round_down(ether_balance) sms_content = '' sms_template = '【shbao】 尊敬的管理员,余额预警。{0}出币地址{1}余额为{2},请立即充值{3}。{4},{5}' + f',{project.pro_name}' try: if token_name == 'ETH': if decim_eth_balance < amount + Decimal('0.01'): str_balance = ( '%.8f' % decim_eth_balance ) if decim_eth_balance > Decimal('0.00000001') else '0' self.logger.error( f'ETH balance {str_balance } < {amount + Decimal("0.01")}' ) sms_content = sms_template.format('ETH', 'ETH', str_balance, 'ETH', str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) #如果当前余额小于设定的预警值 则发送短信通知项目方 if decim_eth_balance < eth_withdraw_cfg.balance_threshold_to_sms: sms_content = sms_template.format('ETH', 'ETH', decim_eth_balance, 'ETH', str(datetime.now()), ENV_NAME.upper()) else: # USDT交易 usdt_withdraw_cfg = self.session.query( WithdrawConfig).filter_by(pro_id=pro_id, token_name='USDT').first() assert isinstance( usdt_withdraw_cfg, WithdrawConfig ), f'not found pro_id:{pro_id} in withdraw_cfg' if decim_eth_balance < Decimal('0.01'): str_balance = ( '%.8f' % decim_eth_balance ) if decim_eth_balance > Decimal('0.00000001') else '0' self.logger.error(f'ETH balance {str_balance} < 0.01') sms_content = sms_template.format('USDT', 'ETH手续费', str_balance, 'ETH', str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) chksum_contract_addr = to_checksum_address( ERC20_USDT_CONTRACT_ADDRESS) contract = myweb3.eth.contract(address=chksum_contract_addr, abi=EIP20_ABI) # symbol = contract.functions.symbol().call() # decimals = contract.functions.decimals().call() erc20_token_balance_int = contract.functions.balanceOf( to_checksum_address(from_addr)).call() if erc20_token_balance_int == 0: self.logger.error(f'token balance is 0') sms_content = sms_template.format('USDT', 'USDT', 0, 'USDT', str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) erc20_token_balance_decimal = myweb3.fromWei( erc20_token_balance_int, unit='mwei') token_balance = round_down(erc20_token_balance_decimal) if token_balance < amount + Decimal('0.1'): self.logger.error( f'token balance {token_balance} < {amount + Decimal("0.1")}' ) sms_content = sms_template.format('USDT', 'USDT', token_balance, 'USDT', str(datetime.now()), ENV_NAME.upper()) raise BalanceNotEnoughException(sms_content) # 如果当前余额小于设定的预警值 则发送短信通知项目方 if token_balance < usdt_withdraw_cfg.balance_threshold_to_sms: sms_content = sms_template.format('USDT', 'USDT', token_balance, 'USDT', str(datetime.now()), ENV_NAME.upper()) except BalanceNotEnoughException as e: tel_no = project.tel_no self.send_sms_queue(sms_content=sms_content, tel_no=tel_no) self.logger.info('send_sms_queue finished') raise e #发送余额预警短信 if len(sms_content) != 0: tel_no = project.tel_no self.send_sms_queue(sms_content=sms_content, tel_no=tel_no) self.logger.info('send_sms_queue finished') return True