Crear una interfaz de usuario para su contrato inteligente.
Ha encontrado una funcionalidad que necesitamos en el ecosistema Ethereum. Escribió los smart contracts para implementarla, y quizás incluso algo de código relacionado que se ejecuta fuera de la cadena (offchain). ¡Esto es genial! Desafortunadamente, sin una interfaz de usuario no tendrá usuarios, y la última vez que creó un sitio web las personas usaban módems de acceso telefónico y JavaScript era nuevo.
Este artículo es para usted. Supongo que sabe programar, y quizá algo de JavaScript y HTML, pero sus habilidades en interfaces de usuario están algo oxidadas y desactualizadas. Juntos revisaremos una aplicación sencilla y moderna para que vea cómo se hace hoy en día.
¿Por qué es esto importante?
En teoría, usted podría simplemente hacer que las personas usen Etherscanopens in a new tab o Blockscoutopens in a new tab para interactuar con sus contratos. Eso será excelente para los usuarios experimentados de Ethereum. Pero intentamos servir a otro mil millones de personasopens in a new tab. Esto no sucederá sin una excelente experiencia de usuario, y una interfaz de usuario amigable es una parte fundamental de ello.
Aplicación Greeter
Existe mucha teoría detrás de cómo funciona una interfaz de usuario moderna, y muchos buenos sitiosopens in a new tab que lo explicanopens in a new tab. En vez de repetir el excelente trabajo de esos sitios, asumiré que prefiere aprender haciendo y empezaré con una aplicación que puede probar. Aun así necesita la teoría para realizar las tareas, y la abordaremos: simplemente revisaremos archivo por archivo y discutiremos los temas a medida que los vayamos encontrando.
Instalación
-
Si es necesario, agregue la blockchain Holeskyopens in a new tab a su wallet y obtenga ETH de pruebaopens in a new tab.
-
Clone el repositorio de github.
1git clone https://github.com/qbzzt/20230801-modern-ui.git -
Instale los paquetes necesarios.
1cd 20230801-modern-ui2pnpm install -
Inicie la aplicación.
1pnpm dev -
Vaya a la URL mostrada por la aplicación. En la mayoría de los casos, es http://localhost:5173/opens in a new tab.
-
Puede ver el código fuente del contrato, una versión ligeramente modificada del Greeter de Hardhat, en un explorador de blockchainopens in a new tab.
Recorrido por los archivos
index.html
Este archivo es un archivo HTML estándar de inicio, excepto por esta línea, que importa el archivo de script.
1<script type="module" src="/src/main.tsx"></script>src/main.tsx
La extensión del archivo nos indica que este archivo es un componente de Reactopens in a new tab escrito en TypeScriptopens in a new tab, una extensión de JavaScript que soporta verificación de tiposopens in a new tab. TypeScript se compila en JavaScript, por lo que podemos usarlo para ejecución en el lado del cliente.
1import '@rainbow-me/rainbowkit/styles.css'2import { RainbowKitProvider } from '@rainbow-me/rainbowkit'3import * as React from 'react'4import * as ReactDOM from 'react-dom/client'5import { WagmiConfig } from 'wagmi'6import { chains, config } from './wagmi'Importe el código de las librerías que necesitamos.
1import { App } from './App'Importe el componente de React que implementa la aplicación (ver más abajo).
1ReactDOM.createRoot(document.getElementById('root')!).render(Cree el componente raíz de React. El parámetro de render es JSXopens in a new tab, un lenguaje de extensión que utiliza tanto HTML como JavaScript/TypeScript. El signo de exclamación aquí le indica al componente TypeScript: "no sabe que document.getElementById('root') será un parámetro válido para ReactDOM.createRoot, pero no se preocupe: soy el desarrollador y le digo que lo será".
1 <React.StrictMode>La aplicación irá dentro de un componente React.StrictModeopens in a new tab. Este componente indica a la librería de React que inserte comprobaciones de depuración adicionales, lo cual es útil durante el desarrollo.
1 <WagmiConfig config={config}>La aplicación también está dentro de un componente WagmiConfigopens in a new tab. La librería wagmi (we are going to make it)opens in a new tab conecta las definiciones de la interfaz de React con la librería viemopens in a new tab para escribir una aplicación descentralizada de Ethereum.
1 <RainbowKitProvider chains={chains}>Y finalmente, un componente RainbowKitProvideropens in a new tab. Este componente gestiona el inicio de sesión y la comunicación entre la wallet y la aplicación.
1 <App />Ahora podemos tener el componente para la aplicación, que realmente implementa la interfaz de usuario. El /> al final del componente le indica a React que este componente no tiene otras definiciones dentro, según el estándar XML.
1 </RainbowKitProvider>2 </WagmiConfig>3 </React.StrictMode>,4)Por supuesto, tenemos que cerrar los demás componentes.
src/App.tsx
1import { ConnectButton } from '@rainbow-me/rainbowkit'2import { useAccount } from 'wagmi'3import { Greeter } from './components/Greeter'45export function App() {Esta es la manera estándar de crear un componente de React: definir una función que se llama cada vez que necesita renderizarse. Esta función normalmente incluye algo de código TypeScript o JavaScript en la parte superior, seguido de una declaración return que regresa el código JSX.
1 const { isConnected } = useAccount()Aquí utilizamos useAccountopens in a new tab para comprobar si estamos conectados a una blockchain mediante una wallet o no.
Por convención, en React las funciones llamadas use... son hooksopens in a new tab que devuelven algún tipo de dato. Cuando se utilizan estos hooks, el componente obtiene los datos y, cuando esa información cambia, el componente se vuelve a renderizar con la información actualizada.
1 return (2 <>El JSX de un componente React debe regresar un solo componente. Cuando tenemos varios componentes y no hay uno que los envuelva de forma "natural", utilizamos un componente vacío (<> ... </>) para agruparlos en un solo componente.
1 <h1>Greeter</h1>2 <ConnectButton />Obtenemos el componente ConnectButtonopens in a new tab de RainbowKit. Cuando no estamos conectados, nos muestra un botón Connect Wallet que abre un modal donde se explica qué son las wallets y permite elegir cuál usar. Cuando estamos conectados, muestra la blockchain que usamos, nuestra dirección de cuenta y el saldo de ETH. Podemos utilizar estas vistas para cambiar de red o desconectarnos.
1 {isConnected && (Cuando necesitamos insertar JavaScript real (o TypeScript que será compilado a JavaScript) dentro de JSX, utilizamos llaves ({}).
La sintaxis a && b es una forma abreviada de a ? b : aopens in a new tab. Es decir, si a es verdadero, evalúa a b y, en caso contrario, evalúa a a (que puede ser false, 0, etc.). Esta es una forma sencilla de indicarle a React que un componente solo debe mostrarse si se cumple determinada condición.
En este caso, solo queremos mostrarle al usuario Greeter si está conectado a una blockchain.
1 <Greeter />2 )}3 </>4 )5}src/components/Greeter.tsx
Este archivo contiene la mayoría de la funcionalidad de la interfaz de usuario. Incluye definiciones que normalmente estarían en varios archivos, pero como esto es un tutorial, el programa está optimizado para que sea fácil de entender la primera vez, en vez de priorizar el rendimiento o la facilidad de mantenimiento.
1import { useState, ChangeEventHandler } from 'react'2import { useNetwork,3 useReadContract,4 usePrepareContractWrite,5 useContractWrite,6 useContractEvent7 } from 'wagmi'Utilizamos estas funciones de la biblioteca. Nuevamente, se explican más abajo donde se usan.
1import { AddressType } from 'abitype'La biblioteca abitypeopens in a new tab nos proporciona definiciones TypeScript para varios tipos de datos de Ethereum, como AddressTypeopens in a new tab.
1let greeterABI = [2 .3 .4 .5] as const // greeterABIEl ABI para el contrato Greeter.
Si está desarrollando los contratos y la interfaz a la vez, normalmente los pondría en el mismo repositorio y usaría el ABI generado por el compilador de Solidity como un archivo en la aplicación. Sin embargo, esto no es necesario aquí porque el contrato ya está desarrollado y no va a cambiar.
1type AddressPerBlockchainType = {2 [key: number]: AddressType3}TypeScript es fuertemente tipado. Utilizamos esta definición para especificar la dirección donde el contrato Greeter está desplegado en diferentes cadenas. La clave es un número (el chainId), y el valor es un AddressType (una dirección).
1const contractAddrs: AddressPerBlockchainType = {2 // Holesky3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',45 // Sepolia6 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'7}La dirección del contrato en las dos redes soportadas: Holeskyopens in a new tab y Sepoliaopens in a new tab.
Nota: En realidad existe una tercera definición, para Redstone Holesky, lo explicaremos más abajo.
1type ShowObjectAttrsType = {2 name: string,3 object: any4}Este tipo se utiliza como parámetro del componente ShowObject (explicado más adelante). Incluye el nombre del objeto y su valor, que se muestran con fines de depuración.
1type ShowGreetingAttrsType = {2 greeting: string | undefined3}En cualquier momento podemos saber cuál es el saludo (greeting), porque lo leímos de la blockchain, o no saberlo (porque aún no lo hemos recibido). Por lo tanto, es útil tener un tipo que pueda ser una cadena o que no tenga valor.
Componente Greeter
1const Greeter = () => {Por fin, definimos el componente.
1 const { chain } = useNetwork()Información sobre la cadena que estamos usando, cortesía de wagmiopens in a new tab.
Como esto es un hook (use...), cada vez que esta información cambia el componente se vuelve a renderizar.
1 const greeterAddr = chain && contractAddrs[chain.id]La dirección del contrato Greeter, que varía según la cadena (y que es undefined si no hay información de la cadena o si estamos en una cadena sin ese contrato).
1 const readResults = useReadContract({2 address: greeterAddr,3 abi: greeterABI,4 functionName: "greet" , // No arguments5 watch: true6 })El hook useReadContractopens in a new tab lee información de un contrato. Puede ver exactamente qué información devuelve expandiendo readResults en la interfaz de usuario. En este caso queremos que siga monitoreando, para que se nos notifique cuando cambie el saludo.
Nota: Podríamos escuchar eventos setGreetingopens in a new tab para saber cuándo cambia el saludo y actualizar de esa manera. Sin embargo, aunque pueda ser más eficiente, no será aplicable en todos los casos. Cuando el usuario cambia a otra cadena, el saludo también cambia, pero ese cambio no viene acompañado de un evento. Podríamos tener una parte del código escuchando eventos y otra para identificar cambios en la cadena, pero eso sería más complicado que simplemente establecer el parámetro watchopens in a new tab.
1 const [ newGreeting, setNewGreeting ] = useState("")El hook useState de Reactopens in a new tab nos permite especificar una variable de estado, cuyo valor persiste entre renderizaciones del componente. El valor inicial es el parámetro, en este caso la cadena vacía.
El hook useState devuelve una lista con dos valores:
- El valor actual de la variable de estado.
- Una función para modificar la variable de estado cuando sea necesario. Como esto es un hook, cada vez que se llama el componente se vuelve a renderizar.
En este caso, utilizamos una variable de estado para el nuevo saludo que el usuario desea establecer.
1 const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>2 setNewGreeting(evt.target.value)Este es el evento manejador para cuando cambia el campo de entrada del nuevo saludo. El tipo, ChangeEventHandler<HTMLInputElement>opens in a new tab, especifica que este es un manejador para un cambio de valor de un elemento de entrada HTML. La parte <HTMLInputElement> se utiliza porque es un tipo genéricoopens in a new tab.
1 const preparedTx = usePrepareContractWrite({2 address: greeterAddr,3 abi: greeterABI,4 functionName: 'setGreeting',5 args: [ newGreeting ]6 })7 const workingTx = useContractWrite(preparedTx.config)Este es el proceso para enviar una transacción a la blockchain desde el lado del cliente:
- Enviar la transacción a un nodo de la blockchain usando
eth_estimateGasopens in a new tab. - Esperar una respuesta del nodo.
- Cuando se recibe la respuesta, pedir al usuario que firme la transacción a través de la wallet. Este paso debe suceder después de recibir la respuesta del nodo porque al usuario se le muestra el coste de gas de la transacción antes de firmarla.
- Esperar la aprobación del usuario.
- Enviar la transacción de nuevo, esta vez usando
eth_sendRawTransactionopens in a new tab.
El paso 2 probablemente tomará un tiempo perceptible, durante el cual los usuarios se preguntarán si su orden fue realmente recibida por la interfaz y por qué aún no se les pide firmar la transacción. Eso genera una mala experiencia de usuario (UX).
La solución es usar prepare hooksopens in a new tab. Cada vez que un parámetro cambia, envíe inmediatamente la solicitud eth_estimateGas al nodo. Luego, cuando el usuario finalmente desea enviar la transacción (en este caso al presionar Update greeting), el coste de gas ya es conocido y la pantalla de la wallet puede aparecer de inmediato.
1 return (Ahora por fin podemos crear el HTML real que se devolverá.
1 <>2 <h2>Greeter</h2>3 {4 !readResults.isError && !readResults.isLoading &&5 <ShowGreeting greeting={readResults.data} />6 }7 <hr />Cree un componente ShowGreeting (explicado más adelante), pero solo si el saludo fue leído correctamente de la blockchain.
1 <input type="text"2 value={newGreeting}3 onChange={greetingChange}4 />Este es el campo de texto donde el usuario puede establecer un nuevo saludo. Cada vez que el usuario presiona una tecla, se llama a greetingChange, que a su vez llama a setNewGreeting. Como setNewGreeting proviene del hook useState, provoca que el componente Greeter se renderice de nuevo. Esto significa que:
- Necesitamos especificar
valuepara mantener el valor del nuevo saludo, ya que de lo contrario volvería al valor por defecto, la cadena vacía. usePrepareContractWritese llama cada vez que cambianewGreeting, lo que significa que siempre tendrá el valor más reciente en la transacción preparada.
1 <button disabled={!workingTx.write}2 onClick={workingTx.write}3 >4 Update greeting5 </button>Si no existe workingTx.write, aún estamos esperando la información necesaria para enviar la actualización del saludo, así que el botón se desactiva. Si existe un valor para workingTx.write, esa es la función que debe llamarse para enviar la transacción.
1 <hr />2 <ShowObject name="readResults" object={readResults} />3 <ShowObject name="preparedTx" object={preparedTx} />4 <ShowObject name="workingTx" object={workingTx} />5 </>6 )7}Por último, para ayudarle a ver qué estamos haciendo, mostramos los tres objetos que usamos:
readResultspreparedTxworkingTx
Componente ShowGreeting
Este componente muestra
1const ShowGreeting = (attrs : ShowGreetingAttrsType) => {Una función de componente recibe un parámetro con todos los atributos del componente.
1 return <b>{attrs.greeting}</b>2}Componente ShowObject
Con fines informativos, usamos el componente ShowObject para mostrar los objetos importantes (readResults para leer el saludo y preparedTx y workingTx para las transacciones que creamos).
1const ShowObject = (attrs: ShowObjectAttrsType ) => {2 const keys = Object.keys(attrs.object)3 const funs = keys.filter(k => typeof attrs.object[k] == "function")4 return <>5 <details>No queremos saturar la interfaz con toda la información, así que, para poder verla o cerrarla, usamos una etiqueta detailsopens in a new tab.
1 <summary>{attrs.name}</summary>2 <pre>3 {JSON.stringify(attrs.object, null, 2)}La mayoría de los campos se muestran con JSON.stringifyopens in a new tab.
1 </pre>2 { funs.length > 0 &&3 <>4 Functions:5 <ul>La excepción son las funciones, que no forman parte del estándar JSONopens in a new tab, así que deben mostrarse por separado.
1 {funs.map((f, i) =>Dentro de JSX, el código dentro de { llaves } se interpreta como JavaScript. A continuación, el código dentro de los paréntesis () se interpreta de nuevo como JSX.
1 (<li key={i}>{f}</li>)2 )}React requiere que las etiquetas en el árbol DOMopens in a new tab tengan identificadores únicos. Esto significa que los hijos de la misma etiqueta (en este caso, la lista desordenadaopens in a new tab), necesitan diferentes atributos key.
1 </ul>2 </>3 }4 </details>5 </>6}Cierre las diversas etiquetas HTML.
El export final
1export { Greeter }El componente Greeter es el que debemos exportar para la aplicación.
src/wagmi.ts
Finalmente, varias definiciones relacionadas con WAGMI se encuentran en src/wagmi.ts. No voy a explicar todo aquí, porque la mayoría es código base (boilerplate) que probablemente no necesitará cambiar.
El código aquí no es exactamente igual al que está en githubopens in a new tab porque más adelante en el artículo agregamos otra cadena (Redstone Holeskyopens in a new tab).
1import { getDefaultWallets } from '@rainbow-me/rainbowkit'2import { configureChains, createConfig } from 'wagmi'3import { holesky, sepolia } from 'wagmi/chains'Importe las blockchains que soporta la aplicación. Puede ver la lista de cadenas soportadas en el github de viemopens in a new tab.
1import { publicProvider } from 'wagmi/providers/public'23const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'Para poder utilizar WalletConnectopens in a new tab necesita un project ID para su aplicación. Puede obtenerlo en cloud.walletconnect.comopens in a new tab.
1const { chains, publicClient, webSocketPublicClient } = configureChains(2 [ holesky, sepolia ],3 [4 publicProvider(),5 ],6)78const { connectors } = getDefaultWallets({9 appName: 'My wagmi + RainbowKit App',10 chains,11 projectId: walletConnectProjectId,12})1314export const config = createConfig({15 autoConnect: true,16 connectors,17 publicClient,18 webSocketPublicClient,19})2021export { chains }Mostrar todoAgregar otra blockchain
Hoy en día existen muchas soluciones de escalado L2, y es posible que quiera soportar algunas que viem aún no soporta. Para hacerlo, modifique src/wagmi.ts. Estas instrucciones explican cómo agregar Redstone Holeskyopens in a new tab.
-
Importe el tipo
defineChaindesde viem.1import { defineChain } from 'viem' -
Agregue la definición de la red.
1const redstoneHolesky = defineChain({2 id: 17_001,3 name: 'Redstone Holesky',4 network: 'redstone-holesky',5 nativeCurrency: {6 decimals: 18,7 name: 'Ether',8 symbol: 'ETH',9 },10 rpcUrls: {11 default: {12 http: ['https://rpc.holesky.redstone.xyz'],13 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],14 },15 public: {16 http: ['https://rpc.holesky.redstone.xyz'],17 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],18 },19 },20 blockExplorers: {21 default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' },22 },23})Mostrar todo -
Agregue la nueva cadena a la llamada de
configureChains.1 const { chains, publicClient, webSocketPublicClient } = configureChains(2 [ holesky, sepolia, redstoneHolesky ],3 [ publicProvider(), ],4 ) -
Asegúrese de que la aplicación conozca la dirección de sus contratos en la nueva red. En este caso, se modifica
src/components/Greeter.tsx:1const contractAddrs : AddressPerBlockchainType = {2 // Holesky3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',45 // Redstone Holesky6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',78 // Sepolia9 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'10}Mostrar todo
Conclusión
Por supuesto, realmente no le interesa crear una interfaz de usuario para Greeter. Usted quiere crear una interfaz de usuario para sus propios contratos. Para crear su propia aplicación, siga estos pasos:
-
Indique que quiere crear una aplicación wagmi.
1pnpm create wagmi -
Nombre la aplicación.
-
Seleccione el framework React.
-
Seleccione la variante Vite.
Ahora haga que sus contratos sean utilizables para el mundo entero.
Vea aquí más de mi trabajoopens in a new tab.
Última actualización de la página: 22 de septiembre de 2025