From cf1091f3637ba01ede2263d1533a47184e5375cd Mon Sep 17 00:00:00 2001
From: Holger Nahrstaedt <holger@nahrstaedt.de>
Date: Mon, 12 Mar 2018 17:35:52 +0100
Subject: [PATCH] Next release 0.19.16

Account
* get_withdraw_routes added
* get_account_votes fixed for appbase
* get_vote, has_voted added
* virtual_op_count, get_curation_reward curation_stats added
* get_account_history and history_reverse added
* history improved
Blockchain
* hash_op added
CLI
* default vote weight added
* upvote added
* Info improved
Steem
* get_block_interval added
Transactionbuilder
* txbuffer.clear() added on exception
Vote
* AccountVotes fixed
GrapheneRPC
* Websocket disconnect exception handling improved
Compare_With_steem_python_account.py added
Unittests for account improved
---
 appveyor.yml                                  |   8 -
 beem/account.py                               | 369 ++++++++++++++----
 beem/blockchain.py                            |  23 +-
 beem/cli.py                                   |  56 ++-
 beem/steem.py                                 |  11 +
 beem/transactionbuilder.py                    |   1 +
 beem/version.py                               |   2 +-
 beem/vote.py                                  |   5 +-
 beemapi/version.py                            |   2 +-
 beembase/version.py                           |   2 +-
 beemgrapheneapi/graphenerpc.py                |  50 +--
 beemgrapheneapi/version.py                    |   2 +-
 beemgraphenebase/version.py                   |   2 +-
 examples/compare_with_steem_python_account.py |  95 +++++
 examples/op_on_testnet.py                     |   2 +
 examples/print_appbase_calls.py               |   1 +
 setup.py                                      |   2 +-
 tests/test_account.py                         |  14 +-
 18 files changed, 503 insertions(+), 144 deletions(-)
 create mode 100644 examples/compare_with_steem_python_account.py

diff --git a/appveyor.yml b/appveyor.yml
index 4e884b56..8906fd16 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -16,14 +16,6 @@ environment:
       PYTHON_ARCH: "64"
       MINICONDA: C:\Miniconda-x64
 
-    - PYTHON: "C:\\Python35"
-      PYTHON_ARCH: "32"
-      MINICONDA: C:\Miniconda35
-
-    - PYTHON: "C:\\Python35-x64"
-      PYTHON_ARCH: "64"
-      MINICONDA: C:\Miniconda35-x64
-
     - PYTHON: "C:\\Python36"
       PYTHON_ARCH: "32"
       MINICONDA: C:\Miniconda36
diff --git a/beem/account.py b/beem/account.py
index 7340a9c6..f3ba0479 100644
--- a/beem/account.py
+++ b/beem/account.py
@@ -4,20 +4,21 @@ from __future__ import division
 from __future__ import print_function
 from __future__ import unicode_literals
 from builtins import bytes, int, str
+import pytz
+import json
+from datetime import datetime, timedelta
+import math
+import random
+import logging
 from beem.instance import shared_steem_instance
 from .exceptions import AccountDoesNotExistsException
 from .blockchainobject import BlockchainObject
-from .utils import formatTimeString, formatTimedelta
+from .blockchain import Blockchain
+from .utils import formatTimeString, formatTimedelta, remove_from_dict
 from beem.amount import Amount
-from datetime import datetime, timedelta
-import pytz
 from beembase import operations
 from beemgraphenebase.account import PrivateKey, PublicKey
 from beemgraphenebase.py23 import bytes_types, integer_types, string_types, text_type
-import json
-import math
-import random
-import logging
 log = logging.getLogger(__name__)
 
 
@@ -360,10 +361,11 @@ class Account(BlockchainObject):
         """ Help function, used in get_followers and get_following
         """
         if self.steem.rpc.get_use_appbase():
+            query = {'account': self.name, 'start': last_user, 'type': "blog", 'limit': 100}
             if direction == "follower":
-                followers = self.steem.rpc.get_followers({'account': self.name, 'start': last_user, 'type': "blog", 'limit': 100}, api='follow')['followers']
+                followers = self.steem.rpc.get_followers(query, api='follow')['followers']
             elif direction == "following":
-                followers = self.steem.rpc.get_following({'account': self.name, 'start': last_user, 'type': "blog", 'limit': 100}, api='follow')['following']
+                followers = self.steem.rpc.get_following(query, api='follow')['following']
         else:
             self.steem.register_apis(["follow"])
             if direction == "follower":
@@ -406,6 +408,9 @@ class Account(BlockchainObject):
 
     @property
     def balances(self):
