hive-nectar 0.2.9__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.
Files changed (87) hide show
  1. hive_nectar-0.2.9.dist-info/METADATA +194 -0
  2. hive_nectar-0.2.9.dist-info/RECORD +87 -0
  3. hive_nectar-0.2.9.dist-info/WHEEL +4 -0
  4. hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
  5. hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
  6. nectar/__init__.py +37 -0
  7. nectar/account.py +5076 -0
  8. nectar/amount.py +553 -0
  9. nectar/asciichart.py +303 -0
  10. nectar/asset.py +122 -0
  11. nectar/block.py +574 -0
  12. nectar/blockchain.py +1242 -0
  13. nectar/blockchaininstance.py +2590 -0
  14. nectar/blockchainobject.py +263 -0
  15. nectar/cli.py +5937 -0
  16. nectar/comment.py +1552 -0
  17. nectar/community.py +854 -0
  18. nectar/constants.py +95 -0
  19. nectar/discussions.py +1437 -0
  20. nectar/exceptions.py +152 -0
  21. nectar/haf.py +381 -0
  22. nectar/hive.py +630 -0
  23. nectar/imageuploader.py +114 -0
  24. nectar/instance.py +113 -0
  25. nectar/market.py +876 -0
  26. nectar/memo.py +542 -0
  27. nectar/message.py +379 -0
  28. nectar/nodelist.py +309 -0
  29. nectar/price.py +603 -0
  30. nectar/profile.py +74 -0
  31. nectar/py.typed +0 -0
  32. nectar/rc.py +333 -0
  33. nectar/snapshot.py +1024 -0
  34. nectar/storage.py +62 -0
  35. nectar/transactionbuilder.py +659 -0
  36. nectar/utils.py +630 -0
  37. nectar/version.py +3 -0
  38. nectar/vote.py +722 -0
  39. nectar/wallet.py +472 -0
  40. nectar/witness.py +728 -0
  41. nectarapi/__init__.py +12 -0
  42. nectarapi/exceptions.py +126 -0
  43. nectarapi/graphenerpc.py +596 -0
  44. nectarapi/node.py +194 -0
  45. nectarapi/noderpc.py +79 -0
  46. nectarapi/openapi.py +107 -0
  47. nectarapi/py.typed +0 -0
  48. nectarapi/rpcutils.py +98 -0
  49. nectarapi/version.py +3 -0
  50. nectarbase/__init__.py +15 -0
  51. nectarbase/ledgertransactions.py +106 -0
  52. nectarbase/memo.py +242 -0
  53. nectarbase/objects.py +521 -0
  54. nectarbase/objecttypes.py +21 -0
  55. nectarbase/operationids.py +102 -0
  56. nectarbase/operations.py +1357 -0
  57. nectarbase/py.typed +0 -0
  58. nectarbase/signedtransactions.py +89 -0
  59. nectarbase/transactions.py +11 -0
  60. nectarbase/version.py +3 -0
  61. nectargraphenebase/__init__.py +27 -0
  62. nectargraphenebase/account.py +1121 -0
  63. nectargraphenebase/aes.py +49 -0
  64. nectargraphenebase/base58.py +197 -0
  65. nectargraphenebase/bip32.py +575 -0
  66. nectargraphenebase/bip38.py +110 -0
  67. nectargraphenebase/chains.py +15 -0
  68. nectargraphenebase/dictionary.py +2 -0
  69. nectargraphenebase/ecdsasig.py +309 -0
  70. nectargraphenebase/objects.py +130 -0
  71. nectargraphenebase/objecttypes.py +8 -0
  72. nectargraphenebase/operationids.py +5 -0
  73. nectargraphenebase/operations.py +25 -0
  74. nectargraphenebase/prefix.py +13 -0
  75. nectargraphenebase/py.typed +0 -0
  76. nectargraphenebase/signedtransactions.py +221 -0
  77. nectargraphenebase/types.py +557 -0
  78. nectargraphenebase/unsignedtransactions.py +288 -0
  79. nectargraphenebase/version.py +3 -0
  80. nectarstorage/__init__.py +57 -0
  81. nectarstorage/base.py +317 -0
  82. nectarstorage/exceptions.py +15 -0
  83. nectarstorage/interfaces.py +244 -0
  84. nectarstorage/masterpassword.py +237 -0
  85. nectarstorage/py.typed +0 -0
  86. nectarstorage/ram.py +27 -0
  87. nectarstorage/sqlite.py +343 -0
