Beispiel #1
0
def _parse_atoken_balance_history(
    history: List[Dict[str, Any]],
    from_ts: Timestamp,
    to_ts: Timestamp,
) -> List[ATokenBalanceHistory]:
    result = []
    for entry in history:
        timestamp = entry['timestamp']
        if timestamp < from_ts or timestamp > to_ts:
            continue

        entry_id = entry['id']
        pairs = entry_id.split('0x')
        if len(pairs) not in (4, 5):
            log.error(
                f'Expected to find 3-4 hashes in graph\'s aTokenBalanceHistory '
                f'id but the encountered id does not match: {entry_id}. Skipping entry...',
            )
            continue

        try:
            address_s = '0x' + pairs[2]
            reserve_address = deserialize_ethereum_address(address_s)
        except DeserializationError:
            log.error(f'Error deserializing reserve address {address_s}', )
            continue

        version = _get_version_from_reserveid(pairs, 3)
        tx_hash = '0x' + pairs[4]
        asset = aave_reserve_address_to_reserve_asset(reserve_address)
        if asset is None:
            log.error(
                f'Unknown aave reserve address returned by atoken balance history '
                f' graph query: {reserve_address}. Skipping entry ...', )
            continue

        _, decimals = _get_reserve_address_decimals(asset)
        if 'currentATokenBalance' in entry:
            balance = token_normalized_value_decimals(
                int(entry['currentATokenBalance']),
                token_decimals=decimals,
            )
        else:
            balance = token_normalized_value_decimals(
                int(entry['balance']),
                token_decimals=decimals,
            )
        result.append(
            ATokenBalanceHistory(
                reserve_address=reserve_address,
                balance=balance,
                tx_hash=tx_hash,
                timestamp=timestamp,
                version=version,
            ))

    return result
Beispiel #2
0
    def _parse_repays(
        self,
        repays: List[Dict[str, Any]],
        from_ts: Timestamp,
        to_ts: Timestamp,
    ) -> List[AaveRepayEvent]:
        events = []
        for entry in repays:
            common = _parse_common_event_data(entry, from_ts, to_ts)
            if common is None:
                continue  # either timestamp out of range or error (logged in the function above)
            timestamp, tx_hash, index = common
            result = _get_reserve_asset_and_decimals(entry,
                                                     reserve_key='reserve')
            if result is None:
                continue  # problem parsing, error already logged
            asset, decimals = result
            if 'amountAfterFee' in entry:
                amount_after_fee = token_normalized_value_decimals(
                    int(entry['amountAfterFee']),
                    token_decimals=decimals,
                )
                fee = token_normalized_value_decimals(int(entry['fee']),
                                                      token_decimals=decimals)
            else:
                # In the V2 subgraph the amountAfterFee and Fee keys are replaced by amount
                amount_after_fee = token_normalized_value_decimals(
                    int(entry['amount']),
                    token_decimals=decimals,
                )
                fee = ZERO
            usd_price = query_usd_price_zero_if_error(
                asset=asset,
                time=timestamp,
                location=f'aave repay event {tx_hash} from graph query',
                msg_aggregator=self.msg_aggregator,
            )
            events.append(
                AaveRepayEvent(
                    event_type='repay',
                    asset=asset,
                    value=Balance(amount=amount_after_fee,
                                  usd_value=amount_after_fee * usd_price),
                    fee=Balance(amount=fee, usd_value=fee * usd_price),
                    block_number=0,  # can't get from graph query
                    timestamp=timestamp,
                    tx_hash=tx_hash,
                    log_index=
                    index,  # not really the log index, but should also be unique
                ))

        return events
