Skip to content

Commit

Permalink
Document new ABI features (#400)
Browse files Browse the repository at this point in the history
* Initial commit

* Fix abi-docs tests (#402)

* Fix abi-docs Sphinx warnings (#401)

* Extend abi-docs with experimental design language (#403)

* Add type fundamentals section

* Add basic type usage and some docstrings for referenced methods

* Finish documenting set and fix overloaded method docs

* Add docstrings for get and __getitem__

* Add reference type docs

* Add txn type examples

* Fix errors

* Make ComputedValue parameter type covariant

* ComputedValue and subroutine sections

* ...wasn't included in previous commit

* Add bare app call and method registration examples

* Add router e2e example and compilation explanation

* Fix post-merge linter/test failures

* Add calling documentation

* Partially address feedback

* Respond to feedback

* Resolve TODOs

* Add pragma references

* didn't make it into the previous commit

* Fix Bool.__module__

* Mention stack size limit

* More pragma documentation

* Address other feedback

* Warn about reference type limits

* **cannot**

Co-authored-by: Michael Diamant <michaeldiamant@users.noreply.github.com>
  • Loading branch information
jasonpaulos and michaeldiamant authored Jul 20, 2022
1 parent 281f2d3 commit 657064f
Show file tree
Hide file tree
Showing 33 changed files with 1,789 additions and 194 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ignore =

per-file-ignores =
pyteal/compiler/optimizer/__init__.py: F401
examples/application/abi/algobank.py: F403, F405
examples/application/asset.py: F403, F405
examples/application/opup.py: F403, F405
examples/application/security_token.py: F403, F405
Expand Down
917 changes: 917 additions & 0 deletions docs/abi.rst

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,19 @@ PyTeal Package
:annotation: = <pyteal.TxnObject object>

The most recently submitted inner transaction. This is an instance of :any:`TxnObject`.

If a transaction group was submitted most recently, then this will be the last transaction in that group.

.. data:: Gitxn
:annotation: = <pyteal.InnerTxnGroup object>

The most recently submitted inner transaction group. This is an instance of :any:`InnerTxnGroup`.

If a single transaction was submitted most recently, then this will be a group of size 1.

.. automodule:: pyteal.abi
:members:
:undoc-members:
:imported-members:
:special-members: __getitem__
:show-inheritance:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ PyTeal **hasn't been security audited**. Use it at your own risk.
versions
compiler_optimization
opup
abi

.. toctree::
:maxdepth: 3
Expand Down
36 changes: 28 additions & 8 deletions docs/versions.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.. _versions:

TEAL Versions
Versions
=============

Each version of PyTeal compiles contracts for a specific version of TEAL. Newer versions of TEAL
Expand All @@ -18,14 +18,34 @@ TEAL Version PyTeal Version
6 >= 0.10.0
============ ==============

.. _version pragmas:

Version Pragmas
----------------

When writing a PyTeal smart contract, it's important to target a specific AVM version and to compile
with a single PyTeal version. This will ensure your compiled program remains consistent and has the
exact same behavior no matter when you compile it.

The :any:`pragma` function can be used to assert that the current PyTeal version matches a constraint
of your choosing. This can help strengthen the dependency your source code has on the PyTeal package
version you used when writing it.

If you are writing code for others to consume, or if your codebase has different PyTeal version
dependencies in different places, the :any:`Pragma` expression can be used to apply a pragma
constraint to only a section of the AST.

PyTeal v0.5.4 and Below
-----------------------

In order to support TEAL v2, PyTeal v0.6.0 breaks backward compatibility with v0.5.4. PyTeal
programs written for PyTeal version 0.5.4 and below will not compile properly and most likely will
display an error of the form :code:`AttributeError: * object has no attribute 'teal'`.

**WARNING:** before updating PyTeal to a version with generates TEAL v2 contracts and fixing the
programs to use the global function :any:`compileTeal` rather the class method :code:`.teal()`, make
sure your program abides by the TEAL safety guidelines `<https://developer.algorand.org/docs/reference/teal/guidelines/>`_.
Changing a v1 TEAL program to a v2 TEAL program without any code changes is insecure because v2
TEAL programs allow rekeying. Specifically, you must add a check that the :code:`RekeyTo` property
of any transaction is set to the zero address when updating an older PyTeal program from v0.5.4 and
below.
.. warning::
If you are updating from a v1 TEAL program, make
sure your program abides by the `TEAL safety guidelines <https://developer.algorand.org/docs/reference/teal/guidelines/>`_.
Changing a v1 TEAL program to a v2 TEAL program without any code changes is insecure because v2
TEAL programs allow rekeying. Specifically, you must add a check that the :code:`RekeyTo` property
of any transaction is set to the zero address when updating an older PyTeal program from v0.5.4 and
below.
Empty file.
54 changes: 54 additions & 0 deletions examples/application/abi/algobank.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "AlgoBank",
"methods": [
{
"name": "deposit",
"args": [
{
"type": "pay",
"name": "payment"
},
{
"type": "account",
"name": "sender"
}
],
"returns": {
"type": "void"
},
"desc": "This method receives a payment from an account opted into this app and records it in their local state. The caller may opt into this app during this call."
},
{
"name": "getBalance",
"args": [
{
"type": "account",
"name": "user"
}
],
"returns": {
"type": "uint64"
},
"desc": "Lookup the balance of a user held by this app."
},
{
"name": "withdraw",
"args": [
{
"type": "uint64",
"name": "amount"
},
{
"type": "account",
"name": "recipient"
}
],
"returns": {
"type": "void"
},
"desc": "Withdraw an amount of Algos held by this app. The sender of this method call will be the source of the Algos, and the destination will be the `recipient` argument. This may or may not be the same as the sender's address. This method will fail if the amount of Algos requested to be withdrawn exceeds the amount of Algos held by this app for the sender. The Algos will be transferred to the recipient using an inner transaction whose fee is set to 0, meaning the caller's transaction must include a surplus fee to cover the inner transaction."
}
],
"desc": null,
"networks": {}
}
113 changes: 113 additions & 0 deletions examples/application/abi/algobank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# This example is provided for informational purposes only and has not been audited for security.
from pyteal import *
import json


@Subroutine(TealType.none)
def assert_sender_is_creator() -> Expr:
return Assert(Txn.sender() == Global.creator_address())


# move any balance that the user has into the "lost" amount when they close out or clear state
transfer_balance_to_lost = App.globalPut(
Bytes("lost"),
App.globalGet(Bytes("lost")) + App.localGet(Txn.sender(), Bytes("balance")),
)

router = Router(
name="AlgoBank",
bare_calls=BareCallActions(
# approve a creation no-op call
no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE),
# approve opt-in calls during normal usage, and during creation as a convenience for the creator
opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.ALL),
# move any balance that the user has into the "lost" amount when they close out or clear state
close_out=OnCompleteAction(
action=transfer_balance_to_lost, call_config=CallConfig.CALL
),
clear_state=OnCompleteAction(
action=transfer_balance_to_lost, call_config=CallConfig.CALL
),
# only the creator can update or delete the app
update_application=OnCompleteAction(
action=assert_sender_is_creator, call_config=CallConfig.CALL
),
delete_application=OnCompleteAction(
action=assert_sender_is_creator, call_config=CallConfig.CALL
),
),
)


@router.method(no_op=CallConfig.CALL, opt_in=CallConfig.CALL)
def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:
"""This method receives a payment from an account opted into this app and records it in
their local state.
The caller may opt into this app during this call.
"""
return Seq(
Assert(payment.get().sender() == sender.address()),
Assert(payment.get().receiver() == Global.current_application_address()),
App.localPut(
sender.address(),
Bytes("balance"),
App.localGet(sender.address(), Bytes("balance")) + payment.get().amount(),
),
)


@router.method
def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:
"""Lookup the balance of a user held by this app."""
return output.set(App.localGet(user.address(), Bytes("balance")))


@router.method
def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:
"""Withdraw an amount of Algos held by this app.
The sender of this method call will be the source of the Algos, and the destination will be
the `recipient` argument. This may or may not be the same as the sender's address.
This method will fail if the amount of Algos requested to be withdrawn exceeds the amount of
Algos held by this app for the sender.
The Algos will be transferred to the recipient using an inner transaction whose fee is set
to 0, meaning the caller's transaction must include a surplus fee to cover the inner
transaction.
"""
return Seq(
# if amount is larger than App.localGet(Txn.sender(), Bytes("balance")), the subtraction
# will underflow and fail this method call
App.localPut(
Txn.sender(),
Bytes("balance"),
App.localGet(Txn.sender(), Bytes("balance")) - amount.get(),
),
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.Payment,
TxnField.receiver: recipient.address(),
TxnField.amount: amount.get(),
TxnField.fee: Int(0),
}
),
InnerTxnBuilder.Submit(),
)


approval_program, clear_state_program, contract = router.compile_program(
version=6, optimize=OptimizeOptions(scratch_slots=True)
)

if __name__ == "__main__":
with open("algobank_approval.teal", "w") as f:
f.write(approval_program)

with open("algobank_clear_state.teal", "w") as f:
f.write(clear_state_program)

with open("algobank.json", "w") as f:
f.write(json.dumps(contract.dictify(), indent=4))
Loading

0 comments on commit 657064f

Please sign in to comment.