DEV Community

Cover image for The Complete Guide to Full Stack Solana Development with React, Anchor, Rust, and Phantom
Nader Dabit for Edge and Node

Posted on • Updated on

The Complete Guide to Full Stack Solana Development with React, Anchor, Rust, and Phantom

Building Full Stack dapps with React, Solana, Anchor, and Phantom wallet.

In The Complete Guide to Full Stack Ethereum Development I did a deep dive into how to build a full stack dapp on Ethereum, which can also be applied to other EVM compatible chains like Polygon, Avalanche, and Ethereum Layer 2's like Arbitrum.

In this guide, I want to dive into Solana to show you how to build a full stack dapp. I also want to introduce the the ecosystem and the developer tooling to you to hopefully help you get up and running building your own ideas and applications going forward.

The code for the project is located here

Solana developer overview

As someone who just started learning Solidity and its ecosystem about 6 months ago, I kind of assumed it couldn't be that much harder to get up and running with. I was wrong.

Parts of the developer tooling are really nice and polished (the Solana CLI and Anchor), while the rest of the ecosystem, and even the documentation for Anchor (which to be fair, is very new), leaves a decent amount to be desired.

That being said, once you get the hang of everything it quickly becomes much easier to understand how to start implementing your own ideas and begin experimenting.

One of the keys to finding answers is to be vigilant about searching across all of Google, Github, and especially the various Discord servers for Anchor and Solana. The developers in those channels have been extremely helpful, especially Armani Ferrante who created the Anchor framework. Get familiar with the search feature, you can often find answers to your questions in past discussions in Discord.

Project overview

The tooling we'll be using today includes:

Solana Tool Suite - This includes a really polished and well documented CLI for interacting with the Solana network.

Anchor Framework - Anchor is actually a life saver for me, and I'm almost certain I would not have been able to get over the hump of building anything without it. It is the Hardhat of Solana development and more, and I love it. It also offers a DSL on top of Rust so that you do not need a deep understanding of the language to get started, though I am still trying to learn Rust as it will probably be useful to build anything non-trivial, even with the DSL. A good free place to learn Rust is The Rust Book.

solana/web3.js - A Solana version of web3.js that seems to work pretty well, but the documentation was almost unusable for me

React - The client-side framework

I will leave off all of the in depth details around how Solana itself works, as other people can cover this better than me. Instead I'll try to just focus on building something and sharing the details that you need to be know to accomplish this, along with things I think are of utmost importance.

If you want to learn more about Solana and how it works, here are a few good pieces:

In this guide we'll focus mainly on project setup, testing, and front end client integration for building out a couple of types of applications, mainly focused on CRUD operations (without the delete of course), which I found to be somewhat undocumented (integration with client applications).

We'll also learn how to airdrop tokens to our own development accounts using the Solana CLI, and deploy our apps to both a local network as well as a live test network.

We won't be focusing on NFTs in this guide, but maybe I will focus on doing that in a future guide. For now, if you're interested in building an NFT marketplace on Solana, I recommend checking out Metaplex.

Prerequisites

This tutorial covers how to build a full stack app on Solana, but does not go into how to install all of the individual dependencies.

Instead, I will list the dependencies and link to the documentation for how to install them, as each project will be able to explain and document these things better than I ever could, as well as keep them up to date.

  1. Node.js - I recommend installing Node using either nvm or fnm

  2. Solana Tool Suite - You can see the installation instructions here. note - If you have any issues installing Solana on an M1 Mac, try building from source and check out this guide.

  3. Anchor (including the Mocha installation) - Anchor installation was pretty straight-forward for me. You can find the installation instructions here.

  4. Solana browser wallet - I recommend Phantom, which is what I have tested this app with.

Getting Started

Before we get started building, let's take a look at the Solana CLI.

Solana CLI

The main things we'll be doing with the Solana CLI will be configuring our network (between localhost and a developer testnet) as well as airdropping tokens into our wallets, pretty much everything else we'll be doing with the Anchor CLI.

For instance, we can check the current network (and other) configuration with this command:

solana config get

# output
Config File: /Users/user/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/user/.config/solana/id.json
Commitment: confirmed
Enter fullscreen mode Exit fullscreen mode

If you do not have a Keypair path, set one up by following the instructions here

We can change the network like so:

# set to localhost
solana config set --url localhost

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

This is important as you will need to be aware of which network you are using as you're building, testing, and deploying your programs. You also need to be sure your wallet is using the same network that your local environment is using when testing, something I'll cover.

We'll be starting by developing on a localhost network, then switching to the devnet network.

We can also use the CLI to see our current local wallet address:

solana address
Enter fullscreen mode Exit fullscreen mode

And then get the full details about an account:

solana account <address from above>
Enter fullscreen mode Exit fullscreen mode

Next let's airdrop some tokens. To do so, first switch to the local network, as this is where we will be working to start off with:

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

Next, start the local network. This is going to be a local Solana node that we can deploy to for testing:

solana-test-validator
Enter fullscreen mode Exit fullscreen mode

Once the local network is running, you can airdrop tokens to your account. With the network running, open a separate window and run the following command:

solana airdrop 100
Enter fullscreen mode Exit fullscreen mode

You can the check the balance of your wallet:

solana balance

# or

