En este tutorial, aprenderemos cómo construir una Dapp (Aplicación Descentralizada) Web3 que se conecta a tu billetera de criptomonedas, permitiéndote transferir fondos y acuñar NFTs. Usaremos Next.js, un popular framework de React, y Wagmi.sh, una colección de React Hooks que integra fácilmente tu billetera en tu sitio web.
Aquí tienes un resumen del tutorial:
- Empezando
- Parte 1: Transferir cripto con React + Wagmi
- Parte 2: Acuñar un NFT con React + Wagmi
Autoría
Este artículo fue coescrito por Aymeric Taylor (LinkedIn, Twitter), pasante de investigación en RareSkills.
Empezando
Por razones de simplicidad, usaremos “Polygon Mumbai” como nuestra red de prueba para este tutorial, también es compatible con OpenSea.
Conecta tu billetera a Polygon Mumbai - MetaMask
Navega a través de MetaMask → Settings → Advanced y asegúrate de habilitar Show test networks.

A continuación, haz clic en la Selección de red en la sección superior derecha y selecciona Add network.

Si Polygon Mumbai aún no está disponible para seleccionar, puedes seguir los pasos a continuación.
Selecciona Add a network manually e ingresa lo siguiente:
Nombre de la red (Network Name): Matic Mumbai
Nueva URL de RPC (New RPC URL): https://rpc-mumbai.maticvigil.com/
Identificador de cadena (Chain ID): 80001
Símbolo de moneda (Currency Symbol): MATIC
URL del explorador de bloques (opcional) : https://mumbai.polygonscan.com/
Ahora simplemente cambia a la red Polygon.
Ejemplos de Wagmi Parte 1: Transferir Ether con React + Wagmi
Paso 1: Configurar el sitio web con node js
Necesitarás tener nodejs instalado para esto.
Primero crea tu proyecto de Next.js con
npx create-next-app@latest myapp
Marca la opción de Typescript y ESLint usando las flechas y la tecla Enter. Debería verse algo así:

