From 880908d857bbe81dc6bf7e4df94abc439f2744a3 Mon Sep 17 00:00:00 2001
From: Inso <insomniak.fr@gmail.com>
Date: Sat, 13 Dec 2014 23:22:12 +0100
Subject: [PATCH] New ucoinpy API

---
 lib/ucoinpy/__init__.py                       |  21 +
 lib/ucoinpy/api/__init__.py                   |   0
 lib/ucoinpy/api/bma/__init__.py               | 139 +++++++
 lib/ucoinpy/api/bma/blockchain/__init__.py    | 151 +++++++
 lib/ucoinpy/api/bma/network/__init__.py       |  35 ++
 .../api/bma/network/peering/__init__.py       |  51 +++
 lib/ucoinpy/api/bma/tx/__init__.py            |  44 ++
 lib/ucoinpy/api/bma/wot/__init__.py           |  89 +++++
 lib/ucoinpy/documents/__init__.py             |  41 ++
 lib/ucoinpy/documents/block.py                | 287 +++++++++++++
 lib/ucoinpy/documents/certification.py        |  86 ++++
 lib/ucoinpy/documents/membership.py           | 125 ++++++
 lib/ucoinpy/documents/peer.py                 | 148 +++++++
 lib/ucoinpy/documents/status.py               |  84 ++++
 lib/ucoinpy/documents/transaction.py          | 297 ++++++++++++++
 lib/ucoinpy/key/__init__.py                   |  36 ++
 lib/ucoinpy/key/hdwallet.py                   | 378 ++++++++++++++++++
 17 files changed, 2012 insertions(+)
 create mode 100644 lib/ucoinpy/__init__.py
 create mode 100644 lib/ucoinpy/api/__init__.py
 create mode 100644 lib/ucoinpy/api/bma/__init__.py
 create mode 100644 lib/ucoinpy/api/bma/blockchain/__init__.py
 create mode 100644 lib/ucoinpy/api/bma/network/__init__.py
 create mode 100644 lib/ucoinpy/api/bma/network/peering/__init__.py
 create mode 100644 lib/ucoinpy/api/bma/tx/__init__.py
 create mode 100644 lib/ucoinpy/api/bma/wot/__init__.py
 create mode 100644 lib/ucoinpy/documents/__init__.py
 create mode 100644 lib/ucoinpy/documents/block.py
 create mode 100644 lib/ucoinpy/documents/certification.py
 create mode 100644 lib/ucoinpy/documents/membership.py
 create mode 100644 lib/ucoinpy/documents/peer.py
 create mode 100644 lib/ucoinpy/documents/status.py
 create mode 100644 lib/ucoinpy/documents/transaction.py
 create mode 100644 lib/ucoinpy/key/__init__.py
 create mode 100644 lib/ucoinpy/key/hdwallet.py