solana balance <address>
Enter fullscreen mode Exit fullscreen mode

You should now have a balance 100 SOL in your wallet. With that, we can get started building.

Let's start building

To get started, initialize a new anchor project and change into the new directory:

anchor init mysolanaapp --javascript

cd mysolanaapp
Enter fullscreen mode Exit fullscreen mode

Be sure to use Anchor version 0.16.0 or later.

In this project, you'll see four main folders (in addition to the node_modules):

app - Where our frontend code will go

programs - This is where the Rust code lives for the Solana program

test - Where the JavaScript tests for the program live

migrations - A basic deploy script

Let's take a look at the program that was created for us.

Anchor uses, and enables us to write, an eDSL (embedded DSL) that abstracts away many of the more complex low level operations you'd typically need to do if you were using Solana & Rust without it, making it more approachable for me.

// programs/src/lib.rs
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

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

This is probably the most basic program you can write. The only thing happening here is we are defining a function called initialize, which when invoked just exits the program successfully. There is no data manipulation at all.

The Initialize struct defines the context as being empty of any arguments. We'll learn more about the function context later.

To compile this program, we can run the Anchor build command:

anchor build
Enter fullscreen mode Exit fullscreen mode

Once a build is completed, you should see a new folder named target.

One of the artifacts created is an IDL located at target/idl/mysolanaapp.json.

IDLs are very similar to an ABI in Solidity (or a query definition in GraphQL), and we will be using them in a similar way in our JavaScript tests and frontends to communicate with our Solana program via RPC.

We can also test out our program. If you open tests/mysolanaapp.js, you will see that there is a test written in JavaScript that allows us to test out the program.

The test should look like this:

const anchor = require('@project-serum/anchor');

describe('mysolanaapp', () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.Provider.env());

  it('Is initialized!', async () => {
    const program = anchor.workspace.Mysolanaapp;
    const tx = await program.rpc.initialize();
    console.log("Your transaction signature", tx);
  });
});
Enter fullscreen mode Exit fullscreen mode

There are a couple of things to learn from this test that are important and we'll be using in the future, both in our tests as well as in the front end JavaScript clients.

To call a Solana program using Anchor, we typically need two main things:

1. Provider - The Provider is an abstraction of a connection to the Solana network, typically consisting of a Connection, Wallet, and a preflight commitment.

In the test, the Anchor framework will create the provider for us based on the environment (anchor.Provider.env()), but on the client we will need to construct the Provider ourselves using the user's Solana wallet.

2. program - The program is an abstraction that combines the Provider, idl, and the programID (which is generated when the program is built) and allows us to call RPC methods against our program.

Again, like with the Provider, Anchor offers a convenient way to access the program, but when building out the front end we'll need to construct this provider ourselves.

Once we have these two things, we can start calling functions in our program. For example, in our program we have an initialize function. In our test, you'll see we can invoke that function directly using program.rpc.functionName:

const tx = await program.rpc.initialize();
Enter fullscreen mode Exit fullscreen mode

This is a very common pattern that you'll use a lot when working with Anchor, and once you get the hang of understanding how it works, makes it really easy to connect to and interact with a Solana program.

We can now test the program by running the test script:

anchor test
Enter fullscreen mode Exit fullscreen mode

Building Hello World

Now that we have our project setup, let's create something a little more interesting.

I know that, as a full stack developer, most of the time I'm wondering how to do CRUD types of operations, so that's what we'll look at next.

The first program we'll create will allow us to create a counter that increments every time we call it from a client application.

The first thing we need to do is open programs/mysolanaapp/src/lib.rs and update it with the following code:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
mod mysolanaapp {
    use super::*;

    pub fn create(ctx: Context<Create>) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count += 1;
        Ok(())
    }
}

// Transaction instructions
#[derive(Accounts)]
pub struct Create<'info> {
    #[account(init, payer = user, space = 16 + 16)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program <'info, System>,
}

// Transaction instructions
#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

// An account that goes inside a transaction instruction
#[account]
pub struct BaseAccount {
    pub count: u64,
}
Enter fullscreen mode Exit fullscreen mode

In this program we have two functions - create and increment. These two functions are the RPC request handlers that we will be able to call from a client application to interact with the program.

The first parameter of an RPC handler is the Context struct, which describes the context that will be passed in when the function is called and how to handle it. In the case of Create, we are expecting three parameters: base_account, user, and system_program.

The #[account(...)] attributes define constraints and instructions that are related to the proceeding account where declared. If any of these constraints do not hold, then the instruction will never execute.

Any client calling this program with the proper base_account can call these RPC methods.

The way that Solana deals with data is much different than anything I've ever worked with. There is no persisted state within the program, everything is attached to what are known as accounts. An account essentially holds all of the state of a program. Because of this, all data is passed by reference from the outside.

There are also no read operations. This is because all you need to do to read the contents of a program is to request the account, from there you are able to view all of the program's state. To read more about how accounts work, check out this post.

To build the program:

anchor build
Enter fullscreen mode Exit fullscreen mode

Next, let's write a test that uses this counter program. To do so, open tests/mysolanaapp.js and update with the following code:

const assert = require("assert");
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;

