Crie um dApp de blog simples na Solana
Aprenda como criar um dApp de blog simples na Solana usando o Rust e o React
Introdução
Neste tutorial, aprenderemos como criar um dapp de postagem de blog simples na blockchain Solana, enquanto construímos este dapp, aprenderemos como escrever o programa com a linguagem Rust da Solana, testar o programa e finalmente integrar o programa ao frontend do React.
Dapp final
Pré-requisitos
Este tutorial assume que você tenha,
- Conhecimento básico do React.js
- Conhecimento básico do Rust
- Completado o Solana 101 Pathway
Requisitos
Este tutorial aborda como construir um dapp na Solana, mas não passa pela instalação de dependências individuais (como pressupõe que você já tenha completado o Solana 101 Pathway no Figment learn).
- Anchor Framework - Anchor é uma estrutura usada para o desenvolvimento do dapp Solana. Ela fornece DSL para interagir com o programa Solana. Se você está familiarizado com o desenvolvimento em Solidity, Truffle ou Hardhat, então considere que a DSL é equivalente à ABI. Siga o guia para instalar o Anchor junto com o Rust e a cli Solana.
- React.js - Para interagir com nosso programa Solana, nós criaremos um aplicativo do lado do cliente com o React.js.
- Carteira Phantom - Phantom é uma carteira digital que lhe permite conectar sua conta criptográfica a qualquer dapp que seja construído sobre a blockchain Solana. Usaremos a carteira Phantom para conectar com nosso Dapp de Blog.
- Vs code - Vou recomendar o uso do vscode com extensão de analisador do Rust, pois ele tem um grande suporte para a linguagem Rust.
Modelo de Programação Solana
Programa - A Solana é uma blockchain rápida e de baixo custo. Para alcançar velocidade e baixo custo a Solana tem um modelo de programação ligeiramente diferente. Ela usa a linguagem de programação Rust para criar programas. Você percebe que continuamos a dizer programa da Solana em vez de contrato inteligente da Solana, já que escolher uma linguagem de programação para nomear conceitos Solana é diferente. No mundo Solana os contratos inteligentes são conhecidos como Programas Solana.
Conta - Os programas Solana são protocolos sem estado, portanto, se você quiser armazenar estado, você precisa usar uma conta para isso e as contas são fixas em tamanho. Uma vez que a conta é inicializada com um tamanho, você não pode mudar o tamanho mais tarde. Então, temos que projetar nosso aplicativo tendo isto em mente.
Aluguel - Na Solana, você precisa pagar aluguel regularmente para armazenar dados na blockchain, de acordo com o espaço que os dados necessitam. A conta pode ser isenta de aluguel (significa que você não terá que pagar aluguel) se seu saldo for maior que algum limite que dependa do espaço que está consumindo.
Decisão sobre o projeto do aplicativo
Como aprendemos, precisamos de uma conta para criar nosso dapp de blog que tenha um tamanho fixo. Assim, se criarmos uma única conta com tamanho X e começarmos a carregar os posts dentro dessa conta, eventualmente a conta excederá seu limite de tamanho e não conseguiremos criar novos posts. Se você conhece o Solidity, em Solidity criamos um array dinâmico e colocamos nele quantos itens quisermos. Mas na Solana nossas contas terão tamanhos fixos e então temos que encontrar uma solução para este problema.
- Solução um - E se criarmos uma conta de tamanho extremamente grande como em gigabytes? Na Solana precisamos pagar o aluguel de uma conta de acordo com seu tamanho, então, se nossa conta crescer em tamanho, o aluguel da conta irá crescer junto com ela.
- Solução dois - E se criarmos várias contas e as conectarmos de alguma forma? Sim, esse é o plano. Criaremos uma nova conta para cada post e criaremos uma cadeia de posts vinculados um após o outro.
Linked 🤔 sim. Você adivinhou bem. Usaremos o LinkedList para conectar todos os posts.
Desenvolvimento da configuração Local
Antes de começarmos com o desenvolvimento real. Conhecemos alguns docs de comandos da CLI Solana:
Para ver sua configuração atual da Solana (presumo que você tenha seguido o Solana 101 Pathway e que tenha feito toda a instalação da CLI) use:
solana config get
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: /home/user/.config/Solana/id.json
Commitment: confirmed
Sua saída pode ter diferentes caminhos de arquivo. Você pode verificar o endereço da carteira atual por:
solana address
Você pode verificar o saldo de sua carteira:
solana balance
Ou então, você pode fazer airdrop de tokens para a sua conta:
solana airdrop 1 <your-account-address>
Verifique novamente o saldo. Agora você deve ter um saldo de 1 SOL em sua carteira.
Agora é a hora de lançar nosso aplicativo de blog com a ajuda da CLI Anchor:
anchor init blog
cd blog
O comando init do Anchor cria os seguintes diretórios:
├── app
├── programs
| └── blog
| └── src
| └── lib.rs
├── test
Antes de escrever o código do programa atualize o Anchor.toml
wallet = "your Keypair Path from the output of solana config get"
Criando o programa de blog
Agora estamos prontos para começar com o programa Rust da Solana. Abra o arquivo lib.rs
localizado na pasta /program/blog/src/
.
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod blog {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
Este é o exemplo básico de um programa Anchor Solana. Há apenas uma função initialize
, que será chamada pelo cliente. A função initialize (inicializar) tem um argumento de tipo de contexto de struct Initialize
.
Outra coisa notável é o declare_id!
, que é uma macro que define o endereço do programa e é usada na validação interna. Não precisamos pensar muito sobre isto. Isto será tratado pela CLI da Anchor.
Agora é hora de começar a declarar os estados de nosso aplicativo de blog.
// pseudo código
blog {
current_post_key // última identificação de post para que possamos percorrer de volta outros posts
authority // quem possui a conta
}
user {
name // armazenar nome do usuário
avatar // avatar do usuário
authority // proprietário
}
post {
title // título do post
content // conteúdo descritivo do post
user // id do usuário
pre_post_key // para criar LinkedList
authority // proprietário
}
Como você viu no primeiro exemplo básico, precisamos criar funções que definirão tarefas que queremos executar em um programa como, init_blog, signup_user, create_post, etc.
Vamos começar com a criação da nossa primeira função init_blog
.
pub fn init_blog(ctx: Context<InitBlog>) -> ProgramResult {
Ok(())
}
// definir ctx type
#[derive(Accounts)]
pub struct InitBlog<'info> {
#[account(init, payer = authority, space = 8 + 32 + 32)]
pub blog_account: Account<'info, BlogState>,
#[account(init, payer = authority, space = 8 + 32 + 32 + 32 + 32 + 8)]
pub genesis_post_account: Account<'info, PostState>,
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
// do estado do pseudo blog
#[account]
pub struct BlogState {
pub current_post_key: Pubkey,
pub authority: Pubkey,
}
Como você sabe, cada função precisa de um contexto digitado como o primeiro argumento. Aqui definimos InitBlog
como um tipo ctx do seu init_blog
. No tipo ctx temos que definir a conta e a conta será fornecida pelo cliente (quem chama a função).
No InitBlog
há 4 contas:
- blog_account
- atributo init para criar/inicializar uma nova conta
- space (espaço) = 8 + 32 + 32. Aqui, estamos criando uma nova conta e é por isso que temos de especificar o tamanho da conta. Veremos mais tarde como calcular o tamanho da conta.
- payer (pagante) = authority. authority é uma das contas fornecidas pelo cliente. A authority (autoridade) é o pagador de aluguel da conta blog_account.
- genesis_post_account
- Também estamos criando esta conta. Por isso o init, o payer e os atributos do space estão lá.
- Para criar a LinkedList, inicializamos a conta do blog com o primeiro post para que possamos vinculá-la ao próximo post.
- authority
- O signatário do programa é um criador do blog.
- system_program
- exigido pelo runtime (tempo de execução) para a criação da conta.
Com init_blog
nosso plano é inicializar a conta do blog com current_post_key e authority como o estado do blog. Então vamos escrever código para isso,
pub fn init_blog(ctx: Context<InitBlog>) -> ProgramResult {
// obter contas do ctx
let blog_account = &mut ctx.accounts.blog_account;
let genesis_post_account = &mut ctx.accounts.genesis_post_account;
let authority = &mut ctx.accounts.authority;
// define o estado do blog
blog_account.authority = authority.key();
blog_account.current_post_key = genesis_post_account.key();
Ok(())
}
Como é fácil criar uma conta que contenha alguns dados de estado com a estrutura Anchor.
Agora, vamos passar para a próxima função. O que podemos fazer a seguir?? Usuário, cadastro de usuário. Vamos definir a função de cadastramento com a qual os usuários podem criar seu perfil fornecendo nome e avatar como inputs.
pub fn signup_user(ctx: Context<SignupUser>) -> ProgramResult {
Ok(())
}
Esse é o esqueleto básico para criar uma nova função, mas como obtemos o nome e o avatar do usuário... Vamos ver.
pub fn signup_user(ctx: Context<SignupUser>, name: String, avatar: String) -> ProgramResult {
Ok(())
}
Podemos aceitar qualquer número de argumentos após ctx como aqui, nome e avatar, como String (O Rust é uma linguagem estaticamente tipificada. Temos que definir o tipo enquanto definimos as variáveis). O próximo é o tipo ctx SignupUser
e o estado UserState.
#[derive(Accounts)]
pub struct SignupUser<'info> {
#[account(init, payer = authority, space = 8 + 40 + 120 + 32)]
pub user_account: Account<'info, UserState>,
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct UserState {
pub name: String,
pub avatar: String,
pub authority: Pubkey,
}
Aqui, precisamos de três contas e você já entendeu, na função anterior, todos os atributos (como init, payer, space). Então, não vou explicar de novo isso aqui. Mas explicarei a você como calcular o espaço da conta desta vez. Para medir o espaço da conta, precisamos dar uma olhada em qual estado a conta está mantida. No caso de user_account o UserState tem 3 valores para armazenar nome, avatar e autoridade.
State Values | Data Types | Size (in bytes) |
authority | Pubkey | 32 |
name | String | 40 |
avatar | String | 120 |
Pubkey: Pubkey é sempre de 32 bytes e a String é variável em tamanho, portanto depende de seu caso de uso.
String: A String é um conjunto de chars (servem para armazenar um caracter) e cada char leva 4 bytes no Rust.
Account Discriminator: Todas as contas criadas com Anchor precisam de 8 bytes.
Indo em frente, vamos completar a função de cadastramento restante:
pub fn signup_user(ctx: Context<SignupUser>, name: String, avatar: String) -> ProgramResult {
let user_account = &mut ctx.accounts.user_account;
let authority = &mut ctx.accounts.authority;
user_account.name = name;
user_account.avatar = avatar;
user_account.authority = authority.key();
Ok(())
}
Até agora, nós criamos duas funções: a init_blog
e a signup_user
com nome e avatar. Especificamente, a signup_user
leva dois argumentos. E se um usuário enviou equivocadamente o nome errado e quer atualizá-lo? Você adivinhou bem. Criaremos uma função que permite ao usuário atualizar o nome e o avatar de sua conta.
pub fn update_user(ctx: Context<UpdateUser>, name: String, avatar: String) -> ProgramResult {
let user_account = &mut ctx.accounts.user_account;
user_account.name = name;
user_account.avatar = avatar;
Ok(())
}
#[derive(Accounts)]
pub struct UpdateUser<'info> {
#[account(
mut,
has_one = authority,
)]
pub user_account: Account<'info, UserState>,
pub authority: Signer<'info>,
}
Novos atributos:
- mut: se quisermos alterar/atualizar o estado/dados da conta, devemos especificar o atributo mut
- has_one: has_one checa se user_account.authority é igual à chave de contas da authority, ou seja, o dono da conta user_account é o signatário (quem chama) da função update_user
Nosso blog é inicializado, um usuário é criado e agora o que resta?? CRUD do post. Na próxima seção, vamos analisar o CRUD da entidade do post. Se você se sentir sobrecarregado, faça uma pausa ou passe pelo que aprendemos até agora.
Agora, vamos passar para o CRUD do post!! CRUD significa Create Read Update Delete (Criar Ler Atualizar Remover).
pub fn create_post(ctx: Context<CreatePost>, title: String, content: String) -> ProgramResult {
Ok(())
}
#[derive(Accounts)]
pub struct CreatePost<'info> {
#[account(init, payer = authority, space = 8 + 50 + 500 + 32 + 32 + 32)]
pub post_account: Account<'info, PostState>,
#[account(mut, has_one = authority)]
pub user_account: Account<'info, UserState>,
#[account(mut)]
pub blog_account: Account<'info, BlogState>,
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct PostState {
title: String,
content: String,
user: Pubkey,
pub pre_post_key: Pubkey,
pub authority: Pubkey,
}
O que você acha? Por que precisamos do blog_account como mut aqui? Você se lembra do campo current_post_key no BlogState. Vejamos o corpo da função.
pub fn create_post(ctx: Context<CreatePost>, title: String, content: String) -> ProgramResult {
let blog_account = &mut ctx.accounts.blog_account;
let post_account = &mut ctx.accounts.post_account;
let user_account = &mut ctx.accounts.user_account;
let authority = &mut ctx.accounts.authority;
post_account.title = title;
post_account.content = content;
post_account.user = user_account.key();
post_account.authority = authority.key();
post_account.pre_post_key = blog_account.current_post_key;
// armazenar a id do post criado como id do post atual na conta do blog
blog_account.current_post_key = post_account.key();
Ok(())
}
O post é criado. Agora podemos avisar ao cliente que o post está criado. O cliente pode ir buscar o post e renderizá-lo na UI. A Anchor fornece uma característica útil para a emissão de um evento. Evento? Sim, você o leu corretamente. Podemos emitir um evento como um evento pós-criado. Antes de emitir um evento, precisamos defini-lo.
#[event]
pub struct PostEvent {
pub label: String, // a etiqueta é semelhante a 'CRIAR', 'ATUALIZAR', 'APAGAR'.'
pub post_id: Pubkey, // post criado
pub next_post_id: Option<Pubkey>, // a partir de agora ignore isso, usaremos isso quando emitirmos o evento de delete
}
Vamos emitir um evento criado pela função post_create
pub fn create_post(ctx: Context<CreatePost>, title: String, content: String) -> ProgramResult {
....
emit!(PostEvent {
label: "CREATE".to_string(),
post_id: post_account.key(),
next_post_id: None // o mesmo que null
});
Ok(())
}
A seguir, Atualizar o Post.
pub fn update_post(ctx: Context<UpdatePost>, title: String, content: String) -> ProgramResult {
let post_account = &mut ctx.accounts.post_account;
post_account.title = title;
post_account.content = content;
emit!(PostEvent {
label: "UPDATE".to_string(),
post_id: post_account.key(),
next_post_id: None // null
});
Ok(())
}
#[derive(Accounts)]
pub struct UpdatePost<'info> {
#[account(
mut,
has_one = authority,
)]
pub post_account: Account<'info, PostState>,
pub authority: Signer<'info>,
}
A atualização do post é realmente simples. Pegue o título e o conteúdo do usuário e atualize a conta mut post_account.
Remover um post é um pequeno desafio. Para armazenar os posts, usamos o LinkedList. Se você conhece o LinkedList, após excluir um nó da LinkedList, precisamos fazer um link do nó adjacente à exclusão de um nó. Vamos entender isto através de um diagrama.
Se quisermos apagar o 2º post, temos que fazer o link 1 -> 3.
Vamos pular para o código, eu sei que você o entenderá facilmente.
// Aqui, precisamos de duas contas de post, a current_post e a next_post. Obtemos o pre_post do current_post e o ligamos à next_post
pub fn delete_post(ctx: Context<DeletePost>) -> ProgramResult {
let post_account = &mut ctx.accounts.post_account;
let next_post_account = &mut ctx.accounts.next_post_account;
next_post_account.pre_post_key = post_account.pre_post_key;
emit!(PostEvent {
label: "DELETE".to_string(),
post_id: post_account.key(),
next_post_id: Some(next_post_account.key())
});
Ok(())
}
#[derive(Accounts)]
pub struct DeletePost<'info> {
#[account(
mut,
has_one = authority,
close = authority,
constraint = post_account.key() == next_post_account.pre_post_key
)]
pub post_account: Account<'info, PostState>,
#[account(mut)]
pub next_post_account: Account<'info, PostState>,
pub authority: Signer<'info>,
}
O atributo constraint (de restrição) realiza uma verificação if simples.
Assim, para remover um post, o usuário precisa enviar a post_account e a next_post_account. E se não tiver next_post?? E se o usuário quiser excluir o último post, ou seja, não tem próximo post? Para lidar com este caso, precisamos criar outra função, a delete_latest_post
pub fn delete_latest_post(ctx: Context<DeleteLatestPost>) -> ProgramResult {
let post_account = &mut ctx.accounts.post_account;
let blog_account = &mut ctx.accounts.blog_account;
blog_account.current_post_key = post_account.pre_post_key;
emit!(PostEvent {
label: "DELETE".to_string(),
post_id: post_account.key(),
next_post_id: None
});
Ok(())
}
#[derive(Accounts)]
pub struct DeleteLatestPost<'info> {
#[account(
mut,
has_one = authority,
close = authority
)]
pub post_account: Account<'info, PostState>,
#[account(mut)]
pub blog_account: Account<'info, BlogState>,
pub authority: Signer<'info>,
}
Essa foi a última função do nosso Programa Rust.
O próximo é o programa de testes. Não se preocupe, vamos avançar rapidamente para a próxima seção.
Escrevendo testes para programa de blog
Antes de mergulharmos na escrita de casos de teste, cada teste precisa de um blog inicializado, um usuário novinho em folha e um post. Para evitar repetições, criaremos 3 funções utilitárias simples e reutilizáveis.
-
createBlog
- Inicializa a conta do novo Blog -
createUser
- Cria um novo User (Usuário) -
createPost
- Cria um novo Post
createBlog.js
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;
// vamos discutir os parâmetros quando formos usá-los
async function createBlog(program, provider) {
const blogAccount = anchor.web3.Keypair.generate(); // cria um keypair aleatório
const genesisPostAccount = anchor.web3.Keypair.generate(); // cria um keypair randomizado
await program.rpc.initBlog({
accounts: {
authority: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
blogAccount: initBlogAccount.publicKey,
genesisPostAccount: genesisPostAccount.publicKey,
},
signers: [initBlogAccount, genesisPostAccount],
});
const blog = await program.account.blogState.fetch(initBlogAccount.publicKey);
return { blog, blogAccount, genesisPostAccount };
}
module.exports = {
createBlog,
};
createUser.js
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;
async function createUser(program, provider) {
const userAccount = anchor.web3.Keypair.generate();
const name = "user name";
const avatar = "https://img.link";
await program.rpc.signupUser(name, avatar, {
accounts: {
authority: provider.wallet.publicKey,
userAccount: userAccount.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [userAccount],
});
const user = await program.account.userState.fetch(userAccount.publicKey);
return { user, userAccount, name, avatar };
}
module.exports = {
createUser,
};
createPost.js
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;
async function createPost(program, provider, blogAccount, userAccount) {
const postAccount = anchor.web3.Keypair.generate();
const title = "post title";
const content = "post content";
await program.rpc.createPost(title, content, {
// passa os argumentos para o programa
accounts: {
blogAccount: blogAccount.publicKey,
authority: provider.wallet.publicKey,
userAccount: userAccount.publicKey,
postAccount: postAccount.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [postAccount],
});
const post = await program.account.postState.fetch(postAccount.publicKey);
return { post, postAccount, title, content };
}
module.exports = {
createPost,
};
Agora estamos prontos para escrever nosso primeiro caso de teste. Abra o arquivo de teste localizado no diretório /test/blog.js
. Escreveremos todos os casos de teste no arquivo blog.js
.
const anchor = require("@project-serum/anchor");
const assert = require("assert");
describe("blog tests", () => {
const provider = anchor.Provider.env();
anchor.setProvider(provider);
const program = anchor.workspace.BlogSol;
it("initialize blog account", async () => {
// chamar a função utilitária
const { blog, blogAccount, genesisPostAccount } = await createBlog(
program,
provider
);
assert.equal(
blog.currentPostKey.toString(),
genesisPostAccount.publicKey.toString()
);
assert.equal(
blog.authority.toString(),
provider.wallet.publicKey.toString()
);
});
});
A seguir, execute o teste:
anchor test
Após executar o anchor test
, você verá o teste 1/1 passar.
Agora completamos os testes restantes e vamos escrever todos os casos de teste abaixo do caso de teste anterior no arquivo blog.js
.
const anchor = require("@project-serum/anchor");
const assert = require("assert");
describe("blog tests", () => {
const provider = anchor.Provider.env();
anchor.setProvider(provider);
const program = anchor.workspace.BlogSol;
it("initialize blog account", async () => {
const { blog, blogAccount, genesisPostAccount } = await createBlog(
program,
provider
);
assert.equal(
blog.currentPostKey.toString(),
genesisPostAccount.publicKey.toString()
);
assert.equal(
blog.authority.toString(),
provider.wallet.publicKey.toString()
);
});
it("signup a new user", async () => {
const { user, name, avatar } = await createUser(program, provider);
assert.equal(user.name, name);
assert.equal(user.avatar, avatar);
assert.equal(
user.authority.toString(),
provider.wallet.publicKey.toString()
);
});
it("creates a new post", async () => {
const { blog, blogAccount } = await createBlog(program, provider);
const { userAccount } = await createUser(program, provider);
const { title, post, content } = await createPost(
program,
provider,
blogAccount,
userAccount
);
assert.equal(post.title, title);
assert.equal(post.content, content);
assert.equal(post.user.toString(), userAccount.publicKey.toString());
assert.equal(post.prePostKey.toString(), blog.currentPostKey.toString());
assert.equal(
post.authority.toString(),
provider.wallet.publicKey.toString()
);
});
it("updates the post", async () => {
const { blog, blogAccount } = await createBlog(program, provider);
const { userAccount } = await createUser(program, provider);
const { postAccount } = await createPost(
program,
provider,
blogAccount,
userAccount
);
// agora atualize o post criado
const updateTitle = "Updated Post title";
const updateContent = "Updated Post content";
const tx = await program.rpc.updatePost(updateTitle, updateContent, {
accounts: {
authority: provider.wallet.publicKey,
postAccount: postAccount.publicKey,
},
});
const post = await program.account.postState.fetch(postAccount.publicKey);
assert.equal(post.title, updateTitle);
assert.equal(post.content, updateContent);
assert.equal(post.user.toString(), userAccount.publicKey.toString());
assert.equal(post.prePostKey.toString(), blog.currentPostKey.toString());
assert.equal(
post.authority.toString(),
provider.wallet.publicKey.toString()
);
});
it("deletes the post", async () => {
const { blogAccount } = await createBlog(program, provider);
const { userAccount } = await createUser(program, provider);
const { postAccount: postAcc1 } = await createPost(
program,
provider,
blogAccount,
userAccount
);
const { post: post2, postAccount: postAcc2 } = await createPost(
program,
provider,
blogAccount,
userAccount
);
const {
post: post3,
postAccount: postAcc3,
title,
content,
} = await createPost(program, provider, blogAccount, userAccount);
assert.equal(postAcc2.publicKey.toString(), post3.prePostKey.toString());
assert.equal(postAcc1.publicKey.toString(), post2.prePostKey.toString());
await program.rpc.deletePost({
accounts: {
authority: provider.wallet.publicKey,
postAccount: postAcc2.publicKey,
nextPostAccount: postAcc3.publicKey,
},
});
const upPost3 = await program.account.postState.fetch(postAcc3.publicKey);
assert.equal(postAcc1.publicKey.toString(), upPost3.prePostKey.toString());
assert.equal(upPost3.title, title);
assert.equal(upPost3.content, content);
assert.equal(upPost3.user.toString(), userAccount.publicKey.toString());
assert.equal(
upPost3.authority.toString(),
provider.wallet.publicKey.toString()
);
});
});
Execute de novo:
anchor test
Construindo o frontend
Agora, estamos prontos para construir o frontend. Vamos criar um novo aplicativo react no diretório de aplicativos existente.
cd app
npx create-react-app .
A estrutura de diretório de um aplicativo básico React feito com create-react-app
:
├── public
├── src
| └── app.js
├── package.json
Antes de começarmos a escrever a parte do frontend do tutorial, criaremos um script simples que copiará o arquivo idl do programa para o aplicativo React. Sempre que implantarmos nosso programa rust com a implantação da Anchor, a CLI da Anchor gera o arquivo idl que tem todos os metadados relacionados ao nosso programa Rust (estes metadados ajudam a construir a interface do lado do cliente com o programa Rust).
Crie o arquivo copy_idl.js
na raiz do seu projeto e copie o código abaixo. O código só está copiando o arquivo idl de /target/idl
para o diretório /app/src
.
const fs = require("fs");
const blog_idl = require("./target/idl/blog_sol.json");
fs.writeFileSync("./app/src/idl.json", JSON.stringify(blog_idl, null, 2));
A seguir, instale as dependências.
npm i @solana/wallet-adapter-react @solana/wallet-adapter-wallets @solana/web3.js
Em seguida, abra o app/src/App.js
e atualize-o com o seguinte.
import {
ConnectionProvider,
WalletProvider,
} from "@solana/wallet-adapter-react";
import { getPhantomWallet } from "@solana/wallet-adapter-wallets";
import { Home } from "./home";
const wallets = [getPhantomWallet()];
const endPoint = "http://127.0.0.1:8899";
const App = () => {
return (
<ConnectionProvider endpoint={endPoint}>
<WalletProvider wallets={wallets} autoConnect>
<Home />
</WalletProvider>
</ConnectionProvider>
);
};
export default App;
No tutorial posterior, vou apenas explicar a parte lógica de nosso dapp e deixarei a parte do estilo por sua conta.
Agora, vamos começar com a funcionalidade de login do nosso dapp. Vamos precisar de um botão que trate do login do usuário com a carteira do navegador Phantom.
Crie um botão dentro do componente Home.js com manipulador on Click onConnect
dessa forma,
<button onClick={onConnect}>Connect with Phantom</button>
Então, crie a função onConnect
que controla o evento click do botão de conexão.
import { WalletName } from "@solana/wallet-adapter-wallets";
import { useWallet } from "@solana/wallet-adapter-react";
// dentro do componente react
const { select } = useWallet();
const onConnect = () => {
select(WalletName.Phantom);
};
Vamos implantar nosso programa Rust primeiro e depois copiar o arquivo idl com a ajuda do script copy_idl.js
que escrevemos anteriormente,
Antes da implantação, certifique-se de que você tenha o cluster localnet
definido no arquivo Anchor.toml. Agora abra uma nova sessão do Terminal e execute o comando solana-test-validator
. Isto iniciará uma rede local da blockchain Solana.
solana-test-validator
Implante o programa com,
anchor deploy
Se der erro,
- Certifique-se de que o
solana-test-validator
esteja sendo executado - Certifique-se de que sua configuração Solana esteja em um estado válido (quero dizer o url RPC, caminho KeyPair, etc.)
Uma vez que você tenha implantado com sucesso o programa Rust com a CLI Anchor, então execute o arquivo copy_idl.js
assim,
node copy_idl.js
Isso copiará o arquivo idl para o diretório /app/src
e você verá o arquivo idl.json
dentro do diretório /app/src
.
Inicializar o Blog
Agora vamos inicializar o blog. Inicializar o blog é um processo único. Vamos criar o arquivo init_blog.js
na pasta /app/src
e copiar o código abaixo.
import { Program } from "@project-serum/anchor";
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import idl from "./idl.json";
const PROGRAM_KEY = new PublicKey(idl.metadata.address);
export async function initBlog(walletKey, provider) {
const program = new Program(idl, PROGRAM_KEY, provider);
const blogAccount = Keypair.generate();
const genesisPostAccount = Keypair.generate();
await program.rpc.initBlog({
accounts: {
authority: walletKey,
systemProgram: SystemProgram.programId,
blogAccount: blogAccount.publicKey,
genesisPostAccount: genesisPostAccount.publicKey,
},
signers: [blogAccount, genesisPostAccount],
});
console.log("Blog pubkey: ", blogAccount.publicKey.toString());
}
Nesta função initBlog
importamos o programa idl, então geramos dois Keypairs para conta no blog e uma conta inicial de postagem fictícia e depois disso apenas chamamos a função initBlog do programa com todas as contas necessárias. Estamos criando duas novas contas aqui, a blogAccount
e a genesisPostAccount
e é por isso que temos que passá-las como signatários.
Agora a parte da UI disto: vamos criar um botão temporário para chamar a função initBlog
. Quando o blog for inicializado, removeremos o botão, pois ele não será mais necessário.
import {
useAnchorWallet,
useConnection
} from "@solana/wallet-adapter-react";
import idl from './idl.json'
const PROGRAM_KEY = new PublicKey(idl.metadata.address);
const BLOG_KEY = /* new PublicKey(blog key) */;
// dentro do componente react
const { connection } = useConnection();
const wallet = useAnchorWallet();
const _initBlog = () => {
const provider = new Provider(connection, wallet, {});
initBlog(provider.wallet.publicKey, provider);
};
<button onClick={_initBlog}>Init blog</button>
A função initBlog
criará um Blog novinho e um registro de console de seu publicKey. Certifique-se de que você ainda tenha o solana-test-validator
funcionando em outro terminal e que sua carteira Phantom esteja conectada à rede local (http://localhost:8899
)
Se você não souber como conectar a carteira Phantom à Localnet aqui estão os passos,
- Vá para a guia de configurações na carteira Phantom
- Role para baixo e selecione a change network (mudar a rede)
- Finalmente escolha o Localhost
Em seguida, você precisa de saldo em sua conta na Localnet para obter o saldo necessário para executar o seguinte comando.
solana airdrop 1 <your-account-address>
Em seguida, verifique o saldo de sua conta com o comando:
solana balance <your-account-address>
Você verá 1 SOL
impresso em seu terminal.
Uma vez conectado ao Localhost, você está pronto para inicializar a conta do blog. Execute o aplicativo com npm run start,
ele abrirá seu Dapp em seu navegador e lá você verá dois botões um é o connect
e o outro é o init blog
. O primeiro passo é conectar a carteira Phantom, clicando no botão connect
. Uma vez conectado, clique no botão init blog
. o que acionará o popup de click de confirmação da carteira Phantom Approve
. Após 1 a 2 segundos a função initBlog
fará o registro do console da publicKey da sua conta de blog. Apenas copie a publicKey do blog e armazene-a na variável BLOG_KEY
. É isso. Iniciamos nossa conta de blog com sucesso.
const BLOG_KEY = new PublicKey(your - blog - key);
Seu blog está inicializado. Agora você pode observar o botão init blog,
assim como a função _initBlog
.
Cadastrar o usuário
Agora vamos passar para a parte em que o usuário digita seu nome e URL do avatar e inicializamos sua conta.
Vamos criar dois campos de entrada, um para o nome do usuário e outro para o avatar do usuário.
<input placeholder="user name" />
<input placeholder="user avatar" />
Em seguida, anexe o estado do React aos campos de entrada e crie o botão signup
const [name, setName] = useState("")
const [avatar, setAvatar] = useState("")
const _signup = ()=> {
}
<input placeholder="user name" value={name} onChange={e => setName(e.target.value)} />
<input placeholder="user avatar" value={avatar} onChange={e => setAvatar(e.target.value)} />
<button onClick={_signup}>Signup</button>
Agora vamos escrever a funcionalidade da função _signup
. Se você se lembra da função initBlog
, lá geramos o Keypair aleatoriamente, mas nesse caso não geraremos o Keypair aleatoriamente, já que precisamos identificar o usuário de todos os logins subsequentes.
O Keypair
possui a função fromSeed
que toma o argumento seed
e gera um Keypair único a partir da semente dada.
Então, criaremos a seed a partir da combinação do PROGRAM_KEY
e da wallet_key
dos usuários, de forma que será criada um único Keypair.
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { Program, Provider } from "@project-serum/anchor";
const genUserKey = (PROGRAM_KEY, walletKey) => {
const userAccount = Keypair.fromSeed(
new TextEncoder().encode(
`${PROGRAM_KEY.toString().slice(0, 15)}__${walletKey
.toString()
.slice(0, 15)}`
)
);
return userAccount;
};
Vamos completar a função _signup
,
const _signup = async () => {
const provider = new Provider(connection, wallet, {});
const program = new Program(idl, PROGRAM_KEY, provider);
const userAccount = genUserKey(PROGRAM_KEY, provider.wallet.publicKey);
await program.rpc.signupUser(name, avatar, {
accounts: {
authority: provider.wallet.publicKey,
userAccount: userAccount.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [userAccount],
});
};
Agora que criamos uma conta de usuário, vamos buscar a conta do usuário para ver se o usuário já se inscreveu.
import { useEffect, useState } from "react";
// dentro do componente react
const fetchUser = async () => {
const provider = new Provider(connection, wallet, {});
const program = new Program(idl, PROGRAM_KEY, provider);
const userAccount = genUserKey(PROGRAM_KEY, provider.wallet.publicKey);
const _user = await program.account.userState.fetch(userAccount.publicKey);
return _user;
};
const [user, setUser] = useState();
// buscar o usuário quando a carteira for conectada
useEffect(() => {
if (wallet?.publicKey) {
fetchUser()
.then((user) => {
setUser(user);
})
.catch((e) => console.log(e));
}
}, [wallet]);
// se o usuário não estiver indefinido, então mostre o nome do usuário e o avatar do usuário na UI
// caso contrário, mostrar formulário de inscrição para o usuário
{
user ? (
<>
<h1>user name: {user.name}</h1>
<h1>user avatar: {user.avatar} </h1>
</>
) : (
<>
<input
placeholder="user name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
placeholder="user avatar"
value={avatar}
onChange={(e) => setAvatar(e.target.value)}
/>
<button onClick={_signup}>Signup</button>{" "}
</>
);
}
Criar o post
Primeiro criaremos um formulário para obter o título do post e o conteúdo do post a partir da entrada do usuário.
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const _createPost = async ()=> {
}
<input
placeholder="post title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
placeholder="post content"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<button onClick={_createPost}>Create post</button>
Agora podemos completar a função _createPost
const _createPost = async () => {
const provider = new Provider(connection, wallet, {});
const program = new Program(idl, PROGRAM_KEY, provider);
const postAccount = Keypair.generate();
const userAccount = genUserKey(PROGRAM_KEY, provider.wallet.publicKey);
await program.rpc.createPost(title, content, {
accounts: {
blogAccount: BLOG_KEY,
authority: provider.wallet.publicKey,
userAccount: userAccount.publicKey,
postAccount: postAccount.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [postAccount],
});
};
Aqui nós passamos à BLOG_KEY
a mesma chave que obtivemos na inicialização do blog.
Agora vamos buscar todos os posts criados em nosso Blog. Se você se lembra do programa Rust, anexamos o id do post anterior ao post atual. Aqui usaremos o id do post anterior para iterar sobre a lista de posts. Para fazer isso, criaremos uma função que localiza o post de um determinado id de postagem.
const getPostById = async (postId) => {
const provider = new Provider(connection, wallet, {});
const program = new Program(idl, PROGRAM_KEY, provider);
try {
const post = await program.account.postState.fetch(new PublicKey(postId));
const userId = post.user.toString();
if (userId === SystemProgram.programId.toString()) {
return;
}
return {
id: postId,
title: post.title,
content: post.content,
userId,
prePostId: post.prePostKey.toString(),
};
} catch (e) {
console.log(e.message);
}
};
Para percorrer todos os posts precisamos da última identificação do post e podemos encontrar a última identificação do post do estado do blog. Crie a função fetchAllPosts
assim,
const fetchAllPosts = async () => {
const provider = new Provider(connection, wallet, {});
const program = new Program(idl, PROGRAM_KEY, provider);
// leia o estado do blog
const blog = await program.account.blogState.fetch(BLOG_KEY);
const latestPostId = blog.currentPostKey.toString();
const posts = [];
let nextPostId = latestPostId;
while (!!nextPostId) {
const post = await getPostById(nextPostId, program);
if (!post) {
break;
}
posts.push(post);
nextPostId = post.prePostId;
}
return posts;
};
Agora acione a função fetchAllPosts quando o usuário fizer o login com a carteira Phantom.
const [posts, setPosts] = useState([]);
// buscar todos os posts quando a carteira for conectada
useEffect(() => {
if (wallet?.publicKey) {
fetchAllPosts()
.then((posts) => {
setPosts(posts);
})
.catch((e) => console.log(e));
}
}, [wallet]);
Mostre os posts na UI
{
posts.map(({ title, content }, i) => {
return (
<div key={i}>
<h2>{title}</h2>
<p>{content}</p>
</div>
);
});
}
Todo (A fazer)
Até agora, integramos a inicialização do blog, a inscrição do usuário, busca do usuário, criação do post, busca de todos os posts, mas ainda resta alguma parte a fazer, como
- atualizar usuário
- atualizar o post
- remover o post
Se você seguir os mesmos passos que nós aprendemos, você poderá completar as tarefas restantes.
Implantando para a Devnet
Implantar em uma rede ao vivo é simples:
- Defina a configuração Solana para a devnet
solana config set --url devnet
- Abra o Anchor.toml e Atualize o cluster para a devnet
cluster = "devnet"
- Construa o programa
anchor build
- Implante o programa
anchor deploy
Conclusão
Parabéns por ter concluído o tutorial. Neste tutorial, criamos com sucesso um programa Rust para o Dapp de Blog na Blockchain Solana. Também integramos o programa Rust de postagem de Blog com o React.js do lado do cliente. Aqui, seguimos uma abordagem um pouco diferente para o armazenamento e recuperação de posts. A Blockchain Solana ainda está em sua fase beta, portanto, não se preocupe em testar. Quem sabe, você pode encontrar alguns padrões legais dela.
Ainda há espaço para melhorias no Dapp de Blog como,
- Melhorias da UI
- Criação de um aplicativo frontend com Typescript
- Adição de paginação durante a busca de todos os posts
- Criação de linha do tempo do usuário (posts criados pelo usuário)
- Adição de Rxjs para a busca de posts (posts de transmissão)
Sobre o Autor
Esse tutorial foi criado por Kiran Bhalerao. Para qualquer sugestão/pergunta, você pode se conectar com o autor em Figment Forum.
Referências
https://github.com/kiran-bhalerao/blog-dapp-Solana
Esse artigo foi traduzido por Fátima Lima e seu original pode ser lido aqui.
Oldest comments (0)