From 47cf2b4ceeeef87532722d8197400ded7e59fc16 Mon Sep 17 00:00:00 2001
From: Holger Nahrstaedt <holger@nahrstaedt.de>
Date: Wed, 21 Feb 2018 18:34:37 +0100
Subject: [PATCH] account:  * reputation added * history improved and fixed
 block: * ops and ops_statistics added blockchain: * function-names improved *
 ops_statistics added blockchainobject *json export added Comment class added
 Discussion class added steem: * follow and account by key api added and fixed
 storage: * more nodes added utils * Helpfunctions added  vote *
 blockchainobject * refresh fixed wallet * purgeWallet added Witness *
 printAsTable function added * WitnessesVotedByAccount added *
 WitnessesRankedByVote added * WitnessesByIds added * LookupWitnesses added
 Steemnoderpc * register_apis fixed test_wallet * unit tests added test_utils
 * unittests added

---
 README.rst                  |   9 ++
 docs/comment.rst            |  17 ++++
 docs/index.rst              |   9 +-
 docs/vote.rst               |  15 ++++
 docs/witness.rst            |  13 +++
 setup.py                    |   2 +-
 steempy/__init__.py         |   4 +-
 steempy/account.py          |  56 +++++++++----
 steempy/block.py            |  23 ++++++
 steempy/blockchain.py       |  61 +++++++++++---
 steempy/blockchainobject.py |  10 +++
 steempy/comment.py          | 146 +++++++++++++++++++++++++++++++++
 steempy/discussions.py      |  23 ++++++
 steempy/exceptions.py       |  12 +++
 steempy/steem.py            |  17 ++--
 steempy/storage.py          |   5 +-
 steempy/utils.py            |  94 ++++++++++++++++++++-
 steempy/vote.py             | 106 +++++++++++++++++++-----
 steempy/wallet.py           |   9 ++
 steempy/witness.py          | 159 ++++++++++++++++++++++++++++++++++--
 steempyapi/exceptions.py    |   4 +
 steempyapi/steemnoderpc.py  |  18 ++--
 tests/test_account.py       |   7 +-
 tests/test_amount.py        |   7 +-
 tests/test_asset.py         |   7 +-
 tests/test_base_objects.py  |   6 +-
 tests/test_bip38.py         |  22 +++++
 tests/test_message.py       |   7 +-
 tests/test_objectcache.py   |   7 +-
 tests/test_price.py         |   7 +-
 tests/test_proposals.py     |   7 +-
 tests/test_steem.py         |   7 +-
 tests/test_transactions.py  |   2 -
 tests/test_txbuffers.py     |   5 +-
 tests/test_utils.py         |  50 ++++++++++--
 tests/test_wallet.py        |  37 ++++++++-
 36 files changed, 874 insertions(+), 116 deletions(-)
 create mode 100644 docs/comment.rst
 create mode 100644 docs/vote.rst
 create mode 100644 steempy/comment.py
 create mode 100644 steempy/discussions.py
 create mode 100644 tests/test_bip38.py

diff --git a/README.rst b/README.rst
index 0b0e9c4e..253cbdb9 100644
--- a/README.rst
+++ b/README.rst
@@ -63,6 +63,15 @@ Documentation is available at http://steempy.readthedocs.io/en/latest/
 
 Changelog
 =========
+
+0.19.3
+------
+* Add Comment/Post
+* Add Witness
+* Several bugfixes
+* Added all transactions that are supported from steem-python
+* New library name planned: beem
+
 0.19.2
 ------
 * Notify and websocket fixed
diff --git a/docs/comment.rst b/docs/comment.rst
new file mode 100644
index 00000000..c68ec253
--- /dev/null
+++ b/docs/comment.rst
@@ -0,0 +1,17 @@
+Comment
+~~~~~~~
+
+Read data about a Comment/Post
+
+.. code-block:: python
+
+   from steempy.comment import Comment
+   Witness("gtg")
+
+.. autoclass:: steempy.comment.Comment
+   :members:
+
+.. autoclass:: steempy.comment.Comments
+   :members:
+
+
diff --git a/docs/index.rst b/docs/index.rst
index eb9f7a79..c358cba8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -12,14 +12,13 @@
 Welcome to steempy's documentation!
 ===============================================
 
-Steem is a **blockchain-based autonomous company** (i.e. a DAC) that
-offers decentralized exchanging as well as sophisticated financial
-instruments as *products*.
+Steem is a blockchain-based rewards platform for publishers to monetize 
+content and grow community.
 
 It is based on *Graphene* (tm), a blockchain technology stack (i.e.
 software) that allows for fast transactions and ascalable blockchain
-solution. In case of Steem, it comes with decentralized trading of
-assets as well as customized on-chain smart contracts.
+solution. In case of Steem, it comes with decentralized publishing of
+content.
 
 About this Library
 ------------------
diff --git a/docs/vote.rst b/docs/vote.rst
new file mode 100644
index 00000000..08e50875
--- /dev/null
+++ b/docs/vote.rst
@@ -0,0 +1,15 @@
+Vote
+~~~~
+
+Read data about a vote
+
+.. code-block:: python
+
+   from steempy.vote import Vote
+   Witness("gtg")
+
+.. autoclass:: steempy.vote.Vote
+   :members:
+
+
+   
diff --git a/docs/witness.rst b/docs/witness.rst
index cac1e93d..f3260776 100644
--- a/docs/witness.rst
+++ b/docs/witness.rst
@@ -13,3 +13,16 @@ Read data about a witness
 
 .. autoclass:: steempy.witness.Witnesses
    :members:
+
+.. autoclass:: steempy.witness.WitnessesVotedByAccount
+   :members:
+   
+.. autoclass:: steempy.witness.WitnessesRankedByVote
+   :members:
+   
+.. autoclass:: steempy.witness.WitnessesByIds
+   :members:
+   
+.. autoclass:: steempy.witness.LookupWitnesses
+   :members:
+   
diff --git a/setup.py b/setup.py
index 890b3ba8..d51e992b 100755
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ except LookupError:
     ascii = codecs.lookup('ascii')
     codecs.register(lambda name, enc=ascii: {True: enc}.get(name == 'mbcs'))
 
-VERSION = '0.19.2'
+VERSION = '0.19.3'
 
 
 def write_version_py(filename):
diff --git a/steempy/__init__.py b/steempy/__init__.py
index 043e5706..aee1d512 100644
--- a/steempy/__init__.py
+++ b/steempy/__init__.py
@@ -14,5 +14,7 @@ __all__ = [
     "utils",
     "wallet",
     "vote",
-    "message"
+    "message",
+    "comment",
+    "discussions"
 ]
diff --git a/steempy/account.py b/steempy/account.py
index 00911e9d..8001df1a 100644
--- a/steempy/account.py
+++ b/steempy/account.py
@@ -1,7 +1,10 @@
 from steempy.instance import shared_steem_instance
 from .exceptions import AccountDoesNotExistsException
 from .blockchainobject import BlockchainObject
