Passer au contenu principal
Retour aux exemples d'applications

Outil de suivi des liquidations Aave V3

Suivez et analysez en temps réel les événements de liquidation sur le protocole Aave V3. Ce projet illustre la puissance du streaming de données blockchain sans serveur grâce à la solution Streams de Quicknode.

Framework/bibliothèque front-end :
Réagir
Langue :
TypeScript
Outil de compilation/serveur de développement :
Vite
Aperçu de l'application d'exemple

Introduction

Suivez et analysez en temps réel les événements de liquidation sur le protocole Aave V3. Ce projet illustre la puissance du streaming de données blockchain sans serveur grâce à la solution Streams de Quicknode.

Présentation du tableau de bordTableau des liquidations
Présentation du tableau de bordTableau des liquidations

Fonctionnalités


  • 📊 Suivi des liquidations en temps réel
  • 💰 Indicateurs et analyses détaillés
  • 📈 Évolution historique des liquidations
  • 🔄 Analyse de la répartition des actifs
  • ⚡ Architecture sans serveur

Contrat de pool Aave et événement « LiquidationCall »

Présentation du contrat de pool Aave V3

Le Pool.sol contrat (pour le réseau principal d'Ethereum, 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2) dans le protocole Aave V3 constitue le cœur du système de prêt et d'emprunt. Il permet d'effectuer des dépôts, des emprunts, des remboursements et des liquidations.

Événement à surveiller : LiquidationCall

L'événement « LiquidationCall » est déclenché lorsqu'une liquidation a lieu dans le protocole Aave. Il fournit des informations essentielles sur la transaction de liquidation, notamment les garanties saisies et la dette remboursée.


// Signature de l'événement
// 0xe413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286

Appel de liquidation(
adresse actif de garantie,
adresse debtAsset,
adresse user,
uint256 detteÀCouvrir,
uint256 montantDesGarantiesLiquidées,
adresse liquidateur,
bool receiveAToken
)

Paramètres de l'événement

ParamètreTypeDescription
actif de garantieadresseL'adresse du bien saisi à titre de garantie.
detteActifadresseL'adresse du bien que le liquidateur rembourse.
utilisateuradresseL'adresse de l'emprunteur faisant l'objet d'une liquidation.
dette à couvriruint256Le montant de la dette remboursée au cours de la liquidation.
montant de la garantie liquidéeuint256Le montant des garanties saisies par le liquidateur.
liquidateuradresseL'adresse du liquidateur chargé de la liquidation.
receiveATokenboolSi vrai, le liquidateur reçoit des aTokens à la place d'une garantie.

Nous utiliserons les journaux décodés du Appel de marge événement permettant de suivre les liquidations sur le réseau principal d'Ethereum.


Comment ça marche ?

Cette application utilise une architecture entièrement sans serveur pour suivre et analyser les événements de liquidation d'Aave V3 :


  1. Collecte de données: Streams surveille les données brutes issues de la blockchain relatives aux événements de liquidation d'Aave V3, tant en temps réel qu'historiquement.
  2. Décodage EVM: les flux décodent les données brutes des événements à l'aide du décodeur EVM afin d'en extraire les informations pertinentes
  3. Traitement des données: Utilisation de viem et les webhooks : nous surveillons temporairement les données entrantes et traitons les données d'événement décodées en récupérant les cours et les détails des jetons
  4. Stockage des données: les données d'événements enrichies sont automatiquement stockées dans la base de données PostgreSQL
  5. Affichage côté client: l'application React récupère et affiche les données avec des mises à jour en temps réel

Pile technologique

  • Interface utilisateur

    • Réagir
    • TypeScript
    • Requête TanStack pour la récupération de données
    • Tailwind CSS pour la mise en forme
    • shadcn/ui pour les composants d'interface utilisateur
    • Recharts pour la visualisation des données
  • Back-end

    • Quicknode Streams: données blockchain historiques et en temps réel
    • Serveur de webhooks permettant d'enrichir les données Stream avec les cours des jetons et les métadonnées
    • Supabase (PostgreSQL) pour le stockage des données

Architecture

Schéma architectural

L'architecture montre comment le système s'appuie sur l'infrastructure de Quicknode pour traiter efficacement les données de la blockchain :


  1. Quicknode Stream surveille les événements liés aux contrats Aave V3
  2. Les événements sont envoyés au serveur de webhooks pour y être traités
  3. Les données sont enrichies d'informations sur les jetons et les cours
  4. Les données traitées sont stockées dans Supabase
  5. Le front-end récupère et affiche des données en temps réel

Collecte et traitement des données

Flux Quicknode

Les flux Quicknode fournissent des données de blockchain en temps réel sans qu'il soit nécessaire d'exécuter un nœud :


  • Surveille les contrats Aave V3 afin de détecter les événements de liquidation
  • Filtre et affiche uniquement les événements pertinents
  • Garantit une transmission fiable des données grâce à des mécanismes de nouvelle tentative
  • Aucune maintenance de l'infrastructure n'est nécessaire

Serveur de webhooks

Nous utilisons un serveur de webhooks pour recevoir et valider les flux de données entrants :

De plus, nous enrichissons les données des flux en fournissant les informations suivantes :

  • Noms/symboles des jetons (WETH, USDC, etc.)
  • Cours en USD au moment de la liquidation
  • Montants formatés (conversion des nombres entiers longs en nombres décimaux)

Démarrer

Prérequis


  • Node.js 18 et versions ultérieures
  • npm ou yarn
  • Essai gratuit d'un compte Quicknode
  • Compte Supabase gratuit
  • Serveur de webhooks (ngrok ou un service similaire)

Installation


  1. Cloner le dépôt :
git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/sample-dapps/ethereum-aave-liquidation-tracker

  1. Installer les dépendances :
npm installer
  1. Créez un nouveau projet et une nouvelle base de données dans Supabase. Enregistrez le mot de passe de la base de données pour pouvoir l'utiliser ultérieurement.

  2. Créer un .env fichier et ajoutez votre Supabase URL et clé de publication. Vous pouvez récupérer l'URL et la clé depuis le tableau de bord Supabase en cliquant sur le Se connecter bouton dans le projet.

VITE_SUPABASE_URL=votre_url_supabase
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=votre_clé_de_publication_Supabase

  1. Configurez votre base de données à l'aide du schéma fourni :
CREATE TABLE liquidations (
id SERIAL PRIMARY KEY,
liquidator_address character varying(42) NOT NULL,
liquidated_wallet character varying(42) NOT NULL,
collateral_asset character varying(42) NOT NULL,
collateral_asset_name character varying(100),
collateral_asset_symbol character varying(20),
collateral_asset_price numeric(20,6),
collateral_seized_amount numeric(78,18),
debt_asset character varying(42) NOT NULL,
debt_asset_name character varying(100),
debt_asset_symbol character varying(20),
debt_asset_price numeric(20,6),
debt_repaid_amount numeric(78,18),
transaction_hash character varying(66) NOT NULL,
block_number bigint NOT NULL,
receive_a_token boolean NOT NULL,
timestamp timestamp without time zone NOT NULL
);

  1. Démarrez le serveur de développement :
npm exécuter en mode développement

Configuration du serveur Webhook


  1. Démarrez votre serveur de webhooks en exécutant la commande suivante : npm run server.
  2. Dans un autre terminal, exécutez la commande suivante : ngrok http VOTRE_NUMÉRO_DE_PORT. Pour ce projet, la valeur par défaut est définie sur 3005 mais n'hésitez pas à modifier cela dans .env.
  3. Une fois le serveur ngrok configuré, vous utiliserez le lien ngrok généré (par exemple, https://abcd.ngrok-free.app) et ajouter /webhook point de terminaison à partir duquel nous écoutons les données entrantes provenant de Quicknode Streams.

Configuration de Quicknode Streams


  1. Créez un compte Quicknode sur quicknode.com
  2. Configurer un nouveau flux :
    • Sélectionnez la chaîne et le réseau (par exemple, le réseau principal d'Ethereum)
    • Sélectionnez l'ensemble de données comme suit : Blocs avec reçus
    • Sélectionnez les numéros des blocs de début et de fin (pour les données en temps réel, conservez Fin du flux comme Ça ne s'arrête pas)
    • Sélectionnez la section « Modifier la charge utile » et ajoutez le code de filtrage des flux suivant :
    • Sélectionnez un bloc de test (par exemple, 18983377) puis cliquez sur Lancer le test
    • Sélectionnez la destination suivante : Webhook et ajoutez l'URL de votre webhook avec /webhook Point de terminaison ajouté.
    • Si vous le souhaitez, vous pouvez ajouter le jeton de sécurité de votre flux dans .env pour utiliser fonctionnalités de sécurité avancées.

Les adresses de contrat, les blocs de test et autres configurations peuvent varier en fonction du projet et du réseau sur lesquels vous travaillez. Dans cette application, nous utilisons le réseau principal d'Ethereum et le protocole Aave V3.

Vous pouvez obtenir le code complet de la fonction de filtrage des flux ci-dessous ou le consulter ici.

Afficher le code de filtrage des flux
// Filtering function on Streams
function main(stream) {
try {
// Aave V3 LiquidationCall ABI
const aaveLiquidationAbi = `[{
"anonymous": false,
"inputs": [
{"indexed": true, "type": "address", "name": "collateralAsset"},
{"indexed": true, "type": "address", "name": "debtAsset"},
{"indexed": true, "type": "address", "name": "user"},
{"indexed": false, "type": "uint256", "name": "debtToCover"},
{"indexed": false, "type": "uint256", "name": "liquidatedCollateralAmount"},
{"indexed": false, "type": "address", "name": "liquidator"},
{"indexed": false, "type": "bool", "name": "receiveAToken"}
],
"name": "LiquidationCall",
"type": "event"
}]`;

// Pool.sol addresses for Aave V3 (lowercase)
const normalizedAddresses = ["0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2"];

// Handle both payload and stream parameter styles
const data = stream?.data ? stream.data : stream;

// Early validation - return null to skip (per Quicknode docs)
if (!data?.[0]?.block?.timestamp || !data?.[0]?.receipts?.length) {
return null;
}

const timestamp = parseInt(data[0].block.timestamp, 16);

// Decode receipts
const decodedReceipts = decodeEVMReceipts(data[0].receipts, [aaveLiquidationAbi]);

// Validate decoded receipts
if (!decodedReceipts || !Array.isArray(decodedReceipts) || decodedReceipts.length === 0) {
return null;
}

// Filter for liquidation events
const filteredLogs = decodedReceipts
.filter(r => r?.decodedLogs?.length > 0)
.flatMap(r => r.decodedLogs)
.filter(log =>
log?.name === "LiquidationCall" &&
log?.address &&
normalizedAddresses.includes(log.address.toLowerCase())
);

// Return data if found, null otherwise (per Quicknode docs)
return filteredLogs?.length > 0
? { timestamp, filteredLogs }
: null;
} catch (e) {
// Return null to skip on error (per Quicknode docs)
return null;
}
}

Configuration de Quicknode

  1. Connectez-vous à votre tableau de bord Quicknode ou créez votre compte d'essai gratuit.
  2. Créez un nouveau point de terminaison pour votre chaîne et votre réseau (par exemple, le réseau principal d'Ethereum).
  3. Copiez l'URL RPC et collez-la dans le QUICKNODE_RPC_URL variable dans le .env fichier.

Configuration de la base de données

  1. Connectez-vous à votre tableau de bord Supabase.
  2. Accédez à votre projet, puis cliquez sur le bouton « Se connecter ».
  3. Dans la section « Informations de connexion », cliquez sur « Frameworks d'application », puis sélectionnez « React » dans le champ « Framework » et cochez la case « Utiliser le framework Vite ».
  4. Copier VITE_SUPABASE_URL et VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY to .env.

Configuration des fonctions PostgreSQL

Pour extraire les données de la base de données, vous pouvez utiliser les requêtes PostgreSQL suivantes afin de créer des fonctions. Ces fonctions sont utilisées dans l'interface utilisateur pour afficher les données.

Afficher le code des fonctions PostgreSQL
-- get_asset_distributions
CREATE OR REPLACE FUNCTION get_asset_distributions(time_range text)
RETURNS json AS $$
DECLARE
interval_start timestamp;
BEGIN
interval_start := CASE time_range
WHEN '24h' THEN NOW() - INTERVAL '24 hours'
WHEN '7d' THEN NOW() - INTERVAL '7 days'
WHEN '30d' THEN NOW() - INTERVAL '30 days'
WHEN '365d' THEN NOW() - INTERVAL '365 days'
ELSE NOW() - INTERVAL '30 days'
END;

RETURN json_build_object(
'topCollateralAssets', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
collateral_asset_symbol as symbol,
COUNT(*) as count,
SUM(collateral_seized_amount * collateral_asset_price) as totalValueUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalProfitUSD,
(COUNT(*)::float / (SELECT COUNT(*) FROM liquidations WHERE timestamp >= interval_start)::float * 100) as percentageOfTotal
FROM liquidations
WHERE timestamp >= interval_start
GROUP BY collateral_asset_symbol
ORDER BY count DESC
LIMIT 5
) t
),
'topDebtAssets', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
debt_asset_symbol as symbol,
COUNT(*) as count,
SUM(debt_repaid_amount * debt_asset_price) as totalValueUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalProfitUSD,
(COUNT(*)::float / (SELECT COUNT(*) FROM liquidations WHERE timestamp >= interval_start)::float * 100) as percentageOfTotal
FROM liquidations
WHERE timestamp >= interval_start
GROUP BY debt_asset_symbol
ORDER BY count DESC
LIMIT 5
) t
)
);
END;
$$ LANGUAGE plpgsql;
-- get_metrics_overview
CREATE OR REPLACE FUNCTION get_metrics_overview()
RETURNS json AS $$
BEGIN
RETURN json_build_object(
'total24h', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '24 hours'
),
'total7d', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '7 days'
),
'total30d', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '30 days'
),
'total365d', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '365 days'
),
'topLiquidators', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
liquidator_address as address,
COUNT(*) as count,
SUM(debt_repaid_amount * debt_asset_price) as totalValueUSD,
AVG(debt_repaid_amount * debt_asset_price) as avgLiquidationUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalProfitUSD,
AVG((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as avgProfitUSD
FROM liquidations
GROUP BY liquidator_address
ORDER BY totalProfitUSD DESC
LIMIT 5
) t
),
'topLiquidatedUsers', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
liquidated_wallet as address,
COUNT(*) as count,
SUM(debt_repaid_amount * debt_asset_price) as totalValueUSD,
AVG(debt_repaid_amount * debt_asset_price) as avgLiquidationUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalLossUSD,
AVG((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as avgLossUSD
FROM liquidations
GROUP BY liquidated_wallet
ORDER BY count DESC
LIMIT 5
) t
),
'largestLiquidations', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
transaction_hash as txHash,
timestamp,
debt_repaid_amount * debt_asset_price as valueUSD,
(collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price) as profitUSD,
collateral_asset_symbol as collateralAsset,
debt_asset_symbol as debtAsset,
liquidator_address as liquidator,
liquidated_wallet as liquidatedUser
FROM liquidations
ORDER BY debt_repaid_amount * debt_asset_price DESC
LIMIT 5
) t
),
'mostProfitableLiquidations', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
transaction_hash as txHash,
timestamp,
debt_repaid_amount * debt_asset_price as valueUSD,
(collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price) as profitUSD,
collateral_asset_symbol as collateralAsset,
debt_asset_symbol as debtAsset,
liquidator_address as liquidator,
liquidated_wallet as liquidatedUser
FROM liquidations
ORDER BY ((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) DESC
LIMIT 5
) t
)
);
END;
$$ LANGUAGE plpgsql;
-- get_liquidation_trends
CREATE OR REPLACE FUNCTION get_liquidation_trends(time_range text)
RETURNS json AS $$
DECLARE
interval_start timestamp;
grouping_interval text;
BEGIN
SELECT
CASE time_range
WHEN '24h' THEN NOW() - INTERVAL '24 hours'
WHEN '7d' THEN NOW() - INTERVAL '7 days'
WHEN '30d' THEN NOW() - INTERVAL '30 days'
WHEN '365d' THEN NOW() - INTERVAL '365 days'
ELSE NOW() - INTERVAL '30 days'
END,
CASE time_range
WHEN '24h' THEN 'hour'
WHEN '7d' THEN 'day'
WHEN '30d' THEN 'day'
WHEN '365d' THEN 'month'
ELSE 'day'
END
INTO interval_start, grouping_interval;

