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
23 changes: 11 additions & 12 deletions internal/cmd/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import (

var abiFormat string

var abiCmd = &cobra.Command{
Use: "abi <wasm-file>",
GroupID: "utility",
Short: "Decompile and display a Soroban contract ABI",
Long: `Parse a compiled Soroban WASM file and pretty-print the contract specification
func NewAbiCmd() *cobra.Command {
abiCmd := &cobra.Command{
Use: "abi <wasm-file>",
GroupID: "utility",
Short: "Decompile and display a Soroban contract ABI",
Long: `Parse a compiled Soroban WASM file and pretty-print the contract specification
(functions, structs, enums, unions, error enums, and events).

The contract spec is read from the "contractspecv0" WASM custom section, which
Expand All @@ -27,8 +28,11 @@ Soroban compilers embed automatically.
Examples:
erst abi ./target/wasm32-unknown-unknown/release/contract.wasm
erst abi --format json ./contract.wasm`,
Args: cobra.ExactArgs(1),
RunE: abiExec,
Args: cobra.ExactArgs(1),
RunE: abiExec,
}
abiCmd.Flags().StringVar(&abiFormat, "format", "text", "Output format: text or json")
return abiCmd
}

func abiExec(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -65,8 +69,3 @@ func abiExec(cmd *cobra.Command, args []string) error {

return nil
}

func init() {
abiCmd.Flags().StringVar(&abiFormat, "format", "text", "Output format: text or json")
rootCmd.AddCommand(abiCmd)
}
127 changes: 62 additions & 65 deletions internal/cmd/auth_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,74 +20,82 @@ var (
authJSONOutputFlag bool
)

var authDebugCmd = &cobra.Command{
Use: "auth-debug <transaction-hash>",
GroupID: "core",
Short: "Debug multi-signature and threshold-based authorization failures",
Long: `Analyze multi-signature authorization flows and identify which signatures or thresholds failed.
func NewAuthDebugCmd() *cobra.Command {
authDebugCmd := &cobra.Command{
Use: "auth-debug <transaction-hash>",
GroupID: "core",
Short: "Debug multi-signature and threshold-based authorization failures",
Long: `Analyze multi-signature authorization flows and identify which signatures or thresholds failed.

Examples:
erst auth-debug <tx-hash>
erst auth-debug --detailed <tx-hash>
erst auth-debug --json <tx-hash>`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
switch rpc.Network(authNetworkFlag) {
case rpc.Testnet, rpc.Mainnet, rpc.Futurenet:
default:
return errors.WrapInvalidNetwork(authNetworkFlag)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
txHash := args[0]

opts := []rpc.ClientOption{
rpc.WithNetwork(rpc.Network(authNetworkFlag)),
}
if authRPCURLFlag != "" {
opts = append(opts, rpc.WithHorizonURL(authRPCURLFlag))
}

client, err := rpc.NewClient(opts...)
if err != nil {
return errors.WrapValidationError(fmt.Sprintf("failed to create client: %v", err))
}
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
switch rpc.Network(authNetworkFlag) {
case rpc.Testnet, rpc.Mainnet, rpc.Futurenet:
default:
return errors.WrapInvalidNetwork(authNetworkFlag)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
txHash := args[0]

logger.Logger.Info("Fetching transaction for auth analysis", "tx_hash", txHash)
opts := []rpc.ClientOption{
rpc.WithNetwork(rpc.Network(authNetworkFlag)),
}
if authRPCURLFlag != "" {
opts = append(opts, rpc.WithHorizonURL(authRPCURLFlag))
}

resp, err := client.GetTransaction(cmd.Context(), txHash)
if err != nil {
return errors.WrapRPCConnectionFailed(err)
}
client, err := rpc.NewClient(opts...)
if err != nil {
return errors.WrapValidationError(fmt.Sprintf("failed to create client: %v", err))
}

fmt.Printf("Transaction Envelope: %d bytes\n", len(resp.EnvelopeXdr))
logger.Logger.Info("Fetching transaction for auth analysis", "tx_hash", txHash)

config := authtrace.AuthTraceConfig{
TraceCustomContracts: true,
CaptureSigDetails: true,
MaxEventDepth: 1000,
}
resp, err := client.GetTransaction(cmd.Context(), txHash)
if err != nil {
return errors.WrapRPCConnectionFailed(err)
}

tracker := authtrace.NewTracker(config)
trace := tracker.GenerateTrace()
reporter := authtrace.NewDetailedReporter(trace)
fmt.Printf("Transaction Envelope: %d bytes\n", len(resp.EnvelopeXdr))

if authJSONOutputFlag {
jsonStr, err := reporter.GenerateJSONString()
if err != nil {
return err
config := authtrace.AuthTraceConfig{
TraceCustomContracts: true,
CaptureSigDetails: true,
MaxEventDepth: 1000,
}
fmt.Println(jsonStr)
} else {
fmt.Println(reporter.GenerateReport())
if authDetailedFlag {
printDetailedAnalysis(reporter)

tracker := authtrace.NewTracker(config)
trace := tracker.GenerateTrace()
reporter := authtrace.NewDetailedReporter(trace)

if authJSONOutputFlag {
jsonStr, err := reporter.GenerateJSONString()
if err != nil {
return err
}
fmt.Println(jsonStr)
} else {
fmt.Println(reporter.GenerateReport())
if authDetailedFlag {
printDetailedAnalysis(reporter)
}
}
}

return nil
},
return nil
},
}
authDebugCmd.Flags().StringVarP(&authNetworkFlag, "network", "n", string(rpc.Mainnet), "Stellar network (testnet, mainnet, futurenet)")
authDebugCmd.Flags().StringVar(&authRPCURLFlag, "rpc-url", "", "Custom Horizon RPC URL")
authDebugCmd.Flags().BoolVar(&authDetailedFlag, "detailed", false, "Show detailed analysis and missing signatures")
authDebugCmd.Flags().BoolVar(&authJSONOutputFlag, "json", false, "Output as JSON")
_ = authDebugCmd.RegisterFlagCompletionFunc("network", completeNetworkFlag)
return authDebugCmd
}

func printDetailedAnalysis(reporter *authtrace.DetailedReporter) {
Expand All @@ -105,14 +113,3 @@ func printDetailedAnalysis(reporter *authtrace.DetailedReporter) {
}
}
}

func init() {
authDebugCmd.Flags().StringVarP(&authNetworkFlag, "network", "n", string(rpc.Mainnet), "Stellar network (testnet, mainnet, futurenet)")
authDebugCmd.Flags().StringVar(&authRPCURLFlag, "rpc-url", "", "Custom Horizon RPC URL")
authDebugCmd.Flags().BoolVar(&authDetailedFlag, "detailed", false, "Show detailed analysis and missing signatures")
authDebugCmd.Flags().BoolVar(&authJSONOutputFlag, "json", false, "Output as JSON")

_ = authDebugCmd.RegisterFlagCompletionFunc("network", completeNetworkFlag)

rootCmd.AddCommand(authDebugCmd)
}
53 changes: 25 additions & 28 deletions internal/cmd/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ func getCacheDir() string {
return filepath.Join(homeDir, ".erst", "cache")
}

var cacheCmd = &cobra.Command{
Use: "cache",
GroupID: "management",
Short: "Manage transaction and simulation cache",
Long: `Manage the local cache that stores transaction data and simulation results.
func NewCacheCmd() *cobra.Command {
cacheCmd := &cobra.Command{
Use: "cache",
GroupID: "management",
Short: "Manage transaction and simulation cache",
Long: `Manage the local cache that stores transaction data and simulation results.
Caching improves performance and enables offline analysis.

Cache location: ~/.erst/cache (configurable via ERST_CACHE_DIR)
Expand All @@ -44,7 +45,7 @@ Available subcommands:
status - View cache size and usage statistics
clean - Remove old files using LRU strategy
clear - Delete all cached data`,
Example: ` # Check cache status
Example: ` # Check cache status
erst cache status

# Clean old cache entries
Expand All @@ -55,10 +56,24 @@ Available subcommands:

# Clear all cache
erst cache clear --force`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
// Add subcommands to cache command
cacheCmd.AddCommand(cacheStatusCmd)
cacheCmd.AddCommand(cacheCleanCmd)
cacheCmd.AddCommand(cacheClearCmd)
cacheCmd.AddCommand(cacheCleanRPCCmd)
// Add flags
cacheCleanCmd.Flags().BoolVarP(&cacheForceFlag, "force", "f", false, "Skip confirmation prompt")
cacheClearCmd.Flags().BoolVarP(&cacheForceFlag, "force", "f", false, "Skip confirmation prompt")
cacheCleanRPCCmd.Flags().IntVar(&cleanOlderThanFlag, "older-than", 0, "Remove entries older than N days")
cacheCleanRPCCmd.Flags().StringVar(&cleanNetworkFlag, "network", "", "Remove entries for a specific network")
cacheCleanRPCCmd.Flags().BoolVar(&cleanAllFlag, "all", false, "Remove all RPC cache entries")
// Add cache command to root
return cacheCmd
}

var cacheStatusCmd = &cobra.Command{
Expand Down Expand Up @@ -232,21 +247,3 @@ At least one filter must be specified. Filters can be combined.`,
return nil
},
}

func init() {
// Add subcommands to cache command
cacheCmd.AddCommand(cacheStatusCmd)
cacheCmd.AddCommand(cacheCleanCmd)
cacheCmd.AddCommand(cacheClearCmd)
cacheCmd.AddCommand(cacheCleanRPCCmd)

// Add flags
cacheCleanCmd.Flags().BoolVarP(&cacheForceFlag, "force", "f", false, "Skip confirmation prompt")
cacheClearCmd.Flags().BoolVarP(&cacheForceFlag, "force", "f", false, "Skip confirmation prompt")
cacheCleanRPCCmd.Flags().IntVar(&cleanOlderThanFlag, "older-than", 0, "Remove entries older than N days")
cacheCleanRPCCmd.Flags().StringVar(&cleanNetworkFlag, "network", "", "Remove entries for a specific network")
cacheCleanRPCCmd.Flags().BoolVar(&cleanAllFlag, "all", false, "Remove all RPC cache entries")

// Add cache command to root
rootCmd.AddCommand(cacheCmd)
}
57 changes: 28 additions & 29 deletions internal/cmd/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ var (
)

// compareCmd implements `erst compare`.
var compareCmd = &cobra.Command{
Use: "compare <transaction-hash>",
GroupID: "testing",
Short: "Compare replay: local WASM vs on-chain WASM side-by-side",
Long: `Simultaneously replay a transaction against a local WASM file and the on-chain
func NewCompareCmd() *cobra.Command {
compareCmd := &cobra.Command{
Use: "compare <transaction-hash>",
GroupID: "testing",
Short: "Compare replay: local WASM vs on-chain WASM side-by-side",
Long: `Simultaneously replay a transaction against a local WASM file and the on-chain
contract, then display a side-by-side diff of events, diagnostic output, budget
usage, and divergent call paths.

Expand All @@ -64,29 +65,27 @@ Examples:

# Override the protocol version used for both passes
erst compare <tx-hash> --wasm ./contract.wasm --protocol-version 22`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if cmpLocalWasmFlag == "" {
return errors.WrapValidationError("--wasm flag is required for compare mode")
}
if _, statErr := os.Stat(cmpLocalWasmFlag); os.IsNotExist(statErr) {
return errors.WrapValidationError(fmt.Sprintf("WASM file not found: %s", cmpLocalWasmFlag))
}
if validateErr := rpc.ValidateTransactionHash(args[0]); validateErr != nil {
return errors.WrapValidationError(fmt.Sprintf("invalid transaction hash: %v", validateErr))
}
switch rpc.Network(cmpNetworkFlag) {
case rpc.Testnet, rpc.Mainnet, rpc.Futurenet:
// valid
default:
return errors.WrapInvalidNetwork(cmpNetworkFlag)
}
return nil
},
RunE: runCompare,
}

func init() {
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if cmpLocalWasmFlag == "" {
return errors.WrapValidationError("--wasm flag is required for compare mode")
}
if _, statErr := os.Stat(cmpLocalWasmFlag); os.IsNotExist(statErr) {
return errors.WrapValidationError(fmt.Sprintf("WASM file not found: %s", cmpLocalWasmFlag))
}
if validateErr := rpc.ValidateTransactionHash(args[0]); validateErr != nil {
return errors.WrapValidationError(fmt.Sprintf("invalid transaction hash: %v", validateErr))
}
switch rpc.Network(cmpNetworkFlag) {
case rpc.Testnet, rpc.Mainnet, rpc.Futurenet:
// valid
default:
return errors.WrapInvalidNetwork(cmpNetworkFlag)
}
return nil
},
RunE: runCompare,
}
compareCmd.Flags().StringVarP(&cmpNetworkFlag, "network", "n", string(rpc.Mainnet),
"Stellar network (testnet, mainnet, futurenet)")
compareCmd.Flags().StringVar(&cmpRPCURLFlag, "rpc-url", "",
Expand All @@ -109,7 +108,7 @@ func init() {
"Override protocol version for both simulation passes (20, 21, 22, …)")
_ = compareCmd.RegisterFlagCompletionFunc("network", completeNetworkFlag)
_ = compareCmd.RegisterFlagCompletionFunc("theme", completeThemeFlag)
rootCmd.AddCommand(compareCmd)
return compareCmd
}

// ─── main handler ─────────────────────────────────────────────────────────────
Expand Down
Loading
Loading