WEB3DEV Español

Hector
Hector

Posted on • Updated on

La Guía Completa para el desarrollo Full Stack de Solana con: React, Anchor, Rust y Phantom

Este artículo es una traducción de Nader Dabit hecha por Héctor Botero. Puedes encontrar el artículo original aquí.

Construyendo un dapps Full Stack con: React, Solana, Anchor y la cartera Phantom.

En la Guía Completa para el desarrollo Full Stack Ethereum profundicé en cómo construir un dapp full stack en Ethereum, el cual también puede ser aplicado a otras blockchains EVM compatibles como Polygon, Avalanche y la segunda capa de Ethereum como Arbitrum.

En esta guía, quiero profundizar en Solana para mostrarte cómo construir un dapp full stack. También quiero introducirte al ecosistema y a las herramientas del desarrollador para poder, con suerte, ayudarte a que puedas iniciar y construir tus propias ideas y aplicaciones en el futuro

El código del proyecto está aquí.

Resumen del desarrollador Solana

Como alguien que acaba de empezar a aprender Solidity y su ecosistema hace unos 6 meses, yo más o menos asumía que no podía ser tan difícil hacerlo y ejecutarlo. Estaba equivocado.

Partes de las herramientas del desarrollador son muy buenas y pulidas (como Solana CLI y Anchor), mientras que el resto del ecosistema, e incluso la documentación para Anchor (que, siendo justo, es muy nueva), deja mucho que desear.

Dicho esto, una vez que le agarres la mano a todo, rápidamente se vuelve mucho más fácil entender cómo empezar a implementar tus propias ideas y comenzar a experimentar.

Una de las claves para encontrar respuestas es estar atento en buscar en Google, Github y, especialmente, en los diferentes servidores de Anchor y Solana, en Discord. Los desarrolladores de estos canales fueron extremadamente útiles, especialmente Armani Ferrante, que creó el framework Anchor. Familiarízate con la función de búsqueda. A menudo, puedes encontrar respuestas a tus preguntas en discusiones anteriores en Discord.

Resumen del Proyecto

Las herramientas que hoy estaremos usando incluye:

  • Solana Tool Suite - Esto incluye una interfaz de línea de comandos CLI, muy buen pulida y bien documentada, para interactuar con la red de Solana.
  • Anchor Framework - Anchor es, en realidad, un salvavidas para mi y, estoy casi seguro, que no hubiese podido ser capaz de superar el obstáculo de construir nada sin él. Es el Hardhat del desarrollo de Solana y mucho más, y me gusta. También ofrece un DSL sobre Rust, así que no necesitas un conocimiento profundo del lenguaje para comenzar, aunque aún estoy intentando aprender Rust ya que, probablemente, será útil para construir cualquier cosa que no sea trivial, incluso con DSL. Un buen lugar gratuito para aprender rust es The Rust book.
  • Solana/web3.js - Una versión de Solana de la web3.js que parece funcionar muy bien, pero la documentación era, prácticamente, inservible para mi.
  • Rehttps://reactjs.org/act - El framework del lado del cliente.

Voy a dejar de lado la profundidad de todos los detalles acerca de cómo Solana, en sí, trabaja ya que, otras personas pueden cubrir esto mejor que yo. En cambio, intentaré solo enfocarme en construir algo, compartiendo los detalles que necesitas saber para lograr esto, además de cosas que, pienso, que son de vital importancia.

Si quieres saber más sobre Solana y cómo funciona, aquí hay algunos buenos lugares:

En esta guía nos enfocaremos principalmente en la instalación del proyecto, hacer las pruebas, y la integración del cliente de front end, para construir un par de tipos de aplicaciones, sobre todo enfocadas en las operaciones CRUD (claro, sin delete), el cual encuentro que no está muy documentado (la integración con las aplicaciones clientes).

También aprenderemos a cómo hacer airdrop de los tokens a nuestra propia cuenta del desarrollo, usando Solana CLI y haciendo el despliegue en nuestras aplicaciones, en una red local así como una red de prueba en vivo.

