Files
mev-beta/pkg/database/database.go
2025-09-16 11:05:47 -05:00

428 lines
13 KiB
Go

// Package database provides database functionality for storing MEV bot data
package database
import (
"database/sql"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/config"
"github.com/fraktal/mev-beta/internal/logger"
_ "github.com/mattn/go-sqlite3"
)
// Database represents the database connection and operations
type Database struct {
db *sql.DB
logger *logger.Logger
config *config.DatabaseConfig
}
// SwapEvent represents a swap event stored in the database
type SwapEvent struct {
ID int64 `json:"id"`
Timestamp time.Time `json:"timestamp"`
BlockNumber uint64 `json:"block_number"`
TxHash common.Hash `json:"tx_hash"`
PoolAddress common.Address `json:"pool_address"`
Token0 common.Address `json:"token0"`
Token1 common.Address `json:"token1"`
Amount0In *big.Int `json:"amount0_in"`
Amount1In *big.Int `json:"amount1_in"`
Amount0Out *big.Int `json:"amount0_out"`
Amount1Out *big.Int `json:"amount1_out"`
Sender common.Address `json:"sender"`
Recipient common.Address `json:"recipient"`
Protocol string `json:"protocol"`
}
// LiquidityEvent represents a liquidity event stored in the database
type LiquidityEvent struct {
ID int64 `json:"id"`
Timestamp time.Time `json:"timestamp"`
BlockNumber uint64 `json:"block_number"`
TxHash common.Hash `json:"tx_hash"`
PoolAddress common.Address `json:"pool_address"`
Token0 common.Address `json:"token0"`
Token1 common.Address `json:"token1"`
Liquidity *big.Int `json:"liquidity"`
Amount0 *big.Int `json:"amount0"`
Amount1 *big.Int `json:"amount1"`
Sender common.Address `json:"sender"`
Recipient common.Address `json:"recipient"`
EventType string `json:"event_type"` // "add" or "remove"
Protocol string `json:"protocol"`
}
// PoolData represents pool data stored in the database
type PoolData struct {
ID int64 `json:"id"`
Address common.Address `json:"address"`
Token0 common.Address `json:"token0"`
Token1 common.Address `json:"token1"`
Fee int64 `json:"fee"`
Liquidity *big.Int `json:"liquidity"`
SqrtPriceX96 *big.Int `json:"sqrt_price_x96"`
Tick int64 `json:"tick"`
LastUpdated time.Time `json:"last_updated"`
Protocol string `json:"protocol"`
}
// NewDatabase creates a new database connection
func NewDatabase(cfg *config.DatabaseConfig, logger *logger.Logger) (*Database, error) {
// Open database connection
db, err := sql.Open("sqlite3", cfg.File)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Set connection limits
db.SetMaxOpenConns(cfg.MaxOpenConnections)
db.SetMaxIdleConns(cfg.MaxIdleConnections)
// Create database instance
database := &Database{
db: db,
logger: logger,
config: cfg,
}
// Initialize database schema
if err := database.initSchema(); err != nil {
return nil, fmt.Errorf("failed to initialize database schema: %w", err)
}
logger.Info("Database initialized successfully")
return database, nil
}
// initSchema initializes the database schema
func (d *Database) initSchema() error {
// Create tables if they don't exist
tables := []string{
`CREATE TABLE IF NOT EXISTS swap_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
block_number INTEGER NOT NULL,
tx_hash TEXT NOT NULL UNIQUE,
pool_address TEXT NOT NULL,
token0 TEXT NOT NULL,
token1 TEXT NOT NULL,
amount0_in TEXT NOT NULL,
amount1_in TEXT NOT NULL,
amount0_out TEXT NOT NULL,
amount1_out TEXT NOT NULL,
sender TEXT NOT NULL,
recipient TEXT NOT NULL,
protocol TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS liquidity_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
block_number INTEGER NOT NULL,
tx_hash TEXT NOT NULL UNIQUE,
pool_address TEXT NOT NULL,
token0 TEXT NOT NULL,
token1 TEXT NOT NULL,
liquidity TEXT NOT NULL,
amount0 TEXT NOT NULL,
amount1 TEXT NOT NULL,
sender TEXT NOT NULL,
recipient TEXT NOT NULL,
event_type TEXT NOT NULL,
protocol TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS pool_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL UNIQUE,
token0 TEXT NOT NULL,
token1 TEXT NOT NULL,
fee INTEGER NOT NULL,
liquidity TEXT NOT NULL,
sqrt_price_x96 TEXT NOT NULL,
tick INTEGER NOT NULL,
last_updated DATETIME NOT NULL,
protocol TEXT NOT NULL
)`,
// Create indexes for performance
`CREATE INDEX IF NOT EXISTS idx_swap_timestamp ON swap_events(timestamp)`,
`CREATE INDEX IF NOT EXISTS idx_swap_pool ON swap_events(pool_address)`,
`CREATE INDEX IF NOT EXISTS idx_liquidity_timestamp ON liquidity_events(timestamp)`,
`CREATE INDEX IF NOT EXISTS idx_liquidity_pool ON liquidity_events(pool_address)`,
`CREATE INDEX IF NOT EXISTS idx_pool_address ON pool_data(address)`,
`CREATE INDEX IF NOT EXISTS idx_pool_tokens ON pool_data(token0, token1)`,
}
// Execute all table creation statements
for _, stmt := range tables {
_, err := d.db.Exec(stmt)
if err != nil {
return fmt.Errorf("failed to execute statement: %s, error: %w", stmt, err)
}
}
return nil
}
// InsertSwapEvent inserts a swap event into the database
func (d *Database) InsertSwapEvent(event *SwapEvent) error {
stmt, err := d.db.Prepare(`
INSERT INTO swap_events (
timestamp, block_number, tx_hash, pool_address, token0, token1,
amount0_in, amount1_in, amount0_out, amount1_out,
sender, recipient, protocol
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
return fmt.Errorf("failed to prepare insert statement: %w", err)
}
defer stmt.Close()
_, err = stmt.Exec(
event.Timestamp.UTC().Format(time.RFC3339), // Store in UTC RFC3339 format
event.BlockNumber,
event.TxHash.Hex(),
event.PoolAddress.Hex(),
event.Token0.Hex(),
event.Token1.Hex(),
event.Amount0In.String(),
event.Amount1In.String(),
event.Amount0Out.String(),
event.Amount1Out.String(),
event.Sender.Hex(),
event.Recipient.Hex(),
event.Protocol,
)
if err != nil {
return fmt.Errorf("failed to insert swap event: %w", err)
}
d.logger.Debug(fmt.Sprintf("Inserted swap event for pool %s", event.PoolAddress.Hex()))
return nil
}
// InsertLiquidityEvent inserts a liquidity event into the database
func (d *Database) InsertLiquidityEvent(event *LiquidityEvent) error {
stmt, err := d.db.Prepare(`
INSERT INTO liquidity_events (
timestamp, block_number, tx_hash, pool_address, token0, token1,
liquidity, amount0, amount1, sender, recipient, event_type, protocol
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
return fmt.Errorf("failed to prepare insert statement: %w", err)
}
defer stmt.Close()
_, err = stmt.Exec(
event.Timestamp.UTC().Format(time.RFC3339), // Store in UTC RFC3339 format
event.BlockNumber,
event.TxHash.Hex(),
event.PoolAddress.Hex(),
event.Token0.Hex(),
event.Token1.Hex(),
event.Liquidity.String(),
event.Amount0.String(),
event.Amount1.String(),
event.Sender.Hex(),
event.Recipient.Hex(),
event.EventType,
event.Protocol,
)
if err != nil {
return fmt.Errorf("failed to insert liquidity event: %w", err)
}
d.logger.Debug(fmt.Sprintf("Inserted liquidity event for pool %s", event.PoolAddress.Hex()))
return nil
}
// InsertPoolData inserts or updates pool data in the database
func (d *Database) InsertPoolData(pool *PoolData) error {
stmt, err := d.db.Prepare(`
INSERT OR REPLACE INTO pool_data (
address, token0, token1, fee, liquidity, sqrt_price_x96, tick, last_updated, protocol
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
return fmt.Errorf("failed to prepare insert statement: %w", err)
}
defer stmt.Close()
_, err = stmt.Exec(
pool.Address.Hex(),
pool.Token0.Hex(),
pool.Token1.Hex(),
pool.Fee,
pool.Liquidity.String(),
pool.SqrtPriceX96.String(),
pool.Tick,
pool.LastUpdated.UTC().Format(time.RFC3339), // Store in UTC RFC3339 format
pool.Protocol,
)
if err != nil {
return fmt.Errorf("failed to insert pool data: %w", err)
}
d.logger.Debug(fmt.Sprintf("Inserted/updated pool data for pool %s", pool.Address.Hex()))
return nil
}
// GetRecentSwapEvents retrieves recent swap events from the database
func (d *Database) GetRecentSwapEvents(limit int) ([]*SwapEvent, error) {
rows, err := d.db.Query(`
SELECT id, timestamp, block_number, tx_hash, pool_address, token0, token1,
amount0_in, amount1_in, amount0_out, amount1_out, sender, recipient, protocol
FROM swap_events
ORDER BY timestamp DESC
LIMIT ?
`, limit)
if err != nil {
return nil, fmt.Errorf("failed to query swap events: %w", err)
}
defer rows.Close()
events := make([]*SwapEvent, 0)
for rows.Next() {
event := &SwapEvent{}
var txHash, poolAddr, token0, token1, sender, recipient string
var amount0In, amount1In, amount0Out, amount1Out string
err := rows.Scan(
&event.ID, &event.Timestamp, &event.BlockNumber, &txHash, &poolAddr, &token0, &token1,
&amount0In, &amount1In, &amount0Out, &amount1Out, &sender, &recipient, &event.Protocol,
)
if err != nil {
return nil, fmt.Errorf("failed to scan swap event: %w", err)
}
// Convert string values to proper types
event.TxHash = common.HexToHash(txHash)
event.PoolAddress = common.HexToAddress(poolAddr)
event.Token0 = common.HexToAddress(token0)
event.Token1 = common.HexToAddress(token1)
event.Sender = common.HexToAddress(sender)
event.Recipient = common.HexToAddress(recipient)
// Convert string amounts to big.Int
event.Amount0In = new(big.Int)
event.Amount0In.SetString(amount0In, 10)
event.Amount1In = new(big.Int)
event.Amount1In.SetString(amount1In, 10)
event.Amount0Out = new(big.Int)
event.Amount0Out.SetString(amount0Out, 10)
event.Amount1Out = new(big.Int)
event.Amount1Out.SetString(amount1Out, 10)
events = append(events, event)
}
return events, nil
}
// GetRecentLiquidityEvents retrieves recent liquidity events from the database
func (d *Database) GetRecentLiquidityEvents(limit int) ([]*LiquidityEvent, error) {
rows, err := d.db.Query(`
SELECT id, timestamp, block_number, tx_hash, pool_address, token0, token1,
liquidity, amount0, amount1, sender, recipient, event_type, protocol
FROM liquidity_events
ORDER BY timestamp DESC
LIMIT ?
`, limit)
if err != nil {
return nil, fmt.Errorf("failed to query liquidity events: %w", err)
}
defer rows.Close()
events := make([]*LiquidityEvent, 0)
for rows.Next() {
event := &LiquidityEvent{}
var txHash, poolAddr, token0, token1, sender, recipient, eventType string
var liquidity, amount0, amount1 string
err := rows.Scan(
&event.ID, &event.Timestamp, &event.BlockNumber, &txHash, &poolAddr, &token0, &token1,
&liquidity, &amount0, &amount1, &sender, &recipient, &eventType, &event.Protocol,
)
if err != nil {
return nil, fmt.Errorf("failed to scan liquidity event: %w", err)
}
// Convert string values to proper types
event.TxHash = common.HexToHash(txHash)
event.PoolAddress = common.HexToAddress(poolAddr)
event.Token0 = common.HexToAddress(token0)
event.Token1 = common.HexToAddress(token1)
event.Sender = common.HexToAddress(sender)
event.Recipient = common.HexToAddress(recipient)
event.EventType = eventType
// Convert string amounts to big.Int
event.Liquidity = new(big.Int)
event.Liquidity.SetString(liquidity, 10)
event.Amount0 = new(big.Int)
event.Amount0.SetString(amount0, 10)
event.Amount1 = new(big.Int)
event.Amount1.SetString(amount1, 10)
events = append(events, event)
}
return events, nil
}
// GetPoolData retrieves pool data from the database
func (d *Database) GetPoolData(poolAddress common.Address) (*PoolData, error) {
row := d.db.QueryRow(`
SELECT id, address, token0, token1, fee, liquidity, sqrt_price_x96, tick, last_updated, protocol
FROM pool_data
WHERE address = ?
`, poolAddress.Hex())
pool := &PoolData{}
var addr, token0, token1, liquidity, sqrtPrice string
var lastUpdated string
err := row.Scan(
&pool.ID, &addr, &token0, &token1, &pool.Fee, &liquidity, &sqrtPrice, &pool.Tick, &lastUpdated, &pool.Protocol,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("pool data not found for address: %s", poolAddress.Hex())
}
return nil, fmt.Errorf("failed to scan pool data: %w", err)
}
// Convert string values to proper types
pool.Address = common.HexToAddress(addr)
pool.Token0 = common.HexToAddress(token0)
pool.Token1 = common.HexToAddress(token1)
// Convert string amounts to big.Int
pool.Liquidity = new(big.Int)
pool.Liquidity.SetString(liquidity, 10)
pool.SqrtPriceX96 = new(big.Int)
pool.SqrtPriceX96.SetString(sqrtPrice, 10)
// Parse timestamp
pool.LastUpdated, err = time.Parse(time.RFC3339, lastUpdated)
if err != nil {
return nil, fmt.Errorf("failed to parse timestamp: %w", err)
}
return pool, nil
}
// Close closes the database connection
func (d *Database) Close() error {
if d.db != nil {
return d.db.Close()
}
return nil
}