From 5bd46cedfba3a53a7257abd485c7c031ca2e3d0c Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 3 Sep 2024 12:43:23 -0700 Subject: [PATCH 1/7] Remove unnecessary indentation --- tests/test_cat_workflow.py | 529 +++++++++++++++++++------------------ 1 file changed, 265 insertions(+), 264 deletions(-) diff --git a/tests/test_cat_workflow.py b/tests/test_cat_workflow.py index 5a17a76..2d2a5fe 100644 --- a/tests/test_cat_workflow.py +++ b/tests/test_cat_workflow.py @@ -68,268 +68,269 @@ async def get_confirmed_balance(client: WalletRpcClient, wallet_id: int) -> int: return int((await client.get_wallet_balance(wallet_id))["confirmed_wallet_balance"]) -class TestCATWorkflow: - @pytest.mark.parametrize( - "org_uid, warehouse_project_id, vintage_year, amount, fee", - [ - ("Ivern", "Rootcaller", 2016, 60, 10), - ("Ivern", "Brushmaker", 2017, 30, 10), - ("Ivern", "Triggerseed", 2018, 50, 10), - ("Ivern", "Daisy!", 2019, 100, 10), - ], +@pytest.mark.parametrize( + "org_uid, warehouse_project_id, vintage_year, amount, fee", + [ + ("Ivern", "Rootcaller", 2016, 60, 10), + ("Ivern", "Brushmaker", 2017, 30, 10), + ("Ivern", "Triggerseed", 2018, 50, 10), + ("Ivern", "Daisy!", 2019, 100, 10), + ], +) +@pytest.mark.anyio +async def test_cat_tokenization_workflow( + self, + wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 + org_uid: str, + warehouse_project_id: str, + vintage_year: int, + amount: int, + fee: int, +) -> None: + env = wallet_rpc_environment + + wallet_node_1: WalletNode = env.wallet_1.node + wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client + + wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client + wallet_2: Wallet = env.wallet_2.wallet + + full_node_api: FullNodeSimulator = env.full_node.api + + # block: + # - registry: fund deposit + + await generate_funds(full_node_api, env.wallet_1) + + fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() + result = await wallet_client_1.get_private_key(fingerprint=fingerprint) + master_secret_key: PrivateKey = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) + root_secret_key: PrivateKey = master_sk_to_root_sk(master_secret_key) + + token_index = ClimateTokenIndex( + org_uid=org_uid, + warehouse_project_id=warehouse_project_id, + vintage_year=vintage_year, + ) + + # block: + # - registry: tokenization + + climate_wallet_1 = await ClimateWallet.create( + token_index=token_index, + root_secret_key=root_secret_key, + wallet_client=wallet_client_1, + ) + result = await climate_wallet_1.send_tokenization_transaction( + to_puzzle_hash=await wallet_2.get_new_puzzlehash(), + amount=amount, + fee=fee, + ) + transaction_records: List[TransactionRecord] = result["transaction_records"] + + await full_node_api.process_all_wallet_transactions( + wallet=wallet_node_1.wallet_state_manager.main_wallet, timeout=120 + ) + await check_transactions(wallet_client_1, 1, transaction_records) + + # block: + # - client: create CAT wallet + + result = await wallet_client_2.get_stray_cats() + asset_id = bytes32.fromhex(result[0]["asset_id"]) + result = await wallet_client_2.create_wallet_for_existing_cat(asset_id=asset_id) + assert result["success"] + cat_wallet_id: int = result["wallet_id"] + + await time_out_assert(60, get_confirmed_balance, amount, wallet_client_2, cat_wallet_id) + + +@pytest.mark.anyio +async def test_cat_detokenization_workflow( + self, + wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 + token_index: ClimateTokenIndex, + amount: int = 10, + fee: int = 10, +) -> None: + env: WalletRpcTestEnvironment = wallet_rpc_environment + + wallet_node_1: WalletNode = env.wallet_1.node + wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client + + wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client + wallet_2: Wallet = env.wallet_2.wallet + + full_node_api: FullNodeSimulator = env.full_node.api + + fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() + result = await wallet_client_1.get_private_key(fingerprint=fingerprint) + master_secret_key: PrivateKey = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) + root_secret_key: PrivateKey = master_sk_to_root_sk(master_secret_key) + + # block: initial fund deposits + + await generate_funds(full_node_api, env.wallet_1) + await generate_funds(full_node_api, env.wallet_2) + + # block: + # - registry: tokenization + # - client: create CAT wallet + + climate_wallet_1 = await ClimateWallet.create( + token_index=token_index, + root_secret_key=root_secret_key, + wallet_client=wallet_client_1, + ) + result = await climate_wallet_1.send_tokenization_transaction( + to_puzzle_hash=await wallet_2.get_new_puzzlehash(), + amount=amount, + fee=fee, + ) + # spend_bundle: SpendBundle = result["spend_bundle"] + transaction_records: List[TransactionRecord] = result["transaction_records"] + + result = await wallet_client_2.create_wallet_for_existing_cat( + asset_id=climate_wallet_1.tail_program.get_tree_hash() + ) + cat_wallet_id: int = result["wallet_id"] + + await full_node_api.process_all_wallet_transactions( + wallet=wallet_node_1.wallet_state_manager.main_wallet, timeout=120 + ) + await check_transactions(wallet_client_1, 1, transaction_records) + + # block: + # - client: create detokenization request + # - registry: check detokenization request + # - registry: sign detokenization request and push + + climate_wallet_2 = ClimateWallet( + token_index=token_index, + root_public_key=climate_wallet_1.root_public_key, + mode_to_public_key=climate_wallet_1.mode_to_public_key, + mode_to_message_and_signature=climate_wallet_1.mode_to_message_and_signature, + wallet_client=wallet_client_2, + constants=climate_wallet_1.constants, + ) + result = await climate_wallet_2.create_detokenization_request( + amount=amount, + fee=fee, + wallet_id=cat_wallet_id, + ) + content: str = result["content"] + transaction_records = result["transaction_records"] + + result = await ClimateWallet.parse_detokenization_request( + content=content, + ) + assert result["mode"] == GatewayMode.DETOKENIZATION + assert result["amount"] == amount + assert result["fee"] == fee + assert result["asset_id"] == climate_wallet_1.tail_program_hash + + result = await climate_wallet_1.sign_and_send_detokenization_request( + content=content, + ) + spend_bundle = result["spend_bundle"] + + await farm_transaction(full_node_api, wallet_node_1, spend_bundle) + await full_node_api.wait_for_wallet_synced(env.wallet_2.node, timeout=60) + await check_transactions(wallet_client_2, cat_wallet_id, transaction_records) + await time_out_assert(60, get_confirmed_balance, 0, wallet_client_2, cat_wallet_id) + + +@pytest.mark.anyio +async def test_cat_permissionless_retirement_workflow( + self, + wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 + token_index: ClimateTokenIndex, + amount: int = 10, + fee: int = 10, + beneficiary_name: bytes = "Ionia".encode(), +) -> None: + env: WalletRpcTestEnvironment = wallet_rpc_environment + + wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client + wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client + wallet_2: Wallet = env.wallet_2.wallet + + full_node_api: FullNodeSimulator = env.full_node.api + full_node_client = env.full_node.rpc_client + + fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() + result = await wallet_client_1.get_private_key(fingerprint=fingerprint) + master_secret_key = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) + root_secret_key = master_sk_to_root_sk(master_secret_key) + + # block: initial fund deposits + + await generate_funds(full_node_api, env.wallet_1) + await generate_funds(full_node_api, env.wallet_2) + + # block: + # - registry: tokenization + # - client: create CAT wallet + + climate_wallet_1 = await ClimateWallet.create( + token_index=token_index, + root_secret_key=root_secret_key, + wallet_client=wallet_client_1, + ) + result = await climate_wallet_1.send_tokenization_transaction( + to_puzzle_hash=await wallet_2.get_new_puzzlehash(), + amount=amount, + fee=fee, + ) + transaction_records = result["transaction_records"] + + result = await wallet_client_2.create_wallet_for_existing_cat( + asset_id=climate_wallet_1.tail_program.get_tree_hash() + ) + cat_wallet_id: int = result["wallet_id"] + + await full_node_api.process_all_wallet_transactions( + wallet=env.wallet_1.node.wallet_state_manager.main_wallet, timeout=120 + ) + await check_transactions(wallet_client_1, 1, transaction_records) + + # block: + # - client: create permissionless retirement transaction + + climate_wallet_2 = ClimateWallet( + token_index=token_index, + root_public_key=climate_wallet_1.root_public_key, + mode_to_public_key=climate_wallet_1.mode_to_public_key, + mode_to_message_and_signature=climate_wallet_1.mode_to_message_and_signature, + wallet_client=wallet_client_2, + constants=climate_wallet_1.constants, ) - @pytest.mark.anyio - async def test_cat_tokenization_workflow( - self, - wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 - org_uid: str, - warehouse_project_id: str, - vintage_year: int, - amount: int, - fee: int, - ) -> None: - env = wallet_rpc_environment - - wallet_node_1: WalletNode = env.wallet_1.node - wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client - - wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client - wallet_2: Wallet = env.wallet_2.wallet - - full_node_api: FullNodeSimulator = env.full_node.api - - # block: - # - registry: fund deposit - - await generate_funds(full_node_api, env.wallet_1) - - fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() - result = await wallet_client_1.get_private_key(fingerprint=fingerprint) - master_secret_key: PrivateKey = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) - root_secret_key: PrivateKey = master_sk_to_root_sk(master_secret_key) - - token_index = ClimateTokenIndex( - org_uid=org_uid, - warehouse_project_id=warehouse_project_id, - vintage_year=vintage_year, - ) - - # block: - # - registry: tokenization - - climate_wallet_1 = await ClimateWallet.create( - token_index=token_index, - root_secret_key=root_secret_key, - wallet_client=wallet_client_1, - ) - result = await climate_wallet_1.send_tokenization_transaction( - to_puzzle_hash=await wallet_2.get_new_puzzlehash(), - amount=amount, - fee=fee, - ) - transaction_records: List[TransactionRecord] = result["transaction_records"] - - await full_node_api.process_all_wallet_transactions( - wallet=wallet_node_1.wallet_state_manager.main_wallet, timeout=120 - ) - await check_transactions(wallet_client_1, 1, transaction_records) - - # block: - # - client: create CAT wallet - - result = await wallet_client_2.get_stray_cats() - asset_id = bytes32.fromhex(result[0]["asset_id"]) - result = await wallet_client_2.create_wallet_for_existing_cat(asset_id=asset_id) - assert result["success"] - cat_wallet_id: int = result["wallet_id"] - - await time_out_assert(60, get_confirmed_balance, amount, wallet_client_2, cat_wallet_id) - - @pytest.mark.anyio - async def test_cat_detokenization_workflow( - self, - wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 - token_index: ClimateTokenIndex, - amount: int = 10, - fee: int = 10, - ) -> None: - env: WalletRpcTestEnvironment = wallet_rpc_environment - - wallet_node_1: WalletNode = env.wallet_1.node - wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client - - wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client - wallet_2: Wallet = env.wallet_2.wallet - - full_node_api: FullNodeSimulator = env.full_node.api - - fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() - result = await wallet_client_1.get_private_key(fingerprint=fingerprint) - master_secret_key: PrivateKey = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) - root_secret_key: PrivateKey = master_sk_to_root_sk(master_secret_key) - - # block: initial fund deposits - - await generate_funds(full_node_api, env.wallet_1) - await generate_funds(full_node_api, env.wallet_2) - - # block: - # - registry: tokenization - # - client: create CAT wallet - - climate_wallet_1 = await ClimateWallet.create( - token_index=token_index, - root_secret_key=root_secret_key, - wallet_client=wallet_client_1, - ) - result = await climate_wallet_1.send_tokenization_transaction( - to_puzzle_hash=await wallet_2.get_new_puzzlehash(), - amount=amount, - fee=fee, - ) - # spend_bundle: SpendBundle = result["spend_bundle"] - transaction_records: List[TransactionRecord] = result["transaction_records"] - - result = await wallet_client_2.create_wallet_for_existing_cat( - asset_id=climate_wallet_1.tail_program.get_tree_hash() - ) - cat_wallet_id: int = result["wallet_id"] - - await full_node_api.process_all_wallet_transactions( - wallet=wallet_node_1.wallet_state_manager.main_wallet, timeout=120 - ) - await check_transactions(wallet_client_1, 1, transaction_records) - - # block: - # - client: create detokenization request - # - registry: check detokenization request - # - registry: sign detokenization request and push - - climate_wallet_2 = ClimateWallet( - token_index=token_index, - root_public_key=climate_wallet_1.root_public_key, - mode_to_public_key=climate_wallet_1.mode_to_public_key, - mode_to_message_and_signature=climate_wallet_1.mode_to_message_and_signature, - wallet_client=wallet_client_2, - constants=climate_wallet_1.constants, - ) - result = await climate_wallet_2.create_detokenization_request( - amount=amount, - fee=fee, - wallet_id=cat_wallet_id, - ) - content: str = result["content"] - transaction_records = result["transaction_records"] - - result = await ClimateWallet.parse_detokenization_request( - content=content, - ) - assert result["mode"] == GatewayMode.DETOKENIZATION - assert result["amount"] == amount - assert result["fee"] == fee - assert result["asset_id"] == climate_wallet_1.tail_program_hash - - result = await climate_wallet_1.sign_and_send_detokenization_request( - content=content, - ) - spend_bundle = result["spend_bundle"] - - await farm_transaction(full_node_api, wallet_node_1, spend_bundle) - await full_node_api.wait_for_wallet_synced(env.wallet_2.node, timeout=60) - await check_transactions(wallet_client_2, cat_wallet_id, transaction_records) - await time_out_assert(60, get_confirmed_balance, 0, wallet_client_2, cat_wallet_id) - - @pytest.mark.anyio - async def test_cat_permissionless_retirement_workflow( - self, - wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 - token_index: ClimateTokenIndex, - amount: int = 10, - fee: int = 10, - beneficiary_name: bytes = "Ionia".encode(), - ) -> None: - env: WalletRpcTestEnvironment = wallet_rpc_environment - - wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client - wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client - wallet_2: Wallet = env.wallet_2.wallet - - full_node_api: FullNodeSimulator = env.full_node.api - full_node_client = env.full_node.rpc_client - - fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() - result = await wallet_client_1.get_private_key(fingerprint=fingerprint) - master_secret_key = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) - root_secret_key = master_sk_to_root_sk(master_secret_key) - - # block: initial fund deposits - - await generate_funds(full_node_api, env.wallet_1) - await generate_funds(full_node_api, env.wallet_2) - - # block: - # - registry: tokenization - # - client: create CAT wallet - - climate_wallet_1 = await ClimateWallet.create( - token_index=token_index, - root_secret_key=root_secret_key, - wallet_client=wallet_client_1, - ) - result = await climate_wallet_1.send_tokenization_transaction( - to_puzzle_hash=await wallet_2.get_new_puzzlehash(), - amount=amount, - fee=fee, - ) - transaction_records = result["transaction_records"] - - result = await wallet_client_2.create_wallet_for_existing_cat( - asset_id=climate_wallet_1.tail_program.get_tree_hash() - ) - cat_wallet_id: int = result["wallet_id"] - - await full_node_api.process_all_wallet_transactions( - wallet=env.wallet_1.node.wallet_state_manager.main_wallet, timeout=120 - ) - await check_transactions(wallet_client_1, 1, transaction_records) - - # block: - # - client: create permissionless retirement transaction - - climate_wallet_2 = ClimateWallet( - token_index=token_index, - root_public_key=climate_wallet_1.root_public_key, - mode_to_public_key=climate_wallet_1.mode_to_public_key, - mode_to_message_and_signature=climate_wallet_1.mode_to_message_and_signature, - wallet_client=wallet_client_2, - constants=climate_wallet_1.constants, - ) - - test_address = "This is a fake address".encode() - result = await climate_wallet_2.send_permissionless_retirement_transaction( - amount=amount, - fee=fee, - beneficiary_name=beneficiary_name, - beneficiary_address=test_address, - wallet_id=cat_wallet_id, - ) - transaction_records = result["transaction_records"] - - await full_node_api.process_all_wallet_transactions( - wallet=env.wallet_2.node.wallet_state_manager.main_wallet, timeout=120 - ) - await time_out_assert(60, get_confirmed_balance, 0, wallet_client_2, cat_wallet_id) - - # block: - # - observer: observe retirement activity - - climate_observer = ClimateObserverWallet( - token_index=token_index, - root_public_key=climate_wallet_1.root_public_key, - full_node_client=full_node_client, - ) - activities = await climate_observer.get_activities(mode=GatewayMode.PERMISSIONLESS_RETIREMENT) - - assert activities[0]["metadata"]["bn"] == beneficiary_name.decode() - assert activities[0]["metadata"]["ba"] == test_address.decode() - assert activities[0]["metadata"]["bp"] == "0x" + + test_address = "This is a fake address".encode() + result = await climate_wallet_2.send_permissionless_retirement_transaction( + amount=amount, + fee=fee, + beneficiary_name=beneficiary_name, + beneficiary_address=test_address, + wallet_id=cat_wallet_id, + ) + transaction_records = result["transaction_records"] + + await full_node_api.process_all_wallet_transactions( + wallet=env.wallet_2.node.wallet_state_manager.main_wallet, timeout=120 + ) + await time_out_assert(60, get_confirmed_balance, 0, wallet_client_2, cat_wallet_id) + + # block: + # - observer: observe retirement activity + + climate_observer = ClimateObserverWallet( + token_index=token_index, + root_public_key=climate_wallet_1.root_public_key, + full_node_client=full_node_client, + ) + activities = await climate_observer.get_activities(mode=GatewayMode.PERMISSIONLESS_RETIREMENT) + + assert activities[0]["metadata"]["bn"] == beneficiary_name.decode() + assert activities[0]["metadata"]["ba"] == test_address.decode() + assert activities[0]["metadata"]["bp"] == "0x" From 314fd06e4424d81e8e5252e4d5119f523640bb79 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 4 Sep 2024 12:31:36 -0700 Subject: [PATCH 2/7] Port `test_cat_tokenization_workflow` --- app/core/climate_wallet/wallet.py | 5 +- tests/test_cat_workflow.py | 94 ++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/app/core/climate_wallet/wallet.py b/app/core/climate_wallet/wallet.py index 3e3da5b..9dbcfac 100644 --- a/app/core/climate_wallet/wallet.py +++ b/app/core/climate_wallet/wallet.py @@ -238,11 +238,12 @@ async def _create_transaction( gateway_spend_bundle, ] ) + additions = spend_bundle.additions() first_transaction_record = dataclasses.replace( first_transaction_record, spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), + additions=additions, + removals=[rem for rem in spend_bundle.removals() if rem not in additions], type=uint32(CLIMATE_WALLET_INDEX + mode.to_int()), name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), diff --git a/tests/test_cat_workflow.py b/tests/test_cat_workflow.py index 2d2a5fe..d4bbe70 100644 --- a/tests/test_cat_workflow.py +++ b/tests/test_cat_workflow.py @@ -6,10 +6,10 @@ from typing import List import pytest +from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia._tests.wallet.rpc.test_wallet_rpc import WalletRpcTestEnvironment, farm_transaction, generate_funds from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.simulator.full_node_simulator import FullNodeSimulator -from chia.types.blockchain_format.sized_bytes import bytes32 from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode @@ -77,30 +77,40 @@ async def get_confirmed_balance(client: WalletRpcClient, wallet_id: int) -> int: ("Ivern", "Daisy!", 2019, 100, 10), ], ) +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 2, + "blocks_needed": [1, 1], + "config_overrides": {"automatically_add_unknown_cats": True}, + } + ], + indirect=True, +) @pytest.mark.anyio async def test_cat_tokenization_workflow( - self, - wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 + wallet_environments: WalletTestFramework, org_uid: str, warehouse_project_id: str, vintage_year: int, amount: int, fee: int, ) -> None: - env = wallet_rpc_environment + env_1 = wallet_environments.environments[0] + env_2 = wallet_environments.environments[1] - wallet_node_1: WalletNode = env.wallet_1.node - wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client + env_1.wallet_aliases = { + "xch": 1, + "cat": 2, + } + env_2.wallet_aliases = { + "xch": 1, + "cat": 2, + } - wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client - wallet_2: Wallet = env.wallet_2.wallet - - full_node_api: FullNodeSimulator = env.full_node.api - - # block: - # - registry: fund deposit - - await generate_funds(full_node_api, env.wallet_1) + wallet_client_1: WalletRpcClient = env_1.rpc_client + wallet_2: Wallet = env_2.xch_wallet fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() result = await wallet_client_1.get_private_key(fingerprint=fingerprint) @@ -113,9 +123,6 @@ async def test_cat_tokenization_workflow( vintage_year=vintage_year, ) - # block: - # - registry: tokenization - climate_wallet_1 = await ClimateWallet.create( token_index=token_index, root_secret_key=root_secret_key, @@ -126,23 +133,44 @@ async def test_cat_tokenization_workflow( amount=amount, fee=fee, ) - transaction_records: List[TransactionRecord] = result["transaction_records"] - await full_node_api.process_all_wallet_transactions( - wallet=wallet_node_1.wallet_state_manager.main_wallet, timeout=120 + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -amount - fee, + "<=#spendable_balance": -amount - fee, + ">=#pending_change": 1, # any amount increase + "<=#max_send_amount": -amount - fee, + "pending_coin_removal_count": 1, + } + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -amount - fee, + ">=#spendable_balance": 1, + "<=#pending_change": -1, # any amount increase + ">=#max_send_amount": 1, + "pending_coin_removal_count": -1, + } + }, + ), + WalletStateTransition( + pre_block_balance_updates={}, + post_block_balance_updates={ + "cat": { + "init": True, + "confirmed_wallet_balance": amount, + "unconfirmed_wallet_balance": amount, + "spendable_balance": amount, + "max_send_amount": amount, + "unspent_coin_count": 1, + } + }, + ), + ] ) - await check_transactions(wallet_client_1, 1, transaction_records) - - # block: - # - client: create CAT wallet - - result = await wallet_client_2.get_stray_cats() - asset_id = bytes32.fromhex(result[0]["asset_id"]) - result = await wallet_client_2.create_wallet_for_existing_cat(asset_id=asset_id) - assert result["success"] - cat_wallet_id: int = result["wallet_id"] - - await time_out_assert(60, get_confirmed_balance, amount, wallet_client_2, cat_wallet_id) @pytest.mark.anyio From cb93f6c1f5250da866994ba7eda6fc72957fed08 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 4 Sep 2024 14:51:29 -0700 Subject: [PATCH 3/7] Port `test_cat_detokenization_workflow` --- app/core/climate_wallet/wallet.py | 55 ++++++------ tests/test_cat_workflow.py | 145 +++++++++++++++++++++++------- 2 files changed, 143 insertions(+), 57 deletions(-) diff --git a/app/core/climate_wallet/wallet.py b/app/core/climate_wallet/wallet.py index 9dbcfac..90b883d 100644 --- a/app/core/climate_wallet/wallet.py +++ b/app/core/climate_wallet/wallet.py @@ -229,31 +229,35 @@ async def _create_transaction( wallet_id=wallet_id, wallet_client=self.wallet_client, ) - (first_transaction_record, *rest_transaction_records) = response.transactions - if first_transaction_record.spend_bundle is None: - raise ValueError("No spend bundle created!") - spend_bundle = SpendBundle.aggregate( - [ - first_transaction_record.spend_bundle, - gateway_spend_bundle, - ] - ) - additions = spend_bundle.additions() - first_transaction_record = dataclasses.replace( - first_transaction_record, - spend_bundle=spend_bundle, - additions=additions, - removals=[rem for rem in spend_bundle.removals() if rem not in additions], - type=uint32(CLIMATE_WALLET_INDEX + mode.to_int()), - name=spend_bundle.name(), - memos=list(compute_memos(spend_bundle).items()), - ) - transaction_records = [first_transaction_record, *rest_transaction_records] + new_txs = [] + for tx in response.transactions: + if unsigned_gateway_coin_spend.coin in tx.additions: + spend_bundle = SpendBundle.aggregate( + [gateway_spend_bundle, *([] if tx.spend_bundle is None else [tx.spend_bundle])] + ) + additions = [ + *(add for add in tx.additions if add != unsigned_gateway_coin_spend.coin), + *gateway_spend_bundle.additions(), + ] + else: + spend_bundle = tx.spend_bundle + additions = tx.additions + removals = [rem for rem in tx.removals if rem not in additions] + new_tx = dataclasses.replace( + tx, + spend_bundle=spend_bundle, + additions=additions, + removals=removals, + type=uint32(CLIMATE_WALLET_INDEX + mode.to_int()), + name=spend_bundle.name(), + memos=list(compute_memos(spend_bundle).items()), + ) + new_txs.append(new_tx) return { - "transaction_id": first_transaction_record.name, - "transaction_records": transaction_records, - "spend_bundle": spend_bundle, + "transaction_id": new_txs[0].name, + "transaction_records": new_txs, + "spend_bundle": SpendBundle.aggregate([tx.spend_bundle for tx in new_txs if tx.spend_bundle is not None]), } async def _create_client_transaction( @@ -559,6 +563,7 @@ async def sign_and_send_detokenization_request( if gateway_coin_spend is None: raise ValueError("Invalid detokenization request: Could not find gateway coin spend!") + additions = spend_bundle.additions() transaction_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -568,8 +573,8 @@ async def sign_and_send_detokenization_request( confirmed=False, sent=uint32(0), spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), + additions=additions, + removals=[rem for rem in spend_bundle.removals() if rem not in additions], wallet_id=uint32(wallet_id), sent_to=[], trade_id=None, diff --git a/tests/test_cat_workflow.py b/tests/test_cat_workflow.py index d4bbe70..b643fd1 100644 --- a/tests/test_cat_workflow.py +++ b/tests/test_cat_workflow.py @@ -7,12 +7,11 @@ import pytest from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework -from chia._tests.wallet.rpc.test_wallet_rpc import WalletRpcTestEnvironment, farm_transaction, generate_funds +from chia._tests.wallet.rpc.test_wallet_rpc import WalletRpcTestEnvironment, generate_funds from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.simulator.full_node_simulator import FullNodeSimulator from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet import Wallet -from chia.wallet.wallet_node import WalletNode from chia_rs import PrivateKey from app.core.climate_wallet.wallet import ClimateObserverWallet, ClimateWallet @@ -173,34 +172,46 @@ async def test_cat_tokenization_workflow( ) +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 2, + "blocks_needed": [1, 1], + "config_overrides": {"automatically_add_unknown_cats": True}, + } + ], + indirect=True, +) @pytest.mark.anyio async def test_cat_detokenization_workflow( - self, - wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 + wallet_environments: WalletTestFramework, token_index: ClimateTokenIndex, amount: int = 10, fee: int = 10, ) -> None: - env: WalletRpcTestEnvironment = wallet_rpc_environment + env_1 = wallet_environments.environments[0] + env_2 = wallet_environments.environments[1] - wallet_node_1: WalletNode = env.wallet_1.node - wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client + env_1.wallet_aliases = { + "xch": 1, + "cat": 2, + } + env_2.wallet_aliases = { + "xch": 1, + "cat": 2, + } - wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client - wallet_2: Wallet = env.wallet_2.wallet + wallet_client_1: WalletRpcClient = env_1.rpc_client - full_node_api: FullNodeSimulator = env.full_node.api + wallet_client_2: WalletRpcClient = env_2.rpc_client + wallet_2: Wallet = env_2.xch_wallet fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() result = await wallet_client_1.get_private_key(fingerprint=fingerprint) master_secret_key: PrivateKey = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) root_secret_key: PrivateKey = master_sk_to_root_sk(master_secret_key) - # block: initial fund deposits - - await generate_funds(full_node_api, env.wallet_1) - await generate_funds(full_node_api, env.wallet_2) - # block: # - registry: tokenization # - client: create CAT wallet @@ -215,18 +226,44 @@ async def test_cat_detokenization_workflow( amount=amount, fee=fee, ) - # spend_bundle: SpendBundle = result["spend_bundle"] - transaction_records: List[TransactionRecord] = result["transaction_records"] - result = await wallet_client_2.create_wallet_for_existing_cat( - asset_id=climate_wallet_1.tail_program.get_tree_hash() - ) - cat_wallet_id: int = result["wallet_id"] - - await full_node_api.process_all_wallet_transactions( - wallet=wallet_node_1.wallet_state_manager.main_wallet, timeout=120 + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -amount - fee, + "<=#spendable_balance": -amount - fee, + ">=#pending_change": 1, # any amount increase + "<=#max_send_amount": -amount - fee, + "pending_coin_removal_count": 1, + } + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -amount - fee, + ">=#spendable_balance": 1, + "<=#pending_change": -1, # any amount increase + ">=#max_send_amount": 1, + "pending_coin_removal_count": -1, + } + }, + ), + WalletStateTransition( + pre_block_balance_updates={}, + post_block_balance_updates={ + "cat": { + "init": True, + "confirmed_wallet_balance": amount, + "unconfirmed_wallet_balance": amount, + "spendable_balance": amount, + "max_send_amount": amount, + "unspent_coin_count": 1, + } + }, + ), + ] ) - await check_transactions(wallet_client_1, 1, transaction_records) # block: # - client: create detokenization request @@ -244,10 +281,9 @@ async def test_cat_detokenization_workflow( result = await climate_wallet_2.create_detokenization_request( amount=amount, fee=fee, - wallet_id=cat_wallet_id, + wallet_id=env_2.wallet_aliases["cat"], ) content: str = result["content"] - transaction_records = result["transaction_records"] result = await ClimateWallet.parse_detokenization_request( content=content, @@ -260,12 +296,57 @@ async def test_cat_detokenization_workflow( result = await climate_wallet_1.sign_and_send_detokenization_request( content=content, ) - spend_bundle = result["spend_bundle"] - await farm_transaction(full_node_api, wallet_node_1, spend_bundle) - await full_node_api.wait_for_wallet_synced(env.wallet_2.node, timeout=60) - await check_transactions(wallet_client_2, cat_wallet_id, transaction_records) - await time_out_assert(60, get_confirmed_balance, 0, wallet_client_2, cat_wallet_id) + # TODO: this will fail without this: + # https://github.com/Chia-Network/chia-blockchain/blob/long_lived/vault/chia/wallet/wallet_state_manager.py#L1829-L1840 + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + # Should probably review whether or not this is intentional/desired + "pending_coin_removal_count": 2, + } + }, + post_block_balance_updates={ + "xch": { + "pending_coin_removal_count": -2, + } + }, + ), + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -fee, + "<=#spendable_balance": -fee, + ">=#pending_change": 1, # any amount increase + "<=#max_send_amount": -fee, + "pending_coin_removal_count": 1, + }, + "cat": { + "unconfirmed_wallet_balance": -amount, + "spendable_balance": -amount, + "max_send_amount": -amount, + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -fee, + ">=#spendable_balance": 1, + "<=#pending_change": -1, # any amount increase + ">=#max_send_amount": 1, + "pending_coin_removal_count": -1, + }, + "cat": { + "confirmed_wallet_balance": -amount, + "unspent_coin_count": -1, + "pending_coin_removal_count": -1, + }, + }, + ), + ] + ) @pytest.mark.anyio From 554dec06a6599e7b441c68468b2d9ef4c44dc75e Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 5 Sep 2024 08:54:45 -0700 Subject: [PATCH 4/7] Port `test_cat_permissionless_retirement_workflow` --- tests/test_cat_workflow.py | 211 ++++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 99 deletions(-) diff --git a/tests/test_cat_workflow.py b/tests/test_cat_workflow.py index b643fd1..b9b61e6 100644 --- a/tests/test_cat_workflow.py +++ b/tests/test_cat_workflow.py @@ -1,16 +1,9 @@ from __future__ import annotations -import asyncio -import logging -import time -from typing import List - import pytest from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework -from chia._tests.wallet.rpc.test_wallet_rpc import WalletRpcTestEnvironment, generate_funds +from chia.rpc.full_node_rpc_client import FullNodeRpcClient from chia.rpc.wallet_rpc_client import WalletRpcClient -from chia.simulator.full_node_simulator import FullNodeSimulator -from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet import Wallet from chia_rs import PrivateKey @@ -18,54 +11,6 @@ from app.core.derive_keys import master_sk_to_root_sk from app.core.types import ClimateTokenIndex, GatewayMode -logger = logging.getLogger(__name__) - - -async def time_out_assert_custom_interval( - timeout: float, interval, function, value=True, *args, **kwargs # type: ignore[no-untyped-def] -): - __tracebackhide__ = True - - start = time.time() - while time.time() - start < timeout: - if asyncio.iscoroutinefunction(function): - f_res = await function(*args, **kwargs) - else: - f_res = function(*args, **kwargs) - if value == f_res: - return None - await asyncio.sleep(interval) - assert False, f"Timed assertion timed out after {timeout} seconds: expected {value!r}, got {f_res!r}" - - -async def time_out_assert(timeout: int, function, value=True, *args, **kwargs): # type: ignore[no-untyped-def] - __tracebackhide__ = True - await time_out_assert_custom_interval(timeout, 0.05, function, value, *args, **kwargs) - - -async def check_transactions( - wallet_client: WalletRpcClient, - wallet_id: int, - transaction_records: List[TransactionRecord], -) -> None: - for transaction_record in transaction_records: - tx = await wallet_client.get_transaction(transaction_id=transaction_record.name) - - assert tx.confirmed_at_height != 0, f"Transaction {transaction_record.name.hex()} not found!" - - -async def check_balance( - wallet_client: WalletRpcClient, - wallet_id: int, - amount: int, -) -> None: - result = await wallet_client.get_wallet_balance(wallet_id=wallet_id) - assert result["confirmed_wallet_balance"] == amount, "Target wallet CAT amount does not match!" - - -async def get_confirmed_balance(client: WalletRpcClient, wallet_id: int) -> int: - return int((await client.get_wallet_balance(wallet_id))["confirmed_wallet_balance"]) - @pytest.mark.parametrize( "org_uid, warehouse_project_id, vintage_year, amount, fee", @@ -349,38 +294,48 @@ async def test_cat_detokenization_workflow( ) +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 2, + "blocks_needed": [1, 1], + "config_overrides": {"automatically_add_unknown_cats": True}, + } + ], + indirect=True, +) @pytest.mark.anyio async def test_cat_permissionless_retirement_workflow( - self, - wallet_rpc_environment: WalletRpcTestEnvironment, # noqa: F811 + self_hostname: str, + wallet_environments: WalletTestFramework, token_index: ClimateTokenIndex, amount: int = 10, fee: int = 10, beneficiary_name: bytes = "Ionia".encode(), ) -> None: - env: WalletRpcTestEnvironment = wallet_rpc_environment + env_1 = wallet_environments.environments[0] + env_2 = wallet_environments.environments[1] - wallet_client_1: WalletRpcClient = env.wallet_1.rpc_client - wallet_client_2: WalletRpcClient = env.wallet_2.rpc_client - wallet_2: Wallet = env.wallet_2.wallet + env_1.wallet_aliases = { + "xch": 1, + "cat": 2, + } + env_2.wallet_aliases = { + "xch": 1, + "cat": 2, + } + + wallet_client_1: WalletRpcClient = env_1.rpc_client - full_node_api: FullNodeSimulator = env.full_node.api - full_node_client = env.full_node.rpc_client + wallet_client_2: WalletRpcClient = env_2.rpc_client + wallet_2: Wallet = env_2.xch_wallet fingerprint: int = await wallet_client_1.get_logged_in_fingerprint() result = await wallet_client_1.get_private_key(fingerprint=fingerprint) master_secret_key = PrivateKey.from_bytes(bytes.fromhex(result["sk"])) root_secret_key = master_sk_to_root_sk(master_secret_key) - # block: initial fund deposits - - await generate_funds(full_node_api, env.wallet_1) - await generate_funds(full_node_api, env.wallet_2) - - # block: - # - registry: tokenization - # - client: create CAT wallet - climate_wallet_1 = await ClimateWallet.create( token_index=token_index, root_secret_key=root_secret_key, @@ -391,20 +346,44 @@ async def test_cat_permissionless_retirement_workflow( amount=amount, fee=fee, ) - transaction_records = result["transaction_records"] - result = await wallet_client_2.create_wallet_for_existing_cat( - asset_id=climate_wallet_1.tail_program.get_tree_hash() - ) - cat_wallet_id: int = result["wallet_id"] - - await full_node_api.process_all_wallet_transactions( - wallet=env.wallet_1.node.wallet_state_manager.main_wallet, timeout=120 + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -amount - fee, + "<=#spendable_balance": -amount - fee, + ">=#pending_change": 1, # any amount increase + "<=#max_send_amount": -amount - fee, + "pending_coin_removal_count": 1, + } + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -amount - fee, + ">=#spendable_balance": 1, + "<=#pending_change": -1, # any amount increase + ">=#max_send_amount": 1, + "pending_coin_removal_count": -1, + } + }, + ), + WalletStateTransition( + pre_block_balance_updates={}, + post_block_balance_updates={ + "cat": { + "init": True, + "confirmed_wallet_balance": amount, + "unconfirmed_wallet_balance": amount, + "spendable_balance": amount, + "max_send_amount": amount, + "unspent_coin_count": 1, + } + }, + ), + ] ) - await check_transactions(wallet_client_1, 1, transaction_records) - - # block: - # - client: create permissionless retirement transaction climate_wallet_2 = ClimateWallet( token_index=token_index, @@ -421,24 +400,58 @@ async def test_cat_permissionless_retirement_workflow( fee=fee, beneficiary_name=beneficiary_name, beneficiary_address=test_address, - wallet_id=cat_wallet_id, + wallet_id=env_2.wallet_aliases["cat"], ) - transaction_records = result["transaction_records"] - await full_node_api.process_all_wallet_transactions( - wallet=env.wallet_2.node.wallet_state_manager.main_wallet, timeout=120 + await wallet_environments.process_pending_states( + [ + WalletStateTransition(), + WalletStateTransition( + pre_block_balance_updates={ + "xch": { + "unconfirmed_wallet_balance": -fee, + "<=#spendable_balance": -fee, + ">=#pending_change": 1, # any amount increase + "<=#max_send_amount": -fee, + "pending_coin_removal_count": 1, + }, + "cat": { + "unconfirmed_wallet_balance": -amount, + "spendable_balance": -amount, + "max_send_amount": -amount, + "pending_coin_removal_count": 1, + }, + }, + post_block_balance_updates={ + "xch": { + "confirmed_wallet_balance": -fee, + ">=#spendable_balance": 1, + "<=#pending_change": -1, # any amount increase + ">=#max_send_amount": 1, + "pending_coin_removal_count": -1, + }, + "cat": { + "confirmed_wallet_balance": -amount, + "unspent_coin_count": -1, + "pending_coin_removal_count": -1, + }, + }, + ), + ] ) - await time_out_assert(60, get_confirmed_balance, 0, wallet_client_2, cat_wallet_id) - - # block: - # - observer: observe retirement activity - climate_observer = ClimateObserverWallet( - token_index=token_index, - root_public_key=climate_wallet_1.root_public_key, - full_node_client=full_node_client, - ) - activities = await climate_observer.get_activities(mode=GatewayMode.PERMISSIONLESS_RETIREMENT) + async with FullNodeRpcClient.create_as_context( + self_hostname, + wallet_environments.full_node.full_node.state_changed_callback.__self__.listen_port, # a hack + wallet_environments.full_node.full_node.root_path, + wallet_environments.full_node.config, + ) as client_node: + climate_observer = ClimateObserverWallet( + token_index=token_index, + root_public_key=climate_wallet_1.root_public_key, + full_node_client=client_node, + ) + activities = await climate_observer.get_activities(mode=GatewayMode.PERMISSIONLESS_RETIREMENT) assert activities[0]["metadata"]["bn"] == beneficiary_name.decode() assert activities[0]["metadata"]["ba"] == test_address.decode() From bda687ada954a6eacb0d634ef349efc31e85a68f Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 23 Oct 2024 14:36:19 -0700 Subject: [PATCH 5/7] Use new full node rpc client --- tests/test_cat_workflow.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/test_cat_workflow.py b/tests/test_cat_workflow.py index f9e1822..ec9d526 100644 --- a/tests/test_cat_workflow.py +++ b/tests/test_cat_workflow.py @@ -2,7 +2,6 @@ import pytest from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework -from chia.rpc.full_node_rpc_client import FullNodeRpcClient from chia.rpc.wallet_request_types import GetPrivateKey from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.wallet.wallet import Wallet @@ -435,18 +434,12 @@ async def test_cat_permissionless_retirement_workflow( ] ) - async with FullNodeRpcClient.create_as_context( - self_hostname, - wallet_environments.full_node.full_node.state_changed_callback.__self__.listen_port, # a hack - wallet_environments.full_node.full_node.root_path, - wallet_environments.full_node.config, - ) as client_node: - climate_observer = ClimateObserverWallet( - token_index=token_index, - root_public_key=climate_wallet_1.root_public_key, - full_node_client=client_node, - ) - activities = await climate_observer.get_activities(mode=GatewayMode.PERMISSIONLESS_RETIREMENT) + climate_observer = ClimateObserverWallet( + token_index=token_index, + root_public_key=climate_wallet_1.root_public_key, + full_node_client=wallet_environments.full_node_rpc_client, + ) + activities = await climate_observer.get_activities(mode=GatewayMode.PERMISSIONLESS_RETIREMENT) assert activities[0]["metadata"]["bn"] == beneficiary_name.decode() assert activities[0]["metadata"]["ba"] == test_address.decode() From d732e78e6792a98c03d0f476d999d974603772ce Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 23 Oct 2024 14:57:23 -0700 Subject: [PATCH 6/7] Update app/core/climate_wallet/wallet.py Co-authored-by: Amine Khaldi --- app/core/climate_wallet/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/climate_wallet/wallet.py b/app/core/climate_wallet/wallet.py index 90b883d..b5286f3 100644 --- a/app/core/climate_wallet/wallet.py +++ b/app/core/climate_wallet/wallet.py @@ -233,7 +233,7 @@ async def _create_transaction( for tx in response.transactions: if unsigned_gateway_coin_spend.coin in tx.additions: spend_bundle = SpendBundle.aggregate( - [gateway_spend_bundle, *([] if tx.spend_bundle is None else [tx.spend_bundle])] + [gateway_spend_bundle] + ([] if tx.spend_bundle is None else [tx.spend_bundle]) ) additions = [ *(add for add in tx.additions if add != unsigned_gateway_coin_spend.coin), From 039ad288e53545e3d1ba64d5c6469381e607db5b Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 23 Oct 2024 14:57:35 -0700 Subject: [PATCH 7/7] Update app/core/climate_wallet/wallet.py Co-authored-by: Amine Khaldi --- app/core/climate_wallet/wallet.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/core/climate_wallet/wallet.py b/app/core/climate_wallet/wallet.py index b5286f3..678e3bb 100644 --- a/app/core/climate_wallet/wallet.py +++ b/app/core/climate_wallet/wallet.py @@ -236,9 +236,8 @@ async def _create_transaction( [gateway_spend_bundle] + ([] if tx.spend_bundle is None else [tx.spend_bundle]) ) additions = [ - *(add for add in tx.additions if add != unsigned_gateway_coin_spend.coin), - *gateway_spend_bundle.additions(), - ] + add for add in tx.additions if add != unsigned_gateway_coin_spend.coin + ] + gateway_spend_bundle.additions() else: spend_bundle = tx.spend_bundle additions = tx.additions