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/anchor anchor-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!
Did you know I spent 2 weeks making this guide? I think it turned out to be great, make sure to give it 50 claps to this guide if you haven’t done it already, and share it with your friends it really helps!
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 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.
Pss, make sure to share this guide in your twitter, facebook or linkedin, your people will love it!
The first and most important file is the lib.rs
it’s the main one that will be loaded in the program.
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:
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,}
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 publicstruct
which makes it accessible for other functions and structs. Without it you’ll get an error sayingcan’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 itBook
or something similar if you’d like. - Then we create the
content
property which is aString
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>,
}
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](https://docs.rs/anchor-derive-accounts/0.18.2/anchor_derive_accounts/trait.Accounts.html)
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 aSigner
type. It’s the equivalent of setting up an owner in solidity, whileSigner
is the address type.pub system_program: Program<'info, System>,
: Thesystem_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
// 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:
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(” “); 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:
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(())
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 withVec::new()
. In this case we’re making itmut
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 thatsplit_iterator
is an iterator which means we gotta loop through it to access each item. We are doing that with a simplefor in
loop which stores each item into thes
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 simpleif
statement.if s.trim().len() >= 15 { return Err(Errors::WordTooLong.into()); }
: Here we’re checking if the word inside the variables
has 15 characters or more, in which case we return an error. In this case I’ve called the errorWordTooLong
. You’ll see later on how we create and define the error messages. TheErr
function is fromanchor
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 thefinal_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 spaceslet mut joined_words = final_words.join(” “);// Add a space at the end with thisjoined_words.push_str(” “);// Article content gets immediately updated*\*article.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
// 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:
- Run
solana config set --url localhost
, this will change the network you use to localhost instead of devnet. - 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. - Open your
Anchor.toml
file and update the[programs.devnet]
block to[programs.localnet]
- Then in that same file update
cluster = "devnet"
tocluster = "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:
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:
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 with createapp.dev, an awesome utility. This is a mac screen recording converted to gif with ffmpeg 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 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}
</>
)
}
}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
</>
)
}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 (
<>
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:
**
Open Global Book
By Merunas
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 (
{children}
)
}
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 forlocalhost
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 importPhantomWalletAdapter
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 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:
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 (
)
}
Then, in the App component add a wallet connect button in the header:
Open Global Book
By Merunas
**
**
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 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.
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 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’
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:
/>
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 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 receivesaccounts
andsigners
. 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 thedata
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
The solana_global_article.json
import you can get it from your target/deploy/<your-program-name>.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:
- Doo
yarn add node-polyfill-webpack-plugin
- In your webpack config add this:
const NodePolyfillPlugin = require(‘node-polyfill-webpack-plugin’)
- 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:
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 ? (
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 the
writeIntoArticle
method which is exactly the functionwrite_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
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!
Leave a Reply