Building Your First DApp on Solana with Rust
A developer's guide to getting started with Solana. Learn the basics of the Solana programming model and build a simple 'Hello, World' smart contract using Rust and Anchor.
While Ethereum dominates the landscape of smart contract development, a new generation of high-performance blockchains is emerging, and at the forefront is Solana. Known for its incredible speed and low transaction fees, Solana offers a different set of trade-offs and a unique programming model that is attracting developers looking to build highly scalable applications.
The primary language for building on Solana is Rust, a language renowned for its safety and performance. This guide will provide a step-by-step introduction to building your first simple program (Solana's term for smart contracts) on Solana using Rust and the popular Anchor framework.
Ethereum vs. Solana: A Different Mental Model
Before we write code, it's crucial to understand a key difference between Ethereum and Solana.
- Ethereum: Smart contracts on Ethereum contain both the code and the state (the data). A single contract holds all of its own data.
- Solana: The Solana programming model separates code and state. A program is deployed to the blockchain and is essentially stateless. The data it operates on is stored in separate "account" objects.
This separation of code and data is a key reason for Solana's performance. It allows for transactions to be processed in parallel, as the network knows in advance which accounts (data) a transaction will interact with.
The Tools You'll Need
- Rust: You'll need to have the Rust programming language installed. You can do this via
rustup
. - Solana Tool Suite: Install the Solana command-line tools.
- Anchor: Anchor is a framework that dramatically simplifies Solana development. It provides a CLI for managing your project and a Rust DSL (Domain Specific Language) for writing programs.
Actionable Step: Follow the official Anchor installation guide to set up your local development environment. This is a crucial first step.
Step 1: Initialize Your Anchor Project
Once Anchor is installed, you can create a new project with a single command.
anchor init my_first_dapp
This will create a new directory called my_first_dapp
with a standard project structure, including:
programs/
: This is where your on-chain Rust program code will live.tests/
: This is where you'll write your JavaScript/TypeScript tests.Anchor.toml
: Your project's configuration file.
Step 2: Writing Your First Program (programs/my_first_dapp/src/lib.rs
)
Let's build a very simple program: a counter that can be initialized and incremented. Open the lib.rs
file and replace the boilerplate with the following code.
use anchor_lang::prelude::*;
// This is your program's ID, which Anchor will generate for you.
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod my_first_dapp {
use super::*;
// This function initializes our counter account.
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let base_account = &mut ctx.accounts.base_account;
base_account.count = 0;
Ok(())
}
// This function increments the counter.
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let base_account = &mut ctx.accounts.base_account;
base_account.count += 1;
Ok(())
}
}
// This struct defines the accounts needed for the `initialize` function.
#[derive(Accounts)]
pub struct Initialize<'info> {
// We are creating a new account of type `BaseAccount`.
// `init` means we are creating the account.
// `payer = user` specifies who pays for the account's creation.
// `space = 8 + 8` defines how much space to allocate for the account.
#[account(init, payer = user, space = 8 + 8)]
pub base_account: Account<'info, BaseAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
// This struct defines the accounts needed for the `increment` function.
#[derive(Accounts)]
pub struct Increment<'info> {
// We need mutable access to the `base_account` to change its data.
#[account(mut)]
pub base_account: Account<'info, BaseAccount>,
}
// This is the custom account struct that holds our counter data.
#[account]
pub struct BaseAccount {
pub count: u64,
}
Key Concepts in the Code
#[program]
: The Anchor attribute that marks this module as a Solana program.initialize
andincrement
: These are the two instructions (functions) our program has.#[derive(Accounts)]
: These structs define the validation logic for the accounts passed into our instructions. Anchor uses these to ensure that only the correct accounts with the right permissions are being used. This is a major security feature.#[account]
: TheBaseAccount
struct defines the structure of the data we are storing on-chain.#[account]
is an Anchor attribute that handles serialization and deserialization for us.
Step 3: Build and Deploy
Now, from your project's root directory, run the following commands:
- Build the program:
anchor build
- Start a local test validator:
solana-test-validator
- Deploy the program (in a new terminal window):
anchor deploy
Anchor will deploy your program to your local test network and update the program ID in your declare_id!
macro and Anchor.toml
.
Step 4: Write a Test (tests/my_first_dapp.ts
)
Anchor generates a test file for you. Let's modify it to test our counter program.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MyFirstDapp } from "../target/types/my_first_dapp";
import { assert } from "chai";
describe("my_first_dapp", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.MyFirstDapp as Program<MyFirstDapp>;
// We need a keypair for our data account.
const baseAccount = anchor.web3.Keypair.generate();
it("Is initialized!", async () => {
// Call the initialize function.
await program.methods.initialize()
.accounts({
baseAccount: baseAccount.publicKey,
user: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([baseAccount]) // We need to sign with the new account's keypair.
.rpc();
// Fetch the account and check if the count is 0.
const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
assert.ok(account.count.toNumber() === 0);
});
it("Increments the count", async () => {
// Call the increment function.
await program.methods.increment()
.accounts({
baseAccount: base.publicKey,
})
.rpc();
// Fetch the account and check if the count is 1.
const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
assert.ok(account.count.toNumber() === 1);
});
});
To run the test, use the command anchor test
.
This simple example covers the fundamental workflow of building on Solana with Anchor: defining your program's instructions, specifying the required accounts and their constraints, and then testing the interactions from a client. While the learning curve for Rust and the Solana account model can be steep, frameworks like Anchor provide a powerful and much more accessible entry point into this high-performance ecosystem.