From 516be5a7b8421ce1ea5daf1c3e5263b9b2d9c7e3 Mon Sep 17 00:00:00 2001
From: Holger Nahrstaedt <holgernahrstaedt@gmx.de>
Date: Sun, 7 Jun 2020 16:22:13 +0200
Subject: [PATCH] Add Prefix class

* add Prefix class for PasswordKey, Brainkey, Address, PublicKey, PrivateKey, Base58
* New Class BitcoinAddress
* Address class has now from_pubkey class method
---
 beemgraphenebase/account.py           | 332 +++++++++++++++++---------
 beemgraphenebase/base58.py            |  37 +--
 beemgraphenebase/bip38.py             |   4 +-
 beemgraphenebase/ecdsasig.py          |  19 +-
 beemgraphenebase/prefix.py            |  11 +
 beemgraphenebase/types.py             |  49 +++-
 tests/beemgraphene/test_account.py    |  16 +-
 tests/beemgraphene/test_key_format.py |  16 +-
 tests/beemgraphene/test_types.py      |  60 ++++-
 9 files changed, 368 insertions(+), 176 deletions(-)
 create mode 100644 beemgraphenebase/prefix.py

diff --git a/beemgraphenebase/account.py b/beemgraphenebase/account.py
index 23f8b47d..03b1df17 100644
--- a/beemgraphenebase/account.py
+++ b/beemgraphenebase/account.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 from __future__ import absolute_import
 from __future__ import division
 from __future__ import print_function
@@ -21,11 +22,12 @@ import itertools
 from binascii import hexlify, unhexlify
 import unicodedata
 
-from .base58 import ripemd160, Base58
+from .base58 import ripemd160, Base58, doublesha256
 from .bip32 import BIP32Key, parse_path
 from .dictionary import words as BrainKeyDictionary
 from .dictionary import words_bip39 as MnemonicDictionary
 from .py23 import py23_bytes, PY2
+from .prefix import Prefix
 
 
 PBKDF2_ROUNDS = 2048
@@ -37,18 +39,18 @@ def binary_search(a, x, lo=0, hi=None):  # can't use a to specify default for hi
     return pos if pos != hi and a[pos] == x else -1  # don't walk off the end
 
 
-class PasswordKey(object):
+class PasswordKey(Prefix):
     """ This class derives a private key given the account name, the
         role and a password. It leverages the technology of Brainkeys
         and allows people to have a secure private key by providing a
         passphrase only.
     """
 
-    def __init__(self, account, password, role="active", prefix="STM"):
+    def __init__(self, account, password, role="active", prefix=None):
+        self.set_prefix(prefix)
         self.account = account
         self.role = role
         self.password = password
-        self.prefix = prefix
 
     def normalize(self, seed):
         """ Correct formating with single whitespace syntax and no trailing space """
@@ -79,7 +81,7 @@ class PasswordKey(object):
 
 
 @python_2_unicode_compatible
-class BrainKey(object):
+class BrainKey(Prefix):
     """Brainkey implementation similar to the graphene-ui web-wallet.
 
         :param str brainkey: Brain Key
@@ -99,7 +101,8 @@ class BrainKey(object):
 
     """
 
-    def __init__(self, brainkey=None, sequence=0):
+    def __init__(self, brainkey=None, sequence=0, prefix=None):
+        self.set_prefix(prefix)
         if not brainkey:
             self.brainkey = self.suggest()
         else:
@@ -132,13 +135,13 @@ class BrainKey(object):
         encoded = "%s %d" % (self.brainkey, self.sequence)
         a = py23_bytes(encoded, 'ascii')
         s = hashlib.sha256(hashlib.sha512(a).digest()).digest()
-        return PrivateKey(hexlify(s).decode('ascii'))
+        return PrivateKey(hexlify(s).decode('ascii'), prefix=self.prefix)
 
     def get_blind_private(self):
         """ Derive private key from the brain key (and no sequence number)
         """
         a = py23_bytes(self.brainkey, 'ascii')
-        return PrivateKey(hashlib.sha256(a).hexdigest())
+        return PrivateKey(hashlib.sha256(a).hexdigest(), prefix=self.prefix)
 
     def get_public(self):
         return self.get_private().pubkey
@@ -349,11 +352,12 @@ class Mnemonic(object):
 
 
 
-class MnemonicKey(object):
+class MnemonicKey(Prefix):
     """ This class derives a private key from a BIP39 mnemoric implementation
     """
 
-    def __init__(self, word_list=None, passphrase="", account_sequence=0, key_sequence=0, prefix="STM"):
+    def __init__(self, word_list=None, passphrase="", account_sequence=0, key_sequence=0, prefix=None):
+        self.set_prefix(prefix)
         if word_list is not None:
             self.set_mnemonic(word_list, passphrase=passphrase)
         else:
