From 7b86ff6039703a5cc0b05f3c3f4926d0db529ada Mon Sep 17 00:00:00 2001 From: philogicae Date: Mon, 24 Feb 2025 14:31:35 +0200 Subject: [PATCH 1/7] Fix programs for SOL payment chain (#201) --- src/aleph/sdk/client/abstract.py | 2 ++ src/aleph/sdk/client/authenticated_http.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/aleph/sdk/client/abstract.py b/src/aleph/sdk/client/abstract.py index 7f9fed8e..2816aa3d 100644 --- a/src/aleph/sdk/client/abstract.py +++ b/src/aleph/sdk/client/abstract.py @@ -364,6 +364,7 @@ async def create_program( runtime: str, metadata: Optional[dict[str, Any]] = None, address: Optional[str] = None, + payment: Optional[Payment] = None, vcpus: Optional[int] = None, memory: Optional[int] = None, timeout_seconds: Optional[float] = None, @@ -387,6 +388,7 @@ async def create_program( :param runtime: Runtime to use :param metadata: Metadata to attach to the message :param address: Address to use (Default: account.get_address()) + :param payment: Payment method used to pay for the program (Default: None) :param vcpus: Number of vCPUs to allocate (Default: 1) :param memory: Memory in MB for the VM to be allocated (Default: 128) :param timeout_seconds: Timeout in seconds (Default: 30.0) diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 9bb9a1e7..1d0d69d7 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -410,6 +410,7 @@ async def create_program( runtime: str, metadata: Optional[dict[str, Any]] = None, address: Optional[str] = None, + payment: Optional[Payment] = None, vcpus: Optional[int] = None, memory: Optional[int] = None, timeout_seconds: Optional[float] = None, @@ -433,6 +434,7 @@ async def create_program( runtime=runtime, metadata=metadata, address=address, + payment=payment, vcpus=vcpus, memory=memory, timeout_seconds=timeout_seconds, From 43bdc76a2d8787f0e26b0508552ef0e3583ad5a6 Mon Sep 17 00:00:00 2001 From: Hugo Herter Date: Thu, 27 Feb 2025 17:10:38 +0100 Subject: [PATCH 2/7] Fix: Remove references to slow server api1.aleph.im Server api1.aleph.im is very slow and outdated (Core i7 from 2018, up to 40 seconds to respond to `/metrics` in the monitoring). We suspect that this causes issues in the monitoring and performance of the network. This branch removes all references to api1 and replaces them with api3 where relevant. --- tests/integration/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/config.py b/tests/integration/config.py index 3e613c18..bd78ef1a 100644 --- a/tests/integration/config.py +++ b/tests/integration/config.py @@ -1,3 +1,3 @@ -TARGET_NODE = "https://api1.aleph.im" +TARGET_NODE = "https://api3.aleph.im" REFERENCE_NODE = "https://api2.aleph.im" TEST_CHANNEL = "INTEGRATION_TESTS" From ef40aa76f2cc232470c9c2ea372425820b058d93 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 11 Mar 2025 10:32:40 +0100 Subject: [PATCH 3/7] feat: aleph-pytezos has been renamed to pytezos-crypto (#206) --- pyproject.toml | 2 +- src/aleph/sdk/chains/tezos.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3fd02d17..42156baf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,8 +80,8 @@ optional-dependencies.substrate = [ "substrate-interface", ] optional-dependencies.tezos = [ - "aleph-pytezos==3.13.4", "pynacl", + "pytezos-crypto==3.13.4.1", ] urls.Documentation = "https://aleph.im/" urls.Homepage = "https://github.com/aleph-im/aleph-sdk-python" diff --git a/src/aleph/sdk/chains/tezos.py b/src/aleph/sdk/chains/tezos.py index cffa3e78..c4ee08ab 100644 --- a/src/aleph/sdk/chains/tezos.py +++ b/src/aleph/sdk/chains/tezos.py @@ -2,9 +2,9 @@ from pathlib import Path from typing import Dict, Optional, Union -from aleph_pytezos.crypto.key import Key from nacl.public import SealedBox from nacl.signing import SigningKey +from pytezos_crypto.key import Key from .common import BaseAccount, get_fallback_private_key, get_verification_buffer From 08927357be7b9f19a6c34aa4b45ef0a2c763f9b7 Mon Sep 17 00:00:00 2001 From: Hugo Herter Date: Tue, 11 Mar 2025 10:36:44 +0100 Subject: [PATCH 4/7] Fix: Redundant dependency in pyproject --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 42156baf..fb0792f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,6 @@ optional-dependencies.substrate = [ "substrate-interface", ] optional-dependencies.tezos = [ - "pynacl", "pytezos-crypto==3.13.4.1", ] urls.Documentation = "https://aleph.im/" From cb5ce9fd789b421f57d0c42ef0fb497511c57d6f Mon Sep 17 00:00:00 2001 From: Hugo Herter Date: Wed, 5 Mar 2025 12:05:34 +0100 Subject: [PATCH 5/7] Fix: Tests were not running on macOS --- .github/workflows/pytest.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f1af47c5..75bc8193 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,9 +6,6 @@ on: branches: - main schedule: - # Run every night at 04:00 (GitHub Actions timezone) - # in order to catch when unfrozen dependency updates - # break the use of the library. - cron: '4 0 * * *' jobs: @@ -16,8 +13,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.9", "3.10", "3.11", "3.12" ] - os: [ubuntu-22.04, ubuntu-24.04] + python-version: ["3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-22.04, ubuntu-24.04, macos-14, macos-15] runs-on: ${{ matrix.os }} steps: @@ -26,15 +23,24 @@ jobs: with: python-version: ${{ matrix.python-version }} - - run: | + - name: "apt-get install" + run: | sudo apt-get update - sudo apt-get install -y python3-pip libsodium-dev + sudo apt-get install -y python3-pip libsodium-dev libgmp-dev + if: runner.os == 'Linux' - run: | + brew install libsodium + echo "DYLD_LIBRARY_PATH=$(brew --prefix libsodium)/lib" >> $GITHUB_ENV + if: runner.os == 'macOS' + + - name: "Install Hatch" + run: | python3 -m venv /tmp/venv /tmp/venv/bin/python -m pip install --upgrade pip hatch coverage - - run: | + - name: "Run Tests" + run: | /tmp/venv/bin/pip freeze /tmp/venv/bin/hatch run testing:pip freeze /tmp/venv/bin/hatch run testing:test From e40033f9461cb3c021e28aa8a39faa0826fd05c9 Mon Sep 17 00:00:00 2001 From: nesitor Date: Tue, 8 Apr 2025 16:50:09 +0200 Subject: [PATCH 6/7] Feature: Implement Sonic Blockchain. (#210) --- pyproject.toml | 6 +++--- src/aleph/sdk/account.py | 1 + src/aleph/sdk/conf.py | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fb0792f9..edb37f7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,15 +30,15 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message>=0.6", + "aleph-message @ git+https://github.com/aleph-im/aleph-message.git@andres-feature-integrate_sonic_blockchain", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", - "pynacl==1.5", # Needed now as default with _load_account changement + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", diff --git a/src/aleph/sdk/account.py b/src/aleph/sdk/account.py index 872ee3c4..15dd79d1 100644 --- a/src/aleph/sdk/account.py +++ b/src/aleph/sdk/account.py @@ -36,6 +36,7 @@ Chain.OPTIMISM: EVMAccount, Chain.POL: EVMAccount, Chain.SOL: SOLAccount, + Chain.SONIC: EVMAccount, Chain.WORLDCHAIN: EVMAccount, Chain.ZORA: EVMAccount, } diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index c925a05e..6a1fcf46 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -163,6 +163,10 @@ class Settings(BaseSettings): chain_id=137, rpc="https://polygon.gateway.tenderly.co", ), + Chain.SONIC: ChainInfo( + chain_id=146, + rpc="https://rpc.soniclabs.com", + ), Chain.WORLDCHAIN: ChainInfo( chain_id=480, rpc="https://worldchain-mainnet.gateway.tenderly.co", @@ -189,6 +193,7 @@ class Settings(BaseSettings): CHAINS_MODE_ACTIVE: Optional[bool] = None CHAINS_OPTIMISM_ACTIVE: Optional[bool] = None CHAINS_POL_ACTIVE: Optional[bool] = None + CHAINS_SONIC_ACTIVE: Optional[bool] = None CHAINS_WORLDCHAIN_ACTIVE: Optional[bool] = None CHAINS_ZORA_ACTIVE: Optional[bool] = None @@ -208,6 +213,7 @@ class Settings(BaseSettings): CHAINS_MODE_RPC: Optional[str] = None CHAINS_OPTIMISM_RPC: Optional[str] = None CHAINS_POL_RPC: Optional[str] = None + CHAINS_SONIC_RPC: Optional[str] = None CHAINS_WORLDCHAIN_RPC: Optional[str] = None CHAINS_ZORA_RPC: Optional[str] = None From 0c848b59f9696375fbe100771ef082f3e621bf13 Mon Sep 17 00:00:00 2001 From: Antony JIN <91880456+Antonyjin@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:40:22 +0200 Subject: [PATCH 7/7] Upgrade pydantic version (#179) * Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. * fix: lint tests were failing - Updated all instances of **extra_fields to ensure proper handling of Optional dictionaries using `(extra_fields or {})` pattern. - Added proper return statements in `AlephHttpClient.get_message_status` to return parsed JSON data as a `MessageStatus` object. - Updated `Settings` class in `conf.py` to correct DNS resolvers type and simplify the `model_config` definition. - Refactored `parse_volume` to ensure correct handling of Mapping types and MachineVolume types, avoiding TypeErrors. - Improved field validation and model validation in `SignedPubKeyHeader` by using correct Pydantic v2 validation decorators and ensuring compatibility with the new model behavior. - Applied formatting and consistency fixes for `model_dump` usage and indentation improvements in test files. * feat: add pyproject-fmt * fix: run pyproject-fmt * Post-SOL fixes (#178) * Missing chain field on auth * Fix Signature of Solana operation for CRN * Add export_private_key func for accounts * Improve _load_account * Add chain arg to _load_account * Increase default HTTP_REQUEST_TIMEOUT * Typing --------- Co-authored-by: Olivier Le Thanh Duong * Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. * fix: add explicit float type for HTTP_REQUEST_TIMEOUT to comply with Pydantic v2 requirements Pydantic v2 requires explicit type annotations for fields, so added `float` to ensure proper validation of HTTP_REQUEST_TIMEOUT. * Fix: Linting tests did not pass: * Fix: Project don't use the good version of aleph-message There were changes made on aleph-message on the main branch about pydantic version. Using the version by the url and then change it later after the release. * fix: Wrong aleph-message version * Fix: list[str] rise an error in ubuntu 20.04 Using List from typing instead to assure the compatibility between python3.8 and above * style: isort * fix: Hugo comments * Add pydantic for better mypy tests + Fixes * fix: Changing version of aleph-message * style: Missing type for URL * style: Missing type for URL * fix: Changing version of aleph-message and fix mypy Changing the version from the branch to the main of aleph-message mypy rose some errors about missing name argument, so setting the as None because they are optional * fix: Changing version of aleph-message * fix: Changing version of pytezos * Changes for new pricing system (#199) - Move/improve flow code parts from CLI to SDK - Add utils functions - Add `make_instance_content` and `make_program_content` - Refactor `create_instance` and `create_program` - Add `get_estimated_price` - Fixes for mypy/ruff/pytest - Minor improvements - Remove firecracker rootfs hashes for instances * Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. * fix: lint tests were failing - Updated all instances of **extra_fields to ensure proper handling of Optional dictionaries using `(extra_fields or {})` pattern. - Added proper return statements in `AlephHttpClient.get_message_status` to return parsed JSON data as a `MessageStatus` object. - Updated `Settings` class in `conf.py` to correct DNS resolvers type and simplify the `model_config` definition. - Refactored `parse_volume` to ensure correct handling of Mapping types and MachineVolume types, avoiding TypeErrors. - Improved field validation and model validation in `SignedPubKeyHeader` by using correct Pydantic v2 validation decorators and ensuring compatibility with the new model behavior. - Applied formatting and consistency fixes for `model_dump` usage and indentation improvements in test files. * Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. * Fix: Linting tests did not pass: * fix: Wrong aleph-message version * fix: Hugo comments * Add pydantic for better mypy tests + Fixes * fix: Changing version of aleph-message * style: Missing type for URL * fix: Changing version of aleph-message and fix mypy Changing the version from the branch to the main of aleph-message mypy rose some errors about missing name argument, so setting the as None because they are optional * fix: Changing version of aleph-message * Fix: Missing pydantic_core and wrong version of tezos * Fix: Access to PersistentVolumeSizeMib is incompatible after migrating to Pydantic2 Using model_validate to access it * Fix: Wrong name given to the variable * Style: isort * Fix: PersistentVolumeSizeMib no longer exist This class has been deleted from aleph_message and the size is now inside the PersistentVolume class * Fix: Update last `aleph-message` version and use again `PersistentVolumeSizeMib` class * fix: invalid signature cause by `0x` + signature.hex() * fix: add '0x' to the signature if not here (error happenings only on unit test) * Refactor: Apply the `.hex()` quick fix on the ETHAccount class instead on the base one as other chains can be affected. * fix: pydantic model should use `.model_dump()` instead of `dict()` * fix: add dummy signature for unauthenticated price estimates When estimating prices without authentication, there's no valid signature available. This fix uses a dummy signature so that message validation passes in these cases. --------- Co-authored-by: Laurent Peuch Co-authored-by: philogicae Co-authored-by: Olivier Le Thanh Duong Co-authored-by: philogicae <38438271+philogicae@users.noreply.github.com> Co-authored-by: Andres D. Molins Co-authored-by: 1yam Co-authored-by: Andres D. Molins --- pyproject.toml | 8 +-- src/aleph/sdk/chains/common.py | 1 + src/aleph/sdk/chains/ethereum.py | 18 ++++++- src/aleph/sdk/chains/remote.py | 4 +- src/aleph/sdk/client/authenticated_http.py | 20 ++++---- src/aleph/sdk/client/http.py | 37 +++++++------- .../sdk/client/vm_confidential_client.py | 2 +- src/aleph/sdk/conf.py | 43 ++++++++-------- src/aleph/sdk/domain.py | 4 +- src/aleph/sdk/query/responses.py | 10 ++-- src/aleph/sdk/types.py | 10 ++-- src/aleph/sdk/utils.py | 6 +-- src/aleph/sdk/vm/cache.py | 2 +- tests/unit/aleph_vm_authentication.py | 49 +++++++++---------- tests/unit/conftest.py | 8 +-- tests/unit/test_price.py | 2 +- tests/unit/test_remote_account.py | 2 +- tests/unit/test_utils.py | 18 +++---- tests/unit/test_vm_client.py | 4 +- 19 files changed, 134 insertions(+), 114 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index edb37f7f..691596fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,15 +30,17 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message @ git+https://github.com/aleph-im/aleph-message.git@andres-feature-integrate_sonic_blockchain", + "aleph-message>=1", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", - "pynacl==1.5", # Needed now as default with _load_account changement + "pydantic>=2,<3", + "pydantic-settings>=2", + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", diff --git a/src/aleph/sdk/chains/common.py b/src/aleph/sdk/chains/common.py index 0a90183c..d2714d62 100644 --- a/src/aleph/sdk/chains/common.py +++ b/src/aleph/sdk/chains/common.py @@ -73,6 +73,7 @@ async def sign_message(self, message: Dict) -> Dict: message = self._setup_sender(message) signature = await self.sign_raw(get_verification_buffer(message)) message["signature"] = signature.hex() + return message @abstractmethod diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index c185d174..863e2bbf 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -2,7 +2,7 @@ import base64 from decimal import Decimal from pathlib import Path -from typing import Awaitable, Optional, Union +from typing import Awaitable, Dict, Optional, Union from aleph_message.models import Chain from eth_account import Account # type: ignore @@ -80,6 +80,22 @@ async def sign_raw(self, buffer: bytes) -> bytes: sig = self._account.sign_message(msghash) return sig["signature"] + async def sign_message(self, message: Dict) -> Dict: + """ + Returns a signed message from an aleph.im message. + Args: + message: Message to sign + Returns: + Dict: Signed message + """ + signed_message = await super().sign_message(message) + + # Apply that fix as seems that sometimes the .hex() method doesn't add the 0x str at the beginning + if not str(signed_message["signature"]).startswith("0x"): + signed_message["signature"] = "0x" + signed_message["signature"] + + return signed_message + def connect_chain(self, chain: Optional[Chain] = None): self.chain = chain if self.chain: diff --git a/src/aleph/sdk/chains/remote.py b/src/aleph/sdk/chains/remote.py index 931b68f3..917cf39b 100644 --- a/src/aleph/sdk/chains/remote.py +++ b/src/aleph/sdk/chains/remote.py @@ -52,7 +52,7 @@ async def from_crypto_host( session = aiohttp.ClientSession(connector=connector) async with session.get(f"{host}/properties") as response: - response.raise_for_status() + await response.raise_for_status() data = await response.json() properties = AccountProperties(**data) @@ -75,7 +75,7 @@ def private_key(self): async def sign_message(self, message: Dict) -> Dict: """Sign a message inplace.""" async with self._session.post(f"{self._host}/sign", json=message) as response: - response.raise_for_status() + await response.raise_for_status() return await response.json() async def sign_raw(self, buffer: bytes) -> bytes: diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 1d0d69d7..2975e112 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -251,7 +251,7 @@ async def _broadcast( url = "/api/v0/messages" logger.debug(f"Posting message on {url}") - message_dict = message.dict(include=self.BROADCAST_MESSAGE_FIELDS) + message_dict = message.model_dump(include=self.BROADCAST_MESSAGE_FIELDS) async with self.http_session.post( url, json={ @@ -293,7 +293,7 @@ async def create_post( ) message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.post, channel=channel, allow_inlining=inline, @@ -321,7 +321,7 @@ async def create_aggregate( ) message, status, _ = await self.submit( - content=content_.dict(exclude_none=True), + content=content_.model_dump(exclude_none=True), message_type=MessageType.aggregate, channel=channel, allow_inlining=inline, @@ -395,7 +395,7 @@ async def create_store( content = StoreContent.parse_obj(values) message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.store, channel=channel, allow_inlining=True, @@ -449,7 +449,7 @@ async def create_program( ) message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.program, channel=channel, storage_engine=storage_engine, @@ -525,7 +525,7 @@ async def create_instance( ) message, status, response = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.instance, channel=channel, storage_engine=storage_engine, @@ -573,7 +573,7 @@ async def forget( ) message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.forget, channel=channel, storage_engine=storage_engine, @@ -617,11 +617,11 @@ async def _storage_push_file_with_message( # Prepare the STORE message message = await self.generate_signed_message( message_type=MessageType.store, - content=store_content.dict(exclude_none=True), + content=store_content.model_dump(exclude_none=True), channel=channel, ) metadata = { - "message": message.dict(exclude_none=True), + "message": message.model_dump(exclude_none=True), "sync": sync, } data.add_field( @@ -665,7 +665,7 @@ async def _upload_file_native( item_hash=ItemHash(file_hash), mime_type=mime_type, # type: ignore time=time.time(), - **extra_fields, + **(extra_fields or {}), ) message, _ = await self._storage_push_file_with_message( file_content=file_content, diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index f4e8b898..3d42d490 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -191,7 +191,7 @@ async def get_posts( posts: List[Post] = [] for post_raw in posts_raw: try: - posts.append(Post.parse_obj(post_raw)) + posts.append(Post.model_validate(post_raw)) except ValidationError as e: if not ignore_invalid_messages: raise e @@ -462,30 +462,31 @@ async def get_estimated_price( self, content: ExecutableContent, ) -> PriceResponse: - cleaned_content = content.dict(exclude_none=True) + cleaned_content = content.model_dump(exclude_none=True) item_content: str = json.dumps( cleaned_content, separators=(",", ":"), default=extended_json_encoder, ) - message = parse_message( - dict( - sender=content.address, - chain=Chain.ETH, - type=( - MessageType.program - if isinstance(content, ProgramContent) - else MessageType.instance - ), - content=cleaned_content, - item_content=item_content, - time=time.time(), - channel=settings.DEFAULT_CHANNEL, - item_type=ItemType.inline, - item_hash=compute_sha256(item_content), - ) + message_dict = dict( + sender=content.address, + chain=Chain.ETH, + type=( + MessageType.program + if isinstance(content, ProgramContent) + else MessageType.instance + ), + content=cleaned_content, + item_content=item_content, + time=time.time(), + channel=settings.DEFAULT_CHANNEL, + item_type=ItemType.inline, + item_hash=compute_sha256(item_content), + signature="0x" + "0" * 130, # Add a dummy signature to pass validation ) + message = parse_message(message_dict) + async with self.http_session.post( "/api/v0/price/estimate", json=dict(message=message) ) as resp: diff --git a/src/aleph/sdk/client/vm_confidential_client.py b/src/aleph/sdk/client/vm_confidential_client.py index e027b384..0d9d6e18 100644 --- a/src/aleph/sdk/client/vm_confidential_client.py +++ b/src/aleph/sdk/client/vm_confidential_client.py @@ -105,7 +105,7 @@ async def measurement(self, vm_id: ItemHash) -> SEVMeasurement: status, text = await self.perform_operation( vm_id, "confidential/measurement", method="GET" ) - sev_measurement = SEVMeasurement.parse_raw(text) + sev_measurement = SEVMeasurement.model_validate_json(text) return sev_measurement async def validate_measure( diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 6a1fcf46..b289cc2b 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -3,11 +3,12 @@ import os from pathlib import Path from shutil import which -from typing import Dict, Optional, Union +from typing import ClassVar, Dict, List, Optional, Union from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType -from pydantic import BaseModel, BaseSettings, Field +from pydantic import BaseModel, Field +from pydantic_settings import BaseSettings, SettingsConfigDict from aleph.sdk.types import ChainInfo @@ -41,7 +42,7 @@ class Settings(BaseSettings): REMOTE_CRYPTO_HOST: Optional[str] = None REMOTE_CRYPTO_UNIX_SOCKET: Optional[str] = None ADDRESS_TO_USE: Optional[str] = None - HTTP_REQUEST_TIMEOUT = 15.0 + HTTP_REQUEST_TIMEOUT: ClassVar[float] = 15.0 DEFAULT_CHANNEL: str = "ALEPH-CLOUDSOLUTIONS" @@ -78,14 +79,14 @@ class Settings(BaseSettings): CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists - VM_URL_PATH = "https://aleph.sh/vm/{hash}" - VM_URL_HOST = "https://{hash_base32}.aleph.sh" - IPFS_GATEWAY = "https://ipfs.aleph.cloud/ipfs/" - CRN_URL_FOR_PROGRAMS = "https://dchq.staging.aleph.sh/" + VM_URL_PATH: ClassVar[str] = "https://aleph.sh/vm/{hash}" + VM_URL_HOST: ClassVar[str] = "https://{hash_base32}.aleph.sh" + IPFS_GATEWAY: ClassVar[str] = "https://ipfs.aleph.cloud/ipfs/" + CRN_URL_FOR_PROGRAMS: ClassVar[str] = "https://dchq.staging.aleph.sh/" # Web3Provider settings - TOKEN_DECIMALS = 18 - TX_TIMEOUT = 60 * 3 + TOKEN_DECIMALS: ClassVar[int] = 18 + TX_TIMEOUT: ClassVar[int] = 60 * 3 CHAINS: Dict[Union[Chain, str], ChainInfo] = { # TESTNETS "SEPOLIA": ChainInfo( @@ -220,16 +221,15 @@ class Settings(BaseSettings): DEFAULT_CHAIN: Chain = Chain.ETH # Dns resolver - DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh" - DNS_PROGRAM_DOMAIN = "program.public.aleph.sh" - DNS_INSTANCE_DOMAIN = "instance.public.aleph.sh" - DNS_STATIC_DOMAIN = "static.public.aleph.sh" - DNS_RESOLVERS = ["9.9.9.9", "1.1.1.1"] - - class Config: - env_prefix = "ALEPH_" - case_sensitive = False - env_file = ".env" + DNS_IPFS_DOMAIN: ClassVar[str] = "ipfs.public.aleph.sh" + DNS_PROGRAM_DOMAIN: ClassVar[str] = "program.public.aleph.sh" + DNS_INSTANCE_DOMAIN: ClassVar[str] = "instance.public.aleph.sh" + DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" + DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] + + model_config = SettingsConfigDict( + env_prefix="ALEPH_", case_sensitive=False, env_file=".env" + ) class MainConfiguration(BaseModel): @@ -240,8 +240,7 @@ class MainConfiguration(BaseModel): path: Path chain: Chain - class Config: - use_enum_values = True + model_config = SettingsConfigDict(use_enum_values=True) # Settings singleton @@ -297,7 +296,7 @@ def save_main_configuration(file_path: Path, data: MainConfiguration): Synchronously save a single ChainAccount object as JSON to a file. """ with file_path.open("w") as file: - data_serializable = data.dict() + data_serializable = data.model_dump() data_serializable["path"] = str(data_serializable["path"]) json.dump(data_serializable, file, indent=4) diff --git a/src/aleph/sdk/domain.py b/src/aleph/sdk/domain.py index a8f3fd82..525e6cef 100644 --- a/src/aleph/sdk/domain.py +++ b/src/aleph/sdk/domain.py @@ -52,11 +52,11 @@ def raise_error(self, status: Dict[str, bool]): def hostname_from_url(url: Union[HttpUrl, str]) -> Hostname: """Extract FQDN from url""" - parsed = urlparse(url) + parsed = urlparse(str(url)) if all([parsed.scheme, parsed.netloc]) is True: url = parsed.netloc - return Hostname(url) + return Hostname(str(url)) async def get_target_type(fqdn: Hostname) -> Optional[TargetType]: diff --git a/src/aleph/sdk/query/responses.py b/src/aleph/sdk/query/responses.py index 4b598f12..277a1bea 100644 --- a/src/aleph/sdk/query/responses.py +++ b/src/aleph/sdk/query/responses.py @@ -9,7 +9,7 @@ ItemType, MessageConfirmation, ) -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class Post(BaseModel): @@ -48,9 +48,9 @@ class Post(BaseModel): ref: Optional[Union[str, Any]] = Field( description="Other message referenced by this one" ) + address: Optional[str] = Field(description="Address of the sender") - class Config: - allow_extra = False + model_config = ConfigDict(extra="forbid") class PaginationResponse(BaseModel): @@ -64,14 +64,14 @@ class PostsResponse(PaginationResponse): """Response from an aleph.im node API on the path /api/v0/posts.json""" posts: List[Post] - pagination_item = "posts" + pagination_item: str = "posts" class MessagesResponse(PaginationResponse): """Response from an aleph.im node API on the path /api/v0/messages.json""" messages: List[AlephMessage] - pagination_item = "messages" + pagination_item: str = "messages" class PriceResponse(BaseModel): diff --git a/src/aleph/sdk/types.py b/src/aleph/sdk/types.py index 05fa9815..cf23f19d 100644 --- a/src/aleph/sdk/types.py +++ b/src/aleph/sdk/types.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Dict, Optional, Protocol, TypeVar -from pydantic import BaseModel +from pydantic import BaseModel, Field __all__ = ("StorageEnum", "Account", "AccountFromPrivateKey", "GenericMessage") @@ -87,10 +87,10 @@ class StoredContent(BaseModel): A stored content. """ - filename: Optional[str] - hash: Optional[str] - url: Optional[str] - error: Optional[str] + filename: Optional[str] = Field(default=None) + hash: Optional[str] = Field(default=None) + url: Optional[str] = Field(default=None) + error: Optional[str] = Field(default=None) class TokenType(str, Enum): diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index 5cbc1e8c..31b2be8d 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -28,6 +28,7 @@ from uuid import UUID from zipfile import BadZipFile, ZipFile +import pydantic_core from aleph_message.models import ( Chain, InstanceContent, @@ -63,7 +64,6 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA -from pydantic.json import pydantic_encoder from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement @@ -202,7 +202,7 @@ def extended_json_encoder(obj: Any) -> Any: elif isinstance(obj, time): return obj.hour * 3600 + obj.minute * 60 + obj.second + obj.microsecond / 1e6 else: - return pydantic_encoder(obj) + return pydantic_core.to_jsonable_python(obj) def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: @@ -213,7 +213,7 @@ def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: for volume_type in get_args(MachineVolume): try: - return volume_type.parse_obj(volume_dict) + return volume_type.model_validate(volume_dict) except ValueError: pass raise ValueError(f"Could not parse volume: {volume_dict}") diff --git a/src/aleph/sdk/vm/cache.py b/src/aleph/sdk/vm/cache.py index ff5ca7c8..a7ac6acc 100644 --- a/src/aleph/sdk/vm/cache.py +++ b/src/aleph/sdk/vm/cache.py @@ -70,7 +70,7 @@ def __init__( ) self.cache = {} - self.api_host = connector_url if connector_url else settings.API_HOST + self.api_host = str(connector_url) if connector_url else settings.API_HOST async def get(self, key: str) -> Optional[bytes]: sanitized_key = sanitize_cache_key(key) diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index 6083a119..c1710c16 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -1,4 +1,6 @@ # Keep datetime import as is as it allow patching in test +from __future__ import annotations + import datetime import functools import json @@ -13,7 +15,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA -from pydantic import BaseModel, ValidationError, root_validator, validator +from pydantic import BaseModel, ValidationError, field_validator, model_validator from aleph.sdk.utils import bytes_from_hex @@ -63,23 +65,21 @@ class SignedPubKeyHeader(BaseModel): signature: bytes payload: bytes - @validator("signature") + @field_validator("signature") def signature_must_be_hex(cls, value: bytes) -> bytes: """Convert the signature from hexadecimal to bytes""" - return bytes_from_hex(value.decode()) - @validator("payload") + @field_validator("payload") def payload_must_be_hex(cls, value: bytes) -> bytes: """Convert the payload from hexadecimal to bytes""" - return bytes_from_hex(value.decode()) - @root_validator(pre=False, skip_on_failure=True) - def check_expiry(cls, values) -> Dict[str, bytes]: + @model_validator(mode="after") # type: ignore + def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: """Check that the token has not expired""" - payload: bytes = values["payload"] - content = SignedPubKeyPayload.parse_raw(payload) + payload: bytes = values.payload + content = SignedPubKeyPayload.model_validate_json(payload) if not is_token_still_valid(content.expires): msg = "Token expired" @@ -87,12 +87,11 @@ def check_expiry(cls, values) -> Dict[str, bytes]: return values - @root_validator(pre=False, skip_on_failure=True) - def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: - """Check that the signature is valid""" - signature: bytes = values["signature"] - payload: bytes = values["payload"] - content = SignedPubKeyPayload.parse_raw(payload) + @model_validator(mode="after") # type: ignore + def check_signature(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: + signature: bytes = values.signature + payload: bytes = values.payload + content = SignedPubKeyPayload.model_validate_json(payload) if not verify_wallet_signature(signature, payload.hex(), content.address): msg = "Invalid signature" @@ -103,7 +102,7 @@ def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: @property def content(self) -> SignedPubKeyPayload: """Return the content of the header""" - return SignedPubKeyPayload.parse_raw(self.payload) + return SignedPubKeyPayload.model_validate_json(self.payload) class SignedOperationPayload(BaseModel): @@ -113,7 +112,7 @@ class SignedOperationPayload(BaseModel): path: str # body_sha256: str # disabled since there is no body - @validator("time") + @field_validator("time") def time_is_current(cls, v: datetime.datetime) -> datetime.datetime: """Check that the time is current and the payload is not a replay attack.""" max_past = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta( @@ -135,7 +134,7 @@ class SignedOperation(BaseModel): signature: bytes payload: bytes - @validator("signature") + @field_validator("signature") def signature_must_be_hex(cls, value: str) -> bytes: """Convert the signature from hexadecimal to bytes""" @@ -147,17 +146,17 @@ def signature_must_be_hex(cls, value: str) -> bytes: logger.warning(value) raise error - @validator("payload") + @field_validator("payload") def payload_must_be_hex(cls, v) -> bytes: """Convert the payload from hexadecimal to bytes""" v = bytes.fromhex(v.decode()) - _ = SignedOperationPayload.parse_raw(v) + _ = SignedOperationPayload.model_validate_json(v) return v @property def content(self) -> SignedOperationPayload: """Return the content of the header""" - return SignedOperationPayload.parse_raw(self.payload) + return SignedOperationPayload.model_validate_json(self.payload) def get_signed_pubkey(request: web.Request) -> SignedPubKeyHeader: @@ -168,7 +167,7 @@ def get_signed_pubkey(request: web.Request) -> SignedPubKeyHeader: raise web.HTTPBadRequest(reason="Missing X-SignedPubKey header") try: - return SignedPubKeyHeader.parse_raw(signed_pubkey_header) + return SignedPubKeyHeader.model_validate_json(signed_pubkey_header) except KeyError as error: logger.debug(f"Missing X-SignedPubKey header: {error}") @@ -199,7 +198,7 @@ def get_signed_operation(request: web.Request) -> SignedOperation: """Get the signed operation public key that is signed by the ephemeral key from the request headers.""" try: signed_operation = request.headers["X-SignedOperation"] - return SignedOperation.parse_raw(signed_operation) + return SignedOperation.model_validate_json(signed_operation) except KeyError as error: raise web.HTTPBadRequest(reason="Missing X-SignedOperation header") from error except json.JSONDecodeError as error: @@ -259,8 +258,8 @@ async def authenticate_websocket_message( message, domain_name: Optional[str] = DOMAIN_NAME ) -> str: """Authenticate a websocket message since JS cannot configure headers on WebSockets.""" - signed_pubkey = SignedPubKeyHeader.parse_obj(message["X-SignedPubKey"]) - signed_operation = SignedOperation.parse_obj(message["X-SignedOperation"]) + signed_pubkey = SignedPubKeyHeader.model_validate(message["X-SignedPubKey"]) + signed_operation = SignedOperation.model_validate(message["X-SignedOperation"]) if signed_operation.content.domain != domain_name: logger.debug( f"Invalid domain '{signed_operation.content.domain}' != '{domain_name}'" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c1c56fcd..385d2836 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -71,7 +71,7 @@ def rejected_message(): @pytest.fixture def aleph_messages() -> List[AlephMessage]: return [ - AggregateMessage.parse_obj( + AggregateMessage.model_validate( { "item_hash": "5b26d949fe05e38f535ef990a89da0473f9d700077cced228f2d36e73fca1fd6", "type": "AGGREGATE", @@ -95,7 +95,7 @@ def aleph_messages() -> List[AlephMessage]: "confirmed": False, } ), - PostMessage.parse_obj( + PostMessage.model_validate( { "item_hash": "70f3798fdc68ce0ee03715a5547ee24e2c3e259bf02e3f5d1e4bf5a6f6a5e99f", "type": "POST", @@ -135,7 +135,9 @@ def json_post() -> dict: def raw_messages_response(aleph_messages) -> Callable[[int], Dict[str, Any]]: return lambda page: { "messages": ( - [message.dict() for message in aleph_messages] if int(page) == 1 else [] + [message.model_dump() for message in aleph_messages] + if int(page) == 1 + else [] ), "pagination_item": "messages", "pagination_page": int(page), diff --git a/tests/unit/test_price.py b/tests/unit/test_price.py index fe9e3468..e60680f8 100644 --- a/tests/unit/test_price.py +++ b/tests/unit/test_price.py @@ -15,7 +15,7 @@ async def test_get_program_price_valid(): required_tokens=3.0555555555555556e-06, payment_type="superfluid", ) - mock_session = make_mock_get_session(expected.dict()) + mock_session = make_mock_get_session(expected.model_dump()) async with mock_session: response = await mock_session.get_program_price("cacacacacacaca") assert response == expected diff --git a/tests/unit/test_remote_account.py b/tests/unit/test_remote_account.py index cb4a2af5..3abe979e 100644 --- a/tests/unit/test_remote_account.py +++ b/tests/unit/test_remote_account.py @@ -22,7 +22,7 @@ async def test_remote_storage(): curve="secp256k1", address=local_account.get_address(), public_key=local_account.get_public_key(), - ).dict() + ).model_dump() ) remote_account = await RemoteAccount.from_crypto_host( diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index c560455d..4ceb5a3f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -13,7 +13,6 @@ ProgramMessage, StoreMessage, ) -from aleph_message.models.execution.environment import MachineResources from aleph_message.models.execution.volume import ( EphemeralVolume, ImmutableVolume, @@ -116,15 +115,16 @@ def test_enum_as_str(): ( MessageType.aggregate, { + "address": "0x1", "content": { - "Hello": MachineResources( - vcpus=1, - memory=1024, - seconds=1, - ) + "Hello": { + "vcpus": 1, + "memory": 1024, + "seconds": 1, + "published_ports": None, + }, }, "key": "test", - "address": "0x1", "time": 1.0, }, ), @@ -141,7 +141,7 @@ async def test_prepare_aleph_message( channel="TEST", ) - assert message.content.dict() == content + assert message.content.model_dump() == content def test_parse_immutable_volume(): @@ -219,7 +219,7 @@ def test_compute_confidential_measure(): assert base64.b64encode(tik) == b"npOTEc4mtRGfXfB+G6EBdw==" expected_hash = "d06471f485c0a61aba5a431ec136b947be56907acf6ed96afb11788ae4525aeb" nonce = base64.b64decode("URQNqJAqh/2ep4drjx/XvA==") - sev_info = SEVInfo.parse_obj( + sev_info = SEVInfo.model_validate( { "enabled": True, "api_major": 1, diff --git a/tests/unit/test_vm_client.py b/tests/unit/test_vm_client.py index 7cc9a2c3..d9a9a36b 100644 --- a/tests/unit/test_vm_client.py +++ b/tests/unit/test_vm_client.py @@ -290,8 +290,8 @@ async def test_vm_client_generate_correct_authentication_headers(): ) path, headers = await vm_client._generate_header(vm_id, "reboot", method="post") - signed_pubkey = SignedPubKeyHeader.parse_raw(headers["X-SignedPubKey"]) - signed_operation = SignedOperation.parse_raw(headers["X-SignedOperation"]) + signed_pubkey = SignedPubKeyHeader.model_validate_json(headers["X-SignedPubKey"]) + signed_operation = SignedOperation.model_validate_json(headers["X-SignedOperation"]) address = verify_signed_operation(signed_operation, signed_pubkey) assert vm_client.account.get_address() == address