WEB3DEV

Cover image for Crie um dApp de blog simples na Solana
Fatima Lima
Fatima Lima

Posted on

Crie um dApp de blog simples na Solana

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

Image description

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
Enter fullscreen mode Exit fullscreen mode

Sua saída pode ter diferentes caminhos de arquivo. Você pode verificar o endereço da carteira atual por:

solana address
Enter fullscreen mode Exit fullscreen mode

Você pode verificar o saldo de sua carteira:

solana balance
Enter fullscreen mode Exit fullscreen mode

Ou então, você pode fazer airdrop de tokens para a sua conta:

solana airdrop 1 <your-account-address>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

O comando init do Anchor cria os seguintes diretórios:

├── app
├── programs
|   └── blog
|        └── src
|             └── lib.rs
├── test
Enter fullscreen mode Exit fullscreen mode

Antes de escrever o código do programa atualize o Anchor.toml

wallet = "your Keypair Path from the output of solana config get"
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
Enter fullscreen mode Exit fullscreen mode

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(())
  }   
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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(())
   }
Enter fullscreen mode Exit fullscreen mode

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>,
  }
Enter fullscreen mode Exit fullscreen mode

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,

    }
Enter fullscreen mode Exit fullscreen mode

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(())
    }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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(())
    }
Enter fullscreen mode Exit fullscreen mode

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>,
    }
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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>,
    }
Enter fullscreen mode Exit fullscreen mode

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>,
    }
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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()
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

A seguir, execute o teste:

anchor test
Enter fullscreen mode Exit fullscreen mode

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()
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

Execute de novo:

anchor test
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

A estrutura de diretório de um aplicativo básico React feito com create-react-app:

├── public
├── src
|   └── app.js
├── package.json
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

A seguir, instale as dependências.

npm i @solana/wallet-adapter-react @solana/wallet-adapter-wallets @solana/web3.js
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Implante o programa com,

anchor deploy
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Em seguida, verifique o saldo de sua conta com o comando:

solana balance <your-account-address>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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],
  });
};
Enter fullscreen mode Exit fullscreen mode

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>{" "}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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],
  });
};
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

Mostre os posts na UI

{
  posts.map(({ title, content }, i) => {
    return (
      <div key={i}>
        <h2>{title}</h2>
        <p>{content}</p>
      </div>
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Defina a configuração Solana para a devnet
solana config set --url devnet

Enter fullscreen mode Exit fullscreen mode
  1. Abra o Anchor.toml e Atualize o cluster para a devnet
cluster = "devnet"

Enter fullscreen mode Exit fullscreen mode
  1. Construa o programa
anchor build

Enter fullscreen mode Exit fullscreen mode
  1. Implante o programa
anchor deploy
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)