eth-portfolio 1.1.0__py3-none-any.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 (47) hide show
  1. eth_portfolio/__init__.py +16 -0
  2. eth_portfolio/_argspec.py +42 -0
  3. eth_portfolio/_cache.py +116 -0
  4. eth_portfolio/_config.py +3 -0
  5. eth_portfolio/_db/__init__.py +0 -0
  6. eth_portfolio/_db/decorators.py +147 -0
  7. eth_portfolio/_db/entities.py +204 -0
  8. eth_portfolio/_db/utils.py +595 -0
  9. eth_portfolio/_decimal.py +122 -0
  10. eth_portfolio/_decorators.py +71 -0
  11. eth_portfolio/_exceptions.py +67 -0
  12. eth_portfolio/_ledgers/__init__.py +0 -0
  13. eth_portfolio/_ledgers/address.py +892 -0
  14. eth_portfolio/_ledgers/portfolio.py +327 -0
  15. eth_portfolio/_loaders/__init__.py +33 -0
  16. eth_portfolio/_loaders/balances.py +78 -0
  17. eth_portfolio/_loaders/token_transfer.py +214 -0
  18. eth_portfolio/_loaders/transaction.py +379 -0
  19. eth_portfolio/_loaders/utils.py +59 -0
  20. eth_portfolio/_shitcoins.py +212 -0
  21. eth_portfolio/_utils.py +286 -0
  22. eth_portfolio/_ydb/__init__.py +0 -0
  23. eth_portfolio/_ydb/token_transfers.py +136 -0
  24. eth_portfolio/address.py +382 -0
  25. eth_portfolio/buckets.py +181 -0
  26. eth_portfolio/constants.py +58 -0
  27. eth_portfolio/portfolio.py +629 -0
  28. eth_portfolio/protocols/__init__.py +66 -0
  29. eth_portfolio/protocols/_base.py +107 -0
  30. eth_portfolio/protocols/convex.py +17 -0
  31. eth_portfolio/protocols/dsr.py +31 -0
  32. eth_portfolio/protocols/lending/__init__.py +49 -0
  33. eth_portfolio/protocols/lending/_base.py +57 -0
  34. eth_portfolio/protocols/lending/compound.py +185 -0
  35. eth_portfolio/protocols/lending/liquity.py +110 -0
  36. eth_portfolio/protocols/lending/maker.py +105 -0
  37. eth_portfolio/protocols/lending/unit.py +47 -0
  38. eth_portfolio/protocols/liquity.py +16 -0
  39. eth_portfolio/py.typed +0 -0
  40. eth_portfolio/structs/__init__.py +43 -0
  41. eth_portfolio/structs/modified.py +69 -0
  42. eth_portfolio/structs/structs.py +637 -0
  43. eth_portfolio/typing.py +1460 -0
  44. eth_portfolio-1.1.0.dist-info/METADATA +174 -0
  45. eth_portfolio-1.1.0.dist-info/RECORD +47 -0
  46. eth_portfolio-1.1.0.dist-info/WHEEL +5 -0
  47. eth_portfolio-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,107 @@