@@ -453,13 +457,12 @@ class MnemonicKey(object):
 
 
 @python_2_unicode_compatible
-class Address(object):
+class Address(Prefix):
     """ Address class
 
         This class serves as an address representation for Public Keys.
 
         :param str address: Base58 encoded address (defaults to ``None``)
-        :param str pubkey: Base58 encoded pubkey (defaults to ``None``)
         :param str prefix: Network prefix (defaults to ``STM``)
 
         Example::
@@ -467,92 +470,99 @@ class Address(object):
            Address("STMFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi")
 
     """
-    def __init__(self, address=None, pubkey=None, prefix="STM"):
-        self.prefix = prefix
-        if pubkey is not None:
-            self._pubkey = Base58(pubkey, prefix=prefix)
-            self._address = None
-        elif address is not None:
-            self._pubkey = None
-            self._address = Base58(address, prefix=prefix)
-        else:
-            raise Exception("Address has to be initialized by either the " +
-                            "pubkey or the address.")
+    def __init__(self, address, prefix=None):
+        self.set_prefix(prefix)
+        self._address = Base58(address, prefix=self.prefix)
 
-    def get_public_key(self):
-        """Returns the pubkey"""
-        return self._pubkey
+    @classmethod
+    def from_pubkey(cls, pubkey, compressed=True, version=56, prefix=None):
+        """ Load an address provided by the public key.
+            Version: 56 => PTS
+        """
+        # Ensure this is a public key
+        pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
+        if compressed:
+            pubkey_plain = pubkey.compressed()
+        else:
+            pubkey_plain = pubkey.uncompressed()
+        sha = hashlib.sha256(unhexlify(pubkey_plain)).hexdigest()
+        rep = hexlify(ripemd160(sha)).decode("ascii")
+        s = ("%.2x" % version) + rep
+        result = s + hexlify(doublesha256(s)[:4]).decode("ascii")
+        result = hexlify(ripemd160(result)).decode("ascii")
+        return cls(result, prefix=pubkey.prefix)
 
-    def derivesha256address(self):
+    @classmethod
+    def derivesha256address(cls, pubkey, compressed=True, prefix=None):
         """ Derive address using ``RIPEMD160(SHA256(x))`` """
-        pubkey = self.get_public_key()
-        if pubkey is None:
-            return None
-        pkbin = unhexlify(repr(pubkey))
-        addressbin = ripemd160(hexlify(hashlib.sha256(pkbin).digest()))
-        return Base58(hexlify(addressbin).decode('ascii'))
-
-    def derive256address_with_version(self, version=56):
-        """ Derive address using ``RIPEMD160(SHA256(x))``
-            and adding version + checksum
-        """
-        pubkey = self.get_public_key()
-        if pubkey is None:
-            return None
-        pkbin = unhexlify(repr(pubkey))
-        addressbin = ripemd160(hexlify(hashlib.sha256(pkbin).digest()))
-        addr = py23_bytes(bytearray(ctypes.c_uint8(version & 0xFF))) + addressbin
-        check = hashlib.sha256(addr).digest()
-        check = hashlib.sha256(check).digest()
-        buffer = addr + check[0:4]
-        return Base58(hexlify(ripemd160(hexlify(buffer))).decode('ascii'))
-
-    def derivesha512address(self):
+        pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
+        if compressed:
+            pubkey_plain = pubkey.compressed()
+        else:
+            pubkey_plain = pubkey.uncompressed()
+        pkbin = unhexlify(repr(pubkey_plain))
+        result = hexlify(hashlib.sha256(pkbin).digest())
+        result = hexlify(ripemd160(result)).decode("ascii")
+        return cls(result, prefix=pubkey.prefix)
+
+    @classmethod
+    def derivesha512address(cls, pubkey, compressed=True, prefix=None):
         """ Derive address using ``RIPEMD160(SHA512(x))`` """
-        pubkey = self.get_public_key()
-        if pubkey is None:
-            return None
-        pkbin = unhexlify(repr(pubkey))
-        addressbin = ripemd160(hexlify(hashlib.sha512(pkbin).digest()))
-        return Base58(hexlify(addressbin).decode('ascii'))
+        pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
+        if compressed:
+            pubkey_plain = pubkey.compressed()
+        else:
+            pubkey_plain = pubkey.uncompressed()
+        pkbin = unhexlify(repr(pubkey_plain))
+        result = hexlify(hashlib.sha512(pkbin).digest())
+        result = hexlify(ripemd160(result)).decode("ascii")
+        return cls(result, prefix=pubkey.prefix)    
 
     def __repr__(self):
         """ Gives the hex representation of the ``GrapheneBase58CheckEncoded``
             Graphene address.
         """
