Ejemplo n.º 1
0
    def pvx_get_actions(self,
                        account: str,
                        count=100,
                        symbol=None,
                        contract=None,
                        history_url=None) -> List[dict]:
        """
        Loads EOS transactions for a given account, and caches them per account to avoid constant queries.

        :param account:  The EOS account to load transactions for
        :param count:    Amount of transactions to load
        :return list transactions: A list of EOS transactions as dict's
        """
        cache_key = f'{self.chain}_pvx_actions:{account}'
        actions = cache.get(cache_key)
        if empty(history_url):
            history_url = self.setting_defaults.get('history_url')

        if empty(actions):
            log.info('Loading %s actions for %s from history API %s',
                     self.chain.upper(), account, history_url)
            # c = self.eos
            # data = c.get_actions(account, pos=-1, offset=-count)
            url = f"{history_url}/api/actions/?limit={count}&tx_to={account}"
            if not empty(symbol): url += f"&symbol={symbol}"
            if not empty(contract): url += f"&account={contract}"
            req = requests.get(url)
            actions = req.json()['results']
            # log.info('%s %s', url, actions)
            cache.set(cache_key, actions, timeout=60)

        return actions
Ejemplo n.º 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
Ejemplo n.º 3
0
    def get_contract(self, symbol: str) -> str:
        """
        Attempt to find the contract account for a given token symbol, searches the database Coin objects first
        using :py:attr:`.settings` - if not found, falls back to :py:attr:`.default_contracts`

        Example usage::

            >>> contract_acc = self.get_contract('EOS')
            >>> print(contract_acc)
             eosio.token


        :param str symbol:              The token symbol to find the contract for, e.g. ``EOS``
        :raises TokenNotFound:          The given ``symbol`` does not exist in self.settings
        :raises MissingTokenMetadata:   Could not find contract in DB coin settings nor default_contracts
        :return str contract_acc:       The contract username as a string, e.g. ``eosio.token``
        """

        symbol = symbol.upper()
        log.debug(
            f'Attempting to find EOS contract for "{symbol}" in DB Coin settings'
        )

        try:
            contract = self.settings[symbol].get('contract')
            if not empty(contract):
                return contract
        except AttributeError:
            raise TokenNotFound(
                f'The coin "{symbol}" was not found in {__name__}.settings')

        log.debug(
            f'No contract found in DB settings for "{symbol}", checking if we have a default...'
        )
        try:
            contract = self.default_contracts[symbol]

            if empty(contract):
                raise MissingTokenMetadata

            log.debug(
                f'Found contract for "{symbol}" in default_contracts, returning "{contract}"'
            )
            return contract
        except (AttributeError, MissingTokenMetadata):
            log.error(
                f'Failed to find a contract for "{symbol}" in Coin objects nor default_contracts...'
            )
            raise MissingTokenMetadata(
                f"Couldn't find '{symbol}' contract in DB coin settings or default_contracts."
            )
Ejemplo n.º 4
0
    def balance(self,
                address: str = None,
                memo: str = None,
                memo_case: bool = False) -> Decimal:
        if not address:
            address = self.coin.our_account

        if not empty(memo):
            raise NotImplemented(
                'Filtering by memos not implemented yet for EOSManager!')
        sym = self.symbol

        contract = self.get_contract(sym)

        bal = self.eos.get_currency_balance(address, code=contract, symbol=sym)
        if len(bal) < 1:
            raise TokenNotFound(
                f'Balance list for EOS symbol {sym} with contract {contract} was empty...'
            )

        amt, curr = bal[0].split()
        amt = Decimal(amt)
        if curr.upper() != sym:
            raise CoinHandlerException(
                f'Expected balance currency of {sym} but got {curr} - aborting'
            )

        return amt
