eth-portfolio-temp 0.2.12__cp313-cp313-win32.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-win32.pyd +0 -0
- eth_portfolio/_argspec.py +42 -0
- eth_portfolio/_cache.py +121 -0
- eth_portfolio/_config.cp313-win32.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-win32.pyd +0 -0
- eth_portfolio/_loaders/_nonce.py +196 -0
- eth_portfolio/_loaders/balances.cp313-win32.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-win32.pyd +0 -0
- eth_portfolio/_loaders/utils.py +68 -0
- eth_portfolio/_shitcoins.cp313-win32.pyd +0 -0
- eth_portfolio/_shitcoins.py +329 -0
- eth_portfolio/_stableish.cp313-win32.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 +194 -0
- eth_portfolio/constants.cp313-win32.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-win32.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 +194 -0
- eth_portfolio_scripts/_utils.py +106 -0
- eth_portfolio_scripts/balances.cp313-win32.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-win32.pyd +0 -0
- eth_portfolio_scripts/docker/__init__.py +16 -0
- eth_portfolio_scripts/docker/check.cp313-win32.pyd +0 -0
- eth_portfolio_scripts/docker/check.py +56 -0
- eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
- eth_portfolio_scripts/docker/docker_compose.cp313-win32.pyd +0 -0
- eth_portfolio_scripts/docker/docker_compose.py +78 -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.2.12.dist-info/METADATA +25 -0
- eth_portfolio_temp-0.2.12.dist-info/RECORD +83 -0
- eth_portfolio_temp-0.2.12.dist-info/WHEEL +5 -0
- eth_portfolio_temp-0.2.12.dist-info/entry_points.txt +2 -0
- eth_portfolio_temp-0.2.12.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from typing import Literal, Optional, Union, final
|
|
2
|
+
|
|
3
|
+
from dictstruct import DictStruct
|
|
4
|
+
from eth_typing import BlockNumber, ChecksumAddress
|
|
5
|
+
from mypy_extensions import mypyc_attr
|
|
6
|
+
|
|
7
|
+
from eth_portfolio._decimal import Decimal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@final
|
|
11
|
+
@mypyc_attr(native_class=False)
|
|
12
|
+
class Balance(
|
|
13
|
+
DictStruct, frozen=True, omit_defaults=True, repr_omit_defaults=True, forbid_unknown_fields=True
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Represents the balance of a single token, including its token amount and equivalent USD value.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> balance1 = Balance(Decimal('100'), Decimal('2000'))
|
|
20
|
+
>>> balance2 = Balance(Decimal('50'), Decimal('1000'))
|
|
21
|
+
>>> combined_balance = balance1 + balance2
|
|
22
|
+
>>> combined_balance.balance
|
|
23
|
+
Decimal('150')
|
|
24
|
+
>>> combined_balance.usd_value
|
|
25
|
+
Decimal('3000')
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
balance: Decimal = Decimal(0)
|
|
29
|
+
"""
|
|
30
|
+
The amount of the token.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
usd_value: Decimal = Decimal(0)
|
|
34
|
+
"""
|
|
35
|
+
The USD equivalent value of the token amount.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
token: Optional[ChecksumAddress] = None
|
|
39
|
+
"""
|
|
40
|
+
The token the balance is of, if known.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
block: Optional[BlockNumber] = None
|
|
44
|
+
"""
|
|
45
|
+
The block from which the balance was taken, if known.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def usd(self) -> Decimal:
|
|
50
|
+
"""
|
|
51
|
+
An alias for `usd_value`. Returns the USD value of the token amount.
|
|
52
|
+
"""
|
|
53
|
+
return self.usd_value
|
|
54
|
+
|
|
55
|
+
def __add__(self, other: "Balance") -> "Balance":
|
|
56
|
+
"""
|
|
57
|
+
Adds two :class:`~eth_portfolio.typing.Balance` objects together. It is the user's responsibility to ensure that the two
|
|
58
|
+
:class:`~eth_portfolio.typing.Balance` instances represent the same token.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
other: Another :class:`~eth_portfolio.typing.Balance` object.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A new :class:`~eth_portfolio.typing.Balance` object with the summed values.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
TypeError: If the other object is not a :class:`~eth_portfolio.typing.Balance`.
|
|
68
|
+
Exception: If any other error occurs during addition.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> balance1 = Balance(Decimal('100'), Decimal('2000'))
|
|
72
|
+
>>> balance2 = Balance(Decimal('50'), Decimal('1000'))
|
|
73
|
+
>>> combined_balance = balance1 + balance2
|
|
74
|
+
>>> combined_balance.balance
|
|
75
|
+
Decimal('150')
|
|
76
|
+
>>> combined_balance.usd_value
|
|
77
|
+
Decimal('3000')
|
|
78
|
+
"""
|
|
79
|
+
if not isinstance(other, Balance):
|
|
80
|
+
raise TypeError(f"{other} is not a `Balance` object")
|
|
81
|
+
if self.token != other.token:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"These Balance objects represent balances of different tokens ({self.token} and {other.token})"
|
|
84
|
+
)
|
|
85
|
+
if self.block != other.block:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"These Balance objects represent balances from different blocks ({self.block} and {other.block})"
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
return Balance(
|
|
91
|
+
balance=self.balance + other.balance,
|
|
92
|
+
usd_value=self.usd_value + other.usd_value,
|
|
93
|
+
token=self.token,
|
|
94
|
+
block=self.block,
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
e.args = (f"Cannot add {self} and {other}: {e}", *e.args)
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
def __radd__(self, other: Union["Balance", Literal[0]]) -> "Balance":
|
|
101
|
+
"""
|
|
102
|
+
Supports the addition operation from the right side to enable use of `sum`.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
other: Another :class:`~eth_portfolio.typing.Balance` object or zero.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
A new :class:`~eth_portfolio.typing.Balance` object with the summed values.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
TypeError: If the other object is not a :class:`~eth_portfolio.typing.Balance`.
|
|
112
|
+
Exception: If any other error occurs during addition.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> balance = Balance(Decimal('100'), Decimal('2000'))
|
|
116
|
+
>>> sum_balance = sum([balance, Balance()])
|
|
117
|
+
>>> sum_balance.balance
|
|
118
|
+
Decimal('100')
|
|
119
|
+
"""
|
|
120
|
+
return self if other == 0 else self.__add__(other) # type: ignore
|
|
121
|
+
|
|
122
|
+
def __sub__(self, other: "Balance") -> "Balance":
|
|
123
|
+
"""
|
|
124
|
+
Subtracts one :class:`~eth_portfolio.typing.Balance` object from another. It is the user's responsibility to ensure that
|
|
125
|
+
the two :class:`~eth_portfolio.typing.Balance` instances represent the same token.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
other: Another :class:`~eth_portfolio.typing.Balance` object.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A new :class:`~eth_portfolio.typing.Balance` object with the subtracted values.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
TypeError: If the other object is not a :class:`~eth_portfolio.typing.Balance`.
|
|
135
|
+
Exception: If any other error occurs during subtraction.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> balance1 = Balance(Decimal('100'), Decimal('2000'))
|
|
139
|
+
>>> balance2 = Balance(Decimal('50'), Decimal('1000'))
|
|
140
|
+
>>> result_balance = balance1 - balance2
|
|
141
|
+
>>> result_balance.balance
|
|
142
|
+
Decimal('50')
|
|
143
|
+
"""
|
|
144
|
+
if not isinstance(other, Balance):
|
|
145
|
+
raise TypeError(f"{other} is not a `Balance` object.")
|
|
146
|
+
if self.token != other.token:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"These Balance objects represent balances of different tokens ({self.token} and {other.token})"
|
|
149
|
+
)
|
|
150
|
+
if self.block != other.block:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"These Balance objects represent balances from different blocks ({self.block} and {other.block})"
|
|
153
|
+
)
|
|
154
|
+
try:
|
|
155
|
+
return Balance(
|
|
156
|
+
balance=self.balance - other.balance,
|
|
157
|
+
usd_value=self.usd_value - other.usd_value,
|
|
158
|
+
token=self.token,
|
|
159
|
+
block=self.block,
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise e.__class__(f"Cannot subtract {self} and {other}: {e}") from e
|
|
163
|
+
|
|
164
|
+
def __bool__(self) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Evaluates the truth value of the :class:`~eth_portfolio.typing.Balance` object.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if either the balance or the USD value is non-zero, otherwise False.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> balance = Balance(Decimal('0'), Decimal('0'))
|
|
173
|
+
>>> bool(balance)
|
|
174
|
+
False
|
|
175
|
+
"""
|
|
176
|
+
return self.balance != 0 or self.usd_value != 0
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from os import environ
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
environ["DANKMIDS_GANACHE_FORK"] = "0"
|
|
5
|
+
environ["DANKMIDS_COLLECT_STATS"] = "0"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from eth_portfolio_scripts._logging import logger, setup_logging
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
setup_logging()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["logger"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from argparse import ArgumentParser
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_arg_parser(description: str) -> ArgumentParser:
|
|
5
|
+
return ArgumentParser(description)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def add_infra_port_args(parser: ArgumentParser) -> None:
|
|
9
|
+
parser.add_argument(
|
|
10
|
+
"--grafana-port",
|
|
11
|
+
type=int,
|
|
12
|
+
help="The port that will be used by grafana",
|
|
13
|
+
default=3000,
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--renderer-port",
|
|
17
|
+
type=int,
|
|
18
|
+
help="The port that will be used by grafana",
|
|
19
|
+
default=8091,
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--victoria-port",
|
|
23
|
+
type=int,
|
|
24
|
+
help="The port that will be used by victoria metrics",
|
|
25
|
+
default=8428,
|
|
26
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from logging import INFO, basicConfig, getLogger
|
|
3
|
+
|
|
4
|
+
from brownie.exceptions import BrownieCompilerWarning, BrownieEnvironmentWarning
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def setup_logging() -> None:
|
|
11
|
+
basicConfig(level=INFO)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
warnings.simplefilter("ignore", BrownieCompilerWarning)
|
|
15
|
+
warnings.simplefilter("ignore", BrownieEnvironmentWarning)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
from math import floor
|
|
5
|
+
from typing import Awaitable, Callable, Final, Iterator, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import a_sync
|
|
8
|
+
import eth_retry
|
|
9
|
+
import y
|
|
10
|
+
from a_sync.functools import cached_property_unsafe as cached_property
|
|
11
|
+
from eth_typing import BlockNumber, ChecksumAddress
|
|
12
|
+
from msgspec import ValidationError, json
|
|
13
|
+
from y import ERC20, Network, NonStandardERC20
|
|
14
|
+
from y.constants import CHAINID
|
|
15
|
+
from y.time import NoBlockFound
|
|
16
|
+
|
|
17
|
+
from eth_portfolio import Portfolio
|
|
18
|
+
from eth_portfolio.buckets import get_token_bucket
|
|
19
|
+
from eth_portfolio.portfolio import _DEFAULT_LABEL
|
|
20
|
+
from eth_portfolio.typing import (
|
|
21
|
+
Addresses,
|
|
22
|
+
Balance,
|
|
23
|
+
RemoteTokenBalances,
|
|
24
|
+
PortfolioBalances,
|
|
25
|
+
TokenBalances,
|
|
26
|
+
)
|
|
27
|
+
from eth_portfolio_scripts import victoria
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
NETWORK_LABEL: Final = Network.label(CHAINID)
|
|
31
|
+
|
|
32
|
+
decode: Final = json.decode
|
|
33
|
+
|
|
34
|
+
logger: Final = getLogger("eth_portfolio")
|
|
35
|
+
log_debug: Final = logger.debug
|
|
36
|
+
log_error: Final = logger.error
|
|
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
|
+
|
|
51
|
+
|
|
52
|
+
class ExportablePortfolio(Portfolio):
|
|
53
|
+
"""Adds methods to export full portoflio data."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
addresses: Addresses,
|
|
58
|
+
*,
|
|
59
|
+
start_block: int = 0,
|
|
60
|
+
label: str = _DEFAULT_LABEL,
|
|
61
|
+
concurrency: int = 40,
|
|
62
|
+
load_prices: bool = True,
|
|
63
|
+
get_bucket: Callable[[ChecksumAddress], Awaitable[str]] = get_token_bucket,
|
|
64
|
+
num_workers_transactions: int = 1000,
|
|
65
|
+
asynchronous: bool = False,
|
|
66
|
+
):
|
|
67
|
+
super().__init__(
|
|
68
|
+
addresses, start_block, label, load_prices, num_workers_transactions, asynchronous
|
|
69
|
+
)
|
|
70
|
+
self.get_bucket = get_bucket
|
|
71
|
+
self._semaphore = a_sync.Semaphore(concurrency)
|
|
72
|
+
|
|
73
|
+
@cached_property
|
|
74
|
+
def _data_queries(self) -> Tuple[str, str]:
|
|
75
|
+
label = self.label.lower().replace(" ", "_")
|
|
76
|
+
return f"{label}_assets", f"{label}_debts"
|
|
77
|
+
|
|
78
|
+
@eth_retry.auto_retry
|
|
79
|
+
@a_sync.Semaphore(16)
|
|
80
|
+
async def data_exists(self, dt: datetime) -> bool:
|
|
81
|
+
# sourcery skip: use-contextlib-suppress
|
|
82
|
+
async for data in a_sync.as_completed(list(self.__get_data_exists_coros(dt)), aiter=True):
|
|
83
|
+
try:
|
|
84
|
+
result = decode(data, type=victoria.types.Response)
|
|
85
|
+
except ValidationError:
|
|
86
|
+
raise victoria.VictoriaMetricsError(data.decode()) from None
|
|
87
|
+
if result.status == "success" and len(result.data.result) > 0:
|
|
88
|
+
print(f"{dt} already loaded")
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
async def export_snapshot(self, dt: datetime) -> None:
|
|
93
|
+
log_debug("checking data at %s for %s", dt, self.label)
|
|
94
|
+
try:
|
|
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)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
log_error("Error processing %s:", dt, exc_info=True)
|
|
103
|
+
|
|
104
|
+
async def get_data_for_export(self, block: BlockNumber, ts: datetime) -> List[victoria.Metric]:
|
|
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):
|
|
116
|
+
metrics_to_export.extend(
|
|
117
|
+
await self.__process_token(ts, section, wallet, token, bals)
|
|
118
|
+
)
|
|
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()
|
|
131
|
+
|
|
132
|
+
print(f"got data for {ts} in {datetime.now(tz=timezone.utc) - start}")
|
|
133
|
+
return metrics_to_export
|
|
134
|
+
|
|
135
|
+
def __get_data_exists_coros(self, dt: datetime) -> Iterator[str]:
|
|
136
|
+
for query in self._data_queries:
|
|
137
|
+
yield victoria.get(f"/api/v1/query?query={query}&time={int(dt.timestamp())}")
|
|
138
|
+
|
|
139
|
+
async def __process_token(
|
|
140
|
+
self,
|
|
141
|
+
ts: datetime,
|
|
142
|
+
section: str,
|
|
143
|
+
wallet: ChecksumAddress,
|
|
144
|
+
token: ChecksumAddress,
|
|
145
|
+
bal: Balance,
|
|
146
|
+
protocol: Optional[str] = None,
|
|
147
|
+
) -> Tuple[victoria.types.PrometheusItem, victoria.types.PrometheusItem]:
|
|
148
|
+
# TODO wallet nicknames in grafana
|
|
149
|
+
# wallet = KNOWN_ADDRESSES[wallet] if wallet in KNOWN_ADDRESSES else wallet
|
|
150
|
+
if protocol is not None:
|
|
151
|
+
wallet = f"{protocol} | {wallet}"
|
|
152
|
+
|
|
153
|
+
label_and_section = f"{self.label}_{section}".lower().replace(" ", "_")
|
|
154
|
+
symbol = await _get_symbol(token)
|
|
155
|
+
bucket = await self.get_bucket(token)
|
|
156
|
+
ts_millis = floor(ts.timestamp()) * 1000
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
victoria.types.PrometheusItem(
|
|
160
|
+
metric=victoria.Metric(
|
|
161
|
+
param="balance",
|
|
162
|
+
wallet=wallet,
|
|
163
|
+
token_address=token,
|
|
164
|
+
token=symbol,
|
|
165
|
+
bucket=bucket,
|
|
166
|
+
network=NETWORK_LABEL,
|
|
167
|
+
__name__=label_and_section,
|
|
168
|
+
),
|
|
169
|
+
values=[float(bal.balance)],
|
|
170
|
+
timestamps=[ts_millis],
|
|
171
|
+
),
|
|
172
|
+
victoria.types.PrometheusItem(
|
|
173
|
+
metric=victoria.Metric(
|
|
174
|
+
param="usd value",
|
|
175
|
+
wallet=wallet,
|
|
176
|
+
token_address=token,
|
|
177
|
+
token=symbol,
|
|
178
|
+
bucket=bucket,
|
|
179
|
+
network=NETWORK_LABEL,
|
|
180
|
+
__name__=label_and_section,
|
|
181
|
+
),
|
|
182
|
+
values=[float(bal.usd)],
|
|
183
|
+
timestamps=[ts_millis],
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def _get_symbol(token: str) -> str:
|
|
189
|
+
if token == "ETH":
|
|
190
|
+
return "ETH"
|
|
191
|
+
try:
|
|
192
|
+
return await ERC20(token, asynchronous=True).symbol
|
|
193
|
+
except NonStandardERC20:
|
|
194
|
+
return "<NonStandardERC20>"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from asyncio import Task, create_task, sleep
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from typing import Any, AsyncGenerator, Dict, Final, List, Optional
|
|
5
|
+
|
|
6
|
+
from brownie import chain
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
timedelta_pattern: Final = re.compile(r"(\d+)([dhms]?)")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_timedelta(value: str) -> timedelta:
|
|
13
|
+
days, hours, minutes, seconds = 0, 0, 0, 0
|
|
14
|
+
|
|
15
|
+
for val, unit in timedelta_pattern.findall(value):
|
|
16
|
+
val = int(val)
|
|
17
|
+
if unit == "d":
|
|
18
|
+
days = val
|
|
19
|
+
elif unit == "h":
|
|
20
|
+
hours = val
|
|
21
|
+
elif unit == "m":
|
|
22
|
+
minutes = val
|
|
23
|
+
elif unit == "s":
|
|
24
|
+
seconds = val
|
|
25
|
+
|
|
26
|
+
return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def aiter_timestamps(
|
|
30
|
+
*,
|
|
31
|
+
start: Optional[datetime] = None,
|
|
32
|
+
interval: timedelta = timedelta(days=1),
|
|
33
|
+
run_forever: bool = False,
|
|
34
|
+
) -> AsyncGenerator[datetime, None]:
|
|
35
|
+
"""
|
|
36
|
+
Generates the timestamps to be queried based on the specified range and interval.
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(run_forever, bool):
|
|
39
|
+
raise TypeError(f"`run_forever` must be boolean. You passed {run_forever}")
|
|
40
|
+
|
|
41
|
+
if start is None:
|
|
42
|
+
start = datetime.now(tz=timezone.utc)
|
|
43
|
+
|
|
44
|
+
block0_ts = datetime.fromtimestamp(chain[0].timestamp, tz=timezone.utc)
|
|
45
|
+
helper = datetime(
|
|
46
|
+
year=block0_ts.year,
|
|
47
|
+
month=block0_ts.month,
|
|
48
|
+
day=block0_ts.day,
|
|
49
|
+
hour=block0_ts.hour,
|
|
50
|
+
minute=block0_ts.minute,
|
|
51
|
+
tzinfo=timezone.utc,
|
|
52
|
+
)
|
|
53
|
+
while helper + interval < start:
|
|
54
|
+
helper += interval
|
|
55
|
+
start = helper
|
|
56
|
+
if start < block0_ts:
|
|
57
|
+
start += interval
|
|
58
|
+
|
|
59
|
+
timestamp = start
|
|
60
|
+
|
|
61
|
+
timestamps = []
|
|
62
|
+
while timestamp <= datetime.now(tz=timezone.utc):
|
|
63
|
+
timestamps.append(timestamp)
|
|
64
|
+
timestamp = timestamp + interval
|
|
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
|
+
|
|
79
|
+
while run_forever:
|
|
80
|
+
while timestamp > datetime.now(tz=timezone.utc):
|
|
81
|
+
await _get_waiter(timestamp)
|
|
82
|
+
yield timestamp
|
|
83
|
+
timestamp += interval
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_waiters: Dict[datetime, "Task[None]"] = {}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_waiter(timestamp: datetime) -> "Task[None]":
|
|
90
|
+
if timestamp not in _waiters:
|
|
91
|
+
waiter = create_task(sleep_until(timestamp))
|
|
92
|
+
waiter.add_done_callback(_sleep_done_callback)
|
|
93
|
+
_waiters[timestamp] = waiter
|
|
94
|
+
return _waiters[timestamp]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def sleep_until(until: datetime) -> None:
|
|
98
|
+
now = datetime.now(tz=timezone.utc)
|
|
99
|
+
await sleep((until - now).total_seconds())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _sleep_done_callback(t: "Task[Any]") -> None:
|
|
103
|
+
low_to_hi = sorted(_waiters)
|
|
104
|
+
for k in low_to_hi:
|
|
105
|
+
if _waiters[k] is t:
|
|
106
|
+
_waiters.pop(k)
|
|
Binary file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from argparse import Namespace
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
import a_sync
|
|
6
|
+
import a_sync.asyncio
|
|
7
|
+
|
|
8
|
+
from eth_portfolio_scripts import docker
|
|
9
|
+
from eth_portfolio_scripts._utils import aiter_timestamps, parse_timedelta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_UTC: Final = timezone.utc
|
|
13
|
+
|
|
14
|
+
create_task: Final = a_sync.create_task
|
|
15
|
+
yield_to_loop: Final = a_sync.asyncio.sleep0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@docker.ensure_containers
|
|
19
|
+
async def export_balances(args: Namespace) -> None:
|
|
20
|
+
import dank_mids
|
|
21
|
+
|
|
22
|
+
from eth_portfolio_scripts._portfolio import ExportablePortfolio
|
|
23
|
+
|
|
24
|
+
if args.daemon is True:
|
|
25
|
+
raise NotImplementedError("This feature must be implemented")
|
|
26
|
+
|
|
27
|
+
interval = parse_timedelta(args.interval)
|
|
28
|
+
portfolio = ExportablePortfolio(
|
|
29
|
+
args.wallet,
|
|
30
|
+
label=args.label,
|
|
31
|
+
start_block=args.first_tx_block,
|
|
32
|
+
concurrency=args.concurrency,
|
|
33
|
+
load_prices=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if export_start_block := args.export_start_block or args.first_tx_block:
|
|
37
|
+
start_ts = await dank_mids.eth.get_block_timestamp(export_start_block)
|
|
38
|
+
start = datetime.fromtimestamp(start_ts, tz=timezone.utc)
|
|
39
|
+
else:
|
|
40
|
+
start = None
|
|
41
|
+
|
|
42
|
+
print(f"Exporting {portfolio}")
|
|
43
|
+
async for ts in aiter_timestamps(start=start, interval=interval, run_forever=True):
|
|
44
|
+
create_task(
|
|
45
|
+
coro=portfolio.export_snapshot(ts, sync=False),
|
|
46
|
+
name=f"eth-portfolio export snapshot {ts}",
|
|
47
|
+
skip_gc_until_done=True,
|
|
48
|
+
)
|
|
49
|
+
# get some work in before yielding the next task
|
|
50
|
+
await yield_to_loop()
|
|
51
|
+
await yield_to_loop()
|
|
52
|
+
await yield_to_loop()
|