eth-portfolio 0.5.7__cp312-cp312-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eth-portfolio might be problematic. Click here for more details.

Files changed (83) hide show
  1. eth_portfolio/__init__.py +24 -0
  2. eth_portfolio/_argspec.cp312-win32.pyd +0 -0
  3. eth_portfolio/_argspec.py +43 -0
  4. eth_portfolio/_cache.py +119 -0
  5. eth_portfolio/_config.cp312-win32.pyd +0 -0
  6. eth_portfolio/_config.py +4 -0
  7. eth_portfolio/_db/__init__.py +0 -0
  8. eth_portfolio/_db/decorators.py +147 -0
  9. eth_portfolio/_db/entities.py +311 -0
  10. eth_portfolio/_db/utils.py +616 -0
  11. eth_portfolio/_decimal.py +154 -0
  12. eth_portfolio/_decorators.py +84 -0
  13. eth_portfolio/_exceptions.py +65 -0
  14. eth_portfolio/_ledgers/__init__.py +0 -0
  15. eth_portfolio/_ledgers/address.py +924 -0
  16. eth_portfolio/_ledgers/portfolio.py +328 -0
  17. eth_portfolio/_loaders/__init__.py +33 -0
  18. eth_portfolio/_loaders/_nonce.cp312-win32.pyd +0 -0
  19. eth_portfolio/_loaders/_nonce.py +193 -0
  20. eth_portfolio/_loaders/balances.cp312-win32.pyd +0 -0
  21. eth_portfolio/_loaders/balances.py +95 -0
  22. eth_portfolio/_loaders/token_transfer.py +215 -0
  23. eth_portfolio/_loaders/transaction.py +240 -0
  24. eth_portfolio/_loaders/utils.cp312-win32.pyd +0 -0
  25. eth_portfolio/_loaders/utils.py +67 -0
  26. eth_portfolio/_shitcoins.cp312-win32.pyd +0 -0
  27. eth_portfolio/_shitcoins.py +342 -0
  28. eth_portfolio/_stableish.cp312-win32.pyd +0 -0
  29. eth_portfolio/_stableish.py +42 -0
  30. eth_portfolio/_submodules.py +72 -0
  31. eth_portfolio/_utils.py +215 -0
  32. eth_portfolio/_ydb/__init__.py +0 -0
  33. eth_portfolio/_ydb/token_transfers.py +145 -0
  34. eth_portfolio/address.py +396 -0
  35. eth_portfolio/buckets.py +212 -0
  36. eth_portfolio/constants.cp312-win32.pyd +0 -0
  37. eth_portfolio/constants.py +87 -0
  38. eth_portfolio/portfolio.py +662 -0
  39. eth_portfolio/protocols/__init__.py +64 -0
  40. eth_portfolio/protocols/_base.py +107 -0
  41. eth_portfolio/protocols/convex.py +17 -0
  42. eth_portfolio/protocols/dsr.py +50 -0
  43. eth_portfolio/protocols/lending/README.md +6 -0
  44. eth_portfolio/protocols/lending/__init__.py +50 -0
  45. eth_portfolio/protocols/lending/_base.py +56 -0
  46. eth_portfolio/protocols/lending/compound.py +186 -0
  47. eth_portfolio/protocols/lending/liquity.py +108 -0
  48. eth_portfolio/protocols/lending/maker.py +110 -0
  49. eth_portfolio/protocols/lending/unit.py +44 -0
  50. eth_portfolio/protocols/liquity.py +17 -0
  51. eth_portfolio/py.typed +0 -0
  52. eth_portfolio/structs/__init__.py +43 -0
  53. eth_portfolio/structs/modified.py +69 -0
  54. eth_portfolio/structs/structs.py +626 -0
  55. eth_portfolio/typing/__init__.py +1418 -0
  56. eth_portfolio/typing/balance/single.py +176 -0
  57. eth_portfolio-0.5.7.dist-info/METADATA +26 -0
  58. eth_portfolio-0.5.7.dist-info/RECORD +83 -0
  59. eth_portfolio-0.5.7.dist-info/WHEEL +5 -0
  60. eth_portfolio-0.5.7.dist-info/entry_points.txt +2 -0
  61. eth_portfolio-0.5.7.dist-info/top_level.txt +3 -0
  62. eth_portfolio__mypyc.cp312-win32.pyd +0 -0
  63. eth_portfolio_scripts/__init__.py +17 -0
  64. eth_portfolio_scripts/_args.py +26 -0
  65. eth_portfolio_scripts/_logging.py +14 -0
  66. eth_portfolio_scripts/_portfolio.py +209 -0
  67. eth_portfolio_scripts/_utils.py +106 -0
  68. eth_portfolio_scripts/balances.cp312-win32.pyd +0 -0
  69. eth_portfolio_scripts/balances.py +56 -0
  70. eth_portfolio_scripts/docker/.grafana/dashboards/Portfolio/Balances.json +1962 -0
  71. eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml +10 -0
  72. eth_portfolio_scripts/docker/.grafana/datasources/datasources.yml +11 -0
  73. eth_portfolio_scripts/docker/__init__.cp312-win32.pyd +0 -0
  74. eth_portfolio_scripts/docker/__init__.py +16 -0
  75. eth_portfolio_scripts/docker/check.cp312-win32.pyd +0 -0
  76. eth_portfolio_scripts/docker/check.py +66 -0
  77. eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
  78. eth_portfolio_scripts/docker/docker_compose.cp312-win32.pyd +0 -0
  79. eth_portfolio_scripts/docker/docker_compose.py +97 -0
  80. eth_portfolio_scripts/main.py +118 -0
  81. eth_portfolio_scripts/py.typed +1 -0
  82. eth_portfolio_scripts/victoria/__init__.py +72 -0
  83. eth_portfolio_scripts/victoria/types.py +38 -0
