Build on Algorand
Basic Smart Contract with Python
Evolution from TEAL to Python
Traditional Algorand smart contracts use TEAL (Transaction Execution Approval Language), a low-level stack-based language.
New Python-based frameworks allow writing smart contracts with familiar syntax:
- PyTeal: Python library generating TEAL.
- PuyaPy: A new high-level Pythonic framework compiling Python directly to TEAL.
Key Components
- ARC-4 Standard: Standardized ABI for method calls.
- AlgoKit: Toolkit for development, testing, and deployment.
- PyTeal: Library for TEAL code generation.
- PuyaPy: Combines Python readability with Algorand-specific features.
What is PuyaPy?
- Python framework for Algorand smart contracts compiling to TEAL.
- Enables writing contracts using Python classes and decorators.
- Supports ARC-4 compliance and simplifies state management.
Core Class Structure
- All contracts inherit from the ARC4Contract base class.
- For stateful contracts, inherit from ARC4Contract.
- __init__ method initializes contract state during deployment.
Example: Voting Contract Skeleton
from algopy import ARC4Contract, UInt64, String
from algopy.arc4 import abimethod
class Voting(ARC4Contract):
title: String
description: String
noOfOptions: UInt64
def __init__(self) -> None:
self.title = String("")
self.description = String("")
self.noOfOptions = UInt64(0)
Method Types
Example: Voting Method
@abimethod()
def vote(self, option: UInt64) -> None:
assert Global.latest_timestamp < self.endsAt, "Voting has ended"
assert Global.latest_timestamp > self.startsAt, "Voting has not started"
assert option >= 1 and option <= self.noOfOptions, "Invalid option"
val, exist = self.localState.maybe(Txn.sender)
assert not exist, "Already voted"
self.localState[Txn.sender] = option
match option:
case 1:
self.option1Votes += 1
case 2:
self.option2Votes += 1
case 3:
self.option3Votes += 1
case 4:
self.option4Votes += 1
case _:
op.exit(0)
State Management System
Global State
- Stored on-chain, accessible by all users.
- Declared as class attributes with ARC4 types.
Local State
- Stored per user account.
- Use LocalState class for explicit management.
Example: Defining Global and Local State
from algopy import ARC4Contract, UInt64, String, LocalState
class Voting(ARC4Contract):
title: String
description: LocalState[String]
noOfOptions: UInt64
def __init__(self) -> None:
self.title = String("")
self.noOfOptions = UInt64(0)
self.is_voted = LocalState(UInt64)
ARC4 Data Types
The Algorand Virtual Machine supports uint64 and byte arrays. ARC4 maps Python types to ABI-compatible types:
- UInt64: 64-bit unsigned int
- String: UTF-8 string
- Bytes: Binary data
- Address: Algorand address
- Asset: Algorand assets
Example: Storing Data
from algopy import ARC4Contract, UInt64, String
from algopy import arc4
class Store(ARC4Contract):
stored_value: UInt64
stored_tag: String
@arc4.abimethod()
def store_data(self, store_v: UInt64, s_tag: String) -> None:
self.stored_value = store_v
self.stored_tag = s_tag
Function Types & Decorators
ABI Methods (@abimethod())
- Entry points for external calls.
- Can modify state (default readonly=False).
Example: Opt-In Method
@abimethod(allow_actions=["OptIn"])
def opt_in(self) -> None:
pass
- allow_actions=["OptIn"] enables handling of Opt-In transactions.
- Opt-In allows users to allocate local state storage.
Additional decorator options:
- allow_actions: Specify in-built actions.
- create: Require calling only during deployment (e.g. AppId == 0).
- name: Name the ABI method selector.
- readonly: If true, method can be simulated/dry-run without fees.
Subroutines (@subroutine())
- Reusable helper functions.
- Can not directly access contract state.
- Private/internal logic.
Example: Price Calculation
@subroutine()
def price_b(self) -> UInt64:
return self.shares_a + UInt64(1)
# Price increases with shares of Outcome A
Advanced Features
Error Handling
- Use assert for runtime validation.
- Compiled into TEAL assert opcode.
Example:
from algopy import *
from algopy.arc4 import abimethod
class Demo(ARC4Contract):
#state variables
price: UInt64
@abimethod()
def set_price(self, cost: UInt64) -> None:
assert cost > UInt64(0), "Price must be greater than 0"
self.price = cost
Inner Transactions: Create payments or asset transfers within contract logic.
Example: Delete Application
#Delete the application (only the creator can delete)
@abimethod(allow_actions=["DeleteApplication"])
def delete_application(self) -> None:
assert Txn.sender == self.creator, "Only the creator can delete the application"
#Transfer remaining funds to the creator
itxn.Payment(
receiver=self.creator,
amount=0,
close_remainder_to=self.creator,
fee=1_000,
).submit()
Best Practices
- Minimize state writes (each write costs gas).
- Validate inputs early using assert.
- Use readonly=True for methods that do not modify state.
- Always specify ARC4 types for ABI methods.
Full Contract: Counter
from algopy import *
from algopy.arc4 import abimethod
class Counter(ARC4Contract):
count: UInt64
def __init__(self) -> None:
self.count = UInt64(0)
@abimethod()
def increment(self) -> None:
self.count = self.count + UInt64(1)
@abimethod()
def decrement(self) -> None:
self.count = self.count - UInt64(1)
@abimethod()
def custom_increment(self, amount: UInt64) -> None:
assert amount > UInt64(0), "Amount must be positive"
self.count = self.count + amount
@abimethod(readonly=True)
def read_counter(self) -> UInt64:
return self.count
Comments
You need to enroll in the course to be able to comment!