Esempio n. 1
0
 def _construct_itxn(self) -> Expr:
     if self.mode == OpUpMode.Explicit:
         return Seq(
             InnerTxnBuilder.Begin(),
             InnerTxnBuilder.SetFields({
                 TxnField.type_enum:
                 TxnType.ApplicationCall,
                 TxnField.application_id:
                 self.target_app_id,
             }),
             InnerTxnBuilder.Submit(),
         )
     else:
         return Seq(
             InnerTxnBuilder.Begin(),
             InnerTxnBuilder.SetFields({
                 TxnField.type_enum:
                 TxnType.ApplicationCall,
                 TxnField.on_completion:
                 OnComplete.DeleteApplication,
                 TxnField.approval_program:
                 ON_CALL_APP,
                 TxnField.clear_state_program:
                 ON_CALL_APP,
             }),
             InnerTxnBuilder.Submit(),
         )
Esempio n. 2
0
    def __teal__(self, options: "CompileOptions"):
        if len(self.cond) > 1:
            asserts: list[Expr] = []
            for cond in self.cond:
                asrt = Assert(cond, comment=self.comment)
                asrt.trace = cond.trace
                asserts.append(asrt)
            return Seq(*asserts).__teal__(options)

        if options.version >= Op.assert_.min_version:
            # use assert op if available
            conds: list[Expr] = [self.cond[0]]
            if self.comment is not None:
                conds.append(Comment(self.comment))
            return TealBlock.FromOp(options, TealOp(self, Op.assert_), *conds)

        # if assert op is not available, use branches and err
        condStart, condEnd = self.cond[0].__teal__(options)

        end = TealSimpleBlock([])
        errBlock = TealSimpleBlock([TealOp(self, Op.err)])

        branchBlock = TealConditionalBlock([])
        branchBlock.setTrueBlock(end)
        branchBlock.setFalseBlock(errBlock)

        condEnd.setNextBlock(branchBlock)

        return condStart, end
Esempio n. 3
0
def _store_encoded_expr_byte_string_into_var(value: Expr, location: ScratchVar) -> Expr:
    return Seq(
        location.store(value),
        location.store(
            Concat(Suffix(Itob(Len(location.load())), Int(6)), location.load())
        ),
    )
Esempio n. 4
0
    def ExecuteMethodCall(
        cls,
        *,
        app_id: Expr,
        method_signature: str,
        args: list[abi.BaseType | Expr | dict[TxnField, Expr | list[Expr]]],
        extra_fields: dict[TxnField, Expr | list[Expr]] = None,
    ) -> Expr:
        """Performs a single app call transaction formatted as an ABI method call.

        A convenience method that accepts fields to submit a single inner transaction, which is equivalent to:

        .. code-block:: python

            InnerTxnBuilder.Begin()
            InnerTxnBuilder.MethodCall(
                app_id=app_id,
                method_signature=method_signature,
                args=args,
                extra_fields=extra_fields,
            ),
            InnerTxnBuilder.Submit()

        Requires program version 5 or higher. This operation is only permitted in application mode.

        Args:
            app_id: An expression that evaluates to a `TealType.uint64` corresponding to the application being called.
            method_signature: A string representing the method signature of the method we're calling. This is used to do
                type checking on the arguments passed and to create the method selector passed as the first argument.
            args: A list of arguments to pass to the application. The values in this list depend on the kind of argument you wish to pass:

                - For basic ABI arguments (not Reference or Transaction types):
                    If an ABI type is passed it **MUST** match the type specified in the `method_signature`. If an Expr is passed it must evaluate to `TealType.bytes` but beyond that no type checking is performed.

                - For Reference arguments:
                    Either the Reference type or an Expr that returns the type corresponding to the reference type are allowed.
                    (i.e. Asset is TealType.uint64, Application is TealType.uint64, Account is TealType.bytes)

                - For Transaction arguments:
                    A dictionary containing TxnField to Expr that describe Transactions to be pre-pended to the transaction group being constructed.  The `TxnField.type_enum` key MUST be set and MUST match the expected transaction type specified in the `method_signature`.

            extra_fields (optional): A dictionary whose keys are fields to set and whose values are the value each
                field should take. Each value must evaluate to a type that is compatible with the
                field being set. These fields are set on the ApplicationCallTransaction being constructed
        """
        return Seq(
            cls.Begin(),
            cls.MethodCall(
                app_id=app_id,
                method_signature=method_signature,
                args=args,
                extra_fields=extra_fields,
            ),
            cls.Submit(),
        )
