Saltar al contenido principal

Construir un Frontend de Dapp

Esta es una continuación del tutorial de Comenzar, donde deberías haber desplegado dos contratos inteligentes en la red pública. En esta sección, crearemos una aplicación web que interactúe con los contratos a través de llamadas RPC.

Vamos a comenzar.

Inicializar una herramienta frontend

Puedes crear una aplicación Soroban con cualquier herramienta frontend o integrarla en cualquier aplicación de pila completa existente. Para este tutorial, vamos a usar Astro. Astro funciona con React, Vue, Svelte, cualquier otra biblioteca de interfaz de usuario, o ninguna biblioteca de interfaz de usuario en absoluto. En este tutorial, no estamos usando una biblioteca de interfaz de usuario. Las partes específicas de Soroban de este tutorial serán similares sin importar qué herramienta frontend uses.

Si eres nuevo en el frontend, no te preocupes. No profundizaremos demasiado. Pero será útil para ti ver y experimentar el proceso de desarrollo frontend utilizado por las aplicaciones Soroban. Cubriremos las partes relevantes de JavaScript y Astro, pero enseñar todo el desarrollo frontend y Astro está más allá del alcance de este tutorial.

Vamos a comenzar.

Vas a necesitar Node.js v18.14.1 o superior. Si aún no lo has hecho, instálalo ahora.

Queremos inicializar nuestro proyecto actual como un proyecto de Astro. Para hacer esto, podemos recurrir nuevamente al comando stellar contract init, que tiene una bandera --frontend-template que nos permite pasar la url de un repositorio de plantilla frontend. Como aprendimos en Almacenamiento de Datos, stellar contract init no sobrescribirá archivos existentes, y es seguro usarlo para agregar a un proyecto existente.

Desde nuestro directorio soroban-hello-world, ejecuta el siguiente comando para agregar los archivos de plantilla de Astro.

stellar contract init ./ \
--frontend-template https://github.com/stellar/soroban-astro-template

Esto añadirá lo siguiente a tu proyecto, que revisaremos con más detalle a continuación.

├── CONTRIBUTING.md
├── initialize.js
├── package-lock.json
├── package.json
├── packages
├── public
│   └── favicon.svg
├── src
│   ├── components
│   │   └── Card.astro
│   ├── env.d.ts
│   ├── layouts
│   │   └── Layout.astro
│   └── pages
│   └── index.astro
└── tsconfig.json

Generar un paquete NPM para el contrato Hello World

Antes de abrir los nuevos archivos frontend, generemos un paquete NPM para el contrato Hello World. Esta es nuestra forma sugerida de interactuar con los contratos desde los frontend. Estas bibliotecas generadas funcionan con cualquier proyecto de JavaScript (no un UI específico como React), y facilitan el trabajo con algunas de las partes más difíciles de Soroban, como la codificación de XDR.

Esto va a utilizar el comando CLI stellar contract bindings typescript:

stellar contract bindings typescript \
--network testnet \
--contract-id $(cat .stellar/contract-ids/hello_world.txt) \
--output-dir packages/hello_world

Este proyecto está configurado como un Workspace de NPM, por lo que la biblioteca cliente hello_world se generó en el directorio packages en packages/hello_world.

Intentamos mantener el código en estas bibliotecas generadas legible, así que ve y observa. Abre el nuevo directorio packages/hello_world en tu editor. Si has construido o contribuido a proyectos de Node, todo debería parecer familiar. Verás un archivo package.json, un directorio src, un tsconfig.json, e incluso un README.

Generar un paquete NPM para el contrato Increment

Aunque podemos ejecutar soroban contract bindings typescript para cada uno de nuestros contratos individualmente, la plantilla soroban-template-astro que utilizamos como nuestra plantilla incluye un script muy útil initialize.js que se encargará de esto para todos los contratos en nuestro directorio contracts.

