Exemple #1
0
    def get_eth_balance(self, account: typing.EthAddress) -> FVal:
        if not self.connected:
            log.debug(
                'Querying etherscan for account balance',
                sensitive_log=True,
                eth_address=account,
            )
            eth_resp = request_get_dict(
                'https://api.etherscan.io/api?module=account&action=balance&address=%s'
                % account,
            )
            if eth_resp['status'] != 1:
                raise ValueError('Failed to query etherscan for accounts balance')
            amount = FVal(eth_resp['result'])

            log.debug(
                'Etherscan account balance result',
                sensitive_log=True,
                eth_address=account,
                wei_amount=amount,
            )
            return from_wei(amount)
        else:
            wei_amount = self.web3.eth.getBalance(account)  # pylint: disable=no-member
            log.debug(
                'Ethereum node account balance result',
                sensitive_log=True,
                eth_address=account,
                wei_amount=wei_amount,
            )
            return from_wei(wei_amount)
Exemple #2
0
    def get_multieth_balance(
        self,
        accounts: List[ChecksumEthAddress],
    ) -> Dict[ChecksumEthAddress, FVal]:
        """Returns a dict with keys being accounts and balances in ETH"""
        balances = {}

        if not self.connected:
            if len(accounts) > 20:
                new_accounts = [
                    accounts[x:x + 2] for x in range(0, len(accounts), 2)
                ]
            else:
                new_accounts = [accounts]

            for account_slice in new_accounts:
                log.debug(
                    'Querying etherscan for multiple accounts balance',
                    sensitive_log=True,
                    eth_accounts=account_slice,
                )
                eth_resp = request_get_dict(
                    'https://api.etherscan.io/api?module=account&action=balancemulti&address=%s'
                    % ','.join(account_slice), )
                if eth_resp['status'] != 1:
                    raise ValueError(
                        'Failed to query etherscan for accounts balance')
                eth_accounts = eth_resp['result']

                for account_entry in eth_accounts:
                    amount = FVal(account_entry['balance'])
                    # Etherscan does not return accounts checksummed so make sure they
                    # are converted properly here
                    checksum_account = to_checksum_address(
                        account_entry['account'])
                    balances[checksum_account] = from_wei(amount)
                    log.debug(
                        'Etherscan account balance result',
                        sensitive_log=True,
                        eth_address=account_entry['account'],
                        wei_amount=amount,
                    )

        else:
            for account in accounts:
                amount = FVal(self.web3.eth.getBalance(account))  # pylint: disable=no-member
                log.debug(
                    'Ethereum node balance result',
                    sensitive_log=True,
                    eth_address=account,
                    wei_amount=amount,
                )
                balances[account] = from_wei(amount)

        return balances
Exemple #3
0
    def get_multieth_balance(
        self,
        accounts: List[ChecksumEthAddress],
        call_order: Optional[Sequence[NodeName]] = None,
    ) -> Dict[ChecksumEthAddress, FVal]:
        """Returns a dict with keys being accounts and balances in ETH

        May raise:
        - RemoteError if an external service such as Etherscan is queried and
          there is a problem with its query.
        """
        balances: Dict[ChecksumEthAddress, FVal] = {}
        log.debug(
            'Querying ethereum chain for ETH balance',
            eth_addresses=accounts,
        )
        result = ETH_SCAN.call(
            ethereum=self,
            method_name='etherBalances',
            arguments=[accounts],
            call_order=call_order
            if call_order is not None else self.default_call_order(),
        )
        balances = {}
        for idx, account in enumerate(accounts):
            balances[account] = from_wei(result[idx])
        return balances