Esempio n. 5
0
    def SetField(cls, field: TxnField, value: Expr | list[Expr]) -> Expr:
        """Set a field of the current inner transaction.

        :any:`InnerTxnBuilder.Begin` must be called before setting any fields on an inner
        transaction.

        Note: For non-array field (e.g., note), setting it twice will overwrite the original value.
              While for array field (e.g., accounts), setting it multiple times will append the values.
              It is also possible to pass the entire array field if desired (e.g., Txn.accounts) to pass all the references.

        Requires program version 5 or higher. This operation is only permitted in application mode.

        Args:
            field: The field to set on the inner transaction.
            value: The value to that the field should take. This must evaluate to a type that is
                compatible with the field being set.
        """
        if not field.is_array:
            if type(value) is list or isinstance(value, TxnArray):
                raise TealInputError(
                    "inner transaction set field {} does not support array value".format(
                        field
                    )
                )
            return InnerTxnFieldExpr(field, cast(Expr, value))
        else:
            if type(value) is not list and not isinstance(value, TxnArray):
                raise TealInputError(
                    "inner transaction set array field {} with non-array value".format(
                        field
                    )
                )

            if type(value) is list:
                for valueIter in value:
                    if not isinstance(valueIter, Expr):
                        raise TealInputError(
                            "inner transaction set array field {} with non PyTeal expression array element {}".format(
                                field, valueIter
                            )
                        )

                return Seq(
                    *[
                        InnerTxnFieldExpr(field, cast(Expr, valueIter))
                        for valueIter in value
                    ]
                )
            else:
                arr = cast(TxnArray, value)
                return For(
                    (i := ScratchVar()).store(Int(0)),
                    i.load() < arr.length(),
                    i.store(i.load() + Int(1)),
                ).Do(InnerTxnFieldExpr(field, arr[i.load()]))
Esempio n. 6
0
    def length(self) -> Expr:
        """Get the element number of this ABI dynamic array.

        The array length (element number) is encoded in the first 2 bytes of the byte encoding.

        Returns:
            A PyTeal expression that represents the dynamic array length.
        """
        output = Uint16()
        return Seq(
            output.decode(self.encode()),
            output.get(),
        )
Esempio n. 7
0
    def __init__(self, base: str, nonce: str, child: Expr) -> None:
        """Create a new Nonce.

        The Nonce expression behaves exactly like the child expression passed into it, except it
        uses the provided nonce string to alter its structure in a way that does not affect
        execution.

        Args:
            base: The base of the nonce. Must be one of utf8, base16, base32, or base64.
            nonce: An arbitrary nonce string that conforms to base.
            child: The expression to wrap.
        """
        super().__init__()

        if base not in ("utf8", "base16", "base32", "base64"):
            raise TealInputError("Invalid base: {}".format(base))

        self.child = child
        if base == "utf8":
            self.nonce_bytes = Bytes(nonce)
        else:
            self.nonce_bytes = Bytes(base, nonce)

        self.seq = Seq([Pop(self.nonce_bytes), self.child])
Esempio n. 8
0
    def use(self, action: Callable[[T_co], Expr]) -> Expr:
        """Compute the value and pass it to a callable expression.

        NOTE: If you call this method multiple times, the computation to determine the value will be
        repeated each time. For this reason, it is recommended to only issue a single call to either
        :code:`store_into` or :code:`use`.

        Args:
            action: A callable object that will receive an instance of this class's produced type
                with the computed value. The callable object may use that value as it sees fit, but
                it must return an Expr to be included in the program's AST.

        Returns:
            An expression which contains the returned expression from invoking `action` with the
            computed value.
        """
        newInstance = cast(T_co, self.produced_type_spec().new_instance())
        return Seq(self.store_into(newInstance), action(newInstance))
Esempio n. 9
0
    def SetFields(cls, fields: dict[TxnField, Expr | list[Expr]]) -> Expr:
        """Set multiple fields of the current inner transaction.

        :any:`InnerTxnBuilder.Begin` must be called before setting any fields on an inner
        transaction.

        Note: For non-array field (e.g., note), setting it twice will overwrite the original value.
              While for array field (e.g., accounts), setting it multiple times will append the values.
              It is also possible to pass the entire array field if desired (e.g., Txn.accounts) to pass all the references.

        Requires program version 5 or higher. This operation is only permitted in application mode.

        Args:
            fields: A dictionary whose keys are fields to set and whose values are the value each
                field should take. Each value must evaluate to a type that is compatible with the
                field being set.
        """
        fieldsToSet = [cls.SetField(field, value) for field, value in fields.items()]
        return Seq(fieldsToSet)
Esempio n. 10
0
    def Execute(cls, fields: dict[TxnField, Expr | list[Expr]]) -> Expr:
        """Performs a single transaction given fields passed in.

        A convenience method that accepts fields to submit a single inner transaction, which is equivalent to:

        .. code-block:: python

            InnerTxnBuilder.Begin()
            InnerTxnBuilder.SetFields(fields)
            InnerTxnBuilder.Submit()

        Requires program version 5 or higher. This operation is only permitted in application mode.

        Args:
            fields: A dictionary whose keys are fields to set and whose values are the value each
                field should take. Each value must evaluate to a type that is compatible with the
                field being set.
        """
        return Seq(cls.Begin(), cls.SetFields(fields), cls.Submit())
Esempio n. 11
0
    def set(
        self,
        values: Union[
            bytes,
            bytearray,
            Expr,
            Sequence[Byte],
            StaticArray[Byte, N],
            ComputedValue[StaticArray[Byte, N]],
        ],
    ) -> Expr:
        """Set the elements of this StaticBytes to the input values.

        The behavior of this method depends on the input argument type:

            * :code:`bytes`: set the value to the Python byte string.
            * :code:`bytearray`: set the value to the Python byte array.
            * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.bytes.
            * :code:`Sequence[Byte]`: set the bytes of this String to those contained in this Python sequence (e.g. a list or tuple).
            * :code:`StaticArray[Byte, N]`: copy the bytes from another StaticArray. The argument's element type and length must exactly match Byte and this StaticBytes' length, otherwise an error will occur.
            * :code:`ComputedValue[StaticArray[Byte, N]]`: copy the bytes from a StaticArray produced by a ComputedType. The argument's element type and length must exactly match Byte and this StaticBytes' length, otherwise an error will occur.

        Args:
            values: The new elements this StaticBytes should have. This must follow the above constraints.

        Returns:
            An expression which stores the given value into this StaticBytes.
        """
        match values:
            case bytes() | bytearray():
                if len(values) != self.type_spec().length_static():
                    raise TealInputError(
                        f"Got bytes with length {len(values)}, expect {self.type_spec().length_static()}"
                    )
                return self.stored_value.store(Bytes(values))
            case Expr():
                return Seq(
                    self.stored_value.store(values),
                    Assert(self.length() == Len(self.stored_value.load())),
                )

        return super().set(values)
