Building Your First Counter Smart Contract
Welcome to your first Move smart contract on Cedra! In this tutorial, you'll learn how to create, compile, test, and deploy a simple counter contract that demonstrates the fundamental concepts of Move programming.
๐ Prerequisitesโ
Before we start, make sure you have:
๐ Step 1: Create a New Move Projectโ
First, let's create a new Move project for our counter contract:
mkdir counter-project
cd counter-project
cedra move init --name counter
This creates a new Move package with the following structure:
counter-project/
โโโ Move.toml
โโโ sources/
For more details on Move package structure, see Move Package Management.
Update Move.tomlโ
Before writing our contract, we need to setup counter address in the Move.toml. Learn more about Move.toml configuration.
...
[addresses]
counter = "YOUR_ACCOUNT_ADDRESS"
...
๐ Step 2: Write the Counter Contractโ
Create a new file sources/counter.move with the following content:
module counter::simple_counter {
use std::signer;
/// The counter resource that will be stored in each account
struct Counter has key {
value: u64,
}
/// Error codes
const E_COUNTER_NOT_EXISTS: u64 = 1;
/// Initialize a counter for the given account with value 0
public entry fun initialize(account: &signer) {
let counter = Counter { value: 0 };
move_to(account, counter);
}
/// Increment the counter by 1
public entry fun increment(account: &signer) acquires Counter {
let account_addr = signer::address_of(account);
assert!(exists<Counter>(account_addr), E_COUNTER_NOT_EXISTS);
let counter = borrow_global_mut<Counter>(account_addr);
counter.value = counter.value + 1;
}
/// Decrement the counter by 1 (with underflow protection)
public entry fun decrement(account: &signer) acquires Counter {
let account_addr = signer::address_of(account);
assert!(exists<Counter>(account_addr), E_COUNTER_NOT_EXISTS);
let counter = borrow_global_mut<Counter>(account_addr);
if (counter.value > 0) {
counter.value = counter.value - 1;
};
}
#[view]
/// Get the current counter value (read-only)
public fun get_count(account_addr: address): u64 acquires Counter {
assert!(exists<Counter>(account_addr), E_COUNTER_NOT_EXISTS);
let counter = borrow_global<Counter>(account_addr);
counter.value
}
/// Reset the counter to 0
public entry fun reset(account: &signer) acquires Counter {
let account_addr = signer::address_of(account);
assert!(exists<Counter>(account_addr), E_COUNTER_NOT_EXISTS);
let counter = borrow_global_mut<Counter>(account_addr);
counter.value = 0;
}
}
๐งช Step 3: Add Testsโ
Let's add comprehensive tests to our contract. For a complete guide on Move testing including annotations and best practices, see Move Unit Testing.
Add the following test functions to your tests/counter.move file:
#[test_only]
module counter::counter_tests {
use std::signer;
use counter::simple_counter;
#[test(account = @0x1)]
public fun test_initialize_and_get_count(account: &signer) {
simple_counter::initialize(account);
let count = simple_counter::get_count(signer::address_of(account));
assert!(count == 0, 1);
}
#[test(account = @0x1)]
public fun test_increment(account: &signer) {
simple_counter::initialize(account);
simple_counter::increment(account);
simple_counter::increment(account);
let count = simple_counter::get_count(signer::address_of(account));
assert!(count == 2, 2);
}
#[test(account = @0x1)]
public fun test_decrement(account: &signer) {
simple_counter::initialize(account);
simple_counter::increment(account);
simple_counter::increment(account);
simple_counter::decrement(account);
let count = simple_counter::get_count(signer::address_of(account));
assert!(count == 1, 3);
}
#[test(account = @0x1)]
public fun test_reset(account: &signer) {
simple_counter::initialize(account);
simple_counter::increment(account);
simple_counter::reset(account);
let count = simple_counter::get_count(signer::address_of(account));
assert!(count == 0, 4);
}
#[test(account = @0x1)]
public fun test_decrement_underflow_protection(account: &signer) {
simple_counter::initialize(account);
simple_counter::decrement(account); // Should not panic, just stay at 0
let count = simple_counter::get_count(signer::address_of(account));
assert!(count == 0, 5);
}
}
๐จ Step 4: Compile and Testโ
Now let's compile and test our contract:
# Navigate to the counter directory
cd counter-project
# Compile the contract
cedra move compile
# Run the tests
cedra move test
You should see output indicating that all tests passed! โ
๐ Step 5: Deploy to Testnetโ
Time to deploy our counter to Cedra testnet. For detailed deployment options, see Deploy to Blockchain.
cedra move publish
When prompted, type yes to confirm the transaction.
๐ฎ Step 6: Interact with Your Contractโ
Once deployed, let's interact with our counter, and don't forget to use module address instead of default:
Initialize the Counterโ
cedra move run --function-id default::simple_counter::initialize
Increment the Counterโ
cedra move run --function-id default::simple_counter::increment
Check the Current Valueโ
cedra move view --function-id default::simple_counter::get_count --args address:default
Increment a Few More Timesโ
cedra move run --function-id default::simple_counter::increment
cedra move run --function-id default::simple_counter::increment
Check the Value Againโ
cedra move view --function-id default::simple_counter::get_count --args address:default
You should see the counter value increasing! ๐
๐ง Understanding the Codeโ
Let's break down the key concepts:
Resources (struct Counter has key)โ
- Resources are Move's way of representing digital assets
- The
keyability allows the struct to be stored at the top level of an account - Our
Counterresource holds a singleu64value
Entry Functions (public entry fun)โ
- Entry functions can be called directly from transactions
- They're the public interface of your smart contract
Acquires (acquires Counter)โ
- Functions that read from or modify global storage must declare what they access
- This helps Move's type system prevent many common bugs
Global Storage Operationsโ
move_to(): Store a resource in an accountborrow_global(): Read from global storageborrow_global_mut(): Modify global storageexists<T>(): Check if a resource exists
View Functions (#[view])โ
- View functions are read-only and don't modify state
- They can be called without creating a transaction
๐จ Next Stepsโ
Congratulations! You've built your first Move smart contract on Cedra. Here are some ideas to extend your counter:
- Add a step parameter - Allow incrementing/decrementing by custom amounts
- Multiple counters - Store multiple named counters in one resource
- Access controls - Add admin functions or ownership features
- Events - Emit events when the counter changes
- Counter factory - Create a system for multiple independent counters
๐ Additional Resourcesโ
Ready to build something more complex? Check out our Fungible Asset Guide to learn about creating tokens on Cedra! ๐ช