Exemple #4
0
    def get_multieth_balance(
        self,
        accounts: List[ChecksumEthAddress],
    ) -> Dict[ChecksumEthAddress, FVal]:
        """Returns a dict with keys being accounts and balances in ETH

        May raise:
        - RemoteError if an external service such as Etherscan is queried and
          there is a problem with its query.
        """
        balances: Dict[ChecksumEthAddress, FVal] = {}
        log.debug(
            'Querying ethereum chain for ETH balance',
            eth_addresses=accounts,
        )
        result = self.call_contract(
            contract_address=ETH_SCAN.address,
            abi=ETH_SCAN.abi,
            method_name='etherBalances',
            arguments=[accounts],
        )
        balances = {}
        for idx, account in enumerate(accounts):
            balances[account] = from_wei(result[idx])
        return balances
Exemple #5
0
    def _decode_send_eth(  # pylint: disable=no-self-use
        self,
        tx_log: EthereumTxReceiptLog,
        transaction: EthereumTransaction,  # pylint: disable=unused-argument
        decoded_events: List[HistoryBaseEntry],  # pylint: disable=unused-argument
        all_logs: List[EthereumTxReceiptLog],  # pylint: disable=unused-argument
        action_items: List[ActionItem],  # pylint: disable=unused-argument
    ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]:
        if tx_log.topics[0] != TRANSFER_TO_L2:
            return None, None

        chain_id = hex_or_bytes_to_int(tx_log.topics[1])
        recipient = hex_or_bytes_to_address(tx_log.topics[2])
        amount_raw = hex_or_bytes_to_int(tx_log.data[:32])

        name = chainid_to_name.get(chain_id,
                                   f'Unknown Chain with id {chain_id}')
        amount = from_wei(FVal(amount_raw))

        for event in decoded_events:
            if event.event_type == HistoryEventType.SPEND and event.counterparty == ETH_BRIDGE and event.asset == A_ETH and event.balance.amount == amount:  # noqa: E501
                if recipient == event.location_label:
                    target_str = 'at the same address'
                else:
                    target_str = f'at address {recipient}'
                event.event_type = HistoryEventType.TRANSFER
                event.event_subtype = HistoryEventSubType.BRIDGE
                event.counterparty = CPT_HOP
                event.notes = f'Bridge {amount} ETH to {name} {target_str} via Hop protocol'
                break

        return None, None
Exemple #6
0
    def get_historical_eth_balance(
            self,
            address: ChecksumEthAddress,
            block_number: int,
    ) -> Optional[FVal]:
        """Attempts to get a historical eth balance from the local own node only.
        If there is no node or the node can't query historical balance (not archive) then
        returns None"""
        web3 = self.web3_mapping.get(NodeName.OWN)
        if web3 is None:
            return None

        try:
            result = web3.eth.get_balance(address, block_identifier=block_number)
        except (
                requests.exceptions.RequestException,
                BlockchainQueryError,
                KeyError,  # saw this happen inside web3.py if resulting json contains unexpected key. Happened with mycrypto's node  # noqa: E501
        ):
            return None

        try:
            balance = from_wei(FVal(result))
        except ValueError:
            return None

        return balance
Exemple #7
0
    def get_multieth_balance(
        self,
        accounts: List[ChecksumEthAddress],
    ) -> Dict[ChecksumEthAddress, FVal]:
        """Returns a dict with keys being accounts and balances in ETH

        May raise:
        - RemoteError if an external service such as Etherscan is queried and
          there is a problem with its query.
        """
        balances: Dict[ChecksumEthAddress, FVal] = {}

        if not self.connected:
            balances = self.etherscan.get_accounts_balance(accounts)
        else:
            for account in accounts:
                amount = FVal(self.web3.eth.getBalance(account))  # pylint: disable=no-member
                log.debug(
                    'Ethereum node balance result',
                    sensitive_log=True,
                    eth_address=account,
                    wei_amount=amount,
                )
                balances[account] = from_wei(amount)

        return balances
