def _decode_legacy_trade( # 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] == KYBER_TRADE_LEGACY: return None, None sender, source_token, destination_token = _legacy_contracts_basic_info(tx_log) if source_token is None or destination_token is None: return None, None 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) _maybe_update_events_legacy_contrats( decoded_events=decoded_events, sender=sender, source_token=source_token, destination_token=destination_token, spent_amount=spent_amount, return_amount=return_amount, ) 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_order_placement( # 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]]: """Some docs: https://docs.gnosis.io/protocol/docs/tutorial-limit-orders/""" topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='OrderPlacement', argument_names=('owner', 'index', 'buyToken', 'sellToken', 'validFrom', 'validUntil', 'priceNumerator', 'priceDenominator'), # noqa: E501 ) owner = topic_data[0] if not self.base.is_tracked(owner): return None, None result = multicall_specific( ethereum=self.ethereum, contract=self.contract, method_name='tokenIdToAddressMap', arguments=[[topic_data[1]], [topic_data[2]]], ) # The resulting addresses are non checksumed but they can be found in the DB buy_token = ethaddress_to_asset(result[0][0]) if buy_token is None: return None, None sell_token = ethaddress_to_asset(result[1][0]) if sell_token is None: return None, None buy_amount = asset_normalized_value(amount=log_data[3], asset=buy_token) sell_amount = asset_normalized_value(amount=log_data[4], asset=sell_token) 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, # Asset means nothing here since the event is informational. TODO: Improve? asset=sell_token, balance=Balance(amount=sell_amount), notes= f'Place an order in DXDao Mesa to sell {sell_amount} {sell_token.symbol} for {buy_amount} {buy_token.symbol}', # noqa: E501 event_type=HistoryEventType.INFORMATIONAL, event_subtype=HistoryEventSubType.PLACE_ORDER, counterparty=CPT_DXDAO_MESA, ) return event, None
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_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_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_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]]: topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='Deposit', argument_names=('user', 'token', 'amount', 'batchId'), ) deposited_asset = ethaddress_to_asset(topic_data[1]) if deposited_asset is None: return None, None amount = asset_normalized_value(amount=log_data[0], asset=deposited_asset) for event in decoded_events: # Find the transfer event which should come before the deposit if event.event_type == HistoryEventType.SPEND and event.asset == deposited_asset and event.balance.amount == amount and event.counterparty == self.contract.address: # noqa: E501 event.event_type = HistoryEventType.DEPOSIT event.event_subtype = HistoryEventSubType.DEPOSIT_ASSET event.counterparty = CPT_DXDAO_MESA event.notes = f'Deposit {amount} {deposited_asset.symbol} to DXDao mesa exchange' # noqa: E501 break return None, None
def _decode_withdraw( # 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]]: topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='Withdraw', argument_names=('user', 'token', 'amount'), ) withdraw_asset = ethaddress_to_asset(topic_data[1]) if withdraw_asset is None: return None, None amount = asset_normalized_value(amount=log_data[0], asset=withdraw_asset) for event in decoded_events: # Find the transfer event which should come before the withdraw if event.event_type == HistoryEventType.RECEIVE and event.asset == withdraw_asset and event.balance.amount == amount and event.counterparty == self.contract.address: # noqa: E501 event.event_type = HistoryEventType.WITHDRAWAL event.event_subtype = HistoryEventSubType.REMOVE_ASSET event.counterparty = CPT_DXDAO_MESA event.notes = f'Withdraw {amount} {withdraw_asset.symbol} from DXDao mesa exchange' # noqa: E501 break 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 _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_makerdao_vault_action( # 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: List[ActionItem], # pylint: disable=unused-argument vault_asset: Asset, vault_type: str, ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: if tx_log.topics[0] == GENERIC_JOIN: raw_amount = hex_or_bytes_to_int(tx_log.topics[3]) amount = asset_normalized_value( amount=raw_amount, asset=vault_asset, ) # Go through decoded events to find and edit the transfer event for event in decoded_events: if event.event_type == HistoryEventType.SPEND and event.asset == vault_asset and event.balance.amount == amount: # noqa: E501 event.sequence_index = tx_log.log_index # to better position it in the list event.event_type = HistoryEventType.DEPOSIT event.event_subtype = HistoryEventSubType.DEPOSIT_ASSET event.counterparty = CPT_VAULT event.notes = f'Deposit {amount} {vault_asset.symbol} to {vault_type} MakerDAO vault' # noqa: E501 event.extra_data = {'vault_type': vault_type} return None, None elif tx_log.topics[0] == GENERIC_EXIT: raw_amount = hex_or_bytes_to_int(tx_log.topics[3]) amount = asset_normalized_value( amount=raw_amount, asset=vault_asset, ) # Go through decoded events to find and edit the transfer event for event in decoded_events: if event.event_type == HistoryEventType.RECEIVE and event.asset == vault_asset and event.balance.amount == amount: # noqa: E501 event.event_type = HistoryEventType.WITHDRAWAL event.event_subtype = HistoryEventSubType.REMOVE_ASSET event.counterparty = CPT_VAULT event.notes = f'Withdraw {amount} {vault_asset.symbol} from {vault_type} MakerDAO vault' # noqa: E501 event.extra_data = {'vault_type': vault_type} return None, None return None, None
def _deserialize_transaction(grant_id: int, rawtx: Dict[str, Any]) -> LedgerAction: """May raise: - DeserializationError - KeyError - UnknownAsset """ timestamp = deserialize_timestamp_from_date( date=rawtx['timestamp'], formatstr='%Y-%m-%dT%H:%M:%S', location='Gitcoin API', skip_milliseconds=True, ) asset = get_gitcoin_asset(symbol=rawtx['asset'], token_address=rawtx['token_address']) raw_amount = deserialize_int_from_str(symbol=rawtx['amount'], location='gitcoin api') amount = asset_normalized_value(raw_amount, asset) if amount == ZERO: raise ZeroGitcoinAmount() # let's use gitcoin's calculated rate for now since they include it in the response usd_value = Price( ZERO) if rawtx['usd_value'] is None else deserialize_price( rawtx['usd_value']) # noqa: E501 rate = Price(ZERO) if usd_value == ZERO else Price(usd_value / amount) raw_txid = rawtx['tx_hash'] tx_type, tx_id = process_gitcoin_txid(key='tx_hash', entry=rawtx) # until we figure out if we can use it https://github.com/gitcoinco/web/issues/9255#issuecomment-874537144 # noqa: E501 clr_round = _calculate_clr_round(timestamp, rawtx) notes = f'Gitcoin grant {grant_id} event' if not clr_round else f'Gitcoin grant {grant_id} event in clr_round {clr_round}' # noqa: E501 return LedgerAction( identifier=1, # whatever -- does not end up in the DB timestamp=timestamp, action_type=LedgerActionType.DONATION_RECEIVED, location=Location.GITCOIN, amount=AssetAmount(amount), asset=asset, rate=rate, rate_asset=A_USD, link=raw_txid, notes=notes, extra_data=GitcoinEventData( tx_id=tx_id, grant_id=grant_id, clr_round=clr_round, tx_type=tx_type, ), )
def get_account_balances(self, account_id: int) -> Dict[Asset, Balance]: """Get the loopring balances of a given account id May Raise: - RemotError if there is a problem querying the loopring api or if the format of the response does not match expectations """ response = self._api_query('user/balances', {'accountId': account_id}) balances = {} for balance_entry in response: try: token_id = balance_entry['tokenId'] total = deserialize_int_from_str(balance_entry['total'], 'loopring_balances') except KeyError as e: raise RemoteError( f'Failed to query loopring balances because a balance entry ' f'{balance_entry} did not contain key {str(e)}', ) from e except DeserializationError as e: raise RemoteError( f'Failed to query loopring balances because a balance entry ' f'amount could not be deserialized {balance_entry}', ) from e if total == ZERO: continue asset = TOKENID_TO_ASSET.get(token_id, None) if asset is None: self.msg_aggregator.add_warning( f'Ignoring loopring balance of unsupported token with id {token_id}', ) continue # not checking for UnsupportedAsset since this should not happen thanks # to the mapping above amount = asset_normalized_value(amount=total, asset=asset) try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing loopring balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balances[asset] = Balance(amount=amount, usd_value=amount * usd_price) return balances
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_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_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 _decode_withdraw_request( # 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]]: topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='WithdrawRequest', argument_names=('user', 'token', 'amount', 'batchId'), ) user = topic_data[0] if not self.base.is_tracked(user): return None, None token = ethaddress_to_asset(topic_data[1]) if token is None: return None, None amount = asset_normalized_value(amount=log_data[0], asset=token) 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, # Asset means nothing here since the event is informational. TODO: Improve? asset=token, balance=Balance(amount=amount), notes= f'Request a withdrawal of {amount} {token.symbol} from DXDao Mesa', event_type=HistoryEventType.INFORMATIONAL, event_subtype=HistoryEventSubType.REMOVE_ASSET, counterparty=CPT_DXDAO_MESA, ) return event, None
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 _query_vault_details( self, vault: MakerDAOVault, proxy: ChecksumEthAddress, urn: ChecksumEthAddress, ) -> Optional[MakerDAOVaultDetails]: # They can raise: # ConversionError due to hex_or_bytes_to_address, hexstr_to_int # RemoteError due to external query errors events = self.ethereum.get_logs( contract_address=MAKERDAO_CDP_MANAGER.address, abi=MAKERDAO_CDP_MANAGER.abi, event_name='NewCdp', argument_filters={'cdp': vault.identifier}, from_block=MAKERDAO_CDP_MANAGER.deployed_block, ) if len(events) == 0: self.msg_aggregator.add_error( 'No events found for a Vault creation. This should never ' 'happen. Please open a bug report: https://github.com/rotki/rotki/issues', ) return None if len(events) != 1: log.error( f'Multiple events found for a Vault creation: {events}. Taking ' f'only the first. This should not happen. Something is wrong', ) self.msg_aggregator.add_error( 'Multiple events found for a Vault creation. This should never ' 'happen. Please open a bug report: https://github.com/rotki/rotki/issues', ) creation_ts = self.ethereum.get_event_timestamp(events[0]) # get vat frob events for cross-checking argument_filters = { 'sig': '0x76088703', # frob 'arg1': '0x' + vault.ilk.hex(), # ilk 'arg2': address_to_bytes32(urn), # urn # arg3 can be urn for the 1st deposit, and proxy/owner for the next ones # so don't filter for it # 'arg3': address_to_bytes32(proxy), # proxy - owner } frob_events = self.ethereum.get_logs( contract_address=MAKERDAO_VAT.address, abi=MAKERDAO_VAT.abi, event_name='LogNote', argument_filters=argument_filters, from_block=MAKERDAO_VAT.deployed_block, ) frob_event_tx_hashes = [x['transactionHash'] for x in frob_events] gemjoin = GEMJOIN_MAPPING.get(vault.collateral_type, None) if gemjoin is None: self.msg_aggregator.add_warning( f'Unknown makerdao vault collateral type detected {vault.collateral_type}.' 'Skipping ...', ) return None vault_events = [] # Get the collateral deposit events argument_filters = { 'sig': '0x3b4da69f', # join # In cases where a CDP has been migrated from a SAI CDP to a DAI # Vault the usr in the first deposit will be the old address. To # detect the first deposit in these cases we need to check for # arg1 being the urn # 'usr': proxy, 'arg1': address_to_bytes32(urn), } events = self.ethereum.get_logs( contract_address=gemjoin.address, abi=gemjoin.abi, event_name='LogNote', argument_filters=argument_filters, from_block=gemjoin.deployed_block, ) # all subsequent deposits should have the proxy as a usr # but for non-migrated CDPS the previous query would also work # so in those cases we will have the first deposit 2 times argument_filters = { 'sig': '0x3b4da69f', # join 'usr': proxy, } events.extend( self.ethereum.get_logs( contract_address=gemjoin.address, abi=gemjoin.abi, event_name='LogNote', argument_filters=argument_filters, from_block=gemjoin.deployed_block, )) deposit_tx_hashes = set() for event in events: tx_hash = event['transactionHash'] if tx_hash in deposit_tx_hashes: # Skip duplicate deposit that would be detected in non migrated CDP case continue if tx_hash not in frob_event_tx_hashes: # If there is no corresponding frob event then skip continue deposit_tx_hashes.add(tx_hash) amount = asset_normalized_value( amount=hexstr_to_int(event['topics'][3]), asset=vault.collateral_asset, ) timestamp = self.ethereum.get_event_timestamp(event) usd_price = query_usd_price_or_use_default( asset=vault.collateral_asset, time=timestamp, default_value=ZERO, location='vault collateral deposit', ) vault_events.append( VaultEvent( event_type=VaultEventType.DEPOSIT_COLLATERAL, value=Balance(amount, amount * usd_price), timestamp=timestamp, tx_hash=tx_hash, )) # Get the collateral withdrawal events argument_filters = { 'sig': '0xef693bed', # exit 'usr': proxy, } events = self.ethereum.get_logs( contract_address=gemjoin.address, abi=gemjoin.abi, event_name='LogNote', argument_filters=argument_filters, from_block=gemjoin.deployed_block, ) for event in events: tx_hash = event['transactionHash'] if tx_hash not in frob_event_tx_hashes: # If there is no corresponding frob event then skip continue amount = asset_normalized_value( amount=hexstr_to_int(event['topics'][3]), asset=vault.collateral_asset, ) timestamp = self.ethereum.get_event_timestamp(event) usd_price = query_usd_price_or_use_default( asset=vault.collateral_asset, time=timestamp, default_value=ZERO, location='vault collateral withdrawal', ) vault_events.append( VaultEvent( event_type=VaultEventType.WITHDRAW_COLLATERAL, value=Balance(amount, amount * usd_price), timestamp=timestamp, tx_hash=event['transactionHash'], )) total_dai_wei = 0 # Get the dai generation events argument_filters = { 'sig': '0xbb35783b', # move 'arg1': address_to_bytes32(urn), # For CDPs that were created by migrating from SAI the first DAI generation # during vault creation will have the old owner as arg2. So we can't # filter for it here. Still seems like the urn as arg1 is sufficient # 'arg2': address_to_bytes32(proxy), } events = self.ethereum.get_logs( contract_address=MAKERDAO_VAT.address, abi=MAKERDAO_VAT.abi, event_name='LogNote', argument_filters=argument_filters, from_block=MAKERDAO_VAT.deployed_block, ) for event in events: given_amount = _shift_num_right_by( hexstr_to_int(event['topics'][3]), RAY_DIGITS) total_dai_wei += given_amount amount = token_normalized_value( token_amount=given_amount, token=A_DAI, ) timestamp = self.ethereum.get_event_timestamp(event) usd_price = query_usd_price_or_use_default( asset=A_DAI, time=timestamp, default_value=FVal(1), location='vault debt generation', ) vault_events.append( VaultEvent( event_type=VaultEventType.GENERATE_DEBT, value=Balance(amount, amount * usd_price), timestamp=timestamp, tx_hash=event['transactionHash'], )) # Get the dai payback events argument_filters = { 'sig': '0x3b4da69f', # join 'usr': proxy, 'arg1': address_to_bytes32(urn), } events = self.ethereum.get_logs( contract_address=MAKERDAO_DAI_JOIN.address, abi=MAKERDAO_DAI_JOIN.abi, event_name='LogNote', argument_filters=argument_filters, from_block=MAKERDAO_DAI_JOIN.deployed_block, ) for event in events: given_amount = hexstr_to_int(event['topics'][3]) total_dai_wei -= given_amount amount = token_normalized_value( token_amount=given_amount, token=A_DAI, ) if amount == ZERO: # it seems there is a zero DAI value transfer from the urn when # withdrawing ETH. So we should ignore these as events continue timestamp = self.ethereum.get_event_timestamp(event) usd_price = query_usd_price_or_use_default( asset=A_DAI, time=timestamp, default_value=FVal(1), location='vault debt payback', ) vault_events.append( VaultEvent( event_type=VaultEventType.PAYBACK_DEBT, value=Balance(amount, amount * usd_price), timestamp=timestamp, tx_hash=event['transactionHash'], )) # Get the liquidation events argument_filters = {'urn': urn} events = self.ethereum.get_logs( contract_address=MAKERDAO_CAT.address, abi=MAKERDAO_CAT.abi, event_name='Bite', argument_filters=argument_filters, from_block=MAKERDAO_CAT.deployed_block, ) sum_liquidation_amount = ZERO sum_liquidation_usd = ZERO for event in events: if isinstance(event['data'], str): lot = event['data'][:66] else: # bytes lot = event['data'][:32] amount = asset_normalized_value( amount=hexstr_to_int(lot), asset=vault.collateral_asset, ) timestamp = self.ethereum.get_event_timestamp(event) sum_liquidation_amount += amount usd_price = query_usd_price_or_use_default( asset=vault.collateral_asset, time=timestamp, default_value=ZERO, location='vault collateral liquidation', ) amount_usd_value = amount * usd_price sum_liquidation_usd += amount_usd_value vault_events.append( VaultEvent( event_type=VaultEventType.LIQUIDATION, value=Balance(amount, amount_usd_value), timestamp=timestamp, tx_hash=event['transactionHash'], )) total_interest_owed = vault.debt.amount - token_normalized_value( token_amount=total_dai_wei, token=A_DAI, ) # sort vault events by timestamp vault_events.sort(key=lambda event: event.timestamp) return MakerDAOVaultDetails( identifier=vault.identifier, collateral_asset=vault.collateral_asset, total_interest_owed=total_interest_owed, creation_ts=creation_ts, total_liquidated=Balance(sum_liquidation_amount, sum_liquidation_usd), events=vault_events, )
def _deserialize_nft( self, entry: Dict[str, Any], owner_address: ChecksumEthAddress, eth_usd_price: FVal, ) -> 'NFT': """May raise: - DeserializationError if the given dict can't be deserialized - UnknownAsset if the given payment token isn't known """ if not isinstance(entry, dict): raise DeserializationError( f'Failed to deserialize NFT value from non dict value: {entry}', ) try: last_sale = entry.get('last_sale') if last_sale: if last_sale['payment_token']['symbol'] in ('ETH', 'WETH'): payment_token = A_ETH else: payment_token = EthereumToken( to_checksum_address(last_sale['payment_token']['address']), ) amount = asset_normalized_value(int(last_sale['total_price']), payment_token) eth_price = FVal(last_sale['payment_token']['eth_price']) last_price_in_eth = amount * eth_price else: last_price_in_eth = ZERO floor_price = ZERO collection = None # NFT might not be part of a collection if 'collection' in entry: saved_entry = self.collections.get(entry['collection']['name']) if saved_entry is None: # we haven't got this collection in memory. Query opensea for info self.gather_account_collections(account=owner_address) # try to get the info again saved_entry = self.collections.get(entry['collection']['name']) if saved_entry: collection = saved_entry if saved_entry.floor_price is not None: floor_price = saved_entry.floor_price else: # should not happen. That means collections endpoint doesnt return anything collection_data = entry['collection'] collection = Collection( name=collection_data['name'], banner_image=collection_data['banner_image_url'], description=collection_data['description'], large_image=collection_data['large_image_url'], ) price_in_eth = max(last_price_in_eth, floor_price) price_in_usd = price_in_eth * eth_usd_price token_id = entry['asset_contract']['address'] + '_' + entry['token_id'] if entry['asset_contract']['asset_contract_type'] == 'semi-fungible': token_id += f'_{str(owner_address)}' return NFT( token_identifier=NFT_DIRECTIVE + token_id, background_color=entry['background_color'], image_url=entry['image_url'], name=entry['name'], external_link=entry['external_link'], permalink=entry['permalink'], price_eth=price_in_eth, price_usd=price_in_usd, collection=collection, ) except KeyError as e: raise DeserializationError(f'Could not find key {str(e)} when processing Opensea NFT data') from e # noqa: E501
def _decode_vault_change( 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]]: """Decode CDPManger Frob (vault change) Used to find the vault id of a collateral deposit """ cdp_id = hex_or_bytes_to_int(tx_log.topics[2]) dink = hex_or_bytes_to_int(tx_log.topics[3]) action_item = None for item in action_items: if item.extra_data and 'vault_address' in item.extra_data: action_item = item break if action_item is not None: # this concerns a vault debt payback. Checking only if CDP matches since # the owner response is at the time of the call and may have changed cdp_address, _ = self._get_vault_details(cdp_id) if cdp_address != action_item.extra_data[ 'vault_address']: # type: ignore return None, None # vault address does not match # now find the payback transfer and transform it for event in decoded_events: if event.event_type == action_item.from_event_type and event.event_subtype == action_item.from_event_subtype and event.asset == action_item.asset and event.balance.amount == action_item.amount: # noqa: E501 if action_item.to_event_type: event.event_type = action_item.to_event_type if action_item.to_event_subtype: event.event_subtype = action_item.to_event_subtype if action_item.to_counterparty: event.counterparty = action_item.to_counterparty if action_item.extra_data: event.extra_data = action_item.extra_data event.extra_data['cdp_id'] = cdp_id event.notes = f'Payback {event.balance.amount} DAI of debt to makerdao vault {cdp_id}' # noqa: E501 break else: # collateral deposit # dink is the raw collateral amount change. Let's use this event to see if # there was a deposit event beforehand to append the cdp id for event in decoded_events: if event.event_type == HistoryEventType.DEPOSIT and event.event_subtype == HistoryEventSubType.DEPOSIT_ASSET and event.counterparty == CPT_VAULT: # noqa: E501 normalized_dink = asset_normalized_value(amount=dink, asset=event.asset) if normalized_dink != event.balance.amount: continue vault_type = event.extra_data.get( 'vault_type', 'unknown' ) if event.extra_data else 'unknown' # noqa: E501 event.notes = f'Deposit {event.balance.amount} {event.asset.symbol} to {vault_type} vault {cdp_id}' # noqa: E501 break return None, None
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