diff --git a/clive/__private/cli/commands/abc/operation_command.py b/clive/__private/cli/commands/abc/operation_command.py index 9e0d8f82781fb22771717960910f90e8cc3ba6ef..c99b2f505432ced9f3c8bcd3c675a4e31ebca982 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 8b212234a9c2f0a07825ba71960c877803557007..3df8b1154889eadab67fe34bea3199159403e11f 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 35a499cc29f2bf7475d6e46b7976a9bc02594668..c15851c9369e5700294381441b33177a93ffc5f7 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 88d7bcb68f22e698bb23c2b1178e9302d9c95467..cc00461f9dffe3c4e420f44fc2b4adec890c1b00 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 e6655b77c7594aee8441dec1a52ce56ac7393f09..878fe3430892e8eca02edfefefe3eadfaf5f69bc 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 d3c53e1668a5b406f57f3e20a79bd900c79bcf38..4ff289dec46fd5269fcd2f2f6b6c7a1196c99302 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 8c1eff575aec5f652c9b285740541053a89957f1..75f96093d80fdb7766a44cd6aafcc6f2227a46e5 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 930279da777b579ca53bb378030904e1ebf3a658..0d5315a75770e6f2df5b764f9b9327e15cce5dc0 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 00464b0b55c2e7ca06c7b7aa50b09e79d38e76b6..46733e2041b92757f2945dd09241ae7f8f9b58cc 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 d4f8c482dfe8ca4357fe3fbd7c44e09f3ca37ea1..9111d5c83b6e9d3c5fcad3f0e7f97e9c32a99c66 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 c67b2d763e487bddd1d4524faad74103fdf8c07c..21ac44e567ebe5dd3f3cfc20c81610ecad56edd4 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 4fd8e72f0c098519cf8c737822ec1183d1251470..1e8ea257cc0dba4de6c69577914a9acfc1e7d0d3 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 new file mode 100644 index 0000000000000000000000000000000000000000..a6d1ea3d7f4e6b797cf9349ba964b8bc1c1971ac --- /dev/null +++ b/clive/__private/cli/commands/process/process_update_witness.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, override + +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 +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.cli.types import ComposeTransaction + + +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(OperationCommand): + 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 _create_operations(self) -> ComposeTransaction: + if self._needs_feed_publish_operation: + yield self._create_feed_publish_operation() + if self._needs_witness_update_operation: + yield self._create_witness_update_operation() + if self._needs_witness_set_properties_operation: + yield self._create_witness_set_properties_operation() + + @override + async def validate(self) -> None: + self._validate_requirements_for_witness_set_propertues_operation() + 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: + wax_operation_wrapper = iwax.WitnessSetPropertiesWrapper.create( + 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 wax_operation_wrapper.to_schemas(self.world.wax_interface) diff --git a/clive/__private/cli/commands/process/process_vote_proposal.py b/clive/__private/cli/commands/process/process_vote_proposal.py index f9098e385438a59e974fbc3b682129c2e320c50e..0ff1e9523b83a03d6778efb73fa6235a5ee23c0b 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 e547ccbb82b42e631f4a9b10cccbf8b054ac3153..1d48eff4cb6ab83d37bc25500930d4a5e7f11f52 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 6fdf661f7fabb04d1b1a14502cfaaacdf340b505..1f64ab88ee0c0cf74410722802c068139c202622 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 e96abdf665d9b88cfb59141c99b8285e612f803a..8f60c8978d86925e0cf6b7fc94bde1573022d3cb 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 ffd0063aed13866d4ceaadfb68583f133cfa2823..1c5747bef300e2e72f74e21db89be86648ed5dfd 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 1564671ef015f37a2875a6e541561c1326a47c04..5302f5287e1364ae52d3e6a424f24a94c7b13da9 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/commands/show/show_witness.py b/clive/__private/cli/commands/show/show_witness.py index 3ed267916132267510e1e43aedaad642e7cf1206..6df25577f459c5509f215ef80b13a5eb0ef2048e 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) @@ -30,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) diff --git a/clive/__private/cli/common/parsers.py b/clive/__private/cli/common/parsers.py index d3e35eabe14b3882036a499894db804b69ab5ac4..08ad4a9cad5e3fe75bc84f6ec0f14f4779a79901 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/exceptions.py b/clive/__private/cli/exceptions.py index 22c60ff0f48820230f3b9c2cb4773e2e9a85c216..46f36a881398edca08bf785b4bcde42f710cf163 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/cli/process/main.py b/clive/__private/cli/process/main.py index cb5e6e2076e9f98e7ed99ffe175ba82bb6b3bbde..fcca5a1e5653a78ca02d9b8289d2c20ff547b13a 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/clive/__private/cli/types.py b/clive/__private/cli/types.py index 5100179034b257c816ececc5019c03af6aceae7f..a633f3bb2ef715b158dc94c9aeb1a3a49dd349f5 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/clive/__private/core/commands/find_witness.py b/clive/__private/core/commands/find_witness.py index d296a6398ff7ad84ae19419e0be4d2db090996ba..63ee46fed248f15febc436e6a4706440682fc739 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 diff --git a/clive/__private/core/iwax.py b/clive/__private/core/iwax.py index 5827909d28447007e329c9a2dcb9dc75a2f6baa4..f00731941429ed3451a5f9c351ea5ba2e145de66 100644 --- a/clive/__private/core/iwax.py +++ b/clive/__private/core/iwax.py @@ -3,12 +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 +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 +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: @@ -16,8 +18,9 @@ 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 + from wax.complex_operations.witness_set_properties import WitnessSetProperties as WaxWitnessSetProperties def cast_hiveint_args[F: Callable[..., Any]](func: F) -> F: @@ -269,3 +272,68 @@ def generate_password_based_private_key( def suggest_brain_key() -> str: result = wax.suggest_brain_key() return result.brain_key.decode() + + +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, + ) + + 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, + ) + ) + ) + + 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 diff --git a/clive/__private/models/schemas.py b/clive/__private/models/schemas.py index 424bc4591274617e271f973575d8f16e86355814..f13068943bd312260b88dc80bc72b6b0b0539e56 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"), ) diff --git a/pydoclint-errors-baseline.txt b/pydoclint-errors-baseline.txt index f7fd54f29002021427521740e76986565f3c0a53..3ab2a513347108699f7bef13ef75c623456b32a5 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 @@ -141,6 +142,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. diff --git a/testnet_node.py b/testnet_node.py index 9b4fa81c99ded344b31ce79931633d33e53dc3dd..c7ff2b0bacce836373a0b1b7501785eafaa0dc3c 100644 --- a/testnet_node.py +++ b/testnet_node.py @@ -18,14 +18,16 @@ 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, ) 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, ) @@ -70,17 +72,24 @@ 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_ACCOUNT_NAMES, + private_key=WITNESS_ACCOUNT.private_key, + key_alias=WITNESS_ACCOUNT_KEY_ALIAS, + ) async def _create_profile_with_wallet( 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 5b469080779f10965810f9efdec91f8ec0d27c52..0cf27669183892dca26415a5b973aae888acbb2c 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 c850208f0c7a19b9d8beab744ac2fc6d473c92dd..8fd8062b8c61412cca855e516d32a51330c107ec 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 e524cef8dbf9313a01b809cfd8dacc9260004e3d..a1c4e1add3b07c25ffee04096673175a09de3d7b 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 6a2e3eb11b54904626731ed52a6f33edee74590d..52038a12e232501556deb09c8db71eb8a3b2a63d 100644 --- a/tests/clive-local-tools/clive_local_tools/data/constants.py +++ b/tests/clive-local-tools/clive_local_tools/data/constants.py @@ -11,8 +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" @@ -27,3 +34,16 @@ 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" + +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 e61fc49a1dee90ba86f5262077c6fd42889190ff..c995a3459bf0cb24bb8a614ea7ba932d08639236 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,12 +7,11 @@ 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, WITNESSES, WORKING_ACCOUNT_DATA, WORKING_ACCOUNT_NAME, @@ -26,13 +25,12 @@ __all__ = [ "ALT_WORKING_ACCOUNT2_NAME", "CREATOR_ACCOUNT", "EMPTY_ACCOUNT", - "KNOWN_ACCOUNTS", "KNOWN_EXCHANGES_NAMES", "PROPOSALS", - "UNKNOWN_ACCOUNT", "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 6ed396f68599da6264ab341484db96961994c6b0..8545f6c4ee2dd3e78695d74fc54b2b61040a9c64 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,15 +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 81f567ca59580a7e7e436332278745d4b4f1caaa..5db910a705a471a66f3f3566b82a0dc4434787a3 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 0f4b85353615571dc9672df0c736d9e15bda73d2..ecf1d5ac241f52ba957d7c4cb6d65d0374df775c 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 1e6a4c0aa72c377b3af0a6ceb7386656a50ad794..a60727d1f42d8f97d7da836d1beb3f451e12975f 100644 --- a/tests/functional/cli/conftest.py +++ b/tests/functional/cli/conftest.py @@ -16,14 +16,17 @@ 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, WORKING_ACCOUNT_PASSWORD, ) 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, WORKING_ACCOUNT_NAME, run_node, @@ -76,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( @@ -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 0000000000000000000000000000000000000000..195fdd5a2efc7921112bb9b773c2ebaa3cb04f31 --- /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 + )