Abre tu proyecto en vscode e instala wagmi.sh y el paquete use-debounce (hablaremos de eso más adelante).
npm i wagmi ethers@^5
npm i use-debounce --save
Wagmi es básicamente un conjunto de React Hooks que simplifica el desarrollo en Ethereum proporcionando características útiles como la conexión de billeteras y la interacción con contratos, las cuales aprenderemos en este tutorial. Más información en wagmi.sh.
Paso 2: Usar configureChains para elegir la red a la que conectarse
Dirígete a pages/_app.tsx y agrega el siguiente código. Cada una de las funcionalidades se explica en las secciones de comentarios.
import "@/styles/globals.css"; // CSS doesnt really matter now
import type { AppProps } from "next/app";
import { WagmiConfig, configureChains, createClient, mainnet } from "wagmi";
import { publicProvider } from "wagmi/providers/public";
import { polygonMumbai } from "wagmi/chains";
import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet'
import { InjectedConnector } from 'wagmi/connectors/injected'
import { MetaMaskConnector } from 'wagmi/connectors/metaMask'
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect'
// configure the chains and provider that you want to use for your app,
// keep in mind that you're allowed to pass any EVM-compatible chain.
// It is also encouraged that you pass both alchemyProvider and infuraProvider.
const { chains, provider, webSocketProvider } = configureChains(
[mainnet, polygonMumbai],
[publicProvider()]
);
// This creates a wagmi client instance of createClient
// and passes in the provider and webSocketProvider.
const client = createClient({
autoConnect: false,
provider,
webSocketProvider,
connectors: [ // connectors is to connect your wallet, defaults to InjectedConnector();
new MetaMaskConnector({ chains }),
new CoinbaseWalletConnector({
chains,
options: {
appName: 'wagmi',
},
}),
new WalletConnectConnector({
chains,
options: {
projectId: '...',
},
}),
new InjectedConnector({
chains,
options: {
name: 'Injected',
shimDisconnect: true,
},
}),
],
});
export default function App({ Component, pageProps }: AppProps) {
return (
// Wrap your application with the WagmiConfig component
// and pass the client instance as a prop to it.
<WagmiConfig client={client}>
<Component {...pageProps} />
</WagmiConfig>
);
}
Ahora tenemos nuestras redes configuradas, nuestro siguiente paso es permitir a los usuarios elegir a qué billetera conectarse. Como puedes ver arriba, tenemos 4 conectores de billetera configurados. MetaMask, WalletConnect, Coinbase e Injected (que, de nuevo, es básicamente tu billetera predeterminada).
Paso 3: Usar useConnect para habilitar la selección de la billetera del navegador
En pages/index.tsx, copia y pega el siguiente código. ¡Asegúrate de agregar el css para que se vea bien!
import { useAccount, useConnect } from "wagmi";
import { useEffect } from "react";
import styles from "@/styles/Home.module.css";
export default function Home() {
const { connect, connectors } = useConnect();
const { isConnected } = useAccount();
useEffect(() => {
console.log(
`Current connection status: ${isConnected ? "connected" : "disconnected"}`
);
}, [isConnected]);
return (
<>
<p
className={styles.status}
style={{
color: isConnected ? "green" : "red",
}}
>
{" "}
{isConnected !== undefined
? isConnected
? "Connected"
: "Disconnected"
: "loading..."}
</p>
<div className={styles.maincontainer}>
<div className={styles.container}>
<div className={styles.buttonswrapper}>
<div className={styles.buttonswrapperGrid}>
{connectors.map((connector) => (
<button
suppressHydrationWarning
key={connector.id}
onClick={() => connect({ connector })}
className={styles.button28}
>
{connector.name}
</button>
))}
</div>
</div>
</div>
{/* send funds */}
{/* mint nft */}
</div>
</>
);
}
Paso 4: Agregar css para que se vea bien
Elimina todo en styles/globals.css y styles/Home.module.css.
Copia y pega el código css a continuación en styles/globals.css.
body {
height: 100vh;
background: rgb(11,3,48); /* For browsers that do not support gradients */
background: linear-gradient(to bottom right,#0b0330, #5904a4);
font-family: 'Inter Medium', sans-serif;
}
Copia y pega el código css a continuación en styles/Home.module.css.
.status {
text-align: left;
margin: 0px;
font-family: "Inter Medium", sans-serif;
}
.maincontainer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 60%;
width: 60%;
}
.buttonswrapperGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
grid-gap: 1.5rem;
justify-items: center;
align-items: center;
justify-content: center;
margin-bottom: 40px;
padding: 20px;
}
/* CSS */
.button28 {
align-items: center;
background-color: #da5fff;
border: 2px solid #1d0321;
border-radius: 8px;
box-sizing: border-box;
color: #111;
cursor: pointer;
display: flex;
font-family: Inter, sans-serif;
font-size: 16px;
height: 55px;
justify-content: center;
line-height: 24px;
max-width: 75%;
padding: 0 25px;
position: relative;
text-align: center;
text-decoration: none;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
width: 100%;
}
.button28:after {
background-color: #210c20;
border-radius: 8px;
content: "";
display: block;
height: 48px;
left: 0;
width: 100%;
position: absolute;
top: -2px;
transform: translate(8px, 14px);
transition: transform 0.2s ease-out;
z-index: -1;
}
.button28:hover:after {
transform: translate(0, 0);
}
.button28:active {
background-color: #ffdeda;
outline: 0;
}
.button28:hover {
outline: 0;
}
@media (min-width: 768px) {
.button28 {
padding: 0 40px;
}
}
.myinput {
background-color: rgb(253, 232, 255);
border-radius: 15px;
border: 2px solid #1d0321;
padding: 20px;
width: 44%;
height: 15px;
}
.inputcontainer {
display: flex;
justify-content: space-between; /* Space the input fields evenly */
align-items: center; /* Align the input fields vertically in the container */
}
.buttoncontainer {
display: flex;
justify-content: center; /* Center the button horizontally */
align-items: center; /* Center the button vertically */
margin-top: 16px;
margin-bottom: 16px;
}
.mintcontainer {
display: flex;
justify-content: center; /* Center the button horizontally */
align-items: center; /* Center the button vertically */
margin-top: 50px;
margin-bottom: 16px;
}
/* CSS */
.button64 {
align-items: center;
background-image: linear-gradient(144deg, #af40ff, #280b36 50%, #e30eff);
border: 0;
border-radius: 8px;
box-shadow: rgba(151, 65, 252, 0.2) 0 15px 30px -5px;
box-sizing: border-box;
color: #ffffff;
display: flex;
font-family: Phantomsans, sans-serif;
font-size: 20px;
justify-content: center;
line-height: 2em;
max-width: 100%;
min-width: 140px;
padding: 3px;
text-decoration: none;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
white-space: nowrap;
cursor: pointer;
}
.button64:active,
.button64:hover {
outline: 0;
}
.button64 span {
background-color: rgb(5, 6, 45);
padding: 16px 24px;
border-radius: 6px;
width: 100%;
height: 100%;
transition: 300ms;
}
.button64:hover span {
background: none;
}
@media (min-width: 768px) {
.button64 {
font-size: 24px;
min-width: 196px;
}
}
/* CSS */
.button49,
.button49:after {
width: 150px;
height: 76px;
line-height: 78px;
font-size: 20px;
font-family: 'Bebas Neue', sans-serif;
background: linear-gradient(45deg, transparent 5%, #ff01ee 5%);
border: 0;
color: #fff;
letter-spacing: 3px;
box-shadow: 6px 0px 0px #00E6F6;
outline: transparent;
position: relative;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.button49:after {
--slice-0: inset(50% 50% 50% 50%);
--slice-1: inset(80% -6px 0 0);
--slice-2: inset(50% -6px 30% 0);
--slice-3: inset(10% -6px 85% 0);
--slice-4: inset(40% -6px 43% 0);
--slice-5: inset(80% -6px 5% 0);
content: 'GET YOUR NFT';
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 3%, #00E6F6 3%, #00E6F6 5%, #c401ff 5%);
text-shadow: -3px -3px 0px #F8F005, 3px 3px 0px #00E6F6;
clip-path: var(--slice-0);
}
.button49:hover:after {
animation: 1s glitch;
animation-timing-function: steps(2, end);
}
@keyframes glitch {
0% {
clip-path: var(--slice-1);
transform: translate(-20px, -10px);
}
10% {
clip-path: var(--slice-3);
transform: translate(10px, 10px);
}
20% {
clip-path: var(--slice-1);
transform: translate(-10px, 10px);
}
30% {
clip-path: var(--slice-3);
transform: translate(0px, 5px);
}
40% {
clip-path: var(--slice-2);
transform: translate(-5px, 0px);
}
50% {
clip-path: var(--slice-3);
transform: translate(5px, 0px);
}
60% {
clip-path: var(--slice-4);
transform: translate(5px, 10px);
}
70% {
clip-path: var(--slice-2);
transform: translate(-10px, 10px);
}
80% {
clip-path: var(--slice-5);
transform: translate(20px, -10px);
}
90% {
clip-path: var(--slice-1);
transform: translate(-10px, 0px);
}
100% {
clip-path: var(--slice-1);
transform: translate(0);
}
}
@media (min-width: 768px) {
.button49,
.button49:after {
width: 200px;
height: 86px;
line-height: 88px;
}
}
Paso 5: Ejecutar el sitio web y probarlo
Al ejecutar esto, debería iniciarse una conexión a tu billetera. Una vez que hayas aprobado la conexión, debería verse algo así:
npm run dev

Paso 6: Agregar la capacidad de transferir criptomonedas
Ahora que hemos conectado nuestra billetera, podemos transferir fondos de nuestra billetera a otras. Asegúrate de tener algo de MATIC de Mumbai en tu billetera. Puedes obtener un poco aquí https://faucet.polygon.technology
Ahora crearemos los campos de entrada y el botón de envío para la transacción. Crea un nuevo archivo en el directorio pages/ y nómbralo RareSend.tsx. Copia y pega el código a continuación. Las explicaciones están en los comentarios del código.
import { parseEther } from "ethers/lib/utils.js";
import React, { useState } from "react";
import { useDebounce } from "use-debounce";
import { usePrepareSendTransaction, useSendTransaction } from "wagmi";
import styles from "@/styles/Home.module.css";
// what this does is simply disable the SendFunds function if the value passed is false
interface SendFundsProps {
disabled?: boolean;
}
export default function SendFunds(props: SendFundsProps) {
// declare two state variables for the recipient and the amount
const [to, setTo] = useState("");
const [debouncedTo] = useDebounce(to, 500); // useDebounce() hook to debounce the recipient input
const [amount, setAmount] = useState("");
const [debouncedAmount] = useDebounce(amount, 500); // useDebounce() hook to debounce the amount input
// use the usePrepareSendTransaction() hook to prepare a transaction request
const { config } = usePrepareSendTransaction({
request: {
to: debouncedTo, // recipient from debounced input
value: debouncedAmount ? parseEther(debouncedAmount) : undefined, // amount from debounced input
},
});
// use the useSendTransaction() hook to create a transaction and send it
const { sendTransaction } = useSendTransaction(config);
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
sendTransaction?.(); // if sendTransaction is defined, execute it
}}
>
<div className={styles.inputcontainer}>
<input
aria-label="Recipient"
onChange={(e) => setTo(e.target.value)} // update the recipient state on input change
placeholder="Address destination"
value={to}
className={styles.myinput}
/>
<input
aria-label="Amount (ether)"
onChange={(e) => setAmount(e.target.value)} // update the amount state on input change
placeholder="enter amount"
value={amount}
className={styles.myinput}
/>
</div>
<div className={styles.buttoncontainer}>
<button
disabled={!sendTransaction || !to || !amount}
className={styles.button64}
>
Send
</button>{" "}
{/* disable the button if required fields are empty */}
</div>
</form>
</>
);
}
Explicación de useDebounce()
Para evitar sobrecargar el RPC y ser limitados por tasa (rate-limited), limitaremos el uso del hook usePrepareContractWrite, el cual solicita estimaciones de gas al montar el componente y al modificar argumentos. Usamos el hook useDebounce en el componente para retrasar la actualización del ID del token por 500 ms si no se han realizado cambios.
Paso 7: Insertar el componente SendFunds
A continuación, simplemente agrega el <SendFunds disabled={!isConnected}/> de esta manera en pages/index.tsx.
import { useAccount, useConnect } from "wagmi";
import SendFunds from "./RareSend";
import { useEffect } from "react";
import styles from "@/styles/Home.module.css";
export default function Home() {
{
/* some code... */
}
return (
<>
<div>
{/* some code... */}
{/* send funds */}
<SendFunds disabled={!isConnected} />
{/* mint nft */}
</div>
</>
);
}
¡Mientras tengas Matic en tu dirección, podrás enviarlo a otra cuenta!
Así debería verse tu sitio web:

