Solana Web3 Technology Stack - Developer's Guide

Introduction

In this blog, we discuss the Solana blockchain and how you can start building dapps on Solana as a developer. While writing this article, we had new developers and beginners with only a little knowledge of smart contracts and dapps in mind. We'll explore some of the high-level concepts, tools, and techniques needed for Solana development, and we'll end with building a small dapp. If this excites you, join in and enjoy!

start

Solana is a high-performance blockchain that provides high throughput and very low gas costs. It achieves this through its proof-of-history mechanism, which is used to improve the performance of its PoS consensus mechanism.

Now, when it comes to developing on Solana, there are certain pros and cons. On the plus side, developer tools like Solana CLI, Anchor CLI and their SDKs are nice and easy to understand and implement. However, since the ecosystem and these tools are very new, the documentation is spotty and lacks the necessary explanations.

Still, Solana's developer community is very strong, and people will be keen to help another fellow developer. It is highly recommended to join the Solana and Anchor  Discord to stay up to date with the latest changes to the ecosystem. Also, if you run into any technical issues during Solana development, a great place to get your questions answered is the Solana Stack Exchange .

Solana Web3 technology stack

Solana has a very good tool ecosystem and technology stack. Let's look at the tools needed and used to develop the program:

1. Solana Toolkit

Solana Tool Suite comes with Solana CLI tool which makes the development process smooth and easy. You can perform many tasks with the CLI tool, from deploying Solana programs to transferring SPL tokens to another account.

Download the toolkit here .

2. Rust

Solana smart contracts (called Programs) can be written in the C, C++, or Rust programming languages. But my favorite is Rust.

Rust is a low-level programming language that has gained popularity due to its emphasis on performance, as well as type and memory safety.

Rust can feel a little intimidating at first, but once you start to get the hang of it, you'll love it a lot. It has a very good documentation and it also serves as a great learning resource. Some other resources on Rust include Rustlings and Rust-By-Example .

You can install Rust here .

3. Anchor

Anchor is a framework for Solana's Sealevel runtime that provides several convenient developer tools for writing smart contracts. Anchor makes our development easier by handling a lot of boilerplate code so we can focus on the important parts. It also does a lot of checks on our behalf to keep the Solana program safe.

Anchor Book is the current document of Anchor, which is a good reference value for writing Solana programs using Anchor. The Anchor SDK typedoc has all the methods, interfaces and classes you can use in the JS client. The SDK really needs better documentation.

You can install Anchor here .

4. Front-end framework

In order for your users to use the dapp, you need to have a front end that can communicate with the blockchain. You can write your client logic in any common framework (React/Vue/Angular).

If you want to build your clients with these frameworks, you need to have NodeJS installed in your system. You can install it here .

Building a Solana Dapp

Now that we have an understanding of the Solana development workflow, let's build a Solana Dapp, let's start by building a simple counter application!

set environment

Before building the dapp, we need to make sure that the tools we need have been successfully installed. Requires rust, anchor and solana to be installed on your system.

NOTE: If you are on windows, you will need a WSL terminal to run Solana. Solana doesn't work well with Powershell.

Open your terminal and run these commands:

$ rustc --version
rustc 1.63.0-nightly

$ anchor --version
anchor-cli 0.25.0

$ solana --version
solana-cli 1.10.28

If you got the correct version, it means the tools are installed correctly.

Now run this command:

$ solana-test-validator

Ledger location: test-ledger 
Log: test-ledger/validator.log 

Initializing... 

Version: 1.10.28 
Shred Version: 483 
Gossip Address: 127.0.0.1:1024 
TPU Address: 127.0.0.1:1027 
JSON RPC URL: http://127.0.0.1:8899 
00:00:16 | Processed Slot: 12 | Confirmed Slot: 12 | Finalized Slot: 0 | Full Snapshot Slot: - | Incremental Snapshot Slot: - | Transactions: 11 | ◎499.999945000

If your terminal output looks like this, it means the test validator has successfully run on your system and you are ready to start building!

Now, if something goes wrong, you don't have to panic. Just take a step back and reinstall.

Overview of Counter Programs

Before writing the code, let's take a step back and discuss what functionality our counter program needs. There should be a function to initialize the counter, a function to increment it , and another function to decrement it .

The first thing you should know by now is that Solana programs don't store state, to store the state of a program, you need to initialize something called an **account**. There are basically three types of accounts:

  1. Program account : An account that stores executable code, where contracts are deployed.
  2. Storage Accounts : Accounts that store data, typically, they store the state of a program.
  3. Token Accounts : Accounts where balances of different SPL tokens are stored, and where tokens are transferred.

In the counter program we are building, our executable code will be stored in the program account , and our counter data will be stored in the storage account .

I hope you get it, if not, don't worry. It will eventually become intuitive. Alright, let's move on!

Create a counter program

Let's finally start building the program! Open a terminal and run:

$ anchor init counter

This will initialize a template program with several files. Here are the important files:

At the root of your project, you'll find the file Anchor.toml , which will contain the program's workspace-wide configuration.

