diff --git a/hathor/builder/builder.py b/hathor/builder/builder.py index d2b52d353..860feafee 100644 --- a/hathor/builder/builder.py +++ b/hathor/builder/builder.py @@ -45,6 +45,7 @@ from hathor.reactor import ReactorProtocol as Reactor from hathor.storage import RocksDBStorage from hathor.stratum import StratumFactory +from hathor.transaction.json_serializer import VertexJsonSerializer from hathor.transaction.storage import TransactionCacheStorage, TransactionRocksDBStorage, TransactionStorage from hathor.transaction.vertex_children import RocksDBVertexChildrenService from hathor.transaction.vertex_parser import VertexParser @@ -196,6 +197,8 @@ def __init__(self) -> None: self._runner_factory: RunnerFactory | None = None self._nc_log_config: NCLogConfig = NCLogConfig.NONE + self._vertex_json_serializer: VertexJsonSerializer | None = None + def build(self) -> BuildArtifacts: if self.artifacts is not None: raise ValueError('cannot call build twice') @@ -228,6 +231,7 @@ def build(self) -> BuildArtifacts: vertex_parser = self._get_or_create_vertex_parser() poa_block_producer = self._get_or_create_poa_block_producer() runner_factory = self._get_or_create_runner_factory() + vertex_json_serializer = self._get_or_create_vertex_json_serializer() if settings.ENABLE_NANO_CONTRACTS: tx_storage.nc_catalog = self._get_nc_catalog() @@ -273,6 +277,7 @@ def build(self) -> BuildArtifacts: poa_block_producer=poa_block_producer, runner_factory=runner_factory, feature_service=feature_service, + vertex_json_serializer=vertex_json_serializer, **kwargs ) @@ -687,6 +692,17 @@ def _get_or_create_poa_block_producer(self) -> PoaBlockProducer | None: return self._poa_block_producer + def _get_or_create_vertex_json_serializer(self) -> VertexJsonSerializer: + if self._vertex_json_serializer is None: + tx_storage = self._get_or_create_tx_storage() + nc_log_storage = self._get_or_create_nc_log_storage() + self._vertex_json_serializer = VertexJsonSerializer( + storage=tx_storage, + nc_log_storage=nc_log_storage, + ) + + return self._vertex_json_serializer + def set_rocksdb_path(self, path: str | tempfile.TemporaryDirectory) -> 'Builder': if self._tx_storage: raise ValueError('cannot set rocksdb path after tx storage is set') diff --git a/hathor/consensus/block_consensus.py b/hathor/consensus/block_consensus.py index f0fb5efc6..9157cade9 100644 --- a/hathor/consensus/block_consensus.py +++ b/hathor/consensus/block_consensus.py @@ -286,7 +286,13 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None: # We only emit events when the nc is successfully executed. assert self.context.nc_events is not None last_call_info = runner.get_last_call_info() - self.context.nc_events.append((tx, last_call_info.nc_logger.__events__)) + events_list = last_call_info.nc_logger.__events__ + self.context.nc_events.append((tx, events_list)) + + # Store events in transaction metadata + if events_list: + tx_meta.nc_events = [(event.nc_id, event.data) for event in events_list] + self.context.save(tx) finally: # We save logs regardless of whether the nc successfully executed. self._nc_log_storage.save_logs(tx, runner.get_last_call_info(), exception_and_tb) diff --git a/hathor/manager.py b/hathor/manager.py index d48deb611..30682f918 100644 --- a/hathor/manager.py +++ b/hathor/manager.py @@ -57,6 +57,7 @@ from hathor.reward_lock import is_spent_reward_locked from hathor.stratum import StratumFactory from hathor.transaction import BaseTransaction, Block, MergeMinedBlock, Transaction, TxVersion +from hathor.transaction.json_serializer import VertexJsonSerializer from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.transaction.storage.transaction_storage import TransactionStorage from hathor.transaction.storage.tx_allow_scope import TxAllowScope @@ -114,6 +115,7 @@ def __init__( vertex_parser: VertexParser, runner_factory: RunnerFactory, feature_service: FeatureService, + vertex_json_serializer: VertexJsonSerializer, hostname: Optional[str] = None, wallet: Optional[BaseWallet] = None, capabilities: Optional[list[str]] = None, @@ -203,6 +205,7 @@ def __init__( self.vertex_parser = vertex_parser self.runner_factory = runner_factory self.feature_service = feature_service + self.vertex_json_serializer = vertex_json_serializer self.websocket_factory = websocket_factory diff --git a/hathor/nanocontracts/resources/history.py b/hathor/nanocontracts/resources/history.py index 8cf5be76d..19be828c4 100644 --- a/hathor/nanocontracts/resources/history.py +++ b/hathor/nanocontracts/resources/history.py @@ -101,9 +101,15 @@ def render_GET(self, request: 'Request') -> bytes: count = params.count has_more = False - history_list = [] + history_list: list[dict[str, Any]] = [] for idx, tx_id in enumerate(iter_history): - history_list.append(tx_storage.get_transaction(tx_id).to_json_extended()) + tx = tx_storage.get_transaction(tx_id) + tx_json = self.manager.vertex_json_serializer.to_json_extended( + tx, + include_nc_logs=params.include_nc_logs, + include_nc_events=params.include_nc_events, + ) + history_list.append(tx_json) if idx >= count - 1: # Check if iterator still has more elements try: @@ -129,6 +135,8 @@ class NCHistoryParams(QueryParams): after: Optional[str] before: Optional[str] count: int = Field(default=100, lt=500) + include_nc_logs: bool = Field(default=False) + include_nc_events: bool = Field(default=False) class NCHistoryResponse(Response): @@ -231,6 +239,23 @@ class NCHistoryResponse(Response): 'schema': { 'type': 'string', } + }, { + 'name': 'include_nc_logs', + 'in': 'query', + 'description': 'Include nano contract execution logs in the response. Default is false.', + 'required': False, + 'schema': { + 'type': 'boolean', + } + }, { + 'name': 'include_nc_events', + 'in': 'query', + 'description': 'Include nano contract events emitted during execution in the response. ' + 'Default is false.', + 'required': False, + 'schema': { + 'type': 'boolean', + } } ], 'responses': { diff --git a/hathor/transaction/json_serializer.py b/hathor/transaction/json_serializer.py new file mode 100644 index 000000000..f6046c53d --- /dev/null +++ b/hathor/transaction/json_serializer.py @@ -0,0 +1,152 @@ +# Copyright 2021 Hathor Labs +# +# Licensed 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. + +from typing import TYPE_CHECKING, Any, Optional + +from hathor.transaction import BaseTransaction, Transaction + +if TYPE_CHECKING: + from hathor.nanocontracts.nc_exec_logs import NCLogStorage + from hathor.transaction.storage import TransactionStorage + + +class VertexJsonSerializer: + """Helper class for vertex/transaction serialization.""" + + def __init__( + self, + storage: 'TransactionStorage', + nc_log_storage: Optional['NCLogStorage'] = None, + ) -> None: + self.tx_storage = storage + self.nc_log_storage = nc_log_storage + + def to_json( + self, + tx: BaseTransaction, + decode_script: bool = False, + include_metadata: bool = False, + include_nc_logs: bool = False, + include_nc_events: bool = False, + ) -> dict[str, Any]: + """Serialize transaction to JSON.""" + # Get base JSON from transaction + data = tx.to_json(decode_script=decode_script, include_metadata=include_metadata) + + # Add nano contract logs if requested + if include_nc_logs: + self._add_nc_logs_to_dict(tx, data) + + # Add nano contract events if requested + if include_nc_events: + self._add_nc_events_to_dict(tx, data) + + return data + + def to_json_extended( + self, + tx: BaseTransaction, + include_nc_logs: bool = False, + include_nc_events: bool = False, + ) -> dict[str, Any]: + """Serialize transaction to extended JSON format.""" + data = tx.to_json_extended() + + # Add nano contract logs if requested + if include_nc_logs: + self._add_nc_logs_to_dict(tx, data) + + # Add nano contract events if requested + if include_nc_events: + self._add_nc_events_to_dict(tx, data) + + # Add decoded arguments if applicable + self._add_nc_args_decoded(tx, data) + + return data + + def _add_nc_logs_to_dict(self, tx: BaseTransaction, data: dict[str, Any]) -> None: + """Add nano contract execution logs to the data dictionary.""" + if not tx.is_nano_contract(): + return + + nc_logs: dict[str, Any] | None + if self.nc_log_storage is None: + nc_logs = {} + else: + nc_logs = self.nc_log_storage.get_json_logs(tx.hash) + + data['nc_logs'] = nc_logs + + def _add_nc_events_to_dict(self, tx: BaseTransaction, data: dict[str, Any]) -> None: + """Add nano contract events to the data dictionary.""" + if not tx.is_nano_contract(): + return + + meta = tx.get_metadata() + if meta.nc_events is None: + nc_events = [] + else: + nc_events = [ + {'nc_id': nc_id.hex(), 'data': event_data.hex()} + for nc_id, event_data in meta.nc_events + ] + + data['nc_events'] = nc_events + + def _add_nc_args_decoded(self, tx: BaseTransaction, data: dict[str, Any]) -> None: + if not tx.is_nano_contract(): + return + + assert isinstance(tx, Transaction) + nc_args_decoded = self.decode_nc_args(tx) + if nc_args_decoded is not None: + data['nc_args_decoded'] = nc_args_decoded + + def decode_nc_args(self, tx: 'Transaction') -> Any: + """Decode nano contract arguments. + + Returns a list of JSON-serialized argument strings, or None if decoding is not applicable. + """ + from hathor.nanocontracts.exception import NCFail + from hathor.nanocontracts.method import Method + from hathor.nanocontracts.types import BlueprintId + + meta = tx.get_metadata() + nano_header = tx.get_nano_header() + + if meta.nc_calls and len(meta.nc_calls) > 0: + # Get blueprint_id from the first nc_calls record + blueprint_id = BlueprintId(meta.nc_calls[0].blueprint_id) + else: + # Get blueprint_id from NanoHeader + blueprint_id = nano_header.get_blueprint_id(accept_failed_execution=True) + + try: + blueprint_class = self.tx_storage.get_blueprint_class(blueprint_id) + except NCFail: + return None + + method_callable = getattr(blueprint_class, nano_header.nc_method, None) + if method_callable is None: + return None + + method = Method.from_callable(method_callable) + + try: + args_tuple = method.deserialize_args_bytes(nano_header.nc_args_bytes) + except NCFail: + return None + + return method.args._value_to_json(args_tuple) diff --git a/hathor/transaction/resources/transaction.py b/hathor/transaction/resources/transaction.py index f9a700533..6f22a4437 100644 --- a/hathor/transaction/resources/transaction.py +++ b/hathor/transaction/resources/transaction.py @@ -139,13 +139,15 @@ def get_tx_extra_data( serialized['tokens'] = detailed_tokens - return { + result = { 'success': True, 'tx': serialized, 'meta': meta.to_json_extended(tx.storage), 'spent_outputs': spent_outputs, } + return result + @register_resource class TransactionResource(Resource): @@ -202,8 +204,22 @@ def get_one_tx(self, request: Request) -> bytes: hash_bytes = bytes.fromhex(requested_hash) tx = self.manager.tx_storage.get_transaction(hash_bytes) tx.storage = self.manager.tx_storage + data = get_tx_extra_data(tx) + # Check for optional log/event parameters and add them if requested + include_nc_logs = raw_args.get(b'include_nc_logs', [b'false'])[0].decode('utf-8').lower() == 'true' + include_nc_events = raw_args.get(b'include_nc_events', [b'false'])[0].decode('utf-8').lower() == 'true' + + if include_nc_logs or include_nc_events: + if include_nc_logs: + self.manager.vertex_json_serializer._add_nc_logs_to_dict(tx, data) + if include_nc_events: + self.manager.vertex_json_serializer._add_nc_events_to_dict(tx, data) + + # Add decoded nano contract arguments if applicable + self.manager.vertex_json_serializer._add_nc_args_decoded(tx, data) + return json_dumpb(data) def _validate_index(self, request: Request) -> bytes | None: @@ -374,6 +390,26 @@ def get_list_tx(self, request): 'schema': { 'type': 'string' } + }, + { + 'name': 'include_nc_logs', + 'in': 'query', + 'description': 'Include nano contract execution logs for nano contract transactions. ' + 'Default is false.', + 'required': False, + 'schema': { + 'type': 'boolean' + } + }, + { + 'name': 'include_nc_events', + 'in': 'query', + 'description': 'Include nano contract events emitted during execution for nano contract ' + 'transactions. Default is false.', + 'required': False, + 'schema': { + 'type': 'boolean' + } } ], 'responses': { diff --git a/hathor/transaction/transaction_metadata.py b/hathor/transaction/transaction_metadata.py index 7c9ce789f..f6b69934a 100644 --- a/hathor/transaction/transaction_metadata.py +++ b/hathor/transaction/transaction_metadata.py @@ -54,6 +54,8 @@ class TransactionMetadata: nc_block_root_id: Optional[bytes] nc_execution: Optional[NCExecutionState] nc_calls: Optional[list[MetaNCCallRecord]] + # Stores events emitted during nano contract execution + nc_events: Optional[list[tuple[bytes, bytes]]] # [(nc_id, event_data)] # A dict of features in the feature activation process and their respective state. Must only be used by Blocks, # is None otherwise. This is only used for caching, so it can be safely cleared up, as it would be recalculated @@ -85,6 +87,7 @@ def __init__( self.nc_block_root_id = nc_block_root_id self.nc_execution = None self.nc_calls = None + self.nc_events = None # Tx outputs that have been spent. # The key is the output index, while the value is a set of the transactions which spend the output. @@ -187,7 +190,7 @@ def __eq__(self, other: Any) -> bool: return False for field in ['hash', 'conflict_with', 'voided_by', 'received_by', 'accumulated_weight', 'twins', 'score', 'first_block', 'validation', - 'feature_states', 'nc_block_root_id', 'nc_calls', 'nc_execution']: + 'feature_states', 'nc_block_root_id', 'nc_calls', 'nc_execution', 'nc_events']: if (getattr(self, field) or None) != (getattr(other, field) or None): return False @@ -244,6 +247,11 @@ def to_storage_json(self) -> dict[str, Any]: data['nc_block_root_id'] = self.nc_block_root_id.hex() if self.nc_block_root_id else None data['nc_calls'] = [x.to_json() for x in self.nc_calls] if self.nc_calls else None data['nc_execution'] = self.nc_execution.value if self.nc_execution else None + # Serialize nc_events: [(nc_id, event_data)] + if self.nc_events: + data['nc_events'] = [(nc_id.hex(), event_data.hex()) for nc_id, event_data in self.nc_events] + else: + data['nc_events'] = None return data def to_json(self) -> dict[str, Any]: @@ -315,7 +323,7 @@ def create_from_json(cls, data: dict[str, Any]) -> 'TransactionMetadata': else: meta.nc_block_root_id = None - nc_execution_raw = data.get('nc_execution_raw') + nc_execution_raw = data.get('nc_execution') if nc_execution_raw is not None: meta.nc_execution = NCExecutionState(nc_execution_raw) else: @@ -327,6 +335,13 @@ def create_from_json(cls, data: dict[str, Any]) -> 'TransactionMetadata': else: meta.nc_calls = None + nc_events_raw = data.get('nc_events') + if nc_events_raw is not None: + meta.nc_events = [(bytes.fromhex(nc_id), bytes.fromhex(event_data)) + for nc_id, event_data in nc_events_raw] + else: + meta.nc_events = None + return meta @classmethod diff --git a/hathor_cli/builder.py b/hathor_cli/builder.py index 312978608..529447b16 100644 --- a/hathor_cli/builder.py +++ b/hathor_cli/builder.py @@ -48,6 +48,7 @@ from hathor.verification.verification_service import VerificationService from hathor.verification.vertex_verifiers import VertexVerifiers from hathor.vertex_handler import VertexHandler +from hathor.transaction.json_serializer import VertexJsonSerializer from hathor.wallet import BaseWallet, HDWallet, Wallet logger = get_logger() @@ -329,6 +330,8 @@ def create_manager(self, reactor: Reactor) -> HathorManager: SyncSupportLevel.add_factories(settings, p2p_manager, SyncSupportLevel.ENABLED, vertex_parser, vertex_handler) + vertex_json_serializer = VertexJsonSerializer(storage=tx_storage, nc_log_storage=nc_log_storage) + from hathor.consensus.poa import PoaBlockProducer, PoaSignerFile poa_block_producer: PoaBlockProducer | None = None if settings.CONSENSUS_ALGORITHM.is_poa(): @@ -365,6 +368,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager: poa_block_producer=poa_block_producer, runner_factory=runner_factory, feature_service=self.feature_service, + vertex_json_serializer=vertex_json_serializer, ) if self._args.x_ipython_kernel: diff --git a/hathor_tests/resources/nanocontracts/test_history2.py b/hathor_tests/resources/nanocontracts/test_history2.py new file mode 100644 index 000000000..d5687323a --- /dev/null +++ b/hathor_tests/resources/nanocontracts/test_history2.py @@ -0,0 +1,149 @@ +from twisted.internet.defer import inlineCallbacks + +from hathor.conf import HathorSettings +from hathor.nanocontracts import Blueprint, Context, public +from hathor.nanocontracts.catalog import NCBlueprintCatalog +from hathor.nanocontracts.nc_exec_logs import NCLogConfig +from hathor.nanocontracts.resources import NanoContractHistoryResource +from hathor.transaction import Transaction +from hathor.transaction.resources import TransactionResource +from hathor_tests.dag_builder.builder import TestDAGBuilder +from hathor_tests.resources.base_resource import StubSite, _BaseResourceTest + +settings = HathorSettings() + + +class TestBlueprint(Blueprint): + value: int + + @public + def initialize(self, ctx: Context, value: int) -> None: + self.value = value + + @public + def log_and_emit(self, ctx: Context, message: str) -> None: + self.log.info(f'Log: {message}') + self.syscall.emit_event(message.encode('utf-8')) + + +class TransactionNanoContractTest(_BaseResourceTest._ResourceTest): + def setUp(self): + super().setUp() + + self.blueprint_id = b'x' * 32 + self.catalog = NCBlueprintCatalog({ + self.blueprint_id: TestBlueprint + }) + + self.manager = self.create_peer( + 'unittests', + unlock_wallet=True, + wallet_index=True, + nc_indexes=True, + nc_log_config=NCLogConfig.ALL, + ) + self.manager.tx_storage.nc_catalog = self.catalog + self.web_transaction = StubSite(TransactionResource(self.manager)) + self.web_history = StubSite(NanoContractHistoryResource(self.manager)) + + @inlineCallbacks + def test_include_nc_logs_and_events(self): + """Test include_nc_logs and include_nc_events parameters for both TransactionResource + and NanoContractHistoryResource.""" + dag_builder = TestDAGBuilder.from_manager(self.manager) + + # nc1: initialize (no logs, no events) + # nc2: log_and_emit (logs and events) + artifacts = dag_builder.build_from_str(f''' + blockchain genesis b[1..33] + b30 < dummy + + nc1.nc_id = "{self.blueprint_id.hex()}" + nc1.nc_method = initialize(42) + + nc2.nc_id = nc1 + nc2.nc_method = log_and_emit("combined test") + + b31 --> nc1 + b32 --> nc2 + ''') + + artifacts.propagate_with(self.manager) + + nc1, nc2 = artifacts.get_typed_vertices(['nc1', 'nc2'], Transaction) + + # Test TransactionResource API + # Test nc1 (initialize - no logs, no events) + response = yield self.web_transaction.get('transaction', { + b'id': nc1.hash.hex().encode('ascii'), + b'include_nc_logs': b'true', + b'include_nc_events': b'true', + }) + data = response.json_value() + self.assertTrue(data['success']) + self.assertEqual(data['nc_args_decoded'], [42]) + self.assertIn('nc_logs', data) + self.assertIn('nc_events', data) + # Should have empty events list for initialize method + self.assertEqual(data['nc_events'], []) + + # Test nc2 (log_and_emit - has logs and events) + response = yield self.web_transaction.get('transaction', { + b'id': nc2.hash.hex().encode('ascii'), + b'include_nc_logs': b'true', + b'include_nc_events': b'true', + }) + data = response.json_value() + self.assertTrue(data['success']) + self.assertEqual(data['nc_args_decoded'], ["combined test"]) + # Should have both logs and events + self.assertIn('nc_logs', data) + self.assertIsInstance(data['nc_logs'], dict) + self.assertGreater(len(data['nc_logs']), 0) + self.assertIn('nc_events', data) + self.assertIsInstance(data['nc_events'], list) + self.assertEqual(len(data['nc_events']), 1) + event = data['nc_events'][0] + self.assertEqual(bytes.fromhex(event['data']), b'combined test') + + # Test NanoContractHistoryResource API + # Test history for nc1 + response = yield self.web_history.get('history', { + b'id': nc1.hash.hex().encode('ascii'), + b'include_nc_logs': b'true', + b'include_nc_events': b'true', + }) + data = response.json_value() + self.assertTrue(data['success']) + self.assertGreater(len(data['history']), 0) + + # Find nc1 in history (it should be the initialize transaction) + nc1_in_history = None + for tx_data in data['history']: + if tx_data['hash'] == nc1.hash_hex: + nc1_in_history = tx_data + break + + self.assertIsNotNone(nc1_in_history) + self.assertEqual(nc1_in_history['nc_args_decoded'], [42]) + self.assertIn('nc_logs', nc1_in_history) + self.assertIn('nc_events', nc1_in_history) + self.assertEqual(nc1_in_history['nc_events'], []) + + # Find nc2 in history (log_and_emit transaction) + nc2_in_history = None + for tx_data in data['history']: + if tx_data['hash'] == nc2.hash_hex: + nc2_in_history = tx_data + break + + self.assertIsNotNone(nc2_in_history) + self.assertEqual(nc2_in_history['nc_args_decoded'], ["combined test"]) + self.assertIn('nc_logs', nc2_in_history) + self.assertIsInstance(nc2_in_history['nc_logs'], dict) + self.assertGreater(len(nc2_in_history['nc_logs']), 0) + self.assertIn('nc_events', nc2_in_history) + self.assertIsInstance(nc2_in_history['nc_events'], list) + self.assertEqual(len(nc2_in_history['nc_events']), 1) + event = nc2_in_history['nc_events'][0] + self.assertEqual(bytes.fromhex(event['data']), b'combined test') diff --git a/hathor_tests/resources/transaction/test_mining.py b/hathor_tests/resources/transaction/test_mining.py index 3be2747a5..5f8fc2f56 100644 --- a/hathor_tests/resources/transaction/test_mining.py +++ b/hathor_tests/resources/transaction/test_mining.py @@ -44,6 +44,7 @@ def test_get_block_template_with_address(self): 'nc_block_root_id': None, 'nc_execution': None, 'nc_calls': None, + 'nc_events': None, }, 'tokens': [], 'data': '', @@ -83,6 +84,7 @@ def test_get_block_template_without_address(self): 'nc_block_root_id': None, 'nc_execution': None, 'nc_calls': None, + 'nc_events': None, }, 'tokens': [], 'data': '', diff --git a/hathor_tests/tx/test_metadata.py b/hathor_tests/tx/test_metadata.py new file mode 100644 index 000000000..44bc42115 --- /dev/null +++ b/hathor_tests/tx/test_metadata.py @@ -0,0 +1,114 @@ +# Copyright 2025 Hathor Labs +# +# Licensed 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. + +from unittest.mock import Mock + +from hathor import BlueprintId, ContractId, TokenUid +from hathor.feature_activation.feature import Feature +from hathor.feature_activation.model.feature_state import FeatureState +from hathor.nanocontracts.runner.index_records import ( + CreateContractRecord, + CreateTokenRecord, + IndexRecordType, + UpdateAuthoritiesRecord, + UpdateTokenBalanceRecord, +) +from hathor.transaction import TransactionMetadata +from hathor.transaction.nc_execution_state import NCExecutionState +from hathor.transaction.token_info import TokenVersion +from hathor.transaction.types import MetaNCCallRecord +from hathor.transaction.validation_state import ValidationState +from hathor.util import not_none +from hathor_tests import unittest + + +class TestMetadata(unittest.TestCase): + def test_round_trip(self) -> None: + meta = TransactionMetadata() + meta._tx_ref = Mock() + meta.hash = b'abc' + meta.spent_outputs = {0: [b'1', b'2'], 10: [b'3']} + meta.conflict_with = [b'1', b'2'] + meta.voided_by = {b'1', b'2'} + meta.received_by = [1, 2, 3] + meta.twins = [b'1', b'2'] + meta.accumulated_weight = 123 + meta.score = 456 + meta.first_block = b'123' + meta.validation = ValidationState.FULL + meta.nc_block_root_id = b'456' + meta.nc_execution = NCExecutionState.SUCCESS + meta.nc_calls = [ + MetaNCCallRecord( + blueprint_id=b'foo', + contract_id=b'bar', + method_name='aaa', + index_updates=[ + CreateContractRecord( + blueprint_id=BlueprintId(b'bbb'), + contract_id=ContractId(b'ccc'), + ), + CreateTokenRecord( + token_uid=TokenUid(b'ttt'), + amount=123, + token_symbol='s', + token_name='n', + token_version=TokenVersion.FEE, + ), + UpdateTokenBalanceRecord( + token_uid=TokenUid(b'ttt'), + amount=123, + ), + UpdateAuthoritiesRecord( + type=IndexRecordType.REVOKE_AUTHORITIES, + token_uid=TokenUid(b'ttt'), + mint=True, + melt=True, + ), + ], + ), + ] + meta.nc_events = [ + (b'a', b'b'), + (b'c', b'd'), + ] + meta.feature_states = { + Feature.NOP_FEATURE_1: FeatureState.FAILED, + Feature.NOP_FEATURE_2: FeatureState.ACTIVE, + } + + storage_json = meta.to_storage_json() + meta2 = TransactionMetadata.create_from_json(storage_json) + meta3 = TransactionMetadata.from_bytes(meta.to_bytes()) + + assert meta.hash == meta2.hash and meta.hash == meta3.hash + assert meta.spent_outputs == meta2.spent_outputs and meta.spent_outputs == meta3.spent_outputs + assert ( + set(not_none(meta.conflict_with)) == set(not_none(meta2.conflict_with)) + and set(not_none(meta.conflict_with)) == set(not_none(meta3.conflict_with)) + ) + assert meta.voided_by == meta2.voided_by and meta.voided_by == meta3.voided_by + assert meta.received_by == meta2.received_by and meta.received_by == meta3.received_by + assert meta.twins == meta2.twins and meta.twins == meta3.twins + assert ( + meta.accumulated_weight == meta2.accumulated_weight and meta.accumulated_weight == meta3.accumulated_weight + ) + assert meta.score == meta2.score and meta.score == meta3.score + assert meta.first_block == meta2.first_block and meta.first_block == meta3.first_block + assert meta.validation == meta2.validation and meta.validation == meta3.validation + assert meta.nc_block_root_id == meta2.nc_block_root_id and meta.nc_block_root_id == meta3.nc_block_root_id + assert meta.nc_execution == meta2.nc_execution and meta.nc_execution == meta3.nc_execution + assert meta.nc_calls == meta2.nc_calls and meta.nc_calls == meta3.nc_calls + assert meta.nc_events == meta2.nc_events and meta.nc_events == meta3.nc_events + assert meta.feature_states == meta2.feature_states and meta.feature_states == meta3.feature_states