Ejemplo n.º 5
0
    def _clean_settings(self, d_settings: Dict[str, dict]) -> Dict[str, dict]:
        """
        Clean up ``d_settings`` by setting any missing/empty settings to default values, and cast non-string settings
        to the correct types.

        :param dict d_settings: The dict<str,dict> mapping symbol->settings to clean up
        :return dict d_settings: The cleaned dictionary. Only needed if you passed a deep-copy for d_settings, as the
                                 passed dict will be altered in-place unless it's a copy.
        """

        defs = self._bc_defaults
        # Loop over each symbol and settings dict we were passed
        for sym, conn in d_settings.items(
        ):  # coin symbol : str, settings: dict
            # log.debug("Cleaning settings for symbol %s", sym)
            z = d_settings[sym]  # Pointer to the settings dict for this symbol
            # Loop over our default settings, compare to the user's settings
            for def_key, def_val in defs.items(
            ):  # settings key : str, settings value : any
                # Check if required setting key exists in user's settings
                if def_key in z and not empty(z[def_key]):
                    continue
                # Setting doesn't exist, or was empty. Update user's setting to our default.
                z[def_key] = def_val
            # Cast settings keys to avoid casting errors
            z['confirms_needed'] = int(z['confirms_needed'])
            z['port'] = int(z['port'])
            z['use_trusted'] = z['use_trusted'] in [
                True, 'true', 'True', 'TRUE', 1, 'yes'
            ]
        return d_settings
    def balance(self,
                address: str = None,
                memo: str = None,
                memo_case: bool = False) -> Decimal:
        """
        Get token balance for a given Steem account, if memo is given - get total symbol amt received with this memo.

        :param address:    Steem account to get balance for, if not set, uses self.coin.our_account
        :param memo:       If not None, get total `self.symbol` received with this memo.
        :param memo_case:  Case sensitive memo search
        :return: Decimal(balance)
        """
        if address is None:
            address = self.coin.our_account
        address = address.lower()
        if memo is not None:
            memo = str(memo).strip()
        if empty(memo):
            return self.eng_rpc.get_token_balance(user=address,
                                                  symbol=self.symbol)
        txs = self.eng_rpc.list_transactions(user=address,
                                             symbol=self.symbol,
                                             limit=1000)
        bal = Decimal(0)
        for t in txs:
            if t['to'] == address and t['symbol'] == self.symbol:
                m = t['memo'].strip()
                if m == memo or (not memo_case and m == memo.lower()):
                    bal += Decimal(t['quantity'])
        return bal
    def load(self, tx_count=1000):
        """
        Simply imports `tx_count` into an instance variable, and then sets `self.loaded` to True.

        If `self.need_account` is set to True by a child/parent class, this method will remove any coins from
        `self.coins` and `self.symbols` which have a blank/null `our_account` in the DB, ensuring that you can trust
        that all coins listed in symbols/coins have an `our_account` which isn't empty or None.

        :param int tx_count:  The amount of transactions to load per symbol specified in constructor
        """

        log.debug('Initialising %s with TX count %d',
                  type(self).__name__, tx_count)
        self.tx_count = tx_count
        if not self.need_account:
            self.loaded = True
            return
        for symbol, coin in self.coins.items():
            if not empty(coin.our_account):
                continue
            log.warning(
                'The coin %s does not have `our_account` set. Refusing to load transactions.',
                coin)
            del self.coins[symbol]
            self.symbols = [s for s in self.symbols if s != symbol]

        self.loaded = True
    def balance(self,
                address: str = None,
                memo: str = None,
                memo_case: bool = False) -> Decimal:
        """
        Get token balance for a given Bitshares account, if memo is given - get total symbol amt received with this memo.

        :param address:    Bitshares account to get balance for, if not set, uses self.coin.our_account
        :param memo:       If not None, get total `self.symbol` received with this memo.
        :param memo_case:  Case sensitive memo search

        :raises AccountNotFound:  The requested account/address doesn't exist

        :return: Decimal(balance)
        """
        balance = Decimal(0)
        if address is None:
            address = self.coin.our_account

        if not empty(memo):
            raise NotImplemented(
                'Filtering by memos not implemented yet for BitsharesManager!')

        address = address.strip().lower()
        account_obj = self.get_account_obj(address)
        if account_obj is None:
            raise exceptions.AccountNotFound(
                f'Account {address} does not exist')

        asset_obj = self.get_asset_obj(self.symbol)
        if asset_obj is not None:
            amount_obj = account_obj.balance(self.symbol)
            balance = self.get_decimal_from_amount(amount_obj)

        return balance
Ejemplo n.º 9
0
    def balance(self,
                address: str = None,
                memo: str = None,
                memo_case: bool = False) -> Decimal:
        """
        Get token balance for a given Steem account, if memo is given - get total symbol amt received with this memo.

        :param address:    Steem account to get balance for, if not set, uses self.coin.our_account
        :param memo:       If not None, get total `self.symbol` received with this memo.
        :param memo_case:  Case sensitive memo search
        :return: Decimal(balance)
        """
        if not address:
            address = self.coin.our_account

        acc = Account(address, steem_instance=self.rpc)

        if not empty(memo):
            hist = acc.get_account_history(-1, 10000, only_ops=['transfer'])
            total = Decimal(0)
            for h in hist:
                tx = SteemLoader.clean_tx(h, self.symbol, address, memo)
                if tx is None:
                    continue
                total += tx['amount']
            return total

        bal = acc.get_balance('available', self.symbol)
        return Decimal(bal.amount)
Ejemplo n.º 10
0
    def issue(self,
              amount: Decimal,
              address: str,
              memo: str = None,
              trigger_data=None):
        acc = self.coin.our_account

        # Some basic sanity checks, e.g. do the from/to account exist? validate/cast the sending amount
        self.address_valid_ex(acc, address)
        memo = "" if empty(memo) else memo
        # Note: since we're issuing, no from_account kwarg to avoid NotEnoughBalance exceptions
        amount = self.validate_amount(amount=amount)

        # Grab the coin's symbol and find it's contract account
        sym, contract = self.symbol, self.get_contract(self.symbol)

        # Craft the transaction arguments for the issue operation, then broadcast it and get the result
        precision = self.settings[self.symbol].get('precision', 4)
        amt = f"{amount:.{precision}f} {sym}"
        tx_args = {"to": address, "quantity": amt, "memo": memo}
        tfr = self.build_tx("issue", contract, acc, tx_args)

        # Some of the important data, e.g. how much was actually sent, is buried in the processed>action_traces
        tx_output = tfr['processed']['action_traces'][0]['act']['data']
        tx_amt_final = Decimal(tx_output['quantity'].split()[0])

        return {
            'txid': tfr['transaction_id'],
            'coin': sym,
            'amount': tx_amt_final,
            'fee': Decimal(0),
            'from': acc,
            'send_type': 'issue'
        }