+from .utils import formatTimeString
+from datetime import datetime
 import json
+import math
 
 
 class Account(BlockchainObject):
@@ -40,7 +43,7 @@ class Account(BlockchainObject):
         self,
         account,
         id_item="name",
-        full=False,
+        full=True,
         lazy=False,
         steem_instance=None
     ):
@@ -68,7 +71,7 @@ class Account(BlockchainObject):
             account = account[0]
         if not account:
             raise AccountDoesNotExistsException(self.identifier)
-        # self.identifier = account["id"]
+        self.identifier = account["name"]
 
         super(Account, self).__init__(account, id_item="name")
 
@@ -87,6 +90,22 @@ class Account(BlockchainObject):
         """
         return json.loads(self["json_metadata"])["profile"]
 
+    @property
+    def rep(self):
+        return self.reputation()
+
+    def reputation(self, precision=2):
+        rep = int(self['reputation'])
+        if rep == 0:
+            return 25
+        score = (math.log10(abs(rep)) - 9) * 9 + 25
+        if rep < 0:
+            score = 50 - score
+        if precision is not None:
+            return round(score, precision)
+        else:
+            return score
+
     @property
     def available_balances(self):
         """ List balances of an account. This call returns instances of
@@ -173,24 +192,20 @@ class Account(BlockchainObject):
             self.refresh()
 
     def history(
-        self, first=None,
-        limit=100,
+        self, limit=100,
         only_ops=[], exclude_ops=[]
     ):
         """ Returns a generator for individual account transactions. The
             latest operation will be first. This call can be used in a
             ``for`` loop.
 
-            :param int first: sequence number of the first
-                transaction to return (*optional*)
-            :param int limit: limit number of transactions to
+            :param int/datetime limit: limit number 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*)
         """
-        from steempybase.operations import getOperationNameForId
         _limit = 100
         cnt = 0
 
@@ -202,9 +217,7 @@ class Account(BlockchainObject):
         if not mostrecent:
             return
 
-        if not first:
-            # first = int(mostrecent[0].get("id").split(".")[2]) + 1
-            first = 9999999999
+        first = int(mostrecent[0][0])
 
         while True:
             # RPC call
@@ -213,21 +226,30 @@ class Account(BlockchainObject):
                 first,
                 _limit,
             )
-            for i in txs:
+            for i in reversed(txs):
                 if exclude_ops and i[1]["op"][0] in exclude_ops:
                     continue
                 if not only_ops or i[1]["op"][0] in only_ops:
                     cnt += 1
-                    yield i
-                    if limit >= 0 and cnt >= limit:
-                        return
-
+                    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:
                 break
             # first = int(txs[-1]["id"].split(".")[2])
-            first = txs[-1][1]["block"]
+            first = txs[0][0]
+            if first < 2:
+                break
+            if first < _limit:
+                _limit = first - 1
 
     # def upgrade(self):
     #    return self.steem.upgrade_account(account=self)
diff --git a/steempy/block.py b/steempy/block.py
index f983eb09..1b76eba0 100644
--- a/steempy/block.py
+++ b/steempy/block.py
@@ -40,6 +40,29 @@ class Block(BlockchainObject):
         """
         return parse_time(self['timestamp'])
 
+    def ops(self):
+        ops = []
+        for tx in self["transactions"]:
+            for op in tx["operations"]:
+                # Replace opid by op name
+                # op[0] = getOperationNameForId(op[0])
+                ops.append(op)
+        return ops
+
+    def ops_statistics(self, add_to_ops_stat=None):
+        if add_to_ops_stat is None:
+            import steempybase.operationids
+            ops_stat = steempybase.operationids.operations.copy()
+            for key in ops_stat:
+                ops_stat[key] = 0
+        else:
+            ops_stat = add_to_ops_stat.copy()
+
+        for tx in self["transactions"]:
+            for op in tx["operations"]:
+                ops_stat[op[0]] += 1
+        return ops_stat
+
 
 class BlockHeader(BlockchainObject):
     def refresh(self):
diff --git a/steempy/blockchain.py b/steempy/blockchain.py
index c3b915a6..e624a089 100644
--- a/steempy/blockchain.py
+++ b/steempy/blockchain.py
@@ -29,27 +29,27 @@ class Blockchain(object):
         else:
             raise ValueError("invalid value for 'mode'!")
 
-    def info(self):
+    def get_dynamic_global_properties(self):
         """ This call returns the *dynamic global properties*
         """
         return self.steem.rpc.get_dynamic_global_properties()
 
-    def feed_history(self):
+    def get_feed_history(self):
         """ Returns the feed_history
         """
         return self.steem.rpc.get_feed_history()
 
-    def current_median_history_price(self):
+    def get_current_median_history_price(self):
         """ Returns the current median price
         """
         return self.steem.rpc.get_current_median_history_price()
 
-    def next_scheduled_hardfork(self):
+    def get_next_scheduled_hardfork(self):
         """ Returns Hardfork and live_time of the hardfork
         """
         return self.steem.rpc.get_next_scheduled_hardfork()
 
-    def hardfork_version(self):
+    def get_hardfork_version(self):
         """ Current Hardfork Version as String
         """
         return self.steem.rpc.get_hardfork_version()
@@ -63,12 +63,23 @@ class Blockchain(object):
         return self.steem.rpc.get_network()
 
     def get_chain_properties(self):
-        """ Return chain properties
+        """ Return witness elected chain properties
+
+            ::
+                {'account_creation_fee': '30.000 STEEM',
+                 'maximum_block_size': 65536,
+                 'sbd_interest_rate': 250}
+
         """
         return self.steem.rpc.get_chain_properties()
 
-    def config(self):
-        """ Returns config
+    def get_state(self, path="value"):
+        """ get_state
+        """
+        return self.steem.rpc.get_state(path)
+
+    def get_config(self):
+        """ Returns internal chain configuration.
         """
         return self.steem.rpc.get_config()
 
@@ -78,7 +89,7 @@ class Blockchain(object):
             .. note:: The block number returned depends on the ``mode`` used
                       when instanciating from this class.
         """
