diff --git a/pkg/client/compatibility_helper.go b/pkg/client/compatibility_helper.go index b9be76301a..9310e0a038 100644 --- a/pkg/client/compatibility_helper.go +++ b/pkg/client/compatibility_helper.go @@ -83,7 +83,7 @@ func toFilterArg(q ethereum.FilterQuery) (interface{}, error) { if q.BlockHash != nil { arg["blockHash"] = *q.BlockHash if q.FromBlock != nil || q.ToBlock != nil { - return nil, errors.New("cannot specify both BlockHash and FromBlock/ToBlock") + return nil, errors.New("invalid filter query: cannot specify both BlockHash and FromBlock/ToBlock parameters simultaneously. Use either BlockHash for a single block or FromBlock/ToBlock for a block range") } } else { if q.FromBlock == nil { diff --git a/pkg/client/errors.go b/pkg/client/errors.go index 38fb3bff40..485a12140d 100644 --- a/pkg/client/errors.go +++ b/pkg/client/errors.go @@ -307,7 +307,7 @@ var monad = ClientErrors{ InsufficientEth: regexp.MustCompile("Signer had insufficient balance"), } -const TerminallyStuckMsg = "transaction terminally stuck" +const TerminallyStuckMsg = "transaction is terminally stuck in the mempool and cannot be included in a block: this transaction will not be retried and must be investigated manually" // Tx.Error messages that are set internally so they are not chain or client specific var internal = ClientErrors{ @@ -567,20 +567,20 @@ func ExtractRPCErrorOrNil(err error) *JsonError { // { "error": { "code": 3, "data": "0xABC123...", "message": "execution reverted: hello world" } } // revert reason automatically parsed if a simple require and included in message. func ExtractRPCError(baseErr error) (*JsonError, error) { if baseErr == nil { - return nil, pkgerrors.New("no error present") + return nil, pkgerrors.New("failed to extract RPC error: no error was provided to ExtractRPCError") } cause := pkgerrors.Cause(baseErr) jsonBytes, err := json.Marshal(cause) if err != nil { - return nil, pkgerrors.Wrap(err, "unable to marshal err to json") + return nil, pkgerrors.Wrap(err, "failed to extract RPC error: unable to marshal the underlying error to JSON for inspection") } jErr := JsonError{} err = json.Unmarshal(jsonBytes, &jErr) if err != nil { - return nil, pkgerrors.Wrapf(err, "unable to unmarshal json into jsonError struct (got: %v)", baseErr) + return nil, pkgerrors.Wrapf(err, "failed to extract RPC error: unable to unmarshal JSON into JsonError struct, the error may not be a standard JSON-RPC error (got: %v)", baseErr) } if jErr.Code == 0 { - return nil, pkgerrors.Errorf("not a RPCError because it does not have a code (got: %v)", baseErr) + return nil, pkgerrors.Errorf("failed to extract RPC error: error does not contain a JSON-RPC error code, so it is not a standard RPC error (got: %v)", baseErr) } return &jErr, nil } diff --git a/pkg/client/limited_transport.go b/pkg/client/limited_transport.go index cc8c8ccc65..64aab9b079 100644 --- a/pkg/client/limited_transport.go +++ b/pkg/client/limited_transport.go @@ -54,7 +54,7 @@ func GetResponseSizeLimit(ctx context.Context) uint32 { return limit } -var errResponseTooLarge = errors.New("response is too large") +var errResponseTooLarge = errors.New("response is too large: the RPC response exceeded the configured maximum size limit. Consider increasing the response size limit in node configuration or narrowing the query scope") // limitReader returns a Reader that reads from r // but stops with EOF after n bytes. diff --git a/pkg/client/rpc_client.go b/pkg/client/rpc_client.go index e66765d574..b7f946dc63 100644 --- a/pkg/client/rpc_client.go +++ b/pkg/client/rpc_client.go @@ -191,7 +191,7 @@ func (r *RPCClient) Dial(callerCtx context.Context) error { ws := r.ws.Load() httpClient := r.http.Load() if ws == nil && httpClient == nil { - return errors.New("cannot dial rpc client when both ws and http info are missing") + return errors.New("failed to dial RPC client: both WebSocket and HTTP URLs are missing. At least one connection URL must be configured") } promEVMPoolRPCNodeDials.WithLabelValues(r.chainID.String(), r.name).Inc() @@ -356,8 +356,8 @@ func (r *RPCClient) BatchCallContext(rootCtx context.Context, b []rpc.BatchElem) if r.chainType == chaintype.ChainAstar { for _, el := range b { if el.Method == "eth_getLogs" { - r.rpcLog.Critical("evmclient.BatchCallContext: eth_getLogs is not supported") - return errors.New("evmclient.BatchCallContext: eth_getLogs is not supported") + r.rpcLog.Critical("evmclient.BatchCallContext failed: eth_getLogs is not supported for Astar chain type in batch calls") + return errors.New("evmclient.BatchCallContext failed: eth_getLogs is not supported for Astar chain type in batch calls. Use individual log queries instead") } if !isRequestingFinalizedBlock(el) { continue @@ -441,7 +441,7 @@ func (r *RPCClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H } if ws == nil { - return nil, nil, errors.New("SubscribeNewHead is not allowed without ws url") + return nil, nil, errors.New("failed to subscribe to new heads: WebSocket URL is required for head subscriptions but none was configured. Enable HTTP polling or configure a WebSocket URL") } if multinode.CtxIsHealthCheckRequest(ctx) { @@ -639,7 +639,7 @@ func (r *RPCClient) astarLatestFinalizedBlock(ctx context.Context, result interf } if astarHead.Number == nil { - return r.wrapRPCClientError(errors.New("expected non empty head number of finalized block")) + return r.wrapRPCClientError(errors.New("failed to get Astar finalized block: the finalized block header returned a nil block number. The RPC node may be syncing or unhealthy")) } err = r.ethGetBlockByNumber(ctx, astarHead.Number.String(), result) @@ -790,7 +790,7 @@ func (r *RPCClient) SendTransaction(ctx context.Context, tx *types.Transaction) lggr.Debug("RPC call: evmclient.Client#SendTransaction") start := time.Now() if r.isChainType(chaintype.ChainTron) { - err := errors.New("SendTransaction not implemented for Tron, this should never be called") + err := errors.New("SendTransaction is not supported for Tron chain type: Tron uses a different transaction submission mechanism. This method should never be called for Tron nodes") return struct{}{}, multinode.Fatal, err } @@ -804,7 +804,7 @@ func (r *RPCClient) SendTransaction(ctx context.Context, tx *types.Transaction) func (r *RPCClient) SimulateTransaction(ctx context.Context, tx *types.Transaction) error { // Not Implemented - return pkgerrors.New("SimulateTransaction not implemented") + return pkgerrors.New("SimulateTransaction is not implemented for this RPC client") } func (r *RPCClient) SendEmptyTransaction( @@ -816,7 +816,7 @@ func (r *RPCClient) SendEmptyTransaction( fromAddress common.Address, ) (txhash string, err error) { // Not Implemented - return "", pkgerrors.New("SendEmptyTransaction not implemented") + return "", pkgerrors.New("SendEmptyTransaction is not implemented for this RPC client") } // PendingSequenceAt returns one higher than the highest nonce from both mempool and mined transactions @@ -831,7 +831,7 @@ func (r *RPCClient) PendingSequenceAt(ctx context.Context, account common.Addres // Tron doesn't have the concept of nonces, this shouldn't be called but just in case we'll return an error if r.isChainType(chaintype.ChainTron) { - err = errors.New("tron does not support eth_getTransactionCount") + err = errors.New("Tron chain type does not support eth_getTransactionCount: Tron does not use the nonce-based transaction model") return } @@ -861,7 +861,7 @@ func (r *RPCClient) NonceAt(ctx context.Context, account common.Address, blockNu // Tron doesn't have the concept of nonces, this shouldn't be called but just in case we'll return an error if r.isChainType(chaintype.ChainTron) { - err = errors.New("tron does not support eth_getTransactionCount") + err = errors.New("Tron chain type does not support eth_getTransactionCount: Tron does not use the nonce-based transaction model") return } @@ -1225,7 +1225,7 @@ func (r *RPCClient) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQu ctx, cancel, chStopInFlight, ws, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) defer cancel() if ws == nil { - return nil, errors.New("SubscribeFilterLogs is not allowed without ws url") + return nil, errors.New("failed to subscribe to filter logs: WebSocket URL is required for log subscriptions but none was configured. Configure a WebSocket URL to enable log subscriptions") } lggr := r.newRqLggr().With("q", q) @@ -1486,7 +1486,7 @@ func (r *RPCClient) doWithConfidence(ctx context.Context, request rpc.BatchElem, } if referencedHead == nil { - return errors.New("referenced block request returned nil. RPC is unhealthy or chain does not support specified tag") + return errors.New("referenced block request returned nil block header: the RPC node may be unhealthy, still syncing, or the chain does not support the specified block tag. Verify the RPC node status and chain compatibility") } maxAvailableHeight, err := r.referenceHeadToMaxAvailableHeight(confidence, referencedHead.Number) diff --git a/pkg/client/rpc_client_internal_test.go b/pkg/client/rpc_client_internal_test.go index b8a3b1fa75..f6f0f1bf47 100644 --- a/pkg/client/rpc_client_internal_test.go +++ b/pkg/client/rpc_client_internal_test.go @@ -217,7 +217,7 @@ func TestRPCClient_doWithConfidence(t *testing.T) { EthCallResult: "0x00", ExpectedTag: "safe", BlockByNumberResult: "null", - ExpectedError: "referenced block request returned nil. RPC is unhealthy or chain does not support specified tag", + ExpectedError: "referenced block request returned nil block header: the RPC node may be unhealthy, still syncing, or the chain does not support the specified block tag. Verify the RPC node status and chain compatibility", }, { Name: "Happy path", diff --git a/pkg/client/rpc_client_test.go b/pkg/client/rpc_client_test.go index ffb1e5028b..8a171d60d1 100644 --- a/pkg/client/rpc_client_test.go +++ b/pkg/client/rpc_client_test.go @@ -98,7 +98,7 @@ func TestRPCClient_SubscribeToHeads(t *testing.T) { t.Run("WS and HTTP URL cannot be both empty", func(t *testing.T) { // ws is optional when LogBroadcaster is disabled, however SubscribeFilterLogs will return error if ws is missing rpcClient := client.NewTestRPCClient(t, client.RPCClientOpts{}) - require.Equal(t, errors.New("cannot dial rpc client when both ws and http info are missing"), rpcClient.Dial(ctx)) + require.Equal(t, errors.New("failed to dial RPC client: both WebSocket and HTTP URLs are missing. At least one connection URL must be configured"), rpcClient.Dial(ctx)) }) t.Run("Updates chain info on new blocks", func(t *testing.T) { @@ -391,7 +391,7 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { require.NoError(t, rpcClient.Dial(ctx)) _, err = rpcClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) - require.Equal(t, errors.New("SubscribeFilterLogs is not allowed without ws url"), err) + require.Equal(t, errors.New("failed to subscribe to filter logs: WebSocket URL is required for log subscriptions but none was configured. Configure a WebSocket URL to enable log subscriptions"), err) }) t.Run("Failed SubscribeFilterLogs logs and returns proper error", func(t *testing.T) { server := testutils.NewWSServer(t, chainID, func(reqMethod string, reqParams gjson.Result) (resp testutils.JSONRPCResponse) { @@ -901,7 +901,7 @@ func TestRPCClient_Tron(t *testing.T) { // Verify it returns the expected error for Tron require.Error(t, err) - assert.Equal(t, "SendTransaction not implemented for Tron, this should never be called", err.Error()) + assert.Equal(t, "SendTransaction is not supported for Tron chain type: Tron uses a different transaction submission mechanism. This method should never be called for Tron nodes", err.Error()) }) t.Run("NonceAt", func(t *testing.T) { diff --git a/pkg/client/simulated_backend_client.go b/pkg/client/simulated_backend_client.go index 144601d156..4480877d32 100644 --- a/pkg/client/simulated_backend_client.go +++ b/pkg/client/simulated_backend_client.go @@ -167,7 +167,7 @@ func (c *SimulatedBackendClient) TokenBalance(ctx context.Context, address commo } err = balanceOfABI.UnpackIntoInterface(balance, "balanceOf", b) if err != nil { - return nil, errors.New("unable to unpack balance") + return nil, errors.New("failed to unpack ERC20 balanceOf response: the returned data could not be decoded. The contract may not implement the standard ERC20 balanceOf interface") } return balance, nil } @@ -243,7 +243,7 @@ func (c *SimulatedBackendClient) blockNumber(ctx context.Context, number interfa return nil, nil } if n.Sign() < 0 { - return nil, errors.New("block number must be non-negative") + return nil, errors.New("invalid block number: block number must be non-negative. Use nil for the latest block or provide a valid block number >= 0") } return n, nil default: @@ -959,13 +959,13 @@ func interfaceToAddress(value interface{}) (common.Address, error) { return *v, nil case string: if ok := common.IsHexAddress(v); !ok { - return common.Address{}, errors.New("string not formatted as a hex encoded evm address") + return common.Address{}, errors.New("invalid address format: string is not a valid hex-encoded EVM address. Expected format: 0x followed by 40 hex characters (e.g., 0x1234...abcd)") } return common.HexToAddress(v), nil case *big.Int: if v.Uint64() > 0 || len(v.Bytes()) > 20 { - return common.Address{}, errors.New("invalid *big.Int; value must be larger than 0 with a byte length <= 20") + return common.Address{}, errors.New("invalid *big.Int for address conversion: value must be greater than 0 with a byte length of 20 or fewer bytes to represent a valid EVM address") } return common.BigToAddress(v), nil @@ -983,7 +983,7 @@ func interfaceToHash(value interface{}) (*common.Hash, error) { case string: b, err := hex.DecodeString(v) if err != nil || len(b) != 32 { - return nil, errors.New("string does not represent a 32-byte hexadecimal number") + return nil, errors.New("invalid hash format: string does not represent a valid 32-byte (64 hex character) hash. Ensure the input is a valid hex-encoded 256-bit hash") } h := common.Hash(b) return &h, nil diff --git a/pkg/read/errors.go b/pkg/read/errors.go index bbeb77b9b2..e99f87a17d 100644 --- a/pkg/read/errors.go +++ b/pkg/read/errors.go @@ -51,9 +51,9 @@ func newErrorFromCall(err error, call Call, block string, tp readType) Error { func (e Error) Error() string { var builder strings.Builder - builder.WriteString("[read error]") + builder.WriteString("[contract read error]") builder.WriteString(fmt.Sprintf(" err: %s;", e.Err.Error())) - builder.WriteString(fmt.Sprintf(" type: %s;", e.Type)) + builder.WriteString(fmt.Sprintf(" read type: %s;", e.Type)) if e.Detail != nil { builder.WriteString(fmt.Sprintf(" block: %s;", e.Detail.Block)) @@ -101,9 +101,9 @@ func newErrorFromCalls(err error, calls []Call, block string, tp readType) Multi func (e MultiCallError) Error() string { var builder strings.Builder - builder.WriteString("[read error]") + builder.WriteString("[batch contract read error]") builder.WriteString(fmt.Sprintf(" err: %s;", e.Err.Error())) - builder.WriteString(fmt.Sprintf(" type: %s;", e.Type)) + builder.WriteString(fmt.Sprintf(" read type: %s;", e.Type)) if e.Detail != nil { builder.WriteString(fmt.Sprintf(" block: %s;", e.Detail.Block)) @@ -133,25 +133,25 @@ type ConfigError struct { func newMissingReadIdentifierErr(readIdentifier string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[no configured reader] read-identifier: '%s'", readIdentifier), + Msg: fmt.Sprintf("[contract reader configuration error] no configured reader found for read-identifier: '%s'. Ensure the contract and read name are registered in the chain reader configuration", readIdentifier), } } func newMissingContractErr(readIdentifier, contract string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[no configured reader] read-identifier: %s; contract: %s;", readIdentifier, contract), + Msg: fmt.Sprintf("[contract reader configuration error] no configured reader found for contract '%s' (read-identifier: %s). Ensure the contract is registered in the chain reader configuration", contract, readIdentifier), } } func newMissingReadNameErr(readIdentifier, contract, readName string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[no configured reader] read-identifier: %s; contract: %s; read-name: %s;", readIdentifier, contract, readName), + Msg: fmt.Sprintf("[contract reader configuration error] no configured reader found for read-name '%s' on contract '%s' (read-identifier: %s). Ensure the method or event is registered in the chain reader configuration", readName, contract, readIdentifier), } } func newUnboundAddressErr(address, contract, readName string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[address not bound] address: %s; contract: %s; read-name: %s;", address, contract, readName), + Msg: fmt.Sprintf("[contract reader configuration error] address '%s' is not bound to contract '%s' for read-name '%s'. Bind the address to the contract before attempting reads", address, contract, readName), } } @@ -166,7 +166,7 @@ type FilterError struct { } func (e FilterError) Error() string { - return fmt.Sprintf("[logpoller filter error] action: %s; err: %s; filter: %+v;", e.Action, e.Err.Error(), e.Filter) + return fmt.Sprintf("[log poller filter error] failed during '%s' action: %s; filter details: %+v. Check that the filter configuration matches the expected contract events and addresses", e.Action, e.Err.Error(), e.Filter) } func (e FilterError) Unwrap() error { @@ -179,7 +179,7 @@ type NoContractExistsError struct { } func (e NoContractExistsError) Error() string { - return fmt.Sprintf("%s: contract does not exist at address: %s", e.Err.Error(), e.Address) + return fmt.Sprintf("%s: no contract exists at address %s. Verify that the contract has been deployed to this address on the correct chain and that the address is not an externally-owned account (EOA)", e.Err.Error(), e.Address) } func (e NoContractExistsError) Unwrap() error {