如何创建一个代币授权检查器应用程序

  • QuickNode
  • 发布于 2024-06-25 11:32
  • 阅读 25

本文介绍了如何创建一个代币授权检查器应用程序,以便用户可以检查他们的 ERC-20 代币的授权情况和历史记录。文章通过分步指导,详细阐述了使用 QuickNode 创建流、PostgreSQL 数据库设计,以及使用 React 和 Express 搭建前后端,结合示例代码提供了完整的实现思路。

概述

在 DeFi 中进行交易时,无论是交换、质押等,你会授权你的 ERC-20 代币给智能合约,从而允许智能合约为你支出代币。大多数情况下,智能合约批准的代币数量比该特定交易所需的要多,以节省未来的 gas 费用,但这可能会带来安全风险,因为你授权了大量的代币。因此,让我们创建一个代币允许/批准检查应用程序,你或者任何人都可以检查给定钱包地址的各种代币的 token allowances 以及你的批准历史。

如何创建代币批准检查应用程序 - YouTube

QuickNode

131K 订阅者

如何创建代币批准检查应用程序

QuickNode

搜索

信息

购物

点击即可静音

如果播放没有开始,尝试重启你的设备。

你已注销

你观看的视频可能会被添加到电视观看历史中并影响电视推荐。为避免这种情况,请在计算机上取消并登录 YouTube。

取消确认

分享

包含播放列表

检索共享信息时发生错误。请稍后再试。

稍后观看

分享

复制链接

观看

0:00

/ •实时

在 YouTube 上观看

订阅我们的 YouTube 频道以获取更多视频! 订阅

代币允许检查应用程序

第一步:创建 QuickNode Stream

我们需要通过过滤各个区块的日志来创建索引允许事件;现在,由于我们也需要获取历史允许,我们需要从创世区块获取数据。

如果你还没有,请创建一个 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 事件的编码/哈希格式。除了过滤事件外,我们还使地址和区块号更易于阅读,并返回一系列有用的信息,如 tokenwalletspenderamountblockNumbertransactionHash

我们在将 Stream 数据发送到目标之前对其进行过滤,因为我们只想要特定的数据,而不需要处理不需要的数据。使用 Streams,你只为流式传输的数据付费,因此是双赢。

现在,点击 下一步 并选择 PostgreSQL 作为目标类型,并填写你的 Postgres 实例的配置详细信息。在 Table 字段中写入表名,此表将由 Streams 创建,数据将流式传输到该表。

第二步:创建 pgSQL 函数

我们将首先创建一个新的表,包含 walletapproval_historylatest_approvalslast_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 服务器

在这个 示例应用程序的目录 中找到 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。