Exemple #8
0
    def get_avax_balance(self, account: ChecksumEthAddress) -> FVal:
        """Gets the balance of the given account in AVAX

        May raise:
        - RemoteError if Covalent is used and there is a problem querying it or
        parsing its response
        """
        result = self.covalent.get_token_balances_address(account)
        if result is None:
            balance = from_wei(FVal(self.w3.eth.get_balance(account)))
        else:
            balance = ZERO
            for entry in result:
                if entry.get('contract_ticker_symbol') == 'AVAX':
                    balance = from_wei(FVal(entry.get('balance', 0)))
                    break
            if balance == ZERO:
                balance = from_wei(FVal(self.w3.eth.get_balance(account)))
        return FVal(balance)
Exemple #9
0
    def get_accounts_balance(
        self,
        accounts: List[ChecksumEthAddress],
    ) -> Dict[ChecksumEthAddress, FVal]:
        """Gets the balance of the given accounts in ETH

        May raise:
        - RemoteError due to self._query(). Also if the returned result
        is not in the expected format
        """
        # Etherscan can only accept up to 20 accounts in the multi account balance endpoint
        if len(accounts) > 20:
            new_accounts = [
                accounts[x:x + 2] for x in range(0, len(accounts), 2)
            ]
        else:
            new_accounts = [accounts]

        balances = {}
        for account_slice in new_accounts:
            result = self._query(
                module='account',
                action='balancemulti',
                options={'address': ','.join(account_slice)},
            )
            if not isinstance(result, list):
                raise RemoteError(
                    f'Etherscan multibalance result {result} is in unexpected format',
                )

            try:
                for account_entry in result:
                    amount = FVal(account_entry['balance'])
                    # Etherscan does not return accounts checksummed so make sure they
                    # are converted properly here
                    checksum_account = to_checksum_address(
                        account_entry['account'])
                    balances[checksum_account] = from_wei(amount)
                    log.debug(
                        'Etherscan account balance result',
                        sensitive_log=True,
                        eth_address=account_entry['account'],
                        wei_amount=amount,
                    )
            except (KeyError, ValueError):
                raise RemoteError(
                    'Unexpected data format in etherscan multibalance response: {result}',
                )

        return balances
Exemple #10
0
    def _maybe_decode_internal_transactions(
        self,
        tx: EthereumTransaction,
        tx_receipt: EthereumTxReceipt,
        events: List[HistoryBaseEntry],
        tx_hash_hex: str,
        ts_ms: TimestampMS,
    ) -> None:
        """
        check for internal transactions if the transaction is not canceled. This function mutates
        the events argument.
        """
        if tx_receipt.status is False:
            return

        internal_txs = self.dbethtx.get_ethereum_internal_transactions(
            parent_tx_hash=tx.tx_hash, )
        for internal_tx in internal_txs:
            if internal_tx.to_address is None:
                continue  # can that happen? Internal transaction deploying a contract?
            direction_result = self.base.decode_direction(
                internal_tx.from_address, internal_tx.to_address)  # noqa: E501
            if direction_result is None:
                continue

            amount = ZERO if internal_tx.value == 0 else from_wei(
                FVal(internal_tx.value))
            if amount == ZERO:
                continue

            event_type, location_label, counterparty, verb = direction_result
            events.append(
                HistoryBaseEntry(
                    event_identifier=tx_hash_hex,
                    sequence_index=self.base.get_next_sequence_counter(),
                    timestamp=ts_ms,
                    location=Location.BLOCKCHAIN,
                    location_label=location_label,
                    asset=A_ETH,
                    balance=Balance(amount=amount),
                    notes=
                    f'{verb} {amount} ETH {internal_tx.from_address} -> {internal_tx.to_address}',  # noqa: E501
                    event_type=event_type,
                    event_subtype=HistoryEventSubType.NONE,
                    counterparty=counterparty,
                ))
Exemple #11
0
    def get_eth_balance(self, account: ChecksumEthAddress) -> FVal:
        """Gets the balance of the given account in ETH

        May raise:
        - RemoteError if Etherscan is used and there is a problem querying it or
        parsing its response
        """
        if not self.connected:
            wei_amount = self.etherscan.get_account_balance(account)
        else:
            wei_amount = self.web3.eth.getBalance(account)  # pylint: disable=no-member

        log.debug(
            'Ethereum account balance result',
            sensitive_log=True,
            eth_address=account,
            wei_amount=wei_amount,
        )
        return from_wei(wei_amount)
