diff --git a/README.md b/README.md
index 1092b8d..bd29928 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
* [Nodes](#nodes)
* [Configuration file](#configuration-file)
* [Service](#service)
- * [Account](#account)
+ * [Batch](#batch)
* [Development](#development)
* [Requirements](#development-requirements)
* [Docker](#docker)
@@ -116,6 +116,70 @@ $ remme account get-balance \
}
```
+### Batch
+
+Get a list of batches — ``remme batch get-list``:
+
+| Arguments | Type | Required | Description |
+| :-------: | :----: | :-------: | --------------------------------------------------- |
+| ids | String | No | Identifiers to get a list of batches by. |
+| start | String | No | Parameter to list batches starting from. |
+| limit | Integer| No | Parameter to limit amount of batches. |
+| head | String | No | Block identifier to get a list of batches from. |
+| reverse | String | No | Parameter to reverse result. |
+| node-url | String | No | Node URL to apply the command to. |
+
+```bash
+$ remme batch get-list \
+ --ids=[c2eeb94926d3432e41cb5ceed078f78466389e4fe685ec958021a1368f634c035072e434e5d0ff64820dd01fde6e5afc67eb5a9ae6c48d3983ff43abe98aef6b] \
+ --node-url=159.89.104.9
+{
+ "result": {
+ "data": [
+ {
+ "header": {
+ "signer_public_key": "02d1fbda50dbcd0d3c286a6a9fa71aa7ce2d97159b90ddd463e0816422d621e135",
+ "transaction_ids": [
+ "0317216042416f38087fed16bd4a0e1ea90ca240d22547495f8eb0d23c9b680e0d763199e7864f2384256d16d5ea56ca31ee8087f6c579434dcd454d97140faa"
+ ]
+ },
+ "header_signature": "c2eeb94926d3432e41cb5ceed078f78466389e4fe685ec958021a1368f634c035072e434e5d0ff64820dd01fde6e5afc67eb5a9ae6c48d3983ff43abe98aef6b",
+ "trace": false,
+ "transactions": [
+ {
+ "header": {
+ "batcher_public_key": "02d1fbda50dbcd0d3c286a6a9fa71aa7ce2d97159b90ddd463e0816422d621e135",
+ "dependencies": [],
+ "family_name": "block_info",
+ "family_version": "1.0",
+ "inputs": [
+ "00b10c0100000000000000000000000000000000000000000000000000000000000000",
+ "00b10c00"
+ ],
+ "nonce": "",
+ "outputs": [
+ "00b10c0100000000000000000000000000000000000000000000000000000000000000",
+ "00b10c00"
+ ],
+ "payload_sha512": "68073292cc2595ed11eac0a9b61c9a54cbee641209ab75ce83f8a4ccb78c64fb77c78d05d54f52624664a4725f5c57cd89f665d22dfcebe9fe11acd28c48884d",
+ "signer_public_key": "02d1fbda50dbcd0d3c286a6a9fa71aa7ce2d97159b90ddd463e0816422d621e135"
+ },
+ "header_signature": "0317216042416f38087fed16bd4a0e1ea90ca240d22547495f8eb0d23c9b680e0d763199e7864f2384256d16d5ea56ca31ee8087f6c579434dcd454d97140faa",
+ "payload": "CtICCHoSgAFkZGJhZTU2ZDZkYzJiMDE2MjQyODc1NWE1MmJkZDdhYzkzODJjNGRiOTk3MTkzMWEyMTA5MmZhYjA1ZGIwZDJjNWYzMzE2M2NlODZhZTRlNzY0MGEyYmY0ZWNhNWJiMzdlZTM2NzU1N2M2YTc4ZDMwOGQ0ZGYyMWQxY2Y3Y2EyORpCMDJkMWZiZGE1MGRiY2QwZDNjMjg2YTZhOWZhNzFhYTdjZTJkOTcxNTliOTBkZGQ0NjNlMDgxNjQyMmQ2MjFlMTM1IoABMGU4ODRkYjdiYjRkN2ZkYTdhODc2YWQ4NzVmNTA0NWFjNmY1MDUwZDZlOGVkYzk2NjRhZjg0NWI2YTM0Mzk4ODc4YmQ2MDA2ODgyY2JjMTMzNTMzYzQ1OWRhZDE3ZDExODMwNGViZjNjOGFmMmE2ZGZhNWViNWIzMjc4MTViY2QoyPb85QU="
+ }
+ ]
+ }
+ ],
+ "head": "b6ac30a480b237c8796fa4903354af835c85d78e781644265bc7a202b61d750c465790865b28ce69473d02b4e056d823194fd29e3f986800f0e7316865bffd7b",
+ "paging": {
+ "limit": null,
+ "next": "",
+ "start": null
+ }
+ }
+}
+```
+
## Development
Requirements
diff --git a/cli/batch/__init__.py b/cli/batch/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/cli/batch/cli.py b/cli/batch/cli.py
new file mode 100644
index 0000000..4c2144f
--- /dev/null
+++ b/cli/batch/cli.py
@@ -0,0 +1,105 @@
+"""
+Provide implementation of the command line interface's batch commands.
+"""
+import asyncio
+import sys
+
+import click
+from remme import Remme
+
+from cli.batch.forms import ListBatchesForm
+from cli.batch.help import (
+ BATCH_HEAD_ARGUMENT_HELP_MESSAGE,
+ BATCH_IDS_ARGUMENT_HELP_MESSAGE,
+ BATCH_LIMIT_ARGUMENT_HELP_MESSAGE,
+ BATCH_REVERSE_ARGUMENT_HELP_MESSAGE,
+ BATCH_START_ARGUMENT_HELP_MESSAGE,
+)
+from cli.batch.service import Batch
+from cli.constants import (
+ FAILED_EXIT_FROM_COMMAND_CODE,
+ NODE_URL_ARGUMENT_HELP_MESSAGE,
+)
+from cli.utils import (
+ default_node_url,
+ dict_to_pretty_json,
+ print_errors,
+ print_result,
+)
+
+loop = asyncio.get_event_loop()
+
+
+@click.group('batch', chain=True)
+def batch_commands():
+ """
+ Provide commands for working with batches.
+ """
+ pass
+
+
+def _split_ids(ctx, param, value):
+ """
+ Convert string to list. Remove '[', ']' and split by ','.
+ """
+ if value is None:
+ return
+
+ if value[0] != '[' or value[-1] != ']':
+ click.echo(dict_to_pretty_json({
+ 'ids': ['Not a valid list.'],
+ }))
+ sys.exit(FAILED_EXIT_FROM_COMMAND_CODE)
+ value = value[1:-1]
+ values = [c.strip() for c in value.split(',')]
+
+ return values
+
+
+@click.option('--ids', type=str, required=False, help=BATCH_IDS_ARGUMENT_HELP_MESSAGE, callback=_split_ids)
+@click.option('--start', type=str, required=False, help=BATCH_START_ARGUMENT_HELP_MESSAGE)
+@click.option('--limit', type=int, required=False, help=BATCH_LIMIT_ARGUMENT_HELP_MESSAGE)
+@click.option('--head', type=str, required=False, help=BATCH_HEAD_ARGUMENT_HELP_MESSAGE)
+@click.option('--reverse', type=str, required=False, help=BATCH_REVERSE_ARGUMENT_HELP_MESSAGE)
+@click.option('--node-url', type=str, required=False, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url())
+@batch_commands.command('get-list')
+def get_batches(ids, start, limit, head, reverse, node_url):
+ """
+ Get a list of batches.
+ """
+ arguments, errors = ListBatchesForm().load({
+ 'ids': ids,
+ 'start': start,
+ 'limit': limit,
+ 'head': head,
+ 'reverse': reverse,
+ 'node_url': node_url,
+ })
+
+ if errors:
+ print_errors(errors)
+ sys.exit(FAILED_EXIT_FROM_COMMAND_CODE)
+
+ ids = arguments.get('ids')
+ start = arguments.get('start')
+ limit = arguments.get('limit')
+ head = arguments.get('head')
+ reverse = arguments.get('reverse')
+ node_url = arguments.get('node_url')
+
+ remme = Remme(network_config={
+ 'node_address': str(node_url) + ':8080',
+ })
+
+ query = {
+ 'ids': ids,
+ 'start': start,
+ 'limit': limit,
+ 'head': head,
+ 'reverse': reverse,
+ }
+
+ batch_service = Batch(service=remme)
+ batches = loop.run_until_complete(batch_service.get_list(query))
+
+ print_result(batches)
diff --git a/cli/batch/forms.py b/cli/batch/forms.py
new file mode 100644
index 0000000..1fbb421
--- /dev/null
+++ b/cli/batch/forms.py
@@ -0,0 +1,87 @@
+"""
+Provide forms for command line interface's batch commands.
+"""
+import re
+
+from marshmallow import (
+ Schema,
+ ValidationError,
+ fields,
+ validates,
+)
+
+from cli.constants import (
+ DOMAIN_NAME_REGEXP,
+ HEADER_SIGNATURE_REGEXP,
+)
+
+
+class ListBatchesForm(Schema):
+ """
+ List batches validation form.
+ """
+
+ ids = fields.List(fields.String(), allow_none=True, required=False)
+ start = fields.String(allow_none=True, required=False)
+ limit = fields.Integer(allow_none=True, required=False)
+ head = fields.String(allow_none=True, required=False)
+ reverse = fields.String(allow_none=True, required=False)
+ node_url = fields.String(allow_none=True, required=False)
+
+ @validates('ids')
+ def validate_ids(self, ids):
+ """
+ Validate batch identifiers.
+ """
+ if ids is not None:
+ for batch_id in ids:
+ if re.match(pattern=HEADER_SIGNATURE_REGEXP, string=batch_id) is None:
+ raise ValidationError(f'The following batch id `{batch_id}` is invalid.')
+
+ @validates('start')
+ def validate_start(self, start):
+ """
+ Validate start.
+ """
+ if start is not None and re.match(pattern=HEADER_SIGNATURE_REGEXP, string=start) is None:
+ raise ValidationError(f'The following batch id `{start}` is invalid.')
+
+ @validates('limit')
+ def validate_limit(self, limit):
+ """
+ Validate limit.
+ """
+ if limit is not None and limit < 0:
+ raise ValidationError("Limit can't be negative.")
+
+ @validates('head')
+ def validate_head(self, head):
+ """
+ Validate head.
+ """
+ if head is not None and re.match(pattern=HEADER_SIGNATURE_REGEXP, string=head) is None:
+ raise ValidationError(f'The following block id `{head}` is invalid.')
+
+ @validates('reverse')
+ def validate_reverse(self, reverse):
+ """
+ Validate reverse.
+ """
+ if reverse is not None and reverse not in ('true', 'false'):
+ raise ValidationError("Invalid reverse field. Should be either 'true' or 'false'.")
+
+ @validates('node_url')
+ def validate_node_url(self, node_url):
+ """
+ Validate node URL.
+
+ If node URL is localhost, it means client didn't passed any URL, so nothing to validate.
+ """
+ if node_url == 'localhost':
+ return
+
+ if 'http' in node_url or 'https' in node_url:
+ raise ValidationError(f'Pass the following node URL `{node_url}` without protocol (http, https, etc.).')
+
+ if re.match(pattern=DOMAIN_NAME_REGEXP, string=node_url) is None:
+ raise ValidationError(f'The following node URL `{node_url}` is invalid.')
diff --git a/cli/batch/help.py b/cli/batch/help.py
new file mode 100644
index 0000000..1c6e3a4
--- /dev/null
+++ b/cli/batch/help.py
@@ -0,0 +1,8 @@
+"""
+Provide help messages for command line interface's batch commands.
+"""
+BATCH_IDS_ARGUMENT_HELP_MESSAGE = 'Identifiers to get a list of batches by.'
+BATCH_START_ARGUMENT_HELP_MESSAGE = 'Parameter to list batches starting from.'
+BATCH_LIMIT_ARGUMENT_HELP_MESSAGE = 'Parameter to limit amount of batches.'
+BATCH_HEAD_ARGUMENT_HELP_MESSAGE = 'Block identifier to get a list of batches from.'
+BATCH_REVERSE_ARGUMENT_HELP_MESSAGE = 'Parameter to reverse result.'
diff --git a/cli/batch/interfaces.py b/cli/batch/interfaces.py
new file mode 100644
index 0000000..d5335b7
--- /dev/null
+++ b/cli/batch/interfaces.py
@@ -0,0 +1,21 @@
+"""
+Provide implementation of the batch interfaces.
+"""
+
+
+class BatchInterface:
+ """
+ Implements batch interface.
+ """
+
+ async def get_list(self, query=None):
+ """
+ Get a list of batches from REMChain.
+
+ Arguments:
+ query (dict, optional): dictionary with specific parameters
+
+ Returns:
+ List of batches.
+ """
+ pass
diff --git a/cli/batch/service.py b/cli/batch/service.py
new file mode 100644
index 0000000..d66a1e7
--- /dev/null
+++ b/cli/batch/service.py
@@ -0,0 +1,34 @@
+"""
+Provide implementation of the batch service.
+"""
+from accessify import implements
+
+from cli.batch.interfaces import BatchInterface
+
+
+@implements(BatchInterface)
+class Batch:
+ """
+ Implements batch interface.
+ """
+
+ def __init__(self, service):
+ """
+ Constructor.
+
+ Arguments:
+ service: object to interact with Remme core API.
+ """
+ self.service = service
+
+ async def get_list(self, query=None):
+ """
+ Get all batches from REMChain.
+
+ Arguments:
+ query (dict, optional): dictionary with specific parameters
+
+ Returns:
+ List of batches.
+ """
+ return await self.service.blockchain_info.get_batches(query=query)
diff --git a/cli/entrypoint.py b/cli/entrypoint.py
index e45563e..eb76ddd 100644
--- a/cli/entrypoint.py
+++ b/cli/entrypoint.py
@@ -4,6 +4,7 @@
import click
from cli.account.cli import account_commands
+from cli.batch.cli import batch_commands
@click.group()
@@ -17,3 +18,4 @@ def cli():
cli.add_command(account_commands)
+cli.add_command(batch_commands)
diff --git a/tests/batch/test_get_batches.py b/tests/batch/test_get_batches.py
new file mode 100644
index 0000000..204a9b7
--- /dev/null
+++ b/tests/batch/test_get_batches.py
@@ -0,0 +1,276 @@
+"""
+Provide tests for command line interface's get batches command.
+"""
+import json
+
+from click.testing import CliRunner
+
+from cli.constants import (
+ FAILED_EXIT_FROM_COMMAND_CODE,
+ NODE_IP_ADDRESS_FOR_TESTING,
+ PASSED_EXIT_FROM_COMMAND_CODE,
+)
+from cli.entrypoint import cli
+
+BATCH_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE = \
+ '93225596c6dcd8e520846700e052651728b1c85d1ae2fcf44e14bb628d97b4c9' \
+ '6f314618ab3cea3e43775f9874089db6040e86cf5ab3d7be50454bef1d6bc73b'
+
+BATCH_HEAD_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE = \
+ 'eecfeb7fe9c82193ba7e4b731cb8e3aec1b384d9fcb1f481f812573319093801' \
+ '56da6ee44533c77b95bd311f1a72c303a8b60a3415570e3d0d369ff2b210e829'
+
+
+def test_get_batches_by_ids():
+ """
+ Case: get batches by identifiers.
+ Expect: list with batch data is returned, header, header signature, transactions are presented.
+ """
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--ids',
+ '[' + BATCH_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE + ']',
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ command_response = json.loads(result.output)
+ data = command_response.get('result').get('data')
+ first_batch = data[0]
+ transactions = first_batch.get('transactions')
+ header = first_batch.get('header')
+ header_signature = first_batch.get('header_signature')
+
+ assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert BATCH_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE == header_signature
+ assert len(data) == 1
+ assert len(transactions) > 0
+ assert header is not None
+
+
+def test_get_batches_invalid_ids():
+ """
+ Case: get batches by invalid identifiers.
+ Expect: the following batch id is invalid error message.
+ """
+ invalid_batch_identifier = '152f3be91d8238538a83077ec8cd5d1d937767c0930eea61b59151b0dfa7c5a1'
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--ids',
+ '[' + invalid_batch_identifier + ']',
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert 'The following batch id `{id}` is invalid.'.format(id=invalid_batch_identifier) in result.output
+
+
+def test_get_batches_invalid_list_ids():
+ """
+ Case: get batches by invalid list of identifiers.
+ Expect: not a valid list error message.
+ """
+ invalid_identifiers_list = '152f3be91d8238538a83077ec8cd5d1d937767c0930eea61b59151b0dfa7c5a1'
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--ids',
+ invalid_identifiers_list,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert 'Not a valid list.' in result.output
+
+
+def test_get_batches_by_start():
+ """
+ Case: get batches by start.
+ Expect: list with batch data is returned, header, header signature, transactions are presented.
+ """
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--start',
+ BATCH_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ command_response = json.loads(result.output)
+ data = command_response.get('result').get('data')
+ first_batch = data[0]
+ transactions = first_batch.get('transactions')
+ header = first_batch.get('header')
+ header_signature = first_batch.get('header_signature')
+
+ assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert BATCH_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE == header_signature
+ assert len(data) >= 1
+ assert len(transactions) > 0
+ assert header is not None
+
+
+def test_get_batches_invalid_start():
+ """
+ Case: get batches by invalid start.
+ Expect: the following batch id is invalid error message.
+ """
+ invalid_start_identifier = '152f3be91d8238538a83077ec8cd5d1d937767c0930eea61b59151b0dfa7c5a1'
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--start',
+ invalid_start_identifier,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert 'The following batch id `{id}` is invalid.'.format(id=invalid_start_identifier) in result.output
+
+
+def test_get_batches_limit():
+ """
+ Case: get batches with limit field.
+ Expect: limited list with batch data is returned.
+ """
+ limit = 2
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--limit',
+ limit,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ command_response = json.loads(result.output)
+ data = command_response.get('result').get('data')
+
+ assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert len(data) <= limit
+
+
+def test_get_batches_negative_limit():
+ """
+ Case: get batches with negative limit.
+ Expect: limit can't be negative error message.
+ """
+ limit = -1
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--limit',
+ limit,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert "Limit can't be negative." in result.output
+
+
+def test_get_batches_invalid_limit():
+ """
+ Case: get batches with invalid limit.
+ Expect: invalid integer error message.
+ """
+ limit = '152f3be91d8238538a83077ec8cd5d1d937767c0930eea61b59151b0dfa7c5a1'
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--limit',
+ limit,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ assert limit + ' is not a valid integer' in result.output
+
+
+def test_get_batches_by_head():
+ """
+ Case: get batches by head.
+ Expect: list with batch data is returned, header, header signature, transactions are presented.
+ """
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--head',
+ BATCH_HEAD_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ command_response = json.loads(result.output)
+ data = command_response.get('result').get('data')
+ first_batch = data[0]
+ transactions = first_batch.get('transactions')
+ header = first_batch.get('header')
+
+ assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert len(data) >= 1
+ assert len(transactions) > 0
+ assert header is not None
+
+
+def test_get_batches_invalid_head():
+ """
+ Case: get batches by invalid head.
+ Expect: the following block id is invalid error message.
+ """
+ invalid_head_identifier = '152f3be91d8238538a83077ec8cd5d1d937767c0930eea61b59151b0dfa7c5a1'
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--head',
+ invalid_head_identifier,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert 'The following block id `{id}` is invalid.'.format(id=invalid_head_identifier) in result.output
+
+
+def test_get_batches_invalid_reverse():
+ """
+ Case: get batches with invalid reverse field.
+ Expect: invalid reverse field error message.
+ """
+ invalid_reverse_identifier = 'invalid_reverse'
+
+ runner = CliRunner()
+ result = runner.invoke(cli, [
+ 'batch',
+ 'get-list',
+ '--reverse',
+ invalid_reverse_identifier,
+ '--node-url',
+ NODE_IP_ADDRESS_FOR_TESTING,
+ ])
+
+ assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code
+ assert "Invalid reverse field. Should be either 'true' or 'false'." in result.output