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)
Beispiel #2
0
    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
Beispiel #4
0
    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.'
        )
Beispiel #5
0
    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
Beispiel #6
0
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/')