No nos enfocaremos en los NFTs en esta guía, pero puede que me enfoque en hacer una guía en el futuro. Por ahora, si estás interesado en construir un mercado de NFT en Solana, te recomiendo revisar nuestro Metaplex.

Prerrequisitos

Este tutorial cubre cómo construir una aplicación full stack en Solana, pero no cubre cómo instalar todas las dependencias individuales.

En cambio, enumeraré una lista de todas las dependencias y dejaré el enlace a la documentación de cómo instalarlos, ya que cada proyecto será capaz de explicar y documentar estas cosas de una mejor manera de lo que yo podría hacer, además de mantenerlas actualizadas.

  1. Node.js - Recomiendo instalar Node usando cualquiera nvm o fnm
  2. Solana Tool Suite - Puedes ver las instrucciones de la instalación aquí. Nota: si tienes cualquier problema instalando Solana en una Mac M1, intenta construirlo desde el código fuente y revisa esta guía.
  3. Anchor (incluye la instalación de Mocha) - La instalación de Anchor fue muy fácil para mi. Puedes encontrar las instrucciones aquí.
  4. Cartera del navegador de Solana - Yo te recomiendo Phantom, ya que con esto fue con lo que probé esta aplicación.

Primeros pasos

Antes de comenzar a construir, vamos a dar un vistazo a Solana CLI.

Solana CLI

Las cosas principales que estaremos haciendo con Solana CLI será la configuración de nuestra red (entre el localhost y el testnet del desarrollador), así como el airdrop de los tokens en nuestra cartera. Todo lo demás, lo estaremos haciendo con Anchor CLI.

En este caso, por ejemplo, podemos revisar la red actual (y otras) configuraciones con este comando:

solana config get

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

Si no tienes un Keypair path, configura uno siguiendo las instrucciones aquí

Podemos cambiar la red así:

# set to localhost
solana config set --url localhost

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

Esto es importante ya que necesitarás estar consciente de cuál red estás usando mientras vas construyendo, probando y desplegando tus programas. También necesitas asegurarte que tu cartera está usando la misma red que tu entorno local está usando cuando estés probando, tema que estaré abordando.

Estaremos comenzando el desarrollo en una red localhost, cambiando luego a la red devnet.

También podemos usar CLI para ver la dirección actual de nuestra cartera:

solana address
Enter fullscreen mode Exit fullscreen mode

Y luego tener los detalles completos de una cuenta:

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

Luego, haremos un airdrop de algunos tokens. Para hacerlo, primero cambia a la red local, ya que aquí es donde estaremos trabajando para comenzar:

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

Luego, inicia la red local. Esto será un nodo local de Solana que podremos desplegar para hacer la prueba:

solana-test-validator
Enter fullscreen mode Exit fullscreen mode

Una vez que la red local esté ejecutándose, puedes hacer el airdrop de los tokens a tu cuenta. Con la red ejecutada, abre una ventana separada y ejecuta el siguiente comando:

solana airdrop 100
Enter fullscreen mode Exit fullscreen mode

Puedes revisar el balance de tu cartera:

solana balance

# or

solana balance <address>
Enter fullscreen mode Exit fullscreen mode

Ahora deberías tener un balance de 100 SOL en tu cartera. Con eso, podemos empezar a construir

Empecemos a construir

Para poder comenzar, inicializa un nuevo proyecto de anchor y cámbialo al nuevo directorio:

anchor init mysolanaapp --javascript

cd mysolanaapp
Enter fullscreen mode Exit fullscreen mode

Asegúrate de usar la versión de Anchor 0.16.0 o la más nueva.

En este proyecto, verás nuestras cuatro carpetas principales (además del node_modules):

  • App - Donde irá nuestro código fronted
  • Programas - Aquí es donde el código Rust está, para el programa de Solana
  • Test - Aquí es donde JavaScript prueba el programa
  • Migraciones - El despliegue básico de un script

Vamos a ver el programa que fue creado por nosotros.