Exemple #12
0
def test_eth_connection_initial_balances(
        blockchain,
        number_of_eth_accounts,
        ethereum_accounts,
        inquirer,  # pylint: disable=unused-argument
):
    result = blockchain.query_balances()

    per_eth_account = result.per_account.eth
    assert len(ethereum_accounts) == len(per_eth_account) == number_of_eth_accounts

    eth_default_balance = from_wei(DEFAULT_BALANCE)
    for acc, values in per_eth_account.items():
        assert acc in ethereum_accounts
        assert values.asset_balances['ETH'].amount == eth_default_balance
        assert values.asset_balances['ETH'].usd_value > ZERO
        assert values.total_usd_value > ZERO

    totals_eth = result.totals['ETH']
    assert totals_eth.amount == number_of_eth_accounts * eth_default_balance
    assert totals_eth.usd_value > ZERO
Exemple #13
0
def test_eth_connection_initial_balances(
        blockchain,
        number_of_eth_accounts,
        ethereum_accounts,
        inquirer,  # pylint: disable=unused-argument
):
    result = blockchain.query_balances()
    assert 'per_account' in result
    assert 'totals' in result

    per_eth_account = result['per_account']['ETH']
    assert len(ethereum_accounts) == len(
        per_eth_account) == number_of_eth_accounts

    eth_default_balance = from_wei(DEFAULT_BALANCE)
    for acc, values in per_eth_account.items():
        assert acc in ethereum_accounts
        assert values['assets']['ETH']['amount'] == eth_default_balance
        assert 'usd_value' in values['assets']['ETH']

    totals_eth = result['totals']['ETH']
    assert totals_eth['amount'] == number_of_eth_accounts * eth_default_balance
    assert 'usd_value' in totals_eth
Exemple #14
0
def assert_eth_balances_result(
        rotki: Rotkehlchen,
        result: Dict[str, Any],
        eth_accounts: List[str],
        eth_balances: List[str],
        token_balances: Dict[EthereumToken, List[str]],
        also_btc: bool,
        expected_liabilities: Dict[EthereumToken, List[str]] = None,
        totals_only: bool = False,
) -> None:
    """Asserts for correct ETH blockchain balances when mocked in tests

    If totals_only is given then this is a query for all balances so only the totals are shown
    """
    if not totals_only:
        per_account = result['per_account']
        if also_btc:
            assert len(per_account) == 2
        else:
            assert len(per_account) == 1
        per_account = per_account['ETH']
        assert len(per_account) == len(eth_accounts)
        for idx, account in enumerate(eth_accounts):
            expected_amount = from_wei(FVal(eth_balances[idx]))
            amount = FVal(per_account[account]['assets']['ETH']['amount'])
            usd_value = FVal(per_account[account]['assets']['ETH']['usd_value'])
            assert amount == expected_amount
            if amount == ZERO:
                assert usd_value == ZERO
            else:
                assert usd_value > ZERO
            for token, balances in token_balances.items():
                expected_token_amount = FVal(balances[idx])
                if expected_token_amount == ZERO:
                    msg = f'{account} should have no entry for {token}'
                    assert token.identifier not in per_account[account], msg
                else:
                    token_amount = FVal(per_account[account]['assets'][token.identifier]['amount'])
                    usd_value = FVal(
                        per_account[account]['assets'][token.identifier]['usd_value'],
                    )
                    assert token_amount == from_wei(expected_token_amount)
                    assert usd_value > ZERO

    if totals_only:
        totals = result
    else:
        totals = result['totals']['assets']

    if expected_liabilities is not None:
        per_account = result['per_account']['ETH']
        for token, balances in expected_liabilities.items():
            total_amount = ZERO
            for idx, account in enumerate(eth_accounts):
                amount = FVal(per_account[account]['liabilities'][token.identifier]['amount'])
                assert amount == FVal(balances[idx])
                total_amount += amount

            assert FVal(result['totals']['liabilities'][token.identifier]['amount']) == total_amount  # noqa: E501

    # Check our owned eth tokens here since the test may have changed their number
    owned_assets = set(rotki.chain_manager.totals.assets.keys())
    if not also_btc:
        owned_assets.discard(A_BTC)
    assert len(totals) == len(owned_assets)

    expected_total_eth = sum(from_wei(FVal(balance)) for balance in eth_balances)
    assert FVal(totals['ETH']['amount']) == expected_total_eth
    if expected_total_eth == ZERO:
        assert FVal(totals['ETH']['usd_value']) == ZERO
    else:
        assert FVal(totals['ETH']['usd_value']) > ZERO

    for token, balances in token_balances.items():
        symbol = token.identifier

        expected_total_token = sum(from_wei(FVal(balance)) for balance in balances)
        assert FVal(totals[symbol]['amount']) == expected_total_token
        if expected_total_token == ZERO:
            msg = f"{FVal(totals[symbol]['usd_value'])} is not ZERO"
            assert FVal(totals[symbol]['usd_value']) == ZERO, msg
        else:
            assert FVal(totals[symbol]['usd_value']) > ZERO
Exemple #15
0
def assert_eth_balances_result(
    rotki: Rotkehlchen,
    json_data: Dict[str, Any],
    eth_accounts: List[str],
    eth_balances: List[str],
    token_balances: Dict[EthereumToken, List[str]],
    also_btc: bool,
    totals_only: bool = False,
) -> None:
    """Asserts for correct ETH blockchain balances when mocked in tests

    If totals_only is given then this is a query for all balances so only the totals are shown
    """
    result = json_data['result']
    if not totals_only:
        per_account = result['per_account']
        if also_btc:
            assert len(per_account) == 2
        else:
            assert len(per_account) == 1
        per_account = per_account['ETH']
        assert len(per_account) == len(eth_accounts)
        for idx, account in enumerate(eth_accounts):
            expected_amount = from_wei(FVal(eth_balances[idx]))
            amount = FVal(per_account[account]['assets']['ETH']['amount'])
            usd_value = FVal(
                per_account[account]['assets']['ETH']['usd_value'])
            assert amount == expected_amount
            if amount == ZERO:
                assert usd_value == ZERO
            else:
                assert usd_value > ZERO
            have_tokens = False
            for token, balances in token_balances.items():
                expected_token_amount = FVal(balances[idx])
                if expected_token_amount == ZERO:
                    msg = f'{account} should have no entry for {token}'
                    assert token.identifier not in per_account[account], msg
                else:
                    have_tokens = True
                    token_amount = FVal(per_account[account]['assets'][
                        token.identifier]['amount'])
                    usd_value = FVal(
                        per_account[account]['assets'][token.identifier]
                        ['usd_value'], )
                    assert token_amount == from_wei(expected_token_amount)
                    assert usd_value > ZERO

            account_total_usd_value = FVal(
                per_account[account]['total_usd_value'])
            if amount != ZERO or have_tokens:
                assert account_total_usd_value > ZERO
            else:
                assert account_total_usd_value == ZERO

    if totals_only:
        totals = result
    else:
        totals = result['totals']

    # Check our owned eth tokens here since the test may have changed their number
    expected_totals_num = 1 + len(rotki.chain_manager.owned_eth_tokens)
    if also_btc:
        assert len(totals) == expected_totals_num + 1
    else:
        assert len(totals) == expected_totals_num

    expected_total_eth = sum(
        from_wei(FVal(balance)) for balance in eth_balances)
    assert FVal(totals['ETH']['amount']) == expected_total_eth
    if expected_total_eth == ZERO:
        assert FVal(totals['ETH']['usd_value']) == ZERO
    else:
        assert FVal(totals['ETH']['usd_value']) > ZERO

    for token, balances in token_balances.items():
        symbol = token.identifier
        if token not in rotki.chain_manager.owned_eth_tokens:
            # If the token got removed from the owned tokens in the test make sure
            # it's not in the totals anymore
            assert symbol not in totals
            continue

        expected_total_token = sum(
            from_wei(FVal(balance)) for balance in balances)
        assert FVal(totals[symbol]['amount']) == expected_total_token
        if expected_total_token == ZERO:
            msg = f"{FVal(totals[symbol]['usd_value'])} is not ZERO"
            assert FVal(totals[symbol]['usd_value']) == ZERO, msg
        else:
            assert FVal(totals[symbol]['usd_value']) > ZERO