Ejemplo n.º 11
0
    def clean_tx(self,
                 tx: dict,
                 symbol: str,
                 account: str,
                 memo: str = None,
                 memo_case: bool = False) -> Union[dict, None]:
        """Filters an individual transaction. See :meth:`.clean_txs` for info"""
        # log.debug(tx)
        if tx.get('type', 'NOT SET') != 'transfer':
            log.debug('Steem TX is not transfer. Type is: %s',
                      tx.get('type', 'NOT SET'))
            return None

        txid = tx.get('trx_id', None)

        _am = tx[
            'amount']  # Transfer ops contain a dict 'amount', containing amount:int, nai:str, precision:int

        if type(_am
                ) is str:  # Extract and validate asset 'ABC' from '12.345 ABC'
            amt, _symbol = _am.split()
            _asset = Asset(symbol, steem_instance=self.get_rpc(symbol))
        else:  # Conv asset ID (e.g. @@000000021) to symbol, i.e. "STEEM"
            _asset = Asset(_am['nai'], steem_instance=self.get_rpc(symbol))
            # Convert integer amount/precision to Decimal's, preventing floating point issues
            amt_int = Decimal(_am['amount'])
            amt_prec = Decimal(_am['precision'])

            amt = amt_int / (
                Decimal(10)**amt_prec
            )  # Use precision value to convert from integer amt to decimal amt
        # Get validated symbol from beem Asset
        amt_sym = str(_asset.symbol)
        if amt_sym != symbol:  # If the symbol doesn't match the symbol we were passed, skip this TX
            return None

        tx_memo = tx.get('memo')

        log.debug('Filtering/cleaning steem transaction, Amt: %f, TXID: %s',
                  amt, txid)

        if tx['to'] != account or tx['from'] == account:
            return None  # If the transaction isn't to us (account), or it's from ourselves, ignore it.
        if not empty(memo) and (tx_memo != memo or
                                (not memo_case
                                 and tx_memo.lower() != memo.lower())):
            return None

        d = parse(tx['timestamp'])
        d = timezone.make_aware(d, pytz.UTC)

        return dict(txid=txid,
                    coin=symbol,
                    vout=int(tx.get('op_in_trx', 0)),
                    tx_timestamp=d,
                    from_account=tx.get('from', None),
                    to_account=tx.get('to', None),
                    memo=tx_memo,
                    amount=Decimal(amt))
Ejemplo n.º 12
0
 def load(self, tx_count=1000):
     log.info('Loading Steem Engine transactions...')
     # with transaction.atomic():
     self.tx_count = tx_count
     for symbol, coin in self.coins.items():
         if empty(coin.our_account):
             log.warning('The coin %s does not have `our_account` set. Refusing to load transactions.', coin)
             del self.coins[symbol]
             self.symbols = [s for s in self.symbols if s != symbol]
     self.loaded = True
Ejemplo n.º 13
0
 def save(self, *args, **kwargs):
     """To avoid inconsistency, the symbol is automatically made uppercase"""
     self.symbol = self.symbol.upper()
     if empty(self.symbol_id):
         self.symbol_id = self.symbol
     super(Coin, self).save(*args, **kwargs)
     # After a coin is updated in the DB, we should reload any coin_handlers to detect compatible loaders.
     # We need to use in-line loading to prevent recursive loading from the coin handlers causing issues.
     from payments.coin_handlers import reload_handlers
     reload_handlers()
Ejemplo n.º 14
0
 def load(self, tx_count=1000):
     # Unlike other coins, it's important to load a lot of TXs, because many won't actually be transfers
     # Thus the default TX count for Hive is 1000
     self.tx_count = tx_count
     for symbol, coin in self.coins.items():
         if not empty(coin.our_account):
             continue
         log.warning('The coin %s does not have `our_account` set. Refusing to load transactions.', coin)
         del self.coins[symbol]
         self.symbols = [s for s in self.symbols if s != symbol]
    def send(self, amount, address, from_address=None, memo=None) -> dict:
        """
        Send a given ``amount`` of EOS (or a token on EOS) from ``from_address`` to ``address`` with the memo ``memo``.

        Only ``amount`` and ``address`` are mandatory.

        :param Decimal amount:      Amount of coins/tokens to send, as a Decimal()
        :param str address:         Destination EOS account to send the coins/tokens to
        :param str memo:            Memo to send coins/tokens with (default: "")
        :param str from_address:    EOS Account to send from (default: uses Coin.our_account)
        :raises AuthorityMissing:   Cannot send because we don't have authority to (missing key etc.)
        :raises AccountNotFound:    The requested account doesn't exist
        :raises NotEnoughBalance:   Sending account/address does not have enough balance to send
        :return dict:  Result Information

        Format::

          dict {
            txid:str       - Transaction ID - None if not known,
            coin:str       - Symbol that was sent,
            amount:Decimal - The amount that was sent (after fees),
            fee:Decimal    - TX Fee that was taken from the amount (static Decimal(0) for EOS)
            from:str       - The account the coins were sent from.
            send_type:str  - Statically set to "send"
          }

        """
        # Fallback to the coin's `our_account` if `from_address` is not specified
        from_address = self.coin.our_account if not from_address else from_address

        # Some basic sanity checks, e.g. do the from/to account exist? validate/cast the sending amount
        self.address_valid_ex(from_address, address)
        memo = "" if empty(memo) else memo
        amount = self.validate_amount(amount=amount, from_account=from_address)

        # Grab the coin's symbol and find it's contract account
        sym, contract = self.symbol, self.get_contract(self.symbol)

        # Craft the transaction arguments for the transfer operation, then broadcast it and get the result
        tx_args = {"from": from_address, "to": address, "quantity": f"{amount:.4f} {sym}", "memo": memo}
        tfr = self.build_tx("transfer", contract, from_address, tx_args)

        # Some of the important data, e.g. how much was actually sent, is buried in the processed>action_traces
        tx_output = tfr['processed']['action_traces'][0]['act']['data']
        tx_amt_final = Decimal(tx_output['quantity'].split()[0])

        return {
            'txid': tfr['transaction_id'],
            'coin': self.orig_symbol,
            'amount': tx_amt_final,
            'fee': Decimal(0),
            'from': from_address,
            'send_type': 'send'
        }