Esempio n. 12
0
 def add_method_to_ast(
     self, method_signature: str, cond: Expr | int, handler: ABIReturnSubroutine
 ) -> None:
     walk_in_cond = Txn.application_args[0] == MethodSignature(method_signature)
     match cond:
         case Expr():
             self.conditions_n_branches.append(
                 CondNode(
                     walk_in_cond,
                     Seq(Assert(cond), self.wrap_handler(True, handler)),
                 )
             )
         case 1:
             self.conditions_n_branches.append(
                 CondNode(walk_in_cond, self.wrap_handler(True, handler))
             )
         case 0:
             return
         case _:
             raise TealInputError("Invalid condition input for add_method_to_ast")
Esempio n. 13
0
 def approval_construction(self) -> Optional[Expr]:
     oc_action_pair: list[tuple[EnumInt, OnCompleteAction]] = [
         (OnComplete.NoOp, self.no_op),
         (OnComplete.OptIn, self.opt_in),
         (OnComplete.CloseOut, self.close_out),
         (OnComplete.UpdateApplication, self.update_application),
         (OnComplete.DeleteApplication, self.delete_application),
     ]
     if all(oca.is_empty() for _, oca in oc_action_pair):
         return None
     conditions_n_branches: list[CondNode] = list()
     for oc, oca in oc_action_pair:
         if oca.is_empty():
             continue
         wrapped_handler = ASTBuilder.wrap_handler(
             False,
             cast(Expr | SubroutineFnWrapper | ABIReturnSubroutine, oca.action),
         )
         match oca.call_config:
             case CallConfig.ALL:
                 cond_body = wrapped_handler
             case CallConfig.CALL | CallConfig.CREATE:
                 cond_body = Seq(
                     Assert(
                         cast(
                             Expr, oca.call_config.approval_condition_under_config()
                         )
                     ),
                     wrapped_handler,
                 )
             case _:
                 raise TealInternalError(
                     f"Unexpected CallConfig: {oca.call_config!r}"
                 )
         conditions_n_branches.append(
             CondNode(
                 Txn.on_completion() == oc,
                 cond_body,
             )
         )
     return Cond(*[[n.condition, n.branch] for n in conditions_n_branches])
Esempio n. 14
0
def Comment(comment: str, expr: Expr = None) -> Expr:
    """Wrap an existing expression with a comment.

    This comment will be present in the compiled TEAL source immediately before the first op of the
    expression.

    Note that when TEAL source is assembled into bytes, all comments are omitted.

    Args:
        comment: The comment that will be associated with the expression.
        expr: The expression to be commented.

    Returns:
        A new expression which is functionally equivalent to the input expression, but which will
        compile with the given comment string.
    """
    lines = comment.splitlines()
    comment_lines: list[Expr] = [CommentExpr(line) for line in lines]
    if expr is not None:
        comment_lines.append(expr)
    return Seq(*comment_lines)
Esempio n. 15
0
    def ensure_budget(self, required_budget: Expr) -> Expr:
        """Ensure that the budget will be at least the required_budget.

        Note: the available budget just prior to calling ensure_budget() must be
        high enough to execute the budget increase code. The exact budget required
        depends on the provided required_budget expression, but a budget of ~20
        should be sufficient for most use cases. If lack of budget is an issue then
        consider moving the call to ensure_budget() earlier in the pyteal program."""
        require_type(required_budget, TealType.uint64)

        # A budget buffer is necessary to deal with an edge case of ensure_budget():
        #   if the current budget is equal to or only slightly higher than the
        #   required budget then it's possible for ensure_budget() to return with a
        #   current budget less than the required budget. The buffer prevents this
        #   from being the case.
        buffer = Int(10)
        buffered_budget = ScratchVar(TealType.uint64)
        return Seq(
            buffered_budget.store(required_budget + buffer),
            While(buffered_budget.load() > Global.opcode_budget()).Do(
                self._construct_itxn()),
        )
Esempio n. 16
0
class Nonce(Expr):
    """A meta expression only used to change the hash of a TEAL program."""
    def __init__(self, base: str, nonce: str, child: Expr) -> None:
        """Create a new Nonce.

        The Nonce expression behaves exactly like the child expression passed into it, except it
        uses the provided nonce string to alter its structure in a way that does not affect
        execution.

        Args:
            base: The base of the nonce. Must be one of utf8, base16, base32, or base64.
            nonce: An arbitrary nonce string that conforms to base.
            child: The expression to wrap.
        """
        super().__init__()

        if base not in ("utf8", "base16", "base32", "base64"):
            raise TealInputError("Invalid base: {}".format(base))

        self.child = child
        if base == "utf8":
            self.nonce_bytes = Bytes(nonce)
        else:
            self.nonce_bytes = Bytes(base, nonce)

        self.seq = Seq([Pop(self.nonce_bytes), self.child])

    def __teal__(self, options: "CompileOptions"):
        return self.seq.__teal__(options)

    def __str__(self):
        return "(nonce: {}) {}".format(self.nonce_bytes, self.child)

    def type_of(self):
        return self.child.type_of()

    def has_return(self):
        return self.child.has_return()
