编写简易 dApp
结合之前的知识,现在我们可以编写一个简易的 dApp
应用了。
dApp 是什么
dApp(去中心化应用,Decentralized Application)
是基于区块链技术开发的应用程序,与传统中心化应用(如微信、淘宝等由单一机构控制服务器和数据的应用)不同,dApp 的核心特点是去中心化,数据和逻辑分布在区块链网络的多个节点上,而非集中存储在单一服务器中。
开发 dApp 简易应用
主要功能包括:
- 连接
MetaMask
钱包 - 显示账户地址和
ETH
余额 - 发送
ETH
转账交易 - 查看交易状态和详情
技术栈:
- React:前端框架
- TypeScript:类型系统
- Ethers.js v6:以太坊交互库
- Ant Design:UI 组件库
定义状态
定义一组状态,用于 UI 界面显示。
// 基础状态
const [account, setAccount] = useState<string>(''); // 当前连接的账户地址
const [balance, setBalance] = useState<string>(''); // 账户余额
const [provider, setProvider] = useState<ethers.BrowserProvider | null>(null); // Ethers.js provider
const [signer, setSigner] = useState<ethers.Signer | null>(null); // 签名者对象
const [txHash, setTxHash] = useState<string>(''); // 交易哈希
const [isLoading, setIsLoading] = useState<boolean>(false); // 加载状态
// 表单状态管理
const [form] = Form.useForm<TransferForm>(); // Ant Design Form 实例
连接钱包
判断 MetaMask
钱包是否安装,并打开授权当前网站,获得 Signer
签名。
const connectWallet = async () => {
try {
// window.ethereum 存在说明钱包注入成功
if (typeof window.ethereum !== "undefined") {
const provider = new ethers.BrowserProvider(window.ethereum);
setProvider(provider);
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const account = accounts[0];
setAccount(account);
// 获取余额
const balance = await provider.getBalance(account);
setBalance(ethers.formatEther(balance));
// 获取 signer
const signer = await provider.getSigner();
setSigner(signer);
return;
}
// MetaMask 钱包没有安装,提示安装
notification.error({
message: "错误",
description: "请安装 MetaMask!",
});
} catch (error) {
// 用户可能拒绝授权
notification.error({
message: "连接钱包失败",
description: error.message,
});
console.error("连接钱包失败:", error);
}
};
发送交易
- 在发送转账之前容错判断
Signer
是否存在。 - 使用
isAddress
方法判断转账地址是否有效的以太坊地址。 - 调用
Signer
的sendTransaction
方法发送交易。 - 等待交易确认,更新余额反应到前端 UI。
// 发送交易
const sendTransaction = async (values: TransferForm) => {
if (!signer) {
notification.error({
message: "错误",
description: "请先连接钱包",
});
return;
}
if (ethers.isAddress(values.recipient) === false) {
notification.error({
message: "不是一个有效的以太坊地址",
});
return;
}
try {
setIsLoading(true);
const tx = await signer.sendTransaction({
to: values.recipient,
value: ethers.parseEther(String(values.amount)),
});
setTxHash(tx.hash);
notification.success({
message: `交易已发送: ${tx.hash}`,
});
// 等待交易确认
await tx.wait();
notification.success({
message: `交易已确认`,
});
// 更新余额
const newBalance = await provider?.getBalance(account);
if (newBalance) {
setBalance(ethers.formatEther(newBalance));
}
} catch (error) {
console.error("转账失败:", error);
notification.error({
message: "转账失败",
description: error.message,
});
} finally {
setIsLoading(false);
}
};
前端 UI
<div>
<Card>
<Title level={2}>简易 dApp 示例</Title>
{!account ? (
<Button
type="primary"
icon={<WalletOutlined />}
onClick={connectWallet}
size="large"
>
连接钱包
</Button>
) : (
<Space direction="vertical" style={{ width: "100%" }}>
<Alert
message="钱包已连接"
description={
<>
<Text strong>当前账户:</Text> {account}
<br />
<Text strong>账户余额:</Text> {balance} ETH
</>
}
type="success"
showIcon
/>
<Card title="发送 ETH" size="small">
<Form form={form} onFinish={sendTransaction} layout="vertical">
<Form.Item
initialValue="0x817C6Ef5f2EF3CC56ce87942BF7ed74138EC284C"
name="recipient"
label="接收地址"
rules={[{ required: true, message: "请输入接收地址" }]}
>
<Input placeholder="0x..." disabled={isLoading} />
</Form.Item>
<Form.Item
initialValue={0.00001}
name="amount"
label="发送数量 (ETH)"
rules={[{ required: true, message: "请输入发送数量" }]}
>
<InputNumber
style={{ width: "100%" }}
placeholder="请输入要转账的地址"
min={0.00001}
max={1}
step={0.00001}
disabled={isLoading}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
icon={<SendOutlined />}
loading={isLoading}
block
>
发送
</Button>
</Form.Item>
</Form>
</Card>
{txHash && (
<Alert
message="交易已发送"
description={
<Space>
<Text>交易哈希: {txHash}</Text>
<Button
type="link"
icon={<LinkOutlined />}
href={`https://sepolia.etherscan.io/tx/${txHash}`}
target="_blank"
>
在 Etherscan 上查看
</Button>
</Space>
}
type="info"
showIcon
/>
)}
</Space>
)}
</Card>
</div>
完整代码
import React, { useState } from "react";
import { ethers } from "ethers";
import {
Button,
Form,
Input,
Card,
Typography,
Space,
Alert,
InputNumber,
notification,
} from "antd";
import { WalletOutlined, SendOutlined, LinkOutlined } from "@ant-design/icons";
const { Title, Text } = Typography;
// 声明 window.ethereum 类型
declare global {
interface Window {
ethereum: any;
}
}
interface TransferForm {
recipient: string;
amount: string;
}
const DApp: React.FC = () => {
const [account, setAccount] = useState<string>("");
const [balance, setBalance] = useState<string>("");
const [provider, setProvider] = useState<ethers.BrowserProvider | null>(null);
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [txHash, setTxHash] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [form] = Form.useForm<TransferForm>();
// 连接钱包
const connectWallet = async () => {
try {
if (typeof window.ethereum !== "undefined") {
const provider = new ethers.BrowserProvider(window.ethereum);
setProvider(provider);
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const account = accounts[0];
setAccount(account);
// 获取余额
const balance = await provider.getBalance(account);
setBalance(ethers.formatEther(balance));
// 获取 signer
const signer = await provider.getSigner();
setSigner(signer);
return;
}
notification.error({
message: "错误",
description: "请安装 MetaMask!",
});
} catch (error) {
notification.error({
message: "连接钱包失败",
description: error.message,
});
console.error("连接钱包失败:", error);
}
};
// 发送交易
const sendTransaction = async (values: TransferForm) => {
if (!signer) {
notification.error({
message: "错误",
description: "请先连接钱包",
});
return;
}
if (ethers.isAddress(values.recipient) === false) {
notification.error({
message: "不是一个有效的以太坊地址",
});
return;
}
try {
setIsLoading(true);
const tx = await signer.sendTransaction({
to: values.recipient,
value: ethers.parseEther(String(values.amount)),
});
setTxHash(tx.hash);
notification.success({
message: `交易已发送: ${tx.hash}`,
});
// 等待交易确认
await tx.wait();
notification.success({
message: `交易已确认`,
});
// 更新余额
const newBalance = await provider?.getBalance(account);
if (newBalance) {
setBalance(ethers.formatEther(newBalance));
}
} catch (error) {
console.error("转账失败:", error);
notification.error({
message: "转账失败",
description: error.message,
});
} finally {
setIsLoading(false);
}
};
return (
<div>
<Card>
<Title level={2}>简易 dApp 示例</Title>
{!account ? (
<Button
type="primary"
icon={<WalletOutlined />}
onClick={connectWallet}
size="large"
>
连接钱包
</Button>
) : (
<Space direction="vertical" style={{ width: "100%" }}>
<Alert
message="钱包已连接"
description={
<>
<Text strong>当前账户:</Text> {account}
<br />
<Text strong>账户余额:</Text> {balance} ETH
</>
}
type="success"
showIcon
/>
<Card title="发送 ETH" size="small">
<Form form={form} onFinish={sendTransaction} layout="vertical">
<Form.Item
initialValue="0x817C6Ef5f2EF3CC56ce87942BF7ed74138EC284C"
name="recipient"
label="接收地址"
rules={[{ required: true, message: "请输入接收地址" }]}
>
<Input placeholder="0x..." disabled={isLoading} />
</Form.Item>
<Form.Item
initialValue={0.00001}
name="amount"
label="发送数量 (ETH)"
rules={[{ required: true, message: "请输入发送数量" }]}
>
<InputNumber
style={{ width: "100%" }}
placeholder="请输入要转账的地址"
min={0.00001}
max={1}
step={0.00001}
disabled={isLoading}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
icon={<SendOutlined />}
loading={isLoading}
block
>
发送
</Button>
</Form.Item>
</Form>
</Card>
{txHash && (
<Alert
message="交易已发送"
description={
<Space>
<Text>交易哈希: {txHash}</Text>
<Button
type="link"
icon={<LinkOutlined />}
href={`https://sepolia.etherscan.io/tx/${txHash}`}
target="_blank"
>
在 Etherscan 上查看
</Button>
</Space>
}
type="info"
showIcon
/>
)}
</Space>
)}
</Card>
</div>
);
};
export default DApp;
尝试一下
简易 dApp 示例
总结
本章我们基于 Ethers.js
、React
和 Ant Design
,手把手实现了一个简易的以太坊 dApp
,涵盖了钱包连接、余额查询、ETH
转账、交易状态反馈等核心功能。通过本例,你可以掌握 dApp 前端与以太坊交互的基本流程,为后续开发更复杂的区块链应用打下基础。建议读者多动手实践,尝试扩展更多功能,如切换网络、显示交易历史、集成合约调用等,进一步加深对区块链前端开发的理解。
本章所有示例代码,均可在 GitHub 中找到。