本教程详细介绍了如何使用Next.js和Wagmi.sh构建一个Web3 Dapp,允许用户通过连接到加密货币钱包来转移资金和铸造NFT。教程包括从设置开发环境到实现转账和NFT铸造的完整步骤。
在本教程中,我们将学习如何构建一个连接到你的加密钱包的 Web3 Dapp(去中心化应用程序),使你能够转账和铸造NFT。我们将使用 Next.js,这是一个流行的 React 框架,以及 Wagmi.sh,这是一组 React Hooks,可以轻松将你的钱包集成到你的网站中。
以下是教程的大纲:
本文由 RareSkills 的研究实习生 Aymeric Taylor 共同撰写 (LinkedIn, Twitter)。
为了简化起见,我们将使用“Polygon Mumbai”作为本教程的测试网络,它在 OpenSea 上也受支持。
通过 MetaMask → 设置 → 高级,确保你允许 显示测试网络。
接下来,点击右上角的网络选择,选择 添加网络。
如果 Polygon Mumbai 尚不可选,则可以按照以下步骤操作。
选择 手动添加网络 并输入以下内容:
网络名称: Matic Mumbai
新的 RPC URL: https://rpc-mumbai.maticvigil.com/
链 ID: 80001
货币符号: MATIC
区块探测器 URL(可选): https://mumbai.polygonscan.com/
现在只需切换到 Polygon 网络即可。
你需要安装 nodejs。
首先使用以下命令创建你的 Next.js 项目:
npx create-next-app@latest myapp
使用箭头和 Enter 键检查 Typescript 和 ESLint 选项。它应该类似于:
在 vscode 中打开你的项目并安装 wagmi.sh 和 use-debounce package(稍后会涉及到)。
npm i wagmi ethers@^5
npm i use-debounce --save
Wagmi 基本上是一组 React Hooks,通过提供连接钱包和与合约交互等有用功能,简化了 以太坊开发,这些我们将在本教程中学习。有关更多内容,见 wagmi.sh。
前往 pages/_app.tsx 并添加以下代码。每个功能在注释部分都有说明。
import "@/styles/globals.css"; // CSS 现在并不重要
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'
// 配置你想要使用的链和提供程序,
// 请记住,你可以传递任何 EVM 兼容链。
// 也建议你同时传递 alchemyProvider 和 infuraProvider。
const { chains, provider, webSocketProvider } = configureChains(
[mainnet, polygonMumbai],
[publicProvider()]
);
// 这会创建一个 wagmi 客户端实例并传入提供者和 webSocketProvider。
const client = createClient({
autoConnect: false,
provider,
webSocketProvider,
connectors: [ // connectors 用于连接你的钱包,默认为 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 (
// 用 WagmiConfig 组件包裹你的应用程序
// 并将 client 实例作为 prop 传递给它。
<WagmiConfig client={client}>
<Component {...pageProps} />
</WagmiConfig>
);
}
现在我们已经配置了网络,下一步是让用户选择要连接的钱包。如上所示,我们设置了 4 个钱包连接器。MetaMask、WalletConnect、Coinbase 和 Injected(这基本上是你的默认钱包)。
在 pages/index.tsx 中复制并粘贴以下代码。确保你添加 CSS,以使其看起来不错!
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(
`当前连接状态: ${isConnected ? "已连接" : "未连接"}`
);
}, [isConnected]);
return (
<>
<p
className={styles.status}
style={{
color: isConnected ? "green" : "red",
}}
>
{" "}
{isConnected !== undefined
? isConnected
? "已连接"
: "未连接"
: "加载中..."}
</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>
{/* 发送资金 */}
{/* 铸造 nft */}
</div>
</>
);
}
删除 styles/globals.css 和 styles/Home.module.css 中的所有内容。
将以下 CSS 代码复制粘贴到 styles/globals.css 中。
body {
height: 100vh;
background: rgb(11,3,48); /* 未支持渐变的浏览器 */
background: linear-gradient(to bottom right,#0b0330, #5904a4);
font-family: 'Inter Medium', sans-serif;
}
将以下 CSS 代码复制到 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; /* 均匀间隔输入框 */
align-items: center; /* 垂直对齐输入框 */
}
.buttoncontainer {
display: flex;
justify-content: center; /* 水平居中按钮 */
align-items: center; /* 垂直居中按钮 */
margin-top: 16px;
margin-bottom: 16px;
}
.mintcontainer {
display: flex;
justify-content: center; /* 水平居中按钮 */
align-items: center; /* 垂直居中按钮 */
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: '获取你的 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;
}
}
当你运行此命令时,这应该会启动与你的钱包的连接。一旦你批准连接,它应该看起来类似于:
npm run dev
现在我们已经连接了钱包,我们可以将资金从我们的钱包转移到其他钱包。确保你在钱包中有一些 Mumbai MATIC。你可以在这里获取:https://faucet.polygon.technology
现在我们将创建转账的输入字段和发送按钮。在 pages/ 目录中创建一个新文件并命名为 RareSend.tsx。复制并粘贴以下代码。解释在代码注释中。
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";
// 如果传递的值为 false,这个函数会简单地禁用 SendFunds 函数
interface SendFundsProps {
disabled?: boolean;
}
export default function SendFunds(props: SendFundsProps) {
// 声明两个状态变量用于收款人和金额
const [to, setTo] = useState("");
const [debouncedTo] = useDebounce(to, 500); // useDebounce() hook 延迟收款人输入500ms
const [amount, setAmount] = useState("");
const [debouncedAmount] = useDebounce(amount, 500); // useDebounce() hook 延迟金额输入500ms
// 使用 usePrepareSendTransaction() hook 准备事务请求
const { config } = usePrepareSendTransaction({
request: {
to: debouncedTo, // 从延迟输入获取收款人
value: debouncedAmount ? parseEther(debouncedAmount) : undefined, // 从延迟输入获取金额
},
});
// 使用 useSendTransaction() hook 创建事务并发送
const { sendTransaction } = useSendTransaction(config);
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
sendTransaction?.(); // 如果 sendTransaction 被定义,则执行
}}
>
<div className={styles.inputcontainer}>
<input
aria-label="Recipient"
onChange={(e) => setTo(e.target.value)} // 输入更改时更新收款人状态
placeholder="地址目标"
value={to}
className={styles.myinput}
/>
<input
aria-label="Amount (ether)"
onChange={(e) => setAmount(e.target.value)} // 输入更改时更新金额状态
placeholder="输入金额"
value={amount}
className={styles.myinput}
/>
</div>
<div className={styles.buttoncontainer}>
<button
disabled={!sendTransaction || !to || !amount}
className={styles.button64}
>
发送
</button>{" "}
{/* 必填字段为空时禁用按钮 */}
</div>
</form>
</>
);
}
useDebounce() 解释
为了避免过载 RPC 并且被限速,我们将限制使用 usePrepareContractWrite hook,该 hook 会在组件挂载和参数修改时请求 gas 估算。我们在组件中使用 useDebounce hook 来延迟更新 token ID, 如果没有关 键已更改,延迟为500ms。
接下来,简单地将其添加到 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() {
{
/* 一些代码... */
}
return (
<>
<div>
{/* 一些代码... */}
{/* 发送资金 */}
<SendFunds disabled={!isConnected} />
{/* 铸造 nft */}
</div>
</>
);
}
只要你的地址中有 Matic,你就可以将其发送到另一个账户!
这就是你的网站应该看起来的样子:
恭喜你来到这个步骤!你已经成功创建了自己的网站,可以将资金从你的钱包发送到另一个账户。你现在能够轻松地在账户之间转移加密货币,而无需依赖于第三方服务或手动输入交易详细信息。干得好!就这么简单!
我们假设你已经将 NFT 智能合约 部署到区块链上。你可以遵循此视频教程来完成此操作:<https://youtu.be/LIoFbudNVZs>。
AI 生成的猫 NFT
铸造函数可以如下创建。创建一个 mint.tsx 文件,并添加以下代码。
// 初始化 ethers.js 和 wagmi 依赖
import { ethers } from "ethers";
import * as React from "react";
import {
usePrepareContractWrite,
useContractWrite,
useWaitForTransaction,
} from "wagmi";
import styles from "@/styles/Home.module.css";
// 定义铸造 NFT 的 React 函数组件
export function MintNFT() {
// 准备合约写入配置,提供合约地址、ABI、函数名称和重载
const { config } = usePrepareContractWrite({
address: "你的合约地址",
abi: [
{
name: "mint",
type: "function",
stateMutability: "payable",
inputs: [],
outputs: [],
},
],
functionName: "mint",
overrides: {
from: "你的钱包地址",
value: ethers.utils.parseEther("0.000000001"), // 整数值应符合 NFT 铸造要求
},
});
// 使用 useContractWrite hook 来写入合约的 mint 函数并获得交易数据和写入函数
const { data, write } = useContractWrite(config);
// 使用 useWaitForTransaction hook 等待交易被挖掘并返回加载和成功状态
const { isLoading, isSuccess } = useWaitForTransaction({
hash: data?.hash,
});
// 渲染组件,带有一个按钮,当点击时触发铸造交易,交易进行时显示加载信息,并在交易成功时显示成功信息
return (
<div className={styles.mintcontainer}>
<button
disabled={!write || isLoading}
onClick={() => write?.()}
className={styles.button49}
>
{isLoading ? "铸造中..." : "铸造"}
</button>
{isSuccess && (
<div>
成功铸造你的 NFT!
<div>
<a href={`https://etherscan.io/tx/${data?.hash}`}>Etherscan</a>
</div>
</div>
)}
</div>
);
}
现在你可以简单地将其插入到 index.tsx 文件中,如下所示:
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(
`当前连接状态: ${isConnected ? "已连接" : "未连接"}`
);
}, [isConnected]);
return (
<>
<p
className={styles.status}
style={{
color: isConnected ? "green" : "red",
}}
>
{" "}
{isConnected !== undefined
? isConnected
? "已连接"
: "未连接"
: "加载中..."}
</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>
{/* 发送资金 */}
<SendFunds disabled={!isConnected} />
{/* 铸造 nft */}
<MintNFT />
</div>
</>
);
}
你的最终网站应看起来类似于:
恭喜你!你刚刚创建了自己的去中心化应用程序,具备发送交易和铸造 NFT 的能力。
最初发布于 2023 年 4 月 24 日
- 原文链接: rareskills.io/post/wagmi...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!