Freshness diagnostics API, UI trust notes, mission control/stats updates, and deploy scripts.
Made-with: Cursor
This commit is contained in:
@@ -2,62 +2,190 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/explorer/backend/api/freshness"
|
||||
)
|
||||
|
||||
type explorerStats struct {
|
||||
TotalBlocks int64 `json:"total_blocks"`
|
||||
TotalTransactions int64 `json:"total_transactions"`
|
||||
TotalAddresses int64 `json:"total_addresses"`
|
||||
LatestBlock int64 `json:"latest_block"`
|
||||
TotalBlocks int64 `json:"total_blocks"`
|
||||
TotalTransactions int64 `json:"total_transactions"`
|
||||
TotalAddresses int64 `json:"total_addresses"`
|
||||
LatestBlock int64 `json:"latest_block"`
|
||||
AverageBlockTime *float64 `json:"average_block_time,omitempty"`
|
||||
GasPrices *explorerGasPrices `json:"gas_prices,omitempty"`
|
||||
NetworkUtilizationPercentage *float64 `json:"network_utilization_percentage,omitempty"`
|
||||
TransactionsToday *int64 `json:"transactions_today,omitempty"`
|
||||
Freshness freshness.Snapshot `json:"freshness"`
|
||||
Completeness freshness.SummaryCompleteness `json:"completeness"`
|
||||
Sampling freshness.Sampling `json:"sampling"`
|
||||
}
|
||||
|
||||
type statsQueryFunc func(ctx context.Context, sql string, args ...any) pgx.Row
|
||||
type explorerGasPrices struct {
|
||||
Average *float64 `json:"average,omitempty"`
|
||||
}
|
||||
|
||||
type statsQueryFunc = freshness.QueryRowFunc
|
||||
|
||||
func queryNullableFloat64(ctx context.Context, queryRow statsQueryFunc, query string, args ...any) (*float64, error) {
|
||||
var value sql.NullFloat64
|
||||
if err := queryRow(ctx, query, args...).Scan(&value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !value.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return &value.Float64, nil
|
||||
}
|
||||
|
||||
func queryNullableInt64(ctx context.Context, queryRow statsQueryFunc, query string, args ...any) (*int64, error) {
|
||||
var value sql.NullInt64
|
||||
if err := queryRow(ctx, query, args...).Scan(&value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !value.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return &value.Int64, nil
|
||||
}
|
||||
|
||||
func loadExplorerStats(ctx context.Context, chainID int, queryRow statsQueryFunc) (explorerStats, error) {
|
||||
var stats explorerStats
|
||||
_ = chainID
|
||||
|
||||
if err := queryRow(ctx,
|
||||
`SELECT COUNT(*) FROM blocks WHERE chain_id = $1`,
|
||||
chainID,
|
||||
`SELECT COUNT(*) FROM blocks`,
|
||||
).Scan(&stats.TotalBlocks); err != nil {
|
||||
return explorerStats{}, fmt.Errorf("query total blocks: %w", err)
|
||||
}
|
||||
|
||||
if err := queryRow(ctx,
|
||||
`SELECT COUNT(*) FROM transactions WHERE chain_id = $1`,
|
||||
chainID,
|
||||
`SELECT COUNT(*) FROM transactions WHERE block_hash IS NOT NULL`,
|
||||
).Scan(&stats.TotalTransactions); err != nil {
|
||||
return explorerStats{}, fmt.Errorf("query total transactions: %w", err)
|
||||
}
|
||||
|
||||
if err := queryRow(ctx,
|
||||
`SELECT COUNT(*) FROM (
|
||||
SELECT from_address AS address
|
||||
SELECT from_address_hash AS address
|
||||
FROM transactions
|
||||
WHERE chain_id = $1 AND from_address IS NOT NULL AND from_address <> ''
|
||||
WHERE from_address_hash IS NOT NULL
|
||||
UNION
|
||||
SELECT to_address AS address
|
||||
SELECT to_address_hash AS address
|
||||
FROM transactions
|
||||
WHERE chain_id = $1 AND to_address IS NOT NULL AND to_address <> ''
|
||||
WHERE to_address_hash IS NOT NULL
|
||||
) unique_addresses`,
|
||||
chainID,
|
||||
).Scan(&stats.TotalAddresses); err != nil {
|
||||
return explorerStats{}, fmt.Errorf("query total addresses: %w", err)
|
||||
}
|
||||
|
||||
if err := queryRow(ctx,
|
||||
`SELECT COALESCE(MAX(number), 0) FROM blocks WHERE chain_id = $1`,
|
||||
chainID,
|
||||
`SELECT COALESCE(MAX(number), 0) FROM blocks`,
|
||||
).Scan(&stats.LatestBlock); err != nil {
|
||||
return explorerStats{}, fmt.Errorf("query latest block: %w", err)
|
||||
}
|
||||
|
||||
statsIssues := map[string]string{}
|
||||
|
||||
averageBlockTime, err := queryNullableFloat64(ctx, queryRow,
|
||||
`SELECT CASE
|
||||
WHEN COUNT(*) >= 2
|
||||
THEN (EXTRACT(EPOCH FROM (MAX(timestamp) - MIN(timestamp))) * 1000.0) / NULLIF(COUNT(*) - 1, 0)
|
||||
ELSE NULL
|
||||
END
|
||||
FROM (
|
||||
SELECT timestamp
|
||||
FROM blocks
|
||||
ORDER BY number DESC
|
||||
LIMIT 100
|
||||
) recent_blocks`,
|
||||
)
|
||||
if err != nil {
|
||||
statsIssues["average_block_time"] = err.Error()
|
||||
} else {
|
||||
stats.AverageBlockTime = averageBlockTime
|
||||
}
|
||||
|
||||
averageGasPrice, err := queryNullableFloat64(ctx, queryRow,
|
||||
`SELECT AVG(gas_price_wei)::double precision / 1000000000.0
|
||||
FROM (
|
||||
SELECT gas_price AS gas_price_wei
|
||||
FROM transactions
|
||||
WHERE block_hash IS NOT NULL
|
||||
AND gas_price IS NOT NULL
|
||||
ORDER BY block_number DESC, "index" DESC
|
||||
LIMIT 1000
|
||||
) recent_transactions`,
|
||||
)
|
||||
if err != nil {
|
||||
statsIssues["average_gas_price"] = err.Error()
|
||||
} else if averageGasPrice != nil {
|
||||
stats.GasPrices = &explorerGasPrices{Average: averageGasPrice}
|
||||
}
|
||||
|
||||
networkUtilization, err := queryNullableFloat64(ctx, queryRow,
|
||||
`SELECT AVG((gas_used::double precision / NULLIF(gas_limit, 0)) * 100.0)
|
||||
FROM (
|
||||
SELECT gas_used, gas_limit
|
||||
FROM blocks
|
||||
WHERE gas_limit IS NOT NULL
|
||||
AND gas_limit > 0
|
||||
ORDER BY number DESC
|
||||
LIMIT 100
|
||||
) recent_blocks`,
|
||||
)
|
||||
if err != nil {
|
||||
statsIssues["network_utilization_percentage"] = err.Error()
|
||||
} else {
|
||||
stats.NetworkUtilizationPercentage = networkUtilization
|
||||
}
|
||||
|
||||
transactionsToday, err := queryNullableInt64(ctx, queryRow,
|
||||
`SELECT COUNT(*)::bigint
|
||||
FROM transactions t
|
||||
JOIN blocks b
|
||||
ON b.number = t.block_number
|
||||
WHERE b.timestamp >= NOW() - INTERVAL '24 hours'`,
|
||||
)
|
||||
if err != nil {
|
||||
statsIssues["transactions_today"] = err.Error()
|
||||
} else {
|
||||
stats.TransactionsToday = transactionsToday
|
||||
}
|
||||
|
||||
rpcURL := strings.TrimSpace(os.Getenv("RPC_URL"))
|
||||
snapshot, completeness, sampling, err := freshness.BuildSnapshot(
|
||||
ctx,
|
||||
chainID,
|
||||
queryRow,
|
||||
func(ctx context.Context) (*freshness.Reference, error) {
|
||||
return freshness.ProbeChainHead(ctx, rpcURL)
|
||||
},
|
||||
time.Now().UTC(),
|
||||
averageGasPrice,
|
||||
networkUtilization,
|
||||
)
|
||||
if err != nil {
|
||||
return explorerStats{}, fmt.Errorf("build freshness snapshot: %w", err)
|
||||
}
|
||||
if len(statsIssues) > 0 {
|
||||
if sampling.Issues == nil {
|
||||
sampling.Issues = map[string]string{}
|
||||
}
|
||||
for key, value := range statsIssues {
|
||||
sampling.Issues[key] = value
|
||||
}
|
||||
}
|
||||
stats.Freshness = snapshot
|
||||
stats.Completeness = completeness
|
||||
stats.Sampling = sampling
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user