Let's build a simple counter contract step by step.
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;
}
}uint64 public count;
address public owner;State variables are stored on-chain. The public keyword automatically generates getter functions.
uint64- 64-bit unsigned integeraddress- Solana public key (32 bytes)
event CountChanged(address indexed changer, uint64 oldValue, uint64 newValue);Events are logged to the transaction and can be monitored by clients. The indexed keyword allows efficient filtering.
error Unauthorized(address caller);
error InvalidAmount(uint64 amount);Custom errors provide meaningful failure messages with associated data.
constructor() {
owner = msg.sender;
count = 0;
}The constructor runs once when the contract is deployed. msg.sender is the account that deployed the contract.
function increment() public {
count += 1;
}Public functions can be called by anyone.
function getCount() public view returns (uint64) {
return count;
}View functions don't modify state and are read-only.
require(msg.sender == owner, "Only owner can reset");Use require to enforce conditions. If the condition is false, the transaction reverts with the error message.
emit CountChanged(msg.sender, oldValue, count);Use emit to log events to the blockchain.
solscript check src/main.sol#[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();
}solscript test src/main.sol# Start local validator
solana-test-validator
# Deploy
solscript deploy src/main.sol --cluster localnet# Get devnet SOL
solana airdrop 2 --url devnet
# Deploy
solscript deploy src/main.sol --cluster devnetAfter 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());