From 2c2b0f4e1ab182e26246bf8ea7d19638c353c51e Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 29 Jul 2024 11:24:06 -0700 Subject: [PATCH] [CHIA-1032] Add a config slot in action scopes and use it for wallets (#18365) * Add a config slot in action scopes and use it for wallets * Simplify tx_endpoint a bit * pylint --- chia/_tests/util/test_action_scope.py | 27 +++++++++++++++++---------- chia/rpc/util.py | 3 --- chia/rpc/wallet_rpc_api.py | 15 ++++++--------- chia/util/action_scope.py | 15 ++++++++++++--- chia/wallet/wallet_action_scope.py | 18 +++++++++++++++--- 5 files changed, 50 insertions(+), 28 deletions(-) diff --git a/chia/_tests/util/test_action_scope.py b/chia/_tests/util/test_action_scope.py index 56ee3c1ec2..5c2e89b2ff 100644 --- a/chia/_tests/util/test_action_scope.py +++ b/chia/_tests/util/test_action_scope.py @@ -21,6 +21,12 @@ class TestSideEffects: return cls(blob) +@final +@dataclass +class TestConfig: + test_foo: str = "test_foo" + + async def default_async_callback(interface: StateInterface[TestSideEffects]) -> None: return None # pragma: no cover @@ -36,13 +42,14 @@ def test_set_callback() -> None: @pytest.fixture(name="action_scope") -async def action_scope_fixture() -> AsyncIterator[ActionScope[TestSideEffects]]: - async with ActionScope.new_scope(TestSideEffects) as scope: +async def action_scope_fixture() -> AsyncIterator[ActionScope[TestSideEffects, TestConfig]]: + async with ActionScope.new_scope(TestSideEffects, TestConfig()) as scope: + assert scope.config == TestConfig(test_foo="test_foo") yield scope @pytest.mark.anyio -async def test_new_action_scope(action_scope: ActionScope[TestSideEffects]) -> None: +async def test_new_action_scope(action_scope: ActionScope[TestSideEffects, TestConfig]) -> None: """ Assert we can immediately check out some initial state """ @@ -51,7 +58,7 @@ async def test_new_action_scope(action_scope: ActionScope[TestSideEffects]) -> N @pytest.mark.anyio -async def test_scope_persistence(action_scope: ActionScope[TestSideEffects]) -> None: +async def test_scope_persistence(action_scope: ActionScope[TestSideEffects, TestConfig]) -> None: async with action_scope.use() as interface: interface.side_effects.buf = b"baz" @@ -60,7 +67,7 @@ async def test_scope_persistence(action_scope: ActionScope[TestSideEffects]) -> @pytest.mark.anyio -async def test_transactionality(action_scope: ActionScope[TestSideEffects]) -> None: +async def test_transactionality(action_scope: ActionScope[TestSideEffects, TestConfig]) -> None: async with action_scope.use() as interface: interface.side_effects.buf = b"baz" @@ -75,7 +82,7 @@ async def test_transactionality(action_scope: ActionScope[TestSideEffects]) -> N @pytest.mark.anyio async def test_callbacks() -> None: - async with ActionScope.new_scope(TestSideEffects) as action_scope: + async with ActionScope.new_scope(TestSideEffects, TestConfig()) as action_scope: async with action_scope.use() as interface: async def callback(interface: StateInterface[TestSideEffects]) -> None: @@ -89,7 +96,7 @@ async def test_callbacks() -> None: @pytest.mark.anyio async def test_callback_in_callback_error() -> None: with pytest.raises(RuntimeError, match="Callback"): - async with ActionScope.new_scope(TestSideEffects) as action_scope: + async with ActionScope.new_scope(TestSideEffects, TestConfig()) as action_scope: async with action_scope.use() as interface: async def callback(interface: StateInterface[TestSideEffects]) -> None: @@ -101,7 +108,7 @@ async def test_callback_in_callback_error() -> None: @pytest.mark.anyio async def test_no_callbacks_if_error() -> None: with pytest.raises(Exception, match="This should prevent the callbacks from being called"): - async with ActionScope.new_scope(TestSideEffects) as action_scope: + async with ActionScope.new_scope(TestSideEffects, TestConfig()) as action_scope: async with action_scope.use() as interface: async def callback(interface: StateInterface[TestSideEffects]) -> None: @@ -113,7 +120,7 @@ async def test_no_callbacks_if_error() -> None: raise RuntimeError("This should prevent the callbacks from being called") with pytest.raises(Exception, match="This should prevent the callbacks from being called"): - async with ActionScope.new_scope(TestSideEffects) as action_scope: + async with ActionScope.new_scope(TestSideEffects, TestConfig()) as action_scope: async with action_scope.use() as interface: async def callback2(interface: StateInterface[TestSideEffects]) -> None: @@ -126,7 +133,7 @@ async def test_no_callbacks_if_error() -> None: # TODO: add suport, change this test to test it and add a test for nested transactionality @pytest.mark.anyio -async def test_nested_use_banned(action_scope: ActionScope[TestSideEffects]) -> None: +async def test_nested_use_banned(action_scope: ActionScope[TestSideEffects, TestConfig]) -> None: async with action_scope.use(): with pytest.raises(RuntimeError, match="cannot currently support nested transactions"): async with action_scope.use(): diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 924a12bc83..5d545a20df 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -102,8 +102,6 @@ def wrap_http_handler(f) -> Callable: def tx_endpoint( push: bool = False, merge_spends: bool = True, - # The purpose of this is in case endpoints need to raise based on certain non default values - requires_default_information: bool = False, ) -> Callable[[RpcEndpoint], RpcEndpoint]: def _inner(func: RpcEndpoint) -> RpcEndpoint: async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]: @@ -162,7 +160,6 @@ def tx_endpoint( request, *args, action_scope, - *([push] if requires_default_information else []), tx_config=tx_config, extra_conditions=extra_conditions, **kwargs, diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index b03f37b996..1334030d40 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -694,12 +694,11 @@ class WalletRpcApi: response["fingerprint"] = self.service.logged_in_fingerprint return response - @tx_endpoint(push=True, requires_default_information=True) + @tx_endpoint(push=True) async def create_new_wallet( self, request: Dict[str, Any], action_scope: WalletActionScope, - push: bool = True, tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), ) -> EndpointResult: @@ -715,7 +714,7 @@ class WalletRpcApi: name = request.get("name", None) if request["mode"] == "new": if request.get("test", False): - if not push: + if not action_scope.config.push: raise ValueError("Test CAT minting must be pushed automatically") # pragma: no cover async with self.service.wallet_state_manager.lock: cat_wallet = await CATWallet.create_new_cat_wallet( @@ -1825,16 +1824,15 @@ class WalletRpcApi: else: return {"wallet_id": wallet.id(), "name": (wallet.get_name())} - @tx_endpoint(push=False, requires_default_information=True) + @tx_endpoint(push=False) async def create_offer_for_ids( self, request: Dict[str, Any], action_scope: WalletActionScope, - push: bool = False, tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), ) -> EndpointResult: - if push: + if action_scope.config.push: raise ValueError("Cannot push an incomplete spend") # pragma: no cover offer: Dict[str, int] = request["offer"] @@ -3617,16 +3615,15 @@ class WalletRpcApi: {asset["asset"]: uint64(asset["amount"]) for asset in request.get("fungible_assets", [])}, ) - @tx_endpoint(push=False, requires_default_information=True) + @tx_endpoint(push=False) async def nft_mint_bulk( self, request: Dict[str, Any], action_scope: WalletActionScope, - push: bool = False, tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), ) -> EndpointResult: - if push: + if action_scope.config.push: raise ValueError("Automatic pushing of nft minting transactions not yet available") # pragma: no cover if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced.") diff --git a/chia/util/action_scope.py b/chia/util/action_scope.py index 370af52443..03b97cc96a 100644 --- a/chia/util/action_scope.py +++ b/chia/util/action_scope.py @@ -84,11 +84,12 @@ class SideEffects(Protocol): _T_SideEffects = TypeVar("_T_SideEffects", bound=SideEffects) +_T_Config = TypeVar("_T_Config") @final @dataclass -class ActionScope(Generic[_T_SideEffects]): +class ActionScope(Generic[_T_SideEffects, _T_Config]): """ The idea of an "action" is to map a single client input to many potentially distributed functions and side effects. The action holds on to a temporary state that the many callers modify at will but only one at a time. @@ -100,6 +101,7 @@ class ActionScope(Generic[_T_SideEffects]): _resource_manager: ResourceManager _side_effects_format: Type[_T_SideEffects] + _config: _T_Config # An object not intended to be mutated during the lifetime of the scope _callback: Optional[Callable[[StateInterface[_T_SideEffects]], Awaitable[None]]] = None _final_side_effects: Optional[_T_SideEffects] = field(init=False, default=None) @@ -113,15 +115,22 @@ class ActionScope(Generic[_T_SideEffects]): return self._final_side_effects + @property + def config(self) -> _T_Config: + return self._config + @classmethod @contextlib.asynccontextmanager async def new_scope( cls, side_effects_format: Type[_T_SideEffects], + # I want a default here in case a use case doesn't want to take advantage of the config but no default seems to + # satisfy the type hint _T_Config so we'll just ignore this. + config: _T_Config = object(), # type: ignore[assignment] resource_manager_backend: Type[ResourceManager] = SQLiteResourceManager, - ) -> AsyncIterator[ActionScope[_T_SideEffects]]: + ) -> AsyncIterator[ActionScope[_T_SideEffects, _T_Config]]: async with resource_manager_backend.managed(side_effects_format()) as resource_manager: - self = cls(_resource_manager=resource_manager, _side_effects_format=side_effects_format) + self = cls(_resource_manager=resource_manager, _side_effects_format=side_effects_format, _config=config) yield self diff --git a/chia/wallet/wallet_action_scope.py b/chia/wallet/wallet_action_scope.py index 85f4cb759b..acb20cf287 100644 --- a/chia/wallet/wallet_action_scope.py +++ b/chia/wallet/wallet_action_scope.py @@ -2,7 +2,7 @@ from __future__ import annotations import contextlib from dataclasses import dataclass, field -from typing import TYPE_CHECKING, AsyncIterator, List, Optional, cast +from typing import TYPE_CHECKING, AsyncIterator, List, Optional, cast, final from chia.types.spend_bundle import SpendBundle from chia.util.action_scope import ActionScope @@ -65,7 +65,17 @@ class WalletSideEffects: return instance -WalletActionScope = ActionScope[WalletSideEffects] +@final +@dataclass(frozen=True) +class WalletActionConfig: + push: bool + merge_spends: bool + sign: Optional[bool] + additional_signing_responses: List[SigningResponse] + extra_spends: List[SpendBundle] + + +WalletActionScope = ActionScope[WalletSideEffects, WalletActionConfig] @contextlib.asynccontextmanager @@ -77,7 +87,9 @@ async def new_wallet_action_scope( additional_signing_responses: List[SigningResponse] = [], extra_spends: List[SpendBundle] = [], ) -> AsyncIterator[WalletActionScope]: - async with ActionScope.new_scope(WalletSideEffects) as self: + async with ActionScope.new_scope( + WalletSideEffects, WalletActionConfig(push, merge_spends, sign, additional_signing_responses, extra_spends) + ) as self: self = cast(WalletActionScope, self) async with self.use() as interface: interface.side_effects.signing_responses = additional_signing_responses.copy()