From 599f64f68cd681a3a87f1684df9023663931ed3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Eckel?= Date: Thu, 26 Jan 2023 17:41:45 +0100 Subject: [PATCH] Compose & Decompose Support (#304) Added the newest transaction package and support for Compose and Decompose as specified in PRP-5 https://github.com/planetmint/PRPs/tree/main/5 --- CHANGELOG.md | 3 + planetmint/lib.py | 98 +++++-- planetmint/version.py | 4 +- pytest.ini | 2 +- setup.py | 2 +- tests/assets/test_digital_assets.py | 55 ++++ .../validation/test_transaction_structure.py | 261 ------------------ tests/validation/test_validation.py | 10 + tests/web/test_transactions.py | 94 ++++++- 9 files changed, 236 insertions(+), 293 deletions(-) delete mode 100644 tests/validation/test_transaction_structure.py create mode 100644 tests/validation/test_validation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 900bdf3..38b118f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ For reference, the possible headings are: * **Known Issues** * **Notes** +## [2.1.0] - 2023-26-01 +* **Added** validation for compose and decompose transaction types + ## [2.0.0] - 2023-12-01 * **Changed** changed tarantool db schema * **Removed** removed text_search routes diff --git a/planetmint/lib.py b/planetmint/lib.py index 334d714..2bcb850 100644 --- a/planetmint/lib.py +++ b/planetmint/lib.py @@ -374,20 +374,70 @@ class Planetmint(object): return False if transaction.operation == Transaction.CREATE: - duplicates = any(txn for txn in current_transactions if txn.id == transaction.id) - if self.is_committed(transaction.id) or duplicates: - raise DuplicateTransaction("transaction `{}` already exists".format(transaction.id)) + self.validate_create_inputs(transaction, current_transactions) elif transaction.operation in [Transaction.TRANSFER, Transaction.VOTE]: self.validate_transfer_inputs(transaction, current_transactions) + elif transaction.operation in [Transaction.COMPOSE]: + self.validate_compose_inputs(transaction, current_transactions) return transaction - def validate_transfer_inputs(self, tx, current_transactions=[]): + def validate_create_inputs(self, tx, current_transactions=[]) -> bool: + duplicates = any(txn for txn in current_transactions if txn.id == tx.id) + if self.is_committed(tx.id) or duplicates: + raise DuplicateTransaction("transaction `{}` already exists".format(tx.id)) + + fulfilling_inputs = [i for i in tx.inputs if i.fulfills is not None and i.fulfills.txid is not None] + + if len(fulfilling_inputs) > 0: + input_txs, input_conditions = self.get_input_txs_and_conditions(fulfilling_inputs, current_transactions) + create_asset = tx.assets[0] + input_asset = input_txs[0].assets[tx.inputs[0].fulfills.output]["data"] + if create_asset != input_asset: + raise ValidationError("CREATE must have matching asset description with input transaction") + if input_txs[0].operation != Transaction.DECOMPOSE: + raise SchemaValidationError("CREATE can only consume DECOMPOSE outputs") + + return True + + def validate_transfer_inputs(self, tx, current_transactions=[]) -> bool: + input_txs, input_conditions = self.get_input_txs_and_conditions(tx.inputs, current_transactions) + + self.validate_input_conditions(tx, input_conditions) + + self.validate_asset_id(tx, input_txs) + + self.validate_inputs_distinct(tx) + + input_amount = sum([input_condition.amount for input_condition in input_conditions]) + output_amount = sum([output_condition.amount for output_condition in tx.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) + ) + + return True + + def validate_compose_inputs(self, tx, current_transactions=[]) -> bool: + input_txs, input_conditions = self.get_input_txs_and_conditions(tx.inputs, current_transactions) + + self.validate_input_conditions(tx, input_conditions) + + self.validate_asset_id(tx, input_txs) + + self.validate_inputs_distinct(tx) + + return True + + def get_input_txs_and_conditions(self, inputs, current_transactions=[]): # store the inputs so that we can check if the asset ids match input_txs = [] input_conditions = [] - for input_ in tx.inputs: + for input_ in inputs: input_txid = input_.fulfills.txid input_tx = self.get_transaction(input_txid) _output = self.get_outputs_by_tx_id(input_txid) @@ -416,16 +466,9 @@ class Planetmint(object): pm_transaction = Transaction.from_dict(tx_dict, False) input_txs.append(pm_transaction) - # Validate that all inputs are distinct - links = [i.fulfills.to_uri() for i in tx.inputs] - if len(links) != len(set(links)): - raise DoubleSpend('tx "{}" spends inputs twice'.format(tx.id)) - - # validate asset id - asset_id = tx.get_asset_id(input_txs) - if asset_id != Transaction.read_out_asset_id(tx): - raise AssetIdMismatch(("The asset id of the input does not" " match the asset id of the" " transaction")) + return (input_txs, input_conditions) + def validate_input_conditions(self, tx, input_conditions): # convert planetmint.Output objects to transactions.common.Output objects input_conditions_dict = Output.list_to_dict(input_conditions) input_conditions_converted = [] @@ -435,17 +478,24 @@ class Planetmint(object): if not tx.inputs_valid(input_conditions_converted): raise InvalidSignature("Transaction signature is invalid.") - input_amount = sum([input_condition.amount for input_condition in input_conditions]) - output_amount = sum([output_condition.amount for output_condition in tx.outputs]) + def validate_asset_id(self, tx: Transaction, input_txs: list): + # validate asset + if tx.operation != Transaction.COMPOSE: + asset_id = tx.get_asset_id(input_txs) + if asset_id != Transaction.read_out_asset_id(tx): + raise AssetIdMismatch( + ("The asset id of the input does not" " match the asset id of the" " transaction") + ) + else: + asset_ids = Transaction.get_asset_ids(input_txs) + if Transaction.read_out_asset_id(tx) in asset_ids: + raise AssetIdMismatch(("The asset ID of the compose must be different to all of its input asset IDs")) - 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) - ) - - return True + def validate_inputs_distinct(self, tx): + # Validate that all inputs are distinct + links = [i.fulfills.to_uri() for i in tx.inputs] + if len(links) != len(set(links)): + raise DoubleSpend('tx "{}" spends inputs twice'.format(tx.id)) def is_valid_transaction(self, tx, current_transactions=[]): # NOTE: the function returns the Transaction object in case diff --git a/planetmint/version.py b/planetmint/version.py index 1cbd364..8100550 100644 --- a/planetmint/version.py +++ b/planetmint/version.py @@ -3,8 +3,8 @@ # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # Code is Apache-2.0 and docs are CC-BY-4.0 -__version__ = "2.0.0" -__short_version__ = "2.0" +__version__ = "2.1.0" +__short_version__ = "2.1" # Supported Tendermint versions __tm_supported_versions__ = ["0.34.15"] diff --git a/pytest.ini b/pytest.ini index 423dc14..b13d936 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ [pytest] testpaths = tests/ norecursedirs = .* *.egg *.egg-info env* devenv* docs -#addopts = -m "not abci" +addopts = -m "abci" looponfailroots = planetmint tests asyncio_mode = strict markers = diff --git a/setup.py b/setup.py index a9fc93b..da186fb 100644 --- a/setup.py +++ b/setup.py @@ -129,7 +129,7 @@ install_requires = [ "planetmint-ipld>=0.0.3", "pyasn1>=0.4.8", "python-decouple", - "planetmint-transactions>=0.5.0", + "planetmint-transactions>=0.6.0", ] setup( diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index e0e678d..87a8964 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -7,6 +7,8 @@ import pytest from transactions.types.assets.create import Create from transactions.types.assets.transfer import Transfer +from transactions.types.assets.compose import Compose +from transactions.types.assets.decompose import Decompose def test_asset_transfer(b, signed_create_tx, user_pk, user_sk, _bdb): @@ -69,6 +71,59 @@ def test_asset_id_mismatch(alice, user_pk): Transaction.get_asset_id([tx1, tx2]) +def test_compose_valid_transactions(b, user_pk, user_sk, alice, signed_create_tx, _bdb): + validated = b.validate_transaction(signed_create_tx) + b.store_bulk_transactions([validated]) + + inputs = signed_create_tx.to_inputs() + assets = [signed_create_tx.id, "QmW5GVMW98D3mktSDfWHS8nX2UiCd8gP1uCiujnFX4yK8n"] + compose_transaction = Compose.generate(inputs=inputs, recipients=[([user_pk], 1)], assets=assets) + compose_transaction.sign([user_sk]) + assert b.validate_transaction(compose_transaction) + + +def test_decompose_valid_transactions(b, user_pk, user_sk, alice, signed_create_tx, _bdb): + validated = b.validate_transaction(signed_create_tx) + b.store_bulk_transactions([validated]) + + inputs = signed_create_tx.to_inputs() + assets = [ + signed_create_tx.id, + "bafkreiawyk3ou5qzqec4ggbvrs56dv5ske2viwprf6he5wj5gr4yv5orsu", + "bafkreibncbonglm6mi3znbrqbchk56wmgftk4gfevxqlgeif3g5jdotcka", + "bafkreibkokzihpnnyqf3xslcievqkadf2ozkdi72wyibijih447vq42kjm", + ] + decompose_transaction = Decompose.generate( + inputs=inputs, recipients=[([user_pk], 1), ([user_pk], 2), ([user_pk], 3)], assets=assets + ) + decompose_transaction.sign([user_sk]) + assert b.validate_transaction(decompose_transaction) + + +def test_create_decompose_output(b, user_pk, user_sk, signed_create_tx, _bdb): + validated = b.validate_transaction(signed_create_tx) + b.store_bulk_transactions([validated]) + + inputs = signed_create_tx.to_inputs() + assets = [ + "bafkreiawyk3ou5qzqec4ggbvrs56dv5ske2viwprf6he5wj5gr4yv5orsu", + "bafkreibncbonglm6mi3znbrqbchk56wmgftk4gfevxqlgeif3g5jdotcka", + "bafkreibkokzihpnnyqf3xslcievqkadf2ozkdi72wyibijih447vq42kjm", + signed_create_tx.id, + ] + decompose_transaction = Decompose.generate( + inputs=inputs, recipients=[([user_pk], 1), ([user_pk], 2), ([user_pk], 3)], assets=assets + ) + decompose_transaction.sign([user_sk]) + validated_decompose = b.validate_transaction(decompose_transaction) + b.store_bulk_transactions([validated_decompose]) + + create_inputs = decompose_transaction.to_inputs([0]) + create_tx = Create.generate([user_pk], recipients=[([user_pk], 1)], assets=[assets[0]], inputs=create_inputs) + signed_decompose_create_tx = create_tx.sign([user_sk]) + assert b.validate_transaction(signed_decompose_create_tx) + + def test_create_valid_divisible_asset(b, user_pk, user_sk, _bdb): tx = Create.generate([user_pk], [([user_pk], 2)]) tx_signed = tx.sign([user_sk]) diff --git a/tests/validation/test_transaction_structure.py b/tests/validation/test_transaction_structure.py deleted file mode 100644 index ed6eae3..0000000 --- a/tests/validation/test_transaction_structure.py +++ /dev/null @@ -1,261 +0,0 @@ -# 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 - -"""All tests of transaction structure. The concern here is that transaction -structural / schematic issues are caught when reading a transaction -(ie going from dict -> transaction). -""" -import json -import pytest -import hashlib as sha3 - -from unittest.mock import MagicMock -from transactions.common.exceptions import AmountError, SchemaValidationError, ThresholdTooDeep -from transactions.common.transaction import Transaction -from transactions.common.utils import _fulfillment_to_details, _fulfillment_from_details -from ipld import marshal, multihash - -################################################################################ -# Helper functions - - -def validate(tx): - if isinstance(tx, Transaction): - tx = tx.to_dict() - Transaction.from_dict(tx, False) - - -def validate_raises(tx, exc=SchemaValidationError): - with pytest.raises(exc): - validate(tx) - - -# We should test that validation works when we expect it to -def test_validation_passes(signed_create_tx): - Transaction.from_dict(signed_create_tx.to_dict(), False) - - -################################################################################ -# ID - - -def test_tx_serialization_hash_function(signed_create_tx): - tx = signed_create_tx.to_dict() - tx["id"] = None - payload = json.dumps(tx, skipkeys=False, sort_keys=True, separators=(",", ":")) - assert sha3.sha3_256(payload.encode()).hexdigest() == signed_create_tx.id - - -def test_tx_serialization_with_incorrect_hash(signed_create_tx): - from transactions.common.exceptions import InvalidHash - - tx = signed_create_tx.to_dict() - tx["id"] = "a" * 64 - with pytest.raises(InvalidHash): - Transaction.validate_id(tx) - - -def test_tx_serialization_with_no_hash(signed_create_tx): - from transactions.common.exceptions import InvalidHash - - tx = signed_create_tx.to_dict() - del tx["id"] - with pytest.raises(InvalidHash): - Transaction.from_dict(tx, False) - - -################################################################################ -# Operation - - -def test_validate_invalid_operation(b, create_tx, alice): - create_tx.operation = "something invalid" - signed_tx = create_tx.sign([alice.private_key]) - validate_raises(signed_tx) - - -################################################################################ -# Metadata - - -def test_validate_fails_metadata_empty_dict(b, create_tx, alice): - create_tx.metadata = multihash(marshal({"a": 1})) - signed_tx = create_tx.sign([alice.private_key]) - validate(signed_tx) - - create_tx._id = None - create_tx.fulfillment = None - create_tx.metadata = None - signed_tx = create_tx.sign([alice.private_key]) - validate(signed_tx) - - create_tx._id = None - create_tx.fulfillment = None - create_tx.metadata = {} - signed_tx = create_tx.sign([alice.private_key]) - validate_raises(signed_tx) - - -################################################################################ -# Asset - - -def test_transfer_asset_schema(user_sk, signed_transfer_tx): - from transactions.common.transaction import Transaction - - tx = signed_transfer_tx.to_dict() - validate(tx) - tx["id"] = None - tx["assets"][0]["data"] = {} - tx = Transaction.from_dict(tx).sign([user_sk]).to_dict() - validate_raises(tx) - tx["id"] = None - del tx["assets"][0]["data"] - tx["assets"][0]["id"] = "b" * 63 - tx = Transaction.from_dict(tx).sign([user_sk]).to_dict() - validate_raises(tx) - - -def test_create_tx_no_asset_id(b, create_tx, alice): - create_tx.assets[0]["id"] = "b" * 64 - signed_tx = create_tx.sign([alice.private_key]) - validate_raises(signed_tx) - - -def test_create_tx_asset_type(b, create_tx, alice): - create_tx.assets[0]["data"] = multihash(marshal({"a": ""})) - signed_tx = create_tx.sign([alice.private_key]) - validate(signed_tx) - # validate_raises(signed_tx) - - -def test_create_tx_no_asset_data(b, create_tx, alice): - tx_body = create_tx.to_dict() - del tx_body["assets"][0]["data"] - tx_serialized = json.dumps(tx_body, skipkeys=False, sort_keys=True, separators=(",", ":")) - tx_body["id"] = sha3.sha3_256(tx_serialized.encode()).hexdigest() - validate_raises(tx_body) - - -################################################################################ -# Inputs - - -def test_no_inputs(b, create_tx, alice): - create_tx.inputs = [] - signed_tx = create_tx.sign([alice.private_key]) - validate_raises(signed_tx) - - -def test_create_single_input(b, create_tx, alice): - from transactions.common.transaction import Transaction - - tx = create_tx.to_dict() - tx["inputs"] += tx["inputs"] - tx = Transaction.from_dict(tx).sign([alice.private_key]).to_dict() - validate_raises(tx) - tx["id"] = None - tx["inputs"] = [] - tx = Transaction.from_dict(tx).sign([alice.private_key]).to_dict() - validate_raises(tx) - - -def test_create_tx_no_fulfills(b, create_tx, alice): - from transactions.common.transaction import Transaction - - tx = create_tx.to_dict() - tx["inputs"][0]["fulfills"] = {"transaction_id": "a" * 64, "output_index": 0} - tx = Transaction.from_dict(tx).sign([alice.private_key]).to_dict() - validate_raises(tx) - - -def test_transfer_has_inputs(user_sk, signed_transfer_tx, alice): - signed_transfer_tx.inputs = [] - signed_transfer_tx._id = None - signed_transfer_tx.sign([user_sk]) - validate_raises(signed_transfer_tx) - - -################################################################################ -# Outputs - - -def test_low_amounts(b, user_sk, create_tx, signed_transfer_tx, alice): - for sk, tx in [(alice.private_key, create_tx), (user_sk, signed_transfer_tx)]: - tx.outputs[0].amount = 0 - tx._id = None - tx.sign([sk]) - validate_raises(tx, AmountError) - tx.outputs[0].amount = -1 - tx._id = None - tx.sign([sk]) - validate_raises(tx) - - -def test_high_amounts(b, create_tx, alice): - # Should raise a SchemaValidationError - don't want to allow ridiculously - # large numbers to get converted to int - create_tx.outputs[0].amount = 10**21 - create_tx.sign([alice.private_key]) - validate_raises(create_tx) - # Should raise AmountError - create_tx.outputs[0].amount = 9 * 10**18 + 1 - create_tx._id = None - create_tx.sign([alice.private_key]) - validate_raises(create_tx, AmountError) - # Should pass - create_tx.outputs[0].amount -= 1 - create_tx._id = None - create_tx.sign([alice.private_key]) - validate(create_tx) - - -################################################################################ -# Conditions - - -def test_handle_threshold_overflow(): - cond = { - "type": "ed25519-sha-256", - "public_key": "a" * 43, - } - for i in range(1000): - cond = { - "type": "threshold-sha-256", - "threshold": 1, - "subconditions": [cond], - } - with pytest.raises(ThresholdTooDeep): - _fulfillment_from_details(cond) - - -def test_unsupported_condition_type(): - from planetmint_cryptoconditions.exceptions import UnsupportedTypeError - - with pytest.raises(UnsupportedTypeError): - _fulfillment_from_details({"type": "a"}) - - with pytest.raises(UnsupportedTypeError): - _fulfillment_to_details(MagicMock(type_name="a")) - - -################################################################################ -# Version - - -def test_validate_version(b, create_tx, alice): - create_tx.version = "3.0" - create_tx.sign([alice.private_key]) - validate(create_tx) - - create_tx.version = "0.10" - create_tx._id = None - create_tx.sign([alice.private_key]) - validate_raises(create_tx) - - create_tx.version = "110" - create_tx._id = None - create_tx.sign([alice.private_key]) - validate_raises(create_tx) diff --git a/tests/validation/test_validation.py b/tests/validation/test_validation.py new file mode 100644 index 0000000..e6bd9df --- /dev/null +++ b/tests/validation/test_validation.py @@ -0,0 +1,10 @@ +# 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 + +import pytest + + +def test_something(): + return True diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index f988cef..04e9101 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -15,12 +15,21 @@ from hashlib import sha3_256 from transactions.common import crypto from transactions.common.transaction import Transaction from transactions.types.assets.create import Create +from transactions.types.assets.compose import Compose +from transactions.types.assets.decompose import Decompose from transactions.types.assets.transfer import Transfer from transactions.common.transaction_mode_types import ( BROADCAST_TX_COMMIT, BROADCAST_TX_ASYNC, BROADCAST_TX_SYNC, ) +from transactions.common.transaction import ( + Input, + TransactionLink, +) +from transactions.common.utils import _fulfillment_from_details +from transactions.common.crypto import generate_key_pair + TX_ENDPOINT = "/api/v1/transactions/" @@ -448,8 +457,6 @@ def test_transactions_get_list_bad(client): ], ) def test_post_transaction_valid_modes(mock_post, client, mode): - from transactions.common.crypto import generate_key_pair - def _mock_post(*args, **kwargs): return Mock(json=Mock(return_value={"result": {"code": 0}})) @@ -465,11 +472,90 @@ def test_post_transaction_valid_modes(mock_post, client, mode): @pytest.mark.abci def test_post_transaction_invalid_mode(client): - from transactions.common.crypto import generate_key_pair - alice = generate_key_pair() tx = Create.generate([alice.public_key], [([alice.public_key], 1)], assets=None).sign([alice.private_key]) mode_endpoint = TX_ENDPOINT + "?mode=nope" response = client.post(mode_endpoint, data=json.dumps(tx.to_dict())) assert "400 BAD REQUEST" in response.status assert 'Mode must be "async", "sync" or "commit"' == json.loads(response.data.decode("utf8"))["message"]["mode"] + + +def test_post_transaction_compose_valid_wo_abci(b, _bdb): + alice = generate_key_pair() + tx = Create.generate( + [alice.public_key], + [([alice.public_key], 1)], + assets=[{"data": "QmW5GVMW98D3mktSDfWHS8nX2UiCd8gP1uCiujnFX4yK97"}], + ).sign([alice.private_key]) + validated = b.validate_transaction(tx) + b.store_bulk_transactions([validated]) + + tx_obj = tx + tx = tx.to_dict() + compose_asset_cid = "bafkreignwcoye67vn6edp23mj4llhpzzkgyuefu7xesjzjxcv2bz3p4nfm" + inputs_ = tx_obj.to_inputs() + + assets_ = [tx["id"], compose_asset_cid] + compose_transaction = Compose.generate(inputs=inputs_, recipients=[([alice.public_key], 1)], assets=assets_) + signed_compose_tx = compose_transaction.sign([alice.private_key]) + compose_dict = signed_compose_tx.to_dict() + compose_obj = Transaction.from_dict(compose_dict) + validated_compose = b.validate_transaction(compose_obj) + b.store_bulk_transactions([validated_compose]) + + +@pytest.mark.abci +def test_post_transaction_compose_valid(client, b): + mode = ("?mode=commit", BROADCAST_TX_COMMIT) + alice = generate_key_pair() + tx = Create.generate( + [alice.public_key], + [([alice.public_key], 1)], + assets=[{"data": "QmW5GVMW98D3mktSDfWHS8nX2UiCd8gP1uCiujnFX4yK97"}], + ).sign([alice.private_key]) + mode_endpoint = TX_ENDPOINT + mode[0] + response = client.post(mode_endpoint, data=json.dumps(tx.to_dict())) + assert "202 ACCEPTED" in response.status + tx_obj = tx + tx = tx.to_dict() + compose_asset_cid = "bafkreignwcoye67vn6edp23mj4llhpzzkgyuefu7xesjzjxcv2bz3p4nfm" + inputs_ = tx_obj.to_inputs() + + assets_ = [tx["id"], compose_asset_cid] + compose_transaction = Compose.generate(inputs=inputs_, recipients=[([alice.public_key], 1)], assets=assets_) + signed_tx = compose_transaction.sign([alice.private_key]) + validated_compose = b.validate_transaction(signed_tx) + mode_endpoint = TX_ENDPOINT + "?mode=commit" + response = client.post(mode_endpoint, data=json.dumps(signed_tx.to_dict())) + assert "202 ACCEPTED" in response.status + + +@pytest.mark.abci +def test_post_transaction_decompose_valid(client, b): + mode = ("?mode=commit", BROADCAST_TX_COMMIT) + alice = generate_key_pair() + tx = Create.generate( + [alice.public_key], + [([alice.public_key], 1)], + assets=[{"data": "QmW5GVMW98D3mktSDfWHS8nX2UiCd8gP1uCiujnFX4yK97"}], + ).sign([alice.private_key]) + mode_endpoint = TX_ENDPOINT + mode[0] + response = client.post(mode_endpoint, data=json.dumps(tx.to_dict())) + assert "202 ACCEPTED" in response.status + tx_obj = tx + tx = tx.to_dict() + inputs_ = tx_obj.to_inputs() + + assets = [ + tx["id"], + "bafkreiawyk3ou5qzqec4ggbvrs56dv5ske2viwprf6he5wj5gr4yv5orsu", + "bafkreibncbonglm6mi3znbrqbchk56wmgftk4gfevxqlgeif3g5jdotcka", + "bafkreibkokzihpnnyqf3xslcievqkadf2ozkdi72wyibijih447vq42kjm", + ] + recipients = [([alice.public_key], 1), ([alice.public_key], 2), ([alice.public_key], 3)] + decompose_transaction = Decompose.generate(inputs=inputs_, recipients=recipients, assets=assets) + signed_tx = decompose_transaction.sign([alice.private_key]) + validated_decompose = b.validate_transaction(signed_tx) + mode_endpoint = TX_ENDPOINT + "?mode=commit" + response = client.post(mode_endpoint, data=json.dumps(signed_tx.to_dict())) + assert "202 ACCEPTED" in response.status