Esempio n. 17
0
def uint_set(size: int, uint_var: ScratchVar, value: Union[int, Expr, "Uint"]) -> Expr:
    if size > 64:
        raise NotImplementedError(
            "Uint operations have not yet been implemented for bit sizes larger than 64"
        )

    checked = False
    if type(value) is int:
        if value >= 2**size:
            raise TealInputError("Value exceeds uint{} maximum: {}".format(size, value))
        value = Int(value)
        checked = True

    if isinstance(value, Uint):
        value = value.get()
        checked = True

    if checked or size == 64:
        return uint_var.store(cast(Expr, value))

    return Seq(
        uint_var.store(cast(Expr, value)),
        Assert(uint_var.load() < Int(2**size)),
    )
Esempio n. 18
0
def _encode_tuple(values: Sequence[BaseType]) -> Expr:
    heads: List[Expr] = []
    head_length_static: int = 0

    dynamicValueIndexToHeadIndex: Dict[int, int] = dict()
    ignoreNext = 0
    for i, elem in enumerate(values):
        if ignoreNext > 0:
            ignoreNext -= 1
            continue

        elemType = elem.type_spec()

        if elemType == BoolTypeSpec():
            numBools = _consecutive_bool_instance_num(values, i)
            ignoreNext = numBools - 1
            head_length_static += _bool_sequence_length(numBools)
            heads.append(
                _encode_bool_sequence(cast(Sequence[Bool], values[i : i + numBools]))
            )
            continue

        if elemType.is_dynamic():
            head_length_static += 2
            dynamicValueIndexToHeadIndex[i] = len(heads)
            heads.append(Seq())  # a placeholder
            continue

        head_length_static += elemType.byte_length_static()
        heads.append(elem.encode())

    tail_offset = Uint16()
    tail_offset_accumulator = Uint16()
    tail_holder = ScratchVar(TealType.bytes)
    encoded_tail = ScratchVar(TealType.bytes)

    firstDynamicTail = True
    for i, elem in enumerate(values):
        if elem.type_spec().is_dynamic():
            if firstDynamicTail:
                firstDynamicTail = False
                updateVars = Seq(
                    tail_holder.store(encoded_tail.load()),
                    tail_offset.set(head_length_static),
                )
            else:
                updateVars = Seq(
                    tail_holder.store(Concat(tail_holder.load(), encoded_tail.load())),
                    tail_offset.set(tail_offset_accumulator),
                )

            notLastDynamicValue = any(
                [nextValue.type_spec().is_dynamic() for nextValue in values[i + 1 :]]
            )
            if notLastDynamicValue:
                updateAccumulator = tail_offset_accumulator.set(
                    tail_offset.get() + Len(encoded_tail.load())
                )
            else:
                updateAccumulator = Seq()

            heads[dynamicValueIndexToHeadIndex[i]] = Seq(
                encoded_tail.store(elem.encode()),
                updateVars,
                updateAccumulator,
                tail_offset.encode(),
            )

    toConcat = heads
    if not firstDynamicTail:
        toConcat.append(tail_holder.load())

    if len(toConcat) == 0:
        return Bytes("")

    return Concat(*toConcat)
