Skip to content
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
1 change: 1 addition & 0 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ See which `algorand-python` stubs are implemented by the `algorand-python-testin
| algopy.ensure_budget | Emulated |
| algopy.log | Emulated |
| algopy.logicsig | Emulated |
| algopy.public | Emulated |
| algopy.size_of | Emulated |
| algopy.subroutine | Native |
| algopy.uenumerate | Native |
Expand Down
8 changes: 4 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class VotingContract(algopy.ARC4Contract):
)
self.voted = algopy.LocalState(algopy.UInt64, key="voted", description="Tracks if an account has voted")

@arc4.abimethod
@algopy.public
def set_topic(self, topic: arc4.String) -> None:
self.topic.value = topic.bytes

Expand All @@ -79,7 +79,7 @@ class VotingContract(algopy.ARC4Contract):
self.voted[algopy.Txn.sender] = algopy.UInt64(1)
return arc4.Bool(True)

@arc4.abimethod(readonly=True)
@algopy.public(readonly=True)
def get_votes(self) -> arc4.UInt64:
return arc4.UInt64(self.votes.value)

Expand Down Expand Up @@ -141,9 +141,9 @@ This example demonstrates key aspects of testing with `algorand-python-testing`
1. ARC4 Contract Features:

- Use of `algopy.ARC4Contract` as the base class for the contract.
- ABI methods defined using the `@arc4.abimethod` decorator.
- ABI methods defined using the `@arc4.abimethod`, or its alias `@algopy.public`, decorator.
- Use of ARC4-specific types like `arc4.String`, `arc4.Bool`, and `arc4.UInt64`.
- Readonly method annotation with `@arc4.abimethod(readonly=True)`.
- Readonly method annotation with `@arc4.abimethod(readonly=True)` or `@algopy.public(readonly=True)` .

2. Testing ARC4 Contracts:

Expand Down
4 changes: 2 additions & 2 deletions docs/testing-guide/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ For a full list of all public `algopy` types and their corresponding implementat

## Data Validation

Algorand Python and the puya compiler have functionality to perform validation of transaction inputs via the `--validate-abi-args`, `--validate-abi-return` CLI arguments, `arc4.abimethod(validate_encoding=...)` decorator and `.validate()` methods.
The Algorand Python Testing library does *NOT* implement this validation behaviour, as you should test invalid inputs using an integrated test against a real Algorand network.
Algorand Python and the puya compiler have functionality to perform validation of transaction inputs via the `--validate-abi-args`, `--validate-abi-return` CLI arguments, `arc4.abimethod(validate_encoding=...)` decorator (or its alias, `algopy.public`) and `.validate()` methods.
The Algorand Python Testing library does _NOT_ implement this validation behaviour, as you should test invalid inputs using an integrated test against a real Algorand network.
10 changes: 5 additions & 5 deletions docs/testing-guide/contract-testing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Smart Contract Testing

This guide provides an overview of how to test smart contracts using the Algorand Python SDK (`algopy`). It covers the basics of testing `ARC4Contract` and `Contract` classes, focusing on the `abimethod` and `baremethod` decorators.
This guide provides an overview of how to test smart contracts using the Algorand Python SDK (`algopy`). It covers the basics of testing `ARC4Contract` and `Contract` classes, focusing on the `baremethod`, `abimethod` and `public` (an alias of `abimethod`) decorators.

![](https://mermaid.ink/img/pako:eNqVkrFugzAQhl_Fujnp1ImhEiJrJNREWeoOV9sNVsFG9iEVBd69R5w0JE2llsk2n7-7_-AAymsDGewDtpXYrqQT_GyKFwl5vfcBnRZlT5V3IjYYSCjvKKAiCa-JzXfrObyzgTqsxRpVZZ25YOX2nnRrIomCneZzpszLkllktu0f8ratrUKyjFsXCZ1K2gTH7i01_8dGUjOT_55YeLdUFVr3zRunf5b6R5hZoFnBq9cX72_Br_Cj8bl4vJCHaVucvowYxHk5Xg_sfPkY6SbbphDL5dMgQZu29n0U5DMJwzTVGyApySKZKFSNMXKVxPJYYAGNCQ1azX_VYboqgSrTcAcZLzWGDwnSjcxhR37TOwUZhc4sIPhuX0H2jnXkXddqrrCyyKNpTqfjF5m74B8?type=png)

Expand All @@ -24,7 +24,7 @@ context = ctx_manager.__enter__()

Subclasses of `algopy.ARC4Contract` are **required** to be instantiated with an active test context. As part of instantiation, the test context will automatically create a matching `algopy.Application` object instance.

Within the class implementation, methods decorated with `algopy.arc4.abimethod` and `algopy.arc4.baremethod` will automatically assemble an `algopy.gtxn.ApplicationCallTransaction` to emulate the AVM application call. This behaviour can be overridden by setting the transaction group manually as part of test setup; this is done via implicit invocation of the `algopy_testing.context.any_application()` _value generator_ (refer to the [API](../api.md) for more details).
Within the class implementation, methods decorated with `algopy.arc4.abimethod` (or its alias, `algopy.public`) and `algopy.arc4.baremethod` will automatically assemble an `algopy.gtxn.ApplicationCallTransaction` to emulate the AVM application call. This behaviour can be overridden by setting the transaction group manually as part of test setup; this is done via implicit invocation of the `algopy_testing.context.any_application()` _value generator_ (refer to the [API](../api.md) for more details).

```{testcode}
class SimpleVotingContract(algopy.ARC4Contract):
Expand All @@ -42,14 +42,14 @@ class SimpleVotingContract(algopy.ARC4Contract):
self.topic.value = initial_topic
self.votes.value = algopy.UInt64(0)

