diff --git a/chia/_tests/pools/test_pool_rpc.py b/chia/_tests/pools/test_pool_rpc.py index 4ab209b99a28..d46cbc5a0abe 100644 --- a/chia/_tests/pools/test_pool_rpc.py +++ b/chia/_tests/pools/test_pool_rpc.py @@ -22,7 +22,7 @@ from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia._tests.util.setup_nodes import setup_simulators_and_wallets_service from chia._tests.util.time_out_assert import time_out_assert -from chia.pools.pool_wallet_info import PoolSingletonState, PoolWalletInfo +from chia.pools.pool_wallet_info import NewPoolWalletInitialTargetState, PoolSingletonState, PoolWalletInfo from chia.rpc.rpc_client import ResponseFailureError from chia.simulator.add_blocks_in_batches import add_blocks_in_batches from chia.simulator.block_tools import BlockTools, get_plot_dir @@ -41,6 +41,8 @@ from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_node import WalletNode from chia.wallet.wallet_request_types import ( + CreateNewWallet, + CreateNewWalletType, DeleteUnconfirmedTransactions, GetTransactions, GetWalletBalance, @@ -50,6 +52,7 @@ PWSelfPool, PWStatus, SendTransaction, + WalletCreationMode, ) from chia.wallet.wallet_rpc_client import WalletRpcClient from chia.wallet.wallet_service import WalletService @@ -269,14 +272,20 @@ async def create_new_plotnft( async with wallet_state_manager.new_action_scope(wallet_test_framework.tx_config, push=True) as action_scope: our_ph = await action_scope.get_puzzle_hash(wallet_state_manager) - await wallet_rpc.create_new_pool_wallet( - target_puzzlehash=our_ph, - backup_host="", - mode="new", - relative_lock_height=uint32(0) if self_pool else LOCK_HEIGHT, - state="SELF_POOLING" if self_pool else "FARMING_TO_POOL", - pool_url="" if self_pool else "http://pool.example.com", - fee=uint64(0), + await wallet_rpc.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + target_puzzle_hash=our_ph, + state="SELF_POOLING" if self_pool else "FARMING_TO_POOL", + pool_url=None if self_pool else "http://pool.example.com", + relative_lock_height=None if self_pool else LOCK_HEIGHT, + ), + mode=WalletCreationMode.NEW, + fee=uint64(0), + push=True, + ), + wallet_test_framework.tx_config, ) return await process_plotnft_create( @@ -297,15 +306,22 @@ async def test_create_new_pool_wallet_self_farm( client, wallet_node, full_node_api, _total_block_rewards, _ = one_wallet_node_and_rpc wallet = wallet_node.wallet_state_manager.main_wallet - async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope: - our_ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET.value))) assert len(summaries_response.wallets) == 0 - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", uint32(0), f"{self_hostname}:5000", "new", "SELF_POOLING", fee + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + state="SELF_POOLING", + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.process_transaction_records(records=[creation_tx]) + await full_node_api.process_transaction_records(records=create_response.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=30) summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET.value))) @@ -334,7 +350,7 @@ async def test_create_new_pool_wallet_self_farm( ) # It can be one of multiple launcher IDs, due to selecting a different coin launcher_id = None - for addition in creation_tx.additions: + for addition in create_response.transactions[0].additions: if addition.puzzle_hash == SINGLETON_LAUNCHER_HASH: launcher_id = addition.name() break @@ -357,10 +373,22 @@ async def test_create_new_pool_wallet_farm_to_pool( summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET))) assert len(summaries_response.wallets) == 0 - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "http://pool.example.com", uint32(10), f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + target_puzzle_hash=our_ph, + state="FARMING_TO_POOL", + pool_url="http://pool.example.com", + relative_lock_height=uint32(10), + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.process_transaction_records(records=[creation_tx]) + await full_node_api.process_transaction_records(records=create_response.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET))) @@ -389,7 +417,7 @@ async def test_create_new_pool_wallet_farm_to_pool( ) # It can be one of multiple launcher IDs, due to selecting a different coin launcher_id = None - for addition in creation_tx.additions: + for addition in create_response.transactions[0].additions: if addition.puzzle_hash == SINGLETON_LAUNCHER_HASH: launcher_id = addition.name() break @@ -415,15 +443,38 @@ async def test_create_multiple_pool_wallets( summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET))) assert len(summaries_response.wallets) == 0 - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph_1, "", uint32(0), f"{self_hostname}:5000", "new", "SELF_POOLING", fee + create_response_1 = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + state="SELF_POOLING", + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) - creation_tx_2: TransactionRecord = await client.create_new_pool_wallet( - our_ph_1, self_hostname, uint32(12), f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee + create_response_2 = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + target_puzzle_hash=our_ph_1, + state="FARMING_TO_POOL", + pool_url=self_hostname, + relative_lock_height=uint32(12), + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.process_transaction_records(records=[creation_tx, creation_tx_2]) + await full_node_api.process_transaction_records( + records=[*create_response_1.transactions, *create_response_2.transactions] + ) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) async def pw_created(check_wallet_id: int) -> bool: @@ -471,10 +522,19 @@ def mempool_empty() -> bool: for i in range(5): await time_out_assert(10, mempool_empty) - res = await client.create_new_cat_and_wallet(uint64(20), test=True) - assert res["success"] - cat_0_id = res["wallet_id"] - asset_id = bytes.fromhex(res["asset_id"]) + create_cat_res = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.NEW, + amount=uint64(20), + test=True, + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, + ) + cat_0_id = create_cat_res.wallet_id + asset_id = create_cat_res.asset_id + assert asset_id is not None # mypy doesn't know about __post_init__ assert len(asset_id) > 0 await full_node_api.process_all_wallet_transactions(wallet=wallet) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) @@ -486,10 +546,22 @@ def mempool_empty() -> bool: if not trusted: for i in range(22): await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) - creation_tx_3: TransactionRecord = await client.create_new_pool_wallet( - our_ph_1, self_hostname, uint32(5), f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + target_puzzle_hash=our_ph_1, + state="FARMING_TO_POOL", + pool_url=self_hostname, + relative_lock_height=uint32(5), + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.process_transaction_records(records=[creation_tx_3]) + await full_node_api.process_transaction_records(records=create_response.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) full_config = load_config(wallet.wallet_state_manager.root_path, "config.yaml") @@ -524,10 +596,19 @@ async def test_absorb_self( summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET))) assert len(summaries_response.wallets) == 0 - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", uint32(0), f"{self_hostname}:5000", "new", "SELF_POOLING", fee + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + state="SELF_POOLING", + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.process_transaction_records(records=[creation_tx]) + await full_node_api.process_transaction_records(records=create_response.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) status: PoolWalletInfo = (await client.pw_status(PWStatus(uint32(2)))).state @@ -633,10 +714,19 @@ async def test_absorb_self_multiple_coins( assert len(summaries_response.wallets) == 0 main_expected_confirmed_balance = total_block_rewards - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", uint32(0), f"{self_hostname}:5000", "new", "SELF_POOLING", fee + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + state="SELF_POOLING", + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.process_transaction_records(records=[creation_tx]) + await full_node_api.process_transaction_records(records=create_response.transactions) main_expected_confirmed_balance -= fee main_expected_confirmed_balance -= 1 pool_expected_confirmed_balance = 0 @@ -708,10 +798,22 @@ async def test_absorb_pooling( await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET))) assert len(summaries_response.wallets) == 0 - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "http://123.45.67.89", uint32(10), f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + target_puzzle_hash=our_ph, + state="FARMING_TO_POOL", + pool_url="http://123.45.67.89", + relative_lock_height=uint32(10), + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.process_transaction_records(records=[creation_tx]) + await full_node_api.process_transaction_records(records=create_response.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) main_expected_confirmed_balance -= 1 main_expected_confirmed_balance -= fee @@ -827,7 +929,7 @@ async def test_self_pooling_to_pooling(self, setup: Setup, fee: uint64, self_hos if fee != 0: pytest.skip("need to fix this test for non-zero fees") - full_node_api, wallet_node, our_ph, _total_block_rewards, client = setup + full_node_api, wallet_node, _our_ph, _total_block_rewards, client = setup pool_ph = bytes32.zeros assert wallet_node._wallet_state_manager is not None @@ -835,20 +937,38 @@ async def test_self_pooling_to_pooling(self, setup: Setup, fee: uint64, self_hos summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET))) assert len(summaries_response.wallets) == 0 - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", uint32(0), f"{self_hostname}:5000", "new", "SELF_POOLING", fee + create_response_1 = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + state="SELF_POOLING", + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.wait_transaction_records_entered_mempool(records=[creation_tx]) - creation_tx_2: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", uint32(0), f"{self_hostname}:5001", "new", "SELF_POOLING", fee + await full_node_api.wait_transaction_records_entered_mempool(records=create_response_1.transactions) + create_response_2 = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + state="SELF_POOLING", + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - for r in creation_tx.removals: - assert r not in creation_tx_2.removals + for r in create_response_1.transactions[0].removals: + assert r not in create_response_2.transactions[0].removals - await full_node_api.process_transaction_records(records=[creation_tx_2]) + await full_node_api.process_transaction_records(records=create_response_2.transactions) - assert not full_node_api.txs_in_mempool(txs=[creation_tx]) + assert not full_node_api.txs_in_mempool(txs=create_response_1.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) summaries_response = await client.get_wallets(GetWallets(uint16(WalletType.POOLING_WALLET))) @@ -927,14 +1047,23 @@ async def test_leave_pool(self, setup: Setup, fee: uint64, self_hostname: str) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", uint32(0), f"{self_hostname}:5000", "new", "SELF_POOLING", fee + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + state="SELF_POOLING", + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.wait_transaction_records_entered_mempool(records=[creation_tx]) + await full_node_api.wait_transaction_records_entered_mempool(records=create_response.transactions) await full_node_api.farm_blocks_to_puzzlehash(count=6, farm_to=our_ph, guarantee_transaction_blocks=True) - assert not full_node_api.txs_in_mempool(txs=[creation_tx]) + assert not full_node_api.txs_in_mempool(txs=create_response.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) @@ -1119,14 +1248,26 @@ async def test_change_pools_reorg(self, setup: Setup, fee: uint64, self_hostname await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) - creation_tx: TransactionRecord = await client.create_new_pool_wallet( - pool_a_ph, "https://pool-a.org", uint32(5), f"{self_hostname}:5000", "new", "FARMING_TO_POOL", uint64(fee) + create_response = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + target_puzzle_hash=pool_a_ph, + state="FARMING_TO_POOL", + pool_url="https://pool-a.org", + relative_lock_height=uint32(5), + ), + mode=WalletCreationMode.NEW, + fee=uint64(0), + push=True, + ), + DEFAULT_TX_CONFIG, ) - await full_node_api.wait_transaction_records_entered_mempool(records=[creation_tx]) + await full_node_api.wait_transaction_records_entered_mempool(records=create_response.transactions) await full_node_api.farm_blocks_to_puzzlehash(count=6, farm_to=our_ph, guarantee_transaction_blocks=True) - assert not full_node_api.txs_in_mempool(txs=[creation_tx]) + assert not full_node_api.txs_in_mempool(txs=create_response.transactions) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) diff --git a/chia/_tests/wallet/did_wallet/test_did.py b/chia/_tests/wallet/did_wallet/test_did.py index 45fc25b39cb8..d8fcf81e9635 100644 --- a/chia/_tests/wallet/did_wallet/test_did.py +++ b/chia/_tests/wallet/did_wallet/test_did.py @@ -34,7 +34,14 @@ from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_node import WalletNode -from chia.wallet.wallet_request_types import DIDFindLostDID, DIDGetCurrentCoinInfo, DIDGetInfo +from chia.wallet.wallet_request_types import ( + CreateNewWallet, + CreateNewWalletType, + DIDFindLostDID, + DIDGetCurrentCoinInfo, + DIDGetInfo, + DIDType, +) from chia.wallet.wallet_rpc_api import WalletRpcApi @@ -284,11 +291,14 @@ async def test_creation_from_backup_file(wallet_environments: WalletTestFramewor backup_data = did_wallet_1.create_backup() # Wallet2 recovers DIDWallet2 to a new set of keys - await env_2.rpc_client.create_new_did_wallet( - uint64(1), - DEFAULT_TX_CONFIG, - type="recovery", - backup_data=backup_data, + await env_2.rpc_client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.DID_WALLET, + did_type=DIDType.RECOVERY, + backup_data=backup_data, + push=True, + ), + wallet_environments.tx_config, ) did_wallet_2 = env_2.wallet_state_manager.get_wallet(id=uint32(2), required_type=DIDWallet) current_coin_info_response = await env_0.rpc_client.did_get_current_coin_info( 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 f6b1cff0361d..ac905f4f9d8b 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,15 @@ 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, SelectCoins +from chia.wallet.wallet_request_types import ( + CreateNewWallet, + CreateNewWalletType, + NFTGetNFTs, + NFTMintBulk, + NFTMintMetadata, + PushTransactions, + SelectCoins, +) async def nft_count(wallet: NFTWallet) -> int: @@ -263,9 +271,15 @@ async def test_nft_mint_rpc(wallet_environments: WalletTestFramework, zero_royal hex_did_id = did_wallet_maker.get_my_DID() hmr_did_id = encode_puzzle_hash(bytes32.from_hexstr(hex_did_id), AddressType.DID.hrp(env_0.node.config)) - nft_wallet_maker = await env_0.rpc_client.create_new_nft_wallet(name="NFT WALLET 1", did_id=hmr_did_id) + create_wallet_res = await env_0.rpc_client.create_new_wallet( + CreateNewWallet(wallet_type=CreateNewWalletType.NFT_WALLET, name="NFT WALLET 1", did_id=hmr_did_id, push=True), + wallet_environments.tx_config, + ) - await env_1.rpc_client.create_new_nft_wallet(name="NFT WALLET 2", did_id=None) + await env_1.rpc_client.create_new_wallet( + CreateNewWallet(wallet_type=CreateNewWalletType.NFT_WALLET, name="NFT WALLET 2", did_id=None, push=True), + wallet_environments.tx_config, + ) await env_0.change_balances({"nft": {"init": True}}) await env_1.change_balances({"nft": {"init": True}}) @@ -316,7 +330,7 @@ async def test_nft_mint_rpc(wallet_environments: WalletTestFramework, zero_royal for i in range(0, n, chunk): resp = await env_0.rpc_client.nft_mint_bulk( NFTMintBulk( - wallet_id=nft_wallet_maker["wallet_id"], + wallet_id=create_wallet_res.wallet_id, metadata_list=[NFTMintMetadata.from_json_dict(metadata) for metadata in metadata_list[i : i + chunk]], target_list=target_list[i : i + chunk], royalty_percentage=uint16.construct_optional(royalty_percentage), diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index 65033b8e3f5d..e4adc32f512a 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -118,6 +118,8 @@ CheckOfferValidity, ClawbackPuzzleDecoratorOverride, CombineCoins, + CreateNewWallet, + CreateNewWalletType, CreateOfferForIDs, CreateSignedTransaction, DefaultCAT, @@ -132,6 +134,7 @@ DIDMessageSpend, DIDSetWalletName, DIDTransferDID, + DIDType, DIDUpdateMetadata, FungibleAsset, GetAllOffers, @@ -169,6 +172,7 @@ TakeOffer, VerifySignature, VerifySignatureResponse, + WalletCreationMode, ) from chia.wallet.wallet_rpc_api import WalletRpcApi from chia.wallet.wallet_rpc_client import WalletRpcClient @@ -642,9 +646,17 @@ async def test_create_signed_transaction( if is_cat: generated_funds = 10**9 - res = await wallet_1_rpc.create_new_cat_and_wallet(uint64(generated_funds), test=True) - assert res["success"] - wallet_id = res["wallet_id"] + create_cat_res = await wallet_1_rpc.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.NEW, + amount=uint64(generated_funds), + test=True, + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, + ) + wallet_id = create_cat_res.wallet_id await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) for _ in range(5): @@ -1248,10 +1260,17 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty assert asset_to_name_response.name == next(iter(DEFAULT_CATS.items()))[1]["name"] # Creates a second wallet with the same CAT - res = await env_1.rpc_client.create_wallet_for_existing_cat(asset_id) - assert res["success"] - cat_1_id = res["wallet_id"] - cat_1_asset_id = bytes.fromhex(res["asset_id"]) + create_wallet_res = await env_1.rpc_client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.EXISTING, + asset_id=asset_id, + push=True, + ), + tx_config=wallet_environments.tx_config, + ) + cat_1_id = create_wallet_res.wallet_id + cat_1_asset_id = create_wallet_res.asset_id assert cat_1_asset_id == asset_id await wallet_environments.process_pending_states( @@ -1505,7 +1524,15 @@ async def test_offer_endpoints(wallet_environments: WalletTestFramework, wallet_ cat_asset_id = cat_wallet.cat_info.limitations_program_hash # Creates a wallet for the same CAT on wallet_2 and send 4 CAT from wallet_1 to it - await env_2.rpc_client.create_wallet_for_existing_cat(cat_asset_id) + await env_2.rpc_client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.EXISTING, + asset_id=cat_asset_id, + push=True, + ), + tx_config=wallet_environments.tx_config, + ) wallet_2_address = (await env_2.rpc_client.get_next_address(GetNextAddress(cat_wallet_id, False))).address adds = [Addition(puzzle_hash=decode_puzzle_hash(wallet_2_address), amount=uint64(4), memos=["the cat memo"])] tx_res = ( @@ -2036,15 +2063,23 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment) - await generate_funds(env.full_node.api, env.wallet_1, 5) # Create a DID wallet - res = await wallet_1_rpc.create_new_did_wallet(amount=1, tx_config=DEFAULT_TX_CONFIG, name="Profile 1") - assert res["success"] - did_wallet_id_0 = res["wallet_id"] - did_id_0 = res["my_did"] + create_new_res = await wallet_1_rpc.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.DID_WALLET, + did_type=DIDType.NEW, + amount=uint64(1), + wallet_name="Profile 1", + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, + ) + did_wallet_id_0 = create_new_res.wallet_id + did_id_0 = create_new_res.my_did # Get wallet name get_name_res = await wallet_1_rpc.did_get_wallet_name(DIDGetWalletName(did_wallet_id_0)) assert get_name_res.name == "Profile 1" - nft_wallet = wallet_1_node.wallet_state_manager.wallets[did_wallet_id_0 + 1] + nft_wallet = wallet_1_node.wallet_state_manager.wallets[uint32(did_wallet_id_0 + 1)] assert isinstance(nft_wallet, NFTWallet) assert nft_wallet.get_name() == "Profile 1 NFT Wallet" @@ -2153,8 +2188,11 @@ async def test_nft_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment) - await generate_funds(env.full_node.api, env.wallet_1, 5) - res = await wallet_1_rpc.create_new_nft_wallet(None) - nft_wallet_id = res["wallet_id"] + create_wallet_res = await wallet_1_rpc.create_new_wallet( + CreateNewWallet(wallet_type=CreateNewWalletType.NFT_WALLET, did_id=None, push=True), + DEFAULT_TX_CONFIG, + ) + nft_wallet_id = create_wallet_res.wallet_id mint_res = await wallet_1_rpc.mint_nft( request=NFTMintNFTRequest( wallet_id=nft_wallet_id, @@ -3012,13 +3050,19 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes client: WalletRpcClient = env.wallet_1.rpc_client await generate_funds(full_node_api, env.wallet_1) wc = env.wallet_1.rpc_client - await wc.create_new_did_wallet(1, DEFAULT_TX_CONFIG, 0) + await wc.create_new_wallet( + CreateNewWallet(wallet_type=CreateNewWalletType.DID_WALLET, did_type=DIDType.NEW, amount=uint64(1), push=True), + DEFAULT_TX_CONFIG, + ) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) await farm_transaction_block(full_node_api, env.wallet_1.node) await time_out_assert(20, check_client_synced, True, wc) - nft_wallet = await wc.create_new_nft_wallet(None) - nft_wallet_id = nft_wallet["wallet_id"] + create_wallet_res = await wc.create_new_wallet( + CreateNewWallet(wallet_type=CreateNewWalletType.NFT_WALLET, did_id=None, push=True), + DEFAULT_TX_CONFIG, + ) + nft_wallet_id = create_wallet_res.wallet_id address = (await wc.get_next_address(GetNextAddress(env.wallet_1.wallet.id(), True))).address await wc.mint_nft( request=NFTMintNFTRequest( @@ -3227,9 +3271,16 @@ async def test_cat_spend_run_tail(wallet_rpc_environment: WalletRpcTestEnvironme await farm_transaction(full_node_api, wallet_node, eve_spend) # Make sure we have the CAT - res = await client.create_wallet_for_existing_cat(Program.to(None).get_tree_hash()) - assert res["success"] - cat_wallet_id = res["wallet_id"] + create_wallet_res = await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.EXISTING, + asset_id=Program.to(None).get_tree_hash(), + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, + ) + cat_wallet_id = create_wallet_res.wallet_id await time_out_assert(20, get_confirmed_balance, tx_amount, client, cat_wallet_id) # Attempt to melt it fully @@ -3270,11 +3321,28 @@ async def test_get_balances(wallet_rpc_environment: WalletRpcTestEnvironment) -> await time_out_assert(20, check_client_synced, True, client) # Creates a CAT wallet with 100 mojos and a CAT with 20 mojos - await client.create_new_cat_and_wallet(uint64(100), test=True) + await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.NEW, + amount=uint64(100), + test=True, + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, + ) await time_out_assert(20, check_client_synced, True, client) - res = await client.create_new_cat_and_wallet(uint64(20), test=True) - assert res["success"] + await client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.NEW, + amount=uint64(20), + test=True, + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, + ) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 2) await farm_transaction_block(full_node_api, wallet_node) await time_out_assert(20, check_client_synced, True, client) diff --git a/chia/cmds/plotnft_funcs.py b/chia/cmds/plotnft_funcs.py index 6b3bd830776a..46c94c02ef34 100644 --- a/chia/cmds/plotnft_funcs.py +++ b/chia/cmds/plotnft_funcs.py @@ -30,7 +30,7 @@ load_pool_config, update_pool_config, ) -from chia.pools.pool_wallet_info import PoolSingletonState, PoolWalletInfo +from chia.pools.pool_wallet_info import NewPoolWalletInitialTargetState, PoolSingletonState, PoolWalletInfo from chia.protocols.pool_protocol import POOL_PROTOCOL_VERSION from chia.rpc.rpc_client import ResponseFailureError from chia.server.server import ssl_context_for_root @@ -38,11 +38,12 @@ from chia.util.bech32m import encode_puzzle_hash from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.errors import CliRpcConnectionError -from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.address_type import AddressType from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_request_types import ( + CreateNewWallet, + CreateNewWalletType, GetTransaction, GetWalletBalance, GetWallets, @@ -51,6 +52,7 @@ PWSelfPool, PWStatus, TransactionEndpointResponse, + WalletCreationMode, WalletInfoResponse, ) from chia.wallet.wallet_rpc_client import WalletRpcClient @@ -91,7 +93,7 @@ async def create( # Could use initial_pool_state_from_dict to simplify if state == "SELF_POOLING": pool_url = None - relative_lock_height = uint32(0) + relative_lock_height = None target_puzzle_hash = None # wallet will fill this in elif state == "FARMING_TO_POOL": enforce_https = wallet_info.config["selected_network"] == "mainnet" @@ -110,22 +112,31 @@ async def create( cli_confirm("Confirm (y/n): ", "Aborting.") try: - tx_record: TransactionRecord = await wallet_info.client.create_new_pool_wallet( - target_puzzle_hash, - pool_url, - relative_lock_height, - "localhost:5000", - "new", - state, - fee, + create_response = await wallet_info.client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.POOL_WALLET, + initial_target_state=NewPoolWalletInitialTargetState( + target_puzzle_hash=target_puzzle_hash, + state=state, + pool_url=pool_url, + relative_lock_height=relative_lock_height, + ), + mode=WalletCreationMode.NEW, + fee=fee, + push=True, + ), + DEFAULT_TX_CONFIG, ) + assert create_response.transaction is not None # mypy doesn't know about __post_init__ start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) - tx = (await wallet_info.client.get_transaction(GetTransaction(tx_record.name))).transaction + tx = ( + await wallet_info.client.get_transaction(GetTransaction(create_response.transaction.name)) + ).transaction if len(tx.sent_to) > 0: print(transaction_submitted_msg(tx)) - print(transaction_status_msg(wallet_info.fingerprint, tx_record.name)) + print(transaction_status_msg(wallet_info.fingerprint, create_response.transaction.name)) return None except Exception as e: raise CliRpcConnectionError( diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 437dfb420336..666b0284c968 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -53,6 +53,8 @@ CATSpend, CATSpendResponse, ClawbackPuzzleDecoratorOverride, + CreateNewWallet, + CreateNewWalletType, CreateOfferForIDs, DeleteNotifications, DeleteUnconfirmedTransactions, @@ -62,6 +64,7 @@ DIDMessageSpend, DIDSetWalletName, DIDTransferDID, + DIDType, DIDUpdateMetadata, ExtendDerivationIndex, FungibleAsset, @@ -99,6 +102,7 @@ VCMint, VCRevoke, VCSpend, + WalletCreationMode, ) from chia.wallet.wallet_rpc_client import WalletRpcClient @@ -484,8 +488,16 @@ async def add_token( existing_info = await wallet_client.cat_asset_id_to_name(CATAssetIDToName(asset_id)) if existing_info.wallet_id is None: - response = await wallet_client.create_wallet_for_existing_cat(asset_id) - wallet_id = response["wallet_id"] + response = await wallet_client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.CAT_WALLET, + mode=WalletCreationMode.EXISTING, + asset_id=asset_id, + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, + ) + wallet_id = response.wallet_id await wallet_client.set_cat_name(CATSetName(wallet_id, token_name)) print(f"Successfully added {token_name} with wallet id {wallet_id} on key {fingerprint}") else: @@ -1045,16 +1057,20 @@ async def create_did_wallet( ) -> list[TransactionRecord]: async with get_wallet_client(root_path, wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: - response = await wallet_client.create_new_did_wallet( - amount, - CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), - fee, - name, - push=push, + response = await wallet_client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.DID_WALLET, + did_type=DIDType.NEW, + amount=uint64(amount), + fee=fee, + wallet_name=name, + push=push, + ), + tx_config=CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), timelock_info=condition_valid_times, ) - wallet_id = response["wallet_id"] - my_did = response["my_did"] + wallet_id = response.wallet_id + my_did = response.my_did print(f"Successfully created a DID wallet with name {name} and id {wallet_id} on key {fingerprint}") print(f"Successfully created a DID {my_did} in the newly created DID wallet") return [] # TODO: fix this endpoint to return transactions @@ -1246,9 +1262,16 @@ async def create_nft_wallet( ) -> None: async with get_wallet_client(root_path, wallet_rpc_port, fp) as (wallet_client, fingerprint, _): try: - response = await wallet_client.create_new_nft_wallet(did_id.original_address if did_id else None, name) - wallet_id = response["wallet_id"] - print(f"Successfully created an NFT wallet with id {wallet_id} on key {fingerprint}") + response = await wallet_client.create_new_wallet( + CreateNewWallet( + wallet_type=CreateNewWalletType.NFT_WALLET, + did_id=(did_id.original_address if did_id else None), + name=name, + push=True, + ), + DEFAULT_TX_CONFIG, + ) + print(f"Successfully created an NFT wallet with id {response.wallet_id} on key {fingerprint}") except Exception as e: print(f"Failed to create NFT wallet: {e}") diff --git a/chia/pools/pool_wallet_info.py b/chia/pools/pool_wallet_info.py index c70dd528a871..47fbf1bb233c 100644 --- a/chia/pools/pool_wallet_info.py +++ b/chia/pools/pool_wallet_info.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from enum import IntEnum -from typing import Any, Optional +from typing import Optional from chia_rs import G1Element from chia_rs.sized_bytes import bytes32 @@ -61,22 +61,52 @@ class PoolState(Streamable): relative_lock_height: uint32 +@streamable +@dataclass(frozen=True) +class NewPoolWalletInitialTargetState(Streamable): + state: str # must map to name of PoolSingletonState Enum + # only when state == "FARMING_TO_POOL" + target_puzzle_hash: Optional[bytes32] = None + pool_url: Optional[str] = None + relative_lock_height: Optional[uint32] = None + + def __post_init__(self) -> None: + if self.state not in {member.name for member in PoolSingletonState}: + raise ValueError(f"Invalid pool wallet initial state: {self.state}") + if PoolSingletonState[self.state] == PoolSingletonState.FARMING_TO_POOL: + if self.target_puzzle_hash is None: + raise ValueError("target_puzzle_hash must be set when state is FARMING_TO_POOL") + if self.pool_url is None: + raise ValueError("pool_url must be set when state is FARMING_TO_POOL") + if self.relative_lock_height is None: + raise ValueError("relative_lock_height must be set when state is FARMING_TO_POOL") + else: + if self.target_puzzle_hash is not None: + raise ValueError("target_puzzle_hash is only valid for FARMING_TO_POOL") + if self.pool_url is not None: + raise ValueError("pool_url is only valid for FARMING_TO_POOL") + if self.relative_lock_height is not None: + raise ValueError("relative_lock_height is only valid for FARMING_TO_POOL") + + super().__post_init__() + + def initial_pool_state_from_dict( - state_dict: dict[str, Any], + initial_state: NewPoolWalletInitialTargetState, owner_pubkey: G1Element, owner_puzzle_hash: bytes32, ) -> PoolState: - state_str = state_dict["state"] - singleton_state: PoolSingletonState = PoolSingletonState[state_str] + singleton_state: PoolSingletonState = PoolSingletonState[initial_state.state] if singleton_state == SELF_POOLING: target_puzzle_hash = owner_puzzle_hash pool_url: str = "" relative_lock_height = uint32(0) elif singleton_state == FARMING_TO_POOL: - target_puzzle_hash = bytes32.from_hexstr(state_dict["target_puzzle_hash"]) - pool_url = state_dict["pool_url"] - relative_lock_height = uint32(state_dict["relative_lock_height"]) + # mypy doesn't know about our __post_init__ + target_puzzle_hash = initial_state.target_puzzle_hash # type: ignore[assignment] + pool_url = initial_state.pool_url # type: ignore[assignment] + relative_lock_height = initial_state.relative_lock_height # type: ignore[assignment] else: raise ValueError("Initial state must be SELF_POOLING or FARMING_TO_POOL") diff --git a/chia/wallet/util/wallet_types.py b/chia/wallet/util/wallet_types.py index 5830addd4c70..4e7cd23f134a 100644 --- a/chia/wallet/util/wallet_types.py +++ b/chia/wallet/util/wallet_types.py @@ -33,8 +33,8 @@ class WalletType(IntEnum): def to_json_dict(self) -> str: # yes, this isn't a `dict`, but it is json and # unfortunately the magic method name is misleading - # not sure this code is used - # TODO: determine if this code is used and if not, remove it + + # This gets called with EnhancedJSONEncoder in the RPC return self.name diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index ee4b6322464b..17febb666529 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -1,7 +1,8 @@ from __future__ import annotations import sys -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields +from enum import Enum from functools import cached_property from typing import Any, BinaryIO, Optional, TypeVar, Union, final @@ -12,12 +13,13 @@ from chia.data_layer.data_layer_wallet import DataLayerSummary, Mirror from chia.data_layer.singleton_record import SingletonRecord -from chia.pools.pool_wallet_info import PoolWalletInfo +from chia.pools.pool_wallet_info import NewPoolWalletInitialTargetState, PoolWalletInfo +from chia.types.blockchain_format.coin import coin_as_list 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.hash import std_hash -from chia.util.streamable import Streamable, streamable +from chia.util.streamable import Streamable, streamable, streamable_enum from chia.wallet.conditions import ( AssertCoinAnnouncement, AssertPuzzleAnnouncement, @@ -43,6 +45,7 @@ from chia.wallet.util.puzzle_decorator_type import PuzzleDecoratorType from chia.wallet.util.query_filter import TransactionTypeFilter from chia.wallet.util.tx_config import CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig +from chia.wallet.util.wallet_types import WalletType from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_node import Balance @@ -2186,3 +2189,249 @@ class CancelOffers(TransactionEndpointRequest): @dataclass(frozen=True) class CancelOffersResponse(TransactionEndpointResponse): pass + + +# utilities for CreateNewWallet +@streamable_enum(str) +class CreateNewWalletType(Enum): + CAT_WALLET = "cat_wallet" + DID_WALLET = "did_wallet" + NFT_WALLET = "nft_wallet" + POOL_WALLET = "pool_wallet" + + +@streamable_enum(str) +class WalletCreationMode(Enum): + NEW = "new" + EXISTING = "existing" + + +@streamable_enum(str) +class DIDType(Enum): + NEW = "new" + RECOVERY = "recovery" + + +@streamable +@dataclass(frozen=True) +class CreateNewWallet(TransactionEndpointRequest): + wallet_type: CreateNewWalletType = field(default_factory=default_raise) + # CAT_WALLET + mode: Optional[WalletCreationMode] = None # required + amount: Optional[uint64] = None # required in "new" + name: Optional[str] = None # If not provided, the name will be autogenerated based on the tail hash + test: bool = False # must be True in "new" + asset_id: Optional[bytes32] = None # required in "existing" + + # DID_WALLET + did_type: Optional[DIDType] = None # required + # only in "new" + # amount: uint64 # already defined, required + backup_dids: list[str] = field(default_factory=list) # must error if not [] + metadata: dict[str, str] = field(default_factory=dict) + wallet_name: Optional[str] = None + # only in "recovery" + backup_data: Optional[str] = None # required + + # NFT_WALLET + did_id: Optional[str] = None + # name: Optional[str] = None # already defined + + # POOL_WALLET + # mode: WalletCreationMode # already defined, required, must be "new" + initial_target_state: Optional[NewPoolWalletInitialTargetState] = None # required + p2_singleton_delayed_ph: Optional[bytes32] = None + p2_singleton_delay_time: Optional[uint64] = None + + def __post_init__(self) -> None: + if self.wallet_type == CreateNewWalletType.CAT_WALLET: + if self.mode is None: + raise ValueError('Must specify a "mode" when creating a new CAT wallet') + if self.mode == WalletCreationMode.NEW: + if not self.test: + raise ValueError( + "Support for this RPC mode has been dropped." + " Please use the CAT Admin Tool @ https://github.com/Chia-Network/CAT-admin-tool instead." + ) + if self.amount is None: + raise ValueError('Must specify an "amount" of CATs to generate') + if self.asset_id is not None: + raise ValueError('"asset_id" is not an argument for new CAT wallets. Maybe you meant existing?') + if self.mode == WalletCreationMode.EXISTING: + if self.asset_id is None: + raise ValueError('Must specify an "asset_id" when creating an existing CAT wallet') + if self.amount is not None: + raise ValueError('"amount" is not an argument for existing CAT wallets') + elif self.test: + raise ValueError('"test" mode is not supported except for new CAT wallets') + else: + if self.asset_id is not None: + raise ValueError( + '"asset_id" is not a valid argument. Maybe you meant to create an existing CAT wallet?' + ) + if self.mode is not None and self.mode != WalletCreationMode.NEW: + raise ValueError('"mode": "existing" is only valid for CAT wallets') + + if self.wallet_type == CreateNewWalletType.DID_WALLET: + if self.did_type is None: + raise ValueError('Must specify "did_type": "new/recovery"') + if self.did_type == DIDType.NEW: + if self.amount is None: + raise ValueError('Must specify an "amount" when creating a new DID') + if self.backup_dids != []: + raise ValueError('Recovery options are no longer supported. "backup_dids" cannot be set.') + if self.backup_data is not None: + raise ValueError('"backup_data" is only an option in "did_type": "recovery"') + if self.did_type == DIDType.RECOVERY: + if self.amount is not None: + raise ValueError('Cannot specify an "amount" when recovering a DID') + if self.backup_dids != []: + raise ValueError('Cannot specify "backup_dids" when recovering a DID') + if self.metadata != {}: + raise ValueError('Cannot specify "metadata" when recovering a DID') + if self.backup_data is None: + raise ValueError('Must specify "backup_data" when recovering a DID') + else: + if self.did_type is not None: + raise ValueError('"did_type" is only a valid argument for DID wallets') + if self.backup_dids != []: + raise ValueError('"backup_dids" is only a valid argument for DID wallets') + if self.metadata != {}: + raise ValueError('"metadata" is only a valid argument for DID wallets') + if self.wallet_name is not None: + raise ValueError('"wallet_name" is only a valid argument for DID wallets') + if self.backup_data is not None: + raise ValueError('"backup_data" is only a valid argument for DID wallets') + + if self.wallet_type != CreateNewWalletType.NFT_WALLET and self.did_id is not None: + raise ValueError('"did_id" is only a valid argument for NFT wallets') + + if self.wallet_type == CreateNewWalletType.POOL_WALLET: + if self.initial_target_state is None: + raise ValueError('"initial_target_state" is required for new pool wallets') + else: + if self.initial_target_state is not None: + raise ValueError('"initial_target_state" is only a valid argument for pool wallets') + if self.p2_singleton_delayed_ph is not None: + raise ValueError('"p2_singleton_delayed_ph" is only a valid argument for pool wallets') + if self.p2_singleton_delay_time is not None: + raise ValueError('"p2_singleton_delay_time" is only a valid argument for pool wallets') + + super().__post_init__() + + +@streamable +@dataclass(frozen=True) +class CreateNewWalletResponse(TransactionEndpointResponse): + type: str # Alias for WalletType which is IntEnum and therefore incompatible + wallet_id: uint32 + # Nothing below is truly optional when that type is being returned + # CAT_WALLET (TXEndpoint) + asset_id: Optional[bytes32] = None + # DID_WALLET - NEW (TXEndpoint) / RECOVERY + my_did: Optional[str] = None + # DID_WALLET - RECOVERY + coin_name: Optional[bytes32] = None + coin_list: Optional[Coin] = None + newpuzhash: Optional[bytes32] = None + pubkey: Optional[G1Element] = None + backup_dids: Optional[list[bytes32]] = None + num_verifications_required: Optional[uint64] = None + # NFT_WALLET + # ... + # POOL_WALLET (TXEndpoint) + total_fee: Optional[uint64] = None + transaction: Optional[TransactionRecord] = None + launcher_id: Optional[bytes32] = None + p2_singleton_puzzle_hash: Optional[bytes32] = None + + def __post_init__(self) -> None: + if self.type not in {member.name for member in WalletType}: + raise ValueError(f"Invalid wallet type: {self.type}") + super().__post_init__() + + def to_json_dict(self) -> dict[str, Any]: + field_names = {"type", "wallet_id"} + tx_endpoint_field_names = set(field.name for field in fields(TransactionEndpointResponse)) + serialization_updates: dict[str, Any] = {} + wallet_type = next(member for member in WalletType if member.name == self.type) + if wallet_type == WalletType.CAT: + if self.asset_id is None: + raise ValueError("`asset_id` is required for CAT wallets") + field_names |= {"asset_id"} + field_names |= tx_endpoint_field_names + elif wallet_type == WalletType.DECENTRALIZED_ID: + if self.my_did is None: + raise ValueError("`my_did` is required for DID wallets") + field_names |= {"my_did"} + if ( + self.coin_name is not None + and self.coin_list is not None + and self.newpuzhash is not None + and self.pubkey is not None + and self.backup_dids is not None + and self.num_verifications_required is not None + ): + field_names |= { + "coin_name", + "coin_list", + "newpuzhash", + "pubkey", + "backup_dids", + "num_verifications_required", + } + serialization_updates["coin_list"] = coin_as_list(self.coin_list) + elif not ( + self.coin_name is None + and self.coin_list is None + and self.newpuzhash is None + and self.pubkey is None + and self.backup_dids is None + and self.num_verifications_required is None + ): + raise ValueError("Must specify all recovery options or none of them") + else: + field_names |= tx_endpoint_field_names + elif wallet_type == WalletType.POOLING_WALLET: + if not ( + ( + self.total_fee is None + and self.transaction is None + and self.launcher_id is None + and self.p2_singleton_puzzle_hash is None + ) + or ( + self.total_fee is not None + and self.transaction is not None + and self.launcher_id is not None + and self.p2_singleton_puzzle_hash is not None + ) + ): + raise ValueError("Must specify all pooling options or none of them") + else: + field_names = { # leaves out wallet_id + "type", + "total_fee", + "transaction", + "launcher_id", + "p2_singleton_puzzle_hash", + } + field_names |= tx_endpoint_field_names + + return {**{k: v for k, v in super().to_json_dict().items() if k in field_names}, **serialization_updates} + + @classmethod + def from_json_dict(cls, json_dict: dict[str, Any]) -> Self: + if "wallet_id" not in json_dict: + json_dict["wallet_id"] = uint32(0) + if "transactions" not in json_dict: + json_dict["transactions"] = [] + if "unsigned_transactions" not in json_dict: + json_dict["unsigned_transactions"] = [] + if "coin_list" in json_dict: + parent_hex, ph_hex, amt = json_dict["coin_list"] + json_dict["coin_list"] = Coin( + bytes32.from_hexstr(parent_hex), bytes32.from_hexstr(ph_hex), uint64(amt) + ).to_json_dict() + + return super().from_json_dict(json_dict) diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index b9d50e130395..240906de6ea8 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -21,7 +21,6 @@ from chia.rpc.rpc_server import Endpoint, EndpointResult, default_get_connections from chia.rpc.util import ALL_TRANSLATION_LAYERS, RpcEndpoint, marshal from chia.server.ws_connection import WSChiaConnection -from chia.types.blockchain_format.coin import coin_as_list from chia.types.blockchain_format.program import INFINITE_COST, Program, run_with_cost from chia.types.coin_record import CoinRecord from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode @@ -132,6 +131,9 @@ CombineCoinsResponse, CreateNewDL, CreateNewDLResponse, + CreateNewWallet, + CreateNewWalletResponse, + CreateNewWalletType, CreateOfferForIDs, CreateOfferForIDsResponse, CreateSignedTransaction, @@ -162,6 +164,7 @@ DIDSetWalletNameResponse, DIDTransferDID, DIDTransferDIDResponse, + DIDType, DIDUpdateMetadata, DIDUpdateMetadataResponse, DLDeleteMirror, @@ -315,6 +318,7 @@ VCSpendResponse, VerifySignature, VerifySignatureResponse, + WalletCreationMode, WalletInfoResponse, ) from chia.wallet.wallet_spend_bundle import WalletSpendBundle @@ -1130,81 +1134,70 @@ async def get_wallets(self, request: GetWallets) -> GetWalletsResponse: return GetWalletsResponse(wallet_infos, uint32.construct_optional(self.service.logged_in_fingerprint)) @tx_endpoint(push=True) - async def create_new_wallet( + @marshal + # Semantics guarantee returning on all paths, or else an error. + # It's probably not great to add a bunch of unreachable code for the sake of mypy. + async def create_new_wallet( # type: ignore[return] self, - request: dict[str, Any], + request: CreateNewWallet, action_scope: WalletActionScope, extra_conditions: tuple[Condition, ...] = tuple(), - ) -> EndpointResult: + ) -> CreateNewWalletResponse: wallet_state_manager = self.service.wallet_state_manager if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced.") main_wallet = wallet_state_manager.main_wallet - fee = uint64(request.get("fee", 0)) - - if request["wallet_type"] == "cat_wallet": - # If not provided, the name will be autogenerated based on the tail hash. - name = request.get("name", None) - if request["mode"] == "new": - if request.get("test", False): - if not action_scope.config.push: - raise ValueError("Test CAT minting must be pushed automatically") # pragma: no cover - async with self.service.wallet_state_manager.lock: - cat_wallet = await CATWallet.create_new_cat_wallet( - wallet_state_manager, - main_wallet, - {"identifier": "genesis_by_id"}, - uint64(request["amount"]), - action_scope, - fee, - name, - ) - asset_id = cat_wallet.get_asset_id() - self.service.wallet_state_manager.state_changed("wallet_created") - return { - "type": cat_wallet.type(), - "asset_id": asset_id, - "wallet_id": cat_wallet.id(), - "transactions": None, # tx_endpoint wrapper will take care of this - } - else: - raise ValueError( - "Support for this RPC mode has been dropped." - " Please use the CAT Admin Tool @ https://github.com/Chia-Network/CAT-admin-tool instead." + + if request.wallet_type == CreateNewWalletType.CAT_WALLET: + if request.mode == WalletCreationMode.NEW: + if not action_scope.config.push: + raise ValueError("Test CAT minting must be pushed automatically") # pragma: no cover + async with self.service.wallet_state_manager.lock: + cat_wallet = await CATWallet.create_new_cat_wallet( + wallet_state_manager, + main_wallet, + {"identifier": "genesis_by_id"}, + # mypy doesn't know about our __post_init__ + request.amount, # type: ignore[arg-type] + action_scope, + request.fee, + request.name, ) + asset_id = bytes32.from_hexstr(cat_wallet.get_asset_id()) + self.service.wallet_state_manager.state_changed("wallet_created") + return CreateNewWalletResponse( + [], [], type=cat_wallet.type().name, asset_id=asset_id, wallet_id=cat_wallet.id() + ) - elif request["mode"] == "existing": + elif request.mode == WalletCreationMode.EXISTING: async with self.service.wallet_state_manager.lock: + assert request.asset_id is not None # mypy doesn't know about our __post_init__ cat_wallet = await CATWallet.get_or_create_wallet_for_cat( - wallet_state_manager, main_wallet, request["asset_id"], name + wallet_state_manager, main_wallet, request.asset_id.hex(), request.name ) - return {"type": cat_wallet.type(), "asset_id": request["asset_id"], "wallet_id": cat_wallet.id()} - - else: # undefined mode - pass - - elif request["wallet_type"] == "did_wallet": - if request["did_type"] == "new": - if "backup_dids" in request and request["backup_dids"] != []: - raise ValueError("Recovery options are no longer supported. `backup_dids` cannot be set.") - metadata: dict[str, str] = {} - if "metadata" in request: - if type(request["metadata"]) is dict: - metadata = request["metadata"] - + return CreateNewWalletResponse( + [], + [], + type=cat_wallet.type().name, + asset_id=request.asset_id, + wallet_id=cat_wallet.id(), + ) + elif request.wallet_type == CreateNewWalletType.DID_WALLET: + if request.did_type == DIDType.NEW: async with self.service.wallet_state_manager.lock: - did_wallet_name: Optional[str] = request.get("wallet_name", None) - if did_wallet_name is not None: - did_wallet_name = did_wallet_name.strip() + did_wallet_name = None + if request.wallet_name is not None: + did_wallet_name = request.wallet_name.strip() + assert request.amount is not None # mypy doesn't know about our __post_init__ did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet( wallet_state_manager, main_wallet, - uint64(request["amount"]), + request.amount, action_scope, - metadata, + request.metadata, did_wallet_name, - uint64(request.get("fee", 0)), + request.fee, extra_conditions=extra_conditions, ) @@ -1220,70 +1213,64 @@ async def create_new_wallet( bytes32.fromhex(did_wallet.get_my_DID()), nft_wallet_name, ) - return { - "success": True, - "type": did_wallet.type(), - "my_did": my_did_id, - "wallet_id": did_wallet.id(), - "transactions": None, # tx_endpoint wrapper will take care of this - } - - elif request["did_type"] == "recovery": + return CreateNewWalletResponse( + [], [], type=did_wallet.type().name, my_did=my_did_id, wallet_id=did_wallet.id() + ) + + elif request.did_type == DIDType.RECOVERY: async with self.service.wallet_state_manager.lock: + assert request.backup_data is not None # mypy doesn't know about our __post_init__ did_wallet = await DIDWallet.create_new_did_wallet_from_recovery( - wallet_state_manager, main_wallet, request["backup_data"] + wallet_state_manager, main_wallet, request.backup_data ) assert did_wallet.did_info.temp_coin is not None assert did_wallet.did_info.temp_puzhash is not None assert did_wallet.did_info.temp_pubkey is not None my_did = did_wallet.get_my_DID() - coin_name = did_wallet.did_info.temp_coin.name().hex() - coin_list = coin_as_list(did_wallet.did_info.temp_coin) + coin_name = did_wallet.did_info.temp_coin.name() newpuzhash = did_wallet.did_info.temp_puzhash pubkey = did_wallet.did_info.temp_pubkey - return { - "success": True, - "type": did_wallet.type(), - "my_did": my_did, - "wallet_id": did_wallet.id(), - "coin_name": coin_name, - "coin_list": coin_list, - "newpuzhash": newpuzhash.hex(), - "pubkey": pubkey.hex(), - "backup_dids": did_wallet.did_info.backup_ids, - "num_verifications_required": did_wallet.did_info.num_of_backup_ids_needed, - } - else: # undefined did_type - pass - elif request["wallet_type"] == "nft_wallet": + return CreateNewWalletResponse( + [], + [], + type=did_wallet.type().name, + my_did=my_did, + wallet_id=did_wallet.id(), + coin_name=coin_name, + coin_list=did_wallet.did_info.temp_coin, + newpuzhash=newpuzhash, + pubkey=G1Element.from_bytes(pubkey), + backup_dids=did_wallet.did_info.backup_ids, + num_verifications_required=did_wallet.did_info.num_of_backup_ids_needed, + ) + elif request.wallet_type == CreateNewWalletType.NFT_WALLET: + did_id: Optional[bytes32] = None + if request.did_id is not None: + did_id = decode_puzzle_hash(request.did_id) for wallet in self.service.wallet_state_manager.wallets.values(): - did_id: Optional[bytes32] = None - if "did_id" in request and request["did_id"] is not None: - did_id = decode_puzzle_hash(request["did_id"]) if wallet.type() == WalletType.NFT: assert isinstance(wallet, NFTWallet) if wallet.get_did() == did_id: log.info("NFT wallet already existed, skipping.") - return { - "success": True, - "type": wallet.type(), - "wallet_id": wallet.id(), - } + return CreateNewWalletResponse( + [], + [], + type=wallet.type().name, + wallet_id=wallet.id(), + ) async with self.service.wallet_state_manager.lock: nft_wallet: NFTWallet = await NFTWallet.create_new_nft_wallet( - wallet_state_manager, main_wallet, did_id, request.get("name", None) + wallet_state_manager, main_wallet, did_id, request.name ) - return { - "success": True, - "type": nft_wallet.type(), - "wallet_id": nft_wallet.id(), - } - elif request["wallet_type"] == "pool_wallet": - if request["mode"] == "new": - if "initial_target_state" not in request: - raise AttributeError("Daemon didn't send `initial_target_state`. Try updating the daemon.") - + return CreateNewWalletResponse( + [], + [], + type=nft_wallet.type().name, + wallet_id=nft_wallet.id(), + ) + elif request.wallet_type == CreateNewWalletType.POOL_WALLET: + if request.mode == WalletCreationMode.NEW: owner_puzzle_hash: bytes32 = await action_scope.get_puzzle_hash(self.service.wallet_state_manager) from chia.pools.pool_wallet_info import initial_pool_state_from_dict @@ -1307,44 +1294,34 @@ async def create_new_wallet( [12381, 8444, 5, max_pwi] ) + assert request.initial_target_state is not None # mypy doesn't know about our __post_init__ initial_target_state = initial_pool_state_from_dict( - request["initial_target_state"], owner_pk, owner_puzzle_hash + request.initial_target_state, owner_pk, owner_puzzle_hash ) assert initial_target_state is not None - try: - delayed_address = None - if "p2_singleton_delayed_ph" in request: - delayed_address = bytes32.from_hexstr(request["p2_singleton_delayed_ph"]) - - p2_singleton_puzzle_hash, launcher_id = await PoolWallet.create_new_pool_wallet_transaction( - wallet_state_manager, - main_wallet, - initial_target_state, - action_scope, - fee, - request.get("p2_singleton_delay_time", None), - delayed_address, - extra_conditions=extra_conditions, - ) + p2_singleton_puzzle_hash, launcher_id = await PoolWallet.create_new_pool_wallet_transaction( + wallet_state_manager, + main_wallet, + initial_target_state, + action_scope, + request.fee, + request.p2_singleton_delay_time, + request.p2_singleton_delayed_ph, + extra_conditions=extra_conditions, + ) - except Exception as e: - raise ValueError(str(e)) - return { - "total_fee": fee * 2, - "transaction": None, # tx_endpoint wrapper will take care of this - "transactions": None, # tx_endpoint wrapper will take care of this - "launcher_id": launcher_id.hex(), - "p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex(), - } - elif request["mode"] == "recovery": - raise ValueError("Need upgraded singleton for on-chain recovery") - - else: # undefined wallet_type - pass - - # TODO: rework this function to report detailed errors for each error case - return {"success": False, "error": "invalid request"} + return CreateNewWalletResponse( + [], + [], + transaction=REPLACEABLE_TRANSACTION_RECORD, + total_fee=uint64(request.fee * 2), + launcher_id=launcher_id, + p2_singleton_puzzle_hash=p2_singleton_puzzle_hash, + # irrelevant, will be replaced in serialization + type=WalletType.POOLING_WALLET.name, + wallet_id=uint32(0), + ) ########################################################################################## # Wallet diff --git a/chia/wallet/wallet_rpc_client.py b/chia/wallet/wallet_rpc_client.py index 5a623b2667df..3cdaf075c5e7 100644 --- a/chia/wallet/wallet_rpc_client.py +++ b/chia/wallet/wallet_rpc_client.py @@ -1,13 +1,12 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any -from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint32, uint64 from chia.data_layer.data_layer_util import DLProof, VerifyProofResponse from chia.rpc.rpc_client import RpcClient -from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_to_json_dicts +from chia.wallet.conditions import Condition, ConditionValidTimes from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable @@ -40,6 +39,8 @@ CombineCoinsResponse, CreateNewDL, CreateNewDLResponse, + CreateNewWallet, + CreateNewWalletResponse, CreateOfferForIDs, CreateOfferForIDsResponse, CreateSignedTransaction, @@ -324,6 +325,20 @@ async def get_transaction_count(self, request: GetTransactionCount) -> GetTransa async def get_next_address(self, request: GetNextAddress) -> GetNextAddressResponse: return GetNextAddressResponse.from_json_dict(await self.fetch("get_next_address", request.to_json_dict())) + async def create_new_wallet( + self, + request: CreateNewWallet, + tx_config: TXConfig, + extra_conditions: tuple[Condition, ...] = tuple(), + timelock_info: ConditionValidTimes = ConditionValidTimes(), + ) -> CreateNewWalletResponse: + return CreateNewWalletResponse.from_json_dict( + await self.fetch( + "create_new_wallet", + request.json_serialize_for_transport(tx_config, extra_conditions, timelock_info), + ) + ) + async def send_transaction( self, request: SendTransaction, @@ -408,37 +423,6 @@ async def get_coin_records_by_names(self, request: GetCoinRecordsByNames) -> Get ) # DID wallet - async def create_new_did_wallet( - self, - amount: int, - tx_config: TXConfig, - fee: int = 0, - name: Optional[str] = "DID Wallet", - backup_ids: list[str] = [], - required_num: int = 0, - type: str = "new", - backup_data: str = "", - push: bool = True, - extra_conditions: tuple[Condition, ...] = tuple(), - timelock_info: ConditionValidTimes = ConditionValidTimes(), - ) -> dict[str, Any]: - request = { - "wallet_type": "did_wallet", - "did_type": type, - "backup_dids": backup_ids, - "num_of_backup_ids_needed": required_num, - "amount": amount, - "fee": fee, - "wallet_name": name, - "push": push, - "backup_data": backup_data, - "extra_conditions": conditions_to_json_dicts(extra_conditions), - **tx_config.to_json_dict(), - **timelock_info.to_json_dict(), - } - response = await self.fetch("create_new_wallet", request) - return response - async def get_did_id(self, request: DIDGetDID) -> DIDGetDIDResponse: return DIDGetDIDResponse.from_json_dict(await self.fetch("did_get_did", request.to_json_dict())) @@ -517,41 +501,6 @@ async def did_set_wallet_name(self, request: DIDSetWalletName) -> DIDSetWalletNa async def did_get_wallet_name(self, request: DIDGetWalletName) -> DIDGetWalletNameResponse: return DIDGetWalletNameResponse.from_json_dict(await self.fetch("did_get_wallet_name", request.to_json_dict())) - # TODO: test all invocations of create_new_pool_wallet with new fee arg. - async def create_new_pool_wallet( - self, - target_puzzlehash: Optional[bytes32], - pool_url: Optional[str], - relative_lock_height: uint32, - backup_host: str, - mode: str, - state: str, - fee: uint64, - p2_singleton_delay_time: Optional[uint64] = None, - p2_singleton_delayed_ph: Optional[bytes32] = None, - extra_conditions: tuple[Condition, ...] = tuple(), - timelock_info: ConditionValidTimes = ConditionValidTimes(), - ) -> TransactionRecord: - request = { - "wallet_type": "pool_wallet", - "mode": mode, - "initial_target_state": { - "target_puzzle_hash": target_puzzlehash.hex() if target_puzzlehash else None, - "relative_lock_height": relative_lock_height, - "pool_url": pool_url, - "state": state, - }, - "fee": fee, - "extra_conditions": conditions_to_json_dicts(extra_conditions), - **timelock_info.to_json_dict(), - } - if p2_singleton_delay_time is not None: - request["p2_singleton_delay_time"] = p2_singleton_delay_time - if p2_singleton_delayed_ph is not None: - request["p2_singleton_delayed_ph"] = p2_singleton_delayed_ph.hex() - res = await self.fetch("create_new_wallet", request) - return TransactionRecord.from_json_dict(res["transaction"]) - async def pw_self_pool( self, request: PWSelfPool, @@ -595,16 +544,6 @@ async def pw_status(self, request: PWStatus) -> PWStatusResponse: return PWStatusResponse.from_json_dict(await self.fetch("pw_status", request.to_json_dict())) # CATS - async def create_new_cat_and_wallet( - self, amount: uint64, fee: uint64 = uint64(0), test: bool = False - ) -> dict[str, Any]: - request = {"wallet_type": "cat_wallet", "mode": "new", "amount": amount, "fee": fee, "test": test} - return await self.fetch("create_new_wallet", request) - - async def create_wallet_for_existing_cat(self, asset_id: bytes) -> dict[str, Any]: - request = {"wallet_type": "cat_wallet", "asset_id": asset_id.hex(), "mode": "existing"} - return await self.fetch("create_new_wallet", request) - async def get_cat_asset_id(self, request: CATGetAssetID) -> CATGetAssetIDResponse: return CATGetAssetIDResponse.from_json_dict(await self.fetch("cat_get_asset_id", request.to_json_dict())) @@ -707,11 +646,6 @@ async def get_cat_list(self) -> GetCATListResponse: return GetCATListResponse.from_json_dict(await self.fetch("get_cat_list", {})) # NFT wallet - async def create_new_nft_wallet(self, did_id: Optional[str], name: Optional[str] = None) -> dict[str, Any]: - request = {"wallet_type": "nft_wallet", "did_id": did_id, "name": name} - response = await self.fetch("create_new_wallet", request) - return response - async def mint_nft( self, request: NFTMintNFTRequest,