Ejemplo n.º 16
0
    def get_rpc(self, symbol):
        """
        Returns a Steem instance for querying data and sending TXs. By default, uses the Beem shared_steem_instance.

        If a custom RPC list is specified in the Coin "custom json" settings, a new instance will be returned with the
        RPCs specified in the json.

        :param symbol: Coin symbol to get Beem RPC instance for
        :return beem.steem.Steem: An instance of :class:`beem.steem.Steem` for querying
        """
        rpc_list = self.settings[symbol]['json'].get('rpcs')
        return self.rpc if empty(rpc_list, itr=True) else Steem(node=rpc_list)
Ejemplo n.º 17
0
    def __init__(self, symbol: str):
        super().__init__(symbol)
        settings = self.coin.settings['json']

        rpcs = settings.get('rpcs')
        # If you've specified custom RPC nodes in the custom JSON, make a new instance with those
        # Otherwise, use the global shared_steem_instance.
        self.rpc = shared_steem_instance() if empty(rpcs, itr=True) else Steem(
            rpcs)  # type: Steem
        self.rpc.set_password_storage(settings.get('pass_store',
                                                   'environment'))
        # Internal storage variables for the properties ``asset`` and ``precisions``
        self._asset = self._precision = None
Ejemplo n.º 18
0
    def convert_deposit(self, deposit: Deposit, dry=False):
        """
        Takes a Deposit in the 'mapped' state (has been through detect_deposit), and attempts to convert it
        to the destination coin.

        :param Deposit deposit: A :class:`payments.models.Deposit` object to convert
        :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.
        """
        d = deposit

        if d.status != 'mapped':
            raise ConvertError(
                "Deposit is not in 'mapped' state during convert_deposit... Something is very wrong!"
            )
        try:
            if empty(d.convert_to) or empty(d.convert_dest_address):
                raise ConvertError(
                    'Deposit "convert_to" or "convert_dest_addr" is empty... Cannot convert!'
                )
            pair = CoinPair.objects.get(from_coin=d.coin, to_coin=d.convert_to)
            log.debug('Converting deposit ID %s from %s to %s, coin pair: %s',
                      d.id, d.coin, d.convert_to, pair)
            # Convert() will send the coins, update the Deposit, and create the Conversion object in the DB,
            # as well as some additional validation such as balance checks.
            if dry:
                log.debug(
                    f"DRT RUN: Would run ConvertCore.convert({d}, {pair}, {d.convert_dest_address}, "
                    f"dest_memo='{d.convert_dest_memo}')")
                return True
            else:
                return ConvertCore.convert(d,
                                           pair,
                                           d.convert_dest_address,
                                           dest_memo=d.convert_dest_memo)

        except (CoinPair.DoesNotExist, Coin.DoesNotExist):
            raise ConvertInvalid('Deposit is for non-existent coin pair')
Ejemplo n.º 19
0
    def __init__(self, symbol: str):
        super().__init__(symbol)
        settings = self.coin.settings['json']

        rpcs = settings.get('rpcs')
        # If you've specified custom RPC nodes in the custom JSON, make a new instance with those
        # Otherwise, use the global shared_steem_instance.
        self.rpc = shared_steem_instance() if empty(rpcs, itr=True) else Steem(
            rpcs)  # type: Steem
        self.rpc.set_password_storage(settings.get('pass_store',
                                                   'environment'))
        # For easy reference, the Beem asset object, and precision
        self.asset = asset = Asset(self.symbol)
        self.precision = int(asset.precision)
