diff --git a/examples/production_python_react/.github/workflows/production-python-react-contracts-cd.yaml b/examples/production_python_react/.github/workflows/production-python-react-contracts-cd.yaml index d846590..b9e5ed3 100644 --- a/examples/production_python_react/.github/workflows/production-python-react-contracts-cd.yaml +++ b/examples/production_python_react/.github/workflows/production-python-react-contracts-cd.yaml @@ -13,7 +13,9 @@ jobs: uses: actions/checkout@v4 - name: Install poetry - run: pipx install poetry + run: | + pipx install poetry + pipx inject poetry poetry-plugin-export - name: Set up Python 3.12 uses: actions/setup-python@v5 diff --git a/examples/production_python_react/.github/workflows/production-python-react-contracts-ci.yaml b/examples/production_python_react/.github/workflows/production-python-react-contracts-ci.yaml index 67097ff..c2e0fce 100644 --- a/examples/production_python_react/.github/workflows/production-python-react-contracts-ci.yaml +++ b/examples/production_python_react/.github/workflows/production-python-react-contracts-ci.yaml @@ -11,7 +11,9 @@ jobs: uses: actions/checkout@v4 - name: Install poetry - run: pipx install poetry + run: | + pipx install poetry + pipx inject poetry poetry-plugin-export - name: Set up Python 3.12 uses: actions/setup-python@v5 diff --git a/examples/production_python_react/projects/production_python_react-contracts/.algokit.toml b/examples/production_python_react/projects/production_python_react-contracts/.algokit.toml index 172e5dd..febb96a 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/.algokit.toml +++ b/examples/production_python_react/projects/production_python_react-contracts/.algokit.toml @@ -34,7 +34,7 @@ test = { commands = [ ], description = 'Run smart contract tests' } audit = { commands = [ 'poetry run pip-audit', -], description = 'Audit with pip-audit' } +], description = 'Audit with pip-audit. NOTE: If used with poetry >v2, make sure to install `poetry-plugin-export` as per https://github.com/python-poetry/poetry-plugin-export#installation.' } lint = { commands = [ 'poetry run black --check --diff .', 'poetry run ruff check .', diff --git a/examples/production_python_react/projects/production_python_react-contracts/.algokit/.copier-answers.yml b/examples/production_python_react/projects/production_python_react-contracts/.algokit/.copier-answers.yml index 25a6bfd..ce34938 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/.algokit/.copier-answers.yml +++ b/examples/production_python_react/projects/production_python_react-contracts/.algokit/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: 1.4.4 +_commit: 1.5.0 _src_path: gh:algorandfoundation/algokit-python-template author_email: None author_name: None diff --git a/examples/production_python_react/projects/production_python_react-contracts/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 b/examples/production_python_react/projects/production_python_react-contracts/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 index eb726b9..a2ff46d 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 +++ b/examples/production_python_react/projects/production_python_react-contracts/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 @@ -1,35 +1,44 @@ import logging import algokit_utils -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient logger = logging.getLogger(__name__) # define deployment behaviour based on supplied app spec -def deploy( - algod_client: AlgodClient, - indexer_client: IndexerClient, - app_spec: algokit_utils.ApplicationSpecification, - deployer: algokit_utils.Account, -) -> None: +def deploy() -> None: from smart_contracts.artifacts.{{ contract_name }}.{{ contract_name }}_client import ( - {{ contract_name.split('_')|map('capitalize')|join }}Client, + {{ contract_name.split('_')|map('capitalize')|join }}Factory, + HelloArgs, ) - app_client = {{ contract_name.split('_')|map('capitalize')|join }}Client( - algod_client, - creator=deployer, - indexer_client=indexer_client, + algorand = algokit_utils.AlgorandClient.from_environment() + deployer_ = algorand.account.from_environment("DEPLOYER") + + factory = algorand.client.get_typed_app_factory( + {{ contract_name.split('_')|map('capitalize')|join }}Factory, default_sender=deployer_.address ) - app_client.deploy( - on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, + + app_client, result = factory.deploy( on_update=algokit_utils.OnUpdate.AppendApp, + on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, ) + + if result.operation_performed in [ + algokit_utils.OperationPerformed.Create, + algokit_utils.OperationPerformed.Replace, + ]: + algorand.send.payment( + algokit_utils.PaymentParams( + amount=algokit_utils.AlgoAmount(algo=1), + sender=deployer_.address, + receiver=app_client.app_address, + ) + ) + name = "world" - response = app_client.hello(name=name) + response = app_client.send.hello(args=HelloArgs(name=name)) logger.info( - f"Called hello on {app_spec.contract.name} ({app_client.app_id}) " - f"with name={name}, received: {response.return_value}" + f"Called hello on {app_client.app_name} ({app_client.app_id}) " + f"with name={name}, received: {response.abi_return}" ) diff --git a/examples/production_python_react/projects/production_python_react-contracts/.vscode/settings.json b/examples/production_python_react/projects/production_python_react-contracts/.vscode/settings.json index 2130dbb..1e7e3c0 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/.vscode/settings.json +++ b/examples/production_python_react/projects/production_python_react-contracts/.vscode/settings.json @@ -13,6 +13,7 @@ }, // Python + "python.analysis.autoImportCompletions": true, "python.analysis.extraPaths": ["${workspaceFolder}/smart_contracts"], "python.analysis.diagnosticSeverityOverrides": { "reportMissingModuleSource": "none" diff --git a/examples/production_python_react/projects/production_python_react-contracts/pyproject.toml b/examples/production_python_react/projects/production_python_react-contracts/pyproject.toml index 55957d5..31add8d 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/pyproject.toml +++ b/examples/production_python_react/projects/production_python_react-contracts/pyproject.toml @@ -7,16 +7,16 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.12" -algokit-utils = "^2.4.0" +algokit-utils = "^3.0.0" python-dotenv = "^1.0.0" algorand-python = "^2.0.0" algorand-python-testing = "^0.4.0" [tool.poetry.group.dev.dependencies] -algokit-client-generator = "^1.1.3" +algokit-client-generator = "^2.0.0" black = {extras = ["d"], version = "*"} -ruff = "^0.1.6" -mypy = "1.11.0" +ruff = "^0.9.4" +mypy = "^1" pytest = "*" pytest-cov = "*" pip-audit = "*" @@ -28,14 +28,10 @@ build-backend = "poetry.core.masonry.api" [tool.ruff] line-length = 120 -select = ["E", "F", "ANN", "UP", "N", "C4", "B", "A", "YTT", "W", "FBT", "Q", "RUF", "I"] -ignore = [ - "ANN101", # no type for self - "ANN102", # no type for cls -] -unfixable = ["B", "RUF"] +lint.select = ["E", "F", "ANN", "UP", "N", "C4", "B", "A", "YTT", "W", "FBT", "Q", "RUF", "I"] +lint.unfixable = ["B", "RUF"] -[tool.ruff.flake8-annotations] +[tool.ruff.lint.flake8-annotations] allow-star-arg-any = true suppress-none-returning = true diff --git a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/__main__.py b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/__main__.py index 9c691ba..6bb9664 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/__main__.py +++ b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/__main__.py @@ -1,35 +1,173 @@ +import dataclasses +import importlib import logging +import subprocess import sys +from collections.abc import Callable from pathlib import Path +from shutil import rmtree +from algokit_utils.config import config from dotenv import load_dotenv -from smart_contracts._helpers.build import build -from smart_contracts._helpers.config import contracts -from smart_contracts._helpers.deploy import deploy - -# Uncomment the following lines to enable auto generation of AVM Debugger compliant sourcemap and simulation trace file. +# Set trace_all to True to capture all transactions, defaults to capturing traces only on failure # Learn more about using AlgoKit AVM Debugger to debug your TEAL source codes and inspect various kinds of # Algorand transactions in atomic groups -> https://github.com/algorandfoundation/algokit-avm-vscode-debugger -# from algokit_utils.config import config -# config.configure(debug=True, trace_all=True) +config.configure(debug=True, trace_all=False) + +# Set up logging and load environment variables. logging.basicConfig( level=logging.DEBUG, format="%(asctime)s %(levelname)-10s: %(message)s" ) logger = logging.getLogger(__name__) logger.info("Loading .env") -# For manual script execution (bypassing `algokit project deploy`) with a custom .env, -# modify `load_dotenv()` accordingly. For example, `load_dotenv('.env.localnet')`. load_dotenv() + +# Determine the root path based on this file's location. root_path = Path(__file__).parent +# ----------------------- Contract Configuration ----------------------- # + + +@dataclasses.dataclass +class SmartContract: + path: Path + name: str + deploy: Callable[[], None] | None = None + + +def import_contract(folder: Path) -> Path: + """Imports the contract from a folder if it exists.""" + contract_path = folder / "contract.py" + if contract_path.exists(): + return contract_path + else: + raise Exception(f"Contract not found in {folder}") + + +def import_deploy_if_exists(folder: Path) -> Callable[[], None] | None: + """Imports the deploy function from a folder if it exists.""" + try: + module_name = f"{folder.parent.name}.{folder.name}.deploy_config" + deploy_module = importlib.import_module(module_name) + return deploy_module.deploy # type: ignore[no-any-return, misc] + except ImportError: + return None + + +def has_contract_file(directory: Path) -> bool: + """Checks whether the directory contains a contract.py file.""" + return (directory / "contract.py").exists() + + +# Use the current directory (root_path) as the base for contract folders and exclude +# folders that start with '_' (internal helpers). +contracts: list[SmartContract] = [ + SmartContract( + path=import_contract(folder), + name=folder.name, + deploy=import_deploy_if_exists(folder), + ) + for folder in root_path.iterdir() + if folder.is_dir() and has_contract_file(folder) and not folder.name.startswith("_") +] + +# -------------------------- Build Logic -------------------------- # + +deployment_extension = "py" + + +def _get_output_path(output_dir: Path, deployment_extension: str) -> Path: + """Constructs the output path for the generated client file.""" + return output_dir / Path( + "{contract_name}" + + ("_client" if deployment_extension == "py" else "Client") + + f".{deployment_extension}" + ) + + +def build(output_dir: Path, contract_path: Path) -> Path: + """ + Builds the contract by exporting (compiling) its source and generating a client. + If the output directory already exists, it is cleared. + """ + output_dir = output_dir.resolve() + if output_dir.exists(): + rmtree(output_dir) + output_dir.mkdir(exist_ok=True, parents=True) + logger.info(f"Exporting {contract_path} to {output_dir}") + + build_result = subprocess.run( + [ + "algokit", + "--no-color", + "compile", + "python", + str(contract_path.resolve()), + f"--out-dir={output_dir}", + "--no-output-arc32", + "--output-arc56", + "--output-source-map", + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + if build_result.returncode: + raise Exception(f"Could not build contract:\n{build_result.stdout}") + + # Look for arc56.json files and generate the client based on them. + app_spec_file_names: list[str] = [ + file.name for file in output_dir.glob("*.arc56.json") + ] + + client_file: str | None = None + if not app_spec_file_names: + logger.warning( + "No '*.arc56.json' file found (likely a logic signature being compiled). Skipping client generation." + ) + else: + for file_name in app_spec_file_names: + client_file = file_name + print(file_name) + generate_result = subprocess.run( + [ + "algokit", + "generate", + "client", + str(output_dir), + "--output", + str(_get_output_path(output_dir, deployment_extension)), + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + if generate_result.returncode: + if "No such command" in generate_result.stdout: + raise Exception( + "Could not generate typed client, requires AlgoKit 2.0.0 or later. Please update AlgoKit" + ) + else: + raise Exception( + f"Could not generate typed client:\n{generate_result.stdout}" + ) + if client_file: + return output_dir / client_file + return output_dir + + +# --------------------------- Main Logic --------------------------- # + def main(action: str, contract_name: str | None = None) -> None: + """Main entry point to build and/or deploy smart contracts.""" artifact_path = root_path / "artifacts" - - # Filter contracts if a specific contract name is provided + # Filter contracts based on an optional specific contract name. filtered_contracts = [ - c for c in contracts if contract_name is None or c.name == contract_name + contract + for contract in contracts + if contract_name is None or contract.name == contract_name ] match action: @@ -44,23 +182,24 @@ def main(action: str, contract_name: str | None = None) -> None: ( file.name for file in output_dir.iterdir() - if file.is_file() and file.suffixes == [".arc32", ".json"] + if file.is_file() and file.suffixes == [".arc56", ".json"] ), None, ) if app_spec_file_name is None: - raise Exception("Could not deploy app, .arc32.json file not found") - app_spec_path = output_dir / app_spec_file_name + raise Exception("Could not deploy app, .arc56.json file not found") if contract.deploy: logger.info(f"Deploying app {contract.name}") - deploy(app_spec_path, contract.deploy) + contract.deploy() case "all": for contract in filtered_contracts: logger.info(f"Building app at {contract.path}") - app_spec_path = build(artifact_path / contract.name, contract.path) + build(artifact_path / contract.name, contract.path) if contract.deploy: - logger.info(f"Deploying {contract.path.name}") - deploy(app_spec_path, contract.deploy) + logger.info(f"Deploying {contract.name}") + contract.deploy() + case _: + logger.error(f"Unknown action: {action}") if __name__ == "__main__": diff --git a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/__init__.py b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/build.py b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/build.py deleted file mode 100644 index 3461154..0000000 --- a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/build.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging -import subprocess -from pathlib import Path -from shutil import rmtree - -logger = logging.getLogger(__name__) -deployment_extension = "py" - - -def _get_output_path(output_dir: Path, deployment_extension: str) -> Path: - return output_dir / Path( - "{contract_name}" - + ("_client" if deployment_extension == "py" else "Client") - + f".{deployment_extension}" - ) - - -def build(output_dir: Path, contract_path: Path) -> Path: - output_dir = output_dir.resolve() - if output_dir.exists(): - rmtree(output_dir) - output_dir.mkdir(exist_ok=True, parents=True) - logger.info(f"Exporting {contract_path} to {output_dir}") - - build_result = subprocess.run( - [ - "algokit", - "--no-color", - "compile", - "python", - contract_path.absolute(), - f"--out-dir={output_dir}", - "--output-arc32", - "--output-source-map", - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - if build_result.returncode: - raise Exception(f"Could not build contract:\n{build_result.stdout}") - - app_spec_file_names = [file.name for file in output_dir.glob("*.arc32.json")] - app_spec_file_name = None - for app_spec_file_name in app_spec_file_names: - if app_spec_file_name is None: - logger.warning( - "No '*.arc32.json' file found (likely a logic signature being compiled). Skipping client generation." - ) - continue - print(app_spec_file_name) - generate_result = subprocess.run( - [ - "algokit", - "generate", - "client", - output_dir, - "--output", - _get_output_path(output_dir, deployment_extension), - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - if generate_result.returncode: - if "No such command" in generate_result.stdout: - raise Exception( - "Could not generate typed client, requires AlgoKit 2.0.0 or " - "later. Please update AlgoKit" - ) - else: - raise Exception( - f"Could not generate typed client:\n{generate_result.stdout}" - ) - - return output_dir / app_spec_file_name if app_spec_file_name else output_dir diff --git a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/config.py b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/config.py deleted file mode 100644 index 8f3ca93..0000000 --- a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/config.py +++ /dev/null @@ -1,61 +0,0 @@ -import dataclasses -import importlib -from collections.abc import Callable -from pathlib import Path - -from algokit_utils import Account, ApplicationSpecification -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient - - -@dataclasses.dataclass -class SmartContract: - path: Path - name: str - deploy: ( - Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] - | None - ) = None - - -def import_contract(folder: Path) -> Path: - """Imports the contract from a folder if it exists.""" - contract_path = folder / "contract.py" - if contract_path.exists(): - return contract_path - else: - raise Exception(f"Contract not found in {folder}") - - -def import_deploy_if_exists( - folder: Path, -) -> ( - Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] - | None -): - """Imports the deploy function from a folder if it exists.""" - try: - deploy_module = importlib.import_module( - f"{folder.parent.name}.{folder.name}.deploy_config" - ) - return deploy_module.deploy # type: ignore - except ImportError: - return None - - -def has_contract_file(directory: Path) -> bool: - """Checks whether the directory contains contract.py file.""" - return (directory / "contract.py").exists() - - -# define contracts to build and/or deploy -base_dir = Path("smart_contracts") -contracts = [ - SmartContract( - path=import_contract(folder), - name=folder.name, - deploy=import_deploy_if_exists(folder), - ) - for folder in base_dir.iterdir() - if folder.is_dir() and has_contract_file(folder) -] diff --git a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/deploy.py b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/deploy.py deleted file mode 100644 index 10185a9..0000000 --- a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/_helpers/deploy.py +++ /dev/null @@ -1,53 +0,0 @@ -# mypy: disable-error-code="no-untyped-call, misc" - - -import logging -from collections.abc import Callable -from pathlib import Path - -from algokit_utils import ( - Account, - ApplicationSpecification, - EnsureBalanceParameters, - ensure_funded, - get_account, - get_algod_client, - get_indexer_client, -) -from algosdk.util import algos_to_microalgos -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient - -logger = logging.getLogger(__name__) - - -def deploy( - app_spec_path: Path, - deploy_callback: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ], - deployer_initial_funds: int = 2, -) -> None: - # get clients - # by default client configuration is loaded from environment variables - algod_client = get_algod_client() - indexer_client = get_indexer_client() - - # get app spec - app_spec = ApplicationSpecification.from_json(app_spec_path.read_text()) - - # get deployer account by name - deployer = get_account(algod_client, "DEPLOYER", fund_with_algos=0) - - minimum_funds_micro_algos = algos_to_microalgos(deployer_initial_funds) - ensure_funded( - algod_client, - EnsureBalanceParameters( - account_to_fund=deployer, - min_spending_balance_micro_algos=minimum_funds_micro_algos, - min_funding_increment_micro_algos=minimum_funds_micro_algos, - ), - ) - - # use provided callback to deploy the app - deploy_callback(algod_client, indexer_client, app_spec, deployer) diff --git a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/hello_world/deploy_config.py b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/hello_world/deploy_config.py index 36dda16..364edf6 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/hello_world/deploy_config.py +++ b/examples/production_python_react/projects/production_python_react-contracts/smart_contracts/hello_world/deploy_config.py @@ -1,36 +1,44 @@ import logging import algokit_utils -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient logger = logging.getLogger(__name__) # define deployment behaviour based on supplied app spec -def deploy( - algod_client: AlgodClient, - indexer_client: IndexerClient, - app_spec: algokit_utils.ApplicationSpecification, - deployer: algokit_utils.Account, -) -> None: +def deploy() -> None: from smart_contracts.artifacts.hello_world.hello_world_client import ( - HelloWorldClient, + HelloArgs, + HelloWorldFactory, ) - app_client = HelloWorldClient( - algod_client, - creator=deployer, - indexer_client=indexer_client, + algorand = algokit_utils.AlgorandClient.from_environment() + deployer_ = algorand.account.from_environment("DEPLOYER") + + factory = algorand.client.get_typed_app_factory( + HelloWorldFactory, default_sender=deployer_.address ) - app_client.deploy( - on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, + app_client, result = factory.deploy( on_update=algokit_utils.OnUpdate.AppendApp, + on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, ) + + if result.operation_performed in [ + algokit_utils.OperationPerformed.Create, + algokit_utils.OperationPerformed.Replace, + ]: + algorand.send.payment( + algokit_utils.PaymentParams( + amount=algokit_utils.AlgoAmount(algo=1), + sender=deployer_.address, + receiver=app_client.app_address, + ) + ) + name = "world" - response = app_client.hello(name=name) + response = app_client.send.hello(args=HelloArgs(name=name)) logger.info( - f"Called hello on {app_spec.contract.name} ({app_client.app_id}) " - f"with name={name}, received: {response.return_value}" + f"Called hello on {app_client.app_name} ({app_client.app_id}) " + f"with name={name}, received: {response.abi_return}" ) diff --git a/examples/production_python_react/projects/production_python_react-contracts/tests/conftest.py b/examples/production_python_react/projects/production_python_react-contracts/tests/conftest.py index aec2485..c15f645 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/tests/conftest.py +++ b/examples/production_python_react/projects/production_python_react-contracts/tests/conftest.py @@ -1,11 +1,6 @@ import pytest -from algokit_utils import ( - get_algod_client, - get_default_localnet_config, - get_indexer_client, -) -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient +from algokit_utils import AlgorandClient +from algokit_utils.config import config # Uncomment if you want to load network specific or generic .env file # @pytest.fixture(autouse=True, scope="session") @@ -13,14 +8,13 @@ # env_path = Path(__file__).parent.parent / ".env" # load_dotenv(env_path) - -@pytest.fixture(scope="session") -def algod_client() -> AlgodClient: - # by default we are using localnet algod - client = get_algod_client(get_default_localnet_config("algod")) - return client +config.configure( + debug=True, + # trace_all=True, # uncomment to trace all transactions +) @pytest.fixture(scope="session") -def indexer_client() -> IndexerClient: - return get_indexer_client(get_default_localnet_config("indexer")) +def algorand_client() -> AlgorandClient: + # by default we are using localnet algod + return AlgorandClient.from_environment() diff --git a/examples/production_python_react/projects/production_python_react-contracts/tests/hello_world_client_test.py b/examples/production_python_react/projects/production_python_react-contracts/tests/hello_world_client_test.py index 1c8ebff..6a9c447 100644 --- a/examples/production_python_react/projects/production_python_react-contracts/tests/hello_world_client_test.py +++ b/examples/production_python_react/projects/production_python_react-contracts/tests/hello_world_client_test.py @@ -1,29 +1,35 @@ import algokit_utils import pytest -from algokit_utils import get_localnet_default_account -from algokit_utils.config import config -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient - -from smart_contracts.artifacts.hello_world.hello_world_client import HelloWorldClient +from algokit_utils import ( + AlgoAmount, + AlgorandClient, + SigningAccount, +) + +from smart_contracts.artifacts.hello_world.hello_world_client import ( + HelloWorldClient, + HelloWorldFactory, +) + + +@pytest.fixture() +def deployer(algorand_client: AlgorandClient) -> SigningAccount: + account = algorand_client.account.from_environment("DEPLOYER") + algorand_client.account.ensure_funded_from_environment( + account_to_fund=account.address, min_spending_balance=AlgoAmount.from_algo(10) + ) + return account -@pytest.fixture(scope="session") +@pytest.fixture() def hello_world_client( - algod_client: AlgodClient, indexer_client: IndexerClient + algorand_client: AlgorandClient, deployer: SigningAccount ) -> HelloWorldClient: - config.configure( - debug=True, - # trace_all=True, - ) - - client = HelloWorldClient( - algod_client, - creator=get_localnet_default_account(algod_client), - indexer_client=indexer_client, + factory = algorand_client.client.get_typed_app_factory( + HelloWorldFactory, default_sender=deployer.address ) - client.deploy( + client, _ = factory.deploy( on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, on_update=algokit_utils.OnUpdate.AppendApp, ) @@ -31,18 +37,19 @@ def hello_world_client( def test_says_hello(hello_world_client: HelloWorldClient) -> None: - result = hello_world_client.hello(name="World") - - assert result.return_value == "Hello, World" + result = hello_world_client.send.hello(args=("World",)) + assert result.abi_return == "Hello, World" def test_simulate_says_hello_with_correct_budget_consumed( - hello_world_client: HelloWorldClient, algod_client: AlgodClient + hello_world_client: HelloWorldClient, ) -> None: result = ( - hello_world_client.compose().hello(name="World").hello(name="Jane").simulate() + hello_world_client.new_group() + .hello(args=("World",)) + .hello(args=("Jane",)) + .simulate() ) - - assert result.abi_results[0].return_value == "Hello, World" - assert result.abi_results[1].return_value == "Hello, Jane" + assert result.returns[0].value == "Hello, World" + assert result.returns[1].value == "Hello, Jane" assert result.simulate_response["txn-groups"][0]["app-budget-consumed"] < 100 diff --git a/examples/production_python_react/projects/production_python_react-frontend/.algokit/.copier-answers.yml b/examples/production_python_react/projects/production_python_react-frontend/.algokit/.copier-answers.yml index 1c668c5..3283fc8 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/.algokit/.copier-answers.yml +++ b/examples/production_python_react/projects/production_python_react-frontend/.algokit/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: 1.0.12 +_commit: 1.0.13 _src_path: gh:algorandfoundation/algokit-react-frontend-template author_email: None author_name: None diff --git a/examples/production_python_react/projects/production_python_react-frontend/.env.template b/examples/production_python_react/projects/production_python_react-frontend/.env.template index e05d499..eb4b229 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/.env.template +++ b/examples/production_python_react/projects/production_python_react-frontend/.env.template @@ -9,7 +9,7 @@ VITE_ENVIRONMENT=local VITE_ALGOD_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa VITE_ALGOD_SERVER=http://localhost VITE_ALGOD_PORT=4001 -VITE_ALGOD_NETWORK="" +VITE_ALGOD_NETWORK=localnet # Indexer VITE_INDEXER_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/examples/production_python_react/projects/production_python_react-frontend/package.json b/examples/production_python_react/projects/production_python_react-frontend/package.json index 8601ca3..fdbb66a 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/package.json +++ b/examples/production_python_react/projects/production_python_react-frontend/package.json @@ -12,7 +12,7 @@ "npm": ">=9.0" }, "devDependencies": { - "@algorandfoundation/algokit-client-generator": "^4.0.0", + "@algorandfoundation/algokit-client-generator": "^4.0.6", "@types/node": "^18.17.14", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", @@ -35,12 +35,12 @@ "vite-plugin-node-polyfills": "^0.22.0" }, "dependencies": { - "@algorandfoundation/algokit-utils": "^7.0.0", - "@blockshake/defly-connect": "1.1.6", - "@daffiwallet/connect": "^1.0.3", - "@perawallet/connect": "1.3.5", - "@txnlab/use-wallet": "^2.8.2", - "algosdk": ">=2.9.0 <3.0", + "@algorandfoundation/algokit-utils": "^8.1.0", + "@blockshake/defly-connect": "^1.2.1", + "@perawallet/connect": "^1.4.1", + "@txnlab/use-wallet": "^4.0.0", + "@txnlab/use-wallet-react": "^4.0.0", + "algosdk": "^3.0.0", "daisyui": "^4.0.0", "notistack": "^3.0.1", "react": "^18.2.0", diff --git a/examples/production_python_react/projects/production_python_react-frontend/src/App.tsx b/examples/production_python_react/projects/production_python_react-frontend/src/App.tsx index 58feddf..f346006 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/src/App.tsx +++ b/examples/production_python_react/projects/production_python_react-frontend/src/App.tsx @@ -1,33 +1,26 @@ -import { DeflyWalletConnect } from '@blockshake/defly-connect' -import { DaffiWalletConnect } from '@daffiwallet/connect' -import { PeraWalletConnect } from '@perawallet/connect' -import { PROVIDER_ID, ProvidersArray, WalletProvider, useInitializeProviders } from '@txnlab/use-wallet' -import algosdk from 'algosdk' +import { SupportedWallet, WalletId, WalletManager, WalletProvider } from '@txnlab/use-wallet-react' import { SnackbarProvider } from 'notistack' import Home from './Home' import { getAlgodConfigFromViteEnvironment, getKmdConfigFromViteEnvironment } from './utils/network/getAlgoClientConfigs' -let providersArray: ProvidersArray -if (import.meta.env.VITE_ALGOD_NETWORK === '') { +let supportedWallets: SupportedWallet[] +if (import.meta.env.VITE_ALGOD_NETWORK === 'localnet') { const kmdConfig = getKmdConfigFromViteEnvironment() - providersArray = [ + supportedWallets = [ { - id: PROVIDER_ID.KMD, - clientOptions: { - wallet: kmdConfig.wallet, - password: kmdConfig.password, - host: kmdConfig.server, + id: WalletId.KMD, + options: { + baseServer: kmdConfig.server, token: String(kmdConfig.token), port: String(kmdConfig.port), }, }, ] } else { - providersArray = [ - { id: PROVIDER_ID.DEFLY, clientStatic: DeflyWalletConnect }, - { id: PROVIDER_ID.PERA, clientStatic: PeraWalletConnect }, - { id: PROVIDER_ID.DAFFI, clientStatic: DaffiWalletConnect }, - { id: PROVIDER_ID.EXODUS }, + supportedWallets = [ + { id: WalletId.DEFLY }, + { id: WalletId.PERA }, + { id: WalletId.EXODUS }, // If you are interested in WalletConnect v2 provider // refer to https://github.com/TxnLab/use-wallet for detailed integration instructions ] @@ -36,20 +29,26 @@ if (import.meta.env.VITE_ALGOD_NETWORK === '') { export default function App() { const algodConfig = getAlgodConfigFromViteEnvironment() - const walletProviders = useInitializeProviders({ - providers: providersArray, - nodeConfig: { - network: algodConfig.network, - nodeServer: algodConfig.server, - nodePort: String(algodConfig.port), - nodeToken: String(algodConfig.token), + const walletManager = new WalletManager({ + wallets: supportedWallets, + defaultNetwork: algodConfig.network, + networks: { + [algodConfig.network]: { + algod: { + baseServer: algodConfig.server, + port: algodConfig.port, + token: String(algodConfig.token), + }, + }, + }, + options: { + resetNetwork: true, }, - algosdkStatic: algosdk, }) return ( - + diff --git a/examples/production_python_react/projects/production_python_react-frontend/src/Home.tsx b/examples/production_python_react/projects/production_python_react-frontend/src/Home.tsx index 192f3b9..68313ee 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/src/Home.tsx +++ b/examples/production_python_react/projects/production_python_react-frontend/src/Home.tsx @@ -1,5 +1,5 @@ // src/components/Home.tsx -import { useWallet } from '@txnlab/use-wallet' +import { useWallet } from '@txnlab/use-wallet-react' import React, { useState } from 'react' import ConnectWallet from './components/ConnectWallet' import Transact from './components/Transact' diff --git a/examples/production_python_react/projects/production_python_react-frontend/src/components/Account.tsx b/examples/production_python_react/projects/production_python_react-frontend/src/components/Account.tsx index 8324312..081ce47 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/src/components/Account.tsx +++ b/examples/production_python_react/projects/production_python_react-frontend/src/components/Account.tsx @@ -1,4 +1,4 @@ -import { useWallet } from '@txnlab/use-wallet' +import { useWallet } from '@txnlab/use-wallet-react' import { useMemo } from 'react' import { ellipseAddress } from '../utils/ellipseAddress' import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' diff --git a/examples/production_python_react/projects/production_python_react-frontend/src/components/AppCalls.tsx b/examples/production_python_react/projects/production_python_react-frontend/src/components/AppCalls.tsx index 1667fbc..1268994 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/src/components/AppCalls.tsx +++ b/examples/production_python_react/projects/production_python_react-frontend/src/components/AppCalls.tsx @@ -1,4 +1,4 @@ -import { useWallet } from '@txnlab/use-wallet' +import { useWallet } from '@txnlab/use-wallet-react' import { useSnackbar } from 'notistack' import { useState } from 'react' import { HelloWorldFactory } from '../contracts/HelloWorld' @@ -15,7 +15,7 @@ const AppCalls = ({ openModal, setModalState }: AppCallsInterface) => { const [loading, setLoading] = useState(false) const [contractInput, setContractInput] = useState('') const { enqueueSnackbar } = useSnackbar() - const { signer, activeAddress } = useWallet() + const { transactionSigner, activeAddress } = useWallet() const algodConfig = getAlgodConfigFromViteEnvironment() const indexerConfig = getIndexerConfigFromViteEnvironment() @@ -23,7 +23,7 @@ const AppCalls = ({ openModal, setModalState }: AppCallsInterface) => { algodConfig, indexerConfig, }) - algorand.setDefaultSigner(signer) + algorand.setDefaultSigner(transactionSigner) const sendAppCall = async () => { setLoading(true) @@ -34,7 +34,7 @@ const AppCalls = ({ openModal, setModalState }: AppCallsInterface) => { // Given the simplicity of the starter contract, we are deploying it on the frontend // for demonstration purposes. const factory = new HelloWorldFactory({ - defaultSender: activeAddress, + defaultSender: activeAddress ?? undefined, algorand, }) const deployResult = await factory diff --git a/examples/production_python_react/projects/production_python_react-frontend/src/components/ConnectWallet.tsx b/examples/production_python_react/projects/production_python_react-frontend/src/components/ConnectWallet.tsx index c4225bc..8ff01dc 100644 --- a/examples/production_python_react/projects/production_python_react-frontend/src/components/ConnectWallet.tsx +++ b/examples/production_python_react/projects/production_python_react-frontend/src/components/ConnectWallet.tsx @@ -1,4 +1,4 @@ -import { Provider, useWallet } from '@txnlab/use-wallet' +import { useWallet, Wallet, WalletId } from '@txnlab/use-wallet-react' import Account from './Account' interface ConnectWalletInterface { @@ -7,9 +7,9 @@ interface ConnectWalletInterface { } const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => { - const { providers, activeAddress } = useWallet() + const { wallets, activeAddress } = useWallet() - const isKmd = (provider: Provider) => provider.metadata.name.toLowerCase() === 'kmd' + const isKmd = (wallet: Wallet) => wallet.id === WalletId.KMD return ( @@ -25,23 +25,23 @@ const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => { )} {!activeAddress && - providers?.map((provider) => ( + wallets?.map((wallet) => ( ))} @@ -60,16 +60,16 @@ const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => { ))} @@ -60,16 +60,16 @@ const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => { ))} @@ -60,16 +60,16 @@ const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => { ))} @@ -60,16 +60,16 @@ const ConnectWallet = ({ openModal, closeModal }: ConnectWalletInterface) => {