本文介绍了如何创建一个代币授权检查器应用程序,以便用户可以检查他们的 ERC-20 代币的授权情况和历史记录。文章通过分步指导,详细阐述了使用 QuickNode 创建流、PostgreSQL 数据库设计,以及使用 React 和 Express 搭建前后端,结合示例代码提供了完整的实现思路。
在 DeFi 中进行交易时,无论是交换、质押等,你会授权你的 ERC-20 代币给智能合约,从而允许智能合约为你支出代币。大多数情况下,智能合约批准的代币数量比该特定交易所需的要多,以节省未来的 gas 费用,但这可能会带来安全风险,因为你授权了大量的代币。因此,让我们创建一个代币允许/批准检查应用程序,你或者任何人都可以检查给定钱包地址的各种代币的 token allowances 以及你的批准历史。
如何创建代币批准检查应用程序 - YouTube
QuickNode
131K 订阅者
QuickNode
搜索
信息
购物
点击即可静音
如果播放没有开始,尝试重启你的设备。
你已注销
你观看的视频可能会被添加到电视观看历史中并影响电视推荐。为避免这种情况,请在计算机上取消并登录 YouTube。
取消确认
分享
包含播放列表
检索共享信息时发生错误。请稍后再试。
稍后观看
分享
复制链接
观看
0:00
/ •实时
•
订阅我们的 YouTube 频道以获取更多视频! 订阅
我们需要通过过滤各个区块的日志来创建索引允许事件;现在,由于我们也需要获取历史允许,我们需要从创世区块获取数据。
如果你还没有,请创建一个 QuickNode 账户 并创建一个 Stream. 一旦你点击该链接,包含 Logs
数据集和过滤代码的数据集字段将自动填入你的 Stream 配置中。
现在,对于 Stream start
,选择 区块 # 并输入区块 0 或 1,因为我们需要从创世区块开始索引链,并保持 Stream end
为未选中状态,因为我们需要最新的区块数据。
这就是过滤代码的样子:
function extractAddress(paddedAddress) {
if (typeof paddedAddress !== 'string' || paddedAddress.length < 40) {
console.error('地址格式无效:', paddedAddress);
return null;
}
return '0x' + paddedAddress.slice(-40);
}
function hexToDecimal(hexValue) {
if (typeof hexValue !== 'string') {
console.error('无效的十六进制值:', hexValue);
return '0';
}
// 如果存在,移除 '0x' 前缀
hexValue = hexValue.replace(/^0x/, '');
// 如果结果字符串为空,则返回 '0'
if (hexValue === '') {
return '0';
}
// 将十六进制字符串解析为 BigInt(以处理大数字)
try {
return BigInt('0x' + hexValue).toString();
} catch (error) {
console.error('转换十六进制到十进制时出错:', error);
return '0';
}
}
function findAndFilterLogs(data) {
const targetTopic = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925";
let results = [];
let signatureFound = false;
function search(item) {
if (Array.isArray(item)) {
item.forEach(search);
} else if (typeof item === 'object' && item !== null) {
if (item.topics && item.topics[0] === targetTopic) {
signatureFound = true;
try {
const wallet = extractAddress(item.topics[1]);
const spender = extractAddress(item.topics[2]);
if (wallet && spender) {
results.push({
token: item.address,
wallet: wallet,
spender: spender,
amount: hexToDecimal(item.data),
blockNumber: hexToDecimal(item.blockNumber),
transactionHash: item.transactionHash
});
}
} catch (error) {
console.error('处理日志条目时出错:', error);
}
} else {
Object.values(item).forEach(search);
}
}
}
search(data);
return signatureFound ? results : null;
}
function main(data) {
const filteredData = findAndFilterLogs(data);
console.log(JSON.stringify(filteredData, null, 2));
return filteredData;
}
在这个代码片段中,我们正在过滤特定的代币允许/批准事件的日志,Approval (index_topic_1 address owner, index_topic_2 address spender, uint256 value)
,我们通过查找具有签名 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
的日志中的事件来执行此操作,这基本上是 Approval 事件的编码/哈希格式。除了过滤事件外,我们还使地址和区块号更易于阅读,并返回一系列有用的信息,如 token
、wallet
、spender
、amount
、blockNumber
和 transactionHash
。
我们在将 Stream 数据发送到目标之前对其进行过滤,因为我们只想要特定的数据,而不需要处理不需要的数据。使用 Streams,你只为流式传输的数据付费,因此是双赢。
现在,点击 下一步 并选择 PostgreSQL 作为目标类型,并填写你的 Postgres 实例的配置详细信息。在 Table
字段中写入表名,此表将由 Streams 创建,数据将流式传输到该表。
我们将首先创建一个新的表,包含 wallet
、approval_history
、latest_approvals
和 last_updated
列。
然后,我们将使用以下函数进一步精炼由 Streams 创建的表中的数据
CREATE OR REPLACE FUNCTION process_wallet_approval_states_latest()
RETURNS void AS $$
DECLARE
-- 确保替换为你表的名称,而不是 token_allowances_new
cur CURSOR FOR SELECT * FROM token_allowances_new ORDER BY block_number ASC;
row_data RECORD;
allowance_events JSONB;
allowance_event JSONB;
v_wallet_address VARCHAR(42);
approval_data JSONB;
total_rows INT;
processed_rows INT := 0;
skipped_rows INT := 0;
BEGIN
SELECT COUNT(*) INTO total_rows FROM token_allowances_new;
RAISE NOTICE '开始处理 % 行', total_rows;
FOR row_data IN cur LOOP
BEGIN
IF jsonb_typeof(row_data.data) = 'array' THEN
allowance_events := row_data.data;
ELSE
-- 如果不是数组,则将其包装在数组中
allowance_events := jsonb_build_array(row_data.data);
END IF;
FOR allowance_event IN SELECT jsonb_array_elements(allowance_events)
LOOP
BEGIN
v_wallet_address := allowance_event->>'wallet';
IF v_wallet_address IS NULL THEN
RAISE EXCEPTION '钱包地址为空';
END IF;
approval_data := jsonb_build_object(
'token', allowance_event->>'token',
'spender', allowance_event->>'spender',
'amount', allowance_event->>'amount',
'blockNumber', (allowance_event->>'blockNumber')::bigint,
'transactionHash', allowance_event->>'transactionHash'
);
-- 插入或更新 wallet_approval_states 表
-- 确保替换为你的表名,而不是 wallet_approval_states_latest
INSERT INTO wallet_approval_states_latest (wallet_address, approval_history, latest_approvals)
VALUES (
v_wallet_address,
ARRAY[approval_data],
jsonb_build_object(
concat(approval_data->>'token', ':', approval_data->>'spender'),
approval_data
)
)
ON CONFLICT (wallet_address)
DO UPDATE SET
approval_history = wallet_approval_states_latest.approval_history || ARRAY[approval_data],
latest_approvals =
CASE
WHEN (approval_data->>'blockNumber')::bigint > COALESCE((wallet_approval_states_latest.latest_approvals->>(concat(approval_data->>'token', ':', approval_data->>'spender')))::jsonb->>'blockNumber', '0')::bigint
THEN wallet_approval_states_latest.latest_approvals || jsonb_build_object(concat(approval_data->>'token', ':', approval_data->>'spender'), approval_data)
ELSE wallet_approval_states_latest.latest_approvals
END,
last_updated = CURRENT_TIMESTAMP
WHERE wallet_approval_states_latest.wallet_address = v_wallet_address;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING '处理第 % 行事件时出错 (区块 %): %', processed_rows, row_data.block_number, SQLERRM;
END;
END LOOP;
processed_rows := processed_rows + 1;
IF processed_rows % 100 = 0 THEN
RAISE NOTICE '已处理 % 行中的 % 行', processed_rows, total_rows;
END IF;
EXCEPTION
WHEN OTHERS THEN
skipped_rows := skipped_rows + 1;
RAISE WARNING '处理第 % 行时出错 (区块 %): %', processed_rows, row_data.block_number, SQLERRM;
END;
END LOOP;
RAISE NOTICE '完成处理 % 行。跳过 % 行。', processed_rows, skipped_rows;
END;
$$ LANGUAGE plpgsql;
注意:在上面的函数中,由 Streams 创建的表是 token_allowances_new,而经过精炼的数据的表是 wallet_approval_states_latest。
在这个 示例应用程序的目录 中找到 React 应用程序和 Express 服务器的代码。
这里我们在 src
目录中有 React 应用程序,一个环境文件,以及一个用于 Express 服务器的 server.js
。
React 应用程序有一个组件 WalletApprovalSearch.js,它从用户那里获取钱包地址,获取来自 Express 服务器的批准数据,从其智能合约使用 QuickNode RPC 获取代币数据,然后显示出来。
import React, { useState } from 'react';
import { ethers } from 'ethers';
import axios from 'axios';
import {
TextField, Button, Table, TableBody, TableCell,
TableContainer, TableHead, TableRow, Paper, Modal,
Box, Typography, Container, CircularProgress
} from '@mui/material';
const WalletApprovalSearch = () => {
const [walletAddress, setWalletAddress] = useState('');
const [approvalData, setApprovalData] = useState(null);
const [historyData, setHistoryData] = useState(null);
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false);
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSearch = async () => {
setIsLoading(true);
setError('');
setApprovalData(null);
setHistoryData(null);
try {
const response = await axios.get(`http://localhost:3001/api/wallet-approval-states/${walletAddress}`);
const { latest_approvals, approval_history } = response.data;
if (latest_approvals) {
const processedApprovals = await processApprovals(latest_approvals);
setApprovalData(processedApprovals);
}
if (approval_history) {
const historyObject = typeof approval_history === 'string'
? JSON.parse(approval_history)
: approval_history;
const processedHistory = await processApprovals(historyObject);
setHistoryData(processedHistory);
}
if (!latest_approvals && !approval_history) {
setError('未找到此钱包地址的批准数据。');
}
} catch (error) {
console.error('获取数据时出错:', error);
setError('获取数据时出错。请重试。 ' + (error.response?.data?.error || error.message));
} finally {
setIsLoading(false);
}
};
const processApprovals = async (approvals) => {
const provider = new ethers.JsonRpcProvider(process.env.REACT_APP_QUICKNODE_ENDPOINT);
return Promise.all(Object.entries(approvals).map(async ([key, approval]) => {
const tokenContract = new ethers.Contract(approval.token, [\
'function name() view returns (string)',\
'function symbol() view returns (string)',\
'function decimals() view returns (uint8)',\
], provider);
try {
const [name, symbol, decimals] = await Promise.all([\
tokenContract.name().catch(() => '未知'),\
tokenContract.symbol().catch(() => 'UNK'),\
tokenContract.decimals().catch(() => 18),\
]);
const normalizedAmount = ethers.formatUnits(approval.amount, decimals);
return {
...approval,
tokenName: name,
tokenSymbol: symbol,
normalizedAmount,
};
} catch (error) {
console.warn(`处理代币 ${approval.token} 时出错:`, error);
return {
...approval,
tokenName: '未知',
tokenSymbol: 'UNK',
normalizedAmount: ethers.formatUnits(approval.amount, 18), // 假设为 18 个小数位
};
}
}));
};
return (
<Container maxWidth="lg">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
钱包批准搜索
</Typography>
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<TextField
fullWidth
label="钱包地址"
variant="outlined"
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
/>
<Button variant="contained" onClick={handleSearch} disabled={isLoading}>
{isLoading ? <CircularProgress size={24} /> : '搜索'}
</Button>
</Box>
{error && (
<Typography color="error" sx={{ mb: 2 }}>
{error}
</Typography>
)}
{isLoading && (
<Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}>
<CircularProgress />
</Box>
)}
{!isLoading && approvalData && approvalData.length > 0 && (
<>
<Typography variant="h6" gutterBottom>
最新批准
</Typography>
{approvalData.some(approval => approval.tokenName === '未知') && (
<Typography color="warning" sx={{ mb: 2 }}>
警告:某些代币信息无法检索。这些代币标记为‘未知’。
</Typography>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>代币名称</TableCell>
<TableCell>代币符号</TableCell>
<TableCell>数量</TableCell>
<TableCell>支出者</TableCell>
<TableCell>块编号</TableCell>
</TableRow>
</TableHead>
<TableBody>
{approvalData.map((approval, index) => (
<TableRow key={index}>
<TableCell>{approval.tokenName}</TableCell>
<TableCell>{approval.tokenSymbol}</TableCell>
<TableCell>{approval.normalizedAmount}</TableCell>
<TableCell>{approval.spender}</TableCell>
<TableCell>{approval.blockNumber}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{historyData && historyData.length > 0 && (
<Button onClick={() => setIsHistoryModalOpen(true)} sx={{ mt: 2 }}>
查看历史
</Button>
)}
</>
)}
{!isLoading && approvalData !== null && approvalData.length === 0 && (
<Typography>未找到记录</Typography>
)}
<Modal
open={isHistoryModalOpen}
onClose={() => setIsHistoryModalOpen(false)}
>
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '80%',
maxWidth: 800,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflow: 'auto',
}}>
<Typography variant="h6" gutterBottom>批准历史</Typography>
{historyData && historyData.some(approval => approval.tokenName === '未知') && (
<Typography color="warning" sx={{ mb: 2 }}>
警告:某些历史代币信息无法检索。这些代币标记为‘未知’。
</Typography>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>代币名称</TableCell>
<TableCell>代币符号</TableCell>
<TableCell>数量</TableCell>
<TableCell>支出者</TableCell>
<TableCell>块编号</TableCell>
</TableRow>
</TableHead>
<TableBody>
{historyData && historyData.map((approval, index) => (
<TableRow key={index}>
<TableCell>{approval.tokenName}</TableCell>
<TableCell>{approval.tokenSymbol}</TableCell>
<TableCell>{approval.normalizedAmount}</TableCell>
<TableCell>{approval.spender}</TableCell>
<TableCell>{approval.blockNumber}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Button onClick={() => setIsHistoryModalOpen(false)} sx={{ mt: 2 }}>
关闭
</Button>
</Box>
</Modal>
</Box>
</Container>
);
};
export default WalletApprovalSearch;
环境变量文件包含 QuickNode URL 和 PostgreSQL 实例的配置。
REACT_APP_QUICKNODE_ENDPOINT=
## 数据库配置
DB_USER=
DB_HOST=
DB_NAME=
DB_PASSWORD=
DB_PORT=
server.js 是一个与 PostgreSQL 实例连接并在 3001 端口上提供服务的 Express 服务器。
require('dotenv').config();
const express = require('express');
const { Pool } = require('pg');
const cors = require('cors');
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
ssl: {
rejectUnauthorized: false
}
});
// 测试数据库连接
pool.connect((err, client, release) => {
if (err) {
return console.error('获取客户端时出错', err.stack);
}
client.query('SELECT NOW()', (err, result) => {
release();
if (err) {
return console.error('执行查询时出错', err.stack);
}
console.log('成功连接到数据库');
});
});
const app = express();
app.use(cors());
app.get('/api/wallet-approval-states/:walletAddress', async (req, res) => {
try {
const { walletAddress } = req.params;
const result = await pool.query(
'SELECT latest_approvals, approval_history FROM wallet_approval_states WHERE wallet_address = $1', //将 wallet_approval_states 替换为你的表名
[walletAddress]
);
if (result.rows.length > 0) {
res.json(result.rows[0]);
} else {
res.status(404).json({ error: '未找到钱包地址' });
}
} catch (error) {
console.error('数据库错误:', error);
res.status(500).json({ error: '内部服务器错误', details: error.message });
}
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`服务器运行在端口 ${PORT}`));
让我们知道 如果你有任何反馈或对新主题的请求。我们想听听你的意见。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!