diff --git a/.circleci/config.yml b/.circleci/config.yml index e0e2bfdb..0a42817e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,6 +25,7 @@ jobs: - checkout - run: pip install -r requirements.txt - run: black --check . + - run: mypy algosdk - run: pytest tests/unit_tests integration-test: parameters: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a177e5d..6d1ae2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +# v2.0.0 + +## What's Changed +### Breaking Changes + +* Remove v1 algod API (`algosdk/algod.py`) due to API end-of-life (2022-12-01). Instead, use v2 algod API (`algosdk/v2client/algod.py`). +* Remove `algosdk.future` package. Move package contents to `algosdk`. +* Remove `encoding.future_msgpack_decode` method in favor of `encoding.msgpack_decode` method. +* Remove `cost` field in `DryrunTxnResult` in favor of 2 fields: `budget-added` and `budget-consumed`. `cost` can be derived by `budget-consumed - budget-added`. +* Remove `mnemonic.to_public_key` in favor of `account.address_from_private_key`. +* Remove logicsig templates, `algosdk/data/langspec.json` and all methods in `logic` depending on it. + +### Bugfixes +* Fix: populate_foreign_array offset logic by @jgomezst in https://github.com/algorand/py-algorand-sdk/pull/406 + +### Enhancements +* v2: Breaking changes from v1 to v2.0.0 by @ahangsu in https://github.com/algorand/py-algorand-sdk/pull/415 +* v2: Delete more references to `langspec` by @algochoi in https://github.com/algorand/py-algorand-sdk/pull/426 +* LogicSig: Add LogicSig usage disclaimer by @michaeldiamant in https://github.com/algorand/py-algorand-sdk/pull/424 +* Infrastructure: Only package `algosdk` in `setup.py` by @algochoi in https://github.com/algorand/py-algorand-sdk/pull/428 +* Tests: Introduce type linting with mypy by @jdtzmn in https://github.com/algorand/py-algorand-sdk/pull/397 + + # v1.20.2 ## What's Changed diff --git a/MANIFEST.in b/MANIFEST.in index 8bc13cdb..98f06925 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ -include algosdk/data/langspec.json +global-include *.pyi +global-include *.typed diff --git a/README.md b/README.md index 9a14cc06..3fe5ab75 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ Format code: * `black .` +Lint types: + +* `mypy algosdk` + ## Quick start Here's a simple example you can run without a node. diff --git a/algosdk/__init__.py b/algosdk/__init__.py index 44dfc011..88cb78b7 100644 --- a/algosdk/__init__.py +++ b/algosdk/__init__.py @@ -1,16 +1,13 @@ from . import abi from . import account -from . import algod from . import auction from . import constants from . import dryrun_results from . import encoding from . import error -from . import future from . import kmd from . import logic from . import mnemonic -from . import template from . import transaction from . import util from . import v2client diff --git a/algosdk/abi/address_type.py b/algosdk/abi/address_type.py index 57050c71..55fd9c85 100644 --- a/algosdk/abi/address_type.py +++ b/algosdk/abi/address_type.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, cast from algosdk.abi.base_type import ABIType from algosdk.abi.byte_type import ByteType @@ -53,15 +53,16 @@ def encode(self, value: Union[str, bytes]) -> bytes: value = encoding.decode_address(value) except Exception as e: raise error.ABIEncodingError( - "cannot encode the following address: {}".format(value) + f"cannot encode the following address: {value!r}" ) from e elif ( not (isinstance(value, bytes) or isinstance(value, bytearray)) or len(value) != 32 ): raise error.ABIEncodingError( - "cannot encode the following public key: {}".format(value) + f"cannot encode the following public key: {value!r}" ) + value = cast(bytes, value) return bytes(value) def decode(self, bytestring: Union[bytearray, bytes]) -> str: @@ -82,9 +83,7 @@ def decode(self, bytestring: Union[bytearray, bytes]) -> str: or len(bytestring) != 32 ): raise error.ABIEncodingError( - "address string must be in bytes and correspond to a byte[32]: {}".format( - bytestring - ) + f"address string must be in bytes and correspond to a byte[32]: {bytestring!r}" ) # Return the base32 encoded address string return encoding.encode_address(bytestring) diff --git a/algosdk/abi/array_dynamic_type.py b/algosdk/abi/array_dynamic_type.py index 97be5b15..ef8adff6 100644 --- a/algosdk/abi/array_dynamic_type.py +++ b/algosdk/abi/array_dynamic_type.py @@ -58,9 +58,7 @@ def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes: or isinstance(value_array, bytearray) ) and not isinstance(self.child_type, ByteType): raise error.ABIEncodingError( - "cannot pass in bytes when the type of the array is not ByteType: {}".format( - value_array - ) + f"cannot pass in bytes when the type of the array is not ByteType: {value_array!r}" ) converted_tuple = self._to_tuple_type(len(value_array)) length_to_encode = len(converted_tuple.child_types).to_bytes( diff --git a/algosdk/abi/array_static_type.py b/algosdk/abi/array_static_type.py index 059de3e7..22fe7ce1 100644 --- a/algosdk/abi/array_static_type.py +++ b/algosdk/abi/array_static_type.py @@ -81,9 +81,7 @@ def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes: or isinstance(value_array, bytearray) ) and not isinstance(self.child_type, ByteType): raise error.ABIEncodingError( - "cannot pass in bytes when the type of the array is not ByteType: {}".format( - value_array - ) + f"cannot pass in bytes when the type of the array is not ByteType: {value_array!r}" ) converted_tuple = self._to_tuple_type() return converted_tuple.encode(value_array) diff --git a/algosdk/abi/base_type.py b/algosdk/abi/base_type.py index 0e6ac628..7b5021ad 100644 --- a/algosdk/abi/base_type.py +++ b/algosdk/abi/base_type.py @@ -77,8 +77,8 @@ def from_string(s: str) -> "ABIType": elif s.endswith("]"): matches = re.search(STATIC_ARRAY_REGEX, s) try: - static_length = int(matches.group(2)) - array_type = ABIType.from_string(matches.group(1)) + static_length = int(matches.group(2)) # type: ignore[union-attr] # we allow attribute errors to be caught + array_type = ABIType.from_string(matches.group(1)) # type: ignore[union-attr] # we allow attribute errors to be caught return ArrayStaticType(array_type, static_length) except Exception as e: raise error.ABITypeError( @@ -103,8 +103,8 @@ def from_string(s: str) -> "ABIType": elif s.startswith("ufixed"): matches = re.search(UFIXED_REGEX, s) try: - bit_size = int(matches.group(1)) - precision = int(matches.group(2)) + bit_size = int(matches.group(1)) # type: ignore[union-attr] # we allow attribute errors to be caught + precision = int(matches.group(2)) # type: ignore[union-attr] # we allow attribute errors to be caught return UfixedType(bit_size, precision) except Exception as e: raise error.ABITypeError( @@ -124,9 +124,6 @@ def from_string(s: str) -> "ABIType": if isinstance(tup, str): tt = ABIType.from_string(tup) tuple_list.append(tt) - elif isinstance(tup, list): - tts = [ABIType.from_string(t_) for t_ in tup] - tuple_list.append(tts) else: raise error.ABITypeError( "cannot convert {} to an ABI type".format(tup) diff --git a/algosdk/abi/bool_type.py b/algosdk/abi/bool_type.py index ee73ae5f..28e3661d 100644 --- a/algosdk/abi/bool_type.py +++ b/algosdk/abi/bool_type.py @@ -60,9 +60,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bool: or len(bytestring) != 1 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a bool: {}".format( - bytestring - ) + f"value string must be in bytes and correspond to a bool: {bytestring!r}" ) if bytestring == b"\x80": return True @@ -70,5 +68,5 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bool: return False else: raise error.ABIEncodingError( - "boolean value could not be decoded: {}".format(bytestring) + f"boolean value could not be decoded: {bytestring!r}" ) diff --git a/algosdk/abi/byte_type.py b/algosdk/abi/byte_type.py index e719e80e..ccaae47c 100644 --- a/algosdk/abi/byte_type.py +++ b/algosdk/abi/byte_type.py @@ -42,7 +42,7 @@ def encode(self, value: int) -> bytes: ) return bytes([value]) - def decode(self, bytestring: Union[bytes, bytearray]) -> bytes: + def decode(self, bytestring: Union[bytes, bytearray]) -> int: """ Decodes a bytestring to a single byte. @@ -50,7 +50,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bytes: bytestring (bytes | bytearray): bytestring to be decoded Returns: - bytes: byte of the encoded bytestring + int: byte value of the encoded bytestring """ if ( not ( @@ -60,8 +60,6 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bytes: or len(bytestring) != 1 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a byte: {}".format( - bytestring - ) + f"value string must be in bytes and correspond to a byte: {bytestring!r}" ) return bytestring[0] diff --git a/algosdk/abi/contract.py b/algosdk/abi/contract.py index cfeb7b11..9a728c54 100644 --- a/algosdk/abi/contract.py +++ b/algosdk/abi/contract.py @@ -1,7 +1,22 @@ import json -from typing import Dict, List, Union +from typing import Dict, List, Union, Optional, TypedDict -from algosdk.abi.method import Method, get_method_by_name +from algosdk.abi.method import Method, MethodDict, get_method_by_name + + +class NetworkInfoDict(TypedDict): + appID: int + + +# In Python 3.11+ the following classes should be combined using `NotRequired` +class ContractDict_Optional(TypedDict, total=False): + desc: str + + +class ContractDict(ContractDict_Optional): + name: str + methods: List[MethodDict] + networks: Dict[str, NetworkInfoDict] class Contract: @@ -20,8 +35,8 @@ def __init__( self, name: str, methods: List[Method], - desc: str = None, - networks: Dict[str, "NetworkInfo"] = None, + desc: Optional[str] = None, + networks: Optional[Dict[str, "NetworkInfo"]] = None, ) -> None: self.name = name self.methods = methods @@ -43,11 +58,12 @@ def from_json(resp: Union[str, bytes, bytearray]) -> "Contract": d = json.loads(resp) return Contract.undictify(d) - def dictify(self) -> dict: - d = {} - d["name"] = self.name - d["methods"] = [m.dictify() for m in self.methods] - d["networks"] = {k: v.dictify() for k, v in self.networks.items()} + def dictify(self) -> ContractDict: + d: ContractDict = { + "name": self.name, + "methods": [m.dictify() for m in self.methods], + "networks": {k: v.dictify() for k, v in self.networks.items()}, + } if self.desc is not None: d["desc"] = self.desc return d @@ -84,7 +100,7 @@ def __eq__(self, o: object) -> bool: return False return self.app_id == o.app_id - def dictify(self) -> dict: + def dictify(self) -> NetworkInfoDict: return {"appID": self.app_id} @staticmethod diff --git a/algosdk/abi/interface.py b/algosdk/abi/interface.py index 5bd79aa4..aaf07655 100644 --- a/algosdk/abi/interface.py +++ b/algosdk/abi/interface.py @@ -1,7 +1,16 @@ import json -from typing import List, Union +from typing import List, Union, Optional, TypedDict -from algosdk.abi.method import Method, get_method_by_name +from algosdk.abi.method import Method, MethodDict, get_method_by_name + +# In Python 3.11+ the following classes should be combined using `NotRequired` +class InterfaceDict_Optional(TypedDict, total=False): + desc: str + + +class InterfaceDict(InterfaceDict_Optional): + name: str + methods: List[MethodDict] class Interface: @@ -15,7 +24,7 @@ class Interface: """ def __init__( - self, name: str, methods: List[Method], desc: str = None + self, name: str, methods: List[Method], desc: Optional[str] = None ) -> None: self.name = name self.methods = methods @@ -35,10 +44,11 @@ def from_json(resp: Union[str, bytes, bytearray]) -> "Interface": d = json.loads(resp) return Interface.undictify(d) - def dictify(self) -> dict: - d = {} - d["name"] = self.name - d["methods"] = [m.dictify() for m in self.methods] + def dictify(self) -> InterfaceDict: + d: InterfaceDict = { + "name": self.name, + "methods": [m.dictify() for m in self.methods], + } if self.desc: d["desc"] = self.desc return d diff --git a/algosdk/abi/method.py b/algosdk/abi/method.py index 08397de0..3bd21842 100644 --- a/algosdk/abi/method.py +++ b/algosdk/abi/method.py @@ -1,10 +1,20 @@ import json -from typing import List, Union +from typing import List, Union, Optional, TypedDict from Cryptodome.Hash import SHA512 from algosdk import abi, constants, error +# In Python 3.11+ the following classes should be combined using `NotRequired` +class MethodDict_Optional(TypedDict, total=False): + desc: str + + +class MethodDict(MethodDict_Optional): + name: str + args: List[dict] + returns: dict + class Method: """ @@ -23,7 +33,7 @@ def __init__( name: str, args: List["Argument"], returns: "Returns", - desc: str = None, + desc: Optional[str] = None, ) -> None: self.name = name self.args = args @@ -108,11 +118,12 @@ def from_signature(s: str) -> "Method": return_type = Returns(tokens[-1]) return Method(name=tokens[0], args=argument_list, returns=return_type) - def dictify(self) -> dict: - d = {} - d["name"] = self.name - d["args"] = [arg.dictify() for arg in self.args] - d["returns"] = self.returns.dictify() + def dictify(self) -> MethodDict: + d: MethodDict = { + "name": self.name, + "args": [arg.dictify() for arg in self.args], + "returns": self.returns.dictify(), + } if self.desc: d["desc"] = self.desc return d @@ -156,12 +167,15 @@ class Argument: """ def __init__( - self, arg_type: str, name: str = None, desc: str = None + self, + arg_type: str, + name: Optional[str] = None, + desc: Optional[str] = None, ) -> None: if abi.is_abi_transaction_type(arg_type) or abi.is_abi_reference_type( arg_type ): - self.type = arg_type + self.type: Union[str, abi.ABIType] = arg_type else: # If the type cannot be parsed into an ABI type, it will error self.type = abi.ABIType.from_string(arg_type) @@ -208,9 +222,9 @@ class Returns: # Represents a void return. VOID = "void" - def __init__(self, arg_type: str, desc: str = None) -> None: + def __init__(self, arg_type: str, desc: Optional[str] = None) -> None: if arg_type == "void": - self.type = self.VOID + self.type: Union[str, abi.ABIType] = self.VOID else: # If the type cannot be parsed into an ABI type, it will error. self.type = abi.ABIType.from_string(arg_type) diff --git a/algosdk/abi/transaction.py b/algosdk/abi/transaction.py index 02c91921..e863095a 100644 --- a/algosdk/abi/transaction.py +++ b/algosdk/abi/transaction.py @@ -1,7 +1,7 @@ from typing import Any from algosdk import constants -from algosdk.future.transaction import Transaction +from algosdk.transaction import Transaction class ABITransactionType: diff --git a/algosdk/abi/tuple_type.py b/algosdk/abi/tuple_type.py index baa0dda6..62fecb46 100644 --- a/algosdk/abi/tuple_type.py +++ b/algosdk/abi/tuple_type.py @@ -1,4 +1,4 @@ -from typing import Any, List, Union +from typing import Any, List, Union, Optional, cast from algosdk.abi.base_type import ABI_LENGTH_SIZE, ABIType from algosdk.abi.bool_type import BoolType @@ -73,7 +73,7 @@ def _find_bool(type_list: List[ABIType], index: int, delta: int) -> int: return until @staticmethod - def _parse_tuple(s: str) -> list: + def _parse_tuple(s: str) -> List[str]: """ Given a tuple string, parses one layer of the tuple and returns tokens as a list. i.e. 'x,(y,(z))' -> ['x', '(y,(z))'] @@ -92,7 +92,7 @@ def _parse_tuple(s: str) -> list: "cannot have consecutive commas in {}".format(s) ) - tuple_strs = [] + tuple_strs: List[str] = [] depth = 0 word = "" for char in s: @@ -175,8 +175,11 @@ def encode(self, values: Union[List[Any], bytes, bytearray]) -> bytes: "expected before index should have number of bool mod 8 equal 0" ) after = min(7, after) + consecutive_bool_list = cast( + List[bool], values[i : i + after + 1] + ) compressed_int = TupleType._compress_multiple_bool( - values[i : i + after + 1] + consecutive_bool_list ) heads.append(bytes([compressed_int])) i += after @@ -229,10 +232,10 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: "value string must be in bytes: {}".format(bytestring) ) tuple_elements = self.child_types - dynamic_segments = ( - list() - ) # Store the start and end of a dynamic element - value_partitions = list() + dynamic_segments: List[ + List[int] + ] = list() # Store the start and end of a dynamic element + value_partitions: List[Optional[Union[bytes, bytearray]]] = list() i = 0 array_index = 0 @@ -291,9 +294,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: array_index += curr_len if array_index >= len(bytestring) and i != len(tuple_elements) - 1: raise error.ABIEncodingError( - "input string is not long enough to be decoded: {}".format( - bytestring - ) + f"input string is not long enough to be decoded: {bytestring!r}" ) i += 1 @@ -302,7 +303,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: array_index = len(bytestring) if array_index < len(bytestring): raise error.ABIEncodingError( - "input string was not fully consumed: {}".format(bytestring) + f"input string was not fully consumed: {bytestring!r}" ) # Check dynamic element partitions diff --git a/algosdk/abi/ufixed_type.py b/algosdk/abi/ufixed_type.py index 6ab2c778..708dce27 100644 --- a/algosdk/abi/ufixed_type.py +++ b/algosdk/abi/ufixed_type.py @@ -97,9 +97,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> int: or len(bytestring) != self.bit_size // 8 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a ufixed{}x{}: {}".format( - self.bit_size, self.precision, bytestring - ) + f"value string must be in bytes and correspond to a ufixed{self.bit_size}x{self.precision}: {bytestring!r}" ) # Convert bytes into an unsigned integer numerator return int.from_bytes(bytestring, byteorder="big", signed=False) diff --git a/algosdk/abi/uint_type.py b/algosdk/abi/uint_type.py index 7e10c2ca..aaf65643 100644 --- a/algosdk/abi/uint_type.py +++ b/algosdk/abi/uint_type.py @@ -82,9 +82,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> int: or len(bytestring) != self.bit_size // 8 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a uint{}: {}".format( - self.bit_size, bytestring - ) + f"value string must be in bytes and correspond to a uint{self.bit_size}: {bytestring!r}" ) # Convert bytes into an unsigned integer return int.from_bytes(bytestring, byteorder="big", signed=False) diff --git a/algosdk/algod.py b/algosdk/algod.py deleted file mode 100644 index 0b1f9529..00000000 --- a/algosdk/algod.py +++ /dev/null @@ -1,383 +0,0 @@ -import base64 -import json -import urllib.error -import warnings -from urllib import parse -from urllib.request import Request, urlopen - -import msgpack - -from . import constants, encoding, error, future -from .v2client.algod import _specify_round_string - -api_version_path_prefix = "/v1" - - -class AlgodClient: - """ - NOTE: This class is deprecated: - v1 algod APIs are deprecated. - Please use the v2 equivalent in `v2client.algod` instead. - - Client class for kmd. Handles all algod requests. - - Args: - algod_token (str): algod API token - algod_address (str): algod address - headers (dict, optional): extra header name/value for all requests - - Attributes: - algod_token (str) - algod_address (str) - headers (dict) - """ - - def __init__(self, algod_token, algod_address, headers=None): - warnings.warn( - "`AlgodClient` is a part of v1 algod APIs that is deprecated. " - "Please use the v2 equivalent in `v2client.algod` instead.", - DeprecationWarning, - ) - self.algod_token = algod_token - self.algod_address = algod_address - self.headers = headers - - def algod_request( - self, - method, - requrl, - params=None, - data=None, - headers=None, - raw_response=False, - ): - """ - Execute a given request. - - Args: - method (str): request method - requrl (str): url for the request - params (dict, optional): parameters for the request - data (dict, optional): data in the body of the request - headers (dict, optional): additional header for request - raw_response (bool, default False): return the HttpResponse object - - Returns: - dict: loaded from json response body - """ - header = {} - - if self.headers: - header.update(self.headers) - - if headers: - header.update(headers) - - if requrl not in constants.no_auth: - header.update({constants.algod_auth_header: self.algod_token}) - - if requrl not in constants.unversioned_paths: - requrl = api_version_path_prefix + requrl - if params: - requrl = requrl + "?" + parse.urlencode(params) - - req = Request( - self.algod_address + requrl, - headers=header, - method=method, - data=data, - ) - - try: - resp = urlopen(req) - except urllib.error.HTTPError as e: - e = e.read().decode("utf-8") - raisex = e - try: - raisex = json.loads(e)["message"] - except: - pass - raise error.AlgodHTTPError(raisex) - if raw_response: - return resp - return json.loads(resp.read().decode("utf-8")) - - def status(self, **kwargs): - """Return node status.""" - req = "/status" - return self.algod_request("GET", req, **kwargs) - - def health(self, **kwargs): - """Return null if the node is running.""" - req = "/health" - return self.algod_request("GET", req, **kwargs) - - def status_after_block(self, block_num=None, round_num=None, **kwargs): - """ - Return node status immediately after blockNum. - - Args: - block_num (int, optional): block number - round_num (int, optional): alias for block_num; specify one of - these - """ - if block_num is None and round_num is None: - raise error.UnderspecifiedRoundError - req = "/status/wait-for-block-after/" + _specify_round_string( - block_num, round_num - ) - - return self.algod_request("GET", req, **kwargs) - - def pending_transactions(self, max_txns=0, **kwargs): - """ - Return pending transactions. - - Args: - max_txns (int): maximum number of transactions to return; - if max_txns is 0, return all pending transactions - """ - query = {"max": max_txns} - req = "/transactions/pending" - return self.algod_request("GET", req, params=query, **kwargs) - - def versions(self, **kwargs): - """Return algod versions.""" - req = "/versions" - return self.algod_request("GET", req, **kwargs) - - def ledger_supply(self, **kwargs): - """Return supply details for node's ledger.""" - req = "/ledger/supply" - return self.algod_request("GET", req, **kwargs) - - def transactions_by_address( - self, - address, - first=None, - last=None, - limit=None, - from_date=None, - to_date=None, - **kwargs - ): - """ - Return transactions for an address. If indexer is not enabled, you can - search by date and you do not have to specify first and last rounds. - - Args: - address (str): account public key - first (int, optional): no transactions before this block will be - returned - last (int, optional): no transactions after this block will be - returned; defaults to last round - limit (int, optional): maximum number of transactions to return; - default is 100 - from_date (str, optional): no transactions before this date will be - returned; format YYYY-MM-DD - to_date (str, optional): no transactions after this date will be - returned; format YYYY-MM-DD - """ - query = dict() - if first is not None: - query["firstRound"] = first - if last is not None: - query["lastRound"] = last - if limit is not None: - query["max"] = limit - if to_date is not None: - query["toDate"] = to_date - if from_date is not None: - query["fromDate"] = from_date - req = "/account/" + address + "/transactions" - return self.algod_request("GET", req, params=query, **kwargs) - - def account_info(self, address, **kwargs): - """ - Return account information. - - Args: - address (str): account public key - """ - req = "/account/" + address - return self.algod_request("GET", req, **kwargs) - - def asset_info(self, index, **kwargs): - """ - Return asset information. - - Args: - index (int): asset index - """ - req = "/asset/" + str(index) - return self.algod_request("GET", req, **kwargs) - - def list_assets(self, max_index=None, max_assets=None, **kwargs): - """ - Return a list of up to max_assets assets, where the maximum asset - index is max_index. - - Args: - max_index (int, optional): maximum asset index; defaults to 0, - which lists most recent assets - max_assets (int, optional): maximum number of assets (0 to 100); - defaults to 100 - """ - query = dict() - query["assetIdx"] = max_index if max_index is not None else 0 - query["max"] = max_assets if max_assets is not None else 100 - req = "/assets" - return self.algod_request("GET", req, params=query, **kwargs) - - def transaction_info(self, address, transaction_id, **kwargs): - """ - Return transaction information. - - Args: - address (str): account public key - transaction_id (str): transaction ID - """ - req = "/account/" + address + "/transaction/" + transaction_id - return self.algod_request("GET", req, **kwargs) - - def pending_transaction_info(self, transaction_id, **kwargs): - """ - Return transaction information for a pending transaction. - - Args: - transaction_id (str): transaction ID - """ - req = "/transactions/pending/" + transaction_id - return self.algod_request("GET", req, **kwargs) - - def transaction_by_id(self, transaction_id, **kwargs): - """ - Return transaction information; only works if indexer is enabled. - - Args: - transaction_id (str): transaction ID - """ - req = "/transaction/" + transaction_id - return self.algod_request("GET", req, **kwargs) - - def suggested_fee(self, **kwargs): - """Return suggested transaction fee.""" - req = "/transactions/fee" - return self.algod_request("GET", req, **kwargs) - - def suggested_params(self, **kwargs): - """Return suggested transaction parameters.""" - req = "/transactions/params" - return self.algod_request("GET", req, **kwargs) - - def suggested_params_as_object(self, **kwargs): - """Return suggested transaction parameters.""" - req = "/transactions/params" - res = self.algod_request("GET", req, **kwargs) - - return future.transaction.SuggestedParams( - res["fee"], - res["lastRound"], - res["lastRound"] + 1000, - res["genesishashb64"], - res["genesisID"], - False, - ) - - def send_raw_transaction(self, txn, headers=None, **kwargs): - """ - Broadcast a signed transaction to the network. - Sets the default Content-Type header, if not previously set. - - Args: - txn (str): transaction to send, encoded in base64 - request_header (dict, optional): additional header for request - - Returns: - str: transaction ID - """ - tx_headers = dict(headers) if headers is not None else {} - if all(map(lambda x: x.lower() != "content-type", [*tx_headers])): - tx_headers["Content-Type"] = "application/x-binary" - txn = base64.b64decode(txn) - req = "/transactions" - return self.algod_request( - "POST", req, data=txn, headers=tx_headers, **kwargs - )["txId"] - - def send_transaction(self, txn, **kwargs): - """ - Broadcast a signed transaction object to the network. - - Args: - txn (SignedTransaction or MultisigTransaction): transaction to send - request_header (dict, optional): additional header for request - - Returns: - str: transaction ID - """ - return self.send_raw_transaction( - encoding.msgpack_encode(txn), **kwargs - ) - - def send_transactions(self, txns, **kwargs): - """ - Broadcast list of a signed transaction objects to the network. - - Args: - txns (SignedTransaction[] or MultisigTransaction[]): - transactions to send - request_header (dict, optional): additional header for request - - Returns: - str: first transaction ID - """ - serialized = [] - for txn in txns: - serialized.append(base64.b64decode(encoding.msgpack_encode(txn))) - - return self.send_raw_transaction( - base64.b64encode(b"".join(serialized)), **kwargs - ) - - def block_info(self, round=None, round_num=None, **kwargs): - """ - Return block information. - - Args: - round (int, optional): block number; deprecated, please use - round_num - round_num (int, optional): alias for round; specify only one of - these - """ - if round is None and round_num is None: - raise error.UnderspecifiedRoundError - req = "/block/" + _specify_round_string(round, round_num) - - return self.algod_request("GET", req, **kwargs) - - def block_raw(self, round=None, round_num=None, **kwargs): - """ - Return decoded raw block as the network sees it. - - Args: - round (int, optional): block number; deprecated, please use - round_num - round_num (int, optional): alias for round; specify only one of - these - """ - if round is None and round_num is None: - raise error.UnderspecifiedRoundError - req = "/block/" + _specify_round_string(round, round_num) - query = {"raw": 1} - kwargs["raw_response"] = True - response = self.algod_request("GET", req, query, **kwargs) - block_type = "application/x-algorand-block-v1" - content_type = response.info().get_content_type() - if content_type != block_type: - raise Exception( - 'expected "Content-Type: {}" but got {!r}'.format( - block_type, content_type - ) - ) - return msgpack.loads(response.read()) diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index 2f556431..e6263c0a 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -2,11 +2,19 @@ import copy from abc import ABC, abstractmethod from enum import IntEnum -from typing import Any, List, Optional, Tuple, TypeVar, Union - -from algosdk import abi, error +from typing import ( + Any, + List, + Dict, + Optional, + Tuple, + TypeVar, + Union, + cast, +) + +from algosdk import abi, error, transaction from algosdk.abi.address_type import AddressType -from algosdk.future import transaction from algosdk.v2client import algod # The first four bytes of an ABI method call return must have this hash @@ -37,7 +45,7 @@ class AtomicTransactionComposerStatus(IntEnum): def populate_foreign_array( - value_to_add: T, foreign_array: List[T], zero_value: T = None + value_to_add: T, foreign_array: List[T], zero_value: Optional[T] = None ) -> int: """ Add a value to an application call's foreign array. The addition will be as @@ -56,10 +64,10 @@ def populate_foreign_array( `value_to_add` will not be added to the array and the 0 index will be returned. """ - if zero_value and value_to_add == zero_value: + if zero_value is not None and value_to_add == zero_value: return 0 - offset = 0 if not zero_value else 1 + offset = 0 if zero_value is None else 1 if value_to_add in foreign_array: return foreign_array.index(value_to_add) + offset @@ -68,6 +76,13 @@ def populate_foreign_array( return offset + len(foreign_array) - 1 +GenericSignedTransaction = Union[ + transaction.SignedTransaction, + transaction.LogicSigTransaction, + transaction.MultisigTransaction, +] + + class AtomicTransactionComposer: """ Constructs an atomic transaction group which may contain a combination of @@ -77,7 +92,7 @@ class AtomicTransactionComposer: status (AtomicTransactionComposerStatus): IntEnum representing the current state of the composer method_dict (dict): dictionary of an index in the transaction list to a Method object txn_list (list[TransactionWithSigner]): list of transactions with signers - signed_txns (list[SignedTransaction]): list of signed transactions + signed_txns (list[GenericSignedTransaction]): list of signed transactions tx_ids (list[str]): list of individual transaction IDs in this atomic group """ @@ -88,10 +103,10 @@ class AtomicTransactionComposer: def __init__(self) -> None: self.status = AtomicTransactionComposerStatus.BUILDING - self.method_dict = {} - self.txn_list = [] - self.signed_txns = [] - self.tx_ids = [] + self.method_dict: Dict[int, abi.Method] = {} + self.txn_list: List[TransactionWithSigner] = [] + self.signed_txns: List[GenericSignedTransaction] = [] + self.tx_ids: List[str] = [] def get_status(self) -> AtomicTransactionComposerStatus: """ @@ -160,20 +175,22 @@ def add_method_call( sender: str, sp: transaction.SuggestedParams, signer: "TransactionSigner", - method_args: List[Union[Any, "TransactionWithSigner"]] = None, + method_args: Optional[ + List[Union[Any, "TransactionWithSigner"]] + ] = None, on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - local_schema: transaction.StateSchema = None, - global_schema: transaction.StateSchema = None, - approval_program: bytes = None, - clear_program: bytes = None, - extra_pages: int = None, - accounts: List[str] = None, - foreign_apps: List[int] = None, - foreign_assets: List[int] = None, - note: bytes = None, - lease: bytes = None, - rekey_to: str = None, - boxes: List[Tuple[int, bytes]] = None, + local_schema: Optional[transaction.StateSchema] = None, + global_schema: Optional[transaction.StateSchema] = None, + approval_program: Optional[bytes] = None, + clear_program: Optional[bytes] = None, + extra_pages: Optional[int] = None, + accounts: Optional[List[str]] = None, + foreign_apps: Optional[List[int]] = None, + foreign_assets: Optional[List[int]] = None, + note: Optional[bytes] = None, + lease: Optional[bytes] = None, + rekey_to: Optional[str] = None, + boxes: Optional[List[Tuple[int, bytes]]] = None, ) -> "AtomicTransactionComposer": """ Add a smart contract method call to this atomic group. @@ -264,7 +281,7 @@ def add_method_call( boxes = boxes[:] if boxes else [] app_args = [] - raw_values = [] + raw_values: List[Any] = [] raw_types = [] txn_list = [] @@ -288,22 +305,24 @@ def add_method_call( txn_list.append(method_args[i]) else: if abi.is_abi_reference_type(arg.type): - current_type = abi.UintType(8) + current_type: Union[str, abi.ABIType] = abi.UintType(8) if arg.type == abi.ABIReferenceType.ACCOUNT: address_type = AddressType() account_arg = address_type.decode( - address_type.encode(method_args[i]) + address_type.encode( + cast(Union[str, bytes], method_args[i]) + ) ) - current_arg = populate_foreign_array( + current_arg: Any = populate_foreign_array( account_arg, accounts, sender ) elif arg.type == abi.ABIReferenceType.ASSET: - asset_arg = int(method_args[i]) + asset_arg = int(cast(int, method_args[i])) current_arg = populate_foreign_array( asset_arg, foreign_assets ) elif arg.type == abi.ABIReferenceType.APPLICATION: - app_arg = int(method_args[i]) + app_arg = int(cast(int, method_args[i])) current_arg = populate_foreign_array( app_arg, foreign_apps, app_id ) @@ -398,14 +417,18 @@ def gather_signatures(self) -> list: An error will be thrown if signing any of the transactions fails. Returns: - list[SignedTransactions]: list of signed transactions + List[GenericSignedTransaction]: list of signed transactions """ if self.status >= AtomicTransactionComposerStatus.SIGNED: # Return cached versions of the signatures return self.signed_txns - stxn_list = [None] * len(self.txn_list) - signer_indexes = {} # Map a signer to a list of indices to sign + stxn_list: List[Optional[GenericSignedTransaction]] = [None] * len( + self.txn_list + ) + signer_indexes: Dict[ + TransactionSigner, List[int] + ] = {} # Map a signer to a list of indices to sign txn_list = self.build_group() for i, txn_with_signer in enumerate(txn_list): if txn_with_signer.signer not in signer_indexes: @@ -424,12 +447,13 @@ def gather_signatures(self) -> list: raise error.AtomicTransactionComposerError( "missing signatures, got {}".format(stxn_list) ) + full_stxn_list = cast(List[GenericSignedTransaction], stxn_list) self.status = AtomicTransactionComposerStatus.SIGNED - self.signed_txns = stxn_list + self.signed_txns = full_stxn_list return self.signed_txns - def submit(self, client: algod.AlgodClient) -> list: + def submit(self, client: algod.AlgodClient) -> List[str]: """ Send the transaction group to the network, but don't wait for it to be committed to a block. An error will be thrown if submission fails. @@ -442,7 +466,7 @@ def submit(self, client: algod.AlgodClient) -> list: client (AlgodClient): Algod V2 client Returns: - list[Transaction]: list of submitted transactions + List[str]: list of submitted transaction IDs """ if self.status <= AtomicTransactionComposerStatus.SUBMITTED: self.gather_signatures() @@ -496,10 +520,10 @@ def execute( method_results = [] for i, tx_id in enumerate(self.tx_ids): - raw_value = None + raw_value: Optional[bytes] = None return_value = None decode_error = None - tx_info = None + tx_info: Optional[Any] = None if i not in self.method_dict: continue @@ -511,7 +535,7 @@ def execute( method_results.append( ABIResult( tx_id=tx_id, - raw_value=raw_value, + raw_value=cast(bytes, raw_value), return_value=return_value, decode_error=decode_error, tx_info=tx_info, @@ -538,18 +562,19 @@ def execute( "app call transaction did not log a return value" ) raw_value = result_bytes[4:] - return_value = self.method_dict[i].returns.type.decode( - raw_value + method_return_type = cast( + abi.ABIType, self.method_dict[i].returns.type ) + return_value = method_return_type.decode(raw_value) except Exception as e: decode_error = e abi_result = ABIResult( tx_id=tx_id, - raw_value=raw_value, + raw_value=cast(bytes, raw_value), return_value=return_value, decode_error=decode_error, - tx_info=tx_info, + tx_info=cast(Any, tx_info), method=self.method_dict[i], ) method_results.append(abi_result) @@ -572,7 +597,7 @@ def __init__(self) -> None: @abstractmethod def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[GenericSignedTransaction]: pass @@ -591,7 +616,7 @@ def __init__(self, private_key: str) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -625,7 +650,7 @@ def __init__(self, lsig: transaction.LogicSigAccount) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -637,7 +662,7 @@ def sign_transactions( txn_group (list[Transaction]): atomic group of transactions indexes (list[int]): array of indexes in the atomic transaction group that should be signed """ - stxns = [] + stxns: List[GenericSignedTransaction] = [] for i in indexes: stxn = transaction.LogicSigTransaction(txn_group[i], self.lsig) stxns.append(stxn) @@ -661,7 +686,7 @@ def __init__(self, msig: transaction.Multisig, sks: str) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -673,7 +698,7 @@ def sign_transactions( txn_group (list[Transaction]): atomic group of transactions indexes (list[int]): array of indexes in the atomic transaction group that should be signed """ - stxns = [] + stxns: List[GenericSignedTransaction] = [] for i in indexes: mtxn = transaction.MultisigTransaction(txn_group[i], self.msig) for sk in self.sks: @@ -693,7 +718,7 @@ def __init__( class ABIResult: def __init__( self, - tx_id: int, + tx_id: str, raw_value: bytes, return_value: Any, decode_error: Optional[Exception], diff --git a/algosdk/constants.py b/algosdk/constants.py index 6ddba7d4..cf3c8a21 100644 --- a/algosdk/constants.py +++ b/algosdk/constants.py @@ -1,3 +1,5 @@ +from typing import List + """ Contains useful constants. """ @@ -9,7 +11,7 @@ """str: header key for indexer requests""" UNVERSIONED_PATHS = ["/health", "/versions", "/metrics", "/genesis"] """str[]: paths that don't use the version path prefix""" -NO_AUTH = [] +NO_AUTH: List[str] = [] """str[]: requests that don't require authentication""" diff --git a/algosdk/data/langspec.json b/algosdk/data/langspec.json deleted file mode 100644 index ee94b0d3..00000000 --- a/algosdk/data/langspec.json +++ /dev/null @@ -1 +0,0 @@ -{"EvalMaxVersion":6,"LogicSigVersion":6,"Ops":[{"Opcode":0,"Name":"err","Cost":1,"Size":1,"Doc":"Fail immediately.","Groups":["Flow Control"]},{"Opcode":1,"Name":"sha256","Args":"B","Returns":"B","Cost":35,"Size":1,"Doc":"SHA256 hash of value A, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":2,"Name":"keccak256","Args":"B","Returns":"B","Cost":130,"Size":1,"Doc":"Keccak256 hash of value A, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":3,"Name":"sha512_256","Args":"B","Returns":"B","Cost":45,"Size":1,"Doc":"SHA512_256 hash of value A, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":4,"Name":"ed25519verify","Args":"BBB","Returns":"U","Cost":1900,"Size":1,"Doc":"for (data A, signature B, pubkey C) verify the signature of (\"ProgData\" || program_hash || data) against the pubkey =\u003e {0 or 1}","DocExtra":"The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack.","Groups":["Arithmetic"]},{"Opcode":5,"Name":"ecdsa_verify","Args":"BBBBB","Returns":"U","Cost":1700,"Size":2,"Doc":"for (data A, signature B, C and pubkey D, E) verify the signature of the data against the pubkey =\u003e {0 or 1}","DocExtra":"The 32 byte Y-component of a public key is the last element on the stack, preceded by X-component of a pubkey, preceded by S and R components of a signature, preceded by the data that is fifth element on the stack. All values are big-endian encoded. The signed data must be 32 bytes long, and signatures in lower-S form are only accepted.","ImmediateNote":"{uint8 curve index}","Groups":["Arithmetic"]},{"Opcode":6,"Name":"ecdsa_pk_decompress","Args":"B","Returns":"BB","Cost":650,"Size":2,"Doc":"decompress pubkey A into components X, Y","DocExtra":"The 33 byte public key in a compressed form to be decompressed into X and Y (top) components. All values are big-endian encoded.","ImmediateNote":"{uint8 curve index}","Groups":["Arithmetic"]},{"Opcode":7,"Name":"ecdsa_pk_recover","Args":"BUBB","Returns":"BB","Cost":2000,"Size":2,"Doc":"for (data A, recovery id B, signature C, D) recover a public key","DocExtra":"S (top) and R elements of a signature, recovery id and data (bottom) are expected on the stack and used to deriver a public key. All values are big-endian encoded. The signed data must be 32 bytes long.","ImmediateNote":"{uint8 curve index}","Groups":["Arithmetic"]},{"Opcode":8,"Name":"+","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A plus B. Fail on overflow.","DocExtra":"Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `addw`.","Groups":["Arithmetic"]},{"Opcode":9,"Name":"-","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A minus B. Fail if B \u003e A.","Groups":["Arithmetic"]},{"Opcode":10,"Name":"/","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A divided by B (truncated division). Fail if B == 0.","DocExtra":"`divmodw` is available to divide the two-element values produced by `mulw` and `addw`.","Groups":["Arithmetic"]},{"Opcode":11,"Name":"*","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A times B. Fail on overflow.","DocExtra":"Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `mulw`.","Groups":["Arithmetic"]},{"Opcode":12,"Name":"\u003c","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A less than B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":13,"Name":"\u003e","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A greater than B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":14,"Name":"\u003c=","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A less than or equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":15,"Name":"\u003e=","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A greater than or equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":16,"Name":"\u0026\u0026","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A is not zero and B is not zero =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":17,"Name":"||","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A is not zero or B is not zero =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":18,"Name":"==","Args":"..","Returns":"U","Cost":1,"Size":1,"Doc":"A is equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":19,"Name":"!=","Args":"..","Returns":"U","Cost":1,"Size":1,"Doc":"A is not equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":20,"Name":"!","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"A == 0 yields 1; else 0","Groups":["Arithmetic"]},{"Opcode":21,"Name":"len","Args":"B","Returns":"U","Cost":1,"Size":1,"Doc":"yields length of byte value A","Groups":["Arithmetic"]},{"Opcode":22,"Name":"itob","Args":"U","Returns":"B","Cost":1,"Size":1,"Doc":"converts uint64 A to big endian bytes","Groups":["Arithmetic"]},{"Opcode":23,"Name":"btoi","Args":"B","Returns":"U","Cost":1,"Size":1,"Doc":"converts bytes A as big endian to uint64","DocExtra":"`btoi` fails if the input is longer than 8 bytes.","Groups":["Arithmetic"]},{"Opcode":24,"Name":"%","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A modulo B. Fail if B == 0.","Groups":["Arithmetic"]},{"Opcode":25,"Name":"|","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-or B","Groups":["Arithmetic"]},{"Opcode":26,"Name":"\u0026","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-and B","Groups":["Arithmetic"]},{"Opcode":27,"Name":"^","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-xor B","Groups":["Arithmetic"]},{"Opcode":28,"Name":"~","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"bitwise invert value A","Groups":["Arithmetic"]},{"Opcode":29,"Name":"mulw","Args":"UU","Returns":"UU","Cost":1,"Size":1,"Doc":"A times B as a 128-bit result in two uint64s. X is the high 64 bits, Y is the low","Groups":["Arithmetic"]},{"Opcode":30,"Name":"addw","Args":"UU","Returns":"UU","Cost":1,"Size":1,"Doc":"A plus B as a 128-bit result. X is the carry-bit, Y is the low-order 64 bits.","Groups":["Arithmetic"]},{"Opcode":31,"Name":"divmodw","Args":"UUUU","Returns":"UUUU","Cost":20,"Size":1,"Doc":"W,X = (A,B / C,D); Y,Z = (A,B modulo C,D)","DocExtra":"The notation J,K indicates that two uint64 values J and K are interpreted as a uint128 value, with J as the high uint64 and K the low.","Groups":["Arithmetic"]},{"Opcode":32,"Name":"intcblock","Cost":1,"Size":0,"Doc":"prepare block of uint64 constants for use by intc","DocExtra":"`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script.","ImmediateNote":"{varuint length} [{varuint value}, ...]","Groups":["Loading Values"]},{"Opcode":33,"Name":"intc","Returns":"U","Cost":1,"Size":2,"Doc":"Ith constant from intcblock","ImmediateNote":"{uint8 int constant index}","Groups":["Loading Values"]},{"Opcode":34,"Name":"intc_0","Returns":"U","Cost":1,"Size":1,"Doc":"constant 0 from intcblock","Groups":["Loading Values"]},{"Opcode":35,"Name":"intc_1","Returns":"U","Cost":1,"Size":1,"Doc":"constant 1 from intcblock","Groups":["Loading Values"]},{"Opcode":36,"Name":"intc_2","Returns":"U","Cost":1,"Size":1,"Doc":"constant 2 from intcblock","Groups":["Loading Values"]},{"Opcode":37,"Name":"intc_3","Returns":"U","Cost":1,"Size":1,"Doc":"constant 3 from intcblock","Groups":["Loading Values"]},{"Opcode":38,"Name":"bytecblock","Cost":1,"Size":0,"Doc":"prepare block of byte-array constants for use by bytec","DocExtra":"`bytecblock` loads the following program bytes into an array of byte-array constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script.","ImmediateNote":"{varuint length} [({varuint value length} bytes), ...]","Groups":["Loading Values"]},{"Opcode":39,"Name":"bytec","Returns":"B","Cost":1,"Size":2,"Doc":"Ith constant from bytecblock","ImmediateNote":"{uint8 byte constant index}","Groups":["Loading Values"]},{"Opcode":40,"Name":"bytec_0","Returns":"B","Cost":1,"Size":1,"Doc":"constant 0 from bytecblock","Groups":["Loading Values"]},{"Opcode":41,"Name":"bytec_1","Returns":"B","Cost":1,"Size":1,"Doc":"constant 1 from bytecblock","Groups":["Loading Values"]},{"Opcode":42,"Name":"bytec_2","Returns":"B","Cost":1,"Size":1,"Doc":"constant 2 from bytecblock","Groups":["Loading Values"]},{"Opcode":43,"Name":"bytec_3","Returns":"B","Cost":1,"Size":1,"Doc":"constant 3 from bytecblock","Groups":["Loading Values"]},{"Opcode":44,"Name":"arg","Returns":"B","Cost":1,"Size":2,"Doc":"Nth LogicSig argument","ImmediateNote":"{uint8 arg index N}","Groups":["Loading Values"]},{"Opcode":45,"Name":"arg_0","Returns":"B","Cost":1,"Size":1,"Doc":"LogicSig argument 0","Groups":["Loading Values"]},{"Opcode":46,"Name":"arg_1","Returns":"B","Cost":1,"Size":1,"Doc":"LogicSig argument 1","Groups":["Loading Values"]},{"Opcode":47,"Name":"arg_2","Returns":"B","Cost":1,"Size":1,"Doc":"LogicSig argument 2","Groups":["Loading Values"]},{"Opcode":48,"Name":"arg_3","Returns":"B","Cost":1,"Size":1,"Doc":"LogicSig argument 3","Groups":["Loading Values"]},{"Opcode":49,"Name":"txn","Returns":".","Cost":1,"Size":2,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice","ExtraProgramPages","Nonparticipation","Logs","NumLogs","CreatedAssetID","CreatedApplicationID","LastLog","StateProofPK"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUUUUBUUUBB","Doc":"field F of current transaction","DocExtra":"FirstValidTime causes the program to fail. The field is reserved for future use.","ImmediateNote":"{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":50,"Name":"global","Returns":".","Cost":1,"Size":2,"Doc":"global field F","ImmediateNote":"{uint8 global field index}","Groups":["Loading Values"]},{"Opcode":51,"Name":"gtxn","Returns":".","Cost":1,"Size":3,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice","ExtraProgramPages","Nonparticipation","Logs","NumLogs","CreatedAssetID","CreatedApplicationID","LastLog","StateProofPK"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUUUUBUUUBB","Doc":"field F of the Tth transaction in the current group","DocExtra":"for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`.","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":52,"Name":"load","Returns":".","Cost":1,"Size":2,"Doc":"Ith scratch space value. All scratch spaces are 0 at program start.","ImmediateNote":"{uint8 position in scratch space to load from}","Groups":["Loading Values"]},{"Opcode":53,"Name":"store","Args":".","Cost":1,"Size":2,"Doc":"store A to the Ith scratch space","ImmediateNote":"{uint8 position in scratch space to store to}","Groups":["Loading Values"]},{"Opcode":54,"Name":"txna","Returns":".","Cost":1,"Size":3,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Ith value of the array field F of the current transaction","ImmediateNote":"{uint8 transaction field index} {uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":55,"Name":"gtxna","Returns":".","Cost":1,"Size":4,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Ith value of the array field F from the Tth transaction in the current group","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index} {uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":56,"Name":"gtxns","Args":"U","Returns":".","Cost":1,"Size":2,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice","ExtraProgramPages","Nonparticipation","Logs","NumLogs","CreatedAssetID","CreatedApplicationID","LastLog","StateProofPK"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUUUUBUUUBB","Doc":"field F of the Ath transaction in the current group","DocExtra":"for notes on transaction fields available, see `txn`. If top of stack is _i_, `gtxns field` is equivalent to `gtxn _i_ field`. gtxns exists so that _i_ can be calculated, often based on the index of the current transaction.","ImmediateNote":"{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":57,"Name":"gtxnsa","Args":"U","Returns":".","Cost":1,"Size":3,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Ith value of the array field F from the Ath transaction in the current group","ImmediateNote":"{uint8 transaction field index} {uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":58,"Name":"gload","Returns":".","Cost":1,"Size":3,"Doc":"Ith scratch space value of the Tth transaction in the current group","DocExtra":"`gload` fails unless the requested transaction is an ApplicationCall and T \u003c GroupIndex.","ImmediateNote":"{uint8 transaction group index} {uint8 position in scratch space to load from}","Groups":["Loading Values"]},{"Opcode":59,"Name":"gloads","Args":"U","Returns":".","Cost":1,"Size":2,"Doc":"Ith scratch space value of the Ath transaction in the current group","DocExtra":"`gloads` fails unless the requested transaction is an ApplicationCall and A \u003c GroupIndex.","ImmediateNote":"{uint8 position in scratch space to load from}","Groups":["Loading Values"]},{"Opcode":60,"Name":"gaid","Returns":"U","Cost":1,"Size":2,"Doc":"ID of the asset or application created in the Tth transaction of the current group","DocExtra":"`gaid` fails unless the requested transaction created an asset or application and T \u003c GroupIndex.","ImmediateNote":"{uint8 transaction group index}","Groups":["Loading Values"]},{"Opcode":61,"Name":"gaids","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"ID of the asset or application created in the Ath transaction of the current group","DocExtra":"`gaids` fails unless the requested transaction created an asset or application and A \u003c GroupIndex.","Groups":["Loading Values"]},{"Opcode":62,"Name":"loads","Args":"U","Returns":".","Cost":1,"Size":1,"Doc":"Ath scratch space value. All scratch spaces are 0 at program start.","Groups":["Loading Values"]},{"Opcode":63,"Name":"stores","Args":"U.","Cost":1,"Size":1,"Doc":"store B to the Ath scratch space","Groups":["Loading Values"]},{"Opcode":64,"Name":"bnz","Args":"U","Cost":1,"Size":3,"Doc":"branch to TARGET if value A is not zero","DocExtra":"The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Starting at v4, the offset is treated as a signed 16 bit integer allowing for backward branches and looping. In prior version (v1 to v3), branch offsets are limited to forward branches only, 0-0x7fff.\n\nAt v2 it became allowed to branch to the end of the program exactly after the last instruction: bnz to byte N (with 0-indexing) was illegal for a TEAL program with N bytes before v2, and is legal after it. This change eliminates the need for a last instruction of no-op as a branch target at the end. (Branching beyond the end--in other words, to a byte larger than N--is still illegal and will cause the program to fail.)","ImmediateNote":"{int16 branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":65,"Name":"bz","Args":"U","Cost":1,"Size":3,"Doc":"branch to TARGET if value A is zero","DocExtra":"See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`.","ImmediateNote":"{int16 branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":66,"Name":"b","Cost":1,"Size":3,"Doc":"branch unconditionally to TARGET","DocExtra":"See `bnz` for details on how branches work. `b` always jumps to the offset.","ImmediateNote":"{int16 branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":67,"Name":"return","Args":"U","Cost":1,"Size":1,"Doc":"use A as success value; end","Groups":["Flow Control"]},{"Opcode":68,"Name":"assert","Args":"U","Cost":1,"Size":1,"Doc":"immediately fail unless A is a non-zero number","Groups":["Flow Control"]},{"Opcode":72,"Name":"pop","Args":".","Cost":1,"Size":1,"Doc":"discard A","Groups":["Flow Control"]},{"Opcode":73,"Name":"dup","Args":".","Returns":"..","Cost":1,"Size":1,"Doc":"duplicate A","Groups":["Flow Control"]},{"Opcode":74,"Name":"dup2","Args":"..","Returns":"....","Cost":1,"Size":1,"Doc":"duplicate A and B","Groups":["Flow Control"]},{"Opcode":75,"Name":"dig","Args":".","Returns":"..","Cost":1,"Size":2,"Doc":"Nth value from the top of the stack. dig 0 is equivalent to dup","ImmediateNote":"{uint8 depth}","Groups":["Flow Control"]},{"Opcode":76,"Name":"swap","Args":"..","Returns":"..","Cost":1,"Size":1,"Doc":"swaps A and B on stack","Groups":["Flow Control"]},{"Opcode":77,"Name":"select","Args":"..U","Returns":".","Cost":1,"Size":1,"Doc":"selects one of two values based on top-of-stack: B if C != 0, else A","Groups":["Flow Control"]},{"Opcode":78,"Name":"cover","Args":".","Returns":".","Cost":1,"Size":2,"Doc":"remove top of stack, and place it deeper in the stack such that N elements are above it. Fails if stack depth \u003c= N.","ImmediateNote":"{uint8 depth}","Groups":["Flow Control"]},{"Opcode":79,"Name":"uncover","Args":".","Returns":".","Cost":1,"Size":2,"Doc":"remove the value at depth N in the stack and shift above items down so the Nth deep value is on top of the stack. Fails if stack depth \u003c= N.","ImmediateNote":"{uint8 depth}","Groups":["Flow Control"]},{"Opcode":80,"Name":"concat","Args":"BB","Returns":"B","Cost":1,"Size":1,"Doc":"join A and B","DocExtra":"`concat` fails if the result would be greater than 4096 bytes.","Groups":["Arithmetic"]},{"Opcode":81,"Name":"substring","Args":"B","Returns":"B","Cost":1,"Size":3,"Doc":"A range of bytes from A starting at S up to but not including E. If E \u003c S, or either is larger than the array length, the program fails","ImmediateNote":"{uint8 start position} {uint8 end position}","Groups":["Byte Array Manipulation"]},{"Opcode":82,"Name":"substring3","Args":"BUU","Returns":"B","Cost":1,"Size":1,"Doc":"A range of bytes from A starting at B up to but not including C. If C \u003c B, or either is larger than the array length, the program fails","Groups":["Byte Array Manipulation"]},{"Opcode":83,"Name":"getbit","Args":".U","Returns":"U","Cost":1,"Size":1,"Doc":"Bth bit of (byte-array or integer) A.","DocExtra":"see explanation of bit ordering in setbit","Groups":["Arithmetic"]},{"Opcode":84,"Name":"setbit","Args":".UU","Returns":".","Cost":1,"Size":1,"Doc":"Copy of (byte-array or integer) A, with the Bth bit set to (0 or 1) C","DocExtra":"When A is a uint64, index 0 is the least significant bit. Setting bit 3 to 1 on the integer 0 yields 8, or 2^3. When A is a byte array, index 0 is the leftmost bit of the leftmost byte. Setting bits 0 through 11 to 1 in a 4-byte-array of 0s yields the byte array 0xfff00000. Setting bit 3 to 1 on the 1-byte-array 0x00 yields the byte array 0x10.","Groups":["Arithmetic"]},{"Opcode":85,"Name":"getbyte","Args":"BU","Returns":"U","Cost":1,"Size":1,"Doc":"Bth byte of A, as an integer","Groups":["Arithmetic"]},{"Opcode":86,"Name":"setbyte","Args":"BUU","Returns":"B","Cost":1,"Size":1,"Doc":"Copy of A with the Bth byte set to small integer (between 0..255) C","Groups":["Arithmetic"]},{"Opcode":87,"Name":"extract","Args":"B","Returns":"B","Cost":1,"Size":3,"Doc":"A range of bytes from A starting at S up to but not including S+L. If L is 0, then extract to the end of the string. If S or S+L is larger than the array length, the program fails","ImmediateNote":"{uint8 start position} {uint8 length}","Groups":["Byte Array Manipulation"]},{"Opcode":88,"Name":"extract3","Args":"BUU","Returns":"B","Cost":1,"Size":1,"Doc":"A range of bytes from A starting at B up to but not including B+C. If B+C is larger than the array length, the program fails","Groups":["Byte Array Manipulation"]},{"Opcode":89,"Name":"extract_uint16","Args":"BU","Returns":"U","Cost":1,"Size":1,"Doc":"A uint16 formed from a range of big-endian bytes from A starting at B up to but not including B+2. If B+2 is larger than the array length, the program fails","Groups":["Byte Array Manipulation"]},{"Opcode":90,"Name":"extract_uint32","Args":"BU","Returns":"U","Cost":1,"Size":1,"Doc":"A uint32 formed from a range of big-endian bytes from A starting at B up to but not including B+4. If B+4 is larger than the array length, the program fails","Groups":["Byte Array Manipulation"]},{"Opcode":91,"Name":"extract_uint64","Args":"BU","Returns":"U","Cost":1,"Size":1,"Doc":"A uint64 formed from a range of big-endian bytes from A starting at B up to but not including B+8. If B+8 is larger than the array length, the program fails","Groups":["Byte Array Manipulation"]},{"Opcode":96,"Name":"balance","Args":".","Returns":"U","Cost":1,"Size":1,"Doc":"get balance for account A, in microalgos. The balance is observed after the effects of previous transactions in the group, and after the fee for the current transaction is deducted.","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ account address), _available_ application id (or, since v4, a Txn.ForeignApps offset). Return: value.","Groups":["State Access"]},{"Opcode":97,"Name":"app_opted_in","Args":".U","Returns":"U","Cost":1,"Size":1,"Doc":"1 if account A is opted in to application B, else 0","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ account address), _available_ application id (or, since v4, a Txn.ForeignApps offset). Return: 1 if opted in and 0 otherwise.","Groups":["State Access"]},{"Opcode":98,"Name":"app_local_get","Args":".B","Returns":".","Cost":1,"Size":1,"Doc":"local state of the key B in the current application in account A","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ account address), state key. Return: value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":99,"Name":"app_local_get_ex","Args":".UB","Returns":".U","Cost":1,"Size":1,"Doc":"X is the local state of application B, key C in account A. Y is 1 if key existed, else 0","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ account address), _available_ application id (or, since v4, a Txn.ForeignApps offset), state key. Return: did_exist flag (top of the stack, 1 if the application and key existed and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":100,"Name":"app_global_get","Args":"B","Returns":".","Cost":1,"Size":1,"Doc":"global state of the key A in the current application","DocExtra":"params: state key. Return: value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":101,"Name":"app_global_get_ex","Args":"UB","Returns":".U","Cost":1,"Size":1,"Doc":"X is the global state of application A, key B. Y is 1 if key existed, else 0","DocExtra":"params: Txn.ForeignApps offset (or, since v4, an _available_ application id), state key. Return: did_exist flag (top of the stack, 1 if the application and key existed and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":102,"Name":"app_local_put","Args":".B.","Cost":1,"Size":1,"Doc":"write C to key B in account A's local state of the current application","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ account address), state key, value.","Groups":["State Access"]},{"Opcode":103,"Name":"app_global_put","Args":"B.","Cost":1,"Size":1,"Doc":"write B to key A in the global state of the current application","Groups":["State Access"]},{"Opcode":104,"Name":"app_local_del","Args":".B","Cost":1,"Size":1,"Doc":"delete key B from account A's local state of the current application","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ account address), state key.\n\nDeleting a key which is already absent has no effect on the application local state. (In particular, it does _not_ cause the program to fail.)","Groups":["State Access"]},{"Opcode":105,"Name":"app_global_del","Args":"B","Cost":1,"Size":1,"Doc":"delete key A from the global state of the current application","DocExtra":"params: state key.\n\nDeleting a key which is already absent has no effect on the application global state. (In particular, it does _not_ cause the program to fail.)","Groups":["State Access"]},{"Opcode":112,"Name":"asset_holding_get","Args":".U","Returns":".U","Cost":1,"Size":2,"ArgEnum":["AssetBalance","AssetFrozen"],"ArgEnumTypes":"UU","Doc":"X is field F from account A's holding of asset B. Y is 1 if A is opted into B, else 0","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ address), asset id (or, since v4, a Txn.ForeignAssets offset). Return: did_exist flag (1 if the asset existed and 0 otherwise), value.","ImmediateNote":"{uint8 asset holding field index}","Groups":["State Access"]},{"Opcode":113,"Name":"asset_params_get","Args":"U","Returns":".U","Cost":1,"Size":2,"ArgEnum":["AssetTotal","AssetDecimals","AssetDefaultFrozen","AssetUnitName","AssetName","AssetURL","AssetMetadataHash","AssetManager","AssetReserve","AssetFreeze","AssetClawback","AssetCreator"],"ArgEnumTypes":"UUUBBBBBBBBB","Doc":"X is field F from asset A. Y is 1 if A exists, else 0","DocExtra":"params: Txn.ForeignAssets offset (or, since v4, an _available_ asset id. Return: did_exist flag (1 if the asset existed and 0 otherwise), value.","ImmediateNote":"{uint8 asset params field index}","Groups":["State Access"]},{"Opcode":114,"Name":"app_params_get","Args":"U","Returns":".U","Cost":1,"Size":2,"ArgEnum":["AppApprovalProgram","AppClearStateProgram","AppGlobalNumUint","AppGlobalNumByteSlice","AppLocalNumUint","AppLocalNumByteSlice","AppExtraProgramPages","AppCreator","AppAddress"],"ArgEnumTypes":"BBUUUUUBB","Doc":"X is field F from app A. Y is 1 if A exists, else 0","DocExtra":"params: Txn.ForeignApps offset or an _available_ app id. Return: did_exist flag (1 if the application existed and 0 otherwise), value.","ImmediateNote":"{uint8 app params field index}","Groups":["State Access"]},{"Opcode":115,"Name":"acct_params_get","Args":".","Returns":".U","Cost":1,"Size":2,"Doc":"X is field F from account A. Y is 1 if A owns positive algos, else 0","ImmediateNote":"{uint8 account params field index}","Groups":["State Access"]},{"Opcode":120,"Name":"min_balance","Args":".","Returns":"U","Cost":1,"Size":1,"Doc":"get minimum required balance for account A, in microalgos. Required balance is affected by [ASA](https://developer.algorand.org/docs/features/asa/#assets-overview) and [App](https://developer.algorand.org/docs/features/asc1/stateful/#minimum-balance-requirement-for-a-smart-contract) usage. When creating or opting into an app, the minimum balance grows before the app code runs, therefore the increase is visible there. When deleting or closing out, the minimum balance decreases after the app executes.","DocExtra":"params: Txn.Accounts offset (or, since v4, an _available_ account address), _available_ application id (or, since v4, a Txn.ForeignApps offset). Return: value.","Groups":["State Access"]},{"Opcode":128,"Name":"pushbytes","Returns":"B","Cost":1,"Size":0,"Doc":"immediate BYTES","DocExtra":"pushbytes args are not added to the bytecblock during assembly processes","ImmediateNote":"{varuint length} {bytes}","Groups":["Loading Values"]},{"Opcode":129,"Name":"pushint","Returns":"U","Cost":1,"Size":0,"Doc":"immediate UINT","DocExtra":"pushint args are not added to the intcblock during assembly processes","ImmediateNote":"{varuint int}","Groups":["Loading Values"]},{"Opcode":136,"Name":"callsub","Cost":1,"Size":3,"Doc":"branch unconditionally to TARGET, saving the next instruction on the call stack","DocExtra":"The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it.","ImmediateNote":"{int16 branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":137,"Name":"retsub","Cost":1,"Size":1,"Doc":"pop the top instruction from the call stack and branch to it","DocExtra":"The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it.","Groups":["Flow Control"]},{"Opcode":144,"Name":"shl","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A times 2^B, modulo 2^64","Groups":["Arithmetic"]},{"Opcode":145,"Name":"shr","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A divided by 2^B","Groups":["Arithmetic"]},{"Opcode":146,"Name":"sqrt","Args":"U","Returns":"U","Cost":4,"Size":1,"Doc":"The largest integer I such that I^2 \u003c= A","Groups":["Arithmetic"]},{"Opcode":147,"Name":"bitlen","Args":".","Returns":"U","Cost":1,"Size":1,"Doc":"The highest set bit in A. If A is a byte-array, it is interpreted as a big-endian unsigned integer. bitlen of 0 is 0, bitlen of 8 is 4","DocExtra":"bitlen interprets arrays as big-endian integers, unlike setbit/getbit","Groups":["Arithmetic"]},{"Opcode":148,"Name":"exp","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A raised to the Bth power. Fail if A == B == 0 and on overflow","Groups":["Arithmetic"]},{"Opcode":149,"Name":"expw","Args":"UU","Returns":"UU","Cost":10,"Size":1,"Doc":"A raised to the Bth power as a 128-bit result in two uint64s. X is the high 64 bits, Y is the low. Fail if A == B == 0 or if the results exceeds 2^128-1","Groups":["Arithmetic"]},{"Opcode":150,"Name":"bsqrt","Args":"B","Returns":"B","Cost":40,"Size":1,"Doc":"The largest integer I such that I^2 \u003c= A. A and I are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":151,"Name":"divw","Args":"UUU","Returns":"U","Cost":1,"Size":1,"Doc":"A,B / C. Fail if C == 0 or if result overflows.","DocExtra":"The notation A,B indicates that A and B are interpreted as a uint128 value, with A as the high uint64 and B the low.","Groups":["Arithmetic"]},{"Opcode":160,"Name":"b+","Args":"BB","Returns":"B","Cost":10,"Size":1,"Doc":"A plus B. A and B are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":161,"Name":"b-","Args":"BB","Returns":"B","Cost":10,"Size":1,"Doc":"A minus B. A and B are interpreted as big-endian unsigned integers. Fail on underflow.","Groups":["Byte Array Arithmetic"]},{"Opcode":162,"Name":"b/","Args":"BB","Returns":"B","Cost":20,"Size":1,"Doc":"A divided by B (truncated division). A and B are interpreted as big-endian unsigned integers. Fail if B is zero.","Groups":["Byte Array Arithmetic"]},{"Opcode":163,"Name":"b*","Args":"BB","Returns":"B","Cost":20,"Size":1,"Doc":"A times B. A and B are interpreted as big-endian unsigned integers.","Groups":["Byte Array Arithmetic"]},{"Opcode":164,"Name":"b\u003c","Args":"BB","Returns":"U","Cost":1,"Size":1,"Doc":"1 if A is less than B, else 0. A and B are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":165,"Name":"b\u003e","Args":"BB","Returns":"U","Cost":1,"Size":1,"Doc":"1 if A is greater than B, else 0. A and B are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":166,"Name":"b\u003c=","Args":"BB","Returns":"U","Cost":1,"Size":1,"Doc":"1 if A is less than or equal to B, else 0. A and B are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":167,"Name":"b\u003e=","Args":"BB","Returns":"U","Cost":1,"Size":1,"Doc":"1 if A is greater than or equal to B, else 0. A and B are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":168,"Name":"b==","Args":"BB","Returns":"U","Cost":1,"Size":1,"Doc":"1 if A is equal to B, else 0. A and B are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":169,"Name":"b!=","Args":"BB","Returns":"U","Cost":1,"Size":1,"Doc":"0 if A is equal to B, else 1. A and B are interpreted as big-endian unsigned integers","Groups":["Byte Array Arithmetic"]},{"Opcode":170,"Name":"b%","Args":"BB","Returns":"B","Cost":20,"Size":1,"Doc":"A modulo B. A and B are interpreted as big-endian unsigned integers. Fail if B is zero.","Groups":["Byte Array Arithmetic"]},{"Opcode":171,"Name":"b|","Args":"BB","Returns":"B","Cost":6,"Size":1,"Doc":"A bitwise-or B. A and B are zero-left extended to the greater of their lengths","Groups":["Byte Array Logic"]},{"Opcode":172,"Name":"b\u0026","Args":"BB","Returns":"B","Cost":6,"Size":1,"Doc":"A bitwise-and B. A and B are zero-left extended to the greater of their lengths","Groups":["Byte Array Logic"]},{"Opcode":173,"Name":"b^","Args":"BB","Returns":"B","Cost":6,"Size":1,"Doc":"A bitwise-xor B. A and B are zero-left extended to the greater of their lengths","Groups":["Byte Array Logic"]},{"Opcode":174,"Name":"b~","Args":"B","Returns":"B","Cost":4,"Size":1,"Doc":"A with all bits inverted","Groups":["Byte Array Logic"]},{"Opcode":175,"Name":"bzero","Args":"U","Returns":"B","Cost":1,"Size":1,"Doc":"zero filled byte-array of length A","Groups":["Loading Values"]},{"Opcode":176,"Name":"log","Args":"B","Cost":1,"Size":1,"Doc":"write A to log state of the current application","DocExtra":"`log` fails if called more than MaxLogCalls times in a program, or if the sum of logged bytes exceeds 1024 bytes.","Groups":["State Access"]},{"Opcode":177,"Name":"itxn_begin","Cost":1,"Size":1,"Doc":"begin preparation of a new inner transaction in a new transaction group","DocExtra":"`itxn_begin` initializes Sender to the application address; Fee to the minimum allowable, taking into account MinTxnFee and credit from overpaying in earlier transactions; FirstValid/LastValid to the values in the invoking transaction, and all other fields to zero or empty values.","Groups":["Inner Transactions"]},{"Opcode":178,"Name":"itxn_field","Args":".","Cost":1,"Size":2,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice","ExtraProgramPages","Nonparticipation","Logs","NumLogs","CreatedAssetID","CreatedApplicationID","LastLog","StateProofPK"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUUUUBUUUBB","Doc":"set field F of the current inner transaction to A","DocExtra":"`itxn_field` fails if A is of the wrong type for F, including a byte array of the wrong size for use as an address when F is an address field. `itxn_field` also fails if A is an account, asset, or app that is not _available_, or an attempt is made extend an array field beyond the limit imposed by consensus parameters. (Addresses set into asset params of acfg transactions need not be _available_.)","ImmediateNote":"{uint8 transaction field index}","Groups":["Inner Transactions"]},{"Opcode":179,"Name":"itxn_submit","Cost":1,"Size":1,"Doc":"execute the current inner transaction group. Fail if executing this group would exceed the inner transaction limit, or if any transaction in the group fails.","DocExtra":"`itxn_submit` resets the current transaction so that it can not be resubmitted. A new `itxn_begin` is required to prepare another inner transaction.","Groups":["Inner Transactions"]},{"Opcode":180,"Name":"itxn","Returns":".","Cost":1,"Size":2,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice","ExtraProgramPages","Nonparticipation","Logs","NumLogs","CreatedAssetID","CreatedApplicationID","LastLog","StateProofPK"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUUUUBUUUBB","Doc":"field F of the last inner transaction","ImmediateNote":"{uint8 transaction field index}","Groups":["Inner Transactions"]},{"Opcode":181,"Name":"itxna","Returns":".","Cost":1,"Size":3,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Ith value of the array field F of the last inner transaction","ImmediateNote":"{uint8 transaction field index} {uint8 transaction field array index}","Groups":["Inner Transactions"]},{"Opcode":182,"Name":"itxn_next","Cost":1,"Size":1,"Doc":"begin preparation of a new inner transaction in the same transaction group","DocExtra":"`itxn_next` initializes the transaction exactly as `itxn_begin` does","Groups":["Inner Transactions"]},{"Opcode":183,"Name":"gitxn","Returns":".","Cost":1,"Size":3,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice","ExtraProgramPages","Nonparticipation","Logs","NumLogs","CreatedAssetID","CreatedApplicationID","LastLog","StateProofPK"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUUUUBUUUBB","Doc":"field F of the Tth transaction in the last inner group submitted","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index}","Groups":["Inner Transactions"]},{"Opcode":184,"Name":"gitxna","Returns":".","Cost":1,"Size":4,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Ith value of the array field F from the Tth transaction in the last inner group submitted","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index} {uint8 transaction field array index}","Groups":["Inner Transactions"]},{"Opcode":192,"Name":"txnas","Args":"U","Returns":".","Cost":1,"Size":2,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Ath value of the array field F of the current transaction","ImmediateNote":"{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":193,"Name":"gtxnas","Args":"U","Returns":".","Cost":1,"Size":3,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Ath value of the array field F from the Tth transaction in the current group","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":194,"Name":"gtxnsas","Args":"UU","Returns":".","Cost":1,"Size":2,"ArgEnum":["ApplicationArgs","Accounts","Assets","Applications","Logs"],"ArgEnumTypes":"BBUUB","Doc":"Bth value of the array field F from the Ath transaction in the current group","ImmediateNote":"{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":195,"Name":"args","Args":"U","Returns":"B","Cost":1,"Size":1,"Doc":"Ath LogicSig argument","Groups":["Loading Values"]},{"Opcode":196,"Name":"gloadss","Args":"UU","Returns":".","Cost":1,"Size":1,"Doc":"Bth scratch space value of the Ath transaction in the current group","Groups":["Loading Values"]},{"Opcode":197,"Name":"itxnas","Args":"U","Returns":".","Cost":1,"Size":2,"Doc":"Ath value of the array field F of the last inner transaction","ImmediateNote":"{uint8 transaction field index}","Groups":["Inner Transactions"]},{"Opcode":198,"Name":"gitxnas","Args":"U","Returns":".","Cost":1,"Size":3,"Doc":"Ath value of the array field F from the Tth transaction in the last inner group submitted","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index}","Groups":["Inner Transactions"]}]} diff --git a/algosdk/dryrun_results.py b/algosdk/dryrun_results.py index cc0efdd6..b8594fbb 100644 --- a/algosdk/dryrun_results.py +++ b/algosdk/dryrun_results.py @@ -1,5 +1,5 @@ import base64 -from typing import List +from typing import List, Optional, cast class StackPrinterConfig: @@ -33,13 +33,13 @@ def __init__(self, dr): self.disassembly = dr["disassembly"] + # cost is separated into 2 fields: `budget-added` and `budget-consumed` optionals = [ "app-call-messages", "local-deltas", "global-delta", "budget-added", "budget-consumed", - "cost", "logic-sig-messages", "logic-sig-disassembly", "logs", @@ -63,13 +63,13 @@ def attrname(field): def app_call_rejected(self) -> bool: return ( False - if self.app_call_messages is None - else "REJECT" in self.app_call_messages + if self.app_call_messages is None # type: ignore[attr-defined] # dynamic attribute + else "REJECT" in self.app_call_messages # type: ignore[attr-defined] # dynamic attribute ) def logic_sig_rejected(self) -> bool: - if self.logic_sig_messages is not None: - return "REJECT" in self.logic_sig_messages + if self.logic_sig_messages is not None: # type: ignore[attr-defined] # dynamic attribute + return "REJECT" in self.logic_sig_messages # type: ignore[attr-defined] # dynamic attribute return False @classmethod @@ -123,16 +123,17 @@ def trace( return "\n".join(trace) + "\n" - def app_trace(self, spc: StackPrinterConfig = None) -> str: + def app_trace(self, spc: Optional[StackPrinterConfig] = None) -> str: if not hasattr(self, "app_call_trace"): return "" if spc == None: spc = StackPrinterConfig(top_of_stack_first=False) + spc = cast(StackPrinterConfig, spc) return self.trace(self.app_call_trace, self.disassembly, spc=spc) - def lsig_trace(self, spc: StackPrinterConfig = None) -> str: + def lsig_trace(self, spc: Optional[StackPrinterConfig] = None) -> str: if not hasattr(self, "logic_sig_trace"): return "" @@ -143,7 +144,7 @@ def lsig_trace(self, spc: StackPrinterConfig = None) -> str: spc = StackPrinterConfig(top_of_stack_first=False) return self.trace( - self.logic_sig_trace, self.logic_sig_disassembly, spc=spc + self.logic_sig_trace, self.logic_sig_disassembly, spc=spc # type: ignore[attr-defined] # dynamic attribute ) @@ -151,9 +152,6 @@ class DryrunTrace: def __init__(self, trace: List[dict]): self.trace = [DryrunTraceLine(line) for line in trace] - def get_trace(self) -> List[str]: - return [line.trace_line() for line in self.trace] - class DryrunTraceLine: def __init__(self, tl): @@ -182,10 +180,13 @@ def __str__(self) -> str: return "0x" + base64.b64decode(self.bytes).hex() return str(self.int) - def __eq__(self, other: "DryrunStackValue"): + def __eq__(self, other: object): return ( - self.type == other.type + hasattr(other, "type") + and self.type == other.type + and hasattr(other, "bytes") and self.bytes == other.bytes + and hasattr(other, "int") and self.int == other.int ) @@ -202,7 +203,7 @@ def scratch_to_string( if not curr_scratch: return "" - new_idx = None + new_idx: Optional[int] = None for idx in range(len(curr_scratch)): if idx >= len(prev_scratch): new_idx = idx @@ -214,6 +215,7 @@ def scratch_to_string( if new_idx == None: return "" + new_idx = cast(int, new_idx) # discharge None type return "{} = {}".format(new_idx, curr_scratch[new_idx]) diff --git a/algosdk/encoding.py b/algosdk/encoding.py index 807b3452..d4dfa05f 100644 --- a/algosdk/encoding.py +++ b/algosdk/encoding.py @@ -1,12 +1,11 @@ import base64 -import warnings from collections import OrderedDict from typing import Union import msgpack from Cryptodome.Hash import SHA512 -from algosdk import auction, constants, error, future, transaction +from algosdk import auction, constants, error, transaction def msgpack_encode(obj): @@ -56,51 +55,8 @@ def _sort_dict(d): return od -def future_msgpack_decode(enc): - """ - Decode a msgpack encoded object from a string. - - Args: - enc (str): string to be decoded - - Returns: - Transaction, SignedTransaction, Multisig, Bid, or SignedBid:\ - decoded object - """ - decoded = enc - if not isinstance(enc, dict): - decoded = msgpack.unpackb(base64.b64decode(enc), raw=False) - if "type" in decoded: - return future.transaction.Transaction.undictify(decoded) - if "l" in decoded: - return future.transaction.LogicSig.undictify(decoded) - if "msig" in decoded: - return future.transaction.MultisigTransaction.undictify(decoded) - if "lsig" in decoded: - if "txn" in decoded: - return future.transaction.LogicSigTransaction.undictify(decoded) - return future.transaction.LogicSigAccount.undictify(decoded) - if "sig" in decoded: - return future.transaction.SignedTransaction.undictify(decoded) - if "txn" in decoded: - return future.transaction.Transaction.undictify(decoded["txn"]) - if "subsig" in decoded: - return future.transaction.Multisig.undictify(decoded) - if "txlist" in decoded: - return future.transaction.TxGroup.undictify(decoded) - if "t" in decoded: - return auction.NoteField.undictify(decoded) - if "bid" in decoded: - return auction.SignedBid.undictify(decoded) - if "auc" in decoded: - return auction.Bid.undictify(decoded) - - def msgpack_decode(enc): """ - NOTE: This method is deprecated: - Please use `future_msgpack_decode` instead. - Decode a msgpack encoded object from a string. Args: @@ -110,11 +66,6 @@ def msgpack_decode(enc): Transaction, SignedTransaction, Multisig, Bid, or SignedBid:\ decoded object """ - warnings.warn( - "`msgpack_decode` is being deprecated. " - "Please use `future_msgpack_decode` instead.", - DeprecationWarning, - ) decoded = enc if not isinstance(enc, dict): decoded = msgpack.unpackb(base64.b64decode(enc), raw=False) @@ -125,7 +76,9 @@ def msgpack_decode(enc): if "msig" in decoded: return transaction.MultisigTransaction.undictify(decoded) if "lsig" in decoded: - return transaction.LogicSigTransaction.undictify(decoded) + if "txn" in decoded: + return transaction.LogicSigTransaction.undictify(decoded) + return transaction.LogicSigAccount.undictify(decoded) if "sig" in decoded: return transaction.SignedTransaction.undictify(decoded) if "txn" in decoded: diff --git a/algosdk/error.py b/algosdk/error.py index 9f5f7f69..284c6524 100644 --- a/algosdk/error.py +++ b/algosdk/error.py @@ -140,16 +140,6 @@ def __init__(self): ) -class WrongContractError(Exception): - def __init__(self, contract_type): - Exception.__init__( - self, - "Wrong contract provided; a " - + contract_type - + " contract is needed", - ) - - class OverspecifiedRoundError(Exception): def __init__(self, contract_type): Exception.__init__( @@ -178,14 +168,6 @@ def __init__(self, attr): Exception.__init__(self, attr + " should not be None") -class TemplateInputError(Exception): - pass - - -class TemplateError(Exception): - pass - - class KMDHTTPError(Exception): pass diff --git a/algosdk/future/__init__.py b/algosdk/future/__init__.py deleted file mode 100644 index 371c8b8e..00000000 --- a/algosdk/future/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import template -from . import transaction - -name = "future" diff --git a/algosdk/future/template.py b/algosdk/future/template.py deleted file mode 100644 index 77b424c1..00000000 --- a/algosdk/future/template.py +++ /dev/null @@ -1,733 +0,0 @@ -import math -import random -from .. import error, encoding, constants, logic, account -from . import transaction -from Cryptodome.Hash import SHA256, keccak -import base64 - - -class Template: - """ - NOTE: This class is deprecated - """ - - def get_address(self): - """ - Return the address of the contract. - """ - return logic.address(self.get_program()) - - def get_program(self): - pass - - -class Split(Template): - """ - NOTE: This class is deprecated. - - Split allows locking algos in an account which allows transfering to two - predefined addresses in a specified ratio such that for the given ratn and - ratd parameters we have: - - first_recipient_amount * rat_2 == second_recipient_amount * rat_1 - - Split also has an expiry round, after which the owner can transfer back - the funds. - - Arguments: - owner (str): an address that can receive the funds after the expiry - round - receiver_1 (str): first address to receive funds - receiver_2 (str): second address to receive funds - rat_1 (int): how much receiver_1 receives (proportionally) - rat_2 (int): how much receiver_2 receives (proportionally) - expiry_round (int): the round on which the funds can be transferred - back to owner - min_pay (int): the minimum number of microalgos that can be transferred - from the account to receiver_1 - max_fee (int): half the maximum fee that can be paid to the network by - the account - """ - - def __init__( - self, - owner: str, - receiver_1: str, - receiver_2: str, - rat_1: int, - rat_2: int, - expiry_round: int, - min_pay: int, - max_fee: int, - ): - self.owner = owner - self.receiver_1 = receiver_1 - self.receiver_2 = receiver_2 - self.rat_1 = rat_1 - self.rat_2 = rat_2 - self.expiry_round = expiry_round - self.min_pay = min_pay - self.max_fee = max_fee - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAIAQUCAAYHCAkmAyDYHIR7TIW5eM/WAZcXdEDqv7BD+baMN6i2/A5JatGbNCDKs" - "aoZHPQ3Zg8zZB/BZ1oDgt77LGo5np3rbto3/gloTyB40AS2H3I72YCbDk4hKpm7J7" - "NnFy2Xrt39TJG0ORFg+zEQIhIxASMMEDIEJBJAABkxCSgSMQcyAxIQMQglEhAxAiE" - "EDRAiQAAuMwAAMwEAEjEJMgMSEDMABykSEDMBByoSEDMACCEFCzMBCCEGCxIQMwAI" - "IQcPEBA=" - ) - orig = base64.b64decode(orig) - offsets = [4, 7, 8, 9, 10, 14, 47, 80] - values = [ - self.max_fee, - self.expiry_round, - self.rat_2, - self.rat_1, - self.min_pay, - self.owner, - self.receiver_1, - self.receiver_2, - ] - types = [int, int, int, int, int, "address", "address", "address"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_split_funds_transaction(contract, amount: int, sp): - """ - Return a group transactions array which transfers funds according to - the contract's ratio. - - Args: - amount (int): total amount to be transferred - sp (SuggestedParams): suggested params from algod - - Returns: - Transaction[] - """ - address = logic.address(contract) - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 8 and len(bytearrays) == 3): - raise error.WrongContractError("split") - rat_1 = ints[6] - rat_2 = ints[5] - min_pay = ints[7] - max_fee = ints[1] - receiver_1 = encoding.encode_address(bytearrays[1]) - receiver_2 = encoding.encode_address(bytearrays[2]) - - amt_1 = 0 - amt_2 = 0 - - gcd = math.gcd(rat_1, rat_2) - rat_1 = rat_1 // gcd - rat_2 = rat_2 // gcd - - if amount % (rat_1 + rat_2) == 0: - amt_1 = amount // (rat_1 + rat_2) * rat_1 - amt_2 = amount - amt_1 - else: - raise error.TemplateInputError( - "the specified amount cannot be split into two " - "parts with the ratio " + str(rat_1) + "/" + str(rat_2) - ) - - if amt_1 < min_pay: - raise error.TemplateInputError( - "the amount paid to receiver_1 must be greater than " - + str(min_pay) - ) - - txn_1 = transaction.PaymentTxn(address, sp, receiver_1, amt_1) - txn_2 = transaction.PaymentTxn(address, sp, receiver_2, amt_2) - - transaction.assign_group_id([txn_1, txn_2]) - - lsig = transaction.LogicSig(contract) - - stx_1 = transaction.LogicSigTransaction(txn_1, lsig) - stx_2 = transaction.LogicSigTransaction(txn_2, lsig) - - if txn_1.fee > max_fee or txn_2.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - return [stx_1, stx_2] - - -class HTLC(Template): - """ - NOTE: This class is deprecated. - - Hash Time Locked Contract allows a user to recieve the Algo prior to a - deadline (in terms of a round) by proving knowledge of a special value - or to forfeit the ability to claim, returning it to the payer. - This contract is usually used to perform cross-chained atomic swaps. - - More formally, algos can be transfered under only two circumstances: - 1. To receiver if hash_function(arg_0) = hash_value - 2. To owner if txn.FirstValid > expiry_round - - Args: - owner (str): an address that can receive the asset after the expiry - round - receiver (str): address to receive Algos - hash_function (str): the hash function to be used (must be either - sha256 or keccak256) - hash_image (str): the hash image in base64 - expiry_round (int): the round on which the assets can be transferred - back to owner - max_fee (int): the maximum fee that can be paid to the network by the - account - - """ - - def __init__( - self, - owner: str, - receiver: str, - hash_function: str, - hash_image: str, - expiry_round: int, - max_fee: int, - ): - self.owner = owner - self.receiver = receiver - self.hash_function = hash_function - self.hash_image = hash_image - self.expiry_round = expiry_round - self.max_fee = max_fee - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAEBQEABiYDIP68oLsUSlpOp7Q4pGgayA5soQW8tgf8VlMlyVaV9qITAQYg5pqWH" - "m8tX3rIZgeSZVK+mCNe0zNjyoiRi7nJOKkVtvkxASIOMRAjEhAxBzIDEhAxCCQSED" - "EJKBItASkSEDEJKhIxAiUNEBEQ" - ) - orig = base64.b64decode(orig) - hash_inject = 0 - if self.hash_function == "sha256": - hash_inject = 1 - elif self.hash_function == "keccak256": - hash_inject = 2 - offsets = [3, 6, 10, 42, 45, 102] - values = [ - self.max_fee, - self.expiry_round, - self.receiver, - self.hash_image, - self.owner, - hash_inject, - ] - types = [int, int, "address", "base64", "address", int] - return inject(orig, offsets, values, types) - - @staticmethod - def get_transaction(contract, preimage, sp): - """ - Return a transaction which will release funds if a matching preimage - is used. - - Args: - contract (bytes): the contract containing information, should be - received from payer - preimage (str): the preimage of the hash in base64 - sp (SuggestedParams): suggested params from algod - - Returns: - LogicSigTransaction: transaction to claim algos from - contract account - """ - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 4 and len(bytearrays) == 3): - raise error.WrongContractError("split") - max_fee = ints[0] - hash_function = contract[-15] - expected_hash_image = bytearrays[1] - if hash_function == 1: - hash_image = SHA256.new() - hash_image.update(base64.b64decode(preimage)) - if hash_image.digest() != expected_hash_image: - raise error.TemplateInputError( - "the hash of the preimage does not match the expected " - "hash image using hash function sha256" - ) - elif hash_function == 2: - hash_image = keccak.new(digest_bits=256) - hash_image.update(base64.b64decode(preimage)) - if hash_image.digest() != expected_hash_image: - raise error.TemplateInputError( - "the hash of the preimage does not match the expected " - "hash image using hash function keccak256" - ) - else: - raise error.TemplateInputError( - "an invalid hash function was provided in the contract" - ) - - receiver = encoding.encode_address(bytearrays[0]) - zero_receiver = ( - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ" - ) - - lsig = transaction.LogicSig(contract, [base64.b64decode(preimage)]) - txn = transaction.PaymentTxn( - logic.address(contract), - sp, - zero_receiver, - 0, - close_remainder_to=receiver, - ) - - if txn.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - ltxn = transaction.LogicSigTransaction(txn, lsig) - return ltxn - - -class DynamicFee(Template): - """ - NOTE: This class is deprecated. - - DynamicFee contract allows you to create a transaction without - specifying the fee. The fee will be determined at the moment of - transfer. - - Args: - receiver (str): address to receive the assets - amount (int): amount of assets to transfer - sp (SuggestedParams): suggested params from algod - close_remainder_address (str, optional): the address that recieves the - remainder - """ - - def __init__( - self, - receiver: str, - amount: int, - sp, - close_remainder_address: str = None, - ): - self.lease_value = bytes( - [random.randint(0, 255) for x in range(constants.lease_length)] - ) - - self.last_valid = sp.last - self.amount = amount - self.first_valid = sp.first - self.close_remainder_address = close_remainder_address - self.receiver = receiver - self.gen = sp.gen - self.gh = sp.gh - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAFAgEFBgcmAyD+vKC7FEpaTqe0OKRoGsgObKEFvLYH/FZTJclWlfaiEyDmmpYeb" - "y1feshmB5JlUr6YI17TM2PKiJGLuck4qRW2+QEGMgQiEjMAECMSEDMABzEAEhAzAA" - "gxARIQMRYjEhAxECMSEDEHKBIQMQkpEhAxCCQSEDECJRIQMQQhBBIQMQYqEhA=" - ) - orig = base64.b64decode(orig) - offsets = [5, 6, 7, 11, 44, 76] - close = self.close_remainder_address - if close is None: - close = encoding.encode_address(bytes(32)) - values = [ - self.amount, - self.first_valid, - self.last_valid, - self.receiver, - close, - base64.b64encode(self.lease_value), - ] - types = [int, int, int, "address", "address", "base64"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_transactions(txn, lsig, private_key, fee): - """ - Create and sign the secondary dynamic fee transaction, update - transaction fields, and sign as the fee payer; return both - transactions. - - Args: - txn (Transaction): main transaction from payer - lsig (LogicSig): signed logic received from payer - private_key (str): the secret key of the account that pays the fee - in base64 - fee (int): fee per byte, for both transactions - """ - txn.fee = fee - txn.fee = max(constants.min_txn_fee, fee * txn.estimate_size()) - - # reimbursement transaction - sp = transaction.SuggestedParams( - fee, - txn.first_valid_round, - txn.last_valid_round, - txn.genesis_hash, - txn.genesis_id, - ) - address = account.address_from_private_key(private_key) - txn_2 = transaction.PaymentTxn( - address, sp, txn.sender, txn.fee, lease=txn.lease - ) - - transaction.assign_group_id([txn_2, txn]) - - stx_1 = transaction.LogicSigTransaction(txn, lsig) - stx_2 = txn_2.sign(private_key) - - return [stx_2, stx_1] - - def sign_dynamic_fee(self, private_key): - """ - Return the main transaction and signed logic needed to complete the - transfer. These should be sent to the fee payer, who can use - get_transactions() to update fields and create the auxiliary - transaction. - - Args: - private_key (bytes): the secret key to sign the contract in base64 - """ - sender = account.address_from_private_key(private_key) - - # main transaction - sp = transaction.SuggestedParams( - 0, self.first_valid, self.last_valid, self.gh, self.gen - ) - txn = transaction.PaymentTxn( - sender, - sp, - self.receiver, - self.amount, - lease=self.lease_value, - close_remainder_to=self.close_remainder_address, - ) - lsig = transaction.LogicSig(self.get_program()) - lsig.sign(private_key) - - return txn, lsig - - -class PeriodicPayment(Template): - """ - NOTE: This class is deprecated. - - PeriodicPayment contract enables creating an account which allows the - withdrawal of a fixed amount of assets every fixed number of rounds to a - specific Algrorand Address. In addition, the contract allows to add - timeout, after which the address can withdraw the rest of the assets. - - Args: - receiver (str): address to receive the assets - amount (int): amount of assets to transfer at every cycle - withdrawing_window (int): the number of blocks in which the user can - withdraw the asset once the period start (must be < 1000) - period (int): how often the address can withdraw assets (in rounds) - fee (int): maximum fee per transaction - timeout (int): a round in which the receiver can withdraw the rest of - the funds after - """ - - def __init__( - self, - receiver: str, - amount: int, - withdrawing_window: int, - period: int, - max_fee: int, - timeout: int, - ): - self.lease_value = bytes( - [random.randint(0, 255) for x in range(constants.lease_length)] - ) - self.receiver = receiver - self.amount = amount - self.withdrawing_window = withdrawing_window - self.period = period - self.max_fee = max_fee - self.timeout = timeout - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAHAQoLAAwNDiYCAQYg/ryguxRKWk6ntDikaBrIDmyhBby2B/xWUyXJVpX2ohMxE" - "CISMQEjDhAxAiQYJRIQMQQhBDECCBIQMQYoEhAxCTIDEjEHKRIQMQghBRIQMQkpEj" - "EHMgMSEDECIQYNEDEIJRIQERA=" - ) - orig = base64.b64decode(orig) - offsets = [4, 5, 7, 8, 9, 12, 15] - values = [ - self.max_fee, - self.period, - self.withdrawing_window, - self.amount, - self.timeout, - base64.b64encode(self.lease_value), - self.receiver, - ] - types = [int, int, int, int, int, "base64", "address"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_withdrawal_transaction(contract, sp): - """ - Return the withdrawal transaction to be sent to the network. - - Args: - contract (bytes): contract containing information, should be - received from payer - sp (SuggestedParams): suggested params from algod; the value of - sp.last will not be used. Instead, the last valid round will - be calculated from first valid round and withdrawing window - """ - address = logic.address(contract) - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 7 and len(bytearrays) == 2): - raise error.WrongContractError("periodic payment") - amount = ints[5] - withdrawing_window = ints[4] - period = ints[2] - max_fee = ints[1] - lease_value = bytearrays[0] - receiver = encoding.encode_address(bytearrays[1]) - - if sp.first % period != 0: - raise error.TemplateInputError( - "first_valid must be divisible by the period" - ) - - sp_copy = transaction.SuggestedParams( - sp.fee, - sp.first, - sp.first + withdrawing_window, - sp.gh, - sp.gen, - flat_fee=sp.flat_fee, - ) - - txn = transaction.PaymentTxn( - address, sp_copy, receiver, amount, lease=lease_value - ) - - if txn.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - lsig = transaction.LogicSig(contract) - stx = transaction.LogicSigTransaction(txn, lsig) - return stx - - -class LimitOrder(Template): - """ - NOTE: This class is deprecated. - - Limit Order allows to trade Algos for other assets given a specific ratio; - for N Algos, swap for Rate * N Assets. - ... - - Args: - owner (str): an address that can receive the asset after the expiry - round - asset_id (int): asset to be transfered - ratn (int): the numerator of the exchange rate - ratd (int): the denominator of the exchange rate - expiry_round (int): the round on which the assets can be transferred - back to owner - max_fee (int): the maximum fee that can be paid to the network by the - account - min_trade (int): the minimum amount (of Algos) to be traded away - """ - - def __init__( - self, - owner: str, - asset_id: int, - ratn: int, - ratd: int, - expiry_round: int, - max_fee: int, - min_trade: int, - ): - self.owner = owner - self.ratn = ratn - self.ratd = ratd - self.expiry_round = expiry_round - self.min_trade = min_trade - self.max_fee = max_fee - self.asset_id = asset_id - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAKAAEFAgYEBwgJHSYBIJKvkYTkEzwJf2arzJOxERsSogG9nQzKPkpIoc4TzPTFM" - "RYiEjEQIxIQMQEkDhAyBCMSQABVMgQlEjEIIQQNEDEJMgMSEDMBECEFEhAzAREhBh" - "IQMwEUKBIQMwETMgMSEDMBEiEHHTUCNQExCCEIHTUENQM0ATQDDUAAJDQBNAMSNAI" - "0BA8QQAAWADEJKBIxAiEJDRAxBzIDEhAxCCISEBA=" - ) - orig = base64.b64decode(orig) - offsets = [5, 7, 9, 10, 11, 12, 16] - values = [ - self.max_fee, - self.min_trade, - self.asset_id, - self.ratd, - self.ratn, - self.expiry_round, - self.owner, - ] - types = [int, int, int, int, int, int, "address"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_swap_assets_transactions( - contract: bytes, - asset_amount: int, - microalgo_amount: int, - private_key: str, - sp, - ): - """ - Return a group transactions array which transfer funds according to - the contract's ratio. - - Args: - contract (bytes): the contract containing information, should be - received from payer - asset_amount (int): the amount of assets to be sent - microalgo_amount (int): the amount of microalgos to be received - private_key (str): the secret key to sign the contract - sp (SuggestedParams): suggested params from algod - """ - address = logic.address(contract) - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 10 and len(bytearrays) == 1): - raise error.WrongContractError("limit order") - min_trade = ints[4] - asset_id = ints[6] - ratn = ints[8] - ratd = ints[7] - max_fee = ints[2] - owner = encoding.encode_address(bytearrays[0]) - - if microalgo_amount < min_trade: - raise error.TemplateInputError( - "At least " + str(min_trade) + " microalgos must be requested" - ) - - if asset_amount * ratd < microalgo_amount * ratn: - raise error.TemplateInputError( - "The exchange ratio of assets to microalgos must be at least " - + str(ratn) - + " / " - + str(ratd) - ) - - txn_1 = transaction.PaymentTxn( - address, - sp, - account.address_from_private_key(private_key), - int(microalgo_amount), - ) - - txn_2 = transaction.AssetTransferTxn( - account.address_from_private_key(private_key), - sp, - owner, - asset_amount, - asset_id, - ) - - if txn_1.fee > max_fee or txn_2.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - if txn_1.fee > max_fee or txn_2.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - transaction.assign_group_id([txn_1, txn_2]) - - lsig = transaction.LogicSig(contract) - stx_1 = transaction.LogicSigTransaction(txn_1, lsig) - stx_2 = txn_2.sign(private_key) - - return [stx_1, stx_2] - - -def put_uvarint(buf, x): - i = 0 - while x >= 0x80: - buf.append((x & 0xFF) | 0x80) - x >>= 7 - i += 1 - - buf.append(x & 0xFF) - return i + 1 - - -def inject(orig, offsets, values, values_types): - # make sure we have enough values - assert len(offsets) == len(values) == len(values_types) - - res = orig[:] - - def replace(arr, new_val, offset, place_holder_length): - return arr[:offset] + new_val + arr[offset + place_holder_length :] - - for i in range(len(offsets)): - val = values[i] - val_type = values_types[i] - dec_len = 0 - - if val_type == int: - buf = [] - dec_len = put_uvarint(buf, val) - 1 - val = bytes(buf) - res = replace(res, val, offsets[i], 1) - - elif val_type == "address": - val = encoding.decode_address(val) - res = replace(res, val, offsets[i], 32) - - elif val_type == "base64": - val = bytes(base64.b64decode(val)) - buf = [] - dec_len = put_uvarint(buf, len(val)) + len(val) - 2 - res = replace(res, bytes(buf) + val, offsets[i], 2) - - else: - raise Exception("Unkown Type") - - # update offsets - if dec_len != 0: - for o in range(len(offsets)): - offsets[o] += dec_len - - return res diff --git a/algosdk/future/transaction.py b/algosdk/future/transaction.py deleted file mode 100644 index 15eab421..00000000 --- a/algosdk/future/transaction.py +++ /dev/null @@ -1,3374 +0,0 @@ -import base64 -import binascii -import warnings -import msgpack -from enum import IntEnum -from typing import List, Union -from collections import OrderedDict - -from algosdk import account, constants, encoding, error, logic, transaction -from algosdk.box_reference import BoxReference -from algosdk.v2client import algod, models -from nacl.exceptions import BadSignatureError -from nacl.signing import SigningKey, VerifyKey - - -class SuggestedParams: - """ - Contains various fields common to all transaction types. - - Args: - fee (int): transaction fee (per byte if flat_fee is false). When flat_fee is true, - fee may fall to zero but a group of N atomic transactions must - still have a fee of at least N*min_txn_fee. - first (int): first round for which the transaction is valid - last (int): last round for which the transaction is valid - gh (str): genesis hash - gen (str, optional): genesis id - flat_fee (bool, optional): whether the specified fee is a flat fee - consensus_version (str, optional): the consensus protocol version as of 'first' - min_fee (int, optional): the minimum transaction fee (flat) - - Attributes: - fee (int) - first (int) - last (int) - gen (str) - gh (str) - flat_fee (bool) - consensus_version (str) - min_fee (int) - """ - - def __init__( - self, - fee, - first, - last, - gh, - gen=None, - flat_fee=False, - consensus_version=None, - min_fee=None, - ): - self.first = first - self.last = last - self.gh = gh - self.gen = gen - self.fee = fee - self.flat_fee = flat_fee - self.consensus_version = consensus_version - self.min_fee = min_fee - - -class Transaction: - """ - Superclass for various transaction types. - """ - - def __init__(self, sender, sp, note, lease, txn_type, rekey_to): - self.sender = sender - self.fee = sp.fee - self.first_valid_round = sp.first - self.last_valid_round = sp.last - self.note = self.as_note(note) - self.genesis_id = sp.gen - self.genesis_hash = sp.gh - self.group = None - self.lease = self.as_lease(lease) - self.type = txn_type - self.rekey_to = rekey_to - - @staticmethod - def as_hash(hash): - """Confirm that a value is 32 bytes. If all zeros, or a falsy value, return None""" - if not hash: - return None - assert isinstance(hash, (bytes, bytearray)), "{} is not bytes".format( - hash - ) - if len(hash) != constants.hash_len: - raise error.WrongHashLengthError - if not any(hash): - return None - return hash - - @staticmethod - def as_note(note): - if not note: - return None - if not isinstance(note, (bytes, bytearray, str)): - raise error.WrongNoteType - if isinstance(note, str): - note = note.encode() - if len(note) > constants.note_max_length: - raise error.WrongNoteLength - return note - - @classmethod - def as_lease(cls, lease): - try: - return cls.as_hash(lease) - except error.WrongHashLengthError: - raise error.WrongLeaseLengthError - - def get_txid(self): - """ - Get the transaction's ID. - - Returns: - str: transaction ID - """ - txn = encoding.msgpack_encode(self) - to_sign = constants.txid_prefix + base64.b64decode(txn) - txid = encoding.checksum(to_sign) - txid = base64.b32encode(txid).decode() - return encoding._undo_padding(txid) - - def sign(self, private_key): - """ - Sign the transaction with a private key. - - Args: - private_key (str): the private key of the signing account - - Returns: - SignedTransaction: signed transaction with the signature - """ - sig = self.raw_sign(private_key) - sig = base64.b64encode(sig).decode() - authorizing_address = None - if not (self.sender == account.address_from_private_key(private_key)): - authorizing_address = account.address_from_private_key(private_key) - stx = SignedTransaction(self, sig, authorizing_address) - return stx - - def _sign_and_skip_rekey_check(self, private_key): - """ - Sign the transaction with a private key, skipping rekey check. - This is only used for size estimation. - - Args: - private_key (str): the private key of the signing account - - Returns: - SignedTransaction: signed transaction with the signature - """ - sig = self.raw_sign(private_key) - sig = base64.b64encode(sig).decode() - stx = SignedTransaction(self, sig) - return stx - - def raw_sign(self, private_key): - """ - Sign the transaction. - - Args: - private_key (str): the private key of the signing account - - Returns: - bytes: signature - """ - private_key = base64.b64decode(private_key) - txn = encoding.msgpack_encode(self) - to_sign = constants.txid_prefix + base64.b64decode(txn) - signing_key = SigningKey(private_key[: constants.key_len_bytes]) - signed = signing_key.sign(to_sign) - sig = signed.signature - return sig - - def estimate_size(self): - sk, _ = account.generate_account() - stx = self._sign_and_skip_rekey_check(sk) - return len(base64.b64decode(encoding.msgpack_encode(stx))) - - def dictify(self): - d = dict() - if self.fee: - d["fee"] = self.fee - if self.first_valid_round: - d["fv"] = self.first_valid_round - if self.genesis_id: - d["gen"] = self.genesis_id - d["gh"] = base64.b64decode(self.genesis_hash) - if self.group: - d["grp"] = self.group - d["lv"] = self.last_valid_round - if self.lease: - d["lx"] = self.lease - if self.note: - d["note"] = self.note - d["snd"] = encoding.decode_address(self.sender) - d["type"] = self.type - if self.rekey_to: - d["rekey"] = encoding.decode_address(self.rekey_to) - - return d - - @staticmethod - def undictify(d): - sp = SuggestedParams( - d["fee"] if "fee" in d else 0, - d["fv"] if "fv" in d else 0, - d["lv"], - base64.b64encode(d["gh"]).decode(), - d["gen"] if "gen" in d else None, - flat_fee=True, - ) - args = { - "sp": sp, - "sender": encoding.encode_address(d["snd"]), - "note": d["note"] if "note" in d else None, - "lease": d["lx"] if "lx" in d else None, - "rekey_to": encoding.encode_address(d["rekey"]) - if "rekey" in d - else None, - } - txn_type = d["type"] - if not isinstance(d["type"], str): - txn_type = txn_type.decode() - if txn_type == constants.payment_txn: - args.update(PaymentTxn._undictify(d)) - txn = PaymentTxn(**args) - elif txn_type == constants.keyreg_txn: - if "nonpart" in d and d["nonpart"]: - args.update(KeyregNonparticipatingTxn._undictify(d)) - txn = KeyregNonparticipatingTxn(**args) - else: - if ( - "votekey" not in d - and "selkey" not in d - and "votefst" not in d - and "votelst" not in d - and "votekd" not in d - ): - args.update(KeyregOfflineTxn._undictify(d)) - txn = KeyregOfflineTxn(**args) - else: - args.update(KeyregOnlineTxn._undictify(d)) - txn = KeyregOnlineTxn(**args) - elif txn_type == constants.assetconfig_txn: - args.update(AssetConfigTxn._undictify(d)) - txn = AssetConfigTxn(**args) - elif txn_type == constants.assetfreeze_txn: - args.update(AssetFreezeTxn._undictify(d)) - txn = AssetFreezeTxn(**args) - elif txn_type == constants.assettransfer_txn: - args.update(AssetTransferTxn._undictify(d)) - txn = AssetTransferTxn(**args) - elif txn_type == constants.appcall_txn: - args.update(ApplicationCallTxn._undictify(d)) - txn = ApplicationCallTxn(**args) - elif txn_type == constants.stateproof_txn: - # a state proof txn does not have these fields - args.pop("note"), args.pop("rekey_to"), args.pop("lease") - args.update(StateProofTxn._undictify(d)) - txn = StateProofTxn(**args) - if "grp" in d: - txn.group = d["grp"] - return txn - - def __eq__(self, other): - if not isinstance(other, (Transaction, transaction.Transaction)): - return False - if isinstance(other, transaction.Transaction): - warnings.warn( - "You are trying to check equality of an older `transaction` " - " format that is being deprecated. " - "Please use the v2 equivalent in `future.transaction` instead.", - DeprecationWarning, - ) - return ( - self.sender == other.sender - and self.fee == other.fee - and self.first_valid_round == other.first_valid_round - and self.last_valid_round == other.last_valid_round - and self.genesis_hash == other.genesis_hash - and self.genesis_id == other.genesis_id - and self.note == other.note - and self.group == other.group - and self.lease == other.lease - and self.type == other.type - and self.rekey_to == other.rekey_to - ) - - @staticmethod - def required(arg): - if not arg: - raise ValueError("{} supplied as a required argument".format(arg)) - return arg - - @staticmethod - def creatable_index(index, required=False): - """Coerce an index for apps or assets to an integer. - - By using this in all constructors, we allow callers to use - strings as indexes, check our convenience Txn types to ensure - index is set, and ensure that 0 is always used internally for - an unset id, not None, so __eq__ works properly. - """ - i = int(index or 0) - if i == 0 and required: - raise IndexError("Required an index") - if i < 0: - raise IndexError(i) - return i - - def __str__(self): - return str(self.__dict__) - - -class PaymentTxn(Transaction): - """ - Represents a payment transaction. - - Args: - sender (str): address of the sender - sp (SuggestedParams): suggested params from algod - receiver (str): address of the receiver - amt (int): amount in microAlgos to be sent - close_remainder_to (str, optional): if nonempty, account will be closed - and remaining algos will be sent to this address - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - note (bytes) - genesis_id (str) - genesis_hash (str) - group (bytes) - receiver (str) - amt (int) - close_remainder_to (str) - type (str) - lease (byte[32]) - rekey_to (str) - """ - - def __init__( - self, - sender, - sp, - receiver, - amt, - close_remainder_to=None, - note=None, - lease=None, - rekey_to=None, - ): - Transaction.__init__( - self, sender, sp, note, lease, constants.payment_txn, rekey_to - ) - if receiver: - self.receiver = receiver - else: - raise error.ZeroAddressError - - self.amt = amt - if (not isinstance(self.amt, int)) or self.amt < 0: - raise error.WrongAmountType - self.close_remainder_to = close_remainder_to - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - def dictify(self): - d = dict() - if self.amt: - d["amt"] = self.amt - if self.close_remainder_to: - d["close"] = encoding.decode_address(self.close_remainder_to) - - decoded_receiver = encoding.decode_address(self.receiver) - if any(decoded_receiver): - d["rcv"] = encoding.decode_address(self.receiver) - - d.update(super(PaymentTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - - return od - - @staticmethod - def _undictify(d): - args = { - "close_remainder_to": encoding.encode_address(d["close"]) - if "close" in d - else None, - "amt": d["amt"] if "amt" in d else 0, - "receiver": encoding.encode_address(d["rcv"]) - if "rcv" in d - else constants.ZERO_ADDRESS, - } - return args - - def __eq__(self, other): - if not isinstance(other, (PaymentTxn, transaction.PaymentTxn)): - return False - return ( - super(PaymentTxn, self).__eq__(other) - and self.receiver == other.receiver - and self.amt == other.amt - and self.close_remainder_to == other.close_remainder_to - ) - - -class KeyregTxn(Transaction): - """ - Represents a key registration transaction. - - Args: - sender (str): address of sender - sp (SuggestedParams): suggested params from algod - votekey (str): participation public key in base64 - selkey (str): VRF public key in base64 - votefst (int): first round to vote - votelst (int): last round to vote - votekd (int): vote key dilution - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - nonpart (bool, optional): mark the account non-participating if true - StateProofPK: state proof - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - note (bytes) - genesis_id (str) - genesis_hash (str) - group(bytes) - votepk (str) - selkey (str) - votefst (int) - votelst (int) - votekd (int) - type (str) - lease (byte[32]) - rekey_to (str) - nonpart (bool) - sprfkey (str) - """ - - def __init__( - self, - sender, - sp, - votekey, - selkey, - votefst, - votelst, - votekd, - note=None, - lease=None, - rekey_to=None, - nonpart=None, - sprfkey=None, - ): - Transaction.__init__( - self, sender, sp, note, lease, constants.keyreg_txn, rekey_to - ) - self.votepk = votekey - self.selkey = selkey - self.votefst = votefst - self.votelst = votelst - self.votekd = votekd - self.nonpart = nonpart - self.sprfkey = sprfkey - - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - def dictify(self): - d = {} - if self.selkey is not None: - d["selkey"] = base64.b64decode(self.selkey) - if self.votefst is not None: - d["votefst"] = self.votefst - if self.votekd is not None: - d["votekd"] = self.votekd - if self.votepk is not None: - d["votekey"] = base64.b64decode(self.votepk) - if self.votelst is not None: - d["votelst"] = self.votelst - if self.nonpart is not None: - d["nonpart"] = self.nonpart - if self.sprfkey is not None: - d["sprfkey"] = base64.b64decode(self.sprfkey) - - d.update(super(KeyregTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - - return od - - def __eq__(self, other): - if not isinstance(other, (KeyregTxn, transaction.KeyregTxn)): - return False - return ( - super(KeyregTxn, self).__eq__(other) - and self.votepk == other.votepk - and self.selkey == other.selkey - and self.votefst == other.votefst - and self.votelst == other.votelst - and self.votekd == other.votekd - and self.nonpart == other.nonpart - and self.sprfkey == other.sprfkey - ) - - -class KeyregOnlineTxn(KeyregTxn): - """ - Represents an online key registration transaction. - nonpart is implicitly False for this transaction. - - Args: - sender (str): address of sender - sp (SuggestedParams): suggested params from algod - votekey (str): participation public key in base64 - selkey (str): VRF public key in base64 - votefst (int): first round to vote - votelst (int): last round to vote - votekd (int): vote key dilution - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - sprfkey (str, optional): state proof ID - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - note (bytes) - genesis_id (str) - genesis_hash (str) - group(bytes) - votepk (str) - selkey (str) - votefst (int) - votelst (int) - votekd (int) - type (str) - lease (byte[32]) - rekey_to (str) - sprfkey (str) - """ - - def __init__( - self, - sender, - sp, - votekey, - selkey, - votefst, - votelst, - votekd, - note=None, - lease=None, - rekey_to=None, - sprfkey=None, - ): - KeyregTxn.__init__( - self, - sender, - sp, - votekey, - selkey, - votefst, - votelst, - votekd, - note, - lease, - rekey_to, - nonpart=False, - sprfkey=sprfkey, - ) - self.votepk = votekey - self.selkey = selkey - self.votefst = votefst - self.votelst = votelst - self.votekd = votekd - self.sprfkey = sprfkey - if votekey is None: - raise error.KeyregOnlineTxnInitError("votekey") - if selkey is None: - raise error.KeyregOnlineTxnInitError("selkey") - if votefst is None: - raise error.KeyregOnlineTxnInitError("votefst") - if votelst is None: - raise error.KeyregOnlineTxnInitError("votelst") - if votekd is None: - raise error.KeyregOnlineTxnInitError("votekd") - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - @staticmethod - def _undictify(d): - votekey = base64.b64encode(d["votekey"]).decode() - selkey = base64.b64encode(d["selkey"]).decode() - votefst = d["votefst"] - votelst = d["votelst"] - votekd = d["votekd"] - if "sprfkey" in d: - sprfID = base64.b64encode(d["sprfkey"]).decode() - - args = { - "votekey": votekey, - "selkey": selkey, - "votefst": votefst, - "votelst": votelst, - "votekd": votekd, - "sprfkey": sprfID, - } - else: - args = { - "votekey": votekey, - "selkey": selkey, - "votefst": votefst, - "votelst": votelst, - "votekd": votekd, - } - - return args - - def __eq__(self, other): - if not isinstance(other, KeyregOnlineTxn): - return False - return super(KeyregOnlineTxn, self).__eq__(other) - - -class KeyregOfflineTxn(KeyregTxn): - """ - Represents an offline key registration transaction. - nonpart is implicitly False for this transaction. - - Args: - sender (str): address of sender - sp (SuggestedParams): suggested params from algod - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - note (bytes) - genesis_id (str) - genesis_hash (str) - group(bytes) - type (str) - lease (byte[32]) - rekey_to (str) - """ - - def __init__(self, sender, sp, note=None, lease=None, rekey_to=None): - KeyregTxn.__init__( - self, - sender, - sp, - None, - None, - None, - None, - None, - note=note, - lease=lease, - rekey_to=rekey_to, - nonpart=False, - sprfkey=None, - ) - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - @staticmethod - def _undictify(d): - args = {} - return args - - def __eq__(self, other): - if not isinstance(other, KeyregOfflineTxn): - return False - return super(KeyregOfflineTxn, self).__eq__(other) - - -class KeyregNonparticipatingTxn(KeyregTxn): - """ - Represents a nonparticipating key registration transaction. - nonpart is implicitly True for this transaction. - - Args: - sender (str): address of sender - sp (SuggestedParams): suggested params from algod - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - note (bytes) - genesis_id (str) - genesis_hash (str) - group(bytes) - type (str) - lease (byte[32]) - rekey_to (str) - """ - - def __init__(self, sender, sp, note=None, lease=None, rekey_to=None): - KeyregTxn.__init__( - self, - sender, - sp, - None, - None, - None, - None, - None, - note=note, - lease=lease, - rekey_to=rekey_to, - nonpart=True, - sprfkey=None, - ) - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - @staticmethod - def _undictify(d): - args = {} - return args - - def __eq__(self, other): - if not isinstance(other, KeyregNonparticipatingTxn): - return False - return super(KeyregNonparticipatingTxn, self).__eq__(other) - - -class AssetConfigTxn(Transaction): - """ - Represents a transaction for asset creation, reconfiguration, or - destruction. - - To create an asset, include the following: - total, default_frozen, unit_name, asset_name, - manager, reserve, freeze, clawback, url, metadata, - decimals - - To destroy an asset, include the following: - index, strict_empty_address_check (set to False) - - To update asset configuration, include the following: - index, manager, reserve, freeze, clawback, - strict_empty_address_check (optional) - - Args: - sender (str): address of the sender - sp (SuggestedParams): suggested params from algod - index (int, optional): index of the asset - total (int, optional): total number of base units of this asset created - default_frozen (bool, optional): whether slots for this asset in user - accounts are frozen by default - unit_name (str, optional): hint for the name of a unit of this asset - asset_name (str, optional): hint for the name of the asset - manager (str, optional): address allowed to change nonzero addresses - for this asset - reserve (str, optional): account whose holdings of this asset should - be reported as "not minted" - freeze (str, optional): account allowed to change frozen state of - holdings of this asset - clawback (str, optional): account allowed take units of this asset - from any account - url (str, optional): a URL where more information about the asset - can be retrieved - metadata_hash (byte[32], optional): a commitment to some unspecified - asset metadata (32 byte hash) - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - strict_empty_address_check (bool, optional): set this to False if you - want to specify empty addresses. Otherwise, if this is left as - True (the default), having empty addresses will raise an error, - which will prevent accidentally removing admin access to assets or - deleting the asset. - decimals (int, optional): number of digits to use for display after - decimal. If set to 0, the asset is not divisible. If set to 1, the - base unit of the asset is in tenths. Must be between 0 and 19, - inclusive. Defaults to 0. - rekey_to (str, optional): additionally rekey the sender to this address - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - genesis_hash (str) - index (int) - total (int) - default_frozen (bool) - unit_name (str) - asset_name (str) - manager (str) - reserve (str) - freeze (str) - clawback (str) - url (str) - metadata_hash (byte[32]) - note (bytes) - genesis_id (str) - type (str) - lease (byte[32]) - decimals (int) - rekey (str) - """ - - def __init__( - self, - sender, - sp, - index=None, - total=None, - default_frozen=None, - unit_name=None, - asset_name=None, - manager=None, - reserve=None, - freeze=None, - clawback=None, - url=None, - metadata_hash=None, - note=None, - lease=None, - strict_empty_address_check=True, - decimals=0, - rekey_to=None, - ): - Transaction.__init__( - self, sender, sp, note, lease, constants.assetconfig_txn, rekey_to - ) - if strict_empty_address_check: - if not (manager and reserve and freeze and clawback): - raise error.EmptyAddressError - self.index = self.creatable_index(index) - self.total = int(total) if total else None - self.default_frozen = bool(default_frozen) - self.unit_name = unit_name - self.asset_name = asset_name - self.manager = manager - self.reserve = reserve - self.freeze = freeze - self.clawback = clawback - self.url = url - self.metadata_hash = self.as_metadata(metadata_hash) - self.decimals = int(decimals) - if self.decimals < 0 or self.decimals > constants.max_asset_decimals: - raise error.OutOfRangeDecimalsError - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - def dictify(self): - d = dict() - - if ( - self.total - or self.default_frozen - or self.unit_name - or self.asset_name - or self.manager - or self.reserve - or self.freeze - or self.clawback - or self.decimals - ): - apar = OrderedDict() - if self.metadata_hash: - apar["am"] = self.metadata_hash - if self.asset_name: - apar["an"] = self.asset_name - if self.url: - apar["au"] = self.url - if self.clawback: - apar["c"] = encoding.decode_address(self.clawback) - if self.decimals: - apar["dc"] = self.decimals - if self.default_frozen: - apar["df"] = self.default_frozen - if self.freeze: - apar["f"] = encoding.decode_address(self.freeze) - if self.manager: - apar["m"] = encoding.decode_address(self.manager) - if self.reserve: - apar["r"] = encoding.decode_address(self.reserve) - if self.total: - apar["t"] = self.total - if self.unit_name: - apar["un"] = self.unit_name - d["apar"] = apar - - if self.index: - d["caid"] = self.index - - d.update(super(AssetConfigTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - - return od - - @staticmethod - def _undictify(d): - index = None - total = None - default_frozen = None - unit_name = None - asset_name = None - manager = None - reserve = None - freeze = None - clawback = None - url = None - metadata_hash = None - decimals = 0 - - if "caid" in d: - index = d["caid"] - if "apar" in d: - if "t" in d["apar"]: - total = d["apar"]["t"] - if "df" in d["apar"]: - default_frozen = d["apar"]["df"] - if "un" in d["apar"]: - unit_name = d["apar"]["un"] - if "an" in d["apar"]: - asset_name = d["apar"]["an"] - if "m" in d["apar"]: - manager = encoding.encode_address(d["apar"]["m"]) - if "r" in d["apar"]: - reserve = encoding.encode_address(d["apar"]["r"]) - if "f" in d["apar"]: - freeze = encoding.encode_address(d["apar"]["f"]) - if "c" in d["apar"]: - clawback = encoding.encode_address(d["apar"]["c"]) - if "au" in d["apar"]: - url = d["apar"]["au"] - if "am" in d["apar"]: - metadata_hash = d["apar"]["am"] - if "dc" in d["apar"]: - decimals = d["apar"]["dc"] - - args = { - "index": index, - "total": total, - "default_frozen": default_frozen, - "unit_name": unit_name, - "asset_name": asset_name, - "manager": manager, - "reserve": reserve, - "freeze": freeze, - "clawback": clawback, - "url": url, - "metadata_hash": metadata_hash, - "strict_empty_address_check": False, - "decimals": decimals, - } - - return args - - def __eq__(self, other): - if not isinstance(other, (AssetConfigTxn, transaction.AssetConfigTxn)): - return False - return ( - super(AssetConfigTxn, self).__eq__(other) - and self.index == other.index - and self.total == other.total - and self.default_frozen == other.default_frozen - and self.unit_name == other.unit_name - and self.asset_name == other.asset_name - and self.manager == other.manager - and self.reserve == other.reserve - and self.freeze == other.freeze - and self.clawback == other.clawback - and self.url == other.url - and self.metadata_hash == other.metadata_hash - and self.decimals == other.decimals - ) - - @classmethod - def as_metadata(cls, md): - try: - return cls.as_hash(md) - except error.WrongHashLengthError: - raise error.WrongMetadataLengthError - - -class AssetCreateTxn(AssetConfigTxn): - """Represents a transaction for asset creation. - - Keyword arguments are required, starting with the special - addresses, to prevent errors, as type checks can't prevent simple - confusion of similar typed arguments. Since the special addresses - are required, strict_empty_address_check is turned off. - - Args: - sender (str): address of the sender - sp (SuggestedParams): suggested params from algod - total (int): total number of base units of this asset created - decimals (int, optional): number of digits to use for display after - decimal. If set to 0, the asset is not divisible. If set to 1, the - base unit of the asset is in tenths. Must be between 0 and 19, - inclusive. Defaults to 0. - default_frozen (bool): whether slots for this asset in user - accounts are frozen by default - manager (str): address allowed to change nonzero addresses - for this asset - reserve (str): account whose holdings of this asset should - be reported as "not minted" - freeze (str): account allowed to change frozen state of - holdings of this asset - clawback (str): account allowed take units of this asset - from any account - unit_name (str): hint for the name of a unit of this asset - asset_name (str): hint for the name of the asset - url (str): a URL where more information about the asset - can be retrieved - metadata_hash (byte[32], optional): a commitment to some unspecified - asset metadata (32 byte hash) - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - - """ - - def __init__( - self, - sender, - sp, - total, - decimals, - default_frozen, - *, - manager=None, - reserve=None, - freeze=None, - clawback=None, - unit_name="", - asset_name="", - url="", - metadata_hash=None, - note=None, - lease=None, - rekey_to=None, - ): - super().__init__( - sender=sender, - sp=sp, - total=total, - decimals=decimals, - default_frozen=default_frozen, - manager=manager, - reserve=reserve, - freeze=freeze, - clawback=clawback, - unit_name=unit_name, - asset_name=asset_name, - url=url, - metadata_hash=metadata_hash, - note=note, - lease=lease, - rekey_to=rekey_to, - strict_empty_address_check=False, - ) - - -class AssetDestroyTxn(AssetConfigTxn): - """Represents a transaction for asset destruction. - - An asset destruction transaction can only be sent by the manager - address, and only when the manager possseses all units of the - asset. - - """ - - def __init__( - self, sender, sp, index, note=None, lease=None, rekey_to=None - ): - super().__init__( - sender=sender, - sp=sp, - index=self.creatable_index(index), - note=note, - lease=lease, - rekey_to=rekey_to, - strict_empty_address_check=False, - ) - - -class AssetUpdateTxn(AssetConfigTxn): - """Represents a transaction for asset modification. - - To update asset configuration, include the following: - index, manager, reserve, freeze, clawback. - - Keyword arguments are required, starting with the special - addresses, to prevent argument reordinering errors. Since the - special addresses are required, strict_empty_address_check is - turned off. - - Args: - sender (str): address of the sender - sp (SuggestedParams): suggested params from algod - index (int): index of the asset to reconfigure - manager (str): address allowed to change nonzero addresses - for this asset - reserve (str): account whose holdings of this asset should - be reported as "not minted" - freeze (str): account allowed to change frozen state of - holdings of this asset - clawback (str): account allowed take units of this asset - from any account - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - - """ - - def __init__( - self, - sender, - sp, - index, - *, - manager, - reserve, - freeze, - clawback, - note=None, - lease=None, - rekey_to=None, - ): - super().__init__( - sender=sender, - sp=sp, - index=self.creatable_index(index), - manager=manager, - reserve=reserve, - freeze=freeze, - clawback=clawback, - note=note, - lease=lease, - rekey_to=rekey_to, - strict_empty_address_check=False, - ) - - -class AssetFreezeTxn(Transaction): - - """ - Represents a transaction for freezing or unfreezing an account's asset - holdings. Must be issued by the asset's freeze manager. - - Args: - sender (str): address of the sender, who must be the asset's freeze - manager - sp (SuggestedParams): suggested params from algod - index (int): index of the asset - target (str): address having its assets frozen or unfrozen - new_freeze_state (bool): true if the assets should be frozen, false if - they should be transferrable - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - genesis_hash (str) - index (int) - target (str) - new_freeze_state (bool) - note (bytes) - genesis_id (str) - type (str) - lease (byte[32]) - rekey_to (str) - """ - - def __init__( - self, - sender, - sp, - index, - target, - new_freeze_state, - note=None, - lease=None, - rekey_to=None, - ): - Transaction.__init__( - self, sender, sp, note, lease, constants.assetfreeze_txn, rekey_to - ) - self.index = self.creatable_index(index, required=True) - self.target = target - self.new_freeze_state = new_freeze_state - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - def dictify(self): - d = dict() - if self.new_freeze_state: - d["afrz"] = self.new_freeze_state - - d["fadd"] = encoding.decode_address(self.target) - - if self.index: - d["faid"] = self.index - - d.update(super(AssetFreezeTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - - @staticmethod - def _undictify(d): - args = { - "index": d["faid"], - "new_freeze_state": d["afrz"] if "afrz" in d else False, - "target": encoding.encode_address(d["fadd"]), - } - - return args - - def __eq__(self, other): - if not isinstance(other, (AssetFreezeTxn, transaction.AssetFreezeTxn)): - return False - return ( - super(AssetFreezeTxn, self).__eq__(other) - and self.index == other.index - and self.target == other.target - and self.new_freeze_state == other.new_freeze_state - ) - - -class AssetTransferTxn(Transaction): - """ - Represents a transaction for asset transfer. - - To begin accepting an asset, supply the same address as both sender and - receiver, and set amount to 0 (or use AssetOptInTxn) - - To revoke an asset, set revocation_target, and issue the transaction from - the asset's revocation manager account. - - Args: - sender (str): address of the sender - sp (SuggestedParams): suggested params from algod - receiver (str): address of the receiver - amt (int): amount of asset base units to send - index (int): index of the asset - close_assets_to (string, optional): send all of sender's remaining - assets, after paying `amt` to receiver, to this address - revocation_target (string, optional): send assets from this address, - rather than the sender's address (can only be used by an asset's - revocation manager, also known as clawback) - note (bytes, optional): arbitrary optional bytes - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - genesis_hash (str) - index (int) - amount (int) - receiver (string) - close_assets_to (string) - revocation_target (string) - note (bytes) - genesis_id (str) - type (str) - lease (byte[32]) - rekey_to (str) - """ - - def __init__( - self, - sender, - sp, - receiver, - amt, - index, - close_assets_to=None, - revocation_target=None, - note=None, - lease=None, - rekey_to=None, - ): - Transaction.__init__( - self, - sender, - sp, - note, - lease, - constants.assettransfer_txn, - rekey_to, - ) - if receiver: - self.receiver = receiver - else: - raise error.ZeroAddressError - - self.amount = amt - if (not isinstance(self.amount, int)) or self.amount < 0: - raise error.WrongAmountType - self.index = self.creatable_index(index, required=True) - self.close_assets_to = close_assets_to - self.revocation_target = revocation_target - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - def dictify(self): - d = dict() - - if self.amount: - d["aamt"] = self.amount - if self.close_assets_to: - d["aclose"] = encoding.decode_address(self.close_assets_to) - - decoded_receiver = encoding.decode_address(self.receiver) - if any(decoded_receiver): - d["arcv"] = encoding.decode_address(self.receiver) - if self.revocation_target: - d["asnd"] = encoding.decode_address(self.revocation_target) - - if self.index: - d["xaid"] = self.index - - d.update(super(AssetTransferTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - - return od - - @staticmethod - def _undictify(d): - args = { - "receiver": encoding.encode_address(d["arcv"]) - if "arcv" in d - else constants.ZERO_ADDRESS, - "amt": d["aamt"] if "aamt" in d else 0, - "index": d["xaid"] if "xaid" in d else None, - "close_assets_to": encoding.encode_address(d["aclose"]) - if "aclose" in d - else None, - "revocation_target": encoding.encode_address(d["asnd"]) - if "asnd" in d - else None, - } - - return args - - def __eq__(self, other): - if not isinstance( - other, (AssetTransferTxn, transaction.AssetTransferTxn) - ): - return False - return ( - super(AssetTransferTxn, self).__eq__(other) - and self.index == other.index - and self.amount == other.amount - and self.receiver == other.receiver - and self.close_assets_to == other.close_assets_to - and self.revocation_target == other.revocation_target - ) - - -class AssetOptInTxn(AssetTransferTxn): - """ - Make a transaction that will opt in to an ASA - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - index (int): the ASA to opt into - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - - Attributes: - See AssetTransferTxn - """ - - def __init__( - self, sender, sp, index, note=None, lease=None, rekey_to=None - ): - super().__init__( - sender=sender, - sp=sp, - receiver=sender, - amt=0, - index=index, - note=note, - lease=lease, - rekey_to=rekey_to, - ) - - -class AssetCloseOutTxn(AssetTransferTxn): - """ - Make a transaction that will send all of an ASA away, and opt out of it. - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - receiver (str): address of the receiver - index (int): the ASA to opt into - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - - Attributes: - See AssetTransferTxn - """ - - def __init__( - self, sender, sp, receiver, index, note=None, lease=None, rekey_to=None - ): - super().__init__( - sender=sender, - sp=sp, - receiver=receiver, - amt=0, - index=index, - close_assets_to=receiver, - note=note, - lease=lease, - rekey_to=rekey_to, - ) - - -class StateSchema: - """ - Restricts state for an application call. - - Args: - num_uints(int, optional): number of uints to store - num_byte_slices(int, optional): number of byte slices to store - - Attributes: - num_uints (int) - num_byte_slices (int) - """ - - def __init__(self, num_uints=None, num_byte_slices=None): - self.num_uints = num_uints - self.num_byte_slices = num_byte_slices - - def dictify(self): - d = dict() - if self.num_uints: - d["nui"] = self.num_uints - if self.num_byte_slices: - d["nbs"] = self.num_byte_slices - od = OrderedDict(sorted(d.items())) - return od - - @staticmethod - def undictify(d): - return StateSchema( - num_uints=d["nui"] if "nui" in d else 0, - num_byte_slices=d["nbs"] if "nbs" in d else 0, - ) - - def __eq__(self, other): - if not isinstance(other, StateSchema): - return False - return ( - self.num_uints == other.num_uints - and self.num_byte_slices == other.num_byte_slices - ) - - -class OnComplete(IntEnum): - # NoOpOC indicates that an application transaction will simply call its - # ApprovalProgram - NoOpOC = 0 - - # OptInOC indicates that an application transaction will allocate some - # LocalState for the application in the sender's account - OptInOC = 1 - - # CloseOutOC indicates that an application transaction will deallocate - # some LocalState for the application from the user's account - CloseOutOC = 2 - - # ClearStateOC is similar to CloseOutOC, but may never fail. This - # allows users to reclaim their minimum balance from an application - # they no longer wish to opt in to. - ClearStateOC = 3 - - # UpdateApplicationOC indicates that an application transaction will - # update the ApprovalProgram and ClearStateProgram for the application - UpdateApplicationOC = 4 - - # DeleteApplicationOC indicates that an application transaction will - # delete the AppParams for the application from the creator's balance - # record - DeleteApplicationOC = 5 - - -class ApplicationCallTxn(Transaction): - """ - Represents a transaction that interacts with the application system. - - Args: - sender (str): address of the sender - sp (SuggestedParams): suggested params from algod - index (int): index of the application to call; 0 if creating a new application - on_complete (OnComplete): intEnum representing what app should do on completion - local_schema (StateSchema, optional): restricts what can be stored by created application; - must be omitted if not creating an application - global_schema (StateSchema, optional): restricts what can be stored by created application; - must be omitted if not creating an application - approval_program (bytes, optional): the program to run on transaction approval; - must be omitted if not creating or updating an application - clear_program (bytes, optional): the program to run when state is being cleared; - must be omitted if not creating or updating an application - app_args (list[bytes], optional): list of arguments to the application, each argument itself a buf - accounts (list[string], optional): list of additional accounts involved in call - foreign_apps (list[int], optional): list of other applications (identified by index) involved in call - foreign_assets (list[int], optional): list of assets involved in call - extra_pages (int, optional): additional program space for supporting larger programs. A page is 1024 bytes. - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - genesis_hash (str) - index (int) - on_complete (int) - local_schema (StateSchema) - global_schema (StateSchema) - approval_program (bytes) - clear_program (bytes) - app_args (list[bytes]) - accounts (list[str]) - foreign_apps (list[int]) - foreign_assets (list[int]) - extra_pages (int) - boxes (list[(int, bytes)]) - """ - - def __init__( - self, - sender, - sp, - index, - on_complete, - local_schema=None, - global_schema=None, - approval_program=None, - clear_program=None, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - extra_pages=0, - boxes=None, - ): - Transaction.__init__( - self, sender, sp, note, lease, constants.appcall_txn, rekey_to - ) - self.index = self.creatable_index(index) - self.on_complete = on_complete if on_complete else 0 - self.local_schema = self.state_schema(local_schema) - self.global_schema = self.state_schema(global_schema) - self.approval_program = self.teal_bytes(approval_program) - self.clear_program = self.teal_bytes(clear_program) - self.app_args = self.bytes_list(app_args) - self.accounts = accounts if accounts else None - self.foreign_apps = self.int_list(foreign_apps) - self.foreign_assets = self.int_list(foreign_assets) - self.extra_pages = extra_pages - self.boxes = BoxReference.translate_box_references(boxes, self.foreign_apps, self.index) # type: ignore - if not sp.flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - @staticmethod - def state_schema(schema): - """Confirm the argument is a StateSchema, or false which is coerced to None""" - if not schema or not schema.dictify(): - return None # Coerce false/empty values to None, to help __eq__ - if not isinstance(schema, StateSchema): - raise TypeError("{} is not a StateSchema".format(schema)) - return schema - - @staticmethod - def teal_bytes(teal): - """Confirm the argument is bytes-like, or false which is coerced to None""" - if not teal: - return None # Coerce false values like "" to None, to help __eq__ - if not isinstance(teal, (bytes, bytearray)): - raise TypeError("Program {} is not bytes".format(teal)) - return teal - - @staticmethod - def bytes_list(lst): - """Confirm or coerce list elements to bytes. Return None for empty/false lst.""" - if not lst: - return None - return [encoding.encode_as_bytes(elt) for elt in lst] - - @staticmethod - def int_list(lst): - """Confirm or coerce list elements to int. Return None for empty/false lst.""" - if not lst: - return None - return [int(elt) for elt in lst] - - def dictify(self): - d = dict() - if self.index: - d["apid"] = self.index - d["apan"] = self.on_complete - if self.local_schema: - d["apls"] = self.local_schema.dictify() - if self.global_schema: - d["apgs"] = self.global_schema.dictify() - if self.approval_program: - d["apap"] = self.approval_program - if self.clear_program: - d["apsu"] = self.clear_program - if self.app_args: - d["apaa"] = self.app_args - if self.accounts: - d["apat"] = [ - encoding.decode_address(account_pubkey) - for account_pubkey in self.accounts - ] - if self.foreign_apps: - d["apfa"] = self.foreign_apps - if self.foreign_assets: - d["apas"] = self.foreign_assets - if self.extra_pages: - d["apep"] = self.extra_pages - if self.boxes: - d["apbx"] = [box.dictify() for box in self.boxes] - - d.update(super(ApplicationCallTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - - return od - - @staticmethod - def _undictify(d): - args = { - "index": d["apid"] if "apid" in d else None, - "on_complete": d["apan"] if "apan" in d else None, - "local_schema": StateSchema.undictify(d["apls"]) - if "apls" in d - else None, - "global_schema": StateSchema.undictify(d["apgs"]) - if "apgs" in d - else None, - "approval_program": d["apap"] if "apap" in d else None, - "clear_program": d["apsu"] if "apsu" in d else None, - "app_args": d["apaa"] if "apaa" in d else None, - "accounts": d["apat"] if "apat" in d else None, - "foreign_apps": d["apfa"] if "apfa" in d else None, - "foreign_assets": d["apas"] if "apas" in d else None, - "extra_pages": d["apep"] if "apep" in d else 0, - "boxes": [BoxReference.undictify(box) for box in d["apbx"]] - if "apbx" in d - else None, - } - if args["accounts"]: - args["accounts"] = [ - encoding.encode_address(account_bytes) - for account_bytes in args["accounts"] - ] - return args - - def __eq__(self, other): - if not isinstance(other, ApplicationCallTxn): - return False - return ( - super(ApplicationCallTxn, self).__eq__(other) - and self.index == other.index - and self.on_complete == other.on_complete - and self.local_schema == other.local_schema - and self.global_schema == other.global_schema - and self.approval_program == other.approval_program - and self.clear_program == other.clear_program - and self.app_args == other.app_args - and self.accounts == other.accounts - and self.foreign_apps == other.foreign_apps - and self.foreign_assets == other.foreign_assets - and self.extra_pages == other.extra_pages - and self.boxes == other.boxes - ) - - -class ApplicationCreateTxn(ApplicationCallTxn): - """ - Make a transaction that will create an application. - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - on_complete (OnComplete): what application should so once the program is done being run - approval_program (bytes): the compiled TEAL that approves a transaction - clear_program (bytes): the compiled TEAL that runs when clearing state - global_schema (StateSchema): restricts the number of ints and byte slices in the global state - local_schema (StateSchema): restricts the number of ints and byte slices in the per-user local state - app_args(list[bytes], optional): any additional arguments to the application - accounts(list[str], optional): any additional accounts to supply to the application - foreign_apps(list[int], optional): any other apps used by the application, identified by app index - foreign_assets(list[int], optional): list of assets involved in call - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - extra_pages(int, optional): provides extra program size - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - Attributes: - See ApplicationCallTxn - """ - - def __init__( - self, - sender, - sp, - on_complete, - approval_program, - clear_program, - global_schema, - local_schema, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - extra_pages=0, - boxes=None, - ): - ApplicationCallTxn.__init__( - self, - sender=sender, - sp=sp, - index=0, - on_complete=on_complete, - approval_program=self.required(approval_program), - clear_program=self.required(clear_program), - global_schema=global_schema, - local_schema=local_schema, - app_args=app_args, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - note=note, - lease=lease, - rekey_to=rekey_to, - extra_pages=extra_pages, - boxes=boxes, - ) - - -class ApplicationUpdateTxn(ApplicationCallTxn): - """ - Make a transaction that will change an application's approval and clear programs. - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - index (int): the application to update - approval_program (bytes): the new compiled TEAL that approves a transaction - clear_program (bytes): the new compiled TEAL that runs when clearing state - app_args(list[bytes], optional): any additional arguments to the application - accounts(list[str], optional): any additional accounts to supply to the application - foreign_apps(list[int], optional): any other apps used by the application, identified by app index - foreign_assets(list[int], optional): list of assets involved in call - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - - Attributes: - See ApplicationCallTxn - """ - - def __init__( - self, - sender, - sp, - index, - approval_program, - clear_program, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - boxes=None, - ): - ApplicationCallTxn.__init__( - self, - sender=sender, - sp=sp, - index=self.creatable_index(index, required=True), - on_complete=OnComplete.UpdateApplicationOC, - approval_program=approval_program, - clear_program=clear_program, - app_args=app_args, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - note=note, - lease=lease, - rekey_to=rekey_to, - boxes=boxes, - ) - - -class ApplicationDeleteTxn(ApplicationCallTxn): - """ - Make a transaction that will delete an application - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - index (int): the application to update - app_args(list[bytes], optional): any additional arguments to the application - accounts(list[str], optional): any additional accounts to supply to the application - foreign_apps(list[int], optional): any other apps used by the application, identified by app index - foreign_assets(list[int], optional): list of assets involved in call - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - Attributes: - See ApplicationCallTxn - """ - - def __init__( - self, - sender, - sp, - index, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - boxes=None, - ): - ApplicationCallTxn.__init__( - self, - sender=sender, - sp=sp, - index=self.creatable_index(index, required=True), - on_complete=OnComplete.DeleteApplicationOC, - app_args=app_args, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - note=note, - lease=lease, - rekey_to=rekey_to, - boxes=boxes, - ) - - -class ApplicationOptInTxn(ApplicationCallTxn): - """ - Make a transaction that will opt in to an application - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - index (int): the application to update - app_args(list[bytes], optional): any additional arguments to the application - accounts(list[str], optional): any additional accounts to supply to the application - foreign_apps(list[int], optional): any other apps used by the application, identified by app index - foreign_assets(list[int], optional): list of assets involved in call - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - Attributes: - See ApplicationCallTxn - """ - - def __init__( - self, - sender, - sp, - index, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - boxes=None, - ): - ApplicationCallTxn.__init__( - self, - sender=sender, - sp=sp, - index=self.creatable_index(index, required=True), - on_complete=OnComplete.OptInOC, - app_args=app_args, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - note=note, - lease=lease, - rekey_to=rekey_to, - boxes=boxes, - ) - - -class ApplicationCloseOutTxn(ApplicationCallTxn): - """ - Make a transaction that will close out a user's state in an application - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - index (int): the application to update - app_args(list[bytes], optional): any additional arguments to the application - accounts(list[str], optional): any additional accounts to supply to the application - foreign_apps(list[int], optional): any other apps used by the application, identified by app index - foreign_assets(list[int], optional): list of assets involved in call - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - Attributes: - See ApplicationCallTxn - """ - - def __init__( - self, - sender, - sp, - index, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - boxes=None, - ): - ApplicationCallTxn.__init__( - self, - sender=sender, - sp=sp, - index=self.creatable_index(index), - on_complete=OnComplete.CloseOutOC, - app_args=app_args, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - note=note, - lease=lease, - rekey_to=rekey_to, - boxes=boxes, - ) - - -class ApplicationClearStateTxn(ApplicationCallTxn): - """ - Make a transaction that will clear a user's state for an application - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - index (int): the application to update - app_args(list[bytes], optional): any additional arguments to the application - accounts(list[str], optional): any additional accounts to supply to the application - foreign_apps(list[int], optional): any other apps used by the application, identified by app index - foreign_assets(list[int], optional): list of assets involved in call - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - Attributes: - See ApplicationCallTxn - """ - - def __init__( - self, - sender, - sp, - index, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - boxes=None, - ): - ApplicationCallTxn.__init__( - self, - sender=sender, - sp=sp, - index=self.creatable_index(index), - on_complete=OnComplete.ClearStateOC, - app_args=app_args, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - note=note, - lease=lease, - rekey_to=rekey_to, - boxes=boxes, - ) - - -class ApplicationNoOpTxn(ApplicationCallTxn): - """ - Make a transaction that will do nothing on application completion - In other words, just call the application - - Args: - sender (str): address of sender - sp (SuggestedParams): contains information such as fee and genesis hash - index (int): the application to update - app_args(list[bytes], optional): any additional arguments to the application - accounts(list[str], optional): any additional accounts to supply to the application - foreign_apps(list[int], optional): any other apps used by the application, identified by app index - foreign_assets(list[int], optional): list of assets involved in call - note(bytes, optional): transaction note field - lease(bytes, optional): transaction lease field - rekey_to(str, optional): rekey-to field, see Transaction - boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - - Attributes: - See ApplicationCallTxn - """ - - def __init__( - self, - sender, - sp, - index, - app_args=None, - accounts=None, - foreign_apps=None, - foreign_assets=None, - note=None, - lease=None, - rekey_to=None, - boxes=None, - ): - ApplicationCallTxn.__init__( - self, - sender=sender, - sp=sp, - index=self.creatable_index(index), - on_complete=OnComplete.NoOpOC, - app_args=app_args, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - note=note, - lease=lease, - rekey_to=rekey_to, - boxes=boxes, - ) - - -class SignedTransaction: - """ - Represents a signed transaction. - - Args: - transaction (Transaction): transaction that was signed - signature (str): signature of a single address - authorizing_address (str, optional): the address authorizing the signed transaction, if different from sender - - Attributes: - transaction (Transaction) - signature (str) - authorizing_address (str) - """ - - def __init__(self, transaction, signature, authorizing_address=None): - self.signature = signature - self.transaction = transaction - self.authorizing_address = authorizing_address - - def get_txid(self): - """ - Get the transaction's ID. - - Returns: - str: transaction ID - """ - return self.transaction.get_txid() - - def dictify(self): - od = OrderedDict() - if self.signature: - od["sig"] = base64.b64decode(self.signature) - od["txn"] = self.transaction.dictify() - if self.authorizing_address: - od["sgnr"] = encoding.decode_address(self.authorizing_address) - return od - - @staticmethod - def undictify(d): - sig = None - if "sig" in d: - sig = base64.b64encode(d["sig"]).decode() - auth = None - if "sgnr" in d: - auth = encoding.encode_address(d["sgnr"]) - txn = Transaction.undictify(d["txn"]) - stx = SignedTransaction(txn, sig, auth) - return stx - - def __eq__(self, other): - if not isinstance( - other, (SignedTransaction, transaction.SignedTransaction) - ): - return False - return ( - self.transaction == other.transaction - and self.signature == other.signature - and self.authorizing_address == other.authorizing_address - ) - - -class MultisigTransaction: - """ - Represents a signed transaction. - - Args: - transaction (Transaction): transaction that was signed - multisig (Multisig): multisig account and signatures - - Attributes: - transaction (Transaction) - multisig (Multisig) - auth_addr (str, optional) - """ - - def __init__(self, transaction: Transaction, multisig: "Multisig") -> None: - self.transaction = transaction - self.multisig = multisig - - msigAddr = multisig.address() - if transaction.sender != msigAddr: - self.auth_addr = msigAddr - else: - self.auth_addr = None - - def sign(self, private_key): - """ - Sign the multisig transaction. - - Args: - private_key (str): private key of signing account - - Note: - A new signature will replace the old if there is already a - signature for the address. To sign another transaction, you can - either overwrite the signatures in the current Multisig, or you - can use Multisig.get_multisig_account() to get a new multisig - object with the same addresses. - """ - self.multisig.validate() - index = -1 - public_key = base64.b64decode(bytes(private_key, "utf-8")) - public_key = public_key[constants.key_len_bytes :] - for s in range(len(self.multisig.subsigs)): - if self.multisig.subsigs[s].public_key == public_key: - index = s - break - if index == -1: - raise error.InvalidSecretKeyError - sig = self.transaction.raw_sign(private_key) - self.multisig.subsigs[index].signature = sig - - def get_txid(self): - """ - Get the transaction's ID. - - Returns: - str: transaction ID - """ - return self.transaction.get_txid() - - def dictify(self): - od = OrderedDict() - if self.multisig: - od["msig"] = self.multisig.dictify() - if self.auth_addr: - od["sgnr"] = encoding.decode_address(self.auth_addr) - od["txn"] = self.transaction.dictify() - return od - - @staticmethod - def undictify(d): - msig = None - if "msig" in d: - msig = Multisig.undictify(d["msig"]) - auth_addr = None - if "sgnr" in d: - auth_addr = encoding.encode_address(d["sgnr"]) - txn = Transaction.undictify(d["txn"]) - mtx = MultisigTransaction(txn, msig) - mtx.auth_addr = auth_addr - return mtx - - @staticmethod - def merge(part_stxs: List["MultisigTransaction"]) -> "MultisigTransaction": - """ - Merge partially signed multisig transactions. - - Args: - part_stxs (MultisigTransaction[]): list of partially signed - multisig transactions - - Returns: - MultisigTransaction: multisig transaction containing signatures - - Note: - Only use this if you are given two partially signed multisig - transactions. To append a signature to a multisig transaction, just - use MultisigTransaction.sign() - """ - ref_msig_addr = None - ref_auth_addr = None - for stx in part_stxs: - try: - other_auth_addr = stx.auth_addr - except AttributeError: - other_auth_addr = None - - if not ref_msig_addr: - ref_msig_addr = stx.multisig.address() - ref_auth_addr = other_auth_addr - if not stx.multisig.address() == ref_msig_addr: - raise error.MergeKeysMismatchError - if not other_auth_addr == ref_auth_addr: - raise error.MergeAuthAddrMismatchError - - msigstx = None - for stx in part_stxs: - if not msigstx: - msigstx = stx - else: - for s in range(len(stx.multisig.subsigs)): - if stx.multisig.subsigs[s].signature: - if not msigstx.multisig.subsigs[s].signature: - msigstx.multisig.subsigs[ - s - ].signature = stx.multisig.subsigs[s].signature - elif ( - not msigstx.multisig.subsigs[s].signature - == stx.multisig.subsigs[s].signature - ): - raise error.DuplicateSigMismatchError - return msigstx - - def __eq__(self, other): - if isinstance(other, transaction.MultisigTransaction): - return ( - self.transaction == other.transaction - and self.auth_addr == None - and self.multisig == other.multisig - ) - - if isinstance(other, MultisigTransaction): - return ( - self.transaction == other.transaction - and self.auth_addr == other.auth_addr - and self.multisig == other.multisig - ) - - return False - - -class Multisig: - """ - Represents a multisig account and signatures. - - Args: - version (int): currently, the version is 1 - threshold (int): how many signatures are necessary - addresses (str[]): addresses in the multisig account - - Attributes: - version (int) - threshold (int) - subsigs (MultisigSubsig[]) - """ - - def __init__(self, version, threshold, addresses): - self.version = version - self.threshold = threshold - self.subsigs = [] - for a in addresses: - self.subsigs.append(MultisigSubsig(encoding.decode_address(a))) - - def validate(self): - """Check if the multisig account is valid.""" - if not self.version == 1: - raise error.UnknownMsigVersionError - if ( - self.threshold <= 0 - or len(self.subsigs) == 0 - or self.threshold > len(self.subsigs) - ): - raise error.InvalidThresholdError - if len(self.subsigs) > constants.multisig_account_limit: - raise error.MultisigAccountSizeError - - def address(self): - """Return the multisig account address.""" - msig_bytes = ( - bytes(constants.msig_addr_prefix, "utf-8") - + bytes([self.version]) - + bytes([self.threshold]) - ) - for s in self.subsigs: - msig_bytes += s.public_key - addr = encoding.checksum(msig_bytes) - return encoding.encode_address(addr) - - def verify(self, message): - """Verify that the multisig is valid for the message.""" - try: - self.validate() - except (error.UnknownMsigVersionError, error.InvalidThresholdError): - return False - counter = sum(map(lambda s: s.signature is not None, self.subsigs)) - if counter < self.threshold: - return False - - verified_count = 0 - for subsig in self.subsigs: - if subsig.signature is not None: - verify_key = VerifyKey(subsig.public_key) - try: - verify_key.verify(message, subsig.signature) - verified_count += 1 - except (BadSignatureError, ValueError, TypeError): - return False - - if verified_count < self.threshold: - return False - - return True - - def dictify(self): - od = OrderedDict() - od["subsig"] = [subsig.dictify() for subsig in self.subsigs] - od["thr"] = self.threshold - od["v"] = self.version - return od - - def json_dictify(self): - d = { - "subsig": [subsig.json_dictify() for subsig in self.subsigs], - "thr": self.threshold, - "v": self.version, - } - return d - - @staticmethod - def undictify(d): - subsigs = [MultisigSubsig.undictify(s) for s in d["subsig"]] - msig = Multisig(d["v"], d["thr"], []) - msig.subsigs = subsigs - return msig - - def get_multisig_account(self): - """Return a Multisig object without signatures.""" - msig = Multisig(self.version, self.threshold, self.get_public_keys()) - for s in msig.subsigs: - s.signature = None - return msig - - def get_public_keys(self): - """Return the base32 encoded addresses for the multisig account.""" - pks = [encoding.encode_address(s.public_key) for s in self.subsigs] - return pks - - def __eq__(self, other): - if not isinstance(other, (Multisig, transaction.Multisig)): - return False - return ( - self.version == other.version - and self.threshold == other.threshold - and self.subsigs == other.subsigs - ) - - -class MultisigSubsig: - """ - Attributes: - public_key (bytes) - signature (bytes) - """ - - def __init__(self, public_key, signature=None): - self.public_key = public_key - self.signature = signature - - def dictify(self): - od = OrderedDict() - od["pk"] = self.public_key - if self.signature: - od["s"] = self.signature - return od - - def json_dictify(self): - d = {"pk": base64.b64encode(self.public_key).decode()} - if self.signature: - d["s"] = base64.b64encode(self.signature).decode() - return d - - @staticmethod - def undictify(d): - sig = None - if "s" in d: - sig = d["s"] - mss = MultisigSubsig(d["pk"], sig) - return mss - - def __eq__(self, other): - if not isinstance(other, (MultisigSubsig, transaction.MultisigSubsig)): - return False - return ( - self.public_key == other.public_key - and self.signature == other.signature - ) - - -class LogicSig: - """ - Represents a logic signature - - NOTE: This type is deprecated. Use LogicSigAccount instead. - - Arguments: - logic (bytes): compiled program - args (list[bytes]): args are not signed, but are checked by logic - - Attributes: - logic (bytes) - sig (bytes) - msig (Multisig) - args (list[bytes]) - """ - - def __init__(self, program, args=None): - self._sanity_check_program(program) - self.logic = program - self.args = args - self.sig = None - self.msig = None - - @staticmethod - def _sanity_check_program(program): - """ - Performs heuristic program validation: - check if passed in bytes are Algorand address, or they are B64 encoded, rather than Teal bytes - - Args: - program (bytes): compiled program - """ - - def is_ascii_printable(program_bytes): - return all( - map( - lambda x: x == ord("\n") or (ord(" ") <= x <= ord("~")), - program_bytes, - ) - ) - - if not program: - raise error.InvalidProgram("empty program") - - if is_ascii_printable(program): - try: - encoding.decode_address(program.decode("utf-8")) - raise error.InvalidProgram( - "requesting program bytes, get Algorand address" - ) - except error.WrongChecksumError: - pass - except error.WrongKeyLengthError: - pass - - try: - base64.b64decode(program.decode("utf-8")) - raise error.InvalidProgram("program should not be b64 encoded") - except binascii.Error: - pass - - raise error.InvalidProgram( - "program bytes are all ASCII printable characters, not looking like Teal byte code" - ) - - def dictify(self): - od = OrderedDict() - if self.args: - od["arg"] = self.args - od["l"] = self.logic - if self.sig: - od["sig"] = base64.b64decode(self.sig) - elif self.msig: - od["msig"] = self.msig.dictify() - return od - - @staticmethod - def undictify(d): - lsig = LogicSig(d["l"], d.get("arg", None)) - if "sig" in d: - lsig.sig = base64.b64encode(d["sig"]).decode() - elif "msig" in d: - lsig.msig = Multisig.undictify(d["msig"]) - return lsig - - def verify(self, public_key): - """ - Verifies LogicSig against the transaction's sender address - - Args: - public_key (bytes): sender address - - Returns: - bool: true if the signature is valid (the sender address matches\ - the logic hash or the signature is valid against the sender\ - address), false otherwise - """ - if self.sig and self.msig: - return False - - try: - self._sanity_check_program(self.logic) - except error.InvalidProgram: - return False - - to_sign = constants.logic_prefix + self.logic - - if not self.sig and not self.msig: - checksum = encoding.checksum(to_sign) - return checksum == public_key - - if self.sig: - verify_key = VerifyKey(public_key) - try: - verify_key.verify(to_sign, base64.b64decode(self.sig)) - return True - except (BadSignatureError, ValueError, TypeError): - return False - - return self.msig.verify(to_sign) - - def address(self): - """ - Compute hash of the logic sig program (that is the same as escrow - account address) as string address - - Returns: - str: program address - """ - return logic.address(self.logic) - - @staticmethod - def sign_program(program, private_key): - private_key = base64.b64decode(private_key) - signing_key = SigningKey(private_key[: constants.key_len_bytes]) - to_sign = constants.logic_prefix + program - signed = signing_key.sign(to_sign) - return base64.b64encode(signed.signature).decode() - - @staticmethod - def single_sig_multisig(program, private_key, multisig): - index = -1 - public_key = base64.b64decode(bytes(private_key, "utf-8")) - public_key = public_key[constants.key_len_bytes :] - for s in range(len(multisig.subsigs)): - if multisig.subsigs[s].public_key == public_key: - index = s - break - if index == -1: - raise error.InvalidSecretKeyError - sig = LogicSig.sign_program(program, private_key) - - return sig, index - - def sign(self, private_key, multisig=None): - """ - Creates signature (if no pk provided) or multi signature - - Args: - private_key (str): private key of signing account - multisig (Multisig): optional multisig account without signatures - to sign with - - Raises: - InvalidSecretKeyError: if no matching private key in multisig\ - object - LogicSigOverspecifiedSignature: if the opposite signature type has - already been provided - """ - if not multisig: - if self.msig: - raise error.LogicSigOverspecifiedSignature - self.sig = LogicSig.sign_program(self.logic, private_key) - else: - if self.sig: - raise error.LogicSigOverspecifiedSignature - sig, index = LogicSig.single_sig_multisig( - self.logic, private_key, multisig - ) - multisig.subsigs[index].signature = base64.b64decode(sig) - self.msig = multisig - - def append_to_multisig(self, private_key): - """ - Appends a signature to multi signature - - Args: - private_key (str): private key of signing account - - Raises: - InvalidSecretKeyError: if no matching private key in multisig\ - object - """ - if self.msig is None: - raise error.InvalidSecretKeyError - sig, index = LogicSig.single_sig_multisig( - self.logic, private_key, self.msig - ) - self.msig.subsigs[index].signature = base64.b64decode(sig) - - def __eq__(self, other): - if not isinstance(other, (LogicSig, transaction.LogicSig)): - return False - return ( - self.logic == other.logic - and self.args == other.args - and self.sig == other.sig - and self.msig == other.msig - ) - - -class LogicSigAccount: - """ - Represents an account that can sign with a LogicSig program. - """ - - def __init__(self, program: bytes, args: List[bytes] = None) -> None: - """ - Create a new LogicSigAccount. By default this will create an escrow - LogicSig account. Call `sign` or `sign_multisig` on the newly created - LogicSigAccount to make it a delegated account. - - Args: - program (bytes): The compiled TEAL program which contains the logic - for this LogicSig. - args (List[bytes], optional): An optional array of arguments for the - program. - """ - self.lsig = LogicSig(program, args) - self.sigkey = None - - def dictify(self): - od = OrderedDict() - od["lsig"] = self.lsig.dictify() - if self.sigkey: - od["sigkey"] = self.sigkey - return od - - @staticmethod - def undictify(d): - lsig = LogicSig.undictify(d["lsig"]) - lsigAccount = LogicSigAccount(lsig.logic, lsig.args) - lsigAccount.lsig = lsig - if "sigkey" in d: - lsigAccount.sigkey = d["sigkey"] - return lsigAccount - - def is_delegated(self) -> bool: - """ - Check if this LogicSigAccount has been delegated to another account with - a signature. - - Returns: - bool: True if and only if this is a delegated LogicSigAccount. - """ - return bool(self.lsig.sig or self.lsig.msig) - - def verify(self) -> bool: - """ - Verifies the LogicSig's program and signatures. - - Returns: - bool: True if and only if the LogicSig program and signatures are - valid. - """ - addr = self.address() - return self.lsig.verify(encoding.decode_address(addr)) - - def address(self) -> str: - """ - Get the address of this LogicSigAccount. - - If the LogicSig is delegated to another account, this will return the - address of that account. - - If the LogicSig is not delegated to another account, this will return an - escrow address that is the hash of the LogicSig's program code. - """ - if self.lsig.sig and self.lsig.msig: - raise error.LogicSigOverspecifiedSignature - - if self.lsig.sig: - if not self.sigkey: - raise error.LogicSigSigningKeyMissing - return encoding.encode_address(self.sigkey) - - if self.lsig.msig: - return self.lsig.msig.address() - - return self.lsig.address() - - def sign_multisig(self, multisig: Multisig, private_key: str) -> None: - """ - Turns this LogicSigAccount into a delegated LogicSig. - - This type of LogicSig has the authority to sign transactions on behalf - of another account, called the delegating account. Use this function if - the delegating account is a multisig account. - - Args: - multisig (Multisig): The multisig delegating account - private_key (str): The private key of one of the members of the - delegating multisig account. Use `append_to_multisig` to add - additional signatures from other members. - - Raises: - InvalidSecretKeyError: if no matching private key in multisig - object - LogicSigOverspecifiedSignature: if this LogicSigAccount has already - been signed with a single private key. - """ - self.lsig.sign(private_key, multisig) - - def append_to_multisig(self, private_key: str) -> None: - """ - Adds an additional signature from a member of the delegating multisig - account. - - Args: - private_key (str): The private key of one of the members of the - delegating multisig account. - - Raises: - InvalidSecretKeyError: if no matching private key in multisig - object - """ - self.lsig.append_to_multisig(private_key) - - def sign(self, private_key: str) -> None: - """ - Turns this LogicSigAccount into a delegated LogicSig. - - This type of LogicSig has the authority to sign transactions on behalf - of another account, called the delegating account. If the delegating - account is a multisig account, use `sign_multisig` instead. - - Args: - private_key (str): The private key of the delegating account. - - Raises: - LogicSigOverspecifiedSignature: if this LogicSigAccount has already - been signed by a multisig account. - """ - self.lsig.sign(private_key) - public_key = base64.b64decode(bytes(private_key, "utf-8")) - public_key = public_key[constants.key_len_bytes :] - self.sigkey = public_key - - def __eq__(self, other) -> bool: - if not isinstance(other, LogicSigAccount): - return False - return self.lsig == other.lsig and self.sigkey == other.sigkey - - -class LogicSigTransaction: - """ - Represents a logic signed transaction - - Arguments: - transaction (Transaction) - lsig (LogicSig or LogicSigAccount) - - Attributes: - transaction (Transaction) - lsig (LogicSig) - auth_addr (str, optional) - """ - - def __init__( - self, transaction: Transaction, lsig: Union[LogicSig, LogicSigAccount] - ) -> None: - self.transaction = transaction - - if isinstance(lsig, LogicSigAccount): - lsigAddr = lsig.address() - self.lsig = lsig.lsig - else: - if lsig.sig: - # For a LogicSig with a non-multisig delegating account, we - # cannot derive the address of that account from only its - # signature, so assume the delegating account is the sender. If - # that's not the case, verify will fail. - lsigAddr = transaction.sender - elif lsig.msig: - lsigAddr = lsig.msig.address() - else: - lsigAddr = lsig.address() - self.lsig = lsig - - if transaction.sender != lsigAddr: - self.auth_addr = lsigAddr - else: - self.auth_addr = None - - def verify(self) -> bool: - """ - Verify the LogicSig used to sign the transaction - - Returns: - bool: true if the signature is valid, false otherwise - """ - if self.auth_addr: - addr_to_verify = self.auth_addr - else: - addr_to_verify = self.transaction.sender - - public_key = encoding.decode_address(addr_to_verify) - return self.lsig.verify(public_key) - - def get_txid(self): - """ - Get the transaction's ID. - - Returns: - str: transaction ID - """ - return self.transaction.get_txid() - - def dictify(self): - od = OrderedDict() - if self.lsig: - od["lsig"] = self.lsig.dictify() - if self.auth_addr: - od["sgnr"] = encoding.decode_address(self.auth_addr) - od["txn"] = self.transaction.dictify() - - return od - - @staticmethod - def undictify(d): - lsig = None - if "lsig" in d: - lsig = LogicSig.undictify(d["lsig"]) - auth_addr = None - if "sgnr" in d: - auth_addr = encoding.encode_address(d["sgnr"]) - txn = Transaction.undictify(d["txn"]) - lstx = LogicSigTransaction(txn, lsig) - lstx.auth_addr = auth_addr - return lstx - - def __eq__(self, other): - if isinstance(other, transaction.LogicSigTransaction): - return ( - self.lsig == other.lsig - and self.auth_addr == None - and self.transaction == other.transaction - ) - - if isinstance(other, LogicSigTransaction): - return ( - self.lsig == other.lsig - and self.auth_addr == other.auth_addr - and self.transaction == other.transaction - ) - - return False - - -class StateProofTxn(Transaction): - """ - Represents a state proof transaction - - Arguments: - sender (str): address of the sender - state_proof (dict(), optional) - state_proof_message (dict(), optional) - state_proof_type (str, optional): state proof type - sp (SuggestedParams): suggested params from algod - - - Attributes: - sender (str) - sprf (dict()) - sprfmsg (dict()) - sprf_type (str) - first_valid_round (int) - last_valid_round (int) - genesis_id (str) - genesis_hash (str) - type (str) - """ - - def __init__( - self, - sender, - sp, - state_proof=None, - state_proof_message=None, - state_proof_type=None, - ): - Transaction.__init__( - self, sender, sp, None, None, constants.stateproof_txn, None - ) - - self.sprf_type = state_proof_type - self.sprf = state_proof - self.sprfmsg = state_proof_message - - def dictify(self): - d = dict() - if self.sprf_type: - d["sptype"] = self.sprf_type - if self.sprfmsg: - d["spmsg"] = self.sprfmsg - if self.sprf: - d["sp"] = self.sprf - d.update(super(StateProofTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - - return od - - @staticmethod - def _undictify(d): - args = {} - if "sptype" in d: - args["state_proof_type"] = d["sptype"] - if "sp" in d: - args["state_proof"] = d["sp"] - if "spmsg" in d: - args["state_proof_message"] = d["spmsg"] - - return args - - def __eq__(self, other): - if not isinstance(other, StateProofTxn): - return False - return ( - super(StateProofTxn, self).__eq__(other) - and self.sprf_type == other.sprf_type - and self.sprf == other.sprf - and self.sprfmsg == other.sprfmsg - ) - - return False - - -def write_to_file(txns, path, overwrite=True): - """ - Write signed or unsigned transactions to a file. - - Args: - txns (Transaction[], SignedTransaction[], or MultisigTransaction[]):\ - can be a mix of the three - path (str): file to write to - overwrite (bool): whether or not to overwrite what's already in the - file; if False, transactions will be appended to the file - - Returns: - bool: true if the transactions have been written to the file - """ - - f = None - if overwrite: - f = open(path, "wb") - else: - f = open(path, "ab") - - for txn in txns: - if isinstance(txn, Transaction): - enc = msgpack.packb({"txn": txn.dictify()}, use_bin_type=True) - f.write(enc) - else: - enc = msgpack.packb(txn.dictify(), use_bin_type=True) - f.write(enc) - f.close() - return True - - -def retrieve_from_file(path): - """ - Retrieve signed or unsigned transactions from a file. - - Args: - path (str): file to read from - - Returns: - Transaction[], SignedTransaction[], or MultisigTransaction[]:\ - can be a mix of the three - """ - - f = open(path, "rb") - txns = [] - unp = msgpack.Unpacker(f, raw=False) - for txn in unp: - if "msig" in txn: - txns.append(MultisigTransaction.undictify(txn)) - elif "sig" in txn: - txns.append(SignedTransaction.undictify(txn)) - elif "lsig" in txn: - txns.append(LogicSigTransaction.undictify(txn)) - elif "type" in txn: - txns.append(Transaction.undictify(txn)) - elif "txn" in txn: - txns.append(Transaction.undictify(txn["txn"])) - f.close() - return txns - - -class TxGroup: - def __init__(self, txns): - assert isinstance(txns, list) - """ - Transactions specifies a list of transactions that must appear - together, sequentially, in a block in order for the group to be - valid. Each hash in the list is a hash of a transaction with - the `Group` field omitted. - """ - if len(txns) > constants.tx_group_limit: - raise error.TransactionGroupSizeError - self.transactions = txns - - def dictify(self): - od = OrderedDict() - od["txlist"] = self.transactions - return od - - @staticmethod - def undictify(d): - txg = TxGroup(d["txlist"]) - return txg - - -def calculate_group_id(txns): - """ - Calculate group id for a given list of unsigned transactions - - Args: - txns (list): list of unsigned transactions - - Returns: - bytes: checksum value representing the group id - """ - if len(txns) > constants.tx_group_limit: - raise error.TransactionGroupSizeError - txids = [] - for txn in txns: - raw_txn = encoding.msgpack_encode(txn) - to_hash = constants.txid_prefix + base64.b64decode(raw_txn) - txids.append(encoding.checksum(to_hash)) - - group = TxGroup(txids) - - encoded = encoding.msgpack_encode(group) - to_sign = constants.tgid_prefix + base64.b64decode(encoded) - gid = encoding.checksum(to_sign) - return gid - - -def assign_group_id(txns, address=None): - """ - Assign group id to a given list of unsigned transactions - - Args: - txns (list): list of unsigned transactions - address (str): optional sender address specifying which transaction - return - - Returns: - txns (list): list of unsigned transactions with group property set - """ - if len(txns) > constants.tx_group_limit: - raise error.TransactionGroupSizeError - gid = calculate_group_id(txns) - result = [] - for tx in txns: - if address is None or tx.sender == address: - tx.group = gid - result.append(tx) - return result - - -def wait_for_confirmation( - algod_client: algod.AlgodClient, txid: str, wait_rounds: int = 0, **kwargs -): - """ - Block until a pending transaction is confirmed by the network. - - Args: - algod_client (algod.AlgodClient): Instance of the `algod` client - txid (str): transaction ID - wait_rounds (int, optional): The number of rounds to block for before - exiting with an Exception. If not supplied, this will be 1000. - """ - last_round = algod_client.status()["last-round"] - current_round = last_round + 1 - - if wait_rounds == 0: - wait_rounds = 1000 - - while True: - # Check that the `wait_rounds` has not passed - if current_round > last_round + wait_rounds: - raise error.ConfirmationTimeoutError( - "Wait for transaction id {} timed out".format(txid) - ) - - try: - tx_info = algod_client.pending_transaction_info(txid, **kwargs) - - # The transaction has been rejected - if "pool-error" in tx_info and len(tx_info["pool-error"]) != 0: - raise error.TransactionRejectedError( - "Transaction rejected: " + tx_info["pool-error"] - ) - - # The transaction has been confirmed - if ( - "confirmed-round" in tx_info - and tx_info["confirmed-round"] != 0 - ): - return tx_info - except error.AlgodHTTPError as e: - # Ignore HTTP errors from pending_transaction_info, since it may return 404 if the algod - # instance is behind a load balancer and the request goes to a different algod than the - # one we submitted the transaction to - pass - - # Wait until the block for the `current_round` is confirmed - algod_client.status_after_block(current_round) - - # Incremenent the `current_round` - current_round += 1 - - -defaultAppId = 1380011588 - - -def create_dryrun( - client: algod.AlgodClient, - txns: List[Union[SignedTransaction, LogicSigTransaction]], - protocol_version=None, - latest_timestamp=None, - round=None, -) -> models.DryrunRequest: - """ - Create DryrunRequest object from a client and list of signed transactions - - Args: - algod_client (algod.AlgodClient): Instance of the `algod` client - txns (List[SignedTransaction]): transaction ID - protocol_version (string, optional): The protocol version to evaluate against - latest_timestamp (int, optional): The latest timestamp to evaluate against - round (int, optional): The round to evaluate against - """ - - # The list of info objects passed to the DryrunRequest object - app_infos, acct_infos = [], [] - - # The running list of things we need to fetch - apps, assets, accts = [], [], [] - for t in txns: - txn = t.transaction - - # we only care about app call transactions - if issubclass(type(txn), ApplicationCallTxn): - accts.append(txn.sender) - - # Add foreign args if they're set - if txn.accounts: - accts.extend(txn.accounts) - if txn.foreign_apps: - apps.extend(txn.foreign_apps) - accts.extend( - [ - logic.get_application_address(aidx) - for aidx in txn.foreign_apps - ] - ) - if txn.foreign_assets: - assets.extend(txn.foreign_assets) - - # For creates, we need to add the source directly from the transaction - if txn.index == 0: - appId = defaultAppId - # Make up app id, since tealdbg/dryrun doesnt like 0s - # https://github.com/algorand/go-algorand/blob/e466aa18d4d963868d6d15279b1c881977fa603f/libgoal/libgoal.go#L1089-L1090 - - ls = txn.local_schema - if ls is not None: - ls = models.ApplicationStateSchema( - ls.num_uints, ls.num_byte_slices - ) - - gs = txn.global_schema - if gs is not None: - gs = models.ApplicationStateSchema( - gs.num_uints, gs.num_byte_slices - ) - - app_infos.append( - models.Application( - id=appId, - params=models.ApplicationParams( - creator=txn.sender, - approval_program=txn.approval_program, - clear_state_program=txn.clear_program, - local_state_schema=ls, - global_state_schema=gs, - ), - ) - ) - else: - apps.append(txn.index) - - # Dedupe and filter none, reset programs to bytecode instead of b64 - apps = [i for i in set(apps) if i] - for app in apps: - app_info = client.application_info(app) - # Need to pass bytes, not b64 string - app_info = decode_programs(app_info) - app_infos.append(app_info) - - # Make sure the application account is in the accounts array - accts.append(logic.get_application_address(app)) - - # Make sure the creator is added to accounts array - accts.append(app_info["params"]["creator"]) - - # Dedupe and filter None, add asset creator to accounts to include in dryrun - assets = [i for i in set(assets) if i] - for asset in assets: - asset_info = client.asset_info(asset) - - # Make sure the asset creator address is in the accounts array - accts.append(asset_info["params"]["creator"]) - - # Dedupe and filter None, fetch and add account info - accts = [i for i in set(accts) if i] - for acct in accts: - acct_info = client.account_info(acct) - if "created-apps" in acct_info: - acct_info["created-apps"] = [ - decode_programs(ca) for ca in acct_info["created-apps"] - ] - acct_infos.append(acct_info) - - return models.DryrunRequest( - txns=txns, - apps=app_infos, - accounts=acct_infos, - protocol_version=protocol_version, - latest_timestamp=latest_timestamp, - round=round, - ) - - -def decode_programs(app): - app["params"]["approval-program"] = base64.b64decode( - app["params"]["approval-program"] - ) - app["params"]["clear-state-program"] = base64.b64decode( - app["params"]["clear-state-program"] - ) - return app diff --git a/algosdk/kmd.py b/algosdk/kmd.py index 0d06bb68..eb7e5a54 100644 --- a/algosdk/kmd.py +++ b/algosdk/kmd.py @@ -4,7 +4,7 @@ from urllib import parse from urllib.request import Request, urlopen -from . import constants, encoding, error, future +from . import constants, encoding, error, transaction api_version_path_prefix = "/v1" @@ -323,7 +323,7 @@ def sign_transaction(self, handle, password, txn, signing_address=None): query["public_key"] = signing_address result = self.kmd_request("POST", req, data=query) result = result["signed_transaction"] - return encoding.future_msgpack_decode(result) + return encoding.msgpack_decode(result) def list_multisig(self, handle): """ @@ -381,7 +381,7 @@ def export_multisig(self, handle, address): result = self.kmd_request("POST", req, data=query) pks = result["pks"] pks = [encoding.encode_address(base64.b64decode(p)) for p in pks] - msig = future.transaction.Multisig( + msig = transaction.Multisig( result["multisig_version"], result["threshold"], pks ) return msig @@ -434,6 +434,6 @@ def sign_multisig_transaction(self, handle, password, public_key, mtx): "partial_multisig": partial, } result = self.kmd_request("POST", req, data=query)["multisig"] - msig = encoding.future_msgpack_decode(result) + msig = encoding.msgpack_decode(result) mtx.multisig = msig return mtx diff --git a/algosdk/logic.py b/algosdk/logic.py index 05e254c2..5c64f5da 100644 --- a/algosdk/logic.py +++ b/algosdk/logic.py @@ -9,295 +9,6 @@ from nacl.signing import SigningKey -spec = None -opcodes = None - - -def check_program(program, args=None): - """ - NOTE: This method is deprecated: - Validation relies on metadata (`langspec.json`) that does not accurately represent opcode behavior across program versions. - The behavior of `check_program` relies on `langspec.json`. Thus, this method is being deprecated. - - Performs program checking for max length and cost - - Args: - program (bytes): compiled program - args (list[bytes]): args are not signed, but are checked by logic - - Returns: - bool: True on success - - Raises: - InvalidProgram: on error - """ - warnings.warn( - "`check_program` relies on metadata (`langspec.json`) that " - "does not accurately represent opcode behavior across program versions. " - "This method is being deprecated.", - DeprecationWarning, - ) - ok, _, _ = read_program(program, args) - return ok - - -def read_program(program, args=None): - """ - NOTE: This method is deprecated: - Validation relies on metadata (`langspec.json`) that does not accurately represent opcode behavior across program versions. - The behavior of `read_program` relies on `langspec.json`. Thus, this method is being deprecated. - """ - warnings.warn( - "`read_program` relies on metadata (`langspec.json`) that " - "does not accurately represent opcode behavior across program versions. " - "This method is being deprecated.", - DeprecationWarning, - ) - - global spec, opcodes - intcblock_opcode = 32 - bytecblock_opcode = 38 - pushbytes_opcode = 128 - pushint_opcode = 129 - - if not program: - raise error.InvalidProgram("empty program") - if not args: - args = [] - - if spec is None: - script_path = os.path.realpath(__file__) - script_dir = os.path.dirname(script_path) - langspec_file = os.path.join(script_dir, "data", "langspec.json") - with open(langspec_file, "rt") as fin: - spec = json.load(fin) - - version, vlen = parse_uvarint(program) - if vlen <= 0 or version > spec["EvalMaxVersion"]: - raise error.InvalidProgram("unsupported version") - - cost = 0 - length = len(program) - for arg in args: - length += len(arg) - - if length > constants.logic_sig_max_size: - raise error.InvalidProgram("program too long") - - if opcodes is None: - opcodes = dict() - for op in spec["Ops"]: - opcodes[op["Opcode"]] = op - - ints = [] - bytearrays = [] - pc = vlen - while pc < len(program): - op = opcodes.get(program[pc], None) - if op is None: - raise error.InvalidProgram("invalid instruction") - - cost += op["Cost"] - size = op["Size"] - if size == 0: - if op["Opcode"] == intcblock_opcode: - size_inc, found_ints = read_int_const_block(program, pc) - ints += found_ints - size += size_inc - elif op["Opcode"] == bytecblock_opcode: - size_inc, found_bytearrays = read_byte_const_block(program, pc) - bytearrays += found_bytearrays - size += size_inc - elif op["Opcode"] == pushint_opcode: - size_inc, found_int = read_push_int_block(program, pc) - ints.append(found_int) - size += size_inc - elif op["Opcode"] == pushbytes_opcode: - size_inc, found_bytearray = read_push_byte_block(program, pc) - bytearrays.append(found_bytearray) - size += size_inc - else: - raise error.InvalidProgram("invalid instruction") - pc += size - - # costs calculated dynamically starting in v4 - if version < 4 and cost >= constants.logic_sig_max_cost: - raise error.InvalidProgram( - "program too costly for version < 4. consider using v4." - ) - - return True, ints, bytearrays - - -def check_int_const_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size, _ = read_int_const_block(program, pc) - return size - - -def read_int_const_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size = 1 - ints = [] - num_ints, bytes_used = parse_uvarint(program[pc + size :]) - if bytes_used <= 0: - raise error.InvalidProgram( - "could not decode int const block size at pc=%d" % (pc + size) - ) - size += bytes_used - for i in range(0, num_ints): - if pc + size >= len(program): - raise error.InvalidProgram("intcblock ran past end of program") - num, bytes_used = parse_uvarint(program[pc + size :]) - if bytes_used <= 0: - raise error.InvalidProgram( - "could not decode int const[%d] at pc=%d" % (i, pc + size) - ) - ints.append(num) - size += bytes_used - return size, ints - - -def check_byte_const_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size, _ = read_byte_const_block(program, pc) - return size - - -def read_byte_const_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size = 1 - bytearrays = [] - num_ints, bytes_used = parse_uvarint(program[pc + size :]) - if bytes_used <= 0: - raise error.InvalidProgram( - "could not decode []byte const block size at pc=%d" % (pc + size) - ) - size += bytes_used - for i in range(0, num_ints): - if pc + size >= len(program): - raise error.InvalidProgram("bytecblock ran past end of program") - item_len, bytes_used = parse_uvarint(program[pc + size :]) - if bytes_used <= 0: - raise error.InvalidProgram( - "could not decode []byte const[%d] at pc=%d" % (i, pc + size) - ) - size += bytes_used - if pc + size + item_len > len(program): - raise error.InvalidProgram("bytecblock ran past end of program") - bytearrays.append(program[pc + size : pc + size + item_len]) - size += item_len - return size, bytearrays - - -def check_push_int_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size, _ = read_push_int_block(program, pc) - return size - - -def read_push_int_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size = 1 - single_int, bytes_used = parse_uvarint(program[pc + size :]) - if bytes_used <= 0: - raise error.InvalidProgram( - "could not decode push int const at pc=%d" % (pc + size) - ) - size += bytes_used - return size, single_int - - -def check_push_byte_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size, _ = read_push_byte_block(program, pc) - return size - - -def read_push_byte_block(program, pc): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - size = 1 - item_len, bytes_used = parse_uvarint(program[pc + size :]) - if bytes_used <= 0: - raise error.InvalidProgram( - "could not decode push []byte const size at pc=%d" % (pc + size) - ) - size += bytes_used - if pc + size + item_len > len(program): - raise error.InvalidProgram("pushbytes ran past end of program") - single_bytearray = program[pc + size : pc + size + item_len] - size += item_len - return size, single_bytearray - - -def parse_uvarint(buf): - """ - NOTE: This method is deprecated - """ - warnings.warn( - "This method is being deprecated.", - DeprecationWarning, - ) - x = 0 - s = 0 - for i, b in enumerate(buf): - if b < 0x80: - if i > 9 or i == 9 and b > 1: - return 0, -(i + 1) - return x | int(b) << s, i + 1 - x |= int(b & 0x7F) << s - s += 7 - - return 0, 0 - def address(program): """ diff --git a/algosdk/mnemonic.py b/algosdk/mnemonic.py index 197c02fb..4908f401 100644 --- a/algosdk/mnemonic.py +++ b/algosdk/mnemonic.py @@ -78,28 +78,6 @@ def to_private_key(mnemonic): return base64.b64encode(key.encode() + key.verify_key.encode()).decode() -def to_public_key(mnemonic): - """ - Return the public key for the mnemonic. - This method returns the Algorand address and will be deprecated, use - account.address_from_private_key instead. - - Args: - mnemonic (str): mnemonic of the private key - - Returns: - str: public key in base32 - """ - warnings.warn( - "to_public_key returns the Algorand address and will be " - "deprecated, use account.address_from_private_key instead", - DeprecationWarning, - ) - key_bytes = _to_key(mnemonic) - key = signing.SigningKey(key_bytes) - return encoding.encode_address(key.verify_key.encode()) - - def _from_key(key): """ Return the mnemonic for the key. diff --git a/algosdk/py.typed b/algosdk/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/algosdk/source_map.py b/algosdk/source_map.py index b459f9a6..d72a2129 100644 --- a/algosdk/source_map.py +++ b/algosdk/source_map.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, List, Tuple +from typing import Dict, Any, List, Tuple, Final, Optional, cast from algosdk.error import SourceMapVersionError @@ -43,14 +43,14 @@ def __init__(self, source_map: Dict[str, Any]): self.line_to_pc[last_line].append(index) self.pc_to_line[index] = last_line - def get_line_for_pc(self, pc: int) -> int: + def get_line_for_pc(self, pc: int) -> Optional[int]: return self.pc_to_line.get(pc, None) - def get_pcs_for_line(self, line: int) -> List[int]: + def get_pcs_for_line(self, line: int) -> Optional[List[int]]: return self.line_to_pc.get(line, None) -def _decode_int_value(value: str) -> int: +def _decode_int_value(value: str) -> Optional[int]: # Mappings may have up to 5 segments: # Third segment represents the zero-based starting line in the original source represented. decoded_value = _base64vlq_decode(value) @@ -62,19 +62,20 @@ def _decode_int_value(value: str) -> int: """ _b64chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -_b64table = [None] * (max(_b64chars) + 1) +_b64table: Final[List[Optional[int]]] = [None] * (max(_b64chars) + 1) for i, b in enumerate(_b64chars): _b64table[b] = i shiftsize, flag, mask = 5, 1 << 5, (1 << 5) - 1 -def _base64vlq_decode(vlqval: str) -> Tuple[int]: +def _base64vlq_decode(vlqval: str) -> Tuple[int, ...]: """Decode Base64 VLQ value""" results = [] shift = value = 0 # use byte values and a table to go from base64 characters to integers for v in map(_b64table.__getitem__, vlqval.encode("ascii")): + v = cast(int, v) # force int type given context value += (v & mask) << shift if v & flag: shift += shiftsize @@ -82,4 +83,4 @@ def _base64vlq_decode(vlqval: str) -> Tuple[int]: # determine sign and add to results results.append((value >> 1) * (-1 if value & 1 else 1)) shift = value = 0 - return results + return tuple(results) diff --git a/algosdk/template.py b/algosdk/template.py deleted file mode 100644 index 59890510..00000000 --- a/algosdk/template.py +++ /dev/null @@ -1,755 +0,0 @@ -import math -import random -from . import error, encoding, constants, transaction, logic, account -from Cryptodome.Hash import SHA256, keccak -import base64 - - -class Template: - """ - NOTE: This class is deprecated - """ - - def get_address(self): - """ - Return the address of the contract. - """ - return logic.address(self.get_program()) - - def get_program(self): - pass - - -class Split(Template): - """ - NOTE: This class is deprecated - - Split allows locking algos in an account which allows transfering to two - predefined addresses in a specified ratio such that for the given ratn and - ratd parameters we have: - - first_recipient_amount * rat_2 == second_recipient_amount * rat_1 - - Split also has an expiry round, after which the owner can transfer back - the funds. - - Arguments: - owner (str): an address that can receive the funds after the expiry - round - receiver_1 (str): first address to receive funds - receiver_2 (str): second address to receive funds - rat_1 (int): how much receiver_1 receives (proportionally) - rat_2 (int): how much receiver_2 receives (proportionally) - expiry_round (int): the round on which the funds can be transferred - back to owner - min_pay (int): the minimum number of microalgos that can be transferred - from the account to receiver_1 - max_fee (int): half the maximum fee that can be paid to the network by - the account - """ - - def __init__( - self, - owner: str, - receiver_1: str, - receiver_2: str, - rat_1: int, - rat_2: int, - expiry_round: int, - min_pay: int, - max_fee: int, - ): - self.owner = owner - self.receiver_1 = receiver_1 - self.receiver_2 = receiver_2 - self.rat_1 = rat_1 - self.rat_2 = rat_2 - self.expiry_round = expiry_round - self.min_pay = min_pay - self.max_fee = max_fee - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAIAQUCAAYHCAkmAyDYHIR7TIW5eM/WAZcXdEDqv7BD+baMN6i2/A5JatGbNCDKs" - "aoZHPQ3Zg8zZB/BZ1oDgt77LGo5np3rbto3/gloTyB40AS2H3I72YCbDk4hKpm7J7" - "NnFy2Xrt39TJG0ORFg+zEQIhIxASMMEDIEJBJAABkxCSgSMQcyAxIQMQglEhAxAiE" - "EDRAiQAAuMwAAMwEAEjEJMgMSEDMABykSEDMBByoSEDMACCEFCzMBCCEGCxIQMwAI" - "IQcPEBA=" - ) - orig = base64.b64decode(orig) - offsets = [4, 7, 8, 9, 10, 14, 47, 80] - values = [ - self.max_fee, - self.expiry_round, - self.rat_2, - self.rat_1, - self.min_pay, - self.owner, - self.receiver_1, - self.receiver_2, - ] - types = [int, int, int, int, int, "address", "address", "address"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_split_funds_transaction( - contract, amount: int, fee: int, first_valid, last_valid, gh - ): - """ - Return a group transactions array which transfers funds according to - the contract's ratio. - Args: - amount (int): total amount to be transferred - fee (int): fee per byte - first_valid (int): first round where the transactions are valid - gh (str): genesis hash in base64 - Returns: - Transaction[] - """ - address = logic.address(contract) - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 8 and len(bytearrays) == 3): - raise error.WrongContractError("split") - rat_1 = ints[6] - rat_2 = ints[5] - min_pay = ints[7] - max_fee = ints[1] - receiver_1 = encoding.encode_address(bytearrays[1]) - receiver_2 = encoding.encode_address(bytearrays[2]) - - amt_1 = 0 - amt_2 = 0 - - gcd = math.gcd(rat_1, rat_2) - rat_1 = rat_1 // gcd - rat_2 = rat_2 // gcd - - if amount % (rat_1 + rat_2) == 0: - amt_1 = amount // (rat_1 + rat_2) * rat_1 - amt_2 = amount - amt_1 - else: - raise error.TemplateInputError( - "the specified amount cannot be split into two " - + "parts with the ratio " - + str(rat_1) - + "/" - + str(rat_2) - ) - - if amt_1 < min_pay: - raise error.TemplateInputError( - "the amount paid to receiver_1 must be greater than " - + str(min_pay) - ) - - txn_1 = transaction.PaymentTxn( - address, fee, first_valid, last_valid, gh, receiver_1, amt_1 - ) - txn_2 = transaction.PaymentTxn( - address, fee, first_valid, last_valid, gh, receiver_2, amt_2 - ) - - transaction.assign_group_id([txn_1, txn_2]) - - lsig = transaction.LogicSig(contract) - - stx_1 = transaction.LogicSigTransaction(txn_1, lsig) - stx_2 = transaction.LogicSigTransaction(txn_2, lsig) - - if txn_1.fee > max_fee or txn_2.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - return [stx_1, stx_2] - - -class HTLC(Template): - """ - NOTE: This class is deprecated - - Hash Time Locked Contract allows a user to recieve the Algo prior to a - deadline (in terms of a round) by proving knowledge of a special value - or to forfeit the ability to claim, returning it to the payer. - - This contract is usually used to perform cross-chained atomic swaps. - - More formally, algos can be transfered under only two circumstances: - 1. To receiver if hash_function(arg_0) = hash_value - 2. To owner if txn.FirstValid > expiry_round - - Args: - owner (str): an address that can receive the asset after the expiry - round - receiver (str): address to receive Algos - hash_function (str): the hash function to be used (must be either - sha256 or keccak256) - hash_image (str): the hash image in base64 - expiry_round (int): the round on which the assets can be transferred - back to owner - max_fee (int): the maximum fee that can be paid to the network by the - account - """ - - def __init__( - self, - owner: str, - receiver: str, - hash_function: str, - hash_image: str, - expiry_round: int, - max_fee: int, - ): - self.owner = owner - self.receiver = receiver - self.hash_function = hash_function - self.hash_image = hash_image - self.expiry_round = expiry_round - self.max_fee = max_fee - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAEBQEABiYDIP68oLsUSlpOp7Q4pGgayA5soQW8tgf8VlMlyVaV9qITAQ" - + "Yg5pqWHm8tX3rIZgeSZVK+mCNe0zNjyoiRi7nJOKkVtvkxASIOMRAjEhAx" - + "BzIDEhAxCCQSEDEJKBItASkSEDEJKhIxAiUNEBEQ" - ) - orig = base64.b64decode(orig) - hash_inject = 0 - if self.hash_function == "sha256": - hash_inject = 1 - elif self.hash_function == "keccak256": - hash_inject = 2 - offsets = [3, 6, 10, 42, 45, 102] - values = [ - self.max_fee, - self.expiry_round, - self.receiver, - self.hash_image, - self.owner, - hash_inject, - ] - types = [int, int, "address", "base64", "address", int] - return inject(orig, offsets, values, types) - - @staticmethod - def get_transaction(contract, preimage, first_valid, last_valid, gh, fee): - """ - Return a transaction which will release funds if a matching preimage - is used. - - Args: - contract (bytes): the contract containing information, should be - received from payer - preimage (str): the preimage of the hash in base64 - first_valid (int): first valid round for the transactions - last_valid (int): last valid round for the transactions - gh (str): genesis hash in base64 - fee (int): fee per byte - - Returns: - LogicSigTransaction: transaction to claim algos from - contract account - """ - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 4 and len(bytearrays) == 3): - raise error.WrongContractError("split") - max_fee = ints[0] - hash_function = contract[-15] - expected_hash_image = bytearrays[1] - if hash_function == 1: - hash_image = SHA256.new() - hash_image.update(base64.b64decode(preimage)) - if hash_image.digest() != expected_hash_image: - raise error.TemplateInputError( - "the hash of the preimage does not match the expected " - "hash image using hash function sha256" - ) - elif hash_function == 2: - hash_image = keccak.new(digest_bits=256) - hash_image.update(base64.b64decode(preimage)) - if hash_image.digest() != expected_hash_image: - raise error.TemplateInputError( - "the hash of the preimage does not match the expected " - "hash image using hash function keccak256" - ) - else: - raise error.TemplateInputError( - "an invalid hash function was provided in the contract" - ) - - receiver = encoding.encode_address(bytearrays[0]) - - lsig = transaction.LogicSig(contract, [base64.b64decode(preimage)]) - txn = transaction.PaymentTxn( - logic.address(contract), - fee, - first_valid, - last_valid, - gh, - None, - 0, - close_remainder_to=receiver, - ) - - if txn.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - ltxn = transaction.LogicSigTransaction(txn, lsig) - return ltxn - - -class DynamicFee(Template): - """ - DynamicFee contract allows you to create a transaction without - specifying the fee. The fee will be determined at the moment of - transfer. - - Args: - receiver (str): address to receive the assets - amount (int): amount of assets to transfer - first_valid (int): first valid round for the transaction - last_valid (int, optional): last valid round for the transaction - (defaults to first_valid + 1000) - close_remainder_address (str, optional): the address that recieves the - remainder - """ - - def __init__( - self, - receiver: str, - amount: int, - first_valid: int, - last_valid: int = None, - close_remainder_address: str = None, - ): - self.lease_value = bytes( - [random.randint(0, 255) for x in range(constants.lease_length)] - ) - - self.last_valid = last_valid - if last_valid is None: - last_valid = first_valid + 1000 - - self.amount = amount - self.first_valid = first_valid - self.close_remainder_address = close_remainder_address - self.receiver = receiver - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAFAgEFBgcmAyD+vKC7FEpaTqe0OKRoGsgObKEFvLYH/FZTJclWlfaiEy" - + "DmmpYeby1feshmB5JlUr6YI17TM2PKiJGLuck4qRW2+QEGMgQiEjMAECMS" - + "EDMABzEAEhAzAAgxARIQMRYjEhAxECMSEDEHKBIQMQkpEhAxCCQSEDECJR" - + "IQMQQhBBIQMQYqEhA=" - ) - orig = base64.b64decode(orig) - offsets = [5, 6, 7, 11, 44, 76] - close = self.close_remainder_address - if close is None: - close = encoding.encode_address(bytes(32)) - values = [ - self.amount, - self.first_valid, - self.last_valid, - self.receiver, - close, - base64.b64encode(self.lease_value), - ] - types = [int, int, int, "address", "address", "base64"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_transactions(txn, lsig, private_key, fee): - """ - Create and sign the secondary dynamic fee transaction, update - transaction fields, and sign as the fee payer; return both - transactions. - - Args: - txn (Transaction): main transaction from payer - lsig (LogicSig): signed logic received from payer - private_key (str): the secret key of the account that pays the fee - in base64 - fee (int): fee per byte, for both transactions - """ - txn.fee = fee - txn.fee = max(constants.min_txn_fee, fee * txn.estimate_size()) - - # reimbursement transaction - address = account.address_from_private_key(private_key) - txn_2 = transaction.PaymentTxn( - address, - fee, - txn.first_valid_round, - txn.last_valid_round, - txn.genesis_hash, - txn.sender, - txn.fee, - lease=txn.lease, - ) - - transaction.assign_group_id([txn_2, txn]) - - stx_1 = transaction.LogicSigTransaction(txn, lsig) - stx_2 = txn_2.sign(private_key) - - return [stx_2, stx_1] - - def sign_dynamic_fee(self, private_key, gh): - """ - Return the main transaction and signed logic needed to complete the - transfer. These should be sent to the fee payer, who can use - get_transactions() to update fields and create the auxiliary - transaction. - - Args: - private_key (bytes): the secret key to sign the contract in base64 - gh (str): genesis hash, in base64 - """ - sender = account.address_from_private_key(private_key) - - # main transaction - txn = transaction.PaymentTxn( - sender, - 0, - self.first_valid, - self.last_valid, - gh, - self.receiver, - self.amount, - lease=self.lease_value, - close_remainder_to=self.close_remainder_address, - ) - lsig = transaction.LogicSig(self.get_program()) - lsig.sign(private_key) - - return txn, lsig - - -class PeriodicPayment(Template): - """ - NOTE: This class is deprecated - - PeriodicPayment contract enables creating an account which allows the - withdrawal of a fixed amount of assets every fixed number of rounds to a - specific Algrorand Address. In addition, the contract allows to add - timeout, after which the address can withdraw the rest of the assets. - - Args: - receiver (str): address to receive the assets - amount (int): amount of assets to transfer at every cycle - withdrawing_window (int): the number of blocks in which the user can - withdraw the asset once the period start (must be < 1000) - period (int): how often the address can withdraw assets (in rounds) - fee (int): maximum fee per transaction - timeout (int): a round in which the receiver can withdraw the rest of - the funds after - """ - - def __init__( - self, - receiver: str, - amount: int, - withdrawing_window: int, - period: int, - max_fee: int, - timeout: int, - ): - self.lease_value = bytes( - [random.randint(0, 255) for x in range(constants.lease_length)] - ) - self.receiver = receiver - self.amount = amount - self.withdrawing_window = withdrawing_window - self.period = period - self.max_fee = max_fee - self.timeout = timeout - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAHAQoLAAwNDiYCAQYg/ryguxRKWk6ntDikaBrIDmyhBby2B/xWUyXJVp" - + "X2ohMxECISMQEjDhAxAiQYJRIQMQQhBDECCBIQMQYoEhAxCTIDEjEHKRIQ" - + "MQghBRIQMQkpEjEHMgMSEDECIQYNEDEIJRIQERA=" - ) - orig = base64.b64decode(orig) - offsets = [4, 5, 7, 8, 9, 12, 15] - values = [ - self.max_fee, - self.period, - self.withdrawing_window, - self.amount, - self.timeout, - base64.b64encode(self.lease_value), - self.receiver, - ] - types = [int, int, int, int, int, "base64", "address"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_withdrawal_transaction(contract, first_valid, gh, fee): - """ - Return the withdrawal transaction to be sent to the network. - - Args: - contract (bytes): contract containing information, should be - received from payer - first_valid (int): first round the transaction should be valid; - this must be a multiple of self.period - gh (str): genesis hash in base64 - fee (int): fee per byte - """ - address = logic.address(contract) - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 7 and len(bytearrays) == 2): - raise error.WrongContractError("periodic payment") - amount = ints[5] - withdrawing_window = ints[4] - period = ints[2] - max_fee = ints[1] - lease_value = bytearrays[0] - receiver = encoding.encode_address(bytearrays[1]) - - if first_valid % period != 0: - raise error.TemplateInputError( - "first_valid must be divisible by the period" - ) - txn = transaction.PaymentTxn( - address, - fee, - first_valid, - first_valid + withdrawing_window, - gh, - receiver, - amount, - lease=lease_value, - ) - - if txn.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - lsig = transaction.LogicSig(contract) - stx = transaction.LogicSigTransaction(txn, lsig) - return stx - - -class LimitOrder(Template): - """ - NOTE: This class is deprecated - - Limit Order allows to trade Algos for other assets given a specific ratio; - for N Algos, swap for Rate * N Assets. - - Args: - owner (str): an address that can receive the asset after the expiry - round - asset_id (int): asset to be transfered - ratn (int): the numerator of the exchange rate - ratd (int): the denominator of the exchange rate - expiry_round (int): the round on which the assets can be transferred - back to owner - max_fee (int): the maximum fee that can be paid to the network by the - account - min_trade (int): the minimum amount (of Algos) to be traded away - """ - - def __init__( - self, - owner: str, - asset_id: int, - ratn: int, - ratd: int, - expiry_round: int, - max_fee: int, - min_trade: int, - ): - self.owner = owner - self.ratn = ratn - self.ratd = ratd - self.expiry_round = expiry_round - self.min_trade = min_trade - self.max_fee = max_fee - self.asset_id = asset_id - - def get_program(self): - """ - Return a byte array to be used in LogicSig. - """ - orig = ( - "ASAKAAEFAgYEBwgJHSYBIJKvkYTkEzwJf2arzJOxERsSogG9nQzKPkpIoc" - + "4TzPTFMRYiEjEQIxIQMQEkDhAyBCMSQABVMgQlEjEIIQQNEDEJMgMSEDMB" - + "ECEFEhAzAREhBhIQMwEUKBIQMwETMgMSEDMBEiEHHTUCNQExCCEIHTUENQ" - + "M0ATQDDUAAJDQBNAMSNAI0BA8QQAAWADEJKBIxAiEJDRAxBzIDEhAxCCIS" - + "EBA=" - ) - orig = base64.b64decode(orig) - offsets = [5, 7, 9, 10, 11, 12, 16] - values = [ - self.max_fee, - self.min_trade, - self.asset_id, - self.ratd, - self.ratn, - self.expiry_round, - self.owner, - ] - types = [int, int, int, int, int, int, "address"] - return inject(orig, offsets, values, types) - - @staticmethod - def get_swap_assets_transactions( - contract: bytes, - asset_amount: int, - microalgo_amount: int, - private_key: str, - first_valid, - last_valid, - gh, - fee, - ): - """ - Return a group transactions array which transfer funds according to - the contract's ratio. - - Args: - contract (bytes): the contract containing information, should be - received from payer - asset_amount (int): the amount of assets to be sent - microalgo_amount (int): the amount of microalgos to be received - private_key (str): the secret key to sign the contract - first_valid (int): first valid round for the transactions - last_valid (int): last valid round for the transactions - gh (str): genesis hash in base64 - fee (int): fee per byte - """ - address = logic.address(contract) - _, ints, bytearrays = logic.read_program(contract) - if not (len(ints) == 10 and len(bytearrays) == 1): - raise error.WrongContractError( - "Wrong contract provided; a limit order contract" - + " is needed" - ) - min_trade = ints[4] - asset_id = ints[6] - ratn = ints[8] - ratd = ints[7] - max_fee = ints[2] - owner = encoding.encode_address(bytearrays[0]) - - if microalgo_amount < min_trade: - raise error.TemplateInputError( - "At least " + str(min_trade) + " microalgos must be requested" - ) - - if asset_amount * ratd < microalgo_amount * ratn: - raise error.TemplateInputError( - "The exchange ratio of assets to microalgos must be at least " - + str(ratn) - + " / " - + str(ratd) - ) - - txn_1 = transaction.PaymentTxn( - address, - fee, - first_valid, - last_valid, - gh, - account.address_from_private_key(private_key), - int(microalgo_amount), - ) - - txn_2 = transaction.AssetTransferTxn( - account.address_from_private_key(private_key), - fee, - first_valid, - last_valid, - gh, - owner, - asset_amount, - asset_id, - ) - - if txn_1.fee > max_fee or txn_2.fee > max_fee: - raise error.TemplateInputError( - "the transaction fee should not be greater than " - + str(max_fee) - ) - - transaction.assign_group_id([txn_1, txn_2]) - - lsig = transaction.LogicSig(contract) - stx_1 = transaction.LogicSigTransaction(txn_1, lsig) - stx_2 = txn_2.sign(private_key) - - return [stx_1, stx_2] - - -def put_uvarint(buf, x): - i = 0 - while x >= 0x80: - buf.append((x & 0xFF) | 0x80) - x >>= 7 - i += 1 - - buf.append(x & 0xFF) - return i + 1 - - -def inject(orig, offsets, values, values_types): - # make sure we have enough values - assert len(offsets) == len(values) == len(values_types) - - res = orig[:] - - def replace(arr, new_val, offset, place_holder_length): - return arr[:offset] + new_val + arr[offset + place_holder_length :] - - for i in range(len(offsets)): - val = values[i] - val_type = values_types[i] - dec_len = 0 - - if val_type == int: - buf = [] - dec_len = put_uvarint(buf, val) - 1 - val = bytes(buf) - res = replace(res, val, offsets[i], 1) - - elif val_type == "address": - val = encoding.decode_address(val) - res = replace(res, val, offsets[i], 32) - - elif val_type == "base64": - val = bytes(base64.b64decode(val)) - buf = [] - dec_len = put_uvarint(buf, len(val)) + len(val) - 2 - res = replace(res, bytes(buf) + val, offsets[i], 2) - - else: - raise Exception("Unkown Type") - - # update offsets - if dec_len != 0: - for o in range(len(offsets)): - offsets[o] += dec_len - - return res diff --git a/algosdk/testing/dryrun.py b/algosdk/testing/dryrun.py index bc561e16..bebc406a 100644 --- a/algosdk/testing/dryrun.py +++ b/algosdk/testing/dryrun.py @@ -2,10 +2,10 @@ import binascii import string from dataclasses import dataclass -from typing import List, Union +from typing import Union, List, Optional from algosdk.constants import payment_txn, appcall_txn, ZERO_ADDRESS -from algosdk.future import transaction +from algosdk import transaction from algosdk.encoding import encode_address, msgpack_encode from algosdk.v2client.models import ( DryrunRequest, @@ -25,7 +25,7 @@ class LSig: """Logic Sig program parameters""" - args: List[bytes] = None + args: Optional[List[bytes]] = None @dataclass @@ -33,12 +33,12 @@ class App: """Application program parameters""" creator: str = ZERO_ADDRESS - round: int = None + round: Optional[int] = None app_idx: int = 0 on_complete: int = 0 - args: List[bytes] = None - accounts: List[Union[str, Account]] = None - global_state: List[TealKeyValue] = None + args: Optional[List[bytes]] = None + accounts: Optional[List[Union[str, Account]]] = None + global_state: Optional[List[TealKeyValue]] = None class DryrunTestCaseMixin: @@ -621,7 +621,7 @@ def _build_logicsig_txn(program, txn, lsig): # replacing program with an empty one is OK since it set by source # LogicSig does not like None/invalid programs because of validation program = program if isinstance(program, bytes) else b"\x01" - logicsig = transaction.LogicSig(program, lsig.args) + logicsig = transaction.LogicSigAccount(program, lsig.args) return transaction.LogicSigTransaction(txn, logicsig) @staticmethod diff --git a/algosdk/transaction.py b/algosdk/transaction.py index bec36028..e25af8da 100644 --- a/algosdk/transaction.py +++ b/algosdk/transaction.py @@ -1,66 +1,118 @@ import base64 import binascii -import warnings +import msgpack +from enum import IntEnum +from typing import List, Union, Optional, cast from collections import OrderedDict -import msgpack +from algosdk import account, constants, encoding, error, logic +from algosdk.box_reference import BoxReference +from algosdk.v2client import algod, models from nacl.exceptions import BadSignatureError from nacl.signing import SigningKey, VerifyKey -from . import account, constants, encoding, error, future, logic - -class Transaction: +class SuggestedParams: """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. + Contains various fields common to all transaction types. - Superclass for various transaction types. + Args: + fee (int): transaction fee (per byte if flat_fee is false). When flat_fee is true, + fee may fall to zero but a group of N atomic transactions must + still have a fee of at least N*min_txn_fee. + first (int): first round for which the transaction is valid + last (int): last round for which the transaction is valid + gh (str): genesis hash + gen (str, optional): genesis id + flat_fee (bool, optional): whether the specified fee is a flat fee + consensus_version (str, optional): the consensus protocol version as of 'first' + min_fee (int, optional): the minimum transaction fee (flat) + + Attributes: + fee (int) + first (int) + last (int) + gen (str) + gh (str) + flat_fee (bool) + consensus_version (str) + min_fee (int) """ def __init__( self, - sender, fee, first, last, - note, - gen, gh, - lease, - txn_type, - rekey_to, + gen=None, + flat_fee=False, + consensus_version=None, + min_fee=None, ): - warnings.warn( - "`Transaction` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) - self.sender = sender + self.first = first + self.last = last + self.gh = gh + self.gen = gen self.fee = fee - self.first_valid_round = first - self.last_valid_round = last - self.note = note - if self.note is not None: - if not isinstance(self.note, bytes): - raise error.WrongNoteType - if len(self.note) > constants.note_max_length: - raise error.WrongNoteLength - self.genesis_id = gen - self.genesis_hash = gh + self.flat_fee = flat_fee + self.consensus_version = consensus_version + self.min_fee = min_fee + + +class Transaction: + """ + Superclass for various transaction types. + """ + + def __init__(self, sender, sp, note, lease, txn_type, rekey_to): + self.sender = sender + self.fee = sp.fee + self.first_valid_round = sp.first + self.last_valid_round = sp.last + self.note = self.as_note(note) + self.genesis_id = sp.gen + self.genesis_hash = sp.gh self.group = None - self.lease = lease - if self.lease is not None: - if len(self.lease) != constants.lease_length: - raise error.WrongLeaseLengthError + self.lease = self.as_lease(lease) self.type = txn_type self.rekey_to = rekey_to + @staticmethod + def as_hash(hash): + """Confirm that a value is 32 bytes. If all zeros, or a falsy value, return None""" + if not hash: + return None + assert isinstance(hash, (bytes, bytearray)), "{} is not bytes".format( + hash + ) + if len(hash) != constants.hash_len: + raise error.WrongHashLengthError + if not any(hash): + return None + return hash + + @staticmethod + def as_note(note): + if not note: + return None + if not isinstance(note, (bytes, bytearray, str)): + raise error.WrongNoteType + if isinstance(note, str): + note = note.encode() + if len(note) > constants.note_max_length: + raise error.WrongNoteLength + return note + + @classmethod + def as_lease(cls, lease): + try: + return cls.as_hash(lease) + except error.WrongHashLengthError: + raise error.WrongLeaseLengthError + def get_txid(self): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Get the transaction's ID. Returns: @@ -74,9 +126,6 @@ def get_txid(self): def sign(self, private_key): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Sign the transaction with a private key. Args: @@ -93,11 +142,24 @@ def sign(self, private_key): stx = SignedTransaction(self, sig, authorizing_address) return stx - def raw_sign(self, private_key): + def _sign_and_skip_rekey_check(self, private_key): + """ + Sign the transaction with a private key, skipping rekey check. + This is only used for size estimation. + + Args: + private_key (str): the private key of the signing account + + Returns: + SignedTransaction: signed transaction with the signature """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. + sig = self.raw_sign(private_key) + sig = base64.b64encode(sig).decode() + stx = SignedTransaction(self, sig) + return stx + def raw_sign(self, private_key): + """ Sign the transaction. Args: @@ -115,19 +177,11 @@ def raw_sign(self, private_key): return sig def estimate_size(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ sk, _ = account.generate_account() - stx = self.sign(sk) + stx = self._sign_and_skip_rekey_check(sk) return len(base64.b64decode(encoding.msgpack_encode(stx))) def dictify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ d = dict() if self.fee: d["fee"] = self.fee @@ -152,19 +206,18 @@ def dictify(self): @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ + sp = SuggestedParams( + d["fee"] if "fee" in d else 0, + d["fv"] if "fv" in d else 0, + d["lv"], + base64.b64encode(d["gh"]).decode(), + d["gen"] if "gen" in d else None, + flat_fee=True, + ) args = { + "sp": sp, "sender": encoding.encode_address(d["snd"]), - "fee": d["fee"] if "fee" in d else 0, - "first": d["fv"] if "fv" in d else 0, - "last": d["lv"], - "gh": base64.b64encode(d["gh"]).decode(), "note": d["note"] if "note" in d else None, - "gen": d["gen"] if "gen" in d else None, - "flat_fee": True, "lease": d["lx"] if "lx" in d else None, "rekey_to": encoding.encode_address(d["rekey"]) if "rekey" in d @@ -177,8 +230,22 @@ def undictify(d): args.update(PaymentTxn._undictify(d)) txn = PaymentTxn(**args) elif txn_type == constants.keyreg_txn: - args.update(KeyregTxn._undictify(d)) - txn = KeyregTxn(**args) + if "nonpart" in d and d["nonpart"]: + args.update(KeyregNonparticipatingTxn._undictify(d)) + txn = KeyregNonparticipatingTxn(**args) + else: + if ( + "votekey" not in d + and "selkey" not in d + and "votefst" not in d + and "votelst" not in d + and "votekd" not in d + ): + args.update(KeyregOfflineTxn._undictify(d)) + txn = KeyregOfflineTxn(**args) + else: + args.update(KeyregOnlineTxn._undictify(d)) + txn = KeyregOnlineTxn(**args) elif txn_type == constants.assetconfig_txn: args.update(AssetConfigTxn._undictify(d)) txn = AssetConfigTxn(**args) @@ -188,18 +255,20 @@ def undictify(d): elif txn_type == constants.assettransfer_txn: args.update(AssetTransferTxn._undictify(d)) txn = AssetTransferTxn(**args) + elif txn_type == constants.appcall_txn: + args.update(ApplicationCallTxn._undictify(d)) + txn = ApplicationCallTxn(**args) + elif txn_type == constants.stateproof_txn: + # a state proof txn does not have these fields + args.pop("note"), args.pop("rekey_to"), args.pop("lease") + args.update(StateProofTxn._undictify(d)) + txn = StateProofTxn(**args) if "grp" in d: txn.group = d["grp"] return txn def __eq__(self, other): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - if not isinstance( - other, (Transaction, future.transaction.Transaction) - ): + if not isinstance(other, Transaction): return False return ( self.sender == other.sender @@ -215,29 +284,44 @@ def __eq__(self, other): and self.rekey_to == other.rekey_to ) + @staticmethod + def required(arg): + if not arg: + raise ValueError("{} supplied as a required argument".format(arg)) + return arg + + @staticmethod + def creatable_index(index, required=False): + """Coerce an index for apps or assets to an integer. + + By using this in all constructors, we allow callers to use + strings as indexes, check our convenience Txn types to ensure + index is set, and ensure that 0 is always used internally for + an unset id, not None, so __eq__ works properly. + """ + i = int(index or 0) + if i == 0 and required: + raise IndexError("Required an index") + if i < 0: + raise IndexError(i) + return i + + def __str__(self): + return str(self.__dict__) + class PaymentTxn(Transaction): """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - Represents a payment transaction. Args: sender (str): address of the sender - fee (int): transaction fee (per byte if flat_fee is false). When flat_fee is true, - fee may fall to zero but a group of N atomic transactions must - still have a fee of at least N*min_txn_fee. - first (int): first round for which the transaction is valid - last (int): last round for which the transaction is valid - gh (str): genesis_hash + sp (SuggestedParams): suggested params from algod receiver (str): address of the receiver amt (int): amount in microAlgos to be sent close_remainder_to (str, optional): if nonempty, account will be closed and remaining algos will be sent to this address note (bytes, optional): arbitrary optional bytes - gen (str, optional): genesis_id - flat_fee (bool, optional): whether the specified fee is a flat fee lease (byte[32], optional): specifies a lease, and no other transaction with the same sender and lease can be confirmed in this transaction's valid rounds @@ -263,41 +347,27 @@ class PaymentTxn(Transaction): def __init__( self, sender, - fee, - first, - last, - gh, + sp, receiver, amt, close_remainder_to=None, note=None, - gen=None, - flat_fee=False, lease=None, rekey_to=None, ): Transaction.__init__( - self, - sender, - fee, - first, - last, - note, - gen, - gh, - lease, - constants.payment_txn, - rekey_to, + self, sender, sp, note, lease, constants.payment_txn, rekey_to ) if receiver: self.receiver = receiver else: raise error.ZeroAddressError + self.amt = amt if (not isinstance(self.amt, int)) or self.amt < 0: raise error.WrongAmountType self.close_remainder_to = close_remainder_to - if not flat_fee: + if not sp.flat_fee: self.fee = max( self.estimate_size() * self.fee, constants.min_txn_fee ) @@ -327,12 +397,12 @@ def _undictify(d): "amt": d["amt"] if "amt" in d else 0, "receiver": encoding.encode_address(d["rcv"]) if "rcv" in d - else None, + else constants.ZERO_ADDRESS, } return args def __eq__(self, other): - if not isinstance(other, (PaymentTxn, future.transaction.PaymentTxn)): + if not isinstance(other, PaymentTxn): return False return ( super(PaymentTxn, self).__eq__(other) @@ -344,31 +414,23 @@ def __eq__(self, other): class KeyregTxn(Transaction): """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - Represents a key registration transaction. Args: sender (str): address of sender - fee (int): transaction fee (per byte if flat_fee is false). When flat_fee is true, - fee may fall to zero but a group of N atomic transactions must - still have a fee of at least N*min_txn_fee. - first (int): first round for which the transaction is valid - last (int): last round for which the transaction is valid - gh (str): genesis_hash - votekey (str): participation public key - selkey (str): VRF public key + sp (SuggestedParams): suggested params from algod + votekey (str): participation public key in base64 + selkey (str): VRF public key in base64 votefst (int): first round to vote votelst (int): last round to vote votekd (int): vote key dilution note (bytes, optional): arbitrary optional bytes - gen (str, optional): genesis_id - flat_fee (bool, optional): whether the specified fee is a flat fee lease (byte[32], optional): specifies a lease, and no other transaction with the same sender and lease can be confirmed in this transaction's valid rounds rekey_to (str, optional): additionally rekey the sender to this address + nonpart (bool, optional): mark the account non-participating if true + StateProofPK: state proof Attributes: sender (str) @@ -387,262 +449,471 @@ class KeyregTxn(Transaction): type (str) lease (byte[32]) rekey_to (str) + nonpart (bool) + sprfkey (str) """ def __init__( self, sender, - fee, - first, - last, - gh, + sp, votekey, selkey, votefst, votelst, votekd, note=None, - gen=None, - flat_fee=False, lease=None, rekey_to=None, + nonpart=None, + sprfkey=None, ): Transaction.__init__( - self, - sender, - fee, - first, - last, - note, - gen, - gh, - lease, - constants.keyreg_txn, - rekey_to, + self, sender, sp, note, lease, constants.keyreg_txn, rekey_to ) self.votepk = votekey self.selkey = selkey self.votefst = votefst self.votelst = votelst self.votekd = votekd - if not flat_fee: + self.nonpart = nonpart + self.sprfkey = sprfkey + + if not sp.flat_fee: self.fee = max( self.estimate_size() * self.fee, constants.min_txn_fee ) def dictify(self): - d = { - "selkey": encoding.decode_address(self.selkey), - "votefst": self.votefst, - "votekd": self.votekd, - "votekey": encoding.decode_address(self.votepk), - "votelst": self.votelst, - } + d = {} + if self.selkey is not None: + d["selkey"] = base64.b64decode(self.selkey) + if self.votefst is not None: + d["votefst"] = self.votefst + if self.votekd is not None: + d["votekd"] = self.votekd + if self.votepk is not None: + d["votekey"] = base64.b64decode(self.votepk) + if self.votelst is not None: + d["votelst"] = self.votelst + if self.nonpart is not None: + d["nonpart"] = self.nonpart + if self.sprfkey is not None: + d["sprfkey"] = base64.b64decode(self.sprfkey) + d.update(super(KeyregTxn, self).dictify()) od = OrderedDict(sorted(d.items())) return od - @staticmethod - def _undictify(d): - args = { - "votekey": encoding.encode_address(d["votekey"]), - "selkey": encoding.encode_address(d["selkey"]), - "votefst": d["votefst"], - "votelst": d["votelst"], - "votekd": d["votekd"], - } - return args - def __eq__(self, other): - if not isinstance(other, (KeyregTxn, future.transaction.KeyregTxn)): + if not isinstance(other, KeyregTxn): return False return ( - super(KeyregTxn, self).__eq__(self, other) + super(KeyregTxn, self).__eq__(other) and self.votepk == other.votepk and self.selkey == other.selkey and self.votefst == other.votefst and self.votelst == other.votelst and self.votekd == other.votekd + and self.nonpart == other.nonpart + and self.sprfkey == other.sprfkey ) -class AssetConfigTxn(Transaction): +class KeyregOnlineTxn(KeyregTxn): """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - - Represents a transaction for asset creation, reconfiguration, or - destruction. - - To create an asset, include the following: - total, default_frozen, unit_name, asset_name, - manager, reserve, freeze, clawback, url, metadata, - decimals - - To destroy an asset, include the following: - index, strict_empty_address_check (set to False) - - To update asset configuration, include the following: - index, manager, reserve, freeze, clawback, - strict_empty_address_check (optional) + Represents an online key registration transaction. + nonpart is implicitly False for this transaction. Args: - sender (str): address of the sender - fee (int): transaction fee (per byte if flat_fee is false). When flat_fee is true, - fee may fall to zero but a group of N atomic transactions must - still have a fee of at least N*min_txn_fee. - first (int): first round for which the transaction is valid - last (int): last round for which the transaction is valid - gh (str): genesis_hash - index (int, optional): index of the asset - total (int, optional): total number of base units of this asset created - default_frozen (bool, optional): whether slots for this asset in user - accounts are frozen by default - unit_name (str, optional): hint for the name of a unit of this asset - asset_name (str, optional): hint for the name of the asset - manager (str, optional): address allowed to change nonzero addresses - for this asset - reserve (str, optional): account whose holdings of this asset should - be reported as "not minted" - freeze (str, optional): account allowed to change frozen state of - holdings of this asset - clawback (str, optional): account allowed take units of this asset - from any account - url (str, optional): a URL where more information about the asset - can be retrieved - metadata_hash (byte[32], optional): a commitment to some unspecified - asset metadata (32 byte hash) + sender (str): address of sender + sp (SuggestedParams): suggested params from algod + votekey (str): participation public key in base64 + selkey (str): VRF public key in base64 + votefst (int): first round to vote + votelst (int): last round to vote + votekd (int): vote key dilution note (bytes, optional): arbitrary optional bytes - gen (str, optional): genesis_id - flat_fee (bool, optional): whether the specified fee is a flat fee lease (byte[32], optional): specifies a lease, and no other transaction with the same sender and lease can be confirmed in this transaction's valid rounds - strict_empty_address_check (bool, optional): set this to False if you - want to specify empty addresses. Otherwise, if this is left as - True (the default), having empty addresses will raise an error, - which will prevent accidentally removing admin access to assets or - deleting the asset. - decimals (int, optional): number of digits to use for display after - decimal. If set to 0, the asset is not divisible. If set to 1, the - base unit of the asset is in tenths. Must be between 0 and 19, - inclusive. Defaults to 0. rekey_to (str, optional): additionally rekey the sender to this address + sprfkey (str, optional): state proof ID Attributes: sender (str) fee (int) first_valid_round (int) last_valid_round (int) - genesis_hash (str) - index (int) - total (int) - default_frozen (bool) - unit_name (str) - asset_name (str) - manager (str) - reserve (str) - freeze (str) - clawback (str) - url (str) - metadata_hash (byte[32]) note (bytes) genesis_id (str) + genesis_hash (str) + group(bytes) + votepk (str) + selkey (str) + votefst (int) + votelst (int) + votekd (int) type (str) lease (byte[32]) - decimals (int) rekey_to (str) + sprfkey (str) """ def __init__( self, sender, - fee, - first, - last, - gh, - index=None, - total=None, - default_frozen=None, - unit_name=None, - asset_name=None, - manager=None, - reserve=None, - freeze=None, - clawback=None, - url=None, - metadata_hash=None, + sp, + votekey, + selkey, + votefst, + votelst, + votekd, note=None, - gen=None, - flat_fee=False, lease=None, - strict_empty_address_check=True, - decimals=0, rekey_to=None, + sprfkey=None, ): - Transaction.__init__( + KeyregTxn.__init__( self, sender, - fee, - first, - last, + sp, + votekey, + selkey, + votefst, + votelst, + votekd, note, - gen, - gh, lease, - constants.assetconfig_txn, rekey_to, + nonpart=False, + sprfkey=sprfkey, ) - if strict_empty_address_check: - if not (manager and reserve and freeze and clawback): - raise error.EmptyAddressError - self.index = index - self.total = total - self.default_frozen = default_frozen - self.unit_name = unit_name - self.asset_name = asset_name - self.manager = manager - self.reserve = reserve - self.freeze = freeze - self.clawback = clawback - self.url = url - self.metadata_hash = metadata_hash - self.decimals = decimals - if decimals < 0 or decimals > constants.max_asset_decimals: - raise error.OutOfRangeDecimalsError - if metadata_hash is not None: - if len(metadata_hash) != constants.metadata_length: - raise error.WrongMetadataLengthError - if not flat_fee: + self.votepk = votekey + self.selkey = selkey + self.votefst = votefst + self.votelst = votelst + self.votekd = votekd + self.sprfkey = sprfkey + if votekey is None: + raise error.KeyregOnlineTxnInitError("votekey") + if selkey is None: + raise error.KeyregOnlineTxnInitError("selkey") + if votefst is None: + raise error.KeyregOnlineTxnInitError("votefst") + if votelst is None: + raise error.KeyregOnlineTxnInitError("votelst") + if votekd is None: + raise error.KeyregOnlineTxnInitError("votekd") + if not sp.flat_fee: self.fee = max( self.estimate_size() * self.fee, constants.min_txn_fee ) - def dictify(self): - d = dict() + @staticmethod + def _undictify(d): + votekey = base64.b64encode(d["votekey"]).decode() + selkey = base64.b64encode(d["selkey"]).decode() + votefst = d["votefst"] + votelst = d["votelst"] + votekd = d["votekd"] + if "sprfkey" in d: + sprfID = base64.b64encode(d["sprfkey"]).decode() + + args = { + "votekey": votekey, + "selkey": selkey, + "votefst": votefst, + "votelst": votelst, + "votekd": votekd, + "sprfkey": sprfID, + } + else: + args = { + "votekey": votekey, + "selkey": selkey, + "votefst": votefst, + "votelst": votelst, + "votekd": votekd, + } - if ( - self.total - or self.default_frozen - or self.unit_name - or self.asset_name - or self.manager - or self.reserve - or self.freeze - or self.clawback - or self.decimals - ): - apar = OrderedDict() - if self.metadata_hash: - apar["am"] = self.metadata_hash - if self.asset_name: - apar["an"] = self.asset_name - if self.url: - apar["au"] = self.url - if self.clawback: - apar["c"] = encoding.decode_address(self.clawback) - if self.decimals: + return args + + def __eq__(self, other): + if not isinstance(other, KeyregOnlineTxn): + return False + return super(KeyregOnlineTxn, self).__eq__(other) + + +class KeyregOfflineTxn(KeyregTxn): + """ + Represents an offline key registration transaction. + nonpart is implicitly False for this transaction. + + Args: + sender (str): address of sender + sp (SuggestedParams): suggested params from algod + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + rekey_to (str, optional): additionally rekey the sender to this address + + Attributes: + sender (str) + fee (int) + first_valid_round (int) + last_valid_round (int) + note (bytes) + genesis_id (str) + genesis_hash (str) + group(bytes) + type (str) + lease (byte[32]) + rekey_to (str) + """ + + def __init__(self, sender, sp, note=None, lease=None, rekey_to=None): + KeyregTxn.__init__( + self, + sender, + sp, + None, + None, + None, + None, + None, + note=note, + lease=lease, + rekey_to=rekey_to, + nonpart=False, + sprfkey=None, + ) + if not sp.flat_fee: + self.fee = max( + self.estimate_size() * self.fee, constants.min_txn_fee + ) + + @staticmethod + def _undictify(d): + args = {} + return args + + def __eq__(self, other): + if not isinstance(other, KeyregOfflineTxn): + return False + return super(KeyregOfflineTxn, self).__eq__(other) + + +class KeyregNonparticipatingTxn(KeyregTxn): + """ + Represents a nonparticipating key registration transaction. + nonpart is implicitly True for this transaction. + + Args: + sender (str): address of sender + sp (SuggestedParams): suggested params from algod + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + rekey_to (str, optional): additionally rekey the sender to this address + + Attributes: + sender (str) + fee (int) + first_valid_round (int) + last_valid_round (int) + note (bytes) + genesis_id (str) + genesis_hash (str) + group(bytes) + type (str) + lease (byte[32]) + rekey_to (str) + """ + + def __init__(self, sender, sp, note=None, lease=None, rekey_to=None): + KeyregTxn.__init__( + self, + sender, + sp, + None, + None, + None, + None, + None, + note=note, + lease=lease, + rekey_to=rekey_to, + nonpart=True, + sprfkey=None, + ) + if not sp.flat_fee: + self.fee = max( + self.estimate_size() * self.fee, constants.min_txn_fee + ) + + @staticmethod + def _undictify(d): + args = {} + return args + + def __eq__(self, other): + if not isinstance(other, KeyregNonparticipatingTxn): + return False + return super(KeyregNonparticipatingTxn, self).__eq__(other) + + +class AssetConfigTxn(Transaction): + """ + Represents a transaction for asset creation, reconfiguration, or + destruction. + + To create an asset, include the following: + total, default_frozen, unit_name, asset_name, + manager, reserve, freeze, clawback, url, metadata, + decimals + + To destroy an asset, include the following: + index, strict_empty_address_check (set to False) + + To update asset configuration, include the following: + index, manager, reserve, freeze, clawback, + strict_empty_address_check (optional) + + Args: + sender (str): address of the sender + sp (SuggestedParams): suggested params from algod + index (int, optional): index of the asset + total (int, optional): total number of base units of this asset created + default_frozen (bool, optional): whether slots for this asset in user + accounts are frozen by default + unit_name (str, optional): hint for the name of a unit of this asset + asset_name (str, optional): hint for the name of the asset + manager (str, optional): address allowed to change nonzero addresses + for this asset + reserve (str, optional): account whose holdings of this asset should + be reported as "not minted" + freeze (str, optional): account allowed to change frozen state of + holdings of this asset + clawback (str, optional): account allowed take units of this asset + from any account + url (str, optional): a URL where more information about the asset + can be retrieved + metadata_hash (byte[32], optional): a commitment to some unspecified + asset metadata (32 byte hash) + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + strict_empty_address_check (bool, optional): set this to False if you + want to specify empty addresses. Otherwise, if this is left as + True (the default), having empty addresses will raise an error, + which will prevent accidentally removing admin access to assets or + deleting the asset. + decimals (int, optional): number of digits to use for display after + decimal. If set to 0, the asset is not divisible. If set to 1, the + base unit of the asset is in tenths. Must be between 0 and 19, + inclusive. Defaults to 0. + rekey_to (str, optional): additionally rekey the sender to this address + + Attributes: + sender (str) + fee (int) + first_valid_round (int) + last_valid_round (int) + genesis_hash (str) + index (int) + total (int) + default_frozen (bool) + unit_name (str) + asset_name (str) + manager (str) + reserve (str) + freeze (str) + clawback (str) + url (str) + metadata_hash (byte[32]) + note (bytes) + genesis_id (str) + type (str) + lease (byte[32]) + decimals (int) + rekey (str) + """ + + def __init__( + self, + sender, + sp, + index=None, + total=None, + default_frozen=None, + unit_name=None, + asset_name=None, + manager=None, + reserve=None, + freeze=None, + clawback=None, + url=None, + metadata_hash=None, + note=None, + lease=None, + strict_empty_address_check=True, + decimals=0, + rekey_to=None, + ): + Transaction.__init__( + self, sender, sp, note, lease, constants.assetconfig_txn, rekey_to + ) + if strict_empty_address_check: + if not (manager and reserve and freeze and clawback): + raise error.EmptyAddressError + self.index = self.creatable_index(index) + self.total = int(total) if total else None + self.default_frozen = bool(default_frozen) + self.unit_name = unit_name + self.asset_name = asset_name + self.manager = manager + self.reserve = reserve + self.freeze = freeze + self.clawback = clawback + self.url = url + self.metadata_hash = self.as_metadata(metadata_hash) + self.decimals = int(decimals) + if self.decimals < 0 or self.decimals > constants.max_asset_decimals: + raise error.OutOfRangeDecimalsError + if not sp.flat_fee: + self.fee = max( + self.estimate_size() * self.fee, constants.min_txn_fee + ) + + def dictify(self): + d = dict() + + if ( + self.total + or self.default_frozen + or self.unit_name + or self.asset_name + or self.manager + or self.reserve + or self.freeze + or self.clawback + or self.decimals + ): + apar = OrderedDict() + if self.metadata_hash: + apar["am"] = self.metadata_hash + if self.asset_name: + apar["an"] = self.asset_name + if self.url: + apar["au"] = self.url + if self.clawback: + apar["c"] = encoding.decode_address(self.clawback) + if self.decimals: apar["dc"] = self.decimals if self.default_frozen: apar["df"] = self.default_frozen @@ -726,9 +997,7 @@ def _undictify(d): return args def __eq__(self, other): - if not isinstance( - other, (AssetConfigTxn, future.transaction.AssetConfigTxn) - ): + if not isinstance(other, AssetConfigTxn): return False return ( super(AssetConfigTxn, self).__eq__(other) @@ -746,39 +1015,201 @@ def __eq__(self, other): and self.decimals == other.decimals ) + @classmethod + def as_metadata(cls, md): + try: + return cls.as_hash(md) + except error.WrongHashLengthError: + raise error.WrongMetadataLengthError -class AssetFreezeTxn(Transaction): - """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - Represents a transaction for freezing or unfreezing an account's asset - holdings. Must be issued by the asset's freeze manager. +class AssetCreateTxn(AssetConfigTxn): + """Represents a transaction for asset creation. + + Keyword arguments are required, starting with the special + addresses, to prevent errors, as type checks can't prevent simple + confusion of similar typed arguments. Since the special addresses + are required, strict_empty_address_check is turned off. Args: - sender (str): address of the sender, who must be the asset's freeze - manager - fee (int): transaction fee (per byte if flat_fee is false). When flat_fee is true, - fee may fall to zero but a group of N atomic transactions must - still have a fee of at least N*min_txn_fee. - first (int): first round for which the transaction is valid - last (int): last round for which the transaction is valid - gh (str): genesis_hash - index (int): index of the asset - target (str): address having its assets frozen or unfrozen - new_freeze_state (bool): true if the assets should be frozen, false if - they should be transferrable + sender (str): address of the sender + sp (SuggestedParams): suggested params from algod + total (int): total number of base units of this asset created + decimals (int, optional): number of digits to use for display after + decimal. If set to 0, the asset is not divisible. If set to 1, the + base unit of the asset is in tenths. Must be between 0 and 19, + inclusive. Defaults to 0. + default_frozen (bool): whether slots for this asset in user + accounts are frozen by default + manager (str): address allowed to change nonzero addresses + for this asset + reserve (str): account whose holdings of this asset should + be reported as "not minted" + freeze (str): account allowed to change frozen state of + holdings of this asset + clawback (str): account allowed take units of this asset + from any account + unit_name (str): hint for the name of a unit of this asset + asset_name (str): hint for the name of the asset + url (str): a URL where more information about the asset + can be retrieved + metadata_hash (byte[32], optional): a commitment to some unspecified + asset metadata (32 byte hash) note (bytes, optional): arbitrary optional bytes - gen (str, optional): genesis_id - flat_fee (bool, optional): whether the specified fee is a flat fee lease (byte[32], optional): specifies a lease, and no other transaction with the same sender and lease can be confirmed in this transaction's valid rounds rekey_to (str, optional): additionally rekey the sender to this address - Attributes: - sender (str) - fee (int) + """ + + def __init__( + self, + sender, + sp, + total, + decimals, + default_frozen, + *, + manager=None, + reserve=None, + freeze=None, + clawback=None, + unit_name="", + asset_name="", + url="", + metadata_hash=None, + note=None, + lease=None, + rekey_to=None, + ): + super().__init__( + sender=sender, + sp=sp, + total=total, + decimals=decimals, + default_frozen=default_frozen, + manager=manager, + reserve=reserve, + freeze=freeze, + clawback=clawback, + unit_name=unit_name, + asset_name=asset_name, + url=url, + metadata_hash=metadata_hash, + note=note, + lease=lease, + rekey_to=rekey_to, + strict_empty_address_check=False, + ) + + +class AssetDestroyTxn(AssetConfigTxn): + """Represents a transaction for asset destruction. + + An asset destruction transaction can only be sent by the manager + address, and only when the manager possseses all units of the + asset. + + """ + + def __init__( + self, sender, sp, index, note=None, lease=None, rekey_to=None + ): + super().__init__( + sender=sender, + sp=sp, + index=self.creatable_index(index), + note=note, + lease=lease, + rekey_to=rekey_to, + strict_empty_address_check=False, + ) + + +class AssetUpdateTxn(AssetConfigTxn): + """Represents a transaction for asset modification. + + To update asset configuration, include the following: + index, manager, reserve, freeze, clawback. + + Keyword arguments are required, starting with the special + addresses, to prevent argument reordinering errors. Since the + special addresses are required, strict_empty_address_check is + turned off. + + Args: + sender (str): address of the sender + sp (SuggestedParams): suggested params from algod + index (int): index of the asset to reconfigure + manager (str): address allowed to change nonzero addresses + for this asset + reserve (str): account whose holdings of this asset should + be reported as "not minted" + freeze (str): account allowed to change frozen state of + holdings of this asset + clawback (str): account allowed take units of this asset + from any account + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + rekey_to (str, optional): additionally rekey the sender to this address + + """ + + def __init__( + self, + sender, + sp, + index, + *, + manager, + reserve, + freeze, + clawback, + note=None, + lease=None, + rekey_to=None, + ): + super().__init__( + sender=sender, + sp=sp, + index=self.creatable_index(index), + manager=manager, + reserve=reserve, + freeze=freeze, + clawback=clawback, + note=note, + lease=lease, + rekey_to=rekey_to, + strict_empty_address_check=False, + ) + + +class AssetFreezeTxn(Transaction): + + """ + Represents a transaction for freezing or unfreezing an account's asset + holdings. Must be issued by the asset's freeze manager. + + Args: + sender (str): address of the sender, who must be the asset's freeze + manager + sp (SuggestedParams): suggested params from algod + index (int): index of the asset + target (str): address having its assets frozen or unfrozen + new_freeze_state (bool): true if the assets should be frozen, false if + they should be transferrable + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + rekey_to (str, optional): additionally rekey the sender to this address + + Attributes: + sender (str) + fee (int) first_valid_round (int) last_valid_round (int) genesis_hash (str) @@ -795,237 +1226,920 @@ class AssetFreezeTxn(Transaction): def __init__( self, sender, - fee, - first, - last, - gh, + sp, + index, + target, + new_freeze_state, + note=None, + lease=None, + rekey_to=None, + ): + Transaction.__init__( + self, sender, sp, note, lease, constants.assetfreeze_txn, rekey_to + ) + self.index = self.creatable_index(index, required=True) + self.target = target + self.new_freeze_state = new_freeze_state + if not sp.flat_fee: + self.fee = max( + self.estimate_size() * self.fee, constants.min_txn_fee + ) + + def dictify(self): + d = dict() + if self.new_freeze_state: + d["afrz"] = self.new_freeze_state + + d["fadd"] = encoding.decode_address(self.target) + + if self.index: + d["faid"] = self.index + + d.update(super(AssetFreezeTxn, self).dictify()) + od = OrderedDict(sorted(d.items())) + return od + + @staticmethod + def _undictify(d): + args = { + "index": d["faid"], + "new_freeze_state": d["afrz"] if "afrz" in d else False, + "target": encoding.encode_address(d["fadd"]), + } + + return args + + def __eq__(self, other): + if not isinstance(other, AssetFreezeTxn): + return False + return ( + super(AssetFreezeTxn, self).__eq__(other) + and self.index == other.index + and self.target == other.target + and self.new_freeze_state == other.new_freeze_state + ) + + +class AssetTransferTxn(Transaction): + """ + Represents a transaction for asset transfer. + + To begin accepting an asset, supply the same address as both sender and + receiver, and set amount to 0 (or use AssetOptInTxn) + + To revoke an asset, set revocation_target, and issue the transaction from + the asset's revocation manager account. + + Args: + sender (str): address of the sender + sp (SuggestedParams): suggested params from algod + receiver (str): address of the receiver + amt (int): amount of asset base units to send + index (int): index of the asset + close_assets_to (string, optional): send all of sender's remaining + assets, after paying `amt` to receiver, to this address + revocation_target (string, optional): send assets from this address, + rather than the sender's address (can only be used by an asset's + revocation manager, also known as clawback) + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + rekey_to (str, optional): additionally rekey the sender to this address + + Attributes: + sender (str) + fee (int) + first_valid_round (int) + last_valid_round (int) + genesis_hash (str) + index (int) + amount (int) + receiver (string) + close_assets_to (string) + revocation_target (string) + note (bytes) + genesis_id (str) + type (str) + lease (byte[32]) + rekey_to (str) + """ + + def __init__( + self, + sender, + sp, + receiver, + amt, + index, + close_assets_to=None, + revocation_target=None, + note=None, + lease=None, + rekey_to=None, + ): + Transaction.__init__( + self, + sender, + sp, + note, + lease, + constants.assettransfer_txn, + rekey_to, + ) + if receiver: + self.receiver = receiver + else: + raise error.ZeroAddressError + + self.amount = amt + if (not isinstance(self.amount, int)) or self.amount < 0: + raise error.WrongAmountType + self.index = self.creatable_index(index, required=True) + self.close_assets_to = close_assets_to + self.revocation_target = revocation_target + if not sp.flat_fee: + self.fee = max( + self.estimate_size() * self.fee, constants.min_txn_fee + ) + + def dictify(self): + d = dict() + + if self.amount: + d["aamt"] = self.amount + if self.close_assets_to: + d["aclose"] = encoding.decode_address(self.close_assets_to) + + decoded_receiver = encoding.decode_address(self.receiver) + if any(decoded_receiver): + d["arcv"] = encoding.decode_address(self.receiver) + if self.revocation_target: + d["asnd"] = encoding.decode_address(self.revocation_target) + + if self.index: + d["xaid"] = self.index + + d.update(super(AssetTransferTxn, self).dictify()) + od = OrderedDict(sorted(d.items())) + + return od + + @staticmethod + def _undictify(d): + args = { + "receiver": encoding.encode_address(d["arcv"]) + if "arcv" in d + else constants.ZERO_ADDRESS, + "amt": d["aamt"] if "aamt" in d else 0, + "index": d["xaid"] if "xaid" in d else None, + "close_assets_to": encoding.encode_address(d["aclose"]) + if "aclose" in d + else None, + "revocation_target": encoding.encode_address(d["asnd"]) + if "asnd" in d + else None, + } + + return args + + def __eq__(self, other): + if not isinstance(other, AssetTransferTxn): + return False + return ( + super(AssetTransferTxn, self).__eq__(other) + and self.index == other.index + and self.amount == other.amount + and self.receiver == other.receiver + and self.close_assets_to == other.close_assets_to + and self.revocation_target == other.revocation_target + ) + + +class AssetOptInTxn(AssetTransferTxn): + """ + Make a transaction that will opt in to an ASA + + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the ASA to opt into + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + + Attributes: + See AssetTransferTxn + """ + + def __init__( + self, sender, sp, index, note=None, lease=None, rekey_to=None + ): + super().__init__( + sender=sender, + sp=sp, + receiver=sender, + amt=0, + index=index, + note=note, + lease=lease, + rekey_to=rekey_to, + ) + + +class AssetCloseOutTxn(AssetTransferTxn): + """ + Make a transaction that will send all of an ASA away, and opt out of it. + + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + receiver (str): address of the receiver + index (int): the ASA to opt into + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + + Attributes: + See AssetTransferTxn + """ + + def __init__( + self, sender, sp, receiver, index, note=None, lease=None, rekey_to=None + ): + super().__init__( + sender=sender, + sp=sp, + receiver=receiver, + amt=0, + index=index, + close_assets_to=receiver, + note=note, + lease=lease, + rekey_to=rekey_to, + ) + + +class StateSchema: + """ + Restricts state for an application call. + + Args: + num_uints(int, optional): number of uints to store + num_byte_slices(int, optional): number of byte slices to store + + Attributes: + num_uints (int) + num_byte_slices (int) + """ + + def __init__(self, num_uints=None, num_byte_slices=None): + self.num_uints = num_uints + self.num_byte_slices = num_byte_slices + + def dictify(self): + d = dict() + if self.num_uints: + d["nui"] = self.num_uints + if self.num_byte_slices: + d["nbs"] = self.num_byte_slices + od = OrderedDict(sorted(d.items())) + return od + + @staticmethod + def undictify(d): + return StateSchema( + num_uints=d["nui"] if "nui" in d else 0, + num_byte_slices=d["nbs"] if "nbs" in d else 0, + ) + + def __eq__(self, other): + if not isinstance(other, StateSchema): + return False + return ( + self.num_uints == other.num_uints + and self.num_byte_slices == other.num_byte_slices + ) + + +class OnComplete(IntEnum): + # NoOpOC indicates that an application transaction will simply call its + # ApprovalProgram + NoOpOC = 0 + + # OptInOC indicates that an application transaction will allocate some + # LocalState for the application in the sender's account + OptInOC = 1 + + # CloseOutOC indicates that an application transaction will deallocate + # some LocalState for the application from the user's account + CloseOutOC = 2 + + # ClearStateOC is similar to CloseOutOC, but may never fail. This + # allows users to reclaim their minimum balance from an application + # they no longer wish to opt in to. + ClearStateOC = 3 + + # UpdateApplicationOC indicates that an application transaction will + # update the ApprovalProgram and ClearStateProgram for the application + UpdateApplicationOC = 4 + + # DeleteApplicationOC indicates that an application transaction will + # delete the AppParams for the application from the creator's balance + # record + DeleteApplicationOC = 5 + + +class ApplicationCallTxn(Transaction): + """ + Represents a transaction that interacts with the application system. + + Args: + sender (str): address of the sender + sp (SuggestedParams): suggested params from algod + index (int): index of the application to call; 0 if creating a new application + on_complete (OnComplete): intEnum representing what app should do on completion + local_schema (StateSchema, optional): restricts what can be stored by created application; + must be omitted if not creating an application + global_schema (StateSchema, optional): restricts what can be stored by created application; + must be omitted if not creating an application + approval_program (bytes, optional): the program to run on transaction approval; + must be omitted if not creating or updating an application + clear_program (bytes, optional): the program to run when state is being cleared; + must be omitted if not creating or updating an application + app_args (list[bytes], optional): list of arguments to the application, each argument itself a buf + accounts (list[string], optional): list of additional accounts involved in call + foreign_apps (list[int], optional): list of other applications (identified by index) involved in call + foreign_assets (list[int], optional): list of assets involved in call + extra_pages (int, optional): additional program space for supporting larger programs. A page is 1024 bytes. + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access + + Attributes: + sender (str) + fee (int) + first_valid_round (int) + last_valid_round (int) + genesis_hash (str) + index (int) + on_complete (int) + local_schema (StateSchema) + global_schema (StateSchema) + approval_program (bytes) + clear_program (bytes) + app_args (list[bytes]) + accounts (list[str]) + foreign_apps (list[int]) + foreign_assets (list[int]) + extra_pages (int) + boxes (list[(int, bytes)]) + """ + + def __init__( + self, + sender, + sp, + index, + on_complete, + local_schema=None, + global_schema=None, + approval_program=None, + clear_program=None, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, + note=None, + lease=None, + rekey_to=None, + extra_pages=0, + boxes=None, + ): + Transaction.__init__( + self, sender, sp, note, lease, constants.appcall_txn, rekey_to + ) + self.index = self.creatable_index(index) + self.on_complete = on_complete if on_complete else 0 + self.local_schema = self.state_schema(local_schema) + self.global_schema = self.state_schema(global_schema) + self.approval_program = self.teal_bytes(approval_program) + self.clear_program = self.teal_bytes(clear_program) + self.app_args = self.bytes_list(app_args) + self.accounts = accounts if accounts else None + self.foreign_apps = self.int_list(foreign_apps) + self.foreign_assets = self.int_list(foreign_assets) + self.extra_pages = extra_pages + self.boxes = BoxReference.translate_box_references( + boxes, self.foreign_apps, self.index + ) + if not sp.flat_fee: + self.fee = max( + self.estimate_size() * self.fee, constants.min_txn_fee + ) + + @staticmethod + def state_schema(schema): + """Confirm the argument is a StateSchema, or false which is coerced to None""" + if not schema or not schema.dictify(): + return None # Coerce false/empty values to None, to help __eq__ + if not isinstance(schema, StateSchema): + raise TypeError("{} is not a StateSchema".format(schema)) + return schema + + @staticmethod + def teal_bytes(teal): + """Confirm the argument is bytes-like, or false which is coerced to None""" + if not teal: + return None # Coerce false values like "" to None, to help __eq__ + if not isinstance(teal, (bytes, bytearray)): + raise TypeError("Program {} is not bytes".format(teal)) + return teal + + @staticmethod + def bytes_list(lst): + """Confirm or coerce list elements to bytes. Return None for empty/false lst.""" + if not lst: + return None + return [encoding.encode_as_bytes(elt) for elt in lst] + + @staticmethod + def int_list(lst): + """Confirm or coerce list elements to int. Return None for empty/false lst.""" + if not lst: + return None + return [int(elt) for elt in lst] + + def dictify(self): + d = dict() + if self.index: + d["apid"] = self.index + d["apan"] = self.on_complete + if self.local_schema: + d["apls"] = self.local_schema.dictify() + if self.global_schema: + d["apgs"] = self.global_schema.dictify() + if self.approval_program: + d["apap"] = self.approval_program + if self.clear_program: + d["apsu"] = self.clear_program + if self.app_args: + d["apaa"] = self.app_args + if self.accounts: + d["apat"] = [ + encoding.decode_address(account_pubkey) + for account_pubkey in self.accounts + ] + if self.foreign_apps: + d["apfa"] = self.foreign_apps + if self.foreign_assets: + d["apas"] = self.foreign_assets + if self.extra_pages: + d["apep"] = self.extra_pages + if self.boxes: + d["apbx"] = [box.dictify() for box in self.boxes] + + d.update(super(ApplicationCallTxn, self).dictify()) + od = OrderedDict(sorted(d.items())) + + return od + + @staticmethod + def _undictify(d): + args = { + "index": d["apid"] if "apid" in d else None, + "on_complete": d["apan"] if "apan" in d else None, + "local_schema": StateSchema.undictify(d["apls"]) + if "apls" in d + else None, + "global_schema": StateSchema.undictify(d["apgs"]) + if "apgs" in d + else None, + "approval_program": d["apap"] if "apap" in d else None, + "clear_program": d["apsu"] if "apsu" in d else None, + "app_args": d["apaa"] if "apaa" in d else None, + "accounts": d["apat"] if "apat" in d else None, + "foreign_apps": d["apfa"] if "apfa" in d else None, + "foreign_assets": d["apas"] if "apas" in d else None, + "extra_pages": d["apep"] if "apep" in d else 0, + "boxes": [BoxReference.undictify(box) for box in d["apbx"]] + if "apbx" in d + else None, + } + if args["accounts"]: + args["accounts"] = [ + encoding.encode_address(account_bytes) + for account_bytes in args["accounts"] + ] + return args + + def __eq__(self, other): + if not isinstance(other, ApplicationCallTxn): + return False + return ( + super(ApplicationCallTxn, self).__eq__(other) + and self.index == other.index + and self.on_complete == other.on_complete + and self.local_schema == other.local_schema + and self.global_schema == other.global_schema + and self.approval_program == other.approval_program + and self.clear_program == other.clear_program + and self.app_args == other.app_args + and self.accounts == other.accounts + and self.foreign_apps == other.foreign_apps + and self.foreign_assets == other.foreign_assets + and self.extra_pages == other.extra_pages + and self.boxes == other.boxes + ) + + +class ApplicationCreateTxn(ApplicationCallTxn): + """ + Make a transaction that will create an application. + + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + on_complete (OnComplete): what application should so once the program is done being run + approval_program (bytes): the compiled TEAL that approves a transaction + clear_program (bytes): the compiled TEAL that runs when clearing state + global_schema (StateSchema): restricts the number of ints and byte slices in the global state + local_schema (StateSchema): restricts the number of ints and byte slices in the per-user local state + app_args(list[bytes], optional): any additional arguments to the application + accounts(list[str], optional): any additional accounts to supply to the application + foreign_apps(list[int], optional): any other apps used by the application, identified by app index + foreign_assets(list[int], optional): list of assets involved in call + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + extra_pages(int, optional): provides extra program size + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access + + Attributes: + See ApplicationCallTxn + """ + + def __init__( + self, + sender, + sp, + on_complete, + approval_program, + clear_program, + global_schema, + local_schema, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, + note=None, + lease=None, + rekey_to=None, + extra_pages=0, + boxes=None, + ): + ApplicationCallTxn.__init__( + self, + sender=sender, + sp=sp, + index=0, + on_complete=on_complete, + approval_program=self.required(approval_program), + clear_program=self.required(clear_program), + global_schema=global_schema, + local_schema=local_schema, + app_args=app_args, + accounts=accounts, + foreign_apps=foreign_apps, + foreign_assets=foreign_assets, + note=note, + lease=lease, + rekey_to=rekey_to, + extra_pages=extra_pages, + boxes=boxes, + ) + + +class ApplicationUpdateTxn(ApplicationCallTxn): + """ + Make a transaction that will change an application's approval and clear programs. + + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the application to update + approval_program (bytes): the new compiled TEAL that approves a transaction + clear_program (bytes): the new compiled TEAL that runs when clearing state + app_args(list[bytes], optional): any additional arguments to the application + accounts(list[str], optional): any additional accounts to supply to the application + foreign_apps(list[int], optional): any other apps used by the application, identified by app index + foreign_assets(list[int], optional): list of assets involved in call + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access + + + Attributes: + See ApplicationCallTxn + """ + + def __init__( + self, + sender, + sp, + index, + approval_program, + clear_program, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, + note=None, + lease=None, + rekey_to=None, + boxes=None, + ): + ApplicationCallTxn.__init__( + self, + sender=sender, + sp=sp, + index=self.creatable_index(index, required=True), + on_complete=OnComplete.UpdateApplicationOC, + approval_program=approval_program, + clear_program=clear_program, + app_args=app_args, + accounts=accounts, + foreign_apps=foreign_apps, + foreign_assets=foreign_assets, + note=note, + lease=lease, + rekey_to=rekey_to, + boxes=boxes, + ) + + +class ApplicationDeleteTxn(ApplicationCallTxn): + """ + Make a transaction that will delete an application + + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the application to update + app_args(list[bytes], optional): any additional arguments to the application + accounts(list[str], optional): any additional accounts to supply to the application + foreign_apps(list[int], optional): any other apps used by the application, identified by app index + foreign_assets(list[int], optional): list of assets involved in call + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access + + Attributes: + See ApplicationCallTxn + """ + + def __init__( + self, + sender, + sp, index, - target, - new_freeze_state, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, note=None, - gen=None, - flat_fee=False, lease=None, rekey_to=None, + boxes=None, ): - Transaction.__init__( + ApplicationCallTxn.__init__( self, - sender, - fee, - first, - last, - note, - gen, - gh, - lease, - constants.assetfreeze_txn, - rekey_to, + sender=sender, + sp=sp, + index=self.creatable_index(index, required=True), + on_complete=OnComplete.DeleteApplicationOC, + app_args=app_args, + accounts=accounts, + foreign_apps=foreign_apps, + foreign_assets=foreign_assets, + note=note, + lease=lease, + rekey_to=rekey_to, + boxes=boxes, ) - self.index = index - self.target = target - self.new_freeze_state = new_freeze_state - if not flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - - def dictify(self): - d = dict() - if self.new_freeze_state: - d["afrz"] = self.new_freeze_state - d["fadd"] = encoding.decode_address(self.target) - - if self.index: - d["faid"] = self.index - d.update(super(AssetFreezeTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od +class ApplicationOptInTxn(ApplicationCallTxn): + """ + Make a transaction that will opt in to an application - @staticmethod - def _undictify(d): - args = { - "index": d["faid"], - "new_freeze_state": d["afrz"] if "afrz" in d else False, - "target": encoding.encode_address(d["fadd"]), - } + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the application to update + app_args(list[bytes], optional): any additional arguments to the application + accounts(list[str], optional): any additional accounts to supply to the application + foreign_apps(list[int], optional): any other apps used by the application, identified by app index + foreign_assets(list[int], optional): list of assets involved in call + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - return args + Attributes: + See ApplicationCallTxn + """ - def __eq__(self, other): - if not isinstance( - other, (AssetFreezeTxn, future.transaction.AssetFreezeTxn) - ): - return False - return ( - super(AssetFreezeTxn, self).__eq__(other) - and self.index == other.index - and self.target == other.target - and self.new_freeze_state == other.new_freeze_state + def __init__( + self, + sender, + sp, + index, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, + note=None, + lease=None, + rekey_to=None, + boxes=None, + ): + ApplicationCallTxn.__init__( + self, + sender=sender, + sp=sp, + index=self.creatable_index(index, required=True), + on_complete=OnComplete.OptInOC, + app_args=app_args, + accounts=accounts, + foreign_apps=foreign_apps, + foreign_assets=foreign_assets, + note=note, + lease=lease, + rekey_to=rekey_to, + boxes=boxes, ) -class AssetTransferTxn(Transaction): +class ApplicationCloseOutTxn(ApplicationCallTxn): """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - - Represents a transaction for asset transfer. - To begin accepting an asset, supply the same address as both sender and - receiver, and set amount to 0. - To revoke an asset, set revocation_target, and issue the transaction from - the asset's revocation manager account. + Make a transaction that will close out a user's state in an application Args: - sender (str): address of the sender - fee (int): transaction fee (per byte if flat_fee is false). When flat_fee is true, - fee may fall to zero but a group of N atomic transactions must - still have a fee of at least N*min_txn_fee. - first (int): first round for which the transaction is valid - last (int): last round for which the transaction is valid - gh (str): genesis_hash - receiver (str): address of the receiver - amt (int): amount of asset base units to send - index (int): index of the asset - close_assets_to (string, optional): send all of sender's remaining - assets, after paying `amt` to receiver, to this address - revocation_target (string, optional): send assets from this address, - rather than the sender's address (can only be used by an asset's - revocation manager, also known as clawback) - note (bytes, optional): arbitrary optional bytes - gen (str, optional): genesis_id - flat_fee (bool, optional): whether the specified fee is a flat fee - lease (byte[32], optional): specifies a lease, and no other transaction - with the same sender and lease can be confirmed in this - transaction's valid rounds - rekey_to (str, optional): additionally rekey the sender to this address + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the application to update + app_args(list[bytes], optional): any additional arguments to the application + accounts(list[str], optional): any additional accounts to supply to the application + foreign_apps(list[int], optional): any other apps used by the application, identified by app index + foreign_assets(list[int], optional): list of assets involved in call + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access Attributes: - sender (str) - fee (int) - first_valid_round (int) - last_valid_round (int) - genesis_hash (str) - index (int) - amount (int) - receiver (string) - close_assets_to (string) - revocation_target (string) - note (bytes) - genesis_id (str) - type (str) - lease (byte[32]) - rekey_to (str) + See ApplicationCallTxn """ def __init__( self, sender, - fee, - first, - last, - gh, - receiver, - amt, + sp, index, - close_assets_to=None, - revocation_target=None, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, note=None, - gen=None, - flat_fee=False, lease=None, rekey_to=None, + boxes=None, ): - Transaction.__init__( + ApplicationCallTxn.__init__( self, - sender, - fee, - first, - last, - note, - gen, - gh, - lease, - constants.assettransfer_txn, - rekey_to, + sender=sender, + sp=sp, + index=self.creatable_index(index), + on_complete=OnComplete.CloseOutOC, + app_args=app_args, + accounts=accounts, + foreign_apps=foreign_apps, + foreign_assets=foreign_assets, + note=note, + lease=lease, + rekey_to=rekey_to, + boxes=boxes, ) - if receiver: - self.receiver = receiver - else: - raise error.ZeroAddressError - self.amount = amt - if (not isinstance(self.amount, int)) or self.amount < 0: - raise error.WrongAmountType - self.index = index - self.close_assets_to = close_assets_to - self.revocation_target = revocation_target - if not flat_fee: - self.fee = max( - self.estimate_size() * self.fee, constants.min_txn_fee - ) - def dictify(self): - d = dict() - if self.amount: - d["aamt"] = self.amount - if self.close_assets_to: - d["aclose"] = encoding.decode_address(self.close_assets_to) +class ApplicationClearStateTxn(ApplicationCallTxn): + """ + Make a transaction that will clear a user's state for an application - decoded_receiver = encoding.decode_address(self.receiver) - if any(decoded_receiver): - d["arcv"] = encoding.decode_address(self.receiver) + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the application to update + app_args(list[bytes], optional): any additional arguments to the application + accounts(list[str], optional): any additional accounts to supply to the application + foreign_apps(list[int], optional): any other apps used by the application, identified by app index + foreign_assets(list[int], optional): list of assets involved in call + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - if self.revocation_target: - d["asnd"] = encoding.decode_address(self.revocation_target) + Attributes: + See ApplicationCallTxn + """ - if self.index: - d["xaid"] = self.index + def __init__( + self, + sender, + sp, + index, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, + note=None, + lease=None, + rekey_to=None, + boxes=None, + ): + ApplicationCallTxn.__init__( + self, + sender=sender, + sp=sp, + index=self.creatable_index(index), + on_complete=OnComplete.ClearStateOC, + app_args=app_args, + accounts=accounts, + foreign_apps=foreign_apps, + foreign_assets=foreign_assets, + note=note, + lease=lease, + rekey_to=rekey_to, + boxes=boxes, + ) - d.update(super(AssetTransferTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od +class ApplicationNoOpTxn(ApplicationCallTxn): + """ + Make a transaction that will do nothing on application completion + In other words, just call the application - @staticmethod - def _undictify(d): - args = { - "receiver": encoding.encode_address(d["arcv"]) - if "arcv" in d - else None, - "amt": d["aamt"] if "aamt" in d else 0, - "index": d["xaid"] if "xaid" in d else None, - "close_assets_to": encoding.encode_address(d["aclose"]) - if "aclose" in d - else None, - "revocation_target": encoding.encode_address(d["asnd"]) - if "asnd" in d - else None, - } + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the application to update + app_args(list[bytes], optional): any additional arguments to the application + accounts(list[str], optional): any additional accounts to supply to the application + foreign_apps(list[int], optional): any other apps used by the application, identified by app index + foreign_assets(list[int], optional): list of assets involved in call + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access - return args + Attributes: + See ApplicationCallTxn + """ - def __eq__(self, other): - if not isinstance( - other, (AssetTransferTxn, future.transaction.AssetTransferTxn) - ): - return False - return ( - super(AssetTransferTxn, self).__eq__(other) - and self.index == other.index - and self.amount == other.amount - and self.receiver == other.receiver - and self.close_assets_to == other.close_assets_to - and self.revocation_target == other.revocation_target + def __init__( + self, + sender, + sp, + index, + app_args=None, + accounts=None, + foreign_apps=None, + foreign_assets=None, + note=None, + lease=None, + rekey_to=None, + boxes=None, + ): + ApplicationCallTxn.__init__( + self, + sender=sender, + sp=sp, + index=self.creatable_index(index), + on_complete=OnComplete.NoOpOC, + app_args=app_args, + accounts=accounts, + foreign_apps=foreign_apps, + foreign_assets=foreign_assets, + note=note, + lease=lease, + rekey_to=rekey_to, + boxes=boxes, ) class SignedTransaction: """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - Represents a signed transaction. Args: @@ -1039,25 +2153,23 @@ class SignedTransaction: authorizing_address (str) """ - def __init__(self, transaction, signature, authorizing_address=None): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - warnings.warn( - "`SignedTransaction` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) + def __init__( + self, transaction: Transaction, signature, authorizing_address=None + ): self.signature = signature self.transaction = transaction self.authorizing_address = authorizing_address - def dictify(self): + def get_txid(self): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. + Get the transaction's ID. + + Returns: + str: transaction ID """ + return self.transaction.get_txid() + + def dictify(self): od = OrderedDict() if self.signature: od["sig"] = base64.b64decode(self.signature) @@ -1068,10 +2180,6 @@ def dictify(self): @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ sig = None if "sig" in d: sig = base64.b64encode(d["sig"]).decode() @@ -1083,13 +2191,7 @@ def undictify(d): return stx def __eq__(self, other): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - if not isinstance( - other, (SignedTransaction, future.transaction.SignedTransaction) - ): + if not isinstance(other, SignedTransaction): return False return ( self.transaction == other.transaction @@ -1100,9 +2202,6 @@ def __eq__(self, other): class MultisigTransaction: """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - Represents a signed transaction. Args: @@ -1112,26 +2211,21 @@ class MultisigTransaction: Attributes: transaction (Transaction) multisig (Multisig) + auth_addr (str, optional) """ - def __init__(self, transaction, multisig): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - warnings.warn( - "`MultisigTransaction` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) + def __init__(self, transaction: Transaction, multisig: "Multisig") -> None: self.transaction = transaction self.multisig = multisig + msigAddr = multisig.address() + if transaction.sender != msigAddr: + self.auth_addr = msigAddr + else: + self.auth_addr = None + def sign(self, private_key): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Sign the multisig transaction. Args: @@ -1145,9 +2239,6 @@ def sign(self, private_key): object with the same addresses. """ self.multisig.validate() - addr = self.multisig.address() - if not self.transaction.sender == addr: - raise error.BadTxnSenderError index = -1 public_key = base64.b64decode(bytes(private_key, "utf-8")) public_key = public_key[constants.key_len_bytes :] @@ -1160,36 +2251,42 @@ def sign(self, private_key): sig = self.transaction.raw_sign(private_key) self.multisig.subsigs[index].signature = sig - def dictify(self): + def get_txid(self): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. + Get the transaction's ID. + + Returns: + str: transaction ID """ + return self.transaction.get_txid() + + def dictify(self): od = OrderedDict() if self.multisig: od["msig"] = self.multisig.dictify() + if self.auth_addr: + od["sgnr"] = encoding.decode_address(self.auth_addr) od["txn"] = self.transaction.dictify() return od @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ msig = None if "msig" in d: msig = Multisig.undictify(d["msig"]) + auth_addr = None + if "sgnr" in d: + auth_addr = encoding.encode_address(d["sgnr"]) txn = Transaction.undictify(d["txn"]) mtx = MultisigTransaction(txn, msig) + mtx.auth_addr = auth_addr return mtx @staticmethod - def merge(part_stxs): + def merge( + part_stxs: List["MultisigTransaction"], + ) -> Optional["MultisigTransaction"]: """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Merge partially signed multisig transactions. Args: @@ -1204,12 +2301,22 @@ def merge(part_stxs): transactions. To append a signature to a multisig transaction, just use MultisigTransaction.sign() """ - ref_addr = None + ref_msig_addr = None + ref_auth_addr = None for stx in part_stxs: - if not ref_addr: - ref_addr = stx.multisig.address() - elif not stx.multisig.address() == ref_addr: + try: + other_auth_addr = stx.auth_addr + except AttributeError: + other_auth_addr = None + + if not ref_msig_addr: + ref_msig_addr = stx.multisig.address() + ref_auth_addr = other_auth_addr + if not stx.multisig.address() == ref_msig_addr: raise error.MergeKeysMismatchError + if not other_auth_addr == ref_auth_addr: + raise error.MergeAuthAddrMismatchError + msigstx = None for stx in part_stxs: if not msigstx: @@ -1229,26 +2336,18 @@ def merge(part_stxs): return msigstx def __eq__(self, other): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - if not isinstance( - other, - (MultisigTransaction, future.transaction.MultisigTransaction), - ): - return False - return ( - self.transaction == other.transaction - and self.multisig == other.multisig - ) + if isinstance(other, MultisigTransaction): + return ( + self.transaction == other.transaction + and self.auth_addr == other.auth_addr + and self.multisig == other.multisig + ) + + return False class Multisig: """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - Represents a multisig account and signatures. Args: @@ -1263,15 +2362,6 @@ class Multisig: """ def __init__(self, version, threshold, addresses): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - warnings.warn( - "`Multisig` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) self.version = version self.threshold = threshold self.subsigs = [] @@ -1279,12 +2369,7 @@ def __init__(self, version, threshold, addresses): self.subsigs.append(MultisigSubsig(encoding.decode_address(a))) def validate(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - - Check if the multisig account is valid. - """ + """Check if the multisig account is valid.""" if not self.version == 1: raise error.UnknownMsigVersionError if ( @@ -1297,12 +2382,7 @@ def validate(self): raise error.MultisigAccountSizeError def address(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - - Return the multisig account address. - """ + """Return the multisig account address.""" msig_bytes = ( bytes(constants.msig_addr_prefix, "utf-8") + bytes([self.version]) @@ -1314,12 +2394,7 @@ def address(self): return encoding.encode_address(addr) def verify(self, message): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - - Verify that the multisig is valid for the message. - """ + """Verify that the multisig is valid for the message.""" try: self.validate() except (error.UnknownMsigVersionError, error.InvalidThresholdError): @@ -1335,7 +2410,7 @@ def verify(self, message): try: verify_key.verify(message, subsig.signature) verified_count += 1 - except BadSignatureError: + except (BadSignatureError, ValueError, TypeError): return False if verified_count < self.threshold: @@ -1344,10 +2419,6 @@ def verify(self, message): return True def dictify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ od = OrderedDict() od["subsig"] = [subsig.dictify() for subsig in self.subsigs] od["thr"] = self.threshold @@ -1355,10 +2426,6 @@ def dictify(self): return od def json_dictify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ d = { "subsig": [subsig.json_dictify() for subsig in self.subsigs], "thr": self.threshold, @@ -1368,20 +2435,12 @@ def json_dictify(self): @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ subsigs = [MultisigSubsig.undictify(s) for s in d["subsig"]] msig = Multisig(d["v"], d["thr"], []) msig.subsigs = subsigs return msig def get_multisig_account(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ """Return a Multisig object without signatures.""" msig = Multisig(self.version, self.threshold, self.get_public_keys()) for s in msig.subsigs: @@ -1389,20 +2448,12 @@ def get_multisig_account(self): return msig def get_public_keys(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ """Return the base32 encoded addresses for the multisig account.""" pks = [encoding.encode_address(s.public_key) for s in self.subsigs] return pks def __eq__(self, other): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - if not isinstance(other, (Multisig, future.transaction.Multisig)): + if not isinstance(other, Multisig): return False return ( self.version == other.version @@ -1413,32 +2464,16 @@ def __eq__(self, other): class MultisigSubsig: """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - Attributes: public_key (bytes) signature (bytes) """ def __init__(self, public_key, signature=None): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - warnings.warn( - "`MultisigSubsig` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) self.public_key = public_key self.signature = signature def dictify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ od = OrderedDict() od["pk"] = self.public_key if self.signature: @@ -1446,10 +2481,6 @@ def dictify(self): return od def json_dictify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ d = {"pk": base64.b64encode(self.public_key).decode()} if self.signature: d["s"] = base64.b64encode(self.signature).decode() @@ -1457,10 +2488,6 @@ def json_dictify(self): @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ sig = None if "s" in d: sig = d["s"] @@ -1468,13 +2495,7 @@ def undictify(d): return mss def __eq__(self, other): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - if not isinstance( - other, (MultisigSubsig, future.transaction.MultisigSubsig) - ): + if not isinstance(other, MultisigSubsig): return False return ( self.public_key == other.public_key @@ -1484,10 +2505,9 @@ def __eq__(self, other): class LogicSig: """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. + Represents a logic signature - Represents a logic signature. + NOTE: LogicSig cannot sign transactions in all cases. Instead, use LogicSigAccount as a safe, general purpose signing mechanism. Since LogicSig does not track the provided signature's public key, LogicSig cannot sign transactions when delegated to a non-multisig account _and_ the sender is not the delegating account. Arguments: logic (bytes): compiled program @@ -1501,15 +2521,6 @@ class LogicSig: """ def __init__(self, program, args=None): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - warnings.warn( - "`LogicSig` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) self._sanity_check_program(program) self.logic = program self.args = args @@ -1519,9 +2530,6 @@ def __init__(self, program, args=None): @staticmethod def _sanity_check_program(program): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Performs heuristic program validation: check if passed in bytes are Algorand address, or they are B64 encoded, rather than Teal bytes @@ -1562,10 +2570,6 @@ def is_ascii_printable(program_bytes): ) def dictify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ od = OrderedDict() if self.args: od["arg"] = self.args @@ -1578,10 +2582,6 @@ def dictify(self): @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ lsig = LogicSig(d["l"], d.get("arg", None)) if "sig" in d: lsig.sig = base64.b64encode(d["sig"]).decode() @@ -1591,9 +2591,6 @@ def undictify(d): def verify(self, public_key): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Verifies LogicSig against the transaction's sender address Args: @@ -1623,16 +2620,13 @@ def verify(self, public_key): try: verify_key.verify(to_sign, base64.b64decode(self.sig)) return True - except BadSignatureError: + except (BadSignatureError, ValueError, TypeError): return False return self.msig.verify(to_sign) def address(self): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Compute hash of the logic sig program (that is the same as escrow account address) as string address @@ -1643,10 +2637,6 @@ def address(self): @staticmethod def sign_program(program, private_key): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ private_key = base64.b64decode(private_key) signing_key = SigningKey(private_key[: constants.key_len_bytes]) to_sign = constants.logic_prefix + program @@ -1655,10 +2645,6 @@ def sign_program(program, private_key): @staticmethod def single_sig_multisig(program, private_key, multisig): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ index = -1 public_key = base64.b64decode(bytes(private_key, "utf-8")) public_key = public_key[constants.key_len_bytes :] @@ -1674,9 +2660,6 @@ def single_sig_multisig(program, private_key, multisig): def sign(self, private_key, multisig=None): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Creates signature (if no pk provided) or multi signature Args: @@ -1687,10 +2670,16 @@ def sign(self, private_key, multisig=None): Raises: InvalidSecretKeyError: if no matching private key in multisig\ object + LogicSigOverspecifiedSignature: if the opposite signature type has + already been provided """ if not multisig: + if self.msig: + raise error.LogicSigOverspecifiedSignature self.sig = LogicSig.sign_program(self.logic, private_key) else: + if self.sig: + raise error.LogicSigOverspecifiedSignature sig, index = LogicSig.single_sig_multisig( self.logic, private_key, multisig ) @@ -1699,9 +2688,6 @@ def sign(self, private_key, multisig=None): def append_to_multisig(self, private_key): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Appends a signature to multi signature Args: @@ -1719,11 +2705,7 @@ def append_to_multisig(self, private_key): self.msig.subsigs[index].signature = base64.b64decode(sig) def __eq__(self, other): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - if not isinstance(other, (LogicSig, future.transaction.LogicSig)): + if not isinstance(other, LogicSig): return False return ( self.logic == other.logic @@ -1733,98 +2715,333 @@ def __eq__(self, other): ) -class LogicSigTransaction: +class LogicSigAccount: """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. + Represents an account that can sign with a LogicSig program. + """ + + def __init__( + self, program: bytes, args: Optional[List[bytes]] = None + ) -> None: + """ + Create a new LogicSigAccount. By default this will create an escrow + LogicSig account. Call `sign` or `sign_multisig` on the newly created + LogicSigAccount to make it a delegated account. + + Args: + program (bytes): The compiled TEAL program which contains the logic + for this LogicSig. + args (List[bytes], optional): An optional array of arguments for the + program. + """ + self.lsig = LogicSig(program, args) + self.sigkey: Optional[bytes] = None + + def dictify(self): + od = OrderedDict() + od["lsig"] = self.lsig.dictify() + if self.sigkey: + od["sigkey"] = self.sigkey + return od + + @staticmethod + def undictify(d): + lsig = LogicSig.undictify(d["lsig"]) + lsigAccount = LogicSigAccount(lsig.logic, lsig.args) + lsigAccount.lsig = lsig + if "sigkey" in d: + lsigAccount.sigkey = d["sigkey"] + return lsigAccount + + def is_delegated(self) -> bool: + """ + Check if this LogicSigAccount has been delegated to another account with + a signature. + + Returns: + bool: True if and only if this is a delegated LogicSigAccount. + """ + return bool(self.lsig.sig or self.lsig.msig) + + def verify(self) -> bool: + """ + Verifies the LogicSig's program and signatures. + + Returns: + bool: True if and only if the LogicSig program and signatures are + valid. + """ + addr = self.address() + return self.lsig.verify(encoding.decode_address(addr)) + + def address(self) -> str: + """ + Get the address of this LogicSigAccount. + + If the LogicSig is delegated to another account, this will return the + address of that account. + + If the LogicSig is not delegated to another account, this will return an + escrow address that is the hash of the LogicSig's program code. + """ + if self.lsig.sig and self.lsig.msig: + raise error.LogicSigOverspecifiedSignature + + if self.lsig.sig: + if not self.sigkey: + raise error.LogicSigSigningKeyMissing + return encoding.encode_address(self.sigkey) + + if self.lsig.msig: + return self.lsig.msig.address() + + return self.lsig.address() + + def sign_multisig(self, multisig: Multisig, private_key: str) -> None: + """ + Turns this LogicSigAccount into a delegated LogicSig. + + This type of LogicSig has the authority to sign transactions on behalf + of another account, called the delegating account. Use this function if + the delegating account is a multisig account. + + Args: + multisig (Multisig): The multisig delegating account + private_key (str): The private key of one of the members of the + delegating multisig account. Use `append_to_multisig` to add + additional signatures from other members. + + Raises: + InvalidSecretKeyError: if no matching private key in multisig + object + LogicSigOverspecifiedSignature: if this LogicSigAccount has already + been signed with a single private key. + """ + self.lsig.sign(private_key, multisig) + + def append_to_multisig(self, private_key: str) -> None: + """ + Adds an additional signature from a member of the delegating multisig + account. + + Args: + private_key (str): The private key of one of the members of the + delegating multisig account. + + Raises: + InvalidSecretKeyError: if no matching private key in multisig + object + """ + self.lsig.append_to_multisig(private_key) + + def sign(self, private_key: str) -> None: + """ + Turns this LogicSigAccount into a delegated LogicSig. + + This type of LogicSig has the authority to sign transactions on behalf + of another account, called the delegating account. If the delegating + account is a multisig account, use `sign_multisig` instead. + + Args: + private_key (str): The private key of the delegating account. + + Raises: + LogicSigOverspecifiedSignature: if this LogicSigAccount has already + been signed by a multisig account. + """ + self.lsig.sign(private_key) + public_key = base64.b64decode(bytes(private_key, "utf-8")) + public_key = public_key[constants.key_len_bytes :] + self.sigkey = public_key + + def __eq__(self, other) -> bool: + if not isinstance(other, LogicSigAccount): + return False + return self.lsig == other.lsig and self.sigkey == other.sigkey + - Represents a logic signed transaction. +class LogicSigTransaction: + """ + Represents a logic signed transaction Arguments: transaction (Transaction) - lsig (LogicSig) + lsig (LogicSig or LogicSigAccount) Attributes: transaction (Transaction) lsig (LogicSig) + auth_addr (str, optional) """ - def __init__(self, transaction, lsig): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - warnings.warn( - "`LogicSigTransaction` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) + def __init__( + self, transaction: Transaction, lsig: Union[LogicSig, LogicSigAccount] + ) -> None: self.transaction = transaction - self.lsig = lsig - def verify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. + if isinstance(lsig, LogicSigAccount): + lsigAddr = lsig.address() + self.lsig = lsig.lsig + else: + if lsig.sig: + # For a LogicSig with a non-multisig delegating account, we + # cannot derive the address of that account from only its + # signature, so assume the delegating account is the sender. If + # that's not the case, verify will fail. + lsigAddr = transaction.sender + elif lsig.msig: + lsigAddr = lsig.msig.address() + else: + lsigAddr = lsig.address() + self.lsig = lsig - Verify LogicSig against the transaction + if transaction.sender != lsigAddr: + self.auth_addr: Optional[str] = lsigAddr + else: + self.auth_addr = None + + def verify(self) -> bool: + """ + Verify the LogicSig used to sign the transaction Returns: - bool: true if the signature is valid (the sender address matches\ - the logic hash or the signature is valid against the sender\ - address), false otherwise + bool: true if the signature is valid, false otherwise """ - public_key = encoding.decode_address(self.transaction.sender) + if self.auth_addr: + addr_to_verify = self.auth_addr + else: + addr_to_verify = self.transaction.sender + + public_key = encoding.decode_address(addr_to_verify) return self.lsig.verify(public_key) - def dictify(self): + def get_txid(self): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. + Get the transaction's ID. + + Returns: + str: transaction ID """ + return self.transaction.get_txid() + + def dictify(self): od = OrderedDict() if self.lsig: od["lsig"] = self.lsig.dictify() + if self.auth_addr: + od["sgnr"] = encoding.decode_address(self.auth_addr) od["txn"] = self.transaction.dictify() + return od @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ lsig = None if "lsig" in d: lsig = LogicSig.undictify(d["lsig"]) + auth_addr = None + if "sgnr" in d: + auth_addr = encoding.encode_address(d["sgnr"]) txn = Transaction.undictify(d["txn"]) lstx = LogicSigTransaction(txn, lsig) + lstx.auth_addr = auth_addr return lstx def __eq__(self, other): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - if not isinstance( - other, - (LogicSigTransaction, future.transaction.LogicSigTransaction), - ): + if isinstance(other, LogicSigTransaction): + return ( + self.lsig == other.lsig + and self.auth_addr == other.auth_addr + and self.transaction == other.transaction + ) + + return False + + +class StateProofTxn(Transaction): + """ + Represents a state proof transaction + + Arguments: + sender (str): address of the sender + state_proof (dict(), optional) + state_proof_message (dict(), optional) + state_proof_type (str, optional): state proof type + sp (SuggestedParams): suggested params from algod + + + Attributes: + sender (str) + sprf (dict()) + sprfmsg (dict()) + sprf_type (str) + first_valid_round (int) + last_valid_round (int) + genesis_id (str) + genesis_hash (str) + type (str) + """ + + def __init__( + self, + sender, + sp, + state_proof=None, + state_proof_message=None, + state_proof_type=None, + ): + Transaction.__init__( + self, sender, sp, None, None, constants.stateproof_txn, None + ) + + self.sprf_type = state_proof_type + self.sprf = state_proof + self.sprfmsg = state_proof_message + + def dictify(self): + d = dict() + if self.sprf_type: + d["sptype"] = self.sprf_type + if self.sprfmsg: + d["spmsg"] = self.sprfmsg + if self.sprf: + d["sp"] = self.sprf + d.update(super(StateProofTxn, self).dictify()) + od = OrderedDict(sorted(d.items())) + + return od + + @staticmethod + def _undictify(d): + args = {} + if "sptype" in d: + args["state_proof_type"] = d["sptype"] + if "sp" in d: + args["state_proof"] = d["sp"] + if "spmsg" in d: + args["state_proof_message"] = d["spmsg"] + + return args + + def __eq__(self, other): + if not isinstance(other, StateProofTxn): return False return ( - self.lsig == other.lsig and self.transaction == other.transaction + super(StateProofTxn, self).__eq__(other) + and self.sprf_type == other.sprf_type + and self.sprf == other.sprf + and self.sprfmsg == other.sprfmsg ) + return False -def write_to_file(objs, path, overwrite=True): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. +def write_to_file(txns, path, overwrite=True): + """ Write signed or unsigned transactions to a file. Args: - txns (Transaction[], SignedTransaction[], or MultisigTransaction[]): + txns (Transaction[], SignedTransaction[], or MultisigTransaction[]):\ can be a mix of the three path (str): file to write to overwrite (bool): whether or not to overwrite what's already in the @@ -1834,84 +3051,55 @@ def write_to_file(objs, path, overwrite=True): bool: true if the transactions have been written to the file """ - """ - Write objects to a file. - Args: - objs (Object[]): list of encodable objects - path (str): file to write to - overwrite (bool): whether or not to overwrite what's already in the - file; if False, transactions will be appended to the file - Returns: - bool: true if the transactions have been written to the file - """ - warnings.warn( - "`write_to_file` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) f = None if overwrite: f = open(path, "wb") else: f = open(path, "ab") - for obj in objs: - if isinstance(obj, Transaction): - f.write( - base64.b64decode( - encoding.msgpack_encode({"txn": obj.dictify()}) - ) - ) + for txn in txns: + if isinstance(txn, Transaction): + enc = msgpack.packb({"txn": txn.dictify()}, use_bin_type=True) + f.write(enc) else: - f.write(base64.b64decode(encoding.msgpack_encode(obj))) - + enc = msgpack.packb(txn.dictify(), use_bin_type=True) + f.write(enc) f.close() return True def retrieve_from_file(path): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - - Retrieve encoded objects from a file. + Retrieve signed or unsigned transactions from a file. Args: path (str): file to read from Returns: - Object[]: list of objects + Transaction[], SignedTransaction[], or MultisigTransaction[]:\ + can be a mix of the three """ - warnings.warn( - "`retrieve_from_file` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) + f = open(path, "rb") - objs = [] + txns = [] unp = msgpack.Unpacker(f, raw=False) - for obj in unp: - objs.append(encoding.msgpack_decode(obj)) + for txn in unp: + if "msig" in txn: + txns.append(MultisigTransaction.undictify(txn)) + elif "sig" in txn: + txns.append(SignedTransaction.undictify(txn)) + elif "lsig" in txn: + txns.append(LogicSigTransaction.undictify(txn)) + elif "type" in txn: + txns.append(Transaction.undictify(txn)) + elif "txn" in txn: + txns.append(Transaction.undictify(txn["txn"])) f.close() - return objs + return txns class TxGroup: - """ - NOTE: This class is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - def __init__(self, txns): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ - warnings.warn( - "`TxGroup` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) assert isinstance(txns, list) """ Transactions specifies a list of transactions that must appear @@ -1924,29 +3112,18 @@ def __init__(self, txns): self.transactions = txns def dictify(self): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ od = OrderedDict() od["txlist"] = self.transactions return od @staticmethod def undictify(d): - """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - """ txg = TxGroup(d["txlist"]) return txg def calculate_group_id(txns): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - Calculate group id for a given list of unsigned transactions Args: @@ -1955,11 +3132,6 @@ def calculate_group_id(txns): Returns: bytes: checksum value representing the group id """ - warnings.warn( - "`calculate_group_id` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) if len(txns) > constants.tx_group_limit: raise error.TransactionGroupSizeError txids = [] @@ -1978,24 +3150,16 @@ def calculate_group_id(txns): def assign_group_id(txns, address=None): """ - NOTE: This method is deprecated: - Please use the equivalent in `future.transaction` instead. - - Assign group id to a given list of unsigned transactions. + Assign group id to a given list of unsigned transactions Args: txns (list): list of unsigned transactions address (str): optional sender address specifying which transaction - to return + return Returns: txns (list): list of unsigned transactions with group property set """ - warnings.warn( - "`assign_group_id` is a part of an older `transaction` format that is being deprecated. " - "Please use the equivalent in `future.transaction` instead.", - DeprecationWarning, - ) if len(txns) > constants.tx_group_limit: raise error.TransactionGroupSizeError gid = calculate_group_id(txns) @@ -2005,3 +3169,190 @@ def assign_group_id(txns, address=None): tx.group = gid result.append(tx) return result + + +def wait_for_confirmation( + algod_client: algod.AlgodClient, txid: str, wait_rounds: int = 0, **kwargs +): + """ + Block until a pending transaction is confirmed by the network. + + Args: + algod_client (algod.AlgodClient): Instance of the `algod` client + txid (str): transaction ID + wait_rounds (int, optional): The number of rounds to block for before + exiting with an Exception. If not supplied, this will be 1000. + """ + last_round = algod_client.status()["last-round"] + current_round = last_round + 1 + + if wait_rounds == 0: + wait_rounds = 1000 + + while True: + # Check that the `wait_rounds` has not passed + if current_round > last_round + wait_rounds: + raise error.ConfirmationTimeoutError( + "Wait for transaction id {} timed out".format(txid) + ) + + try: + tx_info = algod_client.pending_transaction_info(txid, **kwargs) + + # The transaction has been rejected + if "pool-error" in tx_info and len(tx_info["pool-error"]) != 0: + raise error.TransactionRejectedError( + "Transaction rejected: " + tx_info["pool-error"] + ) + + # The transaction has been confirmed + if ( + "confirmed-round" in tx_info + and tx_info["confirmed-round"] != 0 + ): + return tx_info + except error.AlgodHTTPError as e: + # Ignore HTTP errors from pending_transaction_info, since it may return 404 if the algod + # instance is behind a load balancer and the request goes to a different algod than the + # one we submitted the transaction to + pass + + # Wait until the block for the `current_round` is confirmed + algod_client.status_after_block(current_round) + + # Incremenent the `current_round` + current_round += 1 + + +defaultAppId = 1380011588 + + +def create_dryrun( + client: algod.AlgodClient, + txns: List[Union[SignedTransaction, LogicSigTransaction]], + protocol_version=None, + latest_timestamp=None, + round=None, +) -> models.DryrunRequest: + """ + Create DryrunRequest object from a client and list of signed transactions + + Args: + algod_client (algod.AlgodClient): Instance of the `algod` client + txns (List[SignedTransaction]): transaction ID + protocol_version (string, optional): The protocol version to evaluate against + latest_timestamp (int, optional): The latest timestamp to evaluate against + round (int, optional): The round to evaluate against + """ + + # The list of info objects passed to the DryrunRequest object + app_infos, acct_infos = [], [] + + # The running list of things we need to fetch + apps, assets, accts = [], [], [] + for t in txns: + txn = t.transaction + + # we only care about app call transactions + if issubclass(type(txn), ApplicationCallTxn): + appTxn = cast(ApplicationCallTxn, txn) + accts.append(appTxn.sender) + + # Add foreign args if they're set + if appTxn.accounts: + accts.extend(appTxn.accounts) + if appTxn.foreign_apps: + apps.extend(appTxn.foreign_apps) + accts.extend( + [ + logic.get_application_address(aidx) + for aidx in appTxn.foreign_apps + ] + ) + if appTxn.foreign_assets: + assets.extend(appTxn.foreign_assets) + + # For creates, we need to add the source directly from the transaction + if appTxn.index == 0: + appId = defaultAppId + # Make up app id, since tealdbg/dryrun doesnt like 0s + # https://github.com/algorand/go-algorand/blob/e466aa18d4d963868d6d15279b1c881977fa603f/libgoal/libgoal.go#L1089-L1090 + + ls = appTxn.local_schema + if ls is not None: + ls = models.ApplicationStateSchema( + ls.num_uints, ls.num_byte_slices + ) + + gs = appTxn.global_schema + if gs is not None: + gs = models.ApplicationStateSchema( + gs.num_uints, gs.num_byte_slices + ) + + app_infos.append( + models.Application( + id=appId, + params=models.ApplicationParams( + creator=appTxn.sender, + approval_program=appTxn.approval_program, + clear_state_program=appTxn.clear_program, + local_state_schema=ls, + global_state_schema=gs, + ), + ) + ) + else: + if appTxn.index: + apps.append(appTxn.index) + + # Dedupe and filter none, reset programs to bytecode instead of b64 + apps = [i for i in set(apps) if i] + for app in apps: + app_info = client.application_info(app) + # Need to pass bytes, not b64 string + app_info = decode_programs(app_info) + app_infos.append(app_info) + + # Make sure the application account is in the accounts array + accts.append(logic.get_application_address(app)) + + # Make sure the creator is added to accounts array + accts.append(app_info["params"]["creator"]) + + # Dedupe and filter None, add asset creator to accounts to include in dryrun + assets = [i for i in set(assets) if i] + for asset in assets: + asset_info = client.asset_info(asset) + + # Make sure the asset creator address is in the accounts array + accts.append(asset_info["params"]["creator"]) + + # Dedupe and filter None, fetch and add account info + accts = [i for i in set(accts) if i] + for acct in accts: + acct_info = client.account_info(acct) + if "created-apps" in acct_info: + acct_info["created-apps"] = [ + decode_programs(ca) for ca in acct_info["created-apps"] + ] + acct_infos.append(acct_info) + + return models.DryrunRequest( + txns=txns, + apps=app_infos, + accounts=acct_infos, + protocol_version=protocol_version, + latest_timestamp=latest_timestamp, + round=round, + ) + + +def decode_programs(app): + app["params"]["approval-program"] = base64.b64decode( + app["params"]["approval-program"] + ) + app["params"]["clear-state-program"] = base64.b64decode( + app["params"]["clear-state-program"] + ) + return app diff --git a/algosdk/v2client/algod.py b/algosdk/v2client/algod.py index 334ea2a3..1614f847 100644 --- a/algosdk/v2client/algod.py +++ b/algosdk/v2client/algod.py @@ -4,7 +4,7 @@ from urllib import parse from urllib.request import Request, urlopen -from algosdk import constants, encoding, error, future, util +from algosdk import constants, encoding, error, transaction, util api_version_path_prefix = "/v2" @@ -265,7 +265,7 @@ def send_transaction(self, txn, **kwargs): str: transaction ID """ assert not isinstance( - txn, future.transaction.Transaction + txn, transaction.Transaction ), "Attempt to send UNSIGNED transaction {}".format(txn) return self.send_raw_transaction( encoding.msgpack_encode(txn), **kwargs @@ -356,7 +356,7 @@ def send_transactions(self, txns, **kwargs): serialized = [] for txn in txns: assert not isinstance( - txn, future.transaction.Transaction + txn, transaction.Transaction ), "Attempt to send UNSIGNED transaction {}".format(txn) serialized.append(base64.b64decode(encoding.msgpack_encode(txn))) @@ -369,7 +369,7 @@ def suggested_params(self, **kwargs): req = "/transactions/params" res = self.algod_request("GET", req, **kwargs) - return future.transaction.SuggestedParams( + return transaction.SuggestedParams( res["fee"], res["last-round"], res["last-round"] + 1000, diff --git a/algosdk/v2client/indexer.py b/algosdk/v2client/indexer.py index 7ed634c3..630ceaac 100644 --- a/algosdk/v2client/indexer.py +++ b/algosdk/v2client/indexer.py @@ -4,7 +4,6 @@ import json import base64 from .. import error -from .. import encoding from .. import constants from .algod import _specify_round_string diff --git a/docs/algosdk/algod.rst b/docs/algosdk/algod.rst deleted file mode 100644 index 56e8d2f6..00000000 --- a/docs/algosdk/algod.rst +++ /dev/null @@ -1,7 +0,0 @@ -algod -===== - -.. automodule:: algosdk.algod - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/algosdk/future/index.rst b/docs/algosdk/future/index.rst deleted file mode 100644 index 7f62648a..00000000 --- a/docs/algosdk/future/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -future -====== -.. toctree:: - :maxdepth: 10 - - template - transaction - \ No newline at end of file diff --git a/docs/algosdk/future/template.rst b/docs/algosdk/future/template.rst deleted file mode 100644 index 587280ec..00000000 --- a/docs/algosdk/future/template.rst +++ /dev/null @@ -1,7 +0,0 @@ -future.template -=============== - -.. automodule:: algosdk.future.template - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/algosdk/future/transaction.rst b/docs/algosdk/future/transaction.rst deleted file mode 100644 index 8eadaacf..00000000 --- a/docs/algosdk/future/transaction.rst +++ /dev/null @@ -1,7 +0,0 @@ -future.transaction -================== - -.. automodule:: algosdk.future.transaction - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/algosdk/index.rst b/docs/algosdk/index.rst index ce7e7cea..18cd8399 100644 --- a/docs/algosdk/index.rst +++ b/docs/algosdk/index.rst @@ -5,7 +5,6 @@ algosdk abi/index account - algod atomic_transaction_composer auction constants @@ -15,7 +14,6 @@ algosdk kmd logic mnemonic - template transaction util v2client/index diff --git a/docs/algosdk/template.rst b/docs/algosdk/template.rst deleted file mode 100644 index 5e7ee29d..00000000 --- a/docs/algosdk/template.rst +++ /dev/null @@ -1,7 +0,0 @@ -template -======== - -.. automodule:: algosdk.template - :members: - :undoc-members: - :show-inheritance: diff --git a/examples/asset_accept_example.py b/examples/asset_accept_example.py index bd1d64b7..b8778792 100644 --- a/examples/asset_accept_example.py +++ b/examples/asset_accept_example.py @@ -1,7 +1,6 @@ # Example: accepting assets -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction private_key, address = account.generate_account() diff --git a/examples/asset_create_example.py b/examples/asset_create_example.py index 8a8afd2e..963ea193 100644 --- a/examples/asset_create_example.py +++ b/examples/asset_create_example.py @@ -1,7 +1,6 @@ # Example: creating an asset -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction # creator private_key, address = account.generate_account() diff --git a/examples/asset_destroy_example.py b/examples/asset_destroy_example.py index 51066be8..0408fbee 100644 --- a/examples/asset_destroy_example.py +++ b/examples/asset_destroy_example.py @@ -1,7 +1,6 @@ # Example: destroying an asset -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction # this transaction must be sent from the creator's account creator_private_key, creator_address = account.generate_account() diff --git a/examples/asset_freeze_example.py b/examples/asset_freeze_example.py index b2f3b44d..4f72d754 100644 --- a/examples/asset_freeze_example.py +++ b/examples/asset_freeze_example.py @@ -1,7 +1,6 @@ # Example: freezing or unfreezing an account -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction # this transaction must be sent from the account specified as the freeze manager for the asset freeze_private_key, freeze_address = account.generate_account() diff --git a/examples/asset_revoke_example.py b/examples/asset_revoke_example.py index 76764efd..14a41b68 100644 --- a/examples/asset_revoke_example.py +++ b/examples/asset_revoke_example.py @@ -1,7 +1,6 @@ # Example: revoking assets -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction # this transaction must be sent by the asset's clawback manager clawback_private_key, clawback_address = account.generate_account() diff --git a/examples/asset_send_example.py b/examples/asset_send_example.py index a2d68331..70f8ebeb 100644 --- a/examples/asset_send_example.py +++ b/examples/asset_send_example.py @@ -1,7 +1,6 @@ # Example: sending assets -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction sender_private_key, sender_address = account.generate_account() diff --git a/examples/asset_update_example.py b/examples/asset_update_example.py index c6beeb9d..79d53866 100644 --- a/examples/asset_update_example.py +++ b/examples/asset_update_example.py @@ -1,7 +1,6 @@ # Example: updating asset configuration -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction # this transaction must be sent from the manager's account manager_private_key, manager_address = account.generate_account() diff --git a/examples/example.py b/examples/example.py index 898e55e4..f6ed8a2b 100644 --- a/examples/example.py +++ b/examples/example.py @@ -1,5 +1,5 @@ from algosdk import encoding -from algosdk.future import transaction +from algosdk import transaction from algosdk import kmd from algosdk.v2client import algod from algosdk import account diff --git a/examples/log_sig_example.py b/examples/log_sig_example.py index 1350661e..ee0ab628 100644 --- a/examples/log_sig_example.py +++ b/examples/log_sig_example.py @@ -1,9 +1,8 @@ # Example: creating a LogicSig transaction signed by a program that never approves the transfer. import tokens -from algosdk import account +from algosdk import account, transaction from algosdk.v2client import algod -from algosdk.future import transaction program = b"\x01\x20\x01\x00\x22" # int 0 lsig = transaction.LogicSigAccount(program) diff --git a/examples/multisig_example.py b/examples/multisig_example.py index 77521b7b..3eb01b97 100644 --- a/examples/multisig_example.py +++ b/examples/multisig_example.py @@ -2,8 +2,7 @@ import tokens -from algosdk import account, encoding -from algosdk.future import transaction +from algosdk import account, encoding, transaction from algosdk.v2client import algod # generate three accounts diff --git a/examples/notefield_example.py b/examples/notefield_example.py index b7079f41..fc73bcf7 100644 --- a/examples/notefield_example.py +++ b/examples/notefield_example.py @@ -7,8 +7,7 @@ import tokens -from algosdk import account, auction, constants, encoding -from algosdk.future import transaction +from algosdk import account, auction, constants, encoding, transaction from algosdk.v2client import algod acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) diff --git a/examples/rekey_example.py b/examples/rekey_example.py index 6e2f17c0..dc08123a 100644 --- a/examples/rekey_example.py +++ b/examples/rekey_example.py @@ -2,8 +2,7 @@ import tokens -from algosdk import account -from algosdk.future import transaction +from algosdk import account, transaction from algosdk.v2client import algod # this should be the current account diff --git a/examples/transaction_group_example.py b/examples/transaction_group_example.py index fa72d927..8265628b 100644 --- a/examples/transaction_group_example.py +++ b/examples/transaction_group_example.py @@ -2,8 +2,7 @@ import tokens -from algosdk import account, kmd -from algosdk.future import transaction +from algosdk import account, kmd, transaction from algosdk.v2client import algod # generate accounts diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..f8b1844b --- /dev/null +++ b/mypy.ini @@ -0,0 +1 @@ +[mypy] diff --git a/requirements.txt b/requirements.txt index 95843578..c3d13e9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ black==22.3.0 glom==20.11.0 pytest==6.2.5 +mypy==0.990 +msgpack-types==0.2.0 git+https://github.com/behave/behave diff --git a/setup.py b/setup.py index 83dc6c3d..933255bc 100644 --- a/setup.py +++ b/setup.py @@ -9,9 +9,10 @@ description="Algorand SDK in Python", author="Algorand", author_email="pypiservice@algorand.com", - version="v1.20.2", + version="v2.0.0", long_description=long_description, long_description_content_type="text/markdown", + url="https://github.com/algorand/py-algorand-sdk", license="MIT", project_urls={ "Source": "https://github.com/algorand/py-algorand-sdk", @@ -21,8 +22,13 @@ "pycryptodomex>=3.6.0,<4", "msgpack>=1.0.0,<2", ], - packages=setuptools.find_packages(), + packages=setuptools.find_packages( + include=( + "algosdk", + "algosdk.*", + ) + ), python_requires=">=3.8", - package_data={"": ["data/langspec.json"]}, + package_data={"": ["*.pyi", "py.typed"]}, include_package_data=True, ) diff --git a/test_integration.py b/test_integration.py deleted file mode 100644 index 8d9d3acb..00000000 --- a/test_integration.py +++ /dev/null @@ -1,531 +0,0 @@ -import base64 -import unittest -from examples import tokens -import os -from algosdk import kmd -from algosdk.future import transaction -from algosdk import encoding -from algosdk import algod -from algosdk import account -from algosdk import mnemonic -from algosdk import error -from algosdk import auction -from algosdk import constants -from algosdk import wallet - - -wallet_name = "unencrypted-default-wallet" -wallet_pswd = "" - - -class TestIntegration(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) - cls.kcl = kmd.KMDClient(tokens.kmd_token, tokens.kmd_address) - w = wallet.Wallet(wallet_name, wallet_pswd, cls.kcl) - keys = w.list_keys() - max_balance = 0 - cls.account_0 = "" - for k in keys: - account_info = cls.acl.account_info(k) - if account_info["amount"] > max_balance: - max_balance = account_info["amount"] - cls.account_0 = k - - def test_auction(self): - # get the default wallet - wallets = self.kcl.list_wallets() - wallet_id = None - for w in wallets: - if w["name"] == wallet_name: - wallet_id = w["id"] - - # get a new handle for the wallet - handle = self.kcl.init_wallet_handle(wallet_id, wallet_pswd) - - # generate account with kmd - account_1 = self.kcl.generate_key(handle, False) - - # get self.account_0 private key - private_key_0 = self.kcl.export_key( - handle, wallet_pswd, self.account_0 - ) - - # create bid - bid = auction.Bid( - self.account_0, 10000, 260, "bid_id", account_1, "auc_id" - ) - sb = bid.sign(private_key_0) - nf = auction.NoteField(sb, constants.note_field_type_bid) - - # get suggested parameters and fee - gh = self.acl.versions()["genesis_hash_b64"] - rnd = int(self.acl.status()["lastRound"]) - sp = transaction.SuggestedParams(0, rnd, rnd + 100, gh) - - # create transaction - txn = transaction.PaymentTxn( - self.account_0, - sp, - account_1, - 100000, - note=base64.b64decode(encoding.msgpack_encode(nf)), - ) - - # sign transaction with account - signed_account = txn.sign(private_key_0) - - # send transaction - send = self.acl.send_transaction(signed_account) - self.assertEqual(send, txn.get_txid()) - del_1 = self.kcl.delete_key(handle, wallet_pswd, account_1) - self.assertTrue(del_1) - - def test_handle(self): - # create wallet; should raise error since wallet already exists - self.assertRaises( - error.KMDHTTPError, - self.kcl.create_wallet, - wallet_name, - wallet_pswd, - ) - - # get the wallet ID - wallets = self.kcl.list_wallets() - - wallet_id = None - for w in wallets: - if w["name"] == wallet_name: - wallet_id = w["id"] - - # rename wallet - self.assertEqual( - wallet_name + "newname", - self.kcl.rename_wallet( - wallet_id, wallet_pswd, wallet_name + "newname" - )["name"], - ) - - # change it back - self.assertEqual( - wallet_name, - self.kcl.rename_wallet(wallet_id, wallet_pswd, wallet_name)[ - "name" - ], - ) - - # get a new handle for the wallet - handle = self.kcl.init_wallet_handle(wallet_id, wallet_pswd) - - # get wallet - self.assertIn("expires_seconds", self.kcl.get_wallet(handle)) - - # renew the handle - renewed_handle = self.kcl.renew_wallet_handle(handle) - self.assertIn("expires_seconds", renewed_handle) - - # release the handle - released = self.kcl.release_wallet_handle(handle) - self.assertTrue(released) - - # check that the handle has been released - self.assertRaises(error.KMDHTTPError, self.kcl.get_wallet, handle) - - def test_transaction(self): - # get the default wallet - wallets = self.kcl.list_wallets() - wallet_id = None - for w in wallets: - if w["name"] == wallet_name: - wallet_id = w["id"] - - # get a new handle for the wallet - handle = self.kcl.init_wallet_handle(wallet_id, wallet_pswd) - - # generate account and check if it's valid - private_key_1, account_1 = account.generate_account() - self.assertTrue(encoding.is_valid_address(account_1)) - - # import generated account - import_key = self.kcl.import_key(handle, private_key_1) - self.assertEqual(import_key, account_1) - - # generate account with kmd - account_2 = self.kcl.generate_key(handle, False) - - # get suggested parameters and fee - gh = self.acl.versions()["genesis_hash_b64"] - rnd = int(self.acl.status()["lastRound"]) - sp = transaction.SuggestedParams(0, rnd, rnd + 100, gh) - - # create transaction - txn = transaction.PaymentTxn(self.account_0, sp, account_1, 100000) - - # sign transaction with kmd - signed_kmd = self.kcl.sign_transaction(handle, wallet_pswd, txn) - - # get self.account_0 private key - private_key_0 = self.kcl.export_key( - handle, wallet_pswd, self.account_0 - ) - # sign transaction with account - signed_account = txn.sign(private_key_0) - txid = txn.get_txid() - - # check that signing both ways results in the same thing - self.assertEqual( - encoding.msgpack_encode(signed_account), - encoding.msgpack_encode(signed_kmd), - ) - - # send the transaction - send = self.acl.send_transaction(signed_account) - self.assertEqual(send, txid) - - # get transaction info in pending transactions - self.assertEqual(self.acl.pending_transaction_info(txid)["tx"], txid) - - # wait for transaction to send - transaction.wait_for_confirmation(self.acl, txid, 10) - - # get transaction info two different ways - info_1 = self.acl.transactions_by_address( - self.account_0, sp.first - 2, sp.first + 2 - ) - info_2 = self.acl.transaction_info(self.account_0, txid) - self.assertIn("transactions", info_1) - self.assertIn("type", info_2) - - # delete accounts - del_1 = self.kcl.delete_key(handle, wallet_pswd, account_1) - del_2 = self.kcl.delete_key(handle, wallet_pswd, account_2) - self.assertTrue(del_1) - self.assertTrue(del_2) - - def test_multisig(self): - # get the default wallet - wallets = self.kcl.list_wallets() - wallet_id = None - for w in wallets: - if w["name"] == wallet_name: - wallet_id = w["id"] - - # get a new handle for the wallet - handle = self.kcl.init_wallet_handle(wallet_id, wallet_pswd) - - # generate two accounts with kmd - account_1 = self.kcl.generate_key(handle, False) - account_2 = self.kcl.generate_key(handle, False) - - # get their private keys - private_key_1 = self.kcl.export_key(handle, wallet_pswd, account_1) - private_key_2 = self.kcl.export_key(handle, wallet_pswd, account_2) - - # get suggested parameters and fee - gh = self.acl.versions()["genesis_hash_b64"] - rnd = int(self.acl.status()["lastRound"]) - sp = transaction.SuggestedParams(0, rnd, rnd + 100, gh) - - # create multisig account and transaction - msig = transaction.Multisig(1, 2, [account_1, account_2]) - txn = transaction.PaymentTxn(msig.address(), sp, self.account_0, 1000) - - # check that the multisig account is valid - msig.validate() - - # import multisig account - msig_address = self.kcl.import_multisig(handle, msig) - - # export multisig account - exported = self.kcl.export_multisig(handle, msig_address) - self.assertEqual(len(exported.subsigs), 2) - - # create multisig transaction - mtx = transaction.MultisigTransaction(txn, msig) - - # sign using kmd - msig_1 = self.kcl.sign_multisig_transaction( - handle, wallet_pswd, account_1, mtx - ) - signed_kmd = self.kcl.sign_multisig_transaction( - handle, wallet_pswd, account_2, msig_1 - ) - - # sign offline - mtx1 = transaction.MultisigTransaction(txn, msig) - mtx1.sign(private_key_1) - mtx2 = transaction.MultisigTransaction(txn, msig) - mtx2.sign(private_key_2) - signed_account = transaction.MultisigTransaction.merge([mtx1, mtx2]) - - # check that they are the same - self.assertEqual( - encoding.msgpack_encode(signed_account), - encoding.msgpack_encode(signed_kmd), - ) - - # delete accounts - del_1 = self.kcl.delete_key(handle, wallet_pswd, account_1) - del_2 = self.kcl.delete_key(handle, wallet_pswd, account_2) - del_3 = self.kcl.delete_multisig(handle, wallet_pswd, msig_address) - self.assertTrue(del_1) - self.assertTrue(del_2) - self.assertTrue(del_3) - - def test_wallet_info(self): - # get the default wallet - wallets = self.kcl.list_wallets() - wallet_id = None - for w in wallets: - if w["name"] == wallet_name: - wallet_id = w["id"] - - # get a new handle for the wallet - handle = self.kcl.init_wallet_handle(wallet_id, wallet_pswd) - - # test listKeys - list_keys = self.kcl.list_keys(handle) - self.assertIn(self.account_0, list_keys) - - # test listMultisig - list_multisig = self.kcl.list_multisig(handle) - self.assertIsInstance(list_multisig, list) - # either addresses are listed or there are no multisig accounts - - # test getting the master derivation key - mdk = self.kcl.export_master_derivation_key(handle, wallet_pswd) - self.assertIsInstance(mdk, str) - - def test_wallet(self): - # initialize wallet - w = wallet.Wallet(wallet_name, wallet_pswd, self.kcl) - - # get master derivation key - mdk = w.export_master_derivation_key() - - # get mnemonic - mn = w.get_mnemonic() - - # make sure mnemonic can be converted back to mdk - self.assertEqual(mdk, mnemonic.to_master_derivation_key(mn)) - - # generate account with account and check if it's valid - private_key_1, account_1 = account.generate_account() - - # import generated account - import_key = w.import_key(private_key_1) - self.assertEqual(import_key, account_1) - - # check that the account is in the wallet - keys = w.list_keys() - self.assertIn(account_1, keys) - - # generate account with kmd - account_2 = w.generate_key() - private_key_2 = w.export_key(account_2) - - # get suggested parameters and fee - gh = self.acl.versions()["genesis_hash_b64"] - rnd = int(self.acl.status()["lastRound"]) - sp = transaction.SuggestedParams(0, rnd, rnd + 100, gh) - - # create transaction - txn = transaction.PaymentTxn(self.account_0, sp, account_1, 100000) - - # sign transaction with wallet - signed_kmd = w.sign_transaction(txn) - - # get self.account_0 private key - private_key_0 = w.export_key(self.account_0) - - # sign transaction with account - signed_account = txn.sign(private_key_0) - - # check that signing both ways results in the same thing - self.assertEqual( - encoding.msgpack_encode(signed_account), - encoding.msgpack_encode(signed_kmd), - ) - - # create multisig account and transaction - msig = transaction.Multisig(1, 2, [account_1, account_2]) - txn = transaction.PaymentTxn(msig.address(), sp, self.account_0, 1000) - - # import multisig account - msig_address = w.import_multisig(msig) - - # check that the multisig account is listed - msigs = w.list_multisig() - self.assertIn(msig_address, msigs) - - # export multisig account - exported = w.export_multisig(msig_address) - self.assertEqual(len(exported.subsigs), 2) - - # create multisig transaction - mtx = transaction.MultisigTransaction(txn, msig) - - # sign the multisig using kmd - msig_1 = w.sign_multisig_transaction(account_1, mtx) - signed_kmd = w.sign_multisig_transaction(account_2, msig_1) - - # sign the multisig offline - mtx1 = transaction.MultisigTransaction(txn, msig) - mtx1.sign(private_key_1) - mtx2 = transaction.MultisigTransaction(txn, msig) - mtx2.sign(private_key_2) - signed_account = transaction.MultisigTransaction.merge([mtx1, mtx2]) - - # check that they are the same - self.assertEqual( - encoding.msgpack_encode(signed_account), - encoding.msgpack_encode(signed_kmd), - ) - - # delete accounts - del_1 = w.delete_key(account_1) - del_2 = w.delete_key(account_2) - del_3 = w.delete_multisig(msig_address) - self.assertTrue(del_1) - self.assertTrue(del_2) - self.assertTrue(del_3) - - # test renaming the wallet - w.rename(wallet_name + "1") - self.assertEqual(wallet_name + "1", w.info()["wallet"]["name"]) - w.rename(wallet_name) - self.assertEqual(wallet_name, w.info()["wallet"]["name"]) - - # test releasing the handle - w.release_handle() - self.assertRaises(error.KMDHTTPError, self.kcl.get_wallet, w.handle) - - # test handle automation - w.info() - - def test_file_read_write(self): - # get suggested parameters and fee - gh = self.acl.versions()["genesis_hash_b64"] - rnd = int(self.acl.status()["lastRound"]) - sp = transaction.SuggestedParams(0, rnd, rnd + 100, gh) - - # create transaction - txn = transaction.PaymentTxn(self.account_0, sp, self.account_0, 1000) - - # get private key - w = wallet.Wallet(wallet_name, wallet_pswd, self.kcl) - private_key = w.export_key(self.account_0) - - # sign transaction - stx = txn.sign(private_key) - - # write to file - dir_path = os.path.dirname(os.path.realpath(__file__)) - transaction.write_to_file([txn, stx], dir_path + "/raw.tx") - - # read from file - txns = transaction.retrieve_from_file(dir_path + "/raw.tx") - - # check that the transactions are still the same - self.assertEqual( - encoding.msgpack_encode(txn), encoding.msgpack_encode(txns[0]) - ) - self.assertEqual( - encoding.msgpack_encode(stx), encoding.msgpack_encode(txns[1]) - ) - - # delete the file - os.remove("raw.tx") - - def test_health(self): - result = self.acl.health() - self.assertEqual(result, None) - - def test_status_after_block(self): - last_round = self.acl.status()["lastRound"] - curr_round = self.acl.status_after_block(last_round)["lastRound"] - self.assertEqual(last_round + 1, curr_round) - - def test_pending_transactions(self): - result = self.acl.pending_transactions(0) - self.assertIn("truncatedTxns", result) - - def test_algod_versions(self): - result = self.acl.versions() - self.assertIn("versions", result) - - def test_ledger_supply(self): - result = self.acl.ledger_supply() - self.assertIn("totalMoney", result) - - def test_block_info(self): - last_round = self.acl.status()["lastRound"] - result = self.acl.block_info(last_round) - self.assertIn("hash", result) - - def test_kmd_versions(self): - result = self.kcl.versions() - self.assertIn("v1", result) - - def test_suggested_fee(self): - result = self.acl.suggested_fee() - self.assertIn("fee", result) - - def test_transaction_group(self): - # get the default wallet - wallets = self.kcl.list_wallets() - wallet_id = None - for w in wallets: - if w["name"] == wallet_name: - wallet_id = w["id"] - - # get a new handle for the wallet - handle = self.kcl.init_wallet_handle(wallet_id, wallet_pswd) - - # get private key - private_key_0 = self.kcl.export_key( - handle, wallet_pswd, self.account_0 - ) - - # get suggested parameters and fee - gh = self.acl.versions()["genesis_hash_b64"] - rnd = int(self.acl.status()["lastRound"]) - sp = transaction.SuggestedParams(0, rnd, rnd + 100, gh) - - # create transaction - txn = transaction.PaymentTxn(self.account_0, sp, self.account_0, 1000) - - # calculate group id - gid = transaction.calculate_group_id([txn]) - txn.group = gid - - # sign using kmd - stxn1 = self.kcl.sign_transaction(handle, wallet_pswd, txn) - # sign using transaction call - stxn2 = txn.sign(private_key_0) - # check that they are the same - self.assertEqual( - encoding.msgpack_encode(stxn1), encoding.msgpack_encode(stxn2) - ) - - try: - send = self.acl.send_transactions([stxn1]) - self.assertEqual(send, txn.get_txid()) - except error.AlgodHTTPError as ex: - self.assertNotIn('{"message"', str(ex)) - self.assertIn( - "TransactionPool.Remember: transaction groups not supported", - str(ex), - ) - - -if __name__ == "__main__": - to_run = [TestIntegration] - loader = unittest.TestLoader() - suites = [ - loader.loadTestsFromTestCase(test_class) for test_class in to_run - ] - suite = unittest.TestSuite(suites) - runner = unittest.TextTestRunner(verbosity=2) - results = runner.run(suite) diff --git a/tests/steps/account_v2_steps.py b/tests/steps/account_v2_steps.py index 28764cbd..892b3bd7 100644 --- a/tests/steps/account_v2_steps.py +++ b/tests/steps/account_v2_steps.py @@ -1,7 +1,6 @@ from typing import Union -from algosdk import account, encoding, logic -from algosdk.future import transaction +from algosdk import account, encoding, logic, transaction from behave import given, then, when import tests.steps.other_v2_steps # Imports MaybeString @@ -240,6 +239,7 @@ def parse_accounts_auth(context, roundNum, length, index, authAddr): @given('a signing account with address "{address}" and mnemonic "{mnemonic}"') def signing_account(context, address, mnemonic): + context.signing_address = address context.signing_mnemonic = mnemonic diff --git a/tests/steps/application_v2_steps.py b/tests/steps/application_v2_steps.py index 55c0d89a..94d1e809 100644 --- a/tests/steps/application_v2_steps.py +++ b/tests/steps/application_v2_steps.py @@ -6,10 +6,16 @@ import pytest from behave import given, step, then, when -from algosdk import abi, atomic_transaction_composer, encoding, mnemonic +from algosdk import ( + abi, + account, + atomic_transaction_composer, + encoding, + mnemonic, + transaction, +) from algosdk.abi.contract import NetworkInfo from algosdk.error import ABITypeError, AtomicTransactionComposerError -from algosdk.future import transaction from tests.steps.other_v2_steps import read_program @@ -419,14 +425,9 @@ def reset_appid_list(context): @step("I remember the new application ID.") def remember_app_id(context): - if hasattr(context, "acl"): - app_id = context.acl.pending_transaction_info(context.app_txid)[ - "txresults" - ]["createdapp"] - else: - app_id = context.app_acl.pending_transaction_info(context.app_txid)[ - "application-index" - ] + app_id = context.app_acl.pending_transaction_info(context.app_txid)[ + "application-index" + ] context.current_application_id = app_id if not hasattr(context, "app_ids"): @@ -637,7 +638,7 @@ def abi_method_adder( if account_type == "transient": sender = context.transient_pk elif account_type == "signing": - sender = mnemonic.to_public_key(context.signing_mnemonic) + sender = context.signing_address else: raise NotImplementedError( "cannot make transaction signer for " + account_type diff --git a/tests/steps/other_v2_steps.py b/tests/steps/other_v2_steps.py index 288c0f74..ef038d29 100644 --- a/tests/steps/other_v2_steps.py +++ b/tests/steps/other_v2_steps.py @@ -11,9 +11,15 @@ from behave import given, step, then, when from glom import glom -from algosdk import dryrun_results, encoding, error, mnemonic, source_map +from algosdk import ( + dryrun_results, + encoding, + error, + mnemonic, + source_map, + transaction, +) from algosdk.error import AlgodHTTPError -from algosdk.future import transaction from algosdk.testing.dryrun import DryrunTestCaseMixin from algosdk.v2client import * from algosdk.v2client.models import ( @@ -1011,7 +1017,7 @@ def compare_to_base64_golden(context, golden): @then("the decoded transaction should equal the original") def compare_to_original(context): encoded = encoding.msgpack_encode(context.signed_transaction) - decoded = encoding.future_msgpack_decode(encoded) + decoded = encoding.msgpack_decode(encoded) assert decoded.transaction == context.transaction @@ -1077,7 +1083,7 @@ def dryrun_step(context, kind, program): sources = [] if kind == "compiled": - lsig = transaction.LogicSig(data) + lsig = transaction.LogicSigAccount(bytes(data)) txns = [transaction.LogicSigTransaction(txn, lsig)] elif kind == "source": txns = [transaction.SignedTransaction(txn, None)] diff --git a/tests/steps/steps.py b/tests/steps/steps.py index 9116c662..86d1c608 100644 --- a/tests/steps/steps.py +++ b/tests/steps/steps.py @@ -15,8 +15,8 @@ mnemonic, util, wallet, + transaction, ) -from algosdk.future import transaction @parse.with_pattern(r".*") @@ -940,7 +940,7 @@ def heuristic_check_over_bytes(context): context.sanity_check_err = "" try: - transaction.LogicSig(context.seemingly_program) + transaction.LogicSigAccount(context.seemingly_program) except Exception as e: context.sanity_check_err = str(e) diff --git a/tests/unit_tests/test_dryrun.py b/tests/unit_tests/test_dryrun.py index 2cf6f985..8754643f 100644 --- a/tests/unit_tests/test_dryrun.py +++ b/tests/unit_tests/test_dryrun.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import Mock -from algosdk.future import transaction +from algosdk import transaction from algosdk.testing import dryrun diff --git a/tests/unit_tests/test_logicsig.py b/tests/unit_tests/test_logicsig.py index 6d5fc6d9..ef2cfac8 100644 --- a/tests/unit_tests/test_logicsig.py +++ b/tests/unit_tests/test_logicsig.py @@ -1,8 +1,7 @@ import base64 import unittest -from algosdk import account, encoding, error, mnemonic -from algosdk.future import transaction +from algosdk import account, encoding, error, mnemonic, transaction class TestLogicSig(unittest.TestCase): @@ -11,7 +10,7 @@ def test_basic(self): lsig = transaction.LogicSig(None) with self.assertRaises(error.InvalidProgram): - lsig = transaction.LogicSig(b"") + lsig = transaction.LogicSigAccount(b"") program = b"\x01\x20\x01\x01\x22" # int 1 program_hash = ( @@ -19,7 +18,7 @@ def test_basic(self): ) public_key = encoding.decode_address(program_hash) - lsig = transaction.LogicSig(program) + lsig = transaction.LogicSigAccount(program).lsig self.assertEqual(lsig.logic, program) self.assertEqual(lsig.args, None) self.assertEqual(lsig.sig, None) @@ -32,7 +31,7 @@ def test_basic(self): b"\x01\x02\x03", b"\x04\x05\x06", ] - lsig = transaction.LogicSig(program, args) + lsig = transaction.LogicSigAccount(program, args).lsig self.assertEqual(lsig.logic, program) self.assertEqual(lsig.args, args) self.assertEqual(lsig.sig, None) @@ -49,7 +48,7 @@ def test_basic(self): # check signature verification on modified program program = b"\x01\x20\x01\x03\x22" - lsig = transaction.LogicSig(program) + lsig = transaction.LogicSigAccount(program).lsig self.assertEqual(lsig.logic, program) verified = lsig.verify(public_key) self.assertFalse(verified) @@ -57,7 +56,7 @@ def test_basic(self): # check invalid program fails program = b"\x00\x20\x01\x03\x22" - lsig = transaction.LogicSig(program) + lsig = transaction.LogicSigAccount(program).lsig verified = lsig.verify(public_key) self.assertFalse(verified) @@ -65,7 +64,7 @@ def test_signature(self): private_key, address = account.generate_account() public_key = encoding.decode_address(address) program = b"\x01\x20\x01\x01\x22" # int 1 - lsig = transaction.LogicSig(program) + lsig = transaction.LogicSigAccount(program).lsig lsig.sign(private_key) self.assertEqual(lsig.logic, program) self.assertEqual(lsig.args, None) @@ -90,7 +89,7 @@ def test_multisig(self): # create multisig address with invalid version msig = transaction.Multisig(1, 2, [account_1, account_2]) program = b"\x01\x20\x01\x01\x22" # int 1 - lsig = transaction.LogicSig(program) + lsig = transaction.LogicSigAccount(program).lsig lsig.sign(private_key_1, msig) self.assertEqual(lsig.logic, program) self.assertEqual(lsig.args, None) @@ -110,7 +109,7 @@ def test_multisig(self): self.assertTrue(verified) # combine sig and multisig, ensure it fails - lsigf = transaction.LogicSig(program) + lsigf = transaction.LogicSigAccount(program).lsig lsigf.sign(private_key) lsig.sig = lsigf.sig verified = lsig.verify(public_key) @@ -172,7 +171,7 @@ def test_transaction(self): program = b"\x01\x20\x01\x01\x22" # int 1 args = [b"123", b"456"] sk = mnemonic.to_private_key(mn) - lsig = transaction.LogicSig(program, args) + lsig = transaction.LogicSigAccount(program, args).lsig lsig.sign(sk) lstx = transaction.LogicSigTransaction(tx, lsig) verified = lstx.verify() @@ -218,7 +217,7 @@ def test_create_no_args(self): expectedEncoded = "gaRsc2lngaFsxAUBIAEBIg==" self.assertEqual(encoded, expectedEncoded) - decoded = encoding.future_msgpack_decode(encoded) + decoded = encoding.msgpack_decode(encoded) self.assertEqual(decoded, lsigAccount) def test_create_with_args(self): @@ -238,7 +237,7 @@ def test_create_with_args(self): expectedEncoded = "gaRsc2lngqNhcmeSxAEBxAICA6FsxAUBIAEBIg==" self.assertEqual(encoded, expectedEncoded) - decoded = encoding.future_msgpack_decode(encoded) + decoded = encoding.msgpack_decode(encoded) self.assertEqual(decoded, lsigAccount) def test_sign(self): @@ -264,7 +263,7 @@ def test_sign(self): expectedEncoded = "gqRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqNzaWfEQEkTuAXRnn8sEID2M34YVKfO6u4Q3b0TZYS/k7dfMGMVkcojDO3vI9F0G1KdsP/vN1TWRvS1YfyLvC17TmNcvQKmc2lna2V5xCAbfsCwS+pht5aQl+bL9AfhCKcFNR0LyYq+sSIJqKuBeA==" self.assertEqual(encoded, expectedEncoded) - decoded = encoding.future_msgpack_decode(encoded) + decoded = encoding.msgpack_decode(encoded) self.assertEqual(decoded, lsigAccount) def test_sign_multisig(self): @@ -277,7 +276,7 @@ def test_sign_multisig(self): expectedSig = base64.b64decode( "SRO4BdGefywQgPYzfhhUp87q7hDdvRNlhL+Tt18wYxWRyiMM7e8j0XQbUp2w/+83VNZG9LVh/Iu8LXtOY1y9Ag==" ) - expectedMsig = encoding.future_msgpack_decode( + expectedMsig = encoding.msgpack_decode( encoding.msgpack_encode(sampleMsig) ) expectedMsig.subsigs[0].signature = expectedSig @@ -293,7 +292,7 @@ def test_sign_multisig(self): expectedEncoded = "gaRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RASRO4BdGefywQgPYzfhhUp87q7hDdvRNlhL+Tt18wYxWRyiMM7e8j0XQbUp2w/+83VNZG9LVh/Iu8LXtOY1y9AoGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AQ==" self.assertEqual(encoded, expectedEncoded) - decoded = encoding.future_msgpack_decode(encoded) + decoded = encoding.msgpack_decode(encoded) self.assertEqual(decoded, lsigAccount) def test_append_to_multisig(self): @@ -301,7 +300,7 @@ def test_append_to_multisig(self): args = [b"\x01", b"\x02\x03"] msig1of3Encoded = "gaRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RASRO4BdGefywQgPYzfhhUp87q7hDdvRNlhL+Tt18wYxWRyiMM7e8j0XQbUp2w/+83VNZG9LVh/Iu8LXtOY1y9AoGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AQ==" - lsigAccount = encoding.future_msgpack_decode(msig1of3Encoded) + lsigAccount = encoding.msgpack_decode(msig1of3Encoded) lsigAccount.append_to_multisig(sampleAccount2) @@ -311,7 +310,7 @@ def test_append_to_multisig(self): expectedSig2 = base64.b64decode( "ZLxV2+2RokHUKrZg9+FKuZmaUrOxcVjO/D9P58siQRStqT1ehAUCChemaYMDIk6Go4tqNsVUviBQ/9PuqLMECQ==" ) - expectedMsig = encoding.future_msgpack_decode( + expectedMsig = encoding.msgpack_decode( encoding.msgpack_encode(sampleMsig) ) expectedMsig.subsigs[0].signature = expectedSig1 @@ -328,16 +327,16 @@ def test_append_to_multisig(self): expectedEncoded = "gaRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RASRO4BdGefywQgPYzfhhUp87q7hDdvRNlhL+Tt18wYxWRyiMM7e8j0XQbUp2w/+83VNZG9LVh/Iu8LXtOY1y9AoKicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxoXPEQGS8VdvtkaJB1Cq2YPfhSrmZmlKzsXFYzvw/T+fLIkEUrak9XoQFAgoXpmmDAyJOhqOLajbFVL4gUP/T7qizBAmBonBrxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaN0aHICoXYB" self.assertEqual(encoded, expectedEncoded) - decoded = encoding.future_msgpack_decode(encoded) + decoded = encoding.msgpack_decode(encoded) self.assertEqual(decoded, lsigAccount) def test_verify(self): escrowEncoded = "gaRsc2lngqNhcmeSxAEBxAICA6FsxAUBIAEBIg==" - escrowLsigAccount = encoding.future_msgpack_decode(escrowEncoded) + escrowLsigAccount = encoding.msgpack_decode(escrowEncoded) self.assertEqual(escrowLsigAccount.verify(), True) sigEncoded = "gqRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqNzaWfEQEkTuAXRnn8sEID2M34YVKfO6u4Q3b0TZYS/k7dfMGMVkcojDO3vI9F0G1KdsP/vN1TWRvS1YfyLvC17TmNcvQKmc2lna2V5xCAbfsCwS+pht5aQl+bL9AfhCKcFNR0LyYq+sSIJqKuBeA==" - sigLsigAccount = encoding.future_msgpack_decode(sigEncoded) + sigLsigAccount = encoding.msgpack_decode(sigEncoded) self.assertEqual(sigLsigAccount.verify(), True) sigLsigAccount.lsig.sig = "AQ==" # wrong length of bytes @@ -347,7 +346,7 @@ def test_verify(self): self.assertEqual(sigLsigAccount.verify(), False) msigEncoded = "gaRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RASRO4BdGefywQgPYzfhhUp87q7hDdvRNlhL+Tt18wYxWRyiMM7e8j0XQbUp2w/+83VNZG9LVh/Iu8LXtOY1y9AoKicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxoXPEQGS8VdvtkaJB1Cq2YPfhSrmZmlKzsXFYzvw/T+fLIkEUrak9XoQFAgoXpmmDAyJOhqOLajbFVL4gUP/T7qizBAmBonBrxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaN0aHICoXYB" - msigLsigAccount = encoding.future_msgpack_decode(msigEncoded) + msigLsigAccount = encoding.msgpack_decode(msigEncoded) self.assertEqual(msigLsigAccount.verify(), True) msigLsigAccount.lsig.msig.subsigs[0].signature = None @@ -355,34 +354,34 @@ def test_verify(self): def test_is_delegated(self): escrowEncoded = "gaRsc2lngqNhcmeSxAEBxAICA6FsxAUBIAEBIg==" - escrowLsigAccount = encoding.future_msgpack_decode(escrowEncoded) + escrowLsigAccount = encoding.msgpack_decode(escrowEncoded) self.assertEqual(escrowLsigAccount.is_delegated(), False) sigEncoded = "gqRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqNzaWfEQEkTuAXRnn8sEID2M34YVKfO6u4Q3b0TZYS/k7dfMGMVkcojDO3vI9F0G1KdsP/vN1TWRvS1YfyLvC17TmNcvQKmc2lna2V5xCAbfsCwS+pht5aQl+bL9AfhCKcFNR0LyYq+sSIJqKuBeA==" - sigLsigAccount = encoding.future_msgpack_decode(sigEncoded) + sigLsigAccount = encoding.msgpack_decode(sigEncoded) self.assertEqual(sigLsigAccount.is_delegated(), True) msigEncoded = "gaRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RASRO4BdGefywQgPYzfhhUp87q7hDdvRNlhL+Tt18wYxWRyiMM7e8j0XQbUp2w/+83VNZG9LVh/Iu8LXtOY1y9AoKicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxoXPEQGS8VdvtkaJB1Cq2YPfhSrmZmlKzsXFYzvw/T+fLIkEUrak9XoQFAgoXpmmDAyJOhqOLajbFVL4gUP/T7qizBAmBonBrxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaN0aHICoXYB" - msigLsigAccount = encoding.future_msgpack_decode(msigEncoded) + msigLsigAccount = encoding.msgpack_decode(msigEncoded) self.assertEqual(msigLsigAccount.is_delegated(), True) def test_address(self): escrowEncoded = "gaRsc2lngqNhcmeSxAEBxAICA6FsxAUBIAEBIg==" - escrowLsigAccount = encoding.future_msgpack_decode(escrowEncoded) + escrowLsigAccount = encoding.msgpack_decode(escrowEncoded) escrowExpectedAddr = ( "6Z3C3LDVWGMX23BMSYMANACQOSINPFIRF77H7N3AWJZYV6OH6GWTJKVMXY" ) self.assertEqual(escrowLsigAccount.address(), escrowExpectedAddr) sigEncoded = "gqRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqNzaWfEQEkTuAXRnn8sEID2M34YVKfO6u4Q3b0TZYS/k7dfMGMVkcojDO3vI9F0G1KdsP/vN1TWRvS1YfyLvC17TmNcvQKmc2lna2V5xCAbfsCwS+pht5aQl+bL9AfhCKcFNR0LyYq+sSIJqKuBeA==" - sigLsigAccount = encoding.future_msgpack_decode(sigEncoded) + sigLsigAccount = encoding.msgpack_decode(sigEncoded) sigExpectedAddr = ( "DN7MBMCL5JQ3PFUQS7TMX5AH4EEKOBJVDUF4TCV6WERATKFLQF4MQUPZTA" ) self.assertEqual(sigLsigAccount.address(), sigExpectedAddr) msigEncoded = "gaRsc2lng6NhcmeSxAEBxAICA6FsxAUBIAEBIqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RASRO4BdGefywQgPYzfhhUp87q7hDdvRNlhL+Tt18wYxWRyiMM7e8j0XQbUp2w/+83VNZG9LVh/Iu8LXtOY1y9AoKicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxoXPEQGS8VdvtkaJB1Cq2YPfhSrmZmlKzsXFYzvw/T+fLIkEUrak9XoQFAgoXpmmDAyJOhqOLajbFVL4gUP/T7qizBAmBonBrxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaN0aHICoXYB" - msigLsigAccount = encoding.future_msgpack_decode(msigEncoded) + msigLsigAccount = encoding.msgpack_decode(msigEncoded) msigExpectedAddr = ( "RWJLJCMQAFZ2ATP2INM2GZTKNL6OULCCUBO5TQPXH3V2KR4AG7U5UA5JNM" ) @@ -423,22 +422,22 @@ def _test_sign_txn( actualEncoded = encoding.msgpack_encode(actual) self.assertEqual(actualEncoded, expectedEncoded) - decoded = encoding.future_msgpack_decode(actualEncoded) + decoded = encoding.msgpack_decode(actualEncoded) self.assertEqual(decoded, actual) def test_LogicSig_escrow(self): - lsig = transaction.LogicSig( + lsig = transaction.LogicSigAccount( TestLogicSigTransaction.program, TestLogicSigTransaction.args - ) + ).lsig sender = lsig.address() expected = "gqRsc2lngqNhcmeSxAEBxAICA6FsxAUBIAEBIqN0eG6Ko2FtdM0TiKNmZWXOAANPqKJmds4ADtbco2dlbq10ZXN0bmV0LXYzMS4womdoxCAmCyAJoJOohot5WHIvpeVG7eftF+TYXEx4r7BFJpDt0qJsds4ADtrEpG5vdGXECLRReTn8+tJxo3JjdsQgtMYiaKTDNVD1im3UuMojnJ8dELNBqn4aNuPOYfv8+Yqjc25kxCD2di2sdbGZfWwslhgGgFB0kNeVES/+f7dgsnOK+cfxraR0eXBlo3BheQ==" self._test_sign_txn(lsig, sender, expected) def test_LogicSig_escrow_different_sender(self): - lsig = transaction.LogicSig( + lsig = transaction.LogicSigAccount( TestLogicSigTransaction.program, TestLogicSigTransaction.args - ) + ).lsig sender = TestLogicSigTransaction.otherAddr expected = "g6Rsc2lngqNhcmeSxAEBxAICA6FsxAUBIAEBIqRzZ25yxCD2di2sdbGZfWwslhgGgFB0kNeVES/+f7dgsnOK+cfxraN0eG6Ko2FtdM0TiKNmZWXOAANPqKJmds4ADtbco2dlbq10ZXN0bmV0LXYzMS4womdoxCAmCyAJoJOohot5WHIvpeVG7eftF+TYXEx4r7BFJpDt0qJsds4ADtrEpG5vdGXECLRReTn8+tJxo3JjdsQgtMYiaKTDNVD1im3UuMojnJ8dELNBqn4aNuPOYfv8+Yqjc25kxCC0xiJopMM1UPWKbdS4yiOcnx0Qs0Gqfho2485h+/z5iqR0eXBlo3BheQ==" @@ -449,9 +448,9 @@ def test_LogicSig_single_delegated(self): "olympic cricket tower model share zone grid twist sponsor avoid eight apology patient party success claim famous rapid donor pledge bomb mystery security ability often" ) - lsig = transaction.LogicSig( + lsig = transaction.LogicSigAccount( TestLogicSigTransaction.program, TestLogicSigTransaction.args - ) + ).lsig lsig.sign(sk) sender = account.address_from_private_key(sk) @@ -463,18 +462,18 @@ def test_LogicSig_single_delegated_different_sender(self): "olympic cricket tower model share zone grid twist sponsor avoid eight apology patient party success claim famous rapid donor pledge bomb mystery security ability often" ) - lsig = transaction.LogicSig( + lsig = transaction.LogicSigAccount( TestLogicSigTransaction.program, TestLogicSigTransaction.args - ) + ).lsig lsig.sign(sk) sender = TestLogicSigTransaction.otherAddr self._test_sign_txn(lsig, sender, None, False) def test_LogicSig_msig_delegated(self): - lsig = transaction.LogicSig( + lsig = transaction.LogicSigAccount( TestLogicSigTransaction.program, TestLogicSigTransaction.args - ) + ).lsig lsig.sign(sampleAccount1, sampleMsig) lsig.append_to_multisig(sampleAccount2) @@ -483,9 +482,9 @@ def test_LogicSig_msig_delegated(self): self._test_sign_txn(lsig, sender, expected) def test_LogicSig_msig_delegated_different_sender(self): - lsig = transaction.LogicSig( + lsig = transaction.LogicSigAccount( TestLogicSigTransaction.program, TestLogicSigTransaction.args - ) + ).lsig lsig.sign(sampleAccount1, sampleMsig) lsig.append_to_multisig(sampleAccount2) diff --git a/tests/unit_tests/test_other.py b/tests/unit_tests/test_other.py index a156c235..46dfe4e9 100644 --- a/tests/unit_tests/test_other.py +++ b/tests/unit_tests/test_other.py @@ -177,7 +177,7 @@ def test_payment_txn_future(self): ) self.assertEqual( paytxn, - encoding.msgpack_encode(encoding.future_msgpack_decode(paytxn)), + encoding.msgpack_encode(encoding.msgpack_decode(paytxn)), ) def test_asset_xfer_txn_future(self): @@ -189,7 +189,7 @@ def test_asset_xfer_txn_future(self): ) self.assertEqual( axfer, - encoding.msgpack_encode(encoding.future_msgpack_decode(axfer)), + encoding.msgpack_encode(encoding.msgpack_decode(axfer)), ) def test_multisig_txn(self): @@ -227,11 +227,9 @@ def test_keyreg_txn_offline(self): "OUJOiKibHbOALuxk6NzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tu" "H9pHR5cGWma2V5cmVn" ) - # using future_msgpack_decode instead of msgpack_decode - # because non-future transactions do not support offline keyreg self.assertEqual( keyregtxn, - encoding.msgpack_encode(encoding.future_msgpack_decode(keyregtxn)), + encoding.msgpack_encode(encoding.msgpack_decode(keyregtxn)), ) def test_keyreg_txn_nonpart(self): @@ -240,11 +238,9 @@ def test_keyreg_txn_nonpart(self): "OUJOiKibHbOALuxk6dub25wYXJ0w6NzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUO" "B+jFx2mGR9tuH9pHR5cGWma2V5cmVn" ) - # using future_msgpack_decode instead of msgpack_decode - # because non-future transactions do not support nonpart keyreg self.assertEqual( keyregtxn, - encoding.msgpack_encode(encoding.future_msgpack_decode(keyregtxn)), + encoding.msgpack_encode(encoding.msgpack_decode(keyregtxn)), ) def test_stateproof_txn(self): @@ -406,9 +402,7 @@ def test_stateproof_txn(self): self.assertEqual( stateprooftxn, - encoding.msgpack_encode( - encoding.future_msgpack_decode(stateprooftxn) - ), + encoding.msgpack_encode(encoding.msgpack_decode(stateprooftxn)), ) def test_asset_create(self): @@ -546,134 +540,6 @@ def test_verify_negative(self): class TestLogic(unittest.TestCase): - def test_parse_uvarint(self): - data = b"\x01" - value, length = logic.parse_uvarint(data) - self.assertEqual(length, 1) - self.assertEqual(value, 1) - - data = b"\x7b" - value, length = logic.parse_uvarint(data) - self.assertEqual(length, 1) - self.assertEqual(value, 123) - - data = b"\xc8\x03" - value, length = logic.parse_uvarint(data) - self.assertEqual(length, 2) - self.assertEqual(value, 456) - - def test_parse_intcblock(self): - data = b"\x20\x05\x00\x01\xc8\x03\x7b\x02" - size = logic.check_int_const_block(data, 0) - self.assertEqual(size, len(data)) - - def test_parse_bytecblock(self): - data = ( - b"\x26\x02\x0d\x31\x32\x33\x34\x35\x36\x37\x38\x39\x30\x31" - b"\x32\x33\x02\x01\x02" - ) - size = logic.check_byte_const_block(data, 0) - self.assertEqual(size, len(data)) - - def test_parse_pushint(self): - data = b"\x81\x80\x80\x04" - size = logic.check_push_int_block(data, 0) - self.assertEqual(size, len(data)) - - def test_parse_pushbytes(self): - data = b"\x80\x0b\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64" - size = logic.check_push_byte_block(data, 0) - self.assertEqual(size, len(data)) - - def test_check_program(self): - program = b"\x01\x20\x01\x01\x22" # int 1 - self.assertTrue(logic.check_program(program, None)) - - self.assertTrue(logic.check_program(program, ["a" * 10])) - - # too long arg - with self.assertRaises(error.InvalidProgram): - logic.check_program(program, ["a" * 1000]) - - program += b"\x22" * 10 - self.assertTrue(logic.check_program(program, None)) - - # too long program - program += b"\x22" * 1000 - with self.assertRaises(error.InvalidProgram): - logic.check_program(program, []) - - # invalid opcode - program = b"\x01\x20\x01\x01\x81" - with self.assertRaises(error.InvalidProgram): - logic.check_program(program, []) - - # check single keccak256 and 10x keccak256 work - program = b"\x01\x26\x01\x01\x01\x01\x28\x02" # byte 0x01 + keccak256 - self.assertTrue(logic.check_program(program, [])) - - program += b"\x02" * 10 - self.assertTrue(logic.check_program(program, None)) - - # check 800x keccak256 fail for v3 and below - versions = [b"\x01", b"\x02", b"\x03"] - program += b"\x02" * 800 - for v in versions: - programv = v + program - with self.assertRaises(error.InvalidProgram): - logic.check_program(programv, []) - - versions = [b"\x04"] - for v in versions: - programv = v + program - self.assertTrue(logic.check_program(programv, None)) - - def test_check_program_avm_2(self): - # check AVM v2 opcodes - self.assertIsNotNone( - logic.spec, "Must be called after any of logic.check_program" - ) - self.assertTrue(logic.spec["EvalMaxVersion"] >= 2) - self.assertTrue(logic.spec["LogicSigVersion"] >= 2) - - # balance - program = b"\x02\x20\x01\x00\x22\x60" # int 0; balance - self.assertTrue(logic.check_program(program, None)) - - # app_opted_in - program = b"\x02\x20\x01\x00\x22\x22\x61" # int 0; int 0; app_opted_in - self.assertTrue(logic.check_program(program, None)) - - # asset_holding_get - program = b"\x02\x20\x01\x00\x22\x22\x70\x00" # int 0; int 0; asset_holding_get Balance - self.assertTrue(logic.check_program(program, None)) - - def test_check_program_avm_3(self): - # check AVM v2 opcodes - self.assertIsNotNone( - logic.spec, "Must be called after any of logic.check_program" - ) - self.assertTrue(logic.spec["EvalMaxVersion"] >= 3) - self.assertTrue(logic.spec["LogicSigVersion"] >= 3) - - # min_balance - program = b"\x03\x20\x01\x00\x22\x78" # int 0; min_balance - self.assertTrue(logic.check_program(program, None)) - - # pushbytes - program = b"\x03\x20\x01\x00\x22\x80\x02\x68\x69\x48" # int 0; pushbytes "hi"; pop - self.assertTrue(logic.check_program(program, None)) - - # pushint - program = b"\x03\x20\x01\x00\x22\x81\x01\x48" # int 0; pushint 1; pop - self.assertTrue(logic.check_program(program, None)) - - # swap - program = ( - b"\x03\x20\x02\x00\x01\x22\x23\x4c\x48" # int 0; int 1; swap; pop - ) - self.assertTrue(logic.check_program(program, None)) - def test_teal_sign(self): """test tealsign""" data = base64.b64decode("Ux8jntyBJQarjKGF8A==") @@ -696,90 +562,6 @@ def test_teal_sign(self): res = verify_key.verify(msg, sig1) self.assertIsNotNone(res) - def test_check_program_avm_4(self): - # check AVM v4 opcodes - self.assertIsNotNone( - logic.spec, "Must be called after any of logic.check_program" - ) - self.assertTrue(logic.spec["EvalMaxVersion"] >= 4) - - # divmodw - program = b"\x04\x20\x03\x01\x00\x02\x22\x81\xd0\x0f\x23\x24\x1f" # int 1; pushint 2000; int 0; int 2; divmodw - self.assertTrue(logic.check_program(program, None)) - - # gloads i - program = b"\x04\x20\x01\x00\x22\x3b\x00" # int 0; gloads 0 - self.assertTrue(logic.check_program(program, None)) - - # callsub - program = b"\x04\x20\x02\x01\x02\x22\x88\x00\x02\x23\x12\x49" # int 1; callsub double; int 2; ==; double: dup; - self.assertTrue(logic.check_program(program, None)) - - # b>= - program = b"\x04\x26\x02\x01\x11\x01\x10\x28\x29\xa7" # byte 0x11; byte 0x10; b>= - self.assertTrue(logic.check_program(program, None)) - - # b^ - program = b"\x04\x26\x03\x01\x11\x01\x10\x01\x01\x28\x29\xad\x2a\x12" # byte 0x11; byte 0x10; b>= - self.assertTrue(logic.check_program(program, None)) - - # callsub, retsub - program = b"\x04\x20\x02\x01\x02\x22\x88\x00\x03\x23\x12\x43\x49\x08\x89" # int 1; callsub double; int 2; ==; return; double: dup; +; retsub; - self.assertTrue(logic.check_program(program, None)) - - # loop - program = b"\x04\x20\x04\x01\x02\x0a\x10\x22\x23\x0b\x49\x24\x0c\x40\xff\xf8\x25\x12" # int 1; loop: int 2; *; dup; int 10; <; bnz loop; int 16; == - self.assertTrue(logic.check_program(program, None)) - - def test_check_program_avm_5(self): - # check AVM v5 opcodes - self.assertIsNotNone( - logic.spec, "Must be called after any of logic.check_program" - ) - self.assertTrue(logic.spec["EvalMaxVersion"] >= 5) - - # itxn ops - program = b"\x05\x20\x01\xc0\x84\x3d\xb1\x81\x01\xb2\x10\x22\xb2\x08\x31\x00\xb2\x07\xb3\xb4\x08\x22\x12" - # itxn_begin; int pay; itxn_field TypeEnum; int 1000000; itxn_field Amount; txn Sender; itxn_field Receiver; itxn_submit; itxn Amount; int 1000000; == - self.assertTrue(logic.check_program(program, None)) - - # ECDSA ops - program = bytes.fromhex( - "058008746573746461746103802079bfa8245aeac0e714b7bd2b3252d03979e5e7a43cb039715a5f8109a7dd9ba180200753d317e54350d1d102289afbde3002add4529f10b9f7d3d223843985de62e0802103abfb5e6e331fb871e423f354e2bd78a384ef7cb07ac8bbf27d2dd1eca00e73c106000500" - ) - # byte "testdata"; sha512_256; byte 0x79bfa8245aeac0e714b7bd2b3252d03979e5e7a43cb039715a5f8109a7dd9ba1; byte 0x0753d317e54350d1d102289afbde3002add4529f10b9f7d3d223843985de62e0; byte 0x03abfb5e6e331fb871e423f354e2bd78a384ef7cb07ac8bbf27d2dd1eca00e73c1; ecdsa_pk_decompress Secp256k1; ecdsa_verify Secp256k1 - self.assertTrue(logic.check_program(program, None)) - - # cover, uncover, log - program = b"\x05\x80\x01\x61\x80\x01\x62\x80\x01\x63\x4e\x02\x4f\x02\x50\x50\xb0\x81\x01" - # byte "a"; byte "b"; byte "c"; cover 2; uncover 2; concat; concat; log; int 1 - self.assertTrue(logic.check_program(program, None)) - - def test_check_program_avm_6(self): - # check AVM v6 opcodes - - self.assertIsNotNone( - logic.spec, "Must be called after any of logic.check_program" - ) - self.assertTrue(logic.spec["EvalMaxVersion"] >= 6) - - # bsqrt - program = b"\x06\x80\x01\x90\x96\x80\x01\x0c\xa8" - # byte 0x90; bsqrt; byte 0x0c; b== - self.assertTrue(logic.check_program(program, None)) - - # divw - program = b"\x06\x81\x09\x81\xec\xff\xff\xff\xff\xff\xff\xff\xff\x01\x81\x0a\x97\x81\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01\x12" - # int 9; int 18446744073709551596; int 10; divw; int 18446744073709551614; == - self.assertTrue(logic.check_program(program, None)) - - # txn fields - program = ( - b"\x06\x31\x3f\x15\x81\x40\x12\x33\x00\x3e\x15\x81\x0a\x12\x10" - ) - # txn StateProofPK; len; int 64; ==; gtxn 0 LastLog; len; int 10; ==; && - self.assertTrue(logic.check_program(program, None)) - class TestEncoding(unittest.TestCase): """ diff --git a/tests/unit_tests/test_transaction.py b/tests/unit_tests/test_transaction.py index f6537de7..d1e7b12e 100644 --- a/tests/unit_tests/test_transaction.py +++ b/tests/unit_tests/test_transaction.py @@ -4,8 +4,15 @@ import unittest import uuid -from algosdk import account, constants, encoding, error, logic, mnemonic -from algosdk.future import transaction +from algosdk import ( + account, + constants, + encoding, + error, + logic, + mnemonic, + transaction, +) class TestPaymentTransaction(unittest.TestCase): @@ -192,7 +199,7 @@ def test_sign(self): def test_sign_logic_multisig(self): program = b"\x01\x20\x01\x01\x22" - lsig = transaction.LogicSig(program) + lsig_account = transaction.LogicSigAccount(program) passphrase = "sight garment riot tattoo tortoise identify left talk sea ill walnut leg robot myth toe perfect rifle dizzy spend april build legend brother above hospital" sk = mnemonic.to_private_key(passphrase) addr = account.address_from_private_key(sk) @@ -202,8 +209,8 @@ def test_sign_logic_multisig(self): addr2 = account.address_from_private_key(sk2) msig = transaction.Multisig(1, 2, [addr, addr2]) - lsig.sign(sk, msig) - lsig.append_to_multisig(sk2) + lsig_account.sign_multisig(msig, sk) + lsig_account.append_to_multisig(sk2) receiver = "DOMUC6VGZH7SSY5V332JR5HRLZSOJDWNPBI4OI2IIBU6A3PFLOBOXZ3KFY" gh = "zNQES/4IqimxRif40xYvzBBIYCZSbYvNSRIzVIh4swo=" @@ -212,7 +219,7 @@ def test_sign_logic_multisig(self): 0, 447, 1447, gh, gen="network-v1" ) txn = transaction.PaymentTxn(msig.address(), params, receiver, 1000000) - lstx = transaction.LogicSigTransaction(txn, lsig) + lstx = transaction.LogicSigTransaction(txn, lsig_account) golden = ( "gqRsc2lngqFsxAUBIAEBIqRtc2lng6ZzdWJzaWeSgqJwa8QgeUdQSBmJmLH5xdID" @@ -451,7 +458,7 @@ def test_serialize_keyregonlinetxn(self): "asure need above hundred" ) sk = mnemonic.to_private_key(mn) - pk = mnemonic.to_public_key(mn) + pk = account.address_from_private_key(sk) fee = 1000 gh = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" votepk = "Kv7QI7chi1y6axoy+t7wzAVpePqRq/rkjzWh/RMYyLo=" @@ -493,7 +500,7 @@ def test_serialize_write_read_keyregonlinetxn(self): "asure need above hundred" ) sk = mnemonic.to_private_key(mn) - pk = mnemonic.to_public_key(mn) + pk = account.address_from_private_key(sk) fee = 1000 gh = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" votepk = "Kv7QI7chi1y6axoy+t7wzAVpePqRq/rkjzWh/RMYyLo=" @@ -530,7 +537,8 @@ def test_init_keyregonlinetxn_with_none_values(self): "asure need above hundred" ) sk = mnemonic.to_private_key(mn) - pk = mnemonic.to_public_key(mn) + pk = account.address_from_private_key(sk) + fee = 1000 gh = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" votepk = "Kv7QI7chi1y6axoy+t7wzAVpePqRq/rkjzWh/RMYyLo=" @@ -563,7 +571,7 @@ def test_serialize_keyregofflinetxn(self): "measure need above hundred" ) sk = mnemonic.to_private_key(mn) - pk = mnemonic.to_public_key(mn) + pk = account.address_from_private_key(sk) fee = 1000 gh = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" @@ -610,7 +618,7 @@ def test_serialize_keyregnonparttxn(self): "measure need above hundred" ) sk = mnemonic.to_private_key(mn) - pk = mnemonic.to_public_key(mn) + pk = account.address_from_private_key(sk) fee = 1000 gh = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="