describe("mysolanaapp", () => {
  /* create and set a Provider */
  const provider = anchor.Provider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.Mysolanaapp;
  it("Creates a counter)", async () => {
    /* Call the create function via RPC */
    const baseAccount = anchor.web3.Keypair.generate();
    await program.rpc.create({
      accounts: {
        baseAccount: baseAccount.publicKey,
        user: provider.wallet.publicKey,
        systemProgram: SystemProgram.programId,
      },
      signers: [baseAccount],
    });

    /* Fetch the account and check the value of count */
    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('Count 0: ', account.count.toString())
    assert.ok(account.count.toString() == 0);
    _baseAccount = baseAccount;

  });

  it("Increments the counter", async () => {
    const baseAccount = _baseAccount;

    await program.rpc.increment({
      accounts: {
        baseAccount: baseAccount.publicKey,
      },
    });

    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('Count 1: ', account.count.toString())
    assert.ok(account.count.toString() == 1);
  });
});
Enter fullscreen mode Exit fullscreen mode

Before we continue to test and deploy the program, we want to get the dynamically generated Program ID that was generated by the build. We need this ID to use in the Rust program to replace the placeholder ID we set up when we created the project. To get this ID, we can run the following command:

solana address -k target/deploy/mysolanaapp-keypair.json
Enter fullscreen mode Exit fullscreen mode

We can now update the program IDs in lib.rs:

// mysolanaapp/src/lib.rs

declare_id!("your-program-id");
Enter fullscreen mode Exit fullscreen mode

And in Anchor.toml:

# Anchor.toml
[programs.localnet]
mysolanaapp = "your-program-id"
Enter fullscreen mode Exit fullscreen mode

Next, run the test:

anchor test
Enter fullscreen mode Exit fullscreen mode

Once the test passes, we can now deploy.

We can now deploy the program. Be sure that solana-test-validator is running:

anchor deploy
Enter fullscreen mode Exit fullscreen mode

You can also view the validator logging by opening a separate window and running solana logs

Now we're ready to build out the front end.

Building the React app

In the root of the Anchor project, create a new react app to overwrite the existing app directory:

npx create-react-app app
Enter fullscreen mode Exit fullscreen mode

Next, install the dependencies we'll need for Anchor and Solana Web3:

cd app

npm install @project-serum/anchor @solana/web3.js
Enter fullscreen mode Exit fullscreen mode

We'll also be using Solana Wallet Adapter to handle connecting the user's Solana wallet. Let's install those dependencies as well:

npm install @solana/wallet-adapter-react \
@solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets \
@solana/wallet-adapter-base
Enter fullscreen mode Exit fullscreen mode

Next, in the src directory, create a new file named idl.json. Here, copy the IDL JSON that was created for you in the main project folder, located at target/idl/mysolanaapp.json.

It would be nice if we could copy this idl file automatically to our client application src folder, but as of now I have not found a way to do this natively. You can of course create your own script that does this if you'd like, or else you need to copy and paste over the IDL after every change to your main program.

If you want a script like this, you can do it in just a couple of lines of code:

// copyIdl.js
const fs = require('fs');
const idl = require('./target/idl/mysolanaapp.json');

fs.writeFileSync('./app/src/idl.json', JSON.stringify(idl));
Enter fullscreen mode Exit fullscreen mode

Next, open app/src/App.js and update it with the following:

import './App.css';
import { useState } from 'react';
import { Connection, PublicKey } from '@solana/web3.js';
import {
  Program, Provider, web3
} from '@project-serum/anchor';
import idl from './idl.json';

