mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
454 lines
13 KiB
PL/PgSQL
454 lines
13 KiB
PL/PgSQL
-- Lux EVM Indexer Database Functions
|
|
-- Version: 1.0.0
|
|
-- Date: 2025-12-25
|
|
-- Utility functions for indexer operations
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Address Statistics Functions
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Update address transaction count after new transactions
|
|
CREATE OR REPLACE FUNCTION update_address_tx_count()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Update from address count
|
|
INSERT INTO addresses (chain_id, hash, transactions_count, inserted_at, updated_at)
|
|
VALUES (NEW.chain_id, NEW.from_address_hash, 1, NOW(), NOW())
|
|
ON CONFLICT (chain_id, hash) DO UPDATE
|
|
SET transactions_count = addresses.transactions_count + 1,
|
|
updated_at = NOW();
|
|
|
|
-- Update to address count if exists
|
|
IF NEW.to_address_hash IS NOT NULL THEN
|
|
INSERT INTO addresses (chain_id, hash, transactions_count, inserted_at, updated_at)
|
|
VALUES (NEW.chain_id, NEW.to_address_hash, 1, NOW(), NOW())
|
|
ON CONFLICT (chain_id, hash) DO UPDATE
|
|
SET transactions_count = addresses.transactions_count + 1,
|
|
updated_at = NOW();
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Trigger for address tx count (disabled by default for bulk import)
|
|
-- CREATE TRIGGER trigger_update_address_tx_count
|
|
-- AFTER INSERT ON transactions
|
|
-- FOR EACH ROW
|
|
-- EXECUTE FUNCTION update_address_tx_count();
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Token Holder Count Functions
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Update token holder count after balance changes
|
|
CREATE OR REPLACE FUNCTION update_token_holder_count()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_old_value NUMERIC(100);
|
|
v_new_value NUMERIC(100);
|
|
BEGIN
|
|
v_old_value := COALESCE(OLD.value, 0);
|
|
v_new_value := COALESCE(NEW.value, 0);
|
|
|
|
-- If going from zero to non-zero, increment holder count
|
|
IF v_old_value = 0 AND v_new_value > 0 THEN
|
|
UPDATE tokens
|
|
SET holder_count = holder_count + 1,
|
|
updated_at = NOW()
|
|
WHERE chain_id = NEW.chain_id
|
|
AND contract_address_hash = NEW.token_contract_address_hash;
|
|
-- If going from non-zero to zero, decrement holder count
|
|
ELSIF v_old_value > 0 AND v_new_value = 0 THEN
|
|
UPDATE tokens
|
|
SET holder_count = GREATEST(0, holder_count - 1),
|
|
updated_at = NOW()
|
|
WHERE chain_id = NEW.chain_id
|
|
AND contract_address_hash = NEW.token_contract_address_hash;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Block Statistics Functions
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Calculate average block time for a chain
|
|
CREATE OR REPLACE FUNCTION get_average_block_time(
|
|
p_chain_id BIGINT,
|
|
p_block_count INTEGER DEFAULT 100
|
|
)
|
|
RETURNS NUMERIC AS $$
|
|
DECLARE
|
|
v_avg_time NUMERIC;
|
|
BEGIN
|
|
SELECT AVG(b1.timestamp - b2.timestamp)
|
|
INTO v_avg_time
|
|
FROM (
|
|
SELECT number, timestamp,
|
|
LAG(timestamp) OVER (ORDER BY number) as prev_timestamp
|
|
FROM blocks
|
|
WHERE chain_id = p_chain_id
|
|
ORDER BY number DESC
|
|
LIMIT p_block_count
|
|
) AS b1
|
|
JOIN blocks b2 ON b1.number = b2.number AND b1.prev_timestamp IS NOT NULL
|
|
WHERE b1.prev_timestamp IS NOT NULL;
|
|
|
|
RETURN COALESCE(v_avg_time, 2000); -- Default 2 seconds
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Get gas price statistics
|
|
CREATE OR REPLACE FUNCTION get_gas_price_stats(
|
|
p_chain_id BIGINT,
|
|
p_block_count INTEGER DEFAULT 200
|
|
)
|
|
RETURNS TABLE (
|
|
slow NUMERIC,
|
|
average NUMERIC,
|
|
fast NUMERIC
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
PERCENTILE_CONT(0.1) WITHIN GROUP (ORDER BY gas_price) as slow,
|
|
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY gas_price) as average,
|
|
PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY gas_price) as fast
|
|
FROM (
|
|
SELECT gas_price
|
|
FROM transactions t
|
|
JOIN blocks b ON t.chain_id = b.chain_id AND t.block_number = b.number
|
|
WHERE t.chain_id = p_chain_id
|
|
AND t.gas_price IS NOT NULL
|
|
AND t.gas_price > 0
|
|
ORDER BY b.number DESC
|
|
LIMIT p_block_count * 100 -- ~100 tx per block average
|
|
) recent_txs;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Search Functions
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Universal search function
|
|
CREATE OR REPLACE FUNCTION search_all(
|
|
p_chain_id BIGINT,
|
|
p_query TEXT,
|
|
p_limit INTEGER DEFAULT 10
|
|
)
|
|
RETURNS TABLE (
|
|
type TEXT,
|
|
hash BYTEA,
|
|
name TEXT,
|
|
symbol TEXT,
|
|
block_number BIGINT,
|
|
address_hash BYTEA,
|
|
token_type TEXT
|
|
) AS $$
|
|
DECLARE
|
|
v_query_normalized TEXT;
|
|
v_is_hash BOOLEAN;
|
|
v_is_number BOOLEAN;
|
|
BEGIN
|
|
v_query_normalized := LOWER(TRIM(p_query));
|
|
|
|
-- Check if query looks like a hash (0x + 64 hex chars or 40 hex chars for address)
|
|
v_is_hash := v_query_normalized ~ '^0x[0-9a-f]{40}$' OR v_query_normalized ~ '^0x[0-9a-f]{64}$';
|
|
|
|
-- Check if query is a number
|
|
v_is_number := v_query_normalized ~ '^[0-9]+$';
|
|
|
|
-- Search blocks by number
|
|
IF v_is_number THEN
|
|
RETURN QUERY
|
|
SELECT
|
|
'block'::TEXT as type,
|
|
b.hash,
|
|
NULL::TEXT as name,
|
|
NULL::TEXT as symbol,
|
|
b.number as block_number,
|
|
NULL::BYTEA as address_hash,
|
|
NULL::TEXT as token_type
|
|
FROM blocks b
|
|
WHERE b.chain_id = p_chain_id
|
|
AND b.number = v_query_normalized::BIGINT
|
|
LIMIT 1;
|
|
END IF;
|
|
|
|
-- Search by hash (transaction, block, or address)
|
|
IF v_is_hash THEN
|
|
-- Transaction hash
|
|
RETURN QUERY
|
|
SELECT
|
|
'transaction'::TEXT as type,
|
|
t.hash,
|
|
NULL::TEXT as name,
|
|
NULL::TEXT as symbol,
|
|
t.block_number,
|
|
NULL::BYTEA as address_hash,
|
|
NULL::TEXT as token_type
|
|
FROM transactions t
|
|
WHERE t.chain_id = p_chain_id
|
|
AND t.hash = decode(substring(v_query_normalized from 3), 'hex')
|
|
LIMIT 1;
|
|
|
|
-- Block hash
|
|
RETURN QUERY
|
|
SELECT
|
|
'block'::TEXT as type,
|
|
b.hash,
|
|
NULL::TEXT as name,
|
|
NULL::TEXT as symbol,
|
|
b.number as block_number,
|
|
NULL::BYTEA as address_hash,
|
|
NULL::TEXT as token_type
|
|
FROM blocks b
|
|
WHERE b.chain_id = p_chain_id
|
|
AND b.hash = decode(substring(v_query_normalized from 3), 'hex')
|
|
LIMIT 1;
|
|
|
|
-- Address
|
|
IF LENGTH(v_query_normalized) = 42 THEN
|
|
RETURN QUERY
|
|
SELECT
|
|
'address'::TEXT as type,
|
|
NULL::BYTEA as hash,
|
|
NULL::TEXT as name,
|
|
NULL::TEXT as symbol,
|
|
NULL::BIGINT as block_number,
|
|
a.hash as address_hash,
|
|
NULL::TEXT as token_type
|
|
FROM addresses a
|
|
WHERE a.chain_id = p_chain_id
|
|
AND a.hash = decode(substring(v_query_normalized from 3), 'hex')
|
|
LIMIT 1;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Token search by name/symbol
|
|
RETURN QUERY
|
|
SELECT
|
|
'token'::TEXT as type,
|
|
NULL::BYTEA as hash,
|
|
tk.name,
|
|
tk.symbol,
|
|
NULL::BIGINT as block_number,
|
|
tk.contract_address_hash as address_hash,
|
|
tk.type as token_type
|
|
FROM tokens tk
|
|
WHERE tk.chain_id = p_chain_id
|
|
AND (
|
|
tk.name ILIKE '%' || p_query || '%'
|
|
OR tk.symbol ILIKE '%' || p_query || '%'
|
|
)
|
|
ORDER BY tk.holder_count DESC
|
|
LIMIT p_limit;
|
|
|
|
-- Contract search by name
|
|
RETURN QUERY
|
|
SELECT
|
|
'contract'::TEXT as type,
|
|
NULL::BYTEA as hash,
|
|
sc.name,
|
|
NULL::TEXT as symbol,
|
|
NULL::BIGINT as block_number,
|
|
sc.address_hash as address_hash,
|
|
NULL::TEXT as token_type
|
|
FROM smart_contracts sc
|
|
WHERE sc.chain_id = p_chain_id
|
|
AND sc.name ILIKE '%' || p_query || '%'
|
|
LIMIT p_limit;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Gap Detection Functions
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Find gaps in indexed blocks
|
|
CREATE OR REPLACE FUNCTION find_block_gaps(
|
|
p_chain_id BIGINT,
|
|
p_min_block BIGINT DEFAULT 0,
|
|
p_max_block BIGINT DEFAULT NULL
|
|
)
|
|
RETURNS TABLE (
|
|
gap_start BIGINT,
|
|
gap_end BIGINT
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
WITH block_numbers AS (
|
|
SELECT number
|
|
FROM blocks
|
|
WHERE chain_id = p_chain_id
|
|
AND number >= p_min_block
|
|
AND (p_max_block IS NULL OR number <= p_max_block)
|
|
ORDER BY number
|
|
),
|
|
gaps AS (
|
|
SELECT
|
|
number + 1 as gap_start,
|
|
LEAD(number) OVER (ORDER BY number) - 1 as gap_end
|
|
FROM block_numbers
|
|
)
|
|
SELECT g.gap_start, g.gap_end
|
|
FROM gaps g
|
|
WHERE g.gap_end IS NOT NULL
|
|
AND g.gap_end >= g.gap_start
|
|
ORDER BY g.gap_start;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Maintenance Functions
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Vacuum analyze all partitions for a chain
|
|
CREATE OR REPLACE FUNCTION maintain_chain_partitions(p_chain_id BIGINT)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_table_name TEXT;
|
|
BEGIN
|
|
FOR v_table_name IN
|
|
SELECT tablename
|
|
FROM pg_tables
|
|
WHERE tablename LIKE '%_' || p_chain_id
|
|
AND schemaname = 'public'
|
|
LOOP
|
|
EXECUTE format('ANALYZE %I', v_table_name);
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Clean up old pending transactions
|
|
CREATE OR REPLACE FUNCTION cleanup_stale_pending_txs(
|
|
p_chain_id BIGINT,
|
|
p_max_age INTERVAL DEFAULT '1 hour'
|
|
)
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted INTEGER;
|
|
BEGIN
|
|
DELETE FROM transactions
|
|
WHERE chain_id = p_chain_id
|
|
AND status IS NULL
|
|
AND inserted_at < NOW() - p_max_age;
|
|
|
|
GET DIAGNOSTICS v_deleted = ROW_COUNT;
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Helper Functions for API
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Format address as 0x-prefixed hex string
|
|
CREATE OR REPLACE FUNCTION format_address(p_bytes BYTEA)
|
|
RETURNS TEXT AS $$
|
|
BEGIN
|
|
RETURN '0x' || encode(p_bytes, 'hex');
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
-- Format hash as 0x-prefixed hex string
|
|
CREATE OR REPLACE FUNCTION format_hash(p_bytes BYTEA)
|
|
RETURNS TEXT AS $$
|
|
BEGIN
|
|
RETURN '0x' || encode(p_bytes, 'hex');
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
-- Parse hex string to bytea (with or without 0x prefix)
|
|
CREATE OR REPLACE FUNCTION parse_hex(p_hex TEXT)
|
|
RETURNS BYTEA AS $$
|
|
BEGIN
|
|
IF LEFT(p_hex, 2) = '0x' OR LEFT(p_hex, 2) = '0X' THEN
|
|
RETURN decode(substring(p_hex from 3), 'hex');
|
|
ELSE
|
|
RETURN decode(p_hex, 'hex');
|
|
END IF;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
-- Calculate effective gas price for EIP-1559 transactions
|
|
CREATE OR REPLACE FUNCTION calc_effective_gas_price(
|
|
p_base_fee NUMERIC,
|
|
p_max_fee NUMERIC,
|
|
p_max_priority_fee NUMERIC,
|
|
p_gas_price NUMERIC
|
|
)
|
|
RETURNS NUMERIC AS $$
|
|
BEGIN
|
|
-- For legacy transactions
|
|
IF p_gas_price IS NOT NULL AND p_max_fee IS NULL THEN
|
|
RETURN p_gas_price;
|
|
END IF;
|
|
|
|
-- For EIP-1559 transactions
|
|
IF p_base_fee IS NOT NULL AND p_max_fee IS NOT NULL THEN
|
|
RETURN LEAST(
|
|
p_max_fee,
|
|
p_base_fee + COALESCE(p_max_priority_fee, 0)
|
|
);
|
|
END IF;
|
|
|
|
RETURN p_gas_price;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Statistics Aggregation Functions
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Get daily transaction count for charts
|
|
CREATE OR REPLACE FUNCTION get_daily_tx_count(
|
|
p_chain_id BIGINT,
|
|
p_days INTEGER DEFAULT 30
|
|
)
|
|
RETURNS TABLE (
|
|
date DATE,
|
|
tx_count BIGINT
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
DATE(to_timestamp(block_timestamp)) as date,
|
|
COUNT(*)::BIGINT as tx_count
|
|
FROM transactions
|
|
WHERE chain_id = p_chain_id
|
|
AND block_timestamp >= EXTRACT(EPOCH FROM NOW() - (p_days || ' days')::INTERVAL)
|
|
GROUP BY DATE(to_timestamp(block_timestamp))
|
|
ORDER BY date DESC;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Get daily new addresses count
|
|
CREATE OR REPLACE FUNCTION get_daily_new_addresses(
|
|
p_chain_id BIGINT,
|
|
p_days INTEGER DEFAULT 30
|
|
)
|
|
RETURNS TABLE (
|
|
date DATE,
|
|
address_count BIGINT
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
DATE(inserted_at) as date,
|
|
COUNT(*)::BIGINT as address_count
|
|
FROM addresses
|
|
WHERE chain_id = p_chain_id
|
|
AND inserted_at >= NOW() - (p_days || ' days')::INTERVAL
|
|
GROUP BY DATE(inserted_at)
|
|
ORDER BY date DESC;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|