Beispiel #3
0
def check_airdrops(
    addresses: List[ChecksumEthAddress],
    data_dir: Path,
) -> Dict[ChecksumEthAddress, Dict]:
    """Checks airdrop data for the given list of ethereum addresses

    May raise:
        - RemoteError if the remote request fails
    """
    found_data: Dict[ChecksumEthAddress,
                     Dict] = defaultdict(lambda: defaultdict(dict))
    for protocol_name, airdrop_data in AIRDROPS.items():
        data, csvfile = get_airdrop_data(protocol_name, data_dir)
        for addr, amount, *_ in data:
            # not doing to_checksum_address() here since the file addresses are checksummed
            # and doing to_checksum_address() so many times hits performance
            if protocol_name in ('cornichon', 'tornado', 'grain', 'lido'):
                amount = token_normalized_value_decimals(int(amount), 18)
            if addr in addresses:
                found_data[addr][protocol_name] = {
                    'amount': str(amount),
                    'asset': airdrop_data[1],
                    'link': airdrop_data[2],
                }
        csvfile.close()

    return dict(found_data)
Beispiel #4
0
def _decode_token(entry: Tuple) -> TokenDetails:
    decimals = entry[0][3]
    return TokenDetails(
        address=entry[0][0],
        name=entry[0][1],
        symbol=entry[0][2],
        decimals=decimals,
        amount=token_normalized_value_decimals(entry[1], decimals),
    )
Beispiel #5
0
def check_airdrops(
    addresses: List[ChecksumEthAddress],
    data_dir: Path,
) -> Dict[ChecksumEthAddress, Dict]:
    """Checks airdrop data for the given list of ethereum addresses

    May raise:
        - RemoteError if the remote request fails
    """
    found_data: Dict[ChecksumEthAddress,
                     Dict] = defaultdict(lambda: defaultdict(dict))
    for protocol_name, airdrop_data in AIRDROPS.items():
        data, csvfile = get_airdrop_data(protocol_name, data_dir)
        for row in data:
            if len(row) < 2:
                raise UnableToDecryptRemoteData(
                    f'Airdrop CSV for {protocol_name} contains an invalid row: {row}',
                )
            addr, amount, *_ = row
            # not doing to_checksum_address() here since the file addresses are checksummed
            # and doing to_checksum_address() so many times hits performance
            if protocol_name in (
                    'cornichon',
                    'tornado',
                    'grain',
                    'lido',
                    'sdl',
                    'cow_mainnet',
                    'cow_gnosis',
            ):
                amount = token_normalized_value_decimals(int(amount), 18)
            if addr in addresses:
                found_data[addr][protocol_name] = {
                    'amount': str(amount),
                    'asset': airdrop_data[1],
                    'link': airdrop_data[2],
                }
        csvfile.close()

    for protocol_name, poap_airdrop_data in POAP_AIRDROPS.items():
        data_dict = get_poap_airdrop_data(protocol_name, data_dir)
        for addr, assets in data_dict.items():
            # not doing to_checksum_address() here since the file addresses are checksummed
            # and doing to_checksum_address() so many times hits performance
            if addr in addresses:
                if 'poap' not in found_data[addr]:
                    found_data[addr]['poap'] = []

                found_data[addr]['poap'].append({
                    'event': protocol_name,
                    'assets': assets,
                    'link': poap_airdrop_data[1],
                    'name': poap_airdrop_data[2],
                })

    return dict(found_data)
Beispiel #6
0
 def on_account_addition(self, address: ChecksumEthAddress) -> Optional[List[AssetBalance]]:
     """When an account is added for adex check its balances"""
     balance = self.staking_pool.call(self.ethereum, 'balanceOf', arguments=[address])
     if balance == 0:
         return None
     # else the address has staked adex
     usd_price = Inquirer().find_usd_price(A_ADX)
     share_price = self.staking_pool.call(self.ethereum, 'shareValue')
     amount = token_normalized_value_decimals(
         token_amount=balance * share_price / (FVal(10) ** 18),
         token_decimals=18,
     )
     return [AssetBalance(asset=A_ADX, balance=Balance(amount=amount, usd_value=amount * usd_price))]  # noqa: E501
