From 067acaea678494e57d466ee2f13fc49adb6654bf Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Fri, 27 Jan 2023 14:52:14 +0000 Subject: [PATCH 1/2] Support the method pseudo op --- tealish/expression_nodes.py | 2 ++ tealish/langspec.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tealish/expression_nodes.py b/tealish/expression_nodes.py index 22f2e92..b882cb5 100644 --- a/tealish/expression_nodes.py +++ b/tealish/expression_nodes.py @@ -228,6 +228,8 @@ def process_op_call(self, op: Op) -> None: immediates[i] = x.name elif isinstance(x, Integer): immediates[i] = x.value + elif isinstance(x, Bytes): + immediates[i] = f'"{x.value}"' self.immediate_args = " ".join(map(str, immediates)) returns = op.returns_types[::-1] self.type = returns[0] if len(returns) == 1 else returns diff --git a/tealish/langspec.py b/tealish/langspec.py index d15b847..f54b5e9 100644 --- a/tealish/langspec.py +++ b/tealish/langspec.py @@ -8,6 +8,19 @@ abc = "ABCDEFGHIJK" +# Hopefully these will eventually be added to langspec.json. Including here until then. +# Note: Other pseudo ops like addr and base32 are not included here because their syntax isn't parseable by Tealish currently. +# e.g addr(RIKLQ5HEVXAOAWYSW2LGQFYGWVO4J6LIAQQ72ZRULHZ4KS5NRPCCKYPCUU) is not parseable because the address isn't quoted. +pseudo_ops = [ + { + "Name": "method", + "Opcode": "method", + "Size": 2, + "Args": [], + "Returns": ["B"], + }, +] + _opcode_type_map = { ".": AVMType.any, @@ -119,7 +132,9 @@ class LangSpec: def __init__(self, spec: Dict[str, Any]) -> None: self.is_packaged = False self.spec = spec - self.ops: Dict[str, Op] = {op["Name"]: Op(op) for op in spec["Ops"]} + self.ops: Dict[str, Op] = { + op["Name"]: Op(op) for op in (spec["Ops"] + pseudo_ops) + } self.fields: Dict[str, Any] = { "Global": self.ops["global"].arg_enum_dict, From 31b6d81e04df359525a22d6daaabcc086ba77a53 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Fri, 27 Jan 2023 19:53:32 +0000 Subject: [PATCH 2/2] Example of an app using the ARC4 ABI --- examples/arc4/app.tl | 165 +++++++++++++++++++++++ examples/arc4/build/abi.teal | 253 +++++++++++++++++++++++++++++++++++ examples/arc4/contract.json | 102 ++++++++++++++ examples/arc4/tests.py | 95 +++++++++++++ examples/arc4/utils.py | 32 +++++ tealish/__init__.py | 1 + tealish/base.py | 8 ++ 7 files changed, 656 insertions(+) create mode 100644 examples/arc4/app.tl create mode 100644 examples/arc4/build/abi.teal create mode 100644 examples/arc4/contract.json create mode 100644 examples/arc4/tests.py create mode 100644 examples/arc4/utils.py diff --git a/examples/arc4/app.tl b/examples/arc4/app.tl new file mode 100644 index 0000000..85ce5b0 --- /dev/null +++ b/examples/arc4/app.tl @@ -0,0 +1,165 @@ +#pragma version 8 + +struct Item: + id: int + owner: bytes[32] + content: bytes[100] +end + +if Txn.ApplicationID == 0: + # Handle Create App + exit(1) +end + +switch Txn.OnCompletion: + NoOp: main + OptIn: opt_in + CloseOut: close_out + UpdateApplication: update_app + DeleteApplication: delete_app +end + +block opt_in: + # Disallow Opt In + exit(0) +end + +block close_out: + # Disallow Closing Out + exit(0) +end + +block update_app: + # Handle Update App + # Only allow the Creator to update the app + assert(Txn.Sender == Global.CreatorAddress) + exit(1) +end + +block delete_app: + # Handle Delete App + # Only allow the Creator to delete the app + assert(Txn.Sender == Global.CreatorAddress) + exit(1) +end + +block main: + switch Txn.ApplicationArgs[0]: + method("setup()void"): setup + method("add(uint64,uint64)uint64"): add + method("mulw(uint64,uint64)uint128"): mulw + method("hello(string)string"): hello + method("send(address)void"): send + method("store_data(byte[10],byte[100])void"): store_data + method("store_tuple(byte[10],(uint64,address,byte[100]))void"): store_tuple + method("balance(account)uint64"): balance + end + + block setup: + # Do some setup stuff here + exit(1) + end + + block add: + # Add 2 integers + int x = btoi(Txn.ApplicationArgs[1]) + int y = btoi(Txn.ApplicationArgs[2]) + int result = x + y + abi_return(abi_encode_uint64(result)) + exit(1) + end + + block mulw: + # Multiply 2 integers, returing a uint128 + bytes x = Txn.ApplicationArgs[1] + bytes y = Txn.ApplicationArgs[2] + bytes result = x b* y + abi_return(abi_encode_uint128(result)) + exit(1) + end + + + block hello: + # Return a greeting + bytes name = abi_decode_string(Txn.ApplicationArgs[1]) + bytes result = concat("Hello ", name) + abi_return(abi_encode_string(result)) + exit(1) + end + + block send: + # Send some Algo to the given address + bytes address = Txn.ApplicationArgs[1] + inner_txn: + TypeEnum: Pay + Receiver: address + Amount: 10000000 + Fee: 0 + end + exit(1) + end + + block store_data: + # Store some fixed size data in a box with the specified key + bytes key = Txn.ApplicationArgs[1] + bytes data = Txn.ApplicationArgs[2] + box_put(key, data) + exit(1) + end + + block store_tuple: + # Store some structured data in a box with the specified key + bytes key = Txn.ApplicationArgs[1] + Item data = Txn.ApplicationArgs[2] + # make some assertion about the data for the fun of it + assert(data.owner == Txn.Sender) + box_put(key, data) + exit(1) + end + + block balance: + # Return balance of the specified account + int result = balance(Txn.Accounts[btoi(Txn.ApplicationArgs[1])]) + abi_return(abi_encode_uint64(result)) + exit(1) + end +end + +func abi_return(result: bytes): + log(concat("\x15\x1f\x7c\x75", result)) + return +end + +func abi_decode_string(value: bytes) bytes: + # return the content portion of the string, skipping the first 2 bytes which encode the length + return extract(2, 0, value) +end + +func abi_encode_string(value: bytes) bytes: + # return the bytestring with a uint16 prefix denoting the length + return concat(extract(6, 2, itob(len(value))), value) +end + +func abi_encode_uint64(value: int) bytes: + return itob(value) +end + +func abi_encode_uint32(value: int) bytes: + # return the last 4 bytes + return extract(4, 4, itob(value)) +end + +func abi_encode_uint16(value: int) bytes: + # return the last 2 bytes + return extract(6, 2, itob(value)) +end + +func abi_encode_uint8(value: int) bytes: + # return the last 1 byte + return extract(7, 1, itob(value)) +end + +func abi_encode_uint128(value: bytes) bytes: + # return 16 bytes with zero padding + return bzero(16) b| value +end \ No newline at end of file diff --git a/examples/arc4/build/abi.teal b/examples/arc4/build/abi.teal new file mode 100644 index 0000000..007f252 --- /dev/null +++ b/examples/arc4/build/abi.teal @@ -0,0 +1,253 @@ +#pragma version 8 + +// if Txn.ApplicationID == 0: + txn ApplicationID + pushint 0 + == + bz l0_end + // then: + // Handle Create App + // exit(1) + pushint 1 + return + l0_end: // end + +// switch Txn.OnCompletion: +txn OnCompletion +pushint 0 // NoOp +== +bnz main +txn OnCompletion +pushint 1 // OptIn +== +bnz opt_in +txn OnCompletion +pushint 2 // CloseOut +== +bnz close_out +txn OnCompletion +pushint 4 // UpdateApplication +== +bnz update_app +txn OnCompletion +pushint 5 // DeleteApplication +== +bnz delete_app +err // unexpected value + +// block opt_in +opt_in: + // Disallow Opt In + // exit(0) + pushint 0 + return + +// block close_out +close_out: + // Disallow Closing Out + // exit(0) + pushint 0 + return + +// block update_app +update_app: + // Handle Update App + // Only allow the Creator to update the app + // assert(Txn.Sender == Global.CreatorAddress) + txn Sender + global CreatorAddress + == + assert + // exit(1) + pushint 1 + return + +// block delete_app +delete_app: + // Handle Delete App + // Only allow the Creator to delete the app + // assert(Txn.Sender == Global.CreatorAddress) + txn Sender + global CreatorAddress + == + assert + // exit(1) + pushint 1 + return + +// block main +main: + // switch Txn.ApplicationArgs[0]: + txna ApplicationArgs 0 + method "setup()void" + == + bnz main__setup + txna ApplicationArgs 0 + method "add(uint64,uint64)uint64" + == + bnz main__add + txna ApplicationArgs 0 + method "mulw(uint64,uint64)uint128" + == + bnz main__mulw + txna ApplicationArgs 0 + method "hello(string)string" + == + bnz main__mulw + err // unexpected value + + // block setup + main__setup: + // Do some setup stuff here + // exit(1) + pushint 1 + return + + // block add + main__add: + // Add 2 integers + // int x = btoi(Txn.ApplicationArgs[1]) [slot 0] + txna ApplicationArgs 1 + btoi + store 0 // x + // int y = btoi(Txn.ApplicationArgs[2]) [slot 1] + txna ApplicationArgs 2 + btoi + store 1 // y + // int result = x + y [slot 2] + load 0 // x + load 1 // y + + + store 2 // result + // abi_return(abi_encode_uint64(result)) + load 2 // result + callsub __func__abi_encode_uint64 + callsub __func__abi_return + // exit(1) + pushint 1 + return + + // block mulw + main__mulw: + // Multiply 2 integers, returing a uint128 + // bytes x = Txn.ApplicationArgs[1] [slot 0] + txna ApplicationArgs 1 + store 0 // x + // bytes y = Txn.ApplicationArgs[2] [slot 1] + txna ApplicationArgs 2 + store 1 // y + // bytes result = x b* y [slot 2] + load 0 // x + load 1 // y + b* + store 2 // result + // abi_return(abi_encode_uint128(result)) + load 2 // result + callsub __func__abi_encode_uint128 + callsub __func__abi_return + // exit(1) + pushint 1 + return + + + // block hello + main__hello: + // Return a greeting + // bytes name = abi_decode_string(Txn.ApplicationArgs[1]) [slot 0] + txna ApplicationArgs 1 + callsub __func__abi_decode_string + store 0 // name + // bytes result = concat("Hello ", name) [slot 1] + pushbytes "Hello " + load 0 // name + concat + store 1 // result + // abi_return(abi_encode_string(result)) + load 1 // result + callsub __func__abi_encode_string + callsub __func__abi_return + // exit(1) + pushint 1 + return + +// func abi_return(result: bytes): +__func__abi_return: +store 3 // result +// log(concat("\x15\x1f\x7c\x75", result)) +pushbytes "\x15\x1f\x7c\x75" +load 3 // result +concat +log +// return +retsub + +// func abi_decode_string(value: bytes) bytes: +__func__abi_decode_string: +store 4 // value +// return the content portion of the string, skipping the first 2 bytes which encode the length +// return extract(2, 0, value) +load 4 // value +extract 2 0 +retsub + +// func abi_encode_string(value: bytes) bytes: +__func__abi_encode_string: +store 5 // value +// return the bytestring with a uint16 prefix denoting the length +// return concat(extract(6, 2, itob(len(value))), value) +load 5 // value +len +itob +extract 6 2 +load 5 // value +concat +retsub + +// func abi_encode_uint64(value: int) bytes: +__func__abi_encode_uint64: +store 6 // value +// return itob(value) +load 6 // value +itob +retsub + +// func abi_encode_uint32(value: int) bytes: +__func__abi_encode_uint32: +store 7 // value +// return the last 4 bytes +// return extract(4, 4, itob(value)) +load 7 // value +itob +extract 4 4 +retsub + +// func abi_encode_uint16(value: int) bytes: +__func__abi_encode_uint16: +store 8 // value +// return the last 2 bytes +// return extract(6, 2, itob(value)) +load 8 // value +itob +extract 6 2 +retsub + +// func abi_encode_uint8(value: int) bytes: +__func__abi_encode_uint8: +store 9 // value +// return the last 1 byte +// return extract(7, 1, itob(value)) +load 9 // value +itob +extract 7 1 +retsub + +// func abi_encode_uint128(value: bytes) bytes: +__func__abi_encode_uint128: +store 10 // value +// return 16 bytes with zero padding +// return bzero(16) b| value +pushint 16 +bzero +load 10 // value +b| +retsub diff --git a/examples/arc4/contract.json b/examples/arc4/contract.json new file mode 100644 index 0000000..70df96d --- /dev/null +++ b/examples/arc4/contract.json @@ -0,0 +1,102 @@ +{ + "name": "app", + "methods": [ + { + "name": "setup", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "add", + "args": [ + { + "type": "uint64" + }, + { + "type": "uint64" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "mulw", + "args": [ + { + "type": "uint64" + }, + { + "type": "uint64" + } + ], + "returns": { + "type": "uint128" + } + }, + { + "name": "hello", + "args": [ + { + "type": "string" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "send", + "args": [ + { + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "store_data", + "args": [ + { + "type": "byte[10]" + }, + { + "type": "byte[100]" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "store_tuple", + "args": [ + { + "type": "byte[10]" + }, + { + "type": "(uint64,address,byte[100])" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "balance", + "args": [ + { + "type": "account" + } + ], + "returns": { + "type": "uint64" + } + } + ], + "networks": {} +} diff --git a/examples/arc4/tests.py b/examples/arc4/tests.py new file mode 100644 index 0000000..8c7484b --- /dev/null +++ b/examples/arc4/tests.py @@ -0,0 +1,95 @@ +import os +import unittest + +from algojig import TealishProgram +from algojig.ledger import JigLedger +from algojig.algod import JigAlgod +from algosdk.account import generate_account +from algosdk.logic import get_application_address +from algosdk.atomic_transaction_composer import ( + AtomicTransactionComposer, + AccountTransactionSigner, +) + +from utils import extract_methods + +dirname = os.path.dirname(__file__) +approval_program = TealishProgram(os.path.join(dirname, "app.tl")) + + +class TestApp(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.app_creator_sk, cls.app_creator_address = generate_account() + cls.user_sk, cls.user_address = generate_account() + + def setUp(self): + self.app_id = 1 + self.app_address = get_application_address(self.app_id) + self.ledger = JigLedger() + self.ledger.set_account_balance(self.user_address, 1_000_000) + self.ledger.set_account_balance(self.app_address, 10_000_000) + self.ledger.create_app(app_id=self.app_id, approval_program=approval_program) + + def call_method(self, method_name, args, fee=None, **extra): + methods = { + m.name: m for m in extract_methods(approval_program.tealish_source_lines) + } + algod = JigAlgod(self.ledger) + atc = AtomicTransactionComposer() + signer = AccountTransactionSigner(self.user_sk) + sp = algod.get_suggested_params() + if fee: + sp.fee = fee + atc.add_method_call( + method=methods[method_name], + sender=self.user_address, + signer=signer, + sp=sp, + method_args=args, + app_id=self.app_id, + **extra, + ) + atc.gather_signatures() + result = atc.execute(algod, 1) + return result.abi_results[0].return_value + + def test_add(self): + result = self.call_method("add", [500, 12]) + self.assertEqual(result, 512) + + def test_mulw(self): + result = self.call_method("mulw", [(2**64 - 1), 1000]) + self.assertEqual(result, (2**64 - 1) * 1000) + + def test_hello(self): + result = self.call_method("hello", ["world"]) + self.assertEqual(result, "Hello world") + + def test_send(self): + result = self.call_method("send", [self.user_address], fee=2000) + self.assertEqual(result, None) + + def test_store_data(self): + key = b"my_key".zfill(10) + data = b"some_data".ljust(100, b" ") + result = self.call_method( + method_name="store_data", + args=[key, data], + boxes=[(0, key)], + ) + self.assertEqual(result, None) + + def test_store_tuple(self): + key = b"my_key".zfill(10) + data = (42, self.user_address, b"some_data".ljust(100, b" ")) + result = self.call_method( + method_name="store_tuple", + args=[key, data], + boxes=[(0, key)], + ) + self.assertEqual(result, None) + + def test_balance(self): + result = self.call_method("balance", [self.app_address]) + self.assertEqual(result, 10_000_000) diff --git a/examples/arc4/utils.py b/examples/arc4/utils.py new file mode 100644 index 0000000..d4bfdc1 --- /dev/null +++ b/examples/arc4/utils.py @@ -0,0 +1,32 @@ +import sys +import json +from algosdk import abi +from algosdk.error import ABITypeError +from tealish import TealishCompiler +from tealish.expression_nodes import FunctionCall + + +def extract_methods(tealish_lines): + compiler = TealishCompiler(tealish_lines) + program = compiler.parse() + results = program.find_child_nodes(FunctionCall, lambda n: n.name == "method") + methods = [] + for node in results: + method_sig = node.args[0].value + try: + method = abi.Method.from_signature(method_sig) + except ABITypeError: + if " " in method_sig: + raise Exception( + f"ABI method signatures must not contain spaces: {method_sig}" + ) from None + raise + methods.append(method) + return methods + + +if __name__ == "__main__": + lines = open(sys.argv[1]).readlines() + methods = extract_methods(lines) + contract = abi.Contract(name="app", methods=methods) + print(json.dumps(contract.dictify(), indent=2)) diff --git a/tealish/__init__.py b/tealish/__init__.py index 77bd8ee..5c6ec7d 100644 --- a/tealish/__init__.py +++ b/tealish/__init__.py @@ -79,6 +79,7 @@ def write(self, lines: Union[str, List[str]] = "", line_no: int = 0) -> None: def parse(self) -> None: node = Program.consume(self, None) self.nodes.append(node) + return node def process(self) -> None: for node in self.nodes: diff --git a/tealish/base.py b/tealish/base.py index 1e11f28..e8c6356 100644 --- a/tealish/base.py +++ b/tealish/base.py @@ -132,6 +132,14 @@ def has_child_node(self, node_class: type) -> bool: return True return False + def find_child_nodes(self, node_class: type, filter=None) -> List["Node"]: + results = [] + for node in getattr(self, "nodes", []): # type: ignore + if isinstance(node, node_class) and (filter and filter(node)): + results.append(node) + results += node.find_child_nodes(node_class, filter) + return results + def get_current_scope(self) -> Scope: # TODO: Only available on Node and other subclasses return self.parent.get_current_scope() # type: ignore