mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Added ability to CREATE
divisible assets
This commit is contained in:
parent
d47ed214cd
commit
dd382ee4e6
@ -9,7 +9,8 @@ from cryptoconditions.exceptions import ParsingError
|
|||||||
|
|
||||||
from bigchaindb.common.crypto import SigningKey, hash_data
|
from bigchaindb.common.crypto import SigningKey, hash_data
|
||||||
from bigchaindb.common.exceptions import (KeypairMismatchException,
|
from bigchaindb.common.exceptions import (KeypairMismatchException,
|
||||||
InvalidHash, InvalidSignature)
|
InvalidHash, InvalidSignature,
|
||||||
|
AmountError)
|
||||||
from bigchaindb.common.util import serialize, gen_timestamp
|
from bigchaindb.common.util import serialize, gen_timestamp
|
||||||
|
|
||||||
|
|
||||||
@ -268,7 +269,7 @@ class Condition(object):
|
|||||||
return cond
|
return cond
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate(cls, owners_after):
|
def generate(cls, owners_after, amount=1):
|
||||||
"""Generates a Condition from a specifically formed tuple or list.
|
"""Generates a Condition from a specifically formed tuple or list.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@ -305,7 +306,9 @@ class Condition(object):
|
|||||||
owners_after, threshold = owners_after
|
owners_after, threshold = owners_after
|
||||||
else:
|
else:
|
||||||
threshold = len(owners_after)
|
threshold = len(owners_after)
|
||||||
|
|
||||||
|
if not isinstance(amount, int):
|
||||||
|
raise TypeError('`amount` must be a int')
|
||||||
if not isinstance(owners_after, list):
|
if not isinstance(owners_after, list):
|
||||||
raise TypeError('`owners_after` must be an instance of list')
|
raise TypeError('`owners_after` must be an instance of list')
|
||||||
if len(owners_after) == 0:
|
if len(owners_after) == 0:
|
||||||
@ -316,12 +319,12 @@ class Condition(object):
|
|||||||
ffill = Ed25519Fulfillment(public_key=owners_after[0])
|
ffill = Ed25519Fulfillment(public_key=owners_after[0])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
ffill = owners_after[0]
|
ffill = owners_after[0]
|
||||||
return cls(ffill, owners_after)
|
return cls(ffill, owners_after, amount=amount)
|
||||||
else:
|
else:
|
||||||
initial_cond = ThresholdSha256Fulfillment(threshold=threshold)
|
initial_cond = ThresholdSha256Fulfillment(threshold=threshold)
|
||||||
threshold_cond = reduce(cls._gen_condition, owners_after,
|
threshold_cond = reduce(cls._gen_condition, owners_after,
|
||||||
initial_cond)
|
initial_cond)
|
||||||
return cls(threshold_cond, owners_after)
|
return cls(threshold_cond, owners_afteri, amount=amount)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _gen_condition(cls, initial, current):
|
def _gen_condition(cls, initial, current):
|
||||||
@ -466,7 +469,7 @@ class Asset(object):
|
|||||||
"""Generates a unqiue uuid for an Asset"""
|
"""Generates a unqiue uuid for an Asset"""
|
||||||
return str(uuid4())
|
return str(uuid4())
|
||||||
|
|
||||||
def _validate_asset(self):
|
def _validate_asset(self, amount=None):
|
||||||
"""Validates the asset"""
|
"""Validates the asset"""
|
||||||
if self.data is not None and not isinstance(self.data, dict):
|
if self.data is not None and not isinstance(self.data, dict):
|
||||||
raise TypeError('`data` must be a dict instance or None')
|
raise TypeError('`data` must be a dict instance or None')
|
||||||
@ -477,6 +480,29 @@ class Asset(object):
|
|||||||
if not isinstance(self.updatable, bool):
|
if not isinstance(self.updatable, bool):
|
||||||
raise TypeError('`updatable` must be a boolean')
|
raise TypeError('`updatable` must be a boolean')
|
||||||
|
|
||||||
|
if self.refillable:
|
||||||
|
raise NotImplementedError('Refillable assets are not yet'
|
||||||
|
' implemented')
|
||||||
|
if self.updatable:
|
||||||
|
raise NotImplementedError('Updatable assets are not yet'
|
||||||
|
' implemented')
|
||||||
|
|
||||||
|
# If the amount is supplied we can perform extra validations to
|
||||||
|
# the asset
|
||||||
|
if amount is not None:
|
||||||
|
if not isinstance(amount, int):
|
||||||
|
raise TypeError('`amount` must be an int')
|
||||||
|
|
||||||
|
if self.divisible is False and amount != 1:
|
||||||
|
raise AmountError('non divisible assets always have'
|
||||||
|
' amount equal to one')
|
||||||
|
|
||||||
|
# Since refillable assets are not yet implemented this should
|
||||||
|
# raise and exception
|
||||||
|
if self.divisible is True and amount < 2:
|
||||||
|
raise AmountError('divisible assets must have an amount'
|
||||||
|
' greater than one')
|
||||||
|
|
||||||
|
|
||||||
class Metadata(object):
|
class Metadata(object):
|
||||||
"""Metadata is used to store a dictionary and its hash in a Transaction."""
|
"""Metadata is used to store a dictionary and its hash in a Transaction."""
|
||||||
@ -621,6 +647,7 @@ class Transaction(object):
|
|||||||
|
|
||||||
if conditions is not None and not isinstance(conditions, list):
|
if conditions is not None and not isinstance(conditions, list):
|
||||||
raise TypeError('`conditions` must be a list instance or None')
|
raise TypeError('`conditions` must be a list instance or None')
|
||||||
|
# TODO: Check if there is a case in which conditions may be None
|
||||||
elif conditions is None:
|
elif conditions is None:
|
||||||
self.conditions = []
|
self.conditions = []
|
||||||
else:
|
else:
|
||||||
@ -628,6 +655,7 @@ class Transaction(object):
|
|||||||
|
|
||||||
if fulfillments is not None and not isinstance(fulfillments, list):
|
if fulfillments is not None and not isinstance(fulfillments, list):
|
||||||
raise TypeError('`fulfillments` must be a list instance or None')
|
raise TypeError('`fulfillments` must be a list instance or None')
|
||||||
|
# TODO: Check if there is a case in which fulfillments may be None
|
||||||
elif fulfillments is None:
|
elif fulfillments is None:
|
||||||
self.fulfillments = []
|
self.fulfillments = []
|
||||||
else:
|
else:
|
||||||
@ -638,9 +666,16 @@ class Transaction(object):
|
|||||||
else:
|
else:
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
|
|
||||||
|
# validate asset
|
||||||
|
# we know that each transaction relates to a single asset
|
||||||
|
# we can sum the amount of all the conditions
|
||||||
|
amount = sum([condition.amount for condition in self.conditions])
|
||||||
|
self.asset._validate_asset(amount=amount)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, owners_before, owners_after, metadata=None, asset=None,
|
def create(cls, owners_before, owners_after, metadata=None, asset=None,
|
||||||
secret=None, time_expire=None):
|
secret=None, time_expire=None, amount=1):
|
||||||
"""A simple way to generate a `CREATE` transaction.
|
"""A simple way to generate a `CREATE` transaction.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@ -675,6 +710,8 @@ class Transaction(object):
|
|||||||
raise TypeError('`owners_before` must be a list instance')
|
raise TypeError('`owners_before` must be a list instance')
|
||||||
if not isinstance(owners_after, list):
|
if not isinstance(owners_after, list):
|
||||||
raise TypeError('`owners_after` must be a list instance')
|
raise TypeError('`owners_after` must be a list instance')
|
||||||
|
if not isinstance(amount, int):
|
||||||
|
raise TypeError('`amount` must be a int')
|
||||||
|
|
||||||
metadata = Metadata(metadata)
|
metadata = Metadata(metadata)
|
||||||
if len(owners_before) == len(owners_after) and len(owners_after) == 1:
|
if len(owners_before) == len(owners_after) and len(owners_after) == 1:
|
||||||
@ -683,7 +720,7 @@ class Transaction(object):
|
|||||||
# fulfillment for the fulfillment and condition.
|
# fulfillment for the fulfillment and condition.
|
||||||
ffill = Ed25519Fulfillment(public_key=owners_before[0])
|
ffill = Ed25519Fulfillment(public_key=owners_before[0])
|
||||||
ffill_tx = Fulfillment(ffill, owners_before)
|
ffill_tx = Fulfillment(ffill, owners_before)
|
||||||
cond_tx = Condition.generate(owners_after)
|
cond_tx = Condition.generate(owners_after, amount=amount)
|
||||||
return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata)
|
return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata)
|
||||||
|
|
||||||
elif len(owners_before) == len(owners_after) and len(owners_after) > 1:
|
elif len(owners_before) == len(owners_after) and len(owners_after) > 1:
|
||||||
@ -693,7 +730,8 @@ class Transaction(object):
|
|||||||
ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before),
|
ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before),
|
||||||
[owner_before])
|
[owner_before])
|
||||||
for owner_before in owners_before]
|
for owner_before in owners_before]
|
||||||
conds = [Condition.generate(owners) for owners in owners_after]
|
conds = [Condition.generate(owners, amount=amount)
|
||||||
|
for owners in owners_after]
|
||||||
return cls(cls.CREATE, asset, ffills, conds, metadata)
|
return cls(cls.CREATE, asset, ffills, conds, metadata)
|
||||||
|
|
||||||
elif len(owners_before) == 1 and len(owners_after) > 1:
|
elif len(owners_before) == 1 and len(owners_after) > 1:
|
||||||
@ -707,7 +745,7 @@ class Transaction(object):
|
|||||||
secret is not None):
|
secret is not None):
|
||||||
# NOTE: Hashlock condition case
|
# NOTE: Hashlock condition case
|
||||||
hashlock = PreimageSha256Fulfillment(preimage=secret)
|
hashlock = PreimageSha256Fulfillment(preimage=secret)
|
||||||
cond_tx = Condition(hashlock.condition_uri)
|
cond_tx = Condition(hashlock.condition_uri, amount=amount)
|
||||||
ffill = Ed25519Fulfillment(public_key=owners_before[0])
|
ffill = Ed25519Fulfillment(public_key=owners_before[0])
|
||||||
ffill_tx = Fulfillment(ffill, owners_before)
|
ffill_tx = Fulfillment(ffill, owners_before)
|
||||||
return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata)
|
return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata)
|
||||||
|
@ -183,7 +183,7 @@ class Bigchain(object):
|
|||||||
except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist,
|
except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist,
|
||||||
exceptions.TransactionOwnerError, exceptions.DoubleSpend,
|
exceptions.TransactionOwnerError, exceptions.DoubleSpend,
|
||||||
exceptions.InvalidHash, exceptions.InvalidSignature,
|
exceptions.InvalidHash, exceptions.InvalidSignature,
|
||||||
exceptions.FulfillmentNotInValidBlock):
|
exceptions.FulfillmentNotInValidBlock, exceptions.AmountError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_transaction(self, txid, include_status=False):
|
def get_transaction(self, txid, include_status=False):
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from ..db.conftest import inputs
|
from ..db.conftest import inputs
|
||||||
|
|
||||||
|
|
||||||
@ -161,3 +163,47 @@ def test_get_txs_by_asset_id(b, user_vk, user_sk):
|
|||||||
assert tx_transfer.id in [t.id for t in txs]
|
assert tx_transfer.id in [t.id for t in txs]
|
||||||
assert asset_id == txs[0].asset.data_id
|
assert asset_id == txs[0].asset.data_id
|
||||||
assert asset_id == txs[1].asset.data_id
|
assert asset_id == txs[1].asset.data_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_invalid_divisible_asset(b, user_vk, user_sk):
|
||||||
|
from bigchaindb.models import Transaction, Asset
|
||||||
|
from bigchaindb.common.exceptions import AmountError
|
||||||
|
|
||||||
|
# non divisible assets cannot have amount > 1
|
||||||
|
# Transaction.__init__ should raise an exception
|
||||||
|
asset = Asset(divisible=False)
|
||||||
|
with pytest.raises(AmountError):
|
||||||
|
Transaction.create([user_vk], [user_vk], asset=asset, amount=2)
|
||||||
|
|
||||||
|
# divisible assets need to have an amount > 1
|
||||||
|
# Transaction.__init__ should raise an exception
|
||||||
|
asset = Asset(divisible=True)
|
||||||
|
with pytest.raises(AmountError):
|
||||||
|
Transaction.create([user_vk], [user_vk], asset=asset, amount=1)
|
||||||
|
|
||||||
|
# even if a transaction is badly constructed the server should raise the
|
||||||
|
# exception
|
||||||
|
asset = Asset(divisible=False)
|
||||||
|
with patch.object(Asset, '_validate_asset', return_value=None):
|
||||||
|
tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=2)
|
||||||
|
tx_signed = tx.sign([user_sk])
|
||||||
|
with pytest.raises(AmountError):
|
||||||
|
tx_signed.validate(b)
|
||||||
|
assert b.is_valid_transaction(tx_signed) is False
|
||||||
|
|
||||||
|
asset = Asset(divisible=True)
|
||||||
|
with patch.object(Asset, '_validate_asset', return_value=None):
|
||||||
|
tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=1)
|
||||||
|
tx_signed = tx.sign([user_sk])
|
||||||
|
with pytest.raises(AmountError):
|
||||||
|
tx_signed.validate(b)
|
||||||
|
assert b.is_valid_transaction(tx_signed) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_valid_divisible_asset(b, user_vk, user_sk):
|
||||||
|
from bigchaindb.models import Transaction, Asset
|
||||||
|
|
||||||
|
asset = Asset(divisible=True)
|
||||||
|
tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=2)
|
||||||
|
tx_signed = tx.sign([user_sk])
|
||||||
|
assert b.is_valid_transaction(tx_signed)
|
||||||
|
@ -22,6 +22,7 @@ def test_asset_creation_with_data(data):
|
|||||||
def test_asset_invalid_asset_initialization():
|
def test_asset_invalid_asset_initialization():
|
||||||
from bigchaindb.common.transaction import Asset
|
from bigchaindb.common.transaction import Asset
|
||||||
|
|
||||||
|
# check types
|
||||||
with raises(TypeError):
|
with raises(TypeError):
|
||||||
Asset(data='some wrong type')
|
Asset(data='some wrong type')
|
||||||
with raises(TypeError):
|
with raises(TypeError):
|
||||||
@ -31,6 +32,12 @@ def test_asset_invalid_asset_initialization():
|
|||||||
with raises(TypeError):
|
with raises(TypeError):
|
||||||
Asset(updatable=1)
|
Asset(updatable=1)
|
||||||
|
|
||||||
|
# check for features that are not yet implemented
|
||||||
|
with raises(NotImplementedError):
|
||||||
|
Asset(updatable=True)
|
||||||
|
with raises(NotImplementedError):
|
||||||
|
Asset(refillable=True)
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_asset_comparison(data, data_id):
|
def test_invalid_asset_comparison(data, data_id):
|
||||||
from bigchaindb.common.transaction import Asset
|
from bigchaindb.common.transaction import Asset
|
||||||
@ -69,12 +76,17 @@ def test_asset_deserialization(data, data_id):
|
|||||||
|
|
||||||
def test_validate_asset():
|
def test_validate_asset():
|
||||||
from bigchaindb.common.transaction import Asset
|
from bigchaindb.common.transaction import Asset
|
||||||
|
from bigchaindb.common.exceptions import AmountError
|
||||||
|
|
||||||
|
# test amount errors
|
||||||
|
asset = Asset(divisible=False)
|
||||||
|
with raises(AmountError):
|
||||||
|
asset._validate_asset(amount=2)
|
||||||
|
|
||||||
|
asset = Asset(divisible=True)
|
||||||
|
with raises(AmountError):
|
||||||
|
asset._validate_asset(amount=1)
|
||||||
|
|
||||||
|
asset = Asset()
|
||||||
with raises(TypeError):
|
with raises(TypeError):
|
||||||
Asset(divisible=1)
|
asset._validate_asset(amount='a')
|
||||||
with raises(TypeError):
|
|
||||||
Asset(refillable=1)
|
|
||||||
with raises(TypeError):
|
|
||||||
Asset(updatable=1)
|
|
||||||
with raises(TypeError):
|
|
||||||
Asset(data='we need more lemon pledge')
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user