Además de generar los paquetes NPM, initialize.js también:

  • Generar y financiar nuestra cuenta Stellar
  • Construir todos los contratos en el directorio contracts
  • Desplegar nuestros contratos
  • Crear clientes de contrato útiles para cada contrato

Ya nos hemos ocupado de los primeros tres puntos en pasos anteriores de este tutorial, por lo que esas tareas serán noops cuando ejecutemos initialize.js.

Configurar initialize.js

Necesitamos asegurarnos de que initialize.js tenga todas las variables de entorno que necesita antes de hacer nada más. Copia el archivo .env.example a .env. Las variables de entorno establecidas en .env son utilizadas por el script initialize.js.

cp .env.example .env

Veamos el contenido del archivo .env:

# Prefix with "PUBLIC_" to make available in Astro frontend files
PUBLIC_SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017"
PUBLIC_SOROBAN_RPC_URL="http://localhost:8000/soroban/rpc"

SOROBAN_ACCOUNT="me"
SOROBAN_NETWORK="standalone"

# env vars that begin with PUBLIC_ will be available to the client
PUBLIC_SOROBAN_RPC_URL=$SOROBAN_RPC_URL

Este archivo .env está configurado para conectarse a una red que se ejecuta localmente, pero queremos configurar nuestro proyecto para comunicarse con Testnet, ya que ahí es donde desplegamos nuestros contratos. Para hacer eso, actualicemos el archivo .env para que se vea así:

# Prefix with "PUBLIC_" to make available in Astro frontend files
-PUBLIC_SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017"
+PUBLIC_SOROBAN_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
-PUBLIC_SOROBAN_RPC_URL="http://localhost:8000/soroban/rpc"
+PUBLIC_SOROBAN_RPC_URL="https://soroban-testnet.stellar.org:443"

-SOROBAN_ACCOUNT="me"
+SOROBAN_ACCOUNT="alice"
-SOROBAN_NETWORK="standalone"
+SOROBAN_NETWORK="testnet"
información

Este archivo .env se utiliza en el script initialize.js. Al usar la CLI, aún podemos utilizar la configuración de red que establecimos en el paso de Configuración, o pasando las banderas --rpc-url y --network-passphrase.

Ejecutar initialize.js

Primero, instalemos las dependencias de JavaScript:

npm install

Y luego ejecutemos initialize.js:

npm run init

Como se mencionó anteriormente, este script intenta construir y desplegar nuestros contratos, lo cual ya hemos hecho. El script es lo suficientemente inteligente como para comprobar si un paso ya ha sido tratado, y es un no-op en ese caso, por lo que es seguro ejecutarlo más de una vez.

Llamar al contrato desde el frontend

Ahora abramos src/pages/index.astro y veamos cómo el código frontend se integra con el paquete NPM que creamos para nuestros contratos.

Aquí podemos ver que estamos importando nuestro cliente helloWorld generado desde ../contracts/hello_world. Luego, estamos invocando el método hello y agregando el resultado a la página.

src/pages/index.astro
---
import Layout from "../layouts/Layout.astro";
import Card from "../components/Card.astro";
import helloWorld from "../contracts/hello_world";
const { result } = await helloWorld.hello({ to: "you" });
const greeting = result.join(" ");
---

...

<h1>{greeting}</h1>

¡Veamos esto en acción! Inicia el servidor de desarrollo:

npm run dev

Y abre localhost:4321 en tu navegador. ¡Deberías ver el saludo del contrato!

Puedes intentar actualizar el argumento { to: 'Soroban' }. Cuando guardes el archivo, la página se actualizará automáticamente.

información

Cuando inicies el servidor de desarrollo con npm run dev, verás una salida similar en tu terminal a la que obtuviste cuando ejecutaste npm run init. Esto se debe a que el script dev en package.json está configurado para ejecutar npm run init y astro dev, para que puedas asegurarte de que tu contrato desplegado y tu paquete NPM generado estén siempre sincronizados. Si solo quieres iniciar el servidor de desarrollo sin el script initialize.js, puedes ejecutar npm run astro dev.

