mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
[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
This commit is contained in:
@@ -21,6 +21,12 @@ class TestSideEffects:
|
|||||||
return cls(blob)
|
return cls(blob)
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass
|
||||||
|
class TestConfig:
|
||||||
|
test_foo: str = "test_foo"
|
||||||
|
|
||||||
|
|
||||||
async def default_async_callback(interface: StateInterface[TestSideEffects]) -> None:
|
async def default_async_callback(interface: StateInterface[TestSideEffects]) -> None:
|
||||||
return None # pragma: no cover
|
return None # pragma: no cover
|
||||||
|
|
||||||
@@ -36,13 +42,14 @@ def test_set_callback() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="action_scope")
|
@pytest.fixture(name="action_scope")
|
||||||
async def action_scope_fixture() -> AsyncIterator[ActionScope[TestSideEffects]]:
|
async def action_scope_fixture() -> AsyncIterator[ActionScope[TestSideEffects, TestConfig]]:
|
||||||
async with ActionScope.new_scope(TestSideEffects) as scope:
|
async with ActionScope.new_scope(TestSideEffects, TestConfig()) as scope:
|
||||||
|
assert scope.config == TestConfig(test_foo="test_foo")
|
||||||
yield scope
|
yield scope
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@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
|
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
|
@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:
|
async with action_scope.use() as interface:
|
||||||
interface.side_effects.buf = b"baz"
|
interface.side_effects.buf = b"baz"
|
||||||
|
|
||||||
@@ -60,7 +67,7 @@ async def test_scope_persistence(action_scope: ActionScope[TestSideEffects]) ->
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@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:
|
async with action_scope.use() as interface:
|
||||||
interface.side_effects.buf = b"baz"
|
interface.side_effects.buf = b"baz"
|
||||||
|
|
||||||
@@ -75,7 +82,7 @@ async def test_transactionality(action_scope: ActionScope[TestSideEffects]) -> N
|
|||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_callbacks() -> None:
|
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 with action_scope.use() as interface:
|
||||||
|
|
||||||
async def callback(interface: StateInterface[TestSideEffects]) -> None:
|
async def callback(interface: StateInterface[TestSideEffects]) -> None:
|
||||||
@@ -89,7 +96,7 @@ async def test_callbacks() -> None:
|
|||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_callback_in_callback_error() -> None:
|
async def test_callback_in_callback_error() -> None:
|
||||||
with pytest.raises(RuntimeError, match="Callback"):
|
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 with action_scope.use() as interface:
|
||||||
|
|
||||||
async def callback(interface: StateInterface[TestSideEffects]) -> None:
|
async def callback(interface: StateInterface[TestSideEffects]) -> None:
|
||||||
@@ -101,7 +108,7 @@ async def test_callback_in_callback_error() -> None:
|
|||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_no_callbacks_if_error() -> None:
|
async def test_no_callbacks_if_error() -> None:
|
||||||
with pytest.raises(Exception, match="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 with action_scope.use() as interface:
|
||||||
|
|
||||||
async def callback(interface: StateInterface[TestSideEffects]) -> None:
|
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")
|
raise RuntimeError("This should prevent the callbacks from being called")
|
||||||
|
|
||||||
with pytest.raises(Exception, match="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 with action_scope.use() as interface:
|
||||||
|
|
||||||
async def callback2(interface: StateInterface[TestSideEffects]) -> None:
|
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
|
# TODO: add suport, change this test to test it and add a test for nested transactionality
|
||||||
@pytest.mark.anyio
|
@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():
|
async with action_scope.use():
|
||||||
with pytest.raises(RuntimeError, match="cannot currently support nested transactions"):
|
with pytest.raises(RuntimeError, match="cannot currently support nested transactions"):
|
||||||
async with action_scope.use():
|
async with action_scope.use():
|
||||||
|
|||||||
@@ -102,8 +102,6 @@ def wrap_http_handler(f) -> Callable:
|
|||||||
def tx_endpoint(
|
def tx_endpoint(
|
||||||
push: bool = False,
|
push: bool = False,
|
||||||
merge_spends: bool = True,
|
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]:
|
) -> Callable[[RpcEndpoint], RpcEndpoint]:
|
||||||
def _inner(func: RpcEndpoint) -> RpcEndpoint:
|
def _inner(func: RpcEndpoint) -> RpcEndpoint:
|
||||||
async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]:
|
async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]:
|
||||||
@@ -162,7 +160,6 @@ def tx_endpoint(
|
|||||||
request,
|
request,
|
||||||
*args,
|
*args,
|
||||||
action_scope,
|
action_scope,
|
||||||
*([push] if requires_default_information else []),
|
|
||||||
tx_config=tx_config,
|
tx_config=tx_config,
|
||||||
extra_conditions=extra_conditions,
|
extra_conditions=extra_conditions,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|||||||
@@ -694,12 +694,11 @@ class WalletRpcApi:
|
|||||||
response["fingerprint"] = self.service.logged_in_fingerprint
|
response["fingerprint"] = self.service.logged_in_fingerprint
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@tx_endpoint(push=True, requires_default_information=True)
|
@tx_endpoint(push=True)
|
||||||
async def create_new_wallet(
|
async def create_new_wallet(
|
||||||
self,
|
self,
|
||||||
request: Dict[str, Any],
|
request: Dict[str, Any],
|
||||||
action_scope: WalletActionScope,
|
action_scope: WalletActionScope,
|
||||||
push: bool = True,
|
|
||||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||||
) -> EndpointResult:
|
) -> EndpointResult:
|
||||||
@@ -715,7 +714,7 @@ class WalletRpcApi:
|
|||||||
name = request.get("name", None)
|
name = request.get("name", None)
|
||||||
if request["mode"] == "new":
|
if request["mode"] == "new":
|
||||||
if request.get("test", False):
|
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
|
raise ValueError("Test CAT minting must be pushed automatically") # pragma: no cover
|
||||||
async with self.service.wallet_state_manager.lock:
|
async with self.service.wallet_state_manager.lock:
|
||||||
cat_wallet = await CATWallet.create_new_cat_wallet(
|
cat_wallet = await CATWallet.create_new_cat_wallet(
|
||||||
@@ -1825,16 +1824,15 @@ class WalletRpcApi:
|
|||||||
else:
|
else:
|
||||||
return {"wallet_id": wallet.id(), "name": (wallet.get_name())}
|
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(
|
async def create_offer_for_ids(
|
||||||
self,
|
self,
|
||||||
request: Dict[str, Any],
|
request: Dict[str, Any],
|
||||||
action_scope: WalletActionScope,
|
action_scope: WalletActionScope,
|
||||||
push: bool = False,
|
|
||||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||||
) -> EndpointResult:
|
) -> EndpointResult:
|
||||||
if push:
|
if action_scope.config.push:
|
||||||
raise ValueError("Cannot push an incomplete spend") # pragma: no cover
|
raise ValueError("Cannot push an incomplete spend") # pragma: no cover
|
||||||
|
|
||||||
offer: Dict[str, int] = request["offer"]
|
offer: Dict[str, int] = request["offer"]
|
||||||
@@ -3617,16 +3615,15 @@ class WalletRpcApi:
|
|||||||
{asset["asset"]: uint64(asset["amount"]) for asset in request.get("fungible_assets", [])},
|
{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(
|
async def nft_mint_bulk(
|
||||||
self,
|
self,
|
||||||
request: Dict[str, Any],
|
request: Dict[str, Any],
|
||||||
action_scope: WalletActionScope,
|
action_scope: WalletActionScope,
|
||||||
push: bool = False,
|
|
||||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||||
) -> EndpointResult:
|
) -> EndpointResult:
|
||||||
if push:
|
if action_scope.config.push:
|
||||||
raise ValueError("Automatic pushing of nft minting transactions not yet available") # pragma: no cover
|
raise ValueError("Automatic pushing of nft minting transactions not yet available") # pragma: no cover
|
||||||
if await self.service.wallet_state_manager.synced() is False:
|
if await self.service.wallet_state_manager.synced() is False:
|
||||||
raise ValueError("Wallet needs to be fully synced.")
|
raise ValueError("Wallet needs to be fully synced.")
|
||||||
|
|||||||
@@ -84,11 +84,12 @@ class SideEffects(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
_T_SideEffects = TypeVar("_T_SideEffects", bound=SideEffects)
|
_T_SideEffects = TypeVar("_T_SideEffects", bound=SideEffects)
|
||||||
|
_T_Config = TypeVar("_T_Config")
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass
|
@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
|
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.
|
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
|
_resource_manager: ResourceManager
|
||||||
_side_effects_format: Type[_T_SideEffects]
|
_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
|
_callback: Optional[Callable[[StateInterface[_T_SideEffects]], Awaitable[None]]] = None
|
||||||
_final_side_effects: Optional[_T_SideEffects] = field(init=False, default=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
|
return self._final_side_effects
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> _T_Config:
|
||||||
|
return self._config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def new_scope(
|
async def new_scope(
|
||||||
cls,
|
cls,
|
||||||
side_effects_format: Type[_T_SideEffects],
|
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,
|
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:
|
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
|
yield self
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
from dataclasses import dataclass, field
|
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.types.spend_bundle import SpendBundle
|
||||||
from chia.util.action_scope import ActionScope
|
from chia.util.action_scope import ActionScope
|
||||||
@@ -65,7 +65,17 @@ class WalletSideEffects:
|
|||||||
return instance
|
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
|
@contextlib.asynccontextmanager
|
||||||
@@ -77,7 +87,9 @@ async def new_wallet_action_scope(
|
|||||||
additional_signing_responses: List[SigningResponse] = [],
|
additional_signing_responses: List[SigningResponse] = [],
|
||||||
extra_spends: List[SpendBundle] = [],
|
extra_spends: List[SpendBundle] = [],
|
||||||
) -> AsyncIterator[WalletActionScope]:
|
) -> 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)
|
self = cast(WalletActionScope, self)
|
||||||
async with self.use() as interface:
|
async with self.use() as interface:
|
||||||
interface.side_effects.signing_responses = additional_signing_responses.copy()
|
interface.side_effects.signing_responses = additional_signing_responses.copy()
|
||||||
|
|||||||
Reference in New Issue
Block a user