블록체인 개발에서 스마트 컨트랙트의 상태 변경을 실시간으로 감지하는 것은 핵심 기술입니다. 이번 튜토리얼에서는 Ethereum Event Logs를 가져오는 방법을 초보자도 이해할 수 있도록 단계별로 설명드리겠습니다. 또한 HolySheep AI의 AI API를 활용하여 이벤트 로그를 지능적으로 분석하는 방법도 함께 다룹니다.
Ethereum Event Logs란 무엇인가?
Ethereum Event Logs는 스마트 컨트랙트가 실행 중 발생한 사건들을 기록한 데이터입니다. 마치 앱에서 로그를 출력하는 것처럼, 컨트랙트에서도emit 키워드로 특정 사건을 기록합니다.
실생활 비유: 은행 앱에서 "100만원 입금됨" 알림을 받는 것과 같습니다. Ethereum에서는 컨트랙트의 이벤트(emitted events)가 이 알림 역할을 합니다.
Event Logs가 중요한 이유
- 실시간 감지: 블록 확인 없이 트랜잭션 상태 변경을 즉시 알 수 있습니다
- 비용 절감: 컨트랙트 storage를 읽지 않고도 필요한 정보를 얻을 수 있습니다
- 감사 추적: 모든 활동을 투명하게 기록하고 검증할 수 있습니다
준비물: 개발 환경 설정
1. 필요한 도구 설치
Node.js가 없다면 먼저 설치해주세요. 터미널에서 다음 명령어를 실행합니다:
node --version
npm --version
버전 번호가 보인다면 정상적으로 설치된 것입니다. 이제 프로젝트 폴더를 만들고 필요한 패키지를 설치합니다:
mkdir ethereum-events-tutorial
cd ethereum-events-tutorial
npm init -y
npm install ethers@6 axios
2. Ethereum 노드 연결 준비
Ethereum 이벤트 로그를 가져하려면 RPC 노드에 연결해야 합니다. 대표적인 옵션:
- Infura: https://www.infura.io - 무료 티어 제공
- Alchemy: https://www.alchemy.com - 무료 티어 제공
- QuickNode: https://www.quicknode.com
저는 테스트넷에서 먼저 연습할 것을 권장합니다. Sepolia 테스트넷은 실제 환경과 동일하게 동작하면서 무료로 테스트할 수 있습니다.
기본: ethers.js로 Event Logs 가져오기
첫 번째 예제: ERC-20 토큰 전송 이벤트监听
가장 흔한用例인 Transfer 이벤트를取得해 보겠습니다. ERC-20 토큰이 이동할 때마다 이 이벤트가 발생합니다.
const { ethers } = require('ethers');
// RPC URL 설정 - Infura 또는 Alchemy에서 받은 URL 사용
const RPC_URL = "https://sepolia.infura.io/v3/YOUR_INFURA_KEY";
const provider = new ethers.JsonRpcProvider(RPC_URL);
// 토큰 컨트랙트 주소 (예: Sepolia의 USDT)
const TOKEN_ADDRESS = "0x1a1A1b1B2b2b3b4C4C4D4D5E5F6F7G8G9H0H1";
// Transfer 이벤트 시그니처 (keccak256 해시)
// Transfer(address,address,uint256) = 0xddf252ad...
const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df35b9dc";
async function listenToTransferEvents() {
console.log("🚀 ERC-20 Transfer 이벤트 리스닝 시작...");
// 최신 블록에서부터监听 시작
const latestBlock = await provider.getBlockNumber();
console.log(현재 블록 번호: ${latestBlock});
// getLogs로 과거 이벤트 가져오기
const logs = await provider.getLogs({
address: TOKEN_ADDRESS,
topics: [TRANSFER_TOPIC],
fromBlock: latestBlock - 1000, // 최근 1000블록 범위
toBlock: latestBlock
});
console.log(\n📊 발견된 Transfer 이벤트: ${logs.length}개\n);
// 각 로그 분석
for (let i = 0; i < Math.min(logs.length, 5); i++) {
const log = logs[i];
console.log(--- 이벤트 #${i + 1} ---);
console.log(블록: ${log.blockNumber});
console.log(트랜잭션: ${log.transactionHash});
console.log(로그 인덱스: ${log.logIndex});
// topics에서 주소 추출 (from, to)
if (log.topics.length >= 3) {
const fromAddress = ethers.getAddress(
"0x" + log.topics[1].slice(26)
);
const toAddress = ethers.getAddress(
"0x" + log.topics[2].slice(26)
);
console.log(From: ${fromAddress});
console.log(To: ${toAddress});
}
console.log();
}
}
listenToTransferEvents().catch(console.error);
실행 결과 예시:
🚀 ERC-20 Transfer 이벤트 리스닝 시작...
현재 블록 번호: 5847654
📊 발견된 Transfer 이벤트: 3개
--- 이벤트 #1 ---
블록: 5847650
트랜잭션: 0xabc123...def456
로그 인덱스: 0
From: 0x742d35Cc6634C0532925a3b8D4C9f6F2b9c1d2e3
To: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1709
이벤트 데이터 디코딩 방법
이제 Raw 로그를 사람이 읽을 수 있는 형태로 변환해 보겠습니다:
const { ethers } = require('ethers');
// ERC-20 ABI 중 Transfer 이벤트 정의
const ERC20_ABI = [
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
const RPC_URL = "https://sepolia.infura.io/v3/YOUR_INFURA_KEY";
const provider = new ethers.JsonRpcProvider(RPC_URL);
const TOKEN_ADDRESS = "0x1a1A1b1B2b2b3b4C4C4D4D5E5F6F7G8G9H0H1";
// 컨트랙트 인스턴스 생성
const contract = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, provider);
async function decodeEvents() {
const latestBlock = await provider.getBlockNumber();
// queryFilter로 자동으로 디코딩된 이벤트 가져오기
const events = await contract.queryFilter(
"Transfer",
latestBlock - 500,
latestBlock
);
console.log(✅ 총 ${events.length}개의 Transfer 이벤트 발견\n);
for (const event of events.slice(0, 3)) {
console.log("═══════════════════════════════");
console.log(블록: ${event.blockNumber});
console.log(보낸 사람: ${event.args.from});
console.log(받는 사람: ${event.args.to});
console.log(금액: ${ethers.formatEther(event.args.value)} tokens);
console.log(트랜잭션 해시: ${event.transactionHash});
}
}
decodeEvents().catch(console.error);
출력 예시:
✅ 총 3개의 Transfer 이벤트 발견
═══════════════════════════════
블록: 5847650
보내는 사람: 0x742d35Cc6634C0532925a3b8D4C9f6F2b9c1d2e3
받는 사람: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1709
금액: 100.5 tokens
트랜잭션 해시: 0xabc123...def456
보시는 것처럼 queryFilter를 사용하면 ABI 기반으로 자동으로 데이터를 파싱해주므로 훨씬 편리합니다.
실전 활용: HolySheep AI로 이벤트 로그 분석하기
이제 HolySheep AI의 AI API를 활용하여 대량의 이벤트 로그를 지능적으로 분석하는 고급用例를 알아보겠습니다. HolySheep AI는 단일 API 키로 GPT-4.1, Claude, Gemini 등 모든 주요 모델을 통합하여 사용할 수 있습니다.
예제: 의심스러운 거래 패턴 탐지
const axios = require('axios');
const { ethers } = require('ethers');
// ============================================
// HolySheep AI API 설정
// https://www.holysheep.ai/register 에서 무료 가입
// ============================================
const HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY";
const HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1";
// Ethereum 설정
const RPC_URL = "https://sepolia.infura.io/v3/YOUR_INFURA_KEY";
const provider = new ethers.JsonRpcProvider(RPC_URL);
const SUSPICIOUS_CONTRACT = "0x742d35Cc6634C0532925a3b8D4C9f6F2b9c1d2e3";
const ERC20_ABI = [
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
const contract = new ethers.Contract(SUSPICIOUS_CONTRACT, ERC20_ABI, provider);
// HolySheep AI로 이벤트 분석
async function analyzeEventsWithAI(events) {
// 이벤트 데이터를 텍스트로 변환
const eventsText = events.map((e, i) =>
${i + 1}. From: ${e.args.from}, To: ${e.args.to}, Amount: ${ethers.formatEther(e.args.value)} ETH, TX: ${e.transactionHash.slice(0, 10)}...
).join('\n');
const prompt = `다음 Ethereum 토큰 전송 이벤트를 분석하여 의심스러운 패턴이 있는지 검토해주세요:
${eventsText}
다음 사항을 확인해주세요:
1. 비정상적으로 큰 금액의 거래
2. 짧은 시간 내 반복 거래 (스냅샷 방지)
3. 특정 주소로만 집중되는 거래
4. 분석 결과를 한국어로 설명`;
try {
const response = await axios.post(
${HOLYSHEEP_BASE_URL}/chat/completions,
{
model: "gpt-4.1", // HolySheep에서 최적의 모델 선택
messages: [
{
role: "system",
content: "당신은 블록체인 보안 전문가입니다. 트랜잭션 로그를 분석하여 의심스러운 활동을 탐지합니다."
},
{
role: "user",
content: prompt
}
],
temperature: 0.3
},
{
headers: {
"Authorization": Bearer ${HOLYSHEEP_API_KEY},
"Content-Type": "application/json"
}
}
);
return response.data.choices[0].message.content;
} catch (error) {
console.error("HolySheep AI API 오류:", error.response?.data || error.message);
throw error;
}
}
async function main() {
console.log("🔍 의심스러운 컨트랙트 이벤트 분석 시작...\n");
const latestBlock = await provider.getBlockNumber();
// 최근 100블록 범위에서 Transfer 이벤트 수집
const events = await contract.queryFilter(
"Transfer",
latestBlock - 100,
latestBlock
);
console.log(📊 수집된 이벤트: ${events.length}개\n);
if (events.length > 0) {
// HolySheep AI로 분석
const analysis = await analyzeEventsWithAI(events);
console.log("🤖 AI 분석 결과:\n");
console.log(analysis);
}
}
main().catch(console.error);
HolySheep AI 분석 결과 예시:
🤖 AI 분석 결과:
이벤트 보안 분석 보고서
발견된 의심스러운 패턴
1. **비정상적 대금 거래** ⚠️
- 이벤트 #3에서 총 15,000 토큰 이동 확인
- 일반 평균 거래량의 50배 이상
2. **짧은 시간 내 반복 거래** ⚠️
- 3초 내에 5건의 연속 거래 발생
- 스냅샷 방지 시도로 의심됨
3. **집중된 수신 주소** ✓ 확인 필요
- 단일 주소(0x8626...)로 80% 자금 집중
- 추가 모니터링 권장
권장 조치
- 해당 트랜잭션의 전체 调用 체인 분석 필요
- 토큰 승인(Approval) 이벤트 동시 확인 권장
고급: 실시간 이벤트 스트리밍
폴링 방식이 아닌 실시간 푸시 알림을 원한다면 WebSocket을 사용해야 합니다:
const { ethers } = require('ethers');
// WebSocket으로 실시간 이벤트监听
const WS_URL = "wss://sepolia.infura.io/ws/v3/YOUR_INFURA_KEY";
const provider = new ethers.WebSocketProvider(WS_URL);
const TOKEN_ADDRESS = "0x1a1A1b1B2b2b3b4C4C4D4D5E5F6F7G8G9H0H1";
const ERC20_ABI = [
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
const contract = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, provider);
async function realTimeListener() {
console.log("⚡ 실시간 Transfer 이벤트监听 시작 (WebSocket)\n");
// Transfer 이벤트 발생 시 즉시 처리
contract.on("Transfer", (from, to, value, event) => {
console.log("═══════════════════════════════");
console.log("🆕 새 Transfer 이벤트 감지!");
console.log(보낸 사람: ${from});
console.log(받는 사람: ${to});
console.log(금액: ${ethers.formatEther(value)} tokens);
console.log(블록: ${event.blockNumber});
console.log(시간: ${new Date().toISOString()});
// TODO: 알림 보내기, DB 저장 등 추가 작업
});
// 오류 처리
provider.on("error", (error) => {
console.error("WebSocket 오류:", error);
});
// 종료 시그널 처리
process.on("SIGINT", () => {
console.log("\n🛑 리스닝 종료...");
provider.destroy();
process.exit(0);
});
}
realTimeListener().catch(console.error);
실행 결과:
⚡ 실시간 Transfer 이벤트监听 시작 (WebSocket)
═══════════════════════════════
🆕 새 Transfer 이벤트 감지!
보낸 사람: 0x742d35Cc6634C0532925a3b8D4C9f6F2b9c1d2e3
받는 사람: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1709
금액: 250.75 tokens
블록: 5847700
시간: 2025-01-19T10:30:45.123Z
커스텀 이벤트 만들기
나만의 컨트랙트에서 이벤트를 정의하고监听하는 방법을 알아보겠습니다:
// 커스텀 이벤트 정의 예시 (Solidity)
// event NewProductCreated(
// uint256 indexed productId,
// string name,
// uint256 price,
// address indexed seller
// );
// 커스텀 이벤트 시그니처 계산
const { ethers } = require('ethers');
// 시그니처 계산
const eventSignature = "NewProductCreated(uint256,string,uint256,address)";
const topic = ethers.id(eventSignature);
console.log("이벤트 시그니처:", eventSignature);
console.log("토픽 해시:", topic);
// 출력: 0x3c7a3c1e0e2e8c9a8d7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5
// Indexed 파라미터는 개별 topics로 나뉨
const PRODUCT_ID_TOPIC = ethers.id("NewProductCreated(uint256,string,uint256,address)");
const SELLER_TOPIC = ethers.id("NewProductCreated(uint256,string,uint256,address)");
// Indexed(uint256) = keccak256 of type
const INDEXED_TOPIC0 = "0xf3c1e0e2e8c9a8d7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5";
console.log("\n필터 설정 예시:");
console.log(JSON.stringify({
address: "YOUR_CONTRACT_ADDRESS",
topics: [topic], // non-indexed 파라미터
fromBlock: 5800000,
toBlock: "latest"
}, null, 2));
성능 최적화 팁
대량의 이벤트를 다룰 때는 다음 사항을 고려해야 합니다:
- 블록 범위 제한: 한 번에 너무 넓은 범위를 조회하면 RPC 제한에 걸릴 수 있습니다
- 캐싱 활용: 이미 읽은 블록의 이벤트는 로컬에 저장하세요
- 필터 정확히 지정: topics를 최대한 구체적으로 지정하여 불필요한 데이터 전송을 줄이세요
- 배치 처리: 큰 범위는 여러 요청으로 나누어 처리하세요
// 대량 이벤트高效取得 예시
async function getEventsInBatches(contract, eventName, fromBlock, toBlock, batchSize = 1000) {
const allEvents = [];
for (let start = fromBlock; start <= toBlock; start += batchSize) {
const end = Math.min(start + batchSize - 1, toBlock);
console.log(배치処理: ${start} ~ ${end});
try {
const events = await contract.queryFilter(
eventName,
start,
end
);
allEvents.push(...events);
} catch (error) {
console.error(배치 ${start}-${end} 실패:, error.message);
// 실패 시 더 작은 단위로 재시도
const halfBatch = Math.floor(batchSize / 2);
if (halfBatch >= 10) {
const retryEvents = await getEventsInBatches(
contract, eventName, start, end, halfBatch
);
allEvents.push(...retryEvents);
}
}
// Rate limit 방지
await new Promise(r => setTimeout(r, 100));
}
return allEvents;
}
자주 발생하는 오류와 해결책
1. "Invalid JSON RPC Response" 오류
// ❌ 오류 메시지
// HttpProviderError: Invalid JSON RPC Response at undefined
// ✅ 해결 방법 1: RPC URL 확인
const RPC_URL = "https://sepolia.infura.io/v3/YOUR_INFURA_KEY";
// ↑ 정확한 프로젝트 ID로 교체했는지 확인
// ✅ 해결 방법 2: HTTP 대신 HTTPS 확인
// Infura: https://sepolia.infura.io/v3/...
// Alchemy: https://eth-sepolia.g.alchemy.com/v2/...
// ✅ 해결 방법 3: rate limit 체크
const provider = new ethers.JsonRpcProvider({
url: RPC_URL,
skipFetchSetup: true
});
// ✅ 해결 방법 4: 대안 RPC 사용
const ALTERNATIVE_RPCS = [
"https://rpc.ankr.com/eth_sepolia", // Ankr 무료 RPC
"https://ethereum-sepolia.publicnode.com" // PublicNode
];
2. "missing revert data" 또는 트랜잭션 실패
// ❌ 오류: Web3TransactionError: missing revert data
// ✅ 해결 방법: try-catch로 상세 오류 캡처
try {
const result = await contract.someFunction(params);
} catch (error) {
// 상세 오류 정보 확인
if (error.reason) {
console.error("리버트 이유:", error.reason);
}
if (error.data) {
console.error("리버트 데이터:", error.data);
}
// ABI에 에러 정의 추가
// error CustomError(uint256 code);
const CUSTOM_ERROR_ABI = [
"error InsufficientBalance(uint256 available, uint256 required)",
"error Unauthorized(address caller)"
];
}
// ✅ getLogs 시 리버트된 트랜잭션 제외
const logs = await provider.getLogs({
address: TOKEN_ADDRESS,
topics: [TRANSFER_TOPIC],
fromBlock: latestBlock - 100,
toBlock: latestBlock
});
// 리버트 여부 확인 (트랜잭션 영수증 필요)
async function checkIfReverted(txHash) {
const receipt = await provider.getTransactionReceipt(txHash);
return receipt.status === 1; // 1 = 성공, 0 = 리버트
}
3. ".filter" is not a function 또는 ABI 오류
// ❌ 오류: TypeError: contract.queryFilter is not a function
// ✅ 해결 방법: Contract 인스턴스 생성 방식 확인
const { ethers } = require('ethers');
// 잘못된 방법
const wrongContract = {
queryFilter: () => {} // 이건 객체, 컨트랙트가 아님
};
// 올바른 방법
const correctContract = new ethers.Contract(
ADDRESS,
["event Transfer(address,address,uint256)"],
provider
);
// ✅ 해결 방법: ABI 형식 확인
const VALID_ABI = [
"event Transfer(address indexed from, address indexed to, uint256 value)"
// ↑ indexed 키워드 위치 정확히
];
// ✅ 해결 방법: event 키워드 포함 확인
const WRONG_ABI = [
"Transfer(address,address,uint256)" // ❌ event 빠짐
];
const RIGHT_ABI = [
"event Transfer(address,address,uint256)" // ✅
];
// ✅ 해결 방법: BigInt 처리 (ethers v6)
const value = event.args.value; // BigInt 타입
const formatted = ethers.formatEther(value); // 문자열로 변환
console.log(Number(value)); // 또는 숫자로 변환
4. 토픽 일치하지 않음 (Zero Events)
// ❌ 문제: 이벤트가 있는데 getLogs 결과가 0
// ✅ 해결 방법 1: 정확한 이벤트 시그니처 계산
const { ethers } = require('ethers');
const eventSig = "Transfer(address,address,uint256)";
const topic0 = ethers.id(eventSig);
console.log("올바른 토픽:", topic0);
// 출력: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df35b9dc
// ✅ 해결 방법 2: indexed 파라미터가 있으면 topics 배열 사용
const logs = await provider.getLogs({
address: contractAddress,
topics: [
topic0, // 이벤트 시그니처 (필수)
ethers.zeroPadValue(fromAddress, 32), // indexed from
null, // indexed to (어떤 값이든)
null // indexed value 없음
],
fromBlock: fromBlock,
toBlock: toBlock
});
// ✅ 해결 방법 3: 블록 범위 확인 (너무 과거는 안 됨)
constgenesis = 1653607400; // Sepolia 배포일
const latestBlock = await provider.getBlockNumber();
console.log(검색 가능 범위: ${latestBlock - 100} ~ ${latestBlock});
// ✅ 해결 방법 4: 컨트랙트에서 이벤트 발생 여부 확인
// Etherscan에서 "Logs" 탭 확인
// https://sepolia.etherscan.io/address/YOUR_CONTRACT#events
5. WebSocket 연결 끊김
// ❌ WebSocket이 갑자기 끊김
// ✅ 해결 방법: 자동 재연결 구현
const { ethers } = require('ethers');
class ReconnectingProvider {
constructor(url) {
this.url = url;
this.provider = null;
this.reconnectDelay = 1000;
this.maxDelay = 30000;
}
async connect() {
this.provider = new ethers.WebSocketProvider(this.url);
this.provider.websocket.on('close', () => {
console.log("연결 끊김, 재연결 시도...");
this.reconnect();
});
this.provider.websocket.on('error', (err) => {
console.error("WebSocket 오류:", err.message);
});
}
reconnect() {
setTimeout(async () => {
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxDelay
);
console.log(${this.reconnectDelay}ms 후 재연결...);
await this.connect();
}, this.reconnectDelay);
}
on(event, callback) {
return this.provider.on(event, callback);
}
}
// 사용법
const wsProvider = new ReconnectingProvider(WS_URL);
await wsProvider.connect();
wsProvider.on("Transfer", (from, to, value) => {
console.log(전송: ${from} -> ${to});
});
정리
이번 튜토리얼에서 다룬 내용을 정리하면:
- Event Logs의 기본 개념: Ethereum에서 발생하는 사건들을 기록하는 메커니즘
- ethers.js 활용:
getLogs와queryFilter로 이벤트取得 - WebSocket 실시간监听: 즉각적인 이벤트 감지가 필요한 경우
- HolySheep AI 통합: AI를 활용한 스마트한 로그 분석
- 일반적인 오류 해결: RPC, ABI, 토픽 관련 문제 해결 방법
Event Logs는 DeFi, NFT, DAO 등 다양한 블록체인 앱에서 핵심적인 역할을 합니다. 기본기를 잘 다지면 더 복잡한 백오피스 시스템, 감사 도구, 자동화 봇 등을 만들 수 있습니다.
AI API를 활용한 고급 분석이 필요하시면 지금 HolySheep AI에 가입하여 무료 크레딧으로 바로 시작해보세요. 단일 API 키로 GPT-4.1, Claude Sonnet, Gemini 등 최적의 모델을 선택적으로 사용할 수 있습니다.
궁금한 점이 있으시면 댓글 남겨주세요. 다음 튜토리얼에서는 이벤트 기반 자동 거래 봇 만들기를 다루겠습니다.
👉 HolySheep AI 가입하고 무료 크레딧 받기