DEV Community

Cover image for The ultimate Solana step-by-step guide (including programs, dapps and Rust from scratch)
merlox
merlox

Posted on

The ultimate Solana step-by-step guide (including programs, dapps and Rust from scratch)

Welcome to the most complete open guide for people that want to dive
deep into Solana.

Whether you're an Ethereum developer, a blockchain developer, a web
developer or just a curious person, this is the #1 guide to become a
Solana developer. With extremely detailed steps so there's no room for
confusion along the way.

By the end of this complete guide you'll have accomplished a working
dapp
that can be deployed to the blockchain and interact with the real
Solana blockchain for extremely low fees and fast confirmation times.

Getting into Solana development can be extremely profitable since
there's already a huge market of investors, users, defi enthusiasts, NFT
maniacs, cryptocurrency trades... A massive network of people that will
pay big money for the right products.

And it's not nearly as saturated as in Ethereum since many tools and
products haven't been developed yet in Solana for a good reason:
learning Solana and Rust is far more difficult but rewarding, plus it's
a new blockchain that continues to grow tremendously fast!

I'm telling you, you'll find Rust hard at first but you'll love it by
the end of this article!

You'll learn a ton of cool things including:

  • Writing Solana Rust programs from scratch (no experience required)
  • Testing Solana applications with the Anchor framework
  • Developing a frontend for your dapp step-by-step
  • Connecting all the popular Solana wallets to your dapp
  • Deploying your dapp so it's permanently online

I like doing things hands-on, I believe it's the best way to learn.
That's why in this guide you'll work through a dapp from an idea to its
complete deployment including all the blockchain coding and frontend
development as you learn along the way.

We're gonna build a dapp that allows users to write a collective
article.
Imagine an open blockchain book where anybody can come in and
leave their little contribution to a grand story. It could end up a
cool book or a total disaster.
In any case I'm excited to see how it
turns out!

Technically speaking, each wallet will be able to write 5 words to a
collective public article that lives permanently on the blockchain.
Kinda like a fun and open experimental game.

It's a simple dapp and has a few functions which makes it ideal for
people to start getting into Solana development.

If you're already familiar with the Solana Anchor setup already skip
directly to the section: 4. Coding the Rust program to see how
to start coding this dapp. Otherwise read from the beginning.

If you feel this guide helped you in any way, be sure to join my email
list for updated on things I'm building in crypto and NFTs here:
http://eepurl.com/dDQ2yX and subscribe to me here!

Each chapter contains a short bullet list of the things you'll learn to
get you hyped up and excited for the new knowledge you're about to
acquire.

Here's what you're gonna learn:

  1. Installing Rust, Solana, Yarn and Anchor
  2. Setting up the project from scratch
  3. Understanding the Anchor framework setup
  4. Coding the Rust program
  5. Creating the article update function
  6. Testing the Solana program
  7. Creating the decentralized application frontend
  8. Setting up the Solana wallets connection

Let's do this! The updated code is available in my github as always
public and open to everybody. If you use it just give me credit so
people know who created it. The code is here:
https://github.com/merlox/solana-world-article

1. Installing Rust, Solana, Yarn and Anchor

Start by installing all the dependencies. We're gonna use Anchor which
is a framework that will make our lives easier for developing Solana
Rust programs.

In this section you'll learn:

  • How to install rust, solana, yarn and anchor
  • The commands required to verify the successful installation
  • Explanations for the different command tools installed

First, install Rust. Rust is the programming language used for Solana
programs, also known as Smart Contracts on Ethereum. You'll need to
install it first and foremost. To install Rust do:

curl -- proto '=https' -- tlsv1.2 -sSf <https://sh.rustup.rs> | sh
Enter fullscreen mode Exit fullscreen mode

If you're in windows, install Git Bash from here:
https://git-scm.com/downloads which is a terminal that allows you to
run more unique commands not commonly available on windows, and then run
the previous Rust installation command on the Git Bash terminal.

Then run the following to add all the Rust executables to your PATH:

export PATH="$HOME/.cargo/bin:$PATH"
Enter fullscreen mode Exit fullscreen mode

Make sure the installation was successful by running:

rustup --version  
rustc --version  
cargo --version
Enter fullscreen mode Exit fullscreen mode

Rustup is the upgrade utility that allows you to keep rust updated.
You won't use it much.

Rustc is the compiler. It's awesome because it allows you to take your
program written in rust and make it executable on all operative
systems. You won't use it for Solana programs but it's excellent if
you're building any other app outside of it. Including desktop apps
for all major systems.

Cargo is the utility that allows us to install and manage
dependencies. Think of it as npm for Rust.

Next, you can continue by installing Solana itself with this command:

sh -c "$(curl -sSfL https://release.solana.com/v1.9.8/install)"
Enter fullscreen mode Exit fullscreen mode

Remember to keep an eye on this link
https://docs.solana.com/cli/install-solana-cli-tools to see the latest
version since they are constantly updating it.

After a successful installation run:

solana --version
Enter fullscreen mode Exit fullscreen mode

To confirm that it has been added.

Now you'll have to install node.js with yarn which is required to work
with Anchor programs. Go to https://nodejs.org/ and install the LTS
version.

Once the installation is completed confirm the successful installation
like this:

node --version  
npm --version
Enter fullscreen mode Exit fullscreen mode

Then install yarn, which is an improved version of npm with this
command:

npm i -g yarn
Enter fullscreen mode Exit fullscreen mode

Finally install Anchor. Anchor is a protocol that allows us to build
programs on solana much faster and easier than without it. Think of it
as hardhat or truffle from Ethereum. An essential tool for any Solana
developer.

To install Anchor run:

cargo install --git https://github.com/project-serum/anchor
anchor-cli --locked
Enter fullscreen mode Exit fullscreen mode

As you can see we're using Cargo which we installed earlier, it's very
simple just do cargo install and the git repository you wish to receive.

Confirm the installation with:

anchor --version
Enter fullscreen mode Exit fullscreen mode

That should be it for the installation of all the dependencies. Let's
move on by setting up the project so we can create the program!

2. Setting up the project from scratch

Solana is configured to work on the mainnet network by default. This
means every transaction has a real SOL coin cost as the transaction fee.
You don't want to do that when developing applications. There's a better
way.

In this section you'll learn:

  • How to configure the solana cli utility to use devnet
  • Useful commands for solana
  • How to init a project with anchor framework

Use the devnet or testnet networks to develop your program and see how
they perform before deploying them to the main network where they will
be available to everyone.

So start by setting up solana to work with the devnet network like this:

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

Then generate your wallet which will be required to run and deploy your
programs with:

solana-keygen new --force
Enter fullscreen mode Exit fullscreen mode

You'll see be asked to input a password to lock your wallet for
additional protection. Then you'll see your mnemonic which is a
combination of 12 words used to generate infinite addresses for your
wallet:

Generating a new solana wallet with solana-keygen<br>
new

You can then check your address with:

solana address
Enter fullscreen mode Exit fullscreen mode

Give yourself some test Solana coins with the airdrop command:

solana airdrop 2
Enter fullscreen mode Exit fullscreen mode