Ejemplo n.º 20
0
    def save(self, *args, **kwargs):
        """
        To ensure that private keys can only be entered / updated from the admin panel and not viewed, we
        encrypt them with AES-128 when saving.

        To avoid encrypting an already encrypted key, we only encrypt the key if we're sure it's not encrypted already.

        :raises EncryptionError:   Something went wrong while encrypting the key
        :raises EncryptKeyMissing: The key ``settings.ENCRYPT_KEY`` is not set or is not a valid encryption key.
        """
        # If the private_key isn't already encrypted, then encrypt it with AES-128 before saving it to the DB
        pk = self.private_key
        if not empty(pk) and not is_encrypted(pk):
            self.private_key = encrypt_str(pk)

        return super(CryptoKeyPair, self).save(*args, **kwargs)
    def health(self) -> Tuple[str, tuple, tuple]:
        """
        Return health data for the passed symbol.

        Health data will include: Symbol, Status, Current Block, Node Version, Wallet Balance,
        and number of p2p connections (all as strings)

        :return tuple health_data: (manager_name:str, headings:list/tuple, health_data:list/tuple,)
        """
        headers = ('Symbol', 'Status', 'API Node', 'Token Name', 'Issuer',
                   'Precision', 'Our Account', 'Our Balance')

        class_name = type(self).__name__
        api_node = token_name = issuer = precision = our_account = balance = ''

        status = 'Okay'
        try:
            rpc = self.eng_rpc
            api_node = rpc.rpc.url
            our_account = self.coin.our_account
            if not rpc.account_exists(our_account):
                status = 'Account {} not found'.format(our_account)
            tk = rpc.get_token(self.symbol)
            if empty(tk, itr=True):
                raise exceptions.TokenNotFound('Token data was empty')
            tk = dict(tk)
            issuer = tk.get('issuer', 'ERROR GETTING ISSUER')
            token_name = tk.get('name', 'ERROR GETTING NAME')
            precision = str(tk.get('precision', 'ERROR GETTING PRECISION'))
            balance = self.balance(our_account)
            balance = ('{0:,.' + str(tk['precision']) + 'f}').format(balance)
        except exceptions.TokenNotFound:
            status = 'ERROR'
            token_name = '<b style="color: red">Token does not exist...</b>'
        except:
            status = 'ERROR'
            log.exception('Exception during %s.health for symbol %s',
                          class_name, self.symbol)

        if status == 'Okay':
            status = '<b style="color: green">{}</b>'.format(status)
        else:
            status = '<b style="color: red">{}</b>'.format(status)

        data = (self.symbol, status, api_node, token_name, issuer, precision,
                our_account, balance)
        return class_name, headers, data
    def handle(self, *args, **options):
        coins = self.coins

        if not empty(options['coins']):
            coins = self.coins.filter(
                symbol__in=[c.upper() for c in options['coins'].split(',')])
            log.info(
                'Option --coins was specified. Only loading TXs for coins: %s',
                [str(c) for c in coins])

        for c in coins:
            try:
                self.load_txs(c.symbol)
            except:
                log.exception(
                    'Error loading transactions for coin %s. Moving onto the next coin.',
                    c)
Ejemplo n.º 23
0
    def load(self, tx_count=1000):
        """
        Prepares the loader by disabling any symbols / coin objects that don't have an `our_account` set, or don't
        have a `contract` set in either :py:attr:`models.Coin.settings_json` or :py:attr:`.default_contracts`

        :param tx_count: Amount of transactions to load per account, most recent first
        :return: None
        """
        chain = self.chain.upper()
        log.info('Loading %s transactions...', chain)

        self.tx_count = tx_count
        # This just forces self.settings to be loaded before we loop over self.coins
        loadsettings = dict(self.settings)
        # Loop over each Coin we're responsible for, make sure every EOS token has both an `our_account` and
        # a contract set (either in Coin.setting_json or EOSMixin.default_contracts). Disable any that don't.
        for symbol, coin in self.coins.items():
            safe = False  # Assume a token is not valid by default.
            symbol = symbol.upper()
            try:
                if empty(coin.our_account):
                    raise AccountNotFound(
                        f'{chain} token "{coin}" has blank `our_account`. Refusing to load TXs.'
                    )
                self.get_contract(symbol)
            except Exception as e:
                log.warning(
                    f'Refusing to load TXs for {chain} token "{coin}". Reason: {type(e)} - {str(e)}'
                )
            else:
                log.debug(
                    f'{chain} token with symbol "{coin}" passed tests. Has non-empty our_account and contract.'
                )
                safe = True
            # If a token didn't pass basic sanity checks (has our account + contract), remove it from coins and symbols.
            if not safe:
                log.debug(
                    f'Removing symbol "{symbol}" from self.coins and self.symbols...'
                )
                del self.coins[symbol]
                self.symbols = [s for s in self.symbols if s != symbol]
        log.debug('Remaining %s symbols that were not disabled: %s', __name__,
                  self.symbols)
        self.loaded = True
Ejemplo n.º 24
0
    def get_actions(self, account: str, count=100) -> List[dict]:
        """
        Loads EOS transactions for a given account, and caches them per account to avoid constant queries.

        :param account:  The EOS account to load transactions for
        :param count:    Amount of transactions to load
        :return list transactions: A list of EOS transactions as dict's
        """
        cache_key = f'{self.chain}_actions:{account}'
        actions = cache.get(cache_key)

        if empty(actions):
            log.info('Loading %s actions for %s from node %s',
                     self.chain.upper(), account, self.url)
            c = self.eos
            data = c.get_actions(account, pos=-1, offset=-count)
            actions = data['actions']
            cache.set(cache_key, actions, timeout=60)

        return actions
