def _decode_curve_events( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, decoded_events: List[HistoryBaseEntry], all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument action_items: Optional[List[ActionItem]], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: if tx_log.topics[0] in ( REMOVE_LIQUIDITY, REMOVE_ONE, REMOVE_LIQUIDITY_IMBALANCE, REMOVE_LIQUIDITY_3_ASSETS, REMOVE_LIQUIDITY_4_ASSETS, ): user_address = hex_or_bytes_to_address(tx_log.topics[1]) self._decode_curve_remove_events( tx_log=tx_log, transaction=transaction, decoded_events=decoded_events, user_address=user_address, ) elif tx_log.topics[0] in ( ADD_LIQUIDITY, ADD_LIQUIDITY_2_ASSETS, ADD_LIQUIDITY_4_ASSETS, ): user_address = hex_or_bytes_to_address(tx_log.topics[1]) self._decode_curve_deposit_events( tx_log=tx_log, decoded_events=decoded_events, user_address=user_address, ) return None, None
def _decode_claim( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument action_items: Optional[List[ActionItem]], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: if tx_log.topics[0] != VOTIUM_CLAIM: return None, None claimed_token_address = hex_or_bytes_to_address(tx_log.topics[1]) claimed_token = ethaddress_to_asset(claimed_token_address) if claimed_token is None: return None, None receiver = hex_or_bytes_to_address(tx_log.topics[2]) claimed_amount_raw = hex_or_bytes_to_int(tx_log.data[32:64]) amount = asset_normalized_value(amount=claimed_amount_raw, asset=claimed_token) for event in decoded_events: if event.event_type == HistoryEventType.RECEIVE and event.location_label == receiver and event.balance.amount == amount and claimed_token == event.asset: # noqa: E501 event.event_subtype = HistoryEventSubType.REWARD event.counterparty = CPT_VOTIUM event.notes = f'Receive {event.balance.amount} {event.asset.symbol} from votium bribe' # noqa: E501 return None, None
def _maybe_decode_erc20_approve( self, token: Optional[EthereumToken], tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument action_items: List[ActionItem], # pylint: disable=unused-argument ) -> Optional[HistoryBaseEntry]: if tx_log.topics[0] != ERC20_APPROVE or token is None: return None owner_address = hex_or_bytes_to_address(tx_log.topics[1]) spender_address = hex_or_bytes_to_address(tx_log.topics[2]) if not any( self.base.is_tracked(x) for x in (owner_address, spender_address)): return None amount_raw = hex_or_bytes_to_int(tx_log.data) amount = token_normalized_value(token_amount=amount_raw, token=token) notes = f'Approve {amount} {token.symbol} of {owner_address} for spending by {spender_address}' # noqa: E501 return HistoryBaseEntry( event_identifier=transaction.tx_hash.hex(), sequence_index=self.base.get_sequence_index(tx_log), timestamp=ts_sec_to_ms(transaction.timestamp), location=Location.BLOCKCHAIN, location_label=owner_address, asset=token, balance=Balance(amount=amount), notes=notes, event_type=HistoryEventType.INFORMATIONAL, event_subtype=HistoryEventSubType.APPROVE, counterparty=spender_address, )
def decode_proxy_creation( self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, 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] == b'%\x9b0\xca9\x88\\m\x80\x1a\x0b]\xbc\x98\x86@\xf3\xc2^/7S\x1f\xe18\xc5\xc5\xaf\x89U\xd4\x1b': # noqa: E501 owner_address = hex_or_bytes_to_address(tx_log.topics[2]) if not self.base.is_tracked(owner_address): return None, None proxy_address = hex_or_bytes_to_address(tx_log.data[0:32]) notes = f'Create DSR proxy {proxy_address} with owner {owner_address}' event = HistoryBaseEntry( event_identifier=transaction.tx_hash.hex(), sequence_index=self.base.get_sequence_index(tx_log), timestamp=ts_sec_to_ms(transaction.timestamp), location=Location.BLOCKCHAIN, location_label=owner_address, # TODO: This should be null for proposals and other informational events asset=A_ETH, balance=Balance(), notes=notes, event_type=HistoryEventType.INFORMATIONAL, event_subtype=HistoryEventSubType.DEPLOY, counterparty=proxy_address, ) return event, None return None, None
def _decode_swapped( # 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 ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: sender = hex_or_bytes_to_address(tx_log.topics[1]) source_token_address = hex_or_bytes_to_address(tx_log.topics[2]) destination_token_address = hex_or_bytes_to_address(tx_log.topics[3]) source_token = ethaddress_to_asset(source_token_address) if source_token is None: return None, None destination_token = ethaddress_to_asset(destination_token_address) if destination_token is None: return None, None receiver = hex_or_bytes_to_address(tx_log.data[0:32]) spent_amount_raw = hex_or_bytes_to_int(tx_log.data[64:96]) return_amount_raw = hex_or_bytes_to_int(tx_log.data[96:128]) spent_amount = asset_normalized_value(amount=spent_amount_raw, asset=source_token) return_amount = asset_normalized_value(amount=return_amount_raw, asset=destination_token) out_event = in_event = None for event in decoded_events: # Now find the sending and receiving events if event.event_type == HistoryEventType.SPEND and event.location_label == sender and spent_amount == event.balance.amount and source_token == event.asset: # noqa: E501 event.event_type = HistoryEventType.TRADE event.event_subtype = HistoryEventSubType.SPEND event.counterparty = CPT_ONEINCH_V2 event.notes = f'Swap {spent_amount} {source_token.symbol} in {CPT_ONEINCH_V2}' # noqa: E501 out_event = event elif event.event_type == HistoryEventType.RECEIVE and event.location_label == sender and receiver == event.location_label and return_amount == event.balance.amount and destination_token == event.asset: # noqa: E501 event.event_type = HistoryEventType.TRADE event.event_subtype = HistoryEventSubType.RECEIVE event.counterparty = CPT_ONEINCH_V2 event.notes = f'Receive {return_amount} {destination_token.symbol} from {CPT_ONEINCH_V2} swap' # noqa: E501 # use this index as the event may be an ETH transfer and appear at the start event.sequence_index = tx_log.log_index in_event = event maybe_reshuffle_events(out_event=out_event, in_event=in_event, events_list=decoded_events) return None, None
def _decode_elfi_claim( # 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]]: """Example: https://etherscan.io/tx/0x1e58aed1baf70b57e6e3e880e1890e7fe607fddc94d62986c38fe70e483e594b """ if tx_log.topics[0] != ELFI_VOTE_CHANGE: return None, None user_address = hex_or_bytes_to_address(tx_log.topics[1]) delegate_address = hex_or_bytes_to_address(tx_log.topics[2]) raw_amount = hex_or_bytes_to_int(tx_log.data[0:32]) amount = asset_normalized_value(amount=raw_amount, asset=A_ELFI) # now we need to find the transfer, but can't use decoded events # since the transfer is from one of at least 2 airdrop contracts to # vote/locking contract. Since neither the from, nor the to is a # tracked address there won't be a decoded transfer. So we search for # the raw log for other_log in all_logs: if other_log.topics[0] != ERC20_OR_ERC721_TRANSFER: continue transfer_raw = hex_or_bytes_to_int(other_log.data[0:32]) if other_log.address == A_ELFI.ethereum_address and transfer_raw == raw_amount: delegate_str = 'self-delegate' if user_address == delegate_address else f'delegate it to {delegate_address}' # noqa: E501 event = HistoryBaseEntry( event_identifier=transaction.tx_hash.hex(), sequence_index=self.base.get_sequence_index(tx_log), timestamp=ts_sec_to_ms(transaction.timestamp), location=Location.BLOCKCHAIN, location_label=user_address, asset=A_ELFI, balance=Balance(amount=amount), notes= f'Claim {amount} ELFI from element-finance airdrop and {delegate_str}', event_type=HistoryEventType.RECEIVE, event_subtype=HistoryEventSubType.AIRDROP, counterparty=CPT_ELEMENT_FINANCE, ) return event, None return None, None
def decode_comp_claim( 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: Optional[List[ActionItem]], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: """Example tx: https://etherscan.io/tx/0x024bd402420c3ba2f95b875f55ce2a762338d2a14dac4887b78174254c9ab807 """ if tx_log.topics[0] != DISTRIBUTED_SUPPLIER_COMP: return None, None supplier_address = hex_or_bytes_to_address(tx_log.topics[2]) if not self.base.is_tracked(supplier_address): return None, None comp_raw_amount = hex_or_bytes_to_int(tx_log.data[0:32]) if comp_raw_amount == 0: return None, None # do not count zero comp collection # A DistributedSupplierComp event can happen without a transfer. Just accrues # comp in the Comptroller until enough for a transfer is there. We should only # count a payout if the transfer occurs for event in decoded_events: if event.event_type == HistoryEventType.RECEIVE and event.location_label == supplier_address and event.asset == A_COMP and event.counterparty == COMPTROLLER_PROXY.address: # noqa: E501 event.event_subtype = HistoryEventSubType.REWARD event.counterparty = CPT_COMPOUND event.notes = f'Collect {event.balance.amount} COMP from compound' break return None, None
def _decode_fpis_claim( # 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 airdrop: Literal['convex', 'fpis'], ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: if tx_log.topics[0] != FPIS_CONVEX_CLAIM: return None, None user_address = hex_or_bytes_to_address(tx_log.data[0:32]) raw_amount = hex_or_bytes_to_int(tx_log.data[32:64]) if airdrop == CPT_CONVEX: amount = asset_normalized_value(amount=raw_amount, asset=A_CVX) note_location = 'from convex airdrop' counterparty = CPT_CONVEX else: amount = asset_normalized_value(amount=raw_amount, asset=A_FPIS) note_location = 'from FPIS airdrop' counterparty = CPT_FRAX for event in decoded_events: if event.event_type == HistoryEventType.RECEIVE and event.location_label == user_address and amount == event.balance.amount and A_FPIS == event.asset: # noqa: E501 event.event_type = HistoryEventType.RECEIVE event.event_subtype = HistoryEventSubType.AIRDROP event.counterparty = counterparty event.notes = f'Claim {amount} {event.asset.symbol} {note_location}' # noqa: E501 break return None, None
def _decode_redeem( self, tx_log: EthereumTxReceiptLog, decoded_events: List[HistoryBaseEntry], compound_token: EthereumToken, ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: redeemer = hex_or_bytes_to_address(tx_log.data[0:32]) if not self.base.is_tracked(redeemer): return None, None redeem_amount_raw = hex_or_bytes_to_int(tx_log.data[32:64]) redeem_tokens_raw = hex_or_bytes_to_int(tx_log.data[64:96]) underlying_token = symbol_to_asset_or_token(compound_token.symbol[1:]) redeem_amount = asset_normalized_value(redeem_amount_raw, underlying_token) redeem_tokens = token_normalized_value(redeem_tokens_raw, compound_token) out_event = in_event = None for event in decoded_events: # Find the transfer event which should have come before the redeeming if event.event_type == HistoryEventType.RECEIVE and event.asset == underlying_token and event.balance.amount == redeem_amount: # noqa: E501 event.event_type = HistoryEventType.WITHDRAWAL event.event_subtype = HistoryEventSubType.REMOVE_ASSET event.counterparty = CPT_COMPOUND event.notes = f'Withdraw {redeem_amount} {underlying_token.symbol} from compound' in_event = event if event.event_type == HistoryEventType.SPEND and event.asset == compound_token and event.balance.amount == redeem_tokens: # noqa: E501 event.event_type = HistoryEventType.SPEND event.event_subtype = HistoryEventSubType.RETURN_WRAPPED event.counterparty = CPT_COMPOUND event.notes = f'Return {redeem_tokens} {compound_token.symbol} to compound' out_event = event maybe_reshuffle_events(out_event=out_event, in_event=in_event, events_list=decoded_events) return None, None
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 _decode_deposit( # 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]]: """Match a zksync deposit with the transfer to decode it TODO: This is now quite bad. We don't use the token id of zksync as we should. Example: https://etherscan.io/tx/0xdd6d1f92980faf622c09acd84dbff4fe0bd7ae466a23c2479df709f8996d250e#eventlog We should include the zksync api querying module which is in this PR: https://github.com/rotki/rotki/pull/3985/files to get the ids of tokens and then match them to what is deposited. """ # noqa: E501 user_address = hex_or_bytes_to_address(tx_log.topics[1]) amount_raw = hex_or_bytes_to_int(tx_log.data) for event in decoded_events: if event.event_type == HistoryEventType.SPEND and event.location_label == user_address: event_raw_amount = asset_raw_value(amount=event.balance.amount, asset=event.asset) if event_raw_amount != amount_raw: continue # found the deposit transfer event.event_type = HistoryEventType.DEPOSIT event.event_subtype = HistoryEventSubType.BRIDGE event.counterparty = CPT_ZKSYNC event.notes = f'Deposit {event.balance.amount} {event.asset.symbol} to zksync' # noqa: E501 break return None, None
def _decode_vault_creation( self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: owner_address = self._get_address_or_proxy( hex_or_bytes_to_address(tx_log.topics[2])) if owner_address is None: return None, None if not self.base.is_tracked(owner_address): return None, None cdp_id = hex_or_bytes_to_int(tx_log.topics[3]) notes = f'Create MakerDAO vault with id {cdp_id} and owner {owner_address}' event = HistoryBaseEntry( event_identifier=transaction.tx_hash.hex(), sequence_index=self.base.get_sequence_index(tx_log), timestamp=ts_sec_to_ms(transaction.timestamp), location=Location.BLOCKCHAIN, location_label=owner_address, # TODO: This should be null for proposals and other informational events asset=A_ETH, balance=Balance(), notes=notes, event_type=HistoryEventType.INFORMATIONAL, event_subtype=HistoryEventSubType.DEPLOY, counterparty='makerdao vault', ) return event, None
def decode_makerdao_debt_payback( # 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] == GENERIC_JOIN: join_user_address = hex_or_bytes_to_address(tx_log.topics[2]) raw_amount = hex_or_bytes_to_int(tx_log.topics[3]) amount = token_normalized_value( token_amount=raw_amount, token=A_DAI, ) # The transfer comes right before, but we don't have enough information # yet to make sure that this transfer is indeed a vault payback debt. We # need to get a cdp frob event and compare vault id to address matches action_item = ActionItem( action='transform', sequence_index=tx_log.log_index, from_event_type=HistoryEventType.SPEND, from_event_subtype=HistoryEventSubType.NONE, asset=A_DAI, amount=amount, to_event_subtype=HistoryEventSubType.PAYBACK_DEBT, to_counterparty=CPT_VAULT, extra_data={'vault_address': join_user_address}, ) return None, action_item return None, None
def _legacy_contracts_basic_info(tx_log: EthereumTxReceiptLog) -> Tuple[ChecksumEthAddress, Optional[Asset], Optional[Asset]]: # noqa: E501 """ Returns: - address of the sender - source token (can be none) - destination token (can be none) May raise: - DeserializationError when using hex_or_bytes_to_address """ sender = hex_or_bytes_to_address(tx_log.topics[1]) source_token_address = hex_or_bytes_to_address(tx_log.data[:32]) destination_token_address = hex_or_bytes_to_address(tx_log.data[32:64]) source_token = ethaddress_to_asset(source_token_address) destination_token = ethaddress_to_asset(destination_token_address) return sender, source_token, destination_token
def _decode_history( 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 ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: sender = hex_or_bytes_to_address(tx_log.topics[1]) if not self.base.is_tracked(sender): return None, None from_token_address = hex_or_bytes_to_address(tx_log.data[0:32]) to_token_address = hex_or_bytes_to_address(tx_log.data[32:64]) from_asset = ethaddress_to_asset(from_token_address) to_asset = ethaddress_to_asset(to_token_address) if None in (from_asset, to_asset): return None, None from_raw = hex_or_bytes_to_int(tx_log.data[64:96]) from_amount = asset_normalized_value(from_raw, from_asset) # type: ignore to_raw = hex_or_bytes_to_int(tx_log.data[96:128]) to_amount = asset_normalized_value(to_raw, to_asset) # type: ignore out_event = in_event = None for event in decoded_events: if event.event_type == HistoryEventType.SPEND and event.location_label == sender and from_amount == event.balance.amount and from_asset == event.asset: # noqa: E501 # find the send event event.event_type = HistoryEventType.TRADE event.event_subtype = HistoryEventSubType.SPEND event.counterparty = CPT_ONEINCH_V1 event.notes = f'Swap {from_amount} {from_asset.symbol} in {CPT_ONEINCH_V1} from {event.location_label}' # noqa: E501 out_event = event elif event.event_type == HistoryEventType.RECEIVE and event.location_label == sender and to_amount == event.balance.amount and to_asset == event.asset: # noqa: E501 # find the receive event event.event_type = HistoryEventType.TRADE event.event_subtype = HistoryEventSubType.RECEIVE event.counterparty = CPT_ONEINCH_V1 event.notes = f'Receive {to_amount} {to_asset.symbol} from {CPT_ONEINCH_V1} swap in {event.location_label}' # noqa: E501 # use this index as the event may be an ETH transfer and appear at the start event.sequence_index = tx_log.log_index in_event = event maybe_reshuffle_events(out_event=out_event, in_event=in_event, events_list=decoded_events) return None, None
def _decode_redeem_underlying_event( # 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]]: reserve_address = hex_or_bytes_to_address(tx_log.topics[1]) reserve_asset = ethaddress_to_asset(reserve_address) if reserve_asset is None: return None, None user_address = hex_or_bytes_to_address(tx_log.topics[2]) raw_amount = hex_or_bytes_to_int(tx_log.data[0:32]) amount = asset_normalized_value(raw_amount, reserve_asset) atoken = asset_to_atoken(asset=reserve_asset, version=1) if atoken is None: return None, None receive_event = return_event = None for event in decoded_events: if event.event_type == HistoryEventType.RECEIVE and event.location_label == user_address and amount == event.balance.amount and reserve_asset == event.asset: # noqa: E501 event.event_type = HistoryEventType.WITHDRAWAL event.event_subtype = HistoryEventSubType.REMOVE_ASSET event.counterparty = CPT_AAVE_V1 event.notes = f'Withdraw {amount} {reserve_asset.symbol} from aave-v1' receive_event = event elif event.event_type == HistoryEventType.SPEND and event.location_label == user_address and amount == event.balance.amount and atoken == event.asset: # noqa: E501 # find the redeem aToken transfer event.event_type = HistoryEventType.SPEND event.event_subtype = HistoryEventSubType.RETURN_WRAPPED event.counterparty = CPT_AAVE_V1 event.notes = f'Return {amount} {atoken.symbol} to aave-v1' return_event = event elif event.event_type == HistoryEventType.RECEIVE and event.location_label == user_address and event.counterparty == ZERO_ADDRESS and event.asset == atoken: # noqa: E501 event.event_subtype = HistoryEventSubType.REWARD event.counterparty = CPT_AAVE_V1 event.notes = f'Gain {event.balance.amount} {atoken.symbol} from aave-v1 as interest' # noqa: E501 maybe_reshuffle_events(out_event=return_event, in_event=receive_event, events_list=decoded_events) # noqa: E501 return None, None
def _maybe_enrich_curve_transfers( # pylint: disable=no-self-use self, token: EthereumToken, # pylint: disable=unused-argument tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, event: HistoryBaseEntry, action_items: List[ActionItem], # pylint: disable=unused-argument ) -> bool: source_address = hex_or_bytes_to_address(tx_log.topics[1]) to_address = hex_or_bytes_to_address(tx_log.topics[2]) if ( # deposit give asset event.event_type == HistoryEventType.RECEIVE and event.event_subtype == HistoryEventSubType.NONE and source_address == CURVE_Y_DEPOSIT and transaction.from_address == to_address ): event.event_type = HistoryEventType.WITHDRAWAL event.event_subtype = HistoryEventSubType.REMOVE_ASSET event.counterparty = CPT_CURVE event.notes = f'Receive {event.balance.amount} {event.asset.symbol} from the curve pool {CURVE_Y_DEPOSIT}' # noqa: E501 return True return False
def _maybe_decode_swap( # pylint: disable=no-self-use self, token: Optional[EthereumToken], # pylint: disable=unused-argument tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], action_items: List[ActionItem], # pylint: disable=unused-argument ) -> None: """Search for both events. Since the order is not guaranteed try reshuffle in both cases""" out_event = in_event = None if tx_log.topics[0] == TOKEN_PURCHASE: buyer = hex_or_bytes_to_address(tx_log.topics[1]) # search for a send to buyer from a tracked address for event in decoded_events: if event.event_type == HistoryEventType.SPEND and event.counterparty == buyer: event.event_type = HistoryEventType.TRADE event.event_subtype = HistoryEventSubType.SPEND event.counterparty = CPT_UNISWAP_V1 event.notes = f'Swap {event.balance.amount} {event.asset.symbol} in uniswap-v1 from {event.location_label}' # noqa: E501 out_event = event elif event.event_type == HistoryEventType.TRADE and event.event_subtype == HistoryEventSubType.RECEIVE and event.counterparty == CPT_UNISWAP_V1: # noqa: :E501 in_event = event elif tx_log.topics[0] == ETH_PURCHASE: buyer = hex_or_bytes_to_address(tx_log.topics[1]) # search for a receive to buyer for event in decoded_events: if event.event_type == HistoryEventType.RECEIVE and event.location_label == buyer: event.event_type = HistoryEventType.TRADE event.event_subtype = HistoryEventSubType.RECEIVE event.counterparty = CPT_UNISWAP_V1 event.notes = f'Receive {event.balance.amount} {event.asset.symbol} from uniswap-v1 swap in {event.location_label}' # noqa: E501 in_event = event elif event.event_type == HistoryEventType.TRADE and event.event_subtype == HistoryEventSubType.SPEND and event.counterparty == CPT_UNISWAP_V1: # noqa: :E501 out_event = event maybe_reshuffle_events(out_event=out_event, in_event=in_event)
def _decode_swapped( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: """We use the Swapped event to get the fee kept by 1inch""" to_token_address = hex_or_bytes_to_address(tx_log.topics[2]) to_asset = ethaddress_to_asset(to_token_address) if to_asset is None: return None, None to_raw = hex_or_bytes_to_int(tx_log.data[32:64]) fee_raw = hex_or_bytes_to_int(tx_log.data[96:128]) if fee_raw == 0: return None, None # no need to do anything for zero fee taken full_amount = asset_normalized_value(to_raw + fee_raw, to_asset) sender_address = None for event in decoded_events: # Edit the full amount in the swap's receive event if event.event_type == HistoryEventType.TRADE and event.event_subtype == HistoryEventSubType.RECEIVE and event.counterparty == CPT_ONEINCH_V1: # noqa: E501 event.balance.amount = full_amount event.notes = f'Receive {full_amount} {event.asset.symbol} from {CPT_ONEINCH_V1} swap in {event.location_label}' # noqa: E501 sender_address = event.location_label break if sender_address is None: return None, None # And now create a new event for the fee fee_amount = asset_normalized_value(fee_raw, to_asset) fee_event = HistoryBaseEntry( event_identifier=transaction.tx_hash.hex(), sequence_index=self.base.get_sequence_index(tx_log), timestamp=ts_sec_to_ms(transaction.timestamp), location=Location.BLOCKCHAIN, location_label=sender_address, asset=to_asset, balance=Balance(amount=fee_amount), notes= f'Deduct {fee_amount} {to_asset.symbol} from {sender_address} as {CPT_ONEINCH_V1} fees', # noqa: E501 event_type=HistoryEventType.SPEND, event_subtype=HistoryEventSubType.FEE, counterparty=CPT_ONEINCH_V1, ) return fee_event, None
def _decode_mint( self, transaction: EthereumTransaction, tx_log: EthereumTxReceiptLog, decoded_events: List[HistoryBaseEntry], compound_token: EthereumToken, ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: minter = hex_or_bytes_to_address(tx_log.data[0:32]) if not self.base.is_tracked(minter): return None, None mint_amount_raw = hex_or_bytes_to_int(tx_log.data[32:64]) minted_amount_raw = hex_or_bytes_to_int(tx_log.data[64:96]) underlying_asset = symbol_to_asset_or_token(compound_token.symbol[1:]) mint_amount = asset_normalized_value(mint_amount_raw, underlying_asset) minted_amount = token_normalized_value(minted_amount_raw, compound_token) out_event = None for event in decoded_events: # Find the transfer event which should have come before the minting if event.event_type == HistoryEventType.SPEND and event.asset == underlying_asset and event.balance.amount == mint_amount: # noqa: E501 event.event_type = HistoryEventType.DEPOSIT event.event_subtype = HistoryEventSubType.DEPOSIT_ASSET event.counterparty = CPT_COMPOUND event.notes = f'Deposit {mint_amount} {underlying_asset.symbol} to compound' out_event = event break if out_event is None: log.debug(f'At compound mint decoding of tx {transaction.tx_hash.hex()} the out event was not found') # noqa: E501 return None, None # also create an action item for the receive of the cTokens action_item = ActionItem( action='transform', sequence_index=tx_log.log_index, from_event_type=HistoryEventType.RECEIVE, from_event_subtype=HistoryEventSubType.NONE, asset=compound_token, amount=minted_amount, to_event_subtype=HistoryEventSubType.RECEIVE_WRAPPED, to_notes=f'Receive {minted_amount} {compound_token.symbol} from compound', to_counterparty=CPT_COMPOUND, paired_event_data=(out_event, True), ) return None, action_item
def decode_saidai_migration( self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, 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] == ERC20_OR_ERC721_TRANSFER: to_address = hex_or_bytes_to_address(tx_log.topics[2]) if to_address != '0xc73e0383F3Aff3215E6f04B0331D58CeCf0Ab849': return None, None # sending SAI to migration contract transfer = self.base.decode_erc20_721_transfer( token=A_SAI, tx_log=tx_log, transaction=transaction) # noqa: E501 if transfer is None: return None, None transfer.event_type = HistoryEventType.MIGRATE transfer.event_subtype = HistoryEventSubType.SPEND transfer.notes = f'Migrate {transfer.balance.amount} SAI to DAI' transfer.counterparty = CPT_MIGRATION # also create action item for the receive transfer action_item = ActionItem( action='transform', sequence_index=tx_log.log_index, from_event_type=HistoryEventType.RECEIVE, from_event_subtype=HistoryEventSubType.NONE, asset=A_DAI, amount=transfer.balance.amount, to_event_type=HistoryEventType.MIGRATE, to_event_subtype=HistoryEventSubType.RECEIVE, to_notes= f'Receive {transfer.balance.amount} DAI from SAI->DAI migration', to_counterparty='makerdao migration', ) return transfer, action_item return None, None
def _decode_vault_debt_generation( 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 ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: """Decode vault debt generation by parsing a lognote for cdpmanager move""" cdp_id = hex_or_bytes_to_int(tx_log.topics[2]) destination = hex_or_bytes_to_address(tx_log.topics[3]) owner = self._get_address_or_proxy(destination) if owner is None: return None, None # now we need to get the rad and since it's the 3rd argument its not in the indexed topics # but it's part of the data location after the first 132 bytes. # also need to shift by ray since it's in rad raw_amount = shift_num_right_by( hex_or_bytes_to_int(tx_log.data[132:164]), RAY_DIGITS) amount = token_normalized_value( token_amount=raw_amount, token=A_DAI, ) # The transfer event appears after the debt generation event, so we need to transform it action_item = ActionItem( action='transform', sequence_index=tx_log.log_index, from_event_type=HistoryEventType.RECEIVE, from_event_subtype=HistoryEventSubType.NONE, asset=A_DAI, amount=amount, to_event_type=HistoryEventType.WITHDRAWAL, to_event_subtype=HistoryEventSubType.GENERATE_DEBT, to_counterparty=CPT_VAULT, to_notes=f'Generate {amount} DAI from MakerDAO vault {cdp_id}', extra_data={'cdp_id': cdp_id}, ) return None, action_item
def _decode_fox_claim( # 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] != FOX_CLAIMED: return None, None user_address = hex_or_bytes_to_address(tx_log.topics[1]) raw_amount = hex_or_bytes_to_int(tx_log.data[64:96]) amount = asset_normalized_value(amount=raw_amount, asset=A_FOX) for event in decoded_events: if event.event_type == HistoryEventType.RECEIVE and event.location_label == user_address and amount == event.balance.amount and A_FOX == event.asset: # noqa: E501 event.event_type = HistoryEventType.RECEIVE event.event_subtype = HistoryEventSubType.AIRDROP event.counterparty = CPT_SHAPESHIFT event.notes = f'Claim {amount} FOX from shapeshift airdrop' # noqa: E501 break return None, None
def decode_pot_for_dsr( self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], all_logs: List[EthereumTxReceiptLog], action_items: List[ActionItem], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: if tx_log.topics[0] == POT_JOIN: potjoin_user_address = hex_or_bytes_to_address(tx_log.topics[1]) user = self._get_address_or_proxy(potjoin_user_address) if user is None: return None, None # Now gotta find the DAI join event to get actual DAI value daijoin_log = None for event_log in all_logs: if event_log.address == MAKERDAO_DAI_JOIN.address and event_log.topics[ 0] == GENERIC_JOIN: # noqa: E501 daijoin_user_address = hex_or_bytes_to_address( event_log.topics[1]) if daijoin_user_address != potjoin_user_address: continue # not a match daijoin_log = event_log break if daijoin_log is None: return None, None # no matching daijoin for potjoin raw_amount = hex_or_bytes_to_int(daijoin_log.topics[3]) amount = token_normalized_value( token_amount=raw_amount, token=A_DAI, ) # The transfer event should be right before for event in decoded_events: if event.asset == A_DAI and event.event_type == HistoryEventType.SPEND and event.balance.amount == amount: # noqa: E501 # found the event event.location_label = user event.counterparty = CPT_DSR event.event_type = HistoryEventType.DEPOSIT event.event_subtype = HistoryEventSubType.DEPOSIT_ASSET event.notes = f'Deposit {amount} DAI in the DSR' return None, None elif tx_log.topics[0] == POT_EXIT: pot_exit_address = hex_or_bytes_to_address(tx_log.topics[1]) user = self._get_address_or_proxy(pot_exit_address) if user is None: return None, None # Now gotta find the DAI exit event to get actual DAI value daiexit_log = None for event_log in all_logs: if event_log.address == MAKERDAO_DAI_JOIN.address and event_log.topics[ 0] == GENERIC_EXIT: # noqa: E501 daiexit_user_address = hex_or_bytes_to_address( event_log.topics[2]) if daiexit_user_address != user: continue # not a match daiexit_log = event_log break if daiexit_log is None: return None, None # no matching daiexit for potexit raw_amount = hex_or_bytes_to_int(daiexit_log.topics[3]) amount = token_normalized_value( token_amount=raw_amount, token=A_DAI, ) # The transfer event will be in a subsequent logs action_item = ActionItem( action='transform', sequence_index=tx_log.log_index, from_event_type=HistoryEventType.RECEIVE, from_event_subtype=HistoryEventSubType.NONE, asset=A_DAI, amount=amount, to_event_type=HistoryEventType.WITHDRAWAL, to_event_subtype=HistoryEventSubType.REMOVE_ASSET, to_notes=f'Withdraw {amount} DAI from the DSR', to_counterparty=CPT_DSR, ) return None, action_item return None, None
def get_events_for_atoken_and_address( self, user_address: ChecksumEthAddress, atoken: EthereumToken, deposit_events: List[Dict[str, Any]], withdraw_events: List[Dict[str, Any]], from_block: int, to_block: int, ) -> List[AaveEvent]: """This function should be entered while holding the history_lock semaphore""" argument_filters = { 'from': ZERO_ADDRESS, 'to': user_address, } mint_events = self.ethereum.get_logs( contract_address=atoken.ethereum_address, abi=ATOKEN_ABI, event_name='Transfer', argument_filters=argument_filters, from_block=from_block, to_block=to_block, ) mint_data = set() mint_data_to_log_index = {} for event in mint_events: amount = hex_or_bytes_to_int(event['data']) if amount == 0: continue # first mint can be for 0. Ignore entry = ( deserialize_blocknumber(event['blockNumber']), amount, self.ethereum.get_event_timestamp(event), event['transactionHash'], ) mint_data.add(entry) mint_data_to_log_index[entry] = deserialize_int_from_hex_or_int( event['logIndex'], 'aave log index', ) reserve_asset = _atoken_to_reserve_asset(atoken) reserve_address, decimals = _get_reserve_address_decimals( reserve_asset.identifier) aave_events = [] for event in deposit_events: if hex_or_bytes_to_address(event['topics'][1]) == reserve_address: # first 32 bytes of the data are the amount deposit = hex_or_bytes_to_int(event['data'][:66]) block_number = deserialize_blocknumber(event['blockNumber']) timestamp = self.ethereum.get_event_timestamp(event) tx_hash = event['transactionHash'] log_index = deserialize_int_from_hex_or_int( event['logIndex'], 'aave log index') # If there is a corresponding deposit event remove the minting event data entry = (block_number, deposit, timestamp, tx_hash) if entry in mint_data: mint_data.remove(entry) del mint_data_to_log_index[entry] usd_price = query_usd_price_zero_if_error( asset=reserve_asset, time=timestamp, location='aave deposit', msg_aggregator=self.msg_aggregator, ) deposit_amount = deposit / (FVal(10)**FVal(decimals)) aave_events.append( AaveEvent( event_type='deposit', asset=reserve_asset, value=Balance( amount=deposit_amount, usd_value=deposit_amount * usd_price, ), block_number=block_number, timestamp=timestamp, tx_hash=tx_hash, log_index=log_index, )) for data in mint_data: usd_price = query_usd_price_zero_if_error( asset=atoken, time=data[2], location='aave interest profit', msg_aggregator=self.msg_aggregator, ) interest_amount = data[1] / (FVal(10)**FVal(decimals)) aave_events.append( AaveEvent( event_type='interest', asset=atoken, value=Balance( amount=interest_amount, usd_value=interest_amount * usd_price, ), block_number=data[0], timestamp=data[2], tx_hash=data[3], log_index=mint_data_to_log_index[data], )) for event in withdraw_events: if hex_or_bytes_to_address(event['topics'][1]) == reserve_address: # first 32 bytes of the data are the amount withdrawal = hex_or_bytes_to_int(event['data'][:66]) block_number = deserialize_blocknumber(event['blockNumber']) timestamp = self.ethereum.get_event_timestamp(event) tx_hash = event['transactionHash'] usd_price = query_usd_price_zero_if_error( asset=reserve_asset, time=timestamp, location='aave withdrawal', msg_aggregator=self.msg_aggregator, ) withdrawal_amount = withdrawal / (FVal(10)**FVal(decimals)) aave_events.append( AaveEvent( event_type='withdrawal', asset=reserve_asset, value=Balance( amount=withdrawal_amount, usd_value=withdrawal_amount * usd_price, ), block_number=block_number, timestamp=timestamp, tx_hash=tx_hash, log_index=deserialize_int_from_hex_or_int( event['logIndex'], 'aave log index'), )) return aave_events
def _maybe_enrich_pickle_transfers( # pylint: disable=no-self-use self, token: EthereumToken, # pylint: disable=unused-argument tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, event: HistoryBaseEntry, action_items: List[ActionItem], # pylint: disable=unused-argument ) -> bool: """Enrich tranfer transactions to address for jar deposits and withdrawals""" if not ( hex_or_bytes_to_address(tx_log.topics[2]) in self.pickle_contracts or hex_or_bytes_to_address(tx_log.topics[1]) in self.pickle_contracts or tx_log.address in self.pickle_contracts ): return False if ( # Deposit give asset event.event_type == HistoryEventType.SPEND and event.event_subtype == HistoryEventSubType.NONE and event.location_label == transaction.from_address and hex_or_bytes_to_address(tx_log.topics[2]) in self.pickle_contracts ): if EthereumToken(tx_log.address) != event.asset: return True amount_raw = hex_or_bytes_to_int(tx_log.data) amount = asset_normalized_value(amount=amount_raw, asset=event.asset) if event.balance.amount == amount: event.event_type = HistoryEventType.DEPOSIT event.event_subtype = HistoryEventSubType.DEPOSIT_ASSET event.counterparty = CPT_PICKLE event.notes = f'Deposit {event.balance.amount} {event.asset.symbol} in pickle contract' # noqa: E501 elif ( # Deposit receive wrapped event.event_type == HistoryEventType.RECEIVE and event.event_subtype == HistoryEventSubType.NONE and tx_log.address in self.pickle_contracts ): amount_raw = hex_or_bytes_to_int(tx_log.data) amount = asset_normalized_value(amount=amount_raw, asset=event.asset) if event.balance.amount == amount: # noqa: E501 event.event_type = HistoryEventType.RECEIVE event.event_subtype = HistoryEventSubType.RECEIVE_WRAPPED event.counterparty = CPT_PICKLE event.notes = f'Receive {event.balance.amount} {event.asset.symbol} after depositing in pickle contract' # noqa: E501 elif ( # Withdraw send wrapped event.event_type == HistoryEventType.SPEND and event.event_subtype == HistoryEventSubType.NONE and event.location_label == transaction.from_address and hex_or_bytes_to_address(tx_log.topics[2]) == ZERO_ADDRESS and hex_or_bytes_to_address(tx_log.topics[1]) in transaction.from_address ): if event.asset != EthereumToken(tx_log.address): return True amount_raw = hex_or_bytes_to_int(tx_log.data) amount = asset_normalized_value(amount=amount_raw, asset=event.asset) if event.balance.amount == amount: # noqa: E501 event.event_type = HistoryEventType.SPEND event.event_subtype = HistoryEventSubType.RETURN_WRAPPED event.counterparty = CPT_PICKLE event.notes = f'Return {event.balance.amount} {event.asset.symbol} to the pickle contract' # noqa: E501 elif ( # Withdraw receive asset event.event_type == HistoryEventType.RECEIVE and event.event_subtype == HistoryEventSubType.NONE and event.location_label == transaction.from_address and hex_or_bytes_to_address(tx_log.topics[2]) == transaction.from_address and hex_or_bytes_to_address(tx_log.topics[1]) in self.pickle_contracts ): if event.asset != EthereumToken(tx_log.address): return True amount_raw = hex_or_bytes_to_int(tx_log.data) amount = asset_normalized_value(amount=amount_raw, asset=event.asset) if event.balance.amount == amount: # noqa: E501 event.event_type = HistoryEventType.WITHDRAWAL event.event_subtype = HistoryEventSubType.REMOVE_ASSET event.counterparty = CPT_PICKLE event.notes = f'Unstake {event.balance.amount} {event.asset.symbol} from the pickle contract' # noqa: E501 return True
def _try_get_chi_close_to(self, time: Timestamp) -> FVal: """Best effort attempt to get a chi value close to the given timestamp It can't be 100% accurate since we use the logs of join() or exit() in order to find the closest time chi was changed. It also may not work if for some reason there is no logs in the block range we are looking for. Better solution would have been an archive node's query. May raise: - RemoteError if there are problems with querying etherscan - ChiRetrievalError if we are unable to query chi at the given timestamp - BlockchainQueryError if an ethereum node is used and the contract call queries fail for some reason """ if time > 1584386100: # If the time is after 16/03/2020 19:15 GMT we know that # makerdao DSR was set to 0% we know chi has not changed # https://twitter.com/MakerDAO/status/1239270910810411008 return FVal('1018008449363110619399951035') block_number = self.ethereum.etherscan.get_blocknumber_by_time(time) latest_block = self.ethereum.get_latest_block_number() blocks_queried = 0 counter = 1 # Keep trying to find events that could reveal the chi to us. Go back # as far as MAX_BLOCKS_TO_QUERY and only then give up while blocks_queried < MAX_BLOCKS_TO_QUERY: back_from_block = max( MAKERDAO_POT.deployed_block, block_number - counter * CHI_BLOCKS_SEARCH_DISTANCE, ) back_to_block = block_number - (counter - 1) * CHI_BLOCKS_SEARCH_DISTANCE forward_from_block = min( latest_block, block_number + (counter - 1) * CHI_BLOCKS_SEARCH_DISTANCE, ) forward_to_block = min( latest_block, block_number + CHI_BLOCKS_SEARCH_DISTANCE, ) back_joins, back_exits = self._get_join_exit_events( back_from_block, back_to_block) forward_joins, forward_exits = self._get_join_exit_events( from_block=forward_from_block, to_block=forward_to_block, ) no_results = all( len(x) == 0 for x in (back_joins, back_exits, forward_joins, forward_exits)) if latest_block == forward_to_block and no_results: # if our forward querying got us to the latest block and there is # still no other results, then take current chi return self.ethereum.call_contract( contract_address=MAKERDAO_POT.address, abi=MAKERDAO_POT.abi, method_name='chi', ) if not no_results: # got results! break blocks_queried += 2 * CHI_BLOCKS_SEARCH_DISTANCE counter += 1 if no_results: raise ChiRetrievalError( f'Found no DSR events around timestamp {time}. Cant query chi.', ) # Find the closest event to the to_block number, looking both at events # in the blocks before and in the blocks after block_number found_event = None back_event = _find_closest_event(back_joins, back_exits, -1, operator.gt) forward_event = _find_closest_event(forward_joins, forward_exits, 0, operator.lt) if back_event and not forward_event: found_event = back_event elif forward_event and not back_event: found_event = forward_event else: # We have both backward and forward events, get the one closer to block number back_block_number = back_event['blockNumber'] # type: ignore forward_block_number = forward_event['blockNumber'] # type: ignore if block_number - back_block_number <= forward_block_number - block_number: found_event = back_event else: found_event = forward_event assert found_event, 'at this point found_event should be populated' # helps mypy event_block_number = found_event['blockNumber'] first_topic = found_event['topics'][0] amount = self._get_vat_join_exit_at_transaction( movement_type='join' if first_topic.startswith('0x049878f3') else 'exit', proxy_address=hex_or_bytes_to_address(found_event['topics'][1]), block_number=event_block_number, transaction_index=found_event['transactionIndex'], ) if amount is None: raise ChiRetrievalError( f'Found no VAT.move events around timestamp {time}. Cant query chi.', ) wad_val = hexstr_to_int(found_event['topics'][2]) chi = FVal(amount) / FVal(wad_val) return chi