跳到主要内容

编写简易 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);
}
};

发送交易

  1. 在发送转账之前容错判断 Signer 是否存在。
  2. 使用 isAddress 方法判断转账地址是否有效的以太坊地址。
  3. 调用 SignersendTransaction 方法发送交易。
  4. 等待交易确认,更新余额反应到前端 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.jsReactAnt Design,手把手实现了一个简易的以太坊 dApp,涵盖了钱包连接、余额查询、ETH 转账、交易状态反馈等核心功能。通过本例,你可以掌握 dApp 前端与以太坊交互的基本流程,为后续开发更复杂的区块链应用打下基础。建议读者多动手实践,尝试扩展更多功能,如切换网络、显示交易历史、集成合约调用等,进一步加深对区块链前端开发的理解。

本章所有示例代码,均可在 GitHub 中找到。