¿Qué está pasando aquí?

Si inspeccionas la página (clic derecho, inspeccionar) y actualizas, verás un par de cosas interesantes:

  • La pestaña "Red" muestra que no se han realizado solicitudes Fetch/XHR. ¡Pero las llamadas RPC ocurren a través de Fetch/XHR! ¿Entonces, cómo está llamando el frontend al contrato?
  • No hay JavaScript en la página. ¡Pero acabamos de escribir algo de JavaScript! ¿Cómo está funcionando?

Esta es parte de la filosofía de Astro: el frontend debe enviarse con la menor cantidad de activos posible. Prefiriendo cero JavaScript. Cuando pones JavaScript en el frontmatter, Astro lo ejecutará en el momento de la construcción y luego reemplazará cualquier cosa en los corchetes {...} con la salida.

Al usar el servidor de desarrollo con npm run dev, se ejecuta el código del frontmatter en el servidor e inyecta los valores resultantes en la página en el cliente.

Puedes intentar construir para ver esto de manera más evidente:

npm run build

Luego verifica la carpeta dist. Verás que se generó un archivo HTML y CSS, pero no JavaScript. Y si miras el archivo HTML, verás un "Hola Soroban" estático en el <h1>.

Durante la construcción, Astro realizó una única llamada a tu contrato, luego inyectó el resultado estático en la página. Esto es genial para métodos de contrato que no cambian, pero probablemente no funcionará para la mayoría de los métodos de contrato. Vamos a integrar con el contrato incrementor para ver cómo manejar métodos interactivos en Astro. -->

Llamar al contrato incrementor desde el frontend

Mientras hello es un método simple de solo lectura, increment cambia el estado on-chain. Esto significa que alguien necesita firmar la transacción. Así que necesitaremos agregar capacidades de firma de transacciones al frontend.

La forma en que funciona la firma en un navegador es con una billetera. Las billeteras pueden ser aplicaciones web, extensiones de navegador, aplicaciones independientes, o incluso dispositivos de hardware separados.

Instalar la extensión Freighter

En este momento, la billetera que mejor admite Soroban es Freighter. Está disponible como un complemento para Firefox, así como extensiones para Chrome y Brave. Ve y instálalo ahora.

Una vez instalado, ábrelo haciendo clic en el ícono de la extensión. Si esta es tu primera vez usando Freighter, necesitarás crear una nueva billetera. Sigue las indicaciones para crear una contraseña y guardar tu frase de recuperación.

Ve a Configuración (el ícono de la tuerca) → Preferencias y activa la opción para Habilitar Modo Experimental. Luego regresa a su pantalla de inicio y selecciona "Test Net" en el menú desplegable de la esquina superior derecha. Finalmente, si muestra el mensaje de que tu dirección Stellar no está financiada, ve y haz clic en el botón "Financiar con Friendbot".

Ahora estás completamente configurado para usar Freighter como usuario, y puedes agregarlo a tu aplicación.

Agregar StellarWalletsKit y configurarlo

Aunque estamos usando Freighter para probar nuestra aplicación, hay más billeteras que admiten la firma de transacciones de contratos inteligentes. Para facilitar su integración, estamos utilizando la biblioteca StellarWalletsKit que nos permite soportar todas las billeteras Stellar con una sola biblioteca.

Para instalar este kit, vamos a incluir el siguiente paquete:

npm install @creit.tech/stellar-wallets-kit

Con el paquete instalado, vamos a crear un nuevo archivo simple donde se encontrarán nuestro kit instanciado y estado simple. Crea el archivo src/stellar-wallets-kit.ts y pega esto:

src/stellar-wallets-kit.ts
import {
allowAllModules,
FREIGHTER_ID,
StellarWalletsKit,
WalletNetwork,
} from "@creit.tech/stellar-wallets-kit";