-        return self.info().get(self.mode)
+        return self.get_dynamic_global_properties().get(self.mode)
 
     def get_current_block(self):
         """ This call returns the current block
@@ -123,7 +134,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!
-        block_interval = self.config().get("STEEMIT_BLOCK_INTERVAL")
+        block_interval = self.get_config().get("STEEMIT_BLOCK_INTERVAL")
 
         if not start:
             start = self.get_current_block_num()
@@ -179,6 +190,36 @@ class Blockchain(object):
                         "timestamp": block["timestamp"]
                     }
 
+    def ops_statistics(self, start, stop=None, add_to_ops_stat=None, verbose=True):
+        """ Generates a statistics for all operations (including virtual operations) starting from
+            ``start``.
+
+            :param int start: Starting block
+            :param int stop: Stop at this block, if set to None, the current_block_num is taken
+            :param dict add_to_ops_stat, if set, the result is added to add_to_ops_stat
+            :param bool verbose, if True, the current block number and timestamp is printed
+            This call returns a dict with all possible operations and their occurence.
+        """
+        if add_to_ops_stat is None:
+            import steempybase.operationids
+            ops_stat = steempybase.operationids.operations.copy()
+            for key in ops_stat:
+                ops_stat[key] = 0
+        else:
+            ops_stat = add_to_ops_stat.copy()
+        current_block = self.get_current_block_num()
+        if start > current_block:
+            return
+        if stop is None:
+            stop = current_block
+        for block in self.blocks(start=start, stop=stop):
+            if verbose:
+                print(str(block["block_num"]) + " " + block["timestamp"])
+            for tx in block["transactions"]:
+                for op in tx["operations"]:
+                    ops_stat[op[0]] += 1
+        return ops_stat
+
     def stream(self, opNames=[], *args, **kwargs):
         """ Yield specific operations (e.g. comments) only
 
diff --git a/steempy/blockchainobject.py b/steempy/blockchainobject.py
index 7101284c..9b6df825 100644
--- a/steempy/blockchainobject.py
+++ b/steempy/blockchainobject.py
@@ -1,5 +1,6 @@
 from steempy.instance import shared_steem_instance
 from datetime import datetime, timedelta
+import json
 
 
 class ObjectCache(dict):
@@ -95,6 +96,12 @@ class BlockchainObject(dict):
             self[self.id_item] = str(data)
             # Set identifier again as it is overwritten in super() in refresh()
             self.identifier = data
+        elif isinstance(data, str):
+            self.identifier = data
+            if not lazy and not self.cached:
+                self.refresh()
+            self[self.id_item] = str(data)
+            self.identifier = data
         else:
             self.identifier = data
             if self.test_valid_objectid(self.identifier):
@@ -158,3 +165,6 @@ class BlockchainObject(dict):
     def __repr__(self):
         return "<%s %s>" % (
             self.__class__.__name__, str(self.identifier))
+
+    def json(self):
+        return json.loads(str(json.dumps(self)))
diff --git a/steempy/comment.py b/steempy/comment.py
new file mode 100644
index 00000000..419e2a51
--- /dev/null
+++ b/steempy/comment.py
@@ -0,0 +1,146 @@
+from .instance import shared_steem_instance
+from .account import Account
+from .utils import resolve_authorperm, construct_authorperm
+from .blockchainobject import BlockchainObject
+from .exceptions import ContentDoesNotExistsException
+import json
+import logging
+log = logging.getLogger(__name__)
+
+
+class Comment(BlockchainObject):
+    """ Read data about a Comment/Post in the chain
+
+        :param str authorperm: perm link to post/comment
+        :param steem steem_instance: Steem() instance to use when accesing a RPC
+
+    """
+    type_id = 8
+
+    def __init__(
+        self,
+        authorperm,
+        full=False,
+        lazy=False,
+        steem_instance=None
+    ):
+        self.full = full
+        if isinstance(authorperm, str):
+            [author, permlink] = resolve_authorperm(authorperm)
+            self["author"] = author
+            self["permlink"] = permlink
+            self["authorperm"] = construct_authorperm(author, permlink)
+        elif isinstance(authorperm, dict) and "author" in authorperm and "permlink" in authorperm:
+            self["author"] = authorperm["author"]
+            self["permlink"] = authorperm["permlink"]
+            self["authorperm"] = construct_authorperm(authorperm)
+        super().__init__(
+            authorperm,
+            id_item="authorperm",
+            lazy=lazy,
+            full=full,
+            steem_instance=steem_instance
+        )
+
+    def refresh(self):
+        [author, permlink] = resolve_authorperm(self.identifier)
+        content = self.steem.rpc.get_content(author, permlink)
+        if not content:
+            raise ContentDoesNotExistsException
+        super(Comment, self).__init__(content, id_item="authorperm", steem_instance=self.steem)
+
+        self.identifier = self.authorperm
+
+    def json(self):
+        output = self
+        output.pop("authorperm")
+        return json.loads(str(json.dumps(output)))
+
+    @property
+    def id(self):
+        return self["id"]
+
+    @property
+    def author(self):
+        return self["author"]
+
+    @property
+    def permlink(self):
+        return self["permlink"]
+
+    @property
+    def authorperm(self):
+        return construct_authorperm(self["author"], self["permlink"])
+
+    @property
+    def category(self):
+        return self["category"]
+
+    @property
+    def parent_author(self):
+        return self["parent_author"]
+
+    @property
+    def parent_permlink(self):
+        return self["parent_permlink"]
+
+    @property
+    def title(self):
+        return self["title"]
+
+    @property
+    def body(self):
+        return self["body"]
+
+    @property
+    def json_metadata(self):
+        return self["json_metadata"]
+
+    def is_main_post(self):
+        """ Retuns True if main post, and False if this is a comment (reply).
+        """
+        return self['depth'] == 0
+
+    def is_comment(self):
+        """ Retuns True if post is a comment
+        """
+        return self['depth'] > 0
+
+
+class RecentReplies(list):
+    """ Obtain a list of recent replies
+
+        :param str author: author
+        :param steem steem_instance: Steem() instance to use when accesing a RPC
+    """
+    def __init__(self, author, skip_own=True, steem_instance=None):
+        self.steem = steem_instance or shared_steem_instance()
+        state = self.steem.rpc.get_state("/@%s/recent-replies" % author)
+        replies = state["accounts"][author].get("recent_replies", [])
+        comments = []
+        for reply in replies:
+            post = state["content"][reply]
+            if skip_own and post["author"] == author:
+                continue
+            comments.append(Comment(post, lazy=True, steem_instance=self.steem))
+        super(RecentReplies, self).__init__(comments)
+
+
+class RecentByPath(list):
+    """ Obtain a list of votes for an account
+
+        :param str account: Account name
+        :param steem steem_instance: Steem() instance to use when accesing a RPC
+    """
+    def __init__(self, path="promoted", category=None, steem_instance=None):
+        self.steem = steem_instance or shared_steem_instance()
+
+        state = self.steem.rpc.get_state("/" + path)
+        replies = state["discussion_idx"][''].get(path, [])
+        comments = []
+        for reply in replies:
+            post = state["content"][reply]
+            if category is not None and post["category"] != category:
+                continue
+            comments.append(Comment(post, lazy=True, steem_instance=self.steem))
+        super(RecentByPath, self).__init__(comments)
diff --git a/steempy/discussions.py b/steempy/discussions.py
new file mode 100644
index 00000000..37c2788f
--- /dev/null
+++ b/steempy/discussions.py
@@ -0,0 +1,23 @@
+from .instance import shared_steem_instance
+from .account import Account
+from .comment import Comment
+from .utils import resolve_authorperm
+import logging
+log = logging.getLogger(__name__)
+
+
+class Discussions_by_trending(list):
+    """ get_discussions_by_trending
+
+        :param str discussion_query
+        :param steem steem_instance: Steem() instance to use when accesing a RPC
+    """
+    def __init__(self, discussion_query, steem_instance=None):
+        self.steem = steem_instance or shared_steem_instance()
+        posts = self.steem.rpc.get_discussions_by_trending(discussion_query)
+        super(Discussions_by_trending, self).__init__(
+            [
+                Comment(x)
+                for x in posts
+            ]
+        )
diff --git a/steempy/exceptions.py b/steempy/exceptions.py
index b82924f8..55ee9caf 100644
--- a/steempy/exceptions.py
+++ b/steempy/exceptions.py
@@ -78,6 +78,18 @@ class WitnessDoesNotExistsException(Exception):
     pass
 
 
