如今,Web3 应用早已不只是“连接个钱包”那么简单。一个真正可用的 DApp,至少需要做到三件事:前端流畅地连接钱包、与智能合约安全交互、后端可靠地验证用户身份。本文将以 React + wagmi + viem 为前端核心,Node.js 为后端,带你一步步搭建一个包含完整钱包集成流程的全栈 DApp。
读完这篇文章,你将能够:
- 在 React 应用中集成多钱包支持
- 读取链上数据并发送交易
- 实现“连接钱包即登录”
- 在后端验证钱包签名以确保用户身份
全文代码均可直接运行,你也可以在这个基础上自由扩展。
1. 环境准备
开始之前,请确保本机已安装:
- Node.js ≥ 18
- pnpm(或 npm / yarn)
- MetaMask 或其他钱包浏览器插件
我们将在同一个仓库中维护前端和后端代码,最终目录结构如下:
web3-fullstack-demo/
├── frontend/ # React + Vite
└── backend/ # Express
2. 前端:项目初始化 & 钱包连接
2.1 创建项目
使用 Vite 快速搭建 React + TypeScript 工程:
pnpm create vite frontend --template react-ts
cd frontend
pnpm install
2.2 安装 Web3 依赖
我们使用 wagmi 管理钱包连接,viem 负责底层 RPC 调用与合约交互:
pnpm add wagmi viem @tanstack/react-query
wagmi v2 基于 vanilla 设计,与 viem 深度集成,是目前 React 生态中最主流的钱包连接方案。
2.3 配置 wagmi 与链
创建 frontend/src/config/web3.ts:
import { http, createConfig } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'
import { injected, metaMask, walletConnect } from 'wagmi/connectors'
export const config = createConfig({
chains: [mainnet, sepolia],
connectors: [
injected(), // 浏览器注入钱包(如 MetaMask)
metaMask(), // 显式指定 MetaMask
walletConnect({ // WalletConnect 扫码连接
projectId: '你的_WalletConnect_Project_ID', // 从 cloud.walletconnect.com 获取
}),
],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
},
})
2.4 提供全局 Context
修改 frontend/src/main.tsx,包裹 WagmiProvider:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { WagmiProvider } from 'wagmi'
import { config } from './config/web3'
import App from './App'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</WagmiProvider>
</React.StrictMode>
)
2.5 实现“连接钱包”按钮
在 App.tsx 中,我们用 wagmi 的 Hook 快速实现连接与账户信息展示:
import { useAccount, useConnect, useDisconnect, useBalance } from 'wagmi'
function App() {
const { address, isConnected } = useAccount()
const { connect, connectors, error, status } = useConnect()
const { disconnect } = useDisconnect()
const { data: balance } = useBalance({ address })
return (
<div style={{ padding: 32 }}>
<h1> Web3 钱包集成 Demo</h1>
{isConnected ? (
<div>
<p> 已连接:{address}</p>
<p> 余额:{balance?.formatted} {balance?.symbol}</p>
<button onClick={() => disconnect()}>断开连接</button>
</div>
) : (
<div>
<p>请先连接钱包</p>
{connectors.map((connector) => (
<button
key={connector.id}
onClick={() => connect({ connector })}
disabled={status === 'pending'}
>
{connector.name}
{!connector.ready && ' (未安装)'}
</button>
))}
{error && <p style={{ color: 'red' }}>{error.message}</p>}
</div>
)}
</div>
)
}
export default App
运行 pnpm dev,你应该能看到连接按钮,并能成功连接 MetaMask 或 WalletConnect。
到此,钱包连接的基础设施就搭建完成了。
3. 前端:与合约交互
3.1 准备合约 ABI 与地址
假设我们已部署一个简单的“存取值”合约在 Sepolia 测试网:
// ValueStore.sol
contract ValueStore {
uint256 public value;
event ValueChanged(uint256 newValue);
function setValue(uint256 _value) external {
value = _value;
emit ValueChanged(_value);
}
}
合约地址:0xYourContractAddress
ABI 只需保留会用到的片段(也可以直接导入完整 JSON)。
创建 frontend/src/contracts/valueStore.ts:
export const VALUE_STORE_ADDRESS = '0xYourContractAddress'
export const VALUE_STORE_ABI = [
{
inputs: [],
name: 'value',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: '_value', type: 'uint256' }],
name: 'setValue',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
anonymous: false,
inputs: [{ indexed: false, name: 'newValue', type: 'uint256' }],
name: 'ValueChanged',
type: 'event',
},
] as const
3.2 读取合约数据
使用 useReadContract 读取链上状态:
import { useReadContract } from 'wagmi'
import { VALUE_STORE_ADDRESS, VALUE_STORE_ABI } from './contracts/valueStore'
function ContractData() {
const { data: value, isError, isLoading } = useReadContract({
address: VALUE_STORE_ADDRESS,
abi: VALUE_STORE_ABI,
functionName: 'value',
})
if (isLoading) return <p>加载中…</p>
if (isError) return <p>读取失败</p>
return <p> 合约当前值:{value?.toString()}</p>
}
3.3 写入合约(发送交易)
用 useWriteContract + useWaitForTransactionReceipt 实现写操作并等待上链:
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { VALUE_STORE_ADDRESS, VALUE_STORE_ABI } from './contracts/valueStore'
import { parseEther } from 'viem'
import { useState } from 'react'
function SetValue() {
const [inputValue, setInputValue] = useState('')
const { data: hash, writeContract } = useWriteContract()
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({ hash })
const handleSet = () => {
if (!inputValue) return
writeContract({
address: VALUE_STORE_ADDRESS,
abi: VALUE_STORE_ABI,
functionName: 'setValue',
args: [BigInt(inputValue)],
})
}
return (
<div>
<input
type="number"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="新值"
/>
<button onClick={handleSet} disabled={isConfirming}>
{isConfirming ? '确认中…' : '设置'}
</button>
{hash && <p>交易哈希:{hash}</p>}
{isConfirmed && <p style={{ color: 'green' }}> 上链成功!</p>}
</div>
)
}
这样我们就完成了钱包连接、余额查询、合约读写等前端核心功能。
4. 连接钱包即登录:签名验证流程
很多 DApp 需要后端确认“某个操作确实来自该钱包地址”。标准做法是:
- 后端生成一条随机消息(nonce)
- 前端用钱包对该消息签名
- 后端验证签名,返回 JWT 或 session
这比传统的密码登录更安全、更去中心化。
4.1 后端:生成 Nonce 接口
创建 backend/ 目录并初始化:
mkdir backend && cd backend
pnpm init
pnpm add express cors viem
pnpm add -D typescript @types/express @types/cors tsx
配置 tsconfig.json,然后编写 backend/src/index.ts:
import express from 'express'
import cors from 'cors'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const app = express()
app.use(cors())
app.use(express.json())
// 用于签名验证的客户端(也可以不连链,直接用 viem 的 verifyMessage)
// 这里使用 viem 原生方法,不需要 provider
// 存储 nonce(实际项目应存数据库,并绑定 session)
const nonces = new Map<string, string>()
// 1. 请求 nonce
app.post('/auth/nonce', (req, res) => {
const { address } = req.body
if (!address) return res.status(400).json({ error: '缺少地址' })
const nonce = `登录验证:${Math.floor(Math.random() * 1e9)}`
nonces.set(address, nonce)
// 设置过期时间(示例 5 分钟)
setTimeout(() => nonces.delete(address), 5 * 60 * 1000)
res.json({ nonce })
})
// 2. 验证签名
app.post('/auth/verify', (req, res) => {
const { address, signature } = req.body
const nonce = nonces.get(address)
if (!nonce) return res.status(400).json({ error: 'nonce 无效或已过期' })
try {
// viem 的 verifyMessage 可以直接验证以太坊个人签名
const isValid = verifyMessage({
address: address as `0x${string}`,
message: nonce,
signature: signature as `0x${string}`,
})
if (isValid) {
nonces.delete(address)
// 这里可以签发 JWT,示例中直接返回成功
res.json({ success: true, token: 'your-jwt-token' })
} else {
res.status(401).json({ error: '签名验证失败' })
}
} catch (e) {
res.status(401).json({ error: '签名验证异常' })
}
})
app.listen(3001, () => {
console.log('后端运行在 http://localhost:3001')
})
注意:上面的
verifyMessage需要从viem导入,实际代码顶部加上:
import { verifyMessage } from 'viem'
4.2 前端:发起登录流程
在前端封装登录逻辑,点击“使用钱包登录”时调用:
import { useSignMessage } from 'wagmi'
function LoginWithWallet() {
const { address } = useAccount()
const { signMessageAsync } = useSignMessage()
const login = async () => {
if (!address) return
// 1. 获取 nonce
const nonceRes = await fetch('http://localhost:3001/auth/nonce', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address }),
})
const { nonce } = await nonceRes.json()
// 2. 钱包签名
const signature = await signMessageAsync({ message: nonce })
// 3. 发送签名到后端验证
const verifyRes = await fetch('http://localhost:3001/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature }),
})
const result = await verifyRes.json()
if (result.success) {
alert('登录成功!')
// 保存 token,后续请求携带
} else {
alert('登录失败')
}
}
return <button onClick={login}> 钱包登录</button>
}
至此,“连接钱包即登录”的完整流程就跑通了。
5. 常见问题与优化建议
5.1 切换网络
wagmi 提供了 useSwitchChain Hook,可以引导用户切换到指定链,或者使用 switchChain 直接调用。
5.2 多钱包冲突
当用户同时安装 MetaMask 和 Coinbase Wallet 时,injected 连接器会出现多个选项。使用 metaMask() 或 coinbaseWallet() 显式指定可以避免混淆。
5.3 生产环境安全
- Nonce 应存储在 Redis/数据库中,并设置合理过期时间。
- 避免在 Nonce 中泄露敏感信息。
- 签名验证通过后务必签发具备时效性的 JWT,后续接口验证 JWT 而非重复签名。
- 前端请求后端时,使用 HTTPS 防止中间人攻击。
6. 总结
这篇文章我们从零搭建了一个 Web3 全栈应用的钱包集成方案,覆盖了:
- React 中使用 wagmi + viem 连接多种钱包
- 读取链上余额与合约状态
- 发送交易并等待上链确认
- 实现“签名即登录”的用户认证流程
完整代码已经具备了可扩展性:你可以在前端接入更多的合约方法,在后端加入数据库持久化用户数据,或者用 JWT 保护 API 接口。希望这篇教程能帮你迈出 Web3 全栈开发坚实的一步。
如果这篇教程对你有帮助,欢迎分享或留言交流。我们下一篇文章再见!