+        return self.get_balances()
+
+    def get_balances(self):
 
         return {
             'available': self.available_balances,
@@ -522,6 +527,15 @@ class Account(BlockchainObject):
         else:
             return self.steem.rpc.get_conversion_requests(account)
 
+    def get_withdraw_routes(self, account=None):
+        """Returns withdraw_routes """
+        if account is None:
+            account = self["name"]
+        if self.steem.rpc.get_use_appbase():
+            return self.steem.rpc.find_withdraw_vesting_routes({'account': account, 'order': 'by_withdraw_route'}, api="database")['routes']
+        else:
+            return self.steem.rpc.get_withdraw_routes(account, 'all')
+
     def get_recovery_request(self, account=None):
         """ get_recovery_request """
         if account is None:
@@ -541,74 +555,287 @@ class Account(BlockchainObject):
             return self.steem.rpc.verify_account_authority(account, keys)
 
     def get_account_votes(self, account=None):
+        """Returns all votes that the account has done"""
         if account is None:
-            account = self["name"]
+            account = self
+        else:
+            account = Account(account, steem_instance=self.steem)
+        if self.steem.rpc.get_use_appbase():
+            vote_hist = account.history(only_ops=["vote"], batch_size=1000)
+            votes = []
+            for vote in vote_hist:
+                votes.append(vote[1]["op"][1])
+            return votes
+        else:
+            return self.steem.rpc.get_account_votes(account["name"])
+
+    def get_vote(self, comment):
+        """Returns a vote if the account has already voted for comment.
+
+            :param str/Comment comment: can be a Comment object or a authorpermlink
+        """
+        from beem.comment import Comment
+        c = Comment(comment, steem_instance=self.steem)
+        for v in c["active_votes"]:
+            if v["voter"] == self["name"]:
+                return v
+        return None
+
+    def has_voted(self, comment):
+        """Returns if the account has already voted for comment
+
+            :param str/Comment comment: can be a Comment object or a authorpermlink
+        """
+        from beem.comment import Comment
+        c = Comment(comment, steem_instance=self.steem)
+        active_votes = {v["voter"]: v for v in c["active_votes"]}
+        return self["name"] in active_votes
+
+    def virtual_op_count(self, until=None):
+        """Returns the number of individual account transactions"""
+        if until is not None and isinstance(until, datetime):
+            limit = until
+            last_gen = self.history_reverse(limit=limit)
+            last_item = 0
+            for item in last_gen:
+                last_item = item[0]
+            return last_item
+        else:
+            try:
+                if self.steem.rpc.get_use_appbase():
+                    return self.steem.rpc.get_account_history(self["name"], -1, 0)[0][0]
+                else:
+                    return self.steem.rpc.get_account_history(self["name"], -1, 0, api="database")[0][0]
+            except IndexError:
+                return 0
+
+    def get_curation_reward(self, days=7):
+        """Returns the curation reward of the last `days` days
+
+            :param int days: limit number of days to be included int the return value
+        """
+        timedelta(days=days)
+        utc = pytz.timezone('UTC')
+        stop = utc.localize(datetime.utcnow()) - timedelta(days=days)
+        reward_vests = Amount("0 VESTS")
+        for reward in self.history_reverse(stop=stop, only_ops=["curation_reward"]):
+            reward_vests += Amount(reward['reward'])
+        return self.steem.vests_to_sp(reward_vests.amount)
+
+    def curation_stats(self):
+        return {"24hr": self.get_curation_reward(days=1),
+                "7d": self.get_curation_reward(days=7),
+                "avg": self.get_curation_reward(days=7) / 7}
+
+    def get_account_history(self, index, limit, start=None, stop=None, order=-1, only_ops=[], exclude_ops=[], raw_output=False):
+        """ Returns a generator for individual account transactions. This call can be used in a
+            ``for`` loop.
+            :param int index: first number of transactions to
+                return
+            :param int limit: limit number of transactions to
+                return
+            :param int/datetime start: start number/date of transactions to
+                return (*optional*)
+            :param int/datetime stop: stop number/date of transactions to
+                return (*optional*)
+            :param array only_ops: Limit generator by these
+                operations (*optional*)
+            :param array exclude_ops: Exclude thse operations from
+                generator (*optional*)
+            :param int batch_size: internal api call batch size (*optional*)
+            :param (-1, 1) order: 1 for chronological, -1 for reverse order
+        """
+        if order != -1 and order != 1:
+            raise ValueError("order must be -1 or 1!")
         if self.steem.rpc.get_use_appbase():
-            return self.steem.rpc.find_votes({'author': account["name"], 'permlink': ''}, api="database")['votes']
+            txs = self.steem.rpc.get_account_history(self["name"], index, limit)
+        else:
+            txs = self.steem.rpc.get_account_history(self["name"], index, limit, api="database")
+
+        if start and isinstance(start, datetime) and start.tzinfo is None:
+            utc = pytz.timezone('UTC')
+            start = utc.localize(start)
+        if stop and isinstance(stop, datetime) and stop.tzinfo is None:
+            utc = pytz.timezone('UTC')
+            stop = utc.localize(stop)
+
+        if order == -1:
+            txs_list = reversed(txs)
         else:
-            return self.steem.rpc.get_account_votes(account)
+            txs_list = txs
+        for item in txs_list:
+            item_index, event = item
+            if start and isinstance(start, datetime):
+                timediff = start - formatTimeString(event["timestamp"])
+                if timediff.total_seconds() * float(order) > 0:
+                    continue
+            elif start and order == 1 and item_index < start:
+                continue
+            elif start and order == -1 and item_index > start:
+                continue
+            if stop and isinstance(stop, datetime):
+                timediff = stop - formatTimeString(event["timestamp"])
+                if timediff.total_seconds() * float(order) < 0:
+                    return
+            elif stop and order == 1 and item_index > stop:
+                return
+            elif stop and order == -1 and item_index < stop:
+                return
+            op_type, op = event['op']
+            block_props = remove_from_dict(event, keys=['op'], keep_keys=False)
+
+            def construct_op(account_name):
+                # verbatim output from steemd
+                if raw_output:
+                    return item
+
+                # index can change during reindexing in
+                # future hard-forks. Thus we cannot take it for granted.
+                immutable = op.copy()
+                immutable.update(block_props)
+                immutable.update({
+                    'account': account_name,
+                    'type': op_type,
+                })
+                _id = Blockchain.hash_op(immutable)
+                immutable.update({
+                    '_id': _id,
+                    'index': item_index,
+                })
+                return immutable
+
+            if exclude_ops and op_type in exclude_ops:
+                continue
+            if not only_ops or op_type in only_ops:
+                yield construct_op(self["name"])
 
     def history(
-        self, limit=100,
-        only_ops=[], exclude_ops=[]
+        self, start=None, stop=None,
+        only_ops=[], exclude_ops=[], batch_size=1000, raw_output=False
     ):
         """ Returns a generator for individual account transactions. The
-            latest operation will be first. This call can be used in a
+            earlist operation will be first. This call can be used in a
             ``for`` loop.
 
-            :param int/datetime limit: limit number of transactions to
+            :param int/datetime start: start number/date of transactions to
+                return (*optional*)
+            :param int/datetime stop: stop number/date of transactions to
                 return (*optional*)
             :param array only_ops: Limit generator by these
                 operations (*optional*)
             :param array exclude_ops: Exclude thse operations from
                 generator (*optional*)
+            :param int batch_size: internal api call batch size (*optional*)
         """
-        _limit = 100
-        cnt = 0
-
-        mostrecent = self.steem.rpc.get_account_history(
-            self["name"],
-            -1,
-            1
-        )
-        if not mostrecent:
-            return
-        if limit < 2:
-            yield mostrecent
+        _limit = batch_size
+        max_index = self.virtual_op_count()
+        if not max_index:
             return
-        first = int(mostrecent[0][0])
+        if start is None:
+            start_index = 0
+        else:
+            if isinstance(start, datetime):
+                start_index = 0
+            else:
+                start_index = start
 
+        first = start_index + _limit
         while True:
             # RPC call
-            txs = self.steem.rpc.get_account_history(
-                self["name"],
-                first,
-                _limit,
-            )
-            for i in reversed(txs):
-                if exclude_ops and i[1]["op"][0] in exclude_ops:
+            for item in self.get_account_history(first, _limit, start=start, stop=stop, order=1, raw_output=raw_output):
+                if raw_output:
+                    item_index, event = item
+                    op_type, op = event['op']
+                    timestamp = event["timestamp"]
+                else:
+                    item_index = item['index']
+                    op_type = item['type']
+                    timestamp = item["timestamp"]
+                if start and isinstance(start, datetime):
+                    timediff = start - formatTimeString(timestamp)
+                    if timediff.total_seconds() > 0:
+                        continue
+                elif start and item_index < start:
                     continue
-                if not only_ops or i[1]["op"][0] in only_ops:
-                    cnt += 1
-                    if isinstance(limit, datetime):
-                        timediff = limit - formatTimeString(i[1]["timestamp"])
-                        if timediff.total_seconds() > 0:
-                            return
-                        yield i
-                    else:
-                        yield i
-                        if limit >= 0 and cnt >= limit:
-                            return
-            if not txs:
-                break
-            if len(txs) < _limit:
+                if exclude_ops and op_type in exclude_ops:
+                    continue
+                if not only_ops or op_type in only_ops:
+                    yield item
+                if stop and isinstance(stop, datetime):
+                    timediff = stop - formatTimeString(timestamp)
+                    if timediff.total_seconds() < 0:
+                        first = max_index + _limit
+                        return
+                elif stop and item_index >= stop:
+                    first = max_index + _limit
+                    return
+            first += (_limit + 1)
+            if first >= max_index + _limit:
                 break
-            # first = int(txs[-1]["id"].split(".")[2])
-            first = txs[0][0]
-            if first < 2:
+
+    def history_reverse(
+        self, start=None, stop=None,
+        only_ops=[], exclude_ops=[], batch_size=1000, raw_output=False
+    ):
+        """ Returns a generator for individual account transactions. The
+            latest operation will be first. This call can be used in a
+            ``for`` loop.
+
+            :param int/datetime start: start number/date of transactions to
+                return. If negative the virtual_op_count is added. (*optional*)
+            :param int/datetime stop: stop number/date of transactions to
+                return. If negative the virtual_op_count is added. (*optional*)
+            :param array only_ops: Limit generator by these
+                operations (*optional*)
+            :param array exclude_ops: Exclude thse operations from
+                generator (*optional*)
+            :param int batch_size: internal api call batch size (*optional*)
+        """
+        _limit = batch_size
+        first = self.virtual_op_count()
+        if not first:
+            return
+        if start is not None and isinstance(start, int) and start < 0:
+            start += first
+        elif not isinstance(start, datetime):
+            first = start
+        if stop is not None and isinstance(stop, int) and stop < 0:
+            stop += first
+
+        while True:
+            # RPC call
+            if first - _limit < 0:
+                _limit = first
+            for item in self.get_account_history(first, _limit, start=start, stop=stop, order=-1, only_ops=only_ops, exclude_ops=exclude_ops, raw_output=raw_output):
+                if raw_output:
+                    item_index, event = item
+                    op_type, op = event['op']
+                    timestamp = event["timestamp"]
+                else:
+                    item_index = item['index']
+                    op_type = item['type']
+                    timestamp = item["timestamp"]
+                if start and isinstance(start, datetime):
+                    timediff = start - formatTimeString(timestamp)
+                    if timediff.total_seconds() < 0:
+                        continue
+                elif start and item_index > start:
+                    continue
+                if exclude_ops and op_type in exclude_ops:
+                    continue
+                if not only_ops or op_type in only_ops:
+                    yield item            
+                if stop and isinstance(stop, datetime):
+                    timediff = stop - formatTimeString(timestamp)
+                    if timediff.total_seconds() > 0:
+                        first = 0
+                        return
+                elif stop and item_index < stop:
+                    first = 0
+                    return
+            first -= (_limit + 1)
+            if first < 1:
                 break
-            if first < _limit:
-                _limit = first - 1
 
     def unfollow(self, unfollow, what=["blog"], account=None):
         """ Unfollow another account's blog
@@ -694,7 +921,7 @@ class Account(BlockchainObject):
             "approve": approve,
             "prefix": self.steem.prefix,
         })
-        return self.steem.finalizeOp(op, account["name"], "active", **kwargs)
+        return self.steem.finalizeOp(op, account, "active", **kwargs)
 
     def disapprovewitness(self, witness, account=None, **kwargs):
         """ Disapprove a witness
@@ -731,7 +958,7 @@ class Account(BlockchainObject):
             "json_metadata": account["json_metadata"],
             "prefix": self.steem.prefix,
         })
