From 3300af22d653bb22368e6eaee8f5e3bb4f90dec5 Mon Sep 17 00:00:00 2001 From: Moul <moul@moul.re> Date: Wed, 16 Mar 2022 08:10:07 +0100 Subject: [PATCH] [feat] #134: Add ability to pass a file containing the tx recipients and amounts Introduce function to parse the file and compute the values Comments (#) are ignored ABSOLUTE/RELATIVE header required to specify the reference of the specified amounts Set recipients argument to no longer required, add a check instead Set file mutuality exclusive with amounts(UD), allsources, and recipients Update tx cli tests Add tests --- silkaj/tx.py | 95 +++++++++++++++++++++++++++++++++++-------- tests/test_tx.py | 12 +++--- tests/test_tx_file.py | 79 +++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 tests/test_tx_file.py diff --git a/silkaj/tx.py b/silkaj/tx.py index 685806f3..65702fa8 100644 --- a/silkaj/tx.py +++ b/silkaj/tx.py @@ -13,8 +13,12 @@ # You should have received a copy of the GNU Affero General Public License # along with Silkaj. If not, see <https://www.gnu.org/licenses/>. + import math +import shlex +import sys from re import compile, search +from typing import List import click from duniterpy.api.bma.tx import process @@ -60,7 +64,7 @@ NBR_ISSUERS = 1 type=click.FloatRange(MINIMAL_ABSOLUTE_TX_AMOUNT), help=f"Quantitative amount(s):\n-a <amount>\nMinimum amount is {MINIMAL_ABSOLUTE_TX_AMOUNT}.", cls=cli_tools.MutuallyExclusiveOption, - mutually_exclusive=["amountsud", "allsources"], + mutually_exclusive=["amountsud", "allsources", "file_path"], ) @click.option( "amountsud", @@ -70,25 +74,34 @@ NBR_ISSUERS = 1 type=click.FloatRange(MINIMAL_RELATIVE_TX_AMOUNT), help=f"Relative amount(s):\n-d <amount_UD>\nMinimum amount is {MINIMAL_RELATIVE_TX_AMOUNT}", cls=cli_tools.MutuallyExclusiveOption, - mutually_exclusive=["amounts", "allsources"], + mutually_exclusive=["amounts", "allsources", "file_path"], ) @click.option( "--allSources", is_flag=True, help="Send all sources to one recipient", cls=cli_tools.MutuallyExclusiveOption, - mutually_exclusive=["amounts", "amountsud"], + mutually_exclusive=["amounts", "amountsud", "file_path"], ) @click.option( "recipients", "--recipient", "-r", multiple=True, - required=True, help="Pubkey(s)’ recipients + optional checksum:\n-r <pubkey>[:checksum]\n\ Sending to many recipients is possible:\n\ * With one amount, all will receive the amount\n\ * With many amounts (one per recipient)", + cls=cli_tools.MutuallyExclusiveOption, + mutually_exclusive=["file_path"], +) +@click.option( + "file_path", + "--file", + "-f", + help="File’s path containing a list of amounts in absolute or relative reference and recipients’ pubkeys", + cls=cli_tools.MutuallyExclusiveOption, + mutually_exclusive=["recipients", "amounts", "amountsUD", "allsources"], ) @click.option("--comment", "-c", default="", help="Comment") @click.option( @@ -103,22 +116,25 @@ def send_transaction( amountsud, allsources, recipients, + file_path, comment, outputbackchange, yes, ): - """ - Main function - """ - if not (amounts or amountsud or allsources): - tools.message_exit("Error: amount, amountUD or allSources is not set.") - if allsources and len(recipients) > 1: - tools.message_exit( - "Error: the --allSources option can only be used with one recipient." - ) - # compute amounts and amountsud - if not allsources: - tx_amounts = transaction_amount(amounts, amountsud, recipients) + if file_path: + tx_amounts, recipients = parse_file_containing_amounts_recipients(file_path) + else: + if not (amounts or amountsud or allsources): + tools.message_exit("Error: amount, amountUD or allSources is not set.") + if not recipients: + tools.message_exit("Error: A recipient should be passed") + if allsources and len(recipients) > 1: + tools.message_exit( + "Error: the --allSources option can only be used with one recipient." + ) + # compute amounts and amountsud + if not allsources: + tx_amounts = transaction_amount(amounts, amountsud, recipients) key = auth.auth_method() issuer_pubkey = key.pubkey @@ -167,6 +183,53 @@ def send_transaction( ) +def parse_file_containing_amounts_recipients(file_path: str) -> List: + """ + Parse file in a specific format + Comments are ignored + Format should be: + ```txt + [ABSOLUTE/RELATIVE] + + # comment1 + amount1 recipient1’s pubkey + # comment2 + amount2 recipient2’s pubkey + ``` + """ + reference = "" + amounts, recipients = [], [] + with open(file_path) as file: + for n, line in enumerate(file): + line = shlex.split(line, True) + if line: + if n == 0: + reference = line[0] + else: + try: + amounts.append(float(line[0])) + recipients.append(line[1]) + except (ValueError, IndexError): + tools.message_exit(f"Syntax error at line {n + 1}") + + if not reference or (reference != "ABSOLUTE" and reference != "RELATIVE"): + tools.message_exit( + f"{file_path} must contain at first line 'ABSOLUTE' or 'RELATIVE' header" + ) + + if not amounts or not recipients: + tools.message_exit("No amounts or recipients specified") + + # Compute amount depending on the reference + if reference == "ABSOLUTE": + reference_mult = CENT_MULT_TO_UNIT + else: + reference_mult = money.UDValue().ud_value + tx_amounts = compute_amounts(amounts, reference_mult) + + return tx_amounts, recipients + + def transaction_amount(amounts, UDs_amounts, outputAddresses): """ Check that the number of passed amounts(UD) and recipients are the same diff --git a/tests/test_tx.py b/tests/test_tx.py index d5c90417..94464efc 100644 --- a/tests/test_tx.py +++ b/tests/test_tx.py @@ -135,16 +135,16 @@ def test_transaction_amount_errors( def test_tx_passed_amount_cli(): """One option""" result = CliRunner().invoke(cli, ["tx", "--amount", "1"]) - assert "Error: Missing option" in result.output - assert result.exit_code == 2 + assert "Error: A recipient should be passed\n" in result.output + assert result.exit_code == 1 result = CliRunner().invoke(cli, ["tx", "--amountUD", "1"]) - assert "Error: Missing option" in result.output - assert result.exit_code == 2 + assert "Error: A recipient should be passed\n" in result.output + assert result.exit_code == 1 result = CliRunner().invoke(cli, ["tx", "--allSources"]) - assert "Error: Missing option" in result.output - assert result.exit_code == 2 + assert "Error: A recipient should be passed\n" in result.output + assert result.exit_code == 1 """Multiple options""" result = CliRunner().invoke(cli, ["tx", "--amount", 1, "--amountUD", 1]) diff --git a/tests/test_tx_file.py b/tests/test_tx_file.py new file mode 100644 index 00000000..628a7d1e --- /dev/null +++ b/tests/test_tx_file.py @@ -0,0 +1,79 @@ +# Copyright 2016-2022 Maël Azimi <m.a@moul.re> +# +# Silkaj is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Silkaj is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Silkaj. If not, see <https://www.gnu.org/licenses/>. + +import pytest +from click.testing import CliRunner + +from silkaj.constants import CENT_MULT_TO_UNIT +from silkaj.money import UDValue +from silkaj.tx import parse_file_containing_amounts_recipients + +FILE_PATH = "recipients.txt" + +ud_value = UDValue().ud_value + + +@pytest.mark.parametrize( + "file_content, amounts_exp, recipients_exp", + [ + ( + "ABSOLUTE\n10 pubkey1\n20 pubkey2", + [10 * CENT_MULT_TO_UNIT, 20 * CENT_MULT_TO_UNIT], + ["pubkey1", "pubkey2"], + ), + ( + "RELATIVE\n#toto\n10 pubkey1\n#titi\n20 pubkey2", + [10 * ud_value, 20 * ud_value], + ["pubkey1", "pubkey2"], + ), + ], +) +def test_parse_file_containing_amounts_recipients( + file_content, amounts_exp, recipients_exp +): + runner = CliRunner() + with runner.isolated_filesystem(): + with open(FILE_PATH, "w") as f: + f.write(file_content) + amounts, recipients = parse_file_containing_amounts_recipients(FILE_PATH) + assert amounts == amounts_exp + assert recipients == recipients_exp + + +HEADER_ERR = ( + "recipients.txt must contain at first line 'ABSOLUTE' or 'RELATIVE' header\n" +) +SYNTAX_ERR = "Syntax error at line" +SPEC_ERR = "No amounts or recipients specified" + + +@pytest.mark.parametrize( + "file_content, error", + [ + ("ABSOLUTE\n10 pubkey1\n20", SYNTAX_ERR), + ("#RELATIVE\n10 pubkey1\n20 pubkey2", HEADER_ERR), + ("RELATIVE\npubkey1 10\n20 pubkey2", SYNTAX_ERR), + ("ABSOLUTE", SPEC_ERR), + ("", HEADER_ERR), + ], +) +def test_parse_file_containing_amounts_recipients_errors(file_content, error, capsys): + runner = CliRunner() + with runner.isolated_filesystem(): + with open(FILE_PATH, "w") as f: + f.write(file_content) + with pytest.raises(SystemExit) as pytest_exit: + parse_file_containing_amounts_recipients(FILE_PATH) + assert error in capsys.readouterr().out -- GitLab