From 2ac819d3611be3b8770a1304b36c0086bf9f0de5 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Thu, 2 Oct 2025 14:10:13 +0000 Subject: [PATCH 1/7] Replace AuthorityType with AurhotiryLevelRegular --- .../process/process_account_update.py | 9 ++-- .../cli/commands/show/show_authority.py | 4 +- .../__private/cli/process/update_authority.py | 5 ++- clive/__private/cli/types.py | 5 +-- clive/__private/core/constants/authority.py | 5 ++- .../clive_local_tools/cli/checkers.py | 14 +++--- .../clive_local_tools/cli/cli_tester.py | 7 ++- .../test_process_update_authority_account.py | 17 ++++---- ...ocess_update_authority_command_chaining.py | 17 ++++---- .../test_process_update_authority_key.py | 17 ++++---- ..._process_update_authority_options_order.py | 43 +++++++++++-------- ...test_process_update_authority_threshold.py | 13 +++--- .../cli/show/test_show_authority.py | 9 ++-- 13 files changed, 88 insertions(+), 77 deletions(-) diff --git a/clive/__private/cli/commands/process/process_account_update.py b/clive/__private/cli/commands/process/process_account_update.py index 5ebeeca1a0..35a499cc29 100644 --- a/clive/__private/cli/commands/process/process_account_update.py +++ b/clive/__private/cli/commands/process/process_account_update.py @@ -9,11 +9,8 @@ from clive.__private.cli.exceptions import CLIPrettyError from clive.__private.models.schemas import AccountName, AccountUpdate2Operation, Authority, HiveInt, PublicKey if TYPE_CHECKING: - from clive.__private.cli.types import ( - AccountUpdateFunction, - AuthorityType, - AuthorityUpdateFunction, - ) + from clive.__private.cli.types import AccountUpdateFunction, AuthorityUpdateFunction + from clive.__private.core.types import AuthorityLevelRegular from clive.__private.models.schemas import Account @@ -179,7 +176,7 @@ def set_memo_key(operation: AccountUpdate2Operation, key: str) -> AccountUpdate2 def update_authority( - operation: AccountUpdate2Operation, attribute: AuthorityType, callback: AuthorityUpdateFunction + operation: AccountUpdate2Operation, attribute: AuthorityLevelRegular, callback: AuthorityUpdateFunction ) -> AccountUpdate2Operation: auth_attribute = getattr(operation, attribute) if not auth_attribute: diff --git a/clive/__private/cli/commands/show/show_authority.py b/clive/__private/cli/commands/show/show_authority.py index ce7a46b495..8f036715da 100644 --- a/clive/__private/cli/commands/show/show_authority.py +++ b/clive/__private/cli/commands/show/show_authority.py @@ -9,13 +9,13 @@ from clive.__private.cli.commands.abc.world_based_command import WorldBasedComma from clive.__private.cli.print_cli import print_cli if TYPE_CHECKING: - from clive.__private.cli.types import AuthorityType + from clive.__private.core.types import AuthorityLevelRegular @dataclass(kw_only=True) class ShowAuthority(WorldBasedCommand): account_name: str - authority: AuthorityType + authority: AuthorityLevelRegular async def _run(self) -> None: accounts = (await self.world.commands.find_accounts(accounts=[self.account_name])).result_or_raise diff --git a/clive/__private/cli/process/update_authority.py b/clive/__private/cli/process/update_authority.py index 0908967946..e1ce94b9a0 100644 --- a/clive/__private/cli/process/update_authority.py +++ b/clive/__private/cli/process/update_authority.py @@ -13,7 +13,8 @@ from clive.__private.core._async import asyncio_run if TYPE_CHECKING: from clive.__private.cli.commands.process.process_account_update import ProcessAccountUpdate - from clive.__private.cli.types import AccountUpdateFunction, AuthorityType + from clive.__private.cli.types import AccountUpdateFunction + from clive.__private.core.types import AuthorityLevelRegular _authority_account_name = typer.Option( @@ -91,7 +92,7 @@ def modify_command_common_options( ) -def get_update_authority_typer(authority: AuthorityType) -> CliveTyper: # noqa: PLR0915 +def get_update_authority_typer(authority: AuthorityLevelRegular) -> CliveTyper: # noqa: PLR0915 epilog = f"Look also at the help for command update-{authority}-authority for more options." update = CliveTyper( name=f"update-{authority}-authority", diff --git a/clive/__private/cli/types.py b/clive/__private/cli/types.py index ba401c4094..5100179034 100644 --- a/clive/__private/cli/types.py +++ b/clive/__private/cli/types.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable @@ -9,6 +9,3 @@ if TYPE_CHECKING: AccountUpdateFunction = Callable[[AccountUpdate2Operation], AccountUpdate2Operation] AuthorityUpdateFunction = Callable[[Authority], Authority] - - -AuthorityType = Literal["owner", "active", "posting"] diff --git a/clive/__private/core/constants/authority.py b/clive/__private/core/constants/authority.py index bf740a57e6..1bcde4862d 100644 --- a/clive/__private/core/constants/authority.py +++ b/clive/__private/core/constants/authority.py @@ -2,8 +2,11 @@ from __future__ import annotations from typing import Final, get_args -from clive.__private.core.types import AuthorityLevel +from clive.__private.core.types import AuthorityLevel, AuthorityLevelRegular +AUTHORITY_LEVELS_REGULAR: Final[tuple[AuthorityLevelRegular, ...]] = tuple( + authority_level for authority_level in get_args(AuthorityLevelRegular) +) AUTHORITY_LEVELS: Final[tuple[AuthorityLevel, ...]] = tuple( authority_level for union_member in get_args(AuthorityLevel) for authority_level in get_args(union_member) ) diff --git a/tests/clive-local-tools/clive_local_tools/cli/checkers.py b/tests/clive-local-tools/clive_local_tools/cli/checkers.py index dea6697f59..9de4d29dfe 100644 --- a/tests/clive-local-tools/clive_local_tools/cli/checkers.py +++ b/tests/clive-local-tools/clive_local_tools/cli/checkers.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: import test_tools as tt - from clive.__private.cli.types import AuthorityType + from clive.__private.core.types import AuthorityLevelRegular from clive.__private.models.schemas import PublicKey @@ -47,12 +47,12 @@ def assert_pending_withrawals(context: CLITester | Result, account_name: str, as ), f"no {asset_amount.pretty_amount()} {asset_amount.token()} in pending withdrawals output:\n{output}" -def get_authority_output(context: CLITester | Result, authority: AuthorityType) -> str: +def get_authority_output(context: CLITester | Result, authority: AuthorityLevelRegular) -> str: result = context.show_authority(authority) if isinstance(context, CLITester) else context return result.output -def assert_is_authority(context: CLITester | Result, entry: str | PublicKey, authority: AuthorityType) -> None: +def assert_is_authority(context: CLITester | Result, entry: str | PublicKey, authority: AuthorityLevelRegular) -> None: output = get_authority_output(context, authority) table = output.split("\n")[2:] assert any(str(entry) in line for line in table), ( @@ -60,7 +60,9 @@ def assert_is_authority(context: CLITester | Result, entry: str | PublicKey, aut ) -def assert_is_not_authority(context: CLITester | Result, entry: str | PublicKey, authority: AuthorityType) -> None: +def assert_is_not_authority( + context: CLITester | Result, entry: str | PublicKey, authority: AuthorityLevelRegular +) -> None: output = get_authority_output(context, authority) table = output.split("\n")[2:] assert not any(str(entry) in line for line in table), ( @@ -71,7 +73,7 @@ def assert_is_not_authority(context: CLITester | Result, entry: str | PublicKey, def assert_authority_weight( context: CLITester | Result, entry: str | PublicKey, - authority: AuthorityType, + authority: AuthorityLevelRegular, weight: int, ) -> None: output = get_authority_output(context, authority) @@ -80,7 +82,7 @@ def assert_authority_weight( ) -def assert_weight_threshold(context: CLITester | Result, authority: AuthorityType, threshold: int) -> None: +def assert_weight_threshold(context: CLITester | Result, authority: AuthorityLevelRegular, threshold: int) -> None: output = get_authority_output(context, authority) expected_output = f"weight threshold is {threshold}" command = f"show {authority}-authority" diff --git a/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py b/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py index 170bf1b70a..aae1a50d27 100644 --- a/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py +++ b/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py @@ -20,8 +20,7 @@ if TYPE_CHECKING: from typer.testing import CliRunner from clive.__private.cli.clive_typer import CliveTyper - from clive.__private.cli.types import AuthorityType - from clive.__private.core.types import AlreadySignedMode + from clive.__private.core.types import AlreadySignedMode, AuthorityLevelRegular from clive.__private.core.world import World from clive.__private.models.schemas import PublicKey from clive_local_tools.cli.command_options import CliOptionT @@ -44,7 +43,7 @@ class CLITester: raise CLITestCommandError(command, result.exit_code, result.stdout, result) return result - def show_authority(self, authority: AuthorityType, *, account_name: str | None = None) -> Result: + def show_authority(self, authority: AuthorityLevelRegular, *, account_name: str | None = None) -> Result: match authority: case "owner": return self.show_owner_authority(account_name=account_name) @@ -57,7 +56,7 @@ class CLITester: def process_update_authority( # noqa: PLR0913 self, - authority: AuthorityType, + authority: AuthorityLevelRegular, *, account_name: str | None = None, threshold: int | None = None, diff --git a/tests/functional/cli/process/test_process_update_authority_account.py b/tests/functional/cli/process/test_process_update_authority_account.py index 8cb790c40a..95a407cadc 100644 --- a/tests/functional/cli/process/test_process_update_authority_account.py +++ b/tests/functional/cli/process/test_process_update_authority_account.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final, get_args +from typing import TYPE_CHECKING, Final import pytest -from clive.__private.cli.types import AuthorityType +from clive.__private.core.constants.authority import AUTHORITY_LEVELS_REGULAR from clive_local_tools.cli.checkers import assert_authority_weight, assert_is_authority, assert_is_not_authority from clive_local_tools.data.constants import WORKING_ACCOUNT_KEY_ALIAS from clive_local_tools.testnet_block_log import WATCHED_ACCOUNTS_DATA @@ -12,6 +12,7 @@ from clive_local_tools.testnet_block_log import WATCHED_ACCOUNTS_DATA if TYPE_CHECKING: import test_tools as tt + from clive.__private.core.types import AuthorityLevelRegular from clive_local_tools.cli.cli_tester import CLITester @@ -20,8 +21,8 @@ WEIGHT: Final[int] = 123 MODIFIED_WEIGHT: Final[int] = 124 -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_add_account(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_add_account(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority(authority, sign_with=WORKING_ACCOUNT_KEY_ALIAS).add_account( account=OTHER_ACCOUNT.name, weight=WEIGHT @@ -32,8 +33,8 @@ async def test_add_account(cli_tester: CLITester, authority: AuthorityType) -> N assert_authority_weight(cli_tester, OTHER_ACCOUNT.name, authority, WEIGHT) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_remove_account(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_remove_account(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ARRANGE cli_tester.process_update_authority(authority, sign_with=WORKING_ACCOUNT_KEY_ALIAS).add_account( account=OTHER_ACCOUNT.name, weight=WEIGHT @@ -49,8 +50,8 @@ async def test_remove_account(cli_tester: CLITester, authority: AuthorityType) - assert_is_not_authority(cli_tester, OTHER_ACCOUNT.name, authority) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_modify_account(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_modify_account(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ARRANGE cli_tester.process_update_authority(authority, sign_with=WORKING_ACCOUNT_KEY_ALIAS).add_account( account=OTHER_ACCOUNT.name, weight=WEIGHT diff --git a/tests/functional/cli/process/test_process_update_authority_command_chaining.py b/tests/functional/cli/process/test_process_update_authority_command_chaining.py index 370fd38925..4a62cafe8a 100644 --- a/tests/functional/cli/process/test_process_update_authority_command_chaining.py +++ b/tests/functional/cli/process/test_process_update_authority_command_chaining.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final, get_args +from typing import TYPE_CHECKING, Final import pytest -from clive.__private.cli.types import AuthorityType +from clive.__private.core.constants.authority import AUTHORITY_LEVELS_REGULAR from clive_local_tools.cli.checkers import ( assert_authority_weight, assert_is_authority, @@ -17,6 +17,7 @@ from clive_local_tools.testnet_block_log import WATCHED_ACCOUNTS_DATA, WORKING_A if TYPE_CHECKING: import test_tools as tt + from clive.__private.core.types import AuthorityLevelRegular from clive_local_tools.cli.cli_tester import CLITester @@ -27,8 +28,8 @@ MODIFIED_WEIGHT: Final[int] = 214 WEIGHT_THRESHOLD: Final[int] = 2 -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_chaining(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_chaining(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -51,8 +52,8 @@ async def test_chaining(cli_tester: CLITester, authority: AuthorityType) -> None assert_authority_weight(cli_tester, OTHER_ACCOUNT.public_key, authority, WEIGHT) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_chaining2(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_chaining2(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -81,8 +82,8 @@ async def test_chaining2(cli_tester: CLITester, authority: AuthorityType) -> Non assert_is_not_authority(cli_tester, WORKING_ACCOUNT_DATA.account.public_key, authority) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_chaining3(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_chaining3(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, diff --git a/tests/functional/cli/process/test_process_update_authority_key.py b/tests/functional/cli/process/test_process_update_authority_key.py index 5b0f3bd56a..99db3f948c 100644 --- a/tests/functional/cli/process/test_process_update_authority_key.py +++ b/tests/functional/cli/process/test_process_update_authority_key.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final, get_args +from typing import TYPE_CHECKING, Final import pytest -from clive.__private.cli.types import AuthorityType +from clive.__private.core.constants.authority import AUTHORITY_LEVELS_REGULAR from clive_local_tools.cli.checkers import assert_authority_weight, assert_is_authority, assert_is_not_authority from clive_local_tools.data.constants import WORKING_ACCOUNT_KEY_ALIAS from clive_local_tools.testnet_block_log import WATCHED_ACCOUNTS_DATA @@ -12,6 +12,7 @@ from clive_local_tools.testnet_block_log import WATCHED_ACCOUNTS_DATA if TYPE_CHECKING: import test_tools as tt + from clive.__private.core.types import AuthorityLevelRegular from clive_local_tools.cli.cli_tester import CLITester @@ -20,8 +21,8 @@ WEIGHT: Final[int] = 123 MODIFIED_WEIGHT: Final[int] = 124 -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_add_key(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_add_key(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -36,8 +37,8 @@ async def test_add_key(cli_tester: CLITester, authority: AuthorityType) -> None: assert_authority_weight(cli_tester, OTHER_ACCOUNT.public_key, authority, WEIGHT) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_remove_key(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_remove_key(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ARRANGE cli_tester.process_update_authority( authority, @@ -60,8 +61,8 @@ async def test_remove_key(cli_tester: CLITester, authority: AuthorityType) -> No assert_is_not_authority(cli_tester, OTHER_ACCOUNT.public_key, authority) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_modify_key(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_modify_key(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ARRANGE cli_tester.process_update_authority( authority, diff --git a/tests/functional/cli/process/test_process_update_authority_options_order.py b/tests/functional/cli/process/test_process_update_authority_options_order.py index 6b361f1ada..d97685ada8 100644 --- a/tests/functional/cli/process/test_process_update_authority_options_order.py +++ b/tests/functional/cli/process/test_process_update_authority_options_order.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final, get_args +from typing import TYPE_CHECKING, Final import pytest -from clive.__private.cli.types import AuthorityType +from clive.__private.core.constants.authority import AUTHORITY_LEVELS_REGULAR from clive_local_tools.cli.checkers import ( assert_authority_weight, assert_is_authority, @@ -19,6 +19,7 @@ if TYPE_CHECKING: import test_tools as tt + from clive.__private.core.types import AuthorityLevelRegular from clive_local_tools.cli.cli_tester import CLITester @@ -29,8 +30,8 @@ MODIFIED_WEIGHT: Final[int] = 214 WEIGHT_THRESHOLD: Final[int] = 2 -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_sign_before_chaining(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_sign_before_chaining(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -56,8 +57,8 @@ async def test_sign_before_chaining(cli_tester: CLITester, authority: AuthorityT assert_authority_weight(cli_tester, OTHER_ACCOUNT.name, authority, WEIGHT) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_sign_after_chaining(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_sign_after_chaining(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -83,8 +84,10 @@ async def test_sign_after_chaining(cli_tester: CLITester, authority: AuthorityTy assert_authority_weight(cli_tester, OTHER_ACCOUNT.name, authority, WEIGHT) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_save_file_before_chaining(cli_tester: CLITester, authority: AuthorityType, tmp_path: Path) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_save_file_before_chaining( + cli_tester: CLITester, authority: AuthorityLevelRegular, tmp_path: Path +) -> None: # ARRANGE file_path = tmp_path / f"trx_update_{authority}_authority.json" @@ -113,8 +116,10 @@ async def test_save_file_before_chaining(cli_tester: CLITester, authority: Autho assert_is_not_authority(cli_tester, OTHER_ACCOUNT.name, authority) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_save_file_option_after_chaining(cli_tester: CLITester, authority: AuthorityType, tmp_path: Path) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_save_file_option_after_chaining( + cli_tester: CLITester, authority: AuthorityLevelRegular, tmp_path: Path +) -> None: # ARRANGE file_path = tmp_path / f"trx_update_{authority}_authority.json" @@ -143,8 +148,8 @@ async def test_save_file_option_after_chaining(cli_tester: CLITester, authority: assert_is_not_authority(cli_tester, OTHER_ACCOUNT.name, authority) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_no_broadcast_before_chaining(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_no_broadcast_before_chaining(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -165,8 +170,8 @@ async def test_no_broadcast_before_chaining(cli_tester: CLITester, authority: Au assert_is_not_authority(cli_tester, OTHER_ACCOUNT.name, authority) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_no_broadcast_overriden(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_no_broadcast_overriden(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -191,8 +196,8 @@ async def test_no_broadcast_overriden(cli_tester: CLITester, authority: Authorit assert_authority_weight(cli_tester, OTHER_ACCOUNT.public_key, authority, WEIGHT) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_sign_option_multiple_times(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_sign_option_multiple_times(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT cli_tester.process_update_authority( authority, @@ -219,8 +224,10 @@ async def test_sign_option_multiple_times(cli_tester: CLITester, authority: Auth assert_authority_weight(cli_tester, WORKING_ACCOUNT_DATA.account.public_key, authority, MODIFIED_WEIGHT) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_save_file_option_multiple_times(cli_tester: CLITester, authority: AuthorityType, tmp_path: Path) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_save_file_option_multiple_times( + cli_tester: CLITester, authority: AuthorityLevelRegular, tmp_path: Path +) -> None: # ARRANGE first_file_path = tmp_path / "notcreated.json" second_file_path = tmp_path / f"trx_update_{authority}_authority.json" diff --git a/tests/functional/cli/process/test_process_update_authority_threshold.py b/tests/functional/cli/process/test_process_update_authority_threshold.py index f220aec4bc..4989738f3d 100644 --- a/tests/functional/cli/process/test_process_update_authority_threshold.py +++ b/tests/functional/cli/process/test_process_update_authority_threshold.py @@ -1,21 +1,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING import pytest from clive.__private.cli.commands.process.process_account_update import NoChangesTransactionError -from clive.__private.cli.types import AuthorityType +from clive.__private.core.constants.authority import AUTHORITY_LEVELS_REGULAR from clive_local_tools.cli.checkers import assert_weight_threshold from clive_local_tools.cli.exceptions import CLITestCommandError from clive_local_tools.data.constants import WORKING_ACCOUNT_KEY_ALIAS if TYPE_CHECKING: + from clive.__private.core.types import AuthorityLevelRegular from clive_local_tools.cli.cli_tester import CLITester -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_set_threshold(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_set_threshold(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ARRANGE weight_threshold = 3 @@ -28,8 +29,8 @@ async def test_set_threshold(cli_tester: CLITester, authority: AuthorityType) -> assert_weight_threshold(cli_tester, authority, weight_threshold) -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_negative_do_nothing_command(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_negative_do_nothing_command(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT & ASSERT with pytest.raises(CLITestCommandError, match=NoChangesTransactionError.MESSAGE): cli_tester.process_update_authority(authority, sign_with=WORKING_ACCOUNT_KEY_ALIAS).fire() diff --git a/tests/functional/cli/show/test_show_authority.py b/tests/functional/cli/show/test_show_authority.py index 01c943fabd..2f96ae5e4d 100644 --- a/tests/functional/cli/show/test_show_authority.py +++ b/tests/functional/cli/show/test_show_authority.py @@ -1,19 +1,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING import pytest -from clive.__private.cli.types import AuthorityType +from clive.__private.core.constants.authority import AUTHORITY_LEVELS_REGULAR from clive_local_tools.cli.checkers import assert_is_authority from clive_local_tools.testnet_block_log import WORKING_ACCOUNT_DATA if TYPE_CHECKING: + from clive.__private.core.types import AuthorityLevelRegular from clive_local_tools.cli.cli_tester import CLITester -@pytest.mark.parametrize("authority", get_args(AuthorityType)) -async def test_show_authority_basic(cli_tester: CLITester, authority: AuthorityType) -> None: +@pytest.mark.parametrize("authority", AUTHORITY_LEVELS_REGULAR) +async def test_show_authority_basic(cli_tester: CLITester, authority: AuthorityLevelRegular) -> None: # ACT # ASSERT assert_is_authority(cli_tester, WORKING_ACCOUNT_DATA.account.public_key, authority) -- GitLab From e10f2d53cd13130ca2924cd9130fc6beb1977710 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Thu, 2 Oct 2025 11:23:54 +0000 Subject: [PATCH 2/7] Add method post_run in ExternalCLICommand --- clive/__private/cli/commands/abc/contextual_cli_command.py | 1 + clive/__private/cli/commands/abc/external_cli_command.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/clive/__private/cli/commands/abc/contextual_cli_command.py b/clive/__private/cli/commands/abc/contextual_cli_command.py index fe7414f673..4ecbeb8aa1 100644 --- a/clive/__private/cli/commands/abc/contextual_cli_command.py +++ b/clive/__private/cli/commands/abc/contextual_cli_command.py @@ -64,3 +64,4 @@ class ContextualCLICommand[AsyncContextManagerT: AbstractAsyncContextManager[Any await self.validate_inside_context_manager() await self._configure_inside_context_manager() await self._run() + await self.post_run() diff --git a/clive/__private/cli/commands/abc/external_cli_command.py b/clive/__private/cli/commands/abc/external_cli_command.py index 84d001bf02..98f0a8d6a5 100644 --- a/clive/__private/cli/commands/abc/external_cli_command.py +++ b/clive/__private/cli/commands/abc/external_cli_command.py @@ -34,6 +34,11 @@ class ExternalCLICommand(ABC): await self.validate() await self._configure() await self._run() + await self.post_run() + + async def post_run(self) -> None: + """Performed after _run, can be overridden in subclass.""" + return async def validate(self) -> None: """ -- GitLab From ef9cc5029b27b874fa04a71eb7050544c46c90d3 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Mon, 6 Oct 2025 11:09:38 +0000 Subject: [PATCH 3/7] Core command for get_witness_schedule api call --- clive/__private/core/commands/commands.py | 8 ++++++++ .../data_retrieval/get_witness_schedule.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 clive/__private/core/commands/data_retrieval/get_witness_schedule.py diff --git a/clive/__private/core/commands/commands.py b/clive/__private/core/commands/commands.py index 27d3efc037..4a08f86b27 100644 --- a/clive/__private/core/commands/commands.py +++ b/clive/__private/core/commands/commands.py @@ -67,6 +67,7 @@ if TYPE_CHECKING: Proposal, TransactionStatus, Witness, + WitnessSchedule, ) from clive.__private.models.transaction import Transaction from wax.models.authority import WaxAccountAuthorityInfo @@ -619,6 +620,13 @@ class Commands[WorldT: World]: FindScheduledTransfers(node=self._world.node, account_name=account_name) ) + async def get_witness_schedule(self) -> CommandWithResultWrapper[WitnessSchedule]: + from clive.__private.core.commands.data_retrieval.get_witness_schedule import ( # noqa: PLC0415 + GetWitnessSchedule, + ) + + return await self.__surround_with_exception_handlers(GetWitnessSchedule(node=self._world.node)) + async def save_profile(self) -> NoOpWrapper | CommandWrapper: profile = self._world.profile if not profile.should_be_saved: diff --git a/clive/__private/core/commands/data_retrieval/get_witness_schedule.py b/clive/__private/core/commands/data_retrieval/get_witness_schedule.py new file mode 100644 index 0000000000..2bdad81970 --- /dev/null +++ b/clive/__private/core/commands/data_retrieval/get_witness_schedule.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from clive.__private.core.commands.abc.command_data_retrieval import CommandDataRetrieval +from clive.__private.models.schemas import WitnessSchedule + +if TYPE_CHECKING: + from clive.__private.core.node import Node + + +@dataclass(kw_only=True) +class GetWitnessSchedule(CommandDataRetrieval[WitnessSchedule, WitnessSchedule, WitnessSchedule]): + node: Node + + async def _harvest_data_from_api(self) -> WitnessSchedule: + return await self.node.api.database_api.get_witness_schedule() -- GitLab From f5f5d26490a00e4b9577795a17516015275a5cd4 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Thu, 2 Oct 2025 07:48:49 +0000 Subject: [PATCH 4/7] Cli command clive process account-creation Allowing to pass public keys as positional arguments/options --- .../process/process_account_creation.py | 173 ++++++++++++++++++ .../parameters/argument_related_options.py | 39 ++++ .../cli/common/parameters/arguments.py | 27 ++- .../cli/common/parameters/options.py | 6 + clive/__private/cli/common/parsers.py | 13 ++ clive/__private/cli/exceptions.py | 13 ++ clive/__private/cli/process/main.py | 88 +++++++++ clive/__private/core/constants/cli.py | 3 + pydoclint-errors-baseline.txt | 2 + 9 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 clive/__private/cli/commands/process/process_account_creation.py diff --git a/clive/__private/cli/commands/process/process_account_creation.py b/clive/__private/cli/commands/process/process_account_creation.py new file mode 100644 index 0000000000..8b212234a9 --- /dev/null +++ b/clive/__private/cli/commands/process/process_account_creation.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, override + +from clive.__private.cli.commands.abc.operation_command import OperationCommand +from clive.__private.cli.exceptions import CLIPrettyError +from clive.__private.cli.print_cli import print_cli +from clive.__private.core.constants.cli import ( + DEFAULT_AUTHORITY_THRESHOLD, + DEFAULT_AUTHORITY_WEIGHT, +) +from clive.__private.models.asset import Asset +from clive.__private.models.schemas import ( + AccountCreateOperation, + Authority, + CreateClaimedAccountOperation, +) + +if TYPE_CHECKING: + from clive.__private.core.keys.keys import PublicKey + from clive.__private.core.types import AuthorityLevel, AuthorityLevelRegular + + +class MissingAuthorityError(CLIPrettyError): + """ + Raised when trying to create a account without authority given as parameter. + + Args: + level: A type of authority that is missing. + """ + + def __init__(self, level: AuthorityLevel) -> None: + super().__init__(f"Missing entry in the `{level}` authority definition.") + + +@dataclass(kw_only=True) +class ProcessAccountCreation(OperationCommand): + creator: str + new_account_name: str + fee: bool + json_metadata: str + + def __post_init__(self) -> None: + self._owner_authority = self._create_empty_authority() + self._active_authority = self._create_empty_authority() + self._posting_authority = self._create_empty_authority() + self._memo_key: PublicKey | None = None + self._fee_value: Asset.Hive | None = None # Set after fetching from node + + @property + def fee_value_ensure(self) -> Asset.Hive: + assert self._fee_value is not None, "Value of fee must be fetched from node" + return self._fee_value + + @property + def is_active_authority_set(self) -> bool: + return self._is_authority_set(self._active_authority) + + @property + def is_owner_authority_set(self) -> bool: + return self._is_authority_set(self._owner_authority) + + @property + def is_posting_authority_set(self) -> bool: + return self._is_authority_set(self._posting_authority) + + @property + def memo_key_ensure(self) -> PublicKey: + assert self._memo_key is not None, "Memo key must be specified by user and set with method `set_memo_key`" + return self._memo_key + + @property + def is_memo_key_set(self) -> bool: + return self._memo_key is not None + + @override + async def fetch_data(self) -> None: + if self.fee: + wrapper = await self.world.commands.get_witness_schedule() + witness_schedule = wrapper.result_or_raise + assert witness_schedule.median_props.account_creation_fee is not None, ( + "Account creation fee must be set in response of `get_witness_schedule`." + ) # TODO: remove after https://gitlab.syncad.com/hive/schemas/-/issues/46 is fixed + self._fee_value = witness_schedule.median_props.account_creation_fee + print_cli(f"Account creation fee: `{Asset.to_legacy(self._fee_value)}` will be paid.") + else: + self._fee_value = Asset.hive(0) + + @override + async def post_run(self) -> None: + if not self.profile.accounts.is_account_known(self.new_account_name): + print_cli(f"Adding account `{self.new_account_name}` to known accounts.") + self.profile.accounts.add_known_account(self.new_account_name) + + def set_keys(self, owner: PublicKey, active: PublicKey, posting: PublicKey) -> None: + for authority_type in ("owner", "active", "posting"): + self._set_threshold(authority_type, DEFAULT_AUTHORITY_THRESHOLD) + self._add_key_authority( + authority_type, + { + "owner": owner, + "active": active, + "posting": posting, + }[authority_type], + DEFAULT_AUTHORITY_WEIGHT, + ) + + def set_memo_key(self, key: PublicKey) -> None: + self._memo_key = key + + @override + async def validate(self) -> None: + self._validate_all_authorities_are_set() + await super().validate() + + def _add_key_authority(self, level: AuthorityLevelRegular, key: PublicKey, weight: int) -> None: + self._get_authority(level).key_auths.append((key.value, weight)) + + @classmethod + def _create_empty_authority(cls) -> Authority: + return Authority(weight_threshold=DEFAULT_AUTHORITY_THRESHOLD, account_auths=[], key_auths=[]) + + @override + async def _create_operation(self) -> AccountCreateOperation | CreateClaimedAccountOperation: + if self.fee: + return AccountCreateOperation( + fee=self.fee_value_ensure, + creator=self.creator, + new_account_name=self.new_account_name, + json_metadata=self.json_metadata, + owner=self._owner_authority, + active=self._active_authority, + posting=self._posting_authority, + memo_key=self.memo_key_ensure.value, + ) + return CreateClaimedAccountOperation( + creator=self.creator, + new_account_name=self.new_account_name, + json_metadata=self.json_metadata, + owner=self._owner_authority, + active=self._active_authority, + posting=self._posting_authority, + memo_key=self.memo_key_ensure.value, + ) + + def _get_authority(self, level: AuthorityLevelRegular) -> Authority: + mapping: dict[AuthorityLevelRegular, Authority] = { + "owner": self._owner_authority, + "active": self._active_authority, + "posting": self._posting_authority, + } + try: + return mapping[level] + except KeyError as err: + raise ValueError(f"Unknown authority type: {level}") from err + + @staticmethod + def _is_authority_set(auth: Authority) -> bool: + return bool(auth.account_auths) or bool(auth.key_auths) + + def _set_threshold(self, level: AuthorityLevelRegular, threshold: int) -> None: + self._get_authority(level).weight_threshold = threshold + + def _validate_all_authorities_are_set(self) -> None: + if not self.is_owner_authority_set: + raise MissingAuthorityError("owner") + if not self.is_active_authority_set: + raise MissingAuthorityError("active") + if not self.is_posting_authority_set: + raise MissingAuthorityError("posting") + if not self.is_memo_key_set: + raise MissingAuthorityError("memo") diff --git a/clive/__private/cli/common/parameters/argument_related_options.py b/clive/__private/cli/common/parameters/argument_related_options.py index ad943c527a..40537daa3e 100644 --- a/clive/__private/cli/common/parameters/argument_related_options.py +++ b/clive/__private/cli/common/parameters/argument_related_options.py @@ -13,6 +13,7 @@ import typer from clive.__private.cli.common.parameters import options from clive.__private.cli.common.parameters.modified_param import modified_param +from clive.__private.cli.common.parsers import public_key from clive.__private.core.constants.cli import LOOK_INTO_ARGUMENT_OPTION_HELP if TYPE_CHECKING: @@ -38,6 +39,8 @@ account_name = _make_argument_related_option(options.account_name) profile_name = _make_argument_related_option(options.profile_name) +new_account_name = _make_argument_related_option(options.new_account_name) + name = _make_argument_related_option("--name") key = _make_argument_related_option("--key") @@ -51,3 +54,39 @@ transaction_id = _make_argument_related_option("--transaction-id") chain_id = _make_argument_related_option("--chain-id") node_address = _make_argument_related_option("--node-address") + +owner_key = _make_argument_related_option( + typer.Option( + None, + "--owner", + parser=public_key, + help="Owner public key that will be set for account.", + ) +) + +active_key = _make_argument_related_option( + typer.Option( + None, + "--active", + parser=public_key, + help="Active public key that will be set for account.", + ) +) + +posting_key = _make_argument_related_option( + typer.Option( + None, + "--posting", + parser=public_key, + help="Posting public key that will be set for account.", + ) +) + +memo_key = _make_argument_related_option( + typer.Option( + None, + "--memo", + parser=public_key, + help="Memo public key that will be set for account.", + ) +) diff --git a/clive/__private/cli/common/parameters/arguments.py b/clive/__private/cli/common/parameters/arguments.py index 222a17c1c9..2ed23d5e34 100644 --- a/clive/__private/cli/common/parameters/arguments.py +++ b/clive/__private/cli/common/parameters/arguments.py @@ -11,6 +11,7 @@ from __future__ import annotations import typer from clive.__private.cli.common.parameters import modified_param +from clive.__private.cli.common.parsers import public_key from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD, REQUIRED_AS_ARG_OR_OPTION working_account_template = typer.Argument( @@ -21,4 +22,28 @@ working_account_template = typer.Argument( account_name = modified_param(working_account_template) -profile_name = typer.Argument(..., help=f"The profile to use. {REQUIRED_AS_ARG_OR_OPTION}") +profile_name = typer.Argument(..., help=f"The profile to use. ({REQUIRED_AS_ARG_OR_OPTION}") + +owner_key = typer.Argument( + None, + parser=public_key, + help=f"Owner public key that will be set for account. ({REQUIRED_AS_ARG_OR_OPTION})", +) + +active_key = typer.Argument( + None, + parser=public_key, + help=f"Active public key that will be set for account. ({REQUIRED_AS_ARG_OR_OPTION})", +) + +posting_key = typer.Argument( + None, + parser=public_key, + help=f"Posting public key that will be set for account. ({REQUIRED_AS_ARG_OR_OPTION})", +) + +memo_key = typer.Argument( + None, + parser=public_key, + help=f"Memo public key that will be set for account. ({REQUIRED_AS_ARG_OR_OPTION})", +) diff --git a/clive/__private/cli/common/parameters/options.py b/clive/__private/cli/common/parameters/options.py index 8a7148d9c1..5ce6fb7681 100644 --- a/clive/__private/cli/common/parameters/options.py +++ b/clive/__private/cli/common/parameters/options.py @@ -45,6 +45,12 @@ profile_name = typer.Option( account_name = modified_param(working_account_template, param_decls=("--account-name",)) +new_account_name = typer.Option( + ..., + "--new-account-name", + help="The name of the new account.", +) + from_account_name = modified_param( working_account_template, param_decls=("--from",), diff --git a/clive/__private/cli/common/parsers.py b/clive/__private/cli/common/parsers.py index f647af1fff..d3e35eabe1 100644 --- a/clive/__private/cli/common/parsers.py +++ b/clive/__private/cli/common/parsers.py @@ -5,11 +5,14 @@ from typing import TYPE_CHECKING, get_args import typer +from clive.__private.cli.exceptions import CLIPublicKeyInvalidFormatError + if TYPE_CHECKING: from collections.abc import Callable from datetime import timedelta from decimal import Decimal + from clive.__private.core.keys.keys import PublicKey from clive.__private.models.asset import Asset @@ -124,3 +127,13 @@ def scheduled_transfer_frequency_parser(raw: str) -> timedelta: if status.is_valid: return shorthand_timedelta_to_timedelta(raw) raise typer.BadParameter(humanize_validation_result(status)) + + +def public_key(raw: str) -> PublicKey: + from clive.__private.core.keys.keys import PublicKey, PublicKeyInvalidFormatError # noqa: PLC0415 + + try: + parsed = PublicKey(value=raw) + except PublicKeyInvalidFormatError as error: + raise CLIPublicKeyInvalidFormatError(raw) from error + return parsed diff --git a/clive/__private/cli/exceptions.py b/clive/__private/cli/exceptions.py index 9a60d8b65d..04c0bc27cd 100644 --- a/clive/__private/cli/exceptions.py +++ b/clive/__private/cli/exceptions.py @@ -462,3 +462,16 @@ class CLITransactionAlreadySignedError(CLIPrettyError): def __init__(self) -> None: super().__init__(self.MESSAGE, errno.EINVAL) + + +class CLIPublicKeyInvalidFormatError(CLIPrettyError): + """ + Raise when trying to load public key in invalid format. + + Args: + invalid_key: Key that did not pass validation and will be displayed on screen. + """ + + def __init__(self, invalid_key: str) -> None: + message = f"Given public key: `{invalid_key}` has an invalid format." + super().__init__(message, errno.EINVAL) diff --git a/clive/__private/cli/process/main.py b/clive/__private/cli/process/main.py index 4982fc4444..7159229958 100644 --- a/clive/__private/cli/process/main.py +++ b/clive/__private/cli/process/main.py @@ -8,6 +8,10 @@ import typer from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import options +from clive.__private.cli.common.parameters import argument_related_options, arguments, modified_param +from clive.__private.cli.common.parameters.ensure_single_value import ( + EnsureSingleValue, +) from clive.__private.cli.process.claim import claim from clive.__private.cli.process.custom_operations.custom_json import custom_json from clive.__private.cli.process.hive_power.delegations import delegations @@ -20,9 +24,13 @@ from clive.__private.cli.process.transfer_schedule import transfer_schedule from clive.__private.cli.process.update_authority import get_update_authority_typer from clive.__private.cli.process.vote_proposal import vote_proposal from clive.__private.cli.process.vote_witness import vote_witness +from clive.__private.core.constants.cli import ( + REQUIRED_AS_ARG_OR_OPTION, +) from clive.__private.core.constants.data_retrieval import ALREADY_SIGNED_MODE_DEFAULT, ALREADY_SIGNED_MODES if TYPE_CHECKING: + from clive.__private.core.keys.keys import PublicKey from clive.__private.models.asset import Asset process = CliveTyper(name="process", help="Process something (e.g. perform a transfer).") @@ -137,3 +145,83 @@ async def process_update_memo_key( # noqa: PLR0913 ) operation.add_callback(update_memo_key_callback) await operation.run() + + +_new_account_name_argument = typer.Argument( + None, + help=f"The name of the new account. ({REQUIRED_AS_ARG_OR_OPTION})", +) + + +@process.command(name="account-creation") +async def process_account_creation( # noqa: PLR0913 + creator: str = modified_param(options.working_account_template, param_decls=("--creator",)), + new_account_name: str | None = _new_account_name_argument, + new_account_name_option: str | None = argument_related_options.new_account_name, + owner: str | None = arguments.owner_key, + active: str | None = arguments.active_key, + posting: str | None = arguments.posting_key, + memo: str | None = arguments.memo_key, + owner_option: str | None = argument_related_options.owner_key, + active_option: str | None = argument_related_options.active_key, + posting_option: str | None = argument_related_options.posting_key, + memo_option: str | None = argument_related_options.memo_key, + fee: bool = typer.Option( # noqa: FBT001 + default=False, + help="If set to true then account creation fee will be paid, you can check it with command `clive show chain`." + " If set to false then new account token will be used.", + ), + json_metadata: str = typer.Option( + "", + "--json-metadata", + help="The json metadata of the new account passed as string. Default is empty string.", + show_default=True, + ), + sign_with: str | None = options.sign_with, + autosign: bool | None = options.autosign, # noqa: FBT001 + broadcast: bool = options.broadcast, # noqa: FBT001 + save_file: str | None = options.save_file, +) -> None: + """ + A simple account creation command that allows to create a new account with authority specified via 4 public keys. + + Thresholds and weights have default values of 1 and 1. + + Example: + 1) positional + clive process account-creation --fee + 2) named options + clive process account-creation --fee --new-account-name --owner \ +--active --posting --memo + """ + # indentation matters in docstring as this is displayed to user as help for cli commands + from clive.__private.cli.commands.process.process_account_creation import ProcessAccountCreation # noqa: PLC0415 + from clive.__private.core.keys.keys import PublicKey # noqa: PLC0415 + + owner_ = cast("PublicKey | None", owner) + active_ = cast("PublicKey | None", active) + posting_ = cast("PublicKey | None", posting) + memo_ = cast("PublicKey | None", memo) + + owner_option_ = cast("PublicKey | None", owner_option) + active_option_ = cast("PublicKey | None", active_option) + posting_option_ = cast("PublicKey | None", posting_option) + memo_option_ = cast("PublicKey | None", memo_option) + + account_creation_command = ProcessAccountCreation( + creator=creator, + new_account_name=EnsureSingleValue("new-account-name").of(new_account_name, new_account_name_option), + fee=fee, + json_metadata=json_metadata, + sign_with=sign_with, + broadcast=broadcast, + save_file=save_file, + autosign=autosign, + ) + account_creation_command.set_keys( + EnsureSingleValue[PublicKey]("owner").of(owner_, owner_option_), + EnsureSingleValue[PublicKey]("active").of(active_, active_option_), + EnsureSingleValue[PublicKey]("posting").of(posting_, posting_option_), + ) + account_creation_command.set_memo_key(EnsureSingleValue[PublicKey]("memo").of(memo_, memo_option_)) + await account_creation_command.run() diff --git a/clive/__private/core/constants/cli.py b/clive/__private/core/constants/cli.py index 612cc8f8dc..6ee2698aef 100644 --- a/clive/__private/core/constants/cli.py +++ b/clive/__private/core/constants/cli.py @@ -23,3 +23,6 @@ UNLOCK_CREATE_PROFILE_SELECT: Final[str] = "create a new profile" PAGE_SIZE_OPTION_MINIMAL_VALUE: Final[int] = 1 PAGE_NUMBER_OPTION_MINIMAL_VALUE: Final[int] = 0 + +DEFAULT_AUTHORITY_THRESHOLD: Final[int] = 1 +DEFAULT_AUTHORITY_WEIGHT: Final[int] = 1 diff --git a/pydoclint-errors-baseline.txt b/pydoclint-errors-baseline.txt index 2171fe88b1..a785eeb757 100644 --- a/pydoclint-errors-baseline.txt +++ b/pydoclint-errors-baseline.txt @@ -139,6 +139,8 @@ clive/__private/cli/process/main.py DOC103: Function `process_transaction`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [already_signed_mode: AlreadySignedModeEnum, autosign: bool | None, broadcast: bool, force: bool, force_unsign: bool, from_file: str, save_file: str | None, sign_with: str | None]. DOC101: Function `process_update_memo_key`: Docstring contains fewer arguments than in function signature. DOC103: Function `process_update_memo_key`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [account_name: str, autosign: bool | None, broadcast: bool, memo_key: str, save_file: str | None, sign_with: str | None]. + DOC101: Function `process_account_creation`: Docstring contains fewer arguments than in function signature. + DOC103: Function `process_account_creation`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [active: str | None, active_option: str | None, autosign: bool | None, broadcast: bool, creator: str, fee: bool, json_metadata: str, memo: str | None, memo_option: str | None, new_account_name: str | None, new_account_name_option: str | None, owner: str | None, owner_option: str | None, posting: str | None, posting_option: str | None, save_file: str | None, sign_with: str | None]. -------------------- clive/__private/cli/process/proxy.py DOC101: Function `process_proxy_set`: Docstring contains fewer arguments than in function signature. -- GitLab From b71c8b732ccc84338bb80c8d04bc7fd1d946abaa Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Thu, 2 Oct 2025 09:59:03 +0000 Subject: [PATCH 5/7] Tests for new command --- .../clive_local_tools/cli/cli_tester.py | 37 +++- .../process/test_process_account_creation.py | 170 ++++++++++++++++++ 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 tests/functional/cli/process/test_process_account_creation.py diff --git a/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py b/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py index aae1a50d27..f830f1a440 100644 --- a/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py +++ b/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from clive.__private.core.types import AlreadySignedMode, AuthorityLevelRegular from clive.__private.core.world import World from clive.__private.models.schemas import PublicKey - from clive_local_tools.cli.command_options import CliOptionT + from clive_local_tools.cli.command_options import CliOptionT, StringConvertibleOptionTypes class CLITester: @@ -576,3 +576,38 @@ class CLITester: def generate_secret_phrase(self) -> Result: return self.__invoke_command_with_options(["generate", "secret-phrase"]) + + def process_claim_new_account_token( # noqa: PLR0913 + self, + *, + account_name: str | None = None, + fee: tt.Asset.HiveT | None = None, + sign_with: str | None = None, + broadcast: bool | None = None, + save_file: Path | None = None, + autosign: bool | None = None, + ) -> Result: + return self.__invoke_command_with_options(["process", "claim", "new-account-token"], **extract_params(locals())) + + def process_account_creation( # noqa: PLR0913 + self, + *args: StringConvertibleOptionTypes, + creator: str | None = None, + new_account_name: str | None = None, + fee: bool | None = None, + json_metadata: str | None = None, + owner: PublicKey | None = None, + active: PublicKey | None = None, + posting: PublicKey | None = None, + memo: PublicKey | None = None, + sign_with: str | None = None, + broadcast: bool | None = None, + save_file: Path | None = None, + autosign: bool | None = None, + ) -> Result: + named_params = locals() + named_params.pop("args") + positional_args = [str(arg) for arg in args] + return self.__invoke_command_with_options( + ["process", "account-creation", *positional_args], **extract_params(named_params) + ) diff --git a/tests/functional/cli/process/test_process_account_creation.py b/tests/functional/cli/process/test_process_account_creation.py new file mode 100644 index 0000000000..a113ea7fa8 --- /dev/null +++ b/tests/functional/cli/process/test_process_account_creation.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from clive.__private.core.constants.cli import DEFAULT_AUTHORITY_THRESHOLD, DEFAULT_AUTHORITY_WEIGHT +from clive.__private.core.keys.keys import PrivateKey +from clive.__private.models.schemas import ( + AccountCreateOperation, + AssetHive, + Authority, + CreateClaimedAccountOperation, + PublicKey, +) +from clive_local_tools.checkers.blockchain_checkers import ( + assert_operations_placed_in_blockchain, + assert_transaction_in_blockchain, +) +from clive_local_tools.testnet_block_log import WATCHED_ACCOUNTS_DATA, WORKING_ACCOUNT_DATA + +if TYPE_CHECKING: + from pathlib import Path + + import test_tools as tt + + from clive.__private.core.types import AuthorityLevel + from clive_local_tools.cli.cli_tester import CLITester + + +NEW_ACCOUNT_NAME: Final[str] = "newaccount" +OTHER_ACCOUNT2: Final[tt.Account] = WATCHED_ACCOUNTS_DATA[1].account +WEIGHT: Final[int] = 213 +MODIFIED_WEIGHT: Final[int] = 214 +WEIGHT_THRESHOLD: Final[int] = 2 + + +def create_public_key_for_role(new_account_name: str = NEW_ACCOUNT_NAME, *, role: AuthorityLevel) -> PublicKey: + private_key = PrivateKey.create_from_seed("seed", new_account_name, role=role) + return private_key.calculate_public_key().value + + +OWNER_KEY: Final[PublicKey] = create_public_key_for_role(role="owner") +OWNER_AUTHORITY: Final[Authority] = Authority( + weight_threshold=DEFAULT_AUTHORITY_THRESHOLD, + account_auths=[], + key_auths=[(OWNER_KEY, DEFAULT_AUTHORITY_WEIGHT)], +) +ACTIVE_KEY: Final[PublicKey] = create_public_key_for_role(role="active") +ACTIVE_AUTHORITY: Final[Authority] = Authority( + weight_threshold=DEFAULT_AUTHORITY_THRESHOLD, + account_auths=[], + key_auths=[(ACTIVE_KEY, DEFAULT_AUTHORITY_WEIGHT)], +) +POSTING_KEY: Final[PublicKey] = create_public_key_for_role(role="posting") +POSTING_AUTHORITY: Final[Authority] = Authority( + weight_threshold=DEFAULT_AUTHORITY_THRESHOLD, + account_auths=[], + key_auths=[(POSTING_KEY, DEFAULT_AUTHORITY_WEIGHT)], +) +MEMO_KEY: Final[PublicKey] = create_public_key_for_role(role="memo") + + +def fetch_account_creation_fee(node: tt.RawNode) -> AssetHive: + fee = node.api.database_api.get_witness_schedule().median_props.account_creation_fee + assert fee is not None, "Account creation fee should be present in database_api.get_witness_schedule" + return fee + + +async def test_account_creation_with_fee(node: tt.RawNode, cli_tester: CLITester) -> None: + # ARRANGE + fee = fetch_account_creation_fee(node) + operation = AccountCreateOperation( + fee=fee, + creator=WORKING_ACCOUNT_DATA.account.name, + new_account_name=NEW_ACCOUNT_NAME, + owner=OWNER_AUTHORITY, + active=ACTIVE_AUTHORITY, + posting=POSTING_AUTHORITY, + memo_key=MEMO_KEY, + json_metadata="", + ) + + # ACT + result = cli_tester.process_account_creation( + new_account_name=operation.new_account_name, + owner=OWNER_KEY, + active=ACTIVE_KEY, + posting=POSTING_KEY, + memo=operation.memo_key, + fee=True, + ) + + # ASSERT + assert_operations_placed_in_blockchain(node, result, operation) + + +async def test_account_creation_with_token(node: tt.RawNode, cli_tester: CLITester) -> None: + # ARRANGE + # in block_log only initminer has account subsidies so we pay fee to get token + fee = fetch_account_creation_fee(node) + cli_tester.process_claim_new_account_token(fee=fee) + operation = CreateClaimedAccountOperation( + creator=WORKING_ACCOUNT_DATA.account.name, + new_account_name=NEW_ACCOUNT_NAME, + owner=OWNER_AUTHORITY, + active=ACTIVE_AUTHORITY, + posting=POSTING_AUTHORITY, + memo_key=MEMO_KEY, + json_metadata="", + ) + + # ACT + result = cli_tester.process_account_creation( + new_account_name=operation.new_account_name, + owner=OWNER_KEY, + active=ACTIVE_KEY, + posting=POSTING_KEY, + memo=operation.memo_key, + ) + + # ASSERT + assert_operations_placed_in_blockchain(node, result, operation) + + +async def test_save_to_file(node: tt.RawNode, cli_tester: CLITester, tmp_path: Path) -> None: + # ARRANGE + trx_path = tmp_path / "account_creation.json" + + # ACT + cli_tester.process_account_creation( + new_account_name=NEW_ACCOUNT_NAME, + owner=OWNER_KEY, + active=ACTIVE_KEY, + posting=POSTING_KEY, + memo=MEMO_KEY, + fee=True, + broadcast=False, + save_file=trx_path, + ) + + # ASSERT + result = cli_tester.process_transaction(from_file=trx_path) + assert_transaction_in_blockchain(node, result) + + +async def test_positional_arguments_are_allowed(node: tt.RawNode, cli_tester: CLITester) -> None: + # ARRANGE + fee = fetch_account_creation_fee(node) + operation = AccountCreateOperation( + fee=fee, + creator=WORKING_ACCOUNT_DATA.account.name, + new_account_name=NEW_ACCOUNT_NAME, + owner=OWNER_AUTHORITY, + active=ACTIVE_AUTHORITY, + posting=POSTING_AUTHORITY, + memo_key=MEMO_KEY, + json_metadata="", + ) + + # ACT + result = cli_tester.process_account_creation( + operation.new_account_name, + OWNER_KEY, + ACTIVE_KEY, + POSTING_KEY, + operation.memo_key, + fee=True, + ) + + # ASSERT + assert_operations_placed_in_blockchain(node, result, operation) -- GitLab From 70a056e3ffbbeca12bf142510595f955bc9218c9 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Mon, 6 Oct 2025 13:59:37 +0000 Subject: [PATCH 6/7] Allow to pass positional arguments in cli test framework --- .../clive_local_tools/cli/cli_tester.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py b/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py index f830f1a440..c850208f0c 100644 --- a/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py +++ b/tests/clive-local-tools/clive_local_tools/cli/cli_tester.py @@ -10,7 +10,7 @@ from .chaining.update_authority import ( UpdateOwnerAuthority, UpdatePostingAuthority, ) -from .command_options import extract_params, kwargs_to_cli_options +from .command_options import extract_params, kwargs_to_cli_options, option_to_string from .exceptions import CLITestCommandError if TYPE_CHECKING: @@ -373,9 +373,15 @@ class CLITester: return self.__invoke_command_with_options(["show", "chain"]) def __invoke_command_with_options( - self, command: list[str], password_stdin: str | None = None, /, **cli_options: CliOptionT + self, + command: list[str], + cli_positional_args: tuple[StringConvertibleOptionTypes, ...] | None = None, + password_stdin: str | None = None, + /, + **cli_named_args: CliOptionT, ) -> Result: - full_command = [*command, *kwargs_to_cli_options(**cli_options)] + positional = [option_to_string(arg) for arg in cli_positional_args] if cli_positional_args is not None else [] + full_command = [*command, *positional, *kwargs_to_cli_options(**cli_named_args)] return self.invoke_raw_command(full_command, password_stdin) def process_transfer( # noqa: PLR0913 @@ -423,7 +429,7 @@ class CLITester: ) -> Result: named_params = locals() named_params.pop("password_stdin") - return self.__invoke_command_with_options(["unlock"], password_stdin, **extract_params(named_params)) + return self.__invoke_command_with_options(["unlock"], None, password_stdin, **extract_params(named_params)) def lock(self) -> Result: return self.__invoke_command_with_options(["lock"], **extract_params(locals())) @@ -468,7 +474,7 @@ class CLITester: named_params = locals() named_params.pop("password_stdin") return self.__invoke_command_with_options( - ["configure", "profile", "create"], password_stdin, **extract_params(named_params) + ["configure", "profile", "create"], None, password_stdin, **extract_params(named_params) ) def configure_profile_delete(self, *, profile_name: str, force: bool | None = None) -> Result: @@ -564,11 +570,11 @@ class CLITester: named_params = locals() named_params.pop("password_stdin") return self.__invoke_command_with_options( - ["generate", "key-from-seed"], password_stdin, **extract_params(named_params) + ["generate", "key-from-seed"], None, password_stdin, **extract_params(named_params) ) def generate_public_key(self, *, password_stdin: str | None = None) -> Result: - return self.__invoke_command_with_options(["generate", "public-key"], password_stdin) + return self.__invoke_command_with_options(["generate", "public-key"], None, password_stdin) def generate_random_key(self, *, key_pairs: int | None = None) -> Result: named_params = locals() @@ -605,9 +611,6 @@ class CLITester: save_file: Path | None = None, autosign: bool | None = None, ) -> Result: - named_params = locals() - named_params.pop("args") - positional_args = [str(arg) for arg in args] return self.__invoke_command_with_options( - ["process", "account-creation", *positional_args], **extract_params(named_params) + ["process", "account-creation"], args, **extract_params(locals(), "args") ) -- GitLab From dffbe8d6d3e60f09ded2f9a0c4c7c2639d8af926 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 7 Oct 2025 12:02:04 +0000 Subject: [PATCH 7/7] Fix typo --- clive/__private/cli/common/parameters/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clive/__private/cli/common/parameters/arguments.py b/clive/__private/cli/common/parameters/arguments.py index 2ed23d5e34..4113852a97 100644 --- a/clive/__private/cli/common/parameters/arguments.py +++ b/clive/__private/cli/common/parameters/arguments.py @@ -22,7 +22,7 @@ working_account_template = typer.Argument( account_name = modified_param(working_account_template) -profile_name = typer.Argument(..., help=f"The profile to use. ({REQUIRED_AS_ARG_OR_OPTION}") +profile_name = typer.Argument(..., help=f"The profile to use. ({REQUIRED_AS_ARG_OR_OPTION})") owner_key = typer.Argument( None, -- GitLab