const kit: StellarWalletsKit = new StellarWalletsKit({
modules: allowAllModules(),
network: WalletNetwork.TESTNET,
selectedWalletId: FREIGHTER_ID,
});

const connectionState: { publicKey: string | undefined } = {
publicKey: undefined,
};

function loadedPublicKey(): string | undefined {
return connectionState.publicKey;
}

function setPublicKey(data: string): void {
connectionState.publicKey = data;
}

export { kit, loadedPublicKey, setPublicKey };

En el código anterior, creamos una instancia del kit y dos funciones simples que se encargarán de "establecer" y "cargar" la clave pública del usuario. Esto nos permite usar la clave pública del usuario en otras partes de nuestro código. El kit se inicia con Freighter como la billetera predeterminada y la red Testnet como la red predeterminada. Puedes aprender más sobre cómo funciona el kit en la documentación de StellarWalletsKit

Ahora vamos a agregar un botón de "Conectar" a la página que abrirá el modal incorporado del kit, y pedirá al usuario que use su billetera preferida. Una vez que el usuario elija su billetera preferida y otorgue permiso para aceptar solicitudes del sitio web, recuperaremos la clave pública y el botón de "Conectar" será reemplazado con un mensaje que dice: "Conectado como [su clave pública]".

Ahora agreguemos un nuevo componente al directorio src/components llamado ConnectWallet.astro con el siguiente contenido:

src/components/ConnectWallet.astro
<div id="connect-wrap" class="wrap" aria-live="polite">
<div class="ellipsis">
<button data-connect aria-controls="connect-wrap">Connect</button>
</div>
</div>

<style>
.wrap {
text-align: center;
}

.ellipsis {
line-height: 2.7rem;
margin: auto;
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
white-space: nowrap;
}
</style>

<script>
import { kit, setPublicKey } from "../stellar-wallets-kit";

const ellipsis = document.querySelector("#connect-wrap .ellipsis");
const button = document.querySelector("[data-connect]");

async function setLoggedIn(publicKey: string) {
ellipsis.innerHTML = `Signed in as ${publicKey}`;
ellipsis.title = publicKey;
}

button.addEventListener("click", async () => {
button.disabled = true;

try {
await kit.openModal({
onWalletSelected: async (option) => {
try {
kit.setWallet(option.id);
const { address } = await kit.getAddress();
setPublicKey(address);
await setLoggedIn(address);
} catch (e) {
console.error(e);
}
},
});
} catch (e) {
console.error(e);
}

button.disabled = false;
});
</script>

Algunas de estas cosas pueden parecer sorprendentes. ¿<style> y <script> en medio de la página? ¿Nombres de clase poco creativos como wrap? ¿Declaraciones de import en un <script>? ¿await de nivel superior? ¿Qué está pasando aquí?

Astro automáticamente delimita los estilos dentro de un componente a ese componente, así que no hay razón para que tengamos que idear nombres ingeniosos para nuestras clases.

Y todas las declaraciones de script se agrupan y se incluyen inteligentemente en la página. Incluso si usas el mismo componente múltiples veces, el script solo se incluirá una vez. Y sí, puedes usar await de nivel superior.

Puedes leer más sobre esto en la página de Astro sobre scripts del lado del cliente.

El código en sí aquí es bastante autocontenido. Importamos el kit de billeteras del archivo que creamos anteriormente. Luego, cuando el usuario haga clic en el botón, lanzamos el modal incorporado para mostrar las opciones de conexión al usuario. Una vez que el usuario elija su billetera preferida, la establecemos como la billetera predeterminada del kit antes de solicitar y guardar la dirección.

Ahora podemos importar el componente en el frontmatter de pages/index.astro:

pages/index.astro
 ---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
import helloWorld from "../contracts/hello_world";
+import ConnectWallet from '../components/ConnectWallet.astro'
...

