diff --git a/chia/_tests/cmds/wallet/test_wallet.py b/chia/_tests/cmds/wallet/test_wallet.py index 86fdd159aabf..238bd5de7b89 100644 --- a/chia/_tests/cmds/wallet/test_wallet.py +++ b/chia/_tests/cmds/wallet/test_wallet.py @@ -29,7 +29,7 @@ from chia.types.blockchain_format.program import Program from chia.types.signing_mode import SigningMode from chia.util.bech32m import encode_puzzle_hash -from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.conditions import Condition, ConditionValidTimes from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus @@ -44,6 +44,7 @@ BalanceResponse, CancelOfferResponse, CATSpendResponse, + ClawbackPuzzleDecoratorOverride, CreateOfferForIDsResponse, DeleteUnconfirmedTransactions, ExtendDerivationIndex, @@ -64,6 +65,7 @@ NFTGetWalletDID, NFTGetWalletDIDResponse, RoyaltyAsset, + SendTransaction, SendTransactionResponse, TakeOfferResponse, TransactionRecordWithMetadata, @@ -333,19 +335,24 @@ def test_send(capsys: object, get_test_cli_clients: tuple[TestRpcClients, Path]) class SendWalletRpcClient(TestWalletRpcClient): async def send_transaction( self, - wallet_id: int, - amount: uint64, - address: str, + request: SendTransaction, tx_config: TXConfig, - fee: uint64 = uint64(0), - memos: Optional[list[str]] = None, - puzzle_decorator_override: Optional[list[dict[str, Union[str, int, bool]]]] = None, - push: bool = True, + extra_conditions: tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), ) -> SendTransactionResponse: self.add_to_log( "send_transaction", - (wallet_id, amount, address, tx_config, fee, memos, puzzle_decorator_override, push, timelock_info), + ( + request.wallet_id, + request.amount, + request.address, + tx_config, + request.fee, + request.memos, + request.puzzle_decorator, + request.push, + timelock_info, + ), ) name = get_bytes32(2) tx_rec = TransactionRecord( @@ -463,7 +470,7 @@ async def cat_spend( ), 500000000000, ["0x6262626262626262626262626262626262626262626262626262626262626262"], - [{"decorator": "CLAWBACK", "clawback_timelock": 60}], + [ClawbackPuzzleDecoratorOverride(decorator="CLAWBACK", clawback_timelock=uint64(60))], True, test_condition_valid_times, ) diff --git a/chia/_tests/pools/test_pool_rpc.py b/chia/_tests/pools/test_pool_rpc.py index ee99e981009f..4ab209b99a28 100644 --- a/chia/_tests/pools/test_pool_rpc.py +++ b/chia/_tests/pools/test_pool_rpc.py @@ -49,6 +49,7 @@ PWJoinPool, PWSelfPool, PWStatus, + SendTransaction, ) from chia.wallet.wallet_rpc_client import WalletRpcClient from chia.wallet.wallet_service import WalletService @@ -591,7 +592,13 @@ async def test_absorb_self( tr: TransactionRecord = ( await client.send_transaction( - 1, uint64(100), encode_puzzle_hash(status.p2_singleton_puzzle_hash, "txch"), DEFAULT_TX_CONFIG + SendTransaction( + wallet_id=uint32(1), + amount=uint64(100), + address=encode_puzzle_hash(status.p2_singleton_puzzle_hash, "txch"), + push=True, + ), + DEFAULT_TX_CONFIG, ) ).transaction diff --git a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py index 802a06b015dd..4fc1d16baf41 100644 --- a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py +++ b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py @@ -44,7 +44,7 @@ from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_interested_store import WalletInterestedStore from chia.wallet.wallet_node import WalletNode -from chia.wallet.wallet_request_types import GetTransactionMemo, PushTX +from chia.wallet.wallet_request_types import GetTransactionMemo, PushTX, SendTransaction from chia.wallet.wallet_state_manager import WalletStateManager @@ -1429,7 +1429,12 @@ async def test_cat_change_detection(wallet_environments: WalletTestFramework, wa cat_amount_0 = uint64(100) cat_amount_1 = uint64(5) - tx = (await env.rpc_client.send_transaction(1, cat_amount_0, addr, wallet_environments.tx_config)).transaction + tx = ( + await env.rpc_client.send_transaction( + SendTransaction(wallet_id=uint32(1), amount=cat_amount_0, address=addr, push=True), + wallet_environments.tx_config, + ) + ).transaction spend_bundle = tx.spend_bundle assert spend_bundle is not None diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index d3936307cb22..428ca7e8e6eb 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -105,6 +105,7 @@ from chia.wallet.wallet_request_types import ( AddKey, CheckDeleteKey, + ClawbackPuzzleDecoratorOverride, CombineCoins, DefaultCAT, DeleteKey, @@ -140,6 +141,7 @@ PushTransactions, PushTX, RoyaltyAsset, + SendTransaction, SetWalletResyncOnStartup, SplitCoins, VerifySignature, @@ -382,24 +384,25 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment addr = encode_puzzle_hash(await action_scope.get_puzzle_hash(wallet_2.wallet_state_manager), "txch") tx_amount = uint64(15600000) with pytest.raises(ValueError): - await client.send_transaction(1, uint64(100000000000000001), addr, DEFAULT_TX_CONFIG) + await client.send_transaction( + SendTransaction(wallet_id=uint32(1), amount=uint64(100000000000000001), address=addr, push=True), + DEFAULT_TX_CONFIG, + ) # Tests sending a basic transaction extra_conditions = (Remark(Program.to(("test", None))),) non_existent_coin = Coin(bytes32.zeros, bytes32.zeros, uint64(0)) tx_no_push = ( await client.send_transaction( - 1, - tx_amount, - addr, - memos=["this is a basic tx"], + SendTransaction( + wallet_id=uint32(1), amount=tx_amount, address=addr, memos=["this is a basic tx"], push=False + ), tx_config=DEFAULT_TX_CONFIG.override( excluded_coin_amounts=[uint64(250000000000)], excluded_coin_ids=[non_existent_coin.name()], reuse_puzhash=True, ), extra_conditions=extra_conditions, - push=False, ) ).transaction response = await client.fetch( @@ -841,12 +844,14 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron wallet_2_puzhash = await action_scope.get_puzzle_hash(wallet_2.wallet_state_manager) tx = ( await wallet_1_rpc.send_transaction( - wallet_id=1, - amount=uint64(500), - address=encode_puzzle_hash(wallet_2_puzhash, "txch"), + SendTransaction( + wallet_id=uint32(1), + amount=uint64(500), + address=encode_puzzle_hash(wallet_2_puzhash, "txch"), + puzzle_decorator=[ClawbackPuzzleDecoratorOverride(decorator="CLAWBACK", clawback_timelock=uint64(5))], + push=True, + ), tx_config=DEFAULT_TX_CONFIG, - fee=uint64(0), - puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) ).transaction clawback_coin_id_1 = tx.additions[0].name() @@ -855,12 +860,14 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron await full_node_api.wait_for_wallet_synced(wallet_node=wallet_2_node, timeout=20) tx = ( await wallet_2_rpc.send_transaction( - wallet_id=1, - amount=uint64(500), - address=encode_puzzle_hash(wallet_1_puzhash, "txch"), + SendTransaction( + wallet_id=uint32(1), + amount=uint64(500), + address=encode_puzzle_hash(wallet_1_puzhash, "txch"), + puzzle_decorator=[ClawbackPuzzleDecoratorOverride(decorator="CLAWBACK", clawback_timelock=uint64(5))], + push=True, + ), tx_config=DEFAULT_TX_CONFIG, - fee=uint64(0), - puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) ).transaction assert tx.spend_bundle is not None @@ -1023,7 +1030,8 @@ async def test_get_transactions(wallet_rpc_environment: WalletRpcTestEnvironment puzhash = await action_scope.get_puzzle_hash(wallet.wallet_state_manager) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) await client.send_transaction( - 1, uint64(1), encode_puzzle_hash(puzhash, "txch"), DEFAULT_TX_CONFIG + SendTransaction(wallet_id=uint32(1), amount=uint64(1), address=encode_puzzle_hash(puzhash, "txch"), push=True), + DEFAULT_TX_CONFIG, ) # Create a pending tx with pytest.raises(ValueError, match="There is no known sort foo"): @@ -1049,7 +1057,12 @@ async def test_get_transactions(wallet_rpc_environment: WalletRpcTestEnvironment async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope: ph_by_addr = await action_scope.get_puzzle_hash(wallet.wallet_state_manager) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) - await client.send_transaction(1, uint64(1), encode_puzzle_hash(ph_by_addr, "txch"), DEFAULT_TX_CONFIG) + await client.send_transaction( + SendTransaction( + wallet_id=uint32(1), amount=uint64(1), address=encode_puzzle_hash(ph_by_addr, "txch"), push=True + ), + DEFAULT_TX_CONFIG, + ) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) tx_for_address = ( await client.get_transactions(GetTransactions(uint32(1), to_address=encode_puzzle_hash(ph_by_addr, "txch"))) @@ -1801,7 +1814,12 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) # Spend half of it back to the same wallet get some spent coins in the wallet - tx = (await client.send_transaction(1, uint64(generated_funds / 2), address, DEFAULT_TX_CONFIG)).transaction + tx = ( + await client.send_transaction( + SendTransaction(wallet_id=uint32(1), amount=uint64(generated_funds / 2), address=address, push=True), + DEFAULT_TX_CONFIG, + ) + ).transaction assert tx.spend_bundle is not None await time_out_assert(20, tx_in_mempool, True, client, tx.name) await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) @@ -2167,7 +2185,11 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn addr = encode_puzzle_hash(ph, "txch") tx_amount = uint64(15600000) await env.full_node.api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) - created_tx = (await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG)).transaction + created_tx = ( + await client.send_transaction( + SendTransaction(wallet_id=uint32(1), amount=tx_amount, address=addr, push=True), DEFAULT_TX_CONFIG + ) + ).transaction await time_out_assert(20, tx_in_mempool, True, client, created_tx.name) assert len(await wallet.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(1)) == 1 @@ -2230,7 +2252,10 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn assert await get_unconfirmed_balance(client, int(wallets[0].id)) == 0 with pytest.raises(ValueError): - await client.send_transaction(wallets[0].id, uint64(100), addr, DEFAULT_TX_CONFIG) + await client.send_transaction( + SendTransaction(wallet_id=uint32(wallets[0].id), amount=uint64(100), address=addr, push=True), + DEFAULT_TX_CONFIG, + ) # Delete all keys await client.delete_all_keys() @@ -2256,7 +2281,11 @@ async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment for tx_amount in tx_amounts: funds -= tx_amount # create coins for tests - tx = (await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG)).transaction + tx = ( + await client.send_transaction( + SendTransaction(wallet_id=uint32(1), amount=tx_amount, address=addr, push=True), DEFAULT_TX_CONFIG + ) + ).transaction spend_bundle = tx.spend_bundle assert spend_bundle is not None for coin in spend_bundle.additions(): @@ -2817,12 +2846,14 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes # Test Clawback resync tx = ( await wc.send_transaction( - wallet_id=1, - amount=uint64(500), - address=address, + SendTransaction( + wallet_id=uint32(1), + amount=uint64(500), + address=address, + puzzle_decorator=[ClawbackPuzzleDecoratorOverride(decorator="CLAWBACK", clawback_timelock=uint64(5))], + push=True, + ), tx_config=DEFAULT_TX_CONFIG, - fee=uint64(0), - puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) ).transaction clawback_coin_id = tx.additions[0].name() @@ -2965,7 +2996,11 @@ async def test_cat_spend_run_tail(wallet_rpc_environment: WalletRpcTestEnvironme ) tx_amount = uint64(100) - tx = (await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG)).transaction + tx = ( + await client.send_transaction( + SendTransaction(wallet_id=uint32(1), amount=tx_amount, address=addr, push=True), DEFAULT_TX_CONFIG + ) + ).transaction transaction_id = tx.name spend_bundle = tx.spend_bundle assert spend_bundle is not None diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 50c4f17c0ef3..034bf18a6e5b 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -45,6 +45,7 @@ from chia.wallet.wallet_coin_store import GetCoinRecords from chia.wallet.wallet_request_types import ( CATSpendResponse, + ClawbackPuzzleDecoratorOverride, DeleteNotifications, DeleteUnconfirmedTransactions, DIDFindLostDID, @@ -72,6 +73,7 @@ NFTSetNFTDID, NFTTransferNFT, RoyaltyAsset, + SendTransaction, SendTransactionResponse, SignMessageByAddress, SignMessageByAddressResponse, @@ -335,7 +337,7 @@ async def send( ) -> list[TransactionRecord]: async with get_wallet_client(root_path, wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if memo is None: - memos = None + memos = [] else: memos = [memo] @@ -364,23 +366,29 @@ async def send( if typ == WalletType.STANDARD_WALLET: print("Submitting transaction...") res: Union[CATSpendResponse, SendTransactionResponse] = await wallet_client.send_transaction( - wallet_id, - final_amount, - address.original_address, - CMDTXConfigLoader( + SendTransaction( + wallet_id=uint32(wallet_id), + amount=final_amount, + address=address.original_address, + fee=fee, + memos=memos, + push=push, + puzzle_decorator=( + [ + ClawbackPuzzleDecoratorOverride( + PuzzleDecoratorType.CLAWBACK.name, clawback_timelock=uint64(clawback_time_lock) + ) + ] + if clawback_time_lock > 0 + else None + ), + ), + tx_config=CMDTXConfigLoader( min_coin_amount=min_coin_amount, max_coin_amount=max_coin_amount, excluded_coin_ids=list(excluded_coin_ids), reuse_puzhash=reuse_puzhash, ).to_tx_config(mojo_per_unit, config, fingerprint), - fee, - memos, - puzzle_decorator_override=( - [{"decorator": PuzzleDecoratorType.CLAWBACK.name, "clawback_timelock": clawback_time_lock}] - if clawback_time_lock > 0 - else None - ), - push=push, timelock_info=condition_valid_times, ) elif typ in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}: diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index 74b616a34bff..5983b99ddbbc 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -15,7 +15,7 @@ from chia.types.blockchain_format.program import Program from chia.util.byte_types import hexstr_to_bytes from chia.util.streamable import Streamable, streamable -from chia.wallet.conditions import Condition, ConditionValidTimes +from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_to_json_dicts from chia.wallet.nft_wallet.nft_info import NFTInfo from chia.wallet.notification_store import Notification from chia.wallet.signer_protocol import ( @@ -30,6 +30,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable +from chia.wallet.util.puzzle_decorator_type import PuzzleDecoratorType from chia.wallet.util.query_filter import TransactionTypeFilter from chia.wallet.util.tx_config import TXConfig from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord @@ -1100,7 +1101,7 @@ def json_serialize_for_transport( return { **tx_config.to_json_dict(), **timelock_info.to_json_dict(), - "extra_conditions": [condition.to_json_dict() for condition in extra_conditions], + "extra_conditions": conditions_to_json_dicts(extra_conditions), **self.to_json_dict(_avoid_ban=True), } @@ -1112,6 +1113,39 @@ class TransactionEndpointResponse(Streamable): transactions: list[TransactionRecord] +# utility for SendTransaction +@streamable +@dataclass(frozen=True) +class ClawbackPuzzleDecoratorOverride(Streamable): + decorator: str + clawback_timelock: uint64 + + def __post_init__(self) -> None: + if self.decorator != PuzzleDecoratorType.CLAWBACK.name: + raise ValueError("Invalid clawback puzzle decorator override specified") + super().__post_init__() + + +@streamable +@dataclass(frozen=True) +class SendTransaction(TransactionEndpointRequest): + wallet_id: uint32 = field(default_factory=default_raise) + amount: uint64 = field(default_factory=default_raise) + address: str = field(default_factory=default_raise) + memos: list[str] = field(default_factory=list) + # 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: Optional[list[ClawbackPuzzleDecoratorOverride]] = None + + +@streamable +@dataclass(frozen=True) +class SendTransactionResponse(TransactionEndpointResponse): + transaction: TransactionRecord + transaction_id: bytes32 + + @streamable @dataclass(frozen=True) class PushTransactions(TransactionEndpointRequest): @@ -1547,11 +1581,6 @@ class VCRevokeResponse(TransactionEndpointResponse): # TODO: The section below needs corresponding request types # TODO: The section below should be added to the API (currently only for client) -@streamable -@dataclass(frozen=True) -class SendTransactionResponse(TransactionEndpointResponse): - transaction: TransactionRecord - transaction_id: bytes32 @streamable diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index 3c687a8f6ea4..351a08cea2aa 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -242,6 +242,8 @@ PWSelfPoolResponse, PWStatus, PWStatusResponse, + SendTransaction, + SendTransactionResponse, SetWalletResyncOnStartup, SignMessageByAddress, SignMessageByAddressResponse, @@ -1588,51 +1590,39 @@ async def get_next_address(self, request: GetNextAddress) -> GetNextAddressRespo ) @tx_endpoint(push=True) + @marshal async def send_transaction( self, - request: dict[str, Any], + request: SendTransaction, action_scope: WalletActionScope, extra_conditions: tuple[Condition, ...] = tuple(), - ) -> EndpointResult: + ) -> SendTransactionResponse: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before sending transactions") - wallet_id = uint32(request["wallet_id"]) - wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=Wallet) + wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Wallet) # TODO: Add support for multiple puzhash/amount/memo sets - if not isinstance(request["amount"], int) or not isinstance(request["fee"], int): - raise ValueError("An integer amount or fee is required (too many decimals)") - amount: uint64 = uint64(request["amount"]) - address = request["address"] selected_network = self.service.config["selected_network"] expected_prefix = self.service.config["network_overrides"]["config"][selected_network]["address_prefix"] - if address[0 : len(expected_prefix)] != expected_prefix: + if request.address[0 : len(expected_prefix)] != expected_prefix: raise ValueError("Unexpected Address Prefix") - puzzle_hash: bytes32 = decode_puzzle_hash(address) - - memos: list[bytes] = [] - if "memos" in request: - memos = [mem.encode("utf-8") for mem in request["memos"]] - - fee: uint64 = uint64(request.get("fee", 0)) await wallet.generate_signed_transaction( - [amount], - [puzzle_hash], + [request.amount], + [decode_puzzle_hash(request.address)], action_scope, - fee, - memos=[memos], - puzzle_decorator_override=request.get("puzzle_decorator", None), + 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, ) # Transaction may not have been included in the mempool yet. Use get_transaction to check. - return { - "transaction": None, # tx_endpoint wrapper will take care of this - "transactions": None, # tx_endpoint wrapper will take care of this - "transaction_id": None, # tx_endpoint wrapper will take care of this - } + # tx_endpoint will take care of the default values here + return SendTransactionResponse([], [], transaction=REPLACEABLE_TRANSACTION_RECORD, transaction_id=bytes32.zeros) async def send_transaction_multi(self, request: dict[str, Any]) -> EndpointResult: if await self.service.wallet_state_manager.synced() is False: diff --git a/chia/wallet/wallet_rpc_client.py b/chia/wallet/wallet_rpc_client.py index 632c5e919ca0..37491888f4c9 100644 --- a/chia/wallet/wallet_rpc_client.py +++ b/chia/wallet/wallet_rpc_client.py @@ -157,6 +157,7 @@ PWSelfPoolResponse, PWStatus, PWStatusResponse, + SendTransaction, SendTransactionMultiResponse, SendTransactionResponse, SetWalletResyncOnStartup, @@ -296,32 +297,17 @@ async def get_next_address(self, request: GetNextAddress) -> GetNextAddressRespo async def send_transaction( self, - wallet_id: int, - amount: uint64, - address: str, + request: SendTransaction, tx_config: TXConfig, - fee: uint64 = uint64(0), - memos: Optional[list[str]] = None, - puzzle_decorator_override: Optional[list[dict[str, Union[str, int, bool]]]] = None, extra_conditions: tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), - push: bool = True, ) -> SendTransactionResponse: - request = { - "wallet_id": wallet_id, - "amount": amount, - "address": address, - "fee": fee, - "puzzle_decorator": puzzle_decorator_override, - "extra_conditions": conditions_to_json_dicts(extra_conditions), - "push": push, - **tx_config.to_json_dict(), - **timelock_info.to_json_dict(), - } - if memos is not None: - request["memos"] = memos - response = await self.fetch("send_transaction", request) - return json_deserialize_with_clvm_streamable(response, SendTransactionResponse) + return SendTransactionResponse.from_json_dict( + await self.fetch( + "send_transaction", + request.json_serialize_for_transport(tx_config, extra_conditions, timelock_info), + ) + ) async def send_transaction_multi( self,