Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.

Feat/copytrader#142

Closed
evgensheff wants to merge 4 commits into
vultisig:mainfrom
evgensheff:feat/copytrader
Closed

Feat/copytrader#142
evgensheff wants to merge 4 commits into
vultisig:mainfrom
evgensheff:feat/copytrader

Conversation

@evgensheff

@evgensheff evgensheff commented Jul 25, 2025

Copy link
Copy Markdown
Contributor

Draft for copytrader plugin

Summary by CodeRabbit

  • New Features

    • Introduced a copytrader plugin with support for policy-based transaction validation, Uniswap V2 swap monitoring, and Ethereum transaction handling.
    • Added server and worker applications for the copytrader service, including configuration, database, Redis, and Datadog integration.
    • Implemented asynchronous task processing and transaction indexing for copytrader operations.
  • Bug Fixes

    • None.
  • Chores

    • Updated module dependencies and local development setup.
    • Improved code organization and import structure in related plugin files.

@coderabbitai

coderabbitai Bot commented Jul 25, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

This 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

Cohort / File(s) Change Summary
Copytrader Server: Config & Main
cmd/copytrader/server/config.go, cmd/copytrader/server/main.go
Adds configuration struct and loader for the server, and implements the server's main entry point with initialization of logging, metrics, Redis, Asynq, storage, database, and API server.
Copytrader Worker: Config & Main
cmd/copytrader/worker/config.go, cmd/copytrader/worker/main.go
Adds worker configuration struct and loader, and implements the worker's main entry point with initialization of telemetry, Redis, Asynq server, storage, database, Ethereum RPC, and task handlers for plugin transactions, key signing, and resharing.
Go Module Management
go.mod
Adds a local replace directive for the github.com/vultisig/verifier module to use a local path.
Copytrader Plugin: Core Logic
plugin/copytrader/copytrader.go, plugin/copytrader/policy.go, plugin/copytrader/transaction.go, plugin/copytrader/watcher.go
Implements the copytrader plugin struct, its constructor, transaction validation against policy, transaction proposal and signing logic, swap task handling, and a watcher for Uniswap V2 swap transactions.
Copytrader Plugin: Types & Constants
plugin/copytrader/types.go, plugin/copytrader/const.go
Adds a SwapTask struct for swap task payloads and constants for Uniswap V2 router address and swap method signature.
Unrelated Plugin: Import Reordering
plugin/payroll/payroll.go
Reorders imports for clarity; no logic changes.

Sequence Diagram(s)

Copytrader Server Startup

sequenceDiagram
    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
Loading

Copytrader Worker Startup and Task Handling

sequenceDiagram
    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
Loading

Copytrader Plugin: Swap Watcher

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

In the warren where code bunnies dwell,
A copytrader plugin began to swell.
With configs and workers, swaps on the fly,
Uniswap hops as blocks pass by.
Tasks and policies, all in a row—
This rabbit’s release is ready to go! 🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f531bb and 46773a0.

📒 Files selected for processing (1)
  • go.mod (1 hunks)
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🔭 Outside diff range comments (1)
go.mod (1)

3-3: Invalid Go version specified

Go 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 configurable

The 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 cleanup

Multiple resources are initialized but not cleaned up on error. While panic is 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() and logger.Fatalf() for error handling without a clear pattern. Consider standardizing on one approach - preferably logger.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

📥 Commits

Reviewing files that changed from the base of the PR and between 8baf582 and 7f531bb.

📒 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 impact

Only import grouping changed; logic untouched. Looks good.

plugin/payroll/transaction.go (1)

14-33: Imports reorganized – nothing to address

Purely cosmetic re-ordering; implementation remains the same. No action needed.

go.mod (1)

199-199: Remove local replace directive before merging

The 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.

Comment on lines +15 to +27
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"`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +38 to +55
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +21 to +102
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)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +22 to +23
// Don't scale payroll.worker, it has scheduler which must be single instance running
// Consider moving scheduler to separate worker

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
// 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.

Comment on lines +9 to +13
type SwapTask struct {
Sender common.Address
Path []common.Address
Amount *big.Int
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +16 to +95
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)
}
}
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider architectural improvements for production readiness.

The current implementation has several limitations for production use:

  1. Block persistence: blockID is not persisted, leading to reprocessing after restarts
  2. No actual copy trading logic: The method only logs swaps but doesn't trigger any copy trading actions
  3. 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.

Comment on lines +18 to +21
err := json.Unmarshal([]byte(uniswapv2_router.Uniswapv2RouterMetaData.ABI), &uniswapABI)
if err != nil {
panic(err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +34 to +42
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +87 to +89
fmt.Println("sender", sender.String())
fmt.Println("amount", amountIn.String())
fmt.Println("path: ", tokens)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@evgensheff evgensheff Jul 25, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread plugin/copytrader/watcher.go
# Conflicts:
#	plugin/payroll/transaction.go
@evgensheff evgensheff closed this Jul 31, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants