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)
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
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
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
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
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
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
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)
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
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, ))
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)
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
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
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
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
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