A Beginner’s Guide to the Solana Web3 Stack
Published: 2023-06-19 09:55:47

## Introduction

In this blog, we will talk about the Solana Blockchain, and how you as a developer can get started with building your dapps on Solana. This article has been written keeping new developers and beginners in mind, who have some general knowledge about smart contracts and dapps. We will look at some high-level concepts, tools, and technologies you need for Solana development, and at the end, we will build a small dapp. If that gets you excited, hop in and enjoy the ride!

## Getting Started

Solana is a high-performance blockchain that offers a high throughput and very low gas fees. It achieves this through its Proof-of-History mechanism, which is used to improve the performance of its Proof-of-Stake consensus mechanism.

Now, talking about development on Solana, there are certain pros and cons. The pros are that the developer tools like the Solana CLI, the Anchor CLI, and their SDKs are great, and are easy to understand and implement. But, since the ecosystem and such tools are very new, the documentations are not perfect and lack necessary explanations.

That being said, the developer community in Solana is very strong and people will be eager to help out another fellow developer. I strongly recommend joining the [Solana](https://discord.gg/solana) discord server and the [Anchor](https://discord.gg/k9BmrUjHpr) discord server to be updated about the latest changes in the ecosystem. Also, if you are stuck in your Solana dev journey with any technical questions, a great place to get your problems resolved is the newly formed [Solana Stack Exchange](https://solana.stackexchange.com/).

## The Solana Web3 Tech Stack

Solana has a very nice tooling ecosystem and tech stack. Let's check out the tools we will need and use for our program.

### 1. Solana Tool Suite

The [Solana Tool Suite](https://docs.solana.com/cli/install-solana-cli-tools) comes with the Solana CLI Tools which makes the process of development smooth and easy. You can perform a lot of tasks with the CLI Tools ranging from deploying Solana Programs to transferring SPL Tokens to another account.

Download the tool suite [here](https://docs.solana.com/cli/install-solana-cli-tools).

### 2. Rust

Solana Smart Contracts (called Programs) can be written in C, C++ or Rust programming languages. But the most preferred one is Rust.

[Rust](https://www.rust-lang.org/) is a low-level programming language which has gained a lot of popularity due to its emphasis on performance, and type and memory safety.

Rust can feel a bit intimidating at first but once you start getting the hang of it, you will enjoy it a lot. It has a very well articulated [documentation](https://doc.rust-lang.org/book/), which can be used as a good learning resource too. Some other resources for Rust include [Rustlings](https://github.com/rust-lang/rustlings/) and [Rust-By-Example](https://doc.rust-lang.org/stable/rust-by-example/).

You can install Rust [here](https://www.rust-lang.org/tools/install).

### 3. Anchor

[Anchor](https://www.anchor-lang.com/) is a framework for Solana's Sealevel runtime providing several convenient developer tools for writing smart contracts.
Anchor makes our lives a lot easier by taking care of a lot of boilerplate code so that we can focus on the important bits. It also does a lot of checks on our behalf so that our Solana programs remain secure.

[Anchor Book](https://book.anchor-lang.com/), which is Anchor's current documentation, has good references for writing Solana programs using Anchor. The [Anchor SDK typedoc](https://coral-xyz.github.io/anchor/ts/index.html) has all the methods, interfaces, and classes you can use in your JS client. The SDK does need better documentation.

You can install Anchor [here](https://www.anchor-lang.com/docs/installation).

### 4. A Frontend Framework

For your users to use your dapp, you need to have a frontend client which can communicate with the blockchain. You can write your client-side logic with any of the common frameworks (React / Vue / Angular).

You need to have NodeJS installed in your system if you want to build your client using these frameworks. You can install it [here](https://nodejs.org/en/download/).

## Buidling a Solana Dapp

Now since we have an overview of the Solana development workflow, let's build a Solana dapp. We are going to build a simple counter application. Let's dive in!

### Setting Up The Environment

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

**Note:** If you are on windows, you will need a WSL Terminal to run Solana. Solana does not work well with Powershell.

Open your terminal and run these commands.

```bash
$ rustc --version

rustc 1.63.0-nightly
```

```bash
$ anchor --version

anchor-cli 0.25.0
```

```bash
$ solana --version

solana-cli 1.10.28
```

If you got the versions correctly, that means the tools were installed correctly.

Now run this command.

```bash
$ 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 you got this as the terminal output, that means a test validator is running successfully on your system and you are ready to start buidling!

Now, if something went wrong you need not panic. Just take a step back and go through the installation process once again.

### Overview of our Counter Program

Before writing code, let's take a step back and discuss what features we need for our counter program. There should be one function that **initializes** the counter. There should be one function that **increments** the counter. And there should be another function which **decrements** the counter.

Now the first thing you should know 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 essentially three types of accounts.

1. **Program Accounts:** Accounts that store executable code. These are where your contracts are deployed.

2. **Storage Accounts:** Accounts that store data. Typically, they store the state of a program.

3. **Token Accounts:** Accounts that store the balance of different SPL tokens and from where tokens are transferred.

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

I hope that made sense, don't worry if it did not. It will eventually be intuitive. Okay, let's move on!

### Buidling our Counter Program

Let's finally start building our program! Open up the terminal and run

```bash
$ anchor init counter
```

This will initialize a template program with a few files. Let's talk about the important ones here.

In the root of your project directory, you will find the file `Anchor.toml`. It will contain configurations for the workspace-wide settings for our programs.

The file `programs/counter/src/lib.rs` will contain our source code for our counter program. This is where most of the logic will go. It will already have some sample code in place.

The file `programs/counter/Cargo.toml` will contain the information about `package`, `lib`, `features` and `dependencies` for our counter program.

And last but not the least, under the `tests` directory, we will have all the tests necessary for our programs. Testing is very crucial for smart contract development because we cannot afford to have exploits in it.

Now, let's run `anchor build`. This will build our workspace containing the counter program. It will create an `IDL (Interface Description Language)` for us under `./target/idl/counter.json`. An IDL gives us an interface for any client to interact with our program after it has been deployed on chain.

```bash
$ anchor build
```

Running `anchor build` will show a warning, but you can ignore that for now.

Now open up `lib.rs` and delete some sample code so that it looks like this. We will start from a clean slate.

```rust
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

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

Next, `declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");` lets us have the unique id for our Solana program. The text `Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS` is default, and we will be changing that in a bit.

`#[program]` is an attribute that defines the module containing all instruction handlers (the functions we write) defining all entries into a Solana program.

Great, now that we understand what all that is, let's write the account that will go inside our transaction instructions. Our `lib.rs` should look like this.

```rust
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 for a data structure representing a Solana account. We create a struct called `BaseAccount` which stores the `count` state as a 64-bit unsigned integer. This is where our count will be stored. `BaseAccount` is essentially our `storage account`.

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

```rust
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 are creating a struct called `Initialize` where we declare all the accounts we need for this transaction. Let's look at them one by one.

1. **base_account**: To initialize the `base_account`, we need to have the account in our instruction (obviously). In the `account` attribute, we pass in 3 arguments. 
`init` declares that we are initializing the account. Now one thing that might come to mind is how will we pass `baseAccount` in the instruction if it has not been even initialized. The reason we can do so is, and we will see it while writing tests too, we will be creating and passing just a keypair for `baseAccount`. Only after the instruction happens successfully, the `baseAccount` account will be created on the Solana chain for the keypair we created. Hope that makes sense.
`payer` declares the user who will pay to create the account. Now, one thing to note here is, that storing data on-chain is not free. It costs SOL. In this case, the `user` account will pay the `rent` to initialize `base_account`.
`space` denotes the amount of space we need to give to the account. 8 bytes for a unique discriminator, and 16 bytes for count data.

2. **user**: `user` is the authority that will `sign` the transaction to initialize `base_account`.

3. **system_program**: `system_program` is a native program on Solana that is responsible for creating accounts, allocating data on accounts, and assigning ownership of accounts to the connected programs. We need to pass it in the transaction instruction whenever we want to initialize an account.

Great! Now let's write our handler function which will initialize our `base_account`

```rust
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,
}
```

Inside our `counter` module, we write the `initialize` function which takes the `Initialize` account struct as the context. Inside our function, all we do is, take a mutable reference of `base_account`, and set the count of `base_account` to 0. As simple as that.

Great! We have successfully written the logic for initializing a `base_account` on-chain which will store the `count`.

### Incrementing the Counter

Let's add the logic to increment our counter! Add the transaction instruction struct for increment.

```rust
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 we need in our transaction instruction for increasing our counter is `base_account`.

Let's add the `increment` handler function.

```rust
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 do here is take a mutable reference of `base_account` and increment it by one. Simple enough!

Great! We now have logic to increment the counter.

### Decrementing the Counter

The code will be very similar to incrementing the counter.

```rust
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 last step, let's build the workspace once again to check that our code compiles without errors.

```bash
$ anchor build
```

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

## Testing our Programs

It is very crucial to test our smart contracts properly so that there are no exploits in our programs. For our counter program, we will implement basic tests to check if the handlers work properly.

Great! Head over to `tests/counter.ts`. We will have all our tests here. Modify the test file in this way.

```ts
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();
}
```

We are generating a new Keypair for `baseAccount` which we will be using for our tests.

### Test to Initialize the Counter

```ts
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 make a call to `program.methods.initialize()` and pass in the accounts that are necessary for the instruction. Now one thing to note here is, that in the object we passed into accounts, we are using `baseAccount` and `systemProgram` as fields, although we defined them as `base_account` and `system_program` in the transaction instruction in rust. 
This is because anchor allows us to follow the naming convention of the respective languages, which is `camelCase` for typescript and `snake_case` for rust.

Then, we pass in the signers' array for the transaction, which would be the account we passed and the user who creates the account. But you would see we haven't added `provider.wallet` in the signers' array. 
This is because signer adds `provider.wallet` as a default signer in the array and we don't need to pass it explicitly. Had we created a keypair separately for a user, we would have to pass it in this array. Hope that made sense.

After the RPC call has been made, we try to fetch the created `baseAccount` using the publicKey we created. After that, we just assert that the count inside the fetched `baseAccount` is 0.

If the test passes, we know that everything went perfectly. First, we need to set the Solana config to use localhost. Spin up the terminal and run the command.

```bash
$ solana config set --url localhost
```

This should show

```bash
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 our code.

```bash
$ anchor test
```

This should give a passing test as an output

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/giu39d19k6y7gj06g5p4.png)
 

Great! This means that our counter can be initialized successfully.

### Test to Increment the Counter

Let's see the code. It's pretty straightforward here.

```ts
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 we do here is make an rpc call to our `increment` and after it happens we check that `count` is 1.

Let's test the program.

```bash
$ anchor test
```

It should output

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bv4d4mb8j7qx66c8k1bt.png)
 
Great! Now we know our increment logic works as well.

### Test to Decrement the Counter

```ts
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 the counter. In the end we check in the `count` is 0.

```bash
$ anchor test
```

This should output

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vs36zrkyklugfq7k4dng.png) 

That's it! 

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

## Deploying our Counter Program

Let's deploy our program, but first we need to change some stuff. Spin up the terminal and run this command.

```bash
$ anchor keys list

counter: 3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ
```

In my case `3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ` will be the unique id to my program.

Head over to `lib.rs` and change the following line.

```rust
declare_id!("3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ");
```

Another change would be in Anchor.toml

```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.devnet]` under `[programs.localnet]` and changed the `counter` in both of them.
Another change we need to do is under `[provider]`. Change `cluster` from `localnet` to `devnet`.

Great! Now we have to build the workspace. This is a *very* important step.

```bash
$ anchor build
```

Now we need to change the solana config to `devnet`. Run the command

```bash
$ solana config set --url devnet
```

This should show

```bash
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

```bash
$ anchor deploy
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a3r17plc1ahgr9et8ecm.png)

If you get `Deploy success`, that means your program deployed successfully.

Awesome! Now let's go to the [explorer](https://explorer.solana.com/) and check with the program id. Make sure the cluster is set to devnet. You will get the id by running `anchor keys list`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ltuwazwyi4wcfjjh2a92.png) 

Our program shows up on the explorer.

## Bonus: Some Additional Technologies

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

### Arweave

Arweave is a community owned, decentralized and permanent data storage protocol. You can check it out [here](https://www.arweave.org/).

### Metaplex

Metaplex is an NFT ecosystem built on top of the Solana blockchain. The protocol enables artists and creators to launch self-hosted NFT storefronts as easily as building a website. The Metaplex NFT Standard is the most used NFT Standard in the Solana ecosystem. Check them out [here](https://www.metaplex.com/).

### Conclusion

That brings us to the end of this tutorial. Hope you enjoyed it a lot and learned something along the way!

One thing I would like to say is that Solana development might feel a bit intimidating at first, but if you stay consistent, you will start to appreciate the beauty of the Solana ecosystem.

Just keep yourself updated with what's happening, be active on Twitter, and try to contribute to open-source Solana projects.

And if you are stuck somewhere, don't forget to visit the [Solana Stack Exchange](https://solana.stackexchange.com/).

That's it from my side! Have a great Solana dev journey out there!