diff --git a/.github/workflows/bindings-python-publish.yml b/.github/workflows/bindings-python-publish.yml index fbf928fa25..cb90bda6e2 100644 --- a/.github/workflows/bindings-python-publish.yml +++ b/.github/workflows/bindings-python-publish.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -105,11 +105,7 @@ jobs: matrix: os: [ubuntu-latest] docker-container: - [ - "python:3.9.12-slim-bullseye", - "python:3.10.10-slim-bullseye", - "python:3.11.1-slim-bullseye", - ] + ["python:3.10.10-slim-bullseye", "python:3.11.1-slim-bullseye"] steps: - name: Checkout diff --git a/.github/workflows/bindings-python.yml b/.github/workflows/bindings-python.yml index b78fc63137..8525900d93 100644 --- a/.github/workflows/bindings-python.yml +++ b/.github/workflows/bindings-python.yml @@ -45,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9"] + python-version: ["3.10"] steps: - name: Checkout the Source Code @@ -67,7 +67,6 @@ jobs: working-directory: bindings/python run: tox -e format - test: name: Linter & Tests needs: lint @@ -78,7 +77,7 @@ jobs: matrix: # os: [windows-latest, macos-latest, ubuntu-latest] os: [windows-latest, ubuntu-latest] - python-version: ["3.9"] + python-version: ["3.10"] steps: - name: Checkout the Source Code @@ -125,4 +124,4 @@ jobs: # TODO temporarily disabled https://github.com/iotaledger/iota-sdk/issues/647 # - name: Run tests # working-directory: bindings/python - # run: tox \ No newline at end of file + # run: tox diff --git a/bindings/python/README.md b/bindings/python/README.md index 190ebaa0a6..605b38fb46 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -17,7 +17,7 @@ Python binding to the [iota-sdk library](/README.md). ## Requirements -- [Python 3.9+](https://www.python.org) +- [Python 3.10+](https://www.python.org) - [pip ^21.x](https://pypi.org/project/pip) - `Rust` and `Cargo` to compile the binding. Install them [here](https://doc.rust-lang.org/cargo/getting-started/installation.html). diff --git a/bindings/python/iota_sdk/__init__.py b/bindings/python/iota_sdk/__init__.py index 05dabae550..d77785ee3f 100644 --- a/bindings/python/iota_sdk/__init__.py +++ b/bindings/python/iota_sdk/__init__.py @@ -30,6 +30,7 @@ from .types.output import * from .types.output_data import * from .types.output_id import * +from .types.output_metadata import * from .types.output_params import * from .types.payload import * from .types.send_params import * diff --git a/bindings/python/iota_sdk/client/_high_level_api.py b/bindings/python/iota_sdk/client/_high_level_api.py index ea4328dc86..08dd84cdcc 100644 --- a/bindings/python/iota_sdk/client/_high_level_api.py +++ b/bindings/python/iota_sdk/client/_high_level_api.py @@ -6,7 +6,7 @@ from abc import ABCMeta, abstractmethod from iota_sdk.types.block import Block from iota_sdk.types.common import CoinType, HexStr, json -from iota_sdk.types.output import OutputWithMetadata +from iota_sdk.types.output_metadata import OutputWithMetadata from iota_sdk.types.output_id import OutputId diff --git a/bindings/python/iota_sdk/client/_node_core_api.py b/bindings/python/iota_sdk/client/_node_core_api.py index c32ae6940f..3db17d052d 100644 --- a/bindings/python/iota_sdk/client/_node_core_api.py +++ b/bindings/python/iota_sdk/client/_node_core_api.py @@ -1,14 +1,14 @@ # Copyright 2023 IOTA Stiftung # SPDX-License-Identifier: Apache-2.0 -from typing import List, Union +from typing import List, Optional, Union from abc import ABCMeta, abstractmethod from dacite import from_dict from iota_sdk.types.block import Block, BlockMetadata from iota_sdk.types.common import HexStr from iota_sdk.types.node_info import NodeInfo, NodeInfoWrapper -from iota_sdk.types.output import OutputWithMetadata, OutputMetadata +from iota_sdk.types.output_metadata import OutputWithMetadata, OutputMetadata from iota_sdk.types.output_id import OutputId @@ -149,7 +149,7 @@ def get_included_block_metadata( })) def call_plugin_route(self, base_plugin_path: str, method: str, - endpoint: str, query_params: [str] = None, request: str = None): + endpoint: str, query_params: Optional[List[str]] = None, request: Optional[str] = None): """Extension method which provides request methods for plugins. Args: diff --git a/bindings/python/iota_sdk/client/client.py b/bindings/python/iota_sdk/client/client.py index 2002eb6325..2d501d06e2 100644 --- a/bindings/python/iota_sdk/client/client.py +++ b/bindings/python/iota_sdk/client/client.py @@ -18,7 +18,7 @@ from iota_sdk.types.feature import Feature from iota_sdk.types.native_token import NativeToken from iota_sdk.types.network_info import NetworkInfo -from iota_sdk.types.output import AccountOutput, BasicOutput, FoundryOutput, NftOutput, output_from_dict +from iota_sdk.types.output import AccountOutput, BasicOutput, FoundryOutput, NftOutput, deserialize_output from iota_sdk.types.payload import Payload, TransactionPayload from iota_sdk.types.token_scheme import SimpleTokenScheme from iota_sdk.types.unlock_condition import UnlockCondition @@ -197,7 +197,7 @@ def build_account_output(self, if mana: mana = str(mana) - return output_from_dict(self._call_method('buildAccountOutput', { + return deserialize_output(self._call_method('buildAccountOutput', { 'accountId': account_id, 'unlockConditions': unlock_conditions, 'amount': amount, @@ -245,7 +245,7 @@ def build_basic_output(self, if mana: mana = str(mana) - return output_from_dict(self._call_method('buildBasicOutput', { + return deserialize_output(self._call_method('buildBasicOutput', { 'unlockConditions': unlock_conditions, 'amount': amount, 'mana': mana, @@ -292,7 +292,7 @@ def build_foundry_output(self, if amount: amount = str(amount) - return output_from_dict(self._call_method('buildFoundryOutput', { + return deserialize_output(self._call_method('buildFoundryOutput', { 'serialNumber': serial_number, 'tokenScheme': token_scheme.to_dict(), 'unlockConditions': unlock_conditions, @@ -344,7 +344,7 @@ def build_nft_output(self, if mana: mana = str(mana) - return output_from_dict(self._call_method('buildNftOutput', { + return deserialize_output(self._call_method('buildNftOutput', { 'nftId': nft_id, 'unlockConditions': unlock_conditions, 'amount': amount, diff --git a/bindings/python/iota_sdk/types/address.py b/bindings/python/iota_sdk/types/address.py index f73e2c5490..05c9e5a299 100644 --- a/bindings/python/iota_sdk/types/address.py +++ b/bindings/python/iota_sdk/types/address.py @@ -2,10 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 from enum import IntEnum - from dataclasses import dataclass, field - - +from typing import Any, Dict, List, TypeAlias, Union from iota_sdk.types.common import HexStr, json @@ -69,19 +67,6 @@ class NFTAddress(Address): type: int = field(default_factory=lambda: int(AddressType.NFT), init=False) -@json -@dataclass -# pylint: disable=function-redefined -# TODO: Change name -class AccountAddress(): - """An Address of the Account. - """ - address: str - key_index: int - internal: bool - used: bool - - @json @dataclass class AddressWithUnspentOutputs(): @@ -91,3 +76,34 @@ class AddressWithUnspentOutputs(): key_index: int internal: bool output_ids: bool + + +AddressUnion: TypeAlias = Union[Ed25519Address, AccountAddress, NFTAddress] + + +def deserialize_address(d: Dict[str, Any]) -> AddressUnion: + """ + Takes a dictionary as input and returns an instance of a specific class based on the value of the 'type' key in the dictionary. + + Arguments: + * `d`: A dictionary that is expected to have a key called 'type' which specifies the type of the returned value. + """ + address_type = d['type'] + if address_type == AddressType.ED25519: + return Ed25519Address.from_dict(d) + if address_type == AddressType.ACCOUNT: + return AccountAddress.from_dict(d) + if address_type == AddressType.NFT: + return NFTAddress.from_dict(d) + raise Exception(f'invalid address type: {address_type}') + + +def deserialize_addresses( + dicts: List[Dict[str, Any]]) -> List[AddressUnion]: + """ + Takes a list of dictionaries as input and returns a list with specific instances of a classes based on the value of the 'type' key in the dictionary. + + Arguments: + * `dicts`: A list of dictionaries that are expected to have a key called 'type' which specifies the type of the returned value. + """ + return list(map(deserialize_address, dicts)) diff --git a/bindings/python/iota_sdk/types/context_input.py b/bindings/python/iota_sdk/types/context_input.py index 2d2b2c2021..f8e85ef0c0 100644 --- a/bindings/python/iota_sdk/types/context_input.py +++ b/bindings/python/iota_sdk/types/context_input.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import IntEnum +from typing import Any, Dict, List, TypeAlias, Union from iota_sdk.types.common import HexStr, json @@ -70,3 +71,35 @@ class RewardContextInput(ContextInput): default_factory=lambda: int( ContextInputType.Reward), init=False) + + +ContextInputUnion: TypeAlias = Union[CommitmentContextInput, + BlockIssuanceCreditContextInput, RewardContextInput] + + +def deserialize_context_input(d: Dict[str, Any]) -> ContextInputUnion: + """ + Takes a dictionary as input and returns an instance of a specific class based on the value of the 'type' key in the dictionary. + + Arguments: + * `d`: A dictionary that is expected to have a key called 'type' which specifies the type of the returned value. + """ + context_input_type = dict['type'] + if context_input_type == ContextInputType.Commitment: + return CommitmentContextInput.from_dict(d) + if context_input_type == ContextInputType.BlockIssuanceCredit: + return BlockIssuanceCreditContextInput.from_dict(d) + if context_input_type == ContextInputType.Reward: + return RewardContextInput.from_dict(d) + raise Exception(f'invalid context input type: {context_input_type}') + + +def deserialize_context_inputs( + dicts: List[Dict[str, Any]]) -> List[ContextInputUnion]: + """ + Takes a list of dictionaries as input and returns a list with specific instances of a classes based on the value of the 'type' key in the dictionary. + + Arguments: + * `dicts`: A list of dictionaries that are expected to have a key called 'type' which specifies the type of the returned value. + """ + return list(map(deserialize_context_input, dicts)) diff --git a/bindings/python/iota_sdk/types/essence.py b/bindings/python/iota_sdk/types/essence.py index 530b45713f..16a3c0e34c 100644 --- a/bindings/python/iota_sdk/types/essence.py +++ b/bindings/python/iota_sdk/types/essence.py @@ -3,17 +3,15 @@ from __future__ import annotations from enum import IntEnum -from typing import TYPE_CHECKING, Optional, List, Union +from typing import TYPE_CHECKING, Optional, List from dataclasses import dataclass, field from iota_sdk.types.common import HexStr, json, SlotIndex from iota_sdk.types.mana import ManaAllotment -# TODO: Add missing output types in #1174 -# pylint: disable=no-name-in-module -from iota_sdk.types.output import BasicOutput, AccountOutput, FoundryOutput, NftOutput, DelegationOutput from iota_sdk.types.input import UtxoInput -from iota_sdk.types.context_input import CommitmentContextInput, BlockIssuanceCreditContextInput, RewardContextInput +from iota_sdk.types.context_input import ContextInputUnion +from iota_sdk.types.output import OutputUnion # Required to prevent circular import if TYPE_CHECKING: @@ -57,10 +55,8 @@ class RegularTransactionEssence(TransactionEssence): creation_slot: SlotIndex inputs: List[UtxoInput] inputs_commitment: HexStr - outputs: List[Union[BasicOutput, AccountOutput, - FoundryOutput, NftOutput, DelegationOutput]] - context_inputs: Optional[List[Union[CommitmentContextInput, - BlockIssuanceCreditContextInput, RewardContextInput]]] = None + outputs: List[OutputUnion] + context_inputs: Optional[List[ContextInputUnion]] = None allotments: Optional[List[ManaAllotment]] = None payload: Optional[Payload] = None type: int = field( diff --git a/bindings/python/iota_sdk/types/feature.py b/bindings/python/iota_sdk/types/feature.py index 4fc675e145..ab1cb11398 100644 --- a/bindings/python/iota_sdk/types/feature.py +++ b/bindings/python/iota_sdk/types/feature.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 from enum import IntEnum -from typing import List, Union +from typing import Dict, List, TypeAlias, Union, Any from dataclasses import dataclass, field - -from iota_sdk.types.address import Ed25519Address, AccountAddress, NFTAddress +from dataclasses_json import config +from iota_sdk.types.address import AddressUnion, deserialize_address from iota_sdk.types.common import EpochIndex, HexStr, json, SlotIndex @@ -43,7 +43,10 @@ class SenderFeature(Feature): Attributes: address: A given sender address. """ - address: Union[Ed25519Address, AccountAddress, NFTAddress] + address: AddressUnion = field( + metadata=config( + decoder=deserialize_address + )) type: int = field( default_factory=lambda: int( FeatureType.Sender), @@ -57,7 +60,10 @@ class IssuerFeature(Feature): Attributes: address: A given issuer address. """ - address: Union[Ed25519Address, AccountAddress, NFTAddress] + address: AddressUnion = field( + metadata=config( + decoder=deserialize_address + )) type: int = field( default_factory=lambda: int( FeatureType.Issuer), @@ -91,7 +97,7 @@ class TagFeature(Feature): @json @dataclass -class BlockIssuer(Feature): +class BlockIssuerFeature(Feature): """Contains the public keys to verify block signatures and allows for unbonding the issuer deposit. Attributes: expiry_slot: The slot index at which the Block Issuer Feature expires and can be removed. @@ -124,3 +130,40 @@ class StakingFeature(Feature): default_factory=lambda: int( FeatureType.Staking), init=False) + + +FeatureUnion: TypeAlias = Union[SenderFeature, IssuerFeature, + MetadataFeature, TagFeature, BlockIssuerFeature, StakingFeature] + + +def deserialize_feature(d: Dict[str, Any]) -> FeatureUnion: + """ + Takes a dictionary as input and returns an instance of a specific class based on the value of the 'type' key in the dictionary. + + Arguments: + * `d`: A dictionary that is expected to have a key called 'type' which specifies the type of the returned value. + """ + feature_type = d['type'] + if feature_type == FeatureType.Sender: + return SenderFeature.from_dict(d) + if feature_type == FeatureType.Issuer: + return IssuerFeature.from_dict(d) + if feature_type == FeatureType.Metadata: + return MetadataFeature.from_dict(d) + if feature_type == FeatureType.Tag: + return TagFeature.from_dict(d) + if feature_type == FeatureType.BlockIssuer: + return BlockIssuerFeature.from_dict(d) + if feature_type == FeatureType.Staking: + return StakingFeature.from_dict(d) + raise Exception(f'invalid feature type: {feature_type}') + + +def deserialize_features(dicts: List[Dict[str, Any]]) -> List[FeatureUnion]: + """ + Takes a list of dictionaries as input and returns a list with specific instances of a classes based on the value of the 'type' key in the dictionary. + + Arguments: + * `dicts`: A list of dictionaries that are expected to have a key called 'type' which specifies the type of the returned value. + """ + return list(map(deserialize_feature, dicts)) diff --git a/bindings/python/iota_sdk/types/output.py b/bindings/python/iota_sdk/types/output.py index 96d0970ec7..a42923d6fb 100644 --- a/bindings/python/iota_sdk/types/output.py +++ b/bindings/python/iota_sdk/types/output.py @@ -2,15 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations -from dataclasses import dataclass, field from enum import IntEnum -from typing import Dict, Optional, List, Union - +from typing import Dict, Optional, List, TypeAlias, Union, Any +from dataclasses import dataclass, field +from dataclasses_json import config from iota_sdk.types.common import HexStr, json -from iota_sdk.types.feature import SenderFeature, IssuerFeature, MetadataFeature, TagFeature +from iota_sdk.types.feature import deserialize_features, SenderFeature, IssuerFeature, MetadataFeature, TagFeature from iota_sdk.types.native_token import NativeToken from iota_sdk.types.token_scheme import SimpleTokenScheme -from iota_sdk.types.unlock_condition import AddressUnlockCondition, StorageDepositReturnUnlockCondition, TimelockUnlockCondition, ExpirationUnlockCondition, StateControllerAddressUnlockCondition, GovernorAddressUnlockCondition, ImmutableAccountAddressUnlockCondition +from iota_sdk.types.unlock_condition import deserialize_unlock_conditions, AddressUnlockCondition, StorageDepositReturnUnlockCondition, TimelockUnlockCondition, ExpirationUnlockCondition, StateControllerAddressUnlockCondition, GovernorAddressUnlockCondition, ImmutableAccountAddressUnlockCondition class OutputType(IntEnum): @@ -57,11 +57,16 @@ class BasicOutput(Output): """ amount: str mana: str - unlockConditions: List[Union[AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition, - TimelockUnlockCondition]] + unlock_conditions: List[Union[AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition, + TimelockUnlockCondition]] = field(metadata=config( + decoder=deserialize_unlock_conditions + )) features: Optional[List[Union[SenderFeature, - MetadataFeature, TagFeature]]] = None - nativeTokens: Optional[List[NativeToken]] = None + MetadataFeature, TagFeature]]] = field(default=None, + metadata=config( + decoder=deserialize_features + )) + native_tokens: Optional[List[NativeToken]] = None type: int = field( default_factory=lambda: int( OutputType.Basic), @@ -99,14 +104,23 @@ class AccountOutput(Output): amount: str mana: str account_id: HexStr - stateIndex: int + state_index: int foundry_counter: int unlock_conditions: List[Union[StateControllerAddressUnlockCondition, - GovernorAddressUnlockCondition]] + GovernorAddressUnlockCondition]] = field( + metadata=config( + decoder=deserialize_unlock_conditions + )) features: Optional[List[Union[SenderFeature, - MetadataFeature]]] = None + MetadataFeature]]] = field(default=None, + metadata=config( + decoder=deserialize_features + )) immutable_features: Optional[List[Union[IssuerFeature, - MetadataFeature]]] = None + MetadataFeature]]] = field(default=None, + metadata=config( + decoder=deserialize_features + )) state_metadata: Optional[HexStr] = None native_tokens: Optional[List[NativeToken]] = None type: int = field( @@ -141,8 +155,14 @@ class FoundryOutput(Output): serial_number: int token_scheme: SimpleTokenScheme unlock_conditions: List[ImmutableAccountAddressUnlockCondition] - features: Optional[List[MetadataFeature]] = None - immutable_features: Optional[List[MetadataFeature]] = None + features: Optional[List[MetadataFeature]] = field(default=None, + metadata=config( + decoder=deserialize_features + )) + immutable_features: Optional[List[MetadataFeature]] = field(default=None, + metadata=config( + decoder=deserialize_features + )) native_tokens: Optional[List[NativeToken]] = None type: int = field( default_factory=lambda: int( @@ -176,11 +196,20 @@ class NftOutput(Output): mana: str nft_id: HexStr unlock_conditions: List[Union[AddressUnlockCondition, ExpirationUnlockCondition, - StorageDepositReturnUnlockCondition, TimelockUnlockCondition]] + StorageDepositReturnUnlockCondition, TimelockUnlockCondition]] = field( + metadata=config( + decoder=deserialize_unlock_conditions + )) features: Optional[List[Union[SenderFeature, - MetadataFeature, TagFeature]]] = None + MetadataFeature, TagFeature]]] = field(default=None, + metadata=config( + decoder=deserialize_features + )) immutable_features: Optional[List[Union[ - IssuerFeature, MetadataFeature]]] = None + IssuerFeature, MetadataFeature]]] = field(default=None, + metadata=config( + decoder=deserialize_features + )) native_tokens: Optional[List[NativeToken]] = None type: int = field(default_factory=lambda: int(OutputType.Nft), init=False) @@ -194,93 +223,40 @@ class DelegationOutput(Output): The type of output. """ # TODO fields done in #1174 - type: int = field(default_factory=lambda: int(OutputType.Delegation), init=False) - - -@json -@dataclass -class OutputMetadata: - """Metadata about an output. - - Attributes: - block_id: The ID of the block in which the output appeared in. - transaction_id: The ID of the transaction in which the output was created. - output_index: The index of the output within the corresponding transaction. - is_spent: Whether the output is already spent. - milestone_index_booked: The index of the milestone which booked/created the output. - milestone_timestamp_booked: The timestamp of the milestone which booked/created the output. - ledger_index: The current ledger index. - milestone_index_spent: The index of the milestone which spent the output. - milestone_timestamp_spent: The timestamp of the milestone which spent the output. - transaction_id_spent: The ID of the transaction that spent the output. - """ - block_id: HexStr - transaction_id: HexStr - output_index: int - is_spent: bool - milestone_index_booked: int - milestone_timestamp_booked: int - ledger_index: int - milestone_index_spent: Optional[int] = None - milestone_timestamp_spent: Optional[int] = None - transaction_id_spent: Optional[HexStr] = None - - -@json -@dataclass -class OutputWithMetadata: - """An output with its metadata. - - Attributes: - metadata: The `OutputMetadata` object that belongs to `output`. - output: An `Output` object. - """ - - metadata: OutputMetadata - output: Union[AccountOutput, FoundryOutput, NftOutput, BasicOutput] - - @classmethod - def from_dict(cls, data_dict: Dict) -> OutputWithMetadata: - """Creates an output with metadata instance from the dict object. - """ - obj = cls.__new__(cls) - super(OutputWithMetadata, obj).__init__() - for k, v in data_dict.items(): - setattr(obj, k, v) - return obj - - def as_dict(self): - """Returns a dictionary representation of OutputWithMetadata, with the fields metadata and output. - """ - config = {} + type: int = field(default_factory=lambda: int( + OutputType.Delegation), init=False) - config['metadata'] = self.metadata.__dict__ - config['output'] = self.output.as_dict() - return config +OutputUnion: TypeAlias = Union[BasicOutput, AccountOutput, + FoundryOutput, NftOutput, DelegationOutput] -def output_from_dict( - output: Dict[str, any]) -> Union[BasicOutput, AccountOutput, FoundryOutput, NftOutput, Output]: +def deserialize_output(d: Dict[str, Any]) -> OutputUnion: """ - The function `output_from_dict` takes a dictionary as input and returns an instance of a specific - output class based on the value of the 'type' key in the dictionary. + Takes a dictionary as input and returns an instance of a specific class based on the value of the 'type' key in the dictionary. Arguments: - - * `output`: The `output` parameter is a dictionary that contains information about the output. It is - expected to have a key called 'type' which specifies the type of the output. The value of 'type' - should be one of the values defined in the `OutputType` enum. + * `d`: A dictionary that is expected to have a key called 'type' which specifies the type of the returned value. """ - output_type = OutputType(output['type']) - + output_type = dict['type'] if output_type == OutputType.Basic: - return BasicOutput.from_dict(output) + return BasicOutput.from_dict(d) if output_type == OutputType.Account: - return AccountOutput.from_dict(output) + return AccountOutput.from_dict(d) if output_type == OutputType.Foundry: - return FoundryOutput.from_dict(output) + return FoundryOutput.from_dict(d) if output_type == OutputType.Nft: - return NftOutput.from_dict(output) + return NftOutput.from_dict(d) + if output_type == OutputType.Delegation: + return DelegationOutput.from_dict(d) + raise Exception(f'invalid output type: {output_type}') - return Output.from_dict(output) + +def deserialize_outputs(dicts: List[Dict[str, Any]]) -> List[OutputUnion]: + """ + Takes a list of dictionaries as input and returns a list with specific instances of a classes based on the value of the 'type' key in the dictionary. + + Arguments: + * `dicts`: A list of dictionaries that are expected to have a key called 'type' which specifies the type of the returned value. + """ + return list(map(deserialize_output, dicts)) diff --git a/bindings/python/iota_sdk/types/output_data.py b/bindings/python/iota_sdk/types/output_data.py index 37bce51c0d..2c5fdeb428 100644 --- a/bindings/python/iota_sdk/types/output_data.py +++ b/bindings/python/iota_sdk/types/output_data.py @@ -3,10 +3,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, Union -from iota_sdk.types.address import Ed25519Address, AccountAddress, NFTAddress +from typing import Optional +from iota_sdk.types.address import AddressUnion from iota_sdk.types.common import HexStr, json -from iota_sdk.types.output import BasicOutput, AccountOutput, FoundryOutput, NftOutput, OutputMetadata +from iota_sdk.types.output import OutputUnion +from iota_sdk.types.output_metadata import OutputMetadata from iota_sdk.types.signature import Bip44 @@ -27,9 +28,9 @@ class OutputData(): """ output_id: HexStr metadata: OutputMetadata - output: Union[AccountOutput, FoundryOutput, NftOutput, BasicOutput] + output: OutputUnion is_spent: bool - address: Union[Ed25519Address, AccountAddress, NFTAddress] + address: AddressUnion network_id: str remainder: bool chain: Optional[Bip44] = None diff --git a/bindings/python/iota_sdk/types/output_metadata.py b/bindings/python/iota_sdk/types/output_metadata.py new file mode 100644 index 0000000000..22cb4ac88f --- /dev/null +++ b/bindings/python/iota_sdk/types/output_metadata.py @@ -0,0 +1,75 @@ +# Copyright 2023 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations +from typing import Dict, Optional, Union +from dataclasses import dataclass, field +from dataclasses_json import config +from iota_sdk.types.common import HexStr, json +from iota_sdk.types.output import AccountOutput, BasicOutput, DelegationOutput, FoundryOutput, NftOutput, deserialize_output + + +@json +@dataclass +class OutputMetadata: + """Metadata about an output. + + Attributes: + block_id: The ID of the block in which the output appeared in. + transaction_id: The ID of the transaction in which the output was created. + output_index: The index of the output within the corresponding transaction. + is_spent: Whether the output is already spent. + milestone_index_booked: The index of the milestone which booked/created the output. + milestone_timestamp_booked: The timestamp of the milestone which booked/created the output. + ledger_index: The current ledger index. + milestone_index_spent: The index of the milestone which spent the output. + milestone_timestamp_spent: The timestamp of the milestone which spent the output. + transaction_id_spent: The ID of the transaction that spent the output. + """ + block_id: HexStr + transaction_id: HexStr + output_index: int + is_spent: bool + milestone_index_booked: int + milestone_timestamp_booked: int + ledger_index: int + milestone_index_spent: Optional[int] = None + milestone_timestamp_spent: Optional[int] = None + transaction_id_spent: Optional[HexStr] = None + + +@json +@dataclass +class OutputWithMetadata: + """An output with its metadata. + + Attributes: + metadata: The `OutputMetadata` object that belongs to `output`. + output: An `Output` object. + """ + + metadata: OutputMetadata + output: Union[AccountOutput, FoundryOutput, + NftOutput, BasicOutput, DelegationOutput] = field(metadata=config( + decoder=deserialize_output + )) + + @classmethod + def from_dict(cls, data_dict: Dict) -> OutputWithMetadata: + """Creates an output with metadata instance from the dict object. + """ + obj = cls.__new__(cls) + super(OutputWithMetadata, obj).__init__() + for k, v in data_dict.items(): + setattr(obj, k, v) + return obj + + def as_dict(self): + """Returns a dictionary representation of OutputWithMetadata, with the fields metadata and output. + """ + d = {} + + d['metadata'] = self.metadata.__dict__ + d['output'] = self.output.as_dict() + + return d diff --git a/bindings/python/iota_sdk/types/payload.py b/bindings/python/iota_sdk/types/payload.py index 1db7518ee1..06eaa0c31c 100644 --- a/bindings/python/iota_sdk/types/payload.py +++ b/bindings/python/iota_sdk/types/payload.py @@ -3,10 +3,8 @@ from __future__ import annotations from enum import IntEnum -from typing import List, Union - +from typing import Any, Dict, List, TypeAlias, Union from dataclasses import dataclass, field - from iota_sdk.types.common import HexStr, json from iota_sdk.types.essence import RegularTransactionEssence from iota_sdk.types.unlock import SignatureUnlock, ReferenceUnlock @@ -63,3 +61,32 @@ class TransactionPayload(Payload): default_factory=lambda: int( PayloadType.Transaction), init=False) + + +PayloadUnion: TypeAlias = Union[TaggedDataPayload, TransactionPayload] + + +def deserialize_payload(d: Dict[str, Any]) -> PayloadUnion: + """ + Takes a dictionary as input and returns an instance of a specific class based on the value of the 'type' key in the dictionary. + + Arguments: + * `d`: A dictionary that is expected to have a key called 'type' which specifies the type of the returned value. + """ + payload_type = d['type'] + if payload_type == PayloadType.TaggedData: + return TaggedDataPayload.from_dict(d) + if payload_type == PayloadType.Transaction: + return TransactionPayload.from_dict(d) + raise Exception(f'invalid payload type: {payload_type}') + + +def deserialize_payloads( + dicts: List[Dict[str, Any]]) -> List[PayloadUnion]: + """ + Takes a list of dictionaries as input and returns a list with specific instances of a classes based on the value of the 'type' key in the dictionary. + + Arguments: + * `dicts`: A list of dictionaries that are expected to have a key called 'type' which specifies the type of the returned value. + """ + return list(map(deserialize_payload, dicts)) diff --git a/bindings/python/iota_sdk/types/transaction.py b/bindings/python/iota_sdk/types/transaction.py index 18497e4d01..7ae9266109 100644 --- a/bindings/python/iota_sdk/types/transaction.py +++ b/bindings/python/iota_sdk/types/transaction.py @@ -6,7 +6,7 @@ from typing import List, Optional from enum import Enum from iota_sdk.types.common import HexStr, json -from iota_sdk.types.output import OutputWithMetadata +from iota_sdk.types.output_metadata import OutputWithMetadata from iota_sdk.types.payload import TransactionPayload diff --git a/bindings/python/iota_sdk/types/transaction_data.py b/bindings/python/iota_sdk/types/transaction_data.py index ae071d5011..4d59d3b539 100644 --- a/bindings/python/iota_sdk/types/transaction_data.py +++ b/bindings/python/iota_sdk/types/transaction_data.py @@ -3,9 +3,10 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, List, Union -from iota_sdk.types.address import Ed25519Address, AccountAddress, NFTAddress -from iota_sdk.types.output import BasicOutput, AccountOutput, FoundryOutput, NftOutput, OutputMetadata +from typing import Optional, List +from iota_sdk.types.address import AddressUnion +from iota_sdk.types.output import OutputUnion +from iota_sdk.types.output_metadata import OutputMetadata from iota_sdk.types.essence import RegularTransactionEssence from iota_sdk.types.payload import TransactionPayload from iota_sdk.types.signature import Bip44 @@ -22,7 +23,7 @@ class InputSigningData: output_metadata: The output metadata. chain: The BIP44 chain for the address to unlock the output. """ - output: Union[AccountOutput, FoundryOutput, NftOutput, BasicOutput] + output: OutputUnion output_metadata: OutputMetadata chain: Optional[Bip44] = None @@ -37,8 +38,8 @@ class RemainderData: address: The remainder address. chain: The BIP44 chain for the remainder address. """ - output: Union[AccountOutput, FoundryOutput, NftOutput, BasicOutput] - address: Union[Ed25519Address, AccountAddress, NFTAddress] + output: OutputUnion + address: AddressUnion chain: Optional[Bip44] = None diff --git a/bindings/python/iota_sdk/types/unlock.py b/bindings/python/iota_sdk/types/unlock.py index 2ac60b9291..82e71e486e 100644 --- a/bindings/python/iota_sdk/types/unlock.py +++ b/bindings/python/iota_sdk/types/unlock.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import IntEnum +from typing import Dict, List, Union, Any from iota_sdk.types.signature import Ed25519Signature from iota_sdk.types.common import json @@ -74,3 +75,33 @@ class NftUnlock: """ reference: int type: int = field(default_factory=lambda: int(UnlockType.Nft), init=False) + + +def deserialize_unlock(d: Dict[str, Any]) -> Union[SignatureUnlock, ReferenceUnlock, AccountUnlock, NftUnlock]: + """ + Takes a dictionary as input and returns an instance of a specific class based on the value of the 'type' key in the dictionary. + + Arguments: + * `d`: A dictionary that is expected to have a key called 'type' which specifies the type of the returned value. + """ + unlock_type = d['type'] + if unlock_type == UnlockType.Signature: + return SignatureUnlock.from_dict(d) + if unlock_type == UnlockType.Reference: + return ReferenceUnlock.from_dict(d) + if unlock_type == UnlockType.Account: + return AccountUnlock.from_dict(d) + if unlock_type == UnlockType.Nft: + return NftUnlock.from_dict(d) + raise Exception(f'invalid unlock type: {unlock_type}') + + +def deserialize_unlocks(dicts: List[Dict[str, Any]]) -> List[Union[SignatureUnlock, + ReferenceUnlock, AccountUnlock, NftUnlock]]: + """ + Takes a list of dictionaries as input and returns a list with specific instances of a classes based on the value of the 'type' key in the dictionary. + + Arguments: + * `dicts`: A list of dictionaries that are expected to have a key called 'type' which specifies the type of the returned value. + """ + return list(map(deserialize_unlock, dicts)) diff --git a/bindings/python/iota_sdk/types/unlock_condition.py b/bindings/python/iota_sdk/types/unlock_condition.py index 535c3377ca..e13f118516 100644 --- a/bindings/python/iota_sdk/types/unlock_condition.py +++ b/bindings/python/iota_sdk/types/unlock_condition.py @@ -2,12 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 from enum import IntEnum - from dataclasses import dataclass, field -from typing import Union - -from iota_sdk.types.address import Ed25519Address, AccountAddress, NFTAddress +from typing import Dict, List, TypeAlias, Union, Any +from dataclasses_json import config +from iota_sdk.types.address import AddressUnion, AccountAddress from iota_sdk.types.common import json +from iota_sdk.types.address import deserialize_address class UnlockConditionType(IntEnum): @@ -39,6 +39,7 @@ class UnlockCondition(): type: int +@json @dataclass class AddressUnlockCondition(UnlockCondition): """An address unlock condition. @@ -46,7 +47,10 @@ class AddressUnlockCondition(UnlockCondition): Args: address: An address unlocked with a private key. """ - address: Union[Ed25519Address, AccountAddress, NFTAddress] + address: AddressUnion = field( + metadata=config( + decoder=deserialize_address + )) type: int = field( default_factory=lambda: int( UnlockConditionType.Address), @@ -62,7 +66,10 @@ class StorageDepositReturnUnlockCondition(UnlockCondition): return_address: The address to return the amount to. """ amount: str - return_address: Union[Ed25519Address, AccountAddress, NFTAddress] + return_address: AddressUnion = field( + metadata=config( + decoder=deserialize_address + )) type: int = field(default_factory=lambda: int( UnlockConditionType.StorageDepositReturn), init=False) @@ -90,7 +97,10 @@ class ExpirationUnlockCondition(UnlockCondition): return_address: The return address if the output was not claimed in time. """ unix_time: int - return_address: Union[Ed25519Address, AccountAddress, NFTAddress] + return_address: AddressUnion = field( + metadata=config( + decoder=deserialize_address + )) type: int = field( default_factory=lambda: int( UnlockConditionType.Expiration), @@ -104,7 +114,10 @@ class StateControllerAddressUnlockCondition(UnlockCondition): Args: address: The state controller address that owns the output. """ - address: Union[Ed25519Address, AccountAddress, NFTAddress] + address: AddressUnion = field( + metadata=config( + decoder=deserialize_address + )) type: int = field(default_factory=lambda: int( UnlockConditionType.StateControllerAddress), init=False) @@ -116,7 +129,10 @@ class GovernorAddressUnlockCondition(UnlockCondition): Args: address: The governor address that owns the output. """ - address: Union[Ed25519Address, AccountAddress, NFTAddress] + address: AddressUnion = field( + metadata=config( + decoder=deserialize_address + )) type: int = field(default_factory=lambda: int( UnlockConditionType.GovernorAddress), init=False) @@ -131,3 +147,43 @@ class ImmutableAccountAddressUnlockCondition(UnlockCondition): address: AccountAddress type: int = field(default_factory=lambda: int( UnlockConditionType.ImmutableAccountAddress), init=False) + + +UnlockConditionUnion: TypeAlias = Union[AddressUnlockCondition, StorageDepositReturnUnlockCondition, TimelockUnlockCondition, + ExpirationUnlockCondition, StateControllerAddressUnlockCondition, GovernorAddressUnlockCondition, ImmutableAccountAddressUnlockCondition] + + +def deserialize_unlock_condition(d: Dict[str, Any]) -> UnlockConditionUnion: + """ + Takes a dictionary as input and returns an instance of a specific class based on the value of the 'type' key in the dictionary. + + Arguments: + * `d`: A dictionary that is expected to have a key called 'type' which specifies the type of the returned value. + """ + # pylint: disable=too-many-return-statements + uc_type = d['type'] + if uc_type == UnlockConditionType.Address: + return AddressUnlockCondition.from_dict(d) + if uc_type == UnlockConditionType.StorageDepositReturn: + return StorageDepositReturnUnlockCondition.from_dict(d) + if uc_type == UnlockConditionType.Timelock: + return TimelockUnlockCondition.from_dict(d) + if uc_type == UnlockConditionType.Expiration: + return ExpirationUnlockCondition.from_dict(d) + if uc_type == UnlockConditionType.StateControllerAddress: + return StateControllerAddressUnlockCondition.from_dict(d) + if uc_type == UnlockConditionType.GovernorAddress: + return GovernorAddressUnlockCondition.from_dict(d) + if uc_type == UnlockConditionType.ImmutableAccountAddress: + return ImmutableAccountAddressUnlockCondition.from_dict(d) + raise Exception(f'invalid unlock condition type: {uc_type}') + + +def deserialize_unlock_conditions(dicts: List[Dict[str, Any]]) -> List[UnlockConditionUnion]: + """ + Takes a list of dictionaries as input and returns a list with specific instances of a classes based on the value of the 'type' key in the dictionary. + + Arguments: + * `dicts`: A list of dictionaries that are expected to have a key called 'type' which specifies the type of the returned value. + """ + return list(map(deserialize_unlock_condition, dicts)) diff --git a/bindings/python/iota_sdk/wallet/account.py b/bindings/python/iota_sdk/wallet/account.py index 74914fc3e9..e71ef7b837 100644 --- a/bindings/python/iota_sdk/wallet/account.py +++ b/bindings/python/iota_sdk/wallet/account.py @@ -16,7 +16,7 @@ from iota_sdk.types.native_token import NativeToken from iota_sdk.types.output_data import OutputData from iota_sdk.types.output_id import OutputId -from iota_sdk.types.output import BasicOutput, NftOutput, Output, output_from_dict +from iota_sdk.types.output import BasicOutput, NftOutput, Output, deserialize_output from iota_sdk.types.output_params import OutputParams from iota_sdk.types.transaction_data import PreparedTransactionData, SignedTransactionData from iota_sdk.types.send_params import CreateAccountOutputParams, CreateNativeTokenParams, MintNftParams, SendNativeTokensParams, SendNftParams, SendParams @@ -393,7 +393,7 @@ def prepare_output(self, params: OutputParams, When the assets contain an nft_id, the data from the existing nft output will be used, just with the address unlock conditions replaced """ - return output_from_dict(self._call_account_method( + return deserialize_output(self._call_account_method( 'prepareOutput', { 'params': params, 'transactionOptions': transaction_options diff --git a/bindings/python/setup.py b/bindings/python/setup.py index 70be8a1d84..ac8754097a 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -9,7 +9,7 @@ def get_py_version_cfgs(): # For now each Cfg Py_3_X flag is interpreted as "at least 3.X" version = sys.version_info[0:3] - py3_min = 9 + py3_min = 10 out_cfg = [] for minor in range(py3_min, version[1] + 1): out_cfg.append("--cfg=Py_3_%d" % minor) diff --git a/bindings/python/tests/test_output.py b/bindings/python/tests/test_output.py index 4398411b82..76c9f7aee1 100644 --- a/bindings/python/tests/test_output.py +++ b/bindings/python/tests/test_output.py @@ -28,6 +28,7 @@ def test_feature(): def test_output(): basic_output_dict = { "type": 3, + "mana": "999500700", "amount": "999500700", "unlockConditions": [ { @@ -44,6 +45,7 @@ def test_output(): basic_output_dict = { "type": 3, + "mana": "57600", "amount": "57600", "nativeTokens": [ { @@ -82,6 +84,7 @@ def test_output(): basic_output_dict = { "type": 3, + "mana": "50100", "amount": "50100", "nativeTokens": [ { @@ -108,6 +111,7 @@ def test_output(): account_output_dict = { "type": 4, + "mana": "168200", "amount": "168200", "accountId": "0x8d073d15074834785046d9cacec7ac4d672dcb6dad342624a936f3c4334520f1", "stateIndex": 4, @@ -144,6 +148,7 @@ def test_output(): account_output_dict = { "type": 4, + "mana": "55100", "amount": "55100", "accountId": "0x5380cce0ac342b8fa3e9c4f46d5b473ee9e824f0017fe43682dca77e6b875354", "stateIndex": 2, @@ -213,6 +218,7 @@ def test_output(): nft_output_dict = { "type": 6, + "mana": "47800", "amount": "47800", "nftId": "0x90e84936bd0cffd1595d2a58f63b1a8d0d3e333ed893950a5f3f0043c6e59ec1", "unlockConditions": [