eth-portfolio-temp 0.2.14__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.
Files changed (83) hide show
  1. eth_portfolio/__init__.py +25 -0
  2. eth_portfolio/_argspec.cp313-win_amd64.pyd +0 -0
  3. eth_portfolio/_argspec.py +42 -0
  4. eth_portfolio/_cache.py +121 -0
  5. eth_portfolio/_config.cp313-win_amd64.pyd +0 -0
  6. eth_portfolio/_config.py +4 -0
  7. eth_portfolio/_db/__init__.py +0 -0
  8. eth_portfolio/_db/decorators.py +147 -0
  9. eth_portfolio/_db/entities.py +311 -0
  10. eth_portfolio/_db/utils.py +604 -0
  11. eth_portfolio/_decimal.py +156 -0
  12. eth_portfolio/_decorators.py +84 -0
  13. eth_portfolio/_exceptions.py +67 -0
  14. eth_portfolio/_ledgers/__init__.py +0 -0
  15. eth_portfolio/_ledgers/address.py +938 -0
  16. eth_portfolio/_ledgers/portfolio.py +327 -0
  17. eth_portfolio/_loaders/__init__.py +33 -0
  18. eth_portfolio/_loaders/_nonce.cp313-win_amd64.pyd +0 -0
  19. eth_portfolio/_loaders/_nonce.py +196 -0
  20. eth_portfolio/_loaders/balances.cp313-win_amd64.pyd +0 -0
  21. eth_portfolio/_loaders/balances.py +94 -0
  22. eth_portfolio/_loaders/token_transfer.py +217 -0
  23. eth_portfolio/_loaders/transaction.py +240 -0
  24. eth_portfolio/_loaders/utils.cp313-win_amd64.pyd +0 -0
  25. eth_portfolio/_loaders/utils.py +68 -0
  26. eth_portfolio/_shitcoins.cp313-win_amd64.pyd +0 -0
  27. eth_portfolio/_shitcoins.py +329 -0
  28. eth_portfolio/_stableish.cp313-win_amd64.pyd +0 -0
  29. eth_portfolio/_stableish.py +42 -0
  30. eth_portfolio/_submodules.py +73 -0
  31. eth_portfolio/_utils.py +225 -0
  32. eth_portfolio/_ydb/__init__.py +0 -0
  33. eth_portfolio/_ydb/token_transfers.py +145 -0
  34. eth_portfolio/address.py +397 -0
  35. eth_portfolio/buckets.py +194 -0
  36. eth_portfolio/constants.cp313-win_amd64.pyd +0 -0
  37. eth_portfolio/constants.py +82 -0
  38. eth_portfolio/portfolio.py +661 -0
  39. eth_portfolio/protocols/__init__.py +67 -0
  40. eth_portfolio/protocols/_base.py +108 -0
  41. eth_portfolio/protocols/convex.py +17 -0
  42. eth_portfolio/protocols/dsr.py +51 -0
  43. eth_portfolio/protocols/lending/README.md +6 -0
  44. eth_portfolio/protocols/lending/__init__.py +50 -0
  45. eth_portfolio/protocols/lending/_base.py +57 -0
  46. eth_portfolio/protocols/lending/compound.py +187 -0
  47. eth_portfolio/protocols/lending/liquity.py +110 -0
  48. eth_portfolio/protocols/lending/maker.py +104 -0
  49. eth_portfolio/protocols/lending/unit.py +46 -0
  50. eth_portfolio/protocols/liquity.py +16 -0
  51. eth_portfolio/py.typed +0 -0
  52. eth_portfolio/structs/__init__.py +43 -0
  53. eth_portfolio/structs/modified.py +69 -0
  54. eth_portfolio/structs/structs.py +637 -0
  55. eth_portfolio/typing/__init__.py +1447 -0
  56. eth_portfolio/typing/balance/single.py +176 -0
  57. eth_portfolio__mypyc.cp313-win_amd64.pyd +0 -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 +194 -0
  62. eth_portfolio_scripts/_utils.py +106 -0
  63. eth_portfolio_scripts/balances.cp313-win_amd64.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-win_amd64.pyd +0 -0
  69. eth_portfolio_scripts/docker/__init__.py +16 -0
  70. eth_portfolio_scripts/docker/check.cp313-win_amd64.pyd +0 -0
  71. eth_portfolio_scripts/docker/check.py +67 -0
  72. eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
  73. eth_portfolio_scripts/docker/docker_compose.cp313-win_amd64.pyd +0 -0
  74. eth_portfolio_scripts/docker/docker_compose.py +81 -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.14.dist-info/METADATA +26 -0
  80. eth_portfolio_temp-0.2.14.dist-info/RECORD +83 -0
  81. eth_portfolio_temp-0.2.14.dist-info/WHEEL +5 -0
  82. eth_portfolio_temp-0.2.14.dist-info/entry_points.txt +2 -0
  83. eth_portfolio_temp-0.2.14.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