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 @@ -95,6 +95,7 @@ See which `algorand-python` stubs are implemented by the `algorand-python-testin
| algopy.itxn.KeyRegistrationInnerTransaction | Emulated |
| algopy.itxn.Payment | Emulated |
| algopy.itxn.PaymentInnerTransaction | Emulated |
| algopy.itxn.submit_staged | Emulated |
| algopy.itxn.submit_txns | Emulated |
| algopy.op.Base64 | Native |
| algopy.op.EC | Native |
Expand Down
69 changes: 69 additions & 0 deletions docs/testing-guide/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,75 @@ To access the submitted inner transactions:

These methods provide type validation and will raise an error if the requested transaction type doesn't match the actual type of the inner transaction.

### Submitting a group with dynamic number of inner transactions

`algorand-python` supports composing inner transaction groups with a dynamic number of transactions. To use this feature, call the `.stage()` method on inner transaction classes to queue transactions, then call `algopy.itxn.submit_staged()` to submit all staged transactions as a group.

The following example demonstrates how to test this functionality using the `algorand-python-testing` package.

```{testcode}
from algopy import Application, ARC4Contract, Array, Global, arc4, gtxn, itxn, TransactionType, Txn, UInt64, urange

class DynamicItxnGroup(ARC4Contract):
@arc4.abimethod
def distribute(
self, addresses: Array[arc4.Address], funds: gtxn.PaymentTransaction, verifier: Application
) -> None:
assert funds.receiver == Global.current_application_address, "Funds must be sent to app"

assert addresses.length, "must provide some accounts"

share = funds.amount // addresses.length

itxn.Payment(amount=share, receiver=addresses[0].native).stage(begin_group=True)

for i in urange(1, addresses.length):
addr = addresses[i]
itxn.Payment(amount=share, receiver=addr.native).stage()

itxn.ApplicationCall(
app_id=verifier.id, app_args=(arc4.arc4_signature("verify()void"),)
).stage()

itxn.AssetConfig(asset_name="abc").stage()

itxn.submit_staged()

class VerifierContract(ARC4Contract):
@arc4.abimethod
def verify(self) -> None:
for i in urange(Txn.group_index):
txn = gtxn.Transaction(i)
assert txn.type == TransactionType.Payment, "Txn must be pay"


# create contract instaces
verifier = VerifierContract()
dynamic_itxn_group = DynamicItxnGroup()

# get application id for contract instances
verifier_app = context.ledger.get_app(verifier)
dynamic_itxn_group_app = context.ledger.get_app(dynamic_itxn_group)

# create test accounts to distribute funds to and initial fund
addresses = Array([arc4.Address(context.any.account()) for _ in range(3)])
payment = context.any.txn.payment(
amount=UInt64(9),
receiver=dynamic_itxn_group_app.address,
)

# call contract method which creates inner transactions according to number of addresses passed in
dynamic_itxn_group.distribute(addresses, payment, verifier_app)

# get inner transaction group to assert the details
itxns = context.txn.last_group.get_itxn_group(-1)
assert len(itxns) == 5
for i in range(3):
assert itxns.payment(i).amount == 3
assert itxns.application_call(3).app_id == verifier_app
assert itxns.asset_config(4).asset_name == b"abc"
```

## References

- [API](../api.md) for more details on the test context manager and inner transactions related methods that perform implicit inner transaction type validation.
Expand Down
8 changes: 4 additions & 4 deletions src/_algopy_testing/context_helpers/txn_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,19 +326,19 @@ def _get_index(self, txn: algopy.gtxn.TransactionBase) -> int:
except ValueError:
raise ValueError("Transaction is not part of this group") from None

def _begin_itxn_group(self) -> None:
def _begin_itxn_group(self, itxn: InnerTransaction | None = None) -> None:
if self._constructing_itxn_group:
raise RuntimeError("itxn begin without itxn submit")

if self.active_txn.on_completion == OnCompleteAction.ClearState:
raise RuntimeError("Cannot begin inner transaction group in a clear state call")

self._constructing_itxn_group.append(InnerTransaction())
self._constructing_itxn_group.append(itxn or InnerTransaction())

def _append_itxn_group(self) -> None:
def _append_itxn_group(self, itxn: InnerTransaction | None = None) -> None:
if not self._constructing_itxn_group:
raise RuntimeError("itxn next without itxn begin")
self._constructing_itxn_group.append(InnerTransaction())
self._constructing_itxn_group.append(itxn or InnerTransaction())

def _submit_itxn_group(self) -> None:
if not self._constructing_itxn_group:
Expand Down
11 changes: 11 additions & 0 deletions src/_algopy_testing/itxn.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"KeyRegistrationInnerTransaction",
"Payment",
"PaymentInnerTransaction",
"submit_staged",
"submit_txns",
]

Expand Down Expand Up @@ -113,6 +114,12 @@ def set(self, **fields: typing.Any) -> None:
_narrow_covariant_types(fields)
self.fields.update(fields)

def stage(self, *, begin_group: bool = False) -> None:
if begin_group:
lazy_context.active_group._begin_itxn_group(self) # type: ignore[arg-type]
else:
lazy_context.active_group._append_itxn_group(self) # type: ignore[arg-type]

def submit(self) -> _TResult_co:
result = _get_itxn_result(self)
lazy_context.active_group._add_itxn_group([result]) # type: ignore[list-item]
Expand Down Expand Up @@ -170,6 +177,10 @@ def submit_txns(
return results


def submit_staged() -> None:
lazy_context.active_group._submit_itxn_group()


def _get_itxn_result(
itxn: _BaseInnerTransactionFields[_TResult_co],
) -> _BaseInnerTransactionResult:
Expand Down
Empty file.
63 changes: 63 additions & 0 deletions tests/artifacts/DynamicITxnGroup/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from algopy import (
Application,
ARC4Contract,
Array,
Global,
arc4,
gtxn,
itxn,
urange,
)


class DynamicItxnGroup(ARC4Contract):
@arc4.abimethod
def test_firstly(
self, addresses: Array[arc4.Address], funds: gtxn.PaymentTransaction, verifier: Application
) -> None:
assert funds.receiver == Global.current_application_address, "Funds must be sent to app"

assert addresses.length, "must provide some accounts"

share = funds.amount // addresses.length

itxn.Payment(amount=share, receiver=addresses[0].native).stage(begin_group=True)

for i in urange(1, addresses.length):
addr = addresses[i]
itxn.Payment(amount=share, receiver=addr.native).stage()

itxn.ApplicationCall(
app_id=verifier.id, app_args=(arc4.arc4_signature("verify()void"),)
).stage()

itxn.AssetConfig(asset_name="abc").stage()

itxn.submit_staged()

@arc4.abimethod
def test_looply(
self,
addresses: Array[arc4.Address],
funds: gtxn.PaymentTransaction,
verifier: Application,
) -> None:
assert funds.receiver == Global.current_application_address, "Funds must be sent to app"

assert addresses.length, "must provide some accounts"

share = funds.amount // addresses.length

is_first = True
for addr in addresses:
my_txn = itxn.Payment(amount=share, receiver=addr.native)
my_txn.stage(begin_group=is_first)
is_first = False

itxn.ApplicationCall(
app_id=verifier.id, app_args=(arc4.arc4_signature("verify()void"),)
).stage()

itxn.AssetConfig(asset_name="abc").stage()

itxn.submit_staged()
Loading