読了時間:24分
Quicknode Streamでは、1つのパイプラインから複数の宛先へ同時に配信できるようになりました。ストリームの重複がなく、設定のずれも発生せず、宛先ごとの追加料金もかかりません。ストリームごとに利用可能な宛先はプランによって異なります。詳細はこちら。
概要
DeFiでスワップやステーキングなどの取引を行う際、ERC-20トークンについてスマートコントラクトへの承認を行い、スマートコントラクトがユーザーに代わってトークンを使用できるようにします。 多くの場合、スマートコントラクトは将来のガス代を節約するために、その特定の取引に必要な量よりもはるかに多くのトークンを承認しますが、大量のトークンを承認することになるため、セキュリティ上のリスクとなる可能性があります。そこで、あなたや誰でも、特定のウォレットアドレスにおける各種トークンの承認限度額や承認履歴を確認できる「トークン承認限度額チェッカー」アプリを作成してみましょう。
トークン割り当てチェッカーアプリ
ステップ 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 , これは基本的に、 承認 イベント。イベントのフィルタリングに加え、住所やブロック番号の可読性を高め、次のような豊富な有用な情報を返すようにしています。 トークン, 財布, 浪費家, 金額, ブロック番号、および トランザクションハッシュ.
特定のデータのみが必要で、不要なデータを処理する必要がないため、ストリームデータを宛先に送信する前にフィルタリングを行います。ストリームでは、ストリーミングされたデータ分のみ課金されるため、まさに一石二鳥です。
それでは、[ここ]をクリックしてください。 次へ そして、[選択] をクリックします 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 サーバー
サンプルアプリのGitHubmonoリポジトリのこのディレクトリに、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は、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
}
});
// 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}`));
皆様からのフィードバックを心よりお待ちしております!❤️
ご意見や新しいトピックに関するご要望などがありましたら、ぜひお知らせください。皆様からのご連絡を心よりお待ちしております。
