Skip to content

Commit

Permalink
first init
Browse files Browse the repository at this point in the history
  • Loading branch information
Deniz Umut Dereli committed Jul 1, 2024
0 parents commit 73c7b16
Show file tree
Hide file tree
Showing 14 changed files with 675 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
NODE_ENV=development
APP_NAME=evm-event-tracker
LOG_DIR=../logs
LOG_SERVER=
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=dev_user
DB_PASSWORD=dev_password
DB_NAME=event_tracker_dev
REDIS_HOST=localhost
REDIS_PORT=6379
CONTRACT_ADDRESS=0x7ceb23fd6bc0add59e62ac25578270cff1b9f619
CONTRACT_ABI_PATH=./src/abi/WETH.json
CONTRACT_EVENT_NAMES=Approval,Transfer
RPC_URL=https://polygon-mainnet.infura.io/v3/INFURA_ID
POLL_INTERVAL_MS=60000
STARTING_BLOCK_NUMBER=58799213
RESCAN=true
RESCAN_TIMER=600000
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.env
.env.production
.env.development

/log
/logs

package-lock.json
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: '3.8'

services:
postgres:
image: postgres:14
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "${DB_PORT}:5432"

redis:
image: redis:6
ports:
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data

volumes:
postgres_data:
redis_data:
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "evm-event-tracker",
"author": {
"name": "Deniz Umut Dereli",
"email": "deniz.umut.dereli@gmail.com"},
"version": "1.0.0",
"description": "EVM event tracker using TypeScript, PostgreSQL, and Redis",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"typeorm": "typeorm-ts-node-commonjs"
},
"dependencies": {
"commander": "^12.1.0",
"dotenv": "^16.0.3",
"ethers": "^6.7.1",
"inquirer": "^9.3.1",
"pg": "^8.10.0",
"redis": "^3.1.2",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.15",
"winston": "^3.13.0",
"winston-daily-rotate-file": "^5.0.0",
"winston-syslog": "^2.7.0"
},
"devDependencies": {
"@types/inquirer": "^9.0.7",
"@types/node": "^18.15.11",
"@types/redis": "^2.8.32",
"@types/winston-syslog": "^2.4.3",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.4"
},
"engines": {
"node": ">=14.0.0"
}
}
5 changes: 5 additions & 0 deletions src/abi/WETH.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"status": "1",
"message": "OK-Missing/Invalid API Key, rate limit of 1/5sec applied",
"result": "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"childChainManager\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"userAddress\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address payable\",\"name\":\"relayerAddress\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"functionSignature\",\"type\":\"bytes\"}],\"name\":\"MetaTransactionExecuted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"previousAdminRole\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"newAdminRole\",\"type\":\"bytes32\"}],\"name\":\"RoleAdminChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"RoleGranted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"RoleRevoked\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"CHILD_CHAIN_ID\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"CHILD_CHAIN_ID_BYTES\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEFAULT_ADMIN_ROLE\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSITOR_ROLE\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"ERC712_VERSION\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"ROOT_CHAIN_ID\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"ROOT_CHAIN_ID_BYTES\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"subtractedValue\",\"type\":\"uint256\"}],\"name\":\"decreaseAllowance\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"depositData\",\"type\":\"bytes\"}],\"name\":\"deposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"userAddress\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"functionSignature\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"sigR\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"sigS\",\"type\":\"bytes32\"},{\"internalType\":\"uint8\",\"name\":\"sigV\",\"type\":\"uint8\"}],\"name\":\"executeMetaTransaction\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getChainId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getDomainSeperator\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"getNonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"}],\"name\":\"getRoleAdmin\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"getRoleMember\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"}],\"name\":\"getRoleMemberCount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"grantRole\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"hasRole\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"addedValue\",\"type\":\"uint256\"}],\"name\":\"increaseAllowance\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"renounceRole\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"role\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"revokeRole\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]"
}
65 changes: 65 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Command } from 'commander';
import dotenv from 'dotenv';
import path from 'path';
import { Config } from 'src/interfaces';

const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
dotenv.config({ path: path.resolve(process.cwd(), envFile) });

const program = new Command();
const genesis = '0';

program
.option('--appname <appname>', 'Application name')
.option('--rpc <url>', 'RPC URL')
.option('--abi <path>', 'ABI path')
.option('--events <names>', 'Event names, comma separated')
.option('--block <number>', 'Starting block number', (value) => parseInt(value, 10))
.option('--contract <address>', 'Contract address')
.option('--rescan <rescan>', 'Enable rescan feature', (value) => value === 'true');

const options = program.opts();

const eventNames: string[] = options.events
? options.events.split(',').map((event: string) => event.trim())
: (process.env.CONTRACT_EVENT_NAMES
? process.env.CONTRACT_EVENT_NAMES.split(',').map((event: string) => event.trim())
: []);

const pollIntervalMs = parseInt(process.env.POLL_INTERVAL_MS || '60000', 10);
const rescanTimer = parseInt(process.env.RESCAN_TIMER || '600000', 10);

if (rescanTimer <= pollIntervalMs) {
console.error("RESCAN_TIMER must be greater than POLL_INTERVAL_MS");
process.exit(1);
}

