diff --git a/sai-trading/.env.example b/sai-trading/.env.example new file mode 100644 index 000000000..b933f263b --- /dev/null +++ b/sai-trading/.env.example @@ -0,0 +1,4 @@ +EVM_MNEMONIC="" + +# Optional: Slack webhook for error notifications (leave empty to disable) +SLACK_WEBHOOK="" \ No newline at end of file diff --git a/sai-trading/Dockerfile b/sai-trading/Dockerfile new file mode 100644 index 000000000..fabcd1103 --- /dev/null +++ b/sai-trading/Dockerfile @@ -0,0 +1,49 @@ +FROM golang:1.24 AS builder + +WORKDIR /sai-trading +ARG WASMVM_VERSION=1.5.8 + + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + musl-dev build-essential libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev wget ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +ARG TARGETARCH +RUN mkdir -p /usr/lib && \ + if [ "${TARGETARCH}" = "arm64" ]; then \ + wget --progress=dot:giga --timeout=30 --tries=3 \ + https://github.com/CosmWasm/wasmvm/releases/download/v${WASMVM_VERSION}/libwasmvm_muslc.aarch64.a \ + -O /usr/lib/libwasmvm_muslc.a; \ + else \ + wget --progress=dot:giga --timeout=30 --tries=3 \ + https://github.com/CosmWasm/wasmvm/releases/download/v${WASMVM_VERSION}/libwasmvm_muslc.x86_64.a \ + -O /usr/lib/libwasmvm_muslc.a; \ + fi + +COPY go.sum go.mod ./ +RUN go mod download + +COPY . . + +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + --mount=type=cache,target=/sai-trading/temp \ + CGO_ENABLED=1 \ + go build -tags "netgo muslc" \ + -ldflags '-linkmode=external -extldflags "-Wl,-z,muldefs -static -lstdc++ -lm -lz -lbz2 -lsnappy -llz4 -lzstd -lpthread"' \ + -o ./build/trader ./cmd/trader + +FROM alpine:latest + +WORKDIR /root + +RUN apk --no-cache add ca-certificates + +COPY --from=builder /sai-trading/build/trader /usr/local/bin/trader + +COPY --from=builder /sai-trading/networks.toml /root/networks.toml +COPY --from=builder /sai-trading/auto-trader.localnet.json /root/ +COPY --from=builder /sai-trading/auto-trader.testnet.json /root/ + +ENTRYPOINT ["trader"] \ No newline at end of file diff --git a/sai-trading/README.md b/sai-trading/README.md index e7d134be7..a41f860d5 100644 --- a/sai-trading/README.md +++ b/sai-trading/README.md @@ -46,51 +46,90 @@ https://github.com/mikefarah/yq?tab=readme-ov-file#github-action ## Running the EVM Trader -### Configuration via `.env` file +### Configuration -Create a `.env` file in the root directory to configure the trader: +Create a `.env` file in the root directory (see `.env.example`): ```bash # Account credentials (use either private key OR mnemonic) -EVM_PRIVATE_KEY=0x1234567890abcdef... # Your private key in hex format +EVM_PRIVATE_KEY=0x1234567890abcdef... # OR -EVM_MNEMONIC="word1 word2 word3 ..." # Your BIP39 mnemonic phrase +EVM_MNEMONIC="word1 word2 word3 ..." + +SLACK_WEBHOOK="" +``` + +#### Slack Notification Configuration (Optional) + +Slack notifications are optional. If not configured, errors will still be logged to stdout. + +**Step 1: Set Slack Webhook** + +Set your Slack webhook URL in `.env` file: + +```bash +SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" +``` + +**Step 2: Configure Error Filters** + +Configure which errors to send to Slack in `networks.toml`: + +```toml +[notifications.filters] +include = ["insufficient funds", "gas error", "execution failed"] +exclude = ["expected error", "test"] +``` + +**Filter Logic:** +- **Exclude list**: If any keyword matches, skip the notification (highest priority) +- **Include list**: If not empty, only send if at least one keyword matches +- **Empty filters**: Send all errors to Slack ### Running the trader -**Dynamic trading** (uses config parameters): +**Auto trading** : ```bash -just run-trader -# or with custom parameters: -just run-trader --market-index 0 --leverage-min 5 --leverage-max 20 +just run-trader auto --network testnet ``` -**Static JSON file trading**: +With custom parameters: ```bash +just run-trader auto --market-index 0 --min-leverage 5 --max-leverage 20 --blocks-before-close 20 --network testnet +``` + +Or use a config file: +```bash +go run ./cmd/trader auto --config auto-trader.localnet.json +``` + +**Manual trading**: +```bash +# Open a single position +just run-trader open --trade-type trade --market-index 0 --long false --trade-size 1 --network testnet + +# Using JSON file just run-trader --trade-json sample_txs/open_trade.json ``` ### Available flags -- `--network`: Network mode (`localnet`, `testnet`, `mainnet`) +**Root flags (shared across all commands):** +- `--network`: Network mode (`localnet`, `testnet`, `mainnet`) (default: `localnet`) +- `--evm-rpc`: EVM RPC URL (overrides network mode default) +- `--networks-toml`: Path to networks TOML configuration file (default: `networks.toml`) +- `--contracts-env`: Path to contracts env file (legacy, overrides networks.toml) - `--private-key`: Private key in hex format (overrides `EVM_PRIVATE_KEY` env var) - `--mnemonic`: BIP39 mnemonic phrase (overrides `EVM_MNEMONIC` env var) -- `--contracts-env`: Path to contracts env file (defaults to `.cache/localnet_contracts.env`) -- `--trade-json`: Path to JSON file with trade parameters (overrides dynamic trading) -- `--market-index`: Market index to trade (default: 0) -- `--collateral-index`: Collateral token index (default: 1) -- `--leverage-min`: Minimum leverage (default: 5) -- `--leverage-max`: Maximum leverage (default: 20) -- `--trade-size-min`: Minimum trade size in smallest units (default: 10000) -- `--trade-size-max`: Maximum trade size in smallest units (default: 50000) -- `--enable-limit-order`: Enable limit order trading (default: false) - -### Example `.env` file - -```bash -# Account -EVM_MNEMONIC="guard cream sadness conduct invite crumble clock pudding hole grit liar hotel maid produce squeeze return argue turtle know drive eight casino maze host" -``` - -**Note**: The `.env` file is automatically loaded if present. You can also pass values via command-line flags, which take precedence over environment variables. +**Auto command flags:** +- `--config`: Path to JSON config file (optional) +- `--market-index`: Market index to trade (default: 0) +- `--collateral-index`: Collateral token index (default: 0, uses market's quote token) +- `--min-trade-size`: Minimum trade size in smallest units (default: 1000000) +- `--max-trade-size`: Maximum trade size in smallest units (default: 5000000) +- `--min-leverage`: Minimum leverage (default: 1, e.g., 1 for 1x) +- `--max-leverage`: Maximum leverage (default: 10, e.g., 10 for 10x) +- `--blocks-before-close`: Number of blocks to wait before closing a position (default: 20) +- `--max-open-positions`: Maximum number of positions to keep open at once (default: 5) +- `--loop-delay`: Delay in seconds between each loop iteration (default: 5) diff --git a/sai-trading/auto-trader.localnet.json b/sai-trading/auto-trader.localnet.json new file mode 100644 index 000000000..500297282 --- /dev/null +++ b/sai-trading/auto-trader.localnet.json @@ -0,0 +1,20 @@ +{ + "network": { + "mode": "localnet", + "evm_rpc_url": "http://localhost:8545", + "networks_toml": "networks.toml" + }, + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 10000, + "max_trade_size": 50000, + "min_leverage": 2, + "max_leverage": 8 + }, + "bot": { + "blocks_before_close": 20, + "max_open_positions": 3, + "loop_delay_seconds": 5 + } +} diff --git a/sai-trading/auto-trader.testnet.json b/sai-trading/auto-trader.testnet.json new file mode 100644 index 000000000..1bfeca1ff --- /dev/null +++ b/sai-trading/auto-trader.testnet.json @@ -0,0 +1,19 @@ +{ + "network": { + "mode": "testnet", + "networks_toml": "networks.toml" + }, + "trading": { + "market_index": 0, + "collateral_index": 3, + "min_trade_size": 10000, + "max_trade_size": 100000, + "min_leverage": 1, + "max_leverage": 5 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 2, + "loop_delay_seconds": 10 + } +} diff --git a/sai-trading/cmd/trader/main.go b/sai-trading/cmd/trader/main.go index 1c6f97d21..582eed875 100644 --- a/sai-trading/cmd/trader/main.go +++ b/sai-trading/cmd/trader/main.go @@ -19,6 +19,7 @@ var ( // Network config (shared across subcommands) evmRPCUrl string contractsEnvFile string + networksTomlFile string networkMode string // Account (shared across subcommands) @@ -37,7 +38,8 @@ to interact with Sai Perps contracts.`, // Shared flags for all subcommands rootCmd.PersistentFlags().StringVar(&networkMode, "network", "localnet", "Network mode: localnet, testnet, or mainnet") rootCmd.PersistentFlags().StringVar(&evmRPCUrl, "evm-rpc", "", "EVM RPC URL (overrides network mode default)") - rootCmd.PersistentFlags().StringVar(&contractsEnvFile, "contracts-env", "", "Path to contracts env file (auto-detected if not set)") + rootCmd.PersistentFlags().StringVar(&networksTomlFile, "networks-toml", "networks.toml", "Path to networks TOML configuration file") + rootCmd.PersistentFlags().StringVar(&contractsEnvFile, "contracts-env", "", "Path to contracts env file (legacy, overrides networks.toml)") rootCmd.PersistentFlags().StringVar(&privateKeyHex, "private-key", "", "Private key in hex format (or set EVM_PRIVATE_KEY env var)") rootCmd.PersistentFlags().StringVar(&mnemonic, "mnemonic", "", "BIP39 mnemonic phrase (or set EVM_MNEMONIC env var)") @@ -46,6 +48,7 @@ to interact with Sai Perps contracts.`, rootCmd.AddCommand(newCloseCmd()) rootCmd.AddCommand(newListCmd()) rootCmd.AddCommand(newPositionsCmd()) + rootCmd.AddCommand(newAutoCmd()) // Default to open command for backward compatibility rootCmd.RunE = newOpenCmd().RunE @@ -61,11 +64,7 @@ func newOpenCmd() *cobra.Command { var ( // Strategy config tradeSize uint64 - tradeSizeMin uint64 - tradeSizeMax uint64 leverage uint64 - leverageMin uint64 - leverageMax uint64 long bool marketIndex uint64 collateralIndex uint64 @@ -85,9 +84,12 @@ Examples: # Market order (auto-fetch price) trader open --market-index 0 --leverage 5 --long true - # Limit order (must specify trigger price) + # Limit order with explicit trigger price trader open --trade-type limit --market-index 0 --open-price 70000 --long + # Limit order with auto-fetch price (uses oracle price as-is) + trader open --trade-type limit --market-index 0 --leverage 5 --long + # Short position trader open --market-index 0 --long=false @@ -102,23 +104,17 @@ Examples: if cmd.Flags().Changed("open-price") { openPricePtr = &openPrice } - return runOpen(tradeSize, tradeSizeMin, tradeSizeMax, leverage, leverageMin, leverageMax, longPtr, marketIndex, collateralIndex, tradeType, openPricePtr, tradeJSONFile) + return runOpen(tradeSize, leverage, longPtr, marketIndex, collateralIndex, tradeType, openPricePtr, tradeJSONFile) }, } // Strategy flags - exact values (override ranges if set) cmd.Flags().Uint64Var(&tradeSize, "trade-size", 0, "Exact trade size in smallest units (overrides min/max)") - cmd.Flags().Uint64Var(&leverage, "leverage", 0, "Exact leverage (e.g., 10 for 10x, overrides min/max)") - cmd.Flags().BoolVar(&long, "long", false, "Trade direction: true for long, false for short (default: random)") - cmd.Flags().Float64Var(&openPrice, "open-price", 0, "Open price (required for limit/stop orders, optional for market orders)") - - // Strategy flags - ranges (used if exact values not set) - cmd.Flags().Uint64Var(&tradeSizeMin, "trade-size-min", 10_000, "Minimum trade size in smallest units") - cmd.Flags().Uint64Var(&tradeSizeMax, "trade-size-max", 50_000, "Maximum trade size in smallest units") - cmd.Flags().Uint64Var(&leverageMin, "leverage-min", 5, "Minimum leverage (e.g., 5 for 5x)") - cmd.Flags().Uint64Var(&leverageMax, "leverage-max", 20, "Maximum leverage (e.g., 20 for 20x)") + cmd.Flags().Uint64Var(&leverage, "leverage", 0, "Exact leverage (e.g., 10 for 10x, default: 1)") + cmd.Flags().BoolVar(&long, "long", false, "Trade direction: true for long, false for short (default: true)") + cmd.Flags().Float64Var(&openPrice, "open-price", 0, "Open price (optional: if not set, fetched from oracle and used as-is)") cmd.Flags().Uint64Var(&marketIndex, "market-index", 0, "Market index to trade") - cmd.Flags().Uint64Var(&collateralIndex, "collateral-index", 1, "Collateral token index") + cmd.Flags().Uint64Var(&collateralIndex, "collateral-index", 0, "Collateral token index") cmd.Flags().StringVar(&tradeType, "trade-type", "", "Trade type: 'trade' (market), 'limit', or 'stop' (default: 'trade')") cmd.Flags().StringVar(&tradeJSONFile, "trade-json", "", "Path to JSON file with open_trade parameters (overrides dynamic trading)") @@ -134,7 +130,7 @@ func newCloseCmd() *cobra.Command { Short: "Close a market trade order in Sai Perps", Long: `Close a market trade order (position) in Sai Perps. -This command sends a close_trade_market message to the perp contract to close a specific trade position.`, +This command sends a close_trade message to the perp contract to close a specific trade position.`, RunE: func(cmd *cobra.Command, args []string) error { return runClose(tradeIndex) }, @@ -155,7 +151,7 @@ func newListCmd() *cobra.Command { This command queries the perp contract to display all configured markets with their details.`, RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd) + return runList() }, } @@ -178,7 +174,62 @@ This command queries the perp contract to display all trades/positions with thei return cmd } -func runOpen(tradeSize, tradeSizeMin, tradeSizeMax, leverage, leverageMin, leverageMax uint64, long *bool, marketIndex, collateralIndex uint64, tradeType string, openPrice *float64, tradeJSONFile string) error { +// newAutoCmd creates the auto subcommand +func newAutoCmd() *cobra.Command { + var ( + configFile string + marketIndex uint64 + collateralIndex uint64 + minTradeSize uint64 + maxTradeSize uint64 + minLeverage uint64 + maxLeverage uint64 + blocksBeforeClose uint64 + maxOpenPositions int + loopDelaySeconds int + ) + + cmd := &cobra.Command{ + Use: "auto", + Short: "Run automated random trading", + Long: `Run an automated trading bot that randomly opens and closes positions. + +This command continuously: +- Opens random trades with random parameters (size, leverage, direction) +- Tracks open positions and their opening block numbers +- Closes positions after a specified number of blocks +- Checks balance before opening trades to avoid insufficient funds + +Examples: + # Run with config file + trader auto --config auto-trader.json + + # Run with defaults (market 0, random size/leverage, close after 20 blocks) + trader auto + + # Custom parameters via flags (overrides config file) + trader auto --config auto-trader.json --min-leverage 5 --max-leverage 20`, + RunE: func(cmd *cobra.Command, args []string) error { + return runAuto(configFile, marketIndex, collateralIndex, minTradeSize, maxTradeSize, + minLeverage, maxLeverage, blocksBeforeClose, maxOpenPositions, loopDelaySeconds, cmd) + }, + } + + cmd.Flags().StringVar(&configFile, "config", "", "Path to JSON config file (optional)") + cmd.Flags().Uint64Var(&marketIndex, "market-index", 0, "Market index to trade") + cmd.Flags().Uint64Var(&collateralIndex, "collateral-index", 0, "Collateral token index (default: use market's quote token)") + cmd.Flags().Uint64Var(&minTradeSize, "min-trade-size", 1000000, "Minimum trade size in smallest units") + cmd.Flags().Uint64Var(&maxTradeSize, "max-trade-size", 5000000, "Maximum trade size in smallest units") + cmd.Flags().Uint64Var(&minLeverage, "min-leverage", 1, "Minimum leverage (e.g., 1 for 1x)") + cmd.Flags().Uint64Var(&maxLeverage, "max-leverage", 10, "Maximum leverage (e.g., 10 for 10x)") + cmd.Flags().Uint64Var(&blocksBeforeClose, "blocks-before-close", 20, "Number of blocks to wait before closing a position") + cmd.Flags().IntVar(&maxOpenPositions, "max-open-positions", 5, "Maximum number of positions to keep open at once") + cmd.Flags().IntVar(&loopDelaySeconds, "loop-delay", 5, "Delay in seconds between each loop iteration") + + return cmd +} + +func runOpen(tradeSize, leverage uint64, long *bool, marketIndex, collateralIndex uint64, tradeType string, openPrice *float64, tradeJSONFile string) error { cfg, err := setupConfig(true) if err != nil { return err @@ -186,41 +237,28 @@ func runOpen(tradeSize, tradeSizeMin, tradeSizeMax, leverage, leverageMin, lever // Strategy config cfg.TradeSize = tradeSize - cfg.TradeSizeMin = tradeSizeMin - cfg.TradeSizeMax = tradeSizeMax cfg.Leverage = leverage - cfg.LeverageMin = leverageMin - cfg.LeverageMax = leverageMax cfg.Long = long cfg.MarketIndex = marketIndex cfg.CollateralIndex = collateralIndex cfg.OpenPrice = openPrice // Validate trade-type if provided if tradeType != "" { - if tradeType != "trade" && tradeType != "limit" && tradeType != "stop" { - return fmt.Errorf("invalid trade-type: %s (must be 'trade', 'limit', or 'stop')", tradeType) + if !evmtrader.IsValidTradeType(tradeType) { + return fmt.Errorf("invalid trade-type: %s (must be '%s', '%s', or '%s')", + tradeType, evmtrader.TradeTypeMarket, evmtrader.TradeTypeLimit, evmtrader.TradeTypeStop) } cfg.TradeType = tradeType - cfg.EnableLimitOrder = (tradeType == "limit" || tradeType == "stop") - // Validate open-price is provided for limit/stop orders - if (tradeType == "limit" || tradeType == "stop") && openPrice == nil { - return fmt.Errorf("--open-price is required for %s orders (trigger price)", tradeType) - } + cfg.EnableLimitOrder = evmtrader.IsLimitOrStopOrder(tradeType) + // Note: --open-price is optional for limit/stop orders + // If not provided, the price will be fetched from oracle and used as-is } else { // Default to market order if not specified - cfg.TradeType = "trade" + cfg.TradeType = evmtrader.TradeTypeMarket cfg.EnableLimitOrder = false } cfg.TradeJSONFile = tradeJSONFile - // Validate config - if cfg.TradeSizeMax > 0 && cfg.TradeSizeMin >= cfg.TradeSizeMax { - return fmt.Errorf("trade-size-min must be less than trade-size-max") - } - if cfg.LeverageMax > 0 && cfg.LeverageMin >= cfg.LeverageMax { - return fmt.Errorf("leverage-min must be less than leverage-max") - } - // Create trader ctx := context.Background() trader, err := createTrader(ctx, cfg) @@ -266,7 +304,7 @@ func runClose(tradeIndex uint64) error { return nil } -func runList(cmd *cobra.Command) error { +func runList() error { cfg, err := setupConfig(false) if err != nil { return err @@ -289,26 +327,40 @@ func runList(cmd *cobra.Command) error { // Display markets if len(markets) == 0 { fmt.Println("No markets found") - return nil + } else { + fmt.Println("Available Markets:") + fmt.Println("==================") + for _, market := range markets { + fmt.Printf("Market Index: %d\n", market.Index) + if market.BaseToken != nil { + fmt.Printf(" Base Token: %d\n", *market.BaseToken) + } + if market.QuoteToken != nil { + fmt.Printf(" Quote Token: %d\n", *market.QuoteToken) + } + if market.MaxOI != nil { + fmt.Printf(" Max OI: %s\n", *market.MaxOI) + } + if market.FeePerBlock != nil { + fmt.Printf(" Fee Per Block: %s\n", *market.FeePerBlock) + } + fmt.Println() + } } - fmt.Println("Available Markets:") - fmt.Println("==================") - for _, market := range markets { - fmt.Printf("Market Index: %d\n", market.Index) - if market.BaseToken != nil { - fmt.Printf(" Base Token: %d\n", *market.BaseToken) - } - if market.QuoteToken != nil { - fmt.Printf(" Quote Token: %d\n", *market.QuoteToken) - } - if market.MaxOI != nil { - fmt.Printf(" Max OI: %s\n", *market.MaxOI) - } - if market.FeePerBlock != nil { - fmt.Printf(" Fee Per Block: %s\n", *market.FeePerBlock) + // Query collaterals + collaterals, err := trader.QueryCollaterals(ctx) + if err != nil { + // Don't fail if collaterals query fails, just log + fmt.Fprintf(os.Stderr, "Warning: Failed to query collaterals: %v\n", err) + } else if len(collaterals) > 0 { + fmt.Println("Available Collaterals:") + fmt.Println("======================") + for _, collateral := range collaterals { + fmt.Printf("Collateral Index: %d\n", collateral.Index) + fmt.Printf(" Denom: %s\n", collateral.Denom) + fmt.Println() } - fmt.Println() } return nil @@ -336,6 +388,100 @@ func runPositions() error { return nil } +func runAuto(configFile string, marketIndex, collateralIndex, minTradeSize, maxTradeSize, minLeverage, maxLeverage, + blocksBeforeClose uint64, maxOpenPositions, loopDelaySeconds int, cmd *cobra.Command) error { + + var autoCfg evmtrader.AutoTradingConfig + + // Load from config file if provided + if configFile != "" { + jsonCfg, err := evmtrader.LoadAutoTradingConfig(configFile) + if err != nil { + return fmt.Errorf("load config file: %w", err) + } + + // Convert JSON config to AutoTradingConfig + autoCfg = jsonCfg.ToAutoTradingConfig() + + // Apply network settings from config if present + if jsonCfg.Network != nil { + if jsonCfg.Network.Mode != "" && !cmd.Flags().Changed("network") { + networkMode = jsonCfg.Network.Mode + } + if jsonCfg.Network.EVMRPCUrl != "" && !cmd.Flags().Changed("evm-rpc") { + evmRPCUrl = jsonCfg.Network.EVMRPCUrl + } + if jsonCfg.Network.NetworksToml != "" && !cmd.Flags().Changed("networks-toml") { + networksTomlFile = jsonCfg.Network.NetworksToml + } + } + + fmt.Printf("Loaded config from: %s\n", configFile) + } else { + // Use command-line flags as defaults + autoCfg = evmtrader.AutoTradingConfig{ + MarketIndex: marketIndex, + CollateralIndex: collateralIndex, + MinTradeSize: minTradeSize, + MaxTradeSize: maxTradeSize, + MinLeverage: minLeverage, + MaxLeverage: maxLeverage, + BlocksBeforeClose: blocksBeforeClose, + MaxOpenPositions: maxOpenPositions, + LoopDelaySeconds: loopDelaySeconds, + } + } + + // Override config file with command-line flags if they were explicitly set + if cmd.Flags().Changed("market-index") { + autoCfg.MarketIndex = marketIndex + } + if cmd.Flags().Changed("collateral-index") { + autoCfg.CollateralIndex = collateralIndex + } + if cmd.Flags().Changed("min-trade-size") { + autoCfg.MinTradeSize = minTradeSize + } + if cmd.Flags().Changed("max-trade-size") { + autoCfg.MaxTradeSize = maxTradeSize + } + if cmd.Flags().Changed("min-leverage") { + autoCfg.MinLeverage = minLeverage + } + if cmd.Flags().Changed("max-leverage") { + autoCfg.MaxLeverage = maxLeverage + } + if cmd.Flags().Changed("blocks-before-close") { + autoCfg.BlocksBeforeClose = blocksBeforeClose + } + if cmd.Flags().Changed("max-open-positions") { + autoCfg.MaxOpenPositions = maxOpenPositions + } + if cmd.Flags().Changed("loop-delay") { + autoCfg.LoopDelaySeconds = loopDelaySeconds + } + + cfg, err := setupConfig(true) + if err != nil { + return err + } + + // Create trader + ctx := context.Background() + trader, err := createTrader(ctx, cfg) + if err != nil { + return err + } + defer trader.Close() + + // Run the auto-trading loop + if err := trader.RunAutoTrading(ctx, autoCfg); err != nil { + return fmt.Errorf("auto trading: %w", err) + } + + return nil +} + // setupConfig creates and configures an EVMTrader config with network settings and authentication. // requireAuth determines whether a valid private key is required (false for read-only queries). func setupConfig(requireAuth bool) (evmtrader.Config, error) { @@ -344,39 +490,80 @@ func setupConfig(requireAuth bool) (evmtrader.Config, error) { cfg := evmtrader.Config{} - // Set network defaults if not overridden + // Try to load from TOML file first (unless --contracts-env is explicitly set for legacy mode) var grpcUrl, chainID string - if evmRPCUrl == "" { - switch networkMode { - case "localnet": - evmRPCUrl = "http://localhost:8545" - grpcUrl = "localhost:9090" - chainID = "nibiru-localnet-0" - // case "testnet": - // evmRPCUrl = "https://evm-rpc.testnet-2.nibiru.fi" - // grpcUrl = "grpc.testnet-2.nibiru.fi:443" - // chainID = "nibiru-testnet-2" - // case "mainnet": - // evmRPCUrl = "https://evm-rpc.nibiru.fi" - // grpcUrl = "grpc.nibiru.fi:443" - // chainID = "nibiru-mainnet-1" - default: - return cfg, fmt.Errorf("unknown network mode: %s (use: localnet, testnet, mainnet)", networkMode) - } - } else { - // If EVM RPC is set but gRPC/ChainID aren't, use localnet defaults - if grpcUrl == "" { - grpcUrl = "localhost:9090" + useTOML := contractsEnvFile == "" && networksTomlFile != "" + + if useTOML { + // Check if TOML file exists + if _, err := os.Stat(networksTomlFile); err == nil { + // Load network config from TOML + networkConfig, err := evmtrader.LoadNetworkConfig(networksTomlFile) + if err != nil { + // Fall back to hardcoded defaults if TOML fails to load + fmt.Fprintf(os.Stderr, "Warning: Failed to load TOML config: %v, using hardcoded defaults\n", err) + useTOML = false + } else { + netInfo, err := evmtrader.GetNetworkInfo(networkConfig, networkMode) + if err != nil { + return cfg, err + } + + // Use TOML config unless overridden by flags + if evmRPCUrl == "" { + evmRPCUrl = netInfo.EVMRPCUrl + } + grpcUrl = netInfo.GrpcUrl + chainID = netInfo.ChainID + + // Load contract addresses from TOML + contractAddrs := evmtrader.ContractAddressesFromNetworkInfo(netInfo) + cfg.ContractAddresses = &contractAddrs + + // Load notification filters from TOML + cfg.SlackErrorFilters = &networkConfig.Notifications.Filters + } + } else { + // TOML file doesn't exist, fall back to hardcoded defaults + useTOML = false } - if chainID == "" { - chainID = "nibiru-localnet-0" + } + + // Fall back to hardcoded defaults if not using TOML or if TOML failed + if !useTOML { + if evmRPCUrl == "" { + switch networkMode { + case "localnet": + evmRPCUrl = "http://localhost:8545" + grpcUrl = "localhost:9090" + chainID = "nibiru-localnet-0" + case "testnet": + evmRPCUrl = "https://evm-rpc.testnet-2.nibiru.fi" + grpcUrl = "grpc.testnet-2.nibiru.fi:443" + chainID = "nibiru-testnet-2" + case "mainnet": + evmRPCUrl = "https://evm-rpc.nibiru.fi" + grpcUrl = "grpc.nibiru.fi:443" + chainID = "nibiru-mainnet-1" + default: + return cfg, fmt.Errorf("unknown network mode: %s (use: localnet, testnet, mainnet)", networkMode) + } + } else { + // If EVM RPC is set but gRPC/ChainID aren't, use localnet defaults + if grpcUrl == "" { + grpcUrl = "localhost:9090" + } + if chainID == "" { + chainID = "nibiru-localnet-0" + } } } + cfg.EVMRPCUrl = evmRPCUrl cfg.GrpcUrl = grpcUrl cfg.ChainID = chainID - // Auto-detect contracts env file if not provided + // Auto-detect contracts env file if not provided (legacy mode) if contractsEnvFile == "" { contractsEnvFile = detectContractsEnvFile(networkMode) } @@ -414,6 +601,9 @@ func setupConfig(requireAuth bool) (evmtrader.Config, error) { cfg.PrivateKeyHex = privKey + // Load Slack webhook from environment variable + cfg.SlackWebhook = os.Getenv("SLACK_WEBHOOK") + return cfg, nil } diff --git a/sai-trading/coverage.out b/sai-trading/coverage.out new file mode 100644 index 000000000..97980bdd9 --- /dev/null +++ b/sai-trading/coverage.out @@ -0,0 +1,621 @@ +mode: set +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:46.79,48.16 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:48.16,50.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:52.2,53.51 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:53.51,55.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:58.2,58.39 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:58.39,60.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:62.2,62.18 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:66.52,68.57 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:68.57,71.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:72.2,72.55 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:72.55,75.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:76.2,76.34 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:76.34,78.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:79.2,79.35 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:79.35,81.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:84.2,84.36 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:84.36,86.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:87.2,87.35 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:87.35,89.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:90.2,90.35 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:90.35,92.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:94.2,94.12 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_config.go:98.75,110.2 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:32.86,46.6 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:46.6,49.17 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:49.17,52.12 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:55.3,59.17 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:59.17,62.12 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:66.3,67.32 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:67.32,68.20 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:68.20,70.5 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:73.3,78.39 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:78.39,80.18 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:80.18,89.13 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:93.4,94.48 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:94.48,96.18 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:96.18,97.14 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:100.5,106.67 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:106.67,108.6 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:108.11,113.6 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:116.5,117.10 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:123.3,123.62 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:123.62,135.23 8 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:135.23,137.5 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:137.10,139.19 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:139.19,142.14 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:145.5,146.41 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:146.41,152.14 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:157.4,158.28 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:158.28,160.19 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:160.19,163.14 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:165.5,165.33 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:165.33,168.14 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:170.5,170.41 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:174.4,175.18 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:175.18,178.13 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:182.4,183.18 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:183.18,186.13 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:190.4,191.36 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:191.36,194.5 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:194.10,197.15 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:197.15,200.37 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:200.37,203.7 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:203.12,206.7 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:207.11,210.37 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:210.37,213.7 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:213.12,216.7 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:220.4,243.60 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:243.60,253.55 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:253.55,259.6 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:260.10,263.19 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:263.19,265.6 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:265.11,267.38 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:267.38,268.72 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:268.72,278.13 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:283.5,283.32 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:285.9,287.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:290.3,290.64 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:295.43,296.16 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:296.16,298.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:299.2,301.16 3 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:301.16,304.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:305.2,305.25 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:309.24,311.43 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:311.43,313.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:314.2,314.20 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:318.31,321.11 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:322.12,323.25 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:324.9,325.24 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:326.10,327.23 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:332.46,333.16 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:333.16,335.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:336.2,339.43 3 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:339.43,341.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/auto_trader.go:343.2,345.29 3 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:101.71,103.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:103.16,105.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:106.2,108.29 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:108.29,110.49 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:110.49,111.12 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:113.3,114.19 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:114.19,115.12 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:117.3,119.14 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:120.25,121.29 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:122.23,123.27 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:124.24,125.28 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:126.23,127.32 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:128.17,129.27 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:132.2,132.19 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:136.64,138.16 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:138.16,140.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:142.2,143.54 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:143.54,145.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:147.2,147.20 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:151.85,152.21 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:153.18,154.31 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:155.17,156.30 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:157.17,158.30 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:159.10,160.66 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:165.79,173.2 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:176.40,177.73 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:177.73,178.65 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:178.65,180.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:185.37,186.23 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:186.23,188.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:189.2,189.23 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:189.23,191.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:192.2,192.32 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:192.32,195.66 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/config.go:195.66,197.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/erc20.go:9.28,11.2 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/erc20.go:14.37,16.2 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:14.71,23.38 3 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:23.38,32.20 3 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:32.20,33.12 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:37.3,37.41 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:37.41,40.33 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:40.33,41.68 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:41.68,43.6 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:47.4,47.27 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:47.27,49.70 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:49.70,50.36 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:50.36,51.80 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:51.80,53.8 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:59.4,59.40 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:59.40,61.68 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:61.68,62.34 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:62.34,63.78 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:63.78,65.8 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:74.2,74.23 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:74.23,76.67 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:76.67,78.4 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:81.3,81.79 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:81.79,83.57 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:83.57,84.58 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:84.58,86.6 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:89.4,90.60 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:90.60,91.65 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:91.65,93.6 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:98.2,98.125 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:102.62,106.46 3 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:106.46,108.3 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/event_parser.go:110.2,111.29 2 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:42.63,47.25 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:47.25,49.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:50.2,50.29 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:50.29,52.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:55.2,56.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:56.16,58.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:61.2,62.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:62.16,64.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:65.2,75.139 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:75.139,77.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:77.8,79.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:80.2,83.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:83.16,85.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:88.2,93.34 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:93.34,95.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:95.8,98.17 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:98.17,100.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:102.2,114.20 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:118.29,119.21 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:119.21,121.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:123.2,123.23 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:123.23,124.44 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:124.44,126.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:131.68,133.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:133.16,135.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:138.2,141.16 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:141.16,143.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:146.2,147.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:147.16,149.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:150.2,150.19 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:150.19,152.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:155.2,155.42 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:159.101,162.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:162.16,164.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:165.2,165.19 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:165.19,167.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:170.2,171.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:171.16,173.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:176.2,177.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:177.16,179.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:182.2,184.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:184.16,187.3 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:189.2,190.18 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:190.18,192.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:194.2,201.12 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:205.78,207.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:207.16,209.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:212.2,213.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:213.16,215.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:218.2,219.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:219.16,221.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:223.2,229.12 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:233.48,235.36 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:235.36,238.3 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:239.2,241.47 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:245.53,249.30 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:249.30,251.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:254.2,255.36 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:255.36,258.3 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:261.2,261.36 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:261.36,263.47 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:263.47,264.60 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:264.60,266.73 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:266.73,268.6 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:270.5,270.35 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:270.35,272.75 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:272.75,274.7 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:280.3,280.47 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:280.47,284.60 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:284.60,285.73 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:285.73,287.11 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:292.4,292.16 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:292.16,293.35 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:293.35,295.62 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:295.62,296.76 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:296.76,298.13 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:301.6,301.17 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:301.17,302.12 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:308.4,308.16 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:308.16,310.5 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:315.2,333.56 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:337.71,339.27 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:339.27,344.29 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:344.29,345.9 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:348.2,348.20 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:352.79,354.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:354.16,356.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:358.2,359.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:359.16,361.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:363.2,367.16 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:367.16,369.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:370.2,370.25 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/evm_trader.go:374.40,376.2 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:19.130,22.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:22.16,24.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:27.2,32.16 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:32.16,34.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:37.2,45.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:45.16,47.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:50.2,51.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:51.16,53.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:54.2,54.24 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:54.24,56.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:59.2,60.9 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:60.9,62.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:64.2,64.27 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:68.73,71.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:71.16,73.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:76.2,77.31 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:77.31,78.19 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:78.19,80.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:83.2,83.29 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:83.29,86.3 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:88.2,90.38 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:90.38,98.40 8 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:98.40,100.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:101.3,101.64 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:101.64,103.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:104.3,104.16 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:107.2,107.12 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:111.77,119.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:119.16,121.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:124.2,125.70 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:125.70,130.67 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:130.67,132.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:133.3,133.31 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:137.2,138.47 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:138.47,141.88 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:141.88,143.76 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:143.76,144.13 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:149.3,150.17 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:150.17,155.12 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:157.3,157.37 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:160.2,160.21 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:164.77,176.16 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:176.16,178.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:181.2,182.66 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:182.66,184.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:187.2,188.32 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:188.32,205.87 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:205.87,207.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:208.3,212.92 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:212.92,214.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:215.3,219.94 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:219.94,221.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:222.3,224.46 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:227.2,227.26 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:231.95,240.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:240.16,242.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:245.2,246.67 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:246.67,248.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:250.2,255.74 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:255.74,257.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:257.8,260.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:263.2,264.9 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:264.9,266.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:267.2,268.72 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:268.72,270.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:271.2,275.9 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:275.9,277.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:278.2,279.74 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:279.74,281.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:282.2,285.52 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:285.52,287.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:290.2,290.65 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:290.65,292.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:294.2,294.21 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:298.95,309.16 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:309.16,311.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:316.2,319.70 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:319.70,321.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:323.2,323.31 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:323.31,325.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:327.2,328.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:328.16,330.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:332.2,332.19 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:337.107,349.16 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:349.16,351.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:355.2,358.73 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:358.73,360.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:362.2,362.41 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:362.41,364.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:366.2,367.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:367.16,369.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:371.2,371.18 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:375.142,377.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:377.16,379.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:380.2,386.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:386.16,388.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:389.2,389.40 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:393.148,396.2 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:400.85,407.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:407.16,410.75 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:410.75,415.68 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:415.68,417.5 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:420.3,420.33 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:420.33,423.57 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:423.57,426.97 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:426.97,428.86 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:428.86,429.15 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:434.5,435.19 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:435.19,436.14 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:438.5,441.7 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:443.4,443.27 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:448.2,449.35 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:449.35,451.32 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:451.32,456.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:459.2,459.25 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:469.103,478.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:478.16,480.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:483.2,486.71 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:486.71,488.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:490.2,490.32 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:490.32,492.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:494.2,494.34 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:499.91,508.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:508.16,511.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:514.2,519.70 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:519.70,522.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/querier.go:524.2,524.18 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:13.84,15.52 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:15.52,17.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:19.2,30.29 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:30.29,32.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:34.2,34.68 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:34.68,36.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:37.2,41.22 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:41.22,43.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:44.2,44.22 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:44.22,46.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:48.2,52.35 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:56.83,58.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:58.16,60.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:63.2,64.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:64.16,66.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:68.2,69.55 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:69.55,71.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:73.2,74.9 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:74.9,76.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:79.2,80.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:80.16,82.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:85.2,85.68 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:85.68,87.17 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:87.17,89.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:90.3,91.28 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:95.2,95.42 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:105.101,116.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:116.16,118.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:119.2,123.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:123.16,125.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:126.2,129.80 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:129.80,131.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:131.8,131.28 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:131.28,133.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:137.2,137.81 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:137.81,139.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:139.8,139.23 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:139.23,141.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:141.8,143.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:147.2,147.41 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:147.41,149.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:152.2,152.73 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:152.73,154.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:157.2,157.71 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:157.71,159.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:164.2,164.55 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:164.55,166.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:169.2,169.41 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:169.41,170.65 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:170.65,172.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:175.2,175.20 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:180.75,182.26 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:182.26,184.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:186.2,188.24 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:189.14,190.14 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:190.14,192.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:193.3,195.10 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:195.10,197.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:198.15,199.29 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:200.13,201.22 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:202.11,203.29 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:204.10,205.108 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:208.2,208.33 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:208.33,210.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:212.2,212.17 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:222.57,224.26 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:224.26,226.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:229.2,229.29 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:229.29,231.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:234.2,234.68 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:234.68,236.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:239.2,239.28 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:239.28,241.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:244.2,244.81 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:244.81,246.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:248.2,248.12 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:253.108,255.24 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:255.24,257.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:259.2,260.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:260.16,262.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:264.2,264.25 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:269.109,271.24 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:271.24,273.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:275.2,276.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:276.16,278.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:280.2,281.21 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:288.96,290.27 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:290.27,292.43 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:292.43,294.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:297.3,297.13 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:300.2,301.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:301.16,303.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:306.2,306.56 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:306.56,308.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:311.2,311.16 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:311.16,313.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:315.2,316.12 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:320.105,322.73 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:322.73,324.17 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:324.17,326.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:327.3,327.18 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:331.2,331.73 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:331.73,333.17 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:333.17,335.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:336.3,336.18 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:339.2,339.12 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:343.67,345.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:345.16,347.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:349.2,349.17 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:349.17,351.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:353.2,353.20 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:357.67,359.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:359.16,361.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:363.2,363.17 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:363.17,365.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:367.2,367.20 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:372.98,374.25 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:374.25,376.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:378.2,379.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:379.16,381.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:383.2,383.18 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_builder.go:387.79,397.2 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:16.109,20.26 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:20.26,22.17 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:22.17,24.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:25.3,25.31 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:25.31,27.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:28.3,29.135 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:30.8,32.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:36.2,48.28 7 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:48.28,52.3 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:52.8,55.17 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:55.17,57.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:58.3,58.17 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:58.17,61.4 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:62.3,64.62 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:69.2,73.40 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:73.40,75.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:78.2,91.8 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:96.78,99.25 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:99.25,102.32 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:102.32,105.4 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:106.8,109.22 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:109.22,112.4 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:115.2,115.22 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:120.48,121.24 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:121.24,123.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:125.2,125.10 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:130.47,131.23 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:131.23,133.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:135.2,135.13 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:140.49,141.27 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:141.27,144.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:147.2,147.44 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:147.44,149.19 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:149.19,151.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:152.3,152.17 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:155.2,155.16 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:161.94,162.34 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:162.34,165.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:168.2,169.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:169.16,171.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:172.2,172.19 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:177.86,179.2 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:183.104,186.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:186.16,188.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:190.2,190.29 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:190.29,192.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:193.2,193.30 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:193.30,195.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:198.2,199.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:199.16,202.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:204.2,204.18 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:209.110,211.2 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:215.74,218.35 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:218.35,220.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:222.2,231.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:236.82,238.28 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:238.28,240.27 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:240.27,243.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:245.3,245.46 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:245.46,247.31 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:247.31,250.5 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:252.4,252.17 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:254.3,254.13 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:258.2,258.28 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:258.28,260.27 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:260.27,263.4 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:264.3,264.13 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_params.go:268.2,268.12 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_types.go:11.48,13.2 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_types.go:16.46,20.2 1 1 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/trade_types.go:24.48,26.2 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:24.152,27.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:27.16,29.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:32.2,39.33 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:39.33,41.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:44.2,45.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:45.16,47.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:49.2,49.40 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:49.40,51.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:54.2,74.60 6 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:74.60,76.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:80.2,82.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:82.16,84.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:87.2,88.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:88.16,90.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:93.2,97.16 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:97.16,99.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:101.2,101.34 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:101.34,103.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:106.2,113.52 7 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:113.52,114.10 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:115.21,116.25 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:117.17,119.45 2 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:119.45,121.5 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:122.20,123.74 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:127.2,127.87 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:131.175,138.16 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:138.16,141.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:143.2,151.16 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:151.16,153.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:156.2,156.84 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:160.128,172.16 5 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:172.16,174.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:177.2,177.84 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:181.59,183.56 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:183.56,187.24 3 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:187.24,206.4 4 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:207.3,207.181 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:211.2,211.104 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:211.104,213.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:216.2,216.96 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:216.96,218.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:221.2,221.131 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:221.131,223.3 1 0 +github.com/NibiruChain/nibiru/sai-trading/services/evmtrader/transaction.go:226.2,226.137 1 0 diff --git a/sai-trading/go.mod b/sai-trading/go.mod index 85dbedc6a..38759f6c0 100644 --- a/sai-trading/go.mod +++ b/sai-trading/go.mod @@ -14,13 +14,14 @@ require ( // Cosmos-SDK and IBC github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect github.com/cosmos/cosmos-sdk v0.47.11-nibiru.5 - github.com/cosmos/gogoproto v1.4.10 // indirect + github.com/cosmos/gogoproto v1.7.2 // indirect github.com/cosmos/ibc-go/v7 v7.4.0 // indirect github.com/ethereum/go-ethereum v1.14.13 ) require ( github.com/joho/godotenv v1.5.1 + github.com/pelletier/go-toml/v2 v2.1.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.62.1 @@ -169,7 +170,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -229,8 +229,8 @@ require ( google.golang.org/api v0.155.0 // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/sai-trading/go.sum b/sai-trading/go.sum index cdcc078cd..a9ce66c91 100644 --- a/sai-trading/go.sum +++ b/sai-trading/go.sum @@ -910,8 +910,8 @@ github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4x github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= github.com/cosmos/gogoproto v1.4.2/go.mod h1:cLxOsn1ljAHSV527CHOtaIP91kK6cCrZETRBrkzItWU= -github.com/cosmos/gogoproto v1.4.10 h1:QH/yT8X+c0F4ZDacDv3z+xE3WU1P1Z3wQoLMBRJoKuI= -github.com/cosmos/gogoproto v1.4.10/go.mod h1:3aAZzeRWpAwr+SS/LLkICX2/kDFyaYVzckBDzygIxek= +github.com/cosmos/gogoproto v1.7.2 h1:5G25McIraOC0mRFv9TVO139Uh3OklV2hczr13KKVHCA= +github.com/cosmos/gogoproto v1.7.2/go.mod h1:8S7w53P1Y1cHwND64o0BnArT6RmdgIvsBuco6uTllsk= github.com/cosmos/iavl v0.20.0 h1:fTVznVlepH0KK8NyKq8w+U7c2L6jofa27aFX6YGlm38= github.com/cosmos/iavl v0.20.0/go.mod h1:WO7FyvaZJoH65+HFOsDir7xU9FWk2w9cHXNW1XHcl7A= github.com/cosmos/ibc-go/modules/light-clients/08-wasm v0.3.2-0.20240730185603-13c071f0b34d h1:QJK/Zr0HblNx6z1x5LXIHEblY7DCCftMkUBcjpKe1qY= @@ -2774,8 +2774,8 @@ google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/b google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v0.0.0-20170208002647-2a6bf6142e96/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2848,8 +2848,8 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= @@ -2887,8 +2887,8 @@ gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sai-trading/justfile b/sai-trading/justfile index e392e066d..a325b4bd4 100644 --- a/sai-trading/justfile +++ b/sai-trading/justfile @@ -16,4 +16,13 @@ e2e-deploy: bun run e2e_deploy.ts run-trader *args: - go run ./cmd/trader {{args}} + go run ./cmd/trader {{args}} + +run-auto-trader *args: + go run ./cmd/trader auto {{args}} + +run-auto-localnet: + go run ./cmd/trader auto --config auto-trader.localnet.json + +run-auto-testnet: + go run ./cmd/trader auto --config auto-trader.testnet.json diff --git a/sai-trading/networks.toml b/sai-trading/networks.toml new file mode 100644 index 000000000..7da44ed51 --- /dev/null +++ b/sai-trading/networks.toml @@ -0,0 +1,52 @@ +# Network Configuration for Sai Trading +# This file contains contract addresses and RPC endpoints for different networks + +# Slack notification error filtering +[notifications.filters] +include = [] +exclude = [] + +[localnet] +name = "Local Testnet" +evm_rpc_url = "http://localhost:8545" +grpc_url = "localhost:9090" +chain_id = "nibiru-localnet-0" + +[localnet.contracts] +oracle_address = "nibi18cszlvm6pze0x9sz32qnjq4vtd45xehqs8dq7cwy8yhq35wfnn3qm02xqh" +perp_address = "nibi1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrs0gfase" +vault_address = "nibi1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sxn9f7a" + +[localnet.tokens] +stnibi_evm = "0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6" +stnibi_denom = "tf/nibi1zaavvzxez0elundtn32qnk9lkm8kmcsz44g7xl/stnibi" + +[testnet] +name = "Nibiru Testnet 2" +evm_rpc_url = "https://evm-rpc.testnet-2.nibiru.fi" +grpc_url = "grpc.testnet-2.nibiru.fi:443" +chain_id = "nibiru-testnet-2" + +[testnet.contracts] +oracle_address = "nibi1mqlrsvfhm5vzsz0wxr6mh8pzxzpz6dd4g7nuyycjf6gy5zc53fvq3lq2fz" +perp_address = "nibi1qtkcns647w959cj9x2yytateu6dgscfnfkraywwa443pr2erak0s5ux7e5" +evm_interface = "0x282F097930E24e4fba97EE8687d15Ed1f298ad19" + +[testnet.tokens] +usdc_evm = "0xAb68f1D1d91854383fd4Df9016E3040D03e8191a" +stnibi_evm = "0xCae3d404AFB50016154a4B18091351065154E9bD" + +[mainnet] +name = "Nibiru Mainnet" +evm_rpc_url = "https://evm-rpc.nibiru.fi" +grpc_url = "grpc.nibiru.fi:443" +chain_id = "nibiru-mainnet-1" + +[mainnet.contracts] +oracle_address = "nibi1xfwyfwtdame6645lgcs4xvf4u0hpsuvxrcelfwtztu0pv7n4l6hqw5a8gj" +perp_address = "nibi1ntmw2dfvd0qnw5fnwdu9pev2hsnqfdj9ny9n0nzh2a5u8v0scflq930mph" +evm_interface = "0x9F48A925Dda8528b3A5c2A6717Df0F03c8b167c0" + +[mainnet.tokens] +usdc_evm = "0x0829F361A05D993d5CEb035cA6DF3446b060970b" +stnibi_evm = "0xcA0a9Fb5FBF692fa12fD13c0A900EC56Bb3f0a7b" diff --git a/sai-trading/services/evmtrader/auto_config.go b/sai-trading/services/evmtrader/auto_config.go new file mode 100644 index 000000000..cab1e3be1 --- /dev/null +++ b/sai-trading/services/evmtrader/auto_config.go @@ -0,0 +1,110 @@ +package evmtrader + +import ( + "encoding/json" + "fmt" + "os" +) + +// AutoTradingJSONConfig represents the JSON configuration file structure +type AutoTradingJSONConfig struct { + // Network settings (optional, can use command-line flags instead) + Network *NetworkSettings `json:"network,omitempty"` + + // Trading parameters + Trading TradingSettings `json:"trading"` + + // Bot behavior + Bot BotSettings `json:"bot"` +} + +// NetworkSettings contains network-related configuration +type NetworkSettings struct { + Mode string `json:"mode"` // "localnet", "testnet", "mainnet" + EVMRPCUrl string `json:"evm_rpc_url"` // Optional override + NetworksToml string `json:"networks_toml"` // Path to networks.toml +} + +// TradingSettings contains trading strategy parameters +type TradingSettings struct { + MarketIndex uint64 `json:"market_index"` + CollateralIndex uint64 `json:"collateral_index"` + MinTradeSize uint64 `json:"min_trade_size"` + MaxTradeSize uint64 `json:"max_trade_size"` + MinLeverage uint64 `json:"min_leverage"` + MaxLeverage uint64 `json:"max_leverage"` +} + +// BotSettings contains bot behavior parameters +type BotSettings struct { + BlocksBeforeClose uint64 `json:"blocks_before_close"` + MaxOpenPositions int `json:"max_open_positions"` + LoopDelaySeconds int `json:"loop_delay_seconds"` +} + +// LoadAutoTradingConfig loads the auto-trading configuration from a JSON file +func LoadAutoTradingConfig(configPath string) (*AutoTradingJSONConfig, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("read config file: %w", err) + } + + var cfg AutoTradingJSONConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config JSON: %w", err) + } + + // Validate configuration + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + return &cfg, nil +} + +// Validate checks if the configuration is valid +func (cfg *AutoTradingJSONConfig) Validate() error { + // Validate trading settings + if cfg.Trading.MinTradeSize > cfg.Trading.MaxTradeSize { + return fmt.Errorf("min_trade_size (%d) cannot be greater than max_trade_size (%d)", + cfg.Trading.MinTradeSize, cfg.Trading.MaxTradeSize) + } + if cfg.Trading.MinLeverage > cfg.Trading.MaxLeverage { + return fmt.Errorf("min_leverage (%d) cannot be greater than max_leverage (%d)", + cfg.Trading.MinLeverage, cfg.Trading.MaxLeverage) + } + if cfg.Trading.MinLeverage == 0 { + return fmt.Errorf("min_leverage must be at least 1") + } + if cfg.Trading.MinTradeSize == 0 { + return fmt.Errorf("min_trade_size must be greater than 0") + } + + // Validate bot settings + if cfg.Bot.BlocksBeforeClose == 0 { + return fmt.Errorf("blocks_before_close must be greater than 0") + } + if cfg.Bot.MaxOpenPositions <= 0 { + return fmt.Errorf("max_open_positions must be greater than 0") + } + if cfg.Bot.LoopDelaySeconds <= 0 { + return fmt.Errorf("loop_delay_seconds must be greater than 0") + } + + return nil +} + +// ToAutoTradingConfig converts JSON config to AutoTradingConfig +func (cfg *AutoTradingJSONConfig) ToAutoTradingConfig() AutoTradingConfig { + return AutoTradingConfig{ + MarketIndex: cfg.Trading.MarketIndex, + CollateralIndex: cfg.Trading.CollateralIndex, + MinTradeSize: cfg.Trading.MinTradeSize, + MaxTradeSize: cfg.Trading.MaxTradeSize, + MinLeverage: cfg.Trading.MinLeverage, + MaxLeverage: cfg.Trading.MaxLeverage, + BlocksBeforeClose: cfg.Bot.BlocksBeforeClose, + MaxOpenPositions: cfg.Bot.MaxOpenPositions, + LoopDelaySeconds: cfg.Bot.LoopDelaySeconds, + } +} diff --git a/sai-trading/services/evmtrader/auto_config_test.go b/sai-trading/services/evmtrader/auto_config_test.go new file mode 100644 index 000000000..13992ccae --- /dev/null +++ b/sai-trading/services/evmtrader/auto_config_test.go @@ -0,0 +1,558 @@ +package evmtrader_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/NibiruChain/nibiru/sai-trading/services/evmtrader" + "github.com/stretchr/testify/require" +) + +func TestLoadAutoTradingConfig(t *testing.T) { + tmpDir := t.TempDir() + + tests := []struct { + name string + jsonContent string + expectError bool + errorMsg string + }{ + { + name: "valid configuration", + jsonContent: `{ + "network": { + "mode": "localnet", + "evm_rpc_url": "http://localhost:8545", + "networks_toml": "networks.toml" + }, + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 5, + "loop_delay_seconds": 30 + } + }`, + expectError: false, + }, + { + name: "valid configuration without network settings", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 5, + "loop_delay_seconds": 30 + } + }`, + expectError: false, + }, + { + name: "invalid JSON syntax", + jsonContent: `{ + "trading": { + "market_index": 0 + "collateral_index": 1 + } + }`, + expectError: true, + errorMsg: "parse config JSON", + }, + { + name: "min_trade_size greater than max_trade_size", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 10000000, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 5, + "loop_delay_seconds": 30 + } + }`, + expectError: true, + errorMsg: "min_trade_size", + }, + { + name: "min_leverage greater than max_leverage", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 20, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 5, + "loop_delay_seconds": 30 + } + }`, + expectError: true, + errorMsg: "min_leverage", + }, + { + name: "min_leverage is zero", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 0, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 5, + "loop_delay_seconds": 30 + } + }`, + expectError: true, + errorMsg: "min_leverage must be at least 1", + }, + { + name: "min_trade_size is zero", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 0, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 5, + "loop_delay_seconds": 30 + } + }`, + expectError: true, + errorMsg: "min_trade_size must be greater than 0", + }, + { + name: "blocks_before_close is zero", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 0, + "max_open_positions": 5, + "loop_delay_seconds": 30 + } + }`, + expectError: true, + errorMsg: "blocks_before_close must be greater than 0", + }, + { + name: "max_open_positions is zero", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 0, + "loop_delay_seconds": 30 + } + }`, + expectError: true, + errorMsg: "max_open_positions must be greater than 0", + }, + { + name: "loop_delay_seconds is zero", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": 5, + "loop_delay_seconds": 0 + } + }`, + expectError: true, + errorMsg: "loop_delay_seconds must be greater than 0", + }, + { + name: "max_open_positions is negative", + jsonContent: `{ + "trading": { + "market_index": 0, + "collateral_index": 1, + "min_trade_size": 1000000, + "max_trade_size": 5000000, + "min_leverage": 1, + "max_leverage": 10 + }, + "bot": { + "blocks_before_close": 10, + "max_open_positions": -1, + "loop_delay_seconds": 30 + } + }`, + expectError: true, + errorMsg: "max_open_positions must be greater than 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary config file + configFile := filepath.Join(tmpDir, tt.name+".json") + err := os.WriteFile(configFile, []byte(tt.jsonContent), 0644) + require.NoError(t, err) + + // Load config + cfg, err := evmtrader.LoadAutoTradingConfig(configFile) + + if tt.expectError { + require.Error(t, err) + if tt.errorMsg != "" { + require.Contains(t, err.Error(), tt.errorMsg) + } + require.Nil(t, cfg) + } else { + require.NoError(t, err) + require.NotNil(t, cfg) + } + }) + } +} + +func TestLoadAutoTradingConfig_NonExistentFile(t *testing.T) { + _, err := evmtrader.LoadAutoTradingConfig("/nonexistent/config.json") + require.Error(t, err) + require.Contains(t, err.Error(), "read config file") +} + +func TestValidate(t *testing.T) { + tests := []struct { + name string + config evmtrader.AutoTradingJSONConfig + expectError bool + errorMsg string + }{ + { + name: "valid configuration", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MarketIndex: 0, + CollateralIndex: 1, + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + }, + }, + expectError: false, + }, + { + name: "min equals max is valid", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MarketIndex: 0, + CollateralIndex: 1, + MinTradeSize: 5000000, + MaxTradeSize: 5000000, + MinLeverage: 5, + MaxLeverage: 5, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + }, + }, + expectError: false, + }, + { + name: "min_trade_size > max_trade_size", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 10000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + }, + }, + expectError: true, + errorMsg: "min_trade_size (10000000) cannot be greater than max_trade_size (5000000)", + }, + { + name: "min_leverage > max_leverage", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 20, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + }, + }, + expectError: true, + errorMsg: "min_leverage (20) cannot be greater than max_leverage (10)", + }, + { + name: "min_leverage is zero", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 0, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + }, + }, + expectError: true, + errorMsg: "min_leverage must be at least 1", + }, + { + name: "min_trade_size is zero", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 0, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + }, + }, + expectError: true, + errorMsg: "min_trade_size must be greater than 0", + }, + { + name: "blocks_before_close is zero", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 0, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + }, + }, + expectError: true, + errorMsg: "blocks_before_close must be greater than 0", + }, + { + name: "max_open_positions is zero", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 0, + LoopDelaySeconds: 30, + }, + }, + expectError: true, + errorMsg: "max_open_positions must be greater than 0", + }, + { + name: "loop_delay_seconds is zero", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 0, + }, + }, + expectError: true, + errorMsg: "loop_delay_seconds must be greater than 0", + }, + { + name: "max_open_positions is negative", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: -5, + LoopDelaySeconds: 30, + }, + }, + expectError: true, + errorMsg: "max_open_positions must be greater than 0", + }, + { + name: "loop_delay_seconds is negative", + config: evmtrader.AutoTradingJSONConfig{ + Trading: evmtrader.TradingSettings{ + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: -10, + }, + }, + expectError: true, + errorMsg: "loop_delay_seconds must be greater than 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + + if tt.expectError { + require.Error(t, err) + if tt.errorMsg != "" { + require.Equal(t, tt.errorMsg, err.Error()) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestToAutoTradingConfig(t *testing.T) { + jsonConfig := evmtrader.AutoTradingJSONConfig{ + Network: &evmtrader.NetworkSettings{ + Mode: "testnet", + EVMRPCUrl: "https://evm-rpc.testnet.nibiru.fi", + NetworksToml: "/path/to/networks.toml", + }, + Trading: evmtrader.TradingSettings{ + MarketIndex: 2, + CollateralIndex: 3, + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 2, + MaxLeverage: 15, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 20, + MaxOpenPositions: 10, + LoopDelaySeconds: 60, + }, + } + + result := jsonConfig.ToAutoTradingConfig() + + // Verify all fields are correctly mapped + require.Equal(t, uint64(2), result.MarketIndex) + require.Equal(t, uint64(3), result.CollateralIndex) + require.Equal(t, uint64(1000000), result.MinTradeSize) + require.Equal(t, uint64(5000000), result.MaxTradeSize) + require.Equal(t, uint64(2), result.MinLeverage) + require.Equal(t, uint64(15), result.MaxLeverage) + require.Equal(t, uint64(20), result.BlocksBeforeClose) + require.Equal(t, 10, result.MaxOpenPositions) + require.Equal(t, 60, result.LoopDelaySeconds) +} + +func TestToAutoTradingConfig_WithoutNetworkSettings(t *testing.T) { + jsonConfig := evmtrader.AutoTradingJSONConfig{ + Network: nil, // No network settings + Trading: evmtrader.TradingSettings{ + MarketIndex: 0, + CollateralIndex: 1, + MinTradeSize: 500000, + MaxTradeSize: 2000000, + MinLeverage: 1, + MaxLeverage: 5, + }, + Bot: evmtrader.BotSettings{ + BlocksBeforeClose: 5, + MaxOpenPositions: 3, + LoopDelaySeconds: 15, + }, + } + + result := jsonConfig.ToAutoTradingConfig() + + // Verify conversion works even without network settings + require.Equal(t, uint64(0), result.MarketIndex) + require.Equal(t, uint64(1), result.CollateralIndex) + require.Equal(t, uint64(500000), result.MinTradeSize) + require.Equal(t, uint64(2000000), result.MaxTradeSize) + require.Equal(t, uint64(1), result.MinLeverage) + require.Equal(t, uint64(5), result.MaxLeverage) + require.Equal(t, uint64(5), result.BlocksBeforeClose) + require.Equal(t, 3, result.MaxOpenPositions) + require.Equal(t, 15, result.LoopDelaySeconds) +} diff --git a/sai-trading/services/evmtrader/auto_trader.go b/sai-trading/services/evmtrader/auto_trader.go new file mode 100644 index 000000000..b58406cf8 --- /dev/null +++ b/sai-trading/services/evmtrader/auto_trader.go @@ -0,0 +1,346 @@ +package evmtrader + +import ( + "context" + "crypto/rand" + "math/big" + "strings" + "time" +) + +// AutoTradingConfig holds configuration for automated trading +type AutoTradingConfig struct { + MarketIndex uint64 + CollateralIndex uint64 + MinTradeSize uint64 + MaxTradeSize uint64 + MinLeverage uint64 + MaxLeverage uint64 + BlocksBeforeClose uint64 + MaxOpenPositions int + LoopDelaySeconds int +} + +// PositionTracker tracks an open position and when it was opened +type PositionTracker struct { + TradeIndex uint64 + OpenBlock uint64 + MarketIndex uint64 +} + +// RunAutoTrading runs the automated trading loop +func (t *EVMTrader) RunAutoTrading(ctx context.Context, cfg AutoTradingConfig) error { + t.log("Starting automated trading", + "market_index", cfg.MarketIndex, + "min_trade_size", cfg.MinTradeSize, + "max_trade_size", cfg.MaxTradeSize, + "min_leverage", cfg.MinLeverage, + "max_leverage", cfg.MaxLeverage, + "blocks_before_close", cfg.BlocksBeforeClose, + "max_open_positions", cfg.MaxOpenPositions, + ) + + // Map to track positions we've opened (tradeIndex -> PositionTracker) + trackedPositions := make(map[uint64]*PositionTracker) + + for { + // Get current block number + currentBlock, err := t.client.BlockNumber(ctx) + if err != nil { + t.logError("Failed to get block number", "error", err.Error()) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + + t.log("Auto-trading loop iteration", "current_block", currentBlock, "tracked_positions", len(trackedPositions)) + + // Query current open positions + trades, err := t.QueryTrades(ctx) + if err != nil { + t.logError("Failed to query trades", "error", err.Error()) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + + // Filter for open positions + openPositions := make([]ParsedTrade, 0) + for _, trade := range trades { + if trade.IsOpen { + openPositions = append(openPositions, trade) + } + } + + t.log("Found open positions", "count", len(openPositions)) + + // Check if any tracked positions should be closed + // Only close one position per block iteration to avoid nonce conflicts + closedOne := false + for _, trade := range openPositions { + tracker, isTracked := trackedPositions[trade.UserTradeIndex] + if !isTracked { + // Position not tracked (may have been opened before bot started) + // Add it to tracking with current block as open block + trackedPositions[trade.UserTradeIndex] = &PositionTracker{ + TradeIndex: trade.UserTradeIndex, + OpenBlock: currentBlock, + MarketIndex: trade.MarketIndex, + } + t.log("Added existing position to tracking", "trade_index", trade.UserTradeIndex, "current_block", currentBlock) + continue + } + + // Check if position should be closed + blocksSinceOpen := currentBlock - tracker.OpenBlock + if blocksSinceOpen >= cfg.BlocksBeforeClose { + // Only close one position per block iteration + if closedOne { + continue + } + + t.log("Closing position (reached block threshold)", + "trade_index", trade.UserTradeIndex, + "blocks_since_open", blocksSinceOpen, + "threshold", cfg.BlocksBeforeClose, + ) + + if err := t.CloseTrade(ctx, trade.UserTradeIndex); err != nil { + t.logError("Failed to close trade", "trade_index", trade.UserTradeIndex, "error", err.Error()) + } else { + // Remove from tracking + delete(trackedPositions, trade.UserTradeIndex) + // Wait 2 seconds after closing to ensure nonce is updated before next operation + time.Sleep(2 * time.Second) + } + + // Mark that we've closed one position this iteration + closedOne = true + break + } + } + + // Check if we should open a new position + // Only open if we didn't close a position in this iteration (to avoid nonce conflicts) + if !closedOne && len(openPositions) < cfg.MaxOpenPositions { + t.log("Opening new random position", "current_positions", len(openPositions), "max", cfg.MaxOpenPositions) + + // Generate random trade parameters + tradeSize := randomUint64(cfg.MinTradeSize, cfg.MaxTradeSize) + leverage := randomUint64(cfg.MinLeverage, cfg.MaxLeverage) + isLong := randomBool() + tradeType := randomTradeType() + + // Check balance before opening + erc20ABI := getERC20ABI() + erc20Addr := t.addrs.TokenStNIBIERC20 + if erc20Addr == "" { + t.log("ERC20 address not configured, skipping balance check") + } else { + balance, err := t.queryERC20BalanceFromString(ctx, erc20ABI, erc20Addr, t.accountAddr) + if err != nil { + t.logError("Failed to query balance", "error", err.Error()) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + + requiredBalance := new(big.Int).SetUint64(tradeSize) + if balance.Cmp(requiredBalance) < 0 { + t.logError("Insufficient balance for trade", + "balance", balance.String(), + "required", requiredBalance.String(), + ) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + } + + // Determine collateral index + collateralIndex := cfg.CollateralIndex + if collateralIndex == 0 { + market, err := t.queryMarket(ctx, cfg.MarketIndex) + if err != nil { + t.logError("Failed to query market for collateral index", "error", err.Error()) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + if market.QuoteToken == nil { + t.logError("Market has no quote token", "market_index", cfg.MarketIndex) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + collateralIndex = *market.QuoteToken + } + + // Open the trade + chainID, err := t.client.ChainID(ctx) + if err != nil { + t.logError("Failed to get chain ID", "error", err.Error()) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + + // Fetch current market price from oracle (needed for all trade types) + marketPrice, err := t.fetchMarketPriceForIndex(ctx, cfg.MarketIndex) + if err != nil { + t.logError("Failed to fetch market price from oracle", "error", err.Error()) + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + continue + } + + // Determine open_price based on trade type + var openPrice *float64 + if tradeType == TradeTypeMarket { + // For market orders, use current market price + openPrice = &marketPrice + } else { + // For limit/stop orders, adjust price by ±2-5% to create trigger price + adjustmentPercent := randomFloat64(2.0, 5.0) / 100.0 + if isLong { + // For long limit orders, set trigger price below market (buy cheaper) + // For long stop orders, set trigger price below market (stop loss when price drops) + if tradeType == TradeTypeLimit { + triggerPrice := marketPrice * (1.0 - adjustmentPercent) + openPrice = &triggerPrice + } else { // stop + triggerPrice := marketPrice * (1.0 - adjustmentPercent) + openPrice = &triggerPrice + } + } else { + // For short limit orders, set trigger price above market (sell higher) + // For short stop orders, set trigger price above market (stop loss when price rises) + if tradeType == TradeTypeLimit { + triggerPrice := marketPrice * (1.0 + adjustmentPercent) + openPrice = &triggerPrice + } else { // stop + triggerPrice := marketPrice * (1.0 + adjustmentPercent) + openPrice = &triggerPrice + } + } + } + + params := &OpenTradeParams{ + MarketIndex: cfg.MarketIndex, + Leverage: leverage, + Long: isLong, + CollateralIndex: collateralIndex, + TradeType: tradeType, + OpenPrice: openPrice, + TP: nil, + SL: nil, + SlippageP: "1", + CollateralAmt: new(big.Int).SetUint64(tradeSize), + } + + t.log("Opening random trade", + "trade_size", tradeSize, + "leverage", leverage, + "long", isLong, + "trade_type", tradeType, + "market_index", cfg.MarketIndex, + "collateral_index", collateralIndex, + "open_price", *openPrice, + ) + + if err := t.OpenTrade(ctx, chainID, params); err != nil { + t.logError("Failed to open trade", + "error", err.Error(), + "trade_type", tradeType, + "leverage", leverage, + "long", isLong, + "trade_size", tradeSize, + "market_index", cfg.MarketIndex, + ) + // If it's a nonce error, wait a bit longer before retrying + if strings.Contains(err.Error(), "invalid nonce") { + t.logError("Nonce conflict detected", + "error", err.Error(), + "action", "waiting before next iteration", + ) + time.Sleep(3 * time.Second) + } + } else { + // Query trades again to find the new position and add it to tracking + newTrades, err := t.QueryTrades(ctx) + if err != nil { + t.logError("Failed to query trades after opening", "error", err.Error()) + } else { + // Find the newest open position that we're not tracking yet + for _, trade := range newTrades { + if trade.IsOpen && trackedPositions[trade.UserTradeIndex] == nil { + trackedPositions[trade.UserTradeIndex] = &PositionTracker{ + TradeIndex: trade.UserTradeIndex, + OpenBlock: currentBlock, + MarketIndex: trade.MarketIndex, + } + t.log("Added new position to tracking", + "trade_index", trade.UserTradeIndex, + "open_block", currentBlock, + ) + break + } + } + } + // Wait 2 seconds after successfully opening a trade to ensure nonce is updated + time.Sleep(2 * time.Second) + } + } else { + t.log("Maximum open positions reached, waiting to close positions", "current", len(openPositions), "max", cfg.MaxOpenPositions) + } + + // Sleep before next iteration + time.Sleep(time.Duration(cfg.LoopDelaySeconds) * time.Second) + } +} + +// randomUint64 returns a random uint64 between min and max (inclusive) +func randomUint64(min, max uint64) uint64 { + if min >= max { + return min + } + diff := max - min + 1 + n, err := rand.Int(rand.Reader, big.NewInt(int64(diff))) + if err != nil { + // Fallback to min on error + return min + } + return min + n.Uint64() +} + +// randomBool returns a cryptographically secure random boolean +func randomBool() bool { + var b [1]byte + if _, err := rand.Read(b[:]); err != nil { + return false + } + return b[0]&1 == 1 +} + +// randomTradeType returns a random trade type (market, limit, or stop) +func randomTradeType() string { + // Randomly select between market (50%), limit (25%), and stop (25%) + n := randomUint64(0, 3) + switch n { + case 0, 1: + return TradeTypeMarket + case 2: + return TradeTypeLimit + default: + return TradeTypeStop + } +} + +// randomFloat64 returns a random float64 between min and max +func randomFloat64(min, max float64) float64 { + if min >= max { + return min + } + diff := max - min + // Generate random bytes and convert to float64 + var b [8]byte + if _, err := rand.Read(b[:]); err != nil { + return min + } + // Convert bytes to uint64, then to float64 in range [0, 1) + randUint64 := new(big.Int).SetBytes(b[:]).Uint64() + randFloat := float64(randUint64) / float64(^uint64(0)) + return min + randFloat*diff +} diff --git a/sai-trading/services/evmtrader/auto_trader_test.go b/sai-trading/services/evmtrader/auto_trader_test.go new file mode 100644 index 000000000..cf2e230f7 --- /dev/null +++ b/sai-trading/services/evmtrader/auto_trader_test.go @@ -0,0 +1,45 @@ +package evmtrader + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestPositionTracker tests the PositionTracker struct +func TestPositionTracker(t *testing.T) { + tracker := &PositionTracker{ + TradeIndex: 123, + OpenBlock: 1000, + MarketIndex: 0, + } + + require.Equal(t, uint64(123), tracker.TradeIndex) + require.Equal(t, uint64(1000), tracker.OpenBlock) + require.Equal(t, uint64(0), tracker.MarketIndex) +} + +// TestAutoTradingConfig tests the AutoTradingConfig struct +func TestAutoTradingConfig(t *testing.T) { + cfg := AutoTradingConfig{ + MarketIndex: 0, + CollateralIndex: 1, + MinTradeSize: 1000000, + MaxTradeSize: 5000000, + MinLeverage: 1, + MaxLeverage: 10, + BlocksBeforeClose: 10, + MaxOpenPositions: 5, + LoopDelaySeconds: 30, + } + + require.Equal(t, uint64(0), cfg.MarketIndex) + require.Equal(t, uint64(1), cfg.CollateralIndex) + require.Equal(t, uint64(1000000), cfg.MinTradeSize) + require.Equal(t, uint64(5000000), cfg.MaxTradeSize) + require.Equal(t, uint64(1), cfg.MinLeverage) + require.Equal(t, uint64(10), cfg.MaxLeverage) + require.Equal(t, uint64(10), cfg.BlocksBeforeClose) + require.Equal(t, 5, cfg.MaxOpenPositions) + require.Equal(t, 30, cfg.LoopDelaySeconds) +} diff --git a/sai-trading/services/evmtrader/config.go b/sai-trading/services/evmtrader/config.go index 842845942..2467269bc 100644 --- a/sai-trading/services/evmtrader/config.go +++ b/sai-trading/services/evmtrader/config.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/pelletier/go-toml/v2" ) // Config holds runtime configuration for the EVM trader service. @@ -14,10 +16,16 @@ type Config struct { GrpcUrl string ChainID string ContractsEnvFile string + // ContractAddresses can be set directly (from TOML) or loaded from ContractsEnvFile + ContractAddresses *ContractAddresses // Account PrivateKeyHex string + // Notifications + SlackWebhook string + SlackErrorFilters *ErrorFilters // Error filtering configuration (nil = send all) + // Strategy TradeSize uint64 // Exact trade size (if set, overrides min/max) TradeSizeMin uint64 @@ -45,6 +53,50 @@ type ContractAddresses struct { StNIBIDenom string } +// ErrorFilters defines include/exclude keyword filters for Slack notifications +type ErrorFilters struct { + Include []string `toml:"include"` // Only send errors containing these keywords (empty = send all) + Exclude []string `toml:"exclude"` // Never send errors containing these keywords +} + +// NetworkConfig represents the TOML configuration for all networks +type NetworkConfig struct { + Localnet NetworkInfo `toml:"localnet"` + Testnet NetworkInfo `toml:"testnet"` + Mainnet NetworkInfo `toml:"mainnet"` + Notifications NotificationConfig `toml:"notifications"` +} + +// NotificationConfig contains notification filter settings +type NotificationConfig struct { + Filters ErrorFilters `toml:"filters"` +} + +// NetworkInfo contains configuration for a specific network +type NetworkInfo struct { + Name string `toml:"name"` + EVMRPCUrl string `toml:"evm_rpc_url"` + GrpcUrl string `toml:"grpc_url"` + ChainID string `toml:"chain_id"` + Contracts ContractConfig `toml:"contracts"` + Tokens TokenConfig `toml:"tokens"` +} + +// ContractConfig contains contract addresses +type ContractConfig struct { + OracleAddress string `toml:"oracle_address"` + PerpAddress string `toml:"perp_address"` + VaultAddress string `toml:"vault_address"` + EVMInterface string `toml:"evm_interface"` +} + +// TokenConfig contains token addresses +type TokenConfig struct { + USDCEvm string `toml:"usdc_evm"` + StNIBIEvm string `toml:"stnibi_evm"` + StNIBIDenom string `toml:"stnibi_denom"` +} + // loadContractAddresses reads a simple KEY=VALUE env file. func loadContractAddresses(envFile string) (ContractAddresses, error) { data, err := os.ReadFile(envFile) @@ -80,6 +132,46 @@ func loadContractAddresses(envFile string) (ContractAddresses, error) { return addrs, nil } +// LoadNetworkConfig loads network configuration from TOML file +func LoadNetworkConfig(tomlFile string) (NetworkConfig, error) { + data, err := os.ReadFile(tomlFile) + if err != nil { + return NetworkConfig{}, fmt.Errorf("read TOML file: %w", err) + } + + var config NetworkConfig + if err := toml.Unmarshal(data, &config); err != nil { + return NetworkConfig{}, fmt.Errorf("parse TOML: %w", err) + } + + return config, nil +} + +// GetNetworkInfo returns the NetworkInfo for a given network mode +func GetNetworkInfo(config NetworkConfig, networkMode string) (*NetworkInfo, error) { + switch networkMode { + case "localnet": + return &config.Localnet, nil + case "testnet": + return &config.Testnet, nil + case "mainnet": + return &config.Mainnet, nil + default: + return nil, fmt.Errorf("unknown network mode: %s", networkMode) + } +} + +// ContractAddressesFromNetworkInfo converts NetworkInfo to ContractAddresses +func ContractAddressesFromNetworkInfo(netInfo *NetworkInfo) ContractAddresses { + return ContractAddresses{ + OracleAddress: netInfo.Contracts.OracleAddress, + PerpAddress: netInfo.Contracts.PerpAddress, + VaultAddress: netInfo.Contracts.VaultAddress, + TokenStNIBIERC20: netInfo.Tokens.StNIBIEvm, + StNIBIDenom: netInfo.Tokens.StNIBIDenom, + } +} + // normalizeConfigPaths normalizes file paths in config to be resilient to different CWDs. func normalizeConfigPaths(cfg *Config) { if !filepath.IsAbs(cfg.ContractsEnvFile) && cfg.ContractsEnvFile != "" { diff --git a/sai-trading/services/evmtrader/config_test.go b/sai-trading/services/evmtrader/config_test.go new file mode 100644 index 000000000..fb08812a4 --- /dev/null +++ b/sai-trading/services/evmtrader/config_test.go @@ -0,0 +1,228 @@ +package evmtrader_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/NibiruChain/nibiru/sai-trading/services/evmtrader" + "github.com/stretchr/testify/require" +) + +func TestGetNetworkInfo(t *testing.T) { + // Create a test network config + config := evmtrader.NetworkConfig{ + Localnet: evmtrader.NetworkInfo{ + Name: "localnet", + EVMRPCUrl: "http://localhost:8545", + GrpcUrl: "localhost:9090", + ChainID: "nibiru-localnet-0", + }, + Testnet: evmtrader.NetworkInfo{ + Name: "testnet", + EVMRPCUrl: "https://evm-rpc.testnet-1.nibiru.fi", + GrpcUrl: "grpc.testnet-1.nibiru.fi:443", + ChainID: "nibiru-testnet-1", + }, + Mainnet: evmtrader.NetworkInfo{ + Name: "mainnet", + EVMRPCUrl: "https://evm-rpc.nibiru.fi", + GrpcUrl: "grpc.nibiru.fi:443", + ChainID: "cataclysm-1", + }, + } + + tests := []struct { + name string + networkMode string + expectError bool + expected *evmtrader.NetworkInfo + }{ + { + name: "localnet returns correct config", + networkMode: "localnet", + expectError: false, + expected: &config.Localnet, + }, + { + name: "testnet returns correct config", + networkMode: "testnet", + expectError: false, + expected: &config.Testnet, + }, + { + name: "mainnet returns correct config", + networkMode: "mainnet", + expectError: false, + expected: &config.Mainnet, + }, + { + name: "invalid network mode returns error", + networkMode: "invalid", + expectError: true, + expected: nil, + }, + { + name: "empty network mode returns error", + networkMode: "", + expectError: true, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := evmtrader.GetNetworkInfo(config, tt.networkMode) + if tt.expectError { + require.Error(t, err) + require.Nil(t, result) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +} + +func TestContractAddressesFromNetworkInfo(t *testing.T) { + netInfo := &evmtrader.NetworkInfo{ + Name: "testnet", + EVMRPCUrl: "https://evm-rpc.testnet-1.nibiru.fi", + GrpcUrl: "grpc.testnet-1.nibiru.fi:443", + ChainID: "nibiru-testnet-1", + Contracts: evmtrader.ContractConfig{ + OracleAddress: "0x1111111111111111111111111111111111111111", + PerpAddress: "0x2222222222222222222222222222222222222222", + VaultAddress: "0x3333333333333333333333333333333333333333", + EVMInterface: "0x4444444444444444444444444444444444444444", + }, + Tokens: evmtrader.TokenConfig{ + USDCEvm: "0x5555555555555555555555555555555555555555", + StNIBIEvm: "0x6666666666666666666666666666666666666666", + StNIBIDenom: "factory/nibi1abc/stnibi", + }, + } + + result := evmtrader.ContractAddressesFromNetworkInfo(netInfo) + + require.Equal(t, "0x1111111111111111111111111111111111111111", result.OracleAddress) + require.Equal(t, "0x2222222222222222222222222222222222222222", result.PerpAddress) + require.Equal(t, "0x3333333333333333333333333333333333333333", result.VaultAddress) + require.Equal(t, "0x6666666666666666666666666666666666666666", result.TokenStNIBIERC20) + require.Equal(t, "factory/nibi1abc/stnibi", result.StNIBIDenom) +} + +func TestLoadNetworkConfig(t *testing.T) { + // Create a temporary TOML file for testing + tmpDir := t.TempDir() + tomlFile := filepath.Join(tmpDir, "networks.toml") + + tomlContent := ` +[localnet] +name = "localnet" +evm_rpc_url = "http://localhost:8545" +grpc_url = "localhost:9090" +chain_id = "nibiru-localnet-0" + +[localnet.contracts] +oracle_address = "0xOracle" +perp_address = "0xPerp" +vault_address = "0xVault" +evm_interface = "0xEVM" + +[localnet.tokens] +usdc_evm = "0xUSDC" +stnibi_evm = "0xStNIBI" +stnibi_denom = "factory/nibi1abc/stnibi" + +[testnet] +name = "testnet" +evm_rpc_url = "https://evm-rpc.testnet-1.nibiru.fi" +grpc_url = "grpc.testnet-1.nibiru.fi:443" +chain_id = "nibiru-testnet-1" + +[testnet.contracts] +oracle_address = "0xTestOracle" +perp_address = "0xTestPerp" +vault_address = "0xTestVault" +evm_interface = "0xTestEVM" + +[testnet.tokens] +usdc_evm = "0xTestUSDC" +stnibi_evm = "0xTestStNIBI" +stnibi_denom = "factory/nibi1test/stnibi" + +[mainnet] +name = "mainnet" +evm_rpc_url = "https://evm-rpc.nibiru.fi" +grpc_url = "grpc.nibiru.fi:443" +chain_id = "cataclysm-1" + +[mainnet.contracts] +oracle_address = "0xMainOracle" +perp_address = "0xMainPerp" +vault_address = "0xMainVault" +evm_interface = "0xMainEVM" + +[mainnet.tokens] +usdc_evm = "0xMainUSDC" +stnibi_evm = "0xMainStNIBI" +stnibi_denom = "unibi" + +[notifications.filters] +include = ["critical", "error"] +exclude = ["debug"] +` + + err := os.WriteFile(tomlFile, []byte(tomlContent), 0644) + require.NoError(t, err) + + // Test loading the config + config, err := evmtrader.LoadNetworkConfig(tomlFile) + require.NoError(t, err) + + // Verify localnet + require.Equal(t, "localnet", config.Localnet.Name) + require.Equal(t, "http://localhost:8545", config.Localnet.EVMRPCUrl) + require.Equal(t, "localhost:9090", config.Localnet.GrpcUrl) + require.Equal(t, "nibiru-localnet-0", config.Localnet.ChainID) + require.Equal(t, "0xOracle", config.Localnet.Contracts.OracleAddress) + require.Equal(t, "0xPerp", config.Localnet.Contracts.PerpAddress) + require.Equal(t, "0xVault", config.Localnet.Contracts.VaultAddress) + require.Equal(t, "0xUSDC", config.Localnet.Tokens.USDCEvm) + require.Equal(t, "0xStNIBI", config.Localnet.Tokens.StNIBIEvm) + require.Equal(t, "factory/nibi1abc/stnibi", config.Localnet.Tokens.StNIBIDenom) + + // Verify testnet + require.Equal(t, "testnet", config.Testnet.Name) + require.Equal(t, "https://evm-rpc.testnet-1.nibiru.fi", config.Testnet.EVMRPCUrl) + require.Equal(t, "0xTestOracle", config.Testnet.Contracts.OracleAddress) + + // Verify mainnet + require.Equal(t, "mainnet", config.Mainnet.Name) + require.Equal(t, "https://evm-rpc.nibiru.fi", config.Mainnet.EVMRPCUrl) + require.Equal(t, "0xMainOracle", config.Mainnet.Contracts.OracleAddress) + + // Verify notifications + require.Equal(t, []string{"critical", "error"}, config.Notifications.Filters.Include) + require.Equal(t, []string{"debug"}, config.Notifications.Filters.Exclude) +} + +func TestLoadNetworkConfig_NonExistentFile(t *testing.T) { + _, err := evmtrader.LoadNetworkConfig("/nonexistent/file.toml") + require.Error(t, err) + require.Contains(t, err.Error(), "read TOML file") +} + +func TestLoadNetworkConfig_InvalidTOML(t *testing.T) { + tmpDir := t.TempDir() + tomlFile := filepath.Join(tmpDir, "invalid.toml") + + // Write invalid TOML + err := os.WriteFile(tomlFile, []byte("this is not valid toml {{{"), 0644) + require.NoError(t, err) + + _, err = evmtrader.LoadNetworkConfig(tomlFile) + require.Error(t, err) + require.Contains(t, err.Error(), "parse TOML") +} diff --git a/sai-trading/services/evmtrader/event_parser_test.go b/sai-trading/services/evmtrader/event_parser_test.go new file mode 100644 index 000000000..4dd9337bc --- /dev/null +++ b/sai-trading/services/evmtrader/event_parser_test.go @@ -0,0 +1,323 @@ +package evmtrader + +import ( + "encoding/json" + "testing" + + abcitypes "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +// TestParseWrappedIndex tests the parseWrappedIndex helper function +func TestParseWrappedIndex(t *testing.T) { + // Create a minimal EVMTrader instance for testing + trader := &EVMTrader{} + + tests := []struct { + name string + input string + expected int + expectError bool + }{ + { + name: "parse MarketIndex", + input: "MarketIndex(123)", + expected: 123, + expectError: false, + }, + { + name: "parse TokenIndex", + input: "TokenIndex(456)", + expected: 456, + expectError: false, + }, + { + name: "parse UserTradeIndex", + input: "UserTradeIndex(789)", + expected: 789, + expectError: false, + }, + { + name: "parse zero index", + input: "Index(0)", + expected: 0, + expectError: false, + }, + { + name: "parse large index", + input: "Index(999999)", + expected: 999999, + expectError: false, + }, + { + name: "invalid format - no parentheses", + input: "MarketIndex123", + expected: 0, + expectError: true, + }, + { + name: "invalid format - missing closing paren", + input: "MarketIndex(123", + expected: 0, + expectError: true, + }, + { + name: "invalid format - missing opening paren", + input: "MarketIndex123)", + expected: 0, + expectError: true, + }, + { + name: "invalid format - empty parentheses", + input: "MarketIndex()", + expected: 0, + expectError: true, + }, + { + name: "invalid format - non-numeric content", + input: "MarketIndex(abc)", + expected: 0, + expectError: true, + }, + { + name: "invalid format - empty string", + input: "", + expected: 0, + expectError: true, + }, + { + name: "invalid format - just parentheses", + input: "(123)", + expected: 123, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := trader.parseWrappedIndex(tt.input) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +} + +// TestParseTradeID tests the parseTradeID function with various event formats +func TestParseTradeID(t *testing.T) { + trader := &EVMTrader{} + + tests := []struct { + name string + txResp *sdk.TxResponse + expected int + expectError bool + }{ + { + name: "trade_index attribute in process_opening_fees", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "wasm-process_opening_fees", + Attributes: []abcitypes.EventAttribute{ + {Key: "trade_index", Value: "UserTradeIndex(42)"}, + }, + }, + }, + }, + expected: 42, + expectError: false, + }, + { + name: "trade attribute with JSON", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "wasm-register_trade", + Attributes: []abcitypes.EventAttribute{ + { + Key: "trade", + Value: `{"user":"nibi1abc","user_trade_index":"UserTradeIndex(123)"}`, + }, + }, + }, + }, + }, + expected: 123, + expectError: false, + }, + { + name: "global_trade_index attribute with JSON", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "wasm-store_trade", + Attributes: []abcitypes.EventAttribute{ + { + Key: "global_trade_index", + Value: `{"user":"nibi1xyz","user_trade_index":"UserTradeIndex(789)"}`, + }, + }, + }, + }, + }, + expected: 789, + expectError: false, + }, + { + name: "trigger_trade event", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "wasm-trigger_trade/register_trade", + Attributes: []abcitypes.EventAttribute{ + {Key: "trade_index", Value: "UserTradeIndex(555)"}, + }, + }, + }, + }, + expected: 555, + expectError: false, + }, + { + name: "multiple events - use first match", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "transfer", + Attributes: []abcitypes.EventAttribute{ + {Key: "amount", Value: "1000unibi"}, + }, + }, + { + Type: "wasm-register_trade", + Attributes: []abcitypes.EventAttribute{ + {Key: "trade_index", Value: "UserTradeIndex(111)"}, + }, + }, + { + Type: "wasm-process_opening_fees", + Attributes: []abcitypes.EventAttribute{ + {Key: "trade_index", Value: "UserTradeIndex(222)"}, + }, + }, + }, + }, + expected: 111, + expectError: false, + }, + { + name: "no trade events", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "transfer", + Attributes: []abcitypes.EventAttribute{ + {Key: "amount", Value: "1000unibi"}, + }, + }, + }, + }, + expected: -1, + expectError: true, + }, + { + name: "empty events", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{}, + }, + expected: -1, + expectError: true, + }, + { + name: "invalid trade_index format", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "wasm-register_trade", + Attributes: []abcitypes.EventAttribute{ + {Key: "trade_index", Value: "invalid"}, + }, + }, + }, + }, + expected: -1, + expectError: true, + }, + { + name: "invalid JSON in trade attribute", + txResp: &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{ + { + Type: "wasm-register_trade", + Attributes: []abcitypes.EventAttribute{ + {Key: "trade", Value: `{invalid json}`}, + }, + }, + }, + }, + expected: -1, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := trader.parseTradeID(tt.txResp) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +} + +// TestParseTradeIDFromData tests parsing trade ID from transaction data field +func TestParseTradeIDFromData(t *testing.T) { + trader := &EVMTrader{} + + // Test with data field containing wrapped index + txResp := &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{}, // No events + Data: "UserTradeIndex(999)", + } + + result, err := trader.parseTradeID(txResp) + require.NoError(t, err) + require.Equal(t, 999, result) + + // Test with base64-encoded JSON data + jsonData := map[string]interface{}{ + "user_trade_index": float64(888), + } + jsonBytes, err := json.Marshal(jsonData) + require.NoError(t, err) + base64Data := sdk.MustSortJSON(jsonBytes) // This will be string encoded + + txResp2 := &sdk.TxResponse{ + Height: 100, + Events: []abcitypes.Event{}, + Data: string(base64Data), + } + + // This test checks if the parser can handle JSON in the data field + _, err = trader.parseTradeID(txResp2) + // This may error depending on the exact format, which is ok for this edge case + // The main test is that it doesn't panic +} diff --git a/sai-trading/services/evmtrader/evm_trader.go b/sai-trading/services/evmtrader/evm_trader.go index 87b8e67a7..1241a308a 100644 --- a/sai-trading/services/evmtrader/evm_trader.go +++ b/sai-trading/services/evmtrader/evm_trader.go @@ -1,11 +1,14 @@ package evmtrader import ( + "bytes" "context" "crypto/ecdsa" + "crypto/tls" "encoding/json" "fmt" "math/big" + "net/http" "os" "strings" "time" @@ -18,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" ) @@ -66,8 +70,15 @@ func New(ctx context.Context, cfg Config) (*EVMTrader, error) { } // Connect to gRPC for transaction broadcasting + // Use TLS for remote servers (testnet/mainnet), insecure for localhost + var grpcCreds credentials.TransportCredentials + if strings.Contains(cfg.GrpcUrl, ":443") || (!strings.Contains(cfg.GrpcUrl, "localhost") && !strings.Contains(cfg.GrpcUrl, "127.0.0.1")) { + grpcCreds = credentials.NewTLS(&tls.Config{}) + } else { + grpcCreds = insecure.NewCredentials() + } grpcConn, err := grpc.DialContext(ctx, cfg.GrpcUrl, - grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithTransportCredentials(grpcCreds), ) if err != nil { return nil, fmt.Errorf("dial grpc: %w", err) @@ -77,11 +88,18 @@ func New(ctx context.Context, cfg Config) (*EVMTrader, error) { encCfg := getEncConfig() txClient := txtypes.NewServiceClient(grpcConn) - addrs, err := loadContractAddresses(cfg.ContractsEnvFile) - if err != nil { - return nil, fmt.Errorf("load contracts: %w", err) + // Load contract addresses: use from Config if provided, otherwise load from file + var addrs ContractAddresses + if cfg.ContractAddresses != nil { + addrs = *cfg.ContractAddresses + } else { + var err error + addrs, err = loadContractAddresses(cfg.ContractsEnvFile) + if err != nil { + return nil, fmt.Errorf("load contracts: %w", err) + } } - return &EVMTrader{ + trader := &EVMTrader{ cfg: cfg, client: client, txClient: txClient, @@ -91,7 +109,9 @@ func New(ctx context.Context, cfg Config) (*EVMTrader, error) { ethPrivKey: ethPrivKey, accountAddr: accountAddr, addrs: addrs, - }, nil + } + + return trader, nil } // Close releases underlying resources. @@ -159,7 +179,7 @@ func (t *EVMTrader) OpenTrade(ctx context.Context, chainID *big.Int, params *Ope } // Parse trade ID from response - isLimitOrder := params.TradeType == "limit" || params.TradeType == "stop" + isLimitOrder := isLimitOrStopOrder(params.TradeType) tradeID, err := t.parseTradeID(txResp) if err != nil { t.log("Failed to parse trade ID", "error", err.Error(), "tx_hash", txResp.TxHash) @@ -188,7 +208,7 @@ func (t *EVMTrader) CloseTrade(ctx context.Context, tradeIndex uint64) error { return fmt.Errorf("chain id: %w", err) } - // Build close_trade_market message + // Build close_trade message msgBytes, err := t.buildCloseTradeMessage(tradeIndex) if err != nil { return fmt.Errorf("build message: %w", err) @@ -221,6 +241,135 @@ func (t *EVMTrader) log(msg string, kv ...any) { _ = json.NewEncoder(os.Stdout).Encode(fields) } +// logError logs an error and optionally sends it to Slack webhook +func (t *EVMTrader) logError(msg string, kv ...any) { + t.log(msg, kv...) + + // Check if Slack webhook is configured + if t.cfg.SlackWebhook == "" { + return + } + + // Build error message for Slack + errorFields := map[string]any{} + for i := 0; i+1 < len(kv); i += 2 { + k, _ := kv[i].(string) + errorFields[k] = kv[i+1] + } + + // Apply error filters if configured + if t.cfg.SlackErrorFilters != nil { + // Check exclude list first - if any exclude keyword matches, skip notification + if len(t.cfg.SlackErrorFilters.Exclude) > 0 { + for _, keyword := range t.cfg.SlackErrorFilters.Exclude { + // Check message + if strings.Contains(strings.ToLower(msg), strings.ToLower(keyword)) { + return + } + // Check error fields + for _, v := range errorFields { + vStr := fmt.Sprintf("%v", v) + if strings.Contains(strings.ToLower(vStr), strings.ToLower(keyword)) { + return + } + } + } + } + + // Check include list - if not empty, only send if at least one keyword matches + if len(t.cfg.SlackErrorFilters.Include) > 0 { + matched := false + + // Check if message contains any include keyword + for _, keyword := range t.cfg.SlackErrorFilters.Include { + if strings.Contains(strings.ToLower(msg), strings.ToLower(keyword)) { + matched = true + break + } + } + + // If message didn't match, check error fields + if !matched { + for _, v := range errorFields { + vStr := fmt.Sprintf("%v", v) + for _, keyword := range t.cfg.SlackErrorFilters.Include { + if strings.Contains(strings.ToLower(vStr), strings.ToLower(keyword)) { + matched = true + break + } + } + if matched { + break + } + } + } + + // If no include keywords matched, don't send to Slack + if !matched { + return + } + } + } + + // Format Slack message + slackMsg := map[string]interface{}{ + "text": fmt.Sprintf("🚨 Auto-Trader Error: %s", msg), + "blocks": []map[string]interface{}{ + { + "type": "section", + "text": map[string]interface{}{ + "type": "mrkdwn", + "text": fmt.Sprintf("*%s*\n\n*Details:*", msg), + }, + }, + { + "type": "section", + "fields": buildSlackFields(errorFields), + }, + }, + } + + // Send to Slack (non-blocking) + go sendSlackNotification(t.cfg.SlackWebhook, slackMsg) +} + +// buildSlackFields converts error fields to Slack field format +func buildSlackFields(fields map[string]any) []map[string]interface{} { + slackFields := []map[string]interface{}{} + for k, v := range fields { + slackFields = append(slackFields, map[string]interface{}{ + "type": "mrkdwn", + "text": fmt.Sprintf("*%s:*\n%s", k, fmt.Sprintf("%v", v)), + }) + if len(slackFields) >= 10 { // Slack has a limit on fields + break + } + } + return slackFields +} + +// sendSlackNotification sends a notification to Slack webhook +func sendSlackNotification(webhookURL string, payload map[string]interface{}) { + jsonData, err := json.Marshal(payload) + if err != nil { + return // Silently fail if we can't marshal + } + + req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(jsonData)) + if err != nil { + return // Silently fail if we can't create request + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + return // Silently fail if request fails + } + defer resp.Body.Close() +} + // getEncConfig returns the encoding configuration for the Nibiru chain. func getEncConfig() app.EncodingConfig { return app.MakeEncodingConfig() diff --git a/sai-trading/services/evmtrader/querier.go b/sai-trading/services/evmtrader/querier.go index 0e5b3e53c..acc56ac44 100644 --- a/sai-trading/services/evmtrader/querier.go +++ b/sai-trading/services/evmtrader/querier.go @@ -389,6 +389,82 @@ func (t *EVMTrader) queryERC20Balance(ctx context.Context, erc20ABI abi.ABI, tok return new(big.Int).SetBytes(out), nil } +// queryERC20BalanceFromString queries the ERC20 balance of an account using string addresses +func (t *EVMTrader) queryERC20BalanceFromString(ctx context.Context, erc20ABI abi.ABI, tokenAddr string, account common.Address) (*big.Int, error) { + token := common.HexToAddress(tokenAddr) + return t.queryERC20Balance(ctx, erc20ABI, token, account) +} + +// QueryCollaterals queries the perp contract for all available collaterals +// Tries to list collaterals, and if that's not supported, tries common indices (0-10) +func (t *EVMTrader) QueryCollaterals(ctx context.Context) ([]CollateralInfo, error) { + // Try list_collaterals query (similar to list_markets) + queryMsg := map[string]interface{}{ + "list_collaterals": map[string]interface{}{}, + } + + responseBytes, err := t.queryWasmContract(ctx, t.addrs.PerpAddress, queryMsg) + if err == nil { + // Parse JSON response - list_collaterals might return an array of collateral indices + var collateralIndices []string + if err := json.Unmarshal(responseBytes, &collateralIndices); err != nil { + // Try with data wrapper + var wrapped struct { + Data []string `json:"data"` + } + if err2 := json.Unmarshal(responseBytes, &wrapped); err2 == nil { + collateralIndices = wrapped.Data + } + } + + if len(collateralIndices) > 0 { + // Query each collateral individually to get full details + var collaterals []CollateralInfo + for _, collateralIndexStr := range collateralIndices { + // Extract collateral index from string (e.g., "TokenIndex(0)" or just "0") + var collateralIndex uint64 + if _, err := fmt.Sscanf(collateralIndexStr, "TokenIndex(%d)", &collateralIndex); err != nil { + // Try parsing as just a number + if _, err := fmt.Sscanf(collateralIndexStr, "%d", &collateralIndex); err != nil { + continue // Skip invalid indices + } + } + + // Query individual collateral details + denom, err := t.queryCollateralDenom(ctx, collateralIndex) + if err != nil { + continue + } + collaterals = append(collaterals, CollateralInfo{ + Index: collateralIndex, + Denom: denom, + }) + } + return collaterals, nil + } + } + + // Fallback: try common indices (0-10) to find available collaterals + var collaterals []CollateralInfo + for i := uint64(0); i <= 10; i++ { + denom, err := t.queryCollateralDenom(ctx, i) + if err == nil && denom != "" { + collaterals = append(collaterals, CollateralInfo{ + Index: i, + Denom: denom, + }) + } + } + + return collaterals, nil +} + +// CollateralInfo contains information about a collateral token +type CollateralInfo struct { + Index uint64 + Denom string +} + // queryCollateralDenom queries the perp contract for the denomination of a collateral token by index func (t *EVMTrader) queryCollateralDenom(ctx context.Context, collateralIndex uint64) (string, error) { queryMsg := map[string]interface{}{ diff --git a/sai-trading/services/evmtrader/trade_builder.go b/sai-trading/services/evmtrader/trade_builder.go index 56208aa00..5b902448c 100644 --- a/sai-trading/services/evmtrader/trade_builder.go +++ b/sai-trading/services/evmtrader/trade_builder.go @@ -31,12 +31,13 @@ func (t *EVMTrader) buildOpenTradeMessage(params *OpenTradeParams) ([]byte, erro return nil, fmt.Errorf("open_price is required") } // For limit/stop orders, open_price must be non-zero (trigger price) - if (params.TradeType == "limit" || params.TradeType == "stop") && *params.OpenPrice == 0 { + if isLimitOrStopOrder(params.TradeType) && *params.OpenPrice == 0 { return nil, fmt.Errorf("open_price must be non-zero for %s orders", params.TradeType) } openTradeMsgData["open_price"] = strconv.FormatFloat(*params.OpenPrice, 'f', -1, 64) - // Only set TP/SL if provided + // Only set TP/SL if explicitly provided by user + // Note: The contract may set its own default TP/SL values for limit/stop orders if params.TP != nil { openTradeMsgData["tp"] = strconv.FormatFloat(*params.TP, 'f', -1, 64) } @@ -81,7 +82,7 @@ func (t *EVMTrader) OpenTradeFromJSON(ctx context.Context, jsonPath string) erro } // If open_price not provided for market orders, fetch from oracle - if params.OpenPrice == nil && params.TradeType == "trade" { + if params.OpenPrice == nil && params.TradeType == TradeTypeMarket { price, err := t.fetchMarketPriceForIndex(ctx, params.MarketIndex) if err != nil { return fmt.Errorf("fetch market price for market %d: %w", params.MarketIndex, err) @@ -105,9 +106,9 @@ func (t *EVMTrader) parseTradeParamsFromJSON(data map[string]interface{}) (*Open // Initialize params with defaults params := &OpenTradeParams{ SlippageP: defaultSlippagePercent, - Long: true, // Default to long position - Leverage: 1, // Default leverage is 1x - TradeType: "trade", // Default to market trade + Long: true, // Default to long position + Leverage: 1, // Default leverage is 1x + TradeType: TradeTypeMarket, // Default to market trade } // Parse collateral_amount (required) @@ -125,15 +126,10 @@ func (t *EVMTrader) parseTradeParamsFromJSON(data map[string]interface{}) (*Open params.MarketIndex = idx // Parse leverage (optional, defaults to 1) - if levStr, ok := data["leverage"].(string); ok && levStr != "" { - leverage, err := strconv.ParseUint(levStr, 10, 64) - if err != nil { - return nil, fmt.Errorf("parse leverage: %w", err) - } - if leverage == 0 { - return nil, fmt.Errorf("leverage must be greater than 0, got: %d", leverage) - } - params.Leverage = leverage + if leverage, err := parseOptionalPositiveUint64(data, "leverage"); err != nil { + return nil, err + } else if leverage != nil { + params.Leverage = *leverage } // else: use default leverage of 1 @@ -170,7 +166,7 @@ func (t *EVMTrader) parseTradeParamsFromJSON(data map[string]interface{}) (*Open } // Parse TP/SL (only for limit/stop orders) - if params.TradeType != "trade" { + if params.TradeType != TradeTypeMarket { if err := t.parseTakeProfitStopLoss(data, params); err != nil { return nil, err } @@ -235,7 +231,7 @@ func validateTradeParams(params *OpenTradeParams) error { } // For limit/stop orders, open_price must be non-zero (trigger price) - if (params.TradeType == "limit" || params.TradeType == "stop") && *params.OpenPrice == 0 { + if isLimitOrStopOrder(params.TradeType) && *params.OpenPrice == 0 { return fmt.Errorf("open_price must be non-zero for %s orders (trigger price)", params.TradeType) } @@ -293,7 +289,7 @@ func (t *EVMTrader) parseOpenPrice(data map[string]interface{}, params *OpenTrad priceStr, ok := data["open_price"].(string) if !ok || priceStr == "" { // For limit/stop orders, open_price is REQUIRED (trigger price) - if params.TradeType == "limit" || params.TradeType == "stop" { + if isLimitOrStopOrder(params.TradeType) { return fmt.Errorf("open_price is required for %s orders (trigger price)", params.TradeType) } // For market orders, open_price is OPTIONAL - will be fetched from oracle @@ -307,7 +303,7 @@ func (t *EVMTrader) parseOpenPrice(data map[string]interface{}, params *OpenTrad } // For limit/stop orders, open_price must be non-zero (trigger price) - if (params.TradeType == "limit" || params.TradeType == "stop") && price == 0 { + if isLimitOrStopOrder(params.TradeType) && price == 0 { return fmt.Errorf("open_price must be non-zero for %s orders (trigger price)", params.TradeType) } @@ -357,14 +353,44 @@ func parsePositiveFloat(value, fieldName string) (float64, error) { return parsed, nil } -// buildCloseTradeMessage builds the close_trade_market message from trade index +// parsePositiveUint64 parses a uint64 value from a string and validates it's greater than zero. +func parsePositiveUint64(value, fieldName string) (uint64, error) { + parsed, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return 0, fmt.Errorf("parse %s: %w", fieldName, err) + } + + if parsed == 0 { + return 0, fmt.Errorf("%s must be greater than 0, got: %d", fieldName, parsed) + } + + return parsed, nil +} + +// parseOptionalPositiveUint64 parses an optional uint64 field from JSON data. +// Returns nil if the field doesn't exist or is empty, allowing caller to use default value. +func parseOptionalPositiveUint64(data map[string]interface{}, fieldName string) (*uint64, error) { + valStr, ok := data[fieldName].(string) + if !ok || valStr == "" { + return nil, nil // Field not present or empty, use default + } + + val, err := parsePositiveUint64(valStr, fieldName) + if err != nil { + return nil, err + } + + return &val, nil +} + +// buildCloseTradeMessage builds the close_trade message from trade index func (t *EVMTrader) buildCloseTradeMessage(tradeIndex uint64) ([]byte, error) { closeTradeMsgData := map[string]interface{}{ "trade_index": fmt.Sprintf("UserTradeIndex(%d)", tradeIndex), } closeTradeMsg := map[string]interface{}{ - "close_trade_market": closeTradeMsgData, + "close_trade": closeTradeMsgData, } return json.Marshal(closeTradeMsg) diff --git a/sai-trading/services/evmtrader/trade_params.go b/sai-trading/services/evmtrader/trade_params.go index 189628637..ac7bf0837 100644 --- a/sai-trading/services/evmtrader/trade_params.go +++ b/sai-trading/services/evmtrader/trade_params.go @@ -2,16 +2,11 @@ package evmtrader import ( "context" - "crypto/rand" "fmt" "math/big" ) const ( - // Price adjustment percentages for limit orders - limitOrderPriceAdjustmentUp = 1.1 // 10% above for long positions - limitOrderPriceAdjustmentDown = 0.9 // 10% below for short positions - // Default slippage percentage defaultSlippagePercent = "1" ) @@ -19,27 +14,40 @@ const ( // prepareTradeFromConfig prepares trade parameters from the trader's config. // This is the main orchestration function that coordinates the trade preparation process. func (t *EVMTrader) prepareTradeFromConfig(ctx context.Context, balance *big.Int) (*OpenTradeParams, error) { - // Step 1: Determine trade amount (validates balance internally) - tradeAmt, err := t.determineTradeAmount(balance) - if err != nil { - return nil, err - } - if tradeAmt == nil { - // Insufficient balance - not an error, just skip this trade - return nil, nil + // Step 1: Determine collateral index + // If user provided a collateral index, use it. Otherwise, default to the market's quote token index. + collateralIndex := t.cfg.CollateralIndex + if collateralIndex == 0 { + market, err := t.queryMarket(ctx, t.cfg.MarketIndex) + if err != nil { + return nil, fmt.Errorf("query market for collateral index (market=%d): %w", t.cfg.MarketIndex, err) + } + if market.QuoteToken == nil { + return nil, fmt.Errorf("market %d has no quote token to use as default collateral index", t.cfg.MarketIndex) + } + collateralIndex = *market.QuoteToken + t.log("Using quote token index as default collateral index", "market_index", t.cfg.MarketIndex, "collateral_index", collateralIndex) + } else { + t.log("Using user-provided collateral index", "market_index", t.cfg.MarketIndex, "collateral_index", collateralIndex) } - // Step 2: Determine trade parameters (pure logic, no I/O) + // Step 2: Determine trade amount (validates balance internally) + // TODO: validate balance after + tradeAmt := new(big.Int).SetUint64(t.cfg.TradeSize) + + // Step 3: Determine trade parameters (pure logic, no I/O) leverage := t.determineLeverage() isLong := t.determineDirection() tradeType := t.determineTradeType() - // Step 3: Determine open_price + // Step 4: Determine open_price // - If set in config (from CLI --open-price flag), use that // - Otherwise, fetch from oracle var openPrice float64 + var userProvidedPrice bool if t.cfg.OpenPrice != nil { openPrice = *t.cfg.OpenPrice + userProvidedPrice = true t.log("Using open_price from config", "price", openPrice) } else { // Fetch market price from oracle (I/O operation) @@ -52,45 +60,39 @@ func (t *EVMTrader) prepareTradeFromConfig(ctx context.Context, balance *big.Int return nil, nil } openPrice = price + userProvidedPrice = false t.log("Fetched open_price from oracle", "price", openPrice) } - // Step 4: Adjust price for limit orders - isLimitOrder := (tradeType == "limit" || tradeType == "stop") - adjustedPrice := t.adjustPriceForLimitOrder(openPrice, isLong, isLimitOrder) + // Step 5: Adjust price for limit orders + // Only adjust if price was fetched from oracle, not user-provided + isLimitOrder := isLimitOrStopOrder(tradeType) + adjustedPrice := t.adjustPriceForLimitOrder(openPrice, isLong, isLimitOrder, userProvidedPrice) // Validate adjusted price is non-zero for limit/stop orders (required by specification) if isLimitOrder && adjustedPrice == 0 { return nil, fmt.Errorf("adjusted open_price cannot be zero for %s orders (trigger price required)", tradeType) } - // Step 5: Calculate TP/SL for limit orders - var tp, sl *float64 - if isLimitOrder { - tpVal, slVal := t.calculateTPSL(adjustedPrice, isLong) - tp = &tpVal - sl = &slVal - } - // Step 6: Log and return - t.logTradePreparation(tradeType, isLong, leverage, tradeAmt, adjustedPrice, openPrice, tp, sl) + t.logTradePreparation(tradeType, isLong, leverage, tradeAmt, adjustedPrice, openPrice, nil, nil) return &OpenTradeParams{ MarketIndex: t.cfg.MarketIndex, Leverage: leverage, Long: isLong, - CollateralIndex: t.cfg.CollateralIndex, + CollateralIndex: collateralIndex, TradeType: tradeType, OpenPrice: &adjustedPrice, - TP: tp, - SL: sl, + TP: nil, // Only set if explicitly provided + SL: nil, // Only set if explicitly provided SlippageP: defaultSlippagePercent, CollateralAmt: tradeAmt, }, nil } -// determineTradeAmount calculates the trade amount based on config and available balance. -// Returns nil if balance is insufficient (not an error condition). +// determineTradeAmount calculates the trade amount based on user-provided config only. +// Returns nil if balance is insufficient or no trade size is configured (not an error condition). func (t *EVMTrader) determineTradeAmount(balance *big.Int) (*big.Int, error) { var tradeAmt *big.Int @@ -102,10 +104,10 @@ func (t *EVMTrader) determineTradeAmount(balance *big.Int) (*big.Int, error) { return nil, nil } } else { - // Use random amount within configured range - tradeAmt = t.calculateRandomTradeAmount(balance) + // Use user-provided TradeSizeMin or TradeSizeMax only (no fallback to balance) + tradeAmt = t.calculateDeterministicTradeAmount(balance) if tradeAmt == nil { - t.log("Insufficient ERC20 balance for trade", "balance", balance.String()) + t.log("Insufficient ERC20 balance for trade or no trade size configured", "balance", balance.String()) return nil, nil } } @@ -114,28 +116,23 @@ func (t *EVMTrader) determineTradeAmount(balance *big.Int) (*big.Int, error) { } // determineLeverage returns the leverage to use for the trade. -// Uses config value if set, otherwise random within configured range. -// Ensures leverage > 0 as required by specification. +// Uses config value if set, otherwise defaults to 1. func (t *EVMTrader) determineLeverage() uint64 { if t.cfg.Leverage > 0 { return t.cfg.Leverage } - leverage := t.calculateRandomLeverage() - // Ensure leverage is at least 1 (required by specification) - if leverage == 0 { - return 1 - } - return leverage + // Default to 1x leverage if not specified + return 1 } // determineDirection returns the trade direction (long or short). -// Uses config value if set, otherwise random using cryptographically secure randomness. +// Uses config value if set, otherwise defaults to long (true). func (t *EVMTrader) determineDirection() bool { if t.cfg.Long != nil { return *t.cfg.Long } - // Use crypto/rand for unpredictable trade direction - return secureRandomBool() + // Default to long if not specified + return true } // determineTradeType returns the trade type (trade, limit, or stop). @@ -147,9 +144,9 @@ func (t *EVMTrader) determineTradeType() string { } // Auto-determine based on enableLimitOrder flag - if t.cfg.EnableLimitOrder && secureRandomBool() { + if t.cfg.EnableLimitOrder && randomBool() { // Randomly choose between limit and stop - if secureRandomBool() { + if randomBool() { return "stop" } return "limit" @@ -162,7 +159,7 @@ func (t *EVMTrader) determineTradeType() string { // For market orders, it fetches the exchange rate between base and quote tokens. // For limit/stop orders, it fetches the collateral token price. func (t *EVMTrader) fetchMarketPrice(ctx context.Context, tradeType string) (float64, error) { - if tradeType == "trade" { + if tradeType == TradeTypeMarket { // For market orders, get the exchange rate (base per quote) return t.fetchExchangeRateForMarket(ctx) } @@ -207,19 +204,10 @@ func (t *EVMTrader) fetchMarketPriceForIndex(ctx context.Context, marketIndex ui return rate, nil } -// adjustPriceForLimitOrder adjusts the price for limit orders. -// For long positions, increases price by 10% (buy limit above current price). -// For short positions, decreases price by 10% (sell limit below current price). -// For market orders, returns the price unchanged. -func (t *EVMTrader) adjustPriceForLimitOrder(price float64, isLong, isLimitOrder bool) float64 { - if !isLimitOrder { - return price - } - - if isLong { - return price * limitOrderPriceAdjustmentUp - } - return price * limitOrderPriceAdjustmentDown +// adjustPriceForLimitOrder returns the price unchanged. +// All prices (user-provided and oracle-fetched) are used as-is without any adjustment. +func (t *EVMTrader) adjustPriceForLimitOrder(price float64, isLong, isLimitOrder, userProvided bool) float64 { + return price } // logTradePreparation logs the prepared trade parameters. @@ -227,7 +215,7 @@ func (t *EVMTrader) logTradePreparation(tradeType string, isLong bool, leverage tradeAmt *big.Int, adjustedPrice, oraclePrice float64, tp, sl *float64) { whatTraderOpens := "position" - if tradeType == "limit" || tradeType == "stop" { + if isLimitOrStopOrder(tradeType) { whatTraderOpens = "limit order" } @@ -243,82 +231,39 @@ func (t *EVMTrader) logTradePreparation(tradeType string, isLong bool, leverage ) } -// secureRandomBool returns a cryptographically secure random boolean. -// Uses crypto/rand instead of math/rand for unpredictable randomness. -func secureRandomBool() bool { - var b [1]byte - if _, err := rand.Read(b[:]); err != nil { - // Fallback to false on error (should never happen) - return false - } - return b[0]&1 == 1 -} - -// calculateRandomTradeAmount calculates a random trade amount within configured range. -// Uses cryptographically secure randomness for unpredictable trade amounts. -func (t *EVMTrader) calculateRandomTradeAmount(balance *big.Int) *big.Int { - if t.cfg.TradeSizeMax <= t.cfg.TradeSizeMin { - // Use min if max not set +// calculateDeterministicTradeAmount calculates a deterministic trade amount based on user-provided config only. +// Uses TradeSizeMin if set, otherwise TradeSizeMax. Returns nil if balance is insufficient or no size is configured. +func (t *EVMTrader) calculateDeterministicTradeAmount(balance *big.Int) *big.Int { + // Prefer TradeSizeMin if set + if t.cfg.TradeSizeMin > 0 { amt := new(big.Int).SetUint64(t.cfg.TradeSizeMin) if balance.Cmp(amt) < 0 { + // Insufficient balance - return nil (don't use available balance) return nil } + // If TradeSizeMax is set and larger than min, use TradeSizeMax (if balance allows) + if t.cfg.TradeSizeMax > t.cfg.TradeSizeMin { + maxAmt := new(big.Int).SetUint64(t.cfg.TradeSizeMax) + if balance.Cmp(maxAmt) < 0 { + // Insufficient balance for max - return nil (don't use available balance) + return nil + } + // Use max if balance is sufficient + return maxAmt + } return amt } - // Random amount between min and max using crypto/rand - rangeSize := t.cfg.TradeSizeMax - t.cfg.TradeSizeMin - randomOffset := secureRandomUint64(rangeSize + 1) - tradeAmt := t.cfg.TradeSizeMin + randomOffset - - amt := new(big.Int).SetUint64(tradeAmt) - if balance.Cmp(amt) < 0 { - // If balance is less than min, try to use what we have - if balance.Cmp(big.NewInt(0)) > 0 { - return balance + // If TradeSizeMin not set, try TradeSizeMax + if t.cfg.TradeSizeMax > 0 { + amt := new(big.Int).SetUint64(t.cfg.TradeSizeMax) + if balance.Cmp(amt) < 0 { + // Insufficient balance - return nil (don't use available balance) + return nil } - return nil - } - return amt -} - -// calculateRandomLeverage calculates a random leverage within configured range. -// Uses cryptographically secure randomness for unpredictable leverage selection. -func (t *EVMTrader) calculateRandomLeverage() uint64 { - if t.cfg.LeverageMax <= t.cfg.LeverageMin { - return t.cfg.LeverageMin - } - rangeSize := t.cfg.LeverageMax - t.cfg.LeverageMin - randomOffset := secureRandomUint64(rangeSize + 1) - return t.cfg.LeverageMin + randomOffset -} - -// secureRandomUint64 returns a cryptographically secure random uint64 in the range [0, max). -func secureRandomUint64(max uint64) uint64 { - if max == 0 { - return 0 - } - - // Generate random big.Int - maxBig := new(big.Int).SetUint64(max) - n, err := rand.Int(rand.Reader, maxBig) - if err != nil { - // Fallback to 0 on error (should never happen) - return 0 + return amt } - return n.Uint64() -} -// calculateTPSL calculates take profit and stop loss based on open price and direction -func (t *EVMTrader) calculateTPSL(openPrice float64, isLong bool) (tp, sl float64) { - if isLong { - // Long: TP above, SL below - tp = openPrice * 1.5 - sl = openPrice / 1.5 - } else { - // Short: TP below, SL above - tp = openPrice / 1.5 - sl = openPrice * 1.5 - } - return tp, sl + // If neither min nor max is set, return nil (no user input) + return nil } diff --git a/sai-trading/services/evmtrader/trade_types.go b/sai-trading/services/evmtrader/trade_types.go new file mode 100644 index 000000000..ef3734e72 --- /dev/null +++ b/sai-trading/services/evmtrader/trade_types.go @@ -0,0 +1,26 @@ +package evmtrader + +// Trade type constants +const ( + TradeTypeMarket = "trade" + TradeTypeLimit = "limit" + TradeTypeStop = "stop" +) + +// IsLimitOrStopOrder returns true if the trade type is a limit or stop order. +func IsLimitOrStopOrder(tradeType string) bool { + return tradeType == TradeTypeLimit || tradeType == TradeTypeStop +} + +// IsValidTradeType returns true if the trade type is valid. +func IsValidTradeType(tradeType string) bool { + return tradeType == TradeTypeMarket || + tradeType == TradeTypeLimit || + tradeType == TradeTypeStop +} + +// isLimitOrStopOrder is an internal helper that calls IsLimitOrStopOrder. +// This is provided for backward compatibility with internal code. +func isLimitOrStopOrder(tradeType string) bool { + return IsLimitOrStopOrder(tradeType) +} diff --git a/sai-trading/services/evmtrader/trade_types_test.go b/sai-trading/services/evmtrader/trade_types_test.go new file mode 100644 index 000000000..c8a57ec13 --- /dev/null +++ b/sai-trading/services/evmtrader/trade_types_test.go @@ -0,0 +1,107 @@ +package evmtrader_test + +import ( + "testing" + + "github.com/NibiruChain/nibiru/sai-trading/services/evmtrader" + "github.com/stretchr/testify/require" +) + +func TestIsLimitOrStopOrder(t *testing.T) { + tests := []struct { + name string + tradeType string + expected bool + }{ + { + name: "market order is not limit or stop", + tradeType: evmtrader.TradeTypeMarket, + expected: false, + }, + { + name: "limit order is limit or stop", + tradeType: evmtrader.TradeTypeLimit, + expected: true, + }, + { + name: "stop order is limit or stop", + tradeType: evmtrader.TradeTypeStop, + expected: true, + }, + { + name: "invalid trade type is not limit or stop", + tradeType: "invalid", + expected: false, + }, + { + name: "empty string is not limit or stop", + tradeType: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := evmtrader.IsLimitOrStopOrder(tt.tradeType) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestIsValidTradeType(t *testing.T) { + tests := []struct { + name string + tradeType string + expected bool + }{ + { + name: "market is valid", + tradeType: evmtrader.TradeTypeMarket, + expected: true, + }, + { + name: "limit is valid", + tradeType: evmtrader.TradeTypeLimit, + expected: true, + }, + { + name: "stop is valid", + tradeType: evmtrader.TradeTypeStop, + expected: true, + }, + { + name: "invalid trade type is not valid", + tradeType: "invalid", + expected: false, + }, + { + name: "empty string is not valid", + tradeType: "", + expected: false, + }, + { + name: "uppercase TRADE is not valid", + tradeType: "TRADE", + expected: false, + }, + { + name: "uppercase LIMIT is not valid", + tradeType: "LIMIT", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := evmtrader.IsValidTradeType(tt.tradeType) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestTradeTypeConstants(t *testing.T) { + // Verify the constants have expected values + require.Equal(t, "trade", evmtrader.TradeTypeMarket) + require.Equal(t, "limit", evmtrader.TradeTypeLimit) + require.Equal(t, "stop", evmtrader.TradeTypeStop) +} diff --git a/sai-trading/services/evmtrader/transaction.go b/sai-trading/services/evmtrader/transaction.go index 107172907..79b4686a3 100644 --- a/sai-trading/services/evmtrader/transaction.go +++ b/sai-trading/services/evmtrader/transaction.go @@ -136,6 +136,7 @@ func (t *EVMTrader) sendOpenTradeTransaction(ctx context.Context, chainID *big.I // Query the correct denomination for the collateral index collateralDenom, err := t.queryCollateralDenom(ctx, collateralIndex) if err != nil { + // Provide helpful error message suggesting common alternatives return nil, fmt.Errorf("query collateral denom for index %d: %w", collateralIndex, err) } @@ -155,13 +156,13 @@ func (t *EVMTrader) sendOpenTradeTransaction(ctx context.Context, chainID *big.I return t.sendEVMTransaction(ctx, wasmPrecompileAddr, big.NewInt(0), data, chainID) } -// sendCloseTradeTransaction sends the close_trade_market transaction +// sendCloseTradeTransaction sends the close_trade transaction func (t *EVMTrader) sendCloseTradeTransaction(ctx context.Context, chainID *big.Int, msgBytes []byte) (*sdk.TxResponse, error) { // Build WASM execute call wasmABI := getWasmPrecompileABI() wasmPrecompileAddr := precompile.PrecompileAddr_Wasm - // No funds needed for close_trade_market + // No funds needed for close_trade funds := []struct { Denom string Amount *big.Int diff --git a/sai-trading/trader b/sai-trading/trader new file mode 100755 index 000000000..118fd469b Binary files /dev/null and b/sai-trading/trader differ