diff --git a/planetmint/transactions/common/transaction.py b/planetmint/transactions/common/transaction.py index c7301f6..aa8de36 100644 --- a/planetmint/transactions/common/transaction.py +++ b/planetmint/transactions/common/transaction.py @@ -25,13 +25,13 @@ try: except ImportError: from sha3 import sha3_256 -from planetmint.common.crypto import PrivateKey, hash_data -from planetmint.common.exceptions import (KeypairMismatchException, +from planetmint.transactions.common.crypto import PrivateKey, hash_data +from planetmint.transactions.common.exceptions import (KeypairMismatchException, InputDoesNotExist, DoubleSpend, InvalidHash, InvalidSignature, AmountError, AssetIdMismatch, ThresholdTooDeep) -from planetmint.common.utils import serialize +from planetmint.transactions.common.utils import serialize from .memoize import memoize_from_dict, memoize_to_dict diff --git a/planetmint/elections/__init__.py b/planetmint/transactions/types/assets/__init__.py similarity index 100% rename from planetmint/elections/__init__.py rename to planetmint/transactions/types/assets/__init__.py diff --git a/planetmint/transactions/types/assets/create.py b/planetmint/transactions/types/assets/create.py new file mode 100644 index 0000000..8af8216 --- /dev/null +++ b/planetmint/transactions/types/assets/create.py @@ -0,0 +1,78 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from planetmint.transactions.common.transaction import (Transaction, Input, Output) + +class Create(Transaction): + + OPERATION = 'CREATE' + # NOTE: This class inherits TRANSFER txn type. The `TRANSFER` property is + # overriden to re-use methods from parent class + TRANSFER = OPERATION + ALLOWED_OPERATIONS = (OPERATION,) + + @classmethod + def validate(self, tx_signers, recipients, asset, metadata): + 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') + if not (metadata is None or isinstance(metadata, dict)): + raise TypeError('`metadata` 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 `([],' + ' )`')) + pub_keys, amount = recipient + outputs.append(Output.generate(pub_keys, amount)) + + # generate inputs + inputs.append(Input.generate(tx_signers)) + + return (inputs, outputs) + + @classmethod + def generate(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 Planetmint + use cases: + - Multiple inputs and outputs. + + Args: + tx_signers (:obj:`list` of :obj:`str`): A list of keys that + represent the signers of the CREATE Transaction. + recipients (:obj:`list` of :obj:`tuple`): A list of + ([keys],amount) that represent the recipients of this + Transaction. + metadata (dict): The metadata to be stored along with the + Transaction. + asset (dict): The metadata associated with the asset that will + be created in this Transaction. + + Returns: + :class:`~planetmint.common.transaction.Transaction` + """ + + (inputs, outputs) = cls.validate(tx_signers, recipients, asset, metadata) + return cls(cls.OPERATION, {'data': asset}, inputs, outputs, metadata) diff --git a/planetmint/transactions/types/assets/transfer.py b/planetmint/transactions/types/assets/transfer.py new file mode 100644 index 0000000..98c8602 --- /dev/null +++ b/planetmint/transactions/types/assets/transfer.py @@ -0,0 +1,85 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from planetmint.transactions.common.transaction import (Transaction, Input, Output) +from planetmint.transactions.common.schema import (_validate_schema, + TX_SCHEMA_COMMON, + TX_SCHEMA_TRANSFER) +from copy import deepcopy + +class Transfer(Transaction): + + OPERATION = 'TRANSFER' + # NOTE: This class inherits TRANSFER txn type. The `TRANSFER` property is + # overriden to re-use methods from parent class + TRANSFER = OPERATION + ALLOWED_OPERATIONS = (OPERATION,) + + @classmethod + def validate(cls, inputs, recipients, asset_id, metadata): + 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 `([],' + ' )`')) + 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') + + return (deepcopy(inputs), outputs) + + @classmethod + def generate(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`. + + Args: + inputs (:obj:`list` of :class:`~planetmint.common.transaction. + Input`): Converted `Output`s, intended to + be used as inputs in the transfer to generate. + recipients (:obj:`list` of :obj:`tuple`): A list of + ([keys],amount) that represent the recipients of this + Transaction. + asset_id (str): The asset ID of the asset to be transferred in + this Transaction. + metadata (dict): Python dictionary to be stored along with the + Transaction. + + Returns: + :class:`~planetmint.common.transaction.Transaction` + """ + (inputs, outputs) = cls.validate_transfer(inputs, recipients, asset_id, metadata) + return cls(cls.OPERATION, {'id': asset_id}, inputs, outputs, metadata) diff --git a/planetmint/transactions/types/elections/__init__.py b/planetmint/transactions/types/elections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/planetmint/elections/election.py b/planetmint/transactions/types/elections/election.py similarity index 95% rename from planetmint/elections/election.py rename to planetmint/transactions/types/elections/election.py index d73b319..736212a 100644 --- a/planetmint/elections/election.py +++ b/planetmint/transactions/types/elections/election.py @@ -8,16 +8,18 @@ import base58 from uuid import uuid4 from planetmint import backend -from planetmint.elections.vote import Vote -from planetmint.common.exceptions import (InvalidSignature, +from planetmint.transactions.types.assets.create import Create +from planetmint.transactions.types.assets.transfer import Transfer +from planetmint.transactions.types.elections.vote import Vote +from planetmint.transactions.common.exceptions import (InvalidSignature, MultipleInputsError, InvalidProposer, UnequalValidatorSet, DuplicateTransaction) from planetmint.tendermint_utils import key_from_base64, public_key_to_base64 -from planetmint.common.crypto import (public_key_from_ed25519_key) -from planetmint.common.transaction import Transaction -from planetmint.common.schema import (_validate_schema, +from planetmint.transactions.common.crypto import (public_key_from_ed25519_key) +from planetmint.transactions.common.transaction import Transaction +from planetmint.transactions.common.schema import (_validate_schema, TX_SCHEMA_COMMON, TX_SCHEMA_CREATE) @@ -161,11 +163,11 @@ class Election(Transaction): @classmethod def create(cls, tx_signers, recipients, metadata=None, asset=None): - raise NotImplementedError + Create.generate(tx_signers, recipients, metadata=None, asset=None) @classmethod def transfer(cls, tx_signers, recipients, metadata=None, asset=None): - raise NotImplementedError + Transfer.generate(tx_signers, recipients, metadata=None, asset=None) @classmethod def to_public_key(cls, election_id): diff --git a/planetmint/elections/vote.py b/planetmint/transactions/types/elections/vote.py similarity index 84% rename from planetmint/elections/vote.py rename to planetmint/transactions/types/elections/vote.py index a4bb0c9..16d7dcd 100644 --- a/planetmint/elections/vote.py +++ b/planetmint/transactions/types/elections/vote.py @@ -3,8 +3,10 @@ # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # Code is Apache-2.0 and docs are CC-BY-4.0 -from planetmint.common.transaction import Transaction -from planetmint.common.schema import (_validate_schema, +from planetmint.transactions.types.assets.create import Create +from planetmint.transactions.types.assets.transfer import Transfer +from planetmint.transactions.common.transaction import Transaction +from planetmint.transactions.common.schema import (_validate_schema, TX_SCHEMA_COMMON, TX_SCHEMA_TRANSFER, TX_SCHEMA_VOTE) @@ -57,8 +59,8 @@ class Vote(Transaction): @classmethod def create(cls, tx_signers, recipients, metadata=None, asset=None): - raise NotImplementedError + return Create.generate(tx_signers, recipients, metadata=None, asset=None) @classmethod def transfer(cls, tx_signers, recipients, metadata=None, asset=None): - raise NotImplementedError + return Transfer.generate(tx_signers, recipients, metadata=None, asset=None)