import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';
import { useWallet, WalletProvider, ConnectionProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
require('@solana/wallet-adapter-react-ui/styles.css');

const wallets = [
  /* view list of available wallets at https://github.com/solana-labs/wallet-adapter#wallets */
  new PhantomWalletAdapter()
]

const { SystemProgram, Keypair } = web3;
/* create an account  */
const baseAccount = Keypair.generate();
const opts = {
  preflightCommitment: "processed"
}
const programID = new PublicKey(idl.metadata.address);

function App() {
  const [value, setValue] = useState(null);
  const wallet = useWallet();

  async function getProvider() {
    /* create the provider and return it to the caller */
    /* network set to local network for now */
    const network = "http://127.0.0.1:8899";
    const connection = new Connection(network, opts.preflightCommitment);

    const provider = new Provider(
      connection, wallet, opts.preflightCommitment,
    );
    return provider;
  }

  async function createCounter() {    
    const provider = await getProvider()
    /* create the program interface combining the idl, program ID, and provider */
    const program = new Program(idl, programID, provider);
    try {
      /* interact with the program via rpc */
      await program.rpc.create({
        accounts: {
          baseAccount: baseAccount.publicKey,
          user: provider.wallet.publicKey,
          systemProgram: SystemProgram.programId,
        },
        signers: [baseAccount]
      });

      const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
      console.log('account: ', account);
      setValue(account.count.toString());
    } catch (err) {
      console.log("Transaction error: ", err);
    }
  }

  async function increment() {
    const provider = await getProvider();
    const program = new Program(idl, programID, provider);
    await program.rpc.increment({
      accounts: {
        baseAccount: baseAccount.publicKey
      }
    });

    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('account: ', account);
    setValue(account.count.toString());
  }

  if (!wallet.connected) {
    /* If the user's wallet is not connected, display connect wallet button. */
    return (
      <div style={{ display: 'flex', justifyContent: 'center', marginTop:'100px' }}>
        <WalletMultiButton />
      </div>
    )
  } else {
    return (
      <div className="App">
        <div>
          {
            !value && (<button onClick={createCounter}>Create counter</button>)
          }
          {
            value && <button onClick={increment}>Increment counter</button>
          }

          {
            value && value >= Number(0) ? (
              <h2>{value}</h2>
            ) : (
              <h3>Please create the counter.</h3>
            )
          }
        </div>
      </div>
    );
  }
}

/* wallet configuration as specified here: https://github.com/solana-labs/wallet-adapter#setup */
const AppWithProvider = () => (
  <ConnectionProvider endpoint="http://127.0.0.1:8899">
    <WalletProvider wallets={wallets} autoConnect>
      <WalletModalProvider>
        <App />
      </WalletModalProvider>
    </WalletProvider>
  </ConnectionProvider>
)

export default AppWithProvider;
Enter fullscreen mode Exit fullscreen mode

Switching your wallet network

Before we can interact with a program on the localhost network, we must switch our Phantom wallet to the proper network.

To do so, open your Phantom wallet and click the settings button. Then scroll down to Change Network:

Updating network

Next, choose Localhost:

Choosing localhost

Now we need to airdrop tokens to this wallet. At the top of the wallet interface, click on your address to copy it to your clipboard.

Wallet address

Next, open your terminal and run this command (be sure solana-test-validator is running):

solana airdrop 10 <address>
Enter fullscreen mode Exit fullscreen mode

You should now have 10 tokens in your wallet. Now, we can run and test the app!

Change into the app directory and run the following command:

npm start
Enter fullscreen mode Exit fullscreen mode

You should be able to connect your wallet, create a counter, and increment it.

You'll notice that when you refresh, you lose the state of the program. This is because we are dynamically generating the base account when the program loads. If you wanted to read and interact with the program data across various clients, you would need to create and store the Keypair somewhere in your project. I've put together a gist of a naive approach of how this might look.

Hello World part 2

Let's create a variation of this program that, instead of dealing with a counter, allows us to create a message and keep track of all of the previously created messages.

To do so, let's update our Rust program to look like this:

/* programs/mysolanaapp/src/lib.rs */
use anchor_lang::prelude::*;

declare_id!("your-program-id");

#[program]
mod mysolanaapp {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, data: String) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        let copy = data.clone();
        base_account.data = data;
        base_account.data_list.push(copy);
        Ok(())
    }

    pub fn update(ctx: Context<Update>, data: String) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        let copy = data.clone();
        base_account.data = data;
        base_account.data_list.push(copy);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 64 + 64)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

#[account]
pub struct BaseAccount {
    pub data: String,
    pub data_list: Vec<String>,
}
Enter fullscreen mode Exit fullscreen mode

In this program we have two main pieces of data we're keeping track of, a String named data and a Vector that holds a list of all data ever added to the program named data_list.

You'll notice that the memory allocation here is higher (128 + 128) than the previous program in order to account for the Vector. I don't know how many updates you'd be able to store in this program as is, but may be something to investigate more or experiment with, as this example in and of itself is experimental and just to give you an understanding of how things work.

Next, we can update the test for this new program:

const assert = require("assert");
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;

describe("Mysolanaapp", () => {
  const provider = anchor.Provider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.Mysolanaapp;
  it("It initializes the account", async () => {
    const baseAccount = anchor.web3.Keypair.generate();
    await program.rpc.initialize("Hello World", {
      accounts: {
        baseAccount: baseAccount.publicKey,
        user: provider.wallet.publicKey,
        systemProgram: SystemProgram.programId,
      },
      signers: [baseAccount],
    });

    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('Data: ', account.data);
    assert.ok(account.data === "Hello World");
    _baseAccount = baseAccount;

  });

  it("Updates a previously created account", async () => {
    const baseAccount = _baseAccount;

    await program.rpc.update("Some new data", {
      accounts: {
        baseAccount: baseAccount.publicKey,
      },
    });

    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('Updated data: ', account.data)
    assert.ok(account.data === "Some new data");
    console.log('all account data:', account)
    console.log('All data: ', account.dataList);
    assert.ok(account.dataList.length === 2);
  });
});
Enter fullscreen mode Exit fullscreen mode

To test it out:

anchor test
Enter fullscreen mode Exit fullscreen mode

If the test fails, try turning off the validator and then running again.

Next, let's update the client.

/* app/src/App.js */
import './App.css';
import { useState } from 'react';
import { Connection, PublicKey } from '@solana/web3.js';
import { Program, Provider, web3 } from '@project-serum/anchor';
import idl from './idl.json';