You can check your balance anytime with:

solana balance
Enter fullscreen mode Exit fullscreen mode

Now that you have solana configured to work with the devnet network and
have a new wallet ready, let's setup an Anchor project which will create
all the folders and boring configuration for us. Go to your Desktop and
run this command:

anchor init solana-global-article  
cd solana-global-article
Enter fullscreen mode Exit fullscreen mode

3. Understanding the Anchor framework setup

Let's take a look at what Anchor has done for you. Open the project with
your preferred code editor.

In this section you'll learn:

  • How to understand the code that anchor has created for you
  • The main files to pay attention to
  • Resources for deeper understanding of the anchor setup

You only have to pay attention to 2 files to begin coding.

The first and most important file is the lib.rs it's the main one that
will be loaded in the program:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod solana_global_article {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}
Enter fullscreen mode Exit fullscreen mode

Anchor programs are pretty simple, you import the framework, then you
indicate where the #program contains the main logic and specify the
#[derive(Accounts)] which is where the data will be stored and where
you can access accounts.

The first line use anchor_lang::prelude::*; is just importing Anchor
so you can use all the goodness it provides for your program.

The declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); line
indicates the id of your program, the equivalent of the address for your
to-be-deployed smart contract in Ethereum. This is necessary for Anchor.

Then you specify the structs which are elements that contain the data
for your project with #[derive(Accounts)] . In Rust Solana programs
you separate the data and the functionality. Inside the #program block
you modify and access data but you don't store it there, all the data is
stored in structs.

Don't worry if it doesn't make much sense yet, you'll see what I mean
with the functionality / data separation soon enough.

The second file you have to understand is Cargo.toml which is the
equivalent to package.json if you're a javascript developer:

[package]
  name = "solana-global-article"
  version = "0.1.0"
  description = "Created with Anchor"
  edition = "2018"

[lib]
  crate-type = ["cdylib", "lib"]
  name = "solana_global_article"

[features]
  no-entrypoint = []
  no-idl = []
  no-log-ix-name = []
  cpi = ["no-entrypoint"]
  default = []

[dependencies]
  anchor-lang = "0.20.1"
Enter fullscreen mode Exit fullscreen mode

As you can see you define the project data such the name, description
and then the dependencies at the bottom. You'll be adding things there
with cargo install.

I highly recommend you check this official anchor resource to see a
minimal example of the anchor configuration:
https://project-serum.github.io/anchor/tutorials/tutorial-0.html#generating-a-client

Continue ahead with the most interesting section where you'll begin to
use one of the coolest languages ever, Rust!

4. Coding the Rust program

Now the fun part, let's code the Rust program!

In this section you'll learn:

  • How to create a working Solana program according to your specification
  • The different attributes anchor gives you to make your life easier
  • How to understand Rust variables and syntax

If you remember we wanted to build a simple decentralized application
that allows users to write a shared article with anyone that uses
Solana. Kinda like an open book. Where people add their little knowledge
and fun elements.

The first step before coding is defining what we want our program to do
in a simple list:

  • Solana users will be able to connect their wallets and write 3 words on the global article
  • Up to 3 words per wallet but people can write less, so if someone just wants to add 1 word or 2 to the open book, they can do so
  • Words will be separated by spaces and we'll remove any empty space between them in Rust
  • Each word will have a maximum of 15 characters to keep it clean

Every great project always starts with a clear vision of what the
founder wants to build, it will save time and define a clear end goal

Great! Now open the global article project with your code editor and
navigate to Programs > src > lib.rs and open that file.

Remember that lib.rs is the starter and main file for our solana
program.

Let's start by creating the data structures where we'll store our
content. Add the Article struct:

#[account]
pub struct Article {
  pub content: String,
}
Enter fullscreen mode Exit fullscreen mode

Here's the breakdown for you to fully understand what we just did:

  • As you can see we use the pub keyword which indicates this is a public struct which makes it accessible for other functions and structs. Without it you'll get an error saying can't leak private type .
  • Next we named our struct Article simply because this will be the article where we'll store our data. You can name it Book or something similar if you'd like.
  • Then we create the content property which is a String that will contain our information.

Important: "Accounts" in Solana programs are like "files" in your
computer. Their purpose is to store data. So when we say "account" in
Solana, we mean a place to store your data. Accounts also have
metadata that indicate the owner of that file and more.

Continue by creating the Initialize struct which is the one used to
setup the initial data and configurations. This is required because
unlike solidity, variables must be set initially:

#[derive(Accounts)]  
pub struct Initialize<'info> {  
  #[account(  
      init,  
      payer = person_that_pays,  
      space = 8 // account discriminator  
      + 32 // pubkey  
      + 10000 // make the message max 10k bytes long  
  )]  
  pub article: Account<'info, Article>,  
  #[account(mut)]  
  pub person_that_pays: Signer<'info>,  
  pub system_program: Program<'info, System>,  
}
Enter fullscreen mode Exit fullscreen mode

Let's go line by line to understand what's going on. Try to copy the
code first and then read along:

  • #[derive(Accounts)] According to the official documentation derive accounts means: Implements an Accounts deserializer on the given struct. Meaning it allows this struct to process user addresses and accounts. You can see the official description here: https://docs.rs/anchor-derive-accounts/0.18.2/anchor_derive_accounts/derive.Accounts.html
  • Then we create a public struct like before but this time it is called Initialize and it has an 'info lifetime. Lifetimes are a Rust thing that allow you to tell him to use a specific data from somewhere else. It's a way to pass variables. Don't worry if it's confusing you'll get used to it over time.
  • Next we initialize an #[account(init, payer = person_that_pays, space = 8 + 32 + 10000] . What we're doing here is telling the solana program to initialize an account where the data will be stored, then we define who's gonna pay for that transaction and the space we need for that data.
  • pub article: Account<'info, Article> : Here we are telling solana to store the article in the new data account we've created for it to be retrieved later.
  • #[account(mut)] pub person_that_pays: Signer<'info> : We defining the person that will pay to create the data account, which is a Signer type. It's the equivalent of setting up an owner in solidity, while Signer is the address type.
  • pub system_program: Program<'info, System>, : The system_program is a required element to create your solana data. Must be included in the initializer.

Now go to the #program section and create the main function to start
and setup the program like this:

#[program]  
pub mod solana_global_article {  
use super::*;  
pub fn initialize(ctx: Context) -> ProgramResult {  
  // Get the article  
  let article_account = &mut ctx.accounts.article;  
  // Initialize the variables (this is required)  
  article_account.content = ("").to_string();
  Ok(())  
  }  
}
Enter fullscreen mode Exit fullscreen mode

The initialize function is receiving a Context with the Initialize
struct we've created before. Solana programs don't store state variables
in the same place like Ethereum smart contracts do, instead they
separate the data storage and the functionality.

That's why we always have to pass a Context into every solana program
function to receive the data we want to manipulate, since it can't
access data on its own.

What we're doing in this function is select the article struct we've
defined previously:

let article_account = &mut ctx.accounts.article;
Enter fullscreen mode Exit fullscreen mode

And setup the content of that article struct to an empty string:

