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.
- eth_portfolio/__init__.py +25 -0
- eth_portfolio/_argspec.cp313-win_amd64.pyd +0 -0
- eth_portfolio/_argspec.py +42 -0
- eth_portfolio/_cache.py +121 -0
- eth_portfolio/_config.cp313-win_amd64.pyd +0 -0
- eth_portfolio/_config.py +4 -0
- eth_portfolio/_db/__init__.py +0 -0
- eth_portfolio/_db/decorators.py +147 -0
- eth_portfolio/_db/entities.py +311 -0
- eth_portfolio/_db/utils.py +604 -0
- eth_portfolio/_decimal.py +156 -0
- eth_portfolio/_decorators.py +84 -0
- eth_portfolio/_exceptions.py +67 -0
- eth_portfolio/_ledgers/__init__.py +0 -0
- eth_portfolio/_ledgers/address.py +938 -0
- eth_portfolio/_ledgers/portfolio.py +327 -0
- eth_portfolio/_loaders/__init__.py +33 -0
- eth_portfolio/_loaders/_nonce.cp313-win_amd64.pyd +0 -0
- eth_portfolio/_loaders/_nonce.py +196 -0
- eth_portfolio/_loaders/balances.cp313-win_amd64.pyd +0 -0
- eth_portfolio/_loaders/balances.py +94 -0
- eth_portfolio/_loaders/token_transfer.py +217 -0
- eth_portfolio/_loaders/transaction.py +240 -0
- eth_portfolio/_loaders/utils.cp313-win_amd64.pyd +0 -0
- eth_portfolio/_loaders/utils.py +68 -0
- eth_portfolio/_shitcoins.cp313-win_amd64.pyd +0 -0
- eth_portfolio/_shitcoins.py +330 -0
- eth_portfolio/_stableish.cp313-win_amd64.pyd +0 -0
- eth_portfolio/_stableish.py +42 -0
- eth_portfolio/_submodules.py +73 -0
- eth_portfolio/_utils.py +225 -0
- eth_portfolio/_ydb/__init__.py +0 -0
- eth_portfolio/_ydb/token_transfers.py +145 -0
- eth_portfolio/address.py +397 -0
- eth_portfolio/buckets.py +212 -0
- eth_portfolio/constants.cp313-win_amd64.pyd +0 -0
- eth_portfolio/constants.py +82 -0
- eth_portfolio/portfolio.py +661 -0
- eth_portfolio/protocols/__init__.py +67 -0
- eth_portfolio/protocols/_base.py +108 -0
- eth_portfolio/protocols/convex.py +17 -0
- eth_portfolio/protocols/dsr.py +51 -0
- eth_portfolio/protocols/lending/README.md +6 -0
- eth_portfolio/protocols/lending/__init__.py +50 -0
- eth_portfolio/protocols/lending/_base.py +57 -0
- eth_portfolio/protocols/lending/compound.py +187 -0
- eth_portfolio/protocols/lending/liquity.py +110 -0
- eth_portfolio/protocols/lending/maker.py +104 -0
- eth_portfolio/protocols/lending/unit.py +46 -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/__init__.py +1447 -0
- eth_portfolio/typing/balance/single.py +176 -0
- eth_portfolio__mypyc.cp313-win_amd64.pyd +0 -0
- eth_portfolio_scripts/__init__.py +20 -0
- eth_portfolio_scripts/_args.py +26 -0
- eth_portfolio_scripts/_logging.py +15 -0
- eth_portfolio_scripts/_portfolio.py +209 -0
- eth_portfolio_scripts/_utils.py +106 -0
- eth_portfolio_scripts/balances.cp313-win_amd64.pyd +0 -0
- eth_portfolio_scripts/balances.py +52 -0
- eth_portfolio_scripts/docker/.grafana/dashboards/Portfolio/Balances.json +1962 -0
- eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml +10 -0
- eth_portfolio_scripts/docker/.grafana/datasources/datasources.yml +11 -0
- eth_portfolio_scripts/docker/__init__.cp313-win_amd64.pyd +0 -0
- eth_portfolio_scripts/docker/__init__.py +16 -0
- eth_portfolio_scripts/docker/check.cp313-win_amd64.pyd +0 -0
- eth_portfolio_scripts/docker/check.py +67 -0
- eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
- eth_portfolio_scripts/docker/docker_compose.cp313-win_amd64.pyd +0 -0
- eth_portfolio_scripts/docker/docker_compose.py +98 -0
- eth_portfolio_scripts/main.py +119 -0
- eth_portfolio_scripts/py.typed +1 -0
- eth_portfolio_scripts/victoria/__init__.py +73 -0
- eth_portfolio_scripts/victoria/types.py +38 -0
- eth_portfolio_temp-0.3.0.dist-info/METADATA +26 -0
- eth_portfolio_temp-0.3.0.dist-info/RECORD +83 -0
- eth_portfolio_temp-0.3.0.dist-info/WHEEL +5 -0
- eth_portfolio_temp-0.3.0.dist-info/entry_points.txt +2 -0
- 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
|