본문으로 건너뛰기
샘플 앱으로 돌아가기

Aave V3 청산 추적기

Aave V3 프로토콜에서 발생하는 청산 이벤트를 실시간으로 모니터링하고 분석합니다. 이 프로젝트는 Quicknode Streams 활용한 서버리스 블록체인 데이터 스트리밍의 강력한 기능을 보여줍니다.

프론트엔드 프레임워크/라이브러리:
React
언어:
TypeScript
빌드 도구/개발 서버:
Vite
샘플 앱 미리 보기

서론

Aave V3 프로토콜에서 발생하는 청산 이벤트를 실시간으로 모니터링하고 분석합니다. 이 프로젝트는 QuicknodeStreams를 활용한 서버리스 블록체인 데이터 스트리밍의 강력한 성능을 보여줍니다.

대시보드 개요청산표
대시보드 개요청산표

주요 기능


  • 📊 실시간 청산 모니터링
  • 💰 상세한 지표 및 분석
  • 📈 과거 청산 추세
  • 🔄 자산 배분 분석
  • ⚡ 서버리스 아키텍처

Aave 풀 계약 및 청산 요청 이벤트

Aave V3 풀 계약 개요

Pool.sol 계약 ( Ethereum Mainnet용, 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2) Aave V3 프로토콜 내의 이 기능은 대출 및 차입 시스템의 핵심입니다. 이 기능을 통해 예치, 차입, 상환 및 청산이 이루어집니다.

주목할 이벤트: LiquidationCall

LiquidationCall 이벤트는 Aave 프로토콜에서 청산이 발생할 때 발생합니다. 이 이벤트는 압류된 담보와 상환된 부채를 포함하여 청산 거래에 대한 중요한 정보를 제공합니다.


// 이벤트 시그니처
// 0xe413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286

청산 요청(
주소 담보자산,
주소 debtAsset,
주소 user,
uint256 상환해야 할 부채,
uint256 청산된담보금액,
address 청산인,
bool receiveAToken
)

이벤트 매개변수

매개변수유형설명
담보자산주소담보로 압류되는 자산의 주소.
부채자산주소청산인이 상환하고 있는 자산의 주소.
사용자주소청산 절차가 진행 중인 차주의 주소.
상환해야 할 부채uint256청산 과정에서 상환되는 채무의 금액.
청산된 담보 금액uint256청산인이 압류한 담보물의 금액.
청산인주소청산을 수행하고 있는 청산인의 주소.
receiveATokenbool만약 true, 청산인은 담보 대신 aTokens를 받습니다.

저희는 다음의 디코딩된 로그를 사용할 예정입니다. 청산 요청 Mainnet에서 청산 내역을 추적하기 위한 이벤트.


작동 원리

이 애플리케이션은 Aave V3 청산 이벤트를 추적하고 분석하기 위해 완전한 서버리스 아키텍처를 사용합니다:


  1. 데이터 수집: Streams 는 Aave V3의 청산 이벤트에 대한 원시 블록체인 이벤트 데이터를 과거 데이터와 실시간 데이터 모두에서 모니터링합니다.
  2. EVM 디코딩: StreamsEVM 디코더를 사용하여 원시 이벤트 데이터를 디코딩하고 관련 정보를 추출합니다
  3. 데이터 처리: 사용법 viem 그리고 webhooks 통해, 들어오는 데이터를 일시적으로 수신하고, 토큰 가격 및 토큰 세부 정보를 조회하여 디코딩된 이벤트 데이터를 처리합니다.
  4. 데이터 저장: 보강된 이벤트 데이터는 PostgreSQL 데이터베이스에 자동으로 저장됩니다.
  5. 프론트엔드 표시: React 애플리케이션이 데이터를 가져와 실시간으로 업데이트하며 표시합니다.

기술 스택

  • 프론트엔드

    • React
    • TypeScript
    • 데이터 가져오기를 위한 TanStack 쿼리
    • 스타일링을 위한 Tailwind CSS
    • UI 구성 요소를 위한 shadcn/ui
    • 데이터 시각화를 위한 Recharts
  • 백엔드

    • Quicknode Streams 과거 및 실시간 블록체인 데이터를 위한
    • 스트림 데이터에 토큰 가격 및 메타데이터를 추가하기 위한 웹훅 서버
    • 데이터 저장을 위한 Supabase (PostgreSQL)

건축

건축 도면