article_account.content = ("").to_string();
Enter fullscreen mode Exit fullscreen mode

Finally we're returning the function with the Ok(()) result. So what
we did is we went to this struct:

pub struct Article {
  pub content: String,
}
Enter fullscreen mode Exit fullscreen mode

And initialized the content to an empty string that can be accessed
later. Variables need to be initialized to a starter value always.

Let's recap what we've done so far:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod solana_global_article {
  use super::*;
  pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
    // Get the article
    let article_account = &mut ctx.accounts.article;
    // Initialize the variables (this is required)
    article_account.content = ("").to_string();

    Ok(())
  }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
  #[account(
    init,
    payer = person_that_pays,
    space = 8 // account discriminator
    + 32 // pubkey
    + 10000 // make the message max 10k bytes long
  )]
  pub article: Account<'info, Article>,
  #[account(mut)]
  pub person_that_pays: Signer<'info>,
  pub system_program: Program<'info, System>,
}

#[account]
pub struct Article {
    pub content: String,
}
Enter fullscreen mode Exit fullscreen mode

You should have that code just like that. Now it's the perfect time to
run a anchor build in your terminal. The build command will tell you
whether your code is great or if it has any errors. It's very important
to do it often to catch issues early.

Continue reading to develop a complex Rust function in the next section.

5. Creating the article update function

So far we've setup a simple program that doesn't do much. It's time to
create the function that allows people to write the article data.

In this section you'll learn:

  • How to create a struct that updates data in the Solana blockchain
  • A simple explanation on Rust variable types
  • How to use the .spli() function and iterate over it

The first step when creating a function that updates blockchain data, is
to create a struct with the variables you want to have updated like so:

#[derive(Accounts)]  
pub struct WriteIntoArticle<'info> {  
  // Here goes the info that you want to modify like this  
  #[account(mut)]  
  pub article: Account<'info, Article>,  
}
Enter fullscreen mode Exit fullscreen mode

You start by adding the #[derive(Accounts)] modifier which is required
to make this work.

Then you setup the name of it. In this case I chose WriteIntoArticle .
After that you include the struct you want to modify, in this case it's
gonna be the Article struct so that's why I saved it with the
article variable name.

As you can see we've added the #[account(mut)] attribute. This is so
we can modify the article data since mut in Rust indicates a mutable
variable that can be updated.

To put it simply, in Rust you declare a constant like this:

let my_variable = 10; // This value can't be changed
Enter fullscreen mode Exit fullscreen mode

And a variable with the mut modifier:

let mut my_variable = 10;  
my_variable = 5; // It works
Enter fullscreen mode Exit fullscreen mode

When we create a mutable account with an attribute like this
#[account(mut)] what we're doing is telling Solana that this data will
be modified in the future. In this case, our article variable will be
updated with new data.

The mutable account attribute is required otherwise we won't be able to
modify that data.

Now we can write the function that will use our newly-created struct:

