Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: updating ts deployer to use v8 utils; updating py to support v3 utils-py #94

Merged
merged 7 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .github/workflows/check-python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,10 @@ jobs:
- name: Install dependencies
run: poetry env use 3.12 && poetry install --no-interaction --no-root

- name: Check formatting with Black
run: |
# stop the build if there are files that don't meet formatting requirements
poetry run black --check .

- name: Check linting with Ruff
run: |
# stop the build if there are Python syntax errors or undefined names
poetry run ruff .
poetry run ruff check .

- name: Configure git
shell: bash
Expand Down
11 changes: 1 addition & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
repos:
- repo: local
hooks:
- id: black
name: black
description: "Black: The uncompromising Python code formatter"
entry: poetry run black
language: system
minimum_pre_commit_version: 2.9.2
require_serial: true
types_or: [ python, pyi ]
- id: ruff
name: ruff
description: "Run 'ruff' for extremely fast Python linting"
entry: poetry run ruff
entry: poetry run ruff check --fix
language: system
'types': [python]
args: [--fix]
require_serial: false
additional_dependencies: []
minimum_pre_commit_version: '0'
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,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 @@ -19,7 +19,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 @@ -12,7 +12,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 @@ -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 @@ -29,14 +29,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
Loading
Loading