-        return self.steem.finalizeOp(op, account["name"], "active", **kwargs)
+        return self.steem.finalizeOp(op, account, "active", **kwargs)
 
     # -------------------------------------------------------------------------
     # Simple Transfer
@@ -789,9 +1016,7 @@ class Account(BlockchainObject):
         if not to:
             to = account  # powerup on the same account
         account = Account(account, steem_instance=self.steem)
-        if isinstance(amount, string_types):
-            amount = Amount(amount, steem_instance=self.steem)
-        elif isinstance(amount, Amount):
+        if isinstance(amount, (string_types, Amount)):
             amount = Amount(amount, steem_instance=self.steem)
         else:
             amount = Amount(amount, "STEEM", steem_instance=self.steem)
@@ -820,9 +1045,7 @@ class Account(BlockchainObject):
         if not account:
             raise ValueError("You need to provide an account")
         account = Account(account, steem_instance=self.steem)
-        if isinstance(amount, string_types):
-            amount = Amount(amount, steem_instance=self.steem)
-        elif isinstance(amount, Amount):
+        if isinstance(amount, (string_types, Amount)):
             amount = Amount(amount, steem_instance=self.steem)
         else:
             amount = Amount(amount, "SBD", steem_instance=self.steem)
@@ -963,27 +1186,21 @@ class Account(BlockchainObject):
 
         # if no values were set by user, claim all outstanding balances on
         # account
