Repository: Chia-Network/chia-blockchain

Issue:
[Bug] missing output filename causes exception, but still creates offer and does not show to user
### What happened?

```
~/chia-blockchain$ chia wallet make_offer -f 1849xxxx -o 2:2 -r 1:0.0000001 -p /home/jm -m 0.000000000005
Creating Offer
--------------

OFFERING:
  - 2 Chia Holiday 2021 Token (2000 mojos)
REQUESTING:
  - 0.0000001 XCH (100000 mojos)
Confirm (y/n): y
Exception from 'wallet' [Errno 21] Is a directory: '/home/jm'
```

repeat the command again
```
Exception from 'wallet' {'error': "Error creating offer: Can't make this transaction at the moment. Waiting for the change from the previous transaction.", 'success': False}
```

able to recover offer with `chia wallet get_offers -id <id> -p foo.offer`


### Version

1.3.1

### What platform are you using?

Linux

### What ui mode are you using?

CLI

### Relevant log output

_No response_


Discussion:
Repro'd against 1.6.0
This issue has not been updated in 14 days and is now flagged as stale. If this issue is still affecting you and in need of further review, please comment on it with an update to keep it from auto closing in 7 days.
This issue was automatically closed because it has been flagged as stale, and subsequently passed 7 days with no further activity from the submitter or watchers.

Resolving patch:
diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py
--- a/chia/cmds/wallet.py
+++ b/chia/cmds/wallet.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import asyncio
+import pathlib
 from decimal import Decimal
 from typing import List, Optional, Sequence
 
@@ -443,7 +444,13 @@ def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str
     help="A wallet id of an asset to receive and the amount you wish to receive (formatted like wallet_id:amount)",
     multiple=True,
 )
-@click.option("-p", "--filepath", help="The path to write the generated offer file to", required=True)
+@click.option(
+    "-p",
+    "--filepath",
+    help="The path to write the generated offer file to",
+    required=True,
+    type=click.Path(dir_okay=False, writable=True, path_type=pathlib.Path),
+)
 @click.option(
     "-m", "--fee", help="A fee to add to the offer when it gets taken, in XCH", default="0", show_default=True
 )
@@ -459,7 +466,7 @@ def make_offer_cmd(
     fingerprint: int,
     offer: Sequence[str],
     request: Sequence[str],
-    filepath: str,
+    filepath: pathlib.Path,
     fee: str,
     reuse: bool,
     override: bool,
diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py
--- a/chia/cmds/wallet_funcs.py
+++ b/chia/cmds/wallet_funcs.py
@@ -413,7 +413,7 @@ async def make_offer(
     d_fee: Decimal,
     offers: Sequence[str],
     requests: Sequence[str],
-    filepath: str,
+    filepath: pathlib.Path,
     reuse_puzhash: Optional[bool],
 ) -> None:
     async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
@@ -550,23 +550,24 @@ async def make_offer(
 
                 cli_confirm("Confirm (y/n): ", "Not creating offer...")
 
-                offer, trade_record = await wallet_client.create_offer_for_ids(
-                    offer_dict,
-                    driver_dict=driver_dict,
-                    fee=fee,
-                    tx_config=CMDTXConfigLoader(
-                        reuse_puzhash=reuse_puzhash,
-                    ).to_tx_config(units["chia"], config, fingerprint),
-                )
-                if offer is not None:
-                    with open(pathlib.Path(filepath), "w") as file:
-                        file.write(offer.to_bech32())
-                    print(f"Created offer with ID {trade_record.trade_id}")
-                    print(
-                        f"Use chia wallet get_offers --id " f"{trade_record.trade_id} -f {fingerprint} to view status"
+                with filepath.open(mode="w") as file:
+                    offer, trade_record = await wallet_client.create_offer_for_ids(
+                        offer_dict,
+                        driver_dict=driver_dict,
+                        fee=fee,
+                        tx_config=CMDTXConfigLoader(
+                            reuse_puzhash=reuse_puzhash,
+                        ).to_tx_config(units["chia"], config, fingerprint),
                     )
-                else:
-                    print("Error creating offer")
+                    if offer is not None:
+                        file.write(offer.to_bech32())
+                        print(f"Created offer with ID {trade_record.trade_id}")
+                        print(
+                            f"Use chia wallet get_offers --id "
+                            f"{trade_record.trade_id} -f {fingerprint} to view status"
+                        )
+                    else:
+                        print("Error creating offer")
 
 
 def timestamp_to_time(timestamp: int) -> str:


Test patch:
diff --git a/tests/cmds/wallet/test_wallet.py b/tests/cmds/wallet/test_wallet.py
--- a/tests/cmds/wallet/test_wallet.py
+++ b/tests/cmds/wallet/test_wallet.py
@@ -4,6 +4,7 @@
 from typing import Any, Dict, List, Optional, Tuple, Union, cast
 
 import pkg_resources
+import pytest
 from chia_rs import Coin, G2Element
 
 from chia.server.outbound_message import NodeType
@@ -627,6 +628,59 @@ async def set_cat_name(self, wallet_id: int, name: str) -> None:
     test_rpc_clients.wallet_rpc_client.check_log(expected_calls)
 
 
+def test_make_offer_bad_filename(
+    capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path], tmp_path: Path
+) -> None:
+    _, root_dir = get_test_cli_clients
+
+    request_cat_id = get_bytes32(2)
+    request_nft_id = get_bytes32(2)
+    request_nft_addr = encode_puzzle_hash(request_nft_id, "nft")
+    # we offer xch and a random cat via wallet id and request a random cat, nft via coin and tail
+    command_args_dir = [
+        "wallet",
+        "make_offer",
+        FINGERPRINT_ARG,
+        f"-p{str(tmp_path)}",
+        "--reuse",
+        "-m1",
+        "--offer",
+        "1:10",
+        "--offer",
+        "3:100",
+        "--request",
+        f"{request_cat_id.hex()}:10",
+        "--request",
+        f"{request_nft_addr}:1",
+    ]
+
+    test_file: Path = tmp_path / "test.offer"
+    test_file.touch(mode=0o400)
+
+    command_args_unwritable = [
+        "wallet",
+        "make_offer",
+        FINGERPRINT_ARG,
+        f"-p{str(test_file)}",
+        "--reuse",
+        "-m1",
+        "--offer",
+        "1:10",
+        "--offer",
+        "3:100",
+        "--request",
+        f"{request_cat_id.hex()}:10",
+        "--request",
+        f"{request_nft_addr}:1",
+    ]
+
+    with pytest.raises(AssertionError, match=r".*Invalid value for '-p' / '--filepath.*is a directory.*"):
+        run_cli_command_and_assert(capsys, root_dir, command_args_dir, [""])
+
+    with pytest.raises(AssertionError, match=r".*Invalid value for '-p' / '--filepath.*is not writable.*"):
+        run_cli_command_and_assert(capsys, root_dir, command_args_unwritable, [""])
+
+
 def test_make_offer(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path], tmp_path: Path) -> None:
     test_rpc_clients, root_dir = get_test_cli_clients