Anchor usa, y nos permite escribir, un eDSL (embedded DSL) que abstrae muchas de las operaciones complejas de bajo nivel que usualmente necesitarías hacer si estuvieses usando Solana y Rust sin el eDSL, haciendo que sea más amigable para mi.

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

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

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

Este es, probablemente, el programa más básico que puedas escribir. La única cosa que sucede aquí es que estamos definiendo una función llamada initialize que, cuando es invocada, solo sale del programa exitosamente. No hay ninguna manipulación de los datos.

El struct Initialize define el contexto, siendo vacío de argumentos. Aprenderemos más sobre el contexto de la función después.

Para compilar este programa, podemos ejecutar el comando build de Anchor:

anchor build
Enter fullscreen mode Exit fullscreen mode

Una vez que esté compilado, deberías ver una nueva carpeta llamada target.

Uno de los artefactos creados es un IDL localizado en target/idl/mysolanaapp.json.

Los IDL son muy similares a los ABI en Solidity (o una definición de consulta en GraphQL), y los estaremos usando de una forma similar en las pruebas de nuestro JavaScript y frontends, para comunicar con nuestro programa de Solana a través de RPC.

También podemos probar nuestro programa. Si abres tests/mysolanaapp.jps, verás que hay una prueba escrita en JavaScript que nos permite probar nuestro programa.

La prueba debería verse como esta:

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

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

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

Hay un par de cosas para aprender de esta prueba que son importantes y estaremos usándolas a futuro para ambas cosas: nuestras pruebas como también en el cliente frontend de JavaScript.

Para llamar el programa de Solana usando Anchor, normalmente necesitaremos de dos cosas principales:

  1. **Provider** - El Provider es una abstracción de la conexión a la red de Solana, que normalmente consiste de una [conexión](https://solana-labs.github.io/solana-web3.js/classes/Connection.html), una cartera y el commitment preflight.

En la prueba, el framework Anchor creará un proveedor para nosotros, basado en el entorno (anchor.Provider.env()), pero en el cliente necesitaremos construir el Proveedor nosotros mismos, usando el usuario de la cartera de Solana.

  1. **Program** - El program es una abstracción que combina el Provider , idl , y el programID (el cual es generado cuando el programa es compilado), y nos permite invocar métodos RPC respecto al de nuestro programa.

De nuevo, como con el Provider, Anchor ofrece una forma conveniente para acceder al program pero, cuando estemos construyendo el front end, necesitaremos construir este provider nosotros mismos.

Una vez que tengamos estas dos cosas, podremos empezar a invocar funciones en nuestro programa. Por ejemplo, en nuestro programa tenemos la función initialize . En nuestra prueba, puedes ver que hemos invocado la función directamente program.rpc.functionName :

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

Este es un patrón muy común que lo usarás mucho cuando estés trabajando con Anchor, y una vez que empieces a entender cómo funciona, hace que sea muy fácil conectarse e interactuar con el programa de Solana.

Ahora podemos probar el programa ejecutando el script test :

anchor test
Enter fullscreen mode Exit fullscreen mode

Construyendo el Hola Mundo

Ahora que tenemos la configuración de nuestro proyecto, vamos a crear algo mucho más interesante.

Yo se que, como un desarrollador full stack, la mayoría del tiempo me estoy preguntando cómo hacer operaciones de tipo CRUD, así que eso es lo que veremos en seguida.

El primer programa que crearemos nos permitirá crear un contador que se incrementa cada vez que lo invocamos desde la aplicación cliente.

La primera cosa que necesitamos hacer es abrir programs/mysolanaapp/src/libs.rs y actualizarlo con el siguiente 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,
}
Enter fullscreen mode Exit fullscreen mode

En este programa tenemos dos funciones - create e increment . Estas dos funciones son el pedido del controlador del RPC, que tendremos la capacidad de llamar desde la aplicación cliente para interactuar con el programa.

El primer parámetro de un controlador RPC es la estructura del Contexto. el cual, describe el contexto al que se pasará cuando la función es llamada y el cómo será manejada. En el caso de Create , estamos esperando tres parámetros: base_account , user , y system_program .