-        if isinstance(reward_steem, string_types):
-            reward_steem = Amount(reward_steem, steem_instance=self.steem)
-        elif isinstance(reward_steem, Amount):
+        if isinstance(reward_steem, (string_types, Amount)):
             reward_steem = Amount(reward_steem, steem_instance=self.steem)
         else:
             reward_steem = Amount(reward_steem, "STEEM", steem_instance=self.steem)
         if not reward_steem["symbol"] == "STEEM":
             raise AssertionError()
 
-        if isinstance(reward_sbd, string_types):
-            reward_sbd = Amount(reward_sbd, steem_instance=self.steem)
-        elif isinstance(reward_sbd, Amount):
+        if isinstance(reward_sbd, (string_types, Amount)):
             reward_sbd = Amount(reward_sbd, steem_instance=self.steem)
         else:
             reward_sbd = Amount(reward_sbd, "SBD", steem_instance=self.steem)
         if not reward_sbd["symbol"] == "SBD":
             raise AssertionError()
 
-        if isinstance(reward_vests, string_types):
-            reward_vests = Amount(reward_vests, steem_instance=self.steem)
-        elif isinstance(reward_vests, Amount):
+        if isinstance(reward_vests, (string_types, Amount)):
             reward_vests = Amount(reward_vests, steem_instance=self.steem)
         else:
             reward_vests = Amount(reward_vests, "VESTS", steem_instance=self.steem)
@@ -1020,9 +1237,7 @@ class Account(BlockchainObject):
         if not account:
             raise ValueError("You need to provide an account")
         account = Account(account, steem_instance=self.steem)
-        if isinstance(vesting_shares, string_types):
-            vesting_shares = Amount(vesting_shares, steem_instance=self.steem)
-        elif isinstance(vesting_shares, Amount):
+        if isinstance(vesting_shares, (string_types, Amount)):
             vesting_shares = Amount(vesting_shares, steem_instance=self.steem)
         else:
             vesting_shares = Amount(vesting_shares, "VESTS", steem_instance=self.steem)