Ejemplo n.º 25
0
    def v2_get_actions(self, account: str, count=100) -> List[dict]:
        """
        Loads EOS transactions for a given account, and caches them per account to avoid constant queries.
        Uses v2 account history API

        :param account:  The EOS account to load transactions for
        :param count:    Amount of transactions to load
        :return list transactions: A list of EOS transactions as dict's
        """
        cache_key = f'{self.chain}_v2_actions:{account}'
        actions = cache.get(cache_key)

        if empty(actions):
            log.info('Loading %s v2 actions for %s from node %s',
                     self.chain.upper(), account, self.url)
            url = f'{self.url}/v2/history/get_actions?limit={count}&account={account}'
            req = requests.get(url)
            actions = req.json()['actions']
            cache.set(cache_key, actions, timeout=60)

        return actions
    def _clean_tx(self, tx, symbol, address):
        """Filters an individual transaction. See :meth:`.clean_txs` for info"""

        need_confs = self.settings[symbol]['confirms_needed']
        use_trusted = self.settings[symbol]['use_trusted']

        txid = tx.get('txid', None)
        category = tx.get('category', 'UNKNOWN')
        trust = tx.get('trusted', False)
        amt = tx['amount']
        # To avoid issues with floats, we convert the amount to a string with 8DP
        if type(amt) == float:
            amt = '{0:.8f}'.format(amt)

        log.debug('Filtering/cleaning transaction, Cat: %s, Amt: %s, TXID: %s', category, amt, txid)

        if category != 'receive': return None                                       # Ignore non-receive transactions
        if 'generated' in tx and tx['generated'] in [True, 'true', 1]: return None  # Ignore mining transactions
        # Filter by receiving address if needed
        if not empty(address) and tx['address'] != address: return None
        # If a TX has less confirmations than needed, check if we can trust unconfirmed TXs.
        # If not, we can't accept this TX.
        if int(tx['confirmations']) < need_confs:
            if not use_trusted or trust not in [True, 'true', 1]:
                log.debug('Got %s transaction %s, but only has %d confs, needs %d', symbol, txid,
                          tx['confirmations'], need_confs)
                return None
        d = datetime.utcfromtimestamp(tx['time'])
        d = timezone.make_aware(d, pytz.UTC)

        return dict(
            txid=txid,
            coin=self.coins[symbol].symbol,
            vout=int(tx['vout']),
            tx_timestamp=d,
            address=tx['address'],
            amount=Decimal(amt)
        )
Ejemplo n.º 27
0
    def validate_deposit(deposit: Deposit) -> Tuple[str, CoinPair, str]:
        """
        Validates and identifies the destination CoinPair and account/address of a given Deposit.

        Returns a tuple containing the destination address/account, the CoinPair for conversion, and
        the destination memo (if it has one, otherwise it will be blank or None).

        :param Deposit deposit: The Deposit object to validate and return destination details for

        :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.
        :raises Coin.DoesNotExist: Destination coin doesn't exist
        :raises CoinPair.DoesNotExist: Detected conversion pair does not exist

        :return tuple: (dest_address: str, coin_pair: CoinPair, dest_memo: str)
        """
        d = deposit
        # There's a OneToOne relation between a deposit and a conversion
        # If we try to insert a conversion when a deposit already has one, it'll throw an error...
        if Conversion.objects.filter(deposit=d).count() > 0:
            log.warning(
                'Error: A conversion already exists for deposit "%s". Aborting conversion.',
                d)
            raise ConvertError(
                'A Conversion object already exists for this deposit. An admin should investigate the logs, '
                'make sure no coins were sent in the previous conversion attempt, then remove the related '
                'conversion from the DB.')
        # If you're not supposed to be able to convert from this coin, then we can't process it.
        if d.coin.pairs_from.count() < 1:
            raise ConvertInvalid('No coin pairs with from_coin = {}'.format(
                d.coin.symbol))

        memo = d.memo.strip() if d.memo is not None else None

        # If a deposit has a memo, but not a crypto address, then we parse the memo
        if not empty(memo) and empty(d.address):
            log.debug(
                'Deposit ID %s has a memo, attempting to detect destination pair',
                d.id)
            m = memo.split()
            if len(m) < 2:
                log.warning(
                    'Marking "inv" - memo split by spaces has less than 2 items... "%s"',
                    d)
                raise ConvertInvalid(
                    'Memo is not valid - splitting by whitespace resulted in <2 items.'
                )

            symbol, address = (
                m[0].upper(), m[1]
            )  # First item is dest symbol, second is address/account
            dest_memo = ' '.join(m[2:]) if len(
                m
            ) >= 3 else ''  # 3+ items means there's a destination memo at the end
            pair = CoinPair.objects.get(
                from_coin=d.coin, to_coin=Coin.objects.get(symbol=symbol))
            return address, pair, dest_memo
        if not empty(d.address):
            a_map = AddressAccountMap.objects.filter(deposit_coin=d.coin,
                                                     deposit_address=d.address)
            a_map = a_map.filter(
                deposit_memo=memo) if not empty(memo) else a_map
            a_map = a_map.first()
            if a_map is None:
                raise ConvertInvalid(
                    "Deposit address {} has no known coin destination mapped to it."
                    .format(d.address))

            pair = CoinPair.objects.get(from_coin=d.coin,
                                        to_coin=a_map.destination_coin)
            address = a_map.destination_address
            dest_memo = a_map.destination_memo
            return address, pair, dest_memo

        # If there's no address, and no memo, we have no idea what to do with this deposit
        raise ConvertInvalid(
            'No deposit address nor memo - unable to route this deposit anywhere...'
        )