Los atributos #[account(...)] definen las limitaciones e instrucciones que están relacionadas con la cuenta del procedimiento cuando se declare. Si cualquiera de las limitaciones no se mantienen, entonces la instrucción nunca se ejecutará.

Cualquier cliente que invoque este programa con el adecuado base_account puede invocar cualquiera de estos métodos RPC.

La forma en la que Solana se encarga de los datos es muy distinto a cualquier otra cosa en la que haya trabajado. No hay un estado persistente dentro del programa, todo está adjunto a lo que se conoce como cuentas. Una cuenta, en esencia, mantiene todos los estados del programa. Por esto, todos los datos son pasados por la referencia desde afuera.

Tampoco hay operaciones de lectura. Esto es porque todo lo que necesitas para leer los contenidos del programa es hacer el pedido a la cuenta, de allí podrás ver todos los estados del programa. Para leer más sobre cómo funcionan las cuentas, revisa este post.

Para construir el programa:

anchor build
Enter fullscreen mode Exit fullscreen mode

Ahora, vamos a escribir una prueba que usa este contador del programa. Para hacerlo, abre tests/mysolanaapp.js y actualiza con el siguiente código:

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

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

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

  });

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

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

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

Antes de continuar probando y desplegando el programa, queremos obtener el Program ID generado dinámicamente por build. Necesitamos este ID para usarlo en el programa de Rust para reemplazar el ID por defecto que hicimos cuando creamos el proyecto. Para obtener este ID, podemos usar el siguiente comando:

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

Ahora podemos actualizar el ID del programa en lib.rs:

// mysolanaapp/src/lib.rs

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

Y también en Anchor.toml:

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

Ahora, ejecuta la prueba:

anchor test
Enter fullscreen mode Exit fullscreen mode

Una vez la prueba sea aprobada, ahora podemos hacer el despliegue.

Ahora podemos desplegar el programa. Aseguráte que solana-test-validator esté ejecutando:

anchor deploy
Enter fullscreen mode Exit fullscreen mode

También puedes ver el log del validador, abriendo una ventana separada y ejecutando solana logs

Ahora estamos listos para construir el frontend.

Construyendo la aplicación React

En la raíz del proyecto de Anchor, crea una nueva aplicación react para sobreescribir el directorio existente app:

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

Luego, instala las dependencias que necesitaremos para Anchor y para Solana Web3:

cd app

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

También estaremos usando Solana Wallet Adapter para manejar la conexión del usuario de la cartera de Solana. Vamos a instalar esas dependencias también:

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

Después, en el directorio src, crea un nuevo archivo llamado idl.json. Aquí, copia el IDL JSON que creaste en la carpeta del proyecto principal, ubicado en target/idl/mysolanaapp.json.

Sería bueno si pudiésemos copiar este archivo idl automáticamente al archivo de la aplicación de nuestro cliente src, pero por ahora no he encontrado una forma nativa de hacer esto. Puedes, por supuesto, crear tu propio script que hace esto si tú quieres, si no, necesitas copiar y pegar sobre el IDL luego de cada cambio a tu programa principal.

Si quieres un script como este, puedes hacerlo en solo un par de líneas de código:

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

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

Luego, abre app/src/App.js y actualízalo con el siguiente código:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

export default AppWithProvider;
Enter fullscreen mode Exit fullscreen mode

Cambiarte a la red de tu cartera

Antes que podamos interactuar con el programa con la red localhost , tenemos que cambiar nuestra cartera Phantom a la red adecuada.

Para hacerlo, abre la cartera Phantom y haz click en el botón settings. Luego baja a Change Network:

Image description

Luego elige Localhost:

Image description

Ahora, necesitamos hacer el airdrop de los tokens a esta cartera. En el tope de la interfaz de la cartera, haz click en la dirección para copiarla a tu portapapeles.

Image description

Luego, abre tu terminal y ejecuta este comando (asegúrate que solana-test-validator esté ejecutándose):

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

Deberías tener 10 tokens en tu cartera. Ahora, ¡podemos ejecutar y probar la aplicación!