@@ -1049,9 +1264,7 @@ class Account(BlockchainObject):
         if not account:
             raise ValueError("You need to provide an account")
         account = Account(account, steem_instance=self.steem)
-        if isinstance(amount, string_types):
-            amount = Amount(amount, steem_instance=self.steem)
-        elif isinstance(amount, Amount):
+        if isinstance(amount, (string_types, Amount)):
             amount = Amount(amount, steem_instance=self.steem)
         else:
             amount = Amount(amount, "VESTS", steem_instance=self.steem)
diff --git a/beem/blockchain.py b/beem/blockchain.py
index 6427dd23..45eabdff 100644
--- a/beem/blockchain.py
+++ b/beem/blockchain.py
@@ -8,12 +8,15 @@ from builtins import str
 from builtins import range
 from builtins import object
 import time
+import hashlib
+import json
+import math
+from datetime import datetime, timedelta
 from .block import Block
 from .blockchainobject import BlockchainObject
+from beemgraphenebase.py23 import py23_bytes
 from beem.instance import shared_steem_instance
 from .amount import Amount
-from datetime import datetime, timedelta
-import math
 FUTURES_MODULE = None
 if not FUTURES_MODULE:
     try:
@@ -98,7 +101,7 @@ class Blockchain(object):
             .. note:: The block number returned depends on the ``mode`` used
                       when instanciating from this class.
         """
-        block_time_seconds = 3
+        block_time_seconds = self.steem.get_block_interval()
         last_block = self.get_current_block()
         if estimateForwards:
             block_offset = 10
@@ -160,13 +163,7 @@ class Blockchain(object):
              confirmed by 2/3 of all block producers and is thus irreversible)
         """
         # Let's find out how often blocks are generated!
-        props = self.steem.get_config()
-        if "STEEMIT_BLOCK_INTERVAL" in props:
-            block_interval = props["STEEMIT_BLOCK_INTERVAL"]
-        elif "STEEM_BLOCK_INTERVAL" in props:
-            block_interval = props["STEEM_BLOCK_INTERVAL"]
-        else:
-            block_interval = 3
+        block_interval = self.steem.get_block_interval()
 
         if not start:
             start = self.get_current_block_num()
@@ -358,6 +355,12 @@ class Blockchain(object):
                 raise Exception(
                     "The operation has not been added after 10 blocks!")
 
