eth-portfolio-temp 0.0.28.dev0__cp310-cp310-win_amd64.whl → 0.2.17__cp310-cp310-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/_argspec.cp310-win_amd64.pyd +0 -0
- eth_portfolio/_cache.py +2 -2
- eth_portfolio/_config.cp310-win_amd64.pyd +0 -0
- eth_portfolio/_db/utils.py +7 -9
- eth_portfolio/_decimal.py +11 -10
- eth_portfolio/_ledgers/address.py +1 -1
- eth_portfolio/_loaders/_nonce.cp310-win_amd64.pyd +0 -0
- eth_portfolio/_loaders/_nonce.py +4 -4
- eth_portfolio/_loaders/balances.cp310-win_amd64.pyd +0 -0
- eth_portfolio/_loaders/token_transfer.py +1 -1
- eth_portfolio/_loaders/transaction.py +1 -1
- eth_portfolio/_loaders/utils.cp310-win_amd64.pyd +0 -0
- eth_portfolio/_loaders/utils.py +1 -1
- eth_portfolio/_shitcoins.cp310-win_amd64.pyd +0 -0
- eth_portfolio/_shitcoins.py +60 -0
- eth_portfolio/{typing/balance/single.cp310-win_amd64.pyd → _stableish.cp310-win_amd64.pyd} +0 -0
- eth_portfolio/_stableish.py +42 -0
- eth_portfolio/_utils.py +12 -10
- eth_portfolio/_ydb/token_transfers.py +32 -23
- eth_portfolio/address.py +2 -1
- eth_portfolio/buckets.py +39 -26
- eth_portfolio/constants.cp310-win_amd64.pyd +0 -0
- eth_portfolio/constants.py +21 -1
- eth_portfolio/portfolio.py +1 -1
- eth_portfolio/protocols/lending/liquity.py +1 -1
- eth_portfolio/protocols/lending/maker.py +13 -14
- eth_portfolio/structs/structs.py +2 -2
- eth_portfolio/typing/__init__.py +6 -6
- eth_portfolio__mypyc.cp310-win_amd64.pyd +0 -0
- eth_portfolio_scripts/_portfolio.py +54 -41
- eth_portfolio_scripts/_utils.py +20 -6
- eth_portfolio_scripts/balances.cp310-win_amd64.pyd +0 -0
- eth_portfolio_scripts/balances.py +7 -4
- eth_portfolio_scripts/docker/.grafana/dashboards/{portfolio → Portfolio}/Balances.json +23 -23
- eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml +2 -2
- eth_portfolio_scripts/docker/__init__.cp310-win_amd64.pyd +0 -0
- eth_portfolio_scripts/docker/check.cp310-win_amd64.pyd +0 -0
- eth_portfolio_scripts/docker/check.py +28 -17
- eth_portfolio_scripts/docker/docker-compose.yaml +2 -6
- eth_portfolio_scripts/docker/docker_compose.cp310-win_amd64.pyd +0 -0
- eth_portfolio_scripts/docker/docker_compose.py +38 -18
- eth_portfolio_scripts/main.py +6 -0
- eth_portfolio_scripts/victoria/__init__.py +3 -0
- {eth_portfolio_temp-0.0.28.dev0.dist-info → eth_portfolio_temp-0.2.17.dist-info}/METADATA +8 -7
- eth_portfolio_temp-0.2.17.dist-info/RECORD +83 -0
- {eth_portfolio_temp-0.0.28.dev0.dist-info → eth_portfolio_temp-0.2.17.dist-info}/top_level.txt +1 -1
- 93dc731d39cdfeb0971f__mypyc.cp310-win_amd64.pyd +0 -0
- eth_portfolio_temp-0.0.28.dev0.dist-info/RECORD +0 -82
- {eth_portfolio_temp-0.0.28.dev0.dist-info → eth_portfolio_temp-0.2.17.dist-info}/WHEEL +0 -0
- {eth_portfolio_temp-0.0.28.dev0.dist-info → eth_portfolio_temp-0.2.17.dist-info}/entry_points.txt +0 -0
eth_portfolio/buckets.py
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Optional, Set
|
|
2
|
+
from typing import Any, Final, Optional, Set
|
|
3
3
|
|
|
4
4
|
from a_sync import igather
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from y.constants import STABLECOINS, WRAPPED_GAS_COIN
|
|
5
|
+
from eth_typing import ChecksumAddress
|
|
6
|
+
from faster_async_lru import alru_cache
|
|
7
|
+
from y.constants import CHAINID, STABLECOINS, WRAPPED_GAS_COIN
|
|
8
|
+
from y.convert import to_address
|
|
8
9
|
from y.datatypes import Address, AnyAddressType
|
|
10
|
+
from y.exceptions import ContractNotVerified
|
|
9
11
|
from y.prices.lending.aave import aave
|
|
10
12
|
from y.prices.lending.compound import CToken, compound
|
|
11
13
|
from y.prices.stable_swap.curve import curve
|
|
12
14
|
from y.prices.yearn import YearnInspiredVault, is_yearn_vault
|
|
13
15
|
|
|
14
16
|
from eth_portfolio.constants import BTC_LIKE, ETH_LIKE, INTL_STABLECOINS
|
|
17
|
+
from eth_portfolio._stableish import STABLEISH_COINS
|
|
15
18
|
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
19
|
+
logger: Final = logging.getLogger(__name__)
|
|
20
|
+
log_debug: Final = logger.debug
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
SORT_AS_STABLES: Final = STABLECOINS.keys() | STABLEISH_COINS[CHAINID]
|
|
23
|
+
OTHER_LONG_TERM_ASSETS: Final[Set[ChecksumAddress]] = {}.get(CHAINID, set()) # type: ignore [call-overload]
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
async def get_token_bucket(token: AnyAddressType) -> str:
|
|
@@ -53,27 +58,25 @@ async def get_token_bucket(token: AnyAddressType) -> str:
|
|
|
53
58
|
- :func:`_unwrap_token`
|
|
54
59
|
- :func:`_is_stable`
|
|
55
60
|
"""
|
|
56
|
-
|
|
61
|
+
token_address = to_address(token)
|
|
57
62
|
try:
|
|
58
|
-
|
|
59
|
-
except
|
|
60
|
-
|
|
61
|
-
return "Other short term assets"
|
|
62
|
-
raise
|
|
63
|
+
token_address = await _unwrap_token(token_address)
|
|
64
|
+
except ContractNotVerified as e:
|
|
65
|
+
return "Other short term assets"
|
|
63
66
|
|
|
64
|
-
if _is_stable(
|
|
67
|
+
if _is_stable(token_address):
|
|
65
68
|
return "Cash & cash equivalents"
|
|
66
|
-
if
|
|
69
|
+
if token_address in ETH_LIKE:
|
|
67
70
|
return "ETH"
|
|
68
|
-
if
|
|
71
|
+
if token_address in BTC_LIKE:
|
|
69
72
|
return "BTC"
|
|
70
|
-
if
|
|
73
|
+
if token_address in OTHER_LONG_TERM_ASSETS:
|
|
71
74
|
return "Other long term assets"
|
|
72
75
|
return "Other short term assets"
|
|
73
76
|
|
|
74
77
|
|
|
75
78
|
@alru_cache(maxsize=None)
|
|
76
|
-
async def _unwrap_token(token) ->
|
|
79
|
+
async def _unwrap_token(token: Any) -> ChecksumAddress:
|
|
77
80
|
"""
|
|
78
81
|
Recursively unwrap a token to its underlying asset.
|
|
79
82
|
|
|
@@ -101,23 +104,31 @@ async def _unwrap_token(token) -> str:
|
|
|
101
104
|
- :class:`y.prices.lending.aave`
|
|
102
105
|
- :class:`y.prices.lending.compound.CToken`
|
|
103
106
|
"""
|
|
107
|
+
log_debug("unwrapping %s", token)
|
|
104
108
|
if str(token) in {"ETH", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}:
|
|
109
|
+
log_debug("returning eee address")
|
|
105
110
|
return token
|
|
106
111
|
|
|
107
112
|
if await is_yearn_vault(token, sync=False):
|
|
108
113
|
underlying = await YearnInspiredVault(token, asynchronous=True).underlying
|
|
114
|
+
log_debug("underlying: %s", underlying)
|
|
109
115
|
return await _unwrap_token(underlying)
|
|
110
116
|
if curve and (pool := await curve.get_pool(token)):
|
|
111
117
|
pool_tokens = set(await igather(map(_unwrap_token, await pool.coins)))
|
|
118
|
+
log_debug("pool_tokens: %s", pool_tokens)
|
|
112
119
|
if pool_bucket := _pool_bucket(pool_tokens):
|
|
120
|
+
log_debug("returning pool bucket: %s", pool_bucket)
|
|
113
121
|
return pool_bucket # type: ignore
|
|
114
122
|
if aave and await aave.is_atoken(token):
|
|
123
|
+
log_debug("atoken")
|
|
115
124
|
return str(await aave.underlying(token))
|
|
116
125
|
if compound and await compound.is_compound_market(token):
|
|
126
|
+
log_debug("unwrapping ctoken %s", token)
|
|
117
127
|
try:
|
|
118
128
|
return str(await CToken(token, asynchronous=True).underlying)
|
|
119
129
|
except AttributeError:
|
|
120
130
|
return WRAPPED_GAS_COIN
|
|
131
|
+
log_debug("returning: %s", token)
|
|
121
132
|
return token
|
|
122
133
|
|
|
123
134
|
|
|
@@ -148,28 +159,29 @@ def _pool_bucket(pool_tokens: set) -> Optional[str]:
|
|
|
148
159
|
- :data:`STABLECOINS`
|
|
149
160
|
- :data:`INTL_STABLECOINS`
|
|
150
161
|
"""
|
|
151
|
-
|
|
162
|
+
log_debug("Pool tokens: %s", pool_tokens)
|
|
152
163
|
if pool_tokens < BTC_LIKE:
|
|
153
164
|
return list(BTC_LIKE)[0]
|
|
154
165
|
if pool_tokens < ETH_LIKE:
|
|
155
166
|
return list(ETH_LIKE)[0]
|
|
156
|
-
if pool_tokens <
|
|
157
|
-
return list(
|
|
167
|
+
if pool_tokens < SORT_AS_STABLES:
|
|
168
|
+
return list(SORT_AS_STABLES)[0]
|
|
158
169
|
return list(INTL_STABLECOINS)[0] if pool_tokens < INTL_STABLECOINS else None
|
|
159
170
|
|
|
160
171
|
|
|
161
|
-
def _is_stable(token:
|
|
172
|
+
def _is_stable(token: ChecksumAddress) -> bool:
|
|
162
173
|
"""
|
|
163
|
-
Check if a token is a stablecoin.
|
|
174
|
+
Check if a token is a stablecoin or stable-ish coin.
|
|
164
175
|
|
|
165
|
-
This function checks if a given token is present in the :data:`STABLECOINS
|
|
166
|
-
:data:`INTL_STABLECOINS` sets, indicating that it is
|
|
176
|
+
This function checks if a given token is present in the :data:`STABLECOINS`,
|
|
177
|
+
:data:`INTL_STABLECOINS`, or :data:`STABLEISH_COINS` sets, indicating that it is
|
|
178
|
+
a stablecoin or considered stable by the wider market.
|
|
167
179
|
|
|
168
180
|
Args:
|
|
169
181
|
token: The address of the token to check.
|
|
170
182
|
|
|
171
183
|
Example:
|
|
172
|
-
Check if a token is a stablecoin:
|
|
184
|
+
Check if a token is a stablecoin or stable-ish coin:
|
|
173
185
|
|
|
174
186
|
>>> _is_stable("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
|
|
175
187
|
True
|
|
@@ -177,5 +189,6 @@ def _is_stable(token: Address) -> bool:
|
|
|
177
189
|
See Also:
|
|
178
190
|
- :data:`STABLECOINS`
|
|
179
191
|
- :data:`INTL_STABLECOINS`
|
|
192
|
+
- :data:`STABLEISH_COINS`
|
|
180
193
|
"""
|
|
181
|
-
return token in
|
|
194
|
+
return token in SORT_AS_STABLES or token in INTL_STABLECOINS
|
|
Binary file
|
eth_portfolio/constants.py
CHANGED
|
@@ -28,8 +28,19 @@ ETH_LIKE: Final = {
|
|
|
28
28
|
"0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb", # seth
|
|
29
29
|
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # eth
|
|
30
30
|
"0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", # steth
|
|
31
|
+
"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", # wstETH
|
|
31
32
|
"0x9559Aaa82d9649C7A7b220E7c461d2E74c9a3593", # reth
|
|
32
33
|
"0xE95A203B1a91a908F9B9CE46459d101078c2c3cb", # ankreth
|
|
34
|
+
"0x04C154b66CB340F3Ae24111CC767e0184Ed00Cc6", # pxETH
|
|
35
|
+
"0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3", # oETH
|
|
36
|
+
"0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6", # alETH
|
|
37
|
+
"0x1BED97CBC3c24A4fb5C069C6E311a967386131f7", # yETH
|
|
38
|
+
"0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E", # mevETH
|
|
39
|
+
"0x5E8422345238F34275888049021821E8E08CAa1f", # frxETH
|
|
40
|
+
"0x821A278dFff762c76410264303F25bF42e195C0C", # pETH
|
|
41
|
+
"0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", # cbETH
|
|
42
|
+
"0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", # weETH
|
|
43
|
+
"0x7C07F7aBe10CE8e33DC6C5aD68FE033085256A84", # icETH
|
|
33
44
|
},
|
|
34
45
|
}.get(chain.id, set())
|
|
35
46
|
|
|
@@ -43,6 +54,8 @@ BTC_LIKE: Final = {
|
|
|
43
54
|
"0x0316EB71485b0Ab14103307bf65a021042c6d380", # hbtc
|
|
44
55
|
"0x5228a22e72ccC52d415EcFd199F99D0665E7733b", # pbtc
|
|
45
56
|
"0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa", # tbtc
|
|
57
|
+
"0x66eFF5221ca926636224650Fd3B9c497FF828F7D", # multiBTC
|
|
58
|
+
"0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", # eBTC
|
|
46
59
|
},
|
|
47
60
|
}.get(chain.id, set())
|
|
48
61
|
|
|
@@ -52,11 +65,18 @@ INTL_STABLECOINS: Final = {
|
|
|
52
65
|
"0xC581b735A1688071A1746c968e0798D642EDE491", # EURT
|
|
53
66
|
"0xdB25f211AB05b1c97D595516F45794528a807ad8", # EURS
|
|
54
67
|
"0x96E61422b6A9bA0e068B6c5ADd4fFaBC6a4aae27", # ibEUR
|
|
55
|
-
"
|
|
68
|
+
"0x9fcf418B971134625CdF38448B949C8640971671", # EURN
|
|
69
|
+
"0x39b8B6385416f4cA36a20319F70D28621895279D", # EURe
|
|
70
|
+
"0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", # EURC
|
|
56
71
|
"0x3F1B0278A9ee595635B61817630cC19DE792f506", # sAUD
|
|
72
|
+
"0xFAFdF0C4c1CB09d430Bf88c75D88BB46DAe09967", # ibAUD
|
|
57
73
|
"0x97fe22E7341a0Cd8Db6F6C021A24Dc8f4DAD855F", # sGBP
|
|
74
|
+
"0x69681f8fde45345C3870BCD5eaf4A05a60E7D227", # ibGBP
|
|
58
75
|
"0xF6b1C627e95BFc3c1b4c9B825a032Ff0fBf3e07d", # sJPY
|
|
76
|
+
"0x5555f75e3d5278082200Fb451D1b6bA946D8e13b", # ibJPY
|
|
59
77
|
"0x0F83287FF768D1c1e17a42F44d644D7F22e8ee1d", # sCHF
|
|
78
|
+
"0x1CC481cE2BD2EC7Bf67d1Be64d4878b16078F309", # ibCHF
|
|
60
79
|
"0x269895a3dF4D73b077Fc823dD6dA1B95f72Aaf9B", # sKRW
|
|
80
|
+
"0x95dFDC8161832e4fF7816aC4B6367CE201538253", # ibKRW
|
|
61
81
|
},
|
|
62
82
|
}.get(chain.id, set())
|
eth_portfolio/portfolio.py
CHANGED
|
@@ -37,7 +37,7 @@ from eth_portfolio.typing import Addresses, PortfolioBalances
|
|
|
37
37
|
logger = logging.getLogger(__name__)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
class PortfolioWallets(Iterable[PortfolioAddress], Dict[Address, PortfolioAddress]): # type: ignore [
|
|
40
|
+
class PortfolioWallets(Iterable[PortfolioAddress], Dict[Address, PortfolioAddress]): # type: ignore [metaclass]
|
|
41
41
|
"""
|
|
42
42
|
A container that holds all :class:`~eth_portfolio.address.PortfolioAddress` objects for a specific :class:`~eth_portfolio.Portfolio`.
|
|
43
43
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from asyncio import gather
|
|
2
|
-
from typing import List, Optional
|
|
2
|
+
from typing import Final, List, Optional
|
|
3
3
|
|
|
4
4
|
from a_sync import igather
|
|
5
|
-
from
|
|
5
|
+
from brownie import ZERO_ADDRESS
|
|
6
6
|
from dank_mids.exceptions import Revert
|
|
7
7
|
from eth_typing import HexStr
|
|
8
|
+
from faster_async_lru import alru_cache
|
|
9
|
+
from faster_eth_abi import encode
|
|
8
10
|
from y import Contract, Network, contract_creation_block_async, get_price
|
|
9
11
|
from y._decorators import stuck_coro_debugger
|
|
10
12
|
from y.constants import dai
|
|
@@ -14,18 +16,15 @@ from eth_portfolio._utils import Decimal
|
|
|
14
16
|
from eth_portfolio.protocols.lending._base import LendingProtocolWithLockedCollateral
|
|
15
17
|
from eth_portfolio.typing import Balance, TokenBalances
|
|
16
18
|
|
|
17
|
-
try:
|
|
18
|
-
# this is only available in 4.0.0+
|
|
19
|
-
from eth_abi import encode
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
yfi: Final = "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e"
|
|
21
|
+
dai: Contract
|
|
22
|
+
_1e18: Final = Decimal(10**18)
|
|
23
|
+
_1e45: Final = Decimal(10**45)
|
|
24
24
|
|
|
25
|
-
encode_bytes = lambda bytestring: encode_single("bytes32", bytestring)
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
def encode_bytes(bytestring: str) -> bytes:
|
|
27
|
+
return encode(["bytes32"], [bytestring])
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
class Maker(LendingProtocolWithLockedCollateral):
|
|
@@ -50,8 +49,8 @@ class Maker(LendingProtocolWithLockedCollateral):
|
|
|
50
49
|
|
|
51
50
|
balances: TokenBalances = TokenBalances(block=block)
|
|
52
51
|
for token, data in zip(gems, ink_data):
|
|
53
|
-
if ink := data.dict()["ink"]:
|
|
54
|
-
balance = ink /
|
|
52
|
+
if token != ZERO_ADDRESS and (ink := data.dict()["ink"]):
|
|
53
|
+
balance = ink / _1e18
|
|
55
54
|
value = round(balance * Decimal(await get_price(token, block, sync=False)), 18)
|
|
56
55
|
balances[token] = Balance(balance, value, token=token, block=block)
|
|
57
56
|
return balances
|
|
@@ -75,7 +74,7 @@ class Maker(LendingProtocolWithLockedCollateral):
|
|
|
75
74
|
for urns, ilk_info in data:
|
|
76
75
|
art = urns.dict()["art"]
|
|
77
76
|
rate = ilk_info.dict()["rate"]
|
|
78
|
-
debt = art * rate /
|
|
77
|
+
debt = art * rate / _1e45
|
|
79
78
|
balances[dai.address] += Balance(debt, debt, token=dai, block=block)
|
|
80
79
|
return balances
|
|
81
80
|
|
eth_portfolio/structs/structs.py
CHANGED
|
@@ -80,7 +80,7 @@ class _LedgerEntryBase(DictStruct, kw_only=True, frozen=True, omit_defaults=True
|
|
|
80
80
|
The USD value of the cryptocurrency transferred in the {cls_name}, if price is known.
|
|
81
81
|
"""
|
|
82
82
|
|
|
83
|
-
def __init_subclass__(cls, **kwargs):
|
|
83
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
84
84
|
super().__init_subclass__(**kwargs)
|
|
85
85
|
|
|
86
86
|
# Replace {cls_name} in attribute-level docstrings
|
|
@@ -273,7 +273,7 @@ class _TransactionBase(
|
|
|
273
273
|
return self.transaction.yParity
|
|
274
274
|
|
|
275
275
|
@property
|
|
276
|
-
def __db_primary_key__(self):
|
|
276
|
+
def __db_primary_key__(self) -> Dict[str, tuple[int, Address] | int]:
|
|
277
277
|
return {"from_address": (chain.id, self.from_address), "nonce": self.nonce}
|
|
278
278
|
|
|
279
279
|
|
eth_portfolio/typing/__init__.py
CHANGED
|
@@ -43,10 +43,10 @@ from typing import (
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
from checksum_dict import DefaultChecksumDict
|
|
46
|
-
from eth_typing import BlockNumber
|
|
46
|
+
from eth_typing import BlockNumber, HexAddress
|
|
47
47
|
from pandas import DataFrame, concat
|
|
48
48
|
from typing_extensions import ParamSpec, Self
|
|
49
|
-
from y import ERC20
|
|
49
|
+
from y import Contract, ERC20
|
|
50
50
|
from y.datatypes import Address
|
|
51
51
|
|
|
52
52
|
from eth_portfolio._decimal import Decimal
|
|
@@ -159,10 +159,10 @@ class TokenBalances(DefaultChecksumDict[Balance], _SummableNonNumericMixin): #
|
|
|
159
159
|
raise
|
|
160
160
|
self[token.address] += balance
|
|
161
161
|
|
|
162
|
-
def __getitem__(self, key) -> Balance:
|
|
162
|
+
def __getitem__(self, key: HexAddress) -> Balance:
|
|
163
163
|
return super().__getitem__(key) if key in self else Balance(token=key, block=self.block)
|
|
164
164
|
|
|
165
|
-
def __setitem__(self, key, value):
|
|
165
|
+
def __setitem__(self, key: HexAddress, value: Balance) -> None:
|
|
166
166
|
"""
|
|
167
167
|
Sets the balance for a given token address.
|
|
168
168
|
|
|
@@ -393,7 +393,7 @@ class RemoteTokenBalances(DefaultDict[ProtocolLabel, TokenBalances], _SummableNo
|
|
|
393
393
|
)
|
|
394
394
|
self[remote] += token_balances # type: ignore [has-type]
|
|
395
395
|
|
|
396
|
-
def __setitem__(self, protocol: str, value: TokenBalances):
|
|
396
|
+
def __setitem__(self, protocol: str, value: TokenBalances) -> None:
|
|
397
397
|
"""
|
|
398
398
|
Sets the token balances for a given protocol.
|
|
399
399
|
|
|
@@ -911,7 +911,7 @@ class PortfolioBalances(DefaultChecksumDict[WalletBalances], _SummableNonNumeric
|
|
|
911
911
|
)
|
|
912
912
|
self[wallet] += balances
|
|
913
913
|
|
|
914
|
-
def __setitem__(self, key, value):
|
|
914
|
+
def __setitem__(self, key: HexAddress, value: WalletBalances) -> None:
|
|
915
915
|
if not isinstance(value, WalletBalances):
|
|
916
916
|
raise TypeError(
|
|
917
917
|
f"value must be a `WalletBalances` object. You passed {value}"
|
|
Binary file
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from datetime import datetime, timezone
|
|
2
3
|
from logging import getLogger
|
|
3
4
|
from math import floor
|
|
@@ -5,10 +6,11 @@ from typing import Awaitable, Callable, Final, Iterator, List, Optional, Tuple
|
|
|
5
6
|
|
|
6
7
|
import a_sync
|
|
7
8
|
import eth_retry
|
|
9
|
+
import y
|
|
8
10
|
from a_sync.functools import cached_property_unsafe as cached_property
|
|
9
11
|
from eth_typing import BlockNumber, ChecksumAddress
|
|
10
12
|
from msgspec import ValidationError, json
|
|
11
|
-
from y import ERC20, Network, NonStandardERC20
|
|
13
|
+
from y import ERC20, Network, NonStandardERC20
|
|
12
14
|
from y.constants import CHAINID
|
|
13
15
|
from y.time import NoBlockFound
|
|
14
16
|
|
|
@@ -33,6 +35,19 @@ logger: Final = getLogger("eth_portfolio")
|
|
|
33
35
|
log_debug: Final = logger.debug
|
|
34
36
|
log_error: Final = logger.error
|
|
35
37
|
|
|
38
|
+
_block_at_timestamp_semaphore: Final = a_sync.Semaphore(
|
|
39
|
+
50, name="eth-portfolio get_block_at_timestamp"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def get_block_at_timestamp(dt: datetime) -> BlockNumber:
|
|
44
|
+
async with _block_at_timestamp_semaphore:
|
|
45
|
+
while True:
|
|
46
|
+
try:
|
|
47
|
+
return await y.get_block_at_timestamp(dt, sync=False)
|
|
48
|
+
except NoBlockFound:
|
|
49
|
+
await asyncio.sleep(10)
|
|
50
|
+
|
|
36
51
|
|
|
37
52
|
class ExportablePortfolio(Portfolio):
|
|
38
53
|
"""Adds methods to export full portoflio data."""
|
|
@@ -40,8 +55,10 @@ class ExportablePortfolio(Portfolio):
|
|
|
40
55
|
def __init__(
|
|
41
56
|
self,
|
|
42
57
|
addresses: Addresses,
|
|
58
|
+
*,
|
|
43
59
|
start_block: int = 0,
|
|
44
60
|
label: str = _DEFAULT_LABEL,
|
|
61
|
+
concurrency: int = 40,
|
|
45
62
|
load_prices: bool = True,
|
|
46
63
|
get_bucket: Callable[[ChecksumAddress], Awaitable[str]] = get_token_bucket,
|
|
47
64
|
num_workers_transactions: int = 1000,
|
|
@@ -51,6 +68,7 @@ class ExportablePortfolio(Portfolio):
|
|
|
51
68
|
addresses, start_block, label, load_prices, num_workers_transactions, asynchronous
|
|
52
69
|
)
|
|
53
70
|
self.get_bucket = get_bucket
|
|
71
|
+
self._semaphore = a_sync.Semaphore(concurrency)
|
|
54
72
|
|
|
55
73
|
@cached_property
|
|
56
74
|
def _data_queries(self) -> Tuple[str, str]:
|
|
@@ -71,53 +89,48 @@ class ExportablePortfolio(Portfolio):
|
|
|
71
89
|
return True
|
|
72
90
|
return False
|
|
73
91
|
|
|
74
|
-
async def export_snapshot(self, dt: datetime):
|
|
92
|
+
async def export_snapshot(self, dt: datetime) -> None:
|
|
75
93
|
log_debug("checking data at %s for %s", dt, self.label)
|
|
76
94
|
try:
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
else:
|
|
84
|
-
break
|
|
85
|
-
log_debug("block at %s: %s", dt, block)
|
|
86
|
-
data = await self.get_data_for_export(block, dt, sync=False)
|
|
87
|
-
await victoria.post_data(data)
|
|
95
|
+
if await self.data_exists(dt, sync=False):
|
|
96
|
+
return
|
|
97
|
+
block = await get_block_at_timestamp(dt)
|
|
98
|
+
log_debug("block at %s: %s", dt, block)
|
|
99
|
+
data = await self.get_data_for_export(block, dt, sync=False)
|
|
100
|
+
await victoria.post_data(data)
|
|
88
101
|
except Exception as e:
|
|
89
102
|
log_error("Error processing %s:", dt, exc_info=True)
|
|
90
103
|
|
|
91
|
-
@a_sync.Semaphore(60)
|
|
92
104
|
async def get_data_for_export(self, block: BlockNumber, ts: datetime) -> List[victoria.Metric]:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await self.__process_token(ts, section, wallet, token, bals)
|
|
105
|
-
)
|
|
106
|
-
elif isinstance(section_data, RemoteTokenBalances):
|
|
107
|
-
if section == "external":
|
|
108
|
-
section = "assets"
|
|
109
|
-
for protocol, token_bals in section_data.items():
|
|
110
|
-
for token, bals in dict.items(token_bals):
|
|
105
|
+
async with self._semaphore:
|
|
106
|
+
print(f"exporting {ts} for {self.label}")
|
|
107
|
+
start = datetime.now(tz=timezone.utc)
|
|
108
|
+
|
|
109
|
+
metrics_to_export = []
|
|
110
|
+
data: PortfolioBalances = await self.describe(block, sync=False)
|
|
111
|
+
|
|
112
|
+
for wallet, wallet_data in dict.items(data):
|
|
113
|
+
for section, section_data in wallet_data.items():
|
|
114
|
+
if isinstance(section_data, TokenBalances):
|
|
115
|
+
for token, bals in dict.items(section_data):
|
|
111
116
|
metrics_to_export.extend(
|
|
112
|
-
await self.__process_token(
|
|
113
|
-
ts, section, wallet, token, bals, protocol=protocol
|
|
114
|
-
)
|
|
117
|
+
await self.__process_token(ts, section, wallet, token, bals)
|
|
115
118
|
)
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
elif isinstance(section_data, RemoteTokenBalances):
|
|
120
|
+
if section == "external":
|
|
121
|
+
section = "assets"
|
|
122
|
+
for protocol, token_bals in section_data.items():
|
|
123
|
+
for token, bals in dict.items(token_bals):
|
|
124
|
+
metrics_to_export.extend(
|
|
125
|
+
await self.__process_token(
|
|
126
|
+
ts, section, wallet, token, bals, protocol=protocol
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
raise NotImplementedError()
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
print(f"got data for {ts} in {datetime.now(tz=timezone.utc) - start}")
|
|
133
|
+
return metrics_to_export
|
|
121
134
|
|
|
122
135
|
def __get_data_exists_coros(self, dt: datetime) -> Iterator[str]:
|
|
123
136
|
for query in self._data_queries:
|
|
@@ -131,7 +144,7 @@ class ExportablePortfolio(Portfolio):
|
|
|
131
144
|
token: ChecksumAddress,
|
|
132
145
|
bal: Balance,
|
|
133
146
|
protocol: Optional[str] = None,
|
|
134
|
-
):
|
|
147
|
+
) -> Tuple[victoria.types.PrometheusItem, victoria.types.PrometheusItem]:
|
|
135
148
|
# TODO wallet nicknames in grafana
|
|
136
149
|
# wallet = KNOWN_ADDRESSES[wallet] if wallet in KNOWN_ADDRESSES else wallet
|
|
137
150
|
if protocol is not None:
|
|
@@ -172,7 +185,7 @@ class ExportablePortfolio(Portfolio):
|
|
|
172
185
|
)
|
|
173
186
|
|
|
174
187
|
|
|
175
|
-
async def _get_symbol(token) -> str:
|
|
188
|
+
async def _get_symbol(token: str) -> str:
|
|
176
189
|
if token == "ETH":
|
|
177
190
|
return "ETH"
|
|
178
191
|
try:
|
eth_portfolio_scripts/_utils.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from asyncio import Task, create_task, sleep
|
|
3
3
|
from datetime import datetime, timedelta, timezone
|
|
4
|
-
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
4
|
+
from typing import Any, AsyncGenerator, Dict, Final, List, Optional
|
|
5
5
|
|
|
6
6
|
from brownie import chain
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
result = regex.findall(value)
|
|
9
|
+
timedelta_pattern: Final = re.compile(r"(\d+)([dhms]?)")
|
|
10
|
+
|
|
12
11
|
|
|
12
|
+
def parse_timedelta(value: str) -> timedelta:
|
|
13
13
|
days, hours, minutes, seconds = 0, 0, 0, 0
|
|
14
14
|
|
|
15
|
-
for val, unit in
|
|
15
|
+
for val, unit in timedelta_pattern.findall(value):
|
|
16
16
|
val = int(val)
|
|
17
17
|
if unit == "d":
|
|
18
18
|
days = val
|
|
@@ -58,10 +58,24 @@ async def aiter_timestamps(
|
|
|
58
58
|
|
|
59
59
|
timestamp = start
|
|
60
60
|
|
|
61
|
+
timestamps = []
|
|
61
62
|
while timestamp <= datetime.now(tz=timezone.utc):
|
|
62
|
-
|
|
63
|
+
timestamps.append(timestamp)
|
|
63
64
|
timestamp = timestamp + interval
|
|
64
65
|
|
|
66
|
+
# cycle between yielding earliest, latest, and middle from `timestamps` until complete
|
|
67
|
+
while timestamps:
|
|
68
|
+
# yield the earliest timestamp
|
|
69
|
+
yield timestamps.pop(0)
|
|
70
|
+
# yield the most recent timestamp if there is one
|
|
71
|
+
if timestamps:
|
|
72
|
+
yield timestamps.pop(-1)
|
|
73
|
+
# yield the most middle timestamp if there is one
|
|
74
|
+
if timestamps:
|
|
75
|
+
yield timestamps.pop(len(timestamps) // 2)
|
|
76
|
+
|
|
77
|
+
del timestamps
|
|
78
|
+
|
|
65
79
|
while run_forever:
|
|
66
80
|
while timestamp > datetime.now(tz=timezone.utc):
|
|
67
81
|
await _get_waiter(timestamp)
|
|
Binary file
|
|
@@ -26,13 +26,16 @@ async def export_balances(args: Namespace) -> None:
|
|
|
26
26
|
|
|
27
27
|
interval = parse_timedelta(args.interval)
|
|
28
28
|
portfolio = ExportablePortfolio(
|
|
29
|
-
args.wallet,
|
|
29
|
+
args.wallet,
|
|
30
|
+
label=args.label,
|
|
31
|
+
start_block=args.first_tx_block,
|
|
32
|
+
concurrency=args.concurrency,
|
|
33
|
+
load_prices=False,
|
|
30
34
|
)
|
|
31
35
|
|
|
32
36
|
if export_start_block := args.export_start_block or args.first_tx_block:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
37
|
+
start_ts = await dank_mids.eth.get_block_timestamp(export_start_block)
|
|
38
|
+
start = datetime.fromtimestamp(start_ts, tz=timezone.utc)
|
|
36
39
|
else:
|
|
37
40
|
start = None
|
|
38
41
|
|