import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';
import { useWallet, WalletProvider, ConnectionProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
require('@solana/wallet-adapter-react-ui/styles.css');

const wallets = [ new PhantomWalletAdapter() ]

const { SystemProgram, Keypair } = web3;
const baseAccount = Keypair.generate();
const opts = {
  preflightCommitment: "processed"
}
const programID = new PublicKey(idl.metadata.address);

function App() {
  const [value, setValue] = useState('');
  const [dataList, setDataList] = useState([]);
  const [input, setInput] = useState('');
  const wallet = useWallet()

  async function getProvider() {
    /* create the provider and return it to the caller */
    /* network set to local network for now */
    const network = "http://127.0.0.1:8899";
    const connection = new Connection(network, opts.preflightCommitment);

    const provider = new Provider(
      connection, wallet, opts.preflightCommitment,
    );
    return provider;
  }

  async function initialize() {    
    const provider = await getProvider();
    /* create the program interface combining the idl, program ID, and provider */
    const program = new Program(idl, programID, provider);
    try {
      /* interact with the program via rpc */
      await program.rpc.initialize("Hello World", {
        accounts: {
          baseAccount: baseAccount.publicKey,
          user: provider.wallet.publicKey,
          systemProgram: SystemProgram.programId,
        },
        signers: [baseAccount]
      });

      const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
      console.log('account: ', account);
      setValue(account.data.toString());
      setDataList(account.dataList);
    } catch (err) {
      console.log("Transaction error: ", err);
    }
  }

  async function update() {
    if (!input) return
    const provider = await getProvider();
    const program = new Program(idl, programID, provider);
    await program.rpc.update(input, {
      accounts: {
        baseAccount: baseAccount.publicKey
      }
    });

    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('account: ', account);
    setValue(account.data.toString());
    setDataList(account.dataList);
    setInput('');
  }

  if (!wallet.connected) {
    return (
      <div style={{ display: 'flex', justifyContent: 'center', marginTop:'100px' }}>
        <WalletMultiButton />
      </div>
    )
  } else {
    return (
      <div className="App">
        <div>
          {
            !value && (<button onClick={initialize}>Initialize</button>)
          }

          {
            value ? (
              <div>
                <h2>Current value: {value}</h2>
                <input
                  placeholder="Add new data"
                  onChange={e => setInput(e.target.value)}
                  value={input}
                />
                <button onClick={update}>Add data</button>
              </div>
            ) : (
              <h3>Please Inialize.</h3>
            )
          }
          {
            dataList.map((d, i) => <h4 key={i}>{d}</h4>)
          }
        </div>
      </div>
    );
  }
}

const AppWithProvider = () => (
  <ConnectionProvider endpoint="http://127.0.0.1:8899">
    <WalletProvider wallets={wallets} autoConnect>
      <WalletModalProvider>
        <App />
      </WalletModalProvider>
    </WalletProvider>
  </ConnectionProvider>
)

export default AppWithProvider;    
Enter fullscreen mode Exit fullscreen mode

Next, build and deploy the program (be sure that solana-test-validator is running):

anchor build

anchor deploy
Enter fullscreen mode Exit fullscreen mode

With the new build you'll have a new IDL that you'll need to update for your client. Either copy over the new IDL to app/src/idl.json or run your copyIdl.js script.

Testing it out

When testing out the new program, be sure to update the idl.json file that was created by the build.

Change into the app directory and run the start command:

npm start
Enter fullscreen mode Exit fullscreen mode

Deploying to Devnet

Deploying to a live network is pretty straightforward from here. The main things we need to do are:

1. Update the Solana CLI to use devnet:

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

2. Update Phantom wallet to use devnet

3. Open Anchor.toml and update the cluster from localnet to devnet.

4. Rebuild the program. Be sure the program ID in Anchor.toml matches the current program ID.

5. Deploy the program again, this time it will be deployed to devnet

6. In app/src/App.js, we need to also update the network, this time using the clusterApiUrl from @solana/web3, like this:

/* before */
<ConnectionProvider endpoint="http://127.0.0.1:8899">

/* after */
import {
  ...,
  clusterApiUrl
} from '@solana/web3.js';

const network = clusterApiUrl('devnet');

<ConnectionProvider endpoint={network}>
Enter fullscreen mode Exit fullscreen mode

From here, you should be able to deploy and test as we have done the the previous steps.

The code for this project is located here

Next steps

Another in depth tutorial I'd suggest checking out next is Create a Solana dApp from scratch which implements a simplified version of Twitter as a Solana dapp.


If you're interested in working with technology like this full time, come join me and my team at Edge & Node, we're hiring!

Top comments (129)

Collapse
 
danmt profile image
Daniel Marin

Really nice read,

Just one tiny thing, when using Anchor it's recommended to use useAnchorWallet hook instead of useWallet. Some wallets don't support signing txs, if you provide such a wallet to the Provider constructor you'll get runtime errors.

I guess you're aware of that, just in case somebody has issues.

Collapse
 
haodev007 profile image
Holmes

Hi, Daniel.

If I use useAnchorWallet instead of useWallet, can I get AnchorError via any solana wallets?
When I have called rpc api from solana program on Frontend, I need to get AnchorError in try-catch cause.
But I can see AnchorError on browser console(via solfare wallet) but I cannot catch the error. Any thoughts?

Thanks! Have a nice day~

Collapse
 
alfonzomillions profile image
Alfonzo

Great post!

One thing though in the 1st test, you will need to declare the variable "_baseAccount" before you can assign it.

just add "let _baseAccount" in the describe test block.

Also I had to turn off the solana-test-validator for the tests to pass for some reason. Just thought I'd post just incase anyone ran into a similar issue!

Collapse
 
danmt profile image
Daniel Marin

I believe this happens because Anchor tests spin up a test validator instance. I know somewhere in the Anchor tutorial docs it's explained. Just make sure to turn off the test validator when running the test suite and you should be ready to go.

Collapse
 
marcinkozuchowski profile image
Marcin Kożuchowski

man, saved my life!

Collapse
 
breadandwater profile image
Adrian Paniagua Sanchez

You man made my day! Thanks a lot, I was trying to figure out why the tests are getting errors and errors... Adding this line of code and also stopping the solana-test-validator made my test work! Thanks!

Collapse
 
saxenism profile image
Rahul Saxena

You could also go for anchor test --skip-local-validator

Collapse
 
mrslappy profile image
mrslappy

Thankyou, this had me fcked for ages

Collapse
 
3lii06 profile image
Ali ₿

thx for posting appreciate it ran into the same issues XD

Collapse
 
hypeman profile image
hype • Edited

If you end up with

Module not found: Error: Can't resolve './adapter' in '/Users/me/Desktop/mysolanaapp/app/node_modules/@solana/wallet-adapter-bitkeep/lib'
Did you mean 'adapter.js'?
BREAKING CHANGE: The request './adapter' failed to resolve only because it was resolved as fully specified
(probably because the origin is strict EcmaScript Module, e. g. a module with javascript mimetype, a '*.mjs' file, or a '*.js' file where the package.json contains '"type": "module"').
The extension in the request is mandatory for it to be fully specified.
Add the extension to the request." 
Enter fullscreen mode Exit fullscreen mode

Error, its probably because of create-react-app using "react-scripts": "5.0.0", in order to fix - downgrade to "react-scripts": "^4.0.3" or do manual webpack hacking
source: giters.com/solana-labs/wallet-adap...
and github.com/reduxjs/redux/issues/4174

Collapse
 
mcintyre94 profile image
mcintyre94

This Alchemy tutorial is a really good guide to the manual webpack hacking: alchemy.com/blog/how-to-polyfill-n...

You only need these overrides:

Object.assign(fallback, {
    "crypto": require.resolve("crypto-browserify"),
    "stream": require.resolve("stream-browserify"),
  })
Enter fullscreen mode Exit fullscreen mode
Collapse
 
shoaib7889 profile image
Muhammad Shoaib

Saved

Collapse
 
walkre profile image
walkre • Edited

I changed "react-scripts": "5.0.0" into "react-scripts": "4.0.3" and I got the following error at Building the React app when I run npm start.
Anyone could help me?

Failed to compile.

./node_modules/@solana/wallet-adapter-base/lib/esm/signer.js 16:16
Module parse failed: Unexpected token (16:16)
File was processed with these loaders:
 * ./node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|           ...sendOptions
|         } = options;
>         signers?.length && transaction.partialSign(...signers);
|         transaction = await this.signTransaction(transaction);
|         const rawTransaction = transaction.serialize();
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mengni profile image
How

