From 9da41a0cd486f667035d7376565af04d74373bae Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 25 Oct 2024 15:42:00 +0800 Subject: [PATCH 1/3] doc: update documentation regarding nested tuples, named tuples and immutability options --- docs/lg-arc4.md | 26 +++++++++++++++++++++++--- docs/lg-types.md | 29 +++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/docs/lg-arc4.md b/docs/lg-arc4.md index f22aa64e7d..f2022d061d 100644 --- a/docs/lg-arc4.md +++ b/docs/lg-arc4.md @@ -109,6 +109,15 @@ 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` @@ -138,9 +147,11 @@ ARC4 Tuples are immutable statically sized arrays of mixed item types. Item type **Type:** `algopy.arc4.Struct` **Encoding:** See [ARC4 Container Packing](#ARC4-Container-Packing) -**Native equivalent:** _none_ +**Native equivalent:** `typing.NamedTuple` -ARC4 Structs are mutable named tuples. Items can be accessed and mutated via names instead of indexes. +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 @@ -149,7 +160,7 @@ from algopy import arc4 Decimal: typing.TypeAlias = arc4.UFixedNxM[typing.Literal[64], typing.Literal[9]] -class Vector(arc4.Struct, kw_only=True): +class Vector(arc4.Struct, kw_only=True, frozen=True): x: Decimal y: Decimal ``` @@ -200,7 +211,16 @@ class Reference(ARC4Contract): ... ``` +### 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) ## Typed clients diff --git a/docs/lg-types.md b/docs/lg-types.md index 4f0ca59545..99ac4aabbc 100644 --- a/docs/lg-types.md +++ b/docs/lg-types.md @@ -201,8 +201,11 @@ if a: ### Account -[`Account`](#algopy.Account) represents a logical Account, backed by a `bytes[]` representing the -public key. It has various account related methods that can be called from the type. +[`Account`](#algopy.Account) represents a logical Account, backed by a `bytes[32]` representing the +bytes of the public key (without the checksum). It has various account related methods that can be called from the type. + +Also see [`algopy.arc4.Address`](#algopy.arc4.Address) if needing to represent the address as a distinct type. + ### Asset @@ -230,8 +233,26 @@ In saying that, there are many places where built-in Python types can be used an ### tuple -Python tuples are supported as arguments to subroutines, local variables, return types. Nested tuples -are _not_ currently supported. +Python tuples are supported as arguments to subroutines, local variables, return types. + +### typing.NamedTuple + +Python named tuples are also supported using [`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple). + +```{note} +Default field values and subclassing a NamedTuple are not supported +``` + +```python +import typing + +import algopy + + +class Pair(typing.NamedTuple): + foo: algopy.Bytes + bar: algopy.Bytes +``` ### None From 4fa8479863487a52e22509d6940c3970c2cd1a42 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Thu, 7 Nov 2024 15:23:08 +0800 Subject: [PATCH 2/3] doc: remove note about --match-algod-bytecode --- docs/compiler.md | 1 - docs/lg-compile.md | 7 ------- 2 files changed, 8 deletions(-) diff --git a/docs/compiler.md b/docs/compiler.md index e4797614b0..7141ee8638 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -115,7 +115,6 @@ puyapy [-h] [--version] [-O {0,1,2}] | `--output-arc32`, `--no-output-arc32` | Output {contract}.arc32.json ARC-32 app spec file if the contract is an ARC-4 contract | `True` | | `--output-client`, `--no-output-client` | Output Algorand Python contract client for typed ARC4 ABI calls | `False` | | `--output-bytecode`, `--no-output-bytecode` | Output AVM bytecode | `False` | -| `--match-algod-bytecode` | When outputting bytecode (via `--output-bytecode` or compiled programs), ensure bytecode matches algod output, by disabling additional optimizations | False | | `--out-dir OUT_DIR` | The path for outputting artefacts | Same folder as contract | | `--log-level {notset,debug,info,warning,error,critical}` | Minimum level to log to console | `info` | | `-g {0,1,2}`, `--debug-level {0,1,2}` | Output debug information level
`0` = No debug annotations
`1` = Output debug annotations
`2` = Reserved for future use, currently the same as `1` | `1` | diff --git a/docs/lg-compile.md b/docs/lg-compile.md index b26c4f7e64..e6182e127a 100644 --- a/docs/lg-compile.md +++ b/docs/lg-compile.md @@ -7,13 +7,6 @@ Once compiled, this bytecode can be utilized to construct AVM Application Call t The `--output-bytecode` option can be used to generate `.bin` files for smart contracts and logic signatures, producing an approval and clear program for each smart contract. -```{note} -The Puya compiler incorporates several optimizations that are not present in the bytecode output generated by the -[`/v2/teal/compile`](https://developer.algorand.org/docs/rest-apis/algod/#post-v2tealcompile) endpoint. -When comparing the outputs of PuyaPy and Algod these differences may be observed. -To disable these optimizations and produce bytecode identical to Algod use the `--match-algod-bytecode` option. -``` - ## Obtaining bytecode within other contracts The [`compile_contract`](#algopy.compile_contract) function takes an Algorand Python smart contract class and returns a [`CompiledContract`](#algopy.CompiledContract), From 917c913f8d4e205be0ac7c1fcf5f74d9c6dbe986 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Thu, 7 Nov 2024 15:42:52 +0800 Subject: [PATCH 3/3] doc: consolidate existing documentation on calling other applications --- docs/language-guide.md | 1 + docs/lg-arc4.md | 66 ------------------- docs/lg-calling-apps.md | 142 ++++++++++++++++++++++++++++++++++++++++ docs/lg-compile.md | 58 ---------------- docs/lg-transactions.md | 39 ----------- 5 files changed, 143 insertions(+), 163 deletions(-) create mode 100644 docs/lg-calling-apps.md diff --git a/docs/language-guide.md b/docs/language-guide.md index 7c3d7cc585..ef035576da 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -80,6 +80,7 @@ lg-ops lg-opcode-budget lg-arc4 lg-arc28 +lg-calling-apps lg-compile lg-unsupported-python-features ``` diff --git a/docs/lg-arc4.md b/docs/lg-arc4.md index f2022d061d..a51377fea6 100644 --- a/docs/lg-arc4.md +++ b/docs/lg-arc4.md @@ -221,69 +221,3 @@ Which means if one reference is mutated, the other references would not see the 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) - -## Typed clients - -[`arc4.abi_call`](#algopy.arc4.abi_call) can be used to do type safe calls to an ABI method of another contract, these -calls can be expressed in a few ways. - -### ARC4Contract method - -An ARC4Contract method written in Algorand Python can be referenced directly e.g. - -```python -from algopy import arc4, subroutine - -class HelloWorldContract(arc4.ARC4Contract): - - def hello(self, name: arc4.String) -> arc4.String: ... # implementation omitted - -@subroutine -def call_another_contract() -> None: - result, txn = arc4.abi_call(HelloWorldContract.hello, arc4.String("World"), app=...) - assert result == "Hello, World" -``` - -### ARC4Client method - -A ARC4Client client represents the ARC4 abimethods of a smart contract and can be used to call abimethods in a type safe way - -ARC4Client's can be produced by using `puyapy --output-client=True` when compiling a smart contract -(this would be useful if you wanted to publish a client for consumption by other smart contracts) -An ARC4Client can also be be generated from an ARC-32 application.json using `puyapy-clientgen` -e.g. `puyapy-clientgen examples/hello_world_arc4/out/HelloWorldContract.arc32.json`, this would be -the recommended approach for calling another smart contract that is not written in Algorand Python or does not provide the source - -```python -from algopy import arc4, subroutine - -class HelloWorldClient(arc4.ARC4Client): - - def hello(self, name: arc4.String) -> arc4.String: ... - -@subroutine -def call_another_contract() -> None: - # can reference another algopy contract method - result, txn = arc4.abi_call(HelloWorldClient.hello, arc4.String("World"), app=...) - assert result == "Hello, World" -``` - -### Method signature or name - -An ARC4 method selector can be used e.g. `"hello(string)string` along with a type index to specify the return type. -Additionally just a name can be provided and the method signature will be inferred e.g. - -```python -from algopy import arc4, subroutine - - -@subroutine -def call_another_contract() -> None: - # can reference a method selector - result, txn = arc4.abi_call[arc4.String]("hello(string)string", arc4.String("Algo"), app=...) - assert result == "Hello, Algo" - - # can reference a method name, the method selector is inferred from arguments and return type - result, txn = arc4.abi_call[arc4.String]("hello", "There", app=...) - assert result == "Hello, There" -``` diff --git a/docs/lg-calling-apps.md b/docs/lg-calling-apps.md new file mode 100644 index 0000000000..966ffe1650 --- /dev/null +++ b/docs/lg-calling-apps.md @@ -0,0 +1,142 @@ +# Calling other applications + +The preferred way to call other smart contracts is using [`algopy.arc4.abi_call`](#algopyarc4abi_call), [`algopy.arc4.arc4_create`](#algopyarc4arc4_create) or +[`algopy.arc4.arc4_update`](#algopyarc4arc4_update). These methods support type checking and encoding of arguments, decoding of results, group transactions, +and in the case of `arc4_create` and `arc4_update` automatic inclusion of approval and clear state programs. + +## `algopy.arc4.abi_call` + +[`algopy.arc4.abi_call`](#algopy.arc4.abi_call) can be used to call other ARC4 contracts, the first argument should refer to +an ARC4 method either by referencing an Algorand Python [`algopy.arc4.ARC4Contract`](#algopy.arc4.ARC4Contract) method, +an [`algopy.arc4.ARC4Client`](#algopy.arc4.ARC4Client) method generated from an ARC-32 app spec, or a string representing +the ARC4 method signature or name. +The following arguments should then be the arguments required for the call, these arguments will be type checked and converted where appropriate. +Any other related transaction parameters such as `app_id`, `fee` etc. can also be provided as keyword arguments. + +If the ARC4 method returns an ARC4 result then the result will be a tuple of the ARC4 result and the inner transaction. +If the ARC4 method does not return a result, or if the result type is not fully qualified then just the inner transaction is returned. + +```python +from algopy import Application, ARC4Contract, String, arc4, subroutine + +class HelloWorld(ARC4Contract): + + @arc4.abimethod() + def greet(self, name: String) -> String: + return "Hello " + name + +@subroutine +def call_existing_application(app: Application) -> None: + greeting, greet_txn = arc4.abi_call(HelloWorld.greet, "there", app_id=app) + + assert greeting == "Hello there" + assert greet_txn.app_id == 1234 +``` + + +### Alternative ways to use `arc4.abi_call` + +#### ARC4Client method + +A ARC4Client client represents the ARC4 abimethods of a smart contract and can be used to call abimethods in a type safe way + +ARC4Client's can be produced by using `puyapy --output-client=True` when compiling a smart contract +(this would be useful if you wanted to publish a client for consumption by other smart contracts) +An ARC4Client can also be be generated from an ARC-32 application.json using `puyapy-clientgen` +e.g. `puyapy-clientgen examples/hello_world_arc4/out/HelloWorldContract.arc32.json`, this would be +the recommended approach for calling another smart contract that is not written in Algorand Python or does not provide the source + +```python +from algopy import arc4, subroutine + +class HelloWorldClient(arc4.ARC4Client): + + def hello(self, name: arc4.String) -> arc4.String: ... + +@subroutine +def call_another_contract() -> None: + # can reference another algopy contract method + result, txn = arc4.abi_call(HelloWorldClient.hello, arc4.String("World"), app=...) + assert result == "Hello, World" +``` + +#### Method signature or name + +An ARC4 method selector can be used e.g. `"hello(string)string` along with a type index to specify the return type. +Additionally just a name can be provided and the method signature will be inferred e.g. + +```python +from algopy import arc4, subroutine + + +@subroutine +def call_another_contract() -> None: + # can reference a method selector + result, txn = arc4.abi_call[arc4.String]("hello(string)string", arc4.String("Algo"), app=...) + assert result == "Hello, Algo" + + # can reference a method name, the method selector is inferred from arguments and return type + result, txn = arc4.abi_call[arc4.String]("hello", "There", app=...) + assert result == "Hello, There" +``` + + +## `algopy.arc4.arc4_create` + +[`algopy.arc4.arc4_create`](#algopy.arc4.arc4_create) can be used to create ARC4 applications, and will automatically populate required fields for app creation (such as approval program, clear state program, and global/local state allocation). + +Like [`algopy.arc4.abi_call`](lg-transactions.md#arc4-application-calls) it handles ARC4 arguments and provides ARC4 return values. + +If the compiled programs and state allocation fields need to be customized (for example due to [template variables](#within-other-contracts)), +this can be done by passing a [`algopy.CompiledContract`](#algopy.CompiledContract) via the `compiled` keyword argument. + +```python +from algopy import ARC4Contract, String, arc4, subroutine + +class HelloWorld(ARC4Contract): + + @arc4.abimethod() + def greet(self, name: String) -> String: + return "Hello " + name + +@subroutine +def create_new_application() -> None: + hello_world_app = arc4.arc4_create(HelloWorld).created_app + + greeting, _txn = arc4.abi_call(HelloWorld.greet, "there", app_id=hello_world_app) + + assert greeting == "Hello there" +``` + +## `algopy.arc4.arc4_update` + +[`algopy.arc4.arc4_update`](#algopy.arc4.arc4_update) is used to update an existing ARC4 application and will automatically populate the required approval and clear state program fields. + +Like [`algopy.arc4.abi_call`](lg-transactions.md#arc4-application-calls) it handles ARC4 arguments and provides ARC4 return values. + +If the compiled programs need to be customized (for example due to (for example due to [template variables](#within-other-contracts)), +this can be done by passing a [`algopy.CompiledContract`](#algopy.CompiledContract) via the `compiled` keyword argument. + +```python +from algopy import Application, ARC4Contract, String, arc4, subroutine + +class NewApp(ARC4Contract): + + @arc4.abimethod() + def greet(self, name: String) -> String: + return "Hello " + name + +@subroutine +def update_existing_application(existing_app: Application) -> None: + hello_world_app = arc4.arc4_update(NewApp, app_id=existing_app) + + greeting, _txn = arc4.abi_call(NewApp.greet, "there", app_id=hello_world_app) + + assert greeting == "Hello there" +``` + +## Using `itxn.ApplicationCall` + +If the application being called is not an ARC4 contract, or an application specification is not available, +then [`algopy.itxn.ApplicationCall`](#algopy.itxn.ApplicationCall) can be used. This approach is generally more verbose +than the above approaches, so should only be used if required. See [here](./lg-transactions.md#create-an-arc4-application-and-then-call-it) for an example diff --git a/docs/lg-compile.md b/docs/lg-compile.md index e6182e127a..6062823fa8 100644 --- a/docs/lg-compile.md +++ b/docs/lg-compile.md @@ -16,64 +16,6 @@ This compiled contract can then be used to create an [`algopy.itxn.ApplicationCa The [`compile_logicsig`](#algopy.compile_logicsig) takes an Algorand Python logic signature and returns a [`CompiledLogicSig`](#algopy.CompiledLogicSig), which can be used to verify if a transaction has been signed by a particular logic signature. -## ARC4 contracts - -Additional functions are available for [creating](lg-compile.md#create) and [updating](lg-compile.md#update) ARC4 applications on-chain via an inner transaction. - -### Create - -[`algopy.arc4.arc4_create`](#algopy.arc4.arc4_create) can be used to create ARC4 applications, and will automatically populate required fields for app creation (such as approval program, clear state program, and global/local state allocation). - -Like [`algopy.arc4.abi_call`](lg-transactions.md#arc4-application-calls) it handles ARC4 arguments and provides ARC4 return values. - -If the compiled programs and state allocation fields need to be customized (for example due to [template variables](#within-other-contracts)), -this can be done by passing a [`algopy.CompiledContract`](#algopy.CompiledContract) via the `compiled` keyword argument. - -```python -from algopy import ARC4Contract, String, arc4, subroutine - -class HelloWorld(ARC4Contract): - - @arc4.abimethod() - def greet(self, name: String) -> String: - return "Hello " + name - -@subroutine -def create_new_application() -> None: - hello_world_app = arc4.arc4_create(HelloWorld).created_app - - greeting, _txn = arc4.abi_call(HelloWorld.greet, "there", app_id=hello_world_app) - - assert greeting == "Hello there" -``` - -### Update - -[`algopy.arc4.arc4_update`](#algopy.arc4.arc4_update) is used to update an existing ARC4 application and will automatically populate the required approval and clear state program fields. - -Like [`algopy.arc4.abi_call`](lg-transactions.md#arc4-application-calls) it handles ARC4 arguments and provides ARC4 return values. - -If the compiled programs need to be customized (for example due to (for example due to [template variables](#within-other-contracts)), -this can be done by passing a [`algopy.CompiledContract`](#algopy.CompiledContract) via the `compiled` keyword argument. - -```python -from algopy import Application, ARC4Contract, String, arc4, subroutine - -class NewApp(ARC4Contract): - - @arc4.abimethod() - def greet(self, name: String) -> String: - return "Hello " + name - -@subroutine -def update_existing_application(existing_app: Application) -> None: - hello_world_app = arc4.arc4_update(NewApp, app_id=existing_app) - - greeting, _txn = arc4.abi_call(NewApp.greet, "there", app_id=hello_world_app) - - assert greeting == "Hello there" -``` - ## Template variables Algorand Python supports defining [`algopy.TemplateVar`](#algopy.TemplateVar) variables that can be substituted during compilation. diff --git a/docs/lg-transactions.md b/docs/lg-transactions.md index 4d24a95637..362ac1744d 100644 --- a/docs/lg-transactions.md +++ b/docs/lg-transactions.md @@ -149,14 +149,6 @@ def example() -> None: ).submit() # extract result hello_world_result = arc4.String.from_log(call_txn.last_log) - - # OR, call it automatic ARC4 encoding, type validation and result handling - hello_world_result, call_txn = arc4.abi_call[arc4.String]( # declare return type - "hello(string)string", # method signature to call - "again", # abi method arguments - fee=0, # other transaction parameters - app_id=app - ) ``` #### Create and submit transactions in a loop @@ -175,37 +167,6 @@ def example(receivers: tuple[Account, Account, Account]) -> None: ).submit() ``` -### ARC4 Application calls - -#### `algopy.arc4.abi_call` - -[`algopy.arc4.abi_call`](#algopy.arc4.abi_call) can be used to call other ARC4 contracts, the first argument should refer to -an ARC4 method either by referencing an Algorand Python [`algopy.arc4.ARC4Contract`](#algopy.arc4.ARC4Contract) method, -an [`algopy.arc4.ARC4Client`](#algopy.arc4.ARC4Client) method generated from an ARC-32 app spec, or a string representing -the ARC4 method signature or name. -The following arguments should then be the arguments required for the call, these arguments will be type checked and converted where appropriate. -Any other related transaction parameters such as `app_id`, `fee` etc. can also be provided as keyword arguments. - -If the ARC4 method returns an ARC4 result then the result will be a tuple of the ARC4 result and the inner transaction. -If the ARC4 method does not return a result, or if the result type is not fully qualified then just the inner transaction is returned. - -```python -from algopy import Application, ARC4Contract, String, arc4, subroutine - -class HelloWorld(ARC4Contract): - - @arc4.abimethod() - def greet(self, name: String) -> String: - return "Hello " + name - -@subroutine -def call_existing_application(app: Application) -> None: - greeting, greet_txn = arc4.abi_call(HelloWorld.greet, "there", app_id=app) - - assert greeting == "Hello there" - assert greet_txn.app_id == 1234 -``` - ### Limitations Inner transactions are powerful, but currently do have some restrictions in how they are used.