diff --git a/docs/language-guide.md b/docs/language-guide.md index ef035576da..a53074fcfd 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -78,7 +78,6 @@ lg-logs lg-transactions lg-ops lg-opcode-budget -lg-arc4 lg-arc28 lg-calling-apps lg-compile diff --git a/docs/lg-arc28.md b/docs/lg-arc28.md index 3da1d5d407..e354d00444 100644 --- a/docs/lg-arc28.md +++ b/docs/lg-arc28.md @@ -2,7 +2,7 @@ [ARC-28](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0028.md) provides a methodology for structured logging by Algorand smart contracts. It introduces the concept of Events, where data contained in logs may be categorized and structured. -Each Event is identified by a unique 4-byte identifier derived from its `Event Signature`. The Event Signature is a UTF-8 string comprised of the event's name, followed by the names of the [ARC-4](./lg-arc4.md) data types contained in the event, all enclosed in parentheses (`EventName(type1,type2,...)`) e.g.: +Each Event is identified by a unique 4-byte identifier derived from its `Event Signature`. The Event Signature is a UTF-8 string comprised of the event's name, followed by the names of the ARC-4 data types contained in the event, all enclosed in parentheses (`EventName(type1,type2,...)`) e.g.: ``` Swapped(uint64,uint64) @@ -14,7 +14,7 @@ Events are emitting by including them in the [log output](./lg-logs.md). The met To emit an ARC-28 event in Algorand Python you can use the `emit` function, which appears in the `algopy.arc4` namespace for convenience since it heavily uses ARC-4 types and is essentially an extension of the ARC-4 specification. This function takes care of encoding the event payload to conform to the ARC-28 specification and there are 3 overloads: -- An [ARC-4 struct](./lg-arc4.md), from what the name of the struct will be used as a the event name and the struct parameters will be used as the event fields - `arc4.emit(Swapped(a, b))` +- An ARC-4 struct, from what the name of the struct will be used as a the event name and the struct parameters will be used as the event fields - `arc4.emit(Swapped(a, b))` - An event signature as a [string literal (or module variable)](./lg-types.md), followed by the values - `arc4.emit("Swapped(uint64,uint64)", a, b)` - An event name as a [string literal (or module variable)](./lg-types.md), followed by the values - `arc4.emit("Swapped", a, b)` diff --git a/docs/lg-arc4.md b/docs/lg-arc4.md deleted file mode 100644 index 84c66e268d..0000000000 --- a/docs/lg-arc4.md +++ /dev/null @@ -1,225 +0,0 @@ -# ARC-4: Application Binary Interface - -[ARC4](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md) defines a set of encodings and behaviors for authoring and interacting with an Algorand Smart Contract. It is not the only way to author a smart contract, but adhering to it will make it easier for other clients and users to interop with your contract. - -To author an arc4 contract you should extend the `ARC4Contract` base class. - -```python -from algopy import ARC4Contract - -class HelloWorldContract(ARC4Contract): - ... -``` - -## ARC-32 and ARC-56 - -[ARC32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) extends the concepts in ARC4 to include an Application Specification which more holistically describes a smart contract and its associated state. - -ARC-32/ARC-56 Application Specification files are automatically generated by the compiler for ARC4 -contracts as `.arc32.json` or `.arc56.json` - -## Methods - -Individual methods on a smart contract should be annotated with an `abimethod` decorator. This decorator is used to indicate a method which should be externally callable. The decorator itself includes properties to restrict when the method should be callable, for instance only when the application is being created or only when the OnComplete action is OptIn. - -A method that should not be externally available should be annotated with a `subroutine` decorator. - -Method docstrings will be used when outputting ARC-32 or ARC-56 application specifications, the following docstrings styles are supported ReST, Google, Numpydoc-style and Epydoc. - -```python -from algopy import ARC4Contract, subroutine, arc4 - - -class HelloWorldContract(ARC4Contract): - @arc4.abimethod(create=False, allow_actions=["NoOp", "OptIn"], name="external_name") - def hello(self, name: arc4.String) -> arc4.String: - return self.internal_method() + name - - @subroutine - def internal_method(self) -> arc4.String: - return arc4.String("Hello, ") -``` - -## Router - -Algorand Smart Contracts only have two possible programs that are invoked when making an ApplicationCall Transaction (`appl`). The "clear state" program which is called when using an OnComplete action of `ClearState` or the "approval" program which is called for all other OnComplete actions. - -Routing is required to dispatch calls handled by the approval program to the relevant ABI methods. When extending `ARC4Contract`, the routing code is automatically generated for you by the PuyaPy compiler. - -## Types - -ARC4 defines a number of [data types](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md#types) which can be used in an ARC4 compatible contract and details how these types should be encoded in binary. - -Algorand Python exposes these through a number of types which can be imported from the `algopy.arc4` module. These types represent binary encoded values following the rules prescribed in the ARC which can mean operations performed directly on these types are not as efficient as ones performed on natively supported types (such as `algopy.UInt64` or `algopy.Bytes`) - -Where supported, the native equivalent of an ARC4 type can be obtained via the `.native` property. It is possible to use native types in an ABI method and the router will automatically encode and decode these types to their ARC4 equivalent. - -### Booleans - -**Type:** `algopy.arc4.Bool` -**Encoding:** A single byte where the most significant bit is `1` for `True` and `0` for `False` -**Native equivalent:** `builtins.bool` - -### Unsigned ints - -**Types:** `algopy.arc4.UIntN` (<= 64 bits) `algopy.arc4.BigUIntN` (> 64 bits) -**Encoding:** A big endian byte array of N bits -**Native equivalent:** `algopy.UInt64` or `puya.py.BigUInt` - -Common bit sizes have also been aliased under `algopy.arc4.UInt8`, `algopy.arc4.UInt16` etc. A uint of any size between 8 and 512 bits (in intervals of 8bits) can be created using a generic parameter. It can be helpful to define your own alias for this type. - -```python -import typing as t -from algopy import arc4 - -UInt40: t.TypeAlias = arc4.UIntN[t.Literal[40]] -``` - -### Unsigned fixed point decimals - -**Types:** `algopy.arc4.UFixedNxM` (<= 64 bits) `algopy.arc4.BigUFixedNxM` (> 64 bits) -**Encoding:** A big endian byte array of N bits where `encoded_value = value / (10^M)` -**Native equivalent:** _none_ - -```python -import typing as t -from algopy import arc4 - -Decimal: t.TypeAlias = arc4.UFixedNxM[t.Literal[64], t.Literal[10]] -``` - -### Bytes and strings - -**Types:** `algopy.arc4.DynamicBytes` and `algopy.arc4.String` -**Encoding:** A variable length byte array prefixed with a 16-bit big endian header indicating the length of the data -**Native equivalent:** `algopy.Bytes` and `algopy.String` - -Strings are assumed to be utf-8 encoded and the length of a string is the total number of bytes, _not the total number of characters_. - -### Static arrays - -**Type:** `algopy.arc4.StaticArray` -**Encoding:** See [ARC4 Container Packing](#ARC4-Container-Packing) -**Native equivalent:** _none_ - -An ARC4 StaticArray is an array of a fixed size. The item type is specified by the first generic parameter and the size is specified by the second. - -```python -import typing as t -from algopy import arc4 - -FourBytes: t.TypeAlias = arc4.StaticArray[arc4.Byte, t.Literal[4]] -``` - - -### Address -**Type:** `algopy.arc4.Address` -**Encoding:** A byte array 32 bytes long -**Native equivalent:** [`algopy.Account`](#algopy.Account) - -Address represents an Algorand address's public key, and can be used instead of `algopy.Account` when needing to -reference an address in an ARC4 struct, tuple or return type. It is a subclass of `arc4.StaticArray[arc4.Byte, typing.Literal[32]]` - -### Dynamic arrays - -**Type:** `algopy.arc4.DynamicArray` -**Encoding:** See [ARC4 Container Packing](#ARC4-Container-Packing) -**Native equivalent:** _none_ - -An ARC4 DynamicArray is an array of a variable size. The item type is specified by the first generic parameter. Items can be added and removed via `.pop`, `.append`, and `.extend`. - -The current length of the array is encoded in a 16-bit prefix similar to the `arc4.DynamicBytes` and `arc4.String` types - -```python -import typing as t -from algopy import arc4 - -UInt64Array: t.TypeAlias = arc4.DynamicArray[arc4.UInt64] -``` - -### Tuples - -**Type:** `algopy.arc4.Tuple` -**Encoding:** See [ARC4 Container Packing](#ARC4-Container-Packing) -**Native equivalent:** `builtins.tuple` - -ARC4 Tuples are immutable statically sized arrays of mixed item types. Item types can be specified via generic parameters or inferred from constructor parameters. - -### Structs - -**Type:** `algopy.arc4.Struct` -**Encoding:** See [ARC4 Container Packing](#ARC4-Container-Packing) -**Native equivalent:** `typing.NamedTuple` - -ARC4 Structs are named tuples. The class keyword `frozen` can be used to indicate if a struct can be mutated. -Items can be accessed and mutated via names instead of indexes. Structs do not have a `.native` property, -but a NamedTuple can be used in ABI methods are will be encoded/decode to an ARC4 struct automatically. - -```python -import typing - -from algopy import arc4 - -Decimal: typing.TypeAlias = arc4.UFixedNxM[typing.Literal[64], typing.Literal[9]] - -class Vector(arc4.Struct, kw_only=True, frozen=True): - x: Decimal - y: Decimal -``` - -### ARC4 Container Packing - -ARC4 encoding rules are detailed explicitly in the [ARC](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md#encoding-rules). A summary is included here. - -Containers are composed of a head and tail portion. - - For dynamic arrays, the head is prefixed with the length of the array encoded as a 16-bit number. This prefix is not included in offset calculation - - For fixed sized items (eg. Bool, UIntN, or a StaticArray of UIntN), the item is included in the head - - Consecutive Bool items are compressed into the minimum number of whole bytes possible by using a single bit to represent each Bool - - For variable sized items (eg. DynamicArray, String etc), a pointer is included to the head and the data is added to the tail. This pointer represents the offset from the start of the head to the start of the item data in the tail. - - -### Reference types - -**Types:** `algopy.Account`, `algopy.Application`, `algopy.Asset`, `algopy.gtxn.PaymentTransaction`, `algopy.gtxn.KeyRegistrationTransaction`, `algopy.gtxn.AssetConfigTransaction`, `algopy.gtxn.AssetTransferTransaction`, `algopy.gtxn.AssetFreezeTransaction`, `algopy.gtxn.ApplicationCallTransaction` - -The ARC4 specification allows for using a number of [reference types](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md#reference-types) in an ABI method signature where this reference type refers to... - - another transaction in the group - - an account in the accounts array (`apat` property of the transaction) - - an asset in the foreign assets array (`apas` property of the transaction) - - an application in the foreign apps array (`apfa` property of the transaction) - -These types can only be used as parameters, and not as return types. - -```python -from algopy import ( - Account, - Application, - ARC4Contract, - Asset, - arc4, - gtxn, -) - -class Reference(ARC4Contract): - @arc4.abimethod - def with_transactions( - self, - asset: Asset, - pay: gtxn.PaymentTransaction, - account: Account, - app: Application, - axfr: gtxn.AssetTransferTransaction - ) -> None: - ... - -``` -### Mutability - -To ensure semantic compatability the compiler will also check for any usages of mutable ARC4 types (arrays and structs) and ensure that any additional references are copied using the `.copy()` method. - -Python values are passed by reference, and when an object (eg. an array or struct) is mutated in one place, all references to that object see the mutated version. In Python this is managed via the heap. -In Algorand Python these mutable values are instead stored on the stack, so when an additional reference is made (i.e. by assigning to another variable) a copy is added to the stack. -Which means if one reference is mutated, the other references would not see the change. -In order to keep the semantics the same, the compiler forces the addition of `.copy()` each time a new reference to the same object to match what will happen on the AVM. - -Struct types can be indicated as `frozen` which will eliminate the need for a `.copy()` as long as the struct also contains no mutable fields (such as arrays or another mutable struct) diff --git a/docs/lg-control.md b/docs/lg-control.md index 1eda98694b..e7bd579937 100644 --- a/docs/lg-control.md +++ b/docs/lg-control.md @@ -43,7 +43,7 @@ Note: we don't currently have support for while-else statements. ## For Loops -For loops are used to iterate over sequences, ranges and [ARC-4 arrays](./lg-arc4.md). They work the same as Python. +For loops are used to iterate over sequences, ranges and ARC-4 arrays. They work the same as Python. Algorand Python provides functions like `uenumerate` and `urange` to facilitate creating sequences and ranges; in-built Python `reversed` method works with these. diff --git a/docs/lg-structure.md b/docs/lg-structure.md index 8fde1246e1..0070469b78 100644 --- a/docs/lg-structure.md +++ b/docs/lg-structure.md @@ -262,7 +262,7 @@ Things to note here: methods must be decorated with one of `algopy.arc4.abimethod`, `alogpy.arc4.baremethod`, or `algopy.subroutine`. `subroutines` won't be directly callable through the default router. -See the [ARC-4 section](lg-arc4.md) of this language guide for more info on the above. +See the ARC-4 section of this language guide for more info on the above. ## Logic signatures diff --git a/docs/lg-types.md b/docs/lg-types.md index 99ac4aabbc..7b4a1c73fd 100644 --- a/docs/lg-types.md +++ b/docs/lg-types.md @@ -206,7 +206,6 @@ bytes of the public key (without the checksum). It has various account related m Also see [`algopy.arc4.Address`](#algopy.arc4.Address) if needing to represent the address as a distinct type. - ### Asset [`Asset`](#algopy.Asset) represents a logical Asset, backed by a `uint64` ID. @@ -315,5 +314,5 @@ the `native` property to retrieve the value. Most of the ARC-4 types also allow you can edit values in arrays by index. Please see the [reference documentation](./api-algopy.arc4.md) for the different classes that can -be used to represent ARC-4 values or the [ARC-4 documentation](./lg-arc4.md) for more information +be used to represent ARC-4 values or the ARC-4 documentation for more information about ARC-4. diff --git a/stubs/algopy-stubs/arc4.pyi b/stubs/algopy-stubs/arc4.pyi index c2094a40d7..8f1cd4c064 100644 --- a/stubs/algopy-stubs/arc4.pyi +++ b/stubs/algopy-stubs/arc4.pyi @@ -9,16 +9,41 @@ _R = typing.TypeVar("_R") _ReadOnlyNoArgsMethod: typing.TypeAlias = Callable[..., typing.Any] # type: ignore[misc] class ARC4Contract(algopy.Contract): - """A contract that conforms to the ARC4 ABI specification, functions decorated with - `@abimethod` or `@baremethod` will form the public interface of the contract + """The base class for a contract that conforms to the [ARC4 ABI specification](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md). Most contracts + should inherit from this class or a superclass thereof. - The approval_program will be implemented by the compiler, and route application args - according to the ARC4 ABI specification + ```python + class HelloWorldContract(ARC4Contract): + # ... + ``` + + Functions decorated with {py:func}`algopy.arc4.abimethod` or {py:func}`algopy.arc4.baremethod` will form the public + interface of the contract. + + The {py:meth}`algopy.arc4.ARC4Contract.approval_program` will be implemented by the compiler, and route application args + according to the ARC4 ABI specification. + + The {py:meth}`algopy.arc4.ARC4Contract.clear_state_program` will by default return True, but can be overridden + + The Puya compiler will generate ARC32 and ARC56 application specifications for the contract + automatically. + """ - The clear_state_program will by default return True, but can be overridden""" + def approval_program(self) -> bool: + """ + The approval program for the ARC4Contract is implemented by the compile in + accordance with ARC4 + """ + + def clear_state_program(self) -> algopy.UInt64 | bool: + """ + The clear_state_program contains the logic when the `OnCompletion` is `ClearState`. + + The default implementation simply returns True, but this can be overridden. - def approval_program(self) -> bool: ... - def clear_state_program(self) -> algopy.UInt64 | bool: ... + ClearState transactions always clear local state of the sender. Documentation on + `ClearState` behavior should be read before implementing this method: https://developer.algorand.org/docs/get-details/dapps/smart-contracts/frontend/apps/#clear-state + """ # if we use type aliasing here for Callable[_P, _R], mypy thinks it involves Any... @typing.overload @@ -42,11 +67,30 @@ def abimethod( readonly: bool = False, default_args: Mapping[str, str | _ReadOnlyNoArgsMethod] = ..., ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: - """Decorator that indicates a method is an ARC4 ABI method. + """ + Decorator that indicates a method is an ARC4 ABI method. If the method should not be externally + callable, use {py:func}`algopy.subroutine` instead. + + Method docstrings will be used when outputting ARC-32 or ARC-56 application specifications, + the following docstrings styles are supported ReST, Google, Numpydoc-style and Epydoc. + + ```python + from algopy import ARC4Contract, subroutine, arc4 + + + class HelloWorldContract(ARC4Contract): + @arc4.abimethod(create=False, allow_actions=["NoOp", "OptIn"], name="external_name") + def hello(self, name: arc4.String) -> arc4.String: + return self.internal_method() + name + + @subroutine + def internal_method(self) -> arc4.String: + return arc4.String("Hello, ") + ``` :arg name: Name component of the ABI method selector. Defaults to using the function name. :arg create: Controls the validation of the Application ID. "require" means it must be zero, - "disallow" requires it must be non-zero, and "allow" disables the validation. + "disallow" requires it must be non-zero, and "allow" disables the validation. :arg allow_actions: A sequence of allowed On-Completion Actions to validate against. :arg readonly: If True, then this method can be used via dry-run / simulate. :arg default_args: Default argument sources for clients to use. @@ -92,7 +136,9 @@ class _ABIEncoded(algopy.BytesBacked, typing.Protocol): """ class String(_ABIEncoded): - """An ARC4 sequence of bytes containing a UTF8 string""" + """An ARC4 sequence of bytes containing a UTF8 string. + + The length is the number of bytes, NOT the number of characters""" def __init__(self, value: algopy.String | str = "", /) -> None: ... @property @@ -142,7 +188,20 @@ class _UIntN(_ABIEncoded, typing.Protocol): """Returns `True` if not equal to zero""" class UIntN(_UIntN, typing.Generic[_TBitSize]): - """An ARC4 UInt consisting of the number of bits specified. + """An ARC4 UInt consisting of the number of bits specified (big-endian encoded). + + Common bit sizes have also been aliased under \ + {py:type}`algopy.arc4.UInt8`, {py:type}`algopy.arc4.UInt16` etc. + + A uint of any size between 8 and 512 bits (in intervals of 8bits) can be created using a + generic parameter. It can be helpful to define your own alias for this type. + + ```python + import typing as t + from algopy import arc4 + + UInt40: t.TypeAlias = arc4.UIntN[t.Literal[40]] + ``` Max Size: 64 bits""" @@ -164,7 +223,16 @@ _TDecimalPlaces = typing.TypeVar("_TDecimalPlaces", bound=int) class UFixedNxM(_ABIEncoded, typing.Generic[_TBitSize, _TDecimalPlaces]): """An ARC4 UFixed representing a decimal with the number of bits and precision specified. - Max size: 64 bits""" + Max size: 64 bits + + ```python + import typing as t + from algopy import arc4 + + Decimal: t.TypeAlias = arc4.UFixedNxM[t.Literal[64], t.Literal[10]] + ``` + + """ def __init__(self, value: str = "0.0", /): """ @@ -220,7 +288,7 @@ UInt512: typing.TypeAlias = BigUIntN[typing.Literal[512]] """An ARC4 UInt512""" class Bool(_ABIEncoded): - """An ARC4 encoded bool""" + """An ARC4 encoded bool. The most significant bit is `1` for `True` and `0` for `False`""" def __init__(self, value: bool = False, /) -> None: ... # noqa: FBT001, FBT002 def __eq__(self, other: Bool | bool) -> bool: ... # type: ignore[override] @@ -237,7 +305,15 @@ class StaticArray( typing.Generic[_TArrayItem, _TArrayLength], Reversible[_TArrayItem], ): - """A fixed length ARC4 Array of the specified type and length""" + """A fixed length ARC4 Array of the specified type and length + + ```python + import typing as t + from algopy import arc4 + + FourBytes: t.TypeAlias = arc4.StaticArray[arc4.Byte, t.Literal[4]] + ``` + """ @typing.overload def __init__(self) -> None: ... @@ -364,7 +440,15 @@ class StaticArray( """Create a copy of this array""" class DynamicArray(_ABIEncoded, typing.Generic[_TArrayItem], Reversible[_TArrayItem]): - """A dynamically sized ARC4 Array of the specified type""" + """A dynamically sized ARC4 Array of the specified type + + ```python + import typing as t + from algopy import arc4 + + UInt64Array: t.TypeAlias = arc4.DynamicArray[arc4.UInt64] + ``` + """ def __init__(self, *items: _TArrayItem): """Initializes a new array with items provided""" @@ -459,7 +543,9 @@ class DynamicBytes(DynamicArray[Byte]): _TTuple = typing.TypeVarTuple("_TTuple") class Tuple(_ABIEncoded, tuple[typing.Unpack[_TTuple]]): - """An ARC4 ABI tuple, containing other ARC4 ABI types""" + """An ARC4 ABI tuple, containing other ARC4 ABI types. ARC4 Tuples are immutable statically + sized arrays of mixed item types. Item types can be specified via generic parameters or + inferred from constructor parameters.""" def __init__(self, items: tuple[typing.Unpack[_TTuple]], /): """Construct an ARC4 tuple from a native Python tuple""" @@ -486,7 +572,24 @@ class _StructMeta(type): ) -> _StructMeta: ... class Struct(metaclass=_StructMeta): - """Base class for ARC4 Struct types""" + """Base class for ARC4 Struct types. ARC4 Structs are named tuples. The class keyword `frozen` + can be used to indicate if a struct can be mutated. + + Items can be accessed and mutated via names instead of indexes. Structs do not have a `.native` + property, but a NamedTuple can be used in ABI methods are will be encoded/decode to an ARC4 + struct automatically. + + ```python + import typing + + from algopy import arc4 + + Decimal: typing.TypeAlias = arc4.UFixedNxM[typing.Literal[64], typing.Literal[9]] + + class Vector(arc4.Struct, kw_only=True, frozen=True): + x: Decimal + y: Decimal + ```""" @classmethod def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self: