阅读时间 24 分钟
您的Quicknode 流现可通过单一管道同时向多个目的地进行分发。无需重复流,不会出现配置漂移,且每个目的地均不收取额外费用。每个流可用的目的地数量因套餐而异。了解更多。
概述
在 DeFi 中进行交易时,无论是代币兑换、质押等操作,您都需要向智能合约授权您的 ERC-20 代币,从而允许智能合约代表您消耗这些代币。 大多数情况下,智能合约会授权远超该笔交易所需的代币额度,以节省您未来的 gas 费用,但这可能会带来安全风险,因为您授权了大量代币。因此,让我们开发一个代币授权额度检查器应用,您或任何人都可以通过该应用查看特定钱包地址中各类代币的授权额度及其授权历史记录。
代币额度查询应用
步骤 1:创建 Quicknode 流
我们需要通过筛选各个区块的日志来创建配额事件;由于还需要获取历史配额数据,因此我们需要从创世区块中获取相关数据。
创建一个 Quicknode 账户 如果你还没有,请创建一个 流. 点击该链接后,包含 日志 数据集和过滤器代码将预先填入您的 Stream 配置中。
接下来,关于 流媒体开始 选择 区块 # 并输入区块 0 或 1,因为我们需要从创世区块开始对区块链进行索引,并保留 流结束 由于我们需要在最新区块数据生成时立即获取它,因此该字段为未检查字段。
过滤器代码将如下所示:
function extractAddress(paddedAddress) {
if (typeof paddedAddress !== 'string' || paddedAddress.length < 40) {
console.error('Invalid address format:', paddedAddress);
return null;
}
return '0x' + paddedAddress.slice(-40);
}
function hexToDecimal(hexValue) {
if (typeof hexValue !== 'string') {
console.error('Invalid hex value:', hexValue);
return '0';
}
// Remove '0x' prefix if present
hexValue = hexValue.replace(/^0x/, '');
// If the resulting string is empty, return '0'
if (hexValue === '') {
return '0';
}
// Parse the hex string to a BigInt (to handle large numbers)
try {
return BigInt('0x' + hexValue).toString();
} catch (error) {
console.error('Error converting hex to decimal:', 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 processing log entry:', 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;
}
在此代码片段中,我们正在筛选日志中与特定代币配额/授权事件相关的记录, 批准 (index_topic_1 地址所有者, index_topic_2 地址支出方, uint256 值) 我们通过在日志中查找具有特定特征的事件来实现这一点 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 ,这基本上就是该的编码/哈希格式: 批准 事件。除了对事件进行筛选外,我们还提高了地址和街区编号的可读性,并返回了大量有用的信息,例如 token, 钱包, 挥霍者, 金额, blockNumber,以及 交易哈希.
我们在将流数据发送至目标之前会对其进行过滤,因为我们只想要特定的数据,而无需处理不需要的数据。使用流服务时,您只需为实际流式传输的数据付费,因此可谓一举两得。
现在,请点击 下一页 然后选择 PostgreSQL 将目标类型设置为,并填写您的 Postgres 实例的配置信息。在 表格 在字段中填写表名,该表将由 Streams 创建,数据将流式传输到该表中。
步骤 2:创建一个 pgSQL 函数
首先,我们将创建一个包含以下列的新表: 钱包, 审批历史, 最新批准事项,以及 最后更新时间.
然后,我们将使用以下函数对 Streams 生成的表中的数据进行进一步处理
CREATE OR REPLACE FUNCTION process_wallet_approval_states_latest()
RETURNS void AS $$
DECLARE
-- make sure to add your table's name instead of 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 'Starting to process % rows', total_rows;
FOR row_data IN cur LOOP
BEGIN
IF jsonb_typeof(row_data.data) = 'array' THEN
allowance_events := row_data.data;
ELSE
-- If it's not an array, wrap it in an array
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 'Wallet address is null';
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'
);
-- Insert or update the wallet_approval_states table
-- make sure to add your table's name instead of 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 'Error processing event in row % (block %): %', processed_rows, row_data.block_number, SQLERRM;
END;
END LOOP;
processed_rows := processed_rows + 1;
IF processed_rows % 100 = 0 THEN
RAISE NOTICE 'Processed % out of % rows', processed_rows, total_rows;
END IF;
EXCEPTION
WHEN OTHERS THEN
skipped_rows := skipped_rows + 1;
RAISE WARNING 'Error processing row % (block %): %', processed_rows, row_data.block_number, SQLERRM;
END;
END LOOP;
RAISE NOTICE 'Finished processing % rows. Skipped % rows.', processed_rows, skipped_rows;
END;
$$ LANGUAGE plpgsql;
注:在上述函数中,由 Streams 创建的表是token_allowances_new,而另一个包含经过精炼数据的表是wallet_approval_states_latest。
第 3 步:React 应用和 Express 服务器
请在示例应用GitHub mono 仓库的此目录中查找 React 应用和 Express 服务器的代码。
这里是位于 src 目录、一个环境文件以及一个 server.js 用于 Express 服务器。
该 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('No approval data found for this wallet address.');
}
} catch (error) {
console.error('Error fetching data:', error);
setError('Error fetching data. Please try again. ' + (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(() => 'Unknown'),
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(`Error processing token ${approval.token}:`, error);
return {
...approval,
tokenName: 'Unknown',
tokenSymbol: 'UNK',
normalizedAmount: ethers.formatUnits(approval.amount, 18), // assume 18 decimals
};
}
}));
};
return (
<Container maxWidth="lg">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Wallet Approval Search
</Typography>
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<TextField
fullWidth
label="Wallet Address"
variant="outlined"
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
/>
<Button variant="contained" onClick={handleSearch} disabled={isLoading}>
{isLoading ? <CircularProgress size={24} /> : 'Search'}
</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>
Latest Approvals
</Typography>
{approvalData.some(approval => approval.tokenName === 'Unknown') && (
<Typography color="warning" sx={{ mb: 2 }}>
Warning: Some token information could not be retrieved. These tokens are marked as 'Unknown'.
</Typography>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Token Name</TableCell>
<TableCell>Token Symbol</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Spender</TableCell>
<TableCell>Block Number</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 }}>
View History
</Button>
)}
</>
)}
{!isLoading && approvalData !== null && approvalData.length === 0 && (
<Typography>No records found</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>Approval History</Typography>
{historyData && historyData.some(approval => approval.tokenName === 'Unknown') && (
<Typography color="warning" sx={{ mb: 2 }}>
Warning: Some historical token information could not be retrieved. These tokens are marked as 'Unknown'.
</Typography>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Token Name</TableCell>
<TableCell>Token Symbol</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Spender</TableCell>
<TableCell>Block Number</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 }}>
Close
</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是一个 Express 服务器,它连接到 PostgreSQL 实例,并在 3001 端口上提供服务。
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
}
});
// Test the database connection
pool.connect((err, client, release) => {
if (err) {
return console.error('Error acquiring client', err.stack);
}
client.query('SELECT NOW()', (err, result) => {
release();
if (err) {
return console.error('Error executing query', err.stack);
}
console.log('Connected to database successfully');
});
});
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', //replace wallet_approval_states with your table name
[walletAddress]
);
if (result.rows.length > 0) {
res.json(result.rows[0]);
} else {
res.status(404).json({ error: 'Wallet address not found' });
}
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
我们 ❤️ 您的反馈!
如果您有任何反馈或对新主题的建议,请告诉我们。我们非常期待您的来信。
