24 minutos de lectura
Tu Quicknode Stream ya puede enviar datos a varios destinos simultáneamente desde un único canal. Sin flujos duplicados, sin desviaciones en la configuración y sin costes adicionales por destino. Los destinos disponibles por flujo varían según el plan. Más información.
Resumen
Al realizar transacciones en DeFi, ya sea mediante intercambio, staking, etc., autorizas el uso de tus tokens ERC-20 para un contrato inteligente, lo que permite que este gaste tokens en tu nombre. La mayoría de las veces, los contratos inteligentes autorizan muchos más tokens de los necesarios para esa transacción concreta con el fin de ahorrarte futuras comisiones de gas, pero esto puede suponer un riesgo de seguridad, ya que estás autorizando un gran número de tokens. Por eso, vamos a crear una aplicación de comprobación de límites de autorización de tokens en la que tú o cualquier otra persona podáis consultar los límites de autorización de una dirección de monedero determinada para diversos tokens, junto con el historial de tus autorizaciones.
Aplicación para comprobar el saldo de tokens
Paso 1: Creación de un flujo de Quicknode
Tendremos que crear eventos de asignación de índices filtrando los registros de varios bloques; ahora bien, dado que también necesitamos obtener las asignaciones históricas, tendremos que recuperar los datos desde el bloque Génesis.
Crear un Cuenta de Quicknode si aún no tienes una, crea una Transmisión. Una vez que hagas clic en el enlace, el campo del conjunto de datos con Registros El conjunto de datos y el código del filtro aparecerán ya rellenados en la configuración de tu Stream.
Ahora, en cuanto a Inicio de la retransmisión seleccionar Bloque n.º e introduce el bloque 0 o 1, ya que tendremos que indexar la cadena desde el bloque génesis, y mantén el Fin de la transmisión sin marcar, ya que necesitaremos los datos del último bloque a medida que se vayan creando.
Así es como quedará el código del filtro:
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;
}
En este fragmento de código, estamos filtrando los registros en busca de un evento concreto relacionado con la asignación o aprobación de un token, Aprobación (propietario de la dirección index_topic_1, usuario de la dirección index_topic_2, valor uint256) y lo estamos haciendo buscando eventos en los registros que coincidan con la firma 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 , que es básicamente el formato codificado o con hash del Aprobación evento. Además de filtrar por evento, también estamos mejorando la legibilidad de las direcciones y los números de manzana, y ofrecemos una gran cantidad de información útil, como ficha, monedero, derrochador, cantidad, número de bloque, y transactionHash.
Filtramos los datos de Stream antes de enviarlos a su destino porque solo queremos datos específicos y no necesitamos ocuparnos de los datos no deseados. Con Streams, solo pagas por los datos que se transmiten, así que es todo ventajas.
Ahora, haz clic en Siguiente y selecciona PostgreSQL como tipo de destino e introduce los datos de configuración de tu instancia de Postgres. En el Tabla Escribe el nombre de una tabla en este campo; Streams creará dicha tabla y los datos se transferirán a ella.
Paso 2: Crear una función de PostgreSQL
En primer lugar, crearemos una nueva tabla con columnas monedero, historial_de_aprobaciones, últimas_aprobaciones, y última_actualización.
A continuación, utilizaremos la siguiente función para refinar aún más los datos de la tabla creada por 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;
Nota: En la función anterior, la tabla creada por Streams es «token_allowances_new » y otra tabla con datos más detallados es «wallet_approval_states_latest».
Paso 3: Aplicación React y servidor Express
Encontrarás el código de la aplicación React y del servidor Express en este directorio del repositorio único de GitHub de aplicaciones de ejemplo.
Aquí tenemos la aplicación de React en el src directorio, un archivo de configuración y un server.js para el servidor Express.
La aplicación React cuenta con un componente llamado WalletApprovalSearch.js, que recoge la dirección del monedero del usuario, obtiene los datos de aprobación del servidor Express, recupera los datos del token de sus contratos inteligentes mediante Quicknode RPC y, a continuación, los muestra.
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;
El archivo de variables de entorno contiene la URL de Quicknode y la configuración de la instancia de PostgreSQL.
REACT_APP_QUICKNODE_ENDPOINT=
# Configuración de la base de datos
DB_USER=
DB_HOST=
DB_NAME=
DB_PASSWORD=
DB_PORT=
El archivo server.js es un servidor Express que se conecta a la instancia de PostgreSQL y funciona en el puerto 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}`));
¡Nos encanta recibir comentarios!
Si tienes algún comentario o sugerencia sobre nuevos temas, no dudes en hacérnoslo saber. Nos encantaría saber tu opinión.
