From 60cb9271d2cd6555931524a7bbd298e8bd6c414b Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 14 Oct 2025 12:35:21 +0000 Subject: [PATCH 01/10] Additional models in schemas for operations updating witness --- clive/__private/models/schemas.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/clive/__private/models/schemas.py b/clive/__private/models/schemas.py index 424bc45912..f13068943b 100644 --- a/clive/__private/models/schemas.py +++ b/clive/__private/models/schemas.py @@ -112,6 +112,7 @@ __all__ = [ # noqa: RUF022 # basic fields "AccountName", "ChainId", + "Hex", "HiveDateTime", "HiveInt", "JsonString", @@ -120,12 +121,14 @@ __all__ = [ # noqa: RUF022 "TransactionId", "Uint16t", "Uint32t", + "WitnessPropsSerializedKey", # compound models "Account", "Authority", "ChangeRecoveryAccountRequest", "DeclineVotingRightsRequest", "HbdExchangeRate", + "LegacyChainProperties", "Manabar", "PriceFeed", "Proposal", @@ -198,8 +201,15 @@ if TYPE_CHECKING: from schemas.errors import DecodeError, ValidationError from schemas.fields.assets import AssetHbd, AssetHive, AssetVests from schemas.fields.basic import AccountName, PublicKey - from schemas.fields.compound import Authority, HbdExchangeRate, Manabar, Price, Proposal - from schemas.fields.hex import Sha256, Signature, TransactionId + from schemas.fields.compound import ( + Authority, + HbdExchangeRate, + LegacyChainProperties, + Manabar, + Price, + Proposal, + ) + from schemas.fields.hex import Hex, Sha256, Signature, TransactionId from schemas.fields.hive_datetime import HiveDateTime from schemas.fields.hive_int import HiveInt from schemas.fields.integers import Uint16t, Uint32t @@ -264,6 +274,7 @@ if TYPE_CHECKING: HF26RepresentationRecurrentTransferPairIdOperationExtension, ) from schemas.operations.recurrent_transfer_operation import RecurrentTransferOperation + from schemas.operations.witness_set_properties_operation import WitnessPropsSerializedKey from schemas.policies import ( ExtraFieldsPolicy, MissingFieldsInGetConfigPolicy, @@ -417,6 +428,7 @@ __getattr__ = lazy_module_factory( ), *aggregate_same_import( "Authority", + "LegacyChainProperties", "Manabar", ("Price", "PriceFeed"), module="schemas.fields.compound", @@ -425,6 +437,7 @@ __getattr__ = lazy_module_factory( "Signature", "TransactionId", ("Sha256", "ChainId"), + "Hex", module="schemas.fields.hex", ), *aggregate_same_import( @@ -455,7 +468,10 @@ __getattr__ = lazy_module_factory( ("schemas.base", "field"), ("schemas.apis.rc_api", "FindRcAccounts"), ("schemas.apis.transaction_status_api", "FindTransaction", "TransactionStatus"), - ("schemas.apis.account_history_api", "GetAccountHistory"), + *aggregate_same_import( + "GetAccountHistory", + module="schemas.apis.account_history_api", + ), ( "schemas.operations.extensions.representation_types", "HF26RepresentationRecurrentTransferPairIdOperationExtension", @@ -473,5 +489,9 @@ __getattr__ = lazy_module_factory( "RecurrentTransferPairId", "RecurrentTransferPairIdExtension", ), + ( + "schemas.operations.witness_set_properties_operation", + "WitnessPropsSerializedKey", + ), ("schemas.transaction", "Transaction"), ) -- GitLab From dbfbe62c6da34ff8563c3fc2186d8fc582cb86b1 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 14 Oct 2025 12:37:23 +0000 Subject: [PATCH 02/10] Expose creating witness properties in iwax interface --- clive/__private/core/iwax.py | 58 ++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/clive/__private/core/iwax.py b/clive/__private/core/iwax.py index 5827909d28..4e3a68361a 100644 --- a/clive/__private/core/iwax.py +++ b/clive/__private/core/iwax.py @@ -3,12 +3,13 @@ from __future__ import annotations import datetime from collections.abc import Callable from functools import wraps -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import TYPE_CHECKING, Any, Protocol, cast, get_args import wax from clive.__private.core.constants.precision import HIVE_PERCENT_PRECISION_DOT_PLACES from clive.__private.core.decimal_conventer import DecimalConverter -from clive.__private.core.percent_conversions import hive_percent_to_percent +from clive.__private.core.percent_conversions import hive_percent_to_percent, percent_to_hive_percent +from clive.__private.models.schemas import AccountName, WitnessPropsSerializedKey from clive.exceptions import CliveError if TYPE_CHECKING: @@ -16,7 +17,7 @@ if TYPE_CHECKING: from clive.__private.core.keys import PrivateKey, PublicKey from clive.__private.models.asset import Asset - from clive.__private.models.schemas import OperationUnion, PriceFeed + from clive.__private.models.schemas import Hex, OperationUnion, PriceFeed from clive.__private.models.transaction import Transaction @@ -269,3 +270,54 @@ def generate_password_based_private_key( def suggest_brain_key() -> str: result = wax.suggest_brain_key() return result.brain_key.decode() + + +def create_witness_set_properties( # noqa: PLR0913 + wax_interface: wax.IWaxBaseInterface, + owner: str, + key: PublicKey, + new_signing_key: PublicKey | None = None, + account_creation_fee: Asset.Hive | None = None, + url: str | None = None, + hbd_exchange_rate: Asset.Hbd | None = None, + maximum_block_size: int | None = None, + hbd_interest_rate: Decimal | None = None, + account_subsidy_budget: int | None = None, + account_subsidy_decay: int | None = None, +) -> list[tuple[WitnessPropsSerializedKey, Hex]]: + from clive.__private.models.asset import Asset # noqa: PLC0415 + from wax.complex_operations.witness_set_properties import ( # noqa: PLC0415 + WitnessSetProperties, + WitnessSetPropertiesData, + ) + + wax_operation = WitnessSetProperties( + data=WitnessSetPropertiesData( + owner=AccountName(owner), + witness_signing_key=key.value, + new_signing_key=new_signing_key.value if new_signing_key is not None else None, + account_creation_fee=account_creation_fee.as_serialized_nai() if account_creation_fee is not None else None, + url=url, + hbd_exchange_rate=wax.complex_operations.witness_set_properties.HbdExchangeRate( + base=hbd_exchange_rate.as_serialized_nai(), + quote=Asset.hive(1).as_serialized_nai(), + ) + if hbd_exchange_rate + else None, + maximum_block_size=maximum_block_size, + hbd_interest_rate=percent_to_hive_percent(hbd_interest_rate) if hbd_interest_rate else None, + account_subsidy_budget=account_subsidy_budget, + account_subsidy_decay=account_subsidy_decay, + ) + ) + first = next(iter(wax_operation.finalize(wax_interface))) + props = first.props + schemas_properties_model: list[tuple[WitnessPropsSerializedKey, Hex]] = [] + + def append_optional_property(name: WitnessPropsSerializedKey) -> None: + if property_value := props.get(name): + schemas_properties_model.append((name, property_value)) + + for property_name in get_args(WitnessPropsSerializedKey): + append_optional_property(property_name) + return schemas_properties_model -- GitLab From 21d4d736b05c8a2323eeaa8407f3ede6c7058441 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 14 Oct 2025 12:41:48 +0000 Subject: [PATCH 03/10] Raise pretty exception if in `clive show witness` witness does not exist --- clive/__private/cli/commands/show/show_witness.py | 8 ++++++-- clive/__private/cli/exceptions.py | 13 +++++++++++++ clive/__private/core/commands/find_witness.py | 9 ++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/clive/__private/cli/commands/show/show_witness.py b/clive/__private/cli/commands/show/show_witness.py index 3ed2679161..759ff25956 100644 --- a/clive/__private/cli/commands/show/show_witness.py +++ b/clive/__private/cli/commands/show/show_witness.py @@ -5,7 +5,9 @@ from dataclasses import dataclass from rich.table import Table from clive.__private.cli.commands.abc.world_based_command import WorldBasedCommand +from clive.__private.cli.exceptions import CLIWitnessNotFoundError from clive.__private.cli.print_cli import print_cli +from clive.__private.core.commands.find_witness import WitnessNotFoundError from clive.__private.core.formatters.data_labels import HBD_EXCHANGE_RATE_LABEL, HBD_SAVINGS_APR_LABEL from clive.__private.core.formatters.humanize import ( humanize_hbd_exchange_rate, @@ -21,8 +23,10 @@ class ShowWitness(WorldBasedCommand): name: str async def _run(self) -> None: - wrapper = await self.world.commands.find_witness(witness_name=self.name) - witness = wrapper.result_or_raise + try: + witness = (await self.world.commands.find_witness(witness_name=self.name)).result_or_raise + except WitnessNotFoundError as err: + raise CLIWitnessNotFoundError(self.name) from err gdpo = await self.world.node.api.database_api.get_dynamic_global_properties() votes = humanize_votes_with_comma(witness.votes, gdpo) diff --git a/clive/__private/cli/exceptions.py b/clive/__private/cli/exceptions.py index 22c60ff0f4..46f36a8813 100644 --- a/clive/__private/cli/exceptions.py +++ b/clive/__private/cli/exceptions.py @@ -504,3 +504,16 @@ class CLIAccountDoesNotExistsOnNodeError(CLIPrettyError): def __init__(self, account_name: str, http_endpoint: HttpUrl) -> None: message = f"Account `{account_name}` doesn't exist on node `{http_endpoint}`." super().__init__(message, errno.EINVAL) + + +class CLIWitnessNotFoundError(CLIPrettyError): + """ + Raise when witness was not found. + + Args: + name: Name of witness that couldn't be found. + """ + + def __init__(self, name: str) -> None: + message = f"Witness `{name}` was not found." + super().__init__(message, errno.EINVAL) diff --git a/clive/__private/core/commands/find_witness.py b/clive/__private/core/commands/find_witness.py index d296a6398f..63ee46fed2 100644 --- a/clive/__private/core/commands/find_witness.py +++ b/clive/__private/core/commands/find_witness.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING +from clive.__private.core.commands.abc.command import Command, CommandError from clive.__private.core.commands.abc.command_with_result import CommandWithResult from clive.__private.models.schemas import FindWitnesses, Witness @@ -10,6 +11,11 @@ if TYPE_CHECKING: from clive.__private.core.node import Node +class WitnessNotFoundError(CommandError): + def __init__(self, command: Command, witness_name: str) -> None: + super().__init__(command, f"Witness '{witness_name}' was not found.") + + @dataclass(kw_only=True) class FindWitness(CommandWithResult[Witness]): node: Node @@ -17,7 +23,8 @@ class FindWitness(CommandWithResult[Witness]): async def _execute(self) -> None: response: FindWitnesses = await self.node.api.database_api.find_witnesses(owners=[self.witness_name]) - assert len(response.witnesses) == 1 + if not response.witnesses: + raise WitnessNotFoundError(self, self.witness_name) first_witness = response.witnesses[0] assert first_witness.owner == self.witness_name self._result = first_witness -- GitLab From 833e1d5471307c7534e9f82d9f29608cee731fb9 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 14 Oct 2025 12:43:31 +0000 Subject: [PATCH 04/10] Fix bug displaying interest rate as None if interest rate is lesser then 1 --- clive/__private/cli/commands/show/show_witness.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/clive/__private/cli/commands/show/show_witness.py b/clive/__private/cli/commands/show/show_witness.py index 759ff25956..6df25577f4 100644 --- a/clive/__private/cli/commands/show/show_witness.py +++ b/clive/__private/cli/commands/show/show_witness.py @@ -34,9 +34,10 @@ class ShowWitness(WorldBasedCommand): account_creation_fee: str | None = None if witness.props.account_creation_fee: account_creation_fee = witness.props.account_creation_fee.as_legacy() - hbd_savings_apr: str | None = None - if witness.props.hbd_interest_rate: - hbd_savings_apr = humanize_hbd_savings_apr(hive_percent_to_percent(witness.props.hbd_interest_rate)) + assert witness.props.hbd_interest_rate is not None, ( + "Hbd interest rate must be set in response of `find_witnesses`." + ) # TODO: remove after https://gitlab.syncad.com/hive/schemas/-/issues/46 is fixed + hbd_savings_apr = humanize_hbd_savings_apr(hive_percent_to_percent(witness.props.hbd_interest_rate)) props_as_legacy = witness.props.copy(exclude={"account_creation_fee", "hbd_interest_rate"}) table = Table(title=f"Details of `{self.name}` witness", show_header=False) -- GitLab From 5221bbf68a7d85b3504be53a258689b617988fa4 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 14 Oct 2025 12:45:05 +0000 Subject: [PATCH 05/10] Cli command update-witness implementation --- .../process/process_update_witness.py | 194 ++++++++++++++++++ clive/__private/cli/common/parsers.py | 6 + clive/__private/cli/process/main.py | 74 +++++++ pydoclint-errors-baseline.txt | 2 + 4 files changed, 276 insertions(+) create mode 100644 clive/__private/cli/commands/process/process_update_witness.py diff --git a/clive/__private/cli/commands/process/process_update_witness.py b/clive/__private/cli/commands/process/process_update_witness.py new file mode 100644 index 0000000000..a32fee0cf6 --- /dev/null +++ b/clive/__private/cli/commands/process/process_update_witness.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, override + +from clive.__private.cli.commands.abc.perform_actions_on_transaction_command import PerformActionsOnTransactionCommand +from clive.__private.cli.exceptions import CLIPrettyError, CLIWitnessNotFoundError +from clive.__private.core import iwax +from clive.__private.core.commands.find_witness import WitnessNotFoundError +from clive.__private.core.keys.keys import PublicKey +from clive.__private.core.percent_conversions import percent_to_hive_percent +from clive.__private.models.asset import Asset +from clive.__private.models.schemas import ( + AccountName, + FeedPublishOperation, + HbdExchangeRate, + LegacyChainProperties, + Witness, + WitnessSetPropertiesOperation, + WitnessUpdateOperation, +) + +if TYPE_CHECKING: + from decimal import Decimal + + from clive.__private.core.ensure_transaction import TransactionConvertibleType + from clive.__private.models.schemas import OperationBase + + +class RequiresWitnessSetPropertiesOperationError(CLIPrettyError): + """ + Raised when operation must be signed with active authority but signing with witness key is requested. + + Args: + property_name: Name of property that changing requires signing with witness key + and operation witness_set_properties_operation. + """ + + def __init__(self, property_name: str) -> None: + super().__init__( + f"Changing property `{property_name}` can be performed in 'witness_set_properties_operation'" + " and requires signing with witness signing key.\n" + "You can skip the '--use-active-authority' option or explicitly use '--use-witness-key'." + ) + + +@dataclass(kw_only=True) +class ProcessUpdateWitness(PerformActionsOnTransactionCommand): + owner: str + use_witness_key: bool + account_creation_fee: Asset.Hive | None + maximum_block_size: int | None + hbd_interest_rate: Decimal | None + account_subsidy_budget: int | None + account_subsidy_decay: int | None + new_signing_key: PublicKey | None + hbd_exchange_rate: Asset.Hbd | None + url: str | None + + _witness: Witness | None = field(init=False, default=None) + + @property + def use_active_authority(self) -> bool: + return not self.use_witness_key + + @property + def witness_ensure(self) -> Witness: + assert self._witness, "Witness data was not fetched yet" + return self._witness + + @override + async def fetch_data(self) -> None: + try: + self._witness = (await self.world.commands.find_witness(witness_name=self.owner)).result_or_raise + except WitnessNotFoundError as err: + raise CLIWitnessNotFoundError(self.owner) from err + + @override + async def _get_transaction_content(self) -> TransactionConvertibleType: + operations: list[OperationBase] = [] + if self._needs_feed_publish_operation: + operations.append(self._create_feed_publish_operation()) + if self._needs_witness_update_operation: + operations.append(self._create_witness_update_operation()) + if self._needs_witness_set_properties_operation: + operations.append(self._create_witness_set_properties_operation()) + if not bool(operations): + raise CLIPrettyError("Transaction with no changes to authority cannot be created.") + return operations + + @override + async def validate(self) -> None: + self._validate_requirements_for_witness_set_propertues_operation() + self._validate_if_broadcasting_signed_transaction() + self._validate_not_empty() + await super().validate() + + def _validate_not_empty(self) -> None: + is_operation_required = any( + [ + self._needs_feed_publish_operation, + self._needs_witness_update_operation, + self._needs_witness_set_properties_operation, + ] + ) + if not is_operation_required: + raise CLIPrettyError( + "Transaction with no changes to witness cannot be created. Use '--help' flag to display help." + ) + + def _validate_requirements_for_witness_set_propertues_operation(self) -> None: + if self.use_active_authority and self.account_subsidy_budget is not None: + raise RequiresWitnessSetPropertiesOperationError("account-subsidy-budget") + if self.use_active_authority and self.account_subsidy_decay is not None: + raise RequiresWitnessSetPropertiesOperationError("account-subsidy-decay") + + @property + def _needs_feed_publish_operation(self) -> bool: + return self.use_active_authority and self.hbd_exchange_rate is not None + + @property + def _needs_witness_set_properties_operation(self) -> bool: + are_witness_set_properties_options_required: bool = ( + self.account_creation_fee is not None + or self.maximum_block_size is not None + or self.hbd_interest_rate is not None + or self.new_signing_key is not None + or self.url is not None + or self.hbd_exchange_rate is not None + or self.account_subsidy_budget is not None + or self.account_subsidy_decay is not None + ) + return self.use_witness_key and are_witness_set_properties_options_required + + @property + def _needs_witness_update_operation(self) -> bool: + are_witness_update_options_required: bool = ( + self.account_creation_fee is not None + or self.maximum_block_size is not None + or self.hbd_interest_rate is not None + or self.new_signing_key is not None + or self.url is not None + ) + return self.use_active_authority and are_witness_update_options_required + + def _create_feed_publish_operation(self) -> FeedPublishOperation: + assert self.hbd_exchange_rate is not None, ( + "Feed publish should be created only if command requires changing hbd_exchange_rate" + ) + return FeedPublishOperation( + publisher=AccountName(self.owner), + exchange_rate=HbdExchangeRate(base=self.hbd_exchange_rate, quote=Asset.hive(1)), + ) + + def _create_witness_update_operation(self) -> WitnessUpdateOperation: + witness = self.witness_ensure + # TODO: remove those 3 assertions after https://gitlab.syncad.com/hive/schemas/-/issues/46 is fixed + assert witness.props.account_creation_fee is not None, "Account creation fee must is always set" + assert witness.props.maximum_block_size is not None, "Maximum block size is always set" + assert witness.props.hbd_interest_rate is not None, "Hbd interest rate is always set" + hbd_interest_rate = ( + percent_to_hive_percent(self.hbd_interest_rate) + if self.hbd_interest_rate + else witness.props.hbd_interest_rate + ) + return WitnessUpdateOperation( + owner=AccountName(self.owner), + url=self.url or witness.url, + block_signing_key=self.new_signing_key.value if self.new_signing_key else witness.signing_key, + props=LegacyChainProperties( + account_creation_fee=self.account_creation_fee or witness.props.account_creation_fee, + maximum_block_size=self.maximum_block_size or witness.props.maximum_block_size, + hbd_interest_rate=hbd_interest_rate, + ), + ) + + def _create_witness_set_properties_operation(self) -> WitnessSetPropertiesOperation: + schemas_props = iwax.create_witness_set_properties( + wax_interface=self.world.wax_interface, + owner=self.owner, + key=PublicKey(value=self.witness_ensure.signing_key), + new_signing_key=self.new_signing_key, + account_creation_fee=self.account_creation_fee, + url=self.url, + hbd_exchange_rate=self.hbd_exchange_rate, + maximum_block_size=self.maximum_block_size, + hbd_interest_rate=self.hbd_interest_rate, + account_subsidy_budget=self.account_subsidy_budget, + account_subsidy_decay=self.account_subsidy_decay, + ) + return WitnessSetPropertiesOperation( + owner=AccountName(self.owner), + props=schemas_props, + ) diff --git a/clive/__private/cli/common/parsers.py b/clive/__private/cli/common/parsers.py index d3e35eabe1..08ad4a9cad 100644 --- a/clive/__private/cli/common/parsers.py +++ b/clive/__private/cli/common/parsers.py @@ -79,6 +79,12 @@ def voting_asset(raw: str) -> Asset.VotingT: return _parse_asset(raw, *get_args(Asset.VotingT)) # type: ignore[no-any-return] +def hbd_asset(raw: str) -> Asset.Hbd: + from clive.__private.models.asset import Asset # noqa: PLC0415 + + return _parse_asset(raw, Asset.Hbd) + + def hive_asset(raw: str) -> Asset.Hive: from clive.__private.models.asset import Asset # noqa: PLC0415 diff --git a/clive/__private/cli/process/main.py b/clive/__private/cli/process/main.py index cb5e6e2076..fcca5a1e56 100644 --- a/clive/__private/cli/process/main.py +++ b/clive/__private/cli/process/main.py @@ -1,5 +1,6 @@ from __future__ import annotations +from decimal import Decimal # noqa: TC003 from functools import partial from typing import TYPE_CHECKING, cast @@ -11,6 +12,7 @@ from clive.__private.cli.common.parameters import argument_related_options, argu from clive.__private.cli.common.parameters.ensure_single_value import ( EnsureSingleValue, ) +from clive.__private.cli.common.parsers import decimal_percent, hbd_asset, hive_asset, public_key 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 @@ -219,3 +221,75 @@ async def process_account_creation( # noqa: PLR0913 ) account_creation_command.set_memo_key(EnsureSingleValue[PublicKey]("memo").of(memo_, memo_option_)) await account_creation_command.run() + + +@process.command(name="update-witness") +async def process_witness_update( # noqa: PLR0913 + witness_name: str = modified_param( + options.working_account_template, + help="Witness account name, aka owner in the operation. (default is working account of profile).", + ), + use_witness_key: bool = typer.Option( # noqa: FBT001 + True, # noqa: FBT003 + "--use-witness-key/--use-active-authority", + help=( + "There are 3 operations for updating the witness properties. By default," + " `witness_set_properties` is used and a witness key is required to sign such a transaction." + " Alternatively, active authority can be used with operations `witness_update` and `feed_publish`." + ), + ), + account_creation_fee: str | None = typer.Option( + None, + parser=hive_asset, + help="Fee paid by creator of new account, applies also to price of new account tokens.", + ), + maximum_block_size: int | None = typer.Option( + None, + help="Block size (in bytes) that cannot be exceeded.", + ), + hbd_interest_rate: Decimal | None = typer.Option( + None, + help="Interest paid when hbd is in savings, in percent (0.00-100.00).", + parser=decimal_percent, + ), + account_subsidy_budget: int | None = typer.Option(None, help="Requires to be signed with witness key."), + account_subsidy_decay: int | None = typer.Option(None, help="Requires to be signed with witness key."), + new_signing_key: str | None = typer.Option( + None, + help="New witness public key.", + parser=public_key, + ), + hbd_exchange_rate: str | None = typer.Option( + None, + parser=hbd_asset, + help="Updated price value of 1 HIVE in HBD.", + ), + url: str | None = typer.Option(None, help="New witness url."), + 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: + """Update witness properties with witness update, feed publish or witness set properties operation.""" + from clive.__private.cli.commands.process.process_update_witness import ProcessUpdateWitness # noqa: PLC0415 + + account_creation_fee_ = cast("Asset.Hive | None", account_creation_fee) + new_signing_key_ = cast("PublicKey | None", new_signing_key) + hbd_exchange_rate_ = cast("Asset.Hbd | None", hbd_exchange_rate) + operation = ProcessUpdateWitness( + owner=witness_name, + use_witness_key=use_witness_key, + account_creation_fee=account_creation_fee_, + maximum_block_size=maximum_block_size, + hbd_interest_rate=hbd_interest_rate, + account_subsidy_budget=account_subsidy_budget, + account_subsidy_decay=account_subsidy_decay, + new_signing_key=new_signing_key_, + hbd_exchange_rate=hbd_exchange_rate_, + url=url, + sign_with=sign_with, + broadcast=broadcast, + save_file=save_file, + autosign=autosign, + ) + await operation.run() diff --git a/pydoclint-errors-baseline.txt b/pydoclint-errors-baseline.txt index f7fd54f290..b09655094e 100644 --- a/pydoclint-errors-baseline.txt +++ b/pydoclint-errors-baseline.txt @@ -141,6 +141,8 @@ clive/__private/cli/process/main.py 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]. + DOC101: Function `process_witness_update`: Docstring contains fewer arguments than in function signature. + DOC103: Function `process_witness_update`: 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_creation_fee: str | None, account_subsidy_budget: int | None, account_subsidy_decay: int | None, autosign: bool | None, broadcast: bool, hbd_exchange_rate: str | None, hbd_interest_rate: Decimal | None, maximum_block_size: int | None, new_signing_key: str | None, save_file: str | None, sign_with: str | None, url: str | None, use_witness_key: bool, witness_name: str]. -------------------- clive/__private/cli/process/proxy.py DOC101: Function `process_proxy_set`: Docstring contains fewer arguments than in function signature. -- GitLab From 5e92aafc0f52e7279cc3a1b6176d5ddd9aa8d4d0 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 14 Oct 2025 12:47:17 +0000 Subject: [PATCH 06/10] Tests for command update-witness --- .../checkers/blockchain_checkers.py | 43 ++- .../clive_local_tools/cli/cli_tester.py | 26 ++ .../clive_local_tools/cli/command_options.py | 5 +- .../clive_local_tools/data/constants.py | 3 + .../testnet_block_log/__init__.py | 2 + .../testnet_block_log/constants.py | 2 + tests/functional/cli/conftest.py | 21 ++ .../process/test_process_update_witness.py | 314 ++++++++++++++++++ 8 files changed, 408 insertions(+), 8 deletions(-) create mode 100644 tests/functional/cli/process/test_process_update_witness.py diff --git a/tests/clive-local-tools/clive_local_tools/checkers/blockchain_checkers.py b/tests/clive-local-tools/clive_local_tools/checkers/blockchain_checkers.py index 5b46908077..0cf2766918 100644 --- a/tests/clive-local-tools/clive_local_tools/checkers/blockchain_checkers.py +++ b/tests/clive-local-tools/clive_local_tools/checkers/blockchain_checkers.py @@ -11,7 +11,7 @@ from clive_local_tools.helpers import get_transaction_id_from_output if TYPE_CHECKING: import test_tools as tt - from clive.__private.models.schemas import OperationUnion + from clive.__private.models.schemas import GetTransaction, OperationBase, OperationUnion def _ensure_transaction_id(trx_id_or_result: Result | str) -> str: @@ -20,6 +20,17 @@ def _ensure_transaction_id(trx_id_or_result: Result | str) -> str: return trx_id_or_result +def _get_transaction( + node: tt.RawNode, trx_id_or_result: str | Result, *, wait_for_the_next_block: bool +) -> GetTransaction: + assert_transaction_in_blockchain(node, trx_id_or_result, wait_for_the_next_block=wait_for_the_next_block) + transaction_id = _ensure_transaction_id(trx_id_or_result) + return node.api.account_history.get_transaction( + id=transaction_id, + include_reversible=True, # type: ignore[call-arg] # TODO: id -> id_ after helpy bug fixed + ) + + def assert_transaction_in_blockchain( node: tt.RawNode, trx_id_or_result: str | Result, *, wait_for_the_next_block: bool = True ) -> None: @@ -39,12 +50,7 @@ def assert_operations_placed_in_blockchain( *expected_operations: OperationUnion, wait_for_the_next_block: bool = True, ) -> None: - assert_transaction_in_blockchain(node, trx_id_or_result, wait_for_the_next_block=wait_for_the_next_block) - transaction_id = _ensure_transaction_id(trx_id_or_result) - transaction = node.api.account_history.get_transaction( - id=transaction_id, - include_reversible=True, # type: ignore[call-arg] # TODO: id -> id_ after helpy bug fixed - ) + transaction = _get_transaction(node, trx_id_or_result, wait_for_the_next_block=wait_for_the_next_block) operations_to_check = list(expected_operations) for operation_representation in transaction.operations: @@ -59,3 +65,26 @@ def assert_operations_placed_in_blockchain( f"{transaction}." ) assert not operations_to_check, message + + +def assert_operation_type_in_blockchain( + node: tt.RawNode, + trx_id_or_result: str | Result, + *expected_types: type[OperationBase], + wait_for_the_next_block: bool = True, +) -> None: + transaction = _get_transaction(node, trx_id_or_result, wait_for_the_next_block=wait_for_the_next_block) + types_to_check = list(expected_types) + + for operation_representation in transaction.operations: + operation_type = type(operation_representation.value) + if operation_type in types_to_check: + types_to_check.remove(operation_type) + + message = ( + "Operation types missing in blockchain.\n" + f"Types: {types_to_check}\n" + "were not found in the transaction:\n" + f"{transaction}." + ) + assert not types_to_check, message 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 c850208f0c..8fd8062b8c 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 @@ -14,6 +14,7 @@ from .command_options import extract_params, kwargs_to_cli_options, option_to_st from .exceptions import CLITestCommandError if TYPE_CHECKING: + from decimal import Decimal from pathlib import Path from click.testing import Result @@ -614,3 +615,28 @@ class CLITester: return self.__invoke_command_with_options( ["process", "account-creation"], args, **extract_params(locals(), "args") ) + + def process_update_witness( # noqa: PLR0913 + self, + *, + owner: str | None = None, + use_witness_key: bool | None = None, + account_creation_fee: tt.Asset.HiveT | None = None, + maximum_block_size: int | None = None, + hbd_interest_rate: Decimal | None = None, + account_subsidy_budget: int | None = None, + account_subsidy_decay: int | None = None, + new_signing_key: PublicKey | None = None, + hbd_exchange_rate: tt.Asset.HbdT | None = None, + url: str | None = None, + sign_with: str | None = None, + broadcast: bool | None = None, + save_file: Path | None = None, + autosign: bool | None = None, + ) -> Result: + named_params = locals() + # the actual option names are `--use-witness-key/--use-active-authority` + if use_witness_key is False: + named_params.pop("use_witness_key") + named_params["use_active_authority"] = True + return self.__invoke_command_with_options(["process", "update-witness"], **extract_params(named_params)) diff --git a/tests/clive-local-tools/clive_local_tools/cli/command_options.py b/tests/clive-local-tools/clive_local_tools/cli/command_options.py index e524cef8db..a1c4e1add3 100644 --- a/tests/clive-local-tools/clive_local_tools/cli/command_options.py +++ b/tests/clive-local-tools/clive_local_tools/cli/command_options.py @@ -1,6 +1,7 @@ from __future__ import annotations from copy import copy, deepcopy +from decimal import Decimal from pathlib import Path import test_tools as tt @@ -9,7 +10,7 @@ from clive.__private.models.schemas import PublicKey from .exceptions import UnsupportedOptionError -type StringConvertibleOptionTypes = str | int | tt.Asset.AnyT | PublicKey | Path +type StringConvertibleOptionTypes = str | int | Decimal | tt.Asset.AnyT | PublicKey | Path type CliOptionT = bool | StringConvertibleOptionTypes | list[StringConvertibleOptionTypes] | None @@ -18,6 +19,8 @@ def option_to_string(value: StringConvertibleOptionTypes) -> str: return value if isinstance(value, int): return str(value) + if isinstance(value, Decimal): + return str(value) if isinstance(value, Path): return str(value) if isinstance(value, tt.Asset.AnyT): diff --git a/tests/clive-local-tools/clive_local_tools/data/constants.py b/tests/clive-local-tools/clive_local_tools/data/constants.py index 6a2e3eb11b..99ccbab4d6 100644 --- a/tests/clive-local-tools/clive_local_tools/data/constants.py +++ b/tests/clive-local-tools/clive_local_tools/data/constants.py @@ -12,6 +12,7 @@ from clive.__private.settings import clive_prefixed_envvar from clive_local_tools.testnet_block_log import ( ALT_WORKING_ACCOUNT1_DATA, ALT_WORKING_ACCOUNT2_DATA, + WITNESS_ACCOUNT, WORKING_ACCOUNT_DATA, ) @@ -27,3 +28,5 @@ BEEKEEPER_REMOTE_ADDRESS_ENV_NAME: Final[str] = clive_prefixed_envvar(BEEKEEPER_ BEEKEEPER_SESSION_TOKEN_ENV_NAME: Final[str] = clive_prefixed_envvar(BEEKEEPER_SESSION_TOKEN) NODE_CHAIN_ID_ENV_NAME: Final[str] = clive_prefixed_envvar(NODE_CHAIN_ID) SECRETS_NODE_ADDRESS_ENV_NAME: Final[str] = clive_prefixed_envvar(SECRETS_NODE_ADDRESS) +WITNESS_ACCOUNT_PASSWORD: Final[str] = WITNESS_ACCOUNT.name +WITNESS_ACCOUNT_KEY_ALIAS: Final[str] = f"{WITNESS_ACCOUNT.name}_key" diff --git a/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py b/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py index e61fc49a1d..5705721969 100644 --- a/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py +++ b/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py @@ -13,6 +13,7 @@ from .constants import ( UNKNOWN_ACCOUNT, WATCHED_ACCOUNTS_DATA, WATCHED_ACCOUNTS_NAMES, + WITNESS_ACCOUNT, WITNESSES, WORKING_ACCOUNT_DATA, WORKING_ACCOUNT_NAME, @@ -33,6 +34,7 @@ __all__ = [ "WATCHED_ACCOUNTS_DATA", "WATCHED_ACCOUNTS_NAMES", "WITNESSES", + "WITNESS_ACCOUNT", "WORKING_ACCOUNT_DATA", "WORKING_ACCOUNT_NAME", "get_alternate_chain_spec", diff --git a/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py b/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py index 6ed396f685..f134dd7e08 100644 --- a/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py +++ b/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py @@ -70,3 +70,5 @@ KNOWN_ACCOUNTS: Final[list[str]] = [ ALT_WORKING_ACCOUNT2_NAME, *KNOWN_EXCHANGES_NAMES, ] + +WITNESS_ACCOUNT: Final[tt.Account] = WITNESSES[0] diff --git a/tests/functional/cli/conftest.py b/tests/functional/cli/conftest.py index 1e6a4c0aa7..c457da154f 100644 --- a/tests/functional/cli/conftest.py +++ b/tests/functional/cli/conftest.py @@ -16,6 +16,8 @@ from clive_local_tools.cli.cli_tester import CLITester from clive_local_tools.data.constants import ( ALT_WORKING_ACCOUNT1_KEY_ALIAS, ALT_WORKING_ACCOUNT1_PASSWORD, + WITNESS_ACCOUNT_KEY_ALIAS, + WITNESS_ACCOUNT_PASSWORD, WORKING_ACCOUNT_KEY_ALIAS, WORKING_ACCOUNT_PASSWORD, ) @@ -24,6 +26,7 @@ from clive_local_tools.testnet_block_log import ( ALT_WORKING_ACCOUNT1_NAME, KNOWN_ACCOUNTS, WATCHED_ACCOUNTS_NAMES, + WITNESS_ACCOUNT, WORKING_ACCOUNT_DATA, WORKING_ACCOUNT_NAME, run_node, @@ -193,3 +196,21 @@ async def cli_tester_locked_with_second_profile(cli_tester_locked: CLITester) -> await world_cm.commands.lock() world_cm.profile.skip_saving() # cannot save profile when it is locked because encryption is not possible return cli_tester_locked + + +@pytest.fixture +async def cli_tester_unlocked_with_witness_profile(cli_tester_locked: CLITester) -> CLITester: + """There is witness profile with witness key imported and cli_tester is unlocked.""" + async with World() as world_cm: + await world_cm.create_new_profile_with_wallets( + name=WITNESS_ACCOUNT.name, + password=WITNESS_ACCOUNT_PASSWORD, + working_account=WITNESS_ACCOUNT.name, + ) + world_cm.profile.keys.add_to_import( + PrivateKeyAliased(value=WITNESS_ACCOUNT.private_key, alias=WITNESS_ACCOUNT_KEY_ALIAS) + ) + await world_cm.commands.sync_data_with_beekeeper() + await world_cm.commands.save_profile() # required for saving imported keys aliases + await cli_tester_locked.world.switch_profile(world_cm.profile) + return cli_tester_locked diff --git a/tests/functional/cli/process/test_process_update_witness.py b/tests/functional/cli/process/test_process_update_witness.py new file mode 100644 index 0000000000..195fdd5a2e --- /dev/null +++ b/tests/functional/cli/process/test_process_update_witness.py @@ -0,0 +1,314 @@ +from __future__ import annotations + +from decimal import Decimal +from typing import TYPE_CHECKING + +import pytest +import test_tools as tt + +from clive.__private.cli.commands.process.process_update_witness import RequiresWitnessSetPropertiesOperationError +from clive.__private.core.keys.keys import PrivateKey +from clive.__private.core.percent_conversions import percent_to_hive_percent +from clive.__private.models.schemas import ( + FeedPublishOperation, + OperationBase, + WitnessSetPropertiesOperation, + WitnessUpdateOperation, +) +from clive_local_tools.checkers.blockchain_checkers import assert_operation_type_in_blockchain +from clive_local_tools.cli.exceptions import CLITestCommandError +from clive_local_tools.helpers import get_formatted_error_message + +if TYPE_CHECKING: + from clive_local_tools.cli.cli_tester import CLITester + + +@pytest.mark.parametrize("use_witness_key", [True, False]) +async def test_account_creation_fee( + node: tt.RawNode, + cli_tester_unlocked_with_witness_profile: CLITester, + use_witness_key: bool, # noqa: FBT001 +) -> None: + # ARRANGE + operations: list[type[OperationBase]] = ( + [WitnessUpdateOperation] if not use_witness_key else [WitnessSetPropertiesOperation] + ) + amount = tt.Asset.Hive(3.456) + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + use_witness_key=use_witness_key, account_creation_fee=amount + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert witness.props.account_creation_fee is not None, "Account creation fee should be set during witness creation" + assert amount == witness.props.account_creation_fee, ( + f"Witness '{owner}' account creation fee should change after command witness-update," + f" expected: `{amount.pretty_amount()}`, actual: `{witness.props.account_creation_fee.pretty_amount()}`" + ) + + +@pytest.mark.parametrize("use_witness_key", [True, False]) +async def test_maximum_block_size( + node: tt.RawNode, + cli_tester_unlocked_with_witness_profile: CLITester, + use_witness_key: bool, # noqa: FBT001 +) -> None: + # ARRANGE + operations: list[type[OperationBase]] = ( + [WitnessUpdateOperation] if not use_witness_key else [WitnessSetPropertiesOperation] + ) + maximum_block_size = 1_048_576 + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + use_witness_key=use_witness_key, maximum_block_size=maximum_block_size + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert maximum_block_size == witness.props.maximum_block_size, ( + f"Witness '{owner}' maximum block size should change after command witness-update," + f" expected: `{maximum_block_size}`, actual: `{witness.props.maximum_block_size}`" + ) + + +@pytest.mark.parametrize("use_witness_key", [True, False]) +async def test_hbd_interest_rate( + node: tt.RawNode, + cli_tester_unlocked_with_witness_profile: CLITester, + use_witness_key: bool, # noqa: FBT001 +) -> None: + # ARRANGE + operations: list[type[OperationBase]] = ( + [WitnessUpdateOperation] if not use_witness_key else [WitnessSetPropertiesOperation] + ) + hbd_interest_rate = Decimal("54.32") + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + use_witness_key=use_witness_key, hbd_interest_rate=hbd_interest_rate + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert percent_to_hive_percent(hbd_interest_rate) == witness.props.hbd_interest_rate, ( + f"Witness '{owner}' hbd interest rate should change after command witness-update," + f" expected: `{percent_to_hive_percent(hbd_interest_rate)}`, actual: `{witness.props.hbd_interest_rate}`" + ) + + +@pytest.mark.parametrize("use_witness_key", [True, False]) +async def test_new_signing_key( + node: tt.RawNode, + cli_tester_unlocked_with_witness_profile: CLITester, + use_witness_key: bool, # noqa: FBT001 +) -> None: + # ARRANGE + operations: list[type[OperationBase]] = ( + [WitnessUpdateOperation] if not use_witness_key else [WitnessSetPropertiesOperation] + ) + new_signing_key = PrivateKey.create().calculate_public_key().value + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + use_witness_key=use_witness_key, new_signing_key=new_signing_key + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert new_signing_key == witness.signing_key, ( + f"Witness '{owner}' signing key should change after command witness-update," + f" expected: `{new_signing_key}`, actual: `{witness.signing_key}`" + ) + + +@pytest.mark.parametrize("use_witness_key", [True, False]) +async def test_hbd_exchange_rate( + node: tt.RawNode, + cli_tester_unlocked_with_witness_profile: CLITester, + use_witness_key: bool, # noqa: FBT001 +) -> None: + # ARRANGE + operations: list[type[OperationBase]] = ( + [FeedPublishOperation] if not use_witness_key else [WitnessSetPropertiesOperation] + ) + hbd_exchange_rate = tt.Asset.Hbd(0.234) + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + use_witness_key=use_witness_key, hbd_exchange_rate=hbd_exchange_rate + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert witness.hbd_exchange_rate.base == hbd_exchange_rate, ( + f"Witness '{owner}' hbd exchange rate should change after command witness-update," + f" expected: `{hbd_exchange_rate}`, actual: `{witness.hbd_exchange_rate.base}`" + ) + assert witness.hbd_exchange_rate.quote == tt.Asset.Hive(1), "hbd exchange rate should be given as price of 1 hive" + + +@pytest.mark.parametrize("use_witness_key", [True, False]) +async def test_url( + node: tt.RawNode, + cli_tester_unlocked_with_witness_profile: CLITester, + use_witness_key: bool, # noqa: FBT001 +) -> None: + # ARRANGE + operations: list[type[OperationBase]] = ( + [WitnessUpdateOperation] if not use_witness_key else [WitnessSetPropertiesOperation] + ) + url = "example.com" + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness(use_witness_key=use_witness_key, url=url) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert url == witness.url, ( + f"Witness '{owner}' url change after command witness-update, expected: `{url}`, actual: `{witness.url}`" + ) + + +async def test_account_subsidy_budget(node: tt.RawNode, cli_tester_unlocked_with_witness_profile: CLITester) -> None: + # ARRANGE + operations: list[type[OperationBase]] = [WitnessSetPropertiesOperation] + account_subsidy_budget = 1234 + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + account_subsidy_budget=account_subsidy_budget + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert account_subsidy_budget == witness.props.account_subsidy_budget, ( + f"Witness '{owner}' account subsidy budget change after command witness-update," + f" expected: `{account_subsidy_budget}`, actual: `{witness.props.account_subsidy_budget}`" + ) + + +async def test_account_subsidy_decay(node: tt.RawNode, cli_tester_unlocked_with_witness_profile: CLITester) -> None: + # ARRANGE + operations: list[type[OperationBase]] = [WitnessSetPropertiesOperation] + account_subsidy_decay = 5678 + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + account_subsidy_decay=account_subsidy_decay + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert account_subsidy_decay == witness.props.account_subsidy_decay, ( + f"Witness '{owner}' account subsidy decay should change after command witness-update," + f" expected: `{account_subsidy_decay}`, actual: `{witness.props.account_subsidy_decay}`" + ) + + +async def test_two_operations_in_transaction( + node: tt.RawNode, cli_tester_unlocked_with_witness_profile: CLITester +) -> None: + # ARRANGE + operations: list[type[OperationBase]] = [WitnessUpdateOperation, FeedPublishOperation] + hbd_exchange_rate = tt.Asset.Hbd(0.3456) + hbd_interest_rate = Decimal("65.43") + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + use_witness_key=False, hbd_exchange_rate=hbd_exchange_rate, hbd_interest_rate=hbd_interest_rate + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert witness.hbd_exchange_rate.base == hbd_exchange_rate, ( + f"Witness '{owner}' hbd exchange rate should change after command witness-update," + f" expected: `{hbd_exchange_rate}`, actual: `{witness.hbd_exchange_rate.base}`" + ) + assert witness.hbd_exchange_rate.quote == tt.Asset.Hive(1), "hbd exchange rate should be given as price of 1 hive" + assert percent_to_hive_percent(hbd_interest_rate) == witness.props.hbd_interest_rate, ( + f"Witness '{owner}' hbd interest rate should change after command witness-update," + f" expected: `{percent_to_hive_percent(hbd_interest_rate)}`, actual: `{witness.props.hbd_interest_rate}`" + ) + + +async def test_using_updated_witness_key(node: tt.RawNode, cli_tester_unlocked_with_witness_profile: CLITester) -> None: + # ARRANGE + operations: list[type[OperationBase]] = [WitnessSetPropertiesOperation] + account_subsidy_decay = 6543 + alias = "updated_signing_key" + new_private_signing_key = PrivateKey.create() + cli_tester_unlocked_with_witness_profile.process_update_witness( + new_signing_key=new_private_signing_key.calculate_public_key().value + ) + cli_tester_unlocked_with_witness_profile.configure_key_add(key=new_private_signing_key.value, alias=alias) + owner = cli_tester_unlocked_with_witness_profile.world.profile.accounts.working.name + + # ACT + result = cli_tester_unlocked_with_witness_profile.process_update_witness( + account_subsidy_decay=account_subsidy_decay, sign_with=alias + ) + + # ASSERT + assert_operation_type_in_blockchain(node, result, *operations) + witness = ( + await cli_tester_unlocked_with_witness_profile.world.commands.find_witness(witness_name=owner) + ).result_or_raise + assert account_subsidy_decay == witness.props.account_subsidy_decay, ( + f"Witness '{owner}' account subsidy decay should change after command witness-update," + f" expected: `{account_subsidy_decay}`, actual: `{witness.props.account_subsidy_decay}`" + ) + + +async def test_negative_account_subsidy_with_active_authority( + cli_tester_unlocked_with_witness_profile: CLITester, +) -> None: + # ARRANGE + account_subsidy_budget = 2345 + + # ACT & ASSERT + with pytest.raises( + CLITestCommandError, + match=get_formatted_error_message(RequiresWitnessSetPropertiesOperationError("account-subsidy-budget")), + ): + cli_tester_unlocked_with_witness_profile.process_update_witness( + use_witness_key=False, account_subsidy_budget=account_subsidy_budget + ) -- GitLab From 088efc470008b2fb4221aa978b48345980f452e5 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 14 Oct 2025 12:50:35 +0000 Subject: [PATCH 07/10] Add third profile `witness` to testnet docker image --- testnet_node.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testnet_node.py b/testnet_node.py index 9b4fa81c99..450fd9fb4f 100644 --- a/testnet_node.py +++ b/testnet_node.py @@ -19,6 +19,7 @@ from clive.__private.settings import get_settings, safe_settings from clive_local_tools.data.constants import ( ALT_WORKING_ACCOUNT1_KEY_ALIAS, TESTNET_CHAIN_ID, + WITNESS_ACCOUNT_KEY_ALIAS, WORKING_ACCOUNT_KEY_ALIAS, ) from clive_local_tools.testnet_block_log import run_node @@ -26,6 +27,7 @@ from clive_local_tools.testnet_block_log.constants import ( ALT_WORKING_ACCOUNT1_DATA, ALT_WORKING_ACCOUNT1_NAME, KNOWN_ACCOUNTS, + WITNESS_ACCOUNT, WORKING_ACCOUNT_DATA, WORKING_ACCOUNT_NAME, ) @@ -81,6 +83,13 @@ async def prepare_profiles(node: tt.RawNode) -> None: private_key=ALT_WORKING_ACCOUNT1_DATA.account.private_key, key_alias=ALT_WORKING_ACCOUNT1_KEY_ALIAS, ) + await _create_profile_with_wallet( + profile_name="witness", + working_account_name=WITNESS_ACCOUNT.name, + known_accounts=KNOWN_ACCOUNTS, + private_key=WITNESS_ACCOUNT.private_key, + key_alias=WITNESS_ACCOUNT_KEY_ALIAS, + ) async def _create_profile_with_wallet( -- GitLab From bfe880f6373437e1f132dcac1b8719b3c868588b Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Mon, 20 Oct 2025 12:29:44 +0000 Subject: [PATCH 08/10] Return Iterable from _create_operation --- .../cli/commands/abc/operation_command.py | 10 +++++--- .../process/process_account_creation.py | 25 +++++++++++-------- .../process/process_account_update.py | 6 ++--- .../process_claim_new_account_token.py | 5 ++-- .../commands/process/process_custom_json.py | 8 ++++-- .../commands/process/process_delegations.py | 5 ++-- .../cli/commands/process/process_deposit.py | 5 ++-- .../commands/process/process_power_down.py | 8 ++++-- .../cli/commands/process/process_power_up.py | 5 ++-- .../commands/process/process_proxy_clear.py | 8 ++++-- .../cli/commands/process/process_proxy_set.py | 8 ++++-- .../process/process_transfer_schedule.py | 9 ++++--- .../process/process_update_witness.py | 20 ++++++--------- .../commands/process/process_vote_proposal.py | 8 ++++-- .../commands/process/process_vote_witness.py | 8 ++++-- .../process/process_withdraw_routes.py | 8 ++++-- .../commands/process/process_withdrawal.py | 5 ++-- .../process/process_withdrawal_cancel.py | 8 ++++-- .../cli/commands/process/transfer.py | 5 ++-- clive/__private/cli/types.py | 5 ++-- pydoclint-errors-baseline.txt | 3 ++- 21 files changed, 106 insertions(+), 66 deletions(-) diff --git a/clive/__private/cli/commands/abc/operation_command.py b/clive/__private/cli/commands/abc/operation_command.py index 9e0d8f8278..c99b2f5054 100644 --- a/clive/__private/cli/commands/abc/operation_command.py +++ b/clive/__private/cli/commands/abc/operation_command.py @@ -7,8 +7,8 @@ from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.perform_actions_on_transaction_command import PerformActionsOnTransactionCommand if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.core.ensure_transaction import TransactionConvertibleType - from clive.__private.models.schemas import OperationUnion @dataclass(kw_only=True) @@ -16,11 +16,13 @@ class OperationCommand(PerformActionsOnTransactionCommand, ABC): force_unsign: bool = field(init=False, default=False) @abstractmethod - async def _create_operation(self) -> OperationUnion: - """Get the operation to be processed.""" + async def _create_operations(self) -> ComposeTransaction: + """Get async generator with the operations to be processed, intended to be overridden.""" + if False: + yield # keeps MyPy happy that it's an async generator https://mypy.readthedocs.io/en/stable/more_types.html#asynchronous-iterators async def _get_transaction_content(self) -> TransactionConvertibleType: - return await self._create_operation() + return [operation async for operation in self._create_operations()] async def validate(self) -> None: self._validate_if_broadcasting_signed_transaction() diff --git a/clive/__private/cli/commands/process/process_account_creation.py b/clive/__private/cli/commands/process/process_account_creation.py index 8b212234a9..3df8b11548 100644 --- a/clive/__private/cli/commands/process/process_account_creation.py +++ b/clive/__private/cli/commands/process/process_account_creation.py @@ -18,6 +18,7 @@ from clive.__private.models.schemas import ( ) if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.core.keys.keys import PublicKey from clive.__private.core.types import AuthorityLevel, AuthorityLevelRegular @@ -122,9 +123,9 @@ class ProcessAccountCreation(OperationCommand): return Authority(weight_threshold=DEFAULT_AUTHORITY_THRESHOLD, account_auths=[], key_auths=[]) @override - async def _create_operation(self) -> AccountCreateOperation | CreateClaimedAccountOperation: - if self.fee: - return AccountCreateOperation( + async def _create_operations(self) -> ComposeTransaction: + yield ( + AccountCreateOperation( fee=self.fee_value_ensure, creator=self.creator, new_account_name=self.new_account_name, @@ -134,14 +135,16 @@ class ProcessAccountCreation(OperationCommand): 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, + if self.fee + else 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: diff --git a/clive/__private/cli/commands/process/process_account_update.py b/clive/__private/cli/commands/process/process_account_update.py index 35a499cc29..c15851c936 100644 --- a/clive/__private/cli/commands/process/process_account_update.py +++ b/clive/__private/cli/commands/process/process_account_update.py @@ -9,7 +9,7 @@ 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, AuthorityUpdateFunction + from clive.__private.cli.types import AccountUpdateFunction, AuthorityUpdateFunction, ComposeTransaction from clive.__private.core.types import AuthorityLevelRegular from clive.__private.models.schemas import Account @@ -45,14 +45,14 @@ class ProcessAccountUpdate(OperationCommand): raise NoChangesTransactionError await super().validate() - async def _create_operation(self) -> AccountUpdate2Operation: + async def _create_operations(self) -> ComposeTransaction: previous_state = self.__create_operation_from_stored_state(self._account) modified_state = deepcopy(previous_state) for callback in self._callbacks: modified_state = callback(modified_state) - return self.__skip_untouched_fields(previous_state, modified_state) + yield self.__skip_untouched_fields(previous_state, modified_state) def add_callback(self, callback: AccountUpdateFunction) -> None: self._callbacks.append(callback) diff --git a/clive/__private/cli/commands/process/process_claim_new_account_token.py b/clive/__private/cli/commands/process/process_claim_new_account_token.py index 88d7bcb68f..cc00461f9d 100644 --- a/clive/__private/cli/commands/process/process_claim_new_account_token.py +++ b/clive/__private/cli/commands/process/process_claim_new_account_token.py @@ -10,6 +10,7 @@ from clive.__private.core.constants.node_special_assets import HIVE_FEE_TO_USE_R from clive.__private.models.schemas import ClaimAccountOperation if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.models.asset import Asset @@ -19,12 +20,12 @@ class ProcessClaimNewAccountToken(OperationCommand): fee: Asset.Hive | None """None means RC will be used instead of payment in Hive""" - async def _create_operation(self) -> ClaimAccountOperation: + async def _create_operations(self) -> ComposeTransaction: if self.fee == HIVE_FEE_TO_USE_RC_IN_CLAIM_ACCOUNT_TOKEN_OPERATION_ASSET: raise CLIClaimAccountTokenZeroFeeError fee = self.fee if self.fee is not None else HIVE_FEE_TO_USE_RC_IN_CLAIM_ACCOUNT_TOKEN_OPERATION_ASSET.copy() - return ClaimAccountOperation(creator=self.creator, fee=fee) + yield ClaimAccountOperation(creator=self.creator, fee=fee) class CLIClaimAccountTokenZeroFeeError(CLIPrettyError): diff --git a/clive/__private/cli/commands/process/process_custom_json.py b/clive/__private/cli/commands/process/process_custom_json.py index e6655b77c7..878fe34308 100644 --- a/clive/__private/cli/commands/process/process_custom_json.py +++ b/clive/__private/cli/commands/process/process_custom_json.py @@ -3,6 +3,7 @@ from __future__ import annotations import errno from dataclasses import dataclass from pathlib import Path +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.cli.exceptions import CLIPrettyError @@ -10,6 +11,9 @@ from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD from clive.__private.models.schemas import CustomJsonOperation, JsonString from clive.__private.validators.json_validator import JsonValidator +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessCustomJson(OperationCommand): @@ -18,9 +22,9 @@ class ProcessCustomJson(OperationCommand): authorize: list[str] json_or_path: str - async def _create_operation(self) -> CustomJsonOperation: + async def _create_operations(self) -> ComposeTransaction: json_ = self.ensure_json_from_json_string_or_path(self.json_or_path) - return CustomJsonOperation( + yield CustomJsonOperation( id_=self.id_, json_=JsonString(json_), required_auths=self.authorize_by_active, diff --git a/clive/__private/cli/commands/process/process_delegations.py b/clive/__private/cli/commands/process/process_delegations.py index d3c53e1668..4ff289dec4 100644 --- a/clive/__private/cli/commands/process/process_delegations.py +++ b/clive/__private/cli/commands/process/process_delegations.py @@ -10,6 +10,7 @@ from clive.__private.core.ensure_vests import ensure_vests_async from clive.__private.models.schemas import DelegateVestingSharesOperation if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.models.asset import Asset @@ -23,10 +24,10 @@ class ProcessDelegations(OperationCommand): await self._validate_amount() await super().validate() - async def _create_operation(self) -> DelegateVestingSharesOperation: + async def _create_operations(self) -> ComposeTransaction: vesting_shares = await ensure_vests_async(self.amount, self.world) - return DelegateVestingSharesOperation( + yield DelegateVestingSharesOperation( delegator=self.delegator, delegatee=self.delegatee, vesting_shares=vesting_shares, diff --git a/clive/__private/cli/commands/process/process_deposit.py b/clive/__private/cli/commands/process/process_deposit.py index 8c1eff575a..75f96093d8 100644 --- a/clive/__private/cli/commands/process/process_deposit.py +++ b/clive/__private/cli/commands/process/process_deposit.py @@ -7,6 +7,7 @@ from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import TransferToSavingsOperation if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.models.asset import Asset @@ -17,8 +18,8 @@ class ProcessDeposit(OperationCommand): amount: Asset.LiquidT memo: str - async def _create_operation(self) -> TransferToSavingsOperation: - return TransferToSavingsOperation( + async def _create_operations(self) -> ComposeTransaction: + yield TransferToSavingsOperation( from_=self.from_account, to=self.to_account, amount=self.amount, diff --git a/clive/__private/cli/commands/process/process_power_down.py b/clive/__private/cli/commands/process/process_power_down.py index 930279da77..0d5315a757 100644 --- a/clive/__private/cli/commands/process/process_power_down.py +++ b/clive/__private/cli/commands/process/process_power_down.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.cli.exceptions import PowerDownInProgressError @@ -9,16 +10,19 @@ from clive.__private.core.ensure_vests import ensure_vests_async from clive.__private.models.asset import Asset from clive.__private.models.schemas import WithdrawVestingOperation +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessPowerDown(OperationCommand): account_name: str amount: Asset.VotingT - async def _create_operation(self) -> WithdrawVestingOperation: + async def _create_operations(self) -> ComposeTransaction: vesting_shares = await ensure_vests_async(self.amount, self.world) - return WithdrawVestingOperation( + yield WithdrawVestingOperation( account=self.account_name, vesting_shares=vesting_shares, ) diff --git a/clive/__private/cli/commands/process/process_power_up.py b/clive/__private/cli/commands/process/process_power_up.py index 00464b0b55..46733e2041 100644 --- a/clive/__private/cli/commands/process/process_power_up.py +++ b/clive/__private/cli/commands/process/process_power_up.py @@ -7,6 +7,7 @@ from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import TransferToVestingOperation if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.models.asset import Asset @@ -16,8 +17,8 @@ class ProcessPowerUp(OperationCommand): to_account: str amount: Asset.Hive - async def _create_operation(self) -> TransferToVestingOperation: - return TransferToVestingOperation( + async def _create_operations(self) -> ComposeTransaction: + yield TransferToVestingOperation( from_=self.from_account, to=self.to_account, amount=self.amount, diff --git a/clive/__private/cli/commands/process/process_proxy_clear.py b/clive/__private/cli/commands/process/process_proxy_clear.py index d4f8c482df..9111d5c83b 100644 --- a/clive/__private/cli/commands/process/process_proxy_clear.py +++ b/clive/__private/cli/commands/process/process_proxy_clear.py @@ -1,17 +1,21 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import AccountWitnessProxyOperation +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessProxyClear(OperationCommand): account_name: str - async def _create_operation(self) -> AccountWitnessProxyOperation: - return AccountWitnessProxyOperation( + async def _create_operations(self) -> ComposeTransaction: + yield AccountWitnessProxyOperation( account=self.account_name, proxy="", ) diff --git a/clive/__private/cli/commands/process/process_proxy_set.py b/clive/__private/cli/commands/process/process_proxy_set.py index c67b2d763e..21ac44e567 100644 --- a/clive/__private/cli/commands/process/process_proxy_set.py +++ b/clive/__private/cli/commands/process/process_proxy_set.py @@ -1,18 +1,22 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import AccountWitnessProxyOperation +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessProxySet(OperationCommand): account_name: str proxy: str - async def _create_operation(self) -> AccountWitnessProxyOperation: - return AccountWitnessProxyOperation( + async def _create_operations(self) -> ComposeTransaction: + yield AccountWitnessProxyOperation( account=self.account_name, proxy=self.proxy, ) diff --git a/clive/__private/cli/commands/process/process_transfer_schedule.py b/clive/__private/cli/commands/process/process_transfer_schedule.py index 4fd8e72f0c..1e8ea257cc 100644 --- a/clive/__private/cli/commands/process/process_transfer_schedule.py +++ b/clive/__private/cli/commands/process/process_transfer_schedule.py @@ -26,6 +26,7 @@ from clive.__private.models.schemas import ( ) if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.core.commands.data_retrieval.find_scheduled_transfers import ( AccountScheduledTransferData, ScheduledTransfer, @@ -121,11 +122,11 @@ class _ProcessTransferScheduleCreateModifyCommon(_ProcessTransferScheduleCommon) if scheduled_transfer_lifetime > SCHEDULED_TRANSFER_MAX_LIFETIME: raise ProcessTransferScheduleTooLongLifetimeError(requested_lifetime=scheduled_transfer_lifetime) - async def _create_operation(self) -> RecurrentTransferOperation: + async def _create_operations(self) -> ComposeTransaction: assert self.repeat is not None, "Value of repeat is None." assert self.memo is not None, "Value of memo is None." assert self.amount is not None, "Value of amount is None." - return RecurrentTransferOperation( + yield RecurrentTransferOperation( from_=self.from_account, to=self.to, amount=self.amount, @@ -181,8 +182,8 @@ class ProcessTransferScheduleModify(_ProcessTransferScheduleCreateModifyCommon): @dataclass(kw_only=True) class ProcessTransferScheduleRemove(_ProcessTransferScheduleCommon): - async def _create_operation(self) -> RecurrentTransferOperation: - return RecurrentTransferOperation( + async def _create_operations(self) -> ComposeTransaction: + yield RecurrentTransferOperation( from_=self.from_account, to=self.to, amount=SCHEDULED_TRANSFER_REMOVE_ASSETS[0].copy(), diff --git a/clive/__private/cli/commands/process/process_update_witness.py b/clive/__private/cli/commands/process/process_update_witness.py index a32fee0cf6..2b93936eb7 100644 --- a/clive/__private/cli/commands/process/process_update_witness.py +++ b/clive/__private/cli/commands/process/process_update_witness.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, override -from clive.__private.cli.commands.abc.perform_actions_on_transaction_command import PerformActionsOnTransactionCommand +from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.cli.exceptions import CLIPrettyError, CLIWitnessNotFoundError from clive.__private.core import iwax from clive.__private.core.commands.find_witness import WitnessNotFoundError @@ -23,8 +23,7 @@ from clive.__private.models.schemas import ( if TYPE_CHECKING: from decimal import Decimal - from clive.__private.core.ensure_transaction import TransactionConvertibleType - from clive.__private.models.schemas import OperationBase + from clive.__private.cli.types import ComposeTransaction class RequiresWitnessSetPropertiesOperationError(CLIPrettyError): @@ -45,7 +44,7 @@ class RequiresWitnessSetPropertiesOperationError(CLIPrettyError): @dataclass(kw_only=True) -class ProcessUpdateWitness(PerformActionsOnTransactionCommand): +class ProcessUpdateWitness(OperationCommand): owner: str use_witness_key: bool account_creation_fee: Asset.Hive | None @@ -76,22 +75,17 @@ class ProcessUpdateWitness(PerformActionsOnTransactionCommand): raise CLIWitnessNotFoundError(self.owner) from err @override - async def _get_transaction_content(self) -> TransactionConvertibleType: - operations: list[OperationBase] = [] + async def _create_operations(self) -> ComposeTransaction: if self._needs_feed_publish_operation: - operations.append(self._create_feed_publish_operation()) + yield self._create_feed_publish_operation() if self._needs_witness_update_operation: - operations.append(self._create_witness_update_operation()) + yield self._create_witness_update_operation() if self._needs_witness_set_properties_operation: - operations.append(self._create_witness_set_properties_operation()) - if not bool(operations): - raise CLIPrettyError("Transaction with no changes to authority cannot be created.") - return operations + yield self._create_witness_set_properties_operation() @override async def validate(self) -> None: self._validate_requirements_for_witness_set_propertues_operation() - self._validate_if_broadcasting_signed_transaction() self._validate_not_empty() await super().validate() diff --git a/clive/__private/cli/commands/process/process_vote_proposal.py b/clive/__private/cli/commands/process/process_vote_proposal.py index f9098e3854..0ff1e9523b 100644 --- a/clive/__private/cli/commands/process/process_vote_proposal.py +++ b/clive/__private/cli/commands/process/process_vote_proposal.py @@ -2,12 +2,16 @@ from __future__ import annotations import errno from dataclasses import dataclass +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.cli.exceptions import CLIPrettyError from clive.__private.core.constants.node import MAX_NUMBER_OF_PROPOSAL_IDS_IN_SINGLE_OPERATION from clive.__private.models.schemas import UpdateProposalVotesOperation +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessVoteProposal(OperationCommand): @@ -15,9 +19,9 @@ class ProcessVoteProposal(OperationCommand): proposal_ids: list[int] approve: bool - async def _create_operation(self) -> UpdateProposalVotesOperation: + async def _create_operations(self) -> ComposeTransaction: self.proposal_ids.sort() - return UpdateProposalVotesOperation( + yield UpdateProposalVotesOperation( voter=self.account_name, proposal_ids=self.proposal_ids, approve=self.approve, diff --git a/clive/__private/cli/commands/process/process_vote_witness.py b/clive/__private/cli/commands/process/process_vote_witness.py index e547ccbb82..1d48eff4cb 100644 --- a/clive/__private/cli/commands/process/process_vote_witness.py +++ b/clive/__private/cli/commands/process/process_vote_witness.py @@ -1,10 +1,14 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import AccountWitnessVoteOperation +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessVoteWitness(OperationCommand): @@ -12,8 +16,8 @@ class ProcessVoteWitness(OperationCommand): witness_name: str approve: bool - async def _create_operation(self) -> AccountWitnessVoteOperation: - return AccountWitnessVoteOperation( + async def _create_operations(self) -> ComposeTransaction: + yield AccountWitnessVoteOperation( account=self.account_name, witness=self.witness_name, approve=self.approve, diff --git a/clive/__private/cli/commands/process/process_withdraw_routes.py b/clive/__private/cli/commands/process/process_withdraw_routes.py index 6fdf661f7f..1f64ab88ee 100644 --- a/clive/__private/cli/commands/process/process_withdraw_routes.py +++ b/clive/__private/cli/commands/process/process_withdraw_routes.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field from decimal import Decimal +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.cli.exceptions import WithdrawRoutesZeroPercentError @@ -9,6 +10,9 @@ from clive.__private.core.constants.node import PERCENT_TO_REMOVE_WITHDRAW_ROUTE from clive.__private.core.percent_conversions import percent_to_hive_percent from clive.__private.models.schemas import SetWithdrawVestingRouteOperation +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessWithdrawRoutes(OperationCommand): @@ -21,8 +25,8 @@ class ProcessWithdrawRoutes(OperationCommand): await self._validate_percent() await super().validate() - async def _create_operation(self) -> SetWithdrawVestingRouteOperation: - return SetWithdrawVestingRouteOperation( + async def _create_operations(self) -> ComposeTransaction: + yield SetWithdrawVestingRouteOperation( from_account=self.from_account, to_account=self.to_account, percent=percent_to_hive_percent(self.percent), diff --git a/clive/__private/cli/commands/process/process_withdrawal.py b/clive/__private/cli/commands/process/process_withdrawal.py index e96abdf665..8f60c8978d 100644 --- a/clive/__private/cli/commands/process/process_withdrawal.py +++ b/clive/__private/cli/commands/process/process_withdrawal.py @@ -7,6 +7,7 @@ from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import TransferFromSavingsOperation if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.core.commands.data_retrieval.savings_data import SavingsData from clive.__private.models.asset import Asset @@ -19,13 +20,13 @@ class ProcessWithdrawal(OperationCommand): amount: Asset.LiquidT memo: str - async def _create_operation(self) -> TransferFromSavingsOperation: + async def _create_operations(self) -> ComposeTransaction: if self.request_id is None: wrapper = await self.world.commands.retrieve_savings_data(account_name=self.profile.accounts.working.name) savings_data: SavingsData = wrapper.result_or_raise self.request_id = savings_data.create_request_id() - return TransferFromSavingsOperation( + yield TransferFromSavingsOperation( from_=self.from_account, request_id=self.request_id, to=self.to_account, diff --git a/clive/__private/cli/commands/process/process_withdrawal_cancel.py b/clive/__private/cli/commands/process/process_withdrawal_cancel.py index ffd0063aed..1c5747bef3 100644 --- a/clive/__private/cli/commands/process/process_withdrawal_cancel.py +++ b/clive/__private/cli/commands/process/process_withdrawal_cancel.py @@ -1,18 +1,22 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import CancelTransferFromSavingsOperation +if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction + @dataclass(kw_only=True) class ProcessWithdrawalCancel(OperationCommand): from_account: str request_id: int - async def _create_operation(self) -> CancelTransferFromSavingsOperation: - return CancelTransferFromSavingsOperation( + async def _create_operations(self) -> ComposeTransaction: + yield CancelTransferFromSavingsOperation( from_=self.from_account, request_id=self.request_id, ) diff --git a/clive/__private/cli/commands/process/transfer.py b/clive/__private/cli/commands/process/transfer.py index 1564671ef0..5302f5287e 100644 --- a/clive/__private/cli/commands/process/transfer.py +++ b/clive/__private/cli/commands/process/transfer.py @@ -7,6 +7,7 @@ from clive.__private.cli.commands.abc.operation_command import OperationCommand from clive.__private.models.schemas import TransferOperation if TYPE_CHECKING: + from clive.__private.cli.types import ComposeTransaction from clive.__private.models.asset import Asset @@ -17,8 +18,8 @@ class Transfer(OperationCommand): amount: Asset.LiquidT memo: str - async def _create_operation(self) -> TransferOperation: - return TransferOperation( + async def _create_operations(self) -> ComposeTransaction: + yield TransferOperation( from_=self.from_account, to=self.to, amount=self.amount, diff --git a/clive/__private/cli/types.py b/clive/__private/cli/types.py index 5100179034..a633f3bb2e 100644 --- a/clive/__private/cli/types.py +++ b/clive/__private/cli/types.py @@ -3,9 +3,10 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import AsyncGenerator, Callable - from clive.__private.models.schemas import AccountUpdate2Operation, Authority + from clive.__private.models.schemas import AccountUpdate2Operation, Authority, OperationUnion AccountUpdateFunction = Callable[[AccountUpdate2Operation], AccountUpdate2Operation] AuthorityUpdateFunction = Callable[[Authority], Authority] + type ComposeTransaction = AsyncGenerator[OperationUnion] diff --git a/pydoclint-errors-baseline.txt b/pydoclint-errors-baseline.txt index b09655094e..3ab2a51334 100644 --- a/pydoclint-errors-baseline.txt +++ b/pydoclint-errors-baseline.txt @@ -12,7 +12,8 @@ clive/__private/cli/commands/abc/external_cli_command.py DOC103: Method `ExternalCLICommand._supply_with_correct_default_for_working_account`: 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: [profile: Profile]. -------------------- clive/__private/cli/commands/abc/operation_command.py - DOC201: Method `OperationCommand._create_operation` does not have a return section in docstring + DOC201: Method `OperationCommand._create_operations` does not have a return section in docstring + DOC402: Method `OperationCommand._create_operations` has "yield" statements, but the docstring does not have a "Yields" section -------------------- clive/__private/cli/commands/abc/perform_actions_on_transaction_command.py DOC201: Method `PerformActionsOnTransactionCommand._get_transaction_content` does not have a return section in docstring -- GitLab From 8ffe05509b823caa67b8759f13e41f8e15ad0fbd Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Thu, 23 Oct 2025 09:04:40 +0000 Subject: [PATCH 09/10] Change module for KNOWN_ACCOUNTS and UNKNOWN_ACCOUNT in tests --- testnet_node.py | 8 ++++---- .../clive_local_tools/data/constants.py | 17 +++++++++++++++++ .../testnet_block_log/__init__.py | 4 ---- .../testnet_block_log/constants.py | 11 ----------- .../test_known_accounts_disabled.py | 7 ++++--- .../test_known_accounts_enabled.py | 12 ++++++------ tests/functional/cli/conftest.py | 4 ++-- 7 files changed, 33 insertions(+), 30 deletions(-) diff --git a/testnet_node.py b/testnet_node.py index 450fd9fb4f..c7ff2b0bac 100644 --- a/testnet_node.py +++ b/testnet_node.py @@ -18,6 +18,7 @@ from clive.__private.run_tui import run_tui from clive.__private.settings import get_settings, safe_settings from clive_local_tools.data.constants import ( ALT_WORKING_ACCOUNT1_KEY_ALIAS, + KNOWN_ACCOUNT_NAMES, TESTNET_CHAIN_ID, WITNESS_ACCOUNT_KEY_ALIAS, WORKING_ACCOUNT_KEY_ALIAS, @@ -26,7 +27,6 @@ from clive_local_tools.testnet_block_log import run_node from clive_local_tools.testnet_block_log.constants import ( ALT_WORKING_ACCOUNT1_DATA, ALT_WORKING_ACCOUNT1_NAME, - KNOWN_ACCOUNTS, WITNESS_ACCOUNT, WORKING_ACCOUNT_DATA, WORKING_ACCOUNT_NAME, @@ -72,21 +72,21 @@ async def prepare_profiles(node: tt.RawNode) -> None: await _create_profile_with_wallet( profile_name=WORKING_ACCOUNT_NAME, working_account_name=WORKING_ACCOUNT_NAME, - known_accounts=KNOWN_ACCOUNTS, + known_accounts=KNOWN_ACCOUNT_NAMES, private_key=WORKING_ACCOUNT_DATA.account.private_key, key_alias=WORKING_ACCOUNT_KEY_ALIAS, ) await _create_profile_with_wallet( profile_name=ALT_WORKING_ACCOUNT1_NAME, working_account_name=ALT_WORKING_ACCOUNT1_NAME, - known_accounts=KNOWN_ACCOUNTS, + known_accounts=KNOWN_ACCOUNT_NAMES, private_key=ALT_WORKING_ACCOUNT1_DATA.account.private_key, key_alias=ALT_WORKING_ACCOUNT1_KEY_ALIAS, ) await _create_profile_with_wallet( profile_name="witness", working_account_name=WITNESS_ACCOUNT.name, - known_accounts=KNOWN_ACCOUNTS, + known_accounts=KNOWN_ACCOUNT_NAMES, private_key=WITNESS_ACCOUNT.private_key, key_alias=WITNESS_ACCOUNT_KEY_ALIAS, ) diff --git a/tests/clive-local-tools/clive_local_tools/data/constants.py b/tests/clive-local-tools/clive_local_tools/data/constants.py index 99ccbab4d6..52038a12e2 100644 --- a/tests/clive-local-tools/clive_local_tools/data/constants.py +++ b/tests/clive-local-tools/clive_local_tools/data/constants.py @@ -11,9 +11,15 @@ from clive.__private.core.constants.setting_identifiers import ( from clive.__private.settings import clive_prefixed_envvar from clive_local_tools.testnet_block_log import ( ALT_WORKING_ACCOUNT1_DATA, + ALT_WORKING_ACCOUNT1_NAME, ALT_WORKING_ACCOUNT2_DATA, + ALT_WORKING_ACCOUNT2_NAME, + EMPTY_ACCOUNT, + KNOWN_EXCHANGES_NAMES, + WATCHED_ACCOUNTS_NAMES, WITNESS_ACCOUNT, WORKING_ACCOUNT_DATA, + WORKING_ACCOUNT_NAME, ) TESTNET_CHAIN_ID: Final[str] = "18dcf0a285365fc58b71f18b3d3fec954aa0c141c44e4e5cb4cf777b9eab274e" @@ -30,3 +36,14 @@ NODE_CHAIN_ID_ENV_NAME: Final[str] = clive_prefixed_envvar(NODE_CHAIN_ID) SECRETS_NODE_ADDRESS_ENV_NAME: Final[str] = clive_prefixed_envvar(SECRETS_NODE_ADDRESS) WITNESS_ACCOUNT_PASSWORD: Final[str] = WITNESS_ACCOUNT.name WITNESS_ACCOUNT_KEY_ALIAS: Final[str] = f"{WITNESS_ACCOUNT.name}_key" + +UNKNOWN_ACCOUNT_NAME: Final[str] = "null" + +KNOWN_ACCOUNT_NAMES: Final[list[str]] = [ + EMPTY_ACCOUNT.name, + *WATCHED_ACCOUNTS_NAMES, + WORKING_ACCOUNT_NAME, + ALT_WORKING_ACCOUNT1_NAME, + ALT_WORKING_ACCOUNT2_NAME, + *KNOWN_EXCHANGES_NAMES, +] diff --git a/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py b/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py index 5705721969..c995a3459b 100644 --- a/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py +++ b/tests/clive-local-tools/clive_local_tools/testnet_block_log/__init__.py @@ -7,10 +7,8 @@ from .constants import ( ALT_WORKING_ACCOUNT2_NAME, CREATOR_ACCOUNT, EMPTY_ACCOUNT, - KNOWN_ACCOUNTS, KNOWN_EXCHANGES_NAMES, PROPOSALS, - UNKNOWN_ACCOUNT, WATCHED_ACCOUNTS_DATA, WATCHED_ACCOUNTS_NAMES, WITNESS_ACCOUNT, @@ -27,10 +25,8 @@ __all__ = [ "ALT_WORKING_ACCOUNT2_NAME", "CREATOR_ACCOUNT", "EMPTY_ACCOUNT", - "KNOWN_ACCOUNTS", "KNOWN_EXCHANGES_NAMES", "PROPOSALS", - "UNKNOWN_ACCOUNT", "WATCHED_ACCOUNTS_DATA", "WATCHED_ACCOUNTS_NAMES", "WITNESSES", diff --git a/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py b/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py index f134dd7e08..8545f6c4ee 100644 --- a/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py +++ b/tests/clive-local-tools/clive_local_tools/testnet_block_log/constants.py @@ -58,17 +58,6 @@ ALT_WORKING_ACCOUNT2_DATA: Final[AccountData] = AccountData( vests=tt.Asset.Test(122_000), # in hive power ) -UNKNOWN_ACCOUNT: Final[str] = "null" - KNOWN_EXCHANGES_NAMES: Final[list[str]] = [exchange.name for exchange in KnownExchanges()] -KNOWN_ACCOUNTS: Final[list[str]] = [ - EMPTY_ACCOUNT.name, - *WATCHED_ACCOUNTS_NAMES, - WORKING_ACCOUNT_NAME, - ALT_WORKING_ACCOUNT1_NAME, - ALT_WORKING_ACCOUNT2_NAME, - *KNOWN_EXCHANGES_NAMES, -] - WITNESS_ACCOUNT: Final[tt.Account] = WITNESSES[0] diff --git a/tests/functional/cli/accounts_validation/test_known_accounts_disabled.py b/tests/functional/cli/accounts_validation/test_known_accounts_disabled.py index 81f567ca59..5db910a705 100644 --- a/tests/functional/cli/accounts_validation/test_known_accounts_disabled.py +++ b/tests/functional/cli/accounts_validation/test_known_accounts_disabled.py @@ -4,8 +4,7 @@ from typing import TYPE_CHECKING import test_tools as tt -from clive_local_tools.data.constants import WORKING_ACCOUNT_KEY_ALIAS -from clive_local_tools.testnet_block_log.constants import UNKNOWN_ACCOUNT +from clive_local_tools.data.constants import UNKNOWN_ACCOUNT_NAME, WORKING_ACCOUNT_KEY_ALIAS if TYPE_CHECKING: from clive_local_tools.cli.cli_tester import CLITester @@ -24,4 +23,6 @@ async def test_no_trasaction_validation_to_unknown_account(cli_tester: CLITester cli_tester.configure_known_account_disable() # ACT & ASSERT - cli_tester.process_delegations_set(delegatee=UNKNOWN_ACCOUNT, amount=amount, sign_with=WORKING_ACCOUNT_KEY_ALIAS) + cli_tester.process_delegations_set( + delegatee=UNKNOWN_ACCOUNT_NAME, amount=amount, sign_with=WORKING_ACCOUNT_KEY_ALIAS + ) diff --git a/tests/functional/cli/accounts_validation/test_known_accounts_enabled.py b/tests/functional/cli/accounts_validation/test_known_accounts_enabled.py index 0f4b853536..ecf1d5ac24 100644 --- a/tests/functional/cli/accounts_validation/test_known_accounts_enabled.py +++ b/tests/functional/cli/accounts_validation/test_known_accounts_enabled.py @@ -8,9 +8,9 @@ import test_tools as tt from clive.__private.cli.exceptions import CLITransactionUnknownAccountError from clive.__private.models.schemas import TransferOperation from clive_local_tools.cli.exceptions import CLITestCommandError -from clive_local_tools.data.constants import WORKING_ACCOUNT_KEY_ALIAS +from clive_local_tools.data.constants import KNOWN_ACCOUNT_NAMES, UNKNOWN_ACCOUNT_NAME, WORKING_ACCOUNT_KEY_ALIAS from clive_local_tools.helpers import create_transaction_file, get_formatted_error_message -from clive_local_tools.testnet_block_log.constants import KNOWN_ACCOUNTS, UNKNOWN_ACCOUNT, WORKING_ACCOUNT_NAME +from clive_local_tools.testnet_block_log.constants import WORKING_ACCOUNT_NAME if TYPE_CHECKING: from collections.abc import Callable @@ -21,13 +21,13 @@ if TYPE_CHECKING: from .conftest import ActionSelector AMOUNT: Final[tt.Asset.HiveT] = tt.Asset.Hive(10) -KNOWN_ACCOUNT: Final[str] = KNOWN_ACCOUNTS[0] +KNOWN_ACCOUNT: Final[str] = KNOWN_ACCOUNT_NAMES[0] EXPECTED_UNKNOWN_ACCOUNT_ERROR_MSG: Final[str] = get_formatted_error_message( - CLITransactionUnknownAccountError(UNKNOWN_ACCOUNT) + CLITransactionUnknownAccountError(UNKNOWN_ACCOUNT_NAME) ) -VALIDATION_RECEIVERS: Final[list[str]] = [UNKNOWN_ACCOUNT, KNOWN_ACCOUNT] +VALIDATION_RECEIVERS: Final[list[str]] = [UNKNOWN_ACCOUNT_NAME, KNOWN_ACCOUNT] VALIDATION_IDS: Final[list[str]] = ["unknown_account", "known_account"] @@ -37,7 +37,7 @@ def _assert_operation_to_unknown_account_fails(perform_operation_cb: Callable[[] def _assert_validation_of_known_accounts(perform_operation_cb: Callable[[], None], receiver: str) -> None: - if receiver == UNKNOWN_ACCOUNT: + if receiver == UNKNOWN_ACCOUNT_NAME: _assert_operation_to_unknown_account_fails(perform_operation_cb) else: perform_operation_cb() diff --git a/tests/functional/cli/conftest.py b/tests/functional/cli/conftest.py index c457da154f..a60727d1f4 100644 --- a/tests/functional/cli/conftest.py +++ b/tests/functional/cli/conftest.py @@ -16,6 +16,7 @@ from clive_local_tools.cli.cli_tester import CLITester from clive_local_tools.data.constants import ( ALT_WORKING_ACCOUNT1_KEY_ALIAS, ALT_WORKING_ACCOUNT1_PASSWORD, + KNOWN_ACCOUNT_NAMES, WITNESS_ACCOUNT_KEY_ALIAS, WITNESS_ACCOUNT_PASSWORD, WORKING_ACCOUNT_KEY_ALIAS, @@ -24,7 +25,6 @@ from clive_local_tools.data.constants import ( from clive_local_tools.testnet_block_log import ( ALT_WORKING_ACCOUNT1_DATA, ALT_WORKING_ACCOUNT1_NAME, - KNOWN_ACCOUNTS, WATCHED_ACCOUNTS_NAMES, WITNESS_ACCOUNT, WORKING_ACCOUNT_DATA, @@ -79,7 +79,7 @@ async def _prepare_profile_with_wallet_cli(world_cli: World) -> Profile: password=WORKING_ACCOUNT_PASSWORD, working_account=WORKING_ACCOUNT_NAME, watched_accounts=WATCHED_ACCOUNTS_NAMES, - known_accounts=KNOWN_ACCOUNTS, + known_accounts=KNOWN_ACCOUNT_NAMES, ) await world_cli.commands.sync_state_with_beekeeper() world_cli.profile.keys.add_to_import( -- GitLab From e898fe3a4b9ff2997d32ce99ed600c6e83efc3b7 Mon Sep 17 00:00:00 2001 From: Marcin Sobczyk Date: Tue, 21 Oct 2025 12:38:49 +0000 Subject: [PATCH 10/10] Wrapper for wax witness_set_properties operation that transforms it to schemas model --- .../process/process_update_witness.py | 8 +- clive/__private/core/iwax.py | 106 ++++++++++-------- 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/clive/__private/cli/commands/process/process_update_witness.py b/clive/__private/cli/commands/process/process_update_witness.py index 2b93936eb7..a6d1ea3d7f 100644 --- a/clive/__private/cli/commands/process/process_update_witness.py +++ b/clive/__private/cli/commands/process/process_update_witness.py @@ -169,8 +169,7 @@ class ProcessUpdateWitness(OperationCommand): ) def _create_witness_set_properties_operation(self) -> WitnessSetPropertiesOperation: - schemas_props = iwax.create_witness_set_properties( - wax_interface=self.world.wax_interface, + wax_operation_wrapper = iwax.WitnessSetPropertiesWrapper.create( owner=self.owner, key=PublicKey(value=self.witness_ensure.signing_key), new_signing_key=self.new_signing_key, @@ -182,7 +181,4 @@ class ProcessUpdateWitness(OperationCommand): account_subsidy_budget=self.account_subsidy_budget, account_subsidy_decay=self.account_subsidy_decay, ) - return WitnessSetPropertiesOperation( - owner=AccountName(self.owner), - props=schemas_props, - ) + return wax_operation_wrapper.to_schemas(self.world.wax_interface) diff --git a/clive/__private/core/iwax.py b/clive/__private/core/iwax.py index 4e3a68361a..f007319414 100644 --- a/clive/__private/core/iwax.py +++ b/clive/__private/core/iwax.py @@ -3,13 +3,14 @@ from __future__ import annotations import datetime from collections.abc import Callable from functools import wraps -from typing import TYPE_CHECKING, Any, Protocol, cast, get_args +from typing import TYPE_CHECKING, Any, Protocol, Self, cast, get_args import wax from clive.__private.core.constants.precision import HIVE_PERCENT_PRECISION_DOT_PLACES from clive.__private.core.decimal_conventer import DecimalConverter from clive.__private.core.percent_conversions import hive_percent_to_percent, percent_to_hive_percent from clive.__private.models.schemas import AccountName, WitnessPropsSerializedKey +from clive.__private.models.schemas import WitnessSetPropertiesOperation as SchemasWitnessSetPropertiesOperation from clive.exceptions import CliveError if TYPE_CHECKING: @@ -19,6 +20,7 @@ if TYPE_CHECKING: from clive.__private.models.asset import Asset from clive.__private.models.schemas import Hex, OperationUnion, PriceFeed from clive.__private.models.transaction import Transaction + from wax.complex_operations.witness_set_properties import WitnessSetProperties as WaxWitnessSetProperties def cast_hiveint_args[F: Callable[..., Any]](func: F) -> F: @@ -272,52 +274,66 @@ def suggest_brain_key() -> str: return result.brain_key.decode() -def create_witness_set_properties( # noqa: PLR0913 - wax_interface: wax.IWaxBaseInterface, - owner: str, - key: PublicKey, - new_signing_key: PublicKey | None = None, - account_creation_fee: Asset.Hive | None = None, - url: str | None = None, - hbd_exchange_rate: Asset.Hbd | None = None, - maximum_block_size: int | None = None, - hbd_interest_rate: Decimal | None = None, - account_subsidy_budget: int | None = None, - account_subsidy_decay: int | None = None, -) -> list[tuple[WitnessPropsSerializedKey, Hex]]: - from clive.__private.models.asset import Asset # noqa: PLC0415 - from wax.complex_operations.witness_set_properties import ( # noqa: PLC0415 - WitnessSetProperties, - WitnessSetPropertiesData, - ) +class WitnessSetPropertiesWrapper: + def __init__(self, operation: WaxWitnessSetProperties) -> None: + self._operation = operation + + @classmethod + def create( # noqa: PLR0913 + cls, + owner: str, + key: PublicKey, + new_signing_key: PublicKey | None = None, + account_creation_fee: Asset.Hive | None = None, + url: str | None = None, + hbd_exchange_rate: Asset.Hbd | None = None, + maximum_block_size: int | None = None, + hbd_interest_rate: Decimal | None = None, + account_subsidy_budget: int | None = None, + account_subsidy_decay: int | None = None, + ) -> Self: + from clive.__private.models.asset import Asset # noqa: PLC0415 + from wax.complex_operations.witness_set_properties import ( # noqa: PLC0415 + WitnessSetProperties, + WitnessSetPropertiesData, + ) - wax_operation = WitnessSetProperties( - data=WitnessSetPropertiesData( - owner=AccountName(owner), - witness_signing_key=key.value, - new_signing_key=new_signing_key.value if new_signing_key is not None else None, - account_creation_fee=account_creation_fee.as_serialized_nai() if account_creation_fee is not None else None, - url=url, - hbd_exchange_rate=wax.complex_operations.witness_set_properties.HbdExchangeRate( - base=hbd_exchange_rate.as_serialized_nai(), - quote=Asset.hive(1).as_serialized_nai(), + return cls( + WitnessSetProperties( + data=WitnessSetPropertiesData( + owner=AccountName(owner), + witness_signing_key=key.value, + new_signing_key=new_signing_key.value if new_signing_key is not None else None, + account_creation_fee=account_creation_fee.as_serialized_nai() + if account_creation_fee is not None + else None, + url=url, + hbd_exchange_rate=wax.complex_operations.witness_set_properties.HbdExchangeRate( + base=hbd_exchange_rate.as_serialized_nai(), + quote=Asset.hive(1).as_serialized_nai(), + ) + if hbd_exchange_rate + else None, + maximum_block_size=maximum_block_size, + hbd_interest_rate=percent_to_hive_percent(hbd_interest_rate) if hbd_interest_rate else None, + account_subsidy_budget=account_subsidy_budget, + account_subsidy_decay=account_subsidy_decay, + ) ) - if hbd_exchange_rate - else None, - maximum_block_size=maximum_block_size, - hbd_interest_rate=percent_to_hive_percent(hbd_interest_rate) if hbd_interest_rate else None, - account_subsidy_budget=account_subsidy_budget, - account_subsidy_decay=account_subsidy_decay, ) - ) - first = next(iter(wax_operation.finalize(wax_interface))) - props = first.props - schemas_properties_model: list[tuple[WitnessPropsSerializedKey, Hex]] = [] - def append_optional_property(name: WitnessPropsSerializedKey) -> None: - if property_value := props.get(name): - schemas_properties_model.append((name, property_value)) + def to_schemas(self, wax_interface: wax.IWaxBaseInterface) -> SchemasWitnessSetPropertiesOperation: + first = next(iter(self._operation.finalize(wax_interface))) + props = self._props_to_schemas(first.props) + return SchemasWitnessSetPropertiesOperation(owner=self._operation.owner, props=props) + + def _props_to_schemas(self, wax_props: dict[str, str]) -> list[tuple[WitnessPropsSerializedKey, Hex]]: + schemas_properties_model: list[tuple[WitnessPropsSerializedKey, Hex]] = [] + + def append_optional_property(name: WitnessPropsSerializedKey) -> None: + if property_value := wax_props.get(name): + schemas_properties_model.append((name, property_value)) - for property_name in get_args(WitnessPropsSerializedKey): - append_optional_property(property_name) - return schemas_properties_model + for property_name in get_args(WitnessPropsSerializedKey): + append_optional_property(property_name) + return schemas_properties_model -- GitLab