diff --git a/README.md b/README.md index df3cec1..a6d3365 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,21 @@ Transfer available tokens from frozen to unfrozen reputation's balances (executa ```bash $ remme node-account transfer-tokens-from-frozen-to-unfrozen +{ + "result": { + "batch_identifier": "045c2b7c43a7ca7c3dc60e92714c03265572a726d1fae631c39a404eaf97770e3f6a7a8c35c86f6361afb2e4f12b4a17d71a66a19158b62f30531ab32b62f06f" + } +} +``` + +Transfer tokens from unfrozen reputational balance to operational balance (executable only on the machine which runs the node) — `remme node-account transfer-tokens-from-unfrozen-to-operational`. + +| Arguments | Type | Required | Description | +| :------: | :-----: | :------: | -------------------- | +| amount | Integer | Yes | Amount to transfer. | + +```bash +$ remme node-account transfer-tokens-from-unfrozen-to-operational --amount=1000 { "result": { "batch_id": "045c2b7c43a7ca7c3dc60e92714c03265572a726d1fae631c39a404eaf97770e3f6a7a8c35c86f6361afb2e4f12b4a17d71a66a19158b62f30531ab32b62f06f" diff --git a/cli/node_account/cli.py b/cli/node_account/cli.py index 56bee4a..a970ccb 100644 --- a/cli/node_account/cli.py +++ b/cli/node_account/cli.py @@ -14,7 +14,10 @@ ) from cli.errors import NotSupportedOsToGetNodePrivateKeyError from cli.generic.forms.forms import TransferTokensForm -from cli.node_account.forms import GetNodeAccountInformationForm +from cli.node_account.forms import ( + GetNodeAccountInformationForm, + TransferTokensFromUnfrozenToOperationalForm, +) from cli.node_account.help import ( ACCOUNT_ADDRESS_TO_ARGUMENT_HELP_MESSAGE, AMOUNT_ARGUMENT_HELP_MESSAGE, @@ -132,3 +135,38 @@ def transfer_tokens_from_frozen_to_unfrozen(): sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) print_result(result=result) + + +@node_account_commands.command('transfer-tokens-from-unfrozen-to-operational') +@click.option('--amount', type=int, required=True, help=AMOUNT_ARGUMENT_HELP_MESSAGE) +def transfer_tokens_from_unfrozen_to_operational(amount): + """ + Transfer tokens from unfrozen reputational balance to operational balance. + """ + arguments, errors = TransferTokensFromUnfrozenToOperationalForm().load({ + 'amount': amount, + }) + + if errors: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + try: + node_private_key = NodePrivateKey().get() + + except (NotSupportedOsToGetNodePrivateKeyError, FileNotFoundError) as error: + print_errors(errors=str(error)) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + remme = Remme( + account_config={'private_key_hex': node_private_key, 'account_type': AccountType.NODE}, + network_config={'node_address': 'localhost' + ':8080'}, + ) + + result, errors = NodeAccount(service=remme).transfer_tokens_from_unfrozen_to_operational(amount=amount) + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + print_result(result=result) diff --git a/cli/node_account/forms.py b/cli/node_account/forms.py index 4643660..6d38000 100644 --- a/cli/node_account/forms.py +++ b/cli/node_account/forms.py @@ -1,7 +1,11 @@ """ Provide forms for command line interface's node account commands. """ -from marshmallow import Schema +from marshmallow import ( + Schema, + fields, + validate, +) from cli.generic.forms.fields import ( AccountAddressField, @@ -16,3 +20,17 @@ class GetNodeAccountInformationForm(Schema): address = AccountAddressField(required=True) node_url = NodeUrlField(required=True) + + +class TransferTokensFromUnfrozenToOperationalForm(Schema): + """ + A transfer of tokens from unfrozen reputational balance to operational balance form. + """ + + amount = fields.Integer( + strict=True, + required=True, + validate=[ + validate.Range(min=1, error='Amount must be greater than 0.'), + ], + ) diff --git a/cli/node_account/interfaces.py b/cli/node_account/interfaces.py index c2d6bea..6229ef7 100644 --- a/cli/node_account/interfaces.py +++ b/cli/node_account/interfaces.py @@ -28,3 +28,9 @@ def transfer_tokens_from_frozen_to_unfrozen(self): Transfer available tokens from frozen to unfrozen reputation's balances. """ pass + + def transfer_tokens_from_unfrozen_to_operational(self, amount): + """ + Transfer tokens from unfrozen to operational balance. + """ + pass diff --git a/cli/node_account/service.py b/cli/node_account/service.py index 391f6db..0b191b1 100644 --- a/cli/node_account/service.py +++ b/cli/node_account/service.py @@ -75,3 +75,19 @@ def transfer_tokens_from_frozen_to_unfrozen(self): return { 'batch_identifier': transfer_transaction.batch_id, }, None + + def transfer_tokens_from_unfrozen_to_operational(self, amount): + """ + Transfer tokens from unfrozen reputational balance to operational balance. + """ + try: + transfer_transaction = loop.run_until_complete( + self.service.token.transfer_from_unfrozen_to_operational(amount=amount), + ) + + except Exception as error: + return None, str(error) + + return { + 'batch_identifier': transfer_transaction.batch_id, + }, None diff --git a/tests/node_account/test_transfer_tokens_from_unfrozen_to_operational.py b/tests/node_account/test_transfer_tokens_from_unfrozen_to_operational.py new file mode 100644 index 0000000..9d5231b --- /dev/null +++ b/tests/node_account/test_transfer_tokens_from_unfrozen_to_operational.py @@ -0,0 +1,100 @@ +""" +Provide tests for command line interface's node account transfer tokens from unfrozen to operational balance. +""" +import json + +import pytest +from click.testing import CliRunner + +from cli.constants import ( + FAILED_EXIT_FROM_COMMAND_CODE, + INCORRECT_ENTERED_COMMAND_CODE, + PASSED_EXIT_FROM_COMMAND_CODE, +) +from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json + + +def test_transfer_tokens_from_unfrozen_to_operational(mocker, transaction): + """ + Case: transfer tokens from unfrozen reputational balance to operational balance. + Expect: transaction's batch identifier is returned. + """ + mock_get_node_private_key = mocker.patch('cli.config.NodePrivateKey.get') + mock_get_node_private_key.return_value = '42dada12f863528bd456785d8c544154db6ec9455be2c123d91b687df3697314' + + mock_node_account_transfer_tokens_from_unfrozen_to_operational = \ + mocker.patch('cli.node_account.service.loop.run_until_complete') + mock_node_account_transfer_tokens_from_unfrozen_to_operational.return_value = transaction + + runner = CliRunner() + result = runner.invoke(cli, [ + 'node-account', + 'transfer-tokens-from-unfrozen-to-operational', + '--amount', + 1000, + ]) + + transaction_batch_identifier = json.loads(result.output).get('result').get('batch_identifier') + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert transaction.batch_id == transaction_batch_identifier + + +def test_transfer_tokens_from_unfrozen_to_operational_invalid_amount(mocker, transaction): + """ + Case: transfer tokens from unfrozen reputational balance to operational balance with invalid amount. + Expect: amount is not a valid integer error message. + """ + invalid_amount = 'je682' + + mock_get_node_private_key = mocker.patch('cli.config.NodePrivateKey.get') + mock_get_node_private_key.return_value = '42dada12f863528bd456785d8c544154db6ec9455be2c123d91b687df3697314' + + mock_node_account_transfer_tokens_from_unfrozen_to_operational = \ + mocker.patch('cli.node_account.service.loop.run_until_complete') + mock_node_account_transfer_tokens_from_unfrozen_to_operational.return_value = transaction + + runner = CliRunner() + result = runner.invoke(cli, [ + 'node-account', + 'transfer-tokens-from-unfrozen-to-operational', + '--amount', + invalid_amount, + ]) + + assert INCORRECT_ENTERED_COMMAND_CODE == result.exit_code + assert f'{invalid_amount} is not a valid integer' in result.output + + +@pytest.mark.parametrize('insufficient_amount', [-1, 0]) +def test_transfer_tokens_from_unfrozen_to_operational_insufficient_amount(mocker, transaction, insufficient_amount): + """ + Case: transfer tokens from unfrozen reputational balance to operational balance with insufficient amount. + Expect: amount must be greater than 0 error message. + """ + mock_get_node_private_key = mocker.patch('cli.config.NodePrivateKey.get') + mock_get_node_private_key.return_value = '42dada12f863528bd456785d8c544154db6ec9455be2c123d91b687df3697314' + + mock_node_account_transfer_tokens_from_unfrozen_to_operational = \ + mocker.patch('cli.node_account.service.loop.run_until_complete') + mock_node_account_transfer_tokens_from_unfrozen_to_operational.return_value = transaction + + runner = CliRunner() + result = runner.invoke(cli, [ + 'node-account', + 'transfer-tokens-from-unfrozen-to-operational', + '--amount', + insufficient_amount, + ]) + + expected_error = { + 'errors': { + 'amount': [ + f'Amount must be greater than 0.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output