eth-portfolio-temp 0.3.0__cp313-cp313-win_amd64.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-temp might be problematic. Click here for more details.

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