def uint_decode( size: int, uint_var: ScratchVar, encoded: Expr, start_index: Optional[Expr], end_index: Optional[Expr], length: Optional[Expr], ) -> Expr: if size > 64: raise NotImplementedError( "Uint operations have not yet been implemented for bit sizes larger than 64" ) if size == 64: if start_index is None: if end_index is None and length is None: return uint_var.store(Btoi(encoded)) start_index = Int(0) return uint_var.store(ExtractUint64(encoded, start_index)) if start_index is None: start_index = Int(0) if size == 8: return uint_var.store(GetByte(encoded, start_index)) if size == 16: return uint_var.store(ExtractUint16(encoded, start_index)) if size == 32: return uint_var.store(ExtractUint32(encoded, start_index)) raise ValueError("Unsupported uint size: {}".format(size))
def substring_for_decoding( encoded: Expr, *, start_index: Expr = None, end_index: Expr = None, length: Expr = None, ) -> Expr: """A helper function for getting the substring to decode according to the rules of BaseType.decode.""" if length is not None and end_index is not None: raise TealInputError("length and end_index are mutually exclusive arguments") if start_index is not None: if length is not None: # substring from start_index to start_index + length return Extract(encoded, start_index, length) if end_index is not None: # substring from start_index to end_index return Substring(encoded, start_index, end_index) # substring from start_index to end of string return Suffix(encoded, start_index) if length is not None: # substring from 0 to length return Extract(encoded, Int(0), length) if end_index is not None: # substring from 0 to end_index return Substring(encoded, Int(0), end_index) # the entire string return encoded
def decode(self, encoded: Expr, *, start_index: Expr = None, end_index: Expr = None, length: Expr = None) -> Expr: if start_index is None: start_index = Int(0) return self.decode_bit(encoded, start_index * Int(NUM_BITS_IN_BYTE))
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()]))
def approval_condition_under_config(self) -> Expr | int: match self: case CallConfig.NEVER: return 0 case CallConfig.CALL: return Txn.application_id() != Int(0) case CallConfig.CREATE: return Txn.application_id() == Int(0) case CallConfig.ALL: return 1 case _: raise TealInternalError(f"unexpected CallConfig {self}")
def maximize_budget(self, fee: Expr) -> Expr: """Maximize the available opcode budget without spending more than the given fee. Note: the available budget just prior to calling maximize_budget() must be high enough to execute the budget increase code. The exact budget required depends on the provided fee expression, but a budget of ~25 should be sufficient for most use cases. If lack of budget is an issue then consider moving the call to maximize_budget() earlier in the pyteal program.""" require_type(fee, TealType.uint64) i = ScratchVar(TealType.uint64) n = fee / Global.min_txn_fee() return For(i.store(Int(0)), i.load() < n, i.store(i.load() + Int(1))).Do(self._construct_itxn())
def length(self) -> Expr: """Get the element number of this ABI static array. Returns: A PyTeal expression that represents the static array length. """ return Int(self.type_spec().length_static())
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()) ), )
def get(self) -> Expr: """Return the value held by this String as a PyTeal expression. The expression will have the type TealType.bytes. """ return Suffix( self.stored_value.load(), Int(Uint16TypeSpec().byte_length_static()) )
def get(self) -> Expr: """Get the byte encoding of this DynamicBytes. Dropping the uint16 encoding prefix for dynamic array length. Returns: A Pyteal expression that loads byte encoding of this DynamicBytes, and drop the first uint16 DynamicArray length encoding. """ return Suffix(self.stored_value.load(), Int(2))
def uint_encode(size: int, uint_var: Expr | ScratchVar) -> Expr: if isinstance(uint_var, ScratchVar): uint_var = uint_var.load() if size > 64: raise NotImplementedError( "Uint operations have not yet been implemented for bit sizes larger than 64" ) if size == 8: return SetByte(Bytes(b"\x00"), Int(0), uint_var) if size == 16: return Suffix(Itob(uint_var), Int(6)) if size == 32: return Suffix(Itob(uint_var), Int(4)) if size == 64: return Itob(uint_var) raise ValueError("Unsupported uint size: {}".format(size))
def __init__( self, name: str, bare_calls: BareCallActions = None, descr: str = None, ) -> None: """ Args: name: the name of the smart contract, used in the JSON object. bare_calls: the bare app call registered for each on_completion. descr: a description of the smart contract, used in the JSON object. """ self.name: str = name self.descr = descr self.approval_ast = ASTBuilder() self.clear_state_ast = ASTBuilder() self.methods: list[sdk_abi.Method] = [] self.method_sig_to_selector: dict[str, bytes] = dict() self.method_selector_to_sig: dict[bytes, str] = dict() if bare_calls and not bare_calls.is_empty(): bare_call_approval = bare_calls.approval_construction() if bare_call_approval: self.approval_ast.conditions_n_branches.append( CondNode( Txn.application_args.length() == Int(0), cast(Expr, bare_call_approval), ) ) bare_call_clear = bare_calls.clear_state_construction() if bare_call_clear: self.clear_state_ast.conditions_n_branches.append( CondNode( Txn.application_args.length() == Int(0), cast(Expr, bare_call_clear), ) )
def _set_index( self, value: Union[int, Expr, "Transaction", ComputedValue["Transaction"]] ) -> Expr: match value: case ComputedValue(): return self._set_with_computed_type(value) case BaseType(): return self.stored_value.store(self.stored_value.load()) case int(): return self.stored_value.store(Int(value)) case Expr(): return self.stored_value.store(value) case _: raise TealInputError(f"Cant store a {type(value)} in a Transaction")
def __teal__(self, options: "CompileOptions"): if not isinstance(self.startArg, Int) or not isinstance( self.endArg, Int): return TernaryExpr( Op.substring3, (TealType.bytes, TealType.uint64, TealType.uint64), TealType.bytes, self.stringArg, self.startArg, self.endArg, ).__teal__(options) op = self.__get_op(options) verifyProgramVersion( op.min_version, options.version, "Program version too low to use op {}".format(op), ) start, end = cast(Int, self.startArg).value, cast(Int, self.endArg).value if op == Op.extract: length = end - start return TealBlock.FromOp( options, TealOp(self, op, self.startArg.value, length), self.stringArg, ) elif op == Op.extract3: length = end - start return TealBlock.FromOp( options, TealOp(self, op), self.stringArg, self.startArg, Int(length), ) elif op == Op.substring: return TealBlock.FromOp(options, TealOp(self, op, start, end), self.stringArg) elif op == Op.substring3: return TealBlock.FromOp( options, TealOp(self, op), self.stringArg, self.startArg, self.endArg, )
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)), )
def _encode_bool_sequence(values: Sequence[Bool]) -> Expr: """Encoding a sequences of boolean values into a byte string. Args: values: The values to encode. Each must be an instance of Bool. Returns: An expression which creates an encoded byte string with the input boolean values. """ length = _bool_sequence_length(len(values)) expr: Expr = Bytes(b"\x00" * length) for i, value in enumerate(values): expr = SetBit(expr, Int(i), value.get()) return expr
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()), )
def set(self, value: Union[bool, Expr, "Bool", ComputedValue["Bool"]]) -> Expr: """Set the value of this Bool to the input value. The behavior of this method depends on the input argument type: * :code:`bool`: set the value to a Python boolean value. * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.uint64. All values greater than 0 are considered true, while 0 is considered false. * :code:`Bool`: copy the value from another Bool. * :code:`ComputedValue[Bool]`: copy the value from a Bool produced by a ComputedValue. Args: value: The new value this Bool should take. This must follow the above constraints. Returns: An expression which stores the given value into this Bool. """ if isinstance(value, ComputedValue): return self._set_with_computed_type(value) checked = False if type(value) is bool: value = Int(1 if value else 0) checked = True if isinstance(value, BaseType): if value.type_spec() != self.type_spec(): raise TealInputError("Cannot set type bool to {}".format( value.type_spec())) value = value.get() checked = True if checked: return self.stored_value.store(value) # Not(Not(value)) coerces all values greater than 0 to 1 return self.stored_value.store(Not(Not(value)))
def _index_tuple( value_types: Sequence[TypeSpec], encoded: Expr, index: int, output: BaseType ) -> Expr: if not (0 <= index < len(value_types)): raise ValueError("Index outside of range") offset = 0 ignoreNext = 0 lastBoolStart = 0 lastBoolLength = 0 for i, typeBefore in enumerate(value_types[:index]): if ignoreNext > 0: ignoreNext -= 1 continue if typeBefore == BoolTypeSpec(): lastBoolStart = offset lastBoolLength = _consecutive_bool_type_spec_num(value_types, i) offset += _bool_sequence_length(lastBoolLength) ignoreNext = lastBoolLength - 1 continue if typeBefore.is_dynamic(): offset += 2 continue offset += typeBefore.byte_length_static() valueType = value_types[index] if output.type_spec() != valueType: raise TypeError("Output type does not match value type") if type(output) is Bool: if ignoreNext > 0: # value is in the middle of a bool sequence bitOffsetInBoolSeq = lastBoolLength - ignoreNext bitOffsetInEncoded = lastBoolStart * NUM_BITS_IN_BYTE + bitOffsetInBoolSeq else: # value is the beginning of a bool sequence (or a single bool) bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE return output.decode_bit(encoded, Int(bitOffsetInEncoded)) if valueType.is_dynamic(): hasNextDynamicValue = False nextDynamicValueOffset = offset + 2 ignoreNext = 0 for i, typeAfter in enumerate(value_types[index + 1 :], start=index + 1): if ignoreNext > 0: ignoreNext -= 1 continue if type(typeAfter) is BoolTypeSpec: boolLength = _consecutive_bool_type_spec_num(value_types, i) nextDynamicValueOffset += _bool_sequence_length(boolLength) ignoreNext = boolLength - 1 continue if typeAfter.is_dynamic(): hasNextDynamicValue = True break nextDynamicValueOffset += typeAfter.byte_length_static() start_index = ExtractUint16(encoded, Int(offset)) if not hasNextDynamicValue: # This is the final dynamic value, so decode the substring from start_index to the end of # encoded return output.decode(encoded, start_index=start_index) # There is a dynamic value after this one, and end_index is where its tail starts, so decode # the substring from start_index to end_index end_index = ExtractUint16(encoded, Int(nextDynamicValueOffset)) return output.decode(encoded, start_index=start_index, end_index=end_index) start_index = Int(offset) length = Int(valueType.byte_length_static()) if index + 1 == len(value_types): if offset == 0: # This is the first and only value in the tuple, so decode all of encoded return output.decode(encoded) # This is the last value in the tuple, so decode the substring from start_index to the end of # encoded return output.decode(encoded, start_index=start_index) if offset == 0: # This is the first value in the tuple, so decode the substring from 0 with length length return output.decode(encoded, length=length) # This is not the first or last value, so decode the substring from start_index with length length return output.decode(encoded, start_index=start_index, length=length)
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(), )
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" )
"""An Enum object that defines the mode used for the OpUp utility. Note: the Explicit mode requires the app id to be provided through the foreign apps array in order for it to be accessible during evaluation. """ # The app to call must be provided by the user. Explicit = 0 # The app to call is created then deleted for each request to increase budget. OnCall = 1 ON_CALL_APP = Bytes("base16", "068101") # v6 program "int 1" MIN_TXN_FEE = Int(1000) class OpUp: """Utility for increasing opcode budget during app execution. Requires program version 6 or higher. Example: .. code-block:: python # OnCall mode: doesn't accept target_app_id as an argument opup = OpUp(OpUpMode.OnCall) program_with_opup = Seq( ..., opup.ensure_budget(Int(1000)),
def encode(self) -> Expr: return SetBit(Bytes(b"\x00"), Int(0), self.get())
def Reject() -> Expr: """Immediately exit the program and mark the execution as unsuccessful.""" return ExitProgram(Int(0))
def Approve() -> Expr: """Immediately exit the program and mark the execution as successful.""" return ExitProgram(Int(1))
def length(self) -> Expr: """Get the number of values this tuple holds as an Expr.""" return Int(self.type_spec().length_static())