Skip to content

Commit

Permalink
Refactor CLI by adding add new click types for fees, amounts, address…
Browse files Browse the repository at this point in the history
…es and bytes32 (#15718)
  • Loading branch information
jack60612 committed Jun 24, 2024
1 parent a83f59b commit a447734
Show file tree
Hide file tree
Showing 21 changed files with 1,438 additions and 1,307 deletions.
193 changes: 193 additions & 0 deletions chia/_tests/cmds/test_click_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
from __future__ import annotations

from decimal import Decimal
from pathlib import Path
from typing import Any, cast

import pytest
from click import BadParameter, Context

from chia.cmds.param_types import (
AddressParamType,
AmountParamType,
Bytes32ParamType,
CliAddress,
CliAmount,
TransactionFeeParamType,
Uint64ParamType,
)
from chia.cmds.units import units
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.bech32m import encode_puzzle_hash
from chia.util.ints import uint64
from chia.wallet.util.address_type import AddressType

"""
This File tests all of the custom click param types.
Click automatically handles all cases where it is None and all cases where it is in some sort of Iterable.
"""

burn_ph = bytes32.from_hexstr("0x000000000000000000000000000000000000000000000000000000000000dead")
burn_address = encode_puzzle_hash(burn_ph, "xch")
burn_address_txch = encode_puzzle_hash(burn_ph, "txch")
burn_nft_addr = encode_puzzle_hash(burn_ph, "did:chia:")
burn_bad_prefix = encode_puzzle_hash(burn_ph, "badprefix")
overflow_ammt = 18446744073709551616 # max coin + 1
overflow_decimal_str = "18446744.073709551616"
overflow_decimal = Decimal(overflow_decimal_str)


class FakeContext:
obj: dict[Any, Any] = {}

def __init__(self, obj: dict[Any, Any]):
self.obj = obj


def test_click_tx_fee_type() -> None:
# Test uint64 (only used as default)
# assert TransactionFeeParamType().convert(uint64(10000), None, None) == uint64(10000)

# TODO: Test MOJO Logic When Implemented

# Test Decimal / XCH
assert TransactionFeeParamType().convert("0.5", None, None) == uint64(Decimal("0.5") * units["chia"])
assert TransactionFeeParamType().convert("0.000000000001", None, None) == uint64(1)
assert TransactionFeeParamType().convert("0", None, None) == uint64(0)
# Test Decimal Failures
with pytest.raises(BadParameter):
TransactionFeeParamType().convert("test", None, None)
with pytest.raises(BadParameter):
TransactionFeeParamType().convert("0.6", None, None)
with pytest.raises(BadParameter):
TransactionFeeParamType().convert("0.0000000000001", None, None) # 0.1 mojos
with pytest.raises(BadParameter):
TransactionFeeParamType().convert("-0.6", None, None)
with pytest.raises(BadParameter):
TransactionFeeParamType().convert(overflow_decimal_str, None, None)
# Test Type Failures
with pytest.raises(BadParameter):
TransactionFeeParamType().convert(float(0.01), None, None)


def test_click_amount_type() -> None:
decimal_cli_amount = CliAmount(mojos=False, amount=Decimal("5.25"))
large_decimal_amount = CliAmount(mojos=False, amount=overflow_decimal)
mojos_cli_amount = CliAmount(mojos=True, amount=uint64(100000))
one_mojo_cli_amount = CliAmount(mojos=False, amount=Decimal("0.000000000001"))
# Test CliAmount (Generally is not used)
assert AmountParamType().convert(decimal_cli_amount, None, None) == decimal_cli_amount

# Test uint64 (only usable as default)
# assert AmountParamType().convert(uint64(100000), None, None) == mojos_cli_amount

# TODO: Test MOJO Logic When Implemented

# Test Decimal / XCH (we don't test overflow because we don't know the conversion ratio yet)
assert AmountParamType().convert("5.25", None, None) == decimal_cli_amount
assert AmountParamType().convert(overflow_decimal_str, None, None) == large_decimal_amount
assert AmountParamType().convert("0.000000000001", None, None) == one_mojo_cli_amount
# Test Decimal Failures
with pytest.raises(BadParameter):
AmountParamType().convert("test", None, None)
with pytest.raises(BadParameter):
AmountParamType().convert("0.0000000000001", None, None) # 0.1 mojos
with pytest.raises(BadParameter):
AmountParamType().convert("-999999", None, None)
with pytest.raises(BadParameter):
AmountParamType().convert("-0.6", None, None)
# Test Type Failures
with pytest.raises(BadParameter):
AmountParamType().convert(0.01, None, None)

# Test CliAmount Class
assert decimal_cli_amount.convert_amount(units["chia"]) == uint64(Decimal("5.25") * units["chia"])
assert mojos_cli_amount.convert_amount(units["chia"]) == uint64(100000)
assert one_mojo_cli_amount.convert_amount(units["chia"]) == uint64(1)
with pytest.raises(ValueError): # incorrect arg
CliAmount(mojos=True, amount=Decimal("5.25")).convert_amount(units["chia"])
with pytest.raises(ValueError): # incorrect arg
CliAmount(mojos=False, amount=uint64(100000)).convert_amount(units["chia"])
with pytest.raises(ValueError): # overflow
large_decimal_amount.convert_amount(units["chia"])


def test_click_address_type() -> None:
context = cast(Context, FakeContext(obj={"expected_prefix": "xch"})) # this makes us not have to use a config file
std_cli_address = CliAddress(burn_ph, burn_address, AddressType.XCH)
nft_cli_address = CliAddress(burn_ph, burn_nft_addr, AddressType.DID)
# Test CliAddress (Generally is not used)
# assert AddressParamType().convert(std_cli_address, None, context) == std_cli_address

# test address parsing
assert AddressParamType().convert(burn_address, None, context) == std_cli_address
assert AddressParamType().convert(burn_nft_addr, None, context) == nft_cli_address

# check address type validation
assert std_cli_address.validate_address_type(AddressType.XCH) == burn_address
assert std_cli_address.validate_address_type_get_ph(AddressType.XCH) == burn_ph
assert nft_cli_address.validate_address_type(AddressType.DID) == burn_nft_addr
assert nft_cli_address.validate_address_type_get_ph(AddressType.DID) == burn_ph
# check error handling
with pytest.raises(BadParameter):
AddressParamType().convert("test", None, None)
with pytest.raises(AttributeError): # attribute error because the context does not have a real error handler
AddressParamType().convert(burn_address_txch, None, context)
with pytest.raises(BadParameter):
AddressParamType().convert(burn_bad_prefix, None, None)
# Test Type Failures
with pytest.raises(BadParameter):
AddressParamType().convert(float(0.01), None, None)

# check class error handling
with pytest.raises(ValueError):
std_cli_address.validate_address_type_get_ph(AddressType.DID)
with pytest.raises(ValueError):
std_cli_address.validate_address_type(AddressType.DID)


def test_click_address_type_config(root_path_populated_with_config: Path) -> None:
# set a root path in context.
context = cast(Context, FakeContext(obj={"root_path": root_path_populated_with_config}))
# run test that should pass
assert AddressParamType().convert(burn_address, None, context) == CliAddress(burn_ph, burn_address, AddressType.XCH)
assert context.obj["expected_prefix"] == "xch" # validate that the prefix was set correctly
# use txch address
with pytest.raises(AttributeError): # attribute error because the context does not have a real error handler
AddressParamType().convert(burn_address_txch, None, context)


def test_click_bytes32_type() -> None:
# Test bytes32 (Generally it is not used)
# assert Bytes32ParamType().convert(burn_ph, None, None) == burn_ph

# test bytes32 parsing
assert Bytes32ParamType().convert("0x" + burn_ph.hex(), None, None) == burn_ph
# check error handling
with pytest.raises(BadParameter):
Bytes32ParamType().convert("test", None, None)
# Test Type Failures
with pytest.raises(BadParameter):
Bytes32ParamType().convert(float(0.01), None, None)


def test_click_uint64_type() -> None:
# Test uint64 (only used as default)
assert Uint64ParamType().convert(uint64(10000), None, None) == uint64(10000)

# Test Uint64 Parsing
assert Uint64ParamType().convert("5", None, None) == uint64(5)
assert Uint64ParamType().convert("10000000000000", None, None) == uint64(10000000000000)
assert Uint64ParamType().convert("0", None, None) == uint64(0)
# Test Failures
with pytest.raises(BadParameter):
Uint64ParamType().convert("test", None, None)
with pytest.raises(BadParameter):
Uint64ParamType().convert("0.1", None, None)
with pytest.raises(BadParameter):
Uint64ParamType().convert("-1", None, None)
with pytest.raises(BadParameter):
Uint64ParamType().convert(str(overflow_ammt), None, None)
# Test Type Failures
with pytest.raises(BadParameter):
Uint64ParamType().convert(float(0.01), None, None)
18 changes: 10 additions & 8 deletions chia/_tests/cmds/test_tx_config_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
from click.testing import CliRunner

from chia.cmds.cmds_util import CMDCoinSelectionConfigLoader, CMDTXConfigLoader, coin_selection_args, tx_config_args
from chia.cmds.param_types import CliAmount
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.config import create_default_chia_config, load_config


def test_coin_selection_args() -> None:
@click.command()
@coin_selection_args
def test_cmd(
min_coin_amount: Optional[str],
max_coin_amount: Optional[str],
coins_to_exclude: Sequence[str],
amounts_to_exclude: Sequence[str],
min_coin_amount: CliAmount,
max_coin_amount: CliAmount,
coins_to_exclude: Sequence[bytes32],
amounts_to_exclude: Sequence[CliAmount],
) -> None:
print(
CMDCoinSelectionConfigLoader(
Expand Down Expand Up @@ -95,10 +97,10 @@ def test_tx_config_args() -> None:
@click.command()
@tx_config_args
def test_cmd(
min_coin_amount: Optional[str],
max_coin_amount: Optional[str],
coins_to_exclude: Sequence[str],
amounts_to_exclude: Sequence[str],
min_coin_amount: CliAmount,
max_coin_amount: CliAmount,
coins_to_exclude: Sequence[bytes32],
amounts_to_exclude: Sequence[CliAmount],
reuse: Optional[bool],
) -> None:
print(
Expand Down
5 changes: 4 additions & 1 deletion chia/_tests/cmds/wallet/test_coins.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ def test_coins_get_info(capsys: object, get_test_cli_clients: Tuple[TestRpcClien
(
1,
CoinSelectionConfig(
min_coin_amount=uint64(0), max_coin_amount=uint64(0), excluded_coin_amounts=[], excluded_coin_ids=[]
min_coin_amount=uint64(0),
max_coin_amount=DEFAULT_TX_CONFIG.max_coin_amount,
excluded_coin_amounts=[],
excluded_coin_ids=[],
),
)
],
Expand Down
28 changes: 14 additions & 14 deletions chia/_tests/cmds/wallet/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ async def cat_spend(
"wallet",
"send",
"-a1",
"-m1",
"-m0.5",
"-o",
WALLET_ID_ARG,
f"-e{bytes32_hexstr}",
Expand Down Expand Up @@ -410,7 +410,7 @@ async def cat_spend(
excluded_coin_ids=[bytes32([98] * 32)],
reuse_puzhash=True,
),
1000000000000,
500000000000,
["0x6262626262626262626262626262626262626262626262626262626262626262"],
[{"decorator": "CLAWBACK", "clawback_timelock": 60}],
)
Expand All @@ -427,7 +427,7 @@ async def cat_spend(
),
1000,
"xch1qvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvps82kgr2",
1000000000000,
500000000000,
["0x6262626262626262626262626262626262626262626262626262626262626262"],
None,
None,
Expand Down Expand Up @@ -494,14 +494,14 @@ async def spend_clawback_coins(
"clawback",
WALLET_ID_ARG,
FINGERPRINT_ARG,
"-m1",
"-m0.5",
"--tx_ids",
f"{tx_ids[0].hex()},{tx_ids[1].hex()}, {tx_ids[2].hex()}",
]
run_cli_command_and_assert(capsys, root_dir, command_args, ["transaction_ids", str(r_tx_ids_hex)])
# these are various things that should be in the output
expected_calls: logType = {
"spend_clawback_coins": [(tx_ids, 1000000000000, False)],
"spend_clawback_coins": [(tx_ids, 500000000000, False)],
}
test_rpc_clients.wallet_rpc_client.check_log(expected_calls)

Expand Down Expand Up @@ -644,7 +644,7 @@ def test_make_offer_bad_filename(
FINGERPRINT_ARG,
f"-p{str(tmp_path)}",
"--reuse",
"-m1",
"-m0.5",
"--offer",
"1:10",
"--offer",
Expand All @@ -664,7 +664,7 @@ def test_make_offer_bad_filename(
FINGERPRINT_ARG,
f"-p{str(test_file)}",
"--reuse",
"-m1",
"-m0.5",
"--offer",
"1:10",
"--offer",
Expand Down Expand Up @@ -735,7 +735,7 @@ def to_bech32(self) -> str:
FINGERPRINT_ARG,
f"-p{str(tmp_path / 'test.offer')}",
"--reuse",
"-m1",
"-m0.5",
"--offer",
"1:10",
"--offer",
Expand All @@ -749,7 +749,7 @@ def to_bech32(self) -> str:
"OFFERING:\n - 10 XCH (10000000000000 mojos)\n - 100 test3 (100000 mojos)",
"REQUESTING:\n - 10 test2 (10000 mojos)\n"
" - 1 nft1qgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyql4ft (1 mojos)",
"Including Fees: 1 XCH, 1000000000000 mojos",
"Including Fees: 0.5 XCH, 500000000000 mojos",
"Created offer with ID 0202020202020202020202020202020202020202020202020202020202020202",
]
run_cli_command_and_assert(capsys, root_dir, command_args[:-4], ["without --override"])
Expand Down Expand Up @@ -802,7 +802,7 @@ def to_bech32(self) -> str:
}
},
None,
1000000000000,
500000000000,
False,
)
],
Expand Down Expand Up @@ -961,7 +961,7 @@ async def take_offer(
]

with importlib_resources.as_file(test_offer_file_path) as test_offer_file_name:
command_args = ["wallet", "take_offer", os.fspath(test_offer_file_name), FINGERPRINT_ARG, "-m1", "--reuse"]
command_args = ["wallet", "take_offer", os.fspath(test_offer_file_name), FINGERPRINT_ARG, "-m0.5", "--reuse"]
run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)

expected_calls: logType = {
Expand All @@ -970,7 +970,7 @@ async def take_offer(
(cat2,),
(bytes32.from_hexstr("accce8e1c71b56624f2ecaeff5af57eac41365080449904d0717bd333c04806d"),),
],
"take_offer": [(Offer.from_bech32(test_offer_file_bech32), DEFAULT_TX_CONFIG, None, 1000000000000)],
"take_offer": [(Offer.from_bech32(test_offer_file_bech32), DEFAULT_TX_CONFIG, None, 500000000000)],
}
test_rpc_clients.wallet_rpc_client.check_log(expected_calls)

Expand Down Expand Up @@ -1006,7 +1006,7 @@ async def cancel_offer(

inst_rpc_client = CancelOfferRpcClient() # pylint: disable=no-value-for-parameter
test_rpc_clients.wallet_rpc_client = inst_rpc_client
command_args = ["wallet", "cancel_offer", FINGERPRINT_ARG, "-m1", "--id", test_offer_id]
command_args = ["wallet", "cancel_offer", FINGERPRINT_ARG, "-m0.5", "--id", test_offer_id]
# these are various things that should be in the output
cat1 = bytes32.from_hexstr("fd6a341ed39c05c31157d5bfea395a0e142398ced24deea1e82f836d7ec2909c")
cat2 = bytes32.from_hexstr("dc59bcd60ce5fc9c93a5d3b11875486b03efb53a53da61e453f5cf61a7746860")
Expand All @@ -1022,7 +1022,7 @@ async def cancel_offer(
run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)
expected_calls: logType = {
"get_offer": [(test_offer_id_bytes, True)],
"cancel_offer": [(test_offer_id_bytes, DEFAULT_TX_CONFIG, 1000000000000, True)],
"cancel_offer": [(test_offer_id_bytes, DEFAULT_TX_CONFIG, 500000000000, True)],
"cat_asset_id_to_name": [
(cat1,),
(cat2,),
Expand Down
Loading

0 comments on commit a447734

Please sign in to comment.