¡Felicitaciones por llegar a este punto! Has creado con éxito tu propio sitio web que puede enviar fondos desde tu billetera a otra cuenta. Ahora puedes transferir criptomonedas fácilmente entre cuentas, sin tener que depender de un servicio de terceros ni ingresar manualmente los detalles de la transacción. ¡Bien hecho! ¡Es así de fácil!
Ejemplos de Wagmi Parte 2: Acuñar un NFT con React + Wagmi
Asumimos que ya has desplegado un contrato inteligente de NFT en la blockchain. Puedes seguir este video tutorial para hacerlo: https://youtu.be/LIoFbudNVZs.

NFTs de gatos generados por IA
La función mint se puede crear de la siguiente manera. Crea un archivo mint.tsx y agrega el código a continuación.
// Initialize ethers.js and wagmi dependencies
import { ethers } from "ethers";
import * as React from "react";
import {
usePrepareContractWrite,
useContractWrite,
useWaitForTransaction,
} from "wagmi";
import styles from "@/styles/Home.module.css";
// Define a React function component to mint an NFT
export function MintNFT() {
// Prepare the contract write configuration by providing the contract's address, ABI, function name, and overrides
const { config } = usePrepareContractWrite({
address: "Your Contract Address",
abi: [
{
name: "mint",
type: "function",
stateMutability: "payable",
inputs: [],
outputs: [],
},
],
functionName: "mint",
overrides: {
from: "Your Walllet Address",
value: ethers.utils.parseEther("0.000000001"), //the integer value should match your nft minting requirements
},
});
// Use the useContractWrite hook to write to the contract's mint function and obtain the transaction data and write function
const { data, write } = useContractWrite(config);
// Use the useWaitForTransaction hook to wait for the transaction to be mined and return loading and success states
const { isLoading, isSuccess } = useWaitForTransaction({
hash: data?.hash,
});
// Render the component with a button that triggers the mint transaction when clicked, a loading message while the transaction is in progress, and a success message when the transaction is successful
return (
<div className={styles.mintcontainer}>
<button
disabled={!write || isLoading}
onClick={() => write?.()}
className={styles.button49}
>
{isLoading ? "Minting..." : "Mint"}
</button>
{isSuccess && (
<div>
Successfully minted your NFT!
<div>
<a href={`https://etherscan.io/tx/${data?.hash}`}>Etherscan</a>
</div>
</div>
)}
</div>
);
}
Ahora simplemente puedes insertarlo en el archivo index.tsx de esta manera:
import { useAccount, useConnect } from "wagmi";
import SendFunds from "./RareSend";
import { useEffect } from "react";
import styles from "@/styles/Home.module.css";
import { MintNFT } from "./mint";
export default function Home() {
const { connect, connectors } = useConnect();
const { isConnected } = useAccount();
useEffect(() => {
console.log(
`Current connection status: ${isConnected ? "connected" : "disconnected"}`
);
}, [isConnected]);
return (
<>
<p
className={styles.status}
style={{
color: isConnected ? "green" : "red",
}}
>
{" "}
{isConnected !== undefined
? isConnected
? "Connected"
: "Disconnected"
: "loading..."}
</p>
<div className={styles.maincontainer}>
<div className={styles.container}>
<div className={styles.buttonswrapper}>
<div className={styles.buttonswrapperGrid}>
{connectors.map((connector) => (
<button
suppressHydrationWarning
key={connector.id}
onClick={() => connect({ connector })}
className={styles.button28}
>
{connector.name}
</button>
))}
</div>
</div>
</div>
{/* send funds */}
<SendFunds disabled={!isConnected} />
{/* mint nft */}
<MintNFT />
</div>
</>
);
}
Tu sitio web final debería verse algo así:

¡Felicidades! Acabas de crear tu propia Aplicación Descentralizada que es capaz de Enviar Transacciones y Acuñar un NFT.
Publicado originalmente el 24 de abril de 2023