eth-portfolio-temp 0.2.12__cp313-cp313-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of eth-portfolio-temp might be problematic. Click here for more details.
- eth_portfolio/__init__.py +25 -0
- eth_portfolio/_argspec.cp313-win32.pyd +0 -0
- eth_portfolio/_argspec.py +42 -0
- eth_portfolio/_cache.py +121 -0
- eth_portfolio/_config.cp313-win32.pyd +0 -0
- eth_portfolio/_config.py +4 -0
- eth_portfolio/_db/__init__.py +0 -0
- eth_portfolio/_db/decorators.py +147 -0
- eth_portfolio/_db/entities.py +311 -0
- eth_portfolio/_db/utils.py +604 -0
- eth_portfolio/_decimal.py +156 -0
- eth_portfolio/_decorators.py +84 -0
- eth_portfolio/_exceptions.py +67 -0
- eth_portfolio/_ledgers/__init__.py +0 -0
- eth_portfolio/_ledgers/address.py +938 -0
- eth_portfolio/_ledgers/portfolio.py +327 -0
- eth_portfolio/_loaders/__init__.py +33 -0
- eth_portfolio/_loaders/_nonce.cp313-win32.pyd +0 -0
- eth_portfolio/_loaders/_nonce.py +196 -0
- eth_portfolio/_loaders/balances.cp313-win32.pyd +0 -0
- eth_portfolio/_loaders/balances.py +94 -0
- eth_portfolio/_loaders/token_transfer.py +217 -0
- eth_portfolio/_loaders/transaction.py +240 -0
- eth_portfolio/_loaders/utils.cp313-win32.pyd +0 -0
- eth_portfolio/_loaders/utils.py +68 -0
- eth_portfolio/_shitcoins.cp313-win32.pyd +0 -0
- eth_portfolio/_shitcoins.py +329 -0
- eth_portfolio/_stableish.cp313-win32.pyd +0 -0
- eth_portfolio/_stableish.py +42 -0
- eth_portfolio/_submodules.py +73 -0
- eth_portfolio/_utils.py +225 -0
- eth_portfolio/_ydb/__init__.py +0 -0
- eth_portfolio/_ydb/token_transfers.py +145 -0
- eth_portfolio/address.py +397 -0
- eth_portfolio/buckets.py +194 -0
- eth_portfolio/constants.cp313-win32.pyd +0 -0
- eth_portfolio/constants.py +82 -0
- eth_portfolio/portfolio.py +661 -0
- eth_portfolio/protocols/__init__.py +67 -0
- eth_portfolio/protocols/_base.py +108 -0
- eth_portfolio/protocols/convex.py +17 -0
- eth_portfolio/protocols/dsr.py +51 -0
- eth_portfolio/protocols/lending/README.md +6 -0
- eth_portfolio/protocols/lending/__init__.py +50 -0
- eth_portfolio/protocols/lending/_base.py +57 -0
- eth_portfolio/protocols/lending/compound.py +187 -0
- eth_portfolio/protocols/lending/liquity.py +110 -0
- eth_portfolio/protocols/lending/maker.py +104 -0
- eth_portfolio/protocols/lending/unit.py +46 -0
- eth_portfolio/protocols/liquity.py +16 -0
- eth_portfolio/py.typed +0 -0
- eth_portfolio/structs/__init__.py +43 -0
- eth_portfolio/structs/modified.py +69 -0
- eth_portfolio/structs/structs.py +637 -0
- eth_portfolio/typing/__init__.py +1447 -0
- eth_portfolio/typing/balance/single.py +176 -0
- eth_portfolio__mypyc.cp313-win32.pyd +0 -0
- eth_portfolio_scripts/__init__.py +20 -0
- eth_portfolio_scripts/_args.py +26 -0
- eth_portfolio_scripts/_logging.py +15 -0
- eth_portfolio_scripts/_portfolio.py +194 -0
- eth_portfolio_scripts/_utils.py +106 -0
- eth_portfolio_scripts/balances.cp313-win32.pyd +0 -0
- eth_portfolio_scripts/balances.py +52 -0
- eth_portfolio_scripts/docker/.grafana/dashboards/Portfolio/Balances.json +1962 -0
- eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml +10 -0
- eth_portfolio_scripts/docker/.grafana/datasources/datasources.yml +11 -0
- eth_portfolio_scripts/docker/__init__.cp313-win32.pyd +0 -0
- eth_portfolio_scripts/docker/__init__.py +16 -0
- eth_portfolio_scripts/docker/check.cp313-win32.pyd +0 -0
- eth_portfolio_scripts/docker/check.py +56 -0
- eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
- eth_portfolio_scripts/docker/docker_compose.cp313-win32.pyd +0 -0
- eth_portfolio_scripts/docker/docker_compose.py +78 -0
- eth_portfolio_scripts/main.py +119 -0
- eth_portfolio_scripts/py.typed +1 -0
- eth_portfolio_scripts/victoria/__init__.py +73 -0
- eth_portfolio_scripts/victoria/types.py +38 -0
- eth_portfolio_temp-0.2.12.dist-info/METADATA +25 -0
- eth_portfolio_temp-0.2.12.dist-info/RECORD +83 -0
- eth_portfolio_temp-0.2.12.dist-info/WHEEL +5 -0
- eth_portfolio_temp-0.2.12.dist-info/entry_points.txt +2 -0
- eth_portfolio_temp-0.2.12.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module orchestrates the process of loading and processing token transfers within the eth_portfolio ecosystem.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import decimal
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import Final, Optional, Set
|
|
8
|
+
|
|
9
|
+
from a_sync import create_task, gather
|
|
10
|
+
from dank_mids import BlockSemaphore
|
|
11
|
+
from eth_typing import ChecksumAddress
|
|
12
|
+
from evmspec.data import TransactionIndex
|
|
13
|
+
from faster_async_lru import alru_cache
|
|
14
|
+
from msgspec import Struct, ValidationError
|
|
15
|
+
from msgspec.json import decode
|
|
16
|
+
from pony.orm import TransactionIntegrityError, UnexpectedError
|
|
17
|
+
from y import ERC20
|
|
18
|
+
from y._db.log import Log
|
|
19
|
+
from y._decorators import stuck_coro_debugger
|
|
20
|
+
from y.exceptions import NonStandardERC20, reraise_excs_with_extra_context
|
|
21
|
+
|
|
22
|
+
from eth_portfolio._cache import cache_to_disk
|
|
23
|
+
from eth_portfolio._db import utils as db
|
|
24
|
+
from eth_portfolio._decimal import Decimal
|
|
25
|
+
from eth_portfolio._loaders.utils import get_transaction_receipt
|
|
26
|
+
from eth_portfolio._utils import _get_price
|
|
27
|
+
from eth_portfolio.structs import TokenTransfer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
token_transfer_semaphore: Final = BlockSemaphore(
|
|
33
|
+
20_000, # Some arbitrary number
|
|
34
|
+
name="eth_portfolio.token_transfers",
|
|
35
|
+
)
|
|
36
|
+
"""A semaphore that regulates the concurrent processing of token transfers by processing lower blocks first."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@stuck_coro_debugger
|
|
40
|
+
async def load_token_transfer(
|
|
41
|
+
transfer_log: "Log", load_prices: bool
|
|
42
|
+
) -> Optional[TokenTransfer]: # sourcery skip: simplify-boolean-comparison
|
|
43
|
+
"""
|
|
44
|
+
Processes and loads a token transfer from a log entry, with comprehensive error handling and optional price fetching.
|
|
45
|
+
|
|
46
|
+
This function employs a multi-step process:
|
|
47
|
+
1. Validates the transfer against a known set of 'shitcoins', skipping processing if matched.
|
|
48
|
+
2. Checks for existing database entries, potentially deleting and reprocessing if price data is requested but missing.
|
|
49
|
+
3. Decodes the transfer log and retrieves associated metadata (e.g., token scale, symbol, transaction index).
|
|
50
|
+
4. Optionally fetches the token price at the time of the transaction.
|
|
51
|
+
5. Constructs and persists a :class:`~eth_portfolio.structs.TokenTransfer` object in the database.
|
|
52
|
+
|
|
53
|
+
The function handles various exceptions, including :class:`~y.exceptions.NonStandardERC20` for non-compliant tokens and :class:`decimal.InvalidOperation` for extreme :class:`~Decimal` values.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
transfer_log: A dictionary containing the raw log entry of the token transfer.
|
|
57
|
+
load_prices: A flag indicating whether to fetch and include price data for the token at the time of transfer.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A processed TokenTransfer object if successful, or None if the transfer cannot be processed due to various constraints or errors.
|
|
61
|
+
|
|
62
|
+
Note:
|
|
63
|
+
This function employs caching mechanisms and database operations to optimize performance.
|
|
64
|
+
"""
|
|
65
|
+
if transfer_log.removed:
|
|
66
|
+
if transfer := await db.get_token_transfer(transfer_log):
|
|
67
|
+
await db.delete_token_transfer(transfer)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
if transfer := await db.get_token_transfer(transfer_log):
|
|
71
|
+
if load_prices is False or transfer.price:
|
|
72
|
+
return transfer
|
|
73
|
+
await db.delete_token_transfer(transfer)
|
|
74
|
+
|
|
75
|
+
token_address: ChecksumAddress = transfer_log.address
|
|
76
|
+
if token_address in _non_standard_erc20:
|
|
77
|
+
logger.debug("%s is not a standard ERC20 token, skipping.", token_address)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
async with token_transfer_semaphore[transfer_log.block]:
|
|
81
|
+
token = ERC20(token_address, asynchronous=True)
|
|
82
|
+
try:
|
|
83
|
+
try:
|
|
84
|
+
# This will be mem cached so no need to gather and add a bunch of overhead
|
|
85
|
+
scale = await token.scale
|
|
86
|
+
except NonStandardERC20 as e:
|
|
87
|
+
# NOTE: if we cant fetch scale, this is probably either a shitcoin or an NFT (which we don't support at this time)
|
|
88
|
+
logger.debug("%s for %s, skipping.", e, transfer_log)
|
|
89
|
+
_non_standard_erc20.add(token_address)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# This will be mem cached so no need to include it in the gather and add a bunch of overhead
|
|
93
|
+
symbol = await get_symbol(token)
|
|
94
|
+
|
|
95
|
+
tx_index_coro = get_transaction_index(transfer_log.transactionHash.hex())
|
|
96
|
+
coro_results = {"token": symbol}
|
|
97
|
+
|
|
98
|
+
if load_prices:
|
|
99
|
+
coro_results.update(
|
|
100
|
+
await gather(
|
|
101
|
+
{
|
|
102
|
+
"transaction_index": tx_index_coro,
|
|
103
|
+
"price": _get_price(token.address, transfer_log.blockNumber),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
coro_results["transaction_index"] = await tx_index_coro
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(
|
|
112
|
+
f"%s %s for %s %s at block %s:",
|
|
113
|
+
e.__class__.__name__,
|
|
114
|
+
e,
|
|
115
|
+
await get_symbol(token) or token.address,
|
|
116
|
+
token_address,
|
|
117
|
+
transfer_log.blockNumber,
|
|
118
|
+
)
|
|
119
|
+
logger.exception(e)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
value = Decimal(transfer_log.data.as_uint256) / scale
|
|
123
|
+
|
|
124
|
+
if price := coro_results.get("price"):
|
|
125
|
+
coro_results["value_usd"] = round(value * price, 18)
|
|
126
|
+
|
|
127
|
+
transfer = TokenTransfer(log=transfer_log, value=value, **coro_results)
|
|
128
|
+
|
|
129
|
+
create_task(
|
|
130
|
+
_insert_to_db(transfer, load_prices),
|
|
131
|
+
skip_gc_until_done=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return transfer
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _insert_to_db(transfer: TokenTransfer, load_prices: bool) -> None:
|
|
138
|
+
with reraise_excs_with_extra_context(transfer):
|
|
139
|
+
try:
|
|
140
|
+
await db.insert_token_transfer(transfer)
|
|
141
|
+
except TransactionIntegrityError:
|
|
142
|
+
if load_prices:
|
|
143
|
+
await db.delete_token_transfer(transfer)
|
|
144
|
+
await db.insert_token_transfer(transfer)
|
|
145
|
+
except UnexpectedError:
|
|
146
|
+
digits_before_decimal = str(transfer.value).split(".")[0]
|
|
147
|
+
if len(digits_before_decimal) <= 20:
|
|
148
|
+
raise
|
|
149
|
+
except decimal.InvalidOperation as e:
|
|
150
|
+
# TODO: debug why this happens sometimes
|
|
151
|
+
logger.exception("%s %s", e, transfer)
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_non_standard_erc20: Final[Set[ChecksumAddress]] = set()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@stuck_coro_debugger
|
|
159
|
+
async def get_symbol(token: ERC20) -> Optional[str]:
|
|
160
|
+
"""
|
|
161
|
+
Retrieves the symbol of a given ERC20 token, with error handling for non-standard implementations.
|
|
162
|
+
|
|
163
|
+
This function attempts to access the token's symbol through the standard ERC20 symbol method. If the token contract
|
|
164
|
+
does not adhere to the standard ERC20 interface, indicated by a :class:`~y.exceptions.NonStandardERC20` exception, the function
|
|
165
|
+
returns `None` instead of propagating the error.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
token: An ERC20 token object representing the token whose symbol is to be retrieved.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The token's symbol as a string if successfully retrieved, or None if the token does not implement a standard symbol method.
|
|
172
|
+
"""
|
|
173
|
+
if token.address in _non_standard_erc20:
|
|
174
|
+
return None
|
|
175
|
+
try:
|
|
176
|
+
return await token.__symbol__
|
|
177
|
+
except NonStandardERC20:
|
|
178
|
+
_non_standard_erc20.add(token.address)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@alru_cache(ttl=60 * 60)
|
|
183
|
+
@stuck_coro_debugger
|
|
184
|
+
@cache_to_disk
|
|
185
|
+
async def get_transaction_index(hash: str) -> int:
|
|
186
|
+
"""
|
|
187
|
+
Retrieves the transaction index for a given transaction hash, with results cached to disk.
|
|
188
|
+
|
|
189
|
+
This function fetches the transaction receipt corresponding to the provided hash and extracts the transaction index,
|
|
190
|
+
which represents the position of the transaction within its containing block. The result is cached to disk to
|
|
191
|
+
optimize performance for future runs.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
hash: The hexadecimal string representation of the transaction hash.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
The zero-based index of the transaction within its block.
|
|
198
|
+
"""
|
|
199
|
+
while True:
|
|
200
|
+
receipt_bytes = await get_transaction_receipt(hash)
|
|
201
|
+
if receipt_bytes is not None:
|
|
202
|
+
# TODO: debug why this happens, its something inside of dank_mids
|
|
203
|
+
break
|
|
204
|
+
logger.info("get_transaction_index failed, retrying...")
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
return decode(
|
|
208
|
+
receipt_bytes, type=HasTxIndex, dec_hook=TransactionIndex._decode_hook
|
|
209
|
+
).transactionIndex
|
|
210
|
+
except ValidationError as e:
|
|
211
|
+
new = TypeError(e, receipt_bytes, decode(receipt_bytes))
|
|
212
|
+
logger.exception(new)
|
|
213
|
+
raise new from e
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class HasTxIndex(Struct):
|
|
217
|
+
transactionIndex: TransactionIndex
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains loader functions that facilitate the retrieval and loading of blockchain transaction data.
|
|
3
|
+
|
|
4
|
+
The functions defined here use various async operations to retrieve transaction data from Ethereum-like blockchains, and process them, to then store them in a local database. The module leverages utilities like retry mechanisms, caching, and efficient async processing to handle potentially large amounts of blockchain data.
|
|
5
|
+
|
|
6
|
+
The primary focus of this module is to support eth-portfolio's internal operations such as loading transactions by address and nonce, retrieving transaction details from specific blocks, and managing transaction-related data.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from logging import getLogger
|
|
10
|
+
from typing import Awaitable, Callable, Final, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import a_sync
|
|
13
|
+
import dank_mids
|
|
14
|
+
import eth_retry
|
|
15
|
+
import evmspec
|
|
16
|
+
import msgspec
|
|
17
|
+
from a_sync import SmartProcessingQueue
|
|
18
|
+
from eth_typing import ChecksumAddress
|
|
19
|
+
from evmspec import data
|
|
20
|
+
from faster_async_lru import alru_cache
|
|
21
|
+
from pony.orm import TransactionIntegrityError
|
|
22
|
+
from y import get_price
|
|
23
|
+
from y._decorators import stuck_coro_debugger
|
|
24
|
+
from y.constants import EEE_ADDRESS
|
|
25
|
+
from y.datatypes import Block
|
|
26
|
+
from y.exceptions import reraise_excs_with_extra_context
|
|
27
|
+
from y.utils.events import decode_logs
|
|
28
|
+
|
|
29
|
+
from eth_portfolio.structs import structs
|
|
30
|
+
from eth_portfolio._cache import cache_to_disk
|
|
31
|
+
from eth_portfolio._db import utils as db
|
|
32
|
+
from eth_portfolio._decimal import Decimal
|
|
33
|
+
from eth_portfolio._loaders._nonce import Nonce, get_block_for_nonce
|
|
34
|
+
from eth_portfolio._loaders._nonce import get_nonce_at_block as _get_nonce_at_block
|
|
35
|
+
from eth_portfolio._loaders.utils import get_transaction_receipt
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Transactions = List[evmspec.Transaction]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
logger: Final = getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@eth_retry.auto_retry
|
|
45
|
+
@stuck_coro_debugger
|
|
46
|
+
async def load_transaction(
|
|
47
|
+
address: ChecksumAddress, nonce: Nonce, load_prices: bool
|
|
48
|
+
) -> Tuple[Nonce, Optional[structs.Transaction]]:
|
|
49
|
+
"""
|
|
50
|
+
Loads a transaction by address and nonce.
|
|
51
|
+
|
|
52
|
+
This function attempts to load a transaction from the local database based on the given address and nonce.
|
|
53
|
+
|
|
54
|
+
If the transaction is not found in the local database or if price data is missing but requested by the user, it retrieves the transaction data from the blockchain using a binary search approach, processes it, and stores it back in the local database.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
address: The address of the account involved in the transaction.
|
|
58
|
+
nonce: The nonce associated with the transaction.
|
|
59
|
+
load_prices: Whether to load and calculate price-related information for the transaction.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A tuple containing the nonce and the retrieved or newly created :class:`~eth_portfolio.structs.Transaction` object, or None if the transaction could not be found.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> print(await load_transaction(address="0x1234567890abcdef1234567890abcdef12345678", nonce=5, load_prices=True))
|
|
66
|
+
(5, Transaction(...))
|
|
67
|
+
"""
|
|
68
|
+
if transaction := await db.get_transaction(address, nonce):
|
|
69
|
+
if load_prices and transaction.price is None:
|
|
70
|
+
await db.delete_transaction(transaction)
|
|
71
|
+
else:
|
|
72
|
+
return nonce, transaction
|
|
73
|
+
|
|
74
|
+
block = await get_block_for_nonce(address, nonce)
|
|
75
|
+
tx = await get_transaction_by_nonce_and_block(address, nonce, block)
|
|
76
|
+
if tx is None:
|
|
77
|
+
return nonce, None
|
|
78
|
+
|
|
79
|
+
if load_prices:
|
|
80
|
+
# TODO: debug why `tx.value` isnt already a Wei obj
|
|
81
|
+
scaled = data.Wei(tx.value).scaled
|
|
82
|
+
price = Decimal(await get_price(EEE_ADDRESS, block=tx.blockNumber, sync=False))
|
|
83
|
+
transaction = structs.Transaction.from_rpc_response(
|
|
84
|
+
tx, price=round(price, 18), value_usd=round(scaled * price, 18)
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
transaction = structs.Transaction.from_rpc_response(tx)
|
|
88
|
+
|
|
89
|
+
a_sync.create_task(
|
|
90
|
+
coro=_insert_to_db(transaction, load_prices),
|
|
91
|
+
skip_gc_until_done=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return nonce, transaction
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def _insert_to_db(transaction: structs.Transaction, load_prices: bool) -> None:
|
|
98
|
+
with reraise_excs_with_extra_context(transaction):
|
|
99
|
+
try:
|
|
100
|
+
await db.insert_transaction(transaction)
|
|
101
|
+
except TransactionIntegrityError:
|
|
102
|
+
if load_prices:
|
|
103
|
+
await db.delete_transaction(transaction)
|
|
104
|
+
await db.insert_transaction(transaction)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@eth_retry.auto_retry
|
|
108
|
+
@stuck_coro_debugger
|
|
109
|
+
@cache_to_disk
|
|
110
|
+
async def get_transaction_by_nonce_and_block(
|
|
111
|
+
address: ChecksumAddress, nonce: int, block: Block
|
|
112
|
+
) -> Optional[evmspec.Transaction]:
|
|
113
|
+
"""
|
|
114
|
+
This function retrieves a transaction for a specifified address by its nonce and block, if any match.
|
|
115
|
+
|
|
116
|
+
It also handles special cases:
|
|
117
|
+
|
|
118
|
+
1. Contract creation transactions where the transaction's 'to' field is None.
|
|
119
|
+
2. Gnosis Safe deployments where the transaction's 'to' field is a specific address.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
address: The addresses of the accounts that sent the transaction.
|
|
123
|
+
nonce: The nonce associated with the transaction creator.
|
|
124
|
+
block: The block number from which to retrieve the transactions.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The transaction data if found, otherwise None.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> transaction = await get_transaction_by_nonce_and_block(address="0x1234567890abcdef1234567890abcdef12345678", nonce=5, block=1234567)
|
|
131
|
+
>>> if transaction:
|
|
132
|
+
... print(transaction.hash)
|
|
133
|
+
"""
|
|
134
|
+
for tx in await get_block_transactions(block):
|
|
135
|
+
if tx.sender == address and tx.nonce == nonce:
|
|
136
|
+
return tx
|
|
137
|
+
|
|
138
|
+
receipt_bytes: msgspec.Raw
|
|
139
|
+
# Special handler for contract creation transactions
|
|
140
|
+
if tx.to is None:
|
|
141
|
+
receipt_bytes = await get_transaction_receipt(tx.hash.hex())
|
|
142
|
+
receipt_0 = msgspec.json.decode(
|
|
143
|
+
receipt_bytes,
|
|
144
|
+
type=ReceiptContractAddress,
|
|
145
|
+
dec_hook=data.Address._decode_hook,
|
|
146
|
+
)
|
|
147
|
+
if receipt_0.contractAddress == address:
|
|
148
|
+
return tx
|
|
149
|
+
# Special handler for Gnosis Safe deployments
|
|
150
|
+
elif tx.to == "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2":
|
|
151
|
+
receipt_bytes = await get_transaction_receipt(tx.hash.hex())
|
|
152
|
+
receipt_1 = msgspec.json.decode(
|
|
153
|
+
receipt_bytes,
|
|
154
|
+
type=ReceiptLogs,
|
|
155
|
+
dec_hook=data._decode_hook,
|
|
156
|
+
)
|
|
157
|
+
events = decode_logs(receipt_1.logs)
|
|
158
|
+
if (
|
|
159
|
+
"SafeSetup" in events
|
|
160
|
+
and "ProxyCreation" in events
|
|
161
|
+
and any(event["proxy"] == address for event in events["ProxyCreation"])
|
|
162
|
+
):
|
|
163
|
+
return tx
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ReceiptContractAddress(msgspec.Struct):
|
|
168
|
+
"""We only decode what we need and immediately discard the rest of the receipt."""
|
|
169
|
+
|
|
170
|
+
contractAddress: data.Address
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ReceiptLogs(msgspec.Struct):
|
|
174
|
+
logs: List[evmspec.Log]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
get_transaction_count: Final = dank_mids.eth.get_transaction_count
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@alru_cache(maxsize=None)
|
|
181
|
+
@eth_retry.auto_retry
|
|
182
|
+
@stuck_coro_debugger
|
|
183
|
+
@cache_to_disk
|
|
184
|
+
async def get_nonce_at_block(address: ChecksumAddress, block: Block) -> int:
|
|
185
|
+
"""
|
|
186
|
+
Retrieves the nonce of an address at a specific block.
|
|
187
|
+
|
|
188
|
+
This function gets the transaction count (nonce) for the given address at the specified block. It also includes a special case to handle known issues on certain networks like Arbitrum.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
address: The address of the account.
|
|
192
|
+
block: The block number at which to retrieve the nonce.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The nonce of the address at the given block.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> block = 12345678
|
|
199
|
+
>>> nonce = await get_nonce_at_block("0x1234567890abcdef1234567890abcdef12345678", block)
|
|
200
|
+
>>> print(f"The nonce at block {block} is {nonce}.")
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
return await _get_nonce_at_block(address, block)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
_get_block_transactions: Final[Callable[[Block], Awaitable[Transactions]]] = alru_cache(
|
|
207
|
+
ttl=60 * 60
|
|
208
|
+
)(eth_retry.auto_retry(stuck_coro_debugger(dank_mids.eth.get_transactions)))
|
|
209
|
+
"""
|
|
210
|
+
Retrieves all transactions from a specific block.
|
|
211
|
+
|
|
212
|
+
This function fetches the full transaction data for a block using async caching to optimize repeated requests.
|
|
213
|
+
|
|
214
|
+
The cache has a time-to-live (TTL) of 1 hour to avoid memory leaks for services or long-running scripts.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
block: The block number from which to retrieve the transactions.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
A list of transaction data objects from the block.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> transactions = await _get_block_transactions(block=12345678)
|
|
224
|
+
>>> [print(tx.hash) for tx in transactions]
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
get_block_transactions: Final[SmartProcessingQueue[Block, [], Transactions]] = SmartProcessingQueue(
|
|
229
|
+
_get_block_transactions, 1_000
|
|
230
|
+
)
|
|
231
|
+
"""
|
|
232
|
+
A smart processing queue that retrieves transactions from blocks with managable concurrency.
|
|
233
|
+
|
|
234
|
+
This queue processes the retrieval of block transactions using a limited number of workers to handle potentially overwhelming volume of transactions.
|
|
235
|
+
It wraps the _get_block_transactions function to provide efficient concurrent processing.
|
|
236
|
+
|
|
237
|
+
Example:
|
|
238
|
+
>>> transactions = await get_block_transactions.process(block=12345678)
|
|
239
|
+
>>> [print(tx['hash']) for tx in transactions]
|
|
240
|
+
"""
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
import dank_mids
|
|
4
|
+
import eth_retry
|
|
5
|
+
import msgspec
|
|
6
|
+
from a_sync import SmartProcessingQueue
|
|
7
|
+
from eth_typing import HexStr
|
|
8
|
+
from faster_async_lru import alru_cache
|
|
9
|
+
from y._decorators import stuck_coro_debugger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Raw: Final = msgspec.Raw
|
|
13
|
+
TxReceiptQueue = SmartProcessingQueue[HexStr, [], msgspec.Raw]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@eth_retry.auto_retry(min_sleep_time=1, max_sleep_time=3, max_retries=20, suppress_logs=1)
|
|
17
|
+
@alru_cache(maxsize=None, ttl=60 * 60)
|
|
18
|
+
@stuck_coro_debugger
|
|
19
|
+
async def _get_transaction_receipt(txhash: HexStr) -> msgspec.Raw:
|
|
20
|
+
"""
|
|
21
|
+
Fetches the transaction receipt for a given transaction hash.
|
|
22
|
+
|
|
23
|
+
This function retrieves the transaction receipt from the Ethereum network
|
|
24
|
+
using the provided transaction hash. It utilizes caching to store results
|
|
25
|
+
for a specified time-to-live (TTL) to improve performance and reduce
|
|
26
|
+
network calls. The function is also decorated to automatically retry
|
|
27
|
+
in case of failures and to debug if the coroutine gets stuck.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
txhash: The transaction hash for which to retrieve the receipt.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
msgspec.Raw: The raw transaction receipt data.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
>>> txhash = "0x1234567890abcdef..."
|
|
37
|
+
>>> receipt = await _get_transaction_receipt(txhash)
|
|
38
|
+
>>> print(receipt)
|
|
39
|
+
|
|
40
|
+
See Also:
|
|
41
|
+
- :func:`eth_retry.auto_retry`: For automatic retry logic.
|
|
42
|
+
- :func:`async_lru.alru_cache`: For caching the results.
|
|
43
|
+
- :func:`y._decorators.stuck_coro_debugger`: For debugging stuck coroutines.
|
|
44
|
+
"""
|
|
45
|
+
return await __get_transaction_receipt(txhash, decode_to=Raw, decode_hook=None)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
get_transaction_receipt: Final[TxReceiptQueue] = SmartProcessingQueue(
|
|
49
|
+
_get_transaction_receipt, 5000
|
|
50
|
+
)
|
|
51
|
+
"""
|
|
52
|
+
A queue for processing transaction receipt requests.
|
|
53
|
+
|
|
54
|
+
This queue manages the processing of transaction receipt requests, allowing
|
|
55
|
+
up to 5000 concurrent requests. It uses the `_get_transaction_receipt` function
|
|
56
|
+
to fetch the receipts.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
>>> txhash = "0x1234567890abcdef..."
|
|
60
|
+
>>> receipt = await get_transaction_receipt(txhash)
|
|
61
|
+
>>> print(receipt)
|
|
62
|
+
|
|
63
|
+
See Also:
|
|
64
|
+
- :class:`a_sync.SmartProcessingQueue`: For managing asynchronous processing queues.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__get_transaction_receipt: Final = dank_mids.eth.get_transaction_receipt
|
|
Binary file
|