이 아키텍처는 시스템이 Quicknode 인프라를 활용하여 블록체인 데이터를 효율적으로 처리하는 방식을 보여줍니다:


  1. Quicknode Aave V3 계약 이벤트를 모니터링합니다.
  2. 이벤트는 처리를 위해 웹훅 서버로 전송됩니다
  3. 데이터에는 토큰 및 가격 정보가 추가됩니다.
  4. 처리된 데이터는 Supabase에 저장됩니다.
  5. 프론트엔드는 실시간 데이터를 가져와 표시합니다

데이터 수집 및 처리

Quicknode Streams

Quicknode Streams 노드를 실행하지 않고도 실시간 블록체인 데이터를Streams :


  • Aave V3 계약에서 청산 발생 여부를 모니터링합니다
  • 관련 있는 이벤트만 필터링하여 전달합니다
  • 재시도 메커니즘을 통해 안정적인 데이터 전달을 보장합니다
  • 인프라 유지 관리가 전혀 필요하지 않습니다

웹훅 서버

저희는 웹훅 서버를 사용하여 수신되는 streams 수신하고 유효성을 확인하고 있습니다:

또한, 당사는 다음과 같은 데이터 항목을 제공하여 streams 보강합니다:

  • 토큰 이름/기호 (WETH, USDC 등)
  • 청산 시점의 USD 가격
  • 서식 지정된 수치 (bigint를 소수형으로 변환)

시작하기

선행 조건


  • Node.js 18 이상
  • npm 또는 yarn
  • 무료 체험 Quicknode 계정
  • 무료 Supabase 계정
  • 웹훅 서버 (ngrok 또는 이와 유사한 서비스)

설치


  1. 저장소를 복제하세요:
git https://github.com/quiknode-labs/qn-guide-examples.git 복제
cdethereum

  1. 의존성 설치:
npm 설치
  1. Supabase에서 새 프로젝트와 데이터베이스를 생성합니다. 나중에 사용할 수 있도록 데이터베이스 비밀번호를 저장해 둡니다.

  2. 만들기 .env 파일을 열고 다음 내용을 추가하세요. Supabase URL 및 게시용 키. Supabase 대시보드에서 다음을 클릭하면 URL과 키를 확인할 수 있습니다. 연결 프로젝트 내의 버튼.

VITE_SUPABASE_URL=사용자의_Supabase_URL
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=사용자의_Supabase_게시 가능 키

  1. 제공된 스키마를 사용하여 데이터베이스를 설정하십시오:
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. 개발 서버를 시작합니다:
npm run dev

웹훅 서버 설정


  1. 다음 명령을 실행하여 웹훅 서버를 시작하세요. npm run server.
  2. 별도의 터미널에서 다음 명령을 실행하십시오. ngrok http YOUR_PORT_NUMBER. 이 프로젝트의 경우 기본값은 다음과 같이 설정되어 있습니다. 3005 하지만 원하시면 이 부분을 자유롭게 수정하셔도 됩니다. .env.
  3. ngrok 서버가 설정되면, 생성된 ngrok 링크(예: https://abcd.ngrok-free.app)를 추가하고 /웹훅 Streams Streams에서 들어오는 데이터를 수신 대기하는 endpoint .

Quicknode Streams 설정하기


  1. quicknode.com에서 Quicknode 만드세요
  2. 새 스트림 설정하기:
    • 체인 및 네트워크(예: Ethereum Mainnet)를 선택하세요.
    • 데이터셋을 다음과 같이 선택하십시오. 영수증이 첨부된 블록
    • 시작 및 종료 블록 번호를 선택하십시오(실시간 데이터의 경우, 스트림 종료 ~처럼 끝나지 않는다)
    • '페이로드 수정' 섹션을 선택하고 다음 Streams 코드를 추가하십시오.
    • 테스트 블록을 선택하십시오(예: 18983377)을 클릭하고 테스트 실행
    • 목적지를 다음과 같이 선택하십시오. 웹훅 그리고 다음을 사용하여 웹훅 URL을 추가하세요. /웹훅 endpoint .
    • 선택 사항으로, 다음 위치에 스트림의 보안 토큰을 추가할 수 있습니다. .env ~을 활용하다 고급 보안 기능.

계약 주소, 테스트 블록 및 기타 구성 사항은 진행 중인 구체적인 프로젝트와 네트워크에 따라 달라질 수 있습니다. 이 앱에서는Mainnet Aave V3 프로토콜을 사용합니다.

아래에서 Streams 함수의 전체 코드를 확인하거나, 여기에서 볼 수 있습니다.

Streams 코드 보기
// 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;
}
}