@@ -0,0 +1,64 @@
1
+ import a_sync
2
+ from y.datatypes import Address, Block
3
+
4
+ from eth_portfolio._submodules import get_protocols, import_submodules
5
+ from eth_portfolio.protocols import lending
6
+ from eth_portfolio.protocols._base import StakingPoolABC
7
+ from eth_portfolio.typing import RemoteTokenBalances
8
+
9
+ import_submodules()
10
+
11
+ protocols: list[StakingPoolABC] = get_protocols() # type: ignore [assignment]
12
+
13
+
14
+ @a_sync.future
15
+ async def balances(address: Address, block: Block | None = None) -> RemoteTokenBalances:
16
+ """
17
+ Fetch token balances for a given address across various protocols.
18
+
19
+ This function retrieves the token balances for a specified Ethereum address
20
+ at a given block across all available protocols. It is decorated with
21
+ :func:`a_sync.future`, allowing it to be used in both synchronous and
22
+ asynchronous contexts.
23
+
24
+ If no protocols are available, the function returns an empty
25
+ :class:`~eth_portfolio.typing.RemoteTokenBalances` object.
26
+
27
+ Args:
28
+ address: The Ethereum address for which to fetch balances.
29
+ block: The block number at which to fetch balances.
30
+ If not provided, the latest block is used.
31
+
32
+ Examples:
33
+ Fetching balances asynchronously:
34
+
35
+ >>> from eth_portfolio.protocols import balances
36
+ >>> address = "0x1234567890abcdef1234567890abcdef12345678"
37
+ >>> block = 12345678
38
+ >>> remote_balances = await balances(address, block)
39
+ >>> print(remote_balances)
40
+
41
+ Fetching balances synchronously:
42
+
43
+ >>> remote_balances = balances(address, block)
44
+ >>> print(remote_balances)
45
+
46
+ The function constructs a dictionary `data` with protocol class names
47
+ as keys and their corresponding protocol balances as values. The `protocol_balances`
48
+ variable is a result of mapping the `balances` method over the `protocols` using
49
+ :func:`a_sync.map`. The asynchronous comprehension iterates over `protocol_balances`
50
+ to filter and construct the `data` dictionary. This dictionary is subsequently used
51
+ to initialize the :class:`~eth_portfolio.typing.RemoteTokenBalances` object.
52
+ """
53
+ if not protocols:
54
+ return RemoteTokenBalances(block=block)
55
+ protocol_balances = a_sync.map(
56
+ lambda protocol: protocol.balances(address, block),
57
+ protocols,
58
+ )
59
+ data = {
60
+ type(protocol).__name__: protocol_balances
61
+ async for protocol, protocol_balances in protocol_balances
62
+ if protocol_balances is not None
63
+ }
64
+ return RemoteTokenBalances(data, block=block)
@@ -0,0 +1,107 @@
1
+ import abc
2
+ from asyncio import gather
3
+
4
+ import a_sync
5
+ from a_sync import igather
6
+ from a_sync.a_sync import ASyncFunctionSyncDefault, HiddenMethod
7
+ from brownie.network.contract import ContractCall
8
+ from y import ERC20, Contract
9
+ from y._decorators import stuck_coro_debugger
10
+ from y.contracts import contract_creation_block
11
+ from y.datatypes import Address, Block
12
+
13
+ from eth_portfolio._utils import Decimal
14
+ from eth_portfolio.typing import Balance, TokenBalances
15
+
16
+
17
+ class ProtocolABC(metaclass=abc.ABCMeta):
18
+ @a_sync.future
19
+ async def balances(self, address: Address, block: Block | None = None) -> TokenBalances:
20
+ return await self._balances(address, block=block)
21
+
22
+ @abc.abstractmethod
23
+ async def _balances(self, address: Address, block: Block | None = None) -> TokenBalances: ...
24
+
25
+
26
+ class ProtocolWithStakingABC(ProtocolABC, metaclass=abc.ABCMeta):
27
+ pools: list["StakingPoolABC"]
28
+
29
+ @stuck_coro_debugger
30
+ async def _balances(self, address: Address, block: Block | None = None) -> TokenBalances:
31
+ return sum(await igather(pool.balances(address, block) for pool in self.pools)) # type: ignore
32
+
33
+
34
+ class StakingPoolABC(ProtocolABC, metaclass=abc.ABCMeta):
35
+ contract_address: Address
36
+ """
37
+ The address of the staking pool.
38
+ """
39
+
40
+ balance_method_name: str
41
+ """
42
+ The name of the method that is used to query a staker's balance in the staking pool.
43
+ """
44
+
45
+ @a_sync.future
46
+ async def __call__(self, *args, block: Block | None = None, **_) -> int:
47
+ if _:
48
+ raise ValueError(
49
+ "SingleTokenStakingPoolABC.__call__ does not support keyword arguments"
50
+ )
51
+ return await self.contract_call.coroutine(*args, block_identifier=block)
52
+
53
+ @property
54
+ def contract(self) -> Contract:
55
+ return Contract(self.contract_address)
56
+
57
+ @property
58
+ def contract_call(self) -> ContractCall:
59
+ return getattr(self.contract, self.balance_method_name)
60
+
61
+ @property
62
+ def deploy_block(self) -> Block:
63
+ return contract_creation_block(self.contract_address)
64
+
65
+ def should_check(self, block: Block | None) -> bool:
66
+ return block is None or block >= self.deploy_block
67
+
68
+
69
+ class SingleTokenStakingPoolABC(StakingPoolABC, metaclass=abc.ABCMeta):
70
+ """
71
+ Works for any contract that has a view method with the following signature:
72
+
73
+ ```
74
+ methodName(address) -> uint256
75
+ ```
76
+
77
+ In the above example:
78
+ - ``address`` is the user's address, provided at runtime.
79
+ - ``methodName`` is whatever string you set for the staking pool's ``balance_method_name``.
80
+ - ``uint256`` is the user's ``token`` balance held in ``contract_address`` at the queried block.
81
+ """
82
+
83
+ token: ERC20
84
+ """
85
+ The token that is used for staking in this pool.
86
+ """
87
+
88
+ @property
89
+ def price(self) -> ASyncFunctionSyncDefault[Block, Decimal]:
90
+ return self.token.price
91
+
92
+ @property
93
+ def scale(self) -> HiddenMethod[ERC20, int]:
94
+ return self.token.__scale__
95
+
96
+ @stuck_coro_debugger
97
+ async def _balances(self, address: Address, block: Block | None = None) -> TokenBalances:
98
+ balances: TokenBalances = TokenBalances(block=block)
99
+ if self.should_check(block):
100
+ balance = Decimal(await self(address, block=block)) # type: ignore
101
+ if balance:
102
+ scale, price = await gather(self.scale, self.price(block, sync=False))
103
+ balance /= scale # type: ignore
104
+ balances[self.token.address] = Balance(
105
+ balance, balance * Decimal(price), token=self.token.address, block=block
106
+ )
107
+ return balances
@@ -0,0 +1,17 @@
1
+ from y import ERC20, Network
2
+
3
+ from eth_portfolio.protocols._base import ProtocolWithStakingABC, SingleTokenStakingPoolABC
4
+
5
+
6
+ class _CvxLockerV2(SingleTokenStakingPoolABC):
7
+ contract_address = "0x72a19342e8F1838460eBFCCEf09F6585e32db86E"
8
+ balance_method_name = "lockedBalanceOf"
9
+ token = ERC20("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B")
10
+
11
+
12
+ class Convex(ProtocolWithStakingABC):
13
+ networks = [Network.Mainnet]
14
+
15
+ def __init__(self) -> None:
16
+ super().__init__()
17
+ self.pools = [_CvxLockerV2()]
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ from typing import ClassVar, Final, final
3
+
4
+ import y
5
+ from y.datatypes import Address, Block
6
+
7
+ from eth_portfolio import _decimal
8
+ from eth_portfolio.protocols._base import ProtocolABC
9
+ from eth_portfolio.typing import Balance, TokenBalances
10
+
11
+ gather: Final = asyncio.gather
12
+
13
+ Contract: Final = y.Contract
14
+ Network: Final = y.Network
15
+ contract_creation_block: Final = y.contract_creation_block
16
+ dai: Final = y.dai
17
+
18
+ Decimal: Final = _decimal.Decimal
19
+
20
+
21
+ @final
22
+ class MakerDSR(ProtocolABC):
23
+ networks: ClassVar = [Network.Mainnet]
24
+
25
+ def __init__(self) -> None:
26
+ dsr_manager = "0x373238337Bfe1146fb49989fc222523f83081dDb"
27
+ pot = "0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7"
28
+ self.dsr_manager: Final = Contract(dsr_manager)
29
+ self.pot: Final = Contract(pot)
30
+ self._start_block: Final = max(
31
+ contract_creation_block(dsr_manager), contract_creation_block(pot)
32
+ )
33
+ self._get_chi: Final = self.pot.chi.coroutine
34
+ self._get_pie: Final = self.dsr_manager.pieOf.coroutine
35
+
36
+ async def _balances(self, address: Address, block: Block | None = None) -> TokenBalances:
37
+ balances = TokenBalances(block=block)
38
+ if block and block < self._start_block:
39
+ return balances
40
+ pie, exchange_rate = await gather(
41
+ self._get_pie(address, block_identifier=block),
42
+ self._exchange_rate(block),
43
+ )
44
+ if pie:
45
+ dai_in_dsr = pie * exchange_rate / 10**18
46
+ balances[dai] = Balance(dai_in_dsr, dai_in_dsr, token=dai, block=block)
47
+ return balances
48
+
49
+ async def _exchange_rate(self, block: Block | None = None) -> Decimal:
50
+ return Decimal(await self._get_chi(block_identifier=block)) / 10**27
@@ -0,0 +1,6 @@
1
+ This module contains the base classes for lending protocols in the _base.py file, and all lending protocols supported by eth-portfolio.
2
+
3
+ If you want to add support for a new lending protocol, there are only a few steps:
4
+ 1. Create a new python file, named appropriately.
5
+ 2. Subclass one of the 2 classes in _base.py and define the methods specified in the parent definition with the custom logic for the protocol you wish to add.
6
+ 3. That's it, eth-portfolio will automagically handle the rest!
@@ -0,0 +1,50 @@
1
+ from typing import List, Optional, Union
2
+
3
+ import a_sync
4
+ from y._decorators import stuck_coro_debugger
5
+ from y.datatypes import Address, Block
6
+
7
+ from eth_portfolio._submodules import get_protocols, import_submodules
8
+ from eth_portfolio.protocols.lending._base import (
9
+ LendingProtocol,
10
+ LendingProtocolWithLockedCollateral,
11
+ )
12
+ from eth_portfolio.typing import RemoteTokenBalances
13
+
14
+ import_submodules()
15
+
16
+ protocols: list[LendingProtocol | LendingProtocolWithLockedCollateral] = get_protocols() # type: ignore [assignment]
17
+
18
+ has_collateral = lambda protocol: isinstance(protocol, LendingProtocolWithLockedCollateral)
19
+
20
+
21
+ @a_sync.future
22
+ @stuck_coro_debugger
23
+ async def collateral(address: Address, block: Block | None = None) -> RemoteTokenBalances:
24
+ protocol_balances = a_sync.map(
25
+ lambda protocol: protocol.balances(address, block),
26
+ filter(has_collateral, protocols),
27
+ )
28
+ data = {
29
+ type(protocol).__name__: token_balances
30
+ async for protocol, token_balances in protocol_balances
31
+ if token_balances is not None
32
+ }
33
+ return RemoteTokenBalances(data, block=block)
34
+
35
+
36
+ @a_sync.future
37
+ @stuck_coro_debugger
38
+ async def debt(address: Address, block: Block | None = None) -> RemoteTokenBalances:
39
+ if not protocols:
40
+ return RemoteTokenBalances(block=block)
41
+ protocol_debts = a_sync.map(
42
+ lambda protocol: protocol.debt(address, block),
43
+ protocols,
44
+ )
45
+ data = {
46
+ type(protocol).__name__: token_balances
47
+ async for protocol, token_balances in protocol_debts
48
+ if token_balances is not None
49
+ }
50
+ return RemoteTokenBalances(data, block=block)
@@ -0,0 +1,56 @@
1
+ import abc
2
+
3
+ import a_sync
4
+ from y.datatypes import Address, Block
5
+
6
+ from eth_portfolio.protocols._base import ProtocolABC
7
+ from eth_portfolio.typing import TokenBalances
8
+
9
+
10
+ class LendingProtocol(metaclass=abc.ABCMeta):
11
+ """
12
+ Subclass this class for any protocol that maintains a debt balance for a user but doesn't maintain collateral internally.
13
+ Example: Aave, because the user holds on to their collateral in the form of ERC-20 aTokens.
14
+
15
+ You must define the following async method:
16
+ `_debt(self, address: Address, block: Optional[Block] = None)`
17
+
18
+ Example:
19
+ >>> class AaveProtocol(LendingProtocol):
20
+ ... async def _debt(self, address: Address, block: Optional[Block] = None) -> TokenBalances:
21
+ ... # Implementation for fetching debt from Aave
22
+ ... pass
23
+
24
+ See Also:
25
+ - :class:`LendingProtocolWithLockedCollateral`
26
+ """
27
+
28
+ @a_sync.future
29
+ async def debt(self, address: Address, block: Block | None = None) -> TokenBalances:
30
+ return await self._debt(address, block) # type: ignore
31
+
32
+ @abc.abstractmethod
33
+ async def _debt(self, address: Address, block: Block | None = None) -> TokenBalances: ...
34
+
35
+
36
+ class LendingProtocolWithLockedCollateral(LendingProtocol, ProtocolABC):
37
+ """
38
+ Subclass this class for any protocol that maintains a debt balance for a user AND holds collateral internally.
39
+ Example: Maker, because collateral is locked up inside of Maker's smart contracts.
40
+
41
+ You must define the following async methods:
42
+ - `_debt(self, address: Address, block: Optional[Block] = None)`
43
+ - `_balances(self, address: Address, block: Optional[Block] = None)`
44
+
45
+ Example:
46
+ >>> class MakerProtocol(LendingProtocolWithLockedCollateral):
47
+ ... async def _debt(self, address: Address, block: Optional[Block] = None) -> TokenBalances:
48
+ ... # Implementation for fetching debt from Maker
49
+ ... pass
50
+ ... async def _balances(self, address: Address, block: Optional[Block] = None) -> TokenBalances:
51
+ ... # Implementation for fetching balances from Maker
52
+ ... pass
53
+
54
+ See Also:
55
+ - :class:`LendingProtocol`
56
+ """
@@ -0,0 +1,186 @@
1
+ from asyncio import gather
2
+
3
+ import a_sync
4
+ from a_sync import igather
5
+ from async_lru import alru_cache
6
+ from brownie import ZERO_ADDRESS, Contract
7
+ from eth_typing import ChecksumAddress
8
+ from y import ERC20, Contract, map_prices, weth
9
+ from y._decorators import stuck_coro_debugger
10
+ from y.datatypes import Block
11
+ from y.exceptions import ContractNotVerified
12
+ from y.prices.lending.compound import CToken, compound
13
+
14
+ from eth_portfolio._utils import Decimal
15
+ from eth_portfolio.protocols.lending._base import LendingProtocol
16
+ from eth_portfolio.typing import Balance, TokenBalances
17
+
18
+
19
+ def _get_contract(market: CToken) -> Contract | None:
20
+ try:
21
+ return market.contract
22
+ except ContractNotVerified:
23
+ # We will skip these for now. Might consider supporting them later if necessary.
24
+ return None
25
+
26
+
27
+ class Compound(LendingProtocol):
28
+ _markets: list[Contract]
29
+
30
+ @a_sync.future
31
+ @alru_cache(ttl=300)
32
+ @stuck_coro_debugger
33
+ async def underlyings(self) -> list[ERC20]:
34
+ """
35
+ Fetches the underlying ERC20 tokens for all Compound markets.
36
+
37
+ This method gathers all markets from the Compound protocol's trollers
38
+ and filters out those that do not have a `borrowBalanceStored` attribute
39
+ by using the :func:`hasattr` function directly on the result of
40
+ :func:`_get_contract`. It then separates markets into those that use
41
+ the native gas token and those that have an underlying ERC20 token,
42
+ fetching the underlying tokens accordingly.
43
+
44
+ Returns:
45
+ A list of :class:`~y.classes.common.ERC20` instances representing the underlying tokens.
46
+
47
+ Examples:
48
+ >>> compound = Compound()
49
+ >>> underlyings = await compound.underlyings()
50
+ >>> for token in underlyings:
51
+ ... print(token.symbol)
52
+
53
+ See Also:
54
+ - :meth:`markets`: To get the list of market contracts.
55
+ """
56
+ all_markets: list[list[CToken]] = await igather(
57
+ comp.markets for comp in compound.trollers.values()
58
+ )
59
+ markets: list[Contract] = [
60
+ market.contract
61
+ for troller in all_markets
62
+ for market in troller
63
+ if hasattr(_get_contract(market), "borrowBalanceStored")
64
+ ] # this last part takes out xinv
65
+ gas_token_markets = [market for market in markets if not hasattr(market, "underlying")]
66
+ other_markets = [market for market in markets if hasattr(market, "underlying")]
67
+
68
+ markets = gas_token_markets + other_markets
69
+ underlyings = [weth for market in gas_token_markets] + await igather(
70
+ market.underlying for market in other_markets
71
+ )
72
+
73
+ markets_zip = zip(markets, underlyings)
74
+ self._markets, underlyings = [], []
75
+ for contract, underlying in markets_zip:
76
+ if underlying != ZERO_ADDRESS:
77
+ self._markets.append(contract)
78
+ underlyings.append(underlying)
79
+ return [ERC20(underlying, asynchronous=True) for underlying in underlyings]
80
+
81
+ @a_sync.future
82
+ @stuck_coro_debugger
83
+ async def markets(self) -> list[Contract]:
84
+ """
85
+ Fetches the list of market contracts for the Compound protocol.
86
+
87
+ This method ensures that the underlying tokens are fetched first,
88
+ as they are used to determine the markets.
89
+
90
+ Returns:
91
+ A list of :class:`~brownie.network.contract.Contract` instances representing the markets.
92
+
93
+ Examples:
94
+ >>> compound = Compound()
95
+ >>> markets = await compound.markets()
96
+ >>> for market in markets:
97
+ ... print(market.address)
98
+
99
+ See Also:
100
+ - :meth:`underlyings`: To get the list of underlying tokens.
101
+ """
102
+ await self.underlyings()
103
+ return self._markets
104
+
105
+ async def _debt(self, address: ChecksumAddress, block: Block | None = None) -> TokenBalances:
106
+ """
107
+ Calculates the debt balance for a given address in the Compound protocol.
108
+
109
+ This method fetches the borrow balance for each market and calculates
110
+ the debt in terms of the underlying token and its USD value.
111
+
112
+ Args:
113
+ address: The Ethereum address to calculate the debt for.
114
+ block: The block number to query. Defaults to the latest block.
115
+
116
+ Returns:
117
+ A :class:`~eth_portfolio.typing.TokenBalances` object representing the debt balances.
118
+
119
+ Examples:
120
+ >>> compound = Compound()
121
+ >>> debt_balances = await compound._debt("0x1234567890abcdef1234567890abcdef12345678")
122
+ >>> for token, balance in debt_balances.items():
123
+ ... print(f"Token: {token}, Balance: {balance.balance}, USD Value: {balance.usd_value}")
124
+
125
+ See Also:
126
+ - :meth:`debt`: Public method to get the debt balances.
127
+ """
128
+ # if ypricemagic doesn't support any Compound forks on current chain
129
+ if len(compound.trollers) == 0:
130
+ return TokenBalances(block=block)
131
+
132
+ address = str(address)
133
+ markets: list[Contract]
134
+ underlyings: list[ERC20]
135
+ markets, underlyings = await gather(self.markets(), self.underlyings())
136
+ debt_data, underlying_scale = await gather(
137
+ igather(_borrow_balance_stored(market, address, block) for market in markets),
138
+ igather(underlying.__scale__ for underlying in underlyings),
139
+ )
140
+
141
+ balances: TokenBalances = TokenBalances(block=block)
142
+ if debts := {
143
+ underlying: Decimal(debt) / scale
144
+ for underlying, scale, debt in zip(underlyings, underlying_scale, debt_data)
145
+ if debt
146
+ }:
147
+ async for underlying, price in map_prices(debts, block=block):
148
+ debt = debts.pop(underlying)
149
+ balances[underlying] += Balance(
150
+ debt, debt * Decimal(price), token=underlying.address, block=block
151
+ )
152
+ return balances
153
+
154
+
155
+ @stuck_coro_debugger
156
+ async def _borrow_balance_stored(
157
+ market: Contract, address: ChecksumAddress, block: Block | None = None
158
+ ) -> int | None:
159
+ """
160
+ Fetches the stored borrow balance for a given market and address.
161
+
162
+ This function attempts to call the `borrowBalanceStored` method on the
163
+ market contract. If the call reverts, it returns None.
164
+
165
+ Args:
166
+ market: The market contract to query.
167
+ address: The Ethereum address to fetch the borrow balance for.
168
+ block: The block number to query. Defaults to the latest block.
169
+
170
+ Returns:
171
+ The stored borrow balance as an integer, or None if the call reverts.
172
+
173
+ Examples:
174
+ >>> market = Contract.from_explorer("0x1234567890abcdef1234567890abcdef12345678")
175
+ >>> balance = await _borrow_balance_stored(market, "0xabcdefabcdefabcdefabcdefabcdefabcdef")
176
+ >>> print(balance)
177
+
178
+ See Also:
179
+ - :meth:`_debt`: Uses this function to calculate debt balances.
180
+ """
181
+ try:
182
+ return await market.borrowBalanceStored.coroutine(str(address), block_identifier=block)
183
+ except ValueError as e:
184
+ if str(e) != "No data was returned - the call likely reverted":
185
+ raise
186
+ return None
@@ -0,0 +1,108 @@
1
+ from faster_async_lru import alru_cache
2
+ from y import Contract, Network, get_price
3
+ from y._decorators import stuck_coro_debugger
4
+ from y.constants import EEE_ADDRESS
5
+ from y.datatypes import Address, Block
6
+
7
+ from eth_portfolio.protocols.lending._base import LendingProtocolWithLockedCollateral
8
+ from eth_portfolio.typing import Balance, TokenBalances
9
+
10
+ lusd = "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0"
11
+
12
+
13
+ class Liquity(LendingProtocolWithLockedCollateral):
14
+ """
15
+ Represents the Liquity protocol, a decentralized borrowing protocol that allows users to draw loans against Ether collateral.
16
+
17
+ This class is a subclass of :class:`~eth_portfolio.protocols.lending._base.LendingProtocolWithLockedCollateral`, which means it maintains a debt balance for a user and holds collateral internally.
18
+
19
+ Examples:
20
+ >>> liquity = Liquity()
21
+ >>> balances = await liquity.balances("0xYourAddress", 12345678)
22
+ >>> print(balances)
23
+
24
+ See Also:
25
+ - :class:`~eth_portfolio.protocols.lending._base.LendingProtocolWithLockedCollateral`
26
+ - :class:`~eth_portfolio.typing.TokenBalances`
27
+ """
28
+
29
+ networks = [Network.Mainnet]
30
+ """The networks on which the protocol is available."""
31
+
32
+ def __init__(self) -> None:
33
+ self.troveManager = Contract("0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2")
34
+ """The contract instance for the Trove Manager."""
35
+ self.start_block = 12178557
36
+ """The block number from which the protocol starts."""
37
+
38
+ @alru_cache(maxsize=128)
39
+ @stuck_coro_debugger
40
+ async def get_trove(self, address: Address, block: Block) -> dict:
41
+ """
42
+ Retrieves the trove data for a given address at a specific block.
43
+
44
+ Args:
45
+ address: The Ethereum address of the user.
46
+ block: The block number to query.
47
+
48
+ Examples:
49
+ >>> trove_data = await liquity.get_trove("0xYourAddress", 12345678)
50
+ >>> print(trove_data)
51
+ """
52
+ return await self.troveManager.Troves.coroutine(address, block_identifier=block)
53
+
54
+ @stuck_coro_debugger
55
+ async def _balances(self, address: Address, block: Block | None = None) -> TokenBalances:
56
+ """
57
+ Retrieves the collateral balances for a given address at a specific block.
58
+
59
+ Args:
60
+ address: The Ethereum address of the user.
61
+ block: The block number to query.
62
+
63
+ Examples:
64
+ >>> balances = await liquity._balances("0xYourAddress", 12345678)
65
+ >>> print(balances)
66
+
67
+ See Also:
68
+ - :class:`~eth_portfolio.typing.TokenBalances`
69
+ """
70
+ balances: TokenBalances = TokenBalances(block=block)
71
+ if block and block < self.start_block:
72
+ return balances
73
+ data = await self.get_trove(address, block)
74
+ eth_collateral_balance = data[1]
75
+ if eth_collateral_balance:
76
+ eth_collateral_balance /= 10**18
77
+ value = eth_collateral_balance * await get_price(EEE_ADDRESS, block, sync=False)
78
+ balances[EEE_ADDRESS] = Balance(
79
+ eth_collateral_balance, value, token=EEE_ADDRESS, block=block
80
+ )
81
+ return balances
82
+
83
+ @stuck_coro_debugger
84
+ async def _debt(self, address: Address, block: Block | None = None) -> TokenBalances:
85
+ """
86
+ Retrieves the debt balances for a given address at a specific block.
87
+
88
+ Args:
89
+ address: The Ethereum address of the user.
90
+ block: The block number to query.
91
+
92
+ Examples:
93
+ >>> debt_balances = await liquity._debt("0xYourAddress", 12345678)
94
+ >>> print(debt_balances)
95
+
96
+ See Also:
97
+ - :class:`~eth_portfolio.typing.TokenBalances`
98
+ """
99
+ balances: TokenBalances = TokenBalances(block=block)
100
+ if block and block < self.start_block:
101
+ return balances
102
+ data = await self.get_trove(address, block)
103
+ lusd_debt = data[0]
104
+ if lusd_debt:
105
+ lusd_debt /= 10**18
106
+ value = lusd_debt * await get_price(lusd, block, sync=False)
107
+ balances[lusd] = Balance(lusd_debt, value, token=lusd, block=block)
108
+ return balances