RETURN (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
date_trunc(grouping_interval, timestamp) as date,
COUNT(*) as count,
COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0) as totalValueUSD,
COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0) as totalProfitUSD
FROM liquidations
WHERE timestamp >= interval_start
GROUP BY date_trunc(grouping_interval, timestamp)
ORDER BY date_trunc(grouping_interval, timestamp)
) t
);
END;
$$ LANGUAGE plpgsql;

Configuration des index PostgreSQL

Pour améliorer les performances des requêtes, nous pouvons créer des index sur la liquidations table. Voici un exemple illustrant comment créer les index nécessaires :

CRÉER INDEX idx_debt_asset SUR liquidations(symbole_dette_actif opérations_textuelles);
CREATE INDEX idx_timestamp ON liquidations(timestamp timestamp_ops);
CREATE INDEX idx_liquidator_address ON liquidations(adresse_du_liquidateur opérations_textuelles);
CREATE INDEX idx_liquidated_wallet ON liquidations(portefeuille_liquidé opérations_texte);
CREATE INDEX idx_collateral_asset ON liquidations(symbole_de_l'actif_de_garantie opérations_textuelles);
CREATE INDEX idx_liquidations_assets ON liquidations(symbole_actif_garantie text_ops,symbole_de_l'actif_de_dette text_ops);

Développement

Structure du projet

src/
├── components/ # Composants d'interface utilisateur réutilisables
├── hooks/ # Hooks React personnalisés
├── lib/
│ ├── security.ts # Validation des données entrantes de Streams
│ ├── server.ts # Notre serveur de webhooks
│ ├── supabase-backend.ts # Clés Supabase pour les opérations backend
│ ├── supabase-frontend.ts # Clés Supabase pour les opérations front-end
│ ├── transform.ts # Transformation des données Streams entrantes
│ └── utils.ts # Fonctions utilitaires générales
└── types/ # Définitions de types TypeScript

Scripts disponibles


  • npm run dev: Démarrer le serveur de développement
  • npm run build: Compilation pour la production
  • npm run server: Démarrer le serveur de webhooks
  • npm run lint: Lancer ESLint

Fonctionnalités futures possibles

Fonctionnalités avancées


  • Suivi des portefeuilles « Whale »
  • Seuils d'alerte personnalisés
  • Évaluation des risques du portefeuille
  • Intégration avec d'autres protocoles DeFi
  • Notifications en temps réel pour :
    • Liquidations de grande envergure
    • Actifs spécifiques
    • Alertes de seuil de prix

Améliorations techniques


  • Prise en charge de WebSocket pour les mises à jour en temps réel
  • Prise en charge de plusieurs réseaux (Polygon, Arbitrum, etc.)
  • Options avancées de visualisation des données
  • Version pour application mobile
  • Accès à l'API pour les développeurs

Contributions et commentaires
Nous serions ravis de connaître votre avis et nous vous invitons à contribuer à cette application d'exemple !
Pour signaler des problèmes ou faire part de vos commentaires, ouvrez un ticket GitHub dans le qn-guide-exemples référentiel.
Pour contribuer, procédez comme suit :
  1. Créer une branche du dépôt
  2. Créer une branche de fonctionnalité :
    git checkout -b feature/amazing-feature
  3. Validez vos modifications :
    git commit -m "Ajout d'une fonctionnalité géniale"
  4. Publiez votre branche :
    git push origin feature/amazing-feature
  5. Créez une Pull Request.