Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
March 28, 2022 01:39 am GMT

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 experiencerequired)
  • 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

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"

Make sure the installation was successful by running:

rustup --version  rustc --version  cargo --version

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)"

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

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

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

npm i -g yarn

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/anchoranchor-cli --locked

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

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

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

solana-keygen new --force

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

Give yourself some test Solana coins with the airdrop command:

solana airdrop 2

You can check your balance anytime with:

solana balance

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

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 {}

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"

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 yourspecification
  • 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 wordson the global article
  • Up to 3 words per wallet but people can write less, so if someonejust 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 spacebetween 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,}

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 apublic struct which makes it accessible for other functions andstructs. Without it you'll get an error saying can't leak privatetype .
  • Next we named our struct Article simply because this will be thearticle where we'll store our data. You can name it Book orsomething similar if you'd like.
  • Then we create the content property which is a String that willcontain 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>,  }

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 deriveaccounts means: Implements an Accounts deserializer on the givenstruct. Meaning it allows this struct to process user addresses andaccounts. 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 iscalled Initialize and it has an 'info lifetime. Lifetimes are aRust thing that allow you to tell him to use a specific data fromsomewhere else. It's a way to pass variables. Don't worry if it'sconfusing 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 thesolana program to initialize an account where the data will bestored, then we define who's gonna pay for that transaction and thespace we need for that data.
  • pub article: Account<'info, Article> : Here we are telling solanato store the article in the new data account we've created for it tobe retrieved later.
  • #[account(mut)] pub person_that_pays: Signer<'info> : Wedefining the person that will pay to create the data account, whichis a Signer type. It's the equivalent of setting up an owner insolidity, while Signer is the address type.
  • pub system_program: Program<'info, System>, : The system_programis a required element to create your solana data. Must be includedin 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(())    }  }

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;

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

article_account.content = ("").to_string();

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

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

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

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

And a variable with the mut modifier:

let mut my_variable = 10;  my_variable = 5; // It works

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 %}

Ok(())

}

First we define the `write_into_article` function which receives thecontext `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 `&mutctx.accounts.article` context variable.Since we want people to send a string made of 3 words, we gotta split itinto 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 moreThe `trim()` function will remove empty spaces between words while`split(" ")` will separate words by spaces. Note that `split()` returnsan 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 wordsthan permitted since that's not allowed because we want multiple peopleto contribute to this global article project. Add the following belowthe `split_iterator` variable:```rslet 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(())

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 willcontain a list of the 3 words. Vectors are arrays of variable sizein Rust. You can push items to them. They have to be initializedwith Vec::new() . In this case we're making it mut able becausewe want to add items to it later.
  • let mut counter_added = 0; : This is a counter that will keeptrack of how many words we're adding to the list to stop at 3 andnot add more than necessary.
  • for s in split_iterator {} : Remember that split_iterator is aniterator which means we gotta loop through it to access each item.We are doing that with a simple for in loop which stores each iteminto the s variable.
  • if s.trim().is_empty() { continue; } : Here we're checking if theword is empty or not and if so, skip it. This is because when wesplit by spaces we may find that we have words separated by severalspaces. The split function then recognizes empty spaces as words, sowe 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 15characters or more, in which case we return an error. In this caseI've called the error WordTooLong . You'll see later on how wecreate and define the error messages. The Err function is fromanchor and it allows us to send and error and stop execution.
  • final_words.push(s); counter_added += 1; : Here we're simplyadding the word to the final_words vector after checking it isvalid 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 cutoff 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 spaceslet mut joined_words = final_words.join(" ");// Add a space at the end with thisjoined_words.push_str(" ");// Article content gets immediately updatedarticle.content.push_str(&joined_words);

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

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)  })})

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],    })})

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 thenetwork you use to localhost instead of devnet.
  2. Then do run the command solana-test-validator and stop it after afew seconds. If you keep it running your tests won't run, since itneeds 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)

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 () => {})

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],    })})

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: [],  })

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 ')

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'

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 importsit('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})

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)

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})

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)

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

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

Create a file called:

.gitignore

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"},

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

yarn add http-server

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

npm i -g npm-run-all

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'  },

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

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

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

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'

To:

import { Paper, Skeleton, TextField, Button} from '@mui/material'

Then install these ones:

yarn add @mui/material @emotion/react @emotion/styled

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>    </>  )}

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%

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',}),

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>

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>

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',    },  ],}),

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')

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',}

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

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> )}

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 walletconnections. This is only necessary for the Solflare, Sollet andSolletExtension wallets. For some reason the WalletAdapterNetworkdoesn't have an option for localhost networks. But that's fine, wecan work without it.
  • Then we create an array of wallets we are gonna use. Simply do anew for every wallet we've imported previously from the@solana/wallet-adapter-wallets library. You can just importPhantomWalletAdapter if you're not gonna use the others.
  • Then, in the return you gotta place those components in the orderI've shown you. Notice the {children} variable. That one isnecessary because our entire dapp will be placed there. The childrenvariable is a function argument as you can see at the beginningexport 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>

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'

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

export default () => { return (  <WalletContext>   <App/>  </WalletContext> )}

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>

That button comes from this library:

import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'

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 fornode.js core modules by default.  This is no longer the case. Verify if you need this module and configurea polyfill for it.

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

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',

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 throughyour 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'

Then create these variables inside the App component:

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

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()

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

import { useConnection, useAnchorWallet } from '@solana/wallet-adapter-react'

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  )}

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)}/>

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(' '))}

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)  }}

Here's the breakdown:

  • We create the provider which is the connection to the phantomwallet.
  • Then we setup the program to interact with it. As you can see weneed an IDL, programID and provider. We'll get those in a moment.
  • Then we create a new account keypair. This is the account where thedata of our program will be stored.
  • Next, we do a try catch to run the initialize function from theprogram methods. It receives accounts and signers . This is thesame structure we have in our program.
  • Accounts holds the person_that_pays which is the account that paysthe transaction fees and rent costs for 2 years to keep that dataonline.
  • The article variable is the account we've passed and is gonna beused for the data we're about to store. And the system program isjust the Solana main program.
  • After we're done, we log the account generated since we'll need itin 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

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>

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',

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)}

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])

Remember to import it at the top of your file:

import React, { useState, useEffect } from 'react'

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>)}

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()}

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 thewriteIntoArticle method which is exactly the functionwrite_into_article in our Rust program.
  • Notice how we're passing as the first parameter the inputValuewhich is nothing more than what the user has typed in the input formwe 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 thearticle data.
  • At the end we update the data shown in the dapp by retrieving what'sbeing 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>

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


Original Link: https://dev.to/merlox/the-ultimate-solana-step-by-step-guide-including-programs-dapps-and-rust-from-scratch-46ai

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To