Esempio n. 19
0
    def wrap_handler(
        is_method_call: bool, handler: ABIReturnSubroutine | SubroutineFnWrapper | Expr
    ) -> Expr:
        """This is a helper function that handles transaction arguments passing in bare-app-call/abi-method handlers.

        If `is_method_call` is True, then it can only be `ABIReturnSubroutine`,
        otherwise:
            - both `ABIReturnSubroutine` and `Subroutine` takes 0 argument on the stack.
            - all three cases have none (or void) type.

        On ABI method case, if the ABI method has more than 15 args, this function manages to de-tuple
        the last (16-th) Txn app-arg into a list of ABI method arguments, and pass in to the ABI method.

        Args:
            is_method_call: a boolean value that specify if the handler is an ABI method.
            handler: an `ABIReturnSubroutine`, or `SubroutineFnWrapper` (for `Subroutine` case), or an `Expr`.
        Returns:
            Expr:
                - for bare-appcall it returns an expression that the handler takes no txn arg and Approve
                - for abi-method it returns the txn args correctly decomposed into ABI variables,
                  passed in ABIReturnSubroutine and logged, then approve.
        """
        if not is_method_call:
            match handler:
                case Expr():
                    if handler.type_of() != TealType.none:
                        raise TealInputError(
                            f"bare appcall handler should be TealType.none not {handler.type_of()}."
                        )
                    return handler if handler.has_return() else Seq(handler, Approve())
                case SubroutineFnWrapper():
                    if handler.type_of() != TealType.none:
                        raise TealInputError(
                            f"subroutine call should be returning TealType.none not {handler.type_of()}."
                        )
                    if handler.subroutine.argument_count() != 0:
                        raise TealInputError(
                            f"subroutine call should take 0 arg for bare-app call. "
                            f"this subroutine takes {handler.subroutine.argument_count()}."
                        )
                    return Seq(handler(), Approve())
                case ABIReturnSubroutine():
                    if handler.type_of() != "void":
                        raise TealInputError(
                            f"abi-returning subroutine call should be returning void not {handler.type_of()}."
                        )
                    if handler.subroutine.argument_count() != 0:
                        raise TealInputError(
                            f"abi-returning subroutine call should take 0 arg for bare-app call. "
                            f"this abi-returning subroutine takes {handler.subroutine.argument_count()}."
                        )
                    return Seq(cast(Expr, handler()), Approve())
                case _:
                    raise TealInputError(
                        "bare appcall can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg"
                    )
        else:
            if not isinstance(handler, ABIReturnSubroutine):
                raise TealInputError(
                    f"method call should be only registering ABIReturnSubroutine, got {type(handler)}."
                )
            if not handler.is_abi_routable():
                raise TealInputError(
                    f"method call ABIReturnSubroutine is not routable "
                    f"got {handler.subroutine.argument_count()} args with {len(handler.subroutine.abi_args)} ABI args."
                )

            # All subroutine args types
            arg_type_specs = cast(
                list[abi.TypeSpec], handler.subroutine.expected_arg_types
            )

            # All subroutine arg values, initialize here and use below instead of
            # creating new instances on the fly so we dont have to think about splicing
            # back in the transaction types
            arg_vals = [typespec.new_instance() for typespec in arg_type_specs]

            # Only args that appear in app args
            app_arg_vals: list[abi.BaseType] = [
                ats for ats in arg_vals if not isinstance(ats, abi.Transaction)
            ]

            for aav in app_arg_vals:
                # If we're here we know the top level isnt a Transaction but a transaction may
                # be included in some collection type like a Tuple or Array, raise error
                # as these are not supported
                if abi.contains_type_spec(aav.type_spec(), abi.TransactionTypeSpecs):
                    raise TealInputError(
                        "A Transaction type may not be included in Tuples or Arrays"
                    )

            # assign to a var here since we modify app_arg_vals later
            tuplify = len(app_arg_vals) > METHOD_ARG_NUM_CUTOFF

            # only transaction args (these are omitted from app args)
            txn_arg_vals: list[abi.Transaction] = [
                ats for ats in arg_vals if isinstance(ats, abi.Transaction)
            ]

            # Tuple-ify any app args after the limit
            if tuplify:
                last_arg_specs_grouped: list[abi.TypeSpec] = [
                    t.type_spec() for t in app_arg_vals[METHOD_ARG_NUM_CUTOFF - 1 :]
                ]
                app_arg_vals = app_arg_vals[: METHOD_ARG_NUM_CUTOFF - 1]
                app_arg_vals.append(
                    abi.TupleTypeSpec(*last_arg_specs_grouped).new_instance()
                )

            # decode app args
            decode_instructions: list[Expr] = [
                app_arg.decode(Txn.application_args[idx + 1])
                for idx, app_arg in enumerate(app_arg_vals)
            ]

            # "decode" transaction types by setting the relative index
            if len(txn_arg_vals) > 0:
                txn_arg_len = len(txn_arg_vals)
                # The transactions should appear in the group in the order they're specified in the method signature
                # and should be relative to the current transaction.

                # ex:
                # doit(axfer,pay,appl)
                # would be 4 transactions
                #      current_idx-3 = axfer
                #      current_idx-2 = pay
                #      current_idx-1 = appl
                #      current_idx-0 = the txn that triggered the current eval (not specified but here for completeness)

                # since we're iterating in order of the txns appearance in the args we
                # subtract the current index from the total length to get the offset.
                # and subtract that from the current index to get the absolute position
                # in the group

                txn_decode_instructions: list[Expr] = []

                for idx, arg_val in enumerate(txn_arg_vals):
                    txn_decode_instructions.append(
                        arg_val._set_index(Txn.group_index() - Int(txn_arg_len - idx))
                    )
                    spec = arg_val.type_spec()
                    if type(spec) is not abi.TransactionTypeSpec:
                        # this is a specific transaction type
                        txn_decode_instructions.append(
                            Assert(arg_val.get().type_enum() == spec.txn_type_enum())
                        )

                decode_instructions += txn_decode_instructions

            # de-tuple into specific values using `store_into` on
            # each element of the tuple'd arguments
            if tuplify:
                tupled_abi_vals: list[abi.BaseType] = arg_vals[
                    METHOD_ARG_NUM_CUTOFF - 1 :
                ]
                tupled_arg: abi.Tuple = cast(abi.Tuple, app_arg_vals[-1])
                de_tuple_instructions: list[Expr] = [
                    tupled_arg[idx].store_into(arg_val)
                    for idx, arg_val in enumerate(tupled_abi_vals)
                ]
                decode_instructions += de_tuple_instructions

            # NOTE: does not have to have return, can be void method
            if handler.type_of() == "void":
                return Seq(
                    *decode_instructions,
                    cast(Expr, handler(*arg_vals)),
                    Approve(),
                )
            else:
                output_temp: abi.BaseType = cast(
                    OutputKwArgInfo, handler.output_kwarg_info
                ).abi_type.new_instance()
                subroutine_call: abi.ReturnedValue = cast(
                    abi.ReturnedValue, handler(*arg_vals)
                )
                return Seq(
                    *decode_instructions,
                    subroutine_call.store_into(output_temp),
                    abi.MethodReturn(output_temp),
                    Approve(),
                )