nectar/blockchain.py ADDED
@@ -0,0 +1,1242 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import math
5
+ import time
6
+ from datetime import date, datetime, timedelta
7
+ from datetime import time as datetime_time
8
+ from queue import Queue
9
+ from threading import Event, Thread
10
+ from time import sleep
11
+ from typing import Any, Callable, Dict, List, Optional, Union, cast
12
+
13
+ from nectar.instance import shared_blockchain_instance
14
+ from nectarapi.exceptions import UnknownTransaction
15
+
16
+ from .block import Block, BlockHeader
17
+ from .exceptions import (
18
+ BatchedCallsNotSupported,
19
+ BlockDoesNotExistsException,
20
+ BlockWaitTimeExceeded,
21
+ OfflineHasNoRPCException,
22
+ )
23
+ from .utils import addTzInfo
24
+
25
+ log = logging.getLogger(__name__)
26
+
27
+ FUTURES_MODULE = None
28
+ if not FUTURES_MODULE:
29
+ try:
30
+ from concurrent.futures import ThreadPoolExecutor, as_completed
31
+
32
+ FUTURES_MODULE = "futures"
33
+ # FUTURES_MODULE = None
34
+ except ImportError:
35
+ FUTURES_MODULE = None
36
+
37
+
38
+ # default exception handler. if you want to take some action on failed tasks
39
+ # maybe add the task back into the queue, then make your own handler and pass it in
40
+ def default_handler(name: str, exception: Exception, *args: Any, **kwargs: Any) -> None:
41
+ log.warning(f"{name} raised {exception} with args {args!r} and kwargs {kwargs!r}")
42
+ pass
43
+
44
+
45
+ class Worker(Thread):
46
+ """Thread executing tasks from a given tasks queue"""
47
+
48
+ def __init__(
49
+ self,
50
+ name: str,
51
+ queue: Queue,
52
+ results: Queue,
53
+ abort: Event,
54
+ idle: Event,
55
+ exception_handler: Callable,
56
+ ) -> None:
57
+ Thread.__init__(self)
58
+ self.name = name
59
+ self.queue = queue
60
+ self.results = results
61
+ self.abort = abort
62
+ self.idle = idle
63
+ self.exception_handler = exception_handler
64
+ self.daemon = True
65
+ self.start()
66
+
67
+ def run(self) -> None:
68
+ """Thread work loop calling the function with the params"""
69
+ # keep running until told to abort
70
+ while not self.abort.is_set():
71
+ try:
72
+ # get a task and raise immediately if none available
73
+ func, args, kwargs = self.queue.get(False)
74
+ self.idle.clear()
75
+ except Exception:
76
+ # no work to do
77
+ # if not self.idle.is_set():
78
+ # print >> stdout, '%s is idle' % self.name
79
+ self.idle.set()
80
+ # time.sleep(1)
81
+ continue
82
+
83
+ try:
84
+ # the function may raise
85
+ result = func(*args, **kwargs)
86
+ # print(result)
87
+ if result is not None:
88
+ self.results.put(result)
89
+ except Exception as e:
90
+ # so we move on and handle it in whatever way the caller wanted
91
+ self.exception_handler(self.name, e, args, kwargs)
92
+ finally:
93
+ # task complete no matter what happened
94
+ self.queue.task_done()
95
+
96
+
97
+ # class for thread pool
98
+ class Pool:
99
+ """Pool of threads consuming tasks from a queue"""
100
+
101
+ def __init__(
102
+ self,
103
+ thread_count: int,
104
+ batch_mode: bool = True,
105
+ exception_handler: Callable = default_handler,
106
+ ) -> None:
107
+ # batch mode means block when adding tasks if no threads available to process
108
+ self.queue = Queue(thread_count if batch_mode else 0)
109
+ self.resultQueue = Queue(0)
110
+ self.thread_count = thread_count
111
+ self.exception_handler = exception_handler
112
+ self.aborts = []
113
+ self.idles = []
114
+ self.threads = []
115
+
116
+ def __del__(self) -> None:
117
+ """Tell my threads to quit"""
118
+ self.abort()
119
+
120
+ def run(self, block: bool = False) -> bool:
121
+ """Start the threads, or restart them if you've aborted"""
122
+ # either wait for them to finish or return false if some arent
123
+ if block:
124
+ while self.alive():
125
+ sleep(1)
126
+ elif self.alive():
127
+ return False
128
+
129
+ # go start them
130
+ self.aborts = []
131
+ self.idles = []
132
+ self.threads = []
133
+ for n in range(self.thread_count):
134
+ abort = Event()
135
+ idle = Event()
136
+ self.aborts.append(abort)
137
+ self.idles.append(idle)
138
+ self.threads.append(
139
+ Worker(
140
+ "thread-%d" % n,
141
+ self.queue,
142
+ self.resultQueue,
143
+ abort,
144
+ idle,
145
+ self.exception_handler,
146
+ )
147
+ )
148
+ return True
149
+
150
+ def enqueue(self, func: Callable, *args: Any, **kargs: Any) -> None:
151
+ """Add a task to the queue"""
152
+ self.queue.put((func, args, kargs))
153
+
154
+ def join(self) -> None:
155
+ """Wait for completion of all the tasks in the queue"""
156
+ self.queue.join()
157
+
158
+ def abort(self, block: bool = False) -> None:
159
+ """Tell each worker that its done working"""
160
+ # tell the threads to stop after they are done with what they are currently doing
161
+ for a in self.aborts:
162
+ a.set()
163
+ # wait for them to finish if requested
164
+ while block and self.alive():
165
+ sleep(1)
166
+
167
+ def alive(self) -> bool:
168
+ """Returns True if any threads are currently running"""
169
+ return True in [t.is_alive() for t in self.threads]
170
+
171
+ def idle(self) -> bool:
172
+ """Returns True if all threads are waiting for work"""
173
+ return False not in [i.is_set() for i in self.idles]
174
+
175
+ def done(self) -> bool:
176
+ """Returns True if not tasks are left to be completed"""
177
+ return self.queue.empty()
178
+
179
+ def results(self, sleep_time: Union[int, float] = 0) -> List[Any]:
180
+ """Get the set of results that have been processed, repeatedly call until done"""
181
+ sleep(sleep_time)
182
+ results = []
183
+ try:
184
+ while True:
185
+ # get a result, raises empty exception immediately if none available
186
+ results.append(self.resultQueue.get(False))
187
+ self.resultQueue.task_done()
188
+ except Exception:
189
+ return results
190
+ return results
191
+
192
+
193
+ class Blockchain:
194
+ """This class allows to access the blockchain and read data
195
+ from it
196
+
197
+ :param Blockchain blockchain_instance: Blockchain instance
198
+ :param str mode: (default) Irreversible block (``irreversible``) or
199
+ actual head block (``head``)
200
+ :param int max_block_wait_repetition: maximum wait repetition for next block
201
+ where each repetition is block_interval long (default is 3)
202
+
203
+ This class let's you deal with blockchain related data and methods.
204
+ Read blockchain related data:
205
+
206
+ .. testsetup::
207
+
208
+ from nectar.blockchain import Blockchain
209
+ chain = Blockchain()
210
+
211
+ Read current block and blockchain info
212
+
213
+ .. testcode::
214
+
215
+ print(chain.get_current_block())
216
+ print(chain.blockchain.info())
217
+
218
+ Monitor for new blocks. When ``stop`` is not set, monitoring will never stop.
219
+
220
+ .. testcode::
221
+
222
+ blocks = []
223
+ current_num = chain.get_current_block_num()
224
+ for block in chain.blocks(start=current_num - 99, stop=current_num):
225
+ blocks.append(block)
226
+ len(blocks)
227
+
228
+ .. testoutput::
229
+
230
+ 100
231
+
232
+ or each operation individually:
233
+
234
+ .. testcode::
235
+
236
+ ops = []
237
+ current_num = chain.get_current_block_num()
238
+ for operation in chain.ops(start=current_num - 99, stop=current_num):
239
+ ops.append(operation)
240
+
241
+ """
242
+
243
+ def __init__(
244
+ self,
245
+ blockchain_instance: Any = None,
246
+ mode: str = "irreversible",
247
+ max_block_wait_repetition: Optional[int] = None,
248
+ data_refresh_time_seconds: int = 900,
249
+ **kwargs,
250
+ ) -> None:
251
+ """
252
+ Initialize the Blockchain helper.
253
+
254
+ Sets the underlying blockchain connection (uses shared instance if none provided), configures mode to map to the underlying RPC key ("last_irreversible_block_num" or "head_block_number"), sets max_block_wait_repetition (default 3), and reads the chain's block interval.
255
+
256
+ Parameters:
257
+ mode (str): "irreversible" to operate against the last irreversible block, or "head" to operate against the chain head.
258
+ max_block_wait_repetition (int, optional): Number of times to retry waiting for a block before giving up; defaults to 3.
259
+
260
+ Raises:
261
+ ValueError: If `mode` is not "irreversible" or "head".
262
+ """
263
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
264
+
265
+ if mode == "irreversible":
266
+ self.mode = "last_irreversible_block_num"
267
+ elif mode == "head":
268
+ self.mode = "head_block_number"
269
+ else:
270
+ raise ValueError("invalid value for 'mode'!")
271
+ if max_block_wait_repetition:
272
+ self.max_block_wait_repetition = max_block_wait_repetition
273
+ else:
274
+ self.max_block_wait_repetition = 3
275
+ self.block_interval = self.blockchain.get_block_interval()
276
+
277
+ def is_irreversible_mode(self) -> bool:
278
+ return self.mode == "last_irreversible_block_num"
279
+
280
+ def is_transaction_existing(self, transaction_id: str) -> bool:
281
+ """Returns true, if the transaction_id is valid"""
282
+ try:
283
+ self.get_transaction(transaction_id)
284
+ return True
285
+ except UnknownTransaction:
286
+ return False
287
+
288
+ def get_transaction(self, transaction_id: str) -> Dict[str, Any]:
289
+ """Returns a transaction from the blockchain
290
+
291
+ :param str transaction_id: transaction_id
292
+ """
293
+ if not self.blockchain.is_connected():
294
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
295
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
296
+ ret = self.blockchain.rpc.get_transaction({"id": transaction_id})
297
+ return ret
298
+
299
+ def get_transaction_hex(self, transaction: Dict[str, Any]) -> str:
300
+ """Returns a hexdump of the serialized binary form of a transaction.
301
+
302
+ :param dict transaction: transaction
303
+ """
304
+ if not self.blockchain.is_connected():
305
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
306
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
307
+ ret = self.blockchain.rpc.get_transaction_hex({"trx": transaction})["hex"]
308
+ return ret
309
+
310
+ def get_current_block_num(self) -> int:
311
+ """This call returns the current block number
312
+
313
+ .. note:: The block number returned depends on the ``mode`` used
314
+ when instantiating from this class.
315
+ """
316
+ props = self.blockchain.get_dynamic_global_properties(False)
317
+ if props is None:
318
+ raise ValueError("Could not receive dynamic_global_properties!")
319
+ if self.mode not in props:
320
+ raise ValueError(self.mode + " is not in " + str(props))
321
+ return int(props.get(self.mode))
322
+
323
+ def get_current_block(self, only_ops: bool = False, only_virtual_ops: bool = False) -> Block:
324
+ """This call returns the current block
325
+
326
+ :param bool only_ops: Returns block with operations only, when set to True (default: False)
327
+ :param bool only_virtual_ops: Includes only virtual operations (default: False)
328
+
329
+ .. note:: The block number returned depends on the ``mode`` used
330
+ when instantiating from this class.
331
+ """
332
+ return Block(
333
+ self.get_current_block_num(),
334
+ only_ops=only_ops,
335
+ only_virtual_ops=only_virtual_ops,
336
+ blockchain_instance=self.blockchain,
337
+ )
338
+
339
+ def get_estimated_block_num(
340
+ self,
341
+ date: Union[datetime, date, datetime_time, None],
342
+ estimateForwards: bool = False,
343
+ accurate: bool = True,
344
+ ) -> int:
345
+ """This call estimates the block number based on a given date
346
+
347
+ :param datetime date: block time for which a block number is estimated
348
+
349
+ .. note:: The block number returned depends on the ``mode`` used
350
+ when instantiating from this class.
351
+
352
+ .. code-block:: python
353
+
354
+ >>> from nectar.blockchain import Blockchain
355
+ >>> from datetime import datetime
356
+ >>> blockchain = Blockchain()
357
+ >>> block_num = blockchain.get_estimated_block_num(datetime(2019, 6, 18, 5 ,8, 27))
358
+ >>> block_num == 33898182
359
+ True
360
+
361
+ """
362
+ last_block = self.get_current_block()
363
+ date = addTzInfo(date)
364
+ # Ensure we have a datetime object for arithmetic operations
365
+ if date is None or not isinstance(date, datetime):
366
+ raise ValueError("date must be a datetime object after addTzInfo processing")
367
+ block_number: int = 1
368
+ if estimateForwards:
369
+ block_offset = 10
370
+ first_block = BlockHeader(block_offset, blockchain_instance=self.blockchain)
371
+ time_diff = date - first_block.time()
372
+ block_number = math.floor(
373
+ time_diff.total_seconds() / self.block_interval + block_offset
374
+ )
375
+ else:
376
+ time_diff = last_block.time() - date
377
+ if last_block.identifier is not None:
378
+ block_number = math.floor(
379
+ last_block.identifier - time_diff.total_seconds() / self.block_interval
380
+ )
381
+ if block_number < 1:
382
+ block_number = 1
383
+
384
+ if accurate:
385
+ if last_block.identifier is not None and block_number > int(last_block.identifier):
386
+ block_number = int(last_block.identifier)
387
+ block_time_diff = timedelta(seconds=10)
388
+
389
+ last_block_time_diff_seconds = 10
390
+ second_last_block_time_diff_seconds = 10
391
+
392
+ while (
393
+ block_time_diff.total_seconds() > self.block_interval
394
+ or block_time_diff.total_seconds() < -self.block_interval
395
+ ):
396
+ block = BlockHeader(int(block_number), blockchain_instance=self.blockchain)
397
+ second_last_block_time_diff_seconds = last_block_time_diff_seconds
398
+ last_block_time_diff_seconds = block_time_diff.total_seconds()
399
+ block_time_diff = date - block.time()
400
+ if (
401
+ second_last_block_time_diff_seconds == block_time_diff.total_seconds()
402
+ and second_last_block_time_diff_seconds < 10
403
+ ):
404
+ return int(block_number)
405
+ delta: int = int(block_time_diff.total_seconds() // self.block_interval)
406
+ if delta == 0 and block_time_diff.total_seconds() < 0:
407
+ delta = -1
408
+ elif delta == 0 and block_time_diff.total_seconds() > 0:
409
+ delta = 1
410
+ block_number += delta
411
+ if block_number < 1:
412
+ break
413
+ if last_block.identifier is not None and block_number > int(last_block.identifier):
414
+ break
415
+
416
+ return int(block_number)
417
+
418
+ def block_time(self, block_num: int) -> datetime:
419
+ """Returns a datetime of the block with the given block
420
+ number.
421
+
422
+ :param int block_num: Block number
423
+ """
424
+ return Block(block_num, blockchain_instance=self.blockchain).time()
425
+
426
+ def block_timestamp(self, block_num: int) -> int:
427
+ """Returns the timestamp of the block with the given block
428
+ number as integer.
429
+
430
+ :param int block_num: Block number
431
+ """
432
+ block_time = Block(block_num, blockchain_instance=self.blockchain).time()
433
+ return int(time.mktime(block_time.timetuple()))
434
+
435
+ @property
436
+ def participation_rate(self) -> float:
437
+ """Returns the witness participation rate in a range from 0 to 1"""
438
+ return (
439
+ bin(
440
+ int(
441
+ self.blockchain.get_dynamic_global_properties(use_stored_data=False)[
442
+ "recent_slots_filled"
443
+ ]
444
+ )
445
+ ).count("1")
446
+ / 128
447
+ )
448
+
449
+ def blocks(
450
+ self,
451
+ start: Optional[int] = None,
452
+ stop: Optional[int] = None,
453
+ max_batch_size: Optional[int] = None,
454
+ threading: bool = False,
455
+ thread_num: int = 8,
456
+ only_ops: bool = False,
457
+ only_virtual_ops: bool = False,
458
+ ) -> Any:
459
+ """
460
+ Yield Block objects from `start` up to `stop` (or the chain head).
461
+
462
+ This generator retrieves blocks from the connected blockchain instance and yields them as Block objects. It supports three retrieval modes:
463
+ - Single-threaded sequential fetching (default).
464
+ - Threaded parallel fetching across multiple blockchain instances when `threading=True`.
465
+ - Batched RPC calls for appbase-compatible nodes when `max_batch_size` is set (cannot be combined with `threading`).
466
+
467
+ Parameters:
468
+ start (int, optional): First block number to fetch. If omitted, defaults to the current block.
469
+ stop (int, optional): Last block number to fetch. If omitted, the generator follows the chain head indefinitely.
470
+ max_batch_size (int, optional): Use batched RPC calls (appbase only). Cannot be used with `threading`.
471
+ threading (bool): If True, fetch blocks in parallel using `thread_num` workers.
472
+ thread_num (int): Number of worker threads to use when `threading=True`.
473
+ only_ops (bool): If True, blocks will contain only regular operations (no block metadata).
474
+ Mutually exclusive with `only_virtual_ops=True`.
475
+ only_virtual_ops (bool): If True, yield only virtual operations.
476
+
477
+ Yields:
478
+ Block: A Block object for each fetched block (may contain only ops or only virtual ops depending on flags).
479
+
480
+ Exceptions:
481
+ OfflineHasNoRPCException: Raised if batched mode is requested while offline (no RPC available).
482
+ BatchedCallsNotSupported: Raised if the node does not support batched calls when `max_batch_size` is used.
483
+
484
+ Notes:
485
+ - For instant (non-irreversible) confirmations, initialize the Blockchain with mode="head"; otherwise this method will wait for irreversible blocks.
486
+ - `max_batch_size` requires appbase-compatible RPC; threaded mode creates additional blockchain instances for parallel RPC calls.
487
+ """
488
+ # Let's find out how often blocks are generated!
489
+ current_block = self.get_current_block()
490
+ current_block_num = current_block.block_num
491
+ if not start and current_block_num is not None:
492
+ start = current_block_num
493
+ head_block_reached = False
494
+ pool: Any = None
495
+ if threading and FUTURES_MODULE is not None:
496
+ pool = ThreadPoolExecutor(max_workers=thread_num)
497
+ elif threading:
498
+ pool = Pool(thread_num, batch_mode=True)
499
+ if threading:
500
+ blockchain_instance = [self.blockchain]
501
+ nodelist = self.blockchain.rpc.nodes.export_working_nodes()
502
+ for i in range(thread_num - 1):
503
+ blockchain_instance.append(
504
+ self.blockchain.__class__(
505
+ node=nodelist,
506
+ num_retries=self.blockchain.rpc.num_retries,
507
+ num_retries_call=self.blockchain.rpc.num_retries_call,
508
+ timeout=self.blockchain.rpc.timeout,
509
+ )
510
+ )
511
+ # We are going to loop indefinitely
512
+ latest_block = 0
513
+ while True:
514
+ if stop:
515
+ head_block = stop
516
+ else:
517
+ current_block_num = self.get_current_block_num()
518
+ head_block = current_block_num
519
+ if threading and not head_block_reached and start is not None:
520
+ if pool is None:
521
+ raise RuntimeError("Threading is enabled but no pool was initialized")
522
+ pool_any = cast(Any, pool)
523
+ latest_block = start - 1
524
+ result_block_nums = []
525
+ for blocknum in range(start, head_block + 1, thread_num):
526
+ # futures = []
527
+ i = 0
528
+ if FUTURES_MODULE is not None:
529
+ futures = []
530
+ block_num_list = []
531
+ # freeze = self.blockchain.rpc.nodes.freeze_current_node
532
+ num_retries = self.blockchain.rpc.nodes.num_retries
533
+ # self.blockchain.rpc.nodes.freeze_current_node = True
534
+ self.blockchain.rpc.nodes.num_retries = thread_num
535
+ error_cnt = self.blockchain.rpc.nodes.node.error_cnt
536
+ while i < thread_num and blocknum + i <= head_block:
537
+ block_num_list.append(blocknum + i)
538
+ results = []
539
+ if FUTURES_MODULE is not None:
540
+ futures.append(
541
+ pool_any.submit(
542
+ Block,
543
+ blocknum + i,
544
+ only_ops=only_ops,
545
+ only_virtual_ops=only_virtual_ops,
546
+ blockchain_instance=blockchain_instance[i],
547
+ )
548
+ )
549
+ else:
550
+ pool_any.enqueue(
551
+ Block,
552
+ blocknum + i,
553
+ only_ops=only_ops,
554
+ only_virtual_ops=only_virtual_ops,
555
+ blockchain_instance=blockchain_instance[i],
556
+ )
557
+ i += 1
558
+ if FUTURES_MODULE is not None:
559
+ try:
560
+ results = [r.result() for r in as_completed(futures)]
561
+ except Exception as e:
562
+ log.error(str(e))
563
+ else:
564
+ pool_any.run(True)
565
+ pool_any.join()
566
+ for result in pool_any.results():
567
+ results.append(result)
568
+ pool_any.abort()
569
+ self.blockchain.rpc.nodes.num_retries = num_retries
570
+ # self.blockchain.rpc.nodes.freeze_current_node = freeze
571
+ new_error_cnt = self.blockchain.rpc.nodes.node.error_cnt
572
+ self.blockchain.rpc.nodes.node.error_cnt = error_cnt
573
+ if new_error_cnt > error_cnt:
574
+ self.blockchain.rpc.nodes.node.error_cnt += 1
575
+ # self.blockchain.rpc.next()
576
+
577
+ checked_results = []
578
+ for b in results:
579
+ if b.block_num is not None and int(b.block_num) not in result_block_nums:
580
+ b["id"] = b.block_num
581
+ b.identifier = b.block_num
582
+ checked_results.append(b)
583
+ result_block_nums.append(int(b.block_num))
584
+
585
+ missing_block_num = list(set(block_num_list).difference(set(result_block_nums)))
586
+ while len(missing_block_num) > 0:
587
+ for blocknum in missing_block_num:
588
+ try:
589
+ block = Block(
590
+ blocknum,
591
+ only_ops=only_ops,
592
+ only_virtual_ops=only_virtual_ops,
593
+ blockchain_instance=self.blockchain,
594
+ )
595
+ block_num_value = block.block_num
596
+ if block_num_value is None:
597
+ log.debug(
598
+ "Skipping missing block with no block_num: %s", blocknum
599
+ )
600
+ continue
601
+ block["id"] = block_num_value
602
+ block.identifier = block_num_value
603
+ checked_results.append(block)
604
+ result_block_nums.append(int(block_num_value))
605
+ except Exception as e:
606
+ log.error(str(e))
607
+ missing_block_num = list(
608
+ set(block_num_list).difference(set(result_block_nums))
609
+ )
610
+ from operator import itemgetter
611
+
612
+ blocks = sorted(checked_results, key=itemgetter("id"))
613
+ for b in blocks:
614
+ block_num_value = b.block_num
615
+ if block_num_value is None:
616
+ log.debug("Skipping yielded block with no block_num: %s", b)
617
+ continue
618
+ if latest_block < int(block_num_value):
619
+ latest_block = int(block_num_value)
620
+ yield b
621
+
622
+ if latest_block <= head_block:
623
+ for blocknum in range(latest_block + 1, head_block + 1):
624
+ if blocknum not in result_block_nums:
625
+ block = Block(
626
+ blocknum,
627
+ only_ops=only_ops,
628
+ only_virtual_ops=only_virtual_ops,
629
+ blockchain_instance=self.blockchain,
630
+ )
631
+ result_block_nums.append(blocknum)
632
+ yield block
633
+ elif (
634
+ max_batch_size is not None
635
+ and start is not None
636
+ and (head_block - start) >= max_batch_size
637
+ and not head_block_reached
638
+ ):
639
+ if not self.blockchain.is_connected():
640
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
641
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
642
+ if start is not None:
643
+ latest_block = start - 1
644
+ for blocknumblock in range(start, head_block + 1, max_batch_size):
645
+ batch_count = min(max_batch_size, head_block - blocknumblock + 1)
646
+ if only_virtual_ops:
647
+ batch_blocks = []
648
+ for blocknum in range(blocknumblock, blocknumblock + batch_count):
649
+ ops_resp = self.blockchain.rpc.get_ops_in_block(
650
+ {"block_num": blocknum, "only_virtual": True},
651
+ )
652
+ ops = (
653
+ ops_resp.get("ops", [])
654
+ if isinstance(ops_resp, dict)
655
+ else ops_resp
656
+ )
657
+ if not ops:
658
+ continue
659
+ block_dict = {
660
+ "block": blocknum,
661
+ "timestamp": ops[0]["timestamp"],
662
+ "operations": ops,
663
+ }
664
+ batch_blocks.append(block_dict)
665
+ else:
666
+ resp = self.blockchain.rpc.get_block_range(
667
+ {"starting_block_num": blocknumblock, "count": batch_count},
668
+ )
669
+ batch_blocks = (
670
+ resp.get("blocks", []) if isinstance(resp, dict) else resp
671
+ )
672
+
673
+ if not batch_blocks:
674
+ raise BatchedCallsNotSupported(
675
+ f"{self.blockchain.rpc.url} Doesn't support batched calls"
676
+ )
677
+
678
+ for raw_block in batch_blocks:
679
+ block_obj = Block(
680
+ raw_block,
681
+ only_ops=only_ops,
682
+ only_virtual_ops=only_virtual_ops,
683
+ blockchain_instance=self.blockchain,
684
+ )
685
+ block_num_value = block_obj.block_num
686
+ if block_num_value is None:
687
+ log.debug("Skipping block with missing block_num: %s", raw_block)
688
+ continue
689
+ block_obj["id"] = block_num_value
690
+ block_obj.identifier = block_num_value
691
+ if latest_block < int(block_num_value):
692
+ latest_block = int(block_num_value)
693
+ yield block_obj
694
+ else:
695
+ # Blocks from start until head block
696
+ if start is not None:
697
+ for blocknum in range(start, head_block + 1):
698
+ # Get full block
699
+ block = self.wait_for_and_get_block(
700
+ blocknum,
701
+ only_ops=only_ops,
702
+ only_virtual_ops=only_virtual_ops,
703
+ block_number_check_cnt=5,
704
+ last_current_block_num=current_block_num,
705
+ )
706
+ yield block
707
+ # Set new start
708
+ start = head_block + 1
709
+ head_block_reached = True
710
+
711
+ if stop and start > stop:
712
+ return
713
+
714
+ # Sleep for one block
715
+ time.sleep(self.block_interval)
716
+
717
+ def wait_for_and_get_block(
718
+ self,
719
+ block_number: int,
720
+ blocks_waiting_for: Optional[int] = None,
721
+ only_ops: bool = False,
722
+ only_virtual_ops: bool = False,
723
+ block_number_check_cnt: int = -1,
724
+ last_current_block_num: Optional[int] = None,
725
+ ) -> Optional[Block]:
726
+ """Get the desired block from the chain, if the current head block is smaller (for both head and irreversible)
727
+ then we wait, but a maxmimum of blocks_waiting_for * max_block_wait_repetition time before failure.
728
+
729
+ :param int block_number: desired block number
730
+ :param int blocks_waiting_for: difference between block_number and current head and defines
731
+ how many blocks we are willing to wait, positive int (default: None)
732
+ :param bool only_ops: Returns blocks with operations only, when set to True (default: False)
733
+ :param bool only_virtual_ops: Includes only virtual operations (default: False)
734
+ :param int block_number_check_cnt: limit the number of retries when greater than -1
735
+ :param int last_current_block_num: can be used to reduce the number of get_current_block_num() api calls
736
+
737
+ """
738
+ if last_current_block_num is None:
739
+ last_current_block_num = self.get_current_block_num()
740
+ elif last_current_block_num - block_number < 50:
741
+ last_current_block_num = self.get_current_block_num()
742
+
743
+ if not blocks_waiting_for:
744
+ blocks_waiting_for = max(1, block_number - last_current_block_num)
745
+
746
+ repetition = 0
747
+ # can't return the block before the chain has reached it (support future block_num)
748
+ while last_current_block_num < block_number:
749
+ repetition += 1
750
+ time.sleep(self.block_interval)
751
+ if last_current_block_num - block_number < 50:
752
+ last_current_block_num = self.get_current_block_num()
753
+ if repetition > blocks_waiting_for * self.max_block_wait_repetition:
754
+ raise BlockWaitTimeExceeded(
755
+ "Already waited %d s"
756
+ % (
757
+ blocks_waiting_for
758
+ * self.max_block_wait_repetition
759
+ * self.block_interval
760
+ )
761
+ )
762
+ # block has to be returned properly
763
+ repetition = 0
764
+ cnt = 0
765
+ block = None
766
+ while (
767
+ block is None or block.block_num is None or int(block.block_num) != block_number
768
+ ) and (block_number_check_cnt < 0 or cnt < block_number_check_cnt):
769
+ try:
770
+ block = Block(
771
+ block_number,
772
+ only_ops=only_ops,
773
+ only_virtual_ops=only_virtual_ops,
774
+ blockchain_instance=self.blockchain,
775
+ )
776
+ cnt += 1
777
+ except BlockDoesNotExistsException:
778
+ block = None
779
+ if repetition > blocks_waiting_for * self.max_block_wait_repetition:
780
+ raise BlockWaitTimeExceeded(
781
+ "Already waited %d s"
782
+ % (
783
+ blocks_waiting_for
784
+ * self.max_block_wait_repetition
785
+ * self.block_interval
786
+ )
787
+ )
788
+ repetition += 1
789
+ time.sleep(self.block_interval)
790
+
791
+ return block
792
+
793
+ def ops(
794
+ self,
795
+ start: Optional[int] = None,
796
+ stop: Optional[int] = None,
797
+ only_virtual_ops: bool = False,
798
+ **kwargs: Any,
799
+ ) -> None:
800
+ """Blockchain.ops() is deprecated. Please use Blockchain.stream() instead."""
801
+ import warnings
802
+
803
+ warnings.warn(
804
+ "Blockchain.ops() is deprecated. Please use Blockchain.stream() instead.",
805
+ DeprecationWarning,
806
+ stacklevel=2,
807
+ )
808
+ raise NotImplementedError("Use Blockchain.stream() instead.")
809
+
810
+ def ops_statistics(
811
+ self,
812
+ start: int,
813
+ stop: Optional[int] = None,
814
+ add_to_ops_stat: Optional[Dict[str, int]] = None,
815
+ with_virtual_ops: bool = True,
816
+ verbose: bool = False,
817
+ ) -> Optional[Dict[str, int]]:
818
+ """Generates statistics for all operations (including virtual operations) starting from
819
+ ``start``.
820
+
821
+ :param int start: Starting block
822
+ :param int stop: Stop at this block, if set to None, the current_block_num is taken
823
+ :param dict add_to_ops_stat: if set, the result is added to add_to_ops_stat
824
+ :param bool verbose: if True, the current block number and timestamp is printed
825
+
826
+ This call returns a dict with all possible operations and their occurrence.
827
+
828
+ """
829
+ if add_to_ops_stat is None:
830
+ import nectarbase.operationids
831
+
832
+ ops_stat = nectarbase.operationids.operations.copy()
833
+ for key in ops_stat:
834
+ ops_stat[key] = 0
835
+ else:
836
+ ops_stat = add_to_ops_stat.copy()
837
+ current_block = self.get_current_block_num()
838
+ if start > current_block:
839
+ return
840
+ if stop is None:
841
+ stop = current_block
842
+ for block in self.blocks(start=start, stop=stop, only_ops=False, only_virtual_ops=False):
843
+ if verbose:
844
+ print(block["identifier"] + " " + block["timestamp"])
845
+ ops_stat = block.ops_statistics(add_to_ops_stat=ops_stat)
846
+ if with_virtual_ops:
847
+ for block in self.blocks(start=start, stop=stop, only_ops=True, only_virtual_ops=True):
848
+ if verbose:
849
+ print(block["identifier"] + " " + block["timestamp"])
850
+ ops_stat = block.ops_statistics(add_to_ops_stat=ops_stat)
851
+ return ops_stat
852
+
853
+ def stream(
854
+ self, opNames: Optional[List[str]] = None, raw_ops: bool = False, *args: Any, **kwargs: Any
855
+ ) -> Any:
856
+ """
857
+ Yield blockchain operations filtered by type, normalizing several node event formats into a consistent output.
858
+
859
+ Parameters:
860
+ opNames (list): Operation type names to filter for (e.g., ['transfer', 'comment']). If empty, all operations are yielded.
861
+ raw_ops (bool): If True, yield raw operation tuples with minimal metadata; if False (default), yield a flattened dict with operation fields and metadata.
862
+ *args, **kwargs: Passed through to self.blocks(...) (e.g., start, stop, max_batch_size, threading, thread_num, only_ops, only_virtual_ops).
863
+
864
+ Yields:
865
+ dict: When raw_ops is False, yields a dictionary with at least:
866
+ - 'type': operation name
867
+ - operation fields (e.g., 'from', 'to', 'amount', ...)
868
+ - '_id': deterministic operation hash
869
+ - 'timestamp': block timestamp
870
+ - 'block_num': block number
871
+ - 'trx_num': transaction index within the block
872
+ - 'trx_id': transaction id
873
+
874
+ dict: When raw_ops is True, yields a compact dictionary:
875
+ - 'block_num': block number
876
+ - 'trx_num': transaction index
877
+ - 'op': [op_type, op_payload]
878
+ - 'timestamp': block timestamp
879
+
880
+ Notes:
881
+ - The method accepts the same control parameters as blocks(...) via kwargs. The block stream determines timestamps and block-related metadata.
882
+ - Operation events from different node formats (lists, legacy dicts, appbase-style dicts) are normalized by this method before yielding.
883
+ """
884
+ if opNames is None:
885
+ opNames = []
886
+ for block in self.blocks(**kwargs):
887
+ block_num_val = (
888
+ getattr(block, "block_num", None)
889
+ or block.get("block")
890
+ or block.get("block_num")
891
+ or block.get("id")
892
+ or block.get("identifier", 0)
893
+ )
894
+ timestamp_val = block.get("timestamp")
895
+
896
+ if "transactions" in block:
897
+ transactions = block["transactions"]
898
+ tx_ids = block.get("transaction_ids", [])
899
+ for trx_nr, trx in enumerate(transactions):
900
+ if "operations" not in trx:
901
+ continue
902
+ for event in trx["operations"]:
903
+ trx_id = tx_ids[trx_nr] if trx_nr < len(tx_ids) else ""
904
+ op_type = ""
905
+ op = {}
906
+ if isinstance(event, list):
907
+ op_type, op = event
908
+ op_hash_input = event
909
+ elif isinstance(event, dict) and "type" in event and "value" in event:
910
+ op_type = event["type"]
911
+ if op_type.endswith("_operation"):
912
+ op_type = op_type[:-10]
913
+ op = event["value"]
914
+ op_hash_input = event
915
+ elif (
916
+ "op" in event
917
+ and isinstance(event["op"], dict)
918
+ and "type" in event["op"]
919
+ and "value" in event["op"]
920
+ ):
921
+ op_type = event["op"]["type"]
922
+ if op_type.endswith("_operation"):
923
+ op_type = op_type[:-10]
924
+ op = event["op"]["value"]
925
+ trx_id = event.get("trx_id", trx_id)
926
+ op_hash_input = event["op"]
927
+ elif "op" in event and isinstance(event["op"], list):
928
+ op_type, op = event["op"]
929
+ trx_id = event.get("trx_id", trx_id)
930
+ op_hash_input = event["op"]
931
+ timestamp_val = event.get("timestamp", timestamp_val)
932
+ else:
933
+ continue
934
+
935
+ if (not opNames or op_type in opNames) and block_num_val:
936
+ if raw_ops:
937
+ yield {
938
+ "block_num": block_num_val,
939
+ "trx_num": trx_nr,
940
+ "op": [op_type, op],
941
+ "timestamp": timestamp_val,
942
+ }
943
+ else:
944
+ updated_op = {"type": op_type}
945
+ updated_op.update(op.copy())
946
+ updated_op.update(
947
+ {
948
+ "_id": self.hash_op(op_hash_input),
949
+ "timestamp": timestamp_val,
950
+ "block_num": block_num_val,
951
+ "trx_num": trx_nr,
952
+ "trx_id": trx_id,
953
+ }
954
+ )
955
+ yield updated_op
956
+ elif "operations" in block:
957
+ for event in block["operations"]:
958
+ trx_nr = event.get("trx_in_block", 0)
959
+ trx_id = event.get("trx_id", "")
960
+ op_type = ""
961
+ op = {}
962
+ op_hash_input = None
963
+ if "op" in event and isinstance(event["op"], list):
964
+ op_type, op = event["op"]
965
+ op_hash_input = event["op"]
966
+ elif (
967
+ "op" in event
968
+ and isinstance(event["op"], dict)
969
+ and "type" in event["op"]
970
+ and "value" in event["op"]
971
+ ):
972
+ op_type = event["op"]["type"]
973
+ if op_type.endswith("_operation"):
974
+ op_type = op_type[:-10]
975
+ op = event["op"]["value"]
976
+ op_hash_input = event["op"]
977
+ else:
978
+ continue
979
+ timestamp_val = event.get("timestamp", timestamp_val)
980
+ block_num_event = event.get("block", block_num_val)
981
+ if (not opNames or op_type in opNames) and block_num_event:
982
+ if raw_ops:
983
+ yield {
984
+ "block_num": block_num_event,
985
+ "trx_num": trx_nr,
986
+ "op": [op_type, op],
987
+ "timestamp": timestamp_val,
988
+ }
989
+ else:
990
+ updated_op = {"type": op_type}
991
+ updated_op.update(op.copy())
992
+ updated_op.update(
993
+ {
994
+ "_id": self.hash_op(op_hash_input),
995
+ "timestamp": timestamp_val,
996
+ "block_num": block_num_event,
997
+ "trx_num": trx_nr,
998
+ "trx_id": trx_id,
999
+ }
1000
+ )
1001
+ yield updated_op
1002
+
1003
+ def awaitTxConfirmation(
1004
+ self, transaction: Dict[str, Any], limit: int = 10
1005
+ ) -> Optional[Dict[str, Any]]:
1006
+ """Returns the transaction as seen by the blockchain after being
1007
+ included into a block
1008
+
1009
+ :param dict transaction: transaction to wait for
1010
+ :param int limit: (optional) number of blocks to wait for the transaction (default: 10)
1011
+
1012
+ .. note:: If you want instant confirmation, you need to instantiate
1013
+ class:`nectar.blockchain.Blockchain` with
1014
+ ``mode="head"``, otherwise, the call will wait until
1015
+ confirmed in an irreversible block.
1016
+
1017
+ .. note:: This method returns once the blockchain has included a
1018
+ transaction with the **same signature**. Even though the
1019
+ signature is not usually used to identify a transaction,
1020
+ it still cannot be forfeited and is derived from the
1021
+ transaction contented and thus identifies a transaction
1022
+ uniquely.
1023
+ """
1024
+ counter = 0
1025
+ for block in self.blocks():
1026
+ counter += 1
1027
+ for tx in block["transactions"]:
1028
+ if sorted(tx["signatures"]) == sorted(transaction["signatures"]):
1029
+ return tx
1030
+ if counter > limit:
1031
+ raise Exception("The operation has not been added after %d blocks!" % (limit))
1032
+
1033
+ @staticmethod
1034
+ def hash_op(event: Union[Dict[str, Any], List[Any]]) -> str:
1035
+ """This method generates a hash of blockchain operation."""
1036
+ if isinstance(event, dict) and "type" in event and "value" in event:
1037
+ op_type = event["type"]
1038
+ if len(op_type) > 10 and op_type[len(op_type) - 10 :] == "_operation":
1039
+ op_type = op_type[:-10]
1040
+ op = event["value"]
1041
+ event = [op_type, op]
1042
+ data = json.dumps(event, sort_keys=True)
1043
+ return hashlib.sha1(bytes(data, "utf-8")).hexdigest()
1044
+
1045
+ def get_all_accounts(
1046
+ self,
1047
+ start: str = "",
1048
+ stop: str = "",
1049
+ steps: Union[int, float] = 1e3,
1050
+ limit: int = -1,
1051
+ **kwargs: Any,
1052
+ ) -> Any:
1053
+ """Yields account names between start and stop.
1054
+
1055
+ :param str start: Start at this account name
1056
+ :param str stop: Stop at this account name
1057
+ :param int steps: Obtain ``steps`` ret with a single call from RPC
1058
+ """
1059
+ cnt = 1
1060
+ if not self.blockchain.is_connected():
1061
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
1062
+ if start == "":
1063
+ lastname = None
1064
+ else:
1065
+ lastname = start
1066
+ self.blockchain.rpc.set_next_node_on_empty_reply(True)
1067
+ while True:
1068
+ ret = self.blockchain.rpc.list_accounts(
1069
+ {"start": lastname, "limit": steps, "order": "by_name"}
1070
+ )["accounts"]
1071
+ for account in ret:
1072
+ if isinstance(account, dict):
1073
+ account_name = account["name"]
1074
+ else:
1075
+ account_name = account
1076
+ if account_name != lastname:
1077
+ yield account_name
1078
+ cnt += 1
1079
+ if account_name == stop or (limit > 0 and cnt > limit):
1080
+ return
1081
+ if lastname == account_name:
1082
+ return
1083
+ lastname = account_name
1084
+ if len(ret) < steps:
1085
+ return
1086
+
1087
+ def get_account_count(self) -> int:
1088
+ """Returns the number of accounts"""
1089
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
1090
+ ret = self.blockchain.rpc.get_account_count()
1091
+ return ret
1092
+
1093
+ def get_account_reputations(
1094
+ self,
1095
+ start: str = "",
1096
+ stop: str = "",
1097
+ steps: Union[int, float] = 1e3,
1098
+ limit: int = -1,
1099
+ **kwargs: Any,
1100
+ ) -> Any:
1101
+ """Yields account reputation between start and stop.
1102
+
1103
+ :param str start: Start at this account name
1104
+ :param str stop: Stop at this account name
1105
+ :param int steps: Obtain ``steps`` ret with a single call from RPC
1106
+ """
1107
+ cnt = 1
1108
+ if not self.blockchain.is_connected():
1109
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
1110
+ if start == "":
1111
+ lastname = None
1112
+ else:
1113
+ lastname = start
1114
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
1115
+ batch_limit = min(int(steps), 1000)
1116
+ first_batch = True
1117
+ while True:
1118
+ resp = self.blockchain.rpc.list_accounts(
1119
+ {"start": lastname, "limit": batch_limit, "order": "by_name"}
1120
+ )
1121
+ accounts = resp.get("accounts", []) if isinstance(resp, dict) else resp or []
1122
+ for account in accounts:
1123
+ account_name = account["name"]
1124
+ reputation = int(account.get("reputation", 0))
1125
+ if first_batch or account_name != lastname:
1126
+ yield {"account": account_name, "reputation": reputation}
1127
+ cnt += 1
1128
+ if account_name == stop or (limit > 0 and cnt > limit):
1129
+ return
1130
+ if not accounts:
1131
+ return
1132
+ lastname = accounts[-1]["name"]
1133
+ first_batch = False
1134
+ if len(accounts) < batch_limit or (stop and lastname == stop):
1135
+ return
1136
+
1137
+ def get_similar_account_names(self, name: str, limit: int = 5) -> Optional[List[str]]:
1138
+ """
1139
+ Return a list of accounts with names similar to the given name.
1140
+
1141
+ Performs an RPC call to fetch accounts starting at `name`. If the underlying node uses the
1142
+ appbase API this returns the raw `accounts` list from the `list_accounts` response (list of
1143
+ account objects/dicts). If using the legacy API this returns the list of account names
1144
+ returned by `lookup_accounts`. If the local blockchain connection is offline, returns None.
1145
+
1146
+ Parameters:
1147
+ name (str): Prefix or starting account name to search for.
1148
+ limit (int): Maximum number of accounts to return.
1149
+
1150
+ Returns:
1151
+ list or None: A list of account names or account objects (depending on RPC), or None if offline.
1152
+ """
1153
+ if not self.blockchain.is_connected():
1154
+ return None
1155
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
1156
+ account = self.blockchain.rpc.list_accounts(
1157
+ {"start": name, "limit": limit, "order": "by_name"}
1158
+ )
1159
+ if bool(account):
1160
+ return account["accounts"]
1161
+
1162
+ def find_rc_accounts(
1163
+ self, name: Union[str, List[str]]
1164
+ ) -> Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]:
1165
+ """
1166
+ Return resource credit (RC) parameters for one or more accounts.
1167
+
1168
+ If given a single account name (str), returns the RC parameters for that account as a dict.
1169
+ If given a list of account names, returns a list of RC-parameter dicts in the same order.
1170
+ Returns None when the underlying blockchain RPC is not connected or if the RPC returns no data.
1171
+
1172
+ Parameters:
1173
+ name (str | list): An account name or a list of account names.
1174
+
1175
+ Returns:
1176
+ dict | list | None: RC parameters for the account(s), or None if offline or no result.
1177
+ """
1178
+ if not self.blockchain.is_connected():
1179
+ return None
1180
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
1181
+ if isinstance(name, list):
1182
+ account = self.blockchain.rpc.find_rc_accounts({"accounts": name})
1183
+ if bool(account):
1184
+ return account["rc_accounts"]
1185
+ else:
1186
+ account = self.blockchain.rpc.find_rc_accounts({"accounts": [name]})
1187
+ if bool(account):
1188
+ return account["rc_accounts"][0]
1189
+
1190
+ def list_change_recovery_account_requests(
1191
+ self, start: Union[str, List[str]] = "", limit: int = 1000, order: str = "by_account"
1192
+ ) -> Optional[List[Dict[str, Any]]]:
1193
+ """
1194
+ Return pending change_recovery_account requests from the blockchain.
1195
+
1196
+ If the local blockchain connection is offline, returns None.
1197
+
1198
+ Parameters:
1199
+ start (str or list): Starting point for the listing.
1200
+ - For order="by_account": an account name (string).
1201
+ - For order="by_effective_date": a two-item list [effective_on, account_to_recover]
1202
+ e.g. ['2018-12-18T01:46:24', 'bott'].
1203
+ limit (int): Maximum number of results to return (default and max: 1000).
1204
+ order (str): Index to iterate: "by_account" (default) or "by_effective_date".
1205
+
1206
+ Returns:
1207
+ list or None: A list of change_recovery_account request entries, or None if offline.
1208
+ """
1209
+ if not self.blockchain.is_connected():
1210
+ return None
1211
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
1212
+ requests = self.blockchain.rpc.list_change_recovery_account_requests(
1213
+ {"start": start, "limit": limit, "order": order}
1214
+ )
1215
+ if bool(requests):
1216
+ return requests["requests"]
1217
+
1218
+ def find_change_recovery_account_requests(
1219
+ self, accounts: Union[str, List[str]]
1220
+ ) -> Optional[List[Dict[str, Any]]]:
1221
+ """
1222
+ Find pending change_recovery_account requests for one or more accounts.
1223
+
1224
+ Accepts a single account name or a list of account names and queries the connected node for pending
1225
+ change_recovery_account requests. Returns the list of matching request entries, or None if the
1226
+ local blockchain instance is not connected or the RPC returned no data.
1227
+
1228
+ Parameters:
1229
+ accounts (str | list): Account name or list of account names to search for.
1230
+
1231
+ Returns:
1232
+ list | None: List of change_recovery_account request objects for the given account(s), or
1233
+ None if offline or no requests were returned.
1234
+ """
1235
+ if not self.blockchain.is_connected():
1236
+ return None
1237
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
1238
+ if isinstance(accounts, str):
1239
+ accounts = [accounts]
1240
+ requests = self.blockchain.rpc.find_change_recovery_account_requests({"accounts": accounts})
1241
+ if bool(requests):
1242
+ return requests["requests"]