Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions examples/canonical_json_demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand Down
78 changes: 76 additions & 2 deletions internal/cmd/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand All @@ -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)

Expand Down Expand Up @@ -1208,4 +1282,4 @@ func displaySourceLocation(loc *simulator.SourceLocation) {
}
}
fmt.Println()
}
}
2 changes: 1 addition & 1 deletion internal/cmd/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
135 changes: 64 additions & 71 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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",
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The hardcoded path "/etc/erst/config.toml" is Unix-specific and will not work correctly on Windows. On Windows, this path would be invalid and could cause issues. Consider using a platform-appropriate system config path or making this path configurable, or at least document that system-level configuration is only supported on Unix-like systems.

Copilot uses AI. Check for mistakes.
}

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
}
}

Expand All @@ -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))
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The stripInlineComment function is called on every line of the TOML file, but the result is only used after TrimSpace. This means that if stripInlineComment returns a line with trailing spaces before a comment, those spaces will be trimmed anyway. Consider calling stripInlineComment before the initial TrimSpace to avoid unnecessary whitespace processing, or ensuring stripInlineComment returns an already-trimmed result for better performance.

Suggested change
line = strings.TrimSpace(stripInlineComment(line))
line = stripInlineComment(strings.TrimSpace(line))

Copilot uses AI. Check for mistakes.

if line == "" || strings.HasPrefix(line, "#") {
continue
Expand All @@ -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
Expand All @@ -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":
Expand All @@ -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":
Expand Down Expand Up @@ -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
}
Comment on lines +466 to 483
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The stripInlineComment function lacks unit test coverage. This function handles complex quote-aware comment parsing logic, including nested quote handling for single and double quotes. Given its complexity and importance for correct TOML parsing, dedicated unit tests should be added to verify behavior with edge cases like unmatched quotes, escaped quotes, and comments within strings.

Copilot uses AI. Check for mistakes.
Comment on lines +466 to 483
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The stripInlineComment function does not handle escaped quotes. For example, a string like key = "value with \" quote" would incorrectly toggle the inDouble state when encountering the escaped quote, potentially causing comments to be parsed incorrectly. Consider adding escape character handling by checking if the previous character is a backslash before toggling quote states.

Copilot uses AI. Check for mistakes.

func DefaultConfig() *Config {
return &Config{
RpcUrl: defaultConfig.RpcUrl,
Expand Down
Loading
Loading