resdb_driver/offchain.py (101 lines of code) (raw):
# 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()