Esempio n. 20
0
def evaluate_subroutine(subroutine: SubroutineDefinition) -> SubroutineDeclaration:
    """
    Puts together the data necessary to define the code for a subroutine.
    "evaluate" is used here to connote evaluating the PyTEAL AST into a SubroutineDeclaration,
    but not actually placing it at call locations. The trickiest part here is managing the subroutine's arguments.
    The arguments are needed for two different code-paths, and there are 2 different argument types to consider
    for each of the code-paths:

    2 Argument Usages / Code-Paths
    - -------- ------   ----------
    Usage (A) for run-time: "argumentVars" --reverse--> "body_ops"
        These are "store" expressions that pick up parameters that have been pre-placed on the stack prior to subroutine invocation.
        The argumentVars are stored into local scratch space to be used by the TEAL subroutine.

    Usage (B) for compile-time: "loadedArgs"
        These are expressions supplied to the user-defined PyTEAL function.
        The loadedArgs are invoked to by the subroutine to create a self-contained AST which will translate into a TEAL subroutine.

    In both usage cases, we need to handle

    2 Argument Types
    - -------- -----
    Type 1 (by-value): these have python type Expr
    Type 2 (by-reference): these have python type ScratchVar
    Type 3 (ABI): these are ABI typed variables with scratch space storage, and still pass by value
    Type 4 (ABI-output-arg): ABI typed variables with scratch space, a new ABI instance is generated inside function body,
        not one of the cases in the previous three options

    Usage (A) "argumentVars" - Storing pre-placed stack variables into local scratch space:
        Type 1. (by-value) use ScratchVar.store() to pick the actual value into a local scratch space
        Type 2. (by-reference) ALSO use ScratchVar.store() to pick up from the stack
            NOTE: SubroutineCall.__teal__() has placed the _SLOT INDEX_ on the stack so this is stored into the local scratch space
        Type 3. (ABI) abi_value.stored_value.store() to pick from the stack
        Type 4. (ABI-output-arg) it is not really used here, since it is only generated internal of the subroutine

    Usage (B) "loadedArgs" - Passing through to an invoked PyTEAL subroutine AST:
        Type 1. (by-value) use ScratchVar.load() to have an Expr that can be compiled in python by the PyTEAL subroutine
        Type 2. (by-reference) use a DynamicScratchVar as the user will have written the PyTEAL in a way that satisfies
            the ScratchVar API. I.e., the user will write `x.load()` and `x.store(val)` as opposed to just `x`.
        Type 3. (ABI) use abi_value itself after storing stack value into scratch space.
        Type 4. (ABI-output-arg) generates a new instance of the ABI value,
            and appends a return expression of stored value of the ABI keyword value.
    """

    def var_n_loaded(
        param: str,
    ) -> tuple[ScratchVar, ScratchVar | abi.BaseType | Expr]:
        loaded_var: ScratchVar | abi.BaseType | Expr
        argument_var: ScratchVar

        if param in subroutine.by_ref_args:
            argument_var = DynamicScratchVar(TealType.anytype)
            loaded_var = argument_var
        elif param in subroutine.abi_args:
            internal_abi_var = subroutine.abi_args[param].new_instance()
            argument_var = internal_abi_var.stored_value
            loaded_var = internal_abi_var
        else:
            argument_var = ScratchVar(TealType.anytype)
            loaded_var = argument_var.load()

        return argument_var, loaded_var

    if len(subroutine.output_kwarg) > 1:
        raise TealInputError(
            f"ABI keyword argument num: {len(subroutine.output_kwarg)}. "
            f"Exceeding abi output keyword argument max number 1."
        )

    args = subroutine.arguments()

    arg_vars: list[ScratchVar] = []
    loaded_args: list[ScratchVar | Expr | abi.BaseType] = []
    for arg in args:
        arg_var, loaded_arg = var_n_loaded(arg)
        arg_vars.append(arg_var)
        loaded_args.append(loaded_arg)

    abi_output_kwargs: dict[str, abi.BaseType] = {}
    output_kwarg_info = OutputKwArgInfo.from_dict(subroutine.output_kwarg)
    output_carrying_abi: Optional[abi.BaseType] = None

    if output_kwarg_info:
        output_carrying_abi = output_kwarg_info.abi_type.new_instance()
        abi_output_kwargs[output_kwarg_info.name] = output_carrying_abi

    # Arg usage "B" supplied to build an AST from the user-defined PyTEAL function:
    subroutine_body = subroutine.implementation(*loaded_args, **abi_output_kwargs)

    if not isinstance(subroutine_body, Expr):
        raise TealInputError(
            f"Subroutine function does not return a PyTeal expression. Got type {type(subroutine_body)}."
        )

    deferred_expr: Optional[Expr] = None

    # if there is an output keyword argument for ABI, place the storing on the stack
    if output_carrying_abi:
        if subroutine_body.type_of() != TealType.none:
            raise TealInputError(
                f"ABI returning subroutine definition should evaluate to TealType.none, "
                f"while evaluate to {subroutine_body.type_of()}."
            )
        deferred_expr = output_carrying_abi.stored_value.load()

    # Arg usage "A" to be pick up and store in scratch parameters that have been placed on the stack
    # need to reverse order of argumentVars because the last argument will be on top of the stack
    body_ops = [var.slot.store() for var in arg_vars[::-1]]
    body_ops.append(subroutine_body)

    sd = SubroutineDeclaration(subroutine, Seq(body_ops), deferred_expr)
    sd.trace = subroutine_body.trace
    return sd