+    @staticmethod
+    def hash_op(event):
+        """ This method generates a hash of blockchain operation. """
+        data = json.dumps(event, sort_keys=True)
+        return hashlib.sha1(py23_bytes(data, 'utf-8')).hexdigest()
+
     def get_all_accounts(self, start='', stop='', steps=1e3, limit=-1, **kwargs):
         """ Yields account names between start and stop.
 
diff --git a/beem/cli.py b/beem/cli.py
index 5979d3f3..93e436fc 100644
--- a/beem/cli.py
+++ b/beem/cli.py
@@ -8,6 +8,7 @@ from beem.instance import set_shared_steem_instance, shared_steem_instance
 from beem.amount import Amount
 from beem.account import Account
 from beem.steem import Steem
+from beem.comment import Comment
 from beem.storage import configStorage
 from beem.version import version as __version__
 from datetime import datetime, timedelta
@@ -68,21 +69,24 @@ def cli(node, offline, nobroadcast, unsigned, blocking, bundle, expiration, debu
 
 @cli.command()
 @click.option(
-    '--account', '-a', multiple=False)
+    '--account', '-a', multiple=False, help="Set default account")
 @click.option(
-    '--node', '-n', multiple=True)
-def set(account, node):
+    '--weight', '-w', multiple=False, help="Set your default vote weight")
+@click.option(
+    '--node', '-n', multiple=True, help="Set nodes")
+def set(account, weight, node):
     """ Set configuration
     """
     stm = shared_steem_instance()
     if account:
         stm.set_default_account(account)
+    if weight:
+        configStorage["default_vote_weight"] = weight
     if len(node) > 0:
         nodes = []
         for n in node:
             nodes.append(n)
-        if "node" in stm.config:
-            stm.config["node"] = nodes
+        configStorage["node"] = nodes
 
 
 @cli.command()
@@ -96,7 +100,8 @@ def config():
         # hide internal config data
         if key in availableConfigurationKeys:
             t.add_row([key, stm.config[key]])
-    t.add_row(["nodes", stm.config.nodes])
+    for node in stm.config.nodes:
+        t.add_row(["node", node])
     t.add_row(["data_dir", stm.config.data_dir])
     print(t)
 
@@ -174,8 +179,7 @@ def listkeys():
 
 @cli.command()
 def listaccounts():
-    """ Show stored accounts
-    """
+    """Show stored accounts"""
     stm = shared_steem_instance()
     t = PrettyTable(["Name", "Type", "Available Key"])
     t.align = "l"
@@ -187,6 +191,20 @@ def listaccounts():
     print(t)
 
 
+@cli.command()
+@click.argument('post', nargs=1)
+@click.option('--account', '-a')
+@click.option('--weight', '-w', default=100.0, help='Vote weight (from 0.1 to 100.0)')
+def upvote(post, account, weight):
+    """Upvote a post/comment"""
+    if not weight:
+        weight = configStorage["default_vote_weight"]
+    if not account:
+        account = configStorage["default_account"]
+    post = Comment(post)
+    post.upvote(weight, voter=account)
+
+
 @cli.command()
 @click.option('--password', prompt=True, hide_input=True,
               confirmation_prompt=True)
@@ -240,14 +258,26 @@ def balance(account):
 
 
 @cli.command()
-@click.option(
-    '--account', '-a', multiple=True)
-def info(account):
+@click.argument('objects', nargs=-1)
+def info(objects):
     """ Show info
     """
     stm = shared_steem_instance()
-    for name in account:
-        a = Account(name, steem_instance=stm)
+    if not objects:
+        t = PrettyTable(["Key", "Value"])
+        t.align = "l"
+        info = stm.get_dynamic_global_properties()
+        median_price = stm.get_current_median_history()
+        steem_per_mvest = stm.get_steem_per_mvest()
+        price = (Amount(median_price["base"]).amount / Amount(
+            median_price["quote"]).amount)
+        for key in info:
+            t.add_row([key, info[key]])
+        t.add_row(["steem per mvest", steem_per_mvest])
+        t.add_row(["internal price", price])
+        print(t.get_string(sortby="Key"))
+    for o in objects:
+        a = Account(o, steem_instance=stm)
         a.print_info()
         print("\n")
 
diff --git a/beem/steem.py b/beem/steem.py
index 370d534c..23aacbb6 100644
--- a/beem/steem.py
+++ b/beem/steem.py
@@ -337,6 +337,17 @@ class Steem(object):
         )
         return a.as_base("SBD")
 
+    def get_block_interval(self):
+        """Returns the block intervall in seconds"""
+        props = self.get_config()
+        if "STEEMIT_BLOCK_INTERVAL" in props:
+            block_interval = props["STEEMIT_BLOCK_INTERVAL"]
+        elif "STEEM_BLOCK_INTERVAL" in props:
+            block_interval = props["STEEM_BLOCK_INTERVAL"]
+        else:
+            block_interval = 3
+        return block_interval
+
     def rshares_to_sbd(self, rshares):
         """ Calculates the SBD amount of a vote
         """
diff --git a/beem/transactionbuilder.py b/beem/transactionbuilder.py
index c8681968..9f4a64ee 100644
--- a/beem/transactionbuilder.py
+++ b/beem/transactionbuilder.py
@@ -296,6 +296,7 @@ class TransactionBuilder(dict):
                 self.steem.rpc.broadcast_transaction(
                     ret, api="network_broadcast")
         except Exception as e:
+            self.clear()
             raise e
 
         self.clear()
diff --git a/beem/version.py b/beem/version.py
index 269ef186..3f796c97 100644
--- a/beem/version.py
+++ b/beem/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.15'
+version = '0.19.16'
diff --git a/beem/vote.py b/beem/vote.py
index eb924d71..95d95cee 100644
--- a/beem/vote.py
+++ b/beem/vote.py
@@ -228,10 +228,7 @@ class AccountVotes(VotesObject):
         self.steem = steem_instance or shared_steem_instance()
 
         account = Account(account, steem_instance=self.steem)
-        if self.steem.rpc.get_use_appbase():
-            votes = self.steem.rpc.get_account_votes(account["name"])
-        else:
-            votes = self.steem.rpc.get_account_votes(account["name"])
+        votes = account.get_account_votes()
 
         super(AccountVotes, self).__init__(
             [
diff --git a/beemapi/version.py b/beemapi/version.py
index 269ef186..3f796c97 100644
--- a/beemapi/version.py
+++ b/beemapi/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.15'
+version = '0.19.16'
diff --git a/beembase/version.py b/beembase/version.py
index 269ef186..3f796c97 100644
--- a/beembase/version.py
+++ b/beembase/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.15'
+version = '0.19.16'
diff --git a/beemgrapheneapi/graphenerpc.py b/beemgrapheneapi/graphenerpc.py
index 7e736b46..069d9400 100644
--- a/beemgrapheneapi/graphenerpc.py
+++ b/beemgrapheneapi/graphenerpc.py
@@ -24,6 +24,7 @@ WEBSOCKET_MODULE = None
 if not WEBSOCKET_MODULE:
     try:
         import websocket
+        from websocket._exceptions import WebSocketConnectionClosedException
         WEBSOCKET_MODULE = "websocket"
     except ImportError:
         WEBSOCKET_MODULE = None
@@ -110,33 +111,34 @@ class GrapheneRPC(object):
         """Check if node is appbase ready"""
         return self.current_rpc >= 2
 
-    def rpcconnect(self):
+    def rpcconnect(self, next_url=True):
         """Connect to next url in a loop."""
         cnt = 0
         if self.urls is None:
             return
         while True:
-            cnt += 1
-            self.url = next(self.urls)
-            log.debug("Trying to connect to node %s" % self.url)
-            if self.url[:3] == "wss":
-                if WEBSOCKET_MODULE is None:
-                    raise Exception()
-                sslopt_ca_certs = {'cert_reqs': ssl.CERT_NONE}
-                self.ws = websocket.WebSocket(sslopt=sslopt_ca_certs, enable_multithread=True)
-                self.current_rpc = self.rpc_methods["ws"]
-            elif self.url[:3] == "ws":
-                if WEBSOCKET_MODULE is None:
-                    raise Exception()
-                self.ws = websocket.WebSocket(enable_multithread=True)
-                self.current_rpc = self.rpc_methods["ws"]
-            else:
-                if REQUEST_MODULE is None:
-                    raise Exception()
-                self.ws = None
-                self.current_rpc = self.rpc_methods["jsonrpc"]
-                self.headers = {'User-Agent': 'beem v0.19.14',
-                                'content-type': 'application/json'}
+            cnt += 1 
+            if next_url:                
+                self.url = next(self.urls)
+                log.debug("Trying to connect to node %s" % self.url)
+                if self.url[:3] == "wss":
+                    if WEBSOCKET_MODULE is None:
+                        raise Exception()
+                    sslopt_ca_certs = {'cert_reqs': ssl.CERT_NONE}
+                    self.ws = websocket.WebSocket(sslopt=sslopt_ca_certs, enable_multithread=True)
+                    self.current_rpc = self.rpc_methods["ws"]
+                elif self.url[:3] == "ws":
+                    if WEBSOCKET_MODULE is None:
+                        raise Exception()
+                    self.ws = websocket.WebSocket(enable_multithread=True)
+                    self.current_rpc = self.rpc_methods["ws"]
+                else:
+                    if REQUEST_MODULE is None:
+                        raise Exception()
+                    self.ws = None
+                    self.current_rpc = self.rpc_methods["jsonrpc"]
+                    self.headers = {'User-Agent': 'beem v0.19.14',
+                                    'content-type': 'application/json'}
 
             try:
                 if not self.ws:
@@ -149,6 +151,7 @@ class GrapheneRPC(object):
             except Exception as e:
                 log.critical("Error: {}\n".format(str(e)))
                 sleep_and_check_retries(self.num_retries, cnt, self.url)
+                next_url = True
         try:
             props = self.get_config(api="database")
         except:
@@ -216,6 +219,9 @@ class GrapheneRPC(object):
                 break
             except KeyboardInterrupt:
                 raise
+            except WebSocketConnectionClosedException:
+                self.rpcconnect(next_url=False)
+                self.register_apis()                
             except Exception as e:
                 log.critical("Error: {}\n".format(str(e)))
                 sleep_and_check_retries(self.num_retries, cnt, self.url)
diff --git a/beemgrapheneapi/version.py b/beemgrapheneapi/version.py
index 269ef186..3f796c97 100644
--- a/beemgrapheneapi/version.py
+++ b/beemgrapheneapi/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.15'
+version = '0.19.16'
diff --git a/beemgraphenebase/version.py b/beemgraphenebase/version.py
index 269ef186..3f796c97 100644
--- a/beemgraphenebase/version.py
+++ b/beemgraphenebase/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.15'
+version = '0.19.16'
diff --git a/examples/compare_with_steem_python_account.py b/examples/compare_with_steem_python_account.py
new file mode 100644
index 00000000..06ad1038
--- /dev/null
+++ b/examples/compare_with_steem_python_account.py
@@ -0,0 +1,95 @@
+from __future__ import print_function
+import sys
+from datetime import timedelta
+import time
+import io
+from beem import Steem
+from beem.account import Account
+from beem.amount import Amount
+from beem.utils import parse_time
+from steem.account import Account as steemAccount
+from steem.post import Post as steemPost
+from steem import Steem as steemSteem
+import logging
+log = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+
+if __name__ == "__main__":
+    stm = Steem("https://api.steemit.com")
+    beem_acc = Account("holger80", steem_instance=stm)
+    stm2 = steemSteem(nodes=["https://api.steemit.com"])
+    steem_acc = steemAccount("holger80", steemd_instance=stm2)
+
+    # profile
+    print("beem_acc.profile  {}".format(beem_acc.profile))
+    print("steem_acc.profile {}".format(steem_acc.profile))
+    # sp
+    print("beem_acc.sp  {}".format(beem_acc.sp))
+    print("steem_acc.sp {}".format(steem_acc.sp))
+    # rep
+    print("beem_acc.rep  {}".format(beem_acc.rep))
+    print("steem_acc.rep {}".format(steem_acc.rep))
+    # balances
+    print("beem_acc.balances  {}".format(beem_acc.balances))
+    print("steem_acc.balances {}".format(steem_acc.balances))
+    # get_balances()
+    print("beem_acc.get_balances()  {}".format(beem_acc.get_balances()))
+    print("steem_acc.get_balances() {}".format(steem_acc.get_balances()))
+    # reputation()
+    print("beem_acc.get_reputation()  {}".format(beem_acc.get_reputation()))
+    print("steem_acc.reputation() {}".format(steem_acc.reputation()))
+    # voting_power()
+    print("beem_acc.get_voting_power()  {}".format(beem_acc.get_voting_power()))
+    print("steem_acc.voting_power() {}".format(steem_acc.voting_power()))
+    # get_followers()
+    print("beem_acc.get_followers()  {}".format(beem_acc.get_followers()))
+    print("steem_acc.get_followers() {}".format(steem_acc.get_followers()))
+    # get_following()
+    print("beem_acc.get_following()  {}".format(beem_acc.get_following()))
+    print("steem_acc.get_following() {}".format(steem_acc.get_following()))
+    # has_voted()
+    print("beem_acc.has_voted()  {}".format(beem_acc.has_voted("@holger80/api-methods-list-for-appbase")))
+    print("steem_acc.has_voted() {}".format(steem_acc.has_voted(steemPost("@holger80/api-methods-list-for-appbase"))))
+    # curation_stats()
+    print("beem_acc.curation_stats()  {}".format(beem_acc.curation_stats()))
+    print("steem_acc.curation_stats() {}".format(steem_acc.curation_stats()))
+    # virtual_op_count
+    print("beem_acc.virtual_op_count()  {}".format(beem_acc.virtual_op_count()))
+    print("steem_acc.virtual_op_count() {}".format(steem_acc.virtual_op_count()))
+    # get_account_votes
+    print("beem_acc.get_account_votes()  {}".format(beem_acc.get_account_votes()))
+    print("steem_acc.get_account_votes() {}".format(steem_acc.get_account_votes()))
+    # get_withdraw_routes
+    print("beem_acc.get_withdraw_routes()  {}".format(beem_acc.get_withdraw_routes()))
+    print("steem_acc.get_withdraw_routes() {}".format(steem_acc.get_withdraw_routes()))
+    # get_conversion_requests
+    print("beem_acc.get_conversion_requests()  {}".format(beem_acc.get_conversion_requests()))
+    print("steem_acc.get_conversion_requests() {}".format(steem_acc.get_conversion_requests()))
+    # export
+    # history
+    beem_hist = []
+    for h in beem_acc.history(only_ops=["transfer"]):
+        beem_hist.append(h)
+        if len(beem_hist) >= 10:
+            break
+    steem_hist = []
+    for h in steem_acc.history(filter_by="transfer", start=0):
+        steem_hist.append(h)
+        if len(steem_hist) >= 10:
+            break
+    print("beem_acc.history()  {}".format(beem_hist))
+    print("steem_acc.history() {}".format(steem_hist))
+    # history_reverse
+    beem_hist = []
+    for h in beem_acc.history_reverse(only_ops=["transfer"]):
+        beem_hist.append(h)
+        if len(beem_hist) >= 10:
+            break
+    steem_hist = []
+    for h in steem_acc.history_reverse(filter_by="transfer"):
+        steem_hist.append(h)
+        if len(steem_hist) >= 10:
+            break
+    print("beem_acc.history_reverse()  {}".format(beem_hist))
+    print("steem_acc.history_reverse() {}".format(steem_hist))
diff --git a/examples/op_on_testnet.py b/examples/op_on_testnet.py
index d03f0d00..4d8d1c48 100644
--- a/examples/op_on_testnet.py
+++ b/examples/op_on_testnet.py
@@ -49,6 +49,8 @@ if __name__ == "__main__":
     stm.wallet.addPrivateKey(memo_privkey)
     stm.wallet.addPrivateKey(posting_privkey)
 
+    account.allow('beem1', weight=1, permission='posting', account=None)
+
     stm.wallet.getAccountFromPrivateKey(str(active_privkey))
 
     # stm.create_account("beem1", creator=account, password=password1)
diff --git a/examples/print_appbase_calls.py b/examples/print_appbase_calls.py
index 29af2277..1e1712f1 100644
--- a/examples/print_appbase_calls.py
+++ b/examples/print_appbase_calls.py
@@ -16,6 +16,7 @@ logging.basicConfig(level=logging.INFO)
 
 if __name__ == "__main__":
     stm = Steem(node="https://api.steemitstage.com")
+    # stm = Steem(node="wss://appbasetest.timcliff.com")
     all_calls = stm.rpc.get_methods(api="jsonrpc")
     t = PrettyTable(["method", "args", "ret"])
     t.align = "l"
diff --git a/setup.py b/setup.py
index 4ab1fcfd..489c711b 100755
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@ except LookupError:
     ascii = codecs.lookup('ascii')
     codecs.register(lambda name, enc=ascii: {True: enc}.get(name == 'mbcs'))
 
-VERSION = '0.19.15'
+VERSION = '0.19.16'
 
 tests_require = ['mock >= 2.0.0', 'pytest', 'pytest-mock', 'parameterized']
 
diff --git a/tests/test_account.py b/tests/test_account.py
index 42a13bb3..30b9d62b 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -66,9 +66,17 @@ class Testcases(unittest.TestCase):
         account.print_info()
         # self.assertIsInstance(account.balance({"symbol": symbol}), Amount)
         self.assertIsInstance(account.available_balances, list)
-        for h in account.history(limit=1):
-            pass
-
+        self.assertTrue(account.virtual_op_count() > 0)
+        h_list = []
+        for h in account.history(stop=10, batch_size=11, raw_output=True):
+            h_list.append(h)
+        self.assertEqual(h_list[0][0], 0)
+        self.assertEqual(h_list[-1][0], 10)
+        h_list = []
+        for h in account.history_reverse(start=10, stop=0, batch_size=11, raw_output=False):
+            h_list.append(h)
+        self.assertEqual(h_list[0]['index'], 10)
+        self.assertEqual(h_list[-1]['index'], 0)
         # BlockchainObjects method
         account.cached = False
         self.assertTrue(list(account.items()))
-- 
GitLab