From f7852ff6294e62a6f01faaddee6fa7ae1da6c002 Mon Sep 17 00:00:00 2001 From: Anastasiia Bilova Date: Mon, 6 May 2019 15:41:11 +0300 Subject: [PATCH] REM-1312: Get particular Atomic Swap information by its identifier (#24) --- README.md | 32 ++++- cli/atomic_swap/cli.py | 38 +++++- cli/atomic_swap/forms.py | 14 +- cli/atomic_swap/help.py | 1 + cli/atomic_swap/interfaces.py | 6 + cli/atomic_swap/service.py | 18 +++ cli/constants.py | 1 + cli/generic/forms/fields.py | 21 +++ tests/atomic_swap/test_get_info.py | 208 +++++++++++++++++++++++++++++ tests/conftest.py | 33 +++++ 10 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 tests/atomic_swap/test_get_info.py diff --git a/README.md b/README.md index b5a12b0..831a85e 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ * [Requirements](#getting-started-requirements) * [Installation](#installation) * [Usage](#usage) - * [Nodes](#nodes) * [Configuration file](#configuration-file) * [Service](#service) * [Account](#account) + * [Atomic Swap](#atomic-swap) * [Node](#node) * [Public key](#public-key) * [Transaction](#transaction) @@ -159,6 +159,36 @@ $ remme atomic-swap get-public-key --node-url=node-6-testnet.remme.io } ``` +Get information about atomic swap by its identifier — ``remme atomic-swap get-info``: + +| Arguments | Type | Required | Description | +| :-------: | :----: | :------: | ------------------------------------------------- | +| id | String | Yes | Swap identifier to get information about swap by. | +| node-url | String | No | Node URL to apply a command to. | + +```bash +$ remme atomic-swap get-info \ + --id=033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808 \ + --node-url=node-genesis-testnet.remme.io +{ + "result": { + "information": { + "amount": "10.0000", + "created_at": 1556803765, + "email_address_encrypted_optional": "", + "is_initiator": false, + "receiver_address": "112007484def48e1c6b77cf784aeabcac51222e48ae14f3821697f4040247ba01558b1", + "secret_key": "", + "secret_lock": "0728356568862f9da0825aa45ae9d3642d64a6a732ad70b8857b2823dbf2a0b8", + "sender_address": "1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf", + "sender_address_non_local": "0xe6ca0e7c974f06471759e9a05d18b538c5ced11e", + "state": "OPENED", + "swap_id": "033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808" + } + } +} +``` + ### Node Get node configurations — ``remme node get-configs``: diff --git a/cli/atomic_swap/cli.py b/cli/atomic_swap/cli.py index b1cbfd3..14af8d6 100644 --- a/cli/atomic_swap/cli.py +++ b/cli/atomic_swap/cli.py @@ -6,7 +6,11 @@ import click from remme import Remme -from cli.atomic_swap.forms import GetAtomicSwapPublicKeyForm +from cli.atomic_swap.forms import ( + GetAtomicSwapInformationForm, + GetAtomicSwapPublicKeyForm, +) +from cli.atomic_swap.help import SWAP_IDENTIFIER_ARGUMENT_HELP_MESSAGE from cli.atomic_swap.service import AtomicSwap from cli.constants import ( FAILED_EXIT_FROM_COMMAND_CODE, @@ -54,3 +58,35 @@ def get_public_key(node_url): sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) print_result(result=result) + + +@click.option('--id', type=str, required=True, help=SWAP_IDENTIFIER_ARGUMENT_HELP_MESSAGE) +@click.option('--node-url', type=str, required=False, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url()) +@atomic_swap_commands.command('get-info') +def get_swap_info(id, node_url): + """ + Get information about atomic swap by its identifier. + """ + arguments, errors = GetAtomicSwapInformationForm().load({ + 'id': id, + 'node_url': node_url, + }) + + if errors: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + swap_id = arguments.get('id') + node_url = arguments.get('node_url') + + remme = Remme(network_config={ + 'node_address': str(node_url) + ':8080', + }) + + result, errors = AtomicSwap(service=remme).get(swap_id=swap_id) + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + print_result(result=result) diff --git a/cli/atomic_swap/forms.py b/cli/atomic_swap/forms.py index fd65a79..7969484 100644 --- a/cli/atomic_swap/forms.py +++ b/cli/atomic_swap/forms.py @@ -3,7 +3,19 @@ """ from marshmallow import Schema -from cli.generic.forms.fields import NodeUrlField +from cli.generic.forms.fields import ( + NodeUrlField, + SwapIdentifierField, +) + + +class GetAtomicSwapInformationForm(Schema): + """ + Get information about atomic swap by its identifier form. + """ + + id = SwapIdentifierField(required=True) + node_url = NodeUrlField(required=False) class GetAtomicSwapPublicKeyForm(Schema): diff --git a/cli/atomic_swap/help.py b/cli/atomic_swap/help.py index 738ea13..69a355d 100644 --- a/cli/atomic_swap/help.py +++ b/cli/atomic_swap/help.py @@ -1,3 +1,4 @@ """ Provide help messages for command line interface's atomic swap commands. """ +SWAP_IDENTIFIER_ARGUMENT_HELP_MESSAGE = 'Swap identifier to get information about swap by.' diff --git a/cli/atomic_swap/interfaces.py b/cli/atomic_swap/interfaces.py index 89a8bc3..8e88e87 100644 --- a/cli/atomic_swap/interfaces.py +++ b/cli/atomic_swap/interfaces.py @@ -13,3 +13,9 @@ def get_public_key(self): Get public key of atomic swap. """ pass + + def get(self, swap_id): + """ + Get information about atomic swap by its identifier. + """ + pass diff --git a/cli/atomic_swap/service.py b/cli/atomic_swap/service.py index d996d53..a48d503 100644 --- a/cli/atomic_swap/service.py +++ b/cli/atomic_swap/service.py @@ -4,6 +4,7 @@ import asyncio from accessify import implements +from aiohttp_json_rpc import RpcGenericServerDefinedError from cli.atomic_swap.interfaces import AtomicSwapInterface @@ -38,3 +39,20 @@ def get_public_key(self): return { 'public_key': public_key, }, None + + def get(self, swap_id): + """ + Get information about atomic swap by its identifier. + """ + try: + swap_info = loop.run_until_complete(self.service.swap.get_info(swap_id=swap_id)) + + except RpcGenericServerDefinedError as error: + return None, str(error.message) + + except Exception as error: + return None, str(error) + + return { + 'information': swap_info.data, + }, None diff --git a/cli/constants.py b/cli/constants.py index 6dcdd05..336a077 100644 --- a/cli/constants.py +++ b/cli/constants.py @@ -9,6 +9,7 @@ PRIVATE_KEY_REGEXP = r'^[a-f0-9]{64}$' PUBLIC_KEY_ADDRESS_REGEXP = r'^[0-9a-f]{70}$' HEADER_SIGNATURE_REGEXP = r'^[0-9a-f]{128}$' +SWAP_IDENTIFIER_REGEXP = r'^[a-f0-9]{64}$' TRANSACTION_HEADER_SIGNATURE_REGEXP = r'^[0-9a-f]{128}$' DOMAIN_NAME_REGEXP = r'(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]' diff --git a/cli/generic/forms/fields.py b/cli/generic/forms/fields.py index 1505642..352cda2 100644 --- a/cli/generic/forms/fields.py +++ b/cli/generic/forms/fields.py @@ -14,6 +14,7 @@ FAMILY_NAMES, PRIVATE_KEY_REGEXP, PUBLIC_KEY_ADDRESS_REGEXP, + SWAP_IDENTIFIER_REGEXP, TRANSACTION_HEADER_SIGNATURE_REGEXP, ) @@ -165,3 +166,23 @@ def _deserialize(self, value, attr, data, **kwargs): raise ValidationError(f'The following public key address `{public_key_address}` is invalid.') return value + + +class SwapIdentifierField(fields.Field): + """ + Implements validation of the swap identifier. + + References: + - https://marshmallow.readthedocs.io/en/3.0/custom_fields.html + """ + + def _deserialize(self, value, attr, data, **kwargs): + """ + Validate data (swap identifier) that was passed to field. + """ + swap_identifier = value + + if re.match(pattern=SWAP_IDENTIFIER_REGEXP, string=swap_identifier) is None: + raise ValidationError(f'The following swap identifier `{swap_identifier}` is invalid.') + + return swap_identifier diff --git a/tests/atomic_swap/test_get_info.py b/tests/atomic_swap/test_get_info.py new file mode 100644 index 0000000..08ce4a3 --- /dev/null +++ b/tests/atomic_swap/test_get_info.py @@ -0,0 +1,208 @@ +""" +Provide tests for command line interface's atomic swap commands. +""" +import json +import re + +import pytest +from click.testing import CliRunner + +from cli.constants import ( + ADDRESS_REGEXP, + FAILED_EXIT_FROM_COMMAND_CODE, + NODE_27_IN_TESTNET_ADDRESS, + PASSED_EXIT_FROM_COMMAND_CODE, + SWAP_IDENTIFIER_REGEXP, +) +from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json + +SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE = '033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808' + + +def test_get_swap_info(): + """ + Case: get information about atomic swap by its identifier. + Expect: information about the swap is returned. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + NODE_27_IN_TESTNET_ADDRESS, + ]) + + swap_info = json.loads(result.output).get('result').get('information') + swap_identifier = swap_info.get('swap_id') + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE == swap_identifier + assert re.match(pattern=ADDRESS_REGEXP, string=swap_info.get('sender_address')) is not None + assert re.match(pattern=ADDRESS_REGEXP, string=swap_info.get('receiver_address')) is not None + assert re.match(pattern=SWAP_IDENTIFIER_REGEXP, string=swap_identifier) is not None + assert isinstance(swap_info.get('is_initiator'), bool) + + +def test_get_swap_info_without_node_url(mocker, swap_info): + """ + Case: get information about atomic swap by its identifier without passing node URL. + Expect: information about the swap is returned from node on localhost. + """ + mock_swap_get_info = mocker.patch('cli.atomic_swap.service.loop.run_until_complete') + mock_swap_get_info.return_value = swap_info + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + ]) + + expected_result = { + 'result': { + 'information': swap_info.data, + }, + } + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert expected_result == json.loads(result.output) + + +def test_get_swap_info_invalid_swap_id(): + """ + Case: get information about atomic swap by its invalid identifier. + Expect: the following swap identifier is invalid error message. + """ + invalid_swap_id = '033402fe1346742486b15a3a9966eb524927' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + invalid_swap_id, + '--node-url', + NODE_27_IN_TESTNET_ADDRESS, + ]) + + expected_error = { + 'errors': { + 'id': [ + f'The following swap identifier `{invalid_swap_id}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +def test_get_swap_info_non_existing_swap_id(): + """ + Case: get information about atomic swap by passing non-existing identifier. + Expect: atomic swap with identifier not found error message. + """ + non_existing_swap_id = '033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6809' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + non_existing_swap_id, + '--node-url', + NODE_27_IN_TESTNET_ADDRESS, + ]) + + expected_error = { + 'errors': f'Atomic swap with id "{non_existing_swap_id}" not found.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +def test_get_swap_info_non_existing_node_url(): + """ + Case: get information about atomic swap by passing non-existing node URL. + Expect: check if node running at URL error message. + """ + non_existing_node_url = 'non-existing-node.com' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + non_existing_node_url, + ]) + + expected_error = { + 'errors': f'Please check if your node running at http://{non_existing_node_url}:8080.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +def test_get_swap_info_invalid_node_url(): + """ + Case: get information about atomic swap by passing invalid node URL. + Expect: the following node URL is invalid error message. + """ + invalid_node_url = 'domainwithoutextention' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + invalid_node_url, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'The following node URL `{invalid_node_url}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +@pytest.mark.parametrize('node_url_with_protocol', ['http://masternode.com', 'https://masternode.com']) +def test_get_swap_info_node_url_with_protocol(node_url_with_protocol): + """ + Case: get information about atomic swap by passing node URL with explicit protocol. + Expect: the following node URL contains protocol error message. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + node_url_with_protocol, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_protocol}` without protocol (http, https, etc.).', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output diff --git a/tests/conftest.py b/tests/conftest.py index 7114926..2ffbc20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -156,6 +156,31 @@ def data(self): } +class AtomicSwapInformation: + """ + Impose atomic swap information data transfer object. + """ + + @property + def data(self): + """ + Get atomic swap information. + """ + return { + 'sender_address': '112007be95c8bb240396446ec359d0d7f04d257b72aeb4ab1ecfe50cf36e400a96ab9c', + 'receiver_address': '112007484def48e1c6b77cf784aeabcac51222e48ae14f3821697f4040247ba01558b1', + 'amount': '10.0000', + 'swap_id': '033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808', + 'secret_lock': '0728356568862f9da0825aa45ae9d3642d64a6a732ad70b8857b2823dbf2a0b8', + 'created_at': 1555943451, + 'sender_address_non_local': '0xe6ca0e7c974f06471759e9a05d18b538c5ced11e', + 'state': 'OPENED', + 'email_address_encrypted_optional': '', + 'secret_key': '', + 'is_initiator': False, + } + + class NodeConfigurations: """ Impose node configurations data transfer object. @@ -188,6 +213,14 @@ def public_key_info(): return PublicKeyInformation() +@pytest.fixture() +def swap_info(): + """ + Get atomic swap information fixture. + """ + return AtomicSwapInformation() + + @pytest.fixture() def node_configurations(): """