Esempio n. 21
0
    def MethodCall(
        cls,
        *,
        app_id: Expr,
        method_signature: str,
        args: list[abi.BaseType | Expr | dict[TxnField, Expr | list[Expr]]],
        extra_fields: dict[TxnField, Expr | list[Expr]] = None,
    ) -> Expr:
        """Adds an ABI method call transaction to the current inner transaction group.

        :any:`InnerTxnBuilder.Begin` must be called before a MethodCall can be added.

        Requires Teal version 6 or higher. This operation is only permitted in application mode.

        Args:
            app_id: An expression that evaluates to a `TealType.uint64` corresponding to the application being called.
            method_signature: A string representing the method signature of the method we're calling. This is used to do
                type checking on the arguments passed and to create the method selector passed as the first argument.
            args: A list of arguments to pass to the application. The values in this list depend on the kind of argument you wish to pass:

                - For basic ABI arguments (not Reference or Transaction types):
                    If an ABI type is passed it **MUST** match the type specified in the `method_signature`. If an Expr is passed it must evaluate to `TealType.bytes` but beyond that no type checking is performed.

                - For Reference arguments:
                    Either the Reference type or an Expr that returns the type corresponding to the reference type are allowed.
                    (i.e. Asset is TealType.uint64, Application is TealType.uint64, Account is TealType.bytes)

                - For Transaction arguments:
                    A dictionary containing TxnField to Expr that describe Transactions to be pre-pended to the transaction group being constructed.  The `TxnField.type_enum` key MUST be set and MUST match the expected transaction type specified in the `method_signature`.

            extra_fields (optional): A dictionary whose keys are fields to set and whose values are the value each
                field should take. Each value must evaluate to a type that is compatible with the
                field being set. These fields are set on the ApplicationCallTransaction being constructed
        """

        require_type(app_id, TealType.uint64)

        # Default, always need these
        fields_to_set = [
            cls.SetField(TxnField.type_enum, TxnType.ApplicationCall),
            cls.SetField(TxnField.application_id, app_id),
        ]

        # We only care about the args
        arg_type_specs: list[abi.TypeSpec]
        arg_type_specs, _ = abi.type_specs_from_signature(method_signature)

        if len(args) != len(arg_type_specs):
            raise TealInputError(
                f"Expected {len(arg_type_specs)} arguments, got {len(args)}"
            )

        # Start app args with the method selector
        app_args: list[Expr] = [MethodSignature(method_signature)]

        # Transactions are not included in the App Call
        txns_to_pass: list[Expr] = []

        # Reference Types are treated specially
        accts: list[Expr] = []
        apps: list[Expr] = []
        assets: list[Expr] = []

        for idx, method_arg_ts in enumerate(arg_type_specs):
            arg = args[idx]

            if method_arg_ts in abi.TransactionTypeSpecs:
                if not isinstance(arg, dict):
                    raise TealTypeError(arg, dict[TxnField, Expr | list[Expr]])

                if TxnField.type_enum not in arg:
                    raise TealInputError(
                        f"Expected Transaction at arg {idx} to contain field type_enum"
                    )

                if type(arg[TxnField.type_enum]) is not EnumInt:
                    raise TealTypeError(arg[TxnField.type_enum], EnumInt)

                txntype = cast(EnumInt, arg[TxnField.type_enum]).name
                # If the arg is an unspecified transaction, no need to check the type_enum
                if not type(
                    method_arg_ts
                ) is abi.TransactionTypeSpec and txntype != str(method_arg_ts):
                    raise TealInputError(
                        f"Expected Transaction at arg {idx} to be {method_arg_ts}, got {txntype}"
                    )

                txns_to_pass.append(InnerTxnBuilder.SetFields(arg))

            elif method_arg_ts in abi.ReferenceTypeSpecs:
                match method_arg_ts:
                    # For both acct and application, add index to
                    # app args _after_ appending since 0 is implicitly set
                    case abi.AccountTypeSpec():
                        if isinstance(arg, Expr):
                            # require the address is passed
                            require_type(arg, TealType.bytes)
                            accts.append(arg)
                        elif isinstance(arg, abi.Account):
                            accts.append(arg.address())
                        else:
                            raise TealTypeError(arg, abi.Account | Expr)

                        app_args.append(
                            Bytes(
                                algosdk.abi.ABIType.from_string("uint8").encode(
                                    len(accts)
                                )
                            )
                        )

                    case abi.ApplicationTypeSpec():
                        if isinstance(arg, Expr):
                            # require the app id be passed
                            require_type(arg, TealType.uint64)
                            apps.append(arg)
                        elif isinstance(arg, abi.Application):
                            apps.append(arg.application_id())
                        else:
                            raise TealTypeError(arg, abi.Application | Expr)

                        app_args.append(
                            Bytes(
                                algosdk.abi.ABIType.from_string("uint8").encode(
                                    len(apps)
                                )
                            )
                        )

                    # For assets, add to app_args prior to appending to assets array
                    case abi.AssetTypeSpec():
                        app_args.append(
                            Bytes(
                                algosdk.abi.ABIType.from_string("uint8").encode(
                                    len(assets)
                                )
                            )
                        )

                        if isinstance(arg, Expr):
                            require_type(arg, TealType.uint64)
                            assets.append(arg)
                        elif isinstance(arg, abi.Asset):
                            assets.append(arg.asset_id())
                        else:
                            raise TealTypeError(arg, abi.Asset | Expr)
            else:
                if isinstance(arg, Expr):
                    # This should _always_ be bytes, since we assume its already abi encoded
                    require_type(arg, TealType.bytes)
                    app_args.append(arg)
                elif isinstance(arg, abi.BaseType):
                    if arg.type_spec() != method_arg_ts:
                        raise TealTypeError(arg.type_spec(), method_arg_ts)
                    app_args.append(arg.encode())
                else:
                    raise TealTypeError(arg, abi.BaseType | Expr)

        if len(accts) > 0:
            fields_to_set.append(cls.SetField(TxnField.accounts, accts))

        if len(apps) > 0:
            fields_to_set.append(cls.SetField(TxnField.applications, apps))

        if len(assets) > 0:
            fields_to_set.append(cls.SetField(TxnField.assets, assets))

        fields_to_set.append(cls.SetField(TxnField.application_args, app_args))

        return Seq(
            # Add the transactions first
            *[Seq(ttp, InnerTxnBuilder.Next()) for ttp in txns_to_pass],
            # Set the fields for the app call in app args and foreign arrays
            *fields_to_set,
            # Add any remaining fields specified by the user
            InnerTxnBuilder.SetFields({} if extra_fields is None else extra_fields),
        )
