前面四篇文章,我们的全栈应用始终锚定在 Sepolia 这一条测试网上。但在现实世界中,一个成熟的 DApp 需要同时服务 Ethereum、Polygon、Arbitrum、Optimism 甚至更多链的用户。与此同时,越来越多的项目和个人使用智能合约钱包(如 Safe 多签钱包)来管理资产,它和普通 EOA 钱包的交互方式略有不同。
今天这篇文章,你将完成:
- 让前端动态切换多条 EVM 链
- 统一管理各链上的合约读写与事件监听
- 集成 Safe 智能钱包作为登录和签名方案
- 后端适配多链事件索引
- 构建一条“多链代币余额总览”面板
所有代码延续 React + wagmi + viem + Node.js。
1. 多链架构设计
我们不必为每条链都写一套代码。wagmi 本身就支持多链配置,只需在 config 里添加更多链,然后在组件中动态获取当前 chainId,就可以决定使用哪个合约地址和 RPC。
1.1 配置更多链
打开 frontend/src/config/web3.ts,增加几条常用的 EVM 链:
import { http, createConfig } from 'wagmi'
import {
mainnet, sepolia,
polygon, polygonMumbai,
arbitrum, arbitrumSepolia,
optimism, optimismSepolia,
} from 'wagmi/chains'
import { injected, metaMask, walletConnect } from 'wagmi/connectors'
export const config = createConfig({
chains: [
mainnet, sepolia,
polygon, polygonMumbai,
arbitrum, arbitrumSepolia,
optimism, optimismSepolia,
],
connectors: [
injected(),
metaMask(),
walletConnect({
projectId: '你的_WalletConnect_Project_ID',
}),
],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
[polygon.id]: http(),
[polygonMumbai.id]: http(),
[arbitrum.id]: http(),
[arbitrumSepolia.id]: http(),
[optimism.id]: http(),
[optimismSepolia.id]: http(),
},
})
1.2 根据链动态选择地址
之前我们的合约地址是硬编码的。现在可以创建一个映射表:
// contracts/addresses.ts
export const TOKEN_ADDRESSES: Record<number, string> = {
1: '0x...', // mainnet MTK
11155111: '0xA1B2C3...', // sepolia MTK
137: '0x...',
80001: '0x...', // mumbai
42161: '0x...',
421614: '0x...',
10: '0x...',
11155420: '0x...',
}
export const WETH_ADDRESSES: Record<number, string> = {
1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
11155111: '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14',
// ...其他链的 WETH
}
export const UNISWAP_V2_ROUTER_ADDRESSES: Record<number, string> = {
1: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
11155111: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
// ...
}
在组件中使用 useAccount 的 chainId 即可动态取到正确的地址:
const { chainId } = useAccount()
const tokenAddress = TOKEN_ADDRESSES[chainId ?? 11155111]
1.3 切换链与添加链
wagmi 提供了 useSwitchChain 和 useAddChain(实验性)。我们可以快速构造一个网络选择器:
import { useSwitchChain, useChainId } from 'wagmi'
function ChainSwitcher() {
const chainId = useChainId()
const { switchChain, isPending } = useSwitchChain()
const supportedChains = [
{ id: 1, name: 'Ethereum' },
{ id: 11155111, name: 'Sepolia' },
{ id: 137, name: 'Polygon' },
{ id: 42161, name: 'Arbitrum' },
{ id: 10, name: 'Optimism' },
]
return (
<div>
<h4>当前网络:{supportedChains.find(c => c.id === chainId)?.name}</h4>
{supportedChains.map((c) => (
<button key={c.id} onClick={() => switchChain({ chainId: c.id })} disabled={isPending}>
{c.name}
</button>
))}
</div>
)
}
这样一来,你的 DApp 就能自由穿梭在各大 EVM 生态之间。
2. 多链余额面板
读取一个地址在多个链上的代币余额,可以组合多个 useReadContract,但这样会产生大量请求。更好的做法是封装一个 Hook 统一调用。
2.1 创建 useMultichainBalances Hook
import { useReadContracts, useAccount } from 'wagmi'
import { erc20Abi } from 'viem'
const CHAINS_TO_CHECK = [1, 11155111, 137, 42161, 10] as const
export function useMultichainBalances(tokenAddressMap: Record<number, string>) {
const { address } = useAccount()
const contracts = CHAINS_TO_CHECK.map((chainId) => ({
chainId,
address: tokenAddressMap[chainId] as `0x${string}`,
abi: erc20Abi,
functionName: 'balanceOf' as const,
args: address ? [address] : undefined,
}))
const { data, isLoading } = useReadContracts({
contracts,
query: { enabled: !!address },
})
// 将结果与链名称映射
const result = CHAINS_TO_CHECK.map((chainId, index) => ({
chainId,
chainName: getChainName(chainId),
balance: data?.[index]?.result,
isLoading,
}))
return result
}
然后在组件中展示:
function BalancePanel() {
const balances = useMultichainBalances(TOKEN_ADDRESSES)
return (
<div>
<h3>📊 多链 MTK 余额</h3>
<ul>
{balances.map((b) => (
<li key={b.chainId}>
{b.chainName}: {b.balance ? formatUnits(b.balance, 18) : '—'} MTK
</li>
))}
</ul>
</div>
)
}
3. 集成 Safe 智能钱包
Safe(原 Gnosis Safe)是最流行的智能合约钱包,支持多签和账户抽象。它对外暴露的接口与普通 EOA 类似,但交易需要通过 execTransaction 或 Safe SDK 来进行,签名验证逻辑也不同。
在 DApp 层面,直接使用 Safe{Core} SDK 可以无缝集成。我们也可以利用 wagmi 的 connectors 中 safe 连接器(需要额外安装),或直接手动适配。
3.1 使用 Safe 连接器
安装依赖:
pnpm add @safe-global/safe-apps-sdk @wagmi/connectors
在 web3.ts 中添加 Safe 连接器:
import { safe } from 'wagmi/connectors'
export const config = createConfig({
// ...
connectors: [
injected(),
metaMask(),
walletConnect({ projectId: '...' }),
safe(),
],
// ...
})
这样,在 Safe 内嵌浏览器或 Safe 移动 App 内打开 DApp 时,会自动使用 Safe 连接器。但对于桌面版普通浏览器,Safe 连接器不会直接弹出钱包,需要用户手动复制 Safe 地址后粘贴进行模拟连接(通常用于开发者测试)。
3.2 识别是否为智能钱包
我们可以通过获取地址的 code 来判断是 EOA 还是合约:
import { useBytecode } from 'wagmi'
import { useAccount } from 'wagmi'
function useIsSmartWallet() {
const { address } = useAccount()
const { data: bytecode } = useBytecode({ address })
return bytecode && bytecode !== '0x'
}
智能钱包的签名方式可能不同。Safe 使用 EIP-712 签名或合约签名,wagmi 的 signMessage 会自动适配(Safe 支持标准 eth_sign)。但如果遇到不支持的,则需要使用 Safe SDK 的 signMessage 方法。
3.3 发送交易时 Gas 估计
智能钱包的交易消耗 Gas 更多,且 Gas 预估依赖于内部逻辑。wagmi 和 viem 会自动处理大部分情况,但有时需要手动设置 gasLimit。例如 Safe 的 execTransaction 有时需要额外缓冲:
const { writeContract } = useWriteContract()
// 在 mutate 时传入 gas 缓冲
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [to, amount],
gas: 300000n, // Safe 交易一般需要更高的 Gas
})
4. 多签流程与自定义 Hooks
Safe 的核心流程:一个用户发起交易,其他拥有者确认(approve),达到阈值后任意一人执行。
如果要在 DApp 内构建多签管理界面,可以使用 @safe-global/api-kit 和 @safe-global/protocol-kit。这里给一个最小示例。
4.1 安装
pnpm add @safe-global/protocol-kit @safe-global/api-kit
4.2 创建 Safe 实例并发起交易
import SafeApiKit from '@safe-global/api-kit'
import Safe from '@safe-global/protocol-kit'
import { ethers } from 'ethers' // v5 兼容,或 viem 适配器
const SAFE_ADDRESS = '0xYourSafeAddress'
async function proposeTransaction() {
const provider = new ethers.BrowserProvider(window.ethereum)
const signer = await provider.getSigner()
const safeSdk = await Safe.init({
provider: window.ethereum,
signer: await signer.getAddress(),
safeAddress: SAFE_ADDRESS,
})
const safeTransaction = await safeSdk.createTransaction({
transactions: [{
to: TOKEN_ADDRESS,
value: '0',
data: encodeTransferData(to, amount),
}],
})
const safeTxHash = await safeSdk.getTransactionHash(safeTransaction)
const signature = await safeSdk.signHash(safeTxHash)
// 使用 API Kit 提交到 Safe 服务
const apiKit = new SafeApiKit({ chainId: 11155111n })
await apiKit.proposeTransaction({
safeAddress: SAFE_ADDRESS,
safeTransactionData: safeTransaction.data,
safeTxHash,
senderAddress: await signer.getAddress(),
senderSignature: signature.data,
})
}
在你的 DApp 中,可以封装一个“多签建议”页面,列出待确认交易并允许签名。
5. 后端:多链事件索引
后端也可以用 viem 监听多条链,只需创建多个 PublicClient 并分别启动监听器。
5.1 动态创建客户端
// backend/src/chains.ts
import { createPublicClient, http } from 'viem'
import { mainnet, sepolia, polygon } from 'viem/chains'
export const chainClients = {
[mainnet.id]: createPublicClient({ chain: mainnet, transport: http() }),
[sepolia.id]: createPublicClient({ chain: sepolia, transport: http() }),
[polygon.id]: createPublicClient({ chain: polygon, transport: http() }),
}
5.2 启动多链 Swap 监听
export function startAllSwapListeners(pairMap: Record<number, string>) {
for (const [chainId, pairAddress] of Object.entries(pairMap)) {
const client = chainClients[Number(chainId)]
if (!client || !pairAddress) continue
client.watchContractEvent({
address: pairAddress as `0x${string}`,
abi: pairAbi,
eventName: 'Swap',
onLogs(logs) {
for (const log of logs) {
insertSwapEvent({ chainId: Number(chainId), ...log })
}
},
})
}
}
记得在 swap_events 表中增加 chainId 字段,API 也支持按链过滤。
6. 前端集成多链历史
修改 TransferHistory 组件,增加链切换筛选。也可以直接合并展示:
function AllChainSwapHistory() {
const [chainFilter, setChainFilter] = useState<number | 'all'>('all')
const [events, setEvents] = useState([])
useEffect(() => {
fetch(`http://localhost:3001/swaps?chainId=${chainFilter}`)
.then(res => res.json())
.then(setEvents)
}, [chainFilter])
// 渲染...
}
7. 安全与 UX 考量
- 网络切换前提示:自动切换可能被浏览器拦截,应在 UI 提供手动切换按钮。
- 智能钱包 Gas 补贴:考虑使用账户抽象基础设施(如 Biconomy、Pimlico)来赞助 Gas。
- 多签提示:当检测到用户使用多签钱包时,应显示“待确认”状态,避免误以为交易已立即完成。
- RPC 稳定性:生产环境建议使用自定义 RPC URL(如 Infura、Alchemy),而非公共端点。
8. 总结
这篇为你打开了“多链世界”的大门:
- 动态多链配置与网络切换
- 基于链 ID 的合约地址映射
- 多链余额聚合展示
- 集成 Safe 智能钱包与多签流程
- 后端多链事件监听