1
+ import abc
2
+ from asyncio import gather
3
+ from typing import List, Optional
4
+
5
+ import a_sync
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: Optional[Block] = None) -> TokenBalances:
20
+ return await self._balances(address, block=block)
21
+
22
+ @abc.abstractmethod
23
+ async def _balances(self, address: Address, block: Optional[Block] = 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: Optional[Block] = None) -> TokenBalances:
31
+ return sum(await gather(*[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: Optional[Block] = 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: Optional[Block]) -> 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: Optional[Block] = 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,31 @@
1
+ from asyncio import gather
2
+ from typing import Optional
3
+
4
+ from y import Contract, Network, dai
5
+ from y.datatypes import Address, Block
6
+
7
+ from eth_portfolio._decimal import Decimal
8
+ from eth_portfolio.protocols._base import ProtocolABC
9
+ from eth_portfolio.typing import Balance, TokenBalances
10
+
11
+
12
+ class MakerDSR(ProtocolABC):
13
+ networks = [Network.Mainnet]
14
+
15
+ def __init__(self) -> None:
16
+ self.dsr_manager = Contract("0x373238337Bfe1146fb49989fc222523f83081dDb")
17
+ self.pot = Contract("0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7")
18
+
19
+ async def _balances(self, address: Address, block: Optional[Block] = None) -> TokenBalances:
20
+ balances = TokenBalances(block=block)
21
+ pie, exchange_rate = await gather(
22
+ self.dsr_manager.pieOf.coroutine(address, block_identifier=block),
23
+ self._exchange_rate(block),
24
+ )
25
+ if pie:
26
+ dai_in_dsr = pie * exchange_rate / 10**18
27
+ balances[dai] = Balance(dai_in_dsr, dai_in_dsr, token=dai, block=block)
28
+ return balances
29
+
30
+ async def _exchange_rate(self, block: Optional[Block] = None) -> Decimal:
31
+ return Decimal(await self.pot.chi.coroutine(block_identifier=block)) / 10**27
@@ -0,0 +1,49 @@
1
+ from typing import List, Optional, Union
2
+
3
+ import a_sync
4
+ from eth_portfolio._utils import _get_protocols_for_submodule, _import_submodules
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
+ _import_submodules()
14
+
15
+ protocols: List[Union[LendingProtocol, LendingProtocolWithLockedCollateral]] = _get_protocols_for_submodule() # type: ignore [assignment]
16
+
17
+ has_collateral = lambda protocol: isinstance(protocol, LendingProtocolWithLockedCollateral)
18
+
19
+
20
+ @a_sync.future
21
+ @stuck_coro_debugger
22
+ async def collateral(address: Address, block: Optional[Block] = None) -> RemoteTokenBalances:
23
+ protocol_balances = a_sync.map(
24
+ lambda protocol: protocol.balances(address, block),
25
+ filter(has_collateral, protocols),
26
+ )
27
+ data = {
28
+ type(protocol).__name__: token_balances
29
+ async for protocol, token_balances in protocol_balances
30
+ if token_balances is not None
31
+ }
32
+ return RemoteTokenBalances(data, block=block)
33
+
34
+
35
+ @a_sync.future
36
+ @stuck_coro_debugger
37
+ async def debt(address: Address, block: Optional[Block] = None) -> RemoteTokenBalances:
38
+ if not protocols:
39
+ return RemoteTokenBalances(block=block)
40
+ protocol_debts = a_sync.map(
41
+ lambda protocol: protocol.debt(address, block),
42
+ protocols,
43
+ )
44
+ data = {
45
+ type(protocol).__name__: token_balances
46
+ async for protocol, token_balances in protocol_debts
47
+ if token_balances is not None
48
+ }
49
+ 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,185 @@
1
+ from asyncio import gather
2
+ from typing import List, Optional
3
+
4
+ import a_sync
5
+ from async_lru import alru_cache
6
+ from brownie import ZERO_ADDRESS, Contract
7
+ from y import ERC20, Contract, map_prices, weth
8
+ from y._decorators import stuck_coro_debugger
9
+ from y.datatypes import Block
10
+ from y.exceptions import ContractNotVerified
11
+ from y.prices.lending.compound import CToken, compound
12
+
13
+ from eth_portfolio._utils import Decimal
14
+ from eth_portfolio.protocols.lending._base import LendingProtocol
15
+ from eth_portfolio.typing import Address, Balance, TokenBalances
16
+
17
+
18
+ def _get_contract(market: CToken) -> Optional[Contract]:
19
+ try:
20
+ return market.contract
21
+ except ContractNotVerified:
22
+ # We will skip these for now. Might consider supporting them later if necessary.
23
+ return None
24
+
25
+
26
+ class Compound(LendingProtocol):
27
+ _markets: List[Contract]
28
+
29
+ @a_sync.future
30
+ @alru_cache(ttl=300)
31
+ @stuck_coro_debugger
32
+ async def underlyings(self) -> List[ERC20]:
33
+ """
34
+ Fetches the underlying ERC20 tokens for all Compound markets.
35
+
36
+ This method gathers all markets from the Compound protocol's trollers
37
+ and filters out those that do not have a `borrowBalanceStored` attribute
38
+ by using the :func:`hasattr` function directly on the result of
39
+ :func:`_get_contract`. It then separates markets into those that use
40
+ the native gas token and those that have an underlying ERC20 token,
41
+ fetching the underlying tokens accordingly.
42
+
43
+ Returns:
44
+ A list of :class:`~y.classes.common.ERC20` instances representing the underlying tokens.
45
+
46
+ Examples:
47
+ >>> compound = Compound()
48
+ >>> underlyings = await compound.underlyings()
49
+ >>> for token in underlyings:
50
+ ... print(token.symbol)
51
+
52
+ See Also:
53
+ - :meth:`markets`: To get the list of market contracts.
54
+ """
55
+ all_markets: List[List[CToken]] = await gather(
56
+ *[comp.markets for comp in compound.trollers.values()]
57
+ )
58
+ markets: List[Contract] = [
59
+ market.contract
60
+ for troller in all_markets
61
+ for market in troller
62
+ if hasattr(_get_contract(market), "borrowBalanceStored")
63
+ ] # this last part takes out xinv
64
+ gas_token_markets = [market for market in markets if not hasattr(market, "underlying")]
65
+ other_markets = [market for market in markets if hasattr(market, "underlying")]
66
+
67
+ markets = gas_token_markets + other_markets
68
+ underlyings = [weth for market in gas_token_markets] + await gather(
69
+ *[market.underlying.coroutine() for market in other_markets]
70
+ )
71
+
72
+ markets_zip = zip(markets, underlyings)
73
+ self._markets, underlyings = [], []
74
+ for contract, underlying in markets_zip:
75
+ if underlying != ZERO_ADDRESS:
76
+ self._markets.append(contract)
77
+ underlyings.append(underlying)
78
+ return [ERC20(underlying, asynchronous=True) for underlying in underlyings]
79
+
80
+ @a_sync.future
81
+ @stuck_coro_debugger
82
+ async def markets(self) -> List[Contract]:
83
+ """
84
+ Fetches the list of market contracts for the Compound protocol.
85
+
86
+ This method ensures that the underlying tokens are fetched first,
87
+ as they are used to determine the markets.
88
+
89
+ Returns:
90
+ A list of :class:`~brownie.network.contract.Contract` instances representing the markets.
91
+
92
+ Examples:
93
+ >>> compound = Compound()
94
+ >>> markets = await compound.markets()
95
+ >>> for market in markets:
96
+ ... print(market.address)
97
+
98
+ See Also:
99
+ - :meth:`underlyings`: To get the list of underlying tokens.
100
+ """
101
+ await self.underlyings()
102
+ return self._markets
103
+
104
+ async def _debt(self, address: Address, block: Optional[Block] = None) -> TokenBalances:
105
+ """
106
+ Calculates the debt balance for a given address in the Compound protocol.
107
+
108
+ This method fetches the borrow balance for each market and calculates
109
+ the debt in terms of the underlying token and its USD value.
110
+
111
+ Args:
112
+ address: The Ethereum address to calculate the debt for.
113
+ block: The block number to query. Defaults to the latest block.
114
+
115
+ Returns:
116
+ A :class:`~eth_portfolio.typing.TokenBalances` object representing the debt balances.
117
+
118
+ Examples:
119
+ >>> compound = Compound()
120
+ >>> debt_balances = await compound._debt("0x1234567890abcdef1234567890abcdef12345678")
121
+ >>> for token, balance in debt_balances.items():
122
+ ... print(f"Token: {token}, Balance: {balance.balance}, USD Value: {balance.usd_value}")
123
+
124
+ See Also:
125
+ - :meth:`debt`: Public method to get the debt balances.
126
+ """
127
+ # if ypricemagic doesn't support any Compound forks on current chain
128
+ if len(compound.trollers) == 0:
129
+ return TokenBalances(block=block)
130
+
131
+ address = str(address)
132
+ markets: List[Contract]
133
+ underlyings: List[ERC20]
134
+ markets, underlyings = await gather(*[self.markets(), self.underlyings()])
135
+ debt_data, underlying_scale = await gather(
136
+ gather(*[_borrow_balance_stored(market, address, block) for market in markets]),
137
+ gather(*[underlying.__scale__ for underlying in underlyings]),
138
+ )
139
+
140
+ balances: TokenBalances = TokenBalances(block=block)
141
+ if debts := {
142
+ underlying: Decimal(debt) / scale
143
+ for underlying, scale, debt in zip(underlyings, underlying_scale, debt_data)
144
+ if debt
145
+ }:
146
+ async for underlying, price in map_prices(debts, block=block):
147
+ debt = debts.pop(underlying)
148
+ balances[underlying] += Balance(
149
+ debt, debt * Decimal(price), token=underlying.address, block=block
150
+ )
151
+ return balances
152
+
153
+
154
+ @stuck_coro_debugger
155
+ async def _borrow_balance_stored(
156
+ market: Contract, address: Address, block: Optional[Block] = None
157
+ ) -> Optional[int]:
158
+ """
159
+ Fetches the stored borrow balance for a given market and address.
160
+
161
+ This function attempts to call the `borrowBalanceStored` method on the
162
+ market contract. If the call reverts, it returns None.
163
+
164
+ Args:
165
+ market: The market contract to query.
166
+ address: The Ethereum address to fetch the borrow balance for.
167
+ block: The block number to query. Defaults to the latest block.
168
+
169
+ Returns:
170
+ The stored borrow balance as an integer, or None if the call reverts.
171
+
172
+ Examples:
173
+ >>> market = Contract.from_explorer("0x1234567890abcdef1234567890abcdef12345678")
174
+ >>> balance = await _borrow_balance_stored(market, "0xabcdefabcdefabcdefabcdefabcdefabcdef")
175
+ >>> print(balance)
176
+
177
+ See Also:
178
+ - :meth:`_debt`: Uses this function to calculate debt balances.
179
+ """
180
+ try:
181
+ return await market.borrowBalanceStored.coroutine(str(address), block_identifier=block)
182
+ except ValueError as e:
183
+ if str(e) != "No data was returned - the call likely reverted":
184
+ raise
185
+ return None
@@ -0,0 +1,110 @@
1
+ from typing import Optional
2
+
3
+ from 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
@@ -0,0 +1,105 @@
1
+ from asyncio import gather
2
+ from typing import List, Optional
3
+
4
+ from async_lru import alru_cache
5
+ from dank_mids.exceptions import Revert
6
+ from eth_typing import HexStr
7
+ from y import Contract, Network, contract_creation_block_async, get_price
8
+ from y._decorators import stuck_coro_debugger
9
+ from y.constants import dai
10
+ from y.datatypes import Address, Block
11
+
12
+ from eth_portfolio._utils import Decimal
13
+ from eth_portfolio.protocols.lending._base import LendingProtocolWithLockedCollateral
14
+ from eth_portfolio.typing import Balance, TokenBalances
15
+
16
+ try:
17
+ # this is only available in 4.0.0+
18
+ from eth_abi import encode
19
+
20
+ encode_bytes = lambda bytestring: encode(["bytes32"], [bytestring])
21
+ except ImportError:
22
+ from eth_abi import encode_single
23
+
24
+ encode_bytes = lambda bytestring: encode_single("bytes32", bytestring)
25
+
26
+ yfi = "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e"
27
+
28
+
29
+ class Maker(LendingProtocolWithLockedCollateral):
30
+ networks = [Network.Mainnet]
31
+
32
+ def __init__(self) -> None:
33
+ self.proxy_registry = Contract("0x4678f0a6958e4D2Bc4F1BAF7Bc52E8F3564f3fE4")
34
+ self.cdp_manager = Contract("0x5ef30b9986345249bc32d8928B7ee64DE9435E39")
35
+ self.ilk_registry = Contract("0x5a464C28D19848f44199D003BeF5ecc87d090F87")
36
+ self.vat = Contract("0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B")
37
+
38
+ @stuck_coro_debugger
39
+ async def _balances(self, address: Address, block: Optional[Block] = None) -> TokenBalances:
40
+ ilks, urn = await gather(self.get_ilks(block), self._urn(address))
41
+
42
+ gem_coros = gather(*[self.get_gem(str(ilk)) for ilk in ilks])
43
+ ink_coros = gather(
44
+ *[self.vat.urns.coroutine(ilk, urn, block_identifier=block) for ilk in ilks]
45
+ )
46
+
47
+ gems, ink_data = await gather(gem_coros, ink_coros)
48
+
49
+ balances: TokenBalances = TokenBalances(block=block)
50
+ for token, data in zip(gems, ink_data):
51
+ if ink := data.dict()["ink"]:
52
+ balance = ink / Decimal(10**18)
53
+ value = round(balance * Decimal(await get_price(token, block, sync=False)), 18)
54
+ balances[token] = Balance(balance, value, token=token, block=block)
55
+ return balances
56
+
57
+ @stuck_coro_debugger
58
+ async def _debt(self, address: Address, block: Optional[int] = None) -> TokenBalances:
59
+ if block is not None and block <= await contract_creation_block_async(self.ilk_registry):
60
+ return TokenBalances(block=block)
61
+
62
+ ilks, urn = await gather(self.get_ilks(block), self._urn(address))
63
+
64
+ data = await gather(
65
+ *[
66
+ gather(
67
+ self.vat.urns.coroutine(ilk, urn, block_identifier=block),
68
+ self.vat.ilks.coroutine(ilk, block_identifier=block),
69
+ )
70
+ for ilk in ilks
71
+ ]
72
+ )
73
+
74
+ balances: TokenBalances = TokenBalances(block=block)
75
+ for urns, ilk_info in data:
76
+ art = urns.dict()["art"]
77
+ rate = ilk_info.dict()["rate"]
78
+ debt = art * rate / Decimal(1e45)
79
+ balances[dai] += Balance(debt, debt, token=dai, block=block)
80
+ return balances
81
+
82
+ async def get_ilks(self, block: Optional[int]) -> List[HexStr]:
83
+ """List all ilks (cdp keys of sorts) for MakerDAO"""
84
+ try:
85
+ return await self.ilk_registry.list.coroutine(block_identifier=block)
86
+ except Revert:
87
+ return []
88
+
89
+ @alru_cache
90
+ async def get_gem(self, ilk: bytes) -> str:
91
+ return await self.ilk_registry.gem.coroutine(ilk)
92
+
93
+ @alru_cache
94
+ async def _proxy(self, address: Address) -> Address:
95
+ return await self.proxy_registry.proxies.coroutine(address)
96
+
97
+ @alru_cache
98
+ async def _cdp(self, address: Address) -> Address:
99
+ proxy = await self._proxy(address)
100
+ return await self.cdp_manager.first.coroutine(proxy)
101
+
102
+ @alru_cache
103
+ async def _urn(self, address: Address) -> Address:
104
+ cdp = await self._cdp(address)
105
+ return await self.cdp_manager.urns.coroutine(cdp)