tonutils 2.0.1b5__py3-none-any.whl → 2.0.1b6__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.
- tonutils/__meta__.py +1 -1
- tonutils/clients/__init__.py +10 -10
- tonutils/clients/adnl/balancer.py +21 -24
- tonutils/clients/adnl/client.py +21 -24
- tonutils/clients/adnl/provider/config.py +22 -7
- tonutils/clients/base.py +17 -11
- tonutils/clients/http/__init__.py +11 -11
- tonutils/clients/http/balancer.py +11 -11
- tonutils/clients/http/clients/__init__.py +10 -10
- tonutils/clients/http/clients/chainstack.py +3 -3
- tonutils/clients/http/clients/quicknode.py +2 -3
- tonutils/clients/http/clients/tatum.py +3 -3
- tonutils/clients/http/clients/tonapi.py +9 -10
- tonutils/clients/http/clients/toncenter.py +50 -23
- tonutils/clients/http/{providers → provider}/__init__.py +4 -1
- tonutils/clients/http/{providers → provider}/base.py +71 -7
- tonutils/clients/http/{providers/toncenter → provider}/models.py +43 -1
- tonutils/clients/http/{providers/tonapi/provider.py → provider/tonapi.py} +8 -8
- tonutils/clients/http/{providers/toncenter/provider.py → provider/toncenter.py} +22 -14
- tonutils/clients/limiter.py +61 -59
- tonutils/clients/protocol.py +2 -2
- tonutils/contracts/wallet/base.py +2 -3
- tonutils/contracts/wallet/messages.py +4 -8
- tonutils/tonconnect/bridge/__init__.py +0 -0
- tonutils/tonconnect/events.py +0 -0
- tonutils/tonconnect/models/__init__.py +0 -0
- tonutils/tonconnect/storage.py +0 -0
- tonutils/tonconnect/tonconnect.py +0 -0
- tonutils/tools/block_scanner/__init__.py +2 -9
- tonutils/tools/block_scanner/events.py +48 -7
- tonutils/tools/block_scanner/scanner.py +316 -222
- tonutils/tools/block_scanner/storage.py +11 -0
- tonutils/utils.py +0 -48
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b6.dist-info}/METADATA +1 -1
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b6.dist-info}/RECORD +39 -41
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b6.dist-info}/WHEEL +1 -1
- tonutils/clients/http/providers/response.py +0 -85
- tonutils/clients/http/providers/tonapi/__init__.py +0 -3
- tonutils/clients/http/providers/tonapi/models.py +0 -47
- tonutils/clients/http/providers/toncenter/__init__.py +0 -3
- tonutils/tools/block_scanner/annotations.py +0 -23
- tonutils/tools/block_scanner/dispatcher.py +0 -141
- tonutils/tools/block_scanner/traversal.py +0 -97
- tonutils/tools/block_scanner/where.py +0 -53
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b6.dist-info}/entry_points.txt +0 -0
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b6.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b6.dist-info}/top_level.txt +0 -0
|
@@ -1,138 +1,188 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import typing as t
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
|
|
5
|
-
from pytoniq_core import Transaction
|
|
6
4
|
from pytoniq_core.tl import BlockIdExt
|
|
5
|
+
from pytoniq_core.tlb.block import ExtBlkRef
|
|
7
6
|
|
|
8
7
|
from tonutils.clients import LiteBalancer, LiteClient
|
|
9
|
-
from tonutils.tools.block_scanner.annotations import (
|
|
10
|
-
BlockWhere,
|
|
11
|
-
Decorator,
|
|
12
|
-
Handler,
|
|
13
|
-
TransactionWhere,
|
|
14
|
-
TransactionsWhere,
|
|
15
|
-
)
|
|
16
|
-
from tonutils.tools.block_scanner.dispatcher import EventDispatcher
|
|
17
8
|
from tonutils.tools.block_scanner.events import (
|
|
18
9
|
BlockEvent,
|
|
19
|
-
|
|
10
|
+
ErrorEvent,
|
|
20
11
|
TransactionsEvent,
|
|
21
12
|
)
|
|
22
|
-
from tonutils.tools.block_scanner.
|
|
23
|
-
from tonutils.types import
|
|
24
|
-
|
|
13
|
+
from tonutils.tools.block_scanner.storage import BlockScannerStorageProtocol
|
|
14
|
+
from tonutils.types import MASTERCHAIN_SHARD, WorkchainID
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
ShardKey = t.Tuple[int, int]
|
|
17
|
+
SeenShardSeqno = t.Dict[ShardKey, int]
|
|
18
|
+
BlockQueue = asyncio.Queue[BlockIdExt]
|
|
29
19
|
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
OnError = t.Callable[[ErrorEvent], t.Awaitable[None]]
|
|
21
|
+
OnBlock = t.Callable[[BlockEvent], t.Awaitable[None]]
|
|
22
|
+
OnTransactions = t.Callable[[TransactionsEvent], t.Awaitable[None]]
|
|
32
23
|
|
|
33
24
|
|
|
34
25
|
class BlockScanner:
|
|
35
|
-
"""
|
|
26
|
+
"""
|
|
27
|
+
Asynchronous queue-based TON block scanner.
|
|
28
|
+
|
|
29
|
+
Discovers shard blocks by following masterchain shard tips, emits events for each
|
|
30
|
+
shard block, and optionally fetches transactions for shard blocks.
|
|
31
|
+
|
|
32
|
+
Handlers can be passed via constructor or set later via decorators:
|
|
33
|
+
- on_error: receives ErrorEvent
|
|
34
|
+
- on_block: receives BlockEvent
|
|
35
|
+
- on_transactions: receives TransactionsEvent
|
|
36
|
+
"""
|
|
36
37
|
|
|
37
38
|
def __init__(
|
|
38
39
|
self,
|
|
39
|
-
*,
|
|
40
40
|
client: t.Union[LiteBalancer, LiteClient],
|
|
41
|
+
*,
|
|
42
|
+
on_error: t.Optional[OnError] = None,
|
|
43
|
+
on_block: t.Optional[OnBlock] = None,
|
|
44
|
+
on_transactions: t.Optional[OnTransactions] = None,
|
|
45
|
+
storage: t.Optional[BlockScannerStorageProtocol] = None,
|
|
41
46
|
poll_interval: float = 0.1,
|
|
42
|
-
include_transactions: bool = True,
|
|
43
|
-
max_concurrency: int = 1000,
|
|
44
47
|
**context: t.Any,
|
|
45
48
|
) -> None:
|
|
46
49
|
"""
|
|
47
|
-
Initialize
|
|
48
|
-
|
|
49
|
-
:param client:
|
|
50
|
-
:param
|
|
51
|
-
:param
|
|
52
|
-
:param
|
|
53
|
-
:param
|
|
50
|
+
Initialize scanner.
|
|
51
|
+
|
|
52
|
+
:param client: Lite client/balancer.
|
|
53
|
+
:param on_error: Called on internal errors and handler failures.
|
|
54
|
+
:param on_block: Called for each discovered shard block.
|
|
55
|
+
:param on_transactions: Called for shard blocks with fetched transactions.
|
|
56
|
+
:param storage: Progress storage (masterchain seqno).
|
|
57
|
+
:param poll_interval: Poll delay while waiting for next masterchain block.
|
|
58
|
+
:param context: Shared context passed to all events.
|
|
54
59
|
"""
|
|
55
60
|
self._client = client
|
|
56
|
-
self.
|
|
61
|
+
self._on_error = on_error
|
|
62
|
+
self._on_block = on_block
|
|
63
|
+
self._on_transactions = on_transactions
|
|
64
|
+
self._storage = storage
|
|
57
65
|
self._poll_interval = poll_interval
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
self._traversal = ShardTraversal()
|
|
61
|
-
self._dispatcher = EventDispatcher(max_concurrency)
|
|
66
|
+
self._context = dict(context)
|
|
62
67
|
|
|
68
|
+
self._pending_blocks: BlockQueue = asyncio.Queue()
|
|
63
69
|
self._stop_event = asyncio.Event()
|
|
64
70
|
self._running = False
|
|
65
71
|
|
|
66
|
-
@
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _shard_key(blk: BlockIdExt) -> ShardKey:
|
|
74
|
+
"""Return shard key as (workchain, shard)."""
|
|
75
|
+
return blk.workchain, blk.shard
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _overflow_i64(x: int) -> int:
|
|
79
|
+
"""Wrap integer to signed 64-bit range."""
|
|
80
|
+
return (x + 2**63) % 2**64 - 2**63
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _lowbit64(x: int) -> int:
|
|
84
|
+
"""Return lowest set bit (64-bit shard math helper)."""
|
|
85
|
+
return x & (~x + 1)
|
|
86
|
+
|
|
87
|
+
def _child_shard(self, shard: int, *, left: bool) -> int:
|
|
88
|
+
"""Return left/right child shard id for split shards."""
|
|
89
|
+
step = self._lowbit64(shard) >> 1
|
|
90
|
+
return self._overflow_i64(shard - step if left else shard + step)
|
|
91
|
+
|
|
92
|
+
def _parent_shard(self, shard: int) -> int:
|
|
93
|
+
"""Return parent shard id for merged shards."""
|
|
94
|
+
step = self._lowbit64(shard)
|
|
95
|
+
return self._overflow_i64((shard - step) | (step << 1))
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def last_mc_block(self) -> BlockIdExt:
|
|
99
|
+
"""Return last known masterchain block from provider cache."""
|
|
100
|
+
return self._client.provider.last_mc_block
|
|
74
101
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
self,
|
|
78
|
-
event_type: t.Type[TransactionEvent],
|
|
79
|
-
handler: Handler[TransactionEvent],
|
|
80
|
-
*,
|
|
81
|
-
where: t.Optional[TransactionWhere] = None,
|
|
82
|
-
) -> None: ...
|
|
102
|
+
def on_error(self, fn: t.Optional[OnError] = None) -> t.Any:
|
|
103
|
+
"""Decorator to set error handler."""
|
|
83
104
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
def decorator(handler: OnError) -> OnError:
|
|
106
|
+
self._on_error = handler
|
|
107
|
+
return handler
|
|
108
|
+
|
|
109
|
+
return decorator if fn is None else decorator(fn)
|
|
110
|
+
|
|
111
|
+
def on_block(self, fn: t.Optional[OnBlock] = None) -> t.Any:
|
|
112
|
+
"""Decorator to set block handler."""
|
|
113
|
+
|
|
114
|
+
def decorator(handler: OnBlock) -> OnBlock:
|
|
115
|
+
self._on_block = handler
|
|
116
|
+
return handler
|
|
117
|
+
|
|
118
|
+
return decorator if fn is None else decorator(fn)
|
|
92
119
|
|
|
93
|
-
def
|
|
120
|
+
def on_transactions(self, fn: t.Optional[OnTransactions] = None) -> t.Any:
|
|
121
|
+
"""Decorator to set transactions handler."""
|
|
122
|
+
|
|
123
|
+
def decorator(handler: OnTransactions) -> OnTransactions:
|
|
124
|
+
self._on_transactions = handler
|
|
125
|
+
return handler
|
|
126
|
+
|
|
127
|
+
return decorator if fn is None else decorator(fn)
|
|
128
|
+
|
|
129
|
+
async def _call_error_handler(
|
|
94
130
|
self,
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
error: BaseException,
|
|
132
|
+
mc_block: BlockIdExt,
|
|
97
133
|
*,
|
|
98
|
-
|
|
134
|
+
event: t.Any = None,
|
|
135
|
+
handler: t.Any = None,
|
|
136
|
+
block: t.Optional[BlockIdExt] = None,
|
|
99
137
|
) -> None:
|
|
100
|
-
"""
|
|
101
|
-
self.
|
|
102
|
-
|
|
103
|
-
def on_block(
|
|
104
|
-
self,
|
|
105
|
-
where: t.Optional[BlockWhere] = None,
|
|
106
|
-
) -> Decorator[BlockEvent]:
|
|
107
|
-
"""Decorator for block event handlers."""
|
|
108
|
-
return self._dispatcher.on(BlockEvent, where=where)
|
|
138
|
+
"""Call error handler with ErrorEvent. Never raises."""
|
|
139
|
+
if self._on_error is None:
|
|
140
|
+
return
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
try:
|
|
143
|
+
await self._on_error(
|
|
144
|
+
ErrorEvent(
|
|
145
|
+
client=self._client,
|
|
146
|
+
mc_block=mc_block,
|
|
147
|
+
context=self._context,
|
|
148
|
+
error=error,
|
|
149
|
+
event=event,
|
|
150
|
+
handler=handler,
|
|
151
|
+
block=block,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
except asyncio.CancelledError:
|
|
155
|
+
raise
|
|
156
|
+
except (BaseException,):
|
|
157
|
+
return
|
|
116
158
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"""Decorator for batch transaction event handlers."""
|
|
122
|
-
return self._dispatcher.on(TransactionsEvent, where=where)
|
|
159
|
+
async def _call_handler(self, handler: t.Any, event: t.Any) -> None:
|
|
160
|
+
"""Call handler(event). Route failures to on_error."""
|
|
161
|
+
if handler is None:
|
|
162
|
+
return
|
|
123
163
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
164
|
+
try:
|
|
165
|
+
await handler(event)
|
|
166
|
+
except asyncio.CancelledError:
|
|
167
|
+
raise
|
|
168
|
+
except BaseException as error:
|
|
169
|
+
await self._call_error_handler(
|
|
170
|
+
error,
|
|
171
|
+
event.mc_block,
|
|
172
|
+
event=event,
|
|
173
|
+
handler=handler,
|
|
174
|
+
block=event.block,
|
|
175
|
+
)
|
|
127
176
|
|
|
128
177
|
async def _lookup_mc_block(
|
|
129
178
|
self,
|
|
179
|
+
*,
|
|
130
180
|
seqno: t.Optional[int] = None,
|
|
131
181
|
lt: t.Optional[int] = None,
|
|
132
182
|
utime: t.Optional[int] = None,
|
|
133
183
|
) -> BlockIdExt:
|
|
134
|
-
"""Lookup masterchain block by seqno
|
|
135
|
-
mc_block,
|
|
184
|
+
"""Lookup masterchain block by seqno/lt/utime."""
|
|
185
|
+
mc_block, _ = await self._client.lookup_block(
|
|
136
186
|
workchain=WorkchainID.MASTERCHAIN,
|
|
137
187
|
shard=MASTERCHAIN_SHARD,
|
|
138
188
|
seqno=seqno,
|
|
@@ -141,173 +191,217 @@ class BlockScanner:
|
|
|
141
191
|
)
|
|
142
192
|
return mc_block
|
|
143
193
|
|
|
144
|
-
async def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
lt: t.Optional[int] = None,
|
|
148
|
-
utime: t.Optional[int] = None,
|
|
149
|
-
) -> _ScanState:
|
|
150
|
-
"""Initialize scanning state."""
|
|
151
|
-
if seqno is None and lt is None and utime is None:
|
|
152
|
-
mc_block = self._get_last_mc_block()
|
|
153
|
-
else:
|
|
154
|
-
mc_block = await self._lookup_mc_block(seqno=seqno, lt=lt, utime=utime)
|
|
194
|
+
async def _wait_next_mc_block(self, mc_block: BlockIdExt) -> BlockIdExt:
|
|
195
|
+
"""Wait until next masterchain block becomes available."""
|
|
196
|
+
next_mc_seqno = mc_block.seqno + 1
|
|
155
197
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
198
|
+
while not self._stop_event.is_set():
|
|
199
|
+
last_mc_block = self.last_mc_block
|
|
200
|
+
if next_mc_seqno <= last_mc_block.seqno:
|
|
201
|
+
if next_mc_seqno == last_mc_block.seqno:
|
|
202
|
+
return last_mc_block
|
|
203
|
+
return await self._lookup_mc_block(seqno=next_mc_seqno)
|
|
160
204
|
|
|
161
|
-
|
|
162
|
-
for shard in await self._client.get_all_shards_info(prev_mc):
|
|
163
|
-
shards_seqno[self._traversal.shard_key(shard)] = shard.seqno
|
|
205
|
+
await asyncio.sleep(self._poll_interval)
|
|
164
206
|
|
|
165
|
-
return
|
|
207
|
+
return mc_block
|
|
166
208
|
|
|
167
|
-
def
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
|
|
209
|
+
async def _get_seen_shard_seqno(self, mc_block: BlockIdExt) -> SeenShardSeqno:
|
|
210
|
+
"""Build map of last processed shard seqno from previous masterchain block."""
|
|
211
|
+
seen_shard_seqno: SeenShardSeqno = {}
|
|
212
|
+
if mc_block.seqno <= 0:
|
|
213
|
+
return seen_shard_seqno
|
|
171
214
|
|
|
172
|
-
|
|
173
|
-
self
|
|
174
|
-
|
|
175
|
-
shards_seqno: t.Dict[t.Tuple[int, int], int],
|
|
176
|
-
) -> t.List[BlockIdExt]:
|
|
177
|
-
"""Collect all unseen shard blocks for a masterchain block."""
|
|
178
|
-
shards = await self._client.get_all_shards_info(mc_block)
|
|
179
|
-
|
|
180
|
-
blocks: t.List[BlockIdExt] = []
|
|
181
|
-
for shard_tip in shards:
|
|
182
|
-
blocks.extend(
|
|
183
|
-
await self._traversal.walk_unseen(
|
|
184
|
-
root=shard_tip,
|
|
185
|
-
seen_seqno=shards_seqno,
|
|
186
|
-
get_header=self._client.get_block_header,
|
|
187
|
-
)
|
|
188
|
-
)
|
|
189
|
-
# Update seen_seqno after collecting blocks for this shard
|
|
190
|
-
shards_seqno[self._traversal.shard_key(shard_tip)] = shard_tip.seqno
|
|
215
|
+
prev_mc_block = await self._lookup_mc_block(seqno=mc_block.seqno - 1)
|
|
216
|
+
for shard_tip in await self._client.get_all_shards_info(prev_mc_block):
|
|
217
|
+
seen_shard_seqno[self._shard_key(shard_tip)] = shard_tip.seqno
|
|
191
218
|
|
|
192
|
-
return
|
|
219
|
+
return seen_shard_seqno
|
|
193
220
|
|
|
194
|
-
def
|
|
195
|
-
"""
|
|
196
|
-
self.
|
|
197
|
-
|
|
198
|
-
|
|
221
|
+
async def _process_pending_blocks(self, mc_block: BlockIdExt) -> None:
|
|
222
|
+
"""Process queued shard blocks and emit events."""
|
|
223
|
+
while not self._stop_event.is_set():
|
|
224
|
+
try:
|
|
225
|
+
shard_block = self._pending_blocks.get_nowait()
|
|
226
|
+
except asyncio.QueueEmpty:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
block_event = BlockEvent(
|
|
199
230
|
client=self._client,
|
|
231
|
+
mc_block=mc_block,
|
|
232
|
+
block=shard_block,
|
|
200
233
|
context=self._context,
|
|
201
|
-
block=block,
|
|
202
234
|
)
|
|
203
|
-
|
|
235
|
+
await self._call_handler(self._on_block, block_event)
|
|
236
|
+
|
|
237
|
+
if self._on_transactions is None:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
get_block_transactions = self._client.get_block_transactions_ext
|
|
241
|
+
try:
|
|
242
|
+
transactions = await get_block_transactions(shard_block)
|
|
243
|
+
except asyncio.CancelledError:
|
|
244
|
+
raise
|
|
245
|
+
except BaseException as error:
|
|
246
|
+
await self._call_error_handler(
|
|
247
|
+
error,
|
|
248
|
+
mc_block,
|
|
249
|
+
event=block_event,
|
|
250
|
+
handler=get_block_transactions,
|
|
251
|
+
block=shard_block,
|
|
252
|
+
)
|
|
253
|
+
transactions = []
|
|
204
254
|
|
|
205
|
-
|
|
206
|
-
self,
|
|
207
|
-
mc_block: BlockIdExt,
|
|
208
|
-
block: BlockIdExt,
|
|
209
|
-
transactions: t.List[Transaction],
|
|
210
|
-
) -> None:
|
|
211
|
-
"""Emit batch transactions event."""
|
|
212
|
-
self._dispatcher.emit(
|
|
213
|
-
TransactionsEvent(
|
|
214
|
-
mc_block=mc_block,
|
|
255
|
+
transactions_event = TransactionsEvent(
|
|
215
256
|
client=self._client,
|
|
216
|
-
|
|
217
|
-
block=
|
|
257
|
+
mc_block=mc_block,
|
|
258
|
+
block=shard_block,
|
|
218
259
|
transactions=transactions,
|
|
260
|
+
context=self._context,
|
|
219
261
|
)
|
|
220
|
-
|
|
262
|
+
await self._call_handler(self._on_transactions, transactions_event)
|
|
221
263
|
|
|
222
|
-
def
|
|
264
|
+
async def _enqueue_missing_blocks(
|
|
223
265
|
self,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
transaction: Transaction,
|
|
266
|
+
shard_tip: BlockIdExt,
|
|
267
|
+
seen_seqno: SeenShardSeqno,
|
|
227
268
|
) -> None:
|
|
228
|
-
"""
|
|
229
|
-
self.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
269
|
+
"""Enqueue unseen shard blocks in order (oldest -> newest)."""
|
|
270
|
+
shard_id = self._shard_key(shard_tip)
|
|
271
|
+
if seen_seqno.get(shard_id, -1) >= shard_tip.seqno:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
_, header = await self._client.get_block_header(shard_tip)
|
|
275
|
+
info = header.info
|
|
276
|
+
prev_ref = info.prev_ref
|
|
277
|
+
|
|
278
|
+
if prev_ref.type_ == "prev_blk_info":
|
|
279
|
+
prev: ExtBlkRef = prev_ref.prev
|
|
280
|
+
prev_shard = (
|
|
281
|
+
self._parent_shard(shard_tip.shard)
|
|
282
|
+
if info.after_split
|
|
283
|
+
else shard_tip.shard
|
|
236
284
|
)
|
|
237
|
-
)
|
|
238
285
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
286
|
+
await self._enqueue_missing_blocks(
|
|
287
|
+
shard_tip=BlockIdExt(
|
|
288
|
+
workchain=shard_tip.workchain,
|
|
289
|
+
shard=prev_shard,
|
|
290
|
+
seqno=prev.seqno,
|
|
291
|
+
root_hash=prev.root_hash,
|
|
292
|
+
file_hash=prev.file_hash,
|
|
293
|
+
),
|
|
294
|
+
seen_seqno=seen_seqno,
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
prev1, prev2 = prev_ref.prev1, prev_ref.prev2
|
|
298
|
+
|
|
299
|
+
await self._enqueue_missing_blocks(
|
|
300
|
+
shard_tip=BlockIdExt(
|
|
301
|
+
workchain=shard_tip.workchain,
|
|
302
|
+
shard=self._child_shard(shard_tip.shard, left=True),
|
|
303
|
+
seqno=prev1.seqno,
|
|
304
|
+
root_hash=prev1.root_hash,
|
|
305
|
+
file_hash=prev1.file_hash,
|
|
306
|
+
),
|
|
307
|
+
seen_seqno=seen_seqno,
|
|
308
|
+
)
|
|
309
|
+
await self._enqueue_missing_blocks(
|
|
310
|
+
shard_tip=BlockIdExt(
|
|
311
|
+
workchain=shard_tip.workchain,
|
|
312
|
+
shard=self._child_shard(shard_tip.shard, left=False),
|
|
313
|
+
seqno=prev2.seqno,
|
|
314
|
+
root_hash=prev2.root_hash,
|
|
315
|
+
file_hash=prev2.file_hash,
|
|
316
|
+
),
|
|
317
|
+
seen_seqno=seen_seqno,
|
|
318
|
+
)
|
|
247
319
|
|
|
248
|
-
|
|
249
|
-
|
|
320
|
+
await self._pending_blocks.put(shard_tip)
|
|
321
|
+
|
|
322
|
+
async def _run(self, mc_block: BlockIdExt) -> None:
|
|
323
|
+
"""Run scanning loop from provided masterchain block."""
|
|
324
|
+
if self._running:
|
|
325
|
+
raise RuntimeError("BlockScanner already running")
|
|
326
|
+
|
|
327
|
+
self._running = True
|
|
328
|
+
self._stop_event.clear()
|
|
250
329
|
|
|
251
|
-
|
|
252
|
-
|
|
330
|
+
try:
|
|
331
|
+
seen_shard_seqno = await self._get_seen_shard_seqno(mc_block)
|
|
253
332
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
333
|
+
while not self._stop_event.is_set():
|
|
334
|
+
for shard_tip in await self._client.get_all_shards_info(mc_block):
|
|
335
|
+
await self._enqueue_missing_blocks(shard_tip, seen_shard_seqno)
|
|
336
|
+
seen_shard_seqno[self._shard_key(shard_tip)] = shard_tip.seqno
|
|
257
337
|
|
|
258
|
-
|
|
259
|
-
"""Wait for next masterchain block, polling until available."""
|
|
260
|
-
next_seqno = current.seqno + 1
|
|
338
|
+
await self._process_pending_blocks(mc_block)
|
|
261
339
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
340
|
+
if self._storage is not None:
|
|
341
|
+
await self._storage.set_mc_seqno(mc_block.seqno)
|
|
342
|
+
mc_block = await self._wait_next_mc_block(mc_block)
|
|
343
|
+
finally:
|
|
344
|
+
self._running = False
|
|
345
|
+
self._stop_event.set()
|
|
265
346
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
347
|
+
async def resume(self) -> None:
|
|
348
|
+
"""Resume from storage."""
|
|
349
|
+
if self._storage is None:
|
|
350
|
+
raise RuntimeError("Storage is not configured")
|
|
270
351
|
|
|
271
|
-
|
|
352
|
+
saved_seqno = await self._storage.get_mc_seqno()
|
|
353
|
+
if saved_seqno is None or saved_seqno < 0:
|
|
354
|
+
raise RuntimeError("No masterchain seqno in storage")
|
|
355
|
+
|
|
356
|
+
last_mc_block = self.last_mc_block
|
|
357
|
+
if saved_seqno > last_mc_block.seqno:
|
|
358
|
+
raise RuntimeError("Storage masterchain seqno is ahead of network")
|
|
272
359
|
|
|
273
|
-
|
|
360
|
+
if saved_seqno >= last_mc_block.seqno:
|
|
361
|
+
mc_block = await self._wait_next_mc_block(last_mc_block)
|
|
362
|
+
else:
|
|
363
|
+
next_seqno = saved_seqno + 1
|
|
364
|
+
mc_block = (
|
|
365
|
+
last_mc_block
|
|
366
|
+
if next_seqno >= last_mc_block.seqno
|
|
367
|
+
else await self._lookup_mc_block(seqno=next_seqno)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
await self._run(mc_block)
|
|
371
|
+
|
|
372
|
+
async def start_from(
|
|
274
373
|
self,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
374
|
+
*,
|
|
375
|
+
seqno: t.Optional[int] = None,
|
|
376
|
+
utime: t.Optional[int] = None,
|
|
377
|
+
lt: t.Optional[int] = None,
|
|
278
378
|
) -> None:
|
|
279
379
|
"""
|
|
280
|
-
Start scanning from
|
|
380
|
+
Start scanning from an explicit masterchain point.
|
|
281
381
|
|
|
282
|
-
:param
|
|
283
|
-
:param
|
|
284
|
-
:param
|
|
382
|
+
:param seqno: Masterchain seqno.
|
|
383
|
+
:param utime: Unix time (resolved to masterchain block).
|
|
384
|
+
:param lt: Logical time (resolved to masterchain block).
|
|
285
385
|
"""
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
386
|
+
provided = sum(v is not None for v in (seqno, utime, lt))
|
|
387
|
+
if provided != 1:
|
|
388
|
+
raise ValueError("Provide exactly one of seqno, utime, lt")
|
|
389
|
+
|
|
390
|
+
if seqno is not None:
|
|
391
|
+
mc_block = await self._lookup_mc_block(seqno=seqno)
|
|
392
|
+
elif utime is not None:
|
|
393
|
+
mc_block = await self._lookup_mc_block(utime=utime)
|
|
394
|
+
elif lt is not None:
|
|
395
|
+
mc_block = await self._lookup_mc_block(lt=lt)
|
|
396
|
+
else:
|
|
397
|
+
raise AssertionError("unreachable")
|
|
291
398
|
|
|
292
|
-
|
|
293
|
-
seqno=from_seqno,
|
|
294
|
-
lt=from_lt,
|
|
295
|
-
utime=from_utime,
|
|
296
|
-
)
|
|
399
|
+
await self._run(mc_block)
|
|
297
400
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
mc_block=state.mc_block,
|
|
302
|
-
shards_seqno=state.shards_seqno,
|
|
303
|
-
)
|
|
304
|
-
for block in blocks:
|
|
305
|
-
await self._handle_block(state.mc_block, block)
|
|
306
|
-
state.mc_block = await self._wait_next_mc_block(state.mc_block)
|
|
307
|
-
finally:
|
|
308
|
-
await self._dispatcher.aclose()
|
|
309
|
-
self._running = False
|
|
401
|
+
async def start(self) -> None:
|
|
402
|
+
"""Start from the current last masterchain block."""
|
|
403
|
+
await self._run(self.last_mc_block)
|
|
310
404
|
|
|
311
405
|
async def stop(self) -> None:
|
|
312
|
-
"""
|
|
406
|
+
"""Request scanner stop."""
|
|
313
407
|
self._stop_event.set()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BlockScannerStorageProtocol(t.Protocol):
|
|
5
|
+
"""Store BlockScanner progress (masterchain seqno)."""
|
|
6
|
+
|
|
7
|
+
async def get_mc_seqno(self) -> t.Optional[int]:
|
|
8
|
+
"""Return last processed masterchain seqno (or None)."""
|
|
9
|
+
|
|
10
|
+
async def set_mc_seqno(self, seqno: int) -> None:
|
|
11
|
+
"""Persist last processed masterchain seqno."""
|