Your First Contract¶
Let's build a simple counter contract step by step.
The Complete Contract¶
contract Counter {
// State variables
uint64 public count;
address public owner;
// Events
event CountChanged(address indexed changer, uint64 oldValue, uint64 newValue);
event OwnerChanged(address indexed oldOwner, address indexed newOwner);
// Errors
error Unauthorized(address caller);
error InvalidAmount(uint64 amount);
// Constructor
constructor() {
owner = msg.sender;
count = 0;
}
// Increment the counter
function increment() public {
uint64 oldValue = count;
count += 1;
emit CountChanged(msg.sender, oldValue, count);
}
// Increment by a specific amount
function incrementBy(uint64 amount) public {
require(amount > 0, "Amount must be positive");
uint64 oldValue = count;
count += amount;
emit CountChanged(msg.sender, oldValue, count);
}
// Decrement the counter
function decrement() public {
require(count > 0, "Counter cannot go below zero");
uint64 oldValue = count;
count -= 1;
emit CountChanged(msg.sender, oldValue, count);
}
// Reset counter (owner only)
function reset() public {
require(msg.sender == owner, "Only owner can reset");
uint64 oldValue = count;
count = 0;
emit CountChanged(msg.sender, oldValue, 0);
}
// Transfer ownership
function transferOwnership(address newOwner) public {
require(msg.sender == owner, "Only owner can transfer");
require(newOwner != address(0), "Invalid new owner");
address oldOwner = owner;
owner = newOwner;
emit OwnerChanged(oldOwner, newOwner);
}
// View functions
function getCount() public view returns (uint64) {
return count;
}
function getOwner() public view returns (address) {
return owner;
}
}
Breaking It Down¶
State Variables¶
State variables are stored on-chain. The public keyword automatically generates getter functions.
uint64- 64-bit unsigned integeraddress- Solana public key (32 bytes)
Events¶
Events are logged to the transaction and can be monitored by clients. The indexed keyword allows efficient filtering.
Errors¶
Custom errors provide meaningful failure messages with associated data.
Constructor¶
The constructor runs once when the contract is deployed. msg.sender is the account that deployed the contract.
Functions¶
Public Functions¶
Public functions can be called by anyone.
View Functions¶
View functions don't modify state and are read-only.
Access Control¶
Use require to enforce conditions. If the condition is false, the transaction reverts with the error message.
Emitting Events¶
Use emit to log events to the blockchain.
Build and Test¶
Check for Errors¶
Add Tests¶
#[test]
function testIncrement() {
// Initial state
assertEq(count, 0, "Initial count should be 0");
// Increment
increment();
assertEq(count, 1, "Count should be 1 after increment");
// Increment again
increment();
assertEq(count, 2, "Count should be 2 after second increment");
}
#[test]
function testIncrementBy() {
incrementBy(5);
assertEq(count, 5, "Count should be 5");
incrementBy(10);
assertEq(count, 15, "Count should be 15");
}
#[test]
#[should_fail]
function testDecrementUnderflow() {
// This should fail - can't decrement below 0
decrement();
}
Run Tests¶
Deploy¶
To Localnet¶
# Start local validator
solana-test-validator
# Deploy
solscript deploy src/main.sol --cluster localnet
To Devnet¶
# Get devnet SOL
solana airdrop 2 --url devnet
# Deploy
solscript deploy src/main.sol --cluster devnet
Interact with Your Contract¶
After deployment, you can interact with your contract using:
- Solana CLI - For basic operations
- Anchor Client - For TypeScript/JavaScript
- Web3.js - For web applications
Example using Anchor client:
import * as anchor from "@coral-xyz/anchor";
const program = anchor.workspace.Counter;
// Call increment
await program.methods.increment().rpc();
// Read count
const count = await program.account.counter.fetch(counterPDA);
console.log("Count:", count.count.toString());