def _post(self, request: Request): d = request.data # If they didn't specify one of these, it will simply raise an exception for us to handle below. try: from_coin = str(d['from_coin']).upper() to_coin = str(d['to_coin']).upper() destination = str(d['destination']) dest_memo = d.get('memo', None) except (AttributeError, KeyError): return r_err( "You must specify 'from_coin', 'to_coin', and 'destination'", 400) # Check if the coin pair specified actually exists. If it doesn't, it'll throw a DoesNotExist. try: c = CoinPair.objects.get(from_coin__symbol=from_coin, to_coin__symbol=to_coin) except CoinPair.DoesNotExist: return r_err( "There is no such coin pair {} -> {}".format( d['from_coin'], d['to_coin']), 404) # Grab the x(BaseManager) instances for the from/to coin, so we can do some validation + generate deposit info m = get_manager(from_coin) m_to = get_manager(to_coin) # To save users from sending their coins into the abyss, make sure their destination address/account is # actually valid / exists. if not m_to.address_valid(destination): return r_err( "The destination {} address/account '{}' is not valid".format( to_coin, destination), 400) # Ask the Coin Handler for their from_coin how to handle deposits for that coin dep_type, dep_addr = m.get_deposit() # Data to return in the 'result' key of our response. res = dict(ex_rate=c.exchange_rate, destination=destination, pair=str(c)) if dep_type == 'account': # If the coin handler uses an account system, that means we just give them our account to deposit into, # and generate a memo with destination coin/address details. res['memo'] = "{} {}".format(to_coin, destination) if dest_memo is not None: res['memo'] = "{} {} {}".format(to_coin, destination, dest_memo) res['account'] = dep_addr else: # If it's not account based, assume it's address based. dep_data = dict(deposit_coin=c.from_coin, deposit_address=dep_addr, destination_coin=c.to_coin, destination_address=destination, destination_memo=dest_memo) res['address'] = dep_addr # Store the address so we can map it to their destination coin when they deposit to it. AddressAccountMap(**dep_data).save() return Response(res)
def refund_sender(deposit: Deposit, reason: str = None, return_to: str = None) -> Tuple[Deposit, dict]: """ Returns a Deposit back to it's original sender, sets the Deposit status to 'refund', and stores the refund details onto the Deposit. :param Deposit deposit: The :class:`models.Deposit` object to refund :param str reason: If specified, will use this instead of ``deposit.error_reason`` for the memo. :param str return_to: If specified, will return to this addr/acc instead of deposit.address/from_account :return tuple refund_data: Returns a tuple containing the updated Deposit object, and the tx data from send(). """ d = deposit reason = d.error_reason if empty(reason) else reason if empty(reason): reason = f'Returned to sender due to unknown error processing deposit amount {d.amount} ' \ f'with TXID {d.txid}...' log.info(f'Refunding Deposit {d} due to reason: {reason}') if d.status == 'refund': raise ConvertError(f'The deposit {d} is already refunded!') if d.status == 'conv': raise ConvertError( f'The deposit {d} is already successfully converted!') c = d.coin sym = c.symbol.upper() mgr = get_manager(sym) # Return destination priority: ``return_to`` arg, sender address, sender account dest = d.address if empty(return_to) else return_to dest = d.from_account if empty(dest) else dest if empty(dest): raise AttributeError( 'refund_sender could not find any non-empty return address/account...' ) metadata = dict(deposit=deposit, action="refund") log.info( f'(REFUND) Sending {d.amount} {sym} to addr/acc {dest} with memo "{reason}"' ) txdata = mgr.send_or_issue(amount=d.amount, address=dest, memo=reason, trigger_data=metadata) log.debug(f'(REFUND) Storing refund details for deposit {d}') d.status = 'refund' d.refund_address = dest d.refund_amount = txdata['amount'] d.refund_coin = sym d.refund_memo = reason d.refund_txid = txdata['txid'] d.refunded_at = timezone.now() d.save() log.info(f'(REFUND) SUCCESS. Saved details for {d}') return d, txdata
def handler_dic(self): """View function to be called from template. Loads and queries coin handlers for health, with caching.""" hdic = { } # A dictionary of {handler_name: {headings:list, results:list[tuple/list]} reload_handlers() for coin in Coin.objects.all(): try: if not has_manager(coin.symbol): self.coin_fails.append( 'Cannot check {} (no manager registered in coin handlers)' .format(coin)) continue # Try to load coin health data from cache. # If it's not found, query the manager and cache it for up to 30 seconds to avoid constant RPC hits. c_health = coin.symbol + '_health' mname, mhead, mres = cache.get_or_set( c_health, get_manager(coin.symbol).health(), 30) # Create the dict keys for the manager name if needed, then add the health results d = hdic[mname] = dict( headings=list(mhead), results=[]) if mname not in hdic else hdic[mname] d['results'].append(list(mres)) except: log.exception( 'Something went wrong loading health data for coin %s', coin) self.coin_fails.append( 'Failed checking {} (something went wrong loading health data, check server logs)' .format(coin)) return hdic
def handle(self, *args, **options): # Load all "new" deposits, max of 200 in memory at a time to avoid memory leaks. new_deposits = Deposit.objects.filter(status='new').iterator(200) log.info('Coin converter and deposit validator started') # ---------------------------------------------------------------- # Validate deposits and map them to a destination coin / address # ---------------------------------------------------------------- log.info('Validating deposits that are in state "new"') for d in new_deposits: try: log.debug('Validating and mapping deposit %s', d) with transaction.atomic(): try: self.detect_deposit(d) except ConvertError as e: # Something went very wrong while processing this deposit. Log the error, store the reason # onto the deposit, and then save it. log.error( 'ConvertError while validating deposit "%s" !!! Message: %s', d, str(e)) try: mgr = get_manager( d.coin.symbol) # type: SettingsMixin auto_refund = mgr.settings.get( d.coin.symbol, {}).get('auto_refund', False) if is_true(auto_refund): log.info( f'Auto refund is enabled for coin {d.coin}. Attempting return to sender.' ) ConvertCore.refund_sender(deposit=d) else: d.status = 'err' d.error_reason = str(e) d.save() except Exception as e: log.exception( 'An exception occurred while checking if auto_refund was enabled...' ) d.status = 'err' d.error_reason = f'Auto refund failure: {str(e)}' d.save() except ConvertInvalid as e: # This exception usually means the sender didn't read the instructions properly, or simply # that the transaction wasn't intended to be exchanged. log.error( 'ConvertInvalid (user mistake) while validating deposit "%s" Message: %s', d, str(e)) d.status = 'inv' d.error_reason = str(e) d.save() except: log.exception( 'UNHANDLED EXCEPTION. Deposit could not be validated/detected... %s', d) d.status = 'err' d.error_reason = 'Unknown error while validating deposit. An admin must manually check the error logs.' d.save() log.info('Finished validating new deposits for conversion') # ---------------------------------------------------------------- # Convert any validated deposits into their destination coin # ---------------------------------------------------------------- conv_deposits = Deposit.objects.filter(status='mapped').iterator(200) log.info('Converting deposits that are in state "mapped"...') for d in conv_deposits: try: log.debug('Converting deposit %s', d) with transaction.atomic(): try: self.convert_deposit(d, options['dry']) except ConvertError as e: # Something went very wrong while processing this deposit. Log the error, store the reason # onto the deposit, and then save it. log.error( 'ConvertError while converting deposit "%s" !!! Message: %s', d, str(e)) d.status = 'err' d.error_reason = str(e) d.save() except ConvertInvalid as e: # This exception usually means the sender didn't read the instructions properly, or simply # that the transaction wasn't intended to be exchanged. log.error( 'ConvertInvalid (user mistake) while converting deposit "%s" Message: %s', d, str(e)) d.status = 'inv' d.error_reason = str(e) d.save() except: log.exception( 'UNHANDLED EXCEPTION. Conversion error for deposit... %s', d) d.status = 'err' d.error_reason = 'Unknown error while converting. An admin must manually check the error logs.' d.save() log.info('Finished converting deposits.') log.debug( 'Resetting any Coins "funds_low" if they have no "mapped" deposits' ) for c in Coin.objects.filter(funds_low=True): log.debug(' -> Coin %s currently has low funds', c) map_deps = c.deposit_converts.filter(status='mapped').count() if map_deps == 0: log.debug( ' +++ Coin %s has no mapped deposits, resetting funds_low to false', c) c.funds_low = False c.save() else: log.debug( ' !!! Coin %s still has %d mapped deposits. Ignoring.', c, map_deps) log.debug( 'Finished resetting coins with "funds_low" that have been resolved.' )
def convert(deposit: Deposit, pair: CoinPair, address: str, dest_memo: str = None): """ After a Deposit has passed the validation checks of :py:meth:`.detect_deposit` , this method loads the appropriate coin handler, calculates fees, generates a memo, and sends the exchanged coins to their destination. :param Deposit deposit: The deposit object to be converted :param CoinPair pair: A CoinPair object for getting the exchange rate + destination coin :param str address: The destination crypto address, or account :param str dest_memo: Optionally specify a memo for the coins to be sent with :raises ConvertError: Raised when a serious error occurs that generally isn't the sender's fault. :raises ConvertInvalid: Raised when a Deposit fails validation, i.e. the sender ignored our instructions. :return Conversion c: The inserted conversion object after a successful transfer :return None None: Something is wrong with the coin handler, try again later, do not set deposit to error. """ # f/tcoin are the actual Coin objects fcoin = pair.from_coin tcoin = pair.to_coin # dest/src are string symbols dest_coin = tcoin.symbol.upper() src_coin = fcoin.symbol.upper() mgr = get_manager(dest_coin) if empty(dest_memo): dest_memo = 'Token Conversion' if not empty(deposit.address): dest_memo += ' via {} deposit address {}'.format( src_coin, deposit.address) if not empty(deposit.from_account): dest_memo += ' from {} account {}'.format( src_coin, deposit.from_account) send_amount, ex_fee = ConvertCore.amount_converted( deposit.amount, pair.exchange_rate, settings.EX_FEE) log.info('Attempting to send %f %s to address/account %s', send_amount, dest_coin, address) try: if not mgr.health_test(): log.warning( "Coin %s health test has reported that it's down. Will try again later...", tcoin) deposit.last_convert_attempt = timezone.now() deposit.save() return None metadata = dict(deposit=deposit, pair=pair, action="convert") if tcoin.can_issue: s = mgr.send_or_issue(amount=send_amount, address=address, memo=dest_memo, trigger_data=metadata) else: s = mgr.send(amount=send_amount, address=address, memo=dest_memo, trigger_data=metadata) log.info('Successfully sent %f %s to address/account %s', send_amount, dest_coin, address) deposit.status = 'conv' deposit.convert_to = tcoin deposit.processed_at = timezone.now() deposit.save() c = Conversion(deposit=deposit, from_coin=fcoin, to_coin=tcoin, from_address=s.get('from', None), to_address=address, to_memo=dest_memo, to_amount=s.get('amount', deposit.amount), to_txid=s.get('txid', None), tx_fee=s.get('fee', Decimal(0)), ex_fee=ex_fee) c.save() log.info('Successfully stored Conversion. Conversion ID is %s', c.id) return c except AccountNotFound: raise ConvertInvalid( 'Destination address "{}" appears to be invalid. Exc: AccountNotFound' .format(address)) except NotEnoughBalance: log.error( 'Not enough balance to send %f %s. Will try again later...', send_amount, dest_coin) try: deposit.last_convert_attempt = timezone.now() deposit.save() ConvertCore.notify_low_bal(pair=pair, send_amount=send_amount, balance=mgr.balance(), deposit_addr=mgr.get_deposit()[1]) except: log.exception( 'Failed to send ADMINS email notifications for low balance of coin %s', dest_coin) return None
def refund_deposits(request): rp = request.POST objlist = rp.getlist('objects[]') if empty(rp.get('refund')) or empty(objlist, itr=True): log.info('Refund value: %s - Objects: %s', rp.get('refund'), objlist) add_message(request, messages.ERROR, f'Error - missing POST data for refund_deposits!') return redirect('/admin/payments/deposit/') deposits = Deposit.objects.filter(id__in=objlist) for d in deposits: # type: Deposit try: if empty(d.from_account): add_message( request, messages.ERROR, f'Cannot refund deposit ({d}) - from_account is blank!') continue with transaction.atomic(): log.info('Attempting to refund deposit (%s)', d) convs = Conversion.objects.filter(deposit=d) if len(convs) > 0: for conv in convs: log.info('Removing linked conversion %s', conv) add_message(request, messages.WARNING, f'Removed linked conversion {str(conv)}') conv.delete() sym = d.coin_symbol memo = f'Refund of {sym} deposit {d.txid}' log.debug('Initializing manager for %s', sym) mgr = get_manager(sym) log.debug( 'Calling send_or_issue for amount "%s" to address "%s" with memo "%s"', d.amount, d.from_account, memo) res = mgr.send_or_issue(amount=d.amount, address=d.from_account, memo=memo) d.status = 'refund' d.refunded_at = timezone.now() d.refund_coin = res.get('coin', sym) d.refund_memo = memo d.refund_amount = res.get('amount', d.amount) d.refund_address = d.from_account d.refund_txid = res.get('txid', 'N/A') d.save() add_message( request, messages.SUCCESS, f'Successfully refunded {d.amount} {sym} to {d.from_account}' ) except Exception as e: d.status = 'err' d.error_reason = f'Error while refunding: {type(e)} {str(e)}' log.exception('Error while refunding deposit %s', d) d.save() add_message( request, messages.ERROR, f'Error while refunding deposit ({d}) - Reason: {type(e)} {str(e)}' ) return redirect('/admin/payments/deposit/')