示例#1
0
文件: structures.py 项目: rotki/rotki
def _evaluate_balance_sheet_input(other: Any, operation: str) -> BalanceSheet:
    transformed_input = other
    if isinstance(other, dict):
        if len(other) == 2 and 'assets' in other and 'liabilities' in other:
            try:
                assets = defaultdict(Balance)
                liabilities = defaultdict(Balance)
                for asset, entry in other['assets'].items():
                    assets[asset] = _evaluate_balance_input(entry, operation)
                for asset, entry in other['liabilities'].items():
                    liabilities[asset] = _evaluate_balance_input(
                        entry, operation)
            except InputError as e:
                raise InputError(
                    f'Found valid dict object but with invalid values '
                    f'during BalanceSheet {operation}', ) from e
            transformed_input = BalanceSheet(assets=assets,
                                             liabilities=liabilities)
        else:
            raise InputError(
                f'Found invalid dict object during BalanceSheet {operation}')
    elif not isinstance(other, BalanceSheet):
        raise InputError(
            f'Found a {type(other)} object during BalanceSheet {operation}')

    return transformed_input
示例#2
0
文件: handler.py 项目: jsloane/rotki
    def delete_custom_asset(identifier: str) -> None:
        """Deletes an asset (non-ethereum token) from the global DB

        May raise InputError if the asset does not exist in the DB or
        some other constraint is hit. Such as for example trying to delete
        an asset that is in another asset's forked or swapped attribute
        """
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()
        try:
            cursor.execute(
                'DELETE FROM assets WHERE identifier=?;',
                (identifier, ),
            )
        except sqlite3.IntegrityError as e:
            asset_data = GlobalDBHandler().get_asset_data(
                identifier, form_with_incomplete_data=False)  # noqa: E501
            if asset_data is None:
                details_str = f'asset with identifier {identifier}'
            else:
                details_str = (f'asset with name "{asset_data.name}" and '
                               f'symbol "{asset_data.symbol}"')
            raise InputError(
                f'Tried to delete {details_str} '
                f'but its deletion would violate a constraint so deletion '
                f'failed. Make sure that this asset is not already used by '
                f'other assets as a swapped_for or forked asset', ) from e

        affected_rows = cursor.rowcount
        if affected_rows != 1:
            raise InputError(
                f'Tried to delete asset with identifier {identifier} '
                f'but it was not found in the DB', )

        connection.commit()
示例#3
0
文件: handler.py 项目: step21/rotki
    def edit_custom_asset(data: Dict[str, Any]) -> None:
        """Edits an already existing custom asset in the DB

        The data should already be typed (as given in by marshmallow).

        May raise InputError if the token already exists or other error

        Returns the asset's identifier
        """
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()

        identifier = data['identifier']
        forked_asset = data.get('forked', None)
        forked = forked_asset.identifier if forked_asset else None
        swapped_for_asset = data.get('swapped_for', None)
        swapped_for = swapped_for_asset.identifier if swapped_for_asset else None
        try:
            cursor.execute(
                'UPDATE assets SET type=?, name=?, symbol=?, started=?, swapped_for=?, '
                'coingecko=?, cryptocompare=? WHERE identifier = ?',
                (
                    data['asset_type'].serialize_for_db(),
                    data.get('name'),
                    data.get('symbol'),
                    data.get('started'),
                    swapped_for,
                    data.get('coingecko'),
                    data.get('cryptocompare'),
                    identifier,
                ),
            )
        except sqlite3.IntegrityError as e:
            raise InputError(
                f'Failed to update DB entry for asset with identifier {identifier} '
                f'due to a constraint being hit. Make sure the new values are valid ',
            ) from e

        if cursor.rowcount != 1:
            raise InputError(
                f'Tried to edit non existing asset with identifier {identifier}',
            )

        # Edit the common asset details
        try:
            cursor.execute(
                'UPDATE common_asset_details SET forked=? WHERE asset_id=?',
                (forked, identifier),
            )
        except sqlite3.IntegrityError as e:
            connection.rollback()
            raise InputError(
                f'Failure at editing custom asset {identifier} common asset details',
            ) from e

        connection.commit()