The file programs/counter/src/lib.rs will contain the source code for the counter program. This is where most of the logic will go, there is already some example code in there.

The file programs/counter/Cargo.toml will contain the counter program's package/ , lib/ , ***features/ and dependencies/*** information.

Last but not least, under the tests directory, there are all the tests required by the program. Testing is critical to the development of smart contracts, as we cannot afford to have bugs in them.

Now, let's run anchor build to build the program that includes the counter. It will create an IDL (Interface Description Language) under *./target/idl/counter.json* . IDL provides us with an interface that any client can interact with after our program is deployed on the chain.

$ anchor build

Running anchor build will display a warning, but you can ignore it for now.

Now open lib.rs and delete some example code so it looks like this:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;
}

Let's take a look at what's in here. In use anchor_lang::prelude::*; one line, what we're doing is importing  everything in the prelude module under anchor_lang . In any program you write using anchor-lang, you need to have this line.

Next, declare_id!(Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS);let's have a unique ID for the Solana program. The text Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS is the default, we will change it later.

**#[program]** is an attribute used to define a module. The module contains all instruction handlers (handlers, that is, the functions we write), and they define all the entries into the Solana program.

Great, now that we understand what it all is, let's write the account that will go into the transaction order, lib.rs should look like this:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;
}

// --new change--
#[account]
pub struct BaseAccount {
    pub count: u64,
}

#[account] is an attribute of a data structure representing a Solana account. Here a struct called BaseAccount is created that stores the count state as a 64-bit unsigned integer. This is where the count will be stored. BaseAccount is essentially our storage account .

Great! Now let's look at the transaction instruction to initialize the BaseAccount.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;
}

// --new change--
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 16)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct BaseAccount {
    pub count: u64,
}

Here, we create a structure called Initialize , where we declare all the accounts needed for this transaction, let's take a look at them one by one.

  1. base_account : To initialize base_account, we need this account in our directive (obviously). In the account attribute, we pass in 3 parameters. init declares that we are initializing the account. One thing that might come to mind now is how do we pass baseAccount in the directive if it hasn't been initialized yet . The reason we can do this is, as we will see later when writing tests, we will create and pass a key pair for the baseAccount . Only after the command happens successfully, the baseAccount account will be created on the Solana chain for the key pair we created. payer declares the user who will pay to create the account. It is important to note here that storing data on-chain is not free. It costs SOL. In this case, the user account will pay rent to initialize the base_account . space indicates the amount of space we need to give the account. 8 bytes for a unique discriminator** and 16 bytes for count data.
  2. User : user is the authorizer who has the authority to sign the transaction that initializes the base_account .
  3. system_programsystem_program is a local program on Solana that is responsible for creating accounts, storing data on accounts, and assigning ownership of accounts to connected programs. Whenever we want to initialize an account, we need to pass it in the transaction instruction.

Great! Now let's write the handler function, which will initialize base_account:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    // --new change--
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 16)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct BaseAccount {
    pub count: u64,
}

In the counter module, an initialize function is written that takes the Initialize account structure as the context. In our function, all we do is, get a mutable reference to base_account and set the count of base_account to 0, simple as that.

very good! We have successfully written the logic to initialize the base_account on-chain , which will store the count count.

counter increment

Let's add logic to increment the counter, and a transaction order structure to do the incrementing.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 16)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

// --new change--
#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

#[account]
pub struct BaseAccount {
    pub count: u64,
}

The only account in our transaction order that needs a counter to increment is the base_account .

Let's add the increment handler:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }

    // --new change--
    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count += 1;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 16)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

#[account]
pub struct BaseAccount {
    pub count: u64,
}

All we're doing here is taking a mutable reference to base_account and incrementing it by 1. Simple enough!

Great! We now have logic to increment the counter.

counter decrement

This code will be very similar to the code that increments the counter:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count += 1;
        Ok(())
    }

    // --new change--
    pub fn decrement(ctx: Context<Decrement>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count -= 1;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 16)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

// --new change--
#[derive(Accounts)]
pub struct Decrement<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

#[account]
pub struct BaseAccount {
    pub count: u64,
}

As a final step, let's build again to check that our code compiles without errors.

$ anchor build

Great! We now have a smart contract for our counter program!

test our program

It is critical to properly test our smart contracts so that programs are less prone to bugs. For the counter program, we will implement a basic test to check that the handler is working correctly.

Go to tests/counter.ts and go. We'll put all our tests here. Modify the test file this way:

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();
}

The code is generating a new key pair for baseAccount , which we will use in our tests.

Test initialization counter

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();

    // -- new changes --
    it("initializes the counter", async () => {
        await program.methods
            .initialize()
            .accounts({
                baseAccount: baseAccount.publicKey,
                user: provider.wallet.publicKey,
                systemProgram: anchor.web3.SystemProgram.programId,
            })
            .signers([baseAccount])
            .rpc();

        const createdCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(createdCounter.count.toNumber(), 0);
    });
});

We call program.methods.initialize() and pass in the account required by the command. Now the thing to note here is that in the object we pass in the account, we use baseAccount and systemProgram as fields, even though we defined them as base_account and system_program in the rust transaction directive .

