diff --git a/bindings/python/examples/wallet/transaction_options.py b/bindings/python/examples/wallet/transaction_options.py new file mode 100644 index 0000000000..6d91e220bb --- /dev/null +++ b/bindings/python/examples/wallet/transaction_options.py @@ -0,0 +1,30 @@ +from iota_sdk import Wallet, TransactionOptions, TaggedDataPayload, utf8_to_hex, RemainderValueStrategy +from dotenv import load_dotenv +import os + +load_dotenv() + +# This example sends a transaction with a tagged data payload. + +wallet = Wallet('./alice-database') + +account = wallet.get_account('Alice') + +# Sync account with the node +response = account.sync() + +if 'STRONGHOLD_PASSWORD' not in os.environ: + raise Exception(".env STRONGHOLD_PASSWORD is undefined, see .env.example") + +wallet.set_stronghold_password(os.environ["STRONGHOLD_PASSWORD"]) + +outputs = [{ + "address": "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu", + "amount": "1000000", +}] + +transaction = account.send_amount(outputs, TransactionOptions(remainder_value_strategy=RemainderValueStrategy.ReuseAddress, + note="my first tx", tagged_data_payload=TaggedDataPayload(utf8_to_hex("tag"), utf8_to_hex("data")))) +print(transaction) +print( + f'Block sent: {os.environ["EXPLORER_URL"]}/block/{transaction["blockId"]}') diff --git a/bindings/python/iota_sdk/__init__.py b/bindings/python/iota_sdk/__init__.py index 610afe11d4..aa815b24bb 100644 --- a/bindings/python/iota_sdk/__init__.py +++ b/bindings/python/iota_sdk/__init__.py @@ -14,5 +14,7 @@ from .types.feature import * from .types.native_token import * from .types.output_id import * +from .types.payload import * from .types.token_scheme import * +from .types.transaction_options import * from .types.unlock_condition import * diff --git a/bindings/python/iota_sdk/types/payload.py b/bindings/python/iota_sdk/types/payload.py new file mode 100644 index 0000000000..6b5c5634c0 --- /dev/null +++ b/bindings/python/iota_sdk/types/payload.py @@ -0,0 +1,67 @@ +# Copyright 2023 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +from iota_sdk.types.common import HexStr +from enum import Enum +from typing import Any, Optional + + +class PayloadType(Enum): + TreasuryTransaction = 4 + TaggedData = 5 + Transaction = 6 + Milestone = 7 + + +class Payload(): + def __init__(self, type, milestone: Optional[Any] = None, tagged_data=None, transaction=None, treasury_transaction: Optional[Any] = None): + """Initialize a payload + """ + self.type = type + self.milestone = milestone + self.tagged_data = tagged_data + self.transaction = transaction + self.treasury_transaction = treasury_transaction + + def as_dict(self): + config = {k: v for k, v in self.__dict__.items() if v != None} + + if "milestone" in config: + del config["milestone"] + if "tagged_data" in config: + del config["tagged_data"] + if "transaction" in config: + del config["transaction"] + if "treasury_transaction" in config: + del config["treasury_transaction"] + + config['type'] = config['type'].value + + return config + + +class MilestonePayload(Payload): + def __init__(self, essence, signatures): + """Initialize a MilestonePayload + """ + self.essence = essence + self.signatures = signatures + super().__init__(PayloadType.Milestone, milestone=self) + + +class TaggedDataPayload(Payload): + def __init__(self, tag: HexStr, data: HexStr): + """Initialize a TaggedDataPayload + """ + self.tag = tag + self.data = data + super().__init__(PayloadType.TaggedData, tagged_data=self) + + +class TransactionPayload(Payload): + def __init__(self, essence, unlocks): + """Initialize a TransactionPayload + """ + self.essence = essence + self.unlocks = unlocks + super().__init__(PayloadType.Transaction, transaction=self) diff --git a/bindings/python/iota_sdk/types/transaction_options.py b/bindings/python/iota_sdk/types/transaction_options.py new file mode 100644 index 0000000000..bf8b72f9c5 --- /dev/null +++ b/bindings/python/iota_sdk/types/transaction_options.py @@ -0,0 +1,53 @@ +# Copyright 2023 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +from iota_sdk.types.burn import Burn +from iota_sdk.types.output_id import OutputId +from iota_sdk.types.payload import TaggedDataPayload +from enum import Enum +from typing import Optional, List + + +class RemainderValueStrategyCustomAddress: + def __init__(self, + address: str, + key_index: int, + internal: bool, + used: bool): + self.address = address + self.keyIndex = key_index + self.internal = internal + self.used = used + + def as_dict(self): + return dict({"strategy": "CustomAddress", "value": self.__dict__}) + + +class RemainderValueStrategy(Enum): + ChangeAddress = None, + ReuseAddress = None, + + def as_dict(self): + return dict({"strategy": self.name, "value": self.value[0]}) + + +class TransactionOptions(): + def __init__(self, remainder_value_strategy: Optional[RemainderValueStrategy | RemainderValueStrategyCustomAddress] = None, + tagged_data_payload: Optional[TaggedDataPayload] = None, + custom_inputs: Optional[List[OutputId]] = None, + mandatory_inputs: Optional[List[OutputId]] = None, + burn: Optional[Burn] = None, + note: Optional[str] = None, + allow_micro_amount: Optional[bool] = None): + """Initialize TransactionOptions + """ + self.remainder_value_strategy = remainder_value_strategy + self.tagged_data_payload = tagged_data_payload + self.custom_inputs = custom_inputs + self.mandatory_inputs = mandatory_inputs + self.burn = burn + self.note = note + self.allow_micro_amount = allow_micro_amount + + def as_dict(self): + return dict(self.__dict__) diff --git a/bindings/python/iota_sdk/wallet/account.py b/bindings/python/iota_sdk/wallet/account.py index 1cd582366e..c9f25dce4b 100644 --- a/bindings/python/iota_sdk/wallet/account.py +++ b/bindings/python/iota_sdk/wallet/account.py @@ -7,6 +7,7 @@ from iota_sdk.types.common import HexStr from iota_sdk.types.native_token import NativeToken from iota_sdk.types.output_id import OutputId +from iota_sdk.types.transaction_options import TransactionOptions from typing import List, Optional @@ -41,7 +42,7 @@ def _call_account_method(self, method, data=None): return message - def prepare_burn(self, burn: Burn, options=None): + def prepare_burn(self, burn: Burn, options: Optional[TransactionOptions] = None): """ A generic `prepare_burn()` function that can be used to prepare the burn of native tokens, nfts, foundries and aliases. """ @@ -54,9 +55,9 @@ def prepare_burn(self, burn: Burn, options=None): return PreparedTransactionData(self, prepared) def prepare_burn_native_token(self, - token_id: HexStr, - burn_amount: int, - options=None): + token_id: HexStr, + burn_amount: int, + options: Optional[TransactionOptions] = None): """Burn native tokens. This doesn't require the foundry output which minted them, but will not increase the foundries `melted_tokens` field, which makes it impossible to destroy the foundry output. Therefore it's recommended to use melting, if the foundry output is available. @@ -70,8 +71,8 @@ def prepare_burn_native_token(self, return PreparedTransactionData(self, prepared) def prepare_burn_nft(self, - nft_id: HexStr, - options=None): + nft_id: HexStr, + options: Optional[TransactionOptions] = None): """Burn an nft output. """ prepared = self._call_account_method( @@ -83,8 +84,8 @@ def prepare_burn_nft(self, return PreparedTransactionData(self, prepared) def prepare_consolidate_outputs(self, - force: bool, - output_consolidation_threshold: Optional[int] = None): + force: bool, + output_consolidation_threshold: Optional[int] = None): """Consolidate outputs. """ prepared = self._call_account_method( @@ -96,8 +97,8 @@ def prepare_consolidate_outputs(self, return PreparedTransactionData(self, prepared) def prepare_create_alias_output(self, - params, - options): + params, + options: Optional[TransactionOptions] = None): """Create an alias output. """ prepared = self._call_account_method( @@ -109,8 +110,8 @@ def prepare_create_alias_output(self, return PreparedTransactionData(self, prepared) def prepare_destroy_alias(self, - alias_id: HexStr, - options=None): + alias_id: HexStr, + options: Optional[TransactionOptions] = None): """Destroy an alias output. """ @@ -123,8 +124,8 @@ def prepare_destroy_alias(self, return PreparedTransactionData(self, prepared) def prepare_destroy_foundry(self, - foundry_id: HexStr, - options=None): + foundry_id: HexStr, + options: Optional[TransactionOptions] = None): """Destroy a foundry output with a circulating supply of 0. """ prepared = self._call_account_method( @@ -226,9 +227,9 @@ def pending_transactions(self): ) def prepare_decrease_native_token_supply(self, - token_id: HexStr, - melt_amount: int, - options=None): + token_id: HexStr, + melt_amount: int, + options: Optional[TransactionOptions] = None): """Melt native tokens. This happens with the foundry output which minted them, by increasing it's `melted_tokens` field. """ @@ -241,7 +242,7 @@ def prepare_decrease_native_token_supply(self, ) return PreparedTransactionData(self, prepared) - def prepare_increase_native_token_supply(self, token_id: HexStr, mint_amount: int, options=None): + def prepare_increase_native_token_supply(self, token_id: HexStr, mint_amount: int, options: Optional[TransactionOptions] = None): """Mint more native token. """ prepared = self._call_account_method( @@ -253,7 +254,7 @@ def prepare_increase_native_token_supply(self, token_id: HexStr, mint_amount: in ) return PreparedMintTokenTransaction(account=self, prepared_transaction_data=prepared) - def prepare_mint_native_token(self, params, options=None): + def prepare_mint_native_token(self, params, options: Optional[TransactionOptions] = None): """Mint native token. """ prepared = self._call_account_method( @@ -273,7 +274,7 @@ def minimum_required_storage_deposit(self, output): } ) - def prepare_mint_nfts(self, params, options=None): + def prepare_mint_nfts(self, params, options: Optional[TransactionOptions] = None): """Mint nfts. """ prepared = self._call_account_method( @@ -291,7 +292,7 @@ def get_balance(self): 'getBalance' ) - def prepare_output(self, output_options, transaction_options=None): + def prepare_output(self, output_options, transaction_options: Optional[TransactionOptions] = None): """Prepare an output for sending If the amount is below the minimum required storage deposit, by default the remaining amount will automatically be added with a StorageDepositReturn UnlockCondition, when setting the ReturnStrategy to `gift`, the full @@ -306,7 +307,7 @@ def prepare_output(self, output_options, transaction_options=None): } ) - def prepare_send_amount(self, params, options=None): + def prepare_send_amount(self, params, options: Optional[TransactionOptions] = None): """Prepare send amount. """ prepared = self._call_account_method( @@ -317,7 +318,7 @@ def prepare_send_amount(self, params, options=None): ) return PreparedTransactionData(self, prepared) - def prepare_transaction(self, outputs, options=None): + def prepare_transaction(self, outputs, options: Optional[TransactionOptions] = None): """Prepare transaction. """ prepared = self._call_account_method( @@ -351,7 +352,7 @@ def sync(self, options=None): } ) - def send_amount(self, params, options=None): + def send_amount(self, params, options: Optional[TransactionOptions] = None): """Send amount. """ return self._call_account_method( @@ -361,7 +362,7 @@ def send_amount(self, params, options=None): } ) - def prepare_send_native_tokens(self, params, options=None): + def prepare_send_native_tokens(self, params, options: Optional[TransactionOptions] = None): """Send native tokens. """ prepared = self._call_account_method( @@ -372,7 +373,7 @@ def prepare_send_native_tokens(self, params, options=None): ) return PreparedTransactionData(self, prepared) - def prepare_send_nft(self, params, options=None): + def prepare_send_nft(self, params, options: Optional[TransactionOptions] = None): """Send nft. """ prepared = self._call_account_method( @@ -438,7 +439,7 @@ def claim_outputs(self, output_ids_to_claim: List[OutputId]): } ) - def send_outputs(self, outputs, options=None): + def send_outputs(self, outputs, options: Optional[TransactionOptions] = None): """Send outputs in a transaction. """ return self._call_account_method( diff --git a/bindings/python/iota_sdk/wallet/common.py b/bindings/python/iota_sdk/wallet/common.py index 0a7d5b45bf..d334172554 100644 --- a/bindings/python/iota_sdk/wallet/common.py +++ b/bindings/python/iota_sdk/wallet/common.py @@ -2,17 +2,53 @@ # SPDX-License-Identifier: Apache-2.0 from iota_sdk import call_wallet_method +import humps import json -from json import dumps +from json import dumps, JSONEncoder +from enum import Enum def _call_method_routine(func): """The routine of dump json string and call call_wallet_method() """ def wrapper(*args, **kwargs): + class MyEncoder(JSONEncoder): + def default(self, obj): + as_dict_method = getattr(obj, "as_dict", None) + if callable(as_dict_method): + return obj.as_dict() + if isinstance(obj, str): + return obj + if isinstance(obj, Enum): + return obj.__dict__ + if isinstance(obj, dict): + return obj + if hasattr(obj, "__dict__"): + obj_dict = obj.__dict__ + + items_method = getattr(self, "items", None) + if callable(items_method): + for k, v in obj_dict.items(): + obj_dict[k] = dumps(v, cls=MyEncoder) + return obj_dict + return obj_dict + return obj message = func(*args, **kwargs) - message = dumps(message) + message = dumps(list(message.values()), cls=MyEncoder) + deserialized = json.loads(message) + + def remove_none(obj): + if isinstance(obj, (list, tuple, set)): + return type(obj)(remove_none(x) for x in obj if x is not None) + elif isinstance(obj, dict): + return type(obj)((remove_none(k), remove_none(v)) + for k, v in obj.items() if k is not None and v is not None) + else: + return obj + deserialized_null_filtered = remove_none(deserialized) + + message = dumps(humps.camelize(deserialized_null_filtered)) # Send message to the Rust library response = call_wallet_method(args[0].handle, message)