示例#4
0
    def remove_blockchain_accounts(
        self,
        blockchain: SupportedBlockchain,
        accounts: ListOfBlockchainAddresses,
    ) -> BlockchainBalancesUpdate:
        """Removes blockchain accounts and requeries all balances after the removal.

        The accounts are removed from the blockchain object and not from the database.
        Returns the new total balances, the actually removes accounts (some
        accounts may have been invalid) and also any errors that occured
        during the removal.

        If any of the given accounts are not known an inputError is raised and
        no account is modified.

        May Raise:
        - EthSyncError from modify_blockchain_accounts
        - InputError if the given accounts list is empty, or if
        it contains an unknown account or invalid account
        - RemoteError if an external service such as Etherscan is queried and
          there is a problem
        """
        if len(accounts) == 0:
            raise InputError(
                'Empty list of blockchain accounts to remove was given')

        unknown_accounts = set(accounts).difference(
            self.accounts.get(blockchain))
        if len(unknown_accounts) != 0:
            raise InputError(
                f'Tried to remove unknown {blockchain.value} '
                f'accounts {",".join(unknown_accounts)}', )

        # If no blockchain query has happened before then we need to query the relevant
        # chain to populate the self.balances mapping. But query has to happen after
        # account removal so as not to query unneeded accounts
        balances_queried_before = True
        if not self.balances.is_queried(blockchain):
            balances_queried_before = False

        self.modify_blockchain_accounts(
            blockchain=blockchain,
            accounts=accounts,
            append_or_remove='remove',
            add_or_sub=operator.sub,
        )

        if not balances_queried_before:
            self.query_balances(blockchain, ignore_cache=True)

        result = self.get_balances_update()
        return result
示例#5
0
    def edit_ethereum_token(entry: CustomEthereumToken, ) -> str:
        """Adds a new ethereum token into the global DB

        May raise InputError if the token already exists or other error

        Returns the token's rotki identifier
        """
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()
        db_tuple = entry.to_db_tuple()
        swapped_tuple = (*db_tuple[1:], db_tuple[0])
        try:
            cursor.execute(
                'UPDATE ethereum_tokens SET decimals=?, name=?, symbol=?, started=?, swapped_for=?, '  # noqa: E501
                'coingecko=?, cryptocompare=?, protocol=? WHERE address = ?',
                swapped_tuple,
            )
        except sqlite3.IntegrityError as e:
            raise InputError(
                f'Failed to update DB entry for ethereum token with address {entry.address} '
                f'due to a consraint being hit. Make sure the new values are valid ',
            ) from e

        if cursor.rowcount != 1:
            raise InputError(
                f'Tried to edit non existing ethereum token with address {entry.address}',
            )

        # Since this is editing, make sure no underlying tokens exist
        cursor.execute(
            'DELETE from underlying_tokens_list WHERE parent_token_entry=?',
            (entry.address, ),
        )
        if entry.underlying_tokens is not None:  # and now add any if needed
            GlobalDBHandler()._add_underlying_tokens(
                connection=connection,
                parent_token_address=entry.address,
                underlying_tokens=entry.underlying_tokens,
            )

        rotki_id = GlobalDBHandler().get_ethereum_token_identifier(
            entry.address)
        if rotki_id is None:
            connection.rollback()
            raise InputError(
                f'Unexpected DB state. Ethereum token {entry.address} exists in the DB '
                f'but its corresponding asset entry was not found.', )

        connection.commit()
        return rotki_id