This is because anchor allows us to follow the naming rules of the respective languages, camelCase for typescript and snake_case for rust .

Then, we pass in the array of signers for the transaction, which is the account we pass in and the user who created it. But you'll see that we didn't include provider.wallet in the signers array . This is because signer includes provider.wallet in the array as the default signer, so there is no need to pass it explicitly. If we create a separate key pair for a user, we need to pass it in this array.

After the RPC call is done, we try to get the created baseAccount using the created publicKey . Afterwards, we assert that the count in the retrieved baseAccount is 0.

If the test passes, we know that all is well. First, we need to set Solana's configuration to use localhost. Open a terminal and run this command:

$ solana config set --url localhost

should display:

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: /home/swarnab/.config/solana/id.json
Commitment: confirmed

Now, let's test the code:

$ anchor test

This should be the output of a passing test

Great! This means the counter was initialized successfully.

The test counter is incremented

Let's go straight to the code:

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();

    it("initializes the counter", async () => {
        await program.methods
            .initialize()
            .accounts({
                baseAccount: baseAccount.publicKey,
                user: provider.wallet.publicKey,
                systemProgram: anchor.web3.SystemProgram.programId,
            })
            .signers([baseAccount])
            .rpc();

        const createdCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(createdCounter.count.toNumber(), 0);
    });

    // -- new changes --
    it("increments the counter", async () => {
        await program.methods
            .increment()
            .accounts({ baseAccount: baseAccount.publicKey })
            .signers([])
            .rpc();

        const incrementedCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(incrementedCounter.count.toNumber(), 1);
    });
});

All it does here is make an rpc call to increment and check if count is 1 after the call happens .

Let's test this program:

$ anchor test

It should show that our two tests passed

Great! Now know that the increment logic is working too.

Decrement the test counter

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();

    it("initializes the counter", async () => {
        await program.methods
            .initialize()
            .accounts({
                baseAccount: baseAccount.publicKey,
                user: provider.wallet.publicKey,
                systemProgram: anchor.web3.SystemProgram.programId,
            })
            .signers([baseAccount])
            .rpc();

        const createdCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(createdCounter.count.toNumber(), 0);
    });

    it("increments the counter", async () => {
        await program.methods
            .increment()
            .accounts({ baseAccount: baseAccount.publicKey })
            .signers([])
            .rpc();

        const incrementedCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(incrementedCounter.count.toNumber(), 1);
    });

    // -- new changes --
    it("decrements the counter", async () => {
        await program.methods
            .decrement()
            .accounts({ baseAccount: baseAccount.publicKey })
            .signers([])
            .rpc();

        const decrementedCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(decrementedCounter.count.toNumber(), 0);
    });
});

This is very similar to incrementing a counter. Finally, check if the number is 0.

$ anchor test

It should show that all three of our tests passed

That's all!

That brings our work of building and testing our own smart contracts on Solana to an end! As a final step, let's deploy our counter program to the Solana Devnet.

Deploy the counter program

Deploy the counter program, but first we need to change a few things. Open a terminal and run this command:

$ anchor keys list

counter: 3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ

In my case, 3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ will be my program's unique ID.

Go to lib.rs and modify the following line:

declare_id!("3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ");

Another change is in Anchor.toml .

[features]
seeds = false
skip-lint = false

[programs.localnet]
counter = "3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ"
[programs.devnet]
counter = "3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "/home/swarnab/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

We added [programs.localnet]** under [programs.devnet] and changed the counters in both places.

Another change we need to make is under **[provider] . clusterWill change from localnet to devnet**.

Great! Now we have to rebuild, a very important step:

$ anchor build

Now we need to change the configuration of solana to devnet . Run the command:

$ solana config set --url devnet

should display:

Config File: ~/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /home/swarnab/.config/solana/id.json
Commitment: confirmed

Now let's deploy the program:

$ anchor deploy

successful deployment

If you get a prompt that the deployment was successful , it means that your program was deployed successfully.

Go to the block chain browser to query with the program ID. Make sure clusterit is set to devnet . The program ID is obtained by running anchor keys list .

Results of our program displayed on a blockchain explorer

some extra techniques

There are some additional tools that you can use in your Solana dapps.

Arweave

Arweave is a community-owned, decentralized, persistent data storage protocol, official website here .

Metaplex

Metaplex is an NFT ecosystem built on top of the Solana blockchain. The protocol enables artists and creators to launch a self-hosted NFT marketplace as easily as building a website. The Metaplex NFT standard is the most used NFT standard in the Solana ecosystem. Please check them out here .

epilogue

We have come to the end of this tutorial. Hope you enjoyed it and learned something in this article.

The point I'm trying to make is that Solana development might feel a bit overwhelming at first, but if you stick with it, you'll start to appreciate the magic of the Solana ecosystem.

Make yourself aware of what's going on, and try contributing to the open-source Solana project.

If you get stuck somewhere, don't forget to visit the Solana Stack Exchange .

Good luck with your Solana development journey!

Guess you like

Origin blog.csdn.net/smartContractXH/article/details/126821722