eth-portfolio 0.5.8__cp310-cp310-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 might be problematic. Click here for more details.

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