示例#6
0
文件: handler.py 项目: jsloane/rotki
    def _add_underlying_tokens(
        connection: sqlite3.Connection,
        parent_token_address: ChecksumEthAddress,
        underlying_tokens: List[UnderlyingToken],
    ) -> None:
        """Add the underlying tokens for the parent token

        Passing in the connection so it can be rolled back in case of error
        """
        cursor = GlobalDBHandler()._conn.cursor()
        for underlying_token in underlying_tokens:
            # make sure underlying token address is tracked if not already there
            asset_id = GlobalDBHandler.get_ethereum_token_identifier(
                underlying_token.address)  # noqa: E501
            if asset_id is None:
                try:  # underlying token does not exist. Track it
                    cursor.execute(
                        'INSERT INTO ethereum_tokens(address) VALUES(?)',
                        (underlying_token.address, ),
                    )
                    asset_id = ethaddress_to_identifier(
                        underlying_token.address)
                    cursor.execute(
                        """INSERT INTO assets(identifier, type, name, symbol,
                        started, swapped_for, coingecko, cryptocompare, details_reference)
                        VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                        (asset_id, 'C', None, None, None, None, None, None,
                         underlying_token.address),
                    )
                except sqlite3.IntegrityError as e:
                    connection.rollback()
                    raise InputError(
                        f'Failed to add underlying tokens for {parent_token_address} '
                        f'due to {str(e)}', ) from e
            try:
                cursor.execute(
                    'INSERT INTO underlying_tokens_list(address, weight, parent_token_entry) '
                    'VALUES(?, ?, ?)',
                    (
                        underlying_token.address,
                        str(underlying_token.weight),
                        parent_token_address,
                    ),
                )
            except sqlite3.IntegrityError as e:
                connection.rollback()
                raise InputError(
                    f'Failed to add underlying tokens for {parent_token_address} due to {str(e)}',
                ) from e
示例#7
0
    def modify_eth_account(
            self,
            account: ChecksumEthAddress,
            append_or_remove: str,
            add_or_sub: Callable[[FVal, FVal], FVal],
    ) -> None:
        """Either appends or removes an ETH acccount.

        Call with 'append', operator.add to add the account
        Call with 'remove', operator.sub to remove the account

        May raise:
        - Input error if the given_account is not a valid ETH address
        - BadFunctionCallOutput if a token is queried from a local chain
        and the chain is not synced
        - RemoteError if there is a problem with a query to an external
        service such as Etherscan or cryptocompare
        """
        eth_usd_price = Inquirer().find_usd_price(A_ETH)
        remove_with_populated_balance = (
            append_or_remove == 'remove' and len(self.balances.eth) != 0
        )
        # Query the balance of the account except for the case when it's removed
        # and there is no other account in the balances
        if append_or_remove == 'append' or remove_with_populated_balance:
            amount = self.ethchain.get_eth_balance(account)
            usd_value = amount * eth_usd_price

        if append_or_remove == 'append':
            self.accounts.eth.append(account)
            self.balances.eth[account] = EthereumAccountBalance(
                start_eth_amount=amount,
                start_eth_usd_value=usd_value,
            )
        elif append_or_remove == 'remove':
            if account not in self.accounts.eth:
                raise InputError('Tried to remove a non existing ETH account')
            self.accounts.eth.remove(account)
            if account in self.balances.eth:
                del self.balances.eth[account]
        else:
            raise AssertionError('Programmer error: Should be append or remove')

        if len(self.balances.eth) == 0:
            # If the last account was removed balance should be 0
            self.totals[A_ETH].amount = ZERO
            self.totals[A_ETH].usd_value = ZERO
        else:
            self.totals[A_ETH].amount = add_or_sub(self.totals[A_ETH].amount, amount)
            self.totals[A_ETH].usd_value = add_or_sub(self.totals[A_ETH].usd_value, usd_value)

        action = AccountAction.APPEND if append_or_remove == 'append' else AccountAction.REMOVE
        try:
            self._query_ethereum_tokens_alethio(action=action, given_accounts=[account])
        except RemoteError:
            self._query_ethereum_tokens_normal(
                tokens=self.owned_eth_tokens,
                action=action,
                given_accounts=[account],
            )
示例#8
0
    def add_common_asset_details(data: Dict[str, Any]) -> None:
        """Adds a new row in common asset details

        The data should already be typed (as given in by marshmallow).

        May raise InputError if the asset already exists

        Does not commit to the DB. Commit must be called from the caller.
        """
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()
        # assuming they are already serialized
        asset_id = data['identifier']
        forked_asset = data.get('forked', None)
        forked = forked_asset.identifier if forked_asset else None
        try:
            cursor.execute(
                'INSERT INTO common_asset_details(asset_id, forked)'
                'VALUES(?, ?)',
                (asset_id, forked),
            )
        except sqlite3.IntegrityError as e:
            raise InputError(  # should not really happen, marshmallow should check forked for
                f'Adding common asset details for {asset_id} failed',
            ) from e
示例#9
0
文件: manager.py 项目: rudygt/rotki
    def add_blockchain_accounts(
        self,
        blockchain: SupportedBlockchain,
        accounts: ListOfBlockchainAddresses,
    ) -> BlockchainBalancesUpdate:
        """Adds new blockchain accounts and requeries all balances after the addition.
        The accounts are added in the blockchain object and not in the database.
        Returns the new total balances, the actually added accounts (some
        accounts may have been invalid) and also any errors that occured
        during the addition.

        May Raise:
        - EthSyncError from modify_blockchain_accounts
        - InputError if the given accounts list is empty, or if it contains invalid accounts
        - RemoteError if an external service such as Etherscan is queried and
          there is a problem
        """
        if len(accounts) == 0:
            raise InputError(
                'Empty list of blockchain accounts to add was given')

        # If no blockchain query has happened before then we need to query the relevant
        # chain to populate the self.balances mapping.
        if not self.balances.is_queried(blockchain):
            self.query_balances(blockchain, ignore_cache=True)

        result = self.modify_blockchain_accounts(
            blockchain=blockchain,
            accounts=accounts,
            append_or_remove='append',
            add_or_sub=operator.add,
        )

        return result
示例#10
0
    def add_asset(
        asset_id: str,
        asset_type: AssetType,
        data: Union[CustomEthereumToken, Dict[str, Any]],
    ) -> None:
        """May raise InputError in case of error, meaning asset exists or some constraint hit"""
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()

        details_id: Union[str, ChecksumEthAddress]
        if asset_type == AssetType.ETHEREUM_TOKEN:
            token = cast(CustomEthereumToken, data)
            GlobalDBHandler().add_ethereum_token(token)
            details_id = token.address
        else:
            asset_data = cast(Dict[str, Any], data)
            asset_data['identifier'] = asset_id
            GlobalDBHandler().add_common_asset_details(asset_data)
            details_id = asset_id

        try:
            cursor.execute(
                'INSERT INTO assets(identifier, type, details_reference) '
                'VALUES(?, ?, ?)',
                (asset_id, asset_type.serialize_for_db(), details_id),
            )
        except sqlite3.IntegrityError as e:
            connection.rollback()
            raise InputError(
                f'Failed to add asset {asset_id} into the assets table for details id {details_id}',  # noqa: E501
            ) from e

        connection.commit()  # success
示例#11
0
    def add_ethereum_token(entry: CustomEthereumToken) -> None:
        """Adds a new ethereum token into the global DB

        May raise InputError if the token already exists

        Returns the token's rotki identifier
        """
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()
        try:
            cursor.execute(
                'INSERT INTO '
                'ethereum_tokens(address, decimals, name, symbol, started, '
                'swapped_for, coingecko, cryptocompare, protocol) '
                'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)',
                entry.to_db_tuple(),
            )
        except sqlite3.IntegrityError as e:
            exception_msg = str(e)
            if 'FOREIGN KEY' in exception_msg:
                # should not really happen since API should check for this
                msg = (
                    f'Ethereum token with address {entry.address} can not be put '
                    f'in the DB due to swapped for entry not existing')
            else:
                msg = f'Ethereum token with address {entry.address} already exists in the DB'
            raise InputError(msg) from e

        if entry.underlying_tokens is not None:
            GlobalDBHandler()._add_underlying_tokens(
                connection=connection,
                parent_token_address=entry.address,
                underlying_tokens=entry.underlying_tokens,
            )
示例#12
0
文件: handler.py 项目: step21/rotki
    def add_ethereum_token_data(entry: CustomEthereumToken) -> None:
        """Adds ethereum token specific information into the global DB

        May raise InputError if the token already exists
        """
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()
        try:
            cursor.execute(
                'INSERT INTO '
                'ethereum_tokens(address, decimals, protocol) '
                'VALUES(?, ?, ?)',
                (entry.address, entry.decimals, entry.protocol),
            )
        except sqlite3.IntegrityError as e:
            exception_msg = str(e)
            if 'FOREIGN KEY' in exception_msg:
                # should not really happen since API should check for this
                msg = (
                    f'Ethereum token with address {entry.address} can not be put '
                    f'in the DB due to swapped for entry not existing')
            else:
                msg = f'Ethereum token with address {entry.address} already exists in the DB'
            raise InputError(msg) from e

        if entry.underlying_tokens is not None:
            GlobalDBHandler()._add_underlying_tokens(
                connection=connection,
                parent_token_address=entry.address,
                underlying_tokens=entry.underlying_tokens,
            )
示例#13
0
    def track_new_tokens(
            self, new_tokens: List[EthereumToken]) -> BlockchainBalancesUpdate:
        """
        Adds new_tokens to the state and tracks their balance for each account.

        May raise:
        - InputError if some of the tokens already exist
        - RemoteError if an external service such as Etherscan is queried and
          there is a problem with its query.
        - EthSyncError if querying the token balances through a provided ethereum
          client and the chain is not synced
        """

        intersection = set(new_tokens).intersection(set(self.owned_eth_tokens))
        if intersection != set():
            raise InputError(
                'Some of the new provided tokens to track already exist')

        self.owned_eth_tokens.extend(new_tokens)
        if self.balances.eth == {}:
            # if balances have not been yet queried then we should do the entire
            # balance query first in order to create the eth_balances mappings
            self.query_ethereum_balances()
        else:
            # simply update all accounts with any changes adding the token may have
            self.query_ethereum_tokens(tokens=new_tokens, )
        return self.get_balances_update()
示例#14
0
    def add_common_asset_details(data: Dict[str, Any]) -> None:
        """Adds a new row in common asset details

        May raise InputError if the token already exists

        Returns the stringified rowid of the entry
        """
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()
        # assuming they are already serialized
        asset_id = data['identifier']
        entry_tuple = (
            asset_id,
            data['name'],
            data['symbol'],
            data.get('started', None),
            data.get('forked', None),
            data.get('swapped_for', None),
            data.get('coingecko', None),
            data.get('cryptocompare', None),
        )
        try:
            cursor.execute(
                'INSERT INTO common_asset_details('
                'asset_id, name, symbol, started, forked, '
                'swapped_for, coingecko, cryptocompare) '
                'VALUES(?, ?, ?, ?, ?, ?, ?, ?)',
                entry_tuple,
            )
        except sqlite3.IntegrityError as e:
            raise InputError(  # should not really happen
                f'Adding common asset details for {asset_id} failed', ) from e
示例#15
0
    def query_btc_account_balance(account: BTCAddress) -> FVal:
        """Queries blockchain.info for the balance of account

        May raise:
        - InputError if the given account is not a valid BTC address
        - RemotError if there is a problem querying blockchain.info
        """
        try:
            btc_resp = request_get_direct(
                url='https://blockchain.info/q/addressbalance/%s' % account,
                handle_429=True,
                # If we get a 429 then their docs suggest 10 seconds
                # https://blockchain.info/q
                backoff_in_seconds=10,
            )
        except InvalidBTCAddress:
            # TODO: Move this validation into our own code and before the balance query
            raise InputError(
                f'The given string {account} is not a valid BTC address')
        except (requests.exceptions.ConnectionError,
                UnableToDecryptRemoteData) as e:
            raise RemoteError(
                f'blockchain.info API request failed due to {str(e)}')

        return satoshis_to_btc(FVal(btc_resp))  # result is in satoshis
示例#16
0
    def edit_blockchain_accounts(
        self,
        blockchain: SupportedBlockchain,
        account_data: List[BlockchainAccountData],
    ) -> None:
        """Edits blockchain accounts

        Edits blockchain account data for the given accounts

        May raise:
        - InputError if the given accounts list is empty or if
        any of the accounts to edit do not exist.
        - TagConstraintError if any of the given account data contain unknown tags.
        """
        # First check for validity of account data addresses
        if len(account_data) == 0:
            raise InputError(
                'Empty list of blockchain account data to edit was given')
        accounts = [x.address for x in account_data]
        unknown_accounts = set(accounts).difference(
            self.chain_manager.accounts.get(blockchain))
        if len(unknown_accounts) != 0:
            raise InputError(
                f'Tried to edit unknown {blockchain.value} '
                f'accounts {",".join(unknown_accounts)}', )

        # Then See if any of the given tags for the accounts do not exist in the DB
        existing_tags = self.data.db.get_tags()
        existing_tag_keys = existing_tags.keys()
        unknown_tags: Set[str] = set()
        for entry in account_data:
            if entry.tags is not None:
                unknown_tags.update(
                    set(entry.tags).difference(existing_tag_keys))

        if len(unknown_tags) != 0:
            raise TagConstraintError(
                f'When editing blockchain accounts, unknown tags '
                f'{", ".join(unknown_tags)} were found', )

        # Finally edit the accounts
        self.data.db.edit_blockchain_accounts(
            blockchain=blockchain,
            account_data=account_data,
        )

        return None
示例#17
0
文件: handler.py 项目: step21/rotki
    def add_asset(
        asset_id: str,
        asset_type: AssetType,
        data: Union[CustomEthereumToken, Dict[str, Any]],
    ) -> None:
        """
        Add an asset in the DB. Either an ethereum token or a custom asset.

        If it's a custom asset the data should be typed. As given in by marshmallow.

        May raise InputError in case of error, meaning asset exists or some constraint hit"""
        connection = GlobalDBHandler()._conn
        cursor = connection.cursor()

        details_id: Union[str, ChecksumEthAddress]
        if asset_type == AssetType.ETHEREUM_TOKEN:
            token = cast(CustomEthereumToken, data)
            GlobalDBHandler().add_ethereum_token_data(token)
            details_id = token.address
            name = token.name
            symbol = token.symbol
            started = token.started
            swapped_for = token.swapped_for.identifier if token.swapped_for else None
            coingecko = token.coingecko
            cryptocompare = token.cryptocompare
        else:
            details_id = asset_id
            data = cast(Dict[str, Any], data)
            # The data should already be typed (as given in by marshmallow)
            name = data.get('name', None)
            symbol = data.get('symbol', None)
            started = data.get('started', None)
            swapped_for_asset = data.get('swapped_for', None)
            swapped_for = swapped_for_asset.identifier if swapped_for_asset else None
            coingecko = data.get('coingecko', None)
            cryptocompare = data.get('cryptocompare', None)

        try:
            cursor.execute(
                'INSERT INTO assets('
                'identifier, type, name, symbol, started, swapped_for, '
                'coingecko, cryptocompare, details_reference) '
                'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)',
                (asset_id, asset_type.serialize_for_db(), name, symbol,
                 started, swapped_for, coingecko, cryptocompare, details_id),
            )
        except sqlite3.IntegrityError as e:
            connection.rollback()
            raise InputError(
                f'Failed to add asset {asset_id} into the assets table for details id {details_id}',  # noqa: E501
            ) from e

        # for common asset details we have to add them after the addition of the main asset table
        if asset_type != AssetType.ETHEREUM_TOKEN:
            asset_data = cast(Dict[str, Any], data)
            asset_data['identifier'] = asset_id
            GlobalDBHandler().add_common_asset_details(asset_data)

        connection.commit()  # success
示例#18
0
    def track_new_tokens(self, tokens):
        intersection = set(tokens).intersection(set(self.owned_eth_tokens))
        if intersection != set():
            raise InputError('Some of the new provided tokens to track already exist')

        self.owned_eth_tokens.extend(tokens)
        self.query_ethereum_tokens(tokens, self.balances['ETH'])
        return {'per_account': self.balances, 'totals': self.totals}
示例#19
0
文件: nfts.py 项目: rotki/rotki
    def delete_price_for_nft(self, asset: Asset) -> bool:
        cursor = self.db.conn.cursor()
        try:
            cursor.execute(
                'UPDATE nfts SET last_price=?, last_price_asset=? WHERE identifier=?',
                (None, None, asset.identifier),
            )
        except sqlcipher.DatabaseError as e:  # pylint: disable=no-member
            raise InputError(
                f'Failed to delete price for {asset.identifier} due to {str(e)}'
            ) from e  # noqa: E501
        if cursor.rowcount != 1:
            raise InputError(
                f'Failed to delete price for unknown asset {asset.identifier}')

        self.db.update_last_write()
        return True
示例#20
0
文件: manager.py 项目: rizoraz/rotki
    def modify_eth_account(
            self,
            account: ChecksumEthAddress,
            append_or_remove: str,
    ) -> None:
        """Either appends or removes an ETH acccount.

        Call with 'append' to add the account
        Call with 'remove' remove the account

        May raise:
        - Input error if the given_account is not a valid ETH address
        - BadFunctionCallOutput if a token is queried from a local chain
        and the chain is not synced
        - RemoteError if there is a problem with a query to an external
        service such as Etherscan or cryptocompare
        """
        eth_usd_price = Inquirer().find_usd_price(A_ETH)
        remove_with_populated_balance = (
            append_or_remove == 'remove' and len(self.balances.eth) != 0
        )
        # Query the balance of the account except for the case when it's removed
        # and there is no other account in the balances
        if append_or_remove == 'append' or remove_with_populated_balance:
            amount = self.ethereum.get_eth_balance(account)
            usd_value = amount * eth_usd_price

        if append_or_remove == 'append':
            self.accounts.eth.append(account)
            self.balances.eth[account] = BalanceSheet(
                assets=defaultdict(Balance, {A_ETH: Balance(amount, usd_value)}),
            )
            # Check if the new account has any staked eth2 deposits
            self.account_for_staked_eth2_balances([account], at_addition=True)
        elif append_or_remove == 'remove':
            if account not in self.accounts.eth:
                raise InputError('Tried to remove a non existing ETH account')
            self.accounts.eth.remove(account)
            balances = self.balances.eth.get(account, None)
            if balances is not None:
                for asset, balance in balances.assets.items():
                    self.totals.assets[asset] -= balance
                    if self.totals.assets[asset].amount <= ZERO:
                        self.totals.assets[asset] = Balance()
            self.balances.eth.pop(account, None)
        else:
            raise AssertionError('Programmer error: Should be append or remove')

        if len(self.balances.eth) == 0:
            # If the last account was removed balance should be 0
            self.totals.assets[A_ETH] = Balance()
        elif append_or_remove == 'append':
            self.totals.assets[A_ETH] += Balance(amount, usd_value)
            self._query_ethereum_tokens(
                action=AccountAction.APPEND,
                given_accounts=[account],
            )
示例#21
0
    def track_new_tokens(self, tokens: List[typing.EthToken]) -> BlockchainBalancesUpdate:
        intersection = set(tokens).intersection(set(self.owned_eth_tokens))
        if intersection != set():
            raise InputError('Some of the new provided tokens to track already exist')

        self.owned_eth_tokens.extend(tokens)
        eth_balances = cast(EthBalances, self.balances[S_ETH])
        self.query_ethereum_tokens(tokens, eth_balances)
        return {'per_account': self.balances, 'totals': self.totals}
示例#22
0
    def modify_blockchain_account(self, blockchain, account, append_or_remove, add_or_sub):

        if blockchain == 'BTC':
            if append_or_remove == 'remove' and account not in self.accounts['BTC']:
                raise InputError('Tried to remove a non existing BTC account')
            self.modify_btc_account(account, append_or_remove, add_or_sub)

        elif blockchain == 'ETH':
            if append_or_remove == 'remove' and account not in self.accounts['ETH']:
                raise InputError('Tried to remove a non existing ETH account')
            self.modify_eth_account(account, append_or_remove, add_or_sub)
        else:
            raise InputError(
                'Unsupported blockchain {} provided at remove_blockchain_account'.format(
                    blockchain)
            )

        return {'per_account': self.balances, 'totals': self.totals}
示例#23
0
def _evaluate_balance_input(other: Any, operation: str) -> Balance:
    transformed_input = other
    if isinstance(other, dict):
        if len(other) == 2 and 'amount' in other and 'usd_value' in other:
            try:
                amount = FVal(other['amount'])
                usd_value = FVal(other['usd_value'])
            except (ValueError, KeyError) as e:
                raise InputError(
                    f'Found valid dict object but with invalid values during Balance {operation}',
                ) from e
            transformed_input = Balance(amount=amount, usd_value=usd_value)
        else:
            raise InputError(f'Found invalid dict object during Balance {operation}')
    elif not isinstance(other, Balance):
        raise InputError(f'Found a {type(other)} object during Balance {operation}')

    return transformed_input
示例#24
0
    def add_exchange(self, name, api_key, api_secret):
        if name not in SUPPORTED_EXCHANGES:
            raise InputError('Unsupported exchange {}'.format(name))

        cursor = self.conn.cursor()
        cursor.execute(
            'INSERT INTO user_credentials (name, api_key, api_secret) VALUES (?, ?, ?)',
            (name, api_key, api_secret))
        self.conn.commit()
        self.update_last_write()
示例#25
0
    def modify_blockchain_account(
            self,
            blockchain: str,
            account: typing.BlockchainAddress,
            append_or_remove: str,
            add_or_sub: Callable[[FVal, FVal], FVal],
    ) -> BlockchainBalancesUpdate:

        if blockchain == S_BTC:
            if append_or_remove == 'remove' and account not in self.accounts.btc:
                raise InputError('Tried to remove a non existing BTC account')

            # above we check that account is a BTC account
            self.modify_btc_account(
                typing.BTCAddress(account),
                append_or_remove,
                add_or_sub,
            )

        elif blockchain == S_ETH:
            if append_or_remove == 'remove' and account not in self.accounts.eth:
                raise InputError('Tried to remove a non existing ETH account')
            try:
                # above we check that account is an ETH account
                self.modify_eth_account(typing.EthAddress(account), append_or_remove, add_or_sub)
            except BadFunctionCallOutput as e:
                log.error(
                    'Assuming unsynced chain. Got web3 BadFunctionCallOutput '
                    'exception: {}'.format(str(e)),
                )
                raise EthSyncError(
                    'Tried to use the ethereum chain of a local client to edit '
                    'an eth account but the chain is not synced.',
                )

        else:
            raise InputError(
                'Unsupported blockchain {} provided at remove_blockchain_account'.format(
                    blockchain),
            )

        return {'per_account': self.balances, 'totals': self.totals}
示例#26
0
    def query_grant_history(
        self,
        grant_id: Optional[int],
        from_ts: Optional[Timestamp] = None,
        to_ts: Optional[Timestamp] = None,
        only_cache: bool = False,
    ) -> Dict[int, Dict[str, Any]]:
        """May raise:
        - RemotError if there is an error querying the gitcoin API
        - InputError if only_cache is False and grant_id is missing
        """
        if only_cache:
            return self.get_history_from_db(
                grant_id=grant_id,
                from_ts=from_ts,
                to_ts=to_ts,
            )

        if grant_id is None:
            raise InputError(
                'Attempted to query gitcoin events from the api without specifying a grant id',
            )

        entry_name = f'{GITCOIN_GRANTS_PREFIX}_{grant_id}'
        dbranges = DBQueryRanges(self.db)
        from_timestamp = GITCOIN_START_TS if from_ts is None else from_ts
        to_timestamp = ts_now() if to_ts is None else to_ts
        ranges = dbranges.get_location_query_ranges(
            location_string=entry_name,
            start_ts=from_timestamp,
            end_ts=to_timestamp,
        )
        grant_created_on: Optional[Timestamp] = None

        for period_range in ranges:
            actions, grant_created_on = self.query_grant_history_period(
                grant_id=grant_id,
                grant_created_on=grant_created_on,
                from_timestamp=period_range[0],
                to_timestamp=period_range[1],
            )
            self.db_ledger.add_ledger_actions(actions)

        dbranges.update_used_query_range(
            location_string=entry_name,
            start_ts=from_timestamp,
            end_ts=to_timestamp,
            ranges_to_query=ranges,
        )
        return self.get_history_from_db(
            grant_id=grant_id,
            from_ts=from_ts,
            to_ts=to_ts,
        )
示例#27
0
    def remove_blockchain_accounts(
        self,
        blockchain: SupportedBlockchain,
        accounts: ListOfBlockchainAddresses,
    ) -> Tuple[BlockchainBalancesUpdate, ListOfBlockchainAddresses, str]:
        """Removes blockchain accounts and requeries all balances after the removal.

        The accounts are removed from the blockchain object and not from the database.
        Returns the new total balances, the actually removes accounts (some
        accounts may have been invalid) and also any errors that occured
        during the removal.

        May Raise:
        - EthSyncError from modify_blockchain_account
        - InputError if the given accounts list is empty
        - RemoteError if an external service such as Etherscan is queried and
          there is a problem
        """
        if len(accounts) == 0:
            raise InputError(
                'Empty list of blockchain accounts to add was given')

        # If no blockchain query has happened before then we need to query the relevant
        # chain to populate the self.balances mapping. But query has to happen after
        # account removal so as not to query unneeded accounts
        balances_queried_before = True
        if blockchain.value not in self.balances:
            balances_queried_before = False

        removed_accounts = []
        full_msg = ''
        for account in accounts:
            try:
                self.modify_blockchain_account(
                    blockchain=blockchain,
                    account=account,
                    append_or_remove='remove',
                    add_or_sub=operator.sub,
                )
                removed_accounts.append(account)
            except InputError as e:
                full_msg += '. ' + str(e)

        if not balances_queried_before:
            self.query_balances(blockchain, ignore_cache=True)

        result: BlockchainBalancesUpdate = {
            'per_account': self.balances,
            'totals': self.totals
        }

        # Ignore type checks here. removed_accounts is the same type as accounts
        # but not sure how to show that to mypy
        return result, removed_accounts, full_msg  # type: ignore
示例#28
0
    def get_report_data(
        self,
        filter_: ReportDataFilterQuery,
        with_limit: bool,
    ) -> Tuple[List[Dict[str, Any]], int]:
        """Retrieve the event data of a PnL report depending on the given filter

        May raise:
        - InputError if the report ID does not exist in the DB
        """
        cursor = self.db.conn_transient.cursor()
        report_id = filter_.report_id
        query_result = cursor.execute(
            'SELECT COUNT(*) FROM pnl_reports WHERE identifier=?',
            (report_id, ),
        )
        if query_result.fetchone()[0] != 1:
            raise InputError(
                f'Tried to get PnL events from non existing report with id {report_id}',
            )

        query, bindings = filter_.prepare()
        query = 'SELECT event_type, data FROM pnl_events ' + query
        results = cursor.execute(query, bindings)

        records = []
        for result in results:
            try:
                record = NamedJson.deserialize_from_db(result).data
            except DeserializationError as e:
                self.db.msg_aggregator.add_error(
                    f'Error deserializing AccountingEvent from the DB. Skipping it.'
                    f'Error was: {str(e)}', )
                continue

            records.append(record)

        if filter_.pagination is not None:
            no_pagination_filter = deepcopy(filter_)
            no_pagination_filter.pagination = None
            query, bindings = no_pagination_filter.prepare()
            query = 'SELECT COUNT(*) FROM pnl_events ' + query
            results = cursor.execute(query, bindings).fetchone()
            total_filter_count = results[0]
        else:
            total_filter_count = len(records)

        return _get_reports_or_events_maybe_limit(
            entry_type='events',
            entries_found=total_filter_count,
            entries=records,
            with_limit=with_limit,
        )
示例#29
0
    def edit_blockchain_accounts(
        self,
        blockchain: SupportedBlockchain,
        account_data: List[BlockchainAccountData],
    ) -> None:
        """Edits blockchain accounts

        Edits blockchain account data for the given accounts

        May raise:
        - InputError if the given accounts list is empty or if
        any of the accounts to edit do not exist.
        - TagConstraintError if any of the given account data contain unknown tags.
        """
        # First check for validity of account data addresses
        if len(account_data) == 0:
            raise InputError(
                'Empty list of blockchain account data to edit was given')
        accounts = [x.address for x in account_data]
        unknown_accounts = set(accounts).difference(
            self.chain_manager.accounts.get(blockchain))
        if len(unknown_accounts) != 0:
            raise InputError(
                f'Tried to edit unknown {blockchain.value} '
                f'accounts {",".join(unknown_accounts)}', )

        self.data.db.ensure_tags_exist(
            given_data=account_data,
            action='editing',
            data_type='blockchain accounts',
        )

        # Finally edit the accounts
        self.data.db.edit_blockchain_accounts(
            blockchain=blockchain,
            account_data=account_data,
        )

        return None
示例#30
0
    def purge_report_data(self, report_id: int) -> None:
        """Deletes all report data of the given report from the DB

        Raises InputError if the report did not exist in the DB.
        """
        cursor = self.db.conn_transient.cursor()
        cursor.execute('DELETE FROM pnl_reports WHERE identifier=?',
                       (report_id, ))
        if cursor.rowcount != 1:
            raise InputError(
                f'Could not delete PnL report {report_id} from the DB. Report was not found',
            )
        self.db.conn.commit()
        self.db.update_last_write()