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