Soon 101: Overview of building program on soon
Shivam Soni
Posted on November 15, 2024
What Is SOON
SOON is the most efficient roll-up stack delivering top performance to every L1, powered by Decoupled SVM.
SOON Stack
SOON Stack is the collection of components that allows for the deployment and running of an SVM Layer 2 on top of any base Layer 1. Chains deployed using the SOON Stack are referred to as SOON Chains.
Decoupled SVM
Decoupled SVM refers to the separation of the Solana Virtual Machine (SVM) from Solana's native consensus layer, turning the SVM into an independent execution layer. This allows SVM to be deployed across different Layer 1 ecosystems, providing enhanced performance and security for rollup architectures.
What This Guide Will Teach You
This guide will teach you how to create and deploy a Solana program on SOON Devnet and connect it to a UI for a basic on-chain counter d-App.
What You Will Learn
- Setting up your development environment
- Using npx create-solana-dApp
- Anchor program development
- How to create and store data in a solana account
- How to update the data of an account
- Deploying a Solana program to Soon Devnet
- Connecting counter on-chain program to a React UI
Prerequisites
For this guide, you will need to have your local development environment setup with a few tools:
For more information about the setup, you can explore this setup.
What We Are Building
We are developing a counter program.
In this d-App, Solana accounts will be utilized to:
- Create a counter account.
- Update the counter account by incrementing and decrementing the counter’s count.
Setting Up The Project
Please ensure all necessary development environments and dependencies are properly installed and configured.
This project uses npx create-solana-dapp
to create a quick Solana dApp. You can find the source code here.
First, clone the soon-counter example repository to your local machine.
git clone git@github.com:shivamSspirit/soon-counter.git
Change into the project directory:
cd soon-counter
npm i
npm run dev
Anchor Program Development
If you're new to Anchor, The Anchor Book and Anchor Examples are great references to help you learn.
In the project folder, navigate to soon-counter/anchor/programs/counter/src/lib.rs
. Let's delete it and start from scratch to walk through each step.
use anchor_lang::prelude::*;
declare_id!("EBgotvhJTqX98LR7moF9V85dgnbQYedmCyY2nsXqTaCV");
#[program]
pub mod soo_counter {
use super::*;
}
Counter Account State
use anchor_lang::prelude::*;
declare_id!("EBgotvhJTqX98LR7moF9V85dgnbQYedmCyY2nsXqTaCV");
#[program]
pub mod soo_counter {
use super::*;
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
}
In the above code, we are creating an account struct that will be used to keep track of our counter’s count, which is of type u64
.
The #[derive(InitSpace)]
attribute will calculate the initial space for the counter account struct. space is used to calculate how many bytes our account will need, You can learn more about account space here.
Create Counter Context
use anchor_lang::prelude::*;
declare_id!("EBgotvhJTqX98LR7moF9V85dgnbQYedmCyY2nsXqTaCV");
#[program]
pub mod soo_counter {
use super::*;
}
#[derive(Accounts)]
pub struct InitializeCounter<'info> {
#[account(
init,
payer = signer,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
}
In the code above, we are creating a counter account context of type Counter. This context specifies to our program which accounts are passed when we send a transaction from the client side. The #[derive(Accounts)]
attribute handles many abstractions for us, such as serialization and deserialization of account data, allowing us to focus on the core logic.
To initialize the counter account, we need three account keys: the counter account, the signer account, and the system program.
To create the counter account, we use Anchor constraints such as init
, payer
, and space
:
-
init
: This constraint creates the counter account via a CPI to the system program and initializes it. -
payer
: This Specifies the payer responsible for covering the rent associated with the counter account. -
space
: Allocates the required space for the counter account.
The second account is the signer account, which will sign the transaction to create the counter account.
The third is the system program, which will be the owner of our counter account and facilitate the account creation.
Initialize Counter Instruction In The Program Module
The program module is where you define your business logic. You do so by writing functions which can be called by clients or other programs.
use anchor_lang::prelude::*;
declare_id!("EBgotvhJTqX98LR7moF9V85dgnbQYedmCyY2nsXqTaCV");
#[program]
pub mod soo_counter {
use super::*;
pub fn initialize(ctx: Context<InitializeCounter>) -> Result<()> {
let counter_account = &mut ctx.accounts.counter;
counter_account.count = 0;
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeCounter<'info> {
#[account(
init,
payer = signer,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
}
In the updated code above,
We define an initialize
function and set the InitializeCounter
context as the function argument.
Since the context contains the program ID, accounts, and other elements that we will explore in future guides, we fetch the counter account from the context and set the counter’s count
to zero as its initial value.
Now that our counter account is initialized—great job, everyone!—we can move on to the increment and decrement operations.
Increment Counter
use anchor_lang::prelude::*;
declare_id!("EBgotvhJTqX98LR7moF9V85dgnbQYedmCyY2nsXqTaCV");
#[program]
pub mod soo_counter {
use super::*;
pub fn initialize(ctx: Context<InitializeCounter>) -> Result<()> {
let counter_account = &mut ctx.accounts.counter;
counter_account.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
ctx.accounts.counter.count = ctx.accounts.counter.count.checked_add(1).unwrap();
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeCounter<'info> {
#[account(
init,
payer = signer,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
}
In the updated code above,
We first create an Increment
context struct and mark the counter account as mutable, which ensures that the account can be updated to modify the counter’s count.
Next, in the program module, we create an increment
function that takes the Increment
context as an argument, fetches the counter account from the context, and increases the count by one.
Decrement Counter
use anchor_lang::prelude::*;
declare_id!("EBgotvhJTqX98LR7moF9V85dgnbQYedmCyY2nsXqTaCV");
#[program]
pub mod soo_counter {
use super::*;
pub fn initialize(ctx: Context<InitializeCounter>) -> Result<()> {
let counter_account = &mut ctx.accounts.counter;
counter_account.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
ctx.accounts.counter.count = ctx.accounts.counter.count.checked_add(1).unwrap();
Ok(())
}
pub fn decrement(ctx: Context<Decrement>) -> Result<()> {
ctx.accounts.counter.count = ctx.accounts.counter.count.checked_sub(1).unwrap();
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeCounter<'info> {
#[account(
init,
payer = signer,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[derive(Accounts)]
pub struct Decrement<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
}
In the updated code above,
We first create a Decrement
context struct and add the counter account to update the counter’s count
.
Next, in the program module, we create a decrement
function that takes the Decrement
context as an argument, fetches the counter account from the context, and decreases the count by one.
Wohlaa! We’re all set with the counter operations, including initialize, increment, and decrement. Now, we can build this program and deploy it to Devnet.
Build, Deploy and Test program
In your project folder, open soon-counter/anchor/Anchor.toml
and check the cluster URL. It should look like this, as we are deploying the program to Soon Devnet.
[provider]
cluster = "https://rpc.devnet.soo.network/rpc"
wallet = "~/.config/solana/id.json" /** add your wallet path here **/
Next, go to the CLI, navigate to the anchor directory, and run:
anchor build
This will generate the program ID, IDL, and program types needed for client-side interaction.
To deploy the program, you'll need access to the Soon Solana faucet. You can request it in Discord here or go to our faucet to get them. https://faucet.soo.network/
After receiving faucet access, configure the Solana CLI to use the SOON Devnet RPC endpoint.
To do this, run:
solana config set --url https://rpc.devnet.soo.network/rpc
Make sure everything is configured correctly. To check this, run:
solana config get
This command generates output like this:
Config File: ~/.config/solana/cli/config.yml
RPC URL: https://rpc.devnet.soo.network/rpc
WebSocket URL: wss://rpc.devnet.soo.network/rpc (computed)
Keypair Path: ~/.config/solana/id.json
Commitment: confirmed
Finally, check your Soon Devnet SOL balance by running:
solana balance
Next, go to the CLI, navigate to the anchor directory, and run:
anchor deploy --provider.cluster https://rpc.devnet.soo.network/rpc
After this, you will receive your deployed program ID.
Next, test your program by running:
anchor test
Connect program to UI
Create-solana-dapp already sets up a UI with data access and a wallet connector for you. All you need to do is simply modify it to fit your newly created program.
Since this counter program has three instructions, we will need components in the UI that will be able to call each of these instructions:
- Initialize counter
- Increment counter
- Decrement counter
In your project folder open soon-counter/anchor/src/counter-exports.ts
// Here we export some useful types and functions for interacting with the Anchor program.
import { AnchorProvider, Program } from '@coral-xyz/anchor'
import { Cluster, PublicKey } from '@solana/web3.js'
import SoocounterIDL from '../target/idl/soo_counter.json'
import type { SooCounter } from '../target/types/soo_counter'
// Re-export the generated IDL and type
export { SooCounter, SoocounterIDL }
// The programId is imported from the program IDL.
export const SOONCOUNTER_PROGRAM_ID = new PublicKey(SoocounterIDL.address)
// This is a helper function to get the Soonsoonsooncounter Anchor program.
export function getSoocounterProgram(provider: AnchorProvider) {
return new Program(SoocounterIDL as SooCounter, provider)
}
// This is a helper function to get the program ID for the Soocounter program depending on the cluster.
export function getSoocounterProgramId(cluster: Cluster) {
switch (cluster) {
case 'devnet':
case 'testnet':
// This is the program ID for the Soocounter program on devnet and testnet.
return new PublicKey('Ho4gWX427c2qWdy1ZrQ97qA5B8eeSe86okrxJ1nMxvkR')
case 'mainnet-beta':
default:
return SOONCOUNTER_PROGRAM_ID
}
}
In the above code, we are importing IDL and Program types from the generated target folder. and re-exporting IDL, types, program ID and Program API.
Next, update your cluster to the Soon Devnet network. To do this, go to soon-counter/src/components/cluster/cluster-data-access.tsx
and update the custom cluster to the Soon Devnet RPC endpoint.
export const defaultClusters: Cluster[] = [
{
name: 'devnet',
endpoint: clusterApiUrl('devnet'),
network: ClusterNetwork.Devnet,
},
{ name: 'local', endpoint: 'http://localhost:8899' },
{
name: 'testnet',
endpoint: clusterApiUrl('testnet'),
network: ClusterNetwork.Testnet,
},
{
name: 'custom',
endpoint: 'https://rpc.devnet.soo.network/rpc',
network: ClusterNetwork.Custom,
}
]
Next, move to soon-counter/src/components/counter/counter-data-access.tsx
And update useSoocounterProgram()
to initialize the counter:
export function useSoocounterProgram() {
const { connection } = useConnection()
const { cluster } = useCluster()
const transactionToast = useTransactionToast()
const provider = useAnchorProvider()
const programId = useMemo(() => getSoocounterProgramId(cluster.network as Cluster), [cluster])
const program = getSoocounterProgram(provider)
const accounts = useQuery({
queryKey: ['soocounter', 'all', { cluster }],
queryFn: () => program.account.counter.all(),
})
const getProgramAccount = useQuery({
queryKey: ['get-program-account', { cluster }],
queryFn: () => connection.getParsedAccountInfo(programId),
})
// keypair for counter account
const counter = Keypair.generate();
const initialize = useMutation({
mutationKey: ['soocounter', 'initialize', { cluster }],
mutationFn: ({ user }: { user: PublicKey }) =>
program.methods.initialize().accounts({
counter: counter.publicKey,
signer: user
}).signers([counter]).rpc(),
onSuccess: (signature) => {
transactionToast(signature)
return accounts.refetch()
},
onError: () => toast.error('Failed to initialize account'),
})
return {
program,
programId,
accounts,
getProgramAccount,
initialize,
}
}
In the above code, we call our first instruction initialize()
to initialize a counter account.
Next, update the useSoocounterProgramAccount()
function to be able to call increment and decrement instructions:
export function useSoocounterProgramAccount({ account }: { account: PublicKey }) {
const { cluster } = useCluster()
const transactionToast = useTransactionToast()
const { program, accounts } = useSoocounterProgram()
const accountQuery = useQuery({
queryKey: ['soocounter', 'fetch', { cluster, account }],
queryFn: () => program.account.counter.fetch(account),
})
const incrementMutation = useMutation({
mutationKey: ['soocounter', 'increment', { cluster, account }],
mutationFn: () => program.methods.increment().accounts({ counter: account }).rpc(),
onSuccess: (tx) => {
transactionToast(tx)
return accountQuery.refetch()
},
})
const decrementMutation = useMutation({
mutationKey: ['soocounter', 'decrement', { cluster, account }],
mutationFn: () => program.methods.decrement().accounts({ counter: account }).rpc(),
onSuccess: (tx) => {
transactionToast(tx)
return accountQuery.refetch()
},
})
return {
accountQuery,
decrementMutation,
incrementMutation,
}
}
Next update UI, for this, go into soon-counter/src/components/counter/counter-ui.tsx
and create a UI for the initialize counter button
export function SoocounterCreate() {
const { initialize } = useSoocounterProgram()
const { publicKey } = useWallet();
return (
<button
className="btn btn-xs lg:btn-md btn-primary"
onClick={() => initialize.mutateAsync({ user: publicKey! })}
disabled={initialize.isPending}
>
Create {initialize.isPending && '...'}
</button>
)
}
Next, create a UI for the list of counters that will be created.
export function SoocounterList() {
const { accounts, getProgramAccount } = useSoocounterProgram()
if (getProgramAccount.isLoading) {
return <span className="loading loading-spinner loading-lg"></span>
}
if (!getProgramAccount.data?.value) {
return (
<div className="alert alert-info flex justify-center">
<span>Program account not found. Make sure you have deployed the program and are on the correct cluster.</span>
</div>
)
}
return (
<div className={'space-y-6'}>
{accounts.isLoading ? (
<span className="loading loading-spinner loading-lg"></span>
) : accounts.data?.length ? (
<div className="grid md:grid-cols-2 gap-4">
{accounts.data?.map((account) => (
<SooncounterCard key={account.publicKey.toString()} account={account.publicKey} />
))}
</div>
) : (
<div className="text-center">
<h2 className={'text-2xl'}>No accounts</h2>
No accounts found. Create one above to get started.
</div>
)}
</div>
)
}
Finally, create a UI for the counter’s increment and decrement controls.
function SooncounterCard({ account }: { account: PublicKey }) {
const { accountQuery, incrementMutation, decrementMutation } = useSoocounterProgramAccount({
account,
})
const count = useMemo(() => accountQuery.data?.count ?? 0, [accountQuery.data?.count])
return accountQuery.isLoading ? (
<span className="loading loading-spinner loading-lg"></span>
) : (
<div className="card card-bordered border-base-300 border-4 text-neutral-content">
<div className="card-body items-center text-center">
<div className="space-y-6">
<h2 className="card-title justify-center text-3xl cursor-pointer" onClick={() => accountQuery.refetch()}>
{count.toString()}
</h2>
<div className="card-actions justify-around">
<button
className="btn btn-xs lg:btn-md btn-outline"
onClick={() => incrementMutation.mutateAsync()}
disabled={incrementMutation.isPending}
>
Increment
</button>
<button
className="btn btn-xs lg:btn-md btn-outline"
onClick={() => decrementMutation.mutateAsync()}
disabled={decrementMutation.isPending}
>
Decrement
</button>
</div>
<div className="text-center space-y-4">
<p>
<ExplorerLink path={`account/${account}`} label={ellipsify(account.toString())} />
</p>
</div>
</div>
</div>
</div>
)
}
Resources
Github: soon-counter-github
Vercel: soon-counter-live
Posted on November 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.