Esempio n. 22
0
 def outputReducer(self, reducer: Callable[..., Expr]) -> Expr:
     input = [
         slot.load(self.types[i])
         for i, slot in enumerate(self.output_slots)
     ]
     return Seq(self, reducer(*input))
Esempio n. 23
0
    def set(
        self,
        value: Union[
            str,
            bytes,
            Expr,
            Sequence[Byte],
            StaticArray[Byte, Literal[AddressLength.Bytes]],
            ComputedValue[StaticArray[Byte, Literal[AddressLength.Bytes]]],
            "Address",
            ComputedValue["Address"],
        ],
    ):
        """Set the value of this Address to the input value.

        The behavior of this method depends on the input argument type:

            * :code:`str`: set the value to the address from the encoded address string. This string must be a valid 58-character base-32 Algorand address with checksum.
            * :code:`bytes`: set the value to the raw address bytes. This byte string must have length 32.
            * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.bytes. The program will fail if the evaluated byte string length is not 32.
            * :code:`Sequence[Byte]`: set the bytes of this Address to those contained in this Python sequence (e.g. a list or tuple). A compiler error will occur if the sequence length is not 32.
            * :code:`StaticArray[Byte, 32]`: copy the bytes from a StaticArray of 32 bytes.
            * :code:`ComputedValue[StaticArray[Byte, 32]]`: copy the bytes from a StaticArray of 32 bytes produced by a ComputedValue.
            * :code:`Address`: copy the value from another Address.
            * :code:`ComputedValue[Address]`: copy the value from an Address produced by a ComputedValue.

        Args:
            value: The new value this Address should take. This must follow the above constraints.

        Returns:
            An expression which stores the given value into this Address.
        """

        match value:
            case ComputedValue():
                pts = value.produced_type_spec()
                if pts == AddressTypeSpec() or pts == StaticArrayTypeSpec(
                    ByteTypeSpec(), AddressLength.Bytes
                ):
                    return value.store_into(self)

                raise TealInputError(
                    f"Got ComputedValue with type spec {pts}, expected AddressTypeSpec or StaticArray[Byte, Literal[AddressLength.Bytes]]"
                )
            case BaseType():
                if (
                    value.type_spec() == AddressTypeSpec()
                    or value.type_spec()
                    == StaticArrayTypeSpec(ByteTypeSpec(), AddressLength.Bytes)
                ):
                    return self.stored_value.store(value.stored_value.load())

                raise TealInputError(
                    f"Got {value} with type spec {value.type_spec()}, expected AddressTypeSpec"
                )
            case str():
                # Addr throws if value is invalid address
                return self.stored_value.store(Addr(value))
            case bytes():
                if len(value) == AddressLength.Bytes:
                    return self.stored_value.store(Bytes(value))
                raise TealInputError(
                    f"Got bytes with length {len(value)}, expected {AddressLength.Bytes}"
                )
            case Expr():
                return Seq(
                    self.stored_value.store(value),
                    Assert(
                        Len(self.stored_value.load()) == Int(AddressLength.Bytes.value)
                    ),
                )
            case CollectionSequence():
                if len(value) != AddressLength.Bytes:
                    raise TealInputError(
                        f"Got bytes with length {len(value)}, expected {AddressLength.Bytes}"
                    )
                return super().set(cast(Sequence[Byte], value))

        raise TealInputError(
            f"Got {type(value)}, expected Sequence, StaticArray, ComputedValue, Address, str, bytes, Expr"
        )