Repository: Chia-Network/chia-blockchain
Pull request: Refactor CLI by adding add new click types for fees, amounts, addresses and bytes32
<!-- Merging Requirements:
- Please give your PR a title that is release-note friendly
- In order to be merged, you must add the most appropriate category Label (Added, Changed, Fixed) to your PR
-->
<!-- Explain why this is an improvement (Does this add missing functionality, improve performance, or reduce complexity?) -->
### Purpose:

- Adds new `TransactionFeeParamType` for automatic fee conversion 
- Adds new `CliAmount` class and `AmountParamType` for easy conversion between units.
- Adds new `CliAddress` class and `AddressParamType` for easy bech32m address conversion and wallet type tracking 
- Adds new `Bytes32ParamType` for automatic str to bytes32 conversion.
- Lastly this pr does more standardization too.

Merge requirements:

- [x] #15704 Needs to be Merged
- [x] PR needs to be rebased on main after merge
- [x] ~~Some light testing of the --mojos argument~~ (Moved to future PR)
- [x] Add Unit Tests
- [x] Cli tests added by #15886 Merged

<!-- Does this PR introduce a breaking change? -->
### Current Behavior:
No Breaking Changes, just lots of backend changes.


### New Behavior:
N/A


<!-- As we aim for complete code coverage, please include details regarding unit, and regression tests -->
### Testing Notes:
This PR has unit tests for the new types, and actual cli tests are coming in #15718 


<!-- Attach any visual examples, or supporting evidence (attach any .gif/video/console output below) -->


Problem statement:
[Bug] Combining coins with fee
### What happened?

The user wants to combine coins using

1. Run CLI combine
`chia wallet coins combine -f 1269832460 --number-of-coins 100 --id 25 --fee 0.0000001`

2. Response:
```
Combining 7 coins.
Would you like to Continue? (y/n): y
```

3. Response:
```
Exception from 'wallet' {'error': 'Transaction for 100000 is greater than spendable balance of 0. There may be other transactions pending or our minimum coin amount is too 
high.', 'success': False, 'traceback': 'Traceback (most recent call last):\n  File "chia\\rpc\\util.py", line 21, in inner\n  File "chia\\rpc\\wallet_rpc_api.py", line 1072, in send_transaction_multi\n  File "chia\\rpc\\util.py", line 82, in rpc_endpoint\n  File "chia\\rpc\\wallet_rpc_api.py", line 1616, in cat_spend\n  File "chia\\wallet\\cat_wallet\\cat_wallet.py", line 818, in generate_signed_transaction\n  File "chia\\wallet\\cat_wallet\\cat_wallet.py", line 717, in generate_unsigned_spendbundle\n  File "chia\\wallet\\cat_wallet\\cat_wallet.py", line 581, in create_tandem_xch_tx\n  File "chia\\wallet\\wallet.py", line 256, in select_coins\n  File "chia\\wallet\\coin_selection.py", line 57, in select_coins\nValueError: Transaction for 100000 is greater than spendable balance of 0. There may be other transactions pending or our minimum coin amount is too high.\n'}
```

However, standard wallet does have enough spendable balance:
```
Chia Wallet:
   -Total Balance:         5.345662453651 xch (5345662453651 mojo)
   -Pending Total Balance: 5.345662453651 xch (5345662453651 mojo)
   -Spendable:             2.311613686008 xch (2311613686008 mojo)
   -Type:                  STANDARD_WALLET
   -Wallet ID:             1
```

**Running the ``coins split`` command including same fee size works without problems**

### Version

2.1.1

### What platform are you using?

Windows

### What ui mode are you using?

CLI

### Relevant log output

_No response_

Hints from issue discussion:

Commit message: Update chia/cmds/param_types.py

Co-authored-by: Kyle Altendorf <sda@fstab.net>

Patch to review:
diff --git a/chia/cmds/coin_funcs.py b/chia/cmds/coin_funcs.py
index ace88131f206..01f7f82716f2 100644
--- a/chia/cmds/coin_funcs.py
+++ b/chia/cmds/coin_funcs.py
@@ -1,11 +1,10 @@
 from __future__ import annotations
 
 import sys
-from decimal import Decimal
 from typing import Dict, List, Optional, Sequence, Tuple, Union
 
 from chia.cmds.cmds_util import cli_confirm, get_wallet_client
-from chia.cmds.units import units
+from chia.cmds.param_types import CliAmount
 from chia.cmds.wallet_funcs import get_mojo_per_unit, get_wallet_type, print_balance
 from chia.types.blockchain_format.coin import Coin
 from chia.types.blockchain_format.sized_bytes import bytes32
