diff --git a/chia/_tests/cmds/cmd_test_utils.py b/chia/_tests/cmds/cmd_test_utils.py index 8234fb838f03..1829790dc1be 100644 --- a/chia/_tests/cmds/cmd_test_utils.py +++ b/chia/_tests/cmds/cmd_test_utils.py @@ -22,7 +22,6 @@ from chia.full_node.full_node_rpc_client import FullNodeRpcClient from chia.rpc.rpc_client import RpcClient from chia.simulator.simulator_full_node_rpc_client import SimulatorFullNodeRpcClient -from chia.types.coin_record import CoinRecord from chia.types.signing_mode import SigningMode from chia.util.bech32m import encode_puzzle_hash from chia.util.config import load_config @@ -31,7 +30,7 @@ from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType -from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig +from chia.wallet.util.tx_config import TXConfig from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_request_types import ( GetSyncStatusResponse, @@ -232,46 +231,6 @@ async def nft_calculate_royalties( ) ) - async def get_spendable_coins( - self, - wallet_id: int, - coin_selection_config: CoinSelectionConfig, - ) -> tuple[list[CoinRecord], list[CoinRecord], list[Coin]]: - """ - We return a tuple containing: (confirmed records, unconfirmed removals, unconfirmed additions) - """ - self.add_to_log( - "get_spendable_coins", - (wallet_id, coin_selection_config), - ) - confirmed_records = [ - CoinRecord( - Coin(bytes32([1] * 32), bytes32([2] * 32), uint64(1234560000)), - uint32(123456), - uint32(0), - False, - uint64(0), - ), - CoinRecord( - Coin(bytes32([3] * 32), bytes32([4] * 32), uint64(1234560000)), - uint32(123456), - uint32(0), - False, - uint64(0), - ), - ] - unconfirmed_removals = [ - CoinRecord( - Coin(bytes32([5] * 32), bytes32([6] * 32), uint64(1234570000)), - uint32(123457), - uint32(0), - True, - uint64(0), - ) - ] - unconfirmed_additions = [Coin(bytes32([7] * 32), bytes32([8] * 32), uint64(1234580000))] - return confirmed_records, unconfirmed_removals, unconfirmed_additions - async def send_transaction_multi( self, wallet_id: int, diff --git a/chia/_tests/cmds/wallet/test_wallet.py b/chia/_tests/cmds/wallet/test_wallet.py index 238bd5de7b89..073fd806928d 100644 --- a/chia/_tests/cmds/wallet/test_wallet.py +++ b/chia/_tests/cmds/wallet/test_wallet.py @@ -67,6 +67,8 @@ RoyaltyAsset, SendTransaction, SendTransactionResponse, + SpendClawbackCoins, + SpendClawbackCoinsResponse, TakeOfferResponse, TransactionRecordWithMetadata, WalletInfoResponse, @@ -539,23 +541,25 @@ def test_clawback(capsys: object, get_test_cli_clients: tuple[TestRpcClients, Pa class ClawbackWalletRpcClient(TestWalletRpcClient): async def spend_clawback_coins( self, - coin_ids: list[bytes32], - fee: int = 0, - force: bool = False, - push: bool = True, + request: SpendClawbackCoins, + tx_config: TXConfig, + extra_conditions: tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), - ) -> dict[str, Any]: - self.add_to_log("spend_clawback_coins", (coin_ids, fee, force, push, timelock_info)) - tx_hex_list = [get_bytes32(6).hex(), get_bytes32(7).hex(), get_bytes32(8).hex()] - return { - "transaction_ids": tx_hex_list, - "transactions": [STD_TX.to_json_dict()], - } + ) -> SpendClawbackCoinsResponse: + self.add_to_log( + "spend_clawback_coins", (request.coin_ids, request.fee, request.force, request.push, timelock_info) + ) + tx_list = [get_bytes32(6), get_bytes32(7), get_bytes32(8)] + return SpendClawbackCoinsResponse( + transaction_ids=tx_list, + transactions=[STD_TX], + unsigned_transactions=[STD_UTX], + ) inst_rpc_client = ClawbackWalletRpcClient() test_rpc_clients.wallet_rpc_client = inst_rpc_client tx_ids = [get_bytes32(3), get_bytes32(4), get_bytes32(5)] - r_tx_ids_hex = [get_bytes32(6).hex(), get_bytes32(7).hex(), get_bytes32(8).hex()] + r_tx_ids_hex = ["0x" + get_bytes32(6).hex(), "0x" + get_bytes32(7).hex(), "0x" + get_bytes32(8).hex()] command_args = [ "wallet", "clawback", @@ -569,7 +573,7 @@ async def spend_clawback_coins( "--expires-at", "150", ] - run_cli_command_and_assert(capsys, root_dir, command_args, ["transaction_ids", str(r_tx_ids_hex)]) + run_cli_command_and_assert(capsys, root_dir, command_args, ["transaction_ids", *r_tx_ids_hex]) # these are various things that should be in the output expected_calls: logType = { "spend_clawback_coins": [(tx_ids, 500000000000, False, True, test_condition_valid_times)], diff --git a/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py b/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py index 26ae27020071..f6b1cff0361d 100644 --- a/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py +++ b/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py @@ -15,7 +15,7 @@ from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.address_type import AddressType -from chia.wallet.wallet_request_types import NFTGetNFTs, NFTMintBulk, NFTMintMetadata, PushTransactions +from chia.wallet.wallet_request_types import NFTGetNFTs, NFTMintBulk, NFTMintMetadata, PushTransactions, SelectCoins async def nft_count(wallet: NFTWallet) -> int: @@ -291,22 +291,25 @@ async def test_nft_mint_rpc(wallet_environments: WalletTestFramework, zero_royal fee = 100 num_chunks = int(n / chunk) + (1 if n % chunk > 0 else 0) required_amount = n + (fee * num_chunks) - xch_coins = await env_0.rpc_client.select_coins( - amount=required_amount, - coin_selection_config=wallet_environments.tx_config.coin_selection_config, - wallet_id=wallet_0.id(), + select_coins_response = await env_0.rpc_client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(required_amount), + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + wallet_id=wallet_0.id(), + ) ) - funding_coin = xch_coins[0] + funding_coin = select_coins_response.coins[0] assert funding_coin.amount >= required_amount - funding_coin_dict = xch_coins[0].to_json_dict() next_coin = funding_coin did_coin = ( await env_0.rpc_client.select_coins( - amount=1, - coin_selection_config=wallet_environments.tx_config.coin_selection_config, - wallet_id=env_0.wallet_aliases["did"], + SelectCoins.from_coin_selection_config( + amount=uint64(1), + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + wallet_id=uint32(env_0.wallet_aliases["did"]), + ) ) - )[0] + ).coins[0] did_lineage_parent: Optional[bytes32] = None txs: list[TransactionRecord] = [] nft_ids = set() @@ -321,7 +324,7 @@ async def test_nft_mint_rpc(wallet_environments: WalletTestFramework, zero_royal mint_number_start=uint16(i + 1), mint_total=uint16(n), xch_coins=[next_coin], - xch_change_target=funding_coin_dict["puzzle_hash"], + xch_change_target=funding_coin.puzzle_hash.hex(), did_coin=did_coin if with_did else None, did_lineage_parent=did_lineage_parent if with_did else None, mint_from_did=with_did, diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index 428ca7e8e6eb..8c5082d3b5df 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -121,9 +121,11 @@ DIDTransferDID, DIDUpdateMetadata, FungibleAsset, + GetCoinRecordsByNames, GetNextAddress, GetNotifications, GetPrivateKey, + GetSpendableCoins, GetSyncStatusResponse, GetTimestampForHeight, GetTransaction, @@ -141,8 +143,10 @@ PushTransactions, PushTX, RoyaltyAsset, + SelectCoins, SendTransaction, SetWalletResyncOnStartup, + SpendClawbackCoins, SplitCoins, VerifySignature, VerifySignatureResponse, @@ -644,25 +648,28 @@ async def test_create_signed_transaction( selected_coin = None if select_coin: - selected_coin = await wallet_1_rpc.select_coins( - amount=amount_total, wallet_id=wallet_id, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await wallet_1_rpc.select_coins( + SelectCoins.from_coin_selection_config( + amount=amount_total, wallet_id=uint32(wallet_id), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) - assert len(selected_coin) == 1 + assert len(select_coins_response.coins) == 1 + selected_coin = select_coins_response.coins[0] txs = ( await wallet_1_rpc.create_signed_transactions( outputs, - coins=selected_coin, + coins=[selected_coin] if selected_coin is not None else [], fee=amount_fee, wallet_id=wallet_id, # shouldn't actually block it tx_config=DEFAULT_TX_CONFIG.override( - excluded_coin_amounts=[uint64(selected_coin[0].amount)] if selected_coin is not None else [], + excluded_coin_amounts=[uint64(selected_coin.amount)] if selected_coin is not None else [], ), push=True, ) ).transactions - change_expected = not selected_coin or selected_coin[0].amount - amount_total > 0 + change_expected = not selected_coin or selected_coin.amount - amount_total > 0 assert_tx_amounts(txs[-1], outputs, amount_fee=amount_fee, change_expected=change_expected, is_cat=is_cat) # Farm the transaction and make sure the wallet balance reflects it correct @@ -783,38 +790,42 @@ async def test_create_signed_transaction_with_excluded_coins(wallet_rpc_environm await generate_funds(full_node_api, env.wallet_1) async def it_does_not_include_the_excluded_coins() -> None: - selected_coins = await wallet_1_rpc.select_coins( - amount=250000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await wallet_1_rpc.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(250000000000), wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) - assert len(selected_coins) == 1 + assert len(select_coins_response.coins) == 1 outputs = await create_tx_outputs(wallet_1, [(uint64(250000000000), None)]) tx = ( await wallet_1_rpc.create_signed_transactions( outputs, DEFAULT_TX_CONFIG.override( - excluded_coin_ids=[c.name() for c in selected_coins], + excluded_coin_ids=[c.name() for c in select_coins_response.coins], ), ) ).signed_tx assert len(tx.removals) == 1 - assert tx.removals[0] != selected_coins[0] + assert tx.removals[0] != select_coins_response.coins[0] assert tx.removals[0].amount == uint64(1750000000000) await assert_push_tx_error(full_node_rpc, tx) async def it_throws_an_error_when_all_spendable_coins_are_excluded() -> None: - selected_coins = await wallet_1_rpc.select_coins( - amount=1750000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await wallet_1_rpc.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1750000000000), wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) - assert len(selected_coins) == 1 + assert len(select_coins_response.coins) == 1 outputs = await create_tx_outputs(wallet_1, [(uint64(1750000000000), None)]) with pytest.raises(ValueError): await wallet_1_rpc.create_signed_transactions( outputs, DEFAULT_TX_CONFIG.override( - excluded_coin_ids=[c.name() for c in selected_coins], + excluded_coin_ids=[c.name() for c in select_coins_response.coins], ), ) @@ -833,7 +844,6 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron wallet_1 = wallet_1_node.wallet_state_manager.main_wallet wallet_2 = wallet_2_node.wallet_state_manager.main_wallet full_node_api: FullNodeSimulator = env.full_node.api - wallet_2_api = WalletRpcApi(wallet_2_node) generated_funds = await generate_funds(full_node_api, env.wallet_1, 1) await generate_funds(full_node_api, env.wallet_2, 1) @@ -876,32 +886,28 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron await time_out_assert(20, get_confirmed_balance, generated_funds - 500, wallet_1_rpc, 1) await time_out_assert(20, get_confirmed_balance, generated_funds - 500, wallet_2_rpc, 1) await asyncio.sleep(10) - # Test missing coin_ids - has_exception = False - try: - await wallet_2_api.spend_clawback_coins({}) - except ValueError: - has_exception = True - assert has_exception # Test coin ID is not a Clawback coin invalid_coin_id = tx.removals[0].name() - resp = await wallet_2_rpc.spend_clawback_coins([invalid_coin_id], 500) - assert resp["success"] - assert resp["transaction_ids"] == [] + resp = await wallet_2_rpc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[invalid_coin_id], fee=uint64(500), push=True), tx_config=DEFAULT_TX_CONFIG + ) + assert resp.transaction_ids == [] # Test unsupported wallet coin_record = await wallet_1_node.wallet_state_manager.coin_store.get_coin_record(clawback_coin_id_1) assert coin_record is not None await wallet_1_node.wallet_state_manager.coin_store.add_coin_record( dataclasses.replace(coin_record, wallet_type=WalletType.CAT) ) - resp = await wallet_1_rpc.spend_clawback_coins([clawback_coin_id_1], 100) - assert resp["success"] - assert len(resp["transaction_ids"]) == 0 + resp = await wallet_1_rpc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[clawback_coin_id_1], fee=uint64(100), push=True), tx_config=DEFAULT_TX_CONFIG + ) + assert len(resp.transaction_ids) == 0 # Test missing metadata await wallet_1_node.wallet_state_manager.coin_store.add_coin_record(dataclasses.replace(coin_record, metadata=None)) - resp = await wallet_1_rpc.spend_clawback_coins([clawback_coin_id_1], 100) - assert resp["success"] - assert len(resp["transaction_ids"]) == 0 + resp = await wallet_1_rpc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[clawback_coin_id_1], fee=uint64(100), push=True), tx_config=DEFAULT_TX_CONFIG + ) + assert len(resp.transaction_ids) == 0 # Test missing incoming tx coin_record = await wallet_1_node.wallet_state_manager.coin_store.get_coin_record(clawback_coin_id_2) assert coin_record is not None @@ -909,8 +915,10 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron await wallet_1_node.wallet_state_manager.coin_store.add_coin_record( dataclasses.replace(coin_record, coin=fake_coin) ) - resp = await wallet_1_rpc.spend_clawback_coins([fake_coin.name()], 100) - assert resp["transaction_ids"] == [] + resp = await wallet_1_rpc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[fake_coin.name()], fee=uint64(100), push=True), tx_config=DEFAULT_TX_CONFIG + ) + assert resp.transaction_ids == [] # Test coin puzzle hash doesn't match the puzzle farmed_tx = (await wallet_1.wallet_state_manager.tx_store.get_farming_rewards())[0] await wallet_1.wallet_state_manager.tx_store.add_transaction_record( @@ -919,8 +927,10 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron await wallet_1_node.wallet_state_manager.coin_store.add_coin_record( dataclasses.replace(coin_record, coin=fake_coin) ) - resp = await wallet_1_rpc.spend_clawback_coins([fake_coin.name()], 100) - assert resp["transaction_ids"] == [] + resp = await wallet_1_rpc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[fake_coin.name()], fee=uint64(100), push=True), tx_config=DEFAULT_TX_CONFIG + ) + assert resp.transaction_ids == [] # Test claim spend await wallet_2_rpc.set_auto_claim( AutoClaimSettings( @@ -930,11 +940,12 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron batch_size=uint16(1), ) ) - resp = await wallet_2_rpc.spend_clawback_coins([clawback_coin_id_1, clawback_coin_id_2], 100) - assert resp["success"] - assert len(resp["transaction_ids"]) == 2 - for _tx in resp["transactions"]: - clawback_tx = TransactionRecord.from_json_dict(_tx) + resp = await wallet_2_rpc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[clawback_coin_id_1, clawback_coin_id_2], fee=uint64(100), push=True), + tx_config=DEFAULT_TX_CONFIG, + ) + assert len(resp.transaction_ids) == 2 + for clawback_tx in resp.transactions: if clawback_tx.spend_bundle is not None: await time_out_assert_not_none( 10, full_node_api.full_node.mempool_manager.get_spendbundle, clawback_tx.spend_bundle.name() @@ -942,9 +953,10 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron await farm_transaction_block(full_node_api, wallet_2_node) await time_out_assert(20, get_confirmed_balance, generated_funds + 300, wallet_2_rpc, 1) # Test spent coin - resp = await wallet_2_rpc.spend_clawback_coins([clawback_coin_id_1], 500) - assert resp["success"] - assert resp["transaction_ids"] == [] + resp = await wallet_2_rpc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[clawback_coin_id_1], fee=uint64(500), push=True), tx_config=DEFAULT_TX_CONFIG + ) + assert resp.transaction_ids == [] @pytest.mark.anyio @@ -958,8 +970,10 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir generated_funds = await generate_funds(full_node_api, env.wallet_1) - removals = await client.select_coins( - 1750000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1750000000000), wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) # we want a coin that won't be selected by default outputs = await create_tx_outputs(wallet_2, [(uint64(1), ["memo_1"]), (uint64(2), ["memo_2"])]) amount_outputs = sum(output["amount"] for output in outputs) @@ -970,7 +984,7 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir 1, outputs, DEFAULT_TX_CONFIG, - coins=removals, + coins=select_coins_response.coins, fee=amount_fee, ) ).transaction @@ -979,7 +993,7 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir assert send_tx_res is not None assert_tx_amounts(send_tx_res, outputs, amount_fee=amount_fee, change_expected=True) - assert send_tx_res.removals == removals + assert send_tx_res.removals == select_coins_response.coins await farm_transaction(full_node_api, wallet_node, spend_bundle) @@ -1353,8 +1367,12 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty await wallet_environments.process_pending_states(cat_spend_changes) # Test CAT spend with a fee and pre-specified removals / coins - removals = await env_0.rpc_client.select_coins( - amount=uint64(2), wallet_id=cat_0_id, coin_selection_config=wallet_environments.tx_config.coin_selection_config + select_coins_response = await env_0.rpc_client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(2), + wallet_id=cat_0_id, + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + ) ) tx_res = await env_0.rpc_client.cat_spend( cat_0_id, @@ -1363,12 +1381,12 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty addr_1, uint64(5_000_000), ["the cat memo"], - removals=removals, + removals=select_coins_response.coins, ) spend_bundle = tx_res.transaction.spend_bundle assert spend_bundle is not None - assert removals[0] in {removal for tx in tx_res.transactions for removal in tx.removals} + assert select_coins_response.coins[0] in {removal for tx in tx_res.transactions for removal in tx.removals} await wallet_environments.process_pending_states(cat_spend_changes) @@ -1380,10 +1398,14 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty assert len(cats) == 1 # Test CAT coin selection - selected_coins = await env_0.rpc_client.select_coins( - amount=1, wallet_id=cat_0_id, coin_selection_config=wallet_environments.tx_config.coin_selection_config + select_coins_response = await env_0.rpc_client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1), + wallet_id=cat_0_id, + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + ) ) - assert len(selected_coins) > 0 + assert len(select_coins_response.coins) > 0 # Test get_cat_list cat_list = (await env_0.rpc_client.get_cat_list()).cat_list @@ -1483,13 +1505,17 @@ async def test_offer_endpoints(wallet_environments: WalletTestFramework, wallet_ ] ) - test_crs: list[CoinRecord] = await env_1.rpc_client.get_coin_records_by_names( - [a.name() for a in spend_bundle.additions() if a.amount != 4] - ) + test_crs: list[CoinRecord] = ( + await env_1.rpc_client.get_coin_records_by_names( + GetCoinRecordsByNames([a.name() for a in spend_bundle.additions() if a.amount != 4]) + ) + ).coin_records for cr in test_crs: assert cr.coin in spend_bundle.additions() with pytest.raises(ValueError): - await env_1.rpc_client.get_coin_records_by_names([a.name() for a in spend_bundle.additions() if a.amount == 4]) + await env_1.rpc_client.get_coin_records_by_names( + GetCoinRecordsByNames([a.name() for a in spend_bundle.additions() if a.amount == 4]) + ) # Create an offer of 5 chia for one CAT await env_1.rpc_client.create_offer_for_ids( {uint32(1): -5, cat_asset_id.hex(): 1}, wallet_environments.tx_config, validate_only=True @@ -1834,16 +1860,18 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn assert len(coin_ids_unspent) > 0 # Do some queries to trigger all parameters # 1. Empty coin_ids - assert await client.get_coin_records_by_names([]) == [] + assert (await client.get_coin_records_by_names(GetCoinRecordsByNames([]))).coin_records == [] # 2. All coins - rpc_result = await client.get_coin_records_by_names(coin_ids + coin_ids_unspent) - assert {record.coin for record in rpc_result} == {*coins, *coins_unspent} + rpc_result = await client.get_coin_records_by_names(GetCoinRecordsByNames(coin_ids + coin_ids_unspent)) + assert {record.coin for record in rpc_result.coin_records} == {*coins, *coins_unspent} # 3. All spent coins - rpc_result = await client.get_coin_records_by_names(coin_ids, include_spent_coins=True) - assert {record.coin for record in rpc_result} == coins + rpc_result = await client.get_coin_records_by_names(GetCoinRecordsByNames(coin_ids, include_spent_coins=True)) + assert {record.coin for record in rpc_result.coin_records} == coins # 4. All unspent coins - rpc_result = await client.get_coin_records_by_names(coin_ids_unspent, include_spent_coins=False) - assert {record.coin for record in rpc_result} == coins_unspent + rpc_result = await client.get_coin_records_by_names( + GetCoinRecordsByNames(coin_ids_unspent, include_spent_coins=False) + ) + assert {record.coin for record in rpc_result.coin_records} == coins_unspent # 5. Filter start/end height filter_records = result.records[:10] assert len(filter_records) == 10 @@ -1852,11 +1880,13 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn min_height = min(record.confirmed_block_height for record in filter_records) max_height = max(record.confirmed_block_height for record in filter_records) assert min_height != max_height - rpc_result = await client.get_coin_records_by_names(filter_coin_ids, start_height=min_height, end_height=max_height) - assert {record.coin for record in rpc_result} == filter_coins + rpc_result = await client.get_coin_records_by_names( + GetCoinRecordsByNames(filter_coin_ids, start_height=min_height, end_height=max_height) + ) + assert {record.coin for record in rpc_result.coin_records} == filter_coins # 8. Test the failure case with pytest.raises(ValueError, match="not found"): - await client.get_coin_records_by_names(coin_ids, include_spent_coins=False) + await client.get_coin_records_by_names(GetCoinRecordsByNames(coin_ids, include_spent_coins=False)) @pytest.mark.anyio @@ -2297,51 +2327,63 @@ async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment await time_out_assert(20, get_confirmed_balance, funds, client, 1) # test min coin amount - min_coins: list[Coin] = await client_2.select_coins( - amount=1000, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(min_coin_amount=uint64(1001)), + min_coins_response = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1000), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(min_coin_amount=uint64(1001)), + ) ) - assert len(min_coins) == 1 - assert min_coins[0].amount == uint64(10_000) + assert len(min_coins_response.coins) == 1 + assert min_coins_response.coins[0].amount == uint64(10_000) # test max coin amount - max_coins: list[Coin] = await client_2.select_coins( - amount=2000, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( - min_coin_amount=uint64(999), max_coin_amount=uint64(9999) - ), + max_coins_reponse = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(2000), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( + min_coin_amount=uint64(999), max_coin_amount=uint64(9999) + ), + ) ) - assert len(max_coins) == 2 - assert max_coins[0].amount == uint64(1000) + assert len(max_coins_reponse.coins) == 2 + assert max_coins_reponse.coins[0].amount == uint64(1000) # test excluded coin amounts non_1000_amt: int = sum(a for a in tx_amounts if a != 1000) - excluded_amt_coins: list[Coin] = await client_2.select_coins( - amount=non_1000_amt, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + excluded_amt_coins_response = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(non_1000_amt), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + ) ) - assert len(excluded_amt_coins) == len([a for a in tx_amounts if a != 1000]) - assert sum(c.amount for c in excluded_amt_coins) == non_1000_amt + assert len(excluded_amt_coins_response.coins) == len([a for a in tx_amounts if a != 1000]) + assert sum(c.amount for c in excluded_amt_coins_response.coins) == non_1000_amt # test excluded coins with pytest.raises(ValueError): await client_2.select_coins( - amount=5000, - wallet_id=1, + SelectCoins.from_coin_selection_config( + amount=uint64(5000), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( + excluded_coin_ids=[c.name() for c in min_coins_response.coins] + ), + ) + ) + excluded_test_response = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1300), + wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( - excluded_coin_ids=[c.name() for c in min_coins] + excluded_coin_ids=[c.name() for c in coin_300] ), ) - excluded_test = await client_2.select_coins( - amount=1300, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_ids=[c.name() for c in coin_300]), ) - assert len(excluded_test) == 2 - for coin in excluded_test: + assert len(excluded_test_response.coins) == 2 + for coin in excluded_test_response.coins: assert coin != coin_300[0] # test backwards compatibility in the RPC @@ -2360,27 +2402,40 @@ async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment assert coin != coin_300[0] # test get coins - all_coins, _, _ = await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( - excluded_coin_ids=[c.name() for c in excluded_amt_coins] + spendable_coins_response = await client_2.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( + excluded_coin_ids=[c.name() for c in excluded_amt_coins_response.coins] + ), ), ) - assert set(excluded_amt_coins).intersection({rec.coin for rec in all_coins}) == set() - all_coins, _, _ = await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + assert ( + set(excluded_amt_coins_response.coins).intersection( + {rec.coin for rec in spendable_coins_response.confirmed_records} + ) + == set() + ) + spendable_coins_response = await client_2.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + ) ) - assert len([rec for rec in all_coins if rec.coin.amount == 1000]) == 0 - all_coins_2, _, _ = await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(max_coin_amount=uint64(999)), + assert len([rec for rec in spendable_coins_response.confirmed_records if rec.coin.amount == 1000]) == 0 + spendable_coins_response = await client_2.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(max_coin_amount=uint64(999)), + ) ) - assert all_coins_2[0].coin == coin_300[0] + assert spendable_coins_response.confirmed_records[0].coin == coin_300[0] with pytest.raises(ValueError): # validate fail on invalid coin id. await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_ids=[b"a"]), + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_ids=[b"a"]), + ) ) @@ -2861,12 +2916,11 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) await time_out_assert(20, check_client_synced, True, wc) await asyncio.sleep(10) - resp = await wc.spend_clawback_coins([clawback_coin_id], 0) - assert resp["success"] - assert len(resp["transaction_ids"]) == 1 - await time_out_assert_not_none( - 10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0]) + resp = await wc.spend_clawback_coins( + SpendClawbackCoins(coin_ids=[clawback_coin_id], fee=uint64(0), push=True), tx_config=DEFAULT_TX_CONFIG ) + assert len(resp.transaction_ids) == 1 + await time_out_assert_not_none(10, full_node_api.full_node.mempool_manager.get_spendbundle, resp.transaction_ids[0]) await farm_transaction_block(full_node_api, wallet_node) await time_out_assert(20, check_client_synced, True, wc) wallet_node_2._close() diff --git a/chia/_tests/wallet/test_wallet.py b/chia/_tests/wallet/test_wallet.py index 1f1662953276..1bd42dbd512c 100644 --- a/chia/_tests/wallet/test_wallet.py +++ b/chia/_tests/wallet/test_wallet.py @@ -397,8 +397,6 @@ async def test_wallet_clawback_clawback(self, wallet_environments: WalletTestFra assert not txs["transactions"][0]["confirmed"] assert txs["transactions"][0]["metadata"]["recipient_puzzle_hash"][2:] == normal_puzhash.hex() assert txs["transactions"][0]["metadata"]["coin_id"] == "0x" + merkle_coin.name().hex() - with pytest.raises(ValueError): - await api_0.spend_clawback_coins({}) test_fee = 10 resp = await api_0.spend_clawback_coins( @@ -408,7 +406,6 @@ async def test_wallet_clawback_clawback(self, wallet_environments: WalletTestFra **wallet_environments.tx_config.to_json_dict(), } ) - assert resp["success"] assert len(resp["transaction_ids"]) == 1 await wallet_environments.process_pending_states( @@ -541,7 +538,6 @@ async def test_wallet_clawback_sent_self(self, wallet_environments: WalletTestFr **wallet_environments.tx_config.to_json_dict(), } ) - assert resp["success"] assert len(resp["transaction_ids"]) == 1 # Wait mempool update await wallet_environments.process_pending_states( @@ -678,7 +674,6 @@ async def test_wallet_clawback_claim_manual(self, wallet_environments: WalletTes **wallet_environments.tx_config.to_json_dict(), } ) - assert resp["success"] assert len(resp["transaction_ids"]) == 1 await wallet_environments.process_pending_states( @@ -1094,10 +1089,8 @@ async def test_clawback_resync(self, self_hostname: str, wallet_environments: Wa await time_out_assert(20, wsm_2.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK) # clawback merkle coin resp = await api_1.spend_clawback_coins({"coin_ids": [clawback_coin_id_1.hex()], "fee": 0}) - assert resp["success"] assert len(resp["transaction_ids"]) == 1 resp = await api_1.spend_clawback_coins({"coin_ids": [clawback_coin_id_2.hex()], "fee": 0}) - assert resp["success"] assert len(resp["transaction_ids"]) == 1 await wallet_environments.process_pending_states( diff --git a/chia/cmds/coin_funcs.py b/chia/cmds/coin_funcs.py index 1a9bc9c1dea0..f6b205d38f44 100644 --- a/chia/cmds/coin_funcs.py +++ b/chia/cmds/coin_funcs.py @@ -18,7 +18,7 @@ from chia.wallet.conditions import ConditionValidTimes from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet_request_types import CombineCoins, SplitCoins +from chia.wallet.wallet_request_types import CombineCoins, GetCoinRecordsByNames, GetSpendableCoins, SplitCoins async def async_list( @@ -44,23 +44,28 @@ async def async_list( if not (await client_info.client.get_sync_status()).synced: print("Wallet not synced. Please wait.") return - conf_coins, unconfirmed_removals, unconfirmed_additions = await client_info.client.get_spendable_coins( - wallet_id=wallet_id, - coin_selection_config=CMDCoinSelectionConfigLoader( - max_coin_amount=max_coin_amount, - min_coin_amount=min_coin_amount, - excluded_coin_amounts=list(excluded_amounts), - excluded_coin_ids=list(excluded_coin_ids), - ).to_coin_selection_config(mojo_per_unit), + response = await client_info.client.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(wallet_id), + coin_selection_config=CMDCoinSelectionConfigLoader( + max_coin_amount=max_coin_amount, + min_coin_amount=min_coin_amount, + excluded_coin_amounts=list(excluded_amounts), + excluded_coin_ids=list(excluded_coin_ids), + ).to_coin_selection_config(mojo_per_unit), + ) + ) + print( + f"There are a total of {len(response.confirmed_records) + len(response.unconfirmed_additions)}" + f" coins in wallet {wallet_id}." ) - print(f"There are a total of {len(conf_coins) + len(unconfirmed_additions)} coins in wallet {wallet_id}.") - print(f"{len(conf_coins)} confirmed coins.") - print(f"{len(unconfirmed_additions)} unconfirmed additions.") - print(f"{len(unconfirmed_removals)} unconfirmed removals.") + print(f"{len(response.confirmed_records)} confirmed coins.") + print(f"{len(response.unconfirmed_additions)} unconfirmed additions.") + print(f"{len(response.unconfirmed_removals)} unconfirmed removals.") print("Confirmed coins:") print_coins( "\tAddress: {} Amount: {}, Confirmed in block: {}\n", - [(cr.coin, str(cr.confirmed_block_index)) for cr in conf_coins], + [(cr.coin, str(cr.confirmed_block_index)) for cr in response.confirmed_records], mojo_per_unit, addr_prefix, paginate, @@ -69,7 +74,7 @@ async def async_list( print("\nUnconfirmed Removals:") print_coins( "\tPrevious Address: {} Amount: {}, Confirmed in block: {}\n", - [(cr.coin, str(cr.confirmed_block_index)) for cr in unconfirmed_removals], + [(cr.coin, str(cr.confirmed_block_index)) for cr in response.unconfirmed_removals], mojo_per_unit, addr_prefix, paginate, @@ -77,7 +82,7 @@ async def async_list( print("\nUnconfirmed Additions:") print_coins( "\tNew Address: {} Amount: {}, Not yet confirmed in a block.{}\n", - [(coin, "") for coin in unconfirmed_additions], + [(coin, "") for coin in response.unconfirmed_additions], mojo_per_unit, addr_prefix, paginate, @@ -217,19 +222,19 @@ async def async_split( return [] if number_of_coins is None: - coins = await client_info.client.get_coin_records_by_names([target_coin_id]) - if len(coins) == 0: + response = await client_info.client.get_coin_records_by_names(GetCoinRecordsByNames([target_coin_id])) + if len(response.coin_records) == 0: print("Could not find target coin.") return [] assert amount_per_coin is not None - number_of_coins = int(coins[0].coin.amount // amount_per_coin.convert_amount(mojo_per_unit)) + number_of_coins = int(response.coin_records[0].coin.amount // amount_per_coin.convert_amount(mojo_per_unit)) elif amount_per_coin is None: - coins = await client_info.client.get_coin_records_by_names([target_coin_id]) - if len(coins) == 0: + response = await client_info.client.get_coin_records_by_names(GetCoinRecordsByNames([target_coin_id])) + if len(response.coin_records) == 0: print("Could not find target coin.") return [] assert number_of_coins is not None - amount_per_coin = CliAmount(True, uint64(coins[0].coin.amount // number_of_coins)) + amount_per_coin = CliAmount(True, uint64(response.coin_records[0].coin.amount // number_of_coins)) final_amount_per_coin = amount_per_coin.convert_amount(mojo_per_unit) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 034bf18a6e5b..f111500df6e3 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -40,6 +40,7 @@ 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 +from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG 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 @@ -79,6 +80,7 @@ SignMessageByAddressResponse, SignMessageByID, SignMessageByIDResponse, + SpendClawbackCoins, VCAddProofs, VCGet, VCGetList, @@ -1684,14 +1686,12 @@ async def spend_clawback( print("Batch fee cannot be negative.") return [] response = await wallet_client.spend_clawback_coins( - tx_ids, - fee, - force, - push=push, + SpendClawbackCoins(coin_ids=tx_ids, fee=fee, force=force, push=push), + tx_config=DEFAULT_TX_CONFIG, timelock_info=condition_valid_times, ) print(str(response)) - return [TransactionRecord.from_json_dict(tx) for tx in response["transactions"]] + return response.transactions async def mint_vc( diff --git a/chia/wallet/util/tx_config.py b/chia/wallet/util/tx_config.py index ddca5c89280a..2f4b05f07414 100644 --- a/chia/wallet/util/tx_config.py +++ b/chia/wallet/util/tx_config.py @@ -88,7 +88,7 @@ def autofill( @classmethod def from_json_dict(cls, json_dict: dict[str, Any]) -> Self: - if "excluded_coins" in json_dict: + if json_dict.get("excluded_coins") is not None: excluded_coins: list[Coin] = [Coin.from_json_dict(c) for c in json_dict["excluded_coins"]] excluded_coin_ids: list[str] = [c.name().hex() for c in excluded_coins] if "excluded_coin_ids" in json_dict: @@ -98,7 +98,8 @@ def from_json_dict(cls, json_dict: dict[str, Any]) -> Self: return super().from_json_dict(json_dict) # This function is purely for ergonomics - def override(self, **kwargs: Any) -> CoinSelectionConfigLoader: + # But creates a small linting complication + def override(self, **kwargs: Any) -> Self: return dataclasses.replace(self, **kwargs) @@ -138,10 +139,6 @@ def autofill( reuse_puzhash, ) - # This function is purely for ergonomics - def override(self, **kwargs: Any) -> TXConfigLoader: - return dataclasses.replace(self, **kwargs) - DEFAULT_COIN_SELECTION_CONFIG = CoinSelectionConfig(uint64(0), uint64(DEFAULT_CONSTANTS.MAX_COIN_AMOUNT), [], []) DEFAULT_TX_CONFIG = TXConfig( diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index 5983b99ddbbc..d40fcfc8df75 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -13,6 +13,7 @@ from chia.data_layer.singleton_record import SingletonRecord from chia.pools.pool_wallet_info import PoolWalletInfo from chia.types.blockchain_format.program import Program +from chia.types.coin_record import CoinRecord from chia.util.byte_types import hexstr_to_bytes from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_to_json_dicts @@ -32,7 +33,7 @@ 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.util.tx_config import CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_node import Balance @@ -480,6 +481,79 @@ class DeleteUnconfirmedTransactions(Streamable): wallet_id: uint32 +@streamable +@dataclass(frozen=True) +class SelectCoins(CoinSelectionConfigLoader): + wallet_id: uint32 = field(default_factory=default_raise) + amount: uint64 = field(default_factory=default_raise) + exclude_coins: Optional[list[Coin]] = None # for backwards compatibility + + def __post_init__(self) -> None: + if self.excluded_coin_ids is not None and self.exclude_coins is not None: + raise ValueError( + "Cannot specify both excluded_coin_ids/excluded_coins and exclude_coins (the latter is deprecated)" + ) + super().__post_init__() + + @classmethod + def from_coin_selection_config( + cls, wallet_id: uint32, amount: uint64, coin_selection_config: CoinSelectionConfig + ) -> Self: + return cls( + wallet_id=wallet_id, + amount=amount, + min_coin_amount=coin_selection_config.min_coin_amount, + max_coin_amount=coin_selection_config.max_coin_amount, + excluded_coin_amounts=coin_selection_config.excluded_coin_amounts, + excluded_coin_ids=coin_selection_config.excluded_coin_ids, + ) + + +@streamable +@dataclass(frozen=True) +class SelectCoinsResponse(Streamable): + coins: list[Coin] + + +@streamable +@dataclass(frozen=True) +class GetSpendableCoins(CoinSelectionConfigLoader): + wallet_id: uint32 = field(default_factory=default_raise) + + @classmethod + def from_coin_selection_config(cls, wallet_id: uint32, coin_selection_config: CoinSelectionConfig) -> Self: + return cls( + wallet_id=wallet_id, + min_coin_amount=coin_selection_config.min_coin_amount, + max_coin_amount=coin_selection_config.max_coin_amount, + excluded_coin_amounts=coin_selection_config.excluded_coin_amounts, + excluded_coin_ids=coin_selection_config.excluded_coin_ids, + ) + + +@streamable +@dataclass(frozen=True) +class GetSpendableCoinsResponse(Streamable): + confirmed_records: list[CoinRecord] + unconfirmed_removals: list[CoinRecord] + unconfirmed_additions: list[Coin] + + +@streamable +@dataclass(frozen=True) +class GetCoinRecordsByNames(Streamable): + names: list[bytes32] + start_height: Optional[uint32] = None + end_height: Optional[uint32] = None + include_spent_coins: bool = True + + +@streamable +@dataclass(frozen=True) +class GetCoinRecordsByNamesResponse(Streamable): + coin_records: list[CoinRecord] + + @streamable @dataclass(frozen=True) class GetCurrentDerivationIndexResponse(Streamable): @@ -1146,6 +1220,20 @@ class SendTransactionResponse(TransactionEndpointResponse): transaction_id: bytes32 +@streamable +@dataclass(frozen=True) +class SpendClawbackCoins(TransactionEndpointRequest): + coin_ids: list[bytes32] = field(default_factory=default_raise) + batch_size: Optional[uint16] = None + force: bool = False + + +@streamable +@dataclass(frozen=True) +class SpendClawbackCoinsResponse(TransactionEndpointResponse): + transaction_ids: list[bytes32] + + @streamable @dataclass(frozen=True) class PushTransactions(TransactionEndpointRequest): diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index 351a08cea2aa..0d4a97610ff2 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -27,7 +27,7 @@ from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes -from chia.util.config import load_config, str2bool +from chia.util.config import load_config from chia.util.errors import KeychainIsLocked from chia.util.hash import std_hash from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic @@ -172,6 +172,8 @@ GatherSigningInfo, GatherSigningInfoResponse, GenerateMnemonicResponse, + GetCoinRecordsByNames, + GetCoinRecordsByNamesResponse, GetCurrentDerivationIndexResponse, GetHeightInfoResponse, GetLoggedInFingerprintResponse, @@ -183,6 +185,8 @@ GetPrivateKeyFormat, GetPrivateKeyResponse, GetPublicKeysResponse, + GetSpendableCoins, + GetSpendableCoinsResponse, GetSyncStatusResponse, GetTimestampForHeight, GetTimestampForHeightResponse, @@ -242,6 +246,8 @@ PWSelfPoolResponse, PWStatus, PWStatusResponse, + SelectCoins, + SelectCoinsResponse, SendTransaction, SendTransactionResponse, SetWalletResyncOnStartup, @@ -249,6 +255,8 @@ SignMessageByAddressResponse, SignMessageByID, SignMessageByIDResponse, + SpendClawbackCoins, + SpendClawbackCoinsResponse, SplitCoins, SplitCoinsResponse, SubmitTransactions, @@ -1655,12 +1663,13 @@ async def send_transaction_multi(self, request: dict[str, Any]) -> EndpointResul } @tx_endpoint(push=True, merge_spends=False) + @marshal async def spend_clawback_coins( self, - request: dict[str, Any], + request: SpendClawbackCoins, action_scope: WalletActionScope, extra_conditions: tuple[Condition, ...] = tuple(), - ) -> EndpointResult: + ) -> SpendClawbackCoinsResponse: """Spend clawback coins that were sent (to claw them back) or received (to claim them). :param coin_ids: list of coin ids to be spent @@ -1668,21 +1677,19 @@ async def spend_clawback_coins( :param fee: transaction fee in mojos :return: """ - if "coin_ids" not in request: - raise ValueError("Coin IDs are required.") - coin_ids: list[bytes32] = [bytes32.from_hexstr(coin) for coin in request["coin_ids"]] - tx_fee: uint64 = uint64(request.get("fee", 0)) # Get inner puzzle coin_records = await self.service.wallet_state_manager.coin_store.get_coin_records( - coin_id_filter=HashFilter.include(coin_ids), + coin_id_filter=HashFilter.include(request.coin_ids), coin_type=CoinType.CLAWBACK, wallet_type=WalletType.STANDARD_WALLET, spent_range=UInt32Range(stop=uint32(0)), ) coins: dict[Coin, ClawbackMetadata] = {} - batch_size = request.get( - "batch_size", self.service.wallet_state_manager.config.get("auto_claim", {}).get("batch_size", 50) + batch_size = ( + request.batch_size + if request.batch_size is not None + else self.service.wallet_state_manager.config.get("auto_claim", {}).get("batch_size", 50) ) for coin_id, coin_record in coin_records.coin_id_to_record.items(): try: @@ -1692,9 +1699,9 @@ async def spend_clawback_coins( if len(coins) >= batch_size: await self.service.wallet_state_manager.spend_clawback_coins( coins, - tx_fee, + request.fee, action_scope, - request.get("force", False), + request.force, extra_conditions=extra_conditions, ) coins = {} @@ -1703,17 +1710,14 @@ async def spend_clawback_coins( if len(coins) > 0: await self.service.wallet_state_manager.spend_clawback_coins( coins, - tx_fee, + request.fee, action_scope, - request.get("force", False), + request.force, extra_conditions=extra_conditions, ) - return { - "success": True, - "transaction_ids": None, # tx_endpoint wrapper will take care of this - "transactions": None, # tx_endpoint wrapper will take care of this - } + # tx_endpoint will fill in the default values here + return SpendClawbackCoinsResponse([], [], transaction_ids=[]) @marshal async def delete_unconfirmed_transactions(self, request: DeleteUnconfirmedTransactions) -> Empty: @@ -1730,74 +1734,63 @@ async def delete_unconfirmed_transactions(self, request: DeleteUnconfirmedTransa wallet.target_state = None return Empty() + @marshal async def select_coins( self, - request: dict[str, Any], - ) -> EndpointResult: + request: SelectCoins, + ) -> SelectCoinsResponse: assert self.service.logged_in_fingerprint is not None - tx_config_loader: TXConfigLoader = TXConfigLoader.from_json_dict(request) # Some backwards compat fill-ins - if tx_config_loader.excluded_coin_ids is None: - excluded_coins: Optional[list[dict[str, Any]]] = request.get("excluded_coins", request.get("exclude_coins")) - if excluded_coins is not None: - tx_config_loader = tx_config_loader.override( - excluded_coin_ids=[Coin.from_json_dict(c).name() for c in excluded_coins], + if request.excluded_coin_ids is None: + if request.exclude_coins is not None: + request = request.override( + excluded_coin_ids=[c.name() for c in request.exclude_coins], + exclude_coins=None, ) - tx_config: TXConfig = tx_config_loader.autofill( + # don't love this snippet of code + # but I think action scopes need to accept CoinSelectionConfigs + # instead of solely TXConfigs in order for this to be less ugly + autofilled_cs_config = request.autofill( constants=self.service.wallet_state_manager.constants, ) + tx_config = DEFAULT_TX_CONFIG.override( + **{ + field.name: getattr(autofilled_cs_config, field.name) + for field in dataclasses.fields(autofilled_cs_config) + } + ) if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before selecting coins") - amount = uint64(request["amount"]) - wallet_id = uint32(request["wallet_id"]) - - wallet = self.service.wallet_state_manager.wallets[wallet_id] + wallet = self.service.wallet_state_manager.wallets[request.wallet_id] async with self.service.wallet_state_manager.new_action_scope(tx_config, push=False) as action_scope: - selected_coins = await wallet.select_coins(amount, action_scope) + selected_coins = await wallet.select_coins(request.amount, action_scope) - return {"coins": [coin.to_json_dict() for coin in selected_coins]} + return SelectCoinsResponse(coins=list(selected_coins)) - async def get_spendable_coins(self, request: dict[str, Any]) -> EndpointResult: + @marshal + async def get_spendable_coins(self, request: GetSpendableCoins) -> GetSpendableCoinsResponse: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before getting all coins") - wallet_id = uint32(request["wallet_id"]) - min_coin_amount = uint64(request.get("min_coin_amount", 0)) - max_coin_amount: uint64 = uint64(request.get("max_coin_amount", 0)) - if max_coin_amount == 0: - max_coin_amount = uint64(self.service.wallet_state_manager.constants.MAX_COIN_AMOUNT) - excluded_coin_amounts: Optional[list[uint64]] = request.get("excluded_coin_amounts") - if excluded_coin_amounts is not None: - excluded_coin_amounts = [uint64(a) for a in excluded_coin_amounts] - else: - excluded_coin_amounts = [] - excluded_coins_input: Optional[dict[str, dict[str, Any]]] = request.get("excluded_coins") - if excluded_coins_input is not None: - excluded_coins = [Coin.from_json_dict(json_coin) for json_coin in excluded_coins_input.values()] - else: - excluded_coins = [] - excluded_coin_ids_input: Optional[list[str]] = request.get("excluded_coin_ids") - if excluded_coin_ids_input is not None: - excluded_coin_ids = [bytes32.from_hexstr(hex_id) for hex_id in excluded_coin_ids_input] - else: - excluded_coin_ids = [] state_mgr = self.service.wallet_state_manager - wallet = state_mgr.wallets[wallet_id] + wallet = state_mgr.wallets[request.wallet_id] async with state_mgr.lock: - all_coin_records = await state_mgr.coin_store.get_unspent_coins_for_wallet(wallet_id) + all_coin_records = await state_mgr.coin_store.get_unspent_coins_for_wallet(request.wallet_id) if wallet.type() in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}: assert isinstance(wallet, CATWallet) spendable_coins: list[WalletCoinRecord] = await wallet.get_cat_spendable_coins(all_coin_records) else: - spendable_coins = list(await state_mgr.get_spendable_coins_for_wallet(wallet_id, all_coin_records)) + spendable_coins = list( + await state_mgr.get_spendable_coins_for_wallet(request.wallet_id, all_coin_records) + ) # Now we get the unconfirmed transactions and manually derive the additions and removals. unconfirmed_transactions: list[TransactionRecord] = await state_mgr.tx_store.get_unconfirmed_for_wallet( - wallet_id + request.wallet_id ) unconfirmed_removal_ids: dict[bytes32, uint64] = { coin.name(): transaction.created_at_time @@ -1808,54 +1801,54 @@ async def get_spendable_coins(self, request: dict[str, Any]) -> EndpointResult: coin for transaction in unconfirmed_transactions for coin in transaction.additions - if await state_mgr.does_coin_belong_to_wallet(coin, wallet_id) + if await state_mgr.does_coin_belong_to_wallet(coin, request.wallet_id) ] valid_spendable_cr: list[CoinRecord] = [] unconfirmed_removals: list[CoinRecord] = [] for coin_record in all_coin_records: if coin_record.name() in unconfirmed_removal_ids: unconfirmed_removals.append(coin_record.to_coin_record(unconfirmed_removal_ids[coin_record.name()])) + + cs_config = request.autofill(constants=self.service.wallet_state_manager.constants) for coin_record in spendable_coins: # remove all the unconfirmed coins, exclude coins and dust. if coin_record.name() in unconfirmed_removal_ids: continue - if coin_record.coin in excluded_coins: - continue - if coin_record.name() in excluded_coin_ids: + if coin_record.coin.name() in cs_config.excluded_coin_ids: continue - if coin_record.coin.amount < min_coin_amount or coin_record.coin.amount > max_coin_amount: + if (coin_record.coin.amount < cs_config.min_coin_amount) or ( + coin_record.coin.amount > cs_config.max_coin_amount + ): continue - if coin_record.coin.amount in excluded_coin_amounts: + if coin_record.coin.amount in cs_config.excluded_coin_amounts: continue c_r = await state_mgr.get_coin_record_by_wallet_record(coin_record) assert c_r is not None and c_r.coin == coin_record.coin # this should never happen valid_spendable_cr.append(c_r) - return { - "confirmed_records": [cr.to_json_dict() for cr in valid_spendable_cr], - "unconfirmed_removals": [cr.to_json_dict() for cr in unconfirmed_removals], - "unconfirmed_additions": [coin.to_json_dict() for coin in unconfirmed_additions], - } + return GetSpendableCoinsResponse( + confirmed_records=valid_spendable_cr, + unconfirmed_removals=unconfirmed_removals, + unconfirmed_additions=unconfirmed_additions, + ) - async def get_coin_records_by_names(self, request: dict[str, Any]) -> EndpointResult: + @marshal + async def get_coin_records_by_names(self, request: GetCoinRecordsByNames) -> GetCoinRecordsByNamesResponse: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before finding coin information") - if "names" not in request: - raise ValueError("Names not in request") - coin_ids = [bytes32.from_hexstr(name) for name in request["names"]] kwargs: dict[str, Any] = { - "coin_id_filter": HashFilter.include(coin_ids), + "coin_id_filter": HashFilter.include(request.names), } confirmed_range = UInt32Range() - if "start_height" in request: - confirmed_range = dataclasses.replace(confirmed_range, start=uint32(request["start_height"])) - if "end_height" in request: - confirmed_range = dataclasses.replace(confirmed_range, stop=uint32(request["end_height"])) + if request.start_height is not None: + confirmed_range = dataclasses.replace(confirmed_range, start=request.start_height) + if request.end_height is not None: + confirmed_range = dataclasses.replace(confirmed_range, stop=request.end_height) if confirmed_range != UInt32Range(): kwargs["confirmed_range"] = confirmed_range - if "include_spent_coins" in request and not str2bool(request["include_spent_coins"]): + if not request.include_spent_coins: kwargs["spent_range"] = unspent_range async with self.service.wallet_state_manager.lock: @@ -1863,12 +1856,12 @@ async def get_coin_records_by_names(self, request: dict[str, Any]) -> EndpointRe **kwargs ) missed_coins: list[str] = [ - "0x" + c_id.hex() for c_id in coin_ids if c_id not in [cr.name for cr in coin_records] + "0x" + c_id.hex() for c_id in request.names if c_id not in [cr.name for cr in coin_records] ] if missed_coins: raise ValueError(f"Coin ID's: {missed_coins} not found.") - return {"coin_records": [cr.to_json_dict() for cr in coin_records]} + return GetCoinRecordsByNamesResponse(coin_records) @marshal async def get_current_derivation_index(self, request: Empty) -> GetCurrentDerivationIndexResponse: diff --git a/chia/wallet/wallet_rpc_client.py b/chia/wallet/wallet_rpc_client.py index 37491888f4c9..14d2a2fcd515 100644 --- a/chia/wallet/wallet_rpc_client.py +++ b/chia/wallet/wallet_rpc_client.py @@ -9,14 +9,13 @@ from chia.rpc.rpc_client import RpcClient from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program -from chia.types.coin_record import CoinRecord from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_to_json_dicts from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable -from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig +from chia.wallet.util.tx_config import TXConfig from chia.wallet.wallet_coin_store import GetCoinRecords from chia.wallet.wallet_request_types import ( AddKey, @@ -88,6 +87,8 @@ GatherSigningInfoResponse, GenerateMnemonicResponse, GetCATListResponse, + GetCoinRecordsByNames, + GetCoinRecordsByNamesResponse, GetCurrentDerivationIndexResponse, GetHeightInfoResponse, GetLoggedInFingerprintResponse, @@ -99,6 +100,8 @@ GetPrivateKey, GetPrivateKeyResponse, GetPublicKeysResponse, + GetSpendableCoins, + GetSpendableCoinsResponse, GetSyncStatusResponse, GetTimestampForHeight, GetTimestampForHeightResponse, @@ -157,6 +160,8 @@ PWSelfPoolResponse, PWStatus, PWStatusResponse, + SelectCoins, + SelectCoinsResponse, SendTransaction, SendTransactionMultiResponse, SendTransactionResponse, @@ -165,6 +170,8 @@ SignMessageByAddressResponse, SignMessageByID, SignMessageByIDResponse, + SpendClawbackCoins, + SpendClawbackCoinsResponse, SplitCoins, SplitCoinsResponse, SubmitTransactions, @@ -341,23 +348,16 @@ async def send_transaction_multi( async def spend_clawback_coins( self, - coin_ids: list[bytes32], - fee: int = 0, - force: bool = False, - push: bool = True, + request: SpendClawbackCoins, + tx_config: TXConfig, extra_conditions: tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), - ) -> dict[str, Any]: - request = { - "coin_ids": [cid.hex() for cid in coin_ids], - "fee": fee, - "force": force, - "extra_conditions": conditions_to_json_dicts(extra_conditions), - "push": push, - **timelock_info.to_json_dict(), - } - response = await self.fetch("spend_clawback_coins", request) - return response + ) -> SpendClawbackCoinsResponse: + return SpendClawbackCoinsResponse.from_json_dict( + await self.fetch( + "spend_clawback_coins", request.json_serialize_for_transport(tx_config, extra_conditions, timelock_info) + ) + ) async def delete_unconfirmed_transactions(self, request: DeleteUnconfirmedTransactions) -> None: await self.fetch("delete_unconfirmed_transactions", request.to_json_dict()) @@ -410,43 +410,19 @@ async def create_signed_transactions( response = await self.fetch("create_signed_transaction", request) return json_deserialize_with_clvm_streamable(response, CreateSignedTransactionsResponse) - async def select_coins(self, amount: int, wallet_id: int, coin_selection_config: CoinSelectionConfig) -> list[Coin]: - request = {"amount": amount, "wallet_id": wallet_id, **coin_selection_config.to_json_dict()} - response = await self.fetch("select_coins", request) - return [Coin.from_json_dict(coin) for coin in response["coins"]] + async def select_coins(self, request: SelectCoins) -> SelectCoinsResponse: + return SelectCoinsResponse.from_json_dict(await self.fetch("select_coins", request.to_json_dict())) async def get_coin_records(self, request: GetCoinRecords) -> dict[str, Any]: return await self.fetch("get_coin_records", request.to_json_dict()) - async def get_spendable_coins( - self, wallet_id: int, coin_selection_config: CoinSelectionConfig - ) -> tuple[list[CoinRecord], list[CoinRecord], list[Coin]]: - """ - We return a tuple containing: (confirmed records, unconfirmed removals, unconfirmed additions) - """ - request = {"wallet_id": wallet_id, **coin_selection_config.to_json_dict()} - response = await self.fetch("get_spendable_coins", request) - confirmed_wrs = [CoinRecord.from_json_dict(coin) for coin in response["confirmed_records"]] - unconfirmed_removals = [CoinRecord.from_json_dict(coin) for coin in response["unconfirmed_removals"]] - unconfirmed_additions = [Coin.from_json_dict(coin) for coin in response["unconfirmed_additions"]] - return confirmed_wrs, unconfirmed_removals, unconfirmed_additions - - async def get_coin_records_by_names( - self, - names: list[bytes32], - include_spent_coins: bool = True, - start_height: Optional[int] = None, - end_height: Optional[int] = None, - ) -> list[CoinRecord]: - names_hex = [name.hex() for name in names] - request = {"names": names_hex, "include_spent_coins": include_spent_coins} - if start_height is not None: - request["start_height"] = start_height - if end_height is not None: - request["end_height"] = end_height - - response = await self.fetch("get_coin_records_by_names", request) - return [CoinRecord.from_json_dict(cr) for cr in response["coin_records"]] + async def get_spendable_coins(self, request: GetSpendableCoins) -> GetSpendableCoinsResponse: + return GetSpendableCoinsResponse.from_json_dict(await self.fetch("get_spendable_coins", request.to_json_dict())) + + async def get_coin_records_by_names(self, request: GetCoinRecordsByNames) -> GetCoinRecordsByNamesResponse: + return GetCoinRecordsByNamesResponse.from_json_dict( + await self.fetch("get_coin_records_by_names", request.to_json_dict()) + ) # DID wallet async def create_new_did_wallet(