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