Files
zeekayandHanzo Dev 64519916bf the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:01:04 -07:00

726 lines
20 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
// Package stats provides blockchain statistics, counters, and caching functionality.
// Implements gas price oracle, block time calculations, and aggregate metrics.
package stats
import (
"context"
"database/sql"
"fmt"
"math/big"
"sort"
"sync"
"time"
)
// GasPrice represents gas price for a fee tier
type GasPrice struct {
Price float64 `json:"price"` // Gas price in Gwei
PriorityFee float64 `json:"priority_fee"` // Priority fee in Gwei
BaseFee float64 `json:"base_fee"` // Base fee in Gwei
Time float64 `json:"time"` // Estimated confirmation time in ms
FiatPrice float64 `json:"fiat_price"` // Fiat price for simple tx
Wei string `json:"wei"` // Full price in Wei
}
// GasPrices represents slow/average/fast gas price estimates
type GasPrices struct {
Slow *GasPrice `json:"slow"`
Average *GasPrice `json:"average"`
Fast *GasPrice `json:"fast"`
UpdatedAt time.Time `json:"updated_at"`
BaseFee string `json:"base_fee"` // Current base fee
GasUsedRatio float64 `json:"gas_used_ratio"` // Recent block gas usage
}
// Counter represents an aggregate counter value
type Counter struct {
Name string `json:"name"`
Value int64 `json:"value"`
UpdatedAt time.Time `json:"updated_at"`
}
// Counters holds all aggregate statistics
type Counters struct {
TotalBlocks int64 `json:"total_blocks"`
TotalTransactions int64 `json:"total_transactions"`
TotalAddresses int64 `json:"total_addresses"`
TotalContracts int64 `json:"total_contracts"`
TotalTokens int64 `json:"total_tokens"`
TotalTokenTransfers int64 `json:"total_token_transfers"`
VerifiedContracts int64 `json:"verified_contracts"`
AverageBlockTime float64 `json:"average_block_time_seconds"`
TPS24h float64 `json:"tps_24h"`
GasUsed24h int64 `json:"gas_used_24h"`
UpdatedAt time.Time `json:"updated_at"`
}
// BlockTimeStats contains block time statistics
type BlockTimeStats struct {
AverageBlockTime time.Duration `json:"average_block_time"`
MinBlockTime time.Duration `json:"min_block_time"`
MaxBlockTime time.Duration `json:"max_block_time"`
BlocksAnalyzed int `json:"blocks_analyzed"`
UpdatedAt time.Time `json:"updated_at"`
}
// TokenStats contains token-specific statistics
type TokenStats struct {
Address string `json:"address"`
HolderCount int64 `json:"holder_count"`
TransferCount int64 `json:"transfer_count"`
UpdatedAt time.Time `json:"updated_at"`
}
// AddressStats contains address-specific statistics
type AddressStats struct {
Address string `json:"address"`
TransactionCount int64 `json:"transaction_count"`
TokenTransferCount int64 `json:"token_transfer_count"`
InternalTxCount int64 `json:"internal_tx_count"`
GasUsed int64 `json:"gas_used"`
Balance string `json:"balance"`
}
// Service provides statistics and caching functionality
type Service struct {
db *sql.DB
cache *Cache
updateMutex sync.Mutex
config Config
prefix string // table name prefix, e.g. "cchain", "zoo", "hanzo"
}
// Tbl returns the prefixed table name, e.g. Tbl("transactions") returns "cchain_transactions"
func (s *Service) Tbl(name string) string {
return s.prefix + "_" + name
}
// Config holds statistics service configuration
type Config struct {
CacheTTL time.Duration
GasPriceBlocks int // Number of blocks to analyze for gas prices
SlowPercentile float64 // Percentile for slow gas price
AveragePercentile float64 // Percentile for average gas price
FastPercentile float64 // Percentile for fast gas price
SimpleTransactionGas int64 // Gas for simple ETH transfer
CounterUpdateInterval time.Duration
BlockTimeBlocks int // Number of blocks for block time calculation
}
// DefaultConfig returns sensible defaults
func DefaultConfig() Config {
return Config{
CacheTTL: 5 * time.Minute,
GasPriceBlocks: 200,
SlowPercentile: 35.0,
AveragePercentile: 60.0,
FastPercentile: 90.0,
SimpleTransactionGas: 21000,
CounterUpdateInterval: 30 * time.Second,
BlockTimeBlocks: 100,
}
}
// Cache provides in-memory caching for statistics
type Cache struct {
mu sync.RWMutex
gasPrices *GasPrices
counters *Counters
blockTime *BlockTimeStats
tokenStats map[string]*TokenStats
lastUpdated time.Time
ttl time.Duration
}
// NewCache creates a new cache with specified TTL
func NewCache(ttl time.Duration) *Cache {
return &Cache{
tokenStats: make(map[string]*TokenStats),
ttl: ttl,
}
}
// NewService creates a new statistics service.
// Defaults to "cchain" prefix when chainSlug is omitted.
func NewService(db *sql.DB, config Config, chainSlug ...string) *Service {
prefix := "cchain"
if len(chainSlug) > 0 && chainSlug[0] != "" {
prefix = chainSlug[0]
}
return &Service{
db: db,
cache: NewCache(config.CacheTTL),
config: config,
prefix: prefix,
}
}
// GetGasPrices returns current gas price estimates
func (s *Service) GetGasPrices(ctx context.Context) (*GasPrices, error) {
s.cache.mu.RLock()
if s.cache.gasPrices != nil && time.Since(s.cache.lastUpdated) < s.cache.ttl {
prices := s.cache.gasPrices
s.cache.mu.RUnlock()
return prices, nil
}
s.cache.mu.RUnlock()
return s.updateGasPrices(ctx)
}
// updateGasPrices calculates and caches gas prices
func (s *Service) updateGasPrices(ctx context.Context) (*GasPrices, error) {
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
// Double-check cache after acquiring lock
s.cache.mu.RLock()
if s.cache.gasPrices != nil && time.Since(s.cache.lastUpdated) < s.cache.ttl {
prices := s.cache.gasPrices
s.cache.mu.RUnlock()
return prices, nil
}
s.cache.mu.RUnlock()
// Get recent transactions with gas prices
rows, err := s.db.QueryContext(ctx, fmt.Sprintf(`
SELECT t.gas_price, t.gas_used, b.timestamp
FROM %s t
JOIN blocks b ON b.height = t.block_number
WHERE t.gas_price IS NOT NULL AND t.gas_price != '0'
ORDER BY t.block_number DESC
LIMIT $1
`, s.Tbl("transactions")), s.config.GasPriceBlocks*50) // Assume ~50 txs per block
if err != nil {
return nil, fmt.Errorf("query gas prices: %w", err)
}
defer rows.Close()
type txGas struct {
gasPrice *big.Int
gasUsed uint64
timestamp time.Time
}
var txGasData []txGas
for rows.Next() {
var gasPriceStr string
var gasUsed uint64
var timestamp time.Time
if err := rows.Scan(&gasPriceStr, &gasUsed, &timestamp); err != nil {
continue
}
gasPrice := new(big.Int)
gasPrice.SetString(gasPriceStr, 10)
if gasPrice.Sign() > 0 {
txGasData = append(txGasData, txGas{
gasPrice: gasPrice,
gasUsed: gasUsed,
timestamp: timestamp,
})
}
}
if len(txGasData) == 0 {
return &GasPrices{
Slow: nil,
Average: nil,
Fast: nil,
UpdatedAt: time.Now(),
}, nil
}
// Sort by gas price
sort.Slice(txGasData, func(i, j int) bool {
return txGasData[i].gasPrice.Cmp(txGasData[j].gasPrice) < 0
})
// Calculate percentiles
slowIdx := int(float64(len(txGasData)) * s.config.SlowPercentile / 100)
avgIdx := int(float64(len(txGasData)) * s.config.AveragePercentile / 100)
fastIdx := int(float64(len(txGasData)) * s.config.FastPercentile / 100)
if slowIdx >= len(txGasData) {
slowIdx = len(txGasData) - 1
}
if avgIdx >= len(txGasData) {
avgIdx = len(txGasData) - 1
}
if fastIdx >= len(txGasData) {
fastIdx = len(txGasData) - 1
}
// Get base fee from latest block
var baseFeeStr sql.NullString
err = s.db.QueryRowContext(ctx, `
SELECT metadata->>'baseFee'
FROM blocks
ORDER BY height DESC
LIMIT 1
`).Scan(&baseFeeStr)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("query base fee: %w", err)
}
baseFee := new(big.Int)
if baseFeeStr.Valid && baseFeeStr.String != "" {
baseFee.SetString(baseFeeStr.String, 10)
}
// Get average block time
blockTime, _ := s.GetAverageBlockTime(ctx)
avgBlockTimeMs := float64(blockTime.AverageBlockTime.Milliseconds())
// Create gas prices
prices := &GasPrices{
Slow: createGasPrice(txGasData[slowIdx].gasPrice, baseFee, avgBlockTimeMs*2.5, s.config.SimpleTransactionGas),
Average: createGasPrice(txGasData[avgIdx].gasPrice, baseFee, avgBlockTimeMs*1.5, s.config.SimpleTransactionGas),
Fast: createGasPrice(txGasData[fastIdx].gasPrice, baseFee, avgBlockTimeMs*0.8, s.config.SimpleTransactionGas),
UpdatedAt: time.Now(),
BaseFee: weiToGwei(baseFee),
}
// Calculate gas used ratio
var gasUsed, gasLimit int64
err = s.db.QueryRowContext(ctx, `
SELECT COALESCE(SUM((metadata->>'gasUsed')::bigint), 0),
COALESCE(SUM((metadata->>'gasLimit')::bigint), 1)
FROM blocks
WHERE height > (SELECT MAX(height) - 10 FROM blocks)
`).Scan(&gasUsed, &gasLimit)
if err == nil && gasLimit > 0 {
prices.GasUsedRatio = float64(gasUsed) / float64(gasLimit)
}
// Update cache
s.cache.mu.Lock()
s.cache.gasPrices = prices
s.cache.lastUpdated = time.Now()
s.cache.mu.Unlock()
return prices, nil
}
// createGasPrice creates a GasPrice from wei values
func createGasPrice(gasPrice, baseFee *big.Int, estimatedTimeMs float64, simpleGas int64) *GasPrice {
priceGwei := weiToGweiFloat(gasPrice)
baseFeeGwei := weiToGweiFloat(baseFee)
priorityFee := priceGwei - baseFeeGwei
if priorityFee < 0 {
priorityFee = 0
}
fiatPrice := priceGwei * float64(simpleGas) * 0.001
return &GasPrice{
Price: priceGwei,
PriorityFee: priorityFee,
BaseFee: baseFeeGwei,
Time: estimatedTimeMs,
FiatPrice: fiatPrice,
Wei: gasPrice.String(),
}
}
// weiToGwei converts wei to gwei string
func weiToGwei(wei *big.Int) string {
if wei == nil || wei.Sign() == 0 {
return "0"
}
gwei := new(big.Float).SetInt(wei)
gwei.Quo(gwei, big.NewFloat(1e9))
return gwei.Text('f', 6)
}
// weiToGweiFloat converts wei to gwei float
func weiToGweiFloat(wei *big.Int) float64 {
if wei == nil || wei.Sign() == 0 {
return 0
}
gwei := new(big.Float).SetInt(wei)
gwei.Quo(gwei, big.NewFloat(1e9))
result, _ := gwei.Float64()
return result
}
// GetCounters returns aggregate statistics
func (s *Service) GetCounters(ctx context.Context) (*Counters, error) {
s.cache.mu.RLock()
if s.cache.counters != nil && time.Since(s.cache.counters.UpdatedAt) < s.config.CounterUpdateInterval {
counters := s.cache.counters
s.cache.mu.RUnlock()
return counters, nil
}
s.cache.mu.RUnlock()
return s.updateCounters(ctx)
}
// updateCounters calculates and caches aggregate counters
func (s *Service) updateCounters(ctx context.Context) (*Counters, error) {
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
counters := &Counters{UpdatedAt: time.Now()}
// Total blocks
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM blocks`).Scan(&counters.TotalBlocks)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count blocks: %w", err)
}
// Total transactions
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s`, s.Tbl("transactions"))).Scan(&counters.TotalTransactions)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count transactions: %w", err)
}
// Total addresses
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s`, s.Tbl("addresses"))).Scan(&counters.TotalAddresses)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count addresses: %w", err)
}
// Total contracts
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COUNT(*) FROM %s WHERE is_contract = true
`, s.Tbl("addresses"))).Scan(&counters.TotalContracts)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count contracts: %w", err)
}
// Total tokens
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s`, s.Tbl("tokens"))).Scan(&counters.TotalTokens)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count tokens: %w", err)
}
// Total token transfers
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s`, s.Tbl("token_transfers"))).Scan(&counters.TotalTokenTransfers)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count token transfers: %w", err)
}
// TPS 24h
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COUNT(*)::float / 86400.0
FROM %s
WHERE timestamp > NOW() - INTERVAL '24 hours'
`, s.Tbl("transactions"))).Scan(&counters.TPS24h)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("calculate tps: %w", err)
}
// Gas used 24h
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COALESCE(SUM(gas_used), 0)
FROM %s
WHERE timestamp > NOW() - INTERVAL '24 hours'
`, s.Tbl("transactions"))).Scan(&counters.GasUsed24h)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("sum gas used: %w", err)
}
// Average block time
blockTime, err := s.GetAverageBlockTime(ctx)
if err == nil {
counters.AverageBlockTime = blockTime.AverageBlockTime.Seconds()
}
// Update cache
s.cache.mu.Lock()
s.cache.counters = counters
s.cache.mu.Unlock()
return counters, nil
}
// GetAverageBlockTime returns block time statistics
func (s *Service) GetAverageBlockTime(ctx context.Context) (*BlockTimeStats, error) {
s.cache.mu.RLock()
if s.cache.blockTime != nil && time.Since(s.cache.blockTime.UpdatedAt) < s.cache.ttl {
stats := s.cache.blockTime
s.cache.mu.RUnlock()
return stats, nil
}
s.cache.mu.RUnlock()
return s.updateBlockTime(ctx)
}
// updateBlockTime calculates block time statistics
func (s *Service) updateBlockTime(ctx context.Context) (*BlockTimeStats, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT timestamp
FROM blocks
ORDER BY height DESC
LIMIT $1
`, s.config.BlockTimeBlocks+1)
if err != nil {
return nil, fmt.Errorf("query blocks: %w", err)
}
defer rows.Close()
var timestamps []time.Time
for rows.Next() {
var ts time.Time
if err := rows.Scan(&ts); err != nil {
continue
}
timestamps = append(timestamps, ts)
}
if len(timestamps) < 2 {
return &BlockTimeStats{
AverageBlockTime: 2 * time.Second, // Default estimate
UpdatedAt: time.Now(),
}, nil
}
var totalDiff time.Duration
var minDiff, maxDiff time.Duration
minDiff = time.Hour // Start with large value
count := 0
for i := 0; i < len(timestamps)-1; i++ {
diff := timestamps[i].Sub(timestamps[i+1])
if diff < 0 {
diff = -diff
}
if diff > 0 {
totalDiff += diff
if diff < minDiff {
minDiff = diff
}
if diff > maxDiff {
maxDiff = diff
}
count++
}
}
avgDiff := time.Duration(0)
if count > 0 {
avgDiff = totalDiff / time.Duration(count)
}
stats := &BlockTimeStats{
AverageBlockTime: avgDiff,
MinBlockTime: minDiff,
MaxBlockTime: maxDiff,
BlocksAnalyzed: count,
UpdatedAt: time.Now(),
}
s.cache.mu.Lock()
s.cache.blockTime = stats
s.cache.mu.Unlock()
return stats, nil
}
// GetTokenStats returns statistics for a specific token
func (s *Service) GetTokenStats(ctx context.Context, tokenAddress string) (*TokenStats, error) {
tokenAddress = normalizeAddress(tokenAddress)
s.cache.mu.RLock()
if stats, ok := s.cache.tokenStats[tokenAddress]; ok {
if time.Since(stats.UpdatedAt) < s.cache.ttl {
s.cache.mu.RUnlock()
return stats, nil
}
}
s.cache.mu.RUnlock()
stats := &TokenStats{
Address: tokenAddress,
UpdatedAt: time.Now(),
}
// Get holder count
err := s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT holder_count FROM %s WHERE address = $1
`, s.Tbl("tokens")), tokenAddress).Scan(&stats.HolderCount)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("get holder count: %w", err)
}
// Get transfer count
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COUNT(*) FROM %s WHERE token_address = $1
`, s.Tbl("token_transfers")), tokenAddress).Scan(&stats.TransferCount)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("get transfer count: %w", err)
}
s.cache.mu.Lock()
s.cache.tokenStats[tokenAddress] = stats
s.cache.mu.Unlock()
return stats, nil
}
// GetAddressStats returns statistics for a specific address
func (s *Service) GetAddressStats(ctx context.Context, address string) (*AddressStats, error) {
address = normalizeAddress(address)
stats := &AddressStats{Address: address}
// Get transaction count
err := s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COUNT(*) FROM %s
WHERE tx_from = $1 OR tx_to = $1
`, s.Tbl("transactions")), address).Scan(&stats.TransactionCount)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count transactions: %w", err)
}
// Get token transfer count
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COUNT(*) FROM %s
WHERE tx_from = $1 OR tx_to = $1
`, s.Tbl("token_transfers")), address).Scan(&stats.TokenTransferCount)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("count token transfers: %w", err)
}
// Get internal tx count
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COUNT(*) FROM %s
WHERE tx_from = $1 OR tx_to = $1
`, s.Tbl("internal_transactions")), address).Scan(&stats.InternalTxCount)
if err != nil && err != sql.ErrNoRows {
// Table might not exist, continue
}
// Get total gas used
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COALESCE(SUM(gas_used), 0) FROM %s
WHERE tx_from = $1
`, s.Tbl("transactions")), address).Scan(&stats.GasUsed)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("sum gas used: %w", err)
}
// Get balance
err = s.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT balance FROM %s WHERE hash = $1
`, s.Tbl("addresses")), address).Scan(&stats.Balance)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("get balance: %w", err)
}
return stats, nil
}
// GetDailyTransactionStats returns daily transaction counts
func (s *Service) GetDailyTransactionStats(ctx context.Context, days int) ([]DailyStats, error) {
if days <= 0 {
days = 30
}
if days > 365 {
days = 365
}
rows, err := s.db.QueryContext(ctx, fmt.Sprintf(`
SELECT DATE(timestamp) as date, COUNT(*) as count
FROM %s
WHERE timestamp > NOW() - make_interval(days => $1)
GROUP BY DATE(timestamp)
ORDER BY date DESC
`, s.Tbl("transactions")), days)
if err != nil {
return nil, fmt.Errorf("query daily stats: %w", err)
}
defer rows.Close()
var stats []DailyStats
for rows.Next() {
var stat DailyStats
if err := rows.Scan(&stat.Date, &stat.Count); err != nil {
continue
}
stats = append(stats, stat)
}
return stats, nil
}
// DailyStats represents daily count statistics
type DailyStats struct {
Date time.Time `json:"date"`
Count int64 `json:"count"`
}
// GetHourlyGasUsage returns hourly gas usage for the last 24 hours
func (s *Service) GetHourlyGasUsage(ctx context.Context) ([]HourlyStats, error) {
rows, err := s.db.QueryContext(ctx, fmt.Sprintf(`
SELECT DATE_TRUNC('hour', timestamp) as hour, COALESCE(SUM(gas_used), 0) as gas_used
FROM %s
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY DATE_TRUNC('hour', timestamp)
ORDER BY hour DESC
`, s.Tbl("transactions")))
if err != nil {
return nil, fmt.Errorf("query hourly gas: %w", err)
}
defer rows.Close()
var stats []HourlyStats
for rows.Next() {
var stat HourlyStats
if err := rows.Scan(&stat.Hour, &stat.Value); err != nil {
continue
}
stats = append(stats, stat)
}
return stats, nil
}
// HourlyStats represents hourly statistics
type HourlyStats struct {
Hour time.Time `json:"hour"`
Value int64 `json:"value"`
}
// InvalidateCache clears all cached data
func (s *Service) InvalidateCache() {
s.cache.mu.Lock()
defer s.cache.mu.Unlock()
s.cache.gasPrices = nil
s.cache.counters = nil
s.cache.blockTime = nil
s.cache.tokenStats = make(map[string]*TokenStats)
}
// StartBackgroundUpdater starts a background goroutine to update stats periodically
func (s *Service) StartBackgroundUpdater(ctx context.Context) {
ticker := time.NewTicker(s.config.CounterUpdateInterval)
go func() {
for {
select {
case <-ctx.Done():
ticker.Stop()
return
case <-ticker.C:
s.updateCounters(ctx)
s.updateGasPrices(ctx)
s.updateBlockTime(ctx)
}
}
}()
}
// normalizeAddress converts address to lowercase with 0x prefix
func normalizeAddress(address string) string {
if len(address) >= 2 && (address[:2] == "0x" || address[:2] == "0X") {
return "0x" + address[2:]
}
return "0x" + address
}