Beispiel #7
0
    def query_defi_balances(
        self,
        addresses: List[ChecksumEthAddress],
    ) -> Dict[ChecksumEthAddress, List[DefiProtocolBalances]]:
        defi_balances = defaultdict(list)
        for account in addresses:
            balances = self.zerion_sdk.all_balances_for_account(account)
            if len(balances) != 0:
                defi_balances[account] = balances

        # and also query balances of tokens that are not detected by zerion adapter contract
        result = multicall_specific(
            ethereum=self.ethereum,
            contract=VOTE_ESCROWED_CRV,
            method_name='locked',
            arguments=[[x] for x in addresses],
        )
        crv_price = Price(ZERO)
        if any(x[0] != 0 for x in result):
            crv_price = Inquirer().find_usd_price(A_CRV)
        for idx, address in enumerate(addresses):
            balance = result[idx][0]
            if balance == 0:
                continue
            # else the address has vote escrowed CRV
            amount = token_normalized_value_decimals(token_amount=balance,
                                                     token_decimals=18)
            protocol_balance = DefiProtocolBalances(
                protocol=DefiProtocol(
                    name='Curve • Vesting',
                    description='Curve vesting or locked in escrow for voting',
                    url='https://www.curve.fi/',
                    version=1,
                ),
                balance_type='Asset',
                base_balance=DefiBalance(
                    token_address=VOTE_ESCROWED_CRV.address,
                    token_name='Vote-escrowed CRV',
                    token_symbol='veCRV',
                    balance=Balance(
                        amount=amount,
                        usd_value=amount * crv_price,
                    ),
                ),
                underlying_balances=[],
            )
            defi_balances[address].append(protocol_balance)

        return defi_balances
Beispiel #8
0
    def _get_single_balance(
        self,
        protocol_name: str,
        entry: Tuple[Tuple[str, str, str, int], int],
    ) -> DefiBalance:
        """
        This method can raise DeserializationError while deserializing the token address
        or handling the specific protocol.
        """
        metadata = entry[0]
        balance_value = entry[1]
        decimals = metadata[3]
        normalized_value = token_normalized_value_decimals(
            balance_value, decimals)
        token_symbol = metadata[2]
        token_address = deserialize_ethereum_address(metadata[0])
        token_name = metadata[1]

        special_handling = self.handle_protocols(
            protocol_name=protocol_name,
            token_symbol=token_symbol,
            normalized_balance=normalized_value,
            token_address=token_address,
            token_name=token_name,
        )
        if special_handling:
            return special_handling

        try:
            token = EthereumToken(token_address)
            usd_price = Inquirer().find_usd_price(token)
        except (UnknownAsset, UnsupportedAsset):
            if not _is_token_non_standard(token_symbol, token_address):
                self.msg_aggregator.add_warning(
                    f'Unsupported asset {token_symbol} with address '
                    f'{token_address} encountered during DeFi protocol queries',
                )
            usd_price = Price(ZERO)

        usd_value = normalized_value * usd_price
        defi_balance = DefiBalance(
            token_address=token_address,
            token_name=token_name,
            token_symbol=token_symbol,
            balance=Balance(amount=normalized_value, usd_value=usd_value),
        )
        return defi_balance
Beispiel #9
0
def _parse_atoken_balance_history(
    history: List[Dict[str, Any]],
    from_ts: Timestamp,
    to_ts: Timestamp,
) -> List[ATokenBalanceHistory]:
    result = []
    for entry in history:
        timestamp = entry['timestamp']
        if timestamp < from_ts or timestamp > to_ts:
            continue

        entry_id = entry['id']
        pairs = entry_id.split('0x')
        if len(pairs) != 4:
            log.error(
                f'Expected to find 3 hashes in graps\'s aTokenBalanceHistory '
                f'id but the encountered id does not match: {entry_id}. Skipping entry...',
            )
            continue

        reserve_address = to_checksum_address('0x' + pairs[2])
        tx_hash = '0x' + pairs[3]
        asset = AAVE_RESERVE_TO_ASSET.get(reserve_address, None)
        if asset is None:
            log.error(
                f'Unknown aave reserve address returned by atoken balance history '
                f' graph query: {reserve_address}. Skipping entry ...', )
            continue

        _, decimals = _get_reserve_address_decimals(asset.identifier)
        balance = token_normalized_value_decimals(int(entry['balance']),
                                                  token_decimals=decimals)
        result.append(
            ATokenBalanceHistory(
                reserve_address=reserve_address,
                balance=balance,
                tx_hash=tx_hash,
                timestamp=timestamp,
            ))

    return result
