Initial Commit
diff --git a/INSTALL.sh b/INSTALL.sh
new file mode 100755
index 0000000..a1868c4
--- /dev/null
+++ b/INSTALL.sh
@@ -0,0 +1,10 @@
+sudo apt-get install rapidjson-dev -y
+
+# for pybind
+sudo add-apt-repository ppa:deadsnakes/ppa
+sudo apt-get install python3.10-dev -y
+sudo apt-get install python3-dev -y
+sudo apt install python3.10-venv -y
+
+# for crow
+sudo apt-get install libboost-dev -y
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e665579
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+cryptoconditions==0.8.1
+pysha3==1.0.2
+python-rapidjson==1.8
+requests==2.28.1
\ No newline at end of file
diff --git a/resdb_driver/.DS_Store b/resdb_driver/.DS_Store
new file mode 100644
index 0000000..34c7308
--- /dev/null
+++ b/resdb_driver/.DS_Store
Binary files differ
diff --git a/resdb_driver/__init__.py b/resdb_driver/__init__.py
new file mode 100644
index 0000000..7f81314
--- /dev/null
+++ b/resdb_driver/__init__.py
@@ -0,0 +1 @@
+from .driver import Resdb
diff --git a/resdb_driver/__pycache__/__init__.cpython-310.pyc b/resdb_driver/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..85d4e6a
--- /dev/null
+++ b/resdb_driver/__pycache__/__init__.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/connection.cpython-310.pyc b/resdb_driver/__pycache__/connection.cpython-310.pyc
new file mode 100644
index 0000000..e8e7c4b
--- /dev/null
+++ b/resdb_driver/__pycache__/connection.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/crypto.cpython-310.pyc b/resdb_driver/__pycache__/crypto.cpython-310.pyc
new file mode 100644
index 0000000..c8f40bb
--- /dev/null
+++ b/resdb_driver/__pycache__/crypto.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/driver.cpython-310.pyc b/resdb_driver/__pycache__/driver.cpython-310.pyc
new file mode 100644
index 0000000..7722e96
--- /dev/null
+++ b/resdb_driver/__pycache__/driver.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/exceptions.cpython-310.pyc b/resdb_driver/__pycache__/exceptions.cpython-310.pyc
new file mode 100644
index 0000000..80ae021
--- /dev/null
+++ b/resdb_driver/__pycache__/exceptions.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/offchain.cpython-310.pyc b/resdb_driver/__pycache__/offchain.cpython-310.pyc
new file mode 100644
index 0000000..d3ecfeb
--- /dev/null
+++ b/resdb_driver/__pycache__/offchain.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/pool.cpython-310.pyc b/resdb_driver/__pycache__/pool.cpython-310.pyc
new file mode 100644
index 0000000..e973073
--- /dev/null
+++ b/resdb_driver/__pycache__/pool.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/transaction.cpython-310.pyc b/resdb_driver/__pycache__/transaction.cpython-310.pyc
new file mode 100644
index 0000000..81424fa
--- /dev/null
+++ b/resdb_driver/__pycache__/transaction.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/transport.cpython-310.pyc b/resdb_driver/__pycache__/transport.cpython-310.pyc
new file mode 100644
index 0000000..dbb98f9
--- /dev/null
+++ b/resdb_driver/__pycache__/transport.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/__pycache__/utils.cpython-310.pyc b/resdb_driver/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000..d55e42f
--- /dev/null
+++ b/resdb_driver/__pycache__/utils.cpython-310.pyc
Binary files differ
diff --git a/resdb_driver/connection.py b/resdb_driver/connection.py
new file mode 100644
index 0000000..a99ff41
--- /dev/null
+++ b/resdb_driver/connection.py
@@ -0,0 +1,151 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+import time
+
+from collections import namedtuple
+from datetime import datetime, timedelta
+
+from requests import Session
+from requests.exceptions import ConnectionError
+
+from .exceptions import HTTP_EXCEPTIONS, TransportError
+
+
+BACKOFF_DELAY = 0.5  # seconds
+
+HttpResponse = namedtuple("HttpResponse", ("status_code", "headers", "data"))
+
+
+class Connection:
+    """! A Connection object to make HTTP requests to a particular node."""
+
+    def __init__(self, *, node_url: str, headers: dict = None):
+        """! Initializes a :class:`~resdb_driver.connection.Connection`
+        instance.
+
+            @param node_url (str): Url of the node to connect to.
+            @param headers (dict): Optional headers to send with each request.
+
+            @return An instance of the Connection class 
+        """
+        self.node_url = node_url
+        self.session = Session()
+        if headers:
+            self.session.headers.update(headers)
+
+        self._retries = 0
+        self.backoff_time = None
+
+    def request(
+        self,
+        method: str,
+        *,
+        path: str = None,
+        json: dict = None,
+        params: dict = None,
+        headers: dict = None,
+        timeout: int = None,
+        backoff_cap: int = None,
+        **kwargs
+    ) -> HttpResponse:
+        """! Performs an HTTP request with the given parameters. Implements exponential backoff.
+
+        If `ConnectionError` occurs, a timestamp equal to now +
+        the default delay (`BACKOFF_DELAY`) is assigned to the object.
+        The timestamp is in UTC. Next time the function is called, it either
+        waits till the timestamp is passed or raises `TimeoutError`.
+
+        If `ConnectionError` occurs two or more times in a row,
+        the retry count is incremented and the new timestamp is calculated
+        as now + the default delay multiplied by two to the power of the
+        number of retries.
+
+        If a request is successful, the backoff timestamp is removed,
+        the retry count is back to zero.
+
+        @param method (str): HTTP method (e.g.: ``'GET'``).
+        @param path (str): API endpoint path (e.g.: ``'/transactions'``).
+        @param json (dict): JSON data to send along with the request.
+        @param params (dict): Dictionary of URL (query) parameters.
+        @param headers (dict): Optional headers to pass to the request.
+        @param timeout (int): Optional timeout in seconds.
+        @param backoff_cap (int): The maximal allowed backoff delay in seconds to be assigned to a node.
+        @param kwargs: Optional keyword arguments.
+
+        @return Response of the HTTP request.
+        """
+
+        backoff_timedelta = self.get_backoff_timedelta()
+
+        if timeout is not None and timeout < backoff_timedelta:
+            raise TimeoutError
+
+        if backoff_timedelta > 0:
+            time.sleep(backoff_timedelta)
+
+        connExc = None
+        timeout = timeout if timeout is None else timeout - backoff_timedelta
+        try:
+            response = self._request(
+                method=method,
+                timeout=timeout,
+                url=self.node_url + path if path else self.node_url,
+                json=json,
+                params=params,
+                headers=headers,
+                **kwargs,
+            )
+        except ConnectionError as err:
+            connExc = err
+            raise err
+        finally:
+            self.update_backoff_time(
+                success=connExc is None, backoff_cap=backoff_cap)
+        return response
+
+    def get_backoff_timedelta(self) -> float:
+        if self.backoff_time is None:
+            return 0
+
+        return (self.backoff_time - datetime.utcnow()).total_seconds()
+
+    def update_backoff_time(self, success, backoff_cap=None):
+        if success:
+            self._retries = 0
+            self.backoff_time = None
+        else:
+            utcnow = datetime.utcnow()
+            backoff_delta = BACKOFF_DELAY * 2**self._retries
+            if backoff_cap is not None:
+                backoff_delta = min(backoff_delta, backoff_cap)
+            self.backoff_time = utcnow + timedelta(seconds=backoff_delta)
+            self._retries += 1
+
+    def _request(self, **kwargs) -> HttpResponse:
+        response = self.session.request(**kwargs)
+        text = response.text
+        try:
+            json = response.json()
+        except ValueError:
+            json = None
+        if not (200 <= response.status_code < 300):
+            exc_cls = HTTP_EXCEPTIONS.get(response.status_code, TransportError)
+            raise exc_cls(response.status_code, text, json, kwargs["url"])
+        data = json if json is not None else text
+        return HttpResponse(response.status_code, response.headers, data)
diff --git a/resdb_driver/crypto.py b/resdb_driver/crypto.py
new file mode 100644
index 0000000..28ddaec
--- /dev/null
+++ b/resdb_driver/crypto.py
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+
+from collections import namedtuple
+from cryptoconditions import crypto
+
+import sha3
+
+CryptoKeypair = namedtuple("CryptoKeypair", ("private_key", "public_key"))
+
+
+def generate_keypair(seed=None) -> CryptoKeypair:
+    """! Generates a cryptographic key pair.
+
+    @param seed (bytes): 32-byte seed for deterministic generation. Defaults to `None`.
+
+    @return collections.namedtuple object containing the public and private keys.
+    """
+
+    return CryptoKeypair(*(k.decode() for k in crypto.ed25519_generate_key_pair(seed)))
+
+
+def hash_data(data) -> sha3.sha3_256:
+    """! Hash the provided data using SHA3-256
+
+    @param data Data to be hashed using SHA3-256
+
+    @return Hashed data
+    """
+    # print(f"HASH DATA {type(data)}, {data=}")
+    return sha3.sha3_256(data.encode()).hexdigest()
+
+
+PrivateKey = crypto.Ed25519SigningKey
+PublicKey = crypto.Ed25519VerifyingKey
diff --git a/resdb_driver/driver.py b/resdb_driver/driver.py
new file mode 100644
index 0000000..e7af548
--- /dev/null
+++ b/resdb_driver/driver.py
@@ -0,0 +1,543 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+from crypt import methods
+from curses import meta
+from .transport import Transport
+from .offchain import prepare_transaction, fulfill_transaction
+from .utils import normalize_nodes
+from typing import Any, Union
+
+
+class Resdb:
+    """! A :class:`~resdb_driver.Resdb` driver is able to create, sign,
+    and submit transactions to one or more nodes in a Federation.
+
+    If initialized with ``>1`` nodes, the driver will send successive
+    requests to different nodes in a round-robin fashion (this will be
+    customizable in the future).
+
+    """
+
+    def __init__(
+        self,
+        *nodes: list[Union[str, dict]],
+        transport_class: Transport = Transport,
+        headers=None,
+        timeout=20
+    ):
+        """! Initialize a :class:`~resdb_driver.Resdb` driver instance.
+
+        @param *nodes (list of (str or dict)): Resdb nodes to connect to.
+                Currently, the full URL must be given. In the absence of any
+                node, the default(``'http://localhost:9984'``) will be used.
+                If node is passed as a dict, `endpoint` is a required key;
+                `headers` is an optional `dict` of headers.
+        @param transport_class Optional transport class to use.
+                Defaults to :class:`~resdb_driver.transport.Transport`.
+        @param headers (dict): Optional headers that will be passed with
+                each request. To pass headers only on a per-request
+                basis, you can pass the headers to the method of choice
+                (e.g. :meth:`Resdb().transactions.send_commit()
+                <.TransactionsEndpoint.send_commit>`).
+        @param timeout (int): Optional timeout in seconds that will be passed
+                to each request.
+
+        @return An instance of the Resdb class
+        """
+        self._nodes = normalize_nodes(*nodes, headers=headers)
+        self._transport = transport_class(*self._nodes, timeout=timeout)
+        self._transactions = TransactionsEndpoint(self)
+        self._outputs = OutputsEndpoint(self)
+        self._blocks = BlocksEndpoint(self)
+        self._assets = AssetsEndpoint(self)
+        self._metadata = MetadataEndpoint(self)
+        self.api_prefix = "/v1"
+
+    @property
+    def nodes(self):
+        """! :obj:`tuple` of :obj:`str`: URLs of connected nodes.
+        """
+        return self._nodes
+
+    @property
+    def transport(self):
+        """! :class:`~resdb_driver.transport.Transport`: Object
+        responsible for forwarding requests to a
+        :class:`~resdb_driver.connection.Connection` instance (node).
+        """
+        return self._transport
+
+    @property
+    def transactions(self):
+        """! :class:`~resdb_driver.driver.TransactionsEndpoint`:
+        Exposes functionalities of the ``'/transactions'`` endpoint.
+        TODO: check
+        """
+        return self._transactions
+
+    @property
+    def outputs(self):
+        """! :class:`~resdb_driver.driver.OutputsEndpoint`:
+        Exposes functionalities of the ``'/outputs'`` endpoint.
+        TODO: check
+        """
+        return self._outputs
+
+    @property
+    def assets(self):
+        """! :class:`~resdb_driver.driver.AssetsEndpoint`:
+        Exposes functionalities of the ``'/assets'`` endpoint.
+        TODO: check
+        """
+        return self._assets
+
+    @property
+    def metadata(self):
+        """! :class:`~resdb_driver.driver.MetadataEndpoint`:
+        Exposes functionalities of the ``'/metadata'`` endpoint.
+        TODO: check 
+        """
+        return self._metadata
+
+    @property
+    def blocks(self):
+        """! :class:`~resdb_driver.driver.BlocksEndpoint`:
+        Exposes functionalities of the ``'/blocks'`` endpoint.
+        TODO: check
+        """
+        return self._blocks
+
+    def info(self, headers=None) -> dict:
+        """! Retrieves information of the node being connected to via the
+        root endpoint ``'/'``.
+
+        Note:
+            TODO: implement the endpoint in the node (Single node)
+
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return Details of the node that this instance is connected
+        to. Some information that may be interesting - Server version, overview of all the endpoints
+        """
+
+        return self.transport.forward_request(method="GET", path="/", headers=headers)
+
+    def api_info(self, headers=None) -> dict:
+        """! Retrieves information provided by the API root endpoint
+        ``'/api/v1'``.
+
+        TODO: implement the endpoint in the node 
+
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return Details of the HTTP API provided by the Resdb
+        server.
+        """
+        return self.transport.forward_request(
+            method="GET",
+            path=self.api_prefix,
+            headers=headers,
+        )
+
+    def get_transaction(self, txid):
+        # TODO
+        # use transactions.retieve() instead
+        # return self._transactions.retrieve(txid=txid)
+        raise NotImplementedError
+
+
+class NamespacedDriver:
+    """! Base class for creating endpoints (namespaced objects) that can be added
+    under the :class:`~resdb_driver.driver.Resdb` driver.
+    """
+
+    PATH = "/"
+
+    def __init__(self, driver: Resdb):
+        """! Initializes an instance of
+            :class:`~resdb_driver.driver.NamespacedDriver` with the given
+            driver instance.
+
+        @param driver (Resdb): Instance of :class:`~resdb_driver.driver.Resdb`.
+
+        @return An instance of the NamespacedDriver class.
+        """
+        self.driver = driver
+
+    @property
+    def transport(self) -> Transport:
+        return self.driver.transport
+
+    @property
+    def api_prefix(self) -> str:
+        return self.driver.api_prefix
+
+    @property
+    def path(self) -> str:
+        return self.api_prefix + self.PATH
+
+
+class TransactionsEndpoint(NamespacedDriver):
+    """! Exposes functionality of the ``'/transactions/'`` endpoint.
+
+    Attributes:
+        path (str): The path of the endpoint.
+
+    """
+
+    PATH = "/transactions/"
+
+    @staticmethod
+    def prepare(
+        *,
+        operation="CREATE",
+        signers=None,
+        recipients=None,
+        asset=None,
+        metadata=None,
+        inputs=None
+    ) -> dict:
+        """! Prepares a transaction payload, ready to be fulfilled.
+
+        @param operation (str): The operation to perform. Must be ``'CREATE'``
+                or ``'TRANSFER'``. Case insensitive. Defaults to ``'CREATE'``.
+        @param signers (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): One or more public keys representing the issuer(s) of
+                the asset being created. Only applies for ``'CREATE'``
+                operations. Defaults to ``None``.
+        @param recipients (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): One or more public keys representing the new recipients(s)
+                of the asset being created or transferred. Defaults to ``None``.
+        @param asset (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): The asset to be created or transferred. MUST be supplied 
+                for ``'TRANSFER'`` operations. Defaults to ``None``.
+        @param metadata (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): Metadata associated with the transaction. Defaults to ``None``.
+        @param inputs (:obj:`dict` | :obj:`list` | :obj:`tuple`, optional): One or more inputs holding the condition(s) that this
+                transaction intends to fulfill. Each input is expected to
+                be a :obj:`dict`. Only applies to, and MUST be supplied for,
+                ``'TRANSFER'`` operations.
+
+        @return The prepared transaction (dict)
+
+        @exception :class:`~.exceptions.ResdbException`: If ``operation`` is
+            not ``'CREATE'`` or ``'TRANSFER'``.
+
+        .. important::
+
+            **CREATE operations**
+
+            * ``signers`` MUST be set.
+            * ``recipients``, ``asset``, and ``metadata`` MAY be set.
+            * If ``asset`` is set, it MUST be in the form of::
+
+                {
+                    'data': {
+                        ...
+                    }
+                }
+
+            * The argument ``inputs`` is ignored.
+            * If ``recipients`` is not given, or evaluates to
+              ``False``, it will be set equal to ``signers``::
+
+                if not recipients:
+                    recipients = signers
+
+            **TRANSFER operations**
+
+            * ``recipients``, ``asset``, and ``inputs`` MUST be set.
+            * ``asset`` MUST be in the form of::
+
+                {
+                    'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
+                }
+
+            * ``metadata`` MAY be set.
+            * The argument ``signers`` is ignored.
+
+        """
+        return prepare_transaction(
+            operation=operation,
+            signers=signers,
+            recipients=recipients,
+            asset=asset,
+            metadata=metadata,
+            inputs=inputs,
+        )
+
+    @staticmethod
+    def fulfill(
+        transaction: dict, private_keys: Union[str, list, tuple]
+    ) -> dict[str, Any]:
+        """! Fulfills the given transaction.
+
+        @param transaction (dict): The transaction to be fulfilled.
+        @param private_keys (:obj:`str` | :obj:`list` | :obj:`tuple`): One or more private keys to be 
+                used for fulfilling the transaction.
+
+        @return The fulfilled transaction payload, ready to 
+                be sent to a Resdb federation.
+
+        @exception :exc:`~.exceptions.MissingPrivateKeyError`: If a private
+                key is missing.
+        """
+        return fulfill_transaction(transaction, private_keys=private_keys)
+
+    def get(self, *, asset_id, operation=None, headers=None) -> list:
+        """! Given an asset id, get its list of transactions (and
+        optionally filter for only ``'CREATE'`` or ``'TRANSFER'``
+        transactions).
+
+        Note:
+            Please note that the id of an asset in Resdb is
+            actually the id of the transaction which created the asset.
+            In other words, when querying for an asset id with the
+            operation set to ``'CREATE'``, only one transaction should
+            be expected. This transaction will be the transaction in
+            which the asset was created, and the transaction id will be
+            equal to the given asset id. Hence, the following calls to
+            :meth:`.retrieve` and :meth:`.get` should return the same
+            transaction.
+
+                >>> resdb = Resdb()
+                >>> resdb.transactions.retrieve('foo')
+                >>> resdb.transactions.get(asset_id='foo', operation='CREATE')
+
+            Since :meth:`.get` returns a list of transactions, it may
+            be more efficient to use :meth:`.retrieve` instead, if one
+            is only interested in the ``'CREATE'`` operation.
+
+        @param asset_id (str): Id of the asset.
+        @param operation (str): The type of operation the transaction
+                should be. Either ``'CREATE'`` or ``'TRANSFER'``.
+                Defaults to ``None``.
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return List of transactions
+        """
+
+        return self.transport.forward_request(
+            method="GET",
+            path=self.path,
+            params={"asset_id": asset_id, "operation": operation},
+            headers=headers,
+        )
+
+    def send_async(self, transaction, headers=None):
+        """!
+        Note:
+            Not used in resdb
+        Submit a transaction to the Federation with the mode `async`.
+
+            @param transaction (dict): The transaction to be sent
+                to the Federation node(s).
+            @param headers (dict): Optional headers to pass to the request.
+
+            @return The transaction sent to the Federation node(s).
+
+        """
+        # return self.transport.forward_request(
+        #     method="POST",
+        #     path=self.path,
+        #     json=transaction,
+        #     params={"mode": "async"},
+        #     headers=headers,
+        # )
+        raise NotImplementedError
+
+    def send_sync(self, transaction, headers=None):
+        """! Note:
+        Not used in resdb
+        Submit a transaction to the Federation with the mode `sync`.
+
+        @param transaction (dict): The transaction to be sent
+            to the Federation node(s).
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return The transaction sent to the Federation node(s).
+
+        """
+        # return self.transport.forward_request(
+        #     method="POST",
+        #     path=self.path,
+        #     json=transaction,
+        #     params={"mode": "sync"},
+        #     headers=headers,
+        # )
+        raise NotImplementedError
+
+    def send_commit(self, transaction: dict, headers: dict = None) -> dict:
+        """! Submit a transaction to the Federation with the mode `commit`.
+
+        @param transaction (dict): The transaction to be sent
+            to the Federation node(s).
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return The transaction sent to the Federation node(s).
+        """
+        # return self.transport.forward_request(
+        #     method='POST',
+        #     path=self.path,
+        #     json=transaction,
+        #     params={'mode': 'commit'},
+        #     headers=headers)
+        function = "commit"
+        path = self.path + function
+        return self.transport.forward_request(
+            method="POST", path=path, json=transaction, headers=headers
+        )
+
+    def retrieve(self, txid: str, headers: dict = None) -> dict:
+        """! Retrieves the transaction with the given id.
+
+        @param txid (str): Id of the transaction to retrieve.
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return The transaction with the given id.
+        """
+        path = self.path + txid
+        return self.transport.forward_request(method="GET", path=path, headers=None)
+
+
+class OutputsEndpoint(NamespacedDriver):
+    """! TODO:
+    add endpoint in nodes
+    Exposes functionality of the ``'/outputs'`` endpoint.
+
+    path (str): The path of the endpoint.
+    """
+
+    PATH = "/outputs/"
+
+    def get(self, public_key, spent=None, headers=None) -> list[dict]:
+        """! Get transaction outputs by public key. The public_key parameter
+        must be a base58 encoded ed25519 public key associated with
+        transaction output ownership.
+
+        Example:
+            Given a transaction with `id` ``da1b64a907ba54`` having an
+            `ed25519` condition (at index ``0``) with alice's public
+            key::
+
+                >>> resdb = Resdb()
+                >>> resdb.outputs.get(alice_pubkey)
+                ... ['../transactions/da1b64a907ba54/conditions/0']
+
+        @param public_key (str): Public key for which unfulfilled
+            conditions are sought.
+        @param spent (bool): Indicate if the result set should include only spent
+            or only unspent outputs. If not specified (``None``) the
+            result includes all the outputs (both spent and unspent)
+            associated with the public key.
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return List of unfulfilled conditions.
+        """
+
+        return self.transport.forward_request(
+            method="GET",
+            path=self.path,
+            params={"public_key": public_key, "spent": spent},
+            headers=headers,
+        )
+
+
+class BlocksEndpoint(NamespacedDriver):
+    """! Exposes functionality of the ``'/blocks'`` endpoint.
+
+    Attributes:
+        path (str): The path of the endpoint.
+
+    """
+
+    PATH = "/blocks/"
+
+    def get(self, *, txid, headers=None) -> list[dict]:
+        """! TODO:
+        Add endpoints in nodes. transaction_id is a query parameter here
+        Get the block that contains the given transaction id (``txid``)
+        else return ``None``
+
+        @param txid (str): Transaction id.
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return List of block heights.
+
+        """
+        block_list = self.transport.forward_request(
+            method="GET",
+            path=self.path,
+            params={"transaction_id": txid},
+            headers=headers,
+        )
+        return block_list[0] if len(block_list) else None
+
+
+class AssetsEndpoint(NamespacedDriver):
+    """! Exposes functionality of the ``'/assets'`` endpoint.
+
+        path (str): The path of the endpoint.
+    """
+
+    PATH = "/assets/"
+
+    def get(self, *, search, limit=0, headers=None) -> list[dict]:
+        """! TODO:
+        add endpoints in nodes. transaction_id is a query parameter here
+        Retrieves the assets that match a given text search string.
+
+        @param search (str): Text search string.
+        @param limit (int): Limit the number of returned documents. Defaults to
+            zero meaning that it returns all the matching assets.
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return List of assets that match the query.
+        """
+
+        return self.transport.forward_request(
+            method="GET",
+            path=self.path,
+            params={"search": search, "limit": limit},
+            headers=headers,
+        )
+
+
+class MetadataEndpoint(NamespacedDriver):
+    """! Exposes functionality of the ``'/metadata'`` endpoint.
+
+    path (str): The path of the endpoint.
+
+    """
+
+    PATH = "/metadata/"
+
+    def get(self, *, search, limit=0, headers=None) -> list[dict]:
+        """! Retrieves the metadata that match a given text search string.
+
+        @param search (str): Text search string.
+        @param limit (int): Limit the number of returned documents. Defaults to
+            zero meaning that it returns all the matching metadata.
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return List of metadata that match the query.
+
+        """
+        return self.transport.forward_request(
+            method="GET",
+            path=self.path,
+            params={"search": search, "limit": limit},
+            headers=headers,
+        )
diff --git a/resdb_driver/driver_experiment.py b/resdb_driver/driver_experiment.py
new file mode 100644
index 0000000..fb43ef2
--- /dev/null
+++ b/resdb_driver/driver_experiment.py
@@ -0,0 +1,292 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+#%%[markdown]
+## Overview
+"""
+3 main steps:
+    1. prepare the payload
+    2. Fulfilling the prepared transaction payload
+    3. send the set command over the c++ tcp endpoint
+
+- Transaction must be signed before being sent, the fulfillment must be provided by the client.
+- Cryptographic signatures, the payloads need to be fully prepared and signed on the client side. This prevents the server(s) from tampering with the provided data.
+## Transaction component
+```
+{
+    "id": id,
+    "version": version,
+    "inputs": inputs,
+    "outputs": outputs,
+    "operation": operation,
+    "asset": asset,
+    "metadata": metadata
+}```
+
+Transaction inputs and outputs are the mechanism by which control or ownership of an asset is transferred
+
+### Input overview
+an input is a pointer to an output of a previous transaction. It specifies to whom an asset belonged before and it provides a proof that the conditions required to transfer the ownership of that asset (e.g. a person needs to sign) are fulfilled. In a CREATE transaction, there is no previous owner, so an input in a CREATE transaction simply specifies who the person is that is registering the object (this is usually the same as the initial owner of the asset). In a TRANSFER transaction, an input contains a proof that the user is authorized to “spend” (transfer or update) this particular output. In practical terms, this means that with the input, a user is stating which asset (e.g. the bike) should be transferred. He also demonstrates that he or she is authorized to do the transfer of that asset
+
+### Output overview
+# A transaction output specifies the conditions that need to be fulfilled to change the ownership of a specific asset. For instance: to transfer a NFT, a person needs to sign the transaction with his or her private key. This also implicitly contains the information that the public key associated with that private key is the current owner of the asset.
+
+
+
+"""
+#%%[markdown]
+## creating user
+from email import message
+from cryptoconditions import Ed25519Sha256
+import base58
+from resdb_driver.crypto import generate_keypair
+
+alice = generate_keypair()
+
+
+#%%[markdown]
+## Prepare transaction
+#%%
+### operation
+operation = "CREATE"
+
+### asset
+asset = {
+    "data": {
+        "NFT": {
+            "size": "10x10",
+            "hash": "abcd1234",
+        },
+    },
+}
+
+### metadata
+metadata = {"price": "$10"}
+
+#%%
+### outputs
+"""
+The purpose of the output condition is to lock the transaction, such that a valid input fulfillment is required to unlock it. In the case of signature-based schemes, the lock is basically a public key, such that in order to unlock the transaction one needs to have the private key.
+
+"""
+
+ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))
+
+#%%
+ed25519.condition_uri
+
+"""
+  +-----------+--------------------------------------+----------------+
+   | Field     | Value                                | Description    |
+   +-----------+--------------------------------------+----------------+
+   | scheme    | "ni:///"                             | The named      |
+   |           |                                      | information    |
+   |           |                                      | scheme.        |
+   |           |                                      |                |
+   | hash      | "sha-256"                            | The            |
+   | function  |                                      | fingerprint is |
+   | name      |                                      | hashed with    |
+   |           |                                      | the SHA-256    |
+   |           |                                      | digest         |
+   |           |                                      | function       |
+   |           |                                      |                |
+   | fingerpri | "f4OxZX_x_FO5LcGBSKHWXfwtSx-         | The            |
+   | nt        | j1ncoSt3SABJtkGk"                    | fingerprint    |
+   |           |                                      | for this       |
+   |           |                                      | condition.     |
+   |           |                                      |                |
+   | type      | "preimage-sha-256"                   | This is a      |
+   |           |                                      | PREIMAGE-      |
+   |           |                                      | SHA-256        |
+   |           |                                      | (Section 8.1)  |
+   |           |                                      | condition.     |
+   |           |                                      |                |
+   | cost      | "12"                                 | The            |
+   |           |                                      | fulfillment    |
+   |           |                                      | payload is 12  |
+   |           |                                      | bytes long,    |
+   |           |                                      | therefore the  |
+   |           |                                      | cost is 12.    |
+   +-----------+--------------------------------------+----------------+
+
+   https://datatracker.ietf.org/doc/html/draft-thomas-crypto-conditions-02#section-8.1
+
+"""
+# So now we have a condition URI for Alice’s public key.
+
+#%%
+condition_details = {
+    "type": ed25519.TYPE_NAME,
+    "public_key": base58.b58encode(ed25519.public_key).decode(),
+}
+
+output = {
+    "amount": "1",
+    "condition": {
+        "details": condition_details,
+        "uri": ed25519.condition_uri,
+    },
+    "public_keys": (alice.public_key,),  # TODO make it a single value
+}
+outputs = (output,)
+
+## Each output indicates the crypto-conditions which must be satisfied by anyone wishing to spend/transfer that output.
+
+
+#%%
+# input
+
+input_ = {"fulfillment": None, "fulfills": None, "owners_before": (alice.public_key,)}
+
+"""
+The fulfills field is empty because it’s a CREATE operation;
+The 'fulfillment' value is None as it will be set during the fulfillment step; and
+The 'owners_before' field identifies the issuer(s) of the asset that is being created.
+"""
+
+inputs = (input_,)
+
+#%%
+
+creation_tx = {
+    "asset": asset,
+    "metadata": metadata,
+    "operation": operation,
+    "outputs": outputs,
+    "inputs": inputs,
+    "id": None,
+}
+
+creation_tx
+
+## Id will be set during the fulfillment step
+
+#%%[markdown]
+## fulfilled transaction
+
+#%%
+from sha3 import sha3_256
+import json
+from cryptoconditions.crypto import Ed25519SigningKey
+
+ed25519.to_dict()
+message = json.dumps(
+    creation_tx, sort_keys=True, separators=(",", ":"), ensure_ascii=False
+)
+
+message = sha3_256(message.encode())
+ed25519.sign(message.digest(), base58.b58decode(alice.private_key))
+fulfillment_uri = ed25519.serialize_uri()
+creation_tx["inputs"][0]["fulfillment"] = fulfillment_uri
+
+#%%
+# ID - SHA3-256 hash of the entire transaction
+json_str_tx = json.dumps(
+    creation_tx,
+    sort_keys=True,
+    separators=(",", ":"),
+    ensure_ascii=False,
+)
+creation_txid = sha3_256(json_str_tx.encode()).hexdigest()
+creation_tx["id"] = creation_txid
+
+
+#%%[markdown]
+## Transfer
+
+#%%
+import json
+
+import base58
+import sha3
+from cryptoconditions import Ed25519Sha256
+
+from resdb_driver.crypto import generate_keypair
+
+bob = generate_keypair()
+
+operation = "TRANSFER"
+asset = {"id": creation_tx["id"]}
+metadata = None
+
+ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))
+
+output = {
+    "amount": "1",
+    "condition": {
+        "details": {
+            "type": ed25519.TYPE_NAME,
+            "public_key": base58.b58encode(ed25519.public_key).decode(),
+        },
+        "uri": ed25519.condition_uri,
+    },
+    "public_keys": (bob.public_key,),
+}
+outputs = (output,)
+
+input_ = {
+    "fulfillment": None,
+    "fulfills": {
+        "transaction_id": creation_tx["id"],
+        "output_index": 0,
+    },
+    "owners_before": (alice.public_key,),
+}
+inputs = (input_,)
+
+transfer_tx = {
+    "asset": asset,
+    "metadata": metadata,
+    "operation": operation,
+    "outputs": outputs,
+    "inputs": inputs,
+    "id": None,
+}
+
+message = json.dumps(
+    transfer_tx,
+    sort_keys=True,
+    separators=(",", ":"),
+    ensure_ascii=False,
+)
+
+message = sha3.sha3_256(message.encode())
+
+message.update(
+    "{}{}".format(
+        transfer_tx["inputs"][0]["fulfills"]["transaction_id"],
+        transfer_tx["inputs"][0]["fulfills"]["output_index"],
+    ).encode()
+)
+
+ed25519.sign(message.digest(), base58.b58decode(alice.private_key))
+
+fulfillment_uri = ed25519.serialize_uri()
+
+transfer_tx["inputs"][0]["fulfillment"] = fulfillment_uri
+
+json_str_tx = json.dumps(
+    transfer_tx,
+    sort_keys=True,
+    separators=(",", ":"),
+    ensure_ascii=False,
+)
+
+transfer_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()
+
+transfer_tx["id"] = transfer_txid
diff --git a/resdb_driver/exceptions.py b/resdb_driver/exceptions.py
new file mode 100644
index 0000000..b59e1ad
--- /dev/null
+++ b/resdb_driver/exceptions.py
@@ -0,0 +1,199 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+class ResdbException(Exception):
+    """Base exception for all resdb exceptions."""
+
+
+class ResdbException(Exception):
+    """Base exception for all Resdb exceptions."""
+
+
+class MissingPrivateKeyError(ResdbException):
+    """Raised if a private key is missing."""
+
+
+class TransportError(ResdbException):
+    """Base exception for transport related errors.
+
+    This is mainly for cases where the status code denotes an HTTP error, and
+    for cases in which there was a connection error.
+
+    """
+
+    @property
+    def status_code(self):
+        return self.args[0]
+
+    @property
+    def error(self):
+        return self.args[1]
+
+    @property
+    def info(self):
+        return self.args[2]
+
+    @property
+    def url(self):
+        return self.args[3]
+
+
+class BadRequest(TransportError):
+    """Exception for HTTP 400 errors."""
+
+
+class NotFoundError(TransportError):
+    """Exception for HTTP 404 errors."""
+
+
+class ServiceUnavailable(TransportError):
+    """Exception for HTTP 503 errors."""
+
+
+class GatewayTimeout(TransportError):
+    """Exception for HTTP 503 errors."""
+
+
+class TimeoutError(ResdbException):
+    """Raised if the request algorithm times out."""
+
+    @property
+    def connection_errors(self):
+        """Returns connection errors occurred before timeout expired."""
+        return self.args[0]
+
+
+HTTP_EXCEPTIONS = {
+    400: BadRequest,
+    404: NotFoundError,
+    503: ServiceUnavailable,
+    504: GatewayTimeout,
+}
+
+
+class ResDBError(Exception):
+    """Base class for ResDB exceptions."""
+
+
+class ConfigurationError(ResDBError):
+    """Raised when there is a problem with server configuration"""
+
+
+class DatabaseAlreadyExists(ResDBError):
+    """Raised when trying to create the database but the db is already there"""
+
+
+class DatabaseDoesNotExist(ResDBError):
+    """Raised when trying to delete the database but the db is not there"""
+
+
+class StartupError(ResDBError):
+    """Raised when there is an error starting up the system"""
+
+
+class CyclicBlockchainError(ResDBError):
+    """Raised when there is a cycle in the blockchain"""
+
+
+class KeypairMismatchException(ResDBError):
+    """Raised if the private key(s) provided for signing don't match any of the
+    current owner(s)
+    """
+
+
+class OperationError(ResDBError):
+    """Raised when an operation cannot go through"""
+
+
+##############################################################################
+# Validation errors
+#
+# All validation errors (which are handleable errors, not faults) should
+# subclass ValidationError. However, where possible they should also have their
+# own distinct type to differentiate them from other validation errors,
+# especially for the purposes of testing.
+
+
+class ValidationError(ResDBError):
+    """Raised if there was an error in validation"""
+
+
+class DoubleSpend(ValidationError):
+    """Raised if a double spend is found"""
+
+
+class DoubleSpend(ValidationError):
+    """Raised if a double spend is found"""
+
+
+class InvalidHash(ValidationError):
+    """Raised if there was an error checking the hash for a particular
+    operation
+    """
+
+
+class InputDoesNotExist(ValidationError):
+    """Raised if a transaction input does not exist"""
+
+
+class SchemaValidationError(ValidationError):
+    """Raised if there was any error validating an object's schema"""
+
+
+class InvalidSignature(ValidationError):
+    """Raised if there was an error checking the signature for a particular
+    operation
+    """
+
+
+class TransactionNotInValidBlock(ValidationError):
+    """Raised when a transfer transaction is attempting to fulfill the
+    outputs of a transaction that is in an invalid or undecided block
+    """
+
+
+class AssetIdMismatch(ValidationError):
+    """Raised when multiple transaction inputs related to different assets"""
+
+
+class AmountError(ValidationError):
+    """Raised when there is a problem with a transaction's output amounts"""
+
+
+class InputDoesNotExist(ValidationError):
+    """Raised if a transaction input does not exist"""
+
+
+class TransactionOwnerError(ValidationError):
+    """Raised if a user tries to transfer a transaction they don't own"""
+
+
+class DuplicateTransaction(ValidationError):
+    """Raised if a duplicated transaction is found"""
+
+
+class ThresholdTooDeep(ValidationError):
+    """Raised if threshold condition is too deep"""
+
+
+class GenesisBlockAlreadyExistsError(ValidationError):
+    """Raised when trying to create the already existing genesis block"""
+
+
+class MultipleValidatorOperationError(ValidationError):
+    """Raised when a validator update pending but new request is submited"""
diff --git a/resdb_driver/offchain.py b/resdb_driver/offchain.py
new file mode 100644
index 0000000..2824393
--- /dev/null
+++ b/resdb_driver/offchain.py
@@ -0,0 +1,353 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+"""
+Module for offchain operations. Connection to resdb nodes not required!
+"""
+
+import logging
+from functools import singledispatch
+
+from .transaction import Input, Transaction, TransactionLink, _fulfillment_from_details
+from .exceptions import KeypairMismatchException
+
+from .exceptions import ResdbException, MissingPrivateKeyError
+from .utils import (
+    CreateOperation,
+    TransferOperation,
+    _normalize_operation,
+)
+
+logger = logging.getLogger(__name__)
+
+
+@singledispatch
+def _prepare_transaction(
+    operation, signers=None, recipients=None, asset=None, metadata=None, inputs=None
+):
+    raise ResdbException(
+        (
+            "Unsupported operation: {}. "
+            'Only "CREATE" and "TRANSFER" are supported.'.format(operation)
+        )
+    )
+
+
+@_prepare_transaction.register(CreateOperation)
+def _prepare_create_transaction_dispatcher(operation, **kwargs):
+    del kwargs["inputs"]
+    return prepare_create_transaction(**kwargs)
+
+
+@_prepare_transaction.register(TransferOperation)
+def _prepare_transfer_transaction_dispatcher(operation, **kwargs):
+    del kwargs["signers"]
+    return prepare_transfer_transaction(**kwargs)
+
+
+def prepare_transaction(
+    *,
+    operation="CREATE",
+    signers=None,
+    recipients=None,
+    asset=None,
+    metadata=None,
+    inputs=None
+) -> dict:
+    """! Prepares a transaction payload, ready to be fulfilled. Depending on
+    the value of ``operation``, simply dispatches to either
+    :func:`~.prepare_create_transaction` or
+    :func:`~.prepare_transfer_transaction`.
+
+    @param operation (str): The operation to perform. Must be ``'CREATE'``
+            or ``'TRANSFER'``. Case insensitive. Defaults to ``'CREATE'``.
+    @param signers (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): 
+            One or more public keys representing the issuer(s) of
+            the asset being created. Only applies for ``'CREATE'``
+            operations. Defaults to ``None``.
+    @param recipients (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): 
+            One or more public keys representing the new recipients(s)
+            of the asset being created or transferred.
+            Defaults to ``None``.
+    @param asset (:obj:`dict`, optional): 
+            The asset to be created orctransferred. 
+            MUST be supplied for ``'TRANSFER'`` operations.
+            Defaults to ``None``.
+    @param metadata (:obj:`dict`, optional): 
+            Metadata associated with the
+            transaction. Defaults to ``None``.
+    @param inputs (:obj:`dict` | :obj:`list` | :obj:`tuple`, optional):
+            One or more inputs holding the condition(s) that this
+            transaction intends to fulfill. Each input is expected to
+            be a :obj:`dict`. Only applies to, and MUST be supplied for,
+            ``'TRANSFER'`` operations.
+
+    @return The prepared transaction
+
+    @exception :class:`~.exceptions.ResdbException`: If ``operation`` is
+        not ``'CREATE'`` or ``'TRANSFER'``.
+
+    .. important::
+
+        **CREATE operations**
+
+        * ``signers`` MUST be set.
+        * ``recipients``, ``asset``, and ``metadata`` MAY be set.
+        * If ``asset`` is set, it MUST be in the form of::
+
+            {
+                'data': {
+                    ...
+                }
+            }
+
+        * The argument ``inputs`` is ignored.
+        * If ``recipients`` is not given, or evaluates to
+          ``False``, it will be set equal to ``signers``::
+
+            if not recipients:
+                recipients = signers
+
+        **TRANSFER operations**
+
+        * ``recipients``, ``asset``, and ``inputs`` MUST be set.
+        * ``asset`` MUST be in the form of::
+
+            {
+                'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
+            }
+
+        * ``metadata`` MAY be set.
+        * The argument ``signers`` is ignored.
+
+    """
+    operation = _normalize_operation(operation)
+    return _prepare_transaction(
+        operation,
+        signers=signers,
+        recipients=recipients,
+        asset=asset,
+        metadata=metadata,
+        inputs=inputs,
+    )
+
+
+def prepare_create_transaction(*, signers, recipients=None, asset=None, metadata=None):
+    """! Prepares a ``"CREATE"`` transaction payload, ready to be
+    fulfilled.
+
+    @param signers (:obj:`list` | :obj:`tuple` | :obj:`str`): 
+            One or more public keys representing 
+            the issuer(s) of the asset being created.
+    @param recipients (:obj:`list` | :obj:`tuple` | :obj:`str`, optional): 
+            One or more public keys representing 
+            the new recipients(s) of the asset being created. Defaults to ``None``.
+    @param asset (:obj:`dict`, optional): The asset to be created. Defaults to ``None``.
+    @param metadata (:obj:`dict`, optional): Metadata associated with the transaction. Defaults to ``None``.
+
+    @return The prepared ``"CREATE"`` transaction.
+
+    .. important::
+
+        * If ``asset`` is set, it MUST be in the form of::
+
+                {
+                    'data': {
+                        ...
+                    }
+                }
+
+        * If ``recipients`` is not given, or evaluates to
+          ``False``, it will be set equal to ``signers``::
+
+            if not recipients:
+                recipients = signers
+
+    """
+    if not isinstance(signers, (list, tuple)):
+        signers = [signers]
+    # NOTE: Needed for the time being. See
+    # https://github.com/bigchaindb/bigchaindb/issues/797
+    elif isinstance(signers, tuple):
+        signers = list(signers)
+
+    if not recipients:
+        recipients = [(signers, 1)]
+    elif not isinstance(recipients, (list, tuple)):
+        recipients = [([recipients], 1)]
+    # NOTE: Needed for the time being. See
+    # https://github.com/bigchaindb/bigchaindb/issues/797
+    elif isinstance(recipients, tuple):
+        recipients = [(list(recipients), 1)]
+
+    transaction = Transaction.create(
+        signers,
+        recipients,
+        metadata=metadata,
+        asset=asset["data"] if asset else None,
+    )
+    return transaction.to_dict()
+
+
+def prepare_transfer_transaction(*, inputs, recipients, asset, metadata=None):
+    """! Prepares a ``"TRANSFER"`` transaction payload, ready to be
+    fulfilled.
+
+    @param inputs (:obj:`dict` | :obj:`list` | :obj:`tuple`): 
+                One or more inputs holding the condition(s) that this transaction
+                intends to fulfill. Each input is expected to be a
+                :obj:`dict`.
+    @param recipients (:obj:`str` | :obj:`list` | :obj:`tuple`): 
+            One or more public keys representing the 
+            new recipients(s) of the
+            asset being transferred.
+    @param asset (:obj:`dict`): A single-key dictionary holding the ``id``
+            of the asset being transferred with this transaction.
+    @param metadata (:obj:`dict`): Metadata associated with the
+            transaction. Defaults to ``None``.
+
+    @return The prepared ``"TRANSFER"`` transaction.
+
+    .. important::
+
+        * ``asset`` MUST be in the form of::
+
+            {
+                'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
+            }
+
+    Example:
+
+        # .. todo:: Replace this section with docs.
+
+        In case it may not be clear what an input should look like, say
+        Alice (public key: ``'3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf'``)
+        wishes to transfer an asset over to Bob
+        (public key: ``'EcRawy3Y22eAUSS94vLF8BVJi62wbqbD9iSUSUNU9wAA'``).
+        Let the asset creation transaction payload be denoted by
+        ``tx``::
+
+            # noqa E501
+            >>> tx
+                {'asset': {'data': {'msg': 'Hello Resdb!'}},
+                 'id': '9650055df2539223586d33d273cb8fd05bd6d485b1fef1caf7c8901a49464c87',
+                 'inputs': [{'fulfillment': {'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
+                                             'type': 'ed25519-sha-256'},
+                             'fulfills': None,
+                             'owners_before': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}],
+                 'metadata': None,
+                 'operation': 'CREATE',
+                 'outputs': [{'amount': '1',
+                              'condition': {'details': {'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
+                                                        'type': 'ed25519-sha-256'},
+                                            'uri': 'ni:///sha-256;7ApQLsLLQgj5WOUipJg1txojmge68pctwFxvc3iOl54?fpt=ed25519-sha-256&cost=131072'},
+                              'public_keys': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}],
+                 'version': '2.0'}
+
+        Then, the input may be constructed in this way::
+
+            output_index
+            output = tx['transaction']['outputs'][output_index]
+            input_ = {
+                'fulfillment': output['condition']['details'],
+                'input': {
+                    'output_index': output_index,
+                    'transaction_id': tx['id'],
+                },
+                'owners_before': output['public_keys'],
+            }
+
+        Displaying the input on the prompt would look like::
+
+            >>> input_
+            {'fulfillment': {
+              'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
+              'type': 'ed25519-sha-256'},
+             'input': {'output_index': 0,
+              'transaction_id': '9650055df2539223586d33d273cb8fd05bd6d485b1fef1caf7c8901a49464c87'},
+             'owners_before': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}
+
+
+        To prepare the transfer:
+
+        >>> prepare_transfer_transaction(
+        ...     inputs=input_,
+        ...     recipients='EcRawy3Y22eAUSS94vLF8BVJi62wbqbD9iSUSUNU9wAA',
+        ...     asset=tx['transaction']['asset'],
+        ... )
+
+    """
+    if not isinstance(inputs, (list, tuple)):
+        inputs = (inputs,)
+    if not isinstance(recipients, (list, tuple)):
+        recipients = [([recipients], 1)]
+
+    # NOTE: Needed for the time being. See
+    # https://github.com/bigchaindb/bigchaindb/issues/797
+    if isinstance(recipients, tuple):
+        recipients = [(list(recipients), 1)]
+
+    fulfillments = [
+        Input(
+            _fulfillment_from_details(input_["fulfillment"]),
+            input_["owners_before"],
+            fulfills=TransactionLink(
+                txid=input_["fulfills"]["transaction_id"],
+                output=input_["fulfills"]["output_index"],
+            ),
+        )
+        for input_ in inputs
+    ]
+
+    transaction = Transaction.transfer(
+        fulfillments,
+        recipients,
+        asset_id=asset["id"],
+        metadata=metadata,
+    )
+    return transaction.to_dict()
+
+
+def fulfill_transaction(transaction, *, private_keys) -> dict:
+    """! Fulfills the given transaction.
+
+    @param transaction The transaction to be fulfilled.
+    @param private_keys One or more private keys to be 
+            used for fulfilling the transaction.
+
+    @return The fulfilled transaction payload, ready to be sent to a
+            ResDB federation.
+
+    @exception :exc:`~.exceptions.MissingPrivateKeyError`: If a private
+        key is missing.
+    """
+    if not isinstance(private_keys, (list, tuple)):
+        private_keys = [private_keys]
+
+    # NOTE: Needed for the time being. See
+    # https://github.com/bigchaindb/bigchaindb/issues/797
+    if isinstance(private_keys, tuple):
+        private_keys = list(private_keys)
+
+    transaction_obj = Transaction.from_dict(transaction)
+    try:
+        signed_transaction = transaction_obj.sign(private_keys)
+    except KeypairMismatchException as exc:
+        raise MissingPrivateKeyError("A private key is missing!") from exc
+
+    return signed_transaction.to_dict()
diff --git a/resdb_driver/pool.py b/resdb_driver/pool.py
new file mode 100644
index 0000000..9ff68b7
--- /dev/null
+++ b/resdb_driver/pool.py
@@ -0,0 +1,78 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+from abc import ABCMeta, abstractmethod
+from .connection import Connection
+from datetime import datetime
+
+
+class AbstractPicker(metaclass=ABCMeta):
+    """! Abstract class for picker classes that pick connections from a pool."""
+
+    @abstractmethod
+    def pick(self, connections: list[Connection]):
+        """! Picks a :class:`~resdb_driver.connection.Connection`
+        instance from the given list of
+        :class:`~resdb_driver.connection.Connection` instances.
+
+        @param connections (list): List of :class:`~resdb_driver.connection.Connection` instances.
+        """
+        pass
+
+
+class RoundRobinPicker(AbstractPicker):
+    """! Picks a :class:`~resdb_driver.connection.Connection`
+    instance from a list of connections.
+    """
+
+    def pick(self, connections: list[Connection]) -> Connection:
+        """! Picks a connection with the earliest backoff time.
+        As a result, the first connection is picked
+        for as long as it has no backoff time.
+        Otherwise, the connections are tried in a round robin fashion.
+
+        @param connections (:obj:list): List of :class:`~resdb_driver.connection.Connection` instances.
+        """
+        if len(connections) == 1:
+            return connections[0]
+
+        return min(
+            *connections,
+            key=lambda conn: datetime.min
+            if conn.backoff_time is None
+            else conn.backoff_time
+        )
+
+
+class Pool:
+    """! Pool of connections.
+    """
+
+    def __init__(self, connections: list[Connection], picker_class=RoundRobinPicker):
+        """! Initializes a :class:`~resdb_driver.pool.Pool` instance.
+        @param connections (list): List of :class:`~resdb_driver.connection.Connection` instances.
+        """
+        self.connections = connections
+        self.picker = picker_class()
+
+    def get_connection(self) -> Connection:
+        """! Gets a :class:`~resdb_driver.connection.Connection`
+        instance from the pool.
+        @return A :class:`~resdb_driver.connection.Connection` instance.
+        """
+        return self.picker.pick(self.connections)
diff --git a/resdb_driver/transaction.py b/resdb_driver/transaction.py
new file mode 100644
index 0000000..610acf9
--- /dev/null
+++ b/resdb_driver/transaction.py
@@ -0,0 +1,1275 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+"""!
+Transaction related models to parse and construct transaction
+payloads.
+
+Attributes:
+    UnspentOutput (namedtuple): Object holding the information
+        representing an unspent output.
+
+"""
+from collections import namedtuple
+from copy import deepcopy
+from functools import reduce
+
+import base58
+from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256
+from cryptoconditions.exceptions import (
+    ParsingError,
+    ASN1DecodeError,
+    ASN1EncodeError,
+    UnsupportedTypeError,
+)
+from sha3 import sha3_256
+
+from .crypto import PrivateKey, hash_data
+from .exceptions import (
+    KeypairMismatchException,
+    InvalidHash,
+    InvalidSignature,
+    AmountError,
+    AssetIdMismatch,
+    ThresholdTooDeep,
+    DoubleSpend,
+    InputDoesNotExist,
+)
+from .utils import serialize
+
+
+UnspentOutput = namedtuple(
+    "UnspentOutput",
+    (
+        # TODO 'utxo_hash': sha3_256(f'{txid}{output_index}'.encode())
+        # 'utxo_hash',   # noqa
+        "transaction_id",
+        "output_index",
+        "amount",
+        "asset_id",
+        "condition_uri",
+    ),
+)
+
+
+class Input(object):
+    """! A Input is used to spend assets locked by an Output.
+    Wraps around a Crypto-condition Fulfillment.
+
+    fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment
+        to be signed with a private key.
+    owners_before (:obj:`list` of :obj:`str`): A list of owners after a
+        Transaction was confirmed.
+    fulfills (:class:`~resdb.transaction. TransactionLink`,
+        optional): A link representing the input of a `TRANSFER`
+        Transaction.
+    """
+
+    def __init__(self, fulfillment, owners_before, fulfills=None):
+        """! Create an instance of an :class:`~.Input`.
+        @param fulfillment (:class:`cryptoconditions.Fulfillment`): A
+            Fulfillment to be signed with a private key.
+        @param owners_before (:obj:`list` of :obj:`str`): A list of owners
+            after a Transaction was confirmed.
+        @param fulfills (:class:`~resdb.transaction.
+                TransactionLink`, optional): A link representing the input
+            of a `TRANSFER` Transaction.
+        """
+        if fulfills is not None and not isinstance(fulfills, TransactionLink):
+            raise TypeError("`fulfills` must be a TransactionLink instance")
+        if not isinstance(owners_before, list):
+            raise TypeError("`owners_after` must be a list instance")
+
+        self.fulfillment = fulfillment
+        self.fulfills = fulfills
+        self.owners_before = owners_before
+
+    def __eq__(self, other):
+        # TODO: If `other !== Fulfillment` return `False`
+        return self.to_dict() == other.to_dict()
+
+    def to_dict(self):
+        """! Transforms the object to a Python dictionary.
+        If an Input hasn't been signed yet, this method returns a
+        dictionary representation.
+
+        @return dict: The Input as an alternative serialization format.
+        """
+        try:
+            fulfillment = self.fulfillment.serialize_uri()
+        except (TypeError, AttributeError, ASN1EncodeError, ASN1DecodeError):
+            fulfillment = _fulfillment_to_details(self.fulfillment)
+
+        try:
+            # NOTE: `self.fulfills` can be `None` and that's fine
+            fulfills = self.fulfills.to_dict()
+        except AttributeError:
+            fulfills = None
+
+        input_ = {
+            "owners_before": self.owners_before,
+            "fulfills": fulfills,
+            "fulfillment": fulfillment,
+        }
+        return input_
+
+    @classmethod
+    def generate(cls, public_keys):
+        # TODO: write docstring
+        # The amount here does not really matter. It is only use on the
+        # output data model but here we only care about the fulfillment
+        output = Output.generate(public_keys, 1)
+        return cls(output.fulfillment, public_keys)
+
+    @classmethod
+    def from_dict(cls, data):
+        """! Transforms a Python dictionary to an Input object.
+        Note:
+            Optionally, this method can also serialize a Cryptoconditions-
+            Fulfillment that is not yet signed.
+
+        @param data (dict): The Input to be transformed.
+        @return :class:`~resdb.transaction.Input`
+        @exception InvalidSignature: If an Input's URI couldn't be parsed.
+        """
+        fulfillment = data["fulfillment"]
+        if not isinstance(fulfillment, (Fulfillment, type(None))):
+            try:
+                fulfillment = Fulfillment.from_uri(data["fulfillment"])
+            except ASN1DecodeError:
+                # TODO Remove as it is legacy code, and simply fall back on
+                # ASN1DecodeError
+                raise InvalidSignature("Fulfillment URI couldn't been parsed")
+            except TypeError:
+                # NOTE: See comment about this special case in
+                #       `Input.to_dict`
+                fulfillment = _fulfillment_from_details(data["fulfillment"])
+        fulfills = TransactionLink.from_dict(data["fulfills"])
+        return cls(fulfillment, data["owners_before"], fulfills)
+
+
+def _fulfillment_to_details(fulfillment):
+    """! Encode a fulfillment as a details dictionary
+    Args:
+        @param fulfillment (:class:`cryptoconditions.Fulfillment`): Crypto-conditions Fulfillment object
+    """
+
+    if fulfillment.type_name == "ed25519-sha-256":
+        return {
+            "type": "ed25519-sha-256",
+            "public_key": base58.b58encode(fulfillment.public_key).decode(),
+        }
+
+    if fulfillment.type_name == "threshold-sha-256":
+        subconditions = [
+            _fulfillment_to_details(cond["body"]) for cond in fulfillment.subconditions
+        ]
+        return {
+            "type": "threshold-sha-256",
+            "threshold": fulfillment.threshold,
+            "subconditions": subconditions,
+        }
+
+    raise UnsupportedTypeError(fulfillment.type_name)
+
+
+def _fulfillment_from_details(data, _depth=0):
+    """! Load a fulfillment for a signing spec dictionary
+        @param data tx.output[].condition.details dictionary
+    """
+    if _depth == 100:
+        raise ThresholdTooDeep()
+
+    if data["type"] == "ed25519-sha-256":
+        public_key = base58.b58decode(data["public_key"])
+        return Ed25519Sha256(public_key=public_key)
+
+    if data["type"] == "threshold-sha-256":
+        threshold = ThresholdSha256(data["threshold"])
+        for cond in data["subconditions"]:
+            cond = _fulfillment_from_details(cond, _depth + 1)
+            threshold.add_subfulfillment(cond)
+        return threshold
+
+    raise UnsupportedTypeError(data.get("type"))
+
+
+class TransactionLink(object):
+    """! An object for unidirectional linking to a Transaction's Output.
+    Attributes:
+        txid (str, optional): A Transaction to link to.
+        output (int, optional): An output's index in a Transaction with id
+        `txid`.
+    """
+
+    def __init__(self, txid=None, output=None):
+        """! Create an instance of a :class:`~.TransactionLink`.
+        Note:
+            In an IPLD implementation, this class is not necessary anymore,
+            as an IPLD link can simply point to an object, as well as an
+            objects properties. So instead of having a (de)serializable
+            class, we can have a simple IPLD link of the form:
+            `/<tx_id>/transaction/outputs/<output>/`.
+
+            @param txid (str): A Transaction to link to.
+            @param output An (int): Outputs's index in a Transaction
+            with id `txid`.
+        """
+        self.txid = txid
+        self.output = output
+
+    def __bool__(self):
+        return self.txid is not None and self.output is not None
+
+    def __eq__(self, other):
+        # TODO: If `other !== TransactionLink` return `False`
+        return self.to_dict() == other.to_dict()
+
+    def __hash__(self):
+        return hash((self.txid, self.output))
+
+    @classmethod
+    def from_dict(cls, link):
+        """! Transforms a Python dictionary to a TransactionLink object.
+
+            @param link (dict): The link to be transformed.
+
+            @return :class:`~resdb.transaction.TransactionLink`
+        """
+        try:
+            return cls(link["transaction_id"], link["output_index"])
+        except TypeError:
+            return cls()
+
+    def to_dict(self):
+        """! Transforms the object to a Python dictionary.
+            @return The link as an alternative serialization format.
+        """
+        if self.txid is None and self.output is None:
+            return None
+        else:
+            return {
+                "transaction_id": self.txid,
+                "output_index": self.output,
+            }
+
+    def to_uri(self, path=""):
+        if self.txid is None and self.output is None:
+            return None
+        return "{}/transactions/{}/outputs/{}".format(path, self.txid, self.output)
+
+
+class Output(object):
+    """! An Output is used to lock an asset.
+    Wraps around a Crypto-condition Condition.
+        Attributes:
+            fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment
+                to extract a Condition from.
+            public_keys (:obj:`list` of :obj:`str`, optional): A list of
+                owners before a Transaction was confirmed.
+    """
+
+    MAX_AMOUNT = 9 * 10**18
+
+    def __init__(self, fulfillment, public_keys=None, amount=1):
+        """! Create an instance of a :class:`~.Output`.
+        Args:
+            @param fulfillment (:class:`cryptoconditions.Fulfillment`): A
+                Fulfillment to extract a Condition from.
+            @param public_keys (:obj:`list` of :obj:`str`, optional): A list of
+                owners before a Transaction was confirmed.
+            @param amount (int): The amount of Assets to be locked with this
+                Output.
+
+            @exception TypeError: if `public_keys` is not instance of `list`.
+        """
+        if not isinstance(public_keys, list) and public_keys is not None:
+            raise TypeError("`public_keys` must be a list instance or None")
+        if not isinstance(amount, int):
+            raise TypeError("`amount` must be an int")
+        if amount < 1:
+            raise AmountError("`amount` must be greater than 0")
+        if amount > self.MAX_AMOUNT:
+            raise AmountError("`amount` must be <= %s" % self.MAX_AMOUNT)
+
+        self.fulfillment = fulfillment
+        self.amount = amount
+        self.public_keys = public_keys
+
+    def __eq__(self, other):
+        # TODO: If `other !== Condition` return `False`
+        return self.to_dict() == other.to_dict()
+
+    def to_dict(self):
+        """! Transforms the object to a Python dictionary.
+        Note:
+            A dictionary serialization of the Input the Output was
+            derived from is always provided.
+
+            @return The Output as an alternative serialization format.
+        """
+        # TODO FOR CC: It must be able to recognize a hashlock condition
+        #              and fulfillment!
+        condition = {}
+        try:
+            condition["details"] = _fulfillment_to_details(self.fulfillment)
+        except AttributeError:
+            pass
+
+        try:
+            condition["uri"] = self.fulfillment.condition_uri
+        except AttributeError:
+            condition["uri"] = self.fulfillment
+
+        output = {
+            "public_keys": self.public_keys,
+            "condition": condition,
+            "amount": str(self.amount),
+        }
+        return output
+
+    @classmethod
+    def generate(cls, public_keys, amount):
+        """! Generates a Output from a specifically formed tuple or list.
+        Note:
+            If a ThresholdCondition has to be generated where the threshold
+            is always the number of subconditions it is split between, a
+            list of the following structure is sufficient:
+            [(address|condition)*, [(address|condition)*, ...], ...]
+
+        @param public_keys (:obj:`list` of :obj:`str`): The public key of
+            the users that should be able to fulfill the Condition
+            that is being created.
+        @param amount (:obj:`int`): The amount locked by the Output.
+        @return An Output that can be used in a Transaction.
+
+        @exception TypeError: If `public_keys` is not an instance of `list`.
+        @exception ValueError: If `public_keys` is an empty list.
+        """
+        threshold = len(public_keys)
+        if not isinstance(amount, int):
+            raise TypeError("`amount` must be a int")
+        if amount < 1:
+            raise AmountError("`amount` needs to be greater than zero")
+        if not isinstance(public_keys, list):
+            raise TypeError("`public_keys` must be an instance of list")
+        if len(public_keys) == 0:
+            raise ValueError(
+                "`public_keys` needs to contain at least one" "owner")
+        elif len(public_keys) == 1 and not isinstance(public_keys[0], list):
+            if isinstance(public_keys[0], Fulfillment):
+                ffill = public_keys[0]
+            else:
+                ffill = Ed25519Sha256(
+                    public_key=base58.b58decode(public_keys[0]))
+            return cls(ffill, public_keys, amount=amount)
+        else:
+            # Threshold conditions not supported by resdb yet
+            initial_cond = ThresholdSha256(threshold=threshold)
+            threshold_cond = reduce(
+                cls._gen_condition, public_keys, initial_cond)
+            return cls(threshold_cond, public_keys, amount=amount)
+
+    @classmethod
+    def _gen_condition(cls, initial, new_public_keys):
+        """! Generates ThresholdSha256 conditions from a list of new owners.
+        Note:
+            This method is intended only to be used with a reduce function.
+            For a description on how to use this method, see
+            :meth:`~.Output.generate`.
+        Args:
+            @param initial (:class:`cryptoconditions.ThresholdSha256`): A Condition representing the overall root.
+            @param new_public_keys (:obj:`list` of :obj:`str`|str): A list of new
+                owners or a single new owner.
+            @return :class:`cryptoconditions.ThresholdSha256`:
+        """
+        try:
+            threshold = len(new_public_keys)
+        except TypeError:
+            threshold = None
+
+        if isinstance(new_public_keys, list) and len(new_public_keys) > 1:
+            ffill = ThresholdSha256(threshold=threshold)
+            reduce(cls._gen_condition, new_public_keys, ffill)
+        elif isinstance(new_public_keys, list) and len(new_public_keys) <= 1:
+            raise ValueError("Sublist cannot contain single owner")
+        else:
+            try:
+                new_public_keys = new_public_keys.pop()
+            except AttributeError:
+                pass
+            # NOTE: Instead of submitting base58 encoded addresses, a user
+            #       of this class can also submit fully instantiated
+            #       Cryptoconditions. In the case of casting
+            #       `new_public_keys` to a Ed25519Fulfillment with the
+            #       result of a `TypeError`, we're assuming that
+            #       `new_public_keys` is a Cryptocondition then.
+            if isinstance(new_public_keys, Fulfillment):
+                ffill = new_public_keys
+            else:
+                ffill = Ed25519Sha256(
+                    public_key=base58.b58decode(new_public_keys))
+        initial.add_subfulfillment(ffill)
+        return initial
+
+    @classmethod
+    def from_dict(cls, data):
+        """! Transforms a Python dictionary to an Output object.
+        Note:
+            To pass a serialization cycle multiple times, a
+            Cryptoconditions Fulfillment needs to be present in the
+            passed-in dictionary, as Condition URIs are not serializable
+            anymore.
+
+        @param data (dict): The dict to be transformed.
+        @return :class:`~resdb.transaction.Output`
+        """
+        try:
+            fulfillment = _fulfillment_from_details(
+                data["condition"]["details"])
+        except KeyError:
+            # NOTE: Hashlock condition case
+            fulfillment = data["condition"]["uri"]
+        try:
+            amount = int(data["amount"])
+        except ValueError:
+            raise AmountError("Invalid amount: %s" % data["amount"])
+        return cls(fulfillment, data["public_keys"], amount)
+
+
+class Transaction(object):
+    """! A Transaction is used to create and transfer assets.
+    Note:
+        For adding Inputs and Outputs, this class provides methods
+        to do so.
+    Attributes:
+        operation (str): Defines the operation of the Transaction.
+        inputs (:obj:`list` of :class:`~resdb.
+            transaction.Input`, optional): Define the assets to
+            spend.
+        outputs (:obj:`list` of :class:`~resdb.
+            transaction.Output`, optional): Define the assets to lock.
+        asset (dict): Asset payload for this Transaction. ``CREATE``
+            Transactions require a dict with a ``data``
+            property while ``TRANSFER`` Transactions require a dict with a
+            ``id`` property.
+        metadata (dict):
+            Metadata to be stored along with the Transaction.
+        version (string): Defines the version number of a Transaction.
+    """
+
+    CREATE = "CREATE"
+    TRANSFER = "TRANSFER"
+    ALLOWED_OPERATIONS = (CREATE, TRANSFER)
+    VERSION = "2.0"
+
+    def __init__(
+        self,
+        operation,
+        asset,
+        inputs=None,
+        outputs=None,
+        metadata=None,
+        version=None,
+        hash_id=None,
+    ):
+        """! The constructor allows to create a customizable Transaction.
+        Note:
+            When no `version` is provided, one is being
+            generated by this method.
+
+        @param operation (str): Defines the operation of the Transaction.
+        @param asset (dict): Asset payload for this Transaction.
+        @param inputs (:obj:`list` of :class:`~resdb.transaction.Input`, optional):Define the assets to
+        @param outputs (:obj:`list` of :class:`~resdb.transaction.Output`, optional):Define the assets to lock.
+        @param metadata (dict): Metadata to be stored along with the Transaction.
+        @param version (string): Defines the version number of a Transaction.
+        @param hash_id (string): Hash id of the transaction.
+        """
+        if operation not in Transaction.ALLOWED_OPERATIONS:
+            allowed_ops = ", ".join(self.__class__.ALLOWED_OPERATIONS)
+            raise ValueError(
+                "`operation` must be one of {}".format(allowed_ops))
+
+        # Asset payloads for 'CREATE' operations must be None or
+        # dicts holding a `data` property. Asset payloads for 'TRANSFER'
+        # operations must be dicts holding an `id` property.
+        if (
+            operation == Transaction.CREATE
+            and asset is not None
+            and not (isinstance(asset, dict) and "data" in asset)
+        ):
+            raise TypeError(
+                (
+                    "`asset` must be None or a dict holding a `data` "
+                    " property instance for '{}' "
+                    "Transactions".format(operation)
+                )
+            )
+        elif operation == Transaction.TRANSFER and not (
+            isinstance(asset, dict) and "id" in asset
+        ):
+            raise TypeError(
+                (
+                    "`asset` must be a dict holding an `id` property "
+                    "for 'TRANSFER' Transactions".format(operation)
+                )
+            )
+
+        if outputs and not isinstance(outputs, list):
+            raise TypeError("`outputs` must be a list instance or None")
+
+        if inputs and not isinstance(inputs, list):
+            raise TypeError("`inputs` must be a list instance or None")
+
+        if metadata is not None and not isinstance(metadata, dict):
+            raise TypeError("`metadata` must be a dict or None")
+
+        self.version = version if version is not None else self.VERSION
+        self.operation = operation
+        self.asset = asset
+        self.inputs = inputs or []
+        self.outputs = outputs or []
+        self.metadata = metadata
+        self._id = hash_id
+
+    @property
+    def unspent_outputs(self):
+        """! UnspentOutput: The outputs of this transaction, in a data
+        structure containing relevant information for storing them in
+        a UTXO set, and performing validation.
+        """
+        if self.operation == Transaction.CREATE:
+            self._asset_id = self._id
+        elif self.operation == Transaction.TRANSFER:
+            self._asset_id = self.asset["id"]
+        return (
+            UnspentOutput(
+                transaction_id=self._id,
+                output_index=output_index,
+                amount=output.amount,
+                asset_id=self._asset_id,
+                condition_uri=output.fulfillment.condition_uri,
+            )
+            for output_index, output in enumerate(self.outputs)
+        )
+
+    @property
+    def spent_outputs(self):
+        """! Tuple of :obj:`dict`: Inputs of this transaction. Each input
+        is represented as a dictionary containing a transaction id and
+        output index.
+        """
+        return (input_.fulfills.to_dict() for input_ in self.inputs if input_.fulfills)
+
+    @property
+    def serialized(self):
+        return Transaction._to_str(self.to_dict())
+
+    def _hash(self):
+        self._id = hash_data(self.serialized)
+
+    @classmethod
+    def create(cls, tx_signers, recipients, metadata=None, asset=None):
+        """! A simple way to generate a `CREATE` transaction.
+        Note:
+            This method currently supports the following Cryptoconditions
+            use cases:
+                - Ed25519
+                - ThresholdSha256
+            Additionally, it provides support for the following Resdb
+            use cases:
+                - Multiple inputs and outputs.
+
+        @param tx_signers (:obj:`list` of :obj:`str`): A list of keys that
+            represent the signers of the CREATE Transaction.
+        @param recipients (:obj:`list` of :obj:`tuple`): A list of
+            ([keys],amount) that represent the recipients of this
+            Transaction.
+        @param metadata (dict): The metadata to be stored along with the
+            Transaction.
+        @param asset (dict): The metadata associated with the asset that will
+            be created in this Transaction.
+
+        @return :class:`~resdb.transaction.Transaction`
+        """
+        if not isinstance(tx_signers, list):
+            raise TypeError("`tx_signers` must be a list instance")
+        if not isinstance(recipients, list):
+            raise TypeError("`recipients` must be a list instance")
+        if len(tx_signers) == 0:
+            raise ValueError("`tx_signers` list cannot be empty")
+        if len(recipients) == 0:
+            raise ValueError("`recipients` list cannot be empty")
+        if not (asset is None or isinstance(asset, dict)):
+            raise TypeError("`asset` must be a dict or None")
+
+        inputs = []
+        outputs = []
+
+        # generate_outputs
+        for recipient in recipients:
+            if not isinstance(recipient, tuple) or len(recipient) != 2:
+                raise ValueError(
+                    (
+                        "Each `recipient` in the list must be a"
+                        " tuple of `([<list of public keys>],"
+                        " <amount>)`"
+                    )
+                )
+            pub_keys, amount = recipient
+            outputs.append(Output.generate(pub_keys, amount))
+
+        # generate inputs
+        inputs.append(Input.generate(tx_signers))
+
+        return cls(cls.CREATE, {"data": asset}, inputs, outputs, metadata)
+
+    @classmethod
+    def transfer(cls, inputs, recipients, asset_id, metadata=None):
+        """! A simple way to generate a `TRANSFER` transaction.
+        Note:
+            Different cases for threshold conditions:
+            Combining multiple `inputs` with an arbitrary number of
+            `recipients` can yield interesting cases for the creation of
+            threshold conditions we'd like to support. The following
+            notation is proposed:
+            1. The index of a `recipient` corresponds to the index of
+               an input:
+               e.g. `transfer([input1], [a])`, means `input1` would now be
+                    owned by user `a`.
+            2. `recipients` can (almost) get arbitrary deeply nested,
+               creating various complex threshold conditions:
+               e.g. `transfer([inp1, inp2], [[a, [b, c]], d])`, means
+                    `a`'s signature would have a 50% weight on `inp1`
+                    compared to `b` and `c` that share 25% of the leftover
+                    weight respectively. `inp2` is owned completely by `d`.
+
+        @param inputs (:obj:`list` of :class:`~resdb.transaction.Input`): Converted `Output`s, intended to
+            be used as inputs in the transfer to generate.
+        @param recipients (:obj:`list` of :obj:`tuple`): A list of
+            ([keys],amount) that represent the recipients of this
+            Transaction.
+        @param asset_id (str): The asset ID of the asset to be transferred in
+            this Transaction.
+        @param metadata (dict): Python dictionary to be stored along with the
+            Transaction.
+
+        @return :class:`~resdb.transaction.Transaction`
+        """
+        if not isinstance(inputs, list):
+            raise TypeError("`inputs` must be a list instance")
+        if len(inputs) == 0:
+            raise ValueError("`inputs` must contain at least one item")
+        if not isinstance(recipients, list):
+            raise TypeError("`recipients` must be a list instance")
+        if len(recipients) == 0:
+            raise ValueError("`recipients` list cannot be empty")
+
+        outputs = []
+        for recipient in recipients:
+            if not isinstance(recipient, tuple) or len(recipient) != 2:
+                raise ValueError(
+                    (
+                        "Each `recipient` in the list must be a"
+                        " tuple of `([<list of public keys>],"
+                        " <amount>)`"
+                    )
+                )
+            pub_keys, amount = recipient
+            outputs.append(Output.generate(pub_keys, amount))
+
+        if not isinstance(asset_id, str):
+            raise TypeError("`asset_id` must be a string")
+
+        inputs = deepcopy(inputs)
+        return cls(cls.TRANSFER, {"id": asset_id}, inputs, outputs, metadata)
+
+    def __eq__(self, other):
+        try:
+            other = other.to_dict()
+        except AttributeError:
+            return False
+        return self.to_dict() == other
+
+    def to_inputs(self, indices=None):
+        """! Converts a Transaction's outputs to spendable inputs.
+        Note:
+            Takes the Transaction's outputs and derives inputs
+            from that can then be passed into `Transaction.transfer` as
+            `inputs`.
+            A list of integers can be passed to `indices` that
+            defines which outputs should be returned as inputs.
+            If no `indices` are passed (empty list or None) all
+            outputs of the Transaction are returned.
+
+        @param indices (:obj:`list` of int): Defines which
+            outputs should be returned as inputs.
+        @return :obj:`list` of :class:`~resdb.transaction.
+            Input`
+        """
+        # NOTE: If no indices are passed, we just assume to take all outputs
+        #       as inputs.
+        indices = indices or range(len(self.outputs))
+        return [
+            Input(
+                self.outputs[idx].fulfillment,
+                self.outputs[idx].public_keys,
+                TransactionLink(self.id, idx),
+            )
+            for idx in indices
+        ]
+
+    def add_input(self, input_):
+        """! Adds an input to a Transaction's list of inputs.
+        @param input_ (:class:`~resdb.transaction.
+            Input`): An Input to be added to the Transaction.
+        """
+        if not isinstance(input_, Input):
+            raise TypeError("`input_` must be a Input instance")
+        self.inputs.append(input_)
+
+    def add_output(self, output):
+        """! Adds an output to a Transaction's list of outputs.
+        @param output (:class:`~resdb.transaction.
+            Output`): An Output to be added to the
+            Transaction.
+        """
+        if not isinstance(output, Output):
+            raise TypeError("`output` must be an Output instance or None")
+        self.outputs.append(output)
+
+    def sign(self, private_keys):
+        """! Fulfills a previous Transaction's Output by signing Inputs.
+        Note:
+            This method works only for the following Cryptoconditions
+            currently:
+                - Ed25519Fulfillment
+                - ThresholdSha256
+            Furthermore, note that all keys required to fully sign the
+            Transaction have to be passed to this method. A subset of all
+            will cause this method to fail.
+
+        @param private_keys (:obj:`list` of :obj:`str`): A complete list of
+            all private keys needed to sign all Fulfillments of this
+            Transaction.
+        @return :class:`~resdb.transaction.Transaction`
+        """
+        # TODO: Singing should be possible with at least one of all private
+        #       keys supplied to this method.
+        if private_keys is None or not isinstance(private_keys, list):
+            raise TypeError("`private_keys` must be a list instance")
+
+        # NOTE: Generate public keys from private keys and match them in a
+        #       dictionary:
+        #                   key:     public_key
+        #                   value:   private_key
+        def gen_public_key(private_key):
+            # TODO FOR CC: Adjust interface so that this function becomes
+            #              unnecessary
+
+            # cc now provides a single method `encode` to return the key
+            # in several different encodings.
+            public_key = private_key.get_verifying_key().encode()
+            # Returned values from cc are always bytestrings so here we need
+            # to decode to convert the bytestring into a python str
+            return public_key.decode()
+
+        key_pairs = {
+            gen_public_key(PrivateKey(private_key)): PrivateKey(private_key)
+            for private_key in private_keys
+        }
+
+        tx_dict = self.to_dict()
+        tx_dict = Transaction._remove_signatures(tx_dict)
+        tx_serialized = Transaction._to_str(tx_dict)
+        for i, input_ in enumerate(self.inputs):
+            self.inputs[i] = self._sign_input(input_, tx_serialized, key_pairs)
+
+        self._hash()
+
+        return self
+
+    @classmethod
+    def _sign_input(cls, input_, message, key_pairs):
+        """! Signs a single Input.
+        Note:
+            This method works only for the following Cryptoconditions
+            currently:
+                - Ed25519Fulfillment
+                - ThresholdSha256.
+
+        @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed.
+        @param message (str): The message to be signed
+        @param key_pairs (dict): The keys to sign the Transaction with.
+        """
+        if isinstance(input_.fulfillment, Ed25519Sha256):
+            return cls._sign_simple_signature_fulfillment(input_, message, key_pairs)
+        elif isinstance(input_.fulfillment, ThresholdSha256):
+            return cls._sign_threshold_signature_fulfillment(input_, message, key_pairs)
+        else:
+            raise ValueError(
+                "Fulfillment couldn't be matched to "
+                "Cryptocondition fulfillment type."
+            )
+
+    @classmethod
+    def _sign_simple_signature_fulfillment(cls, input_, message, key_pairs):
+        """! Signs a Ed25519Fulfillment.
+
+        @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed.
+        @param message (str): The message to be signed
+        @param key_pairs (dict): The keys to sign the Transaction with.
+        """
+        # NOTE: To eliminate the dangers of accidentally signing a condition by
+        #       reference, we remove the reference of input_ here
+        #       intentionally. If the user of this class knows how to use it,
+        #       this should never happen, but then again, never say never.
+        input_ = deepcopy(input_)
+        public_key = input_.owners_before[0]
+        message = sha3_256(message.encode())
+        if input_.fulfills:
+            message.update(
+                "{}{}".format(input_.fulfills.txid,
+                              input_.fulfills.output).encode()
+            )
+
+        try:
+            # cryptoconditions makes no assumptions of the encoding of the
+            # message to sign or verify. It only accepts bytestrings
+            input_.fulfillment.sign(
+                message.digest(), base58.b58decode(
+                    key_pairs[public_key].encode())
+            )
+        except KeyError:
+            raise KeypairMismatchException(
+                "Public key {} is not a pair to "
+                "any of the private keys".format(public_key)
+            )
+        return input_
+
+    @classmethod
+    def _sign_threshold_signature_fulfillment(cls, input_, message, key_pairs):
+        """! Signs a ThresholdSha256.
+
+        @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed.
+        @param message (str): The message to be signed
+        @param key_pairs (dict): The keys to sign the Transaction with.
+        """
+        input_ = deepcopy(input_)
+        message = sha3_256(message.encode())
+        if input_.fulfills:
+            message.update(
+                "{}{}".format(input_.fulfills.txid,
+                              input_.fulfills.output).encode()
+            )
+
+        for owner_before in set(input_.owners_before):
+            # TODO: CC should throw a KeypairMismatchException, instead of
+            #       our manual mapping here
+
+            # TODO FOR CC: Naming wise this is not so smart,
+            #              `get_subcondition` in fact doesn't return a
+            #              condition but a fulfillment
+
+            # TODO FOR CC: `get_subcondition` is singular. One would not
+            #              expect to get a list back.
+            ccffill = input_.fulfillment
+            subffills = ccffill.get_subcondition_from_vk(
+                base58.b58decode(owner_before))
+            if not subffills:
+                raise KeypairMismatchException(
+                    "Public key {} cannot be found "
+                    "in the fulfillment".format(owner_before)
+                )
+            try:
+                private_key = key_pairs[owner_before]
+            except KeyError:
+                raise KeypairMismatchException(
+                    "Public key {} is not a pair "
+                    "to any of the private keys".format(owner_before)
+                )
+
+            # cryptoconditions makes no assumptions of the encoding of the
+            # message to sign or verify. It only accepts bytestrings
+            for subffill in subffills:
+                subffill.sign(message.digest(),
+                              base58.b58decode(private_key.encode()))
+        return input_
+
+    def inputs_valid(self, outputs=None):
+        """! Validates the Inputs in the Transaction against given
+        Outputs.
+            Note:
+                Given a `CREATE` Transaction is passed,
+                dummy values for Outputs are submitted for validation that
+                evaluate parts of the validation-checks to `True`.
+
+        @param outputs (:obj:`list` of :class:`~resdb.
+            transaction.Output`): A list of Outputs to check the
+            Inputs against.
+        @return If all Inputs are valid.
+        """
+        if self.operation == Transaction.CREATE:
+            # NOTE: Since in the case of a `CREATE`-transaction we do not have
+            #       to check for outputs, we're just submitting dummy
+            #       values to the actual method. This simplifies it's logic
+            #       greatly, as we do not have to check against `None` values.
+            return self._inputs_valid(["dummyvalue" for _ in self.inputs])
+        elif self.operation == Transaction.TRANSFER:
+            return self._inputs_valid(
+                [output.fulfillment.condition_uri for output in outputs]
+            )
+        else:
+            allowed_ops = ", ".join(self.__class__.ALLOWED_OPERATIONS)
+            raise TypeError(
+                "`operation` must be one of {}".format(allowed_ops))
+
+    def _inputs_valid(self, output_condition_uris):
+        """!Validates an Input against a given set of Outputs.
+        Note:
+            The number of `output_condition_uris` must be equal to the
+            number of Inputs a Transaction has.
+
+        @param output_condition_uris (:obj:`list` of :obj:`str`): A list of
+            Outputs to check the Inputs against.
+        @return If all Outputs are valid.
+        """
+
+        if len(self.inputs) != len(output_condition_uris):
+            raise ValueError(
+                "Inputs and " "output_condition_uris must have the same count"
+            )
+
+        tx_dict = self.to_dict()
+        tx_dict = Transaction._remove_signatures(tx_dict)
+        tx_dict["id"] = None
+        tx_serialized = Transaction._to_str(tx_dict)
+
+        def validate(i, output_condition_uri=None):
+            """Validate input against output condition URI"""
+            return self._input_valid(
+                self.inputs[i], self.operation, tx_serialized, output_condition_uri
+            )
+
+        return all(validate(i, cond) for i, cond in enumerate(output_condition_uris))
+
+    @staticmethod
+    def _input_valid(input_, operation, message, output_condition_uri=None):
+        """! Validates a single Input against a single Output.
+        Note:
+            In case of a `CREATE` Transaction, this method
+            does not validate against `output_condition_uri`.
+
+        @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed.
+        @param operation (str): The type of Transaction.
+        @param message (str): The fulfillment message.
+        @param output_condition_uri (str, optional): An Output to check the
+            Input against.
+        @return If the Input is valid.
+        """
+        ccffill = input_.fulfillment
+        try:
+            parsed_ffill = Fulfillment.from_uri(ccffill.serialize_uri())
+        except (TypeError, ValueError, ParsingError, ASN1DecodeError, ASN1EncodeError):
+            return False
+
+        if operation == Transaction.CREATE:
+            # NOTE: In the case of a `CREATE` transaction, the
+            #       output is always valid.
+            output_valid = True
+        else:
+            output_valid = output_condition_uri == ccffill.condition_uri
+
+        message = sha3_256(message.encode())
+        if input_.fulfills:
+            message.update(
+                "{}{}".format(input_.fulfills.txid,
+                              input_.fulfills.output).encode()
+            )
+
+        # NOTE: We pass a timestamp to `.validate`, as in case of a timeout
+        #       condition we'll have to validate against it
+
+        # cryptoconditions makes no assumptions of the encoding of the
+        # message to sign or verify. It only accepts bytestrings
+        ffill_valid = parsed_ffill.validate(message=message.digest())
+        return output_valid and ffill_valid
+
+    def to_dict(self):
+        """! Transforms the object to a Python dictionary.
+        @return The Transaction as an alternative serialization format.
+        """
+        return {
+            "inputs": [input_.to_dict() for input_ in self.inputs],
+            "outputs": [output.to_dict() for output in self.outputs],
+            "operation": str(self.operation),
+            "metadata": self.metadata,
+            "asset": self.asset,
+            "version": self.version,
+            "id": self._id,
+        }
+
+    @staticmethod
+    # TODO: Remove `_dict` prefix of variable.
+    def _remove_signatures(tx_dict):
+        """! Takes a Transaction dictionary and removes all signatures.
+        @param (dict): tx_dict The Transaction to remove all signatures from.
+        @return dict
+        """
+        # NOTE: We remove the reference since we need `tx_dict` only for the
+        #       transaction's hash
+        tx_dict = deepcopy(tx_dict)
+        for input_ in tx_dict["inputs"]:
+            # NOTE: Not all Cryptoconditions return a `signature` key (e.g.
+            #       ThresholdSha256), so setting it to `None` in any
+            #       case could yield incorrect signatures. This is why we only
+            #       set it to `None` if it's set in the dict.
+            input_["fulfillment"] = None
+        return tx_dict
+
+    @staticmethod
+    def _to_hash(value):
+        return hash_data(value)
+
+    @property
+    def id(self):
+        return self._id
+
+    def to_hash(self):
+        return self.to_dict()["id"]
+
+    @staticmethod
+    def _to_str(value):
+        return serialize(value)
+
+    # TODO: This method shouldn't call `_remove_signatures`
+    def __str__(self):
+        tx = Transaction._remove_signatures(self.to_dict())
+        return Transaction._to_str(tx)
+
+    @staticmethod
+    def get_asset_id(transactions):
+        """! Get the asset id from a list of :class:`~.Transactions`.
+        This is useful when we want to check if the multiple inputs of a
+        transaction are related to the same asset id.
+        Args:
+            @param transactions (:obj:`list` of :class:`~resdb.transaction.Transaction`):
+                A list of Transactions.
+                Usually input Transactions that should have a matching
+                asset ID.
+            @return ID of the asset.
+            @exception If the inputs are related to different assets.
+        """
+
+        if not isinstance(transactions, list):
+            transactions = [transactions]
+
+        # create a set of the transactions' asset ids
+        asset_ids = {
+            tx.id if tx.operation == Transaction.CREATE else tx.asset["id"]
+            for tx in transactions
+        }
+
+        # check that all the transasctions have the same asset id
+        if len(asset_ids) > 1:
+            raise AssetIdMismatch(
+                (
+                    "All inputs of all transactions passed"
+                    " need to have the same asset id"
+                )
+            )
+        return asset_ids.pop()
+
+    @staticmethod
+    def validate_id(tx_body):
+        """! Validate the transaction ID of a transaction
+        @param tx_body (dict): The Transaction to be transformed.
+        """
+        # NOTE: Remove reference to avoid side effects
+        tx_body = deepcopy(tx_body)
+        try:
+            proposed_tx_id = tx_body["id"]
+        except KeyError:
+            raise InvalidHash("No transaction id found!")
+
+        tx_body["id"] = None
+
+        tx_body_serialized = Transaction._to_str(tx_body)
+        valid_tx_id = Transaction._to_hash(tx_body_serialized)
+
+        if proposed_tx_id != valid_tx_id:
+            err_msg = (
+                "The transaction's id '{}' isn't equal to "
+                "the hash of its body, i.e. it's not valid."
+            )
+            raise InvalidHash(err_msg.format(proposed_tx_id))
+
+    @classmethod
+    def from_dict(cls, tx, skip_schema_validation=True):
+        """! Transforms a Python dictionary to a Transaction object.
+        @param tx_body (dict): The Transaction to be transformed.
+        @return :class:`~resdb.transaction.Transaction`
+        """
+        inputs = [Input.from_dict(input_) for input_ in tx["inputs"]]
+        outputs = [Output.from_dict(output) for output in tx["outputs"]]
+
+        if not skip_schema_validation:
+            cls.validate_id(tx)
+            cls.validate_schema(tx)
+        return cls(
+            tx["operation"],
+            tx["asset"],
+            inputs,
+            outputs,
+            tx["metadata"],
+            tx["version"],
+            hash_id=tx["id"],
+        )
+
+    @classmethod
+    def from_db(cls, resdb, tx_dict_list):
+        """! Helper method that reconstructs a transaction dict that was returned
+        from the database. It checks what asset_id to retrieve, retrieves the
+        asset from the asset table and reconstructs the transaction.
+
+
+        @param resdb An instance of ResDB used to perform database queries.
+        @param tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or
+            list of transaction dict as returned from the database.
+
+        @return :class:`~Transaction`
+
+        """
+        return_list = True
+        if isinstance(tx_dict_list, dict):
+            tx_dict_list = [tx_dict_list]
+            return_list = False
+
+        tx_map = {}
+        tx_ids = []
+        for tx in tx_dict_list:
+            tx.update({"metadata": None})
+            tx_map[tx["id"]] = tx
+            tx_ids.append(tx["id"])
+
+        assets = list(resdb.get_assets(tx_ids))
+        for asset in assets:
+            if asset is not None:
+                tx = tx_map[asset["id"]]
+                del asset["id"]
+                tx["asset"] = asset
+
+        tx_ids = list(tx_map.keys())
+        metadata_list = list(resdb.get_metadata(tx_ids))
+        for metadata in metadata_list:
+            tx = tx_map[metadata["id"]]
+            tx.update({"metadata": metadata.get("metadata")})
+
+        if return_list:
+            tx_list = []
+            for tx_id, tx in tx_map.items():
+                tx_list.append(cls.from_dict(tx))
+            return tx_list
+        else:
+            tx = list(tx_map.values())[0]
+            return cls.from_dict(tx)
+
+    type_registry = {}
+
+    @staticmethod
+    def register_type(tx_type, tx_class):
+        Transaction.type_registry[tx_type] = tx_class
+
+    def resolve_class(operation):
+        """! For the given `tx` based on the `operation` key return its implementation class
+        """
+
+        create_txn_class = Transaction.type_registry.get(Transaction.CREATE)
+        return Transaction.type_registry.get(operation, create_txn_class)
+
+    @classmethod
+    def validate_schema(cls, tx):
+        # TODO
+        pass
+
+    def validate_transfer_inputs(self, resdb, current_transactions=[]):
+        # store the inputs so that we can check if the asset ids match
+        input_txs = []
+        input_conditions = []
+        for input_ in self.inputs:
+            input_txid = input_.fulfills.txid
+
+            input_tx = resdb.get_transaction(input_txid)
+
+            if input_tx is None:
+                for ctxn in current_transactions:
+                    if ctxn.id == input_txid:
+                        input_tx = ctxn
+
+            if input_tx is None:
+                raise InputDoesNotExist(
+                    "input `{}` doesn't exist".format(input_txid))
+
+            spent = resdb.get_spent(
+                input_txid, input_.fulfills.output, current_transactions
+            )
+            if spent:
+                raise DoubleSpend(
+                    "input `{}` was already spent".format(input_txid))
+
+            output = input_tx.outputs[input_.fulfills.output]
+            input_conditions.append(output)
+            input_txs.append(input_tx)
+
+        # Validate that all inputs are distinct
+        links = [i.fulfills.to_uri() for i in self.inputs]
+        if len(links) != len(set(links)):
+            raise DoubleSpend('tx "{}" spends inputs twice'.format(self.id))
+
+        # validate asset id
+        asset_id = self.get_asset_id(input_txs)
+        if asset_id != self.asset["id"]:
+            raise AssetIdMismatch(
+                (
+                    "The asset id of the input does not"
+                    " match the asset id of the"
+                    " transaction"
+                )
+            )
+
+        input_amount = sum(
+            [input_condition.amount for input_condition in input_conditions]
+        )
+        output_amount = sum(
+            [output_condition.amount for output_condition in self.outputs]
+        )
+
+        if output_amount != input_amount:
+            raise AmountError(
+                (
+                    "The amount used in the inputs `{}`"
+                    " needs to be same as the amount used"
+                    " in the outputs `{}`"
+                ).format(input_amount, output_amount)
+            )
+
+        if not self.inputs_valid(input_conditions):
+            raise InvalidSignature("Transaction signature is invalid.")
+
+        return True
diff --git a/resdb_driver/transport.py b/resdb_driver/transport.py
new file mode 100644
index 0000000..ce3f563
--- /dev/null
+++ b/resdb_driver/transport.py
@@ -0,0 +1,107 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+from time import time
+from requests import request, Response
+
+from requests.exceptions import ConnectionError
+
+from .connection import Connection
+from .exceptions import TimeoutError
+from .pool import Pool
+
+
+NO_TIMEOUT_BACKOFF_CAP = 10  # seconds
+
+
+class Transport:
+    """! Transport class.
+    """
+
+    def __init__(self, *nodes: list, timeout: int = None):
+        """! Initializes an instance of
+            :class:`~resdb_driver.transport.Transport`.
+        @param nodes Each node is a dictionary with the keys `endpoint` and
+                `headers`
+        @param timeout (int): Optional timeout in seconds.
+        """
+        self.nodes = nodes
+        self.timeout = timeout
+        self.connection_pool = Pool(
+            [
+                Connection(node_url=node["endpoint"], headers=node["headers"])
+                for node in nodes
+            ]
+        )
+
+    def forward_request(
+        self,
+        method: str,
+        path: str = None,
+        json: dict = None,
+        params: dict = None,
+        headers: dict = None,
+    ) -> Response.json:
+        """! Makes HTTP requests to the configured nodes.
+           Retries connection errors
+           (e.g. DNS failures, refused connection, etc).
+           A user may choose to retry other errors
+           by catching the corresponding
+           exceptions and retrying `forward_request`.
+           Exponential backoff is implemented individually for each node.
+           Backoff delays are expressed as timestamps stored on the object and
+           they are not reset in between multiple function calls.
+           Times out when `self.timeout` is expired, if not `None`.
+
+        @param method (str): HTTP method name (e.g.: ``'GET'``).
+        @param path (str): Path to be appended to the base url of a node. E.g.:
+            ``'/transactions'``).
+        @param json (dict): Payload to be sent with the HTTP request.
+        @param params (dict): Dictionary of URL (query) parameters.
+        @param headers (dict): Optional headers to pass to the request.
+
+        @return Result of :meth:`requests.models.Response.json`
+
+        """
+        error_trace = []
+        timeout = self.timeout
+        backoff_cap = NO_TIMEOUT_BACKOFF_CAP if timeout is None else timeout / 2
+        while timeout is None or timeout > 0:
+            connection: Connection = self.connection_pool.get_connection()
+
+            start = time()
+            try:
+                response = connection.request(
+                    method=method,
+                    path=path,
+                    params=params,
+                    json=json,
+                    headers=headers,
+                    timeout=timeout,
+                    backoff_cap=backoff_cap,
+                )
+            except ConnectionError as err:
+                error_trace.append(err)
+                continue
+            else:
+                return response.data
+            finally:
+                elapsed = time() - start
+                if timeout is not None:
+                    timeout -= elapsed
+
+        raise TimeoutError(error_trace)
diff --git a/resdb_driver/utils.py b/resdb_driver/utils.py
new file mode 100644
index 0000000..c0741d3
--- /dev/null
+++ b/resdb_driver/utils.py
@@ -0,0 +1,139 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+from urllib.parse import urlparse, urlunparse
+import rapidjson
+import time
+import re
+
+
+from .exceptions import ValidationError
+
+
+def serialize(data: dict) -> str:
+    """! Serialize a dict into a JSON formatted string.
+
+    This function enforces rules like the separator and order of keys.
+    This ensures that all dicts are serialized in the same way.
+
+    This is specially important for hashing data. We need to make sure that
+    everyone serializes their data in the same way so that we do not have
+    hash mismatches for the same structure due to serialization
+    differences.
+
+    @param data (dict): Data to serialize
+
+    @return JSON formatted string
+
+    """
+    return rapidjson.dumps(data, skipkeys=False, ensure_ascii=False, sort_keys=True)
+
+
+def gen_timestamp():
+    """! The Unix time, rounded to the nearest second.
+    See https://en.wikipedia.org/wiki/Unix_time
+    @return The Unix time
+    """
+    return str(round(time.time()))
+
+
+DEFAULT_NODE = "http://localhost:9984"
+
+
+class CreateOperation:
+    """! Class representing the ``'CREATE'`` transaction operation.
+    """
+
+
+class TransferOperation:
+    """! Class representing the ``'TRANSFER'`` transaction operation.
+    """
+
+
+ops_map = {
+    "CREATE": CreateOperation,
+    "TRANSFER": TransferOperation,
+}
+
+
+def _normalize_operation(operation):
+    """! Normalizes the given operation string. For now, this simply means
+    converting the given string to uppercase, looking it up in
+    :attr:`~.ops_map`, and returning the corresponding class if
+    present.
+
+    @param operation (str): The operation string to convert.
+
+    @return The class corresponding to the given string,
+            :class:`~.CreateOperation` or :class:`~TransferOperation`.
+
+        .. important:: If the :meth:`str.upper` step, or the
+            :attr:`~.ops_map` lookup fails, the given ``operation``
+            argument is returned.
+
+    """
+    try:
+        operation = operation.upper()
+    except AttributeError:
+        pass
+
+    try:
+        operation = ops_map[operation]()
+    except KeyError:
+        pass
+
+    return operation
+
+
+def _get_default_port(scheme):
+    return 443 if scheme == "https" else 9984
+
+
+def normalize_url(node):
+    """! Normalizes the given node url"""
+    if not node:
+        node = DEFAULT_NODE
+    elif "://" not in node:
+        node = "//{}".format(node)
+    parts = urlparse(node, scheme="http", allow_fragments=False)
+    port = parts.port if parts.port else _get_default_port(parts.scheme)
+    netloc = "{}:{}".format(parts.hostname, port)
+    return urlunparse((parts.scheme, netloc, parts.path, "", "", ""))
+
+
+def normalize_node(node, headers=None):
+    """! Normalizes given node as str or dict with headers"""
+    headers = {} if headers is None else headers
+    if isinstance(node, str):
+        url = normalize_url(node)
+        return {"endpoint": url, "headers": headers}
+
+    url = normalize_url(node["endpoint"])
+    node_headers = node.get("headers", {})
+    return {"endpoint": url, "headers": {**headers, **node_headers}}
+
+
+def normalize_nodes(*nodes, headers=None):
+    """! Normalizes given dict or array of driver nodes"""
+    if not nodes:
+        return (normalize_node(DEFAULT_NODE, headers),)
+
+    normalized_nodes = ()
+    for node in nodes:
+        normalized_nodes += (normalize_node(node, headers),)
+    return normalized_nodes
diff --git a/resdb_driver/validate.py b/resdb_driver/validate.py
new file mode 100644
index 0000000..a696898
--- /dev/null
+++ b/resdb_driver/validate.py
@@ -0,0 +1,90 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+
+# from resdb_driver.backend.schema import validate_language_key
+from resdb_driver.exceptions import InvalidSignature, DuplicateTransaction
+
+# from resdb_driver.schema import validate_transaction_schema
+from resdb_driver.transaction import Transaction
+
+# from resdb_driver.utils import (validate_txn_obj, validate_key)
+
+
+class Transaction(Transaction):
+    ASSET = "asset"
+    METADATA = "metadata"
+    DATA = "data"
+
+    def validate(self, resdb, current_transactions=[]):
+        """! Validate transaction spend
+        @param resdb An instantiated resdb_driver.Resdb object.
+        @return The transaction (Transaction) if the transaction is valid else it
+                raises an exception describing the reason why the transaction is
+                invalid.
+
+        @exception ValidationError: If the transaction is invalid
+
+        """
+        input_conditions = []
+
+        if self.operation == Transaction.CREATE:
+            duplicates = any(
+                txn for txn in current_transactions if txn.id == self.id)
+            # TODO check if id already committed
+            # if resdb.is_committed(self.id) or duplicates:
+            #     raise DuplicateTransaction('transaction `{}` already exists'
+            #                                .format(self.id))
+
+            if not self.inputs_valid(input_conditions):
+                raise InvalidSignature("Transaction signature is invalid.")
+
+        elif self.operation == Transaction.TRANSFER:
+            self.validate_transfer_inputs(resdb, current_transactions)
+
+        return self
+
+    @classmethod
+    def from_dict(cls, tx_body):
+        return super().from_dict(tx_body)
+
+    # @classmethod
+    # def validate_schema(cls, tx_body):
+    #     validate_transaction_schema(tx_body)
+    #     validate_txn_obj(cls.ASSET, tx_body[cls.ASSET], cls.DATA, validate_key)
+    #     validate_txn_obj(cls.METADATA, tx_body, cls.METADATA, validate_key)
+    #     validate_language_key(tx_body[cls.ASSET], cls.DATA)
+    #     validate_language_key(tx_body, cls.METADATA)
+
+
+class FastTransaction:
+    """! A minimal wrapper around a transaction dictionary. This is useful for
+    when validation is not required but a routine expects something that looks
+    like a transaction, for example during block creation.
+
+    Note: immutability could also be provided
+    """
+
+    def __init__(self, tx_dict):
+        self.data = tx_dict
+
+    @property
+    def id(self):
+        return self.data["id"]
+
+    def to_dict(self):
+        return self.data
diff --git a/resdb_orm/__init__.py b/resdb_orm/__init__.py
new file mode 100644
index 0000000..8058231
--- /dev/null
+++ b/resdb_orm/__init__.py
@@ -0,0 +1,2 @@
+# resdb_orm/__init__.py
+from .orm import ResDBORM, connect
diff --git a/resdb_orm/__pycache__/__init__.cpython-310.pyc b/resdb_orm/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..becf40b
--- /dev/null
+++ b/resdb_orm/__pycache__/__init__.cpython-310.pyc
Binary files differ
diff --git a/resdb_orm/__pycache__/orm.cpython-310.pyc b/resdb_orm/__pycache__/orm.cpython-310.pyc
new file mode 100644
index 0000000..b4a6735
--- /dev/null
+++ b/resdb_orm/__pycache__/orm.cpython-310.pyc
Binary files differ
diff --git a/resdb_orm/orm.py b/resdb_orm/orm.py
new file mode 100644
index 0000000..457a840
--- /dev/null
+++ b/resdb_orm/orm.py
@@ -0,0 +1,66 @@
+# resdb_orm/orm.py
+import requests
+import json
+from resdb_driver import Resdb
+from resdb_driver.crypto import generate_keypair
+
+class ResDBORM:
+    def __init__(self, conn_string):
+        self.db_root_url = conn_string
+        self.db = Resdb(conn_string)
+
+    def create(self, data):
+        keypair = generate_keypair()
+        prepared_tx = self.db.transactions.prepare(
+            operation="CREATE",
+            signers=keypair.public_key,
+            asset={"data": data},
+        )
+        fulfilled_tx = self.db.transactions.fulfill(prepared_tx, private_keys=keypair.private_key)
+        response = self.db.transactions.send_commit(fulfilled_tx)
+
+        alphanumeric_id = response.split(":")[1].strip()
+        return alphanumeric_id
+
+    def read(self, key):
+        response = requests.get(f'{self.db_root_url}/v1/transactions/{key}')
+        return response.json()
+
+    def delete(self, key):
+        payload = {"id": key}
+        print("cp1")
+        headers = {'Content-Type': 'application/json'}
+        response = requests.post(f'{self.db_root_url}/v1/transactions/commit', 
+                     data=json.dumps(payload), headers=headers)
+    
+        print("cp2")
+        # Check if response is successful and handle empty response content
+        if response.status_code == 201:
+            if response.content:
+                return {"status": "delete successful"} 
+            else:
+                return {"status": "delete successful, no content in response"}
+    
+    def update(self, key, new_data):
+        # Delete the existing record first
+        delete_response = self.delete(key)
+    
+        # Handle the response accordingly
+        if "status" in delete_response and "no content in response" in delete_response["status"]:
+            print("Warning: Delete operation returned no content.")
+    
+        # Update by creating a new entry with the same key
+        payload = {"id": key, "value": new_data}
+        headers = {'Content-Type': 'application/json'}
+        response = requests.post(f'{self.db_root_url}/v1/transactions/commit', 
+                             data=json.dumps(payload), headers=headers)
+    
+        # Check if response is successful and handle empty response content
+        if response.status_code == 201:
+            if response.content:
+                return {"status": "update successful"}
+            else:
+                return {"status": "update successful, no content in response"}
+
+def connect(conn_string):
+    return ResDBORM(conn_string)
diff --git a/resdb_orm/resdb_orm.egg-info/PKG-INFO b/resdb_orm/resdb_orm.egg-info/PKG-INFO
new file mode 100644
index 0000000..b9b15d5
--- /dev/null
+++ b/resdb_orm/resdb_orm.egg-info/PKG-INFO
@@ -0,0 +1,11 @@
+Metadata-Version: 2.1
+Name: resdb-orm
+Version: 0.1
+Summary: A simple ORM for ResilientDB's key-value store.
+Home-page: UNKNOWN
+Author: Your Name
+License: UNKNOWN
+Platform: UNKNOWN
+
+UNKNOWN
+
diff --git a/resdb_orm/resdb_orm.egg-info/SOURCES.txt b/resdb_orm/resdb_orm.egg-info/SOURCES.txt
new file mode 100644
index 0000000..e90d949
--- /dev/null
+++ b/resdb_orm/resdb_orm.egg-info/SOURCES.txt
@@ -0,0 +1,6 @@
+setup.py
+resdb_orm.egg-info/PKG-INFO
+resdb_orm.egg-info/SOURCES.txt
+resdb_orm.egg-info/dependency_links.txt
+resdb_orm.egg-info/requires.txt
+resdb_orm.egg-info/top_level.txt
\ No newline at end of file
diff --git a/resdb_orm/resdb_orm.egg-info/dependency_links.txt b/resdb_orm/resdb_orm.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/resdb_orm/resdb_orm.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/resdb_orm/resdb_orm.egg-info/requires.txt b/resdb_orm/resdb_orm.egg-info/requires.txt
new file mode 100644
index 0000000..f229360
--- /dev/null
+++ b/resdb_orm/resdb_orm.egg-info/requires.txt
@@ -0,0 +1 @@
+requests
diff --git a/resdb_orm/resdb_orm.egg-info/top_level.txt b/resdb_orm/resdb_orm.egg-info/top_level.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/resdb_orm/resdb_orm.egg-info/top_level.txt
@@ -0,0 +1 @@
+
diff --git a/resdb_orm/setup.py b/resdb_orm/setup.py
new file mode 100644
index 0000000..5dfea9f
--- /dev/null
+++ b/resdb_orm/setup.py
@@ -0,0 +1,13 @@
+# setup.py
+from setuptools import setup, find_packages
+
+setup(
+    name="resdb_orm",
+    version="0.1",
+    packages=find_packages(),
+    install_requires=[
+        "requests",
+    ],
+    description="A simple ORM for ResilientDB's key-value store.",
+    author="Your Name",
+)
diff --git a/temp_req.txt b/temp_req.txt
new file mode 100644
index 0000000..36841b9
--- /dev/null
+++ b/temp_req.txt
@@ -0,0 +1,16 @@
+base58==2.1.0
+certifi==2024.7.4
+cffi==1.17.0
+charset-normalizer==2.1.1
+cryptoconditions==0.8.1
+cryptography==3.4.7
+idna==3.7
+pyasn1==0.4.8
+pycparser==2.22
+PyNaCl==1.4.0
+pysha3==1.0.2
+python-rapidjson==1.8
+requests==2.28.1
+resdb-orm==0.1
+six==1.16.0
+urllib3==1.26.19
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..27e39ab
--- /dev/null
+++ b/test.py
@@ -0,0 +1,25 @@
+# another_script.py
+
+from resdb_orm import connect
+import requests
+
+db = connect("http://0.0.0.0:18000")
+
+# Create records
+data = {"name": "abc", "age": 123}
+create_response = db.create(data)
+print("Create Response:", create_response)
+
+# Retrieve records
+read_response = db.read(create_response)
+print("Read Response:", read_response)
+print("Data:", read_response["asset"]["data"])
+
+# Update records
+update_response = db.update(create_response, {"name": "def", "age": 456})
+print("Update Response:", update_response)
+
+# Delete records
+delete_response = db.delete(create_response)
+print("Delete Response:", delete_response)
+