From 945a91f38fd6df1f199166d1230fffaddc49eca9 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Mon, 13 Mar 2023 18:06:06 -0400 Subject: [PATCH] Enhancement: Upgrade black, mypy, and add type annotations to algod.py (#442) --- algosdk/abi/interface.py | 1 + algosdk/abi/method.py | 1 + algosdk/atomic_transaction_composer.py | 500 ++++++++++++------------- algosdk/dryrun_results.py | 2 - algosdk/error.py | 5 + algosdk/source_map.py | 1 - algosdk/testing/dryrun.py | 2 - algosdk/transaction.py | 24 +- algosdk/v2client/algod.py | 262 +++++++++---- algosdk/v2client/indexer.py | 4 +- requirements.txt | 4 +- scripts/generate_init.py | 1 - tests/steps/other_v2_steps.py | 6 +- tests/unit_tests/test_logicsig.py | 1 - 14 files changed, 459 insertions(+), 355 deletions(-) diff --git a/algosdk/abi/interface.py b/algosdk/abi/interface.py index aaf07655..9b595f15 100644 --- a/algosdk/abi/interface.py +++ b/algosdk/abi/interface.py @@ -3,6 +3,7 @@ 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 diff --git a/algosdk/abi/method.py b/algosdk/abi/method.py index 3bd21842..cb7a6810 100644 --- a/algosdk/abi/method.py +++ b/algosdk/abi/method.py @@ -5,6 +5,7 @@ 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 diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index b54604f0..1e13cccb 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -1,11 +1,11 @@ +from abc import ABC, abstractmethod import base64 import copy -from abc import ABC, abstractmethod from enum import IntEnum from typing import ( Any, - List, Dict, + List, Optional, Tuple, TypeVar, @@ -14,6 +14,7 @@ ) from algosdk import abi, error, transaction +from algosdk.transaction import GenericSignedTransaction from algosdk.abi.address_type import AddressType from algosdk.v2client import algod @@ -23,27 +24,6 @@ T = TypeVar("T") -class AtomicTransactionComposerStatus(IntEnum): - # BUILDING indicates that the atomic group is still under construction - BUILDING = 0 - - # BUILT indicates that the atomic group has been finalized, - # but not yet signed. - BUILT = 1 - - # SIGNED indicates that the atomic group has been finalized and signed, - # but not yet submitted to the network. - SIGNED = 2 - - # SUBMITTED indicates that the atomic group has been finalized, signed, - # and submitted to the network. - SUBMITTED = 3 - - # COMMITTED indicates that the atomic group has been finalized, signed, - # submitted, and successfully committed to a block. - COMMITTED = 4 - - def populate_foreign_array( value_to_add: T, foreign_array: List[T], zero_value: Optional[T] = None ) -> int: @@ -76,11 +56,234 @@ def populate_foreign_array( return offset + len(foreign_array) - 1 -GenericSignedTransaction = Union[ - transaction.SignedTransaction, - transaction.LogicSigTransaction, - transaction.MultisigTransaction, -] +class AtomicTransactionComposerStatus(IntEnum): + # BUILDING indicates that the atomic group is still under construction + BUILDING = 0 + + # BUILT indicates that the atomic group has been finalized, + # but not yet signed. + BUILT = 1 + + # SIGNED indicates that the atomic group has been finalized and signed, + # but not yet submitted to the network. + SIGNED = 2 + + # SUBMITTED indicates that the atomic group has been finalized, signed, + # and submitted to the network. + SUBMITTED = 3 + + # COMMITTED indicates that the atomic group has been finalized, signed, + # submitted, and successfully committed to a block. + COMMITTED = 4 + + +class TransactionSigner(ABC): + """ + Represents an object which can sign transactions from an atomic transaction group. + """ + + def __init__(self) -> None: + pass + + @abstractmethod + def sign_transactions( + self, txn_group: List[transaction.Transaction], indexes: List[int] + ) -> List[GenericSignedTransaction]: + pass + + +class AccountTransactionSigner(TransactionSigner): + """ + Represents a Transaction Signer for an account that can sign transactions from an + atomic transaction group. + + Args: + private_key (str): private key of signing account + """ + + def __init__(self, private_key: str) -> None: + super().__init__() + self.private_key = private_key + + def sign_transactions( + self, txn_group: List[transaction.Transaction], indexes: List[int] + ) -> List[GenericSignedTransaction]: + """ + Sign transactions in a transaction group given the indexes. + + Returns an array of encoded signed transactions. The length of the + array will be the same as the length of indexesToSign, and each index i in the array + corresponds to the signed transaction from txnGroup[indexesToSign[i]]. + + Args: + txn_group (list[Transaction]): atomic group of transactions + indexes (list[int]): array of indexes in the atomic transaction group that should be signed + """ + stxns = [] + for i in indexes: + stxn = txn_group[i].sign(self.private_key) + stxns.append(stxn) + return stxns + + +class LogicSigTransactionSigner(TransactionSigner): + """ + Represents a Transaction Signer for a LogicSig that can sign transactions from an + atomic transaction group. + + Args: + lsig (LogicSigAccount): LogicSig account + """ + + def __init__(self, lsig: transaction.LogicSigAccount) -> None: + super().__init__() + self.lsig = lsig + + def sign_transactions( + self, txn_group: List[transaction.Transaction], indexes: List[int] + ) -> List[GenericSignedTransaction]: + """ + Sign transactions in a transaction group given the indexes. + + Returns an array of encoded signed transactions. The length of the + array will be the same as the length of indexesToSign, and each index i in the array + corresponds to the signed transaction from txnGroup[indexesToSign[i]]. + + Args: + txn_group (list[Transaction]): atomic group of transactions + indexes (list[int]): array of indexes in the atomic transaction group that should be signed + """ + stxns: List[GenericSignedTransaction] = [] + for i in indexes: + stxn = transaction.LogicSigTransaction(txn_group[i], self.lsig) + stxns.append(stxn) + return stxns + + +class MultisigTransactionSigner(TransactionSigner): + """ + Represents a Transaction Signer for a Multisig that can sign transactions from an + atomic transaction group. + + Args: + msig (Multisig): Multisig account + sks (str): private keys of multisig + """ + + def __init__(self, msig: transaction.Multisig, sks: List[str]) -> None: + super().__init__() + self.msig = msig + self.sks = sks + + def sign_transactions( + self, txn_group: List[transaction.Transaction], indexes: List[int] + ) -> List[GenericSignedTransaction]: + """ + Sign transactions in a transaction group given the indexes. + + Returns an array of encoded signed transactions. The length of the + array will be the same as the length of indexesToSign, and each index i in the array + corresponds to the signed transaction from txnGroup[indexesToSign[i]]. + + Args: + txn_group (list[Transaction]): atomic group of transactions + indexes (list[int]): array of indexes in the atomic transaction group that should be signed + """ + stxns: List[GenericSignedTransaction] = [] + for i in indexes: + mtxn = transaction.MultisigTransaction(txn_group[i], self.msig) + for sk in self.sks: + mtxn.sign(sk) + stxns.append(mtxn) + return stxns + + +class EmptySigner(TransactionSigner): + def __init__(self) -> None: + super().__init__() + + def sign_transactions( + self, txn_group: List[transaction.Transaction], indexes: List[int] + ) -> List[GenericSignedTransaction]: + stxns: List[GenericSignedTransaction] = [] + for i in indexes: + stxns.append(transaction.SignedTransaction(txn_group[i], "")) + return stxns + + +class TransactionWithSigner: + def __init__( + self, txn: transaction.Transaction, signer: TransactionSigner + ) -> None: + self.txn = txn + self.signer = signer + + +class ABIResult: + def __init__( + self, + tx_id: str, + raw_value: bytes, + return_value: Any, + decode_error: Optional[Exception], + tx_info: dict, + method: abi.Method, + ) -> None: + self.tx_id = tx_id + self.raw_value = raw_value + self.return_value = return_value + self.decode_error = decode_error + self.tx_info = tx_info + self.method = method + + +class AtomicTransactionResponse: + def __init__( + self, confirmed_round: int, tx_ids: List[str], results: List[ABIResult] + ) -> None: + self.confirmed_round = confirmed_round + self.tx_ids = tx_ids + self.abi_results = results + + +class SimulateABIResult(ABIResult): + def __init__( + self, + tx_id: str, + raw_value: bytes, + return_value: Any, + decode_error: Optional[Exception], + tx_info: dict, + method: abi.Method, + missing_signature: bool, + ) -> None: + self.tx_id = tx_id + self.raw_value = raw_value + self.return_value = return_value + self.decode_error = decode_error + self.tx_info = tx_info + self.method = method + self.missing_signature = missing_signature + + +class SimulateAtomicTransactionResponse: + def __init__( + self, + version: int, + would_succeed: bool, + failure_message: str, + failed_at: Optional[List[int]], + simulate_response: Dict[str, Any], + tx_ids: List[str], + results: List[SimulateABIResult], + ) -> None: + self.version = version + self.would_succeed = would_succeed + self.failure_message = failure_message + self.failed_at = failed_at + self.simulate_response = simulate_response + self.tx_ids = tx_ids + self.abi_results = results class AtomicTransactionComposer: @@ -102,7 +305,9 @@ class AtomicTransactionComposer: MAX_APP_ARG_LIMIT = 16 def __init__(self) -> None: - self.status = AtomicTransactionComposerStatus.BUILDING + self.status: AtomicTransactionComposerStatus = ( + AtomicTransactionComposerStatus.BUILDING + ) self.method_dict: Dict[int, abi.Method] = {} self.txn_list: List[TransactionWithSigner] = [] self.signed_txns: List[GenericSignedTransaction] = [] @@ -135,7 +340,7 @@ def clone(self) -> "AtomicTransactionComposer": return cloned def add_transaction( - self, txn_and_signer: "TransactionWithSigner" + self, txn_and_signer: TransactionWithSigner ) -> "AtomicTransactionComposer": """ Adds a transaction to this atomic group. @@ -174,16 +379,14 @@ def add_method_call( method: abi.Method, sender: str, sp: transaction.SuggestedParams, - signer: "TransactionSigner", - method_args: Optional[ - List[Union[Any, "TransactionWithSigner"]] - ] = None, + signer: TransactionSigner, + method_args: Optional[List[Union[Any, TransactionWithSigner]]] = None, on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, 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, + extra_pages: int = 0, accounts: Optional[List[str]] = None, foreign_apps: Optional[List[int]] = None, foreign_assets: Optional[List[int]] = None, @@ -381,7 +584,7 @@ def add_method_call( self.method_dict[len(self.txn_list) - 1] = method return self - def build_group(self) -> list: + def build_group(self) -> List[TransactionWithSigner]: """ Finalize the transaction group and returns the finalized transactions with signers. The composer's status will be at least BUILT after executing this method. @@ -409,7 +612,7 @@ def build_group(self) -> list: self.status = AtomicTransactionComposerStatus.BUILT return self.txn_list - def gather_signatures(self) -> list: + def gather_signatures(self) -> List[GenericSignedTransaction]: """ Obtain signatures for each transaction in this group. If signatures have already been obtained, this method will return cached versions of the signatures. @@ -481,7 +684,7 @@ def submit(self, client: algod.AlgodClient) -> List[str]: def simulate( self, client: algod.AlgodClient - ) -> "SimulateAtomicTransactionResponse": + ) -> SimulateAtomicTransactionResponse: """ Send the transaction group to the `simulate` endpoint and wait for results. An error will be thrown if submission or execution fails. @@ -507,8 +710,8 @@ def simulate( "lower to simulate a group" ) - simulation_result: Dict[str, Any] = client.simulate_transactions( - self.signed_txns + simulation_result = cast( + Dict[str, Any], client.simulate_transactions(self.signed_txns) ) # Only take the first group in the simulate response txn_group: Dict[str, Any] = simulation_result["txn-groups"][0] @@ -546,7 +749,7 @@ def simulate( def execute( self, client: algod.AlgodClient, wait_rounds: int - ) -> "AtomicTransactionResponse": + ) -> AtomicTransactionResponse: """ Send the transaction group to the network and wait until it's committed to a block. An error will be thrown if submission or execution fails. @@ -584,7 +787,8 @@ def execute( confirmed_round = resp["confirmed-round"] tx_results = [ - client.pending_transaction_info(tx_id) for tx_id in self.tx_ids + cast(Dict[str, Any], client.pending_transaction_info(tx_id)) + for tx_id in self.tx_ids ] method_results = self.parse_response(tx_results) @@ -595,8 +799,7 @@ def execute( results=method_results, ) - def parse_response(self, txns: List[Dict[str, Any]]) -> List["ABIResult"]: - + def parse_response(self, txns: List[Dict[str, Any]]) -> List[ABIResult]: method_results = [] for i, tx_info in enumerate(txns): tx_id = self.tx_ids[i] @@ -659,212 +862,3 @@ def parse_response(self, txns: List[Dict[str, Any]]) -> List["ABIResult"]: ) return method_results - - -class TransactionSigner(ABC): - """ - Represents an object which can sign transactions from an atomic transaction group. - """ - - def __init__(self) -> None: - pass - - @abstractmethod - def sign_transactions( - self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[GenericSignedTransaction]: - pass - - -class EmptySigner(TransactionSigner): - def __init__(self) -> None: - super().__init__() - - def sign_transactions( - self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[GenericSignedTransaction]: - stxns: List[GenericSignedTransaction] = [] - for i in indexes: - stxns.append(transaction.SignedTransaction(txn_group[i], "")) - return stxns - - -class AccountTransactionSigner(TransactionSigner): - """ - Represents a Transaction Signer for an account that can sign transactions from an - atomic transaction group. - - Args: - private_key (str): private key of signing account - """ - - def __init__(self, private_key: str) -> None: - super().__init__() - self.private_key = private_key - - def sign_transactions( - self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[GenericSignedTransaction]: - """ - Sign transactions in a transaction group given the indexes. - - Returns an array of encoded signed transactions. The length of the - array will be the same as the length of indexesToSign, and each index i in the array - corresponds to the signed transaction from txnGroup[indexesToSign[i]]. - - Args: - txn_group (list[Transaction]): atomic group of transactions - indexes (list[int]): array of indexes in the atomic transaction group that should be signed - """ - stxns = [] - for i in indexes: - stxn = txn_group[i].sign(self.private_key) - stxns.append(stxn) - return stxns - - -class LogicSigTransactionSigner(TransactionSigner): - """ - Represents a Transaction Signer for a LogicSig that can sign transactions from an - atomic transaction group. - - Args: - lsig (LogicSigAccount): LogicSig account - """ - - def __init__(self, lsig: transaction.LogicSigAccount) -> None: - super().__init__() - self.lsig = lsig - - def sign_transactions( - self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[GenericSignedTransaction]: - """ - Sign transactions in a transaction group given the indexes. - - Returns an array of encoded signed transactions. The length of the - array will be the same as the length of indexesToSign, and each index i in the array - corresponds to the signed transaction from txnGroup[indexesToSign[i]]. - - Args: - txn_group (list[Transaction]): atomic group of transactions - indexes (list[int]): array of indexes in the atomic transaction group that should be signed - """ - stxns: List[GenericSignedTransaction] = [] - for i in indexes: - stxn = transaction.LogicSigTransaction(txn_group[i], self.lsig) - stxns.append(stxn) - return stxns - - -class MultisigTransactionSigner(TransactionSigner): - """ - Represents a Transaction Signer for a Multisig that can sign transactions from an - atomic transaction group. - - Args: - msig (Multisig): Multisig account - sks (str): private keys of multisig - """ - - def __init__(self, msig: transaction.Multisig, sks: List[str]) -> None: - super().__init__() - self.msig = msig - self.sks = sks - - def sign_transactions( - self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[GenericSignedTransaction]: - """ - Sign transactions in a transaction group given the indexes. - - Returns an array of encoded signed transactions. The length of the - array will be the same as the length of indexesToSign, and each index i in the array - corresponds to the signed transaction from txnGroup[indexesToSign[i]]. - - Args: - txn_group (list[Transaction]): atomic group of transactions - indexes (list[int]): array of indexes in the atomic transaction group that should be signed - """ - stxns: List[GenericSignedTransaction] = [] - for i in indexes: - mtxn = transaction.MultisigTransaction(txn_group[i], self.msig) - for sk in self.sks: - mtxn.sign(sk) - stxns.append(mtxn) - return stxns - - -class TransactionWithSigner: - def __init__( - self, txn: transaction.Transaction, signer: TransactionSigner - ) -> None: - self.txn = txn - self.signer = signer - - -class ABIResult: - def __init__( - self, - tx_id: str, - raw_value: bytes, - return_value: Any, - decode_error: Optional[Exception], - tx_info: dict, - method: abi.Method, - ) -> None: - self.tx_id = tx_id - self.raw_value = raw_value - self.return_value = return_value - self.decode_error = decode_error - self.tx_info = tx_info - self.method = method - - -class AtomicTransactionResponse: - def __init__( - self, confirmed_round: int, tx_ids: List[str], results: List[ABIResult] - ) -> None: - self.confirmed_round = confirmed_round - self.tx_ids = tx_ids - self.abi_results = results - - -class SimulateABIResult(ABIResult): - def __init__( - self, - tx_id: str, - raw_value: bytes, - return_value: Any, - decode_error: Optional[Exception], - tx_info: dict, - method: abi.Method, - missing_signature: bool, - ) -> None: - self.tx_id = tx_id - self.raw_value = raw_value - self.return_value = return_value - self.decode_error = decode_error - self.tx_info = tx_info - self.method = method - self.missing_signature = missing_signature - - -class SimulateAtomicTransactionResponse: - def __init__( - self, - version: int, - would_succeed: bool, - failure_message: str, - failed_at: Optional[List[int]], - simulate_response: Dict[str, Any], - tx_ids: List[str], - results: List[SimulateABIResult], - ) -> None: - self.version = version - self.would_succeed = would_succeed - self.failure_message = failure_message - self.failed_at = failed_at - self.simulate_response = simulate_response - self.tx_ids = tx_ids - self.abi_results = results diff --git a/algosdk/dryrun_results.py b/algosdk/dryrun_results.py index e1e7e4e3..383441e7 100644 --- a/algosdk/dryrun_results.py +++ b/algosdk/dryrun_results.py @@ -79,11 +79,9 @@ def trace( disassembly: List[str], spc: StackPrinterConfig, ) -> str: - # 16 for length of the header up to spaces lines = [["pc#", "ln#", "source", "scratch", "stack"]] for idx in range(len(dr_trace.trace)): - trace_line = dr_trace.trace[idx] src = disassembly[trace_line.line] diff --git a/algosdk/error.py b/algosdk/error.py index 84c41214..b0931977 100644 --- a/algosdk/error.py +++ b/algosdk/error.py @@ -172,6 +172,11 @@ class KMDHTTPError(Exception): pass +class AlgodRequestError(Exception): + def __init__(self, msg): + super().__init__(msg) + + class AlgodHTTPError(Exception): def __init__(self, msg, code=None): super().__init__(msg) diff --git a/algosdk/source_map.py b/algosdk/source_map.py index d72a2129..a5bac058 100644 --- a/algosdk/source_map.py +++ b/algosdk/source_map.py @@ -13,7 +13,6 @@ class SourceMap: """ def __init__(self, source_map: Dict[str, Any]): - self.version: int = source_map["version"] if self.version != 3: diff --git a/algosdk/testing/dryrun.py b/algosdk/testing/dryrun.py index bebc406a..db6c29d2 100644 --- a/algosdk/testing/dryrun.py +++ b/algosdk/testing/dryrun.py @@ -294,7 +294,6 @@ def assertGlobalStateContains( and txn_res["global-delta"] is not None and len(txn_res["global-delta"]) > 0 ): - found = Helper.find_delta_value( txn_res["global-delta"], delta_value ) @@ -363,7 +362,6 @@ def assertLocalStateContains( and txn_res["local-deltas"] is not None and len(txn_res["local-deltas"]) > 0 ): - for local_delta in txn_res["local-deltas"]: addr_found = False if local_delta["address"] == addr: diff --git a/algosdk/transaction.py b/algosdk/transaction.py index e25af8da..dbb350c9 100644 --- a/algosdk/transaction.py +++ b/algosdk/transaction.py @@ -3183,7 +3183,9 @@ def wait_for_confirmation( 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"] + algod.AlgodClient._assert_json_response(kwargs, "wait_for_confirmation") + + last_round = cast(int, cast(dict, algod_client.status())["last-round"]) current_round = last_round + 1 if wait_rounds == 0: @@ -3197,7 +3199,9 @@ def wait_for_confirmation( ) try: - tx_info = algod_client.pending_transaction_info(txid, **kwargs) + tx_info = cast( + dict, 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: @@ -3246,7 +3250,8 @@ def create_dryrun( """ # The list of info objects passed to the DryrunRequest object - app_infos, acct_infos = [], [] + app_infos: List[Union[dict, models.Application]] = [] + acct_infos = [] # The running list of things we need to fetch apps, assets, accts = [], [], [] @@ -3309,7 +3314,7 @@ def create_dryrun( # 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) + app_info = cast(dict, client.application_info(app)) # Need to pass bytes, not b64 string app_info = decode_programs(app_info) app_infos.append(app_info) @@ -3323,7 +3328,7 @@ def create_dryrun( # 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) + asset_info = cast(dict, client.asset_info(asset)) # Make sure the asset creator address is in the accounts array accts.append(asset_info["params"]["creator"]) @@ -3331,7 +3336,7 @@ def create_dryrun( # 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) + acct_info = cast(dict, client.account_info(acct)) if "created-apps" in acct_info: acct_info["created-apps"] = [ decode_programs(ca) for ca in acct_info["created-apps"] @@ -3356,3 +3361,10 @@ def decode_programs(app): app["params"]["clear-state-program"] ) return app + + +GenericSignedTransaction = Union[ + SignedTransaction, + LogicSigTransaction, + MultisigTransaction, +] diff --git a/algosdk/v2client/algod.py b/algosdk/v2client/algod.py index 15cf0403..c0ce20ea 100644 --- a/algosdk/v2client/algod.py +++ b/algosdk/v2client/algod.py @@ -1,11 +1,29 @@ import base64 import json +from typing import ( + Any, + Dict, + Final, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, + cast, +) import urllib.error from urllib import parse from urllib.request import Request, urlopen from algosdk import constants, encoding, error, transaction, util +AlgodResponseType = Union[Dict[str, Any], bytes] + +# for compatibility with urllib.parse.urlencode +ParamsType = Union[Mapping[str, Any], Sequence[Tuple[str, Any]]] + api_version_path_prefix = "/v2" @@ -24,32 +42,39 @@ class AlgodClient: headers (dict) """ - def __init__(self, algod_token, algod_address, headers=None): - self.algod_token = algod_token - self.algod_address = algod_address - self.headers = headers + def __init__( + self, + algod_token: str, + algod_address: str, + headers: Optional[Dict[str, str]] = None, + ): + self.algod_token: Final[str] = algod_token + self.algod_address: Final[str] = algod_address + self.headers: Final[Optional[Dict[str, str]]] = headers def algod_request( self, - method, - requrl, - params=None, - data=None, - headers=None, - response_format="json", - ): + method: str, + requrl: str, + params: Optional[ParamsType] = None, + data: Optional[bytes] = None, + headers: Optional[Dict[str, str]] = None, + response_format: Optional[str] = "json", + ) -> AlgodResponseType: """ 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 + params (ParamsType, optional): parameters for the request + data (bytes, optional): data in the body of the request headers (dict, optional): additional header for request + response_format (str, optional): format of the response Returns: - dict: loaded from json response body + dict loaded from json response body when response_format == "json" + otherwise returns the response body as bytes """ header = {"User-Agent": "py-algorand-sdk"} @@ -78,9 +103,9 @@ def algod_request( resp = urlopen(req) except urllib.error.HTTPError as e: code = e.code - e = e.read().decode("utf-8") + es = e.read().decode("utf-8") try: - e = json.loads(e)["message"] + e = json.loads(es)["message"] finally: raise error.AlgodHTTPError(e, code) if response_format == "json": @@ -93,7 +118,18 @@ def algod_request( else: return resp.read() - def account_info(self, address, exclude=None, **kwargs): + @classmethod + def _assert_json_response( + cls, params: Mapping[str, Any], endpoint: str = "" + ) -> None: + if params.get("response_format", "json") != "json": + raise error.AlgodRequestError( + f"Only json response is supported{ (' for ' + endpoint) if endpoint else ''}." + ) + + def account_info( + self, address: str, exclude: Optional[bool] = None, **kwargs: Any + ) -> AlgodResponseType: """ Return account information. @@ -106,7 +142,7 @@ def account_info(self, address, exclude=None, **kwargs): req = "/accounts/" + address return self.algod_request("GET", req, query, **kwargs) - def asset_info(self, asset_id, **kwargs): + def asset_info(self, asset_id: int, **kwargs: Any) -> AlgodResponseType: """ Return information about a specific asset. @@ -116,7 +152,9 @@ def asset_info(self, asset_id, **kwargs): req = "/assets/" + str(asset_id) return self.algod_request("GET", req, **kwargs) - def application_info(self, application_id, **kwargs): + def application_info( + self, application_id: int, **kwargs: Any + ) -> AlgodResponseType: """ Return information about a specific application. @@ -127,8 +165,8 @@ def application_info(self, application_id, **kwargs): return self.algod_request("GET", req, **kwargs) def application_box_by_name( - self, application_id: int, box_name: bytes, **kwargs - ): + self, application_id: int, box_name: bytes, **kwargs: Any + ) -> AlgodResponseType: """ Return the value of an application's box. @@ -144,7 +182,9 @@ def application_box_by_name( params = {"name": box_name_encoded} return self.algod_request("GET", req, params=params, **kwargs) - def application_boxes(self, application_id: int, limit: int = 0, **kwargs): + def application_boxes( + self, application_id: int, limit: int = 0, **kwargs: Any + ) -> AlgodResponseType: """ Given an application ID, return all Box names. No particular ordering is guaranteed. Request fails when client or server-side configured limits prevent returning all Box names. @@ -159,7 +199,9 @@ def application_boxes(self, application_id: int, limit: int = 0, **kwargs): params = {"max": limit} if limit else {} return self.algod_request("GET", req, params=params, **kwargs) - def account_asset_info(self, address, asset_id, **kwargs): + def account_asset_info( + self, address: str, asset_id: int, **kwargs: Any + ) -> AlgodResponseType: """ Return asset information for a specific account. @@ -167,11 +209,13 @@ def account_asset_info(self, address, asset_id, **kwargs): address (str): account public key asset_id (int): The ID of the asset to look up. """ - query = {} + query: Mapping = {} req = "/accounts/" + address + "/assets/" + str(asset_id) return self.algod_request("GET", req, query, **kwargs) - def account_application_info(self, address, application_id, **kwargs): + def account_application_info( + self, address: str, application_id: int, **kwargs: Any + ) -> AlgodResponseType: """ Return application information for a specific account. @@ -179,13 +223,17 @@ def account_application_info(self, address, application_id, **kwargs): address (str): account public key application_id (int): The ID of the application to look up. """ - query = {} + query: Mapping = {} req = "/accounts/" + address + "/applications/" + str(application_id) return self.algod_request("GET", req, query, **kwargs) def pending_transactions_by_address( - self, address, limit=0, response_format="json", **kwargs - ): + self, + address: str, + limit: int = 0, + response_format: str = "json", + **kwargs: Any, + ) -> AlgodResponseType: """ Get the list of pending transactions by address, sorted by priority, in decreasing order, truncated at the end at MAX. If MAX = 0, returns @@ -197,7 +245,7 @@ def pending_transactions_by_address( response_format (str): the format in which the response is returned: either "json" or "msgpack" """ - query = {"format": response_format} + query: Dict[str, Union[str, int]] = {"format": response_format} if limit: query["max"] = limit req = "/accounts/" + address + "/transactions/pending" @@ -207,8 +255,12 @@ def pending_transactions_by_address( return res def block_info( - self, block=None, response_format="json", round_num=None, **kwargs - ): + self, + block: Optional[int] = None, + response_format: str = "json", + round_num: Optional[int] = None, + **kwargs: Any, + ) -> AlgodResponseType: """ Get the block for the given round. @@ -219,25 +271,28 @@ def block_info( round_num (int, optional): alias for block; specify one of these """ query = {"format": response_format} - if block is None and round_num is None: - raise error.UnderspecifiedRoundError req = "/blocks/" + _specify_round_string(block, round_num) res = self.algod_request( "GET", req, query, response_format=response_format, **kwargs ) return res - def ledger_supply(self, **kwargs): + def ledger_supply(self, **kwargs: Any) -> AlgodResponseType: """Return supply details for node's ledger.""" req = "/ledger/supply" return self.algod_request("GET", req, **kwargs) - def status(self, **kwargs): + def status(self, **kwargs: Any) -> AlgodResponseType: """Return node status.""" req = "/status" return self.algod_request("GET", req, **kwargs) - def status_after_block(self, block_num=None, round_num=None, **kwargs): + def status_after_block( + self, + block_num: Optional[int] = None, + round_num: Optional[int] = None, + **kwargs: Any, + ) -> AlgodResponseType: """ Return node status immediately after blockNum. @@ -246,14 +301,14 @@ def status_after_block(self, block_num=None, round_num=None, **kwargs): 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 send_transaction(self, txn, **kwargs): + def send_transaction( + self, txn: "transaction.Transaction", **kwargs: Any + ) -> str: """ Broadcast a signed transaction object to the network. @@ -266,12 +321,14 @@ def send_transaction(self, txn, **kwargs): """ assert not isinstance( txn, transaction.Transaction - ), "Attempt to send UNSIGNED transaction {}".format(txn) + ), "Attempt to send UNSUPPORTED type of transaction {}".format(txn) return self.send_raw_transaction( encoding.msgpack_encode(txn), **kwargs ) - def send_raw_transaction(self, txn, **kwargs): + def send_raw_transaction( + self, txn: Union[bytes, str], **kwargs: Any + ) -> str: """ Broadcast a signed transaction to the network. @@ -282,7 +339,9 @@ def send_raw_transaction(self, txn, **kwargs): Returns: str: transaction ID """ - txn = base64.b64decode(txn) + self._assert_json_response(kwargs, "send_raw_transaction") + + txn_bytes = base64.b64decode(txn) req = "/transactions" headers = util.build_headers_from( kwargs.get("headers", False), @@ -290,11 +349,12 @@ def send_raw_transaction(self, txn, **kwargs): ) kwargs["headers"] = headers - return self.algod_request("POST", req, data=txn, **kwargs)["txId"] + resp = self.algod_request("POST", req, data=txn_bytes, **kwargs) + return cast(str, cast(dict, resp)["txId"]) def pending_transactions( - self, max_txns=0, response_format="json", **kwargs - ): + self, max_txns: int = 0, response_format: str = "json", **kwargs: Any + ) -> AlgodResponseType: """ Return pending transactions. @@ -304,18 +364,17 @@ def pending_transactions( response_format (str): the format in which the response is returned: either "json" or "msgpack" """ - query = {"format": response_format} + query: Dict[str, Union[int, str]] = {"format": response_format} if max_txns: query["max"] = max_txns req = "/transactions/pending" - res = self.algod_request( + return self.algod_request( "GET", req, params=query, response_format=response_format, **kwargs ) - return res def pending_transaction_info( - self, transaction_id, response_format="json", **kwargs - ): + self, transaction_id: str, response_format: str = "json", **kwargs: Any + ) -> AlgodResponseType: """ Return transaction information for a pending transaction. @@ -326,22 +385,25 @@ def pending_transaction_info( """ req = "/transactions/pending/" + transaction_id query = {"format": response_format} - res = self.algod_request( + return self.algod_request( "GET", req, params=query, response_format=response_format, **kwargs ) - return res - def health(self, **kwargs): + def health(self, **kwargs: Any) -> AlgodResponseType: """Return null if the node is running.""" req = "/health" return self.algod_request("GET", req, **kwargs) - def versions(self, **kwargs): + def versions(self, **kwargs: Any) -> AlgodResponseType: """Return algod versions.""" req = "/versions" return self.algod_request("GET", req, **kwargs) - def send_transactions(self, txns, **kwargs): + def send_transactions( + self, + txns: "Iterable[transaction.GenericSignedTransaction]", + **kwargs: Any, + ) -> str: """ Broadcast list of a signed transaction objects to the network. @@ -353,21 +415,22 @@ def send_transactions(self, txns, **kwargs): Returns: str: first transaction ID """ - serialized = [] + serialized: List[bytes] = [] for txn in txns: assert not isinstance( txn, transaction.Transaction ), "Attempt to send UNSIGNED transaction {}".format(txn) serialized.append(base64.b64decode(encoding.msgpack_encode(txn))) - return self.send_raw_transaction( base64.b64encode(b"".join(serialized)), **kwargs ) - def suggested_params(self, **kwargs): + def suggested_params(self, **kwargs: Any) -> "transaction.SuggestedParams": """Return suggested transaction parameters.""" + self._assert_json_response(kwargs, "suggested_params") + req = "/transactions/params" - res = self.algod_request("GET", req, **kwargs) + res = cast(dict, self.algod_request("GET", req, **kwargs)) return transaction.SuggestedParams( res["fee"], @@ -380,7 +443,9 @@ def suggested_params(self, **kwargs): res["min-fee"], ) - def compile(self, source, source_map=False, **kwargs): + def compile( + self, source: str, source_map: bool = False, **kwargs: Any + ) -> Dict[str, Any]: """ Compile TEAL source with remote algod. @@ -390,8 +455,9 @@ def compile(self, source, source_map=False, **kwargs): Returns: dict: loaded from json response body. "result" property contains compiled bytes, "hash" - program hash (escrow address) - """ + self._assert_json_response(kwargs, "compile") + req = "/teal/compile" headers = util.build_headers_from( kwargs.get("headers", False), @@ -399,23 +465,34 @@ def compile(self, source, source_map=False, **kwargs): ) kwargs["headers"] = headers params = {"sourcemap": source_map} - return self.algod_request( - "POST", req, params=params, data=source.encode("utf-8"), **kwargs + return cast( + Dict[str, Any], + self.algod_request( + "POST", + req, + params=params, + data=source.encode("utf-8"), + **kwargs, + ), ) - def disassemble(self, program_bytes, **kwargs): + def disassemble( + self, program_bytes: bytes, **kwargs: Any + ) -> Dict[str, str]: """ Disassable TEAL program bytes with remote algod. Args: program (bytes): bytecode to be disassembled request_header (dict, optional): additional header for request Returns: - str: disassembled TEAL source code in plain text + dict: response dictionary containing disassembled TEAL source code + in plain text as the value of the unique "result" key. """ if not isinstance(program_bytes, bytes): raise error.InvalidProgram( message=f"disassemble endpoints only accepts bytes but request program_bytes is of type {type(program_bytes)}" ) + self._assert_json_response(kwargs, "disassemble") req = "/teal/disassemble" headers = util.build_headers_from( @@ -423,9 +500,12 @@ def disassemble(self, program_bytes, **kwargs): {"Content-Type": "application/x-binary"}, ) kwargs["headers"] = headers - return self.algod_request("POST", req, data=program_bytes, **kwargs) + return cast( + Dict[str, str], + self.algod_request("POST", req, data=program_bytes, **kwargs), + ) - def dryrun(self, drr, **kwargs): + def dryrun(self, drr: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: """ Dryrun with remote algod. @@ -436,6 +516,8 @@ def dryrun(self, drr, **kwargs): Returns: dict: loaded from json response body """ + self._assert_json_response(kwargs, "dryrun") + req = "/teal/dryrun" headers = util.build_headers_from( kwargs.get("headers", False), @@ -445,16 +527,21 @@ def dryrun(self, drr, **kwargs): data = encoding.msgpack_encode(drr) data = base64.b64decode(data) - return self.algod_request("POST", req, data=data, **kwargs) + return cast(dict, self.algod_request("POST", req, data=data, **kwargs)) - def genesis(self, **kwargs): + def genesis(self, **kwargs: Any) -> AlgodResponseType: """Returns the entire genesis file.""" req = "/genesis" return self.algod_request("GET", req, **kwargs) def transaction_proof( - self, round_num, txid, hashtype="", response_format="json", **kwargs - ): + self, + round_num: int, + txid: str, + hashtype: str = "", + response_format: str = "json", + **kwargs: Any, + ) -> AlgodResponseType: """ Get a proof for a transaction in a block. @@ -475,9 +562,11 @@ def transaction_proof( **kwargs, ) - def lightblockheader_proof(self, round_num, **kwargs): + def lightblockheader_proof( + self, round_num: int, **kwargs: Any + ) -> AlgodResponseType: """ - Gets a proof for a given light block header inside a state proof commitment. + Gets a proof for a given light block header inside a state proof commitment. Args: round_num (int): The round to which the light block header belongs. @@ -485,7 +574,7 @@ def lightblockheader_proof(self, round_num, **kwargs): req = "/blocks/{}/lightheader/proof".format(round_num) return self.algod_request("GET", req, **kwargs) - def stateproofs(self, round_num, **kwargs): + def stateproofs(self, round_num: int, **kwargs: Any) -> AlgodResponseType: """ Get a state proof that covers a given round @@ -495,7 +584,9 @@ def stateproofs(self, round_num, **kwargs): req = "/stateproofs/{}".format(round_num) return self.algod_request("GET", req, **kwargs) - def get_block_hash(self, round_num, **kwargs): + def get_block_hash( + self, round_num: int, **kwargs: Any + ) -> AlgodResponseType: """ Get the block hash for the block on the given round. @@ -505,7 +596,11 @@ def get_block_hash(self, round_num, **kwargs): req = "/blocks/{}/hash".format(round_num) return self.algod_request("GET", req, **kwargs) - def simulate_transactions(self, txns, **kwargs): + def simulate_transactions( + self, + txns: "Iterable[transaction.GenericSignedTransaction]", + **kwargs: Any, + ) -> AlgodResponseType: """ Simulate a list of a signed transaction objects being sent to the network. @@ -547,7 +642,9 @@ def simulate_raw_transaction(self, txn, **kwargs): return self.algod_request("POST", req, data=txn, **kwargs) -def _specify_round_string(block, round_num): +def _specify_round_string( + block: Union[int, None], round_num: Union[int, None] +) -> str: """ Return the round number specified in either 'block' or 'round_num'. @@ -555,10 +652,13 @@ def _specify_round_string(block, round_num): block (int): user specified variable round_num (int): user specified variable """ + if block is None and round_num is None: + raise error.UnderspecifiedRoundError() if block is not None and round_num is not None: - raise error.OverspecifiedRoundError - elif block is not None: - return str(block) - elif round_num is not None: + raise error.OverspecifiedRoundError() + + if round_num is not None: return str(round_num) + + return str(block) diff --git a/algosdk/v2client/indexer.py b/algosdk/v2client/indexer.py index 630ceaac..9054fe4e 100644 --- a/algosdk/v2client/indexer.py +++ b/algosdk/v2client/indexer.py @@ -221,8 +221,6 @@ def block_info( round_num (int, optional): alias for block; specify one of these header_only (bool, optional): """ - if block is None and round_num is None: - raise error.UnderspecifiedRoundError req = "/blocks/" + _specify_round_string(block, round_num) query = dict() @@ -986,7 +984,7 @@ def _specify_round(query, block, round_num): """ if block is not None and round_num is not None: - raise error.OverspecifiedRoundError + raise error.OverspecifiedRoundError() elif block is not None: if block: query["round"] = block diff --git a/requirements.txt b/requirements.txt index c3d13e9d..803ff6a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ . -black==22.3.0 +black==23.1.0 glom==20.11.0 pytest==6.2.5 -mypy==0.990 +mypy==1.0 msgpack-types==0.2.0 git+https://github.com/behave/behave diff --git a/scripts/generate_init.py b/scripts/generate_init.py index aafc3a64..cd1b4d39 100644 --- a/scripts/generate_init.py +++ b/scripts/generate_init.py @@ -94,7 +94,6 @@ def overwrite(regen: str): if __name__ == "__main__": - parser = argparse.ArgumentParser() parser.add_argument( "--check", diff --git a/tests/steps/other_v2_steps.py b/tests/steps/other_v2_steps.py index c81224e9..b6bd1b87 100644 --- a/tests/steps/other_v2_steps.py +++ b/tests/steps/other_v2_steps.py @@ -900,8 +900,9 @@ def expect_path(context, path): context.response["path"] ) actual_query = urllib.parse.parse_qs(actual_query) - assert exp_path == actual_path.replace("%3A", ":") - assert exp_query == actual_query + actual_path = actual_path.replace("%3A", ":") + assert exp_path == actual_path, f"{exp_path} != {actual_path}" + assert exp_query == actual_query, f"{exp_query} != {actual_query}" @then('expect error string to contain "{err:MaybeString}"') @@ -1377,7 +1378,6 @@ def check_source_map(context, pc_to_line): @then('getting the line associated with a pc "{pc}" equals "{line}"') def check_pc_to_line(context, pc, line): - actual_line = context.source_map.get_line_for_pc(int(pc)) assert actual_line == int(line), f"expected line {line} got {actual_line}" diff --git a/tests/unit_tests/test_logicsig.py b/tests/unit_tests/test_logicsig.py index ef2cfac8..4bed5123 100644 --- a/tests/unit_tests/test_logicsig.py +++ b/tests/unit_tests/test_logicsig.py @@ -761,7 +761,6 @@ def test_msig_address(self): self.assertEqual(msig2.address(), golden) def test_errors(self): - # get random private key private_key_1, account_1 = account.generate_account() _, account_2 = account.generate_account()