Cambia al directorio de la aplicación y ejecuta el siguiente comando:

npm start
Enter fullscreen mode Exit fullscreen mode

Deberías poder conectarte con tu cartera, crear un contador e incrementarlo

Te darás cuenta que cuando recargas, pierdes el estado del programa. Esto es porque estamos generando dinámicamente la cuenta base, cuando el programa se carga. Si quieres leer e interactuar con los datos del programa entre varios clientes, necesitas crear y guardar el Keypair en algún lugar de tu proyecto. He construido un gist con un acercamiento ingenuo de cómo esto se puede ver.

Hola Mundo parte 2

Vamos a crear una variación de este programa en el que, en vez de lidiar con un contador, nos permite crear un mensaje y mantener el rastreo de todos los mensajes que se han creado con anterioridad.

Para hacerlo, vamos a actualizar nuestro programa Rust para que se vea así:

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

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

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

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

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

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

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

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

En este programa, tenemos dos piezas principales de datos que vamos a estar rastreando: un String llamado data y un Vector que mantiene la lista de todos los datos que se han añadido al programa llamado data_list .

Te percatarás que aquí, la asignación de la memoria es mucho más alta ( 128 + 128 ) que la de los programas anteriores, para poder hacer la cuenta del Vector. No sé cuántas actualizaciones serás capaz de guardar en este programa pero, puede ser algo para investigar o experimentar más, ya que este ejemplo en sí mismo, es experimental y solo es para darte una idea de cómo funcionan las cosas.

Luego, vamos a actualizar la prueba para este nuevo programa:

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

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

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

  });

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

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

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

Para probarlo:

anchor test
Enter fullscreen mode Exit fullscreen mode

Si la prueba falla, intenta desactivar el validador y luego, ejecútalo otra vez.

Después, vamos a actualizar el cliente:

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

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

const wallets = [ new PhantomWalletAdapter() ]

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

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

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

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

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

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

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

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

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

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

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

export default AppWithProvider;
Enter fullscreen mode Exit fullscreen mode

Luego, compila y despliega el programa (asegúrate que solana-test-validator is running ):

anchor build

anchor deploy
Enter fullscreen mode Exit fullscreen mode

Con la nueva compilación, tendrás un nuevo IDL que necesitarás actualizar para tu cliente. Puedes copiar el nuevo IDL en app/src/idl.json o ejecutar tu script copyldl.js.

Probándolo

Cuando probamos el programa nuevo, asegúrate de actualizar el archivo idl.json que fue creado en la compilación.
Cambia al directorio de la app y corre el comando start :

npm start
Enter fullscreen mode Exit fullscreen mode

Desplegando a Devnet

Desplegar a una red en vivo es muy sencillo desde aquí. Las cosas principales que necesitamos hacer son:

  1. Actualiza el Solana CLI para usar devnet :
solana config set --url devnet
Enter fullscreen mode Exit fullscreen mode
  1. Actualiza la cartera Phantom para usar devnet
  2. Abre Anchor.toml y actualiza el clúster de localnet a devnet .
  3. Reconstruye el programa. Asegúrate que el ID en Anchor.toml coincida con el programa ID actual.
  4. Despliega el programa de nuevo, esta vez será desplegado en devnet
  5. En app/src/App.js, también necesitamos actualizar la red, esta vez usando clusterApiUrl de @solana/web3 , como esta:
/* before */
<ConnectionProvider endpoint="http://127.0.0.1:8899">

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

const network = clusterApiUrl('devnet');

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

De aquí, deberías poder hacer el despliegue y probarlo, como ya hemos hecho en pasos anteriores.

El código para este proyecto está aquí.

Siguientes pasos

Otro tutorial con profundidad, que aconsejo revisar después, es Crea una dApp de Solana desde cero que implementa una versión simplificada de Twitter como una dapp de Solana.

Si estás interesado en trabajar con tecnologías similares, a tiempo completo, únete a mi equipo en Edge & Node, ¡estamos contratando!

Discussion (0)