Skip to content

Commit 8752ad5

Browse files
authored
Refactoring Router taking CSP expression (#636)
1 parent 0ff8227 commit 8752ad5

File tree

7 files changed

+112
-330
lines changed

7 files changed

+112
-330
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## Added
44

55
## Changed
6+
* Improved Router clear state program generation. ([#636](https://github.com/algorand/pyteal/pull/636))
7+
* NOTE: a backwards incompatable change was imposed in this PR: previous Clear State Program (CSP) can be constructed in router by registering ABI methods or bare app calls, now one has to use `clear_state` argument in `Router.__init__` to construct the CSP.
68

79
## Fixed
810

docs/abi.rst

+7-2
Original file line numberDiff line numberDiff line change
@@ -732,13 +732,16 @@ The AVM supports 6 types of OnCompletion options that may be specified on an app
732732
#. **Update application**, which updates an app, represented by :any:`OnComplete.UpdateApplication`
733733
#. **Delete application**, which deletes an app, represented by :any:`OnComplete.DeleteApplication`
734734

735-
In PyTeal, you have the ability to register a bare app call handler for each of these actions. Additionally, a bare app call handler must also specify whether the handler can be invoking during an **app creation transaction** (:any:`CallConfig.CREATE`), during a **non-creation app call** (:any:`CallConfig.CALL`), or during **either** (:any:`CallConfig.ALL`).
735+
.. note::
736+
While **clear state** is a valid OnCompletion action, its behavior differs significantly from the others. For this reason, the :any:`Router` does not support bare app calls or methods to be called during clear state. Instead, you may use the :code:`clear_state` argument in :any:`Router.__init__` to do work during clear state.
737+
738+
In PyTeal, you have the ability to register a bare app call handler for each of these actions, except for clear state. Additionally, a bare app call handler must also specify whether the handler can be invoking during an **app creation transaction** (:any:`CallConfig.CREATE`), during a **non-creation app call** (:any:`CallConfig.CALL`), or during **either** (:any:`CallConfig.ALL`).
736739

737740
The :any:`BareCallActions` class is used to define a bare app call handler for on completion actions. Each bare app call handler must be an instance of the :any:`OnCompleteAction` class.
738741

739742
The :any:`OnCompleteAction` class is responsible for holding the actual code for the bare app call handler (an instance of either :code:`Expr` or a subroutine that takes no args and returns nothing) as well as a :any:`CallConfig` option that indicates whether the action is able to be called during a creation app call, a non-creation app call, or either.
740743

741-
All the bare app calls that an application wishes to support must be provided to the :any:`Router.__init__` method.
744+
All the bare app calls that an application wishes to support must be provided to the :any:`Router.__init__` method. Additionally, if you wish to perform actions during clear state, you can specify the :code:`clear_state` argument.
742745

743746
A brief example is below:
744747

@@ -785,6 +788,7 @@ A brief example is below:
785788
action=assert_sender_is_creator, call_config=CallConfig.CALL
786789
),
787790
),
791+
clear_state=Approve(),
788792
)
789793
790794
.. note::
@@ -820,6 +824,7 @@ The first way to register a method is with the :any:`Router.add_method_handler`
820824
opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
821825
close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
822826
),
827+
clear_state=Approve(),
823828
)
824829
825830
@ABIReturnSubroutine

examples/application/abi/algobank.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ def assert_sender_is_creator() -> Expr:
2525
close_out=OnCompleteAction(
2626
action=transfer_balance_to_lost, call_config=CallConfig.CALL
2727
),
28-
clear_state=OnCompleteAction(
29-
action=transfer_balance_to_lost, call_config=CallConfig.CALL
30-
),
3128
# only the creator can update or delete the app
3229
update_application=OnCompleteAction(
3330
action=assert_sender_is_creator, call_config=CallConfig.CALL
@@ -36,6 +33,7 @@ def assert_sender_is_creator() -> Expr:
3633
action=assert_sender_is_creator, call_config=CallConfig.CALL
3734
),
3835
),
36+
clear_state=transfer_balance_to_lost,
3937
)
4038

4139

examples/application/abi/algobank_clear_state.teal

-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
#pragma version 6
2-
txn NumAppArgs
3-
int 0
4-
==
5-
bnz main_l2
6-
err
7-
main_l2:
82
byte "lost"
93
byte "lost"
104
app_global_get

