Build Your First Contract
A complete step-by-step tutorial to build, test, and deploy a counter smart contract.
What you'll build
A counter contract with the following features:
- Increment and decrement functionality
- Owner-only reset capability
- Event logging for state changes
- Full test coverage using Ghost testing
1. Project Setup
First, let's create a new project:
- Go to Projects
- Click New Project
- Name it "counter-tutorial"
- Select Algorand TypeScript
- Click Create
You'll be taken to the editor with a starter template.
2. Define the Contract Structure
Replace the starter code with our counter contract. Let's build it piece by piece.
Import and Class Definition
1import { Contract } from '@algorandfoundation/algorand-typescript'
2
3class Counter extends Contract {
4 // Contract implementation goes here
5}
Every PuyaTS contract extends the Contract base class from the Algorand TypeScript library.
3. Add State Variables
Our counter needs to store two pieces of data:
1import { Contract, GlobalState, Address, uint64 } from '@algorandfoundation/algorand-typescript'
2
3class Counter extends Contract {
4 // The current count value
5 count = GlobalState<uint64>({ initialValue: 0 })
6
7 // The owner who can reset the counter
8 owner = GlobalState<Address>()
9}
GlobalState stores data persistently on the blockchain. The initialValue is set when the contract is first created.4. Add the Create Method
The createApplication method runs when the contract is first deployed:
1import { Contract, GlobalState, Address, Txn, uint64 } from '@algorandfoundation/algorand-typescript'
2
3class Counter extends Contract {
4 count = GlobalState<uint64>({ initialValue: 0 })
5 owner = GlobalState<Address>()
6
7 // Called when contract is created
8 createApplication(): void {
9 // Set the deployer as the owner
10 this.owner.value = Txn.sender
11 }
12}
Txn.sender gives us the address of whoever is calling the contract - in this case, the deployer.
5. Add Counter Methods
Now let's add the core functionality:
1import {
2 Contract,
3 GlobalState,
4 Address,
5 Txn,
6 assert,
7 uint64
8} from '@algorandfoundation/algorand-typescript'
9
10class Counter extends Contract {
11 count = GlobalState<uint64>({ initialValue: 0 })
12 owner = GlobalState<Address>()
13
14 createApplication(): void {
15 this.owner.value = Txn.sender
16 }
17
18 // Increment the counter by 1
19 increment(): void {
20 this.count.value = this.count.value + 1
21 }
22
23 // Decrement the counter by 1
24 decrement(): void {
25 assert(this.count.value > 0, 'Counter cannot go below zero')
26 this.count.value = this.count.value - 1
27 }
28
29 // Get the current count
30 getCount(): uint64 {
31 return this.count.value
32 }
33
34 // Reset to zero (owner only)
35 reset(): void {
36 assert(Txn.sender === this.owner.value, 'Only owner can reset')
37 this.count.value = 0
38 }
39}
assert() function checks a condition and fails the transaction with an error message if it's false. Use it for input validation and access control.6. Build the Contract
With our code complete, let's compile it:
- Click the Build button (or press Cmd/Ctrl + B)
- Wait for compilation to complete
- Check the build panel for success or errors
If successful, you'll see artifacts named after your contract:
- Counter.approval.teal - The main contract logic
- Counter.clear.teal - The clear state program
- Counter.arc56.json - The ARC-56 specification (ABI)
- CounterClient.ts - Generated TypeScript client
- Counter.docs.md - Human-readable documentation
7. Test with Ghost Testing
Before deploying, let's test our contract without using real blockchain resources.
- Click the Ghost tab in the right panel
- Click New Test
- Add test calls in sequence:
increment()- Should succeedgetCount()- Should return 1increment()- Should succeedgetCount()- Should return 2decrement()- Should succeedgetCount()- Should return 1
- Click Run Test
Ghost testing shows you:
- State changes - What global/local state was modified
- Return values - What each method returned
- Opcode cost - How much computation each call used
- Execution trace - Step-by-step execution details
8. Test Edge Cases
Let's verify our safety checks work:
Test: Decrement Below Zero
- Create a new test
- Add
decrement()without any increments - Run the test - it should fail with "Counter cannot go below zero"
Test: Unauthorized Reset
- Create a new test
- Change the test sender to a different address
- Add
reset() - Run the test - it should fail with "Only owner can reset"
9. Deploy to Testnet
Once tests pass, let's deploy to Algorand Testnet:
- Go to the Deploy panel
- Select Algorand Testnet as the network
- Configure a signing account:
- Use a connected wallet, OR
- Add a signing account in Settings (with a funded testnet account)
- Click Deploy
- Review and sign the transaction
After deployment, you'll see:
- App ID - Your contract's unique identifier
- App Address - The contract's account address
- Transaction ID - The deployment transaction
10. Interact with Your Contract
Now you can call your deployed contract:
- Go to Explorer
- Enter your App ID
- You'll see the contract's current state and available methods
- Click on a method to call it
Try calling increment() a few times, then check getCount() to see the updated value!
Complete Code
Here's the final contract for reference:
1import {
2 Contract,
3 GlobalState,
4 Address,
5 Txn,
6 assert,
7 uint64
8} from '@algorandfoundation/algorand-typescript'
9
10/**
11 * A simple counter contract with owner-only reset.
12 */
13class Counter extends Contract {
14 /** Current counter value */
15 count = GlobalState<uint64>({ initialValue: 0 })
16
17 /** Address that can reset the counter */
18 owner = GlobalState<Address>()
19
20 /** Initialize the contract and set deployer as owner */
21 createApplication(): void {
22 this.owner.value = Txn.sender
23 }
24
25 /** Increment the counter by 1 */
26 increment(): void {
27 this.count.value = this.count.value + 1
28 }
29
30 /** Decrement the counter by 1 (cannot go below 0) */
31 decrement(): void {
32 assert(this.count.value > 0, 'Counter cannot go below zero')
33 this.count.value = this.count.value - 1
34 }
35
36 /** Get the current counter value */
37 getCount(): uint64 {
38 return this.count.value
39 }
40
41 /** Reset counter to 0 (owner only) */
42 reset(): void {
43 assert(Txn.sender === this.owner.value, 'Only owner can reset')
44 this.count.value = 0
45 }
46}
Next Steps
Congratulations! You've built, tested, and deployed a complete smart contract. Continue learning:
- PuyaTS Deep Dive - Advanced TypeScript patterns
- Ghost Testing Guide - Comprehensive testing strategies
- Network Configuration - Deploy to other networks
- Transaction Composer - Build complex transaction groups