+class ContentDoesNotExistsException(Exception):
+    """ The content does not exist
+    """
+    pass
+
+
+class VoteDoesNotExistsException(Exception):
+    """ The vote does not exist
+    """
+    pass
+
+
 class WrongMasterPasswordException(Exception):
     """ The password provided could not properly unlock the wallet
     """
diff --git a/steempy/steem.py b/steempy/steem.py
index fbb978d0..a7443860 100644
--- a/steempy/steem.py
+++ b/steempy/steem.py
@@ -3,6 +3,7 @@ import logging
 
 from datetime import datetime, timedelta
 from steempyapi.steemnoderpc import SteemNodeRPC
+from steempyapi.exceptions import NoAccessApi
 from steempybase.account import PrivateKey, PublicKey
 from steempybase import transactions, operations
 from .asset import Asset
@@ -114,11 +115,11 @@ class Steem(object):
             kwargs["apis"] = [
                 "database",
                 "network_broadcast",
-                "market_history",
-                "follow",
-                "account_by_key",
-                "tag",
-                "raw_block"
+                # "market_history",
+                # "follow",
+                # "account_by_key",
+                # "tag",
+                # "raw_block"
             ]
 
         self.rpc = None
@@ -140,6 +141,12 @@ class Steem(object):
                          rpcpassword=rpcpassword,
                          **kwargs)
 
+        # Try Optional APIs
+        try:
+            self.rpc.register_apis(["account_by_key", "follow"])
+        except NoAccessApi as e:
+            log.info(str(e))
+
         self.wallet = Wallet(self.rpc, **kwargs)
 
         # txbuffers/propbuffer are initialized and cleared
diff --git a/steempy/storage.py b/steempy/storage.py
index 7352e26b..730a1281 100644
--- a/steempy/storage.py
+++ b/steempy/storage.py
@@ -217,8 +217,11 @@ class Configuration(DataDir):
     __tablename__ = "config"
 
     #: Default configuration