export const config:Config = {
nodeEnv: process.env.NODE_ENV || 'development',
appName: options.appname || (process.env.APP_NAME || 'event-tracker'),
logDir: process.env.LOG_DIR || 'logs',
logServer: process.env.LOG_SERVER,
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',
name: process.env.DB_NAME || 'evm-event-tracker',
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
},
contract: {
address: options.contract || process.env.CONTRACT_ADDRESS,
rpcUrl: options.rpc || process.env.RPC_URL,
abiPath: options.abi || process.env.CONTRACT_ABI_PATH || '',
eventNames: eventNames,
},
pollIntervalMs: pollIntervalMs,
rescan: options.rescan,
rescanTimer: rescanTimer,
startBlockNumber: options.block || parseInt(process.env.STARTING_BLOCK_NUMBER || genesis, 10),
};

console.log(`Running with config: ${JSON.stringify(config, null, 2)}`);
29 changes: 29 additions & 0 deletions src/entities/Event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
@Index(['transactionHash', 'logIndex'], { unique: true })
export class Event {
@PrimaryGeneratedColumn()
id: number;

@Column()
eventName: string;

@Column()
eventSignature: string;

@Column({ type: 'text' })
eventData: string;

@Column()
blockNumber: number;

@Column()
transactionHash: string;

@Column()
logIndex: number;

@Column({ type: 'text', nullable: true })
parsedData: string;
}
62 changes: 62 additions & 0 deletions src/events/contractEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ethers } from 'ethers';
import fs from 'fs';
import { config } from '../config/config';
import { logger } from '../logger/logger';

export interface EventDefinition {
name: string;
signature: string;
}

interface AbiInput {
internalType: string;
name: string;
type: string;
}

interface AbiEvent {
anonymous: boolean;
inputs: AbiInput[];
name: string;
type: string;
}

const abiPath = config.contract.abiPath;
const abiContent = fs.readFileSync(abiPath, 'utf8');
const abiJson = JSON.parse(abiContent);
const abi: AbiEvent[] = JSON.parse(abiJson.result);

const extractEventsFromAbi = (abi: AbiEvent[]): EventDefinition[] => {
return abi
.filter(item => item.type === 'event')
.map(event => ({
name: event.name,
signature: `${event.name}(${event.inputs.map((input: AbiInput) => input.type).join(',')})`,
}));
};

export const allContractEvents: EventDefinition[] = extractEventsFromAbi(abi);

export function generateEventSignatures(events: EventDefinition[]): string[] {
return events.map(event => ethers.keccak256(ethers.toUtf8Bytes(event.signature)));
}

const wantedEventNames = new Set([...config.contract.eventNames]);
const filteredEvents = allContractEvents.filter(event => wantedEventNames.has(event.name));

wantedEventNames.forEach(eventName => {
if (!filteredEvents.some(event => event.name === eventName)) {
logger.error(`Warning: Event name "${eventName}" not found in the ABI.`);
process.exit(1);
}
});

export const contractEvents = filteredEvents;

export const eventSignatures = generateEventSignatures(contractEvents);

logger.info(`Filtered contract events: ${JSON.stringify(contractEvents)}`);
logger.info(`Event signatures: ${JSON.stringify(eventSignatures)}`);



62 changes: 62 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Redis from 'redis';
import { DataSource } from 'typeorm';
import { promisify } from 'util';
import { config } from './config/config';
import { Event } from './entities/Event';
import { logger } from './logger/logger';
import { EventTracker } from './services/EventTracker';

async function getLatestBlockFromDb(dataSource: DataSource): Promise<number> {
const latestBlock = await dataSource.query(`SELECT MAX("blockNumber") AS "latestBlock" FROM event`);
return latestBlock[0].latestBlock || config.startBlockNumber;
}

async function main() {
const dataSource = new DataSource({
type: 'postgres',
host: config.database.host,
port: config.database.port,
username: config.database.username,
password: config.database.password,
database: config.database.name,
entities: [Event],
synchronize: true,
});

await dataSource.initialize();
logger.debug('Database connection established');

const redisClient = Redis.createClient({
host: config.redis.host,
port: config.redis.port,
});

const redisGetAsync = promisify(redisClient.get).bind(redisClient);
const currentBlock = await getLatestBlockFromDb(dataSource);

if (config.startBlockNumber > currentBlock) {
console.error(`Configured start block number ${config.startBlockNumber} is bigger than the latest processed block ${currentBlock}. Aborting.`);
process.exit(1);
} else {
config.startBlockNumber = currentBlock;
}

const eventTracker = new EventTracker(dataSource, config.pollIntervalMs);
await eventTracker.initialize();
logger.debug('Event tracker initialized');

// graceful shutdown
process.on('SIGINT', async () => {
logger.debug('Stopping event tracker...');
eventTracker.stop();
await dataSource.destroy();
redisClient.quit();
logger.debug('Database connection closed');
process.exit(0);
});
}

main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
28 changes: 28 additions & 0 deletions src/interfaces/config.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface Config {
nodeEnv: string;
appName: string;
logDir: string;
logServer?: string;
database: {
host: string;
port: number;
username: string;
password: string;
name: string;
};
redis: {
host: string;
port: number;
};
contract: {
address?: string;
rpcUrl?: string;
abiPath: string;
eventNames: string[];
};
pollIntervalMs: number;
rescan: boolean;
rescanTimer: number;
startBlockNumber: number;
}

2 changes: 2 additions & 0 deletions src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./config.interface";

Loading

0 comments on commit 73c7b16

Please sign in to comment.