Beispiel #10
0
    def get_balances(
        self,
        addresses: List[ChecksumAddress],
    ) -> Dict[ChecksumAddress, Balance]:
        """Return the addresses' balances (staked amount per pool) in the AdEx
        protocol.

        May raise:
        - RemoteError: Problem querying the chain
        """
        if len(addresses) == 0:
            return {}

        result = multicall_specific(
            ethereum=self.ethereum,
            contract=self.staking_pool,
            method_name='balanceOf',
            arguments=[[x] for x in addresses],
        )
        if all(x[0] == 0 for x in result):
            return {}  # no balances found

        staking_balances = {}
        usd_price = Inquirer().find_usd_price(A_ADX)
        share_price = self.staking_pool.call(self.ethereum, 'shareValue')
        for idx, address in enumerate(addresses):
            balance = result[idx][0]
            if balance == 0:
                continue
            # else the address has staked adex
            amount = token_normalized_value_decimals(
                token_amount=balance * share_price / (FVal(10)**18),
                token_decimals=18,
            )
            staking_balances[address] = Balance(amount=amount,
                                                usd_value=amount * usd_price)

        return staking_balances
Beispiel #11
0
 def _get_asset_and_balance(
     self,
     entry: Dict[str, Any],
     timestamp: Timestamp,
     reserve_key: str,
     amount_key: str,
     location: str,
 ) -> Optional[Tuple[Asset, Balance]]:
     """Utility function to parse asset from graph query amount and price and return balance"""
     result = _get_reserve_asset_and_decimals(entry, reserve_key)
     if result is None:
         return None
     asset, decimals = result
     amount = token_normalized_value_decimals(
         token_amount=int(entry[amount_key]),
         token_decimals=decimals,
     )
     usd_price = query_usd_price_zero_if_error(
         asset=asset,
         time=timestamp,
         location=location,
         msg_aggregator=self.msg_aggregator,
     )
     return asset, Balance(amount=amount, usd_value=amount * usd_price)
