diff --git a/.gitignore b/.gitignore index 63609a2..30a9c80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ main -*.json .idea cover.out tmtop dist **/.DS_Store out.txt + +node_modules/ + +*.sublime-* +.omega.json +.vite diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fa4ecab --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and Development Commands + +```bash +# Build the complete application (frontend + Go binary) +make build + +# Build just the Go binary +make build-go + +# Build just the frontend +make build-front + +# Install the binary to $GOPATH/bin +make install + +# Run linting with automatic fixes +make lint + +# Run tests with coverage +make test +``` + +## Project Architecture + +tmtop is a terminal-based monitoring tool for Tendermint/CometBFT consensus visualization. The application follows a layered architecture: + +### Core Components + +- **pkg/app.go**: Main application orchestrator that coordinates all goroutines and manages state +- **pkg/aggregator/**: Data collection layer that fetches consensus state, validators, and chain info from multiple sources +- **pkg/fetcher/**: Protocol-specific data fetchers (Cosmos RPC, LCD, Tendermint, WebSocket) +- **pkg/display/**: Terminal UI layer using tview for consensus visualization +- **pkg/types/**: Shared data structures and state management +- **pkg/topology/**: Network topology API and embedded frontend + +### Key Patterns + +- **Concurrent Data Fetching**: Multiple goroutines refresh different data types at configurable intervals +- **State Management**: Centralized state in `types.State` with thread-safe access patterns +- **Mailbox Pattern**: Uses `github.com/brynbellomy/go-utils` mailboxes for async message passing +- **Protocol Abstraction**: `DataFetcher` interface allows supporting different chain types (cosmos-rpc, cosmos-lcd, tendermint) + +### Data Flow + +1. App starts multiple refresh goroutines (consensus, validators, chain info, etc.) +2. Aggregator coordinates data fetching from configured endpoints +3. State is updated and propagated to display layer +4. Terminal UI renders tables showing consensus progress, validator status, and chain info + +### Frontend Integration + +The project includes a React/TypeScript frontend in `pkg/topology/embed/frontend/` that provides a web-based topology view. Frontend assets are embedded into the Go binary. + +## Configuration + +All configuration is handled via CLI flags in `cmd/tmtop.go`. Key parameters: +- RPC host (required) +- Provider RPC host (for consumer chains) +- Chain type (cosmos-rpc, cosmos-lcd, tendermint) +- Various refresh rates for different data types +- Topology API settings + +## Testing + +Tests are located alongside source files with `_test.go` suffix. Current test coverage focuses on utility functions in `pkg/utils/`. \ No newline at end of file diff --git a/Makefile b/Makefile index d1e7292..56cb5fb 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//') +FRONT_DIR = pkg/topology/embed/frontend LDFLAGS = -X main.version=${VERSION} -build: +build: build-front build-go + +build-front: + pnpm -C $(FRONT_DIR) install + pnpm -C $(FRONT_DIR) run build + +build-go: go build -ldflags '$(LDFLAGS)' cmd/tmtop.go install: diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..425f788 --- /dev/null +++ b/TODO.md @@ -0,0 +1,222 @@ +# tmtop Refactoring TODO + +## Immediate Tasks + + +## Architectural Improvements + +### • Implement centralized goroutine lifecycle management +- **Current Issue**: Multiple uncoordinated goroutines with individual timers (`app.go:71-77`) +- **Problems**: No graceful shutdown, potential race conditions, difficult debugging +- **Proposed Solution**: + - Single coordinator goroutine managing refresh cycles + - Context-based cancellation for clean shutdown + - Configurable refresh intervals per data type +- **Benefits**: Better resource management, predictable behavior, easier testing + +### • Simplify State structure +- **Current Complexity**: 15+ fields in State struct with overlapping responsibilities +- **Target**: Reduce to core CometBFT types + minimal UI state +- **Example Simplified State**: + ```go + type State struct { + ValidatorSet *cometbfttypes.ValidatorSet + ConsensusState *cometbft.ConsensusState + NetworkInfo *cometbft.NetInfo + UIState *DisplayState // UI-specific fields only + } + ``` +- **Benefits**: Clearer data model, reduced memory usage, easier reasoning + +### • Decouple display logic from State structure +- **Current Issue**: Display layer tightly coupled to specific State structure (`pkg/display/wrapper.go:272-298`) +- **Solution**: Introduce display models or view interfaces +- **Benefits**: More flexible UI, easier testing, cleaner separation of concerns + +### • Implement better error handling and observability +- **Current State**: Basic error logging, limited debugging capabilities +- **Improvements**: + - Structured logging with context + - Metrics collection for goroutine health + - Better error propagation and user feedback +- **Tools**: Enhanced use of `zerolog`, potential metrics endpoint + +## Research & Investigation + +### • Analyze fetcher architecture consolidation opportunities +- **Question**: Can we simplify the multi-protocol support (cosmos-rpc, cosmos-lcd, tendermint)? +- **Focus**: CometBFT RPC as primary interface, reduce complexity +- **Investigation**: Review actual usage patterns of different fetcher types + +### • Evaluate WebSocket vs polling trade-offs +- **Current**: Mix of WebSocket subscriptions and HTTP polling +- **Question**: Can we standardize on one approach for consistency? +- **Considerations**: Real-time requirements vs. connection reliability + +### • Research CometBFT client library best practices +- **Goal**: Replace custom HTTP client with CometBFT's native RPC client +- **Benefits**: Better error handling, connection management, type safety +- **Investigation**: Review CometBFT client documentation and examples + +### • Consider event-driven architecture +- **Current**: Polling-based data refresh +- **Alternative**: Event-driven updates based on blockchain events +- **Benefits**: More responsive UI, reduced resource usage +- **Challenges**: Complexity of event handling, error recovery + +## Testing & Quality + +### • Implement comprehensive test coverage +- **Current State**: Limited tests, mainly in `pkg/utils/` +- **Priority Areas**: State management, data conversion, goroutine coordination +- **Tools**: Standard `go test`, consider property-based testing for complex state transitions + +### • Add integration tests +- **Scope**: End-to-end tests with mock blockchain data +- **Benefits**: Confidence in refactoring, regression prevention +- **Implementation**: Mock CometBFT responses, test full data flow + +### • Performance profiling and optimization +- **Tools**: `go tool pprof`, memory profiling +- **Focus Areas**: RoundDataMap efficiency, goroutine resource usage +- **Baseline**: Establish current performance metrics before refactoring + +## Done + +### ✅ Find and implement dead code elimination tools +- **Completed**: Used official `deadcode` tool from Go team (`golang.org/x/tools/cmd/deadcode@latest`) +- **Process**: Ran `deadcode ./...` to identify unreachable functions and removed them +- **Result**: Cleaner codebase with reduced maintenance burden + +### ✅ Remove Aggregator layer unnecessary abstraction +- **Completed**: Eliminated Aggregator completely from the codebase +- **Changes**: + - Removed `pkg/aggregator/aggregator.go` entirely + - Moved fetcher instances directly to App struct + - Updated goroutine methods to call fetchers directly (`TendermintClient`, `CosmosRPCClient`, `CosmosLCDClient`) +- **Benefits**: Simplified data flow, reduced indirection, clearer responsibilities + +### ✅ Consolidate redundant types in pkg/types package +- **Completed**: Unified all validator types into single `TMValidator` type +- **Removed Types**: + - `Validator`, `ValidatorWithRoundVote`, `ValidatorWithInfo`, `ValidatorWithChainValidator` + - `ValidatorsWithRoundVote`, `ValidatorsWithInfo`, `ValidatorsWithInfoAndAllRoundVotes` + - `RoundVote` (replaced with `RoundVoteState`) +- **Result**: Single `TMValidators` collection in State, significantly reduced memory usage and complexity + +### ✅ Replace custom types with Cosmos SDK/CometBFT equivalents +- **Vote Types**: Replaced custom `VoteType` enum with `VoteState` aligned to CometBFT types + - Removed: `NoVote`, `VotedNil`, `VotedForBlock` + - Added: `VoteStateNone`, `VoteStateNil`, `VoteStateForBlock` with CometBFT integration +- **Validator Types**: `TMValidator` now embeds `ctypes.Validator` from CometBFT as base +- **Type Integration**: Added utility functions `VoteStateFromCometBFT()`, `VoteStateFromVotesMap()` +- **Benefits**: Better type safety, reduced maintenance, leveraging battle-tested CometBFT code + +### ✅ Remove all legacy/backward compatibility code +- **Completed**: Systematically removed all compatibility layers and fallback logic +- **Scope**: No external packages import tmtop, so breaking changes were safe +- **Removed**: + - All legacy conversion functions and composite validator types + - Backward compatibility methods in State and display layers + - Legacy fallback logic in display wrapper and table components + - Obsolete `converter.go` file with transformation functions +- **Result**: Clean, unified codebase using only modern TMValidator and VoteState types + +### ✅ Add config.toml support +- **Completed**: Comprehensive configuration file support using `spf13/viper` +- **Features Implemented**: + - `--config-file` flag to specify custom config location + - Default location: `~/.config/tmtop/config.toml` (also checks current directory) + - All CLI flags supported in config file with kebab-case naming + - CLI flags override config file values (proper precedence) + - Environment variable support with `TMTOP_` prefix + - Comprehensive example config file with documentation +- **Benefits**: Easier configuration management, reduced command-line complexity, environment-specific configs + +### ✅ Fix terminal corruption on crash +- **Completed**: Implemented comprehensive terminal state restoration and cleanup system +- **Features Implemented**: + - Signal handling (SIGINT, SIGTERM) for graceful shutdown + - Panic recovery with terminal cleanup in all goroutines + - Proper tcell screen cleanup with `screen.Fini()` + - Terminal reset sequences to restore cursor and clear screen + - Cleanup function registration system for extensibility + - Defer-based cleanup ensures terminal is restored even on unexpected exits +- **Technical Details**: + - Added `handleSignals()` method to catch termination signals + - Enhanced `HandlePanic()` to perform cleanup before re-panicking + - Implemented `restoreTerminal()` with proper tcell cleanup and ANSI reset sequences + - Modified display wrapper to handle cleanup on drawing errors +- **Bug Fix**: Removed signal interception that was blocking Ctrl+C, now uses tview's native input handling +- **Benefits**: No more corrupted terminal sessions after crashes or forced exits, Ctrl+C works properly + +### ✅ Fix empty table display bug +- **Completed**: Implemented missing TMValidator data conversion pipeline +- **Root Issue**: Tables showed no data because validator conversion was never implemented after refactoring +- **Changes Made**: + - Implemented `convertToTMValidators()` method in `pkg/app.go` to convert fetched JSON data to display format + - Added `convertToCometBFTValidator()` for proper CometBFT validator creation with ed25519 key support + - Created utility functions `HexToBytes()` and `Base64ToBytes()` in `pkg/utils/utils.go` + - Fixed the RefreshConsensus() TODO that was blocking data flow to display layer + - Added comprehensive error handling and logging for conversion failures +- **Benefits**: Tables now properly display validator data including names, addresses, and voting status + +### ✅ Fix voting power percentage display +- **Completed**: Implemented precise voting power percentage calculation and display +- **Root Issue**: Stake percentages showed as `` because `VotingPowerPercent` field was never calculated +- **Changes Made**: + - Enhanced `convertToTMValidators()` to calculate total voting power and individual validator percentages + - Used `math/big.Float` for precise percentage calculations (handles fractional percentages accurately) + - Fixed `TMValidator.Serialize()` method to properly format `*big.Float` using `.Text('f', 2)` instead of `fmt.Sprintf` + - Added nil-checking and fallback to "0.00" for safety + - Integrated voting power calculation with CometBFT validator creation +- **Benefits**: Stake percentages now display correctly (e.g., "15.23%") instead of ``, accurate to 2 decimal places + +### ✅ Expand RoundDataMap usage to other views +- **Completed**: Extended RoundDataMap usage from `all_rounds_table.go` to `last_round_table.go` and consensus display +- **Changes Made**: + - Updated `LastRoundTableData` to include `RoundData`, `CurrentHeight`, and `CurrentRound` fields + - Added new methods: `SetRoundData()` and `SetCurrentRound()` for round data management + - Created `generateValidatorDisplayText()` method that queries RoundDataMap directly instead of using manually populated `CurrentRoundVote` field + - Updated `State` methods to use RoundDataMap queries instead of TMValidator methods for consensus display + - Modified display wrapper to call new LastRoundTable methods with RoundDataMap and current round information +- **Technical Benefits**: + - Consistent data source: Both LastRoundTable and AllRoundsTable now use the same RoundDataMap source + - Simplified logic: Removed dependency on manually populated `CurrentRoundVote` field + - Better maintainability: Centralized vote state management in RoundDataMap + - Reduced memory usage: No need to duplicate vote state in TMValidator objects +- **Architecture Improvement**: Cleaner separation of concerns with RoundDataMap as central store for all vote data + +### ✅ Implement SQLite storage for historical data +- **Completed**: Comprehensive SQLite backend with sqlc code generation for persistent consensus data storage +- **Database Schema**: + - **Core Tables**: `validators`, `heights`, `rounds`, `votes`, `consensus_events`, `validator_snapshots` + - **Efficient Indexing**: Optimized for height/round lookups, validator searches, and time-based queries + - **Foreign Key Relationships**: Proper data integrity between validators, heights, rounds, and votes +- **sqlc Code Generation**: + - **Schema**: `/pkg/db/migrations/001_initial.sql` with comprehensive table structure + - **Queries**: Organized SQL queries in `/pkg/db/queries/` by entity type (validators, heights, rounds, votes, etc.) + - **Generated Code**: Type-safe Go code in `/pkg/db/sqlc/` with prepared statements and interfaces +- **Service Layer Architecture**: + - **DB Service** (`/pkg/db/db.go`): Connection management, migrations, WAL mode, cleanup routines + - **ConsensusStore** (`/pkg/db/consensus_store.go`): High-level consensus data persistence with transaction support + - **Integration**: Seamless integration with existing RoundDataMap and TMValidator types +- **Configuration & CLI**: + - **New Flags**: `--database-path`, `--max-retain-blocks`, `--max-retain-days` + - **Config File**: Full support in `config.toml.example` with comprehensive documentation + - **Environment Variables**: `TMTOP_DATABASE_PATH`, `TMTOP_MAX_RETAIN_BLOCKS`, `TMTOP_MAX_RETAIN_DAYS` + - **Smart Defaults**: `~/.config/tmtop/tmtop.db` if not specified +- **Real-time Persistence**: + - **Automatic Storage**: Real-time persistence of consensus states, validator data, and vote information + - **Event Tracking**: CometBFT events (new rounds, votes) stored with full context + - **Background Cleanup**: Hourly cleanup routine to manage database size based on retention policies + - **Graceful Degradation**: Application continues working if database fails to initialize +- **Data Retention Management**: + - **Block-based Retention**: Keep last N blocks (default: 10,000) + - **Time-based Retention**: Alternative day-based retention (default: 7 days) + - **Automatic Cleanup**: Hourly background process removes old data while respecting foreign key constraints +- **Technical Features**: + - **Performance**: WAL mode, prepared statements, efficient indexing, batch operations + - **Data Integrity**: Foreign key constraints, transaction-based operations, comprehensive error handling + - **Monitoring**: Structured logging for all database operations, cleanup status tracking +- **Benefits**: Crash resilience, historical analysis capabilities, reduced memory usage, foundation for advanced analytics \ No newline at end of file diff --git a/cmd/tmtop.go b/cmd/tmtop.go index e6104a0..310180d 100644 --- a/cmd/tmtop.go +++ b/cmd/tmtop.go @@ -1,23 +1,32 @@ package main import ( + "fmt" + "os" + "path/filepath" + "time" + "main/pkg" configPkg "main/pkg/config" "main/pkg/logger" - "time" "github.com/spf13/cobra" + "github.com/spf13/viper" ) -var ( - version = "unknown" -) +var version = "unknown" -func Execute(inputConfig configPkg.InputConfig, args []string) { - if len(args) == 0 || args[0] == "" { - inputConfig.RPCHost = "http://localhost:26657" - } else { +func Execute(inputConfig configPkg.InputConfig, args []string, configFile string) { + // Load configuration from file if specified + if err := loadConfigFile(configFile, &inputConfig); err != nil { + logger.GetDefaultLogger().Fatal().Err(err).Msg("Could not load configuration file") + } + + // Override RPC host with positional argument if provided + if len(args) > 0 && args[0] != "" { inputConfig.RPCHost = args[0] + } else if inputConfig.RPCHost == "" { + inputConfig.RPCHost = "http://localhost:26657" } config, err := configPkg.ParseAndValidateConfig(inputConfig) @@ -29,8 +38,137 @@ func Execute(inputConfig configPkg.InputConfig, args []string) { app.Start() } +// loadConfigFile loads configuration from file using viper. +func loadConfigFile(configFile string, config *configPkg.InputConfig) error { + fmt.Println("Loading config file:", configFile) + + v := viper.New() + + // Set config file path + if configFile != "" { + // Use specified config file + v.SetConfigFile(configFile) + } else { + // Use default locations + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + + configDir := filepath.Join(homeDir, ".config", "tmtop") + v.SetConfigName("config") + v.SetConfigType("toml") + v.AddConfigPath(configDir) // ~/.config/tmtop/ + v.AddConfigPath(".") // current directory + } + + // Set environment variable prefix for automatic env binding + v.SetEnvPrefix("TMTOP") + v.AutomaticEnv() + + // Map config keys to viper keys (using kebab-case for consistency with CLI flags) + v.BindEnv("rpc-host", "TMTOP_RPC_HOST") + v.BindEnv("provider-rpc-host", "TMTOP_PROVIDER_RPC_HOST") + v.BindEnv("consumer-id", "TMTOP_CONSUMER_ID") + v.BindEnv("refresh-rate", "TMTOP_REFRESH_RATE") + v.BindEnv("validators-refresh-rate", "TMTOP_VALIDATORS_REFRESH_RATE") + v.BindEnv("chain-info-refresh-rate", "TMTOP_CHAIN_INFO_REFRESH_RATE") + v.BindEnv("upgrade-refresh-rate", "TMTOP_UPGRADE_REFRESH_RATE") + v.BindEnv("block-time-refresh-rate", "TMTOP_BLOCK_TIME_REFRESH_RATE") + v.BindEnv("chain-type", "TMTOP_CHAIN_TYPE") + v.BindEnv("verbose", "TMTOP_VERBOSE") + v.BindEnv("disable-emojis", "TMTOP_DISABLE_EMOJIS") + v.BindEnv("debug-file", "TMTOP_DEBUG_FILE") + v.BindEnv("halt-height", "TMTOP_HALT_HEIGHT") + v.BindEnv("blocks-behind", "TMTOP_BLOCKS_BEHIND") + v.BindEnv("lcd-host", "TMTOP_LCD_HOST") + v.BindEnv("timezone", "TMTOP_TIMEZONE") + v.BindEnv("with-topology-api", "TMTOP_WITH_TOPOLOGY_API") + v.BindEnv("topology-listen-addr", "TMTOP_TOPOLOGY_LISTEN_ADDR") + v.BindEnv("database-path", "TMTOP_DATABASE_PATH") + v.BindEnv("max-retain-blocks", "TMTOP_MAX_RETAIN_BLOCKS") + v.BindEnv("max-retain-days", "TMTOP_MAX_RETAIN_DAYS") + + // Read config file + if err := v.ReadInConfig(); err != nil { + // Config file not found is acceptable - CLI flags/defaults will be used + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return err + } + } + fmt.Println("Loaded config file:", v.ConfigFileUsed()) + + // Map viper values to config struct (only if they exist and are not zero values) + if v.IsSet("rpc-host") { + config.RPCHost = v.GetString("rpc-host") + } + if v.IsSet("provider-rpc-host") { + config.ProviderRPCHost = v.GetString("provider-rpc-host") + } + if v.IsSet("consumer-id") { + config.ConsumerID = v.GetString("consumer-id") + } + if v.IsSet("refresh-rate") { + config.RefreshRate = v.GetDuration("refresh-rate") + } + if v.IsSet("validators-refresh-rate") { + config.ValidatorsRefreshRate = v.GetDuration("validators-refresh-rate") + } + if v.IsSet("chain-info-refresh-rate") { + config.ChainInfoRefreshRate = v.GetDuration("chain-info-refresh-rate") + } + if v.IsSet("upgrade-refresh-rate") { + config.UpgradeRefreshRate = v.GetDuration("upgrade-refresh-rate") + } + if v.IsSet("block-time-refresh-rate") { + config.BlockTimeRefreshRate = v.GetDuration("block-time-refresh-rate") + } + if v.IsSet("chain-type") { + config.ChainType = v.GetString("chain-type") + } + if v.IsSet("verbose") { + config.Verbose = v.GetBool("verbose") + } + if v.IsSet("disable-emojis") { + config.DisableEmojis = v.GetBool("disable-emojis") + } + if v.IsSet("debug-file") { + config.DebugFile = v.GetString("debug-file") + } + if v.IsSet("halt-height") { + config.HaltHeight = v.GetInt64("halt-height") + } + if v.IsSet("blocks-behind") { + config.BlocksBehind = v.GetUint64("blocks-behind") + } + if v.IsSet("lcd-host") { + config.LCDHost = v.GetString("lcd-host") + } + if v.IsSet("timezone") { + config.Timezone = v.GetString("timezone") + } + if v.IsSet("with-topology-api") { + config.WithTopologyAPI = v.GetBool("with-topology-api") + } + if v.IsSet("topology-listen-addr") { + config.TopologyListenAddr = v.GetString("topology-listen-addr") + } + if v.IsSet("database-path") { + config.DatabasePath = v.GetString("database-path") + } + if v.IsSet("max-retain-blocks") { + config.MaxRetainBlocks = v.GetInt64("max-retain-blocks") + } + if v.IsSet("max-retain-days") { + config.MaxRetainDays = v.GetInt("max-retain-days") + } + + return nil +} + func main() { var config configPkg.InputConfig + var configFile string rootCmd := &cobra.Command{ Use: "tmtop [RPC host URL]", @@ -38,10 +176,14 @@ func main() { Version: version, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - Execute(config, args) + Execute(config, args, configFile) }, } + // Configuration file flag + rootCmd.PersistentFlags().StringVar(&configFile, "config-file", "", "Path to configuration file (default: ~/.config/tmtop/config.toml)") + + // All existing flags rootCmd.PersistentFlags().StringVar(&config.ProviderRPCHost, "provider-rpc-host", "", "Provider chain RPC host URL") rootCmd.PersistentFlags().StringVar(&config.ConsumerID, "consumer-id", "", "Consumer ID (not chain ID!)") rootCmd.PersistentFlags().DurationVar(&config.RefreshRate, "refresh-rate", time.Second, "Refresh rate") @@ -57,6 +199,17 @@ func main() { rootCmd.PersistentFlags().Int64Var(&config.HaltHeight, "halt-height", 0, "Custom halt-height") rootCmd.PersistentFlags().Uint64Var(&config.BlocksBehind, "blocks-behind", 1000, "How many blocks behind to check to calculate block time") rootCmd.PersistentFlags().StringVar(&config.Timezone, "timezone", "", "Timezone to display dates in") + rootCmd.PersistentFlags().BoolVar(&config.WithTopologyAPI, "with-topology-api", false, "Enable topology API") + rootCmd.PersistentFlags().StringVar(&config.TopologyListenAddr, "topology-listen-addr", "0.0.0.0:8080", "The address on which to serve topology API") + rootCmd.PersistentFlags().StringVar(&config.DatabasePath, "database-path", "", "Path to SQLite database file (default: ~/.config/tmtop/tmtop.db)") + rootCmd.PersistentFlags().Int64Var(&config.MaxRetainBlocks, "max-retain-blocks", 10000, "Maximum number of blocks to retain in database") + rootCmd.PersistentFlags().IntVar(&config.MaxRetainDays, "max-retain-days", 7, "Maximum number of days to retain data (alternative to max-retain-blocks)") + + // Analytics flags + rootCmd.PersistentFlags().BoolVar(&config.AnalyticsMode, "analytics", false, "Enable analytics mode (requires database)") + rootCmd.PersistentFlags().StringVar(&config.AnalyticsValidator, "analytics-validator", "", "Validator address for analytics (required in analytics mode)") + rootCmd.PersistentFlags().StringVar(&config.AnalyticsTimeWindow, "analytics-time-window", "24h", "Time window for analytics (Go duration: 30m, 1h, 24h, 168h, etc.)") + rootCmd.PersistentFlags().StringVar(&config.AnalyticsCommand, "analytics-command", "performance", "Analytics command to run (performance, rankings, timeseries, debug, diagnose, search)") if err := rootCmd.Execute(); err != nil { logger.GetDefaultLogger().Fatal().Err(err).Msg("Could not start application") diff --git a/config.toml.example b/config.toml.example new file mode 100644 index 0000000..450befa --- /dev/null +++ b/config.toml.example @@ -0,0 +1,149 @@ +# tmtop Configuration File +# +# This is an example configuration file for tmtop, a terminal-based monitoring tool +# for Tendermint/CometBFT consensus visualization. +# +# Place this file at ~/.config/tmtop/config.toml to use it by default, +# or specify a custom location with --config-file flag. +# +# All settings in this file can be overridden by command-line flags. +# Environment variables are also supported with TMTOP_ prefix (e.g., TMTOP_RPC_HOST). + +# ============================================================================== +# RPC Configuration +# ============================================================================== + +# Primary RPC endpoint for the chain you want to monitor +# Can also be specified as the first positional argument +rpc-host = "http://localhost:26657" + +# For consumer chains: Provider chain RPC endpoint +# Leave empty for non-consumer chains +provider-rpc-host = "" + +# Consumer chain ID (required only if provider-rpc-host is set) +# This is NOT the chain ID, but the consumer ID used by the provider +consumer-id = "" + +# LCD/REST API endpoint (required only for chain-type = "cosmos-lcd") +lcd-host = "" + +# ============================================================================== +# Chain Configuration +# ============================================================================== + +# Type of chain to monitor +# Options: "cosmos-rpc", "cosmos-lcd", "tendermint" +chain-type = "cosmos-rpc" + +# Custom halt height (0 = disabled) +# Useful for monitoring planned upgrades +halt-height = 0 + +# How many blocks to look back when calculating average block time +blocks-behind = 1000 + +# ============================================================================== +# Refresh Rates +# ============================================================================== + +# Main consensus state refresh rate +# How often to fetch the current consensus status +refresh-rate = "1s" + +# Validator set refresh rate +# How often to fetch the validator set (slower changing data) +validators-refresh-rate = "1m" + +# Chain information refresh rate +# How often to fetch chain metadata (version, network name, etc.) +chain-info-refresh-rate = "5m" + +# Upgrade plan refresh rate +# How often to check for scheduled chain upgrades +upgrade-refresh-rate = "30m" + +# Block time calculation refresh rate +# How often to recalculate average block time +block-time-refresh-rate = "30s" + +# ============================================================================== +# Display Options +# ============================================================================== + +# Enable verbose logging for debugging +verbose = false + +# Disable emoji output (useful for terminals that don't support emojis) +disable-emojis = false + +# Timezone for displaying timestamps +# Examples: "UTC", "America/New_York", "Europe/London", "Asia/Tokyo" +# Leave empty to use system timezone +timezone = "" + +# ============================================================================== +# Debugging & Development +# ============================================================================== + +# Path to write debug logs to file +# Leave empty to disable file logging +debug-file = "" + +# ============================================================================== +# Topology API (Optional) +# ============================================================================== + +# Enable the topology API server +# Provides a web interface for visualizing network topology +with-topology-api = false + +# Address to serve the topology API on +# Only used if with-topology-api = true +topology-listen-addr = "0.0.0.0:8080" + +# ============================================================================== +# Database Configuration +# ============================================================================== + +# Path to SQLite database file for persistent storage +# Leave empty to use default: ~/.config/tmtop/tmtop.db +# database-path = "" + +# Maximum number of blocks to retain in database +# Older blocks will be automatically cleaned up +max-retain-blocks = 10000 + +# Alternative: Maximum number of days to retain data +# Used as alternative to max-retain-blocks +max-retain-days = 7 + +# ============================================================================== +# Example Configurations +# ============================================================================== + +# Example 1: Monitoring Cosmos Hub mainnet +# rpc-host = "https://cosmos-rpc.publicnode.com:443" +# chain-type = "cosmos-rpc" +# timezone = "UTC" + +# Example 2: Monitoring a local testnet with topology API +# rpc-host = "http://localhost:26657" +# chain-type = "cosmos-rpc" +# with-topology-api = true +# topology-listen-addr = "127.0.0.1:8080" +# verbose = true + +# Example 3: Consumer chain monitoring +# rpc-host = "http://consumer-chain:26657" +# provider-rpc-host = "http://provider-chain:26657" +# consumer-id = "consumer-1" +# chain-type = "cosmos-rpc" + +# Example 4: High-frequency monitoring setup +# rpc-host = "http://localhost:26657" +# refresh-rate = "500ms" +# validators-refresh-rate = "30s" +# chain-info-refresh-rate = "2m" +# verbose = true +# debug-file = "/tmp/tmtop-debug.log" \ No newline at end of file diff --git a/go.mod b/go.mod index dee1abe..45184cf 100644 --- a/go.mod +++ b/go.mod @@ -1,97 +1,105 @@ module main -go 1.22.2 - -toolchain go1.23.1 +go 1.24 require ( cosmossdk.io/x/upgrade v0.1.4 + github.com/brynbellomy/go-utils v0.0.0-20250825055819-60c6be9b3b8d github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d - github.com/cosmos/cosmos-sdk v0.50.9 + github.com/cometbft/cometbft v0.38.17 + github.com/cosmos/cosmos-sdk v0.50.14 github.com/cosmos/interchain-security/v6 v6.1.0 github.com/gdamore/tcell/v2 v2.6.0 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 + github.com/mitchellh/mapstructure v1.5.0 github.com/rivo/tview v0.0.0-20231022175332-f7f32ad28104 - github.com/rs/zerolog v1.33.0 + github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + gonum.org/v1/gonum v0.15.1 + modernc.org/sqlite v1.37.1 ) require ( - cosmossdk.io/api v0.7.5 // indirect + cosmossdk.io/api v0.7.6 // indirect cosmossdk.io/collections v0.4.0 // indirect - cosmossdk.io/core v0.11.1 // indirect - cosmossdk.io/depinject v1.0.0 // indirect + cosmossdk.io/core v0.11.2 // indirect + cosmossdk.io/depinject v1.1.0 // indirect cosmossdk.io/errors v1.0.1 // indirect cosmossdk.io/log v1.4.1 // indirect - cosmossdk.io/math v1.3.0 // indirect - cosmossdk.io/store v1.1.0 // indirect + cosmossdk.io/math v1.4.0 // indirect + cosmossdk.io/store v1.1.1 // indirect cosmossdk.io/x/evidence v0.1.1 // indirect - cosmossdk.io/x/tx v0.13.4 // indirect + cosmossdk.io/x/feegrant v0.1.1 // indirect + cosmossdk.io/x/tx v0.13.7 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect - github.com/99designs/keyring v1.2.1 // indirect - github.com/DataDog/datadog-go v3.2.0+incompatible // indirect + github.com/99designs/keyring v1.2.2 // indirect + github.com/DataDog/datadog-go v4.8.3+incompatible // indirect github.com/DataDog/zstd v1.5.5 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/allora-network/allora-chain v0.12.4 // indirect + github.com/allora-network/allora-sdk-go v1.1.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect - github.com/btcsuite/btcd v0.20.1-beta // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect - github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect - github.com/cockroachdb/pebble v1.1.0 // indirect + github.com/cockroachdb/pebble v1.1.2 // indirect github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect - github.com/cometbft/cometbft v0.38.11 // indirect - github.com/cometbft/cometbft-db v0.12.0 // indirect + github.com/cometbft/cometbft-db v0.14.1 // indirect github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-db v1.0.2 // indirect + github.com/cosmos/cosmos-db v1.1.1 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/gogoproto v1.7.0 // indirect - github.com/cosmos/iavl v1.1.2 // indirect + github.com/cosmos/iavl v1.2.2 // indirect github.com/cosmos/ibc-go/modules/capability v1.0.1 // indirect - github.com/cosmos/ibc-go/v8 v8.5.0 // indirect + github.com/cosmos/ibc-go/v8 v8.7.0 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect - github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect + github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect + github.com/danieljoos/wincred v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/dvsekhvalnov/jose2go v1.6.0 // indirect - github.com/emicklei/dot v1.6.1 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/dvsekhvalnov/jose2go v1.7.0 // indirect + github.com/emicklei/dot v1.6.2 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect - github.com/go-kit/kit v0.12.0 // indirect + github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.2.1 // indirect + github.com/golang/glog v1.2.3 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.1.2 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v1.12.1 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.3 // indirect - github.com/hashicorp/go-plugin v1.5.2 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -101,9 +109,11 @@ require ( github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/linxGnu/grocksdb v1.8.14 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -111,31 +121,32 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mtibben/percent v0.2.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.52.2 // indirect - github.com/prometheus/procfs v0.13.0 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.3 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/rs/cors v1.8.3 // indirect + github.com/rs/cors v1.11.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sasha-s/go-deadlock v0.3.1 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect @@ -145,21 +156,26 @@ require ( go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.35.0 // indirect google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240709173604-40e1e62336c5 // indirect - google.golang.org/grpc v1.66.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.4 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect + modernc.org/libc v1.65.7 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect nhooyr.io/websocket v1.8.6 // indirect pgregory.net/rapid v1.1.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index bbb59aa..a3ab48a 100644 --- a/go.sum +++ b/go.sum @@ -7,30 +7,40 @@ cloud.google.com/go/auth v0.6.0/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/compute v1.27.1 h1:0WbBLIPNANheCRZ4h8QhgzjN53KMutbiVBOLtPiVzBU= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/iam v1.1.9 h1:oSkYLVtVme29uGYrOcKcvJRht7cHJpYD09GM9JaR0TE= cloud.google.com/go/iam v1.1.9/go.mod h1:Nt1eDWNYH9nGQg3d/mY7U1hvfGmsaG9o/kLGoLoLXjQ= cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= cosmossdk.io/api v0.7.5 h1:eMPTReoNmGUm8DeiQL9DyM8sYDjEhWzL1+nLbI9DqtQ= cosmossdk.io/api v0.7.5/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= +cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= +cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= cosmossdk.io/client/v2 v2.0.0-beta.3 h1:+TTuH0DwQYsUq2JFAl3fDZzKq5gQG7nt3dAattkjFDU= cosmossdk.io/client/v2 v2.0.0-beta.3/go.mod h1:CZcL41HpJPOOayTCO28j8weNBQprG+SRiKX39votypo= cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= cosmossdk.io/core v0.11.1 h1:h9WfBey7NAiFfIcUhDVNS503I2P2HdZLebJlUIs8LPA= cosmossdk.io/core v0.11.1/go.mod h1:OJzxcdC+RPrgGF8NJZR2uoQr56tc7gfBKhiKeDO7hH0= +cosmossdk.io/core v0.11.2 h1:20PXbQxhWRKA83pSYW76OXrc1MI2E93flbMAGSVFlyc= +cosmossdk.io/core v0.11.2/go.mod h1:q137AJUo+/BFZ0hTTgx+7enPAar5c3Nr0h042BgcZMY= cosmossdk.io/depinject v1.0.0 h1:dQaTu6+O6askNXO06+jyeUAnF2/ssKwrrszP9t5q050= cosmossdk.io/depinject v1.0.0/go.mod h1:zxK/h3HgHoA/eJVtiSsoaRaRA2D5U4cJ5thIG4ssbB8= +cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E= +cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI= cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= cosmossdk.io/log v1.4.1 h1:wKdjfDRbDyZRuWa8M+9nuvpVYxrEOwbD/CA8hvhU8QM= cosmossdk.io/log v1.4.1/go.mod h1:k08v0Pyq+gCP6phvdI6RCGhLf/r425UT6Rk/m+o74rU= cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= +cosmossdk.io/math v1.4.0 h1:XbgExXFnXmF/CccPPEto40gOO7FpWu9yWNAZPN3nkNQ= +cosmossdk.io/math v1.4.0/go.mod h1:O5PkD4apz2jZs4zqFdTr16e1dcaQCc5z6lkEnrrppuk= cosmossdk.io/store v1.1.0 h1:LnKwgYMc9BInn9PhpTFEQVbL9UK475G2H911CGGnWHk= cosmossdk.io/store v1.1.0/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= +cosmossdk.io/store v1.1.1 h1:NA3PioJtWDVU7cHHeyvdva5J/ggyLDkyH0hGHl2804Y= +cosmossdk.io/store v1.1.1/go.mod h1:8DwVTz83/2PSI366FERGbWSH7hL6sB7HbYp8bqksNwM= cosmossdk.io/x/circuit v0.1.1 h1:KPJCnLChWrxD4jLwUiuQaf5mFD/1m7Omyo7oooefBVQ= cosmossdk.io/x/circuit v0.1.1/go.mod h1:B6f/urRuQH8gjt4eLIXfZJucrbreuYrKh5CSjaOxr+Q= cosmossdk.io/x/evidence v0.1.1 h1:Ks+BLTa3uftFpElLTDp9L76t2b58htjVbSZ86aoK/E4= @@ -39,6 +49,8 @@ cosmossdk.io/x/feegrant v0.1.1 h1:EKFWOeo/pup0yF0svDisWWKAA9Zags6Zd0P3nRvVvw8= cosmossdk.io/x/feegrant v0.1.1/go.mod h1:2GjVVxX6G2fta8LWj7pC/ytHjryA6MHAJroBWHFNiEQ= cosmossdk.io/x/tx v0.13.4 h1:Eg0PbJgeO0gM8p5wx6xa0fKR7hIV6+8lC56UrsvSo0Y= cosmossdk.io/x/tx v0.13.4/go.mod h1:BkFqrnGGgW50Y6cwTy+JvgAhiffbGEKW6KF9ufcDpvk= +cosmossdk.io/x/tx v0.13.7 h1:8WSk6B/OHJLYjiZeMKhq7DK7lHDMyK0UfDbBMxVmeOI= +cosmossdk.io/x/tx v0.13.7/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= cosmossdk.io/x/upgrade v0.1.4 h1:/BWJim24QHoXde8Bc64/2BSEB6W4eTydq0X/2f8+g38= cosmossdk.io/x/upgrade v0.1.4/go.mod h1:9v0Aj+fs97O+Ztw+tG3/tp5JSlrmT7IcFhAebQHmOPo= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -47,23 +59,32 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q= +github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allora-network/allora-chain v0.12.4 h1:TzijjTTTHVx6Jo5lDg+OskAM0SObKZCax9OXxleGqqI= +github.com/allora-network/allora-chain v0.12.4/go.mod h1:GG7BvQaEJ2C8q4fcQSSKyQaNajpvbtHCjZ4EBPsDMFE= +github.com/allora-network/allora-sdk-go v1.1.5 h1:TjGXajjYFzvPQNutACELDthtmUFQG+U7UgH84JHpPtU= +github.com/allora-network/allora-sdk-go v1.1.5/go.mod h1:4kXY1OEdN8cHjeazWX7gNikp7lXik4wX/Qlw5Nzk1Bk= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -88,20 +109,16 @@ github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2 github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= -github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/brynbellomy/go-utils v0.0.0-20250601013634-e8cb1848c310 h1:MInVUV72+bB0DfEtogUkxq2MPZFF8H+nMAjx/EHXeH0= +github.com/brynbellomy/go-utils v0.0.0-20250601013634-e8cb1848c310/go.mod h1:nqSUwdZrKkc5BuR0+5hCwAN6Z8QzyYCFteGXOaYt7Tw= +github.com/brynbellomy/go-utils v0.0.0-20250825055819-60c6be9b3b8d h1:zosrKPcrNrKX5rEj8Brhh6GKEzPP7E16vaFf7qcezJg= +github.com/brynbellomy/go-utils v0.0.0-20250825055819-60c6be9b3b8d/go.mod h1:kliioOI78VfJRwyrIKV2cZJNt4ijf0v/S3UY2hwdkbU= github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= -github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= -github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= -github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -109,6 +126,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -131,24 +150,30 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= -github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= -github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= +github.com/cockroachdb/pebble v1.1.1 h1:XnKU22oiCLy2Xn8vp1re67cXg4SAasg/WDt1NtcRFaw= +github.com/cockroachdb/pebble v1.1.1/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= +github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= +github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.11 h1:6bNDUB8/xq4uYonYwIfGc9OqK1ZH4NkdaMmR1LZIJqk= -github.com/cometbft/cometbft v0.38.11/go.mod h1:jHPx9vQpWzPHEAiYI/7EDKaB1NXhK6o3SArrrY8ExKc= -github.com/cometbft/cometbft-db v0.12.0 h1:v77/z0VyfSU7k682IzZeZPFZrQAKiQwkqGN0QzAjMi0= -github.com/cometbft/cometbft-db v0.12.0/go.mod h1:aX2NbCrjNVd2ZajYxt1BsiFf/Z+TQ2MN0VxdicheYuw= +github.com/cometbft/cometbft v0.38.17 h1:FkrQNbAjiFqXydeAO81FUzriL4Bz0abYxN/eOHrQGOk= +github.com/cometbft/cometbft v0.38.17/go.mod h1:5l0SkgeLRXi6bBfQuevXjKqML1jjfJJlvI1Ulp02/o4= +github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= +github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -157,6 +182,8 @@ github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAKs= github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= github.com/cosmos/cosmos-sdk v0.50.9-lsm h1:nYQVX0YinJ3Zu3PHEee36OeZ8yAw42ctE52S2K3MleM= @@ -170,31 +197,38 @@ github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fr github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= github.com/cosmos/iavl v1.1.2 h1:zL9FK7C4L/P4IF1Dm5fIwz0WXCnn7Bp1M2FxH0ayM7Y= github.com/cosmos/iavl v1.1.2/go.mod h1:jLeUvm6bGT1YutCaL2fIar/8vGUE8cPZvh/gXEWDaDM= +github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= +github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= github.com/cosmos/ibc-go/modules/capability v1.0.1 h1:ibwhrpJ3SftEEZRxCRkH0fQZ9svjthrX2+oXdZvzgGI= github.com/cosmos/ibc-go/modules/capability v1.0.1/go.mod h1:rquyOV262nGJplkumH+/LeYs04P3eV8oB7ZM4Ygqk4E= github.com/cosmos/ibc-go/v8 v8.5.0 h1:OjaSXz480JT8ZuMrASxGgS7XzloZ2NuuJPwZB/fKDgE= github.com/cosmos/ibc-go/v8 v8.5.0/go.mod h1:P5hkAvq0Qbg0h18uLxDVA9q1kOJ0l36htMsskiNwXbo= +github.com/cosmos/ibc-go/v8 v8.7.0 h1:HqhVOkO8bDpClXE81DFQgFjroQcTvtpm0tCS7SQVKVY= +github.com/cosmos/ibc-go/v8 v8.7.0/go.mod h1:G2z+Q6ZQSMcyHI2+BVcJdvfOupb09M2h/tgpXOEdY6k= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/interchain-security/v6 v6.1.0 h1:ycTpT+If90nSEvRVu86ThPJxNtcmnOMjJmFC9ptd/yo= github.com/cosmos/interchain-security/v6 v6.1.0/go.mod h1:+5zIZEzkL4yNHB/UWXCu75t6GeEgEmWHbz5OnBWiL0o= github.com/cosmos/ledger-cosmos-go v0.13.3 h1:7ehuBGuyIytsXbd4MP43mLeoN2LTOEnk5nvue4rK+yM= github.com/cosmos/ledger-cosmos-go v0.13.3/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8= +github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= +github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= -github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= +github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= @@ -211,12 +245,16 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= +github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/emicklei/dot v1.6.1 h1:ujpDlBkkwgWUY+qPId5IwapRW/xEoligRSYjioR6DFI= github.com/emicklei/dot v1.6.1/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -229,6 +267,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -259,8 +299,8 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= @@ -269,8 +309,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -284,6 +324,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= @@ -302,8 +344,8 @@ github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2 github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= -github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM= +github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -335,8 +377,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -358,6 +400,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= @@ -379,8 +423,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= @@ -402,6 +446,8 @@ github.com/hashicorp/go-getter v1.7.4 h1:3yQjWuxICvSpYwqSayAdKRFcvBl1y/vogCxczWS github.com/hashicorp/go-getter v1.7.4/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -411,6 +457,8 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= @@ -456,7 +504,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls= github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -464,9 +511,10 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -479,11 +527,10 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -497,8 +544,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= @@ -526,10 +573,12 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -552,6 +601,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -563,6 +614,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -604,10 +657,11 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= -github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 h1:jik8PHtAIsPlCRJjJzl4udgEf7hawInF9texMeO2jrU= -github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -628,8 +682,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -644,21 +698,23 @@ github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt2 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck= -github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= -github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/tview v0.0.0-20231022175332-f7f32ad28104 h1:wKaxPrOZ2TmfUt+Y+aIqFdBNDMwaEVPftRA3UclgNlc= github.com/rivo/tview v0.0.0-20231022175332-f7f32ad28104/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -671,11 +727,14 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= -github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -684,8 +743,8 @@ github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgY github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= -github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -702,6 +761,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= @@ -730,8 +791,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= @@ -772,12 +834,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -793,7 +859,6 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -804,8 +869,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb h1:xIApU0ow1zwMa2uL1VDNeQlNVFTWMQxZUZCMDy0Q4Us= golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -820,6 +887,10 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -852,13 +923,15 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -867,8 +940,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -922,14 +997,18 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -938,8 +1017,10 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -963,11 +1044,17 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.186.0 h1:n2OPp+PPXX0Axh4GuSsL5QL8xQCTb2oDwyzPnQvqUug= google.golang.org/api v0.186.0/go.mod h1:hvRbBmgoje49RV3xqVXrmP6w93n6ehGgIVPYrGtBFFc= @@ -988,10 +1075,14 @@ google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094 h1:6whtk83KtD3FkGrVb2hFXuQ+ZMbCNdakARIn/aHMmG8= google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094/go.mod h1:Zs4wYw8z1zr6RNF4cwYb31mvN/EGaKAdQjNCF3DW6K4= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240709173604-40e1e62336c5 h1:SbSDUWW1PAO24TNpLdeheoYPd7kllICcLU52x6eD4kQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240709173604-40e1e62336c5/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E= +google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1011,8 +1102,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1027,8 +1118,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1066,6 +1157,30 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= +modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= +modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= diff --git a/pkg/aggregator/aggregator.go b/pkg/aggregator/aggregator.go deleted file mode 100644 index 1614aae..0000000 --- a/pkg/aggregator/aggregator.go +++ /dev/null @@ -1,85 +0,0 @@ -package aggregator - -import ( - configPkg "main/pkg/config" - dataFetcher "main/pkg/fetcher" - "main/pkg/tendermint" - "main/pkg/types" - "sync" - "time" - - "github.com/rs/zerolog" -) - -type Aggregator struct { - Config *configPkg.Config - Logger zerolog.Logger - - TendermintClient *tendermint.RPC - DataFetcher dataFetcher.DataFetcher -} - -func NewAggregator(config *configPkg.Config, logger zerolog.Logger) *Aggregator { - return &Aggregator{ - Config: config, - Logger: logger.With().Str("component", "aggregator").Logger(), - TendermintClient: tendermint.NewRPC(config, logger), - DataFetcher: dataFetcher.GetDataFetcher(config, logger), - } -} - -func (a *Aggregator) GetData() ( - *types.ConsensusStateResponse, - []types.TendermintValidator, - error, -) { - var wg sync.WaitGroup - - var consensusError error - var validatorsError error - - var validators []types.TendermintValidator - var consensus *types.ConsensusStateResponse - - wg.Add(2) - - go func() { - consensus, consensusError = a.TendermintClient.GetConsensusState() - wg.Done() - }() - - go func() { - validators, validatorsError = a.TendermintClient.GetValidators() - wg.Done() - }() - - wg.Wait() - - if consensusError != nil { - a.Logger.Error().Err(consensusError).Msg("Could not fetch consensus data") - return nil, nil, consensusError - } - - if validatorsError != nil { - a.Logger.Error().Err(validatorsError).Msg("Could not fetch validators") - return nil, nil, validatorsError - } - - return consensus, validators, nil -} - -func (a *Aggregator) GetChainValidators() (*types.ChainValidators, error) { - return a.DataFetcher.GetValidators() -} - -func (a *Aggregator) GetChainInfo() (*types.TendermintStatusResponse, error) { - return a.TendermintClient.GetStatus() -} - -func (a *Aggregator) GetUpgrade() (*types.Upgrade, error) { - return a.DataFetcher.GetUpgradePlan() -} - -func (a *Aggregator) GetBlockTime() (time.Duration, error) { - return a.TendermintClient.GetBlockTime() -} diff --git a/pkg/analytics/analytics.go b/pkg/analytics/analytics.go new file mode 100644 index 0000000..0a84a60 --- /dev/null +++ b/pkg/analytics/analytics.go @@ -0,0 +1,452 @@ +package analytics + +import ( + "context" + "database/sql" + "fmt" + "strconv" + "time" + + "main/pkg/db" + "main/pkg/db/sqlc" + + "github.com/rs/zerolog" +) + +// Helper functions to safely convert interface{} types from SQLite. +func convertToInt64(val interface{}) int64 { + if val == nil { + return 0 + } + + switch v := val.(type) { + case int64: + return v + case int: + return int64(v) + case float64: + return int64(v) + case string: + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + return i + } + } + return 0 +} + +func convertToFloat64(val interface{}) float64 { + if val == nil { + return 0.0 + } + + switch v := val.(type) { + case float64: + return v + case int64: + return float64(v) + case int: + return float64(v) + case string: + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + } + return 0.0 +} + +func convertToTime(val interface{}) time.Time { + if val == nil { + return time.Time{} + } + + if timeStr, ok := val.(string); ok { + if t, err := time.Parse("2006-01-02 15:04:05", timeStr); err == nil { + return t + } + } + return time.Time{} +} + +// ValidatorAnalytics provides performance analysis for validators. +type ValidatorAnalytics struct { + db *db.DB + logger zerolog.Logger +} + +// TimeWindow represents a time range for analysis. +type TimeWindow struct { + Start time.Time + End time.Time +} + +// PerformanceMetrics contains comprehensive validator performance data. +type PerformanceMetrics struct { + ValidatorHexAddress string + ValidatorMoniker string + TimeWindow TimeWindow + + // Block signing metrics + TotalBlocks int64 + BlocksSigned int64 + BlocksMissed int64 + SigningEfficiency float64 // Percentage + + // Consensus participation + TotalRounds int64 + PrevoteRate float64 // Percentage + PrecommitRate float64 // Percentage + VotingPower int64 + + // Miss analysis + LongestMissStreak int64 + + // Time-based + CalculatedAt time.Time +} + +// ConsensusMetrics contains detailed consensus participation data. +type ConsensusMetrics struct { + ValidatorHexAddress string + TotalRounds int64 + RoundsWithPrevote int64 + RoundsWithPrecommit int64 + PrevoteRate float64 + PrecommitRate float64 +} + +// MissedBlockStreak represents a sequence of consecutive missed blocks. +type MissedBlockStreak struct { + ConsecutiveMisses int64 + StreakStartHeight int64 + StreakEndHeight int64 + StreakStartTime time.Time + StreakEndTime time.Time +} + +// UptimeMetrics contains uptime analysis for a validator. +type UptimeMetrics struct { + ValidatorHexAddress string + TotalBlocksInWindow int64 + BlocksParticipated int64 + BlocksMissed int64 + UptimePercentage float64 + WindowStart time.Time + WindowEnd time.Time +} + +// ValidatorRanking contains ranking information with multiple metrics. +type ValidatorRanking struct { + HexAddress string + Moniker string + TotalBlocks int64 + BlocksSigned int64 + BlocksMissed int64 + SigningEfficiency float64 + PrevotesCast int64 + PrecommitsCast int64 + VotingPower int64 + EfficiencyRank int64 +} + +// TimeSeriesPoint represents performance metrics at a specific time. +type TimeSeriesPoint struct { + TimeBucket time.Time + BlocksInBucket int64 + BlocksSigned int64 + BlocksMissed int64 + SigningEfficiency float64 +} + +// NewValidatorAnalytics creates a new validator analytics service. +func NewValidatorAnalytics(database *db.DB, logger zerolog.Logger) *ValidatorAnalytics { + return &ValidatorAnalytics{ + db: database, + logger: logger.With().Str("component", "validator_analytics").Logger(), + } +} + +// GetSigningEfficiency calculates signing efficiency for a validator over a time window. +func (va *ValidatorAnalytics) GetSigningEfficiency(ctx context.Context, validatorAddr string, window TimeWindow) (*PerformanceMetrics, error) { + queries := va.db.Queries() + + // Get basic signing efficiency + efficiency, err := queries.GetValidatorSigningEfficiency(ctx, sqlc.GetValidatorSigningEfficiencyParams{ + ValidatorHexAddress: validatorAddr, + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Str("validator", validatorAddr).Msg("Failed to get signing efficiency") + return nil, err + } + + // Get consensus participation + participation, err := queries.GetValidatorConsensusParticipation(ctx, sqlc.GetValidatorConsensusParticipationParams{ + ValidatorHexAddress: validatorAddr, + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Str("validator", validatorAddr).Msg("Failed to get consensus participation") + return nil, err + } + + // Get miss streaks + missStreaks, err := queries.GetValidatorMissedBlockStreaks(ctx, sqlc.GetValidatorMissedBlockStreaksParams{ + ValidatorHexAddress: validatorAddr, + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Str("validator", validatorAddr).Msg("Failed to get missed block streaks") + return nil, err + } + + // Find longest miss streak + longestMissStreak := int64(0) + if len(missStreaks) > 0 { + longestMissStreak = missStreaks[0].ConsecutiveMisses // Already sorted by consecutive_misses DESC + } + + // Convert interface{} types safely + totalBlocks := convertToInt64(efficiency.TotalBlocks) + blocksSigned := convertToInt64(efficiency.BlocksSigned) + blocksMissed := convertToInt64(efficiency.BlocksMissed) + signingEfficiency := convertToFloat64(efficiency.SigningEfficiency) + + totalRounds := convertToInt64(participation.TotalRounds) + prevoteRate := convertToFloat64(participation.PrevoteRate) + precommitRate := convertToFloat64(participation.PrecommitRate) + + return &PerformanceMetrics{ + ValidatorHexAddress: validatorAddr, + TimeWindow: window, + TotalBlocks: totalBlocks, + BlocksSigned: blocksSigned, + BlocksMissed: blocksMissed, + SigningEfficiency: signingEfficiency, + TotalRounds: totalRounds, + PrevoteRate: prevoteRate, + PrecommitRate: precommitRate, + LongestMissStreak: longestMissStreak, + CalculatedAt: time.Now(), + }, nil +} + +// GetMissedBlockStreaks returns all consecutive missed block sequences for a validator. +func (va *ValidatorAnalytics) GetMissedBlockStreaks(ctx context.Context, validatorAddr string, window TimeWindow) ([]MissedBlockStreak, error) { + queries := va.db.Queries() + + streaks, err := queries.GetValidatorMissedBlockStreaks(ctx, sqlc.GetValidatorMissedBlockStreaksParams{ + ValidatorHexAddress: validatorAddr, + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Str("validator", validatorAddr).Msg("Failed to get missed block streaks") + return nil, err + } + + result := make([]MissedBlockStreak, len(streaks)) + for i, streak := range streaks { + // Convert interface{} types safely using helper functions + startHeight := convertToInt64(streak.StreakStartHeight) + endHeight := convertToInt64(streak.StreakEndHeight) + startTime := convertToTime(streak.StreakStartTime) + endTime := convertToTime(streak.StreakEndTime) + + result[i] = MissedBlockStreak{ + ConsecutiveMisses: streak.ConsecutiveMisses, + StreakStartHeight: startHeight, + StreakEndHeight: endHeight, + StreakStartTime: startTime, + StreakEndTime: endTime, + } + } + + return result, nil +} + +// GetConsensusParticipation returns detailed consensus participation metrics. +func (va *ValidatorAnalytics) GetConsensusParticipation(ctx context.Context, validatorAddr string, window TimeWindow) (*ConsensusMetrics, error) { + queries := va.db.Queries() + + participation, err := queries.GetValidatorConsensusParticipation(ctx, sqlc.GetValidatorConsensusParticipationParams{ + ValidatorHexAddress: validatorAddr, + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Str("validator", validatorAddr).Msg("Failed to get consensus participation") + return nil, err + } + + // Convert interface{} types safely + totalRounds := convertToInt64(participation.TotalRounds) + roundsWithPrevote := convertToInt64(participation.RoundsWithPrevote) + roundsWithPrecommit := convertToInt64(participation.RoundsWithPrecommit) + prevoteRate := convertToFloat64(participation.PrevoteRate) + precommitRate := convertToFloat64(participation.PrecommitRate) + + return &ConsensusMetrics{ + ValidatorHexAddress: validatorAddr, + TotalRounds: totalRounds, + RoundsWithPrevote: roundsWithPrevote, + RoundsWithPrecommit: roundsWithPrecommit, + PrevoteRate: prevoteRate, + PrecommitRate: precommitRate, + }, nil +} + +// GetValidatorUptime returns uptime metrics for a validator. +func (va *ValidatorAnalytics) GetValidatorUptime(ctx context.Context, validatorAddr string, window TimeWindow) (*UptimeMetrics, error) { + queries := va.db.Queries() + + uptime, err := queries.GetValidatorUptime(ctx, sqlc.GetValidatorUptimeParams{ + ValidatorHexAddress: validatorAddr, + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Str("validator", validatorAddr).Msg("Failed to get validator uptime") + return nil, err + } + + // Convert interface{} types safely + blocksMissed := convertToInt64(uptime.BlocksMissed) + uptimePercentage := convertToFloat64(uptime.UptimePercentage) + windowStart := convertToTime(uptime.WindowStart) + windowEnd := convertToTime(uptime.WindowEnd) + + // Fallback to original window if conversion failed + if windowStart.IsZero() { + windowStart = window.Start + } + if windowEnd.IsZero() { + windowEnd = window.End + } + + return &UptimeMetrics{ + ValidatorHexAddress: validatorAddr, + TotalBlocksInWindow: uptime.TotalBlocksInWindow, + BlocksParticipated: uptime.BlocksParticipated, + BlocksMissed: blocksMissed, + UptimePercentage: uptimePercentage, + WindowStart: windowStart, + WindowEnd: windowEnd, + }, nil +} + +// GetAllValidatorMetrics returns performance metrics for all validators. +func (va *ValidatorAnalytics) GetAllValidatorMetrics(ctx context.Context, window TimeWindow) ([]ValidatorRanking, error) { + queries := va.db.Queries() + + rankings, err := queries.GetValidatorRanking(ctx, sqlc.GetValidatorRankingParams{ + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Msg("Failed to get validator rankings") + return nil, err + } + + result := make([]ValidatorRanking, len(rankings)) + for i, ranking := range rankings { + // Convert interface{} types safely + votingPower := convertToInt64(ranking.VotingPower) + efficiencyRank := convertToInt64(ranking.EfficiencyRank) + + result[i] = ValidatorRanking{ + HexAddress: ranking.HexAddress, + Moniker: ranking.Moniker.String, + TotalBlocks: ranking.TotalBlocks, + BlocksSigned: ranking.BlocksSigned, + BlocksMissed: ranking.BlocksMissed, + SigningEfficiency: ranking.SigningEfficiency, + PrevotesCast: ranking.PrevotesCast, + PrecommitsCast: ranking.PrecommitsCast, + VotingPower: votingPower, + EfficiencyRank: efficiencyRank, + } + } + + return result, nil +} + +// GetPerformanceTimeSeries returns time-series performance data. +func (va *ValidatorAnalytics) GetPerformanceTimeSeries(ctx context.Context, validatorAddr string, window TimeWindow) ([]TimeSeriesPoint, error) { + queries := va.db.Queries() + + timeSeries, err := queries.GetValidatorPerformanceTimeSeries(ctx, sqlc.GetValidatorPerformanceTimeSeriesParams{ + ValidatorHexAddress: validatorAddr, + BlockTime: sql.NullTime{Time: window.Start, Valid: true}, + BlockTime_2: sql.NullTime{Time: window.End, Valid: true}, + }) + if err != nil { + va.logger.Error().Err(err).Str("validator", validatorAddr).Msg("Failed to get performance time series") + return nil, err + } + + result := make([]TimeSeriesPoint, len(timeSeries)) + for i, point := range timeSeries { + // Convert interface{} types safely + timeBucket := convertToTime(point.TimeBucket) + blocksMissed := convertToInt64(point.BlocksMissed) + signingEfficiency := convertToFloat64(point.SigningEfficiency) + + result[i] = TimeSeriesPoint{ + TimeBucket: timeBucket, + BlocksInBucket: point.BlocksInBucket, + BlocksSigned: int64(point.BlocksSigned), + BlocksMissed: blocksMissed, + SigningEfficiency: signingEfficiency, + } + } + + return result, nil +} + +// ParseTimeWindow parses a duration string and returns a TimeWindow. +func ParseTimeWindow(durationStr string) (TimeWindow, error) { + duration, err := time.ParseDuration(durationStr) + if err != nil { + return TimeWindow{}, fmt.Errorf("invalid duration format '%s': %w. Examples: 1h, 30m, 24h, 168h (7 days)", durationStr, err) + } + + if duration <= 0 { + return TimeWindow{}, fmt.Errorf("duration must be positive, got: %s", durationStr) + } + + now := time.Now() + return TimeWindow{ + Start: now.Add(-duration), + End: now, + }, nil +} + +// GetCommonTimeWindows returns commonly used time windows for analysis (for documentation/examples). +func GetCommonTimeWindows() map[string]TimeWindow { + now := time.Now() + return map[string]TimeWindow{ + "1h": { + Start: now.Add(-1 * time.Hour), + End: now, + }, + "6h": { + Start: now.Add(-6 * time.Hour), + End: now, + }, + "24h": { + Start: now.Add(-24 * time.Hour), + End: now, + }, + "168h": { // 7 days + Start: now.Add(-7 * 24 * time.Hour), + End: now, + }, + "720h": { // 30 days + Start: now.Add(-30 * 24 * time.Hour), + End: now, + }, + } +} diff --git a/pkg/analytics/analytics_test.go b/pkg/analytics/analytics_test.go new file mode 100644 index 0000000..41a2664 --- /dev/null +++ b/pkg/analytics/analytics_test.go @@ -0,0 +1,170 @@ +package analytics + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestParseTimeWindow(t *testing.T) { + testCases := []struct { + input string + shouldError bool + duration time.Duration + }{ + {"1h", false, time.Hour}, + {"30m", false, 30 * time.Minute}, + {"24h", false, 24 * time.Hour}, + {"168h", false, 168 * time.Hour}, // 7 days + {"720h", false, 720 * time.Hour}, // 30 days + {"1s", false, time.Second}, + {"2h30m", false, 2*time.Hour + 30*time.Minute}, + {"invalid", true, 0}, + {"-1h", true, 0}, // negative duration + {"0s", true, 0}, // zero duration + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + window, err := ParseTimeWindow(tc.input) + + if tc.shouldError { + assert.Error(t, err, "Expected error for input: %s", tc.input) + return + } + + assert.NoError(t, err, "Unexpected error for input: %s", tc.input) + assert.True(t, window.End.After(window.Start), "End should be after Start") + + actualDuration := window.End.Sub(window.Start) + // Allow for small timing differences (within 1 second) + assert.True(t, + actualDuration >= tc.duration-time.Second && actualDuration <= tc.duration+time.Second, + "Expected duration %v, got %v for input %s", tc.duration, actualDuration, tc.input) + }) + } +} + +func TestGetCommonTimeWindows(t *testing.T) { + windows := GetCommonTimeWindows() + + // Test that all expected time windows are present + expectedWindows := []string{"1h", "6h", "24h", "168h", "720h"} + for _, expected := range expectedWindows { + _, exists := windows[expected] + assert.True(t, exists, "Expected time window %s to exist", expected) + } + + // Test that time windows are properly ordered (End > Start) + for name, window := range windows { + assert.True(t, window.End.After(window.Start), "Time window %s should have End after Start", name) + } + + // Test specific durations + assert.Equal(t, time.Hour, windows["1h"].End.Sub(windows["1h"].Start), "1h window should be 1 hour") + assert.Equal(t, 24*time.Hour, windows["24h"].End.Sub(windows["24h"].Start), "24h window should be 24 hours") +} + +func TestTimeWindow(t *testing.T) { + start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC) + + window := TimeWindow{ + Start: start, + End: end, + } + + assert.Equal(t, start, window.Start) + assert.Equal(t, end, window.End) + assert.True(t, window.End.After(window.Start)) +} + +func TestPerformanceMetrics(t *testing.T) { + window := TimeWindow{ + Start: time.Now().Add(-24 * time.Hour), + End: time.Now(), + } + + metrics := &PerformanceMetrics{ + ValidatorHexAddress: "cosmosvaloper1test", + ValidatorMoniker: "Test Validator", + TimeWindow: window, + TotalBlocks: 100, + BlocksSigned: 95, + BlocksMissed: 5, + SigningEfficiency: 95.0, + TotalRounds: 100, + PrevoteRate: 98.0, + PrecommitRate: 96.0, + LongestMissStreak: 2, + CalculatedAt: time.Now(), + } + + assert.Equal(t, "cosmosvaloper1test", metrics.ValidatorHexAddress) + assert.Equal(t, "Test Validator", metrics.ValidatorMoniker) + assert.Equal(t, int64(100), metrics.TotalBlocks) + assert.Equal(t, int64(95), metrics.BlocksSigned) + assert.Equal(t, int64(5), metrics.BlocksMissed) + assert.Equal(t, 95.0, metrics.SigningEfficiency) + assert.Equal(t, int64(2), metrics.LongestMissStreak) +} + +func TestMissedBlockStreak(t *testing.T) { + streak := MissedBlockStreak{ + ConsecutiveMisses: 5, + StreakStartHeight: 100, + StreakEndHeight: 104, + StreakStartTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + StreakEndTime: time.Date(2023, 1, 1, 0, 5, 0, 0, time.UTC), + } + + assert.Equal(t, int64(5), streak.ConsecutiveMisses) + assert.Equal(t, int64(100), streak.StreakStartHeight) + assert.Equal(t, int64(104), streak.StreakEndHeight) + assert.True(t, streak.StreakEndTime.After(streak.StreakStartTime)) +} + +func TestValidatorRanking(t *testing.T) { + ranking := ValidatorRanking{ + HexAddress: "cosmosvaloper1test", + Moniker: "Test Validator", + TotalBlocks: 1000, + BlocksSigned: 950, + BlocksMissed: 50, + SigningEfficiency: 95.0, + PrevotesCast: 980, + PrecommitsCast: 960, + VotingPower: 1000000, + EfficiencyRank: 1, + } + + assert.Equal(t, "cosmosvaloper1test", ranking.HexAddress) + assert.Equal(t, "Test Validator", ranking.Moniker) + assert.Equal(t, int64(1000), ranking.TotalBlocks) + assert.Equal(t, int64(950), ranking.BlocksSigned) + assert.Equal(t, int64(50), ranking.BlocksMissed) + assert.Equal(t, 95.0, ranking.SigningEfficiency) + assert.Equal(t, int64(1), ranking.EfficiencyRank) + + // Test that blocks signed + blocks missed = total blocks + assert.Equal(t, ranking.TotalBlocks, ranking.BlocksSigned+ranking.BlocksMissed) +} + +func TestTimeSeriesPoint(t *testing.T) { + point := TimeSeriesPoint{ + TimeBucket: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + BlocksInBucket: 60, + BlocksSigned: 58, + BlocksMissed: 2, + SigningEfficiency: 96.67, + } + + assert.Equal(t, int64(60), point.BlocksInBucket) + assert.Equal(t, int64(58), point.BlocksSigned) + assert.Equal(t, int64(2), point.BlocksMissed) + assert.Equal(t, 96.67, point.SigningEfficiency) + + // Test that blocks signed + blocks missed = blocks in bucket + assert.Equal(t, point.BlocksInBucket, point.BlocksSigned+point.BlocksMissed) +} diff --git a/pkg/analytics/cli.go b/pkg/analytics/cli.go new file mode 100644 index 0000000..b4b14b8 --- /dev/null +++ b/pkg/analytics/cli.go @@ -0,0 +1,239 @@ +package analytics + +import ( + "context" + "fmt" + "time" + + "main/pkg/db" + + "github.com/rs/zerolog" +) + +// CLIAnalytics provides command-line interface for analytics. +type CLIAnalytics struct { + analytics *ValidatorAnalytics + debugger *DatabaseDebugger + logger zerolog.Logger +} + +// NewCLIAnalytics creates a new CLI analytics interface. +func NewCLIAnalytics(database *db.DB, logger zerolog.Logger) *CLIAnalytics { + return &CLIAnalytics{ + analytics: NewValidatorAnalytics(database, logger), + debugger: NewDatabaseDebugger(database, logger), + logger: logger.With().Str("component", "cli_analytics").Logger(), + } +} + +// PrintValidatorPerformance displays comprehensive performance metrics for a validator. +func (cli *CLIAnalytics) PrintValidatorPerformance(ctx context.Context, validatorAddr string, durationStr string) error { + window, err := ParseTimeWindow(durationStr) + if err != nil { + return fmt.Errorf("invalid time window: %w", err) + } + + fmt.Printf("\n=== Validator Performance Analysis ===\n") + fmt.Printf("Validator: %s\n", validatorAddr) + fmt.Printf("Time Window: %s (%s to %s)\n", durationStr, window.Start.Format("2006-01-02 15:04:05"), window.End.Format("2006-01-02 15:04:05")) + fmt.Printf("=======================================\n\n") + + // Get comprehensive performance metrics + metrics, err := cli.analytics.GetSigningEfficiency(ctx, validatorAddr, window) + if err != nil { + return fmt.Errorf("failed to get performance metrics: %w", err) + } + + // Check if we have any data + if metrics.TotalBlocks == 0 { + fmt.Printf("📊 No data found for validator %s in the specified time window.\n", validatorAddr) + fmt.Printf("💡 This could mean:\n") + fmt.Printf(" • The database is empty (tmtop hasn't been running long enough)\n") + fmt.Printf(" • The validator address is incorrect\n") + fmt.Printf(" • The time window doesn't contain any recorded blocks\n") + fmt.Printf("\n💭 Try:\n") + fmt.Printf(" • Run tmtop in normal mode first to collect data\n") + fmt.Printf(" • Use a different time window (e.g., --analytics-time-window 1h)\n") + fmt.Printf(" • Check if the validator address is correct\n") + return nil + } + + // Display block signing metrics + fmt.Printf("📊 Block Signing Performance:\n") + fmt.Printf(" Total Blocks: %d\n", metrics.TotalBlocks) + fmt.Printf(" Blocks Signed: %d\n", metrics.BlocksSigned) + fmt.Printf(" Blocks Missed: %d\n", metrics.BlocksMissed) + fmt.Printf(" Signing Efficiency: %.2f%%\n", metrics.SigningEfficiency) + fmt.Printf("\n") + + // Display consensus participation + fmt.Printf("🗳️ Consensus Participation:\n") + fmt.Printf(" Total Rounds: %d\n", metrics.TotalRounds) + fmt.Printf(" Prevote Rate: %.2f%%\n", metrics.PrevoteRate) + fmt.Printf(" Precommit Rate: %.2f%%\n", metrics.PrecommitRate) + fmt.Printf("\n") + + // Display miss analysis + fmt.Printf("⚠️ Miss Analysis:\n") + fmt.Printf(" Longest Miss Streak: %d blocks\n", metrics.LongestMissStreak) + + // Get detailed miss streaks if any + if metrics.LongestMissStreak > 0 { + streaks, err := cli.analytics.GetMissedBlockStreaks(ctx, validatorAddr, window) + if err == nil && len(streaks) > 0 { + fmt.Printf(" Recent Miss Streaks:\n") + for i, streak := range streaks { + if i >= 5 { // Show only top 5 streaks + break + } + fmt.Printf(" - %d blocks (heights %d-%d)\n", + streak.ConsecutiveMisses, + streak.StreakStartHeight, + streak.StreakEndHeight) + } + } + } + fmt.Printf("\n") + + // Get uptime metrics + uptime, err := cli.analytics.GetValidatorUptime(ctx, validatorAddr, window) + if err == nil { + fmt.Printf("⏱️ Uptime Metrics:\n") + fmt.Printf(" Uptime Percentage: %.2f%%\n", uptime.UptimePercentage) + fmt.Printf(" Blocks Participated: %d/%d\n", uptime.BlocksParticipated, uptime.TotalBlocksInWindow) + } + + return nil +} + +// PrintValidatorRankings displays performance rankings for all validators. +func (cli *CLIAnalytics) PrintValidatorRankings(ctx context.Context, durationStr string, limit int) error { + window, err := ParseTimeWindow(durationStr) + if err != nil { + return fmt.Errorf("invalid time window: %w", err) + } + + fmt.Printf("\n=== Validator Performance Rankings ===\n") + fmt.Printf("Time Window: %s (%s to %s)\n", durationStr, window.Start.Format("2006-01-02 15:04:05"), window.End.Format("2006-01-02 15:04:05")) + fmt.Printf("======================================\n\n") + + rankings, err := cli.analytics.GetAllValidatorMetrics(ctx, window) + if err != nil { + return fmt.Errorf("failed to get validator rankings: %w", err) + } + + if len(rankings) == 0 { + fmt.Printf("No validator data found for the specified time window.\n") + return nil + } + + // Header + fmt.Printf("%-4s %-20s %-12s %-12s %-12s %-8s %-12s\n", + "Rank", "Moniker", "Blocks Signed", "Efficiency", "Voting Power", "Prevotes", "Precommits") + fmt.Printf("%-4s %-20s %-12s %-12s %-12s %-8s %-12s\n", + "----", "--------------------", "------------", "------------", "------------", "--------", "------------") + + // Display top validators (limited) + displayCount := limit + if len(rankings) < displayCount { + displayCount = len(rankings) + } + + for i := 0; i < displayCount; i++ { + v := rankings[i] + moniker := v.Moniker + if len(moniker) > 20 { + moniker = moniker[:17] + "..." + } + if moniker == "" { + moniker = v.HexAddress[:12] + "..." + } + + fmt.Printf("%-4d %-20s %-12s %-12s %-12d %-8d %-12d\n", + v.EfficiencyRank, + moniker, + fmt.Sprintf("%d/%d", v.BlocksSigned, v.TotalBlocks), + fmt.Sprintf("%.2f%%", v.SigningEfficiency), + v.VotingPower, + v.PrevotesCast, + v.PrecommitsCast) + } + + if len(rankings) > limit { + fmt.Printf("\n... and %d more validators\n", len(rankings)-limit) + } + + return nil +} + +// PrintPerformanceTimeSeries displays hourly performance trend for a validator. +func (cli *CLIAnalytics) PrintPerformanceTimeSeries(ctx context.Context, validatorAddr string, durationStr string) error { + window, err := ParseTimeWindow(durationStr) + if err != nil { + return fmt.Errorf("invalid time window: %w", err) + } + + fmt.Printf("\n=== Validator Performance Time Series ===\n") + fmt.Printf("Validator: %s\n", validatorAddr) + fmt.Printf("Time Window: %s (hourly buckets)\n", durationStr) + fmt.Printf("=========================================\n\n") + + series, err := cli.analytics.GetPerformanceTimeSeries(ctx, validatorAddr, window) + if err != nil { + return fmt.Errorf("failed to get performance time series: %w", err) + } + + if len(series) == 0 { + fmt.Printf("No time series data found for the specified time window.\n") + return nil + } + + // Header + fmt.Printf("%-20s %-12s %-12s %-12s\n", "Time", "Blocks", "Signed", "Efficiency") + fmt.Printf("%-20s %-12s %-12s %-12s\n", "--------------------", "------------", "------------", "------------") + + // Display time series data + for _, point := range series { + fmt.Printf("%-20s %-12d %-12d %-12s\n", + point.TimeBucket.Format("2006-01-02 15:04"), + point.BlocksInBucket, + point.BlocksSigned, + fmt.Sprintf("%.2f%%", point.SigningEfficiency)) + } + + return nil +} + +// PrintDatabaseSummary prints a summary of the database contents. +func (cli *CLIAnalytics) PrintDatabaseSummary(ctx context.Context) error { + return cli.debugger.PrintDatabaseSummary(ctx) +} + +// DiagnoseValidator performs detailed diagnosis for a specific validator. +func (cli *CLIAnalytics) DiagnoseValidator(ctx context.Context, validatorAddr string, durationStr string) error { + window, err := ParseTimeWindow(durationStr) + if err != nil { + return fmt.Errorf("invalid time window: %w", err) + } + + return cli.debugger.DiagnoseValidatorData(ctx, validatorAddr, window) +} + +// SearchValidators searches for validators by partial address or moniker. +func (cli *CLIAnalytics) SearchValidators(ctx context.Context, searchTerm string) error { + return cli.debugger.SearchValidators(ctx, searchTerm) +} + +// FormatDuration formats a duration in a human-readable way. +func FormatDuration(d time.Duration) string { + if d < time.Minute { + return fmt.Sprintf("%.0fs", d.Seconds()) + } + if d < time.Hour { + return fmt.Sprintf("%.1fm", d.Minutes()) + } + if d < 24*time.Hour { + return fmt.Sprintf("%.1fh", d.Hours()) + } + return fmt.Sprintf("%.1fd", d.Hours()/24) +} diff --git a/pkg/analytics/debug.go b/pkg/analytics/debug.go new file mode 100644 index 0000000..b954739 --- /dev/null +++ b/pkg/analytics/debug.go @@ -0,0 +1,363 @@ +package analytics + +import ( + "context" + "database/sql" + "fmt" + + "main/pkg/db" + + "github.com/rs/zerolog" +) + +// DatabaseDebugger provides debugging capabilities for the analytics database. +type DatabaseDebugger struct { + db *db.DB + logger zerolog.Logger +} + +// NewDatabaseDebugger creates a new database debugger. +func NewDatabaseDebugger(database *db.DB, logger zerolog.Logger) *DatabaseDebugger { + return &DatabaseDebugger{ + db: database, + logger: logger.With().Str("component", "db_debugger").Logger(), + } +} + +// PrintDatabaseSummary prints a summary of what's in the database. +func (d *DatabaseDebugger) PrintDatabaseSummary(ctx context.Context) error { + fmt.Printf("\n=== Database Summary ===\n") + + // Check each table + tables := []string{"validators", "heights", "rounds", "votes", "consensus_events", "validator_snapshots"} + + for _, table := range tables { + count, err := d.getTableCount(ctx, table) + if err != nil { + fmt.Printf("❌ %s: ERROR - %v\n", table, err) + continue + } + fmt.Printf("📊 %s: %d records\n", table, count) + } + + // Get sample data from key tables + fmt.Printf("\n=== Sample Data ===\n") + + // Sample validators + if err := d.printSampleValidators(ctx); err != nil { + fmt.Printf("❌ Error getting validators: %v\n", err) + } + + // Sample heights + if err := d.printSampleHeights(ctx); err != nil { + fmt.Printf("❌ Error getting heights: %v\n", err) + } + + // Sample votes + if err := d.printSampleVotes(ctx); err != nil { + fmt.Printf("❌ Error getting votes: %v\n", err) + } + + return nil +} + +// DiagnoseValidatorData checks if a specific validator has data. +func (d *DatabaseDebugger) DiagnoseValidatorData(ctx context.Context, validatorAddr string, timeWindow TimeWindow) error { + fmt.Printf("\n=== Validator Diagnosis ===\n") + fmt.Printf("Validator: %s\n", validatorAddr) + fmt.Printf("Time Window: %s to %s\n", timeWindow.Start.Format("2006-01-02 15:04:05"), timeWindow.End.Format("2006-01-02 15:04:05")) + fmt.Printf("==========================\n\n") + + // Check if validator exists in validators table + validatorExists, err := d.checkValidatorExists(ctx, validatorAddr) + if err != nil { + return fmt.Errorf("error checking validator existence: %w", err) + } + + if validatorExists { + fmt.Printf("✅ Validator found in validators table\n") + } else { + fmt.Printf("❌ Validator NOT found in validators table\n") + fmt.Printf("💡 Available validators:\n") + if err := d.printAllValidators(ctx); err != nil { + fmt.Printf(" Error listing validators: %v\n", err) + } + } + + // Check heights in time window + heightCount, err := d.getHeightsInWindow(ctx, timeWindow) + if err != nil { + return fmt.Errorf("error checking heights: %w", err) + } + fmt.Printf("📊 Heights in time window: %d\n", heightCount) + + // Check votes for this validator in time window + voteCount, err := d.getVotesForValidatorInWindow(ctx, validatorAddr, timeWindow) + if err != nil { + return fmt.Errorf("error checking votes: %w", err) + } + fmt.Printf("🗳️ Votes for validator in time window: %d\n", voteCount) + + // Sample some votes for this validator + if err := d.printSampleValidatorVotes(ctx, validatorAddr, 5); err != nil { + fmt.Printf("❌ Error getting sample votes: %v\n", err) + } + + return nil +} + +func (d *DatabaseDebugger) getTableCount(ctx context.Context, tableName string) (int, error) { + query := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName) + row := d.db.DB().QueryRowContext(ctx, query) + + var count int + err := row.Scan(&count) + return count, err +} + +func (d *DatabaseDebugger) printSampleValidators(ctx context.Context) error { + query := "SELECT address, moniker, voting_power FROM validators LIMIT 5" + rows, err := d.db.DB().QueryContext(ctx, query) + if err != nil { + return err + } + defer rows.Close() + + fmt.Printf("👥 Sample Validators:\n") + for rows.Next() { + var address, moniker string + var votingPower int64 + if err := rows.Scan(&address, &moniker, &votingPower); err != nil { + return err + } + fmt.Printf(" %s (%s) - Power: %d\n", address, moniker, votingPower) + } + + return rows.Err() +} + +func (d *DatabaseDebugger) printSampleHeights(ctx context.Context) error { + query := "SELECT height, block_time, proposer_address FROM heights ORDER BY height DESC LIMIT 5" + rows, err := d.db.DB().QueryContext(ctx, query) + if err != nil { + return err + } + defer rows.Close() + + fmt.Printf("📏 Sample Heights (latest):\n") + for rows.Next() { + var height int64 + var blockTime sql.NullTime + var proposer sql.NullString + if err := rows.Scan(&height, &blockTime, &proposer); err != nil { + return err + } + + blockTimeStr := "NULL" + if blockTime.Valid { + blockTimeStr = blockTime.Time.Format("2006-01-02 15:04:05") + } + + proposerStr := "NULL" + if proposer.Valid { + proposerStr = proposer.String + } + + fmt.Printf(" Height: %d, Time: %s, Proposer: %s\n", height, blockTimeStr, proposerStr) + } + + return rows.Err() +} + +func (d *DatabaseDebugger) printSampleVotes(ctx context.Context) error { + query := "SELECT height, round_number, validator_address, vote_type, timestamp FROM votes LIMIT 5" + rows, err := d.db.DB().QueryContext(ctx, query) + if err != nil { + return err + } + defer rows.Close() + + fmt.Printf("🗳️ Sample Votes:\n") + for rows.Next() { + var height, round, voteType int64 + var validatorAddr string + var timestamp sql.NullTime + if err := rows.Scan(&height, &round, &validatorAddr, &voteType, ×tamp); err != nil { + return err + } + + timestampStr := "NULL" + if timestamp.Valid { + timestampStr = timestamp.Time.Format("15:04:05") + } + + fmt.Printf(" H:%d R:%d Validator:%s Type:%d Time:%s\n", height, round, validatorAddr, voteType, timestampStr) + } + + return rows.Err() +} + +func (d *DatabaseDebugger) checkValidatorExists(ctx context.Context, validatorAddr string) (bool, error) { + query := "SELECT COUNT(*) FROM validators WHERE address = ?" + row := d.db.DB().QueryRowContext(ctx, query, validatorAddr) + + var count int + err := row.Scan(&count) + return count > 0, err +} + +func (d *DatabaseDebugger) printAllValidators(ctx context.Context) error { + // First get the total count + countQuery := "SELECT COUNT(*) FROM validators" + row := d.db.DB().QueryRowContext(ctx, countQuery) + var totalCount int + if err := row.Scan(&totalCount); err != nil { + return err + } + + fmt.Printf(" Total validators in database: %d\n", totalCount) + + // Show all validators (no limit) + query := "SELECT address, moniker FROM validators ORDER BY address" + rows, err := d.db.DB().QueryContext(ctx, query) + if err != nil { + return err + } + defer rows.Close() + + fmt.Printf(" All validators:\n") + count := 0 + for rows.Next() { + var address, moniker string + if err := rows.Scan(&address, &moniker); err != nil { + return err + } + count++ + if moniker != "" { + fmt.Printf(" %d. %s (%s)\n", count, address, moniker) + } else { + fmt.Printf(" %d. %s\n", count, address) + } + } + + return rows.Err() +} + +func (d *DatabaseDebugger) getHeightsInWindow(ctx context.Context, window TimeWindow) (int, error) { + query := "SELECT COUNT(*) FROM heights WHERE block_time >= ? AND block_time <= ?" + row := d.db.DB().QueryRowContext(ctx, query, window.Start, window.End) + + var count int + err := row.Scan(&count) + return count, err +} + +func (d *DatabaseDebugger) getVotesForValidatorInWindow(ctx context.Context, validatorAddr string, window TimeWindow) (int, error) { + query := ` + SELECT COUNT(*) + FROM votes v + JOIN heights h ON v.height = h.height + WHERE v.validator_address = ? AND h.block_time >= ? AND h.block_time <= ? + ` + row := d.db.DB().QueryRowContext(ctx, query, validatorAddr, window.Start, window.End) + + var count int + err := row.Scan(&count) + return count, err +} + +func (d *DatabaseDebugger) printSampleValidatorVotes(ctx context.Context, validatorAddr string, limit int) error { + query := ` + SELECT v.height, v.round_number, v.vote_type, v.timestamp, h.block_time + FROM votes v + JOIN heights h ON v.height = h.height + WHERE v.validator_address = ? + ORDER BY v.height DESC + LIMIT ? + ` + rows, err := d.db.DB().QueryContext(ctx, query, validatorAddr, limit) + if err != nil { + return err + } + defer rows.Close() + + fmt.Printf("🔍 Sample votes for validator %s:\n", validatorAddr) + count := 0 + for rows.Next() { + var height, round, voteType int64 + var voteTime, blockTime sql.NullTime + if err := rows.Scan(&height, &round, &voteType, &voteTime, &blockTime); err != nil { + return err + } + + voteTimeStr := "NULL" + if voteTime.Valid { + voteTimeStr = voteTime.Time.Format("15:04:05") + } + + blockTimeStr := "NULL" + if blockTime.Valid { + blockTimeStr = blockTime.Time.Format("15:04:05") + } + + fmt.Printf(" H:%d R:%d Type:%d VoteTime:%s BlockTime:%s\n", + height, round, voteType, voteTimeStr, blockTimeStr) + count++ + } + + if count == 0 { + fmt.Printf(" No votes found for this validator\n") + } + + return rows.Err() +} + +// SearchValidators searches for validators by partial address or moniker. +func (d *DatabaseDebugger) SearchValidators(ctx context.Context, searchTerm string) error { + fmt.Printf("\n=== Validator Search ===\n") + fmt.Printf("Search term: %s\n", searchTerm) + fmt.Printf("=======================\n\n") + + // Search by address (partial match) + query := ` + SELECT address, moniker, voting_power + FROM validators + WHERE address LIKE ? OR LOWER(moniker) LIKE LOWER(?) + ORDER BY address + ` + + searchPattern := "%" + searchTerm + "%" + rows, err := d.db.DB().QueryContext(ctx, query, searchPattern, searchPattern) + if err != nil { + return err + } + defer rows.Close() + + matches := 0 + fmt.Printf("🔍 Matching validators:\n") + for rows.Next() { + var address, moniker string + var votingPower int64 + if err := rows.Scan(&address, &moniker, &votingPower); err != nil { + return err + } + matches++ + if moniker != "" { + fmt.Printf(" %d. %s (%s) - Power: %d\n", matches, address, moniker, votingPower) + } else { + fmt.Printf(" %d. %s - Power: %d\n", matches, address, votingPower) + } + } + + if matches == 0 { + fmt.Printf(" No validators found matching '%s'\n", searchTerm) + fmt.Printf("\n💡 Try:\n") + fmt.Printf(" - Use a shorter search term\n") + fmt.Printf(" - Check the exact validator address format\n") + fmt.Printf(" - Run the 'debug' command to see all available validators\n") + } else { + fmt.Printf("\nFound %d matching validator(s)\n", matches) + } + + return rows.Err() +} diff --git a/pkg/app.go b/pkg/app.go index 0a91d61..05411d9 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -1,13 +1,28 @@ package pkg import ( - "main/pkg/aggregator" + "context" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "main/pkg/analytics" configPkg "main/pkg/config" + "main/pkg/db" "main/pkg/display" + "main/pkg/fetcher" + tmhttp "main/pkg/http" loggerPkg "main/pkg/logger" + "main/pkg/topology" "main/pkg/types" - "time" + butils "github.com/brynbellomy/go-utils" + cnstypes "github.com/cometbft/cometbft/consensus/types" + ctypes "github.com/cometbft/cometbft/types" + "github.com/gdamore/tcell/v2" "github.com/rs/zerolog" ) @@ -15,17 +30,26 @@ type App struct { Logger zerolog.Logger Version string Config *configPkg.Config - Aggregator *aggregator.Aggregator DisplayWrapper *display.Wrapper State *types.State LogChannel chan string + DB *db.DB + ConsensusStore *db.ConsensusStore + + DataFetcher *fetcher.DataFetcher + + mbRPCURLs *butils.Mailbox[string] + rpcURLsLastFetch map[string]time.Time + PauseChannel chan bool IsPaused bool + + cleanupFuncs []func() } func NewApp(config *configPkg.Config, version string) *App { - logChannel := make(chan string) + logChannel := make(chan string, 1000) pauseChannel := make(chan bool) logger := loggerPkg.GetLogger(logChannel, config). @@ -33,80 +57,204 @@ func NewApp(config *configPkg.Config, version string) *App { Str("component", "app_manager"). Logger() + state := types.NewState(config.RPCHost, logger) + + // Initialize database if configured + var database *db.DB + var consensusStore *db.ConsensusStore + + fmt.Println("max retain days", config.MaxRetainDays) + + if config.MaxRetainBlocks > 0 || config.MaxRetainDays > 0 { + dbPath := config.DatabasePath + if dbPath == "" { + // Use default path: ~/.config/tmtop/tmtop.db + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Warn().Err(err).Msg("Could not get user home directory, using current directory for database") + dbPath = "tmtop.db" + } else { + dbPath = filepath.Join(homeDir, ".config", "tmtop", "tmtop.db") + } + } + + fmt.Println("Using database path:", dbPath) + + dbConfig := db.Config{ + DatabasePath: dbPath, + MaxRetainBlocks: config.MaxRetainBlocks, + MaxRetainDays: config.MaxRetainDays, + } + + var err error + database, err = db.New(dbConfig) + if err != nil { + fmt.Println("Failed to initialize database:", err) + os.Exit(-1) + } + + consensusStore = db.NewConsensusStore(database, logger) + logger.Info().Str("path", dbPath).Msg("Database initialized successfully") + } + + fmt.Println("DB:", database) + return &App{ - Logger: logger, - Version: version, - Config: config, - Aggregator: aggregator.NewAggregator(config, logger), - DisplayWrapper: display.NewWrapper(config, logger, pauseChannel, version), - State: types.NewState(), - LogChannel: logChannel, - PauseChannel: pauseChannel, - IsPaused: false, + Logger: logger, + Version: version, + Config: config, + DisplayWrapper: display.NewWrapper(config, state, logger, pauseChannel, version), + State: state, + LogChannel: logChannel, + DB: database, + ConsensusStore: consensusStore, + DataFetcher: fetcher.NewDataFetcher(config, state, logger), + mbRPCURLs: butils.NewMailbox[string](1000), + rpcURLsLastFetch: make(map[string]time.Time), + PauseChannel: pauseChannel, + IsPaused: false, + cleanupFuncs: make([]func(), 0), } } func (a *App) Start() { + // Check if analytics mode is enabled + if a.Config.AnalyticsMode { + a.runAnalyticsMode() + return + } + + // Set up terminal cleanup on exit + defer a.restoreTerminal() + + // Set up database cleanup on exit + if a.DB != nil { + defer func() { + if err := a.DB.Close(); err != nil { + a.Logger.Error().Err(err).Msg("Failed to close database") + } + }() + } + + if a.Config.WithTopologyAPI { + go a.ServeTopology() + topology.LogChannel = a.LogChannel + } + + go a.CrawlRPCURLs() + go a.GoRefreshConsensus() - go a.GoRefreshValidators() - go a.GoRefreshChainInfo() + go a.GoRefreshCometNodeInfo() go a.GoRefreshUpgrade() go a.GoRefreshBlockTime() + go a.GoRefreshNetInfo() + go a.SubscribeCometBFT() go a.DisplayLogs() go a.ListenForPause() + // Start database cleanup routine if database is enabled + if a.DB != nil && a.ConsensusStore != nil { + go a.databaseCleanupRoutine() + } + a.DisplayWrapper.Start() } -func (a *App) GoRefreshConsensus() { - defer a.HandlePanic() - - a.RefreshConsensus() +func (a *App) ServeTopology() { + _ = tmhttp.NewServer( + a.Config.TopologyListenAddr, + topology.WithHTTPTopologyAPI(a.State), + topology.WithHTTPPeersAPI(a.State), + topology.WithHTTPDebugAPI(a.State), + topology.WithFrontendStaticAssets(), + ).Serve() +} - ticker := time.NewTicker(a.Config.RefreshRate) - done := make(chan bool) +func (a *App) CrawlRPCURLs() { + a.mbRPCURLs.Deliver(a.Config.RPCHost) + timer := time.NewTimer(15 * time.Second) for { select { - case <-done: - return - case <-ticker.C: - a.RefreshConsensus() + case <-a.mbRPCURLs.Notify(): + var wg sync.WaitGroup + for _, url := range a.mbRPCURLs.RetrieveAll() { + if lastFetch, ok := a.rpcURLsLastFetch[url]; ok && time.Since(lastFetch) < 15*time.Second { + continue + } + a.rpcURLsLastFetch[url] = time.Now() + + wg.Add(1) + go func() { + defer wg.Done() + a.fetchRPCInfo(url) + }() + } + wg.Wait() + + case <-timer.C: + for _, rpc := range a.State.KnownRPCs().Iter() { + if time.Since(a.rpcURLsLastFetch[rpc.URL]) >= 15*time.Second { + a.mbRPCURLs.Deliver(rpc.URL) + } + } } } } -func (a *App) RefreshConsensus() { - if a.IsPaused { +func (a *App) fetchRPCInfo(rpcURL string) { + netInfo, err := a.DataFetcher.GetNetInfo(rpcURL) + if err != nil { + a.Logger.Error().Err(err).Msg(fmt.Sprintf("error getting /net_info from %s", rpcURL)) return } - consensus, validators, err := a.Aggregator.GetData() - a.State.SetConsensusStateError(err) + status, err := a.DataFetcher.GetCometNodeStatus(rpcURL) if err != nil { - a.Logger.Error().Err(err).Msg("Error getting consensus data") - a.DisplayWrapper.SetState(a.State) + a.Logger.Error().Err(err).Msg(fmt.Sprintf("error getting /status from %s", rpcURL)) return } - err = a.State.SetTendermintResponse(consensus, validators) - a.State.SetConsensusStateError(err) - if err != nil { - a.Logger.Error().Err(err).Msg("Error converting data") - a.DisplayWrapper.SetState(a.State) - return + var rpc types.RPC + if known, ok := a.State.KnownRPCByURL(rpcURL); ok { + rpc = known + } + rpc.ID = string(status.NodeInfo.ID()) + rpc.URL = rpcURL + rpc.Moniker = status.NodeInfo.Moniker + rpc.ValidatorAddress = status.ValidatorInfo.Address.String() + + for _, cv := range a.State.TMValidators { + if strings.EqualFold(cv.CosmosValidator.ConsensusPubkey.Address().String(), rpc.ValidatorAddress) { + rpc.ValidatorMoniker = cv.CosmosValidator.Moniker + break + } } - a.State.SetConsensusStateError(err) - a.DisplayWrapper.SetState(a.State) + a.State.AddKnownRPC(rpc) + a.State.AddRPCPeers(rpcURL, netInfo.Peers) + + for _, peer := range netInfo.Peers { + var peerRPC types.RPC + if known, ok := a.State.KnownRPCByURL(peer.URL()); ok { + peerRPC = known + } + peerRPC.ID = string(peer.NodeInfo.DefaultNodeID) + peerRPC.IP = peer.RemoteIP + peerRPC.URL = peer.URL() + peerRPC.Moniker = peer.NodeInfo.Moniker + a.State.AddKnownRPC(peerRPC) + + a.mbRPCURLs.Deliver(peer.URL()) + } } -func (a *App) GoRefreshValidators() { +func (a *App) GoRefreshConsensus() { defer a.HandlePanic() - a.RefreshValidators() + a.RefreshConsensus() - ticker := time.NewTicker(a.Config.ValidatorsRefreshRate) + ticker := time.NewTicker(a.Config.RefreshRate) done := make(chan bool) for { @@ -114,31 +262,77 @@ func (a *App) GoRefreshValidators() { case <-done: return case <-ticker.C: - a.RefreshValidators() + a.RefreshConsensus() } } } -func (a *App) RefreshValidators() { +func (a *App) RefreshConsensus() { if a.IsPaused { return } - chainValidators, err := a.Aggregator.GetChainValidators() - if err != nil { + var wg sync.WaitGroup + var consensus *cnstypes.RoundState + var vals []types.TMValidator + var consErr error + var valsErr error + + wg.Add(1) + go func() { + defer wg.Done() + consensus, consErr = a.DataFetcher.GetConsensusState() + }() + + wg.Add(1) + go func() { + defer wg.Done() + vals, valsErr = a.DataFetcher.GetValidators() + }() + + wg.Wait() + + if consErr != nil { + a.Logger.Error().Err(consErr).Msg("Could not fetch consensus data") + a.State.SetConsensusStateError(consErr) a.DisplayWrapper.SetState(a.State) - a.Logger.Error().Err(err).Msg("Error getting chain validators") return } - a.State.SetChainValidators(chainValidators) + if valsErr != nil { + a.Logger.Error().Err(valsErr).Msg("Could not fetch validators") + a.State.SetConsensusStateError(valsErr) + a.DisplayWrapper.SetState(a.State) + return + } + + a.State.SetTMValidators(vals) + a.State.SetConsensusStateError(nil) + + // Persist to database if available + if a.ConsensusStore != nil && consensus != nil { + ctx := context.Background() + + // Store validators and height information + if err := a.ConsensusStore.StoreValidators(ctx, consensus.Height, vals); err != nil { + a.Logger.Error().Err(err).Int64("height", consensus.Height).Msg("Failed to persist validators to database") + } + + // Store round data from current state + for hr, roundData := range a.State.VotesByRound.Iter() { + if err := a.ConsensusStore.StoreRoundData(ctx, hr.Height, hr.Round, roundData, vals); err != nil { + a.Logger.Debug().Err(err).Int64("height", hr.Height).Int32("round", hr.Round).Msg("Failed to persist round data") + } + } + } + a.DisplayWrapper.SetState(a.State) } -func (a *App) GoRefreshChainInfo() { +func (a *App) GoRefreshCometNodeInfo() { defer a.HandlePanic() - a.RefreshChainInfo() + a.RefreshCometNodeInfo() ticker := time.NewTicker(a.Config.ChainInfoRefreshRate) done := make(chan bool) @@ -148,25 +342,25 @@ func (a *App) GoRefreshChainInfo() { case <-done: return case <-ticker.C: - a.RefreshChainInfo() + a.RefreshCometNodeInfo() } } } -func (a *App) RefreshChainInfo() { +func (a *App) RefreshCometNodeInfo() { if a.IsPaused { return } - chainInfo, err := a.Aggregator.GetChainInfo() + nodeStatus, err := a.DataFetcher.GetCometNodeStatus(a.State.CurrentRPC().URL) if err != nil { - a.Logger.Error().Err(err).Msg("Error getting chain validators") + a.Logger.Error().Err(err).Msg("Error getting chain info") a.State.SetChainInfoError(err) a.DisplayWrapper.SetState(a.State) return } - a.State.SetChainInfo(&chainInfo.Result) + a.State.SetChainInfo(nodeStatus) a.State.SetChainInfoError(err) a.DisplayWrapper.SetState(a.State) } @@ -205,7 +399,7 @@ func (a *App) RefreshUpgrade() { return } - upgrade, err := a.Aggregator.GetUpgrade() + upgrade, err := a.DataFetcher.GetUpgradePlan() if err != nil { a.Logger.Error().Err(err).Msg("Error getting upgrade") a.State.SetUpgradePlanError(err) @@ -241,7 +435,7 @@ func (a *App) RefreshBlockTime() { return } - blockTime, err := a.Aggregator.GetBlockTime() + blockTime, err := a.DataFetcher.GetBlockTime() if err != nil { a.Logger.Error().Err(err).Msg("Error getting block time") return @@ -251,6 +445,67 @@ func (a *App) RefreshBlockTime() { a.DisplayWrapper.SetState(a.State) } +func (a *App) GoRefreshNetInfo() { + defer a.HandlePanic() + + a.RefreshNetInfo() + + ticker := time.NewTicker(a.Config.RefreshRate) + done := make(chan bool) + + for { + select { + case <-done: + return + case <-ticker.C: + a.RefreshNetInfo() + } + } +} + +func (a *App) RefreshNetInfo() { + if a.IsPaused { + return + } + + netInfo, err := a.DataFetcher.GetNetInfo(a.State.CurrentRPC().URL) + if err != nil { + a.Logger.Error().Err(err).Msg("Error getting netInfo") + return + } + + a.State.SetNetInfo(netInfo) + a.DisplayWrapper.SetState(a.State) +} + +func (a *App) SubscribeCometBFT() { + defer a.HandlePanic() + + fmt.Println("connecting to websocket...") + + mbEvents := butils.NewMailbox[ctypes.TMEventData](1000) + a.DataFetcher.Subscribe(mbEvents, "Vote") + a.DataFetcher.Subscribe(mbEvents, "NewRound") + + for { + select { + case <-mbEvents.Notify(): + events := mbEvents.RetrieveAll() + a.State.AddCometBFTEvents(events) + + // Persist events to database if available + if a.ConsensusStore != nil && len(events) > 0 { + ctx := context.Background() + if err := a.ConsensusStore.StoreCometBFTEvents(ctx, events, a.State.GetTMValidators()); err != nil { + a.Logger.Error().Err(err).Msg("Failed to persist CometBFT events to database") + } + } + + a.DisplayWrapper.SetState(a.State) + } + } +} + func (a *App) DisplayLogs() { for { logString := <-a.LogChannel @@ -267,7 +522,162 @@ func (a *App) ListenForPause() { func (a *App) HandlePanic() { if r := recover(); r != nil { - a.DisplayWrapper.App.Stop() + a.Logger.Error().Interface("panic", r).Msg("Panic caught in goroutine") + a.shutdown() panic(r) } } + +// shutdown performs graceful shutdown with terminal cleanup. +func (a *App) shutdown() { + // Stop the tview application first + if a.DisplayWrapper != nil && a.DisplayWrapper.App != nil { + a.DisplayWrapper.App.Stop() + } + + // Run all registered cleanup functions + a.restoreTerminal() +} + +// restoreTerminal restores terminal state and cleans up. +func (a *App) restoreTerminal() { + // Get the default screen to ensure proper cleanup + screen, err := tcell.NewScreen() + if err == nil && screen != nil { + // Initialize screen briefly to ensure proper state + if err := screen.Init(); err == nil { + // Clear the screen and restore cursor + screen.Clear() + screen.ShowCursor(0, 0) + screen.Sync() + screen.Fini() + } + } + + // Send additional terminal reset sequences + fmt.Print("\033[?25h") // Show cursor + fmt.Print("\033[0m") // Reset colors + fmt.Print("\033[2J") // Clear screen + fmt.Print("\033[H") // Move cursor to top-left + fmt.Print("\033[?1049l") // Exit alternate screen buffer + + // Run any additional cleanup functions + for _, cleanup := range a.cleanupFuncs { + cleanup() + } +} + +// addCleanupFunc registers a function to be called during shutdown. +func (a *App) addCleanupFunc(fn func()) { + a.cleanupFuncs = append(a.cleanupFuncs, fn) +} + +// databaseCleanupRoutine runs periodic database cleanup. +func (a *App) databaseCleanupRoutine() { + ticker := time.NewTicker(1 * time.Hour) // Run cleanup every hour + defer ticker.Stop() + + // Run initial cleanup + ctx := context.Background() + if err := a.DB.CleanupOldData(ctx, db.Config{ + DatabasePath: a.Config.DatabasePath, + MaxRetainBlocks: a.Config.MaxRetainBlocks, + MaxRetainDays: a.Config.MaxRetainDays, + }); err != nil { + a.Logger.Error().Err(err).Msg("Failed to cleanup old database data") + } + + for { + select { + case <-ticker.C: + if err := a.DB.CleanupOldData(ctx, db.Config{ + DatabasePath: a.Config.DatabasePath, + MaxRetainBlocks: a.Config.MaxRetainBlocks, + MaxRetainDays: a.Config.MaxRetainDays, + }); err != nil { + a.Logger.Error().Err(err).Msg("Failed to cleanup old database data") + } else { + a.Logger.Debug().Msg("Database cleanup completed successfully") + } + } + } +} + +// runAnalyticsMode runs the application in analytics mode. +func (a *App) runAnalyticsMode() { + if a.DB == nil { + fmt.Printf("Error: Analytics mode requires database to be enabled. Use --database-path flag.\n") + os.Exit(1) + } + + cliAnalytics := analytics.NewCLIAnalytics(a.DB, a.Logger) + ctx := context.Background() + + switch a.Config.AnalyticsCommand { + case "performance": + if a.Config.AnalyticsValidator == "" { + fmt.Printf("Error: --analytics-validator is required for performance analysis\n") + os.Exit(1) + } + + err := cliAnalytics.PrintValidatorPerformance(ctx, a.Config.AnalyticsValidator, a.Config.AnalyticsTimeWindow) + if err != nil { + fmt.Printf("Error running performance analysis: %v\n", err) + os.Exit(1) + } + + case "rankings": + err := cliAnalytics.PrintValidatorRankings(ctx, a.Config.AnalyticsTimeWindow, 20) + if err != nil { + fmt.Printf("Error running rankings analysis: %v\n", err) + os.Exit(1) + } + + case "timeseries": + if a.Config.AnalyticsValidator == "" { + fmt.Printf("Error: --analytics-validator is required for time series analysis\n") + os.Exit(1) + } + + err := cliAnalytics.PrintPerformanceTimeSeries(ctx, a.Config.AnalyticsValidator, a.Config.AnalyticsTimeWindow) + if err != nil { + fmt.Printf("Error running time series analysis: %v\n", err) + os.Exit(1) + } + + case "debug": + err := cliAnalytics.PrintDatabaseSummary(ctx) + if err != nil { + fmt.Printf("Error running database debug: %v\n", err) + os.Exit(1) + } + + case "diagnose": + if a.Config.AnalyticsValidator == "" { + fmt.Printf("Error: --analytics-validator is required for validator diagnosis\n") + os.Exit(1) + } + + err := cliAnalytics.DiagnoseValidator(ctx, a.Config.AnalyticsValidator, a.Config.AnalyticsTimeWindow) + if err != nil { + fmt.Printf("Error running validator diagnosis: %v\n", err) + os.Exit(1) + } + + case "search": + if a.Config.AnalyticsValidator == "" { + fmt.Printf("Error: --analytics-validator is required as search term for validator search\n") + os.Exit(1) + } + + err := cliAnalytics.SearchValidators(ctx, a.Config.AnalyticsValidator) + if err != nil { + fmt.Printf("Error running validator search: %v\n", err) + os.Exit(1) + } + + default: + fmt.Printf("Error: Unknown analytics command '%s'. Available commands: performance, rankings, timeseries, debug, diagnose, search\n", a.Config.AnalyticsCommand) + os.Exit(1) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 8a7bcf5..e1f553a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,6 +23,17 @@ type InputConfig struct { BlocksBehind uint64 LCDHost string Timezone string + WithTopologyAPI bool + TopologyListenAddr string + DatabasePath string + MaxRetainBlocks int64 + MaxRetainDays int + + // Analytics configuration + AnalyticsMode bool + AnalyticsValidator string + AnalyticsTimeWindow string + AnalyticsCommand string } type ChainType string @@ -99,6 +110,15 @@ func ParseAndValidateConfig(input InputConfig) (*Config, error) { BlocksBehind: input.BlocksBehind, LCDHost: input.LCDHost, Timezone: timezone, + WithTopologyAPI: input.WithTopologyAPI, + TopologyListenAddr: input.TopologyListenAddr, + DatabasePath: input.DatabasePath, + MaxRetainBlocks: input.MaxRetainBlocks, + MaxRetainDays: input.MaxRetainDays, + AnalyticsMode: input.AnalyticsMode, + AnalyticsValidator: input.AnalyticsValidator, + AnalyticsTimeWindow: input.AnalyticsTimeWindow, + AnalyticsCommand: input.AnalyticsCommand, } return config, nil @@ -121,6 +141,17 @@ type Config struct { BlocksBehind uint64 LCDHost string Timezone *time.Location + WithTopologyAPI bool + TopologyListenAddr string + DatabasePath string + MaxRetainBlocks int64 + MaxRetainDays int + + // Analytics configuration + AnalyticsMode bool + AnalyticsValidator string + AnalyticsTimeWindow string + AnalyticsCommand string } func (c Config) GetProviderOrConsumerHost() string { diff --git a/pkg/db/consensus_store.go b/pkg/db/consensus_store.go new file mode 100644 index 0000000..2993bbf --- /dev/null +++ b/pkg/db/consensus_store.go @@ -0,0 +1,329 @@ +package db + +import ( + "context" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "time" + + "main/pkg/db/sqlc" + "main/pkg/types" + + cptypes "github.com/cometbft/cometbft/proto/tendermint/types" + ctypes "github.com/cometbft/cometbft/types" + "github.com/rs/zerolog" +) + +// ConsensusStore handles persistence of consensus data. +type ConsensusStore struct { + db *DB + logger zerolog.Logger +} + +// NewConsensusStore creates a new consensus store. +func NewConsensusStore(db *DB, logger zerolog.Logger) *ConsensusStore { + return &ConsensusStore{ + db: db, + logger: logger.With().Str("component", "consensus_store").Logger(), + } +} + +// StoreRoundData persists round data from RoundDataMap. +func (cs *ConsensusStore) StoreRoundData( + ctx context.Context, + height int64, + round int32, + roundData *types.RoundData, + validators types.TMValidators, +) error { + return cs.db.WithTx(ctx, func(q *sqlc.Queries) error { + // Store the round + roundParams := sqlc.UpsertRoundParams{ + Height: height, + RoundNumber: int64(round), + Step: sql.NullInt64{}, // We can add step tracking later + StartTime: sql.NullTime{ + Time: time.Now(), + Valid: true, + }, + ProposerAddress: sql.NullString{}, + } + + // Set proposer if we have one + if roundData.Proposers != nil && len(roundData.Proposers) > 0 { + // Get first proposer from the set + for proposer := range roundData.Proposers { + roundParams.ProposerAddress = sql.NullString{ + String: proposer, + Valid: true, + } + break // Just get the first one + } + } + + if _, err := q.UpsertRound(ctx, roundParams); err != nil { + return fmt.Errorf("failed to store round: %w", err) + } + + // Store votes for this round + for validatorAddr, voteMap := range roundData.Votes { + for voteType, blockID := range voteMap { + voteParams := sqlc.UpsertVoteParams{ + Height: height, + RoundNumber: int64(round), + ValidatorHexAddress: validatorAddr, + VoteType: int64(voteType), + Signature: sql.NullString{}, // We can add signature tracking later + Timestamp: sql.NullTime{ + Time: time.Now(), + Valid: true, + }, + } + + // Set block hash if not nil vote + if !blockID.IsZero() { + voteParams.BlockHash = sql.NullString{ + String: blockID.Hash.String(), + Valid: true, + } + } + + if _, err := q.UpsertVote(ctx, voteParams); err != nil { + cs.logger.Error().Err(err). + Str("validator", validatorAddr). + Int64("height", height). + Int32("round", round). + Int64("vote_type", int64(voteType)). + Msg("Failed to store vote") + // Continue with other votes instead of failing completely + } + } + } + + return nil + }) +} + +// StoreValidators persists validator information and creates snapshots for a height. +func (cs *ConsensusStore) StoreValidators(ctx context.Context, height int64, validators types.TMValidators) error { + return cs.db.WithTx(ctx, func(q *sqlc.Queries) error { + // Store height record first + heightParams := sqlc.UpsertHeightParams{ + Height: height, + BlockHash: sql.NullString{}, // Can be added later + BlockTime: sql.NullTime{}, // Can be added later + ProposerAddress: sql.NullString{}, // Can be added later + TotalValidators: sql.NullInt64{Int64: int64(len(validators)), Valid: true}, + } + + if _, err := q.UpsertHeight(ctx, heightParams); err != nil { + return fmt.Errorf("failed to store height: %w", err) + } + + // Store validators and their snapshots + for _, validator := range validators { + // Determine operator address - use ChainValidator.Address if available, otherwise use hex address + operatorAddress := validator.GetDisplayAddress() // Default to hex address + if validator.ChainValidator != nil && validator.ChainValidator.Address != "" { + operatorAddress = validator.ChainValidator.Address + } + + // Upsert validator + validatorParams := sqlc.UpsertValidatorParams{ + OperatorAddress: operatorAddress, + HexAddress: validator.GetDisplayAddress(), + PublicKey: hex.EncodeToString(validator.PubKey.Bytes()), + VotingPower: validator.VotingPower, + Moniker: sql.NullString{}, + } + + // Add moniker if available + if validator.ChainValidator != nil && validator.ChainValidator.Moniker != "" { + validatorParams.Moniker = sql.NullString{ + String: validator.ChainValidator.Moniker, + Valid: true, + } + } + + if _, err := q.UpsertValidator(ctx, validatorParams); err != nil { + cs.logger.Error().Err(err). + Str("validator", validator.GetDisplayAddress()). + Msg("Failed to store validator") + continue + } + + // Create validator snapshot for this height + votingPowerPercent := 0.0 + if validator.VotingPowerPercent != nil { + if pct, _ := validator.VotingPowerPercent.Float64(); pct >= 0 { + votingPowerPercent = pct + } + } + + snapshotParams := sqlc.UpsertValidatorSnapshotParams{ + Height: height, + ValidatorHexAddress: validator.GetDisplayAddress(), + VotingPower: validator.VotingPower, + VotingPowerPercent: sql.NullFloat64{Float64: votingPowerPercent, Valid: true}, + IsProposer: sql.NullBool{}, // Will be set when we know the proposer + } + + if _, err := q.UpsertValidatorSnapshot(ctx, snapshotParams); err != nil { + cs.logger.Error().Err(err). + Str("validator", validator.GetDisplayAddress()). + Int64("height", height). + Msg("Failed to store validator snapshot") + } + } + + return nil + }) +} + +// StoreConsensusEvent records a consensus milestone event. +func (cs *ConsensusStore) StoreConsensusEvent(ctx context.Context, height int64, round int32, eventType string, eventData interface{}) error { + var eventDataJSON sql.NullString + + if eventData != nil { + data, err := json.Marshal(eventData) + if err != nil { + cs.logger.Warn().Err(err).Msg("Failed to marshal event data") + } else { + eventDataJSON = sql.NullString{String: string(data), Valid: true} + } + } + + params := sqlc.CreateConsensusEventParams{ + Height: height, + RoundNumber: int64(round), + EventType: eventType, + EventData: eventDataJSON, + Timestamp: time.Now(), + } + + _, err := cs.db.queries.CreateConsensusEvent(ctx, params) + if err != nil { + return fmt.Errorf("failed to store consensus event: %w", err) + } + + return nil +} + +// StoreCometBFTEvents processes and stores CometBFT events. +func (cs *ConsensusStore) StoreCometBFTEvents(ctx context.Context, events []ctypes.TMEventData, validators types.TMValidators) error { + for _, event := range events { + switch x := event.(type) { + case ctypes.EventDataNewRound: + // Store new round event + if err := cs.StoreConsensusEvent(ctx, x.Height, x.Round, "new_round", map[string]interface{}{ + "proposer": x.Proposer.Address.String(), + }); err != nil { + cs.logger.Error().Err(err).Int64("height", x.Height).Int32("round", x.Round).Msg("Failed to store new round event") + } + + // Update proposer in validator snapshots + if err := cs.updateProposerInSnapshots(ctx, x.Height, x.Proposer.Address.String()); err != nil { + cs.logger.Error().Err(err).Int64("height", x.Height).Str("proposer", x.Proposer.Address.String()).Msg("Failed to update proposer") + } + + case ctypes.EventDataVote: + // Individual votes are handled in StoreRoundData, but we can track vote events here + voteType := "prevote" + if x.Vote.Type == cptypes.PrecommitType { + voteType = "precommit" + } + + if err := cs.StoreConsensusEvent(ctx, x.Vote.Height, x.Vote.Round, voteType, map[string]interface{}{ + "validator": x.Vote.ValidatorAddress.String(), + "block_id": x.Vote.BlockID.Hash.String(), + }); err != nil { + cs.logger.Debug().Err(err).Msg("Failed to store vote event") + } + } + } + + return nil +} + +// updateProposerInSnapshots updates the is_proposer flag for validator snapshots. +func (cs *ConsensusStore) updateProposerInSnapshots(ctx context.Context, height int64, proposerAddr string) error { + // First, clear any existing proposer flags for this height + _, err := cs.db.db.ExecContext(ctx, + "UPDATE validator_snapshots SET is_proposer = FALSE WHERE height = ?", + height) + if err != nil { + return fmt.Errorf("failed to clear proposer flags: %w", err) + } + + // Then set the current proposer + _, err = cs.db.db.ExecContext(ctx, + "UPDATE validator_snapshots SET is_proposer = TRUE WHERE height = ? AND validator_hex_address = ?", + height, proposerAddr) + if err != nil { + return fmt.Errorf("failed to set proposer flag: %w", err) + } + + return nil +} + +// LoadRoundDataMap loads historical round data from the database. +func (cs *ConsensusStore) LoadRoundDataMap(ctx context.Context, fromHeight, toHeight int64) (*types.RoundDataMap, error) { + roundDataMap := types.NewRoundDataMap() + + // Get rounds in the specified range + rounds, err := cs.db.queries.GetRoundsInRange(ctx, sqlc.GetRoundsInRangeParams{ + Height: fromHeight, + Height_2: toHeight, + }) + if err != nil { + return nil, fmt.Errorf("failed to get rounds: %w", err) + } + + for _, round := range rounds { + height := round.Height + roundNum := int32(round.RoundNumber) + + // Get votes for this round + votes, err := cs.db.queries.GetVotesForRound(ctx, sqlc.GetVotesForRoundParams{ + Height: height, + RoundNumber: round.RoundNumber, + }) + if err != nil { + cs.logger.Error().Err(err).Int64("height", height).Int64("round", round.RoundNumber).Msg("Failed to get votes for round") + continue + } + + // Add proposer to round data + if round.ProposerAddress.Valid { + roundDataMap.AddProposer(height, roundNum, round.ProposerAddress.String) + } + + // Add votes to round data + for _, vote := range votes { + var blockID ctypes.BlockID + if vote.BlockHash.Valid { + // Parse block hash - simplified for now + blockID = ctypes.BlockID{ + Hash: []byte(vote.BlockHash.String), + } + } + + roundDataMap.AddVote(height, roundNum, vote.ValidatorHexAddress, + cptypes.SignedMsgType(vote.VoteType), blockID) + } + } + + return roundDataMap, nil +} + +// GetRecentRounds returns recent round data for display. +func (cs *ConsensusStore) GetRecentRounds(ctx context.Context, limit int32) ([]sqlc.Round, error) { + return cs.db.queries.GetRecentRounds(ctx, int64(limit)) +} + +// GetValidatorsForHeight returns validator snapshots for a specific height. +func (cs *ConsensusStore) GetValidatorsForHeight(ctx context.Context, height int64) ([]sqlc.GetValidatorsByHeightRow, error) { + return cs.db.queries.GetValidatorsByHeight(ctx, height) +} diff --git a/pkg/db/db.go b/pkg/db/db.go new file mode 100644 index 0000000..8d173d9 --- /dev/null +++ b/pkg/db/db.go @@ -0,0 +1,286 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "os" + "path/filepath" + "time" + + "main/pkg/db/sqlc" + + _ "modernc.org/sqlite" +) + +type DB struct { + db *sql.DB + queries *sqlc.Queries +} + +type Config struct { + DatabasePath string + MaxRetainDays int // How many days of data to retain + MaxRetainBlocks int64 // How many blocks of data to retain (alternative to days) +} + +func New(config Config) (*DB, error) { + // Create directory if it doesn't exist + if err := os.MkdirAll(filepath.Dir(config.DatabasePath), 0o755); err != nil { + return nil, fmt.Errorf("failed to create database directory: %w", err) + } + + // Open SQLite database + db, err := sql.Open("sqlite", config.DatabasePath) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + // Configure SQLite for better performance + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + return nil, fmt.Errorf("failed to set WAL mode: %w", err) + } + if _, err := db.Exec("PRAGMA synchronous=NORMAL"); err != nil { + return nil, fmt.Errorf("failed to set synchronous mode: %w", err) + } + if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil { + return nil, fmt.Errorf("failed to enable foreign keys: %w", err) + } + + service := &DB{ + db: db, + queries: sqlc.New(db), + } + + // Run migrations + if err := service.migrate(); err != nil { + return nil, fmt.Errorf("failed to run migrations: %w", err) + } + + return service, nil +} + +func (d *DB) Close() error { + return d.db.Close() +} + +func (d *DB) Queries() *sqlc.Queries { + return d.queries +} + +func (d *DB) DB() *sql.DB { + return d.db +} + +// WithTx executes a function within a database transaction. +func (d *DB) WithTx(ctx context.Context, fn func(*sqlc.Queries) error) error { + tx, err := d.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + q := d.queries.WithTx(tx) + if err := fn(q); err != nil { + return err + } + + return tx.Commit() +} + +// migrate runs database migrations. +func (d *DB) migrate() error { + // Create migrations table if it doesn't exist + createMigrationsTable := ` + CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version TEXT NOT NULL UNIQUE, + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + ` + + if _, err := d.db.Exec(createMigrationsTable); err != nil { + return fmt.Errorf("failed to create migrations table: %w", err) + } + + // Check if migration 001_initial has been applied + var count int + err := d.db.QueryRow("SELECT COUNT(*) FROM migrations WHERE version = '001_initial'").Scan(&count) + if err != nil { + return fmt.Errorf("failed to check migration status: %w", err) + } + + // If migration already applied, skip it + if count > 0 { + return nil + } + + // Read migration file + migrationSQL := `-- Initial schema for tmtop database + +-- Validators table to store validator information +CREATE TABLE validators ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL UNIQUE, + hex_address TEXT NOT NULL UNIQUE, + public_key TEXT NOT NULL, + voting_power INTEGER NOT NULL, + moniker TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Index for fast validator lookups +CREATE INDEX idx_validators_hex_address ON validators(hex_address); +CREATE INDEX idx_validators_operator_address ON validators(operator_address); + +-- Heights table to store block height information +CREATE TABLE heights ( + height INTEGER PRIMARY KEY, + block_hash TEXT, + block_time DATETIME, + proposer_address TEXT, + total_validators INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (proposer_address) REFERENCES validators(hex_address) +); + +-- Rounds table to store consensus round information +CREATE TABLE rounds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + round_number INTEGER NOT NULL, + step INTEGER, + start_time DATETIME, + proposer_address TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(height, round_number), + FOREIGN KEY (height) REFERENCES heights(height), + FOREIGN KEY (proposer_address) REFERENCES validators(hex_address) +); + +-- Index for efficient round queries +CREATE INDEX idx_rounds_height_round ON rounds(height, round_number); +CREATE INDEX idx_rounds_height_desc ON rounds(height DESC); + +-- Votes table to store individual validator votes +CREATE TABLE votes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + round_number INTEGER NOT NULL, + validator_hex_address TEXT NOT NULL, + vote_type INTEGER NOT NULL, -- 1 = prevote, 2 = precommit + block_hash TEXT, -- NULL for nil votes + signature TEXT, + timestamp DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(height, round_number, validator_hex_address, vote_type), + FOREIGN KEY (height) REFERENCES heights(height), + FOREIGN KEY (validator_hex_address) REFERENCES validators(hex_address) +); + +-- Indexes for efficient vote queries +CREATE INDEX idx_votes_height_round ON votes(height, round_number); +CREATE INDEX idx_votes_validator ON votes(validator_hex_address); +CREATE INDEX idx_votes_height_round_type ON votes(height, round_number, vote_type); + +-- Consensus events table for tracking important consensus milestones +CREATE TABLE consensus_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + round_number INTEGER NOT NULL, + event_type TEXT NOT NULL, -- 'new_round', 'proposal', 'prevote_majority', 'precommit_majority', 'block_commit' + event_data TEXT, -- JSON data for additional context + timestamp DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (height) REFERENCES heights(height) +); + +-- Index for consensus event queries +CREATE INDEX idx_consensus_events_height_round ON consensus_events(height, round_number); +CREATE INDEX idx_consensus_events_type ON consensus_events(event_type); +CREATE INDEX idx_consensus_events_timestamp ON consensus_events(timestamp); + +-- Validator snapshots table to track voting power changes over time +CREATE TABLE validator_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + validator_hex_address TEXT NOT NULL, + voting_power INTEGER NOT NULL, + voting_power_percent REAL, + is_proposer BOOLEAN DEFAULT FALSE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(height, validator_hex_address), + FOREIGN KEY (height) REFERENCES heights(height), + FOREIGN KEY (validator_hex_address) REFERENCES validators(hex_address) +); + +-- Index for efficient validator snapshot queries +CREATE INDEX idx_validator_snapshots_height ON validator_snapshots(height); +CREATE INDEX idx_validator_snapshots_validator ON validator_snapshots(validator_hex_address);` + + // Execute migration + if _, err := d.db.Exec(migrationSQL); err != nil { + return fmt.Errorf("failed to execute migration: %w", err) + } + + // Record that migration was applied + _, err = d.db.Exec("INSERT INTO migrations (version) VALUES ('001_initial')") + if err != nil { + return fmt.Errorf("failed to record migration: %w", err) + } + + return nil +} + +// CleanupOldData removes data older than the configured retention period. +func (d *DB) CleanupOldData(ctx context.Context, config Config) error { + var cutoffHeight int64 + + if config.MaxRetainBlocks > 0 { + // Get latest height and calculate cutoff + latest, err := d.queries.GetLatestHeight(ctx) + if err != nil { + if err == sql.ErrNoRows { + return nil // No data to clean up + } + return fmt.Errorf("failed to get latest height: %w", err) + } + cutoffHeight = latest.Height - config.MaxRetainBlocks + } else if config.MaxRetainDays > 0 { + // Calculate cutoff based on days + _ = time.Now().AddDate(0, 0, -config.MaxRetainDays) + // This is a simplified approach - in a real implementation you'd want to + // find the height that corresponds to the cutoff time + cutoffHeight = 0 // For now, we'll rely on MaxRetainBlocks + } else { + return nil // No cleanup configured + } + + if cutoffHeight <= 0 { + return nil + } + + // Clean up old data in order (due to foreign key constraints) + if err := d.queries.DeleteConsensusEventsOlderThan(ctx, cutoffHeight); err != nil { + return fmt.Errorf("failed to delete old consensus events: %w", err) + } + + if err := d.queries.DeleteVotesOlderThan(ctx, cutoffHeight); err != nil { + return fmt.Errorf("failed to delete old votes: %w", err) + } + + if err := d.queries.DeleteValidatorSnapshotsOlderThan(ctx, cutoffHeight); err != nil { + return fmt.Errorf("failed to delete old validator snapshots: %w", err) + } + + if err := d.queries.DeleteRoundsOlderThan(ctx, cutoffHeight); err != nil { + return fmt.Errorf("failed to delete old rounds: %w", err) + } + + if err := d.queries.DeleteHeightsOlderThan(ctx, cutoffHeight); err != nil { + return fmt.Errorf("failed to delete old heights: %w", err) + } + + return nil +} diff --git a/pkg/db/migrations/001_initial.sql b/pkg/db/migrations/001_initial.sql new file mode 100644 index 0000000..812b537 --- /dev/null +++ b/pkg/db/migrations/001_initial.sql @@ -0,0 +1,102 @@ +-- Initial schema for tmtop database + +-- Validators table to store validator information +CREATE TABLE validators ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL UNIQUE, + hex_address TEXT NOT NULL UNIQUE, + public_key TEXT NOT NULL, + voting_power INTEGER NOT NULL, + moniker TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Index for fast validator lookups +CREATE INDEX idx_validators_hex_address ON validators(hex_address); +CREATE INDEX idx_validators_operator_address ON validators(operator_address); + +-- Heights table to store block height information +CREATE TABLE heights ( + height INTEGER PRIMARY KEY, + block_hash TEXT, + block_time DATETIME, + proposer_address TEXT, + total_validators INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (proposer_address) REFERENCES validators(hex_address) +); + +-- Rounds table to store consensus round information +CREATE TABLE rounds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + round_number INTEGER NOT NULL, + step INTEGER, + start_time DATETIME, + proposer_address TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(height, round_number), + FOREIGN KEY (height) REFERENCES heights(height), + FOREIGN KEY (proposer_address) REFERENCES validators(hex_address) +); + +-- Index for efficient round queries +CREATE INDEX idx_rounds_height_round ON rounds(height, round_number); +CREATE INDEX idx_rounds_height_desc ON rounds(height DESC); + +-- Votes table to store individual validator votes +CREATE TABLE votes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + round_number INTEGER NOT NULL, + validator_hex_address TEXT NOT NULL, + vote_type INTEGER NOT NULL, -- 1 = prevote, 2 = precommit + block_hash TEXT, -- NULL for nil votes + signature TEXT, + timestamp DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(height, round_number, validator_hex_address, vote_type), + FOREIGN KEY (height) REFERENCES heights(height), + FOREIGN KEY (validator_hex_address) REFERENCES validators(hex_address) +); + +-- Indexes for efficient vote queries +CREATE INDEX idx_votes_height_round ON votes(height, round_number); +CREATE INDEX idx_votes_validator ON votes(validator_hex_address); +CREATE INDEX idx_votes_height_round_type ON votes(height, round_number, vote_type); + +-- Consensus events table for tracking important consensus milestones +CREATE TABLE consensus_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + round_number INTEGER NOT NULL, + event_type TEXT NOT NULL, -- 'new_round', 'proposal', 'prevote_majority', 'precommit_majority', 'block_commit' + event_data TEXT, -- JSON data for additional context + timestamp DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (height) REFERENCES heights(height) +); + +-- Index for consensus event queries +CREATE INDEX idx_consensus_events_height_round ON consensus_events(height, round_number); +CREATE INDEX idx_consensus_events_type ON consensus_events(event_type); +CREATE INDEX idx_consensus_events_timestamp ON consensus_events(timestamp); + +-- Validator snapshots table to track voting power changes over time +CREATE TABLE validator_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + height INTEGER NOT NULL, + validator_hex_address TEXT NOT NULL, + voting_power INTEGER NOT NULL, + voting_power_percent REAL, + is_proposer BOOLEAN DEFAULT FALSE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(height, validator_hex_address), + FOREIGN KEY (height) REFERENCES heights(height), + FOREIGN KEY (validator_hex_address) REFERENCES validators(hex_address) +); + +-- Index for efficient validator snapshot queries +CREATE INDEX idx_validator_snapshots_height ON validator_snapshots(height); +CREATE INDEX idx_validator_snapshots_validator ON validator_snapshots(validator_hex_address); \ No newline at end of file diff --git a/pkg/db/queries/analytics.sql b/pkg/db/queries/analytics.sql new file mode 100644 index 0000000..3cbc46c --- /dev/null +++ b/pkg/db/queries/analytics.sql @@ -0,0 +1,188 @@ +-- name: GetValidatorSigningEfficiency :one +-- Calculate signing efficiency for a validator over a time window +WITH block_participation AS ( + SELECT + h.height, + h.block_time, + COUNT(v.id) as votes_cast, + CASE WHEN COUNT(v.id) > 0 THEN 1 ELSE 0 END as participated + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY h.height, h.block_time +) +SELECT + COALESCE(COUNT(*), 0) as total_blocks, + COALESCE(SUM(participated), 0) as blocks_signed, + COALESCE(COUNT(*) - SUM(participated), 0) as blocks_missed, + COALESCE(ROUND(100.0 * SUM(participated) / NULLIF(COUNT(*), 0), 2), 0.0) as signing_efficiency +FROM block_participation; + +-- name: GetValidatorConsensusParticipation :one +-- Calculate consensus participation rates for prevotes and precommits +WITH vote_participation AS ( + SELECT + r.height, + r.round_number, + COUNT(CASE WHEN v.vote_type = 1 THEN 1 END) as prevotes, + COUNT(CASE WHEN v.vote_type = 2 THEN 1 END) as precommits, + COUNT(DISTINCT r.round_number) as total_rounds + FROM rounds r + LEFT JOIN votes v ON r.height = v.height AND r.round_number = v.round_number AND v.validator_hex_address = ? + LEFT JOIN heights h ON r.height = h.height + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY r.height, r.round_number +) +SELECT + COALESCE(COUNT(*), 0) as total_rounds, + COALESCE(SUM(CASE WHEN prevotes > 0 THEN 1 ELSE 0 END), 0) as rounds_with_prevote, + COALESCE(SUM(CASE WHEN precommits > 0 THEN 1 ELSE 0 END), 0) as rounds_with_precommit, + COALESCE(ROUND(100.0 * SUM(CASE WHEN prevotes > 0 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 2), 0.0) as prevote_rate, + COALESCE(ROUND(100.0 * SUM(CASE WHEN precommits > 0 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 2), 0.0) as precommit_rate +FROM vote_participation; + +-- name: GetValidatorMissedBlockStreaks :many +-- Find consecutive missed block sequences for a validator +WITH block_sequence AS ( + SELECT + h.height, + h.block_time, + CASE WHEN v.id IS NULL THEN 1 ELSE 0 END as missed, + ROW_NUMBER() OVER (ORDER BY h.height) as rn + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? +), +miss_groups AS ( + SELECT + height, + block_time, + missed, + rn - ROW_NUMBER() OVER (PARTITION BY missed ORDER BY height) as grp + FROM block_sequence + WHERE missed = 1 +) +SELECT + COUNT(*) as consecutive_misses, + MIN(height) as streak_start_height, + MAX(height) as streak_end_height, + MIN(block_time) as streak_start_time, + MAX(block_time) as streak_end_time +FROM miss_groups +GROUP BY grp +HAVING COUNT(*) > 0 +ORDER BY consecutive_misses DESC; + +-- name: GetAllValidatorsSigningEfficiency :many +-- Get signing efficiency for all validators over a time window +WITH validator_participation AS ( + SELECT + vals.hex_address, + vals.moniker, + h.height, + CASE WHEN v.id IS NOT NULL THEN 1 ELSE 0 END as participated + FROM validators vals + CROSS JOIN heights h + LEFT JOIN votes v ON vals.hex_address = v.validator_hex_address AND h.height = v.height + WHERE h.block_time >= ? AND h.block_time <= ? +) +SELECT + hex_address, + moniker, + COUNT(*) as total_blocks, + SUM(participated) as blocks_signed, + COUNT(*) - SUM(participated) as blocks_missed, + ROUND(100.0 * SUM(participated) / COUNT(*), 2) as signing_efficiency +FROM validator_participation +GROUP BY hex_address, moniker +ORDER BY signing_efficiency DESC; + +-- name: GetValidatorPerformanceTimeSeries :many +-- Get signing efficiency over time buckets (hourly) +WITH time_buckets AS ( + SELECT + datetime(h.block_time, 'start of hour') as time_bucket, + COUNT(*) as blocks_in_bucket, + SUM(CASE WHEN v.id IS NOT NULL THEN 1 ELSE 0 END) as blocks_signed + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY datetime(h.block_time, 'start of hour') +) +SELECT + time_bucket, + COALESCE(blocks_in_bucket, 0) as blocks_in_bucket, + COALESCE(blocks_signed, 0) as blocks_signed, + COALESCE(blocks_in_bucket - blocks_signed, 0) as blocks_missed, + COALESCE(ROUND(100.0 * blocks_signed / NULLIF(blocks_in_bucket, 0), 2), 0.0) as signing_efficiency +FROM time_buckets +ORDER BY time_bucket; + +-- name: GetValidatorRanking :many +-- Get validator performance ranking with multiple metrics +WITH validator_metrics AS ( + SELECT + vals.hex_address, + vals.moniker, + -- Block signing metrics + COUNT(h.height) as total_blocks, + COUNT(v.id) as blocks_signed, + ROUND(100.0 * COUNT(v.id) / COUNT(h.height), 2) as signing_efficiency, + + -- Consensus participation + COUNT(CASE WHEN v.vote_type = 1 THEN 1 END) as prevotes_cast, + COUNT(CASE WHEN v.vote_type = 2 THEN 1 END) as precommits_cast, + + -- Latest voting power + COALESCE(MAX(vs.voting_power), 0) as voting_power + FROM validators vals + CROSS JOIN heights h + LEFT JOIN votes v ON vals.hex_address = v.validator_hex_address AND h.height = v.height + LEFT JOIN validator_snapshots vs ON vals.hex_address = vs.validator_hex_address AND h.height = vs.height + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY vals.hex_address, vals.moniker +) +SELECT + hex_address, + moniker, + total_blocks, + blocks_signed, + total_blocks - blocks_signed as blocks_missed, + signing_efficiency, + prevotes_cast, + precommits_cast, + voting_power, + RANK() OVER (ORDER BY signing_efficiency DESC, voting_power DESC) as efficiency_rank +FROM validator_metrics +ORDER BY efficiency_rank; + +-- name: GetValidatorUptime :one +-- Calculate validator uptime metrics over time window +WITH block_stats AS ( + SELECT + COUNT(*) as total_blocks_in_window, + COUNT(v.id) as blocks_participated, + MIN(h.block_time) as window_start, + MAX(h.block_time) as window_end + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? +) +SELECT + COALESCE(bs.total_blocks_in_window, 0) as total_blocks_in_window, + COALESCE(bs.blocks_participated, 0) as blocks_participated, + COALESCE(bs.total_blocks_in_window - bs.blocks_participated, 0) as blocks_missed, + COALESCE(ROUND(100.0 * bs.blocks_participated / NULLIF(bs.total_blocks_in_window, 0), 2), 0.0) as uptime_percentage, + bs.window_start, + bs.window_end +FROM block_stats bs; + +-- name: GetProposerPerformance :one +-- Calculate proposer performance metrics (when validator is selected as proposer) +SELECT + COUNT(*) as blocks_proposed, + COUNT(CASE WHEN r.proposer_address = ? THEN 1 END) as successful_proposals, + ROUND(100.0 * COUNT(CASE WHEN r.proposer_address = ? THEN 1 END) / COUNT(*), 2) as proposal_success_rate +FROM rounds r +JOIN heights h ON r.height = h.height +WHERE r.proposer_address = ? AND h.block_time >= ? AND h.block_time <= ?; \ No newline at end of file diff --git a/pkg/db/queries/consensus_events.sql b/pkg/db/queries/consensus_events.sql new file mode 100644 index 0000000..6e7880c --- /dev/null +++ b/pkg/db/queries/consensus_events.sql @@ -0,0 +1,26 @@ +-- name: GetConsensusEvent :one +SELECT * FROM consensus_events WHERE id = ? LIMIT 1; + +-- name: GetConsensusEventsForRound :many +SELECT * FROM consensus_events +WHERE height = ? AND round_number = ? +ORDER BY timestamp; + +-- name: GetConsensusEventsForHeight :many +SELECT * FROM consensus_events WHERE height = ? ORDER BY round_number, timestamp; + +-- name: GetRecentConsensusEvents :many +SELECT * FROM consensus_events ORDER BY timestamp DESC LIMIT ?; + +-- name: GetConsensusEventsByType :many +SELECT * FROM consensus_events +WHERE height >= ? AND event_type = ? +ORDER BY height DESC, round_number DESC, timestamp DESC; + +-- name: CreateConsensusEvent :one +INSERT INTO consensus_events (height, round_number, event_type, event_data, timestamp) +VALUES (?, ?, ?, ?, ?) +RETURNING *; + +-- name: DeleteConsensusEventsOlderThan :exec +DELETE FROM consensus_events WHERE height < ?; \ No newline at end of file diff --git a/pkg/db/queries/heights.sql b/pkg/db/queries/heights.sql new file mode 100644 index 0000000..737f477 --- /dev/null +++ b/pkg/db/queries/heights.sql @@ -0,0 +1,26 @@ +-- name: GetHeight :one +SELECT * FROM heights WHERE height = ? LIMIT 1; + +-- name: GetHeights :many +SELECT * FROM heights ORDER BY height DESC LIMIT ?; + +-- name: GetLatestHeight :one +SELECT * FROM heights ORDER BY height DESC LIMIT 1; + +-- name: CreateHeight :one +INSERT INTO heights (height, block_hash, block_time, proposer_address, total_validators) +VALUES (?, ?, ?, ?, ?) +RETURNING *; + +-- name: UpsertHeight :one +INSERT INTO heights (height, block_hash, block_time, proposer_address, total_validators) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(height) DO UPDATE SET + block_hash = excluded.block_hash, + block_time = excluded.block_time, + proposer_address = excluded.proposer_address, + total_validators = excluded.total_validators +RETURNING *; + +-- name: DeleteHeightsOlderThan :exec +DELETE FROM heights WHERE height < ?; \ No newline at end of file diff --git a/pkg/db/queries/rounds.sql b/pkg/db/queries/rounds.sql new file mode 100644 index 0000000..c1bed40 --- /dev/null +++ b/pkg/db/queries/rounds.sql @@ -0,0 +1,33 @@ +-- name: GetRound :one +SELECT * FROM rounds WHERE height = ? AND round_number = ? LIMIT 1; + +-- name: GetRoundsForHeight :many +SELECT * FROM rounds WHERE height = ? ORDER BY round_number; + +-- name: GetRecentRounds :many +SELECT * FROM rounds ORDER BY height DESC, round_number DESC LIMIT ?; + +-- name: GetRoundsInRange :many +SELECT * FROM rounds +WHERE height >= ? AND height <= ? +ORDER BY height DESC, round_number DESC; + +-- name: CreateRound :one +INSERT INTO rounds (height, round_number, step, start_time, proposer_address) +VALUES (?, ?, ?, ?, ?) +RETURNING *; + +-- name: UpsertRound :one +INSERT INTO rounds (height, round_number, step, start_time, proposer_address) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(height, round_number) DO UPDATE SET + step = excluded.step, + start_time = excluded.start_time, + proposer_address = excluded.proposer_address +RETURNING *; + +-- name: UpdateRoundStep :one +UPDATE rounds SET step = ? WHERE height = ? AND round_number = ? RETURNING *; + +-- name: DeleteRoundsOlderThan :exec +DELETE FROM rounds WHERE height < ?; \ No newline at end of file diff --git a/pkg/db/queries/validator_snapshots.sql b/pkg/db/queries/validator_snapshots.sql new file mode 100644 index 0000000..d9a0d4f --- /dev/null +++ b/pkg/db/queries/validator_snapshots.sql @@ -0,0 +1,35 @@ +-- name: GetValidatorSnapshot :one +SELECT * FROM validator_snapshots +WHERE height = ? AND validator_hex_address = ? +LIMIT 1; + +-- name: GetValidatorSnapshotsForHeight :many +SELECT * FROM validator_snapshots +WHERE height = ? +ORDER BY voting_power DESC; + +-- name: GetValidatorSnapshotsForValidator :many +SELECT * FROM validator_snapshots +WHERE validator_hex_address = ? AND height >= ? +ORDER BY height DESC; + +-- name: CreateValidatorSnapshot :one +INSERT INTO validator_snapshots (height, validator_hex_address, voting_power, voting_power_percent, is_proposer) +VALUES (?, ?, ?, ?, ?) +RETURNING *; + +-- name: UpsertValidatorSnapshot :one +INSERT INTO validator_snapshots (height, validator_hex_address, voting_power, voting_power_percent, is_proposer) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(height, validator_hex_address) DO UPDATE SET + voting_power = excluded.voting_power, + voting_power_percent = excluded.voting_power_percent, + is_proposer = excluded.is_proposer +RETURNING *; + +-- name: BatchCreateValidatorSnapshots :exec +INSERT INTO validator_snapshots (height, validator_hex_address, voting_power, voting_power_percent, is_proposer) +VALUES (?, ?, ?, ?, ?); + +-- name: DeleteValidatorSnapshotsOlderThan :exec +DELETE FROM validator_snapshots WHERE height < ?; \ No newline at end of file diff --git a/pkg/db/queries/validators.sql b/pkg/db/queries/validators.sql new file mode 100644 index 0000000..8a2ab27 --- /dev/null +++ b/pkg/db/queries/validators.sql @@ -0,0 +1,37 @@ +-- name: GetValidator :one +SELECT * FROM validators WHERE hex_address = ? LIMIT 1; + +-- name: GetValidators :many +SELECT * FROM validators ORDER BY voting_power DESC; + +-- name: GetValidatorsByHeight :many +SELECT v.*, vs.voting_power, vs.voting_power_percent, vs.is_proposer +FROM validators v +JOIN validator_snapshots vs ON v.hex_address = vs.validator_hex_address +WHERE vs.height = ? +ORDER BY vs.voting_power DESC; + +-- name: CreateValidator :one +INSERT INTO validators (operator_address, hex_address, public_key, voting_power, moniker) +VALUES (?, ?, ?, ?, ?) +RETURNING *; + +-- name: UpdateValidator :one +UPDATE validators +SET operator_address = ?, public_key = ?, voting_power = ?, moniker = ?, updated_at = CURRENT_TIMESTAMP +WHERE hex_address = ? +RETURNING *; + +-- name: UpsertValidator :one +INSERT INTO validators (operator_address, hex_address, public_key, voting_power, moniker) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(hex_address) DO UPDATE SET + operator_address = excluded.operator_address, + public_key = excluded.public_key, + voting_power = excluded.voting_power, + moniker = excluded.moniker, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + +-- name: DeleteValidator :exec +DELETE FROM validators WHERE hex_address = ?; \ No newline at end of file diff --git a/pkg/db/queries/votes.sql b/pkg/db/queries/votes.sql new file mode 100644 index 0000000..176e25a --- /dev/null +++ b/pkg/db/queries/votes.sql @@ -0,0 +1,48 @@ +-- name: GetVote :one +SELECT * FROM votes +WHERE height = ? AND round_number = ? AND validator_hex_address = ? AND vote_type = ? +LIMIT 1; + +-- name: GetVotesForRound :many +SELECT * FROM votes +WHERE height = ? AND round_number = ? +ORDER BY vote_type, validator_hex_address; + +-- name: GetVotesForHeight :many +SELECT * FROM votes WHERE height = ? ORDER BY round_number, vote_type, validator_hex_address; + +-- name: GetVotesForValidator :many +SELECT * FROM votes +WHERE validator_hex_address = ? AND height >= ? +ORDER BY height DESC, round_number DESC; + +-- name: GetVotesByType :many +SELECT * FROM votes +WHERE height = ? AND round_number = ? AND vote_type = ? +ORDER BY validator_hex_address; + +-- name: CreateVote :one +INSERT INTO votes (height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp) +VALUES (?, ?, ?, ?, ?, ?, ?) +RETURNING *; + +-- name: UpsertVote :one +INSERT INTO votes (height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(height, round_number, validator_hex_address, vote_type) DO UPDATE SET + block_hash = excluded.block_hash, + signature = excluded.signature, + timestamp = excluded.timestamp +RETURNING *; + +-- name: GetVotingPowerForRound :one +SELECT + SUM(CASE WHEN v.vote_type = 1 AND v.block_hash IS NOT NULL THEN vs.voting_power ELSE 0 END) as prevote_power, + SUM(CASE WHEN v.vote_type = 2 AND v.block_hash IS NOT NULL THEN vs.voting_power ELSE 0 END) as precommit_power, + SUM(vs.voting_power) as total_power +FROM votes v +JOIN validator_snapshots vs ON v.validator_hex_address = vs.validator_hex_address AND v.height = vs.height +WHERE v.height = ? AND v.round_number = ?; + +-- name: DeleteVotesOlderThan :exec +DELETE FROM votes WHERE height < ?; \ No newline at end of file diff --git a/pkg/db/sqlc/analytics.sql.go b/pkg/db/sqlc/analytics.sql.go new file mode 100644 index 0000000..fa8ecbe --- /dev/null +++ b/pkg/db/sqlc/analytics.sql.go @@ -0,0 +1,498 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: analytics.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const getAllValidatorsSigningEfficiency = `-- name: GetAllValidatorsSigningEfficiency :many +WITH validator_participation AS ( + SELECT + vals.hex_address, + vals.moniker, + h.height, + CASE WHEN v.id IS NOT NULL THEN 1 ELSE 0 END as participated + FROM validators vals + CROSS JOIN heights h + LEFT JOIN votes v ON vals.hex_address = v.validator_hex_address AND h.height = v.height + WHERE h.block_time >= ? AND h.block_time <= ? +) +SELECT + hex_address, + moniker, + COUNT(*) as total_blocks, + SUM(participated) as blocks_signed, + COUNT(*) - SUM(participated) as blocks_missed, + ROUND(100.0 * SUM(participated) / COUNT(*), 2) as signing_efficiency +FROM validator_participation +GROUP BY hex_address, moniker +ORDER BY signing_efficiency DESC +` + +type GetAllValidatorsSigningEfficiencyParams struct { + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetAllValidatorsSigningEfficiencyRow struct { + HexAddress string `json:"hex_address"` + Moniker sql.NullString `json:"moniker"` + TotalBlocks int64 `json:"total_blocks"` + BlocksSigned sql.NullFloat64 `json:"blocks_signed"` + BlocksMissed int64 `json:"blocks_missed"` + SigningEfficiency float64 `json:"signing_efficiency"` +} + +// Get signing efficiency for all validators over a time window +func (q *Queries) GetAllValidatorsSigningEfficiency(ctx context.Context, arg GetAllValidatorsSigningEfficiencyParams) ([]GetAllValidatorsSigningEfficiencyRow, error) { + rows, err := q.query(ctx, q.getAllValidatorsSigningEfficiencyStmt, getAllValidatorsSigningEfficiency, arg.BlockTime, arg.BlockTime_2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetAllValidatorsSigningEfficiencyRow{} + for rows.Next() { + var i GetAllValidatorsSigningEfficiencyRow + if err := rows.Scan( + &i.HexAddress, + &i.Moniker, + &i.TotalBlocks, + &i.BlocksSigned, + &i.BlocksMissed, + &i.SigningEfficiency, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getProposerPerformance = `-- name: GetProposerPerformance :one +SELECT + COUNT(*) as blocks_proposed, + COUNT(CASE WHEN r.proposer_address = ? THEN 1 END) as successful_proposals, + ROUND(100.0 * COUNT(CASE WHEN r.proposer_address = ? THEN 1 END) / COUNT(*), 2) as proposal_success_rate +FROM rounds r +JOIN heights h ON r.height = h.height +WHERE r.proposer_address = ? AND h.block_time >= ? AND h.block_time <= ? +` + +type GetProposerPerformanceParams struct { + ProposerAddress sql.NullString `json:"proposer_address"` + ProposerAddress_2 sql.NullString `json:"proposer_address_2"` + ProposerAddress_3 sql.NullString `json:"proposer_address_3"` + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetProposerPerformanceRow struct { + BlocksProposed int64 `json:"blocks_proposed"` + SuccessfulProposals int64 `json:"successful_proposals"` + ProposalSuccessRate float64 `json:"proposal_success_rate"` +} + +// Calculate proposer performance metrics (when validator is selected as proposer) +func (q *Queries) GetProposerPerformance(ctx context.Context, arg GetProposerPerformanceParams) (GetProposerPerformanceRow, error) { + row := q.queryRow(ctx, q.getProposerPerformanceStmt, getProposerPerformance, + arg.ProposerAddress, + arg.ProposerAddress_2, + arg.ProposerAddress_3, + arg.BlockTime, + arg.BlockTime_2, + ) + var i GetProposerPerformanceRow + err := row.Scan(&i.BlocksProposed, &i.SuccessfulProposals, &i.ProposalSuccessRate) + return i, err +} + +const getValidatorConsensusParticipation = `-- name: GetValidatorConsensusParticipation :one +WITH vote_participation AS ( + SELECT + r.height, + r.round_number, + COUNT(CASE WHEN v.vote_type = 1 THEN 1 END) as prevotes, + COUNT(CASE WHEN v.vote_type = 2 THEN 1 END) as precommits, + COUNT(DISTINCT r.round_number) as total_rounds + FROM rounds r + LEFT JOIN votes v ON r.height = v.height AND r.round_number = v.round_number AND v.validator_hex_address = ? + LEFT JOIN heights h ON r.height = h.height + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY r.height, r.round_number +) +SELECT + COALESCE(COUNT(*), 0) as total_rounds, + COALESCE(SUM(CASE WHEN prevotes > 0 THEN 1 ELSE 0 END), 0) as rounds_with_prevote, + COALESCE(SUM(CASE WHEN precommits > 0 THEN 1 ELSE 0 END), 0) as rounds_with_precommit, + COALESCE(ROUND(100.0 * SUM(CASE WHEN prevotes > 0 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 2), 0.0) as prevote_rate, + COALESCE(ROUND(100.0 * SUM(CASE WHEN precommits > 0 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 2), 0.0) as precommit_rate +FROM vote_participation +` + +type GetValidatorConsensusParticipationParams struct { + ValidatorHexAddress string `json:"validator_hex_address"` + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetValidatorConsensusParticipationRow struct { + TotalRounds interface{} `json:"total_rounds"` + RoundsWithPrevote interface{} `json:"rounds_with_prevote"` + RoundsWithPrecommit interface{} `json:"rounds_with_precommit"` + PrevoteRate interface{} `json:"prevote_rate"` + PrecommitRate interface{} `json:"precommit_rate"` +} + +// Calculate consensus participation rates for prevotes and precommits +func (q *Queries) GetValidatorConsensusParticipation(ctx context.Context, arg GetValidatorConsensusParticipationParams) (GetValidatorConsensusParticipationRow, error) { + row := q.queryRow(ctx, q.getValidatorConsensusParticipationStmt, getValidatorConsensusParticipation, arg.ValidatorHexAddress, arg.BlockTime, arg.BlockTime_2) + var i GetValidatorConsensusParticipationRow + err := row.Scan( + &i.TotalRounds, + &i.RoundsWithPrevote, + &i.RoundsWithPrecommit, + &i.PrevoteRate, + &i.PrecommitRate, + ) + return i, err +} + +const getValidatorMissedBlockStreaks = `-- name: GetValidatorMissedBlockStreaks :many +WITH block_sequence AS ( + SELECT + h.height, + h.block_time, + CASE WHEN v.id IS NULL THEN 1 ELSE 0 END as missed, + ROW_NUMBER() OVER (ORDER BY h.height) as rn + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? +), +miss_groups AS ( + SELECT + height, + block_time, + missed, + rn - ROW_NUMBER() OVER (PARTITION BY missed ORDER BY height) as grp + FROM block_sequence + WHERE missed = 1 +) +SELECT + COUNT(*) as consecutive_misses, + MIN(height) as streak_start_height, + MAX(height) as streak_end_height, + MIN(block_time) as streak_start_time, + MAX(block_time) as streak_end_time +FROM miss_groups +GROUP BY grp +HAVING COUNT(*) > 0 +ORDER BY consecutive_misses DESC +` + +type GetValidatorMissedBlockStreaksParams struct { + ValidatorHexAddress string `json:"validator_hex_address"` + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetValidatorMissedBlockStreaksRow struct { + ConsecutiveMisses int64 `json:"consecutive_misses"` + StreakStartHeight interface{} `json:"streak_start_height"` + StreakEndHeight interface{} `json:"streak_end_height"` + StreakStartTime interface{} `json:"streak_start_time"` + StreakEndTime interface{} `json:"streak_end_time"` +} + +// Find consecutive missed block sequences for a validator +func (q *Queries) GetValidatorMissedBlockStreaks(ctx context.Context, arg GetValidatorMissedBlockStreaksParams) ([]GetValidatorMissedBlockStreaksRow, error) { + rows, err := q.query(ctx, q.getValidatorMissedBlockStreaksStmt, getValidatorMissedBlockStreaks, arg.ValidatorHexAddress, arg.BlockTime, arg.BlockTime_2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetValidatorMissedBlockStreaksRow{} + for rows.Next() { + var i GetValidatorMissedBlockStreaksRow + if err := rows.Scan( + &i.ConsecutiveMisses, + &i.StreakStartHeight, + &i.StreakEndHeight, + &i.StreakStartTime, + &i.StreakEndTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getValidatorPerformanceTimeSeries = `-- name: GetValidatorPerformanceTimeSeries :many +WITH time_buckets AS ( + SELECT + datetime(h.block_time, 'start of hour') as time_bucket, + COUNT(*) as blocks_in_bucket, + SUM(CASE WHEN v.id IS NOT NULL THEN 1 ELSE 0 END) as blocks_signed + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY datetime(h.block_time, 'start of hour') +) +SELECT + time_bucket, + COALESCE(blocks_in_bucket, 0) as blocks_in_bucket, + COALESCE(blocks_signed, 0) as blocks_signed, + COALESCE(blocks_in_bucket - blocks_signed, 0) as blocks_missed, + COALESCE(ROUND(100.0 * blocks_signed / NULLIF(blocks_in_bucket, 0), 2), 0.0) as signing_efficiency +FROM time_buckets +ORDER BY time_bucket +` + +type GetValidatorPerformanceTimeSeriesParams struct { + ValidatorHexAddress string `json:"validator_hex_address"` + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetValidatorPerformanceTimeSeriesRow struct { + TimeBucket interface{} `json:"time_bucket"` + BlocksInBucket int64 `json:"blocks_in_bucket"` + BlocksSigned float64 `json:"blocks_signed"` + BlocksMissed interface{} `json:"blocks_missed"` + SigningEfficiency interface{} `json:"signing_efficiency"` +} + +// Get signing efficiency over time buckets (hourly) +func (q *Queries) GetValidatorPerformanceTimeSeries(ctx context.Context, arg GetValidatorPerformanceTimeSeriesParams) ([]GetValidatorPerformanceTimeSeriesRow, error) { + rows, err := q.query(ctx, q.getValidatorPerformanceTimeSeriesStmt, getValidatorPerformanceTimeSeries, arg.ValidatorHexAddress, arg.BlockTime, arg.BlockTime_2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetValidatorPerformanceTimeSeriesRow{} + for rows.Next() { + var i GetValidatorPerformanceTimeSeriesRow + if err := rows.Scan( + &i.TimeBucket, + &i.BlocksInBucket, + &i.BlocksSigned, + &i.BlocksMissed, + &i.SigningEfficiency, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getValidatorRanking = `-- name: GetValidatorRanking :many +WITH validator_metrics AS ( + SELECT + vals.hex_address, + vals.moniker, + -- Block signing metrics + COUNT(h.height) as total_blocks, + COUNT(v.id) as blocks_signed, + ROUND(100.0 * COUNT(v.id) / COUNT(h.height), 2) as signing_efficiency, + + -- Consensus participation + COUNT(CASE WHEN v.vote_type = 1 THEN 1 END) as prevotes_cast, + COUNT(CASE WHEN v.vote_type = 2 THEN 1 END) as precommits_cast, + + -- Latest voting power + COALESCE(MAX(vs.voting_power), 0) as voting_power + FROM validators vals + CROSS JOIN heights h + LEFT JOIN votes v ON vals.hex_address = v.validator_hex_address AND h.height = v.height + LEFT JOIN validator_snapshots vs ON vals.hex_address = vs.validator_hex_address AND h.height = vs.height + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY vals.hex_address, vals.moniker +) +SELECT + hex_address, + moniker, + total_blocks, + blocks_signed, + total_blocks - blocks_signed as blocks_missed, + signing_efficiency, + prevotes_cast, + precommits_cast, + voting_power, + RANK() OVER (ORDER BY signing_efficiency DESC, voting_power DESC) as efficiency_rank +FROM validator_metrics +ORDER BY efficiency_rank +` + +type GetValidatorRankingParams struct { + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetValidatorRankingRow struct { + HexAddress string `json:"hex_address"` + Moniker sql.NullString `json:"moniker"` + TotalBlocks int64 `json:"total_blocks"` + BlocksSigned int64 `json:"blocks_signed"` + BlocksMissed int64 `json:"blocks_missed"` + SigningEfficiency float64 `json:"signing_efficiency"` + PrevotesCast int64 `json:"prevotes_cast"` + PrecommitsCast int64 `json:"precommits_cast"` + VotingPower interface{} `json:"voting_power"` + EfficiencyRank interface{} `json:"efficiency_rank"` +} + +// Get validator performance ranking with multiple metrics +func (q *Queries) GetValidatorRanking(ctx context.Context, arg GetValidatorRankingParams) ([]GetValidatorRankingRow, error) { + rows, err := q.query(ctx, q.getValidatorRankingStmt, getValidatorRanking, arg.BlockTime, arg.BlockTime_2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetValidatorRankingRow{} + for rows.Next() { + var i GetValidatorRankingRow + if err := rows.Scan( + &i.HexAddress, + &i.Moniker, + &i.TotalBlocks, + &i.BlocksSigned, + &i.BlocksMissed, + &i.SigningEfficiency, + &i.PrevotesCast, + &i.PrecommitsCast, + &i.VotingPower, + &i.EfficiencyRank, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getValidatorSigningEfficiency = `-- name: GetValidatorSigningEfficiency :one +WITH block_participation AS ( + SELECT + h.height, + h.block_time, + COUNT(v.id) as votes_cast, + CASE WHEN COUNT(v.id) > 0 THEN 1 ELSE 0 END as participated + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? + GROUP BY h.height, h.block_time +) +SELECT + COALESCE(COUNT(*), 0) as total_blocks, + COALESCE(SUM(participated), 0) as blocks_signed, + COALESCE(COUNT(*) - SUM(participated), 0) as blocks_missed, + COALESCE(ROUND(100.0 * SUM(participated) / NULLIF(COUNT(*), 0), 2), 0.0) as signing_efficiency +FROM block_participation +` + +type GetValidatorSigningEfficiencyParams struct { + ValidatorHexAddress string `json:"validator_hex_address"` + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetValidatorSigningEfficiencyRow struct { + TotalBlocks interface{} `json:"total_blocks"` + BlocksSigned interface{} `json:"blocks_signed"` + BlocksMissed interface{} `json:"blocks_missed"` + SigningEfficiency interface{} `json:"signing_efficiency"` +} + +// Calculate signing efficiency for a validator over a time window +func (q *Queries) GetValidatorSigningEfficiency(ctx context.Context, arg GetValidatorSigningEfficiencyParams) (GetValidatorSigningEfficiencyRow, error) { + row := q.queryRow(ctx, q.getValidatorSigningEfficiencyStmt, getValidatorSigningEfficiency, arg.ValidatorHexAddress, arg.BlockTime, arg.BlockTime_2) + var i GetValidatorSigningEfficiencyRow + err := row.Scan( + &i.TotalBlocks, + &i.BlocksSigned, + &i.BlocksMissed, + &i.SigningEfficiency, + ) + return i, err +} + +const getValidatorUptime = `-- name: GetValidatorUptime :one +WITH block_stats AS ( + SELECT + COUNT(*) as total_blocks_in_window, + COUNT(v.id) as blocks_participated, + MIN(h.block_time) as window_start, + MAX(h.block_time) as window_end + FROM heights h + LEFT JOIN votes v ON h.height = v.height AND v.validator_hex_address = ? + WHERE h.block_time >= ? AND h.block_time <= ? +) +SELECT + COALESCE(bs.total_blocks_in_window, 0) as total_blocks_in_window, + COALESCE(bs.blocks_participated, 0) as blocks_participated, + COALESCE(bs.total_blocks_in_window - bs.blocks_participated, 0) as blocks_missed, + COALESCE(ROUND(100.0 * bs.blocks_participated / NULLIF(bs.total_blocks_in_window, 0), 2), 0.0) as uptime_percentage, + bs.window_start, + bs.window_end +FROM block_stats bs +` + +type GetValidatorUptimeParams struct { + ValidatorHexAddress string `json:"validator_hex_address"` + BlockTime sql.NullTime `json:"block_time"` + BlockTime_2 sql.NullTime `json:"block_time_2"` +} + +type GetValidatorUptimeRow struct { + TotalBlocksInWindow int64 `json:"total_blocks_in_window"` + BlocksParticipated int64 `json:"blocks_participated"` + BlocksMissed interface{} `json:"blocks_missed"` + UptimePercentage interface{} `json:"uptime_percentage"` + WindowStart interface{} `json:"window_start"` + WindowEnd interface{} `json:"window_end"` +} + +// Calculate validator uptime metrics over time window +func (q *Queries) GetValidatorUptime(ctx context.Context, arg GetValidatorUptimeParams) (GetValidatorUptimeRow, error) { + row := q.queryRow(ctx, q.getValidatorUptimeStmt, getValidatorUptime, arg.ValidatorHexAddress, arg.BlockTime, arg.BlockTime_2) + var i GetValidatorUptimeRow + err := row.Scan( + &i.TotalBlocksInWindow, + &i.BlocksParticipated, + &i.BlocksMissed, + &i.UptimePercentage, + &i.WindowStart, + &i.WindowEnd, + ) + return i, err +} diff --git a/pkg/db/sqlc/consensus_events.sql.go b/pkg/db/sqlc/consensus_events.sql.go new file mode 100644 index 0000000..54ca552 --- /dev/null +++ b/pkg/db/sqlc/consensus_events.sql.go @@ -0,0 +1,229 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: consensus_events.sql + +package sqlc + +import ( + "context" + "database/sql" + "time" +) + +const createConsensusEvent = `-- name: CreateConsensusEvent :one +INSERT INTO consensus_events (height, round_number, event_type, event_data, timestamp) +VALUES (?, ?, ?, ?, ?) +RETURNING id, height, round_number, event_type, event_data, timestamp, created_at +` + +type CreateConsensusEventParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + EventType string `json:"event_type"` + EventData sql.NullString `json:"event_data"` + Timestamp time.Time `json:"timestamp"` +} + +func (q *Queries) CreateConsensusEvent(ctx context.Context, arg CreateConsensusEventParams) (ConsensusEvent, error) { + row := q.queryRow(ctx, q.createConsensusEventStmt, createConsensusEvent, + arg.Height, + arg.RoundNumber, + arg.EventType, + arg.EventData, + arg.Timestamp, + ) + var i ConsensusEvent + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.EventType, + &i.EventData, + &i.Timestamp, + &i.CreatedAt, + ) + return i, err +} + +const deleteConsensusEventsOlderThan = `-- name: DeleteConsensusEventsOlderThan :exec +DELETE FROM consensus_events WHERE height < ? +` + +func (q *Queries) DeleteConsensusEventsOlderThan(ctx context.Context, height int64) error { + _, err := q.exec(ctx, q.deleteConsensusEventsOlderThanStmt, deleteConsensusEventsOlderThan, height) + return err +} + +const getConsensusEvent = `-- name: GetConsensusEvent :one +SELECT id, height, round_number, event_type, event_data, timestamp, created_at FROM consensus_events WHERE id = ? LIMIT 1 +` + +func (q *Queries) GetConsensusEvent(ctx context.Context, id int64) (ConsensusEvent, error) { + row := q.queryRow(ctx, q.getConsensusEventStmt, getConsensusEvent, id) + var i ConsensusEvent + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.EventType, + &i.EventData, + &i.Timestamp, + &i.CreatedAt, + ) + return i, err +} + +const getConsensusEventsByType = `-- name: GetConsensusEventsByType :many +SELECT id, height, round_number, event_type, event_data, timestamp, created_at FROM consensus_events +WHERE height >= ? AND event_type = ? +ORDER BY height DESC, round_number DESC, timestamp DESC +` + +type GetConsensusEventsByTypeParams struct { + Height int64 `json:"height"` + EventType string `json:"event_type"` +} + +func (q *Queries) GetConsensusEventsByType(ctx context.Context, arg GetConsensusEventsByTypeParams) ([]ConsensusEvent, error) { + rows, err := q.query(ctx, q.getConsensusEventsByTypeStmt, getConsensusEventsByType, arg.Height, arg.EventType) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ConsensusEvent{} + for rows.Next() { + var i ConsensusEvent + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.EventType, + &i.EventData, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getConsensusEventsForHeight = `-- name: GetConsensusEventsForHeight :many +SELECT id, height, round_number, event_type, event_data, timestamp, created_at FROM consensus_events WHERE height = ? ORDER BY round_number, timestamp +` + +func (q *Queries) GetConsensusEventsForHeight(ctx context.Context, height int64) ([]ConsensusEvent, error) { + rows, err := q.query(ctx, q.getConsensusEventsForHeightStmt, getConsensusEventsForHeight, height) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ConsensusEvent{} + for rows.Next() { + var i ConsensusEvent + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.EventType, + &i.EventData, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getConsensusEventsForRound = `-- name: GetConsensusEventsForRound :many +SELECT id, height, round_number, event_type, event_data, timestamp, created_at FROM consensus_events +WHERE height = ? AND round_number = ? +ORDER BY timestamp +` + +type GetConsensusEventsForRoundParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` +} + +func (q *Queries) GetConsensusEventsForRound(ctx context.Context, arg GetConsensusEventsForRoundParams) ([]ConsensusEvent, error) { + rows, err := q.query(ctx, q.getConsensusEventsForRoundStmt, getConsensusEventsForRound, arg.Height, arg.RoundNumber) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ConsensusEvent{} + for rows.Next() { + var i ConsensusEvent + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.EventType, + &i.EventData, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getRecentConsensusEvents = `-- name: GetRecentConsensusEvents :many +SELECT id, height, round_number, event_type, event_data, timestamp, created_at FROM consensus_events ORDER BY timestamp DESC LIMIT ? +` + +func (q *Queries) GetRecentConsensusEvents(ctx context.Context, limit int64) ([]ConsensusEvent, error) { + rows, err := q.query(ctx, q.getRecentConsensusEventsStmt, getRecentConsensusEvents, limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ConsensusEvent{} + for rows.Next() { + var i ConsensusEvent + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.EventType, + &i.EventData, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/pkg/db/sqlc/db.go b/pkg/db/sqlc/db.go new file mode 100644 index 0000000..4a71c7c --- /dev/null +++ b/pkg/db/sqlc/db.go @@ -0,0 +1,598 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package sqlc + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + if q.batchCreateValidatorSnapshotsStmt, err = db.PrepareContext(ctx, batchCreateValidatorSnapshots); err != nil { + return nil, fmt.Errorf("error preparing query BatchCreateValidatorSnapshots: %w", err) + } + if q.createConsensusEventStmt, err = db.PrepareContext(ctx, createConsensusEvent); err != nil { + return nil, fmt.Errorf("error preparing query CreateConsensusEvent: %w", err) + } + if q.createHeightStmt, err = db.PrepareContext(ctx, createHeight); err != nil { + return nil, fmt.Errorf("error preparing query CreateHeight: %w", err) + } + if q.createRoundStmt, err = db.PrepareContext(ctx, createRound); err != nil { + return nil, fmt.Errorf("error preparing query CreateRound: %w", err) + } + if q.createValidatorStmt, err = db.PrepareContext(ctx, createValidator); err != nil { + return nil, fmt.Errorf("error preparing query CreateValidator: %w", err) + } + if q.createValidatorSnapshotStmt, err = db.PrepareContext(ctx, createValidatorSnapshot); err != nil { + return nil, fmt.Errorf("error preparing query CreateValidatorSnapshot: %w", err) + } + if q.createVoteStmt, err = db.PrepareContext(ctx, createVote); err != nil { + return nil, fmt.Errorf("error preparing query CreateVote: %w", err) + } + if q.deleteConsensusEventsOlderThanStmt, err = db.PrepareContext(ctx, deleteConsensusEventsOlderThan); err != nil { + return nil, fmt.Errorf("error preparing query DeleteConsensusEventsOlderThan: %w", err) + } + if q.deleteHeightsOlderThanStmt, err = db.PrepareContext(ctx, deleteHeightsOlderThan); err != nil { + return nil, fmt.Errorf("error preparing query DeleteHeightsOlderThan: %w", err) + } + if q.deleteRoundsOlderThanStmt, err = db.PrepareContext(ctx, deleteRoundsOlderThan); err != nil { + return nil, fmt.Errorf("error preparing query DeleteRoundsOlderThan: %w", err) + } + if q.deleteValidatorStmt, err = db.PrepareContext(ctx, deleteValidator); err != nil { + return nil, fmt.Errorf("error preparing query DeleteValidator: %w", err) + } + if q.deleteValidatorSnapshotsOlderThanStmt, err = db.PrepareContext(ctx, deleteValidatorSnapshotsOlderThan); err != nil { + return nil, fmt.Errorf("error preparing query DeleteValidatorSnapshotsOlderThan: %w", err) + } + if q.deleteVotesOlderThanStmt, err = db.PrepareContext(ctx, deleteVotesOlderThan); err != nil { + return nil, fmt.Errorf("error preparing query DeleteVotesOlderThan: %w", err) + } + if q.getAllValidatorsSigningEfficiencyStmt, err = db.PrepareContext(ctx, getAllValidatorsSigningEfficiency); err != nil { + return nil, fmt.Errorf("error preparing query GetAllValidatorsSigningEfficiency: %w", err) + } + if q.getConsensusEventStmt, err = db.PrepareContext(ctx, getConsensusEvent); err != nil { + return nil, fmt.Errorf("error preparing query GetConsensusEvent: %w", err) + } + if q.getConsensusEventsByTypeStmt, err = db.PrepareContext(ctx, getConsensusEventsByType); err != nil { + return nil, fmt.Errorf("error preparing query GetConsensusEventsByType: %w", err) + } + if q.getConsensusEventsForHeightStmt, err = db.PrepareContext(ctx, getConsensusEventsForHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetConsensusEventsForHeight: %w", err) + } + if q.getConsensusEventsForRoundStmt, err = db.PrepareContext(ctx, getConsensusEventsForRound); err != nil { + return nil, fmt.Errorf("error preparing query GetConsensusEventsForRound: %w", err) + } + if q.getHeightStmt, err = db.PrepareContext(ctx, getHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetHeight: %w", err) + } + if q.getHeightsStmt, err = db.PrepareContext(ctx, getHeights); err != nil { + return nil, fmt.Errorf("error preparing query GetHeights: %w", err) + } + if q.getLatestHeightStmt, err = db.PrepareContext(ctx, getLatestHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetLatestHeight: %w", err) + } + if q.getProposerPerformanceStmt, err = db.PrepareContext(ctx, getProposerPerformance); err != nil { + return nil, fmt.Errorf("error preparing query GetProposerPerformance: %w", err) + } + if q.getRecentConsensusEventsStmt, err = db.PrepareContext(ctx, getRecentConsensusEvents); err != nil { + return nil, fmt.Errorf("error preparing query GetRecentConsensusEvents: %w", err) + } + if q.getRecentRoundsStmt, err = db.PrepareContext(ctx, getRecentRounds); err != nil { + return nil, fmt.Errorf("error preparing query GetRecentRounds: %w", err) + } + if q.getRoundStmt, err = db.PrepareContext(ctx, getRound); err != nil { + return nil, fmt.Errorf("error preparing query GetRound: %w", err) + } + if q.getRoundsForHeightStmt, err = db.PrepareContext(ctx, getRoundsForHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetRoundsForHeight: %w", err) + } + if q.getRoundsInRangeStmt, err = db.PrepareContext(ctx, getRoundsInRange); err != nil { + return nil, fmt.Errorf("error preparing query GetRoundsInRange: %w", err) + } + if q.getValidatorStmt, err = db.PrepareContext(ctx, getValidator); err != nil { + return nil, fmt.Errorf("error preparing query GetValidator: %w", err) + } + if q.getValidatorConsensusParticipationStmt, err = db.PrepareContext(ctx, getValidatorConsensusParticipation); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorConsensusParticipation: %w", err) + } + if q.getValidatorMissedBlockStreaksStmt, err = db.PrepareContext(ctx, getValidatorMissedBlockStreaks); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorMissedBlockStreaks: %w", err) + } + if q.getValidatorPerformanceTimeSeriesStmt, err = db.PrepareContext(ctx, getValidatorPerformanceTimeSeries); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorPerformanceTimeSeries: %w", err) + } + if q.getValidatorRankingStmt, err = db.PrepareContext(ctx, getValidatorRanking); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorRanking: %w", err) + } + if q.getValidatorSigningEfficiencyStmt, err = db.PrepareContext(ctx, getValidatorSigningEfficiency); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorSigningEfficiency: %w", err) + } + if q.getValidatorSnapshotStmt, err = db.PrepareContext(ctx, getValidatorSnapshot); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorSnapshot: %w", err) + } + if q.getValidatorSnapshotsForHeightStmt, err = db.PrepareContext(ctx, getValidatorSnapshotsForHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorSnapshotsForHeight: %w", err) + } + if q.getValidatorSnapshotsForValidatorStmt, err = db.PrepareContext(ctx, getValidatorSnapshotsForValidator); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorSnapshotsForValidator: %w", err) + } + if q.getValidatorUptimeStmt, err = db.PrepareContext(ctx, getValidatorUptime); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorUptime: %w", err) + } + if q.getValidatorsStmt, err = db.PrepareContext(ctx, getValidators); err != nil { + return nil, fmt.Errorf("error preparing query GetValidators: %w", err) + } + if q.getValidatorsByHeightStmt, err = db.PrepareContext(ctx, getValidatorsByHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetValidatorsByHeight: %w", err) + } + if q.getVoteStmt, err = db.PrepareContext(ctx, getVote); err != nil { + return nil, fmt.Errorf("error preparing query GetVote: %w", err) + } + if q.getVotesByTypeStmt, err = db.PrepareContext(ctx, getVotesByType); err != nil { + return nil, fmt.Errorf("error preparing query GetVotesByType: %w", err) + } + if q.getVotesForHeightStmt, err = db.PrepareContext(ctx, getVotesForHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetVotesForHeight: %w", err) + } + if q.getVotesForRoundStmt, err = db.PrepareContext(ctx, getVotesForRound); err != nil { + return nil, fmt.Errorf("error preparing query GetVotesForRound: %w", err) + } + if q.getVotesForValidatorStmt, err = db.PrepareContext(ctx, getVotesForValidator); err != nil { + return nil, fmt.Errorf("error preparing query GetVotesForValidator: %w", err) + } + if q.getVotingPowerForRoundStmt, err = db.PrepareContext(ctx, getVotingPowerForRound); err != nil { + return nil, fmt.Errorf("error preparing query GetVotingPowerForRound: %w", err) + } + if q.updateRoundStepStmt, err = db.PrepareContext(ctx, updateRoundStep); err != nil { + return nil, fmt.Errorf("error preparing query UpdateRoundStep: %w", err) + } + if q.updateValidatorStmt, err = db.PrepareContext(ctx, updateValidator); err != nil { + return nil, fmt.Errorf("error preparing query UpdateValidator: %w", err) + } + if q.upsertHeightStmt, err = db.PrepareContext(ctx, upsertHeight); err != nil { + return nil, fmt.Errorf("error preparing query UpsertHeight: %w", err) + } + if q.upsertRoundStmt, err = db.PrepareContext(ctx, upsertRound); err != nil { + return nil, fmt.Errorf("error preparing query UpsertRound: %w", err) + } + if q.upsertValidatorStmt, err = db.PrepareContext(ctx, upsertValidator); err != nil { + return nil, fmt.Errorf("error preparing query UpsertValidator: %w", err) + } + if q.upsertValidatorSnapshotStmt, err = db.PrepareContext(ctx, upsertValidatorSnapshot); err != nil { + return nil, fmt.Errorf("error preparing query UpsertValidatorSnapshot: %w", err) + } + if q.upsertVoteStmt, err = db.PrepareContext(ctx, upsertVote); err != nil { + return nil, fmt.Errorf("error preparing query UpsertVote: %w", err) + } + return &q, nil +} + +func (q *Queries) Close() error { + var err error + if q.batchCreateValidatorSnapshotsStmt != nil { + if cerr := q.batchCreateValidatorSnapshotsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing batchCreateValidatorSnapshotsStmt: %w", cerr) + } + } + if q.createConsensusEventStmt != nil { + if cerr := q.createConsensusEventStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createConsensusEventStmt: %w", cerr) + } + } + if q.createHeightStmt != nil { + if cerr := q.createHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createHeightStmt: %w", cerr) + } + } + if q.createRoundStmt != nil { + if cerr := q.createRoundStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createRoundStmt: %w", cerr) + } + } + if q.createValidatorStmt != nil { + if cerr := q.createValidatorStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createValidatorStmt: %w", cerr) + } + } + if q.createValidatorSnapshotStmt != nil { + if cerr := q.createValidatorSnapshotStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createValidatorSnapshotStmt: %w", cerr) + } + } + if q.createVoteStmt != nil { + if cerr := q.createVoteStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createVoteStmt: %w", cerr) + } + } + if q.deleteConsensusEventsOlderThanStmt != nil { + if cerr := q.deleteConsensusEventsOlderThanStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteConsensusEventsOlderThanStmt: %w", cerr) + } + } + if q.deleteHeightsOlderThanStmt != nil { + if cerr := q.deleteHeightsOlderThanStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteHeightsOlderThanStmt: %w", cerr) + } + } + if q.deleteRoundsOlderThanStmt != nil { + if cerr := q.deleteRoundsOlderThanStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteRoundsOlderThanStmt: %w", cerr) + } + } + if q.deleteValidatorStmt != nil { + if cerr := q.deleteValidatorStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteValidatorStmt: %w", cerr) + } + } + if q.deleteValidatorSnapshotsOlderThanStmt != nil { + if cerr := q.deleteValidatorSnapshotsOlderThanStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteValidatorSnapshotsOlderThanStmt: %w", cerr) + } + } + if q.deleteVotesOlderThanStmt != nil { + if cerr := q.deleteVotesOlderThanStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteVotesOlderThanStmt: %w", cerr) + } + } + if q.getAllValidatorsSigningEfficiencyStmt != nil { + if cerr := q.getAllValidatorsSigningEfficiencyStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getAllValidatorsSigningEfficiencyStmt: %w", cerr) + } + } + if q.getConsensusEventStmt != nil { + if cerr := q.getConsensusEventStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getConsensusEventStmt: %w", cerr) + } + } + if q.getConsensusEventsByTypeStmt != nil { + if cerr := q.getConsensusEventsByTypeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getConsensusEventsByTypeStmt: %w", cerr) + } + } + if q.getConsensusEventsForHeightStmt != nil { + if cerr := q.getConsensusEventsForHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getConsensusEventsForHeightStmt: %w", cerr) + } + } + if q.getConsensusEventsForRoundStmt != nil { + if cerr := q.getConsensusEventsForRoundStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getConsensusEventsForRoundStmt: %w", cerr) + } + } + if q.getHeightStmt != nil { + if cerr := q.getHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getHeightStmt: %w", cerr) + } + } + if q.getHeightsStmt != nil { + if cerr := q.getHeightsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getHeightsStmt: %w", cerr) + } + } + if q.getLatestHeightStmt != nil { + if cerr := q.getLatestHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getLatestHeightStmt: %w", cerr) + } + } + if q.getProposerPerformanceStmt != nil { + if cerr := q.getProposerPerformanceStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getProposerPerformanceStmt: %w", cerr) + } + } + if q.getRecentConsensusEventsStmt != nil { + if cerr := q.getRecentConsensusEventsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getRecentConsensusEventsStmt: %w", cerr) + } + } + if q.getRecentRoundsStmt != nil { + if cerr := q.getRecentRoundsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getRecentRoundsStmt: %w", cerr) + } + } + if q.getRoundStmt != nil { + if cerr := q.getRoundStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getRoundStmt: %w", cerr) + } + } + if q.getRoundsForHeightStmt != nil { + if cerr := q.getRoundsForHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getRoundsForHeightStmt: %w", cerr) + } + } + if q.getRoundsInRangeStmt != nil { + if cerr := q.getRoundsInRangeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getRoundsInRangeStmt: %w", cerr) + } + } + if q.getValidatorStmt != nil { + if cerr := q.getValidatorStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorStmt: %w", cerr) + } + } + if q.getValidatorConsensusParticipationStmt != nil { + if cerr := q.getValidatorConsensusParticipationStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorConsensusParticipationStmt: %w", cerr) + } + } + if q.getValidatorMissedBlockStreaksStmt != nil { + if cerr := q.getValidatorMissedBlockStreaksStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorMissedBlockStreaksStmt: %w", cerr) + } + } + if q.getValidatorPerformanceTimeSeriesStmt != nil { + if cerr := q.getValidatorPerformanceTimeSeriesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorPerformanceTimeSeriesStmt: %w", cerr) + } + } + if q.getValidatorRankingStmt != nil { + if cerr := q.getValidatorRankingStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorRankingStmt: %w", cerr) + } + } + if q.getValidatorSigningEfficiencyStmt != nil { + if cerr := q.getValidatorSigningEfficiencyStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorSigningEfficiencyStmt: %w", cerr) + } + } + if q.getValidatorSnapshotStmt != nil { + if cerr := q.getValidatorSnapshotStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorSnapshotStmt: %w", cerr) + } + } + if q.getValidatorSnapshotsForHeightStmt != nil { + if cerr := q.getValidatorSnapshotsForHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorSnapshotsForHeightStmt: %w", cerr) + } + } + if q.getValidatorSnapshotsForValidatorStmt != nil { + if cerr := q.getValidatorSnapshotsForValidatorStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorSnapshotsForValidatorStmt: %w", cerr) + } + } + if q.getValidatorUptimeStmt != nil { + if cerr := q.getValidatorUptimeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorUptimeStmt: %w", cerr) + } + } + if q.getValidatorsStmt != nil { + if cerr := q.getValidatorsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorsStmt: %w", cerr) + } + } + if q.getValidatorsByHeightStmt != nil { + if cerr := q.getValidatorsByHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getValidatorsByHeightStmt: %w", cerr) + } + } + if q.getVoteStmt != nil { + if cerr := q.getVoteStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getVoteStmt: %w", cerr) + } + } + if q.getVotesByTypeStmt != nil { + if cerr := q.getVotesByTypeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getVotesByTypeStmt: %w", cerr) + } + } + if q.getVotesForHeightStmt != nil { + if cerr := q.getVotesForHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getVotesForHeightStmt: %w", cerr) + } + } + if q.getVotesForRoundStmt != nil { + if cerr := q.getVotesForRoundStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getVotesForRoundStmt: %w", cerr) + } + } + if q.getVotesForValidatorStmt != nil { + if cerr := q.getVotesForValidatorStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getVotesForValidatorStmt: %w", cerr) + } + } + if q.getVotingPowerForRoundStmt != nil { + if cerr := q.getVotingPowerForRoundStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getVotingPowerForRoundStmt: %w", cerr) + } + } + if q.updateRoundStepStmt != nil { + if cerr := q.updateRoundStepStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateRoundStepStmt: %w", cerr) + } + } + if q.updateValidatorStmt != nil { + if cerr := q.updateValidatorStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateValidatorStmt: %w", cerr) + } + } + if q.upsertHeightStmt != nil { + if cerr := q.upsertHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing upsertHeightStmt: %w", cerr) + } + } + if q.upsertRoundStmt != nil { + if cerr := q.upsertRoundStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing upsertRoundStmt: %w", cerr) + } + } + if q.upsertValidatorStmt != nil { + if cerr := q.upsertValidatorStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing upsertValidatorStmt: %w", cerr) + } + } + if q.upsertValidatorSnapshotStmt != nil { + if cerr := q.upsertValidatorSnapshotStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing upsertValidatorSnapshotStmt: %w", cerr) + } + } + if q.upsertVoteStmt != nil { + if cerr := q.upsertVoteStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing upsertVoteStmt: %w", cerr) + } + } + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} + +type Queries struct { + db DBTX + tx *sql.Tx + batchCreateValidatorSnapshotsStmt *sql.Stmt + createConsensusEventStmt *sql.Stmt + createHeightStmt *sql.Stmt + createRoundStmt *sql.Stmt + createValidatorStmt *sql.Stmt + createValidatorSnapshotStmt *sql.Stmt + createVoteStmt *sql.Stmt + deleteConsensusEventsOlderThanStmt *sql.Stmt + deleteHeightsOlderThanStmt *sql.Stmt + deleteRoundsOlderThanStmt *sql.Stmt + deleteValidatorStmt *sql.Stmt + deleteValidatorSnapshotsOlderThanStmt *sql.Stmt + deleteVotesOlderThanStmt *sql.Stmt + getAllValidatorsSigningEfficiencyStmt *sql.Stmt + getConsensusEventStmt *sql.Stmt + getConsensusEventsByTypeStmt *sql.Stmt + getConsensusEventsForHeightStmt *sql.Stmt + getConsensusEventsForRoundStmt *sql.Stmt + getHeightStmt *sql.Stmt + getHeightsStmt *sql.Stmt + getLatestHeightStmt *sql.Stmt + getProposerPerformanceStmt *sql.Stmt + getRecentConsensusEventsStmt *sql.Stmt + getRecentRoundsStmt *sql.Stmt + getRoundStmt *sql.Stmt + getRoundsForHeightStmt *sql.Stmt + getRoundsInRangeStmt *sql.Stmt + getValidatorStmt *sql.Stmt + getValidatorConsensusParticipationStmt *sql.Stmt + getValidatorMissedBlockStreaksStmt *sql.Stmt + getValidatorPerformanceTimeSeriesStmt *sql.Stmt + getValidatorRankingStmt *sql.Stmt + getValidatorSigningEfficiencyStmt *sql.Stmt + getValidatorSnapshotStmt *sql.Stmt + getValidatorSnapshotsForHeightStmt *sql.Stmt + getValidatorSnapshotsForValidatorStmt *sql.Stmt + getValidatorUptimeStmt *sql.Stmt + getValidatorsStmt *sql.Stmt + getValidatorsByHeightStmt *sql.Stmt + getVoteStmt *sql.Stmt + getVotesByTypeStmt *sql.Stmt + getVotesForHeightStmt *sql.Stmt + getVotesForRoundStmt *sql.Stmt + getVotesForValidatorStmt *sql.Stmt + getVotingPowerForRoundStmt *sql.Stmt + updateRoundStepStmt *sql.Stmt + updateValidatorStmt *sql.Stmt + upsertHeightStmt *sql.Stmt + upsertRoundStmt *sql.Stmt + upsertValidatorStmt *sql.Stmt + upsertValidatorSnapshotStmt *sql.Stmt + upsertVoteStmt *sql.Stmt +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + tx: tx, + batchCreateValidatorSnapshotsStmt: q.batchCreateValidatorSnapshotsStmt, + createConsensusEventStmt: q.createConsensusEventStmt, + createHeightStmt: q.createHeightStmt, + createRoundStmt: q.createRoundStmt, + createValidatorStmt: q.createValidatorStmt, + createValidatorSnapshotStmt: q.createValidatorSnapshotStmt, + createVoteStmt: q.createVoteStmt, + deleteConsensusEventsOlderThanStmt: q.deleteConsensusEventsOlderThanStmt, + deleteHeightsOlderThanStmt: q.deleteHeightsOlderThanStmt, + deleteRoundsOlderThanStmt: q.deleteRoundsOlderThanStmt, + deleteValidatorStmt: q.deleteValidatorStmt, + deleteValidatorSnapshotsOlderThanStmt: q.deleteValidatorSnapshotsOlderThanStmt, + deleteVotesOlderThanStmt: q.deleteVotesOlderThanStmt, + getAllValidatorsSigningEfficiencyStmt: q.getAllValidatorsSigningEfficiencyStmt, + getConsensusEventStmt: q.getConsensusEventStmt, + getConsensusEventsByTypeStmt: q.getConsensusEventsByTypeStmt, + getConsensusEventsForHeightStmt: q.getConsensusEventsForHeightStmt, + getConsensusEventsForRoundStmt: q.getConsensusEventsForRoundStmt, + getHeightStmt: q.getHeightStmt, + getHeightsStmt: q.getHeightsStmt, + getLatestHeightStmt: q.getLatestHeightStmt, + getProposerPerformanceStmt: q.getProposerPerformanceStmt, + getRecentConsensusEventsStmt: q.getRecentConsensusEventsStmt, + getRecentRoundsStmt: q.getRecentRoundsStmt, + getRoundStmt: q.getRoundStmt, + getRoundsForHeightStmt: q.getRoundsForHeightStmt, + getRoundsInRangeStmt: q.getRoundsInRangeStmt, + getValidatorStmt: q.getValidatorStmt, + getValidatorConsensusParticipationStmt: q.getValidatorConsensusParticipationStmt, + getValidatorMissedBlockStreaksStmt: q.getValidatorMissedBlockStreaksStmt, + getValidatorPerformanceTimeSeriesStmt: q.getValidatorPerformanceTimeSeriesStmt, + getValidatorRankingStmt: q.getValidatorRankingStmt, + getValidatorSigningEfficiencyStmt: q.getValidatorSigningEfficiencyStmt, + getValidatorSnapshotStmt: q.getValidatorSnapshotStmt, + getValidatorSnapshotsForHeightStmt: q.getValidatorSnapshotsForHeightStmt, + getValidatorSnapshotsForValidatorStmt: q.getValidatorSnapshotsForValidatorStmt, + getValidatorUptimeStmt: q.getValidatorUptimeStmt, + getValidatorsStmt: q.getValidatorsStmt, + getValidatorsByHeightStmt: q.getValidatorsByHeightStmt, + getVoteStmt: q.getVoteStmt, + getVotesByTypeStmt: q.getVotesByTypeStmt, + getVotesForHeightStmt: q.getVotesForHeightStmt, + getVotesForRoundStmt: q.getVotesForRoundStmt, + getVotesForValidatorStmt: q.getVotesForValidatorStmt, + getVotingPowerForRoundStmt: q.getVotingPowerForRoundStmt, + updateRoundStepStmt: q.updateRoundStepStmt, + updateValidatorStmt: q.updateValidatorStmt, + upsertHeightStmt: q.upsertHeightStmt, + upsertRoundStmt: q.upsertRoundStmt, + upsertValidatorStmt: q.upsertValidatorStmt, + upsertValidatorSnapshotStmt: q.upsertValidatorSnapshotStmt, + upsertVoteStmt: q.upsertVoteStmt, + } +} diff --git a/pkg/db/sqlc/heights.sql.go b/pkg/db/sqlc/heights.sql.go new file mode 100644 index 0000000..57b1a27 --- /dev/null +++ b/pkg/db/sqlc/heights.sql.go @@ -0,0 +1,163 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: heights.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const createHeight = `-- name: CreateHeight :one +INSERT INTO heights (height, block_hash, block_time, proposer_address, total_validators) +VALUES (?, ?, ?, ?, ?) +RETURNING height, block_hash, block_time, proposer_address, total_validators, created_at +` + +type CreateHeightParams struct { + Height int64 `json:"height"` + BlockHash sql.NullString `json:"block_hash"` + BlockTime sql.NullTime `json:"block_time"` + ProposerAddress sql.NullString `json:"proposer_address"` + TotalValidators sql.NullInt64 `json:"total_validators"` +} + +func (q *Queries) CreateHeight(ctx context.Context, arg CreateHeightParams) (Height, error) { + row := q.queryRow(ctx, q.createHeightStmt, createHeight, + arg.Height, + arg.BlockHash, + arg.BlockTime, + arg.ProposerAddress, + arg.TotalValidators, + ) + var i Height + err := row.Scan( + &i.Height, + &i.BlockHash, + &i.BlockTime, + &i.ProposerAddress, + &i.TotalValidators, + &i.CreatedAt, + ) + return i, err +} + +const deleteHeightsOlderThan = `-- name: DeleteHeightsOlderThan :exec +DELETE FROM heights WHERE height < ? +` + +func (q *Queries) DeleteHeightsOlderThan(ctx context.Context, height int64) error { + _, err := q.exec(ctx, q.deleteHeightsOlderThanStmt, deleteHeightsOlderThan, height) + return err +} + +const getHeight = `-- name: GetHeight :one +SELECT height, block_hash, block_time, proposer_address, total_validators, created_at FROM heights WHERE height = ? LIMIT 1 +` + +func (q *Queries) GetHeight(ctx context.Context, height int64) (Height, error) { + row := q.queryRow(ctx, q.getHeightStmt, getHeight, height) + var i Height + err := row.Scan( + &i.Height, + &i.BlockHash, + &i.BlockTime, + &i.ProposerAddress, + &i.TotalValidators, + &i.CreatedAt, + ) + return i, err +} + +const getHeights = `-- name: GetHeights :many +SELECT height, block_hash, block_time, proposer_address, total_validators, created_at FROM heights ORDER BY height DESC LIMIT ? +` + +func (q *Queries) GetHeights(ctx context.Context, limit int64) ([]Height, error) { + rows, err := q.query(ctx, q.getHeightsStmt, getHeights, limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Height{} + for rows.Next() { + var i Height + if err := rows.Scan( + &i.Height, + &i.BlockHash, + &i.BlockTime, + &i.ProposerAddress, + &i.TotalValidators, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getLatestHeight = `-- name: GetLatestHeight :one +SELECT height, block_hash, block_time, proposer_address, total_validators, created_at FROM heights ORDER BY height DESC LIMIT 1 +` + +func (q *Queries) GetLatestHeight(ctx context.Context) (Height, error) { + row := q.queryRow(ctx, q.getLatestHeightStmt, getLatestHeight) + var i Height + err := row.Scan( + &i.Height, + &i.BlockHash, + &i.BlockTime, + &i.ProposerAddress, + &i.TotalValidators, + &i.CreatedAt, + ) + return i, err +} + +const upsertHeight = `-- name: UpsertHeight :one +INSERT INTO heights (height, block_hash, block_time, proposer_address, total_validators) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(height) DO UPDATE SET + block_hash = excluded.block_hash, + block_time = excluded.block_time, + proposer_address = excluded.proposer_address, + total_validators = excluded.total_validators +RETURNING height, block_hash, block_time, proposer_address, total_validators, created_at +` + +type UpsertHeightParams struct { + Height int64 `json:"height"` + BlockHash sql.NullString `json:"block_hash"` + BlockTime sql.NullTime `json:"block_time"` + ProposerAddress sql.NullString `json:"proposer_address"` + TotalValidators sql.NullInt64 `json:"total_validators"` +} + +func (q *Queries) UpsertHeight(ctx context.Context, arg UpsertHeightParams) (Height, error) { + row := q.queryRow(ctx, q.upsertHeightStmt, upsertHeight, + arg.Height, + arg.BlockHash, + arg.BlockTime, + arg.ProposerAddress, + arg.TotalValidators, + ) + var i Height + err := row.Scan( + &i.Height, + &i.BlockHash, + &i.BlockTime, + &i.ProposerAddress, + &i.TotalValidators, + &i.CreatedAt, + ) + return i, err +} diff --git a/pkg/db/sqlc/models.go b/pkg/db/sqlc/models.go new file mode 100644 index 0000000..f082cc0 --- /dev/null +++ b/pkg/db/sqlc/models.go @@ -0,0 +1,72 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package sqlc + +import ( + "database/sql" + "time" +) + +type ConsensusEvent struct { + ID int64 `json:"id"` + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + EventType string `json:"event_type"` + EventData sql.NullString `json:"event_data"` + Timestamp time.Time `json:"timestamp"` + CreatedAt sql.NullTime `json:"created_at"` +} + +type Height struct { + Height int64 `json:"height"` + BlockHash sql.NullString `json:"block_hash"` + BlockTime sql.NullTime `json:"block_time"` + ProposerAddress sql.NullString `json:"proposer_address"` + TotalValidators sql.NullInt64 `json:"total_validators"` + CreatedAt sql.NullTime `json:"created_at"` +} + +type Round struct { + ID int64 `json:"id"` + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + Step sql.NullInt64 `json:"step"` + StartTime sql.NullTime `json:"start_time"` + ProposerAddress sql.NullString `json:"proposer_address"` + CreatedAt sql.NullTime `json:"created_at"` +} + +type Validator struct { + ID int64 `json:"id"` + OperatorAddress string `json:"operator_address"` + HexAddress string `json:"hex_address"` + PublicKey string `json:"public_key"` + VotingPower int64 `json:"voting_power"` + Moniker sql.NullString `json:"moniker"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type ValidatorSnapshot struct { + ID int64 `json:"id"` + Height int64 `json:"height"` + ValidatorHexAddress string `json:"validator_hex_address"` + VotingPower int64 `json:"voting_power"` + VotingPowerPercent sql.NullFloat64 `json:"voting_power_percent"` + IsProposer sql.NullBool `json:"is_proposer"` + CreatedAt sql.NullTime `json:"created_at"` +} + +type Vote struct { + ID int64 `json:"id"` + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + ValidatorHexAddress string `json:"validator_hex_address"` + VoteType int64 `json:"vote_type"` + BlockHash sql.NullString `json:"block_hash"` + Signature sql.NullString `json:"signature"` + Timestamp sql.NullTime `json:"timestamp"` + CreatedAt sql.NullTime `json:"created_at"` +} diff --git a/pkg/db/sqlc/querier.go b/pkg/db/sqlc/querier.go new file mode 100644 index 0000000..6d1a14d --- /dev/null +++ b/pkg/db/sqlc/querier.go @@ -0,0 +1,74 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package sqlc + +import ( + "context" +) + +type Querier interface { + BatchCreateValidatorSnapshots(ctx context.Context, arg BatchCreateValidatorSnapshotsParams) error + CreateConsensusEvent(ctx context.Context, arg CreateConsensusEventParams) (ConsensusEvent, error) + CreateHeight(ctx context.Context, arg CreateHeightParams) (Height, error) + CreateRound(ctx context.Context, arg CreateRoundParams) (Round, error) + CreateValidator(ctx context.Context, arg CreateValidatorParams) (Validator, error) + CreateValidatorSnapshot(ctx context.Context, arg CreateValidatorSnapshotParams) (ValidatorSnapshot, error) + CreateVote(ctx context.Context, arg CreateVoteParams) (Vote, error) + DeleteConsensusEventsOlderThan(ctx context.Context, height int64) error + DeleteHeightsOlderThan(ctx context.Context, height int64) error + DeleteRoundsOlderThan(ctx context.Context, height int64) error + DeleteValidator(ctx context.Context, hexAddress string) error + DeleteValidatorSnapshotsOlderThan(ctx context.Context, height int64) error + DeleteVotesOlderThan(ctx context.Context, height int64) error + // Get signing efficiency for all validators over a time window + GetAllValidatorsSigningEfficiency(ctx context.Context, arg GetAllValidatorsSigningEfficiencyParams) ([]GetAllValidatorsSigningEfficiencyRow, error) + GetConsensusEvent(ctx context.Context, id int64) (ConsensusEvent, error) + GetConsensusEventsByType(ctx context.Context, arg GetConsensusEventsByTypeParams) ([]ConsensusEvent, error) + GetConsensusEventsForHeight(ctx context.Context, height int64) ([]ConsensusEvent, error) + GetConsensusEventsForRound(ctx context.Context, arg GetConsensusEventsForRoundParams) ([]ConsensusEvent, error) + GetHeight(ctx context.Context, height int64) (Height, error) + GetHeights(ctx context.Context, limit int64) ([]Height, error) + GetLatestHeight(ctx context.Context) (Height, error) + // Calculate proposer performance metrics (when validator is selected as proposer) + GetProposerPerformance(ctx context.Context, arg GetProposerPerformanceParams) (GetProposerPerformanceRow, error) + GetRecentConsensusEvents(ctx context.Context, limit int64) ([]ConsensusEvent, error) + GetRecentRounds(ctx context.Context, limit int64) ([]Round, error) + GetRound(ctx context.Context, arg GetRoundParams) (Round, error) + GetRoundsForHeight(ctx context.Context, height int64) ([]Round, error) + GetRoundsInRange(ctx context.Context, arg GetRoundsInRangeParams) ([]Round, error) + GetValidator(ctx context.Context, hexAddress string) (Validator, error) + // Calculate consensus participation rates for prevotes and precommits + GetValidatorConsensusParticipation(ctx context.Context, arg GetValidatorConsensusParticipationParams) (GetValidatorConsensusParticipationRow, error) + // Find consecutive missed block sequences for a validator + GetValidatorMissedBlockStreaks(ctx context.Context, arg GetValidatorMissedBlockStreaksParams) ([]GetValidatorMissedBlockStreaksRow, error) + // Get signing efficiency over time buckets (hourly) + GetValidatorPerformanceTimeSeries(ctx context.Context, arg GetValidatorPerformanceTimeSeriesParams) ([]GetValidatorPerformanceTimeSeriesRow, error) + // Get validator performance ranking with multiple metrics + GetValidatorRanking(ctx context.Context, arg GetValidatorRankingParams) ([]GetValidatorRankingRow, error) + // Calculate signing efficiency for a validator over a time window + GetValidatorSigningEfficiency(ctx context.Context, arg GetValidatorSigningEfficiencyParams) (GetValidatorSigningEfficiencyRow, error) + GetValidatorSnapshot(ctx context.Context, arg GetValidatorSnapshotParams) (ValidatorSnapshot, error) + GetValidatorSnapshotsForHeight(ctx context.Context, height int64) ([]ValidatorSnapshot, error) + GetValidatorSnapshotsForValidator(ctx context.Context, arg GetValidatorSnapshotsForValidatorParams) ([]ValidatorSnapshot, error) + // Calculate validator uptime metrics over time window + GetValidatorUptime(ctx context.Context, arg GetValidatorUptimeParams) (GetValidatorUptimeRow, error) + GetValidators(ctx context.Context) ([]Validator, error) + GetValidatorsByHeight(ctx context.Context, height int64) ([]GetValidatorsByHeightRow, error) + GetVote(ctx context.Context, arg GetVoteParams) (Vote, error) + GetVotesByType(ctx context.Context, arg GetVotesByTypeParams) ([]Vote, error) + GetVotesForHeight(ctx context.Context, height int64) ([]Vote, error) + GetVotesForRound(ctx context.Context, arg GetVotesForRoundParams) ([]Vote, error) + GetVotesForValidator(ctx context.Context, arg GetVotesForValidatorParams) ([]Vote, error) + GetVotingPowerForRound(ctx context.Context, arg GetVotingPowerForRoundParams) (GetVotingPowerForRoundRow, error) + UpdateRoundStep(ctx context.Context, arg UpdateRoundStepParams) (Round, error) + UpdateValidator(ctx context.Context, arg UpdateValidatorParams) (Validator, error) + UpsertHeight(ctx context.Context, arg UpsertHeightParams) (Height, error) + UpsertRound(ctx context.Context, arg UpsertRoundParams) (Round, error) + UpsertValidator(ctx context.Context, arg UpsertValidatorParams) (Validator, error) + UpsertValidatorSnapshot(ctx context.Context, arg UpsertValidatorSnapshotParams) (ValidatorSnapshot, error) + UpsertVote(ctx context.Context, arg UpsertVoteParams) (Vote, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/pkg/db/sqlc/rounds.sql.go b/pkg/db/sqlc/rounds.sql.go new file mode 100644 index 0000000..7e5ce2f --- /dev/null +++ b/pkg/db/sqlc/rounds.sql.go @@ -0,0 +1,255 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: rounds.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const createRound = `-- name: CreateRound :one +INSERT INTO rounds (height, round_number, step, start_time, proposer_address) +VALUES (?, ?, ?, ?, ?) +RETURNING id, height, round_number, step, start_time, proposer_address, created_at +` + +type CreateRoundParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + Step sql.NullInt64 `json:"step"` + StartTime sql.NullTime `json:"start_time"` + ProposerAddress sql.NullString `json:"proposer_address"` +} + +func (q *Queries) CreateRound(ctx context.Context, arg CreateRoundParams) (Round, error) { + row := q.queryRow(ctx, q.createRoundStmt, createRound, + arg.Height, + arg.RoundNumber, + arg.Step, + arg.StartTime, + arg.ProposerAddress, + ) + var i Round + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.Step, + &i.StartTime, + &i.ProposerAddress, + &i.CreatedAt, + ) + return i, err +} + +const deleteRoundsOlderThan = `-- name: DeleteRoundsOlderThan :exec +DELETE FROM rounds WHERE height < ? +` + +func (q *Queries) DeleteRoundsOlderThan(ctx context.Context, height int64) error { + _, err := q.exec(ctx, q.deleteRoundsOlderThanStmt, deleteRoundsOlderThan, height) + return err +} + +const getRecentRounds = `-- name: GetRecentRounds :many +SELECT id, height, round_number, step, start_time, proposer_address, created_at FROM rounds ORDER BY height DESC, round_number DESC LIMIT ? +` + +func (q *Queries) GetRecentRounds(ctx context.Context, limit int64) ([]Round, error) { + rows, err := q.query(ctx, q.getRecentRoundsStmt, getRecentRounds, limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Round{} + for rows.Next() { + var i Round + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.Step, + &i.StartTime, + &i.ProposerAddress, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getRound = `-- name: GetRound :one +SELECT id, height, round_number, step, start_time, proposer_address, created_at FROM rounds WHERE height = ? AND round_number = ? LIMIT 1 +` + +type GetRoundParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` +} + +func (q *Queries) GetRound(ctx context.Context, arg GetRoundParams) (Round, error) { + row := q.queryRow(ctx, q.getRoundStmt, getRound, arg.Height, arg.RoundNumber) + var i Round + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.Step, + &i.StartTime, + &i.ProposerAddress, + &i.CreatedAt, + ) + return i, err +} + +const getRoundsForHeight = `-- name: GetRoundsForHeight :many +SELECT id, height, round_number, step, start_time, proposer_address, created_at FROM rounds WHERE height = ? ORDER BY round_number +` + +func (q *Queries) GetRoundsForHeight(ctx context.Context, height int64) ([]Round, error) { + rows, err := q.query(ctx, q.getRoundsForHeightStmt, getRoundsForHeight, height) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Round{} + for rows.Next() { + var i Round + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.Step, + &i.StartTime, + &i.ProposerAddress, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getRoundsInRange = `-- name: GetRoundsInRange :many +SELECT id, height, round_number, step, start_time, proposer_address, created_at FROM rounds +WHERE height >= ? AND height <= ? +ORDER BY height DESC, round_number DESC +` + +type GetRoundsInRangeParams struct { + Height int64 `json:"height"` + Height_2 int64 `json:"height_2"` +} + +func (q *Queries) GetRoundsInRange(ctx context.Context, arg GetRoundsInRangeParams) ([]Round, error) { + rows, err := q.query(ctx, q.getRoundsInRangeStmt, getRoundsInRange, arg.Height, arg.Height_2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Round{} + for rows.Next() { + var i Round + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.Step, + &i.StartTime, + &i.ProposerAddress, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateRoundStep = `-- name: UpdateRoundStep :one +UPDATE rounds SET step = ? WHERE height = ? AND round_number = ? RETURNING id, height, round_number, step, start_time, proposer_address, created_at +` + +type UpdateRoundStepParams struct { + Step sql.NullInt64 `json:"step"` + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` +} + +func (q *Queries) UpdateRoundStep(ctx context.Context, arg UpdateRoundStepParams) (Round, error) { + row := q.queryRow(ctx, q.updateRoundStepStmt, updateRoundStep, arg.Step, arg.Height, arg.RoundNumber) + var i Round + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.Step, + &i.StartTime, + &i.ProposerAddress, + &i.CreatedAt, + ) + return i, err +} + +const upsertRound = `-- name: UpsertRound :one +INSERT INTO rounds (height, round_number, step, start_time, proposer_address) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(height, round_number) DO UPDATE SET + step = excluded.step, + start_time = excluded.start_time, + proposer_address = excluded.proposer_address +RETURNING id, height, round_number, step, start_time, proposer_address, created_at +` + +type UpsertRoundParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + Step sql.NullInt64 `json:"step"` + StartTime sql.NullTime `json:"start_time"` + ProposerAddress sql.NullString `json:"proposer_address"` +} + +func (q *Queries) UpsertRound(ctx context.Context, arg UpsertRoundParams) (Round, error) { + row := q.queryRow(ctx, q.upsertRoundStmt, upsertRound, + arg.Height, + arg.RoundNumber, + arg.Step, + arg.StartTime, + arg.ProposerAddress, + ) + var i Round + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.Step, + &i.StartTime, + &i.ProposerAddress, + &i.CreatedAt, + ) + return i, err +} diff --git a/pkg/db/sqlc/validator_snapshots.sql.go b/pkg/db/sqlc/validator_snapshots.sql.go new file mode 100644 index 0000000..9d2a516 --- /dev/null +++ b/pkg/db/sqlc/validator_snapshots.sql.go @@ -0,0 +1,223 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: validator_snapshots.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const batchCreateValidatorSnapshots = `-- name: BatchCreateValidatorSnapshots :exec +INSERT INTO validator_snapshots (height, validator_hex_address, voting_power, voting_power_percent, is_proposer) +VALUES (?, ?, ?, ?, ?) +` + +type BatchCreateValidatorSnapshotsParams struct { + Height int64 `json:"height"` + ValidatorHexAddress string `json:"validator_hex_address"` + VotingPower int64 `json:"voting_power"` + VotingPowerPercent sql.NullFloat64 `json:"voting_power_percent"` + IsProposer sql.NullBool `json:"is_proposer"` +} + +func (q *Queries) BatchCreateValidatorSnapshots(ctx context.Context, arg BatchCreateValidatorSnapshotsParams) error { + _, err := q.exec(ctx, q.batchCreateValidatorSnapshotsStmt, batchCreateValidatorSnapshots, + arg.Height, + arg.ValidatorHexAddress, + arg.VotingPower, + arg.VotingPowerPercent, + arg.IsProposer, + ) + return err +} + +const createValidatorSnapshot = `-- name: CreateValidatorSnapshot :one +INSERT INTO validator_snapshots (height, validator_hex_address, voting_power, voting_power_percent, is_proposer) +VALUES (?, ?, ?, ?, ?) +RETURNING id, height, validator_hex_address, voting_power, voting_power_percent, is_proposer, created_at +` + +type CreateValidatorSnapshotParams struct { + Height int64 `json:"height"` + ValidatorHexAddress string `json:"validator_hex_address"` + VotingPower int64 `json:"voting_power"` + VotingPowerPercent sql.NullFloat64 `json:"voting_power_percent"` + IsProposer sql.NullBool `json:"is_proposer"` +} + +func (q *Queries) CreateValidatorSnapshot(ctx context.Context, arg CreateValidatorSnapshotParams) (ValidatorSnapshot, error) { + row := q.queryRow(ctx, q.createValidatorSnapshotStmt, createValidatorSnapshot, + arg.Height, + arg.ValidatorHexAddress, + arg.VotingPower, + arg.VotingPowerPercent, + arg.IsProposer, + ) + var i ValidatorSnapshot + err := row.Scan( + &i.ID, + &i.Height, + &i.ValidatorHexAddress, + &i.VotingPower, + &i.VotingPowerPercent, + &i.IsProposer, + &i.CreatedAt, + ) + return i, err +} + +const deleteValidatorSnapshotsOlderThan = `-- name: DeleteValidatorSnapshotsOlderThan :exec +DELETE FROM validator_snapshots WHERE height < ? +` + +func (q *Queries) DeleteValidatorSnapshotsOlderThan(ctx context.Context, height int64) error { + _, err := q.exec(ctx, q.deleteValidatorSnapshotsOlderThanStmt, deleteValidatorSnapshotsOlderThan, height) + return err +} + +const getValidatorSnapshot = `-- name: GetValidatorSnapshot :one +SELECT id, height, validator_hex_address, voting_power, voting_power_percent, is_proposer, created_at FROM validator_snapshots +WHERE height = ? AND validator_hex_address = ? +LIMIT 1 +` + +type GetValidatorSnapshotParams struct { + Height int64 `json:"height"` + ValidatorHexAddress string `json:"validator_hex_address"` +} + +func (q *Queries) GetValidatorSnapshot(ctx context.Context, arg GetValidatorSnapshotParams) (ValidatorSnapshot, error) { + row := q.queryRow(ctx, q.getValidatorSnapshotStmt, getValidatorSnapshot, arg.Height, arg.ValidatorHexAddress) + var i ValidatorSnapshot + err := row.Scan( + &i.ID, + &i.Height, + &i.ValidatorHexAddress, + &i.VotingPower, + &i.VotingPowerPercent, + &i.IsProposer, + &i.CreatedAt, + ) + return i, err +} + +const getValidatorSnapshotsForHeight = `-- name: GetValidatorSnapshotsForHeight :many +SELECT id, height, validator_hex_address, voting_power, voting_power_percent, is_proposer, created_at FROM validator_snapshots +WHERE height = ? +ORDER BY voting_power DESC +` + +func (q *Queries) GetValidatorSnapshotsForHeight(ctx context.Context, height int64) ([]ValidatorSnapshot, error) { + rows, err := q.query(ctx, q.getValidatorSnapshotsForHeightStmt, getValidatorSnapshotsForHeight, height) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ValidatorSnapshot{} + for rows.Next() { + var i ValidatorSnapshot + if err := rows.Scan( + &i.ID, + &i.Height, + &i.ValidatorHexAddress, + &i.VotingPower, + &i.VotingPowerPercent, + &i.IsProposer, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getValidatorSnapshotsForValidator = `-- name: GetValidatorSnapshotsForValidator :many +SELECT id, height, validator_hex_address, voting_power, voting_power_percent, is_proposer, created_at FROM validator_snapshots +WHERE validator_hex_address = ? AND height >= ? +ORDER BY height DESC +` + +type GetValidatorSnapshotsForValidatorParams struct { + ValidatorHexAddress string `json:"validator_hex_address"` + Height int64 `json:"height"` +} + +func (q *Queries) GetValidatorSnapshotsForValidator(ctx context.Context, arg GetValidatorSnapshotsForValidatorParams) ([]ValidatorSnapshot, error) { + rows, err := q.query(ctx, q.getValidatorSnapshotsForValidatorStmt, getValidatorSnapshotsForValidator, arg.ValidatorHexAddress, arg.Height) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ValidatorSnapshot{} + for rows.Next() { + var i ValidatorSnapshot + if err := rows.Scan( + &i.ID, + &i.Height, + &i.ValidatorHexAddress, + &i.VotingPower, + &i.VotingPowerPercent, + &i.IsProposer, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const upsertValidatorSnapshot = `-- name: UpsertValidatorSnapshot :one +INSERT INTO validator_snapshots (height, validator_hex_address, voting_power, voting_power_percent, is_proposer) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(height, validator_hex_address) DO UPDATE SET + voting_power = excluded.voting_power, + voting_power_percent = excluded.voting_power_percent, + is_proposer = excluded.is_proposer +RETURNING id, height, validator_hex_address, voting_power, voting_power_percent, is_proposer, created_at +` + +type UpsertValidatorSnapshotParams struct { + Height int64 `json:"height"` + ValidatorHexAddress string `json:"validator_hex_address"` + VotingPower int64 `json:"voting_power"` + VotingPowerPercent sql.NullFloat64 `json:"voting_power_percent"` + IsProposer sql.NullBool `json:"is_proposer"` +} + +func (q *Queries) UpsertValidatorSnapshot(ctx context.Context, arg UpsertValidatorSnapshotParams) (ValidatorSnapshot, error) { + row := q.queryRow(ctx, q.upsertValidatorSnapshotStmt, upsertValidatorSnapshot, + arg.Height, + arg.ValidatorHexAddress, + arg.VotingPower, + arg.VotingPowerPercent, + arg.IsProposer, + ) + var i ValidatorSnapshot + err := row.Scan( + &i.ID, + &i.Height, + &i.ValidatorHexAddress, + &i.VotingPower, + &i.VotingPowerPercent, + &i.IsProposer, + &i.CreatedAt, + ) + return i, err +} diff --git a/pkg/db/sqlc/validators.sql.go b/pkg/db/sqlc/validators.sql.go new file mode 100644 index 0000000..5e3e747 --- /dev/null +++ b/pkg/db/sqlc/validators.sql.go @@ -0,0 +1,248 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: validators.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const createValidator = `-- name: CreateValidator :one +INSERT INTO validators (operator_address, hex_address, public_key, voting_power, moniker) +VALUES (?, ?, ?, ?, ?) +RETURNING id, operator_address, hex_address, public_key, voting_power, moniker, created_at, updated_at +` + +type CreateValidatorParams struct { + OperatorAddress string `json:"operator_address"` + HexAddress string `json:"hex_address"` + PublicKey string `json:"public_key"` + VotingPower int64 `json:"voting_power"` + Moniker sql.NullString `json:"moniker"` +} + +func (q *Queries) CreateValidator(ctx context.Context, arg CreateValidatorParams) (Validator, error) { + row := q.queryRow(ctx, q.createValidatorStmt, createValidator, + arg.OperatorAddress, + arg.HexAddress, + arg.PublicKey, + arg.VotingPower, + arg.Moniker, + ) + var i Validator + err := row.Scan( + &i.ID, + &i.OperatorAddress, + &i.HexAddress, + &i.PublicKey, + &i.VotingPower, + &i.Moniker, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteValidator = `-- name: DeleteValidator :exec +DELETE FROM validators WHERE hex_address = ? +` + +func (q *Queries) DeleteValidator(ctx context.Context, hexAddress string) error { + _, err := q.exec(ctx, q.deleteValidatorStmt, deleteValidator, hexAddress) + return err +} + +const getValidator = `-- name: GetValidator :one +SELECT id, operator_address, hex_address, public_key, voting_power, moniker, created_at, updated_at FROM validators WHERE hex_address = ? LIMIT 1 +` + +func (q *Queries) GetValidator(ctx context.Context, hexAddress string) (Validator, error) { + row := q.queryRow(ctx, q.getValidatorStmt, getValidator, hexAddress) + var i Validator + err := row.Scan( + &i.ID, + &i.OperatorAddress, + &i.HexAddress, + &i.PublicKey, + &i.VotingPower, + &i.Moniker, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getValidators = `-- name: GetValidators :many +SELECT id, operator_address, hex_address, public_key, voting_power, moniker, created_at, updated_at FROM validators ORDER BY voting_power DESC +` + +func (q *Queries) GetValidators(ctx context.Context) ([]Validator, error) { + rows, err := q.query(ctx, q.getValidatorsStmt, getValidators) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Validator{} + for rows.Next() { + var i Validator + if err := rows.Scan( + &i.ID, + &i.OperatorAddress, + &i.HexAddress, + &i.PublicKey, + &i.VotingPower, + &i.Moniker, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getValidatorsByHeight = `-- name: GetValidatorsByHeight :many +SELECT v.id, v.operator_address, v.hex_address, v.public_key, v.voting_power, v.moniker, v.created_at, v.updated_at, vs.voting_power, vs.voting_power_percent, vs.is_proposer +FROM validators v +JOIN validator_snapshots vs ON v.hex_address = vs.validator_hex_address +WHERE vs.height = ? +ORDER BY vs.voting_power DESC +` + +type GetValidatorsByHeightRow struct { + ID int64 `json:"id"` + OperatorAddress string `json:"operator_address"` + HexAddress string `json:"hex_address"` + PublicKey string `json:"public_key"` + VotingPower int64 `json:"voting_power"` + Moniker sql.NullString `json:"moniker"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` + VotingPower_2 int64 `json:"voting_power_2"` + VotingPowerPercent sql.NullFloat64 `json:"voting_power_percent"` + IsProposer sql.NullBool `json:"is_proposer"` +} + +func (q *Queries) GetValidatorsByHeight(ctx context.Context, height int64) ([]GetValidatorsByHeightRow, error) { + rows, err := q.query(ctx, q.getValidatorsByHeightStmt, getValidatorsByHeight, height) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetValidatorsByHeightRow{} + for rows.Next() { + var i GetValidatorsByHeightRow + if err := rows.Scan( + &i.ID, + &i.OperatorAddress, + &i.HexAddress, + &i.PublicKey, + &i.VotingPower, + &i.Moniker, + &i.CreatedAt, + &i.UpdatedAt, + &i.VotingPower_2, + &i.VotingPowerPercent, + &i.IsProposer, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateValidator = `-- name: UpdateValidator :one +UPDATE validators +SET operator_address = ?, public_key = ?, voting_power = ?, moniker = ?, updated_at = CURRENT_TIMESTAMP +WHERE hex_address = ? +RETURNING id, operator_address, hex_address, public_key, voting_power, moniker, created_at, updated_at +` + +type UpdateValidatorParams struct { + OperatorAddress string `json:"operator_address"` + PublicKey string `json:"public_key"` + VotingPower int64 `json:"voting_power"` + Moniker sql.NullString `json:"moniker"` + HexAddress string `json:"hex_address"` +} + +func (q *Queries) UpdateValidator(ctx context.Context, arg UpdateValidatorParams) (Validator, error) { + row := q.queryRow(ctx, q.updateValidatorStmt, updateValidator, + arg.OperatorAddress, + arg.PublicKey, + arg.VotingPower, + arg.Moniker, + arg.HexAddress, + ) + var i Validator + err := row.Scan( + &i.ID, + &i.OperatorAddress, + &i.HexAddress, + &i.PublicKey, + &i.VotingPower, + &i.Moniker, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const upsertValidator = `-- name: UpsertValidator :one +INSERT INTO validators (operator_address, hex_address, public_key, voting_power, moniker) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(hex_address) DO UPDATE SET + operator_address = excluded.operator_address, + public_key = excluded.public_key, + voting_power = excluded.voting_power, + moniker = excluded.moniker, + updated_at = CURRENT_TIMESTAMP +RETURNING id, operator_address, hex_address, public_key, voting_power, moniker, created_at, updated_at +` + +type UpsertValidatorParams struct { + OperatorAddress string `json:"operator_address"` + HexAddress string `json:"hex_address"` + PublicKey string `json:"public_key"` + VotingPower int64 `json:"voting_power"` + Moniker sql.NullString `json:"moniker"` +} + +func (q *Queries) UpsertValidator(ctx context.Context, arg UpsertValidatorParams) (Validator, error) { + row := q.queryRow(ctx, q.upsertValidatorStmt, upsertValidator, + arg.OperatorAddress, + arg.HexAddress, + arg.PublicKey, + arg.VotingPower, + arg.Moniker, + ) + var i Validator + err := row.Scan( + &i.ID, + &i.OperatorAddress, + &i.HexAddress, + &i.PublicKey, + &i.VotingPower, + &i.Moniker, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/pkg/db/sqlc/votes.sql.go b/pkg/db/sqlc/votes.sql.go new file mode 100644 index 0000000..cd05cdc --- /dev/null +++ b/pkg/db/sqlc/votes.sql.go @@ -0,0 +1,339 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: votes.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const createVote = `-- name: CreateVote :one +INSERT INTO votes (height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp) +VALUES (?, ?, ?, ?, ?, ?, ?) +RETURNING id, height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp, created_at +` + +type CreateVoteParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + ValidatorHexAddress string `json:"validator_hex_address"` + VoteType int64 `json:"vote_type"` + BlockHash sql.NullString `json:"block_hash"` + Signature sql.NullString `json:"signature"` + Timestamp sql.NullTime `json:"timestamp"` +} + +func (q *Queries) CreateVote(ctx context.Context, arg CreateVoteParams) (Vote, error) { + row := q.queryRow(ctx, q.createVoteStmt, createVote, + arg.Height, + arg.RoundNumber, + arg.ValidatorHexAddress, + arg.VoteType, + arg.BlockHash, + arg.Signature, + arg.Timestamp, + ) + var i Vote + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.ValidatorHexAddress, + &i.VoteType, + &i.BlockHash, + &i.Signature, + &i.Timestamp, + &i.CreatedAt, + ) + return i, err +} + +const deleteVotesOlderThan = `-- name: DeleteVotesOlderThan :exec +DELETE FROM votes WHERE height < ? +` + +func (q *Queries) DeleteVotesOlderThan(ctx context.Context, height int64) error { + _, err := q.exec(ctx, q.deleteVotesOlderThanStmt, deleteVotesOlderThan, height) + return err +} + +const getVote = `-- name: GetVote :one +SELECT id, height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp, created_at FROM votes +WHERE height = ? AND round_number = ? AND validator_hex_address = ? AND vote_type = ? +LIMIT 1 +` + +type GetVoteParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + ValidatorHexAddress string `json:"validator_hex_address"` + VoteType int64 `json:"vote_type"` +} + +func (q *Queries) GetVote(ctx context.Context, arg GetVoteParams) (Vote, error) { + row := q.queryRow(ctx, q.getVoteStmt, getVote, + arg.Height, + arg.RoundNumber, + arg.ValidatorHexAddress, + arg.VoteType, + ) + var i Vote + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.ValidatorHexAddress, + &i.VoteType, + &i.BlockHash, + &i.Signature, + &i.Timestamp, + &i.CreatedAt, + ) + return i, err +} + +const getVotesByType = `-- name: GetVotesByType :many +SELECT id, height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp, created_at FROM votes +WHERE height = ? AND round_number = ? AND vote_type = ? +ORDER BY validator_hex_address +` + +type GetVotesByTypeParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + VoteType int64 `json:"vote_type"` +} + +func (q *Queries) GetVotesByType(ctx context.Context, arg GetVotesByTypeParams) ([]Vote, error) { + rows, err := q.query(ctx, q.getVotesByTypeStmt, getVotesByType, arg.Height, arg.RoundNumber, arg.VoteType) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Vote{} + for rows.Next() { + var i Vote + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.ValidatorHexAddress, + &i.VoteType, + &i.BlockHash, + &i.Signature, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getVotesForHeight = `-- name: GetVotesForHeight :many +SELECT id, height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp, created_at FROM votes WHERE height = ? ORDER BY round_number, vote_type, validator_hex_address +` + +func (q *Queries) GetVotesForHeight(ctx context.Context, height int64) ([]Vote, error) { + rows, err := q.query(ctx, q.getVotesForHeightStmt, getVotesForHeight, height) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Vote{} + for rows.Next() { + var i Vote + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.ValidatorHexAddress, + &i.VoteType, + &i.BlockHash, + &i.Signature, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getVotesForRound = `-- name: GetVotesForRound :many +SELECT id, height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp, created_at FROM votes +WHERE height = ? AND round_number = ? +ORDER BY vote_type, validator_hex_address +` + +type GetVotesForRoundParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` +} + +func (q *Queries) GetVotesForRound(ctx context.Context, arg GetVotesForRoundParams) ([]Vote, error) { + rows, err := q.query(ctx, q.getVotesForRoundStmt, getVotesForRound, arg.Height, arg.RoundNumber) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Vote{} + for rows.Next() { + var i Vote + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.ValidatorHexAddress, + &i.VoteType, + &i.BlockHash, + &i.Signature, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getVotesForValidator = `-- name: GetVotesForValidator :many +SELECT id, height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp, created_at FROM votes +WHERE validator_hex_address = ? AND height >= ? +ORDER BY height DESC, round_number DESC +` + +type GetVotesForValidatorParams struct { + ValidatorHexAddress string `json:"validator_hex_address"` + Height int64 `json:"height"` +} + +func (q *Queries) GetVotesForValidator(ctx context.Context, arg GetVotesForValidatorParams) ([]Vote, error) { + rows, err := q.query(ctx, q.getVotesForValidatorStmt, getVotesForValidator, arg.ValidatorHexAddress, arg.Height) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Vote{} + for rows.Next() { + var i Vote + if err := rows.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.ValidatorHexAddress, + &i.VoteType, + &i.BlockHash, + &i.Signature, + &i.Timestamp, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getVotingPowerForRound = `-- name: GetVotingPowerForRound :one +SELECT + SUM(CASE WHEN v.vote_type = 1 AND v.block_hash IS NOT NULL THEN vs.voting_power ELSE 0 END) as prevote_power, + SUM(CASE WHEN v.vote_type = 2 AND v.block_hash IS NOT NULL THEN vs.voting_power ELSE 0 END) as precommit_power, + SUM(vs.voting_power) as total_power +FROM votes v +JOIN validator_snapshots vs ON v.validator_hex_address = vs.validator_hex_address AND v.height = vs.height +WHERE v.height = ? AND v.round_number = ? +` + +type GetVotingPowerForRoundParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` +} + +type GetVotingPowerForRoundRow struct { + PrevotePower sql.NullFloat64 `json:"prevote_power"` + PrecommitPower sql.NullFloat64 `json:"precommit_power"` + TotalPower sql.NullFloat64 `json:"total_power"` +} + +func (q *Queries) GetVotingPowerForRound(ctx context.Context, arg GetVotingPowerForRoundParams) (GetVotingPowerForRoundRow, error) { + row := q.queryRow(ctx, q.getVotingPowerForRoundStmt, getVotingPowerForRound, arg.Height, arg.RoundNumber) + var i GetVotingPowerForRoundRow + err := row.Scan(&i.PrevotePower, &i.PrecommitPower, &i.TotalPower) + return i, err +} + +const upsertVote = `-- name: UpsertVote :one +INSERT INTO votes (height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(height, round_number, validator_hex_address, vote_type) DO UPDATE SET + block_hash = excluded.block_hash, + signature = excluded.signature, + timestamp = excluded.timestamp +RETURNING id, height, round_number, validator_hex_address, vote_type, block_hash, signature, timestamp, created_at +` + +type UpsertVoteParams struct { + Height int64 `json:"height"` + RoundNumber int64 `json:"round_number"` + ValidatorHexAddress string `json:"validator_hex_address"` + VoteType int64 `json:"vote_type"` + BlockHash sql.NullString `json:"block_hash"` + Signature sql.NullString `json:"signature"` + Timestamp sql.NullTime `json:"timestamp"` +} + +func (q *Queries) UpsertVote(ctx context.Context, arg UpsertVoteParams) (Vote, error) { + row := q.queryRow(ctx, q.upsertVoteStmt, upsertVote, + arg.Height, + arg.RoundNumber, + arg.ValidatorHexAddress, + arg.VoteType, + arg.BlockHash, + arg.Signature, + arg.Timestamp, + ) + var i Vote + err := row.Scan( + &i.ID, + &i.Height, + &i.RoundNumber, + &i.ValidatorHexAddress, + &i.VoteType, + &i.BlockHash, + &i.Signature, + &i.Timestamp, + &i.CreatedAt, + ) + return i, err +} diff --git a/pkg/display/all_rounds_table.go b/pkg/display/all_rounds_table.go index a7f13aa..d83e5b0 100644 --- a/pkg/display/all_rounds_table.go +++ b/pkg/display/all_rounds_table.go @@ -1,38 +1,42 @@ package display import ( - "main/pkg/types" + "fmt" "strconv" - "sync" - "github.com/gdamore/tcell/v2" + "main/pkg/types" + "main/pkg/utils" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) type AllRoundsTableData struct { tview.TableContentReadOnly - Validators types.ValidatorsWithInfoAndAllRoundVotes + RoundData *types.RoundDataMap + Validators []types.TMValidator DisableEmojis bool Transpose bool + CurrentHeight int64 cells [][]*tview.TableCell - mutex sync.Mutex + mutex *utils.NoopLocker } func NewAllRoundsTableData(disableEmojis bool, transpose bool) *AllRoundsTableData { return &AllRoundsTableData{ - Validators: types.ValidatorsWithInfoAndAllRoundVotes{}, DisableEmojis: disableEmojis, Transpose: transpose, cells: [][]*tview.TableCell{}, + mutex: &utils.NoopLocker{}, } } func (d *AllRoundsTableData) GetCell(row, column int) *tview.TableCell { - d.mutex.Lock() - defer d.mutex.Unlock() + d.mutex.RLock() + defer d.mutex.RUnlock() if len(d.cells) <= row { return nil @@ -46,15 +50,15 @@ func (d *AllRoundsTableData) GetCell(row, column int) *tview.TableCell { } func (d *AllRoundsTableData) GetRowCount() int { - d.mutex.Lock() - defer d.mutex.Unlock() + d.mutex.RLock() + defer d.mutex.RUnlock() return len(d.cells) } func (d *AllRoundsTableData) GetColumnCount() int { - d.mutex.Lock() - defer d.mutex.Unlock() + d.mutex.RLock() + defer d.mutex.RUnlock() if len(d.cells) == 0 { return 0 @@ -63,68 +67,99 @@ func (d *AllRoundsTableData) GetColumnCount() int { return len(d.cells[0]) } -func (d *AllRoundsTableData) SetValidators(validators types.ValidatorsWithInfoAndAllRoundVotes) { - if d.Validators.Equals(validators) { - return - } +func (d *AllRoundsTableData) SetTMValidators(validators types.TMValidators, height int64) { + d.mutex.Lock() + if d.CurrentHeight == 0 && height > 0 { + d.CurrentHeight = height + } d.Validators = validators - d.redrawCells() + + d.mutex.Unlock() +} + +func (d *AllRoundsTableData) SetRoundData(roundData *types.RoundDataMap) { + d.mutex.Lock() + d.RoundData = roundData + d.mutex.Unlock() } func (d *AllRoundsTableData) SetTranspose(transpose bool) { + d.mutex.Lock() d.Transpose = transpose - d.redrawCells() + d.mutex.Unlock() } -func (d *AllRoundsTableData) redrawCells() { +func (d *AllRoundsTableData) Update() { + cells := d.createCells() + d.mutex.Lock() defer d.mutex.Unlock() + d.cells = cells +} - d.cells = make([][]*tview.TableCell, len(d.Validators.Validators)+1) +// Create cells for the table. +func (d *AllRoundsTableData) createCells() [][]*tview.TableCell { + cells := [][]*tview.TableCell{} - for row := 0; row < len(d.Validators.Validators)+1; row++ { - d.cells[row] = make([]*tview.TableCell, len(d.Validators.RoundsVotes)+1) + // Check if we have validators to display + if len(d.Validators) == 0 { + return cells + } - for column := 0; column < len(d.Validators.RoundsVotes)+1; column++ { - round := column - 1 - if d.Transpose { - round = len(d.Validators.RoundsVotes) - column - } + // Create header row with bold text + headerRow := []*tview.TableCell{ + tview.NewTableCell("Validator"). + SetSelectable(false). + SetStyle(tcell.StyleDefault.Bold(true)), - // Table header. - if row == 0 { - text := "validator" - if column != 0 { - text = strconv.Itoa(round) - } - - d.cells[row][column] = tview. - NewTableCell(text). - SetAlign(tview.AlignCenter). - SetStyle(tcell.StyleDefault.Bold(true)) - continue - } + tview.NewTableCell(""). + SetSelectable(false), + } - // First column is always validators list. - if column == 0 { - text := d.Validators.Validators[row-1].Serialize() - cell := tview.NewTableCell(text) - d.cells[row][column] = cell - continue - } + for hr := range d.RoundData.ReverseIter() { + // Format height to show only last 4 digits -> this can be adjusted by preference + heightStr := strconv.Itoa(int(hr.Height)) + if len(heightStr) > 4 { + heightStr = heightStr[len(heightStr)-4:] + } - roundVotes := d.Validators.RoundsVotes[round] - roundVote := roundVotes[row-1] - text := roundVote.Serialize(d.DisableEmojis) + headerCell := tview.NewTableCell(fmt.Sprintf("%s.%d", heightStr, hr.Round)). + SetSelectable(false). + SetStyle(tcell.StyleDefault.Bold(true)) + headerRow = append(headerRow, headerCell) + } + cells = append(cells, headerRow) - cell := tview.NewTableCell(text) + // Create validator rows using TMValidators + for i, validator := range d.Validators { + row := []*tview.TableCell{} - if roundVote.IsProposer { + // enumerated validator name + name := validator.GetDisplayName() + row = append(row, tview.NewTableCell(fmt.Sprintf("%d. %s", i+1, name))) + row = append(row, tview.NewTableCell(fmt.Sprintf("(%.2f%%)", validator.VotingPowerPercent))) + + for _, roundData := range d.RoundData.ReverseIter() { + valVotes := roundData.Votes[validator.GetDisplayAddress()] + + // Use new VoteState functions with CometBFT types + prevote := types.VoteStateFromVotesMap(valVotes, cmtproto.PrevoteType) + precommit := types.VoteStateFromVotesMap(valVotes, cmtproto.PrecommitType) + + cell := tview.NewTableCell(" " + precommit.Serialize(d.DisableEmojis) + prevote.Serialize(d.DisableEmojis) + " ") + if roundData.Proposers.Has(validator.GetDisplayAddress()) { cell.SetBackgroundColor(tcell.ColorForestGreen) } - - d.cells[row][column] = cell + row = append(row, cell) } + + cells = append(cells, row) } + + return cells +} + +func (d *AllRoundsTableData) HandleKey(event *tcell.EventKey) bool { + return false } diff --git a/pkg/display/last_round_table.go b/pkg/display/last_round_table.go index d8101a8..03ea03e 100644 --- a/pkg/display/last_round_table.go +++ b/pkg/display/last_round_table.go @@ -2,9 +2,12 @@ package display import ( "fmt" + "strconv" + "main/pkg/types" - "sync" + "main/pkg/utils" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -12,40 +15,53 @@ import ( type LastRoundTableData struct { tview.TableContentReadOnly - Validators types.ValidatorsWithInfo + TMValidators types.TMValidators + RoundData *types.RoundDataMap + CurrentHeight int64 + CurrentRound int32 ConsensusError error ColumnsCount int DisableEmojis bool Transpose bool cells [][]*tview.TableCell - mutex sync.Mutex + mutex *utils.NoopLocker } func NewLastRoundTableData(columnsCount int, disableEmojis bool, transpose bool) *LastRoundTableData { return &LastRoundTableData{ ColumnsCount: columnsCount, - Validators: make(types.ValidatorsWithInfo, 0), + TMValidators: make(types.TMValidators, 0), + RoundData: types.NewRoundDataMap(), + CurrentHeight: 0, + CurrentRound: 0, DisableEmojis: disableEmojis, Transpose: transpose, cells: [][]*tview.TableCell{}, + mutex: &utils.NoopLocker{}, } } func (d *LastRoundTableData) SetColumnsCount(count int) { + d.mutex.Lock() d.ColumnsCount = count + d.mutex.Unlock() + d.redrawData() } func (d *LastRoundTableData) SetTranspose(transpose bool) { + d.mutex.Lock() d.Transpose = transpose + d.mutex.Unlock() + d.redrawData() } func (d *LastRoundTableData) GetCell(row, column int) *tview.TableCell { - d.mutex.Lock() - defer d.mutex.Unlock() + d.mutex.RLock() + defer d.mutex.RUnlock() if len(d.cells) <= row { return nil @@ -59,15 +75,15 @@ func (d *LastRoundTableData) GetCell(row, column int) *tview.TableCell { } func (d *LastRoundTableData) GetRowCount() int { - d.mutex.Lock() - defer d.mutex.Unlock() + d.mutex.RLock() + defer d.mutex.RUnlock() return len(d.cells) } func (d *LastRoundTableData) GetColumnCount() int { - d.mutex.Lock() - defer d.mutex.Unlock() + d.mutex.RLock() + defer d.mutex.RUnlock() if len(d.cells) == 0 { return 0 @@ -76,34 +92,64 @@ func (d *LastRoundTableData) GetColumnCount() int { return len(d.cells[0]) } -func (d *LastRoundTableData) SetValidators(validators types.ValidatorsWithInfo, consensusError error) { - d.Validators = validators +// SetTMValidators sets the unified validator collection (preferred). +func (d *LastRoundTableData) SetTMValidators(validators types.TMValidators, consensusError error) { + d.mutex.Lock() + d.TMValidators = validators d.ConsensusError = consensusError + d.mutex.Unlock() + + d.redrawData() +} + +// SetRoundData sets the round data map for vote tracking. +func (d *LastRoundTableData) SetRoundData(roundData *types.RoundDataMap) { + d.mutex.Lock() + d.RoundData = roundData + d.mutex.Unlock() + + d.redrawData() +} + +// SetCurrentRound sets the current height and round for display. +func (d *LastRoundTableData) SetCurrentRound(height int64, round int32) { + d.mutex.Lock() + d.CurrentHeight = height + d.CurrentRound = round + d.mutex.Unlock() + d.redrawData() } func (d *LastRoundTableData) redrawData() { + cells := d.makeCells() + d.mutex.Lock() defer d.mutex.Unlock() + d.cells = cells +} +func (d *LastRoundTableData) makeCells() [][]*tview.TableCell { if d.ConsensusError != nil { - d.cells = [][]*tview.TableCell{ - { - tview.NewTableCell(fmt.Sprintf(" Error fetching consensus: %s", d.ConsensusError)), - }, + return [][]*tview.TableCell{ + {tview.NewTableCell(fmt.Sprintf(" Error fetching consensus: %s", d.ConsensusError))}, } - return + } else if d.ColumnsCount == 0 { + return [][]*tview.TableCell{} } - rowsCount := len(d.Validators)/d.ColumnsCount + 1 - if len(d.Validators)%d.ColumnsCount == 0 { - rowsCount = len(d.Validators) / d.ColumnsCount + // Use TMValidators + validatorCount := len(d.TMValidators) + + rowsCount := validatorCount/d.ColumnsCount + 1 + if validatorCount%d.ColumnsCount == 0 { + rowsCount = validatorCount / d.ColumnsCount } - d.cells = make([][]*tview.TableCell, rowsCount) + cells := make([][]*tview.TableCell, rowsCount) for row := 0; row < rowsCount; row++ { - d.cells[row] = make([]*tview.TableCell, d.ColumnsCount) + cells[row] = make([]*tview.TableCell, d.ColumnsCount) for column := 0; column < d.ColumnsCount; column++ { index := row*d.ColumnsCount + column @@ -114,18 +160,74 @@ func (d *LastRoundTableData) redrawData() { } text := "" + isProposer := false + + // Use TMValidators with RoundDataMap for vote state + if index < len(d.TMValidators) { + validator := d.TMValidators[index] + + // Check if validator is proposer for current round + if d.RoundData != nil { + proposers := d.RoundData.GetProposers(d.CurrentHeight, d.CurrentRound) + isProposer = proposers != nil && proposers.Has(validator.GetDisplayAddress()) + } - if index < len(d.Validators) { - text = d.Validators[index].Serialize(d.DisableEmojis) + // Generate display text using RoundDataMap vote data + text = d.generateValidatorDisplayText(validator) } cell := tview.NewTableCell(text) - if index < len(d.Validators) && d.Validators[index].RoundVote.IsProposer { + if isProposer { cell.SetBackgroundColor(tcell.ColorForestGreen) } - d.cells[row][column] = cell + cells[row][column] = cell + } + } + return cells +} + +// generateValidatorDisplayText creates validator display text using RoundDataMap data. +func (d *LastRoundTableData) generateValidatorDisplayText(validator types.TMValidator) string { + name := validator.GetDisplayName() + if validator.HasAssignedKey() { + emoji := "🔑" + if d.DisableEmojis { + emoji = "[k]" } + name = emoji + " " + name + } + + // Default vote states (no vote) + prevoteStr := "❌" + precommitStr := "❌" + if d.DisableEmojis { + prevoteStr = "[ ]" + precommitStr = "[ ]" + } + + // Query RoundDataMap for current vote states + if d.RoundData != nil && d.CurrentHeight > 0 { + prevoteState := d.RoundData.GetVote(d.CurrentHeight, d.CurrentRound, validator.GetDisplayAddress(), cmtproto.PrevoteType) + precommitState := d.RoundData.GetVote(d.CurrentHeight, d.CurrentRound, validator.GetDisplayAddress(), cmtproto.PrecommitType) + + prevoteStr = prevoteState.Serialize(d.DisableEmojis) + precommitStr = precommitState.Serialize(d.DisableEmojis) } + + // Format voting power percentage + votingPowerStr := "0.00" + if validator.VotingPowerPercent != nil { + votingPowerStr = validator.VotingPowerPercent.Text('f', 2) + } + + return fmt.Sprintf( + " %s %s %s %s%% %s ", + prevoteStr, + precommitStr, + utils.RightPadAndTrim(strconv.Itoa(validator.Index+1), 3), + utils.RightPadAndTrim(votingPowerStr, 6), + utils.LeftPadAndTrim(name, 25), + ) } diff --git a/pkg/display/net_info_table.go b/pkg/display/net_info_table.go new file mode 100644 index 0000000..bfcde9f --- /dev/null +++ b/pkg/display/net_info_table.go @@ -0,0 +1,147 @@ +package display + +import ( + "fmt" + "slices" + "strings" + + "main/pkg/types" + "main/pkg/utils" + + "github.com/rivo/tview" +) + +type NetInfoTableData struct { + tview.TableContentReadOnly + + NetInfo *types.NetInfo + + cells [][]*tview.TableCell + mutex *utils.NoopLocker +} + +func NewNetInfoTableData() *NetInfoTableData { + return &NetInfoTableData{ + cells: [][]*tview.TableCell{}, + mutex: &utils.NoopLocker{}, + } +} + +func (d *NetInfoTableData) GetCell(row, column int) *tview.TableCell { + d.mutex.RLock() + defer d.mutex.RUnlock() + + if len(d.cells) <= row { + return nil + } + + if len(d.cells[row]) <= column { + return nil + } + + return d.cells[row][column] +} + +func (d *NetInfoTableData) GetRowCount() int { + d.mutex.RLock() + defer d.mutex.RUnlock() + + return len(d.cells) +} + +func (d *NetInfoTableData) GetColumnCount() int { + d.mutex.RLock() + defer d.mutex.RUnlock() + + if len(d.cells) == 0 { + return 0 + } + + return len(d.cells[0]) +} + +func (d *NetInfoTableData) SetNetInfo(netInfo *types.NetInfo) { + d.NetInfo = netInfo + d.redrawData() +} + +func (d *NetInfoTableData) redrawData() { + cells := d.makeCells() + + d.mutex.Lock() + defer d.mutex.Unlock() + d.cells = cells +} + +func (d *NetInfoTableData) makeCells() [][]*tview.TableCell { + d.mutex.RLock() + defer d.mutex.RUnlock() + + if d.NetInfo == nil { + return nil + } + + cells := make([][]*tview.TableCell, len(d.NetInfo.Peers)+2) + + slices.SortFunc(d.NetInfo.Peers, func(a, b types.Peer) int { + if a.ConnectionStatus.RecvMonitor.AvgRate > b.ConnectionStatus.RecvMonitor.AvgRate { + return -1 + } + return 1 + }) + + cells[0] = []*tview.TableCell{ + tview.NewTableCell(""), + tview.NewTableCell("IP"), + tview.NewTableCell("Moniker"), + tview.NewTableCell("Duration"), + tview.NewTableCell("Send (cur)"), + tview.NewTableCell("Recv (cur)"), + tview.NewTableCell("Send (avg)"), + tview.NewTableCell("Recv (avg)"), + tview.NewTableCell("Node ID"), + tview.NewTableCell("Version"), + tview.NewTableCell("Proto"), + tview.NewTableCell("RPC"), + } + + cells[1] = []*tview.TableCell{ + tview.NewTableCell(""), + tview.NewTableCell("=="), + tview.NewTableCell("========"), + tview.NewTableCell("========"), + tview.NewTableCell("=========="), + tview.NewTableCell("=========="), + tview.NewTableCell("=========="), + tview.NewTableCell("=========="), + tview.NewTableCell("========================================"), + tview.NewTableCell("======="), + tview.NewTableCell("====="), + tview.NewTableCell("==="), + } + + for i, peer := range d.NetInfo.Peers { + cells[i+2] = make([]*tview.TableCell, 12) + + duration := strings.Split(peer.ConnectionStatus.Duration.String(), ".")[0] + + direction := "in" + if peer.IsOutbound { + direction = "out" + } + + cells[i+2][0] = tview.NewTableCell(direction) + cells[i+2][1] = tview.NewTableCell(peer.RemoteIP) + cells[i+2][2] = tview.NewTableCell(peer.NodeInfo.Moniker) + cells[i+2][3] = tview.NewTableCell(duration) + cells[i+2][4] = tview.NewTableCell(peer.ConnectionStatus.SendMonitor.CurRate.String() + "/s").SetAlign(tview.AlignRight) + cells[i+2][5] = tview.NewTableCell(peer.ConnectionStatus.RecvMonitor.CurRate.String() + "/s").SetAlign(tview.AlignRight) + cells[i+2][6] = tview.NewTableCell(peer.ConnectionStatus.SendMonitor.AvgRate.String() + "/s").SetAlign(tview.AlignRight) + cells[i+2][7] = tview.NewTableCell(peer.ConnectionStatus.RecvMonitor.AvgRate.String() + "/s").SetAlign(tview.AlignRight) + cells[i+2][8] = tview.NewTableCell(string(peer.NodeInfo.DefaultNodeID)) + cells[i+2][9] = tview.NewTableCell(peer.NodeInfo.Version) + cells[i+2][10] = tview.NewTableCell(fmt.Sprintf("%v/%v/%v", peer.NodeInfo.ProtocolVersion.P2P, peer.NodeInfo.ProtocolVersion.Block, peer.NodeInfo.ProtocolVersion.App)) + cells[i+2][11] = tview.NewTableCell(peer.NodeInfo.Other.RPCAddress) + } + return cells +} diff --git a/pkg/display/rpcs_table.go b/pkg/display/rpcs_table.go new file mode 100644 index 0000000..d23ba6b --- /dev/null +++ b/pkg/display/rpcs_table.go @@ -0,0 +1,75 @@ +package display + +import ( + "main/pkg/types" + "main/pkg/utils" + + "github.com/rivo/tview" +) + +type RPCsTableData struct { + tview.TableContentReadOnly + + knownRPCs []types.RPC + + cells [][]*tview.TableCell + mutex *utils.NoopLocker +} + +func NewRPCsTableData() *RPCsTableData { + return &RPCsTableData{ + cells: [][]*tview.TableCell{}, + mutex: &utils.NoopLocker{}, + } +} + +func (d *RPCsTableData) SetKnownRPCs(rpcs []types.RPC) { + d.mutex.Lock() + d.knownRPCs = rpcs + d.mutex.Unlock() + + d.redrawData() +} + +func (d *RPCsTableData) GetCell(row, column int) *tview.TableCell { + d.mutex.RLock() + defer d.mutex.RUnlock() + + if len(d.cells) <= row { + return nil + } + + if len(d.cells[row]) <= column { + return nil + } + + return d.cells[row][column] +} + +func (d *RPCsTableData) GetRowCount() int { + d.mutex.RLock() + defer d.mutex.RUnlock() + + return len(d.cells) +} + +func (d *RPCsTableData) GetColumnCount() int { + d.mutex.RLock() + defer d.mutex.RUnlock() + + if len(d.cells) == 0 { + return 0 + } + + return len(d.cells[0]) +} + +func (d *RPCsTableData) redrawData() { + d.mutex.Lock() + defer d.mutex.Unlock() + + d.cells = make([][]*tview.TableCell, len(d.knownRPCs)) + for i, rpc := range d.knownRPCs { + d.cells[i] = []*tview.TableCell{tview.NewTableCell(rpc.URL), tview.NewTableCell(rpc.Moniker)} + } +} diff --git a/pkg/display/wrapper.go b/pkg/display/wrapper.go index 3ddc415..edb52f3 100644 --- a/pkg/display/wrapper.go +++ b/pkg/display/wrapper.go @@ -2,11 +2,12 @@ package display import ( "fmt" + "strings" + "time" + configPkg "main/pkg/config" "main/pkg/types" "main/static" - "strings" - "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -15,14 +16,15 @@ import ( const ( ModeLastRound = iota - ModeAllRounds = iota + ModeAllRounds + ModeNetInfo ) const ( DefaultColumnsCount = 3 RowsAmount = 10 - DebugBlockHeight = 2 - DefaultMode = ModeLastRound + DebugBlockHeight = 8 + DefaultMode = ModeAllRounds ) type Wrapper struct { @@ -34,6 +36,10 @@ type Wrapper struct { LastRoundTableData *LastRoundTableData AllRoundsTable *tview.Table AllRoundsTableData *AllRoundsTableData + NetInfoTable *tview.Table + NetInfoTableData *NetInfoTableData + RPCsTable *tview.Table + RPCsTableData *RPCsTableData Grid *tview.Grid Pages *tview.Pages App *tview.Application @@ -45,12 +51,14 @@ type Wrapper struct { DebugEnabled bool + State *types.State Logger zerolog.Logger PauseChannel chan bool IsPaused bool - IsHelpDisplayed bool + IsRPCListDisplayed bool + IsHelpDisplayed bool DisableEmojis bool Transpose bool @@ -59,12 +67,15 @@ type Wrapper struct { func NewWrapper( config *configPkg.Config, + state *types.State, logger zerolog.Logger, pauseChannel chan bool, appVersion string, ) *Wrapper { lastRoundTableData := NewLastRoundTableData(DefaultColumnsCount, config.DisableEmojis, false) allRoundsTableData := NewAllRoundsTableData(config.DisableEmojis, false) + netInfoTableData := NewNetInfoTableData() + rpcsTableData := NewRPCsTableData() helpTextBytes, _ := static.TemplatesFs.ReadFile("help.txt") helpText := strings.ReplaceAll(string(helpTextBytes), "{{ Version }}", appVersion) @@ -80,6 +91,20 @@ func NewWrapper( SetContent(allRoundsTableData). SetFixed(1, 1) + netInfoTable := tview.NewTable(). + SetBorders(false). + SetSelectable(false, false). + SetFixed(2, 0). + SetEvaluateAllRows(true). + SetContent(netInfoTableData) + + rpcsTable := tview.NewTable(). + SetBorders(false). + SetSelectable(true, false). + SetEvaluateAllRows(true). + SetContent(rpcsTableData) + rpcsTable.SetTitle("RPCs").SetTitleAlign(tview.AlignCenter).SetBorder(true) + consensusInfoTextView := tview.NewTextView(). SetDynamicColors(true). SetRegions(true) @@ -108,6 +133,7 @@ func NewWrapper( app := tview.NewApplication().SetRoot(pages, true).SetFocus(lastRoundTable) return &Wrapper{ + State: state, ChainInfoTextView: chainInfoTextView, ConsensusInfoTextView: consensusInfoTextView, ProgressTextView: progressTextView, @@ -116,6 +142,10 @@ func NewWrapper( LastRoundTableData: lastRoundTableData, AllRoundsTable: allRoundsTable, AllRoundsTableData: allRoundsTableData, + NetInfoTable: netInfoTable, + NetInfoTableData: netInfoTableData, + RPCsTable: rpcsTable, + RPCsTableData: rpcsTableData, HelpModal: helpModal, Grid: grid, Pages: pages, @@ -135,9 +165,20 @@ func NewWrapper( } func (w *Wrapper) Start() { + w.RPCsTable.SetSelectedFunc(func(row, col int) { + rpc, ok := w.State.RPCAtIndex(row) + if ok { + w.State.SetCurrentRPCURL(rpc.URL) + } + // w.State.Clear() + // w.SetState(w.State) + w.ToggleRPCList() + }) + w.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Rune() == 'q' { + if event.Rune() == 'q' || event.Key() == tcell.KeyCtrlC { w.App.Stop() + return nil } if event.Rune() == 'd' { @@ -156,6 +197,10 @@ func (w *Wrapper) Start() { w.ToggleHelp() } + if event.Rune() == 'r' { + w.ToggleRPCList() + } + if event.Rune() == 'm' { w.ChangeColumnsCount(true) } @@ -185,6 +230,8 @@ func (w *Wrapper) Start() { w.Grid.SetBackgroundColor(tcell.ColorDefault) w.LastRoundTable.SetBackgroundColor(tcell.ColorDefault) w.AllRoundsTable.SetBackgroundColor(tcell.ColorDefault) + w.NetInfoTable.SetBackgroundColor(tcell.ColorDefault) + w.RPCsTable.SetBackgroundColor(tcell.ColorSteelBlue) w.ChainInfoTextView.SetBackgroundColor(tcell.ColorDefault) w.ConsensusInfoTextView.SetBackgroundColor(tcell.ColorDefault) w.ProgressTextView.SetBackgroundColor(tcell.ColorDefault) @@ -205,42 +252,59 @@ func (w *Wrapper) Start() { }) if err := w.App.Run(); err != nil { - w.Logger.Fatal().Err(err).Msg("Could not draw screen") + w.Logger.Error().Err(err).Msg("Could not draw screen") + w.cleanup() } } func (w *Wrapper) ToggleDebug() { w.DebugEnabled = !w.DebugEnabled + w.Redraw() +} +func (w *Wrapper) ToggleRPCList() { + w.IsRPCListDisplayed = !w.IsRPCListDisplayed w.Redraw() } func (w *Wrapper) ToggleHelp() { w.IsHelpDisplayed = !w.IsHelpDisplayed - w.Redraw() } func (w *Wrapper) SetState(state *types.State) { - w.LastRoundTableData.SetValidators(state.GetValidatorsWithInfo(), state.ConsensusStateError) - w.AllRoundsTableData.SetValidators(state.GetValidatorsWithInfoAndAllRoundVotes()) + w.App.QueueUpdateDraw(func() { + w.State = state + + // Use TMValidators and RoundDataMap + w.LastRoundTableData.SetTMValidators(state.GetTMValidators(), state.ConsensusStateError) + w.LastRoundTableData.SetRoundData(state.VotesByRound) + w.LastRoundTableData.SetCurrentRound(state.Height, int32(state.Round)) + if w.AllRoundsTableData != nil { + w.AllRoundsTableData.SetTMValidators(state.GetTMValidators(), state.Height) + w.AllRoundsTableData.SetRoundData(state.VotesByRound) + w.AllRoundsTableData.Update() + } + w.NetInfoTableData.SetNetInfo(state.NetInfo) + w.RPCsTableData.SetKnownRPCs(state.KnownRPCs().Values()) - w.ConsensusInfoTextView.Clear() - w.ChainInfoTextView.Clear() - w.ProgressTextView.Clear() - _, _ = fmt.Fprint(w.ConsensusInfoTextView, state.SerializeConsensus(w.Timezone)) - _, _ = fmt.Fprint(w.ChainInfoTextView, state.SerializeChainInfo(w.Timezone)) + w.ConsensusInfoTextView.Clear() + w.ChainInfoTextView.Clear() + w.ProgressTextView.Clear() + _, _ = fmt.Fprint(w.ConsensusInfoTextView, state.SerializeConsensus(w.Timezone)) + _, _ = fmt.Fprint(w.ChainInfoTextView, state.SerializeChainInfo(w.Timezone)) - _, _, width, height := w.ConsensusInfoTextView.GetInnerRect() - _, _ = fmt.Fprint(w.ProgressTextView, state.SerializePrevotesProgressbar(width, height/2)) - _, _ = fmt.Fprint(w.ProgressTextView, "\n") - _, _ = fmt.Fprint(w.ProgressTextView, state.SerializePrecommitsProgressbar(width, height/2)) + _, _, width, height := w.ConsensusInfoTextView.GetInnerRect() + _, _ = fmt.Fprint(w.ProgressTextView, state.SerializePrevotesProgressbar(width, height/2)) + _, _ = fmt.Fprint(w.ProgressTextView, "\n") + _, _ = fmt.Fprint(w.ProgressTextView, state.SerializePrecommitsProgressbar(width, height/2)) - w.App.Draw() + // w.App.Draw() + }) } func (w *Wrapper) DebugText(text string) { - _, _ = fmt.Fprint(w.DebugTextView, text) + _, _ = fmt.Fprint(w.DebugTextView, text+"\n") w.DebugTextView.ScrollToEnd() } @@ -269,11 +333,13 @@ func (w *Wrapper) ChangeColumnsCount(increase bool) { func (w *Wrapper) ChangeMode() { switch w.Mode { case ModeAllRounds: + w.Mode = ModeNetInfo + case ModeNetInfo: w.Mode = ModeLastRound case ModeLastRound: w.Mode = ModeAllRounds default: - w.Mode = ModeLastRound + w.Mode = ModeAllRounds } w.Redraw() @@ -283,6 +349,8 @@ func (w *Wrapper) Redraw() { table := w.LastRoundTable if w.Mode == ModeAllRounds { table = w.AllRoundsTable + } else if w.Mode == ModeNetInfo { + table = w.NetInfoTable } w.Grid.RemoveItem(w.ConsensusInfoTextView) @@ -292,9 +360,9 @@ func (w *Wrapper) Redraw() { w.Grid.RemoveItem(w.AllRoundsTable) w.Grid.RemoveItem(w.DebugTextView) - w.Grid.AddItem(w.ConsensusInfoTextView, 0, 0, w.InfoBlockWidth, 2, 1, 1, false) - w.Grid.AddItem(w.ChainInfoTextView, 0, 2, w.InfoBlockWidth, 2, 1, 1, false) - w.Grid.AddItem(w.ProgressTextView, 0, 4, w.InfoBlockWidth, 2, 1, 1, false) + w.Grid.AddItem(w.ConsensusInfoTextView, 0, 0, w.InfoBlockWidth, 3, 1, 1, false) + w.Grid.AddItem(w.ChainInfoTextView, 0, 3, w.InfoBlockWidth, 2, 1, 1, false) + w.Grid.AddItem(w.ProgressTextView, 0, 5, w.InfoBlockWidth, 1, 1, 1, false) if w.DebugEnabled { w.Grid.AddItem( @@ -330,11 +398,46 @@ func (w *Wrapper) Redraw() { ) } + w.App.SetFocus(table) + if w.IsHelpDisplayed { w.Pages.AddPage("modal", w.HelpModal, true, true) } else { w.Pages.RemovePage("modal") } - w.App.SetFocus(table) + if w.IsRPCListDisplayed { + _, _, pwidth, pheight := w.Pages.GetRect() + width := int(float64(pwidth) / 2.5) + height := pheight - 8 + x := (pwidth - width) / 2 + y := (pheight - height) / 2 + w.RPCsTable.SetRect(x, y, width, height) + w.Pages.AddPage("rpclist", w.RPCsTable, false, true) + w.App.SetFocus(w.RPCsTable) + } else { + w.Pages.RemovePage("rpclist") + w.App.SetFocus(table) + } +} + +// cleanup ensures proper terminal state restoration. +func (w *Wrapper) cleanup() { + if w.App != nil { + // Stop the application gracefully + w.App.Stop() + } + + // Additional terminal cleanup + w.restoreTerminal() +} + +// restoreTerminal restores terminal state. +func (w *Wrapper) restoreTerminal() { + // Send terminal reset sequences to restore state + fmt.Print("\033[?25h") // Show cursor + fmt.Print("\033[0m") // Reset colors + fmt.Print("\033[2J") // Clear screen + fmt.Print("\033[H") // Move cursor to top-left + fmt.Print("\033[?1049l") // Exit alternate screen buffer } diff --git a/pkg/fetcher/comet_rpc.go b/pkg/fetcher/comet_rpc.go new file mode 100644 index 0000000..eb04439 --- /dev/null +++ b/pkg/fetcher/comet_rpc.go @@ -0,0 +1,199 @@ +package fetcher + +import ( + "errors" + "fmt" + "strings" + "time" + + "main/pkg/config" + "main/pkg/http" + "main/pkg/types" + + cnstypes "github.com/cometbft/cometbft/consensus/types" + cmtjson "github.com/cometbft/cometbft/libs/json" + rpctypes "github.com/cometbft/cometbft/rpc/core/types" + jsonrpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" + ctypes "github.com/cometbft/cometbft/types" + "github.com/rs/zerolog" +) + +type CometRPC struct { + url string + logger zerolog.Logger + blocksBehind uint64 +} + +func NewCometRPC(config *config.Config, state *types.State, logger zerolog.Logger) *CometRPC { + return &CometRPC{ + url: state.CurrentRPC().URL, + logger: logger.With().Str("component", "comet_rpc").Logger(), + blocksBehind: config.BlocksBehind, + } +} + +func (rpc *CometRPC) WithEndpoint(url string) *CometRPC { + return &CometRPC{ + url: url, + logger: rpc.logger, + blocksBehind: rpc.blocksBehind, + } +} + +func (rpc *CometRPC) client() *http.Client { + return http.NewClient(rpc.logger, "comet_rpc", rpc.url) +} + +func (rpc *CometRPC) request(path string, target any) error { + bs, err := rpc.client().GetPlain(path) + if err != nil { + return err + } + + var response jsonrpctypes.RPCResponse + err = cmtjson.Unmarshal(bs, &response) + if err != nil { + return err + } else if response.Error != nil { + return response.Error + } + + err = cmtjson.Unmarshal(response.Result, &target) + return err +} + +func (rpc *CometRPC) GetConsensusState() (*cnstypes.RoundState, error) { + var response rpctypes.ResultConsensusState + if err := rpc.request("/consensus_state", &response); err != nil { + return nil, err + } + + var state cnstypes.RoundState + if err := cmtjson.Unmarshal(response.RoundState, &state); err != nil { + return nil, fmt.Errorf("failed to unmarshal round state: %w", err) + } + + return &state, nil +} + +func (rpc *CometRPC) GetValidators() ([]*ctypes.Validator, error) { + page := 1 + + validators := make([]*ctypes.Validator, 0) + + for { + response, err := rpc.getValidatorsAtPage(page) + if err != nil && strings.Contains(err.Error(), "could not find validator set for height") { + // on genesis, /validators is not working + return rpc.getValidatorsViaDumpConsensusState() + } else if err != nil { + return nil, err + } else if response == nil { + return nil, errors.New("malformed response from node: no response") + } + + validators = append(validators, response.Validators...) + if len(validators) >= response.Total { + break + } + page++ + } + + return validators, nil +} + +func (rpc *CometRPC) getValidatorsAtPage(page int) (*rpctypes.ResultValidators, error) { + var resp rpctypes.ResultValidators + err := rpc.request(fmt.Sprintf("/validators?page=%d&per_page=100", page), &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (rpc *CometRPC) getValidatorsViaDumpConsensusState() ([]*ctypes.Validator, error) { + var response rpctypes.ResultDumpConsensusState + err := rpc.request("/dump_consensus_state", &response) + if err != nil { + return nil, err + } + + if response.RoundState == nil { + return nil, fmt.Errorf("malformed response from /dump_consensus_state") + } + + var state cnstypes.RoundState + if err := cmtjson.Unmarshal(response.RoundState, &state); err != nil { + return nil, fmt.Errorf("failed to unmarshal round state: %w", err) + } + + return state.Validators.Validators, nil +} + +func (rpc *CometRPC) GetCometNodeStatus() (*rpctypes.ResultStatus, error) { + var resp rpctypes.ResultStatus + err := rpc.request("/status", &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (rpc *CometRPC) Block(height int64) (*rpctypes.ResultBlock, error) { + blockURL := "/block" + if height != 0 { + blockURL = fmt.Sprintf("/block?height=%d", height) + } + + var res rpctypes.ResultBlock + err := rpc.request(blockURL, &res) + return &res, err +} + +func (rpc *CometRPC) GetBlockTime() (time.Duration, error) { + latestBlock, err := rpc.Block(0) + if err != nil { + rpc.logger.Error().Err(err).Msg("Could not fetch current block") + return 0, err + } + + if latestBlock.Block == nil { + return 0, fmt.Errorf("no current block present") + } + + latestBlockHeight := latestBlock.Block.Header.Height + olderBlockHeight := latestBlockHeight - int64(rpc.blocksBehind) + if olderBlockHeight <= 0 { + olderBlockHeight = 1 + } + + blocksDiff := latestBlockHeight - olderBlockHeight + if blocksDiff <= 0 { + return 0, fmt.Errorf("cannot calculate block time with the negative blocks counter") + } + + olderBlock, err := rpc.Block(olderBlockHeight) + if err != nil { + rpc.logger.Error().Err(err).Msg("Could not fetch older block") + return 0, err + } + + if olderBlock.Block == nil { + return 0, fmt.Errorf("no older block present") + } + + blocksDiffTime := latestBlock.Block.Header.Time.Sub(olderBlock.Block.Header.Time) + blockTime := blocksDiffTime.Seconds() / float64(blocksDiff) + + duration := time.Duration(int64(blockTime * float64(time.Second))) + return duration, nil +} + +func (rpc *CometRPC) GetNetInfo() (*types.NetInfo, error) { + var result types.NetInfo + err := rpc.request("/net_info", &result) + if err != nil { + return nil, err + } + return &result, nil +} diff --git a/pkg/fetcher/comet_rpc_websocket.go b/pkg/fetcher/comet_rpc_websocket.go new file mode 100644 index 0000000..8188b6e --- /dev/null +++ b/pkg/fetcher/comet_rpc_websocket.go @@ -0,0 +1,235 @@ +package fetcher + +import ( + "net/url" + "strings" + "sync" + "time" + + butils "github.com/brynbellomy/go-utils" + cmtjson "github.com/cometbft/cometbft/libs/json" + rpctypes "github.com/cometbft/cometbft/rpc/core/types" + jsonrpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" + ctypes "github.com/cometbft/cometbft/types" + "github.com/gorilla/websocket" + "github.com/rs/zerolog" +) + +type CometRPCWebsocket struct { + url string + logger zerolog.Logger + + conn *websocket.Conn + muConn *sync.Mutex + + subIDNonce int + subs *butils.SyncMap[int, sub] + + chResetConn chan struct{} + chStop chan struct{} + wgDone *sync.WaitGroup +} + +type sub struct { + id int + events []string + mb *butils.Mailbox[ctypes.TMEventData] +} + +func NewCometRPCWebsocket(rpcURL string, logger zerolog.Logger) *CometRPCWebsocket { + cometLogger := logger.With().Str("component", "comet_rpc_websocket").Logger() + + u, err := url.Parse(rpcURL) + if err != nil { + cometLogger.Fatal().Err(err).Msg("bad url") + } + if u.Scheme == "https" { + u.Scheme = "wss" + } else if u.Scheme == "http" { + u.Scheme = "ws" + } + + url := u.String() + "/websocket" + + ws := &CometRPCWebsocket{ + url: url, + logger: cometLogger, + muConn: &sync.Mutex{}, + subIDNonce: 0, + subs: butils.NewSyncMap[int, sub](), + chResetConn: make(chan struct{}, 1), + chStop: make(chan struct{}), + wgDone: &sync.WaitGroup{}, + } + + ws.resetConnection() + + ws.wgDone.Add(1) + go ws.connectionManager() + + return ws +} + +func (ws *CometRPCWebsocket) Close() { + ws.chStop <- struct{}{} + ws.wgDone.Wait() +} + +func (ws *CometRPCWebsocket) connectionManager() { + defer ws.wgDone.Done() + defer ws.terminateConnection() + + for { + ws.readConnection() + + select { + case <-ws.chStop: + return + default: + } + + ws.resetConnection() + } +} + +func (ws *CometRPCWebsocket) readConnection() { + defer func() { + if perr := recover(); perr != nil { + ws.logger.Error().Msgf("recovered from panic: %v", perr) + } + }() + + for { + select { + case <-ws.chStop: + return + default: + } + + var resp jsonrpctypes.RPCResponse + err := ws.read(&resp) + if err != nil { + ws.logger.Error().Err(err).Msg("could not read websocket msg") + continue + } else if resp.Error != nil { + ws.logger.Error().Err(*resp.Error).Msg("rpc received error") + continue + } + + if string(resp.Result) == "{}" { + continue + } + + var event rpctypes.ResultEvent + err = cmtjson.Unmarshal(resp.Result, &event) + if err != nil { + ws.logger.Error().Err(err).Msg("could not unmarshal websocket msg") + continue + } + + subID := int(resp.ID.(jsonrpctypes.JSONRPCIntID)) + sub, ok := ws.subs.Get(subID) + if !ok { + ws.logger.Error().Msgf("received event for unknown subscription ID %d", subID) + continue + } + + sub.mb.Deliver(event.Data) + } +} + +func (ws *CometRPCWebsocket) read(resp any) error { + ws.muConn.Lock() + defer ws.muConn.Unlock() + + return ws.conn.ReadJSON(&resp) +} + +func (ws *CometRPCWebsocket) resetConnection() { + ws.muConn.Lock() + defer ws.muConn.Unlock() + + // close connection if active + if ws.conn != nil { + ws.logger.Info().Msg("websocket connection closed, reconnecting...") + err := ws.conn.Close() + if err != nil { + ws.logger.Error().Err(err).Msg("error closing websocket connection") + } else { + ws.logger.Info().Msg("websocket connection closed, reconnecting...") + } + ws.conn = nil + } + + // wait for a new connection + for { + conn, _, err := websocket.DefaultDialer.Dial(ws.url, nil) + if err != nil { + ws.logger.Error().Err(err).Msg("websocket dial failed") + select { + case <-ws.chStop: + return + case <-time.After(5 * time.Second): + } + continue + } + + ws.conn = conn + break + } + + ws.logger.Info().Str("url", ws.conn.RemoteAddr().String()).Msg("connected to comet rpc websocket") + + // resubscribe to everything + for _, sub := range ws.subs.Iter() { + ws.logger.Debug().Msgf("subscribing to subscription ID %d with events %v", sub.id, sub.events) + ws.sendSubscribeMsg(sub) + } +} + +// terminateConnection closes the websocket connection and cleans up resources permanently. +func (ws *CometRPCWebsocket) terminateConnection() { + ws.muConn.Lock() + defer ws.muConn.Unlock() + + if ws.conn != nil { + err := ws.conn.Close() + if err != nil { + ws.logger.Error().Err(err).Msg("error closing websocket connection, giving up") + } else { + ws.logger.Info().Msg("websocket connection closed") + } + ws.conn = nil + } +} + +func (ws *CometRPCWebsocket) Subscribe(mb *butils.Mailbox[ctypes.TMEventData], events ...string) { + ws.subIDNonce++ + + sub := sub{ + id: ws.subIDNonce, + events: events, + mb: mb, + } + + ws.subs.Set(sub.id, sub) + ws.sendSubscribeMsg(sub) +} + +func (ws *CometRPCWebsocket) sendSubscribeMsg(sub sub) { + subMsg := map[string]any{ + "jsonrpc": "2.0", + "method": "subscribe", + "id": sub.id, + "params": map[string]any{ + "query": "tm.event='" + strings.Join(sub.events, "' OR tm.event='") + "'", + }, + } + + ws.logger.Info().Msg("subscribing to " + strings.Join(sub.events, ",")) + + err := ws.conn.WriteJSON(subMsg) + if err != nil { + ws.logger.Error().Err(err).Msg("could not write subscription message") + } +} diff --git a/pkg/fetcher/cosmos_lcd.go b/pkg/fetcher/cosmos_lcd.go index 126962f..af50b68 100644 --- a/pkg/fetcher/cosmos_lcd.go +++ b/pkg/fetcher/cosmos_lcd.go @@ -1,64 +1,52 @@ package fetcher import ( - "fmt" - configPkg "main/pkg/config" + "main/pkg/config" "main/pkg/http" "main/pkg/types" - sdkTypes "github.com/cosmos/cosmos-sdk/types" - upgradeTypes "cosmossdk.io/x/upgrade/types" "github.com/cosmos/cosmos-sdk/codec" codecTypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/std" + sdkTypes "github.com/cosmos/cosmos-sdk/types" stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/rs/zerolog" ) -type CosmosLcdDataFetcher struct { - Config *configPkg.Config - Logger zerolog.Logger - Client *http.Client - - Registry codecTypes.InterfaceRegistry - ParseCodec *codec.ProtoCodec +type CosmosLCDDataFetcher struct { + client *http.Client + parseCodec *codec.ProtoCodec LegacyAmino *codec.LegacyAmino } -func NewCosmosLcdDataFetcher(config *configPkg.Config, logger zerolog.Logger) *CosmosLcdDataFetcher { +var _ cosmosRPCFetcher = (*CosmosLCDDataFetcher)(nil) + +func NewCosmosLCDDataFetcher(config *config.Config, logger zerolog.Logger) *CosmosLCDDataFetcher { interfaceRegistry := codecTypes.NewInterfaceRegistry() std.RegisterInterfaces(interfaceRegistry) - parseCodec := codec.NewProtoCodec(interfaceRegistry) - - return &CosmosLcdDataFetcher{ - Config: config, - Logger: logger.With().Str("component", "cosmos_lcd_data_fetcher").Logger(), - Client: http.NewClient(logger, "cosmos_lcd_data_fetcher", config.LCDHost), - Registry: interfaceRegistry, - ParseCodec: parseCodec, + + return &CosmosLCDDataFetcher{ + client: http.NewClient(logger, "cosmos_lcd_data_fetcher", config.LCDHost), + parseCodec: codec.NewProtoCodec(interfaceRegistry), } } -func (f *CosmosLcdDataFetcher) GetValidators() (*types.ChainValidators, error) { - bytes, err := f.Client.GetPlain( - "/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED&pagination.limit=1000", - ) - +func (f *CosmosLCDDataFetcher) GetValidators() (types.CosmosValidators, error) { + bytes, err := f.client.GetPlain("/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED&pagination.limit=1000") if err != nil { return nil, err } var validatorsResponse stakingTypes.QueryValidatorsResponse - - if err := f.ParseCodec.UnmarshalJSON(bytes, &validatorsResponse); err != nil { + if err := f.parseCodec.UnmarshalJSON(bytes, &validatorsResponse); err != nil { return nil, err } - validators := make(types.ChainValidators, len(validatorsResponse.Validators)) + validators := make(types.CosmosValidators, len(validatorsResponse.Validators)) - for index, validator := range validatorsResponse.Validators { - if err := validator.UnpackInterfaces(f.ParseCodec); err != nil { + for i, validator := range validatorsResponse.Validators { + if err := validator.UnpackInterfaces(f.parseCodec); err != nil { return nil, err } @@ -67,26 +55,27 @@ func (f *CosmosLcdDataFetcher) GetValidators() (*types.ChainValidators, error) { return nil, err } - validators[index] = types.ChainValidator{ - Moniker: validator.Description.Moniker, - Address: fmt.Sprintf("%X", addr), - RawAddress: sdkTypes.ConsAddress(addr).String(), + consPubKey, err := validator.ConsPubKey() + cometConsPubKey, err := validator.CmtConsPublicKey() + + validators[i] = types.CosmosValidator{ + Moniker: validator.GetMoniker(), + OperatorAddress: validator.GetOperator(), + ConsensusAddress: sdkTypes.ConsAddress(addr).String(), + ConsensusPubkey: consPubKey, + CometConsensusPubkey: cometConsPubKey, } } - return &validators, nil + return validators, nil } -func (f *CosmosLcdDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { +func (f *CosmosLCDDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { var response upgradeTypes.QueryCurrentPlanResponse - if err := f.Client.Get( - "/cosmos/upgrade/v1beta1/current_plan", - &response, - ); err != nil { + err := f.client.Get("/cosmos/upgrade/v1beta1/current_plan", &response) + if err != nil { return nil, err - } - - if response.Plan == nil { + } else if response.Plan == nil { return nil, nil } diff --git a/pkg/fetcher/cosmos_noop.go b/pkg/fetcher/cosmos_noop.go new file mode 100644 index 0000000..c5c344a --- /dev/null +++ b/pkg/fetcher/cosmos_noop.go @@ -0,0 +1,21 @@ +package fetcher + +import ( + "main/pkg/types" +) + +type noopDataFetcher struct{} + +var _ cosmosRPCFetcher = (*noopDataFetcher)(nil) + +func newNoopDataFetcher() *noopDataFetcher { + return &noopDataFetcher{} +} + +func (f *noopDataFetcher) GetValidators() (types.ChainValidators, error) { + return types.ChainValidators{}, nil +} + +func (f *noopDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { + return nil, nil +} diff --git a/pkg/fetcher/cosmos_rpc.go b/pkg/fetcher/cosmos_rpc.go index 96f43b5..df161e8 100644 --- a/pkg/fetcher/cosmos_rpc.go +++ b/pkg/fetcher/cosmos_rpc.go @@ -4,35 +4,33 @@ import ( "bytes" "encoding/json" "fmt" - configPkg "main/pkg/config" - "main/pkg/http" - "main/pkg/types" - "main/pkg/utils" "net/url" "strconv" "strings" - "github.com/cosmos/cosmos-sdk/x/auth/tx" - genutilTypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - - sdkTypes "github.com/cosmos/cosmos-sdk/types" - - "github.com/cosmos/cosmos-sdk/std" - "github.com/rs/zerolog" + "main/pkg/config" + "main/pkg/http" + "main/pkg/types" + "main/pkg/utils" upgradeTypes "cosmossdk.io/x/upgrade/types" "github.com/cosmos/cosmos-sdk/codec" codecTypes "github.com/cosmos/cosmos-sdk/codec/types" cryptoTypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/std" + sdkTypes "github.com/cosmos/cosmos-sdk/types" queryTypes "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/x/auth/tx" + genutilTypes "github.com/cosmos/cosmos-sdk/x/genutil/types" stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" providerTypes "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" + "github.com/rs/zerolog" ) type CosmosRPCDataFetcher struct { - Config *configPkg.Config + Config *config.Config Logger zerolog.Logger - Client *http.Client + State *types.State ProviderClient *http.Client Registry codecTypes.InterfaceRegistry @@ -40,33 +38,38 @@ type CosmosRPCDataFetcher struct { TxDecoder sdkTypes.TxDecoder } -func NewCosmosRPCDataFetcher(config *configPkg.Config, logger zerolog.Logger) *CosmosRPCDataFetcher { +var _ cosmosRPCFetcher = (*CosmosRPCDataFetcher)(nil) + +func newCosmosRPCDataFetcher(config *config.Config, state *types.State, logger zerolog.Logger) *CosmosRPCDataFetcher { interfaceRegistry := codecTypes.NewInterfaceRegistry() std.RegisterInterfaces(interfaceRegistry) stakingTypes.RegisterInterfaces(interfaceRegistry) // for MsgCreateValidator for gentx parsing + parseCodec := codec.NewProtoCodec(interfaceRegistry) - txDecoder := tx.NewTxConfig(parseCodec, tx.DefaultSignModes) return &CosmosRPCDataFetcher{ Config: config, + State: state, Logger: logger.With().Str("component", "cosmos_data_fetcher").Logger(), ProviderClient: http.NewClient(logger, "cosmos_data_fetcher", config.ProviderRPCHost), - Client: http.NewClient(logger, "cosmos_data_fetcher", config.RPCHost), Registry: interfaceRegistry, ParseCodec: parseCodec, - TxDecoder: txDecoder.TxJSONDecoder(), + TxDecoder: tx.NewTxConfig(parseCodec, tx.DefaultSignModes).TxJSONDecoder(), } } -func (f *CosmosRPCDataFetcher) GetProviderOrConsumerClient() *http.Client { +func (f *CosmosRPCDataFetcher) getProviderOrConsumerClient() *http.Client { if f.Config.ProviderRPCHost != "" { return f.ProviderClient } + return f.client() +} - return f.Client +func (f *CosmosRPCDataFetcher) client() *http.Client { + return http.NewClient(f.Logger, "cosmos_data_fetcher", f.State.CurrentRPC().URL) } -func (f *CosmosRPCDataFetcher) AbciQuery( +func (f *CosmosRPCDataFetcher) abciQuery( method string, message codec.ProtoMarshaler, //nolint:staticcheck output codec.ProtoMarshaler, //nolint:staticcheck @@ -100,24 +103,7 @@ func (f *CosmosRPCDataFetcher) AbciQuery( return output.Unmarshal(response.Result.Response.Value) } -func (f *CosmosRPCDataFetcher) ParseValidator(validator stakingTypes.Validator) (types.ChainValidator, error) { - if err := validator.UnpackInterfaces(f.ParseCodec); err != nil { - return types.ChainValidator{}, err - } - - addr, err := validator.GetConsAddr() - if err != nil { - return types.ChainValidator{}, err - } - - return types.ChainValidator{ - Moniker: validator.GetMoniker(), - Address: fmt.Sprintf("%X", addr), - RawAddress: sdkTypes.ConsAddress(addr).String(), - }, nil -} - -func (f *CosmosRPCDataFetcher) GetValidators() (*types.ChainValidators, error) { +func (f *CosmosRPCDataFetcher) GetValidators() (types.CosmosValidators, error) { query := stakingTypes.QueryValidatorsRequest{ Pagination: &queryTypes.PageRequest{ Limit: 1000, @@ -125,48 +111,45 @@ func (f *CosmosRPCDataFetcher) GetValidators() (*types.ChainValidators, error) { } var validatorsResponse stakingTypes.QueryValidatorsResponse - if err := f.AbciQuery( + if err := f.abciQuery( "/cosmos.staking.v1beta1.Query/Validators", &query, &validatorsResponse, - f.GetProviderOrConsumerClient(), + f.getProviderOrConsumerClient(), ); err != nil { if strings.Contains(err.Error(), " please wait for first block") { - return f.GetGenesisValidators() + return f.getGenesisValidators() } return nil, err } - validators := make(types.ChainValidators, len(validatorsResponse.Validators)) + validators := make(types.CosmosValidators, len(validatorsResponse.Validators)) - for index, validator := range validatorsResponse.Validators { - if chainValidator, err := f.ParseValidator(validator); err != nil { + for i, validator := range validatorsResponse.Validators { + v, err := f.parseValidator(validator) + if err != nil { return nil, err - } else { - validators[index] = chainValidator } + validators[i] = v } if !f.Config.IsConsumer() { - return &validators, nil - } - - assignedKeysQuery := providerTypes.QueryAllPairsValConsAddrByConsumerRequest{ - ConsumerId: f.Config.ConsumerID, + return validators, nil } var assignedKeysResponse providerTypes.QueryAllPairsValConsAddrByConsumerResponse - if err := f.AbciQuery( + err := f.abciQuery( "/interchain_security.ccv.provider.v1.Query/QueryAllPairsValConsAddrByConsumer", - &assignedKeysQuery, + &providerTypes.QueryAllPairsValConsAddrByConsumerRequest{ConsumerId: f.Config.ConsumerID}, &assignedKeysResponse, f.ProviderClient, - ); err != nil { + ) + if err != nil { return nil, err } - for index, validator := range validators { - assignedConsensusAddr, ok := utils.Find( + for i, validator := range validators { + assignedConsensusAddr, found := utils.Find( assignedKeysResponse.PairValConAddr, func(i *providerTypes.PairValConAddrProviderAndConsumer) bool { equal, compareErr := utils.CompareTwoBech32(i.ProviderAddress, validator.RawAddress) @@ -183,18 +166,18 @@ func (f *CosmosRPCDataFetcher) GetValidators() (*types.ChainValidators, error) { }, ) - if ok { + if found { addr, _ := sdkTypes.ConsAddressFromBech32(assignedConsensusAddr.ConsumerAddress) - validators[index].AssignedAddress = addr.String() - validators[index].RawAssignedAddress = fmt.Sprintf("%X", addr) + validators[i].AssignedAddress = addr.String() + validators[i].RawAssignedAddress = fmt.Sprintf("%X", addr) } } - return &validators, nil + return validators, nil } -func (f *CosmosRPCDataFetcher) GetGenesisValidators() (*types.ChainValidators, error) { +func (f *CosmosRPCDataFetcher) getGenesisValidators() (types.CosmosValidators, error) { f.Logger.Info().Msg("Fetching genesis validators...") genesisChunks := make([][]byte, 0) @@ -202,7 +185,7 @@ func (f *CosmosRPCDataFetcher) GetGenesisValidators() (*types.ChainValidators, e for { f.Logger.Info().Int64("chunk", chunk).Msg("Fetching genesis chunk...") - genesisChunk, total, err := f.GetGenesisChunk(chunk) + genesisChunk, total, err := f.getGenesisChunk(chunk) f.Logger.Info().Int64("chunk", chunk).Int64("total", total).Msg("Fetched genesis chunk...") if err != nil { return nil, err @@ -228,7 +211,8 @@ func (f *CosmosRPCDataFetcher) GetGenesisValidators() (*types.ChainValidators, e } var stakingGenesisState stakingTypes.GenesisState - if err := f.ParseCodec.UnmarshalJSON(genesisStruct.AppState.Staking, &stakingGenesisState); err != nil { + err := f.ParseCodec.UnmarshalJSON(genesisStruct.AppState.Staking, &stakingGenesisState) + if err != nil { f.Logger.Error().Err(err).Msg("Error unmarshalling staking genesis state") return nil, err } @@ -238,16 +222,16 @@ func (f *CosmosRPCDataFetcher) GetGenesisValidators() (*types.ChainValidators, e // 1. Trying to fetch validators from staking module. Works for chain which did not start // from the first block but had their genesis as an export from older chain. if len(stakingGenesisState.Validators) > 0 { - validators := make(types.ChainValidators, len(stakingGenesisState.Validators)) + validators := make(types.CosmosValidators, len(stakingGenesisState.Validators)) for index, validator := range stakingGenesisState.Validators { - if chainValidator, err := f.ParseValidator(validator); err != nil { + if chainValidator, err := f.parseValidator(validator); err != nil { return nil, err } else { validators[index] = chainValidator } } - return &validators, nil + return validators, nil } // 2. If there's 0 validators in staking module, then we parse genutil module @@ -258,7 +242,7 @@ func (f *CosmosRPCDataFetcher) GetGenesisValidators() (*types.ChainValidators, e return nil, err } - validators := make(types.ChainValidators, len(genutilGenesisState.GenTxs)) + validators := make(types.CosmosValidators, len(genutilGenesisState.GenTxs)) for index, gentx := range genutilGenesisState.GenTxs { decodedTx, err := f.TxDecoder(gentx) if err != nil { @@ -288,22 +272,20 @@ func (f *CosmosRPCDataFetcher) GetGenesisValidators() (*types.ChainValidators, e addr := sdkTypes.ConsAddress(pubkey.Address()) - validators[index] = types.ChainValidator{ + validators[index] = types.CosmosValidator{ Moniker: msgCreateValidator.Description.Moniker, - Address: fmt.Sprintf("%X", addr), + Address: msgCreateValidator.ValidatorAddress, // Bech32 operator address RawAddress: addr.String(), } } - return &validators, nil + return validators, nil } -func (f *CosmosRPCDataFetcher) GetGenesisChunk(chunk int64) ([]byte, int64, error) { - var response types.TendermintGenesisChunkResponse - if err := f.Client.Get( - fmt.Sprintf("/genesis_chunked?chunk=%d", chunk), - &response, - ); err != nil { +func (f *CosmosRPCDataFetcher) getGenesisChunk(chunk int64) ([]byte, int64, error) { + var response types.CometGenesisChunkResponse + err := f.client().Get(fmt.Sprintf("/genesis_chunked?chunk=%d", chunk), &response) + if err != nil { return nil, 0, err } @@ -319,20 +301,39 @@ func (f *CosmosRPCDataFetcher) GetGenesisChunk(chunk int64) ([]byte, int64, erro return response.Result.Data, total, nil } -func (f *CosmosRPCDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { - query := upgradeTypes.QueryCurrentPlanRequest{} +func (f *CosmosRPCDataFetcher) parseValidator(validator stakingTypes.Validator) (types.CosmosValidator, error) { + if err := validator.UnpackInterfaces(f.ParseCodec); err != nil { + return types.CosmosValidator{}, err + } + consAddr, err := validator.GetConsAddr() + if err != nil { + return types.CosmosValidator{}, err + } + + consPubKey, err := validator.ConsPubKey() + cometConsPubKey, err := validator.CmtConsPublicKey() + + return types.CosmosValidator{ + Moniker: validator.GetMoniker(), + OperatorAddress: validator.GetOperator(), + ConsensusAddress: sdkTypes.ConsAddress(consAddr).String(), + ConsensusPubkey: consPubKey, + CometConsensusPubkey: cometConsPubKey, + }, nil +} + +func (f *CosmosRPCDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { var response upgradeTypes.QueryCurrentPlanResponse - if err := f.AbciQuery( + err := f.abciQuery( "/cosmos.upgrade.v1beta1.Query/CurrentPlan", - &query, + &upgradeTypes.QueryCurrentPlanRequest{}, &response, - f.Client, - ); err != nil { + f.client(), + ) + if err != nil { return nil, err - } - - if response.Plan == nil { + } else if response.Plan == nil { return nil, nil } diff --git a/pkg/fetcher/data_fetcher.go b/pkg/fetcher/data_fetcher.go index 31661a1..5c17185 100644 --- a/pkg/fetcher/data_fetcher.go +++ b/pkg/fetcher/data_fetcher.go @@ -1,25 +1,109 @@ package fetcher import ( + "math/big" + "time" + configPkg "main/pkg/config" "main/pkg/types" + butils "github.com/brynbellomy/go-utils" + cnstypes "github.com/cometbft/cometbft/consensus/types" + rpctypes "github.com/cometbft/cometbft/rpc/core/types" + ctypes "github.com/cometbft/cometbft/types" "github.com/rs/zerolog" ) -type DataFetcher interface { - GetValidators() (*types.ChainValidators, error) +type DataFetcher struct { + logger zerolog.Logger + cosmosFetcher cosmosRPCFetcher + cometFetcher *CometRPC + cometWS *CometRPCWebsocket +} + +type cosmosRPCFetcher interface { + GetValidators() (types.CosmosValidators, error) GetUpgradePlan() (*types.Upgrade, error) } -func GetDataFetcher(config *configPkg.Config, logger zerolog.Logger) DataFetcher { +func NewDataFetcher(config *configPkg.Config, state *types.State, logger zerolog.Logger) *DataFetcher { + var cosmosFetcher cosmosRPCFetcher if config.ChainType == "tendermint" { - return NewNoopDataFetcher() + cosmosFetcher = newNoopDataFetcher() + } else if config.ChainType == "cosmos-lcd" { + cosmosFetcher = NewCosmosLCDDataFetcher(config, logger) + } else { + cosmosFetcher = newCosmosRPCDataFetcher(config, state, logger) } - if config.ChainType == "cosmos-lcd" { - return NewCosmosLcdDataFetcher(config, logger) + return &DataFetcher{ + logger: logger, + cosmosFetcher: cosmosFetcher, + cometFetcher: NewCometRPC(config, state, logger), + cometWS: NewCometRPCWebsocket(config.RPCHost, logger), } +} + +func (f *DataFetcher) GetConsensusState() (*cnstypes.RoundState, error) { + return f.cometFetcher.GetConsensusState() +} + +func (f *DataFetcher) GetCometNodeStatus(rpcURL string) (*rpctypes.ResultStatus, error) { + return f.cometFetcher.WithEndpoint(rpcURL).GetCometNodeStatus() +} + +func (f *DataFetcher) Block(height int64) (*rpctypes.ResultBlock, error) { + return f.cometFetcher.Block(height) +} + +func (f *DataFetcher) GetBlockTime() (time.Duration, error) { + return f.cometFetcher.GetBlockTime() +} + +func (f *DataFetcher) GetNetInfo(rpcURL string) (*types.NetInfo, error) { + return f.cometFetcher.WithEndpoint(rpcURL).GetNetInfo() +} - return NewCosmosRPCDataFetcher(config, logger) +func (f *DataFetcher) GetUpgradePlan() (*types.Upgrade, error) { + return f.cosmosFetcher.GetUpgradePlan() +} + +func (f *DataFetcher) Subscribe(mb *butils.Mailbox[ctypes.TMEventData], events ...string) { + f.cometWS.Subscribe(mb, events...) +} + +func (f *DataFetcher) GetValidators() ([]types.TMValidator, error) { + cometVals, err := f.cometFetcher.GetValidators() + if err != nil { + return nil, err + } + + cosmosVals, err := f.cosmosFetcher.GetValidators() + if err != nil { + return nil, err + } + + // Calculate total voting power first + totalVotingPower := int64(0) + for _, validator := range cometVals { + totalVotingPower += validator.VotingPower + } + + cosmValMap := make(map[string]types.CosmosValidator, len(cosmosVals)) + for _, cosmosVal := range cosmosVals { + cosmValMap[cosmosVal.ConsensusPubkey.Address().String()] = cosmosVal + } + + var vals []types.TMValidator + for i, cometVal := range cometVals { + // Find matching Cosmos validator by consensus address + cosmosVal := cosmValMap[cometVal.PubKey.Address().String()] + vals = append(vals, types.TMValidator{ + CometValidator: cometVal, + CosmosValidator: &cosmosVal, + Index: i, + VotingPowerPercent: big.NewFloat(float64(cometVal.VotingPower / totalVotingPower)), + }) + } + return vals, nil } diff --git a/pkg/fetcher/noop.go b/pkg/fetcher/noop.go deleted file mode 100644 index 8cde2e7..0000000 --- a/pkg/fetcher/noop.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetcher - -import ( - "main/pkg/types" -) - -type NoopDataFetcher struct { -} - -func NewNoopDataFetcher() *NoopDataFetcher { - return &NoopDataFetcher{} -} - -func (f *NoopDataFetcher) GetValidators() (*types.ChainValidators, error) { - return &types.ChainValidators{}, nil -} - -func (f *NoopDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { - return nil, nil -} diff --git a/pkg/http/http.go b/pkg/http/http.go index 0287dee..1cfc009 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -2,9 +2,10 @@ package http import ( "encoding/json" - "fmt" "io" "net/http" + "net/url" + "path" "time" "github.com/rs/zerolog" @@ -25,6 +26,14 @@ func NewClient(logger zerolog.Logger, invoker, host string) *Client { } } +func (c *Client) join(host, rest string) string { + base, _ := url.Parse(host) + ref, _ := url.Parse(rest) + + base.Path = path.Join(base.Path, ref.Path) + return base.String() +} + func (c *Client) GetInternal(relativeURL string) (io.ReadCloser, error) { var transport http.RoundTripper @@ -38,7 +47,7 @@ func (c *Client) GetInternal(relativeURL string) (io.ReadCloser, error) { client := &http.Client{Timeout: 300 * time.Second, Transport: transport} start := time.Now() - fullURL := fmt.Sprintf("%s%s", c.Host, relativeURL) + fullURL := c.join(c.Host, relativeURL) req, err := http.NewRequest(http.MethodGet, fullURL, nil) if err != nil { @@ -60,7 +69,7 @@ func (c *Client) GetInternal(relativeURL string) (io.ReadCloser, error) { return res.Body, nil } -func (c *Client) Get(relativeURL string, target interface{}) error { +func (c *Client) Get(relativeURL string, target any) error { body, err := c.GetInternal(relativeURL) if err != nil { return err @@ -75,7 +84,6 @@ func (c *Client) Get(relativeURL string, target interface{}) error { func (c *Client) GetPlain(relativeURL string) ([]byte, error) { body, err := c.GetInternal(relativeURL) - if err != nil { return nil, err } diff --git a/pkg/http/server.go b/pkg/http/server.go new file mode 100644 index 0000000..ccbfe35 --- /dev/null +++ b/pkg/http/server.go @@ -0,0 +1,61 @@ +package http + +import ( + "net/http" + "time" + + "github.com/gorilla/mux" +) + +type Server struct { + server *http.Server + router *mux.Router +} + +func NewServer(addr string, opts ...Option) *Server { + router := mux.NewRouter() + s := &Server{ + server: &http.Server{ + Addr: addr, + Handler: router, + ReadHeaderTimeout: 5 * time.Second, + }, + router: router, + } + + WithOptions(opts...)(s) + + return s +} + +type Option func(*Server) + +func WithOptions(opts ...Option) Option { + return func(s *Server) { + for _, opt := range opts { + opt(s) + } + } +} + +func WithRouterOption(opt func(*mux.Router)) Option { + return func(s *Server) { + opt(s.router) + } +} + +func WithServerOption(opt func(*http.Server)) Option { + return func(s *Server) { + opt(s.server) + } +} + +func WithRoute(method, path string, handler http.Handler) Option { + return WithRouterOption(func(r *mux.Router) { + r.Methods(method).Path(path).Handler(handler) + }) +} + +func (s *Server) Serve() error { + return s.server.ListenAndServe() +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 1ee87f8..9848d35 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -2,9 +2,10 @@ package logger import ( "io" - configPkg "main/pkg/config" "os" + configPkg "main/pkg/config" + "github.com/rs/zerolog" ) @@ -25,7 +26,7 @@ func NewWriter(logChannel chan string, config *configPkg.Config) Writer { } if config.DebugFile != "" { - debugFile, err := os.OpenFile(config.DebugFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755) + debugFile, err := os.OpenFile(config.DebugFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755) if err != nil { panic(err) } @@ -41,7 +42,11 @@ func (w Writer) Write(msg []byte) (int, error) { if w.DebugFile != nil { if _, err := w.DebugFile.Write(msg); err != nil { - return 0, err + panic(err) + } + err := w.DebugFile.Sync() + if err != nil { + panic(err) } } diff --git a/pkg/tendermint/tendermint.go b/pkg/tendermint/tendermint.go deleted file mode 100644 index d58305d..0000000 --- a/pkg/tendermint/tendermint.go +++ /dev/null @@ -1,172 +0,0 @@ -package tendermint - -import ( - "errors" - "fmt" - configPkg "main/pkg/config" - "main/pkg/http" - "strconv" - "strings" - "time" - - "main/pkg/types" - - "github.com/rs/zerolog" -) - -type RPC struct { - Config *configPkg.Config - Logger zerolog.Logger - Client *http.Client - LogChannel chan string -} - -func NewRPC(config *configPkg.Config, logger zerolog.Logger) *RPC { - return &RPC{ - Config: config, - Logger: logger.With().Str("component", "tendermint_rpc").Logger(), - Client: http.NewClient(logger, "tendermint_rpc", config.RPCHost), - } -} - -func (rpc *RPC) GetConsensusState() (*types.ConsensusStateResponse, error) { - var response types.ConsensusStateResponse - if err := rpc.Client.Get("/consensus_state", &response); err != nil { - return nil, err - } - - return &response, nil -} - -func (rpc *RPC) GetValidators() ([]types.TendermintValidator, error) { - page := 1 - - validators := make([]types.TendermintValidator, 0) - - for { - response, err := rpc.GetValidatorsAtPage(page) - if err != nil { - return nil, err - } - - if response == nil { - return nil, errors.New("malformed response from node: no response") - } - - if response.Error != nil { - // on genesis, /validators is not working - if strings.Contains(response.Error.Data, "could not find validator set for height") { - return rpc.GetValidatorsViaDumpConsensusState() - } - - return nil, fmt.Errorf("malformed response from node: %s: %s", response.Error.Message, response.Error.Data) - } - - if response.Result == nil || response.Result.Total == "" { - return nil, errors.New("malformed response from node") - } - - total, err := strconv.ParseInt(response.Result.Total, 10, 64) - if err != nil { - return nil, err - } - - validators = append(validators, response.Result.Validators...) - if int64(len(validators)) >= total { - break - } - - page++ - } - - return validators, nil -} - -func (rpc *RPC) GetValidatorsViaDumpConsensusState() ([]types.TendermintValidator, error) { - var response types.DumpConsensusStateResponse - if err := rpc.Client.Get("/dump_consensus_state", &response); err != nil { - return nil, err - } - - if response.Result == nil || - response.Result.RoundState == nil || - len(response.Result.RoundState.Validators.Validators) == 0 { - return nil, fmt.Errorf("malformed response from /dump_consensus_state") - } - - return response.Result.RoundState.Validators.Validators, nil -} - -func (rpc *RPC) GetStatus() (*types.TendermintStatusResponse, error) { - var response types.TendermintStatusResponse - if err := rpc.Client.Get("/status", &response); err != nil { - return nil, err - } - - return &response, nil -} - -func (rpc *RPC) GetValidatorsAtPage(page int) (*types.ValidatorsResponse, error) { - var response types.ValidatorsResponse - if err := rpc.Client.Get(fmt.Sprintf("/validators?page=%d&per_page=100", page), &response); err != nil { - return nil, err - } - - return &response, nil -} - -func (rpc *RPC) Block(height int64) (types.TendermintBlockResponse, error) { - blockURL := "/block" - if height != 0 { - blockURL = fmt.Sprintf("/block?height=%d", height) - } - - res := types.TendermintBlockResponse{} - err := rpc.Client.Get(blockURL, &res) - return res, err -} - -func (rpc *RPC) GetBlockTime() (time.Duration, error) { - latestBlock, err := rpc.Block(0) - if err != nil { - rpc.Logger.Error().Err(err).Msg("Could not fetch current block") - return 0, err - } - - if latestBlock.Result.Block == nil { - return 0, fmt.Errorf("no current block present") - } - - latestBlockHeight, err := strconv.ParseInt(latestBlock.Result.Block.Header.Height, 10, 64) - if err != nil { - rpc.Logger.Error(). - Err(err). - Msg("Error converting latest block height to int64, which should never happen.") - return 0, err - } - olderBlockHeight := latestBlockHeight - int64(rpc.Config.BlocksBehind) - if olderBlockHeight <= 0 { - olderBlockHeight = 1 - } - - blocksDiff := latestBlockHeight - olderBlockHeight - if blocksDiff <= 0 { - return 0, fmt.Errorf("cannot calculate block time with the negative blocks counter") - } - - olderBlock, err := rpc.Block(olderBlockHeight) - if err != nil { - rpc.Logger.Error().Err(err).Msg("Could not fetch older block") - return 0, err - } - - if olderBlock.Result.Block == nil { - return 0, fmt.Errorf("no older block present") - } - - blocksDiffTime := latestBlock.Result.Block.Header.Time.Sub(olderBlock.Result.Block.Header.Time) - blockTime := blocksDiffTime.Seconds() / float64(blocksDiff) - - duration := time.Duration(int64(blockTime * float64(time.Second))) - return duration, nil -} diff --git a/pkg/topology/embed/embed.go b/pkg/topology/embed/embed.go new file mode 100644 index 0000000..21e0b94 --- /dev/null +++ b/pkg/topology/embed/embed.go @@ -0,0 +1,8 @@ +package embed + +import ( + "embed" +) + +//go:embed frontend/dist +var Frontend embed.FS diff --git a/pkg/topology/embed/frontend/index.html b/pkg/topology/embed/frontend/index.html new file mode 100644 index 0000000..5b572ca --- /dev/null +++ b/pkg/topology/embed/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + Vite + React + + + +
+ + + diff --git a/pkg/topology/embed/frontend/package.json b/pkg/topology/embed/frontend/package.json new file mode 100644 index 0000000..df0b141 --- /dev/null +++ b/pkg/topology/embed/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "tmtop-topology", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "graphology": "^0.25.4", + "graphology-layout": "^0.6.1", + "graphology-layout-forceatlas2": "^0.10.1", + "graphviz-react": "^1.2.5", + "query-string": "^9.1.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-graph-vis": "^1.0.7", + "reagraph": "^4.21.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^9.13.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "vite": "^5.4.10" + } +} diff --git a/pkg/topology/embed/frontend/pnpm-lock.yaml b/pkg/topology/embed/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..127d501 --- /dev/null +++ b/pkg/topology/embed/frontend/pnpm-lock.yaml @@ -0,0 +1,3690 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + graphology: + specifier: ^0.25.4 + version: 0.25.4(graphology-types@0.24.8) + graphology-layout: + specifier: ^0.6.1 + version: 0.6.1(graphology-types@0.24.8) + graphology-layout-forceatlas2: + specifier: ^0.10.1 + version: 0.10.1(graphology-types@0.24.8) + graphviz-react: + specifier: ^1.2.5 + version: 1.2.5(react@18.3.1) + query-string: + specifier: ^9.1.1 + version: 9.1.1 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-graph-vis: + specifier: ^1.0.7 + version: 1.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(react@18.3.1)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)) + reagraph: + specifier: ^4.21.0 + version: 4.21.0(@types/three@0.170.0)(graphology-types@0.24.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@eslint/js': + specifier: ^9.13.0 + version: 9.15.0 + '@types/react': + specifier: ^18.3.12 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.1 + '@vitejs/plugin-react': + specifier: ^4.3.3 + version: 4.3.3(vite@5.4.11) + eslint: + specifier: ^9.13.0 + version: 9.15.0 + eslint-plugin-react: + specifier: ^7.37.2 + version: 7.37.2(eslint@9.15.0) + eslint-plugin-react-hooks: + specifier: ^5.0.0 + version: 5.0.0(eslint@9.15.0) + eslint-plugin-react-refresh: + specifier: ^0.4.14 + version: 0.4.14(eslint@9.15.0) + globals: + specifier: ^15.11.0 + version: 15.12.0 + vite: + specifier: ^5.4.10 + version: 5.4.11 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@egjs/hammerjs@2.0.17': + resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} + engines: {node: '>=0.8.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.19.0': + resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.0': + resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.15.0': + resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.3': + resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@mediapipe/tasks-vision@0.10.8': + resolution: {integrity: sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==} + + '@react-spring/animated@9.6.1': + resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.6.1': + resolution: {integrity: sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.6.1': + resolution: {integrity: sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==} + + '@react-spring/shared@9.6.1': + resolution: {integrity: sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/three@9.6.1': + resolution: {integrity: sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==} + peerDependencies: + '@react-three/fiber': '>=6.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + three: '>=0.126' + + '@react-spring/types@9.6.1': + resolution: {integrity: sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==} + + '@react-three/fiber@8.13.5': + resolution: {integrity: sha512-x9QdsaB/Wm/6NGvRXQahPPWfn2dQce7Fg3C2r00NNzyDdqRKw32YavL+WEqjZOOd0nvFpzv7FtaKc+VCOTR59w==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-gl: '>=11.0' + react: '>=18.0' + react-dom: '>=18.0' + react-native: '>=0.64' + three: '>=0.133' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + + '@rollup/rollup-android-arm-eabi@4.27.3': + resolution: {integrity: sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.27.3': + resolution: {integrity: sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.27.3': + resolution: {integrity: sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.27.3': + resolution: {integrity: sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.27.3': + resolution: {integrity: sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.27.3': + resolution: {integrity: sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + resolution: {integrity: sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.27.3': + resolution: {integrity: sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.27.3': + resolution: {integrity: sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.27.3': + resolution: {integrity: sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + resolution: {integrity: sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.27.3': + resolution: {integrity: sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.27.3': + resolution: {integrity: sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.27.3': + resolution: {integrity: sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.27.3': + resolution: {integrity: sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.27.3': + resolution: {integrity: sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.27.3': + resolution: {integrity: sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.27.3': + resolution: {integrity: sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==} + cpu: [x64] + os: [win32] + + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + + '@types/prop-types@15.7.13': + resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} + + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + + '@types/react-reconciler@0.26.7': + resolution: {integrity: sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==} + + '@types/react-reconciler@0.28.8': + resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} + + '@types/react@18.3.12': + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + + '@types/stats.js@0.17.3': + resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==} + + '@types/three@0.170.0': + resolution: {integrity: sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg==} + + '@types/webxr@0.5.20': + resolution: {integrity: sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==} + + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + + '@vitejs/plugin-react@4.3.3': + resolution: {integrity: sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + + '@webgpu/types@0.1.51': + resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==} + + '@yomguithereal/helpers@1.1.1': + resolution: {integrity: sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camera-controls@2.9.0: + resolution: {integrity: sha512-TpCujnP0vqPppTXXJRYpvIy0xq9Tro6jQf2iYUxlDpPCNxkvE/XGaTuwIxnhINOkVP/ob2CRYXtY3iVYXeMEzA==} + peerDependencies: + three: '>=0.126.1' + + caniuse-lite@1.0.30001683: + resolution: {integrity: sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + ctrl-keys@1.0.3: + resolution: {integrity: sha512-Kcb05/xUNra57fxpsLOflECWYbjQEQ9ZuQEthB3cgESN5zMLJ364twA9h2kqz8n06RnTY/+rKWM3UbkOWKeEJg==} + engines: {node: '>=10'} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-binarytree@1.0.2: + resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} + + d3-color@1.4.1: + resolution: {integrity: sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==} + + d3-dispatch@1.0.6: + resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} + + d3-drag@1.2.5: + resolution: {integrity: sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==} + + d3-ease@1.0.7: + resolution: {integrity: sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==} + + d3-force-3d@3.0.5: + resolution: {integrity: sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==} + engines: {node: '>=12'} + + d3-format@1.4.5: + resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} + + d3-graphviz@2.6.1: + resolution: {integrity: sha512-878AFSagQyr5tTOrM7YiVYeUC2/NoFcOB3/oew+LAML0xekyJSw9j3WOCUMBsc95KYe9XBYZ+SKKuObVya1tJQ==} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@1.4.0: + resolution: {integrity: sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==} + + d3-octree@1.0.2: + resolution: {integrity: sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@1.4.2: + resolution: {integrity: sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@1.0.10: + resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} + + d3-transition@1.3.2: + resolution: {integrity: sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==} + + d3-zoom@1.8.3: + resolution: {integrity: sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-gpu@5.0.58: + resolution: {integrity: sha512-LvGBf1NeLMEQhUAHkL+tQPVUxFLU4NWiPWEj35GB9GozOeIc+o9kCj5Zg/sSc795J5TpDty5DVc51y13RI5zkg==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + + electron-to-chromium@1.5.64: + resolution: {integrity: sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==} + + ellipsize@0.5.1: + resolution: {integrity: sha512-0jEAyuIRU6U8MN0S5yUqIrkK/AQWkChh642N3zQuGV57s9bsUWYLc0jJOoDIUkZ2sbEL3ySq8xfq71BvG4q3hw==} + + es-abstract@1.23.5: + resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.0: + resolution: {integrity: sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@5.0.0: + resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.14: + resolution: {integrity: sha512-aXvzCTK7ZBv1e7fahFuR3Z/fyQQSIQ711yPgYRj+Oj64tyTgO4iQIDmYXDBqvSWQ/FA4OSCsXOStlF+noU0/NA==} + peerDependencies: + eslint: '>=7' + + eslint-plugin-react@7.37.2: + resolution: {integrity: sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.15.0: + resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + glodrei@0.0.1: + resolution: {integrity: sha512-DMx6ElCSwh1pR4IyDS3LvyFwZHSCCKCqdqo8P1G7klQtqH6PcOjleduCDsHehDtyYQ1E4dzVeoEzHIL1DIxjag==} + peerDependencies: + '@react-three/fiber': '>=8.0' + react: '>=18.0' + react-dom: '>=18.0' + three: '>=0.137' + peerDependenciesMeta: + react-dom: + optional: true + + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graphology-indices@0.17.0: + resolution: {integrity: sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==} + peerDependencies: + graphology-types: '>=0.20.0' + + graphology-layout-forceatlas2@0.10.1: + resolution: {integrity: sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==} + peerDependencies: + graphology-types: '>=0.19.0' + + graphology-layout-noverlap@0.4.2: + resolution: {integrity: sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==} + peerDependencies: + graphology-types: '>=0.19.0' + + graphology-layout@0.6.1: + resolution: {integrity: sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==} + peerDependencies: + graphology-types: '>=0.19.0' + + graphology-metrics@2.3.1: + resolution: {integrity: sha512-131GRSKUR8DrGkLZSYKM3cwxEg+jqXvv1yLh/KgO0My7BOiuo80r0Qrsnv2N3ZjcOlh8namUS4sSk+cCVnTgKA==} + peerDependencies: + graphology-types: '>=0.20.0' + + graphology-shortest-path@2.1.0: + resolution: {integrity: sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==} + peerDependencies: + graphology-types: '>=0.20.0' + + graphology-types@0.24.8: + resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==} + + graphology-utils@2.5.2: + resolution: {integrity: sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==} + peerDependencies: + graphology-types: '>=0.23.0' + + graphology@0.25.4: + resolution: {integrity: sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==} + peerDependencies: + graphology-types: '>=0.24.0' + + graphviz-react@1.2.5: + resolution: {integrity: sha512-IRFDzEt09hRzfqrrvAW1PAPBqG4t8hykArcoxq7UhEqO5RUKCBN6126D6rjiL2QAwAznbUSg0Fba2RnSH2V4sA==} + engines: {npm: '>= 8.3'} + peerDependencies: + react: '>= 16.13.1' + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hold-event@0.2.0: + resolution: {integrity: sha512-rko5P1XgHzy4B0NR0xVHEpWPgj0i23f8Mf8qsOugd1CHvfLR0PyIyy+8TAQQA9v8qAa1OZ4XuCKk04rxmPGHNQ==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.3: + resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} + engines: {node: '>= 0.4'} + + its-fine@1.2.5: + resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} + peerDependencies: + react: '>=18.0' + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keycharm@0.4.0: + resolution: {integrity: sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + mnemonist@0.39.8: + resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + pandemonium@2.4.1: + resolution: {integrity: sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + query-string@9.1.1: + resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==} + engines: {node: '>=18'} + + react-composer@5.0.3: + resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-graph-vis@1.0.7: + resolution: {integrity: sha512-FI35zlBMKU22JEvG1ukd1DDwW185y4YrDvHm6Bom9EGdA+UNMrZrIV/lyPIRWPcRkzbKaA1w1NvOYcRApD4KdQ==} + peerDependencies: + react: '*' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-merge-refs@1.1.0: + resolution: {integrity: sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==} + + react-reconciler@0.27.0: + resolution: {integrity: sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.0.0 + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react-use-measure@2.1.1: + resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + reagraph@4.21.0: + resolution: {integrity: sha512-Hls0BMRY/TznYD7VYyuSM/XJFUoXBYhwbrB3SJCP5cji5GaxKWaDAseBbfiVfSK0hAQLekFW+2gJYQys6kNYiA==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + reakeys@2.0.3: + resolution: {integrity: sha512-5qeGH9xtvFITi+9AyPeTmPhzjDTEBRZICxAg6RJFuEgWFKMHqr6mnMIaL9fgOKJMBzLWCBorpUhyiB824f0EyA==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + reflect.getprototypeof@1.0.6: + resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} + engines: {node: '>= 0.4'} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + rollup@4.27.3: + resolution: {integrity: sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + scheduler@0.21.0: + resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + + string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + + three-mesh-bvh@0.7.6: + resolution: {integrity: sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==} + peerDependencies: + three: '>= 0.151.0' + + three-stdlib@2.34.0: + resolution: {integrity: sha512-U5qJYWgUKBFJqr1coMSbczA964uvouzBjQbtJlaI9LfMwy7hr+kc1Mfh0gqi/2872KmGu9utgff6lj8Oti8+VQ==} + peerDependencies: + three: '>=0.128.0' + + three@0.154.0: + resolution: {integrity: sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==} + + troika-three-text@0.47.2: + resolution: {integrity: sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.47.2: + resolution: {integrity: sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.47.2: + resolution: {integrity: sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA==} + + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.3: + resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + uuid@2.0.3: + resolution: {integrity: sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vis-data@7.1.9: + resolution: {integrity: sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==} + peerDependencies: + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + vis-util: ^5.0.1 + + vis-network@9.1.9: + resolution: {integrity: sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==} + peerDependencies: + '@egjs/hammerjs': ^2.0.0 + component-emitter: ^1.3.0 + keycharm: ^0.2.0 || ^0.3.0 || ^0.4.0 + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + vis-data: ^6.3.0 || ^7.0.0 + vis-util: ^5.0.1 + + vis-util@5.0.7: + resolution: {integrity: sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==} + engines: {node: '>=8'} + peerDependencies: + '@egjs/hammerjs': ^2.0.0 + component-emitter: ^1.3.0 || ^2.0.0 + + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + viz.js@1.8.2: + resolution: {integrity: sha512-W+1+N/hdzLpQZEcvz79n2IgUE9pfx6JLdHh3Kh8RGvLL8P1LdJVQmi2OsDcLdY4QVID4OUy+FPelyerX0nJxIQ==} + deprecated: 2.x is no longer supported, 3.x published as @viz-js/viz + + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-builtin-type@1.1.4: + resolution: {integrity: sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zustand@3.7.2: + resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + + zustand@4.3.9: + resolution: {integrity: sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==} + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.2': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@egjs/hammerjs@2.0.17': + dependencies: + '@types/hammerjs': 2.0.46 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.15.0)': + dependencies: + eslint: 9.15.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.0': {} + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.15.0': {} + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.3': + dependencies: + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@mediapipe/tasks-vision@0.10.8': {} + + '@react-spring/animated@9.6.1(react@18.3.1)': + dependencies: + '@react-spring/shared': 9.6.1(react@18.3.1) + '@react-spring/types': 9.6.1 + react: 18.3.1 + + '@react-spring/core@9.6.1(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.6.1(react@18.3.1) + '@react-spring/rafz': 9.6.1 + '@react-spring/shared': 9.6.1(react@18.3.1) + '@react-spring/types': 9.6.1 + react: 18.3.1 + + '@react-spring/rafz@9.6.1': {} + + '@react-spring/shared@9.6.1(react@18.3.1)': + dependencies: + '@react-spring/rafz': 9.6.1 + '@react-spring/types': 9.6.1 + react: 18.3.1 + + '@react-spring/three@9.6.1(@react-three/fiber@8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0))(react@18.3.1)(three@0.154.0)': + dependencies: + '@react-spring/animated': 9.6.1(react@18.3.1) + '@react-spring/core': 9.6.1(react@18.3.1) + '@react-spring/shared': 9.6.1(react@18.3.1) + '@react-spring/types': 9.6.1 + '@react-three/fiber': 8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0) + react: 18.3.1 + three: 0.154.0 + + '@react-spring/types@9.6.1': {} + + '@react-three/fiber@8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@types/react-reconciler': 0.26.7 + its-fine: 1.2.5(react@18.3.1) + react: 18.3.1 + react-reconciler: 0.27.0(react@18.3.1) + react-use-measure: 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + scheduler: 0.21.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.154.0 + zustand: 3.7.2(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + '@rollup/rollup-android-arm-eabi@4.27.3': + optional: true + + '@rollup/rollup-android-arm64@4.27.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.27.3': + optional: true + + '@rollup/rollup-darwin-x64@4.27.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.27.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.27.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.27.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.27.3': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.27.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.27.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.27.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.27.3': + optional: true + + '@tweenjs/tween.js@23.1.3': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + + '@types/draco3d@1.4.10': {} + + '@types/estree@1.0.6': {} + + '@types/hammerjs@2.0.46': {} + + '@types/json-schema@7.0.15': {} + + '@types/offscreencanvas@2019.7.3': {} + + '@types/prop-types@15.7.13': {} + + '@types/react-dom@18.3.1': + dependencies: + '@types/react': 18.3.12 + + '@types/react-reconciler@0.26.7': + dependencies: + '@types/react': 18.3.12 + + '@types/react-reconciler@0.28.8': + dependencies: + '@types/react': 18.3.12 + + '@types/react@18.3.12': + dependencies: + '@types/prop-types': 15.7.13 + csstype: 3.1.3 + + '@types/stats.js@0.17.3': {} + + '@types/three@0.170.0': + dependencies: + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.3 + '@types/webxr': 0.5.20 + '@webgpu/types': 0.1.51 + fflate: 0.8.2 + meshoptimizer: 0.18.1 + + '@types/webxr@0.5.20': {} + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + + '@vitejs/plugin-react@4.3.3(vite@5.4.11)': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.4.11 + transitivePeerDependencies: + - supports-color + + '@webgpu/types@0.1.51': {} + + '@yomguithereal/helpers@1.1.1': {} + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + balanced-match@1.0.2: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001683 + electron-to-chromium: 1.5.64 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + camera-controls@2.9.0(three@0.154.0): + dependencies: + three: 0.154.0 + + caniuse-lite@1.0.30001683: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + classnames@2.5.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + ctrl-keys@1.0.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-binarytree@1.0.2: {} + + d3-color@1.4.1: {} + + d3-dispatch@1.0.6: {} + + d3-drag@1.2.5: + dependencies: + d3-dispatch: 1.0.6 + d3-selection: 1.4.2 + + d3-ease@1.0.7: {} + + d3-force-3d@3.0.5: + dependencies: + d3-binarytree: 1.0.2 + d3-dispatch: 1.0.6 + d3-octree: 1.0.2 + d3-quadtree: 3.0.1 + d3-timer: 1.0.10 + + d3-format@1.4.5: {} + + d3-graphviz@2.6.1: + dependencies: + d3-dispatch: 1.0.6 + d3-format: 1.4.5 + d3-interpolate: 1.4.0 + d3-path: 1.0.9 + d3-selection: 1.4.2 + d3-timer: 1.0.10 + d3-transition: 1.3.2 + d3-zoom: 1.8.3 + viz.js: 1.8.2 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@1.4.0: + dependencies: + d3-color: 1.4.1 + + d3-octree@1.0.2: {} + + d3-path@1.0.9: {} + + d3-quadtree@3.0.1: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 1.4.5 + d3-interpolate: 1.4.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@1.4.2: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@1.0.10: {} + + d3-transition@1.3.2: + dependencies: + d3-color: 1.4.1 + d3-dispatch: 1.0.6 + d3-ease: 1.0.7 + d3-interpolate: 1.4.0 + d3-selection: 1.4.2 + d3-timer: 1.0.10 + + d3-zoom@1.8.3: + dependencies: + d3-dispatch: 1.0.6 + d3-drag: 1.2.5 + d3-interpolate: 1.4.0 + d3-selection: 1.4.2 + d3-transition: 1.3.2 + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + debounce@1.2.1: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decode-uri-component@0.4.1: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-gpu@5.0.58: + dependencies: + webgl-constants: 1.1.1 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + draco3d@1.5.7: {} + + electron-to-chromium@1.5.64: {} + + ellipsize@0.5.1: {} + + es-abstract@1.23.5: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.3 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + iterator.prototype: 1.1.3 + safe-array-concat: 1.1.2 + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@5.0.0(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + + eslint-plugin-react-refresh@0.4.14(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + + eslint-plugin-react@7.37.2(eslint@9.15.0): + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.0 + eslint: 9.15.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.15.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.0 + '@eslint/core': 0.9.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.15.0 + '@eslint/plugin-kit': 0.2.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + events@3.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fflate@0.6.10: {} + + fflate@0.8.2: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + filter-obj@5.1.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + + flatted@3.3.2: {} + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.12.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + + glodrei@0.0.1(@react-three/fiber@8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0))(@types/three@0.170.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0): + dependencies: + '@babel/runtime': 7.26.0 + '@mediapipe/tasks-vision': 0.10.8 + '@react-spring/three': 9.6.1(@react-three/fiber@8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0))(react@18.3.1)(three@0.154.0) + '@react-three/fiber': 8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0) + '@use-gesture/react': 10.3.1(react@18.3.1) + camera-controls: 2.9.0(three@0.154.0) + cross-env: 7.0.3 + detect-gpu: 5.0.58 + glsl-noise: 0.0.0 + maath: 0.10.8(@types/three@0.170.0)(three@0.154.0) + meshline: 3.3.1(three@0.154.0) + react: 18.3.1 + react-composer: 5.0.3(react@18.3.1) + react-merge-refs: 1.1.0 + stats-gl: 2.4.2(@types/three@0.170.0)(three@0.154.0) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.154.0 + three-mesh-bvh: 0.7.6(three@0.154.0) + three-stdlib: 2.34.0(three@0.154.0) + troika-three-text: 0.47.2(three@0.154.0) + tunnel-rat: 0.1.2(react@18.3.1) + utility-types: 3.11.0 + uuid: 9.0.1 + zustand: 3.7.2(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/three' + - immer + + glsl-noise@0.0.0: {} + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graphology-indices@0.17.0(graphology-types@0.24.8): + dependencies: + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + mnemonist: 0.39.8 + + graphology-layout-forceatlas2@0.10.1(graphology-types@0.24.8): + dependencies: + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + + graphology-layout-noverlap@0.4.2(graphology-types@0.24.8): + dependencies: + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + + graphology-layout@0.6.1(graphology-types@0.24.8): + dependencies: + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + pandemonium: 2.4.1 + + graphology-metrics@2.3.1(graphology-types@0.24.8): + dependencies: + graphology-indices: 0.17.0(graphology-types@0.24.8) + graphology-shortest-path: 2.1.0(graphology-types@0.24.8) + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + mnemonist: 0.39.8 + + graphology-shortest-path@2.1.0(graphology-types@0.24.8): + dependencies: + '@yomguithereal/helpers': 1.1.1 + graphology-indices: 0.17.0(graphology-types@0.24.8) + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + mnemonist: 0.39.8 + + graphology-types@0.24.8: {} + + graphology-utils@2.5.2(graphology-types@0.24.8): + dependencies: + graphology-types: 0.24.8 + + graphology@0.25.4(graphology-types@0.24.8): + dependencies: + events: 3.3.0 + graphology-types: 0.24.8 + obliterator: 2.0.4 + + graphviz-react@1.2.5(react@18.3.1): + dependencies: + d3-graphviz: 2.6.1 + react: 18.3.1 + + has-bigints@1.0.2: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hold-event@0.2.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + internmap@2.0.3: {} + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-async-function@2.0.0: + dependencies: + has-tostringtag: 1.0.2 + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-weakmap@2.0.2: {} + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-weakset@2.0.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.3: + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.6 + set-function-name: 2.0.2 + + its-fine@1.2.5(react@18.3.1): + dependencies: + '@types/react-reconciler': 0.28.8 + react: 18.3.1 + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 + + keycharm@0.4.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + maath@0.10.8(@types/three@0.170.0)(three@0.154.0): + dependencies: + '@types/three': 0.170.0 + three: 0.154.0 + + meshline@3.3.1(three@0.154.0): + dependencies: + three: 0.154.0 + + meshoptimizer@0.18.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + mnemonist@0.39.8: + dependencies: + obliterator: 2.0.4 + + ms@2.1.3: {} + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.18: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.3: {} + + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.entries@1.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + obliterator@2.0.4: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + pandemonium@2.4.1: + dependencies: + mnemonist: 0.39.8 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + possible-typed-array-names@1.0.0: {} + + postcss@8.4.49: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + potpack@1.0.2: {} + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + query-string@9.1.1: + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + + react-composer@5.0.3(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-graph-vis@1.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(react@18.3.1)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)): + dependencies: + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.3.1 + uuid: 2.0.3 + vis-data: 7.1.9(uuid@2.0.3)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)) + vis-network: 9.1.9(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@2.0.3)(vis-data@7.1.9(uuid@2.0.3)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)) + transitivePeerDependencies: + - '@egjs/hammerjs' + - component-emitter + - keycharm + - vis-util + + react-is@16.13.1: {} + + react-merge-refs@1.1.0: {} + + react-reconciler@0.27.0(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.21.0 + + react-refresh@0.14.2: {} + + react-use-measure@2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + debounce: 1.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + reagraph@4.21.0(@types/three@0.170.0)(graphology-types@0.24.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@react-spring/three': 9.6.1(@react-three/fiber@8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0))(react@18.3.1)(three@0.154.0) + '@react-three/fiber': 8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0) + '@use-gesture/react': 10.3.1(react@18.3.1) + camera-controls: 2.9.0(three@0.154.0) + classnames: 2.5.1 + d3-array: 3.2.4 + d3-force-3d: 3.0.5 + d3-hierarchy: 3.1.2 + d3-scale: 4.0.2 + ellipsize: 0.5.1 + glodrei: 0.0.1(@react-three/fiber@8.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0))(@types/three@0.170.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.154.0) + graphology: 0.25.4(graphology-types@0.24.8) + graphology-layout: 0.6.1(graphology-types@0.24.8) + graphology-layout-forceatlas2: 0.10.1(graphology-types@0.24.8) + graphology-layout-noverlap: 0.4.2(graphology-types@0.24.8) + graphology-metrics: 2.3.1(graphology-types@0.24.8) + graphology-shortest-path: 2.1.0(graphology-types@0.24.8) + hold-event: 0.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + reakeys: 2.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + three: 0.154.0 + three-stdlib: 2.34.0(three@0.154.0) + zustand: 4.3.9(react@18.3.1) + transitivePeerDependencies: + - '@types/three' + - expo + - expo-asset + - expo-gl + - graphology-types + - immer + - react-native + + reakeys@2.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + ctrl-keys: 1.0.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + reflect.getprototypeof@1.0.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + globalthis: 1.0.4 + which-builtin-type: 1.1.4 + + regenerator-runtime@0.14.1: {} + + regexp.prototype.flags@1.5.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.27.3: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.27.3 + '@rollup/rollup-android-arm64': 4.27.3 + '@rollup/rollup-darwin-arm64': 4.27.3 + '@rollup/rollup-darwin-x64': 4.27.3 + '@rollup/rollup-freebsd-arm64': 4.27.3 + '@rollup/rollup-freebsd-x64': 4.27.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.27.3 + '@rollup/rollup-linux-arm-musleabihf': 4.27.3 + '@rollup/rollup-linux-arm64-gnu': 4.27.3 + '@rollup/rollup-linux-arm64-musl': 4.27.3 + '@rollup/rollup-linux-powerpc64le-gnu': 4.27.3 + '@rollup/rollup-linux-riscv64-gnu': 4.27.3 + '@rollup/rollup-linux-s390x-gnu': 4.27.3 + '@rollup/rollup-linux-x64-gnu': 4.27.3 + '@rollup/rollup-linux-x64-musl': 4.27.3 + '@rollup/rollup-win32-arm64-msvc': 4.27.3 + '@rollup/rollup-win32-ia32-msvc': 4.27.3 + '@rollup/rollup-win32-x64-msvc': 4.27.3 + fsevents: 2.3.3 + + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + + scheduler@0.21.0: + dependencies: + loose-envify: 1.4.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.3 + + source-map-js@1.2.1: {} + + split-on-first@3.0.0: {} + + stats-gl@2.4.2(@types/three@0.170.0)(three@0.154.0): + dependencies: + '@types/three': 0.170.0 + three: 0.154.0 + + stats.js@0.17.0: {} + + string.prototype.matchall@4.0.11: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.3 + set-function-name: 2.0.2 + side-channel: 1.0.6 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.5 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + suspend-react@0.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + + three-mesh-bvh@0.7.6(three@0.154.0): + dependencies: + three: 0.154.0 + + three-stdlib@2.34.0(three@0.154.0): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.20 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.154.0 + + three@0.154.0: {} + + troika-three-text@0.47.2(three@0.154.0): + dependencies: + bidi-js: 1.0.3 + three: 0.154.0 + troika-three-utils: 0.47.2(three@0.154.0) + troika-worker-utils: 0.47.2 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.47.2(three@0.154.0): + dependencies: + three: 0.154.0 + + troika-worker-utils@0.47.2: {} + + tunnel-rat@0.1.2(react@18.3.1): + dependencies: + zustand: 4.3.9(react@18.3.1) + transitivePeerDependencies: + - immer + - react + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.3: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + reflect.getprototypeof: 1.0.6 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + + utility-types@3.11.0: {} + + uuid@2.0.3: {} + + uuid@9.0.1: {} + + vis-data@7.1.9(uuid@2.0.3)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)): + dependencies: + uuid: 2.0.3 + vis-util: 5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1) + + vis-network@9.1.9(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@2.0.3)(vis-data@7.1.9(uuid@2.0.3)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)): + dependencies: + '@egjs/hammerjs': 2.0.17 + component-emitter: 1.3.1 + keycharm: 0.4.0 + uuid: 2.0.3 + vis-data: 7.1.9(uuid@2.0.3)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)) + vis-util: 5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1) + + vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1): + dependencies: + '@egjs/hammerjs': 2.0.17 + component-emitter: 1.3.1 + + vite@5.4.11: + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.27.3 + optionalDependencies: + fsevents: 2.3.3 + + viz.js@1.8.2: {} + + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-builtin-type@1.1.4: + dependencies: + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.0.2 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zustand@3.7.2(react@18.3.1): + optionalDependencies: + react: 18.3.1 + + zustand@4.3.9(react@18.3.1): + dependencies: + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + react: 18.3.1 diff --git a/pkg/topology/embed/frontend/src/App.css b/pkg/topology/embed/frontend/src/App.css new file mode 100644 index 0000000..c7c939a --- /dev/null +++ b/pkg/topology/embed/frontend/src/App.css @@ -0,0 +1,8 @@ +#root { +/* max-width: 1280px;*/ +/* margin: 0 auto;*/ +/* padding: 2rem;*/ +/* text-align: center;*/ + font-size: 0.8rem; +} + diff --git a/pkg/topology/embed/frontend/src/App.tsx b/pkg/topology/embed/frontend/src/App.tsx new file mode 100644 index 0000000..ff690fd --- /dev/null +++ b/pkg/topology/embed/frontend/src/App.tsx @@ -0,0 +1,365 @@ +import { useState, useCallback, useEffect, useRef, forwardRef } from 'react' +import { fetchTopologyJSON, ComputeTopologyParams, fetchPeers, JSONResponse, RPC, Conn } from '@/lib/api' +import { GraphCanvas, GraphCanvasRef, InternalGraphNode, InternalGraphEdge, InternalGraphPosition, layoutProvider, recommendLayout, useSelection } from 'reagraph' +import forceAtlas2 from 'graphology-layout-forceatlas2' +import random from 'graphology-layout/random' +import circular from 'graphology-layout/circular' +import Graph from 'graphology' + +import './App.css' + +function App() { + const [params, setParams] = useState({}) + + const [graph, setGraph] = useState<{ nodes: InternalGraphNode[], edges: InternalGraphEdge[] }>({ nodes: [], edges: [] }) + const [apiData, setAPIData] = useState({ nodes: [], conns: [] }) + const [clickedEdges, setClickedEdges] = useState>({}) + const [minBytesSec, setMinBytesSec] = useState(null) + const [selectedNode, setSelectedNode] = useState(null) + const nodeRef = useRef(new Map()) + const graphRef = useRef(null) + + const renderGraph = useCallback(async (selectedPeers: { [id: string]: boolean }, crawlDistance: number) => { + setMinBytesSec(minBytesSec) + const graph = await fetchTopologyJSON({ + ...params, + crawlDistance, + minBytesSec: minBytesSec || null, + includeNodes: Object.keys(selectedPeers).filter(url => selectedPeers[url]), + }) + setAPIData(graph) + }, [params, fetchTopologyJSON, setAPIData, minBytesSec]) + + useEffect(() => { + let max = (apiData.conns || []).reduce((max, conn) => { + let total = conn.connectionStatus.send_monitor.avg_rate + conn.connectionStatus.recv_monitor.avg_rate + if (total > max) { + return total + } + return max + }, 0) + + const nodes = (apiData.nodes || []).map(node => ({ + id: node.id, + label: node.validatorMoniker !== '' ? node.validatorMoniker : node.moniker, + fill: node.validatorAddress !== '' ? 'red' : undefined, + })) + + const edges = (apiData.conns || []) + .filter(conn => conn.connectionStatus.send_monitor.avg_rate + conn.connectionStatus.recv_monitor.avg_rate >= (minBytesSec || 0).valueOf()) + .map(conn => ({ + source: conn.from, + target: conn.to, + id: `${conn.from}-${conn.to}`, + label: clickedEdges[`${conn.from}-${conn.to}`] ? `${humanizeBytes(conn.connectionStatus.send_monitor.avg_rate)}/s\n${humanizeBytes(conn.connectionStatus.recv_monitor.avg_rate)}/s` : '', + size: (conn.connectionStatus.send_monitor.avg_rate + conn.connectionStatus.recv_monitor.avg_rate) / max * 5, + })) + + let copy = [...nodes] + + let circleSizes = [10, 25, 50, 75, 130, 200, 350, 500] + let cohorts = [] + let k = 0 + for (let size of circleSizes) { + const graph = new Graph() + let i = 0 + for (let node of copy) { + if (!graph.hasNode(node.id)) { + graph.addNode(node.id) + i++ + } + + if (i >= size) { + copy = copy.slice(i) + break + } + } + random.assign(graph) + + let positions = circular(graph, { scale: size * (8-k) }) + console.log(positions) + + let j = 0 + for (let id in positions) { + let position = positions[id] + nodeRef.current.set(id, { id, x: position.x, vx: position.x, y: position.y, vy: position.y, z: 1, links: [], data: null, index: j++ }) + } + // for (let edge of edges) { + // graph.addEdge(edge.source, edge.target) + // } + k++ + } + + setGraph({ nodes, edges }) + }, [apiData, clickedEdges, setGraph, minBytesSec]) + + let { nodes, edges } = graph + + function onEdgeClick(edge: InternalGraphEdge) { + setClickedEdges({ ...clickedEdges, [edge.id]: !clickedEdges[edge.id] }) + } + + function handleNodeClick(node: InternalGraphNode) { + const selectedRPC = (apiData.nodes || []).find(n => n.id === node.id) + if (selectedRPC) { + const connectedPeers = (apiData.conns || []) + .filter(conn => conn.from === selectedRPC.id || conn.to === selectedRPC.id) + .map(conn => { + const peerId = conn.from === selectedRPC.id ? conn.to : conn.from + const peer = (apiData.nodes || []).find(n => n.id === peerId) + return { + peer, + connectionStatus: conn.connectionStatus, + isOutbound: conn.from === selectedRPC.id + } + }) + setSelectedNode({ ...selectedRPC, connectedPeers }) + } else { + setSelectedNode(null) + } + } + + function handleCanvasClick() { + setSelectedNode(null) + } + + const { + selections, + actives, + onNodeClick: selectionNodeClick, + onCanvasClick: selectionCanvasClick + } = useSelection({ + ref: graphRef, + nodes: nodes, + edges: edges, + pathSelectionType: 'all' + }) + + return ( + <> +
+ +
+ { + let idx = nodes.findIndex(node => node.id === id) + if (idx === -1) { + idx = Math.random() * 100 + } + + const position = { + x: 25 * idx, + y: idx % 2 === 0 ? 0 : 50, + z: 1, + } + + return nodeRef.current?.get(id) || (function() { + // This next bit is quite fraught -- do not modify unless you know what you're doing + nodeRef.current.set(id, { id, x: position.x, vx: position.x, y: position.y, vy: position.y, z: 1, links: [], data: null, index: idx }) + return position + })() + }, + }} + onNodeDragged={node => { + nodeRef.current.set(node.id, node.position) + }} + onEdgeClick={onEdgeClick} + selections={selections} + actives={actives} + onCanvasClick={(event) => { + handleCanvasClick() + selectionCanvasClick(event) + }} + onNodeClick={(node, event) => { + handleNodeClick(node) + selectionNodeClick(node, event) + }} + /> +
+ {selectedNode && ( +
+ )} +
+ + ) +} + +function Sidebar(props: { + minBytesSec: number, + setMinBytesSec: (x: number) => void, + renderGraph: (selectedPeers: { [id: string]: boolean }, crawlDistance: number) => void, +}) { + const { renderGraph, minBytesSec, setMinBytesSec } = props + const [peers, setPeers] = useState([]) + const [selectedPeers, setSelectedPeers] = useState<{ [url: string]: boolean }>({}) + const [sidebarOpen, setSidebarOpen] = useState(true) + const [filterText, setFilterText] = useState('') + const [crawlDistance, setCrawlDistance] = useState(1) + + useEffect(() => { + (async function() { + let peers = await fetchPeers() + setPeers(peers) + })() + }, []) + + const filteredPeers = peers.filter(peer => + peer.moniker.toLowerCase().includes(filterText.toLowerCase()) || + peer.id.toLowerCase().includes(filterText.toLowerCase()) || + peer.url.toLowerCase().includes(filterText.toLowerCase()) + ) + + return ( + <> +
+
+ + setFilterText(e.target.value)} + style={{ width: 'calc(100% - 88px)', height: '1rem', marginTop: 8, marginRight: 10, padding: '5px' }} + /> + setCrawlDistance(Number(e.target.value))} + style={{ width: 64, height: '1rem', marginTop: 8, marginRight: 10, padding: '5px' }} + /> + setMinBytesSec(Number(e.target.value))} + style={{ width: 128, height: '1rem', marginTop: 8, padding: '5px' }} + /> + +
+ + {filteredPeers.map(peer => ( + + + + + + + ))} +
+ setSelectedPeers({ ...selectedPeers, [peer.url]: e.target.checked })} /> + {peer.moniker}{peer.id}{peer.url}
+
+ + + ) +} + +type NodeWithPeers = RPC & { + connectedPeers: Array<{ + peer: RPC | undefined, + connectionStatus: Conn['connectionStatus'], + isOutbound: boolean + }> +} + +function NodeDetails({ node }: { node: NodeWithPeers }) { + return ( +
+

{node.moniker}

+ + + + + + + + {Object.entries(node).map(([key, value]) => { + if (typeof value !== 'object' && !['id', 'url', 'ip', 'moniker', 'validatorMoniker', 'validatorAddress'].includes(key)) { + return ( + + + + + ) + } + return null + })} + +
ID:{node.id}
URL:{node.url}
IP:{node.ip}
Validator Moniker:{node.validatorMoniker || 'N/A'}
Validator Address:{node.validatorAddress || 'N/A'}
{key}:{String(value)}
+

Connected Peers

+ + + + + + + + + + + {node.connectedPeers.map((peerInfo, index) => ( + + + + + + + ))} + +
MonikerDirectionSend RateReceive Rate
{peerInfo.peer?.moniker || 'Unknown'}{peerInfo.isOutbound ? 'Outbound' : 'Inbound'}{humanizeBytes(peerInfo.connectionStatus.send_monitor.avg_rate)}/s{humanizeBytes(peerInfo.connectionStatus.recv_monitor.avg_rate)}/s
+

Connection Statistics

+ + + + + + + +
Total Send Rate:{humanizeBytes(node.connectedPeers.reduce((sum, peer) => sum + peer.connectionStatus.send_monitor.avg_rate, 0))}/s
Total Receive Rate:{humanizeBytes(node.connectedPeers.reduce((sum, peer) => sum + peer.connectionStatus.recv_monitor.avg_rate, 0))}/s
Outbound Connections:{node.connectedPeers.filter(peer => peer.isOutbound).length}
Inbound Connections:{node.connectedPeers.filter(peer => !peer.isOutbound).length}
+
+ ) +} + +function humanizeBytes(bytes: number | string, si = true, dp = 1) { + bytes = Number(bytes) + const thresh = si ? 1000 : 1024 + + if (Math.abs(bytes) < thresh) { + return bytes + ' B' + } + + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] + let u = -1 + const r = 10**dp + + do { + bytes /= thresh + ++u + } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1) + + return bytes.toFixed(dp) + ' ' + units[u] +} + +export default App \ No newline at end of file diff --git a/pkg/topology/embed/frontend/src/index.css b/pkg/topology/embed/frontend/src/index.css new file mode 100644 index 0000000..5a2849e --- /dev/null +++ b/pkg/topology/embed/frontend/src/index.css @@ -0,0 +1,74 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + padding: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +#root { + width: 100vw; + height: 100vh; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/pkg/topology/embed/frontend/src/lib/api.ts b/pkg/topology/embed/frontend/src/lib/api.ts new file mode 100644 index 0000000..f4e56f3 --- /dev/null +++ b/pkg/topology/embed/frontend/src/lib/api.ts @@ -0,0 +1,99 @@ +import queryString from 'query-string' + +export type ComputeTopologyParams = { + includeNodes?: string[] + crawlDistance?: number + minBytesSec?: number +} + +export async function fetchTopologyDOT(params: ComputeTopologyParams): Promise { + params.crawlDistance = params.crawlDistance || 1 + + const queryParams = queryString.stringify({ + // currentHomeNode: params.currentHomeNode, + includeNodes: params.includeNodes, + crawlDistance: params.crawlDistance, + minBytesSec: params.minBytesSec, + // highlightNodes: params.highlightNodes + format: 'dot', + }) + console.log('params', params) + console.log('queryParams', queryParams) + + const response = await fetch(`http://localhost:8080/topology?${queryParams}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + return await response.text() +} + +export type JSONResponse = { + nodes?: RPC[] + conns?: Conn[] +} + +export async function fetchTopologyJSON(params: ComputeTopologyParams): Promise { + params.crawlDistance = params.crawlDistance || 1 + + const queryParams = queryString.stringify({ + includeNodes: params.includeNodes, + crawlDistance: params.crawlDistance, + minBytesSec: params.minBytesSec, + }) + + const response = await fetch(`http://localhost:8080/topology?${queryParams}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return await response.json() +} + +export type RPC = { + id: string + ip: string + url: string + moniker: string + validatorAddress: string + validatorMoniker: string +} + +export type Conn = { + from: string + to: string + connectionStatus: { + send_monitor: { + avg_rate: number + } + recv_monitor: { + avg_rate: number + } + } +} + +export async function fetchPeers(): Promise { + const response = await fetch('http://localhost:8080/peers', { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + return await response.json() +} diff --git a/pkg/topology/embed/frontend/src/main.tsx b/pkg/topology/embed/frontend/src/main.tsx new file mode 100644 index 0000000..b28d2e0 --- /dev/null +++ b/pkg/topology/embed/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/pkg/topology/embed/frontend/tsconfig.json b/pkg/topology/embed/frontend/tsconfig.json new file mode 100644 index 0000000..8da9106 --- /dev/null +++ b/pkg/topology/embed/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "react-jsx", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} \ No newline at end of file diff --git a/pkg/topology/embed/frontend/vite.config.js b/pkg/topology/embed/frontend/vite.config.js new file mode 100644 index 0000000..582a35b --- /dev/null +++ b/pkg/topology/embed/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': '/src', + }, + }, +}) + + diff --git a/pkg/topology/http.go b/pkg/topology/http.go new file mode 100644 index 0000000..2985448 --- /dev/null +++ b/pkg/topology/http.go @@ -0,0 +1,83 @@ +package topology + +import ( + "encoding/json" + "fmt" + "io/fs" + "net/http" + + butils "github.com/brynbellomy/go-utils" + "github.com/gorilla/mux" + + tmhttp "main/pkg/http" + "main/pkg/topology/embed" + "main/pkg/types" +) + +func WithHTTPTopologyAPI(state *types.State) tmhttp.Option { + return tmhttp.WithRoute("GET", "/topology", butils.UnrestrictedCors(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req ComputeTopologyRequest + err := butils.UnmarshalHTTPRequest(&req, r) + if err != nil { + http.Error(w, fmt.Sprintf("bad request: %s", err.Error()), http.StatusBadRequest) + return + } + + if req.Format == "dot" { + graph, err := ComputeTopology(state, req) + if err != nil { + http.Error(w, fmt.Sprintf("could not compute topology: %s", err.Error()), http.StatusInternalServerError) + return + } + + topoGraph, err := ComputeTopologyDOT(graph, req) + if err != nil { + http.Error(w, fmt.Sprintf("could not convert topology to dot: %s", err.Error()), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/vnd.graphviz") + if err := RenderTopologyDOT(topoGraph, w); err != nil { + http.Error(w, fmt.Sprintf("could not marshal dot: %s", err.Error()), http.StatusInternalServerError) + return + } + } else { + topoGraph, err := ComputeTopology(state, req) + if err != nil { + http.Error(w, fmt.Sprintf("could not compute topology: %s", err.Error()), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(topoGraph); err != nil { + http.Error(w, fmt.Sprintf("could not marshal json: %s", err.Error()), http.StatusInternalServerError) + return + } + } + }))) +} + +func WithHTTPPeersAPI(state *types.State) tmhttp.Option { + return tmhttp.WithRoute("GET", "/peers", butils.UnrestrictedCors(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + peers := state.KnownRPCs().Values() + _ = json.NewEncoder(w).Encode(peers) + }))) +} + +func WithHTTPDebugAPI(state *types.State) tmhttp.Option { + return tmhttp.WithRoute("GET", "/debug", butils.UnrestrictedCors(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(state.ChainValidators) + }))) +} + +func WithFrontendStaticAssets() tmhttp.Option { + return tmhttp.WithRouterOption(func(r *mux.Router) { + assets, err := fs.Sub(embed.Frontend, "frontend/dist") + if err != nil { + panic(err) + } + r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.FileServer(http.FS(assets)).ServeHTTP(w, r) + }) + }) +} diff --git a/pkg/topology/topology.go b/pkg/topology/topology.go new file mode 100644 index 0000000..7fbbed3 --- /dev/null +++ b/pkg/topology/topology.go @@ -0,0 +1,176 @@ +package topology + +import ( + "bytes" + "io" + + butils "github.com/brynbellomy/go-utils" + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/graph/encoding/dot" + "gonum.org/v1/gonum/graph/path" + "gonum.org/v1/gonum/graph/simple" + + "main/pkg/types" +) + +var LogChannel chan string + +type ComputeTopologyRequest struct { + CurrentHomeNode string `query:"currentHomeNode"` + IncludeNodes []string `query:"includeNodes"` + CrawlDistance uint64 `query:"crawlDistance"` + MinBytesSec uint64 `query:"minBytesSec"` + HighlightNodes []string `query:"highlightNodes"` + Format string `query:"format"` +} + +func ComputeTopology(state *types.State, req ComputeTopologyRequest) (Graph, error) { + knownRPCs := state.KnownRPCs() + includeNodes := butils.NewSet[types.RPC]() + includeIDs := butils.NewSet[string]() + + // Gather all nodes to include, factoring in the crawl distance + for _, url := range req.IncludeNodes { + rpc, ok := knownRPCs.Get(url) + if !ok { + continue + } + includeNodes.AddSet(stackBasedCrawl(state, rpc, req.CrawlDistance, req.MinBytesSec)) + } + + var g Graph + + // Add nodes + for rpc := range includeNodes { + g.Nodes = append(g.Nodes, rpc) + includeIDs.Add(rpc.ID) + } + + // Add edges + for rpc := range includeNodes { + for _, peer := range state.RPCPeers(rpc.URL) { + if !includeIDs.Has(string(peer.NodeInfo.DefaultNodeID)) { + continue + } + + if peer.IsOutbound { + g.AddConn(rpc.ID, string(peer.NodeInfo.DefaultNodeID), peer.ConnectionStatus) + } else { + g.AddConn(string(peer.NodeInfo.DefaultNodeID), rpc.ID, peer.ConnectionStatus) + } + } + } + + return g, nil +} + +func stackBasedCrawl(state *types.State, homeNode types.RPC, crawlDistance uint64, minBytesSec uint64) butils.Set[types.RPC] { + visited := butils.NewSet[types.RPC]() + visited.Add(homeNode) + + type stackItem struct { + node types.RPC + depth uint64 + } + stack := []stackItem{{node: homeNode, depth: 0}} + + for len(stack) > 0 { + item := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + visited.Add(item.node) + + if item.depth >= crawlDistance { + continue + } + + for _, peer := range state.RPCPeers(item.node.URL) { + if uint64(peer.ConnectionStatus.SendMonitor.AvgRate)+uint64(peer.ConnectionStatus.RecvMonitor.AvgRate) < minBytesSec { + continue + } + stack = append(stack, stackItem{ + node: types.NewRPCFromPeer(peer), + depth: item.depth + 1, + }) + } + } + return visited +} + +func ComputeTopologyDOT(topology Graph, req ComputeTopologyRequest) (graph.Graph, error) { + nodeIDs := make(map[string]int64) + var highlightedGraphNodes []*DOTPeerNode + + highlightRPCs := butils.NewSet[string]() + highlightRPCs.AddAll(req.HighlightNodes...) + + g := simple.NewUndirectedGraph() + + // Render all known RPCs + for _, rpc := range topology.Nodes { + var node *DOTPeerNode + if highlightRPCs.Has(rpc.ID) || highlightRPCs.Has(rpc.IP) || highlightRPCs.Has(rpc.URL) || highlightRPCs.Has(rpc.Moniker) { + node = NewDOTPeerNode(g.NewNode(), rpc, "crimson") + highlightedGraphNodes = append(highlightedGraphNodes, node) + } else { + node = NewDOTPeerNode(g.NewNode(), rpc, "cadetblue") + } + + nodeIDs[rpc.URL] = node.Node.ID() + g.AddNode(node) + } + + // Add edges + for _, conn := range topology.Conns { + nodeID1, ok := nodeIDs[conn.From] + if !ok { + continue + } + + nodeID2, ok := nodeIDs[conn.To] + if !ok { + continue + } + + if g.Edge(nodeID1, nodeID2) == nil && g.Edge(nodeID2, nodeID1) == nil { + g.SetEdge(NewDOTEdge(g.Node(nodeID1), g.Node(nodeID2), "azure4", "1.0")) + } + } + + for i, n := range highlightedGraphNodes { + paths := path.DijkstraFrom(n, g) + for j := i + 1; j < len(highlightedGraphNodes); j++ { + npath, _ := paths.To(highlightedGraphNodes[j].ID()) + if npath == nil { + continue + } + + for e := 0; e < len(npath)-1; e++ { + edge := g.Edge(npath[e].ID(), npath[e+1].ID()).(*DOTEdge) + if edge != nil { + edge.SetColor("crimson") + edge.SetWidth("3.0") + } + + // hack: color the reverse path to make sure we don't color the "copied" reversed edge + edge = g.Edge(npath[e+1].ID(), npath[e].ID()).(*DOTEdge) + if edge != nil { + edge.SetColor("crimson") + edge.SetWidth("3.0") + } + } + } + } + + return g, nil +} + +func RenderTopologyDOT(topology graph.Graph, w io.Writer) error { + raw, err := dot.Marshal(topology, "topology", "", "") + if err != nil { + return err + } + + _, err = bytes.NewReader(raw).WriteTo(w) + return err +} diff --git a/pkg/topology/types.go b/pkg/topology/types.go new file mode 100644 index 0000000..8383633 --- /dev/null +++ b/pkg/topology/types.go @@ -0,0 +1,113 @@ +package topology + +import ( + "main/pkg/types" + + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/graph/encoding" +) + +type Graph struct { + Nodes []types.RPC `json:"nodes"` + Conns []Conn `json:"conns"` + Validators map[string]types.TMValidator `json:"validators"` + + connsMap map[string]bool +} + +func (g *Graph) AddConn(from, to string, connectionStatus types.ConnectionStatus) { + if g.connsMap == nil { + g.connsMap = make(map[string]bool) + } + + _, ok := g.connsMap[from+to] + if !ok { + g.Conns = append(g.Conns, NewConn(from, to, connectionStatus)) + g.connsMap[from+to] = true + } +} + +type Conn struct { + From string `json:"from"` + To string `json:"to"` + ConnectionStatus types.ConnectionStatus `json:"connectionStatus"` +} + +func NewConn(from, to string, connectionStatus types.ConnectionStatus) Conn { + return Conn{ + From: from, + To: to, + ConnectionStatus: connectionStatus, + } +} + +type DOTPeerNode struct { + graph.Node + types.RPC + Color string +} + +func NewDOTPeerNode(n graph.Node, rpc types.RPC, color string) *DOTPeerNode { + return &DOTPeerNode{ + Node: n, + RPC: rpc, + Color: color, + } +} + +func (n *DOTPeerNode) ID() int64 { + return n.Node.ID() +} + +func (n *DOTPeerNode) Attributes() []encoding.Attribute { + return []encoding.Attribute{ + {Key: "label", Value: n.Moniker + "\n" + n.RPC.ID + "\n(" + n.URL + ")"}, + {Key: "style", Value: "filled"}, + {Key: "color", Value: n.Color}, + } +} + +type DOTEdge struct { + from, to graph.Node + color, width string +} + +func NewDOTEdge(from, to graph.Node, color string, width string) *DOTEdge { + return &DOTEdge{ + from: from, + to: to, + color: color, + width: width, + } +} + +func (e *DOTEdge) From() graph.Node { + return e.from +} + +func (e *DOTEdge) To() graph.Node { + return e.to +} + +func (e *DOTEdge) ReversedEdge() graph.Edge { + return &DOTEdge{from: e.to, to: e.from, color: e.color, width: e.width} +} + +func (e *DOTEdge) SetColor(color string) { + e.color = color +} + +func (e *DOTEdge) SetWidth(width string) { + e.width = width +} + +func (e *DOTEdge) Weight() float64 { + return 1.0 +} + +func (e *DOTEdge) Attributes() []encoding.Attribute { + return []encoding.Attribute{ + {Key: "color", Value: e.color}, + {Key: "penwidth", Value: e.width}, + } +} diff --git a/pkg/types/block.go b/pkg/types/block.go index 394e6db..cc2e271 100644 --- a/pkg/types/block.go +++ b/pkg/types/block.go @@ -2,19 +2,19 @@ package types import "time" -type TendermintBlockResponse struct { - Result TendermintBlockResult `json:"result"` +type CometBlockResponse struct { + Result CometBlockResult `json:"result"` } -type TendermintBlockResult struct { - Block *TendermintBlock `json:"block"` +type CometBlockResult struct { + Block *CometBlock `json:"block"` } -type TendermintBlock struct { - Header TendermintBlockHeader `json:"header"` +type CometBlock struct { + Header CometBlockHeader `json:"header"` } -type TendermintBlockHeader struct { +type CometBlockHeader struct { Height string `json:"height"` Time time.Time `json:"time"` } diff --git a/pkg/types/chain_validator.go b/pkg/types/chain_validator.go deleted file mode 100644 index 5f31ec7..0000000 --- a/pkg/types/chain_validator.go +++ /dev/null @@ -1,24 +0,0 @@ -package types - -type ChainValidator struct { - Moniker string - Address string - RawAddress string - AssignedAddress string - RawAssignedAddress string -} - -type ChainValidators []ChainValidator - -func (c ChainValidators) ToMap() map[string]ChainValidator { - valsMap := make(map[string]ChainValidator, len(c)) - - for _, validator := range c { - valsMap[validator.Address] = validator - if validator.RawAssignedAddress != "" { - valsMap[validator.RawAssignedAddress] = validator - } - } - - return valsMap -} diff --git a/pkg/types/consensus_state.go b/pkg/types/consensus_state.go new file mode 100644 index 0000000..fa837a2 --- /dev/null +++ b/pkg/types/consensus_state.go @@ -0,0 +1,52 @@ +package types + +import "time" + +type CometConsensusStateResponse struct { + Result *CometConsensusStateResult `json:"result"` +} + +type CometConsensusStateResult struct { + RoundState *CometConsensusStateRoundState `json:"round_state"` +} + +type CometConsensusStateRoundState struct { + HeightRoundStep string `json:"height/round/step"` + StartTime time.Time `json:"start_time"` + HeightVoteSet []CometConsensusHeightVoteSet `json:"height_vote_set"` + Proposer CometConsensusStateProposer `json:"proposer"` +} + +type CometConsensusHeightVoteSet struct { + Round int `json:"round"` + Prevotes []CometConsensusVote `json:"prevotes"` + Precommits []CometConsensusVote `json:"precommits"` + PrevotesBitArray CometConsensusVoteBitArray `json:"prevotes_bit_array"` + PrecommitsBitArray CometConsensusVoteBitArray `json:"precommits_bit_array"` +} + +type CometConsensusStateProposer struct { + Address string `json:"address"` + Index int `json:"index"` +} + +type ( + CometConsensusVote string + CometConsensusVoteBitArray string +) + +type CometDumpConsensusStateResponse struct { + Result *CometDumpConsensusStateResult `json:"result"` +} + +type CometDumpConsensusStateResult struct { + RoundState *CometDumpConsensusStateRoundState `json:"round_state"` +} + +type CometDumpConsensusStateRoundState struct { + Validators CometDumpConsensusStateRoundStateValidators `json:"validators"` +} + +type CometDumpConsensusStateRoundStateValidators struct { + Validators []CometValidator `json:"validators"` +} diff --git a/pkg/types/converter.go b/pkg/types/converter.go deleted file mode 100644 index 2d4d22c..0000000 --- a/pkg/types/converter.go +++ /dev/null @@ -1,121 +0,0 @@ -package types - -import ( - "errors" - "math/big" - "strings" -) - -func ValidatorsWithLatestRoundFromTendermintResponse( - consensus *ConsensusStateResponse, - tendermintValidators []TendermintValidator, - round int64, -) (ValidatorsWithRoundVote, error) { - lastHeightVoteSet := consensus.Result.RoundState.HeightVoteSet[round] - validators := make(ValidatorsWithRoundVote, len(lastHeightVoteSet.Prevotes)) - - for index, prevote := range lastHeightVoteSet.Prevotes { - precommit := lastHeightVoteSet.Precommits[index] - validator := tendermintValidators[index] - - vp := new(big.Int) - vp, ok := vp.SetString(validator.VotingPower, 10) - if !ok { - return nil, errors.New("error setting string") - } - - validators[index] = ValidatorWithRoundVote{ - Validator: Validator{ - Address: validator.Address, - VotingPower: vp, - }, - RoundVote: RoundVote{ - Address: validator.Address, - Precommit: VoteFromString(precommit), - Prevote: VoteFromString(prevote), - IsProposer: validator.Address == consensus.Result.RoundState.Proposer.Address, - }, - } - } - - totalVP := validators.GetTotalVotingPower() - - for index, validator := range validators { - validators[index].Validator.Index = index - - votingPowerPercent := big.NewFloat(0).SetInt(validator.Validator.VotingPower) - votingPowerPercent = votingPowerPercent.Quo(votingPowerPercent, big.NewFloat(0).SetInt(totalVP)) - votingPowerPercent = votingPowerPercent.Mul(votingPowerPercent, big.NewFloat(100)) - - validators[index].Validator.VotingPowerPercent = votingPowerPercent - } - - return validators, nil -} - -func ValidatorsWithAllRoundsFromTendermintResponse( - consensus *ConsensusStateResponse, - tendermintValidators []TendermintValidator, -) (ValidatorsWithAllRoundsVotes, error) { - validators := make(Validators, len(tendermintValidators)) - for index, validator := range tendermintValidators { - vp := new(big.Int) - vp, ok := vp.SetString(validator.VotingPower, 10) - if !ok { - return ValidatorsWithAllRoundsVotes{}, errors.New("error setting string") - } - - validators[index] = Validator{ - Address: validator.Address, - VotingPower: vp, - } - } - - totalVP := validators.GetTotalVotingPower() - - for index, validator := range validators { - validators[index].Index = index - - votingPowerPercent := big.NewFloat(0).SetInt(validator.VotingPower) - votingPowerPercent = votingPowerPercent.Quo(votingPowerPercent, big.NewFloat(0).SetInt(totalVP)) - votingPowerPercent = votingPowerPercent.Mul(votingPowerPercent, big.NewFloat(100)) - - validators[index].VotingPowerPercent = votingPowerPercent - } - - roundsVotes := make([]RoundVotes, len(consensus.Result.RoundState.HeightVoteSet)) - - for round, roundHeightVoteSet := range consensus.Result.RoundState.HeightVoteSet { - currentRoundVotes := make(RoundVotes, len(roundHeightVoteSet.Prevotes)) - - for index, prevote := range roundHeightVoteSet.Prevotes { - precommit := roundHeightVoteSet.Precommits[index] - validator := tendermintValidators[index] - currentRoundVotes[index] = RoundVote{ - Address: validator.Address, - Precommit: VoteFromString(precommit), - Prevote: VoteFromString(prevote), - IsProposer: validator.Address == consensus.Result.RoundState.Proposer.Address, - } - } - - roundsVotes[round] = currentRoundVotes - } - - return ValidatorsWithAllRoundsVotes{ - Validators: validators, - RoundsVotes: roundsVotes, - }, nil -} - -func VoteFromString(source ConsensusVote) Vote { - if source == "nil-Vote" { - return VotedNil - } - - if strings.Contains(string(source), "SIGNED_MSG_TYPE_PREVOTE(Prevote) 000000000000") { - return VotedZero - } - - return Voted -} diff --git a/pkg/types/genesis.go b/pkg/types/genesis.go index e6f2cd8..b961749 100644 --- a/pkg/types/genesis.go +++ b/pkg/types/genesis.go @@ -10,3 +10,13 @@ type AppState struct { Staking json.RawMessage `json:"staking"` Genutil json.RawMessage `json:"genutil"` } + +type CometGenesisChunkResponse struct { + Result *CometGenesisChunkResult `json:"result"` +} + +type CometGenesisChunkResult struct { + Chunk string `json:"string"` + Total string `json:"total"` + Data []byte `json:"data"` +} diff --git a/pkg/types/net_info.go b/pkg/types/net_info.go new file mode 100644 index 0000000..817ee84 --- /dev/null +++ b/pkg/types/net_info.go @@ -0,0 +1,206 @@ +package types + +import ( + "fmt" + "net/url" + "reflect" + "strconv" + "time" + + cmtbytes "github.com/cometbft/cometbft/libs/bytes" +) + +type NetInfo struct { + Listening bool `mapstructure:"listening"` + Listeners []string `mapstructure:"listeners"` + NPeers string `mapstructure:"n_peers"` + Peers []Peer `mapstructure:"peers"` +} + +type Peer struct { + NodeInfo DefaultNodeInfo `mapstructure:"node_info"` + IsOutbound bool `mapstructure:"is_outbound"` + ConnectionStatus ConnectionStatus `mapstructure:"connection_status"` + RemoteIP string `mapstructure:"remote_ip"` +} + +func (p Peer) URL() string { + u, err := url.Parse(p.NodeInfo.Other.RPCAddress) + if err != nil { + return "http://" + p.RemoteIP + ":26657" + } + return "http" + "://" + p.RemoteIP + ":" + u.Port() +} + +type DefaultNodeInfo struct { + ProtocolVersion ProtocolVersion `mapstructure:"protocol_version"` + + // Authenticate + // TODO: replace with NetAddress + DefaultNodeID ID `mapstructure:"id"` // authenticated identifier + ListenAddr string `mapstructure:"listen_addr"` // accepting incoming + + // Check compatibility. + // Channels are HexBytes so easier to read as JSON + Network string `mapstructure:"network"` // network/chain ID + Version string `mapstructure:"version"` // major.minor.revision + Channels cmtbytes.HexBytes `mapstructure:"channels"` // channels this node knows about + + // ASCIIText fields + Moniker string `mapstructure:"moniker"` // arbitrary moniker + Other DefaultNodeInfoOther `mapstructure:"other"` // other application specific data +} + +type DefaultNodeInfoOther struct { + TxIndex string `mapstructure:"tx_index"` + RPCAddress string `mapstructure:"rpc_address"` +} + +type ProtocolVersion struct { + P2P int64 `mapstructure:"p2p"` + Block int64 `mapstructure:"block"` + App int64 `mapstructure:"app"` +} + +type ID string + +type ConnectionStatus struct { + Duration NanoDuration `json:"duration"` + SendMonitor FlowStatus `json:"send_monitor"` + RecvMonitor FlowStatus `json:"recv_monitor"` + Channels []ChannelStatus `json:"channels"` +} + +type ChannelStatus struct { + ID byte `json:"id"` + SendQueueCapacity string `json:"send_queue_capacity"` + SendQueueSize string `json:"send_queue_size"` + Priority string `json:"priority"` + RecentlySent string `json:"recently_sent"` +} + +type FlowStatus struct { + Start CustomTime `json:"start"` // Transfer start time + Bytes ByteSize `json:"bytes"` // Total number of bytes transferred + Samples ByteSize `json:"samples"` // Total number of samples taken + InstRate ByteSize `json:"inst_rate"` // Instantaneous transfer rate + CurRate ByteSize `json:"cur_rate"` // Current transfer rate (EMA of InstRate) + AvgRate ByteSize `json:"avg_rate"` // Average transfer rate (Bytes / Duration) + PeakRate ByteSize `json:"peak_rate"` // Maximum instantaneous transfer rate + BytesRem ByteSize `json:"bytes_rem"` // Number of bytes remaining in the transfer + Duration NanoDuration `json:"duration"` // Time period covered by the statistics + Idle NanoDuration `json:"idle"` // Time since the last transfer of at least 1 byte + TimeRem NanoDuration `json:"time_rem"` // Estimated time to completion + Progress Percent `json:"progress"` // Overall transfer progress + Active bool `json:"active"` // Flag indicating an active transfer +} + +type NanoDuration time.Duration + +func (nd *NanoDuration) UnmarshalJSON(b []byte) error { + // Remove quotes from the string + // s := string(b) + // s = s[1 : len(s)-1] + + // Parse the string as an int64 + nanos, err := strconv.ParseInt(string(b), 10, 64) + if err != nil { + return fmt.Errorf("invalid duration: %v", err) + } + + // Convert nanoseconds to time.Duration + *nd = NanoDuration(time.Duration(nanos)) + return nil +} + +// String returns the string representation of the duration. +func (nd NanoDuration) String() string { + return time.Duration(nd).String() +} + +type CustomTime struct { + time.Time +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (ct *CustomTime) UnmarshalJSON(b []byte) error { + // Remove quotes + s := string(b) + s = s[1 : len(s)-1] + + // Parse the time string + t, err := time.Parse("2006-01-02T15:04:05.99Z", s) + if err != nil { + return err + } + + ct.Time = t + return nil +} + +func StringToCustomTimeHookFunc( + f reflect.Type, + t reflect.Type, + data any, +) (any, error) { + if f.Kind() != reflect.String { + return data, nil + } else if t != reflect.TypeOf(CustomTime{}) { + return data, nil + } + + str := data.(string) + result, err := time.Parse("2006-01-02T15:04:05.99Z", str) + if err != nil { + return nil, fmt.Errorf("failed to parse time: %v", err) + } + return CustomTime{Time: result}, nil +} + +func (ct CustomTime) String() string { + return ct.Time.Format("2006-01-02T15:04:05.99Z") +} + +// Percent represents a percentage in increments of 1/1000th of a percent. +type Percent uint32 + +func (p Percent) Float() float64 { + return float64(p) * 1e-3 +} + +func (p Percent) String() string { + var buf [12]byte + b := strconv.AppendUint(buf[:0], uint64(p)/1000, 10) + n := len(b) + b = strconv.AppendUint(b, 1000+uint64(p)%1000, 10) + b[n] = '.' + return string(append(b, '%')) +} + +type ByteSize int64 + +const ( + _ = iota // ignore first value by assigning to blank identifier + KB ByteSize = 1 << (10 * iota) + MB + GB + TB + PB +) + +func (b ByteSize) String() string { + switch { + case b >= PB: + return fmt.Sprintf("%.2fpb", float64(b)/float64(PB)) + case b >= TB: + return fmt.Sprintf("%.2ftb", float64(b)/float64(TB)) + case b >= GB: + return fmt.Sprintf("%.2fgb", float64(b)/float64(GB)) + case b >= MB: + return fmt.Sprintf("%.2fmb", float64(b)/float64(MB)) + case b >= KB: + return fmt.Sprintf("%.2fkb", float64(b)/float64(KB)) + default: + return fmt.Sprintf("%db", b) + } +} diff --git a/pkg/types/node_info.go b/pkg/types/node_info.go new file mode 100644 index 0000000..667bdd6 --- /dev/null +++ b/pkg/types/node_info.go @@ -0,0 +1,20 @@ +package types + +type CometNodeStatusResponse struct { + Result CometNodeStatus `json:"result"` +} + +type CometNodeStatus struct { + NodeInfo CometNodeInfo `json:"node_info"` + ValidatorInfo CometValidatorInfo `json:"validator_info"` +} + +type CometNodeInfo struct { + ID string `json:"id"` + Version string `json:"version"` + Network string `json:"network"` + Moniker string `json:"moniker"` + Other struct { + RPCAddress string `json:"rpc_address"` + } `json:"other"` +} diff --git a/pkg/types/state.go b/pkg/types/state.go index 9705023..22398f6 100644 --- a/pkg/types/state.go +++ b/pkg/types/state.go @@ -2,23 +2,47 @@ package types import ( "fmt" - "main/pkg/utils" + "maps" + "math/big" "strings" + "sync" "time" + + "main/pkg/utils" + + butils "github.com/brynbellomy/go-utils" + cptypes "github.com/cometbft/cometbft/proto/tendermint/types" + ctypes "github.com/cometbft/cometbft/types" + "github.com/rs/zerolog" ) type State struct { - Height int64 - Round int64 - Step int64 - Validators *ValidatorsWithRoundVote - ValidatorsWithAllRoundsVotes *ValidatorsWithAllRoundsVotes - ChainValidators *ChainValidators - ChainInfo *TendermintStatusResult - StartTime time.Time - Upgrade *Upgrade - BlockTime time.Duration + logger zerolog.Logger + + Height int64 + Round int64 + Step int64 + StartTime time.Time + + TMValidators TMValidators + validatorsByPeerID map[string]TMValidator + + ChainInfo *CometNodeStatus + Upgrade *Upgrade + BlockTime time.Duration + NetInfo *NetInfo + + // Vote and proposal tracking + VotesByRound *RoundDataMap + ProposalsByRound *butils.SortedMap[int64, *butils.SortedMap[int32, butils.Set[string]]] + // RPC management + currentRPC string + knownRPCs *butils.OrderedMap[string, RPC] + rpcPeers *butils.OrderedMap[string, []Peer] + muRPCs *sync.RWMutex + + // Error tracking ConsensusStateError error ValidatorsError error ChainValidatorsError error @@ -26,51 +50,136 @@ type State struct { ChainInfoError error } -func NewState() *State { - return &State{ - Height: 0, - Round: 0, - Step: 0, - Validators: nil, - ChainValidators: nil, - StartTime: time.Now(), - BlockTime: 0, +type RPC struct { + ID string `json:"id"` + IP string `json:"ip"` + URL string `json:"url"` + Moniker string `json:"moniker"` + ValidatorAddress string `json:"validatorAddress"` + ValidatorMoniker string `json:"validatorMoniker"` +} + +func NewRPCFromPeer(peer Peer) RPC { + return RPC{ + ID: string(peer.NodeInfo.DefaultNodeID), + IP: peer.RemoteIP, + URL: peer.URL(), + Moniker: peer.NodeInfo.Moniker, } } -func (s *State) SetTendermintResponse( - consensus *ConsensusStateResponse, - tendermintValidators []TendermintValidator, -) error { - hrsSplit := strings.Split(consensus.Result.RoundState.HeightRoundStep, "/") +func NewState(firstRPC string, logger zerolog.Logger) *State { + return &State{ + logger: logger.With().Str("component", "state").Logger(), + Height: 0, + Round: 0, + Step: 0, + StartTime: time.Now(), + TMValidators: TMValidators{}, + VotesByRound: NewRoundDataMap(), + ProposalsByRound: butils.NewSortedMap[int64, *butils.SortedMap[int32, butils.Set[string]]](), + BlockTime: 0, + validatorsByPeerID: make(map[string]TMValidator), + currentRPC: firstRPC, + knownRPCs: butils.NewOrderedMap[string, RPC](), + rpcPeers: butils.NewOrderedMap[string, []Peer](), + muRPCs: &sync.RWMutex{}, + } +} - s.Height = utils.MustParseInt64(hrsSplit[0]) - s.Round = utils.MustParseInt64(hrsSplit[1]) - s.Step = utils.MustParseInt64(hrsSplit[2]) - s.StartTime = consensus.Result.RoundState.StartTime +func (s *State) CurrentRPC() RPC { + s.muRPCs.RLock() + defer s.muRPCs.RUnlock() - validators, err := ValidatorsWithLatestRoundFromTendermintResponse(consensus, tendermintValidators, s.Round) - if err != nil { - return err + rpc, ok := s.knownRPCs.Get(s.currentRPC) + if !ok { + return RPC{URL: s.currentRPC} } + return rpc +} - s.Validators = &validators +func (s *State) SetCurrentRPCURL(rpcURL string) { + s.muRPCs.Lock() + defer s.muRPCs.Unlock() - validatorsWithAllRounds, err := ValidatorsWithAllRoundsFromTendermintResponse(consensus, tendermintValidators) - if err != nil { - return err - } + s.currentRPC = rpcURL +} + +func (s *State) KnownRPCByURL(url string) (RPC, bool) { + s.muRPCs.RLock() + defer s.muRPCs.RUnlock() + + rpc, ok := s.knownRPCs.Get(url) + return rpc, ok +} + +func (s *State) KnownRPCs() *butils.OrderedMap[string, RPC] { + s.muRPCs.RLock() + defer s.muRPCs.RUnlock() + + return s.knownRPCs.Copy() +} + +func (s *State) AddKnownRPC(rpc RPC) { + s.muRPCs.Lock() + defer s.muRPCs.Unlock() + + s.knownRPCs.Set(rpc.URL, rpc) +} + +func (s *State) IsKnownRPC(rpcURL string) bool { + s.muRPCs.RLock() + defer s.muRPCs.RUnlock() + + _, ok := s.knownRPCs.Get(rpcURL) + return ok +} - s.ValidatorsWithAllRoundsVotes = &validatorsWithAllRounds +func (s *State) RPCAtIndex(index int) (RPC, bool) { + s.muRPCs.RLock() + defer s.muRPCs.RUnlock() - return nil + _, rpc, ok := s.knownRPCs.GetByIndex(index) + return rpc, ok } -func (s *State) SetChainValidators(validators *ChainValidators) { - s.ChainValidators = validators +func (s *State) AddRPCPeers(rpcURL string, peers []Peer) { + s.muRPCs.Lock() + defer s.muRPCs.Unlock() + + s.rpcPeers.Set(rpcURL, peers) +} + +func (s *State) RPCPeers(rpcURL string) []Peer { + s.muRPCs.RLock() + defer s.muRPCs.RUnlock() + + peers, _ := s.rpcPeers.Get(rpcURL) + return peers +} + +func (s *State) ValidatorByPeerID(peerID string) (TMValidator, bool) { + val, ok := s.validatorsByPeerID[strings.ToLower(peerID)] + return val, ok +} + +func (s *State) ValidatorsByPeerID() map[string]TMValidator { + return maps.Clone(s.validatorsByPeerID) } -func (s *State) SetChainInfo(info *TendermintStatusResult) { +func (s *State) AddCometBFTEvents(events []ctypes.TMEventData) { + for _, event := range events { + switch x := event.(type) { + case ctypes.EventDataNewRound: + s.VotesByRound.AddProposer(x.Height, x.Round, x.Proposer.Address.String()) + + case ctypes.EventDataVote: + s.VotesByRound.AddVote(x.Vote.Height, x.Vote.Round, x.Vote.ValidatorAddress.String(), x.Vote.Type, x.Vote.BlockID) + } + } +} + +func (s *State) SetChainInfo(info *CometNodeStatus) { s.ChainInfo = info } @@ -82,6 +191,10 @@ func (s *State) SetBlockTime(blockTime time.Duration) { s.BlockTime = blockTime } +func (s *State) SetNetInfo(info *NetInfo) { + s.NetInfo = info +} + func (s *State) SetConsensusStateError(err error) { s.ConsensusStateError = err } @@ -103,7 +216,7 @@ func (s *State) SerializeConsensus(timezone *time.Location) string { return fmt.Sprintf(" consensus state error: %s", s.ConsensusStateError) } - if s.Validators == nil { + if len(s.TMValidators) == 0 { return "" } @@ -115,50 +228,16 @@ func (s *State) SerializeConsensus(timezone *time.Location) string { utils.ZeroOrPositiveDuration(utils.SerializeDuration(time.Since(s.StartTime))), utils.SerializeTime(s.StartTime.In(timezone)), )) + sb.WriteString(fmt.Sprintf( " prevote consensus (total/agreeing): %.2f / %.2f\n", - s.Validators.GetTotalVotingPowerPrevotedPercent(true), - s.Validators.GetTotalVotingPowerPrevotedPercent(false), + s.GetTotalVotingPowerPrevotedPercent(true), + s.GetTotalVotingPowerPrevotedPercent(false), )) sb.WriteString(fmt.Sprintf( " precommit consensus (total/agreeing): %.2f / %.2f\n", - s.Validators.GetTotalVotingPowerPrecommittedPercent(true), - s.Validators.GetTotalVotingPowerPrecommittedPercent(false), - )) - - prevoted := 0 - precommitted := 0 - prevotedAgreed := 0 - precommittedAgreed := 0 - - for _, validator := range *s.Validators { - if validator.RoundVote.Prevote != VotedNil { - prevoted += 1 - } - if validator.RoundVote.Precommit != VotedNil { - precommitted += 1 - } - - if validator.RoundVote.Prevote == Voted { - prevotedAgreed += 1 - } - - if validator.RoundVote.Precommit == Voted { - precommittedAgreed += 1 - } - } - - sb.WriteString(fmt.Sprintf( - " prevoted/precommitted: %d/%d (out of %d)\n", - prevoted, - precommitted, - len(*s.Validators), - )) - sb.WriteString(fmt.Sprintf( - " prevoted/precommitted agreed: %d/%d (out of %d)\n", - prevotedAgreed, - precommittedAgreed, - len(*s.Validators), + s.GetTotalVotingPowerPrecommittedPercent(true), + s.GetTotalVotingPowerPrecommittedPercent(false), )) sb.WriteString(fmt.Sprintf(" last updated at: %s\n", utils.SerializeTime(time.Now().In(timezone)))) @@ -169,6 +248,9 @@ func (s *State) SerializeConsensus(timezone *time.Location) string { func (s *State) SerializeChainInfo(timezone *time.Location) string { var sb strings.Builder + sb.WriteString(fmt.Sprintf(" rpc: %v\n", s.CurrentRPC().URL)) + sb.WriteString(fmt.Sprintf(" (%v)\n\n", s.CurrentRPC().Moniker)) + if s.ChainInfoError != nil { sb.WriteString(fmt.Sprintf(" chain info fetch error: %s\n", s.ChainInfoError.Error())) } else if s.ChainInfo != nil { @@ -264,11 +346,11 @@ func (s *State) SerializeProgressbar(width int, height int, prefix string, progr } func (s *State) SerializePrevotesProgressbar(width int, height int) string { - if s.Validators == nil { + if len(s.TMValidators) == 0 { return "" } - prevotePercent := s.Validators.GetTotalVotingPowerPrevotedPercent(true) + prevotePercent := s.GetTotalVotingPowerPrevotedPercent(true) prevotePercentFloat, _ := prevotePercent.Float64() prevotePercentInt := int(prevotePercentFloat) @@ -276,74 +358,241 @@ func (s *State) SerializePrevotesProgressbar(width int, height int) string { } func (s *State) SerializePrecommitsProgressbar(width int, height int) string { - if s.Validators == nil { + if len(s.TMValidators) == 0 { return "" } - precommitPercent := s.Validators.GetTotalVotingPowerPrecommittedPercent(true) + precommitPercent := s.GetTotalVotingPowerPrecommittedPercent(true) precommitPercentFloat, _ := precommitPercent.Float64() precommitPercentInt := int(precommitPercentFloat) return s.SerializeProgressbar(width, height, "Precommits: ", precommitPercentInt) } -func (s *State) GetValidatorsWithInfo() ValidatorsWithInfo { - if s.Validators == nil { - return ValidatorsWithInfo{} - } +// TMValidator-based methods + +// GetTMValidators returns the unified validator collection. +func (s *State) GetTMValidators() TMValidators { + return s.TMValidators +} - validators := make(ValidatorsWithInfo, len(*s.Validators)) +// SetTMValidators sets the unified validator collection. +func (s *State) SetTMValidators(validators TMValidators) { + s.TMValidators = validators +} - for index, validator := range *s.Validators { - validators[index] = ValidatorWithInfo{ - Validator: validator.Validator, - RoundVote: validator.RoundVote, +// UpdateTMValidatorsWithRoundVotes updates current round vote state for all validators. +func (s *State) UpdateTMValidatorsWithRoundVotes(height int64, round int32) { + for i := range s.TMValidators { + validator := &s.TMValidators[i] + + // Create current round vote state from VotesByRound data + roundVoteState := &RoundVoteState{ + Address: validator.GetDisplayAddress(), + Prevote: s.VotesByRound.GetVote(height, round, validator.GetDisplayAddress(), cptypes.PrevoteType), + Precommit: s.VotesByRound.GetVote(height, round, validator.GetDisplayAddress(), cptypes.PrecommitType), + IsProposer: s.VotesByRound.GetProposers(height, round).Has(validator.GetDisplayAddress()), } + + validator.CurrentRoundVote = roundVoteState } +} + +type RoundDataMap struct { + mu *sync.RWMutex + heights *butils.SortedMap[int64, *butils.SortedMap[int32, *RoundData]] +} + +type RoundData struct { + Proposers butils.Set[string] + Votes map[string]map[cptypes.SignedMsgType]ctypes.BlockID +} + +func NewRoundDataMap() *RoundDataMap { + return &RoundDataMap{ + mu: &sync.RWMutex{}, + heights: butils.NewSortedMap[int64, *butils.SortedMap[int32, *RoundData]](), + } +} + +type HeightAndRound struct { + Height int64 + Round int32 +} - if s.ChainValidators == nil { - return validators +func (v *RoundDataMap) Iter() func(yield func(HeightAndRound, *RoundData) bool) { + return func(yield func(hr HeightAndRound, rd *RoundData) bool) { + v.mu.RLock() + defer v.mu.RUnlock() + + for height, heightMap := range v.heights.Iter() { + for round, roundData := range heightMap.Iter() { + if !yield(HeightAndRound{height, round}, roundData) { + return + } + } + } } +} - chainValidatorsMap := s.ChainValidators.ToMap() - for index, validator := range *s.Validators { - if chainValidator, ok := chainValidatorsMap[validator.Validator.Address]; ok { - validators[index].ChainValidator = &chainValidator +func (v *RoundDataMap) ReverseIter() func(yield func(HeightAndRound, *RoundData) bool) { + return func(yield func(hr HeightAndRound, rd *RoundData) bool) { + v.mu.RLock() + defer v.mu.RUnlock() + + for height, heightMap := range v.heights.ReverseIter() { + for round, roundData := range heightMap.ReverseIter() { + if !yield(HeightAndRound{height, round}, roundData) { + return + } + } } } +} - return validators +func (v *RoundDataMap) AddProposer(height int64, round int32, proposer string) { + v.mu.Lock() + defer v.mu.Unlock() + + roundData := v.upsertRoundData(height, round) + roundData.Proposers.Add(proposer) } -func (s *State) GetValidatorsWithInfoAndAllRoundVotes() ValidatorsWithInfoAndAllRoundVotes { - if s.ValidatorsWithAllRoundsVotes == nil { - return ValidatorsWithInfoAndAllRoundVotes{} +func (v *RoundDataMap) GetProposers(height int64, round int32) butils.Set[string] { + v.mu.RLock() + defer v.mu.RUnlock() + + roundMap, ok := v.heights.Get(height) + if !ok { + return nil + } + + roundData, ok := roundMap.Get(round) + if !ok { + return nil } - validators := make([]ValidatorWithChainValidator, len(s.ValidatorsWithAllRoundsVotes.Validators)) + return roundData.Proposers.Copy() +} + +func (v *RoundDataMap) AddVote(height int64, round int32, validator string, msgType cptypes.SignedMsgType, blockID ctypes.BlockID) { + v.mu.Lock() + defer v.mu.Unlock() + + roundData := v.upsertRoundData(height, round) + + votesMap, ok := roundData.Votes[validator] + if !ok { + votesMap = map[cptypes.SignedMsgType]ctypes.BlockID{} + roundData.Votes[validator] = votesMap + } + + votesMap[msgType] = blockID +} - for index, validator := range s.ValidatorsWithAllRoundsVotes.Validators { - validators[index] = ValidatorWithChainValidator{ - Validator: validator, +func (v *RoundDataMap) GetVote(height int64, round int32, validator string, msgType cptypes.SignedMsgType) VoteState { + v.mu.RLock() + defer v.mu.RUnlock() + + roundMap, ok := v.heights.Get(height) + if !ok { + return VoteStateNone + } + + roundData, ok := roundMap.Get(round) + if !ok { + return VoteStateNone + } + + votesMap, ok := roundData.Votes[validator] + if !ok { + return VoteStateNone + } + + blockID, ok := votesMap[msgType] + if !ok { + return VoteStateNone + } else if blockID.IsZero() { + return VoteStateNil + } + return VoteStateForBlock +} + +func (v *RoundDataMap) upsertRoundData(height int64, round int32) *RoundData { + roundMap, ok := v.heights.Get(height) + if !ok { + roundMap = butils.NewSortedMap[int32, *RoundData]() + v.heights.Insert(height, roundMap) + } + + roundData, ok := roundMap.Get(round) + if !ok { + roundData = &RoundData{ + Proposers: butils.NewSet[string](), + Votes: make(map[string]map[cptypes.SignedMsgType]ctypes.BlockID), } + roundMap.Insert(round, roundData) } - if s.ChainValidators == nil { - return ValidatorsWithInfoAndAllRoundVotes{ - Validators: validators, - RoundsVotes: s.ValidatorsWithAllRoundsVotes.RoundsVotes, + return roundData +} + +// GetTotalVotingPowerPrevotedPercent calculates percentage using RoundDataMap. +func (s *State) GetTotalVotingPowerPrevotedPercent(countDisagreeing bool) *big.Float { + if len(s.TMValidators) == 0 { + return big.NewFloat(0) + } + + prevoted := big.NewInt(0) + totalVP := big.NewInt(0) + + for _, validator := range s.TMValidators { + totalVP = totalVP.Add(totalVP, big.NewInt(validator.VotingPower)) + + // Query RoundDataMap for current vote state + prevoteState := s.VotesByRound.GetVote(s.Height, int32(s.Round), validator.GetDisplayAddress(), cptypes.PrevoteType) + if prevoteState == VoteStateForBlock || (countDisagreeing && prevoteState == VoteStateNil) { + prevoted = prevoted.Add(prevoted, big.NewInt(validator.VotingPower)) } } - chainValidatorsMap := s.ChainValidators.ToMap() - for index, validator := range s.ValidatorsWithAllRoundsVotes.Validators { - if chainValidator, ok := chainValidatorsMap[validator.Address]; ok { - validators[index].ChainValidator = &chainValidator + if totalVP.Cmp(big.NewInt(0)) == 0 { + return big.NewFloat(0) + } + + votingPowerPercent := big.NewFloat(0).SetInt(prevoted) + votingPowerPercent = votingPowerPercent.Quo(votingPowerPercent, big.NewFloat(0).SetInt(totalVP)) + votingPowerPercent = votingPowerPercent.Mul(votingPowerPercent, big.NewFloat(100)) + + return votingPowerPercent +} + +// GetTotalVotingPowerPrecommittedPercent calculates percentage using RoundDataMap. +func (s *State) GetTotalVotingPowerPrecommittedPercent(countDisagreeing bool) *big.Float { + if len(s.TMValidators) == 0 { + return big.NewFloat(0) + } + + precommitted := big.NewInt(0) + totalVP := big.NewInt(0) + + for _, validator := range s.TMValidators { + totalVP = totalVP.Add(totalVP, big.NewInt(validator.VotingPower)) + + // Query RoundDataMap for current vote state + precommitState := s.VotesByRound.GetVote(s.Height, int32(s.Round), validator.GetDisplayAddress(), cptypes.PrecommitType) + if precommitState == VoteStateForBlock || (countDisagreeing && precommitState == VoteStateNil) { + precommitted = precommitted.Add(precommitted, big.NewInt(validator.VotingPower)) } } - return ValidatorsWithInfoAndAllRoundVotes{ - Validators: validators, - RoundsVotes: s.ValidatorsWithAllRoundsVotes.RoundsVotes, + if totalVP.Cmp(big.NewInt(0)) == 0 { + return big.NewFloat(0) } + + votingPowerPercent := big.NewFloat(0).SetInt(precommitted) + votingPowerPercent = votingPowerPercent.Quo(votingPowerPercent, big.NewFloat(0).SetInt(totalVP)) + votingPowerPercent = votingPowerPercent.Mul(votingPowerPercent, big.NewFloat(100)) + + return votingPowerPercent } diff --git a/pkg/types/status.go b/pkg/types/status.go deleted file mode 100644 index cb26bd1..0000000 --- a/pkg/types/status.go +++ /dev/null @@ -1,14 +0,0 @@ -package types - -type TendermintStatusResponse struct { - Result TendermintStatusResult `json:"result"` -} - -type TendermintStatusResult struct { - NodeInfo TendermintNodeInfo `json:"node_info"` -} - -type TendermintNodeInfo struct { - Version string `json:"version"` - Network string `json:"network"` -} diff --git a/pkg/types/tendermint_consensus.go b/pkg/types/tendermint_consensus.go deleted file mode 100644 index 4684984..0000000 --- a/pkg/types/tendermint_consensus.go +++ /dev/null @@ -1,36 +0,0 @@ -package types - -import ( - "time" -) - -type ConsensusStateResponse struct { - Result *ConsensusStateResult `json:"result"` -} - -type ConsensusStateResult struct { - RoundState *ConsensusStateRoundState `json:"round_state"` -} - -type ConsensusStateRoundState struct { - HeightRoundStep string `json:"height/round/step"` - StartTime time.Time `json:"start_time"` - HeightVoteSet []ConsensusHeightVoteSet `json:"height_vote_set"` - Proposer ConsensusStateProposer `json:"proposer"` -} - -type ConsensusHeightVoteSet struct { - Round int `json:"round"` - Prevotes []ConsensusVote `json:"prevotes"` - Precommits []ConsensusVote `json:"precommits"` - PrevotesBitArray ConsensusVoteBitArray `json:"prevotes_bit_array"` - PrecommitsBitArray ConsensusVoteBitArray `json:"precommits_bit_array"` -} - -type ConsensusStateProposer struct { - Address string `json:"address"` - Index int `json:"index"` -} - -type ConsensusVote string -type ConsensusVoteBitArray string diff --git a/pkg/types/tendermint_dump_consensus.go b/pkg/types/tendermint_dump_consensus.go deleted file mode 100644 index 3bb4d94..0000000 --- a/pkg/types/tendermint_dump_consensus.go +++ /dev/null @@ -1,16 +0,0 @@ -package types - -type DumpConsensusStateResponse struct { - Result *DumpConsensusStateResult `json:"result"` -} - -type DumpConsensusStateResult struct { - RoundState *DumpConsensusStateRoundState `json:"round_state"` -} - -type DumpConsensusStateRoundState struct { - Validators DumpConsensusStateRoundStateValidators `json:"validators"` -} -type DumpConsensusStateRoundStateValidators struct { - Validators []TendermintValidator `json:"validators"` -} diff --git a/pkg/types/tendermint_genesis_chunked.go b/pkg/types/tendermint_genesis_chunked.go deleted file mode 100644 index c0c1cad..0000000 --- a/pkg/types/tendermint_genesis_chunked.go +++ /dev/null @@ -1,11 +0,0 @@ -package types - -type TendermintGenesisChunkResponse struct { - Result *TendermintGenesisChunkResult `json:"result"` -} - -type TendermintGenesisChunkResult struct { - Chunk string `json:"string"` - Total string `json:"total"` - Data []byte `json:"data"` -} diff --git a/pkg/types/tendermint_validator.go b/pkg/types/tendermint_validator.go deleted file mode 100644 index cb8c688..0000000 --- a/pkg/types/tendermint_validator.go +++ /dev/null @@ -1,22 +0,0 @@ -package types - -type ValidatorsResponse struct { - Result *ValidatorsResult `json:"result"` - Error *ValidatorsError `json:"error"` -} - -type ValidatorsError struct { - Message string `json:"message"` - Data string `json:"data"` -} - -type ValidatorsResult struct { - Count string `json:"count"` - Total string `json:"total"` - Validators []TendermintValidator `json:"validators"` -} - -type TendermintValidator struct { - Address string `json:"address"` - VotingPower string `json:"voting_power"` -} diff --git a/pkg/types/upgrade.go b/pkg/types/upgrade.go index 6d05e0f..96b10a0 100644 --- a/pkg/types/upgrade.go +++ b/pkg/types/upgrade.go @@ -1,6 +1,6 @@ package types type Upgrade struct { - Name string - Height int64 + Name string `json:"name"` + Height int64 `json:"height"` } diff --git a/pkg/types/validator.comet.go b/pkg/types/validator.comet.go new file mode 100644 index 0000000..8af7ae1 --- /dev/null +++ b/pkg/types/validator.comet.go @@ -0,0 +1,33 @@ +package types + +type CometValidatorInfo struct { + Address string `json:"address"` + VotingPower string `json:"voting_power"` +} + +type CometValidatorsResponse struct { + Result *CometValidatorsResult `json:"result"` + Error *CometValidatorsError `json:"error"` +} + +type CometValidatorsError struct { + Message string `json:"message"` + Data string `json:"data"` +} + +type CometValidatorsResult struct { + Count string `json:"count"` + Total string `json:"total"` + Validators []CometValidator `json:"validators"` +} + +type CometValidator struct { + Address string `json:"address"` + VotingPower string `json:"voting_power"` + PubKey CometValidatorPubKey `json:"pub_key"` +} + +type CometValidatorPubKey struct { + Type string `json:"type"` + PubKeyBase64 string `json:"value"` +} diff --git a/pkg/types/validator.cosmos.go b/pkg/types/validator.cosmos.go new file mode 100644 index 0000000..cad0cc7 --- /dev/null +++ b/pkg/types/validator.cosmos.go @@ -0,0 +1,48 @@ +package types + +import ( + "strings" + + cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdkTypes "github.com/cosmos/cosmos-sdk/types" +) + +type CosmosValidator struct { + Moniker string + ConsensusPubkey cryptotypes.PubKey + CometConsensusPubkey cmtprotocrypto.PublicKey + OperatorAddress string + ConsensusAddress string + AssignedAddress string + RawAssignedAddress string +} + +type CosmosValidators []CosmosValidator + +func (c CosmosValidators) ToMap() map[string]CosmosValidator { + valsMap := make(map[string]CosmosValidator, len(c)) + + for _, validator := range c { + // Index by operator address (bech32) + valsMap[validator.Address] = validator + + // Index by consensus address for TMValidator matching + if validator.RawAddress != "" { + valsMap[validator.RawAddress] = validator + + // Also index by hex format for CometBFT TMValidator matching + if consAddr, err := sdkTypes.ConsAddressFromBech32(validator.RawAddress); err == nil { + hexAddr := strings.ToUpper(consAddr.String()) + valsMap[hexAddr] = validator + } + } + + // Index by assigned address if available + if validator.RawAssignedAddress != "" { + valsMap[validator.RawAssignedAddress] = validator + } + } + + return valsMap +} diff --git a/pkg/types/validator.go b/pkg/types/validator.go index 12a0940..7501aca 100644 --- a/pkg/types/validator.go +++ b/pkg/types/validator.go @@ -2,95 +2,113 @@ package types import ( "fmt" - "main/pkg/utils" "math/big" "strconv" + + "main/pkg/utils" + + "github.com/cometbft/cometbft/p2p" + ctypes "github.com/cometbft/cometbft/types" ) -type Validator struct { - Index int - Address string - VotingPower *big.Int - VotingPowerPercent *big.Float +type TMValidator struct { + CometValidator *ctypes.Validator + CosmosValidator *CosmosValidator + + Index int // Display index for UI + VotingPowerPercent *big.Float // Calculated percentage + PeerID p2p.ID // P2P network ID + + // Current round vote state (optional) + CurrentRoundVote *RoundVoteState // Vote state for current round } -type Validators []Validator +// GetDisplayAddress returns the best available address for display. +func (v TMValidator) GetDisplayAddress() string { + return v.CosmosValidator.OperatorAddress +} -type RoundVote struct { - Address string - Prevote Vote - Precommit Vote - IsProposer bool +// GetDisplayName returns the best available name for display. +func (v TMValidator) GetDisplayName() string { + if v.CosmosValidator != nil && v.CosmosValidator.Moniker != "" { + return v.CosmosValidator.Moniker + } + // Truncate address for display + addr := v.GetDisplayAddress() + if len(addr) > 10 { + return addr[:6] + "..." + addr[len(addr)-4:] + } + return addr +} + +// HasAssignedKey returns true if validator has an assigned consensus key. +func (v TMValidator) HasAssignedKey() bool { + return v.CosmosValidator != nil && v.CosmosValidator.AssignedAddress != "" } -func (v RoundVote) Equals(other RoundVote) bool { - if v.Address != other.Address { - return false +// Serialize returns formatted string for display (replaces ValidatorWithInfo.Serialize). +func (v TMValidator) Serialize(disableEmojis bool) string { + name := v.GetDisplayName() + if v.HasAssignedKey() { + emoji := "🔑" + if disableEmojis { + emoji = "[k[]" + } + name = emoji + " " + name } - if v.Prevote != other.Prevote { - return false + // If no current round vote state, show placeholders + prevoteStr := "❌" + precommitStr := "❌" + if disableEmojis { + prevoteStr = "[ []" + precommitStr = "[ []" } - if v.Precommit != other.Precommit { - return false + if v.CurrentRoundVote != nil { + prevoteStr = v.CurrentRoundVote.Prevote.Serialize(disableEmojis) + precommitStr = v.CurrentRoundVote.Precommit.Serialize(disableEmojis) } - if v.IsProposer != other.IsProposer { - return false + // Format voting power percentage + votingPowerStr := "0.00" + if v.VotingPowerPercent != nil { + votingPowerStr = v.VotingPowerPercent.Text('f', 2) } - return false -} -func (v RoundVote) Serialize(disableEmojis bool) string { return fmt.Sprintf( - " %s %s", - v.Prevote.Serialize(disableEmojis), - v.Precommit.Serialize(disableEmojis), + " %s %s %s %s%% %s ", + prevoteStr, + precommitStr, + utils.RightPadAndTrim(strconv.Itoa(v.Index+1), 3), + utils.RightPadAndTrim(votingPowerStr, 6), + utils.LeftPadAndTrim(name, 25), ) } -type RoundVotes []RoundVote - -type ValidatorWithRoundVote struct { - Validator Validator - RoundVote RoundVote -} - -type ValidatorsWithRoundVote []ValidatorWithRoundVote - -type ValidatorsWithAllRoundsVotes struct { - Validators []Validator - RoundsVotes []RoundVotes -} - -func (v Validators) GetTotalVotingPower() *big.Int { - sum := big.NewInt(0) - - for _, validator := range v { - sum = sum.Add(sum, validator.VotingPower) - } +type TMValidators []TMValidator - return sum -} -func (v ValidatorsWithRoundVote) GetTotalVotingPower() *big.Int { +// GetTotalVotingPower returns the sum of all validators' voting power. +func (v TMValidators) GetTotalVotingPower() *big.Int { sum := big.NewInt(0) - for _, validator := range v { - sum = sum.Add(sum, validator.Validator.VotingPower) + sum = sum.Add(sum, big.NewInt(validator.VotingPower)) } - return sum } -func (v ValidatorsWithRoundVote) GetTotalVotingPowerPrevotedPercent(countDisagreeing bool) *big.Float { +// GetTotalVotingPowerPrevotedPercent calculates percentage of voting power that prevoted. +func (v TMValidators) GetTotalVotingPowerPrevotedPercent(countDisagreeing bool) *big.Float { prevoted := big.NewInt(0) totalVP := big.NewInt(0) for _, validator := range v { - totalVP = totalVP.Add(totalVP, validator.Validator.VotingPower) - if validator.RoundVote.Prevote == Voted || (countDisagreeing && validator.RoundVote.Prevote == VotedZero) { - prevoted = prevoted.Add(prevoted, validator.Validator.VotingPower) + totalVP = totalVP.Add(totalVP, big.NewInt(validator.VotingPower)) + if validator.CurrentRoundVote != nil { + if validator.CurrentRoundVote.Prevote == VoteStateForBlock || + (countDisagreeing && validator.CurrentRoundVote.Prevote == VoteStateNone) { + prevoted = prevoted.Add(prevoted, big.NewInt(validator.VotingPower)) + } } } @@ -101,14 +119,18 @@ func (v ValidatorsWithRoundVote) GetTotalVotingPowerPrevotedPercent(countDisagre return votingPowerPercent } -func (v ValidatorsWithRoundVote) GetTotalVotingPowerPrecommittedPercent(countDisagreeing bool) *big.Float { +// GetTotalVotingPowerPrecommittedPercent calculates percentage of voting power that precommitted. +func (v TMValidators) GetTotalVotingPowerPrecommittedPercent(countDisagreeing bool) *big.Float { precommitted := big.NewInt(0) totalVP := big.NewInt(0) for _, validator := range v { - totalVP = totalVP.Add(totalVP, validator.Validator.VotingPower) - if validator.RoundVote.Precommit == Voted || (countDisagreeing && validator.RoundVote.Precommit == VotedZero) { - precommitted = precommitted.Add(precommitted, validator.Validator.VotingPower) + totalVP = totalVP.Add(totalVP, big.NewInt(validator.VotingPower)) + if validator.CurrentRoundVote != nil { + if validator.CurrentRoundVote.Precommit == VoteStateForBlock || + (countDisagreeing && validator.CurrentRoundVote.Precommit == VoteStateNone) { + precommitted = precommitted.Add(precommitted, big.NewInt(validator.VotingPower)) + } } } @@ -119,132 +141,18 @@ func (v ValidatorsWithRoundVote) GetTotalVotingPowerPrecommittedPercent(countDis return votingPowerPercent } -type ValidatorWithInfo struct { - Validator Validator - RoundVote RoundVote - ChainValidator *ChainValidator -} - -func (v ValidatorWithInfo) Serialize(disableEmojis bool) string { - name := v.Validator.Address - if v.ChainValidator != nil { - name = v.ChainValidator.Moniker - if v.ChainValidator.AssignedAddress != "" { - emoji := "🔑" - if disableEmojis { - emoji = "[k[]" - } - name = emoji + " " + name - } - } - - return fmt.Sprintf( - " %s %s %s %s%% %s ", - v.RoundVote.Prevote.Serialize(disableEmojis), - v.RoundVote.Precommit.Serialize(disableEmojis), - utils.RightPadAndTrim(strconv.Itoa(v.Validator.Index+1), 3), - utils.RightPadAndTrim(fmt.Sprintf("%.2f", v.Validator.VotingPowerPercent), 6), - utils.LeftPadAndTrim(name, 25), - ) -} - -type ValidatorsWithInfo []ValidatorWithInfo - -type ValidatorWithChainValidator struct { - Validator Validator - ChainValidator *ChainValidator -} - -func (v ValidatorWithChainValidator) Equals(other ValidatorWithChainValidator) bool { - if v.Validator.Index != other.Validator.Index { - return false - } - - if v.Validator.Address != other.Validator.Address { - return false - } - - if v.Validator.VotingPowerPercent.Cmp(other.Validator.VotingPowerPercent) != 0 { - return false - } - - if v.Validator.VotingPower.Cmp(other.Validator.VotingPower) != 0 { - return false - } - - if (v.ChainValidator == nil) != (other.ChainValidator == nil) { - return false - } - - if v.ChainValidator == nil && other.ChainValidator == nil { - return true - } - - if v.ChainValidator.Moniker != other.ChainValidator.Moniker { - return false - } - - if v.ChainValidator.Address != other.ChainValidator.Address { - return false - } - - if v.ChainValidator.AssignedAddress != other.ChainValidator.AssignedAddress { - return false - } - - return true +// RoundVoteState represents validator vote state for a specific round using new VoteState. +type RoundVoteState struct { + Address string + Prevote VoteState + Precommit VoteState + IsProposer bool } -func (v ValidatorWithChainValidator) Serialize() string { - name := v.Validator.Address - if v.ChainValidator != nil { - name = v.ChainValidator.Moniker - if v.ChainValidator.AssignedAddress != "" { - name = "🔑 " + name - } - } - +func (v RoundVoteState) Serialize(disableEmojis bool) string { return fmt.Sprintf( - " %s %s%% %s ", - utils.RightPadAndTrim(strconv.Itoa(v.Validator.Index+1), 3), - utils.RightPadAndTrim(fmt.Sprintf("%.2f", v.Validator.VotingPowerPercent), 6), - utils.LeftPadAndTrim(name, 25), + " %s %s", + v.Prevote.Serialize(disableEmojis), + v.Precommit.Serialize(disableEmojis), ) } - -type ValidatorsWithInfoAndAllRoundVotes struct { - Validators []ValidatorWithChainValidator - RoundsVotes []RoundVotes -} - -func (v ValidatorsWithInfoAndAllRoundVotes) Equals(other ValidatorsWithInfoAndAllRoundVotes) bool { - if len(v.RoundsVotes) != len(other.RoundsVotes) { - return false - } - - for index, roundsVotes := range v.RoundsVotes { - otherRoundsVotes := other.RoundsVotes[index] - if len(roundsVotes) != len(otherRoundsVotes) { - return false - } - - for innerIndex, roundVotes := range roundsVotes { - otherRoundVotes := otherRoundsVotes[innerIndex] - if roundVotes.Equals(otherRoundVotes) { - return false - } - } - } - - if len(v.Validators) != len(other.Validators) { - return false - } - - for index, validator := range v.Validators { - if !validator.Equals(other.Validators[index]) { - return false - } - } - - return true -} diff --git a/pkg/types/vote.go b/pkg/types/vote.go index cd3d5f8..4687794 100644 --- a/pkg/types/vote.go +++ b/pkg/types/vote.go @@ -1,21 +1,45 @@ package types -type Vote int +import ( + cptypes "github.com/cometbft/cometbft/proto/tendermint/types" + ctypes "github.com/cometbft/cometbft/types" +) + +// VoteState represents the state of a validator's vote using CometBFT types directly. +type VoteState int const ( - Voted Vote = iota - VotedNil - VotedZero + VoteStateNone VoteState = iota + VoteStateNil + VoteStateForBlock ) -func (v Vote) Serialize(disableEmojis bool) string { +// VoteStateFromCometBFT determines vote state from CometBFT types. +func VoteStateFromCometBFT(voteExists bool, blockID ctypes.BlockID) VoteState { + if !voteExists { + return VoteStateNone + } + if blockID.IsZero() { + return VoteStateNil + } + return VoteStateForBlock +} + +// VoteStateFromVotesMap determines vote state from votes map. +func VoteStateFromVotesMap(votesMap map[cptypes.SignedMsgType]ctypes.BlockID, msgType cptypes.SignedMsgType) VoteState { + blockID, exists := votesMap[msgType] + return VoteStateFromCometBFT(exists, blockID) +} + +// Serialize returns UI representation of vote state. +func (v VoteState) Serialize(disableEmojis bool) string { if disableEmojis { switch v { - case Voted: + case VoteStateForBlock: return "[X[]" - case VotedZero: + case VoteStateNil: return "[0[]" - case VotedNil: + case VoteStateNone: return "[ []" default: return "" @@ -23,11 +47,11 @@ func (v Vote) Serialize(disableEmojis bool) string { } switch v { - case Voted: + case VoteStateForBlock: return "✅" - case VotedZero: + case VoteStateNil: return "🤷" - case VotedNil: + case VoteStateNone: return "❌" default: return "" diff --git a/pkg/utils/noop_locker.go b/pkg/utils/noop_locker.go new file mode 100644 index 0000000..ba8d833 --- /dev/null +++ b/pkg/utils/noop_locker.go @@ -0,0 +1,8 @@ +package utils + +type NoopLocker struct{} + +func (l NoopLocker) Lock() {} +func (l NoopLocker) Unlock() {} +func (l NoopLocker) RLock() {} +func (l NoopLocker) RUnlock() {} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9a61bdc..e5eb95f 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,14 +2,37 @@ package utils import ( "bytes" - loggerPkg "main/pkg/logger" + "encoding/base64" + "encoding/hex" "math" "strconv" "time" + loggerPkg "main/pkg/logger" + "github.com/btcsuite/btcutil/bech32" + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/crypto/ed25519" + comet_secp256k1 "github.com/cometbft/cometbft/crypto/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" ) +func PubKeyToPeerID(pubKeyBytes string) (string, error) { + // Convert to ed25519 public key + pubKey := make(ed25519.PubKey, ed25519.PubKeySize) + copy(pubKey[:], pubKeyBytes) + + // Get the peer ID from the public key + peerID := crypto.Address(pubKey).String() + return peerID, nil +} + +func ValidatorAddr(pubkeyBytes []byte) string { + pubkey := comet_secp256k1.PubKey(pubkeyBytes) + pubKeyConvertedToAddress := sdk.ValAddress(pubkey.Address().Bytes()) + return pubKeyConvertedToAddress.String() +} + func MustParseInt64(source string) int64 { result, err := strconv.ParseInt(source, 10, 64) if err != nil { @@ -103,3 +126,17 @@ func CompareTwoBech32(first, second string) (bool, error) { return bytes.Equal(firstBytes, secondBytes), nil } + +// HexToBytes converts a hex string to bytes. +func HexToBytes(hexStr string) ([]byte, error) { + // Remove 0x prefix if present + if len(hexStr) >= 2 && hexStr[:2] == "0x" { + hexStr = hexStr[2:] + } + return hex.DecodeString(hexStr) +} + +// Base64ToBytes converts a base64 string to bytes. +func Base64ToBytes(base64Str string) ([]byte, error) { + return base64.StdEncoding.DecodeString(base64Str) +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..9076b63 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,14 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "pkg/db/queries" + schema: "pkg/db/migrations" + gen: + go: + package: "sqlc" + out: "pkg/db/sqlc" + emit_json_tags: true + emit_prepared_queries: true + emit_interface: true + emit_exact_table_names: false + emit_empty_slices: true \ No newline at end of file