Vitor's Blog
Published on

Desenvolvimento full stack Solana com React, Anchor, Rust, e Phantom

Authors

Solana e Phantom

Solana é uma blockchain de escala web, suporta smart contracts e criação de apps decentralizados. Enquanto isso, Phantom é uma carteira digital, ele auxilia nas transações da Solana.

Também é útil a informação de que você não precisa de conhecimentos prévios muito avançados para desenvolver com Solana. De forma geral as coisas são bem documentadas e provavelmente não terá problemas grandes com esse tutorial. Para mim, foi a porta de entrada e os desafios não foram impossíveis, aprendi e interagi bastante, então vale bastante a pena.

OBS: O código do projeto está nesse repositório

Overview

As ferramentas que vamos usar são: Solana Tool Suite - Aqui tem uma polida e boa documentação da CLI para integir com a Solana network

Anchor Framework - Além de tudo, ele oferece um DSL em Rust para que você não precise saber profundamente da linguagem para iniciar.

solana/web3.js - É a versão Solana de web3.js

ReactJS - Client-side framework

Nesse tutorial, estou tomando como base o artigo The Complete Guide to Full Stack Solana Development with React, Anchor, Rust, and Phantom. Eu não entendi muito bem algumas coisas, e outras ele deixa em aberto, então vim trazer o conteúdo em pt-br, incluindo também os problemas que tive no processo no qual ele não citou.

O tutorial foca em desenvolver o projeto, testar e a integração do front-end para construir tipos de aplicativos - CRUD.

Também a enviar tokens para as contas de desenvolvimento com o SOlana CLI, implementar tais aplicativos localmente ou rede de teste.

Pré-requisito

  1. NodeJS
  2. Solana Tool Suite
  3. Anchor
  4. Solana browser wallet - para carteira, vamos usar o https://phantom.app/

Também é importante ressaltar que você precisa de um ambiente Linux para seguir o tutorial. Se usar Linux já sem problemas, apenas continue.

Caso utilize Windows, siga esse tutorial, que é oficial da Microsoft. Ele ensina a instalar o WSL e isso vai possibilitar você virtualizar no seu terminal um ambiente Linux.

Introdução

Solana CLI

A principal tarefa com Solana CLI é configurar nossa rede, que será entre localhost e testnet.

Para verificar as configurações da rede atual usa-se:

solana config get
# output
Config File: /home/vitor/.config/solana/cli/config.yml
RPC URL: https://api.mainnet-beta.solana.com
WebSocket URL: wss://api.mainnet-beta.solana.com/ (computed)
Keypair Path: /home/vitor/.config/solana/id.json
Commitment: confirmed

Caso você não tenha o Keypair Path, siga essas instruções

Para mudar de rede:

# config. para localhost
solana config set --url localhost
# config. para devnet
solana config set --url devnet

Essa é uma parte importante pois a rede precisa ser a mesma no ambiente de teste e sua carteira. O começo do tutorial será com localhost, e depois para devnet.

Para ver o endereço da carteira local atual:

solana address
# output
Error: No default signer found, run "solana-keygen new -o /home/vitor/.config/solana/id.json" to create a new one

Nessa etapa, caso seja sua primeira vez rodando o comando, ele vai pedir para você gerar esse arquivo (como eu disse lá em cima). E então copie o comando entre aspas e rode, não copie o meu pois tem usuário diferente. Depois disso, rode novamente o solana address

Agora, inicie sua rede local Solana:

solana-test-validator

Daqui para frente, deixe isso rodando num terminal e faça as outras coisas em outro, ele precisa estar rodando para funcionar a rede.

Para obter todos os detalhes de uma conta:

solana account <address>

Agora vamos dar airdrop em alguns tokens. Para isso, precisamos ter certeza de rede que estamos então configure para localhost

solana config set --url localhost

Quando ela tiver rodando, você vai poder rodar airdrops na sua conta. Para isso, em um terminal separado - pois o servidor está rodando no outro - rode esse comando:

solana airdrop 100

E agora você pode checar o saldo da sua carteira com:

solana balance
# ou
solana balance <address>

O resultado final é semelhante a esse: balance

Com isso, você provavelmente vai ter 100 SOL na carteira. Com isso, podemos começar a desenvolver.

Começando o desenvolvimento

Para buildar, rode:

anchor init mysolanaapp --javascript
cd mysolanaapp

app - pasta do frontend programs - onde roda o código Rust para o Solana test - onde roda o test do javascript migrations - Script de deploy

