package analytics import ( "context" "fmt" "strings" "time" "github.com/jackc/pgx/v5/pgxpool" ) // BridgeAnalytics provides bridge analytics type BridgeAnalytics struct { db *pgxpool.Pool } // NewBridgeAnalytics creates a new bridge analytics instance func NewBridgeAnalytics(db *pgxpool.Pool) *BridgeAnalytics { return &BridgeAnalytics{db: db} } // BridgeStats represents bridge statistics type BridgeStats struct { Transfers24h int Volume24h string Chains map[int]ChainStats TopTokens []TokenStats } // ChainStats represents chain statistics type ChainStats struct { Outbound int Inbound int VolumeOut string VolumeIn string } // TokenStats represents token statistics type TokenStats struct { Token string Symbol string Transfers int Volume string } // GetBridgeStats gets bridge statistics func (ba *BridgeAnalytics) GetBridgeStats(ctx context.Context, chainFrom, chainTo *int, startDate, endDate *time.Time) (*BridgeStats, error) { clauses := []string{"timestamp >= NOW() - INTERVAL '24 hours'"} args := []interface{}{} argIndex := 1 if chainFrom != nil { clauses = append(clauses, fmt.Sprintf("chain_from = $%d", argIndex)) args = append(args, *chainFrom) argIndex++ } if chainTo != nil { clauses = append(clauses, fmt.Sprintf("chain_to = $%d", argIndex)) args = append(args, *chainTo) argIndex++ } if startDate != nil { clauses = append(clauses, fmt.Sprintf("timestamp >= $%d", argIndex)) args = append(args, *startDate) argIndex++ } if endDate != nil { clauses = append(clauses, fmt.Sprintf("timestamp <= $%d", argIndex)) args = append(args, *endDate) argIndex++ } filteredCTE := fmt.Sprintf(` WITH filtered AS ( SELECT chain_from, chain_to, token_contract, amount FROM analytics_bridge_history WHERE %s ) `, strings.Join(clauses, " AND ")) var transfers24h int var volume24h string err := ba.db.QueryRow(ctx, filteredCTE+` SELECT COUNT(*) as transfers_24h, COALESCE(SUM(amount)::text, '0') as volume_24h FROM filtered `, args...).Scan(&transfers24h, &volume24h) if err != nil { return nil, fmt.Errorf("failed to get bridge stats: %w", err) } stats := &BridgeStats{ Transfers24h: transfers24h, Volume24h: volume24h, Chains: make(map[int]ChainStats), TopTokens: []TokenStats{}, } rows, err := ba.db.Query(ctx, filteredCTE+` SELECT chain_id, SUM(outbound) as outbound, SUM(inbound) as inbound, COALESCE(SUM(volume_out)::text, '0') as volume_out, COALESCE(SUM(volume_in)::text, '0') as volume_in FROM ( SELECT chain_from AS chain_id, 1 AS outbound, 0 AS inbound, amount AS volume_out, 0::numeric AS volume_in FROM filtered UNION ALL SELECT chain_to AS chain_id, 0 AS outbound, 1 AS inbound, 0::numeric AS volume_out, amount AS volume_in FROM filtered ) chain_rollup GROUP BY chain_id ORDER BY chain_id `, args...) if err != nil { return nil, fmt.Errorf("failed to get chain breakdown: %w", err) } defer rows.Close() for rows.Next() { var chainID, outbound, inbound int var volumeOut, volumeIn string if err := rows.Scan(&chainID, &outbound, &inbound, &volumeOut, &volumeIn); err == nil { stats.Chains[chainID] = ChainStats{ Outbound: outbound, Inbound: inbound, VolumeOut: volumeOut, VolumeIn: volumeIn, } } } tokenRows, err := ba.db.Query(ctx, filteredCTE+` SELECT token_contract, COUNT(*) as transfers, COALESCE(SUM(amount)::text, '0') as volume FROM filtered WHERE token_contract IS NOT NULL AND token_contract <> '' GROUP BY token_contract ORDER BY transfers DESC, volume DESC LIMIT 10 `, args...) if err != nil { return nil, fmt.Errorf("failed to get top bridge tokens: %w", err) } defer tokenRows.Close() for tokenRows.Next() { var token TokenStats if err := tokenRows.Scan(&token.Token, &token.Transfers, &token.Volume); err != nil { continue } stats.TopTokens = append(stats.TopTokens, token) } return stats, nil }