Ejemplo n.º 28
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
    def send(self, amount, address, memo=None, from_address=None) -> dict:
        """
        Send tokens to a given address/account, optionally specifying a memo. The Bitshares network transaction fee
        will be subtracted from the amount before sending.

        There must be a valid :py:class:`models.CryptoKeyPair` in the database for both 'active' and 'memo' keys for the
        from_address account, or an AuthorityMissing exception will be thrown.

        Example - send 1.23 BUILDTEAM from @someguy123 to @privex with memo 'hello'

            >>> s = BitsharesManager('BUILDTEAM')
            >>> s.send(from_address='someguy123', address='privex', amount=Decimal('1.23'), memo='hello')

        :param Decimal amount:      Amount of tokens to send, as a Decimal()
        :param address:             Account to send the tokens to
        :param from_address:        Account to send the tokens from
        :param memo:                Memo to send tokens with
        :raises AttributeError:     When both `from_address` and `self.coin.our_account` are blank.
        :raises ArithmeticError:    When the amount is lower than the lowest amount allowed by the token's precision
                                    (after subtracting the network transaction fee)
        :raises AuthorityMissing:   Cannot send because we don't have authority to (missing key etc.)
        :raises AccountNotFound:    The requested account/address doesn't exist
        :raises TokenNotFound:      When the requested token `symbol` does not exist
        :raises NotEnoughBalance:   The account `from_address` does not have enough balance to send this amount.
        :return dict: Result Information

        Format::

          {
              txid:str - Transaction ID - None if not known,
              coin:str - Symbol that was sent,
              amount:Decimal - The amount that was sent (after fees),
              fee:Decimal    - TX Fee that was taken from the amount,
              from:str       - The account/address the coins were sent from,
              send_type:str       - Should be statically set to "send"
          }

        """
        # Try from_address first. If that's empty, try using self.coin.our_account. If both are empty, abort.
        if empty(from_address):
            if empty(self.coin.our_account):
                raise AttributeError(
                    "Both 'from_address' and 'coin.our_account' are empty. Cannot send."
                )
            from_address = self.coin.our_account

        # make sure we have the necessary private keys loaded (memo key for encrypting memo, active key for sending coins)
        self.set_wallet_keys(from_address, ['memo', 'active'])

        asset_obj = self.get_asset_obj(self.symbol)
        if asset_obj is None:
            raise exceptions.TokenNotFound(
                f'Failed to send because {self.symbol} is an invalid token symbol.'
            )

        # trim input amount to the token's precision just to be safe
        str_amount = ('{0:.' + str(asset_obj['precision']) +
                      'f}').format(amount)
        amount = Decimal(str_amount)

        if not self.is_amount_above_minimum(amount, asset_obj['precision']):
            raise ArithmeticError(
                f'Failed to send because {amount} is less than the minimum amount allowed for {self.symbol} tokens.'
            )

        from_account = self.get_account_obj(from_address)
        if from_account is None:
            raise exceptions.AccountNotFound(
                f'Failed to send because source account {from_address} could not be found.'
            )
        to_account = self.get_account_obj(address)
        if to_account is None:
            raise exceptions.AccountNotFound(
                f'Failed to send because destination account {address} could not be found.'
            )

        # verify from_account balance is sufficient for the transaction
        from_account_balance = self.get_decimal_from_amount(
            from_account.balance(self.symbol))
        if from_account_balance < amount:
            raise exceptions.NotEnoughBalance(
                f'Failed to send because source account {from_address} balance {from_account_balance} {self.symbol} is less than amount to send ({amount} {self.symbol}).'
            )

        amount_obj = Amount(str_amount,
                            self.symbol,
                            blockchain_instance=self.bitshares)

        try:
            if memo is None:
                memo = ''
            memo_obj = Memo(from_account=from_account,
                            to_account=to_account,
                            blockchain_instance=self.bitshares)
            encrypted_memo = memo_obj.encrypt(memo)

            # build preliminary transaction object, without network transaction fee
            op = operations.Transfer(
                **{
                    'fee': {
                        'amount': 0,
                        'asset_id': amount_obj.asset['id']
                    },
                    'from': from_account['id'],
                    'to': to_account['id'],
                    'amount': {
                        'amount': int(amount_obj),
                        'asset_id': amount_obj.asset['id']
                    },
                    'memo': encrypted_memo,
                    'prefix': self.bitshares.prefix,
                })

            # calculate how much the transaction fee is - rather clunky method here but it works
            ops = [self.bitshares.txbuffer.operation_class(op)]
            ops_with_fees = self.bitshares.txbuffer.add_required_fees(
                ops, asset_id=amount_obj.asset['id'])
            raw_fee_amount = Decimal(
                str(ops_with_fees[0][1].data['fee']['amount']))
            fee_amount_str = '{0:f}'.format(
                raw_fee_amount / (10**amount_obj.asset['precision']))
            fee_amount = Amount(fee_amount_str,
                                self.symbol,
                                blockchain_instance=self.bitshares)
            amount_obj = amount_obj - fee_amount

            # verify the amount still makes sense after subtracting the transaction fee
            if int(amount_obj) < 1:
                raise ArithmeticError(
                    f'Failed to send because {amount} is less than the network transaction fee of {fee_amount_str} {self.symbol} tokens.'
                )

            # correct the transaction object to take into account the transaction fee
            adj_op = operations.Transfer(
                **{
                    'fee': {
                        'amount': int(fee_amount),
                        'asset_id': amount_obj.asset['id']
                    },
                    'from': from_account['id'],
                    'to': to_account['id'],
                    'amount': {
                        'amount': int(amount_obj),
                        'asset_id': amount_obj.asset['id']
                    },
                    'memo': encrypted_memo,
                    'prefix': self.bitshares.prefix,
                })

            log.debug(
                'doing Bitshares transaction - from_address[%s], address[%s], amount[%s %s], fee_amount[%s], amount_obj[%s], memo[%s]',
                from_address, address, str_amount, self.symbol, fee_amount,
                amount_obj, memo)

            # and finally, do the op!
            self.bitshares.finalizeOp(adj_op,
                                      from_address,
                                      "active",
                                      fee_asset=amount_obj.asset['id'])
            result = self.bitshares.broadcast()
        except KeyNotFound as e:
            raise exceptions.AuthorityMissing(str(e))

        return {
            'txid':
            None,  # transaction ID is not readily available from the Bitshares API
            'coin': self.orig_symbol,
            'amount': self.get_decimal_from_amount(amount_obj),
            'fee': self.get_decimal_from_amount(fee_amount),
            'from': from_address,
            'send_type': 'send'
        }
