From d90001e3eefa4dcfa8241013697640f2bc353268 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 11 Aug 2026 09:49:16 +1200 Subject: [PATCH] [LABS-170] Add automatic reorg testing to `WalletTestFramework` (#21211) * [LABS-170] Add automatic reorg testing to `WalletTestFramework` * Actually exempt reorgs when specified * Comments by @cursor * Fix CAT wallet tests * Reorg exempt offer tests * Fix DID tests * Fix NFTWallet tests * Fix RPC tests * Fix VC tests * Exempt pool tests * Add reorg_exempt to top level conftest * Fix tests (nd maybe break them?) --- chia/_tests/conftest.py | 1 + chia/_tests/environments/wallet.py | 157 +++++++----- chia/_tests/pools/test_pool_cmdline.py | 9 + chia/_tests/pools/test_pool_rpc.py | 3 + .../wallet/cat_wallet/test_cat_wallet.py | 42 ++- chia/_tests/wallet/cat_wallet/test_trades.py | 73 +++++- chia/_tests/wallet/conftest.py | 1 + .../_tests/wallet/db_wallet/test_dl_offers.py | 12 +- chia/_tests/wallet/did_wallet/test_did.py | 24 +- .../wallet/nft_wallet/test_nft_1_offers.py | 33 ++- .../wallet/nft_wallet/test_nft_offers.py | 24 +- .../wallet/nft_wallet/test_nft_wallet.py | 5 +- .../plotnft_wallet/test_plotnft_wallet.py | 240 +----------------- chia/_tests/wallet/rpc/test_wallet_rpc.py | 12 +- chia/_tests/wallet/test_signer_protocol.py | 14 +- chia/_tests/wallet/test_wallet.py | 191 +------------- chia/wallet/did_wallet/did_wallet.py | 7 +- chia/wallet/vc_wallet/cr_cat_wallet.py | 2 +- chia/wallet/wallet.py | 7 +- chia/wallet/wallet_state_manager.py | 5 + 20 files changed, 339 insertions(+), 523 deletions(-) diff --git a/chia/_tests/conftest.py b/chia/_tests/conftest.py index 32a4fd304d..1eab1b8da6 100644 --- a/chia/_tests/conftest.py +++ b/chia/_tests/conftest.py @@ -1575,4 +1575,5 @@ async def wallet_environments( for service, rpc_client, wallet_state in zip(wallet_services, wallet_rpc_clients, wallet_states) ], tx_config, + request.param.get("reorg_exempt", False), ) diff --git a/chia/_tests/environments/wallet.py b/chia/_tests/environments/wallet.py index 5aff2ce184..9506e48fbb 100644 --- a/chia/_tests/environments/wallet.py +++ b/chia/_tests/environments/wallet.py @@ -19,6 +19,7 @@ from chia.rpc.rpc_server import RpcServer from chia.server.server import ChiaServer from chia.server.start_service import Service from chia.simulator.full_node_simulator import FullNodeSimulator +from chia.simulator.simulator_protocol import ReorgProtocol from chia.wallet.transaction_record import LightTransactionRecord from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig @@ -28,6 +29,7 @@ from chia.wallet.wallet_node_api import WalletNodeAPI from chia.wallet.wallet_request_types import GetWalletBalance from chia.wallet.wallet_rpc_api import WalletRpcApi from chia.wallet.wallet_rpc_client import WalletRpcClient +from chia.wallet.wallet_spend_bundle import WalletSpendBundle from chia.wallet.wallet_state_manager import WalletStateManager STANDARD_TX_ENDPOINT_ARGS: dict[str, Any] = TransactionEndpoint( @@ -309,6 +311,7 @@ class WalletTestFramework: trusted_full_node: bool environments: list[WalletEnvironment] tx_config: TXConfig = DEFAULT_TX_CONFIG + reorg_exempt: bool = False def cmd_tx_endpoint_args(self, env: WalletEnvironment) -> dict[str, Any]: return { @@ -343,7 +346,12 @@ class WalletTestFramework: yield async def process_pending_states( - self, state_transitions: list[WalletStateTransition], invalid_transactions: list[bytes32] = [] + self, + state_transitions: list[WalletStateTransition], + invalid_transactions: list[bytes32] = [], + post_reorg_balance_differences: list[WalletStateTransition] = [], + bundles_to_repush: list[WalletSpendBundle] = [], + reorg_exempt: bool = False, ) -> None: """ This is the main entry point for processing state in wallet tests. It does the following things: @@ -356,6 +364,8 @@ class WalletTestFramework: 6) Checks that if `reuse_puzhash` was set, no new derivations were created 7) Ensures the wallet is in a synced state before progressing to the rest of the test """ + if len(post_reorg_balance_differences) == 0: + post_reorg_balance_differences = [WalletStateTransition()] * len(self.environments) # Take note of the number of puzzle hashes if we're supposed to be reusing if self.tx_config.reuse_puzhash: puzzle_hash_indexes: list[dict[uint32, int]] = [] @@ -365,67 +375,92 @@ class WalletTestFramework: ph_indexes[wallet_id] = await env.wallet_state_manager.puzzle_store.get_used_count(wallet_id) puzzle_hash_indexes.append(ph_indexes) - pending_txs: list[list[LightTransactionRecord]] = [] - peak = self.full_node.full_node.blockchain.get_peak_height() - assert peak is not None - # Check balances prior to block - try: - for i, env in enumerate(self.environments): - await self.full_node.wait_for_wallet_synced(wallet_node=env.node, timeout=20, peak_height=peak) - try: - pending_txs.append( - await env.wait_for_transactions_to_settle( - self.full_node, _exclude_from_mempool_check=invalid_transactions - ) - ) - except TimeoutError: # pragma: no cover - raise TimeoutError(f"All TXs for env-{i} were not found in mempool or marked as in mempool") - for i, (env, transition) in enumerate(zip(self.environments, state_transitions)): - try: - async with env.wallet_state_manager.db_wrapper.reader_no_transaction(): - await env.change_balances(transition.pre_block_balance_updates) - await env.check_balances(transition.pre_block_additional_balance_info) - except Exception: - raise ValueError(f"Error with env index {i}") - except Exception as e: - raise ValueError(f"Error before block was farmed: {e}") from e - - # Farm block - await self.full_node.farm_blocks_to_puzzlehash(count=1, guarantee_transaction_blocks=True) - - # Check balances after block - try: - for i, (env, local_pending_txs) in enumerate(zip(self.environments, pending_txs)): - await self.full_node.wait_for_wallet_synced( - wallet_node=env.node, timeout=20, peak_height=uint32(peak + 1) - ) - try: - await env.wait_for_transactions_to_settle( - self.full_node, - _exclude_from_mempool_check=invalid_transactions + [tx.name for tx in local_pending_txs], - ) - except TimeoutError: # pragma: no cover - raise TimeoutError(f"All TXs for env-{i} were not found in mempool or marked as in mempool") - for i, (env, transition) in enumerate(zip(self.environments, state_transitions)): - try: - async with env.wallet_state_manager.db_wrapper.reader_no_transaction(): - await env.change_balances(transition.post_block_balance_updates) - await env.check_balances(transition.post_block_additional_balance_info) - except Exception: - raise ValueError(f"Error with env {i}") - except Exception as e: - raise ValueError(f"Error after block was farmed: {e}") from e - - # Make sure all pending txs from before the block are now confirmed - for i, (env, txs) in enumerate(zip(self.environments, pending_txs)): + balances_pre_block_updates: list[dict[uint32, WalletState]] = [] + # Check balances after block (and reorg) + for reorg_status in ("no",) if reorg_exempt or self.reorg_exempt else ("before", "during"): + pending_txs: list[list[LightTransactionRecord]] = [] + if reorg_status == "during": + for bundle in bundles_to_repush: + await self.full_node_rpc_client.push_tx(bundle) + peak = self.full_node.full_node.blockchain.get_peak_height() + assert peak is not None + # Check balances prior to block try: - await self.full_node.check_transactions_confirmed(env.wallet_state_manager, txs) - except TimeoutError: # pragma: no cover - unconfirmed: list[ - LightTransactionRecord - ] = await env.wallet_state_manager.tx_store.get_all_unconfirmed() - raise TimeoutError( - f"ENV-{i} TXs not confirmed: {[tx.to_json_dict() for tx in unconfirmed if tx in txs]}" + for i, env in enumerate(self.environments): + await self.full_node.wait_for_wallet_synced(wallet_node=env.node, timeout=20, peak_height=peak) + try: + pending_txs.append( + await env.wait_for_transactions_to_settle( + self.full_node, _exclude_from_mempool_check=invalid_transactions + ) + ) + except TimeoutError: # pragma: no cover + raise TimeoutError(f"All TXs for env-{i} were not found in mempool or marked as in mempool") + for i, (env, transition) in enumerate(zip(self.environments, state_transitions)): + try: + async with env.wallet_state_manager.db_wrapper.reader_no_transaction(): + if reorg_status == "during": + env.wallet_states = balances_pre_block_updates[i] + await env.change_balances(post_reorg_balance_differences[i].pre_block_balance_updates) + else: + await env.change_balances(transition.pre_block_balance_updates) + balances_pre_block_updates.append(env.wallet_states) + await env.check_balances(transition.pre_block_additional_balance_info) + except Exception: + raise ValueError(f"Error with env index {i} - {reorg_status} reorg check") + except Exception as e: + raise ValueError(f"Error before block was farmed: {e}") from e + + # Farm block + await self.full_node.farm_blocks_to_puzzlehash(count=1, guarantee_transaction_blocks=True) + + try: + for i, (env, local_pending_txs) in enumerate(zip(self.environments, pending_txs)): + await self.full_node.wait_for_wallet_synced( + wallet_node=env.node, timeout=20, peak_height=uint32(peak + 1) + ) + try: + await env.wait_for_transactions_to_settle( + self.full_node, + _exclude_from_mempool_check=invalid_transactions + [tx.name for tx in local_pending_txs], + ) + except TimeoutError: # pragma: no cover + raise TimeoutError(f"All TXs for env-{i} were not found in mempool or marked as in mempool") + for i, (env, transition) in enumerate(zip(self.environments, state_transitions)): + try: + async with env.wallet_state_manager.db_wrapper.reader_no_transaction(): + await env.change_balances(transition.post_block_balance_updates) + if reorg_status == "during": + await env.change_balances(post_reorg_balance_differences[i].post_block_balance_updates) + await env.check_balances(transition.post_block_additional_balance_info) + if reorg_status == "before": + for id, balance_updates in transition.post_block_balance_updates.items(): + if balance_updates.get("init", False): + balances_pre_block_updates[i][env.dealias_wallet_id(id)] = WalletState( + balance=Balance() + ) + except Exception: + raise ValueError(f"Error with env {i} - {reorg_status} reorg check") + except Exception as e: + raise ValueError(f"Error after block was farmed: {e}") from e + + # Make sure all pending txs from before the block are now confirmed + for i, (env, txs) in enumerate(zip(self.environments, pending_txs)): + try: + await self.full_node.check_transactions_confirmed(env.wallet_state_manager, txs) + except TimeoutError: # pragma: no cover + unconfirmed: list[ + LightTransactionRecord + ] = await env.wallet_state_manager.tx_store.get_all_unconfirmed() + raise TimeoutError( + f"ENV-{i} TXs not confirmed: {[tx.to_json_dict() for tx in unconfirmed if tx in txs]}" + ) + + if reorg_status == "before": + height = self.full_node.full_node.blockchain.get_peak_height() + assert height is not None + await self.full_node.reorg_from_index_to_new_index( + ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) ) # Finally, check that the number of puzzle hashes did or did not increase by the specified amount diff --git a/chia/_tests/pools/test_pool_cmdline.py b/chia/_tests/pools/test_pool_cmdline.py index f4724441c0..a043dcea6f 100644 --- a/chia/_tests/pools/test_pool_cmdline.py +++ b/chia/_tests/pools/test_pool_cmdline.py @@ -76,6 +76,7 @@ class StateUrlCase: { "num_environments": 1, "blocks_needed": [1], + "reorg_exempt": True, } ], indirect=True, @@ -229,6 +230,7 @@ async def test_plotnft_cli_create_errors( { "num_environments": 1, "blocks_needed": [1], + "reorg_exempt": True, } ], indirect=True, @@ -309,6 +311,7 @@ async def test_plotnft_cli_show( { "num_environments": 1, "blocks_needed": [1], + "reorg_exempt": True, } ], indirect=True, @@ -381,6 +384,7 @@ async def test_plotnft_cli_show_with_farmer( { "num_environments": 1, "blocks_needed": [10], + "reorg_exempt": True, } ], indirect=True, @@ -472,6 +476,7 @@ async def test_plotnft_cli_leave( { "num_environments": 1, "blocks_needed": [10], + "reorg_exempt": True, } ], indirect=True, @@ -704,6 +709,7 @@ async def test_plotnft_cli_join( { "num_environments": 1, "blocks_needed": [10], + "reorg_exempt": True, } ], indirect=True, @@ -829,6 +835,7 @@ async def test_plotnft_cli_claim(wallet_environments: WalletTestFramework, versi "num_environments": 1, "blocks_needed": [10], "reuse_puzhash": False, + "reorg_exempt": True, } ], indirect=True, @@ -918,6 +925,7 @@ async def test_plotnft_cli_inspect( { "num_environments": 1, "blocks_needed": [10], + "reorg_exempt": True, } ], indirect=True, @@ -990,6 +998,7 @@ async def test_plotnft_cli_change_payout( { "num_environments": 1, "blocks_needed": [10], + "reorg_exempt": True, } ], indirect=True, diff --git a/chia/_tests/pools/test_pool_rpc.py b/chia/_tests/pools/test_pool_rpc.py index 79de12171f..e54524e957 100644 --- a/chia/_tests/pools/test_pool_rpc.py +++ b/chia/_tests/pools/test_pool_rpc.py @@ -1171,6 +1171,7 @@ class TestPoolWalletRpc: { "num_environments": 1, "blocks_needed": [10], + "reorg_exempt": True, } ], indirect=True, @@ -1359,6 +1360,7 @@ class TestPoolWalletRpc: { "num_environments": 1, "blocks_needed": [10], + "reorg_exempt": True, } ], indirect=True, @@ -1401,6 +1403,7 @@ class TestPoolWalletRpc: "blocks_needed": [10], "trusted": True, "reuse_puzhash": False, + "reorg_exempt": True, } ], indirect=True, diff --git a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py index cd7715bf89..80838b8ebf 100644 --- a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py +++ b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py @@ -1320,7 +1320,25 @@ async def test_cat_hint(wallet_environments: WalletTestFramework, wallet_type: t } ), ), - ] + ], + post_reorg_balance_differences=[ + WalletStateTransition(), + WalletStateTransition() + if autodiscovery + # after the reorg we'll already have discovered that these CATs belong to us + else WalletStateTransition( + pre_block_balance_updates={ + "cat": { + "confirmed_wallet_balance": 60, + "unconfirmed_wallet_balance": 60, + "spendable_balance": 60, + "max_send_amount": 60, + "unspent_coin_count": 1, + } + }, + post_block_balance_updates={}, # the "init" means the balances will be overidden + ), + ], ) cat_wallet_2 = wallet_node_2.wallet_state_manager.wallets[uint32(2)] @@ -1522,7 +1540,21 @@ async def test_cat_change_detection(wallet_environments: WalletTestFramework, wa } }, ) - ] + ], + bundles_to_repush=[eve_spend], + # We're going to remember we received the CAT through the reorg + post_reorg_balance_differences=[ + WalletStateTransition( + pre_block_balance_updates={ + "cat": { + "unconfirmed_wallet_balance": 5, + "pending_change": 5, + "pending_coin_removal_count": 1, + } + }, + post_block_balance_updates={}, # the "init" means the balances will be overidden + ) + ], ) assert not full_node_api.full_node.subscriptions.has_puzzle_subscription(puzzlehash_unhardened) @@ -1638,7 +1670,8 @@ async def test_cat_melt_balance(wallet_environments: WalletTestFramework) -> Non }, }, ) - ] + ], + bundles_to_repush=[spend_to_wallet], ) cat_wallet = env.wallet_state_manager.wallets[uint32(2)] @@ -1688,7 +1721,8 @@ async def test_cat_melt_balance(wallet_environments: WalletTestFramework) -> Non }, }, ) - ] + ], + bundles_to_repush=[signed_spend], ) diff --git a/chia/_tests/wallet/cat_wallet/test_trades.py b/chia/_tests/wallet/cat_wallet/test_trades.py index d690d33732..c041d74cb5 100644 --- a/chia/_tests/wallet/cat_wallet/test_trades.py +++ b/chia/_tests/wallet/cat_wallet/test_trades.py @@ -63,42 +63,94 @@ async def get_trade_and_status(trade_manager: TradeManager, trade: TradeRecord) "wallet_environments,credential_restricted,active_softfork_height", [ ( - {"num_environments": 2, "trusted": True, "blocks_needed": [1, 1], "reuse_puzhash": True}, + { + "num_environments": 2, + "trusted": True, + "blocks_needed": [1, 1], + "reuse_puzhash": True, + "reorg_exempt": True, + }, True, SOFTFORK_HEIGHTS[0], ), ( - {"num_environments": 2, "trusted": True, "blocks_needed": [1, 1], "reuse_puzhash": True}, + { + "num_environments": 2, + "trusted": True, + "blocks_needed": [1, 1], + "reuse_puzhash": True, + "reorg_exempt": True, + }, False, SOFTFORK_HEIGHTS[0], ), ( - {"num_environments": 2, "trusted": True, "blocks_needed": [1, 1], "reuse_puzhash": False}, + { + "num_environments": 2, + "trusted": True, + "blocks_needed": [1, 1], + "reuse_puzhash": False, + "reorg_exempt": True, + }, True, SOFTFORK_HEIGHTS[0], ), ( - {"num_environments": 2, "trusted": False, "blocks_needed": [1, 1], "reuse_puzhash": True}, + { + "num_environments": 2, + "trusted": False, + "blocks_needed": [1, 1], + "reuse_puzhash": True, + "reorg_exempt": True, + }, True, SOFTFORK_HEIGHTS[0], ), ( - {"num_environments": 2, "trusted": False, "blocks_needed": [1, 1], "reuse_puzhash": False}, + { + "num_environments": 2, + "trusted": False, + "blocks_needed": [1, 1], + "reuse_puzhash": False, + "reorg_exempt": True, + }, False, SOFTFORK_HEIGHTS[0], ), ( - {"num_environments": 2, "trusted": False, "blocks_needed": [1, 1], "reuse_puzhash": True}, + { + "num_environments": 2, + "trusted": False, + "blocks_needed": [1, 1], + "reuse_puzhash": True, + "reorg_exempt": True, + }, False, SOFTFORK_HEIGHTS[0], ), ( - {"num_environments": 2, "trusted": False, "blocks_needed": [1, 1], "reuse_puzhash": False}, + { + "num_environments": 2, + "trusted": False, + "blocks_needed": [1, 1], + "reuse_puzhash": False, + "reorg_exempt": True, + }, True, SOFTFORK_HEIGHTS[0], ), *( - ({"num_environments": 2, "trusted": True, "blocks_needed": [1, 1], "reuse_puzhash": False}, False, height) + ( + { + "num_environments": 2, + "trusted": True, + "blocks_needed": [1, 1], + "reuse_puzhash": False, + "reorg_exempt": True, + }, + False, + height, + ) for height in SOFTFORK_HEIGHTS ), ], @@ -1651,6 +1703,7 @@ async def test_cat_trades( { "num_environments": 2, "blocks_needed": [2, 1], + "reorg_exempt": True, } ], indirect=True, @@ -1963,6 +2016,7 @@ async def test_trade_cancellation(wallet_environments: WalletTestFramework, wall { "num_environments": 3, "blocks_needed": [2, 1, 1], + "reorg_exempt": True, } ], indirect=True, @@ -2160,6 +2214,7 @@ async def test_trade_conflict(wallet_environments: WalletTestFramework, wallet_t { "num_environments": 2, "blocks_needed": [1, 1], + "reorg_exempt": True, } ], indirect=True, @@ -2286,6 +2341,7 @@ async def test_trade_bad_spend( { "num_environments": 2, "blocks_needed": [1, 1], + "reorg_exempt": True, } ], indirect=True, @@ -2433,6 +2489,7 @@ async def test_trade_high_fee(wallet_environments: WalletTestFramework, wallet_t { "num_environments": 2, "blocks_needed": [1, 1], + "reorg_exempt": True, } ], indirect=True, diff --git a/chia/_tests/wallet/conftest.py b/chia/_tests/wallet/conftest.py index 6b73fc31dd..1d5305f8be 100644 --- a/chia/_tests/wallet/conftest.py +++ b/chia/_tests/wallet/conftest.py @@ -277,4 +277,5 @@ async def wallet_environments( for service, rpc_client, wallet_state in zip(wallet_services, wallet_rpc_clients, wallet_states) ], tx_config, + request.param.get("reorg_exempt", False), ) diff --git a/chia/_tests/wallet/db_wallet/test_dl_offers.py b/chia/_tests/wallet/db_wallet/test_dl_offers.py index 95bd9b1fe4..e5a7ac5827 100644 --- a/chia/_tests/wallet/db_wallet/test_dl_offers.py +++ b/chia/_tests/wallet/db_wallet/test_dl_offers.py @@ -39,7 +39,9 @@ def get_parent_branch(value: bytes32, proof: tuple[int, list[bytes32]]) -> tuple @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [2, 2]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [2, 2], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_dl_offers(wallet_environments: WalletTestFramework) -> None: env_maker = wallet_environments.environments[0] @@ -353,7 +355,9 @@ async def test_dl_offers(wallet_environments: WalletTestFramework) -> None: @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 1, "blocks_needed": [3]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 1, "blocks_needed": [3], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_dl_offer_cancellation(wallet_environments: WalletTestFramework) -> None: env_maker = wallet_environments.environments[0] @@ -579,7 +583,9 @@ async def test_dl_offer_cancellation(wallet_environments: WalletTestFramework) - @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [3, 3]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [3, 3], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_multiple_dl_offers(wallet_environments: WalletTestFramework) -> None: env_maker = wallet_environments.environments[0] diff --git a/chia/_tests/wallet/did_wallet/test_did.py b/chia/_tests/wallet/did_wallet/test_did.py index eee34bffd3..3fefa70944 100644 --- a/chia/_tests/wallet/did_wallet/test_did.py +++ b/chia/_tests/wallet/did_wallet/test_did.py @@ -469,7 +469,14 @@ async def test_did_find_lost_did(wallet_environments: WalletTestFramework, capsy }, }, ), - ] + ], + # Due to an ephemeral spend, sync will pick up an extra TX here and thus bump the pending coin removal count + post_reorg_balance_differences=[ + WalletStateTransition( + pre_block_balance_updates={"did_found": {"pending_coin_removal_count": 1}}, + post_block_balance_updates={"did_found": {"pending_coin_removal_count": -1}}, + ) + ], ) coin = await did_wallet.get_coin() @@ -587,7 +594,10 @@ async def test_did_transfer(wallet_environments: WalletTestFramework, capsys: py "did": {"init": True, "confirmed_wallet_balance": 101, "set_remainder": True}, }, ), - ] + ], + # TODO: there's a bug here where the user store autoincrement means this has a new ID after deleted + # Instead of 2, it becomes 3 because there was a 2 at some point (is my best guess) + reorg_exempt=True, ) did_wallets = list( @@ -1046,7 +1056,17 @@ async def test_update_metadata(wallet_environments: WalletTestFramework, capsys: "did": {"confirmed_wallet_balance": 0, "set_remainder": True}, }, ), + ], + post_reorg_balance_differences=[ + WalletStateTransition( + pre_block_balance_updates={"did": {"pending_coin_removal_count": 1}}, + post_block_balance_updates={}, # set_remainder takes care of it + ), + WalletStateTransition(), ] + # TODO: figure out why the reuse_puzhash being off is the only reason this happens + if not wallet_environments.tx_config.reuse_puzhash + else [], ) assert get_parent_num(did_wallet_1) == parent_num + 2 diff --git a/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py b/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py index 5e991c71ca..fae16cf1bc 100644 --- a/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py +++ b/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py @@ -29,7 +29,9 @@ async def get_nft_count(wallet: NFTWallet) -> int: @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.parametrize("zero_royalties", [True, False]) @pytest.mark.anyio async def test_nft_offer_sell_nft(wallet_environments: WalletTestFramework, zero_royalties: bool) -> None: @@ -255,7 +257,9 @@ async def test_nft_offer_sell_nft(wallet_environments: WalletTestFramework, zero @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.parametrize("zero_royalties", [True, False]) @pytest.mark.anyio async def test_nft_offer_request_nft(wallet_environments: WalletTestFramework, zero_royalties: bool) -> None: @@ -485,7 +489,9 @@ async def test_nft_offer_request_nft(wallet_environments: WalletTestFramework, z @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.parametrize("zero_royalties", [True, False]) @pytest.mark.anyio async def test_nft_offer_sell_did_to_did(wallet_environments: WalletTestFramework, zero_royalties: bool) -> None: @@ -777,7 +783,9 @@ async def test_nft_offer_sell_did_to_did(wallet_environments: WalletTestFramewor @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.parametrize("zero_royalties", [True, False]) @pytest.mark.parametrize("wallet_type", [CATWallet, RCATWallet]) @pytest.mark.anyio @@ -1101,7 +1109,9 @@ async def test_nft_offer_sell_nft_for_cat( @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.parametrize("test_change", [True, False]) @pytest.mark.parametrize("wallet_type", [CATWallet, RCATWallet]) @pytest.mark.anyio @@ -1460,7 +1470,9 @@ async def test_nft_offer_request_nft_for_cat( @pytest.mark.limit_consensus_modes -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 1, "blocks_needed": [2]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 1, "blocks_needed": [2], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_nft_offer_sell_cancel(wallet_environments: WalletTestFramework) -> None: env_maker = wallet_environments.environments[0] @@ -1652,7 +1664,14 @@ async def test_nft_offer_sell_cancel(wallet_environments: WalletTestFramework) - ) @pytest.mark.parametrize( "wallet_environments", - [{"num_environments": 2, "blocks_needed": [3, 3], "config_overrides": {"automatically_add_unknown_cats": True}}], + [ + { + "num_environments": 2, + "blocks_needed": [3, 3], + "config_overrides": {"automatically_add_unknown_cats": True}, + "reorg_exempt": True, + } + ], indirect=True, ) @pytest.mark.parametrize("wallet_type", [CATWallet, RCATWallet]) diff --git a/chia/_tests/wallet/nft_wallet/test_nft_offers.py b/chia/_tests/wallet/nft_wallet/test_nft_offers.py index 3257fad0b4..900b6fcfc9 100644 --- a/chia/_tests/wallet/nft_wallet/test_nft_offers.py +++ b/chia/_tests/wallet/nft_wallet/test_nft_offers.py @@ -26,7 +26,9 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: # type: ig @pytest.mark.limit_consensus_modes(reason="irrelevant") -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_nft_offer_with_fee(wallet_environments: WalletTestFramework) -> None: env_0 = wallet_environments.environments[0] @@ -308,7 +310,9 @@ async def test_nft_offer_with_fee(wallet_environments: WalletTestFramework) -> N @pytest.mark.limit_consensus_modes(reason="irrelevant") -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 1, "blocks_needed": [1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 1, "blocks_needed": [1], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_nft_offer_cancellations(wallet_environments: WalletTestFramework) -> None: env_0 = wallet_environments.environments[0] @@ -461,7 +465,9 @@ async def test_nft_offer_cancellations(wallet_environments: WalletTestFramework) @pytest.mark.limit_consensus_modes(reason="irrelevant") -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_nft_offer_with_metadata_update(wallet_environments: WalletTestFramework) -> None: env_0 = wallet_environments.environments[0] @@ -696,7 +702,9 @@ async def test_nft_offer_with_metadata_update(wallet_environments: WalletTestFra @pytest.mark.limit_consensus_modes(reason="irrelevant") -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.parametrize("wallet_type", [CATWallet, RCATWallet]) @pytest.mark.anyio async def test_nft_offer_nft_for_cat(wallet_environments: WalletTestFramework, wallet_type: type[CATWallet]) -> None: @@ -1093,7 +1101,9 @@ async def test_nft_offer_nft_for_cat(wallet_environments: WalletTestFramework, w @pytest.mark.limit_consensus_modes(reason="irrelevant") -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.anyio async def test_nft_offer_nft_for_nft(wallet_environments: WalletTestFramework) -> None: env_0 = wallet_environments.environments[0] @@ -1333,7 +1343,9 @@ async def test_nft_offer_nft_for_nft(wallet_environments: WalletTestFramework) - @pytest.mark.limit_consensus_modes(reason="irrelevant") -@pytest.mark.parametrize("wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], indirect=True) +@pytest.mark.parametrize( + "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1], "reorg_exempt": True}], indirect=True +) @pytest.mark.parametrize("wallet_type", [CATWallet, RCATWallet]) @pytest.mark.anyio async def test_nft_offer_nft0_and_xch_for_cat( diff --git a/chia/_tests/wallet/nft_wallet/test_nft_wallet.py b/chia/_tests/wallet/nft_wallet/test_nft_wallet.py index 78b3c8779d..51f07ad31e 100644 --- a/chia/_tests/wallet/nft_wallet/test_nft_wallet.py +++ b/chia/_tests/wallet/nft_wallet/test_nft_wallet.py @@ -1595,7 +1595,10 @@ async def test_nft_transfer_nft_with_did(wallet_environments: WalletTestFramewor } }, ), - ] + ], + # TODO: there's a bug here where the user store autoincrement means this has a new ID after deleted + # Instead of 2, it becomes 3 because there was a 2 at some point (is my best guess) + reorg_exempt=True, ) # Transfer NFT, wallet will be deleted diff --git a/chia/_tests/wallet/plotnft_wallet/test_plotnft_wallet.py b/chia/_tests/wallet/plotnft_wallet/test_plotnft_wallet.py index 9c0e863a41..29f8bb2455 100644 --- a/chia/_tests/wallet/plotnft_wallet/test_plotnft_wallet.py +++ b/chia/_tests/wallet/plotnft_wallet/test_plotnft_wallet.py @@ -90,43 +90,6 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ uint32(env.wallet_aliases["plotnft"]), required_type=PlotNFT2Wallet ) - # Reorg (creation) - height = wallet_environments.full_node.full_node.blockchain.get_peak_height() - assert height is not None - await wallet_environments.full_node.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) - ) - await wallet_environments.full_node.wait_for_wallet_synced(env.node) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": creation_fee + 1, - "<=#spendable_balance": creation_fee + 1, - "<=#max_send_amount": creation_fee + 1, - ">=#pending_change": 0, - ">=#pending_coin_removal_count": 1, - ">=#unspent_coin_count": 0, - }, - "plotnft": {"unspent_coin_count": -1}, - }, - post_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": -(creation_fee + 1), - ">=#spendable_balance": 1, - ">=#max_send_amount": 1, - "<=#pending_change": 0, - "<=#pending_coin_removal_count": -1, - "<=#unspent_coin_count": 0, - }, - "plotnft": {"unspent_coin_count": 1}, - }, - ) - ] - ) - # (check an error) with pytest.raises(ValueError, match=re.escape("`leave_pool` called on a non-pooling or exiting PlotNFT")): async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope: @@ -244,49 +207,6 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ ): await plotnft_wallet.claim_rewards(action_scope=action_scope) - # Reorg (claim rewards) - height = wallet_environments.full_node.full_node.blockchain.get_peak_height() - assert height is not None - await wallet_environments.full_node.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) - ) - await wallet_environments.full_node.wait_for_wallet_synced(env.node) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": -amount_to_succeed_in_claiming, - "unconfirmed_wallet_balance": -amount_to_succeed_in_claiming, - "spendable_balance": -amount_to_succeed_in_claiming, - "max_send_amount": -amount_to_succeed_in_claiming, - "unspent_coin_count": -1, - }, - "plotnft": { - "confirmed_wallet_balance": REWARDS_GAINED, - "pending_coin_removal_count": NUM_REWARDS_FARMED + 1, - "unspent_coin_count": NUM_REWARDS_FARMED, - }, - }, - post_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": amount_to_succeed_in_claiming, - "unconfirmed_wallet_balance": amount_to_succeed_in_claiming, - "spendable_balance": amount_to_succeed_in_claiming, - "max_send_amount": amount_to_succeed_in_claiming, - "unspent_coin_count": 1, - }, - "plotnft": { - "confirmed_wallet_balance": -REWARDS_GAINED, - "pending_coin_removal_count": -NUM_REWARDS_FARMED - 1, - "unspent_coin_count": -NUM_REWARDS_FARMED, - }, - }, - ) - ] - ) - # JOIN POOL joining_fee = uint64(1_000) @@ -332,43 +252,6 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ == "https://daurl.com" ) - # Reorg (join pool) - height = wallet_environments.full_node.full_node.blockchain.get_peak_height() - assert height is not None - await wallet_environments.full_node.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) - ) - await wallet_environments.full_node.wait_for_wallet_synced(env.node) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": joining_fee, - "<=#spendable_balance": -1, - "<=#max_send_amount": -1, - ">=#pending_change": 0, - ">=#pending_coin_removal_count": 1, - ">=#unspent_coin_count": 0, - }, - "plotnft": {"pending_coin_removal_count": 1}, - }, - post_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": -joining_fee, - ">=#spendable_balance": 1, - ">=#max_send_amount": 1, - "<=#pending_change": 0, - "<=#pending_coin_removal_count": -1, - "<=#unspent_coin_count": 0, - }, - "plotnft": {"pending_coin_removal_count": -1}, - }, - ) - ] - ) - # RECEIVE REWARDS (while pooling) EXTRA_POOLING_REWARDS = 2 await wallet_environments.full_node.farm_blocks_to_puzzlehash( @@ -429,7 +312,15 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ } }, ) - ] + ], + # when we reorg, we'll still remember that we saw an attempt to spend our plotnft + post_reorg_balance_differences=[ + WalletStateTransition( + pre_block_balance_updates={"plotnft": {"pending_coin_removal_count": 2}}, + post_block_balance_updates={"plotnft": {"pending_coin_removal_count": -2}}, + ) + ], + bundles_to_repush=[WalletSpendBundle(coin_spends, G2Element())], ) # LEAVE POOL (to another) @@ -469,33 +360,6 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope: await plotnft_wallet.leave_pool(action_scope=action_scope) - # Reorg (leave pool to another) - height = wallet_environments.full_node.full_node.blockchain.get_peak_height() - assert height is not None - await wallet_environments.full_node.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) - ) - await wallet_environments.full_node.wait_for_wallet_synced(env.node) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - "xch": {}, - "plotnft": { - "pending_coin_removal_count": 1, - }, - }, - post_block_balance_updates={ - "xch": {}, - "plotnft": { - "pending_coin_removal_count": -1, - }, - }, - ) - ] - ) - # FINISH LEAVING (to new pool) plotnft = await plotnft_wallet.get_current_plotnft() await wallet_environments.full_node.farm_blocks_to_puzzlehash( @@ -581,47 +445,6 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ ): await plotnft_wallet.claim_rewards(action_scope=action_scope) - # Reorg (leave pool) - height = wallet_environments.full_node.full_node.blockchain.get_peak_height() - assert height is not None - await wallet_environments.full_node.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) - ) - await wallet_environments.full_node.wait_for_wallet_synced(env.node) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": leave_fee, - "<=#spendable_balance": -1, - "<=#max_send_amount": -1, - ">=#pending_change": 0, - ">=#pending_coin_removal_count": 1, - ">=#unspent_coin_count": 0, - }, - "plotnft": { - "pending_coin_removal_count": 1, - }, - }, - post_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": -leave_fee, - ">=#spendable_balance": 1, - ">=#max_send_amount": 1, - "<=#pending_change": 0, - "<=#pending_coin_removal_count": -1, - "<=#unspent_coin_count": 0, - }, - "plotnft": { - "pending_coin_removal_count": -1, - }, - }, - ) - ] - ) - # LOSE REWARDS (while leaving) plotnft = await plotnft_wallet.get_current_plotnft() [pool_reward] = await env.wallet_state_manager.plotnft2_store.get_pool_rewards(plotnft_id=plotnft_wallet.plotnft_id) @@ -642,7 +465,8 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ } }, ) - ] + ], + bundles_to_repush=[WalletSpendBundle(coin_spends, G2Element())], ) # FINISH LEAVING @@ -684,47 +508,6 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ ] ) - # Reorg (finish leaving) - height = wallet_environments.full_node.full_node.blockchain.get_peak_height() - assert height is not None - await wallet_environments.full_node.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) - ) - await wallet_environments.full_node.wait_for_wallet_synced(env.node) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": finish_leaving_fee, - "<=#spendable_balance": 0, - "<=#max_send_amount": 0, - ">=#pending_change": 0, - ">=#pending_coin_removal_count": 1, - ">=#unspent_coin_count": 0, - }, - "plotnft": { - "pending_coin_removal_count": 1, - }, - }, - post_block_balance_updates={ - "xch": { - "confirmed_wallet_balance": -finish_leaving_fee, - ">=#spendable_balance": 0, - ">=#max_send_amount": 0, - "<=#pending_change": 0, - "<=#pending_coin_removal_count": -1, - "<=#unspent_coin_count": 0, - }, - "plotnft": { - "pending_coin_removal_count": -1, - }, - }, - ) - ] - ) - # Resync start env.node._close() await env.node._await_closed() @@ -762,6 +545,7 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_ { "num_environments": 1, "blocks_needed": [1], + "reorg_exempt": True, } ], indirect=True, diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index 4e611ee4e6..2841ff9968 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -1563,6 +1563,7 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty { "num_environments": 2, "blocks_needed": [1, 1], + "reorg_exempt": True, } ], indirect=True, @@ -2299,7 +2300,11 @@ async def test_did_endpoints(wallet_environments: WalletTestFramework, capsys: p }, ), WalletStateTransition(), - ] + ], + post_reorg_balance_differences=[ + WalletStateTransition({"did": {"set_remainder": True}}), + WalletStateTransition(), + ], ) # Transfer DID @@ -2328,7 +2333,10 @@ async def test_did_endpoints(wallet_environments: WalletTestFramework, capsys: p "did": {"init": True, "set_remainder": True}, } ), - ] + ], + # TODO: there's a bug here where the user store autoincrement means this has a new ID after deleted + # Instead of 2, it becomes 3 because there was a 2 at some point (is my best guess) + reorg_exempt=True, ) async def num_wallets() -> int: diff --git a/chia/_tests/wallet/test_signer_protocol.py b/chia/_tests/wallet/test_signer_protocol.py index 5664d59efb..19dbb79f3d 100644 --- a/chia/_tests/wallet/test_signer_protocol.py +++ b/chia/_tests/wallet/test_signer_protocol.py @@ -263,14 +263,11 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ) ).signed_transactions await wallet_rpc.submit_transactions(SubmitTransactions(signed_transactions=signed_txs)) - await wallet_environments.full_node.wait_bundle_ids_in_mempool( - [ - WalletSpendBundle( - [spend.as_coin_spend() for tx in signed_txs for spend in tx.transaction_info.spends], - G2Element.from_bytes(signing_responses[0].signature), - ).name() - ] + bundle = WalletSpendBundle( + [spend.as_coin_spend() for tx in signed_txs for spend in tx.transaction_info.spends], + G2Element.from_bytes(signing_responses[0].signature), ) + await wallet_environments.full_node.wait_bundle_ids_in_mempool([bundle.name()]) await wallet_environments.process_pending_states( [ @@ -287,7 +284,8 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram }, }, ), - ] + ], + bundles_to_repush=[bundle], ) # And test that we can get compressed versions if we want diff --git a/chia/_tests/wallet/test_wallet.py b/chia/_tests/wallet/test_wallet.py index f5f60499a5..3fe28cf815 100644 --- a/chia/_tests/wallet/test_wallet.py +++ b/chia/_tests/wallet/test_wallet.py @@ -427,7 +427,8 @@ class TestWalletSimulator: } }, ), - ] + ], + reorg_exempt=True, ) await wallet_environments.process_pending_states( [ @@ -890,194 +891,6 @@ class TestWalletSimulator: assert txs_response.transactions[0].confirmed assert txs_response.transactions[1].confirmed - @pytest.mark.parametrize( - "wallet_environments", - [{"num_environments": 2, "blocks_needed": [1, 1], "reuse_puzhash": True}], - indirect=True, - ) - @pytest.mark.limit_consensus_modes(reason="irrelevant") - @pytest.mark.anyio - async def test_wallet_clawback_reorg(self, wallet_environments: WalletTestFramework) -> None: - full_node_api = wallet_environments.full_node - env = wallet_environments.environments[0] - env_2 = wallet_environments.environments[1] - wsm = env.wallet_state_manager - wsm_2 = env_2.wallet_state_manager - - tx_amount = 500 - async with wsm_2.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope: - normal_puzhash = await action_scope.get_puzzle_hash(wsm_2) - # Transfer to normal wallet - await env.rpc_client.send_transaction( - SendTransaction( - wallet_id=env.xch_wallet.id(), - amount=uint64(tx_amount), - address=env.wallet_state_manager.encode_puzzle_hash(normal_puzhash), - puzzle_decorator=[ClawbackPuzzleDecoratorOverride(decorator="CLAWBACK", clawback_timelock=uint64(5))], - push=True, - ), - wallet_environments.tx_config, - ) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - 1: { - "unconfirmed_wallet_balance": -1 * tx_amount, - "<=#spendable_balance": -1 * tx_amount, - "<=#max_send_amount": -1 * tx_amount, - ">=#pending_change": 1, # any amount increase - "pending_coin_removal_count": 1, - } - }, - post_block_balance_updates={ - 1: { - "confirmed_wallet_balance": -1 * tx_amount, - ">=#spendable_balance": 1, # any amount increase - ">=#max_send_amount": 1, # any amount increase - "<=#pending_change": -1, # any amount decrease - "pending_coin_removal_count": -1, - } - }, - ), - WalletStateTransition( - pre_block_balance_updates={}, - post_block_balance_updates={}, - ), - ] - ) - - # Check merkle coins - await time_out_assert(20, wsm.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK) - await time_out_assert(20, wsm_2.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK) - # Reorg before claim - # Test Reorg mint - height = full_node_api.full_node.blockchain.get_peak_height() - assert height is not None - await full_node_api.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 2), uint32(height + 1), bytes32.zeros, None) - ) - - await time_out_assert(20, wsm.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK) - await time_out_assert(20, wsm_2.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - 1: { - "confirmed_wallet_balance": tx_amount, # confirmed balance comes back - # clawback transaction is now outstanding - "<=#spendable_balance": -1 * tx_amount, - "<=#max_send_amount": -1 * tx_amount, - ">=#pending_change": 1, # any amount increase - "pending_coin_removal_count": 1, - } - }, - post_block_balance_updates={ - 1: { - "confirmed_wallet_balance": -1 * tx_amount, - ">=#spendable_balance": 1, # any amount increase - ">=#max_send_amount": 1, # any amount increase - "<=#pending_change": -1, # any amount decrease - "pending_coin_removal_count": -1, - } - }, - ), - WalletStateTransition( - pre_block_balance_updates={}, - post_block_balance_updates={}, - ), - ] - ) - - await time_out_assert(20, wsm.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK) - await time_out_assert(20, wsm_2.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK) - - # Claim merkle coin - await env_2.rpc_client.set_auto_claim(AutoClaimSettings(enabled=True)) - # clawback merkle coin - await wallet_environments.process_pending_states( - [ - WalletStateTransition(), - WalletStateTransition( - pre_block_balance_updates={}, - # After auto claim is set, the next block will trigger submission of clawback claims - post_block_balance_updates={ - 1: { - "unconfirmed_wallet_balance": tx_amount, - "pending_change": tx_amount, # This is a little weird but I think intentional and correct - "pending_coin_removal_count": 1, - } - }, - ), - ] - ) - await wallet_environments.process_pending_states( - [ - WalletStateTransition(), - WalletStateTransition( - pre_block_balance_updates={}, - post_block_balance_updates={ - 1: { - "confirmed_wallet_balance": tx_amount, - "spendable_balance": tx_amount, - "max_send_amount": tx_amount, - "unspent_coin_count": 1, - "pending_change": -1 * tx_amount, - "pending_coin_removal_count": -1, - } - }, - ), - ] - ) - await time_out_assert(20, wsm.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK) - await time_out_assert(20, wsm_2.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK) - # Reorg after claim - height = full_node_api.full_node.blockchain.get_peak_height() - assert height is not None - await full_node_api.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None) - ) - - await time_out_assert(20, wsm.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK) - await time_out_assert(20, wsm_2.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={}, - post_block_balance_updates={}, - ), - WalletStateTransition( - pre_block_balance_updates={ - 1: { - "confirmed_wallet_balance": -1 * tx_amount, - "spendable_balance": -1 * tx_amount, - "max_send_amount": -1 * tx_amount, - "unspent_coin_count": -1, - "pending_change": tx_amount, - "pending_coin_removal_count": 1, - } - }, - post_block_balance_updates={ - 1: { - "confirmed_wallet_balance": tx_amount, - "spendable_balance": tx_amount, - "max_send_amount": tx_amount, - "unspent_coin_count": 1, - "pending_change": -1 * tx_amount, - "pending_coin_removal_count": -1, - } - }, - ), - ] - ) - - await time_out_assert(20, wsm.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK) - await time_out_assert(20, wsm_2.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK) - @pytest.mark.parametrize( "wallet_environments", [{"num_environments": 1, "blocks_needed": [1], "trusted": True, "reuse_puzhash": True}], diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 6dd45d55c8..41319201ed 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -295,6 +295,7 @@ class DIDWallet: async def get_pending_change_balance(self) -> uint64: unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id()) addition_amount = 0 + counted_additions = set() for record in unconfirmed_tx: our_spend = False @@ -314,9 +315,10 @@ class DIDWallet: if len(memos) > 0 and len(memos[0]) == 32 } if (await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id(), hint_dict)) and ( - coin not in record.removals + coin not in record.removals and coin not in counted_additions ): addition_amount += coin.amount + counted_additions.add(coin) return uint64(addition_amount) @@ -1034,7 +1036,8 @@ class DIDWallet: async def add_parent(self, name: bytes32, parent: LineageProof | None) -> None: self.log.info(f"Adding parent {name}: {parent}") - current_list = self.did_info.parent_info.copy() + # coping for not being a dict - thanks streamable! + current_list = [(n, p) for n, p in self.did_info.parent_info if n != name] current_list.append((name, parent)) did_info = DIDInfo( origin_coin=self.did_info.origin_coin, diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index f0323cc1c8..fa889bfe34 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -791,7 +791,7 @@ class CRCATWallet(CATWallet): wallet_id=self.id(), sent_to=[], trade_id=None, - type=uint32(TransactionType.INCOMING_TX.value), + type=uint32(TransactionType.OUTGOING_TX.value), name=claim_bundle.name(), memos=compute_memos(claim_bundle), valid_times=parse_timelock_info(extra_conditions), diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 6435caa937..87a0afb09f 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -115,6 +115,7 @@ class Wallet: unconfirmed_tx: list[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( self.id() ) + counted_additions = set() addition_amount = 0 for record in unconfirmed_tx: @@ -139,8 +140,12 @@ class Wallet: continue for coin in record.additions: - if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()): + if ( + await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()) + and coin not in counted_additions + ): addition_amount += coin.amount + counted_additions.add(coin) return uint64(addition_amount) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 048598e1a6..81ae449c18 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -881,6 +881,11 @@ class WalletStateManager: if await self.does_coin_belong_to_wallet(addition, wallet_id, record.hint_dict()): all_unspent_coins.add(addition) + for record in unconfirmed_tx: + if record.type in CLAWBACK_INCOMING_TRANSACTION_TYPES: + # We do not wish to consider clawback-able funds as unconfirmed. + # That is reserved for when the action to actually claw a tx back or forward is initiated. + continue for removal in record.removals: if ( await self.does_coin_belong_to_wallet(removal, wallet_id, record.hint_dict())