pyteal/ast/router.py

+60-67
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,6 @@ def approval_condition_under_config(self) -> Expr | int:
6363
case _:
6464
raise TealInternalError(f"unexpected CallConfig {self}")
6565

66-
def clear_state_condition_under_config(self) -> int:
67-
match self:
68-
case CallConfig.NEVER:
69-
return 0
70-
case CallConfig.CALL:
71-
return 1
72-
case CallConfig.CREATE | CallConfig.ALL:
73-
raise TealInputError(
74-
"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation"
75-
)
76-
case _:
77-
raise TealInputError(f"unexpected CallConfig {self}")
78-
7966

8067
CallConfig.__module__ = "pyteal"
8168

@@ -96,6 +83,15 @@ class MethodConfig:
9683
update_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
9784
delete_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
9885

86+
def __post_init__(self):
87+
if self.clear_state != CallConfig.NEVER:
88+
raise TealInputError(
89+
"Attempt to construct clear state program from MethodConfig: "
90+
"Use Router top level argument `clear_state` instead. "
91+
"For more details please refer to "
92+
"https://pyteal.readthedocs.io/en/latest/abi.html#registering-bare-app-calls"
93+
)
94+
9995
def is_never(self) -> bool:
10096
return all(map(lambda cc: cc == CallConfig.NEVER, astuple(self)))
10197

@@ -128,8 +124,11 @@ def approval_cond(self) -> Expr | int:
128124
)
129125
return Or(*cond_list)
130126

131-
def clear_state_cond(self) -> Expr | int:
132-
return self.clear_state.clear_state_condition_under_config()
127+
128+
MethodConfig.__module__ = "pyteal"
129+
130+
131+
ActionType = Expr | SubroutineFnWrapper | ABIReturnSubroutine
133132

134133

