Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions hathor/indexes/nc_history_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down
58 changes: 42 additions & 16 deletions hathor/nanocontracts/resources/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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',
Expand Down
14 changes: 14 additions & 0 deletions hathor_tests/resources/nanocontracts/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
56 changes: 31 additions & 25 deletions hathor_tests/resources/nanocontracts/test_history2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Loading