eth-portfolio-temp 0.2.4.dev0__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.

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