@algopy.arc4.abimethod
@algopy.public
def vote(self) -> algopy.UInt64:
assert self.voted[algopy.Txn.sender] == algopy.UInt64(0), "Account has already voted"
self.votes.value += algopy.UInt64(1)
self.voted[algopy.Txn.sender] = algopy.UInt64(1)
return self.votes.value

@algopy.arc4.abimethod(readonly=True)
@algopy.public(readonly=True)
def get_votes(self) -> algopy.UInt64:
return self.votes.value

Expand All @@ -74,7 +74,7 @@ assert contract.topic.value == initial_topic
assert contract.votes.value == algopy.UInt64(0)

# Act - Vote
# The method `.vote()` is decorated with `algopy.arc4.abimethod`, which means it will assemble a transaction to emulate the AVM application call
# The method `.vote()` is decorated with `algopy.public`, an alias of `algopy.arc4.abimethod`, which means it will assemble a transaction to emulate the AVM application call
result = contract.vote()

# Assert - you can access the corresponding auto generated application call transaction via test context
Expand Down
4 changes: 3 additions & 1 deletion docs/testing-guide/subroutines.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ context = ctx_manager.__enter__()

The `@algopy.subroutine` decorator exposes contract methods for isolated testing within the Algorand Python Testing framework. This enables focused validation of core business logic without the overhead of full application deployment and execution.

`@algopy.subroutine` decorator is optional for the methods in a contract which are not callable externally.

## Usage

1. Decorate internal methods with `@algopy.subroutine`:
Expand All @@ -26,7 +28,7 @@ The `@algopy.subroutine` decorator exposes contract methods for isolated testing
from algopy import subroutine, UInt64

class MyContract:
@subroutine
@subroutine # optional
def calculate_value(self, input: UInt64) -> UInt64:
return input * UInt64(2)
```
Expand Down
4 changes: 2 additions & 2 deletions docs/testing-guide/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ When testing smart contracts, to stay consistent with AVM, the framework _does n

```{testcode}
class MyContract(algopy.ARC4Contract):
@algopy.arc4.abimethod
@algopy.public
def pay_via_itxn(self, asset: algopy.Asset) -> None:
algopy.itxn.Payment(
receiver=algopy.Txn.sender,
Expand Down Expand Up @@ -180,7 +180,7 @@ first_payment_txn = first_itxn_group.payment(0)

In this example, we define a contract method `pay_via_itxn` that creates and submits an inner payment transaction. The test context automatically captures and stores the inner transactions submitted by the contract method.

Note that we don't need to wrap the execution in a `create_group` context manager because the method is decorated with `@algopy.arc4.abimethod`, which automatically creates a transaction group for the method. The `create_group` context manager is only needed when you want to create more complex transaction groups or patch transaction fields for various transaction-related opcodes in AVM.
Note that we don't need to wrap the execution in a `create_group` context manager because the method is decorated with `@algopy.public`, an alias of `@algopy.arc4.abimethod`, which automatically creates a transaction group for the method. The `create_group` context manager is only needed when you want to create more complex transaction groups or patch transaction fields for various transaction-related opcodes in AVM.

To access the submitted inner transactions:

Expand Down
3 changes: 0 additions & 3 deletions examples/marketplace/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
gtxn,
itxn,
op,
subroutine,
)
from algopy.arc4 import abimethod

