diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 2d4ecfcdaf23..7a97ff23cde3 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -96,6 +96,12 @@ def get_transaction_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: in default=False, help="Reverse the transaction ordering", ) +@click.option( + "--clawback", + is_flag=True, + default=False, + help="Only show clawback transactions", +) def get_transactions_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -106,7 +112,8 @@ def get_transactions_cmd( paginate: Optional[bool], sort_key: SortKey, reverse: bool, -) -> None: + clawback: bool, +) -> None: # pragma: no cover extra_params = { "id": id, "verbose": verbose, @@ -115,6 +122,7 @@ def get_transactions_cmd( "limit": limit, "sort_key": sort_key, "reverse": reverse, + "clawback": clawback, } import asyncio @@ -185,6 +193,13 @@ def get_transactions_cmd( is_flag=True, default=False, ) +@click.option( + "--clawback_time", + help="The seconds that the recipient needs to wait to claim the fund." + " A positive number will enable the Clawback features.", + type=int, + default=0, +) def send_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -198,7 +213,8 @@ def send_cmd( max_coin_amount: str, coins_to_exclude: Tuple[str], reuse: bool, -) -> None: + clawback_time: int, +) -> None: # pragma: no cover extra_params = { "id": id, "amount": amount, @@ -210,6 +226,7 @@ def send_cmd( "max_coin_amount": max_coin_amount, "exclude_coin_ids": list(coins_to_exclude), "reuse_puzhash": True if reuse else None, + "clawback_time": clawback_time, } import asyncio @@ -274,6 +291,40 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id, fingerprint: int, new_ad asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_address)) +@wallet_cmd.command( + "clawback", + help="Claim or revert a Clawback transaction." + " The wallet will automatically detect if you are able to revert or claim.", +) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@click.option("-i", "--id", help="Id of the wallet to use", type=int, default=1, show_default=True, required=True) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int) +@click.option( + "-ids", + "--tx_ids", + help="IDs of the Clawback transactions you want to revert or claim. Separate multiple IDs by comma (,).", + type=str, + default="", + 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 +) +def clawback(wallet_rpc_port: Optional[int], id, fingerprint: int, tx_ids: str, fee: str) -> None: # pragma: no cover + extra_params = {"id": id, "tx_ids": tx_ids, "fee": fee} + import asyncio + + from .wallet_funcs import spend_clawback + + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, spend_clawback)) + + @wallet_cmd.command("delete_unconfirmed_transactions", help="Deletes all unconfirmed transactions for this wallet ID") @click.option( "-wp", diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 54ab0267e2c9..2fbb174639e2 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -28,9 +28,12 @@ from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.address_type import AddressType, ensure_valid_address -from chia.wallet.util.transaction_type import TransactionType +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_TRANSACTION_TYPES, TransactionType from chia.wallet.util.wallet_types import WalletType from chia.wallet.vc_wallet.vc_store import VCProofs +from chia.wallet.wallet_coin_store import GetCoinRecords CATNameResolver = Callable[[bytes32], Awaitable[Optional[Tuple[Optional[uint32], str]]]] @@ -41,6 +44,9 @@ TransactionType.FEE_REWARD: "rewarded", TransactionType.INCOMING_TRADE: "received in trade", TransactionType.OUTGOING_TRADE: "sent in trade", + TransactionType.INCOMING_CLAWBACK_RECEIVE: "received in clawback as recipient", + TransactionType.INCOMING_CLAWBACK_SEND: "received in clawback as sender", + TransactionType.OUTGOING_CLAWBACK: "claim/clawback", } @@ -48,7 +54,14 @@ def transaction_description_from_type(tx: TransactionRecord) -> str: return transaction_type_descriptions.get(TransactionType(tx.type), "(unknown reason)") -def print_transaction(tx: TransactionRecord, verbose: bool, name, address_prefix: str, mojo_per_unit: int) -> None: +def print_transaction( + tx: TransactionRecord, + verbose: bool, + name, + address_prefix: str, + mojo_per_unit: int, + coin_record: Optional[Dict[str, Any]] = None, +) -> None: # pragma: no cover if verbose: print(tx) else: @@ -60,6 +73,13 @@ def print_transaction(tx: TransactionRecord, verbose: bool, name, address_prefix print(f"Amount {description}: {chia_amount} {name}") print(f"To address: {to_address}") print("Created at:", datetime.fromtimestamp(tx.created_at_time).strftime("%Y-%m-%d %H:%M:%S")) + if coin_record is not None: + print( + "Recipient claimable time:", + datetime.fromtimestamp(tx.created_at_time + coin_record["metadata"]["time_lock"]).strftime( + "%Y-%m-%d %H:%M:%S" + ), + ) print("") @@ -142,7 +162,7 @@ async def get_transaction(args: dict, wallet_client: WalletRpcClient, fingerprin ) -async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: +async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover wallet_id = args["id"] paginate = args["paginate"] if paginate is None: @@ -151,9 +171,15 @@ async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerpri limit = args["limit"] sort_key = args["sort_key"] reverse = args["reverse"] - + type_filter = ( + None + if not args["clawback"] + else TransactionTypeFilter.include( + [TransactionType.INCOMING_CLAWBACK_RECEIVE, TransactionType.INCOMING_CLAWBACK_SEND] + ) + ) txs: List[TransactionRecord] = await wallet_client.get_transactions( - wallet_id, start=offset, end=(offset + limit), sort_key=sort_key, reverse=reverse + wallet_id, start=offset, end=(offset + limit), sort_key=sort_key, reverse=reverse, type_filter=type_filter ) config = load_config(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME) @@ -179,12 +205,20 @@ async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerpri for j in range(0, num_per_screen): if i + j >= len(txs): break + coin_record: Optional[Dict[str, Any]] = None + if txs[i + j].type in CLAWBACK_TRANSACTION_TYPES: + coin_record = ( + await wallet_client.get_coin_records( + GetCoinRecords(coin_id_filter=HashFilter.include([txs[i + j].additions[0].name()])) + ) + )["coin_records"][0] print_transaction( txs[i + j], verbose=(args["verbose"] > 0), name=name, address_prefix=address_prefix, mojo_per_unit=mojo_per_unit, + coin_record=coin_record, ) if i + num_per_screen >= len(txs): return None @@ -201,7 +235,7 @@ def check_unusual_transaction(amount: Decimal, fee: Decimal): return fee >= amount -async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: +async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover wallet_id: int = args["id"] amount = Decimal(args["amount"]) fee = Decimal(args["fee"]) @@ -212,6 +246,7 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> exclude_coin_ids: List[str] = args["exclude_coin_ids"] memo = args["memo"] reuse_puzhash = args["reuse_puzhash"] + clawback_time_lock = args["clawback_time"] if memo is None: memos = None else: @@ -226,7 +261,9 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> 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 try: typ = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client) mojo_per_unit = get_mojo_per_unit(typ) @@ -250,6 +287,11 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> final_max_coin_amount, exclude_coin_ids=exclude_coin_ids, reuse_puzhash=reuse_puzhash, + puzzle_decorator_override=[ + {"decorator": PuzzleDecoratorType.CLAWBACK.name, "clawback_timelock": clawback_time_lock} + ] + if clawback_time_lock > 0 + else None, ) elif typ == WalletType.CAT: print("Submitting transaction...") @@ -1225,6 +1267,21 @@ async def sign_message(args: Dict, wallet_client: WalletRpcClient, fingerprint: print(f"Signing Mode: {signing_mode}") +async def spend_clawback(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover + tx_ids = [] + for tid in args["tx_ids"].split(","): + tx_ids.append(bytes32.from_hexstr(tid)) + if len(tx_ids) == 0: + print("Transaction ID is required.") + return + fee = Decimal(args["fee"]) + if fee < 0: + print("Batch fee cannot be negative.") + return + response = await wallet_client.spend_clawback_coins(tx_ids, int(fee * units["chia"])) + print(str(response)) + + async def mint_vc(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover config = load_config(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME) vc_record, txs = await wallet_client.vc_mint( diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 1a00cc9e12ab..f920ae8332e1 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -75,7 +75,7 @@ from chia.wallet.util.compute_hints import compute_coin_hints from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter -from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import CoinType, WalletType from chia.wallet.vc_wallet.vc_store import VCProofs @@ -899,12 +899,11 @@ async def get_transactions(self, request: Dict) -> EndpointResult: ) tx_list = [] # Format for clawback transactions - clawback_types = {TransactionType.INCOMING_CLAWBACK_RECEIVE.value, TransactionType.INCOMING_CLAWBACK_SEND.value} for tr in transactions: try: tx = (await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config) tx_list.append(tx) - if tx["type"] not in clawback_types: + if tx["type"] not in CLAWBACK_TRANSACTION_TYPES: continue coin: Coin = tr.additions[0] record: Optional[WalletCoinRecord] = await self.service.wallet_state_manager.coin_store.get_coin_record( @@ -914,8 +913,8 @@ async def get_transactions(self, request: Dict) -> EndpointResult: tx["metadata"] = record.parsed_metadata().to_json_dict() tx["metadata"]["coin_id"] = coin.name().hex() tx["metadata"]["spent"] = record.spent - except Exception as e: - log.error(f"Failed to get transaction {tr.name}: {e}") + except Exception: + log.exception(f"Failed to get transaction {tr.name}.") return { "transactions": tx_list, "wallet_id": wallet_id, diff --git a/chia/wallet/puzzles/clawback/metadata.py b/chia/wallet/puzzles/clawback/metadata.py index d16bb04268a5..96557b66a3eb 100644 --- a/chia/wallet/puzzles/clawback/metadata.py +++ b/chia/wallet/puzzles/clawback/metadata.py @@ -32,7 +32,7 @@ class ClawbackVersion(IntEnum): @streamable @dataclass(frozen=True) class AutoClaimSettings(Streamable): - enabled: bool = True + enabled: bool = False tx_fee: uint64 = uint64(0) min_amount: uint64 = uint64(0) batch_size: uint16 = uint16(50) diff --git a/chia/wallet/util/transaction_type.py b/chia/wallet/util/transaction_type.py index b5d1ae927906..eff5090c0c8f 100644 --- a/chia/wallet/util/transaction_type.py +++ b/chia/wallet/util/transaction_type.py @@ -13,3 +13,9 @@ class TransactionType(IntEnum): INCOMING_CLAWBACK_RECEIVE = 6 INCOMING_CLAWBACK_SEND = 7 OUTGOING_CLAWBACK = 8 + + +CLAWBACK_TRANSACTION_TYPES = { + TransactionType.INCOMING_CLAWBACK_SEND.value, + TransactionType.INCOMING_CLAWBACK_RECEIVE.value, +} diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 96477cfbba04..293f8fd5fc8a 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -82,7 +82,7 @@ from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager from chia.wallet.util.query_filter import HashFilter -from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType from chia.wallet.util.wallet_sync_utils import ( PeerRequestException, fetch_coin_spend_for_coin_state, @@ -1442,10 +1442,7 @@ async def _add_coin_states( await self.interested_store.remove_interested_coin_id(coin_state.coin.name()) confirmed_tx_records: List[TransactionRecord] = [] for tx_record in all_unconfirmed: - if tx_record.type in { - TransactionType.INCOMING_CLAWBACK_SEND.value, - TransactionType.INCOMING_CLAWBACK_RECEIVE.value, - }: + if tx_record.type in CLAWBACK_TRANSACTION_TYPES: for add_coin in tx_record.additions: if add_coin == coin_state.coin: confirmed_tx_records.append(tx_record) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 8f236add4abc..767b16320f38 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -427,6 +427,7 @@ async def test_wallet_clawback_claim_manual( wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet wallet_1 = wallet_node_2.wallet_state_manager.main_wallet + api_0 = WalletRpcApi(wallet_node) api_1 = WalletRpcApi(wallet_node_2) if trusted: wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} @@ -486,6 +487,20 @@ async def test_wallet_clawback_claim_manual( await time_out_assert(10, wallet.get_confirmed_balance, 3999999999500) await time_out_assert(10, wallet_1.get_confirmed_balance, 4000000000500) + txs = await api_0.get_transactions( + dict( + type_filter={ + "values": [ + TransactionType.INCOMING_CLAWBACK_SEND.value, + ], + "mode": 1, + }, + wallet_id=1, + ) + ) + assert len(txs["transactions"]) == 1 + assert txs["transactions"][0]["confirmed"] + @pytest.mark.parametrize( "trusted", [True, False],