eth-portfolio 0.5.7__cp312-cp312-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 might be problematic. Click here for more details.

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