A Beginner’s Guide to the Solana Web3 Stack

0xcatrovacer

Swarnab Garang

Posted on June 19, 2023

A Beginner’s Guide to the Solana Web3 Stack

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 discord server and the Anchor 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.

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 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.

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 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, which can be used as a good learning resource too. Some other resources for Rust include Rustlings and Rust-By-Example.

You can install Rust here.

3. Anchor

Anchor 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, which is Anchor's current documentation, has good references for writing Solana programs using Anchor. The Anchor SDK typedoc 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.

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.

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.

$ rustc --version

rustc 1.63.0-nightly
Enter fullscreen mode Exit fullscreen mode
$ anchor --version

anchor-cli 0.25.0
Enter fullscreen mode Exit fullscreen mode
$ solana --version

solana-cli 1.10.28
Enter fullscreen mode Exit fullscreen mode

If you got the versions correctly, that means the tools were 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
Enter fullscreen mode Exit fullscreen mode

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

$ anchor init counter
Enter fullscreen mode Exit fullscreen mode

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.

$ anchor build
Enter fullscreen mode Exit fullscreen mode

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.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;
}
Enter fullscreen mode Exit fullscreen mode

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.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

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

#[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.

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

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

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

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.

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

The only account we need in our transaction instruction for increasing our counter is base_account.

Let's add the increment handler function.

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

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.

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

As a last step, let's build the workspace once again to check that our code compiles without errors.

$ anchor build
Enter fullscreen mode Exit fullscreen mode

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.

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

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

Test to Initialize the 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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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.

$ solana config set --url localhost
Enter fullscreen mode Exit fullscreen mode

This should show

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

Now, let's test our code.

$ anchor test
Enter fullscreen mode Exit fullscreen mode

This should give a passing test as an output

Image description

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.

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

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.

$ anchor test
Enter fullscreen mode Exit fullscreen mode

It should output

Image description

Great! Now we know our increment logic works as well.

Test to Decrement the 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);
    });
});
Enter fullscreen mode Exit fullscreen mode

This is very similar to incrementing the counter. In the end we check in the count is 0.

$ anchor test
Enter fullscreen mode Exit fullscreen mode

This should output

Image description

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.

$ anchor keys list

counter: 3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ
Enter fullscreen mode Exit fullscreen mode

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

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

declare_id!("3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ");
Enter fullscreen mode Exit fullscreen mode

Another change would be 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"
Enter fullscreen mode Exit fullscreen mode

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.

$ anchor build
Enter fullscreen mode Exit fullscreen mode

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

$ solana config set --url devnet
Enter fullscreen mode Exit fullscreen mode

This should show

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

Now let's deploy the program

$ anchor deploy
Enter fullscreen mode Exit fullscreen mode

Image description

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

Awesome! Now let's go to the explorer 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

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.

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.

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.

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

💖 💪 🙅 🚩
0xcatrovacer
Swarnab Garang

Posted on June 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related