135134
@dataclass(frozen=True)
@@ -138,9 +137,7 @@ class OnCompleteAction:
138137
OnComplete Action, registers bare calls to one single OnCompletion case.
139138
"""
140139

141-
action: Optional[Expr | SubroutineFnWrapper | ABIReturnSubroutine] = field(
142-
kw_only=True, default=None
143-
)
140+
action: Optional[ActionType] = field(kw_only=True, default=None)
144141
call_config: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
145142

146143
def __post_init__(self):
@@ -154,21 +151,15 @@ def never() -> "OnCompleteAction":
154151
return OnCompleteAction()
155152

156153
@staticmethod
157-
def create_only(
158-
f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
159-
) -> "OnCompleteAction":
154+
def create_only(f: ActionType) -> "OnCompleteAction":
160155
return OnCompleteAction(action=f, call_config=CallConfig.CREATE)
161156

162157
@staticmethod
163-
def call_only(
164-
f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
165-
) -> "OnCompleteAction":
158+
def call_only(f: ActionType) -> "OnCompleteAction":
166159
return OnCompleteAction(action=f, call_config=CallConfig.CALL)
167160

168161
@staticmethod
169-
def always(
170-
f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
171-
) -> "OnCompleteAction":
162+
def always(f: ActionType) -> "OnCompleteAction":
172163
return OnCompleteAction(action=f, call_config=CallConfig.ALL)
173164

174165
def is_empty(self) -> bool:
@@ -197,6 +188,15 @@ class BareCallActions:
197188
kw_only=True, default=OnCompleteAction.never()
198189
)
199190

191+
def __post_init__(self):
192+
if not self.clear_state.is_empty():
193+
raise TealInputError(
194+
"Attempt to construct clear state program from bare app call: "
195+
"Use Router top level argument `clear_state` instead. "
196+
"For more details please refer to "
197+
"https://pyteal.readthedocs.io/en/latest/abi.html#registering-bare-app-calls"
198+
)
199+
200200
def is_empty(self) -> bool:
201201
for action_field in fields(self):
202202
action: OnCompleteAction = getattr(self, action_field.name)
@@ -220,7 +220,7 @@ def approval_construction(self) -> Optional[Expr]:
220220
continue
221221
wrapped_handler = ASTBuilder.wrap_handler(
222222
False,
223-
cast(Expr | SubroutineFnWrapper | ABIReturnSubroutine, oca.action),
223+
cast(ActionType, oca.action),
224224
)
225225
match oca.call_config:
226226
case CallConfig.ALL:
@@ -246,21 +246,6 @@ def approval_construction(self) -> Optional[Expr]:
246246
)
247247
return Cond(*[[n.condition, n.branch] for n in conditions_n_branches])
248248

249-
def clear_state_construction(self) -> Optional[Expr]:
250-
if self.clear_state.is_empty():
251-
return None
252-
253-
# call this to make sure we error if the CallConfig is CREATE or ALL
254-
self.clear_state.call_config.clear_state_condition_under_config()
255-
256-
return ASTBuilder.wrap_handler(
257-
False,
258-
cast(
259-
Expr | SubroutineFnWrapper | ABIReturnSubroutine,
260-
self.clear_state.action,
261-
),
262-
)
263-
264249

265250
BareCallActions.__module__ = "pyteal"
266251

@@ -280,7 +265,7 @@ class ASTBuilder:
280265

281266
@staticmethod
282267
def wrap_handler(
283-
is_method_call: bool, handler: ABIReturnSubroutine | SubroutineFnWrapper | Expr
268+
is_method_call: bool, handler: ActionType, wrap_to_name: str | None = None
284269
) -> Expr:
285270
"""This is a helper function that handles transaction arguments passing in bare-app-call/abi-method handlers.
286271
@@ -302,11 +287,13 @@ def wrap_handler(
302287
passed in ABIReturnSubroutine and logged, then approve.
303288
"""
304289
if not is_method_call:
290+
wrap_to_name = "bare appcall" if wrap_to_name is None else wrap_to_name
291+
305292
match handler:
306293
case Expr():
307294
if handler.type_of() != TealType.none:
308295
raise TealInputError(
309-
f"bare appcall handler should be TealType.none not {handler.type_of()}."
296+
f"{wrap_to_name} handler should be TealType.none not {handler.type_of()}."
310297
)
311298
return handler if handler.has_return() else Seq(handler, Approve())
312299
case SubroutineFnWrapper():
@@ -316,7 +303,7 @@ def wrap_handler(
316303
)
317304
if handler.subroutine.argument_count() != 0:
318305
raise TealInputError(
319-
f"subroutine call should take 0 arg for bare-app call. "
306+
f"subroutine call should take 0 arg for {wrap_to_name}. "
320307
f"this subroutine takes {handler.subroutine.argument_count()}."
321308
)
322309
return Seq(handler(), Approve())
@@ -327,22 +314,23 @@ def wrap_handler(
327314
)
328315
if handler.subroutine.argument_count() != 0:
329316
raise TealInputError(
330-
f"abi-returning subroutine call should take 0 arg for bare-app call. "
317+
f"abi-returning subroutine call should take 0 arg for {wrap_to_name}. "
331318
f"this abi-returning subroutine takes {handler.subroutine.argument_count()}."
332319
)
333320
return Seq(cast(Expr, handler()), Approve())
334321
case _:
335322
raise TealInputError(
336-
"bare appcall can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg"
323+
f"{wrap_to_name} can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg"
337324
)
338325
else:
326+
wrap_to_name = "method call" if wrap_to_name is None else wrap_to_name
339327
if not isinstance(handler, ABIReturnSubroutine):
340328
raise TealInputError(
341-
f"method call should be only registering ABIReturnSubroutine, got {type(handler)}."
329+
f"{wrap_to_name} should be only registering ABIReturnSubroutine, got {type(handler)}."
342330
)
343331
if not handler.is_abi_routable():
344332
raise TealInputError(
345-
f"method call ABIReturnSubroutine is not routable "
333+
f"{wrap_to_name} ABIReturnSubroutine is not routable "
346334
f"got {handler.subroutine.argument_count()} args with {len(handler.subroutine.abi_args)} ABI args."
347335
)
348336

@@ -508,19 +496,28 @@ def __init__(
508496
name: str,
509497
bare_calls: BareCallActions | None = None,
510498
descr: str | None = None,
499+
*,
500+
clear_state: Optional[ActionType] = None,
511501
) -> None:
512502
"""
513503
Args:
514504
name: the name of the smart contract, used in the JSON object.
515505
bare_calls: the bare app call registered for each on_completion.
516506
descr: a description of the smart contract, used in the JSON object.
507+
clear_state: an expression describing the behavior of clear state program. This
508+
expression will be the entirety of the clear state program; no additional code is
509+
inserted by the Router. If not provided, the clear state program will always reject.
517510
"""
518511

519512
self.name: str = name
520513
self.descr = descr
521514

522515
self.approval_ast = ASTBuilder()
523-
self.clear_state_ast = ASTBuilder()
516+
self.clear_state: Expr = (
517+
Reject()
518+
if clear_state is None
519+
else ASTBuilder.wrap_handler(False, clear_state, "clear state call")
520+
)
524521

525522
self.methods: list[sdk_abi.Method] = []
526523
self.method_sig_to_selector: dict[str, bytes] = dict()
@@ -535,14 +532,6 @@ def __init__(
535532
cast(Expr, bare_call_approval),
536533
)
537534
)
538-
bare_call_clear = bare_calls.clear_state_construction()
539-
if bare_call_clear:
540-
self.clear_state_ast.conditions_n_branches.append(
541-
CondNode(
542-
Txn.application_args.length() == Int(0),
543-
cast(Expr, bare_call_clear),
544-
)
545-
)
546535

547536
def add_method_handler(
548537
self,
@@ -594,13 +583,9 @@ def add_method_handler(
594583
self.method_selector_to_sig[method_selector] = method_signature
595584

596585
method_approval_cond = method_config.approval_cond()
597-
method_clear_state_cond = method_config.clear_state_cond()
598586
self.approval_ast.add_method_to_ast(
599587
method_signature, method_approval_cond, method_call
600588
)
601-
self.clear_state_ast.add_method_to_ast(
602-
method_signature, method_clear_state_cond, method_call
603-
)
604589
return method_call
605590

606591
def method(
@@ -637,21 +622,31 @@ def method(
637622
opt_in (optional): The allowed calls during :code:`OnComplete.OptIn`.
638623
close_out (optional): The allowed calls during :code:`OnComplete.CloseOut`.
639624
clear_state (optional): The allowed calls during :code:`OnComplete.ClearState`.
625+
This argument has been deprecated, and will error on compile time if one wants to access it.
626+
Use Router top level argument `clear_state` instead.
640627
update_application (optional): The allowed calls during :code:`OnComplete.UpdateApplication`.
641628
delete_application (optional): The allowed calls during :code:`OnComplete.DeleteApplication`.
642629
"""
643630
# we use `is None` extensively for CallConfig to distinguish 2 following cases
644631
# - None
645632
# - CallConfig.Never
646633
# both cases evaluate to False in if statement.
634+
635+
if clear_state is not None:
636+
raise TealInputError(
637+
"Attempt to register ABI method for clear state program: "
638+
"Use Router top level argument `clear_state` instead. "
639+
"For more details please refer to "
640+
"https://pyteal.readthedocs.io/en/latest/abi.html#registering-bare-app-calls"
641+
)
642+
647643
def wrap(_func) -> ABIReturnSubroutine:
648644
wrapped_subroutine = ABIReturnSubroutine(_func, overriding_name=name)
649645
call_configs: MethodConfig
650646
if (
651647
no_op is None
652648
and opt_in is None
653649
and close_out is None
654-
and clear_state is None
655650
and update_application is None
656651
and delete_application is None
657652
):
@@ -664,15 +659,13 @@ def none_to_never(x: None | CallConfig):
664659
_no_op = none_to_never(no_op)
665660
_opt_in = none_to_never(opt_in)
666661
_close_out = none_to_never(close_out)
667-
_clear_state = none_to_never(clear_state)
668662
_update_app = none_to_never(update_application)
669663
_delete_app = none_to_never(delete_application)
670664

671665
call_configs = MethodConfig(
672666
no_op=_no_op,
673667
opt_in=_opt_in,
674668
close_out=_close_out,
675-
clear_state=_clear_state,
676669
update_application=_update_app,
677670
delete_application=_delete_app,
678671
)
@@ -714,7 +707,7 @@ def build_program(self) -> tuple[Expr, Expr, sdk_abi.Contract]:
714707
"""
715708
return (
716709
self.approval_ast.program_construction(),
717-
self.clear_state_ast.program_construction(),
710+
self.clear_state,
718711
self.contract_construct(),
719712
)
720713

0 commit comments

Comments
 (0)