diff --git a/lib/utils/seth/seth.go b/lib/utils/seth/seth.go index 64d153d6a..ab8a29830 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/.changeset/v1.51.4.md b/seth/.changeset/v1.51.4.md index 59e5bde49..40c57da3f 100644 --- a/seth/.changeset/v1.51.4.md +++ b/seth/.changeset/v1.51.4.md @@ -1 +1 @@ -- Set From field for bind.CallOpts optionally, warn that some view/pure funcs may work incorrectly without sender \ No newline at end of file +- Set From field for bind.CallOpts optionally, warn that some view/pure funcs may work incorrectly without sender diff --git a/seth/.changeset/v1.51.6.md b/seth/.changeset/v1.51.6.md new file mode 100644 index 000000000..4de028e07 --- /dev/null +++ b/seth/.changeset/v1.51.6.md @@ -0,0 +1 @@ +- Reviewed error messages, made them easier to understand and more actionable. Removed "github.com/pkg/errors" usage. Added `RevertedErr` sentinel for detecting on-chain transaction reverts with `errors.Is`. \ No newline at end of file diff --git a/seth/.golangci.yml b/seth/.golangci.yml deleted file mode 100644 index ac2b2be3d..000000000 --- a/seth/.golangci.yml +++ /dev/null @@ -1,81 +0,0 @@ -run: - concurrency: 4 - timeout: 5m -linters: - enable: - # defaults - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - typecheck - - unused - # extra - - exhaustive - - copyloopvar - - revive - - goimports - - gosec - - misspell - - errorlint - - contextcheck -linters-settings: - exhaustive: - default-signifies-exhaustive: true - goimports: - local-prefixes: github.com/smartcontractkit/chainlink-testing-framework/seth - gosec: - exclude-generated: true - errorlint: - # Allow formatting of errors without %w - errorf: false - revive: - confidence: 0.8 - rules: - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: error-return - - name: error-strings - - name: error-naming - - name: if-return - - name: increment-decrement -# - name: var-naming - - name: var-declaration - - name: package-comments - - name: range - - name: receiver-naming - - name: time-naming - - name: unexported-return - - name: indent-error-flow - - name: errorf - - name: exported - - name: empty-block - - name: superfluous-else - - name: unused-parameter - - name: unreachable-code - - name: redefines-builtin-id - - name: waitgroup-by-value - - name: unconditional-recursion - - name: struct-tag - - name: string-format - - name: string-of-int - - name: range-val-address - - name: range-val-in-closure - - name: modifies-value-receiver - - name: modifies-parameter - - name: identical-branches - - name: get-return - - name: early-return - - name: defer - - name: constant-logical-expr - - name: confusing-naming - - name: bool-literal-in-expr - - name: atomic -issues: - exclude-dirs: - - contracts/* - - examples/* - - examples_wasp/* diff --git a/seth/.tool-versions b/seth/.tool-versions new file mode 100644 index 000000000..cc36d6d3e --- /dev/null +++ b/seth/.tool-versions @@ -0,0 +1 @@ +golangci-lint 2.6.2 \ No newline at end of file diff --git a/seth/abi_finder.go b/seth/abi_finder.go index b7316c442..e24bed144 100644 --- a/seth/abi_finder.go +++ b/seth/abi_finder.go @@ -1,11 +1,11 @@ package seth import ( + "fmt" "strings" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" ) type ABIFinder struct { @@ -46,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: %w", + contractName, address, contractName, ErrNoABIFound) 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 } @@ -83,13 +93,20 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder } } - L.Err(err). + findErr := fmt.Errorf("method signature %s not found in ABI for contract '%s' at address %s, even though the address is registered in the contract map: %w\n"+ + "This usually means the contract map points to the wrong ABI.\n"+ + "Troubleshooting:\n"+ + " 1. Verify '%s.abi' matches the deployed contract at %s\n"+ + " 2. Re-deploy with DeployContract() or update the contract map\n"+ + " 3. If multiple contracts share method signatures, Seth may have mapped the wrong ABI", + stringSignature, contractName, address, err, contractName, address) + L.Err(findErr). Str("Signature", stringSignature). Str("Supposed contract", contractName). Str("Supposed address", address). - Msg("Method not found in known ABI instance. This should not happen. Contract map might be corrupted") + Msg("Method not found in known ABI instance") - return ABIFinderResult{}, err + return ABIFinderResult{}, findErr } result.Method = methodCandidate @@ -127,7 +144,20 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder } if result.Method == nil { - return ABIFinderResult{}, errors.New(ErrNoABIMethod) + return ABIFinderResult{}, fmt.Errorf("no ABI found with method signature %s for contract at address %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"+ + " 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: %w", + stringSignature, address, len(a.ContractStore.ABIs), ErrNoABIMethod) } return result, nil diff --git a/seth/block_stats.go b/seth/block_stats.go index db883331c..153ce7bc5 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,7 +69,10 @@ 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("invalid block range for statistics: start block %d > end block %d.\n"+ + "Ensure the start block is less than or equal to the end block.\n"+ + "If using relative block numbers in block_stats config, verify the values are correct for your network", + startBlock.Int64(), endBlock.Int64()) } L.Info(). Int64("EndBlock", endBlock.Int64()). @@ -107,7 +117,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 no analyze") + 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/client.go b/seth/client.go index 1dfd43ac3..219900005 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" ) @@ -29,27 +29,9 @@ import ( const ( WrnEmptyFromInCallOpts = "you are running Seth without keys, key %d was not found. In case remote blockchain node is used (msg.sender) will be empty. Private functions which check msg.sender may require the correct key to be used" - 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" ) @@ -91,7 +73,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 @@ -102,7 +86,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 @@ -119,11 +109,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 == "" { @@ -137,7 +132,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, set save_deployed_contracts_map = false in seth.toml before creating the client", + cfg.ContractMapFile, err) } } else { L.Debug().Msg("Simulated network, contract map won't be read from file") @@ -152,7 +149,15 @@ 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"+ + "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)) } @@ -184,13 +189,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 @@ -198,7 +209,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 { @@ -215,7 +227,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() @@ -245,7 +263,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) @@ -258,7 +279,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, set save_deployed_contracts_map = false in seth.toml before creating the client", + cfg.ContractMapFile, err) } if len(c.ContractAddressToNameMap.addressMap) > 0 { L.Info(). @@ -290,7 +313,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") @@ -302,7 +328,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() @@ -317,10 +346,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() @@ -354,7 +388,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 } @@ -364,7 +400,15 @@ 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"+ + "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) } c.Tracer = tr @@ -391,7 +435,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) @@ -422,7 +469,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 <---------------- !!!!! ----------------") @@ -443,7 +498,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 @@ -469,14 +526,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(). @@ -832,7 +898,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 @@ -894,7 +966,7 @@ func (m *Client) CalculateGasEstimations(request GasEstimationRequest) GasEstima defer cancel() disableEstimationsIfNeeded := func(err error) { - if strings.Contains(err.Error(), ZeroGasSuggestedErr) { + if errors.Is(err, ErrZeroGasSuggested) { L.Warn().Msg("Received incorrect gas estimations. Disabling them and reverting to hardcoded values. Remember to update your config!") m.Cfg.Network.GasPriceEstimationEnabled = false } @@ -950,7 +1022,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 } @@ -1019,13 +1098,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") @@ -1049,7 +1134,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") @@ -1072,7 +1160,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()) } } @@ -1108,7 +1204,7 @@ func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.AB }), ); err != nil { // pass this specific error, so that Decode knows that it's not the actual revert reason - _, _ = m.Decode(tx, errors.New(ErrContractDeploymentFailed)) + _, _ = m.Decode(tx, errors.New("contract deployment failed")) return DeploymentData{}, wrapErrInMessageWithASuggestion(m.rewriteDeploymentError(err)) } @@ -1172,7 +1268,14 @@ 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" + + "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") @@ -1180,12 +1283,18 @@ 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") + return DeploymentData{}, fmt.Errorf("ABI for contract '%s' not found in contract store.\n"+ + "Total ABIs loaded: %d\n"+ + "Ensure the ABI file '%s.abi' exists in the directory specified by 'abi_dir' in your config", + name, len(m.ContractStore.ABIs), name) } bytecode, ok := m.ContractStore.BINs[name+".bin"] if !ok { - return DeploymentData{}, errors.New("BIN not found") + return DeploymentData{}, fmt.Errorf("bytecode (BIN) for contract '%s' not found in contract store.\n"+ + "Total BINs loaded: %d\n"+ + "Ensure the BIN file '%s.bin' exists in the directory specified by 'bin_dir' in your config", + name, len(m.ContractStore.BINs), name) } data, err := m.DeployContract(auth, name, contractAbi, bytecode, params...) @@ -1376,7 +1485,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 @@ -1399,9 +1515,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 @@ -1410,9 +1536,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"+ + "Addresses are derived from private keys during client initialization.\n"+ + "Verify private keys were loaded via SETH_ROOT_PRIVATE_KEY, private_keys_secret in seth.toml, or WithPrivateKeys()", + 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/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..689776adb 100644 --- a/seth/client_builder.go +++ b/seth/client_builder.go @@ -1,20 +1,21 @@ package seth import ( + "errors" "fmt" "time" "github.com/ethereum/go-ethereum/ethclient/simulated" - "github.com/pkg/errors" ) -const ( - NoNetworkErr = "you need to set the Network" - NoPkForRpcHealthCheckErr = "you need to provide at least one private key to check the RPC health" - NoPkForNonceProtection = "you need to provide at least one private key to enable nonce protection" - NoPkForEphemeralKeys = "you need to provide at least one private key to generate and fund ephemeral addresses" - NoPkForGasPriceEstimation = "you need to provide at least one private key to enable gas price estimations" - EthClientAndUrlsSet = "you cannot set both EthClient and RPC URLs" +var ( + ErrNoPkForRpcHealthCheck = errors.New("you need to provide at least one private key to check the RPC health") + ErrNoPkForNonceProtection = errors.New("you need to provide at least one private key to enable nonce protection") + ErrNoPkForEphemeralKeys = errors.New("you need to provide at least one private key to generate and fund ephemeral addresses") + ErrNoPkForGasPriceEstimation = errors.New("you need to provide at least one private key to enable gas price estimations") + ErrEthClientAndUrlsSet = errors.New("you cannot set both EthClient and RPC URLs") + ErrNetworkNotSet = errors.New("you need to set the Network") + ErrNetworkNotSetButRequired = errors.New("at least one method that required network to be set was called, but network is nil") ) type ClientBuilder struct { @@ -394,11 +395,8 @@ func (c *ClientBuilder) BuildConfig() (*Config, error) { c.handleReadOnlyMode() c.validateConfig() if len(c.errors) > 0 { - var concatenatedErrors string - for _, err := range c.errors { - concatenatedErrors = fmt.Sprintf("%s\n%s", concatenatedErrors, err.Error()) - } - return nil, fmt.Errorf("errors occurred during building the config:%s", concatenatedErrors) + return nil, fmt.Errorf("errors occurred during building the config: %w", errors.Join(c.errors...)) + } return c.config, nil } @@ -422,30 +420,30 @@ func (c *ClientBuilder) handleReadOnlyMode() { func (c *ClientBuilder) validateConfig() { if c.config.Network == nil { - c.errors = append(c.errors, errors.New(NoNetworkErr)) + c.errors = append(c.errors, ErrNetworkNotSet) return } if len(c.config.Network.PrivateKeys) == 0 && c.config.CheckRpcHealthOnStart { - c.errors = append(c.errors, errors.New(NoPkForRpcHealthCheckErr)) + c.errors = append(c.errors, ErrNoPkForRpcHealthCheck) } if len(c.config.Network.PrivateKeys) == 0 && c.config.PendingNonceProtectionEnabled { - c.errors = append(c.errors, errors.New(NoPkForNonceProtection)) + c.errors = append(c.errors, ErrNoPkForNonceProtection) } if len(c.config.Network.PrivateKeys) == 0 && c.config.EphemeralAddrs != nil && *c.config.EphemeralAddrs > 0 { - c.errors = append(c.errors, errors.New(NoPkForEphemeralKeys)) + c.errors = append(c.errors, ErrNoPkForEphemeralKeys) } if len(c.config.Network.PrivateKeys) == 0 && c.config.Network.GasPriceEstimationEnabled { - c.errors = append(c.errors, errors.New(NoPkForGasPriceEstimation)) + c.errors = append(c.errors, ErrNoPkForGasPriceEstimation) } if len(c.config.Network.URLs) > 0 && c.config.ethclient != nil { - c.errors = append(c.errors, errors.New(EthClientAndUrlsSet)) + c.errors = append(c.errors, ErrEthClientAndUrlsSet) } } func (c *ClientBuilder) checkIfNetworkIsSet() bool { if c.config.Network == nil { - c.errors = append(c.errors, errors.New("at least one method that required network to be set was called, but network is nil")) + c.errors = append(c.errors, ErrNetworkNotSetButRequired) return false } return true diff --git a/seth/client_builder_test.go b/seth/client_builder_test.go index feda3282b..515e8baee 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" @@ -170,8 +170,7 @@ func TestConfig_ModifyExistingConfigWithBuilder_UnknownChainId(t *testing.T) { WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}). Build() - expectedError := `errors occurred during building the config: -network with chainId '225' not found + expectedError := `errors occurred during building the config: network with chainId '225' not found at least one method that required network to be set was called, but network is nil you need to set the Network` @@ -196,8 +195,7 @@ func TestConfig_ModifyExistingConfigWithBuilder_UnknownChainId_UseDefault(t *tes WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}). Build() - expectedError := `errors occurred during building the config: -network with chainId '225' not found + expectedError := `errors occurred during building the config: network with chainId '225' not found at least one method that required network to be set was called, but network is nil at least one method that required network to be set was called, but network is nil you need to set the Network` @@ -266,7 +264,7 @@ func TestConfig_NoPrivateKeys_RpcHealthEnabled(t *testing.T) { Build() require.Error(t, err, "succeeded in building the client") - require.Contains(t, err.Error(), seth.NoPkForRpcHealthCheckErr, "expected error message") + require.ErrorIs(t, err, seth.ErrNoPkForRpcHealthCheck, "expected error is not ErrNoPkForRpcHealthCheck") } func TestConfig_NoPrivateKeys_PendingNonce(t *testing.T) { @@ -284,7 +282,7 @@ func TestConfig_NoPrivateKeys_PendingNonce(t *testing.T) { Build() require.Error(t, err, "succeeded in building the client") - require.Contains(t, err.Error(), seth.NoPkForNonceProtection, "expected error message") + require.ErrorIs(t, err, seth.ErrNoPkForNonceProtection, "expected error is not ErrNoPkForNonceProtection") } func TestConfig_NoPrivateKeys_EphemeralKeys(t *testing.T) { @@ -303,7 +301,7 @@ func TestConfig_NoPrivateKeys_EphemeralKeys(t *testing.T) { Build() require.Error(t, err, "succeeded in building the client") - require.Contains(t, err.Error(), seth.NoPkForEphemeralKeys, "expected error message") + require.ErrorIs(t, err, seth.ErrNoPkForEphemeralKeys, "expected error is not ErrNoPkForEphemeralKeys") } func TestConfig_NoPrivateKeys_GasEstimations(t *testing.T) { @@ -317,7 +315,7 @@ func TestConfig_NoPrivateKeys_GasEstimations(t *testing.T) { Build() require.Error(t, err, "succeeded in building the client") - require.Contains(t, err.Error(), seth.NoPkForGasPriceEstimation, "expected error message") + require.ErrorIs(t, err, seth.ErrNoPkForGasPriceEstimation, "expected error is not ErrNoPkForGasPriceEstimation") } func TestConfig_NoPrivateKeys_TxOpts(t *testing.T) { @@ -339,7 +337,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) { @@ -827,7 +825,7 @@ func TestConfig_EthClient_DoesntAllowRpcUrl(t *testing.T) { Build() require.Error(t, err, "failed to build client") - require.Contains(t, err.Error(), seth.EthClientAndUrlsSet, "expected error message") + require.ErrorIs(t, err, seth.ErrEthClientAndUrlsSet, "expected error is not ErrEthClientAndUrlsSet") require.Nil(t, client, "expected client to be nil") } 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..5b7f25495 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,8 @@ 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") + require.ErrorIs(t, err, seth.ErrReverted, "expected revert to be detectable via RevertedErr") }) } } 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/client_test.go b/seth/client_test.go index 37893c594..2e11e2bf1 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,8 @@ 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") + // Geth & Anvil return "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) { diff --git a/seth/client_trace_test.go b/seth/client_trace_test.go index fbb44d6e8..47ca76ace 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) { @@ -1471,7 +1471,8 @@ func TestTraceOldPragmaNoRevertReason(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, "execution reverted", decodeErr.Error(), "expected error message to contain the reverted error type and values") + require.ErrorIs(t, decodeErr, seth.ErrReverted, "expected error to be ErrReverted") + require.Contains(t, decodeErr.Error(), "execution reverted", "expected error message to contain the reverted error type and values") } func TestTraceeRevertReasonNonRootSender(t *testing.T) { @@ -1493,7 +1494,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/cmd/seth.go b/seth/cmd/seth.go index e3bbb0e6a..81ca5ac0d 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,16 +13,11 @@ 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" ) -const ( - ErrNoNetwork = "no network specified, use -n flag. Ex.: 'seth -n Geth stats' or -u and -c flags. Ex.: 'seth -u http://localhost:8545 -c 1337 stats'" -) - var C *seth.Client func RunCLI(args []string) error { @@ -38,7 +34,7 @@ func RunCLI(args []string) error { networkName := cCtx.String("networkName") url := cCtx.String("url") if networkName == "" && url == "" { - return errors.New(ErrNoNetwork) + return errors.New("no network specified, use -n flag. Ex.: 'seth -n Geth stats' or -u and -c flags. Ex.: 'seth -u http://localhost:8545 -c 1337 stats'") } if networkName != "" { _ = os.Setenv(seth.NETWORK_ENV_VAR, networkName) @@ -200,16 +196,16 @@ func RunCLI(args []string) error { cfgPath := os.Getenv(seth.CONFIG_FILE_ENV_VAR) if cfgPath == "" { - return errors.New(seth.ErrEmptyConfigPath) + return errors.New("toml config path is empty, set SETH_CONFIG_PATH") } var cfg *seth.Config d, err := os.ReadFile(cfgPath) if err != nil { - return errors.Wrap(err, seth.ErrReadSethConfig) + return fmt.Errorf("failed to read TOML config file: %w", err) } err = toml.Unmarshal(d, &cfg) if err != nil { - return errors.Wrap(err, seth.ErrUnmarshalSethConfig) + return fmt.Errorf("failed to unmarshal TOML config file: %w", err) } absPath, err := filepath.Abs(cfgPath) if err != nil { @@ -272,7 +268,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 +294,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 b2403d7d0..17e04420f 100644 --- a/seth/config.go +++ b/seth/config.go @@ -13,14 +13,9 @@ 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 ( - ErrReadSethConfig = "failed to read TOML config for seth" - ErrUnmarshalSethConfig = "failed to unmarshal TOML config for seth" - ErrEmptyRootPrivateKey = "no root private key were set, set %s=..." - GETH = "Geth" ANVIL = "Anvil" @@ -131,16 +126,29 @@ 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 (set via SETH_CONFIG_PATH)\n"+ + " 2. You have read permissions for the file", + 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 +170,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 +198,19 @@ 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. '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) if cfg.Network.DialTimeout == nil { @@ -205,7 +227,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 +253,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 +290,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 or via WithTracing() option, if using ClientBuilder().\n"+ + "Recommended: 'REVERTED' for debugging failed transactions, 'NONE' to disable tracing completely", + c.TracingLevel) } for _, output := range c.TraceOutputs { @@ -263,7 +302,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 or via WithTracing() when using ClientBuilder(). "+ + "You can specify multiple outputs as an array", + output) } } @@ -276,11 +318,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/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.go b/seth/contract_store.go index 5d8424f0e..21e53214b 100644 --- a/seth/contract_store.go +++ b/seth/contract_store.go @@ -2,6 +2,7 @@ package seth import ( "encoding/json" + "errors" "fmt" "go/ast" "go/parser" @@ -13,14 +14,10 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" ) -const ( - ErrOpenABIFile = "failed to open ABI file" - ErrParseABI = "failed to parse ABI file" - ErrOpenBINFile = "failed to open BIN file" - ErrNoABIInFile = "no ABI content found in file" +var ( + ErrNoABIInFile = errors.New("no ABI content found in file") ) // ContractStore contains all ABIs that are used in decoding. It might also contain contract bytecode for deployment @@ -111,7 +108,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 +131,32 @@ 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) + 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) } } @@ -159,14 +175,25 @@ 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) + 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) } } @@ -184,7 +211,7 @@ func (c *ContractStore) loadGethWrappers(gethWrappersPaths []string) error { if filepath.Ext(path) == ".go" { contractName, abiContent, err := extractABIFromGethWrapperDir(path) if err != nil { - if !strings.Contains(err.Error(), ErrNoABIInFile) { + if !errors.Is(err, ErrNoABIInFile) { return err } L.Debug().Msgf("ABI not found in file due to: %s. Skipping", err.Error()) @@ -207,7 +234,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 (seth.toml or ClientBuilder)", + gethWrappersPaths) } return nil @@ -247,18 +282,24 @@ TOP_LOOP: } if abiContent == "" { - return "", nil, fmt.Errorf("%s: %s", ErrNoABIInFile, filePath) + return "", nil, fmt.Errorf("%w: %s", ErrNoABIInFile, filePath) } // 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/contract_store_test.go b/seth/contract_store_test.go index f9436e971..071c15396 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" @@ -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 6daa65c7e..5bebdf46e 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,23 +17,17 @@ 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" ) const ( - ErrDecodeInput = "failed to decode transaction input" - ErrDecodeOutput = "failed to decode transaction output" - ErrDecodeLog = "failed to decode log" - ErrDecodedLogNonIndexed = "failed to decode non-indexed log data" - ErrDecodeILogIndexed = "failed to decode indexed log data" - ErrTooShortTxData = "tx data is less than 4 bytes, can't decode" - ErrRPCJSONCastError = "failed to cast CallMsg error as rpc.DataError" - ErrUnableToDecode = "unable to decode revert reason" - WarnNoContractStore = "ContractStore is nil, use seth.NewContractStore(...) to decode transactions" ) +// ErrReverted is returned when a transaction was mined but reverted on-chain. +// Use errors.Is(err, ErrReverted) to detect reverts regardless of the decoded reason string. +var ErrReverted = errors.New("transaction reverted") + // DecodedTransaction decoded transaction type DecodedTransaction struct { CommonData @@ -103,7 +97,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 { @@ -125,7 +119,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(). @@ -180,9 +174,10 @@ func (m *Client) DecodeTx(tx *types.Transaction) (*DecodedTransaction, error) { Msg("No post-decode hook found. Skipping") } - if decodeErr != nil && errors.Is(decodeErr, errors.New(ErrNoABIMethod)) { + if decodeErr != nil && (errors.Is(decodeErr, ErrNoABIMethod) || errors.Is(decodeErr, ErrNoABIFound)) { m.handleTxDecodingError(l, *decoded, decodeErr) - return decoded, revertErr + // do not return here, because we want to continue with tracing + // we might still get some useful information from the trace } if m.Cfg.TracingLevel == TracingLevel_None { @@ -416,7 +411,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 } @@ -434,7 +435,12 @@ 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"+ + "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) } var txIndex uint @@ -498,7 +504,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) @@ -537,7 +549,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 +580,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)) @@ -597,7 +616,7 @@ func (m *Client) callAndGetRevertReason(tx *types.Transaction, rc *types.Receipt return err } if decodedABIErrString != "" { - return errors.New(decodedABIErrString) + return fmt.Errorf("%w with custom error: %s", ErrReverted, decodedABIErrString) } if plainStringErr != nil { @@ -620,7 +639,7 @@ func (m *Client) callAndGetRevertReason(tx *types.Transaction, rc *types.Receipt } } - return plainStringErr + return fmt.Errorf("%w: %w", ErrReverted, plainStringErr) } return nil } @@ -629,7 +648,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{}) @@ -665,7 +690,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 +717,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 +744,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/dot_graph.go b/seth/dot_graph.go index d058c12f7..4c65f710e 100644 --- a/seth/dot_graph.go +++ b/seth/dot_graph.go @@ -18,12 +18,12 @@ import ( func findShortestPath(calls []*DecodedCall) []string { callMap := make(map[string]*DecodedCall) for _, call := range calls { - callMap[call.CommonData.Signature] = call + callMap[call.Signature] = call } var root *DecodedCall for _, call := range calls { - if call.CommonData.ParentSignature == "" { + if call.ParentSignature == "" { root = call break } @@ -35,7 +35,7 @@ func findShortestPath(calls []*DecodedCall) []string { var end *DecodedCall for i := len(calls) - 1; i >= 0; i-- { - if calls[i].CommonData.Error != "" { + if calls[i].Error != "" { end = calls[i] break } @@ -49,7 +49,7 @@ func findShortestPath(calls []*DecodedCall) []string { path []string } - queue := []node{{call: root, path: []string{root.CommonData.Signature}}} + queue := []node{{call: root, path: []string{root.Signature}}} visited := make(map[string]bool) for len(queue) > 0 { @@ -59,16 +59,16 @@ func findShortestPath(calls []*DecodedCall) []string { currentCall := currentNode.call currentPath := currentNode.path - if currentCall.CommonData.Signature == end.CommonData.Signature { + if currentCall.Signature == end.Signature { return currentPath } - visited[currentCall.CommonData.Signature] = true + visited[currentCall.Signature] = true for _, call := range calls { - if call.CommonData.ParentSignature == currentCall.CommonData.Signature && !visited[call.CommonData.Signature] { + if call.ParentSignature == currentCall.Signature && !visited[call.Signature] { newPath := append([]string{}, currentPath...) - newPath = append(newPath, call.CommonData.Signature) + newPath = append(newPath, call.Signature) queue = append(queue, node{call: call, path: newPath}) } } @@ -148,8 +148,8 @@ func (t *Tracer) generateDotGraph(txHash string, calls []*DecodedCall, revertErr to = call.ToAddress } - basicLabel := fmt.Sprintf("\"%s -> %s\n %s\"", from, to, call.CommonData.Method) - extraLabel := fmt.Sprintf("\"Inputs: %s\nOutputs: %s\"", formatMapForLabel(call.CommonData.Input, defaultTruncateTo), formatMapForLabel(call.CommonData.Output, defaultTruncateTo)) + basicLabel := fmt.Sprintf("\"%s -> %s\n %s\"", from, to, call.Method) + extraLabel := fmt.Sprintf("\"Inputs: %s\nOutputs: %s\"", formatMapForLabel(call.Input, defaultTruncateTo), formatMapForLabel(call.Output, defaultTruncateTo)) isMajorNode := false for _, path := range shortestPath { @@ -200,9 +200,9 @@ func (t *Tracer) generateDotGraph(txHash string, calls []*DecodedCall, revertErr } } - if call.CommonData.ParentSignature != "" { + if call.ParentSignature != "" { for _, parentCall := range calls { - if parentCall.CommonData.Signature == call.CommonData.ParentSignature { + if parentCall.Signature == call.ParentSignature { parentHash := hashCall(parentCall) parentID := callHashToID[parentHash] parentBasicNodeID := "node" + strconv.Itoa(parentID) + "_basic" @@ -303,7 +303,7 @@ func (t *Tracer) generateDotGraph(txHash string, calls []*DecodedCall, revertErr func formatTooltip(call *DecodedCall) string { basicTooltip := fmt.Sprintf("\"BASIC\nFrom: %s\nTo: %s\nType: %s\nGas Used/Limit: %s\nValue: %d\n\nINPUTS%s\n\nOUTPUTS%s\n\nEVENTS%s\n\"", - call.FromAddress, call.ToAddress, call.CommonData.CallType, fmt.Sprintf("%d/%d", call.GasUsed, call.GasLimit), call.Value, formatMapForTooltip(call.CommonData.Input), formatMapForTooltip(call.CommonData.Output), formatEvent(call.Events)) + call.FromAddress, call.ToAddress, call.CallType, fmt.Sprintf("%d/%d", call.GasUsed, call.GasLimit), call.Value, formatMapForTooltip(call.Input), formatMapForTooltip(call.Output), formatEvent(call.Events)) if call.Comment == "" { return basicTooltip @@ -359,7 +359,7 @@ func hashCall(call *DecodedCall) string { //we use it only to generate hash that's used to identify a node in graph, so we don't care about this function being weak //nolint h := sha1.New() - h.Write([]byte(fmt.Sprintf("%v", call))) + fmt.Fprintf(h, "%v", call) return hex.EncodeToString(h.Sum(nil)) } 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..e7d255baf 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -2,17 +2,16 @@ package seth import ( "context" + "errors" "fmt" "math" "math/big" "slices" - "strings" "sync" "time" "github.com/avast/retry-go" "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" ) const ( @@ -36,15 +35,17 @@ const ( ) var ( - ZeroGasSuggestedErr = "either base fee or suggested tip is 0" - BlockFetchingErr = "failed to fetch enough block headers for congestion calculation" + ErrZeroGasSuggested = errors.New("either base fee or suggested tip is 0") + ErrBlockFetchingFailed = errors.New("failed to fetch enough block headers for congestion calculation") ) // CalculateNetworkCongestionMetric calculates a simple congestion metric based on the last N blocks // 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: %w", + minBlockCount, len(headers), float64(len(headers))/float64(blocksNumber)*100, ErrBlockFetchingFailed) } 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,13 @@ 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): %w", + ErrZeroGasSuggested) return } @@ -300,7 +321,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( // Apply buffer also to the tip bufferedTipCapFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedTipCap), big.NewFloat(bufferAdjustment)) adjustedTipCap, _ = bufferedTipCapFloat.Int(nil) - } else if !strings.Contains(err.Error(), BlockFetchingErr) { + } else if !errors.Is(err, ErrBlockFetchingFailed) { return } else { L.Debug(). @@ -381,6 +402,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 +477,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 +524,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 @@ -532,7 +591,7 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a // Calculate and apply the buffer. bufferedGasPriceFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedGasPrice), big.NewFloat(bufferAdjustment)) adjustedGasPrice, _ = bufferedGasPriceFloat.Int(nil) - } else if !strings.Contains(err.Error(), BlockFetchingErr) { + } else if !errors.Is(err, ErrBlockFetchingFailed) { return } else { L.Debug(). @@ -568,7 +627,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) } } @@ -583,7 +645,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) } } @@ -614,7 +679,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: 'degen' (internal), '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()) @@ -644,7 +712,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: 'degen' (internal), '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/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 c1752c9d3..a5a152ccd 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/keyfile.go b/seth/keyfile.go index 0fc56fbd1..09e1fade7 100644 --- a/seth/keyfile.go +++ b/seth/keyfile.go @@ -3,12 +3,12 @@ package seth import ( "context" "crypto/ecdsa" + "fmt" "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" ) @@ -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/nonce.go b/seth/nonce.go index b7deab3b1..3a5f7d4b7 100644 --- a/seth/nonce.go +++ b/seth/nonce.go @@ -3,6 +3,8 @@ package seth import ( "context" "crypto/ecdsa" + "errors" + "fmt" "time" "math/big" @@ -10,15 +12,17 @@ import ( "github.com/avast/retry-go" "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" "go.uber.org/ratelimit" ) +var ( + ErrKeySync = errors.New("failed to sync the key") + ErrKeySyncTimeout = errors.New("key sync timeout, consider increasing key_sync_timeout in config (seth.toml or ClientBuilder), or increasing the number of keys") + ErrNonce = errors.New("failed to get nonce") +) + const ( - ErrKeySyncTimeout = "key sync timeout, consider increasing key_sync_timeout in seth.toml, or increasing the number of keys" - ErrKeySync = "failed to sync the key" - ErrNonce = "failed to get nonce" - TimeoutKeyNum = -80001 + TimeoutKeyNum = -80001 ) // NonceManager tracks nonce for each address @@ -41,13 +45,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 +69,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 +144,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: %w", + m.cfg.KeySyncTimeout.Duration(), m.cfg.KeySyncTimeout.Duration(), ErrKeySyncTimeout) + 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(). @@ -154,11 +185,25 @@ func (m *NonceManager) anySyncedKey() int { // Check both pending and latest nonce to determine if key is available pendingNonce, pendingErr := m.Client.Client.PendingNonceAt(rpcCtx, addr) if pendingErr != nil { - return errors.Wrap(pendingErr, ErrNonce) + return fmt.Errorf("failed to get pending 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, + fmt.Errorf("%w: %w", ErrNonce, pendingErr)) } latestNonce, latestErr := m.Client.Client.NonceAt(rpcCtx, addr, nil) if latestErr != nil { - return errors.Wrap(latestErr, ErrNonce) + return fmt.Errorf("failed to get latest 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, + fmt.Errorf("%w: %w", ErrNonce, latestErr)) } // Store for potential recovery use @@ -189,13 +234,17 @@ func (m *NonceManager) anySyncedKey() int { Interface("Address", addr). Msg("Key NOT synced - has pending transaction") - 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: %w", + keyData.KeyNum, m.Addresses[keyData.KeyNum].Hex(), + keyData.Nonce+1, latestNonce, ErrKeySync) }, retry.Attempts(m.cfg.KeySyncRetries), retry.Delay(m.cfg.KeySyncRetryDelay.Duration()), ) if err != nil { - m.Client.Errors = append(m.Client.Errors, errors.New(ErrKeySync)) + m.Client.Errors = append(m.Client.Errors, ErrKeySync) // NEVER leak the key - always return it to the pool var nonceToUse uint64 diff --git a/seth/retry.go b/seth/retry.go index ffbe2360d..fddc5837f 100644 --- a/seth/retry.go +++ b/seth/retry.go @@ -11,17 +11,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 */ - -const ( - ErrRPCConnectionRefused = "connection refused" -) - -const ( - ErrRetryTimeout = "retry timeout" ) // RetryTxAndDecode executes transaction several times, retries if connection is lost and decodes all the data @@ -37,17 +26,25 @@ func (m *Client) RetryTxAndDecode(f func() (*types.Transaction, error)) (*Decode }), retry.DelayType(retry.FixedDelay), retry.Attempts(10), retry.Delay(time.Duration(1)*time.Second), retry.RetryIf(func(err error) bool { - return strings.Contains(err.Error(), ErrRPCConnectionRefused) + return strings.Contains(err.Error(), "connection refused") }), ) 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) 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 @@ -111,7 +108,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()) @@ -123,7 +125,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()) @@ -211,7 +214,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()) @@ -273,7 +278,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 cc9929381..ca944bf2e 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -2,6 +2,7 @@ package seth import ( "context" + "errors" "fmt" "strconv" "strings" @@ -11,17 +12,16 @@ 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" ) +var ( + ErrNoABIMethod = errors.New("no ABI method found") + ErrNoABIFound = errors.New("no ABI found in Contract Store") +) + const ( - ErrNoTrace = "no trace found" - ErrNoABIMethod = "no ABI method found" - ErrNoAbiFound = "no ABI found in Contract Store" - ErrNoFourByteFound = "no method signatures found in tracing data" - ErrInvalidMethodSignature = "no method signature found or it's not 4 bytes long" - WrnMissingCallTrace = "This call was missing from call trace, but it's signature was present in 4bytes trace. Most data is missing; Call order remains unknown" + WrnMissingCallTrace = "This call was missing from call trace, but it's signature was present in 4bytes trace. Most data is missing; Call order remains unknown" FAILED_TO_DECODE = "failed to decode" UNKNOWN = "unknown" @@ -128,10 +128,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 +190,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)") @@ -250,18 +260,21 @@ func (t *Tracer) DecodeTrace(l zerolog.Logger, trace Trace) ([]*DecodedCall, err // we can still decode the calls without 4byte signatures if len(trace.FourByte) == 0 { - L.Debug().Msg(ErrNoFourByteFound) + L.Debug().Msg("No method signatures found in tracing data") } methods := make([]string, 0, len(trace.CallTrace.Calls)+1) 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 +330,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] @@ -384,7 +403,7 @@ func (t *Tracer) decodeCall(byteSignature []byte, rawCall Call) (*DecodedCall, e abiResult, err := t.ABIFinder.FindABIByMethod(rawCall.To, byteSignature) - defaultCall.CommonData.Signature = common.Bytes2Hex(byteSignature) + defaultCall.Signature = common.Bytes2Hex(byteSignature) defaultCall.FromAddress = rawCall.From defaultCall.ToAddress = rawCall.To defaultCall.From = t.getHumanReadableAddressName(rawCall.From) @@ -458,7 +477,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 { @@ -508,96 +529,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)) + missedCalls := make([]*DecodedCall, 0, len(missingSignatures)) - for _, missingSig := range missingSignatures { - byteSignature := common.Hex2Bytes(strings.TrimPrefix(missingSig, "0x")) + for _, missingSig := range missingSignatures { + byteSignature := common.Hex2Bytes(strings.TrimPrefix(missingSig, "0x")) - 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") - - 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) @@ -618,7 +645,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 { @@ -681,7 +709,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 := 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 { + 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 5f9e8f33d..5a7ca9072 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,16 +19,11 @@ 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" ) -const ( - ErrInsufficientRootKeyBalance = "insufficient root key balance: %s" -) - // FundingDetails funding details about shares we put into test keys type FundingDetails struct { RootBalance *big.Int @@ -83,7 +79,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 +103,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 +182,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 } @@ -236,7 +263,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) } } @@ -317,14 +344,14 @@ func CreateOrAppendToJsonArray(filePath string, newItem any) error { jsonValue := string(jsonBytes) if size == 0 { - _, err = f.WriteString(fmt.Sprintf("[%s]", jsonValue)) + _, err = fmt.Fprintf(f, "[%s]", jsonValue) } else { // Move cursor back by one character, so we can append data just before array end. _, err = f.Seek(-1, io.SeekEnd) if err != nil { return err } - _, err = f.WriteString(fmt.Sprintf(",\n%s]", jsonValue)) + _, err = fmt.Fprintf(f, ",\n%s]", jsonValue) } return err }