diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff7c84a7..3552e82e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Unreleased +# 0.20.0 + +## Added +* adding program page related ops ([#412](https://github.com/algorand/pyteal/pull/412)) +* Add Replace ([#413](https://github.com/algorand/pyteal/pull/413)) +* Add Block ([#415](https://github.com/algorand/pyteal/pull/415)) +* Add JsonRef ([#417](https://github.com/algorand/pyteal/pull/417)) +* Add Base64Decode ([#418](https://github.com/algorand/pyteal/pull/418)) +* Support Secp256r1 curve ([#423](https://github.com/algorand/pyteal/pull/423)) +* Add VrfVerify ([#419](https://github.com/algorand/pyteal/pull/419)) +* Add Sha3_256 ([#425](https://github.com/algorand/pyteal/pull/425)) +* Support FirstValidTime transaction field ([#424](https://github.com/algorand/pyteal/pull/424)) +* Add Ed25519Verify_Bare ([#426](https://github.com/algorand/pyteal/pull/426)) +* AVM Boxes Ops in Pyteal ([#438](https://github.com/algorand/pyteal/pull/438)) +* Support new AVM 8 account parameters ([#555](https://github.com/algorand/pyteal/pull/555)) + +## Changed +* Changes to avm8 docs ([#546](https://github.com/algorand/pyteal/pull/546)) + # 0.19.0 ## Added diff --git a/docs/state.rst b/docs/state.rst index 71f1c638f..b45a5a0c6 100644 --- a/docs/state.rst +++ b/docs/state.rst @@ -7,20 +7,30 @@ PyTeal can be used to write `Stateful Algorand Smart Contracts None: + self.id = id + self.arg_name = name + self.type = type + self.min_version = min_version + + def type_of(self) -> TealType: + return self.type + + +AccountParamField.__module__ = "pyteal" + class AccountParam: + @staticmethod + def __makeAccountParamExpr(field: AccountParamField, acct: Expr) -> MaybeValue: + require_type(acct, TealType.anytype) + + def field_and_program_version_check(options: "CompileOptions"): + verifyProgramVersion( + minVersion=Op.acct_params_get.min_version, + version=options.version, + msg=f"{Op.acct_params_get.value} unavailable", + ) + verifyFieldVersion(field.arg_name, field.min_version, options.version) + + return MaybeValue( + Op.acct_params_get, + field.type_of(), + immediate_args=[field.arg_name], + args=[acct], + compile_check=field_and_program_version_check, + ) + @classmethod def balance(cls, acct: Expr) -> MaybeValue: """Get the current balance in microalgos an account. @@ -15,13 +70,7 @@ def balance(cls, acct: Expr) -> MaybeValue: acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. May evaluate to uint64 or an address. """ - require_type(acct, TealType.anytype) - return MaybeValue( - Op.acct_params_get, - TealType.uint64, - immediate_args=["AcctBalance"], - args=[acct], - ) + return cls.__makeAccountParamExpr(AccountParamField.balance, acct) @classmethod def minBalance(cls, acct: Expr) -> MaybeValue: @@ -31,13 +80,7 @@ def minBalance(cls, acct: Expr) -> MaybeValue: acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. May evaluate to uint64 or an address. """ - require_type(acct, TealType.anytype) - return MaybeValue( - Op.acct_params_get, - TealType.uint64, - immediate_args=["AcctMinBalance"], - args=[acct], - ) + return cls.__makeAccountParamExpr(AccountParamField.min_balance, acct) @classmethod def authAddr(cls, acct: Expr) -> MaybeValue: @@ -47,13 +90,115 @@ def authAddr(cls, acct: Expr) -> MaybeValue: acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. May evaluate to uint64 or an address. """ - require_type(acct, TealType.anytype) - return MaybeValue( - Op.acct_params_get, - TealType.bytes, - immediate_args=["AcctAuthAddr"], - args=[acct], - ) + return cls.__makeAccountParamExpr(AccountParamField.auth_addr, acct) + + @classmethod + def totalNumUint(cls, acct: Expr) -> MaybeValue: + """Get the total number of uint64 values allocated by the account in Global and Local States. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_num_uint, acct) + + @classmethod + def totalNumByteSlice(cls, acct: Expr) -> MaybeValue: + """Get the total number of byte array values allocated by the account in Global and Local States. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_num_byte_slice, acct) + + @classmethod + def totalExtraAppPages(cls, acct: Expr) -> MaybeValue: + """Get the number of extra app code pages used by the account. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_extra_app_pages, acct) + + @classmethod + def totalAppsCreated(cls, acct: Expr) -> MaybeValue: + """Get the number of existing apps created by the account. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_apps_created, acct) + + @classmethod + def totalAppsOptedIn(cls, acct: Expr) -> MaybeValue: + """Get the number of apps the account is opted into. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_apps_opted_in, acct) + + @classmethod + def totalAssetsCreated(cls, acct: Expr) -> MaybeValue: + """Get the number of existing ASAs created by the account. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_assets_created, acct) + + @classmethod + def totalAssets(cls, acct: Expr) -> MaybeValue: + """Get the number of ASAs held by the account (including ASAs the account created). + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_assets, acct) + + @classmethod + def totalBoxes(cls, acct: Expr) -> MaybeValue: + """Get the number of existing boxes created by the account's app. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_boxes, acct) + + @classmethod + def totalBoxBytes(cls, acct: Expr) -> MaybeValue: + """Get the total number of bytes used by the account's app's box keys and values. + + Requires program version 8 or higher. + + Args: + acct: An index into Txn.accounts that corresponds to the application to check or an address available at runtime. + May evaluate to uint64 or an address. + """ + return cls.__makeAccountParamExpr(AccountParamField.total_box_bytes, acct) AccountParam.__module__ = "pyteal" @@ -85,5 +230,41 @@ def auth_address(self) -> MaybeValue: If the account is not rekeyed, the empty address is returned.""" return AccountParam.authAddr(self._account) + def total_num_uint(self) -> MaybeValue: + """Get the total number of uint64 values allocated by the account in Global and Local States.""" + return AccountParam.totalNumUint(self._account) + + def total_num_byte_slice(self) -> MaybeValue: + """Get the total number of byte array values allocated by the account in Global and Local States.""" + return AccountParam.totalNumByteSlice(self._account) + + def total_extra_app_pages(self) -> MaybeValue: + """Get the number of extra app code pages used by the account.""" + return AccountParam.totalExtraAppPages(self._account) + + def total_apps_created(self) -> MaybeValue: + """Get the number of existing apps created by the account.""" + return AccountParam.totalAppsCreated(self._account) + + def total_apps_opted_in(self) -> MaybeValue: + """Get the number of apps the account is opted into.""" + return AccountParam.totalAppsOptedIn(self._account) + + def total_assets_created(self) -> MaybeValue: + """Get the number of existing ASAs created by the account.""" + return AccountParam.totalAssetsCreated(self._account) + + def total_assets(self) -> MaybeValue: + """Get the number of ASAs held by the account (including ASAs the account created).""" + return AccountParam.totalAssets(self._account) + + def total_boxes(self) -> MaybeValue: + """Get the number of existing boxes created by the account's app.""" + return AccountParam.totalBoxes(self._account) + + def total_box_bytes(self) -> MaybeValue: + """Get the total number of bytes used by the account's app's box keys and values.""" + return AccountParam.totalBoxBytes(self._account) + AccountParamObject.__module__ = "pyteal" diff --git a/pyteal/ast/acct_test.py b/pyteal/ast/acct_test.py index e4f12f958..8259f8ac6 100644 --- a/pyteal/ast/acct_test.py +++ b/pyteal/ast/acct_test.py @@ -1,79 +1,89 @@ +import pytest + import pyteal as pt +from pyteal.ast.acct import AccountParamField from pyteal.ast.maybe_test import assert_MaybeValue_equality -options = pt.CompileOptions() -avm4Options = pt.CompileOptions(version=4) -avm5Options = pt.CompileOptions(version=5) avm6Options = pt.CompileOptions(version=6) +avm8Options = pt.CompileOptions(version=8) + + +@pytest.mark.parametrize( + "method_name,field_name", + [ + ("balance", "balance"), + ("minBalance", "min_balance"), + ("authAddr", "auth_addr"), + ("totalNumUint", "total_num_uint"), + ("totalNumByteSlice", "total_num_byte_slice"), + ("totalExtraAppPages", "total_extra_app_pages"), + ("totalAppsCreated", "total_apps_created"), + ("totalAppsOptedIn", "total_apps_opted_in"), + ("totalAssetsCreated", "total_assets_created"), + ("totalAssets", "total_assets"), + ("totalBoxes", "total_boxes"), + ("totalBoxBytes", "total_box_bytes"), + ], +) +class TestAcctParam: + @staticmethod + def test_acct_param_fields_valid(method_name, field_name): + arg = pt.Int(1) + account_param_method = getattr(pt.AccountParam, method_name) + expr = account_param_method(arg) + assert expr.type_of() == pt.TealType.none + + account_param_field = AccountParamField[field_name] + assert expr.value().type_of() == account_param_field.type_of() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(arg, pt.Op.int, 1), + pt.TealOp(expr, pt.Op.acct_params_get, account_param_field.arg_name), + pt.TealOp(None, pt.Op.store, expr.slotOk), + pt.TealOp(None, pt.Op.store, expr.slotValue), + ] + ) - -def test_acct_param_balance_valid(): - arg = pt.Int(1) - expr = pt.AccountParam.balance(arg) - assert expr.type_of() == pt.TealType.none - assert expr.value().type_of() == pt.TealType.uint64 - - expected = pt.TealSimpleBlock( - [ - pt.TealOp(arg, pt.Op.int, 1), - pt.TealOp(expr, pt.Op.acct_params_get, "AcctBalance"), - pt.TealOp(None, pt.Op.store, expr.slotOk), - pt.TealOp(None, pt.Op.store, expr.slotValue), - ] - ) - - actual, _ = expr.__teal__(avm6Options) - actual.addIncoming() - actual = pt.TealBlock.NormalizeBlocks(actual) - - with pt.TealComponent.Context.ignoreExprEquality(): - assert actual == expected - - -def test_acct_param_min_balance_valid(): - arg = pt.Int(0) - expr = pt.AccountParam.minBalance(arg) - assert expr.type_of() == pt.TealType.none - assert expr.value().type_of() == pt.TealType.uint64 - - expected = pt.TealSimpleBlock( - [ - pt.TealOp(arg, pt.Op.int, 0), - pt.TealOp(expr, pt.Op.acct_params_get, "AcctMinBalance"), - pt.TealOp(None, pt.Op.store, expr.slotOk), - pt.TealOp(None, pt.Op.store, expr.slotValue), - ] - ) - - actual, _ = expr.__teal__(avm6Options) - actual.addIncoming() - actual = pt.TealBlock.NormalizeBlocks(actual) - - with pt.TealComponent.Context.ignoreExprEquality(): - assert actual == expected - - -def test_acct_param_auth_addr_valid(): - arg = pt.Int(1) - expr = pt.AccountParam.authAddr(arg) - assert expr.type_of() == pt.TealType.none - assert expr.value().type_of() == pt.TealType.bytes - - expected = pt.TealSimpleBlock( - [ - pt.TealOp(arg, pt.Op.int, 1), - pt.TealOp(expr, pt.Op.acct_params_get, "AcctAuthAddr"), - pt.TealOp(None, pt.Op.store, expr.slotOk), - pt.TealOp(None, pt.Op.store, expr.slotValue), - ] - ) - - actual, _ = expr.__teal__(avm6Options) - actual.addIncoming() - actual = pt.TealBlock.NormalizeBlocks(actual) - - with pt.TealComponent.Context.ignoreExprEquality(): - assert actual == expected + supported_options_version = pt.CompileOptions( + version=account_param_field.min_version + ) + actual, _ = expr.__teal__(supported_options_version) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + @staticmethod + def test_acct_param_version_checks(method_name, field_name): + arg = pt.Int(1) + account_param_method = getattr(pt.AccountParam, method_name) + expr = account_param_method(arg) + + account_param_field = AccountParamField[field_name] + + def test_unsupported_version(version: int, match: str = None): + with pytest.raises(pt.TealInputError, match=match): + unsupported_options_version = pt.CompileOptions(version=version) + expr.__teal__(unsupported_options_version) + + # Test program and field version checks + program_unsupported_version = pt.ir.Op.acct_params_get.min_version - 1 + program_error_match = "unavailable" + test_unsupported_version(program_unsupported_version, program_error_match) + + field_unsupported_version = account_param_field.min_version - 1 + + # Since program version dominates, we conditionally check field error message or program error message + # depending on whether the unsupported field version is less than or equal to the program unsupported + # version. + field_error_match = ( + "Program version too low to use field" + if field_unsupported_version > program_unsupported_version + else program_error_match + ) + test_unsupported_version(field_unsupported_version, field_error_match) def test_AccountParamObject(): @@ -94,3 +104,41 @@ def test_AccountParamObject(): assert_MaybeValue_equality( obj.auth_address(), pt.AccountParam.authAddr(account), avm6Options ) + + assert_MaybeValue_equality( + obj.total_num_uint(), pt.AccountParam.totalNumUint(account), avm8Options + ) + assert_MaybeValue_equality( + obj.total_num_byte_slice(), + pt.AccountParam.totalNumByteSlice(account), + avm8Options, + ) + assert_MaybeValue_equality( + obj.total_extra_app_pages(), + pt.AccountParam.totalExtraAppPages(account), + avm8Options, + ) + assert_MaybeValue_equality( + obj.total_apps_created(), + pt.AccountParam.totalAppsCreated(account), + avm8Options, + ) + assert_MaybeValue_equality( + obj.total_apps_opted_in(), + pt.AccountParam.totalAppsOptedIn(account), + avm8Options, + ) + assert_MaybeValue_equality( + obj.total_assets_created(), + pt.AccountParam.totalAssetsCreated(account), + avm8Options, + ) + assert_MaybeValue_equality( + obj.total_assets(), pt.AccountParam.totalAssets(account), avm8Options + ) + assert_MaybeValue_equality( + obj.total_boxes(), pt.AccountParam.totalBoxes(account), avm8Options + ) + assert_MaybeValue_equality( + obj.total_box_bytes(), pt.AccountParam.totalBoxBytes(account), avm8Options + ) diff --git a/pyteal/ast/app.py b/pyteal/ast/app.py index 62dbc0b26..e6f9d0de9 100644 --- a/pyteal/ast/app.py +++ b/pyteal/ast/app.py @@ -1,5 +1,14 @@ from typing import TYPE_CHECKING, Final from enum import Enum +from pyteal.ast.box import ( + BoxCreate, + BoxDelete, + BoxExtract, + BoxReplace, + BoxLen, + BoxGet, + BoxPut, +) from pyteal.types import TealType, require_type from pyteal.ir import TealOp, Op, TealBlock @@ -181,7 +190,7 @@ def globalPut(cls, key: Expr, value: Expr) -> "App": Args: key: The key to write in the global application state. Must evaluate to bytes. - value: THe value to write in the global application state. Can evaluate to any type. + value: The value to write in the global application state. Can evaluate to any type. """ require_type(key, TealType.bytes) require_type(value, TealType.anytype) @@ -211,6 +220,86 @@ def globalDel(cls, key: Expr) -> "App": require_type(key, TealType.bytes) return cls(AppField.globalDel, [key]) + @classmethod + def box_create(cls, name: Expr, size: Expr) -> Expr: + """Create a box with a given name and size. + + New boxes will contain a byte string of all zeros. Performing this operation on a box that + already exists will not change its contents. + + If successful, this expression returns 0 if the box already existed, otherwise it returns 1. + + A failure will occur if you attempt to create a box that already exists with a different size. + + Args: + name: The key used to reference this box. Must evaluate to a bytes. + size: The number of bytes to reserve for this box. Must evaluate to a uint64. + """ + return BoxCreate(name, size) + + @classmethod + def box_delete(cls, name: Expr) -> Expr: + """Deletes a box given it's name. + + This expression returns 1 if the box existed, otherwise it returns 0. + + Deleting a nonexistent box is allowed, but has no effect. + + Args: + name: The key the box was created with. Must evaluate to bytes. + """ + return BoxDelete(name) + + @classmethod + def box_extract(cls, name: Expr, start: Expr, length: Expr) -> Expr: + """Extracts bytes in a box given its name, start index and stop index. + + Args: + name: The key the box was created with. Must evaluate to bytes. + start: The byte index into the box to start reading. Must evaluate to uint64. + length: The byte length into the box from start to stop reading. Must evaluate to uint64. + """ + return BoxExtract(name, start, length) + + @classmethod + def box_replace(cls, name: Expr, start: Expr, value: Expr) -> Expr: + """Replaces bytes in a box given its name, start index, and value. + + Args: + name: The key the box was created with. Must evaluate to bytes. + start: The byte index into the box to start writing. Must evaluate to uint64. + value: The value to start writing at start index. Must evaluate to bytes. + """ + return BoxReplace(name, start, value) + + @classmethod + def box_length(cls, name: Expr) -> MaybeValue: + """Get the byte length of the box specified by its name. + + Args: + name: The key the box was created with. Must evaluate to bytes. + """ + return BoxLen(name) + + @classmethod + def box_get(cls, name: Expr) -> MaybeValue: + """Get the full contents of a box given its name. + + Args: + name: The key the box was created with. Must evaluate to bytes. + """ + return BoxGet(name) + + @classmethod + def box_put(cls, name: Expr, value: Expr) -> Expr: + """Write all contents to a box given its name. + + Args: + name: The key the box was created with. Must evaluate to bytes. + value: The value to write to the box. Must evaluate to bytes. + """ + return BoxPut(name, value) + App.__module__ = "pyteal" diff --git a/pyteal/ast/box.py b/pyteal/ast/box.py new file mode 100644 index 000000000..119f7f24e --- /dev/null +++ b/pyteal/ast/box.py @@ -0,0 +1,226 @@ +from typing import TYPE_CHECKING +from pyteal.ast.maybe import MaybeValue +from pyteal.errors import verifyProgramVersion + +from pyteal.types import TealType, require_type +from pyteal.ir import TealOp, Op, TealBlock +from pyteal.ast.expr import Expr + +if TYPE_CHECKING: + from pyteal.compiler import CompileOptions + + +class BoxCreate(Expr): + """Create a box with a given name and size.""" + + def __init__(self, name: Expr, size: Expr) -> None: + """ + Args: + name: The key used to reference this box. Must evaluate to a bytes. + size: The number of bytes to reserve for this box. Must evaluate to a uint64. + """ + + super().__init__() + require_type(name, TealType.bytes) + require_type(size, TealType.uint64) + self.name = name + self.size = size + + def __teal__(self, options: "CompileOptions"): + verifyProgramVersion( + minVersion=Op.box_create.min_version, + version=options.version, + msg=f"{Op.box_create} unavailable", + ) + return TealBlock.FromOp( + options, TealOp(self, Op.box_create), self.name, self.size + ) + + def __str__(self): + return f"(box_create {self.name} {self.size})" + + def type_of(self): + return TealType.uint64 + + def has_return(self): + return False + + +BoxCreate.__module__ = "pyteal" + + +class BoxDelete(Expr): + """Deletes a box given its name.""" + + def __init__(self, name: Expr) -> None: + """ + Args: + name: The key the box was created with. Must evaluate to bytes. + """ + super().__init__() + require_type(name, TealType.bytes) + self.name = name + + def __teal__(self, options: "CompileOptions"): + verifyProgramVersion( + minVersion=Op.box_del.min_version, + version=options.version, + msg=f"{Op.box_del} unavailable", + ) + return TealBlock.FromOp(options, TealOp(self, Op.box_del), self.name) + + def __str__(self): + return f"(box_del {self.name})" + + def type_of(self): + return TealType.uint64 + + def has_return(self): + return False + + +BoxDelete.__module__ = "pyteal" + + +class BoxReplace(Expr): + """Replaces bytes in a box given its name, start index, and value.""" + + def __init__(self, name: Expr, start: Expr, value: Expr) -> None: + """ + Args: + name: The key the box was created with. Must evaluate to bytes. + start: The byte index into the box to start writing. Must evaluate to uint64. + value: The value to start writing at start index. Must evaluate to bytes. + """ + super().__init__() + require_type(name, TealType.bytes) + require_type(start, TealType.uint64) + require_type(value, TealType.bytes) + self.name = name + self.start = start + self.value = value + + def __teal__(self, options: "CompileOptions"): + verifyProgramVersion( + minVersion=Op.box_replace.min_version, + version=options.version, + msg=f"{Op.box_replace} unavailable", + ) + return TealBlock.FromOp( + options, TealOp(self, Op.box_replace), self.name, self.start, self.value + ) + + def __str__(self): + return f"(box_replace {self.name} {self.start} {self.value})" + + def type_of(self): + return TealType.none + + def has_return(self): + return False + + +BoxReplace.__module__ = "pyteal" + + +class BoxExtract(Expr): + """Extracts bytes in a box given its name, start index and stop index.""" + + def __init__(self, name: Expr, start: Expr, length: Expr) -> None: + """ + Args: + name: The key the box was created with. Must evaluate to bytes. + start: The byte index into the box to start reading. Must evaluate to uint64. + length: The byte length into the box from start to stop reading. Must evaluate to uint64. + """ + + super().__init__() + require_type(name, TealType.bytes) + require_type(start, TealType.uint64) + require_type(length, TealType.uint64) + self.name = name + self.start = start + self.length = length + + def __teal__(self, options: "CompileOptions"): + verifyProgramVersion( + minVersion=Op.box_extract.min_version, + version=options.version, + msg=f"{Op.box_extract} unavailable", + ) + return TealBlock.FromOp( + options, TealOp(self, Op.box_extract), self.name, self.start, self.length + ) + + def __str__(self): + return f"(box_extract {self.name} {self.start} {self.length})" + + def type_of(self): + return TealType.bytes + + def has_return(self): + return False + + +BoxExtract.__module__ = "pyteal" + + +def BoxLen(name: Expr) -> MaybeValue: + """ + Get the byte length of the box specified by its name. + + Args: + name: The key the box was created with. Must evaluate to bytes. + """ + require_type(name, TealType.bytes) + return MaybeValue(Op.box_len, TealType.uint64, args=[name]) + + +def BoxGet(name: Expr) -> MaybeValue: + """ + Get the full contents of a box given its name. + + Args: + name: The key the box was created with. Must evaluate to bytes. + """ + require_type(name, TealType.bytes) + return MaybeValue(Op.box_get, TealType.bytes, args=[name]) + + +class BoxPut(Expr): + """Write all contents to a box given its name.""" + + def __init__(self, name: Expr, value: Expr) -> None: + """ + Args: + name: The key the box was created with. Must evaluate to bytes. + value: The value to write to the box. Must evaluate to bytes. + """ + + super().__init__() + require_type(name, TealType.bytes) + require_type(value, TealType.bytes) + self.name = name + self.value = value + + def __teal__(self, options: "CompileOptions"): + verifyProgramVersion( + minVersion=Op.box_put.min_version, + version=options.version, + msg=f"{Op.box_put} unavailable", + ) + return TealBlock.FromOp( + options, TealOp(self, Op.box_put), self.name, self.value + ) + + def __str__(self): + return f"(box_put {self.name})" + + def type_of(self): + return TealType.none + + def has_return(self): + return False + + +BoxPut.__module__ = "pyteal" diff --git a/pyteal/ast/box_test.py b/pyteal/ast/box_test.py new file mode 100644 index 000000000..008ffb580 --- /dev/null +++ b/pyteal/ast/box_test.py @@ -0,0 +1,184 @@ +from typing import Callable, Tuple + +import pytest +import pyteal as pt + +avm7Options = pt.CompileOptions(version=7) +avm8Options = pt.CompileOptions(version=8) + +POSITIVE_TEST_CASES: list[Tuple[pt.Expr, pt.TealType]] = [ + (pt.BoxCreate(pt.Bytes("box"), pt.Int(10)), pt.TealType.uint64), + (pt.BoxDelete(pt.Bytes("box")), pt.TealType.uint64), + (pt.BoxExtract(pt.Bytes("box"), pt.Int(2), pt.Int(4)), pt.TealType.bytes), + ( + pt.BoxReplace(pt.Bytes("box"), pt.Int(3), pt.Bytes("replace")), + pt.TealType.none, + ), + (pt.BoxLen(pt.Bytes("box")), pt.TealType.none), + (pt.BoxGet(pt.Bytes("box")), pt.TealType.none), + (pt.BoxPut(pt.Bytes("box"), pt.Bytes("goonery")), pt.TealType.none), +] + + +@pytest.mark.parametrize("test_case, test_case_type", POSITIVE_TEST_CASES) +def test_compile_version_and_type(test_case, test_case_type): + with pytest.raises(pt.TealInputError): + test_case.__teal__(avm7Options) + + test_case.__teal__(avm8Options) + assert test_case.type_of() == test_case_type + assert not test_case.has_return() + + return + + +INVALID_TEST_CASES: list[Tuple[list[pt.Expr], type | Callable[..., pt.MaybeValue]]] = [ + ([pt.Bytes("box"), pt.Bytes("ten")], pt.BoxCreate), + ([pt.Int(0xB0B), pt.Int(10)], pt.BoxCreate), + ([pt.Int(0xA11CE)], pt.BoxDelete), + ([pt.Bytes("box"), pt.Int(2), pt.Bytes("three")], pt.BoxExtract), + ([pt.Bytes("box"), pt.Int(2), pt.Int(0x570FF)], pt.BoxReplace), + ([pt.Int(12)], pt.BoxLen), + ([pt.Int(45)], pt.BoxGet), + ([pt.Bytes("box"), pt.Int(123)], pt.BoxPut), +] + + +@pytest.mark.parametrize("test_args, test_expr", INVALID_TEST_CASES) +def test_box_invalid_args(test_args, test_expr): + with pytest.raises(pt.TealTypeError): + test_expr(*test_args) + + +def test_box_create_compile(): + name_arg: pt.Expr = pt.Bytes("eineName") + size_arg: pt.Expr = pt.Int(10) + expr: pt.Expr = pt.BoxCreate(name_arg, size_arg) + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(name_arg, pt.Op.byte, '"eineName"'), + pt.TealOp(size_arg, pt.Op.int, 10), + pt.TealOp(expr, pt.Op.box_create), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert expected == actual + + +def test_box_delete_compile(): + name_arg: pt.Expr = pt.Bytes("eineName") + expr: pt.Expr = pt.BoxDelete(name_arg) + + expected = pt.TealSimpleBlock( + [pt.TealOp(name_arg, pt.Op.byte, '"eineName"'), pt.TealOp(expr, pt.Op.box_del)] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert expected == actual + + +def test_box_extract(): + name_arg: pt.Expr = pt.Bytes("eineName") + srt_arg: pt.Expr = pt.Int(10) + end_arg: pt.Expr = pt.Int(15) + expr: pt.Expr = pt.BoxExtract(name_arg, srt_arg, end_arg) + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(name_arg, pt.Op.byte, '"eineName"'), + pt.TealOp(srt_arg, pt.Op.int, 10), + pt.TealOp(end_arg, pt.Op.int, 15), + pt.TealOp(expr, pt.Op.box_extract), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert expected == actual + + +def test_box_replace(): + name_arg: pt.Expr = pt.Bytes("eineName") + srt_arg: pt.Expr = pt.Int(10) + replace_arg: pt.Expr = pt.Bytes("replace-str") + expr: pt.Expr = pt.BoxReplace(name_arg, srt_arg, replace_arg) + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(name_arg, pt.Op.byte, '"eineName"'), + pt.TealOp(srt_arg, pt.Op.int, 10), + pt.TealOp(replace_arg, pt.Op.byte, '"replace-str"'), + pt.TealOp(expr, pt.Op.box_replace), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert expected == actual + + +def test_box_length(): + name_arg: pt.Expr = pt.Bytes("eineName") + expr: pt.MaybeValue = pt.BoxLen(name_arg) + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, '"eineName"'), + pt.TealOp(None, pt.Op.box_len), + pt.TealOp(None, pt.Op.store, expr.output_slots[1]), + pt.TealOp(None, pt.Op.store, expr.output_slots[0]), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert expected == actual + + +def test_box_get(): + name_arg: pt.Expr = pt.Bytes("eineName") + expr: pt.MaybeValue = pt.BoxGet(name_arg) + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, '"eineName"'), + pt.TealOp(None, pt.Op.box_get), + pt.TealOp(None, pt.Op.store, expr.output_slots[1]), + pt.TealOp(None, pt.Op.store, expr.output_slots[0]), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert expected == actual + + +def test_box_put(): + name_arg: pt.Expr = pt.Bytes("eineName") + put_arg: pt.Expr = pt.Bytes("put-str") + expr: pt.Expr = pt.BoxPut(name_arg, put_arg) + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(name_arg, pt.Op.byte, '"eineName"'), + pt.TealOp(put_arg, pt.Op.byte, '"put-str"'), + pt.TealOp(expr, pt.Op.box_put), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert expected == actual diff --git a/pyteal/ast/frame.py b/pyteal/ast/frame.py new file mode 100644 index 000000000..8f5e24891 --- /dev/null +++ b/pyteal/ast/frame.py @@ -0,0 +1,132 @@ +from typing import TYPE_CHECKING + +from pyteal.ast.expr import Expr +from pyteal.types import TealType, require_type +from pyteal.errors import TealInputError, verifyProgramVersion +from pyteal.ir import TealBlock, TealSimpleBlock, TealOp, Op + +if TYPE_CHECKING: + from pyteal.compiler import CompileOptions + + +class Proto(Expr): + def __init__(self, num_args: int, num_returns: int): + super().__init__() + if num_args < 0: + raise TealInputError( + f"the number of arguments provided to Proto must be >= 0 but {num_args=}" + ) + if num_returns < 0: + raise TealInputError( + f"the number of return values provided to Proto must be >= 0 but {num_returns=}" + ) + self.num_args = num_args + self.num_returns = num_returns + + def __teal__(self, options: "CompileOptions") -> tuple[TealBlock, TealSimpleBlock]: + verifyProgramVersion( + Op.proto.min_version, + options.version, + "Program version too low to use op proto", + ) + op = TealOp(self, Op.proto, self.num_args, self.num_returns) + return TealBlock.FromOp(options, op) + + def __str__(self) -> str: + return f"(proto: num_args = {self.num_args}, num_returns = {self.num_returns})" + + def type_of(self) -> TealType: + return TealType.none + + def has_return(self) -> bool: + return False + + +Proto.__module__ = "pyteal" + + +class FrameDig(Expr): + def __init__(self, frame_index: int): + super().__init__() + self.frame_index = frame_index + + def __teal__(self, options: "CompileOptions") -> tuple[TealBlock, TealSimpleBlock]: + verifyProgramVersion( + Op.frame_dig.min_version, + options.version, + "Program version too low to use op frame_dig", + ) + op = TealOp(self, Op.frame_dig, self.frame_index) + return TealBlock.FromOp(options, op) + + def __str__(self) -> str: + return f"(frame_dig: dig_from = {self.frame_index})" + + def type_of(self) -> TealType: + return TealType.anytype + + def has_return(self) -> bool: + return False + + +FrameDig.__module__ = "pyteal" + + +class FrameBury(Expr): + def __init__(self, value: Expr, frame_index: int): + super().__init__() + require_type(value, TealType.anytype) + self.value = value + self.frame_index = frame_index + + def __teal__(self, options: "CompileOptions") -> tuple[TealBlock, TealSimpleBlock]: + verifyProgramVersion( + Op.frame_bury.min_version, + options.version, + "Program version too low to use op frame_bury", + ) + op = TealOp(self, Op.frame_bury, self.frame_index) + return TealBlock.FromOp(options, op, self.value) + + def __str__(self) -> str: + return f"(frame_bury (bury_to = {self.frame_index}) ({self.value}))" + + def type_of(self) -> TealType: + return TealType.none + + def has_return(self) -> bool: + return False + + +FrameBury.__module__ = "pyteal" + + +class DupN(Expr): + def __init__(self, value: Expr, repetition: int): + super().__init__() + require_type(value, TealType.anytype) + if repetition < 0: + raise TealInputError("dupn repetition should be non negative") + self.value = value + self.repetition = repetition + + def __teal__(self, options: "CompileOptions") -> tuple[TealBlock, TealSimpleBlock]: + verifyProgramVersion( + Op.dupn.min_version, + options.version, + "Program version too low to use op dupn", + ) + op = TealOp(self, Op.dupn, self.repetition) + return TealBlock.FromOp(options, op, self.value) + + def __str__(self) -> str: + return f"(dupn (repetition = {self.repetition}) ({self.value}))" + + def type_of(self) -> TealType: + return self.value.type_of() + + def has_return(self) -> bool: + return False + + +DupN.__module__ = "pyteal" diff --git a/pyteal/ast/frame_test.py b/pyteal/ast/frame_test.py new file mode 100644 index 000000000..e4cde2aa4 --- /dev/null +++ b/pyteal/ast/frame_test.py @@ -0,0 +1,107 @@ +import pytest +import pyteal as pt +from pyteal.ast.frame import FrameBury, FrameDig, Proto, DupN + +avm7Options = pt.CompileOptions(version=7) +avm8Options = pt.CompileOptions(version=8) + + +@pytest.mark.parametrize("input_num, output_num", [(1, 1), (1, 0), (5, 5)]) +def test_proto(input_num: int, output_num: int): + expr = Proto(input_num, output_num) + assert not expr.has_return() + assert expr.type_of() == pt.TealType.none + + expected = pt.TealSimpleBlock([pt.TealOp(expr, pt.Op.proto, input_num, output_num)]) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + +def test_proto_invalid(): + with pytest.raises(pt.TealInputError): + Proto(-1, 1) + + with pytest.raises(pt.TealInputError): + Proto(1, -1) + + with pytest.raises(pt.TealInputError): + Proto(1, 1).__teal__(avm7Options) + + +@pytest.mark.parametrize("depth", [-1, 0, 1, 2]) +def test_frame_dig(depth: int): + expr = FrameDig(depth) + assert not expr.has_return() + assert expr.type_of() == pt.TealType.anytype + + expected = pt.TealSimpleBlock([pt.TealOp(expr, pt.Op.frame_dig, depth)]) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + +def test_frame_dig_invalid(): + with pytest.raises(pt.TealInputError): + FrameDig(1).__teal__(avm7Options) + + +def test_frame_bury(): + byte_expr = pt.Bytes("Astartes") + expr = FrameBury(byte_expr, 4) + assert not expr.has_return() + assert expr.type_of() == pt.TealType.none + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(byte_expr, pt.Op.byte, '"Astartes"'), + pt.TealOp(expr, pt.Op.frame_bury, 4), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + +def test_frame_bury_invalid(): + with pytest.raises(pt.TealTypeError): + FrameBury(pt.Seq(), 1) + + with pytest.raises(pt.TealInputError): + FrameBury(pt.Int(1), 1).__teal__(avm7Options) + + +def test_dupn(): + byte_expr = pt.Bytes("Astartes") + expr = DupN(byte_expr, 4) + assert not expr.has_return() + assert expr.type_of() == byte_expr.type_of() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(byte_expr, pt.Op.byte, '"Astartes"'), + pt.TealOp(expr, pt.Op.dupn, 4), + ] + ) + actual, _ = expr.__teal__(avm8Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + +def test_dupn_invalid(): + with pytest.raises(pt.TealTypeError): + DupN(pt.Seq(), 1) + + with pytest.raises(pt.TealInputError): + DupN(pt.Int(1), -1) + + with pytest.raises(pt.TealInputError): + DupN(pt.Int(1), 1).__teal__(avm7Options) diff --git a/pyteal/ast/maybe.py b/pyteal/ast/maybe.py index 43846384d..9ae1f0245 100644 --- a/pyteal/ast/maybe.py +++ b/pyteal/ast/maybe.py @@ -1,11 +1,15 @@ -from typing import List, Union - -from pyteal.ast.multi import MultiValue +from typing import Callable, List, Union, TYPE_CHECKING +from pyteal.errors import verifyProgramVersion from pyteal.types import TealType from pyteal.ir import Op + from pyteal.ast.expr import Expr from pyteal.ast.scratch import ScratchLoad, ScratchSlot +from pyteal.ast.multi import MultiValue + +if TYPE_CHECKING: + from pyteal.compiler import CompileOptions class MaybeValue(MultiValue): @@ -17,7 +21,8 @@ def __init__( type: TealType, *, immediate_args: List[Union[int, str]] = None, - args: List[Expr] = None + args: List[Expr] = None, + compile_check: Callable[["CompileOptions"], None] = None, ): """Create a new MaybeValue. @@ -26,9 +31,28 @@ def __init__( type: The type of the returned value. immediate_args (optional): Immediate arguments for the op. Defaults to None. args (optional): Stack arguments for the op. Defaults to None. + compile_check (optional): Callable compile check. Defaults to program version check. + This parameter overwrites the default program version check. """ + + # Default compile check if one is not given + def local_version_check(options: "CompileOptions"): + verifyProgramVersion( + minVersion=op.min_version, + version=options.version, + msg=f"{op.value} unavailable", + ) + types = [type, TealType.uint64] - super().__init__(op, types, immediate_args=immediate_args, args=args) + super().__init__( + op, + types, + immediate_args=immediate_args, + args=args, + compile_check=( + local_version_check if compile_check is None else compile_check + ), + ) def hasValue(self) -> ScratchLoad: """Check if the value exists. @@ -49,7 +73,7 @@ def value(self) -> ScratchLoad: def slotOk(self) -> ScratchSlot: """Get the scratch slot that stores hasValue. - Note: This is mainly added for backwards compatability and normally shouldn't be used + Note: This is mainly added for backwards compatibility and normally shouldn't be used directly in pyteal code. """ return self.output_slots[1] @@ -59,7 +83,7 @@ def slotValue(self) -> ScratchSlot: """Get the scratch slot that stores the value or the zero value for the type if the value doesn't exist. - Note: This is mainly added for backwards compatability and normally shouldn't be used + Note: This is mainly added for backwards compatibility and normally shouldn't be used directly in pyteal code. """ return self.output_slots[0] diff --git a/pyteal/compiler/compiler.py b/pyteal/compiler/compiler.py index 472cfdba2..f2b25b489 100644 --- a/pyteal/compiler/compiler.py +++ b/pyteal/compiler/compiler.py @@ -25,7 +25,8 @@ ) from pyteal.compiler.constants import createConstantBlocks -MAX_PROGRAM_VERSION = 7 +MAX_PROGRAM_VERSION = 8 +FRAME_POINTER_VERSION = 8 MIN_PROGRAM_VERSION = 2 DEFAULT_PROGRAM_VERSION = MIN_PROGRAM_VERSION diff --git a/pyteal/compiler/compiler_test.py b/pyteal/compiler/compiler_test.py index 8ad42918b..3016b8254 100644 --- a/pyteal/compiler/compiler_test.py +++ b/pyteal/compiler/compiler_test.py @@ -139,7 +139,7 @@ def test_compile_version_invalid(): pt.compileTeal(expr, pt.Mode.Signature, version=1) # too small with pytest.raises(pt.TealInputError): - pt.compileTeal(expr, pt.Mode.Signature, version=8) # too large + pt.compileTeal(expr, pt.Mode.Signature, version=9) # too large with pytest.raises(pt.TealInputError): pt.compileTeal(expr, pt.Mode.Signature, version=2.0) # decimal diff --git a/pyteal/ir/ops.py b/pyteal/ir/ops.py index 77fb721ae..9ec496288 100644 --- a/pyteal/ir/ops.py +++ b/pyteal/ir/ops.py @@ -33,7 +33,7 @@ def min_version(self) -> int: # fmt: off # meta comment = OpType("//", Mode.Signature | Mode.Application, 0) - # avm + # avm err = OpType("err", Mode.Signature | Mode.Application, 2) sha256 = OpType("sha256", Mode.Signature | Mode.Application, 2) keccak256 = OpType("keccak256", Mode.Signature | Mode.Application, 2) @@ -190,6 +190,19 @@ def min_version(self) -> int: sha3_256 = OpType("sha3_256", Mode.Signature | Mode.Application, 7) vrf_verify = OpType("vrf_verify", Mode.Signature | Mode.Application, 7) block = OpType("block", Mode.Signature | Mode.Application, 7) + box_create = OpType("box_create", Mode.Application, 8) + box_extract = OpType("box_extract", Mode.Application, 8) + box_replace = OpType("box_replace", Mode.Application, 8) + box_del = OpType("box_del", Mode.Application, 8) + box_len = OpType("box_len", Mode.Application, 8) + box_get = OpType("box_get", Mode.Application, 8) + box_put = OpType("box_put", Mode.Application, 8) + popn = OpType("popn", Mode.Signature | Mode.Application, 8) + dupn = OpType("dupn", Mode.Signature | Mode.Application, 8) + bury = OpType("bury", Mode.Signature | Mode.Application, 8) + frame_dig = OpType("frame_dig", Mode.Signature | Mode.Application, 8) + frame_bury = OpType("frame_bury", Mode.Signature | Mode.Application, 8) + proto = OpType("proto", Mode.Signature | Mode.Application, 8) # fmt: on