+    nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+             "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+             "wss://steemd.minnowsupportproject.org"]
     config_defaults = {
-        "node": "wss://steemd.pevo.science",
+        "node": nodes,
         "rpcpassword": "",
         "rpcuser": "",
         "order-expiration": 7 * 24 * 60 * 60,
diff --git a/steempy/utils.py b/steempy/utils.py
index a9e8f079..5fc5330d 100644
--- a/steempy/utils.py
+++ b/steempy/utils.py
@@ -10,9 +10,9 @@ def formatTime(t):
     """ Properly Format Time for permlinks
     """
     if isinstance(t, float):
-        return datetime.utcfromtimestamp(t).strftime(timeFormat)
+        return datetime.utcfromtimestamp(t).strftime("%Y%m%dt%H%M%S%Z")
     if isinstance(t, datetime):
-        return t.strftime(timeFormat)
+        return t.strftime("%Y%m%dt%H%M%S%Z")
 
 
 def formatTimeString(t):
@@ -50,6 +50,34 @@ def assets_from_string(text):
     return re.split(r'[\-:/]', text)
 
 
+def sanitize_permlink(permlink):
+    permlink = permlink.strip()
+    permlink = re.sub("_|\s|\.", "-", permlink)
+    permlink = re.sub("[^\w-]", "", permlink)
+    permlink = re.sub("[^a-zA-Z0-9-]", "", permlink)
+    permlink = permlink.lower()
+    return permlink
+
+
+def derive_permlink(title, parent_permlink=None, parent_author=None):
+    permlink = ""
+
+    if parent_permlink and parent_author:
+        permlink += "re-"
+        permlink += parent_author.replace("@", "")
+        permlink += "-"
+        permlink += parent_permlink
+        permlink += "-" + formatTime(time.time()) + "z"
+    elif parent_permlink:
+        permlink += "re-"
+        permlink += parent_permlink
+        permlink += "-" + formatTime(time.time()) + "z"
+    else:
+        permlink += title
+
+    return sanitize_permlink(permlink)
+
+
 def resolve_authorperm(identifier):
     """Correctly split a string containing an authorperm.
 
@@ -62,6 +90,68 @@ def resolve_authorperm(identifier):
     return match.group(1), match.group(2)
 
 
+def construct_authorperm(*args, username_prefix='@'):
+    """ Create a post identifier from comment/post object or arguments.
+    Examples:
+        ::
+            construct_authorperm('username', 'permlink')
+            construct_authorperm({'author': 'username',
+                'permlink': 'permlink'})
+    """
+    if len(args) == 1:
+        op = args[0]
+        author, permlink = op['author'], op['permlink']
+    elif len(args) == 2:
+        author, permlink = args
+    else:
+        raise ValueError(
+            'construct_identifier() received unparsable arguments')
+
+    fields = dict(prefix=username_prefix, author=author, permlink=permlink)
+    return "{prefix}{author}/{permlink}".format(**fields)
+
+
+def resolve_authorpermvoter(identifier):
+    """Correctly split a string containing an authorpermvoter.
+
+    Splits the string into author and permlink with the
+    following separator: ``/`` and ``|``.
+    """
+    pos = identifier.find("|")
+    if pos < 0:
+        raise ValueError("Invalid identifier")
+    [author, permlink] = resolve_authorperm(identifier[:pos])
+    return author, permlink, identifier[pos + 1:]
+
+
+def construct_authorpermvoter(*args, username_prefix='@'):
+    """ Create a vote identifier from vote object or arguments.
+    Examples:
+        ::
+            construct_authorpermvoter('username', 'permlink', 'voter')
+            construct_authorpermvoter({'author': 'username',
+                'permlink': 'permlink', 'voter': 'voter'})
+    """
+    if len(args) == 1:
+        op = args[0]
+        if "authorperm" in op:
+            authorperm, voter = op['authorperm'], op['voter']
+            [author, permlink] = resolve_authorperm(authorperm)
+        else:
+            author, permlink, voter = op['author'], op['permlink'], op['voter']
+    elif len(args) == 2:
+        authorperm, voter = args
+        [author, permlink] = resolve_authorperm(authorperm)
+    elif len(args) == 3:
+        author, permlink, voter = args
+    else:
+        raise ValueError(
+            'construct_identifier() received unparsable arguments')
+
+    fields = dict(prefix=username_prefix, author=author, permlink=permlink, voter=voter)
+    return "{prefix}{author}/{permlink}|{voter}".format(**fields)
+
+
 def test_proposal_in_buffer(buf, operation_name, id):
     from .transactionbuilder import ProposalBuilder
     from peerplaysbase.operationids import operations
diff --git a/steempy/vote.py b/steempy/vote.py
index 04b1194c..23fd89df 100644
--- a/steempy/vote.py
+++ b/steempy/vote.py
@@ -1,17 +1,15 @@
 from .instance import shared_steem_instance
 from .account import Account
-from .utils import resolve_authorperm
+from .exceptions import VoteDoesNotExistsException
+from .utils import resolve_authorperm, resolve_authorpermvoter, construct_authorpermvoter, construct_authorperm
+from .blockchainobject import BlockchainObject
+from .comment import Comment
+import json
 import logging
 log = logging.getLogger(__name__)
 
 
-class VoteObject(dict):
-    def __repr__(self):
-        return "<%s %s>" % (
-            self.__class__.__name__, str(self.voter))
-
-
-class Vote(VoteObject):
+class Vote(BlockchainObject):
     """ Read data about a Vote in the chain
 
         :param str authorperm: perm link to post/comment
@@ -20,21 +18,74 @@ class Vote(VoteObject):
     """
     type_id = 11
 
-    def refresh(self, authorperm=None, voter=None):
-        vote = self.identifier
-        if authorperm is not None:
-            vote["authorperm"] = authorperm
-        if voter is not None:
-            vote["voter"] = voter
-        super(Vote, self).__init__(vote)
+    def __init__(
+        self,
+        voter,
+        authorperm=None,
+        full=False,
+        lazy=False,
+        steem_instance=None
+    ):
+        self.full = full
+        if isinstance(voter, str) and authorperm is not None:
+            [author, permlink] = resolve_authorperm(authorperm)
+            self["voter"] = voter
+            self["author"] = author
+            self["permlink"] = permlink
+            authorpermvoter = construct_authorpermvoter(author, permlink, voter)
+            self["authorpermvoter"] = authorpermvoter
+        elif isinstance(voter, dict) and "author" in voter and "permlink" in voter and "voter" in voter:
+            self["author"] = voter["author"]
+            self["permlink"] = voter["permlink"]
+            authorpermvoter = construct_authorpermvoter(voter["author"], voter["permlink"], voter["voter"])
+            self["authorpermvoter"] = authorpermvoter
+        elif isinstance(voter, dict) and "authorperm" in voter and "voter" in voter:
+            [author, permlink] = resolve_authorperm(voter["authorperm"])
+            self["author"] = author
+            self["permlink"] = permlink
+            authorpermvoter = construct_authorpermvoter(voter["author"], voter["permlink"], voter["voter"])
+            self["authorpermvoter"] = authorpermvoter
+        elif isinstance(voter, dict) and "voter" in voter and authorperm is not None:
+            [author, permlink] = resolve_authorperm(authorperm)
+            self["author"] = author
+            self["permlink"] = permlink
+            authorpermvoter = construct_authorpermvoter(author, permlink, voter)
+            self["authorpermvoter"] = authorpermvoter
+        else:
+            authorpermvoter = voter
+        super().__init__(
+            authorpermvoter,
+            id_item="authorpermvoter",
+            lazy=lazy,
+            full=full,
+            steem_instance=steem_instance
+        )
+
+    def refresh(self):
+        [author, permlink, voter] = resolve_authorpermvoter(self.identifier)
+        votes = self.steem.rpc.get_active_votes(author, permlink)
+        vote = None
+        for x in votes:
+            if x["voter"] == voter:
+                vote = x
+        if not vote:
+            raise VoteDoesNotExistsException
+        super(Vote, self).__init__(vote, id_item="authorpermvoter", steem_instance=self.steem)
+
+        self.identifier = self.authorpermvoter
+
+    def json(self):
+        output = self
+        output.pop("authorpermvoter")
+        return json.loads(str(json.dumps(output)))
 
     @property
     def voter(self):
         return self["voter"]
 
     @property
-    def authorperm(self):
-        return self["authorperm"]
+    def authorpermvoter(self):
+        return self["authorpermvoter"]
 
     @property
     def weight(self):
@@ -58,15 +109,28 @@ class Vote(VoteObject):
 
 
 class ActiveVotes(list):
-    """ Obtain a list of pending proposals for a post
+    """ Obtain a list of votes for a post
 
         :param str authorperm: authorperm link
         :param steem steem_instance: Steem() instance to use when accesing a RPC
     """
     def __init__(self, authorperm, steem_instance=None):
         self.steem = steem_instance or shared_steem_instance()
-        [author, permlink] = resolve_authorperm(authorperm)
-        votes = self.steem.rpc.get_active_votes(author, permlink)
+        votes = None
+        if isinstance(authorperm, str):
+            [author, permlink] = resolve_authorperm(authorperm)
+            votes = self.steem.rpc.get_active_votes(author, permlink)
+        elif isinstance(authorperm, list):
+            votes = authorperm
+            authorperm = None
+        elif isinstance(authorperm, Comment):
+            votes = authorperm["active_votes"]
+            authorperm = authorperm["authorperm"]
+        elif isinstance(authorperm, dict):
+            votes = authorperm["active_votes"]
+            authorperm = authorperm["authorperm"]
+        if votes is None:
+            return
 
         super(ActiveVotes, self).__init__(
             [
@@ -77,7 +141,7 @@ class ActiveVotes(list):
 
 
 class AccountVotes(list):
-    """ Obtain a list of pending proposals for an account
+    """ Obtain a list of votes for an account
 
         :param str account: Account name
         :param steem steem_instance: Steem() instance to use when accesing a RPC
diff --git a/steempy/wallet.py b/steempy/wallet.py
index fb8c31ca..18cb71b0 100644
--- a/steempy/wallet.py
+++ b/steempy/wallet.py
@@ -178,6 +178,15 @@ class Wallet():
         self.masterpassword = self.masterpwd.decrypted_master
         self.masterpwd.saveEncrytpedMaster()
 
+    def purgeWallet(self):
+        """ Purge all data in wallet database
+        """
+        if self.MasterPassword:
+            self.MasterPassword.purge(self.MasterPassword)
+        for key in self.keyStorage.getPublicKeys():
+            self.keyStorage.delete(key)
+        self.configStorage.delete(self.MasterPassword.config_key)
+
     def encrypt_wif(self, wif):
         """ Encrypt a wif key
         """
diff --git a/steempy/witness.py b/steempy/witness.py
index 5501faed..41571338 100644
--- a/steempy/witness.py
+++ b/steempy/witness.py
@@ -2,34 +2,82 @@ from steempy.instance import shared_steem_instance
 from .account import Account
 from .exceptions import WitnessDoesNotExistsException
 from .blockchainobject import BlockchainObject
+from .utils import formatTimeString, parse_time
+from datetime import datetime, timedelta
 
 
 class Witness(BlockchainObject):
     """ Read data about a witness in the chain
 
         :param str account_name: Name of the witness
-        :param steem steem_instance: BitShares() instance to use when
+        :param steem steem_instance: Steem() instance to use when
                accesing a RPC
 
     """
-    type_ids = [6, 2]
+    type_id = 3
 
-    def refresh(self):
+    def __init__(
+        self,
+        owner,
+        id_item="owner",
+        full=False,
+        lazy=False,
+        steem_instance=None
+    ):
+        self.full = full
+        super().__init__(
+            owner,
+            lazy=lazy,
+            full=full,
+            id_item="owner",
+            steem_instance=steem_instance
+        )
 
+    def refresh(self):
         witness = self.steem.rpc.get_witness_by_account(self.identifier)
         if not witness:
             raise WitnessDoesNotExistsException
-        super(Witness, self).__init__(witness, steem_instance=self.steem)
+        super(Witness, self).__init__(witness, id_item="owner", steem_instance=self.steem)
+        self.identifier = self["owner"]
 
     @property
     def account(self):
         return Account(self["owner"], steem_instance=self.steem)
 
 
-class Witnesses(list):
+class WitnessesObject(list):
+    def printAsTable(self, sort_key="votes", reverse=True):
+        if sort_key == 'base':
+            sortedList = sorted(self, key=lambda self: self['sbd_exchange_rate']['base'], reverse=reverse)
+        elif sort_key == 'quote':
+            sortedList = sorted(self, key=lambda self: self['sbd_exchange_rate']['quote'], reverse=reverse)
+        elif sort_key == 'last_sbd_exchange_update':
+            sortedList = sorted(self, key=lambda self: (datetime.now() - formatTimeString(self['last_sbd_exchange_update'])).total_seconds(), reverse=reverse)
+        elif sort_key == 'account_creation_fee':
+            sortedList = sorted(self, key=lambda self: self['props']['account_creation_fee'], reverse=reverse)
+        elif sort_key == 'sbd_interest_rate':
+            sortedList = sorted(self, key=lambda self: self['props']['sbd_interest_rate'], reverse=reverse)
+        elif sort_key == 'maximum_block_size':
+            sortedList = sorted(self, key=lambda self: self['props']['maximum_block_size'], reverse=reverse)
+        elif sort_key == 'votes':
+            sortedList = sorted(self, key=lambda self: int(self[sort_key]), reverse=reverse)
+        else:
+            sortedList = sorted(self, key=lambda self: self[sort_key], reverse=reverse)
+        for witness in sortedList:
+            outstr = ''
+            outstr += witness['owner'][:15].ljust(15) + " \t " + str(round(int(witness['votes']) / 1e15, 2)).ljust(5) + " PV - " + str(witness['total_missed']).ljust(5)
+            outstr += " missed - feed:" + witness['sbd_exchange_rate']['base'] + "/" + witness['sbd_exchange_rate']['quote']
+            td = datetime.now() - formatTimeString(witness['last_sbd_exchange_update'])
+            outstr += " " + str(td.days) + " days " + str(td.seconds // 3600) + ":" + str((td.seconds // 60) % 60) + " \t "
+            outstr += str(witness['props']['account_creation_fee']) + " " + str(witness['props']['maximum_block_size']) + " Blocks "
+            outstr += str(witness['props']['sbd_interest_rate']) + " \t " + witness['running_version']
+            print(outstr)
+
+
+class Witnesses(WitnessesObject):
     """ Obtain a list of **active** witnesses and the current schedule
 
-        :param steem steem_instance: BitShares() instance to use when
+        :param steem steem_instance: Steem() instance to use when
             accesing a RPC
     """
     def __init__(self, steem_instance=None):
@@ -44,3 +92,102 @@ class Witnesses(list):
                 for x in self.active_witnessess
             ]
         )
+
+
+class WitnessesVotedByAccount(WitnessesObject):
+    """ Obtain a list of witnesses which have been voted by an account
+
+        :param str account: Account name
+        :param steem steem_instance: Steem() instance to use when
+            accesing a RPC
+    """
+    def __init__(self, account, steem_instance=None):
+        self.steem = steem_instance or shared_steem_instance()
+        self.account = Account(account, full=True)
+        if "witness_votes" not in self.account:
+            return
+        witnessess = self.account["witness_votes"]
+
+        super(WitnessesVotedByAccount, self).__init__(
+            [
+                Witness(x, lazy=True, steem_instance=self.steem)
+                for x in witnessess
+            ]
+        )
+
+
+class WitnessesRankedByVote(WitnessesObject):
+    """ Obtain a list of witnesses ranked by Vote
+
+        :param str from_account: Witness name
+        :param steem steem_instance: Steem() instance to use when
+            accesing a RPC
+    """
+    def __init__(self, from_account="", limit=100, steem_instance=None):
+        self.steem = steem_instance or shared_steem_instance()
+        witnessList = []
+        last_limit = limit
+        last_account = from_account
+        if limit > 100:
+            while last_limit > 100:
+                tmpList = WitnessesRankedByVote(last_account, 100)
+                if (last_limit < limit):
+                    witnessList.extend(tmpList[1:])
+                    last_limit -= 99
+                else:
+                    witnessList.extend(tmpList)
+                    last_limit -= 100
+                last_account = witnessList[-1]["owner"]
+        if (last_limit < limit):
+            last_limit += 1
+
+        witnessess = self.steem.rpc.get_witnesses_by_vote(last_account, last_limit)
+        # self.witness_count = len(self.voted_witnessess)
+        if (last_limit < limit):
+            witnessess = witnessess[1:]
+        if len(witnessess) > 0:
+            for x in witnessess:
+                witnessList.append(Witness(x, lazy=True, steem_instance=self.steem))
+        if len(witnessList) == 0:
+            return
+        super(WitnessesRankedByVote, self).__init__(witnessList)
+
+
+class WitnessesByIds(WitnessesObject):
+    """ Obtain a list of witnesses which have been voted by an account
+
+        :param list witness_ids: list of witness_ids
+        :param steem steem_instance: Steem() instance to use when
+            accesing a RPC
+    """
+    def __init__(self, witness_ids, steem_instance=None):
+        self.steem = steem_instance or shared_steem_instance()
+        witnessess = self.steem.rpc.get_witnesses(witness_ids)
+        if len(witnessess) == 0:
+            return
+        super(WitnessesByIds, self).__init__(
+            [
+                Witness(x, lazy=True, steem_instance=self.steem)
+                for x in witnessess
+            ]
+        )
+
+
+class LookupWitnesses(WitnessesObject):
+    """ Obtain a list of witnesses which have been voted by an account
+
+        :param str from_account: Account name
+        :param steem steem_instance: Steem() instance to use when
+            accesing a RPC
+    """
+    def __init__(self, from_account, limit, steem_instance=None):
+        self.steem = steem_instance or shared_steem_instance()
+        witnessess = self.steem.rpc.lookup_witness_accounts(from_account, limit)
+        if len(witnessess) == 0:
+            return
+        super(LookupWitnesses, self).__init__(
+            [
+                Witness(x, lazy=True, steem_instance=self.steem)
+                for x in witnessess
+            ]
+        )
diff --git a/steempyapi/exceptions.py b/steempyapi/exceptions.py
index 91540719..61fda423 100644
--- a/steempyapi/exceptions.py
+++ b/steempyapi/exceptions.py
@@ -32,5 +32,9 @@ class UnhandledRPCError(RPCError):
     pass
 
 
+class NoAccessApi(RPCError):
+    pass
+
+
 class NumRetriesReached(Exception):
     pass
diff --git a/steempyapi/steemnoderpc.py b/steempyapi/steemnoderpc.py
index b7a25606..79bc81a6 100644
--- a/steempyapi/steemnoderpc.py
+++ b/steempyapi/steemnoderpc.py
@@ -21,14 +21,20 @@ class SteemNodeRPC(GrapheneWebsocketRPC):
 
     def __init__(self, *args, **kwargs):
         super(SteemNodeRPC, self).__init__(*args, **kwargs)
+        self.apis = kwargs.pop(
+            "apis",
+            ["database", "network_broadcast"]
+        )
         self.chain_params = self.get_network()
 
-    def register_apis(self):
-        return
-        # self.api_id["database"] = self.database(api_id=1)
-        # self.api_id["market_history"] = self.market_history(api_id=1)
-        # self.api_id["network_broadcast"] = self.network_broadcast(api_id=1)
-        # self.api_id["follow"] = self.follow(api_id=1)
+    def register_apis(self, apis=None):
+        if apis is None:
+            return
+        for api in (apis):
+            api = api.replace("_api", "")
+            self.api_id[api] = self.get_api_by_name("%s_api" % api, api_id=1)
+            if not self.api_id[api] and not isinstance(self.api_id[api], int):
+                raise exceptions.NoAccessApi("No permission to access %s API. " % api)
 
     def rpcexec(self, payload):
         """ Execute a call by sending the payload.
diff --git a/tests/test_account.py b/tests/test_account.py
index 3d41be8e..71b9397b 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -9,8 +9,9 @@ from steempy.instance import set_shared_steem_instance
 from steempybase.operationids import getOperationNameForId
 
 wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -19,7 +20,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
             # We want to bundle many operations into a single transaction
             bundle=True,
diff --git a/tests/test_amount.py b/tests/test_amount.py
index 9b131c0c..be447fdc 100644
--- a/tests/test_amount.py
+++ b/tests/test_amount.py
@@ -3,8 +3,9 @@ from steempy import Steem
 from steempy.amount import Amount
 from steempy.asset import Asset
 from steempy.instance import set_shared_steem_instance, SharedInstance
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -13,7 +14,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
         )
         set_shared_steem_instance(self.bts)
diff --git a/tests/test_asset.py b/tests/test_asset.py
index 8690b024..8bc3b916 100644
--- a/tests/test_asset.py
+++ b/tests/test_asset.py
@@ -3,8 +3,9 @@ from steempy import Steem
 from steempy.asset import Asset
 from steempy.instance import set_shared_steem_instance
 from steempy.exceptions import AssetDoesNotExistsException
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -13,7 +14,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
         )
         set_shared_steem_instance(self.bts)
diff --git a/tests/test_base_objects.py b/tests/test_base_objects.py
index bbadf391..67871657 100644
--- a/tests/test_base_objects.py
+++ b/tests/test_base_objects.py
@@ -3,7 +3,9 @@ from steempy import Steem, exceptions
 from steempy.instance import set_shared_steem_instance
 from steempy.account import Account
 from steempy.witness import Witness
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -12,7 +14,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
         )
         set_shared_steem_instance(self.bts)
diff --git a/tests/test_bip38.py b/tests/test_bip38.py
new file mode 100644
index 00000000..ccd39c67
--- /dev/null
+++ b/tests/test_bip38.py
@@ -0,0 +1,22 @@
+import unittest
+
+from steempybase.account import PrivateKey
+from steempybase.bip38 import encrypt, decrypt
+
+
+class Testcases(unittest.TestCase):
+    def test_encrypt(self):
+        self.assertEqual([format(encrypt(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd"), "TestingOneTwoThree"), "encwif"),
+                          format(encrypt(PrivateKey("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"), "TestingOneTwoThree"), "encwif"),
+                          format(encrypt(PrivateKey("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"), "Satoshi"), "encwif")],
+                         ["6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi",
+                          "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg",
+                          "6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq"])
+
+    def test_deencrypt(self):
+        self.assertEqual([format(decrypt("6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi", "TestingOneTwoThree"), "wif"),
+                          format(decrypt("6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "TestingOneTwoThree"), "wif"),
+                          format(decrypt("6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq", "Satoshi"), "wif")],
+                         ["5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd",
+                          "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR",
+                          "5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"])
diff --git a/tests/test_message.py b/tests/test_message.py
index 40e815b9..5e577c72 100644
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -7,8 +7,9 @@ from steempy.instance import set_shared_steem_instance
 
 wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
 core_unit = "STM"
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -17,7 +18,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
             wif=[wif]
         )
diff --git a/tests/test_objectcache.py b/tests/test_objectcache.py
index 55031210..a2254987 100644
--- a/tests/test_objectcache.py
+++ b/tests/test_objectcache.py
@@ -3,8 +3,9 @@ import unittest
 from steempy import Steem, exceptions
 from steempy.instance import set_shared_steem_instance
 from steempy.blockchainobject import ObjectCache
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -13,7 +14,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
         )
         set_shared_steem_instance(self.bts)
diff --git a/tests/test_price.py b/tests/test_price.py
index 59996318..0e57d8f1 100644
--- a/tests/test_price.py
+++ b/tests/test_price.py
@@ -4,8 +4,9 @@ from steempy.amount import Amount
 from steempy.price import Price
 from steempy.asset import Asset
 import unittest
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -13,7 +14,7 @@ class Testcases(unittest.TestCase):
     def __init__(self, *args, **kwargs):
         super(Testcases, self).__init__(*args, **kwargs)
         steem = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
         )
         set_shared_steem_instance(steem)
diff --git a/tests/test_proposals.py b/tests/test_proposals.py
index a3e0caeb..c063b5e2 100644
--- a/tests/test_proposals.py
+++ b/tests/test_proposals.py
@@ -5,8 +5,9 @@ from steempybase.operationids import getOperationNameForId
 from steempy.instance import set_shared_steem_instance
 
 wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -15,7 +16,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
             keys={"active": wif},
         )
diff --git a/tests/test_steem.py b/tests/test_steem.py
index c71c45ae..0efd5ca5 100644
--- a/tests/test_steem.py
+++ b/tests/test_steem.py
@@ -11,8 +11,9 @@ from steempy.instance import set_shared_steem_instance
 
 wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
 core_unit = "STM"
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -21,7 +22,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
             keys={"active": wif, "owner": wif, "memo": wif},
         )
diff --git a/tests/test_transactions.py b/tests/test_transactions.py
index 64107ae2..ac5708ca 100644
--- a/tests/test_transactions.py
+++ b/tests/test_transactions.py
@@ -24,8 +24,6 @@ wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
 ref_block_num = 34294
 ref_block_prefix = 3707022213
 expiration = "2016-04-06T08:29:27"
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
 
 
 class Testcases(unittest.TestCase):
diff --git a/tests/test_txbuffers.py b/tests/test_txbuffers.py
index 2b264e1c..f92b2465 100644
--- a/tests/test_txbuffers.py
+++ b/tests/test_txbuffers.py
@@ -4,6 +4,9 @@ from steempybase import operations
 from steempy.instance import set_shared_steem_instance
 
 wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -12,7 +15,7 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.bts = Steem(
-            "wss://testnet.steem.vc",
+            nodes,
             nobroadcast=True,
             keys={"active": wif}
         )
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a49f2daa..9137b12d 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,12 +1,46 @@
-from steempy.utils import assets_from_string
-from steempy.utils import resolve_authorperm
+import unittest
+from steempy.utils import (
+    assets_from_string,
+    resolve_authorperm,
+    resolve_authorpermvoter,
+    construct_authorperm,
+    construct_authorpermvoter,
+    sanitize_permlink,
+    derive_permlink
+)
 
 
-def test_assets_from_string():
-    assert assets_from_string('USD:BTS') == ['USD', 'BTS']
-    assert assets_from_string('BTSBOTS.S1:BTS') == ['BTSBOTS.S1', 'BTS']
+class Testcases(unittest.TestCase):
+    def test_constructAuthorperm(self):
+        self.assertEqual(construct_authorperm("A", "B"), "@A/B")
+        self.assertEqual(construct_authorperm({'author': "A", 'permlink': "B"}), "@A/B")
 
+    def test_constructAuthorpermvoter(self):
+        self.assertEqual(construct_authorpermvoter("A", "B", "C"), "@A/B|C")
+        self.assertEqual(construct_authorpermvoter({'author': "A", 'permlink': "B", 'voter': 'C'}), "@A/B|C")
+        self.assertEqual(construct_authorpermvoter({'authorperm': "A/B", 'voter': 'C'}), "@A/B|C")
 
-def test_authorperm_resolve():
-    assert resolve_authorperm('theaussiegame/cryptokittie-giveaway-number-2') == ('theaussiegame', 'cryptokittie-giveaway-number-2')
-    assert resolve_authorperm('holger80/virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt') == ('holger80', 'virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt')
+    def test_assets_from_string(self):
+        self.assertEqual(assets_from_string('USD:BTS'), ['USD', 'BTS'])
+        self.assertEqual(assets_from_string('BTSBOTS.S1:BTS'), ['BTSBOTS.S1', 'BTS'])
+
+    def test_authorperm_resolve(self):
+        self.assertEqual(resolve_authorperm('theaussiegame/cryptokittie-giveaway-number-2'),
+                         ('theaussiegame', 'cryptokittie-giveaway-number-2'))
+        self.assertEqual(resolve_authorperm('holger80/virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt'),
+                         ('holger80', 'virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt'))
+
+    def test_authorpermvoter_resolve(self):
+        self.assertEqual(resolve_authorpermvoter('theaussiegame/cryptokittie-giveaway-number-2|test'),
+                         ('theaussiegame', 'cryptokittie-giveaway-number-2', 'test'))
+        self.assertEqual(resolve_authorpermvoter('holger80/virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt|holger80'),
+                         ('holger80', 'virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt', 'holger80'))
+
+    def test_sanitizePermlink(self):
+        self.assertEqual(sanitize_permlink("aAf_0.12"), "aaf-0-12")
+        self.assertEqual(sanitize_permlink("[](){}|"), "")
+
+    def test_derivePermlink(self):
+        self.assertEqual(derive_permlink("Hello World"), "hello-world")
+        self.assertEqual(derive_permlink("aAf_0.12"), "aaf-0-12")
+        self.assertEqual(derive_permlink("[](){}"), "")
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index fe2baf24..781f59a1 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -5,12 +5,14 @@ from steempy import Steem
 from steempy.account import Account
 from steempy.amount import Amount
 from steempy.asset import Asset
+from steempy.wallet import Wallet
 from steempy.instance import set_shared_steem_instance
 from steempybase.operationids import getOperationNameForId
 
 wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
-# steem_node = "wss://gtg.steem.house:8090"
-steem_node = "wss://steemd.pevo.science"
+nodes = ["wss://steemd.pevo.science", "wss://gtg.steem.house:8090", "wss://rpc.steemliberator.com", "wss://rpc.buildteam.io",
+         "wss://rpc.steemviz.com", "wss://seed.bitcoiner.me", "wss://node.steem.ws", "wss://steemd.steemgigs.org", "wss://steemd.steemit.com",
+         "wss://steemd.minnowsupportproject.org"]
 
 
 class Testcases(unittest.TestCase):
@@ -19,12 +21,39 @@ class Testcases(unittest.TestCase):
         super().__init__(*args, **kwargs)
 
         self.stm = Steem(
-            steem_node,
+            nodes,
             nobroadcast=True,
             # We want to bundle many operations into a single transaction
             bundle=True,
             # Overwrite wallet to use this list of wifs only
-            wif=[wif]
         )
         self.stm.set_default_account("test")
         set_shared_steem_instance(self.stm)
+        # self.stm.newWallet("TestingOneTwoThree")
+        self.wallet = Wallet(rpc=self.stm.rpc)
+        self.wallet.purgeWallet()
+
+        self.wallet.newWallet(pwd="TestingOneTwoThree")
+
+        self.wallet.unlock(pwd="TestingOneTwoThree")
+        self.wallet.addPrivateKey(wif)
+
+    def test_encrypt(self):
+        self.wallet.masterpassword = "TestingOneTwoThree"
+        self.assertEqual([self.wallet.encrypt_wif("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd"),
+                          self.wallet.encrypt_wif("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR")],
+                         ["6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi",
+                          "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg"])
+        self.wallet.masterpassword = "Satoshi"
+        self.assertEqual([self.wallet.encrypt_wif("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5")],
+                         ["6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq"])
+
+    def test_deencrypt(self):
+        self.wallet.masterpassword = "TestingOneTwoThree"
+        self.assertEqual([self.wallet.decrypt_wif("6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi"),
+                          self.wallet.decrypt_wif("6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg")],
+                         ["5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd",
+                          "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"])
+        self.wallet.masterpassword = "Satoshi"
+        self.assertEqual([self.wallet.decrypt_wif("6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq")],
+                         ["5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"])
-- 
GitLab