-        if self._address is None:
-            return repr(self.derivesha512address())
-        else:
-            return repr(self._address)
+        return repr(self._address)
 
     def __str__(self):
         """ Returns the readable Graphene address. This call is equivalent to
             ``format(Address, "STM")``
         """
-        return format(self, self.prefix)
+        return format(self._address, self.prefix)
 
     def __format__(self, _format):
         """  May be issued to get valid "MUSE", "PLAY" or any other Graphene compatible
             address with corresponding prefix.
         """
-        if self._address is None:
-            if _format.lower() == "btc":
-                return format(self.derivesha256address(), _format)
-            else:
-                return format(self.derivesha512address(), _format)
-        else:
-            return format(self._address, _format)
+        return format(self._address, _format)
 
     def __bytes__(self):
         """ Returns the raw content of the ``Base58CheckEncoded`` address """
-        if self._address is None:
-            return py23_bytes(self.derivesha512address())
+        return py23_bytes(self._address)
+
+
+@python_2_unicode_compatible
+class GrapheneAddress(Address):
+    """ Graphene Addresses are different. Hence we have a different class
+    """
+
+    @classmethod
+    def from_pubkey(cls, pubkey, compressed=True, version=56, prefix=None):
+        # Ensure this is a public key
+        pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
+        if compressed:
+            pubkey_plain = pubkey.compressed()
         else:
-            return py23_bytes(self._address)
+            pubkey_plain = pubkey.uncompressed()
+
+        """ Derive address using ``RIPEMD160(SHA512(x))`` """
+        addressbin = ripemd160(hashlib.sha512(unhexlify(pubkey_plain)).hexdigest())
+        result = Base58(hexlify(addressbin).decode("ascii"))
+        return cls(result, prefix=pubkey.prefix)
 
 
 @python_2_unicode_compatible
-class PublicKey(Address):
+class PublicKey(Prefix):
     """ This class deals with Public Keys and inherits ``Address``.
 
         :param str pk: Base58 encoded public key
@@ -569,20 +579,39 @@ class PublicKey(Address):
                       PublicKey("xxxxx").unCompressed()
 
     """
-    def __init__(self, pk, prefix="STM"):
+    def __init__(self, pk, prefix=None):
         """Init PublicKey
             :param str pk: Base58 encoded public key
             :param str prefix: Network prefix (defaults to ``STM``)
         """
-        self.prefix = prefix
-        self._pk = Base58(pk, prefix=prefix)
-        self.address = Address(pubkey=pk, prefix=prefix)
-        self.pubkey = self._pk
+        self.set_prefix(prefix)
+        if isinstance(pk, PublicKey):
+            pk = format(pk, self.prefix)
+            
+        if str(pk).startswith("04"):
+            # We only ever deal with compressed keys, so let's make it
+            # compressed
+            order = ecdsa.SECP256k1.order
+            p = ecdsa.VerifyingKey.from_string(
+                unhexlify(pk[2:]), curve=ecdsa.SECP256k1
+            ).pubkey.point
+            x_str = ecdsa.util.number_to_string(p.x(), order)
+            pk = hexlify(chr(2 + (p.y() & 1)).encode("ascii") + x_str).decode("ascii")
+
+        self._pk = Base58(pk, prefix=self.prefix)
+
+    @property
+    def pubkey(self):
+        return self._pk
 
     def get_public_key(self):
         """Returns the pubkey"""
         return self.pubkey
 
+    @property
+    def compressed_key(self):
+        return PublicKey(self.compressed())
+
     def _derive_y_from_x(self, x, is_even):
         """ Derive y point from x point """
         curve = ecdsa.SECP256k1.curve
@@ -597,14 +626,9 @@ class PublicKey(Address):
 
     def compressed(self):
         """ Derive compressed public key """
-        order = ecdsa.SECP256k1.generator.order()
-        p = ecdsa.VerifyingKey.from_string(py23_bytes(self), curve=ecdsa.SECP256k1).pubkey.point
-        x_str = ecdsa.util.number_to_string(p.x(), order)
-        # y_str = ecdsa.util.number_to_string(p.y(), order)
-        compressed = hexlify(py23_bytes(chr(2 + (p.y() & 1)), 'ascii') + x_str).decode('ascii')
-        return(compressed)
+        return repr(self._pk)
 
-    def unCompressed(self):
+    def uncompressed(self):
         """ Derive uncompressed key """
         public_key = repr(self._pk)
         prefix = public_key[0:2]
@@ -619,9 +643,41 @@ class PublicKey(Address):
 
     def point(self):
         """ Return the point for the public key """
-        string = unhexlify(self.unCompressed())
+        string = unhexlify(self.uncompressed())
         return ecdsa.VerifyingKey.from_string(string[1:], curve=ecdsa.SECP256k1).pubkey.point
 