@@ -22,9 +21,9 @@ async def async_list(
     wallet_rpc_port: Optional[int],
     fingerprint: Optional[int],
     wallet_id: int,
-    max_coin_amount: Decimal,
-    min_coin_amount: Decimal,
-    excluded_amounts: Sequence[int],
+    max_coin_amount: CliAmount,
+    min_coin_amount: CliAmount,
+    excluded_amounts: Sequence[CliAmount],
     excluded_coin_ids: Sequence[str],
     show_unconfirmed: bool,
     paginate: Optional[bool],
@@ -42,10 +41,10 @@ async def async_list(
         if not await wallet_client.get_synced():
             print("Wallet not synced. Please wait.")
             return
-        final_min_coin_amount: uint64 = uint64(int(min_coin_amount * mojo_per_unit))
-        final_max_coin_amount: uint64 = uint64(int(max_coin_amount * mojo_per_unit))
+        final_min_coin_amount: uint64 = min_coin_amount.convert_amount(mojo_per_unit)
+        final_max_coin_amount: uint64 = max_coin_amount.convert_amount(mojo_per_unit)
         final_excluded_amounts: List[uint64] = [
-            uint64(int(Decimal(amount) * mojo_per_unit)) for amount in excluded_amounts
+            cli_amount.convert_amount(mojo_per_unit) for cli_amount in excluded_amounts
         ]
         conf_coins, unconfirmed_removals, unconfirmed_additions = await wallet_client.get_spendable_coins(
             wallet_id=wallet_id,
@@ -107,9 +106,9 @@ def print_coins(
         print("Press q to quit, or c to continue")
         while True:
             entered_key = sys.stdin.read(1)
-            if entered_key == "q":
+            if entered_key.lower() == "q":
                 return None
-            elif entered_key == "c":
+            elif entered_key.lower() == "c":
                 break
 
 
@@ -118,18 +117,17 @@ async def async_combine(
     wallet_rpc_port: Optional[int],
     fingerprint: Optional[int],
     wallet_id: int,
-    fee: Decimal,
-    max_coin_amount: Decimal,
-    min_coin_amount: Decimal,
-    excluded_amounts: Sequence[str],
+    fee: uint64,
+    max_coin_amount: CliAmount,
+    min_coin_amount: CliAmount,
+    excluded_amounts: Sequence[CliAmount],
     number_of_coins: int,
-    target_coin_amount: Decimal,
+    target_coin_amount: CliAmount,
     target_coin_ids_str: Sequence[str],
     largest_first: bool,
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config):
         target_coin_ids: List[bytes32] = [bytes32.from_hexstr(coin_id) for coin_id in target_coin_ids_str]
-        final_fee = uint64(int(fee * units["chia"]))
         if number_of_coins > 500:
             raise ValueError(f"{number_of_coins} coins is greater then the maximum limit of 500 coins.")
         try:
@@ -142,15 +140,15 @@ async def async_combine(
             print("Wallet not synced. Please wait.")
             return
         is_xch: bool = wallet_type == WalletType.STANDARD_WALLET  # this lets us know if we are directly combining Chia
-        final_max_amount = uint64(int(max_coin_amount * mojo_per_unit)) if not target_coin_ids else uint64(0)
-        final_min_coin_amount: uint64 = uint64(int(min_coin_amount * mojo_per_unit))
+        final_max_amount = max_coin_amount.convert_amount(mojo_per_unit) if not target_coin_ids else uint64(0)
+        final_min_coin_amount: uint64 = min_coin_amount.convert_amount(mojo_per_unit)
         final_excluded_amounts: List[uint64] = [
-            uint64(int(Decimal(amount) * mojo_per_unit)) for amount in excluded_amounts
+            cli_amount.convert_amount(mojo_per_unit) for cli_amount in excluded_amounts
         ]
-        final_target_coin_amount = uint64(int(target_coin_amount * mojo_per_unit))
+        final_target_coin_amount = target_coin_amount.convert_amount(mojo_per_unit)
         if final_target_coin_amount != 0:  # if we have a set target, just use standard coin selection.
             removals: List[Coin] = await wallet_client.select_coins(
-                amount=(final_target_coin_amount + final_fee) if is_xch else final_target_coin_amount,
+                amount=(final_target_coin_amount + fee) if is_xch else final_target_coin_amount,
                 wallet_id=wallet_id,
                 max_coin_amount=final_max_amount,
                 min_coin_amount=final_min_coin_amount,
@@ -182,14 +180,12 @@ async def async_combine(
         print(f"Combining {len(removals)} coins.")
         cli_confirm("Would you like to Continue? (y/n): ")
         total_amount: uint128 = uint128(sum(coin.amount for coin in removals))
-        if is_xch and total_amount - final_fee <= 0:
+        if is_xch and total_amount - fee <= 0:
             print("Total amount is less than 0 after fee, exiting.")
             return
         target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, False))
-        additions = [{"amount": (total_amount - final_fee) if is_xch else total_amount, "puzzle_hash": target_ph}]
-        transaction: TransactionRecord = await wallet_client.send_transaction_multi(
-            wallet_id, additions, removals, final_fee
-        )
+        additions = [{"amount": (total_amount - fee) if is_xch else total_amount, "puzzle_hash": target_ph}]
+        transaction: TransactionRecord = await wallet_client.send_transaction_multi(wallet_id, additions, removals, fee)
         tx_id = transaction.name.hex()
         print(f"Transaction sent: {tx_id}")
         print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}")
@@ -200,13 +196,12 @@ async def async_split(
     wallet_rpc_port: Optional[int],
     fingerprint: Optional[int],
     wallet_id: int,
-    fee: Decimal,
+    fee: uint64,
     number_of_coins: int,
-    amount_per_coin: Decimal,
+    amount_per_coin: CliAmount,
     target_coin_id_str: str,
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config):
-        final_fee = uint64(int(fee * units["chia"]))
         target_coin_id: bytes32 = bytes32.from_hexstr(target_coin_id_str)
         if number_of_coins > 500:
             print(f"{number_of_coins} coins is greater then the maximum limit of 500 coins.")
@@ -221,10 +216,10 @@ async def async_split(
             print("Wallet not synced. Please wait.")
             return
         is_xch: bool = wallet_type == WalletType.STANDARD_WALLET  # this lets us know if we are directly spitting Chia
-        final_amount_per_coin = uint64(int(amount_per_coin * mojo_per_unit))
+        final_amount_per_coin = amount_per_coin.convert_amount(mojo_per_unit)
         total_amount = final_amount_per_coin * number_of_coins
         if is_xch:
-            total_amount += final_fee
+            total_amount += fee
         # get full coin record from name, and validate information about it.
         removal_coin_record: CoinRecord = (await wallet_client.get_coin_records_by_names([target_coin_id]))[0]
         if removal_coin_record.coin.amount < total_amount:
@@ -240,7 +235,7 @@ async def async_split(
             target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, new_address=True))
             additions.append({"amount": final_amount_per_coin, "puzzle_hash": target_ph})
         transaction: TransactionRecord = await wallet_client.send_transaction_multi(
-            wallet_id, additions, [removal_coin_record.coin], final_fee
+            wallet_id, additions, [removal_coin_record.coin], fee
         )
         tx_id = transaction.name.hex()
         print(f"Transaction sent: {tx_id}")
@@ -249,8 +244,8 @@ async def async_split(
         spam_filter_after_n_txs = config.get("spam_filter_after_n_txs", 200)  # how many txs to wait before filtering
         if final_amount_per_coin < dust_threshold and wallet_type == WalletType.STANDARD_WALLET:
             print(
-                f"WARNING: The amount per coin: {amount_per_coin} is less than the dust threshold: "
-                f"{dust_threshold / mojo_per_unit}. Some or all of the Coins "
+                f"WARNING: The amount per coin: {amount_per_coin.amount} is less than the dust threshold: "
+                f"{dust_threshold / (1 if amount_per_coin.mojos else mojo_per_unit)}. Some or all of the Coins "
                 f"{'will' if number_of_coins > spam_filter_after_n_txs else 'may'} not show up in your wallet unless "
                 f"you decrease the dust limit to below {final_amount_per_coin} mojos or disable it by setting it to 0."
             )
diff --git a/chia/cmds/coins.py b/chia/cmds/coins.py
index b6cf316b3b13..ca0b76b673da 100644
--- a/chia/cmds/coins.py
+++ b/chia/cmds/coins.py
@@ -1,11 +1,13 @@
 from __future__ import annotations
 
 import asyncio
-from decimal import Decimal
 from typing import Optional, Sequence
 
 import click
 
+from chia.cmds.param_types import AMOUNT_TYPE, TRANSACTION_FEE, CliAmount
+from chia.util.ints import uint64
+
 
 @click.group("coins", help="Manage your wallets coins")
 @click.pass_context
@@ -27,14 +29,14 @@ def coins_cmd(ctx: click.Context) -> None:
 @click.option(
     "--min-amount",
     help="Ignore coins worth less then this much XCH or CAT units",
-    type=str,
-    default="0",
+    type=AMOUNT_TYPE,
+    default=uint64(0),
 )
 @click.option(
     "--max-amount",
     help="Ignore coins worth more then this much XCH or CAT units",
-    type=str,
-    default="0",
+    type=AMOUNT_TYPE,
+    default=uint64(0),
 )
 @click.option(
     "--exclude-coin",
@@ -46,6 +48,7 @@ def coins_cmd(ctx: click.Context) -> None:
     "--exclude-amount",
     "amounts_to_exclude",
     multiple=True,
+    type=AMOUNT_TYPE,
     help="Exclude any coins with this XCH or CAT amount from being included.",
 )
 @click.option(
@@ -60,10 +63,10 @@ def list_cmd(
     fingerprint: int,
     id: int,
     show_unconfirmed: bool,
-    min_amount: str,
-    max_amount: str,
+    min_amount: CliAmount,
+    max_amount: CliAmount,
     coins_to_exclude: Sequence[str],
-    amounts_to_exclude: Sequence[int],
+    amounts_to_exclude: Sequence[CliAmount],
     paginate: Optional[bool],
 ) -> None:
     from .coin_funcs import async_list
@@ -73,8 +76,8 @@ def list_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fingerprint=fingerprint,
             wallet_id=id,
-            max_coin_amount=Decimal(max_amount),
-            min_coin_amount=Decimal(min_amount),
+            max_coin_amount=max_amount,
+            min_coin_amount=min_amount,
             excluded_amounts=amounts_to_exclude,
             excluded_coin_ids=coins_to_exclude,
             show_unconfirmed=show_unconfirmed,
@@ -98,19 +101,20 @@ def list_cmd(
     "--target-amount",
     help="Select coins until this amount (in XCH or CAT) is reached. \
     Combine all selected coins into one coin, which will have a value of at least target-amount",
-    type=str,
-    default="0",
+    type=AMOUNT_TYPE,
+    default=uint64(0),
 )
 @click.option(
     "--min-amount",
     help="Ignore coins worth less then this much XCH or CAT units",
-    type=str,
-    default="0",
+    type=AMOUNT_TYPE,
+    default=uint64(0),
 )
 @click.option(
     "--exclude-amount",
     "amounts_to_exclude",
     multiple=True,
+    type=AMOUNT_TYPE,
     help="Exclude any coins with this XCH or CAT amount from being included.",
 )
 @click.option(
@@ -124,15 +128,15 @@ def list_cmd(
 @click.option(
     "--max-amount",
     help="Ignore coins worth more then this much XCH or CAT units",
-    type=str,
-    default="0",  # 0 means no limit
+    type=AMOUNT_TYPE,
+    default=uint64(0),  # 0 means no limit
 )
 @click.option(
     "-m",
     "--fee",
     help="Set the fees for the transaction, in XCH",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
     required=True,
 )
@@ -152,12 +156,12 @@ def combine_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
     id: int,
-    target_amount: str,
-    min_amount: str,
-    amounts_to_exclude: Sequence[str],
+    target_amount: CliAmount,
+    min_amount: CliAmount,
+    amounts_to_exclude: Sequence[CliAmount],
     number_of_coins: int,
-    max_amount: str,
-    fee: str,
+    max_amount: CliAmount,
+    fee: uint64,
     input_coins: Sequence[str],
     largest_first: bool,
 ) -> None:
@@ -168,12 +172,12 @@ def combine_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fingerprint=fingerprint,
             wallet_id=id,
-            fee=Decimal(fee),
-            max_coin_amount=Decimal(max_amount),
-            min_coin_amount=Decimal(min_amount),
+            fee=fee,
+            max_coin_amount=max_amount,
+            min_coin_amount=min_amount,
             excluded_amounts=amounts_to_exclude,
             number_of_coins=number_of_coins,
-            target_coin_amount=Decimal(target_amount),
+            target_coin_amount=target_amount,
             target_coin_ids_str=input_coins,
             largest_first=largest_first,
         )
@@ -201,8 +205,8 @@ def combine_cmd(
     "-m",
     "--fee",
     help="Set the fees for the transaction, in XCH",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
     required=True,
 )
@@ -210,7 +214,7 @@ def combine_cmd(
     "-a",
     "--amount-per-coin",
     help="The amount of each newly created coin, in XCH",
-    type=str,
+    type=AMOUNT_TYPE,
     required=True,
 )
 @click.option("-t", "--target-coin-id", type=str, required=True, help="The coin id of the coin we are splitting.")
@@ -219,8 +223,8 @@ def split_cmd(
     fingerprint: int,
     id: int,
     number_of_coins: int,
-    fee: str,
-    amount_per_coin: str,
+    fee: uint64,
+    amount_per_coin: CliAmount,
     target_coin_id: str,
 ) -> None:
     from .coin_funcs import async_split
@@ -230,9 +234,9 @@ def split_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fingerprint=fingerprint,
             wallet_id=id,
-            fee=Decimal(fee),
+            fee=fee,
             number_of_coins=number_of_coins,
-            amount_per_coin=Decimal(amount_per_coin),
+            amount_per_coin=amount_per_coin,
             target_coin_id_str=target_coin_id,
         )
     )
diff --git a/chia/cmds/data.py b/chia/cmds/data.py
index c673e8627360..264c309cb64b 100644
--- a/chia/cmds/data.py
+++ b/chia/cmds/data.py
@@ -7,11 +7,12 @@
 
 import click
 
+from chia.cmds.param_types import BYTES32_TYPE, TRANSACTION_FEE
 from chia.types.blockchain_format.sized_bytes import bytes32
+from chia.util.ints import uint64
 
 _T = TypeVar("_T")
 
-
 FC = TypeVar("FC", bound=Union[Callable[..., Any], click.Command])
 
 logger = logging.getLogger(__name__)
@@ -66,7 +67,7 @@ def create_data_store_id_option() -> Callable[[FC], FC]:
         "-store",
         "--id",
         help="The hexadecimal store id.",
-        type=str,
+        type=BYTES32_TYPE,
         required=True,
     )
 
@@ -98,19 +99,29 @@ def create_fee_option() -> Callable[[FC], FC]:
         "-m",
         "--fee",
         help="Set the fees for the transaction, in XCH",
-        type=str,
+        type=TRANSACTION_FEE,
         default=None,
         show_default=True,
         required=False,
     )
 
 
+def create_root_hash_option() -> Callable[[FC], FC]:
+    return click.option(
+        "-r",
+        "--root_hash",
+        help="The hexadecimal root hash",
+        type=BYTES32_TYPE,
+        required=False,
+    )
+
+
 @data_cmd.command("create_data_store", help="Create a new data store")
 @create_rpc_port_option()
 @create_fee_option()
 def create_data_store(
     data_rpc_port: int,
-    fee: Optional[str],
+    fee: Optional[uint64],
 ) -> None:
     from chia.cmds.data_funcs import create_data_store_cmd
 
@@ -120,12 +131,12 @@ def create_data_store(
 @data_cmd.command("get_value", help="Get the value for a given key and store")
 @create_data_store_id_option()
 @create_key_option()
-@click.option("-r", "--root_hash", help="The hexadecimal root hash", type=str, required=False)
+@create_root_hash_option()
 @create_rpc_port_option()
 def get_value(
-    id: str,
+    id: bytes32,
     key_string: str,
-    root_hash: Optional[str],
+    root_hash: Optional[bytes32],
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import get_value_cmd
@@ -139,10 +150,10 @@ def get_value(
 @create_rpc_port_option()
 @create_fee_option()
 def update_data_store(
-    id: str,
+    id: bytes32,
     changelist_string: str,
     data_rpc_port: int,
-    fee: str,
+    fee: Optional[uint64],
 ) -> None:
     from chia.cmds.data_funcs import update_data_store_cmd
 
@@ -151,11 +162,11 @@ def update_data_store(
 
 @data_cmd.command("get_keys", help="Get all keys for a given store")
 @create_data_store_id_option()
-@click.option("-r", "--root_hash", help="The hexadecimal root hash", type=str, required=False)
+@create_root_hash_option()
 @create_rpc_port_option()
 def get_keys(
-    id: str,
-    root_hash: Optional[str],
+    id: bytes32,
+    root_hash: Optional[bytes32],
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import get_keys_cmd
@@ -165,11 +176,11 @@ def get_keys(
 
 @data_cmd.command("get_keys_values", help="Get all keys and values for a given store")
 @create_data_store_id_option()
-@click.option("-r", "--root_hash", help="The hexadecimal root hash", type=str, required=False)
+@create_root_hash_option()
 @create_rpc_port_option()
 def get_keys_values(
-    id: str,
-    root_hash: Optional[str],
+    id: bytes32,
+    root_hash: Optional[bytes32],
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import get_keys_values_cmd
@@ -181,7 +192,7 @@ def get_keys_values(
 @create_data_store_id_option()
 @create_rpc_port_option()
 def get_root(
-    id: str,
+    id: bytes32,
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import get_root_cmd
@@ -201,7 +212,7 @@ def get_root(
 )
 @create_rpc_port_option()
 def subscribe(
-    id: str,
+    id: bytes32,
     urls: List[str],
     data_rpc_port: int,
 ) -> None:
@@ -215,7 +226,7 @@ def subscribe(
 @click.option("-u", "--url", "urls", help="Server urls to remove", type=str, multiple=True)
 @create_rpc_port_option()
 def remove_subscription(
-    id: str,
+    id: bytes32,
     urls: List[str],
     data_rpc_port: int,
 ) -> None:
@@ -228,7 +239,7 @@ def remove_subscription(
 @create_data_store_id_option()
 @create_rpc_port_option()
 def unsubscribe(
-    id: str,
+    id: bytes32,
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import unsubscribe_cmd
@@ -240,13 +251,13 @@ def unsubscribe(
     "get_kv_diff", help="Get the inserted and deleted keys and values between an initial and a final hash"
 )
 @create_data_store_id_option()
-@click.option("-hash_1", "--hash_1", help="Initial hash", type=str)
-@click.option("-hash_2", "--hash_2", help="Final hash", type=str)
+@click.option("-hash_1", "--hash_1", help="Initial hash", type=BYTES32_TYPE, required=True)
+@click.option("-hash_2", "--hash_2", help="Final hash", type=BYTES32_TYPE, required=True)
 @create_rpc_port_option()
 def get_kv_diff(
-    id: str,
-    hash_1: str,
-    hash_2: str,
+    id: bytes32,
+    hash_1: bytes32,
+    hash_2: bytes32,
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import get_kv_diff_cmd
@@ -258,7 +269,7 @@ def get_kv_diff(
 @create_data_store_id_option()
 @create_rpc_port_option()
 def get_root_history(
-    id: str,
+    id: bytes32,
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import get_root_history_cmd
@@ -297,7 +308,7 @@ def add_missing_files(ids: Optional[str], overwrite: bool, foldername: Optional[
 
 
 @data_cmd.command("add_mirror", help="Publish mirror urls on chain")
-@click.option("-i", "--id", help="Store id", type=str, required=True)
+@create_data_store_id_option()
 @click.option(
     "-a", "--amount", help="Amount to spend for this mirror, in mojos", type=int, default=0, show_default=True
 )
@@ -311,7 +322,7 @@ def add_missing_files(ids: Optional[str], overwrite: bool, foldername: Optional[
 )
 @create_fee_option()
 @create_rpc_port_option()
-def add_mirror(id: str, amount: int, urls: List[str], fee: Optional[str], data_rpc_port: int) -> None:
+def add_mirror(id: bytes32, amount: int, urls: List[str], fee: Optional[uint64], data_rpc_port: int) -> None:
     from chia.cmds.data_funcs import add_mirror_cmd
 
     run(
@@ -326,10 +337,10 @@ def add_mirror(id: str, amount: int, urls: List[str], fee: Optional[str], data_r
 
 
 @data_cmd.command("delete_mirror", help="Delete an owned mirror by its coin id")
-@click.option("-c", "--coin_id", help="Coin id", type=str, required=True)
+@click.option("-c", "--coin_id", help="Coin id", type=BYTES32_TYPE, required=True)
 @create_fee_option()
 @create_rpc_port_option()
-def delete_mirror(coin_id: str, fee: Optional[str], data_rpc_port: int) -> None:
+def delete_mirror(coin_id: bytes32, fee: Optional[uint64], data_rpc_port: int) -> None:
     from chia.cmds.data_funcs import delete_mirror_cmd
 
     run(
@@ -342,9 +353,9 @@ def delete_mirror(coin_id: str, fee: Optional[str], data_rpc_port: int) -> None:
 
 
 @data_cmd.command("get_mirrors", help="Get a list of all mirrors for a given store")
-@click.option("-i", "--id", help="Store id", type=str, required=True)
+@create_data_store_id_option()
 @create_rpc_port_option()
-def get_mirrors(id: str, data_rpc_port: int) -> None:
+def get_mirrors(id: bytes32, data_rpc_port: int) -> None:
     from chia.cmds.data_funcs import get_mirrors_cmd
 
     run(
@@ -383,7 +394,7 @@ def get_owned_stores(data_rpc_port: int) -> None:
 @create_data_store_id_option()
 @create_rpc_port_option()
 def get_sync_status(
-    id: str,
+    id: bytes32,
     data_rpc_port: int,
 ) -> None:
     from chia.cmds.data_funcs import get_sync_status_cmd
diff --git a/chia/cmds/data_funcs.py b/chia/cmds/data_funcs.py
index 1be504f1f8d7..7967ab2db467 100644
--- a/chia/cmds/data_funcs.py
+++ b/chia/cmds/data_funcs.py
@@ -1,12 +1,10 @@
 from __future__ import annotations
 
 import json
-from decimal import Decimal
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
 from chia.cmds.cmds_util import get_any_service_client
-from chia.cmds.units import units
 from chia.rpc.data_layer_rpc_client import DataLayerRpcClient
 from chia.types.blockchain_format.sized_bytes import bytes32
 from chia.util.byte_types import hexstr_to_bytes
@@ -14,122 +12,105 @@
 from chia.util.ints import uint64
 
 
-async def create_data_store_cmd(rpc_port: Optional[int], fee: Optional[str]) -> None:
-    final_fee = None if fee is None else uint64(int(Decimal(fee) * units["chia"]))
+async def create_data_store_cmd(rpc_port: Optional[int], fee: Optional[uint64]) -> None:
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.create_data_store(fee=final_fee)
+        res = await client.create_data_store(fee=fee)
         print(res)
 
 
-async def get_value_cmd(rpc_port: Optional[int], store_id: str, key: str, root_hash: Optional[str]) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
+async def get_value_cmd(rpc_port: Optional[int], store_id: bytes32, key: str, root_hash: Optional[bytes32]) -> None:
     key_bytes = hexstr_to_bytes(key)
-    root_hash_bytes = None if root_hash is None else bytes32.from_hexstr(root_hash)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_value(store_id=store_id_bytes, key=key_bytes, root_hash=root_hash_bytes)
+        res = await client.get_value(store_id=store_id, key=key_bytes, root_hash=root_hash)
         print(res)
 
 
 async def update_data_store_cmd(
     rpc_port: Optional[int],
-    store_id: str,
+    store_id: bytes32,
     changelist: List[Dict[str, str]],
-    fee: Optional[str],
+    fee: Optional[uint64],
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
-    final_fee = None if fee is None else uint64(int(Decimal(fee) * units["chia"]))
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.update_data_store(store_id=store_id_bytes, changelist=changelist, fee=final_fee)
+        res = await client.update_data_store(store_id=store_id, changelist=changelist, fee=fee)
         print(res)
 
 
 async def get_keys_cmd(
     rpc_port: Optional[int],
-    store_id: str,
-    root_hash: Optional[str],
+    store_id: bytes32,
+    root_hash: Optional[bytes32],
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
-    root_hash_bytes = None if root_hash is None else bytes32.from_hexstr(root_hash)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_keys(store_id=store_id_bytes, root_hash=root_hash_bytes)
+        res = await client.get_keys(store_id=store_id, root_hash=root_hash)
         print(res)
 
 
 async def get_keys_values_cmd(
     rpc_port: Optional[int],
-    store_id: str,
-    root_hash: Optional[str],
+    store_id: bytes32,
+    root_hash: Optional[bytes32],
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
-    root_hash_bytes = None if root_hash is None else bytes32.from_hexstr(root_hash)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_keys_values(store_id=store_id_bytes, root_hash=root_hash_bytes)
+        res = await client.get_keys_values(store_id=store_id, root_hash=root_hash)
         print(res)
 
 
 async def get_root_cmd(
     rpc_port: Optional[int],
-    store_id: str,
+    store_id: bytes32,
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_root(store_id=store_id_bytes)
+        res = await client.get_root(store_id=store_id)
         print(res)
 
 
 async def subscribe_cmd(
     rpc_port: Optional[int],
-    store_id: str,
+    store_id: bytes32,
     urls: List[str],
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.subscribe(store_id=store_id_bytes, urls=urls)
+        res = await client.subscribe(store_id=store_id, urls=urls)
         print(res)
 
 
 async def unsubscribe_cmd(
     rpc_port: Optional[int],
-    store_id: str,
+    store_id: bytes32,
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.unsubscribe(store_id=store_id_bytes)
+        res = await client.unsubscribe(store_id=store_id)
         print(res)
 
 
 async def remove_subscriptions_cmd(
     rpc_port: Optional[int],
-    store_id: str,
+    store_id: bytes32,
     urls: List[str],
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.remove_subscriptions(store_id=store_id_bytes, urls=urls)
+        res = await client.remove_subscriptions(store_id=store_id, urls=urls)
         print(res)
 
 
 async def get_kv_diff_cmd(
     rpc_port: Optional[int],
-    store_id: str,
-    hash_1: str,
-    hash_2: str,
+    store_id: bytes32,
+    hash_1: bytes32,
+    hash_2: bytes32,
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
-    hash_1_bytes = bytes32.from_hexstr(hash_1)
-    hash_2_bytes = bytes32.from_hexstr(hash_2)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_kv_diff(store_id=store_id_bytes, hash_1=hash_1_bytes, hash_2=hash_2_bytes)
+        res = await client.get_kv_diff(store_id=store_id, hash_1=hash_1, hash_2=hash_2)
         print(res)
 
 
 async def get_root_history_cmd(
     rpc_port: Optional[int],
-    store_id: str,
+    store_id: bytes32,
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_root_history(store_id=store_id_bytes)
+        res = await client.get_root_history(store_id=store_id)
         print(res)
 
 
@@ -146,35 +127,30 @@ async def add_missing_files_cmd(
 
 
 async def add_mirror_cmd(
-    rpc_port: Optional[int], store_id: str, urls: List[str], amount: int, fee: Optional[str]
+    rpc_port: Optional[int], store_id: bytes32, urls: List[str], amount: int, fee: Optional[uint64]
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
-    final_fee = None if fee is None else uint64(int(Decimal(fee) * units["chia"]))
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
         res = await client.add_mirror(
-            store_id=store_id_bytes,
+            store_id=store_id,
             urls=urls,
             amount=amount,
-            fee=final_fee,
+            fee=fee,
         )
         print(res)
 
 
-async def delete_mirror_cmd(rpc_port: Optional[int], coin_id: str, fee: Optional[str]) -> None:
-    coin_id_bytes = bytes32.from_hexstr(coin_id)
-    final_fee = None if fee is None else uint64(int(Decimal(fee) * units["chia"]))
+async def delete_mirror_cmd(rpc_port: Optional[int], coin_id: bytes32, fee: Optional[uint64]) -> None:
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
         res = await client.delete_mirror(
-            coin_id=coin_id_bytes,
-            fee=final_fee,
+            coin_id=coin_id,
+            fee=fee,
         )
         print(res)
 
 
-async def get_mirrors_cmd(rpc_port: Optional[int], store_id: str) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
+async def get_mirrors_cmd(rpc_port: Optional[int], store_id: bytes32) -> None:
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_mirrors(store_id=store_id_bytes)
+        res = await client.get_mirrors(store_id=store_id)
         print(res)
 
 
@@ -192,11 +168,10 @@ async def get_owned_stores_cmd(rpc_port: Optional[int]) -> None:
 
 async def get_sync_status_cmd(
     rpc_port: Optional[int],
-    store_id: str,
+    store_id: bytes32,
 ) -> None:
-    store_id_bytes = bytes32.from_hexstr(store_id)
     async with get_any_service_client(DataLayerRpcClient, rpc_port) as (client, _):
-        res = await client.get_sync_status(store_id=store_id_bytes)
+        res = await client.get_sync_status(store_id=store_id)
         print(res)
 
 
diff --git a/chia/cmds/param_types.py b/chia/cmds/param_types.py
new file mode 100644
index 000000000000..f97e1112ae81
--- /dev/null
+++ b/chia/cmds/param_types.py
@@ -0,0 +1,166 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from decimal import Decimal, InvalidOperation
+from typing import Any, Optional, Union
+
+import click
+
+from chia.cmds.units import units
+from chia.types.blockchain_format.sized_bytes import bytes32
+from chia.util.bech32m import bech32_decode, decode_puzzle_hash
+from chia.util.config import selected_network_address_prefix
+from chia.util.ints import uint64
+from chia.wallet.util.address_type import AddressType
+
+
+class TransactionFeeParamType(click.ParamType):
+    """
+    A Click parameter type for transaction fees, which can be specified in XCH or mojos.
+    """
+
+    name: str = "uint64"  # output type
+    value_limit: Decimal = Decimal(0.5)
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> uint64:
+        if isinstance(value, uint64):  # required by click
+            return value
+        mojos = False  # TODO: Add unit logic
+        if mojos:
+            try:
+                return uint64(int(value))
+            except ValueError:
+                self.fail("Fee must be positive integer value in mojos", param, ctx)
+        try:
+            d_value = Decimal(value)
+            if 0 > d_value:
+                self.fail("Fee can not be negative", param, ctx)
+            if self.value_limit != Decimal(0) and d_value > self.value_limit:
+                self.fail(f"Fee must be in the range 0 to {self.value_limit}", param, ctx)
+            return uint64(d_value * units["chia"])
+        except InvalidOperation:  # won't ever be value error because of the fee limit check
+            self.fail("Fee must be decimal dotted value in XCH (e.g. 0.00005)", param, ctx)
+
+
+@dataclass(frozen=True)
+class CliAmount:
+    """
+    A dataclass for TX / wallet amounts for both XCH and CAT, and of course mojos.
+    """
+
+    mojos: bool
+    amount: Union[uint64, Decimal]  # uint64 if mojos, Decimal if not
+
+    def convert_amount(self, mojo_per_unit: int) -> uint64:
+        if self.mojos:
+            if not isinstance(self.amount, uint64):
+                raise ValueError("Amount must be a uint64 if mojos flag is set.")
+            return self.amount
+        if not isinstance(self.amount, Decimal):
+            raise ValueError("Amount must be a Decimal if mojos flag is not set.")
+        return uint64(int(self.amount * mojo_per_unit))
+
+
+class AmountParamType(click.ParamType):
+    """
+    A Click parameter type for TX / wallet amounts for both XCH and CAT, and of course mojos.
+    """
+
+    name: str = "CliAmount"  # output type
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> CliAmount:
+        if isinstance(value, CliAmount):  # required by click
+            return value
+        if isinstance(value, uint64):
+            return CliAmount(mojos=True, amount=value)
+        mojos = False  # TODO: Add unit logic
+        if mojos:
+            try:
+                return CliAmount(mojos=True, amount=uint64(int(value)))
+            except ValueError:
+                self.fail("Amount must be positive integer value in mojos", param, ctx)
+        try:
+            if Decimal(value) <= 0:
+                self.fail("Amount must be greater than 0", param, ctx)
+            return CliAmount(mojos=False, amount=Decimal(value))
+        except InvalidOperation:
+            self.fail("Amount must be a decimal dotted value in XCH / CAT (e.g. 0.00005)", param, ctx)
+
+
+@dataclass(frozen=True)
+class CliAddress:
+    """
+    A dataclass for the cli, with the address type and puzzle hash.
+    """
+
+    puzzle_hash: bytes32
+    original_address: str
+    address_type: AddressType
+
+    def validate_address_type(self, address_type: AddressType) -> str:
+        if self.address_type is not address_type:
+            raise ValueError(f"Address must be of type {address_type}")
+        return self.original_address
+
+    def validate_address_type_get_ph(self, address_type: AddressType) -> bytes32:
+        if self.address_type is not address_type:
+            raise ValueError(f"Address must be of type {address_type}")
+        return self.puzzle_hash
+
+
+class AddressParamType(click.ParamType):
+    """
+    A Click parameter type for bech32m encoded addresses, it gives a class with the address type and puzzle hash.
+    """
+
+    name: str = "CliAddress"  # output type
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> CliAddress:
+        if isinstance(value, CliAddress):  # required by click
+            return value
+        try:
+            hrp, b32data = bech32_decode(value)
+            if hrp in ["xch", "txch"]:  # I hate having to load the config here
+                expected_prefix = ctx.obj.get("expected_prefix") if ctx else None
+                if expected_prefix is None:
+                    from chia.util.config import load_config
+                    from chia.util.default_root import DEFAULT_ROOT_PATH
+
+                    root_path = ctx.obj["root_path"] if ctx is not None else DEFAULT_ROOT_PATH
+                    config = load_config(root_path, "config.yaml")
+                    expected_prefix = selected_network_address_prefix(config)
+                    if ctx is not None:
+                        ctx.obj["expected_prefix"] = expected_prefix
+                # now that we have the expected prefix, we can validate the address
+                if hrp == expected_prefix:
+                    addr_type = AddressType.XCH
+                else:
+                    self.fail(f"Unexpected Address Prefix: {hrp}, are you sure its for the right network?", param, ctx)
+            else:
+                addr_type = AddressType(hrp)
+            return CliAddress(puzzle_hash=decode_puzzle_hash(value), address_type=addr_type, original_address=value)
+        except ValueError:
+            self.fail("Address must be a valid bech32m address", param, ctx)
+
+
+class Bytes32ParamType(click.ParamType):
+    """
+    A Click parameter type for bytes32 hex strings, with or without the 0x prefix.
+    """
+
+    name: str = "bytes32"  # output type
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> bytes32:
+        if isinstance(value, bytes32):  # required by click
+            return value
+        try:
+            return bytes32.from_hexstr(value)
+        except ValueError:
+            self.fail("Value must be a valid bytes32 hex string like a coin id or puzzle hash", param, ctx)
+
+
+# These are what we use in click decorators
+TRANSACTION_FEE = TransactionFeeParamType()
+AMOUNT_TYPE = AmountParamType()
+ADDRESS_TYPE = AddressParamType()
+BYTES32_TYPE = Bytes32ParamType()
diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py
index ae330067a2ed..56207504226e 100644
--- a/chia/cmds/plotnft.py
+++ b/chia/cmds/plotnft.py
@@ -1,21 +1,11 @@
 from __future__ import annotations
 
-from decimal import Decimal
 from typing import Optional
 
 import click
 
-MAX_CMDLINE_FEE = Decimal(0.5)
-
-
-def validate_fee(ctx: click.Context, param: click.Parameter, value: str) -> str:
-    try:
-        fee = Decimal(value)
-    except ValueError:
-        raise click.BadParameter("Fee must be decimal dotted value in XCH (e.g. 0.00005)")
-    if fee < 0 or fee > MAX_CMDLINE_FEE:
-        raise click.BadParameter(f"Fee must be in the range 0 to {MAX_CMDLINE_FEE}")
-    return value
+from chia.cmds.param_types import ADDRESS_TYPE, TRANSACTION_FEE, CliAddress
+from chia.util.ints import uint64
 
 
 @click.group("plotnft", help="Manage your plot NFTs")
@@ -60,11 +50,10 @@ def get_login_link_cmd(launcher_id: str) -> None:
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH. Fee is used TWICE: once to create the singleton, once for init.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
     required=True,
-    callback=validate_fee,
 )
 @click.option(
     "-wp",
@@ -78,7 +67,7 @@ def create_cmd(
     fingerprint: int,
     pool_url: str,
     state: str,
-    fee: str,
+    fee: uint64,
     dont_prompt: bool,
 ) -> None:
     import asyncio
@@ -92,7 +81,7 @@ def create_cmd(
         print("  pool_url argument (-u) is required for pool starting state")
         return
     valid_initial_states = {"pool": "FARMING_TO_POOL", "local": "SELF_POOLING"}
-    asyncio.run(create(wallet_rpc_port, fingerprint, pool_url, valid_initial_states[state], Decimal(fee), dont_prompt))
+    asyncio.run(create(wallet_rpc_port, fingerprint, pool_url, valid_initial_states[state], fee, dont_prompt))
 
 
 @plotnft_cmd.command("join", help="Join a plot NFT to a Pool")
@@ -104,11 +93,10 @@ def create_cmd(
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH. Fee is used TWICE: once to leave pool, once to join.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
     required=True,
-    callback=validate_fee,
 )
 @click.option(
     "-wp",
@@ -118,7 +106,7 @@ def create_cmd(
     default=None,
 )
 def join_cmd(
-    wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: int, pool_url: str, dont_prompt: bool
+    wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: uint64, pool_url: str, dont_prompt: bool
 ) -> None:
     import asyncio
 
@@ -129,7 +117,7 @@ def join_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fingerprint=fingerprint,
             pool_url=pool_url,
-            fee=Decimal(fee),
+            fee=fee,
             wallet_id=id,
             prompt=dont_prompt,
         )
@@ -144,11 +132,10 @@ def join_cmd(
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH. Fee is charged TWICE.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
     required=True,
-    callback=validate_fee,
 )
 @click.option(
     "-wp",
@@ -157,7 +144,7 @@ def join_cmd(
     type=int,
     default=None,
 )
-def self_pool_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: int, dont_prompt: bool) -> None:
+def self_pool_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: uint64, dont_prompt: bool) -> None:
     import asyncio
 
     from .plotnft_funcs import self_pool
@@ -166,7 +153,7 @@ def self_pool_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee
         self_pool(
             wallet_rpc_port=wallet_rpc_port,
             fingerprint=fingerprint,
-            fee=Decimal(fee),
+            fee=fee,
             wallet_id=id,
             prompt=dont_prompt,
         )
@@ -198,11 +185,10 @@ def inspect(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> None:
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
     required=True,
-    callback=validate_fee,
 )
 @click.option(
     "-wp",
@@ -211,7 +197,7 @@ def inspect(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> None:
     type=int,
     default=None,
 )
-def claim(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: int) -> None:
+def claim(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: uint64) -> None:
     import asyncio
 
     from .plotnft_funcs import claim_cmd
@@ -220,7 +206,7 @@ def claim(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: int) -
         claim_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fingerprint=fingerprint,
-            fee=Decimal(fee),
+            fee=fee,
             wallet_id=id,
         )
     )
@@ -231,8 +217,8 @@ def claim(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: int) -
     help="Change the payout instructions for a pool. To get the launcher id, use plotnft show.",
 )
 @click.option("-l", "--launcher_id", help="Launcher ID of the plotnft", type=str, required=True)
-@click.option("-a", "--address", help="New address for payout instructions", type=str, required=True)
-def change_payout_instructions_cmd(launcher_id: str, address: str) -> None:
+@click.option("-a", "--address", help="New address for payout instructions", type=ADDRESS_TYPE, required=True)
+def change_payout_instructions_cmd(launcher_id: str, address: CliAddress) -> None:
     import asyncio
 
     from .plotnft_funcs import change_payout_instructions
diff --git a/chia/cmds/plotnft_funcs.py b/chia/cmds/plotnft_funcs.py
index 2c5f22b44a5e..785bd0a01855 100644
--- a/chia/cmds/plotnft_funcs.py
+++ b/chia/cmds/plotnft_funcs.py
@@ -5,7 +5,6 @@
 import json
 import time
 from dataclasses import replace
-from decimal import Decimal
 from pprint import pprint
 from typing import Any, Awaitable, Callable, Dict, List, Optional
 
@@ -17,7 +16,7 @@
     transaction_status_msg,
     transaction_submitted_msg,
 )
-from chia.cmds.units import units
+from chia.cmds.param_types import CliAddress
 from chia.cmds.wallet_funcs import print_balance, wallet_coin_unit
 from chia.pools.pool_config import PoolWalletConfig, load_pool_config, update_pool_config
 from chia.pools.pool_wallet_info import PoolSingletonState, PoolWalletInfo
@@ -27,13 +26,14 @@
 from chia.server.server import ssl_context_for_root
 from chia.ssl.create_ssl import get_mozilla_ca_crt
 from chia.types.blockchain_format.sized_bytes import bytes32
-from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
+from chia.util.bech32m import encode_puzzle_hash
 from chia.util.byte_types import hexstr_to_bytes
 from chia.util.config import load_config
 from chia.util.default_root import DEFAULT_ROOT_PATH
 from chia.util.errors import CliRpcConnectionError
 from chia.util.ints import uint32, uint64
 from chia.wallet.transaction_record import TransactionRecord
+from chia.wallet.util.address_type import AddressType
 from chia.wallet.util.wallet_types import WalletType
 
 
@@ -61,10 +61,9 @@ async def create_pool_args(pool_url: str) -> Dict[str, Any]:
 
 
 async def create(
-    wallet_rpc_port: Optional[int], fingerprint: int, pool_url: Optional[str], state: str, fee: Decimal, prompt: bool
+    wallet_rpc_port: Optional[int], fingerprint: int, pool_url: Optional[str], state: str, fee: uint64, prompt: bool
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, _):
-        fee_mojos = uint64(int(fee * units["chia"]))
         target_puzzle_hash: Optional[bytes32]
         # Could use initial_pool_state_from_dict to simplify
         if state == "SELF_POOLING":
@@ -101,7 +100,7 @@ async def create(
                     "localhost:5000",
                     "new",
                     state,
-                    fee_mojos,
+                    fee,
                 )
                 start = time.time()
                 while time.time() - start < 10:
@@ -287,13 +286,12 @@ async def join_pool(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
     pool_url: str,
-    fee: Decimal,
+    fee: uint64,
     wallet_id: int,
     prompt: bool,
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config):
         enforce_https = config["full_node"]["selected_network"] == "mainnet"
-        fee_mojos = uint64(int(fee * units["chia"]))
 
         if enforce_https and not pool_url.startswith("https://"):
             print(f"Pool URLs must be HTTPS on mainnet {pool_url}. Aborting.")
@@ -327,19 +325,18 @@ async def join_pool(
             hexstr_to_bytes(json_dict["target_puzzle_hash"]),
             pool_url,
             json_dict["relative_lock_height"],
-            fee_mojos,
+            fee,
         )
 
         await submit_tx_with_confirmation(msg, prompt, func, wallet_client, fingerprint, wallet_id)
 
 
 async def self_pool(
-    *, wallet_rpc_port: Optional[int], fingerprint: int, fee: Decimal, wallet_id: int, prompt: bool
+    *, wallet_rpc_port: Optional[int], fingerprint: int, fee: uint64, wallet_id: int, prompt: bool
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, _):
-        fee_mojos = uint64(int(fee * units["chia"]))
         msg = f"Will start self-farming with Plot NFT on wallet id {wallet_id} fingerprint {fingerprint}."
-        func = functools.partial(wallet_client.pw_self_pool, wallet_id, fee_mojos)
+        func = functools.partial(wallet_client.pw_self_pool, wallet_id, fee)
         await submit_tx_with_confirmation(msg, prompt, func, wallet_client, fingerprint, wallet_id)
 
 
@@ -356,26 +353,21 @@ async def inspect_cmd(wallet_rpc_port: Optional[int], fingerprint: int, wallet_i
         )
 
 
-async def claim_cmd(*, wallet_rpc_port: Optional[int], fingerprint: int, fee: Decimal, wallet_id: int) -> None:
+async def claim_cmd(*, wallet_rpc_port: Optional[int], fingerprint: int, fee: uint64, wallet_id: int) -> None:
     async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, _):
-        fee_mojos = uint64(int(fee * units["chia"]))
         msg = f"\nWill claim rewards for wallet ID: {wallet_id}."
         func = functools.partial(
             wallet_client.pw_absorb_rewards,
             wallet_id,
-            fee_mojos,
+            fee,
         )
         await submit_tx_with_confirmation(msg, False, func, wallet_client, fingerprint, wallet_id)
 
 
-async def change_payout_instructions(launcher_id: str, address: str) -> None:
+async def change_payout_instructions(launcher_id: str, address: CliAddress) -> None:
     new_pool_configs: List[PoolWalletConfig] = []
     id_found = False
-    try:
-        puzzle_hash = decode_puzzle_hash(address)
-    except ValueError:
-        print(f"Invalid Address: {address}")
-        return
+    puzzle_hash = address.validate_address_type_get_ph(AddressType.XCH)
 
     old_configs: List[PoolWalletConfig] = load_pool_config(DEFAULT_ROOT_PATH)
     for pool_config in old_configs:
diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py
index 92f84abd75ba..0e5ecd404688 100644
--- a/chia/cmds/wallet.py
+++ b/chia/cmds/wallet.py
@@ -1,14 +1,15 @@
 from __future__ import annotations
 
 import asyncio
-from decimal import Decimal
 from typing import List, Optional, Sequence
 
 import click
 
 from chia.cmds.check_wallet_db import help_text as check_help_text
 from chia.cmds.coins import coins_cmd
-from chia.cmds.plotnft import validate_fee
+from chia.cmds.param_types import ADDRESS_TYPE, AMOUNT_TYPE, BYTES32_TYPE, TRANSACTION_FEE, CliAddress, CliAmount
+from chia.types.blockchain_format.sized_bytes import bytes32
+from chia.util.ints import uint32, uint64
 from chia.wallet.transaction_sorting import SortKey
 from chia.wallet.util.address_type import AddressType
 from chia.wallet.util.wallet_types import WalletType
@@ -140,18 +141,19 @@ def get_transactions_cmd(
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-i", "--id", help="Id of the wallet to use", type=int, default=1, show_default=True, required=True)
-@click.option("-a", "--amount", help="How much chia to send, in XCH", type=str, required=True)
+@click.option("-a", "--amount", help="How much chia to send, in XCH", type=AMOUNT_TYPE, required=True)
 @click.option("-e", "--memo", help="Additional memo for the transaction", type=str, default=None)
 @click.option(
     "-m",
     "--fee",
     help="Set the fees for the transaction, in XCH",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
     required=True,
 )
-@click.option("-t", "--address", help="Address to send the XCH", type=str, required=True)
+# TODO: Fix RPC as this should take a puzzle_hash not an address.
+@click.option("-t", "--address", help="Address to send the XCH", type=ADDRESS_TYPE, required=True)
 @click.option(
     "-o", "--override", help="Submits transaction without checking for unusual values", is_flag=True, default=False
 )
@@ -159,17 +161,17 @@ def get_transactions_cmd(
     "-ma",
     "--min-coin-amount",
     help="Ignore coins worth less then this much XCH or CAT units",
-    type=str,
+    type=AMOUNT_TYPE,
     required=False,
-    default="0",
+    default=uint64(0),
 )
 @click.option(
     "-l",
     "--max-coin-amount",
     help="Ignore coins worth more then this much XCH or CAT units",
-    type=str,
+    type=AMOUNT_TYPE,
     required=False,
-    default="0",
+    default=uint64(0),
 )
 @click.option(
     "--exclude-coin",
@@ -194,13 +196,13 @@ def send_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
     id: int,
-    amount: str,
+    amount: CliAmount,
     memo: Optional[str],
-    fee: str,
-    address: str,
+    fee: uint64,
+    address: CliAddress,
     override: bool,
-    min_coin_amount: str,
-    max_coin_amount: str,
+    min_coin_amount: CliAmount,
+    max_coin_amount: CliAmount,
     coins_to_exclude: Sequence[str],
     reuse: bool,
     clawback_time: int,
@@ -212,13 +214,13 @@ def send_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fp=fingerprint,
             wallet_id=id,
-            amount=Decimal(amount),
+            amount=amount,
             memo=memo,
-            fee=Decimal(fee),
+            fee=fee,
             address=address,
             override=override,
-            min_coin_amount=Decimal(min_coin_amount),
-            max_coin_amount=Decimal(max_coin_amount),
+            min_coin_amount=min_coin_amount,
+            max_coin_amount=max_coin_amount,
             excluded_coin_ids=coins_to_exclude,
             reuse_puzhash=True if reuse else None,
             clawback_time_lock=clawback_time,
@@ -298,7 +300,12 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id: int, fingerprint: int, n
     required=True,
 )
 @click.option(
-    "-m", "--fee", help="A fee to add to the offer when it gets taken, in XCH", default="0", show_default=True
+    "-m",
+    "--fee",
+    help="A fee to add to the offer when it gets taken, in XCH",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
+    show_default=True,
 )
 @click.option(
     "--force",
@@ -307,14 +314,12 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id: int, fingerprint: int, n
     default=False,
 )
 def clawback(
-    wallet_rpc_port: Optional[int], id: int, fingerprint: int, tx_ids: str, fee: str, force: bool
+    wallet_rpc_port: Optional[int], id: int, fingerprint: int, tx_ids: str, fee: uint64, force: bool
 ) -> None:  # pragma: no cover
     from .wallet_funcs import spend_clawback
 
     asyncio.run(
-        spend_clawback(
-            wallet_rpc_port=wallet_rpc_port, fp=fingerprint, fee=Decimal(fee), tx_ids_str=tx_ids, force=force
-        )
+        spend_clawback(wallet_rpc_port=wallet_rpc_port, fp=fingerprint, fee=fee, tx_ids_str=tx_ids, force=force)
     )
 
 
@@ -358,9 +363,12 @@ def get_derivation_index_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-a", "--address", help="The address you want to use for signing", type=str, required=True)
+# TODO: Change RPC's to use the puzzle hash instead of address
+@click.option("-a", "--address", help="The address you want to use for signing", type=ADDRESS_TYPE, required=True)
 @click.option("-m", "--hex_message", help="The hex message you want sign", type=str, required=True)
-def address_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, address: str, hex_message: str) -> None:
+def address_sign_message(
+    wallet_rpc_port: Optional[int], fingerprint: int, address: CliAddress, hex_message: str
+) -> None:
     from .wallet_funcs import sign_message
 
     asyncio.run(
@@ -406,6 +414,7 @@ def update_derivation_index_cmd(wallet_rpc_port: Optional[int], fingerprint: int
     "-id",
     "--asset-id",
     help="The Asset ID of the coin you wish to add/rename (the treehash of the TAIL program)",
+    type=BYTES32_TYPE,
     required=True,
 )
 @click.option(
@@ -420,7 +429,7 @@ def update_derivation_index_cmd(wallet_rpc_port: Optional[int], fingerprint: int
     default=None,
     help="The wallet fingerprint you wish to add the token to",
 )
-def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str, fingerprint: int) -> None:
+def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: bytes32, token_name: str, fingerprint: int) -> None:
     from .wallet_funcs import add_token
 
     asyncio.run(add_token(wallet_rpc_port, fingerprint, asset_id, token_name))
@@ -451,7 +460,12 @@ def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str
 )
 @click.option("-p", "--filepath", help="The path to write the generated offer file to", required=True)
 @click.option(
-    "-m", "--fee", help="A fee to add to the offer when it gets taken, in XCH", default="0", show_default=True
+    "-m",
+    "--fee",
+    help="A fee to add to the offer when it gets taken, in XCH",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
+    show_default=True,
 )
 @click.option(
     "--reuse",
@@ -465,7 +479,7 @@ def make_offer_cmd(
     offer: Sequence[str],
     request: Sequence[str],
     filepath: str,
-    fee: str,
+    fee: uint64,
     reuse: bool,
 ) -> None:
     from .wallet_funcs import make_offer
@@ -474,7 +488,7 @@ def make_offer_cmd(
         make_offer(
             wallet_rpc_port=wallet_rpc_port,
             fp=fingerprint,
-            d_fee=Decimal(fee),
+            fee=fee,
             offers=offer,
             requests=request,
             filepath=filepath,
@@ -494,7 +508,7 @@ def make_offer_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-id", "--id", help="The ID of the offer that you wish to examine")
+@click.option("-id", "--id", help="The ID of the offer that you wish to examine", type=BYTES32_TYPE)
 @click.option("-p", "--filepath", help="The path to rewrite the offer file to (must be used in conjunction with --id)")
 @click.option("-em", "--exclude-my-offers", help="Exclude your own offers from the output", is_flag=True)
 @click.option("-et", "--exclude-taken-offers", help="Exclude offers that you've accepted from the output", is_flag=True)
@@ -506,7 +520,7 @@ def make_offer_cmd(
 def get_offers_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    id: Optional[str],
+    id: Optional[bytes32],
     filepath: Optional[str],
     exclude_my_offers: bool,
     exclude_taken_offers: bool,
@@ -543,8 +557,14 @@ def get_offers_cmd(
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-e", "--examine-only", help="Print the summary of the offer file but do not take it", is_flag=True)
 @click.option(
-    "-m", "--fee", help="The fee to use when pushing the completed offer, in XCH", default="0", show_default=True
+    "-m",
+    "--fee",
+    help="The fee to use when pushing the completed offer, in XCH",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
+    show_default=True,
 )
+# TODO: Reuse is not used
 @click.option(
     "--reuse",
     help="Reuse existing address for the offer.",
@@ -556,12 +576,12 @@ def take_offer_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
     examine_only: bool,
-    fee: str,
+    fee: uint64,
     reuse: bool,
 ) -> None:
     from .wallet_funcs import take_offer
 
-    asyncio.run(take_offer(wallet_rpc_port, fingerprint, Decimal(fee), path_or_hex, examine_only))  # reuse is not used
+    asyncio.run(take_offer(wallet_rpc_port, fingerprint, fee, path_or_hex, examine_only))
 
 
 @wallet_cmd.command("cancel_offer", help="Cancel an existing offer")
@@ -573,15 +593,22 @@ def take_offer_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-id", "--id", help="The offer ID that you wish to cancel", required=True)
+@click.option("-id", "--id", help="The offer ID that you wish to cancel", required=True, type=BYTES32_TYPE)
 @click.option("--insecure", help="Don't make an on-chain transaction, simply mark the offer as cancelled", is_flag=True)
 @click.option(
-    "-m", "--fee", help="The fee to use when cancelling the offer securely, in XCH", default="0", show_default=True
+    "-m",
+    "--fee",
+    help="The fee to use when cancelling the offer securely, in XCH",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
+    show_default=True,
 )
-def cancel_offer_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: str, insecure: bool, fee: str) -> None:
+def cancel_offer_cmd(
+    wallet_rpc_port: Optional[int], fingerprint: int, id: bytes32, insecure: bool, fee: uint64
+) -> None:
     from .wallet_funcs import cancel_offer
 
-    asyncio.run(cancel_offer(wallet_rpc_port, fingerprint, Decimal(fee), id, not insecure))
+    asyncio.run(cancel_offer(wallet_rpc_port, fingerprint, fee, id, not insecure))
 
 
 @wallet_cmd.command("check", short_help="Check wallet DB integrity", help=check_help_text)
@@ -625,17 +652,16 @@ def did_cmd() -> None:
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
-    callback=validate_fee,
 )
 def did_create_wallet_cmd(
-    wallet_rpc_port: Optional[int], fingerprint: int, name: Optional[str], amount: int, fee: str
+    wallet_rpc_port: Optional[int], fingerprint: int, name: Optional[str], amount: int, fee: uint64
 ) -> None:
     from .wallet_funcs import create_did_wallet
 
-    asyncio.run(create_did_wallet(wallet_rpc_port, fingerprint, Decimal(fee), name, amount))
+    asyncio.run(create_did_wallet(wallet_rpc_port, fingerprint, fee, name, amount))
 
 
 @did_cmd.command("sign_message", help="Sign a message by a DID")
@@ -647,9 +673,9 @@ def did_create_wallet_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-i", "--did_id", help="DID ID you want to use for signing", type=str, required=True)
+@click.option("-i", "--did_id", help="DID ID you want to use for signing", type=ADDRESS_TYPE, required=True)
 @click.option("-m", "--hex_message", help="The hex message you want to sign", type=str, required=True)
-def did_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, did_id: str, hex_message: str) -> None:
+def did_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, did_id: CliAddress, hex_message: str) -> None:
     from .wallet_funcs import sign_message
 
     asyncio.run(
@@ -853,7 +879,8 @@ def did_message_spend_cmd(
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-i", "--id", help="Id of the DID wallet to use", type=int, required=True)
-@click.option("-ta", "--target-address", help="Target recipient wallet address", type=str, required=True)
+# TODO: Change RPC to use puzzlehash instead of address
+@click.option("-ta", "--target-address", help="Target recipient wallet address", type=ADDRESS_TYPE, required=True)
 @click.option(
     "-rr", "--reset_recovery", help="If you want to reset the recovery DID settings.", is_flag=True, default=False
 )
@@ -861,10 +888,9 @@ def did_message_spend_cmd(
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
-    callback=validate_fee,
 )
 @click.option(
     "--reuse",
@@ -876,9 +902,9 @@ def did_transfer_did(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
     id: int,
-    target_address: str,
+    target_address: CliAddress,
     reset_recovery: bool,
-    fee: int,
+    fee: uint64,
     reuse: bool,
 ) -> None:
     from .wallet_funcs import transfer_did
@@ -904,10 +930,11 @@ def nft_cmd() -> None:
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-di", "--did-id", help="DID Id to use", type=str)
+# TODO: Change RPC to use puzzlehash instead of address
+@click.option("-di", "--did-id", help="DID Id to use", type=ADDRESS_TYPE)
 @click.option("-n", "--name", help="Set the NFT wallet name", type=str)
 def nft_wallet_create_cmd(
-    wallet_rpc_port: Optional[int], fingerprint: int, did_id: Optional[str], name: Optional[str]
+    wallet_rpc_port: Optional[int], fingerprint: int, did_id: Optional[CliAddress], name: Optional[str]
 ) -> None:
     from .wallet_funcs import create_nft_wallet
 
@@ -923,9 +950,9 @@ def nft_wallet_create_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-i", "--nft_id", help="NFT ID you want to use for signing", type=str, required=True)
+@click.option("-i", "--nft_id", help="NFT ID you want to use for signing", type=ADDRESS_TYPE, required=True)
 @click.option("-m", "--hex_message", help="The hex message you want to sign", type=str, required=True)
-def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: str, hex_message: str) -> None:
+def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: CliAddress, hex_message: str) -> None:
     from .wallet_funcs import sign_message
 
     asyncio.run(
@@ -949,8 +976,8 @@ def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: s
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-i", "--id", help="Id of the NFT wallet to use", type=int, required=True)
-@click.option("-ra", "--royalty-address", help="Royalty address", type=str)
-@click.option("-ta", "--target-address", help="Target address", type=str)
+@click.option("-ra", "--royalty-address", help="Royalty address", type=ADDRESS_TYPE)
+@click.option("-ta", "--target-address", help="Target address", type=ADDRESS_TYPE)
 @click.option("--no-did-ownership", help="Disable DID ownership support", is_flag=True, default=False)
 @click.option("-nh", "--hash", help="NFT content hash", type=str, required=True)
 @click.option("-u", "--uris", help="Comma separated list of URIs", type=str, required=True)
@@ -964,10 +991,9 @@ def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: s
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
-    callback=validate_fee,
 )
 @click.option(
     "-rp",
@@ -987,8 +1013,8 @@ def nft_mint_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
     id: int,
-    royalty_address: Optional[str],
-    target_address: Optional[str],
+    royalty_address: Optional[CliAddress],
+    target_address: Optional[CliAddress],
     no_did_ownership: bool,
     hash: str,
     uris: str,
@@ -998,7 +1024,7 @@ def nft_mint_cmd(
     license_uris: Optional[str],
     edition_total: Optional[int],
     edition_number: Optional[int],
-    fee: str,
+    fee: uint64,
     royalty_percentage_fraction: int,
     reuse: bool,
 ) -> None:
@@ -1019,8 +1045,8 @@ def nft_mint_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fp=fingerprint,
             wallet_id=id,
-            royalty_address=royalty_address,
-            target_address=target_address,
+            royalty_cli_address=royalty_address,
+            target_cli_address=target_address,
             no_did_ownership=no_did_ownership,
             hash=hash,
             uris=[u.strip() for u in uris.split(",")],
@@ -1030,7 +1056,7 @@ def nft_mint_cmd(
             license_uris=license_uris_list,
             edition_total=edition_total,
             edition_number=edition_number,
-            d_fee=Decimal(fee),
+            fee=fee,
             royalty_percentage=royalty_percentage_fraction,
             reuse_puzhash=True if reuse else None,
         )
@@ -1047,6 +1073,7 @@ def nft_mint_cmd(
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-i", "--id", help="Id of the NFT wallet to use", type=int, required=True)
+# TODO: change rpc to take bytes instead of a hex string
 @click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to add the URI to", type=str, required=True)
 @click.option("-u", "--uri", help="URI to add to the NFT", type=str)
 @click.option("-mu", "--metadata-uri", help="Metadata URI to add to the NFT", type=str)
@@ -1055,10 +1082,9 @@ def nft_mint_cmd(
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
-    callback=validate_fee,
 )
 @click.option(
     "--reuse",
@@ -1074,7 +1100,7 @@ def nft_add_uri_cmd(
     uri: str,
     metadata_uri: str,
     license_uri: str,
-    fee: str,
+    fee: uint64,
     reuse: bool,
 ) -> None:
     from .wallet_funcs import add_uri_to_nft
@@ -1084,7 +1110,7 @@ def nft_add_uri_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fp=fingerprint,
             wallet_id=id,
-            d_fee=Decimal(fee),
+            fee=fee,
             nft_coin_id=nft_coin_id,
             uri=uri,
             metadata_uri=metadata_uri,
@@ -1105,15 +1131,15 @@ def nft_add_uri_cmd(
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-i", "--id", help="Id of the NFT wallet to use", type=int, required=True)
 @click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to transfer", type=str, required=True)
-@click.option("-ta", "--target-address", help="Target recipient wallet address", type=str, required=True)
+# TODO: Change RPC to use puzzlehash instead of address
+@click.option("-ta", "--target-address", help="Target recipient wallet address", type=ADDRESS_TYPE, required=True)
 @click.option(
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
-    callback=validate_fee,
 )
 @click.option(
     "--reuse",
@@ -1126,8 +1152,8 @@ def nft_transfer_cmd(
     fingerprint: int,
     id: int,
     nft_coin_id: str,
-    target_address: str,
-    fee: str,
+    target_address: CliAddress,
+    fee: uint64,
     reuse: bool,
 ) -> None:
     from .wallet_funcs import transfer_nft
@@ -1137,9 +1163,9 @@ def nft_transfer_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fp=fingerprint,
             wallet_id=id,
-            d_fee=Decimal(fee),
+            fee=fee,
             nft_coin_id=nft_coin_id,
-            target_address=target_address,
+            target_cli_address=target_address,
             reuse_puzhash=True if reuse else None,
         )
     )
@@ -1173,16 +1199,16 @@ def nft_list_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int, num:
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-i", "--id", help="Id of the NFT wallet to use", type=int, required=True)
+# TODO: Change RPC to use bytes instead of hex string
 @click.option("-di", "--did-id", help="DID Id to set on the NFT", type=str, required=True)
 @click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to set the DID on", type=str, required=True)
 @click.option(
     "-m",
     "--fee",
     help="Set the fees per transaction, in XCH.",
-    type=str,
-    default="0",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
     show_default=True,
-    callback=validate_fee,
 )
 @click.option(
     "--reuse",
@@ -1196,7 +1222,7 @@ def nft_set_did_cmd(
     id: int,
     did_id: str,
     nft_coin_id: str,
-    fee: str,
+    fee: uint64,
     reuse: bool,
 ) -> None:
     from .wallet_funcs import set_nft_did
@@ -1206,7 +1232,7 @@ def nft_set_did_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fp=fingerprint,
             wallet_id=id,
-            d_fee=Decimal(fee),
+            fee=fee,
             nft_coin_id=nft_coin_id,
             did_id=did_id,
             reuse_puzhash=True if reuse else None,
@@ -1223,6 +1249,7 @@ def nft_set_did_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
+# TODO: Change RPC to use bytes instead of hex string
 @click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to get information on", type=str, required=True)
 def nft_get_info_cmd(
     wallet_rpc_port: Optional[int],
@@ -1252,29 +1279,30 @@ def notification_cmd() -> None:
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-t", "--to-address", help="The address to send the notification to", type=str, required=True)
+@click.option("-t", "--to-address", help="The address to send the notification to", type=ADDRESS_TYPE, required=True)
 @click.option(
     "-a",
     "--amount",
     help="The amount to send to get the notification past the recipient's spam filter",
-    type=str,
-    default="0.00001",
+    type=AMOUNT_TYPE,
+    default=uint64(10000000),
     required=True,
     show_default=True,
 )
 @click.option("-n", "--message", help="The message of the notification", type=str)
-@click.option("-m", "--fee", help="The fee for the transaction, in XCH", type=str)
+@click.option("-m", "--fee", help="The fee for the transaction, in XCH", type=TRANSACTION_FEE, default=uint64(0))
 def send_notification_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    to_address: str,
-    amount: str,
+    to_address: CliAddress,
+    amount: CliAmount,
     message: str,
-    fee: str,
+    fee: uint64,
 ) -> None:
     from .wallet_funcs import send_notification
 
-    asyncio.run(send_notification(wallet_rpc_port, fingerprint, Decimal(fee), to_address, message, Decimal(amount)))
+    message_bytes: bytes = bytes(message, "utf8")
+    asyncio.run(send_notification(wallet_rpc_port, fingerprint, fee, to_address, message_bytes, amount))
 
 
 @notification_cmd.command("get", help="Get notification(s) that are in your wallet")
@@ -1286,13 +1314,13 @@ def send_notification_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-i", "--id", help="The specific notification ID to show", type=str, multiple=True)
+@click.option("-i", "--id", help="The specific notification ID to show", type=BYTES32_TYPE, multiple=True)
 @click.option("-s", "--start", help="The number of notifications to skip", type=int, default=None)
 @click.option("-e", "--end", help="The number of notifications to stop at", type=int, default=None)
 def get_notifications_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    id: Sequence[str],
+    id: Sequence[bytes32],
     start: Optional[int],
     end: Optional[int],
 ) -> None:
@@ -1310,12 +1338,12 @@ def get_notifications_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-i", "--id", help="A specific notification ID to delete", type=str, multiple=True)
+@click.option("-i", "--id", help="A specific notification ID to delete", type=BYTES32_TYPE, multiple=True)
 @click.option("--all", help="All notifications can be deleted (they will be recovered during resync)", is_flag=True)
 def delete_notifications_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    id: Sequence[str],
+    id: Sequence[bytes32],
     all: bool,
 ) -> None:
     from .wallet_funcs import delete_notifications
@@ -1337,19 +1365,28 @@ def vcs_cmd() -> None:  # pragma: no cover
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-d", "--did", help="The DID of the VC's proof provider", type=str, required=True)
-@click.option("-t", "--target-address", help="The address to send the VC to once it's minted", type=str, required=False)
-@click.option("-m", "--fee", help="Blockchain fee for mint transaction, in XCH", type=str, required=False, default="0")
+@click.option("-d", "--did", help="The DID of the VC's proof provider", type=ADDRESS_TYPE, required=True)
+@click.option(
+    "-t", "--target-address", help="The address to send the VC to once it's minted", type=ADDRESS_TYPE, required=False
+)
+@click.option(
+    "-m",
+    "--fee",
+    help="Blockchain fee for mint transaction, in XCH",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
+    required=False,
+)
 def mint_vc_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    did: str,
-    target_address: Optional[str],
-    fee: str,
+    did: CliAddress,
+    target_address: Optional[CliAddress],
+    fee: uint64,
 ) -> None:  # pragma: no cover
     from .wallet_funcs import mint_vc
 
-    asyncio.run(mint_vc(wallet_rpc_port, fingerprint, did, Decimal(fee), target_address))
+    asyncio.run(mint_vc(wallet_rpc_port, fingerprint, did, fee, target_address))
 
 
 @vcs_cmd.command("get", short_help="Get a list of existing VCs")
@@ -1387,17 +1424,24 @@ def get_vcs_cmd(
     default=None,
 )
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
-@click.option("-l", "--vc-id", help="The launcher ID of the VC whose proofs should be updated", type=str, required=True)
+@click.option(
+    "-l", "--vc-id", help="The launcher ID of the VC whose proofs should be updated", type=BYTES32_TYPE, required=True
+)
 @click.option(
     "-t",
     "--new-puzhash",
     help="The address to send the VC after the proofs have been updated",
-    type=str,
+    type=BYTES32_TYPE,
     required=False,
 )
 @click.option("-p", "--new-proof-hash", help="The new proof hash to update the VC to", type=str, required=True)
 @click.option(
-    "-m", "--fee", help="Blockchain fee for update transaction, in XCH", type=str, required=False, default="0"
+    "-m",
+    "--fee",
+    help="Blockchain fee for update transaction, in XCH",
+    required=False,
+    type=TRANSACTION_FEE,
+    default=uint64(0),
 )
 @click.option(
     "--reuse-puzhash/--generate-new-puzhash",
@@ -1408,10 +1452,10 @@ def get_vcs_cmd(
 def spend_vc_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    vc_id: str,
-    new_puzhash: Optional[str],
+    vc_id: bytes32,
+    new_puzhash: Optional[bytes32],
     new_proof_hash: str,
-    fee: str,
+    fee: uint64,
     reuse_puzhash: bool,
 ) -> None:  # pragma: no cover
     from .wallet_funcs import spend_vc
@@ -1421,7 +1465,7 @@ def spend_vc_cmd(
             wallet_rpc_port=wallet_rpc_port,
             fp=fingerprint,
             vc_id=vc_id,
-            d_fee=Decimal(fee),
+            fee=fee,
             new_puzhash=new_puzhash,
             new_proof_hash=new_proof_hash,
             reuse_puzhash=reuse_puzhash,
@@ -1484,18 +1528,23 @@ def get_proofs_for_root_cmd(
     "-p",
     "--parent-coin-id",
     help="The ID of the parent coin of the VC (optional if VC ID is used)",
-    type=str,
+    type=BYTES32_TYPE,
     required=False,
 )
 @click.option(
     "-l",
     "--vc-id",
     help="The launcher ID of the VC to revoke (must be tracked by wallet) (optional if Parent ID is used)",
-    type=str,
+    type=BYTES32_TYPE,
     required=False,
 )
 @click.option(
-    "-m", "--fee", help="Blockchain fee for revocation transaction, in XCH", type=str, required=False, default="0"
+    "-m",
+    "--fee",
+    help="Blockchain fee for revocation transaction, in XCH",
+    required=False,
+    type=TRANSACTION_FEE,
+    default=uint64(0),
 )
 @click.option(
     "--reuse-puzhash/--generate-new-puzhash",
@@ -1506,14 +1555,14 @@ def get_proofs_for_root_cmd(
 def revoke_vc_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    parent_coin_id: Optional[str],
-    vc_id: Optional[str],
-    fee: str,
+    parent_coin_id: Optional[bytes32],
+    vc_id: Optional[bytes32],
+    fee: uint64,
     reuse_puzhash: bool,
 ) -> None:  # pragma: no cover
     from .wallet_funcs import revoke_vc
 
-    asyncio.run(revoke_vc(wallet_rpc_port, fingerprint, parent_coin_id, vc_id, Decimal(fee), reuse_puzhash))
+    asyncio.run(revoke_vc(wallet_rpc_port, fingerprint, parent_coin_id, vc_id, fee, reuse_puzhash))
 
 
 @vcs_cmd.command("approve_r_cats", help="Claim any R-CATs that are currently pending VC approval")
@@ -1527,13 +1576,22 @@ def revoke_vc_cmd(
 @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
 @click.option("-i", "--id", help="Id of the wallet with the pending approval balance", type=int, required=True)
 @click.option(
-    "-a", "--min-amount-to-claim", help="The minimum amount to approve to move into the wallet", type=str, required=True
+    "-a",
+    "--min-amount-to-claim",
+    help="The minimum amount to approve to move into the wallet",
+    type=AMOUNT_TYPE,
+    required=True,
 )
 @click.option(
-    "-m", "--fee", type=str, default=0, show_default=True, help="Blockchain fee for approval transaction, in XCH"
+    "-m",
+    "--fee",
+    type=TRANSACTION_FEE,
+    default=uint64(0),
+    show_default=True,
+    help="Blockchain fee for approval transaction, in XCH",
 )
-@click.option("-ma", "--min-coin-amount", type=Decimal, help="The minimum coin amount to select")
-@click.option("-l", "--max-coin-amount", type=Decimal, help="The maximum coin amount to select")
+@click.option("-ma", "--min-coin-amount", type=AMOUNT_TYPE, default=uint64(0), help="The minimum coin amount to select")
+@click.option("-l", "--max-coin-amount", type=AMOUNT_TYPE, default=uint64(0), help="The maximum coin amount to select")
 @click.option(
     "--reuse",
     help="Reuse existing address for the change.",
@@ -1544,16 +1602,16 @@ def approve_r_cats_cmd(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
     id: int,
-    min_amount_to_claim: str,
-    fee: str,
-    min_coin_amount: Optional[Decimal],
-    max_coin_amount: Optional[Decimal],
+    min_amount_to_claim: CliAmount,
+    fee: uint64,
+    min_coin_amount: CliAmount,
+    max_coin_amount: CliAmount,
     reuse: bool,
 ) -> None:  # pragma: no cover
     from .wallet_funcs import approve_r_cats
 
     asyncio.run(
         approve_r_cats(
-            wallet_rpc_port, fingerprint, id, min_amount_to_claim, Decimal(fee), min_coin_amount, max_coin_amount, reuse
+            wallet_rpc_port, fingerprint, uint32(id), min_amount_to_claim, fee, min_coin_amount, max_coin_amount, reuse
         )
     )
diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py
index a6b0aed2254f..9e1631d2e98d 100644
--- a/chia/cmds/wallet_funcs.py
+++ b/chia/cmds/wallet_funcs.py
@@ -11,6 +11,7 @@
 from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence, Tuple, Union
 
 from chia.cmds.cmds_util import cli_confirm, get_wallet_client, transaction_status_msg, transaction_submitted_msg
+from chia.cmds.param_types import CliAddress, CliAmount
 from chia.cmds.peer_funcs import print_connections
 from chia.cmds.units import units
 from chia.rpc.wallet_rpc_client import WalletRpcClient
@@ -26,7 +27,7 @@
 from chia.wallet.trading.trade_status import TradeStatus
 from chia.wallet.transaction_record import TransactionRecord
 from chia.wallet.transaction_sorting import SortKey
-from chia.wallet.util.address_type import AddressType, ensure_valid_address
+from chia.wallet.util.address_type import AddressType
 from chia.wallet.util.puzzle_decorator_type import PuzzleDecoratorType
 from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter
 from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType
@@ -60,7 +61,7 @@ def print_transaction(
     address_prefix: str,
     mojo_per_unit: int,
     coin_record: Optional[Dict[str, Any]] = None,
-) -> None:  # pragma: no cover
+) -> None:
     if verbose:
         print(tx)
     else:
@@ -82,7 +83,7 @@ def print_transaction(
         print("")
 
 
-def get_mojo_per_unit(wallet_type: WalletType) -> int:  # pragma: no cover
+def get_mojo_per_unit(wallet_type: WalletType) -> int:
     mojo_per_unit: int
     if wallet_type in {
         WalletType.STANDARD_WALLET,
@@ -115,13 +116,13 @@ async def get_unit_name_for_wallet_id(
     wallet_type: WalletType,
     wallet_id: int,
     wallet_client: WalletRpcClient,
-) -> str:  # pragma: no cover
+) -> str:
     if wallet_type in {
         WalletType.STANDARD_WALLET,
         WalletType.POOLING_WALLET,
         WalletType.DATA_LAYER,
         WalletType.VC,
-    }:  # pragma: no cover
+    }:
         name: str = config["network_overrides"]["config"][config["selected_network"]]["address_prefix"].upper()
     elif wallet_type in {WalletType.CAT, WalletType.CRCAT}:
         name = await wallet_client.get_cat_name(wallet_id=wallet_id)
@@ -175,7 +176,7 @@ async def get_transactions(
     sort_key: SortKey,
     reverse: bool,
     clawback: bool,
-) -> None:  # pragma: no cover
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         if paginate is None:
             paginate = sys.stdout.isatty()
@@ -243,7 +244,7 @@ async def get_transactions(
                     break
 
 
-def check_unusual_transaction(amount: Decimal, fee: Decimal) -> bool:
+def check_unusual_transaction(amount: uint64, fee: uint64) -> bool:
     return fee >= amount
 
 
@@ -252,32 +253,23 @@ async def send(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
     wallet_id: int,
-    amount: Decimal,
+    amount: CliAmount,
     memo: Optional[str],
-    fee: Decimal,
-    address: str,
+    fee: uint64,
+    address: CliAddress,
     override: bool,
-    min_coin_amount: Decimal,
-    max_coin_amount: Decimal,
+    min_coin_amount: CliAmount,
+    max_coin_amount: CliAmount,
     excluded_coin_ids: Sequence[str],
     reuse_puzhash: Optional[bool],
     clawback_time_lock: int,
-) -> None:  # pragma: no cover
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, _):
         if memo is None:
             memos = None
         else:
             memos = [memo]
 
-        if not override and check_unusual_transaction(amount, fee):
-            print(
-                f"A transaction of amount {amount} and fee {fee} is unusual.\n"
-                f"Pass in --override if you are sure you mean to do this."
-            )
-            return
-        if amount == 0:
-            print("You can not send an empty transaction")
-            return
         if clawback_time_lock < 0:
             print("Clawback time lock seconds cannot be negative.")
             return
@@ -288,17 +280,27 @@ async def send(
             print(f"Wallet id: {wallet_id} not found.")
             return
 
-        final_fee: uint64 = uint64(int(fee * units["chia"]))  # fees are always in XCH mojos
-        final_amount: uint64 = uint64(int(amount * mojo_per_unit))
-        final_min_coin_amount: uint64 = uint64(int(min_coin_amount * mojo_per_unit))
-        final_max_coin_amount: uint64 = uint64(int(max_coin_amount * mojo_per_unit))
+        final_amount: uint64 = amount.convert_amount(mojo_per_unit)
+        final_min_coin_amount: uint64 = min_coin_amount.convert_amount(mojo_per_unit)
+        final_max_coin_amount: uint64 = max_coin_amount.convert_amount(mojo_per_unit)
+
+        if not override and check_unusual_transaction(final_amount, fee):
+            print(
+                f"A transaction of amount {final_amount / units['chia']} and fee {fee} is unusual.\n"
+                f"Pass in --override if you are sure you mean to do this."
+            )
+            return
+        if final_amount == 0:
+            print("You can not send an empty transaction")
+            return
+
         if typ == WalletType.STANDARD_WALLET:
             print("Submitting transaction...")
             res = await wallet_client.send_transaction(
                 wallet_id,
                 final_amount,
-                address,
-                final_fee,
+                address.original_address,
+                fee,
                 memos,
                 final_min_coin_amount,
                 final_max_coin_amount,
@@ -315,8 +317,8 @@ async def send(
             res = await wallet_client.cat_spend(
                 wallet_id,
                 final_amount,
-                address,
-                final_fee,
+                address.original_address,
+                fee,
                 memos,
                 final_min_coin_amount,
                 final_max_coin_amount,
@@ -367,44 +369,31 @@ async def update_derivation_index(wallet_rpc_port: Optional[int], fp: Optional[i
         print("Your balances may take a while to update.")
 
 
-async def add_token(wallet_rpc_port: Optional[int], fp: Optional[int], asset_id: str, token_name: str) -> None:
+async def add_token(wallet_rpc_port: Optional[int], fp: Optional[int], asset_id: bytes32, token_name: str) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, _):
-        try:
-            asset_id_bytes: bytes32 = bytes32.from_hexstr(asset_id)
-            existing_info: Optional[Tuple[Optional[uint32], str]] = await wallet_client.cat_asset_id_to_name(
-                asset_id_bytes
-            )
-            if existing_info is None or existing_info[0] is None:
-                response = await wallet_client.create_wallet_for_existing_cat(asset_id_bytes)
-                wallet_id = response["wallet_id"]
-                await wallet_client.set_cat_name(wallet_id, token_name)
-                print(f"Successfully added {token_name} with wallet id {wallet_id} on key {fingerprint}")
-            else:
-                wallet_id, old_name = existing_info
-                await wallet_client.set_cat_name(wallet_id, token_name)
-                print(
-                    f"Successfully renamed {old_name} with wallet_id {wallet_id} on key {fingerprint} to {token_name}"
-                )
-        except ValueError as e:
-            if "fromhex()" in str(e):
-                print(f"{asset_id} is not a valid Asset ID")
-            else:
-                raise
+        existing_info: Optional[Tuple[Optional[uint32], str]] = await wallet_client.cat_asset_id_to_name(asset_id)
+        if existing_info is None or existing_info[0] is None:
+            response = await wallet_client.create_wallet_for_existing_cat(asset_id)
+            wallet_id = response["wallet_id"]
+            await wallet_client.set_cat_name(wallet_id, token_name)
+            print(f"Successfully added {token_name} with wallet id {wallet_id} on key {fingerprint}")
+        else:
+            wallet_id, old_name = existing_info
+            await wallet_client.set_cat_name(wallet_id, token_name)
+            print(f"Successfully renamed {old_name} with wallet_id {wallet_id} on key {fingerprint} to {token_name}")
 
 
 async def make_offer(
     *,
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
-    d_fee: Decimal,
+    fee: uint64,
     offers: Sequence[str],
     requests: Sequence[str],
     filepath: str,
     reuse_puzhash: Optional[bool],
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        fee: int = int(d_fee * units["chia"])
-
         if offers == [] or requests == []:
             print("Not creating offer: Must be offering and requesting at least one asset")
         else:
@@ -618,7 +607,7 @@ async def get_offers(
     *,
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
-    offer_id: Optional[str],
+    offer_id: Optional[bytes32],
     filepath: Optional[str],
     exclude_my_offers: bool = False,
     exclude_taken_offers: bool = False,
@@ -654,7 +643,7 @@ async def get_offers(
                 start = end
                 end += batch_size
         else:
-            records = [await wallet_client.get_offer(bytes32.from_hexstr(offer_id), file_contents)]
+            records = [await wallet_client.get_offer(offer_id, file_contents)]
             if filepath is not None:
                 with open(pathlib.Path(filepath), "w") as file:
                     file.write(Offer.from_bytes(records[0].offer).to_bech32())
@@ -667,7 +656,7 @@ async def get_offers(
 async def take_offer(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
-    d_fee: Decimal,
+    fee: uint64,
     file: str,
     examine_only: bool,
 ) -> None:
@@ -680,8 +669,6 @@ async def take_offer(
         else:
             offer_hex = file
 
-        fee: int = int(d_fee * units["chia"])
-
         try:
             offer = Offer.from_bech32(offer_hex)
         except ValueError:
@@ -757,12 +744,9 @@ async def take_offer(
 
 
 async def cancel_offer(
-    wallet_rpc_port: Optional[int], fp: Optional[int], d_fee: Decimal, offer_id_hex: str, secure: bool
+    wallet_rpc_port: Optional[int], fp: Optional[int], fee: uint64, offer_id: bytes32, secure: bool
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        offer_id = bytes32.from_hexstr(offer_id_hex)
-        fee: int = int(d_fee * units["chia"])
-
         trade_record = await wallet_client.get_offer(offer_id, file_contents=True)
         await print_trade_record(trade_record, wallet_client, summaries=True)
 
@@ -773,7 +757,7 @@ async def cancel_offer(
             print(f"Use chia wallet get_offers --id {trade_record.trade_id} -f {fingerprint} to view cancel status")
 
 
-def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]:  # pragma: no cover
+def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]:
     if typ in {WalletType.CAT, WalletType.CRCAT}:
         return "", units["cat"]
     if typ in [WalletType.STANDARD_WALLET, WalletType.POOLING_WALLET, WalletType.MULTI_SIG]:
@@ -794,7 +778,7 @@ def print_balance(amount: int, scale: int, address_prefix: str, *, decimal_only:
 
 async def print_balances(
     wallet_rpc_port: Optional[int], fp: Optional[int], wallet_type: Optional[WalletType] = None
-) -> None:  # pragma: no cover
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         summaries_response = await wallet_client.get_wallets(wallet_type)
         address_prefix = selected_network_address_prefix(config)
@@ -864,10 +848,9 @@ async def print_balances(
 
 
 async def create_did_wallet(
-    wallet_rpc_port: Optional[int], fp: Optional[int], d_fee: Decimal, name: Optional[str], amount: int
+    wallet_rpc_port: Optional[int], fp: Optional[int], fee: uint64, name: Optional[str], amount: int
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        fee: int = int(d_fee * units["chia"])
         try:
             response = await wallet_client.create_new_did_wallet(amount, fee, name)
             wallet_id = response["wallet_id"]
@@ -952,13 +935,14 @@ async def transfer_did(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
     did_wallet_id: int,
-    fee: int,
-    target_address: str,
+    fee: uint64,
+    target_cli_address: CliAddress,
     with_recovery: bool,
     reuse_puzhash: Optional[bool],
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         try:
+            target_address = target_cli_address.original_address
             response = await wallet_client.did_transfer_did(
                 did_wallet_id,
                 target_address,
@@ -999,11 +983,11 @@ async def find_lost_did(
 
 
 async def create_nft_wallet(
-    wallet_rpc_port: Optional[int], fp: Optional[int], did_id: Optional[str] = None, name: Optional[str] = None
+    wallet_rpc_port: Optional[int], fp: Optional[int], did_id: Optional[CliAddress] = None, name: Optional[str] = None
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         try:
-            response = await wallet_client.create_new_nft_wallet(did_id, name)
+            response = await wallet_client.create_new_nft_wallet(did_id.original_address if did_id else None, name)
             wallet_id = response["wallet_id"]
             print(f"Successfully created an NFT wallet with id {wallet_id} on key {fingerprint}")
         except Exception as e:
@@ -1015,8 +999,8 @@ async def mint_nft(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
     wallet_id: int,
-    royalty_address: Optional[str],
-    target_address: Optional[str],
+    royalty_cli_address: Optional[CliAddress],
+    target_cli_address: Optional[CliAddress],
     no_did_ownership: bool,
     hash: str,
     uris: List[str],
@@ -1026,22 +1010,13 @@ async def mint_nft(
     license_uris: List[str],
     edition_total: Optional[int],
     edition_number: Optional[int],
-    d_fee: Decimal,
+    fee: uint64,
     royalty_percentage: int,
     reuse_puzhash: Optional[bool],
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        royalty_address = (
-            None
-            if not royalty_address
-            else ensure_valid_address(royalty_address, allowed_types={AddressType.XCH}, config=config)
-        )
-        target_address = (
-            None
-            if not target_address
-            else ensure_valid_address(target_address, allowed_types={AddressType.XCH}, config=config)
-        )
-        fee: int = int(d_fee * units["chia"])
+        royalty_address = royalty_cli_address.validate_address_type(AddressType.XCH) if royalty_cli_address else None
+        target_address = target_cli_address.validate_address_type(AddressType.XCH) if target_cli_address else None
         try:
             response = await wallet_client.get_nft_wallet_did(wallet_id)
             wallet_did = response["did_id"]
@@ -1085,7 +1060,7 @@ async def add_uri_to_nft(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
     wallet_id: int,
-    d_fee: Decimal,
+    fee: uint64,
     nft_coin_id: str,
     uri: str,
     metadata_uri: str,
@@ -1107,7 +1082,6 @@ async def add_uri_to_nft(
                 uri_value = license_uri
             else:
                 raise ValueError("You must provide at least one of the URI flags")
-            fee: int = int(d_fee * units["chia"])
             response = await wallet_client.add_uri_to_nft(wallet_id, nft_coin_id, key, uri_value, fee, reuse_puzhash)
             spend_bundle = response["spend_bundle"]
             print(f"URI added successfully with spend bundle: {spend_bundle}")
@@ -1120,15 +1094,14 @@ async def transfer_nft(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
     wallet_id: int,
-    d_fee: Decimal,
+    fee: uint64,
     nft_coin_id: str,
-    target_address: str,
+    target_cli_address: CliAddress,
     reuse_puzhash: Optional[bool],
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         try:
-            target_address = ensure_valid_address(target_address, allowed_types={AddressType.XCH}, config=config)
-            fee: int = int(d_fee * units["chia"])
+            target_address = target_cli_address.validate_address_type(AddressType.XCH)
             response = await wallet_client.transfer_nft(
                 wallet_id, nft_coin_id, target_address, fee, reuse_puzhash=reuse_puzhash
             )
@@ -1197,13 +1170,12 @@ async def set_nft_did(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
     wallet_id: int,
-    d_fee: Decimal,
+    fee: uint64,
     nft_coin_id: str,
     did_id: str,
     reuse_puzhash: Optional[bool],
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        fee: int = int(d_fee * units["chia"])
         try:
             response = await wallet_client.set_nft_did(wallet_id, did_id, nft_coin_id, fee, reuse_puzhash=reuse_puzhash)
             spend_bundle = response["spend_bundle"]
@@ -1282,31 +1254,30 @@ def fungible_assets_from_offer(offer: Offer) -> List[Optional[bytes32]]:
 async def send_notification(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
-    d_fee: Decimal,
-    address_str: str,
-    message_hex: str,
-    d_amount: Decimal,
+    fee: uint64,
+    address: CliAddress,
+    message: bytes,
+    cli_amount: CliAmount,
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        address: bytes32 = decode_puzzle_hash(address_str)
-        amount: uint64 = uint64(d_amount * units["chia"])
-        message: bytes = bytes(message_hex, "utf8")
-        fee: uint64 = uint64(d_fee * units["chia"])
+        amount: uint64 = cli_amount.convert_amount(units["chia"])
 
-        tx = await wallet_client.send_notification(address, message, amount, fee)
+        tx = await wallet_client.send_notification(address.puzzle_hash, message, amount, fee)
 
         print("Notification sent successfully.")
         print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx.name}")
 
 
 async def get_notifications(
-    wallet_rpc_port: Optional[int], fp: Optional[int], str_ids: Sequence[str], start: Optional[int], end: Optional[int]
+    wallet_rpc_port: Optional[int],
+    fp: Optional[int],
+    ids: Optional[Sequence[bytes32]],
+    start: Optional[int],
+    end: Optional[int],
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        ids: Optional[List[bytes32]] = [bytes32.from_hexstr(sid) for sid in str_ids]
         if ids is not None and len(ids) == 0:
             ids = None
-
         notifications = await wallet_client.get_notifications(ids=ids, pagination=(start, end))
         for notification in notifications:
             print("")
@@ -1316,11 +1287,9 @@ async def get_notifications(
 
 
 async def delete_notifications(
-    wallet_rpc_port: Optional[int], fp: Optional[int], str_ids: Sequence[str], delete_all: bool
+    wallet_rpc_port: Optional[int], fp: Optional[int], ids: Sequence[bytes32], delete_all: bool
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
-        ids: Optional[List[bytes32]] = [bytes32.from_hexstr(sid) for sid in str_ids]
-
         if delete_all:
             print(f"Success: {await wallet_client.delete_notifications()}")
         else:
@@ -1333,26 +1302,28 @@ async def sign_message(
     fp: Optional[int],
     addr_type: AddressType,
     message: str,
-    address: Optional[str] = None,
-    did_id: Optional[str] = None,
-    nft_id: Optional[str] = None,
+    address: Optional[CliAddress] = None,
+    did_id: Optional[CliAddress] = None,
+    nft_id: Optional[CliAddress] = None,
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         if addr_type == AddressType.XCH:
             if address is None:
                 print("Address is required for XCH address type.")
                 return
-            pubkey, signature, signing_mode = await wallet_client.sign_message_by_address(address, message)
+            pubkey, signature, signing_mode = await wallet_client.sign_message_by_address(
+                address.original_address, message
+            )
         elif addr_type == AddressType.DID:
             if did_id is None:
                 print("DID id is required for DID address type.")
                 return
-            pubkey, signature, signing_mode = await wallet_client.sign_message_by_id(did_id, message)
+            pubkey, signature, signing_mode = await wallet_client.sign_message_by_id(did_id.original_address, message)
         elif addr_type == AddressType.NFT:
             if nft_id is None:
                 print("NFT id is required for NFT address type.")
                 return
-            pubkey, signature, signing_mode = await wallet_client.sign_message_by_id(nft_id, message)
+            pubkey, signature, signing_mode = await wallet_client.sign_message_by_id(nft_id.original_address, message)
         else:
             print("Invalid wallet type.")
             return
@@ -1364,8 +1335,8 @@ async def sign_message(
 
 
 async def spend_clawback(
-    *, wallet_rpc_port: Optional[int], fp: Optional[int], fee: Decimal, tx_ids_str: str, force: bool = False
-) -> None:  # pragma: no cover
+    *, wallet_rpc_port: Optional[int], fp: Optional[int], fee: uint64, tx_ids_str: str, force: bool = False
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, _):
         tx_ids = []
         for tid in tx_ids_str.split(","):
@@ -1376,22 +1347,22 @@ async def spend_clawback(
         if fee < 0:
             print("Batch fee cannot be negative.")
             return
-        response = await wallet_client.spend_clawback_coins(tx_ids, int(fee * units["chia"]), force)
+        response = await wallet_client.spend_clawback_coins(tx_ids, fee, force)
         print(str(response))
 
 
 async def mint_vc(
-    wallet_rpc_port: Optional[int], fp: Optional[int], did: str, d_fee: Decimal, target_address: Optional[str]
-) -> None:  # pragma: no cover
+    wallet_rpc_port: Optional[int],
+    fp: Optional[int],
+    did: CliAddress,
+    fee: uint64,
+    target_address: Optional[CliAddress],
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         vc_record, txs = await wallet_client.vc_mint(
-            decode_puzzle_hash(ensure_valid_address(did, allowed_types={AddressType.DID}, config=config)),
-            None
-            if target_address is None
-            else decode_puzzle_hash(
-                ensure_valid_address(target_address, allowed_types={AddressType.XCH}, config=config)
-            ),
-            uint64(int(d_fee * units["chia"])),
+            did.validate_address_type_get_ph(AddressType.DID),
+            target_address.validate_address_type_get_ph(AddressType.XCH) if target_address else None,
+            fee,
         )
 
         print(f"New VC with launcher ID minted: {vc_record.vc.launcher_id}")
@@ -1407,9 +1378,7 @@ async def mint_vc(
             )
 
 
-async def get_vcs(
-    wallet_rpc_port: Optional[int], fp: Optional[int], start: int, count: int
-) -> None:  # pragma: no cover
+async def get_vcs(wallet_rpc_port: Optional[int], fp: Optional[int], start: int, count: int) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         vc_records, proofs = await wallet_client.vc_get_list(start, count)
         print("Proofs:")
@@ -1436,18 +1405,18 @@ async def spend_vc(
     *,
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
-    vc_id: str,
-    d_fee: Decimal,
-    new_puzhash: Optional[str],
+    vc_id: bytes32,
+    fee: uint64,
+    new_puzhash: Optional[bytes32],
     new_proof_hash: str,
     reuse_puzhash: bool,
-) -> None:  # pragma: no cover
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         txs = await wallet_client.vc_spend(
-            bytes32.from_hexstr(vc_id),
-            new_puzhash=None if new_puzhash is None else bytes32.from_hexstr(new_puzhash),
+            vc_id,
+            new_puzhash=new_puzhash,
             new_proof_hash=bytes32.from_hexstr(new_proof_hash),
-            fee=uint64(int(d_fee * units["chia"])),
+            fee=fee,
             reuse_puzhash=reuse_puzhash,
         )
 
@@ -1466,7 +1435,7 @@ async def spend_vc(
 
 async def add_proof_reveal(
     wallet_rpc_port: Optional[int], fp: Optional[int], proofs: Sequence[str], root_only: bool
-) -> None:  # pragma: no cover
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         if len(proofs) == 0:
             print("Must specify at least one proof")
@@ -1482,9 +1451,7 @@ async def add_proof_reveal(
             return
 
 
-async def get_proofs_for_root(
-    wallet_rpc_port: Optional[int], fp: Optional[int], proof_hash: str
-) -> None:  # pragma: no cover
+async def get_proofs_for_root(wallet_rpc_port: Optional[int], fp: Optional[int], proof_hash: str) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         proof_dict: Dict[str, str] = await wallet_client.vc_get_proofs_for_root(bytes32.from_hexstr(proof_hash))
         print("Proofs:")
@@ -1495,26 +1462,26 @@ async def get_proofs_for_root(
 async def revoke_vc(
     wallet_rpc_port: Optional[int],
     fp: Optional[int],
-    parent_coin_id: Optional[str],
-    vc_id: Optional[str],
-    fee: Decimal,
+    parent_coin_id: Optional[bytes32],
+    vc_id: Optional[bytes32],
+    fee: uint64,
     reuse_puzhash: bool,
-) -> None:  # pragma: no cover
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
         if parent_coin_id is None:
             if vc_id is None:
                 print("Must specify either --parent-coin-id or --vc-id")
                 return
-            record = await wallet_client.vc_get(bytes32.from_hexstr(vc_id))
+            record = await wallet_client.vc_get(vc_id)
             if record is None:
-                print(f"Cannot find a VC with ID {vc_id}")
+                print(f"Cannot find a VC with ID {vc_id.hex()}")
                 return
             parent_id: bytes32 = bytes32(record.vc.coin.parent_coin_info)
         else:
-            parent_id = bytes32.from_hexstr(parent_coin_id)
+            parent_id = parent_coin_id
         txs = await wallet_client.vc_revoke(
             parent_id,
-            fee=uint64(fee * units["chia"]),
+            fee=fee,
             reuse_puzhash=reuse_puzhash,
         )
 
@@ -1534,22 +1501,22 @@ async def revoke_vc(
 async def approve_r_cats(
     wallet_rpc_port: Optional[int],
     fingerprint: int,
-    id: int,
-    min_amount_to_claim: str,
-    fee: Decimal,
-    min_coin_amount: Optional[Decimal],
-    max_coin_amount: Optional[Decimal],
+    wallet_id: uint32,
+    min_amount_to_claim: CliAmount,
+    fee: uint64,
+    min_coin_amount: CliAmount,
+    max_coin_amount: CliAmount,
     reuse: bool,
-) -> None:  # pragma: no cover
+) -> None:
     async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config):
         if wallet_client is None:
             return
         txs = await wallet_client.crcat_approve_pending(
-            wallet_id=uint32(id),
-            min_amount_to_claim=uint64(int(Decimal(min_amount_to_claim) * units["cat"])),
-            fee=uint64(int(fee * units["chia"])),
-            min_coin_amount=uint64(int(min_coin_amount * units["chia"])) if min_coin_amount is not None else None,
-            max_coin_amount=uint64(int(max_coin_amount * units["chia"])) if max_coin_amount is not None else None,
+            wallet_id=wallet_id,
+            min_amount_to_claim=min_amount_to_claim.convert_amount(units["cat"]),
+            fee=fee,
+            min_coin_amount=min_coin_amount.convert_amount(units["chia"]),
+            max_coin_amount=max_coin_amount.convert_amount(units["chia"]),
             reuse_puzhash=reuse,
         )
 
diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py
index 4002ac41dfb6..4ec4e6d3d7e3 100644
--- a/chia/rpc/wallet_rpc_client.py
+++ b/chia/rpc/wallet_rpc_client.py
@@ -1170,7 +1170,7 @@ async def dl_delete_mirror(self, coin_id: bytes32, fee: uint64 = uint64(0)) -> L
         return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]]
 
     async def get_notifications(
-        self, ids: Optional[List[bytes32]] = None, pagination: Optional[Tuple[Optional[int], Optional[int]]] = None
+        self, ids: Optional[Sequence[bytes32]] = None, pagination: Optional[Tuple[Optional[int], Optional[int]]] = None
     ) -> List[Notification]:
         request: Dict[str, Any] = {}
         if ids is not None:
@@ -1191,7 +1191,7 @@ async def get_notifications(
             for notification in response["notifications"]
         ]
 
-    async def delete_notifications(self, ids: Optional[List[bytes32]] = None) -> bool:
+    async def delete_notifications(self, ids: Optional[Sequence[bytes32]] = None) -> bool:
         request: Dict[str, Any] = {}
         if ids is not None:
             request["ids"] = [id.hex() for id in ids]
diff --git a/chia/wallet/trading/offer.py b/chia/wallet/trading/offer.py
index ca57e4b9fde3..fa2b8c4c8213 100644
--- a/chia/wallet/trading/offer.py
+++ b/chia/wallet/trading/offer.py
@@ -64,7 +64,7 @@ def from_condition_and_nonce(cls, condition: Program, nonce: bytes32) -> "Notari
         return cls(puzzle_hash, amount, memos, nonce)
 
 
-@dataclass(frozen=True)
+@dataclass(frozen=True, eq=False)
 class Offer:
     requested_payments: Dict[
         Optional[bytes32], List[NotarizedPayment]
@@ -74,9 +74,9 @@ class Offer:
 
     # this is a cache of the coin additions made by the SpendBundle (_bundle)
     # ordered by the coin being spent
-    _additions: Dict[Coin, List[Coin]] = field(init=False)
-    _offered_coins: Dict[Optional[bytes32], List[Coin]] = field(init=False)
-    _final_spend_bundle: Optional[SpendBundle] = field(init=False)
+    _additions: Dict[Coin, List[Coin]] = field(init=False, repr=False)
+    _offered_coins: Dict[Optional[bytes32], List[Coin]] = field(init=False, repr=False)
+    _final_spend_bundle: Optional[SpendBundle] = field(init=False, repr=False)
 
     @staticmethod
     def ph() -> bytes32:
diff --git a/tests/cmds/test_click_types.py b/tests/cmds/test_click_types.py
new file mode 100644
index 000000000000..0e5e669a6938
--- /dev/null
+++ b/tests/cmds/test_click_types.py
@@ -0,0 +1,138 @@
+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 ADDRESS_TYPE, AMOUNT_TYPE, BYTES32_TYPE, TRANSACTION_FEE, CliAddress, CliAmount
+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")
+
+
+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 TRANSACTION_FEE.convert(uint64(10000), None, None) == uint64(10000)
+
+    # TODO: Test MOJO Logic When Implemented
+
+    # Test Decimal / XCH
+    assert TRANSACTION_FEE.convert("0.5", None, None) == uint64(int(0.5 * units["chia"]))
+    assert TRANSACTION_FEE.convert("0", None, None) == uint64(0)
+    # Test Decimal Failures
+    with pytest.raises(BadParameter):
+        TRANSACTION_FEE.convert("test", None, None)
+    with pytest.raises(BadParameter):
+        TRANSACTION_FEE.convert("0.6", None, None)
+    with pytest.raises(BadParameter):
+        TRANSACTION_FEE.convert("-0.6", 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=Decimal(10000000000))
+    mojos_cli_amount = CliAmount(mojos=True, amount=uint64(100000))
+    # Test CliAmount (Generally is not used)
+    assert AMOUNT_TYPE.convert(decimal_cli_amount, None, None) == decimal_cli_amount
+
+    # Test uint64 (only usable as default)
+    assert AMOUNT_TYPE.convert(uint64(100000), None, None) == mojos_cli_amount
+
+    # TODO: Test MOJO Logic When Implemented
+
+    # Test Decimal / XCH (we dont test overflow because we dont know the conversion ratio yet)
+    assert AMOUNT_TYPE.convert("5.25", None, None) == decimal_cli_amount
+    assert AMOUNT_TYPE.convert("10000000000", None, None) == large_decimal_amount
+    # Test Decimal Failures
+    with pytest.raises(BadParameter):
+        AMOUNT_TYPE.convert("test", None, None)
+    with pytest.raises(BadParameter):
+        AMOUNT_TYPE.convert("0", None, None)
+    with pytest.raises(BadParameter):
+        AMOUNT_TYPE.convert("-0.6", None, None)
+
+    # Test CliAmount Class
+    assert decimal_cli_amount.convert_amount(units["chia"]) == uint64(int(5.25 * units["chia"]))
+    assert mojos_cli_amount.convert_amount(units["chia"]) == uint64(100000)
+    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 ADDRESS_TYPE.convert(std_cli_address, None, context) == std_cli_address
+
+    # test address parsing
+    assert ADDRESS_TYPE.convert(burn_address, None, context) == std_cli_address
+    assert ADDRESS_TYPE.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):
+        ADDRESS_TYPE.convert("test", None, None)
+    with pytest.raises(AttributeError):  # attribute error because the context does not have a real error handler
+        ADDRESS_TYPE.convert(burn_address_txch, None, context)
+    with pytest.raises(BadParameter):
+        ADDRESS_TYPE.convert(burn_bad_prefix, 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 root path in context.
+    context = cast(Context, FakeContext(obj={"root_path": root_path_populated_with_config}))
+    # run test that should pass
+    assert ADDRESS_TYPE.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
+        ADDRESS_TYPE.convert(burn_address_txch, None, context)
+
+
+def test_click_bytes32_type() -> None:
+    # Test bytes32 (Generally it is not used)
+    assert BYTES32_TYPE.convert(burn_ph, None, None) == burn_ph
+
+    # test bytes32 parsing
+    assert BYTES32_TYPE.convert("0x" + burn_ph.hex(), None, None) == burn_ph
+    # check error handling
+    with pytest.raises(BadParameter):
+        BYTES32_TYPE.convert("test", None, None)
diff --git a/tests/cmds/wallet/config.py b/tests/cmds/wallet/config.py
index 562db3171ae4..a681c1be5ca0 100644
--- a/tests/cmds/wallet/config.py
+++ b/tests/cmds/wallet/config.py
@@ -1,3 +1,5 @@
 from __future__ import annotations
 
 parallel = True
+install_timelord = False
+checkout_blocks_and_plots = False
diff --git a/tests/cmds/wallet/test_did.py b/tests/cmds/wallet/test_did.py
index 1e8ceaec76b3..3ffab1c34cf4 100644
--- a/tests/cmds/wallet/test_did.py
+++ b/tests/cmds/wallet/test_did.py
@@ -318,6 +318,6 @@ async def did_transfer_did(
     ]
     run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)
     expected_calls: logType = {
-        "did_transfer_did": [(w_id, t_address, "0.5", True, True)],
+        "did_transfer_did": [(w_id, t_address, 500000000000, True, True)],
     }
     test_rpc_clients.wallet_rpc_client.check_log(expected_calls)
diff --git a/tests/cmds/wallet/test_notifications.py b/tests/cmds/wallet/test_notifications.py
index b39fcbbbeb5d..71e0c89a9d6e 100644
--- a/tests/cmds/wallet/test_notifications.py
+++ b/tests/cmds/wallet/test_notifications.py
@@ -87,7 +87,7 @@ async def get_notifications(
         "amount: 1000000000",
     ]
     run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)
-    expected_calls: logType = {"get_notifications": [([get_bytes32(1)], (10, 10))]}
+    expected_calls: logType = {"get_notifications": [((get_bytes32(1),), (10, 10))]}
     test_rpc_clients.wallet_rpc_client.check_log(expected_calls)
 
 
diff --git a/tests/cmds/wallet/test_wallet.py b/tests/cmds/wallet/test_wallet.py
index d325c45b4e54..877539cb4359 100644
--- a/tests/cmds/wallet/test_wallet.py
+++ b/tests/cmds/wallet/test_wallet.py
@@ -384,7 +384,7 @@ async def cat_spend(
         "wallet",
         "send",
         "-a1",
-        "-m1",
+        "-m0.5",
         "-o",
         WALLET_ID_ARG,
         f"-e{bytes32_hexstr}",
@@ -413,7 +413,7 @@ async def cat_spend(
                 1,
                 1000000000000,
                 "xch1qvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvps82kgr2",
-                1000000000000,
+                500000000000,
                 ["0x6262626262626262626262626262626262626262626262626262626262626262"],
                 0,
                 10000000000000,
@@ -428,7 +428,7 @@ async def cat_spend(
                 1,
                 1000,
                 "xch1qvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvps82kgr2",
-                1000000000000,
+                500000000000,
                 ["0x6262626262626262626262626262626262626262626262626262626262626262"],
                 0,
                 10000,
@@ -500,14 +500,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)
 
@@ -689,7 +689,7 @@ def to_bech32(self) -> str:
         FINGERPRINT_ARG,
         f"-p{str(tmp_path / 'test.offer')}",
         "--reuse",
-        "-m1",
+        "-m0.5",
         "--offer",
         "1:10",
         "--offer",
@@ -703,7 +703,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, assert_list)
@@ -754,7 +754,7 @@ def to_bech32(self) -> str:
                     }
                 },
                 None,
-                1000000000000,
+                500000000000,
                 False,
                 0,
                 0,
@@ -876,7 +876,7 @@ async def take_offer(
 
     inst_rpc_client = TakeOfferRpcClient()  # pylint: disable=no-value-for-parameter
     test_rpc_clients.wallet_rpc_client = inst_rpc_client
-    command_args = ["wallet", "take_offer", test_offer_file_name, FINGERPRINT_ARG, "-m1", "--reuse"]
+    command_args = ["wallet", "take_offer", test_offer_file_name, FINGERPRINT_ARG, "-m0.5", "--reuse"]
     # these are various things that should be in the output
     cat1 = bytes32.from_hexstr("fd6a341ed39c05c31157d5bfea395a0e142398ced24deea1e82f836d7ec2909c")
     cat2 = bytes32.from_hexstr("dc59bcd60ce5fc9c93a5d3b11875486b03efb53a53da61e453f5cf61a7746860")
@@ -896,7 +896,7 @@ async def take_offer(
             (cat2,),
             (bytes32.from_hexstr("accce8e1c71b56624f2ecaeff5af57eac41365080449904d0717bd333c04806d"),),
         ],
-        "take_offer": [(Offer.from_bech32(test_offer_file_bech32), None, 1000000000000, uint64(0), None)],
+        "take_offer": [(Offer.from_bech32(test_offer_file_bech32), None, 500000000000, uint64(0), None)],
     }
     test_rpc_clients.wallet_rpc_client.check_log(expected_calls)
 
@@ -929,7 +929,7 @@ async def cancel_offer(self, trade_id: bytes32, fee: uint64 = uint64(0), secure:
 
     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")
@@ -945,7 +945,7 @@ async def cancel_offer(self, trade_id: bytes32, fee: uint64 = uint64(0), secure:
     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, 1000000000000, True)],
+        "cancel_offer": [(test_offer_id_bytes, 500000000000, True)],
         "cat_asset_id_to_name": [
             (cat1,),
             (cat2,),
diff --git a/tests/pools/test_pool_cmdline.py b/tests/pools/test_pool_cmdline.py
index 309480e95982..d114fd73a710 100644
--- a/tests/pools/test_pool_cmdline.py
+++ b/tests/pools/test_pool_cmdline.py
@@ -6,28 +6,12 @@
 import pytest
 from click.testing import CliRunner, Result
 
-from chia.cmds.plotnft import create_cmd, show_cmd, validate_fee
+from chia.cmds.plotnft import create_cmd, show_cmd
 
 pytestmark = pytest.mark.skip("TODO: Works locally but fails on CI, needs to be fixed!")
 
 
 class TestPoolNFTCommands:
-    def test_validate_fee(self):
-        with pytest.raises(click.exceptions.BadParameter):
-            r = validate_fee(None, "fee", "1.0")
-
-        with pytest.raises(click.exceptions.BadParameter):
-            r = validate_fee(None, "fee", "-1")
-
-        r = validate_fee(None, "fee", "0")
-        assert r == "0"
-
-        r = validate_fee(None, "fee", "0.000000000001")
-        assert r == "0.000000000001"
-
-        r = validate_fee(None, "fee", "0.5")
-        assert r == "0.5"
-
     def test_plotnft_show(self):
         runner = CliRunner()
         result: Result = runner.invoke(show_cmd, [])


Code review comments:
diff_hunk: @@ -0,0 +1,166 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from decimal import Decimal, InvalidOperation
+from typing import Any, Optional, Union
+
+import click
+
+from chia.cmds.units import units
+from chia.types.blockchain_format.sized_bytes import bytes32
+from chia.util.bech32m import bech32_decode, decode_puzzle_hash
+from chia.util.config import selected_network_address_prefix
+from chia.util.ints import uint64
+from chia.wallet.util.address_type import AddressType
+
+
+class TransactionFeeParamType(click.ParamType):
+    """
+    A Click parameter type for transaction fees, which can be specified in XCH or mojos.
+    """
+
+    name: str = "uint64"  # output type
+    value_limit: Decimal = Decimal(0.5)
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> uint64:
+        if isinstance(value, uint64):  # required by click
+            return value
+        mojos = False  # TODO: Add unit logic
+        if mojos:
+            try:
+                return uint64(int(value))
+            except ValueError:
+                self.fail("Fee must be positive integer value in mojos", param, ctx)
+        try:
+            d_value = Decimal(value)
+            if 0 > d_value:
+                self.fail("Fee can not be negative", param, ctx)
+            if self.value_limit != Decimal(0) and d_value > self.value_limit:
+                self.fail(f"Fee must be in the range 0 to {self.value_limit}", param, ctx)
+            return uint64(int(d_value * units["chia"]))
+        except InvalidOperation:  # won't ever be value error because of the fee limit check
+            self.fail("Fee must be decimal dotted value in XCH (e.g. 0.00005)", param, ctx)
+
+
+@dataclass(frozen=True)
+class CliAmount:
+    """
+    A dataclass for TX / wallet amounts for both XCH and CAT, and of course mojos.
+    """
+
+    mojos: bool
+    amount: Union[uint64, Decimal]  # uint64 if mojos, Decimal if not
+
+    def convert_amount(self, mojo_per_unit: int) -> uint64:
+        if self.mojos:
+            if not isinstance(self.amount, uint64):
+                raise ValueError("Amount must be a uint64 if mojos flag is set.")
+            return self.amount
+        if not isinstance(self.amount, Decimal):
+            raise ValueError("Amount must be a Decimal if mojos flag is not set.")
+        return uint64(int(self.amount * mojo_per_unit))
+
+
+class AmountParamType(click.ParamType):
+    """
+    A Click parameter type for TX / wallet amounts for both XCH and CAT, and of course mojos.
+    """
+
+    name: str = "CliAmount"  # output type
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> CliAmount:
+        if isinstance(value, CliAmount):  # required by click
+            return value
+        if isinstance(value, uint64):
+            return CliAmount(mojos=True, amount=value)
+        mojos = False  # TODO: Add unit logic
+        if mojos:
+            try:
+                return CliAmount(mojos=True, amount=uint64(int(value)))
+            except ValueError:
+                self.fail("Amount must be positive integer value in mojos", param, ctx)
+        try:
+            if Decimal(value) <= 0:
+                self.fail("Amount must be greater than 0", param, ctx)
+            return CliAmount(mojos=False, amount=Decimal(value))
+        except InvalidOperation:
+            self.fail("Amount must be a decimal dotted value in XCH / CAT (e.g. 0.00005)", param, ctx)
+
+
+@dataclass(frozen=True)
+class CliAddress:
+    """
+    A dataclass for the cli, with the address type and puzzle hash.
+    """
+
+    puzzle_hash: bytes32
+    original_address: str
+    address_type: AddressType
+
+    def validate_address_type(self, address_type: AddressType) -> str:
+        if self.address_type is not address_type:
+            raise ValueError(f"Address must be of type {address_type}")
+        return self.original_address
+
+    def validate_address_type_get_ph(self, address_type: AddressType) -> bytes32:
+        if self.address_type is not address_type:
+            raise ValueError(f"Address must be of type {address_type}")
+        return self.puzzle_hash
+
+
+class AddressParamType(click.ParamType):
+    """
+    A Click parameter type for bech32m encoded addresses, it gives a class with the address type and puzzle hash.
+    """
+
+    name: str = "CliAddress"  # output type
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> CliAddress:
+        if isinstance(value, CliAddress):  # required by click
+            return value
+        try:
+            hrp, b32data = bech32_decode(value)
+            if hrp in ["xch", "txch"]:  # I hate having to load the config here
+                expected_prefix = ctx.obj.get("expected_prefix") if ctx else None
+                if expected_prefix is None:
+                    from chia.util.config import load_config
+                    from chia.util.default_root import DEFAULT_ROOT_PATH
+
+                    root_path = ctx.obj["root_path"] if ctx is not None else DEFAULT_ROOT_PATH
+                    config = load_config(root_path, "config.yaml")
+                    expected_prefix = selected_network_address_prefix(config)
+                    if ctx is not None:
+                        ctx.obj["expected_prefix"] = expected_prefix
+                # now that we have the expected prefix, we can validate the address
+                if hrp == expected_prefix:
+                    addr_type = AddressType.XCH
+                else:
+                    self.fail(f"Unexpected Address Prefix: {hrp}, are you sure its for the right network?", param, ctx)
+            else:
+                addr_type = AddressType(hrp)
+            return CliAddress(puzzle_hash=decode_puzzle_hash(value), address_type=addr_type, original_address=value)
+        except ValueError:
+            self.fail("Address must be a valid bech32m address", param, ctx)
+
+
+class Bytes32ParamType(click.ParamType):
+    """
+    A Click parameter type for bytes32 hex strings, with or without the 0x prefix.
+    """
+
+    name: str = "bytes32"  # output type
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> bytes32:
+        if isinstance(value, bytes32):  # required by click
+            return value
+        try:
+            return bytes32.from_hexstr(value)
+        except ValueError:
+            self.fail("Value must be a valid bytes32 hex string like a coin id or puzzle hash", param, ctx)
+
+
+# These are what we use in click decorators
+TRANSACTION_FEE = TransactionFeeParamType()
+AMOUNT_TYPE = AmountParamType()
+ADDRESS_TYPE = AddressParamType()
+BYTES32_TYPE = Bytes32ParamType()
path: chia/cmds/param_types.py
text: @author:
So what should I do 

@user1:
I would personally just `Bytes32ParamType()` where i need it.  It doesn't seem like the extra layer adds much value. 
 Mostly I was trying to find an explanation for the pattern they used.

diff_hunk: @@ -0,0 +1,166 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from decimal import Decimal, InvalidOperation
+from typing import Any, Optional, Union
+
+import click
+
+from chia.cmds.units import units
+from chia.types.blockchain_format.sized_bytes import bytes32
+from chia.util.bech32m import bech32_decode, decode_puzzle_hash
+from chia.util.config import selected_network_address_prefix
+from chia.util.ints import uint64
+from chia.wallet.util.address_type import AddressType
+
+
+class TransactionFeeParamType(click.ParamType):
+    """
+    A Click parameter type for transaction fees, which can be specified in XCH or mojos.
+    """
+
+    name: str = "uint64"  # output type
+    value_limit: Decimal = Decimal(0.5)
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> uint64:
+        if isinstance(value, uint64):  # required by click
+            return value
+        mojos = False  # TODO: Add unit logic
+        if mojos:
+            try:
+                return uint64(int(value))
path: chia/cmds/param_types.py
text: @user1:
Check for any other redundant conversions.

```suggestion
                return uint64(value)
```

@author:
removed extra conversions

diff_hunk: @@ -0,0 +1,166 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from decimal import Decimal, InvalidOperation
+from typing import Any, Optional, Union
+
+import click
+
+from chia.cmds.units import units
+from chia.types.blockchain_format.sized_bytes import bytes32
+from chia.util.bech32m import bech32_decode, decode_puzzle_hash
+from chia.util.config import selected_network_address_prefix
+from chia.util.ints import uint64
+from chia.wallet.util.address_type import AddressType
+
+
+class TransactionFeeParamType(click.ParamType):
+    """
+    A Click parameter type for transaction fees, which can be specified in XCH or mojos.
+    """
+
+    name: str = "uint64"  # output type
+    value_limit: Decimal = Decimal(0.5)
+
+    def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> uint64:
+        if isinstance(value, uint64):  # required by click
+            return value
+        mojos = False  # TODO: Add unit logic
+        if mojos:
+            try:
+                return uint64(int(value))
+            except ValueError:
+                self.fail("Fee must be positive integer value in mojos", param, ctx)
+        try:
+            d_value = Decimal(value)
+            if 0 > d_value:
+                self.fail("Fee can not be negative", param, ctx)
+            if self.value_limit != Decimal(0) and d_value > self.value_limit:
+                self.fail(f"Fee must be in the range 0 to {self.value_limit}", param, ctx)
+            return uint64(d_value * units["chia"])
path: chia/cmds/param_types.py
text: @user1:
It looks like we don't have any check for excess decimal places.  We should check that here and for any other amounts.

@author:
can you clarify what you mean?

@user1:
`0.0000000000000000000000000000000000000001` isn't a valid XCH amount for chia.

@author:
gotcha, thanks!

@author:
added

diff_hunk: @@ -0,0 +1,166 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from decimal import Decimal, InvalidOperation
+from typing import Any, Optional, Union
+
+import click
+
+from chia.cmds.units import units
+from chia.types.blockchain_format.sized_bytes import bytes32
+from chia.util.bech32m import bech32_decode, decode_puzzle_hash
+from chia.util.config import selected_network_address_prefix
+from chia.util.ints import uint64
+from chia.wallet.util.address_type import AddressType
+
+
+class TransactionFeeParamType(click.ParamType):
+    """
+    A Click parameter type for transaction fees, which can be specified in XCH or mojos.
+    """
+
+    name: str = "uint64"  # output type
+    value_limit: Decimal = Decimal(0.5)
path: chia/cmds/param_types.py
text: @user1:
We should have a habit of never feeding floats to `Decimal`.  Check for this elsewhere as well.

```pycon
>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> decimal.Decimal("0.1")
Decimal('0.1')
```

```suggestion
    value_limit: Decimal = Decimal("0.5")
```


@author:
checked and changed