Beispiel #12
0
    def find_curve_pool_price(
        self,
        lp_token: EthereumToken,
    ) -> Optional[Price]:
        """
        1. Obtain the pool for this token
        2. Obtain prices for assets in pool
        3. Obtain the virtual price for share and the balances of each
        token in the pool
        4. Calc the price for a share

        Returns the price of 1 LP token from the pool
        """
        assert self._ethereum is not None, 'Inquirer ethereum manager should have been initialized'  # noqa: E501

        pools = get_curve_pools()
        if lp_token.ethereum_address not in pools:
            return None
        pool = pools[lp_token.ethereum_address]
        tokens = []
        # Translate addresses to tokens
        try:
            for asset in pool.assets:
                if asset == '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE':
                    tokens.append(A_WETH)
                else:
                    tokens.append(EthereumToken(asset))
        except UnknownAsset:
            return None

        # Get price for each token in the pool
        prices = []
        for token in tokens:
            price = self.find_usd_price(token)
            if price == Price(ZERO):
                log.error(
                    f'Could not calculate price for {lp_token} due to inability to '
                    f'fetch price for {token}.', )
                return None
            prices.append(price)

        # Query virtual price of LP share and balances in the pool for each token
        contract = EthereumContract(
            address=pool.pool_address,
            abi=CURVE_POOL_ABI,
            deployed_block=0,
        )
        calls = [(pool.pool_address,
                  contract.encode(method_name='get_virtual_price'))]
        calls += [(pool.pool_address,
                   contract.encode(method_name='balances', arguments=[i]))
                  for i in range(len(pool.assets))]
        output = multicall_2(
            ethereum=self._ethereum,
            require_success=False,
            calls=calls,
        )

        # Check that the output has the correct structure
        if not all([len(call_result) == 2 for call_result in output]):
            log.debug(
                f'Failed to query contract methods while finding curve pool price. '
                f'Not every outcome has length 2. {output}', )
            return None
        # Check that all the requests were successful
        if not all([contract_output[0] for contract_output in output]):
            log.debug(
                f'Failed to query contract methods while finding curve price. {output}'
            )
            return None
        # Deserialize information obtained in the multicall execution
        data = []
        # https://github.com/PyCQA/pylint/issues/4739
        virtual_price_decoded = contract.decode(output[0][1],
                                                'get_virtual_price')  # pylint: disable=unsubscriptable-object  # noqa: E501
        if not _check_curve_contract_call(virtual_price_decoded):
            log.debug(
                f'Failed to decode get_virtual_price while finding curve price. {output}'
            )
            return None
        data.append(FVal(virtual_price_decoded[0]))  # pylint: disable=unsubscriptable-object
        for i in range(len(pool.assets)):
            amount_decoded = contract.decode(output[i + 1][1],
                                             'balances',
                                             arguments=[i])
            if not _check_curve_contract_call(amount_decoded):
                log.debug(
                    f'Failed to decode balances {i} while finding curve price. {output}'
                )
                return None
            # https://github.com/PyCQA/pylint/issues/4739
            amount = amount_decoded[0]  # pylint: disable=unsubscriptable-object
            normalized_amount = token_normalized_value_decimals(
                amount, tokens[i].decimals)
            data.append(normalized_amount)

        # Prices and data should verify this relation for the following operations
        if len(prices) != len(data) - 1:
            log.debug(
                f'Length of prices {len(prices)} does not match len of data {len(data)} '
                f'while querying curve pool price.', )
            return None
        # Total number of assets price in the pool
        total_assets_price = sum(map(operator.mul, data[1:], prices))
        if total_assets_price == 0:
            log.error(
                f'Curve pool price returned unexpected data {data} that lead to a zero price.',
            )
            return None

        # Calculate weight of each asset as the proportion of tokens value
        weights = map(lambda x: data[x + 1] * prices[x] / total_assets_price,
                      range(len(tokens)))
        assets_price = FVal(sum(map(operator.mul, weights, prices)))
        return (assets_price * FVal(data[0])) / (10**lp_token.decimals)
Beispiel #13
0
    def get_positions(
        self,
        addresses_list: List[ChecksumEthAddress],
    ) -> Dict[ChecksumEthAddress, Trove]:
        contract = EthereumContract(
            address=LIQUITY_TROVE_MANAGER.address,
            abi=LIQUITY_TROVE_MANAGER.abi,
            deployed_block=LIQUITY_TROVE_MANAGER.deployed_block,
        )
        # make a copy of the list to avoid modifications in the list that is passed as argument
        addresses = list(addresses_list)
        proxied_addresses = self._get_accounts_having_proxy()
        proxies_to_address = {v: k for k, v in proxied_addresses.items()}
        addresses += proxied_addresses.values()

        calls = [(LIQUITY_TROVE_MANAGER.address,
                  contract.encode(method_name='Troves', arguments=[x]))
                 for x in addresses]
        outputs = multicall_2(
            ethereum=self.ethereum,
            require_success=False,
            calls=calls,
        )

        data: Dict[ChecksumEthAddress, Trove] = {}
        eth_price = Inquirer().find_usd_price(A_ETH)
        lusd_price = Inquirer().find_usd_price(A_LUSD)
        for idx, output in enumerate(outputs):
            status, result = output
            if status is True:
                try:
                    trove_info = contract.decode(result,
                                                 'Troves',
                                                 arguments=[addresses[idx]])
                    trove_is_active = bool(trove_info[3])  # pylint: disable=unsubscriptable-object
                    if not trove_is_active:
                        continue
                    collateral = deserialize_asset_amount(
                        token_normalized_value_decimals(trove_info[1], 18),  # noqa: E501 pylint: disable=unsubscriptable-object
                    )
                    debt = deserialize_asset_amount(
                        token_normalized_value_decimals(trove_info[0], 18),  # noqa: E501 pylint: disable=unsubscriptable-object
                    )
                    collateral_balance = AssetBalance(
                        asset=A_ETH,
                        balance=Balance(
                            amount=collateral,
                            usd_value=eth_price * collateral,
                        ),
                    )
                    debt_balance = AssetBalance(
                        asset=A_LUSD,
                        balance=Balance(
                            amount=debt,
                            usd_value=lusd_price * debt,
                        ),
                    )
                    # Avoid division errors
                    collateralization_ratio: Optional[FVal]
                    liquidation_price: Optional[FVal]
                    if debt > 0:
                        collateralization_ratio = eth_price * collateral / debt * 100
                    else:
                        collateralization_ratio = None
                    if collateral > 0:
                        liquidation_price = debt * lusd_price * FVal(
                            MIN_COLL_RATE) / collateral
                    else:
                        liquidation_price = None

                    account_address = addresses[idx]
                    if account_address in proxies_to_address:
                        account_address = proxies_to_address[account_address]
                    data[account_address] = Trove(
                        collateral=collateral_balance,
                        debt=debt_balance,
                        collateralization_ratio=collateralization_ratio,
                        liquidation_price=liquidation_price,
                        active=trove_is_active,
                        trove_id=trove_info[4],  # pylint: disable=unsubscriptable-object
                    )
                except DeserializationError as e:
                    self.msg_aggregator.add_warning(
                        f'Ignoring Liquity trove information. '
                        f'Failed to decode contract information. {str(e)}.', )
        return data
