Skip to content
Snippets Groups Projects
Commit 3300af22 authored by Moul's avatar Moul
Browse files

[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
parent 3805f52b
No related branches found
No related tags found
1 merge request!202#134: Add ability to pass a file containing the tx recipients and amounts
Pipeline #15116 failed
...@@ -13,8 +13,12 @@ ...@@ -13,8 +13,12 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>. # along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
import math import math
import shlex
import sys
from re import compile, search from re import compile, search
from typing import List
import click import click
from duniterpy.api.bma.tx import process from duniterpy.api.bma.tx import process
...@@ -60,7 +64,7 @@ NBR_ISSUERS = 1 ...@@ -60,7 +64,7 @@ NBR_ISSUERS = 1
type=click.FloatRange(MINIMAL_ABSOLUTE_TX_AMOUNT), type=click.FloatRange(MINIMAL_ABSOLUTE_TX_AMOUNT),
help=f"Quantitative amount(s):\n-a <amount>\nMinimum amount is {MINIMAL_ABSOLUTE_TX_AMOUNT}.", help=f"Quantitative amount(s):\n-a <amount>\nMinimum amount is {MINIMAL_ABSOLUTE_TX_AMOUNT}.",
cls=cli_tools.MutuallyExclusiveOption, cls=cli_tools.MutuallyExclusiveOption,
mutually_exclusive=["amountsud", "allsources"], mutually_exclusive=["amountsud", "allsources", "file_path"],
) )
@click.option( @click.option(
"amountsud", "amountsud",
...@@ -70,25 +74,34 @@ NBR_ISSUERS = 1 ...@@ -70,25 +74,34 @@ NBR_ISSUERS = 1
type=click.FloatRange(MINIMAL_RELATIVE_TX_AMOUNT), type=click.FloatRange(MINIMAL_RELATIVE_TX_AMOUNT),
help=f"Relative amount(s):\n-d <amount_UD>\nMinimum amount is {MINIMAL_RELATIVE_TX_AMOUNT}", help=f"Relative amount(s):\n-d <amount_UD>\nMinimum amount is {MINIMAL_RELATIVE_TX_AMOUNT}",
cls=cli_tools.MutuallyExclusiveOption, cls=cli_tools.MutuallyExclusiveOption,
mutually_exclusive=["amounts", "allsources"], mutually_exclusive=["amounts", "allsources", "file_path"],
) )
@click.option( @click.option(
"--allSources", "--allSources",
is_flag=True, is_flag=True,
help="Send all sources to one recipient", help="Send all sources to one recipient",
cls=cli_tools.MutuallyExclusiveOption, cls=cli_tools.MutuallyExclusiveOption,
mutually_exclusive=["amounts", "amountsud"], mutually_exclusive=["amounts", "amountsud", "file_path"],
) )
@click.option( @click.option(
"recipients", "recipients",
"--recipient", "--recipient",
"-r", "-r",
multiple=True, multiple=True,
required=True,
help="Pubkey(s)’ recipients + optional checksum:\n-r <pubkey>[:checksum]\n\ help="Pubkey(s)’ recipients + optional checksum:\n-r <pubkey>[:checksum]\n\
Sending to many recipients is possible:\n\ Sending to many recipients is possible:\n\
* With one amount, all will receive the amount\n\ * With one amount, all will receive the amount\n\
* With many amounts (one per recipient)", * 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("--comment", "-c", default="", help="Comment")
@click.option( @click.option(
...@@ -103,15 +116,18 @@ def send_transaction( ...@@ -103,15 +116,18 @@ def send_transaction(
amountsud, amountsud,
allsources, allsources,
recipients, recipients,
file_path,
comment, comment,
outputbackchange, outputbackchange,
yes, yes,
): ):
""" if file_path:
Main function tx_amounts, recipients = parse_file_containing_amounts_recipients(file_path)
""" else:
if not (amounts or amountsud or allsources): if not (amounts or amountsud or allsources):
tools.message_exit("Error: amount, amountUD or allSources is not set.") 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: if allsources and len(recipients) > 1:
tools.message_exit( tools.message_exit(
"Error: the --allSources option can only be used with one recipient." "Error: the --allSources option can only be used with one recipient."
...@@ -167,6 +183,53 @@ def send_transaction( ...@@ -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): def transaction_amount(amounts, UDs_amounts, outputAddresses):
""" """
Check that the number of passed amounts(UD) and recipients are the same Check that the number of passed amounts(UD) and recipients are the same
......
...@@ -135,16 +135,16 @@ def test_transaction_amount_errors( ...@@ -135,16 +135,16 @@ def test_transaction_amount_errors(
def test_tx_passed_amount_cli(): def test_tx_passed_amount_cli():
"""One option""" """One option"""
result = CliRunner().invoke(cli, ["tx", "--amount", "1"]) result = CliRunner().invoke(cli, ["tx", "--amount", "1"])
assert "Error: Missing option" in result.output assert "Error: A recipient should be passed\n" in result.output
assert result.exit_code == 2 assert result.exit_code == 1
result = CliRunner().invoke(cli, ["tx", "--amountUD", "1"]) result = CliRunner().invoke(cli, ["tx", "--amountUD", "1"])
assert "Error: Missing option" in result.output assert "Error: A recipient should be passed\n" in result.output
assert result.exit_code == 2 assert result.exit_code == 1
result = CliRunner().invoke(cli, ["tx", "--allSources"]) result = CliRunner().invoke(cli, ["tx", "--allSources"])
assert "Error: Missing option" in result.output assert "Error: A recipient should be passed\n" in result.output
assert result.exit_code == 2 assert result.exit_code == 1
"""Multiple options""" """Multiple options"""
result = CliRunner().invoke(cli, ["tx", "--amount", 1, "--amountUD", 1]) result = CliRunner().invoke(cli, ["tx", "--amount", 1, "--amountUD", 1])
......
# 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment