from bigchaindb_driver import BigchainDB
from bigchaindb_driver.crypto import generate_keypair
from bigchaindb_driver.exceptions import (
BigchaindbException,
NotFoundError,
MissingPrivateKeyError,
TransportError,
ConnectionError,
)
from coalaip.exceptions import (
EntityCreationError,
EntityNotFoundError,
EntityTransferError,
)
from coalaip.plugin import AbstractPlugin
from coalaip_bigchaindb.utils import (
make_transfer_tx,
order_transactions,
reraise_as_persistence_error_if_not,
)
[docs]class Plugin(AbstractPlugin):
"""BigchainDB ledger plugin for `COALA IP's Python reference
implementation <https://github.com/bigchaindb/pycoalaip>`_.
Plugs in a BigchainDB instance as the persistence layer for COALA IP
related actions.
"""
[docs] def __init__(self, *nodes):
"""Initialize a :class:`~.Plugin` instance and connect to one or
more BigchainDB nodes.
Args:
*nodes (str): One or more URLs of BigchainDB nodes to
connect to as the persistence layer
"""
self.driver = BigchainDB(*nodes)
@property
def type(self):
"""str: the type of this plugin (``'BigchainDB'``)"""
return 'BigchainDB'
[docs] def generate_user(self):
"""Create a new public/private keypair for use with
BigchainDB.
Returns:
dict: A dict containing a new user's public and private
keys::
{
'public_key': (str),
'private_key': (str),
}
"""
return generate_keypair()._asdict()
[docs] def is_same_user(self, user_a, user_b):
"""Check if :attr:`user_a` represents the same user as
:attr:`user_b` on BigchainDB by comparing their public keys.
"""
return user_a['public_key'] == user_b['public_key']
[docs] @reraise_as_persistence_error_if_not(EntityNotFoundError)
def get_history(self, persist_id):
"""Get the transaction history of an COALA IP entity on
BigchainDB.
Args:
persist_id (str): Asset id of the entity on the connected
BigchainDB instance
Returns:
list of dict: The ownership history of the entity, sorted
starting from the beginning of the entity's history
(i.e. creation). Each dict is of the form::
{
'user': A dict holding only the user's public key
(the private key is omitted as None).
'event_id': The transaction id for the ownership event
}
Raises:
:exc:`coalaip.EntityNotFoundError`: If no asset whose id
matches :attr:`persist_id` could be found in the
connected BigchainDB instance
:exc:`~.PersistenceError`: If any other unhandled error
from the BigchainDB driver occurred.
"""
try:
transactions = self.driver.transactions.get(asset_id=persist_id)
except NotFoundError:
raise EntityNotFoundError()
# Assume that each transaction will only ever have one owner
# (and therefore one output as well)
history = [{
'user': {
'public_key': tx['outputs'][0]['public_keys'][0],
'private_key': None
},
'event_id': tx['id'],
} for tx in order_transactions(transactions)]
return history
[docs] @reraise_as_persistence_error_if_not(EntityNotFoundError)
def get_status(self, persist_id):
"""Get the status of an COALA IP entity on BigchainDB.
Args:
persist_id (str): Asset id of the entity on the connected
BigchainDB instance
Returns:
str: the status of the entity; one of::
'valid': the transaction has been written in a validated block
'invalid': the block the transaction was in was voted invalid
'undecided': the block the transaction is in is still undecided
'backlog': the transaction is still in the backlog
Raises:
:exc:`coalaip.EntityNotFoundError`: If no transaction whose
'uuid' matches :attr:`persist_id` could be found in the
connected BigchainDB instance
:exc:`~.PersistenceError`: If any other unhandled error
from the BigchainDB driver occurred.
"""
try:
return self.driver.transactions.status(persist_id)
except NotFoundError:
raise EntityNotFoundError()
[docs] @reraise_as_persistence_error_if_not(EntityCreationError)
def save(self, entity_data, *, user):
"""Create and assign a new entity with the given data to the
given user's public key on BigchainDB.
Args:
entity_data (dict): A dict holding the entity's data that
will be saved in a new asset's asset definition
user (dict, keyword): The user to assign the created entity
to on BigchainDB. A dict containing::
{
'public_key': (str),
'private_key': (str),
}
where 'public_key' and 'private_key' are the user's
respective public and private keys.
Returns:
str: Asset id of the new entity
Raises:
:exc:`coalaip.EntityCreationError`: If the creation
transaction fails
:exc:`~.PersistenceError`: If any other unhandled error
from the BigchainDB driver occurred.
"""
try:
tx = self.driver.transactions.prepare(
operation='CREATE',
signers=user['public_key'],
asset={'data': entity_data})
except BigchaindbException as ex:
raise EntityCreationError(error=ex) from ex
try:
fulfilled_tx = self.driver.transactions.fulfill(
tx, private_keys=user['private_key'])
except MissingPrivateKeyError as ex:
raise EntityCreationError(error=ex) from ex
try:
self.driver.transactions.send(fulfilled_tx)
except (TransportError, ConnectionError) as ex:
raise EntityCreationError(error=ex) from ex
return fulfilled_tx['id']
[docs] @reraise_as_persistence_error_if_not(EntityNotFoundError)
def load(self, persist_id):
"""Load the data of the entity associated with the
:attr:`persist_id` from BigchainDB.
Args:
persist_id (str): Asset id of the entity being loaded on the
connected BigchainDB instance
Returns:
dict: The persisted data of the entity
Raises:
:exc:`coalaip.EntityNotFoundError`: If no asset whose id
matches :attr:`persist_id` could be found in the connected
BigchainDB instance
:exc:`~.PersistenceError`: If any other unhandled error
from the BigchainDB driver occurred.
"""
try:
tx_json = self.driver.transactions.retrieve(persist_id)
except NotFoundError:
raise EntityNotFoundError()
if tx_json['operation'] == 'CREATE':
return tx_json['asset']['data']
else:
return tx_json['metadata']
[docs] @reraise_as_persistence_error_if_not(EntityNotFoundError,
EntityTransferError)
def transfer(self, persist_id, transfer_payload=None, *, from_user,
to_user):
"""Transfer the entity matching the given :attr:`persist_id`
from the current owner (:attr:`from_user`) to a new owner
(:attr:`to_user`).
Args:
persist_id (str): Asset id of the entity on the connected
BigchainDB instance
transfer_payload (dict, optional): A dict holding the
transfer's payload
from_user (dict, keyword): A dict holding the current
owner's public key and private key (see
:meth:`generate_user`)
to_user (dict, keyword): A dict holding the new owner's
public key and private key (see
:meth:`generate_user`)
Returns:
str: Id of the transaction transferring the entity from
:attr:`from_user` to :attr:`to_user`
Raises:
:exc:`coalaip.EntityNotFoundError`: If no asset whose id
matches :attr:`persist_id` could be found in the
connected BigchainDB instance
:exc:`coalaip.EntityTransferError`: If the transfer
transaction fails
:exc:`~.PersistenceError`: If any other unhandled error
from the BigchainDB driver occurred.
"""
try:
ordered_tx = order_transactions(
self.driver.transactions.get(asset_id=persist_id))
last_tx = ordered_tx[-1]
except NotFoundError:
raise EntityNotFoundError()
try:
transfer_tx = make_transfer_tx(self.driver, input_tx=last_tx,
recipients=to_user['public_key'],
metadata=transfer_payload)
except BigchaindbException as ex:
raise EntityTransferError(error=ex) from ex
try:
fulfilled_tx = self.driver.transactions.fulfill(
transfer_tx, private_keys=from_user['private_key'])
except MissingPrivateKeyError as ex:
raise EntityTransferError(error=ex) from ex
try:
transfer_json = self.driver.transactions.send(fulfilled_tx)
except (TransportError, ConnectionError) as ex:
raise EntityTransferError(error=ex) from ex
return transfer_json['id']