diff --git a/.flake8 b/.flake8 index 262d4b172..c968f1e00 100644 --- a/.flake8 +++ b/.flake8 @@ -10,7 +10,8 @@ ignore = per-file-ignores = pyteal/compiler/optimizer/__init__.py: F401 - examples/application/abi/algobank.py: F403, F405 + examples/application/abi/algobank/algobank.py: F403, F405 + examples/application/abi/poll/contract.py: F403, F405 examples/application/asset.py: F403, F405 examples/application/opup.py: F403, F405 examples/application/security_token.py: F403, F405 diff --git a/docs/abi.rst b/docs/abi.rst index 63d65147c..85256af93 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -916,12 +916,12 @@ In addition to receiving the approval and clear state programs, the :any:`Router Here's an example of a complete application that uses the :any:`Router` class: -.. literalinclude:: ../examples/application/abi/algobank.py +.. literalinclude:: ../examples/application/abi/algobank/algobank.py :language: python This example uses the :code:`Router.compile_program` method to create the approval program, clear state program, and contract description for the "AlgoBank" contract. The produced :code:`algobank.json` file is below: -.. literalinclude:: ../examples/application/abi/algobank.json +.. literalinclude:: ../examples/application/abi/algobank/algobank.json :language: json Calling an ARC-4 Program diff --git a/examples/application/abi/algobank/__init__.py b/examples/application/abi/algobank/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/application/abi/algobank.json b/examples/application/abi/algobank/algobank.json similarity index 100% rename from examples/application/abi/algobank.json rename to examples/application/abi/algobank/algobank.json diff --git a/examples/application/abi/algobank.py b/examples/application/abi/algobank/algobank.py similarity index 100% rename from examples/application/abi/algobank.py rename to examples/application/abi/algobank/algobank.py diff --git a/examples/application/abi/algobank_approval.teal b/examples/application/abi/algobank/algobank_approval.teal similarity index 100% rename from examples/application/abi/algobank_approval.teal rename to examples/application/abi/algobank/algobank_approval.teal diff --git a/examples/application/abi/algobank_clear_state.teal b/examples/application/abi/algobank/algobank_clear_state.teal similarity index 100% rename from examples/application/abi/algobank_clear_state.teal rename to examples/application/abi/algobank/algobank_clear_state.teal diff --git a/examples/application/abi/poll/__init__.py b/examples/application/abi/poll/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/application/abi/poll/approval.teal b/examples/application/abi/poll/approval.teal new file mode 100644 index 000000000..bcad59a35 --- /dev/null +++ b/examples/application/abi/poll/approval.teal @@ -0,0 +1,451 @@ +#pragma version 8 +txn NumAppArgs +int 0 +== +bnz main_l12 +txna ApplicationArgs 0 +method "create(string[3],bool)void" +== +bnz main_l11 +txna ApplicationArgs 0 +method "open()void" +== +bnz main_l10 +txna ApplicationArgs 0 +method "close()void" +== +bnz main_l9 +txna ApplicationArgs 0 +method "submit(uint8)void" +== +bnz main_l8 +txna ApplicationArgs 0 +method "status()(bool,bool,(string,uint64)[3])" +== +bnz main_l7 +err +main_l7: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +callsub status_4 +store 2 +byte 0x151f7c75 +load 2 +concat +log +int 1 +return +main_l8: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +int 0 +getbyte +callsub submit_3 +int 1 +return +main_l9: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +callsub close_2 +int 1 +return +main_l10: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +callsub open_1 +int 1 +return +main_l11: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +== +&& +assert +txna ApplicationArgs 1 +store 0 +txna ApplicationArgs 2 +int 0 +int 8 +* +getbit +store 1 +load 0 +load 1 +callsub create_0 +int 1 +return +main_l12: +txn OnCompletion +int DeleteApplication +== +bnz main_l14 +err +main_l14: +txn ApplicationID +int 0 +!= +assert +txn Sender +global CreatorAddress +== +assert +int 1 +return + +// create +create_0: +store 20 +store 19 +byte 0x6f70656e +int 0 +app_global_put +byte 0x72657375626d6974 +load 20 +app_global_put +byte 0x6f7074696f6e5f6e616d655f00 +load 19 +load 19 +int 2 +int 0 +* +extract_uint16 +int 0 +int 1 ++ +int 3 +== +bnz create_0_l8 +load 19 +int 2 +int 0 +* +int 2 ++ +extract_uint16 +create_0_l2: +substring3 +extract 2 0 +app_global_put +byte 0x6f7074696f6e5f636f756e745f00 +int 0 +app_global_put +byte 0x6f7074696f6e5f6e616d655f01 +load 19 +load 19 +int 2 +int 1 +* +extract_uint16 +int 1 +int 1 ++ +int 3 +== +bnz create_0_l7 +load 19 +int 2 +int 1 +* +int 2 ++ +extract_uint16 +create_0_l4: +substring3 +extract 2 0 +app_global_put +byte 0x6f7074696f6e5f636f756e745f01 +int 0 +app_global_put +byte 0x6f7074696f6e5f6e616d655f02 +load 19 +load 19 +int 2 +int 2 +* +extract_uint16 +int 2 +int 1 ++ +int 3 +== +bnz create_0_l6 +load 19 +int 2 +int 2 +* +int 2 ++ +extract_uint16 +b create_0_l9 +create_0_l6: +load 19 +len +b create_0_l9 +create_0_l7: +load 19 +len +b create_0_l4 +create_0_l8: +load 19 +len +b create_0_l2 +create_0_l9: +substring3 +extract 2 0 +app_global_put +byte 0x6f7074696f6e5f636f756e745f02 +int 0 +app_global_put +retsub + +// open +open_1: +byte 0x6f70656e +app_global_get +! +assert +byte 0x6f70656e +int 1 +app_global_put +retsub + +// close +close_2: +byte 0x6f70656e +app_global_get +assert +byte 0x6f70656e +int 0 +app_global_put +retsub + +// submit +submit_3: +store 21 +load 21 +int 3 +< +assert +byte 0x6f7074696f6e5f636f756e745f00 +int 13 +load 21 +setbyte +store 22 +txn Sender +box_get +store 25 +store 24 +load 25 +bz submit_3_l2 +byte 0x72657375626d6974 +app_global_get +assert +byte 0x6f7074696f6e5f636f756e745f00 +int 13 +load 24 +btoi +setbyte +store 23 +load 23 +load 23 +app_global_get +int 1 +- +app_global_put +submit_3_l2: +txn Sender +byte 0x00 +int 0 +load 21 +setbyte +box_put +load 22 +load 22 +app_global_get +int 1 ++ +app_global_put +retsub + +// status +status_4: +byte 0x72657375626d6974 +app_global_get +! +! +store 3 +byte 0x6f70656e +app_global_get +! +! +store 4 +byte 0x6f7074696f6e5f6e616d655f00 +app_global_get +store 5 +load 5 +len +itob +extract 6 0 +load 5 +concat +store 5 +byte 0x6f7074696f6e5f636f756e745f00 +app_global_get +store 6 +load 5 +store 11 +int 10 +itob +extract 6 0 +load 6 +itob +concat +load 11 +concat +store 7 +byte 0x6f7074696f6e5f6e616d655f01 +app_global_get +store 5 +load 5 +len +itob +extract 6 0 +load 5 +concat +store 5 +byte 0x6f7074696f6e5f636f756e745f01 +app_global_get +store 6 +load 5 +store 12 +int 10 +itob +extract 6 0 +load 6 +itob +concat +load 12 +concat +store 8 +byte 0x6f7074696f6e5f6e616d655f02 +app_global_get +store 5 +load 5 +len +itob +extract 6 0 +load 5 +concat +store 5 +byte 0x6f7074696f6e5f636f756e745f02 +app_global_get +store 6 +load 5 +store 13 +int 10 +itob +extract 6 0 +load 6 +itob +concat +load 13 +concat +store 9 +load 7 +store 17 +load 17 +store 16 +int 6 +store 14 +load 14 +load 17 +len ++ +store 15 +load 15 +int 65536 +< +assert +load 14 +itob +extract 6 0 +load 8 +store 17 +load 16 +load 17 +concat +store 16 +load 15 +store 14 +load 14 +load 17 +len ++ +store 15 +load 15 +int 65536 +< +assert +load 14 +itob +extract 6 0 +concat +load 9 +store 17 +load 16 +load 17 +concat +store 16 +load 15 +store 14 +load 14 +itob +extract 6 0 +concat +load 16 +concat +store 10 +byte 0x00 +int 0 +load 3 +setbit +int 1 +load 4 +setbit +load 10 +store 18 +int 3 +itob +extract 6 0 +concat +load 18 +concat +retsub \ No newline at end of file diff --git a/examples/application/abi/poll/clear_state.teal b/examples/application/abi/poll/clear_state.teal new file mode 100644 index 000000000..ba95445f2 --- /dev/null +++ b/examples/application/abi/poll/clear_state.teal @@ -0,0 +1,3 @@ +#pragma version 8 +int 0 +return \ No newline at end of file diff --git a/examples/application/abi/poll/contract.json b/examples/application/abi/poll/contract.json new file mode 100644 index 000000000..159fa986c --- /dev/null +++ b/examples/application/abi/poll/contract.json @@ -0,0 +1,65 @@ +{ + "name": "OpenPollingApp", + "methods": [ + { + "name": "create", + "args": [ + { + "type": "string[3]", + "name": "options", + "desc": "A list of options for the poll. This list should not contain duplicate entries." + }, + { + "type": "bool", + "name": "can_resubmit", + "desc": "Whether this poll allows accounts to change their submissions or not." + } + ], + "returns": { + "type": "void" + }, + "desc": "Create a new polling application." + }, + { + "name": "open", + "args": [], + "returns": { + "type": "void" + }, + "desc": "Marks this poll as open.\nThis will fail if the poll is already open.\nThe poll must be open in order to receive user input." + }, + { + "name": "close", + "args": [], + "returns": { + "type": "void" + }, + "desc": "Marks this poll as closed.\nThis will fail if the poll is already closed." + }, + { + "name": "submit", + "args": [ + { + "type": "uint8", + "name": "choice", + "desc": "The choice made by the sender. This must be an index into the options for this poll." + } + ], + "returns": { + "type": "void" + }, + "desc": "Submit a response to the poll.\nSubmissions can only be received if the poll is open. If the poll is closed, this will fail.\nIf a submission has already been made by the sender and the poll allows resubmissions, the sender's choice will be updated to the most recent submission. If the poll does not allow resubmissions, this action will fail." + }, + { + "name": "status", + "args": [], + "returns": { + "type": "(bool,bool,(string,uint64)[3])", + "desc": "A tuple containing the following information, in order: whether the poll allows resubmission, whether the poll is open, and an array of the poll's current results. This array contains one entry per option, and each entry is a tuple of that option's value and the number of accounts who have voted for it." + }, + "desc": "Get the status of this poll." + } + ], + "networks": {}, + "desc": "A polling application with no restrictions on who can participate." +} \ No newline at end of file diff --git a/examples/application/abi/poll/contract.py b/examples/application/abi/poll/contract.py new file mode 100644 index 000000000..1689065cc --- /dev/null +++ b/examples/application/abi/poll/contract.py @@ -0,0 +1,182 @@ +# This example is provided for informational purposes only and has not been audited for security. +import json +from typing import Literal +from pyteal import * + + +def on_delete() -> Expr: + return Assert(Txn.sender() == Global.creator_address()) + + +router = Router( + name="OpenPollingApp", + descr="A polling application with no restrictions on who can participate.", + bare_calls=BareCallActions( + delete_application=OnCompleteAction.call_only(on_delete()) + ), +) + +open_key = Bytes(b"open") +resubmit_key = Bytes(b"resubmit") +option_name_prefix = b"option_name_" +option_name_keys = [ + Bytes(option_name_prefix + b"\x00"), + Bytes(option_name_prefix + b"\x01"), + Bytes(option_name_prefix + b"\x02"), +] +option_count_prefix = b"option_count_" +option_count_keys = [ + Bytes(option_count_prefix + b"\x00"), + Bytes(option_count_prefix + b"\x01"), + Bytes(option_count_prefix + b"\x02"), +] + + +@router.method(no_op=CallConfig.CREATE) +def create( + options: abi.StaticArray[abi.String, Literal[3]], can_resubmit: abi.Bool +) -> Expr: + """Create a new polling application. + + Args: + options: A list of options for the poll. This list should not contain duplicate entries. + can_resubmit: Whether this poll allows accounts to change their submissions or not. + """ + return Seq( + App.globalPut(open_key, Int(0)), + App.globalPut(resubmit_key, can_resubmit.get()), + App.globalPut(option_name_keys[0], options[0].use(lambda value: value.get())), + App.globalPut(option_count_keys[0], Int(0)), + App.globalPut(option_name_keys[1], options[1].use(lambda value: value.get())), + App.globalPut(option_count_keys[1], Int(0)), + App.globalPut(option_name_keys[2], options[2].use(lambda value: value.get())), + App.globalPut(option_count_keys[2], Int(0)), + ) + + +@router.method(name="open") +def open_poll() -> Expr: + """Marks this poll as open. + + This will fail if the poll is already open. + + The poll must be open in order to receive user input. + """ + return Seq( + Assert(Not(App.globalGet(open_key))), + App.globalPut(open_key, Int(1)), + ) + + +@router.method(name="close") +def close_poll() -> Expr: + """Marks this poll as closed. + + This will fail if the poll is already closed. + """ + return Seq( + Assert(App.globalGet(open_key)), + App.globalPut(open_key, Int(0)), + ) + + +@router.method +def submit(choice: abi.Uint8) -> Expr: + """Submit a response to the poll. + + Submissions can only be received if the poll is open. If the poll is closed, this will fail. + + If a submission has already been made by the sender and the poll allows resubmissions, the + sender's choice will be updated to the most recent submission. If the poll does not allow + resubmissions, this action will fail. + + Args: + choice: The choice made by the sender. This must be an index into the options for this poll. + """ + new_choice_count_key = ScratchVar(TealType.bytes) + old_choice_count_key = ScratchVar(TealType.bytes) + return Seq( + Assert(choice.get() < Int(3)), + new_choice_count_key.store( + SetByte(option_count_keys[0], Int(len(option_count_prefix)), choice.get()) + ), + sender_box := App.box_get(Txn.sender()), + If(sender_box.hasValue()).Then( + # the sender has already submitted a response, so it must be cleared + Assert(App.globalGet(resubmit_key)), + old_choice_count_key.store( + SetByte( + option_count_keys[0], + Int(len(option_count_prefix)), + Btoi(sender_box.value()), + ) + ), + App.globalPut( + old_choice_count_key.load(), + App.globalGet(old_choice_count_key.load()) - Int(1), + ), + ), + App.box_put(Txn.sender(), choice.encode()), + App.globalPut( + new_choice_count_key.load(), + App.globalGet(new_choice_count_key.load()) + Int(1), + ), + ) + + +class PollStatus(abi.NamedTuple): + can_resubmit: abi.Field[abi.Bool] + is_open: abi.Field[abi.Bool] + results: abi.Field[abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[3]]] + + +@router.method +def status(*, output: PollStatus) -> Expr: + """Get the status of this poll. + + Returns: + A tuple containing the following information, in order: whether the poll allows + resubmission, whether the poll is open, and an array of the poll's current results. This + array contains one entry per option, and each entry is a tuple of that option's value and + the number of accounts who have voted for it. + """ + can_resubmit = abi.make(abi.Bool) + is_open = abi.make(abi.Bool) + option_name = abi.make(abi.String) + option_count = abi.make(abi.Uint64) + partial_results = [ + abi.make(abi.Tuple2[abi.String, abi.Uint64]), + abi.make(abi.Tuple2[abi.String, abi.Uint64]), + abi.make(abi.Tuple2[abi.String, abi.Uint64]), + ] + results = abi.make(abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[3]]) + return Seq( + can_resubmit.set(App.globalGet(resubmit_key)), + is_open.set(App.globalGet(open_key)), + option_name.set(App.globalGet(option_name_keys[0])), + option_count.set(App.globalGet(option_count_keys[0])), + partial_results[0].set(option_name, option_count), + option_name.set(App.globalGet(option_name_keys[1])), + option_count.set(App.globalGet(option_count_keys[1])), + partial_results[1].set(option_name, option_count), + option_name.set(App.globalGet(option_name_keys[2])), + option_count.set(App.globalGet(option_count_keys[2])), + partial_results[2].set(option_name, option_count), + results.set([partial_results[0], partial_results[1], partial_results[2]]), + output.set(can_resubmit, is_open, results), + ) + + +approval_program, clear_state_program, contract = router.compile_program( + version=8, optimize=OptimizeOptions(scratch_slots=True) +) + +if __name__ == "__main__": + with open("approval.teal", "w") as f: + f.write(approval_program) + + with open("clear_state.teal", "w") as f: + f.write(clear_state_program) + + with open("contract.json", "w") as f: + f.write(json.dumps(contract.dictify(), indent=4)) diff --git a/tests/unit/compile_test.py b/tests/unit/compile_test.py index 427c7e4c6..ed608aac8 100644 --- a/tests/unit/compile_test.py +++ b/tests/unit/compile_test.py @@ -6,13 +6,13 @@ def test_abi_algobank(): - from examples.application.abi.algobank import ( + from examples.application.abi.algobank.algobank import ( approval_program, clear_state_program, contract, ) - target_dir = Path.cwd() / "examples" / "application" / "abi" + target_dir = Path.cwd() / "examples" / "application" / "abi" / "algobank" with open( target_dir / "algobank_approval.teal", "r" @@ -35,6 +35,34 @@ def test_abi_algobank(): assert contract.dictify() == expected_contract +def test_abi_poll(): + from examples.application.abi.poll.contract import ( + approval_program, + clear_state_program, + contract, + ) + + target_dir = Path.cwd() / "examples" / "application" / "abi" / "poll" + + with open(target_dir / "approval.teal", "r") as expected_approval_program_file: + expected_approval_program = "".join( + expected_approval_program_file.readlines() + ).strip() + assert approval_program == expected_approval_program + + with open( + target_dir / "clear_state.teal", "r" + ) as expected_clear_state_program_file: + expected_clear_state_program = "".join( + expected_clear_state_program_file.readlines() + ).strip() + assert clear_state_program == expected_clear_state_program + + with open(target_dir / "contract.json", "r") as expected_contract_file: + expected_contract = json.load(expected_contract_file) + assert contract.dictify() == expected_contract + + def test_basic_bank(): from examples.signature.basic import bank_for_account