Skip to content

Commit

Permalink
fix: use-wallet v4 support; regenerating against latest python templa…
Browse files Browse the repository at this point in the history
…te with utils v3 and v2 (#40)

* fix: support use wallet v4

* chore: regen examples
  • Loading branch information
aorumbayev authored Feb 18, 2025
1 parent 230a15b commit c0c2605
Show file tree
Hide file tree
Showing 76 changed files with 703 additions and 735 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 .',
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},

// Python
"python.analysis.autoImportCompletions": true,
"python.analysis.extraPaths": ["${workspaceFolder}/smart_contracts"],
"python.analysis.diagnosticSeverityOverrides": {
"reportMissingModuleSource": "none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "*"
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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__":
Expand Down
Empty file.
Loading

0 comments on commit c0c2605

Please sign in to comment.