+    def child(self, offset256):
+        """ Derive new public key from this key and a sha256 "offset" """
+        a = bytes(self) + offset256
+        s = hashlib.sha256(a).digest()
+        return self.add(s)
+
+    def add(self, digest256):
+        """ Derive new public key from this key and a sha256 "digest" """
+        from .ecdsa import tweakaddPubkey
+
+        return tweakaddPubkey(self, digest256)
+
+    @classmethod
+    def from_privkey(cls, privkey, prefix=None):
+        """ Derive uncompressed public key """
+        privkey = PrivateKey(privkey, prefix=prefix or Prefix.prefix)
+        secret = unhexlify(repr(privkey))
+        order = ecdsa.SigningKey.from_string(
+            secret, curve=ecdsa.SECP256k1
+        ).curve.generator.order()
+        p = ecdsa.SigningKey.from_string(
+            secret, curve=ecdsa.SECP256k1
+        ).verifying_key.pubkey.point
+        x_str = ecdsa.util.number_to_string(p.x(), order)
+        # y_str = ecdsa.util.number_to_string(p.y(), order)
+        compressed = hexlify(chr(2 + (p.y() & 1)).encode("ascii") + x_str).decode(
+            "ascii"
+        )
+        # uncompressed = hexlify(
+        #    chr(4).encode('ascii') + x_str + y_str).decode('ascii')
+        return cls(compressed, prefix=prefix or Prefix.prefix)
+
     def __repr__(self):
         """ Gives the hex representation of the Graphene public key. """
         return repr(self._pk)
@@ -640,9 +696,26 @@ class PublicKey(Address):
         """ Returns the raw public key (has length 33)"""
         return py23_bytes(self._pk)
 
+    def __lt__(self, other):
+        """ For sorting of public keys (due to graphene),
+            we actually sort according to addresses
+        """
+        assert isinstance(other, PublicKey)
+        return repr(self.address) < repr(other.address)
+
+    def unCompressed(self):
+        """ Alias for self.uncompressed() - LEGACY"""
+        return self.uncompressed()
+
+    @property
+    def address(self):
+        """ Obtain a GrapheneAddress from a public key
+        """
+        return GrapheneAddress.from_pubkey(repr(self), prefix=self.prefix)
+
 
 @python_2_unicode_compatible
-class PrivateKey(PublicKey):
+class PrivateKey(Prefix):
     """ Derives the compressed and uncompressed public keys and
         constructs two instances of :class:`PublicKey`:
 
@@ -665,41 +738,48 @@ class PrivateKey(PublicKey):
             Instance of :class:`Address` using uncompressed key.
 
     """
-    def __init__(self, wif=None, prefix="STM"):
+    def __init__(self, wif=None, prefix=None):
+        self.set_prefix(prefix)
         if wif is None:
-            self._wif = Base58(hexlify(os.urandom(32)).decode('ascii'), prefix=prefix)
+            import os
+            self._wif = Base58(hexlify(os.urandom(32)).decode('ascii'))
+        elif isinstance(wif, PrivateKey):
+            self._wif = wif._wif    
         elif isinstance(wif, Base58):
             self._wif = wif
         else:
-            self._wif = Base58(wif, prefix=prefix)
-        # compress pubkeys only
-        self._pubkeyhex, self._pubkeyuncompressedhex = self.compressedpubkey()
-        self.pubkey = PublicKey(self._pubkeyhex, prefix=prefix)
-        self.uncompressed = PublicKey(self._pubkeyuncompressedhex, prefix=prefix)
-        self.uncompressed.address = Address(pubkey=self._pubkeyuncompressedhex, prefix=prefix)
-        self.address = Address(pubkey=self._pubkeyhex, prefix=prefix)
+            self._wif = Base58(wif)
+
+        assert len(repr(self._wif)) == 64
+
+    @property
+    def bitcoin(self):
+        return BitcoinPublicKey.from_privkey(self)
+
+    @property
+    def address(self):
+        return Address.from_pubkey(self.pubkey, prefix=self.prefix)
+
+    @property
+    def pubkey(self):
+        return self.compressed
 
     def get_public_key(self):
-        """Returns the pubkey"""
+        """Legacy: Returns the pubkey"""
         return self.pubkey
 
-    def compressedpubkey(self):
-        """ Derive uncompressed public key """
-        secret = unhexlify(repr(self._wif))
-        if not len(secret) == ecdsa.SECP256k1.baselen:
-            raise ValueError("{} != {}".format(len(secret), ecdsa.SECP256k1.baselen))
-        order = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).curve.generator.order()
-        p = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).verifying_key.pubkey.point
-        x_str = ecdsa.util.number_to_string(p.x(), order)
-        y_str = ecdsa.util.number_to_string(p.y(), order)
-        compressed = hexlify(py23_bytes(chr(2 + (p.y() & 1)), 'ascii') + x_str).decode('ascii')
-        uncompressed = hexlify(py23_bytes(chr(4), 'ascii') + x_str + y_str).decode('ascii')
-        return([compressed, uncompressed])
+    @property
+    def compressed(self):
+        return PublicKey.from_privkey(self, prefix=self.prefix)
+
+    @property
+    def uncompressed(self):
+        return PublicKey(self.pubkey.uncompressed(), prefix=self.prefix)
 
     def get_secret(self):
         """ Get sha256 digest of the wif key.
         """
