From 8fef5541ef22cbd53f5629f6fddf7faee70f1d31 Mon Sep 17 00:00:00 2001 From: Holger Nahrstaedt <holger@nahrstaedt.de> Date: Mon, 7 May 2018 17:44:11 +0200 Subject: [PATCH] Virtual-Ops support added for block and blockchain. Comment improved. More CLI functions. Account * estimate_account_op renamed to estimate_virtual_op_num Block * only_ops and only_virtual_ops added as new parameter * the transactions property returns a list of transactions * The operations property returns a list of operations * Block which contain only only_ops can be received now Blockchain * only_ops and only_virtual_ops added to get_current_block, blocks, wait_for_and_get_block and stream * ops() is obsolete now * stream uses now blocks internally * only_ops=True streams now also virtual operaitons cli * autoconnect is the to False now and only when needed a stm.rpc.rpcconnect() is performed * ascii option added to all plots * rewards added, this new command lists outstanding rewards (posts, comment and votes) * curation added which shows the vote curation rewards for a single post * verify added, which returns the public signing key of a transaction Comment * reward, is_pending, time_elapsed, curation_penalty_compensation_SBD, estimate_curation_SBD, curation_reward_pct, get_vote, get_beneficiaries_pct, get_rewards, get_author_rewards and get_curation_rewards added Unit tests * new function added and tests adapted to changes --- beem/account.py | 60 ++-- beem/block.py | 78 ++++- beem/blockchain.py | 219 +++++++------ beem/cli.py | 398 ++++++++++++++++++++++-- beem/comment.py | 238 +++++++++++++- beemgraphenebase/signedtransactions.py | 2 +- examples/memory_profiler1.py | 2 +- tests/beem/test_block.py | 26 +- tests/beem/test_blockchain.py | 33 +- tests/beem/test_blockchain_batch.py | 4 +- tests/beem/test_blockchain_threading.py | 3 +- tests/beem/test_cli.py | 21 ++ 12 files changed, 910 insertions(+), 174 deletions(-) diff --git a/beem/account.py b/beem/account.py index 7f546327..f17dba22 100644 --- a/beem/account.py +++ b/beem/account.py @@ -811,7 +811,7 @@ class Account(BlockchainObject): :rtype: list """ if until is not None: - return self.estimate_account_op(until, op_accuracy=1) + return self.estimate_virtual_op_num(until, accuracy=1) else: try: op_count = 0 @@ -837,12 +837,14 @@ class Account(BlockchainObject): ret = self.steem.rpc.get_account_history(account["name"], start, limit, api="database") return ret - def estimate_account_op(self, start, op_accuracy=10, max_count=10, reverse=False): - """ Returns a estiamtion of account operation index for a given time or blockindex + def estimate_virtual_op_num(self, blocktime, accuracy=10, max_count=-1, reverse=False): + """ Returns an estimation of an virtual operation index for a given time or blockindex - :param int/datetime start: start time or start block index from which account + :param int/datetime blocktime: start time or start block index from which account operation should be fetched - :param int op_accuracy: defines the estimation accuracy (default 10) + :param int accuracy: defines the estimation accuracy (default 10) + :param int max_count: sets the maximum number of iterations. -1 disables this (default -1) + :param bool revers: Set to true when used in history_reverse (default is False) Example::: @@ -853,43 +855,43 @@ class Account(BlockchainObject): utc = pytz.timezone('UTC') start_time = utc.localize(datetime.utcnow()) - timedelta(days=7) acc = Account("gtg") - start_op = acc.estimate_account_op(start_time) + start_op = acc.estimate_virtual_op_num(start_time) b = Blockchain() start_block_num = b.get_estimated_block_num(start_time) - start_op2 = acc.estimate_account_op(start_block_num) + start_op2 = acc.estimate_virtual_op_num(start_block_num) """ max_index = self.virtual_op_count() created = self["created"] - if not isinstance(start, datetime): + if not isinstance(blocktime, datetime): b = Blockchain(steem_instance=self.steem) current_block_num = b.get_current_block_num() created_blocknum = b.get_estimated_block_num(created, accurate=True) - if start < created_blocknum and not reverse: + if blocktime < created_blocknum and not reverse: return 0 - elif start < created_blocknum: + elif blocktime < created_blocknum: return max_index else: - if start < created and not reverse: + if blocktime < created and not reverse: return 0 - elif start < created: + elif blocktime < created: return max_index - if max_index < op_accuracy and not reverse: + if max_index < accuracy and not reverse: return 0 - elif max_index < op_accuracy: + elif max_index < accuracy: return max_index - if isinstance(start, datetime): + if isinstance(blocktime, datetime): utc = pytz.timezone('UTC') now = utc.localize(datetime.utcnow()) account_lifespan_sec = (now - created).total_seconds() - start = addTzInfo(start) - estimated_op_num = int((start - created).total_seconds() / account_lifespan_sec * max_index) + blocktime = addTzInfo(blocktime) + estimated_op_num = int((blocktime - created).total_seconds() / account_lifespan_sec * max_index) else: account_lifespan_block = (current_block_num - created_blocknum) - estimated_op_num = int((start - created_blocknum) / account_lifespan_block * max_index) - op_diff = op_accuracy + 1 + estimated_op_num = int((blocktime - created_blocknum) / account_lifespan_block * max_index) + op_diff = accuracy + 1 cnt = 0 - while op_diff > op_accuracy and cnt < max_count: + while op_diff > accuracy and (max_count < 0 or cnt < max_count): op_start = self._get_account_history(start=estimated_op_num) if isinstance(op_start, list) and len(op_start) > 0 and len(op_start[0]) > 0: trx = op_start[0][1] @@ -898,12 +900,12 @@ class Account(BlockchainObject): return 0 else: return max_index - if isinstance(start, datetime): + if isinstance(blocktime, datetime): diff_time = (now - formatTimeString(trx["timestamp"])).total_seconds() - op_diff = ((start - formatTimeString(trx["timestamp"])).total_seconds() / diff_time * (max_index - estimated_op_num)) + op_diff = ((blocktime - formatTimeString(trx["timestamp"])).total_seconds() / diff_time * (max_index - estimated_op_num)) else: diff_block = (current_block_num - trx["block"]) - op_diff = ((start - trx["block"]) / diff_block * (max_index - estimated_op_num)) + op_diff = ((blocktime - trx["block"]) / diff_block * (max_index - estimated_op_num)) if reverse: estimated_op_num += math.ceil(op_diff) else: @@ -917,10 +919,10 @@ class Account(BlockchainObject): return estimated_op_num elif int(op_diff) == 0 and not reverse: return estimated_op_num - elif estimated_op_num > op_accuracy and not reverse: - return estimated_op_num - op_accuracy - elif estimated_op_num + op_accuracy < max_index: - return estimated_op_num + op_accuracy + elif estimated_op_num > accuracy and not reverse: + return estimated_op_num - accuracy + elif estimated_op_num + accuracy < max_index: + return estimated_op_num + accuracy elif reverse: return max_index else: @@ -1141,7 +1143,7 @@ class Account(BlockchainObject): if start is not None and not use_block_num and not isinstance(start, datetime): start_index = start elif start is not None: - start_index = self.estimate_account_op(start, op_accuracy=10) + start_index = self.estimate_virtual_op_num(start, accuracy=10) else: start_index = 0 @@ -1283,7 +1285,7 @@ class Account(BlockchainObject): elif start is not None and isinstance(start, int) and not use_block_num: first = start elif start is not None: - first = self.estimate_account_op(start, op_accuracy=10, reverse=True) + first = self.estimate_virtual_op_num(start, accuracy=10, reverse=True) if stop is not None and isinstance(stop, int) and stop < 0 and not use_block_num: stop += first start = addTzInfo(start) diff --git a/beem/block.py b/beem/block.py index b6b89869..568a87c5 100644 --- a/beem/block.py +++ b/beem/block.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals from .exceptions import BlockDoesNotExistsException from .utils import parse_time from .blockchainobject import BlockchainObject +from beemapi.exceptions import ApiNotSupported class Block(BlockchainObject): @@ -15,6 +16,8 @@ class Block(BlockchainObject): :param beem.steem.Steem steem_instance: Steem instance :param bool lazy: Use lazy loading + :param bool only_ops: Includes only operations, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) Instances of this class are dictionaries that come with additional methods (see below) that allow dealing with a block and it's @@ -34,6 +37,34 @@ class Block(BlockchainObject): refreshed with ``Account.refresh()``. """ + def __init__( + self, + block, + only_ops=False, + only_virtual_ops=False, + full=True, + lazy=False, + steem_instance=None + ): + """ Initilize a block + + :param int block: block number + :param beem.steem.Steem steem_instance: Steem + instance + :param bool lazy: Use lazy loading + :param bool only_ops: Includes only operations, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) + + """ + self.full = full + self.only_ops = only_ops + self.only_virtual_ops = only_virtual_ops + super(Block, self).__init__( + block, + lazy=lazy, + full=full, + steem_instance=steem_instance + ) def refresh(self): """ Even though blocks never change, you freshly obtain its contents @@ -41,12 +72,24 @@ class Block(BlockchainObject): """ if not isinstance(self.identifier, int): self.identifier = int(self.identifier) - if self.steem.rpc.get_use_appbase(): - block = self.steem.rpc.get_block({"block_num": self.identifier}, api="block") - if block and "block" in block: - block = block["block"] + if self.only_ops: + if self.steem.rpc.get_use_appbase(): + try: + ops = self.steem.rpc.get_ops_in_block({"block_num": self.identifier, 'only_virtual': self.only_virtual_ops}, api="account_history")["ops"] + except ApiNotSupported: + ops = self.steem.rpc.get_ops_in_block(self.identifier, self.only_virtual_ops) + else: + ops = self.steem.rpc.get_ops_in_block(self.identifier, self.only_virtual_ops) + block = {'block': ops[0]["block"], + 'timestamp': ops[0]["timestamp"], + 'operations': ops} else: - block = self.steem.rpc.get_block(self.identifier) + if self.steem.rpc.get_use_appbase(): + block = self.steem.rpc.get_block({"block_num": self.identifier}, api="block") + if block and "block" in block: + block = block["block"] + else: + block = self.steem.rpc.get_block(self.identifier) if not block: raise BlockDoesNotExistsException(str(self.identifier)) super(Block, self).__init__(block, steem_instance=self.steem) @@ -60,8 +103,25 @@ class Block(BlockchainObject): """Return a datatime instance for the timestamp of this block""" return parse_time(self['timestamp']) - def ops(self): - """Returns all block operations""" + @property + def transactions(self): + """ Returns all transactions as list""" + if self.only_ops: + return None + trxs = self["transactions"] + for i in range(len(trxs)): + trx = trxs[i] + trx['transaction_id'] = self['transaction_ids'][i] + trx['block_num'] = self.block_num + trx['transaction_num'] = i + trxs[i] = trx + return trxs + + @property + def operations(self): + """Returns all block operations as list""" + if self.only_ops: + return self["operations"] ops = [] trxs = self["transactions"] for tx in trxs: @@ -80,6 +140,10 @@ class Block(BlockchainObject): ops_stat[key] = 0 else: ops_stat = add_to_ops_stat.copy() + if self.only_ops: + for op in self["operations"]: + ops_stat[op["op"][0]] += 1 + return ops_stat trxs = self["transactions"] for tx in trxs: for op in tx["operations"]: diff --git a/beem/blockchain.py b/beem/blockchain.py index 23dac556..5cc677bc 100644 --- a/beem/blockchain.py +++ b/beem/blockchain.py @@ -12,8 +12,9 @@ import hashlib import json import math from datetime import datetime, timedelta +from .utils import formatTimeString from .block import Block -from .exceptions import BatchedCallsNotSupported +from .exceptions import BatchedCallsNotSupported, BlockDoesNotExistsException from .blockchainobject import BlockchainObject from beemgraphenebase.py23 import py23_bytes from beem.instance import shared_steem_instance @@ -96,10 +97,22 @@ class Blockchain(object): self.max_block_wait_repetition = max_block_wait_repetition else: self.max_block_wait_repetition = 3 + self.block_interval = self.steem.get_block_interval() def is_irreversible_mode(self): return self.mode == 'last_irreversible_block_num' + def get_transaction(self, transaction_id): + """ Returns a transaction from the blockchain + + :param str transaction_id: transaction_id + """ + if self.steem.rpc.get_use_appbase(): + ret = self.steem.rpc.get_transaction({'id': transaction_id}, api="database") + else: + ret = self.steem.rpc.get_transaction(transaction_id, api="database") + return ret + def get_current_block_num(self): """ This call returns the current block number @@ -109,14 +122,19 @@ class Blockchain(object): props = self.steem.get_dynamic_global_properties(False) return int(props.get(self.mode)) - def get_current_block(self): + def get_current_block(self, only_ops=False, only_virtual_ops=False): """ This call returns the current block + :param bool only_ops: Returns block with operations only, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) + .. note:: The block number returned depends on the ``mode`` used when instanciating from this class. """ return Block( self.get_current_block_num(), + only_ops=only_ops, + only_virtual_ops=only_virtual_ops, steem_instance=self.steem ) @@ -128,25 +146,24 @@ class Blockchain(object): .. note:: The block number returned depends on the ``mode`` used when instanciating from this class. """ - block_time_seconds = self.steem.get_block_interval() last_block = self.get_current_block() if estimateForwards: block_offset = 10 first_block = Block(block_offset, steem_instance=self.steem) time_diff = date - first_block.time() - block_number = math.floor(time_diff.total_seconds() / block_time_seconds + block_offset) + block_number = math.floor(time_diff.total_seconds() / self.block_interval + block_offset) else: time_diff = last_block.time() - date - block_number = math.floor(last_block.identifier - time_diff.total_seconds() / block_time_seconds) + block_number = math.floor(last_block.identifier - time_diff.total_seconds() / self.block_interval) if accurate: if block_number > last_block.identifier: block_number = last_block.identifier block_time_diff = timedelta(seconds=10) - while block_time_diff.total_seconds() > block_time_seconds or block_time_diff.total_seconds() < -block_time_seconds: + while block_time_diff.total_seconds() > self.block_interval or block_time_diff.total_seconds() < -self.block_interval: block = Block(block_number, steem_instance=self.steem) block_time_diff = date - block.time() - delta = block_time_diff.total_seconds() // block_time_seconds + delta = block_time_diff.total_seconds() // self.block_interval if delta == 0 and block_time_diff.total_seconds() < 0: delta = -1 elif delta == 0 and block_time_diff.total_seconds() > 0: @@ -180,21 +197,29 @@ class Blockchain(object): ).time() return int(time.mktime(block_time.timetuple())) - def blocks(self, start=None, stop=None, max_batch_size=None, threading=False, thread_num=8): + def blocks(self, start=None, stop=None, max_batch_size=None, threading=False, thread_num=8, only_ops=False, only_virtual_ops=False): """ Yields blocks starting from ``start``. :param int start: Starting block :param int stop: Stop at this block - :param str mode: We here have the choice between - "head" (the last block) and "irreversible" (the block that is - confirmed by 2/3 of all block producers and is thus irreversible) + :param int max_batch_size: only for appbase nodes. When not None, batch calls of are used. + Cannot combine with threading + :param bool threading: Enables threading. Cannot be combined with batch calls + :param int thread_num: Defines the number of threads, when `threading` is set. + :param bool only_ops: Only yielding operations, when set to True (default: False) + :param bool only_virtual_ops: Only yield virtual operations (default: False) + + .. note:: If you want instant confirmation, you need to instantiate + class:`beem.blockchain.Blockchain` with + ``mode="head"``, otherwise, the call will wait until + confirmed in an irreversible block. + """ # Let's find out how often blocks are generated! - self.block_interval = self.steem.get_block_interval() current_block_num = self.get_current_block_num() if not start: start = current_block_num - + head_block_reached = False # We are going to loop indefinitely while True: @@ -204,14 +229,14 @@ class Blockchain(object): else: current_block_num = self.get_current_block_num() head_block = current_block_num - if threading and FUTURES_MODULE: + if threading and FUTURES_MODULE and not head_block_reached: pool = ThreadPoolExecutor(max_workers=thread_num + 1) latest_block = 0 for blocknum in range(start, head_block + 1, thread_num): futures = [] i = blocknum while i < blocknum + thread_num and i <= head_block: - futures.append(pool.submit(Block, i, steem_instance=self.steem)) + futures.append(pool.submit(Block, i, only_ops=only_ops, only_virtual_ops=only_virtual_ops, steem_instance=self.steem)) i += 1 results = [r.result() for r in as_completed(futures)] block_nums = [] @@ -225,9 +250,9 @@ class Blockchain(object): yield b if latest_block < head_block: for blocknum in range(latest_block, head_block + 1): - block = Block(blocknum, steem_instance=self.steem) + block = Block(blocknum, only_ops=only_ops, only_virtual_ops=only_virtual_ops, steem_instance=self.steem) yield block - elif max_batch_size is not None and (head_block - start) >= max_batch_size: + elif max_batch_size is not None and (head_block - start) >= max_batch_size and not head_block_reached: latest_block = start - 1 batches = max_batch_size for blocknumblock in range(start, head_block + 1, batches): @@ -235,18 +260,32 @@ class Blockchain(object): if (head_block - blocknumblock) < batches: batches = head_block - blocknumblock + 1 for blocknum in range(blocknumblock, blocknumblock + batches - 1): - if self.steem.rpc.get_use_appbase(): - self.steem.rpc.get_block({"block_num": blocknum}, api="block", add_to_queue=True) + if only_virtual_ops: + if self.steem.rpc.get_use_appbase(): + # self.steem.rpc.get_ops_in_block({"block_num": blocknum, 'only_virtual': only_virtual_ops}, api="account_history", add_to_queue=True) + self.steem.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=True) + else: + self.steem.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=True) else: - self.steem.rpc.get_block(blocknum, add_to_queue=True) + if self.steem.rpc.get_use_appbase(): + self.steem.rpc.get_block({"block_num": blocknum}, api="block", add_to_queue=True) + else: + self.steem.rpc.get_block(blocknum, add_to_queue=True) latest_block = blocknum if batches >= 1: latest_block += 1 if latest_block <= head_block: - if self.steem.rpc.get_use_appbase(): - block_batch = self.steem.rpc.get_block({"block_num": latest_block}, api="block", add_to_queue=False) + if only_virtual_ops: + if self.steem.rpc.get_use_appbase(): + # self.steem.rpc.get_ops_in_block({"block_num": blocknum, 'only_virtual': only_virtual_ops}, api="account_history", add_to_queue=False) + block_batch = self.steem.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=False) + else: + block_batch = self.steem.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=False) else: - block_batch = self.steem.rpc.get_block(latest_block, add_to_queue=False) + if self.steem.rpc.get_use_appbase(): + block_batch = self.steem.rpc.get_block({"block_num": latest_block}, api="block", add_to_queue=False) + else: + block_batch = self.steem.rpc.get_block(latest_block, add_to_queue=False) if not bool(block_batch): raise BatchedCallsNotSupported() blocknum = latest_block - len(block_batch) + 1 @@ -254,18 +293,22 @@ class Blockchain(object): block_batch = [block_batch] for block in block_batch: if self.steem.rpc.get_use_appbase(): - block = block["block"] + if only_virtual_ops: + block = block["ops"] + else: + block = block["block"] block["id"] = blocknum - yield Block(block, steem_instance=self.steem) + yield Block(block, only_ops=only_ops, only_virtual_ops=only_virtual_ops, steem_instance=self.steem) blocknum += 1 else: # Blocks from start until head block for blocknum in range(start, head_block + 1): # Get full block - block = self.wait_for_and_get_block(blocknum, last_fetched_block_num=current_block_num) + block = self.wait_for_and_get_block(blocknum, only_ops=only_ops, only_virtual_ops=only_virtual_ops) yield block # Set new start start = head_block + 1 + head_block_reached = True if stop and start > stop: # raise StopIteration @@ -274,18 +317,20 @@ class Blockchain(object): # Sleep for one block time.sleep(self.block_interval) - def wait_for_and_get_block(self, block_number, blocks_waiting_for=None, last_fetched_block_num=None): + def wait_for_and_get_block(self, block_number, blocks_waiting_for=None, only_ops=False, only_virtual_ops=False): """ Get the desired block from the chain, if the current head block is smaller (for both head and irreversible) then we wait, but a maxmimum of blocks_waiting_for * max_block_wait_repetition time before failure. :param int block_number: desired block number :param int blocks_waiting_for: (default) difference between block_number and current head how many blocks we are willing to wait, positive int + :param bool only_ops: Returns blocks with operations only, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) """ - if last_fetched_block_num is None or (last_fetched_block_num is not None and block_number > last_fetched_block_num): - if not blocks_waiting_for: - blocks_waiting_for = max(1, block_number - self.get_current_block_num()) + if not blocks_waiting_for: + blocks_waiting_for = max( + 1, block_number - self.get_current_block_num()) repetition = 0 # can't return the block before the chain has reached it (support future block_num) @@ -295,41 +340,26 @@ class Blockchain(object): if repetition > blocks_waiting_for * self.max_block_wait_repetition: raise Exception("Wait time for new block exceeded, aborting") # block has to be returned properly - block = Block(block_number, steem_instance=self.steem) + try: + block = Block(block_number, only_ops=only_ops, only_virtual_ops=only_virtual_ops, steem_instance=self.steem) + except BlockDoesNotExistsException: + block = None repetition = 0 while not block: repetition += 1 time.sleep(self.block_interval) - if repetition > self.max_block_wait_repetition: + if repetition > blocks_waiting_for * self.max_block_wait_repetition: raise Exception("Wait time for new block exceeded, aborting") - block = Block(block_number, steem_instance=self.steem) + try: + block = Block(block_number, only_ops=only_ops, only_virtual_ops=only_virtual_ops, steem_instance=self.steem) + except BlockDoesNotExistsException: + block = None return block - def ops(self, start=None, stop=None, **kwargs): - """ Yields all operations (including virtual operations) starting from - ``start``. - - :param int start: Starting block - :param int stop: Stop at this block - :param str mode: We here have the choice between - "head" (the last block) and "irreversible" (the block that is - confirmed by 2/3 of all block producers and is thus irreversible) - :param bool only_virtual_ops: Only yield virtual operations - - This call returns a list that only carries one operation and - its type! + def ops(self, start=None, stop=None, only_virtual_ops=False, **kwargs): + """ Blockchain.ops() is deprecated. Please use Blockchain.stream() instead. """ - - for block in self.blocks(start=start, stop=stop, **kwargs): - for tx in block["transactions"]: - for op in tx["operations"]: - # Replace opid by op name - # op[0] = getOperationNameForId(op[0]) - yield { - "block_num": block.identifier, - "op": op, - "timestamp": block["timestamp"] - } + raise DeprecationWarning('Blockchain.ops() is deprecated. Please use Blockchain.stream() instead.') def ops_statistics(self, start, stop=None, add_to_ops_stat=None, verbose=False): """ Generates a statistics for all operations (including virtual operations) starting from @@ -355,15 +385,10 @@ class Blockchain(object): return if stop is None: stop = current_block - for block in self.blocks(start=start, stop=stop): + for block in self.blocks(start=start, stop=stop, only_ops=True): if verbose: - print(block.identifier + " " + block["timestamp"]) - for tx in block["transactions"]: - for op in tx["operations"]: - if isinstance(op, list): - ops_stat[op[0]] += 1 - elif isinstance(op, dict): - ops_stat[op["type"][:-10]] += 1 + print(block["identifier"] + " " + block["timestamp"]) + ops_stat = block.ops_statistics(add_to_ops_stat=ops_stat) return ops_stat def stream(self, opNames=[], *args, **kwargs): @@ -372,34 +397,54 @@ class Blockchain(object): :param array opNames: List of operations to filter for :param int start: Start at this block :param int stop: Stop at this block - :param str mode: We here have the choice between - "head" (the last block) and "irreversible" (the block that is - confirmed by 2/3 of all block producers and is thus irreversible) + :param int max_batch_size: only for appbase nodes. When not None, batch calls of are used. + Cannot combine with threading + :param bool threading: Enables threading. Cannot be combined with batch calls + :param int thread_num: Defines the number of threads, when `threading` is set. + :param bool only_ops: Only yielding operations, when set to True (default: False) + :param bool only_virtual_ops: Only yield virtual operations (default: False) The dict output is formated such that ``type`` caries the operation type, timestamp and block_num are taken from the block the operation was stored in and the other key depend on the actualy operation. + + .. note:: If you want instant confirmation, you need to instantiate + class:`beem.blockchain.Blockchain` with + ``mode="head"``, otherwise, the call will wait until + confirmed in an irreversible block. + """ - for op in self.ops(**kwargs): - if isinstance(op["op"], list): - if not opNames or op["op"][0] in opNames: - r = { - "type": op["op"][0], - "timestamp": op.get("timestamp"), - "block_num": op.get("block_num"), - } - r.update(op["op"][1]) - yield r - elif isinstance(op["op"], dict): - if not opNames or op["op"]["type"][:-10] in opNames: - r = { - "type": op["op"]["type"][:-10], - "timestamp": op.get("timestamp"), - "block_num": op.get("block_num"), - } - r.update(op["op"]["value"]) - yield r + for block in self.blocks(**kwargs): + if "transactions" in block: + trx = block["transactions"] + else: + trx = [block] + for trx_nr in range(len(trx)): + for event in trx[trx_nr]["operations"]: + if isinstance(event, list): + op_type, op = event + trx_id = block["transaction_ids"][trx_nr] + block_num = block.get("id") + _id = self.hash_op(event) + timestamp = formatTimeString(block.get("timestamp")) + else: + op_type, op = event["op"] + trx_id = event.get("trx_id") + block_num = event.get("block") + _id = self.hash_op(event["op"]) + timestamp = formatTimeString(event.get("timestamp")) + if not opNames or op_type in opNames: + if kwargs.get('raw_output'): + yield event + else: + updated_op = {"type": op_type} + updated_op.update(op.copy()) + updated_op.update({"_id": _id, + "timestamp": timestamp, + "block_num": block_num, + "trx_id": trx_id}) + yield updated_op def awaitTxConfirmation(self, transaction, limit=10): """ Returns the transaction as seen by the blockchain after being diff --git a/beem/cli.py b/beem/cli.py index f1d90372..3f2a9c9c 100644 --- a/beem/cli.py +++ b/beem/cli.py @@ -13,6 +13,7 @@ from beem.comment import Comment from beem.market import Market from beem.block import Block from beem.profile import Profile +from beem.wallet import Wallet from beem.asset import Asset from beem.witness import Witness, WitnessesRankedByVote, WitnessesVotedByAccount from beem.blockchain import Blockchain @@ -22,10 +23,12 @@ from beem import exceptions from beem.version import version as __version__ from datetime import datetime, timedelta from beem.asciichart import AsciiChart +from beem.transactionbuilder import TransactionBuilder import pytz from timeit import default_timer as timer from beembase import operations from beemgraphenebase.account import PrivateKey, PublicKey +from beemgraphenebase.base58 import Base58 import os import ast import json @@ -168,7 +171,11 @@ def cli(node, offline, no_broadcast, no_wallet, unsigned, expires, verbose): nowallet=no_wallet, unsigned=unsigned, expiration=expires, - debug=debug + debug=debug, + num_retries=10, + num_retries_call=3, + timeout=10, + autoconnect=False ) set_shared_steem_instance(stm) @@ -190,6 +197,8 @@ def set(key, value): """ stm = shared_steem_instance() if key == "default_account": + if stm.rpc: + stm.rpc.rpcconnect() stm.set_default_account(value) elif key == "default_vote_weight": stm.set_default_vote_weight(value) @@ -220,6 +229,8 @@ def nextnode(results): """ Uses the next node in list """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() stm.move_current_node_to_front() node = stm.get_default_nodes() offline = stm.offline @@ -266,6 +277,8 @@ def pingnode(raw, sort, remove, threading): """ Returns the answer time in milliseconds """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() nodes = stm.get_default_nodes() if not raw: t = PrettyTable(["Node", "Answer time [ms]"]) @@ -324,6 +337,8 @@ def currentnode(version, url): """ Sets the currently working node at the first place in the list """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() offline = stm.offline stm.move_current_node_to_front() node = stm.get_default_nodes() @@ -466,6 +481,8 @@ def addkey(unsafe_import_key): and a prompt for entering the private key are shown. """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not unlock_wallet(stm): return if not unsafe_import_key: @@ -487,6 +504,8 @@ def delkey(confirm, pub): which will be deleted from the wallet """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not unlock_wallet(stm): return stm.wallet.removePrivateKeyFromPublicKey(pub) @@ -509,6 +528,8 @@ def listkeys(): def listaccounts(): """Show stored accounts""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() t = PrettyTable(["Name", "Type", "Available Key"]) t.align = "l" for account in stm.wallet.getAccounts(): @@ -530,6 +551,8 @@ def upvote(post, vote_weight, account, weight): POST is @author/permlink """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not weight and vote_weight: weight = vote_weight elif not weight and not vote_weight: @@ -559,6 +582,8 @@ def downvote(post, vote_weight, account, weight): POST is @author/permlink """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not weight and vote_weight: weight = vote_weight elif not weight and not vote_weight: @@ -586,6 +611,8 @@ def downvote(post, vote_weight, account, weight): def transfer(to, amount, asset, memo, account): """Transfer SBD/STEEM""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not bool(memo): @@ -605,6 +632,8 @@ def transfer(to, amount, asset, memo, account): def powerup(amount, account, to): """Power up (vest STEEM as STEEM POWER)""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -628,6 +657,8 @@ def powerdown(amount, account): amount is in VESTS """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -651,6 +682,8 @@ def powerdown(amount, account): def powerdownroute(to, percentage, account, auto_vest): """Setup a powerdown route""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -667,6 +700,8 @@ def powerdownroute(to, percentage, account, auto_vest): def convert(amount, account): """Convert STEEMDollars to Steem (takes a week to settle)""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -707,6 +742,8 @@ def power(account): """ Shows vote power and bandwidth """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if len(account) == 0: if "default_account" in stm.config: account = [stm.config["default_account"]] @@ -722,6 +759,8 @@ def balance(account): """ Shows balance """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if len(account) == 0: if "default_account" in stm.config: account = [stm.config["default_account"]] @@ -763,6 +802,8 @@ def interest(account): """ Get information about interest payment """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: if "default_account" in stm.config: account = [stm.config["default_account"]] @@ -791,6 +832,8 @@ def follower(account): """ Get information about followers """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: if "default_account" in stm.config: account = [stm.config["default_account"]] @@ -807,6 +850,8 @@ def following(account): """ Get information about following """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: if "default_account" in stm.config: account = [stm.config["default_account"]] @@ -823,6 +868,8 @@ def muter(account): """ Get information about muter """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: if "default_account" in stm.config: account = [stm.config["default_account"]] @@ -839,6 +886,8 @@ def muting(account): """ Get information about muting """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: if "default_account" in stm.config: account = [stm.config["default_account"]] @@ -855,6 +904,8 @@ def permissions(account): """ Show permissions of an account """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: if "default_account" in stm.config: account = stm.config["default_account"] @@ -891,6 +942,8 @@ def allow(foreign_account, permission, account, weight, threshold): This derived key will then interact with your account. """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -917,6 +970,8 @@ def allow(foreign_account, permission, account, weight, threshold): def disallow(foreign_account, permission, account, threshold): """Remove allowance an account/key to interact with your account""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -928,7 +983,7 @@ def disallow(foreign_account, permission, account, threshold): if not foreign_account: from beemgraphenebase.account import PasswordKey pwd = click.prompt("Password for Key Derivation", confirmation_prompt=True) - foreign_account = format(PasswordKey(account, pwd, permission).get_public(), stm.prefix) + foreign_account = [format(PasswordKey(account, pwd, permission).get_public(), stm.prefix)] tx = acc.disallow(foreign_account, permission=permission, threshold=threshold) tx = json.dumps(tx, indent=4) print(tx) @@ -941,6 +996,8 @@ def disallow(foreign_account, permission, account, threshold): def newaccount(accountname, account, fee): """Create a new account""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -963,6 +1020,8 @@ def newaccount(accountname, account, fee): def setprofile(variable, value, account, pair): """Set a variable in an account\'s profile""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() keys = [] values = [] if pair: @@ -996,6 +1055,8 @@ def setprofile(variable, value, account, pair): def delprofile(variable, account): """Delete a variable in an account\'s profile""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] @@ -1019,6 +1080,8 @@ def importaccount(account, roles): """Import an account using a passphrase""" from beemgraphenebase.account import PasswordKey stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not unlock_wallet(stm): return account = Account(account, steem_instance=stm) @@ -1075,6 +1138,8 @@ def importaccount(account, roles): def updatememokey(account, key): """Update an account\'s memo key""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -1099,6 +1164,8 @@ def updatememokey(account, key): def approvewitness(witness, account): """Approve a witnesses""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -1115,6 +1182,8 @@ def approvewitness(witness, account): def disapprovewitness(witness, account): """Disapprove a witnesses""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -1130,6 +1199,8 @@ def disapprovewitness(witness, account): def sign(file): """Sign a provided transaction with available and required keys""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if file and file != "-": if not os.path.isfile(file): raise Exception("File %s does not exist!" % file) @@ -1148,6 +1219,8 @@ def sign(file): def broadcast(file): """broadcast a signed transaction""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if file and file != "-": if not os.path.isfile(file): raise Exception("File %s does not exist!" % file) @@ -1166,6 +1239,8 @@ def ticker(): """ Show ticker """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() t = PrettyTable(["Key", "Value"]) t.align = "l" market = Market(steem_instance=stm) @@ -1176,12 +1251,15 @@ def ticker(): @cli.command() -@click.option('--width', help='Plot width (default 75)', default=75) -@click.option('--height', help='Plot height (default 15)', default=15) -def pricehistory(width, height): +@click.option('--width', '-w', help='Plot width (default 75)', default=75) +@click.option('--height', '-h', help='Plot height (default 15)', default=15) +@click.option('--ascii', help='Use only ascii symbols', is_flag=True, default=False) +def pricehistory(width, height, ascii): """ Show price history """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() feed_history = stm.get_feed_history() current_base = Amount(feed_history['current_median_history']["base"], steem_instance=stm) current_quote = Amount(feed_history['current_median_history']["quote"], steem_instance=stm) @@ -1191,7 +1269,11 @@ def pricehistory(width, height): base = Amount(h["base"]) quote = Amount(h["quote"]) price.append(base.amount / quote.amount) - chart = AsciiChart(height=height, width=width, offset=4, placeholder='{:6.2f} $') + if ascii: + charset = u'ascii' + else: + charset = u'utf8' + chart = AsciiChart(height=height, width=width, offset=4, placeholder='{:6.2f} $', charset=charset) print("\n Price history for STEEM (median price %4.2f $)\n" % (float(current_base) / float(current_quote))) chart.adapt_on_series(price) @@ -1203,15 +1285,18 @@ def pricehistory(width, height): @cli.command() -@click.option('--days', help='Limit the days of shown trade history (default 7)', default=7) +@click.option('--days', '-d', help='Limit the days of shown trade history (default 7)', default=7) @click.option('--hours', help='Limit the intervall history intervall (default 2 hours)', default=2.0) -@click.option('--limit', help='Limit number of trades which is fetched at each intervall point (default 100)', default=100) -@click.option('--width', help='Plot width (default 75)', default=75) -@click.option('--height', help='Plot height (default 15)', default=15) -def tradehistory(days, hours, limit, width, height): +@click.option('--limit', '-l', help='Limit number of trades which is fetched at each intervall point (default 100)', default=100) +@click.option('--width', '-w', help='Plot width (default 75)', default=75) +@click.option('--height', '-h', help='Plot height (default 15)', default=15) +@click.option('--ascii', help='Use only ascii symbols', is_flag=True, default=False) +def tradehistory(days, hours, limit, width, height, ascii): """ Show price history """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() m = Market(steem_instance=stm) utc = pytz.timezone('UTC') stop = utc.localize(datetime.utcnow()) @@ -1226,7 +1311,11 @@ def tradehistory(days, hours, limit, width, height): base += float(order.as_base("SBD")["base"]) quote += float(order.as_base("SBD")["quote"]) price.append(base / quote) - chart = AsciiChart(height=height, width=width, offset=3, placeholder='{:6.2f} ') + if ascii: + charset = u'ascii' + else: + charset = u'utf8' + chart = AsciiChart(height=height, width=width, offset=3, placeholder='{:6.2f} ', charset=charset) print("\n Trade history %s - %s \n\nSTEEM/SBD" % (formatTimeString(start), formatTimeString(stop))) chart.adapt_on_series(price) chart.new_chart() @@ -1237,13 +1326,16 @@ def tradehistory(days, hours, limit, width, height): @cli.command() @click.option('--chart', help='Enable charting', is_flag=True) -@click.option('--limit', help='Limit number of returned open orders (default 25)', default=25) +@click.option('--limit', '-l', help='Limit number of returned open orders (default 25)', default=25) @click.option('--show-date', help='Show dates', is_flag=True, default=False) -@click.option('--width', help='Plot width (default 75)', default=75) -@click.option('--height', help='Plot height (default 15)', default=15) -def orderbook(chart, limit, show_date, width, height): +@click.option('--width', '-w', help='Plot width (default 75)', default=75) +@click.option('--height', '-h', help='Plot height (default 15)', default=15) +@click.option('--ascii', help='Use only ascii symbols', is_flag=True, default=False) +def orderbook(chart, limit, show_date, width, height, ascii): """Obtain orderbook of the internal market""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() market = Market(steem_instance=stm) orderbook = market.orderbook(limit=limit, raw_data=False) if not show_date: @@ -1283,7 +1375,11 @@ def orderbook(chart, limit, show_date, width, height): if n < len(bids_date): n = len(bids_date) if chart: - chart = AsciiChart(height=height, width=width, offset=4, placeholder=' {:10.2f} $') + if ascii: + charset = u'ascii' + else: + charset = u'utf8' + chart = AsciiChart(height=height, width=width, offset=4, placeholder=' {:10.2f} $', charset=charset) print("\n Orderbook \n") chart.adapt_on_series(sumsum_asks[::-1] + sumsum_bids) chart.new_chart() @@ -1336,6 +1432,8 @@ def buy(amount, asset, price, account, orderid): Limit buy price denoted in (SBD per STEEM) """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if asset == "SBD": @@ -1377,6 +1475,8 @@ def sell(amount, asset, price, account, orderid): Limit sell price denoted in (SBD per STEEM) """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if asset == "SBD": market = Market(base=Asset("STEEM"), quote=Asset("SBD"), steem_instance=stm) else: @@ -1411,6 +1511,8 @@ def sell(amount, asset, price, account, orderid): def cancel(orderid, account): """Cancel order in the internal market""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() market = Market(steem_instance=stm) if not account: account = stm.config["default_account"] @@ -1427,6 +1529,8 @@ def cancel(orderid, account): def openorders(account): """Show open orders""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() market = Market(steem_instance=stm) if not account: account = stm.config["default_account"] @@ -1448,6 +1552,8 @@ def openorders(account): def resteem(identifier, account): """Resteem an existing post""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -1466,6 +1572,8 @@ def resteem(identifier, account): def follow(follow, account, what): """Follow another account""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if isinstance(what, str): @@ -1485,6 +1593,8 @@ def follow(follow, account, what): def mute(mute, account, what): """Mute another account""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if isinstance(what, str): @@ -1503,6 +1613,8 @@ def mute(mute, account, what): def unfollow(unfollow, account): """Unfollow/Unmute another account""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] if not unlock_wallet(stm): @@ -1523,6 +1635,8 @@ def unfollow(unfollow, account): def witnessupdate(witness, maximum_block_size, account_creation_fee, sbd_interest_rate, url, signing_key): """Change witness properties""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not witness: witness = stm.config["default_account"] if not unlock_wallet(stm): @@ -1551,6 +1665,8 @@ def witnessupdate(witness, maximum_block_size, account_creation_fee, sbd_interes def witnesscreate(witness, signing_key, maximum_block_size, account_creation_fee, sbd_interest_rate, url): """Create a witness""" stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not unlock_wallet(stm): return props = { @@ -1574,6 +1690,8 @@ def witnesses(account, limit): """ List witnesses """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if account: witnesses = WitnessesVotedByAccount(account, steem_instance=stm) else: @@ -1589,6 +1707,8 @@ def votes(account, direction, days): """ List outgoing/incoming account votes """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] utc = pytz.timezone('UTC') @@ -1605,6 +1725,151 @@ def votes(account, direction, days): votes.printAsTable(votee=account["name"]) +@cli.command() +@click.argument('authorperm', nargs=1, required=True) +@click.option('--payout', '-p', default=None, help="Show the curation for a potential payout in SBD as float") +def curation(authorperm, payout): + """ Lists curation rewords of all votes for authorperm + """ + stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() + comment = Comment(authorperm, steem_instance=stm) + if payout is not None and comment.is_pending(): + payout = float(payout) + elif payout is not None: + payout = None + t = PrettyTable(["Voter", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + curation_rewards_SBD = comment.get_curation_rewards(pending_payout_SBD=True, pending_payout_value=payout) + curation_rewards_SP = comment.get_curation_rewards(pending_payout_SBD=False, pending_payout_value=payout) + rows = [] + for vote in comment["active_votes"]: + vote_SBD = stm.rshares_to_sbd(int(vote["rshares"])) + curation_SBD = curation_rewards_SBD["active_votes"][vote["voter"]] + curation_SP = curation_rewards_SP["active_votes"][vote["voter"]] + if vote_SBD > 0: + penalty = ((100 - comment.get_curation_penalty(vote_time=vote["time"])) / 100 * vote_SBD) + performance = (float(curation_SBD) / vote_SBD * 100) + else: + performance = 0 + penalty = 0 + rows.append([vote["voter"], + ((formatTimeString(vote["time"]) - comment["created"]).total_seconds() / 60), + vote_SBD, + penalty, + float(curation_SP), + performance]) + sortedList = sorted(rows, key=lambda row: (row[1]), reverse=False) + for row in sortedList: + t.add_row([row[0], + "%.1f min" % row[1], + "%.3f SBD" % float(row[2]), + "%.3f SBD" % float(row[3]), + "%.3f SP" % (row[4]), + "%.1f %%" % (row[5])]) + print(t) + + +@cli.command() +@click.argument('account', nargs=1, required=False) +@click.option('--post', '-p', help='Show pending post payout', is_flag=True, default=False) +@click.option('--comment', '-c', help='Show pending comments payout', is_flag=True, default=False) +@click.option('--curation', '-v', help='Shows pending curation', is_flag=True, default=False) +def rewards(account, post, comment, curation): + """ Lists outstanding rewards + """ + stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not comment and not curation: + post = True + + utc = pytz.timezone('UTC') + limit_time = utc.localize(datetime.utcnow()) - timedelta(days=7) + sum_reward = [0, 0, 0, 0] + account = Account(account, steem_instance=stm) + median_price = Price(stm.get_current_median_history(), steem_instance=stm) + m = Market(steem_instance=stm) + latest = m.ticker()["latest"] + t = PrettyTable(["Description", "Cashout", "SBD", "SP", "Liquid USD", "Invested USD"]) + t.align = "l" + rows = [] + c_list = {} + start_op = account.estimate_virtual_op_num(limit_time) + length = (account.virtual_op_count() - start_op) / 1000 + with click.progressbar(map(Comment, account.history(start=start_op, use_block_num=False, only_ops=["comment"])), length=length) as comment_hist: + for v in comment_hist: + v.refresh() + author_reward = v.get_author_rewards() + if float(author_reward["total_payout_SBD"]) < 0.001: + continue + if v.permlink in c_list: + continue + if not v.is_pending(): + continue + if not post and not v.is_comment(): + continue + if not comment and v.is_comment(): + continue + c_list[v.permlink] = 1 + payout_SBD = author_reward["payout_SBD"] + sum_reward[0] += float(payout_SBD) + payout_SP = author_reward["payout_SP"] + sum_reward[1] += float(payout_SP) + liquid_USD = float(author_reward["payout_SBD"]) / float(latest) * float(median_price) + sum_reward[2] += liquid_USD + invested_USD = float(author_reward["payout_SP"]) * float(median_price) + sum_reward[3] += invested_USD + if v.is_comment(): + title = v.parent_permlink + else: + title = v.permlink + rows.append([title, + ((v["created"] - limit_time).total_seconds() / 60 / 60 / 24), + (payout_SBD), + (payout_SP), + (liquid_USD), + (invested_USD)]) + if curation: + votes = AccountVotes(account, start=limit_time, steem_instance=stm) + for vote in votes: + c = Comment(vote["authorperm"]) + rewards = c.get_curation_rewards() + if not rewards["pending_rewards"]: + continue + payout_SP = rewards["active_votes"][account["name"]] + liquid_USD = 0 + invested_USD = float(payout_SP) * float(median_price) + sum_reward[1] += float(payout_SP) + sum_reward[3] += invested_USD + rows.append([c.permlink, + ((c["created"] - limit_time).total_seconds() / 60 / 60 / 24), + 0.000, + payout_SP, + (liquid_USD), + (invested_USD)]) + sortedList = sorted(rows, key=lambda row: (row[1]), reverse=True) + for row in sortedList: + t.add_row([row[0][:30], + "%.1f days" % row[1], + "%.3f" % float(row[2]), + "%.3f" % float(row[3]), + "%.2f $" % (row[4]), + "%.2f $" % (row[5])]) + + t.add_row(["", "", "", "", "", ""]) + t.add_row(["Sum", + "-", + "%.2f SBD" % (sum_reward[0]), + "%.2f SP" % (sum_reward[1]), + "%.2f $" % (sum_reward[2]), + "%.2f $" % (sum_reward[3])]) + print(t) + + @cli.command() @click.argument('account', nargs=1, required=False) @click.option('--reward_steem', help='Amount of STEEM you would like to claim', default="0 STEEM") @@ -1616,6 +1881,8 @@ def claimreward(account, reward_steem, reward_sbd, reward_vests): By default, this will claim ``all`` outstanding balances. """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not account: account = stm.config["default_account"] acc = Account(account, steem_instance=stm) @@ -1631,6 +1898,75 @@ def claimreward(account, reward_steem, reward_sbd, reward_vests): print(tx) +@cli.command() +@click.argument('blocknumber', nargs=1, required=False) +@click.option('--trx', '-t', help='Show only one transaction number', default=None) +@click.option('--use-api', '-u', help='Uses the get_potential_signatures api call', is_flag=True, default=False) +def verify(blocknumber, trx, use_api): + """Returns the public signing keys for a block""" + stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() + b = Blockchain(steem_instance=stm) + i = 0 + if not blocknumber: + blocknumber = b.get_current_block_num() + try: + int(blocknumber) + block = Block(blocknumber, steem_instance=stm) + if trx is not None: + i = int(trx) + trxs = [block.transactions[int(trx)]] + else: + trxs = block.transactions + except Exception: + trxs = [b.get_transaction(blocknumber)] + blocknumber = trxs[0]["block_num"] + wallet = Wallet(steem_instance=stm) + t = PrettyTable(["trx", "Signer key", "Account"]) + t.align = "l" + if not use_api: + from beembase.signedtransactions import Signed_Transaction + for trx in trxs: + if not use_api: + # trx is now identical to the output of get_transaction + # This is just for testing porpuse + if True: + signed_tx = Signed_Transaction(trx.copy()) + else: + tx = b.get_transaction(trx["transaction_id"]) + signed_tx = Signed_Transaction(tx) + public_keys = [] + for key in signed_tx.verify(chain=stm.chain_params, recover_parameter=True): + public_keys.append(format(Base58(key, prefix=stm.prefix), stm.prefix)) + else: + tx = TransactionBuilder(tx=trx, steem_instance=stm) + public_keys = tx.get_potential_signatures() + accounts = [] + empty_public_keys = [] + for key in public_keys: + account = wallet.getAccountFromPublicKey(key) + if account is None: + empty_public_keys.append(key) + else: + accounts.append(account) + new_public_keys = [] + for key in public_keys: + if key not in empty_public_keys or use_api: + new_public_keys.append(key) + if isinstance(new_public_keys, list) and len(new_public_keys) == 1: + new_public_keys = new_public_keys[0] + else: + new_public_keys = json.dumps(new_public_keys, indent=4) + if isinstance(accounts, list) and len(accounts) == 1: + accounts = accounts[0] + else: + accounts = json.dumps(accounts, indent=4) + t.add_row(["%d" % i, new_public_keys, accounts]) + i += 1 + print(t) + + @cli.command() @click.argument('objects', nargs=-1) def info(objects): @@ -1640,6 +1976,8 @@ def info(objects): a post/comment and a public key """ stm = shared_steem_instance() + if stm.rpc: + stm.rpc.rpcconnect() if not objects: t = PrettyTable(["Key", "Value"]) t.align = "l" @@ -1647,8 +1985,8 @@ def info(objects): median_price = stm.get_current_median_history() steem_per_mvest = stm.get_steem_per_mvest() chain_props = stm.get_chain_properties() - price = (Amount(median_price["base"]).amount / Amount( - median_price["quote"]).amount) + price = (Amount(median_price["base"], steem_instance=stm).amount / Amount( + median_price["quote"], steem_instance=stm).amount) for key in info: t.add_row([key, info[key]]) t.add_row(["steem per mvest", steem_per_mvest]) @@ -1662,11 +2000,11 @@ def info(objects): if re.match("^[0-9-]*:[0-9-]", obj): obj, tran_nr = obj.split(":") if int(obj) < 1: - b = Blockchain() + b = Blockchain(steem_instance=stm) block_number = b.get_current_block_num() + int(obj) - 1 else: block_number = obj - block = Block(block_number) + block = Block(block_number, steem_instance=stm) if block: t = PrettyTable(["Key", "Value"]) t.align = "l" @@ -1680,7 +2018,7 @@ def info(objects): else: tran_nr = int(tran_nr) if len(value) > tran_nr - 1 and tran_nr > -1: - t_value = json.dumps(value[tran_nr - 1], indent=4) + t_value = json.dumps(value[tran_nr], indent=4) t.add_row(["transaction %d/%d" % (tran_nr, len(value)), t_value]) elif key == "transaction_ids" and not bool(tran_nr): t.add_row(["Nr. of transaction_ids", len(value)]) @@ -1690,7 +2028,7 @@ def info(objects): else: tran_nr = int(tran_nr) if len(value) > tran_nr - 1 and tran_nr > -1: - t.add_row(["transaction_id %d/%d" % (int(tran_nr), len(value)), value[tran_nr - 1]]) + t.add_row(["transaction_id %d/%d" % (int(tran_nr), len(value)), value[tran_nr]]) else: t.add_row([key, value]) print(t) @@ -1718,7 +2056,7 @@ def info(objects): # witness available? try: - witness = Witness(obj) + witness = Witness(obj, steem_instance=stm) witness_json = witness.json() t = PrettyTable(["Key", "Value"]) t.align = "l" @@ -1742,13 +2080,16 @@ def info(objects): print("Public Key not known" % obj) # Post identifier elif re.match(".*@.{3,16}/.*$", obj): - post = Comment(obj) + post = Comment(obj, steem_instance=stm) post_json = post.json() if post_json: t = PrettyTable(["Key", "Value"]) t.align = "l" for key in sorted(post_json): - value = post_json[key] + if key in ["body", "active_votes"]: + value = "not shown" + else: + value = post_json[key] if (key in ["json_metadata"]): value = json.loads(value) value = json.dumps(value, indent=4) @@ -1763,4 +2104,7 @@ def info(objects): if __name__ == "__main__": - cli() + if getattr(sys, 'frozen', False): + cli(sys.argv[1:]) + else: + cli() diff --git a/beem/comment.py b/beem/comment.py index fd8e0b3f..6c6352bb 100644 --- a/beem/comment.py +++ b/beem/comment.py @@ -4,18 +4,21 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals from builtins import str +import json +import re +import logging +import pytz +import math +from datetime import datetime from .instance import shared_steem_instance from .account import Account from .amount import Amount +from .price import Price from .utils import resolve_authorperm, construct_authorperm, derive_permlink, remove_from_dict, make_patch, formatTimeString from .blockchainobject import BlockchainObject from .exceptions import ContentDoesNotExistsException, VotingInvalidOnArchivedPost from beembase import operations from beemgraphenebase.py23 import py23_bytes, bytes_types, integer_types, string_types, text_type -import json -import re -import logging -from datetime import datetime log = logging.getLogger(__name__) @@ -222,7 +225,233 @@ class Comment(BlockchainObject): """ return self['depth'] > 0 + @property + def reward(self): + """ Return the estimated total SBD reward. + """ + a_zero = Amount("0 SBD", steem_instance=self.steem) + return self.get("total_payout_value", a_zero) + self.get("pending_payout_value", a_zero) + + def is_pending(self): + """ Return if the payout is pending (the post/comment + is younger than 7 days) + """ + return not float(self["total_payout_value"]) >= Amount("0.001 SBD", steem_instance=self.steem) + + def time_elapsed(self): + """Return a timedelta on how old the post is. + """ + utc = pytz.timezone('UTC') + return utc.localize(datetime.utcnow()) - self['created'] + + def curation_penalty_compensation_SBD(self): + """ Returns The required post payout amount after 30 minutes + which will compentsate the curation penalty, if voting earlier than 30 minutes + """ + self.refresh() + return self.reward * 900. / ((self.time_elapsed()).total_seconds() / 60) ** 2 + + def estimate_curation_SBD(self, vote_value_SBD, estimated_value_SBD=None): + """ Estimates curation reward + + :param float vote_value_SBD: The vote value in SBD for which the curation + should be calculated + :param float estimated_value_SBD: When set, this value is used for calculate + the curation. When not set, the current post value is used. + """ + self.refresh() + if estimated_value_SBD is None: + estimated_value_SBD = float(self.reward) + t = self.get_curation_penalty() / 100. + k = vote_value_SBD / (vote_value_SBD + float(self.reward)) + K = (1 - math.sqrt(1 - k)) / 4 / k + return K * vote_value_SBD * t * math.sqrt(estimated_value_SBD) + + def get_curation_penalty(self, vote_time=None): + """ If post is less than 30 minutes old, it will incur a curation + reward penalty. + + :param datetime vote_time: A vote time can be given and the curation + penalty is calculated regarding the given time (default is None) + When set to None, the current date is used. + + """ + if vote_time is None: + elapsed_seconds = self.time_elapsed().total_seconds() + elif isinstance(vote_time, str): + elapsed_seconds = (formatTimeString(vote_time) - self["created"]).total_seconds() + elif isinstance(vote_time, datetime): + elapsed_seconds = (vote_time - self["created"]).total_seconds() + else: + raise ValueError("vote_time must be a string or a datetime") + reward = (elapsed_seconds / 1800) * 100 + if reward > 100: + reward = 100 + return reward + + def get_vote_with_curation(self, voter=None, raw_data=False, pending_payout_value=None): + """ Returns vote for voter. Returns None, if the voter cannot be found in `active_votes`. + + :param str voter: Voter for which the vote should be returned + :param bool raw_data: If True, the raw data are returned + :param float/str pending_payout_SBD: When not None this value instead of the current + value is used for calculating the rewards + """ + specific_vote = None + if voter is None: + voter = Account(self["author"], steem_instance=self.steem) + else: + voter = Account(voter, steem_instance=self.steem) + for vote in self["active_votes"]: + if voter["name"] == vote["voter"]: + specific_vote = vote + if specific_vote is not None and raw_data: + return specific_vote + elif specific_vote is not None: + curation_reward = self.get_curation_rewards(pending_payout_SBD=True, pending_payout_value=pending_payout_value) + specific_vote["curation_reward"] = curation_reward["active_votes"][voter["name"]] + specific_vote["ROI"] = float(curation_reward["active_votes"][voter["name"]]) / float(voter.get_voting_value_SBD(voting_weight=specific_vote["percent"] / 100)) * 100 + return specific_vote + else: + return None + + def get_beneficiaries_pct(self): + """ Returns the sum of all post beneficiaries in percentage + """ + beneficiaries = self["beneficiaries"] + weight = 0 + for b in beneficiaries: + weight += b["weight"] + return weight / 100. + + def get_rewards(self): + """ Returns the total_payout, author_payout and the curator payout in SBD. + When the payout is still pending, the estimated payout is given out. + + Example::: + + { + 'total_payout': 9.956 SBD, + 'author_payout': 7.166 SBD, + 'curator_payout': 2.790 SBD + } + + """ + if self.is_pending(): + total_payout = self["pending_payout_value"] + author_payout = self.get_author_rewards()["author_payout"] + curator_payout = total_payout - author_payout + else: + total_payout = self["total_payout_value"] + curator_payout = self["curator_payout_value"] + author_payout = total_payout - curator_payout + return {"total_payout": total_payout, "author_payout": author_payout, "curator_payout": curator_payout} + + def get_author_rewards(self): + """ Returns the author rewards. + + Example::: + + { + 'pending_rewards': True, + 'payout_SP': 0.912 STEEM, + 'payout_SBD': 3.583 SBD, + 'total_payout_SBD': 7.166 SBD + } + + """ + if not self.is_pending(): + total_payout = self["total_payout_value"] + curator_payout = self["curator_payout_value"] + author_payout = total_payout - curator_payout + return {'pending_rewards': False, "payout_SP": Amount("0 SBD", steem_instance=self.steem), "payout_SBD": Amount("0 SBD", steem_instance=self.steem), "total_payout_SBD": author_payout} + + median_price = Price(self.steem.get_current_median_history(), steem_instance=self.steem) + beneficiaries_pct = self.get_beneficiaries_pct() + curation_tokens = self.reward * 0.25 + author_tokens = self.reward - curation_tokens + curation_rewards = self.get_curation_rewards() + author_tokens += median_price * curation_rewards['unclaimed_rewards'] + + benefactor_tokens = author_tokens * beneficiaries_pct / 100. + author_tokens -= benefactor_tokens + + sbd_steem = author_tokens * self["percent_steem_dollars"] / 20000. + vesting_steem = median_price.as_base("STEEM") * (author_tokens - sbd_steem) + + return {'pending_rewards': True, "payout_SP": vesting_steem, "payout_SBD": sbd_steem, "total_payout_SBD": author_tokens} + + def get_curation_rewards(self, pending_payout_SBD=False, pending_payout_value=None): + """ Returns the curation rewards. + + :param bool pending_payout_SBD: If True, the rewards are returned in SBD and not in STEEM (default is False) + :param float/str pending_payout_value: When not None this value instead of the current + value is used for calculating the rewards + + `pending_rewards` is True when + the post is younger than 7 days. `unclaimed_rewards` is the + amount of curation_rewards that goes to the author (self-vote or votes within + the first 30 minutes). `active_votes` contains all voter with their curation reward. + + Example::: + + { + 'pending_rewards': True, 'unclaimed_rewards': 0.245 STEEM, + 'active_votes': { + 'leprechaun': 0.006 STEEM, 'timcliff': 0.186 STEEM, + 'st3llar': 0.000 STEEM, 'crokkon': 0.015 STEEM, 'feedyourminnows': 0.003 STEEM, + 'isnochys': 0.003 STEEM, 'loshcat': 0.001 STEEM, 'greenorange': 0.000 STEEM, + 'qustodian': 0.123 STEEM, 'jpphotography': 0.002 STEEM, 'thinkingmind': 0.001 STEEM, + 'oups': 0.006 STEEM, 'mattockfs': 0.001 STEEM, 'holger80': 0.003 STEEM, 'michaelizer': 0.004 STEEM, + 'flugschwein': 0.010 STEEM, 'ulisessabeque': 0.000 STEEM, 'hakancelik': 0.002 STEEM, 'sbi2': 0.008 STEEM, + 'zcool': 0.000 STEEM, 'steemhq': 0.002 STEEM, 'rowdiya': 0.000 STEEM, 'qurator-tier-1-2': 0.012 STEEM + } + } + + """ + median_price = Price(self.steem.get_current_median_history(), steem_instance=self.steem) + pending_rewards = False + total_vote_weight = self["total_vote_weight"] + if not self["allow_curation_rewards"]: + max_rewards = Amount("0 STEEM", steem_instance=self.steem) + unclaimed_rewards = max_rewards.copy() + elif not self.is_pending(): + max_rewards = self["curator_payout_value"] + unclaimed_rewards = self["total_payout_value"] * 0.25 - max_rewards + total_vote_weight = 0 + for vote in self["active_votes"]: + total_vote_weight += vote["weight"] + else: + if pending_payout_value is None: + pending_payout_value = self["pending_payout_value"] + elif isinstance(pending_payout_value, (float, int)): + pending_payout_value = Amount(pending_payout_value, "SBD", steem_instance=self.steem) + elif isinstance(pending_payout_value, str): + pending_payout_value = Amount(pending_payout_value, steem_instance=self.steem) + if pending_payout_SBD: + max_rewards = (pending_payout_value * 0.25) + else: + max_rewards = median_price.as_base("STEEM") * (pending_payout_value * 0.25) + unclaimed_rewards = max_rewards.copy() + pending_rewards = True + + active_votes = {} + for vote in self["active_votes"]: + if total_vote_weight > 0: + claim = max_rewards * vote["weight"] / total_vote_weight + else: + claim = 0 + if claim > 0 and pending_rewards: + unclaimed_rewards -= claim + if claim > 0: + active_votes[vote["voter"]] = claim + else: + active_votes[vote["voter"]] = 0 + + return {'pending_rewards': pending_rewards, 'unclaimed_rewards': unclaimed_rewards, "active_votes": active_votes} + def get_reblogged_by(self, identifier=None): + """Shows in which blogs this post appears""" if not identifier: post_author = self["author"] post_permlink = self["permlink"] @@ -234,6 +463,7 @@ class Comment(BlockchainObject): return self.steem.rpc.get_reblogged_by(post_author, post_permlink, api="follow") def get_votes(self): + """Returns all votes as ActiveVotes object""" from .vote import ActiveVotes return ActiveVotes(self, steem_instance=self.steem) diff --git a/beemgraphenebase/signedtransactions.py b/beemgraphenebase/signedtransactions.py index 74ffac61..9ca0156e 100644 --- a/beemgraphenebase/signedtransactions.py +++ b/beemgraphenebase/signedtransactions.py @@ -170,7 +170,7 @@ class Signed_Transaction(GrapheneObject): phex = hexlify(p).decode('ascii') pubKeysFound.append(phex) except Exception: - continue + p = None else: phex = hexlify(p).decode('ascii') pubKeysFound.append(phex) diff --git a/examples/memory_profiler1.py b/examples/memory_profiler1.py index 50cc9086..fdda2b0c 100644 --- a/examples/memory_profiler1.py +++ b/examples/memory_profiler1.py @@ -37,7 +37,7 @@ def profiling(name_list): startBlockNumber = current_num - 20 endBlockNumber = current_num block_elem = None - for o in blockchain_object.ops(start=startBlockNumber, stop=endBlockNumber): + for o in blockchain_object.stream(start=startBlockNumber, stop=endBlockNumber): print("block %d" % (o["block_num"])) block_elem = o print(block_elem) diff --git a/tests/beem/test_block.py b/tests/beem/test_block.py index 8f441456..993b920b 100644 --- a/tests/beem/test_block.py +++ b/tests/beem/test_block.py @@ -52,7 +52,31 @@ class Testcases(unittest.TestCase): self.assertTrue(isinstance(block.time(), datetime)) self.assertTrue(isinstance(block, dict)) - self.assertTrue(len(block.ops())) + self.assertTrue(len(block.operations)) + self.assertTrue(isinstance(block.ops_statistics(), dict)) + + block2 = Block(self.test_block_id + 1, steem_instance=bts) + self.assertTrue(block2.time() > block.time()) + with self.assertRaises( + exceptions.BlockDoesNotExistsException + ): + Block(0, steem_instance=bts) + + @parameterized.expand([ + ("non_appbase"), + ("appbase"), + ]) + def test_block_only_ops(self, node_param): + if node_param == "non_appbase": + bts = self.bts + else: + bts = self.appbase + block = Block(self.test_block_id, only_ops=True, steem_instance=bts) + self.assertEqual(block.identifier, self.test_block_id) + self.assertTrue(isinstance(block.time(), datetime)) + self.assertTrue(isinstance(block, dict)) + + self.assertTrue(len(block.operations)) self.assertTrue(isinstance(block.ops_statistics(), dict)) block2 = Block(self.test_block_id + 1, steem_instance=bts) diff --git a/tests/beem/test_blockchain.py b/tests/beem/test_blockchain.py index 12288866..8bb1b2c8 100644 --- a/tests/beem/test_blockchain.py +++ b/tests/beem/test_blockchain.py @@ -150,20 +150,6 @@ class Testcases(unittest.TestCase): self.assertEqual(op_stat["transfer"], op_stat2["transfer"]) self.assertEqual(op_stat["vote"], op_stat2["vote"]) - ops_ops = [] - for op in b.ops(start=self.start, stop=self.stop): - ops_ops.append(op) - self.assertTrue(len(ops_ops) > 0) - op_stat3 = {"transfer": 0, "vote": 0} - - for op in ops_ops: - if op["op"][0] in opNames: - op_stat3[op["op"][0]] += 1 - self.assertTrue(op["block_num"] >= self.start) - self.assertTrue(op["block_num"] <= self.stop) - self.assertEqual(op_stat["transfer"], op_stat3["transfer"]) - self.assertEqual(op_stat["vote"], op_stat3["vote"]) - ops_blocks = [] for op in b.blocks(start=self.start, stop=self.stop): ops_blocks.append(op) @@ -184,3 +170,22 @@ class Testcases(unittest.TestCase): ops_blocks.append(op) break self.assertTrue(len(ops_blocks) == 1) + + @parameterized.expand([ + ("non_appbase"), + ("appbase"), + ]) + def test_wait_for_and_get_block(self, node_param): + if node_param == "non_appbase": + bts = self.bts + else: + bts = self.appbase + b = Blockchain(steem_instance=bts) + start_num = b.get_current_block_num() + blocknum = start_num + last_fetched_block_num = None + for i in range(3): + block = b.wait_for_and_get_block(blocknum) + last_fetched_block_num = block.block_num + blocknum = last_fetched_block_num + 2 + self.assertEqual(last_fetched_block_num, start_num + 4) diff --git a/tests/beem/test_blockchain_batch.py b/tests/beem/test_blockchain_batch.py index 92c6e4d7..7746c04e 100644 --- a/tests/beem/test_blockchain_batch.py +++ b/tests/beem/test_blockchain_batch.py @@ -43,10 +43,10 @@ class Testcases(unittest.TestCase): b = Blockchain(steem_instance=bts) ops_stream = [] opNames = ["transfer", "vote"] - for op in b.stream(opNames=opNames, start=self.start, stop=self.stop): + for op in b.stream(opNames=opNames, start=self.start, stop=self.stop, max_batch_size=self.max_batch_size, threading=False): ops_stream.append(op) - self.assertTrue(len(ops_stream) > 0) op_stat = b.ops_statistics(start=self.start, stop=self.stop) + self.assertEqual(op_stat["vote"] + op_stat["transfer"], len(ops_stream)) ops_blocks = [] for op in b.blocks(start=self.start, stop=self.stop, max_batch_size=self.max_batch_size, threading=False): ops_blocks.append(op) diff --git a/tests/beem/test_blockchain_threading.py b/tests/beem/test_blockchain_threading.py index 846125a8..16a4ea78 100644 --- a/tests/beem/test_blockchain_threading.py +++ b/tests/beem/test_blockchain_threading.py @@ -44,10 +44,11 @@ class Testcases(unittest.TestCase): b = Blockchain(steem_instance=bts) ops_stream = [] opNames = ["transfer", "vote"] - for op in b.stream(opNames=opNames, start=self.start, stop=self.stop): + for op in b.stream(opNames=opNames, start=self.start, stop=self.stop, threading=True, thread_num=8): ops_stream.append(op) self.assertTrue(len(ops_stream) > 0) op_stat = b.ops_statistics(start=self.start, stop=self.stop) + self.assertEqual(op_stat["vote"] + op_stat["transfer"], len(ops_stream)) ops_blocks = [] for op in b.blocks(start=self.start, stop=self.stop, threading=True, thread_num=8): ops_blocks.append(op) diff --git a/tests/beem/test_cli.py b/tests/beem/test_cli.py index d3829d8e..c63ee46c 100644 --- a/tests/beem/test_cli.py +++ b/tests/beem/test_cli.py @@ -404,6 +404,27 @@ class Testcases(unittest.TestCase): runner.invoke(cli, ['-o', 'set', 'nodes', 'wss://testnet.steem.vc']) self.assertEqual(result.exit_code, 0) + def test_rewards(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', '']) + result = runner.invoke(cli, ['rewards', '--post', '--comment', '--curation', 'test']) + runner.invoke(cli, ['-o', 'set', 'nodes', 'wss://testnet.steem.vc']) + self.assertEqual(result.exit_code, 0) + + def test_curation(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', '']) + result = runner.invoke(cli, ['curation', "@gtg/witness-gtg-log"]) + runner.invoke(cli, ['-o', 'set', 'nodes', 'wss://testnet.steem.vc']) + self.assertEqual(result.exit_code, 0) + + def test_verify(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', '']) + result = runner.invoke(cli, ['verify', '--trx', '0']) + runner.invoke(cli, ['-o', 'set', 'nodes', 'wss://testnet.steem.vc']) + self.assertEqual(result.exit_code, 0) + def test_tradehistory(self): runner = CliRunner() runner.invoke(cli, ['-o', 'set', 'nodes', '']) -- GitLab