diff --git a/docs/coverage.md b/docs/coverage.md index db774574..42832917 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -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 | diff --git a/docs/testing-guide/transactions.md b/docs/testing-guide/transactions.md index 27805811..d5df1807 100644 --- a/docs/testing-guide/transactions.md +++ b/docs/testing-guide/transactions.md @@ -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. diff --git a/src/_algopy_testing/context_helpers/txn_context.py b/src/_algopy_testing/context_helpers/txn_context.py index 3e48a5ba..e2ab831c 100644 --- a/src/_algopy_testing/context_helpers/txn_context.py +++ b/src/_algopy_testing/context_helpers/txn_context.py @@ -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: diff --git a/src/_algopy_testing/itxn.py b/src/_algopy_testing/itxn.py index 97b5838d..fbc26e8b 100644 --- a/src/_algopy_testing/itxn.py +++ b/src/_algopy_testing/itxn.py @@ -33,6 +33,7 @@ "KeyRegistrationInnerTransaction", "Payment", "PaymentInnerTransaction", + "submit_staged", "submit_txns", ] @@ -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] @@ -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: diff --git a/tests/artifacts/DynamicITxnGroup/__init__.py b/tests/artifacts/DynamicITxnGroup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/artifacts/DynamicITxnGroup/contract.py b/tests/artifacts/DynamicITxnGroup/contract.py new file mode 100644 index 00000000..5f5e7e7d --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/contract.py @@ -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() diff --git a/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.approval.teal b/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.approval.teal new file mode 100644 index 00000000..0fb8792a --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.approval.teal @@ -0,0 +1,284 @@ +#pragma version 11 +#pragma typetrack false + +// algopy.arc4.ARC4Contract.approval_program() -> uint64: +main: + intcblock 1 0 32 6 + bytecblock 0x65a9aecc "abc" + // tests/artifacts/dynamic_itxn_group/contract.py:13 + // class DynamicItxnGroup(ARC4Contract): + txn NumAppArgs + bz main___algopy_default_create@9 + txn OnCompletion + ! + assert + txn ApplicationID + assert + pushbytess 0x17391784 0x3f10aea7 // method "test_firstly(address[],pay,uint64)void", method "test_looply(address[],pay,uint64)void" + txna ApplicationArgs 0 + match test_firstly test_looply + err + +main___algopy_default_create@9: + txn OnCompletion + ! + txn ApplicationID + ! + && + return + + +// tests.artifacts.dynamic_itxn_group.contract.DynamicItxnGroup.test_firstly[routing]() -> void: +test_firstly: + // tests/artifacts/dynamic_itxn_group/contract.py:14 + // @arc4.abimethod + txna ApplicationArgs 1 + txn GroupIndex + intc_0 // 1 + - + dup + gtxns TypeEnum + intc_0 // pay + == + assert // transaction type is pay + txna ApplicationArgs 2 + btoi + cover 2 + // tests/artifacts/dynamic_itxn_group/contract.py:18 + // assert funds.receiver == Global.current_application_address, "Funds must be sent to app" + dup + gtxns Receiver + global CurrentApplicationAddress + == + assert // Funds must be sent to app + // tests/artifacts/dynamic_itxn_group/contract.py:20 + // assert addresses.length, "must provide some accounts" + dig 1 + intc_1 // 0 + extract_uint16 // on error: invalid array length header + dup + cover 3 + dup + assert // must provide some accounts + // tests/artifacts/dynamic_itxn_group/contract.py:22 + // share = funds.amount // addresses.length + swap + gtxns Amount + swap + / + dup + cover 2 + // tests/artifacts/dynamic_itxn_group/contract.py:24 + // itxn.Payment(amount=share, receiver=addresses[0].native).stage(begin_group=True) + itxn_begin + dig 1 + extract 2 0 + cover 2 + swap + extract 2 32 + itxn_field Receiver + itxn_field Amount + intc_0 // pay + itxn_field TypeEnum + intc_1 // 0 + itxn_field Fee + // tests/artifacts/dynamic_itxn_group/contract.py:26 + // for i in urange(1, addresses.length): + intc_0 // 1 + +test_firstly_for_header@2: + // tests/artifacts/dynamic_itxn_group/contract.py:26 + // for i in urange(1, addresses.length): + dup + dig 4 + < + bz test_firstly_after_for@5 + // tests/artifacts/dynamic_itxn_group/contract.py:27 + // addr = addresses[i] + dupn 2 + intc_2 // 32 + * + dig 3 + swap + intc_2 // 32 + extract3 // on error: index access is out of bounds + // tests/artifacts/dynamic_itxn_group/contract.py:28 + // itxn.Payment(amount=share, receiver=addr.native).stage() + itxn_next + itxn_field Receiver + dig 3 + itxn_field Amount + intc_0 // pay + itxn_field TypeEnum + intc_1 // 0 + itxn_field Fee + // tests/artifacts/dynamic_itxn_group/contract.py:26 + // for i in urange(1, addresses.length): + intc_0 // 1 + + + bury 1 + b test_firstly_for_header@2 + +test_firstly_after_for@5: + // tests/artifacts/dynamic_itxn_group/contract.py:30-32 + // itxn.ApplicationCall( + // app_id=verifier.id, app_args=(arc4.arc4_signature("verify()void"),) + // ).stage() + itxn_next + // tests/artifacts/dynamic_itxn_group/contract.py:31 + // app_id=verifier.id, app_args=(arc4.arc4_signature("verify()void"),) + bytec_0 // method "verify()void" + itxn_field ApplicationArgs + dig 4 + itxn_field ApplicationID + // tests/artifacts/dynamic_itxn_group/contract.py:30 + // itxn.ApplicationCall( + intc_3 // appl + itxn_field TypeEnum + intc_1 // 0 + itxn_field Fee + // tests/artifacts/dynamic_itxn_group/contract.py:34 + // itxn.AssetConfig(asset_name="abc").stage() + itxn_next + bytec_1 // "abc" + itxn_field ConfigAssetName + pushint 3 // acfg + itxn_field TypeEnum + intc_1 // 0 + itxn_field Fee + // tests/artifacts/dynamic_itxn_group/contract.py:36 + // itxn.submit_staged() + itxn_submit + // tests/artifacts/dynamic_itxn_group/contract.py:14 + // @arc4.abimethod + intc_0 // 1 + return + + +// tests.artifacts.dynamic_itxn_group.contract.DynamicItxnGroup.test_looply[routing]() -> void: +test_looply: + intc_1 // 0 + // tests/artifacts/dynamic_itxn_group/contract.py:38 + // @arc4.abimethod + txna ApplicationArgs 1 + dup + txn GroupIndex + intc_0 // 1 + - + dup + gtxns TypeEnum + intc_0 // pay + == + assert // transaction type is pay + txna ApplicationArgs 2 + btoi + cover 2 + // tests/artifacts/dynamic_itxn_group/contract.py:45 + // assert funds.receiver == Global.current_application_address, "Funds must be sent to app" + dup + gtxns Receiver + global CurrentApplicationAddress + == + assert // Funds must be sent to app + // tests/artifacts/dynamic_itxn_group/contract.py:47 + // assert addresses.length, "must provide some accounts" + swap + intc_1 // 0 + extract_uint16 // on error: invalid array length header + dup + cover 2 + dup + assert // must provide some accounts + // tests/artifacts/dynamic_itxn_group/contract.py:49 + // share = funds.amount // addresses.length + swap + gtxns Amount + swap + / + // tests/artifacts/dynamic_itxn_group/contract.py:51 + // is_first = True + intc_0 // 1 + intc_1 // 0 + +test_looply_for_header@2: + // tests/artifacts/dynamic_itxn_group/contract.py:52 + // for addr in addresses: + dup + dig 4 + < + bz test_looply_after_for@8 + dig 5 + extract 2 0 + dig 1 + intc_2 // 32 + * + intc_2 // 32 + extract3 // on error: index access is out of bounds + bury 7 + // tests/artifacts/dynamic_itxn_group/contract.py:54 + // my_txn.stage(begin_group=is_first) + dig 1 + bz test_looply_itxn_next@5 + itxn_begin + +test_looply_after_itxn_begin_next@6: + dig 6 + itxn_field Receiver + dig 2 + itxn_field Amount + // tests/artifacts/dynamic_itxn_group/contract.py:53 + // my_txn = itxn.Payment(amount=share, receiver=addr.native) + intc_0 // pay + itxn_field TypeEnum + intc_1 // 0 + itxn_field Fee + // tests/artifacts/dynamic_itxn_group/contract.py:55 + // is_first = False + intc_1 // 0 + bury 2 + dup + intc_0 // 1 + + + bury 1 + b test_looply_for_header@2 + +test_looply_itxn_next@5: + // tests/artifacts/dynamic_itxn_group/contract.py:54 + // my_txn.stage(begin_group=is_first) + itxn_next + b test_looply_after_itxn_begin_next@6 + +test_looply_after_for@8: + // tests/artifacts/dynamic_itxn_group/contract.py:57-59 + // itxn.ApplicationCall( + // app_id=verifier.id, app_args=(arc4.arc4_signature("verify()void"),) + // ).stage() + itxn_next + // tests/artifacts/dynamic_itxn_group/contract.py:58 + // app_id=verifier.id, app_args=(arc4.arc4_signature("verify()void"),) + bytec_0 // method "verify()void" + itxn_field ApplicationArgs + dig 4 + itxn_field ApplicationID + // tests/artifacts/dynamic_itxn_group/contract.py:57 + // itxn.ApplicationCall( + intc_3 // appl + itxn_field TypeEnum + intc_1 // 0 + itxn_field Fee + // tests/artifacts/dynamic_itxn_group/contract.py:61 + // itxn.AssetConfig(asset_name="abc").stage() + itxn_next + bytec_1 // "abc" + itxn_field ConfigAssetName + pushint 3 // acfg + itxn_field TypeEnum + intc_1 // 0 + itxn_field Fee + // tests/artifacts/dynamic_itxn_group/contract.py:63 + // itxn.submit_staged() + itxn_submit + // tests/artifacts/dynamic_itxn_group/contract.py:38 + // @arc4.abimethod + intc_0 // 1 + return diff --git a/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.arc56.json b/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.arc56.json new file mode 100644 index 00000000..ad83e7f9 --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.arc56.json @@ -0,0 +1,161 @@ +{ + "name": "DynamicItxnGroup", + "structs": {}, + "methods": [ + { + "name": "test_firstly", + "args": [ + { + "type": "address[]", + "name": "addresses" + }, + { + "type": "pay", + "name": "funds" + }, + { + "type": "uint64", + "name": "verifier" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "test_looply", + "args": [ + { + "type": "address[]", + "name": "addresses" + }, + { + "type": "pay", + "name": "funds" + }, + { + "type": "uint64", + "name": "verifier" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + } + ], + "arcs": [ + 22, + 28 + ], + "networks": {}, + "state": { + "schema": { + "global": { + "ints": 0, + "bytes": 0 + }, + "local": { + "ints": 0, + "bytes": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [ + "NoOp" + ], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "pc": [ + 85, + 217 + ], + "errorMessage": "Funds must be sent to app" + }, + { + "pc": [ + 141, + 250 + ], + "errorMessage": "index access is out of bounds" + }, + { + "pc": [ + 89, + 220 + ], + "errorMessage": "invalid array length header" + }, + { + "pc": [ + 94, + 225 + ], + "errorMessage": "must provide some accounts" + }, + { + "pc": [ + 72, + 204 + ], + "errorMessage": "transaction type is pay" + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuYXBwcm92YWxfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIGludGNibG9jayAxIDAgMzIgNgogICAgYnl0ZWNibG9jayAweDY1YTlhZWNjICJhYmMiCiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjEzCiAgICAvLyBjbGFzcyBEeW5hbWljSXR4bkdyb3VwKEFSQzRDb250cmFjdCk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9fX2FsZ29weV9kZWZhdWx0X2NyZWF0ZUA5CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQKICAgIHB1c2hieXRlc3MgMHgxNzM5MTc4NCAweDNmMTBhZWE3IC8vIG1ldGhvZCAidGVzdF9maXJzdGx5KGFkZHJlc3NbXSxwYXksdWludDY0KXZvaWQiLCBtZXRob2QgInRlc3RfbG9vcGx5KGFkZHJlc3NbXSxwYXksdWludDY0KXZvaWQiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCB0ZXN0X2ZpcnN0bHkgdGVzdF9sb29wbHkKICAgIGVycgoKbWFpbl9fX2FsZ29weV9kZWZhdWx0X2NyZWF0ZUA5OgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgICEKICAgICYmCiAgICByZXR1cm4KCgovLyB0ZXN0cy5hcnRpZmFjdHMuZHluYW1pY19pdHhuX2dyb3VwLmNvbnRyYWN0LkR5bmFtaWNJdHhuR3JvdXAudGVzdF9maXJzdGx5W3JvdXRpbmddKCkgLT4gdm9pZDoKdGVzdF9maXJzdGx5OgogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weToxNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICB0eG4gR3JvdXBJbmRleAogICAgaW50Y18wIC8vIDEKICAgIC0KICAgIGR1cAogICAgZ3R4bnMgVHlwZUVudW0KICAgIGludGNfMCAvLyBwYXkKICAgID09CiAgICBhc3NlcnQgLy8gdHJhbnNhY3Rpb24gdHlwZSBpcyBwYXkKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIGNvdmVyIDIKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6MTgKICAgIC8vIGFzc2VydCBmdW5kcy5yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLCAiRnVuZHMgbXVzdCBiZSBzZW50IHRvIGFwcCIKICAgIGR1cAogICAgZ3R4bnMgUmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgYXNzZXJ0IC8vIEZ1bmRzIG11c3QgYmUgc2VudCB0byBhcHAKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6MjAKICAgIC8vIGFzc2VydCBhZGRyZXNzZXMubGVuZ3RoLCAibXVzdCBwcm92aWRlIHNvbWUgYWNjb3VudHMiCiAgICBkaWcgMQogICAgaW50Y18xIC8vIDAKICAgIGV4dHJhY3RfdWludDE2IC8vIG9uIGVycm9yOiBpbnZhbGlkIGFycmF5IGxlbmd0aCBoZWFkZXIKICAgIGR1cAogICAgY292ZXIgMwogICAgZHVwCiAgICBhc3NlcnQgLy8gbXVzdCBwcm92aWRlIHNvbWUgYWNjb3VudHMKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6MjIKICAgIC8vIHNoYXJlID0gZnVuZHMuYW1vdW50IC8vIGFkZHJlc3Nlcy5sZW5ndGgKICAgIHN3YXAKICAgIGd0eG5zIEFtb3VudAogICAgc3dhcAogICAgLwogICAgZHVwCiAgICBjb3ZlciAyCiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjI0CiAgICAvLyBpdHhuLlBheW1lbnQoYW1vdW50PXNoYXJlLCByZWNlaXZlcj1hZGRyZXNzZXNbMF0ubmF0aXZlKS5zdGFnZShiZWdpbl9ncm91cD1UcnVlKQogICAgaXR4bl9iZWdpbgogICAgZGlnIDEKICAgIGV4dHJhY3QgMiAwCiAgICBjb3ZlciAyCiAgICBzd2FwCiAgICBleHRyYWN0IDIgMzIKICAgIGl0eG5fZmllbGQgUmVjZWl2ZXIKICAgIGl0eG5fZmllbGQgQW1vdW50CiAgICBpbnRjXzAgLy8gcGF5CiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpbnRjXzEgLy8gMAogICAgaXR4bl9maWVsZCBGZWUKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6MjYKICAgIC8vIGZvciBpIGluIHVyYW5nZSgxLCBhZGRyZXNzZXMubGVuZ3RoKToKICAgIGludGNfMCAvLyAxCgp0ZXN0X2ZpcnN0bHlfZm9yX2hlYWRlckAyOgogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weToyNgogICAgLy8gZm9yIGkgaW4gdXJhbmdlKDEsIGFkZHJlc3Nlcy5sZW5ndGgpOgogICAgZHVwCiAgICBkaWcgNAogICAgPAogICAgYnogdGVzdF9maXJzdGx5X2FmdGVyX2ZvckA1CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBhZGRyID0gYWRkcmVzc2VzW2ldCiAgICBkdXBuIDIKICAgIGludGNfMiAvLyAzMgogICAgKgogICAgZGlnIDMKICAgIHN3YXAKICAgIGludGNfMiAvLyAzMgogICAgZXh0cmFjdDMgLy8gb24gZXJyb3I6IGluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjI4CiAgICAvLyBpdHhuLlBheW1lbnQoYW1vdW50PXNoYXJlLCByZWNlaXZlcj1hZGRyLm5hdGl2ZSkuc3RhZ2UoKQogICAgaXR4bl9uZXh0CiAgICBpdHhuX2ZpZWxkIFJlY2VpdmVyCiAgICBkaWcgMwogICAgaXR4bl9maWVsZCBBbW91bnQKICAgIGludGNfMCAvLyBwYXkKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMSAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weToyNgogICAgLy8gZm9yIGkgaW4gdXJhbmdlKDEsIGFkZHJlc3Nlcy5sZW5ndGgpOgogICAgaW50Y18wIC8vIDEKICAgICsKICAgIGJ1cnkgMQogICAgYiB0ZXN0X2ZpcnN0bHlfZm9yX2hlYWRlckAyCgp0ZXN0X2ZpcnN0bHlfYWZ0ZXJfZm9yQDU6CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjMwLTMyCiAgICAvLyBpdHhuLkFwcGxpY2F0aW9uQ2FsbCgKICAgIC8vICAgICBhcHBfaWQ9dmVyaWZpZXIuaWQsIGFwcF9hcmdzPShhcmM0LmFyYzRfc2lnbmF0dXJlKCJ2ZXJpZnkoKXZvaWQiKSwpCiAgICAvLyApLnN0YWdlKCkKICAgIGl0eG5fbmV4dAogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weTozMQogICAgLy8gYXBwX2lkPXZlcmlmaWVyLmlkLCBhcHBfYXJncz0oYXJjNC5hcmM0X3NpZ25hdHVyZSgidmVyaWZ5KCl2b2lkIiksKQogICAgYnl0ZWNfMCAvLyBtZXRob2QgInZlcmlmeSgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBkaWcgNAogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjMwCiAgICAvLyBpdHhuLkFwcGxpY2F0aW9uQ2FsbCgKICAgIGludGNfMyAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpbnRjXzEgLy8gMAogICAgaXR4bl9maWVsZCBGZWUKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6MzQKICAgIC8vIGl0eG4uQXNzZXRDb25maWcoYXNzZXRfbmFtZT0iYWJjIikuc3RhZ2UoKQogICAgaXR4bl9uZXh0CiAgICBieXRlY18xIC8vICJhYmMiCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0TmFtZQogICAgcHVzaGludCAzIC8vIGFjZmcKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMSAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weTozNgogICAgLy8gaXR4bi5zdWJtaXRfc3RhZ2VkKCkKICAgIGl0eG5fc3VibWl0CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjE0CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGludGNfMCAvLyAxCiAgICByZXR1cm4KCgovLyB0ZXN0cy5hcnRpZmFjdHMuZHluYW1pY19pdHhuX2dyb3VwLmNvbnRyYWN0LkR5bmFtaWNJdHhuR3JvdXAudGVzdF9sb29wbHlbcm91dGluZ10oKSAtPiB2b2lkOgp0ZXN0X2xvb3BseToKICAgIGludGNfMSAvLyAwCiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjM4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGR1cAogICAgdHhuIEdyb3VwSW5kZXgKICAgIGludGNfMCAvLyAxCiAgICAtCiAgICBkdXAKICAgIGd0eG5zIFR5cGVFbnVtCiAgICBpbnRjXzAgLy8gcGF5CiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgcGF5CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICBjb3ZlciAyCiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjQ1CiAgICAvLyBhc3NlcnQgZnVuZHMucmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywgIkZ1bmRzIG11c3QgYmUgc2VudCB0byBhcHAiCiAgICBkdXAKICAgIGd0eG5zIFJlY2VpdmVyCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgPT0KICAgIGFzc2VydCAvLyBGdW5kcyBtdXN0IGJlIHNlbnQgdG8gYXBwCiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjQ3CiAgICAvLyBhc3NlcnQgYWRkcmVzc2VzLmxlbmd0aCwgIm11c3QgcHJvdmlkZSBzb21lIGFjY291bnRzIgogICAgc3dhcAogICAgaW50Y18xIC8vIDAKICAgIGV4dHJhY3RfdWludDE2IC8vIG9uIGVycm9yOiBpbnZhbGlkIGFycmF5IGxlbmd0aCBoZWFkZXIKICAgIGR1cAogICAgY292ZXIgMgogICAgZHVwCiAgICBhc3NlcnQgLy8gbXVzdCBwcm92aWRlIHNvbWUgYWNjb3VudHMKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6NDkKICAgIC8vIHNoYXJlID0gZnVuZHMuYW1vdW50IC8vIGFkZHJlc3Nlcy5sZW5ndGgKICAgIHN3YXAKICAgIGd0eG5zIEFtb3VudAogICAgc3dhcAogICAgLwogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weTo1MQogICAgLy8gaXNfZmlyc3QgPSBUcnVlCiAgICBpbnRjXzAgLy8gMQogICAgaW50Y18xIC8vIDAKCnRlc3RfbG9vcGx5X2Zvcl9oZWFkZXJAMjoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6NTIKICAgIC8vIGZvciBhZGRyIGluIGFkZHJlc3NlczoKICAgIGR1cAogICAgZGlnIDQKICAgIDwKICAgIGJ6IHRlc3RfbG9vcGx5X2FmdGVyX2ZvckA4CiAgICBkaWcgNQogICAgZXh0cmFjdCAyIDAKICAgIGRpZyAxCiAgICBpbnRjXzIgLy8gMzIKICAgICoKICAgIGludGNfMiAvLyAzMgogICAgZXh0cmFjdDMgLy8gb24gZXJyb3I6IGluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidXJ5IDcKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6NTQKICAgIC8vIG15X3R4bi5zdGFnZShiZWdpbl9ncm91cD1pc19maXJzdCkKICAgIGRpZyAxCiAgICBieiB0ZXN0X2xvb3BseV9pdHhuX25leHRANQogICAgaXR4bl9iZWdpbgoKdGVzdF9sb29wbHlfYWZ0ZXJfaXR4bl9iZWdpbl9uZXh0QDY6CiAgICBkaWcgNgogICAgaXR4bl9maWVsZCBSZWNlaXZlcgogICAgZGlnIDIKICAgIGl0eG5fZmllbGQgQW1vdW50CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjUzCiAgICAvLyBteV90eG4gPSBpdHhuLlBheW1lbnQoYW1vdW50PXNoYXJlLCByZWNlaXZlcj1hZGRyLm5hdGl2ZSkKICAgIGludGNfMCAvLyBwYXkKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMSAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weTo1NQogICAgLy8gaXNfZmlyc3QgPSBGYWxzZQogICAgaW50Y18xIC8vIDAKICAgIGJ1cnkgMgogICAgZHVwCiAgICBpbnRjXzAgLy8gMQogICAgKwogICAgYnVyeSAxCiAgICBiIHRlc3RfbG9vcGx5X2Zvcl9oZWFkZXJAMgoKdGVzdF9sb29wbHlfaXR4bl9uZXh0QDU6CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjU0CiAgICAvLyBteV90eG4uc3RhZ2UoYmVnaW5fZ3JvdXA9aXNfZmlyc3QpCiAgICBpdHhuX25leHQKICAgIGIgdGVzdF9sb29wbHlfYWZ0ZXJfaXR4bl9iZWdpbl9uZXh0QDYKCnRlc3RfbG9vcGx5X2FmdGVyX2ZvckA4OgogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weTo1Ny01OQogICAgLy8gaXR4bi5BcHBsaWNhdGlvbkNhbGwoCiAgICAvLyAgICAgYXBwX2lkPXZlcmlmaWVyLmlkLCBhcHBfYXJncz0oYXJjNC5hcmM0X3NpZ25hdHVyZSgidmVyaWZ5KCl2b2lkIiksKQogICAgLy8gKS5zdGFnZSgpCiAgICBpdHhuX25leHQKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6NTgKICAgIC8vIGFwcF9pZD12ZXJpZmllci5pZCwgYXBwX2FyZ3M9KGFyYzQuYXJjNF9zaWduYXR1cmUoInZlcmlmeSgpdm9pZCIpLCkKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJ2ZXJpZnkoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgZGlnIDQKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weTo1NwogICAgLy8gaXR4bi5BcHBsaWNhdGlvbkNhbGwoCiAgICBpbnRjXzMgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaW50Y18xIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL2NvbnRyYWN0LnB5OjYxCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKGFzc2V0X25hbWU9ImFiYyIpLnN0YWdlKCkKICAgIGl0eG5fbmV4dAogICAgYnl0ZWNfMSAvLyAiYWJjIgogICAgaXR4bl9maWVsZCBDb25maWdBc3NldE5hbWUKICAgIHB1c2hpbnQgMyAvLyBhY2ZnCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpbnRjXzEgLy8gMAogICAgaXR4bl9maWVsZCBGZWUKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvY29udHJhY3QucHk6NjMKICAgIC8vIGl0eG4uc3VibWl0X3N0YWdlZCgpCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC9jb250cmFjdC5weTozOAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBpbnRjXzAgLy8gMQogICAgcmV0dXJuCg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMQogICAgcmV0dXJuCg==" + }, + "byteCode": { + "approval": "CyAEAQAgBiYCBGWprswDYWJjMRtBAB0xGRREMRhEggIEFzkXhAQ/EK6nNhoAjgIACQCLADEZFDEYFBBDNhoBMRYiCUk4ECISRDYaAhdOAkk4BzIKEkRLASNZSU4DSURMOAhMCklOArFLAVcCAE4CTFcCILIHsggishAjsgEiSUsEDEEAHUcCJAtLA0wkWLayB0sDsggishAjsgEiCEUBQv/ctiiyGksEshglshAjsgG2KbImgQOyECOyAbMiQyM2GgFJMRYiCUk4ECISRDYaAhdOAkk4BzIKEkRMI1lJTgJJREw4CEwKIiNJSwQMQQAwSwVXAgBLASQLJFhFB0sBQQAasUsGsgdLArIIIrIQI7IBI0UCSSIIRQFC/822Qv/jtiiyGksEshglshAjsgG2KbImgQOyECOyAbMiQw==", + "clear": "C4EBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 5, + "minor": 5, + "patch": 0 + } + }, + "events": [], + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.clear.teal b/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.clear.teal new file mode 100644 index 00000000..75f539be --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/data/DynamicItxnGroup.clear.teal @@ -0,0 +1,7 @@ +#pragma version 11 +#pragma typetrack false + +// algopy.arc4.ARC4Contract.clear_state_program() -> uint64: +main: + pushint 1 + return diff --git a/tests/artifacts/DynamicITxnGroup/data/VerifierContract.approval.teal b/tests/artifacts/DynamicITxnGroup/data/VerifierContract.approval.teal new file mode 100644 index 00000000..27bef149 --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/data/VerifierContract.approval.teal @@ -0,0 +1,66 @@ +#pragma version 11 +#pragma typetrack false + +// algopy.arc4.ARC4Contract.approval_program() -> uint64: +main: + // tests/artifacts/dynamic_itxn_group/verifier.py:11 + // class VerifierContract(ARC4Contract): + txn NumAppArgs + bz main___algopy_default_create@5 + pushbytes 0x65a9aecc // method "verify()void" + txna ApplicationArgs 0 + match main_verify_route@3 + err + +main_verify_route@3: + // tests/artifacts/dynamic_itxn_group/verifier.py:12 + // @arc4.abimethod + txn OnCompletion + ! + txn ApplicationID + && + assert + b verify + +main___algopy_default_create@5: + txn OnCompletion + ! + txn ApplicationID + ! + && + return + + +// tests.artifacts.dynamic_itxn_group.verifier.VerifierContract.verify[routing]() -> void: +verify: + // tests/artifacts/dynamic_itxn_group/verifier.py:14 + // for i in urange(Txn.group_index): + txn GroupIndex + pushint 0 + +verify_for_header@2: + // tests/artifacts/dynamic_itxn_group/verifier.py:14 + // for i in urange(Txn.group_index): + dup + dig 2 + < + bz verify_after_for@5 + // tests/artifacts/dynamic_itxn_group/verifier.py:16 + // assert txn.type == TransactionType.Payment, "Txn must be pay" + dupn 2 + gtxns TypeEnum + pushint 1 // pay + == + assert // Txn must be pay + // tests/artifacts/dynamic_itxn_group/verifier.py:14 + // for i in urange(Txn.group_index): + pushint 1 + + + bury 1 + b verify_for_header@2 + +verify_after_for@5: + // tests/artifacts/dynamic_itxn_group/verifier.py:12 + // @arc4.abimethod + pushint 1 + return diff --git a/tests/artifacts/DynamicITxnGroup/data/VerifierContract.arc56.json b/tests/artifacts/DynamicITxnGroup/data/VerifierContract.arc56.json new file mode 100644 index 00000000..998b91e0 --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/data/VerifierContract.arc56.json @@ -0,0 +1,90 @@ +{ + "name": "VerifierContract", + "structs": {}, + "methods": [ + { + "name": "verify", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + } + ], + "arcs": [ + 22, + 28 + ], + "networks": {}, + "state": { + "schema": { + "global": { + "ints": 0, + "bytes": 0 + }, + "local": { + "ints": 0, + "bytes": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [ + "NoOp" + ], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "pc": [ + 56 + ], + "errorMessage": "Txn must be pay" + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuYXBwcm92YWxfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvdmVyaWZpZXIucHk6MTEKICAgIC8vIGNsYXNzIFZlcmlmaWVyQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4biBOdW1BcHBBcmdzCiAgICBieiBtYWluX19fYWxnb3B5X2RlZmF1bHRfY3JlYXRlQDUKICAgIHB1c2hieXRlcyAweDY1YTlhZWNjIC8vIG1ldGhvZCAidmVyaWZ5KCl2b2lkIgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAogICAgbWF0Y2ggbWFpbl92ZXJpZnlfcm91dGVAMwogICAgZXJyCgptYWluX3ZlcmlmeV9yb3V0ZUAzOgogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC92ZXJpZmllci5weToxMgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgJiYKICAgIGFzc2VydAogICAgYiB2ZXJpZnkKCm1haW5fX19hbGdvcHlfZGVmYXVsdF9jcmVhdGVANToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICAmJgogICAgcmV0dXJuCgoKLy8gdGVzdHMuYXJ0aWZhY3RzLmR5bmFtaWNfaXR4bl9ncm91cC52ZXJpZmllci5WZXJpZmllckNvbnRyYWN0LnZlcmlmeVtyb3V0aW5nXSgpIC0+IHZvaWQ6CnZlcmlmeToKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvdmVyaWZpZXIucHk6MTQKICAgIC8vIGZvciBpIGluIHVyYW5nZShUeG4uZ3JvdXBfaW5kZXgpOgogICAgdHhuIEdyb3VwSW5kZXgKICAgIHB1c2hpbnQgMAoKdmVyaWZ5X2Zvcl9oZWFkZXJAMjoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9keW5hbWljX2l0eG5fZ3JvdXAvdmVyaWZpZXIucHk6MTQKICAgIC8vIGZvciBpIGluIHVyYW5nZShUeG4uZ3JvdXBfaW5kZXgpOgogICAgZHVwCiAgICBkaWcgMgogICAgPAogICAgYnogdmVyaWZ5X2FmdGVyX2ZvckA1CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL3ZlcmlmaWVyLnB5OjE2CiAgICAvLyBhc3NlcnQgdHhuLnR5cGUgPT0gVHJhbnNhY3Rpb25UeXBlLlBheW1lbnQsICJUeG4gbXVzdCBiZSBwYXkiCiAgICBkdXBuIDIKICAgIGd0eG5zIFR5cGVFbnVtCiAgICBwdXNoaW50IDEgLy8gcGF5CiAgICA9PQogICAgYXNzZXJ0IC8vIFR4biBtdXN0IGJlIHBheQogICAgLy8gdGVzdHMvYXJ0aWZhY3RzL2R5bmFtaWNfaXR4bl9ncm91cC92ZXJpZmllci5weToxNAogICAgLy8gZm9yIGkgaW4gdXJhbmdlKFR4bi5ncm91cF9pbmRleCk6CiAgICBwdXNoaW50IDEKICAgICsKICAgIGJ1cnkgMQogICAgYiB2ZXJpZnlfZm9yX2hlYWRlckAyCgp2ZXJpZnlfYWZ0ZXJfZm9yQDU6CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvZHluYW1pY19pdHhuX2dyb3VwL3ZlcmlmaWVyLnB5OjEyCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHB1c2hpbnQgMQogICAgcmV0dXJuCg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMQogICAgcmV0dXJuCg==" + }, + "byteCode": { + "approval": "CzEbQQAYgARlqa7MNhoAjgEAAQAxGRQxGBBEQgAIMRkUMRgUEEMxFoEASUsCDEEAEEcCOBCBARJEgQEIRQFC/+mBAUM=", + "clear": "C4EBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 5, + "minor": 5, + "patch": 0 + } + }, + "events": [], + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/artifacts/DynamicITxnGroup/data/VerifierContract.clear.teal b/tests/artifacts/DynamicITxnGroup/data/VerifierContract.clear.teal new file mode 100644 index 00000000..75f539be --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/data/VerifierContract.clear.teal @@ -0,0 +1,7 @@ +#pragma version 11 +#pragma typetrack false + +// algopy.arc4.ARC4Contract.clear_state_program() -> uint64: +main: + pushint 1 + return diff --git a/tests/artifacts/DynamicITxnGroup/verifier.py b/tests/artifacts/DynamicITxnGroup/verifier.py new file mode 100644 index 00000000..7ff1e0f0 --- /dev/null +++ b/tests/artifacts/DynamicITxnGroup/verifier.py @@ -0,0 +1,16 @@ +from algopy import ( + ARC4Contract, + TransactionType, + Txn, + arc4, + gtxn, + urange, +) + + +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" diff --git a/tests/dynamic_itxn_group/__init__.py b/tests/dynamic_itxn_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/dynamic_itxn_group/test_dynamic_itxn_group.py b/tests/dynamic_itxn_group/test_dynamic_itxn_group.py new file mode 100644 index 00000000..9839f6ec --- /dev/null +++ b/tests/dynamic_itxn_group/test_dynamic_itxn_group.py @@ -0,0 +1,67 @@ +from collections.abc import Generator + +import pytest +from _algopy_testing import arc4 +from _algopy_testing.context import AlgopyTestContext +from _algopy_testing.context_helpers.context_storage import algopy_testing_context +from _algopy_testing.primitives import UInt64 +from _algopy_testing.primitives.array import Array + +from tests.artifacts.DynamicITxnGroup.contract import DynamicItxnGroup +from tests.artifacts.DynamicITxnGroup.verifier import VerifierContract + + +@pytest.fixture() +def context() -> Generator[AlgopyTestContext, None, None]: + with algopy_testing_context() as ctx: + yield ctx + + +def test_firstly(context: AlgopyTestContext) -> None: + verifier = VerifierContract() + dynamic_itxn_group = DynamicItxnGroup() + + verifier_app = context.ledger.get_app(verifier) + dynamic_itxn_group_app = context.ledger.get_app(dynamic_itxn_group) + + test_accounts = [context.any.account() for _ in range(3)] + + addresses = Array([arc4.Address(a) for a in test_accounts]) + payment = context.any.txn.payment( + amount=UInt64(9), + receiver=dynamic_itxn_group_app.address, + ) + dynamic_itxn_group.test_firstly(addresses, payment, verifier_app) + + 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" + + +def test_looply( + context: AlgopyTestContext, +) -> None: + verifier = VerifierContract() + dynamic_itxn_group = DynamicItxnGroup() + + verifier_app = context.ledger.get_app(verifier) + dynamic_itxn_group_app = context.ledger.get_app(dynamic_itxn_group) + + test_accounts = [context.any.account() for _ in range(3)] + + addresses = Array([arc4.Address(a) for a in test_accounts]) + payment = context.any.txn.payment( + amount=UInt64(9), + receiver=dynamic_itxn_group_app.address, + ) + dynamic_itxn_group.test_looply(addresses, payment, verifier_app) + + 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"