From c33b1433bceb1aef25254dba115097bbff6ac0e3 Mon Sep 17 00:00:00 2001 From: Adesanya David Date: Wed, 25 Feb 2026 18:32:40 +0100 Subject: [PATCH 1/2] feat(cli): Comprehensive erst.toml configuration parsing --- internal/cmd/debug.go | 78 ++++++++++++++++++- internal/cmd/explain.go | 2 +- internal/config/config.go | 135 ++++++++++++++++----------------- internal/config/config_test.go | 47 ++++++++++++ tests/config_priority_test.go | 14 ++-- 5 files changed, 193 insertions(+), 83 deletions(-) diff --git a/internal/cmd/debug.go b/internal/cmd/debug.go index 64522d0c..42221713 100644 --- a/internal/cmd/debug.go +++ b/internal/cmd/debug.go @@ -19,6 +19,7 @@ import ( "github.com/dotandev/hintents/internal/decoder" "github.com/dotandev/hintents/internal/errors" "github.com/dotandev/hintents/internal/logger" + "github.com/dotandev/hintents/internal/lto" "github.com/dotandev/hintents/internal/rpc" "github.com/dotandev/hintents/internal/security" "github.com/dotandev/hintents/internal/session" @@ -114,7 +115,7 @@ func (d *DebugCommand) runDebug(cmd *cobra.Command, args []string) error { token = os.Getenv("ERST_RPC_TOKEN") } if token == "" { - cfg, err := config.LoadConfig() + cfg, err := config.Load() if err == nil && cfg.RPCToken != "" { token = cfg.RPCToken } @@ -777,6 +778,9 @@ func runLocalWasmReplay() error { fmt.Printf("Arguments: %v\n", args) fmt.Println() + // Check for LTO in the project that produced the WASM + checkLTOWarning(wasmPath) + // Create simulator runner runner, err := simulator.NewRunner("", tracingEnabled) if err != nil { @@ -1118,6 +1122,50 @@ func collectVisibleSections(resp *simulator.SimulationResponse, findings []secur return sections } +func applySimulationFeeMocks(req *simulator.SimulationRequest) { + if req == nil { + return + } + + if mockBaseFeeFlag > 0 { + baseFee := mockBaseFeeFlag + req.MockBaseFee = &baseFee + } + if mockGasPriceFlag > 0 { + gasPrice := mockGasPriceFlag + req.MockGasPrice = &gasPrice + } +} + +var deprecatedSorobanHostFunctions = []string{ + "bytes_copy_from_linear_memory", + "bytes_copy_to_linear_memory", + "bytes_new_from_linear_memory", + "map_new_from_linear_memory", + "map_unpack_to_linear_memory", + "symbol_new_from_linear_memory", + "string_new_from_linear_memory", + "vec_new_from_linear_memory", + "vec_unpack_to_linear_memory", +} + +func deprecatedHostFunctionInDiagnosticEvent(event simulator.DiagnosticEvent) (string, bool) { + if name, ok := findDeprecatedHostFunction(strings.Join(event.Topics, " ")); ok { + return name, true + } + return findDeprecatedHostFunction(event.Data) +} + +func findDeprecatedHostFunction(input string) (string, bool) { + lower := strings.ToLower(input) + for _, fn := range deprecatedSorobanHostFunctions { + if strings.Contains(lower, strings.ToLower(fn)) { + return fn, true + } + } + return "", false +} + func init() { debugCmd.Flags().StringVarP(&networkFlag, "network", "n", "mainnet", "Stellar network (auto-detected when omitted; testnet, mainnet, futurenet)") debugCmd.Flags().StringVar(&rpcURLFlag, "rpc-url", "", "Custom RPC URL") @@ -1143,6 +1191,32 @@ func init() { rootCmd.AddCommand(debugCmd) } + +// checkLTOWarning searches the directory tree around a WASM file for +// Cargo.toml files with LTO settings and prints a warning if found. +// It searches the WASM file's parent directory and up to two levels up +// to find the project root. +func checkLTOWarning(wasmFilePath string) { + dir := filepath.Dir(wasmFilePath) + + // Walk up to 3 levels to find Cargo.toml files + for i := 0; i < 3; i++ { + results, err := lto.CheckProjectDir(dir) + if err != nil { + logger.Logger.Debug("LTO check failed", "dir", dir, "error", err) + break + } + if lto.HasLTO(results) { + fmt.Fprintf(os.Stderr, "\n%s\n", lto.FormatWarnings(results)) + return + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } +} func displaySourceLocation(loc *simulator.SourceLocation) { fmt.Printf("%s Location: %s:%d:%d\n", visualizer.Symbol("location"), loc.File, loc.Line, loc.Column) @@ -1208,4 +1282,4 @@ func displaySourceLocation(loc *simulator.SourceLocation) { } } fmt.Println() -} \ No newline at end of file +} diff --git a/internal/cmd/explain.go b/internal/cmd/explain.go index ebf52c3d..06920471 100644 --- a/internal/cmd/explain.go +++ b/internal/cmd/explain.go @@ -91,7 +91,7 @@ func explainFromNetwork(cmd *cobra.Command, txHash string) error { token = os.Getenv("ERST_RPC_TOKEN") } if token == "" { - if cfg, err := config.LoadConfig(); err == nil && cfg.RPCToken != "" { + if cfg, err := config.Load(); err == nil && cfg.RPCToken != "" { token = cfg.RPCToken } } diff --git a/internal/config/config.go b/internal/config/config.go index cff92f17..ef407577 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -109,12 +109,16 @@ func (EnvParser) Parse(cfg *Config) error { } if v := os.Getenv("ERST_SIMULATOR_PATH"); v != "" { cfg.SimulatorPath = v + } else if v := os.Getenv("ERST_SIM_PATH"); v != "" { + cfg.SimulatorPath = v } if v := os.Getenv("ERST_LOG_LEVEL"); v != "" { cfg.LogLevel = v } if v := os.Getenv("ERST_CACHE_PATH"); v != "" { cfg.CachePath = v + } else if v := os.Getenv("ERST_CACHE_DIR"); v != "" { + cfg.CachePath = v } if v := os.Getenv("ERST_RPC_TOKEN"); v != "" { cfg.RPCToken = v @@ -136,24 +140,18 @@ func (EnvParser) Parse(cfg *Config) error { switch strings.ToLower(os.Getenv("ERST_CRASH_REPORTING")) { case "": - case "1", "true", "yes": + case "1", "true", "yes", "on": cfg.CrashReporting = true - case "0", "false", "no": + case "0", "false", "no", "off": cfg.CrashReporting = false default: return errors.WrapValidationError("ERST_CRASH_REPORTING must be a boolean") } if urlsEnv := os.Getenv("ERST_RPC_URLS"); urlsEnv != "" { - cfg.RpcUrls = strings.Split(urlsEnv, ",") - for i := range cfg.RpcUrls { - cfg.RpcUrls[i] = strings.TrimSpace(cfg.RpcUrls[i]) - } + cfg.RpcUrls = splitList(urlsEnv) } else if urlsEnv := os.Getenv("STELLAR_RPC_URLS"); urlsEnv != "" { - cfg.RpcUrls = strings.Split(urlsEnv, ",") - for i := range cfg.RpcUrls { - cfg.RpcUrls[i] = strings.TrimSpace(cfg.RpcUrls[i]) - } + cfg.RpcUrls = splitList(urlsEnv) } return nil @@ -248,42 +246,14 @@ func LoadConfig() (*Config, error) { // Load loads the configuration from environment variables and TOML files. // The lifecycle follows three distinct phases: load, merge defaults, validate. func Load() (*Config, error) { - // Phase 1: Load from sources (env vars, then TOML file). - cfg := &Config{ - RpcUrl: getEnv("ERST_RPC_URL", ""), - Network: Network(getEnv("ERST_NETWORK", "")), - SimulatorPath: getEnv("ERST_SIMULATOR_PATH", ""), - LogLevel: getEnv("ERST_LOG_LEVEL", ""), - CachePath: getEnv("ERST_CACHE_PATH", ""), - RPCToken: getEnv("ERST_RPC_TOKEN", ""), - CrashEndpoint: getEnv("ERST_CRASH_ENDPOINT", ""), - CrashSentryDSN: getEnv("ERST_SENTRY_DSN", ""), - RequestTimeout: defaultRequestTimeout, - } - - // ERST_REQUEST_TIMEOUT is an integer env var; parse it explicitly. - if v := os.Getenv("ERST_REQUEST_TIMEOUT"); v != "" { - if n, err := strconv.Atoi(v); err == nil && n > 0 { - cfg.RequestTimeout = n - } - } - - switch strings.ToLower(os.Getenv("ERST_CRASH_REPORTING")) { - case "1", "true", "yes": - cfg.CrashReporting = true - } - - if urlsEnv := os.Getenv("ERST_RPC_URLS"); urlsEnv != "" { - cfg.RpcUrls = strings.Split(urlsEnv, ",") - for i := range cfg.RpcUrls { - cfg.RpcUrls[i] = strings.TrimSpace(cfg.RpcUrls[i]) + cfg := &Config{} + parsers := []Parser{fileParser{}, EnvParser{}} + for _, parser := range parsers { + if err := parser.Parse(cfg); err != nil { + return nil, err } } - if err := cfg.loadFromFile(); err != nil { - return nil, err - } - // Phase 2: Merge defaults for any fields still unset. cfg.MergeDefaults() @@ -303,14 +273,20 @@ func (fileParser) Parse(cfg *Config) error { func (c *Config) loadFromFile() error { paths := []string{ - ".erst.toml", - filepath.Join(os.ExpandEnv("$HOME"), ".erst.toml"), "/etc/erst/config.toml", } + if homeDir, err := os.UserHomeDir(); err == nil && homeDir != "" { + paths = append(paths, filepath.Join(homeDir, ".erst.toml")) + } + paths = append(paths, ".erst.toml") + for _, path := range paths { - if err := c.loadTOML(path); err == nil { - return nil + if err := c.loadTOML(path); err != nil { + if os.IsNotExist(err) { + continue + } + return err } } @@ -333,7 +309,7 @@ func (c *Config) loadTOML(path string) error { func (c *Config) parseTOML(content string) error { lines := strings.Split(content, "\n") for _, line := range lines { - line = strings.TrimSpace(line) + line = strings.TrimSpace(stripInlineComment(line)) if line == "" || strings.HasPrefix(line, "#") { continue @@ -353,7 +329,10 @@ func (c *Config) parseTOML(content string) error { parts := strings.Split(rawVal, ",") var urls []string for _, p := range parts { - urls = append(urls, strings.Trim(strings.TrimSpace(p), "\"'")) + item := strings.TrimSpace(strings.Trim(p, "\"'")) + if item != "" { + urls = append(urls, item) + } } c.RpcUrls = urls continue @@ -366,10 +345,7 @@ func (c *Config) parseTOML(content string) error { c.RpcUrl = value case "rpc_urls": // Fallback if not an array but comma-separated string - c.RpcUrls = strings.Split(value, ",") - for i := range c.RpcUrls { - c.RpcUrls[i] = strings.TrimSpace(c.RpcUrls[i]) - } + c.RpcUrls = splitList(value) case "network": c.Network = Network(value) case "network_passphrase": @@ -382,15 +358,15 @@ func (c *Config) parseTOML(content string) error { c.CachePath = value case "rpc_token": c.RPCToken = value - case "crash_reporting": - switch strings.ToLower(value) { - case "true", "1", "yes": - c.CrashReporting = true - case "false", "0", "no": - c.CrashReporting = false - default: - return errors.WrapValidationError("crash_reporting must be a boolean") - } + case "crash_reporting": + switch strings.ToLower(value) { + case "true", "1", "yes", "on": + c.CrashReporting = true + case "false", "0", "no", "off": + c.CrashReporting = false + default: + return errors.WrapValidationError("crash_reporting must be a boolean") + } case "crash_endpoint": c.CrashEndpoint = value case "crash_sentry_dsn": @@ -475,19 +451,36 @@ func (c *Config) String() string { ) } -func getEnv(key, defaultValue string) string { - // Only allow environment variables that are explicitly namespaced with ERST_ - if !strings.HasPrefix(key, "ERST_") { - return defaultValue +func splitList(value string) []string { + parts := strings.Split(value, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { + item := strings.TrimSpace(part) + if item != "" { + out = append(out, item) + } } + return out +} - if value := os.Getenv(key); value != "" { - return value +func stripInlineComment(line string) string { + inSingle := false + inDouble := false + for i, ch := range line { + if ch == '"' && !inSingle { + inDouble = !inDouble + continue + } + if ch == '\'' && !inDouble { + inSingle = !inSingle + continue + } + if ch == '#' && !inSingle && !inDouble { + return strings.TrimSpace(line[:i]) + } } - - return defaultValue + return line } - func DefaultConfig() *Config { return &Config{ RpcUrl: defaultConfig.RpcUrl, diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 10a14ac1..d950ba80 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -326,6 +326,53 @@ log_level = "trace"` } } +func TestLoad_ConfigPrecedence_LocalOverridesHome(t *testing.T) { + tmpDir := t.TempDir() + homeDir := filepath.Join(tmpDir, "home") + projectDir := filepath.Join(tmpDir, "project") + + if err := os.MkdirAll(homeDir, 0700); err != nil { + t.Fatalf("failed to create home dir: %v", err) + } + if err := os.MkdirAll(projectDir, 0700); err != nil { + t.Fatalf("failed to create project dir: %v", err) + } + + homeConfig := filepath.Join(homeDir, ".erst.toml") + if err := os.WriteFile(homeConfig, []byte(`rpc_url = "https://home.example.com"`), 0644); err != nil { + t.Fatalf("failed to write home config: %v", err) + } + + localConfig := filepath.Join(projectDir, ".erst.toml") + if err := os.WriteFile(localConfig, []byte(`rpc_url = "https://local.example.com"`), 0644); err != nil { + t.Fatalf("failed to write local config: %v", err) + } + + origHome := os.Getenv("HOME") + defer os.Setenv("HOME", origHome) + os.Setenv("HOME", homeDir) + + origPwd, _ := os.Getwd() + defer func() { + _ = os.Chdir(origPwd) + }() + if err := os.Chdir(projectDir); err != nil { + t.Fatalf("failed to chdir: %v", err) + } + + for _, key := range []string{"ERST_RPC_URL", "ERST_RPC_URLS", "STELLAR_RPC_URLS"} { + os.Unsetenv(key) + } + + cfg, err := Load() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.RpcUrl != "https://local.example.com" { + t.Errorf("expected local config to override home, got %s", cfg.RpcUrl) + } +} + func TestValidNetworks(t *testing.T) { networks := []Network{NetworkPublic, NetworkTestnet, NetworkFuturenet, NetworkStandalone} diff --git a/tests/config_priority_test.go b/tests/config_priority_test.go index bc0a50f2..25ebb207 100644 --- a/tests/config_priority_test.go +++ b/tests/config_priority_test.go @@ -33,14 +33,10 @@ func TestConfigPriority(t *testing.T) { os.Setenv("HOME", tmpDir) os.Setenv("USERPROFILE", tmpDir) - // Create a dummy config file - configDir := filepath.Join(tmpDir, ".erst") - err := os.MkdirAll(configDir, 0700) - require.NoError(t, err) - - configFile := filepath.Join(configDir, "config.json") - configData := []byte(`{"rpc_token": "CONFIG_TOKEN"}`) - err = os.WriteFile(configFile, configData, 0600) + // Create a dummy TOML config file + configFile := filepath.Join(tmpDir, ".erst.toml") + configData := []byte(`rpc_token = "CONFIG_TOKEN"`) + err := os.WriteFile(configFile, configData, 0600) require.NoError(t, err) // Helper to resolve token with same logic as debug.go @@ -50,7 +46,7 @@ func TestConfigPriority(t *testing.T) { token = os.Getenv("ERST_RPC_TOKEN") } if token == "" { - cfg, err := config.LoadConfig() + cfg, err := config.Load() if err == nil && cfg.RPCToken != "" { token = cfg.RPCToken } From d04ab73eac5ed5b64af95d2ae418f0f8a28060a4 Mon Sep 17 00:00:00 2001 From: Adesanya David Date: Wed, 4 Mar 2026 16:15:11 +0100 Subject: [PATCH 2/2] chore: remove redundant emojis --- examples/canonical_json_demo.go | 12 ++++++------ internal/trace/printer.go | 8 ++++---- internal/trace/printer_test.go | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/canonical_json_demo.go b/examples/canonical_json_demo.go index 2d949a12..0e43e9e6 100644 --- a/examples/canonical_json_demo.go +++ b/examples/canonical_json_demo.go @@ -53,7 +53,7 @@ func main() { if err != nil { log.Fatal(err) } - fmt.Println(" ✓ Signature verified successfully") + fmt.Println(" [OK] Signature verified successfully") fmt.Println() // Example 3: Demonstrate deterministic hashing @@ -85,10 +85,10 @@ func main() { } if allSame { - fmt.Println(" ✓ All 5 hashes are identical (deterministic)") + fmt.Println(" [OK] All 5 hashes are identical (deterministic)") fmt.Printf(" Hash: %s\n", hashes[0]) } else { - fmt.Println(" ✗ Hashes differ (non-deterministic)") + fmt.Println(" [FAIL] Hashes differ (non-deterministic)") } fmt.Println() @@ -123,9 +123,9 @@ func main() { err = cmd.Verify(&tamperedLog) if err != nil { - fmt.Printf(" ✓ Tampering detected: %v\n", err) + fmt.Printf(" [OK] Tampering detected: %v\n", err) } else { - fmt.Println(" ✗ Tampering not detected (unexpected)") + fmt.Println(" [FAIL] Tampering not detected (unexpected)") } fmt.Println() @@ -163,7 +163,7 @@ func main() { log2, _ := cmd.Generate("tx1", "envelope", "result", []string{"e1"}, []string{"l1"}, privateKeyHex) if log1.TraceHash == log2.TraceHash { - fmt.Println(" ✓ Same data produces same hash regardless of field order") + fmt.Println(" [OK] Same data produces same hash regardless of field order") fmt.Printf(" Hash: %s\n", log1.TraceHash) } fmt.Println() diff --git a/internal/trace/printer.go b/internal/trace/printer.go index bcfbaf1e..9bd4f50f 100644 --- a/internal/trace/printer.go +++ b/internal/trace/printer.go @@ -207,7 +207,7 @@ func PrintExecutionTrace(t *ExecutionTrace, opts PrintOptions) { if j == 0 { fmt.Fprintf(out, "%s %s %s\n", continuation, - p.errorFn.Sprint("✗"), + p.errorFn.Sprint("[FAIL]"), p.errorMsg.Sprint(line), ) } else { @@ -334,7 +334,7 @@ func printTreeNode(out io.Writer, p palette, node *TraceNode, prefix string, isL if j == 0 { fmt.Fprintf(out, "%s %s %s\n", childPrefix, - p.errorFn.Sprint("✗"), + p.errorFn.Sprint("[FAIL]"), p.errorMsg.Sprint(line), ) } else { @@ -444,11 +444,11 @@ func iconForType(t string) string { case "event": return "◉" case "error": - return "✗" + return "[FAIL]" case "log": return "▪" case EventTypeTrap: - return "⚡" + return "[READY]" case "transaction", "simulation": return "▸" case "transaction_complete": diff --git a/internal/trace/printer_test.go b/internal/trace/printer_test.go index 67f91ef8..6320571d 100644 --- a/internal/trace/printer_test.go +++ b/internal/trace/printer_test.go @@ -36,7 +36,7 @@ func TestPrintTraceTree_NoColor(t *testing.T) { {"tree connector", "├─"}, {"leaf connector", "└─"}, {"contract func transfer", "transfer"}, - {"error icon", "✗"}, + {"error icon", "[FAIL]"}, {"error text", "Insufficient balance"}, {"cpu budget", "CPU:"}, {"mem budget", "MEM:"}, @@ -93,7 +93,7 @@ func TestPrintExecutionTrace_NoColor(t *testing.T) { "CONTRACT_CALL", "transfer", "fail_fn", - "✗", + "[FAIL]", "out of gas", "Steps: 3", "Errors: 1",