tonutils 2.0.1b4__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 +22 -12
- 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 -19
- tonutils/tools/block_scanner/events.py +48 -7
- tonutils/tools/block_scanner/scanner.py +315 -223
- tonutils/tools/block_scanner/storage.py +11 -0
- tonutils/utils.py +0 -48
- {tonutils-2.0.1b4.dist-info → tonutils-2.0.1b6.dist-info}/METADATA +1 -1
- {tonutils-2.0.1b4.dist-info → tonutils-2.0.1b6.dist-info}/RECORD +39 -41
- {tonutils-2.0.1b4.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 -96
- tonutils/tools/block_scanner/where.py +0 -151
- {tonutils-2.0.1b4.dist-info → tonutils-2.0.1b6.dist-info}/entry_points.txt +0 -0
- {tonutils-2.0.1b4.dist-info → tonutils-2.0.1b6.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b4.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,175 +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
|
-
)
|
|
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
|
|
189
218
|
|
|
190
|
-
|
|
191
|
-
return blocks
|
|
219
|
+
return seen_shard_seqno
|
|
192
220
|
|
|
193
|
-
def
|
|
194
|
-
"""
|
|
195
|
-
self.
|
|
196
|
-
|
|
197
|
-
|
|
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(
|
|
198
230
|
client=self._client,
|
|
231
|
+
mc_block=mc_block,
|
|
232
|
+
block=shard_block,
|
|
199
233
|
context=self._context,
|
|
200
|
-
block=block,
|
|
201
234
|
)
|
|
202
|
-
|
|
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 = []
|
|
203
254
|
|
|
204
|
-
|
|
205
|
-
self,
|
|
206
|
-
mc_block: BlockIdExt,
|
|
207
|
-
block: BlockIdExt,
|
|
208
|
-
transactions: t.List[Transaction],
|
|
209
|
-
) -> None:
|
|
210
|
-
"""Emit batch transactions event."""
|
|
211
|
-
self._dispatcher.emit(
|
|
212
|
-
TransactionsEvent(
|
|
213
|
-
mc_block=mc_block,
|
|
255
|
+
transactions_event = TransactionsEvent(
|
|
214
256
|
client=self._client,
|
|
215
|
-
|
|
216
|
-
block=
|
|
257
|
+
mc_block=mc_block,
|
|
258
|
+
block=shard_block,
|
|
217
259
|
transactions=transactions,
|
|
260
|
+
context=self._context,
|
|
218
261
|
)
|
|
219
|
-
|
|
262
|
+
await self._call_handler(self._on_transactions, transactions_event)
|
|
220
263
|
|
|
221
|
-
def
|
|
264
|
+
async def _enqueue_missing_blocks(
|
|
222
265
|
self,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
transaction: Transaction,
|
|
266
|
+
shard_tip: BlockIdExt,
|
|
267
|
+
seen_seqno: SeenShardSeqno,
|
|
226
268
|
) -> None:
|
|
227
|
-
"""
|
|
228
|
-
self.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
235
284
|
)
|
|
236
|
-
)
|
|
237
285
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
)
|
|
246
319
|
|
|
247
|
-
|
|
248
|
-
self._emit_block(mc_block, block)
|
|
320
|
+
await self._pending_blocks.put(shard_tip)
|
|
249
321
|
|
|
250
|
-
|
|
251
|
-
|
|
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")
|
|
252
326
|
|
|
253
|
-
|
|
254
|
-
self.
|
|
327
|
+
self._running = True
|
|
328
|
+
self._stop_event.clear()
|
|
255
329
|
|
|
256
|
-
|
|
257
|
-
self.
|
|
258
|
-
self._emit_transaction(mc_block, block, transaction)
|
|
330
|
+
try:
|
|
331
|
+
seen_shard_seqno = await self._get_seen_shard_seqno(mc_block)
|
|
259
332
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
263
337
|
|
|
264
|
-
|
|
265
|
-
self._ensure_running()
|
|
266
|
-
last_mc_block = self._get_last_mc_block()
|
|
338
|
+
await self._process_pending_blocks(mc_block)
|
|
267
339
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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()
|
|
272
346
|
|
|
273
|
-
|
|
347
|
+
async def resume(self) -> None:
|
|
348
|
+
"""Resume from storage."""
|
|
349
|
+
if self._storage is None:
|
|
350
|
+
raise RuntimeError("Storage is not configured")
|
|
351
|
+
|
|
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")
|
|
274
355
|
|
|
275
|
-
|
|
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")
|
|
359
|
+
|
|
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(
|
|
276
373
|
self,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
374
|
+
*,
|
|
375
|
+
seqno: t.Optional[int] = None,
|
|
376
|
+
utime: t.Optional[int] = None,
|
|
377
|
+
lt: t.Optional[int] = None,
|
|
280
378
|
) -> None:
|
|
281
379
|
"""
|
|
282
|
-
Start scanning from
|
|
380
|
+
Start scanning from an explicit masterchain point.
|
|
283
381
|
|
|
284
|
-
:param
|
|
285
|
-
:param
|
|
286
|
-
:param
|
|
382
|
+
:param seqno: Masterchain seqno.
|
|
383
|
+
:param utime: Unix time (resolved to masterchain block).
|
|
384
|
+
:param lt: Logical time (resolved to masterchain block).
|
|
287
385
|
"""
|
|
288
|
-
|
|
289
|
-
|
|
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")
|
|
290
398
|
|
|
291
|
-
self.
|
|
292
|
-
self._stop_event.clear()
|
|
399
|
+
await self._run(mc_block)
|
|
293
400
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
utime=from_utime,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
try:
|
|
301
|
-
while not self._stop_event.is_set():
|
|
302
|
-
blocks = await self._collect_blocks(
|
|
303
|
-
mc_block=state.mc_block,
|
|
304
|
-
shards_seqno=state.shards_seqno,
|
|
305
|
-
)
|
|
306
|
-
for block in blocks:
|
|
307
|
-
await self._handle_block(state.mc_block, block, state.shards_seqno)
|
|
308
|
-
state.mc_block = await self._wait_next_mc_block(state.mc_block)
|
|
309
|
-
finally:
|
|
310
|
-
await self._dispatcher.aclose()
|
|
311
|
-
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)
|
|
312
404
|
|
|
313
405
|
async def stop(self) -> None:
|
|
314
|
-
"""
|
|
406
|
+
"""Request scanner stop."""
|
|
315
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."""
|