本文介绍了如何使用 Beacon REST API 和 Ethers.js 构建一个以太坊 2.0 的验证者排行榜应用。详细说明了如何配置项目,安装依赖,以及各个功能的实现,包括获取验证者余额、计算总余额和生成排行榜等。代码部分包含了重要的实现细节,适合有一定基础的开发者阅读。
Beacon 链是 ETH 2 质押智能合约的所在链。该链需要被查询以获取与验证者和质押合约相关的任何数据。在这个视频中,我们将看到如何查询 Beacon 链以获取 ETH 2 质押合约中总质押的 ETH、获取验证者的余额,并根据他们持有的总 ETH 创建验证者排行榜。
我们将使用 QuickNode 的 Ethereum 端点的 Beacon REST API 与 Ethers.js 进行区块链交互。我们将在 Next.js 和 Tailwind CSS 中构建应用进行样式设计。
依赖项 | 版本 |
---|---|
node.js | latest |
Next.js | latest |
ethers.js | 5.7.2 |
daisyUI | latest |
从零开始构建应用 - Ethereum 2 Beaconchain 验证者排行榜 - YouTube
QuickNode
131K 订阅者
从零开始构建应用 - Ethereum 2 Beaconchain 验证者排行榜
QuickNode
搜索
信息
购物
轻触以取消静音
如果播放没有开始,请尝试重新启动你的设备。
你已退出登录
你观看的视频可能会被添加到电视的观看历史中并影响电视推荐。为了避免这种情况,请在你的计算机上取消并登录 YouTube。
取消确认
分享
包含播放列表
检索共享信息时发生错误。请稍后再试。
稍后观看
分享
复制链接
观看于
0:00
/ •实时
•
订阅我们的 YouTube 频道以获取更多视频! 订阅
按照视频和以下步骤操作以运行该应用,或从此 GitHub 仓库 Git 克隆,进入目录并运行 npm install
,创建 .env
文件并将 NEXT_PUBLIC_QUICKNODE_RPC
作为变量保留你的 QuickNode 端点 HTTPS URL,然后运行 npm run dev
启动项目。
git clone https://github.com/velvet-shark/beacon-validators.git
cd beacon-validators
按照以下步骤逐步创建应用:
创建帐户后,点击 创建端点按钮。然后,选择 Ethereum 主网。
npm i create-next-app
现在,通过运行以下命令初始化 Next.js 应用:
npx create-next-app@latest beacon-validators
beacon-validators 是项目名称,你可以将其更改为你选择的任何名称。
Tailwind 已包含在较新版本的 create-next-app 中。
询问选项时,使用以下配置:
进入新创建的目录并通过运行安装 daisyUI 和其他依赖项:
npm i -D daisyui postcss autoprefixer
我们需要一个特定版本的 Ethers.js 以与 Next.js 配合使用,因此我们将通过以下命令安装:
npm i ethers@5.7.2
找到 tailwind.config.js
文件,并将文件内容替换为以下内容:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./pages/**/*.{js,ts,jsx,tsx}"],
plugins: [require("daisyui")]
};
现在,找到 styles
目录中的 global.css
文件,删除所有内容,保留以下内容:
@tailwind base;
@tailwind components;
@tailwind utilities;
现在,找到 pages
目录中的 index.js
文件,并将代码替换为以下代码:
import Head from "next/head";
import Image from "next/image";
import { Inter } from "@next/font/google";
import styles from "@/styles/Home.module.css";
import { ethers, utils } from "ethers";
import { useState } from "react";
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
const [totalBalance, setTotalBalance] = useState(0);
const [validators, setValidators] = useState([]);
const [validatorBalance, setValidatorBalance] = useState(0);
const [leaderboard, setLeaderboard] = useState([]);
const [pendingInitializedBalance, setPendingInitializedBalance] = useState();
const [pendingQueuedBalance, setPendingQueuedBalance] = useState();
const [activeOngoingBalance, setActiveOngoingBalance] = useState();
const [activeExitingBalance, setActiveExitingBalance] = useState();
const [activeSlashedBalance, setActiveSlashedBalance] = useState();
const [exitedUnslashedBalance, setExitedUnslashedBalance] = useState();
const [exitedSlashedBalance, setExitedSlashedBalance] = useState();
const [withdrawalPossibleBalance, setWithdrawalPossibleBalance] = useState();
const [withdrawalDoneBalance, setWithdrawalDoneBalance] = useState();
// 获取 Beacon 存款合约余额
const fetchBeaconContractBalance = async () => {
const provider = new ethers.providers.JsonRpcProvider(process.env.NEXT_PUBLIC_QUICKNODE_RPC);
const balance = await provider.getBalance("0x00000000219ab540356cBB839Cbe05303d7705Fa");
setTotalBalance(balance);
};
// 从 REST API 获取验证者数据
const fetchValidators = async () => {
try {
console.log("正在获取验证者...");
const validators = await fetch(
`${process.env.NEXT_PUBLIC_QUICKNODE_RPC}eth/v1/beacon/states/head/validators`
).then((res) => res.json());
// 取消注释以在控制台中查看所有验证者数据
// console.log(validators.data);
setValidators(validators.data);
} catch (error) {
console.error(error);
setValidators([]);
}
};
const sumValidatorBalances = (validators) => {
const validatorBalance = validators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
console.log(validatorBalance);
setValidatorBalance(validatorBalance);
};
// 汇总每个验证者状态的余额
const sumValidatorStatuses = (validators) => {
const pendingInitializedValidators = validators.filter(
(validator) => validator.status === "pending_initialized"
);
const pendingQueuedValidators = validators.filter((validator) => validator.status === "pending_queued");
const activeOngoingValidators = validators.filter((validator) => validator.status === "active_ongoing");
const activeExitingValidators = validators.filter((validator) => validator.status === "active_exiting");
const activeSlashedValidators = validators.filter((validator) => validator.status === "active_slashed");
const exitedUnslashedValidators = validators.filter((validator) => validator.status === "exited_unslashed");
const exitedSlashedValidators = validators.filter((validator) => validator.status === "exited_slashed");
const withdrawalPossibleValidators = validators.filter(
(validator) => validator.status === "withdrawal_possible"
);
const withdrawalDoneValidators = validators.filter((validator) => validator.status === "withdrawal_done");
const pendingInitializedBalance = pendingInitializedValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const pendingQueuedBalance = pendingQueuedValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const activeOngoingBalance = activeOngoingValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const activeExitingBalance = activeExitingValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const activeSlashedBalance = activeSlashedValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const exitedUnslashedBalance = exitedUnslashedValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const exitedSlashedBalance = exitedSlashedValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const withdrawalPossibleBalance = withdrawalPossibleValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
const withdrawalDoneBalance = withdrawalDoneValidators.reduce((acc, validator) => {
return acc + parseInt(validator.balance);
}, 0);
setPendingInitializedBalance(pendingInitializedBalance);
setPendingQueuedBalance(pendingQueuedBalance);
setActiveOngoingBalance(activeOngoingBalance);
setActiveExitingBalance(activeExitingBalance);
setActiveSlashedBalance(activeSlashedBalance);
setExitedUnslashedBalance(exitedUnslashedBalance);
setExitedSlashedBalance(exitedSlashedBalance);
setWithdrawalPossibleBalance(withdrawalPossibleBalance);
setWithdrawalDoneBalance(withdrawalDoneBalance);
console.log("pendingInitializedBalance", pendingInitializedBalance);
console.log("pendingQueuedBalance", pendingQueuedBalance);
console.log("activeOngoingBalance", activeOngoingBalance);
console.log("activeExitingBalance", activeExitingBalance);
console.log("activeSlashedBalance", activeSlashedBalance);
console.log("exitedUnslashedBalance", exitedUnslashedBalance);
console.log("exitedSlashedBalance", exitedSlashedBalance);
console.log("withdrawalPossibleBalance", withdrawalPossibleBalance);
console.log("withdrawalDoneBalance", withdrawalDoneBalance);
};
// 处理数据获取
const fetchData = (e) => {
fetchBeaconContractBalance();
};
const calculateData = (e) => {
sumValidatorBalances(validators);
sumValidatorStatuses(validators);
};
const calculateLeaderboard = (e) => {
const leaderboad = validators.sort((a, b) => b.balance - a.balance);
console.log(leaderboad.slice(0, 300));
setLeaderboard(leaderboad.slice(0, 300));
};
return (
<>
<Head>
<title>Ethereum 验证者数据</title>
<meta name="description" content="Ethereum 验证者数据" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="container xl mx-auto px-4">
<div className="hero bg-base-200 py-10">
<div className="hero-content text-center">
<div className="max-w-3xl">
<h1 className="text-5xl font-bold text-primary-content">Ethereum 验证者</h1>
<p className="py-6 text-primary-content">
总质押 ETH + 按质押状态(活动、被削减、退出等)汇总 + 前 300 名
质押排行榜
</p>
<button type="submit" onClick={fetchData} className="btn btn-primary m-2 normal-case">
获取质押余额
</button>
<button type="submit" onClick={fetchValidators} className="btn btn-primary m-2 normal-case">
获取验证者数据
</button>
<button
type="submit"
onClick={calculateData}
className="btn btn-secondary m-2 normal-case"
disabled={validators.length > 0 ? false : true}
>
计算验证者数据
</button>
<button
type="submit"
onClick={calculateLeaderboard}
className="btn btn-accent m-2 normal-case"
disabled={validators.length > 0 ? false : true}
>
显示排行榜
</button>
</div>
</div>
</div>
{totalBalance > 0 && (
<div className="stats bg-primary text-primary-content m-2">
<div className="stat">
<div className="stat-title">总 Beacon 合约余额</div>
<div className="stat-value">
{(parseInt(totalBalance) / 1e18).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
)}
{validators.length > 0 && (
<div className="stats bg-primary text-primary-content m-2">
<div className="stat">
<div className="stat-title">总验证者条目</div>
<div className="stat-value">
{validators.length.toLocaleString(undefined, { minimumFractionDigits: 0 })}
</div>
</div>
</div>
)}
<br />
{validatorBalance > 0 && (
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">所有验证者余额总和</div>
<div className="stat-value">
{(validatorBalance / 1e9).toLocaleString(undefined, { minimumFractionDigits: 0 })} ETH
</div>
</div>
</div>
)}
{validatorBalance > 0 && (
<>
<h2 className="text-4xl text-primary-content font-bold pt-5 pb-3">按状态汇总</h2>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>pending_initialized</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(pendingInitializedBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>pending_queued</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(pendingQueuedBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>active_ongoing</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(activeOngoingBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>active_exiting</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(activeExitingBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>active_slashed</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(activeSlashedBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>exited_unslashed</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(exitedUnslashedBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>exited_slashed</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(exitedSlashedBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>withdrawal_possible</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(withdrawalPossibleBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
<div className="stats bg-secondary text-primary-content m-2">
<div className="stat">
<div className="stat-title">
所有 <strong>withdrawal_done</strong> 验证者的余额总和
</div>
<div className="stat-value">
{(withdrawalDoneBalance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 0
})}{" "}
ETH
</div>
</div>
</div>
</>
)}
{leaderboard.length > 0 && (
<>
<h2 className="text-4xl text-primary-content font-bold pt-5 pb-3">排行榜</h2>
<div className="overflow-x-auto">
<table className="table table-compact w-full">
<thead>
<tr>
<th className="bg-accent text-primary-content">排名</th>
<th className="bg-accent text-primary-content">索引</th>
<th className="bg-accent text-primary-content">余额</th>
<th className="bg-accent text-primary-content">公钥</th>
</tr>
</thead>
<tbody>
{leaderboard.map((validator, index) => (
<tr key={validator.publicKey}>
<th>{index + 1}</th>
<td>{validator.index}</td>
<td>
<strong>
{(validator.balance / 1e9).toLocaleString(undefined, {
minimumFractionDigits: 4
})}{" "}
ETH
</strong>
</td>
<td>
<a
href={`https://beaconscan.com/validator/${validator.validator.pubkey}`}
target="_blank"
rel="noreferrer"
className="text-primary-content"
>
{validator.validator.pubkey}
</a>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<th>排名</th>
<th>索引</th>
<th>余额</th>
<th>公钥</th>
</tr>
</tfoot>
</table>
</div>
</>
)}
</div>
<footer className="footer footer-center p-4 bg-base-300 text-base-content">
<div>
<p>
<a
href="https://www.quicknode.com"
target="_blank"
rel="noreferrer"
className="text-primary-content"
>
<img src="powered-by-quicknode-blue.png" alt="QuickNode" width="150" />
</a>
</p>
<div>
<div className="card inline-block my-2 mx-4 w-69 bg-primary text-primary-content">
<div className="card-body">
<h2 className="card-title">代码</h2>
<p>网站的完整代码。</p>
<div className="card-actions justify-end">
<button className="btn normal-case">
<a
href="https://github.com/velvet-shark/beacon-validators"
target="_blank"
rel="noreferrer"
className="text-primary-content inline"
>
在 GitHub 上获取
</a>
</button>
</div>
</div>
</div>
<div className="card inline-block m-2 w-69 bg-primary text-primary-content">
<div className="card-body">
<h2 className="card-title">视频</h2>
<p>想看看现场构建吗?</p>
<div className="card-actions justify-end">
<button className="btn normal-case">
<a
href="https://youtu.be/DpQqXv8Tq5A"
target="_blank"
rel="noreferrer"
className="text-primary-content inline"
>
在 YouTube 上观看
</a>
</button>
</div>
</div>
</div>
</div>
</div>
</footer>
</>
);
}
在父目录 beacon-validators
中创建一个 .env 文件,并粘贴以下内容:
NEXT_PUBLIC_QUICKNODE_RPC = QUICKNODE_HTTPS_URL
用你的 QuickNode Ethereum 主网 HTTPS 端点替换 QUICKNODE_HTTPS_URL
。
npm run dev
最终的应用应该是这样的:
让我们知道如果你有任何反馈或新主题的请求。我们期待听到你的意见。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!