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 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'
        }
Exemple #3
0
    def send(self, amount, address, memo=None, from_address=None) -> dict:
        """
        Send tokens to a given address/account, optionally specifying a memo if supported

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

            >>> s = SteemEngineManager('SGTK')
            >>> 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 (if supported)
        :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
        :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
        try:
            token = self.eng_rpc.get_token(symbol=self.symbol)

            # If we get passed a float for some reason, make sure we trim it to the token's precision before
            # converting it to a Decimal.
            if type(amount) == float:
                amount = ('{0:.' + str(token['precision']) + 'f}').format(amount)
            amount = Decimal(amount)

            log.debug('Sending %f %s to @%s', amount, self.symbol, address)

            t = self.eng_rpc.send_token(symbol=self.symbol, from_acc=from_address,
                                        to_acc=address, amount=amount, memo=memo)
            txid = None  # There's a risk we can't get the TXID, and so we fall back to None.
            if 'transaction_id' in t:
                txid = t['transaction_id']
            return {
                'txid': txid,
                'coin': self.symbol,
                'amount': amount,
                'fee': Decimal(0),
                'from': from_address,
                'send_type': 'send'
            }
        except SENG.AccountNotFound as e:
            raise exceptions.AccountNotFound(str(e))
        except SENG.TokenNotFound as e:
            raise exceptions.TokenNotFound(str(e))
        except SENG.NotEnoughBalance as e:
            raise exceptions.NotEnoughBalance(str(e))
        except MissingKeyError:
            raise exceptions.AuthorityMissing('Missing active key for sending account {}'.format(from_address))
    def issue(self, amount: Decimal, address: str, memo: str = None, trigger_data=None) -> dict:
        """
        Issue (create/print) tokens to a given address/account, optionally specifying a memo if desired.
        The network transaction fee for issuance will be paid by the issuing account in BTS.

        Example - Issue 5.10 SGTK to @privex

            >>> s = BitsharesManager('SGTK')
            >>> s.issue(address='privex', amount=Decimal('5.10'))

        :param Decimal amount:      Amount of tokens to issue, as a Decimal
        :param address:             Account to issue the tokens to (which is also the issuer account)
        :param memo:                Optional memo for the issuance transaction
        :raises IssuerKeyError:     Cannot issue because we don't have authority to (missing key etc.)
        :raises TokenNotFound:      When the requested token `symbol` does not exist
        :raises AccountNotFound:    The requested account doesn't exist
        :raises ArithmeticError:    When the amount is lower than the lowest amount allowed by the token's precision
        :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 issued,
              fee:Decimal    - TX Fee that was taken from the amount (will be 0 if fee is in BTS rather than the issuing token),
              from:str       - The account/address the coins were issued from,
              send_type:str       - Should be statically set to "issue"
          }

        """
        asset_obj = self.get_asset_obj(self.symbol)
        if asset_obj is None:
            raise exceptions.TokenNotFound(f'Failed to issue 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 issue because {amount} is less than the minimum amount allowed for {self.symbol} tokens.')

        to_account = self.get_account_obj(address)
        if to_account is None:
            raise exceptions.AccountNotFound(f'Failed to issue because issuing account {address} could not be found.')

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

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

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

            # construct the transaction - note that transaction fee for issuance will be paid in BTS, but we retain the
            # flexibility to add support for paying the fee in the issuing token if deemed necessary
            op = operations.Asset_issue(
                **{
                    "fee": {"amount": 0, "asset_id": "1.3.0"},
                    "issuer": to_account["id"],
                    "asset_to_issue": {"amount": int(amount_obj), "asset_id": amount_obj.asset["id"]},
                    "memo": encrypted_memo,
                    "issue_to_account": to_account["id"],
                    "extensions": [],
                    "prefix": self.bitshares.prefix,
                }
            )

            log.debug('doing token issuance - address[%s], amount[%s %s], memo[%s]', address, str_amount, self.symbol, memo)

            # broadcast the transaction
            self.bitshares.finalizeOp(op, to_account, "active")
            result = self.bitshares.broadcast()
        except KeyNotFound as e:
            raise exceptions.IssuerKeyError(str(e))
        except exceptions.AuthorityMissing as e:
            raise exceptions.IssuerKeyError(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': Decimal(0),     # fee is in BTS, not the issuing token
            'from': address,
            'send_type': 'issue'
        }