pub fn write_into_article(  
  ctx: Context,  
  three_words: String, // If more, after 3 they will be removed  
) -> ProgramResult {  
  // To update the article string  
  let article = &mut ctx.accounts.article;  
  let split_iterator = three_words.trim().split(" ");```
{% endraw %}

{% raw %}
Enter fullscreen mode Exit fullscreen mode

Ok(())

}




First we define the `write_into_article` function which receives the
context `WriteIntoArticle` and a String variable called `three_words`
which is the data our users will send to write into the global article.

Then we read the `article` data by accessting the `&mut
ctx.accounts.article` context variable.

Since we want people to send a string made of 3 words, we gotta split it
into separate units so we can check that each word is valid, meaning:

  - Each word is made of less than 15 characters
  - To remove all the extra empty spaces in between words
  - To verify if the user actually sent 3 words or more

The `trim()` function will remove empty spaces between words while
`split(" ")` will separate words by spaces. Note that `split()` returns
an iterator. We can't access the data without iterating over it first or
`collect()` ing it.

Now let's iterate over those words to check it the user sent more words
than permitted since that's not allowed because we want multiple people
to contribute to this global article project. Add the following below
the `split_iterator` variable:



```rs
let split_iterator = three_words.trim().split(" ");  
let mut final_words = Vec::new();  
let mut counter_added = 0;  
for s in split_iterator {  
  if s.trim().is_empty() {  
    continue;  
  }  
  if s.trim().len() >= 15 {  
    return Err(Errors::WordTooLong.into());  
  }  
  final_words.push(s);  
  counter_added += 1;  
  if counter_added >= 3 {  
    break;  
  }  
}  

Ok(())
Enter fullscreen mode Exit fullscreen mode

There' s a lot going on, so let's break it down for your understanding:

  • let mut final_words = Vec::new() : The final_words variable will contain a list of the 3 words. Vectors are arrays of variable size in Rust. You can push items to them. They have to be initialized with Vec::new() . In this case we're making it mut able because we want to add items to it later.
  • let mut counter_added = 0; : This is a counter that will keep track of how many words we're adding to the list to stop at 3 and not add more than necessary.
  • for s in split_iterator {} : Remember that split_iterator is an iterator which means we gotta loop through it to access each item. We are doing that with a simple for in loop which stores each item into the s variable.
  • if s.trim().is_empty() { continue; } : Here we're checking if the word is empty or not and if so, skip it. This is because when we split by spaces we may find that we have words separated by several spaces. The split function then recognizes empty spaces as words, so we get rid of those empty words with a simple if statement.
  • if s.trim().len() >= 15 { return Err(Errors::WordTooLong.into()); } : Here we're checking if the word inside the variable s has 15 characters or more, in which case we return an error. In this case I've called the error WordTooLong . You'll see later on how we create and define the error messages. The Err function is from anchor and it allows us to send and error and stop execution.
  • final_words.push(s); counter_added += 1; : Here we're simply adding the word to the final_words vector after checking it is valid to our conditions and increasing the counter.
  • if counter_added >= 3 { break; }: If the counter is 3 or more, stop the loop. This is so if people send more than 3 words, we cut off and remove the remaining ones.

As you can see we're doing quite a few things in that little code. It's
good that you get familiar with Rust syntax. You'll love it in no time.

Now let's continue with the final part of that function which looks like
the following:

// Join the 3 words after removing spaces
let mut joined_words = final_words.join(" ");
// Add a space at the end with this
joined_words.push_str(" ");
// Article content gets immediately updated
article.content.push_str(&joined_words);
Enter fullscreen mode Exit fullscreen mode

First we join the words we've extracted and cleaned up with the join("
")
method which combines words into one string and separates them by a
space.

Then we add a space at the end of those 3 words. The way you do that is,
you take the joined_words string and push another string to it with
.push_str(" ") which is the way you concatenate strings in Rust.

Finally you update your article global variable with the same push
method to concatenate words article.content.push_str(&joined_words);
note that we don't do article.content =
article.content.push_str(&joined_words);
that's because the push_str
method updates the original string.

Now we can go ahead and define the errors section which is pretty simple
as you'll see. Right at the end of the file below all the structs and
programs write this:

#[error]  
pub enum Errors {  
  #[msg("Each word must be less than 15 characters")]  
  WordTooLong,  
}
Enter fullscreen mode Exit fullscreen mode

The #[error] attribute indicates this enum is the one containing the
error definitions.

Then we simply add the keywords we want, in my case it's just
WordTooLong for the error name and a message on top with the msg
attribute. The message in quotes will be displayed when the error is
called.

That should be it for the Solana program code! You did it!

You can see the updated and complete code for the program in my github
here:
https://github.com/merlox/solana-world-article/blob/master/programs/solana-global-article/src/lib.rs

6. Testing the Solana program

In Solana rust programs you always test the code since as far as I
know, there are no tools you can use to interact with the programs
directly like in ethereum with the verified contracts in etherscan and
remix. You don't have that here.

In this section you'll learn:

  • How to write Solana tests using the anchor protocol
  • How to execute Rust programs from anchor
  • How to get data from the blockchain

So let's get testing! It's pretty simple as you'll see, just long lines
of code. This is the initial setup:

import * as anchor from '@project-serum/anchor'  
import { Program } from '@project-serum/anchor'
import { GlobalArticleTutorial } from '../target/types/global_article_tutorial'

describe('global-article-tutorial', () => {
  // Configure the client to use the local cluster.  
  anchor.setProvider(anchor.Provider.env())
  const program = anchor.workspace.GlobalArticleTutorial as Program

  it('Is initialized!', async () => {  
    // Add your test here.  
    const tx = await program.rpc.initialize({}) 
    console.log("Your transaction signature", tx)
  })
})
Enter fullscreen mode Exit fullscreen mode

Anchor imports all the required libraries for you at the top and then
creates a simple test to see if the intialization would work. There's an
issue, this first test will fail.

Simply because the initialize function is not receiving the right
parameters. Modify the first test initialization function const tx =
await program.rpc.initialize({})
with this object for it to work:

it('Is initialized!', async () => {  
  const deployerKeypair = anchor.web3.Keypair.generate()  
  const personThatPays = program.provider.wallet

  // Add your test here  
  await program.rpc.initialize({  
    accounts: {
      article: deployerKeypair.publicKey,  
      personThatPays: personThatPays.publicKey,  
      systemProgram: anchor.web3.SystemProgram.programId,  
    },  
    signers: [deployerKeypair],  
  })
})
Enter fullscreen mode Exit fullscreen mode

What we're doing there is creating a new Keypair which is a sample
account used for the test. Then we get the wallet that will pay for the
initialization.

As you can see inside initialize we've added an accounts object
which, if you remember from before, it's where the data is stored and we
simply add the initial data.

The required data are the variables from the Initialize struct.
However note that the variables use camelCase notation, when in our rust
program, we've defined those variables with snake_case notation.

For instance, personThatPays in the test is person_that_pays in the
Initialize struct. Keep that in mind.

Now run the test with anchor test and it should be successful. You can
use the devnet network or use the local solana network which lives in
your computer exclusively for testing purposes. Go ahead and follow
these steps:

  1. Run solana config set --url localhost , this will change the network you use to localhost instead of devnet.
  2. Then do run the command solana-test-validator and stop it after a few seconds. If you keep it running your tests won't run, since it needs to work in the background.
  3. Open your Anchor.toml file and update the [programs.devnet] block to [programs.localnet]
  4. Then in that same file update cluster = "devnet" to cluster = "localnet" .

If it was successful you'll see this message:

1 passing (243ms)
Enter fullscreen mode Exit fullscreen mode

Let's now write the second test which will include the functionality to
write into the global article. We'll do a one word test first. Start by
creating the test structure:

it('Should write an article with 1 word successfully', async () => {})
Enter fullscreen mode Exit fullscreen mode

Then copy the code to from the previous test to initialize the program.
When testing we don't care if we're repeating code or adding unnecessary
lines since it's code used exclusively for development. It won't be used
by users:

it('Should write an article with 1 word successfully', async () => {
  const deployerKeypair = anchor.web3.Keypair.generate()  
  const personThatPays = program.provider.wallet

  await program.rpc.initialize({  
    accounts: {  
      article: deployerKeypair.publicKey,  
      personThatPays: personThatPays.publicKey,  
      systemProgram: anchor.web3.SystemProgram.programId,  
    },  
    signers: [deployerKeypair],  
  })
})
Enter fullscreen mode Exit fullscreen mode

Now let's add some additional functionality to write an article to the
blockchain. Go ahead and add the following function right below the
initialize method:

await program.rpc.writeIntoArticle('hey', {  
  accounts: {  
    article: deployerKeypair.publicKey,  
  },  
  signers: [],  
})
Enter fullscreen mode Exit fullscreen mode

As you can see we're executing the writeIntoArticle function from the
program. The first parameter 'hey' is the word we're gonna add to the
blockchain, while the second parameter is a javascript object containing
the accounts with the article data.

Remember that accounts in Solana are pieces of storage kinda like
files stored in the blockchain. They don't represent a username and
password. Although they have some metadata inside them to determine who
created that data and so on.

In this case we're simply updating the article variable and we're
sending it the signer which is the deployedKeypair.publicKey to let
the program know, who is sending this data.

At this point, you may be wondering: "how do I read the information
stored in the blockchain?". Good question. And the way you do that is
with the .fetch() or .all() methods. The .all() method allows you
to retrieve all the elements in a variable, in case you have an array or
vector in your rust program.

Here's how we check the article data we just sent to the blockchain:

const articleData = await program.account.article.fetch(deployerKeypair.publicKey)  
expect(articleData.content).to.equal('hey ')
Enter fullscreen mode Exit fullscreen mode

We do await the method fetch() from the actual article object in
our program while sending it the publicKey . This way we're getting
the actual data stored in the blockchain.

Then we execute the final part of the test which is verifying that the
information we just sent is what we expected with the expect()
function.

Note the empty space right after the word 'hey ' . It is intentional
to add a separator for the next words people add in the future.

If you run the test with anchor test you'll notice that there's an
error, that's because we're using expect() which is a testing function
from the 'chai' package. In order for the test to work, you must
import expect like this at the top of the file:

import { expect } from 'chai'
Enter fullscreen mode Exit fullscreen mode

You don't have to install that dependency because anchor has already
done that for you.

Here's how the test looks in its entirity:

import { expect } from 'chai' // Keep this at the beginning of the file along with the other imports

it('Should write an article with 1 word successfully', async () => {
  const deployerKeypair = anchor.web3.Keypair.generate()
  const personThatPays = program.provider.wallet

  // Add your test here
  await program.rpc.initialize({
    accounts: {
      article: deployerKeypair.publicKey,
      personThatPays: personThatPays.publicKey,
      systemProgram: anchor.web3.SystemProgram.programId,
    },
    signers: [deployerKeypair],
  })

  await program.rpc.writeIntoArticle('hey', {
    accounts: {
      article: deployerKeypair.publicKey,
    },
    signers: [],
  })

  const articleData = await program.account.article.fetch(deployerKeypair.publicKey)
  expect(articleData.content).to.equal('hey ') // Note the space at the end, added by the program
})
Enter fullscreen mode Exit fullscreen mode

Run the tests again with anchor test , you should see a message like
the following if they are successful:

 Is initialized! (494ms)  
 Should write an article with 1 word successfully (942ms)
2 passing (1s)
Enter fullscreen mode Exit fullscreen mode

It's time for the final test. The one where we check if we can add 3
words several times. We won't write a test for when a user sends more
than 3 words to the article since I don't want to bother you with all
this testing.

But if the code is right, when a user send more than 3 words to the
global article, they should only write 3 words while the rest are
removed.

Here's how the third test looks like:

it("should write 3 words two times", async () => {
  const deployerKeypair = anchor.web3.Keypair.generate()
  const personThatPays = program.provider.wallet

  // Add your test here
  await program.rpc.initialize({
    accounts: {
      article: deployerKeypair.publicKey,
      personThatPays: personThatPays.publicKey,
      systemProgram: anchor.web3.SystemProgram.programId,
    },
    signers: [deployerKeypair],
  })

  await program.rpc.writeIntoArticle('hey whats up', {
    accounts: {
      article: deployerKeypair.publicKey,
    },
    signers: [],
  })

  await program.rpc.writeIntoArticle('this is my', {
    accounts: {
      article: deployerKeypair.publicKey,
    },
    signers: [],
  })

  const articleData = await program.account.article.fetch(deployerKeypair.publicKey)
  console.log('article data', articleData)
  expect(articleData.content).to.equal('hey whats up this is my ') // Note the space at the end, added by the program
})
Enter fullscreen mode Exit fullscreen mode

As you can see, we've copied the previous tests initially to keep that
same setup while updating the writeIntoArticle function with the text
'hey whats up' and repeating the same function with the text 'this is
my'
.

You can write anything really but that's to see if we are able to
concatenate several writes to the blockchain for the purposes of having
a global article made by several people.

We then do a fetch() and check if the article content is what we want,
with an empty space at the end. Note the console.log() I've added,
that's to see how the data received looks like. I encourage you to do
the same and get familiar with the responses the solana blockchain gives
you.

Write the test by hand, don't copy paste it because you don't develop
the muscle memory necessary to truly understand how these things work
until you use your hands.

Now run it with anchor test for a successful result:

 Is initialized! (181ms)  
 Should write an article with 1 word successfully (958ms)  
article data { content: 'hey whats up this is my ' }  
 should write 3 words two times (1448ms)
3 passing (3s)
Enter fullscreen mode Exit fullscreen mode

As an exercise, try to write a new test that tries to writeIntoArticle
more than 4 words and see what happens.

That should be it! The program is tested and ready to go! Let us now
create the frontend which is what people will use to interact with the
program from their computers. It's gonna be awesome, let's go.

7. Creating the decentralized application frontend

It's time to put it all together into a great looking dapp for people to
play around and interact with your program. Let's get to it.

In this section you'll learn:

  • How to create a React frontend for your dapp
  • How to setup webpack with the different plugins
  • How to use Material UI and stylus for the design

The frontend will live in a separate folder inside the same project you
started before. That's because there are many files and they need their
own config. Anchor created the app/ folder specifically for that. For
the frontend.

I usually start by creating the webpack configuration myself with the
initial files. But I found a better way.
Createapp.dev is the app (they didn't pay me to
say this, I just like it) I use now to generate the initial setup.

You simply go to the website, choose webpack and setup the files by
clicking on the options you want. If you want my configuration, just
copy the following gif. Make sure to have Material UI selected in the UI
library section since we'll use it later:

A gif showing you the process to generate the starter frontend files<br>
with createapp.dev, an awesome utility. This is a mac screen recording<br>
converted to gif with ffmpeg<br>
btw

Once you download the folder you'll see the files according to the
chosen configuration, in my case they are the following:

package.json  
README.md  
src/  
-- App.js  
-- index.js  
-- styles.styl  
webpack.config.js
Enter fullscreen mode Exit fullscreen mode

What's great is that we have access to the build webpack configuration
and we can adapt it to however we want. I chose to use stylus since it's
great for css configurations.

I put all those files inside the app/ folder of the project we had
earlier. Navigate to that folder from the terminal and execute:

yarn install
Enter fullscreen mode Exit fullscreen mode

Create a file called:

.gitignore
Enter fullscreen mode Exit fullscreen mode

And inside that file simply indicate which folders and files to ignore
when uploading your project in github. You can copy the configuration
from the same site you used to create the config:

Then, inside your package.json in the scripts section add a new
script used to start our dapp so it looks like the following:

"scripts": {  
  "clean": "rm dist/bundle.js",  
  "build-dev": "webpack --mode development",  
  "build-prod": "webpack --mode production",  
  "watch": "webpack --mode production -w",  
  "serve": "npx http-server docs",  
  "start": "npm-run-all --parallel watch serve"
},
Enter fullscreen mode Exit fullscreen mode

You'll need to install http-server locally with:

yarn add http-server
Enter fullscreen mode Exit fullscreen mode

And npm-run-all globally for the scripts to work:

npm i -g npm-run-all
Enter fullscreen mode Exit fullscreen mode

For some reason the createapp.dev app doesn't create a .babelrc file.
You gotta do it yourself. At the root of your app/ folder create a
.babelrc file and copy the contents from the page.

You may see a different configuration based on which parameters you
chose.

Then update your webpack.config.js to output the compiled files to
docs/ since we'll use that to host our dapp for free with github pages
as you'll see later on:

output: {
  path: path.resolve(__dirname, 'docs'),  
  filename: 'bundle.js'  
},
Enter fullscreen mode Exit fullscreen mode

Now let's go ahead and create a simple frontend design, it will look
like the following:

The solana open global book<br>
design

A simple input where we input the words we want to add to the book and
submit. Users can connect with the button at the top right. Then they
choose the wallet they want to use and they approve transactions.

The first thing is to create the design. Open App.js you'll see
something like this:

import React from 'react'  
import Button from '@material-ui/core/Button'

class App extends React.Component {  
  render() {  
    const { name } = this.props  
    return (
      <> 
        Hello {name} this is a material UI button
      </>
    )  
  }  
}

export default App
Enter fullscreen mode Exit fullscreen mode

Let's change that to a functional React component:

import React from 'react'  
import Button from '@material-ui/core/Button'

const App = () => {  
  return (  
    <>  
      Hello this is a material UI button
    </>  
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

You can see how it looks anytime with yarn start in fact I recommend
you to keep it open while you develop.

If you get this error:

ERROR in unable to locate
'/Users/merunas/Desktop/solana-global-article/app-tutorial/src/index.html'
glob
Enter fullscreen mode Exit fullscreen mode

You have to simply update the webpack.config.js file by removing the
CopyPlugin for now.

The Material UI library added by that web app is outdated. So go to your
App.js and update the imports:

import Button from '@material-ui/core/Button'
Enter fullscreen mode Exit fullscreen mode

To:

import { Paper, Skeleton, TextField, Button} from '@mui/material'
Enter fullscreen mode Exit fullscreen mode

Then install these ones:

yarn add @mui/material @emotion/react @emotion/styled
Enter fullscreen mode Exit fullscreen mode

Update your App component to the following for the initial structure
while using the Material UI components we've imported earlier:

const App = () => {
  return (
    <>
    <header className='header'>
      <div className="title-container">
      <h1 className="main-title">Open Global Book</h1>
      <h4 className="main-subtitle">By Merunas</h4>
      </div>
    </header>
    <Paper elevation={20} className='content-box'>
      <Skeleton variant='text' />
      <Skeleton variant='text' />
      <Skeleton variant='text' />
    </Paper>
    <div className="three-words-input-container">
      <TextField
      id='outlined-basic'
      label='Write to the open book (5 words max)'
      variant='outlined'
      className='words-input'
      />
      <Button variant="contained" className="submit-button">Submit</Button>
    </div>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

We added a header with a title and subtitle. Then a Paper section with
3 Skeleton to create a loading text component that feels like it's
retrieving data from the blockchain. We'll create that later on.

Finally we have a section with the inputs using the TextField and
button to submit. It will look like this:

Not great. But that's what the CSS classes we added are for. Go to your
styles.styl file and paste this code:

*
  font-family: 'roboto'

.header
  display: flex
  align-items: center

.title-container
  width: 100%

.main-title
  margin-bottom: 0

.main-subtitle
  margin-top: 0

.wallet-connect
  width: 200px

.content-box
  max-width: 800px
  margin: auto
  padding: 20px
  margin-top: 10px
  margin-bottom: 30px

.solana-image
  width: 70px
  margin-top: 7px

.three-words-input-container
  max-width: 800px
  margin: auto
  display: flex
  justify-content: space-around
  align-items: center

.words-input
  width: 410px

.helper-description
  max-width: 800px
  margin: auto
  margin-top: 100px
  color: grey
  font-size: 10pt

@media (max-width: 500px)
  .three-words-input-container
    flex-direction: column

    .submit-button
      margin-top: 20px

    .words-input
      width: 100%
Enter fullscreen mode Exit fullscreen mode

Your dapp will look like this now:

Notice how the font is not quite right. That's because we haven't added
the Roboto font Material Ui uses. To add the font, we'll update
webpack.config.js so that the HtmlWebpackPlugin uses a file we can
easily edit:

new HtmlWebpackPlugin({
 title: 'Solana Global Book',
 template: './src/index.ejs',
 filename: 'index.html',
}),
Enter fullscreen mode Exit fullscreen mode

Then create an index.ejs inside src . You may be wondering: "What is
.ejs?". EJS is a template node engine that we will use to update the
title of our dapp directly from webpack with a variable. It allows you
to add variables easily.

Here's how the index.ejs looks like:

<!DOCTYPE html>
<html lang="en" dir="ltr">
 <head>
  <meta charset="utf-8" />
  <title><%= htmlWebpackPlugin.options.title %></title>
  <link
    rel="stylesheet"
    href="https://fonts.googleapis.com/css family=Roboto:300,400,500,700&display=swap"
  />
  <meta name="viewport" content="initial-scale=1, width=device-width" />
 </head>
 <body>
  <div id="app"></div>
 </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Notice how we're importing the roboto font and using the title
variable from webpack in between those <%= %> special tags. They come
from EJS.

Now the font is much better:

The next step is to add the solana logo. Simply download it from here:
https://raw.githubusercontent.com/merlox/solana-world-article/master/app/assets/solana.jpeg

Create an assets folder inside app/ and move the solana.jpg file
right there. Then modify App.js to include the logo:

<header className='header'>
  <img src='assets/solana.jpeg' className='solana-image' />
  <div className="title-container">
    <h1 className="main-title">Open Global Book</h1>
    <h4 className="main-subtitle">By Merunas</h4>
  </div>
</header>
Enter fullscreen mode Exit fullscreen mode

However that won't work just yet. We gotta tell webpack to move the
assets to the docs/ folder where the combined files live. Update
webpack.config.js plugins to this:

new CopyPlugin({
  patterns: [
    {
      from: 'assets',
      to: 'assets',
    },
  ],
}),
Enter fullscreen mode Exit fullscreen mode

Now reload webpack by stopping the terminal and doing yarn start
again.

You can see the logo properly positioned! That's because of the css
we've added earlier and the classes in App.js .

In the next section you'll learn how to connect Phantom and other Solana
wallets to work with the blockchain.

8. Setting up the Solana wallets connection

In this section you'll learn:

  • How to connect Phantom and many other wallets easily
  • How to configure the React components to work with Solana
  • How to use the different libraries designed by Anchor and Solana

To interact with the blockchain we need a way to let our dapp send and
receive information to your wallet. There are many ways to set it up.
But in this case we'll use the many libraries configured to work with
react and Phantom.

If you haven't done so yet, download https://phantom.app/ for your
browser and create an account. It is the Metamask equivalent for Solana.
Works flawlessly.

Then create a file called WalletContext.js inside src/ and import
all these libraries at the top:

import React from 'react'
import {
 ConnectionProvider,
 WalletProvider,
} from '@solana/wallet-adapter-react'
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'
import {
 LedgerWalletAdapter,
 PhantomWalletAdapter,
 SlopeWalletAdapter,
 SolflareWalletAdapter,
 SolletExtensionWalletAdapter,
 SolletWalletAdapter,
 TorusWalletAdapter,
} from '@solana/wallet-adapter-wallets'
import {
 WalletModalProvider,
} from '@solana/wallet-adapter-react-ui'
import config from './../config'
require('@solana/wallet-adapter-react-ui/styles.css')
Enter fullscreen mode Exit fullscreen mode

Note that I created a file named config.js which simply contains a
javascript object with some configuration that we'll use for several
files. So go ahead and create a file named config.js right outside the
src/ folder with these contents:

export default {
  solana_article_account: '',
  network: 'devnet',
  endpoint: 'https://api.devnet.solana.com',
}
Enter fullscreen mode Exit fullscreen mode

The solana_article_account will be the address of the account that
holds the data for the article. As you know, accounts hold data.

Install the following dependencies with yarn:

yarn add @solana/wallet-adapter-react @solana/wallet-adapter-base @solana/wallet-adapter-wallets @solana wallet-adapter-react-ui
Enter fullscreen mode Exit fullscreen mode

Then add the rest of the configuration for the wallet connection
context:

export default ({ children }) => {
 const network = WalletAdapterNetwork.Devnet
 const wallets = [
  new PhantomWalletAdapter(),
  new SlopeWalletAdapter(),
  new SolflareWalletAdapter({ network }),
  new TorusWalletAdapter(),
  new LedgerWalletAdapter(),
  new SolletWalletAdapter({ network }),
  new SolletExtensionWalletAdapter({ network }),
 ]
return (
  <ConnectionProvider endpoint={config.endpoint}>
   <WalletProvider wallets={wallets} autoConnect>
    <WalletModalProvider>
     {children}
    </WalletModalProvider>
   </WalletProvider>
  </ConnectionProvider>
 )
}
Enter fullscreen mode Exit fullscreen mode

There's a lot going on. It took me a while to set it up properly because
many tutorial online don't explain how this works so I'll be very
descriptive for you:

  • First we take the network that will be used for the wallet connections. This is only necessary for the Solflare, Sollet and SolletExtension wallets. For some reason the WalletAdapterNetwork doesn't have an option for localhost networks. But that's fine, we can work without it.
  • Then we create an array of wallets we are gonna use. Simply do a new for every wallet we've imported previously from the @solana/wallet-adapter-wallets library. You can just import PhantomWalletAdapter if you're not gonna use the others.
  • Then, in the return you gotta place those components in the order I've shown you. Notice the {children} variable. That one is necessary because our entire dapp will be placed there. The children variable is a function argument as you can see at the beginning export default ({ children }) => {} .

Our entire dapp will be inside that children variable. It is required
because all those wallet and connection providers will be passed down to
the main App where they will be used to interact with the program
we've created.

Just so you understand, in our App component we will add the following:

<WalletContext>
  <App/>
</WalletContext>
Enter fullscreen mode Exit fullscreen mode

Where WalletContext is the entire list of providers that we're
returning from the WalletContext.js file we created earlier. Meaning,
our app is a child of all those providers so we can access the wallet
connection in all of our components. Let me know if you got any more
questions regarding this point in the comments.

Now go back to the App.js file and import the file we've just created:

import WalletContext from './WalletContext'
Enter fullscreen mode Exit fullscreen mode

Right at the end of the file, use it to hold the App like so:

export default () => {
 return (
  <WalletContext>
   <App/>
  </WalletContext>
 )
}
Enter fullscreen mode Exit fullscreen mode

Then, in the App component add a wallet connect button in the header:

<header className='header'>
  <img src='assets/solana.jpeg' className='solana-image' />
  <div className="title-container">
    <h1 className="main-title">Open Global Book</h1>
    <h4 className="main-subtitle">By Merunas</h4>
  </div>
  <div className="wallet-connect">
    <WalletMultiButton />
  </div>
</header>
Enter fullscreen mode Exit fullscreen mode

That button comes from this library:

import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
Enter fullscreen mode Exit fullscreen mode

Now it's a good time to run yarn start and see how the app looks...
unfortunately you will find an error in the webpack build that says:

BREAKING CHANGE: webpack < 5 used to include polyfills for
node.js core modules by default.  
This is no longer the case. Verify if you need this module and configure
a polyfill for it.
Enter fullscreen mode Exit fullscreen mode

This had me searching for solutions for a good while. It basically
means, some of the libraries we've imported use specific node.js
utilities such as fs or path that are meant to be used for server
purposes. Webpack in versions 4 and older did include all those
libraries whenever necessary.

But now, webpack 5 doesn't include the required node libraries for your
imports to work.

In summary, you gotta go and tell webpack to not worry about those
imports. You can do that by adding the following to your webpack config:

resolve: {
  fallback: {
    fs: false,
    tls: false,
    net: false,
    path: false,
    zlib: false,
    http: false,
    https: false,
    stream: false,
    crypto: false,
    process: false,
  },
},
Enter fullscreen mode Exit fullscreen mode

Right at the same level of output and entry . That should fix all
the compilation errors and your app will load with no issues.

Some people suggest downgrading to webpack 4. In my opinion that's a
terrible idea. You shouldn't be forced to use older versions that may or
may not work. It is much better to fix those issues like I just did.

Start your app with yarn start and see how it looks. If you still get
errors, install and import this webpack plugin
https://www.npmjs.com/package/node-polyfill-webpack-plugin.

You'll now see the phantom solana button like so:

You can click on it to connect your wallet, just be sure to be on the
same network you configured previously. Remember that in your
config.js you've added this:

endpoint: 'https://api.devnet.solana.com',
Enter fullscreen mode Exit fullscreen mode

So in your phantom wallet be sure to select the devnet network. However
I recommend you to deploy your program to localhost first and then
you'll be able test it faster. In which case you'll have to change your
phantom extension config to use localhost and update the config.js
file endpoint.

Anyway. Congrats! you've got a working wallet connection that not only
looks good, but works flawlessly. Your frontend is ready to start
interacting with the blockchain. Let's continue with the next section!

9. Connecting the Solana program with our created React frontend

In this last section, you'll learn:

  • How to take connect your program with your phantom wallet through your dapp
  • How to read the Solana blockchain data from your program
  • How to write data into the blockchain for the Article struct

Let' s start by setting up the state variables. Import useState from
react like so in your App.js file:

import React, { useState } from 'react'
Enter fullscreen mode Exit fullscreen mode

Then create these variables inside the App component:

const [inputValue, setInputValue] = useState('')  
const [isLoading, setIsLoading] = useState(true)  
const [solanaArticle, setSolanaArticle] = useState('')
Enter fullscreen mode Exit fullscreen mode

The inputValue variable will be used to hold the data users type into
the input component. The isLoading variable is gonna be used to verify
when the data has finished loading from the blockchain mainly to display
the loading lines from Material UI and replace them with actual data
once available.

Then the solanaArticle variable is going to hold the data stored in
the solana blockchain used to display that information for the book.

After doing that, we'll setup the variables required for the wallet
blockchain connection, you can place them right below the state
variables:

const wallet = useAnchorWallet()  
const { connection } = useConnection()
Enter fullscreen mode Exit fullscreen mode

You'll have to import those 2 elements from the wallet-adapter-react
library like so:

import { useConnection, useAnchorWallet } from '@solana/wallet-adapter-react'
Enter fullscreen mode Exit fullscreen mode

At this point your App component will look like this:

import React, { useState } from 'react'
import { Paper, Skeleton, TextField, Button } from '@mui/material'
import WalletContext from './WalletContext'
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
import { useConnection, useAnchorWallet } from '@solana/wallet-adapter-react'
const App = () => {
  const [inputValue, setInputValue] = useState('')
  const [isLoading, setIsLoading] = useState(true)
  const [solanaArticle, setSolanaArticle] = useState('')
  const wallet = useAnchorWallet()
  const { connection } = useConnection()
  return (
    // Omitted for brevity
  )
}
Enter fullscreen mode Exit fullscreen mode

Before diving into the blockchain stuff, let's make sure our input HTML
element works by verifying the user is inputing 5 words while each word
being limited to 15 characters, separated by a space. To do that, find
your TextField component and include these fields:

<TextField
  id='outlined-basic'
  label='Write to the open book (5 words max)'
  variant='outlined'
  className='words-input'
  value={inputValue}
  onChange={e => checkAndAddWords(e)}
/>
Enter fullscreen mode Exit fullscreen mode

In React, the value attribute is required when updating the input data
programatically. As you can see, the onChange function is executing
the checkAndAddWords(e) we're about to create with the event received:

const checkAndAddWords = e => {
  let words = e.target.value.split(' ')
  for (let i = 0; i < words.length; i++) {
   if (words[i].length > 15) {
    return
   }
  }
  if (words.length > 5) return
  setInputValue(words.join(' '))
}
Enter fullscreen mode Exit fullscreen mode

There we're simply check that the words have 15 chars or less and we
stop the user from typing after adding 5 words.

Now let's get to the juicy part. We're gonna use the initialize
function from our program into the dapp so we can create and store data
into the blockchain. If you remember the Rust initialize function we
created does create an Account that stores the article data. Go ahead
and copy this code by hand:

const initialize = async () => {
  const provider = new Provider(connection, wallet, {})
  const program = new Program(idl, programID, provider)
  const keypairOne = Keypair.generate()
  try {
    await program.rpc.initialize({
    accounts: {
      person_that_pays: provider.wallet.publicKey,
      article: keypairOne.publicKey,
      systemProgram: SystemProgram.programId,
    },
    signers: [keypairOne],
    })
    console.log('done', keypairOne.publicKey.toString())
  } catch (e) {
    console.log('#1', e)
    return alert(e)
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's the breakdown:

  • We create the provider which is the connection to the phantom wallet.
  • Then we setup the program to interact with it. As you can see we need an IDL, programID and provider. We'll get those in a moment.
  • Then we create a new account keypair. This is the account where the data of our program will be stored.
  • Next, we do a try catch to run the initialize function from the program methods. It receives accounts and signers . This is the same structure we have in our program.
  • Accounts holds the person_that_pays which is the account that pays the transaction fees and rent costs for 2 years to keep that data online.
  • The article variable is the account we've passed and is gonna be used for the data we're about to store. And the system program is just the Solana main program.
  • After we're done, we log the account generated since we'll need it in a moment to keep updating the same data later on.

Go ahead and import the required elements like so:

import idl from './solana_global_article.json'
import { Program, Provider, web3 } from '@project-serum/anchor'
import { PublicKey } from '@solana/web3.js'
import config from './../config'
const programID = new PublicKey(idl.metadata.address)
const { SystemProgram, Keypair } = web3
Enter fullscreen mode Exit fullscreen mode

The solana_global_article.json import you can get it from your
target/deploy/.json which was created when you did anchor build .
Simply copy that one to your src/ folder.

Then install yarn add @project-serum/anchor @solana/web3.js which are
required for the dapp to work. After that, setup the programID and the
other web3 dependencies.

Try to run it now with yarn start . If at this point you're getting a
weird error like process not defined in your chrome dev tools, you can
simply fix it by following these steps:

  1. Doo yarn add node-polyfill-webpack-plugin
  2. In your webpack config add this: const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
  3. Then add it to the plugins section: new NodePolyfillPlugin()

Now it should work.

The initialize function should only be ran once by the creator of the
program. Although it can be ran as many times as you want to create new
article accounts used for storing data.

To execute it, simply add a button like this somewhere in your app:

<Button onClick={initialize} color="secondary" variant="contained">initialize</Button>
Enter fullscreen mode Exit fullscreen mode

You'll see the button and after clicking it a Phantom wallet transaction
popup will show up for you to confirm the initialization. Just make sure
you're in the same network as you configured in your WalletContext.js
file.

Then you'll see the console log message with the Keypair.generate()
result:

That's gonna be the address where the article data will be stored. Copy
it and paste it into your config.js file.

solana_article_account: '6LUM5nyBDT7CKqiJPrEsweKzdQktFDiGTsaRq6iG96c4',
Enter fullscreen mode Exit fullscreen mode

You can now remove or comment out the initialize button you've
created.

At this point we can just read the blockchain data with a function like
the following:

const getAndSetArticle = async () => {
  const provider = new Provider(connection, wallet, {})
  const program = new Program(idl, programID, provider)
  const articleData = await program.account.article.fetch(config.solana_article_account)
  setSolanaArticle(articleData.content)
  setIsLoading(false)
}
Enter fullscreen mode Exit fullscreen mode

Which simply initializes the program like before and gets the article
data froom the blockchain with the fetch method. Then it updates the
state variables to see that new information in the dapp.

Make sure to execute that function when the React component is setup and
ready using useEffect from React like so:

useEffect(() => {
  if (wallet) {
   getAndSetArticle()
  }
}, [wallet])
Enter fullscreen mode Exit fullscreen mode

Remember to import it at the top of your file:

import React, { useState, useEffect } from 'react'
Enter fullscreen mode Exit fullscreen mode

That way, once the component is ready to use, the function will be
executed and you'll see the article data.

Now update the Skeleton visual loaders with the information we receive
from the blockchain:

{isLoading ? (
  <Paper elevation={20} className='content-box'>
   <Skeleton variant='text' />
   <Skeleton variant='text' />
   <Skeleton variant='text' />
  </Paper>
) : (
  <Paper elevation={20} className='content-box'>
   {solanaArticle}
  </Paper>
)}
Enter fullscreen mode Exit fullscreen mode

Once the isLoading state is set to false, the app will show the
solanaArticle data to the user. Try it and see how it loads. The
skeleton will dissapear but you won't see any data because there's none
in the blockchain yet.

Let's change that. We're gonna create a function to upload words to our
global article. Here's how it looks like:

const uploadWords = async () => {
  const provider = new Provider(connection, wallet, {})
  const program = new Program(idl, programID, provider)
  try {
    await program.rpc.writeIntoArticle(inputValue, {
      accounts: {
      article: config.solana_article_account,
      },
    })
  } catch (e) {
    console.log('#2', e)
    return alert(e)
  }
  getAndSetArticle()
}
Enter fullscreen mode Exit fullscreen mode

Here's the breakdown:

  • First we setup the provider and program like we did before.
  • Then we do a try catch but this time we're executing the writeIntoArticle method which is exactly the function write_into_article in our Rust program.
  • Notice how we're passing as the first parameter the inputValue which is nothing more than what the user has typed in the input form we have setup.
  • Then we pass the article public key that we've generated before. That is nothing more but the address of the account that holds the article data.
  • At the end we update the data shown in the dapp by retrieving what's being stored in the blockchain with the getAndSetArticle() function.

What's left now, is to find the submit button and add a click event
listener that will call the uploadWords function we've just created:

<Button
  variant='contained'
  className='submit-button'
  onClick={uploadWords}
>
  Submit
</Button>
Enter fullscreen mode Exit fullscreen mode

Go ahead and try it out with yarn start! You'll see your final dapp
fully working, sending and receiving data from the Solana blockchain!
Isn't it awesome? For sure it is. Check it out and play around!

If you've liked it, be sure to join my new 3D interactive NFTs project Nuclei One

Top comments (4)

Collapse
 
4imble_93 profile image
4imble

Couple more issues for me, seems I need to ensure instances of ctx needed to be ctx: Context<Initialize> also return type needed to be -> Result<()> instead of ProgramResult.

The formatting about a 3rd of the way in of this blog goes a bit wrong.
Thanks for doing this though, I am learning something.

Collapse
 
4imble_93 profile image
4imble

This looked promising, but being a massive linux noob I really need things to be correct :D

The first command is wrong, I had to go to the Rust official docs (maybe that's the lesson here).
curl -- proto '=https' -- tlsv1.2 -sSf <https://sh.rustup.rs> | sh
should be
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Fingers cross for the rest of this, wish me luck haha.

Collapse
 
bancambios profile image
Ƀancambios

good input

Collapse
 
sumibisht profile image
Sumit Singh Bisht

how do i interact with an on chain anchor program from nodeJs ? please refer some source code.