Feat/copytrader#142
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughThis change introduces a new copytrader plugin and its supporting infrastructure, including server and worker executables, configuration management, and plugin logic. It adds configuration files for both server and worker, implements the main entry points, and provides plugin logic for transaction validation, proposal, signing, broadcasting, and Uniswap V2 swap monitoring. Supporting types and constants are also defined. Minor changes include a Go module replace directive and import reordering in an unrelated plugin. Changes
Sequence Diagram(s)Copytrader Server StartupsequenceDiagram
participant Main
participant Config
participant Logger
participant Datadog
participant Redis
participant Asynq
participant BlockStorage
participant Database
participant TxIndexer
participant Plugin
participant APIServer
Main->>Config: Load config (GetConfigure/ReadConfig)
Main->>Logger: Initialize logging
Main->>Datadog: Connect StatsD
Main->>Redis: Connect to Redis
Main->>Asynq: Create client/inspector
Main->>BlockStorage: Initialize vault storage
Main->>Database: Initialize PostgreSQL backend
Main->>TxIndexer: Create indexer store/service
Main->>Plugin: Instantiate Copytrader plugin
Main->>APIServer: Start API server with all components
Copytrader Worker Startup and Task HandlingsequenceDiagram
participant Main
participant Config
participant Datadog
participant VaultStorage
participant Redis
participant Asynq
participant Database
participant EthRPC
participant TxIndexer
participant VaultService
participant Plugin
Main->>Config: Load config (GetConfigure/ReadConfig)
Main->>Datadog: Initialize telemetry
Main->>VaultStorage: Create block storage
Main->>Redis: Connect to Redis
Main->>Asynq: Start server
Main->>Database: Connect to PostgreSQL
Main->>EthRPC: Connect to Ethereum node
Main->>TxIndexer: Create indexer service
Main->>VaultService: Create vault management service
Main->>Plugin: Instantiate plugin with dependencies
Asynq->>Plugin: Dispatch tasks to handler methods
Copytrader Plugin: Swap WatchersequenceDiagram
participant Plugin
participant EthereumNode
loop Every 5 seconds
Plugin->>EthereumNode: Get current block number
Plugin->>EthereumNode: For each new block, fetch block data
Plugin->>EthereumNode: For each transaction, check destination
Plugin->>Plugin: If Uniswap V2 swap, decode and log swap details
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 14
🔭 Outside diff range comments (1)
go.mod (1)
3-3: Invalid Go version specifiedGo version 1.24.2 doesn't exist. The latest stable version as of 2025 is in the 1.22.x range.
-go 1.24.2 +go 1.22.0
🧹 Nitpick comments (6)
cmd/copytrader/worker/main.go (2)
50-59: Make concurrency and queue settings configurableThe concurrency and queue settings are hardcoded, which limits deployment flexibility.
These values should come from configuration:
srv := asynq.NewServer( redisOptions, asynq.Config{ Logger: logger, - Concurrency: 10, + Concurrency: cfg.Worker.Concurrency, // Add to config Queues: map[string]int{ - tasks.QUEUE_NAME: 10, + tasks.QUEUE_NAME: cfg.Worker.QueuePriority, // Add to config }, }, )
83-115: Consider using defer for resource cleanupMultiple resources are initialized but not cleaned up on error. While
panicis acceptable in main, consider using defer for cleanup.Add cleanup for initialized resources:
postgressDB, err := postgres.NewPostgresBackend(cfg.Database.DSN, nil) if err != nil { panic(fmt.Errorf("failed to create postgres backend: %w", err)) } + defer postgressDB.Close() // If Close method exists rpcClient, err := ethclient.Dial(cfg.Rpc.Ethereum.URL) if err != nil { panic(fmt.Errorf("failed to create eth client: %w", err)) } + defer rpcClient.Close()cmd/copytrader/server/main.go (1)
24-27: Inconsistent error handling pattern throughout initialization.The initialization code mixes
panic()andlogger.Fatalf()for error handling without a clear pattern. Consider standardizing on one approach - preferablylogger.Fatalf()for better logging consistency.- if err != nil { - panic(err) - } + if err != nil { + logger.Fatalf("Failed to load configuration: %v", err) + }Also applies to: 30-33, 35-37, 54-57, 64-67, 84-86, 99-101
plugin/copytrader/policy.go (1)
62-63: Address TODO comment for plugin ID configuration.The plugin ID is currently hardcoded with a TODO comment. Consider making this configurable or ensure the hardcoded value is correct for production use.
Would you like me to help implement a configurable plugin ID system or should this remain hardcoded for now?
cmd/copytrader/worker/config.go (1)
58-58: Fix grammar in error message.- return nil, fmt.Errorf("fail to reading config file, %w", err) + return nil, fmt.Errorf("failed to read config file, %w", err)plugin/copytrader/transaction.go (1)
32-34: Implement the core swap logic.The method currently contains only placeholder TODO comments. This indicates incomplete functionality that could cause runtime issues if invoked.
Would you like me to help design the implementation structure for the database interactions and swap triggering logic based on the SwapTask struct?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
cmd/copytrader/server/config.go(1 hunks)cmd/copytrader/server/main.go(1 hunks)cmd/copytrader/worker/config.go(1 hunks)cmd/copytrader/worker/main.go(1 hunks)go.mod(1 hunks)plugin/copytrader/const.go(1 hunks)plugin/copytrader/copytrader.go(1 hunks)plugin/copytrader/policy.go(1 hunks)plugin/copytrader/transaction.go(1 hunks)plugin/copytrader/types.go(1 hunks)plugin/copytrader/watcher.go(1 hunks)plugin/payroll/payroll.go(1 hunks)plugin/payroll/transaction.go(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
cmd/copytrader/server/config.go (3)
api/server_config.go (1)
ServerConfig(9-15)storage/redis_storage.go (1)
RedisConfig(18-24)cmd/copytrader/worker/config.go (2)
GetConfigure(43-49)ReadConfig(51-66)
cmd/copytrader/worker/config.go (2)
storage/redis_storage.go (1)
RedisConfig(18-24)cmd/copytrader/server/config.go (2)
GetConfigure(29-36)ReadConfig(38-55)
plugin/copytrader/transaction.go (3)
plugin/copytrader/copytrader.go (1)
Plugin(22-33)plugin/copytrader/types.go (1)
SwapTask(9-13)internal/types/keysign.go (1)
PluginKeysignRequest(7-12)
plugin/copytrader/watcher.go (2)
plugin/copytrader/copytrader.go (1)
Plugin(22-33)plugin/copytrader/const.go (2)
UniswapV2RouterAddress(4-4)SwapExactTokensForTokens(5-5)
plugin/copytrader/policy.go (3)
plugin/copytrader/copytrader.go (1)
Plugin(22-33)internal/types/keysign.go (1)
PluginKeysignRequest(7-12)internal/plugin/plugin.go (1)
ValidatePluginPolicy(13-30)
🔇 Additional comments (11)
plugin/payroll/payroll.go (1)
14-16: Imports reordered – no functional impactOnly import grouping changed; logic untouched. Looks good.
plugin/payroll/transaction.go (1)
14-33: Imports reorganized – nothing to addressPurely cosmetic re-ordering; implementation remains the same. No action needed.
go.mod (1)
199-199: Remove local replace directive before mergingThe local replace directive for the verifier module should not be present in production code.
This appears to be for local development. Ensure this is removed before merging to production, or consider using a development-specific go.mod file.
plugin/copytrader/copytrader.go (2)
20-20: LGTM: Proper interface compliance verification.Good use of compile-time interface compliance check.
35-77: LGTM: Well-structured constructor with proper validation.The constructor properly validates required dependencies, handles optional Ethereum client initialization, and provides good error context through wrapped errors. The conditional initialization pattern is well-implemented.
plugin/copytrader/policy.go (2)
15-52: LGTM: Robust transaction validation logic.The validation logic properly validates the policy, extracts the recipe, and evaluates each transaction against the policy using the recipes engine. Error handling is comprehensive with good error context.
58-117: LGTM: Comprehensive recipe specification.The recipe specification is well-defined with proper parameter constraints for Uniswap V2 operations. The parameter capabilities cover the necessary constraint types (FIXED, WHITELIST, MAX, RANGE) for different parameters.
cmd/copytrader/worker/config.go (1)
14-27: LGTM: Well-structured configuration management.The configuration structure and loading logic follow established patterns with proper environment variable support and error handling. The separation of worker-specific configuration is appropriate.
Also applies to: 43-66
plugin/copytrader/transaction.go (3)
42-70: Well-implemented signing logic with proper validation.The method correctly validates the signature count for EVM transactions and provides comprehensive error handling and logging.
72-97: Excellent transaction broadcasting implementation.The method correctly handles signature components and provides comprehensive logging for transaction observability. The error handling is thorough and appropriate.
1-16: Clean import structure with proper aliasing.The imports are well-organized and the aliasing (gcommon vs vcommon) properly prevents naming conflicts between Ethereum and Verifier common packages.
| type CopytraderServerConfig struct { | ||
| Server api.ServerConfig `mapstructure:"server" json:"server"` | ||
| Database struct { | ||
| DSN string `mapstructure:"dsn" json:"dsn,omitempty"` | ||
| } `mapstructure:"database" json:"database,omitempty"` | ||
| BaseConfigPath string `mapstructure:"base_config_path" json:"base_config_path,omitempty"` | ||
| Redis storage.RedisConfig `mapstructure:"redis" json:"redis,omitempty"` | ||
| BlockStorage vault_config.BlockStorage `mapstructure:"block_storage" json:"block_storage,omitempty"` | ||
| Datadog struct { | ||
| Host string `mapstructure:"host" json:"host,omitempty"` | ||
| Port string `mapstructure:"port" json:"port,omitempty"` | ||
| } `mapstructure:"datadog" json:"datadog"` | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Extract inline structs and consider sensitive data handling
The inline struct definitions for Database and Datadog reduce code reusability and maintainability. Also, the Database DSN may contain sensitive credentials.
Consider extracting these to separate types:
+type DatabaseConfig struct {
+ DSN string `mapstructure:"dsn" json:"dsn,omitempty"`
+}
+
+type DatadogConfig struct {
+ Host string `mapstructure:"host" json:"host,omitempty"`
+ Port string `mapstructure:"port" json:"port,omitempty"`
+}
+
type CopytraderServerConfig struct {
Server api.ServerConfig `mapstructure:"server" json:"server"`
- Database struct {
- DSN string `mapstructure:"dsn" json:"dsn,omitempty"`
- } `mapstructure:"database" json:"database,omitempty"`
+ Database DatabaseConfig `mapstructure:"database" json:"database,omitempty"`
BaseConfigPath string `mapstructure:"base_config_path" json:"base_config_path,omitempty"`
Redis storage.RedisConfig `mapstructure:"redis" json:"redis,omitempty"`
BlockStorage vault_config.BlockStorage `mapstructure:"block_storage" json:"block_storage,omitempty"`
- Datadog struct {
- Host string `mapstructure:"host" json:"host,omitempty"`
- Port string `mapstructure:"port" json:"port,omitempty"`
- } `mapstructure:"datadog" json:"datadog"`
+ Datadog DatadogConfig `mapstructure:"datadog" json:"datadog"`
}Also, ensure that sensitive configuration values like database DSN are properly secured (e.g., using environment variables or secret management systems).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type CopytraderServerConfig struct { | |
| Server api.ServerConfig `mapstructure:"server" json:"server"` | |
| Database struct { | |
| DSN string `mapstructure:"dsn" json:"dsn,omitempty"` | |
| } `mapstructure:"database" json:"database,omitempty"` | |
| BaseConfigPath string `mapstructure:"base_config_path" json:"base_config_path,omitempty"` | |
| Redis storage.RedisConfig `mapstructure:"redis" json:"redis,omitempty"` | |
| BlockStorage vault_config.BlockStorage `mapstructure:"block_storage" json:"block_storage,omitempty"` | |
| Datadog struct { | |
| Host string `mapstructure:"host" json:"host,omitempty"` | |
| Port string `mapstructure:"port" json:"port,omitempty"` | |
| } `mapstructure:"datadog" json:"datadog"` | |
| } | |
| // extract these above your server config | |
| type DatabaseConfig struct { | |
| DSN string `mapstructure:"dsn" json:"dsn,omitempty"` | |
| } | |
| type DatadogConfig struct { | |
| Host string `mapstructure:"host" json:"host,omitempty"` | |
| Port string `mapstructure:"port" json:"port,omitempty"` | |
| } | |
| type CopytraderServerConfig struct { | |
| Server api.ServerConfig `mapstructure:"server" json:"server"` | |
| Database DatabaseConfig `mapstructure:"database" json:"database,omitempty"` | |
| BaseConfigPath string `mapstructure:"base_config_path" json:"base_config_path,omitempty"` | |
| Redis storage.RedisConfig `mapstructure:"redis" json:"redis,omitempty"` | |
| BlockStorage vault_config.BlockStorage `mapstructure:"block_storage" json:"block_storage,omitempty"` | |
| Datadog DatadogConfig `mapstructure:"datadog" json:"datadog"` | |
| } |
🤖 Prompt for AI Agents
In cmd/copytrader/server/config.go around lines 15 to 27, the inline struct
definitions for Database and Datadog reduce code reusability and
maintainability, and the Database DSN may contain sensitive credentials. Extract
the Database and Datadog inline structs into separate named types to improve
clarity and reuse. Additionally, modify the handling of the Database DSN to
avoid hardcoding sensitive data by loading it securely from environment
variables or a secret management system instead of embedding it directly in the
config struct.
| func ReadConfig(configName string) (*CopytraderServerConfig, error) { | ||
| viper.SetConfigName(configName) | ||
| viper.AddConfigPath(".") | ||
| viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) | ||
| viper.AutomaticEnv() | ||
|
|
||
| viper.SetDefault("Server.VaultsFilePath", "vaults") | ||
|
|
||
| if err := viper.ReadInConfig(); err != nil { | ||
| return nil, fmt.Errorf("failed to read config file, %w", err) | ||
| } | ||
| var cfg CopytraderServerConfig | ||
| err := viper.Unmarshal(&cfg) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to decode into struct, %w", err) | ||
| } | ||
| return &cfg, nil | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add configuration validation
The configuration loading doesn't validate required fields, which could lead to runtime errors.
Add validation after unmarshaling:
var cfg CopytraderServerConfig
err := viper.Unmarshal(&cfg)
if err != nil {
return nil, fmt.Errorf("unable to decode into struct, %w", err)
}
+
+ // Validate required fields
+ if cfg.Database.DSN == "" {
+ return nil, fmt.Errorf("database DSN is required")
+ }
+ if cfg.Redis.Host == "" || cfg.Redis.Port == "" {
+ return nil, fmt.Errorf("redis host and port are required")
+ }
+
return &cfg, nil📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func ReadConfig(configName string) (*CopytraderServerConfig, error) { | |
| viper.SetConfigName(configName) | |
| viper.AddConfigPath(".") | |
| viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) | |
| viper.AutomaticEnv() | |
| viper.SetDefault("Server.VaultsFilePath", "vaults") | |
| if err := viper.ReadInConfig(); err != nil { | |
| return nil, fmt.Errorf("failed to read config file, %w", err) | |
| } | |
| var cfg CopytraderServerConfig | |
| err := viper.Unmarshal(&cfg) | |
| if err != nil { | |
| return nil, fmt.Errorf("unable to decode into struct, %w", err) | |
| } | |
| return &cfg, nil | |
| } | |
| func ReadConfig(configName string) (*CopytraderServerConfig, error) { | |
| viper.SetConfigName(configName) | |
| viper.AddConfigPath(".") | |
| viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) | |
| viper.AutomaticEnv() | |
| viper.SetDefault("Server.VaultsFilePath", "vaults") | |
| if err := viper.ReadInConfig(); err != nil { | |
| return nil, fmt.Errorf("failed to read config file, %w", err) | |
| } | |
| var cfg CopytraderServerConfig | |
| err := viper.Unmarshal(&cfg) | |
| if err != nil { | |
| return nil, fmt.Errorf("unable to decode into struct, %w", err) | |
| } | |
| // Validate required fields | |
| if cfg.Database.DSN == "" { | |
| return nil, fmt.Errorf("database DSN is required") | |
| } | |
| if cfg.Redis.Host == "" || cfg.Redis.Port == "" { | |
| return nil, fmt.Errorf("redis host and port are required") | |
| } | |
| return &cfg, nil | |
| } |
🤖 Prompt for AI Agents
In cmd/copytrader/server/config.go around lines 38 to 55, after unmarshaling the
config into the struct, add validation logic to check that all required fields
are set and valid. Implement checks for mandatory fields in the
CopytraderServerConfig struct and return an error if any required field is
missing or invalid before returning the config. This ensures the configuration
is validated early to prevent runtime errors.
| func main() { | ||
| ctx := context.Background() | ||
|
|
||
| cfg, err := GetConfigure() | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
| logger := logrus.New() | ||
|
|
||
| sdClient, err := statsd.New(net.JoinHostPort(cfg.Datadog.Host, cfg.Datadog.Port)) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
| redisStorage, err := storage.NewRedisStorage(cfg.Redis) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
| redisOptions := asynq.RedisClientOpt{ | ||
| Addr: net.JoinHostPort(cfg.Redis.Host, cfg.Redis.Port), | ||
| Username: cfg.Redis.User, | ||
| Password: cfg.Redis.Password, | ||
| DB: cfg.Redis.DB, | ||
| } | ||
|
|
||
| client := asynq.NewClient(redisOptions) | ||
| defer func() { | ||
| if err := client.Close(); err != nil { | ||
| fmt.Println("fail to close asynq client,", err) | ||
| } | ||
| }() | ||
|
|
||
| inspector := asynq.NewInspector(redisOptions) | ||
|
|
||
| vaultStorage, err := vault.NewBlockStorageImp(cfg.BlockStorage) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| db, err := postgres.NewPostgresBackend(cfg.Database.DSN, nil) | ||
| if err != nil { | ||
| logger.Fatalf("Failed to connect to database: %v", err) | ||
| } | ||
|
|
||
| txIndexerStore, err := tx_indexer_storage.NewPostgresTxIndexStore(ctx, cfg.Database.DSN) | ||
| if err != nil { | ||
| panic(fmt.Errorf("tx_indexer_storage.NewPostgresTxIndexStore: %w", err)) | ||
| } | ||
|
|
||
| txIndexerService := tx_indexer.NewService( | ||
| logger, | ||
| txIndexerStore, | ||
| tx_indexer.Chains(), | ||
| ) | ||
|
|
||
| ct, err := copytrader.NewPlugin( | ||
| db, | ||
| nil, // not used by server | ||
| vaultStorage, | ||
| nil, | ||
| txIndexerService, | ||
| client, | ||
| cfg.Server.EncryptionSecret, | ||
| ) | ||
| if err != nil { | ||
| logger.Fatalf("failed to create payroll plugin,err: %s", err) | ||
| } | ||
|
|
||
| server := api.NewServer( | ||
| cfg.Server, | ||
| db, | ||
| redisStorage, | ||
| vaultStorage, | ||
| redisOptions, | ||
| client, | ||
| inspector, | ||
| sdClient, | ||
| ct, | ||
| ) | ||
| if err := server.StartServer(); err != nil { | ||
| panic(err) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider adding graceful shutdown handling.
The main function lacks graceful shutdown capabilities. Consider implementing signal handling to properly close connections and clean up resources on shutdown.
+import (
+ "os"
+ "os/signal"
+ "syscall"
+)
func main() {
ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Setup signal handling
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+ go func() {
+ <-sigChan
+ logger.Info("Shutdown signal received")
+ cancel()
+ }()
// ... existing initialization code ...
- if err := server.StartServer(); err != nil {
- panic(err)
+ go func() {
+ if err := server.StartServer(); err != nil {
+ logger.Fatalf("Server failed to start: %v", err)
+ }
}()
+
+ <-ctx.Done()
+ logger.Info("Shutting down server...")
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In cmd/copytrader/server/main.go around lines 21 to 102, the main function
currently does not handle graceful shutdown, which can lead to improper resource
cleanup. Add signal handling for termination signals (e.g., SIGINT, SIGTERM)
using a channel and context cancellation. Modify the server start logic to
listen for these signals and on receiving them, properly close all clients,
connections, and other resources before exiting the program.
| cfg.Server.EncryptionSecret, | ||
| ) | ||
| if err != nil { | ||
| logger.Fatalf("failed to create payroll plugin,err: %s", err) |
There was a problem hiding this comment.
Fix incorrect error message referencing wrong plugin.
The error message mentions "payroll plugin" instead of "copytrader plugin".
- logger.Fatalf("failed to create payroll plugin,err: %s", err)
+ logger.Fatalf("failed to create copytrader plugin, err: %s", err)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.Fatalf("failed to create payroll plugin,err: %s", err) | |
| logger.Fatalf("failed to create copytrader plugin, err: %s", err) |
🤖 Prompt for AI Agents
In cmd/copytrader/server/main.go at line 85, the error message incorrectly
references "payroll plugin" instead of "copytrader plugin". Update the log
message to correctly state "failed to create copytrader plugin" to accurately
reflect the context of the error.
| // Don't scale payroll.worker, it has scheduler which must be single instance running | ||
| // Consider moving scheduler to separate worker |
There was a problem hiding this comment.
Fix incorrect comment referencing payroll.worker
The comment references "payroll.worker" but this is the copytrader worker.
-// Don't scale payroll.worker, it has scheduler which must be single instance running
-// Consider moving scheduler to separate worker
+// Don't scale copytrader.worker if it has any singleton components
+// Consider architectural patterns for horizontal scaling if needed📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Don't scale payroll.worker, it has scheduler which must be single instance running | |
| // Consider moving scheduler to separate worker | |
| // Don't scale copytrader.worker if it has any singleton components | |
| // Consider architectural patterns for horizontal scaling if needed |
🤖 Prompt for AI Agents
In cmd/copytrader/worker/main.go at lines 22 to 23, the comment incorrectly
references "payroll.worker" instead of the correct "copytrader worker." Update
the comment to replace "payroll.worker" with "copytrader worker" to accurately
reflect the context and avoid confusion.
| type SwapTask struct { | ||
| Sender common.Address | ||
| Path []common.Address | ||
| Amount *big.Int | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add JSON tags and validation for SwapTask
The SwapTask struct should include JSON tags for serialization and a validation method to ensure data integrity.
type SwapTask struct {
- Sender common.Address
- Path []common.Address
- Amount *big.Int
+ Sender common.Address `json:"sender"`
+ Path []common.Address `json:"path"`
+ Amount *big.Int `json:"amount"`
+}
+
+// Validate checks if the SwapTask has valid data
+func (s *SwapTask) Validate() error {
+ if s.Sender == (common.Address{}) {
+ return fmt.Errorf("sender address cannot be zero")
+ }
+ if len(s.Path) < 2 {
+ return fmt.Errorf("swap path must contain at least 2 addresses")
+ }
+ if s.Amount == nil || s.Amount.Sign() <= 0 {
+ return fmt.Errorf("amount must be positive")
+ }
+ return nil
}Don't forget to import fmt if you add the validation method.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type SwapTask struct { | |
| Sender common.Address | |
| Path []common.Address | |
| Amount *big.Int | |
| } | |
| type SwapTask struct { | |
| Sender common.Address `json:"sender"` | |
| Path []common.Address `json:"path"` | |
| Amount *big.Int `json:"amount"` | |
| } | |
| // Validate checks if the SwapTask has valid data | |
| func (s *SwapTask) Validate() error { | |
| if s.Sender == (common.Address{}) { | |
| return fmt.Errorf("sender address cannot be zero") | |
| } | |
| if len(s.Path) < 2 { | |
| return fmt.Errorf("swap path must contain at least 2 addresses") | |
| } | |
| if s.Amount == nil || s.Amount.Sign() <= 0 { | |
| return fmt.Errorf("amount must be positive") | |
| } | |
| return nil | |
| } |
🤖 Prompt for AI Agents
In plugin/copytrader/types.go around lines 9 to 13, the SwapTask struct lacks
JSON tags for proper serialization and does not have a validation method to
ensure data integrity. Add JSON tags to each field in the struct to specify
their JSON key names. Then, implement a validation method on SwapTask that
checks for valid Sender address, non-empty Path, and positive Amount, returning
an error if any check fails. Also, import the "fmt" package to support error
formatting in the validation method.
| func (p *Plugin) WatchSwap(ctx context.Context) { | ||
| var uniswapABI abi.ABI | ||
| err := json.Unmarshal([]byte(uniswapv2_router.Uniswapv2RouterMetaData.ABI), &uniswapABI) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| for { | ||
| select { | ||
| case <-ctx.Done(): | ||
| return | ||
| case <-time.After(5 * time.Second): | ||
| currentBlock, err := p.ethRpc.BlockNumber(ctx) | ||
| if err != nil { | ||
| p.logger.WithError(err).Error("failed to get block") | ||
| continue | ||
| } | ||
|
|
||
| for p.blockID < currentBlock { | ||
| p.blockID++ | ||
| p.logger.Info("Processing block: ", p.blockID) | ||
|
|
||
| block, err := p.ethRpc.BlockByNumber(ctx, big.NewInt(0).SetUint64(p.blockID)) | ||
| if err != nil { | ||
| p.logger.WithError(err).Error("failed to get block") | ||
| continue | ||
| } | ||
|
|
||
| // Process txs to find UniswapV2Router interactions | ||
| for _, tx := range block.Transactions() { | ||
| if tx.To() == nil { | ||
| continue | ||
| } | ||
| // is Uniswap tx check | ||
| if tx.To().String() == UniswapV2RouterAddress { | ||
| inputBytes := tx.Data() | ||
| signature, data := inputBytes[:4], inputBytes[4:] | ||
| if hex.EncodeToString(signature) != SwapExactTokensForTokens { | ||
| continue | ||
| } | ||
|
|
||
| method, err := uniswapABI.MethodById(signature) | ||
| if err != nil { | ||
| p.logger.WithError(err).Error("unknown method") | ||
| continue | ||
| } | ||
|
|
||
| // Getting args from tx to find necessary info | ||
| var args = make(map[string]interface{}) | ||
| err = method.Inputs.UnpackIntoMap(args, data) | ||
| if err != nil { | ||
| p.logger.WithError(err).Error("failed to unpack data") | ||
| continue | ||
| } | ||
|
|
||
| path := args["path"] | ||
| tokens, valid := path.([]common.Address) | ||
| if !valid { | ||
| p.logger.Error("invalid path", path) | ||
| continue | ||
| } | ||
|
|
||
| amountIn, _ := new(big.Int).SetString(fmt.Sprint(args["amountIn"]), 10) | ||
| to := args["to"] | ||
| sender, valid := to.(common.Address) | ||
| if !valid { | ||
| p.logger.Error("invalid sender", to) | ||
| continue | ||
| } | ||
|
|
||
| //Triggering swaps | ||
| fmt.Println("sender", sender.String()) | ||
| fmt.Println("amount", amountIn.String()) | ||
| fmt.Println("path: ", tokens) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider architectural improvements for production readiness.
The current implementation has several limitations for production use:
- Block persistence:
blockIDis not persisted, leading to reprocessing after restarts - No actual copy trading logic: The method only logs swaps but doesn't trigger any copy trading actions
- Sequential processing: Could be slow when catching up with many blocks
Consider implementing:
- Persistent storage for the last processed block number
- Actual copy trading logic integration
- Batch processing or parallel block processing for better performance
- Circuit breaker pattern for RPC failures
🤖 Prompt for AI Agents
In plugin/copytrader/watcher.go from lines 16 to 95, improve production
readiness by persisting the last processed blockID to durable storage to avoid
reprocessing after restarts, implement the actual copy trading logic instead of
just logging swap details, refactor the block processing loop to handle blocks
in batches or process them in parallel to improve performance when catching up,
and add a circuit breaker mechanism around RPC calls to handle repeated failures
gracefully and prevent cascading errors.
| err := json.Unmarshal([]byte(uniswapv2_router.Uniswapv2RouterMetaData.ABI), &uniswapABI) | ||
| if err != nil { | ||
| panic(err) | ||
| } |
There was a problem hiding this comment.
Replace panic with proper error handling.
Panicking on ABI unmarshaling error will crash the entire application. Consider returning an error or logging and exiting gracefully.
- err := json.Unmarshal([]byte(uniswapv2_router.Uniswapv2RouterMetaData.ABI), &uniswapABI)
- if err != nil {
- panic(err)
- }
+ err := json.Unmarshal([]byte(uniswapv2_router.Uniswapv2RouterMetaData.ABI), &uniswapABI)
+ if err != nil {
+ p.logger.WithError(err).Error("failed to unmarshal Uniswap ABI")
+ return
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| err := json.Unmarshal([]byte(uniswapv2_router.Uniswapv2RouterMetaData.ABI), &uniswapABI) | |
| if err != nil { | |
| panic(err) | |
| } | |
| err := json.Unmarshal([]byte(uniswapv2_router.Uniswapv2RouterMetaData.ABI), &uniswapABI) | |
| if err != nil { | |
| p.logger.WithError(err).Error("failed to unmarshal Uniswap ABI") | |
| return | |
| } |
🤖 Prompt for AI Agents
In plugin/copytrader/watcher.go around lines 18 to 21, replace the panic call on
ABI unmarshaling error with proper error handling. Instead of panicking, handle
the error by either returning it to the caller or logging the error and exiting
gracefully to avoid crashing the entire application.
| for p.blockID < currentBlock { | ||
| p.blockID++ | ||
| p.logger.Info("Processing block: ", p.blockID) | ||
|
|
||
| block, err := p.ethRpc.BlockByNumber(ctx, big.NewInt(0).SetUint64(p.blockID)) | ||
| if err != nil { | ||
| p.logger.WithError(err).Error("failed to get block") | ||
| continue | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Handle block processing failures gracefully.
When block fetching fails, the method continues to the next iteration, potentially creating gaps in block processing. Consider implementing retry logic or proper error recovery.
block, err := p.ethRpc.BlockByNumber(ctx, big.NewInt(0).SetUint64(p.blockID))
if err != nil {
p.logger.WithError(err).Error("failed to get block")
- continue
+ // Don't increment blockID on failure to avoid gaps
+ p.blockID--
+ time.Sleep(1 * time.Second) // Brief delay before retry
+ continue
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for p.blockID < currentBlock { | |
| p.blockID++ | |
| p.logger.Info("Processing block: ", p.blockID) | |
| block, err := p.ethRpc.BlockByNumber(ctx, big.NewInt(0).SetUint64(p.blockID)) | |
| if err != nil { | |
| p.logger.WithError(err).Error("failed to get block") | |
| continue | |
| } | |
| @@ plugin/copytrader/watcher.go | |
| - block, err := p.ethRpc.BlockByNumber(ctx, big.NewInt(0).SetUint64(p.blockID)) | |
| - if err != nil { | |
| - p.logger.WithError(err).Error("failed to get block") | |
| - continue | |
| - } | |
| + block, err := p.ethRpc.BlockByNumber(ctx, big.NewInt(0).SetUint64(p.blockID)) | |
| + if err != nil { | |
| + p.logger.WithError(err).Error("failed to get block") | |
| + // Don't increment blockID on failure to avoid gaps | |
| + p.blockID-- | |
| + time.Sleep(1 * time.Second) // Brief delay before retry | |
| + continue | |
| + } |
🤖 Prompt for AI Agents
In plugin/copytrader/watcher.go around lines 34 to 42, the current code skips to
the next block immediately upon a block fetching error, risking gaps in
processing. Modify the code to implement retry logic for fetching the block,
such as retrying a fixed number of times with delays before logging an error and
moving on. This ensures transient errors are handled gracefully without missing
blocks.
| fmt.Println("sender", sender.String()) | ||
| fmt.Println("amount", amountIn.String()) | ||
| fmt.Println("path: ", tokens) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use structured logging instead of fmt.Println.
Replace console output with structured logging for consistency and better observability.
- //Triggering swaps
- fmt.Println("sender", sender.String())
- fmt.Println("amount", amountIn.String())
- fmt.Println("path: ", tokens)
+ // Triggering swaps
+ p.logger.WithFields(logrus.Fields{
+ "sender": sender.String(),
+ "amount": amountIn.String(),
+ "path": tokens,
+ "block": p.blockID,
+ "tx": tx.Hash().String(),
+ }).Info("Detected Uniswap V2 swap")🤖 Prompt for AI Agents
In plugin/copytrader/watcher.go around lines 87 to 89, replace the fmt.Println
calls with structured logging using the project's logger. Use appropriate log
levels and include key-value pairs for "sender", "amount", and "path" to ensure
consistent and observable log output.
| } | ||
| eth = evm.NewSDK(ethEvmChainID, ethRpc, ethRpc.Client()) | ||
|
|
||
| currentBlock, err = ethRpc.BlockNumber(context.Background()) |
There was a problem hiding this comment.
You should save last processed block in db, and fetch it from there on worker start. After block processed, you should increment it in database. Also in case of first start it should use startFrom block number from config in case of empty db
There was a problem hiding this comment.
Keeping a block in db is standard practice, but in my case copy-trading will be repetitive actions in short periods of time and as a rule operations will be scalper. If there will be any long term failure then repetition of all operations during synchronisation can bring big losses. About adding a starting block to the configuration - this is a good take
There was a problem hiding this comment.
If there will be any long term failure then repetition of all operations during synchronisation can bring big losses
Hmm, yes I agree it's better to skip rather than copy a swap executed some time ago
# Conflicts: # plugin/payroll/transaction.go
Draft for copytrader plugin
Summary by CodeRabbit
New Features
Bug Fixes
Chores