I followed the dowgrade instruction for all dependencies, but still got the following error:

Failed to compile.

./src/App.js
Attempted import error: 'PhantomWalletAdapter' is not exported from '@solana/wallet-adapter-wallets'.

Could someone help with this? Thanks!

Collapse
 
jdguzmans profile image
jdguzmans

@walkre I am having the same error, did you solve this?

Thread Thread
 
walkre profile image
walkre

Sadly, no.

Thread Thread
 
jdguzmans profile image
jdguzmans

with these dependencies it worked for me
...
"@solana/wallet-adapter-base": "^0.7.1",
"@solana/wallet-adapter-react": "^0.13.1",
"@solana/wallet-adapter-react-ui": "^0.6.1",
"@solana/wallet-adapter-wallets": "^0.11.3",
"@solana/web3.js": "^1.31.0",
...
"react-scripts": "4.0.3",

Thread Thread
 
walkre profile image
walkre

That worked for me too.
Thank you very much!!!

Collapse
 
chillnarmadillo profile image
chillnArmadillo

Cheers mate! Made my day.

Collapse
 
khrysaoar profile image
Khrysaoar • Edited

This is really nice for developers like me who are new to blockchain apps development.
It's a very informative tutorial!

But I'm currently having trouble at the first Hello World build.
When everything is set and I run npm start inside the mysolanaapp/app folder, I started getting lots of error in regards to the @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @solana/wallet-adapter-base

Example of one error is:
ERROR in ./src/App.js 20:0-16
export 'getPhantomWallet' (imported as 'getPhantomWallet') was not found in '@solana/wallet-adapter-wallets' (module has no exports)

Collapse
 
khrysaoar profile image
Khrysaoar

I solved this by running npm install inside the mysolanaapp\app before running npm start. Thank you so much!!

Collapse
 
chillnarmadillo profile image
chillnArmadillo

Awesome tutorial!

Still having the same issue though as described by @khrysaoar.
The npm install did not get the job done for me.

Hints anyone?
Thank you very much!

Thread Thread
 
chillnarmadillo profile image
chillnArmadillo

Resolved as mentioned by @hypeman.
Changed "react-scripts": "5.0.0" to "react-scripts": "4.0.3"

Thread Thread
 
0cv profile image
Christophe Vidal

thank you it helped! And then I faced the next error with BitKeepWalletAdapter which could not be imported. The solution was to downgrade the following dependencies:

...
    "@solana/wallet-adapter-base": "^0.7.1",
    "@solana/wallet-adapter-react": "^0.13.1",
    "@solana/wallet-adapter-react-ui": "^0.6.1",
    "@solana/wallet-adapter-wallets": "^0.11.3",
    "@solana/web3.js": "^1.31.0",
...
Enter fullscreen mode Exit fullscreen mode

it feels like Solana tooling is breaking dependencies on a weekly basis :-(

Collapse
 
khrysaoar profile image
Khrysaoar

@dabit3 Hi, I hope you can help me or someone. Thank you so much!

Collapse
 
ahmedali profile image
Ahmed Ali

One of the most useful Solana related tutorials. Thank you!
I have couple of questions though:

  • At this point I am getting an AccountNotProgramOwned error which I can understand why, but I don't know how to fix
  • How can we change the program so that it maintains the state between browser sessions? i.e if I close the browser, re-open and connect my wallet, it would restore my last state instead of starting from scratch?
Collapse
 
egemengol profile image
Egemen Göl

Have you managed to solve the AccountNotProgramOwned error? I seem to stuck on that error as well.

Collapse
 
thalesbmc profile image
Thales Brederodes

local storage.

Collapse
 
0cv profile image
Christophe Vidal • Edited

sounds like a terrible idea for anything beyond a demo. What if the user clears his cookies or use another browser, then he loses his account data and starts from new? I could imagine that a base account could be generated from the public key of the provider/user to have something deterministic which can be safely retrieved and reused

EDIT: the method PublicKey.createWithSeed based on the user address, the program ID and a deterministic storage key that can be hardcoded in the front end app, is actually the solution to this

Thread Thread
 
thalesbmc profile image
Thales Brederodes

I agree, just for demo purpose. Thanks for sharing the right way.

Collapse
 
zhbadya profile image
Roman Zhbadynskyi

Very clear tutorial, thanks!
Need some help.
I've follow 'Building Hello World' part and deployed program to localhost cluster.
After frontend calling program.rpc.create method and Phantom wallet approving I recieve

Transaction simulation failed: Error processing Instruction 0: custom program error: 0xa7

Account have enought balance, any suggestions?

Collapse
 
phoenixdevguru profile image
Legendary Phoenix • Edited

I am also getting this issue. I updated the lib.rs and Anchor.toml files with programID.

Here are the steps I've done.

anchor build
Enter fullscreen mode Exit fullscreen mode

And this returned the programID in the form of public key.

solana address -k target/deploy/example2-keypair.json
>BEPhkwFSf3m5Daz5LZPZhBbeVBkvY3vKnGRiWtNwFn54
Enter fullscreen mode Exit fullscreen mode

Replaced the program id in lib.rs and Anchor.toml

// lib.rs
use anchor_lang::prelude::*;

declare_id!("BEPhkwFSf3m5Daz5LZPZhBbeVBkvY3vKnGRiWtNwFn54");

#[program]
mod example2 {
    use super::*;

... ...
Enter fullscreen mode Exit fullscreen mode
// Anchor.toml
[programs.localnet]
example2 = "BEPhkwFSf3m5Daz5LZPZhBbeVBkvY3vKnGRiWtNwFn54"

[registry]
url = "https://anchor.projectserum.com"

[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "mocha -t 1000000 tests/"
Enter fullscreen mode Exit fullscreen mode
anchor deploy
Enter fullscreen mode Exit fullscreen mode

I already checked the programID I am using in the app, and it matches to above one.

// App.js
  async function initialize() {    
    const provider = await getProvider();
    /* create the program interface combining the idl, program ID, and provider */
    const program = new Program(idl, programID, provider);
    console.log(program.programId.toBase58());
Enter fullscreen mode Exit fullscreen mode

But I am still getting this issue when I am trying to call the initialize() function.

Transaction simulation failed: Error processing Instruction 0: custom program error: 0xa7 
    Program BEPhkwFSf3m5Daz5LZPZhBbeVBkvY3vKnGRiWtNwFn54 invoke [1]
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Program log: Custom program error: 0xa7
    Program BEPhkwFSf3m5Daz5LZPZhBbeVBkvY3vKnGRiWtNwFn54 consumed 5512 of 200000 compute units
    Program BEPhkwFSf3m5Daz5LZPZhBbeVBkvY3vKnGRiWtNwFn54 failed: custom program error: 0xa7
Enter fullscreen mode Exit fullscreen mode

I am really not sure why this happens and it's bothering me for days. Please help me if you have any idea.

Collapse
 
proto133 profile image
Peter Roto

Did you fund the wallet?

Collapse
 
halimabergaoui profile image
halimabergaoui

Hey, same problem here. did you solve this?

Collapse
 
zhbadya profile image
Roman Zhbadynskyi

I've carefully checked everything step-by-step and it works. I think my mistake was in

solana address -k target/deploy/mysolanaapp-keypair.json
Enter fullscreen mode Exit fullscreen mode

cause of different app name

Collapse
 
ebushi profile image
e-武士

Great Tutorial, thanks for posting ... Having a little trouble in a particular spot though.

Not sure why but I'm getting this error whenever I type 'anchor test' or 'anchor deploy' . 👇

'Error: Not a directory (os error 20)'

Does anyone know what this means ?

Collapse
 
anderbuendia profile image
anderb

I think that you need to install rust. I did that and then it works!. Use the next command in the terminal to do it:

curl --proto '=https' --tlsv1.2 -sSf sh.rustup.rs | sh
source $HOME/.cargo/env
rustup component add rustfmt

Collapse
 
ntirinigasr profile image
Michael Ntiriniga Michael Senior

Hey, did you solve it I'm encountering the same problem

Collapse
 
franciscogmm profile image
Francisco Mendoza

I keep on getting this for the hello world app. I'm a bit stuck tbh.

1) mysolanaapp
Creates a counter):
Error: 101: Fallback functions are not supported
at Function.parse (node_modules/@project-serum/anchor/dist/cjs/error.js:40:20)
at Object.rpc as create
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async Context. (tests/mysolanaapp.js:13:5)

Collapse
 
fred227 profile image
Frédéric Lesage

hi, CAPITAL letters are not supported for function name in your rust on-chain program.
I get the same error when I use some.

Collapse
 
youngmahesh profile image
Mahesh

For me it was a combination of underscore and number, function name exchange1 is valid but exchange_1 is giving Fallback functions are not supported error

Collapse
 
franciscogmm profile image
Francisco Mendoza

nvm! i tried turning off the test validator and it succeeded :)

Collapse
 
yushoo profile image
kique

This is because anchor test runs the local ledger as well right? Just ran into this issue lol.

Collapse
 
deven96 profile image
Diretnan Domnan

Just the guide I was looking for!

Collapse
 
biiishal profile image
bishal • Edited

For anyone coming to this in 2022 with anchor-cli 0.22.1, you want to replace ProgramResult with updated Result<()>

docs.rs/anchor-lang/latest/anchor_...
rather than
docs.rs/anchor-lang/0.18.0/anchor_...

Also, for others who might have a run in with this error message Error: failed to send transaction: Transaction simulation failed: Attempt to load a program that does not exist while running the tests. Check out this github.com/project-serum/anchor/is...

You might want to turn off your solana-test-validator process, delete your node_modules and do a fresh yarn install, and finally try the anchor test. The issue seems to be when the name of a mod is changed the tests fails, which made sense in my case because I was going with a different name for my project (anchor_crud instead of mysolanaapp).

Collapse
 
quinncuatro profile image
Henry Quinn

In case anyone else was having a problem getting all the base tools set up on Ubuntu, I threw together a shell script to install everything so you can jump straight to writing code!

gist.github.com/Quinncuatro/2ef56e...

Collapse
 
stepanbokhanov profile image
alphadev

why I cant see anchor installation guide?
anchor-lang.com/docs/installation
I followed this guide but I got error.
error: failed to run custom build command for ring v0.16.20

Caused by:
process didn't exit successfully: C:\Users\Baymax\AppData\Local\Temp\cargo-installgjndxS\release\build\ring-bfe791dd4e257bbd\build-script-build (exit code: 101)
--- stdout
OPT_LEVEL = Some("3")
TARGET = Some("x86_64-pc-windows-msvc")
HOST = Some("x86_64-pc-windows-msvc")
CC_x86_64-pc-windows-msvc = None
CC_x86_64_pc_windows_msvc = None
HOST_CC = None
CC = None
CFLAGS_x86_64-pc-windows-msvc = None
CFLAGS_x86_64_pc_windows_msvc = None
HOST_CFLAGS = None
CFLAGS = None
CRATE_CC_NO_DEFAULTS = None
CARGO_CFG_TARGET_FEATURE = Some("fxsr,sse,sse2")
DEBUG = Some("false")
aes_nohw.c

Collapse
 
lunchboxav profile image
Adityo Pratomo

thank you for the guide! just what I'm looking for.

additional info, if anyone is using WSL, to run the test-ledger command succesfully, make sure you're in ~ directory instead of the default mount directory, e.g /mnt/c/Users...

Collapse
 
josephani profile image
Joseph Ani

Hello all,
I go to the point of running the anchor test and encountered errors, can someone help me out. Thanks

Image description

Collapse
 
kimchishi profile image
kimchi-shi

Replace it with "Result<()>"

Collapse
 
josephani profile image
Joseph Ani

Thanks Kimchi-shi, now am encountering the following errors.
Image description

Collapse
 
rwreynolds profile image
Rick Reynolds

Great tutorial. Got me started from scratch.

On the first test however I am receiving the following error. Any thoughts?

Failed to run test: mocha -t 1000000 tests/: No such file or directory (os error 2)

Collapse
 
dimfeld profile image
Daniel Imfeld

I ran into this as well, and in my case the problem was that anchor test expects mocha to be installed globally (npm i -g mocha or equivalent), which I forgot to do.

Collapse
 
rwreynolds profile image
Rick Reynolds

Never mind. It appears to have been an ID10T issue.

Collapse
 
0_tw1tter profile image
Name

Just for future adventurers,

Was the problem that you hadn't changed directory into example1 or example2?

Thread Thread
 
koenrad profile image
Koenrad MacBride

I just ran into the same problem and found this post while trying to solve it.

My problem was that I had the npm anchor package installed globally (which I did before I realized that it doesn't work on the M1 mac currently). Uninstalling it fixed the issue. (of course you also need to build anchor for it to work properly on the M1)

npm uninstall -g @project-serum/anchor-cli
Enter fullscreen mode Exit fullscreen mode