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"
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.
---
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.
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:
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:
<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 = ``;
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
:
---
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>
:
<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:
<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.
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:
---
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:
<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 ungreeter.ts
y unincrementor.ts
. Mueve la lógica denew Contract({ ... })
a esos archivos. También querrás extraer la variablerpcUrl
a un archivosrc/contracts/utils.ts
. - Agrega un método
get_value
al contratoincrementar
, y úsalo para mostrar el valor actual en el componenteContador
. Cuando ejecutesnpm run dev
, el scriptinitialize
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
ystyle
- 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:
- Ver contratos de ejemplo más complejos en la sección Contratos Ejemplo.
- Aprender más sobre la arquitectura interna y diseño de Soroban.
- Aprender cómo encontrar otras plantillas para usar con
--frontend-template
, aparte de stellar/soroban-template-astro, y cómo crear la tuya: Desarrollar plantillas de frontend para inicialización de contratos