DEV Community

Cover image for Soon 101: Overview of building program on soon
Shivam Soni
Shivam Soni

Posted on • Edited on

Soon 101: Overview of building program on soon

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

Change into the project directory:

cd soon-counter
npm i
npm run dev
Enter fullscreen mode Exit fullscreen mode

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::*;
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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 **/
Enter fullscreen mode Exit fullscreen mode

Next, go to the CLI, navigate to the anchor directory, and run:

anchor build
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Make sure everything is configured correctly. To check this, run:

solana config get
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Finally, check your Soon Devnet SOL balance by running:

solana balance 
Enter fullscreen mode Exit fullscreen mode

Next, go to the CLI, navigate to the anchor directory, and run:

anchor deploy --provider.cluster https://rpc.devnet.soo.network/rpc 
Enter fullscreen mode Exit fullscreen mode

After this, you will receive your deployed program ID.

Image description

Next, test your program by running:

anchor test
Enter fullscreen mode Exit fullscreen mode

Image description

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
]
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Resources

Github: soon-counter-github
Vercel: soon-counter-live

Top comments (0)