블록체인 개발에서 스마트 컨트랙트의 상태 변경을 실시간으로 감지하는 것은 핵심 기술입니다. 이번 튜토리얼에서는 Ethereum Event Logs를 가져오는 방법을 초보자도 이해할 수 있도록 단계별로 설명드리겠습니다. 또한 HolySheep AI의 AI API를 활용하여 이벤트 로그를 지능적으로 분석하는 방법도 함께 다룹니다.

Ethereum Event Logs란 무엇인가?

Ethereum Event Logs는 스마트 컨트랙트가 실행 중 발생한 사건들을 기록한 데이터입니다. 마치 앱에서 로그를 출력하는 것처럼, 컨트랙트에서도emit 키워드로 특정 사건을 기록합니다.

실생활 비유: 은행 앱에서 "100만원 입금됨" 알림을 받는 것과 같습니다. Ethereum에서는 컨트랙트의 이벤트(emitted events)가 이 알림 역할을 합니다.

Event Logs가 중요한 이유

준비물: 개발 환경 설정

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 노드에 연결해야 합니다. 대표적인 옵션:

저는 테스트넷에서 먼저 연습할 것을 권장합니다. 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));

성능 최적화 팁

대량의 이벤트를 다룰 때는 다음 사항을 고려해야 합니다:

// 대량 이벤트高效取得 예시
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는 DeFi, NFT, DAO 등 다양한 블록체인 앱에서 핵심적인 역할을 합니다. 기본기를 잘 다지면 더 복잡한 백오피스 시스템, 감사 도구, 자동화 봇 등을 만들 수 있습니다.

AI API를 활용한 고급 분석이 필요하시면 지금 HolySheep AI에 가입하여 무료 크레딧으로 바로 시작해보세요. 단일 API 키로 GPT-4.1, Claude Sonnet, Gemini 등 최적의 모델을 선택적으로 사용할 수 있습니다.

궁금한 점이 있으시면 댓글 남겨주세요. 다음 튜토리얼에서는 이벤트 기반 자동 거래 봇 만들기를 다루겠습니다.

👉 HolySheep AI 가입하고 무료 크레딧 받기