Ejemplo n.º 30
0
    def send(self,
             amount: Decimal,
             address: str,
             from_address: str = None,
             memo: str = None) -> dict:
        """
        Send a supported currency to a given address/account, optionally specifying a memo if supported

        Example - send 1.23 STEEM from @someguy123 to @privex with memo 'hello'

            >>> s = SteemManager('STEEM')
            >>> s.send(from_address='someguy123', address='privex', amount=Decimal('1.23'), memo='hello')

        :param Decimal amount:      Amount of currency to send, as a Decimal()
        :param address:             Account to send the currency to
        :param from_address:        Account to send the currency from
        :param memo:                Memo to send currency with
        :raises AttributeError:     When both `from_address` and `self.coin.our_account` are blank.
        :raises ArithmeticError:    When the amount is lower than the lowest amount allowed by the asset's precision
        :raises AuthorityMissing:   Cannot send because we don't have authority to (missing key etc.)
        :raises AccountNotFound:    The requested account doesn't exist
        :raises NotEnoughBalance:   The account `from_address` does not have enough balance to send this amount.
        :return dict: Result Information

        Format::

          {
              txid:str - Transaction ID - None if not known,
              coin:str - Symbol that was sent,
              amount:Decimal - The amount that was sent (after fees),
              fee:Decimal    - TX Fee that was taken from the amount,
              from:str       - The account/address the coins were sent from,
              send_type:str       - Should be statically set to "send"
          }

        """

        # Try from_address first. If that's empty, try using self.coin.our_account. If both are empty, abort.
        if empty(from_address):
            if empty(self.coin.our_account):
                raise AttributeError(
                    "Both 'from_address' and 'coin.our_account' are empty. Cannot send."
                )
            from_address = self.coin.our_account

        prec = self.precision
        sym = self.symbol
        memo = "" if empty(memo) else memo

        try:
            if type(amount) != Decimal:
                if type(amount) == float:
                    amount = ('{0:.' + str(self.precision) +
                              'f}').format(amount)
                amount = Decimal(amount)
            ###
            # Various sanity checks, e.g. checking amount is valid, to/from account are valid, we have
            # enough balance to send this amt, etc.
            ###
            if amount < Decimal(pow(10, -prec)):
                log.warning(
                    'Amount %s was passed, but is lower than precision for %s',
                    amount, sym)
                raise ArithmeticError(
                    'Amount {} is lower than token {}s precision of {} DP'.
                    format(amount, sym, prec))

            acc = Account(from_address, steem_instance=self.rpc)

            if not self.address_valid(address):
                raise exceptions.AccountNotFound(
                    'Destination account does not exist')
            if not self.address_valid(from_address):
                raise exceptions.AccountNotFound('From account does not exist')

            bal = self.balance(from_address)
            if bal < amount:
                raise exceptions.NotEnoughBalance(
                    'Account {} has balance {} but needs {} to send this tx'.
                    format(from_address, bal, amount))

            ###
            # Broadcast the transfer transaction on the network, and return the necessary data
            ###
            log.debug('Sending %f %s to @%s', amount, sym, address)
            tfr = acc.transfer(address, amount, sym, memo)
            # Beem's finalizeOp doesn't include TXID, so we try to find the TX on the blockchain after broadcast
            tx = self.find_steem_tx(tfr)

            log.debug('Success? TX Data - Transfer: %s Lookup TX: %s', tfr, tx)
            # Return TX data compatible with BaseManager standard
            return {
                # There's a risk we can't get the TXID, and so we fall back to None.
                'txid': tx.get('transaction_id', None),
                'coin': sym,
                'amount': amount,
                'fee': Decimal(0),
                'from': from_address,
                'send_type': 'send'
            }
        except MissingKeyError:
            raise exceptions.AuthorityMissing(
                'Missing active key for sending account {}'.format(
                    from_address))