Beispiel #14
0
    def get_dill_balances(
        self,
        addresses: List[ChecksumEthAddress],
    ) -> Dict[ChecksumEthAddress, DillBalance]:
        """
        Query information for amount locked, pending rewards and time until unlock
        for Pickle's dill.
        """
        api_output = {}
        rewards_calls = [(
            PICKLE_DILL_REWARDS.address,
            self.rewards_contract.encode(method_name='claim', arguments=[x]),
        ) for x in addresses]
        balance_calls = [(PICKLE_DILL.address,
                          self.dill_contract.encode(method_name='locked',
                                                    arguments=[x]))
                         for x in addresses]
        outputs = multicall_2(
            ethereum=self.ethereum,
            require_success=False,
            calls=rewards_calls + balance_calls,
        )
        reward_outputs, dill_outputs = outputs[:len(addresses)], outputs[
            len(addresses):]

        pickle_price = Inquirer().find_usd_price(A_PICKLE)
        for idx, output in enumerate(reward_outputs):
            status_rewards, result = output
            status_dill, result_dill = dill_outputs[idx]
            address = addresses[idx]
            if all((status_rewards, status_dill)):
                try:
                    rewards = self.rewards_contract.decode(result,
                                                           'claim',
                                                           arguments=[address])
                    dill_amounts = self.dill_contract.decode(
                        result_dill,
                        'locked',
                        arguments=[address],
                    )
                    dill_rewards = token_normalized_value_decimals(
                        token_amount=rewards[0],  # pylint: disable=unsubscriptable-object
                        token_decimals=A_PICKLE.decimals,
                    )
                    dill_locked = token_normalized_value_decimals(
                        token_amount=dill_amounts[0],  # pylint: disable=unsubscriptable-object
                        token_decimals=A_PICKLE.decimals,
                    )
                    balance = DillBalance(
                        dill_amount=AssetBalance(
                            asset=A_PICKLE,
                            balance=Balance(
                                amount=dill_locked,
                                usd_value=pickle_price * dill_locked,
                            ),
                        ),
                        pending_rewards=AssetBalance(
                            asset=A_PICKLE,
                            balance=Balance(
                                amount=dill_rewards,
                                usd_value=pickle_price * dill_rewards,
                            ),
                        ),
                        lock_time=deserialize_timestamp(dill_amounts[1]),  # noqa: E501 pylint: disable=unsubscriptable-object
                    )
                    api_output[address] = balance
                except (DeserializationError, IndexError) as e:
                    self.msg_aggregator.add_error(
                        f'Failed to query dill information for address {address}. {str(e)}',
                    )

        return api_output