Y agregarlo justo debajo del <h1>:

pages/index.astro
<h1>{greeting}</h1>
+<ConnectWallet />

Si ya no estás ejecutando tu servidor de dev, ve y reinícialo:

npm run dev

Luego abre la página y haz clic en el botón "Conectar". You should see Freighter pop up and ask you to sign in. Una vez que lo hagas, el botón debería ser reemplazado con un mensaje que dice: "Conectado como [tu clave pública]".

¡Ahora estás listo para firmar la llamada a increment!

Llamar a increment

Ahora podemos importar el cliente del contrato increment desde soroban_increment_contract y comenzar a usarlo. Nuevamente, crearemos un nuevo componente Astro. Crea un nuevo archivo en src/components/Counter.astro con el siguiente contenido:

src/components/Counter.astro
<strong>Incrementor</strong><br />
Current value: <strong id="current-value" aria-live="polite">???</strong><br />
<br />
<button data-increment aria-controls="current-value">Increment</button>

<script>
import { kit, loadedPublicKey } from "../stellar-wallets-kit";
import incrementor from "../contracts/soroban_increment_contract";
const button = document.querySelector("[data-increment]");
const currentValue = document.querySelector("#current-value");

button.addEventListener("click", async () => {
const publicKey = loadedPublicKey();

if (!publicKey) {
alert("Please connect your wallet first");
return;
} else {
incrementor.options.publicKey = publicKey;
}

button.disabled = true;
button.classList.add("loading");
currentValue.innerHTML =
currentValue.innerHTML +
'<span class="visually-hidden"> – updating…</span>';

const tx = await incrementor.increment();

try {
const { result } = await tx.signAndSend({
signTransaction: async (xdr) => {
return await kit.signTransaction(xdr);
},
});

// Only use `innerHTML` with contract values you trust!
// Blindly using values from an untrusted contract opens your users to script injection attacks!
currentValue.innerHTML = result.toString();
} catch (e) {
console.error(e);
}

button.disabled = false;
button.classList.remove("loading");
});
</script>

Esto debería ser algo familiar para ahora. Tenemos un script que, gracias al sistema de construcción de Astro, puede importar módulos directamente. Usamos document.querySelector para encontrar los elementos definidos arriba. Y agregamos un manejador de click al botón, que llama a increment y actualiza el valor en la página. También establece el botón como deshabilitado y agrega una clase de cargando mientras la llamada está en progreso para evitar que el usuario haga clic nuevamente y comunicar visualmente que algo está sucediendo. Para las personas que utilizan lectores de pantalla, el estado de carga se comunica con la etiqueta visualmente oculta, que será anunciada gracias a las etiquetas aria que vimos antes.

La mayor diferencia con la llamada a greeter.hello es que esta transacción se ejecuta en dos pasos. La llamada inicial a increment construye una transacción de Soroban y luego realiza una llamada RPC para simularla. Para llamadas de solo lectura como hello, esto es todo lo que necesitas, por lo que puedes obtener el resultado de inmediato. Para llamadas de escritura como increment, debes signAndSend antes de que la transacción sea realmente incluida en el ledger.

información

Desestructuración { result }: Si eres nuevo en JavaScript, puede que no sepas lo que está sucediendo con esas líneas const { result }. Esto está utilizando la característica de desestructuración de JavaScript. Si la cosa a la derecha del signo igual es un objeto, entonces puedes usar este patrón para obtener rápidamente claves específicas de ese objeto y asignarlas a variables. También puedes nombrar la variable de otra manera, si lo deseas. Por ejemplo, intenta cambiar el código de arriba a:

const { result: newValue } = ...

También, ten en cuenta que no necesitas especificar manualmente a Freighter como la billetera en la llamada a increment. Esto puede cambiar en el futuro, pero mientras Freighter sea la única opción disponible, estas bibliotecas generadas la utilizan automáticamente. Si deseas anular este comportamiento, puedes pasar una opción wallet; consulta la última interfaz Wallet en la fuente de la plantilla para más detalles.

