diff --git a/.github/workflows/pr-type-category.yml b/.github/workflows/pr-type-category.yml index 0ae6369c..c33fce94 100644 --- a/.github/workflows/pr-type-category.yml +++ b/.github/workflows/pr-type-category.yml @@ -17,8 +17,10 @@ jobs: labels: "New Feature, Enhancement, Bug-Fix, Not-Yet-Enabled, Skip-Release-Notes" - name: "Checking for PR Category in PR title. Should be like ': '." + env: + PR_TITLE: ${{ github.event.pull_request.title }} run: | - if [[ ! "${{ github.event.pull_request.title }}" =~ ^.{2,}\:.{2,} ]]; then + if [[ ! "$PR_TITLE" =~ ^.{2,}\:.{2,} ]]; then echo "## PR Category is missing from PR title. Please add it like ': '." >> GITHUB_STEP_SUMMARY exit 1 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 2acca582..9c8ba1d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +# v2.2.0 + +## What's Changed +Supports new devmode block timestamp offset endpoints. +### Bugfixes +* Fix: improve SignedTransaction type signature for dryrun and send_transaction by @barnjamin in https://github.com/algorand/py-algorand-sdk/pull/457 +* Fix: add auth addr for multisig sign when the msig has been rekeyed by @barnjamin in https://github.com/algorand/py-algorand-sdk/pull/460 +### New Features +* Simulation: Lift log limits option in SimulateRequest by @ahangsu in https://github.com/algorand/py-algorand-sdk/pull/469 +### Enhancements +* Docs: Examples by @barnjamin in https://github.com/algorand/py-algorand-sdk/pull/454 +* BugFix: ATC error message improvement by @barnjamin in https://github.com/algorand/py-algorand-sdk/pull/463 +* API: Support updated simulate endpoint by @jasonpaulos in https://github.com/algorand/py-algorand-sdk/pull/466 +* algod: Add endpoints for devmode timestamps, sync, and ready by @algochoi in https://github.com/algorand/py-algorand-sdk/pull/468 +* DevOps: Add CODEOWNERS to restrict workflow editing by @onetechnical in https://github.com/algorand/py-algorand-sdk/pull/473 + + +**Full Changelog**: https://github.com/algorand/py-algorand-sdk/compare/v2.1.2...v2.2.0 + # v2.1.2 ## What's Changed diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..aa26c82a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +.github/ @algorand/dev +.circleci/ @algorand/dev diff --git a/Dockerfile b/Dockerfile index f8fae3d7..1e7a42fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ RUN pip install . -q \ && pip install -r requirements.txt -q # Run integration tests -CMD ["/bin/bash", "-c", "python --version && make unit && make integration"] +CMD ["/bin/bash", "-c", "python --version && make unit && make integration && make smoke-test-examples"] \ No newline at end of file diff --git a/Makefile b/Makefile index 490588e4..2080fd77 100644 --- a/Makefile +++ b/Makefile @@ -45,4 +45,9 @@ docker-pysdk-run: docker ps -a docker run -it --network host py-sdk-testing:latest +# todo replace with ports from harness .env file +smoke-test-examples: + cd examples && bash smoke_test.sh && cd - + + docker-test: harness docker-pysdk-build docker-pysdk-run diff --git a/_examples/indexer.py b/_examples/indexer.py deleted file mode 100644 index 81f5df0b..00000000 --- a/_examples/indexer.py +++ /dev/null @@ -1,55 +0,0 @@ -import json -from algosdk.v2client import indexer - - -# example: CREATE_INDEXER_CLIENT -# instantiate indexer client -indexer_host = "http://localhost:8980" -indexer_token = "a" * 64 -myindexer = indexer.IndexerClient( - indexer_token=indexer_token, indexer_address=indexer_host -) -# example: CREATE_INDEXER_CLIENT - -# example: INDEXER_LOOKUP_ASSET -# lookup a single asset -asset_id = 2044572 -# by passing include_all, we specify that we want to see deleted assets as well -response = myindexer.asset_info(asset_id, include_all=True) -print(f"Asset Info: {json.dumps(response, indent=2,)}") -# example: INDEXER_LOOKUP_ASSET - -# example: INDEXER_SEARCH_MIN_AMOUNT -response = myindexer.search_transactions( - min_amount=10, min_round=1000, max_round=1500 -) -print(f"Transaction results: {json.dumps(response, indent=2)}") -# example: INDEXER_SEARCH_MIN_AMOUNT - -# example: INDEXER_PAGINATE_RESULTS - -nexttoken = "" -has_results = True -page = 0 - -# loop using next_page to paginate until there are -# no more transactions in the response -while has_results: - response = myindexer.search_transactions( - min_amount=10, min_round=1000, max_round=1500 - ) - - has_results = len(response["transactions"]) > 0 - - if has_results: - nexttoken = response["next-token"] - print(f"Tranastion on page {page}: " + json.dumps(response, indent=2)) - - page += 1 -# example: INDEXER_PAGINATE_RESULTS - -# example: INDEXER_PREFIX_SEARCH -note_prefix = "showing prefix".encode() -response = myindexer.search_transactions(note_prefix=note_prefix) -print(f"result: {json.dumps(response, indent=2)}") -# example: INDEXER_PREFIX_SEARCH diff --git a/_examples/utils.py b/_examples/utils.py deleted file mode 100644 index b15f4427..00000000 --- a/_examples/utils.py +++ /dev/null @@ -1,92 +0,0 @@ -from dataclasses import dataclass - -from algosdk.v2client import algod -from algosdk.atomic_transaction_composer import AccountTransactionSigner -from algosdk.kmd import KMDClient -from algosdk.wallet import Wallet - -DEFAULT_KMD_ADDRESS = "http://localhost:4002" -DEFAULT_KMD_TOKEN = "a" * 64 -DEFAULT_KMD_WALLET_NAME = "unencrypted-default-wallet" -DEFAULT_KMD_WALLET_PASSWORD = "" - -DEFAULT_ALGOD_ADDRESS = "http://localhost:4001" -DEFAULT_ALGOD_TOKEN = "a" * 64 - - -def get_algod_client( - addr: str = DEFAULT_ALGOD_ADDRESS, token: str = DEFAULT_ALGOD_TOKEN -) -> algod.AlgodClient: - return algod.AlgodClient(algod_token=token, algod_address=addr) - - -def get_kmd_client() -> KMDClient: - """creates a new kmd client using the default sandbox parameters""" - return KMDClient( - kmd_token=DEFAULT_KMD_TOKEN, kmd_address=DEFAULT_KMD_ADDRESS - ) - - -def get_sandbox_default_wallet() -> Wallet: - """returns the default sandbox kmd wallet""" - return Wallet( - wallet_name=DEFAULT_KMD_WALLET_NAME, - wallet_pswd=DEFAULT_KMD_WALLET_PASSWORD, - kmd_client=get_kmd_client(), - ) - - -@dataclass -class SandboxAccount: - """SandboxAccount is a simple dataclass to hold a sandbox account details""" - - #: The address of a sandbox account - address: str - #: The base64 encoded private key of the account - private_key: str - #: An AccountTransactionSigner that can be used as a TransactionSigner - signer: AccountTransactionSigner - - -def get_accounts( - kmd_address: str = DEFAULT_KMD_ADDRESS, - kmd_token: str = DEFAULT_KMD_TOKEN, - wallet_name: str = DEFAULT_KMD_WALLET_NAME, - wallet_password: str = DEFAULT_KMD_WALLET_PASSWORD, -) -> list[SandboxAccount]: - """gets all the accounts in the sandbox kmd, defaults - to the `unencrypted-default-wallet` created on private networks automatically - """ - - kmd = KMDClient(kmd_token, kmd_address) - wallets = kmd.list_wallets() - - wallet_id = None - for wallet in wallets: - if wallet["name"] == wallet_name: - wallet_id = wallet["id"] - break - - if wallet_id is None: - raise Exception("Wallet not found: {}".format(wallet_name)) - - wallet_handle = kmd.init_wallet_handle(wallet_id, wallet_password) - - try: - addresses = kmd.list_keys(wallet_handle) - private_keys = [ - kmd.export_key(wallet_handle, wallet_password, addr) - for addr in addresses - ] - kmd_accounts = [ - SandboxAccount( - addresses[i], - private_keys[i], - AccountTransactionSigner(private_keys[i]), - ) - for i in range(len(private_keys)) - ] - finally: - kmd.release_wallet_handle(wallet_handle) - - return kmd_accounts diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index cdf40fe9..66291934 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -16,7 +16,8 @@ from algosdk import abi, error, transaction from algosdk.transaction import GenericSignedTransaction from algosdk.abi.address_type import AddressType -from algosdk.v2client import algod +from algosdk.v2client import algod, models + # The first four bytes of an ABI method call return must have this hash ABI_RETURN_HASH = b"\x15\x1f\x7c\x75" @@ -255,7 +256,6 @@ def __init__( decode_error: Optional[Exception], tx_info: dict, method: abi.Method, - missing_signature: bool, ) -> None: self.tx_id = tx_id self.raw_value = raw_value @@ -263,27 +263,60 @@ def __init__( self.decode_error = decode_error self.tx_info = tx_info self.method = method - self.missing_signature = missing_signature + + +class SimulateEvalOverrides: + def __init__( + self, + *, + max_log_calls: Optional[int] = None, + max_log_size: Optional[int] = None, + allow_empty_signatures: Optional[bool] = None, + ) -> None: + self.max_log_calls = max_log_calls + self.max_log_size = max_log_size + self.allow_empty_signatures = allow_empty_signatures + + @staticmethod + def from_simulation_result( + simulation_result: Dict[str, Any] + ) -> Optional["SimulateEvalOverrides"]: + if "eval-overrides" not in simulation_result: + return None + + eval_override_dict = simulation_result.get("eval-overrides", dict()) + eval_override = SimulateEvalOverrides() + + if "max-log-calls" in eval_override_dict: + eval_override.max_log_calls = eval_override_dict["max-log-calls"] + if "max-log-size" in eval_override_dict: + eval_override.max_log_size = eval_override_dict["max-log-size"] + if "allow-empty-signatures" in eval_override_dict: + eval_override.allow_empty_signatures = eval_override_dict[ + "allow-empty-signatures" + ] + + return eval_override 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], + eval_overrides: Optional[SimulateEvalOverrides] = None, ) -> 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 + self.eval_overrides = eval_overrides class AtomicTransactionComposer: @@ -495,15 +528,18 @@ def add_method_call( # or encode a ABI value. for i, arg in enumerate(method.args): if abi.is_abi_transaction_type(arg.type): - if not isinstance( - method_args[i], TransactionWithSigner - ) or not abi.check_abi_transaction_type( + if not isinstance(method_args[i], TransactionWithSigner): + raise error.AtomicTransactionComposerError( + "expected TransactionWithSigner as method argument, " + f"but received: {method_args[i]}" + ) + + if not abi.check_abi_transaction_type( arg.type, method_args[i].txn ): raise error.AtomicTransactionComposerError( - "expected TransactionWithSigner as method argument, but received: {}".format( - method_args[i] - ) + f"expected Transaction type {arg.type} as method argument, " + f"but received: {method_args[i].txn.type}" ) txn_list.append(method_args[i]) else: @@ -683,7 +719,9 @@ def submit(self, client: algod.AlgodClient) -> List[str]: return self.tx_ids def simulate( - self, client: algod.AlgodClient + self, + client: algod.AlgodClient, + request: Optional[models.SimulateRequest] = None, ) -> SimulateAtomicTransactionResponse: """ Send the transaction group to the `simulate` endpoint and wait for results. @@ -693,6 +731,8 @@ def simulate( Args: client (AlgodClient): Algod V2 client + request (models.SimulateRequest): SimulateRequest with options in simulation. + The request's transaction group will be overrwritten by the composer's group, only simulation related options will be used. Returns: SimulateAtomicTransactionResponse: Object with simulation results for this @@ -710,9 +750,18 @@ def simulate( "lower to simulate a group" ) + current_simulation_request = ( + request if request else models.SimulateRequest(txn_groups=list()) + ) + current_simulation_request.txn_groups = [ + models.SimulateRequestTransactionGroup(txns=self.signed_txns) + ] + simulation_result = cast( - Dict[str, Any], client.simulate_transactions(self.signed_txns) + Dict[str, Any], + client.simulate_transactions(current_simulation_request), ) + # Only take the first group in the simulate response txn_group: Dict[str, Any] = simulation_result["txn-groups"][0] @@ -751,18 +800,19 @@ def simulate( decode_error=result.decode_error, tx_info=result.tx_info, method=result.method, - missing_signature=sim_txn.get("missing-signature", False), ) ) return SimulateAtomicTransactionResponse( version=simulation_result.get("version", 0), - would_succeed=simulation_result.get("would-succeed", False), failure_message=txn_group.get("failure-message", ""), failed_at=txn_group.get("failed-at"), simulate_response=simulation_result, tx_ids=self.tx_ids, results=sim_results, + eval_overrides=SimulateEvalOverrides.from_simulation_result( + simulation_result + ), ) def execute( diff --git a/algosdk/constants.py b/algosdk/constants.py index cf3c8a21..d0466e7a 100644 --- a/algosdk/constants.py +++ b/algosdk/constants.py @@ -9,7 +9,7 @@ """str: header key for algod requests""" INDEXER_AUTH_HEADER = "X-Indexer-API-Token" """str: header key for indexer requests""" -UNVERSIONED_PATHS = ["/health", "/versions", "/metrics", "/genesis"] +UNVERSIONED_PATHS = ["/health", "/versions", "/metrics", "/genesis", "/ready"] """str[]: paths that don't use the version path prefix""" NO_AUTH: List[str] = [] """str[]: requests that don't require authentication""" diff --git a/algosdk/kmd.py b/algosdk/kmd.py index eb7e5a54..a6edfa03 100644 --- a/algosdk/kmd.py +++ b/algosdk/kmd.py @@ -433,6 +433,11 @@ def sign_multisig_transaction(self, handle, password, public_key, mtx): "public_key": public_key, "partial_multisig": partial, } + + if hasattr(mtx, "auth_addr") and mtx.auth_addr is not None: + signer = base64.b64encode(encoding.decode_address(mtx.auth_addr)) + query["signer"] = signer.decode() + result = self.kmd_request("POST", req, data=query)["multisig"] msig = encoding.msgpack_decode(result) mtx.multisig = msig diff --git a/algosdk/transaction.py b/algosdk/transaction.py index dbb350c9..80123894 100644 --- a/algosdk/transaction.py +++ b/algosdk/transaction.py @@ -3233,7 +3233,7 @@ def wait_for_confirmation( def create_dryrun( client: algod.AlgodClient, - txns: List[Union[SignedTransaction, LogicSigTransaction]], + txns: List["GenericSignedTransaction"], protocol_version=None, latest_timestamp=None, round=None, diff --git a/algosdk/v2client/algod.py b/algosdk/v2client/algod.py index 90434ee8..c07d3296 100644 --- a/algosdk/v2client/algod.py +++ b/algosdk/v2client/algod.py @@ -18,6 +18,7 @@ from urllib.request import Request, urlopen from algosdk import constants, encoding, error, transaction, util +from algosdk.v2client import models AlgodResponseType = Union[Dict[str, Any], bytes] @@ -112,6 +113,11 @@ def algod_request( try: return json.load(resp) except Exception as e: + # Some algod responses currently return a 200 OK + # but have an empty response. + # Do not return an error, and just return an empty response. + if resp.status == 200 and resp.length == 0: + return {} raise error.AlgodResponseError( "Failed to parse JSON response from algod" ) from e @@ -307,13 +313,13 @@ def status_after_block( return self.algod_request("GET", req, **kwargs) def send_transaction( - self, txn: "transaction.Transaction", **kwargs: Any + self, txn: "transaction.GenericSignedTransaction", **kwargs: Any ) -> str: """ Broadcast a signed transaction object to the network. Args: - txn (SignedTransaction or MultisigTransaction): transaction to send + txn (SignedTransaction, LogicSigTransaction, or MultisigTransaction): transaction to send request_header (dict, optional): additional header for request Returns: @@ -598,48 +604,119 @@ def get_block_hash( def simulate_transactions( self, - txns: "Iterable[transaction.GenericSignedTransaction]", + request: models.SimulateRequest, **kwargs: Any, ) -> AlgodResponseType: """ - Simulate a list of a signed transaction objects being sent to the network. + Simulate transactions being sent to the network. Args: - txns (SignedTransaction[] or MultisigTransaction[]): - transactions to send - request_header (dict, optional): additional header for request + request (models.SimulateRequest): Simulation request object + headers (dict, optional): additional header for request Returns: - Dict[str, Any]: results from simulation of transaction group + Dict[str, Any]: results from simulation of transactions """ - serialized = [] - for txn in txns: - serialized.append(base64.b64decode(encoding.msgpack_encode(txn))) - - return self.simulate_raw_transaction( - base64.b64encode(b"".join(serialized)), **kwargs + body = base64.b64decode(encoding.msgpack_encode(request)) + req = "/transactions/simulate" + headers = util.build_headers_from( + kwargs.get("headers", False), + {"Content-Type": "application/msgpack"}, ) + kwargs["headers"] = headers + return self.algod_request("POST", req, data=body, **kwargs) - def simulate_raw_transaction(self, txn, **kwargs): + def simulate_raw_transactions( + self, txns: "Sequence[transaction.GenericSignedTransaction]", **kwargs + ): """ - Simulate a transaction group + Simulate a transaction group being sent to the network. Args: - txn (str): transaction to send, encoded in base64 - request_header (dict, optional): additional header for request + txns (Sequence[transaction.GenericSignedTransaction]): transaction group to simulate + headers (dict, optional): additional header for request Returns: - Dict[str, Any]: results from simulation of transaction group + Dict[str, Any]: results from simulation of transactions """ - txn = base64.b64decode(txn) - req = "/transactions/simulate" - headers = util.build_headers_from( - kwargs.get("headers", False), - {"Content-Type": "application/x-binary"}, + request = models.SimulateRequest( + txn_groups=[ + models.SimulateRequestTransactionGroup(txns=list(txns)) + ] ) - kwargs["headers"] = headers + return self.simulate_transactions(request, **kwargs) + + def get_sync_round(self, **kwargs: Any) -> AlgodResponseType: + """ + Get the minimum sync round for the ledger. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/ledger/sync" + return self.algod_request("GET", req, **kwargs) + + def set_sync_round(self, round: int, **kwargs: Any) -> AlgodResponseType: + """ + Set the minimum sync round for the ledger. - return self.algod_request("POST", req, data=txn, **kwargs) + Args: + round (int): Sync round + + Returns: + Dict[str, Any]: Response from algod + """ + req = f"/ledger/sync/{round}" + return self.algod_request("POST", req, **kwargs) + + def unset_sync_round(self, **kwargs: Any) -> AlgodResponseType: + """ + Unset the minimum sync round for the ledger. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/ledger/sync" + return self.algod_request("DELETE", req, **kwargs) + + def ready(self, **kwargs: Any) -> AlgodResponseType: + """ + Returns OK if the node is healthy and fully caught up. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/ready" + return self.algod_request("GET", req, **kwargs) + + def get_timestamp_offset(self, **kwargs: Any) -> AlgodResponseType: + """ + Get the timestamp offset in block headers. + This feature is only available in dev mode networks. + + Returns: + Dict[str, Any]: Response from algod + """ + req = "/devmode/blocks/offset" + return self.algod_request("GET", req, **kwargs) + + def set_timestamp_offset( + self, + offset: int, + **kwargs: Any, + ) -> AlgodResponseType: + """ + Set the timestamp offset in block headers. + This feature is only available in dev mode networks. + + Args: + offset (int): Block timestamp offset + + Returns: + Dict[str, Any]: Response from algod + """ + req = f"/devmode/blocks/offset/{offset}" + return self.algod_request("POST", req, **kwargs) def _specify_round_string( diff --git a/algosdk/v2client/models/__init__.py b/algosdk/v2client/models/__init__.py index 025e7dea..c9625b4f 100644 --- a/algosdk/v2client/models/__init__.py +++ b/algosdk/v2client/models/__init__.py @@ -31,10 +31,15 @@ from algosdk.v2client.models.dryrun_source import DryrunSource from algosdk.v2client.models.teal_key_value import TealKeyValue from algosdk.v2client.models.teal_value import TealValue +from algosdk.v2client.models.simulate_request import ( + SimulateRequest, + SimulateRequestTransactionGroup, +) __all__ = [ "Account", "AccountParticipation", + "Application", "ApplicationLocalState", "ApplicationParams", "ApplicationStateSchema", @@ -45,4 +50,6 @@ "DryrunSource", "TealKeyValue", "TealValue", + "SimulateRequest", + "SimulateRequestTransactionGroup", ] diff --git a/algosdk/v2client/models/simulate_request.py b/algosdk/v2client/models/simulate_request.py new file mode 100644 index 00000000..909eb223 --- /dev/null +++ b/algosdk/v2client/models/simulate_request.py @@ -0,0 +1,42 @@ +from typing import List, Dict, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from algosdk import transaction + + +class SimulateRequestTransactionGroup(object): + txns: "List[transaction.GenericSignedTransaction]" + + def __init__( + self, *, txns: "List[transaction.GenericSignedTransaction]" + ) -> None: + self.txns = txns + + def dictify(self) -> Dict[str, Any]: + return {"txns": [txn.dictify() for txn in self.txns]} + + +class SimulateRequest(object): + txn_groups: List[SimulateRequestTransactionGroup] + allow_more_logs: bool + allow_empty_signatures: bool + + def __init__( + self, + *, + txn_groups: List[SimulateRequestTransactionGroup], + allow_more_logs: bool = False, + allow_empty_signatures: bool = False, + ) -> None: + self.txn_groups = txn_groups + self.allow_more_logs = allow_more_logs + self.allow_empty_signatures = allow_empty_signatures + + def dictify(self) -> Dict[str, Any]: + return { + "txn-groups": [ + txn_group.dictify() for txn_group in self.txn_groups + ], + "allow-more-logging": self.allow_more_logs, + "allow-empty-signatures": self.allow_empty_signatures, + } diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..a835396b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +Algorand Python SDK Examples +---------------------------- + +This directory contains examples of how to use the Algorand Python SDK. + +Assuming a sandboxed node is running locally, any example can be run with the following command: + +```sh + python3 .py +``` + diff --git a/_examples/account.py b/examples/account.py similarity index 95% rename from _examples/account.py rename to examples/account.py index 5a34a206..b5ee0b61 100644 --- a/_examples/account.py +++ b/examples/account.py @@ -37,7 +37,7 @@ algod_client = get_algod_client() sp = algod_client.suggested_params() ptxn = transaction.PaymentTxn( - account_1.address, sp, msig.address(), int(1e7) + account_1.address, sp, msig.address(), int(1e5) ).sign(account_1.private_key) txid = algod_client.send_transaction(ptxn) transaction.wait_for_confirmation(algod_client, txid, 4) @@ -45,7 +45,11 @@ # example: MULTISIG_SIGN msig_pay = transaction.PaymentTxn( - msig.address(), sp, account_1.address, int(1e5) + msig.address(), + sp, + account_1.address, + 0, + close_remainder_to=account_1.address, ) msig_txn = transaction.MultisigTransaction(msig_pay, msig) msig_txn.sign(account_2.private_key) diff --git a/examples/account_backup_example.py b/examples/account_backup_example.py deleted file mode 100644 index 978c188d..00000000 --- a/examples/account_backup_example.py +++ /dev/null @@ -1,18 +0,0 @@ -# Example: backing up an account with mnemonic - -from algosdk import account, mnemonic - -# generate an account -private_key, public_key = account.generate_account() -print("Public key:", public_key) -print("Private key:", private_key, "\n") - -# get the backup phrase -backup = mnemonic.from_private_key(private_key) -print("Account backup phrase:", backup, "\n") - -# recover the account from the backup phrase -recovered_private_key = mnemonic.to_private_key(backup) -recovered_public_key = account.address_from_private_key(recovered_private_key) -print("Recovered public key:", recovered_public_key) -print("Recovered private key:", recovered_private_key) diff --git a/_examples/application/approval.teal b/examples/application/approval.teal similarity index 100% rename from _examples/application/approval.teal rename to examples/application/approval.teal diff --git a/_examples/application/approval_refactored.teal b/examples/application/approval_refactored.teal similarity index 100% rename from _examples/application/approval_refactored.teal rename to examples/application/approval_refactored.teal diff --git a/_examples/application/clear.teal b/examples/application/clear.teal similarity index 100% rename from _examples/application/clear.teal rename to examples/application/clear.teal diff --git a/_examples/apps.py b/examples/apps.py similarity index 100% rename from _examples/apps.py rename to examples/apps.py diff --git a/_examples/asa.py b/examples/asa.py similarity index 90% rename from _examples/asa.py rename to examples/asa.py index 8faa2b7d..4d3ea603 100644 --- a/_examples/asa.py +++ b/examples/asa.py @@ -90,6 +90,7 @@ # Wait for the transaction to be confirmed results = transaction.wait_for_confirmation(algod_client, txid, 4) print(f"Result confirmed in round: {results['confirmed-round']}") +# example: ASSET_OPTIN acct_info = algod_client.account_info(acct2.address) matching_asset = [ @@ -99,7 +100,6 @@ ].pop() assert matching_asset["amount"] == 0 assert matching_asset["is-frozen"] is False -# example: ASSET_OPTIN # example: ASSET_XFER @@ -118,6 +118,7 @@ results = transaction.wait_for_confirmation(algod_client, txid, 4) print(f"Result confirmed in round: {results['confirmed-round']}") +# example: ASSET_XFER acct_info = algod_client.account_info(acct2.address) matching_asset = [ @@ -126,7 +127,6 @@ if asset["asset-id"] == created_asset ].pop() assert matching_asset["amount"] == 1 -# example: ASSET_XFER # example: ASSET_FREEZE sp = algod_client.suggested_params() @@ -144,6 +144,7 @@ results = transaction.wait_for_confirmation(algod_client, txid, 4) print(f"Result confirmed in round: {results['confirmed-round']}") +# example: ASSET_FREEZE acct_info = algod_client.account_info(acct2.address) matching_asset = [ @@ -152,7 +153,6 @@ if asset["asset-id"] == created_asset ].pop() assert matching_asset["is-frozen"] is True -# example: ASSET_FREEZE # example: ASSET_CLAWBACK sp = algod_client.suggested_params() @@ -171,6 +171,7 @@ results = transaction.wait_for_confirmation(algod_client, txid, 4) print(f"Result confirmed in round: {results['confirmed-round']}") +# example: ASSET_CLAWBACK acct_info = algod_client.account_info(acct2.address) matching_asset = [ @@ -180,7 +181,27 @@ ].pop() assert matching_asset["amount"] == 0 assert matching_asset["is-frozen"] is True -# example: ASSET_CLAWBACK + +# example: ASSET_OPT_OUT +sp = algod_client.suggested_params() +opt_out_txn = transaction.AssetTransferTxn( + sender=acct2.address, + sp=sp, + index=created_asset, + receiver=acct1.address, + # an opt out transaction sets its close_asset_to parameter + # it is always possible to close an asset to the creator + close_assets_to=acct1.address, + amt=0, +) +signed_opt_out = opt_out_txn.sign(acct2.private_key) +txid = algod_client.send_transaction(signed_opt_out) +print(f"Sent opt out transaction with txid: {txid}") + +results = transaction.wait_for_confirmation(algod_client, txid, 4) +print(f"Result confirmed in round: {results['confirmed-round']}") +# example: ASSET_OPT_OUT + # example: ASSET_DELETE sp = algod_client.suggested_params() diff --git a/examples/asset_accept_example.py b/examples/asset_accept_example.py deleted file mode 100644 index b8778792..00000000 --- a/examples/asset_accept_example.py +++ /dev/null @@ -1,23 +0,0 @@ -# Example: accepting assets - -from algosdk import account, transaction - -private_key, address = account.generate_account() - -fee_per_byte = 10 -first_valid_round = 1000 -last_valid_round = 2000 -genesis_hash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" -receiver = address # to start accepting assets, set receiver to sender -amount = 0 # to start accepting assets, set amount to 0 - -index = 1234 # identifying index of the asset - -# create the asset accept transaction -sp = transaction.SuggestedParams( - fee_per_byte, first_valid_round, last_valid_round, genesis_hash -) -txn = transaction.AssetTransferTxn(address, sp, receiver, amount, index) - -# sign the transaction -signed_txn = txn.sign(private_key) diff --git a/examples/asset_create_example.py b/examples/asset_create_example.py deleted file mode 100644 index 963ea193..00000000 --- a/examples/asset_create_example.py +++ /dev/null @@ -1,50 +0,0 @@ -# Example: creating an asset - -from algosdk import account, transaction - -# creator -private_key, address = account.generate_account() -# account that can freeze other accounts for this asset -_, freeze = account.generate_account() -# account able to update asset configuration -_, manager = account.generate_account() -# account allowed to take this asset from any other account -_, clawback = account.generate_account() -# account that holds reserves for this asset -_, reserve = account.generate_account() - -fee_per_byte = 10 -first_valid_round = 1000 -last_valid_round = 2000 -genesis_hash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" - -total = 100 # how many of this asset there will be -assetname = "assetname" -unitname = "unitname" -url = "website" -# should be a 32-byte hash -metadata = bytes("fACPO4nRgO55j1ndAK3W6Sgc4APkcyFh", "ascii") -# whether accounts should be frozen by default -default_frozen = False - -# create the asset creation transaction -sp = transaction.SuggestedParams( - fee_per_byte, first_valid_round, last_valid_round, genesis_hash -) -txn = transaction.AssetConfigTxn( - address, - sp, - total=total, - manager=manager, - reserve=reserve, - freeze=freeze, - clawback=clawback, - unit_name=unitname, - asset_name=assetname, - url=url, - metadata_hash=metadata, - default_frozen=default_frozen, -) - -# sign the transaction -signed_txn = txn.sign(private_key) diff --git a/examples/asset_destroy_example.py b/examples/asset_destroy_example.py deleted file mode 100644 index 0408fbee..00000000 --- a/examples/asset_destroy_example.py +++ /dev/null @@ -1,24 +0,0 @@ -# Example: destroying an asset - -from algosdk import account, transaction - -# this transaction must be sent from the creator's account -creator_private_key, creator_address = account.generate_account() - -fee_per_byte = 10 -first_valid_round = 1000 -last_valid_round = 2000 -genesis_hash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" - -index = 1234 # identifying index of the asset - -# create the asset destroy transaction -sp = transaction.SuggestedParams( - fee_per_byte, first_valid_round, last_valid_round, genesis_hash -) -txn = transaction.AssetConfigTxn( - creator_address, sp, index=index, strict_empty_address_check=False -) - -# sign the transaction -signed_txn = txn.sign(creator_private_key) diff --git a/examples/asset_freeze_example.py b/examples/asset_freeze_example.py deleted file mode 100644 index 4f72d754..00000000 --- a/examples/asset_freeze_example.py +++ /dev/null @@ -1,29 +0,0 @@ -# Example: freezing or unfreezing an account - -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() - -fee_per_byte = 10 -first_valid_round = 1000 -last_valid_round = 2000 -genesis_hash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" -_, freeze_target = account.generate_account() - -index = 1234 # identifying index of the asset - -# create the asset freeze transaction -sp = transaction.SuggestedParams( - fee_per_byte, first_valid_round, last_valid_round, genesis_hash -) -txn = transaction.AssetFreezeTxn( - freeze_address, - sp, - index=index, - target=freeze_target, - new_freeze_state=True, -) - -# sign the transaction -signed_txn = txn.sign(freeze_private_key) diff --git a/examples/asset_revoke_example.py b/examples/asset_revoke_example.py deleted file mode 100644 index 14a41b68..00000000 --- a/examples/asset_revoke_example.py +++ /dev/null @@ -1,27 +0,0 @@ -# Example: revoking assets - -from algosdk import account, transaction - -# this transaction must be sent by the asset's clawback manager -clawback_private_key, clawback_address = account.generate_account() - -fee_per_byte = 10 -first_valid_round = 1000 -last_valid_round = 2000 -genesis_hash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" -_, receiver = account.generate_account() # where to send the revoked assets -_, target = account.generate_account() # address to revoke assets from -amount = 100 - -index = 1234 # identifying index of the asset - -# create the asset transfer transaction -sp = transaction.SuggestedParams( - fee_per_byte, first_valid_round, last_valid_round, genesis_hash -) -txn = transaction.AssetTransferTxn( - clawback_address, sp, receiver, amount, index, revocation_target=target -) - -# sign the transaction -signed_txn = txn.sign(clawback_private_key) diff --git a/examples/asset_send_example.py b/examples/asset_send_example.py deleted file mode 100644 index 70f8ebeb..00000000 --- a/examples/asset_send_example.py +++ /dev/null @@ -1,26 +0,0 @@ -# Example: sending assets - -from algosdk import account, transaction - -sender_private_key, sender_address = account.generate_account() - -fee_per_byte = 10 -first_valid_round = 1000 -last_valid_round = 2000 -genesis_hash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" -_, close_assets_to = account.generate_account() -_, receiver = account.generate_account() -amount = 100 # amount of assets to transfer - -index = 1234 # identifying index of the asset - -# create the asset transfer transaction -sp = transaction.SuggestedParams( - fee_per_byte, first_valid_round, last_valid_round, genesis_hash -) -txn = transaction.AssetTransferTxn( - sender_address, sp, receiver, amount, index, close_assets_to -) - -# sign the transaction -signed_txn = txn.sign(sender_private_key) diff --git a/examples/asset_update_example.py b/examples/asset_update_example.py deleted file mode 100644 index 79d53866..00000000 --- a/examples/asset_update_example.py +++ /dev/null @@ -1,38 +0,0 @@ -# Example: updating asset configuration - -from algosdk import account, transaction - -# this transaction must be sent from the manager's account -manager_private_key, manager_address = account.generate_account() -# account that can freeze other accounts for this asset -_, new_freeze = account.generate_account() -# account able to update asset configuration -_, new_manager = account.generate_account() -# account allowed to take this asset from any other account -_, new_clawback = account.generate_account() -# account that holds reserves for this asset -_, new_reserve = account.generate_account() - -fee_per_byte = 10 -first_valid_round = 1000 -last_valid_round = 2000 -genesis_hash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" - -index = 1234 # identifying index of the asset - -# create the asset config transaction -sp = transaction.SuggestedParams( - fee_per_byte, first_valid_round, last_valid_round, genesis_hash -) -txn = transaction.AssetConfigTxn( - manager_address, - sp, - manager=new_manager, - reserve=new_reserve, - freeze=new_freeze, - clawback=new_clawback, - index=index, -) - -# sign the transaction -signed_txn = txn.sign(manager_private_key) diff --git a/_examples/atc.py b/examples/atc.py similarity index 65% rename from _examples/atc.py rename to examples/atc.py index 1556ddb2..d249f859 100644 --- a/_examples/atc.py +++ b/examples/atc.py @@ -1,23 +1,22 @@ -from algosdk.v2client import algod +import base64 from algosdk import transaction, abi -from utils import get_accounts +from utils import get_accounts, get_algod_client, deploy_calculator_app -# example: ATC_CREATE from algosdk.atomic_transaction_composer import ( AtomicTransactionComposer, AccountTransactionSigner, TransactionWithSigner, ) + +# example: ATC_CREATE atc = AtomicTransactionComposer() # example: ATC_CREATE accts = get_accounts() acct = accts.pop() -algod_address = "http://localhost:4001" -algod_token = "a" * 64 -algod_client = algod.AlgodClient(algod_token, algod_address) +algod_client = get_algod_client() # example: ATC_ADD_TRANSACTION addr, sk = acct.address, acct.private_key @@ -38,16 +37,15 @@ atc.add_transaction(tws) # example: ATC_ADD_TRANSACTION + +app_id = deploy_calculator_app(algod_client, acct) + # example: ATC_CONTRACT_INIT -with open("path/to/contract.json") as f: +with open("calculator/contract.json") as f: js = f.read() contract = abi.Contract.from_json(js) # example: ATC_CONTRACT_INIT - -# TODO: take it from contract object? -app_id = 123 - # example: ATC_ADD_METHOD_CALL # Simple call to the `add` method, method_args can be any type but _must_ @@ -60,21 +58,6 @@ signer, method_args=[1, 1], ) - -# This method requires a `transaction` as its second argument. -# Construct the transaction and pass it in as an argument. -# The ATC will handle adding it to the group transaction and -# setting the reference in the application arguments. -ptxn = transaction.PaymentTxn(addr, sp, addr, 10000) -txn = TransactionWithSigner(ptxn, signer) -atc.add_method_call( - app_id, - contract.get_method_by_name("txntest"), - addr, - sp, - signer, - method_args=[10000, txn, 1000], -) # example: ATC_ADD_METHOD_CALL @@ -88,7 +71,9 @@ # example: ATC_RESULTS -my_method = contract.get_method_by_name("add_member()void") +my_method = abi.Method( + name="box_ref_demo", args=[], returns=abi.Returns("void") +) # example: ATC_BOX_REF atc = AtomicTransactionComposer() atc.add_method_call( @@ -97,7 +82,6 @@ addr, sp, signer, - method_args=[1, 5], boxes=[[app_id, b"key"]], ) # example: ATC_BOX_REF diff --git a/_examples/atomic_transfers.py b/examples/atomic_transfers.py similarity index 77% rename from _examples/atomic_transfers.py rename to examples/atomic_transfers.py index 97243a58..7cefd7e1 100644 --- a/_examples/atomic_transfers.py +++ b/examples/atomic_transfers.py @@ -1,54 +1,45 @@ from typing import Dict, Any from algosdk import transaction -from algosdk.v2client import algod -from utils import get_accounts +from utils import get_accounts, get_algod_client -algod_address = "http://localhost:4001" -algod_token = "a" * 64 -algod_client = algod.AlgodClient(algod_token, algod_address) +algod_client = get_algod_client() -acct1, acct2, _ = get_accounts() + +accts = get_accounts() +acct1 = accts.pop() addr1, sk1 = acct1.address, acct1.private_key +acct2 = accts.pop() addr2, sk2 = acct2.address, acct2.private_key suggested_params = algod_client.suggested_params() # example: ATOMIC_CREATE_TXNS - # payment from account 1 to account 2 txn_1 = transaction.PaymentTxn(addr1, suggested_params, addr2, 100000) # payment from account 2 to account 1 txn_2 = transaction.PaymentTxn(addr2, suggested_params, addr1, 200000) - # example: ATOMIC_CREATE_TXNS -# example: ATOMIC_GROUP_TXNS +# example: ATOMIC_GROUP_TXNS # Assign group id to the transactions (order matters!) -txn_1, txn_2 = transaction.assign_group_id([txn_1, txn_2]) - +transaction.assign_group_id([txn_1, txn_2]) # Or, equivalently - # get group id and assign it to transactions -gid = transaction.calculate_group_id([txn_1, txn_2]) -txn_1.group = gid -txn_2.group = gid - +# gid = transaction.calculate_group_id([txn_1, txn_2]) +# txn_1.group = gid +# txn_2.group = gid # example: ATOMIC_GROUP_TXNS # example: ATOMIC_GROUP_SIGN - # sign transactions stxn_1 = txn_1.sign(sk1) stxn_2 = txn_2.sign(sk2) - # example: ATOMIC_GROUP_SIGN # example: ATOMIC_GROUP_ASSEMBLE - # combine the signed transactions into a single list signed_group = [stxn_1, stxn_2] - # example: ATOMIC_GROUP_ASSEMBLE # example: ATOMIC_GROUP_SEND @@ -61,5 +52,4 @@ algod_client, tx_id, 4 ) print(f"txID: {tx_id} confirmed in round: {result.get('confirmed-round', 0)}") - # example: ATOMIC_GROUP_SEND diff --git a/_examples/calculator/approval.teal b/examples/calculator/approval.teal similarity index 100% rename from _examples/calculator/approval.teal rename to examples/calculator/approval.teal diff --git a/_examples/calculator/clear.teal b/examples/calculator/clear.teal similarity index 100% rename from _examples/calculator/clear.teal rename to examples/calculator/clear.teal diff --git a/_examples/calculator/contract.json b/examples/calculator/contract.json similarity index 100% rename from _examples/calculator/contract.json rename to examples/calculator/contract.json diff --git a/_examples/codec.py b/examples/codec.py similarity index 69% rename from _examples/codec.py rename to examples/codec.py index d65d7470..e42fc789 100644 --- a/_examples/codec.py +++ b/examples/codec.py @@ -62,5 +62,28 @@ os.remove("signed_pay.txn") -# example: CODEC_BLOCK -# example: CODEC_BLOCK +# example: CODEC_ABI +from algosdk import abi + +# generate a codec from the string representation of the ABI type +# in this case, a tuple of two strings +codec = abi.ABIType.from_string("(string,string)") + +# encode the value to its ABI encoding with the codec +to_encode = ["hello", "world"] +encoded = codec.encode(to_encode) +print(encoded.hex()) + +# decode the value from its ABI encoding with the codec +decoded = codec.decode(encoded) +print(decoded) # prints ["hello", "world"] + +# generate a codec for a uint64 array +uint_array_codec = abi.ABIType.from_string("uint64[]") +uint_array = [1, 2, 3, 4, 5] +encoded_array = uint_array_codec.encode(uint_array) +print(encoded_array.hex()) + +decoded_array = uint_array_codec.decode(encoded_array) +print(decoded_array) # prints [1, 2, 3, 4, 5] +# example: CODEC_ABI diff --git a/examples/custom_header_example.py b/examples/custom_header_example.py deleted file mode 100644 index 6cefbe28..00000000 --- a/examples/custom_header_example.py +++ /dev/null @@ -1,46 +0,0 @@ -# Example: accesing a remote API with a custom token key. - -# In this case, the API is expecting the key "X-API-Key" instead of the -# default "X-Algo-API-Token". This is done by using a dict with our custom -# key, instead of a string, as the token. - -import tokens -from algosdk.v2client import algod - -headers = { - "X-API-Key": "#######", -} - - -def main(): - algod_client = algod.AlgodClient( - tokens.algod_token, tokens.algod_address, headers - ) - - try: - status = algod_client.status() - except Exception as e: - print("Failed to get algod status: {}".format(e)) - - if status: - print("algod last round: {}".format(status.get("lastRound"))) - print( - "algod time since last round: {}".format( - status.get("timeSinceLastRound") - ) - ) - print("algod catchup: {}".format(status.get("catchupTime"))) - print( - "algod latest version: {}".format( - status.get("lastConsensusVersion") - ) - ) - - # Retrieve latest block information - last_round = algod_client.status().get("last-round") - print("####################") - block = algod_client.block_info(last_round) - print(block) - - -main() diff --git a/_examples/debug.py b/examples/debug.py similarity index 73% rename from _examples/debug.py rename to examples/debug.py index c25e119c..46380045 100644 --- a/_examples/debug.py +++ b/examples/debug.py @@ -1,5 +1,5 @@ import base64 -from utils import get_algod_client, get_accounts +from utils import get_algod_client, get_accounts, deploy_calculator_app from algosdk import ( transaction, encoding, @@ -15,15 +15,26 @@ acct2 = accts.pop() app_id = 123 + + my_method = abi.Method( name="cool_method", args=[], returns=abi.Returns("void") ) +app_id = deploy_calculator_app(algod_client, acct1) + +with open("calculator/contract.json") as f: + js = f.read() +contract = abi.Contract.from_json(js) +my_method = contract.get_method_by_name("sub") + # example: DEBUG_DRYRUN_DUMP sp = algod_client.suggested_params() atc = atomic_transaction_composer.AtomicTransactionComposer() -atc.add_method_call(app_id, my_method, acct1.address, sp, acct1.signer) +atc.add_method_call( + app_id, my_method, acct1.address, sp, acct1.signer, method_args=[1, 2] +) txns = atc.gather_signatures() drr = transaction.create_dryrun(algod_client, txns) @@ -33,6 +44,7 @@ f.write(base64.b64decode(encoding.msgpack_encode(drr))) # example: DEBUG_DRYRUN_DUMP + # example: DEBUG_DRYRUN_SUBMIT # Create the dryrun request object dryrun_request = transaction.create_dryrun(algod_client, txns) @@ -45,3 +57,7 @@ if txn.app_call_rejected(): print(txn.app_trace()) # example: DEBUG_DRYRUN_SUBMIT + +import os + +os.remove("dryrun.msgp") diff --git a/examples/example.py b/examples/example.py deleted file mode 100644 index f6ed8a2b..00000000 --- a/examples/example.py +++ /dev/null @@ -1,125 +0,0 @@ -from algosdk import encoding -from algosdk import transaction -from algosdk import kmd -from algosdk.v2client import algod -from algosdk import account -from algosdk import mnemonic -import tokens -import json - -# create kmd and algod clients -kcl = kmd.KMDClient(tokens.kmd_token, tokens.kmd_address) -acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) - -# enter existing wallet and account info here -existing_wallet_name = input("Name of an existing wallet? ") -existing_wallet_pswd = input("Password for " + existing_wallet_name + "? ") -existing_account = input("Address of an account in the wallet? ") - -# or enter info here -# existing_wallet_name = "unencrypted-default-wallet" -# existing_wallet_pswd = "" -# existing_account = "account_address" - -# get the wallet ID -wallets = kcl.list_wallets() -existing_wallet_id = None -for w in wallets: - if w["name"] == existing_wallet_name: - existing_wallet_id = w["id"] - break - -# get a handle for the existing wallet -existing_handle = kcl.init_wallet_handle( - existing_wallet_id, existing_wallet_pswd -) -print("Got the wallet's handle: " + existing_handle) - -# new wallet to create -print("Now we'll create a new wallet.") -wallet_name = input("New wallet name? ") -wallet_pswd = input("New wallet password? ") - -# or enter wallet info here -# wallet_name = "Wallet" -# wallet_pswd = "password" - -# check if the wallet already exists -wallet_id = None -for w in wallets: - if w["name"] == wallet_name: - wallet_id = w["id"] - print("The wallet already exists, but let's just go with it!") - break - -# if it doesn't exist, create the wallet and get its ID -if not wallet_id: - wallet_id = kcl.create_wallet(wallet_name, wallet_pswd)["id"] - print("Wallet created!") - print("Wallet ID: " + wallet_id) - -# get a handle for the wallet -handle = kcl.init_wallet_handle(wallet_id, wallet_pswd) -print("Wallet handle token: " + handle + "\n") - -# generate account with account and check if it's valid -private_key_1, address_1 = account.generate_account() -print("Private key: " + private_key_1 + "\n") -print("First account: " + address_1) - -# import generated account into the wallet -kcl.import_key(handle, private_key_1) - -# generate account with kmd -address_2 = kcl.generate_key(handle, False) -print("Second account: " + address_2 + "\n") - -# get the mnemonic for address_1 -mn = mnemonic.from_private_key(private_key_1) -print("Mnemonic for the first account: " + mn + "\n") - -# get suggested parameters -sp = acl.suggested_params() - -# get last block info -block_info = acl.block_info(sp.first) -print("Block", sp.first, "info:", json.dumps(block_info, indent=2), "\n") - -# create a transaction -amount = 100000 -txn = transaction.PaymentTxn(existing_account, sp, address_1, amount) -print("Encoded transaction:", encoding.msgpack_encode(txn), "\n") - -# sign transaction with kmd -signed_with_kmd = kcl.sign_transaction( - existing_handle, existing_wallet_pswd, txn -) - -# get the private key for the existing account -private_key = kcl.export_key( - existing_handle, existing_wallet_pswd, existing_account -) - -# sign transaction offline -signed_offline = txn.sign(private_key) -print("Signature: " + signed_offline.signature + "\n") - -# check that they're the same -if signed_offline.dictify() == signed_with_kmd.dictify(): - print("Signed transactions are the same!") -else: - print("Well that's not good...") - -# send the transaction -transaction_id = acl.send_transaction(signed_with_kmd) -print("\nTransaction was sent!") -print("Transaction ID: " + transaction_id + "\n") - -# try to see the transaction in pending transactions -print("Transaction info:", acl.pending_transaction_info(transaction_id)) - -# To see the new wallet and accounts that we've created, use goal: -# $ ./goal wallet list -# $ ./goal account list - -# now write your own! diff --git a/examples/indexer.py b/examples/indexer.py new file mode 100644 index 00000000..d786607a --- /dev/null +++ b/examples/indexer.py @@ -0,0 +1,90 @@ +import json +from algosdk import transaction +from algosdk.v2client import indexer +from utils import get_accounts, get_algod_client, get_indexer_client + + +# example: INDEXER_CREATE_CLIENT +# instantiate indexer client +indexer_host = "http://localhost:8980" +indexer_token = "a" * 64 +indexer_client = indexer.IndexerClient( + indexer_token=indexer_token, indexer_address=indexer_host +) +# example: INDEXER_CREATE_CLIENT + +indexer_client = get_indexer_client() + +algod_client = get_algod_client() +acct = get_accounts().pop() + +# create an asset we can lookup +actxn = transaction.AssetCreateTxn( + acct.address, + algod_client.suggested_params(), + 100, + 0, + False, + manager=acct.address, + unit_name="example", + asset_name="example asset", +) + +txid = algod_client.send_transaction(actxn.sign(acct.private_key)) +res = transaction.wait_for_confirmation(algod_client, txid, 4) +asset_id = res["asset-index"] + +ptxn = transaction.PaymentTxn( + acct.address, algod_client.suggested_params(), acct.address, 1000 +) +transaction.wait_for_confirmation( + algod_client, algod_client.send_transaction(ptxn.sign(acct.private_key)), 4 +) + +# sleep for a couple seconds to allow indexer to catch up +import time + +time.sleep(2) + + +# example: INDEXER_LOOKUP_ASSET +# lookup a single asset +# by passing include_all, we specify that we want to see deleted assets as well +response = indexer_client.asset_info(asset_id, include_all=True) +print(f"Asset Info: {json.dumps(response, indent=2,)}") +# example: INDEXER_LOOKUP_ASSET + +# example: INDEXER_SEARCH_MIN_AMOUNT +response = indexer_client.search_transactions( + min_amount=10, min_round=1000, max_round=1500 +) +print(f"Transaction results: {json.dumps(response, indent=2)}") +# example: INDEXER_SEARCH_MIN_AMOUNT + +# example: INDEXER_PAGINATE_RESULTS + +nexttoken = "" +has_results = True +page = 0 + +# loop using next_page to paginate until there are +# no more transactions in the response +while has_results: + response = indexer_client.search_transactions( + min_amount=10, min_round=1000, max_round=1500, next_page=nexttoken + ) + + has_results = len(response["transactions"]) > 0 + + if has_results: + nexttoken = response["next-token"] + print(f"Tranastion on page {page}: " + json.dumps(response, indent=2)) + + page += 1 +# example: INDEXER_PAGINATE_RESULTS + +# example: INDEXER_PREFIX_SEARCH +note_prefix = "showing prefix".encode() +response = indexer_client.search_transactions(note_prefix=note_prefix) +print(f"result: {json.dumps(response, indent=2)}") +# example: INDEXER_PREFIX_SEARCH diff --git a/_examples/kmd.py b/examples/kmd.py similarity index 97% rename from _examples/kmd.py rename to examples/kmd.py index a4c8b142..b60f5299 100644 --- a/_examples/kmd.py +++ b/examples/kmd.py @@ -1,4 +1,5 @@ from algosdk import kmd, wallet, mnemonic, account +from utils import get_kmd_client # example: KMD_CREATE_CLIENT @@ -8,6 +9,8 @@ kmd_client = kmd.KMDClient(kmd_token=kmd_token, kmd_address=kmd_address) # example: KMD_CREATE_CLIENT +kmd_client = get_kmd_client() + def get_wallet_id_from_name(name: str): wallets = kmd_client.list_wallets() diff --git a/examples/log_sig_example.py b/examples/log_sig_example.py deleted file mode 100644 index ee0ab628..00000000 --- a/examples/log_sig_example.py +++ /dev/null @@ -1,29 +0,0 @@ -# Example: creating a LogicSig transaction signed by a program that never approves the transfer. - -import tokens -from algosdk import account, transaction -from algosdk.v2client import algod - -program = b"\x01\x20\x01\x00\x22" # int 0 -lsig = transaction.LogicSigAccount(program) -sender = lsig.address() -_, receiver = account.generate_account() - -# create an algod client -acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) - -# get suggested parameters -sp = acl.suggested_params() - -# create a transaction -amount = 10000 -txn = transaction.PaymentTxn(sender, sp, receiver, amount) - -# note: transaction is signed by logic only (no delegation) -# that means sender address must match to program hash -lstx = transaction.LogicSigTransaction(txn, lsig) -assert lstx.verify() - -# send them over network -# Logicsig will reject the transaction -acl.send_transaction(lstx) diff --git a/_examples/lsig.py b/examples/lsig.py similarity index 100% rename from _examples/lsig.py rename to examples/lsig.py diff --git a/_examples/lsig/sample_arg.teal b/examples/lsig/sample_arg.teal similarity index 100% rename from _examples/lsig/sample_arg.teal rename to examples/lsig/sample_arg.teal diff --git a/_examples/lsig/simple.teal b/examples/lsig/simple.teal similarity index 100% rename from _examples/lsig/simple.teal rename to examples/lsig/simple.teal diff --git a/examples/multisig_example.py b/examples/multisig_example.py deleted file mode 100644 index 3eb01b97..00000000 --- a/examples/multisig_example.py +++ /dev/null @@ -1,35 +0,0 @@ -# Example: manipulating multisig transactions - -import tokens - -from algosdk import account, encoding, transaction -from algosdk.v2client import algod - -# generate three accounts -private_key_1, account_1 = account.generate_account() -private_key_2, account_2 = account.generate_account() -private_key_3, account_3 = account.generate_account() - -# create a multisig account -version = 1 # multisig version -threshold = 2 # how many signatures are necessary -msig = transaction.Multisig(version, threshold, [account_1, account_2]) - -# get suggested parameters -acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) -suggested_params = acl.suggested_params() - -# create a transaction -sender = msig.address() -amount = 10000 -txn = transaction.PaymentTxn(sender, suggested_params, account_3, amount) - -# create a SignedTransaction object -mtx = transaction.MultisigTransaction(txn, msig) - -# sign the transaction -mtx.sign(private_key_1) -mtx.sign(private_key_2) - -# print encoded transaction -print("Encoded transaction:", encoding.msgpack_encode(mtx)) diff --git a/examples/notefield_example.py b/examples/notefield_example.py deleted file mode 100644 index fc73bcf7..00000000 --- a/examples/notefield_example.py +++ /dev/null @@ -1,59 +0,0 @@ -# Example: working with NoteField -# We can put things in the "note" field of a transaction; here's an example -# with an auction bid. Note that you can put any bytes you want in the "note" -# field; you don't have to use the NoteField object. - -import base64 - -import tokens - -from algosdk import account, auction, constants, encoding, transaction -from algosdk.v2client import algod - -acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) - -# generate an account -private_key, public_key = account.generate_account() - -# get suggested parameters -sp = acl.suggested_params() - -# Set other parameters -amount = 100000 -note = "Some Text".encode() -_, receiver = account.generate_account() - -# create the NoteField object -bid_currency = 100 -max_price = 15 -bid_id = 18862 -auction_key = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" -auction_id = 93559 - -bid = auction.Bid( - public_key, bid_currency, max_price, bid_id, auction_key, auction_id -) -signed_bid = bid.sign(private_key) - -notefield = auction.NoteField(signed_bid, constants.note_field_type_bid) - -# create the transaction -txn = transaction.PaymentTxn( - public_key, - sp, - receiver, - amount, - note=base64.b64decode(encoding.msgpack_encode(notefield)), -) - -# encode the transaction -encoded_txn = encoding.msgpack_encode(txn) -print("Encoded transaction:", encoded_txn, "\n") - -# if someone else were to want to access the notefield from an encoded -# transaction, they could just decode the transaction -decoded_txn = encoding.msgpack_decode(encoded_txn) -decoded_notefield = encoding.msgpack_decode(base64.b64encode(decoded_txn.note)) -print( - "Decoded notefield from encoded transaction:", decoded_notefield.dictify() -) diff --git a/_examples/overview.py b/examples/overview.py similarity index 96% rename from _examples/overview.py rename to examples/overview.py index 8a77a9a8..d3864294 100644 --- a/_examples/overview.py +++ b/examples/overview.py @@ -1,7 +1,7 @@ from typing import Dict, Any import json from base64 import b64decode -from utils import get_accounts +from utils import get_accounts, get_algod_client from algosdk import transaction from algosdk.v2client import algod @@ -21,6 +21,7 @@ ) # example: ALGOD_CREATE_CLIENT +algod_client = get_algod_client() accts = get_accounts() acct1 = accts.pop() diff --git a/_examples/participation.py b/examples/participation.py similarity index 100% rename from _examples/participation.py rename to examples/participation.py diff --git a/examples/rekey_example.py b/examples/rekey_example.py deleted file mode 100644 index dc08123a..00000000 --- a/examples/rekey_example.py +++ /dev/null @@ -1,22 +0,0 @@ -# Example: rekeying - -import tokens - -from algosdk import account, transaction -from algosdk.v2client import algod - -# this should be the current account -sender_private_key, sender = account.generate_account() -rekey_private_key, rekey_address = account.generate_account() -receiver = sender -amount = 0 - -# get suggested parameters -acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) -suggested_params = acl.suggested_params() - -# To rekey an account to a new address, add the `rekey_to` argument to creation. -# After sending this rekeying transaction, every transaction needs to be signed by the private key of the new address -rekeying_txn = transaction.PaymentTxn( - sender, suggested_params, receiver, amount, rekey_to=rekey_address -) diff --git a/examples/smoke_test.sh b/examples/smoke_test.sh new file mode 100755 index 00000000..943ff79a --- /dev/null +++ b/examples/smoke_test.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +export ALGOD_PORT="60000" +export INDEXER_PORT="59999" +export KMD_PORT="60001" + + +# Loop over all files in the directory +for file in *; do + # Check if the file ends with ".py" + if [[ $file == *.py ]]; then + # Check if the filename is not "utils.py" + if [[ $file != "utils.py" ]]; then + # Call the file using Python + python3 "$file" + # Check if the test failed + if [ $? -ne 0 ]; then + echo "Test failed, stopping script" + exit 1 + fi + fi + fi +done diff --git a/examples/tokens.py b/examples/tokens.py deleted file mode 100644 index b4dfd181..00000000 --- a/examples/tokens.py +++ /dev/null @@ -1,45 +0,0 @@ -# Examples helper file - -from os import listdir -from os.path import expanduser - -home = expanduser("~") - -# These values are initialized for the SDK sandbox harness. -# You can bring the harness up by running `make harness`. -# -# If you are using your own node installation, change these after starting the node and kmd. -# algod info is in the algod.net and algod.token files in the data directory -# kmd info is in the kmd.net and kmd.token files in the kmd directory in data -kmd_token = "a" * 64 -kmd_address = "http://localhost:59999" - -algod_token = "a" * 64 -algod_address = "http://localhost:60000" - -# you can also get tokens and addresses automatically -get_automatically = False - -# path to the data directory -data_dir_path = home + "/node/network/Node" - -if get_automatically: - if not data_dir_path[-1] == "/": - data_dir_path += "/" - for directory in listdir(data_dir_path): - if "kmd" in directory: - kmd_folder_name = directory - if not kmd_folder_name[-1] == "/": - kmd_folder_name += "/" - algod_token = open(data_dir_path + "algod.token", "r").read().strip("\n") - algod_address = "http://" + open( - data_dir_path + "algod.net", "r" - ).read().strip("\n") - kmd_token = ( - open(data_dir_path + kmd_folder_name + "kmd.token", "r") - .read() - .strip("\n") - ) - kmd_address = "http://" + open( - data_dir_path + kmd_folder_name + "kmd.net", "r" - ).read().strip("\n") diff --git a/examples/transaction_group_example.py b/examples/transaction_group_example.py deleted file mode 100644 index 8265628b..00000000 --- a/examples/transaction_group_example.py +++ /dev/null @@ -1,34 +0,0 @@ -# Example: working with transaction groups - -import tokens - -from algosdk import account, kmd, transaction -from algosdk.v2client import algod - -# generate accounts -private_key_sender, sender = account.generate_account() -private_key_receiver, receiver = account.generate_account() - -# create an algod and kmd client -acl = algod.AlgodClient(tokens.algod_token, tokens.algod_address) -kcl = kmd.KMDClient(tokens.kmd_token, tokens.kmd_address) - -# get suggested parameters -sp = acl.suggested_params() - -# create a transaction -amount = 10000 -txn1 = transaction.PaymentTxn(sender, sp, receiver, amount) -txn2 = transaction.PaymentTxn(receiver, sp, sender, amount) - -# get group id and assign it to transactions -gid = transaction.calculate_group_id([txn1, txn2]) -txn1.group = gid -txn2.group = gid - -# sign transactions -stxn1 = txn1.sign(private_key_sender) -stxn2 = txn2.sign(private_key_receiver) - -# send them over network (note that the accounts need to be funded for this to work) -acl.send_transactions([stxn1, stxn2]) diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 00000000..64c3958d --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,145 @@ +import os +import base64 +from dataclasses import dataclass +from typing import List + +from algosdk import transaction +from algosdk.v2client import algod, indexer +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.kmd import KMDClient +from algosdk.wallet import Wallet + +KMD_ADDRESS = "http://localhost" +KMD_TOKEN = "a" * 64 +KMD_PORT = os.getenv("KMD_PORT", default="4002") +KMD_URL = f"{KMD_ADDRESS}:{KMD_PORT}" + +DEFAULT_KMD_WALLET_NAME = "unencrypted-default-wallet" +DEFAULT_KMD_WALLET_PASSWORD = "" + +ALGOD_ADDRESS = "http://localhost" +ALGOD_TOKEN = "a" * 64 +ALGOD_PORT = os.getenv("ALGOD_PORT", default="4001") +ALGOD_URL = f"{ALGOD_ADDRESS}:{ALGOD_PORT}" + +INDEXER_ADDRESS = "http://localhost" +INDEXER_TOKEN = "a" * 64 +INDEXER_PORT = os.getenv("INDEXER_PORT", default="8980") +INDEXER_URL = f"{INDEXER_ADDRESS}:{INDEXER_PORT}" + + +def get_algod_client( + addr: str = ALGOD_URL, token: str = ALGOD_TOKEN +) -> algod.AlgodClient: + return algod.AlgodClient(algod_token=token, algod_address=addr) + + +def get_kmd_client(addr: str = KMD_URL, token: str = KMD_TOKEN) -> KMDClient: + """creates a new kmd client using the default sandbox parameters""" + return KMDClient(kmd_token=token, kmd_address=addr) + + +def get_indexer_client( + addr: str = INDEXER_URL, token: str = INDEXER_TOKEN +) -> indexer.IndexerClient: + """creates a new indexer client using the default sandbox parameters""" + return indexer.IndexerClient(indexer_token=token, indexer_address=addr) + + +def get_sandbox_default_wallet() -> Wallet: + """returns the default sandbox kmd wallet""" + return Wallet( + wallet_name=DEFAULT_KMD_WALLET_NAME, + wallet_pswd=DEFAULT_KMD_WALLET_PASSWORD, + kmd_client=get_kmd_client(), + ) + + +@dataclass +class SandboxAccount: + """SandboxAccount is a simple dataclass to hold a sandbox account details""" + + #: The address of a sandbox account + address: str + #: The base64 encoded private key of the account + private_key: str + #: An AccountTransactionSigner that can be used as a TransactionSigner + signer: AccountTransactionSigner + + +def get_accounts( + kmd_address: str = KMD_URL, + kmd_token: str = KMD_TOKEN, + wallet_name: str = DEFAULT_KMD_WALLET_NAME, + wallet_password: str = DEFAULT_KMD_WALLET_PASSWORD, +) -> List[SandboxAccount]: + """gets all the accounts in the sandbox kmd, defaults + to the `unencrypted-default-wallet` created on private networks automatically + """ + + kmd = KMDClient(kmd_token, kmd_address) + wallets = kmd.list_wallets() + + wallet_id = None + for wallet in wallets: + if wallet["name"] == wallet_name: + wallet_id = wallet["id"] + break + + if wallet_id is None: + raise Exception("Wallet not found: {}".format(wallet_name)) + + wallet_handle = kmd.init_wallet_handle(wallet_id, wallet_password) + + try: + addresses = kmd.list_keys(wallet_handle) + private_keys = [ + kmd.export_key(wallet_handle, wallet_password, addr) + for addr in addresses + ] + kmd_accounts = [ + SandboxAccount( + addresses[i], + private_keys[i], + AccountTransactionSigner(private_keys[i]), + ) + for i in range(len(private_keys)) + ] + finally: + kmd.release_wallet_handle(wallet_handle) + + return kmd_accounts + + +def deploy_calculator_app( + algod_client: algod.AlgodClient, acct: SandboxAccount +) -> int: + with open("calculator/approval.teal", "r") as f: + approval_program = f.read() + + with open("calculator/clear.teal", "r") as f: + clear_program = f.read() + + approval_result = algod_client.compile(approval_program) + approval_binary = base64.b64decode(approval_result["result"]) + + clear_result = algod_client.compile(clear_program) + clear_binary = base64.b64decode(clear_result["result"]) + + sp = algod_client.suggested_params() + # create the app create transaction, passing compiled programs and schema + app_create_txn = transaction.ApplicationCreateTxn( + acct.address, + sp, + transaction.OnComplete.NoOpOC, + approval_program=approval_binary, + clear_program=clear_binary, + local_schema=transaction.StateSchema(num_uints=1, num_byte_slices=1), + global_schema=transaction.StateSchema(num_uints=1, num_byte_slices=1), + ) + # sign transaction + signed_create_txn = app_create_txn.sign(acct.private_key) + txid = algod_client.send_transaction(signed_create_txn) + result = transaction.wait_for_confirmation(algod_client, txid, 4) + app_id = result["application-index"] + return app_id diff --git a/examples/wallet_backup_example.py b/examples/wallet_backup_example.py deleted file mode 100644 index 1c167f54..00000000 --- a/examples/wallet_backup_example.py +++ /dev/null @@ -1,19 +0,0 @@ -# Example: backing up a wallet with mnemonic - -import tokens -from algosdk import kmd, mnemonic -from algosdk.wallet import Wallet - -# create a kmd client -kcl = kmd.KMDClient(tokens.kmd_token, tokens.kmd_address) - -# create a wallet object -wallet = Wallet("unencrypted-default-wallet", "", kcl) - -# get the wallet's master derivation key -mdk = wallet.export_master_derivation_key() -print("Master Derivation Key:", mdk) - -# get the backup phrase -backup = mnemonic.from_master_derivation_key(mdk) -print("Wallet backup phrase:", backup) diff --git a/examples/wallet_example.py b/examples/wallet_example.py deleted file mode 100644 index 5cbe79ab..00000000 --- a/examples/wallet_example.py +++ /dev/null @@ -1,23 +0,0 @@ -# Example: using the Wallet class - -import tokens -from algosdk import kmd -from algosdk.wallet import Wallet - -# create a kmd client -kcl = kmd.KMDClient(tokens.kmd_token, tokens.kmd_address) - -# create a wallet object -wallet = Wallet("wallet_name", "wallet_password", kcl) - -# get wallet information -info = wallet.info() -print("Wallet name:", info["wallet"]["name"]) - -# create an account -address = wallet.generate_key() -print("New account:", address) - -# delete the account -delete = wallet.delete_key(address) -print("Account deleted:", delete) diff --git a/examples/wallet_recover_example.py b/examples/wallet_recover_example.py deleted file mode 100644 index edddcbc1..00000000 --- a/examples/wallet_recover_example.py +++ /dev/null @@ -1,17 +0,0 @@ -# Example: recovering a wallet using a backup phrase - -import tokens -from algosdk import kmd, mnemonic - -# get the master derivation key from the backup mnemonic -backup = "such chapter crane ugly uncover fun kitten duty culture giant skirt reunion pizza pill web monster upon dolphin aunt close marble dune kangaroo ability merit" -mdk = mnemonic.to_master_derivation_key(backup) - -# create a kmd client -kcl = kmd.KMDClient(tokens.kmd_token, tokens.kmd_address) - -# recover the wallet by passing mdk when creating a wallet -kcl.create_wallet("wallet_name", "wallet_password", master_deriv_key=mdk) - -# list wallets; you should see the new wallet here -print(kcl.list_wallets()) diff --git a/setup.py b/setup.py index f6ee6a38..424859ef 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Algorand SDK in Python", author="Algorand", author_email="pypiservice@algorand.com", - version="2.1.2", + version="2.2.0", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/algorand/py-algorand-sdk", diff --git a/tests/environment.py b/tests/environment.py index 691c3aaf..32a7ecc3 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -53,6 +53,14 @@ def do_POST(self): m = bytes(m, "ascii") self.wfile.write(m) + def do_DELETE(self): + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + m = json.dumps({"path": self.path}) + m = bytes(m, "ascii") + self.wfile.write(m) + def get_status_to_use(): f = open("tests/features/resources/mock_response_status", "r") diff --git a/tests/integration.tags b/tests/integration.tags index 479c9b62..d2c62ff4 100644 --- a/tests/integration.tags +++ b/tests/integration.tags @@ -15,3 +15,4 @@ @send @send.keyregtxn @simulate +@simulate.lift_log_limits diff --git a/tests/steps/other_v2_steps.py b/tests/steps/other_v2_steps.py index b6bd1b87..0d213911 100644 --- a/tests/steps/other_v2_steps.py +++ b/tests/steps/other_v2_steps.py @@ -31,6 +31,7 @@ ApplicationLocalState, DryrunRequest, DryrunSource, + SimulateRequest, ) from tests.steps.steps import algod_port, indexer_port from tests.steps.steps import token as daemon_token @@ -905,6 +906,11 @@ def expect_path(context, path): assert exp_query == actual_query, f"{exp_query} != {actual_query}" +@then('expect the request to be "{method}" "{path}"') +def expect_request(context, method, path): + return expect_path(context, path) + + @then('expect error string to contain "{err:MaybeString}"') def expect_error(context, err): # TODO: this should actually do the claimed action @@ -1433,17 +1439,21 @@ def get_block_hash(context, round): @when("I simulate the transaction") def simulate_transaction(context): - context.simulate_response = context.app_acl.simulate_transactions( + context.simulate_response = context.app_acl.simulate_raw_transactions( [context.stx] ) @then("the simulation should succeed without any failure message") def simulate_transaction_succeed(context): - if hasattr(context, "simulate_response"): - assert context.simulate_response["would-succeed"] is True - else: - assert context.atomic_transaction_composer_return.would_succeed is True + resp = ( + context.simulate_response + if hasattr(context, "simulate_response") + else context.atomic_transaction_composer_return.simulate_response + ) + + for group in resp["txn-groups"]: + assert "failure-message" not in group @then("I simulate the current transaction group with the composer") @@ -1457,21 +1467,46 @@ def simulate_atc(context): 'the simulation should report a failure at group "{group}", path "{path}" with message "{message}"' ) def simulate_atc_failure(context, group, path, message): - resp: SimulateAtomicTransactionResponse = ( - context.atomic_transaction_composer_return - ) + if hasattr(context, "simulate_response"): + resp = context.simulate_response + else: + resp = context.atomic_transaction_composer_return.simulate_response group_idx: int = int(group) fail_path = ",".join( - [ - str(pe) - for pe in resp.simulate_response["txn-groups"][group_idx][ - "failed-at" - ] - ] + [str(pe) for pe in resp["txn-groups"][group_idx]["failed-at"]] ) - assert resp.would_succeed is False assert fail_path == path - assert message in resp.failure_message + assert message in resp["txn-groups"][group_idx]["failure-message"] + + +@when("I make a new simulate request.") +def make_simulate_request(context): + context.simulate_request = SimulateRequest(txn_groups=[]) + + +@then("I allow more logs on that simulate request.") +def allow_more_logs_in_request(context): + context.simulate_request.allow_more_logs = True + + +@then("I simulate the transaction group with the simulate request.") +def simulate_group_with_request(context): + context.atomic_transaction_composer_return = ( + context.atomic_transaction_composer.simulate( + context.app_acl, context.simulate_request + ) + ) + + +@then("I check the simulation result has power packs allow-more-logging.") +def power_pack_simulation_should_pass(context): + assert context.atomic_transaction_composer_return.eval_overrides + assert ( + context.atomic_transaction_composer_return.eval_overrides.max_log_calls + ) + assert ( + context.atomic_transaction_composer_return.eval_overrides.max_log_size + ) @when("I prepare the transaction without signatures for simulation") @@ -1479,22 +1514,36 @@ def step_impl(context): context.stx = transaction.SignedTransaction(context.txn, None) -@then( - 'the simulation should report missing signatures at group "{group}", transactions "{path}"' -) -def check_missing_signatures(context, group, path): - if hasattr(context, "simulate_response"): - resp = context.simulate_response - else: - resp = context.atomic_transaction_composer_return.simulate_response +@when("we make a GetLedgerStateDelta call against round {round}") +def get_ledger_state_delta_call(context, round): + context.response = context.acl.get_ledger_state_delta(round) - group_idx: int = int(group) - tx_idxs: list[int] = [int(pe) for pe in path.split(",")] - assert resp["would-succeed"] is False +@when("we make a SetSyncRound call against round {round}") +def set_sync_round_call(context, round): + context.response = context.acl.set_sync_round(round) + + +@when("we make a GetSyncRound call") +def get_sync_round_call(context): + context.response = context.acl.get_sync_round() + + +@when("we make a UnsetSyncRound call") +def unset_sync_round_call(context): + context.response = context.acl.unset_sync_round() + + +@when("we make a Ready call") +def ready_call(context): + context.response = context.acl.ready() + + +@when("we make a SetBlockTimeStampOffset call against offset {offset}") +def set_block_timestamp_offset(context, offset): + context.response = context.acl.set_timestamp_offset(offset) + - for tx_idx in tx_idxs: - missing_sig = resp["txn-groups"][group_idx]["txn-results"][tx_idx][ - "missing-signature" - ] - assert missing_sig is True +@when("we make a GetBlockTimeStampOffset call") +def get_block_timestamp_offset(context): + context.response = context.acl.get_timestamp_offset() diff --git a/tests/steps/steps.py b/tests/steps/steps.py index 86d1c608..74dd579a 100644 --- a/tests/steps/steps.py +++ b/tests/steps/steps.py @@ -32,7 +32,7 @@ def parse_string(text): algod_port = 60000 kmd_port = 60001 -DEV_ACCOUNT_INITIAL_MICROALGOS: int = 10_000_000 +DEV_ACCOUNT_INITIAL_MICROALGOS: int = 100_000_000 def wait_for_algod_transaction_processing_to_complete(): diff --git a/tests/unit.tags b/tests/unit.tags index 04be53d3..b6b057ce 100644 --- a/tests/unit.tags +++ b/tests/unit.tags @@ -15,14 +15,19 @@ @unit.indexer.logs @unit.offline @unit.program_sanity_check +@unit.ready @unit.rekey @unit.responses @unit.responses.231 @unit.responses.blocksummary @unit.responses.participationupdates +@unit.responses.sync +@unit.responses.timestamp @unit.responses.unlimited_assets @unit.sourcemap +@unit.sync @unit.tealsign +@unit.timestamp @unit.transactions @unit.transactions.keyreg @unit.transactions.payment