Expand All @@ -32,7 +31,6 @@ class DigitalMarketplace(ARC4Contract):
def __init__(self) -> None:
self.listings = BoxMap(ListingKey, ListingValue)

@subroutine
def listings_box_mbr(self) -> UInt64:
return (
2_500
Expand All @@ -55,7 +53,6 @@ def listings_box_mbr(self) -> UInt64:
* 400
)

@subroutine
def quantity_price(self, quantity: UInt64, price: UInt64, asset_decimals: UInt64) -> UInt64:
amount_not_scaled_high, amount_not_scaled_low = op.mulw(price, quantity)
scaling_factor_high, scaling_factor_low = op.expw(10, asset_decimals)
Expand Down
26 changes: 13 additions & 13 deletions examples/proof_of_attendance/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ def __init__(self) -> None:
self.total_attendees = algopy.UInt64(0)
self.box_map = algopy.BoxMap(algopy.Bytes, algopy.UInt64)

@algopy.arc4.abimethod(create="require")
@algopy.public(create="require")
def init(self, max_attendees: algopy.UInt64) -> None:
assert algopy.Txn.sender == algopy.Global.creator_address, "Only creator can initialize"
self.max_attendees = max_attendees

@algopy.arc4.abimethod()
@algopy.public()
def confirm_attendance(self) -> None:
assert self.total_attendees < self.max_attendees, "Max attendees reached"

Expand All @@ -25,7 +25,7 @@ def confirm_attendance(self) -> None:

algopy.op.Box.put(algopy.Txn.sender.bytes, algopy.op.itob(minted_asset.id))

@algopy.arc4.abimethod()
@algopy.public()
def confirm_attendance_with_box(self) -> None:
assert self.total_attendees < self.max_attendees, "Max attendees reached"

Expand All @@ -38,7 +38,7 @@ def confirm_attendance_with_box(self) -> None:

box.value = minted_asset.id

@algopy.arc4.abimethod()
@algopy.public()
def confirm_attendance_with_box_ref(self) -> None:
assert self.total_attendees < self.max_attendees, "Max attendees reached"

Expand All @@ -51,7 +51,7 @@ def confirm_attendance_with_box_ref(self) -> None:

box_ref.value = algopy.op.itob(minted_asset.id)

@algopy.arc4.abimethod()
@algopy.public()
def confirm_attendance_with_box_map(self) -> None:
assert self.total_attendees < self.max_attendees, "Max attendees reached"

Expand All @@ -63,33 +63,33 @@ def confirm_attendance_with_box_map(self) -> None:

self.box_map[algopy.Txn.sender.bytes] = minted_asset.id

@algopy.arc4.abimethod(readonly=True)
@algopy.public(readonly=True)
def get_poa_id(self) -> algopy.UInt64:
poa_id, exists = algopy.op.Box.get(algopy.Txn.sender.bytes)
assert exists, "POA not found"
return algopy.op.btoi(poa_id)

@algopy.arc4.abimethod(readonly=True)
@algopy.public(readonly=True)
def get_poa_id_with_box(self) -> algopy.UInt64:
box = algopy.Box(algopy.UInt64, key=algopy.Txn.sender.bytes)
poa_id, exists = box.maybe()
assert exists, "POA not found"
return poa_id

@algopy.arc4.abimethod(readonly=True)
@algopy.public(readonly=True)
def get_poa_id_with_box_ref(self) -> algopy.UInt64:
box_ref = algopy.Box(algopy.Bytes, key=algopy.Txn.sender.bytes)
poa_id, exists = box_ref.maybe()
assert exists, "POA not found"
return algopy.op.btoi(poa_id)

@algopy.arc4.abimethod(readonly=True)
@algopy.public(readonly=True)
def get_poa_id_with_box_map(self) -> algopy.UInt64:
poa_id, exists = self.box_map.maybe(algopy.Txn.sender.bytes)
assert exists, "POA not found"
return poa_id