Anchor usa e permite a gente escrever, um eDSL que abstrai muitas formas mais complexas de tarefas low level que normalmente precisa ser feito se você estiver usando Solana e Rust.

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

A única coisa que ocorre nesse programa, foi a definição da função initialize, e quando chamada ela apenas sai do programa como Sucesso. Não há manipulação de dados.

Para compilar o programa, rode:

anchor build

quando o build tiver completo, vai ter gerado uma pasta chamada target. Tambem podemos ver nosso teste do frontend em tests/mysolanaapp.js, e ele deve ser assim:

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 () => {
// Add your test here.
const program = anchor.workspace.Mysolanaapp
const tx = await program.rpc.initialize()
console.log('Your transaction signature', tx)
})
})

Para rodar Solana usando Anchor, precisa de duas coisas:

  1. Provider - é uma abstração de uma conexão à rede Solana, normalmente uma Connection, Wallet e preflight commitment

No nosso programa, ele vai rodar e criar Provider baseado ambiente anchor.Provider.env(), mas no client precisa você mesmo contruir o Provider usando a Solana wallet

  1. program - é uma abstração que comina o Provider, idl e o programID, e nos permite chamar métodos RPC no programa.

Quando tivermos essas duas coisas, podemos chamar funções no nosso programa. Exemplo: no nosso programa, temos um initialize, podemos invocar diretamente a função da seguinte forma:

const tx = await program.rpc.functionName()

Agora para testar, rode:

anchor test

Construindo o Hello World

Vamos fazer um CRUD. O programa permitirá criar um contador que aumenta cada vez que o chamamos de um client.

abra programs/mysolanaapp/src/lib.rs e atualize tudo para o seguinte código:

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

Agora, rode:

anchor build

Agora, vamos para o teste do contador. Cole o código no js de teste

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

Agora, precisamos do address para continuar. Para isso, rode o seguinte comando:

solana address -k target/deploy/mysolanaapp-keypair.json

O output disso, cole em:

// mysolanaapp/src/lib.rs
declare_id!("program-id")

E em Anchor.toml, no diretório raiz:

[programs.localnet]
mysolanaapp = "your-program-id"

Agora, rode:

anchor test

Agora, tenha certeza que solana-test-validator esteja rodando e rode:

anchor deploy

Nessa parte, se tudo certo ok. No meu caso, tive um erro mas a solução é simples. Ele não conseguia recuperar o program-id automaticamente, então eu tive que especificar isso durante o anchor deploy.

Como eu não sabia disso antes, eu rodei o deploy todo via solana, usando esse tutorial. Recomendo que faça o mesmo para entender o motivo das coisas, o funcionamento dos diretórios etc. Caso faça isso, te adianto que o arquivo com .so está em target e deploy.

Após ter rodado o solana program deploy target/deploy/mysolanaapp.so, peguei o output e rodei como anchor deploy --program-name <address> e deu certo. O output foi o seguinte:

Deploying workspace: http://localhost:8899
Upgrade authority: /home/vitor/.config/solana/id.json
Deploy success

Building React App

Na raiz do projeto, crie um react app com o seguinte comando:

npx create-react-app app

Agora, instale as dependências que vamos precisar pro Anchor e Solana Web3:

cd app
npm install @project-serum/anchor @solana/web3.js

Usaremos o Solana Wallet Adapter para conectar usuários da carteira que usam Solana. Para isso, instale as dependências:

npm install @solana/wallet-adapter-react \
@solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets \
@solana/wallet-adapter-base

Agora, no diretório src, crie um novo arquivo idl.json. Nele, copie o IDL JSON que foi criado em target/idl/mysolanaapp.json.

Agora, abra app/src/App.js e cole:

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 { getPhantomWallet } 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 */
getPhantomWallet(),
]
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

Trocando a rede da carteira

Precisamos agora mudar a nossa rede do Phantom para a adequada. Para isso, abra as configurações clicando na engrenagem, no final tem Change Network, e ai deixe marcado localhost.

Agora, precisamos lançar tokens para nossa carteira. Para isso, clique no seu endereço que fica no topo, e no terminal rode:

  • Não esqueça de sempre rodar solana-test-validator
solana airdrop 10 <address>

Com isso, deverá ter agora 10 tokens na sua carteira. Vá até a pasta app (a criada pelo react) e rode npm start

Nesse ponto, deve ser possível já conectar na sua carteira, e iniciar o contador.

Nesse momento, se reiniciar a página ela perde o estado anterior. No artigo que tomei como base, existe um gist sobre como isso funciona.