From e1f0879876e7d9b1f70c53dc27a665e46b30f57f Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Wed, 22 Oct 2025 19:08:59 +0200 Subject: [PATCH 1/9] improve error messages --- seth/abi_finder.go | 33 +++++- seth/block_stats.go | 6 +- seth/client.go | 252 +++++++++++++++++++++++++++++++++++------ seth/config.go | 84 +++++++++++--- seth/contract_store.go | 48 ++++++-- seth/decode.go | 29 +++-- seth/gas.go | 53 +++++++-- seth/gas_adjuster.go | 40 ++++++- seth/header_cache.go | 4 +- seth/nonce.go | 65 ++++++++--- seth/retry.go | 4 +- seth/tracing.go | 7 +- 12 files changed, 527 insertions(+), 98 deletions(-) diff --git a/seth/abi_finder.go b/seth/abi_finder.go index b7316c442..cb0067ef7 100644 --- a/seth/abi_finder.go +++ b/seth/abi_finder.go @@ -1,6 +1,7 @@ package seth import ( + "fmt" "strings" "github.com/ethereum/go-ethereum/accounts/abi" @@ -127,7 +128,37 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder } if result.Method == nil { - return ABIFinderResult{}, errors.New(ErrNoABIMethod) + abiCount := len(a.ContractStore.ABIs) + abiSample := "" + if abiCount > 0 { + // Show first few ABIs as examples (max 5) + sampleSize := min(abiCount, 5) + samples := make([]string, 0, sampleSize) + count := 0 + for abiName := range a.ContractStore.ABIs { + if count >= sampleSize { + break + } + samples = append(samples, strings.TrimSuffix(abiName, ".abi")) + count++ + } + abiSample = fmt.Sprintf("\nExample ABIs loaded: %s", strings.Join(samples, ", ")) + if abiCount > sampleSize { + abiSample += fmt.Sprintf(" (and %d more)", abiCount-sampleSize) + } + } + + return ABIFinderResult{}, fmt.Errorf("no ABI found with method signature %s for contract at address %s.\n"+ + "Checked %d ABIs but none matched.%s\n"+ + "This usually means:\n"+ + " 1. The contract ABI wasn't loaded into Seth's contract store\n"+ + " 2. The method signature doesn't match any known ABI\n"+ + " 3. You're calling a non-existent contract address\n"+ + "Solutions:\n"+ + " 1. Add the contract's ABI to the directory specified by 'abi_dir'\n"+ + " 2. Use ContractStore.AddABI() to add it programmatically\n"+ + " 3. Deploy the contract via Seth so it's automatically registered", + stringSignature, address, abiCount, abiSample) } return result, nil diff --git a/seth/block_stats.go b/seth/block_stats.go index db883331c..330d1486a 100644 --- a/seth/block_stats.go +++ b/seth/block_stats.go @@ -62,7 +62,9 @@ func (cs *BlockStats) Stats(startBlock *big.Int, endBlock *big.Int) error { endBlock = latestBlockNumber } if endBlock != nil && startBlock.Int64() > endBlock.Int64() { - return fmt.Errorf("start block is less than the end block") + return fmt.Errorf("start block (%d) is greater than end block (%d). "+ + "Ensure start block comes before end block in the range", + startBlock.Int64(), endBlock.Int64()) } L.Info(). Int64("EndBlock", endBlock.Int64()). @@ -107,7 +109,7 @@ func (cs *BlockStats) Stats(startBlock *big.Int, endBlock *big.Int) error { // CalculateBlockDurations calculates and logs the duration, TPS, gas used, and gas limit between each consecutive block func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error { if len(blocks) == 0 { - return fmt.Errorf("no blocks no analyze") + return fmt.Errorf("no blocks to analyze. Cannot calculate block durations without block data") } var ( durations []time.Duration diff --git a/seth/client.go b/seth/client.go index 35dcb590c..225c702c5 100644 --- a/seth/client.go +++ b/seth/client.go @@ -89,7 +89,9 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { initDefaultLogging() if cfg == nil { - return nil, errors.New(ErrSethConfigIsNil) + return nil, fmt.Errorf("Seth configuration is nil. "+ + "Ensure you're calling NewClientWithConfig() with a valid config, or use NewClient() to load from SETH_CONFIG_PATH environment variable. "+ + "See documentation for configuration examples") } if cfgErr := cfg.Validate(); cfgErr != nil { return nil, cfgErr @@ -100,7 +102,13 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { cfg.setEphemeralAddrs() cs, err := NewContractStore(filepath.Join(cfg.ConfigDir, cfg.ABIDir), filepath.Join(cfg.ConfigDir, cfg.BINDir), cfg.GethWrappersDirs) if err != nil { - return nil, errors.Wrap(err, ErrCreateABIStore) + return nil, fmt.Errorf("failed to create ABI/contract store: %w\n"+ + "Check that:\n"+ + " 1. 'abi_dir' path is correct (current: %s)\n"+ + " 2. 'bin_dir' path is correct (current: %s)\n"+ + " 3. These directories exist and are readable\n"+ + " 4. Or comment out these settings if not using ABI/BIN files", + err, cfg.ABIDir, cfg.BINDir) } if cfg.ephemeral { // we don't care about any other keys, only the root key @@ -117,11 +125,16 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { } addrs, pkeys, err := cfg.ParseKeys() if err != nil { - return nil, errors.Wrap(err, ErrReadingKeys) + return nil, fmt.Errorf("failed to parse private keys: %w\n"+ + "Ensure private keys are valid hex strings (64 characters, without 0x prefix). "+ + "Keys should be set in SETH_ROOT_PRIVATE_KEY env var or 'private_keys_secret' in seth.toml", + err) } nm, err := NewNonceManager(cfg, addrs, pkeys) if err != nil { - return nil, errors.Wrap(err, ErrCreateNonceManager) + return nil, fmt.Errorf("failed to create nonce manager: %w\n"+ + "This is usually a configuration issue. Check 'nonce_manager' settings in your config (seth.toml or ClientBuilder)", + err) } if !cfg.IsSimulatedNetwork() && cfg.SaveDeployedContractsMap && cfg.ContractMapFile == "" { @@ -135,7 +148,9 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { if !cfg.IsSimulatedNetwork() { contractAddressToNameMap.addressMap, err = LoadDeployedContracts(cfg.ContractMapFile) if err != nil { - return nil, errors.Wrap(err, ErrReadContractMap) + return nil, fmt.Errorf("failed to load deployed contracts map from '%s': %w\n"+ + "If this is a fresh deployment or you don't need contract mapping, ignore this error by setting save_deployed_contracts_map = false", + cfg.ContractMapFile, err) } } else { L.Debug().Msg("Simulated network, contract map won't be read from file") @@ -150,7 +165,9 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { if (cfg.ethclient != nil && shouldInitializeTracer(cfg.ethclient, cfg) && len(cfg.Network.URLs) > 0) || cfg.ethclient == nil { tr, err := NewTracer(cs, &abiFinder, cfg, contractAddressToNameMap, addrs) if err != nil { - return nil, errors.Wrap(err, ErrCreateTracer) + return nil, fmt.Errorf("failed to create transaction tracer: %w\n"+ + "This is usually caused by RPC connection issues. Verify your RPC endpoint is accessible", + err) } opts = append(opts, WithTracer(tr)) } @@ -182,13 +199,19 @@ func NewClientRaw( opts ...ClientOpt, ) (*Client, error) { if cfg == nil { - return nil, errors.New(ErrSethConfigIsNil) + return nil, fmt.Errorf("Seth configuration is nil. "+ + "Provide a valid Config when calling NewClientRaw(). "+ + "Consider using NewClient() or NewClientWithConfig() instead") } if cfgErr := cfg.Validate(); cfgErr != nil { return nil, cfgErr } if cfg.ReadOnly && (len(addrs) > 0 || len(pkeys) > 0) { - return nil, errors.New(ErrReadOnlyWithPrivateKeys) + return nil, fmt.Errorf("configuration conflict: read-only mode is enabled, but private keys were provided. "+ + "Read-only mode is for querying blockchain state only (no transactions).\n"+ + "To fix:\n"+ + " 1. Remove private keys if you only need to read data\n"+ + " 2. Set 'read_only = false' in config if you need to send transactions") } var firstUrl string @@ -196,7 +219,8 @@ func NewClientRaw( if cfg.ethclient == nil { L.Info().Msg("Creating new ethereum client") if len(cfg.Network.URLs) == 0 { - return nil, errors.New("no RPC URL provided") + return nil, fmt.Errorf("no RPC URLs provided. "+ + "Set RPC URLs in your seth.toml config under 'urls_secret = [\"http://...\"]' or provide via WithRpcUrl() when using ClientBuilder") } if len(cfg.Network.URLs) > 1 { @@ -213,7 +237,13 @@ func NewClientRaw( }), ) if err != nil { - return nil, fmt.Errorf("failed to connect RPC client to '%s' due to: %w", cfg.MustFirstNetworkURL(), err) + return nil, fmt.Errorf("failed to connect to RPC endpoint '%s': %w\n"+ + "Troubleshooting steps:\n"+ + " 1. Verify the URL is correct and accessible\n"+ + " 2. Check if the RPC node is running\n"+ + " 3. Verify network connectivity and firewall rules\n"+ + " 4. Check if dial_timeout (%s) is sufficient for your network", + cfg.MustFirstNetworkURL(), err, cfg.Network.DialTimeout.String()) } client = ethclient.NewClient(rpcClient) firstUrl = cfg.MustFirstNetworkURL() @@ -243,7 +273,10 @@ func NewClientRaw( if cfg.Network.ChainID == 0 { chainId, err := c.Client.ChainID(context.Background()) if err != nil { - return nil, errors.Wrap(err, "failed to get chain ID") + return nil, fmt.Errorf("failed to get chain ID from RPC: %w\n"+ + "Ensure the RPC endpoint is accessible and the network is running. "+ + "You can also set 'chain_id' explicitly in your seth.toml to avoid this check", + err) } cfg.Network.ChainID = chainId.Uint64() c.ChainID = mustSafeInt64(cfg.Network.ChainID) @@ -256,7 +289,9 @@ func NewClientRaw( if !cfg.IsSimulatedNetwork() { c.ContractAddressToNameMap.addressMap, err = LoadDeployedContracts(cfg.ContractMapFile) if err != nil { - return nil, errors.Wrap(err, ErrReadContractMap) + return nil, fmt.Errorf("failed to load deployed contracts map from '%s': %w\n"+ + "If this is a fresh deployment or you don't need contract mapping, ignore this error by setting save_deployed_contracts_map = false", + cfg.ContractMapFile, err) } if len(c.ContractAddressToNameMap.addressMap) > 0 { L.Info(). @@ -288,7 +323,10 @@ func NewClientRaw( if cfg.CheckRpcHealthOnStart { if cfg.ReadOnly { - return nil, errors.New(ErrReadOnlyRpcHealth) + return nil, fmt.Errorf("RPC health check is not supported in read-only mode because it requires sending transactions. "+ + "Either:\n"+ + " 1. Set 'read_only = false' to enable transaction capabilities\n"+ + " 2. Set 'check_rpc_health_on_start = false' to skip the health check") } if c.NonceManager == nil { L.Debug().Msg("Nonce manager is not set, RPC health check will be skipped. Client will most probably fail on first transaction") @@ -300,7 +338,10 @@ func NewClientRaw( } if cfg.PendingNonceProtectionEnabled && cfg.ReadOnly { - return nil, errors.New(ErrReadOnlyPendingNonce) + return nil, fmt.Errorf("pending nonce protection is not supported in read-only mode because it requires transaction monitoring. "+ + "Either:\n"+ + " 1. Set 'read_only = false' to enable transaction capabilities\n"+ + " 2. Set 'pending_nonce_protection_enabled = false'") } cfg.setEphemeralAddrs() @@ -315,10 +356,15 @@ func NewClientRaw( if cfg.ephemeral { if len(c.Addresses) == 0 { - return nil, errors.New(ErrNoPksEphemeralMode) + return nil, fmt.Errorf("ephemeral mode requires exactly one root private key to fund ephemeral addresses, but no keys were loaded. "+ + "Load the root private key via:\n"+ + " 1. SETH_ROOT_PRIVATE_KEY environment variable\n"+ + " 2. 'root_private_key' in seth.toml\n"+ + " 3. WithPrivateKeys() when using ClientBuilder") } if cfg.ReadOnly { - return nil, errors.New(ErrReadOnlyEphemeralKeys) + return nil, fmt.Errorf("ephemeral mode is not supported in read-only mode because it requires funding transactions. "+ + "Set 'read_only = false' or disable ephemeral mode by removing 'ephemeral_addresses_number' from config") } ctx, cancel := context.WithTimeout(context.Background(), c.Cfg.Network.TxnTimeout.D) defer cancel() @@ -352,7 +398,9 @@ func NewClientRaw( if c.ContractStore == nil { cs, err := NewContractStore(filepath.Join(cfg.ConfigDir, cfg.ABIDir), filepath.Join(cfg.ConfigDir, cfg.BINDir), cfg.GethWrappersDirs) if err != nil { - return nil, errors.Wrap(err, ErrCreateABIStore) + return nil, fmt.Errorf("failed to create contract store for tracing: %w\n"+ + "Tracing requires ABI files. Check 'abi_dir' configuration or disable tracing with tracing_level = 'NONE'", + err) } c.ContractStore = cs } @@ -362,7 +410,9 @@ func NewClientRaw( } tr, err := NewTracer(c.ContractStore, c.ABIFinder, cfg, c.ContractAddressToNameMap, addrs) if err != nil { - return nil, errors.Wrap(err, ErrCreateTracer) + return nil, fmt.Errorf("failed to create transaction tracer: %w\n"+ + "Ensure RPC endpoint supports debug_traceTransaction or set tracing_level = 'NONE'", + err) } c.Tracer = tr @@ -389,7 +439,10 @@ func NewClientRaw( } if c.Cfg.GasBump != nil && c.Cfg.GasBump.Retries != 0 && c.Cfg.ReadOnly { - return nil, errors.New(ErrReadOnlyGasBumping) + return nil, fmt.Errorf("gas bumping is not supported in read-only mode because it requires sending replacement transactions. "+ + "Either:\n"+ + " 1. Set 'read_only = false' to enable transaction capabilities\n"+ + " 2. Set 'gas_bump.retries = 0' to disable gas bumping") } // if gas bumping is enabled, but no strategy is set, we set the default one; otherwise we set the no-op strategy (defensive programming to avoid NPE) @@ -420,7 +473,15 @@ func (m *Client) checkRPCHealth() error { err = m.TransferETHFromKey(ctx, 0, m.Addresses[0].Hex(), big.NewInt(10_000), gasPrice) if err != nil { - return errors.Wrap(err, ErrRpcHealthCheckFailed) + return fmt.Errorf("RPC health check failed: %w\n"+ + "The health check sends a small self-transfer transaction to verify RPC functionality.\n"+ + "Possible issues:\n"+ + " 1. RPC node is not accepting transactions\n"+ + " 2. Root key has insufficient balance\n"+ + " 3. Network connectivity problems\n"+ + " 4. Gas price estimation failed\n"+ + "You can disable this check with 'check_rpc_health_on_start = false' in config (seth.toml or ClientBuilder)", + err) } L.Info().Msg("RPC health check passed <---------------- !!!!! ----------------") @@ -441,7 +502,9 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri chainID, err := m.Client.ChainID(ctx) if err != nil { - return errors.Wrap(err, "failed to get network ID") + return fmt.Errorf("failed to get chain ID from RPC: %w\n"+ + "Ensure the RPC endpoint is accessible. Chain ID is required for EIP-155 transaction signing", + err) } var gasLimit int64 @@ -467,14 +530,23 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri L.Debug().Interface("TransferTx", rawTx).Send() signedTx, err := types.SignNewTx(m.PrivateKeys[fromKeyNum], types.NewEIP155Signer(chainID), rawTx) if err != nil { - return errors.Wrap(err, "failed to sign tx") + return fmt.Errorf("failed to sign transaction with key #%d (address: %s): %w\n"+ + "Verify the private key is valid and corresponds to the expected address", + fromKeyNum, m.Addresses[fromKeyNum].Hex(), err) } ctx, sendCancel := context.WithTimeout(ctx, m.Cfg.Network.TxnTimeout.Duration()) defer sendCancel() err = m.Client.SendTransaction(ctx, signedTx) if err != nil { - return errors.Wrap(err, "failed to send transaction") + return fmt.Errorf("failed to send transaction to network: %w\n"+ + "Common causes:\n"+ + " 1. RPC node rejected the transaction\n"+ + " 2. Gas price too low (try increasing gas_price or gas_fee_cap)\n"+ + " 3. Nonce conflict (transaction with same nonce already pending)\n"+ + " 4. Insufficient funds for gas (check account balance)\n"+ + " 5. Transaction timeout (check transaction_timeout in config)", + err) } l := L.With().Str("Transaction", signedTx.Hash().Hex()).Logger() l.Info(). @@ -852,7 +924,13 @@ This issue is caused by one of two things: opts, err := bind.NewKeyedTransactorWithChainID(m.PrivateKeys[keyNum], big.NewInt(m.ChainID)) if err != nil { - err = errors.Wrapf(err, "failed to create transactor for key %d", keyNum) + err = fmt.Errorf("failed to create transactor for key #%d (address: %s, chain ID: %d): %w\n"+ + "This usually indicates:\n"+ + " 1. Invalid private key format\n"+ + " 2. Chain ID mismatch\n"+ + " 3. Key not properly loaded\n"+ + "Verify the private key is valid and chain_id is correct in config", + keyNum, m.Addresses[keyNum].Hex(), m.ChainID, err) m.Errors = append(m.Errors, err) // can't return nil, otherwise RPC wrapper will panic and we might lose funds on testnets/mainnets, that's why // error is passed in Context here to avoid panic, whoever is using Seth should make sure that there is no error @@ -970,7 +1048,14 @@ func (m *Client) EstimateGasLimitForFundTransfer(from, to common.Address, amount }) if err != nil { L.Debug().Msgf("Failed to estimate gas for fund transfer due to: %s", err.Error()) - return 0, errors.Wrapf(err, "failed to estimate gas for fund transfer") + return 0, fmt.Errorf("failed to estimate gas for fund transfer from %s to %s (amount: %s wei): %w\n"+ + "Possible causes:\n"+ + " 1. Insufficient balance in sender account\n"+ + " 2. Invalid recipient address\n"+ + " 3. RPC node doesn't support gas estimation\n"+ + " 4. Network congestion or RPC issues\n"+ + "Try setting an explicit gas_limit in config if estimation continues to fail", + from.Hex(), to.Hex(), amount.String(), err) } return gasLimit, nil } @@ -1039,13 +1124,19 @@ func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.AB if auth.Context != nil { if err, ok := auth.Context.Value(ContextErrorKey{}).(error); ok { - return DeploymentData{}, errors.Wrapf(err, "aborted contract deployment for %s, because context passed in transaction options had an error set", name) + return DeploymentData{}, fmt.Errorf("aborted contract deployment for '%s': context error: %w\n"+ + "This usually means there was an error creating transaction options. "+ + "Check the error above for details about what went wrong", + name, err) } } if m.Cfg.Hooks != nil && m.Cfg.Hooks.ContractDeployment.Pre != nil { if err := m.Cfg.Hooks.ContractDeployment.Pre(auth, name, abi, bytecode, params...); err != nil { - return DeploymentData{}, errors.Wrap(err, "pre-hook failed") + return DeploymentData{}, fmt.Errorf("contract deployment pre-hook failed for '%s': %w\n"+ + "The pre-deployment hook returned an error. "+ + "Check your hook implementation for issues", + name, err) } } else { L.Trace().Msg("No pre-contract deployment hook defined. Skipping") @@ -1069,7 +1160,10 @@ func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.AB if m.Cfg.Hooks != nil && m.Cfg.Hooks.ContractDeployment.Post != nil { if err := m.Cfg.Hooks.ContractDeployment.Post(m, tx); err != nil { - return DeploymentData{}, errors.Wrap(err, "post-hook failed") + return DeploymentData{}, fmt.Errorf("contract deployment post-hook failed for transaction %s: %w\n"+ + "The post-deployment hook returned an error. "+ + "Check your hook implementation for issues", + tx.Hash().Hex(), err) } } else { L.Trace().Msg("No post-contract deployment hook defined. Skipping") @@ -1092,7 +1186,15 @@ func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.AB cancel() if receipt.Status == 0 { - return errors.New("deployment transaction was reverted") + return fmt.Errorf("contract '%s' deployment transaction was reverted. "+ + "Transaction hash: %s\n"+ + "Common causes:\n"+ + " 1. Constructor parameters are incorrect\n"+ + " 2. Insufficient gas limit\n"+ + " 3. Constructor validation/require failed\n"+ + " 4. Contract bytecode is invalid\n"+ + "Check transaction trace with decode for the specific revert reason", + name, tx.Hash().Hex()) } } @@ -1192,7 +1294,11 @@ type DeploymentData struct { // name of ABI file (you can omit the .abi suffix). func (m *Client) DeployContractFromContractStore(auth *bind.TransactOpts, name string, params ...interface{}) (DeploymentData, error) { if m.ContractStore == nil { - return DeploymentData{}, errors.New("ABIStore is nil") + return DeploymentData{}, fmt.Errorf("contract store is nil. Cannot deploy contract from store.\n"+ + "This usually means:\n"+ + " 1. Seth client wasn't properly initialized\n"+ + " 2. ABI directory path is incorrect in config\n"+ + "Ensure 'abi_dir' and 'bin_dir' are set in seth.toml or use DeployContract() with explicit ABI/bytecode") } name = strings.TrimSuffix(name, ".abi") @@ -1200,12 +1306,62 @@ func (m *Client) DeployContractFromContractStore(auth *bind.TransactOpts, name s contractAbi, ok := m.ContractStore.ABIs[name+".abi"] if !ok { - return DeploymentData{}, errors.New("ABI not found") + abiCount := len(m.ContractStore.ABIs) + abiSample := "" + if abiCount > 0 { + // Show first few ABIs as examples (max 5) + sampleSize := 5 + if abiCount < sampleSize { + sampleSize = abiCount + } + samples := make([]string, 0, sampleSize) + count := 0 + for abiName := range m.ContractStore.ABIs { + if count >= sampleSize { + break + } + samples = append(samples, strings.TrimSuffix(abiName, ".abi")) + count++ + } + abiSample = fmt.Sprintf("\nExample ABIs available: %s", strings.Join(samples, ", ")) + if abiCount > sampleSize { + abiSample += fmt.Sprintf(" (and %d more)", abiCount-sampleSize) + } + } + return DeploymentData{}, fmt.Errorf("ABI for contract '%s' not found in contract store.\n"+ + "Total ABIs loaded: %d%s\n"+ + "Ensure the ABI file '%s.abi' exists in the directory specified by 'abi_dir' in your config", + name, abiCount, abiSample, name) } bytecode, ok := m.ContractStore.BINs[name+".bin"] if !ok { - return DeploymentData{}, errors.New("BIN not found") + binCount := len(m.ContractStore.BINs) + binSample := "" + if binCount > 0 { + // Show first few BINs as examples (max 5) + sampleSize := 5 + if binCount < sampleSize { + sampleSize = binCount + } + samples := make([]string, 0, sampleSize) + count := 0 + for binName := range m.ContractStore.BINs { + if count >= sampleSize { + break + } + samples = append(samples, strings.TrimSuffix(binName, ".bin")) + count++ + } + binSample = fmt.Sprintf("\nExample BINs available: %s", strings.Join(samples, ", ")) + if binCount > sampleSize { + binSample += fmt.Sprintf(" (and %d more)", binCount-sampleSize) + } + } + return DeploymentData{}, fmt.Errorf("bytecode (BIN) for contract '%s' not found in contract store.\n"+ + "Total BINs loaded: %d%s\n"+ + "Ensure the BIN file '%s.bin' exists in the directory specified by 'bin_dir' in your config", + name, binCount, binSample, name) } data, err := m.DeployContract(auth, name, contractAbi, bytecode, params...) @@ -1396,7 +1552,14 @@ func (m *Client) WaitUntilNoPendingTx(address common.Address, timeout time.Durat for { select { case <-waitTimeout.C: - return fmt.Errorf("after '%s' address '%s' still had pending transactions", timeout, address) + return fmt.Errorf("timeout after %s: address %s still has pending transactions. "+ + "This means transactions haven't been mined within the timeout period.\n"+ + "Troubleshooting:\n"+ + " 1. Check if the network is processing transactions (check block explorer)\n"+ + " 2. Gas price might be too low (increase gas_price or gas_fee_cap)\n"+ + " 3. Network congestion (wait longer or increase timeout)\n"+ + " 4. Enable gas bumping: set gas_bump.retries > 0 in config", + timeout, address.Hex()) case <-ticker.C: nonceStatus, err := m.getNonceStatus(address) // if there is an error, we can't be sure if there are pending transactions or not, let's retry on next tick @@ -1419,9 +1582,19 @@ func (m *Client) WaitUntilNoPendingTx(address common.Address, timeout time.Durat func (m *Client) validatePrivateKeysKeyNum(keyNum int) error { if keyNum >= len(m.PrivateKeys) || keyNum < 0 { if len(m.PrivateKeys) == 0 { - return fmt.Errorf("no private keys were loaded, but keyNum %d was requested", keyNum) + return fmt.Errorf("no private keys loaded, but tried to use key #%d.\n"+ + "Load private keys by:\n"+ + " 1. Setting SETH_ROOT_PRIVATE_KEY environment variable\n"+ + " 2. Adding 'private_keys_secret' to your seth.toml network config\n"+ + " 3. Using WithPrivateKeys() with ClientBuilder", + keyNum) } - return fmt.Errorf("keyNum is out of range for known private keys. Expected %d to %d. Got: %d", 0, len(m.PrivateKeys)-1, keyNum) + return fmt.Errorf("keyNum %d is out of range. Available keys: 0-%d (total: %d keys loaded).\n"+ + "Common causes:\n"+ + " 1. Using keyNum from another test/context\n"+ + " 2. Not enough keys configured for parallel test execution\n"+ + " 3. Consider enabling ephemeral_addresses_number in your config for more keys", + keyNum, len(m.PrivateKeys)-1, len(m.PrivateKeys)) } return nil @@ -1430,9 +1603,14 @@ func (m *Client) validatePrivateKeysKeyNum(keyNum int) error { func (m *Client) validateAddressesKeyNum(keyNum int) error { if keyNum >= len(m.Addresses) || keyNum < 0 { if len(m.Addresses) == 0 { - return fmt.Errorf("no addresses were loaded, but keyNum %d was requested", keyNum) + return fmt.Errorf("no addresses loaded, but tried to use key #%d.\n"+ + "This should not happen if private keys were loaded correctly. "+ + "Please report this as a bug", + keyNum) } - return fmt.Errorf("keyNum is out of range for known addresses. Expected %d to %d. Got: %d", 0, len(m.Addresses)-1, keyNum) + return fmt.Errorf("keyNum %d is out of range. Available addresses: 0-%d (total: %d addresses loaded).\n"+ + "This indicates the keyNum is invalid for the current client configuration", + keyNum, len(m.Addresses)-1, len(m.Addresses)) } return nil diff --git a/seth/config.go b/seth/config.go index b2403d7d0..4f683a587 100644 --- a/seth/config.go +++ b/seth/config.go @@ -13,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient/simulated" "github.com/pelletier/go-toml/v2" - "github.com/pkg/errors" ) const ( @@ -131,16 +130,30 @@ func DefaultClient(rpcUrl string, privateKeys []string) (*Client, error) { func ReadConfig() (*Config, error) { cfgPath := os.Getenv(CONFIG_FILE_ENV_VAR) if cfgPath == "" { - return nil, errors.New(ErrEmptyConfigPath) + return nil, fmt.Errorf("SETH_CONFIG_PATH environment variable is not set. "+ + "Set it to the absolute path of your seth.toml configuration file.\n"+ + "Example: export SETH_CONFIG_PATH=/path/to/your/seth.toml") } var cfg *Config d, err := os.ReadFile(cfgPath) if err != nil { - return nil, errors.Wrap(err, ErrReadSethConfig) + return nil, fmt.Errorf("failed to read Seth config file at '%s': %w\n"+ + "Ensure:\n"+ + " 1. The file exists at the specified path\n"+ + " 2. You have read permissions for the file\n"+ + " 3. The path is correct (set via SETH_CONFIG_PATH or parameter)", + cfgPath, err) } err = toml.Unmarshal(d, &cfg) if err != nil { - return nil, errors.Wrap(err, ErrUnmarshalSethConfig) + return nil, fmt.Errorf("failed to parse Seth TOML config from '%s': %w\n"+ + "Ensure the file contains valid TOML syntax. "+ + "Common issues:\n"+ + " 1. Missing quotes around string values\n"+ + " 2. Invalid array or table syntax\n"+ + " 3. Duplicate keys\n"+ + "See example config: https://github.com/smartcontractkit/chainlink-testing-framework/blob/main/seth/seth.toml", + cfgPath, err) } absPath, err := filepath.Abs(cfgPath) if err != nil { @@ -162,7 +175,15 @@ func ReadConfig() (*Config, error) { url := os.Getenv(URL_ENV_VAR) if url == "" { - return nil, fmt.Errorf("network not selected, set %s=... or %s=..., check TOML config for available networks", NETWORK_ENV_VAR, URL_ENV_VAR) + availableNetworks := make([]string, 0, len(cfg.Networks)) + for _, n := range cfg.Networks { + availableNetworks = append(availableNetworks, n.Name) + } + return nil, fmt.Errorf("network not selected. Set either:\n"+ + " - SETH_NETWORK to one of: %s\n"+ + " - SETH_URL to a custom RPC endpoint\n"+ + "Check your TOML config at %s for network details", + strings.Join(availableNetworks, ", "), cfgPath) } //look for default network @@ -182,13 +203,21 @@ func ReadConfig() (*Config, error) { } if cfg.Network == nil { - return nil, fmt.Errorf("default network not defined in the TOML file") + return nil, fmt.Errorf("default network not defined in the TOML file at %s. "+ + "Add a network with name='%s' or specify a network using SETH_NETWORK environment variable", + cfgPath, DefaultNetworkName) } } rootPrivateKey := os.Getenv(ROOT_PRIVATE_KEY_ENV_VAR) if rootPrivateKey == "" { - return nil, errors.Errorf(ErrEmptyRootPrivateKey, ROOT_PRIVATE_KEY_ENV_VAR) + return nil, fmt.Errorf("no root private key was set. "+ + "You can provide the root private key via:\n"+ + " 1. %s environment variable (without 0x prefix)\n"+ + " 2. 'root_private_key' field in seth.toml\n"+ + " 3. WithPrivateKeys() when using ClientBuilder\n"+ + "WARNING: Never commit private keys to source control. Use environment variables or secure secret management", + ROOT_PRIVATE_KEY_ENV_VAR) } cfg.Network.PrivateKeys = append(cfg.Network.PrivateKeys, rootPrivateKey) if cfg.Network.DialTimeout == nil { @@ -205,7 +234,13 @@ func ReadConfig() (*Config, error) { // If any configuration is invalid, it returns an error. func (c *Config) Validate() error { if c.Network == nil { - return errors.New(ErrNetworkIsNil) + return fmt.Errorf("network configuration is nil. "+ + "This usually means the network wasn't selected or configured properly.\n"+ + "Solutions:\n"+ + " 1. Set SETH_NETWORK environment variable to match a network name in seth.toml (e.g., SETH_NETWORK=sepolia)\n"+ + " 2. Ensure your seth.toml has a [[networks]] section with 'name' field matching SETH_NETWORK\n"+ + " 3. Use ClientBuilder with WithNetwork() to configure the network programmatically\n"+ + "See documentation for configuration examples") } if c.Network.GasPriceEstimationEnabled { @@ -225,12 +260,20 @@ func (c *Config) Validate() error { case Priority_Slow: case Priority_Auto: default: - return errors.New("when automating gas estimation is enabled priority must be auto, fast, standard or slow. fix it or disable gas estimation") + return fmt.Errorf("invalid gas estimation priority '%s'. "+ + "Must be one of: 'auto', 'fast', 'standard' or 'slow'. "+ + "Set 'gas_price_estimation_tx_priority' in your seth.toml config. "+ + "To disable gas estimation, set 'gas_price_estimation_enabled = false'", + c.Network.GasPriceEstimationTxPriority) } if c.GasBump != nil && c.GasBump.Retries > 0 && c.Network.GasPriceEstimationTxPriority == Priority_Auto { - return errors.New("gas bumping is not compatible with auto priority gas estimation") + return fmt.Errorf("configuration conflict: gas bumping (retries=%d) is not compatible with auto priority gas estimation. "+ + "Either:\n"+ + " 1. Set gas_price_estimation_tx_priority to 'fast', 'standard', or 'slow'\n"+ + " 2. Set gas_bump.retries = 0 to disable gas bumping", + c.GasBump.Retries) } } @@ -254,7 +297,10 @@ func (c *Config) Validate() error { case TracingLevel_Reverted: case TracingLevel_All: default: - return errors.New("tracing level must be one of: NONE, REVERTED, ALL") + return fmt.Errorf("invalid tracing level '%s'. Must be one of: 'NONE', 'REVERTED', 'ALL'. "+ + "Set 'tracing_level' in your seth.toml config.\n"+ + "Recommended: 'REVERTED' for debugging failed transactions, 'NONE' to disable tracing completely", + c.TracingLevel) } for _, output := range c.TraceOutputs { @@ -263,7 +309,10 @@ func (c *Config) Validate() error { case TraceOutput_JSON: case TraceOutput_DOT: default: - return errors.New("trace output must be one of: console, json, dot") + return fmt.Errorf("invalid trace output '%s'. Must be one of: 'console', 'json', 'dot'. "+ + "Set 'trace_outputs' in your seth.toml config. "+ + "You can specify multiple outputs as an array", + output) } } @@ -276,11 +325,18 @@ func (c *Config) Validate() error { } if c.ethclient == nil && len(c.Network.URLs) == 0 { - return errors.New("at least one url should be present in config in 'secret_urls = []'") + return fmt.Errorf("no RPC URLs configured. "+ + "You can provide RPC URLs via:\n"+ + " 1. 'urls_secret' field in seth.toml: urls_secret = [\"http://your-rpc-url\"]\n"+ + " 2. WithRPCURLs() when using ClientBuilder\n"+ + " 3. WithEthClient() to provide a pre-configured ethclient instance") } if c.ethclient != nil && len(c.Network.URLs) > 0 { - return errors.New(EthClientAndUrlsSet) + return fmt.Errorf("configuration conflict: both ethclient instance and RPC URLs are set. "+ + "You cannot set both. Either:\n"+ + " 1. Use the provided ethclient (remove 'urls_secret' from config)\n"+ + " 2. Use RPC URLs from config (don't provide ethclient)") } return nil diff --git a/seth/contract_store.go b/seth/contract_store.go index 5d8424f0e..03dce4f43 100644 --- a/seth/contract_store.go +++ b/seth/contract_store.go @@ -13,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" ) const ( @@ -111,7 +110,12 @@ func NewContractStore(abiPath, binPath string, gethWrappersPaths []string) (*Con err = cs.loadGethWrappers(gethWrappersPaths) if err != nil { - return nil, errors.Wrapf(err, "failed to load geth wrappers from %v", gethWrappersPaths) + return nil, fmt.Errorf("failed to load geth wrappers from %v: %w\n"+ + "Ensure:\n"+ + " 1. The paths point to valid Go files with geth-generated contract wrappers\n"+ + " 2. Files contain properly formatted ABI JSON in comments\n"+ + " 3. The wrapper files were generated with abigen tool", + gethWrappersPaths, err) } return cs, nil @@ -129,18 +133,29 @@ func (c *ContractStore) loadABIs(abiPath string) error { L.Debug().Str("File", f.Name()).Msg("ABI file loaded") ff, err := os.Open(filepath.Join(abiPath, f.Name())) if err != nil { - return errors.Wrap(err, ErrOpenABIFile) + return fmt.Errorf("failed to open ABI file '%s': %w\n"+ + "Ensure the file exists and has proper read permissions", + filepath.Join(abiPath, f.Name()), err) } a, err := abi.JSON(ff) if err != nil { - return errors.Wrap(err, ErrParseABI) + return fmt.Errorf("failed to parse ABI file '%s': %w\n"+ + "Ensure the file contains valid JSON ABI format. "+ + "ABI files should be generated from contract compilation (e.g., solc, hardhat, foundry)", + f.Name(), err) } c.ABIs[f.Name()] = a foundABI = true } } if !foundABI { - return fmt.Errorf("no ABI files found in '%s'. Fix the path or comment out 'abi_dir' setting", abiPath) + return fmt.Errorf("no ABI files found in '%s'. "+ + "Ensure:\n"+ + " 1. The directory exists and is readable\n"+ + " 2. Files have .abi extension\n"+ + " 3. Path is correct (should be relative to config file or absolute)\n"+ + "Or comment out 'abi_dir' in config if not using ABI files", + abiPath) } } @@ -159,14 +174,23 @@ func (c *ContractStore) loadBINs(binPath string) error { L.Debug().Str("File", f.Name()).Msg("BIN file loaded") bin, err := os.ReadFile(filepath.Join(binPath, f.Name())) if err != nil { - return errors.Wrap(err, ErrOpenBINFile) + return fmt.Errorf("failed to open BIN file '%s': %w\n"+ + "Ensure the file exists and has proper read permissions", + filepath.Join(binPath, f.Name()), err) } c.BINs[f.Name()] = common.FromHex(string(bin)) foundBIN = true } } if !foundBIN { - return fmt.Errorf("no BIN files found in '%s'. Fix the path or comment out 'bin_dir' setting", binPath) + return fmt.Errorf("no BIN files (bytecode) found in '%s'. "+ + "BIN files are needed for contract deployment. "+ + "Ensure:\n"+ + " 1. Files have .bin extension\n"+ + " 2. They contain compiled contract bytecode (hex-encoded)\n"+ + " 3. Path is correct (should be relative to config file or absolute)\n"+ + "Or comment out 'bin_dir' in config if deploying contracts via other means", + binPath) } } @@ -253,12 +277,18 @@ TOP_LOOP: // this cleans up all escape and similar characters that might interfere with the JSON unmarshalling var rawAbi interface{} if err := json.Unmarshal([]byte(abiContent), &rawAbi); err != nil { - return "", nil, errors.Wrap(err, "failed to unmarshal ABI content") + return "", nil, fmt.Errorf("failed to unmarshal ABI content from '%s': %w\n"+ + "The ABI JSON in the wrapper file is malformed. "+ + "Ensure the file was generated correctly with abigen", + filePath, err) } parsedAbi, err := abi.JSON(strings.NewReader(fmt.Sprint(rawAbi))) if err != nil { - return "", nil, errors.Wrap(err, "failed to parse ABI content") + return "", nil, fmt.Errorf("failed to parse ABI content from '%s': %w\n"+ + "The ABI structure is invalid. "+ + "Regenerate the wrapper file with abigen", + filePath, err) } return contractName, &parsedAbi, nil diff --git a/seth/decode.go b/seth/decode.go index 6daa65c7e..ce1afc009 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -125,7 +125,7 @@ func (m *Client) DecodeSendErr(txErr error) error { reason, decodingErr := m.DecodeCustomABIErr(txErr) if decodingErr == nil && reason != "" { - return errors.Wrap(txErr, reason) + return fmt.Errorf("%s: %w", reason, txErr) } L.Trace(). @@ -434,7 +434,9 @@ func (m *Client) decodeTransaction(l zerolog.Logger, tx *types.Transaction, rece txInput, err = decodeTxInputs(l, txData, abiResult.Method) if err != nil { - return defaultTxn, errors.Wrap(err, ErrDecodeInput) + return defaultTxn, fmt.Errorf("failed to decode transaction input for method '%s': %w\n"+ + "The transaction data doesn't match the expected ABI method signature", + abiResult.Method.Name, err) } var txIndex uint @@ -537,7 +539,9 @@ func (m *Client) CallMsgFromTx(tx *types.Transaction) (ethereum.CallMsg, error) signer := types.LatestSignerForChainID(tx.ChainId()) sender, err := types.Sender(signer, tx) if err != nil { - return ethereum.CallMsg{}, errors.Wrapf(err, "failed to get sender from transaction") + return ethereum.CallMsg{}, fmt.Errorf("failed to get sender from transaction %s: %w\n"+ + "This usually means the transaction signature is invalid or doesn't match the chain ID", + tx.Hash().Hex(), err) } if tx.Type() == types.LegacyTxType { @@ -566,7 +570,12 @@ func (m *Client) CallMsgFromTx(tx *types.Transaction) (ethereum.CallMsg, error) func (m *Client) DownloadContractAndGetPragma(address common.Address, block *big.Int) (Pragma, error) { bytecode, err := m.Client.CodeAt(context.Background(), address, block) if err != nil { - return Pragma{}, errors.Wrap(err, "failed to get contract code") + return Pragma{}, fmt.Errorf("failed to get contract code at address %s (block %s): %w\n"+ + "Ensure:\n"+ + " 1. The address contains a deployed contract\n"+ + " 2. The block number is valid\n"+ + " 3. RPC node is synced and accessible", + address.Hex(), block.String(), err) } pragma, err := DecodePragmaVersion(common.Bytes2Hex(bytecode)) @@ -665,7 +674,9 @@ func decodeTxOutputs(l zerolog.Logger, payload []byte, method *abi.Method) (map[ } else { err := method.Outputs.UnpackIntoMap(outputMap, payload) if err != nil { - return nil, errors.Wrap(err, ErrDecodeOutput) + return nil, fmt.Errorf("failed to decode transaction output for method '%s': %w\n"+ + "The output data doesn't match the expected ABI return types", + method.Name, err) } } l.Trace().Interface("Outputs", outputMap).Msg("Transaction outputs") @@ -690,7 +701,9 @@ func decodeEventFromLog( if len(lo.GetData()) != 0 { err := a.UnpackIntoMap(eventsMap, eventABISpec.Name, lo.GetData()) if err != nil { - return nil, nil, errors.Wrap(err, ErrDecodedLogNonIndexed) + return nil, nil, fmt.Errorf("failed to decode non-indexed log data for event '%s': %w\n"+ + "The log data doesn't match the expected event signature", + eventABISpec.Name, err) } l.Trace().Interface("Non-indexed", eventsMap).Send() } @@ -715,7 +728,9 @@ func decodeEventFromLog( l.Trace().Interface("Indexed", indexed).Send() err := abi.ParseTopicsIntoMap(topicsMap, indexed, indexedTopics) if err != nil { - return nil, nil, errors.Wrap(err, ErrDecodeILogIndexed) + return nil, nil, fmt.Errorf("failed to decode indexed log topics for event '%s': %w\n"+ + "The indexed topic data doesn't match the expected event indexed parameters", + eventABISpec.Name, err) } l.Trace().Interface("Indexed", topicsMap).Send() } diff --git a/seth/gas.go b/seth/gas.go index 6f3e6f743..ce3cc5077 100644 --- a/seth/gas.go +++ b/seth/gas.go @@ -6,7 +6,6 @@ import ( "math/big" "github.com/montanaflynn/stats" - "github.com/pkg/errors" ) // GasEstimator estimates gas prices @@ -27,15 +26,25 @@ func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPer estimations := GasSuggestions{} if blockCount == 0 { - return estimations, errors.New("block count must be greater than zero") + return estimations, fmt.Errorf("block count must be greater than zero for gas estimation. "+ + "Check 'gas_price_estimation_blocks' in your config (seth.toml or ClientBuilder) - current value: %d", blockCount) } currentBlock, err := m.Client.Client.BlockNumber(ctx) if err != nil { - return GasSuggestions{}, fmt.Errorf("failed to get current block number: %w", err) + return GasSuggestions{}, fmt.Errorf("failed to get current block number: %w\n"+ + "Ensure RPC endpoint is accessible and synced. "+ + "Block history-based gas estimation requires access to recent block data. "+ + "Alternatively, set 'gas_price_estimation_blocks = 0' to disable block-based estimation", + err) } if currentBlock == 0 { - return GasSuggestions{}, errors.New("current block number is zero. No fee history available") + return GasSuggestions{}, fmt.Errorf("current block number is zero, which indicates either:\n" + + " 1. The network hasn't produced any blocks yet (check if network is running)\n" + + " 2. RPC node is not synced\n" + + " 3. Connection to RPC node failed\n" + + "Block history-based gas estimation is not possible without block history. " + + "You can set 'gas_price_estimation_blocks = 0' to disable block-based estimation") } if blockCount >= currentBlock { blockCount = currentBlock - 1 @@ -43,7 +52,13 @@ func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPer hist, err := m.Client.Client.FeeHistory(ctx, blockCount, big.NewInt(mustSafeInt64(currentBlock)), []float64{priorityPerc}) if err != nil { - return GasSuggestions{}, fmt.Errorf("failed to get fee history: %w", err) + return GasSuggestions{}, fmt.Errorf("failed to get fee history for %d blocks: %w\n"+ + "Possible causes:\n"+ + " 1. RPC node doesn't support eth_feeHistory\n"+ + " 2. Not enough blocks available (current block: %d)\n"+ + " 3. Network connection issues\n"+ + "Try reducing 'gas_price_estimation_blocks' in config", + blockCount, err, currentBlock) } L.Trace(). Interface("History", hist). @@ -60,7 +75,10 @@ func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPer } gasPercs, err := quantilesFromFloatArray(baseFees) if err != nil { - return GasSuggestions{}, fmt.Errorf("failed to calculate quantiles from fee history for base fee: %w", err) + return GasSuggestions{}, fmt.Errorf("failed to calculate gas price quantiles from %d blocks of fee history: %w\n"+ + "This might indicate insufficient or invalid fee data. "+ + "Try reducing 'gas_price_estimation_blocks' in config", + len(baseFees), err) } estimations.BaseFeePerc = gasPercs @@ -82,7 +100,10 @@ func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPer } tipPercs, err := quantilesFromFloatArray(tips) if err != nil { - return GasSuggestions{}, fmt.Errorf("failed to calculate quantiles from fee history for tip cap: %w", err) + return GasSuggestions{}, fmt.Errorf("failed to calculate tip cap quantiles from %d blocks of fee history: %w\n"+ + "This might indicate insufficient or invalid tip data. "+ + "Try reducing 'gas_price_estimation_blocks' in config", + len(tips), err) } estimations.TipCapPerc = tipPercs L.Trace(). @@ -91,20 +112,32 @@ func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPer suggestedGasPrice, err := m.Client.Client.SuggestGasPrice(ctx) if err != nil { - return GasSuggestions{}, fmt.Errorf("failed to get suggested gas price: %w", err) + return GasSuggestions{}, fmt.Errorf("failed to get suggested gas price from RPC: %w\n"+ + "Possible solutions:\n"+ + " 1. Disable gas estimation and set explicit 'gas_price' in config (gas_price_estimation_enabled = false)\n"+ + " 2. Check RPC node capabilities and accessibility\n"+ + " 3. Verify the network supports gas price queries", + err) } estimations.SuggestedGasPrice = suggestedGasPrice suggestedGasTipCap, err := m.Client.Client.SuggestGasTipCap(ctx) if err != nil { - return GasSuggestions{}, fmt.Errorf("failed to get suggested gas tip cap: %w", err) + return GasSuggestions{}, fmt.Errorf("failed to get suggested gas tip cap from RPC: %w\n"+ + "Possible solutions:\n"+ + " 1. Disable gas estimation and set explicit 'gas_tip_cap' in config (gas_price_estimation_enabled = false)\n"+ + " 2. Check if network supports EIP-1559\n"+ + " 3. Verify RPC node capabilities", + err) } estimations.SuggestedGasTipCap = suggestedGasTipCap header, err := m.Client.Client.HeaderByNumber(ctx, nil) if err != nil { - return GasSuggestions{}, fmt.Errorf("failed to get latest block header: %w", err) + return GasSuggestions{}, fmt.Errorf("failed to get latest block header: %w\n"+ + "Cannot determine current base fee. Check RPC connection", + err) } estimations.LastBaseFee = header.BaseFee diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 2d5ed8922..aa1bc25b2 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -381,6 +381,16 @@ func (m *Client) eip1559FeesFromHistory(ctx context.Context, priority string) (b err = fmt.Errorf("failed to fetch EIP1559 historical fees: %w", retryErr) return } + + if baseFee64 == 0.0 { + err = fmt.Errorf("historical base fee is 0.0. This might indicate insufficient or invalid fee data from the node.\n" + + "Possible solutions:\n" + + " 1. Reduce 'gas_price_estimation_blocks' in config\n" + + " 2. Check RPC node capabilities and accessibility\n" + + " 3. Verify the network supports EIP-1559 fee history queries") + return + } + baseFee = big.NewInt(int64(baseFee64)) L.Debug(). @@ -446,6 +456,27 @@ func (m *Client) currentIP1559Fees(ctx context.Context) (baseFee *big.Int, tipCa return } + if baseFee == nil || baseFee.Int64() == 0 { + err = fmt.Errorf("RPC node returned base fee of 0, which is invalid for EIP-1559 transactions.\n" + + "This might indicate:\n" + + " 1. Network doesn't support EIP-1559 (use legacy transactions instead)\n" + + " 2. RPC node configuration issue\n" + + "Solution: Disable gas estimation and set explicit gas prices in config:\n" + + " - Set gas_price_estimation_enabled = false\n" + + " - Set gas_fee_cap and gas_tip_cap for EIP-1559 networks\n" + + " - Or use eip1559_dynamic_fees = false to switch to legacy transactions") + return + } + + if tipCap == nil { + err = fmt.Errorf("RPC node returned nil gas tip cap.\n" + + "This indicates an RPC error when fetching EIP-1559 gas suggestions.\n" + + "Solution: Disable gas estimation and set explicit gas prices in config:\n" + + " - Set gas_price_estimation_enabled = false\n" + + " - Set gas_fee_cap and gas_tip_cap for EIP-1559 networks") + return + } + baseFee64, _ := baseFee.Float64() tipCap64, _ := tipCap.Float64() L.Debug(). @@ -472,7 +503,14 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a } if suggestedGasPrice.Int64() == 0 { - return errors.New("suggested gas price is 0") + return fmt.Errorf("RPC node returned gas price of 0, which is invalid.\n" + + "This might indicate:\n" + + " 1. Network doesn't support gas price estimation (some test networks)\n" + + " 2. RPC node configuration issue\n" + + "Solution: Disable gas estimation and set explicit gas prices in config:\n" + + " - Set gas_price_estimation_enabled = false\n" + + " - For EIP-1559 networks: set gas_fee_cap and gas_tip_cap\n" + + " - For legacy networks: set gas_price") } return nil diff --git a/seth/header_cache.go b/seth/header_cache.go index c1752c9d3..e314a7e81 100644 --- a/seth/header_cache.go +++ b/seth/header_cache.go @@ -44,7 +44,9 @@ func (c *LFUHeaderCache) Get(blockNumber int64) (*types.Header, bool) { // Set adds or updates a header in the cache. func (c *LFUHeaderCache) Set(header *types.Header) error { if header == nil { - return fmt.Errorf("header is nil") + return fmt.Errorf("cannot add nil header to cache. "+ + "This indicates a bug in Seth or the calling code. "+ + "Please report this issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues with the stack trace") } c.mu.Lock() defer c.mu.Unlock() diff --git a/seth/nonce.go b/seth/nonce.go index 68e967493..99ff8ed74 100644 --- a/seth/nonce.go +++ b/seth/nonce.go @@ -3,6 +3,7 @@ package seth import ( "context" "crypto/ecdsa" + "fmt" "time" "math/big" @@ -10,12 +11,11 @@ import ( "github.com/avast/retry-go" "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" "go.uber.org/ratelimit" ) const ( - ErrKeySyncTimeout = "key sync timeout, consider increasing key_sync_timeout in seth.toml, or increasing the number of keys" + ErrKeySyncTimeout = "key sync timeout, consider increasing key_sync_timeout in config (seth.toml or ClientBuilder), or increasing the number of keys" ErrKeySync = "failed to sync the key" ErrNonce = "failed to get nonce" TimeoutKeyNum = -80001 @@ -41,13 +41,22 @@ type KeyNonce struct { func validateNonceManagerConfig(nonceManagerCfg *NonceManagerCfg) error { if nonceManagerCfg.KeySyncRateLimitSec <= 0 { - return errors.New("key_sync_rate_limit_sec should be positive") + return fmt.Errorf("key_sync_rate_limit_sec must be positive (current: %d). "+ + "This controls how many sync attempts per second are allowed. "+ + "Set it in the 'nonce_manager' section of config (seth.toml or ClientBuilder)", + nonceManagerCfg.KeySyncRateLimitSec) } if nonceManagerCfg.KeySyncTimeout == nil || nonceManagerCfg.KeySyncTimeout.Duration() <= 0 { - return errors.New("key_sync_timeout should be positive") + return fmt.Errorf("key_sync_timeout must be positive (current: %v). "+ + "This is how long to wait for a key to sync before timing out. "+ + "Set it in the 'nonce_manager' section of config (seth.toml or ClientBuilder)", + nonceManagerCfg.KeySyncTimeout) } if nonceManagerCfg.KeySyncRetries <= 0 { - return errors.New("key_sync_retries should be positive") + return fmt.Errorf("key_sync_retries must be positive (current: %d). "+ + "This is how many times to retry syncing a key before giving up. "+ + "Set it in the 'nonce_manager' section of config (seth.toml or ClientBuilder)", + nonceManagerCfg.KeySyncRetries) } return nil @@ -56,13 +65,23 @@ func validateNonceManagerConfig(nonceManagerCfg *NonceManagerCfg) error { // NewNonceManager creates a new nonce manager that tracks nonce for each address func NewNonceManager(cfg *Config, addrs []common.Address, privKeys []*ecdsa.PrivateKey) (*NonceManager, error) { if cfg == nil { - return nil, errors.New(ErrSethConfigIsNil) + return nil, fmt.Errorf("Seth configuration is nil. Cannot create nonce manager without valid configuration.\n" + + "This usually means you're trying to create a nonce manager before initializing Seth.\n" + + "Solutions:\n" + + " 1. Use NewClient() or NewClientWithConfig() to create a Seth client first\n" + + " 2. If using ClientBuilder, ensure you call Build() before accessing the nonce manager\n" + + " 3. Check that your configuration file (seth.toml) exists and is valid") } if cfg.NonceManager == nil { - return nil, errors.New(ErrNonceManagerConfigIsNil) + return nil, fmt.Errorf("nonce manager configuration is nil. " + + "Add a [nonce_manager] section to your config (seth.toml) or use ClientBuilder with:\n" + + " - key_sync_rate_limit_per_sec\n" + + " - key_sync_timeout\n" + + " - key_sync_retries\n" + + " - key_sync_retry_delay") } if cfgErr := validateNonceManagerConfig(cfg.NonceManager); cfgErr != nil { - return nil, errors.Wrap(cfgErr, "failed to validate nonce manager config") + return nil, fmt.Errorf("nonce manager configuration validation failed: %w", cfgErr) } nonces := make(map[common.Address]int64) @@ -121,8 +140,16 @@ func (m *NonceManager) anySyncedKey() int { case <-ctx.Done(): m.Lock() defer m.Unlock() - L.Error().Msg(ErrKeySyncTimeout) - m.Client.Errors = append(m.Client.Errors, errors.New(ErrKeySync)) + timeoutErr := fmt.Errorf("key synchronization timed out after %s. "+ + "This means the nonce couldn't be synchronized before the timeout.\n"+ + "Solutions:\n"+ + " 1. Increase 'key_sync_timeout' in config (seth.toml or ClientBuilder) - current: %s\n"+ + " 2. Reduce 'key_sync_rate_limit_per_sec' to allow faster sync attempts\n"+ + " 3. Add more keys with 'ephemeral_addresses_number'\n"+ + " 4. Check RPC node performance and connectivity", + m.cfg.KeySyncTimeout.Duration(), m.cfg.KeySyncTimeout.Duration()) + L.Error().Msg(timeoutErr.Error()) + m.Client.Errors = append(m.Client.Errors, timeoutErr) return TimeoutKeyNum //so that it's pretty unique number of invalid key case keyData := <-m.SyncedKeys: L.Trace(). @@ -140,7 +167,13 @@ func (m *NonceManager) anySyncedKey() int { Msg("Key is syncing") nonce, err := m.Client.Client.NonceAt(context.Background(), m.Addresses[keyData.KeyNum], nil) if err != nil { - return errors.New(ErrNonce) + return fmt.Errorf("failed to get nonce for address %s (key #%d): %w\n"+ + "This usually indicates:\n"+ + " 1. RPC node connection issues\n"+ + " 2. Network congestion or high latency\n"+ + " 3. Address doesn't exist on the network\n"+ + "Consider increasing key_sync_timeout in your config", + m.Addresses[keyData.KeyNum].Hex(), keyData.KeyNum, err) } if nonce == keyData.Nonce+1 { L.Trace(). @@ -162,13 +195,19 @@ func (m *NonceManager) anySyncedKey() int { Interface("Address", m.Addresses[keyData.KeyNum]). Msg("Key NOT synced") - return errors.New(ErrKeySync) + return fmt.Errorf("key #%d (address: %s) sync failed. "+ + "Expected nonce %d, but got %d. "+ + "This indicates the transaction hasn't been mined yet", + keyData.KeyNum, m.Addresses[keyData.KeyNum].Hex(), + keyData.Nonce+1, nonce) }, retry.Attempts(m.cfg.KeySyncRetries), retry.Delay(m.cfg.KeySyncRetryDelay.Duration()), ) if err != nil { - m.Client.Errors = append(m.Client.Errors, errors.New(ErrKeySync)) + syncErr := fmt.Errorf("failed to sync key #%d after %d retries: %w", + keyData.KeyNum, m.cfg.KeySyncRetries, err) + m.Client.Errors = append(m.Client.Errors, syncErr) } }() return keyData.KeyNum diff --git a/seth/retry.go b/seth/retry.go index ffbe2360d..0162ae45c 100644 --- a/seth/retry.go +++ b/seth/retry.go @@ -47,7 +47,9 @@ func (m *Client) RetryTxAndDecode(f func() (*types.Transaction, error)) (*Decode dt, err := m.Decode(tx, nil) if err != nil { - return &DecodedTransaction{}, errors.Wrap(err, "error decoding transaction") + return &DecodedTransaction{}, fmt.Errorf("error decoding transaction %s: %w\n"+ + "Failed to decode transaction details after waiting for confirmation", + tx.Hash().Hex(), err) } return dt, nil diff --git a/seth/tracing.go b/seth/tracing.go index cc9929381..df09179f5 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -458,7 +458,9 @@ func (t *Tracer) decodeCall(byteSignature []byte, rawCall Call) (*DecodedCall, e if rawCall.Output != "" { output, err := hexutil.Decode(rawCall.Output) if err != nil { - return defaultCall, errors.Wrap(err, ErrDecodeOutput) + return defaultCall, fmt.Errorf("failed to decode call output for method '%s': %w\n"+ + "The hex-encoded output is invalid", + abiResult.Method.Name, err) } txOutput, err = decodeTxOutputs(L, output, abiResult.Method) if err != nil { @@ -618,7 +620,8 @@ func (t *Tracer) decodeContractLogs(l zerolog.Logger, logs []TraceLog, a abi.ABI l.Trace().Str("Name", evSpec.RawName).Str("Signature", evSpec.Sig).Msg("Unpacking event") eventsMap, topicsMap, err := decodeEventFromLog(l, a, evSpec, lo) if err != nil { - return nil, errors.Wrap(err, ErrDecodeLog) + return nil, fmt.Errorf("failed to decode log for event '%s' (signature: %s): %w", + evSpec.RawName, evSpec.Sig, err) } parsedEvent := decodedLogFromMaps(&DecodedCommonLog{}, eventsMap, topicsMap) if decodedLog, ok := parsedEvent.(*DecodedCommonLog); ok { From 94430b7e198ca363c2e422d843550295e3249e65 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Wed, 22 Oct 2025 19:25:03 +0200 Subject: [PATCH 2/9] more error messages fixes + removal of pkg/errors --- lib/utils/seth/seth.go | 46 ++++++++++++++++++++---------- seth/abi_finder.go | 2 +- seth/client.go | 56 ++++++++++++++++++------------------- seth/client_api_test.go | 2 +- seth/client_builder.go | 2 +- seth/client_builder_test.go | 2 +- seth/client_main_test.go | 5 ++-- seth/cmd/seth.go | 10 +++---- seth/config.go | 30 ++++++++++---------- seth/contract_store_test.go | 2 +- seth/decode.go | 5 ++-- seth/gas_adjuster.go | 2 +- seth/go.mod | 2 +- seth/header_cache.go | 4 +-- seth/keyfile.go | 2 +- seth/retry.go | 2 +- seth/tracing.go | 2 +- seth/util.go | 4 +-- 18 files changed, 98 insertions(+), 82 deletions(-) diff --git a/lib/utils/seth/seth.go b/lib/utils/seth/seth.go index dc8d21083..0614fd797 100644 --- a/lib/utils/seth/seth.go +++ b/lib/utils/seth/seth.go @@ -1,11 +1,11 @@ package seth import ( + "errors" "fmt" "regexp" "strings" - "github.com/pkg/errors" "github.com/rs/zerolog" pkg_seth "github.com/smartcontractkit/chainlink-testing-framework/seth" @@ -112,27 +112,29 @@ func GetChainClient(c config.SethConfig, network blockchain.EVMNetwork) (*pkg_se func GetChainClientWithConfigFunction(c config.SethConfig, network blockchain.EVMNetwork, configFn ConfigFunction) (*pkg_seth.Client, error) { readSethCfg := c.GetSethConfig() if readSethCfg == nil { - return nil, errors.New("Seth config not found") + return nil, fmt.Errorf("Seth config not found in the provided configuration. " + + "Ensure your TOML config file has a [Seth] section with required settings. " + + "See example: https://github.com/smartcontractkit/chainlink-testing-framework/blob/main/seth/seth.toml") } sethCfg, err := MergeSethAndEvmNetworkConfigs(network, *readSethCfg) if err != nil { - return nil, errors.Wrapf(err, "Error merging seth and evm network configs") + return nil, fmt.Errorf("error merging seth and evm network configs: %w", err) } err = configFn(&sethCfg) if err != nil { - return nil, errors.Wrapf(err, "Error applying seth config function") + return nil, fmt.Errorf("error applying seth config function: %w", err) } err = ValidateSethNetworkConfig(sethCfg.Network) if err != nil { - return nil, errors.Wrapf(err, "Error validating seth network config") + return nil, fmt.Errorf("error validating seth network config: %w", err) } chainClient, err := pkg_seth.NewClientWithConfig(&sethCfg) if err != nil { - return nil, errors.Wrapf(err, "Error creating seth client") + return nil, fmt.Errorf("error creating seth client: %w", err) } return chainClient, nil @@ -255,33 +257,47 @@ func MustReplaceSimulatedNetworkUrlWithK8(l zerolog.Logger, network blockchain.E // ValidateSethNetworkConfig validates the Seth network config func ValidateSethNetworkConfig(cfg *pkg_seth.Network) error { if cfg == nil { - return errors.New("network cannot be nil") + return fmt.Errorf("network configuration cannot be nil. " + + "Ensure your Seth config has properly configured network settings") } if len(cfg.URLs) == 0 { - return errors.New("URLs are required") + return fmt.Errorf("network URLs are required. " + + "Add RPC endpoint URLs in the 'urls_secret' field of your network config") } if len(cfg.PrivateKeys) == 0 { - return errors.New("PrivateKeys are required") + return fmt.Errorf("private keys are required. " + + "Add at least one private key in 'private_keys_secret' or via environment variables") } if cfg.TransferGasFee == 0 { - return errors.New("TransferGasFee needs to be above 0. It's the gas fee for a simple transfer transaction") + return fmt.Errorf("transfer_gas_fee must be greater than 0. " + + "This is the gas fee for a simple transfer transaction. " + + "Set 'transfer_gas_fee' in your network config") } if cfg.TxnTimeout.Duration() == 0 { - return errors.New("TxnTimeout needs to be above 0. It's the timeout for a transaction") + return fmt.Errorf("transaction timeout must be greater than 0. " + + "Set 'txn_timeout' in your network config (e.g., '30s', '1m')") } if cfg.EIP1559DynamicFees { if cfg.GasFeeCap == 0 { - return errors.New("GasFeeCap needs to be above 0. It's the maximum fee per gas for a transaction (including tip)") + return fmt.Errorf("gas_fee_cap must be greater than 0 for EIP-1559 transactions. " + + "This is the maximum fee per gas (base fee + tip). " + + "Set 'gas_fee_cap' in your network config") } if cfg.GasTipCap == 0 { - return errors.New("GasTipCap needs to be above 0. It's the maximum tip per gas for a transaction") + return fmt.Errorf("gas_tip_cap must be greater than 0 for EIP-1559 transactions. " + + "This is the maximum priority fee per gas. " + + "Set 'gas_tip_cap' in your network config") } if cfg.GasFeeCap <= cfg.GasTipCap { - return errors.New("GasFeeCap needs to be above GasTipCap (as it is base fee + tip cap)") + return fmt.Errorf("gas_fee_cap (%d) must be greater than gas_tip_cap (%d). "+ + "Fee cap should be base fee + tip cap. "+ + "Adjust your network config accordingly", + cfg.GasFeeCap, cfg.GasTipCap) } } else { if cfg.GasPrice == 0 { - return errors.New("GasPrice needs to be above 0. It's the price of gas for a transaction") + return fmt.Errorf("gas_price must be greater than 0 for legacy transactions. " + + "Set 'gas_price' in your network config") } } diff --git a/seth/abi_finder.go b/seth/abi_finder.go index cb0067ef7..05820cc19 100644 --- a/seth/abi_finder.go +++ b/seth/abi_finder.go @@ -1,12 +1,12 @@ package seth import ( + "errors" "fmt" "strings" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" ) type ABIFinder struct { diff --git a/seth/client.go b/seth/client.go index 225c702c5..d04ab7afb 100644 --- a/seth/client.go +++ b/seth/client.go @@ -3,6 +3,7 @@ package seth import ( "context" "crypto/ecdsa" + "errors" "fmt" "math/big" "net/http" @@ -21,7 +22,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" "github.com/rs/zerolog" "golang.org/x/sync/errgroup" ) @@ -89,8 +89,8 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { initDefaultLogging() if cfg == nil { - return nil, fmt.Errorf("Seth configuration is nil. "+ - "Ensure you're calling NewClientWithConfig() with a valid config, or use NewClient() to load from SETH_CONFIG_PATH environment variable. "+ + return nil, fmt.Errorf("Seth configuration is nil. " + + "Ensure you're calling NewClientWithConfig() with a valid config, or use NewClient() to load from SETH_CONFIG_PATH environment variable. " + "See documentation for configuration examples") } if cfgErr := cfg.Validate(); cfgErr != nil { @@ -199,18 +199,18 @@ func NewClientRaw( opts ...ClientOpt, ) (*Client, error) { if cfg == nil { - return nil, fmt.Errorf("Seth configuration is nil. "+ - "Provide a valid Config when calling NewClientRaw(). "+ + return nil, fmt.Errorf("Seth configuration is nil. " + + "Provide a valid Config when calling NewClientRaw(). " + "Consider using NewClient() or NewClientWithConfig() instead") } if cfgErr := cfg.Validate(); cfgErr != nil { return nil, cfgErr } if cfg.ReadOnly && (len(addrs) > 0 || len(pkeys) > 0) { - return nil, fmt.Errorf("configuration conflict: read-only mode is enabled, but private keys were provided. "+ - "Read-only mode is for querying blockchain state only (no transactions).\n"+ - "To fix:\n"+ - " 1. Remove private keys if you only need to read data\n"+ + return nil, fmt.Errorf("configuration conflict: read-only mode is enabled, but private keys were provided. " + + "Read-only mode is for querying blockchain state only (no transactions).\n" + + "To fix:\n" + + " 1. Remove private keys if you only need to read data\n" + " 2. Set 'read_only = false' in config if you need to send transactions") } @@ -219,7 +219,7 @@ func NewClientRaw( if cfg.ethclient == nil { L.Info().Msg("Creating new ethereum client") if len(cfg.Network.URLs) == 0 { - return nil, fmt.Errorf("no RPC URLs provided. "+ + return nil, fmt.Errorf("no RPC URLs provided. " + "Set RPC URLs in your seth.toml config under 'urls_secret = [\"http://...\"]' or provide via WithRpcUrl() when using ClientBuilder") } @@ -323,9 +323,9 @@ func NewClientRaw( if cfg.CheckRpcHealthOnStart { if cfg.ReadOnly { - return nil, fmt.Errorf("RPC health check is not supported in read-only mode because it requires sending transactions. "+ - "Either:\n"+ - " 1. Set 'read_only = false' to enable transaction capabilities\n"+ + return nil, fmt.Errorf("RPC health check is not supported in read-only mode because it requires sending transactions. " + + "Either:\n" + + " 1. Set 'read_only = false' to enable transaction capabilities\n" + " 2. Set 'check_rpc_health_on_start = false' to skip the health check") } if c.NonceManager == nil { @@ -338,9 +338,9 @@ func NewClientRaw( } if cfg.PendingNonceProtectionEnabled && cfg.ReadOnly { - return nil, fmt.Errorf("pending nonce protection is not supported in read-only mode because it requires transaction monitoring. "+ - "Either:\n"+ - " 1. Set 'read_only = false' to enable transaction capabilities\n"+ + return nil, fmt.Errorf("pending nonce protection is not supported in read-only mode because it requires transaction monitoring. " + + "Either:\n" + + " 1. Set 'read_only = false' to enable transaction capabilities\n" + " 2. Set 'pending_nonce_protection_enabled = false'") } @@ -356,14 +356,14 @@ func NewClientRaw( if cfg.ephemeral { if len(c.Addresses) == 0 { - return nil, fmt.Errorf("ephemeral mode requires exactly one root private key to fund ephemeral addresses, but no keys were loaded. "+ - "Load the root private key via:\n"+ - " 1. SETH_ROOT_PRIVATE_KEY environment variable\n"+ - " 2. 'root_private_key' in seth.toml\n"+ + return nil, fmt.Errorf("ephemeral mode requires exactly one root private key to fund ephemeral addresses, but no keys were loaded. " + + "Load the root private key via:\n" + + " 1. SETH_ROOT_PRIVATE_KEY environment variable\n" + + " 2. 'root_private_key' in seth.toml\n" + " 3. WithPrivateKeys() when using ClientBuilder") } if cfg.ReadOnly { - return nil, fmt.Errorf("ephemeral mode is not supported in read-only mode because it requires funding transactions. "+ + return nil, fmt.Errorf("ephemeral mode is not supported in read-only mode because it requires funding transactions. " + "Set 'read_only = false' or disable ephemeral mode by removing 'ephemeral_addresses_number' from config") } ctx, cancel := context.WithTimeout(context.Background(), c.Cfg.Network.TxnTimeout.D) @@ -439,9 +439,9 @@ func NewClientRaw( } if c.Cfg.GasBump != nil && c.Cfg.GasBump.Retries != 0 && c.Cfg.ReadOnly { - return nil, fmt.Errorf("gas bumping is not supported in read-only mode because it requires sending replacement transactions. "+ - "Either:\n"+ - " 1. Set 'read_only = false' to enable transaction capabilities\n"+ + return nil, fmt.Errorf("gas bumping is not supported in read-only mode because it requires sending replacement transactions. " + + "Either:\n" + + " 1. Set 'read_only = false' to enable transaction capabilities\n" + " 2. Set 'gas_bump.retries = 0' to disable gas bumping") } @@ -1294,10 +1294,10 @@ type DeploymentData struct { // name of ABI file (you can omit the .abi suffix). func (m *Client) DeployContractFromContractStore(auth *bind.TransactOpts, name string, params ...interface{}) (DeploymentData, error) { if m.ContractStore == nil { - return DeploymentData{}, fmt.Errorf("contract store is nil. Cannot deploy contract from store.\n"+ - "This usually means:\n"+ - " 1. Seth client wasn't properly initialized\n"+ - " 2. ABI directory path is incorrect in config\n"+ + return DeploymentData{}, fmt.Errorf("contract store is nil. Cannot deploy contract from store.\n" + + "This usually means:\n" + + " 1. Seth client wasn't properly initialized\n" + + " 2. ABI directory path is incorrect in config\n" + "Ensure 'abi_dir' and 'bin_dir' are set in seth.toml or use DeployContract() with explicit ABI/bytecode") } diff --git a/seth/client_api_test.go b/seth/client_api_test.go index c27b54b72..3c5b77758 100644 --- a/seth/client_api_test.go +++ b/seth/client_api_test.go @@ -2,13 +2,13 @@ package seth_test import ( "context" + "errors" "math/big" "sync" "testing" "time" "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/seth" diff --git a/seth/client_builder.go b/seth/client_builder.go index c7ecfbf6e..e40f3269e 100644 --- a/seth/client_builder.go +++ b/seth/client_builder.go @@ -1,11 +1,11 @@ package seth import ( + "errors" "fmt" "time" "github.com/ethereum/go-ethereum/ethclient/simulated" - "github.com/pkg/errors" ) const ( diff --git a/seth/client_builder_test.go b/seth/client_builder_test.go index feda3282b..81d267128 100644 --- a/seth/client_builder_test.go +++ b/seth/client_builder_test.go @@ -2,6 +2,7 @@ package seth_test import ( "crypto/ecdsa" + "errors" "math/big" "os" "strings" @@ -13,7 +14,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" - "github.com/pkg/errors" "github.com/pelletier/go-toml/v2" diff --git a/seth/client_main_test.go b/seth/client_main_test.go index d2d09db44..262d9150c 100644 --- a/seth/client_main_test.go +++ b/seth/client_main_test.go @@ -2,6 +2,8 @@ package seth_test import ( "context" + "errors" + "fmt" "math/big" "os" "testing" @@ -11,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient/simulated" - "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/seth" @@ -150,7 +151,7 @@ func NewDebugContractSetup() ( nm, err := seth.NewNonceManager(cfg, addrs, pkeys) if err != nil { - return nil, nil, common.Address{}, common.Address{}, nil, errors.Wrap(err, seth.ErrCreateNonceManager) + return nil, nil, common.Address{}, common.Address{}, nil, fmt.Errorf("failed to create nonce manager: %w", err) } c, err := seth.NewClientRaw(cfg, addrs, pkeys, seth.WithContractStore(cs), seth.WithTracer(tracer), seth.WithNonceManager(nm)) diff --git a/seth/cmd/seth.go b/seth/cmd/seth.go index e3bbb0e6a..1778f8855 100644 --- a/seth/cmd/seth.go +++ b/seth/cmd/seth.go @@ -2,6 +2,7 @@ package seth import ( "context" + "errors" "fmt" "math/big" "os" @@ -12,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/pelletier/go-toml/v2" - "github.com/pkg/errors" "github.com/urfave/cli/v2" "github.com/smartcontractkit/chainlink-testing-framework/seth" @@ -205,11 +205,11 @@ func RunCLI(args []string) error { var cfg *seth.Config d, err := os.ReadFile(cfgPath) if err != nil { - return errors.Wrap(err, seth.ErrReadSethConfig) + return fmt.Errorf("%s: %w", seth.ErrReadSethConfig, err) } err = toml.Unmarshal(d, &cfg) if err != nil { - return errors.Wrap(err, seth.ErrUnmarshalSethConfig) + return fmt.Errorf("%s: %w", seth.ErrUnmarshalSethConfig, err) } absPath, err := filepath.Abs(cfgPath) if err != nil { @@ -272,7 +272,7 @@ func RunCLI(args []string) error { if cfg.Network.Name == seth.DefaultNetworkName { chainId, err := client.ChainID(context.Background()) if err != nil { - return errors.Wrap(err, "failed to get chain ID") + return fmt.Errorf("failed to get chain ID: %w", err) } cfg.Network.ChainID = chainId.Uint64() } @@ -298,7 +298,7 @@ func RunCLI(args []string) error { tx, _, err := client.Client.TransactionByHash(ctx, common.HexToHash(txHash)) cancel() if err != nil { - return errors.Wrapf(err, "failed to get transaction %s", txHash) + return fmt.Errorf("failed to get transaction %s: %w", txHash, err) } _, err = client.Decode(tx, nil) diff --git a/seth/config.go b/seth/config.go index 4f683a587..3980ea01f 100644 --- a/seth/config.go +++ b/seth/config.go @@ -130,8 +130,8 @@ func DefaultClient(rpcUrl string, privateKeys []string) (*Client, error) { func ReadConfig() (*Config, error) { cfgPath := os.Getenv(CONFIG_FILE_ENV_VAR) if cfgPath == "" { - return nil, fmt.Errorf("SETH_CONFIG_PATH environment variable is not set. "+ - "Set it to the absolute path of your seth.toml configuration file.\n"+ + return nil, fmt.Errorf("SETH_CONFIG_PATH environment variable is not set. " + + "Set it to the absolute path of your seth.toml configuration file.\n" + "Example: export SETH_CONFIG_PATH=/path/to/your/seth.toml") } var cfg *Config @@ -234,12 +234,12 @@ func ReadConfig() (*Config, error) { // If any configuration is invalid, it returns an error. func (c *Config) Validate() error { if c.Network == nil { - return fmt.Errorf("network configuration is nil. "+ - "This usually means the network wasn't selected or configured properly.\n"+ - "Solutions:\n"+ - " 1. Set SETH_NETWORK environment variable to match a network name in seth.toml (e.g., SETH_NETWORK=sepolia)\n"+ - " 2. Ensure your seth.toml has a [[networks]] section with 'name' field matching SETH_NETWORK\n"+ - " 3. Use ClientBuilder with WithNetwork() to configure the network programmatically\n"+ + return fmt.Errorf("network configuration is nil. " + + "This usually means the network wasn't selected or configured properly.\n" + + "Solutions:\n" + + " 1. Set SETH_NETWORK environment variable to match a network name in seth.toml (e.g., SETH_NETWORK=sepolia)\n" + + " 2. Ensure your seth.toml has a [[networks]] section with 'name' field matching SETH_NETWORK\n" + + " 3. Use ClientBuilder with WithNetwork() to configure the network programmatically\n" + "See documentation for configuration examples") } @@ -325,17 +325,17 @@ func (c *Config) Validate() error { } if c.ethclient == nil && len(c.Network.URLs) == 0 { - return fmt.Errorf("no RPC URLs configured. "+ - "You can provide RPC URLs via:\n"+ - " 1. 'urls_secret' field in seth.toml: urls_secret = [\"http://your-rpc-url\"]\n"+ - " 2. WithRPCURLs() when using ClientBuilder\n"+ + return fmt.Errorf("no RPC URLs configured. " + + "You can provide RPC URLs via:\n" + + " 1. 'urls_secret' field in seth.toml: urls_secret = [\"http://your-rpc-url\"]\n" + + " 2. WithRPCURLs() when using ClientBuilder\n" + " 3. WithEthClient() to provide a pre-configured ethclient instance") } if c.ethclient != nil && len(c.Network.URLs) > 0 { - return fmt.Errorf("configuration conflict: both ethclient instance and RPC URLs are set. "+ - "You cannot set both. Either:\n"+ - " 1. Use the provided ethclient (remove 'urls_secret' from config)\n"+ + return fmt.Errorf("configuration conflict: both ethclient instance and RPC URLs are set. " + + "You cannot set both. Either:\n" + + " 1. Use the provided ethclient (remove 'urls_secret' from config)\n" + " 2. Use RPC URLs from config (don't provide ethclient)") } diff --git a/seth/contract_store_test.go b/seth/contract_store_test.go index f9436e971..c63dce054 100644 --- a/seth/contract_store_test.go +++ b/seth/contract_store_test.go @@ -1,9 +1,9 @@ package seth_test import ( + "errors" "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/seth" diff --git a/seth/decode.go b/seth/decode.go index ce1afc009..cd2d11cba 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "encoding/hex" - verr "errors" + "errors" "fmt" "math/big" "path/filepath" @@ -17,7 +17,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" - "github.com/pkg/errors" "github.com/rs/zerolog" ) @@ -103,7 +102,7 @@ func getDefaultDecodedCall() *DecodedCall { // Last, but not least, if gas bumps are enabled, we will try to bump gas on transaction mining timeout and resubmit it with higher gas. func (m *Client) Decode(tx *types.Transaction, txErr error) (*DecodedTransaction, error) { if len(m.Errors) > 0 { - return nil, verr.Join(m.Errors...) + return nil, errors.Join(m.Errors...) } if decodedErr := m.DecodeSendErr(txErr); decodedErr != nil { diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index aa1bc25b2..6ea72f567 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -2,6 +2,7 @@ package seth import ( "context" + "errors" "fmt" "math" "math/big" @@ -12,7 +13,6 @@ import ( "github.com/avast/retry-go" "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" ) const ( diff --git a/seth/go.mod b/seth/go.mod index 6c62211ee..446772cb8 100644 --- a/seth/go.mod +++ b/seth/go.mod @@ -10,7 +10,6 @@ require ( github.com/holiman/uint256 v1.3.2 github.com/montanaflynn/stats v0.7.1 github.com/pelletier/go-toml/v2 v2.2.3 - github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.33.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.27.5 @@ -74,6 +73,7 @@ require ( github.com/pion/stun/v2 v2.0.0 // indirect github.com/pion/transport/v2 v2.2.1 // indirect github.com/pion/transport/v3 v3.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.20.4 // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/seth/header_cache.go b/seth/header_cache.go index e314a7e81..a5a152ccd 100644 --- a/seth/header_cache.go +++ b/seth/header_cache.go @@ -44,8 +44,8 @@ func (c *LFUHeaderCache) Get(blockNumber int64) (*types.Header, bool) { // Set adds or updates a header in the cache. func (c *LFUHeaderCache) Set(header *types.Header) error { if header == nil { - return fmt.Errorf("cannot add nil header to cache. "+ - "This indicates a bug in Seth or the calling code. "+ + return fmt.Errorf("cannot add nil header to cache. " + + "This indicates a bug in Seth or the calling code. " + "Please report this issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues with the stack trace") } c.mu.Lock() diff --git a/seth/keyfile.go b/seth/keyfile.go index 0fc56fbd1..9c8414732 100644 --- a/seth/keyfile.go +++ b/seth/keyfile.go @@ -3,12 +3,12 @@ package seth import ( "context" "crypto/ecdsa" + "errors" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" - "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) diff --git a/seth/retry.go b/seth/retry.go index 0162ae45c..6468bc06c 100644 --- a/seth/retry.go +++ b/seth/retry.go @@ -2,6 +2,7 @@ package seth import ( "context" + "errors" "fmt" "math/big" "strings" @@ -11,7 +12,6 @@ import ( "github.com/avast/retry-go" "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" ) /* these are the common errors of RPCs */ diff --git a/seth/tracing.go b/seth/tracing.go index df09179f5..6f746a064 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -2,6 +2,7 @@ package seth import ( "context" + "errors" "fmt" "strconv" "strings" @@ -11,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" - "github.com/pkg/errors" "github.com/rs/zerolog" ) diff --git a/seth/util.go b/seth/util.go index 5f9e8f33d..c624cb6a2 100644 --- a/seth/util.go +++ b/seth/util.go @@ -5,6 +5,7 @@ import ( "database/sql/driver" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "math" @@ -18,7 +19,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" - "github.com/pkg/errors" network_debug_contract "github.com/smartcontractkit/chainlink-testing-framework/seth/contracts/bind/NetworkDebugContract" network_sub_debug_contract "github.com/smartcontractkit/chainlink-testing-framework/seth/contracts/bind/NetworkDebugSubContract" @@ -236,7 +236,7 @@ func (d *Duration) Scan(v interface{}) (err error) { *d, err = MakeDuration(time.Duration(tv)) return err default: - return errors.Errorf(`don't know how to parse "%s" of type %T as a `+ + return fmt.Errorf(`don't know how to parse "%s" of type %T as a `+ `models.Duration`, tv, tv) } } From f59d8051a0e8de748093fb341ae83f96a53f1604 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Wed, 22 Oct 2025 19:57:15 +0200 Subject: [PATCH 3/9] more error message fixes --- seth/abi_finder.go | 34 +++++--- seth/block_stats.go | 24 +++++- seth/contract_store.go | 45 ++++++---- seth/decode.go | 32 ++++++- seth/gas_adjuster.go | 50 +++++++++-- seth/keyfile.go | 16 +++- seth/retry.go | 32 +++++-- seth/tracing.go | 191 ++++++++++++++++++++++++----------------- seth/util.go | 37 +++++++- 9 files changed, 325 insertions(+), 136 deletions(-) diff --git a/seth/abi_finder.go b/seth/abi_finder.go index 05820cc19..af0bfa961 100644 --- a/seth/abi_finder.go +++ b/seth/abi_finder.go @@ -1,7 +1,6 @@ package seth import ( - "errors" "fmt" "strings" @@ -47,11 +46,21 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder contractName := a.ContractMap.GetContractName(address) abiInstanceCandidate, ok := a.ContractStore.ABIs[contractName+".abi"] if !ok { - err := errors.New(ErrNoAbiFound) + err := fmt.Errorf("no ABI found for contract '%s' at address %s, even though it's registered in the contract map. "+ + "This happens when:\n"+ + " 1. Contract address is in the contract map but ABI file is missing from abi_dir\n"+ + " 2. ABI files were moved or deleted after contract deployment\n"+ + " 3. Contract map is corrupted or out of sync\n"+ + "Troubleshooting:\n"+ + " 1. Verify ABI file '%s.abi' exists in the configured abi_dir\n"+ + " 2. Check if save_deployed_contracts_map = true in config\n"+ + " 3. Re-deploy the contract or manually add ABI with ContractStore.AddABI()\n"+ + " 4. For external contracts, obtain and add the ABI manually", + contractName, address, contractName) L.Err(err). Str("Contract", contractName). Str("Address", address). - Msg("ABI not found, even though contract is known. This should not happen. Contract map might be corrupted") + Msg("ABI not found for known contract") return ABIFinderResult{}, err } @@ -150,14 +159,17 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder return ABIFinderResult{}, fmt.Errorf("no ABI found with method signature %s for contract at address %s.\n"+ "Checked %d ABIs but none matched.%s\n"+ - "This usually means:\n"+ - " 1. The contract ABI wasn't loaded into Seth's contract store\n"+ - " 2. The method signature doesn't match any known ABI\n"+ - " 3. You're calling a non-existent contract address\n"+ - "Solutions:\n"+ - " 1. Add the contract's ABI to the directory specified by 'abi_dir'\n"+ - " 2. Use ContractStore.AddABI() to add it programmatically\n"+ - " 3. Deploy the contract via Seth so it's automatically registered", + "Possible causes:\n"+ + " 1. Contract ABI not loaded (check abi_dir and contract_map_file)\n"+ + " 2. Method signature doesn't match any function in loaded ABIs\n"+ + " 3. Contract address not registered in contract map\n"+ + " 4. Wrong contract address (check deployment logs)\n"+ + "Troubleshooting:\n"+ + " 1. Verify contract was deployed with DeployContract() or loaded with LoadContract()\n"+ + " 2. Check the method signature is correct (case-sensitive, including parameter types)\n"+ + " 3. Ensure ABI file exists in the directory specified by 'abi_dir'\n"+ + " 4. Review contract_map_file for address-to-name mappings\n"+ + " 5. Use ContractStore.AddABI() to manually add the ABI", stringSignature, address, abiCount, abiSample) } diff --git a/seth/block_stats.go b/seth/block_stats.go index 330d1486a..30c1ca54e 100644 --- a/seth/block_stats.go +++ b/seth/block_stats.go @@ -48,7 +48,14 @@ func (cs *BlockStats) Stats(startBlock *big.Int, endBlock *big.Int) error { if endBlock == nil || startBlock.Sign() < 0 { header, err := cs.Client.Client.HeaderByNumber(context.Background(), nil) if err != nil { - return fmt.Errorf("failed to get the latest block header: %v", err) + return fmt.Errorf("failed to get the latest block header for block stats: %w\n"+ + "This indicates RPC connectivity issues.\n"+ + "Troubleshooting:\n"+ + " 1. Verify RPC endpoint is accessible\n"+ + " 2. Check network connectivity\n"+ + " 3. Ensure the node is synced\n"+ + " 4. Try increasing dial_timeout in config", + err) } latestBlockNumber = header.Number } @@ -62,8 +69,12 @@ func (cs *BlockStats) Stats(startBlock *big.Int, endBlock *big.Int) error { endBlock = latestBlockNumber } if endBlock != nil && startBlock.Int64() > endBlock.Int64() { - return fmt.Errorf("start block (%d) is greater than end block (%d). "+ - "Ensure start block comes before end block in the range", + return fmt.Errorf("invalid block range for statistics: start block %d > end block %d.\n"+ + "This is a bug in Seth's block stats calculation logic.\n"+ + "Please open a GitHub issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues with:\n"+ + " 1. Your configuration file\n"+ + " 2. The operation you were performing\n"+ + " 3. Network name and chain ID", startBlock.Int64(), endBlock.Int64()) } L.Info(). @@ -109,7 +120,12 @@ func (cs *BlockStats) Stats(startBlock *big.Int, endBlock *big.Int) error { // CalculateBlockDurations calculates and logs the duration, TPS, gas used, and gas limit between each consecutive block func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error { if len(blocks) == 0 { - return fmt.Errorf("no blocks to analyze. Cannot calculate block durations without block data") + return fmt.Errorf("no block data available for duration analysis. " + + "This happens when:\n" + + " 1. No blocks were provided for analysis\n" + + " 2. All block fetch attempts failed\n" + + " 3. Block range is invalid\n" + + "Check RPC connectivity and ensure blocks exist in the specified range") } var ( durations []time.Duration diff --git a/seth/contract_store.go b/seth/contract_store.go index 03dce4f43..3e5197fe9 100644 --- a/seth/contract_store.go +++ b/seth/contract_store.go @@ -149,13 +149,16 @@ func (c *ContractStore) loadABIs(abiPath string) error { } } if !foundABI { - return fmt.Errorf("no ABI files found in '%s'. "+ - "Ensure:\n"+ - " 1. The directory exists and is readable\n"+ - " 2. Files have .abi extension\n"+ - " 3. Path is correct (should be relative to config file or absolute)\n"+ - "Or comment out 'abi_dir' in config if not using ABI files", - abiPath) + absPath, _ := filepath.Abs(abiPath) + return fmt.Errorf("no ABI files (*.abi) found in directory '%s'.\n"+ + "ABI files are JSON files describing contract interfaces.\n"+ + "Solutions:\n"+ + " 1. Verify the path is correct: %s\n"+ + " 2. Ensure .abi files exist in this directory\n"+ + " 3. Check directory permissions (must be readable)\n"+ + " 4. If deploying contracts without ABIs, remove 'abi_dir' from config\n"+ + " 5. Generate ABIs from Solidity: solc --abi YourContract.sol -o abi_dir/", + abiPath, absPath) } } @@ -183,14 +186,16 @@ func (c *ContractStore) loadBINs(binPath string) error { } } if !foundBIN { - return fmt.Errorf("no BIN files (bytecode) found in '%s'. "+ - "BIN files are needed for contract deployment. "+ - "Ensure:\n"+ - " 1. Files have .bin extension\n"+ - " 2. They contain compiled contract bytecode (hex-encoded)\n"+ - " 3. Path is correct (should be relative to config file or absolute)\n"+ - "Or comment out 'bin_dir' in config if deploying contracts via other means", - binPath) + absPath, _ := filepath.Abs(binPath) + return fmt.Errorf("no BIN files (*.bin) found in directory '%s'.\n"+ + "BIN files contain compiled contract bytecode needed for deployment.\n"+ + "Solutions:\n"+ + " 1. Verify the path is correct: %s\n"+ + " 2. Ensure .bin files exist (should contain hex-encoded bytecode)\n"+ + " 3. Check directory permissions (must be readable)\n"+ + " 4. If deploying contracts without BIN files, remove 'bin_dir' from config\n"+ + " 5. Generate BINs from Solidity: solc --bin YourContract.sol -o bin_dir/", + binPath, absPath) } } @@ -231,7 +236,15 @@ func (c *ContractStore) loadGethWrappers(gethWrappersPaths []string) error { } if len(gethWrappersPaths) > 0 && !foundWrappers { - return fmt.Errorf("no geth wrappers found in '%v'. Fix the path or comment out 'geth_wrappers_dirs' setting", gethWrappersPaths) + return fmt.Errorf("no geth wrapper files found in directories: %v\n"+ + "Geth wrappers are Go files generated by abigen containing contract ABIs.\n"+ + "Solutions:\n"+ + " 1. Verify all paths exist and are readable\n"+ + " 2. Generate wrappers using abigen:\n"+ + " abigen --abi contract.abi --bin contract.bin --pkg wrappers --out contract_wrapper.go\n"+ + " 3. Ensure wrapper files contain ABI metadata (check for 'ABI' variable)\n"+ + " 4. If not using geth wrappers, remove 'geth_wrappers_dirs' from config", + gethWrappersPaths) } return nil diff --git a/seth/decode.go b/seth/decode.go index cd2d11cba..de32a2f75 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -415,7 +415,13 @@ func (m *Client) decodeTransaction(l zerolog.Logger, tx *types.Transaction, rece sig := txData[:4] if m.ABIFinder == nil { - l.Err(errors.New("ABIFInder is nil")).Msg("ABIFinder is required for transaction decoding") + err := fmt.Errorf("ABIFinder is not initialized, cannot decode transaction. " + + "This is an internal error - ABIFinder should be set during client initialization.\n" + + "If you see this error:\n" + + " 1. Ensure you're using NewClient() or NewClientWithConfig() to create the client\n" + + " 2. Don't manually modify client.ABIFinder\n" + + " 3. If the issue persists, please open a GitHub issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues") + l.Err(err).Msg("ABIFinder is required for transaction decoding") return defaultTxn, nil } @@ -499,7 +505,13 @@ func (m *Client) DecodeCustomABIErr(txErr error) (string, error) { //nolint cerr, ok := txErr.(rpc.DataError) if !ok { - return "", errors.New(ErrRPCJSONCastError) + return "", fmt.Errorf("failed to extract revert reason from RPC error response. " + + "The RPC response format is not recognized.\n" + + "This could mean:\n" + + " 1. Your RPC node uses a non-standard error format\n" + + " 2. The transaction didn't revert (unexpected state)\n" + + " 3. RPC node is experiencing issues\n" + + "The transaction trace may still contain useful information") } if m.ContractStore == nil { L.Warn().Msg(WarnNoContractStore) @@ -605,7 +617,13 @@ func (m *Client) callAndGetRevertReason(tx *types.Transaction, rc *types.Receipt return err } if decodedABIErrString != "" { - return errors.New(decodedABIErrString) + return fmt.Errorf("failed to decode ABI from contract code: %s\n"+ + "This happens when:\n"+ + " 1. Contract bytecode doesn't contain valid ABI metadata\n"+ + " 2. Contract wasn't compiled with metadata (--metadata-hash none)\n"+ + " 3. Contract code at the address is not a valid contract\n"+ + "For third-party contracts, manually load the ABI instead of relying on auto-detection", + decodedABIErrString) } if plainStringErr != nil { @@ -637,7 +655,13 @@ func (m *Client) callAndGetRevertReason(tx *types.Transaction, rc *types.Receipt func decodeTxInputs(l zerolog.Logger, txData []byte, method *abi.Method) (map[string]interface{}, error) { l.Trace().Msg("Parsing tx inputs") if (len(txData)) < 4 { - return nil, errors.New(ErrTooShortTxData) + return nil, fmt.Errorf("transaction data is too short to contain a valid function call. "+ + "Expected at least 4 bytes for function selector, got %d bytes.\n"+ + "This might indicate:\n"+ + " 1. Plain ETH transfer (no function call)\n"+ + " 2. Invalid/corrupted transaction data\n"+ + " 3. Contract deployment (not a function call)", + len(txData)) } inputMap := make(map[string]interface{}) diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 6ea72f567..5d18c1807 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -2,7 +2,6 @@ package seth import ( "context" - "errors" "fmt" "math" "math/big" @@ -44,7 +43,9 @@ var ( // according to selected strategy. func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy string) (float64, error) { if m.HeaderCache == nil { - return 0, fmt.Errorf("header cache is nil") + return 0, fmt.Errorf("header cache is not initialized. " + + "This is an internal error that shouldn't happen. " + + "If you see this, please open a GitHub issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues with your configuration details") } var getHeaderData = func(bn *big.Int) (*types.Header, error) { if bn == nil { @@ -128,7 +129,18 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy minBlockCount := int(float64(blocksNumber) * 0.8) if len(headers) < minBlockCount { - return 0, fmt.Errorf("%s. Wanted at least %d, got %d", BlockFetchingErr, minBlockCount, len(headers)) + return 0, fmt.Errorf("failed to fetch sufficient block headers for gas estimation. "+ + "Needed at least %d blocks, but only got %d (%.1f%% success rate).\n"+ + "This usually indicates:\n"+ + " 1. RPC node is experiencing high latency or load\n"+ + " 2. Network connectivity issues\n"+ + " 3. RPC rate limiting\n"+ + "Solutions:\n"+ + " 1. Retry the transaction (temporary RPC issue)\n"+ + " 2. Use a different RPC endpoint\n"+ + " 3. Disable gas estimation: set gas_price_estimation_enabled = false\n"+ + " 4. Reduce gas_price_estimation_blocks to fetch fewer blocks", + minBlockCount, len(headers), float64(len(headers))/float64(blocksNumber)*100) } switch strategy { @@ -137,7 +149,10 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy case CongestionStrategy_NewestFirst: return calculateNewestFirstNetworkCongestionMetric(headers), nil default: - return 0, fmt.Errorf("unknown congestion strategy: %s", strategy) + return 0, fmt.Errorf("unknown network congestion strategy '%s'. "+ + "Valid strategies are: 'simple' (equal weight) or 'newest_first' (recent blocks weighted more).\n"+ + "This is likely a configuration error. Check your gas estimation settings", + strategy) } } @@ -202,7 +217,12 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( } // defensive programming if baseFee == nil || currentGasTip == nil { - err = errors.New(ZeroGasSuggestedErr) + err = fmt.Errorf("RPC node returned nil gas price or zero gas tip. " + + "This indicates the node's gas estimation is not working properly.\n" + + "Solutions:\n" + + " 1. Use a different RPC endpoint\n" + + " 2. Disable gas estimation: set gas_price_estimation_enabled = false in config\n" + + " 3. Set explicit gas values: gas_price, gas_fee_cap, and gas_tip_cap (in your config (seth.toml or ClientBuilder)") return } @@ -606,7 +626,10 @@ func getAdjustmentFactor(priority string) (float64, error) { case Priority_Slow: return 0.8, nil default: - return 0, fmt.Errorf("unsupported priority: %s", priority) + return 0, fmt.Errorf("unsupported transaction priority '%s'. "+ + "Valid priorities: 'fast', 'standard', 'slow', 'auto'. "+ + "Set 'gas_price_estimation_tx_priority' in your config (seth.toml or ClientBuilder)", + priority) } } @@ -621,7 +644,10 @@ func getCongestionFactor(congestionClassification string) (float64, error) { case Congestion_VeryHigh: return 1.40, nil default: - return 0, fmt.Errorf("unsupported congestion classification: %s", congestionClassification) + return 0, fmt.Errorf("unsupported congestion classification '%s'. "+ + "Valid classifications: 'low', 'medium', 'high', 'extreme'. "+ + "This is likely an internal error. Please open a GitHub issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues", + congestionClassification) } } @@ -652,7 +678,10 @@ func (m *Client) HistoricalFeeData(ctx context.Context, priority string) (baseFe case Priority_Slow: percentileTip = 25 default: - err = fmt.Errorf("unknown priority: %s", priority) + err = fmt.Errorf("unsupported transaction priority '%s'. "+ + "Valid priorities: 'fast', 'standard', 'slow'. "+ + "Set 'gas_price_estimation_tx_priority' in your config (seth.toml or ClientBuilder)", + priority) L.Debug(). Str("Priority", priority). Msgf("Unknown priority: %s", err.Error()) @@ -682,7 +711,10 @@ func (m *Client) HistoricalFeeData(ctx context.Context, priority string) (baseFe case Priority_Slow: baseFee = stats.BaseFeePerc.Perc25 default: - err = fmt.Errorf("unsupported priority: %s", priority) + err = fmt.Errorf("unsupported transaction priority '%s'. "+ + "Valid priorities: 'fast', 'standard', 'slow'. "+ + "Set 'gas_price_estimation_tx_priority' in your config (seth.toml or ClientBuilder)", + priority) L.Debug(). Str("Priority", priority). Msgf("Unsupported priority: %s", err.Error()) diff --git a/seth/keyfile.go b/seth/keyfile.go index 9c8414732..09e1fade7 100644 --- a/seth/keyfile.go +++ b/seth/keyfile.go @@ -3,7 +3,7 @@ package seth import ( "context" "crypto/ecdsa" - "errors" + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" @@ -22,7 +22,11 @@ func NewAddress() (string, string, error) { publicKey := privateKey.Public() publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) if !ok { - return "", "", errors.New("error casting public key to ECDSA") + return "", "", fmt.Errorf("failed to cast generated public key to ECDSA type.\n"+ + "This is an internal error in the crypto.GenerateKey() function.\n"+ + "Expected type: *ecdsa.PublicKey, got: %T\n"+ + "Please report this issue: https://github.com/smartcontractkit/chainlink-testing-framework/issues", + publicKey) } address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() L.Info(). @@ -49,7 +53,13 @@ func ReturnFunds(c *Client, toAddr string) error { } if len(c.Addresses) == 1 { - return errors.New("No addresses to return funds from. Have you passed correct key file?") + return fmt.Errorf("no ephemeral addresses found to return funds from.\n"+ + "Current addresses count: %d (only root key present)\n"+ + "This indicates either:\n"+ + " 1. Key file doesn't contain ephemeral addresses\n"+ + " 2. Wrong key file was loaded\n"+ + " 3. Ephemeral keys were never created (set 'ephemeral_addresses_number' > 0 in config)", + len(c.Addresses)) } eg, egCtx := errgroup.WithContext(ctx) diff --git a/seth/retry.go b/seth/retry.go index 6468bc06c..a1dfedd99 100644 --- a/seth/retry.go +++ b/seth/retry.go @@ -2,7 +2,6 @@ package seth import ( "context" - "errors" "fmt" "math/big" "strings" @@ -42,7 +41,13 @@ func (m *Client) RetryTxAndDecode(f func() (*types.Transaction, error)) (*Decode ) if err != nil { - return &DecodedTransaction{}, errors.New(ErrRetryTimeout) + return &DecodedTransaction{}, fmt.Errorf("transaction retry timed out after multiple attempts. " + + "The RPC connection was repeatedly refused.\n" + + "Troubleshooting:\n" + + " 1. Check if the RPC node is online and accessible\n" + + " 2. Verify network connectivity\n" + + " 3. Try a different RPC endpoint\n" + + " 4. Check if there are firewall/proxy issues") } dt, err := m.Decode(tx, nil) @@ -113,7 +118,12 @@ var prepareReplacementTransaction = func(client *Client, tx *types.Transaction) // If original transaction used auto priority, we cannot bump it if client.Cfg.Network.GasPriceEstimationTxPriority == Priority_Auto { - return nil, errors.New("gas bumping is not supported for auto priority transactions") + return nil, fmt.Errorf("gas bumping is not supported when using 'auto' priority. " + + "The 'auto' mode lets the RPC node set gas prices automatically, " + + "which conflicts with manual gas bumping.\n" + + "Solutions:\n" + + " 1. Set gas_price_estimation_tx_priority to 'fast', 'standard', or 'slow'\n" + + " 2. Set gas_bump.retries = 0 to disable gas bumping") } ctxPending, cancelPending := context.WithTimeout(context.Background(), client.Cfg.Network.TxnTimeout.Duration()) @@ -125,7 +135,8 @@ var prepareReplacementTransaction = func(client *Client, tx *types.Transaction) if err != nil && !isPending { L.Debug().Str("Tx hash", tx.Hash().Hex()).Msg("Transaction was confirmed before bumping gas") - return nil, errors.New("transaction was confirmed before bumping gas") + return nil, fmt.Errorf("transaction was already confirmed before gas bumping attempt. " + + "This means the original transaction was mined successfully. No action needed") } signer := types.LatestSignerForChainID(tx.ChainId()) @@ -213,7 +224,9 @@ var prepareReplacementTransaction = func(client *Client, tx *types.Transaction) replacementTx, err = types.SignNewTx(privateKey, signer, txData) case types.BlobTxType: if tx.To() == nil { - return nil, fmt.Errorf("blob tx with nil recipient is not supported") + return nil, fmt.Errorf("blob transaction with nil recipient is not supported for gas bumping. " + + "Blob transactions (EIP-4844) with nil recipients are not standard and cannot be replaced.\n" + + "This is likely a bug - blob transactions should always have a recipient address") } newGasFeeCap := client.Cfg.GasBump.StrategyFn(tx.GasFeeCap()) newGasTipCap := client.Cfg.GasBump.StrategyFn(tx.GasTipCap()) @@ -275,7 +288,14 @@ var prepareReplacementTransaction = func(client *Client, tx *types.Transaction) replacementTx, err = types.SignNewTx(privateKey, signer, txData) default: - return nil, fmt.Errorf("unsupported tx type %d", tx.Type()) + return nil, fmt.Errorf("unsupported transaction type %d for gas bumping. "+ + "Seth currently supports gas bumping for:\n"+ + " - Type 0: Legacy transactions\n"+ + " - Type 1: EIP-2930 (access list) transactions\n"+ + " - Type 2: EIP-1559 (dynamic fee) transactions\n"+ + " - Type 3: Blob transactions (with recipient)\n"+ + "If you need support for this transaction type, please open a GitHub issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues", + tx.Type()) } if err != nil { diff --git a/seth/tracing.go b/seth/tracing.go index 6f746a064..5f56fdced 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -2,7 +2,6 @@ package seth import ( "context" - "errors" "fmt" "strconv" "strings" @@ -128,10 +127,12 @@ type Call struct { func NewTracer(cs *ContractStore, abiFinder *ABIFinder, cfg *Config, contractAddressToNameMap ContractMap, addresses []common.Address) (*Tracer, error) { if cfg == nil { - return nil, errors.New("seth config is nil") + return nil, fmt.Errorf("seth configuration is nil. Cannot create tracer without valid configuration.\n" + + "Ensure you're calling NewClient() or NewClientWithConfig() with a valid config") } if cfg.Network == nil { - return nil, errors.New("no Network is set in the config") + return nil, fmt.Errorf("network configuration is not set. Cannot create tracer without network details.\n" + + "Ensure your config has a valid [network] section or use ClientBuilder.WithNetwork()") } ctx, cancel := context.WithTimeout(context.Background(), cfg.Network.DialTimeout.Duration()) @@ -188,7 +189,15 @@ func (t *Tracer) TraceGethTX(txHash string) ([]*DecodedCall, error) { func (t *Tracer) PrintTXTrace(txHash string) error { trace := t.getTrace(txHash) if trace == nil { - return errors.New(ErrNoTrace) + return fmt.Errorf("no trace data available for this transaction. " + + "This usually means:\n" + + " 1. RPC node doesn't support debug_traceTransaction\n" + + " 2. Transaction hasn't been mined yet\n" + + " 3. Trace data was cleaned up (old transaction)\n" + + "Solutions:\n" + + " 1. Use an archive node or node with tracing enabled\n" + + " 2. Wait for transaction to be mined\n" + + " 3. Set tracing_level = 'NONE' to disable tracing") } l := L.With().Str("Transaction", txHash).Logger() l.Trace().Interface("4Byte", trace.FourByte).Msg("Calls function signatures (names)") @@ -257,11 +266,14 @@ func (t *Tracer) DecodeTrace(l zerolog.Logger, trace Trace) ([]*DecodedCall, err var getSignature = func(input string) (string, error) { if len(input) < 10 { - err := errors.New(ErrInvalidMethodSignature) + err := fmt.Errorf("invalid method signature detected in trace. "+ + "Expected 4-byte hex signature (0x12345678), but got invalid format: '%s'.\n"+ + "This is likely an internal error. Please open a GitHub issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues with transaction details", + input) l.Err(err). Str("Input", input). Send() - return "", errors.New(ErrInvalidMethodSignature) + return "", err } return input[2:10], nil @@ -317,7 +329,13 @@ func (t *Tracer) DecodeTrace(l zerolog.Logger, trace Trace) ([]*DecodedCall, err for _, call := range calls { methodCounter++ if methodCounter >= len(methods) { - return errors.New("method counter exceeds the number of methods. This indicates there's a logical error in tracing. Please reach out to Test Tooling team") + return fmt.Errorf("internal error: method counter (%d) exceeds available methods (%d). "+ + "This is a bug in the tracing logic.\n"+ + "Please open a GitHub issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues with:\n"+ + " 1. Transaction hash: %s\n"+ + " 2. Your configuration file\n"+ + " 3. Network name and RPC endpoint", + methodCounter, len(methods), trace.TxHash) } methodHex := methods[methodCounter] @@ -510,96 +528,102 @@ func (t *Tracer) checkForMissingCalls(trace Trace) []*DecodedCall { actual := countAllTracedCallsFn(trace.CallTrace.Calls, 1) // +1 for the main call diff := expected - actual - if diff != 0 { - L.Debug(). - Int("Debugged calls", actual). - Int("4byte signatures", len(trace.FourByte)). - Msgf("Number of calls and signatures does not match. There were %d more call that were't debugged", diff) - - unknownCall := &DecodedCall{ - CommonData: CommonData{Method: NO_DATA, - Input: map[string]interface{}{"warning": NO_DATA}, - Output: map[string]interface{}{"warning": NO_DATA}, - }, - FromAddress: UNKNOWN, - ToAddress: UNKNOWN, - Events: []DecodedCommonLog{ - {Signature: NO_DATA, EventData: map[string]interface{}{"warning": NO_DATA}}, - }, - } + if diff == 0 { + return []*DecodedCall{} + } - var missingSignatures []string - var findSignatureFn func(fourByteSign string, calls []Call) bool - findSignatureFn = func(fourByteSign string, calls []Call) bool { - for _, c := range calls { - if strings.Contains(c.Input, fourByteSign) { - return true - } + if diff > 0 { + L.Debug(). + Int("Traced calls", actual). + Int("Expected calls (from 4byte signatures)", expected). + Msgf("Call trace mismatch: %d call(s) have signatures but weren't found in trace (possibly optimized out or internal)", diff) + } else { + L.Debug(). + Int("Traced calls", actual). + Int("Expected calls (from 4byte signatures)", expected). + Msgf("Call trace mismatch: %d more call(s) in trace than signatures found (possibly DELEGATECALL or internal calls)", -diff) + } + + unknownCall := &DecodedCall{ + CommonData: CommonData{Method: NO_DATA, + Input: map[string]interface{}{"warning": NO_DATA}, + Output: map[string]interface{}{"warning": NO_DATA}, + }, + FromAddress: UNKNOWN, + ToAddress: UNKNOWN, + Events: []DecodedCommonLog{ + {Signature: NO_DATA, EventData: map[string]interface{}{"warning": NO_DATA}}, + }, + } + + var missingSignatures []string + var findSignatureFn func(fourByteSign string, calls []Call) bool + findSignatureFn = func(fourByteSign string, calls []Call) bool { + for _, c := range calls { + if strings.Contains(c.Input, fourByteSign) { + return true + } - if findSignatureFn(fourByteSign, c.Calls) { - return true - } + if findSignatureFn(fourByteSign, c.Calls) { + return true } + } - return false + return false + } + for k := range trace.FourByte { + if strings.Contains(trace.CallTrace.Input, k) { + continue } - for k := range trace.FourByte { - if strings.Contains(trace.CallTrace.Input, k) { - continue - } - found := findSignatureFn(k, trace.CallTrace.Calls) + found := findSignatureFn(k, trace.CallTrace.Calls) - if !found { - missingSignatures = append(missingSignatures, k) - } + if !found { + missingSignatures = append(missingSignatures, k) } + } - missedCalls := make([]*DecodedCall, 0, len(missingSignatures)) - - for _, missingSig := range missingSignatures { - byteSignature := common.Hex2Bytes(strings.TrimPrefix(missingSig, "0x")) + missedCalls := make([]*DecodedCall, 0, len(missingSignatures)) - abiResult, err := t.ABIFinder.FindABIByMethod(UNKNOWN, byteSignature) - if err != nil { - L.Info(). - Str("Signature", missingSig). - Msg("Method not found in any ABI instance. Unable to provide any more tracing information") + for _, missingSig := range missingSignatures { + byteSignature := common.Hex2Bytes(strings.TrimPrefix(missingSig, "0x")) - missedCalls = append(missedCalls, unknownCall) - continue - } + abiResult, err := t.ABIFinder.FindABIByMethod(UNKNOWN, byteSignature) + if err != nil { + L.Info(). + Str("Signature", missingSig). + Msg("Method not found in any ABI instance. Unable to provide any more tracing information") - toAddress := t.ContractAddressToNameMap.GetContractAddress(abiResult.ContractName()) - comment := WrnMissingCallTrace - if abiResult.DuplicateCount > 0 { - comment = fmt.Sprintf("%s; Potentially inaccurate - method present in %d other contracts", comment, abiResult.DuplicateCount) - } + missedCalls = append(missedCalls, unknownCall) + continue + } - missedCalls = append(missedCalls, &DecodedCall{ - CommonData: CommonData{ - Signature: missingSig, - Method: abiResult.Method.Name, - Input: map[string]interface{}{"warning": NO_DATA}, - Output: map[string]interface{}{"warning": NO_DATA}, - }, - FromAddress: UNKNOWN, - ToAddress: toAddress, - To: abiResult.ContractName(), - From: UNKNOWN, - Comment: comment, - Events: []DecodedCommonLog{ - {Signature: NO_DATA, EventData: map[string]interface{}{"warning": NO_DATA}}, - }, - }) + toAddress := t.ContractAddressToNameMap.GetContractAddress(abiResult.ContractName()) + comment := WrnMissingCallTrace + if abiResult.DuplicateCount > 0 { + comment = fmt.Sprintf("%s; Potentially inaccurate - method present in %d other contracts", comment, abiResult.DuplicateCount) } - return missedCalls + missedCalls = append(missedCalls, &DecodedCall{ + CommonData: CommonData{ + Signature: missingSig, + Method: abiResult.Method.Name, + Input: map[string]interface{}{"warning": NO_DATA}, + Output: map[string]interface{}{"warning": NO_DATA}, + }, + FromAddress: UNKNOWN, + ToAddress: toAddress, + To: abiResult.ContractName(), + From: UNKNOWN, + Comment: comment, + Events: []DecodedCommonLog{ + {Signature: NO_DATA, EventData: map[string]interface{}{"warning": NO_DATA}}, + }, + }) } - return []*DecodedCall{} + return missedCalls } - func (t *Tracer) SaveDecodedCallsAsJson(dirname string) error { for txHash, calls := range t.GetAllDecodedCalls() { _, err := saveAsJson(calls, dirname, txHash) @@ -684,7 +708,14 @@ func (t *Tracer) printDecodedCallData(l zerolog.Logger, calls []*DecodedCall, re l.Debug().Str(fmt.Sprintf("%s- Method signature", indentation), dc.Signature).Send() l.Debug().Str(fmt.Sprintf("%s- Method name", indentation), dc.Method).Send() l.Debug().Str(fmt.Sprintf("%s- Gas used/limit", indentation), fmt.Sprintf("%d/%d", dc.GasUsed, dc.GasLimit)).Send() - l.Debug().Str(fmt.Sprintf("%s- Gas left", indentation), fmt.Sprintf("%d", dc.GasLimit-dc.GasUsed)).Send() + + gasLeft := int64(dc.GasLimit) - int64(dc.GasUsed) + if gasLeft < 0 { + l.Debug().Str(fmt.Sprintf("%s- Gas left", indentation), fmt.Sprintf("%d (negative due to gas refunds or stipends)", gasLeft)).Send() + } else { + l.Debug().Str(fmt.Sprintf("%s- Gas left", indentation), fmt.Sprintf("%d", gasLeft)).Send() + } + if dc.Comment != "" { l.Debug().Str(fmt.Sprintf("%s- Comment", indentation), dc.Comment).Send() } diff --git a/seth/util.go b/seth/util.go index c624cb6a2..f0c0fc4da 100644 --- a/seth/util.go +++ b/seth/util.go @@ -83,7 +83,18 @@ func (m *Client) CalculateSubKeyFunding(addrs, gasPrice, rooKeyBuffer int64) (*F Msg("Root key balance") if freeBalance.Cmp(big.NewInt(0)) < 0 { - return nil, fmt.Errorf(ErrInsufficientRootKeyBalance, freeBalance.String()) + return nil, fmt.Errorf("insufficient root key balance.\n"+ + "Current balance: %s wei (%s ETH)\n"+ + "Required for operation: %s wei (%s ETH)\n"+ + "Deficit: %s wei (%s ETH)\n"+ + "Solutions:\n"+ + " 1. Fund the root key address with more ETH\n"+ + " 2. Reduce number of ephemeral keys (current: %d)\n"+ + " 3. Lower root_key_funds_buffer in config (current: %d ETH)", + balance.String(), WeiToEther(balance).Text('f', 6), + new(big.Int).Add(totalFee, rootKeyBuffer).String(), WeiToEther(new(big.Int).Add(totalFee, rootKeyBuffer)).Text('f', 6), + new(big.Int).Abs(freeBalance).String(), WeiToEther(new(big.Int).Abs(freeBalance)).Text('f', 6), + addrs, rooKeyBuffer) } addrFunding := new(big.Int).Div(freeBalance, big.NewInt(addrs)) @@ -96,7 +107,21 @@ func (m *Client) CalculateSubKeyFunding(addrs, gasPrice, rooKeyBuffer int64) (*F Msg("Using hardcoded ephemeral funding") if freeBalance.Cmp(requiredBalance) < 0 { - return nil, fmt.Errorf(ErrInsufficientRootKeyBalance, freeBalance.String()) + return nil, fmt.Errorf("insufficient root key balance for funding %d ephemeral keys.\n"+ + "Available balance: %s wei (%s ETH)\n"+ + "Required balance: %s wei (%s ETH)\n"+ + "Per-key funding: %s wei (%s ETH)\n"+ + "Solutions:\n"+ + " 1. Fund the root key with at least %s ETH\n"+ + " 2. Reduce ephemeral_addresses_number to %d or fewer in config\n"+ + " 3. Reduce root_key_funds_buffer (currently reserves %d ETH)", + addrs, + freeBalance.String(), WeiToEther(freeBalance).Text('f', 6), + requiredBalance.String(), WeiToEther(requiredBalance).Text('f', 6), + addrFunding.String(), WeiToEther(addrFunding).Text('f', 6), + WeiToEther(requiredBalance).Text('f', 6), + new(big.Int).Div(freeBalance, addrFunding).Int64(), + rooKeyBuffer) } bd := &FundingDetails{ @@ -161,7 +186,13 @@ type Duration struct{ D time.Duration } func MakeDuration(d time.Duration) (Duration, error) { if d < time.Duration(0) { - return Duration{}, fmt.Errorf("cannot make negative time duration: %s", d) + return Duration{}, fmt.Errorf("invalid negative duration: %s\n"+ + "Duration values must be non-negative.\n"+ + "Check your configuration values for:\n"+ + " - pending_transaction_timeout\n"+ + " - transaction_timeout\n"+ + " - Any other time.Duration config fields", + d) } return Duration{D: d}, nil } From 3192f350ffd8106a56f8472dc106e3ece19aa885 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Wed, 22 Oct 2025 20:00:16 +0200 Subject: [PATCH 4/9] add changeset --- seth/.changeset/v1.51.4.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 seth/.changeset/v1.51.4.md diff --git a/seth/.changeset/v1.51.4.md b/seth/.changeset/v1.51.4.md new file mode 100644 index 000000000..180ef5c5f --- /dev/null +++ b/seth/.changeset/v1.51.4.md @@ -0,0 +1 @@ +- Reviewed error messages, made them easier to understand and more actionable. Removed "github.com/pkg/error" usage. \ No newline at end of file From 90fa726a723bb01371f89e13a3277edc933edec6 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Thu, 23 Oct 2025 17:20:25 +0200 Subject: [PATCH 5/9] fix tests and one error message --- seth/.tool-versions | 1 + seth/client.go | 20 +------------------- seth/client_builder_test.go | 2 +- seth/client_contract_map_test.go | 4 ++-- seth/client_decode_test.go | 4 ++-- seth/client_test.go | 2 +- seth/client_trace_test.go | 16 ++++++++-------- seth/config_test.go | 14 +++++++------- seth/contract_store_test.go | 18 +++++++++--------- seth/decode.go | 8 +------- seth/tracing.go | 2 +- 11 files changed, 34 insertions(+), 57 deletions(-) create mode 100644 seth/.tool-versions diff --git a/seth/.tool-versions b/seth/.tool-versions new file mode 100644 index 000000000..5f9bce3ec --- /dev/null +++ b/seth/.tool-versions @@ -0,0 +1 @@ +golangci-lint 1.64.5 \ No newline at end of file diff --git a/seth/client.go b/seth/client.go index d04ab7afb..2facf7c23 100644 --- a/seth/client.go +++ b/seth/client.go @@ -28,25 +28,7 @@ import ( const ( ErrEmptyConfigPath = "toml config path is empty, set SETH_CONFIG_PATH" - ErrCreateABIStore = "failed to create ABI store" - ErrReadingKeys = "failed to read keys" - ErrCreateNonceManager = "failed to create nonce manager" - ErrCreateTracer = "failed to create tracer" - ErrReadContractMap = "failed to read deployed contract map" - ErrRpcHealthCheckFailed = "RPC health check failed ¯\\_(ツ)_/¯" ErrContractDeploymentFailed = "contract deployment failed" - ErrNoPksEphemeralMode = "no private keys loaded, cannot fund ephemeral addresses" - // unused by Seth, but used by upstream - ErrNoKeyLoaded = "failed to load private key" - - ErrSethConfigIsNil = "seth config is nil" - ErrNetworkIsNil = "no Network is set in the Seth config" - ErrNonceManagerConfigIsNil = "nonce manager config is nil" - ErrReadOnlyWithPrivateKeys = "read-only mode is enabled, but you tried to load private keys" - ErrReadOnlyEphemeralKeys = "ephemeral mode is not supported in read-only mode" - ErrReadOnlyGasBumping = "gas bumping is not supported in read-only mode" - ErrReadOnlyRpcHealth = "RPC health check is not supported in read-only mode" - ErrReadOnlyPendingNonce = "pending nonce protection is not supported in read-only mode" ContractMapFilePattern = "deployed_contracts_%s_%s.toml" RevertedTransactionsFilePattern = "reverted_transactions_%s_%s.json" @@ -89,7 +71,7 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { initDefaultLogging() if cfg == nil { - return nil, fmt.Errorf("Seth configuration is nil. " + + return nil, fmt.Errorf("seth configuration is nil. " + "Ensure you're calling NewClientWithConfig() with a valid config, or use NewClient() to load from SETH_CONFIG_PATH environment variable. " + "See documentation for configuration examples") } diff --git a/seth/client_builder_test.go b/seth/client_builder_test.go index 81d267128..4560e0aa5 100644 --- a/seth/client_builder_test.go +++ b/seth/client_builder_test.go @@ -339,7 +339,7 @@ func TestConfig_NoPrivateKeys_TxOpts(t *testing.T) { _ = client.NewTXOpts() require.Equal(t, 1, len(client.Errors), "expected 1 error") - require.Equal(t, "no private keys were loaded, but keyNum 0 was requested", client.Errors[0].Error(), "expected error message") + require.Contains(t, client.Errors[0].Error(), "no private keys loaded, but tried to use key #0.", "expected error message") } func TestConfig_NoPrivateKeys_Tracing(t *testing.T) { diff --git a/seth/client_contract_map_test.go b/seth/client_contract_map_test.go index 25f0311a9..dbc297774 100644 --- a/seth/client_contract_map_test.go +++ b/seth/client_contract_map_test.go @@ -142,7 +142,7 @@ func TestContractMapNewClientIsNotCreatedWhenCorruptedContractMapFileExists(t *t cfg.ContractMapFile = file.Name() newClient, err := seth.NewClientRaw(cfg, addresses, pks) require.Error(t, err, "succeeded in creation of new client") - require.Contains(t, err.Error(), seth.ErrReadContractMap, "expected error reading invalid toml") + require.Contains(t, err.Error(), "failed to load deployed contracts map from", "expected error reading invalid toml") require.Nil(t, newClient, "expected new client to be nil") } @@ -162,6 +162,6 @@ func TestContractMapNewClientIsNotCreatedWhenCorruptedContractMapFileExists_Inva cfg.ContractMapFile = file.Name() newClient, err := seth.NewClientRaw(cfg, addresses, pks) require.Error(t, err, "succeeded in creation of new client") - require.Contains(t, err.Error(), seth.ErrReadContractMap, "expected error reading invalid contract address") + require.Contains(t, err.Error(), "failed to load deployed contracts map from", "expected error reading invalid contract address") require.Nil(t, newClient, "expected new client to be nil") } diff --git a/seth/client_decode_test.go b/seth/client_decode_test.go index d55b287d5..7f740f5c0 100644 --- a/seth/client_decode_test.go +++ b/seth/client_decode_test.go @@ -39,7 +39,7 @@ func TestSmokeDebugReverts(t *testing.T) { name: "revert with a custom err", method: "alwaysRevertsCustomError", output: map[string]string{ - seth.GETH: "error type: CustomErr, error values: [12 21]", + seth.GETH: "transaction reverted with custom error: error type: CustomErr, error values: [12 21]", }, }, } @@ -57,7 +57,7 @@ func TestSmokeDebugReverts(t *testing.T) { expectedOutput = eo } } - require.Equal(t, expectedOutput, err.Error()) + require.Contains(t, err.Error(), expectedOutput, "expected error message to contain the reverted error type and values") }) } } diff --git a/seth/client_test.go b/seth/client_test.go index 37893c594..77af168b4 100644 --- a/seth/client_test.go +++ b/seth/client_test.go @@ -40,7 +40,7 @@ func TestRPCHealthCheckEnabled_Node_Unhealthy(t *testing.T) { _, err = seth.NewClientWithConfig(cfg) require.Error(t, err, "expected error when connecting to unhealthy node") - require.Contains(t, err.Error(), seth.ErrRpcHealthCheckFailed, "expected error message when connecting to dead node") + require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: insufficient funds for gas * price + value:", "expected error message when connecting to dead node") } func TestRPCHealthCheckDisabled_Node_Unhealthy(t *testing.T) { diff --git a/seth/client_trace_test.go b/seth/client_trace_test.go index fbb44d6e8..de122ee2c 100644 --- a/seth/client_trace_test.go +++ b/seth/client_trace_test.go @@ -1229,7 +1229,7 @@ func TestTraceContractAll(t *testing.T) { require.NoError(t, txErr, "transaction sending should not fail") _, decodeErr := c.Decode(revertedTx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") okTx, txErr := TestEnv.DebugContract.AddCounter(c.NewTXOpts(), big.NewInt(1), big.NewInt(2)) require.NoError(t, txErr, "transaction should not have reverted") @@ -1282,7 +1282,7 @@ func TestTraceContractOnlyReverted(t *testing.T) { require.NoError(t, txErr, "transaction sending should not fail") _, decodeErr := c.Decode(revertedTx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") okTx, txErr := TestEnv.DebugContract.AddCounter(c.NewTXOpts(), big.NewInt(1), big.NewInt(2)) require.NoError(t, txErr, "transaction should not have reverted") @@ -1320,7 +1320,7 @@ func TestTraceContractNone(t *testing.T) { require.NoError(t, txErr, "transaction sending should not fail") _, decodeErr := c.Decode(revertedTx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") okTx, txErr := TestEnv.DebugContract.AddCounter(c.NewTXOpts(), big.NewInt(1), big.NewInt(2)) require.NoError(t, txErr, "transaction should not have reverted") @@ -1341,7 +1341,7 @@ func TestTraceContractRevertedErrNoValues(t *testing.T) { require.NoError(t, txErr, "transaction should have reverted") _, decodeErr := c.Decode(tx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErrNoValues, error values: []", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErrNoValues, error values: []", decodeErr.Error(), "expected error message to contain the reverted error type and values") require.Equal(t, 1, len(c.Tracer.GetAllDecodedCalls()), "expected 1 decoded transacton") expectedCall := &seth.DecodedCall{ @@ -1373,7 +1373,7 @@ func TestTraceCallRevertFunctionInTheContract(t *testing.T) { require.NoError(t, txErr, "transaction should have reverted") _, decodeErr := c.Decode(tx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErr, error values: [12 21]", decodeErr.Error(), "expected error message to contain the reverted error type and values") require.Equal(t, 1, len(c.Tracer.GetAllDecodedCalls()), "expected 1 decoded transacton") expectedCall := &seth.DecodedCall{ @@ -1407,7 +1407,7 @@ func TestTraceCallRevertFunctionInSubContract(t *testing.T) { require.NoError(t, txErr, "transaction should have reverted") _, decodeErr := c.Decode(tx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErr, error values: [1001 2]", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErr, error values: [1001 2]", decodeErr.Error(), "expected error message to contain the reverted error type and values") require.Equal(t, 1, len(c.Tracer.GetAllDecodedCalls()), "expected 1 decoded transaction") expectedCall := &seth.DecodedCall{ @@ -1446,7 +1446,7 @@ func TestTraceCallRevertInCallback(t *testing.T) { require.NoError(t, txErr, "transaction should have reverted") _, decodeErr := c.Decode(tx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErr, error values: [99 101]", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErr, error values: [99 101]", decodeErr.Error(), "expected error message to contain the reverted error type and values") } func TestTraceOldPragmaNoRevertReason(t *testing.T) { @@ -1493,7 +1493,7 @@ func TestTraceeRevertReasonNonRootSender(t *testing.T) { require.NoError(t, txErr, "transaction should have reverted") _, decodeErr := c.Decode(tx, txErr) require.Error(t, decodeErr, "transaction should have reverted") - require.Equal(t, "error type: CustomErr, error values: [1001 2]", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.Equal(t, "transaction reverted with custom error: error type: CustomErr, error values: [1001 2]", decodeErr.Error(), "expected error message to contain the reverted error type and values") require.Equal(t, 1, len(c.Tracer.GetAllDecodedCalls()), "expected 1 decoded transaction") expectedCall := &seth.DecodedCall{ diff --git a/seth/config_test.go b/seth/config_test.go index 6d550ba2a..095cf86ed 100644 --- a/seth/config_test.go +++ b/seth/config_test.go @@ -131,7 +131,7 @@ func TestConfig_ReadOnly_WithPk(t *testing.T) { _, err := seth.NewClientRaw(&cfg, addrs, nil) require.Error(t, err, "succeeded in creating client") - require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message") + require.Contains(t, err.Error(), "configuration conflict: read-only mode is enabled, but private keys were provided.", "expected different error message") privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") require.NoError(t, err, "failed to parse private key") @@ -139,11 +139,11 @@ func TestConfig_ReadOnly_WithPk(t *testing.T) { pks := []*ecdsa.PrivateKey{privateKey} _, err = seth.NewClientRaw(&cfg, nil, pks) require.Error(t, err, "succeeded in creating client") - require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message") + require.Contains(t, err.Error(), "configuration conflict: read-only mode is enabled, but private keys were provided.", "expected different error message") _, err = seth.NewClientRaw(&cfg, addrs, pks) require.Error(t, err, "succeeded in creating client") - require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message") + require.Contains(t, err.Error(), "configuration conflict: read-only mode is enabled, but private keys were provided.", "expected different error message") } func TestConfig_ReadOnly_GasBumping(t *testing.T) { @@ -161,7 +161,7 @@ func TestConfig_ReadOnly_GasBumping(t *testing.T) { _, err := seth.NewClientRaw(&cfg, nil, nil) require.Error(t, err, "succeeded in creating client") - require.Equal(t, seth.ErrReadOnlyGasBumping, err.Error(), "expected different error message") + require.Contains(t, err.Error(), "gas bumping is not supported in read-only mode because it requires sending replacement transactions.", "expected different error message") } func TestConfig_ReadOnly_RpcHealth(t *testing.T) { @@ -177,7 +177,7 @@ func TestConfig_ReadOnly_RpcHealth(t *testing.T) { _, err := seth.NewClientRaw(&cfg, nil, nil) require.Error(t, err, "succeeded in creating client") - require.Equal(t, seth.ErrReadOnlyRpcHealth, err.Error(), "expected different error message") + require.Contains(t, err.Error(), "RPC health check is not supported in read-only mode", "expected different error message") } func TestConfig_ReadOnly_PendingNonce(t *testing.T) { @@ -193,7 +193,7 @@ func TestConfig_ReadOnly_PendingNonce(t *testing.T) { _, err := seth.NewClientRaw(&cfg, nil, nil) require.Error(t, err, "succeeded in creating client") - require.Equal(t, seth.ErrReadOnlyPendingNonce, err.Error(), "expected different error message") + require.Contains(t, err.Error(), "pending nonce protection is not supported in read-only mode because it requires transaction monitoring.", "expected different error message") } func TestConfig_ReadOnly_EphemeralKeys(t *testing.T) { @@ -210,5 +210,5 @@ func TestConfig_ReadOnly_EphemeralKeys(t *testing.T) { _, err := seth.NewClientRaw(&cfg, nil, nil) require.Error(t, err, "succeeded in creating client") - require.Equal(t, seth.ErrNoPksEphemeralMode, err.Error(), "expected different error message") + require.Contains(t, err.Error(), "ephemeral mode requires exactly one root private key to fund ephemeral addresses, but no keys were loaded.", "expected different error message") } diff --git a/seth/contract_store_test.go b/seth/contract_store_test.go index c63dce054..071c15396 100644 --- a/seth/contract_store_test.go +++ b/seth/contract_store_test.go @@ -63,18 +63,18 @@ func TestSmokeContractABIStore(t *testing.T) { { name: "empty ABI dir", abiPath: "./contracts/emptyContractDir", - err: "no ABI files found in './contracts/emptyContractDir'. Fix the path or comment out 'abi_dir' setting", + err: "no ABI files (*.abi) found in directory './contracts/emptyContractDir'.", }, { name: "empty gethwrappers dir", gethWrappersPaths: []string{"./contracts/emptyContractDir"}, - err: "failed to load geth wrappers from [./contracts/emptyContractDir]: no geth wrappers found in '[./contracts/emptyContractDir]'. Fix the path or comment out 'geth_wrappers_dirs' setting", + err: "failed to load geth wrappers from [./contracts/emptyContractDir]: no geth wrapper files found in directories: [./contracts/emptyContractDir]", }, { name: "empty ABI and gethwrappers dir", abiPath: "./contracts/emptyContractDir", gethWrappersPaths: []string{"./contracts/emptyContractDir"}, - err: "no ABI files found in './contracts/emptyContractDir'. Fix the path or comment out 'abi_dir' setting", + err: "no ABI files (*.abi) found in directory './contracts/emptyContractDir'.", }, { name: "no MetaData in one of gethwrappers", @@ -84,7 +84,7 @@ func TestSmokeContractABIStore(t *testing.T) { { name: "empty MetaData in one of gethwrappers", gethWrappersPaths: []string{"./contracts/emptyMetaDataContractDir"}, - err: "failed to load geth wrappers from [./contracts/emptyMetaDataContractDir]: failed to parse ABI content: EOF", + err: "failed to load geth wrappers from [./contracts/emptyMetaDataContractDir]: failed to parse ABI content from 'contracts/emptyMetaDataContractDir/NetworkDebugContract_Broken.go': EOF", }, { name: "gethwrappers dir mixes regular go files and gethwrappers", @@ -94,12 +94,12 @@ func TestSmokeContractABIStore(t *testing.T) { { name: "invalid ABI inside ABI dir", abiPath: "./contracts/invalidContractDir", - err: "failed to parse ABI file: invalid character ':' after array element", + err: "failed to parse ABI file 'NetworkDebugContract.abi': invalid character ':' after array element", }, { name: "invalid ABI in gethwrappers inside dir", gethWrappersPaths: []string{"./contracts/invalidContractDir"}, - err: "failed to load geth wrappers from [./contracts/invalidContractDir]: failed to parse ABI content: invalid character 'i' looking for beginning of value", + err: "failed to load geth wrappers from [./contracts/invalidContractDir]: failed to parse ABI content from 'contracts/invalidContractDir/NetworkDebugContract_Broken.go': invalid character 'i' looking for beginning of value", }, } @@ -114,7 +114,7 @@ func TestSmokeContractABIStore(t *testing.T) { require.Equal(t, make(map[string][]uint8), cs.BINs) err = errors.New("") } - require.Equal(t, tc.err, err.Error()) + require.Contains(t, err.Error(), tc.err, "error should match") }) } } @@ -152,7 +152,7 @@ func TestSmokeContractBINStore(t *testing.T) { name: "empty BIN dir", abiPath: "./contracts/abi", binPath: "./contracts/emptyContractDir", - err: "no BIN files found in './contracts/emptyContractDir'. Fix the path or comment out 'bin_dir' setting", + err: "no BIN files (*.bin) found in directory './contracts/emptyContractDir'.", }, } @@ -171,7 +171,7 @@ func TestSmokeContractBINStore(t *testing.T) { } else { require.Nil(t, cs, "ContractStore should be nil") } - require.Equal(t, tc.err, err.Error(), "error should match") + require.Contains(t, err.Error(), tc.err, "error should match") }) } } diff --git a/seth/decode.go b/seth/decode.go index de32a2f75..7be4c38b0 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -617,13 +617,7 @@ func (m *Client) callAndGetRevertReason(tx *types.Transaction, rc *types.Receipt return err } if decodedABIErrString != "" { - return fmt.Errorf("failed to decode ABI from contract code: %s\n"+ - "This happens when:\n"+ - " 1. Contract bytecode doesn't contain valid ABI metadata\n"+ - " 2. Contract wasn't compiled with metadata (--metadata-hash none)\n"+ - " 3. Contract code at the address is not a valid contract\n"+ - "For third-party contracts, manually load the ABI instead of relying on auto-detection", - decodedABIErrString) + return fmt.Errorf("transaction reverted with custom error: %s", decodedABIErrString) } if plainStringErr != nil { diff --git a/seth/tracing.go b/seth/tracing.go index 5f56fdced..10744577b 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -709,7 +709,7 @@ func (t *Tracer) printDecodedCallData(l zerolog.Logger, calls []*DecodedCall, re l.Debug().Str(fmt.Sprintf("%s- Method name", indentation), dc.Method).Send() l.Debug().Str(fmt.Sprintf("%s- Gas used/limit", indentation), fmt.Sprintf("%d/%d", dc.GasUsed, dc.GasLimit)).Send() - gasLeft := int64(dc.GasLimit) - int64(dc.GasUsed) + gasLeft := mustSafeInt64(dc.GasLimit) - mustSafeInt64(dc.GasUsed) if gasLeft < 0 { l.Debug().Str(fmt.Sprintf("%s- Gas left", indentation), fmt.Sprintf("%d (negative due to gas refunds or stipends)", gasLeft)).Send() } else { From 78a59f15ced369d90fb2cea38022aa8b2a5fa99b Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 12:15:38 +0200 Subject: [PATCH 6/9] review error messages once more --- seth/abi_finder.go | 24 ++----------- seth/client.go | 80 ++++++++++++++---------------------------- seth/config.go | 14 ++++---- seth/contract_store.go | 2 +- seth/decode.go | 5 ++- seth/gas_adjuster.go | 4 +-- seth/nonce.go | 2 +- 7 files changed, 43 insertions(+), 88 deletions(-) diff --git a/seth/abi_finder.go b/seth/abi_finder.go index af0bfa961..249e88fbc 100644 --- a/seth/abi_finder.go +++ b/seth/abi_finder.go @@ -137,28 +137,8 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder } if result.Method == nil { - abiCount := len(a.ContractStore.ABIs) - abiSample := "" - if abiCount > 0 { - // Show first few ABIs as examples (max 5) - sampleSize := min(abiCount, 5) - samples := make([]string, 0, sampleSize) - count := 0 - for abiName := range a.ContractStore.ABIs { - if count >= sampleSize { - break - } - samples = append(samples, strings.TrimSuffix(abiName, ".abi")) - count++ - } - abiSample = fmt.Sprintf("\nExample ABIs loaded: %s", strings.Join(samples, ", ")) - if abiCount > sampleSize { - abiSample += fmt.Sprintf(" (and %d more)", abiCount-sampleSize) - } - } - return ABIFinderResult{}, fmt.Errorf("no ABI found with method signature %s for contract at address %s.\n"+ - "Checked %d ABIs but none matched.%s\n"+ + "Checked %d ABIs but none matched.\n"+ "Possible causes:\n"+ " 1. Contract ABI not loaded (check abi_dir and contract_map_file)\n"+ " 2. Method signature doesn't match any function in loaded ABIs\n"+ @@ -170,7 +150,7 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder " 3. Ensure ABI file exists in the directory specified by 'abi_dir'\n"+ " 4. Review contract_map_file for address-to-name mappings\n"+ " 5. Use ContractStore.AddABI() to manually add the ABI", - stringSignature, address, abiCount, abiSample) + stringSignature, address, len(a.ContractStore.ABIs)) } return result, nil diff --git a/seth/client.go b/seth/client.go index 2facf7c23..1ea4ad3c8 100644 --- a/seth/client.go +++ b/seth/client.go @@ -30,6 +30,9 @@ const ( ErrEmptyConfigPath = "toml config path is empty, set SETH_CONFIG_PATH" ErrContractDeploymentFailed = "contract deployment failed" + // unused by Seth, but used by upstream + ErrNoKeyLoaded = "failed to load private key" + ContractMapFilePattern = "deployed_contracts_%s_%s.toml" RevertedTransactionsFilePattern = "reverted_transactions_%s_%s.json" ) @@ -148,7 +151,13 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { tr, err := NewTracer(cs, &abiFinder, cfg, contractAddressToNameMap, addrs) if err != nil { return nil, fmt.Errorf("failed to create transaction tracer: %w\n"+ - "This is usually caused by RPC connection issues. Verify your RPC endpoint is accessible", + "Possible causes:\n"+ + " 1. RPC endpoint is not accessible\n"+ + " 2. RPC node doesn't support debug_traceTransaction API\n"+ + "Solutions:\n"+ + " 1. Verify RPC endpoint URL is correct and accessible\n"+ + " 2. Use an RPC node with debug API enabled (archive node recommended)\n"+ + " 3. Set tracing_level = 'NONE' in config to disable tracing", err) } opts = append(opts, WithTracer(tr)) @@ -181,7 +190,7 @@ func NewClientRaw( opts ...ClientOpt, ) (*Client, error) { if cfg == nil { - return nil, fmt.Errorf("Seth configuration is nil. " + + return nil, fmt.Errorf("seth configuration is nil. " + "Provide a valid Config when calling NewClientRaw(). " + "Consider using NewClient() or NewClientWithConfig() instead") } @@ -393,7 +402,13 @@ func NewClientRaw( tr, err := NewTracer(c.ContractStore, c.ABIFinder, cfg, c.ContractAddressToNameMap, addrs) if err != nil { return nil, fmt.Errorf("failed to create transaction tracer: %w\n"+ - "Ensure RPC endpoint supports debug_traceTransaction or set tracing_level = 'NONE'", + "Possible causes:\n"+ + " 1. RPC endpoint is not accessible\n"+ + " 2. RPC node doesn't support debug_traceTransaction API\n"+ + "Solutions:\n"+ + " 1. Verify RPC endpoint URL is correct and accessible\n"+ + " 2. Use an RPC node with debug API enabled (archive node recommended)\n"+ + " 3. Set tracing_level = 'NONE' in config to disable tracing", err) } @@ -1280,7 +1295,10 @@ func (m *Client) DeployContractFromContractStore(auth *bind.TransactOpts, name s "This usually means:\n" + " 1. Seth client wasn't properly initialized\n" + " 2. ABI directory path is incorrect in config\n" + - "Ensure 'abi_dir' and 'bin_dir' are set in seth.toml or use DeployContract() with explicit ABI/bytecode") + "Solutions:\n" + + " 1. Set 'abi_dir' and 'bin_dir' in seth.toml config file\n" + + " 2. Use ClientBuilder: builder.WithABIDir(path).WithBINDir(path)\n" + + " 3. Use DeployContract() method with explicit ABI/bytecode instead") } name = strings.TrimSuffix(name, ".abi") @@ -1288,62 +1306,18 @@ func (m *Client) DeployContractFromContractStore(auth *bind.TransactOpts, name s contractAbi, ok := m.ContractStore.ABIs[name+".abi"] if !ok { - abiCount := len(m.ContractStore.ABIs) - abiSample := "" - if abiCount > 0 { - // Show first few ABIs as examples (max 5) - sampleSize := 5 - if abiCount < sampleSize { - sampleSize = abiCount - } - samples := make([]string, 0, sampleSize) - count := 0 - for abiName := range m.ContractStore.ABIs { - if count >= sampleSize { - break - } - samples = append(samples, strings.TrimSuffix(abiName, ".abi")) - count++ - } - abiSample = fmt.Sprintf("\nExample ABIs available: %s", strings.Join(samples, ", ")) - if abiCount > sampleSize { - abiSample += fmt.Sprintf(" (and %d more)", abiCount-sampleSize) - } - } return DeploymentData{}, fmt.Errorf("ABI for contract '%s' not found in contract store.\n"+ - "Total ABIs loaded: %d%s\n"+ + "Total ABIs loaded: %d\n"+ "Ensure the ABI file '%s.abi' exists in the directory specified by 'abi_dir' in your config", - name, abiCount, abiSample, name) + name, len(m.ContractStore.ABIs), name) } bytecode, ok := m.ContractStore.BINs[name+".bin"] if !ok { - binCount := len(m.ContractStore.BINs) - binSample := "" - if binCount > 0 { - // Show first few BINs as examples (max 5) - sampleSize := 5 - if binCount < sampleSize { - sampleSize = binCount - } - samples := make([]string, 0, sampleSize) - count := 0 - for binName := range m.ContractStore.BINs { - if count >= sampleSize { - break - } - samples = append(samples, strings.TrimSuffix(binName, ".bin")) - count++ - } - binSample = fmt.Sprintf("\nExample BINs available: %s", strings.Join(samples, ", ")) - if binCount > sampleSize { - binSample += fmt.Sprintf(" (and %d more)", binCount-sampleSize) - } - } return DeploymentData{}, fmt.Errorf("bytecode (BIN) for contract '%s' not found in contract store.\n"+ - "Total BINs loaded: %d%s\n"+ + "Total BINs loaded: %d\n"+ "Ensure the BIN file '%s.bin' exists in the directory specified by 'bin_dir' in your config", - name, binCount, binSample, name) + name, len(m.ContractStore.BINs), name) } data, err := m.DeployContract(auth, name, contractAbi, bytecode, params...) @@ -1587,7 +1561,7 @@ func (m *Client) validateAddressesKeyNum(keyNum int) error { if len(m.Addresses) == 0 { return fmt.Errorf("no addresses loaded, but tried to use key #%d.\n"+ "This should not happen if private keys were loaded correctly. "+ - "Please report this as a bug", + "Please report this issue at https://github.com/smartcontractkit/chainlink-testing-framework/issues with the stack trace", keyNum) } return fmt.Errorf("keyNum %d is out of range. Available addresses: 0-%d (total: %d addresses loaded).\n"+ diff --git a/seth/config.go b/seth/config.go index 3980ea01f..2859368ab 100644 --- a/seth/config.go +++ b/seth/config.go @@ -139,9 +139,8 @@ func ReadConfig() (*Config, error) { if err != nil { return nil, fmt.Errorf("failed to read Seth config file at '%s': %w\n"+ "Ensure:\n"+ - " 1. The file exists at the specified path\n"+ - " 2. You have read permissions for the file\n"+ - " 3. The path is correct (set via SETH_CONFIG_PATH or parameter)", + " 1. The file exists at the specified path (set via SETH_CONFIG_PATH)\n"+ + " 2. You have read permissions for the file", cfgPath, err) } err = toml.Unmarshal(d, &cfg) @@ -214,9 +213,8 @@ func ReadConfig() (*Config, error) { return nil, fmt.Errorf("no root private key was set. "+ "You can provide the root private key via:\n"+ " 1. %s environment variable (without 0x prefix)\n"+ - " 2. 'root_private_key' field in seth.toml\n"+ - " 3. WithPrivateKeys() when using ClientBuilder\n"+ - "WARNING: Never commit private keys to source control. Use environment variables or secure secret management", + " 2. 'private_keys_secret' array in seth.toml [[networks]] section\n"+ + "WARNING: Never commit private keys to source control. We recommend to use the environment variable.", ROOT_PRIVATE_KEY_ENV_VAR) } cfg.Network.PrivateKeys = append(cfg.Network.PrivateKeys, rootPrivateKey) @@ -298,7 +296,7 @@ func (c *Config) Validate() error { case TracingLevel_All: default: return fmt.Errorf("invalid tracing level '%s'. Must be one of: 'NONE', 'REVERTED', 'ALL'. "+ - "Set 'tracing_level' in your seth.toml config.\n"+ + "Set 'tracing_level' in your seth.toml config or via WithTracing() option, if using ClientBuilder().\n"+ "Recommended: 'REVERTED' for debugging failed transactions, 'NONE' to disable tracing completely", c.TracingLevel) } @@ -310,7 +308,7 @@ func (c *Config) Validate() error { case TraceOutput_DOT: default: return fmt.Errorf("invalid trace output '%s'. Must be one of: 'console', 'json', 'dot'. "+ - "Set 'trace_outputs' in your seth.toml config. "+ + "Set 'trace_outputs' in your seth.toml config or via WithTracing() option, if using ClientBuilder()"+ "You can specify multiple outputs as an array", output) } diff --git a/seth/contract_store.go b/seth/contract_store.go index 3e5197fe9..0b7f4f5d5 100644 --- a/seth/contract_store.go +++ b/seth/contract_store.go @@ -243,7 +243,7 @@ func (c *ContractStore) loadGethWrappers(gethWrappersPaths []string) error { " 2. Generate wrappers using abigen:\n"+ " abigen --abi contract.abi --bin contract.bin --pkg wrappers --out contract_wrapper.go\n"+ " 3. Ensure wrapper files contain ABI metadata (check for 'ABI' variable)\n"+ - " 4. If not using geth wrappers, remove 'geth_wrappers_dirs' from config", + " 4. If not using geth wrappers, remove 'geth_wrappers_dirs' from config (seth.toml or ClientBuilder)", gethWrappersPaths) } diff --git a/seth/decode.go b/seth/decode.go index 7be4c38b0..8e2303e43 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -440,7 +440,10 @@ func (m *Client) decodeTransaction(l zerolog.Logger, tx *types.Transaction, rece txInput, err = decodeTxInputs(l, txData, abiResult.Method) if err != nil { return defaultTxn, fmt.Errorf("failed to decode transaction input for method '%s': %w\n"+ - "The transaction data doesn't match the expected ABI method signature", + "This could be due to:\n"+ + " 1. Transaction data doesn't match the ABI method signature\n"+ + " 2. Incorrect ABI for this contract\n"+ + " 3. Malformed transaction data", abiResult.Method.Name, err) } diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 5d18c1807..363e1faae 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -679,7 +679,7 @@ func (m *Client) HistoricalFeeData(ctx context.Context, priority string) (baseFe percentileTip = 25 default: err = fmt.Errorf("unsupported transaction priority '%s'. "+ - "Valid priorities: 'fast', 'standard', 'slow'. "+ + "Valid priorities: 'degen' (internal), 'fast', 'standard', 'slow'. "+ "Set 'gas_price_estimation_tx_priority' in your config (seth.toml or ClientBuilder)", priority) L.Debug(). @@ -712,7 +712,7 @@ func (m *Client) HistoricalFeeData(ctx context.Context, priority string) (baseFe baseFee = stats.BaseFeePerc.Perc25 default: err = fmt.Errorf("unsupported transaction priority '%s'. "+ - "Valid priorities: 'fast', 'standard', 'slow'. "+ + "Valid priorities: 'degen' (internal), 'fast', 'standard', 'slow'. "+ "Set 'gas_price_estimation_tx_priority' in your config (seth.toml or ClientBuilder)", priority) L.Debug(). diff --git a/seth/nonce.go b/seth/nonce.go index 99ff8ed74..f8d54288e 100644 --- a/seth/nonce.go +++ b/seth/nonce.go @@ -65,7 +65,7 @@ func validateNonceManagerConfig(nonceManagerCfg *NonceManagerCfg) error { // NewNonceManager creates a new nonce manager that tracks nonce for each address func NewNonceManager(cfg *Config, addrs []common.Address, privKeys []*ecdsa.PrivateKey) (*NonceManager, error) { if cfg == nil { - return nil, fmt.Errorf("Seth configuration is nil. Cannot create nonce manager without valid configuration.\n" + + return nil, fmt.Errorf("seth configuration is nil. Cannot create nonce manager without valid configuration.\n" + "This usually means you're trying to create a nonce manager before initializing Seth.\n" + "Solutions:\n" + " 1. Use NewClient() or NewClientWithConfig() to create a Seth client first\n" + From b372e5df11792d08387b7a542a768473bbb1c9c1 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 12:16:20 +0200 Subject: [PATCH 7/9] fix test --- seth/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seth/client_test.go b/seth/client_test.go index 77af168b4..8425e4bb3 100644 --- a/seth/client_test.go +++ b/seth/client_test.go @@ -40,7 +40,7 @@ func TestRPCHealthCheckEnabled_Node_Unhealthy(t *testing.T) { _, err = seth.NewClientWithConfig(cfg) require.Error(t, err, "expected error when connecting to unhealthy node") - require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: insufficient funds for gas * price + value:", "expected error message when connecting to dead node") + require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: insufficient funds for gas * price + value", "expected error message when connecting to dead node") } func TestRPCHealthCheckDisabled_Node_Unhealthy(t *testing.T) { From 020196f02af7e2bf9f940bd1256577ee11121ac3 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 16:01:44 +0200 Subject: [PATCH 8/9] fix test --- seth/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seth/client_test.go b/seth/client_test.go index 8425e4bb3..dcf35ce9d 100644 --- a/seth/client_test.go +++ b/seth/client_test.go @@ -40,7 +40,7 @@ func TestRPCHealthCheckEnabled_Node_Unhealthy(t *testing.T) { _, err = seth.NewClientWithConfig(cfg) require.Error(t, err, "expected error when connecting to unhealthy node") - require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: insufficient funds for gas * price + value", "expected error message when connecting to dead node") + require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: insufficient funds for gas", "expected error message when connecting to dead node") } func TestRPCHealthCheckDisabled_Node_Unhealthy(t *testing.T) { From cfd9917c04c72017d69f85e589e72bc7079832e2 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 16:47:41 +0200 Subject: [PATCH 9/9] fix test again --- seth/client_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/seth/client_test.go b/seth/client_test.go index dcf35ce9d..fead8d5ae 100644 --- a/seth/client_test.go +++ b/seth/client_test.go @@ -1,6 +1,7 @@ package seth_test import ( + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -40,7 +41,9 @@ func TestRPCHealthCheckEnabled_Node_Unhealthy(t *testing.T) { _, err = seth.NewClientWithConfig(cfg) require.Error(t, err, "expected error when connecting to unhealthy node") - require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: insufficient funds for gas", "expected error message when connecting to dead node") + // Geth returns "insufficient funds for gas" error + // Anvil returns "insufficient funds for gas" error + require.Contains(t, strings.ToLower(err.Error()), strings.ToLower("RPC health check failed: failed to send transaction to network: Insufficient funds for gas"), "expected error message when connecting to dead node") } func TestRPCHealthCheckDisabled_Node_Unhealthy(t *testing.T) {