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.
- eth_portfolio/__init__.py +16 -0
- eth_portfolio/_argspec.py +42 -0
- eth_portfolio/_cache.py +116 -0
- eth_portfolio/_config.py +3 -0
- eth_portfolio/_db/__init__.py +0 -0
- eth_portfolio/_db/decorators.py +147 -0
- eth_portfolio/_db/entities.py +204 -0
- eth_portfolio/_db/utils.py +595 -0
- eth_portfolio/_decimal.py +122 -0
- eth_portfolio/_decorators.py +71 -0
- eth_portfolio/_exceptions.py +67 -0
- eth_portfolio/_ledgers/__init__.py +0 -0
- eth_portfolio/_ledgers/address.py +892 -0
- eth_portfolio/_ledgers/portfolio.py +327 -0
- eth_portfolio/_loaders/__init__.py +33 -0
- eth_portfolio/_loaders/balances.py +78 -0
- eth_portfolio/_loaders/token_transfer.py +214 -0
- eth_portfolio/_loaders/transaction.py +379 -0
- eth_portfolio/_loaders/utils.py +59 -0
- eth_portfolio/_shitcoins.py +212 -0
- eth_portfolio/_utils.py +286 -0
- eth_portfolio/_ydb/__init__.py +0 -0
- eth_portfolio/_ydb/token_transfers.py +136 -0
- eth_portfolio/address.py +382 -0
- eth_portfolio/buckets.py +181 -0
- eth_portfolio/constants.py +58 -0
- eth_portfolio/portfolio.py +629 -0
- eth_portfolio/protocols/__init__.py +66 -0
- eth_portfolio/protocols/_base.py +107 -0
- eth_portfolio/protocols/convex.py +17 -0
- eth_portfolio/protocols/dsr.py +31 -0
- eth_portfolio/protocols/lending/__init__.py +49 -0
- eth_portfolio/protocols/lending/_base.py +57 -0
- eth_portfolio/protocols/lending/compound.py +185 -0
- eth_portfolio/protocols/lending/liquity.py +110 -0
- eth_portfolio/protocols/lending/maker.py +105 -0
- eth_portfolio/protocols/lending/unit.py +47 -0
- eth_portfolio/protocols/liquity.py +16 -0
- eth_portfolio/py.typed +0 -0
- eth_portfolio/structs/__init__.py +43 -0
- eth_portfolio/structs/modified.py +69 -0
- eth_portfolio/structs/structs.py +637 -0
- eth_portfolio/typing.py +1460 -0
- eth_portfolio-1.1.0.dist-info/METADATA +174 -0
- eth_portfolio-1.1.0.dist-info/RECORD +47 -0
- eth_portfolio-1.1.0.dist-info/WHEEL +5 -0
- 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)
|