package config import ( "errors" "fmt" "log" "net/url" "os" "path/filepath" "strings" "time" "github.com/assimon/luuu/util/http_client" "github.com/spf13/viper" "github.com/tidwall/gjson" ) var ( HTTPAccessLog bool SQLDebug bool LogLevel string MysqlDns string RuntimePath string LogSavePath string StaticPath string StaticFilePath string TgBotToken string TgProxy string TgManage int64 UsdtRate float64 RateApiUrl string TRON_GRID_API_KEY string BuildVersion = "0.0.0-dev" BuildCommit = "none" BuildDate = "unknown" configRootPath string explicitConfigPath string ) func SetConfigPath(path string) { explicitConfigPath = strings.TrimSpace(path) } func Init() { configPath, err := resolveConfigFilePath() if err != nil { panic(err) } configRootPath = filepath.Dir(configPath) viper.SetConfigFile(configPath) err = viper.ReadInConfig() if err != nil { panic(err) } HTTPAccessLog = viper.GetBool("http_access_log") SQLDebug = viper.GetBool("sql_debug") LogLevel = normalizeLogLevel(viper.GetString("log_level")) StaticPath = normalizeStaticURLPath(viper.GetString("static_path")) StaticFilePath = filepath.Join(configRootPath, strings.TrimPrefix(StaticPath, "/")) RuntimePath = resolvePathFromBase(configRootPath, viper.GetString("runtime_root_path"), filepath.Join(configRootPath, "runtime")) LogSavePath = resolvePathFromBase(RuntimePath, viper.GetString("log_save_path"), filepath.Join(RuntimePath, "logs")) mustMkdir(RuntimePath) mustMkdir(LogSavePath) MysqlDns = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", url.QueryEscape(viper.GetString("mysql_user")), url.QueryEscape(viper.GetString("mysql_passwd")), fmt.Sprintf("%s:%s", viper.GetString("mysql_host"), viper.GetString("mysql_port")), viper.GetString("mysql_database")) TgBotToken = viper.GetString("tg_bot_token") TgProxy = viper.GetString("tg_proxy") TgManage = viper.GetInt64("tg_manage") RateApiUrl = GetRateApiUrl() TRON_GRID_API_KEY = viper.GetString("tron_grid_api_key") } func mustMkdir(path string) { if err := os.MkdirAll(path, 0o755); err != nil { panic(err) } } func normalizeLogLevel(level string) string { switch strings.ToLower(strings.TrimSpace(level)) { case "debug", "info", "warn", "error": return strings.ToLower(strings.TrimSpace(level)) default: return "info" } } func normalizeStaticURLPath(path string) string { path = strings.TrimSpace(path) if path == "" || path == "/" { return "/static" } if !strings.HasPrefix(path, "/") { path = "/" + path } return path } func resolvePathFromBase(basePath string, path string, fallback string) string { path = strings.TrimSpace(path) if path == "" { return fallback } if filepath.IsAbs(path) { return path } path = strings.TrimPrefix(path, "/") path = strings.TrimPrefix(path, "\\") return filepath.Join(basePath, filepath.FromSlash(path)) } func resolveConfigFilePath() (string, error) { if explicitConfigPath != "" { return normalizeConfiguredPath(explicitConfigPath) } if envPath := strings.TrimSpace(os.Getenv("EPUSDT_CONFIG")); envPath != "" { return normalizeConfiguredPath(envPath) } return normalizeConfiguredPath(".env") } func normalizeConfiguredPath(input string) (string, error) { path := strings.TrimSpace(input) if path == "" { path = ".env" } if !filepath.IsAbs(path) { cwd, err := os.Getwd() if err != nil { return "", err } path = filepath.Join(cwd, path) } info, err := os.Stat(path) if err == nil && info.IsDir() { path = filepath.Join(path, ".env") info, err = os.Stat(path) } if err != nil { if errors.Is(err, os.ErrNotExist) { return "", fmt.Errorf("config file not found: %s", path) } return "", err } if info.IsDir() { return "", fmt.Errorf("config path must point to a file, got directory: %s", path) } return path, nil } func GetAppVersion() string { return BuildVersion } func GetBuildCommit() string { return BuildCommit } func GetBuildDate() string { return BuildDate } func GetAppName() string { appName := viper.GetString("app_name") if appName == "" { return "epusdt" } return appName } func GetAppUri() string { return viper.GetString("app_uri") } func GetApiAuthToken() string { return viper.GetString("api_auth_token") } func GetRateApiUrl() string { rateURL := viper.GetString("api_rate_url") if rateURL == "" { rateURL = os.Getenv("API_RATE_URL") } if rateURL == "" { log.Println("api_rate_url is empty") } RateApiUrl = rateURL return rateURL } func GetRateForCoin(coin string, base string) float64 { coin = strings.ToLower(strings.TrimSpace(coin)) base = strings.ToLower(strings.TrimSpace(base)) if coin == "" || base == "" { return 0 } if coin == base { return 1 } if coin == "usdt" { switch base { case "usd": return 1 case "cny": usdtRate := GetUsdtRate() if usdtRate > 0 { return 1 / usdtRate } } } baseURL := RateApiUrl if baseURL == "" { baseURL = GetRateApiUrl() } if baseURL == "" { log.Printf("rate api url is empty") return 0.0 } if baseURL[len(baseURL)-1] != '/' { baseURL += "/" } client := http_client.GetHttpClient() resp, err := client.R().Get(baseURL + fmt.Sprintf("%s.json", base)) if err != nil { log.Printf("call rate api error: %s", err.Error()) return 0.0 } if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { log.Printf("call rate api unexpected status: %s", resp.Status()) return 0.0 } targetRate := 0.0 gjson.GetBytes(resp.Body(), base).ForEach(func(key, value gjson.Result) bool { if key.String() == coin { targetRate = value.Float() return false } return true }) return targetRate } func GetUsdtRate() float64 { forcedUsdtRate := viper.GetFloat64("forced_usdt_rate") if forcedUsdtRate > 0 { return forcedUsdtRate } if UsdtRate <= 0 { return 6.4 } return UsdtRate } func GetOrderExpirationTime() int { timer := viper.GetInt("order_expiration_time") if timer <= 0 { return 10 } return timer } func GetOrderExpirationTimeDuration() time.Duration { timer := GetOrderExpirationTime() return time.Minute * time.Duration(timer) } func GetRuntimeSqlitePath() string { filename := viper.GetString("runtime_sqlite_filename") if filename == "" { filename = "epusdt-runtime.db" } if filepath.IsAbs(filename) { return filename } filename = strings.TrimPrefix(strings.TrimPrefix(filename, "/"), "\\") return filepath.Join(RuntimePath, filepath.FromSlash(filename)) } func GetPrimarySqlitePath() string { filename := strings.TrimSpace(viper.GetString("sqlite_database_filename")) if filename == "" { return filepath.Join(configRootPath, "epusdt.db") } if filepath.IsAbs(filename) { return filename } filename = strings.TrimPrefix(strings.TrimPrefix(filename, "/"), "\\") return filepath.Join(configRootPath, filepath.FromSlash(filename)) } func GetQueueConcurrency() int { concurrency := viper.GetInt("queue_concurrency") if concurrency <= 0 { return 10 } return concurrency } func GetQueuePollInterval() time.Duration { interval := viper.GetInt("queue_poll_interval_ms") if interval <= 0 { interval = 1000 } return time.Duration(interval) * time.Millisecond } func GetOrderNoticeMaxRetry() int { retry := viper.GetInt("order_notice_max_retry") if retry < 0 { return 0 } return retry } func GetCallbackRetryBaseDuration() time.Duration { seconds := viper.GetInt("callback_retry_base_seconds") if seconds <= 0 { seconds = 5 } return time.Duration(seconds) * time.Second } func GetSolanaRpcUrl() string { rpcUrl := viper.GetString("solana_rpc_url") if rpcUrl == "" { return "https://api.mainnet-beta.solana.com" } return rpcUrl } func GetEthereumWsUrl() string { return strings.TrimSpace(viper.GetString("ethereum_ws_url")) }