def test_balance_sheet_to_dict(): a = BalanceSheet( assets={ A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('3'), usd_value=FVal('900')), }, liabilities={ A_DAI: Balance(amount=FVal('5'), usd_value=FVal('5.1')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) assert a.to_dict() == { 'assets': { 'USD': { 'amount': FVal('2'), 'usd_value': FVal('2') }, 'ETH': { 'amount': FVal('3'), 'usd_value': FVal('900') }, }, 'liabilities': { ethaddress_to_identifier('0x6B175474E89094C44Da98b954EedeAC495271d0F'): { 'amount': FVal('5'), 'usd_value': FVal('5.1') }, # noqa: E501 'ETH': { 'amount': FVal('0.5'), 'usd_value': FVal('150') }, }, }
def test_balance_sheet_addition(): a = BalanceSheet( assets={ A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('1.5'), usd_value=FVal('450')), }, liabilities={ A_DAI: Balance(amount=FVal('5'), usd_value=FVal('5.1')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) b = BalanceSheet( assets={ A_EUR: Balance(amount=FVal('3'), usd_value=FVal('3.5')), A_ETH: Balance(amount=FVal('3'), usd_value=FVal('900')), A_BTC: Balance(amount=FVal('1'), usd_value=FVal('10000')), }, liabilities={ A_DAI: Balance(amount=FVal('10'), usd_value=FVal('10.2')), }, ) assert a != b c = BalanceSheet( assets={ A_EUR: Balance(amount=FVal('3'), usd_value=FVal('3.5')), A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('4.5'), usd_value=FVal('1350')), A_BTC: Balance(amount=FVal('1'), usd_value=FVal('10000')), }, liabilities={ A_DAI: Balance(amount=FVal('15'), usd_value=FVal('15.3')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) assert a + b == c
def test_balance_sheet_to_dict(): a = BalanceSheet( assets={ A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('3'), usd_value=FVal('900')), }, liabilities={ A_DAI: Balance(amount=FVal('5'), usd_value=FVal('5.1')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) assert a.to_dict() == { 'assets': { 'USD': { 'amount': FVal('2'), 'usd_value': FVal('2') }, 'ETH': { 'amount': FVal('3'), 'usd_value': FVal('900') }, }, 'liabilities': { 'DAI': { 'amount': FVal('5'), 'usd_value': FVal('5.1') }, 'ETH': { 'amount': FVal('0.5'), 'usd_value': FVal('150') }, }, }
def test_balance_sheet_serialize(): a = BalanceSheet( assets={ A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('3'), usd_value=FVal('900')), }, liabilities={ A_DAI: Balance(amount=FVal('5'), usd_value=FVal('5.1')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) assert a.serialize() == { 'assets': { 'USD': { 'amount': '2', 'usd_value': '2' }, 'ETH': { 'amount': '3', 'usd_value': '900' }, }, 'liabilities': { 'DAI': { 'amount': '5', 'usd_value': '5.1' }, 'ETH': { 'amount': '0.5', 'usd_value': '150' }, }, }
def query_ethereum_balances(self, force_token_detection: bool) -> None: """Queries all the ethereum balances and populates the state May raise: - RemoteError if an external service such as Etherscan or cryptocompare is queried and there is a problem with its query. - EthSyncError if querying the token balances through a provided ethereum client and the chain is not synced """ if len(self.accounts.eth) == 0: return # Query ethereum ETH balances eth_accounts = self.accounts.eth eth_usd_price = Inquirer().find_usd_price(A_ETH) balances = self.ethereum.get_multieth_balance(eth_accounts) eth_total = FVal(0) for account, balance in balances.items(): eth_total += balance usd_value = balance * eth_usd_price self.balances.eth[account] = BalanceSheet(assets=defaultdict( Balance, {A_ETH: Balance(balance, usd_value)}), ) self.totals.assets[A_ETH] = Balance(amount=eth_total, usd_value=eth_total * eth_usd_price) self.query_defi_balances() self.query_ethereum_tokens(force_token_detection) self._add_protocol_balances()
def get_balance(self) -> BalanceSheet: starting_assets = {self.collateral_asset: self.collateral} if self.collateral.amount != ZERO else {} # noqa: E501 starting_liabilities = {A_DAI: self.debt} if self.debt.amount != ZERO else {} return BalanceSheet( assets=defaultdict(Balance, starting_assets), liabilities=defaultdict(Balance, starting_liabilities), # type: ignore )
def get_balance(self) -> BalanceSheet: return BalanceSheet( assets=defaultdict(Balance, {self.collateral_asset: self.collateral}), liabilities=defaultdict(Balance, {EthereumToken('DAI'): self.debt}), )
def test_get_vault_balance( inquirer, # pylint: disable=unused-argument mocked_current_prices, ): debt_value = FVal('2000') owner = make_ethereum_address() vault = MakerDAOVault( identifier=1, collateral_type='ETH-A', collateral_asset=A_ETH, owner=owner, collateral=Balance(FVal('100'), FVal('20000')), debt=Balance(debt_value, debt_value * mocked_current_prices['DAI']), collateralization_ratio='990%', liquidation_ratio=FVal(1.5), liquidation_price=FVal('50'), # not calculated to be correct urn=make_ethereum_address(), stability_fee=ZERO, ) expected_result = BalanceSheet( assets=defaultdict(Balance, {A_ETH: Balance(FVal('100'), FVal('20000'))}), liabilities=defaultdict(Balance, {A_DAI: Balance(FVal('2000'), FVal('2020'))}), ) assert vault.get_balance() == expected_result
def query_kusama_balances(self, wait_available_node: bool = True) -> None: """Queries the KSM balances of the accounts via Kusama endpoints. May raise: - RemotError: if no nodes are available or the balances request fails. """ if len(self.accounts.ksm) == 0: return ksm_usd_price = Inquirer().find_usd_price(A_KSM) if wait_available_node: wait_until_a_node_is_available( substrate_manager=self.kusama, seconds=KUSAMA_NODE_CONNECTION_TIMEOUT, ) account_amount = self.kusama.get_accounts_balance(self.accounts.ksm) total_balance = Balance() for account, amount in account_amount.items(): balance = Balance( amount=amount, usd_value=amount * ksm_usd_price, ) self.balances.ksm[account] = BalanceSheet( assets=defaultdict(Balance, {A_KSM: balance}), ) total_balance += balance self.totals.assets[A_KSM] = total_balance
def modify_eth_account( self, account: ChecksumEthAddress, append_or_remove: str, ) -> None: """Either appends or removes an ETH acccount. Call with 'append' to add the account Call with 'remove' remove the account May raise: - Input error if the given_account is not a valid ETH address - BadFunctionCallOutput if a token is queried from a local chain and the chain is not synced - RemoteError if there is a problem with a query to an external service such as Etherscan or cryptocompare """ eth_usd_price = Inquirer().find_usd_price(A_ETH) remove_with_populated_balance = ( append_or_remove == 'remove' and len(self.balances.eth) != 0 ) # Query the balance of the account except for the case when it's removed # and there is no other account in the balances if append_or_remove == 'append' or remove_with_populated_balance: amount = self.ethereum.get_eth_balance(account) usd_value = amount * eth_usd_price if append_or_remove == 'append': self.accounts.eth.append(account) self.balances.eth[account] = BalanceSheet( assets=defaultdict(Balance, {A_ETH: Balance(amount, usd_value)}), ) # Check if the new account has any staked eth2 deposits self.account_for_staked_eth2_balances([account], at_addition=True) elif append_or_remove == 'remove': if account not in self.accounts.eth: raise InputError('Tried to remove a non existing ETH account') self.accounts.eth.remove(account) balances = self.balances.eth.get(account, None) if balances is not None: for asset, balance in balances.assets.items(): self.totals.assets[asset] -= balance if self.totals.assets[asset].amount <= ZERO: self.totals.assets[asset] = Balance() self.balances.eth.pop(account, None) else: raise AssertionError('Programmer error: Should be append or remove') if len(self.balances.eth) == 0: # If the last account was removed balance should be 0 self.totals.assets[A_ETH] = Balance() elif append_or_remove == 'append': self.totals.assets[A_ETH] += Balance(amount, usd_value) self._query_ethereum_tokens( action=AccountAction.APPEND, given_accounts=[account], )
def test_default_balance_sheet(): a = BalanceSheet( assets={ A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('1.5'), usd_value=FVal('450')), }, liabilities={ A_DAI: Balance(amount=FVal('5'), usd_value=FVal('5.1')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) b = BalanceSheet() # test no args init gives empty default dicts properly assert isinstance(b.assets, dict) assert len(b.assets) == 0 assert isinstance(b.liabilities, dict) assert len(b.liabilities) == 0 assert a != b b += a assert a == b
def test_balance_sheet_raddition(): a = BalanceSheet( assets={ A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('1.5'), usd_value=FVal('450')), }, liabilities={ A_DAI: Balance(amount=FVal('5'), usd_value=FVal('5.1')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) b = BalanceSheet( assets={ A_EUR: Balance(amount=FVal('3'), usd_value=FVal('3.5')), A_ETH: Balance(amount=FVal('3'), usd_value=FVal('900')), A_BTC: Balance(amount=FVal('1'), usd_value=FVal('10000')), }, liabilities={ A_DAI: Balance(amount=FVal('10'), usd_value=FVal('10.2')), }, ) c = BalanceSheet( assets={ A_EUR: Balance(amount=FVal('3'), usd_value=FVal('3.5')), A_USD: Balance(amount=FVal('2'), usd_value=FVal('2')), A_ETH: Balance(amount=FVal('4.5'), usd_value=FVal('1350')), A_BTC: Balance(amount=FVal('1'), usd_value=FVal('10000')), }, liabilities={ A_DAI: Balance(amount=FVal('15'), usd_value=FVal('15.3')), A_ETH: Balance(amount=FVal('0.5'), usd_value=FVal('150')), }, ) result = sum([a, b]) assert isinstance(result, BalanceSheet) assert result == c
def modify_kusama_account( self, account: KusamaAddress, append_or_remove: Literal['append', 'remove'], ) -> None: """Either appends or removes a kusama acccount. Call with 'append' to add the account Call with 'remove' remove the account May raise: - Input error if the given_account is not a valid kusama address - RemoteError if there is a problem with a query to an external service such as Kusama nodes or cryptocompare """ if append_or_remove not in ('append', 'remove'): raise AssertionError(f'Unexpected action: {append_or_remove}') if append_or_remove == 'remove' and account not in self.accounts.ksm: raise InputError('Tried to remove a non existing KSM account') ksm_usd_price = Inquirer().find_usd_price(A_KSM) if append_or_remove == 'append': # Wait until a node is connected when adding a KSM address for the # first time. if len(self.kusama.available_nodes_call_order) == 0: self.kusama.attempt_connections() wait_until_a_node_is_available( substrate_manager=self.kusama, seconds=KUSAMA_NODE_CONNECTION_TIMEOUT, ) amount = self.kusama.get_account_balance(account) balance = Balance(amount=amount, usd_value=amount * ksm_usd_price) self.accounts.ksm.append(account) self.balances.ksm[account] = BalanceSheet( assets=defaultdict(Balance, {A_KSM: balance}), ) self.totals.assets[A_KSM] += balance if append_or_remove == 'remove': if len(self.balances.ksm) > 1: if account in self.balances.ksm: self.totals.assets[A_KSM] -= self.balances.ksm[account].assets[A_KSM] else: # If the last account was removed balance should be 0 self.totals.assets[A_KSM] = Balance() self.balances.ksm.pop(account, None) self.accounts.ksm.remove(account)
def __init__( self, blockchain_accounts: BlockchainAccounts, ethereum_manager: 'EthereumManager', msg_aggregator: MessagesAggregator, database: DBHandler, greenlet_manager: GreenletManager, premium: Optional[Premium], data_directory: Path, eth_modules: Optional[List[str]] = None, ): log.debug('Initializing ChainManager') super().__init__() self.ethereum = ethereum_manager self.database = database self.msg_aggregator = msg_aggregator self.accounts = blockchain_accounts self.data_directory = data_directory self.defi_balances_last_query_ts = Timestamp(0) self.defi_balances: Dict[ChecksumEthAddress, List[DefiProtocolBalances]] = {} self.defi_lock = Semaphore() self.eth2_lock = Semaphore() # Per account balances self.balances = BlockchainBalances(db=database) # Per asset total balances self.totals: BalanceSheet = BalanceSheet() # TODO: Perhaps turn this mapping into a typed dict? self.eth_modules: Dict[str, Union[EthereumModule, Literal['loading']]] = {} if eth_modules: for given_module in eth_modules: if given_module == 'makerdao_dsr': self.eth_modules['makerdao_dsr'] = MakerDAODSR( ethereum_manager=ethereum_manager, database=self.database, premium=premium, msg_aggregator=msg_aggregator, ) elif given_module == 'makerdao_vaults': self.eth_modules['makerdao_vaults'] = MakerDAOVaults( ethereum_manager=ethereum_manager, database=self.database, premium=premium, msg_aggregator=msg_aggregator, ) elif given_module == 'aave': self.eth_modules['aave'] = Aave( ethereum_manager=ethereum_manager, database=self.database, premium=premium, msg_aggregator=msg_aggregator, ) elif given_module == 'compound': self.eth_modules['compound'] = 'loading' # Since Compound initialization needs a few network calls we do it async greenlet_manager.spawn_and_track( after_seconds=None, task_name='Initialize Compound object', method=self._initialize_compound, premium=premium, ) elif given_module == 'uniswap': self.eth_modules['uniswap'] = Uniswap( ethereum_manager=ethereum_manager, database=self.database, premium=premium, msg_aggregator=msg_aggregator, data_directory=self.data_directory, ) elif given_module == 'yearn_vaults': self.eth_modules['yearn_vaults'] = YearnVaults( ethereum_manager=ethereum_manager, database=self.database, premium=premium, msg_aggregator=msg_aggregator, ) else: log.error( f'Unrecognized module value {given_module} given. Skipping...' ) self.premium = premium self.greenlet_manager = greenlet_manager self.zerion = Zerion(ethereum_manager=self.ethereum, msg_aggregator=self.msg_aggregator) for name, module in self.iterate_modules(): self.greenlet_manager.spawn_and_track( after_seconds=None, task_name=f'startup of {name}', method=module.on_startup, )
def get_balance(self) -> BalanceSheet: return BalanceSheet( assets=defaultdict(Balance, {self.collateral_asset: self.collateral}), liabilities=defaultdict(Balance, {A_DAI: self.debt}), )