-        return hashlib.sha256(py23_bytes(self)).digest()
+        return hashlib.sha256(bytes(self)).digest()
 
     def derive_private_key(self, sequence):
         """ Derive new private key from this private key and an arbitrary
@@ -713,8 +793,7 @@ class PrivateKey(PublicKey):
     def child(self, offset256):
         """ Derive new private key from this key and a sha256 "offset"
         """
-        pubkey = self.get_public_key()
-        a = py23_bytes(pubkey) + offset256
+        a = py23_bytes(self.pubkey) + offset256
         s = hashlib.sha256(a).digest()
         return self.derive_from_seed(s)
 
@@ -726,10 +805,10 @@ class PrivateKey(PublicKey):
         seed = int(hexlify(py23_bytes(self)).decode('ascii'), 16)
         z = int(hexlify(offset).decode('ascii'), 16)
         order = ecdsa.SECP256k1.order
-
         secexp = (seed + z) % order
-
         secret = "%0x" % secexp
+        if len(secret) < 64: # left-pad with zeroes
+            secret = ("0" * (64-len(secret))) + secret        
         return PrivateKey(secret, prefix=self.pubkey.prefix)
 
     def __format__(self, _format):
@@ -751,3 +830,30 @@ class PrivateKey(PublicKey):
     def __bytes__(self):
         """ Returns the raw private key """
         return py23_bytes(self._wif)
+
+
+class BitcoinAddress(Address):
+    @classmethod
+    def from_pubkey(cls, pubkey, compressed=False, version=56, prefix=None):
+        # Ensure this is a public key
+        pubkey = PublicKey(pubkey)
+        if compressed:
+            pubkey = pubkey.compressed()
+        else:
+            pubkey = pubkey.uncompressed()
+
+        """ Derive address using ``RIPEMD160(SHA256(x))`` """
+        addressbin = ripemd160(hexlify(hashlib.sha256(unhexlify(pubkey)).digest()))
+        return cls(hexlify(addressbin).decode("ascii"))
+
+    def __str__(self):
+        """ Returns the readable Graphene address. This call is equivalent to
+            ``format(Address, "GPH")``
+        """
+        return format(self._address, "BTC")
+
+
+class BitcoinPublicKey(PublicKey):
+    @property
+    def address(self):
+        return BitcoinAddress.from_pubkey(repr(self))
diff --git a/beemgraphenebase/base58.py b/beemgraphenebase/base58.py
index b4cf7248..e822c5c3 100644
--- a/beemgraphenebase/base58.py
+++ b/beemgraphenebase/base58.py
@@ -3,39 +3,18 @@ from __future__ import division
 from __future__ import print_function
 from __future__ import unicode_literals
 from builtins import str
-from builtins import object
-from builtins import chr
 from future.utils import python_2_unicode_compatible
 from binascii import hexlify, unhexlify
 from .py23 import py23_bytes, py23_chr, bytes_types, integer_types, string_types, text_type
+from .prefix import Prefix
 import hashlib
 import string
 import logging
 log = logging.getLogger(__name__)
 
-""" Default Prefix """
-PREFIX = "GPH"
-
-known_prefixes = [
-    PREFIX,
-    "BTS",
-    "MUSE",
-    "TEST",
-    "TST",
-    "STM",
-    "STX",
-    "GLX",
-    "GLS",
-    "EOS",
-    "VIT",
-    "WKA",
-    "EUR",
-    "WLS",
-]
-
 
 @python_2_unicode_compatible
-class Base58(object):
+class Base58(Prefix):
     """Base58 base class
 
     This class serves as an abstraction layer to deal with base58 encoded
