From bf4cdf22a52f4e74ae55352bccf3f90b713d7c6a Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Fri, 3 Nov 2023 06:30:59 -0700 Subject: [PATCH] Port VC lifecycle tests to WalletTestFramework (#16292) --- chia/simulator/full_node_simulator.py | 28 + chia/wallet/did_wallet/did_wallet.py | 6 +- chia/wallet/transaction_record.py | 5 +- chia/wallet/vc_wallet/cr_cat_wallet.py | 19 +- chia/wallet/vc_wallet/vc_wallet.py | 11 +- tests/wallet/conftest.py | 23 +- tests/wallet/vc_wallet/test_vc_wallet.py | 703 +++++++++++++++-------- 7 files changed, 538 insertions(+), 257 deletions(-) diff --git a/chia/simulator/full_node_simulator.py b/chia/simulator/full_node_simulator.py index d93f36864645..eed60dbef6ca 100644 --- a/chia/simulator/full_node_simulator.py +++ b/chia/simulator/full_node_simulator.py @@ -479,6 +479,34 @@ async def wait_transaction_records_entered_mempool( await asyncio.sleep(backoff) + async def wait_transaction_records_marked_as_in_mempool( + self, + record_ids: Collection[bytes32], + wallet_node: WalletNode, + timeout: Union[None, float] = 10, + ) -> None: + """Wait until the transaction records have been marked that they have made it into the mempool. Transaction + records with no spend bundle are ignored. + + Arguments: + records: The transaction records to wait for. + """ + with anyio.fail_after(delay=adjusted_timeout(timeout)): + ids_to_check: Set[bytes32] = set(record_ids) + + for backoff in backoff_times(): + found = set() + for txid in ids_to_check: + tx = await wallet_node.wallet_state_manager.tx_store.get_transaction_record(txid) + if tx is not None and (tx.is_in_mempool() or tx.spend_bundle is None): + found.add(txid) + ids_to_check = ids_to_check.difference(found) + + if len(ids_to_check) == 0: + return + + await asyncio.sleep(backoff) + async def process_transaction_records( self, records: Collection[TransactionRecord], diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 5d8390e073f9..9c5b6ed88d85 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -730,7 +730,7 @@ async def transfer_did( sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=bytes32.secret(), + name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), valid_times=parse_timelock_info(extra_conditions), ) @@ -1319,7 +1319,7 @@ async def generate_new_decentralised_id( to_puzzle_hash=await self.standard_wallet.get_puzzle_hash(False), fee_amount=fee, confirmed=False, - sent=uint32(10), + sent=uint32(0), spend_bundle=full_spend, additions=full_spend.additions(), removals=full_spend.removals(), @@ -1327,7 +1327,7 @@ async def generate_new_decentralised_id( sent_to=[], trade_id=None, type=uint32(TransactionType.INCOMING_TX.value), - name=bytes32.secret(), + name=full_spend.name(), memos=[], valid_times=ConditionValidTimes(), ) diff --git a/chia/wallet/transaction_record.py b/chia/wallet/transaction_record.py index 095306bdfba7..6e039dbd67b1 100644 --- a/chia/wallet/transaction_record.py +++ b/chia/wallet/transaction_record.py @@ -57,11 +57,10 @@ class TransactionRecordOld(Streamable): memos: List[Tuple[bytes32, List[bytes]]] def is_in_mempool(self) -> bool: - # If one of the nodes we sent it to responded with success, we set it to success + # If one of the nodes we sent it to responded with success or pending, we return True for _, mis, _ in self.sent_to: - if MempoolInclusionStatus(mis) == MempoolInclusionStatus.SUCCESS: + if MempoolInclusionStatus(mis) in (MempoolInclusionStatus.SUCCESS, MempoolInclusionStatus.PENDING): return True - # Note, transactions pending inclusion (pending) return false return False def height_farmed(self, genesis_challenge: bytes32) -> Optional[uint32]: diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index dae7b3eb4f3d..dd9b59334e1b 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -683,6 +683,8 @@ async def generate_signed_transaction( unsigned_spend_bundle.coin_spends ) + other_tx_removals: Set[Coin] = {removal for tx in other_txs for removal in tx.removals} + other_tx_additions: Set[Coin] = {removal for tx in other_txs for removal in tx.additions} tx_list = [ TransactionRecord( confirmed_at_height=uint32(0), @@ -693,8 +695,8 @@ async def generate_signed_transaction( confirmed=False, sent=uint32(0), spend_bundle=signed_spend_bundle if i == 0 else None, - additions=signed_spend_bundle.additions() if i == 0 else [], - removals=signed_spend_bundle.removals() if i == 0 else [], + additions=list(set(signed_spend_bundle.additions()) - other_tx_additions) if i == 0 else [], + removals=list(set(signed_spend_bundle.removals()) - other_tx_removals) if i == 0 else [], wallet_id=self.id(), sent_to=[], trade_id=None, @@ -814,6 +816,12 @@ async def claim_pending_approval_balance( [claim_bundle, *(tx.spend_bundle for tx in vc_txs if tx.spend_bundle is not None)] ) + other_txs: List[TransactionRecord] = [ + *(dataclasses.replace(tx, spend_bundle=None) for tx in vc_txs), + *((dataclasses.replace(chia_tx, spend_bundle=None),) if chia_tx is not None else []), + ] + other_additions: Set[Coin] = {rem for tx in other_txs for rem in tx.additions} + other_removals: Set[Coin] = {rem for tx in other_txs for rem in tx.removals} return [ TransactionRecord( confirmed_at_height=uint32(0), @@ -824,8 +832,8 @@ async def claim_pending_approval_balance( confirmed=False, sent=uint32(0), spend_bundle=claim_bundle, - additions=claim_bundle.additions(), - removals=claim_bundle.removals(), + additions=list(set(claim_bundle.additions()) - other_additions), + removals=list(set(claim_bundle.removals()) - other_removals), wallet_id=self.id(), sent_to=[], trade_id=None, @@ -834,8 +842,7 @@ async def claim_pending_approval_balance( memos=list(compute_memos(claim_bundle).items()), valid_times=parse_timelock_info(extra_conditions), ), - *(dataclasses.replace(tx, spend_bundle=None) for tx in vc_txs), - *((dataclasses.replace(chia_tx, spend_bundle=None),) if chia_tx is not None else []), + *other_txs, ] async def match_puzzle_info(self, puzzle_driver: PuzzleInfo) -> bool: diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index c93771e3ba39..a89012b289a3 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -334,13 +334,13 @@ async def generate_signed_transaction( raise ValueError( f"Cannot find the required DID {vc_record.vc.proof_provider.hex()}." ) # pragma: no cover + add_list: List[Coin] = list(spend_bundles[0].additions()) + rem_list: List[Coin] = list(spend_bundles[0].removals()) if chia_tx is not None and chia_tx.spend_bundle is not None: spend_bundles.append(chia_tx.spend_bundle) tx_list.append(dataclasses.replace(chia_tx, spend_bundle=None)) spend_bundle = SpendBundle.aggregate(spend_bundles) now = uint64(int(time.time())) - add_list: List[Coin] = list(spend_bundle.additions()) - rem_list: List[Coin] = list(spend_bundle.removals()) tx_list.append( TransactionRecord( confirmed_at_height=uint32(0), @@ -422,16 +422,15 @@ async def revoke_vc( ) assert did_tx.spend_bundle is not None final_bundle: SpendBundle = SpendBundle.aggregate([SpendBundle([vc_spend], G2Element()), did_tx.spend_bundle]) - did_tx = dataclasses.replace(did_tx, spend_bundle=final_bundle) + did_tx = dataclasses.replace(did_tx, spend_bundle=final_bundle, name=final_bundle.name()) if fee > 0: chia_tx: TransactionRecord = await self.wallet_state_manager.main_wallet.create_tandem_xch_tx( fee, tx_config, vc_announcement ) assert did_tx.spend_bundle is not None assert chia_tx.spend_bundle is not None - did_tx = dataclasses.replace( - did_tx, spend_bundle=SpendBundle.aggregate([chia_tx.spend_bundle, did_tx.spend_bundle]) - ) + new_bundle = SpendBundle.aggregate([chia_tx.spend_bundle, did_tx.spend_bundle]) + did_tx = dataclasses.replace(did_tx, spend_bundle=new_bundle, name=new_bundle.name()) chia_tx = dataclasses.replace(chia_tx, spend_bundle=None) return [did_tx, chia_tx] else: diff --git a/tests/wallet/conftest.py b/tests/wallet/conftest.py index 3e5e7b4bdf91..00272e933c88 100644 --- a/tests/wallet/conftest.py +++ b/tests/wallet/conftest.py @@ -236,10 +236,17 @@ async def process_pending_states(self, state_transitions: List[WalletStateTransi puzzle_hash_indexes.append(ph_indexes) # Gather all pending transactions and ensure they enter mempool - pending_txs: List[List[TransactionRecord]] = [] - for env in self.environments: - pending_txs.append(await env.wallet_state_manager.tx_store.get_all_unconfirmed()) + pending_txs: List[List[TransactionRecord]] = [ + await env.wallet_state_manager.tx_store.get_all_unconfirmed() for env in self.environments + ] await self.full_node.wait_transaction_records_entered_mempool([tx for txs in pending_txs for tx in txs]) + for local_pending_txs, (i, env) in zip(pending_txs, enumerate(self.environments)): + try: + await self.full_node.wait_transaction_records_marked_as_in_mempool( + [tx.name for tx in local_pending_txs], env.wallet_node + ) + except TimeoutError: # pragma: no cover + raise ValueError(f"All tx records from env index {i} were not marked correctly with `.is_in_mempool()`") # Check balances prior to block try: @@ -247,8 +254,9 @@ async def process_pending_states(self, state_transitions: List[WalletStateTransi await self.full_node.wait_for_wallet_synced(wallet_node=env.wallet_node, timeout=20) for i, (env, transition) in enumerate(zip(self.environments, state_transitions)): try: - await env.change_balances(transition.pre_block_balance_updates) - await env.check_balances(transition.pre_block_additional_balance_info) + 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: @@ -263,8 +271,9 @@ async def process_pending_states(self, state_transitions: List[WalletStateTransi await self.full_node.wait_for_wallet_synced(wallet_node=env.wallet_node, timeout=20) for i, (env, transition) in enumerate(zip(self.environments, state_transitions)): try: - await env.change_balances(transition.post_block_balance_updates) - await env.check_balances(transition.post_block_additional_balance_info) + 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: diff --git a/tests/wallet/vc_wallet/test_vc_wallet.py b/tests/wallet/vc_wallet/test_vc_wallet.py index d3377ca39817..d220ad1cb5ac 100644 --- a/tests/wallet/vc_wallet/test_vc_wallet.py +++ b/tests/wallet/vc_wallet/test_vc_wallet.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses from typing import Any, Awaitable, Callable, List, Optional import pytest @@ -8,7 +9,7 @@ from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.simulator.full_node_simulator import FullNodeSimulator -from chia.simulator.time_out_assert import time_out_assert, time_out_assert_not_none +from chia.simulator.time_out_assert import time_out_assert_not_none from chia.types.blockchain_format.coin import coin_as_list from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 @@ -16,7 +17,7 @@ from chia.types.peer_info import PeerInfo from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import encode_puzzle_hash -from chia.util.ints import uint32, uint64 +from chia.util.ints import uint64 from chia.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.did_wallet.did_wallet import DIDWallet @@ -29,18 +30,7 @@ from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode -from chia.wallet.wallet_state_manager import WalletStateManager -from tests.wallet.conftest import WalletTestFramework - - -async def is_transaction_confirmed(wallet_state_manager: WalletStateManager, tx_id: bytes32) -> bool: - tr = await wallet_state_manager.get_transaction(tx_id) - if tr is None: - return False # pragma: no cover - elif not tr.confirmed: - return False # pragma: no cover - else: - return True +from tests.wallet.conftest import WalletEnvironment, WalletStateTransition, WalletTestFramework async def mint_cr_cat( @@ -113,7 +103,9 @@ async def mint_cr_cat( G2Element(), ) spend_bundle = SpendBundle.aggregate([spend_bundle, eve_spend]) - await client_0.push_tx(spend_bundle) # type: ignore [no-untyped-call] + await wallet_node_0.wallet_state_manager.add_pending_transaction( + dataclasses.replace(tx, spend_bundle=spend_bundle, name=spend_bundle.name()) + ) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) @@ -130,7 +122,10 @@ async def mint_cr_cat( ) @pytest.mark.anyio async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None: + # Setup full_node_api: FullNodeSimulator = wallet_environments.full_node + env_0 = wallet_environments.environments[0] + env_1 = wallet_environments.environments[1] wallet_node_0 = wallet_environments.environments[0].wallet_node wallet_node_1 = wallet_environments.environments[1].wallet_node wallet_0 = wallet_environments.environments[0].xch_wallet @@ -138,73 +133,147 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None: client_0 = wallet_environments.environments[0].rpc_client client_1 = wallet_environments.environments[1].rpc_client - confirmed_balance: int = await wallet_0.get_confirmed_balance() - did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet( - wallet_node_0.wallet_state_manager, wallet_0, uint64(1) + # Define wallet aliases + env_0.wallet_aliases = { + "xch": 1, + "did": 2, + "vc": 3, + "crcat": 4, + } + env_1.wallet_aliases = { + "xch": 1, + "crcat": 2, + "vc": 3, + } + + # Generate DID as an "authorized provider" + did_id: bytes32 = bytes32.from_hexstr( + (await DIDWallet.create_new_did_wallet(wallet_node_0.wallet_state_manager, wallet_0, uint64(1))).get_my_DID() ) - confirmed_balance -= 1 - spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) - spend_bundle = spend_bundle_list[0].spend_bundle - assert spend_bundle - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + # Mint a VC + vc_record, _ = await client_0.vc_mint( + did_id, wallet_environments.tx_config, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) + ) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await time_out_assert(15, wallet_0.get_confirmed_balance, confirmed_balance) - did_id = bytes32.from_hexstr(did_wallet.get_my_DID()) - vc_record, txs = await client_0.vc_mint( - did_id, DEFAULT_TX_CONFIG, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) - ) - confirmed_balance -= 1 - confirmed_balance -= 200 - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(30, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await time_out_assert(15, wallet_0.get_confirmed_balance, confirmed_balance) - vc_wallet = await wallet_node_0.wallet_state_manager.get_all_wallet_info_entries(wallet_type=WalletType.VC) - assert len(vc_wallet) == 1 + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -202, # 200 for VC mint fee, 1 for VC singleton, 1 for DID mint + # I'm not sure incrementing pending_coin_removal_count here by 3 is the spirit of this number + # One existing coin has been removed and two ephemeral coins have been removed + # Does pending_coin_removal_count attempt to show the number of current pending removals + # Or does it intend to just mean all pending removals that we should eventually get states for? + "pending_coin_removal_count": 4, # 3 for VC mint, 1 for DID mint + "<=#spendable_balance": -202, + "<=#max_send_amount": -202, + "set_remainder": True, + }, + "did": {"init": True, "set_remainder": True}, + "vc": { + "init": True, + "confirmed_wallet_balance": 0, + "unconfirmed_wallet_balance": 0, + "spendable_balance": 0, + "pending_change": 0, + "max_send_amount": 0, + "unspent_coin_count": 0, + "pending_coin_removal_count": 0, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -202, # 200 for VC mint fee, 1 for VC singleton, 1 for DID mint + "pending_coin_removal_count": -4, # 3 for VC mint, 1 for DID mint + "set_remainder": True, + }, + "did": { + "set_remainder": True, + }, + "vc": { + "unspent_coin_count": 1, + }, + }, + ), + WalletStateTransition(), + ] + ) new_vc_record: Optional[VCRecord] = await client_0.vc_get(vc_record.vc.launcher_id) assert new_vc_record is not None # Spend VC proofs: VCProofs = VCProofs({"foo": "1", "bar": "1", "baz": "1", "qux": "1", "grault": "1"}) proof_root: bytes32 = proofs.root() - txs = await client_0.vc_spend( + await client_0.vc_spend( vc_record.vc.launcher_id, - DEFAULT_TX_CONFIG, + wallet_environments.tx_config, new_proof_hash=proof_root, fee=uint64(100), ) - confirmed_balance -= 100 - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await time_out_assert(15, wallet_0.get_confirmed_balance, confirmed_balance) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -100, + "pending_coin_removal_count": 1, + "<=#spendable_balance": -100, + "<=#max_send_amount": -100, + "set_remainder": True, + }, + "did": { + "spendable_balance": -1, + "pending_change": 1, + "pending_coin_removal_count": 1, + }, + "vc": { + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -100, + "pending_coin_removal_count": -1, + "set_remainder": True, + }, + "did": { + "spendable_balance": 1, + "pending_change": -1, + "pending_coin_removal_count": -1, + }, + "vc": { + "pending_coin_removal_count": -1, + }, + }, + ), + WalletStateTransition(), + ] + ) vc_record_updated: Optional[VCRecord] = await client_0.vc_get(vc_record.vc.launcher_id) assert vc_record_updated is not None assert vc_record_updated.vc.proof_hash == proof_root # Do a mundane spend - txs = await client_0.vc_spend(vc_record.vc.launcher_id, DEFAULT_TX_CONFIG) - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await time_out_assert(15, wallet_0.get_confirmed_balance, confirmed_balance) - - async def check_vc_record_has_parent_id( - parent_id: bytes32, client: WalletRpcClient, launcher_id: bytes32 - ) -> Optional[Literal[True]]: - vc_record = await client.vc_get(launcher_id) - result: Optional[Literal[True]] = None - if vc_record is not None: - result = True if vc_record.vc.coin.parent_coin_info == parent_id else None - return result - - await time_out_assert_not_none( - 10, check_vc_record_has_parent_id, vc_record_updated.vc.coin.name(), client_0, vc_record.vc.launcher_id + await client_0.vc_spend(vc_record.vc.launcher_id, wallet_environments.tx_config) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "vc": { + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "vc": { + "pending_coin_removal_count": -1, + }, + }, + ), + WalletStateTransition(), + ] ) - vc_record_updated = await client_0.vc_get(vc_record.vc.launcher_id) - assert vc_record_updated is not None # Add proofs to DB await client_0.vc_add_proofs(proofs.key_value_pairs) @@ -214,29 +283,50 @@ async def check_vc_record_has_parent_id( assert len(vc_records) == 1 assert fetched_proofs[proof_root.hex()] == proofs.key_value_pairs + # Mint CR-CAT await mint_cr_cat(1, wallet_0, wallet_node_0, client_0, full_node_api, [did_id]) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_0) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=20) - confirmed_balance += 2_000_000_000_000 - confirmed_balance -= 100 # cat mint amount - - # Send CR-CAT to another wallet - async def check_length(length: int, func: Callable[..., Awaitable[Any]], *args: Any) -> Optional[Literal[True]]: - if len(await func(*args)) == length: - return True - return None # pragma: no cover - - await time_out_assert_not_none( - 15, check_length, 1, wallet_node_0.wallet_state_manager.get_all_wallet_info_entries, WalletType.CRCAT + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -100, + "<=#spendable_balance": -100, + "<=#max_send_amount": -100, + "set_remainder": True, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -100, + "set_remainder": True, + }, + "crcat": { + "init": True, + "confirmed_wallet_balance": 100, + "unconfirmed_wallet_balance": 100, + "spendable_balance": 100, + "pending_change": 0, + "max_send_amount": 100, + "unspent_coin_count": 1, + "pending_coin_removal_count": 0, + }, + }, + ), + WalletStateTransition(), + ] ) - cr_cat_wallet_id_0: uint32 = ( - await wallet_node_0.wallet_state_manager.get_all_wallet_info_entries(wallet_type=WalletType.CRCAT) - )[0].id - cr_cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[cr_cat_wallet_id_0] + + cr_cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[env_0.dealias_wallet_id("crcat")] assert isinstance(cr_cat_wallet_0, CRCATWallet) + assert await CRCATWallet.create( # just testing the create method doesn't throw + wallet_node_0.wallet_state_manager, + wallet_node_0.wallet_state_manager.main_wallet, + (await wallet_node_0.wallet_state_manager.get_all_wallet_info_entries(wallet_type=WalletType.CRCAT))[0], + ) assert { "data": bytes(cr_cat_wallet_0.info).hex(), - "id": cr_cat_wallet_id_0, + "id": env_0.dealias_wallet_id("crcat"), "name": cr_cat_wallet_0.get_name(), "type": cr_cat_wallet_0.type(), "authorized_providers": [p.hex() for p in cr_cat_wallet_0.info.authorized_providers], @@ -247,58 +337,77 @@ async def check_length(length: int, func: Callable[..., Awaitable[Any]], *args: wallet_1_addr = encode_puzzle_hash(wallet_1_ph, "txch") tx = await client_0.cat_spend( cr_cat_wallet_0.id(), - DEFAULT_TX_CONFIG, + wallet_environments.tx_config, uint64(90), wallet_1_addr, uint64(2000000000), memos=["hey"], ) - confirmed_balance -= 2000000000 await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) - assert tx.spend_bundle is not None - spend_bundle = tx.spend_bundle - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=20) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_1, timeout=20) - await time_out_assert(15, wallet_0.get_confirmed_balance, confirmed_balance) - await time_out_assert(15, cr_cat_wallet_0.get_confirmed_balance, 10) - - # Check the other wallet recieved it - await time_out_assert_not_none( - 15, check_length, 1, wallet_node_1.wallet_state_manager.get_all_wallet_info_entries, WalletType.CRCAT - ) - cr_cat_wallet_info = ( - await wallet_node_1.wallet_state_manager.get_all_wallet_info_entries(wallet_type=WalletType.CRCAT) - )[0] - cr_cat_wallet_id_1: uint32 = cr_cat_wallet_info.id - cr_cat_wallet_1 = wallet_node_1.wallet_state_manager.wallets[cr_cat_wallet_id_1] - assert isinstance(cr_cat_wallet_1, CRCATWallet) - assert await CRCATWallet.create( # just testing the create method doesn't throw - wallet_node_1.wallet_state_manager, - wallet_node_1.wallet_state_manager.main_wallet, - cr_cat_wallet_info, - ) - await time_out_assert(15, cr_cat_wallet_1.get_confirmed_balance, 0) - await time_out_assert(15, cr_cat_wallet_1.get_pending_approval_balance, 90) - await time_out_assert(15, cr_cat_wallet_1.get_unconfirmed_balance, 90) - assert await client_1.get_wallet_balance(cr_cat_wallet_id_1) == { - "confirmed_wallet_balance": 0, - "unconfirmed_wallet_balance": 0, - "spendable_balance": 0, - "pending_change": 0, - "max_send_amount": 0, - "unspent_coin_count": 0, - "pending_coin_removal_count": 0, - "pending_approval_balance": 90, - "wallet_id": cr_cat_wallet_id_1, - "wallet_type": cr_cat_wallet_1.type().value, - "asset_id": cr_cat_wallet_1.get_asset_id(), - "fingerprint": wallet_node_1.logged_in_fingerprint, - } - assert await cr_cat_wallet_1.match_hinted_coin(next(c for c in tx.additions if c.amount == 90), wallet_1_ph) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -2000000000, + "pending_coin_removal_count": 1, + "<=#spendable_balance": -2000000000, + "<=#max_send_amount": -2000000000, + "set_remainder": True, + }, + "vc": { + "pending_coin_removal_count": 1, + }, + "crcat": { + "unconfirmed_wallet_balance": -90, + "spendable_balance": -100, + "max_send_amount": -100, + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -2000000000, + "pending_coin_removal_count": -1, + "set_remainder": True, + }, + "vc": { + "pending_coin_removal_count": -1, + }, + "crcat": { + "confirmed_wallet_balance": -90, + "spendable_balance": 10, + "max_send_amount": 10, + "pending_coin_removal_count": -1, + }, + }, + ), + WalletStateTransition( + post_block_balance_updates={ + "crcat": { + "init": True, + "confirmed_wallet_balance": 0, + "unconfirmed_wallet_balance": 0, + "spendable_balance": 0, + "pending_change": 0, + "max_send_amount": 0, + "unspent_coin_count": 0, + "pending_coin_removal_count": 0, + } + }, + post_block_additional_balance_info={ + "crcat": { + "pending_approval_balance": 90, + }, + }, + ), + ] + ) + assert await wallet_node_1.wallet_state_manager.wallets[env_1.dealias_wallet_id("crcat")].match_hinted_coin( + next(c for c in tx.additions if c.amount == 90), wallet_1_ph + ) pending_tx = await client_1.get_transactions( - cr_cat_wallet_1.id(), + env_1.dealias_wallet_id("crcat"), 0, 1, reverse=True, @@ -307,162 +416,292 @@ async def check_length(length: int, func: Callable[..., Awaitable[Any]], *args: assert len(pending_tx) == 1 # Send the VC to wallet_1 to use for the CR-CATs - txs = await client_0.vc_spend( - vc_record.vc.launcher_id, DEFAULT_TX_CONFIG, new_puzhash=await wallet_1.get_new_puzzlehash() + await client_0.vc_spend( + vc_record.vc.launcher_id, wallet_environments.tx_config, new_puzhash=await wallet_1.get_new_puzzlehash() + ) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "vc": { + "pending_coin_removal_count": 1, + } + }, + post_block_balance_updates={ + "vc": { + "pending_coin_removal_count": -1, + "unspent_coin_count": -1, + } + }, + ), + WalletStateTransition( + post_block_balance_updates={ + "vc": {"init": True, "set_remainder": True}, + } + ), + ] ) - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_1, timeout=20) - vc_record_updated = await client_1.vc_get(vc_record.vc.launcher_id) - assert vc_record_updated is not None await client_1.vc_add_proofs(proofs.key_value_pairs) # Claim the pending approval to our wallet - txs = await client_1.crcat_approve_pending( - uint32(cr_cat_wallet_id_1), + await client_1.crcat_approve_pending( + env_1.dealias_wallet_id("crcat"), uint64(90), - DEFAULT_TX_CONFIG, + wallet_environments.tx_config, fee=uint64(90), ) - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_1, timeout=20) - await time_out_assert(15, cr_cat_wallet_1.get_confirmed_balance, 90) - await time_out_assert(15, cr_cat_wallet_1.get_pending_approval_balance, 0) - await time_out_assert(15, cr_cat_wallet_1.get_unconfirmed_balance, 90) - await time_out_assert( - 15, cr_cat_wallet_1.wallet_state_manager.get_confirmed_balance_for_wallet, 90, cr_cat_wallet_id_1 - ) - await time_out_assert_not_none( - 10, check_vc_record_has_parent_id, vc_record_updated.vc.coin.name(), client_1, vc_record.vc.launcher_id + await wallet_environments.process_pending_states( + [ + WalletStateTransition(), + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -90, + "pending_coin_removal_count": 1, + "<=#spendable_balance": -90, + "<=#max_send_amount": -90, + "set_remainder": True, + }, + "vc": { + "pending_coin_removal_count": 1, + }, + "crcat": { + "unconfirmed_wallet_balance": 90, + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -90, + "pending_coin_removal_count": -1, + "set_remainder": True, + }, + "vc": { + "pending_coin_removal_count": -1, + }, + "crcat": { + "confirmed_wallet_balance": 90, + "spendable_balance": 90, + "max_send_amount": 90, + "unspent_coin_count": 1, + "pending_coin_removal_count": -1, + }, + }, + post_block_additional_balance_info={ + "crcat": { + "pending_approval_balance": 0, + }, + }, + ), + ] ) - vc_record_updated = await client_1.vc_get(vc_record.vc.launcher_id) - assert vc_record_updated is not None - for tx in txs: - await time_out_assert(15, is_transaction_confirmed, True, cr_cat_wallet_1.wallet_state_manager, tx.name) # (Negative test) Try to spend a CR-CAT that we don't have a valid VC for with pytest.raises(ValueError): - tx = await client_0.cat_spend( + await client_0.cat_spend( cr_cat_wallet_0.id(), - DEFAULT_TX_CONFIG, + wallet_environments.tx_config, uint64(10), wallet_1_addr, ) # Test melting a CRCAT tx = await client_1.cat_spend( - cr_cat_wallet_id_1, - DEFAULT_TX_CONFIG.override(reuse_puzhash=True), + env_1.dealias_wallet_id("crcat"), + wallet_environments.tx_config, uint64(20), wallet_1_addr, uint64(0), cat_discrepancy=(-50, Program.to(None), Program.to(None)), ) await wallet_node_1.wallet_state_manager.add_pending_transaction(tx) - assert tx.spend_bundle is not None - spend_bundle = tx.spend_bundle - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_1, timeout=20) - # should go straight to confirmed because we sent to ourselves - await time_out_assert(15, cr_cat_wallet_1.get_confirmed_balance, 40) - await time_out_assert(15, cr_cat_wallet_1.get_pending_approval_balance, 0) - await time_out_assert(15, cr_cat_wallet_1.get_unconfirmed_balance, 40) - - # Revoke VC - await time_out_assert_not_none( - 10, check_vc_record_has_parent_id, vc_record_updated.vc.coin.name(), client_1, vc_record.vc.launcher_id + await wallet_environments.process_pending_states( + [ + WalletStateTransition(), + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": 20, + "pending_coin_removal_count": 1, + "<=#spendable_balance": 20, + "<=#max_send_amount": 20, + "set_remainder": True, + }, + "vc": { + "pending_coin_removal_count": 1, + }, + "crcat": { + "unconfirmed_wallet_balance": -50, + "spendable_balance": -90, + "max_send_amount": -90, + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": 20, + "pending_coin_removal_count": -1, + "set_remainder": True, + }, + "vc": { + "pending_coin_removal_count": -1, + }, + "crcat": { + "confirmed_wallet_balance": -50, # should go straight to confirmed because we sent to ourselves + "spendable_balance": 40, + "max_send_amount": 40, + "pending_coin_removal_count": -1, + "unspent_coin_count": 1, + }, + }, + ), + ] ) vc_record_updated = await client_1.vc_get(vc_record_updated.vc.launcher_id) assert vc_record_updated is not None - txs = await client_0.vc_revoke(vc_record_updated.vc.coin.parent_coin_info, DEFAULT_TX_CONFIG, uint64(1)) - confirmed_balance -= 1 - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=1, wallet=wallet_1) - await time_out_assert(15, wallet_0.get_confirmed_balance, confirmed_balance) - vc_record_revoked: Optional[VCRecord] = await client_1.vc_get(vc_record.vc.launcher_id) - assert vc_record_revoked is None + + # Revoke VC + await client_0.vc_revoke(vc_record_updated.vc.coin.parent_coin_info, wallet_environments.tx_config, uint64(1)) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -1, + "pending_coin_removal_count": 1, + "<=#spendable_balance": -1, + "<=#max_send_amount": -1, + "set_remainder": True, + }, + "did": { + "spendable_balance": -1, + "pending_change": 1, + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -1, + "pending_coin_removal_count": -1, + "set_remainder": True, + }, + "did": { + "spendable_balance": 1, + "pending_change": -1, + "pending_coin_removal_count": -1, + }, + }, + ), + WalletStateTransition( + post_block_balance_updates={ + "vc": { + "unspent_coin_count": -1, + }, + }, + ), + ] + ) assert ( len(await (await wallet_node_0.wallet_state_manager.get_or_create_vc_wallet()).store.get_unconfirmed_vcs()) == 0 ) @pytest.mark.parametrize( - "trusted", - [True, False], + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [1], + } + ], + indirect=True, ) @pytest.mark.anyio -async def test_self_revoke( - self_hostname: str, - one_wallet_and_one_simulator_services: Any, - trusted: Any, -) -> None: - num_blocks = 1 - full_nodes, wallets, bt = one_wallet_and_one_simulator_services - full_node_api: FullNodeSimulator = full_nodes[0]._api - full_node_server = full_node_api.full_node.server - wallet_service_0 = wallets[0] - wallet_node_0 = wallet_service_0._node - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - - client_0 = await WalletRpcClient.create( - bt.config["self_hostname"], - wallet_service_0.rpc_server.listen_port, - wallet_service_0.root_path, - wallet_service_0.config, - ) - - if trusted: - wallet_node_0.config["trusted_peers"] = { - full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() - } - else: - wallet_node_0.config["trusted_peers"] = {} - - await wallet_node_0.server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None) - await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_0) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=20) +async def test_self_revoke(wallet_environments: WalletTestFramework) -> None: + # Setup + env_0: WalletEnvironment = wallet_environments.environments[0] + wallet_node_0 = env_0.wallet_node + wallet_0 = env_0.xch_wallet + client_0 = env_0.rpc_client + + # Aliases + env_0.wallet_aliases = { + "xch": 1, + "did": 2, + "vc": 3, + } + # Generate DID as an "authorized provider" did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet( wallet_node_0.wallet_state_manager, wallet_0, uint64(1) ) - spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) - spend_bundle = spend_bundle_list[0].spend_bundle - assert spend_bundle - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + did_id: bytes32 = bytes32.from_hexstr(did_wallet.get_my_DID()) - did_id = bytes32.from_hexstr(did_wallet.get_my_DID()) - vc_record, txs = await client_0.vc_mint( - did_id, DEFAULT_TX_CONFIG, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) + vc_record, _ = await client_0.vc_mint( + did_id, wallet_environments.tx_config, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) + ) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + # Balance checking for this spend covered in test_vc_lifecycle + pre_block_balance_updates={ + "xch": {"set_remainder": True}, + "did": {"init": True, "set_remainder": True}, + "vc": {"init": True, "set_remainder": True}, + }, + post_block_balance_updates={ + "xch": {"set_remainder": True}, + "did": {"set_remainder": True}, + "vc": {"set_remainder": True}, + }, + ) + ] ) - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(30, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_0) - vc_wallet = await wallet_node_0.wallet_state_manager.get_all_wallet_info_entries(wallet_type=WalletType.VC) - assert len(vc_wallet) == 1 new_vc_record: Optional[VCRecord] = await client_0.vc_get(vc_record.vc.launcher_id) assert new_vc_record is not None # Test a negative case real quick (mostly unrelated) with pytest.raises(ValueError, match="at the same time"): await (await wallet_node_0.wallet_state_manager.get_or_create_vc_wallet()).generate_signed_transaction( - new_vc_record.vc.launcher_id, DEFAULT_TX_CONFIG, new_proof_hash=bytes32([0] * 32), self_revoke=True + new_vc_record.vc.launcher_id, + wallet_environments.tx_config, + new_proof_hash=bytes32([0] * 32), + self_revoke=True, ) - await did_wallet.transfer_did(bytes32([0] * 32), uint64(0), False, DEFAULT_TX_CONFIG) - spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) - spend_bundle = spend_bundle_list[0].spend_bundle - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_0) + # Send the DID to oblivion + await did_wallet.transfer_did(bytes32([0] * 32), uint64(0), False, wallet_environments.tx_config) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "did": {"set_remainder": True}, + }, + post_block_balance_updates={}, + ) + ] + ) - txs = await client_0.vc_revoke(new_vc_record.vc.coin.parent_coin_info, DEFAULT_TX_CONFIG, uint64(0)) - spend_bundle = next(tx.spend_bundle for tx in txs if tx.spend_bundle is not None) - await time_out_assert_not_none(30, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_0) + # Make sure revoking still works + await client_0.vc_revoke(new_vc_record.vc.coin.parent_coin_info, wallet_environments.tx_config, uint64(0)) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + # Balance checking for this spend covered in test_vc_lifecycle + pre_block_balance_updates={ + "vc": { + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "vc": { + "pending_coin_removal_count": -1, + "unspent_coin_count": -1, + }, + }, + ) + ] + ) vc_record_revoked: Optional[VCRecord] = await client_0.vc_get(vc_record.vc.launcher_id) assert vc_record_revoked is None assert (