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
13 changes: 12 additions & 1 deletion hathor/transaction/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from itertools import starmap, zip_longest
from operator import add
from struct import pack
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, Iterator, Optional

from hathor.checkpoint import Checkpoint
from hathor.feature_activation.feature import Feature
Expand Down Expand Up @@ -401,3 +401,14 @@ def get_feature_activation_bit_value(self, bit: int) -> int:
bit_list = self._get_feature_activation_bit_list()

return bit_list[bit]

def iter_transactions_in_this_block(self) -> Iterator[BaseTransaction]:
"""Return an iterator of the transactions that have this block as meta.first_block."""
from hathor.transaction.storage.traversal import BFSOrderWalk
bfs = BFSOrderWalk(self.storage, is_dag_verifications=True, is_dag_funds=True, is_left_to_right=False)
for tx in bfs.run(self, skip_root=True):
tx_meta = tx.get_metadata()
if tx_meta.first_block != self.hash:
bfs.skip_neighbors(tx)
continue
yield tx
68 changes: 48 additions & 20 deletions hathor/transaction/resources/block_at_height.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from hathor.api_util import Resource, get_args, get_missing_params_msg, parse_args, parse_int, set_cors
from hathor.api_util import Resource, set_cors
from hathor.cli.openapi_files.register import register_resource
from hathor.util import json_dumpb
from hathor.utils.api import ErrorResponse, QueryParams

if TYPE_CHECKING:
from twisted.web.http import Request
Expand Down Expand Up @@ -48,38 +49,52 @@ def render_GET(self, request: 'Request') -> bytes:
request.setHeader(b'content-type', b'application/json; charset=utf-8')
set_cors(request, 'GET')

# Height parameter is required
parsed = parse_args(get_args(request), ['height'])
if not parsed['success']:
return get_missing_params_msg(parsed['missing'])
params = BlockAtHeightParams.from_request(request)
if isinstance(params, ErrorResponse):
return params.json_dumpb()

args = parsed['args']
# Get hash of the block with the height
block_hash = self.manager.tx_storage.indexes.height.get(params.height)

# Height parameter must be an integer
try:
height = parse_int(args['height'])
except ValueError as e:
# If there is no block in the index with this height, block_hash will be None
if block_hash is None:
return json_dumpb({
'success': False,
'message': f'Failed to parse \'height\': {e}'
'message': 'No block with height {}.'.format(params.height)
})

# Get hash of the block with the height
block_hash = self.manager.tx_storage.indexes.height.get(height)
block = self.manager.tx_storage.get_block(block_hash)
data = {'success': True, 'block': block.to_json_extended()}

# If there is no block in the index with this height, block_hash will be None
if block_hash is None:
if params.include_transactions is None:
pass

elif params.include_transactions == 'txid':
tx_ids: list[str] = []
for tx in block.iter_transactions_in_this_block():
tx_ids.append(tx.hash.hex())
data['tx_ids'] = tx_ids

elif params.include_transactions == 'full':
tx_list: list[Any] = []
for tx in block.iter_transactions_in_this_block():
tx_list.append(tx.to_json_extended())
data['transactions'] = tx_list

else:
return json_dumpb({
'success': False,
'message': 'No block with height {}.'.format(height)
'message': 'Invalid include_transactions. Choices are: txid or full.'
})

block = self.manager.tx_storage.get_transaction(block_hash)

data = {'success': True, 'block': block.to_json_extended()}
return json_dumpb(data)


class BlockAtHeightParams(QueryParams):
height: int
include_transactions: str | None


BlockAtHeightResource.openapi = {
'/block_at_height': {
'x-visibility': 'public',
Expand Down Expand Up @@ -114,6 +129,19 @@ def render_GET(self, request: 'Request') -> bytes:
'type': 'int'
}
},
{
'name': 'include_transactions',
'in': 'query',
'description': 'Add transactions confirmed by this block.',
'required': False,
'schema': {
'type': 'string',
'enum': [
'txid',
'full',
],
}
},
],
'responses': {
'200': {
Expand Down
59 changes: 58 additions & 1 deletion tests/resources/transaction/test_block_at_height.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from twisted.internet.defer import inlineCallbacks

from hathor.simulator.utils import add_new_blocks
from hathor.simulator.utils import add_new_block, add_new_blocks
from hathor.transaction.resources import BlockAtHeightResource
from tests import unittest
from tests.resources.base_resource import StubSite, _BaseResourceTest
from tests.utils import add_blocks_unlock_reward, add_new_tx


class BaseBlockAtHeightTest(_BaseResourceTest._ResourceTest):
Expand All @@ -14,6 +15,62 @@ def setUp(self):
self.web = StubSite(BlockAtHeightResource(self.manager))
self.manager.wallet.unlock(b'MYPASS')

@inlineCallbacks
def test_include_full(self):
add_new_block(self.manager, advance_clock=1)
add_blocks_unlock_reward(self.manager)
address = self.manager.wallet.get_unused_address()

confirmed_tx_list = []
for _ in range(15):
confirmed_tx_list.append(add_new_tx(self.manager, address, 1))

block = add_new_block(self.manager, advance_clock=1)
height = block.get_height()

# non-confirmed transactions
for _ in range(15):
add_new_tx(self.manager, address, 1)

response = yield self.web.get("block_at_height", {
b'height': str(height).encode('ascii'),
b'include_transactions': b'full',
})
data = response.json_value()

self.assertTrue(data['success'])
response_tx_ids = set(x['tx_id'] for x in data['transactions'])
expected_tx_ids = set(tx.hash.hex() for tx in confirmed_tx_list)
self.assertTrue(response_tx_ids.issubset(expected_tx_ids))

@inlineCallbacks
def test_include_txids(self):
add_new_block(self.manager, advance_clock=1)
add_blocks_unlock_reward(self.manager)
address = self.manager.wallet.get_unused_address()

confirmed_tx_list = []
for _ in range(15):
confirmed_tx_list.append(add_new_tx(self.manager, address, 1))

block = add_new_block(self.manager, advance_clock=1)
height = block.get_height()

# non-confirmed transactions
for _ in range(15):
add_new_tx(self.manager, address, 1)

response = yield self.web.get("block_at_height", {
b'height': str(height).encode('ascii'),
b'include_transactions': b'txid',
})
data = response.json_value()

self.assertTrue(data['success'])
response_tx_ids = set(data['tx_ids'])
expected_tx_ids = set(tx.hash.hex() for tx in confirmed_tx_list)
self.assertTrue(response_tx_ids.issubset(expected_tx_ids))

@inlineCallbacks
def test_get(self):
blocks = add_new_blocks(self.manager, 4, advance_clock=1)
Expand Down