Beginner-friendly Solana smart contract project using the Anchor framework.
The program acts like a tiny CRUD backend:
create_test→ Create a test recordupdate_test→ Update title/scoredelete_test→ Delete a record (close account)get_test→ Read one record (and emit an event)get_user_tests→ Read many records (demonstration; real listing is off-chain)
.
├── Anchor.toml
├── Cargo.toml
├── README.md
├── migrations
│ └── deploy.js
├── package.json
├── programs
│ └── tests_manager
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── tests
│ └── tests_manager.js
Anchor.toml: Anchor config (cluster, wallet, program id for localnet).Cargo.toml: Rust workspace (Anchor expects programs underprograms/*).programs/tests_manager/Cargo.toml: Program crate config + dependencies.programs/tests_manager/src/lib.rs: The on-chain program (accounts + instructions).tests/tests_manager.js: JavaScript tests showing CRUD calls.migrations/deploy.js: Anchor deploy hook (not needed for this simple program).package.json: Node tooling for Anchor tests.
On-chain programs don’t have “auto-increment integers” like SQL. We implement the simplest on-chain version:
- A single
Configaccount storesnext_id: u64 - Every
create_test:- reads
config.next_idas the new ID - creates the
Testaccount at a deterministic PDA using that ID - increments
config.next_id
- reads
This is easy to understand, but note:
- It’s a global counter (IDs are unique across the whole program).
- The program uses a PDA for
Configand for eachTest, so addresses are deterministic.
In programs/tests_manager/src/lib.rs:
-
Config(1 row total)next_id: u64
-
Test(many rows)id: u64(unique)owner: Pubkey(who owns this record)title: Stringscore: u8created_at: i64(Unix timestamp)
Each test lives at a PDA derived from:
["test", owner_pubkey, id_le_bytes]
So the same (owner, id) always points to the same on-chain address.
- Creates
Configif missing (first-ever create) - Uses
config.next_idas the newid - Creates a new
TestPDA for(owner, id) - Saves:
id,owner,title,score,created_at
- Derives the
TestPDA from(owner, id) - Checks
test.owner == signer - Updates
titleandscore
- Derives the
TestPDA from(owner, id) - Checks
test.owner == signer - Closes the account and returns rent to the owner
Solana programs can’t “return” data like a normal web server.
So get_test is implemented as:
- verify the correct PDA was provided for
(owner, id) - emit an event containing the test fields
In practice, clients usually do:
program.account.test.fetch(testPda)(RPC read)
Programs also can’t scan the chain and find “all accounts for this user”.
Recommended approach (off-chain):
program.account.test.all([{ memcmp: { offset: 16, bytes: ownerBase58 } }])
This project still includes a get_user_tests instruction as a demo:
- the client passes candidate
Testaccounts inremaining_accounts - the program filters them and emits events for those owned by
owner_pubkey
Prereqs:
- Solana CLI installed + configured
- Anchor installed (
avmrecommended) - Node.js installed
Commands:
npm install
anchor build
anchor testIf you change the program id, sync it:
anchor keys syncAll examples below are in tests/tests_manager.js.
- Create:
program.methods.createTest("Intro to Anchor", 95).accounts({ ... }).rpc()
- Update:
program.methods.updateTest(new anchor.BN(id), "New title", 91).accounts({ ... }).rpc()
- Delete:
program.methods.deleteTest(new anchor.BN(id)).accounts({ ... }).rpc()
- Fetch one:
program.account.test.fetch(testPda)
- Fetch all for user (recommended):
program.account.test.all([{ memcmp: { offset: 16, bytes: ownerBase58 } }])