@algopy.arc4.abimethod()
@algopy.public()
def claim_poa(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
poa_id, exists = algopy.op.Box.get(algopy.Txn.sender.bytes)
assert exists, "POA not found, attendance validation failed!"
Expand All @@ -108,7 +108,7 @@ def claim_poa(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
algopy.op.btoi(poa_id),
)

@algopy.arc4.abimethod()
@algopy.public()
def claim_poa_with_box(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
box = algopy.Box(algopy.UInt64, key=algopy.Txn.sender.bytes)
poa_id, exists = box.maybe()
Expand All @@ -128,7 +128,7 @@ def claim_poa_with_box(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -
poa_id,
)

@algopy.arc4.abimethod()
@algopy.public()
def claim_poa_with_box_ref(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
box_ref = algopy.Box(algopy.Bytes, key=algopy.Txn.sender.bytes)
poa_id, exists = box_ref.maybe()
Expand All @@ -148,7 +148,7 @@ def claim_poa_with_box_ref(self, opt_in_txn: algopy.gtxn.AssetTransferTransactio
algopy.op.btoi(poa_id),
)

@algopy.arc4.abimethod()
@algopy.public()
def claim_poa_with_box_map(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
poa_id, exists = self.box_map.maybe(algopy.Txn.sender.bytes)
assert exists, "POA not found, attendance validation failed!"
Expand Down
4 changes: 0 additions & 4 deletions examples/simple_voting/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
Txn,
UInt64,
op,
subroutine,
)

VOTE_PRICE = 10_000
Expand All @@ -24,11 +23,9 @@ def __init__(self) -> None:
)
self.voted = LocalState(UInt64, key="voted", description="Tracks if an account has voted")

@subroutine
def set_topic(self, topic: Bytes) -> None:
self.topic.value = topic

@subroutine
def vote(self, voter: Account) -> bool:
assert op.Global.group_size == UInt64(2)
assert op.GTxn.amount(1) == UInt64(VOTE_PRICE)
Expand All @@ -40,7 +37,6 @@ def vote(self, voter: Account) -> bool:
self.voted[voter] = UInt64(1)
return True

@subroutine
def get_votes(self) -> UInt64:
return self.votes.value

Expand Down
5 changes: 5 additions & 0 deletions src/_algopy_testing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# ruff: noqa: I001
from _algopy_testing import arc4, gtxn, itxn
from _algopy_testing.context import AlgopyTestContext
from _algopy_testing.context_helpers.context_storage import algopy_testing_context
Expand All @@ -24,6 +25,9 @@
from _algopy_testing.value_generators.arc4 import ARC4ValueGenerator
from _algopy_testing.value_generators.avm import AVMValueGenerator
from _algopy_testing.value_generators.txn import TxnValueGenerator
from _algopy_testing.decorators.arc4 import (
abimethod as public,
)

__all__ = [
"ARC4Contract",
Expand Down Expand Up @@ -60,6 +64,7 @@
"gtxn",
"itxn",
"logicsig",
"public",
"subroutine",
"uenumerate",
"urange",
Expand Down
5 changes: 5 additions & 0 deletions src/algopy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# ruff: noqa: I001
from _algopy_testing.compiled import (
CompiledContract,
CompiledLogicSig,
Expand Down Expand Up @@ -33,6 +34,9 @@
from _algopy_testing.utilities import OpUpFeeSource, ensure_budget, log, size_of

from . import arc4, gtxn, itxn, op
from _algopy_testing.decorators.arc4 import (
abimethod as public,
)

__all__ = [
"ARC4Contract",
Expand Down Expand Up @@ -76,6 +80,7 @@
"log",
"logicsig",
"op",
"public",
"size_of",
"subroutine",
"uenumerate",
Expand Down
4 changes: 2 additions & 2 deletions tests/arc4/test_abi_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import pytest
from _algopy_testing import AlgopyTestContext, algopy_testing_context
from _algopy_testing.itxn import ApplicationCallInnerTransaction
from algopy import ARC4Contract, arc4
from algopy import ARC4Contract, arc4, public
from pytest_mock import MockerFixture


class Logger(ARC4Contract):
@arc4.abimethod
@public
def echo(self, value: arc4.String) -> arc4.String:
return "echo: " + value

Expand Down
Loading