@@ -60,8 +39,8 @@ class Base58(object):
         * etc.
 
     """
-    def __init__(self, data, prefix=PREFIX):
-        self._prefix = prefix
+    def __init__(self, data, prefix=None):
+        self.set_prefix(prefix)
         if isinstance(data, Base58):
             data = repr(data)
         if all(c in string.hexdigits for c in data):
@@ -70,8 +49,8 @@ class Base58(object):
             self._hex = base58CheckDecode(data)
         elif data[0] == "K" or data[0] == "L":
             self._hex = base58CheckDecode(data)[:-2]
-        elif data[:len(self._prefix)] == self._prefix:
-            self._hex = gphBase58CheckDecode(data[len(self._prefix):])
+        elif data[:len(self.prefix)] == self.prefix:
+            self._hex = gphBase58CheckDecode(data[len(self.prefix):])
         else:
             raise ValueError("Error loading Base58 object")
 
@@ -89,10 +68,7 @@ class Base58(object):
             return base58encode(self._hex)
         elif _format.upper() == "BTC":
             return base58CheckEncode(0x00, self._hex)
-        elif _format.upper() in known_prefixes:
-            return _format.upper() + str(self)
         else:
-            log.warn("Format %s unknown. You've been warned!\n" % _format)
             return _format.upper() + str(self)
 
     def __repr__(self):
@@ -169,7 +145,6 @@ def ripemd160(s):
     ripemd160.update(unhexlify(s))
     return ripemd160.digest()
 
-
 def doublesha256(s):
     return hashlib.sha256(hashlib.sha256(unhexlify(s)).digest()).digest()
 
diff --git a/beemgraphenebase/bip38.py b/beemgraphenebase/bip38.py
index 6bccbde5..0dd4b470 100644
--- a/beemgraphenebase/bip38.py
+++ b/beemgraphenebase/bip38.py
@@ -58,7 +58,7 @@ def encrypt(privkey, passphrase):
 
     """
     privkeyhex = repr(privkey)   # hex
-    addr = format(privkey.uncompressed.address, "BTC")
+    addr = format(privkey.bitcoin.address, "BTC")
     a = py23_bytes(addr, 'ascii')
     salt = hashlib.sha256(hashlib.sha256(a).digest()).digest()[0:4]
     if sys.version < '3':
@@ -125,7 +125,7 @@ def decrypt(encrypted_privkey, passphrase):
     wif = Base58(privraw)
     """ Verify Salt """
     privkey = PrivateKey(format(wif, "wif"))
-    addr = format(privkey.uncompressed.address, "BTC")
+    addr = format(privkey.bitcoin.address, "BTC")
     a = py23_bytes(addr, 'ascii')
     saltverify = hashlib.sha256(hashlib.sha256(a).digest()).digest()[0:4]
     if saltverify != salt:
diff --git a/beemgraphenebase/ecdsasig.py b/beemgraphenebase/ecdsasig.py
index 5b8d1a78..81a3d463 100644
--- a/beemgraphenebase/ecdsasig.py
+++ b/beemgraphenebase/ecdsasig.py
@@ -135,13 +135,13 @@ def recoverPubkeyParameter(message, digest, signature, pubkey):
             pubkey_comp = hexlify(compressedPubkey(pubkey))
             if (p_comp == pubkey_comp):
                 return i
-        else:
+        else:  # pragma: no cover
             p = recover_public_key(digest, signature, i)
             p_comp = hexlify(compressedPubkey(p))
             p_string = hexlify(p.to_string())
             if isinstance(pubkey, PublicKey):
                 pubkey_string = py23_bytes(repr(pubkey), 'latin')
-            else:
+            else:  # pragma: no cover
                 pubkey_string = hexlify(pubkey.to_string())
             if (p_string == pubkey_string or
                     p_comp == pubkey_string):
@@ -208,7 +208,7 @@ def sign_message(message, wif, hashfn=hashlib.sha256):
                 i += 4   # compressed
                 i += 27  # compact
                 break
-    else:
+    else:  # pragma: no branch  # pragma: no cover
         cnt = 0
         p = py23_bytes(priv_key)
         sk = ecdsa.SigningKey.from_string(p, curve=ecdsa.SECP256k1)
