diff --git a/hathor/indexes/nc_history_index.py b/hathor/indexes/nc_history_index.py index 6099ccaa1..4c4d76b26 100644 --- a/hathor/indexes/nc_history_index.py +++ b/hathor/indexes/nc_history_index.py @@ -70,6 +70,11 @@ def get_newest(self, contract_id: bytes) -> Iterable[bytes]: """ return self._get_sorted_from_key(contract_id, reverse=True) + def get_oldest(self, contract_id: bytes) -> Iterable[bytes]: + """Get a list of tx_ids sorted by timestamp for a given contract_id starting from the oldest. + """ + return self._get_sorted_from_key(contract_id, reverse=False) + def get_older(self, contract_id: bytes, tx_start: Optional[BaseTransaction] = None) -> Iterable[bytes]: """Get a list of tx_ids sorted by timestamp for a given contract_id that are older than tx_start. """ diff --git a/hathor/nanocontracts/resources/history.py b/hathor/nanocontracts/resources/history.py index 01fd02544..45d49b7c5 100644 --- a/hathor/nanocontracts/resources/history.py +++ b/hathor/nanocontracts/resources/history.py @@ -19,6 +19,7 @@ from hathor._openapi.register import register_resource from hathor.api_util import Resource, set_cors from hathor.nanocontracts.exception import NanoContractDoesNotExist +from hathor.nanocontracts.resources.on_chain import SortOrder from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.utils.api import ErrorResponse, QueryParams, Response @@ -73,30 +74,46 @@ def render_GET(self, request: 'Request') -> bytes: error_response = ErrorResponse(success=False, error='Nano contract does not exist.') return error_response.json_dumpb() - if params.after: + is_desc = params.order.is_desc() + + if not params.before and not params.after: + iter_history = ( + iter(tx_storage.indexes.nc_history.get_newest(nc_id_bytes)) if is_desc + else iter(tx_storage.indexes.nc_history.get_oldest(nc_id_bytes)) + ) + else: + ref_tx_id_hex = params.before or params.after + assert ref_tx_id_hex is not None + try: - ref_tx = tx_storage.get_transaction(bytes.fromhex(params.after)) - except TransactionDoesNotExist: + ref_tx_id = bytes.fromhex(ref_tx_id_hex) + except ValueError: request.setResponseCode(400) - error_response = ErrorResponse(success=False, error=f'Hash {params.after} is not a transaction hash.') + error_response = ErrorResponse(success=False, error=f'Invalid hash: {ref_tx_id_hex}') return error_response.json_dumpb() - iter_history = iter(tx_storage.indexes.nc_history.get_older(nc_id_bytes, ref_tx)) - # This method returns the iterator including the tx used as `after` - next(iter_history) - elif params.before: try: - ref_tx = tx_storage.get_transaction(bytes.fromhex(params.before)) + ref_tx = tx_storage.get_transaction(ref_tx_id) except TransactionDoesNotExist: - request.setResponseCode(400) - error_response = ErrorResponse(success=False, error=f'Hash {params.before} is not a transaction hash.') + request.setResponseCode(404) + error_response = ErrorResponse(success=False, error=f'Transaction {ref_tx_id_hex} not found.') return error_response.json_dumpb() - iter_history = iter(tx_storage.indexes.nc_history.get_newer(nc_id_bytes, ref_tx)) - # This method returns the iterator including the tx used as `before` - next(iter_history) - else: - iter_history = iter(tx_storage.indexes.nc_history.get_newest(nc_id_bytes)) + if is_desc: + iter_getter = tx_storage.indexes.nc_history.get_newer if params.before \ + else tx_storage.indexes.nc_history.get_older + else: + iter_getter = tx_storage.indexes.nc_history.get_older if params.before \ + else tx_storage.indexes.nc_history.get_newer + + iter_history = iter(iter_getter(nc_id_bytes, ref_tx)) + # This method returns the iterator including the tx used as `before` or `after` + try: + next(iter_history) + except StopIteration: + # This can happen if the `ref_tx` is the only tx in the history, in this case the iterator will be + # empty. It's safe to just ignore this and let the loop below handle the empty iterator. + pass count = params.count has_more = False @@ -134,6 +151,7 @@ class NCHistoryParams(QueryParams): after: Optional[str] before: Optional[str] count: int = Field(default=100, lt=500) + order: SortOrder = SortOrder.DESC include_nc_logs: bool = Field(default=False) include_nc_events: bool = Field(default=False) @@ -238,6 +256,14 @@ class NCHistoryResponse(Response): 'schema': { 'type': 'string', } + }, { + 'name': 'order', + 'in': 'query', + 'description': 'Sort order, either "asc" or "desc".', + 'required': False, + 'schema': { + 'type': 'string', + } }, { 'name': 'include_nc_logs', 'in': 'query', diff --git a/hathor_tests/resources/nanocontracts/test_history.py b/hathor_tests/resources/nanocontracts/test_history.py index b4e8f022d..072e1fbd2 100644 --- a/hathor_tests/resources/nanocontracts/test_history.py +++ b/hathor_tests/resources/nanocontracts/test_history.py @@ -185,6 +185,20 @@ def test_success(self): ids = [tx['hash'] for tx in data2['history']] self.assertEqual(ids, [tx1.hash.hex(), nc1.hash.hex()]) + # Check ascending order + response_asc = yield self.web.get( + 'history', + { + b'id': nc1.hash.hex().encode('ascii'), + b'order': b'asc', + } + ) + data_asc = response_asc.json_value() + self.assertEqual(data_asc['has_more'], False) + self.assertEqual(len(data_asc['history']), 2) + ids_asc = [tx['hash'] for tx in data_asc['history']] + self.assertEqual(ids_asc, [nc1.hash.hex(), tx1.hash.hex()]) + # Check paging works minimally with after response2a = yield self.web.get( 'history', diff --git a/hathor_tests/resources/nanocontracts/test_history2.py b/hathor_tests/resources/nanocontracts/test_history2.py index e355c8155..71f084334 100644 --- a/hathor_tests/resources/nanocontracts/test_history2.py +++ b/hathor_tests/resources/nanocontracts/test_history2.py @@ -107,37 +107,23 @@ def test_include_nc_logs_and_events(self): self.assertEqual(bytes.fromhex(event['data']), b'combined test') # Test NanoContractHistoryResource API - # Test history for nc1 - response = yield self.web_history.get('history', { + # By default, transactions are created with increasing timestamps, so nc2 is newer than nc1. + # Test history for nc1 with default order (desc) + response_desc = 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) + data_desc = response_desc.json_value() + self.assertTrue(data_desc['success']) + self.assertEqual(len(data_desc['history']), 2) - # 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 + # Check order (desc), newest first: nc2, then nc1 + self.assertEqual(data_desc['history'][0]['hash'], nc2.hash_hex) + self.assertEqual(data_desc['history'][1]['hash'], nc1.hash_hex) - 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) + # Check content of nc2 in history + nc2_in_history = data_desc['history'][0] 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) @@ -147,3 +133,23 @@ def test_include_nc_logs_and_events(self): 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') + + # Check content of nc1 in history + nc1_in_history = data_desc['history'][1] + 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'], []) + + # Test history for nc1 with asc order + response_asc = yield self.web_history.get('history', { + b'id': nc1.hash.hex().encode('ascii'), + b'order': b'asc', + }) + data_asc = response_asc.json_value() + self.assertTrue(data_asc['success']) + self.assertEqual(len(data_asc['history']), 2) + + # Check order (asc), oldest first: nc1, then nc2 + self.assertEqual(data_asc['history'][0]['hash'], nc1.hash_hex) + self.assertEqual(data_asc['history'][1]['hash'], nc2.hash_hex)