diff --git a/lib/ucoinpy/__init__.py b/lib/ucoinpy/__init__.py
new file mode 100644
index 00000000..354362cd
--- /dev/null
+++ b/lib/ucoinpy/__init__.py
@@ -0,0 +1,21 @@
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Caner Candan <caner@candan.fr>, http://caner.candan.fr
+#
+
+PROTOCOL_VERSION="1"
+
+MANAGED_API=["BASIC_MERKLED_API"]
diff --git a/lib/ucoinpy/api/__init__.py b/lib/ucoinpy/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/lib/ucoinpy/api/bma/__init__.py b/lib/ucoinpy/api/bma/__init__.py
new file mode 100644
index 00000000..fbd95eaa
--- /dev/null
+++ b/lib/ucoinpy/api/bma/__init__.py
@@ -0,0 +1,139 @@
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Caner Candan <caner@candan.fr>, http://caner.candan.fr
+#
+
+
+__all__ = ['api']
+
+__author__      = 'Caner Candan'
+__version__     = '0.10.0'
+__nonsense__    = 'uCoin'
+
+import requests, logging, json
+# import pylibscrypt
+
+logger = logging.getLogger("ucoin")
+
+
+class ConnectionHandler(object):
+    """Helper class used by other API classes to ease passing server connection information."""
+
+    def __init__(self, server, port):
+        """
+        Arguments:
+        - `server`: server hostname
+        - `port`: port number
+        """
+
+        self.server = server
+        self.port = port
+
+    def __str__(self):
+        return 'connection info: %s:%d' % (self.server, self.port)
+
+
+class API(object):
+    """APIRequest is a class used as an interface. The intermediate derivated classes are the modules and the leaf classes are the API requests."""
+
+    def __init__(self, connection_handler, module):
+        """
+        Asks a module in order to create the url used then by derivated classes.
+
+        Arguments:
+        - `module`: module name
+        - `connection_handler`: connection handler
+        """
+
+        self.module = module
+        self.connection_handler = connection_handler
+        self.headers = {}
+
+    def reverse_url(self, path):
+        """
+        Reverses the url using self.url and path given in parameter.
+
+        Arguments:
+        - `path`: the request path
+        """
+
+        server, port = self.connection_handler.server, self.connection_handler.port
+
+        url = 'http://%s:%d/%s' % (server, port, self.module)
+        return url + path
+
+    def get(self, **kwargs):
+        """wrapper of overloaded __get__ method."""
+
+        return self.__get__(**kwargs)
+
+    def post(self, **kwargs):
+        """wrapper of overloaded __post__ method."""
+
+        logger.debug('do some work with')
+
+        data = self.__post__(**kwargs)
+
+        logger.debug('and send back')
+
+        return data
+
+    def __get__(self, **kwargs):
+        """interface purpose for GET request"""
+
+        pass
+
+    def __post__(self, **kwargs):
+        """interface purpose for POST request"""
+
+        pass
+
+    def requests_get(self, path, **kwargs):
+        """
+        Requests GET wrapper in order to use API parameters.
+
+        Arguments:
+        - `path`: the request path
+        """
+
+        response = requests.get(self.reverse_url(path), params=kwargs, headers=self.headers)
+
+        if response.status_code != 200:
+            raise ValueError('status code != 200 => %d (%s)' % (response.status_code, response.text))
+
+        return response
+
+    def requests_post(self, path, **kwargs):
+        """
+        Requests POST wrapper in order to use API parameters.
+
+        Arguments:
+        - `path`: the request path
+        """
+
+        response = requests.post(self.reverse_url(path), data=kwargs, headers=self.headers)
+
+        if response.status_code != 200:
+            raise ValueError('status code != 200 => %d (%s)' % (response.status_code, response.text))
+
+        return response
+
+    def merkle_easy_parser(self, path, begin=None, end=None):
+        root = self.requests_get(path, leaves='true').json()
+        for leaf in root['leaves'][begin:end]:
+            yield self.requests_get(path, leaf=leaf).json()['leaf']
+
+from . import network, blockchain, tx, wot
diff --git a/lib/ucoinpy/api/bma/blockchain/__init__.py b/lib/ucoinpy/api/bma/blockchain/__init__.py
new file mode 100644
index 00000000..03890265
--- /dev/null
+++ b/lib/ucoinpy/api/bma/blockchain/__init__.py
@@ -0,0 +1,151 @@
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Caner Candan <caner@candan.fr>, http://caner.candan.fr
+#
+
+from .. import API, logging
+
+logger = logging.getLogger("ucoin/blockchain")
+
+
+class Blockchain(API):
+    def __init__(self, connection_handler, module='blockchain'):
+        super(Blockchain, self).__init__(connection_handler, module)
+
+
+class Parameters(Blockchain):
+    """GET the blockchain parameters used by this node."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/parameters', **kwargs).json()
+
+
+class Membership(Blockchain):
+    """POST a Membership document."""
+
+    def __post__(self, **kwargs):
+        assert 'membership' in kwargs
+
+        return self.requests_post('/membership', **kwargs).json()
+
+
+class Block(Blockchain):
+    """GET/POST a block from/to the blockchain."""
+
+    def __init__(self, connection_handler, number=None):
+        """
+        Use the number parameter in order to select a block number.
+
+        Arguments:
+        - `number`: block number to select
+        """
+
+        super(Block, self).__init__(connection_handler)
+
+        self.number = number
+
+    def __get__(self, **kwargs):
+        assert self.number is not None
+        return self.requests_get('/block/%d' % self.number, **kwargs).json()
+
+    def __post__(self, **kwargs):
+        assert 'block' in kwargs
+        assert 'signature' in kwargs
+
+        return self.requests_post('/block', **kwargs).json()
+
+
+class Current(Blockchain):
+    """GET, same as block/[number], but return last accepted block."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/current', **kwargs).json()
+
+
+class Hardship(Blockchain):
+    """GET hardship level for given member's fingerprint for writing next block."""
+
+    def __init__(self, connection_handler, fingerprint):
+        """
+        Use the number parameter in order to select a block number.
+
+        Arguments:
+        - `fingerprint`: member fingerprint
+        """
+
+        super(Hardship, self).__init__(connection_handler)
+
+        self.fingerprint = fingerprint
+
+    def __get__(self, **kwargs):
+        assert self.fingerprint is not None
+        return self.requests_get('/hardship/%s' % self.fingerprint.upper(), **kwargs).json()
+
+
+class Newcomers(Blockchain):
+    """GET, return block numbers containing newcomers."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/newcomers', **kwargs).json()
+
+
+class Certifications(Blockchain):
+    """GET, return block numbers containing certifications."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/certs', **kwargs).json()
+
+
+class Joiners(Blockchain):
+    """GET, return block numbers containing joiners."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/joiners', **kwargs).json()
+
+
+class Actives(Blockchain):
+    """GET, return block numbers containing actives."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/actives', **kwargs).json()
+
+
+class Leavers(Blockchain):
+    """GET, return block numbers containing leavers."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/leavers', **kwargs).json()
+
+
+class Excluded(Blockchain):
+    """GET, return block numbers containing excluded."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/excluded', **kwargs).json()
+
+
+class UD(Blockchain):
+    """GET, return block numbers containing universal dividend."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/ud', **kwargs).json()
+
+
+class TX(Blockchain):
+    """GET, return block numbers containing transactions."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/with/tx', **kwargs).json()
diff --git a/lib/ucoinpy/api/bma/network/__init__.py b/lib/ucoinpy/api/bma/network/__init__.py
new file mode 100644
index 00000000..3d6c73ec
--- /dev/null
+++ b/lib/ucoinpy/api/bma/network/__init__.py
@@ -0,0 +1,35 @@
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Caner Candan <caner@candan.fr>, http://caner.candan.fr
+#
+
+from .. import API, logging
+
+logger = logging.getLogger("ucoin/network")
+
+
+class Network(API):
+    def __init__(self, connection_handler, module='network'):
+        super(Network, self).__init__(connection_handler, module)
+
+
+class Peering(Network):
+    """GET peering information about a peer."""
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/peering', **kwargs).json()
+
+from . import peering
diff --git a/lib/ucoinpy/api/bma/network/peering/__init__.py b/lib/ucoinpy/api/bma/network/peering/__init__.py
new file mode 100644
index 00000000..3ad7cde3
--- /dev/null
+++ b/lib/ucoinpy/api/bma/network/peering/__init__.py
@@ -0,0 +1,51 @@
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Caner Candan <caner@candan.fr>, http://caner.candan.fr
+#
+
+from .. import Network, logging
+
+logger = logging.getLogger("ucoin/network/peering")
+
+
+class Base(Network):
+    def __init__(self, connection_handler):
+        super(Base, self).__init__(connection_handler, 'network/peering')
+
+
+class Peers(Base):
+    """GET peering entries of every node inside the currency network."""
+
+    def __get__(self, **kwargs):
+        """creates a generator with one peering entry per iteration."""
+
+        return self.merkle_easy_parser('/peers')
+
+    def __post__(self, **kwargs):
+        assert 'entry' in kwargs
+        assert 'signature' in kwargs
+
+        return self.requests_post('/peers', **kwargs).json()
+
+
+class Status(Base):
+    """POST a network status document to this node in order notify of its status."""
+
+    def __post__(self, **kwargs):
+        assert 'status' in kwargs
+        assert 'signature' in kwargs
+
+        return self.requests_post('/status', **kwargs).json()
diff --git a/lib/ucoinpy/api/bma/tx/__init__.py b/lib/ucoinpy/api/bma/tx/__init__.py
new file mode 100644
index 00000000..a7adc5f3
--- /dev/null
+++ b/lib/ucoinpy/api/bma/tx/__init__.py
@@ -0,0 +1,44 @@
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Caner Candan <caner@candan.fr>, http://caner.candan.fr
+#
+
+from .. import API, logging
+
+logger = logging.getLogger("ucoin/tx")
+
+
+class Tx(API):
+    def __init__(self, connection_handler, module='tx'):
+        super(Tx, self).__init__(connection_handler, module)
+
+
+class Process(Tx):
+    """POST a transaction."""
+
+    def __post__(self, **kwargs):
+        assert 'transaction' in kwargs
+        assert 'signature' in kwargs
+
+        return self.requests_post('/process', **kwargs).json()
+
+
+class Sources(Tx):
+    """Get transaction sources."""
+
+    def __get__(self, **kwargs):
+        assert self.pubkey is not None
+        return self.requests_get('/sources/%d' % self.pubkey, **kwargs).json()
diff --git a/lib/ucoinpy/api/bma/wot/__init__.py b/lib/ucoinpy/api/bma/wot/__init__.py
new file mode 100644
index 00000000..3e1c694a
--- /dev/null
+++ b/lib/ucoinpy/api/bma/wot/__init__.py
@@ -0,0 +1,89 @@
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Caner Candan <caner@candan.fr>, http://caner.candan.fr
+#
+
+from .. import API, logging
+
+logger = logging.getLogger("ucoin/wot")
+
+
+class WOT(API):
+    def __init__(self, connection_handler, module='wot'):
+        super(WOT, self).__init__(connection_handler, module)
+
+
+class Add(WOT):
+    """POST Public key data."""
+
+    def __post__(self, **kwargs):
+        assert 'pubkey' in kwargs
+        assert 'self' in kwargs
+        assert 'other' in kwargs
+
+        return self.requests_post('/add', **kwargs).json()
+
+
+class Lookup(WOT):
+    """GET Public key data."""
+
+    def __init__(self, connection_handler, search, module='wot'):
+        super(WOT, self).__init__(connection_handler, module)
+
+        self.search = search
+
+    def __get__(self, **kwargs):
+        assert self.search is not None
+
+        return self.requests_get('/lookup/%s' % self.search, **kwargs).json()
+
+
+class CertifiersOf(WOT):
+    """GET Certification data over a member."""
+
+    def __init__(self, connection_handler, search, module='wot'):
+        super(WOT, self).__init__(connection_handler, module)
+
+        self.search = search
+
+    def __get__(self, **kwargs):
+        assert self.search is not None
+
+        return self.requests_get('/certifiers-of/%s' % self.search, **kwargs).json()
+
+
+class CertifiedBy(WOT):
+    """GET Certification data from a member."""
+
+    def __init__(self, connection_handler, search, module='wot'):
+        super(WOT, self).__init__(connection_handler, module)
+
+        self.search = search
+
+    def __get__(self, **kwargs):
+        assert self.search is not None
+
+        return self.requests_get('/certified-by/%s' % self.search, **kwargs).json()
+
+
+class Members(WOT):
+    """GET List all current members of the Web of Trust."""
+
+    def __init__(self, connection_handler, module='wot'):
+        super(WOT, self).__init__(connection_handler, module)
+
+    def __get__(self, **kwargs):
+        return self.requests_get('/members', **kwargs).json()
diff --git a/lib/ucoinpy/documents/__init__.py b/lib/ucoinpy/documents/__init__.py
new file mode 100644
index 00000000..14a7f83d
--- /dev/null
+++ b/lib/ucoinpy/documents/__init__.py
@@ -0,0 +1,41 @@
+'''
+Created on 3 déc. 2014
+
+@author: inso
+'''
+import base58
+import re
+from ..key import Base58Encoder
+from nacl.encoding import Base64Encoder
+
+
+class Document:
+    re_version = re.compile("Version: ([0-9]+)\n")
+    re_currency = re.compile("Currency: ([^\n]+)\n")
+    re_signature = re.compile("([A-Za-z0-9+/]+(?:=|==)?)\n")
+
+    def __init__(self, version, currency, signatures):
+        self.version = version
+        self.currency = currency
+        self.signatures = signatures
+
+    def sign(self, keys):
+        '''
+        Sign the current document.
+        Warning : current signatures will be replaced with the new ones.
+        '''
+        self.signatures = []
+        for k in keys:
+            self.signatures.append(k.sign(self.raw(), Base64Encoder))
+
+    def signed_raw(self):
+        '''
+        If keys are None, returns the raw + current signatures
+        If keys are present, returns the raw signed by these keys
+        '''
+        raw = self.raw()
+        signed_raw = raw
+        for s in self.signatures:
+            if s is not None:
+                signed_raw += s + "\n"
+        return signed_raw
diff --git a/lib/ucoinpy/documents/block.py b/lib/ucoinpy/documents/block.py
new file mode 100644
index 00000000..1bf08afe
--- /dev/null
+++ b/lib/ucoinpy/documents/block.py
@@ -0,0 +1,287 @@
+'''
+Created on 2 déc. 2014
+
+@author: inso
+'''
+
+from .. import PROTOCOL_VERSION
+from . import Document
+from .certification import SelfCertification, Certification
+from .membership import Membership
+from .transaction import Transaction
+
+import re
+
+
+class Block(Document):
+    '''
+Version: VERSION
+Type: Block
+Currency: CURRENCY
+Nonce: NONCE
+Number: BLOCK_NUMBER
+PoWMin: NUMBER_OF_ZEROS
+Time: GENERATED_ON
+MedianTime: MEDIAN_DATE
+UniversalDividend: DIVIDEND_AMOUNT
+Issuer: ISSUER_KEY
+PreviousHash: PREVIOUS_HASH
+PreviousIssuer: PREVIOUS_ISSUER_KEY
+Parameters: PARAMETERS
+MembersCount: WOT_MEM_COUNT
+Identities:
+PUBLIC_KEY:SIGNATURE:TIMESTAMP:USER_ID
+...
+Joiners:
+PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
+...
+Actives:
+PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
+...
+Leavers:
+PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
+...
+Excluded:
+PUBLIC_KEY
+...
+Certifications:
+PUBKEY_FROM:PUBKEY_TO:BLOCK_NUMBER:SIGNATURE
+...
+Transactions:
+COMPACT_TRANSACTION
+...
+BOTTOM_SIGNATURE
+    '''
+
+    re_type = re.compile("Type: (Block)\n")
+    re_noonce = re.compile("Nonce: ([0-9]+)\n")
+    re_number = re.compile("Number: ([0-9]+)\n")
+    re_powmin = re.compile("PoWMin: ([0-9]+)\n")
+    re_time = re.compile("Time: ([0-9]+)\n")
+    re_mediantime = re.compile("MedianTime: ([0-9]+)\n")
+    re_universaldividend = re.compile("UniversalDividend: ([0-9]+)\n")
+    re_issuer = re.compile("Issuer: ([1-9A-Za-z][^OIl]{42,45})\n")
+    re_previoushash = re.compile("PreviousHash: ([0-9a-fA-F]{5,40})\n")
+    re_previousissuer = re.compile("PreviousIssuer: ([1-9A-Za-z][^OIl]{42,45})\n")
+    re_parameters = re.compile("Parameters: ([0-9]+\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):\
+([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):\
+([0-9]+\.[0-9]+)\n")
+    re_memberscount = re.compile("MembersCount: ([0-9]+)\n")
+    re_identities = re.compile("Identities:\n")
+    re_joiners = re.compile("Joiners:\n")
+    re_actives = re.compile("Actives:\n")
+    re_leavers = re.compile("Leavers:\n")
+    re_excluded = re.compile("Excluded:\n")
+    re_certifications = re.compile("Certifications:\n")
+    re_transactions = re.compile("Transactions:\n")
+
+    def __init__(self, version, currency, noonce, number, powmin, time,
+                 mediantime, ud, issuer, prev_hash, prev_issuer,
+                 parameters, members_count, identities, joiners,
+                 actives, leavers, excluded, certifications,
+                 transactions, signature):
+        '''
+        Constructor
+        '''
+        if signature:
+            super().__init__(version, currency, [signature])
+        else:
+            super().__init__(version, currency, [])
+        self.noonce = noonce
+        self.number = number
+        self.powmin = powmin
+        self.time = time
+        self.mediantime = mediantime
+        self.ud = ud
+        self.issuer = issuer
+        self.prev_hash = prev_hash
+        self.prev_issuer = prev_issuer
+        self.parameters = parameters
+        self.members_count = members_count
+        self.identities = identities
+        self.joiners = joiners
+        self.actives = actives
+        self.leavers = leavers
+        self.excluded = excluded
+        self.certifications = certifications
+        self.transactions = transactions
+
+    @classmethod
+    def from_signed_raw(cls, raw, signature=None):
+        lines = raw.splitlines(True)
+        n = 0
+
+        version = int(Block.re_version.match(lines[n]).group(1))
+        n = n + 1
+
+        Block.re_type.match(lines[n]).group(1)
+        n = n + 1
+
+        currency = Block.re_currency.match(lines[n]).group(1)
+        n = n + 1
+
+        noonce = int(Block.re_noonce.match(lines[n]).group(1))
+        n = n + 1
+
+        number = int(Block.re_number.match(lines[n]).group(1))
+        n = n + 1
+
+        powmin = int(Block.re_powmin.match(lines[n]).group(1))
+        n = n + 1
+
+        time = int(Block.re_time.match(lines[n]).group(1))
+        n = n + 1
+
+        mediantime = int(Block.re_mediantime.match(lines[n]).group(1))
+        n = n + 1
+
+        ud = Block.re_universaldividend.match(lines[n])
+        if ud is not None:
+            ud = int(ud.group(1))
+            n = n + 1
+
+        issuer = Block.re_issuer.match(lines[n]).group(1)
+        n = n + 1
+
+        prev_hash = None
+        prev_issuer = None
+        if number > 0:
+            prev_hash = Block.re_previoushash.match(lines[n]).group(1)
+            n = n + 1
+
+            prev_issuer = Block.re_previousissuer.match(lines[n]).group(1)
+            n = n + 1
+
+        parameters = None
+        if number == 0:
+            parameters = Block.re_parameters.match(lines[n]).groups()
+            n = n + 1
+
+        members_count = int(Block.re_memberscount.match(lines[n]).group(1))
+        n = n + 1
+
+        identities = []
+        joiners = []
+        actives = []
+        leavers = []
+        excluded = []
+        certifications = []
+        transactions = []
+
+        if Block.re_identities.match(lines[n]) is not None:
+            n = n + 1
+            while Block.re_joiners.match(lines[n]) is None:
+                selfcert = SelfCertification.from_inline(version, currency, lines[n])
+                identities.append(selfcert)
+                n = n + 1
+
+        if Block.re_joiners.match(lines[n]):
+            n = n + 1
+            while Block.re_actives.match(lines[n]) is None:
+                membership = Membership.from_inline(version, currency, "IN", lines[n])
+                joiners.append(membership)
+                n = n + 1
+
+        if Block.re_actives.match(lines[n]):
+            n = n + 1
+            while Block.re_leavers.match(lines[n]) is None:
+                membership = Membership.from_inline(version, currency, "IN", lines[n])
+                actives.append(membership)
+                n = n + 1
+
+        if Block.re_leavers.match(lines[n]):
+            n = n + 1
+            while Block.re_excluded.match(lines[n]) is None:
+                membership = Membership.from_inline(version, currency, "OUT", lines[n])
+                leavers.append(membership)
+                n = n + 1
+
+        if Block.re_excluded.match(lines[n]):
+            n = n + 1
+            while Block.re_certifications.match(lines[n]) is None:
+                membership = Membership.from_inline(version, currency, "OUT", lines[n])
+                excluded.append(membership)
+                n = n + 1
+
+        if Block.re_certifications.match(lines[n]):
+            n = n + 1
+            while Block.re_transactions.match(lines[n]) is None:
+                certification = Certification.from_inline(version, currency,
+                                                          prev_hash, lines[n])
+                certifications.append(certification)
+                n = n + 1
+
+        if Block.re_transactions.match(lines[n]):
+            n = n + 1
+            while not Block.re_signature.match(lines[n]):
+                transaction = Transaction.from_compact(version, lines[n])
+                transactions.append(transaction)
+                n = n + 1
+
+        signature = Block.re_signature.match(lines[n]).group(1)
+
+        return cls(version, currency, noonce, number, powmin, time,
+                   mediantime, ud, issuer, prev_hash, prev_issuer,
+                   parameters, members_count, identities, joiners,
+                   actives, leavers, excluded, certifications,
+                   transactions, signature)
+
+    def raw(self):
+        doc = """Version: {0}
+Type: Block
+Currency: {1}
+Nonce: {2}
+Number: {3}
+PoWMin: {4}
+Time: {5}
+MedianTime: {6}
+""".format(self.version,
+                      self.currency,
+                      self.noonce,
+                      self.number,
+                      self.powmin,
+                      self.time,
+                      self.mediantime)
+        if self.ud:
+            doc += "UniversalDividend: {0}\n".format(self.ud)
+
+        doc += "Issuer: {0}\n".format(self.issuer)
+
+        if self.number == 0:
+            str_params = ":".join(self.parameters)
+            doc += "Parameters: {0}\n".format(str_params)
+        else:
+            doc += "PreviousHash: {0}\n\
+PreviousIssuer: {1}\n".format(self.prev_hash, self.prev_issuer)
+
+        doc += "MembersCount: {0}\n".format(self.members_count)
+
+        doc += "Identities:\n"
+        for identity in self.identities:
+            doc += "{0}\n".format(identity.inline())
+
+        doc += "Joiners:\n"
+        for joiner in self.joiners:
+            doc += "{0}\n".format(joiner.inline())
+
+        doc += "Actives:\n"
+        for active in self.actives:
+            doc += "{0}\n".format(active.inline())
+
+        doc += "Leavers:\n"
+        for leaver in self.leavers:
+            doc += "{0]\n".format(leaver.inline())
+
+        doc += "Excluded:\n"
+        for exclude in self.excluded:
+            doc += "{0}\n".format(exclude.inline())
+
+        doc += "Certifications:\n"
+        for cert in self.certifications:
+            doc += "{0}\n".format(cert.inline())
+
+        doc += "Transactions:\n"
+        for transaction in self.transactions:
+            doc += "{0}\n".format(transaction.inline())
+
+        return doc
diff --git a/lib/ucoinpy/documents/certification.py b/lib/ucoinpy/documents/certification.py
new file mode 100644
index 00000000..3d758409
--- /dev/null
+++ b/lib/ucoinpy/documents/certification.py
@@ -0,0 +1,86 @@
+'''
+Created on 2 déc. 2014
+
+@author: inso
+'''
+import re
+
+from . import Document
+
+
+class SelfCertification(Document):
+    '''
+    A document discribing a self certification.
+    '''
+
+    re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):([A-Za-z0-9+/]+(?:=|==)?):([0-9]+):([^\n]+)\n")
+    re_uid = re.compile("UID:([^\n]+)\n")
+    re_timestamp = re.compile("META:TS:([0-9]+)\n")
+
+    def __init__(self, version, currency, pubkey, ts, uid, signature):
+        if signature:
+            super().__init__(version, currency, [signature])
+        else:
+            super().__init__(version, currency, [])
+        self.pubkey = pubkey
+        self.timestamp = ts
+        self.uid = uid
+
+    @classmethod
+    def from_inline(cls, version, currency, inline):
+        selfcert_data = SelfCertification.re_inline.match(inline)
+        pubkey = selfcert_data.group(1)
+        signature = selfcert_data.group(2)
+        ts = int(selfcert_data.group(3))
+        uid = selfcert_data.group(4)
+        return cls(version, currency, pubkey, ts, uid, signature)
+
+    def raw(self):
+        return """UID:{0}
+META:TS:{1}""".format(self.uid(), self.ts())
+
+    def inline(self):
+        return "{0}:{1}:{2}:{3}".format(self.pubkey, self.signatures[0],
+                                    self.timestamp, self.uid)
+
+
+class Certification(Document):
+    '''
+    A document describing a certification.
+    '''
+
+    re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):\
+([1-9A-Za-z][^OIl]{42,45}):([0-9]+):([A-Za-z0-9+/]+(?:=|==)?)\n")
+    re_timestamp = re.compile("META:TS:([0-9]+)-([0-9a-fA-F]{5,40})\n")
+
+    def __init__(self, version, currency, pubkey_from, pubkey_to,
+                 blockhash, blocknumber, signature):
+        '''
+        Constructor
+        '''
+        super().__init__(version, currency, [signature])
+        self.pubkey_from = pubkey_from
+        self.pubkey_to = pubkey_to
+        self.blockhash = blockhash
+        self.blocknumber = blocknumber
+
+    @classmethod
+    def from_inline(cls, version, currency, blockhash, inline):
+        cert_data = Certification.re_inline.match(inline)
+        pubkey_from = cert_data.group(1)
+        pubkey_to = cert_data.group(2)
+        blocknumber = int(cert_data.group(3))
+        if blocknumber == 0:
+            blockhash = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
+        signature = cert_data.group(4)
+        return cls(version, currency, pubkey_from, pubkey_to,
+                   blockhash, blocknumber, signature)
+
+    def raw(self, selfcert):
+        return """{0}
+META:TS:{1}-{2}""".format(selfcert.signed_raw(), self.blockhash, self.blocknumber)
+
+    def inline(self):
+        return "{0}:{1}:{2}:{3}".format(self.pubkey_from, self.pubkey_to,
+                                        self.blocknumber, self.signatures[0])
+
diff --git a/lib/ucoinpy/documents/membership.py b/lib/ucoinpy/documents/membership.py
new file mode 100644
index 00000000..3f142a7f
--- /dev/null
+++ b/lib/ucoinpy/documents/membership.py
@@ -0,0 +1,125 @@
+'''
+Created on 2 déc. 2014
+
+@author: inso
+'''
+from .. import PROTOCOL_VERSION
+from . import Document
+
+import re
+
+
+class Membership(Document):
+    '''
+    This is a utility class to generate membership documents :
+    Version: VERSION
+    Type: Membership
+    Currency: CURRENCY_NAME
+    Issuer: ISSUER
+    Block: NUMBER-HASH
+    Membership: MEMBERSHIP_TYPE
+    UserID: USER_ID
+    CertTS: CERTIFICATION_TS
+    '''
+
+    # PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
+    re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):([A-Za-z0-9+/]+(?:=|==)?):\
+([0-9]+):([0-9a-fA-F]{5,40}):([0-9]+):([^\n]+)\n")
+    re_type = re.compile("Type: (Membership)")
+    re_issuer = re.compile("Issuer: ([1-9A-Za-z][^OIl]{42,45})\n")
+    re_block = re.compile("Block: ([0-9]+)-([0-9a-fA-F]{5,40})\n")
+    re_membership_type = re.compile("Membership: (IN|OUT)")
+    re_userid = re.compile("UserID: ([^\n]+)\n")
+    re_certts = re.compile("CertTS: ([0-9]+)\n")
+
+
+
+    def __init__(self, version, currency, issuer, block_number, block_hash,
+                 membership_type, uid, cert_ts, signature):
+        '''
+        Constructor
+        '''
+        if signature:
+            super().__init__(version, currency, [signature])
+        else:
+            super().__init__(version, currency, [])
+        self.issuer = issuer
+        self.block_number = block_number
+        self.block_hash = block_hash
+        self.membership_type = membership_type
+        self.uid = uid
+        self.cert_ts = cert_ts
+
+    @classmethod
+    def from_inline(cls, version, currency, membership_type, inline):
+        data = Membership.re_inline.match(inline)
+        issuer = data.group(1)
+        signature = data.group(2)
+        block_number = int(data.group(3))
+        block_hash = data.group(4)
+        cert_ts = int(data.group(5))
+        uid = data.group(6)
+        return cls(version, currency, issuer, block_number,
+                   block_hash, membership_type, uid, cert_ts, signature)
+
+    @classmethod
+    def from_signed_raw(cls, raw, signature=None):
+        lines = raw.splitlines(True)
+        n = 0
+
+        version = int(Membership.re_version.match(lines[n]).group(1))
+        n = n + 1
+
+        Membership.re_type.match(lines[n]).group(1)
+        n = n + 1
+
+        currency = Membership.re_currency.match(lines[n]).group(1)
+        n = n + 1
+
+        issuer = Membership.re_issuer.match(lines[n]).group(1)
+        n = n + 1
+
+        blockid = Membership.re_block.match(lines[n])
+        blocknumber = int(blockid.group(1))
+        blockhash = blockid.group(2)
+        n = n + 1
+
+        membership_type = Membership.re_membership_type.match(lines[n]).group(1)
+        n = n + 1
+
+        uid = Membership.re_userid.match(lines[n]).group(1)
+        n = n + 1
+
+        cert_ts = int(Membership.re_certts.match(lines[n]).group(1))
+        n = n + 1
+
+        signature = Membership.re_signature.match(lines[n]).group(1)
+        n = n + 1
+
+        return cls(version, currency, issuer, blocknumber, blockhash,
+                   membership_type, uid, cert_ts, signature)
+
+    def raw(self):
+        return """Version: {0}
+Type: Membership
+Currency: {1}
+Issuer: {2}
+Block: {3}-{4}
+Membership: {5}
+UserID: {6}
+CertTS: {7}
+""".format(self.version,
+                      self.currency,
+                      self.issuer,
+                      self.block_number, self.block_hash,
+                      self.membership_type,
+                      self.uid,
+                      self.cert_ts)
+
+    def inline(self):
+        return "{0}:{1}:{2}:{3}:{4}:{5}".format(self.issuer,
+                                        self.signatures[0],
+                                        self.block_number,
+                                        self.block_hash,
+                                        self.cert_ts,
+                                        self.uid)
diff --git a/lib/ucoinpy/documents/peer.py b/lib/ucoinpy/documents/peer.py
new file mode 100644
index 00000000..1148785a
--- /dev/null
+++ b/lib/ucoinpy/documents/peer.py
@@ -0,0 +1,148 @@
+'''
+Created on 2 déc. 2014
+
+@author: inso
+'''
+
+import re
+
+from . import Document
+from .. import PROTOCOL_VERSION, MANAGED_API
+
+
+class Peer(Document):
+    """
+    Version: VERSION
+    Type: Peer
+    Currency: CURRENCY_NAME
+    PublicKey: NODE_PUBLICKEY
+    Block: BLOCK
+    Endpoints:
+    END_POINT_1
+    END_POINT_2
+    END_POINT_3
+    [...]
+    """
+
+    re_type = re.compile("Type: (Peer)")
+    re_pubkey = re.compile("PublicKey: ([1-9A-Za-z][^OIl]{42,45})\n")
+    re_block = re.compile("Block: ([0-9]+-[0-9a-fA-F]{5,40})\n")
+    re_endpoints = re.compile("Endpoints:\n")
+
+    def __init__(self, version, currency, pubkey, blockid,
+                 endpoints, signature):
+        if signature:
+            super().__init__(version, currency, [signature])
+        else:
+            super().__init__(version, currency, [])
+
+        self.pubkey = pubkey
+        self.blockid = blockid
+        self.endpoints = endpoints
+
+    @classmethod
+    def from_signed_raw(cls, raw):
+        lines = raw.splitlines(True)
+        n = 0
+
+        version = int(Peer.re_version.match(lines[n]).group(1))
+        n = n + 1
+
+        Peer.re_type.match(lines[n]).group(1)
+        n = n + 1
+
+        currency = Peer.re_currency.match(lines[n]).group(1)
+        n = n + 1
+
+        pubkey = Peer.re_pubkey.match(lines[n]).group(1)
+        n = n + 1
+
+        blockid = Peer.re_block.match(lines[n]).group(1)
+        n = n + 1
+
+        Peer.re_endpoints.match(lines[n])
+        n = n + 1
+
+        endpoints = []
+        while not Peer.re_signature.match(lines[n]):
+            endpoint = Endpoint.from_inline(lines[n])
+            endpoints.append(endpoint)
+            n = n + 1
+
+        signature = Peer.re_signature.match(lines[n]).group(1)
+
+        return cls(version, currency, pubkey, blockid, endpoints, signature)
+
+    def raw(self):
+        doc = """Version: {0}
+Type: Peer
+Currency: {1}
+PublicKey: {2}
+Block: {3}
+Endpoints:
+""".format(self.version, self.currency, self.pubkey, self.blockid)
+
+        for endpoint in self.endpoints:
+            doc += "{0}\n".format(endpoint.inline())
+
+        doc += "{0}\n".format(self.signatures[0])
+        return doc
+
+
+class Endpoint():
+    """
+    Describing endpoints
+    """
+
+    @staticmethod
+    def from_inline(inline):
+        for api in MANAGED_API:
+            if (inline.startswith(api)):
+                if (api == "BASIC_MERKLED_API"):
+                    return BMAEndpoint.from_inline(inline)
+        return UnknownEndpoint.from_inline(inline)
+
+
+class UnknownEndpoint(Endpoint):
+
+    def __init__(self, api, properties):
+        self.api = api
+        self.properties = properties
+
+    @classmethod
+    def from_inline(cls, inline):
+        api = inline.split()[0]
+        properties = inline.split()[1:]
+        return cls(api, properties)
+
+    def inline(self):
+        doc = self.api
+        for p in self.properties:
+            doc += " {0}".format(p)
+        return doc
+
+
+class BMAEndpoint(Endpoint):
+    re_inline = re.compile('^BASIC_MERKLED_API(?: ([a-z_][a-z0-9-_.]+))?(?: ([0-9.]+))?(?: ([0-9a-f:]+))?(?: ([0-9]+))$')
+
+    @classmethod
+    def from_inline(cls, inline):
+        m = BMAEndpoint.re_inline.match(inline)
+        server = m.group(1)
+        ipv4 = m.group(2)
+        ipv6 = m.group(3)
+        port = int(m.group(4))
+        return cls(server, ipv4, ipv6, port)
+
+    def __init__(self, server, ipv4, ipv6, port):
+        self.server = server
+        self.ipv4 = ipv4
+        self.ipv6 = ipv6
+        self.port = port
+
+    def inline(self):
+        return "BASIC_MERKLED_API {DNS} {IPv4} {IPv6} {PORT}" \
+                    .format(DNS=self.server,
+                            IPv4=self.ipv4,
+                            IPv6=self.ipv6,
+                            PORT=self.port)
diff --git a/lib/ucoinpy/documents/status.py b/lib/ucoinpy/documents/status.py
new file mode 100644
index 00000000..5b25bfae
--- /dev/null
+++ b/lib/ucoinpy/documents/status.py
@@ -0,0 +1,84 @@
+'''
+Created on 2 déc. 2014
+
+@author: inso
+'''
+
+import re
+from . import Document
+
+
+class Status(Document):
+    '''
+    Version: VERSION
+    Type: Status
+    Currency: CURRENCY_NAME
+    Status: STATUS
+    Block: BLOCK
+    From: SENDER
+    To: RECIPIENT
+    '''
+
+    re_type = re.compile("Type: (Status)")
+    re_status = re.compile("Status: (NEW|NEW_BACK|UP|UP_BACK|DOWN)")
+    re_block = re.compile("Block: ([0-9]+-[0-9a-fA-F]{5,40})\n")
+    re_from = re.compile("From: ([1-9A-Za-z][^OIl]{42,45})\n")
+    re_to = re.compile("To: ([1-9A-Za-z][^OIl]{42,45})\n")
+
+    def __init__(self, version, currency, status, blockid, sender,
+                 recipient, signature):
+        '''
+        Constructor
+        '''
+        if signature:
+            super().__init__(version, currency, [signature])
+        else:
+            super().__init__(version, currency, [])
+
+        self.status = status
+        self.blockid = blockid
+        self.sender = sender
+        self.recipient = recipient
+
+    @classmethod
+    def from_signed_raw(cls, raw):
+        lines = raw.splitlines(True)
+        n = 0
+
+        version = int(Status.re_version.match(lines[n]).group(1))
+        n = n + 1
+
+        Status.re_type.match(lines[n]).group(1)
+        n = n + 1
+
+        currency = Status.re_currency.match(lines[n]).group(1)
+        n = n + 1
+
+        status = Status.re_status.match(lines[n]).group(1)
+        n = n + 1
+
+        blockid = Status.re_block.match(lines[n]).group(1)
+        n = n + 1
+
+        sender = Status.re_from.match(lines[n]).group(1)
+        n = n + 1
+
+        recipient = Status.re_to.match(lines[n]).group(1)
+        n = n + 1
+
+        signature = Status.re_signature.match(lines[n]).group(1)
+        n = n + 1
+
+        return cls(version, currency, status, blockid,
+                   sender, recipient, signature)
+
+    def raw(self):
+        return '''Version: {0}
+Type: Status
+Currency: {1}
+Status: {2}
+Block: {3}
+From: {4}
+To: {5}
+'''.format(self.version, self.currency, self.status,
+           self.blockid, self.sender, self.recipient)
diff --git a/lib/ucoinpy/documents/transaction.py b/lib/ucoinpy/documents/transaction.py
new file mode 100644
index 00000000..e5fd76ed
--- /dev/null
+++ b/lib/ucoinpy/documents/transaction.py
@@ -0,0 +1,297 @@
+'''
+Created on 2 déc. 2014
+
+@author: inso
+'''
+
+from . import Document
+import re
+
+
+class Transaction(Document):
+    '''
+Document format :
+Version: VERSION
+Type: Transaction
+Currency: CURRENCY_NAME
+Issuers:
+PUBLIC_KEY
+...
+Inputs:
+INDEX:SOURCE:NUMBER:FINGERPRINT:AMOUNT
+...
+Outputs:
+PUBLIC_KEY:AMOUNT
+...
+Comment: COMMENT
+...
+
+
+Compact format :
+TX:VERSION:NB_ISSUERS:NB_INPUTS:NB_OUTPUTS:HAS_COMMENT
+PUBLIC_KEY:INDEX
+...
+INDEX:SOURCE:FINGERPRINT:AMOUNT
+...
+PUBLIC_KEY:AMOUNT
+...
+COMMENT
+SIGNATURE
+...
+    '''
+
+    re_type = re.compile("Type: (Transaction)\n")
+    re_header = re.compile("TX:([0-9])+:([0-9])+:([0-9])+:([0-9])+:(0|1)\n")
+    re_issuers = re.compile("Issuers:\n")
+    re_inputs = re.compile("Inputs:\n")
+    re_outputs = re.compile("Outputs:\n")
+    re_compact_comment = re.compile("-----@@@-----([^\n]+)\n")
+    re_comment = re.compile("Comment:(?:)?([^\n]*)\n")
+    re_pubkey = re.compile("([1-9A-Za-z][^OIl]{42,45})\n")
+
+    def __init__(self, version, currency, issuers, inputs, outputs,
+                 comment, signatures):
+        '''
+        Constructor
+        '''
+        if signatures:
+            super().__init__(version, currency, signatures)
+        else:
+            super().__init__(version, currency, [])
+
+        self.issuers = issuers
+        self.inputs = inputs
+        self.outputs = outputs
+        self.comment = comment
+
+    @classmethod
+    def from_compact(cls, currency, compact):
+        lines = compact.splitlines(True)
+        n = 0
+
+        header_data = Transaction.re_header.match(lines[n])
+        version = int(header_data.group(1))
+        issuers_num = int(header_data.group(2))
+        inputs_num = int(header_data.group(3))
+        outputs_num = int(header_data.group(4))
+        n = n + 1
+
+        issuers = []
+        inputs = []
+        outputs = []
+        signatures = []
+
+        for i in range(0, issuers_num):
+            issuer = Transaction.re_pubkey.match(lines[n]).group(1)
+            issuers.append(issuer)
+            n = n + 1
+
+        for i in range(0, inputs_num):
+            input_source = InputSource.from_inline(lines[n])
+            inputs.append(input_source)
+            n = n + 1
+
+        for i in range(0, outputs_num):
+            output_source = OutputSource.from_inline(lines[n])
+            outputs.append(output_source)
+            n = n + 1
+
+        comment = None
+        if Transaction.re_comment.match(lines[n]):
+            comment = Transaction.re_compact_comment.match(lines[n]).group(1)
+            n = n + 1
+
+        while n < len(lines):
+            signatures.append(Transaction.re_signature.match(lines[n]).group(1))
+            n = n + 1
+
+        return cls(version, currency, issuers, inputs, outputs, comment, signatures)
+
+    @classmethod
+    def from_signed_raw(cls, raw):
+        lines = raw.splitlines(True)
+        n = 0
+
+        version = int(Transaction.re_version.match(lines[n]).group(1))
+        n = n + 1
+
+        Transaction.re_type.match(lines[n]).group(1)
+        n = n + 1
+
+        currency = Transaction.re_currency.match(lines[n]).group(1)
+        n = n + 1
+
+        issuers = []
+        inputs = []
+        outputs = []
+        signatures = []
+
+        if Transaction.re_issuers.match(lines[n]):
+            n = n + 1
+            while Transaction.re_inputs.match(lines[n]) is None:
+                issuer = Transaction.re_pubkey.match(lines[n]).group(1)
+                issuers.append(issuer)
+                n = n + 1
+
+        if Transaction.re_inputs.match(lines[n]):
+            n = n + 1
+            while Transaction.re_outputs.match(lines[n]) is None:
+                input_source = InputSource.from_inline(lines[n])
+                inputs.append(input_source)
+                n = n + 1
+
+        if Transaction.re_outputs.match(lines[n]) is not None:
+            n = n + 1
+            while not Transaction.re_comment.match(lines[n]):
+                output = OutputSource.from_inline(lines[n])
+                outputs.append(output)
+                n = n + 1
+
+        comment = Transaction.re_comment.match(lines[n]).group(1)
+        n = n + 1
+
+        if Transaction.re_signature.match(lines[n]) is not None:
+            while n < len(lines):
+                sign = Transaction.re_signature.match(lines[n]).group(1)
+                signatures.append(sign)
+                n = n + 1
+
+        return cls(version, currency, issuers, inputs, outputs,
+                   comment, signatures)
+
+    def raw(self):
+        doc = """Version: {0}
+Type: Transaction
+Currency: {1}
+Issuers:
+""".format(self.version,
+                   self.currency)
+
+        for p in self.issuers:
+            doc += "{0}\n".format(p)
+
+        doc += "Inputs:\n"
+        for i in self.inputs:
+            doc += "{0}\n".format(i.inline())
+
+        doc += "Outputs:\n"
+        for o in self.outputs:
+            doc += "{0}\n".format(o.inline())
+
+        doc += "Comment: "
+        if self.comment:
+            doc += "{0}".format(self.comment)
+        doc += "\n"
+
+        for signature in self.signatures:
+            doc += "{0}\n".format(signature)
+
+        return doc
+
+    def compact(self):
+        '''
+        Return a transaction in its compact format.
+        '''
+        """TX:VERSION:NB_ISSUERS:NB_INPUTS:NB_OUTPUTS:HAS_COMMENT
+PUBLIC_KEY:INDEX
+...
+INDEX:SOURCE:FINGERPRINT:AMOUNT
+...
+PUBLIC_KEY:AMOUNT
+...
+COMMENT
+"""
+        doc = "TX:{0}:{1}:{2}:{3}:{4}".format(self.version,
+                                              self.issuers.len,
+                                              self.inputs.len,
+                                              self.outputs.len,
+                                              '1' if self.Comment else '0')
+        for pubkey in self.issuers:
+            doc += "{0}\n".format(pubkey)
+        for i in self.inputs:
+            doc += "{0}\n".format(i.compact())
+        for o in self.outputs:
+            doc += "{0}\n".format(o.inline())
+        if self.comment:
+            doc += "-----@@@----- {0}\n".format(self.comment)
+        for s in self.signatures:
+            doc += "{0}\n".format(s)
+
+        return doc
+
+
+class SimpleTransaction(Transaction):
+    '''
+As transaction class, but for only one issuer.
+...
+    '''
+    def __init__(self, version, currency, issuer,
+                 single_input, outputs, comment, signature):
+        '''
+        Constructor
+        '''
+        super().__init__(version, currency, [issuer], [single_input],
+              outputs, comment, [signature])
+
+
+class InputSource():
+    '''
+    A Transaction INPUT
+
+    Compact :
+    INDEX:SOURCE:FINGERPRINT:AMOUNT
+    '''
+    re_inline = re.compile("([0-9]+):(D|T):([0-9]+):\
+([0-9a-fA-F]{5,40}):([0-9]+)\n")
+    re_compact = re.compile("([0-9]+):(D|T):([0-9a-fA-F]{5,40}):([0-9]+)\n")
+
+    def __init__(self, index, source, number, txhash, amount):
+        self.index = index
+        self.source = source
+        self.number = number
+        self.txhash = txhash
+        self.amount = amount
+
+    @classmethod
+    def from_inline(cls, inline):
+        data = InputSource.re_inline.match(inline)
+        index = int(data.group(1))
+        source = data.group(2)
+        number = int(data.group(3))
+        txhash = data.group(4)
+        amount = int(data.group(5))
+        return cls(index, source, number, txhash, amount)
+
+    def inline(self):
+        return "{0}:{1}:{2}:{3}:{4}".format(self.index,
+                                            self.source,
+                                            self.number,
+                                            self.txhash,
+                                            self.amount)
+
+    def compact(self):
+        return "{0}:{1}:{2}:{3}".format(self.index,
+                                        self.source,
+                                        self.txhash,
+                                        self.amount)
+
+
+class OutputSource():
+    '''
+    A Transaction OUTPUT
+    '''
+    re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):([0-9]+)")
+
+    def __init__(self, pubkey, amount):
+        self.pubkey = pubkey
+        self.amount = amount
+
+    @classmethod
+    def from_inline(cls, inline):
+        data = OutputSource.re_inline.match(inline)
+        pubkey = data.group(1)
+        amount = int(data.group(2))
+        return cls(pubkey, amount)
+
+    def inline(self):
+        return "{0}:{1}".format(self.pubkey, self.amount)
diff --git a/lib/ucoinpy/key/__init__.py b/lib/ucoinpy/key/__init__.py
new file mode 100644
index 00000000..1733e547
--- /dev/null
+++ b/lib/ucoinpy/key/__init__.py
@@ -0,0 +1,36 @@
+'''
+Ucoin public and private keys
+
+@author: inso
+'''
+
+import base58
+import base64
+import scrypt
+from nacl.signing import SigningKey as NaclSigningKey
+
+
+SEED_LENGTH = 32  # Length of the key
+crypto_sign_BYTES = 64
+SCRYPT_PARAMS = {'N': 4096,
+                 'r': 16,
+                 'p': 1
+                 }
+
+
+class SigningKey(NaclSigningKey):
+    def __init__(self, password, salt):
+        seed = scrypt.hash(password, salt,
+                    SCRYPT_PARAMS['N'], SCRYPT_PARAMS['r'], SCRYPT_PARAMS['p'],
+                    SEED_LENGTH)
+        seedb64 = base64.b64encode(seed)
+
+
+class Base58Encoder(object):
+    @staticmethod
+    def encode(data):
+        return base58.b58encode(data)
+
+    @staticmethod
+    def decode(data):
+        return base58.b58decode(data)
diff --git a/lib/ucoinpy/key/hdwallet.py b/lib/ucoinpy/key/hdwallet.py
new file mode 100644
index 00000000..dffb5fff
--- /dev/null
+++ b/lib/ucoinpy/key/hdwallet.py
@@ -0,0 +1,378 @@
+'''
+HD Wallet inspired from Bip32 wallets.
+
+@author: inso
+'''
+'''
+import os
+import hmac
+import hashlib
+import ed25519
+import struct
+import base58
+import base64
+
+from hashlib import sha256
+from ecdsa.curves import SECP256k1
+from ecdsa.ecdsa import int_to_string, string_to_int
+from ecdsa.numbertheory import square_root_mod_prime as sqrt_mod
+
+MIN_ENTROPY_LEN = 128        # bits
+HDWALLET_HARDENED    = 0x80000000 # choose from hardened set of child keys
+CURVE_GEN       = ecdsa.ecdsa.generator_secp256k1
+CURVE_ORDER     = CURVE_GEN.order()
+FIELD_ORDER     = SECP256k1.curve.p()
+INFINITY        = ecdsa.ellipticcurve.INFINITY
+
+
+class HDWalletKey(object):
+
+    # Static initializers to create from entropy or external formats
+    #
+    @staticmethod
+    def fromEntropy(entropy, public=False):
+        "Create a HDWallet using supplied entropy >= MIN_ENTROPY_LEN"
+        if entropy == None:
+            entropy = os.urandom(MIN_ENTROPY_LEN/8) # Python doesn't have os.random()
+        if not len(entropy) >= MIN_ENTROPY_LEN/8:
+            raise ValueError("Initial entropy %i must be at least %i bits" %
+                                (len(entropy), MIN_ENTROPY_LEN))
+        I = hmac.new("UCoin seed", entropy, hashlib.sha512).digest()
+
+        Il, Ir = I[:32], I[32:]
+        # FIXME test Il for 0 or less than SECP256k1 prime field order
+        key = HDWalletKey(secret=Il, chain=Ir, depth=0, index=0, fpr='\0\0\0\0', public=False)
+        if public:
+            key.SetPublic()
+        return key
+
+    @staticmethod
+    def fromExtendedKey(xkey, public=False):
+        """
+        Create a HDWallet by importing from extended private or public key string
+
+        If public is True, return a public-only key regardless of input type.
+        """
+        # Sanity checks
+        raw = base58.b58decode_check(xkey)
+        # To fix
+        #if len(raw) != 78:
+        #    raise ValueError("extended key format wrong length")
+
+        # Verify address version/type
+        #version = raw[:4]
+        #if version == EX_MAIN_PRIVATE:
+        #    raise ValueError("unknown extended key version")
+
+        # Extract remaining fields
+        depth = ord(raw[4])
+        fpr = raw[5:9]
+        child = struct.unpack(">L", raw[9:13])[0]
+        chain = raw[13:45]
+        secret = raw[45:78]
+
+        # Extract private key or public key point
+        if keytype == 'xprv':
+            secret = secret[1:]
+        else:
+            # Recover public curve point from compressed key
+            lsb = ord(secret[0]) & 1
+            x = string_to_int(secret[1:])
+            ys = (x**3+7) % FIELD_ORDER # y^2 = x^3 + 7 mod p
+            y = sqrt_mod(ys, FIELD_ORDER)
+            if y & 1 != lsb:
+                y = FIELD_ORDER-y
+            point = ecdsa.ellipticcurve.Point(SECP256k1.curve, x, y)
+            secret = ecdsa.VerifyingKey.from_public_point(point, curve=SECP256k1)
+
+        is_pubkey = (keytype == 'xpub')
+        key = HDWalletKey(secret=secret, chain=chain, depth=depth, index=child,
+                          fpr=fpr, public=is_pubkey)
+        if not is_pubkey and public:
+            key = key.SetPublic()
+        return key
+
+
+    # Normal class initializer
+    def __init__(self, secret, chain, depth, index, fpr, public=False):
+        """
+        Create a public or private BIP32Key using key material and chain code.
+
+        secret   This is the source material to generate the keypair, either a
+                 32-byte string representation of a private key, or the ECDSA
+                 library object representing a public key.
+
+        chain    This is a 32-byte string representation of the chain code
+
+        depth    Child depth; parent increments its own by one when assigning this
+
+        index    Child index
+
+        fpr      Parent fingerprint
+
+        public   If true, this keypair will only contain a public key and can only create
+                 a public key chain.
+        """
+
+        self.public = public
+        if public is False:
+            self.k = ed25519.SigningKey(base58.b58decode(secret))
+            self.K = self.k.get_verifying_key()
+        else:
+            self.k = None
+            self.K = secret
+
+        self.C = chain
+        self.depth = depth
+        self.index = index
+        self.parent_fpr = fpr
+
+    # Internal methods not intended to be called externally
+    def _hmac(self, data):
+        """
+        Calculate the HMAC-SHA512 of input data using the chain code as key.
+
+        Returns a tuple of the left and right halves of the HMAC
+        """
+        I = hmac.new(self.C, data, hashlib.sha512).digest()
+        return (I[:32], I[32:])
+
+    def _CKDpriv(self, i):
+        """
+        Create a child key of index 'i'.
+
+        If the most significant bit of 'i' is set, then select from the
+        hardened key set, otherwise, select a regular child key.
+
+        Returns a BIP32Key constructed with the child key parameters,
+        or None if i index would result in an invalid key.
+        """
+        # Index as bytes, BE
+        i_str = struct.pack(">L", i)
+
+        # Data to HMAC
+        if i & HDWALLET_HARDENED:
+            data = b'\0' + self.k.to_string() + i_str
+        else:
+            data = self.PublicKey() + i_str
+        # Get HMAC of data
+        (Il, Ir) = self._hmac(data)
+
+        # Construct new key material from Il and current private key
+        Il_int = string_to_int(Il)
+        if Il_int > CURVE_ORDER:
+            return None
+        pvt_int = string_to_int(self.k.to_string())
+        k_int = (Il_int + pvt_int) % CURVE_ORDER
+        if (k_int == 0):
+            return None
+        secret = (b'\0'*32 + int_to_string(k_int))[-32:]
+
+        # Construct and return a new BIP32Key
+        return HDWalletKey(secret=secret, chain=Ir, depth=self.depth+1,
+                           index=i, fpr=self.Fingerprint(), public=False)
+
+    def _CKDpub(self, i):
+        """
+        Create a publicly derived child key of index 'i'.
+
+        If the most significant bit of 'i' is set, this is
+        an error.
+
+        Returns a HDWalletKey constructed with the child key parameters,
+        or None if index would result in invalid key.
+        """
+
+        if i & HDWALLET_HARDENED:
+            raise Exception("Cannot create a hardened child key using public child derivation")
+
+        # Data to HMAC.  Same as CKDpriv() for public child key.
+        data = self.PublicKey() + struct.pack(">L", i)
+
+        # Get HMAC of data
+        (Il, Ir) = self.hmac(data)
+
+        # Construct curve point Il*G+K
+        Il_int = string_to_int(Il)
+        if Il_int >= CURVE_ORDER:
+            return None
+        point = Il_int*CURVE_GEN + self.K.pubkey.point
+        if point == INFINITY:
+            return None
+
+        # Retrieve public key based on curve point
+        K_i = ed25519.VerifyingKey.from_public_point(point, curve=SECP256k1)
+
+        # Construct and return a new BIP32Key
+        return HDWalletKey(secret=K_i, chain=Ir, depth=self.depth, index=i, fpr=self.Fingerprint(), public=True)
+
+
+    # Public methods
+    #
+    def ChildKey(self, i):
+        """
+        Create and return a child key of this one at index 'i'.
+
+        The index 'i' should be summed with BIP32_HARDEN to indicate
+        to use the private derivation algorithm.
+        """
+        if self.public is False:
+            return self.CKDpriv(i)
+        else:
+            return self.CKDpub(i)
+
+
+    def SetPublic(self):
+        "Convert a private BIP32Key into a public one"
+        self.k = None
+        self.public = True
+
+
+    def PrivateKey(self):
+        "Return private key as string"
+        if self.public:
+            raise Exception("Publicly derived deterministic keys have no private half")
+        else:
+            return self.k.to_string()
+
+
+    def PublicKey(self):
+        "Return compressed public key encoding"
+        if self.K.pubkey.point.y() & 1:
+            ck = b'\3'+int_to_string(self.K.pubkey.point.x())
+        else:
+            ck = b'\2'+int_to_string(self.K.pubkey.point.x())
+        return ck
+
+
+    def ChainCode(self):
+        "Return chain code as string"
+        return self.C
+
+
+    def Identifier(self):
+        "Return key identifier as string"
+        cK = self.PublicKey()
+        return hashlib.new('ripemd160', sha256(cK).digest()).digest()
+
+
+    def Fingerprint(self):
+        "Return key fingerprint as string"
+        return self.Identifier()[:4]
+
+
+    def Address(self):
+        "Return compressed public key address"
+        vh160 = '\x00'+self.Identifier()
+        return Base58.check_encode(vh160)
+
+
+    def WalletImportFormat(self):
+        "Returns private key encoded for wallet import"
+        if self.public:
+            raise Exception("Publicly derived deterministic keys have no private half")
+        raw = '\x80' + self.k.to_string() + '\x01' # Always compressed
+        return Base58.check_encode(raw)
+
+
+    def ExtendedKey(self, private=True, encoded=True):
+        "Return extended private or public key as string, optionally Base58 encoded"
+        if self.public is True and private is True:
+            raise Exception("Cannot export an extended private key from a public-only deterministic key")
+        version = EX_MAIN_PRIVATE if private else EX_MAIN_PUBLIC
+        depth = chr(self.depth)
+        fpr = self.parent_fpr
+        child = struct.pack('>L', self.index)
+        chain = self.C
+        if self.public is True or private is False:
+            data = self.PublicKey()
+        else:
+            data = '\x00' + self.PrivateKey()
+        raw = version+depth+fpr+child+chain+data
+        if not encoded:
+            return raw
+        else:
+            return Base58.check_encode(raw)
+
+    # Debugging methods
+    #
+    def dump(self):
+        "Dump key fields mimicking the BIP0032 test vector format"
+        print "   * Identifier"
+        print "     * (hex):      ", self.Identifier().encode('hex')
+        print "     * (fpr):      ", self.Fingerprint().encode('hex')
+        print "     * (main addr):", self.Address()
+        if self.public is False:
+            print "   * Secret key"
+            print "     * (hex):      ", self.PrivateKey().encode('hex')
+            print "     * (wif):      ", self.WalletImportFormat()
+        print "   * Public key"
+        print "     * (hex):      ", self.PublicKey().encode('hex')
+        print "   * Chain code"
+        print "     * (hex):      ", self.C.encode('hex')
+        print "   * Serialized"
+        print "     * (pub hex):  ", self.ExtendedKey(private=False, encoded=False).encode('hex')
+        print "     * (prv hex):  ", self.ExtendedKey(private=True, encoded=False).encode('hex')
+        print "     * (pub b58):  ", self.ExtendedKey(private=False, encoded=True)
+        print "     * (prv b58):  ", self.ExtendedKey(private=True, encoded=True)
+
+
+if __name__ == "__main__":
+    import sys
+
+    # BIP0032 Test vector 1
+    entropy='000102030405060708090A0B0C0D0E0F'.decode('hex')
+    m = BIP32Key.fromEntropy(entropy)
+    print "Test vector 1:"
+    print "Master (hex):", entropy.encode('hex')
+    print "* [Chain m]"
+    m.dump()
+
+    print "* [Chain m/0h]"
+    m = m.ChildKey(0+BIP32_HARDEN)
+    m.dump()
+
+    print "* [Chain m/0h/1]"
+    m = m.ChildKey(1)
+    m.dump()
+
+    print "* [Chain m/0h/1/2h]"
+    m = m.ChildKey(2+BIP32_HARDEN)
+    m.dump()
+
+    print "* [Chain m/0h/1/2h/2]"
+    m = m.ChildKey(2)
+    m.dump()
+
+    print "* [Chain m/0h/1/2h/2/1000000000]"
+    m = m.ChildKey(1000000000)
+    m.dump()
+
+    # BIP0032 Test vector 2
+    entropy = 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542'.decode('hex')
+    m = BIP32Key.fromEntropy(entropy)
+    print "Test vector 2:"
+    print "Master (hex):", entropy.encode('hex')
+    print "* [Chain m]"
+    m.dump()
+
+    print "* [Chain m/0]"
+    m = m.ChildKey(0)
+    m.dump()
+
+    print "* [Chain m/0/2147483647h]"
+    m = m.ChildKey(2147483647+BIP32_HARDEN)
+    m.dump()
+
+    print "* [Chain m/0/2147483647h/1]"
+    m = m.ChildKey(1)
+    m.dump()
+
+    print "* [Chain m/0/2147483647h/1/2147483646h]"
+    m = m.ChildKey(2147483646+BIP32_HARDEN)
+    m.dump()
+
+    print "* [Chain m/0/2147483647h/1/2147483646h/2]"
+    m = m.ChildKey(2)
+    m.dump()
+'''
\ No newline at end of file
-- 
GitLab