From f660ebe8e4ccb859ac6d4f8092ca6574770bf640 Mon Sep 17 00:00:00 2001 From: Jakub Ziebinski <ziebinskijakub@gmail.com> Date: Mon, 24 Feb 2025 11:11:56 +0100 Subject: [PATCH 1/3] Bump wax and beekeepy --- poetry.lock | 56 ++++++++++++++++++++------------------------------ pyproject.toml | 6 ++++-- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7b1dae8d2..7c6e9b8de 100644 --- a/poetry.lock +++ b/poetry.lock @@ -183,21 +183,27 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte [[package]] name = "beekeepy" -version = "0.0.1.dev327+6dbc448" +version = "0.0.1.dev338+58e0bf7" description = "All in one package for beekeeper interaction via Python interface." optional = false python-versions = ">=3.10,<4.0" files = [ - {file = "beekeepy-0.0.1.dev327+6dbc448-py3-none-any.whl", hash = "sha256:84420db8aa9858a8522e48b5ef491c4aa973658e0ef34fee9fdbd2a3a73fa0e8"}, + {file = "beekeepy-0.0.1.dev338+58e0bf7-py3-none-any.whl", hash = "sha256:380dce868a3c6a326f8fc53890f6135eeeceb9f79639b41896dd406b43ed3f89"}, ] [package.dependencies] -helpy = "0.0.1.dev327+6dbc448" +aiohttp = "3.9.1" +httpx = {version = "0.23.3", extras = ["http2"]} +loguru = "0.7.2" +pydantic = "1.10.18" +python-dateutil = "2.8.2" +requests = "2.27.1" +schemas = "0.0.1.dev323+e5a1ba1" [package.source] type = "legacy" url = "https://gitlab.syncad.com/api/v4/projects/434/packages/pypi/simple" -reference = "gitlab-helpy-beekeepy" +reference = "gitlab-beekeepy" [[package]] name = "black" @@ -456,30 +462,6 @@ files = [ hpack = ">=4.0,<5" hyperframe = ">=6.0,<7" -[[package]] -name = "helpy" -version = "0.0.1.dev327+6dbc448" -description = "Easily interact with the Hive blockchain using Python." -optional = false -python-versions = ">=3.10,<4.0" -files = [ - {file = "helpy-0.0.1.dev327+6dbc448-py3-none-any.whl", hash = "sha256:cd22b4c98a9e16065db8e889d8488050ee305d6b2577e5b0256ff04f9a2950f3"}, -] - -[package.dependencies] -aiohttp = "3.9.1" -httpx = {version = "0.23.3", extras = ["http2"]} -loguru = "0.7.2" -python-dateutil = "2.8.2" -requests = "2.27.1" -schemas = "0.0.1.dev323+e5a1ba1" -wax = "0.3.10.dev321+1384595" - -[package.source] -type = "legacy" -url = "https://gitlab.syncad.com/api/v4/projects/434/packages/pypi/simple" -reference = "gitlab-helpy-beekeepy" - [[package]] name = "hpack" version = "4.0.0" @@ -1209,13 +1191,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1256,16 +1238,22 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "wax" -version = "0.3.10.dev321+1384595" +version = "0.3.10.dev558+3a636d1" description = "" optional = false python-versions = ">=3.10,<4.0" files = [ - {file = "wax-0.3.10.dev321+1384595-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:747429dfa159864212acf5f4ad57684f551b33152fa6b2dc0a6d62b7cd50d1d0"}, + {file = "wax-0.3.10.dev558+3a636d1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:ca430196868d2be71c442993192e214c81e2f0d4d86b322a0fc53628cf9003cc"}, ] [package.dependencies] +aiohttp = "3.9.1" +beekeepy = "0.0.1.dev338+58e0bf7" +httpx = {version = "0.23.3", extras = ["http2"]} +loguru = "0.7.2" protobuf = "4.24.4" +python-dateutil = "2.8.2" +requests = "2.27.1" [package.source] type = "legacy" @@ -1376,4 +1364,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e123c4367bb4147c2b3f4564cdae10e60547c57e080242c12e3d0b208d942ac5" +content-hash = "b1c571bf4a14b659113f7cd3c8dba53abc1a32fa1fad6604a6a2a25fbc5d4f75" diff --git a/pyproject.toml b/pyproject.toml index 20e72a7d6..814ac133e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ source = [ { name = "PyPI", priority = "primary" }, { name = "gitlab-schemas", url = "https://gitlab.syncad.com/api/v4/projects/362/packages/pypi/simple", priority = "supplemental" }, { name = "gitlab-wax", url = "https://gitlab.syncad.com/api/v4/projects/419/packages/pypi/simple", priority = "supplemental" }, - { name = "gitlab-helpy-beekeepy", url = "https://gitlab.syncad.com/api/v4/projects/434/packages/pypi/simple", priority = "supplemental" }, + { name = "gitlab-beekeepy", url = "https://gitlab.syncad.com/api/v4/projects/434/packages/pypi/simple", priority = "supplemental" }, ] packages = [ { include = "test_tools", from = "package" }, @@ -27,7 +27,9 @@ requests = "2.27.1" python-dateutil = "2.8.2" abstractcp = "0.9.9" loguru = "0.7.2" -beekeepy = "0.0.1.dev327+6dbc448" +typing-extensions="4.12.2" +beekeepy = "0.0.1.dev338+58e0bf7" +wax = "0.3.10.dev558+3a636d1" [tool.poetry.group.dev.dependencies] -- GitLab From 949fcf6476d240b2fb81855149aa4a5b17c2ab69 Mon Sep 17 00:00:00 2001 From: Jakub Ziebinski <ziebinskijakub@gmail.com> Date: Mon, 24 Feb 2025 14:37:09 +0100 Subject: [PATCH 2/3] Changes due to the new version of wax and beekeepy --- package/test_tools/__init__.py | 4 +- package/test_tools/__private/base_node.py | 7 +- package/test_tools/__private/block_log.py | 2 +- package/test_tools/__private/communication.py | 5 +- package/test_tools/__private/init_node.py | 2 +- package/test_tools/__private/node.py | 7 +- .../node_notification_handler.py | 5 +- .../notifications/node_notification_server.py | 6 +- package/test_tools/__private/old_wallet.py | 6 +- package/test_tools/__private/remote_node.py | 4 +- .../handles/node_handles/init_node_handle.py | 2 +- .../handles/node_handles/node_handle_base.py | 5 +- .../node_handles/remote_node_handle.py | 2 +- .../node_handles/runnable_node_handle.py | 5 +- .../user_handles/handles/wallet_handle.py | 2 +- package/test_tools/__private/vest_price.py | 2 +- .../__private/wallet/create_accounts.py | 23 +++--- .../wallet/single_transaction_context.py | 3 +- package/test_tools/__private/wallet/wallet.py | 71 +++++++++++++------ .../test_tools/__private/wallet/wallet_api.py | 71 ++++++++++++++----- .../beekeeper_wallet_tests/test_smoke.py | 3 +- .../test_networks_disconnecting.py | 4 +- .../node_tests/test_node_api_errors.py | 3 +- .../node_tests/test_node_startup.py | 2 +- .../node_tests/test_wait_for_irreversible.py | 2 +- .../test_transaction_serialization.py | 2 +- ...st_transaction_with_multiple_operations.py | 2 +- .../wallet_tests/test_wallet_api_errors.py | 4 +- 28 files changed, 163 insertions(+), 93 deletions(-) diff --git a/package/test_tools/__init__.py b/package/test_tools/__init__.py index 67f273882..c87230c22 100644 --- a/package/test_tools/__init__.py +++ b/package/test_tools/__init__.py @@ -4,8 +4,6 @@ from typing import TYPE_CHECKING from loguru import logger -from helpy import Hf26Asset as Asset -from helpy import OffsetTimeControl, SpeedUpRateTimeControl, StartTimeControl, Time, TimeFormats from test_tools.__private import ( cleanup_policy, constants, @@ -28,6 +26,8 @@ from test_tools.__private.user_handles import RemoteNodeHandle as RemoteNode from test_tools.__private.user_handles import WalletHandle as Wallet from test_tools.__private.user_handles import WitnessNodeHandle as WitnessNode from test_tools.__private.user_handles import context +from wax.helpy import Hf26Asset as Asset +from wax.helpy import OffsetTimeControl, SpeedUpRateTimeControl, StartTimeControl, Time, TimeFormats __all__ = [ "ApiNode", diff --git a/package/test_tools/__private/base_node.py b/package/test_tools/__private/base_node.py index bce5c7d32..df3742ccd 100644 --- a/package/test_tools/__private/base_node.py +++ b/package/test_tools/__private/base_node.py @@ -4,14 +4,15 @@ from datetime import timedelta from typing import TYPE_CHECKING, Any, cast from beekeepy import Settings +from beekeepy._communication.overseers import StrictOverseer -from helpy import Hived -from helpy._communication.overseers import StrictOverseer from test_tools.__private.scope import context from test_tools.__private.user_handles.implementation import Implementation as UserHandleImplementation +from wax.helpy import Hived if TYPE_CHECKING: - from helpy._interfaces.url import HttpUrl + from beekeepy.interfaces import HttpUrl + from test_tools.__private.user_handles.handles.node_handles.node_handle_base import NodeHandleBase diff --git a/package/test_tools/__private/block_log.py b/package/test_tools/__private/block_log.py index b30323292..3e1ea5093 100644 --- a/package/test_tools/__private/block_log.py +++ b/package/test_tools/__private/block_log.py @@ -8,11 +8,11 @@ import typing from pathlib import Path from typing import ClassVar, Final, Literal, overload -from helpy._interfaces.time import Time, TimeFormats from schemas.apis.block_api.fundaments_of_responses import BlockLogUtilSignedBlock from schemas.transaction import Transaction from test_tools.__private import paths_to_executables from test_tools.__private.exceptions import BlockLogError, BlockLogUtilError, MissingBlockLogArtifactsError +from wax.helpy._interfaces.time import Time, TimeFormats if typing.TYPE_CHECKING: from datetime import datetime diff --git a/package/test_tools/__private/communication.py b/package/test_tools/__private/communication.py index ff666686d..aa92065de 100644 --- a/package/test_tools/__private/communication.py +++ b/package/test_tools/__private/communication.py @@ -13,9 +13,10 @@ from schemas.fields.assets import AssetBase from schemas.fields.json_string import JsonString from schemas._preconfigured_base_model import PreconfiguredBaseModel from schemas.operations.representations import LegacyRepresentation -from helpy.exceptions import CommunicationError +from beekeepy.exceptions import CommunicationError from loguru import logger -from helpy import HttpUrl, Time +from beekeepy.interfaces import HttpUrl +from wax.helpy import Time class CommonJsonEncoder(json.JSONEncoder): diff --git a/package/test_tools/__private/init_node.py b/package/test_tools/__private/init_node.py index e891d40d4..c5a0c109e 100644 --- a/package/test_tools/__private/init_node.py +++ b/package/test_tools/__private/init_node.py @@ -2,10 +2,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -from helpy import Hf26Asset as Asset from test_tools.__private.exceptions import NodeIsNotRunningError from test_tools.__private.vest_price import VestPrice from test_tools.__private.witness_node import WitnessNode +from wax.helpy import Hf26Asset as Asset if TYPE_CHECKING: from test_tools.__private.user_handles.handles.network_handle import NetworkHandle as Network diff --git a/package/test_tools/__private/node.py b/package/test_tools/__private/node.py index 54f653ba7..6045d2a16 100644 --- a/package/test_tools/__private/node.py +++ b/package/test_tools/__private/node.py @@ -9,8 +9,8 @@ from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING, Any -from helpy._interfaces.time import StartTimeControl, TimeControl -from helpy._interfaces.url import HttpUrl +from beekeepy.interfaces import HttpUrl + from test_tools.__private import cleanup_policy, exceptions, paths_to_executables from test_tools.__private.base_node import BaseNode from test_tools.__private.block_log import BlockLog @@ -24,13 +24,14 @@ from test_tools.__private.snapshot import Snapshot from test_tools.__private.user_handles.get_implementation import get_implementation from test_tools.__private.wait_for import wait_for_event from test_tools.node_configs.default import create_default_config +from wax.helpy._interfaces.time import StartTimeControl, TimeControl if TYPE_CHECKING: from collections.abc import Sequence + from beekeepy.interfaces import P2PUrl, WsUrl from loguru import Record - from helpy._interfaces.url import P2PUrl, WsUrl from schemas.apis.network_node_api.response_schemas import SetAllowedPeers from test_tools.__private.alternate_chain_specs import AlternateChainSpecs from test_tools.__private.user_handles.handles.network_handle import NetworkHandle diff --git a/package/test_tools/__private/notifications/node_notification_handler.py b/package/test_tools/__private/notifications/node_notification_handler.py index e0bf25fa7..ef9296b31 100644 --- a/package/test_tools/__private/notifications/node_notification_handler.py +++ b/package/test_tools/__private/notifications/node_notification_handler.py @@ -3,10 +3,11 @@ from __future__ import annotations from threading import Event from typing import TYPE_CHECKING, Any -from helpy import HivedNotificationHandler, HttpUrl -from helpy._interfaces.url import P2PUrl, WsUrl +from beekeepy.interfaces import HttpUrl, P2PUrl, WsUrl + from test_tools.__private import exceptions from test_tools.__private.raise_exception_helper import RaiseExceptionHelper +from wax.helpy import HivedNotificationHandler if TYPE_CHECKING: from loguru import Logger diff --git a/package/test_tools/__private/notifications/node_notification_server.py b/package/test_tools/__private/notifications/node_notification_server.py index 2358fe700..8955b49ae 100644 --- a/package/test_tools/__private/notifications/node_notification_server.py +++ b/package/test_tools/__private/notifications/node_notification_server.py @@ -2,14 +2,14 @@ from __future__ import annotations from typing import TYPE_CHECKING -from helpy._communication.universal_notification_server import UniversalNotificationServer +from beekeepy._communication.universal_notification_server import UniversalNotificationServer + from test_tools.__private.notifications.node_notification_handler import NodeNotificationHandler if TYPE_CHECKING: + from beekeepy.interfaces import HttpUrl from loguru import Logger - from helpy import HttpUrl - class NodeNotificationServer(UniversalNotificationServer): def __init__(self, node_name: str, logger: Logger, notification_endpoint: HttpUrl | None) -> None: diff --git a/package/test_tools/__private/old_wallet.py b/package/test_tools/__private/old_wallet.py index d505b6f58..2a24e4b4d 100644 --- a/package/test_tools/__private/old_wallet.py +++ b/package/test_tools/__private/old_wallet.py @@ -18,13 +18,13 @@ from loguru import logger from test_tools.__private import communication, paths_to_executables from test_tools.__private.account import Account -from helpy import Hf26Asset as Asset +from wax.helpy import Hf26Asset as Asset from test_tools.__private.exceptions import NodeIsNotRunningError -from helpy.exceptions import CommunicationError +from beekeepy.exceptions import CommunicationError from test_tools.__private.node import Node from test_tools.__private.remote_node import RemoteNode from test_tools.__private.scope import ScopedObject, context -from helpy import Time +from wax.helpy import Time from test_tools.__private.user_handles.implementation import Implementation as UserHandleImplementation from test_tools.__private.utilities.fake_time import configure_fake_time diff --git a/package/test_tools/__private/remote_node.py b/package/test_tools/__private/remote_node.py index 6d54432d0..a25cccc8f 100644 --- a/package/test_tools/__private/remote_node.py +++ b/package/test_tools/__private/remote_node.py @@ -2,8 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -from helpy import HttpUrl, WsUrl -from helpy._interfaces.url import P2PUrl +from beekeepy.interfaces import HttpUrl, P2PUrl, WsUrl + from test_tools.__private.base_node import BaseNode if TYPE_CHECKING: diff --git a/package/test_tools/__private/user_handles/handles/node_handles/init_node_handle.py b/package/test_tools/__private/user_handles/handles/node_handles/init_node_handle.py index 0e7981a41..ae8902828 100644 --- a/package/test_tools/__private/user_handles/handles/node_handles/init_node_handle.py +++ b/package/test_tools/__private/user_handles/handles/node_handles/init_node_handle.py @@ -2,10 +2,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -from helpy import Hf26Asset as Asset from test_tools.__private.init_node import InitNode from test_tools.__private.user_handles.get_implementation import get_implementation from test_tools.__private.user_handles.handles.node_handles.runnable_node_handle import RunnableNodeHandle +from wax.helpy import Hf26Asset as Asset if TYPE_CHECKING: from test_tools.__private.user_handles.handles.network_handle import NetworkHandle as Network diff --git a/package/test_tools/__private/user_handles/handles/node_handles/node_handle_base.py b/package/test_tools/__private/user_handles/handles/node_handles/node_handle_base.py index a30c1ead6..5c0171aeb 100644 --- a/package/test_tools/__private/user_handles/handles/node_handles/node_handle_base.py +++ b/package/test_tools/__private/user_handles/handles/node_handles/node_handle_base.py @@ -14,8 +14,9 @@ if TYPE_CHECKING: import datetime from collections.abc import Iterator - from helpy._handles.hived.api.api_collection import HivedSyncApiCollection - from helpy._interfaces.url import HttpUrl + from beekeepy.interfaces import HttpUrl + + from wax.helpy._handles.hived.api.api_collection import HivedSyncApiCollection class NodeHandleBase(Handle): diff --git a/package/test_tools/__private/user_handles/handles/node_handles/remote_node_handle.py b/package/test_tools/__private/user_handles/handles/node_handles/remote_node_handle.py index 69b8827a6..851236843 100644 --- a/package/test_tools/__private/user_handles/handles/node_handles/remote_node_handle.py +++ b/package/test_tools/__private/user_handles/handles/node_handles/remote_node_handle.py @@ -6,7 +6,7 @@ from test_tools.__private.remote_node import RemoteNode from test_tools.__private.user_handles.handles.node_handles.node_handle_base import NodeHandleBase if TYPE_CHECKING: - from helpy._interfaces.url import HttpUrl, WsUrl + from beekeepy.interfaces import HttpUrl, WsUrl class RemoteNodeHandle(NodeHandleBase): diff --git a/package/test_tools/__private/user_handles/handles/node_handles/runnable_node_handle.py b/package/test_tools/__private/user_handles/handles/node_handles/runnable_node_handle.py index 2fb0b640e..06cf00858 100644 --- a/package/test_tools/__private/user_handles/handles/node_handles/runnable_node_handle.py +++ b/package/test_tools/__private/user_handles/handles/node_handles/runnable_node_handle.py @@ -10,13 +10,14 @@ from test_tools.__private.user_handles.handles.node_handles.node_handle_base imp if TYPE_CHECKING: from pathlib import Path - from helpy._interfaces.time import TimeControl - from helpy._interfaces.url import HttpUrl, P2PUrl, WsUrl + from beekeepy.interfaces import HttpUrl, P2PUrl, WsUrl + from test_tools.__private.alternate_chain_specs import AlternateChainSpecs from test_tools.__private.block_log import BlockLog from test_tools.__private.constants import CleanupPolicy from test_tools.__private.node_config import NodeConfig from test_tools.__private.snapshot import Snapshot + from wax.helpy._interfaces.time import TimeControl class RunnableNodeHandle(NodeHandleBase): diff --git a/package/test_tools/__private/user_handles/handles/wallet_handle.py b/package/test_tools/__private/user_handles/handles/wallet_handle.py index f292513fd..62fed4efd 100644 --- a/package/test_tools/__private/user_handles/handles/wallet_handle.py +++ b/package/test_tools/__private/user_handles/handles/wallet_handle.py @@ -13,13 +13,13 @@ from test_tools.__private.wallet.wallet import Wallet if TYPE_CHECKING: from pathlib import Path - from helpy import Hf26Asset as Asset from schemas.fields.hex import Hex from schemas.operations import AnyOperation from test_tools.__private.account import Account from test_tools.__private.type_annotations.any_node import AnyNode from test_tools.__private.wallet.constants import TransactionSerializationTypes, WalletResponse, WalletResponseBase from test_tools.__private.wallet.single_transaction_context import SingleTransactionContext + from wax.helpy import Hf26Asset as Asset class WalletHandle(Handle): diff --git a/package/test_tools/__private/vest_price.py b/package/test_tools/__private/vest_price.py index e408f190c..5ec38e456 100644 --- a/package/test_tools/__private/vest_price.py +++ b/package/test_tools/__private/vest_price.py @@ -6,8 +6,8 @@ from typing import TYPE_CHECKING from schemas.fields.compound import Price if TYPE_CHECKING: - from helpy import Hf26Asset from schemas.apis.database_api import GetDynamicGlobalProperties + from wax.helpy import Hf26Asset @dataclass diff --git a/package/test_tools/__private/wallet/create_accounts.py b/package/test_tools/__private/wallet/create_accounts.py index bbc06ebed..23e30372a 100644 --- a/package/test_tools/__private/wallet/create_accounts.py +++ b/package/test_tools/__private/wallet/create_accounts.py @@ -9,10 +9,7 @@ from typing import TYPE_CHECKING, Any from loguru import logger -import helpy import wax -from helpy import wax as wax_helpy -from helpy._interfaces.asset.asset import Asset from schemas.fields.compound import Authority from schemas.fields.hive_int import HiveInt from schemas.operations.account_create_operation import AccountCreateOperation @@ -24,12 +21,16 @@ from test_tools.__private.wallet.constants import ( SimpleTransaction, WalletResponseBase, ) +from wax._private.core.encoders import to_cpp_string +from wax._private.result_tools import expose_result_as_python_string +from wax.helpy._interfaces.asset.asset import Asset if TYPE_CHECKING: from collections.abc import Callable from beekeepy import Beekeeper, PackedSyncBeekeeper from beekeepy._interface.abc.synchronous.wallet import UnlockedWallet + from beekeepy.interfaces import HttpUrl from schemas.fields.basic import PublicKey from schemas.operations import AnyOperation @@ -67,10 +68,12 @@ def generate_transaction_template(node: RemoteNode) -> SimpleTransaction: def sign_transaction( node: RemoteNode, transaction: SimpleTransaction, beekeeper_wallet: UnlockedWallet ) -> SimpleTransaction: - wax_helpy.calculate_transaction_id(transaction=transaction) + transaction_as_bytes = to_cpp_string(transaction.json()) + wax.calculate_transaction_id(transaction_as_bytes) node_config = node.api.database.get_config() - sig_digest = wax_helpy.calculate_sig_digest(transaction, node_config.HIVE_CHAIN_ID) + wax_result = wax.calculate_sig_digest(transaction_as_bytes, to_cpp_string(node_config.HIVE_CHAIN_ID)) + sig_digest = expose_result_as_python_string(wax_result) key_to_sign_with = beekeeper_wallet.import_key(private_key=Account("initminer").private_key) time_before = datetime.now() @@ -81,7 +84,8 @@ def sign_transaction( transaction.signatures = list(set(transaction.signatures)) - wax_helpy.validate_transaction(transaction) + transaction_as_bytes = to_cpp_string(transaction.json()) + wax.validate_transaction(transaction_as_bytes) return transaction @@ -98,9 +102,10 @@ def prepare_transaction( operation.fee = account_creation_fee # type: ignore[assignment] transaction.add_operation(operation) transaction = sign_transaction(node, transaction, beekeeper_wallet) + transaction_as_bytes = to_cpp_string(transaction.json()) return WalletResponseBase( - transaction_id=wax_helpy.calculate_transaction_id(transaction=transaction), + transaction_id=expose_result_as_python_string(wax.calculate_transaction_id(transaction_as_bytes)), ref_block_num=transaction.ref_block_num, ref_block_prefix=transaction.ref_block_prefix, expiration=transaction.expiration, @@ -113,7 +118,7 @@ def prepare_transaction( def send_transaction( # noqa: C901 accounts_: list[Account], packed_beekeeper: PackedSyncBeekeeper, - node_address: helpy.HttpUrl, + node_address: HttpUrl, beekeeper_wallet_name: str, beekeeper_wallet_password: str, ) -> None: @@ -200,7 +205,7 @@ def create_accounts( predicate: Callable[..., Any], iterable_args: list[Any], packed_beekeeper: PackedSyncBeekeeper, - node_address: helpy.HttpUrl, + node_address: HttpUrl, beekeeper_wallet_name: str, beekeeper_wallet_password: str, *, diff --git a/package/test_tools/__private/wallet/single_transaction_context.py b/package/test_tools/__private/wallet/single_transaction_context.py index 5d4dca397..d5cd3b6be 100644 --- a/package/test_tools/__private/wallet/single_transaction_context.py +++ b/package/test_tools/__private/wallet/single_transaction_context.py @@ -3,7 +3,8 @@ from __future__ import annotations import sys from typing import TYPE_CHECKING, Any -from helpy._interfaces.context import ContextSync +from beekeepy.interfaces import ContextSync + from test_tools.__private.wallet.constants import WalletResponse, WalletResponseBase if TYPE_CHECKING: diff --git a/package/test_tools/__private/wallet/wallet.py b/package/test_tools/__private/wallet/wallet.py index 90e6cdc91..f2aeec1a9 100644 --- a/package/test_tools/__private/wallet/wallet.py +++ b/package/test_tools/__private/wallet/wallet.py @@ -6,12 +6,10 @@ from datetime import timedelta from typing import TYPE_CHECKING, Any, get_args from beekeepy import Beekeeper, Settings +from beekeepy.communication import StrictOverseer +from beekeepy.exceptions import ErrorInResponseError import wax -from helpy import Hf26Asset as Asset -from helpy import wax as wax_helpy -from helpy._communication.overseers import StrictOverseer -from helpy.exceptions import ErrorInResponseError from schemas.fields.basic import PublicKey from schemas.fields.hex import Hex from schemas.fields.hive_int import HiveInt @@ -34,6 +32,10 @@ from test_tools.__private.wallet.create_accounts import ( ) from test_tools.__private.wallet.single_transaction_context import SingleTransactionContext from test_tools.__private.wallet.wallet_api import Api +from wax._private.core.encoders import to_cpp_string, to_python_string +from wax._private.result_tools import expose_result_as_python_string +from wax.helpy import Hf26Asset as Asset +from wax.wax_result import python_minimize_required_signatures_data if TYPE_CHECKING: from pathlib import Path @@ -80,7 +82,11 @@ class Wallet(UserHandleImplementation, ScopedObject): self.run(preconfigure=preconfigure) if self.connected_node is not None: node_version = self.connected_node.api.database.get_version().node_type - is_testnet_wax = wax_helpy.get_hive_protocol_config(self.__get_chain_id())["IS_TEST_NET"] + protocol_config = { + key.decode(): value.decode() + for key, value in wax.get_hive_protocol_config(to_cpp_string(self.__get_chain_id())).items() + } + is_testnet_wax = protocol_config["IS_TEST_NET"] if self._transaction_serialization == "legacy": if node_version == "testnet" and is_testnet_wax == "false": warnings.warn( @@ -282,7 +288,7 @@ class Wallet(UserHandleImplementation, ScopedObject): block_id = gdpo.head_block_id # set header - tapos_data = wax_helpy.calculate_tapos_data(block_id) + tapos_data = wax.get_tapos_data(block_id.encode()) ref_block_num = tapos_data.ref_block_num ref_block_prefix = tapos_data.ref_block_prefix @@ -321,9 +327,11 @@ class Wallet(UserHandleImplementation, ScopedObject): return WalletResponse( transaction_id=( - wax_helpy.calculate_transaction_id(transaction=transaction) - if self._transaction_serialization == "hf26" - else wax_helpy.calculate_legacy_transaction_id(transaction=transaction) + expose_result_as_python_string( + wax.calculate_transaction_id(self.__transaction_to_cpp_string(transaction)) + if self._transaction_serialization == "hf26" + else wax.calculate_legacy_transaction_id(self.__transaction_to_cpp_string(transaction)) + ) ), block_num=broadcast_response.block_num, transaction_num=broadcast_response.trx_num, @@ -339,9 +347,13 @@ class Wallet(UserHandleImplementation, ScopedObject): return WalletResponseBase( transaction_id=( - wax_helpy.calculate_transaction_id(transaction=transaction) + expose_result_as_python_string( + wax.calculate_transaction_id(self.__transaction_to_cpp_string(transaction)) + ) if self._transaction_serialization == "hf26" - else wax_helpy.calculate_legacy_transaction_id(transaction=transaction) + else expose_result_as_python_string( + wax.calculate_legacy_transaction_id(self.__transaction_to_cpp_string(transaction)) + ) ), ref_block_num=transaction.ref_block_num, ref_block_prefix=transaction.ref_block_prefix, @@ -363,9 +375,13 @@ class Wallet(UserHandleImplementation, ScopedObject): def calculate_sig_digest(self, transaction: SimpleTransaction) -> str: chain_id = self.__get_chain_id() return ( - wax_helpy.calculate_sig_digest(transaction, chain_id) + expose_result_as_python_string( + wax.calculate_sig_digest(self.__transaction_to_cpp_string(transaction), to_cpp_string(chain_id)) + ) if self._transaction_serialization == "hf26" - else wax_helpy.calculate_legacy_sig_digest(transaction, chain_id) + else expose_result_as_python_string( + wax.calculate_legacy_sig_digest(self.__transaction_to_cpp_string(transaction), to_cpp_string(chain_id)) + ) ) def sign_transaction( @@ -377,7 +393,7 @@ class Wallet(UserHandleImplementation, ScopedObject): transaction.signatures.append(signature) transaction.signatures = list(set(transaction.signatures)) - wax_helpy.validate_transaction(transaction) + wax.validate_transaction(self.__transaction_to_cpp_string(transaction)) return transaction def reduce_signatures( @@ -391,13 +407,18 @@ class Wallet(UserHandleImplementation, ScopedObject): assert get_witness is not None return get_witness.signing_key.encode() - return wax_helpy.minimize_required_signatures( - transaction, - chain_id=self.__get_chain_id(), - available_keys=keys_to_sign_with, - authorities_map=retrived_authorities, - get_witness_key=retrieve_witness_key, - ) + return [ + to_python_string(pub_key) + for pub_key in wax.minimize_required_signatures( + self.__transaction_to_cpp_string(transaction), + python_minimize_required_signatures_data( + chain_id=to_cpp_string(self.__get_chain_id()), + available_keys=[to_cpp_string(key) for key in keys_to_sign_with], + authorities_map=retrived_authorities, + get_witness_key=retrieve_witness_key, + ), + ) + ] def import_required_keys( self, transaction: SimpleTransaction @@ -434,7 +455,10 @@ class Wallet(UserHandleImplementation, ScopedObject): retrived_authorities.update(retrived_authoritity) return retrived_authoritity - keys_for_signing = wax_helpy.collect_signing_keys(transaction, retrieve_authorities) + keys_for_signing = [ + key.decode() + for key in wax.collect_signing_keys(self.__transaction_to_cpp_string(transaction), retrieve_authorities) + ] if self._use_authority != {}: account_name = next(iter(self._use_authority.keys())) @@ -453,3 +477,6 @@ class Wallet(UserHandleImplementation, ScopedObject): if broadcast is None: broadcast = True return SingleTransactionContext(self, broadcast=broadcast, blocking=blocking) + + def __transaction_to_cpp_string(self, transaction: SimpleTransaction) -> bytes: + return to_cpp_string(transaction.json()) diff --git a/package/test_tools/__private/wallet/wallet_api.py b/package/test_tools/__private/wallet/wallet_api.py index dfb2a69ad..6afa0603b 100644 --- a/package/test_tools/__private/wallet/wallet_api.py +++ b/package/test_tools/__private/wallet/wallet_api.py @@ -6,8 +6,7 @@ from datetime import datetime, timedelta from functools import wraps from typing import TYPE_CHECKING, Any, ParamSpec, cast -from helpy import Hf26Asset as Asset -from helpy import wax as wax_helpy +from schemas.fields.assets.hive import AssetHiveHF26 from schemas.fields.basic import AccountName, EmptyList, PrivateKey, PublicKey from schemas.fields.compound import Authority, HbdExchangeRate, LegacyChainProperties, Proposal from schemas.operations import AnyOperation @@ -70,6 +69,18 @@ from test_tools.__private.wallet.constants import ( from test_tools.__private.wallet.create_accounts import ( get_authority, ) +from wax import ( + calculate_public_key, + create_wax_foundation, + decode_encrypted_memo, + encode_encrypted_memo, + generate_password_based_private_key, + suggest_brain_key, +) +from wax._private.core.encoders import to_cpp_string, to_python_string +from wax._private.exceptions import WaxValidationFailedError +from wax._private.result_tools import expose_result_as_python_string, validate_wax_result +from wax.helpy import Hf26Asset as Asset if TYPE_CHECKING: from collections.abc import Callable @@ -77,7 +88,6 @@ if TYPE_CHECKING: from beekeepy._interface.abc.synchronous.wallet import UnlockedWallet import schemas.apis.database_api.fundaments_of_reponses as fundaments_database_api - from helpy._handles.hived.api.wallet_bridge_api.sync_api import WalletBridgeApi from schemas.apis.block_api.fundaments_of_responses import Hf26Block from schemas.apis.wallet_bridge_api.response_schemas import ( FindProposals, @@ -108,7 +118,6 @@ if TYPE_CHECKING: ) from schemas.fields.assets._base import AssetHF26 from schemas.fields.assets.hbd import AssetHbdHF26 - from schemas.fields.assets.hive import AssetHiveHF26 from schemas.fields.assets.vests import AssetVestsHF26 from schemas.fields.hex import Hex from schemas.fields.hive_int import HiveInt @@ -118,6 +127,7 @@ if TYPE_CHECKING: from test_tools.__private.remote_node import RemoteNode from test_tools.__private.wallet.single_transaction_context import SingleTransactionContext from test_tools.__private.wallet.wallet import Wallet + from wax.helpy._handles.hived.api.wallet_bridge_api.sync_api import WalletBridgeApi AnyNode = Node | RemoteNode @@ -235,8 +245,10 @@ class Api: def __check_memo(self, account: AccountNameApiType, memo: str) -> None: if isinstance(memo, PrivateKey): try: - public_key = wax_helpy.calculate_public_key(wif=memo) - except wax_helpy.WaxOperationFailedError: + wax_result = calculate_public_key(wif=to_cpp_string(memo)) + validate_wax_result(wax_result) + public_key = expose_result_as_python_string(wax_result) + except WaxValidationFailedError: return get_account = self.get_account(account_name=account) if self.__wallet.beekeeper_wallet.has_matching_private_key(key=public_key): @@ -830,9 +842,13 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: The decrypted memo. """ - decrypt_memo = wax_helpy.decrypt_memo( - content=memo, second_step_callback=self.__wallet.beekeeper_wallet.decrypt_data # type: ignore[arg-type] + encrypted_memo = decode_encrypted_memo(to_cpp_string(memo)) + decrypt_memo = self.__wallet.beekeeper_wallet.decrypt_data( + from_key=to_python_string(encrypted_memo.main_encryption_key), # type: ignore[arg-type] + to_key=to_python_string(encrypted_memo.other_encryption_key), # type: ignore[arg-type] + content=to_python_string(encrypted_memo.encrypted_content), ) + if decrypt_memo.startswith("#"): return decrypt_memo[1:] return None @@ -1149,12 +1165,20 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: Estimated hive collateral. """ - return wax_helpy.estimate_hive_collateral( - current_median_history=self.get_feed_history().current_median_history, - current_min_history=self.get_feed_history().current_min_history, - hbd_amount_to_get=hbd_amount_to_get, + wax_base_api = create_wax_foundation() + current_median_history = self.get_feed_history().current_median_history + current_min_history = self.get_feed_history().current_min_history + + wax_asset = wax_base_api.estimate_hive_collateral( + current_median_history.base.dict(), + current_median_history.quote.dict(), + current_min_history.base.dict(), + current_min_history.quote.dict(), + hbd_amount_to_get.dict(), ) + return AssetHiveHF26(amount=int(wax_asset.amount), nai=wax_asset.nai, precision=wax_asset.precision) + @warn_if_only_result_set() def exit(self, only_result: bool | None = None) -> None: # noqa: ARG002 A003 """ @@ -1370,13 +1394,15 @@ class Api: from_key=from_key, to_key=to_key, content=content, nonce=nonce ) - return wax_helpy.encrypt_memo( - main_encryption_key=from_account.memo_key, - other_encryption_key=to_account.memo_key, - content=memo, - second_step_callback=proxy_encrypt_data, # type: ignore[arg-type] + encrypted_memo = proxy_encrypt_data(from_account.memo_key, to_account.memo_key, memo) + encoded_encrypted_memo = encode_encrypted_memo( + encrypted_content=to_cpp_string(encrypted_memo), + main_encryption_key=to_cpp_string(from_account.memo_key), + other_encryption_key=to_cpp_string(to_account.memo_key), ) + return to_python_string(encoded_encrypted_memo) + @warn_if_only_result_set() def get_feed_history(self, only_result: bool | None = None) -> GetFeedHistory: # noqa: ARG002 """ @@ -1458,7 +1484,8 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: A list containing the associated public key and the private key in WIF format. """ - return wax_helpy.generate_password_based_private_key(account_name=account, role=role, password=password) + wax_result = generate_password_based_private_key(account=account, role=role, password=password) + return [to_python_string(wax_result.associated_public_key), to_python_string(wax_result.wif_private_key)] @warn_if_only_result_set() def get_prototype_operation( @@ -2124,7 +2151,13 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: A dictionary containing the suggested brain key, associated WIF private key, and public key. """ - return wax_helpy.suggest_brain_key() + wax_result = suggest_brain_key() + + return { + "brain_priv_key": to_python_string(wax_result.brain_key), + "wif_priv_key": to_python_string(wax_result.wif_private_key), + "pub_key": to_python_string(wax_result.associated_public_key), + } @require_unlocked_wallet @warn_if_only_result_set() diff --git a/tests/functional_tests/beekeeper_wallet_tests/test_smoke.py b/tests/functional_tests/beekeeper_wallet_tests/test_smoke.py index 6089e7e9f..0217bf5fd 100644 --- a/tests/functional_tests/beekeeper_wallet_tests/test_smoke.py +++ b/tests/functional_tests/beekeeper_wallet_tests/test_smoke.py @@ -2,8 +2,7 @@ from __future__ import annotations import pytest import test_tools as tt - -from helpy.exceptions import ErrorInResponseError +from beekeepy.exceptions import ErrorInResponseError @pytest.fixture() diff --git a/tests/functional_tests/network_tests/test_networks_disconnecting.py b/tests/functional_tests/network_tests/test_networks_disconnecting.py index 0d98f4b23..7b8b2cf80 100644 --- a/tests/functional_tests/network_tests/test_networks_disconnecting.py +++ b/tests/functional_tests/network_tests/test_networks_disconnecting.py @@ -7,8 +7,8 @@ import pytest import test_tools as tt from local_tools.network import get_head_block_number, get_head_block_numbers_for_networks -from helpy import Hf26Asset as Asset -from helpy import Time +from wax.helpy import Hf26Asset as Asset +from wax.helpy import Time if TYPE_CHECKING: from collections.abc import Iterable diff --git a/tests/functional_tests/node_tests/test_node_api_errors.py b/tests/functional_tests/node_tests/test_node_api_errors.py index 7f1bb13ac..b048806f4 100644 --- a/tests/functional_tests/node_tests/test_node_api_errors.py +++ b/tests/functional_tests/node_tests/test_node_api_errors.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import pytest - -from helpy.exceptions import ErrorInResponseError +from beekeepy.exceptions import ErrorInResponseError if TYPE_CHECKING: import test_tools as tt diff --git a/tests/functional_tests/node_tests/test_node_startup.py b/tests/functional_tests/node_tests/test_node_startup.py index e80031f65..486b424c9 100644 --- a/tests/functional_tests/node_tests/test_node_startup.py +++ b/tests/functional_tests/node_tests/test_node_startup.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING import pytest import test_tools as tt -from helpy import Time +from wax.helpy import Time if TYPE_CHECKING: from test_tools.__private.type_annotations.any_node import AnyNode diff --git a/tests/functional_tests/node_tests/test_wait_for_irreversible.py b/tests/functional_tests/node_tests/test_wait_for_irreversible.py index 05b18cc1b..8677e6fd2 100644 --- a/tests/functional_tests/node_tests/test_wait_for_irreversible.py +++ b/tests/functional_tests/node_tests/test_wait_for_irreversible.py @@ -5,7 +5,7 @@ from typing import Final import pytest import test_tools as tt -from helpy.exceptions import BlockWaitTimeoutError +from wax.helpy.exceptions import BlockWaitTimeoutError def test_raising_timeout(node: tt.InitNode) -> None: diff --git a/tests/functional_tests/wallet_tests/test_transaction_serialization.py b/tests/functional_tests/wallet_tests/test_transaction_serialization.py index f5f20a834..05be235a9 100644 --- a/tests/functional_tests/wallet_tests/test_transaction_serialization.py +++ b/tests/functional_tests/wallet_tests/test_transaction_serialization.py @@ -5,7 +5,7 @@ from typing import Literal import pytest import test_tools as tt -from helpy import Hf26Asset as Asset +from wax.helpy import Hf26Asset as Asset @pytest.mark.parametrize("transaction_serialization", ["legacy", "hf26"]) diff --git a/tests/functional_tests/wallet_tests/test_transaction_with_multiple_operations.py b/tests/functional_tests/wallet_tests/test_transaction_with_multiple_operations.py index b208b4891..58d52d521 100644 --- a/tests/functional_tests/wallet_tests/test_transaction_with_multiple_operations.py +++ b/tests/functional_tests/wallet_tests/test_transaction_with_multiple_operations.py @@ -4,7 +4,7 @@ import pytest import test_tools as tt from test_tools.__private.exceptions import BroadcastDuringTransactionBuildingError -from helpy import Hf26Asset as Asset +from wax.helpy import Hf26Asset as Asset @pytest.fixture() diff --git a/tests/functional_tests/wallet_tests/test_wallet_api_errors.py b/tests/functional_tests/wallet_tests/test_wallet_api_errors.py index 30052530d..2d6e53e9d 100644 --- a/tests/functional_tests/wallet_tests/test_wallet_api_errors.py +++ b/tests/functional_tests/wallet_tests/test_wallet_api_errors.py @@ -2,9 +2,9 @@ from __future__ import annotations import pytest import test_tools as tt +from beekeepy.exceptions import ErrorInResponseError -from helpy import Hf26Asset as Asset -from helpy.exceptions import ErrorInResponseError +from wax.helpy import Hf26Asset as Asset @pytest.fixture() -- GitLab From d5408d88ff2954a62681fe0ba252b83e6f7c984d Mon Sep 17 00:00:00 2001 From: Jakub Ziebinski <ziebinskijakub@gmail.com> Date: Fri, 28 Feb 2025 14:28:27 +0100 Subject: [PATCH 3/3] Implementation of the wrapper for wax --- package/test_tools/__init__.py | 2 + .../__private/wallet/create_accounts.py | 24 +- package/test_tools/__private/wallet/wallet.py | 115 ++---- .../test_tools/__private/wallet/wallet_api.py | 53 +-- package/test_tools/__private/wax_wrapper.py | 340 ++++++++++++++++++ 5 files changed, 406 insertions(+), 128 deletions(-) create mode 100644 package/test_tools/__private/wax_wrapper.py diff --git a/package/test_tools/__init__.py b/package/test_tools/__init__.py index c87230c22..5c7bf06a1 100644 --- a/package/test_tools/__init__.py +++ b/package/test_tools/__init__.py @@ -9,6 +9,7 @@ from test_tools.__private import ( constants, exceptions, paths_to_executables, + wax_wrapper, ) from test_tools.__private.account import Account, PrivateKey, PublicKey from test_tools.__private.alternate_chain_specs import AlternateChainSpecs, HardforkSchedule, InitialVesting @@ -58,6 +59,7 @@ __all__ = [ "OffsetTimeControl", "SpeedUpRateTimeControl", "StartTimeControl", + "wax_wrapper", ] if TYPE_CHECKING: diff --git a/package/test_tools/__private/wallet/create_accounts.py b/package/test_tools/__private/wallet/create_accounts.py index 23e30372a..468a8565f 100644 --- a/package/test_tools/__private/wallet/create_accounts.py +++ b/package/test_tools/__private/wallet/create_accounts.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any from loguru import logger -import wax from schemas.fields.compound import Authority from schemas.fields.hive_int import HiveInt from schemas.operations.account_create_operation import AccountCreateOperation @@ -21,8 +20,12 @@ from test_tools.__private.wallet.constants import ( SimpleTransaction, WalletResponseBase, ) -from wax._private.core.encoders import to_cpp_string -from wax._private.result_tools import expose_result_as_python_string +from test_tools.__private.wax_wrapper import ( + calculate_sig_digest, + calculate_transaction_id, + get_tapos_data, + validate_transaction, +) from wax.helpy._interfaces.asset.asset import Asset if TYPE_CHECKING: @@ -48,7 +51,7 @@ def generate_transaction_template(node: RemoteNode) -> SimpleTransaction: block_id = gdpo.head_block_id # set header - tapos_data = wax.get_tapos_data(block_id.encode()) + tapos_data = get_tapos_data(block_id) ref_block_num = tapos_data.ref_block_num ref_block_prefix = tapos_data.ref_block_prefix @@ -68,12 +71,10 @@ def generate_transaction_template(node: RemoteNode) -> SimpleTransaction: def sign_transaction( node: RemoteNode, transaction: SimpleTransaction, beekeeper_wallet: UnlockedWallet ) -> SimpleTransaction: - transaction_as_bytes = to_cpp_string(transaction.json()) - wax.calculate_transaction_id(transaction_as_bytes) + calculate_transaction_id(transaction) node_config = node.api.database.get_config() - wax_result = wax.calculate_sig_digest(transaction_as_bytes, to_cpp_string(node_config.HIVE_CHAIN_ID)) - sig_digest = expose_result_as_python_string(wax_result) + sig_digest = calculate_sig_digest(transaction, node_config.HIVE_CHAIN_ID) key_to_sign_with = beekeeper_wallet.import_key(private_key=Account("initminer").private_key) time_before = datetime.now() @@ -83,9 +84,7 @@ def sign_transaction( transaction.signatures.append(signature) transaction.signatures = list(set(transaction.signatures)) - - transaction_as_bytes = to_cpp_string(transaction.json()) - wax.validate_transaction(transaction_as_bytes) + validate_transaction(transaction) return transaction @@ -102,10 +101,9 @@ def prepare_transaction( operation.fee = account_creation_fee # type: ignore[assignment] transaction.add_operation(operation) transaction = sign_transaction(node, transaction, beekeeper_wallet) - transaction_as_bytes = to_cpp_string(transaction.json()) return WalletResponseBase( - transaction_id=expose_result_as_python_string(wax.calculate_transaction_id(transaction_as_bytes)), + transaction_id=calculate_transaction_id(transaction), ref_block_num=transaction.ref_block_num, ref_block_prefix=transaction.ref_block_prefix, expiration=transaction.expiration, diff --git a/package/test_tools/__private/wallet/wallet.py b/package/test_tools/__private/wallet/wallet.py index f2aeec1a9..0271a72db 100644 --- a/package/test_tools/__private/wallet/wallet.py +++ b/package/test_tools/__private/wallet/wallet.py @@ -9,7 +9,6 @@ from beekeepy import Beekeeper, Settings from beekeepy.communication import StrictOverseer from beekeepy.exceptions import ErrorInResponseError -import wax from schemas.fields.basic import PublicKey from schemas.fields.hex import Hex from schemas.fields.hive_int import HiveInt @@ -27,15 +26,23 @@ from test_tools.__private.wallet.constants import ( WalletResponse, WalletResponseBase, ) -from test_tools.__private.wallet.create_accounts import ( - create_accounts, -) +from test_tools.__private.wallet.create_accounts import create_accounts from test_tools.__private.wallet.single_transaction_context import SingleTransactionContext from test_tools.__private.wallet.wallet_api import Api -from wax._private.core.encoders import to_cpp_string, to_python_string -from wax._private.result_tools import expose_result_as_python_string +from test_tools.__private.wax_wrapper import ( + calculate_legacy_sig_digest, + calculate_legacy_transaction_id, + calculate_sig_digest, + calculate_transaction_id, + collect_signing_keys, + get_hive_protocol_config, + get_tapos_data, + minimize_required_signatures, + to_wax_authorities, + validate_transaction, + wax_authorities, +) from wax.helpy import Hf26Asset as Asset -from wax.wax_result import python_minimize_required_signatures_data if TYPE_CHECKING: from pathlib import Path @@ -43,11 +50,6 @@ if TYPE_CHECKING: from beekeepy._interface.abc.synchronous.session import Session from beekeepy._interface.abc.synchronous.wallet import UnlockedWallet - from schemas.apis.wallet_bridge_api.fundaments_of_responses import Account as AccountSchema - from schemas.fields.assets.hbd import AssetHbdHF26 - from schemas.fields.assets.hive import AssetHiveHF26 - from schemas.fields.assets.vests import AssetVestsHF26 - from schemas.fields.compound import Authority from schemas.operations import AnyOperation from test_tools.__private.user_handles.handles.wallet_handle import WalletHandle @@ -82,10 +84,8 @@ class Wallet(UserHandleImplementation, ScopedObject): self.run(preconfigure=preconfigure) if self.connected_node is not None: node_version = self.connected_node.api.database.get_version().node_type - protocol_config = { - key.decode(): value.decode() - for key, value in wax.get_hive_protocol_config(to_cpp_string(self.__get_chain_id())).items() - } + protocol_config = get_hive_protocol_config(self.__get_chain_id()) + is_testnet_wax = protocol_config["IS_TEST_NET"] if self._transaction_serialization == "legacy": if node_version == "testnet" and is_testnet_wax == "false": @@ -288,7 +288,7 @@ class Wallet(UserHandleImplementation, ScopedObject): block_id = gdpo.head_block_id # set header - tapos_data = wax.get_tapos_data(block_id.encode()) + tapos_data = get_tapos_data(block_id) ref_block_num = tapos_data.ref_block_num ref_block_prefix = tapos_data.ref_block_prefix @@ -327,11 +327,9 @@ class Wallet(UserHandleImplementation, ScopedObject): return WalletResponse( transaction_id=( - expose_result_as_python_string( - wax.calculate_transaction_id(self.__transaction_to_cpp_string(transaction)) - if self._transaction_serialization == "hf26" - else wax.calculate_legacy_transaction_id(self.__transaction_to_cpp_string(transaction)) - ) + calculate_transaction_id(transaction) + if self._transaction_serialization == "hf26" + else calculate_legacy_transaction_id(transaction) ), block_num=broadcast_response.block_num, transaction_num=broadcast_response.trx_num, @@ -347,13 +345,9 @@ class Wallet(UserHandleImplementation, ScopedObject): return WalletResponseBase( transaction_id=( - expose_result_as_python_string( - wax.calculate_transaction_id(self.__transaction_to_cpp_string(transaction)) - ) + calculate_transaction_id(transaction) if self._transaction_serialization == "hf26" - else expose_result_as_python_string( - wax.calculate_legacy_transaction_id(self.__transaction_to_cpp_string(transaction)) - ) + else calculate_legacy_transaction_id(transaction) ), ref_block_num=transaction.ref_block_num, ref_block_prefix=transaction.ref_block_prefix, @@ -375,13 +369,9 @@ class Wallet(UserHandleImplementation, ScopedObject): def calculate_sig_digest(self, transaction: SimpleTransaction) -> str: chain_id = self.__get_chain_id() return ( - expose_result_as_python_string( - wax.calculate_sig_digest(self.__transaction_to_cpp_string(transaction), to_cpp_string(chain_id)) - ) + calculate_sig_digest(transaction, chain_id) if self._transaction_serialization == "hf26" - else expose_result_as_python_string( - wax.calculate_legacy_sig_digest(self.__transaction_to_cpp_string(transaction), to_cpp_string(chain_id)) - ) + else calculate_legacy_sig_digest(transaction, chain_id) ) def sign_transaction( @@ -393,72 +383,38 @@ class Wallet(UserHandleImplementation, ScopedObject): transaction.signatures.append(signature) transaction.signatures = list(set(transaction.signatures)) - wax.validate_transaction(self.__transaction_to_cpp_string(transaction)) + validate_transaction(transaction) return transaction def reduce_signatures( self, transaction: SimpleTransaction, keys_to_sign_with: list[PublicKey], - retrived_authorities: dict[bytes, wax.python_authorities], + retrived_authorities: dict[bytes, wax_authorities], ) -> list[str] | list[Any]: def retrieve_witness_key(wittnes_name: bytes) -> bytes: get_witness = self._force_connected_node.api.wallet_bridge.get_witness(wittnes_name.decode()) assert get_witness is not None return get_witness.signing_key.encode() - return [ - to_python_string(pub_key) - for pub_key in wax.minimize_required_signatures( - self.__transaction_to_cpp_string(transaction), - python_minimize_required_signatures_data( - chain_id=to_cpp_string(self.__get_chain_id()), - available_keys=[to_cpp_string(key) for key in keys_to_sign_with], - authorities_map=retrived_authorities, - get_witness_key=retrieve_witness_key, - ), - ) - ] + return minimize_required_signatures( + transaction, self.__get_chain_id(), keys_to_sign_with, retrived_authorities, retrieve_witness_key + ) def import_required_keys( self, transaction: SimpleTransaction - ) -> tuple[list[PublicKey], dict[bytes, wax.python_authorities]]: - def list_to_dict(list_: list[Any]) -> dict[bytes, int]: - result: dict[bytes, int] = {} - for i in list_: - result[i[0].encode()] = i[1] - return result - - def to_python_authority(account_authority: Authority) -> wax.python_authority: - return wax.python_authority( - weight_threshold=account_authority.weight_threshold, - account_auths=list_to_dict(account_authority.account_auths), - key_auths=list_to_dict(account_authority.key_auths), - ) + ) -> tuple[list[PublicKey], dict[bytes, wax_authorities]]: + retrived_authorities: dict[bytes, wax_authorities] = {} - def to_python_authorities( - account_authorities: AccountSchema[AssetHiveHF26, AssetHbdHF26, AssetVestsHF26] - ) -> wax.python_authorities: - return wax.python_authorities( - active=to_python_authority(account_authorities.active), - owner=to_python_authority(account_authorities.owner), - posting=to_python_authority(account_authorities.posting), - ) - - retrived_authorities: dict[bytes, wax.python_authorities] = {} - - def retrieve_authorities(account_names: list[bytes]) -> dict[bytes, wax.python_authorities]: + def retrieve_authorities(account_names: list[bytes]) -> dict[bytes, wax_authorities]: accounts = self._force_connected_node.api.wallet_bridge.get_accounts( [account_name.decode() for account_name in account_names] ) - retrived_authoritity = {acc.name.encode(): to_python_authorities(acc) for acc in accounts} + retrived_authoritity = {acc.name.encode(): to_wax_authorities(acc) for acc in accounts} retrived_authorities.update(retrived_authoritity) return retrived_authoritity - keys_for_signing = [ - key.decode() - for key in wax.collect_signing_keys(self.__transaction_to_cpp_string(transaction), retrieve_authorities) - ] + keys_for_signing = collect_signing_keys(transaction, retrieve_authorities) if self._use_authority != {}: account_name = next(iter(self._use_authority.keys())) @@ -477,6 +433,3 @@ class Wallet(UserHandleImplementation, ScopedObject): if broadcast is None: broadcast = True return SingleTransactionContext(self, broadcast=broadcast, blocking=blocking) - - def __transaction_to_cpp_string(self, transaction: SimpleTransaction) -> bytes: - return to_cpp_string(transaction.json()) diff --git a/package/test_tools/__private/wallet/wallet_api.py b/package/test_tools/__private/wallet/wallet_api.py index 6afa0603b..983e80d37 100644 --- a/package/test_tools/__private/wallet/wallet_api.py +++ b/package/test_tools/__private/wallet/wallet_api.py @@ -6,7 +6,6 @@ from datetime import datetime, timedelta from functools import wraps from typing import TYPE_CHECKING, Any, ParamSpec, cast -from schemas.fields.assets.hive import AssetHiveHF26 from schemas.fields.basic import AccountName, EmptyList, PrivateKey, PublicKey from schemas.fields.compound import Authority, HbdExchangeRate, LegacyChainProperties, Proposal from schemas.operations import AnyOperation @@ -69,17 +68,15 @@ from test_tools.__private.wallet.constants import ( from test_tools.__private.wallet.create_accounts import ( get_authority, ) -from wax import ( +from test_tools.__private.wax_wrapper import ( calculate_public_key, - create_wax_foundation, decode_encrypted_memo, encode_encrypted_memo, generate_password_based_private_key, suggest_brain_key, ) -from wax._private.core.encoders import to_cpp_string, to_python_string +from test_tools.__private.wax_wrapper import estimate_hive_collateral as wax_estimate_hive_collateral from wax._private.exceptions import WaxValidationFailedError -from wax._private.result_tools import expose_result_as_python_string, validate_wax_result from wax.helpy import Hf26Asset as Asset if TYPE_CHECKING: @@ -118,6 +115,7 @@ if TYPE_CHECKING: ) from schemas.fields.assets._base import AssetHF26 from schemas.fields.assets.hbd import AssetHbdHF26 + from schemas.fields.assets.hive import AssetHiveHF26 from schemas.fields.assets.vests import AssetVestsHF26 from schemas.fields.hex import Hex from schemas.fields.hive_int import HiveInt @@ -245,9 +243,7 @@ class Api: def __check_memo(self, account: AccountNameApiType, memo: str) -> None: if isinstance(memo, PrivateKey): try: - wax_result = calculate_public_key(wif=to_cpp_string(memo)) - validate_wax_result(wax_result) - public_key = expose_result_as_python_string(wax_result) + public_key = calculate_public_key(wif=memo) except WaxValidationFailedError: return get_account = self.get_account(account_name=account) @@ -842,11 +838,11 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: The decrypted memo. """ - encrypted_memo = decode_encrypted_memo(to_cpp_string(memo)) + encrypted_memo = decode_encrypted_memo(memo) decrypt_memo = self.__wallet.beekeeper_wallet.decrypt_data( - from_key=to_python_string(encrypted_memo.main_encryption_key), # type: ignore[arg-type] - to_key=to_python_string(encrypted_memo.other_encryption_key), # type: ignore[arg-type] - content=to_python_string(encrypted_memo.encrypted_content), + from_key=PublicKey(encrypted_memo.main_encryption_key), + to_key=PublicKey(encrypted_memo.other_encryption_key), + content=encrypted_memo.encrypted_content, ) if decrypt_memo.startswith("#"): @@ -1165,19 +1161,10 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: Estimated hive collateral. """ - wax_base_api = create_wax_foundation() current_median_history = self.get_feed_history().current_median_history current_min_history = self.get_feed_history().current_min_history - wax_asset = wax_base_api.estimate_hive_collateral( - current_median_history.base.dict(), - current_median_history.quote.dict(), - current_min_history.base.dict(), - current_min_history.quote.dict(), - hbd_amount_to_get.dict(), - ) - - return AssetHiveHF26(amount=int(wax_asset.amount), nai=wax_asset.nai, precision=wax_asset.precision) + return wax_estimate_hive_collateral(hbd_amount_to_get, current_median_history, current_min_history) @warn_if_only_result_set() def exit(self, only_result: bool | None = None) -> None: # noqa: ARG002 A003 @@ -1395,14 +1382,12 @@ class Api: ) encrypted_memo = proxy_encrypt_data(from_account.memo_key, to_account.memo_key, memo) - encoded_encrypted_memo = encode_encrypted_memo( - encrypted_content=to_cpp_string(encrypted_memo), - main_encryption_key=to_cpp_string(from_account.memo_key), - other_encryption_key=to_cpp_string(to_account.memo_key), + return encode_encrypted_memo( + encrypted_content=encrypted_memo, + main_encryption_key=from_account.memo_key, + other_encryption_key=to_account.memo_key, ) - return to_python_string(encoded_encrypted_memo) - @warn_if_only_result_set() def get_feed_history(self, only_result: bool | None = None) -> GetFeedHistory: # noqa: ARG002 """ @@ -1484,8 +1469,8 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: A list containing the associated public key and the private key in WIF format. """ - wax_result = generate_password_based_private_key(account=account, role=role, password=password) - return [to_python_string(wax_result.associated_public_key), to_python_string(wax_result.wif_private_key)] + result = generate_password_based_private_key(account=account, role=role, password=password) + return [result.associated_public_key, result.wif_private_key] @warn_if_only_result_set() def get_prototype_operation( @@ -2151,12 +2136,12 @@ class Api: :param only_result: This argument is no longer active and should not be provided. :return: A dictionary containing the suggested brain key, associated WIF private key, and public key. """ - wax_result = suggest_brain_key() + result = suggest_brain_key() return { - "brain_priv_key": to_python_string(wax_result.brain_key), - "wif_priv_key": to_python_string(wax_result.wif_private_key), - "pub_key": to_python_string(wax_result.associated_public_key), + "brain_priv_key": result.brain_key, + "wif_priv_key": result.wif_private_key, + "pub_key": result.associated_public_key, } @require_unlocked_wallet diff --git a/package/test_tools/__private/wax_wrapper.py b/package/test_tools/__private/wax_wrapper.py new file mode 100644 index 000000000..075653117 --- /dev/null +++ b/package/test_tools/__private/wax_wrapper.py @@ -0,0 +1,340 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from schemas.fields.assets.hive import AssetHiveHF26 +from wax import create_wax_foundation +from wax._private.result_tools import ( + expose_result_as_python_string, + to_cpp_string, + to_python_string, + validate_wax_result, +) +from wax.cpp_python_bridge import calculate_legacy_sig_digest as wax_calculate_legacy_sig_digest +from wax.cpp_python_bridge import calculate_legacy_transaction_id as wax_calculate_legacy_transaction_id +from wax.cpp_python_bridge import calculate_public_key as wax_calculate_public_key +from wax.cpp_python_bridge import calculate_sig_digest as wax_calculate_sig_digest +from wax.cpp_python_bridge import calculate_transaction_id as wax_calculate_transaction_id +from wax.cpp_python_bridge import collect_signing_keys as wax_collect_signing_keys +from wax.cpp_python_bridge import decode_encrypted_memo as wax_decode_encrypted_memo +from wax.cpp_python_bridge import encode_encrypted_memo as wax_encode_encrypted_memo +from wax.cpp_python_bridge import generate_password_based_private_key as wax_generate_password_based_private_key +from wax.cpp_python_bridge import get_hive_protocol_config as wax_get_hive_protocol_config +from wax.cpp_python_bridge import get_tapos_data as wax_get_tapos_data +from wax.cpp_python_bridge import minimize_required_signatures as wax_minimize_required_signatures +from wax.cpp_python_bridge import validate_transaction as wax_validate_transaction +from wax.wax_result import ( + python_authorities, + python_authority, + python_minimize_required_signatures_data, +) + +if TYPE_CHECKING: + from collections.abc import Callable + + from schemas.apis.wallet_bridge_api.fundaments_of_responses import Account as AccountSchema + from schemas.fields.assets.hbd import AssetHbdHF26 + from schemas.fields.assets.vests import AssetVestsHF26 + from schemas.fields.basic import PublicKey + from schemas.fields.compound import Authority, Price + from schemas.transaction import Transaction + from test_tools.__private.wallet.constants import AccountNameApiType + from wax.models.key_data import IBrainKeyData + from wax.wax_result import python_ref_block_data + + +wax_authorities = python_authorities +wax_authority = python_authority + + +@dataclass +class WaxPrivateKeyData: + wif_private_key: str + associated_public_key: str + + +@dataclass +class WaxEncryptedMemo: + main_encryption_key: str + other_encryption_key: str + encrypted_content: str + + +def to_wax_authority(account_authority: Authority) -> wax_authority: + """ + Convert the given account authority (api form) to python authority (wax form). + + Args: + ---- + account_authority: The authority (not account!) object returned from the API. + """ + + def list_to_dict(list_: list[Any]) -> dict[bytes, int]: + result: dict[bytes, int] = {} + for i in list_: + result[i[0].encode()] = i[1] + return result + + return wax_authority( + weight_threshold=account_authority.weight_threshold, + account_auths=list_to_dict(account_authority.account_auths), + key_auths=list_to_dict(account_authority.key_auths), + ) + + +def to_wax_authorities( + account_authorities: AccountSchema[AssetHiveHF26, AssetHbdHF26, AssetVestsHF26] +) -> wax_authorities: + """ + Convert the given account authorities (api form) to python authorities (wax form). + + Args: + ---- + account_authorities: The account object returned from the API. + + Returns: + ------- + The converted python authorities. + """ + return wax_authorities( + active=to_wax_authority(account_authorities.active), + owner=to_wax_authority(account_authorities.owner), + posting=to_wax_authority(account_authorities.posting), + ) + + +def calculate_public_key(wif: str) -> str: + result = wax_calculate_public_key(to_cpp_string(wif)) + validate_wax_result(result) + return expose_result_as_python_string(result) + + +def get_tapos_data(head_block_id: str) -> python_ref_block_data: + return wax_get_tapos_data(to_cpp_string(head_block_id)) + + +def validate_transaction(transaction: Transaction) -> None: + """ + Validate the given transaction. + + Args: + ---- + transaction: The transaction to validate. + + Raises: + ------ + WaxValidationError: If the transaction is invalid. + """ + result = wax_validate_transaction(to_cpp_string(transaction.json())) + validate_wax_result(result) + + +def calculate_transaction_id(transaction: Transaction) -> str: + """ + Calculate the transaction id from the given transaction. + + Args: + ---- + transaction: The transaction to calculate the id for. + + Returns: + ------- + The calculated transaction id. + + Raises: + ------ + WaxValidationError: If the transaction id could not be calculated. + """ + result = wax_calculate_transaction_id(to_cpp_string(transaction.json())) + validate_wax_result(result) + return expose_result_as_python_string(result) + + +def calculate_legacy_transaction_id(transaction: Transaction) -> str: + """ + Calculate the transaction id from the given transaction in the legacy format. + + Args: + ---- + transaction: The transaction to calculate the id for. + + Returns: + ------- + The calculated transaction id in the legacy format. + + Raises: + ------ + WaxValidationError: If the transaction id could not be calculated. + """ + result = wax_calculate_legacy_transaction_id(to_cpp_string(transaction.json())) + validate_wax_result(result) + return expose_result_as_python_string(result) + + +def calculate_sig_digest(transaction: Transaction, chain_id: str) -> str: + """ + Calculate the sig digest from the given transaction and chain id. + + Args: + ---- + transaction: The transaction to calculate the sig digest for. + chain_id: The chain id to calculate the sig digest for. + + Returns: + ------- + The calculated signature digest. + + Raises: + ------ + WaxValidationError: If the signature digest could not be calculated. + """ + result = wax_calculate_sig_digest(to_cpp_string(transaction.json()), to_cpp_string(chain_id)) + validate_wax_result(result) + return expose_result_as_python_string(result) + + +def calculate_legacy_sig_digest(transaction: Transaction, chain_id: str) -> str: + """ + Calculate the sig digest from the given transaction and chain id in the legacy format. + + Args: + ---- + transaction: The transaction to calculate the sig digest for. + chain_id: The chain id to calculate the sig digest for. + + Returns: + ------- + The calculated signature digest in the legacy format. + + Raises: + ------ + WaxValidationError: If the sig digest could not be calculated. + """ + result = wax_calculate_legacy_sig_digest(to_cpp_string(transaction.json()), to_cpp_string(chain_id)) + validate_wax_result(result) + return expose_result_as_python_string(result) + + +def get_hive_protocol_config(chain_id: str) -> dict[str, str]: + return { + to_python_string(key): to_python_string(value) + for key, value in wax_get_hive_protocol_config(to_cpp_string(chain_id)).items() + } + + +def minimize_required_signatures( + transaction: Transaction, + chain_id: str, + available_keys: list[PublicKey], + retrived_authorities: dict[bytes, wax_authorities], + get_witness_key: Callable[[bytes], bytes], +) -> list[str]: + """ + Minimize the required signatures for the given transaction. + + Args: + ---- + transaction: The transaction to minimize the required signatures for. + chain_id: chain id of the current chain type. + available_keys: The available keys. + retrived_authorities: The retrieved authorities. + get_witness_key: The callable object to get the witness key. + + Returns: + ------- + The minimized required signatures. + """ + result = wax_minimize_required_signatures( + to_cpp_string(transaction.json()), + minimize_required_signatures_data=python_minimize_required_signatures_data( + chain_id=to_cpp_string(chain_id), + available_keys=[to_cpp_string(key) for key in available_keys], + authorities_map=retrived_authorities, + get_witness_key=get_witness_key, + ), + ) + return [to_python_string(signature) for signature in result] + + +def collect_signing_keys( + transaction: Transaction, retrieve_authorities: Callable[[list[bytes]], dict[bytes, wax_authorities]] +) -> list[str]: + """ + Collect the signing keys for the given transaction. + + Args: + ---- + transaction: The transaction to collect the signing keys for. + retrieve_authorities: The callable to retrieve the authorities. + + Returns: + ------- + The collected signing keys. + """ + return [ + to_python_string(key) + for key in wax_collect_signing_keys(to_cpp_string(transaction.json()), retrieve_authorities) + ] + + +def estimate_hive_collateral( + hbd_amount_to_get: AssetHbdHF26, + current_median_history: Price[AssetHiveHF26, AssetHbdHF26, AssetVestsHF26], + current_min_history: Price[AssetHiveHF26, AssetHbdHF26, AssetVestsHF26], +) -> AssetHiveHF26: + """ + Estimate the hive collateral for the given HBD amount to get. + + Args: + ---- + _____ + hbd_amount_to_get: The HBD amount to get. + current_median_history: The current median history (from the `get_feed_history`). + current_min_history: The current min history (from the `get_feed_history`). + + Returns: + ------- + The estimated hive collateral. + """ + wax_base_api = create_wax_foundation() + wax_asset = wax_base_api.estimate_hive_collateral( + current_median_history.base.dict(), + current_median_history.quote.dict(), + current_min_history.base.dict(), + current_min_history.quote.dict(), + hbd_amount_to_get.dict(), + ) + return AssetHiveHF26(amount=int(wax_asset.amount), nai=wax_asset.nai, precision=wax_asset.precision) + + +def generate_password_based_private_key(account: AccountNameApiType, role: str, password: str) -> WaxPrivateKeyData: + """Generate a password based private key for the given account and role.""" + wax_result = wax_generate_password_based_private_key(account, role, password) + return WaxPrivateKeyData( + wif_private_key=to_python_string(wax_result.wif_private_key), + associated_public_key=to_python_string(wax_result.associated_public_key), + ) + + +def suggest_brain_key() -> IBrainKeyData: + """Suggest a brain key.""" + wax_base_api = create_wax_foundation() + return wax_base_api.suggest_brain_key() + + +def decode_encrypted_memo(encoded_memo: str) -> WaxEncryptedMemo: + wax_result = wax_decode_encrypted_memo(to_cpp_string(encoded_memo)) + return WaxEncryptedMemo( + main_encryption_key=to_python_string(wax_result.main_encryption_key), + other_encryption_key=to_python_string(wax_result.other_encryption_key), + encrypted_content=to_python_string(wax_result.encrypted_content), + ) + + +def encode_encrypted_memo(encrypted_content: str, main_encryption_key: str, other_encryption_key: str = "") -> str: + return to_python_string( + wax_encode_encrypted_memo( + to_cpp_string(encrypted_content), to_cpp_string(main_encryption_key), to_cpp_string(other_encryption_key) + ) + ) -- GitLab