Welcome to the most complete open guide for people that want to dive
deep into Solana.
Whether you're an Ethereum developer, a blockchain developer, a web
developer or just a curious person, this is the #1 guide to become a
Solana developer. With extremely detailed steps so there's no room for
confusion along the way.
By the end of this complete guide you'll have accomplished a working
dapp that can be deployed to the blockchain and interact with the real
Solana blockchain for extremely low fees and fast confirmation times.
Getting into Solana development can be extremely profitable since
there's already a huge market of investors, users, defi enthusiasts, NFT
maniacs, cryptocurrency trades... A massive network of people that will
pay big money for the right products.
And it's not nearly as saturated as in Ethereum since many tools and
products haven't been developed yet in Solana for a good reason:
learning Solana and Rust is far more difficult but rewarding, plus it's
a new blockchain that continues to grow tremendously fast!
I'm telling you, you'll find Rust hard at first but you'll love it by
the end of this article!
You'll learn a ton of cool things including:
- Writing Solana Rust programs from scratch (no experience required)
- Testing Solana applications with the Anchor framework
- Developing a frontend for your dapp step-by-step
- Connecting all the popular Solana wallets to your dapp
- Deploying your dapp so it's permanently online
I like doing things hands-on, I believe it's the best way to learn.
That's why in this guide you'll work through a dapp from an idea to its
complete deployment including all the blockchain coding and frontend
development as you learn along the way.
We're gonna build a dapp that allows users to write a collective
article. Imagine an open blockchain book where anybody can come in and
leave their little contribution to a grand story. It could end up a
cool book or a total disaster. In any case I'm excited to see how it
turns out!
Technically speaking, each wallet will be able to write 5 words to a
collective public article that lives permanently on the blockchain.
Kinda like a fun and open experimental game.
It's a simple dapp and has a few functions which makes it ideal for
people to start getting into Solana development.
If you're already familiar with the Solana Anchor setup already skip
directly to the section: 4. Coding the Rust program to see how
to start coding this dapp. Otherwise read from the beginning.
If you feel this guide helped you in any way, be sure to join my email
list for updated on things I'm building in crypto and NFTs here:
http://eepurl.com/dDQ2yX and subscribe to me here!
Each chapter contains a short bullet list of the things you'll learn to
get you hyped up and excited for the new knowledge you're about to
acquire.
Here's what you're gonna learn:
- Installing Rust, Solana, Yarn and Anchor
- Setting up the project from scratch
- Understanding the Anchor framework setup
- Coding the Rust program
- Creating the article update function
- Testing the Solana program
- Creating the decentralized application frontend
- 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/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!
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:
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 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 anAccounts
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) -> 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 the
context `WriteIntoArticle` and a String variable called `three_words`
which is the data our users will send to write into the global article.
Then we read the `article` data by accessting the `&mut
ctx.accounts.article` context variable.
Since we want people to send a string made of 3 words, we gotta split it
into separate units so we can check that each word is valid, meaning:
- Each word is made of less than 15 characters
- To remove all the extra empty spaces in between words
- To verify if the user actually sent 3 words or more
The `trim()` function will remove empty spaces between words while
`split(" ")` will separate words by spaces. Note that `split()` returns
an iterator. We can't access the data without iterating over it first or
`collect()` ing it.
Now let's iterate over those words to check it the user sent more words
than permitted since that's not allowed because we want multiple people
to contribute to this global article project. Add the following below
the `split_iterator` variable:
```rs
let split_iterator = three_words.trim().split(" ");
let mut final_words = Vec::new();
let mut counter_added = 0;
for s in split_iterator {
if s.trim().is_empty() {
continue;
}
if s.trim().len() >= 15 {
return Err(Errors::WordTooLong.into());
}
final_words.push(s);
counter_added += 1;
if counter_added >= 3 {
break;
}
}
Ok(())
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 spaces
let mut joined_words = final_words.join(" ");
// Add a space at the end with this
joined_words.push_str(" ");
// Article content gets immediately updated
article.content.push_str(&joined_words);
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 =
that's because the
article.content.push_str(&joined_words);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 =
with this object for it to work:
await program.rpc.initialize({})
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:
import { expect } from 'chai' // Keep this at the beginning of the file along with the other imports
it('Should write an article with 1 word successfully', async () => {
const deployerKeypair = anchor.web3.Keypair.generate()
const personThatPays = program.provider.wallet
// Add your test here
await program.rpc.initialize({
accounts: {
article: deployerKeypair.publicKey,
personThatPays: personThatPays.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
},
signers: [deployerKeypair],
})
await program.rpc.writeIntoArticle('hey', {
accounts: {
article: deployerKeypair.publicKey,
},
signers: [],
})
const articleData = await program.account.article.fetch(deployerKeypair.publicKey)
expect(articleData.content).to.equal('hey ') // Note the space at the end, added by the program
})
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:
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:
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 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:
<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 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:
<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 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/.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:
<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 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
</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
Top comments (4)
Couple more issues for me, seems I need to ensure instances of
ctx
needed to bectx: Context<Initialize>
also return type needed to be-> Result<()>
instead ofProgramResult
.The formatting about a 3rd of the way in of this blog goes a bit wrong.
Thanks for doing this though, I am learning something.
This looked promising, but being a massive linux noob I really need things to be correct :D
The first command is wrong, I had to go to the Rust official docs (maybe that's the lesson here).
curl -- proto '=https' -- tlsv1.2 -sSf <https://sh.rustup.rs> | sh
should be
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Fingers cross for the rest of this, wish me luck haha.
good input
how do i interact with an on chain anchor program from nodeJs ? please refer some source code.