Ahora, usemos este componente. En pages/index.astro, primero impórtalo:

pages/index.astro
 ---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
import helloWorld from "../contracts/hello_world";
import ConnectFreighter from '../components/ConnectFreighter.astro';
+import Counter from '../components/Counter.astro';
...

Luego úsalo. Reemplacemos el contenido del párrafo instrucciones con él:

pages/index.astro
 <p class="instructions">
- To get started, open the directory <code>src/pages</code> in your project.<br />
- <strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
+ <Counter />
</p>

Revisa la página; si aún estás ejecutando tu servidor de desarrollo, debería haberse actualizado ya. Haz clic en el botón "Incrementar"; deberías ver una confirmación de Freighter. Confirma, y... ¡el valor se actualiza! 🎉

Obviamente, falta algo de funcionalidad. Por ejemplo, ese ??? es frustrante. Pero nuestro contrato incrementar no nos da una manera de consultar el valor actual sin actualizarlo también.

Antes de intentar actualizarlo, simplifiquemos el proceso de creación, implementación y generación de clientes para los contratos.

Llévalo más allá

Si quieres llevarlo un poco más lejos y asegurarte de entender todas las piezas aquí, prueba lo siguiente:

  • Crea una carpeta src/contracts con un greeter.ts y un incrementor.ts. Mueve la lógica de new Contract({ ... }) a esos archivos. También querrás extraer la variable rpcUrl a un archivo src/contracts/utils.ts.
  • Agrega un método get_value al contrato incrementar, y úsalo para mostrar el valor actual en el componente Contador. Cuando ejecutes npm run dev, el script initialize se ejecutará y actualizará el contrato y el cliente generado.
  • Agrega un botón "Decrementar" al componente Contador.
  • Implementa tu frontend. Puedes hacerlo rápido y gratis con GitHub. Si tienes problemas instalando stellar-cli y desplegando contratos en GitHub, consulta cómo lo hicimos.
  • En lugar de usar scripts de NPM para todo, intenta usar un corredor de scripts más elegante como just. Los scripts de npm existentes pueden llamar a just, como "setup": "just setup".
  • Actualiza el README para explicar qué es este proyecto y cómo usarlo a posibles colaboradores y empleadores 😉

Resolución de problemas

A veces las cosas salen mal. Como primer paso al resolver problemas, es posible que desees clonar nuestro repositorio del tutorial y ver si el problema ocurre allí también. Si ocurre allí también, entonces puede ser un problema temporal con la red de Soroban.

Aquí hay algunas incidencias comunes y cómo corregirlas.

La llamada a hello falla

A veces la llamada a hello puede comenzar a fallar. Obviamente puedes simular la llamada y definir result de otra manera para solucionar problemas.

Uno de los problemas comunes aquí es que el contrato se vuelve archivado. Para verificar si este es el problema, puedes ejecutar de nuevo npm run init.

Si sigues teniendo problemas, únete a nuestro Discord (enlace de arriba) o abre un problema en GitHub.

Todas las llamadas a contratos comienzan a lanzar errores 403

Esto significa que Testnet está fuera de servicio, y probablemente solo necesites esperar un poco y volver a intentarlo.

Conclusión

Algunas de las cosas que hicimos en esta sección:

  • Aprendimos sobre el enfoque sin JS por defecto de Astro
  • Agregamos componentes de Astro y aprendimos cómo funcionan sus etiquetas script y style
  • Vimos lo fácil que es interactuar con contratos inteligentes desde JavaScript generando bibliotecas de clientes usando stellar contract bindings typescript
  • Aprendimos sobre billeteras y Freighter

¡En este punto, has visto un ejemplo completo de inicio a fin de la creación de un contrato en Stellar! ¿Qué sigue? ¡Tú decides! Puedes: