diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43b507aa..6807511f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,8 @@ repos: - repo: local hooks: - - id: ruff-format - name: ruff-format + - id: format + name: format description: "Run 'ruff format' for extremely fast Python formatting" entry: uv run ruff format language: system @@ -11,30 +11,20 @@ repos: require_serial: true additional_dependencies: [] minimum_pre_commit_version: "2.9.2" + pass_filenames: false files: "^(src|tests)/" - - id: ruff - name: ruff - description: "Run 'ruff' for extremely fast Python linting" - entry: uv run ruff check - language: system - "types": [python] - args: [--fix] - require_serial: false - additional_dependencies: [] - minimum_pre_commit_version: "0" - files: "^(src|tests)/" - exclude: "^tests/artifacts/" - - id: mypy - name: mypy - description: "`mypy` will check Python types for correctness" - entry: uv run mypy + - id: lint + name: lint + description: "`mypy` and 'ruff' will check Python types for correctness" + entry: uv run poe lint language: system types_or: [python, pyi] + args: [] require_serial: true additional_dependencies: [] minimum_pre_commit_version: "2.9.2" + pass_filenames: false files: "^(src|tests)/" - exclude: "^tests/artifacts/" - id: docstrings-check name: docstrings-check description: "Check docstrings for correctness" diff --git a/MIGRATION-NOTES.md b/MIGRATION-NOTES.md new file mode 100644 index 00000000..70e5e2a9 --- /dev/null +++ b/MIGRATION-NOTES.md @@ -0,0 +1,13 @@ +# Migration Notes + +A collection of notes to consolidate todos during decoupling efforts (similar doc exists on ts version as well). + +## API + +### Generator + +- Currently generated models for KMD have explicit request models resulting in slightly different signatures in contrast with indexer and algod and requiring imports of explicit typed request models specifically on kmd client calls. Do we want to further refine the generation to auto flatten the keys to ensure it does not define an explicit request models or change those models to TypedDicts to reduce the import overhead? + +### KMD + +- algokit-core repo on a branch called feat/account-manager, we had a minor refinement in OAS spec for kmd adding a default value for wallet driver field as well as the generator adjustments to ensure generated model for related endpoint falls back to default 'sqlite' value. Do we want to restore this approach?: diff --git a/api/oas-generator/src/oas_generator/builder.py b/api/oas-generator/src/oas_generator/builder.py index d4d2c989..553ab89e 100644 --- a/api/oas-generator/src/oas_generator/builder.py +++ b/api/oas-generator/src/oas_generator/builder.py @@ -27,6 +27,7 @@ class TypeInfo: list_inner_model: str | None = None list_inner_enum: str | None = None is_bytes: bool = False + list_inner_is_bytes: bool = False is_signed_transaction: bool = False needs_datetime: bool = False imports: set[str] = field(default_factory=set) @@ -174,6 +175,7 @@ def _resolve_array(self, schema: ctx.RawSchema, *, hint: str) -> TypeInfo: is_list=True, list_inner_model=inner.model, list_inner_enum=inner.enum, + list_inner_is_bytes=inner.is_bytes, is_signed_transaction=inner.is_signed_transaction, needs_datetime=inner.needs_datetime, imports=set(inner.imports), @@ -283,6 +285,10 @@ def _build_model(self, entry: SchemaEntry) -> ctx.ModelDescriptor: # noqa: C901 imports.add("from ._serde_helpers import encode_model_mapping, mapping_encoder") if "decode_model_mapping" in field.metadata or "mapping_decoder" in field.metadata: imports.add("from ._serde_helpers import decode_model_mapping, mapping_decoder") + if "encode_bytes_base64" in field.metadata or "decode_bytes_base64" in field.metadata: + imports.add("from ._serde_helpers import decode_bytes_base64, encode_bytes_base64") + if "encode_bytes_sequence" in field.metadata or "decode_bytes_sequence" in field.metadata: + imports.add("from ._serde_helpers import decode_bytes_sequence, encode_bytes_sequence") if "nested(" in field.metadata: uses_nested = True if "flatten(" in field.metadata: @@ -391,6 +397,22 @@ def _build_metadata(self, wire_name: str, type_info: TypeInfo) -> str: f" decode=lambda raw: decode_enum_sequence(lambda: {type_info.list_inner_enum}, raw),\n" " )" ) + if type_info.is_list and type_info.list_inner_is_bytes: + return ( + "wire(\n" + f' "{alias}",\n' + " encode=encode_bytes_sequence,\n" + " decode=decode_bytes_sequence,\n" + " )" + ) + if type_info.is_bytes: + return ( + "wire(\n" + f' "{alias}",\n' + " encode=encode_bytes_base64,\n" + " decode=decode_bytes_base64,\n" + " )" + ) if type_info.is_signed_transaction: return f'nested("{alias}", lambda: SignedTransaction)' return f'wire("{alias}")' diff --git a/api/oas-generator/src/oas_generator/naming.py b/api/oas-generator/src/oas_generator/naming.py index 93b1eb9b..5347c0ec 100644 --- a/api/oas-generator/src/oas_generator/naming.py +++ b/api/oas-generator/src/oas_generator/naming.py @@ -7,6 +7,7 @@ _LOWER_TO_UPPER = re.compile(r"([a-z0-9])([A-Z])") _PY_RESERVED = {*keyword.kwlist, *keyword.softkwlist, *dir(builtins), "self", "cls"} + @dataclass(slots=True) class IdentifierSanitizer: """Deterministic naming helper shared across generator stages.""" diff --git a/api/oas-generator/src/oas_generator/renderer/templates/models/_serde_helpers.py.j2 b/api/oas-generator/src/oas_generator/renderer/templates/models/_serde_helpers.py.j2 index f7a4f25a..36b407af 100644 --- a/api/oas-generator/src/oas_generator/renderer/templates/models/_serde_helpers.py.j2 +++ b/api/oas-generator/src/oas_generator/renderer/templates/models/_serde_helpers.py.j2 @@ -1,14 +1,65 @@ - +import base64 +from binascii import Error as BinasciiError from collections.abc import Iterable, Mapping -from enum import Enum from dataclasses import is_dataclass -from typing import Callable, TypeVar +from enum import Enum +from typing import Callable, TypeAlias, TypeVar from algokit_common.serde import from_wire, to_wire T = TypeVar("T") E = TypeVar("E", bound=Enum) +BytesLike: TypeAlias = bytes | bytearray | memoryview + + +def _coerce_bytes(value: bytes | bytearray | memoryview) -> bytes: + if isinstance(value, memoryview): + return value.tobytes() + if isinstance(value, bytearray): + return bytes(value) + return value + + +def encode_bytes_base64(value: BytesLike) -> str: + return base64.b64encode(_coerce_bytes(value)).decode("ascii") + + +def decode_bytes_base64(raw: object) -> bytes: + if isinstance(raw, bytes | bytearray | memoryview): + return bytes(raw) + if isinstance(raw, str): + try: + return base64.b64decode(raw.encode("ascii"), validate=True) + except (BinasciiError, UnicodeEncodeError) as exc: + raise ValueError("Invalid base64 payload") from exc + raise TypeError(f"Unsupported value for bytes field: {type(raw)!r}") + + +def encode_bytes_sequence(values: Iterable[BytesLike | None] | None) -> list[str | None] | None: + if values is None: + return None + encoded: list[str | None] = [] + for value in values: + if value is None: + encoded.append(None) + continue + if not isinstance(value, bytes | bytearray | memoryview): + raise TypeError(f"Unsupported value for bytes field sequence: {type(value)!r}") + encoded.append(encode_bytes_base64(value)) + return encoded or None + + +def decode_bytes_sequence(raw: object) -> list[bytes | None] | None: + if not isinstance(raw, list): + return None + decoded: list[bytes | None] = [] + for item in raw: + if item is None: + decoded.append(None) + continue + decoded.append(decode_bytes_base64(item)) + return decoded or None def encode_model_sequence(values: Iterable[object] | None) -> list[dict[str, object]] | None: diff --git a/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md b/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md index 5041cc9b..1074aecc 100644 --- a/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md +++ b/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md @@ -24,7 +24,7 @@ Provides an account implementation that can be used to sign transactions using k Provides abstractions over KMD that makes it easier to get and manage accounts. -#### kmd() → algosdk.kmd.KMDClient +#### kmd() → algokit_kmd_client.client.KmdClient Returns the KMD client, initializing it if needed. @@ -46,6 +46,8 @@ if no predicate is provided. * **sender** – Optional sender address to use this signer for (aka a rekeyed account) * **Returns:** The signing account or None if no matching wallet or account was found +* **Raises:** + **Exception** – If error received while exporting the private key from KMD #### get_or_create_wallet_account(name: str, fund_with: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) @@ -58,6 +60,8 @@ Provides idempotent access to accounts from LocalNet without specifying the priv * **fund_with** – The number of Algos to fund the account with when created * **Returns:** An Algorand account with private key loaded +* **Raises:** + **Exception** – If error received while creating the wallet or funding the account #### get_localnet_dispenser_account() → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) diff --git a/docs/markdown/autoapi/algokit_utils/algorand/index.md b/docs/markdown/autoapi/algokit_utils/algorand/index.md index 482ca557..8f437d14 100644 --- a/docs/markdown/autoapi/algokit_utils/algorand/index.md +++ b/docs/markdown/autoapi/algokit_utils/algorand/index.md @@ -242,7 +242,7 @@ Returns an AlgorandClient pointing at MainNet using AlgoNode. algorand = AlgorandClient.mainnet() ``` -#### *static* from_clients(algod: algosdk.v2client.algod.AlgodClient, indexer: algosdk.v2client.indexer.IndexerClient | None = None, kmd: algosdk.kmd.KMDClient | None = None) → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) +#### *static* from_clients(algod: algosdk.v2client.algod.AlgodClient, indexer: algokit_indexer_client.IndexerClient | None = None, kmd: algokit_kmd_client.client.KmdClient | None = None) → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) Returns an AlgorandClient pointing to the given client(s). diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md index 684718f6..f203d42e 100644 --- a/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md +++ b/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md @@ -151,7 +151,7 @@ The update result The delete result -### *class* algokit_utils.applications.app_deployer.AppDeployer(app_manager: [algokit_utils.applications.app_manager.AppManager](../app_manager/index.md#algokit_utils.applications.app_manager.AppManager), transaction_sender: [algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender), indexer: algosdk.v2client.indexer.IndexerClient | None = None) +### *class* algokit_utils.applications.app_deployer.AppDeployer(app_manager: [algokit_utils.applications.app_manager.AppManager](../app_manager/index.md#algokit_utils.applications.app_manager.AppManager), transaction_sender: [algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender), indexer: algokit_indexer_client.IndexerClient | None = None) Manages deployment and deployment metadata of applications diff --git a/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md index 524408b7..095f51bd 100644 --- a/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md +++ b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md @@ -9,7 +9,7 @@ ## Module Contents -### *class* algokit_utils.clients.client_manager.AlgoSdkClients(algod: algosdk.v2client.algod.AlgodClient, indexer: algosdk.v2client.indexer.IndexerClient | None = None, kmd: algosdk.kmd.KMDClient | None = None) +### *class* algokit_utils.clients.client_manager.AlgoSdkClients(algod: algosdk.v2client.algod.AlgodClient, indexer: algokit_indexer_client.IndexerClient | None = None, kmd: algokit_kmd_client.KmdClient | None = None) Container for Algorand SDK client instances. @@ -81,25 +81,25 @@ Returns an algosdk Algod API client. * **Returns:** Algod client instance -#### *property* indexer *: algosdk.v2client.indexer.IndexerClient* +#### *property* indexer *: algokit_indexer_client.IndexerClient* -Returns an algosdk Indexer API client. +Returns an Indexer API client. * **Raises:** **ValueError** – If no Indexer client is configured * **Returns:** Indexer client instance -#### *property* indexer_if_present *: algosdk.v2client.indexer.IndexerClient | None* +#### *property* indexer_if_present *: algokit_indexer_client.IndexerClient | None* Returns the Indexer client if configured, otherwise None. * **Returns:** Indexer client instance or None -#### *property* kmd *: algosdk.kmd.KMDClient* +#### *property* kmd *: algokit_kmd_client.KmdClient* -Returns an algosdk KMD API client. +Returns a KMD-compatible API client. * **Raises:** **ValueError** – If no KMD client is configured @@ -231,7 +231,7 @@ Get an Algod client from environment variables. * **Returns:** Algod client instance -#### *static* get_kmd_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig)) → algosdk.kmd.KMDClient +#### *static* get_kmd_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig)) → algokit_kmd_client.KmdClient Get a KMD client from config or environment. @@ -240,14 +240,14 @@ Get a KMD client from config or environment. * **Returns:** KMD client instance -#### *static* get_kmd_client_from_environment() → algosdk.kmd.KMDClient +#### *static* get_kmd_client_from_environment() → algokit_kmd_client.KmdClient Get a KMD client from environment variables. * **Returns:** KMD client instance -#### *static* get_indexer_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig)) → algosdk.v2client.indexer.IndexerClient +#### *static* get_indexer_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig)) → algokit_indexer_client.IndexerClient Get an Indexer client from config or environment. @@ -256,7 +256,7 @@ Get an Indexer client from config or environment. * **Returns:** Indexer client instance -#### *static* get_indexer_client_from_environment() → algosdk.v2client.indexer.IndexerClient +#### *static* get_indexer_client_from_environment() → algokit_indexer_client.IndexerClient Get an Indexer client from environment variables. diff --git a/src/algokit_algod_client/models/_account_participation.py b/src/algokit_algod_client/models/_account_participation.py index f81f38ba..ac1476ea 100644 --- a/src/algokit_algod_client/models/_account_participation.py +++ b/src/algokit_algod_client/models/_account_participation.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class AccountParticipation: @@ -14,7 +16,11 @@ class AccountParticipation: """ selection_participation_key: bytes = field( - metadata=wire("selection-participation-key"), + metadata=wire( + "selection-participation-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) vote_first_valid: int = field( metadata=wire("vote-first-valid"), @@ -26,9 +32,17 @@ class AccountParticipation: metadata=wire("vote-last-valid"), ) vote_participation_key: bytes = field( - metadata=wire("vote-participation-key"), + metadata=wire( + "vote-participation-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) state_proof_key: bytes | None = field( default=None, - metadata=wire("state-proof-key"), + metadata=wire( + "state-proof-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_algod_client/models/_app_call_logs.py b/src/algokit_algod_client/models/_app_call_logs.py index 5a1bf0aa..7dcc2b29 100644 --- a/src/algokit_algod_client/models/_app_call_logs.py +++ b/src/algokit_algod_client/models/_app_call_logs.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_sequence, encode_bytes_sequence + @dataclass(slots=True) class AppCallLogs: @@ -17,7 +19,11 @@ class AppCallLogs: metadata=wire("app_id"), ) logs: list[bytes] = field( - metadata=wire("logs"), + metadata=wire( + "logs", + encode=encode_bytes_sequence, + decode=decode_bytes_sequence, + ), ) tx_id: str = field( metadata=wire("txId"), diff --git a/src/algokit_algod_client/models/_application_params.py b/src/algokit_algod_client/models/_application_params.py index a9ea0cb6..6a8b3591 100644 --- a/src/algokit_algod_client/models/_application_params.py +++ b/src/algokit_algod_client/models/_application_params.py @@ -6,7 +6,7 @@ from algokit_common.serde import nested, wire from ._application_state_schema import ApplicationStateSchema -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_base64, decode_model_sequence, encode_bytes_base64, encode_model_sequence from ._teal_key_value import TealKeyValue @@ -17,10 +17,18 @@ class ApplicationParams: """ approval_program: bytes = field( - metadata=wire("approval-program"), + metadata=wire( + "approval-program", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) clear_state_program: bytes = field( - metadata=wire("clear-state-program"), + metadata=wire( + "clear-state-program", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) creator: str = field( metadata=wire("creator"), diff --git a/src/algokit_algod_client/models/_application_state_operation.py b/src/algokit_algod_client/models/_application_state_operation.py index 579527e4..8e71a2a1 100644 --- a/src/algokit_algod_client/models/_application_state_operation.py +++ b/src/algokit_algod_client/models/_application_state_operation.py @@ -6,6 +6,7 @@ from algokit_common.serde import nested, wire from ._avm_value import AvmValue +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 @dataclass(slots=True) @@ -18,7 +19,11 @@ class ApplicationStateOperation: metadata=wire("app-state-type"), ) key: bytes = field( - metadata=wire("key"), + metadata=wire( + "key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) operation: str = field( metadata=wire("operation"), diff --git a/src/algokit_algod_client/models/_asset_params.py b/src/algokit_algod_client/models/_asset_params.py index ce983a38..a26f092a 100644 --- a/src/algokit_algod_client/models/_asset_params.py +++ b/src/algokit_algod_client/models/_asset_params.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class AssetParams: @@ -44,7 +46,11 @@ class AssetParams: ) metadata_hash: bytes | None = field( default=None, - metadata=wire("metadata-hash"), + metadata=wire( + "metadata-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) name: str | None = field( default=None, @@ -52,7 +58,11 @@ class AssetParams: ) name_b64: bytes | None = field( default=None, - metadata=wire("name-b64"), + metadata=wire( + "name-b64", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) reserve: str | None = field( default=None, @@ -64,7 +74,11 @@ class AssetParams: ) unit_name_b64: bytes | None = field( default=None, - metadata=wire("unit-name-b64"), + metadata=wire( + "unit-name-b64", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) url: str | None = field( default=None, @@ -72,5 +86,9 @@ class AssetParams: ) url_b64: bytes | None = field( default=None, - metadata=wire("url-b64"), + metadata=wire( + "url-b64", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_algod_client/models/_avm_key_value.py b/src/algokit_algod_client/models/_avm_key_value.py index 6cf7b2a9..f917bd09 100644 --- a/src/algokit_algod_client/models/_avm_key_value.py +++ b/src/algokit_algod_client/models/_avm_key_value.py @@ -6,6 +6,7 @@ from algokit_common.serde import nested, wire from ._avm_value import AvmValue +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 @dataclass(slots=True) @@ -15,7 +16,11 @@ class AvmKeyValue: """ key: bytes = field( - metadata=wire("key"), + metadata=wire( + "key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) value: AvmValue = field( metadata=nested("value", lambda: AvmValue), diff --git a/src/algokit_algod_client/models/_box.py b/src/algokit_algod_client/models/_box.py index a772289e..72b61803 100644 --- a/src/algokit_algod_client/models/_box.py +++ b/src/algokit_algod_client/models/_box.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class Box: @@ -13,11 +15,19 @@ class Box: """ name: bytes = field( - metadata=wire("name"), + metadata=wire( + "name", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) round_: int = field( metadata=wire("round"), ) value: bytes = field( - metadata=wire("value"), + metadata=wire( + "value", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_algod_client/models/_box_descriptor.py b/src/algokit_algod_client/models/_box_descriptor.py index 54cfcefa..57bafba7 100644 --- a/src/algokit_algod_client/models/_box_descriptor.py +++ b/src/algokit_algod_client/models/_box_descriptor.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class BoxDescriptor: @@ -13,5 +15,9 @@ class BoxDescriptor: """ name: bytes = field( - metadata=wire("name"), + metadata=wire( + "name", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_algod_client/models/_box_reference.py b/src/algokit_algod_client/models/_box_reference.py index 92924f88..0f166904 100644 --- a/src/algokit_algod_client/models/_box_reference.py +++ b/src/algokit_algod_client/models/_box_reference.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class BoxReference: @@ -16,5 +18,9 @@ class BoxReference: metadata=wire("app"), ) name: bytes = field( - metadata=wire("name"), + metadata=wire( + "name", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_algod_client/models/_dryrun_txn_result.py b/src/algokit_algod_client/models/_dryrun_txn_result.py index 45f5cab0..d8f42735 100644 --- a/src/algokit_algod_client/models/_dryrun_txn_result.py +++ b/src/algokit_algod_client/models/_dryrun_txn_result.py @@ -8,7 +8,7 @@ from ._account_state_delta import AccountStateDelta from ._dryrun_state import DryrunState from ._eval_delta_key_value import EvalDeltaKeyValue -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_sequence, decode_model_sequence, encode_bytes_sequence, encode_model_sequence @dataclass(slots=True) @@ -75,5 +75,9 @@ class DryrunTxnResult: ) logs: list[bytes] | None = field( default=None, - metadata=wire("logs"), + metadata=wire( + "logs", + encode=encode_bytes_sequence, + decode=decode_bytes_sequence, + ), ) diff --git a/src/algokit_algod_client/models/_light_block_header_proof.py b/src/algokit_algod_client/models/_light_block_header_proof.py index 707a0449..f946d88d 100644 --- a/src/algokit_algod_client/models/_light_block_header_proof.py +++ b/src/algokit_algod_client/models/_light_block_header_proof.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class LightBlockHeaderProof: @@ -16,7 +18,11 @@ class LightBlockHeaderProof: metadata=wire("index"), ) proof: bytes = field( - metadata=wire("proof"), + metadata=wire( + "proof", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) treedepth: int = field( metadata=wire("treedepth"), diff --git a/src/algokit_algod_client/models/_pending_transaction_response.py b/src/algokit_algod_client/models/_pending_transaction_response.py index a4fe64f5..16fa134f 100644 --- a/src/algokit_algod_client/models/_pending_transaction_response.py +++ b/src/algokit_algod_client/models/_pending_transaction_response.py @@ -8,7 +8,7 @@ from ._account_state_delta import AccountStateDelta from ._eval_delta_key_value import EvalDeltaKeyValue -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_sequence, decode_model_sequence, encode_bytes_sequence, encode_model_sequence @dataclass(slots=True) @@ -74,7 +74,11 @@ class PendingTransactionResponse: ) logs: list[bytes] | None = field( default=None, - metadata=wire("logs"), + metadata=wire( + "logs", + encode=encode_bytes_sequence, + decode=decode_bytes_sequence, + ), ) receiver_rewards: int | None = field( default=None, diff --git a/src/algokit_algod_client/models/_serde_helpers.py b/src/algokit_algod_client/models/_serde_helpers.py index 7829af73..bef79ee0 100644 --- a/src/algokit_algod_client/models/_serde_helpers.py +++ b/src/algokit_algod_client/models/_serde_helpers.py @@ -1,15 +1,65 @@ # AUTO-GENERATED: oas_generator - - +import base64 +from binascii import Error as BinasciiError from collections.abc import Callable, Iterable, Mapping from dataclasses import is_dataclass from enum import Enum -from typing import TypeVar +from typing import TypeAlias, TypeVar from algokit_common.serde import from_wire, to_wire T = TypeVar("T") E = TypeVar("E", bound=Enum) +BytesLike: TypeAlias = bytes | bytearray | memoryview + + +def _coerce_bytes(value: bytes | bytearray | memoryview) -> bytes: + if isinstance(value, memoryview): + return value.tobytes() + if isinstance(value, bytearray): + return bytes(value) + return value + + +def encode_bytes_base64(value: BytesLike) -> str: + return base64.b64encode(_coerce_bytes(value)).decode("ascii") + + +def decode_bytes_base64(raw: object) -> bytes: + if isinstance(raw, bytes | bytearray | memoryview): + return bytes(raw) + if isinstance(raw, str): + try: + return base64.b64decode(raw.encode("ascii"), validate=True) + except (BinasciiError, UnicodeEncodeError) as exc: + raise ValueError("Invalid base64 payload") from exc + raise TypeError(f"Unsupported value for bytes field: {type(raw)!r}") + + +def encode_bytes_sequence(values: Iterable[BytesLike | None] | None) -> list[str | None] | None: + if values is None: + return None + encoded: list[str | None] = [] + for value in values: + if value is None: + encoded.append(None) + continue + if not isinstance(value, bytes | bytearray | memoryview): + raise TypeError(f"Unsupported value for bytes field sequence: {type(value)!r}") + encoded.append(encode_bytes_base64(value)) + return encoded or None + + +def decode_bytes_sequence(raw: object) -> list[bytes | None] | None: + if not isinstance(raw, list): + return None + decoded: list[bytes | None] = [] + for item in raw: + if item is None: + decoded.append(None) + continue + decoded.append(decode_bytes_base64(item)) + return decoded or None def encode_model_sequence(values: Iterable[object] | None) -> list[dict[str, object]] | None: diff --git a/src/algokit_algod_client/models/_simulation_transaction_exec_trace.py b/src/algokit_algod_client/models/_simulation_transaction_exec_trace.py index 86714150..7fa23c2f 100644 --- a/src/algokit_algod_client/models/_simulation_transaction_exec_trace.py +++ b/src/algokit_algod_client/models/_simulation_transaction_exec_trace.py @@ -5,7 +5,7 @@ from algokit_common.serde import wire -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_base64, decode_model_sequence, encode_bytes_base64, encode_model_sequence from ._simulation_opcode_trace_unit import SimulationOpcodeTraceUnit @@ -18,7 +18,11 @@ class SimulationTransactionExecTrace: approval_program_hash: bytes | None = field( default=None, - metadata=wire("approval-program-hash"), + metadata=wire( + "approval-program-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) approval_program_trace: list[SimulationOpcodeTraceUnit] | None = field( default=None, @@ -30,7 +34,11 @@ class SimulationTransactionExecTrace: ) clear_state_program_hash: bytes | None = field( default=None, - metadata=wire("clear-state-program-hash"), + metadata=wire( + "clear-state-program-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) clear_state_program_trace: list[SimulationOpcodeTraceUnit] | None = field( default=None, @@ -58,7 +66,11 @@ class SimulationTransactionExecTrace: ) logic_sig_hash: bytes | None = field( default=None, - metadata=wire("logic-sig-hash"), + metadata=wire( + "logic-sig-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) logic_sig_trace: list[SimulationOpcodeTraceUnit] | None = field( default=None, diff --git a/src/algokit_algod_client/models/_state_proof.py b/src/algokit_algod_client/models/_state_proof.py index 9be7f690..90e6fc3f 100644 --- a/src/algokit_algod_client/models/_state_proof.py +++ b/src/algokit_algod_client/models/_state_proof.py @@ -5,6 +5,7 @@ from algokit_common.serde import nested, wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 from ._state_proof_message import StateProofMessage @@ -18,5 +19,9 @@ class StateProof: metadata=nested("Message", lambda: StateProofMessage), ) state_proof: bytes = field( - metadata=wire("StateProof"), + metadata=wire( + "StateProof", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_algod_client/models/_state_proof_message.py b/src/algokit_algod_client/models/_state_proof_message.py index d4540311..c6ae784c 100644 --- a/src/algokit_algod_client/models/_state_proof_message.py +++ b/src/algokit_algod_client/models/_state_proof_message.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class StateProofMessage: @@ -13,7 +15,11 @@ class StateProofMessage: """ block_headers_commitment: bytes = field( - metadata=wire("BlockHeadersCommitment"), + metadata=wire( + "BlockHeadersCommitment", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) first_attested_round: int = field( metadata=wire("FirstAttestedRound"), @@ -25,5 +31,9 @@ class StateProofMessage: metadata=wire("LnProvenWeight"), ) voters_commitment: bytes = field( - metadata=wire("VotersCommitment"), + metadata=wire( + "VotersCommitment", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_algod_client/models/_teal_value.py b/src/algokit_algod_client/models/_teal_value.py index b437a5c3..a0b969dc 100644 --- a/src/algokit_algod_client/models/_teal_value.py +++ b/src/algokit_algod_client/models/_teal_value.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class TealValue: @@ -13,7 +15,11 @@ class TealValue: """ bytes_: bytes = field( - metadata=wire("bytes"), + metadata=wire( + "bytes", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) type_: int = field( metadata=wire("type"), diff --git a/src/algokit_algod_client/models/_transaction_params_response_model.py b/src/algokit_algod_client/models/_transaction_params_response_model.py index d6902439..67d67988 100644 --- a/src/algokit_algod_client/models/_transaction_params_response_model.py +++ b/src/algokit_algod_client/models/_transaction_params_response_model.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class TransactionParamsResponseModel: @@ -20,7 +22,11 @@ class TransactionParamsResponseModel: metadata=wire("fee"), ) genesis_hash: bytes = field( - metadata=wire("genesis-hash"), + metadata=wire( + "genesis-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) genesis_id: str = field( metadata=wire("genesis-id"), diff --git a/src/algokit_algod_client/models/_transaction_proof.py b/src/algokit_algod_client/models/_transaction_proof.py index aacea5b6..8e41eb02 100644 --- a/src/algokit_algod_client/models/_transaction_proof.py +++ b/src/algokit_algod_client/models/_transaction_proof.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class TransactionProof: @@ -19,10 +21,18 @@ class TransactionProof: metadata=wire("idx"), ) proof: bytes = field( - metadata=wire("proof"), + metadata=wire( + "proof", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) stibhash: bytes = field( - metadata=wire("stibhash"), + metadata=wire( + "stibhash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) treedepth: int = field( metadata=wire("treedepth"), diff --git a/src/algokit_algod_client/models/_version_contains_the_current_algod_version.py b/src/algokit_algod_client/models/_version_contains_the_current_algod_version.py index 9b7a4deb..fbf3d582 100644 --- a/src/algokit_algod_client/models/_version_contains_the_current_algod_version.py +++ b/src/algokit_algod_client/models/_version_contains_the_current_algod_version.py @@ -8,6 +8,7 @@ from ._build_version_contains_the_current_algod_build_version_information import ( BuildVersionContainsTheCurrentAlgodBuildVersionInformation, ) +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 @dataclass(slots=True) @@ -20,7 +21,11 @@ class VersionContainsTheCurrentAlgodVersion: metadata=nested("build", lambda: BuildVersionContainsTheCurrentAlgodBuildVersionInformation), ) genesis_hash_b64: bytes = field( - metadata=wire("genesis_hash_b64"), + metadata=wire( + "genesis_hash_b64", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) genesis_id: str = field( metadata=wire("genesis_id"), diff --git a/src/algokit_common/constants.py b/src/algokit_common/constants.py index 3147af6f..59f7cc51 100644 --- a/src/algokit_common/constants.py +++ b/src/algokit_common/constants.py @@ -37,3 +37,6 @@ MAX_ASSET_UNIT_NAME_LENGTH = 8 # In bytes MAX_ASSET_URL_LENGTH = 96 # In bytes MAX_ASSET_DECIMALS = 19 + +# Misc +KMD_DEFAULT_WALLET_DRIVER = "sqlite" # TODO: Remove after ensure auto generated client auto sets this by default in kmd diff --git a/src/algokit_indexer_client/models/_account_participation.py b/src/algokit_indexer_client/models/_account_participation.py index f81f38ba..ac1476ea 100644 --- a/src/algokit_indexer_client/models/_account_participation.py +++ b/src/algokit_indexer_client/models/_account_participation.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class AccountParticipation: @@ -14,7 +16,11 @@ class AccountParticipation: """ selection_participation_key: bytes = field( - metadata=wire("selection-participation-key"), + metadata=wire( + "selection-participation-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) vote_first_valid: int = field( metadata=wire("vote-first-valid"), @@ -26,9 +32,17 @@ class AccountParticipation: metadata=wire("vote-last-valid"), ) vote_participation_key: bytes = field( - metadata=wire("vote-participation-key"), + metadata=wire( + "vote-participation-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) state_proof_key: bytes | None = field( default=None, - metadata=wire("state-proof-key"), + metadata=wire( + "state-proof-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_application_log_data.py b/src/algokit_indexer_client/models/_application_log_data.py index 33fcf775..7e238748 100644 --- a/src/algokit_indexer_client/models/_application_log_data.py +++ b/src/algokit_indexer_client/models/_application_log_data.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_sequence, encode_bytes_sequence + @dataclass(slots=True) class ApplicationLogData: @@ -13,7 +15,11 @@ class ApplicationLogData: """ logs: list[bytes] = field( - metadata=wire("logs"), + metadata=wire( + "logs", + encode=encode_bytes_sequence, + decode=decode_bytes_sequence, + ), ) txid: str = field( metadata=wire("txid"), diff --git a/src/algokit_indexer_client/models/_application_params.py b/src/algokit_indexer_client/models/_application_params.py index e261adb0..447ce058 100644 --- a/src/algokit_indexer_client/models/_application_params.py +++ b/src/algokit_indexer_client/models/_application_params.py @@ -6,7 +6,7 @@ from algokit_common.serde import nested, wire from ._application_state_schema import ApplicationStateSchema -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_base64, decode_model_sequence, encode_bytes_base64, encode_model_sequence from ._teal_key_value import TealKeyValue @@ -18,11 +18,19 @@ class ApplicationParams: approval_program: bytes | None = field( default=None, - metadata=wire("approval-program"), + metadata=wire( + "approval-program", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) clear_state_program: bytes | None = field( default=None, - metadata=wire("clear-state-program"), + metadata=wire( + "clear-state-program", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) creator: str | None = field( default=None, diff --git a/src/algokit_indexer_client/models/_asset_params.py b/src/algokit_indexer_client/models/_asset_params.py index ce983a38..a26f092a 100644 --- a/src/algokit_indexer_client/models/_asset_params.py +++ b/src/algokit_indexer_client/models/_asset_params.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class AssetParams: @@ -44,7 +46,11 @@ class AssetParams: ) metadata_hash: bytes | None = field( default=None, - metadata=wire("metadata-hash"), + metadata=wire( + "metadata-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) name: str | None = field( default=None, @@ -52,7 +58,11 @@ class AssetParams: ) name_b64: bytes | None = field( default=None, - metadata=wire("name-b64"), + metadata=wire( + "name-b64", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) reserve: str | None = field( default=None, @@ -64,7 +74,11 @@ class AssetParams: ) unit_name_b64: bytes | None = field( default=None, - metadata=wire("unit-name-b64"), + metadata=wire( + "unit-name-b64", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) url: str | None = field( default=None, @@ -72,5 +86,9 @@ class AssetParams: ) url_b64: bytes | None = field( default=None, - metadata=wire("url-b64"), + metadata=wire( + "url-b64", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_block.py b/src/algokit_indexer_client/models/_block.py index f553189a..ece3e89f 100644 --- a/src/algokit_indexer_client/models/_block.py +++ b/src/algokit_indexer_client/models/_block.py @@ -9,7 +9,7 @@ from ._block_upgrade_state import BlockUpgradeState from ._block_upgrade_vote import BlockUpgradeVote from ._participation_updates import ParticipationUpdates -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_base64, decode_model_sequence, encode_bytes_base64, encode_model_sequence from ._state_proof_tracking import StateProofTracking from ._transaction import Transaction @@ -24,28 +24,48 @@ class Block: """ genesis_hash: bytes = field( - metadata=wire("genesis-hash"), + metadata=wire( + "genesis-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) genesis_id: str = field( metadata=wire("genesis-id"), ) previous_block_hash: bytes = field( - metadata=wire("previous-block-hash"), + metadata=wire( + "previous-block-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) round_: int = field( metadata=wire("round"), ) seed: bytes = field( - metadata=wire("seed"), + metadata=wire( + "seed", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) timestamp: int = field( metadata=wire("timestamp"), ) transactions_root: bytes = field( - metadata=wire("transactions-root"), + metadata=wire( + "transactions-root", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) transactions_root_sha256: bytes = field( - metadata=wire("transactions-root-sha256"), + metadata=wire( + "transactions-root-sha256", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) bonus: int | None = field( default=None, @@ -61,7 +81,11 @@ class Block: ) previous_block_hash_512: bytes | None = field( default=None, - metadata=wire("previous-block-hash-512"), + metadata=wire( + "previous-block-hash-512", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) proposer: str | None = field( default=None, @@ -93,7 +117,11 @@ class Block: ) transactions_root_sha512: bytes | None = field( default=None, - metadata=wire("transactions-root-sha512"), + metadata=wire( + "transactions-root-sha512", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) txn_counter: int | None = field( default=None, diff --git a/src/algokit_indexer_client/models/_box.py b/src/algokit_indexer_client/models/_box.py index a772289e..72b61803 100644 --- a/src/algokit_indexer_client/models/_box.py +++ b/src/algokit_indexer_client/models/_box.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class Box: @@ -13,11 +15,19 @@ class Box: """ name: bytes = field( - metadata=wire("name"), + metadata=wire( + "name", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) round_: int = field( metadata=wire("round"), ) value: bytes = field( - metadata=wire("value"), + metadata=wire( + "value", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_box_descriptor.py b/src/algokit_indexer_client/models/_box_descriptor.py index e21db718..cc6e8e4c 100644 --- a/src/algokit_indexer_client/models/_box_descriptor.py +++ b/src/algokit_indexer_client/models/_box_descriptor.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class BoxDescriptor: @@ -13,5 +15,9 @@ class BoxDescriptor: """ name: bytes = field( - metadata=wire("name"), + metadata=wire( + "name", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_box_reference.py b/src/algokit_indexer_client/models/_box_reference.py index 291dc6be..40f5e05c 100644 --- a/src/algokit_indexer_client/models/_box_reference.py +++ b/src/algokit_indexer_client/models/_box_reference.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class BoxReference: @@ -16,5 +18,9 @@ class BoxReference: metadata=wire("app"), ) name: bytes = field( - metadata=wire("name"), + metadata=wire( + "name", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_hb_proof_fields.py b/src/algokit_indexer_client/models/_hb_proof_fields.py index 2a901ef5..00393292 100644 --- a/src/algokit_indexer_client/models/_hb_proof_fields.py +++ b/src/algokit_indexer_client/models/_hb_proof_fields.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class HbProofFields: @@ -15,21 +17,41 @@ class HbProofFields: hb_pk: bytes | None = field( default=None, - metadata=wire("hb-pk"), + metadata=wire( + "hb-pk", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) hb_pk1sig: bytes | None = field( default=None, - metadata=wire("hb-pk1sig"), + metadata=wire( + "hb-pk1sig", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) hb_pk2: bytes | None = field( default=None, - metadata=wire("hb-pk2"), + metadata=wire( + "hb-pk2", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) hb_pk2sig: bytes | None = field( default=None, - metadata=wire("hb-pk2sig"), + metadata=wire( + "hb-pk2sig", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) hb_sig: bytes | None = field( default=None, - metadata=wire("hb-sig"), + metadata=wire( + "hb-sig", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_indexer_state_proof_message.py b/src/algokit_indexer_client/models/_indexer_state_proof_message.py index abc1eb96..4a474bdb 100644 --- a/src/algokit_indexer_client/models/_indexer_state_proof_message.py +++ b/src/algokit_indexer_client/models/_indexer_state_proof_message.py @@ -5,12 +5,18 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class IndexerStateProofMessage: block_headers_commitment: bytes | None = field( default=None, - metadata=wire("block-headers-commitment"), + metadata=wire( + "block-headers-commitment", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) first_attested_round: int | None = field( default=None, @@ -26,5 +32,9 @@ class IndexerStateProofMessage: ) voters_commitment: bytes | None = field( default=None, - metadata=wire("voters-commitment"), + metadata=wire( + "voters-commitment", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_merkle_array_proof.py b/src/algokit_indexer_client/models/_merkle_array_proof.py index 1beb431b..e75c6783 100644 --- a/src/algokit_indexer_client/models/_merkle_array_proof.py +++ b/src/algokit_indexer_client/models/_merkle_array_proof.py @@ -6,6 +6,7 @@ from algokit_common.serde import nested, wire from ._hash_factory import HashFactory +from ._serde_helpers import decode_bytes_sequence, encode_bytes_sequence @dataclass(slots=True) @@ -16,7 +17,11 @@ class MerkleArrayProof: ) path: list[bytes] | None = field( default=None, - metadata=wire("path"), + metadata=wire( + "path", + encode=encode_bytes_sequence, + decode=decode_bytes_sequence, + ), ) tree_depth: int | None = field( default=None, diff --git a/src/algokit_indexer_client/models/_serde_helpers.py b/src/algokit_indexer_client/models/_serde_helpers.py index 7829af73..bef79ee0 100644 --- a/src/algokit_indexer_client/models/_serde_helpers.py +++ b/src/algokit_indexer_client/models/_serde_helpers.py @@ -1,15 +1,65 @@ # AUTO-GENERATED: oas_generator - - +import base64 +from binascii import Error as BinasciiError from collections.abc import Callable, Iterable, Mapping from dataclasses import is_dataclass from enum import Enum -from typing import TypeVar +from typing import TypeAlias, TypeVar from algokit_common.serde import from_wire, to_wire T = TypeVar("T") E = TypeVar("E", bound=Enum) +BytesLike: TypeAlias = bytes | bytearray | memoryview + + +def _coerce_bytes(value: bytes | bytearray | memoryview) -> bytes: + if isinstance(value, memoryview): + return value.tobytes() + if isinstance(value, bytearray): + return bytes(value) + return value + + +def encode_bytes_base64(value: BytesLike) -> str: + return base64.b64encode(_coerce_bytes(value)).decode("ascii") + + +def decode_bytes_base64(raw: object) -> bytes: + if isinstance(raw, bytes | bytearray | memoryview): + return bytes(raw) + if isinstance(raw, str): + try: + return base64.b64decode(raw.encode("ascii"), validate=True) + except (BinasciiError, UnicodeEncodeError) as exc: + raise ValueError("Invalid base64 payload") from exc + raise TypeError(f"Unsupported value for bytes field: {type(raw)!r}") + + +def encode_bytes_sequence(values: Iterable[BytesLike | None] | None) -> list[str | None] | None: + if values is None: + return None + encoded: list[str | None] = [] + for value in values: + if value is None: + encoded.append(None) + continue + if not isinstance(value, bytes | bytearray | memoryview): + raise TypeError(f"Unsupported value for bytes field sequence: {type(value)!r}") + encoded.append(encode_bytes_base64(value)) + return encoded or None + + +def decode_bytes_sequence(raw: object) -> list[bytes | None] | None: + if not isinstance(raw, list): + return None + decoded: list[bytes | None] = [] + for item in raw: + if item is None: + decoded.append(None) + continue + decoded.append(decode_bytes_base64(item)) + return decoded or None def encode_model_sequence(values: Iterable[object] | None) -> list[dict[str, object]] | None: diff --git a/src/algokit_indexer_client/models/_state_proof_fields.py b/src/algokit_indexer_client/models/_state_proof_fields.py index e9f21713..ece2935b 100644 --- a/src/algokit_indexer_client/models/_state_proof_fields.py +++ b/src/algokit_indexer_client/models/_state_proof_fields.py @@ -6,7 +6,7 @@ from algokit_common.serde import nested, wire from ._merkle_array_proof import MerkleArrayProof -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_base64, decode_model_sequence, encode_bytes_base64, encode_model_sequence from ._state_proof_reveal import StateProofReveal @@ -41,7 +41,11 @@ class StateProofFields: ) sig_commit: bytes | None = field( default=None, - metadata=wire("sig-commit"), + metadata=wire( + "sig-commit", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) sig_proofs: MerkleArrayProof | None = field( default=None, diff --git a/src/algokit_indexer_client/models/_state_proof_signature.py b/src/algokit_indexer_client/models/_state_proof_signature.py index c81e0a37..51693beb 100644 --- a/src/algokit_indexer_client/models/_state_proof_signature.py +++ b/src/algokit_indexer_client/models/_state_proof_signature.py @@ -6,13 +6,18 @@ from algokit_common.serde import nested, wire from ._merkle_array_proof import MerkleArrayProof +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 @dataclass(slots=True) class StateProofSignature: falcon_signature: bytes | None = field( default=None, - metadata=wire("falcon-signature"), + metadata=wire( + "falcon-signature", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) merkle_array_index: int | None = field( default=None, @@ -24,5 +29,9 @@ class StateProofSignature: ) verifying_key: bytes | None = field( default=None, - metadata=wire("verifying-key"), + metadata=wire( + "verifying-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_state_proof_tracking.py b/src/algokit_indexer_client/models/_state_proof_tracking.py index f3d1df96..f337b054 100644 --- a/src/algokit_indexer_client/models/_state_proof_tracking.py +++ b/src/algokit_indexer_client/models/_state_proof_tracking.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class StateProofTracking: @@ -22,5 +24,9 @@ class StateProofTracking: ) voters_commitment: bytes | None = field( default=None, - metadata=wire("voters-commitment"), + metadata=wire( + "voters-commitment", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_state_proof_verifier.py b/src/algokit_indexer_client/models/_state_proof_verifier.py index fc1d1497..74f28958 100644 --- a/src/algokit_indexer_client/models/_state_proof_verifier.py +++ b/src/algokit_indexer_client/models/_state_proof_verifier.py @@ -5,12 +5,18 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class StateProofVerifier: commitment: bytes | None = field( default=None, - metadata=wire("commitment"), + metadata=wire( + "commitment", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) key_lifetime: int | None = field( default=None, diff --git a/src/algokit_indexer_client/models/_teal_value.py b/src/algokit_indexer_client/models/_teal_value.py index b437a5c3..a0b969dc 100644 --- a/src/algokit_indexer_client/models/_teal_value.py +++ b/src/algokit_indexer_client/models/_teal_value.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class TealValue: @@ -13,7 +15,11 @@ class TealValue: """ bytes_: bytes = field( - metadata=wire("bytes"), + metadata=wire( + "bytes", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) type_: int = field( metadata=wire("type"), diff --git a/src/algokit_indexer_client/models/_transaction.py b/src/algokit_indexer_client/models/_transaction.py index 7f27b319..c8893645 100644 --- a/src/algokit_indexer_client/models/_transaction.py +++ b/src/algokit_indexer_client/models/_transaction.py @@ -7,7 +7,14 @@ from ._account_state_delta import AccountStateDelta from ._eval_delta_key_value import EvalDeltaKeyValue -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import ( + decode_bytes_base64, + decode_bytes_sequence, + decode_model_sequence, + encode_bytes_base64, + encode_bytes_sequence, + encode_model_sequence, +) from ._transaction_application import TransactionApplication from ._transaction_asset_config import TransactionAssetConfig from ._transaction_asset_freeze import TransactionAssetFreeze @@ -87,7 +94,11 @@ class Transaction: ) genesis_hash: bytes | None = field( default=None, - metadata=wire("genesis-hash"), + metadata=wire( + "genesis-hash", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) genesis_id: str | None = field( default=None, @@ -103,7 +114,11 @@ class Transaction: ) group: bytes | None = field( default=None, - metadata=wire("group"), + metadata=wire( + "group", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) heartbeat_transaction: TransactionHeartbeat | None = field( default=None, @@ -131,7 +146,11 @@ class Transaction: ) lease: bytes | None = field( default=None, - metadata=wire("lease"), + metadata=wire( + "lease", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) local_state_delta: list[AccountStateDelta] | None = field( default=None, @@ -143,11 +162,19 @@ class Transaction: ) logs: list[bytes] | None = field( default=None, - metadata=wire("logs"), + metadata=wire( + "logs", + encode=encode_bytes_sequence, + decode=decode_bytes_sequence, + ), ) note: bytes | None = field( default=None, - metadata=wire("note"), + metadata=wire( + "note", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) payment_transaction: TransactionPayment | None = field( default=None, diff --git a/src/algokit_indexer_client/models/_transaction_application.py b/src/algokit_indexer_client/models/_transaction_application.py index 0d7f1fac..37f2a00e 100644 --- a/src/algokit_indexer_client/models/_transaction_application.py +++ b/src/algokit_indexer_client/models/_transaction_application.py @@ -8,7 +8,7 @@ from ._box_reference import BoxReference from ._on_completion import OnCompletion from ._resource_ref import ResourceRef -from ._serde_helpers import decode_model_sequence, encode_model_sequence +from ._serde_helpers import decode_bytes_base64, decode_model_sequence, encode_bytes_base64, encode_model_sequence from ._state_schema import StateSchema @@ -45,7 +45,11 @@ class TransactionApplication: ) approval_program: bytes | None = field( default=None, - metadata=wire("approval-program"), + metadata=wire( + "approval-program", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) box_references: list[BoxReference] | None = field( default=None, @@ -57,7 +61,11 @@ class TransactionApplication: ) clear_state_program: bytes | None = field( default=None, - metadata=wire("clear-state-program"), + metadata=wire( + "clear-state-program", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) extra_program_pages: int | None = field( default=None, diff --git a/src/algokit_indexer_client/models/_transaction_heartbeat.py b/src/algokit_indexer_client/models/_transaction_heartbeat.py index 2bfea45b..fa5b7bb1 100644 --- a/src/algokit_indexer_client/models/_transaction_heartbeat.py +++ b/src/algokit_indexer_client/models/_transaction_heartbeat.py @@ -6,6 +6,7 @@ from algokit_common.serde import nested, wire from ._hb_proof_fields import HbProofFields +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 @dataclass(slots=True) @@ -27,8 +28,16 @@ class TransactionHeartbeat: metadata=nested("hb-proof", lambda: HbProofFields), ) hb_seed: bytes = field( - metadata=wire("hb-seed"), + metadata=wire( + "hb-seed", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) hb_vote_id: bytes = field( - metadata=wire("hb-vote-id"), + metadata=wire( + "hb-vote-id", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_transaction_keyreg.py b/src/algokit_indexer_client/models/_transaction_keyreg.py index 94c039f7..58ede05a 100644 --- a/src/algokit_indexer_client/models/_transaction_keyreg.py +++ b/src/algokit_indexer_client/models/_transaction_keyreg.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class TransactionKeyreg: @@ -21,11 +23,19 @@ class TransactionKeyreg: ) selection_participation_key: bytes | None = field( default=None, - metadata=wire("selection-participation-key"), + metadata=wire( + "selection-participation-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) state_proof_key: bytes | None = field( default=None, - metadata=wire("state-proof-key"), + metadata=wire( + "state-proof-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) vote_first_valid: int | None = field( default=None, @@ -41,5 +51,9 @@ class TransactionKeyreg: ) vote_participation_key: bytes | None = field( default=None, - metadata=wire("vote-participation-key"), + metadata=wire( + "vote-participation-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_transaction_signature.py b/src/algokit_indexer_client/models/_transaction_signature.py index d19559cf..45a15157 100644 --- a/src/algokit_indexer_client/models/_transaction_signature.py +++ b/src/algokit_indexer_client/models/_transaction_signature.py @@ -5,6 +5,7 @@ from algokit_common.serde import nested, wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 from ._transaction_signature_logicsig import TransactionSignatureLogicsig from ._transaction_signature_multisig import TransactionSignatureMultisig @@ -26,5 +27,9 @@ class TransactionSignature: ) sig: bytes | None = field( default=None, - metadata=wire("sig"), + metadata=wire( + "sig", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_transaction_signature_logicsig.py b/src/algokit_indexer_client/models/_transaction_signature_logicsig.py index 983a21f7..451ab6d1 100644 --- a/src/algokit_indexer_client/models/_transaction_signature_logicsig.py +++ b/src/algokit_indexer_client/models/_transaction_signature_logicsig.py @@ -5,6 +5,7 @@ from algokit_common.serde import nested, wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 from ._transaction_signature_multisig import TransactionSignatureMultisig @@ -18,7 +19,11 @@ class TransactionSignatureLogicsig: """ logic: bytes = field( - metadata=wire("logic"), + metadata=wire( + "logic", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) args: list[str] | None = field( default=None, @@ -34,5 +39,9 @@ class TransactionSignatureLogicsig: ) signature: bytes | None = field( default=None, - metadata=wire("signature"), + metadata=wire( + "signature", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py b/src/algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py index 3c1e5197..30180c25 100644 --- a/src/algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py +++ b/src/algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py @@ -5,14 +5,24 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class TransactionSignatureMultisigSubsignature: public_key: bytes | None = field( default=None, - metadata=wire("public-key"), + metadata=wire( + "public-key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) signature: bytes | None = field( default=None, - metadata=wire("signature"), + metadata=wire( + "signature", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_kmd_client/models/_import_key_request.py b/src/algokit_kmd_client/models/_import_key_request.py index 3c3d2a91..fb3a7fc1 100644 --- a/src/algokit_kmd_client/models/_import_key_request.py +++ b/src/algokit_kmd_client/models/_import_key_request.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class ImportKeyRequest: @@ -14,7 +16,11 @@ class ImportKeyRequest: private_key: bytes | None = field( default=None, - metadata=wire("private_key"), + metadata=wire( + "private_key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) wallet_handle_token: str | None = field( default=None, diff --git a/src/algokit_kmd_client/models/_postkey_export_response.py b/src/algokit_kmd_client/models/_postkey_export_response.py index 211e7034..8de9e294 100644 --- a/src/algokit_kmd_client/models/_postkey_export_response.py +++ b/src/algokit_kmd_client/models/_postkey_export_response.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class PostkeyExportResponse: @@ -23,5 +25,9 @@ class PostkeyExportResponse: ) private_key: bytes | None = field( default=None, - metadata=wire("private_key"), + metadata=wire( + "private_key", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_kmd_client/models/_postmultisig_program_sign_response.py b/src/algokit_kmd_client/models/_postmultisig_program_sign_response.py index 90f6226f..95dec2cd 100644 --- a/src/algokit_kmd_client/models/_postmultisig_program_sign_response.py +++ b/src/algokit_kmd_client/models/_postmultisig_program_sign_response.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class PostmultisigProgramSignResponse: @@ -23,5 +25,9 @@ class PostmultisigProgramSignResponse: ) multisig: bytes | None = field( default=None, - metadata=wire("multisig"), + metadata=wire( + "multisig", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_kmd_client/models/_postmultisig_transaction_sign_response.py b/src/algokit_kmd_client/models/_postmultisig_transaction_sign_response.py index 5af32f31..f5a0fb7d 100644 --- a/src/algokit_kmd_client/models/_postmultisig_transaction_sign_response.py +++ b/src/algokit_kmd_client/models/_postmultisig_transaction_sign_response.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class PostmultisigTransactionSignResponse: @@ -23,5 +25,9 @@ class PostmultisigTransactionSignResponse: ) multisig: bytes | None = field( default=None, - metadata=wire("multisig"), + metadata=wire( + "multisig", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_kmd_client/models/_postprogram_sign_response.py b/src/algokit_kmd_client/models/_postprogram_sign_response.py index 40b7c594..1ed64774 100644 --- a/src/algokit_kmd_client/models/_postprogram_sign_response.py +++ b/src/algokit_kmd_client/models/_postprogram_sign_response.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class PostprogramSignResponse: @@ -23,5 +25,9 @@ class PostprogramSignResponse: ) sig: bytes | None = field( default=None, - metadata=wire("sig"), + metadata=wire( + "sig", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_kmd_client/models/_posttransaction_sign_response.py b/src/algokit_kmd_client/models/_posttransaction_sign_response.py index f5b7fb4b..78c22969 100644 --- a/src/algokit_kmd_client/models/_posttransaction_sign_response.py +++ b/src/algokit_kmd_client/models/_posttransaction_sign_response.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class PosttransactionSignResponse: @@ -23,5 +25,9 @@ class PosttransactionSignResponse: ) signed_transaction: bytes | None = field( default=None, - metadata=wire("signed_transaction"), + metadata=wire( + "signed_transaction", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) diff --git a/src/algokit_kmd_client/models/_serde_helpers.py b/src/algokit_kmd_client/models/_serde_helpers.py index 7829af73..bef79ee0 100644 --- a/src/algokit_kmd_client/models/_serde_helpers.py +++ b/src/algokit_kmd_client/models/_serde_helpers.py @@ -1,15 +1,65 @@ # AUTO-GENERATED: oas_generator - - +import base64 +from binascii import Error as BinasciiError from collections.abc import Callable, Iterable, Mapping from dataclasses import is_dataclass from enum import Enum -from typing import TypeVar +from typing import TypeAlias, TypeVar from algokit_common.serde import from_wire, to_wire T = TypeVar("T") E = TypeVar("E", bound=Enum) +BytesLike: TypeAlias = bytes | bytearray | memoryview + + +def _coerce_bytes(value: bytes | bytearray | memoryview) -> bytes: + if isinstance(value, memoryview): + return value.tobytes() + if isinstance(value, bytearray): + return bytes(value) + return value + + +def encode_bytes_base64(value: BytesLike) -> str: + return base64.b64encode(_coerce_bytes(value)).decode("ascii") + + +def decode_bytes_base64(raw: object) -> bytes: + if isinstance(raw, bytes | bytearray | memoryview): + return bytes(raw) + if isinstance(raw, str): + try: + return base64.b64decode(raw.encode("ascii"), validate=True) + except (BinasciiError, UnicodeEncodeError) as exc: + raise ValueError("Invalid base64 payload") from exc + raise TypeError(f"Unsupported value for bytes field: {type(raw)!r}") + + +def encode_bytes_sequence(values: Iterable[BytesLike | None] | None) -> list[str | None] | None: + if values is None: + return None + encoded: list[str | None] = [] + for value in values: + if value is None: + encoded.append(None) + continue + if not isinstance(value, bytes | bytearray | memoryview): + raise TypeError(f"Unsupported value for bytes field sequence: {type(value)!r}") + encoded.append(encode_bytes_base64(value)) + return encoded or None + + +def decode_bytes_sequence(raw: object) -> list[bytes | None] | None: + if not isinstance(raw, list): + return None + decoded: list[bytes | None] = [] + for item in raw: + if item is None: + decoded.append(None) + continue + decoded.append(decode_bytes_base64(item)) + return decoded or None def encode_model_sequence(values: Iterable[object] | None) -> list[dict[str, object]] | None: diff --git a/src/algokit_kmd_client/models/_sign_multisig_request.py b/src/algokit_kmd_client/models/_sign_multisig_request.py index bc6403c8..961bf864 100644 --- a/src/algokit_kmd_client/models/_sign_multisig_request.py +++ b/src/algokit_kmd_client/models/_sign_multisig_request.py @@ -6,6 +6,7 @@ from algokit_common.serde import nested, wire from ._multisig_sig import MultisigSig +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 @dataclass(slots=True) @@ -28,7 +29,11 @@ class SignMultisigRequest: ) transaction: bytes | None = field( default=None, - metadata=wire("transaction"), + metadata=wire( + "transaction", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) wallet_handle_token: str | None = field( default=None, diff --git a/src/algokit_kmd_client/models/_sign_program_multisig_request.py b/src/algokit_kmd_client/models/_sign_program_multisig_request.py index 4b41cb03..8d73af30 100644 --- a/src/algokit_kmd_client/models/_sign_program_multisig_request.py +++ b/src/algokit_kmd_client/models/_sign_program_multisig_request.py @@ -6,6 +6,7 @@ from algokit_common.serde import nested, wire from ._multisig_sig import MultisigSig +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 @dataclass(slots=True) @@ -20,7 +21,11 @@ class SignProgramMultisigRequest: ) data: bytes | None = field( default=None, - metadata=wire("data"), + metadata=wire( + "data", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) partial_multisig: MultisigSig | None = field( default=None, diff --git a/src/algokit_kmd_client/models/_sign_program_request.py b/src/algokit_kmd_client/models/_sign_program_request.py index e808686e..f5f8d4ce 100644 --- a/src/algokit_kmd_client/models/_sign_program_request.py +++ b/src/algokit_kmd_client/models/_sign_program_request.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class SignProgramRequest: @@ -18,7 +20,11 @@ class SignProgramRequest: ) data: bytes | None = field( default=None, - metadata=wire("data"), + metadata=wire( + "data", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) wallet_handle_token: str | None = field( default=None, diff --git a/src/algokit_kmd_client/models/_sign_transaction_request.py b/src/algokit_kmd_client/models/_sign_transaction_request.py index 000919fe..c0e8b3a8 100644 --- a/src/algokit_kmd_client/models/_sign_transaction_request.py +++ b/src/algokit_kmd_client/models/_sign_transaction_request.py @@ -5,6 +5,8 @@ from algokit_common.serde import wire +from ._serde_helpers import decode_bytes_base64, encode_bytes_base64 + @dataclass(slots=True) class SignTransactionRequest: @@ -18,7 +20,11 @@ class SignTransactionRequest: ) transaction: bytes | None = field( default=None, - metadata=wire("transaction"), + metadata=wire( + "transaction", + encode=encode_bytes_base64, + decode=decode_bytes_base64, + ), ) wallet_handle_token: str | None = field( default=None, diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py index 86158869..56d92ffd 100644 --- a/src/algokit_utils/accounts/kmd_account_manager.py +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -1,8 +1,14 @@ +from base64 import b64encode from collections.abc import Callable from typing import Any, cast -from algosdk.kmd import KMDClient - +from algokit_common.constants import KMD_DEFAULT_WALLET_DRIVER +from algokit_kmd_client.client import KmdClient +from algokit_kmd_client.models._create_wallet_request import CreateWalletRequest +from algokit_kmd_client.models._export_key_request import ExportKeyRequest +from algokit_kmd_client.models._generate_key_request import GenerateKeyRequest +from algokit_kmd_client.models._init_wallet_handle_token_request import InitWalletHandleTokenRequest +from algokit_kmd_client.models._list_keys_request import ListKeysRequest from algokit_utils.clients.client_manager import ClientManager from algokit_utils.config import config from algokit_utils.models.account import SigningAccount @@ -28,7 +34,7 @@ def __init__(self, private_key: str, address: str | None = None) -> None: class KmdAccountManager: """Provides abstractions over KMD that makes it easier to get and manage accounts.""" - _kmd: KMDClient | None + _kmd: KmdClient | None def __init__(self, client_manager: ClientManager) -> None: self._client_manager = client_manager @@ -37,7 +43,7 @@ def __init__(self, client_manager: ClientManager) -> None: except ValueError: self._kmd = None - def kmd(self) -> KMDClient: + def kmd(self) -> KmdClient: """Returns the KMD client, initializing it if needed. :raises Exception: If KMD client is not configured and not running against LocalNet @@ -68,17 +74,21 @@ def get_wallet_account( :param predicate: Optional filter to use to find the account (otherwise gets a random account from the wallet) :param sender: Optional sender address to use this signer for (aka a rekeyed account) :return: The signing account or None if no matching wallet or account was found + + :raises Exception: If error received while exporting the private key from KMD """ kmd_client = self.kmd() - wallets = kmd_client.list_wallets() - wallet = next((w for w in wallets if w["name"] == wallet_name), None) + wallets = kmd_client.list_wallets().wallets or [] + wallet = next((w for w in wallets if w.name == wallet_name), None) if not wallet: return None - wallet_id = wallet["id"] - wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") - addresses = kmd_client.list_keys(wallet_handle) + wallet_id = wallet.id_ + wallet_handle = kmd_client.init_wallet_handle_token( + InitWalletHandleTokenRequest(wallet_id, "") + ).wallet_handle_token + addresses = kmd_client.list_keys_in_wallet(ListKeysRequest(wallet_handle)).addresses or [] matched_address = None if predicate: @@ -93,8 +103,11 @@ def get_wallet_account( if not matched_address: return None - private_key = kmd_client.export_key(wallet_handle, "", matched_address) - return KmdAccount(private_key=private_key, address=sender) + private_key = kmd_client.export_key(ExportKeyRequest(matched_address, wallet_handle, "")).private_key + if not private_key: + raise Exception(f"Error exporting key for address {matched_address} from KMD wallet {wallet_name}") + private_key_str = b64encode(private_key).decode("ascii") + return KmdAccount(private_key=private_key_str, address=sender) def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = None) -> KmdAccount: """Gets or creates a funded account in a KMD wallet of the given name. @@ -104,6 +117,8 @@ def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = :param name: The name of the wallet to retrieve / create :param fund_with: The number of Algos to fund the account with when created :return: An Algorand account with private key loaded + + :raises Exception: If error received while creating the wallet or funding the account """ fund_with = fund_with or AlgoAmount.from_algo(1000) @@ -112,9 +127,16 @@ def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = return existing kmd_client = self.kmd() - wallet_id = kmd_client.create_wallet(name, "")["id"] - wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") - kmd_client.generate_key(wallet_handle) + wallet = kmd_client.create_wallet( + CreateWalletRequest(wallet_name=name, wallet_password="", wallet_driver_name=KMD_DEFAULT_WALLET_DRIVER) + ).wallet + if not wallet: + raise Exception(f"Error creating KMD wallet with name {name}") + wallet_id = wallet.id_ + wallet_handle = kmd_client.init_wallet_handle_token( + InitWalletHandleTokenRequest(wallet_id, "") + ).wallet_handle_token + kmd_client.generate_key(GenerateKeyRequest(wallet_handle_token=wallet_handle)) account = self.get_wallet_account(name) assert account is not None diff --git a/src/algokit_utils/algorand.py b/src/algokit_utils/algorand.py index 18be6aca..05347abb 100644 --- a/src/algokit_utils/algorand.py +++ b/src/algokit_utils/algorand.py @@ -3,11 +3,11 @@ import typing_extensions from algosdk.atomic_transaction_composer import TransactionSigner -from algosdk.kmd import KMDClient from algosdk.transaction import SuggestedParams from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient +from algokit_indexer_client import IndexerClient +from algokit_kmd_client.client import KmdClient from algokit_utils.accounts.account_manager import AccountManager from algokit_utils.applications.app_deployer import AppDeployer from algokit_utils.applications.app_manager import AppManager @@ -328,7 +328,7 @@ def mainnet() -> "AlgorandClient": @staticmethod def from_clients( - algod: AlgodClient, indexer: IndexerClient | None = None, kmd: KMDClient | None = None + algod: AlgodClient, indexer: IndexerClient | None = None, kmd: KmdClient | None = None ) -> "AlgorandClient": """ Returns an `AlgorandClient` pointing to the given client(s). diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 412515c4..347621df 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -5,8 +5,8 @@ from typing import Literal from algosdk.logic import get_application_address -from algosdk.v2client.indexer import IndexerClient +from algokit_indexer_client import IndexerClient from algokit_utils.applications.abi import ABIReturn from algokit_utils.applications.app_manager import AppManager from algokit_utils.applications.enums import OnSchemaBreak, OnUpdate, OperationPerformed @@ -708,34 +708,37 @@ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = app_lookup: dict[str, ApplicationMetaData] = {} # Get all apps created by account - created_apps = self._indexer.search_applications(creator=creator_address) + # TODO: See if empty iterable responses can be changed to empty lists instead of None + created_apps = self._indexer.search_for_applications(creator=creator_address).applications or [] - for app in created_apps["applications"]: - app_id = app["id"] + for app in created_apps: + app_id = app.id_ # Get creation transaction - creation_txns = self._indexer.search_transactions( + creation_txns = self._indexer.search_for_transactions( application_id=app_id, - min_round=app["created-at-round"], + min_round=app.created_at_round, address=creator_address, address_role="sender", - note_prefix=APP_DEPLOY_NOTE_DAPP.encode(), + note_prefix=APP_DEPLOY_NOTE_DAPP, limit=1, - ) + ).transactions - if not creation_txns["transactions"]: + if not creation_txns: continue - creation_txn = creation_txns["transactions"][0] + creation_txn = creation_txns[0] try: - note = base64.b64decode(creation_txn["note"]).decode() + if not creation_txn.note: + continue + note = base64.b64decode(creation_txn.note).decode() if not note.startswith(f"{APP_DEPLOY_NOTE_DAPP}:j"): continue metadata = json.loads(note[len(APP_DEPLOY_NOTE_DAPP) + 2 :]) - if metadata.get("name"): + if metadata.get("name") and creation_txn.confirmed_round: app_lookup[metadata["name"]] = ApplicationMetaData( reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)), deploy_metadata=AppDeploymentMetaData( @@ -744,9 +747,9 @@ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = deletable=metadata.get("deletable"), updatable=metadata.get("updatable"), ), - created_round=creation_txn["confirmed-round"], - updated_round=creation_txn["confirmed-round"], - deleted=app.get("deleted", False), + created_round=creation_txn.confirmed_round, + updated_round=creation_txn.confirmed_round, + deleted=app.deleted if app.deleted is not None else False, ) except Exception as e: config.logger.warning( diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index ffa1bba6..0f02db24 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -5,16 +5,15 @@ from typing import TYPE_CHECKING, Literal, TypeVar from urllib import parse -import algosdk from algosdk.atomic_transaction_composer import TransactionSigner -from algosdk.kmd import KMDClient from algosdk.source_map import SourceMap from algosdk.transaction import SuggestedParams from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient -from algokit_utils.applications.app_deployer import ApplicationLookup -from algokit_utils.applications.app_spec.arc56 import Arc56Contract +from algokit_indexer_client import ClientConfig as IndexerClientConfig +from algokit_indexer_client import IndexerClient +from algokit_kmd_client import ClientConfig as KmdClientConfig +from algokit_kmd_client import KmdClient from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol @@ -22,7 +21,9 @@ if TYPE_CHECKING: from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_client import AppClient, AppClientCompilationParams + from algokit_utils.applications.app_deployer import ApplicationLookup from algokit_utils.applications.app_factory import AppFactory + from algokit_utils.applications.app_spec.arc56 import Arc56Contract __all__ = [ "AlgoSdkClients", @@ -46,9 +47,9 @@ class AlgoSdkClients: def __init__( self, - algod: algosdk.v2client.algod.AlgodClient, + algod: AlgodClient, indexer: IndexerClient | None = None, - kmd: KMDClient | None = None, + kmd: KmdClient | None = None, ): self.algod = algod self.indexer = indexer @@ -134,7 +135,7 @@ def algod(self) -> AlgodClient: @property def indexer(self) -> IndexerClient: - """Returns an algosdk Indexer API client. + """Returns an Indexer API client. :raises ValueError: If no Indexer client is configured :return: Indexer client instance @@ -152,8 +153,8 @@ def indexer_if_present(self) -> IndexerClient | None: return self._indexer @property - def kmd(self) -> KMDClient: - """Returns an algosdk KMD API client. + def kmd(self) -> KmdClient: + """Returns a KMD-compatible API client. :raises ValueError: If no KMD client is configured :return: KMD client instance @@ -392,16 +393,20 @@ def get_algod_client_from_environment() -> AlgodClient: return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment()) @staticmethod - def get_kmd_client(config: AlgoClientNetworkConfig) -> KMDClient: + def get_kmd_client(config: AlgoClientNetworkConfig) -> KmdClient: """Get a KMD client from config or environment. :param config: Optional client configuration :return: KMD client instance """ - return KMDClient(config.token, config.full_url()) + client_config = KmdClientConfig( + base_url=config.full_url(), + token=config.token or None, + ) + return KmdClient(client_config) @staticmethod - def get_kmd_client_from_environment() -> KMDClient: + def get_kmd_client_from_environment() -> KmdClient: """Get a KMD client from environment variables. :return: KMD client instance @@ -415,12 +420,11 @@ def get_indexer_client(config: AlgoClientNetworkConfig) -> IndexerClient: :param config: Optional client configuration :return: Indexer client instance """ - headers = {"X-Indexer-API-Token": config.token} - return IndexerClient( - indexer_token=config.token, - indexer_address=config.full_url(), - headers=headers, + client_config = IndexerClientConfig( + base_url=config.full_url(), + token=config.token or None, ) + return IndexerClient(client_config) @staticmethod def get_indexer_client_from_environment() -> IndexerClient: