diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index ee7582b0d600..01af927e5e00 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -1225,7 +1225,7 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty amount=uint64(4), inner_address=addr_1, memos=["the cat memo"], - additions=[], + additions=[Addition(amount=uint64(0), puzzle_hash=bytes32.zeros)], ), tx_config=wallet_environments.tx_config, ) @@ -1247,7 +1247,7 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty await env_0.rpc_client.cat_spend( CATSpend( wallet_id=cat_0_id, - additions=[], + additions=[Addition(amount=uint64(0), puzzle_hash=bytes32.zeros)], extra_delta="1", ), tx_config=wallet_environments.tx_config, diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index 045aed5c299d..d68d79b76e70 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -1468,6 +1468,22 @@ class Addition(Streamable): memos: list[str] | None = None +def cat_discrepancy_validation( + extra_delta: str | None, tail_reveal: bytes | None, tail_solution: bytes | None +) -> tuple[int, Program, Program] | None: + if extra_delta is None and tail_reveal is None and tail_solution is None: + return None + elif None in {extra_delta, tail_reveal, tail_solution}: + raise ValueError('Must specify "extra_delta", "tail_reveal" and "tail_solution" together.') + else: + # Curious that mypy doesn't see the elif and know that none of these are None + return ( + int(extra_delta), # type: ignore[arg-type] + Program.from_bytes(tail_reveal), # type: ignore[arg-type] + Program.from_bytes(tail_solution), # type: ignore[arg-type] + ) + + @streamable @dataclass(frozen=True, kw_only=True) class CATSpend(TransactionEndpointRequest): @@ -1493,17 +1509,7 @@ def __post_init__(self) -> None: @property def cat_discrepancy(self) -> tuple[int, Program, Program] | None: - if self.extra_delta is None and self.tail_reveal is None and self.tail_solution is None: - return None - elif None in {self.extra_delta, self.tail_reveal, self.tail_solution}: - raise ValueError('Must specify "extra_delta", "tail_reveal" and "tail_solution" together.') - else: - # Curious that mypy doesn't see the elif and know that none of these are None - return ( - int(self.extra_delta), # type: ignore[arg-type] - Program.from_bytes(self.tail_reveal), # type: ignore[arg-type] - Program.from_bytes(self.tail_solution), # type: ignore[arg-type] - ) + return cat_discrepancy_validation(self.extra_delta, self.tail_reveal, self.tail_solution) @streamable @@ -1910,6 +1916,14 @@ class CreateSignedTransaction(TransactionEndpointRequest): morph_bytes: bytes | None = None coin_announcements: list[CSTCoinAnnouncement] = field(default_factory=list) puzzle_announcements: list[CSTPuzzleAnnouncement] = field(default_factory=list) + # cat specific + extra_delta: str | None = None # str to support negative ints :( + tail_reveal: bytes | None = None + tail_solution: bytes | None = None + # Technically this value was meant to support many types here + # However, only one is supported right now and there are no plans to extend + # So, as a slight hack, we'll specify that only Clawback is supported + puzzle_decorator: list[ClawbackPuzzleDecoratorOverride] | None = None def __post_init__(self) -> None: if len(self.additions) < 1: @@ -1943,6 +1957,10 @@ def asserted_puzzle_announcements(self) -> tuple[AssertPuzzleAnnouncement, ...]: for pa in self.puzzle_announcements ) + @property + def cat_discrepancy(self) -> tuple[int, Program, Program] | None: + return cat_discrepancy_validation(self.extra_delta, self.tail_reveal, self.tail_solution) + @streamable @dataclass(frozen=True) diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index 7feeea0cd750..f5a19d25c144 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -82,7 +82,7 @@ from chia.wallet.trading.offer import Offer, OfferSummary from chia.wallet.transaction_record import TransactionRecord from chia.wallet.uncurried_puzzle import uncurry_puzzle -from chia.wallet.util.address_type import AddressType, is_valid_address +from chia.wallet.util.address_type import AddressType, ensure_valid_address, is_valid_address from chia.wallet.util.clvm_streamable import json_serialize_with_clvm_streamable from chia.wallet.util.compute_hints import compute_spend_hints_and_additions from chia.wallet.util.compute_memos import compute_memos @@ -104,6 +104,7 @@ from chia.wallet.wallet_node import WalletNode, get_wallet_db_path from chia.wallet.wallet_protocol import WalletProtocol from chia.wallet.wallet_request_types import ( + Addition, AddKey, AddKeyResponse, ApplySignatures, @@ -1507,24 +1508,27 @@ async def send_transaction( action_scope: WalletActionScope, extra_conditions: tuple[Condition, ...] = tuple(), ) -> SendTransactionResponse: - wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Wallet) - - # TODO: Add support for multiple puzhash/amount/memo sets - selected_network = self.service.config["selected_network"] - expected_prefix = self.service.config["network_overrides"]["config"][selected_network]["address_prefix"] - if request.address[0 : len(expected_prefix)] != expected_prefix: - raise ValueError("Unexpected Address Prefix") - - await wallet.generate_signed_transaction( - [request.amount], - [decode_puzzle_hash(request.address)], - action_scope, - request.fee, - memos=[[mem.encode("utf-8") for mem in request.memos]], - puzzle_decorator_override=[request.puzzle_decorator[0].to_json_dict()] - if request.puzzle_decorator is not None - else None, - extra_conditions=extra_conditions, + # opportunity to raise + self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Wallet) + await self.create_signed_transaction( + CreateSignedTransaction( + additions=[ + Addition( + request.amount, + decode_puzzle_hash( + ensure_valid_address( + request.address, allowed_types={AddressType.XCH}, config=self.service.config + ) + ), + request.memos, + ) + ], + wallet_id=request.wallet_id, + fee=request.fee, + puzzle_decorator=request.puzzle_decorator, + ).json_serialize_for_transport(action_scope.config.tx_config, extra_conditions, ConditionValidTimes()), + hold_lock=False, + action_scope_override=action_scope, ) # Transaction may not have been included in the mempool yet. Use get_transaction to check. @@ -2005,52 +2009,36 @@ async def cat_spend( extra_conditions: tuple[Condition, ...] = tuple(), hold_lock: bool = True, ) -> CATSpendResponse: - wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=CATWallet) - - amounts: list[uint64] = [] - puzzle_hashes: list[bytes32] = [] - memos: list[list[bytes]] = [] - if request.additions is not None: - for addition in request.additions: - if addition.amount > self.service.constants.MAX_COIN_AMOUNT: - raise ValueError(f"Coin amount cannot exceed {self.service.constants.MAX_COIN_AMOUNT}") - amounts.append(addition.amount) - puzzle_hashes.append(addition.puzzle_hash) - if addition.memos is not None: - memos.append([mem.encode("utf-8") for mem in addition.memos]) - else: - # Our __post_init__ guards against these not being None - amounts.append(request.amount) # type: ignore[arg-type] - puzzle_hashes.append(decode_puzzle_hash(request.inner_address)) # type: ignore[arg-type] - if request.memos is not None: - memos.append([mem.encode("utf-8") for mem in request.memos]) - coins: set[Coin] | None = None - if request.coins is not None and len(request.coins) > 0: - coins = set(request.coins) - - if hold_lock: - async with self.service.wallet_state_manager.lock: - await wallet.generate_signed_transaction( - amounts, - puzzle_hashes, - action_scope, - request.fee, - cat_discrepancy=request.cat_discrepancy, - coins=coins, - memos=memos if memos else None, - extra_conditions=extra_conditions, - ) - else: - await wallet.generate_signed_transaction( - amounts, - puzzle_hashes, - action_scope, - request.fee, - cat_discrepancy=request.cat_discrepancy, - coins=coins, - memos=memos if memos else None, - extra_conditions=extra_conditions, - ) + # opportunity to raise + self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=CATWallet) + await self.create_signed_transaction( + CreateSignedTransaction( + additions=request.additions + if request.additions is not None + else [ + Addition( + # Our __post_init__ guards against these not being None + request.amount, # type: ignore[arg-type] + decode_puzzle_hash( + ensure_valid_address( + request.inner_address, # type: ignore[arg-type] + allowed_types={AddressType.XCH}, + config=self.service.config, + ) + ), + request.memos, + ) + ], + wallet_id=request.wallet_id, + fee=request.fee, + coins=request.coins, + extra_delta=request.extra_delta, + tail_reveal=request.tail_reveal, + tail_solution=request.tail_solution, + ).json_serialize_for_transport(action_scope.config.tx_config, extra_conditions, ConditionValidTimes()), + hold_lock=hold_lock, + action_scope_override=action_scope, + ) # tx_endpoint will fill in these default values return CATSpendResponse([], [], transaction=REPLACEABLE_TRANSACTION_RECORD, transaction_id=bytes32.zeros) @@ -3362,6 +3350,10 @@ async def _generate_signed_transaction() -> CreateSignedTransactionsResponse: request.fee, coins=request.coin_set, memos=[memos_0] + [output.memos if output.memos is not None else [] for output in additional_outputs], + puzzle_decorator_override=[dec.to_json_dict() for dec in request.puzzle_decorator] + if request.puzzle_decorator is not None + else None, + cat_discrepancy=request.cat_discrepancy, extra_conditions=( *extra_conditions, *request.asserted_coin_announcements,