Exemple #16
0
    def _maybe_decode_simple_transactions(
        self,
        tx: EthereumTransaction,
        tx_receipt: EthereumTxReceipt,
    ) -> List[HistoryBaseEntry]:
        """Decodes normal ETH transfers, internal transactions and gas cost payments"""
        events: List[HistoryBaseEntry] = []
        tx_hash_hex = tx.tx_hash.hex()
        ts_ms = ts_sec_to_ms(tx.timestamp)

        # check for gas spent
        direction_result = self.base.decode_direction(tx.from_address,
                                                      tx.to_address)
        if direction_result is not None:
            event_type, location_label, counterparty, verb = direction_result
            if event_type in (HistoryEventType.SPEND,
                              HistoryEventType.TRANSFER):
                eth_burned_as_gas = from_wei(FVal(tx.gas_used * tx.gas_price))
                events.append(
                    HistoryBaseEntry(
                        event_identifier=tx_hash_hex,
                        sequence_index=self.base.get_next_sequence_counter(),
                        timestamp=ts_ms,
                        location=Location.BLOCKCHAIN,
                        location_label=location_label,
                        asset=A_ETH,
                        balance=Balance(amount=eth_burned_as_gas),
                        notes=
                        f'Burned {eth_burned_as_gas} ETH in gas from {location_label}',
                        event_type=HistoryEventType.SPEND,
                        event_subtype=HistoryEventSubType.FEE,
                        counterparty=CPT_GAS,
                    ))

        # Decode internal transactions after gas so gas is always 0 indexed
        self._maybe_decode_internal_transactions(
            tx=tx,
            tx_receipt=tx_receipt,
            events=events,
            tx_hash_hex=tx_hash_hex,
            ts_ms=ts_ms,
        )

        if tx_receipt.status is False or direction_result is None:
            # Not any other action to do for failed transactions or transaction where
            # any tracked address is not involved
            return events

        # now decode the actual transaction eth transfer itself
        amount = ZERO if tx.value == 0 else from_wei(FVal(tx.value))
        if tx.to_address is None:
            if not self.base.is_tracked(tx.from_address):
                return events

            events.append(
                HistoryBaseEntry(  # contract deployment
                    event_identifier=tx_hash_hex,
                    sequence_index=self.base.get_next_sequence_counter(),
                    timestamp=ts_ms,
                    location=Location.BLOCKCHAIN,
                    location_label=tx.from_address,
                    asset=A_ETH,
                    balance=Balance(amount=amount),
                    notes='Contract deployment',
                    event_type=HistoryEventType.INFORMATIONAL,
                    event_subtype=HistoryEventSubType.DEPLOY,
                    counterparty=None,  # TODO: Find out contract address
                ))
            return events

        if amount == ZERO:
            return events

        events.append(
            HistoryBaseEntry(
                event_identifier=tx_hash_hex,
                sequence_index=self.base.get_next_sequence_counter(),
                timestamp=ts_ms,
                location=Location.BLOCKCHAIN,
                location_label=location_label,
                asset=A_ETH,
                balance=Balance(amount=amount),
                notes=
                f'{verb} {amount} ETH {tx.from_address} -> {tx.to_address}',
                event_type=event_type,
                event_subtype=HistoryEventSubType.NONE,
                counterparty=counterparty,
            ))
        return events