Quicknode

  1. Quicknode 로그인하거나 무료 체험 계정을 생성하세요.
  2. 체인 및 네트워크(예: Ethereum Mainnet) Endpoint 새로운 Endpoint 생성하세요.
  3. RPC URL을 복사하여 다음 위치에 붙여넣으십시오. QUICKNODE 의 변수 .env 파일.

데이터베이스 설정

  1. Supabase 대시보드에 로그인하세요.
  2. 프로젝트로 이동한 후 ‘연결’ 버튼을 클릭하세요.
  3. ‘연결 정보’에서 ‘앱 프레임워크’를 클릭한 다음, ‘프레임워크’를 ‘React ’로 설정하고 ‘Vite 프레임워크 사용’을 선택합니다.
  4. 복사 VITE_SUPABASE_URL 그리고 VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY ~에 .env.

PostgreSQL 함수 설정하기

데이터베이스에서 데이터를 가져오려면 다음 PostgreSQL 쿼리를 사용하여 몇 가지 함수를 생성할 수 있습니다. 이 함수들은 프론트엔드에서 데이터를 표시하는 데 사용됩니다.

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;

PostgreSQL 인덱스 설정

쿼리 성능을 높이기 위해, 다음 테이블에 인덱스를 생성할 수 있습니다. 청산 테이블. 다음은 필요한 인덱스를 생성하는 방법의 예시입니다:

생성 INDEX 생성 idx_debt_asset ON 청산(부채_자산_심볼 텍스트_연산);
CREATE INDEX idx_timestamp ON liquidations(타임스탬프 timestamp_ops);
CREATE INDEX idx_liquidator_address ON liquidations(청산자 주소 텍스트_작업);
CREATE INDEX idx_청산된_지갑 ON liquidations(청산된_지갑 text_ops);
CREATE INDEX idx_collateral_asset ON 청산(담보_자산_심볼 text_ops);
CREATE INDEX idx_liquidations_assets ON liquidations(담보_자산_심볼 text_ops,부채_자산_심볼 text_ops);

개발

프로젝트 구조

src/
├── components/ # 재사용 가능한 UI 컴포넌트
├── hooks/ # 사용자 정의 React 훅
├── lib/
│ ├── security.ts # 수신되는 Streams 유효성 검사
│ ├── server.ts # 웹훅 서버
│ ├── supabase-backend.ts # 백엔드 작업을 위한 Supabase 키
│ ├── supabase-frontend.ts # 프론트엔드 작업을 위한 Supabase 키
│ ├── transform.ts # 수신되는 Streams 의 변환
│ └── utils.ts # 일반 유틸리티 함수
└── types/ # TypeScript 타입 정의

사용 가능한 스크립트


  • npm run dev: 개발 서버 시작
  • npm run build: 프로덕션용 빌드
  • npm run server: 웹훅 서버 시작
  • npm run lint: ESLint 실행

향후 추가될 수 있는 기능

고급 기능


  • 고래 지갑 추적
  • 사용자 지정 경고 임계값
  • 포트폴리오 위험 평가
  • 다른 DeFi 프로토콜과의 통합
  • 다음 항목에 대한 실시간 알림:
    • 대규모 청산
    • 특정 자산
    • 가격 임계값 알림

기술적 개선 사항


  • 실시간 업데이트를 위한 WebSocket 지원
  • 다중 네트워크 지원 (Polygon, Arbitrum 등)
  • 고급 데이터 시각화 옵션
  • 모바일 앱 버전
  • 개발자를 위한 API 이용

기여 및 피드백
여러분의 의견을 듣고 싶으며, 이 샘플 앱에 대한 모든 기여를 환영합니다!
문제를 신고하거나 피드백을 공유하려면, 다음에서 GitHub 이슈를 생성해 주세요. qn-가이드-예시 저장소.
기여하시려면 다음 단계를 따르세요:
  1. 저장소를 포크하기
  2. 기능 브랜치 생성:
    git checkout -b feature/amazing-feature
  3. 변경 사항을 커밋하세요:
    git commit -m "멋진 기능 추가"
  4. 브랜치를 푸시하세요:
    git push origin feature/amazing-feature
  5. 풀 리퀘스트를 제출하세요.