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,604 @@
|
|
|
1
|
+
from asyncio import create_task, gather, get_event_loop, sleep
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
import evmspec
|
|
7
|
+
import y._db.common
|
|
8
|
+
import y._db.config as config
|
|
9
|
+
from a_sync import PruningThreadPoolExecutor, a_sync
|
|
10
|
+
from eth_typing import ChecksumAddress, HexAddress
|
|
11
|
+
from evmspec.data import _decode_hook
|
|
12
|
+
from logging import getLogger
|
|
13
|
+
from msgspec import ValidationError, json
|
|
14
|
+
from multicall.utils import get_event_loop
|
|
15
|
+
from pony.orm import (
|
|
16
|
+
BindingError,
|
|
17
|
+
OperationalError,
|
|
18
|
+
TransactionIntegrityError,
|
|
19
|
+
commit,
|
|
20
|
+
db_session,
|
|
21
|
+
flush,
|
|
22
|
+
select,
|
|
23
|
+
)
|
|
24
|
+
from y import ENVIRONMENT_VARIABLES as ENVS
|
|
25
|
+
from y._db.entities import db
|
|
26
|
+
from y.constants import CHAINID
|
|
27
|
+
from y.exceptions import reraise_excs_with_extra_context
|
|
28
|
+
|
|
29
|
+
from eth_portfolio._db import entities
|
|
30
|
+
from eth_portfolio._db.decorators import break_locks, requery_objs_on_diff_tx_err
|
|
31
|
+
from eth_portfolio._db.entities import (
|
|
32
|
+
AddressExtended,
|
|
33
|
+
BlockExtended,
|
|
34
|
+
TokenExtended,
|
|
35
|
+
)
|
|
36
|
+
from eth_portfolio._decimal import Decimal
|
|
37
|
+
from eth_portfolio.structs import InternalTransfer, TokenTransfer, Transaction, TransactionRLP
|
|
38
|
+
from eth_portfolio.typing import _P, _T, Fn
|
|
39
|
+
|
|
40
|
+
logger = getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __bind() -> None:
|
|
44
|
+
try:
|
|
45
|
+
db.bind(**config.connection_settings)
|
|
46
|
+
except BindingError as e:
|
|
47
|
+
if not str(e).startswith("Database object was already bound to"):
|
|
48
|
+
raise e
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__bind()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
db.generate_mapping(create_tables=True)
|
|
55
|
+
except OperationalError as e:
|
|
56
|
+
if not str(e).startswith("no such column:"):
|
|
57
|
+
raise
|
|
58
|
+
raise OperationalError(
|
|
59
|
+
"Since eth-portfolio extends the ypricemagic database with additional column definitions, you will need to delete your ypricemagic database at ~/.ypricemagic and rerun this script"
|
|
60
|
+
) from e
|
|
61
|
+
|
|
62
|
+
from y._db.decorators import retry_locked
|
|
63
|
+
from y._db.entities import Address, Block, Chain, Contract, Token, insert
|
|
64
|
+
|
|
65
|
+
# The db must be bound before we do this since we're adding some new columns to the tables defined in ypricemagic
|
|
66
|
+
from y._db.utils import ensure_chain, get_chain
|
|
67
|
+
from y._db.utils.price import _set_price
|
|
68
|
+
from y._db.utils.traces import insert_trace
|
|
69
|
+
from y import ERC20
|
|
70
|
+
from y.constants import EEE_ADDRESS
|
|
71
|
+
from y.exceptions import NonStandardERC20
|
|
72
|
+
from y.contracts import is_contract
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_big_pool_size = 4 if ENVS.DB_PROVIDER == "sqlite" else 10
|
|
76
|
+
_small_pool_size = 2 if ENVS.DB_PROVIDER == "sqlite" else 4
|
|
77
|
+
|
|
78
|
+
_block_executor = PruningThreadPoolExecutor(_small_pool_size, "eth-portfolio block")
|
|
79
|
+
_token_executor = PruningThreadPoolExecutor(_small_pool_size, "eth-portfolio token")
|
|
80
|
+
_address_executor = PruningThreadPoolExecutor(_small_pool_size, "eth-portfolio address")
|
|
81
|
+
_transaction_read_executor = PruningThreadPoolExecutor(
|
|
82
|
+
_big_pool_size, "eth-portfolio-transaction-read"
|
|
83
|
+
)
|
|
84
|
+
_transaction_write_executor = PruningThreadPoolExecutor(
|
|
85
|
+
_small_pool_size, "eth-portfolio-transaction-write"
|
|
86
|
+
)
|
|
87
|
+
_token_transfer_read_executor = PruningThreadPoolExecutor(
|
|
88
|
+
_big_pool_size, "eth-portfolio-token-transfer-read"
|
|
89
|
+
)
|
|
90
|
+
_token_transfer_write_executor = PruningThreadPoolExecutor(
|
|
91
|
+
_small_pool_size, "eth-portfolio-token-transfer-write"
|
|
92
|
+
)
|
|
93
|
+
_internal_transfer_read_executor = PruningThreadPoolExecutor(
|
|
94
|
+
_big_pool_size, "eth-portfolio-internal-transfer read"
|
|
95
|
+
)
|
|
96
|
+
_internal_transfer_write_executor = PruningThreadPoolExecutor(
|
|
97
|
+
_small_pool_size, "eth-portfolio-internal-transfer write"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def robust_db_session(fn: Fn[_P, _T]) -> Fn[_P, _T]:
|
|
102
|
+
return retry_locked(break_locks(db_session(fn)))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
db_session_cached = lambda func: retry_locked(
|
|
106
|
+
lru_cache(maxsize=None)(db_session(retry_locked(func)))
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@a_sync(default="async", executor=_block_executor)
|
|
111
|
+
@robust_db_session
|
|
112
|
+
def get_block(block: int) -> BlockExtended:
|
|
113
|
+
if b := BlockExtended.get(chain=CHAINID, number=block):
|
|
114
|
+
return b
|
|
115
|
+
elif b := Block.get(chain=CHAINID, number=block):
|
|
116
|
+
if isinstance(b, BlockExtended):
|
|
117
|
+
# in case of race cndtn
|
|
118
|
+
return b
|
|
119
|
+
raise ValueError(b, b.number, b.CHAINID)
|
|
120
|
+
hash = b.hash
|
|
121
|
+
ts = b.timestamp
|
|
122
|
+
prices = [(price.token.address, price.price) for price in b.prices]
|
|
123
|
+
logs = [json.decode(log.raw) for log in b.logs]
|
|
124
|
+
traces = [json.decode(trace.raw) for trace in b.traces]
|
|
125
|
+
for p in b.prices:
|
|
126
|
+
p.delete()
|
|
127
|
+
for l in b.logs:
|
|
128
|
+
l.delete()
|
|
129
|
+
for t in b.traces:
|
|
130
|
+
t.delete()
|
|
131
|
+
flush()
|
|
132
|
+
b.delete()
|
|
133
|
+
commit()
|
|
134
|
+
b = insert(
|
|
135
|
+
type=BlockExtended,
|
|
136
|
+
chain=get_chain(sync=True),
|
|
137
|
+
number=block,
|
|
138
|
+
hash=hash,
|
|
139
|
+
timestamp=ts,
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
# for log in logs:
|
|
143
|
+
# insert_log(log)
|
|
144
|
+
for trace in traces:
|
|
145
|
+
insert_trace(trace)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
e.args = (
|
|
148
|
+
"This is really bad. Might need to nuke your db if you value your logs/traces",
|
|
149
|
+
*e.args,
|
|
150
|
+
)
|
|
151
|
+
raise
|
|
152
|
+
for token, price in prices:
|
|
153
|
+
_set_price(token, price, sync=True)
|
|
154
|
+
asdasd = get_chain(sync=True)
|
|
155
|
+
if not isinstance(asdasd, Chain):
|
|
156
|
+
raise TypeError(asdasd)
|
|
157
|
+
commit()
|
|
158
|
+
if b := insert(type=BlockExtended, chain=asdasd, number=block):
|
|
159
|
+
return b
|
|
160
|
+
return BlockExtended.get(chain=CHAINID, number=block)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@a_sync(default="async", executor=_block_executor)
|
|
164
|
+
@db_session_cached
|
|
165
|
+
def ensure_block(block: int) -> None:
|
|
166
|
+
get_block(block, sync=True)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# TODO refactor this out, async is annoying sometimes
|
|
170
|
+
# process = ProcessPoolExecutor(
|
|
171
|
+
# 1,
|
|
172
|
+
# # NOTE: come on apple, what are you dooooin?
|
|
173
|
+
# mp_context=get_context('fork'),
|
|
174
|
+
# )
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def is_token(address: ChecksumAddress) -> bool:
|
|
178
|
+
if address == EEE_ADDRESS:
|
|
179
|
+
return False
|
|
180
|
+
# with suppress(NonStandardERC20):
|
|
181
|
+
# erc = ERC20(address)
|
|
182
|
+
# if all(erc.symbol, erc.name, erc.total_supply(), erc.scale):
|
|
183
|
+
# #if all(erc._symbol(), erc._name(), erc.total_supply(), erc._scale()):
|
|
184
|
+
# return True
|
|
185
|
+
# return False
|
|
186
|
+
return get_event_loop().run_until_complete(_is_token(address))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def _is_token(address: HexAddress) -> bool:
|
|
190
|
+
# just breaking a weird lock, dont mind me
|
|
191
|
+
if retval := await get_event_loop().run_in_executor(process, __is_token, address): # type: ignore [name-defined]
|
|
192
|
+
logger.debug("%s is token")
|
|
193
|
+
else:
|
|
194
|
+
logger.debug("%s is not token")
|
|
195
|
+
return retval
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def __is_token(address: HexAddress) -> bool:
|
|
199
|
+
with suppress(NonStandardERC20):
|
|
200
|
+
erc = ERC20(address, asynchronous=True)
|
|
201
|
+
if all(
|
|
202
|
+
get_event_loop().run_until_complete(
|
|
203
|
+
gather(erc._symbol(), erc._name(), erc.total_supply_readable())
|
|
204
|
+
)
|
|
205
|
+
):
|
|
206
|
+
return True
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@a_sync(default="async", executor=_address_executor)
|
|
211
|
+
@robust_db_session
|
|
212
|
+
def get_address(address: ChecksumAddress) -> AddressExtended:
|
|
213
|
+
entity_type = TokenExtended
|
|
214
|
+
entity = entities.Address.get(chain=CHAINID, address=address)
|
|
215
|
+
""" TODO: fix this later
|
|
216
|
+
entity = entities.Address.get(chain=chain, address=address)
|
|
217
|
+
if isinstance(entity, (Token, TokenExtended)):
|
|
218
|
+
entity_type = TokenExtended
|
|
219
|
+
elif isinstance(entity, (Contract, ContractExtended)):
|
|
220
|
+
entity_type = ContractExtended
|
|
221
|
+
elif isinstance(entity, (Address, AddressExtended)):
|
|
222
|
+
entity_type = AddressExtended
|
|
223
|
+
elif entity is None:
|
|
224
|
+
# TODO: this logic should live in ypm, prob
|
|
225
|
+
entity_type = AddressExtended if not is_contract(address) else TokenExtended if is_token(address) else ContractExtended
|
|
226
|
+
else:
|
|
227
|
+
raise NotImplementedError(entity, entity_type)
|
|
228
|
+
|
|
229
|
+
if isinstance(entity, entity_type):
|
|
230
|
+
return entity
|
|
231
|
+
|
|
232
|
+
elif entity:
|
|
233
|
+
logger.debug("deleting %s", entity)
|
|
234
|
+
entity.delete()
|
|
235
|
+
commit()
|
|
236
|
+
"""
|
|
237
|
+
if entity := entities.Address.get(chain=CHAINID, address=address):
|
|
238
|
+
return entity
|
|
239
|
+
|
|
240
|
+
ensure_chain()
|
|
241
|
+
return insert(type=entity_type, chain=CHAINID, address=address) or entity_type.get(
|
|
242
|
+
chain=CHAINID, address=address
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@a_sync(default="async", executor=_address_executor)
|
|
247
|
+
@db_session_cached
|
|
248
|
+
def ensure_address(address: ChecksumAddress) -> None:
|
|
249
|
+
get_address(address, sync=True)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@a_sync(default="async", executor=_address_executor)
|
|
253
|
+
@db_session_cached
|
|
254
|
+
def ensure_addresses(*addresses: ChecksumAddress) -> None:
|
|
255
|
+
for address in addresses:
|
|
256
|
+
ensure_address(address, sync=True)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@a_sync(default="async", executor=_token_executor)
|
|
260
|
+
@robust_db_session
|
|
261
|
+
def get_token(address: ChecksumAddress) -> TokenExtended:
|
|
262
|
+
if t := TokenExtended.get(chain=CHAINID, address=address):
|
|
263
|
+
return t
|
|
264
|
+
kwargs = {}
|
|
265
|
+
if t := Address.get(chain=CHAINID, address=address):
|
|
266
|
+
if isinstance(t, TokenExtended):
|
|
267
|
+
# double check due to possible race cntdn
|
|
268
|
+
return t
|
|
269
|
+
"""
|
|
270
|
+
with suppress(TypeError):
|
|
271
|
+
if t.notes:
|
|
272
|
+
kwargs['notes'] = t.notes
|
|
273
|
+
if isinstance(t, Contract):
|
|
274
|
+
with suppress(TypeError):
|
|
275
|
+
if t.deployer:
|
|
276
|
+
kwargs['deployer'] = t.deployer
|
|
277
|
+
with suppress(TypeError):
|
|
278
|
+
if t.deploy_block:
|
|
279
|
+
kwargs['deploy_block'] = t.deploy_block
|
|
280
|
+
if isinstance(t, Token):
|
|
281
|
+
with suppress(TypeError):
|
|
282
|
+
if t.symbol:
|
|
283
|
+
kwargs['symbol'] = t.symbol
|
|
284
|
+
with suppress(TypeError):
|
|
285
|
+
if t.name:
|
|
286
|
+
kwargs['name'] = t.name
|
|
287
|
+
with suppress(TypeError):
|
|
288
|
+
if t.bucket:
|
|
289
|
+
kwargs['bucket'] = t.bucket
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
flush()
|
|
294
|
+
t.delete()
|
|
295
|
+
commit()
|
|
296
|
+
except KeyError as e:
|
|
297
|
+
raise KeyError(f"cant delete {t}") from e
|
|
298
|
+
|
|
299
|
+
ensure_chain()
|
|
300
|
+
commit()
|
|
301
|
+
return insert(
|
|
302
|
+
type=TokenExtended, chain=CHAINID, address=address, **kwargs
|
|
303
|
+
) or TokenExtended.get(chain=CHAINID, address=address)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@a_sync(default="async", executor=_token_executor)
|
|
307
|
+
@db_session_cached
|
|
308
|
+
def ensure_token(token_address: ChecksumAddress) -> None:
|
|
309
|
+
get_token(token_address, sync=True)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def get_transaction(sender: ChecksumAddress, nonce: int) -> Optional[Transaction]:
|
|
313
|
+
startup_txs = await transactions_known_at_startup(CHAINID, sender)
|
|
314
|
+
data = startup_txs.pop(nonce, None) or await __get_transaction_bytes_from_db(sender, nonce)
|
|
315
|
+
if data:
|
|
316
|
+
await _yield_to_loop()
|
|
317
|
+
return decode_transaction(data)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
_decoded = 0
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def _yield_to_loop() -> None:
|
|
324
|
+
"""dont let the event loop get congested, let your rpc begin work asap"""
|
|
325
|
+
global _decoded
|
|
326
|
+
_decoded += 1
|
|
327
|
+
if _decoded % 1000 == 0:
|
|
328
|
+
await sleep(0)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@a_sync(default="async", executor=_transaction_read_executor)
|
|
332
|
+
@robust_db_session
|
|
333
|
+
def __get_transaction_bytes_from_db(sender: ChecksumAddress, nonce: int) -> Optional[bytes]:
|
|
334
|
+
entity: entities.Transaction
|
|
335
|
+
if entity := entities.Transaction.get(from_address=(CHAINID, sender), nonce=nonce):
|
|
336
|
+
return entity.raw
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def decode_transaction(data: bytes) -> Union[Transaction, TransactionRLP]:
|
|
340
|
+
try:
|
|
341
|
+
try:
|
|
342
|
+
return json.decode(data, type=Transaction, dec_hook=_decode_hook)
|
|
343
|
+
except ValidationError as e:
|
|
344
|
+
if str(e) == "Object missing required field `type` - at `$[2]`":
|
|
345
|
+
return json.decode(data, type=TransactionRLP, dec_hook=_decode_hook)
|
|
346
|
+
raise
|
|
347
|
+
except Exception as e:
|
|
348
|
+
e.args = *e.args, json.decode(data)
|
|
349
|
+
raise
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@a_sync(default="async", executor=_transaction_write_executor)
|
|
353
|
+
@robust_db_session
|
|
354
|
+
def delete_transaction(transaction: Transaction) -> None:
|
|
355
|
+
if entity := entities.Transaction.get(**transaction.__db_primary_key__):
|
|
356
|
+
entity.delete()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def insert_transaction(transaction: Transaction) -> None:
|
|
360
|
+
# Make sure these are in the db so below we can call them and use the results all in one transaction
|
|
361
|
+
# NOTE: this create task -> await coro -> await task pattern is faster than a 2-task gather
|
|
362
|
+
block_task = create_task(ensure_block(transaction.block_number))
|
|
363
|
+
if to_address := transaction.to_address:
|
|
364
|
+
await ensure_addresses(to_address, transaction.from_address)
|
|
365
|
+
else:
|
|
366
|
+
await ensure_address(transaction.from_address)
|
|
367
|
+
await block_task
|
|
368
|
+
await _insert_transaction(transaction)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@a_sync(default="async", executor=_transaction_write_executor)
|
|
372
|
+
@requery_objs_on_diff_tx_err
|
|
373
|
+
@robust_db_session
|
|
374
|
+
def _insert_transaction(transaction: Transaction) -> None:
|
|
375
|
+
with reraise_excs_with_extra_context(transaction):
|
|
376
|
+
entities.Transaction(
|
|
377
|
+
**transaction.__db_primary_key__,
|
|
378
|
+
block=(CHAINID, transaction.block_number),
|
|
379
|
+
transaction_index=transaction.transaction_index,
|
|
380
|
+
hash=transaction.hash.hex(),
|
|
381
|
+
to_address=(CHAINID, transaction.to_address) if transaction.to_address else None,
|
|
382
|
+
value=transaction.value,
|
|
383
|
+
price=transaction.price,
|
|
384
|
+
value_usd=transaction.value_usd,
|
|
385
|
+
type=getattr(transaction, "type", None),
|
|
386
|
+
gas=transaction.gas,
|
|
387
|
+
gas_price=transaction.gas_price,
|
|
388
|
+
max_fee_per_gas=getattr(transaction, "max_fee_per_gas", None),
|
|
389
|
+
max_priority_fee_per_gas=getattr(transaction, "max_priority_fee_per_gas", None),
|
|
390
|
+
raw=json.encode(transaction, enc_hook=enc_hook),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@a_sync(default="async", executor=_internal_transfer_read_executor)
|
|
395
|
+
@robust_db_session
|
|
396
|
+
def get_internal_transfer(trace: evmspec.FilterTrace) -> Optional[InternalTransfer]:
|
|
397
|
+
block = trace.blockNumber
|
|
398
|
+
entity: entities.InternalTransfer
|
|
399
|
+
if entity := entities.InternalTransfer.get(
|
|
400
|
+
block=(CHAINID, block),
|
|
401
|
+
transaction_index=trace.transactionPosition,
|
|
402
|
+
hash=trace.transactionHash,
|
|
403
|
+
type=trace.type.name,
|
|
404
|
+
call_type=trace.callType,
|
|
405
|
+
from_address=(CHAINID, trace.sender),
|
|
406
|
+
to_address=(CHAINID, trace.to),
|
|
407
|
+
value=trace.value.scaled,
|
|
408
|
+
trace_address=(CHAINID, trace.traceAddress),
|
|
409
|
+
gas=trace.gas,
|
|
410
|
+
gas_used=trace.gasUsed if "gasUsed" in trace else None,
|
|
411
|
+
input=trace.input,
|
|
412
|
+
output=trace.output,
|
|
413
|
+
subtraces=trace.subtraces,
|
|
414
|
+
address=(CHAINID, trace.address),
|
|
415
|
+
):
|
|
416
|
+
return json.decode(entity.raw, type=InternalTransfer, dec_hook=_decode_hook)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@a_sync(default="async", executor=_internal_transfer_write_executor)
|
|
420
|
+
@robust_db_session
|
|
421
|
+
def delete_internal_transfer(transfer: InternalTransfer) -> None:
|
|
422
|
+
if entity := entities.InternalTransfer.get(
|
|
423
|
+
block=(CHAINID, transfer.block_number),
|
|
424
|
+
transaction_index=transfer.transaction_index,
|
|
425
|
+
hash=transfer.hash,
|
|
426
|
+
type=transfer.type,
|
|
427
|
+
call_type=transfer.call_type,
|
|
428
|
+
from_address=(CHAINID, transfer.from_address),
|
|
429
|
+
to_address=(CHAINID, transfer.to_address),
|
|
430
|
+
value=transfer.value,
|
|
431
|
+
trace_address=(CHAINID, transfer.trace_address),
|
|
432
|
+
gas=transfer.gas,
|
|
433
|
+
gas_used=transfer.gas_used,
|
|
434
|
+
input=transfer.input,
|
|
435
|
+
output=transfer.output,
|
|
436
|
+
subtraces=transfer.subtraces,
|
|
437
|
+
address=(CHAINID, transfer.address),
|
|
438
|
+
):
|
|
439
|
+
entity.delete()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
async def insert_internal_transfer(transfer: InternalTransfer) -> None:
|
|
443
|
+
# NOTE: this create task -> await coro -> await task pattern is faster than a 2-task gather
|
|
444
|
+
block_task = create_task(ensure_block(transfer.block_number))
|
|
445
|
+
if to_address := getattr(transfer, "to_address", None):
|
|
446
|
+
await ensure_addresses(to_address, transfer.from_address)
|
|
447
|
+
else:
|
|
448
|
+
await ensure_address(transfer.from_address)
|
|
449
|
+
await block_task
|
|
450
|
+
await _insert_internal_transfer(transfer)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@a_sync(default="async", executor=_internal_transfer_write_executor)
|
|
454
|
+
@robust_db_session
|
|
455
|
+
def _insert_internal_transfer(transfer: InternalTransfer) -> None:
|
|
456
|
+
entities.InternalTransfer(
|
|
457
|
+
block=(CHAINID, transfer.block_number),
|
|
458
|
+
transaction_index=transfer.transaction_index,
|
|
459
|
+
hash=transfer.hash,
|
|
460
|
+
type=transfer.type,
|
|
461
|
+
call_type=transfer.call_type,
|
|
462
|
+
from_address=(CHAINID, transfer.from_address),
|
|
463
|
+
to_address=(CHAINID, transfer.to_address),
|
|
464
|
+
value=transfer.value,
|
|
465
|
+
price=transfer.price,
|
|
466
|
+
value_usd=transfer.value_usd,
|
|
467
|
+
trace_address=str(transfer.trace_address),
|
|
468
|
+
gas=transfer.gas,
|
|
469
|
+
gas_used=transfer.gas_used,
|
|
470
|
+
raw=json.encode(transfer, enc_hook=enc_hook),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
async def get_token_transfer(transfer: evmspec.Log) -> Optional[TokenTransfer]:
|
|
475
|
+
pk = {
|
|
476
|
+
"block": (CHAINID, transfer.blockNumber),
|
|
477
|
+
"transaction_index": transfer.transactionIndex,
|
|
478
|
+
"log_index": transfer.logIndex,
|
|
479
|
+
}
|
|
480
|
+
startup_xfers = await token_transfers_known_at_startup(CHAINID)
|
|
481
|
+
data = startup_xfers.pop(tuple(pk.values()), None) or await __get_token_transfer_bytes_from_db(
|
|
482
|
+
pk
|
|
483
|
+
)
|
|
484
|
+
if data:
|
|
485
|
+
await _yield_to_loop()
|
|
486
|
+
with reraise_excs_with_extra_context(data):
|
|
487
|
+
return json.decode(data, type=TokenTransfer, dec_hook=_decode_hook)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@a_sync(default="async", executor=_token_transfer_read_executor)
|
|
491
|
+
@robust_db_session
|
|
492
|
+
def __get_token_transfer_bytes_from_db(pk: dict) -> Optional[bytes]:
|
|
493
|
+
entity: entities.TokenTransfer
|
|
494
|
+
if entity := entities.TokenTransfer.get(**pk):
|
|
495
|
+
return entity.raw
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
_TPK = Tuple[Tuple[int, ChecksumAddress], int]
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@a_sync(default="async", executor=_transaction_read_executor, ram_cache_maxsize=None)
|
|
502
|
+
@robust_db_session
|
|
503
|
+
def transactions_known_at_startup(chainid: int, from_address: ChecksumAddress) -> Dict[_TPK, bytes]:
|
|
504
|
+
return dict(
|
|
505
|
+
select(
|
|
506
|
+
(t.nonce, t.raw)
|
|
507
|
+
for t in entities.Transaction # type: ignore [attr-defined]
|
|
508
|
+
if t.from_address.chain.id == chainid and t.from_address.address == from_address
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
_TokenTransferPK = Tuple[Tuple[int, int], int, int]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@a_sync(default="async", executor=_transaction_read_executor, ram_cache_maxsize=None)
|
|
517
|
+
@robust_db_session
|
|
518
|
+
def token_transfers_known_at_startup(chainid: int) -> Dict[_TokenTransferPK, bytes]:
|
|
519
|
+
block: int
|
|
520
|
+
tx_index: int
|
|
521
|
+
log_index: int
|
|
522
|
+
raw: bytes
|
|
523
|
+
|
|
524
|
+
transfers = {}
|
|
525
|
+
for block, tx_index, log_index, raw in select(
|
|
526
|
+
(t.block.number, t.transaction_index, t.log_index, t.raw)
|
|
527
|
+
for t in entities.TokenTransfer # type: ignore [attr-defined]
|
|
528
|
+
if t.block.chain.id == chainid
|
|
529
|
+
):
|
|
530
|
+
pk = ((chainid, block), tx_index, log_index)
|
|
531
|
+
transfers[pk] = raw
|
|
532
|
+
return transfers
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@a_sync(default="async", executor=_token_transfer_write_executor)
|
|
536
|
+
@robust_db_session
|
|
537
|
+
def delete_token_transfer(token_transfer: TokenTransfer) -> None:
|
|
538
|
+
if entity := entities.TokenTransfer.get(
|
|
539
|
+
block=(CHAINID, token_transfer.block_number),
|
|
540
|
+
transaction_index=token_transfer.transaction_index,
|
|
541
|
+
log_index=token_transfer.log_index,
|
|
542
|
+
):
|
|
543
|
+
entity.delete()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
async def insert_token_transfer(token_transfer: TokenTransfer) -> None:
|
|
547
|
+
# two tasks and a coroutine like this should be faster than gather
|
|
548
|
+
block_task = create_task(ensure_block(token_transfer.block_number))
|
|
549
|
+
while True:
|
|
550
|
+
try:
|
|
551
|
+
token_task = create_task(ensure_token(token_transfer.token_address))
|
|
552
|
+
except KeyError:
|
|
553
|
+
# This KeyError comes from a bug in cachetools.ttl_cache
|
|
554
|
+
# TODO: move this handler into evmspec
|
|
555
|
+
pass
|
|
556
|
+
else:
|
|
557
|
+
break
|
|
558
|
+
|
|
559
|
+
while True:
|
|
560
|
+
try:
|
|
561
|
+
address_coro = ensure_addresses(token_transfer.to_address, token_transfer.from_address)
|
|
562
|
+
except KeyError:
|
|
563
|
+
# This KeyError comes from a bug in cachetools.ttl_cache
|
|
564
|
+
# TODO: move this handler into evmspec
|
|
565
|
+
pass
|
|
566
|
+
else:
|
|
567
|
+
break
|
|
568
|
+
|
|
569
|
+
await address_coro
|
|
570
|
+
await block_task
|
|
571
|
+
await token_task
|
|
572
|
+
await _insert_token_transfer(token_transfer)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@a_sync(default="async", executor=_token_transfer_write_executor)
|
|
576
|
+
@requery_objs_on_diff_tx_err
|
|
577
|
+
@robust_db_session
|
|
578
|
+
def _insert_token_transfer(token_transfer: TokenTransfer) -> None:
|
|
579
|
+
try:
|
|
580
|
+
entities.TokenTransfer(
|
|
581
|
+
block=(CHAINID, token_transfer.block_number),
|
|
582
|
+
transaction_index=token_transfer.transaction_index,
|
|
583
|
+
log_index=token_transfer.log_index,
|
|
584
|
+
hash=token_transfer.hash.hex(),
|
|
585
|
+
token=(CHAINID, token_transfer.token_address),
|
|
586
|
+
from_address=(CHAINID, token_transfer.from_address),
|
|
587
|
+
to_address=(CHAINID, token_transfer.to_address),
|
|
588
|
+
value=token_transfer.value,
|
|
589
|
+
price=token_transfer.price,
|
|
590
|
+
value_usd=token_transfer.value_usd,
|
|
591
|
+
raw=json.encode(token_transfer, enc_hook=enc_hook),
|
|
592
|
+
)
|
|
593
|
+
commit()
|
|
594
|
+
except TransactionIntegrityError:
|
|
595
|
+
pass # most likely non-issue, debug later if needed
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def enc_hook(obj: Any) -> Any:
|
|
599
|
+
try:
|
|
600
|
+
return y._db.common.enc_hook(obj)
|
|
601
|
+
except TypeError:
|
|
602
|
+
if type(obj) is Decimal:
|
|
603
|
+
return obj.jsonify()
|
|
604
|
+
raise TypeError(type(obj), obj) from None
|