@@ -299,7 +299,7 @@ def verify_message(message, signature, hashfn=hashlib.sha256, recover_parameter=
         sigder = encode_dss_signature(r, s)
         p.verify(sigder, message, ec.ECDSA(hashes.SHA256()))
         phex = compressedPubkey(p)
-    else:
+    else:  # pragma: no branch  # pragma: no cover
         p = recover_public_key(digest, sig, recover_parameter)
         # Will throw an exception of not valid
         p.verify_digest(
@@ -310,3 +310,14 @@ def verify_message(message, signature, hashfn=hashlib.sha256, recover_parameter=
         phex = compressedPubkey(p)
 
     return phex
+
+
+def tweakaddPubkey(pk, digest256, SECP256K1_MODULE=SECP256K1_MODULE):
+    if SECP256K1_MODULE == "secp256k1":
+        tmp_key = secp256k1.PublicKey(pubkey=bytes(pk), raw=True)
+        new_key = tmp_key.tweak_add(digest256)  # <-- add
+        raw_key = hexlify(new_key.serialize()).decode("ascii")
+    else:
+        raise Exception("Must have secp256k1 for `tweak_add`")
+        # raw_key = ecmult(pk, 1, digest256, SECP256K1_MODULE)
+    return PublicKey(raw_key, prefix=pk.prefix)
diff --git a/beemgraphenebase/prefix.py b/beemgraphenebase/prefix.py
new file mode 100644
index 00000000..25e55098
--- /dev/null
+++ b/beemgraphenebase/prefix.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+class Prefix:
+    """ This class is meant to allow changing the prefix.
+        The prefix is used to link a public key to a specific blockchain.
+    """
+
+    prefix = "STM"
+
+    def set_prefix(self, prefix):
+        if prefix:
+            self.prefix = prefix
diff --git a/beemgraphenebase/types.py b/beemgraphenebase/types.py
index 1e08d201..2d3b4d28 100644
--- a/beemgraphenebase/types.py
+++ b/beemgraphenebase/types.py
@@ -224,6 +224,36 @@ class Bytes(object):
         return str(self.data)
 
 
+@python_2_unicode_compatible
+class Hash(Bytes):
+    def json(self):
+        return str(self.data)
+
+    def __bytes__(self):
+        return unhexlify(bytes(self.data, "utf-8"))
+
+
+@python_2_unicode_compatible
+class Ripemd160(Hash):
+    def __init__(self, a):
+        assert len(a) == 40, "Require 40 char long hex"
+        super().__init__(a)
+
+
+@python_2_unicode_compatible
+class Sha1(Hash):
+    def __init__(self, a):
+        assert len(a) == 40, "Require 40 char long hex"
+        super().__init__(a)
+
+
+@python_2_unicode_compatible
+class Sha256(Hash):
+    def __init__(self, a):
+        assert len(a) == 64, "Require 64 char long hex"
+        super().__init__(a)
+
+
 @python_2_unicode_compatible
 class Void(object):
     def __init__(self):
@@ -401,15 +431,18 @@ class Id(object):
 
 @python_2_unicode_compatible
 class Enum8(Uint8):
+    # List needs to be provided by super class
+    options = []
+
     def __init__(self, selection):
-        if selection not in self.options and \
-                not (isinstance(selection, int) and len(self.options) < selection):
-            raise AssertionError("Options are %s. Given '%s'" % (
-                self.options, selection))
-        if selection in self.options:
-            super(Enum8, self).__init__(self.options.index(selection))
-        else:
-            super(Enum8, self).__init__(selection)
+        if selection not in self.options or (
+            isinstance(selection, int) and len(self.options) < selection
+        ):
+            raise ValueError(
+                "Options are {}. Given '{}'".format(str(self.options), selection)
+            )
+
+        super(Enum8, self).__init__(self.options.index(selection))
 
     def __str__(self):
         """Returns data as string."""
diff --git a/tests/beemgraphene/test_account.py b/tests/beemgraphene/test_account.py
index e6207cff..fadc084b 100644
--- a/tests/beemgraphene/test_account.py
+++ b/tests/beemgraphene/test_account.py
@@ -79,11 +79,11 @@ class Testcases(unittest.TestCase):
                           ])
 
     def test_btsprivkey(self):
-        self.assertEqual([format(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd").address, "BTS"),
-                          format(PrivateKey("5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S").address, "BTS"),
-                          format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").address, "BTS"),
-                          format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").address, "BTS"),
-                          format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").address, "BTS")
+        self.assertEqual([format(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd").compressed.address, "BTS"),
+                          format(PrivateKey("5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S").compressed.address, "BTS"),
+                          format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").compressed.address, "BTS"),
+                          format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").compressed.address, "BTS"),
+                          format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").compressed.address, "BTS")
                           ],
                          ["BTSFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi",
                           "BTSdXrrTXimLb6TEt3nHnePwFmBT6Cck112",
@@ -93,9 +93,9 @@ class Testcases(unittest.TestCase):
                           ])
 
     def test_btcprivkey(self):
-        self.assertEqual([format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").uncompressed.address, "BTC"),
-                          format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").uncompressed.address, "BTC"),
-                          format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").uncompressed.address, "BTC"),
+        self.assertEqual([format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").bitcoin.address, "BTC"),
+                          format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").bitcoin.address, "BTC"),
+                          format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").bitcoin.address, "BTC"),
                           ],
                          ["1G7qw8FiVfHEFrSt3tDi6YgfAdrDrEM44Z",
                           "12c7KAAZfpREaQZuvjC5EhpoN6si9vekqK",
diff --git a/tests/beemgraphene/test_key_format.py b/tests/beemgraphene/test_key_format.py
index e5865da0..338d9968 100644
--- a/tests/beemgraphene/test_key_format.py
+++ b/tests/beemgraphene/test_key_format.py
@@ -53,20 +53,20 @@ class Testcases(unittest.TestCase):
 
     def test_btc_uncompressed(self):
         public_key = PublicKey(key["public_key"])
-        address = Address(address=None, pubkey=public_key.unCompressed())
-        self.assertEqual(str(key["Uncompressed_BTC"]), (format(address.derive256address_with_version(0), "STM")))
+        address = Address.from_pubkey(public_key.uncompressed(), compressed=False, version=0)
+        self.assertEqual(str(key["Uncompressed_BTC"]), (format(address, "STM")))
 
     def test_btc_compressed(self):
         public_key = PublicKey(key["public_key"])
-        address = Address(address=None, pubkey=repr(public_key))
-        self.assertEqual(str(key["Compressed_BTC"]), (format(address.derive256address_with_version(0), "STM")))
+        address = Address.from_pubkey(repr(public_key), version=0)
+        self.assertEqual(str(key["Compressed_BTC"]), (format(address, "STM")))
 
     def test_pts_uncompressed(self):
         public_key = PublicKey(key["public_key"])
-        address = Address(address=None, pubkey=public_key.unCompressed())
-        self.assertEqual(str(key["Uncompressed_PTS"]), (format(address.derive256address_with_version(56), "STM")))
+        address = Address.from_pubkey(public_key.uncompressed(), compressed=False, version=56)
+        self.assertEqual(str(key["Uncompressed_PTS"]), (format(address, "STM")))
 
     def test_pts_compressed(self):
         public_key = PublicKey(key["public_key"])
-        address = Address(address=None, pubkey=repr(public_key))
-        self.assertEqual(str(key["Compressed_PTS"]), (format(address.derive256address_with_version(56), "STM")))
+        address = Address.from_pubkey(repr(public_key), version=56)
+        self.assertEqual(str(key["Compressed_PTS"]), (format(address, "STM")))
diff --git a/tests/beemgraphene/test_types.py b/tests/beemgraphene/test_types.py
index bd39a3ea..fdd341d9 100644
--- a/tests/beemgraphene/test_types.py
+++ b/tests/beemgraphene/test_types.py
@@ -7,8 +7,7 @@ from builtins import str
 import unittest
 import json
 from beemgraphenebase import types
-from beem.amount import Amount
-from beem import Steem
+from binascii import hexlify, unhexlify
 from beemgraphenebase.py23 import (
     py23_bytes,
     py23_chr,
@@ -179,3 +178,60 @@ class Testcases(unittest.TestCase):
         u = types.Map([[types.Uint16(10), types.Uint16(11)]])
         self.assertEqual(py23_bytes(u), b"\x01\n\x00\x0b\x00")
         self.assertEqual(str(u), '[["10", "11"]]')
+
+    def test_enum8(self):
+        class MyEnum(types.Enum8):
+            options = ["foo", "bar"]
+
+        u = MyEnum("bar")
+        self.assertEqual(bytes(u), b"\x01")
+        self.assertEqual(str(u), "bar")
+
+        with self.assertRaises(ValueError):
+            MyEnum("barbar")
+
+    def test_Hash(self):
+        u = types.Hash(hexlify(b"foobar").decode("ascii"))
+        self.assertEqual(bytes(u), b"foobar")
+        self.assertEqual(str(u), "666f6f626172")
+        self.assertEqual(u.json(), "666f6f626172")
+
+    def test_ripemd160(self):
+        u = types.Ripemd160("37f332f68db77bd9d7edd4969571ad671cf9dd3b")
+        self.assertEqual(
+            bytes(u), b"7\xf32\xf6\x8d\xb7{\xd9\xd7\xed\xd4\x96\x95q\xadg\x1c\xf9\xdd;"
+        )
+        self.assertEqual(str(u), "37f332f68db77bd9d7edd4969571ad671cf9dd3b")
+        self.assertEqual(u.json(), "37f332f68db77bd9d7edd4969571ad671cf9dd3b")
+
+        with self.assertRaises(AssertionError):
+            types.Ripemd160("barbar")
+
+    def test_sha1(self):
+        u = types.Sha1("37f332f68db77bd9d7edd4969571ad671cf9dd3b")
+        self.assertEqual(
+            bytes(u), b"7\xf32\xf6\x8d\xb7{\xd9\xd7\xed\xd4\x96\x95q\xadg\x1c\xf9\xdd;"
+        )
+        self.assertEqual(str(u), "37f332f68db77bd9d7edd4969571ad671cf9dd3b")
+        self.assertEqual(u.json(), "37f332f68db77bd9d7edd4969571ad671cf9dd3b")
+
+        with self.assertRaises(AssertionError):
+            types.Sha1("barbar")
+
+    def test_sha256(self):
+        u = types.Sha256(
+            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+        )
+        self.assertEqual(
+            bytes(u),
+            b"\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U",
+        )
+        self.assertEqual(
+            str(u), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+        )
+        self.assertEqual(
+            u.json(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+        )
+
+        with self.assertRaises(AssertionError):
+            types.Sha256("barbar")
-- 
GitLab