From 112f3c4abf8f9912ea93824632954a608d2fc92b Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 13:16:59 +0200 Subject: [PATCH 1/8] add rate limiting when fetching headers, store failed event decoding information, various smaller fixes --- seth/client.go | 64 ++++++++++++++++++++++++++++++----- seth/client_builder.go | 2 +- seth/decode.go | 75 ++++++++++++++++++++++++++++++++++-------- seth/gas_adjuster.go | 23 ++++++++----- seth/nonce.go | 2 +- seth/retry.go | 55 +++++++++++++++++++++---------- 6 files changed, 172 insertions(+), 49 deletions(-) diff --git a/seth/client.go b/seth/client.go index 1ea4ad3c8..40561e2ad 100644 --- a/seth/client.go +++ b/seth/client.go @@ -532,8 +532,6 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri 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 fmt.Errorf("failed to send transaction to network: %w\n"+ @@ -1345,12 +1343,13 @@ func (t TransactionLog) GetData() []byte { return t.Data } -func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs []*abi.ABI) ([]DecodedTransactionLog, error) { +func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs []*abi.ABI) ([]DecodedTransactionLog, []EventDecodingError, error) { l.Trace(). Msg("Decoding events") sigMap := buildEventSignatureMap(allABIs) var eventsParsed []DecodedTransactionLog + var decodeErrors []EventDecodingError for _, lo := range logs { if len(lo.Topics) == 0 { l.Debug(). @@ -1384,6 +1383,7 @@ func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs // Iterate over possible events with the same signature matched := false + var decodeAttempts []ABIDecodingError for _, evWithABI := range possibleEvents { evSpec := evWithABI.EventSpec contractABI := evWithABI.ContractABI @@ -1421,19 +1421,28 @@ func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs } // Proceed to decode the event + // Find ABI name for this contract ABI + abiName := m.findABIName(contractABI) d := TransactionLog{lo.Topics, lo.Data} l.Trace(). Str("Name", evSpec.RawName). Str("Signature", evSpec.Sig). + Str("ABI", abiName). Msg("Unpacking event") eventsMap, topicsMap, err := decodeEventFromLog(l, *contractABI, *evSpec, d) if err != nil { - l.Error(). + l.Debug(). Err(err). Str("Event", evSpec.Name). - Msg("Failed to decode event; skipping") - continue // Skip this event instead of returning an error + Str("ABI", abiName). + Msg("Failed to decode event; trying next ABI") + decodeAttempts = append(decodeAttempts, ABIDecodingError{ + ABIName: abiName, + EventName: evSpec.Name, + Error: err.Error(), + }) + continue // Try next ABI instead of giving up } parsedEvent := decodedLogFromMaps(&DecodedTransactionLog{}, eventsMap, topicsMap) @@ -1455,12 +1464,36 @@ func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs } if !matched { + // Record the decode failure + topics := make([]string, len(lo.Topics)) + for i, topic := range lo.Topics { + topics[i] = topic.Hex() + } + + decodeError := EventDecodingError{ + Signature: eventSig, + LogIndex: lo.Index, + Address: lo.Address.Hex(), + Topics: topics, + Errors: decodeAttempts, + } + decodeErrors = append(decodeErrors, decodeError) + + abiNames := make([]string, len(decodeAttempts)) + for i, attempt := range decodeAttempts { + abiNames[i] = attempt.ABIName + } + l.Warn(). Str("Signature", eventSig). - Msg("No matching event with valid indexed parameter count found for log") + Uint("LogIndex", lo.Index). + Str("Address", lo.Address.Hex()). + Strs("AttemptedABIs", abiNames). + Int("FailedAttempts", len(decodeAttempts)). + Msg("Failed to decode event log") } } - return eventsParsed, nil + return eventsParsed, decodeErrors, nil } type eventWithABI struct { @@ -1484,6 +1517,21 @@ func buildEventSignatureMap(allABIs []*abi.ABI) map[string][]*eventWithABI { return sigMap } +// findABIName finds the name of the ABI in the ContractStore, returns "unknown" if not found +func (m *Client) findABIName(targetABI *abi.ABI) string { + if m.ContractStore == nil { + return "unknown" + } + + for name, storedABI := range m.ContractStore.ABIs { + if reflect.DeepEqual(storedABI, *targetABI) { + return strings.TrimSuffix(name, ".abi") + } + } + + return "unknown" +} + // WaitUntilNoPendingTxForRootKey waits until there's no pending transaction for root key. If after timeout there are still pending transactions, it returns error. func (m *Client) WaitUntilNoPendingTxForRootKey(timeout time.Duration) error { return m.WaitUntilNoPendingTx(m.MustGetRootKeyAddress(), timeout) diff --git a/seth/client_builder.go b/seth/client_builder.go index e40f3269e..d1e50faa3 100644 --- a/seth/client_builder.go +++ b/seth/client_builder.go @@ -32,7 +32,7 @@ func NewClientBuilder() *ClientBuilder { DialTimeout: MustMakeDuration(DefaultDialTimeout), TransferGasFee: DefaultTransferGasFee, GasPriceEstimationEnabled: true, - GasPriceEstimationBlocks: 200, + GasPriceEstimationBlocks: 20, GasPriceEstimationTxPriority: Priority_Standard, GasPrice: DefaultGasPrice, GasFeeCap: DefaultGasFeeCap, diff --git a/seth/decode.go b/seth/decode.go index 8e2303e43..99ea99a79 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -36,12 +36,29 @@ const ( // DecodedTransaction decoded transaction type DecodedTransaction struct { CommonData - Index uint `json:"index"` - Hash string `json:"hash,omitempty"` - Protected bool `json:"protected,omitempty"` - Transaction *types.Transaction `json:"transaction,omitempty"` - Receipt *types.Receipt `json:"receipt,omitempty"` - Events []DecodedTransactionLog `json:"events,omitempty"` + Index uint `json:"index"` + Hash string `json:"hash,omitempty"` + Protected bool `json:"protected,omitempty"` + Transaction *types.Transaction `json:"transaction,omitempty"` + Receipt *types.Receipt `json:"receipt,omitempty"` + Events []DecodedTransactionLog `json:"events,omitempty"` + EventDecodingErrors []EventDecodingError `json:"event_decoding_errors,omitempty"` +} + +// EventDecodingError represents a failed event decode attempt +type EventDecodingError struct { + Signature string `json:"signature"` + LogIndex uint `json:"log_index"` + Address string `json:"address"` + Topics []string `json:"topics,omitempty"` + Errors []ABIDecodingError `json:"errors,omitempty"` +} + +// ABIDecodingError represents a single ABI decode attempt failure +type ABIDecodingError struct { + ABIName string `json:"abi_name"` + EventName string `json:"event_name"` + Error string `json:"error"` } type CommonData struct { @@ -448,6 +465,7 @@ func (m *Client) decodeTransaction(l zerolog.Logger, tx *types.Transaction, rece } var txIndex uint + var decodeErrors []EventDecodingError if receipt != nil { l.Trace().Interface("Receipt", receipt).Msg("TX receipt") @@ -463,7 +481,8 @@ func (m *Client) decodeTransaction(l zerolog.Logger, tx *types.Transaction, rece allABIs = m.ContractStore.GetAllABIs() } - txEvents, err = m.decodeContractLogs(l, logsValues, allABIs) + var err error + txEvents, decodeErrors, err = m.decodeContractLogs(l, logsValues, allABIs) if err != nil { return defaultTxn, err } @@ -475,12 +494,13 @@ func (m *Client) decodeTransaction(l zerolog.Logger, tx *types.Transaction, rece Method: abiResult.Method.Sig, Input: txInput, }, - Index: txIndex, - Receipt: receipt, - Transaction: tx, - Protected: tx.Protected(), - Hash: tx.Hash().String(), - Events: txEvents, + Index: txIndex, + Receipt: receipt, + Transaction: tx, + Protected: tx.Protected(), + Hash: tx.Hash().String(), + Events: txEvents, + EventDecodingErrors: decodeErrors, } return ptx, nil @@ -499,7 +519,34 @@ func (m *Client) printDecodedTXData(l zerolog.Logger, ptx *DecodedTransaction) { for _, e := range ptx.Events { l.Debug(). Str("Signature", e.Signature). - Interface("Log", e.EventData).Send() + Str("Address", e.Address.Hex()). + Interface("Topics", e.Topics). + Interface("Data", e.EventData). + Msg("Event emitted") + } + + // Print event decoding errors separately + if len(ptx.EventDecodingErrors) > 0 { + l.Warn(). + Int("Failed event decodes", len(ptx.EventDecodingErrors)). + Msg("Some events could not be decoded") + + for _, decodeErr := range ptx.EventDecodingErrors { + abiNames := make([]string, len(decodeErr.Errors)) + errorMsgs := make([]string, len(decodeErr.Errors)) + for i, abiErr := range decodeErr.Errors { + abiNames[i] = abiErr.ABIName + errorMsgs[i] = fmt.Sprintf("%s.%s: %s", abiErr.ABIName, abiErr.EventName, abiErr.Error) + } + + l.Warn(). + Str("Signature", decodeErr.Signature). + Uint("LogIndex", decodeErr.LogIndex). + Str("Address", decodeErr.Address). + Strs("AttemptedABIs", abiNames). + Strs("Errors", errorMsgs). + Msg("Failed to decode event log") + } } } diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 363e1faae..cba73ff5c 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -2,6 +2,7 @@ package seth import ( "context" + "errors" "fmt" "math" "math/big" @@ -12,6 +13,7 @@ import ( "github.com/avast/retry-go" "github.com/ethereum/go-ethereum/core/types" + "go.uber.org/ratelimit" ) const ( @@ -56,7 +58,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy return cachedHeader, nil } - timeout := blocksNumber / 100 + timeout := blocksNumber / 10 if timeout < 3 { timeout = 3 } else if timeout > 6 { @@ -94,7 +96,9 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy var wg sync.WaitGroup dataCh := make(chan *types.Header) + limit := ratelimit.New(4) // 4 concurrent requests go func() { + limit.Take() for header := range dataCh { headers = append(headers, header) // placed here, because we want to wait for all headers to be received and added to slice before continuing @@ -262,6 +266,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( Float64("BaseFee", baseFee64). Int64("SuggestedTip", currentGasTip.Int64()). Msgf("Incorrect gas data received from node: base fee was 0. Skipping gas estimation") + err = errors.New("incorrect gas data received from node: base fee was 0. Skipping gas estimation") return } @@ -734,16 +739,18 @@ func calculateGasUsedRatio(headers []*types.Header) float64 { return 0 } - var totalRatio float64 + var totalGasUsedRatio float64 + validHeaders := 0 for _, header := range headers { - if header.GasLimit == 0 { - continue + if header.GasLimit > 0 { + totalGasUsedRatio += float64(header.GasUsed) / float64(header.GasLimit) + validHeaders++ } - ratio := float64(header.GasUsed) / float64(header.GasLimit) - totalRatio += ratio } - averageRatio := totalRatio / float64(len(headers)) - return averageRatio + if validHeaders == 0 { + return 0.0 + } + return totalGasUsedRatio / float64(validHeaders) } func calculateMagnitudeDifference(first, second *big.Float) (int, string) { diff --git a/seth/nonce.go b/seth/nonce.go index f8d54288e..06023fa99 100644 --- a/seth/nonce.go +++ b/seth/nonce.go @@ -107,7 +107,7 @@ func (m *NonceManager) UpdateNonces() error { for addr := range m.Nonces { nonce, err := m.Client.Client.NonceAt(context.Background(), addr, nil) if err != nil { - return err + return fmt.Errorf("failed to updated nonces for address '%s': %w", addr, err) } m.Nonces[addr] = mustSafeInt64(nonce) } diff --git a/seth/retry.go b/seth/retry.go index a1dfedd99..e878f8f0c 100644 --- a/seth/retry.go +++ b/seth/retry.go @@ -71,35 +71,32 @@ var NoOpGasBumpStrategyFn = func(previousGasPrice *big.Int) *big.Int { // PriorityBasedGasBumpingStrategyFn is a function that returns a gas bump strategy based on the priority. // For Fast priority it bumps gas price by 30%, for Standard by 15%, for Slow by 5% and for the rest it does nothing. var PriorityBasedGasBumpingStrategyFn = func(priority string) GasBumpStrategyFn { + divisor := big.NewInt(100) + switch priority { case Priority_Degen: // +100% return func(gasPrice *big.Int) *big.Int { - return gasPrice.Mul(gasPrice, big.NewInt(2)) + multiplier := big.NewInt(200) + return new(big.Int).Div(new(big.Int).Mul(gasPrice, multiplier), divisor) } case Priority_Fast: // +30% return func(gasPrice *big.Int) *big.Int { - gasPriceFloat, _ := gasPrice.Float64() - newGasPriceFloat := big.NewFloat(0.0).Mul(big.NewFloat(gasPriceFloat), big.NewFloat(1.3)) - newGasPrice, _ := newGasPriceFloat.Int64() - return big.NewInt(newGasPrice) + multiplier := big.NewInt(130) + return new(big.Int).Div(new(big.Int).Mul(gasPrice, multiplier), divisor) } case Priority_Standard: // 15% return func(gasPrice *big.Int) *big.Int { - gasPriceFloat, _ := gasPrice.Float64() - newGasPriceFloat := big.NewFloat(0.0).Mul(big.NewFloat(gasPriceFloat), big.NewFloat(1.15)) - newGasPrice, _ := newGasPriceFloat.Int64() - return big.NewInt(newGasPrice) + multiplier := big.NewInt(115) + return new(big.Int).Div(new(big.Int).Mul(gasPrice, multiplier), divisor) } case Priority_Slow: // 5% return func(gasPrice *big.Int) *big.Int { - gasPriceFloat, _ := gasPrice.Float64() - newGasPriceFloat := big.NewFloat(0.0).Mul(big.NewFloat(gasPriceFloat), big.NewFloat(1.05)) - newGasPrice, _ := newGasPriceFloat.Int64() - return big.NewInt(newGasPrice) + multiplier := big.NewInt(105) + return new(big.Int).Div(new(big.Int).Mul(gasPrice, multiplier), divisor) } case Priority_Auto: // No bumping for Auto priority @@ -251,14 +248,38 @@ var prepareReplacementTransaction = func(client *Client, tx *types.Transaction) Str("Gas fee blob diff", fmt.Sprintf("%s wei /%s ether", gasBlobFeeCapDiff, WeiToEther(gasBlobFeeCapDiff).Text('f', -1))). Msg("Bumping gas fee cap and tip cap for Blob transaction") + var value *uint256.Int + if tx.Value() != nil { + var overflow bool + value, overflow = uint256.FromBig(tx.Value()) + if overflow { + return nil, fmt.Errorf("blob transaction value %s overflows uint256", tx.Value().String()) + } + } + + gasFeeCap, overflow := uint256.FromBig(newGasFeeCap) + if overflow { + return nil, fmt.Errorf("gas fee cap %s overflows uint256 after bumping", newGasFeeCap.String()) + } + + gasTipCap, overflow := uint256.FromBig(newGasTipCap) + if overflow { + return nil, fmt.Errorf("gas tip cap %s overflows uint256 after bumping", newGasTipCap.String()) + } + + blobFeeCap, overflow := uint256.FromBig(newBlobFeeCap) + if overflow { + return nil, fmt.Errorf("blob fee cap %s overflows uint256 after bumping", newBlobFeeCap.String()) + } + txData := &types.BlobTx{ Nonce: tx.Nonce(), To: *tx.To(), - Value: uint256.NewInt(tx.Value().Uint64()), + Value: value, Gas: tx.Gas(), - GasFeeCap: uint256.NewInt(newGasFeeCap.Uint64()), - GasTipCap: uint256.NewInt(newGasTipCap.Uint64()), - BlobFeeCap: uint256.NewInt(newBlobFeeCap.Uint64()), + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + BlobFeeCap: blobFeeCap, BlobHashes: tx.BlobHashes(), Data: tx.Data(), } From aec6319e43f1a12492cab59b19b472fee02739b6 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 15:14:16 +0200 Subject: [PATCH 2/8] wrap error correctly --- seth/abi_finder.go | 8 +++++--- seth/client.go | 12 +++++------ seth/decode.go | 49 ++++++++++++++++++++------------------------ seth/gas_adjuster.go | 36 ++++++++++++++++++++++++-------- seth/tracing.go | 17 +++++++-------- 5 files changed, 69 insertions(+), 53 deletions(-) diff --git a/seth/abi_finder.go b/seth/abi_finder.go index 249e88fbc..e79bdd87f 100644 --- a/seth/abi_finder.go +++ b/seth/abi_finder.go @@ -61,7 +61,7 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder Str("Contract", contractName). Str("Address", address). Msg("ABI not found for known contract") - return ABIFinderResult{}, err + return ABIFinderResult{}, fmt.Errorf("%w: %v", ErrNoABIMethod, err) } methodCandidate, err := abiInstanceCandidate.MethodById(signature) @@ -99,7 +99,7 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder Str("Supposed address", address). Msg("Method not found in known ABI instance. This should not happen. Contract map might be corrupted") - return ABIFinderResult{}, err + return ABIFinderResult{}, fmt.Errorf("%w: %v", ErrNoABIMethod, err) } result.Method = methodCandidate @@ -137,7 +137,7 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder } if result.Method == nil { - return ABIFinderResult{}, fmt.Errorf("no ABI found with method signature %s for contract at address %s.\n"+ + err := 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"+ @@ -151,6 +151,8 @@ func (a *ABIFinder) FindABIByMethod(address string, signature []byte) (ABIFinder " 4. Review contract_map_file for address-to-name mappings\n"+ " 5. Use ContractStore.AddABI() to manually add the ABI", stringSignature, address, len(a.ContractStore.ABIs)) + + return ABIFinderResult{}, fmt.Errorf("%w: %v", ErrNoABIMethod, err) } return result, nil diff --git a/seth/client.go b/seth/client.go index 40561e2ad..5965d21b3 100644 --- a/seth/client.go +++ b/seth/client.go @@ -987,7 +987,7 @@ func (m *Client) CalculateGasEstimations(request GasEstimationRequest) GasEstima defer cancel() var disableEstimationsIfNeeded = func(err error) { - if strings.Contains(err.Error(), ZeroGasSuggestedErr) { + if errors.Is(err, GasEstimationErr) { L.Warn().Msg("Received incorrect gas estimations. Disabling them and reverting to hardcoded values. Remember to update your config!") m.Cfg.Network.GasPriceEstimationEnabled = false } @@ -1469,7 +1469,7 @@ func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs for i, topic := range lo.Topics { topics[i] = topic.Hex() } - + decodeError := EventDecodingError{ Signature: eventSig, LogIndex: lo.Index, @@ -1478,12 +1478,12 @@ func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs Errors: decodeAttempts, } decodeErrors = append(decodeErrors, decodeError) - + abiNames := make([]string, len(decodeAttempts)) for i, attempt := range decodeAttempts { abiNames[i] = attempt.ABIName } - + l.Warn(). Str("Signature", eventSig). Uint("LogIndex", lo.Index). @@ -1522,13 +1522,13 @@ func (m *Client) findABIName(targetABI *abi.ABI) string { if m.ContractStore == nil { return "unknown" } - + for name, storedABI := range m.ContractStore.ABIs { if reflect.DeepEqual(storedABI, *targetABI) { return strings.TrimSuffix(name, ".abi") } } - + return "unknown" } diff --git a/seth/decode.go b/seth/decode.go index 99ea99a79..26b41cfb7 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -20,45 +20,40 @@ import ( "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" +var ( + ErrNoABIMethod = errors.New("no ABI method found") +) +const ( WarnNoContractStore = "ContractStore is nil, use seth.NewContractStore(...) to decode transactions" ) // DecodedTransaction decoded transaction type DecodedTransaction struct { CommonData - Index uint `json:"index"` - Hash string `json:"hash,omitempty"` - Protected bool `json:"protected,omitempty"` - Transaction *types.Transaction `json:"transaction,omitempty"` - Receipt *types.Receipt `json:"receipt,omitempty"` - Events []DecodedTransactionLog `json:"events,omitempty"` + Index uint `json:"index"` + Hash string `json:"hash,omitempty"` + Protected bool `json:"protected,omitempty"` + Transaction *types.Transaction `json:"transaction,omitempty"` + Receipt *types.Receipt `json:"receipt,omitempty"` + Events []DecodedTransactionLog `json:"events,omitempty"` EventDecodingErrors []EventDecodingError `json:"event_decoding_errors,omitempty"` } // EventDecodingError represents a failed event decode attempt type EventDecodingError struct { - Signature string `json:"signature"` - LogIndex uint `json:"log_index"` - Address string `json:"address"` - Topics []string `json:"topics,omitempty"` - Errors []ABIDecodingError `json:"errors,omitempty"` + Signature string `json:"signature"` + LogIndex uint `json:"log_index"` + Address string `json:"address"` + Topics []string `json:"topics,omitempty"` + Errors []ABIDecodingError `json:"errors,omitempty"` } // ABIDecodingError represents a single ABI decode attempt failure type ABIDecodingError struct { - ABIName string `json:"abi_name"` - EventName string `json:"event_name"` - Error string `json:"error"` + ABIName string `json:"abi_name"` + EventName string `json:"event_name"` + Error string `json:"error"` } type CommonData struct { @@ -196,7 +191,7 @@ 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) { m.handleTxDecodingError(l, *decoded, decodeErr) return decoded, revertErr } @@ -524,13 +519,13 @@ func (m *Client) printDecodedTXData(l zerolog.Logger, ptx *DecodedTransaction) { Interface("Data", e.EventData). Msg("Event emitted") } - + // Print event decoding errors separately if len(ptx.EventDecodingErrors) > 0 { l.Warn(). Int("Failed event decodes", len(ptx.EventDecodingErrors)). Msg("Some events could not be decoded") - + for _, decodeErr := range ptx.EventDecodingErrors { abiNames := make([]string, len(decodeErr.Errors)) errorMsgs := make([]string, len(decodeErr.Errors)) @@ -538,7 +533,7 @@ func (m *Client) printDecodedTXData(l zerolog.Logger, ptx *DecodedTransaction) { abiNames[i] = abiErr.ABIName errorMsgs[i] = fmt.Sprintf("%s.%s: %s", abiErr.ABIName, abiErr.EventName, abiErr.Error) } - + l.Warn(). Str("Signature", decodeErr.Signature). Uint("LogIndex", decodeErr.LogIndex). diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index cba73ff5c..404197374 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -7,7 +7,6 @@ import ( "math" "math/big" "slices" - "strings" "sync" "time" @@ -37,17 +36,19 @@ const ( ) var ( - ZeroGasSuggestedErr = "either base fee or suggested tip is 0" - BlockFetchingErr = "failed to fetch enough block headers for congestion calculation" + GasEstimationErr = errors.New("incorrect gas data received from node. Skipping gas estimation") + BlockFetchingErr = 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 not initialized. " + + err := 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") + err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + return 0, err } var getHeaderData = func(bn *big.Int) (*types.Header, error) { if bn == nil { @@ -80,6 +81,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy defer cancel() lastBlockNumber, err := m.Client.BlockNumber(ctx) if err != nil { + err = fmt.Errorf("%w: %v", BlockFetchingErr, err) return 0, err } @@ -87,6 +89,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy lastBlock, err := getHeaderData(big.NewInt(mustSafeInt64(lastBlockNumber))) if err != nil { + err = fmt.Errorf("%w: %v", BlockFetchingErr, err) return 0, err } @@ -133,7 +136,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy minBlockCount := int(float64(blocksNumber) * 0.8) if len(headers) < minBlockCount { - return 0, fmt.Errorf("failed to fetch sufficient block headers for gas estimation. "+ + err := 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"+ @@ -145,6 +148,9 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy " 3. Disable gas estimation: set gas_price_estimation_enabled = false\n"+ " 4. Reduce gas_price_estimation_blocks to fetch fewer blocks", minBlockCount, len(headers), float64(len(headers))/float64(blocksNumber)*100) + err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + + return 0, err } switch strategy { @@ -153,10 +159,12 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy case CongestionStrategy_NewestFirst: return calculateNewestFirstNetworkCongestionMetric(headers), nil default: - return 0, fmt.Errorf("unknown network congestion strategy '%s'. "+ + err := 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) + err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + return 0, err } } @@ -209,6 +217,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( // fallback to current fees if historical fetching fails baseFee, currentGasTip, err = m.currentIP1559Fees(ctx) if err != nil { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } L.Debug().Msg("Falling back to current EIP-1559 fees for gas estimation") @@ -216,6 +225,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( } else { baseFee, currentGasTip, err = m.currentIP1559Fees(ctx) if err != nil { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } } @@ -227,6 +237,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( " 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)") + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } @@ -267,6 +278,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( Int64("SuggestedTip", currentGasTip.Int64()). Msgf("Incorrect gas data received from node: base fee was 0. Skipping gas estimation") err = errors.New("incorrect gas data received from node: base fee was 0. Skipping gas estimation") + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } @@ -283,6 +295,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( var adjustmentFactor float64 adjustmentFactor, err = getAdjustmentFactor(priority) if err != nil { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } @@ -315,6 +328,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( var bufferAdjustment float64 bufferAdjustment, err = getCongestionFactor(congestionClassification) if err != nil { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } @@ -325,7 +339,8 @@ 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, BlockFetchingErr) { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } else { L.Debug(). @@ -550,7 +565,7 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a })) if retryErr != nil { - err = retryErr + err = fmt.Errorf("%w: %v", GasEstimationErr, retryErr) return } @@ -562,6 +577,7 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a var adjustmentFactor float64 adjustmentFactor, err = getAdjustmentFactor(priority) if err != nil { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } @@ -589,13 +605,15 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a var bufferAdjustment float64 bufferAdjustment, err = getCongestionFactor(congestionClassification) if err != nil { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } // 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, BlockFetchingErr) { + err = fmt.Errorf("%w: %v", GasEstimationErr, err) return } else { L.Debug(). diff --git a/seth/tracing.go b/seth/tracing.go index 10744577b..2f0864d6d 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -2,6 +2,7 @@ package seth import ( "context" + "errors" "fmt" "strconv" "strings" @@ -14,13 +15,12 @@ import ( "github.com/rs/zerolog" ) +var ( + ErrNoFourByteFound = errors.New("no method signatures found in tracing data") +) + 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" @@ -259,7 +259,7 @@ 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(ErrNoFourByteFound.Error()) } methods := make([]string, 0, len(trace.CallTrace.Calls)+1) @@ -268,7 +268,8 @@ func (t *Tracer) DecodeTrace(l zerolog.Logger, trace Trace) ([]*DecodedCall, err if len(input) < 10 { 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", + "Unless the transaction is a simple ETH transfer with no data or a fallback function,"+ + "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). From a8005df2d3c16553ac07d1ee148c4229b3ba1b1a Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 15:58:58 +0200 Subject: [PATCH 3/8] fix rate limiting when fetching headers, fix for fee history if there is only one block, fix for fee equalizer (+ tests) --- .github/workflows/seth-test.yml | 4 +- seth/client.go | 8 +- seth/gas.go | 3 +- seth/gas_adjuster.go | 56 ++--- seth/gas_adjuster_test.go | 393 ++++++++++++++++++++++++++++++++ 5 files changed, 431 insertions(+), 33 deletions(-) create mode 100644 seth/gas_adjuster_test.go diff --git a/.github/workflows/seth-test.yml b/.github/workflows/seth-test.yml index a561324c2..dad61971f 100644 --- a/.github/workflows/seth-test.yml +++ b/.github/workflows/seth-test.yml @@ -58,12 +58,12 @@ jobs: network-type: Anvil url: "http://localhost:8545" extra_flags: "''" - - regex: "'TestContractMap|TestGasEstimator|TestRPCHealthCheck|TestUtil|TestContract'" + - regex: "'TestContractMap|TestGasEstimator|TestRPCHealthCheck|TestUtil|TestContract|TestGasAdjuster'" network-type: Geth url: "ws://localhost:8546" extra_flags: "-race" # TODO: still expects Geth WS URL for some reason - - regex: "'TestContractMap|TestGasEstimator|TestRPCHealthCheck|TestUtil|TestContract'" + - regex: "'TestContractMap|TestGasEstimator|TestRPCHealthCheck|TestUtil|TestContract|TestGasAdjuster'" network-type: Anvil url: "http://localhost:8545" extra_flags: "-race" diff --git a/seth/client.go b/seth/client.go index 5965d21b3..0a17db347 100644 --- a/seth/client.go +++ b/seth/client.go @@ -375,7 +375,11 @@ func NewClientRaw( // root key is element 0 in ephemeral for _, addr := range c.Addresses[1:] { eg.Go(func() error { - return c.TransferETHFromKey(egCtx, 0, addr.Hex(), bd.AddrFunding, gasPrice) + err := c.TransferETHFromKey(egCtx, 0, addr.Hex(), bd.AddrFunding, gasPrice) + if err != nil { + return fmt.Errorf("failed to fund ephemeral address %s: %w", addr.Hex(), err) + } + return nil }) } if err := eg.Wait(); err != nil { @@ -987,7 +991,7 @@ func (m *Client) CalculateGasEstimations(request GasEstimationRequest) GasEstima defer cancel() var disableEstimationsIfNeeded = func(err error) { - if errors.Is(err, GasEstimationErr) { + if errors.Is(err, ErrGasEstimation) { L.Warn().Msg("Received incorrect gas estimations. Disabling them and reverting to hardcoded values. Remember to update your config!") m.Cfg.Network.GasPriceEstimationEnabled = false } diff --git a/seth/gas.go b/seth/gas.go index ce3cc5077..a89dbb79e 100644 --- a/seth/gas.go +++ b/seth/gas.go @@ -38,6 +38,7 @@ func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPer "Alternatively, set 'gas_price_estimation_blocks = 0' to disable block-based estimation", err) } + if currentBlock == 0 { 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" + @@ -47,7 +48,7 @@ func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPer "You can set 'gas_price_estimation_blocks = 0' to disable block-based estimation") } if blockCount >= currentBlock { - blockCount = currentBlock - 1 + blockCount = max(currentBlock-1, 1) // avoid a case, when we ask for more blocks than exist and when currentBlock = 1 } hist, err := m.Client.Client.FeeHistory(ctx, blockCount, big.NewInt(mustSafeInt64(currentBlock)), []float64{priorityPerc}) diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 404197374..7d590d854 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -36,8 +36,8 @@ const ( ) var ( - GasEstimationErr = errors.New("incorrect gas data received from node. Skipping gas estimation") - BlockFetchingErr = errors.New("failed to fetch enough block headers for congestion calculation") + ErrGasEstimation = errors.New("incorrect gas data received from node. Skipping gas estimation") + ErrBlockFetching = errors.New("failed to fetch enough block headers for congestion calculation") ) // CalculateNetworkCongestionMetric calculates a simple congestion metric based on the last N blocks @@ -47,10 +47,10 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy err := 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") - err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + err = fmt.Errorf("%w: %v", ErrBlockFetching, err) return 0, err } - var getHeaderData = func(bn *big.Int) (*types.Header, error) { + var getHeaderData = func(ctx context.Context, bn *big.Int) (*types.Header, error) { if bn == nil { return nil, fmt.Errorf("block number is nil") } @@ -66,7 +66,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy timeout = 6 } - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(mustSafeInt64(timeout))*time.Second) + ctx, cancel := context.WithTimeout(ctx, time.Duration(mustSafeInt64(timeout))*time.Second) defer cancel() header, err := m.Client.HeaderByNumber(ctx, bn) if err != nil { @@ -81,15 +81,15 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy defer cancel() lastBlockNumber, err := m.Client.BlockNumber(ctx) if err != nil { - err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + err = fmt.Errorf("%w: %v", ErrBlockFetching, err) return 0, err } L.Trace().Msgf("Block range for gas calculation: %d - %d", lastBlockNumber-blocksNumber, lastBlockNumber) - lastBlock, err := getHeaderData(big.NewInt(mustSafeInt64(lastBlockNumber))) + lastBlock, err := getHeaderData(ctx, big.NewInt(mustSafeInt64(lastBlockNumber))) if err != nil { - err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + err = fmt.Errorf("%w: %v", ErrBlockFetching, err) return 0, err } @@ -98,10 +98,9 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy var wg sync.WaitGroup dataCh := make(chan *types.Header) + defer close(dataCh) - limit := ratelimit.New(4) // 4 concurrent requests go func() { - limit.Take() for header := range dataCh { headers = append(headers, header) // placed here, because we want to wait for all headers to be received and added to slice before continuing @@ -109,6 +108,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy } }() + limit := ratelimit.New(4) // 4 concurrent requests startTime := time.Now() for i := lastBlockNumber; i > lastBlockNumber-blocksNumber; i-- { // better safe than sorry (might happen for brand-new chains) @@ -118,7 +118,8 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy wg.Add(1) go func(bn *big.Int) { - header, err := getHeaderData(bn) + limit.Take() + header, err := getHeaderData(ctx, bn) if err != nil { L.Debug().Msgf("Failed to get block %d header due to: %s", bn.Int64(), err.Error()) wg.Done() @@ -129,7 +130,6 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy } wg.Wait() - close(dataCh) endTime := time.Now() L.Debug().Msgf("Time to fetch %d block headers: %v", blocksNumber, endTime.Sub(startTime)) @@ -148,7 +148,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy " 3. Disable gas estimation: set gas_price_estimation_enabled = false\n"+ " 4. Reduce gas_price_estimation_blocks to fetch fewer blocks", minBlockCount, len(headers), float64(len(headers))/float64(blocksNumber)*100) - err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + err = fmt.Errorf("%w: %v", ErrBlockFetching, err) return 0, err } @@ -163,7 +163,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy "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) - err = fmt.Errorf("%w: %v", BlockFetchingErr, err) + err = fmt.Errorf("%w: %v", ErrBlockFetching, err) return 0, err } } @@ -217,7 +217,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( // fallback to current fees if historical fetching fails baseFee, currentGasTip, err = m.currentIP1559Fees(ctx) if err != nil { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } L.Debug().Msg("Falling back to current EIP-1559 fees for gas estimation") @@ -225,7 +225,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( } else { baseFee, currentGasTip, err = m.currentIP1559Fees(ctx) if err != nil { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } } @@ -237,7 +237,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( " 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)") - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } @@ -263,7 +263,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( L.Debug().Msg("Suggested tip is 0.0. Will use historical base fee as tip.") currentGasTip = big.NewInt(int64(baseFee64)) } - } else if baseFeeTipMagnitudeDiff < 3 { + } else if baseFeeTipMagnitudeDiff < -3 { L.Debug().Msg("Historical base fee is 3 orders of magnitude lower than suggested tip. Will use suggested tip as base fee.") baseFee64 = float64(currentGasTip.Int64()) } else if baseFeeTipMagnitudeDiff > 3 { @@ -278,7 +278,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( Int64("SuggestedTip", currentGasTip.Int64()). Msgf("Incorrect gas data received from node: base fee was 0. Skipping gas estimation") err = errors.New("incorrect gas data received from node: base fee was 0. Skipping gas estimation") - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } @@ -295,7 +295,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( var adjustmentFactor float64 adjustmentFactor, err = getAdjustmentFactor(priority) if err != nil { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } @@ -328,7 +328,7 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) ( var bufferAdjustment float64 bufferAdjustment, err = getCongestionFactor(congestionClassification) if err != nil { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } @@ -339,8 +339,8 @@ 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 !errors.Is(err, BlockFetchingErr) { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + } else if !errors.Is(err, ErrBlockFetching) { + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } else { L.Debug(). @@ -565,7 +565,7 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a })) if retryErr != nil { - err = fmt.Errorf("%w: %v", GasEstimationErr, retryErr) + err = fmt.Errorf("%w: %v", ErrGasEstimation, retryErr) return } @@ -577,7 +577,7 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a var adjustmentFactor float64 adjustmentFactor, err = getAdjustmentFactor(priority) if err != nil { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } @@ -605,15 +605,15 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a var bufferAdjustment float64 bufferAdjustment, err = getCongestionFactor(congestionClassification) if err != nil { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } // Calculate and apply the buffer. bufferedGasPriceFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedGasPrice), big.NewFloat(bufferAdjustment)) adjustedGasPrice, _ = bufferedGasPriceFloat.Int(nil) - } else if !errors.Is(err, BlockFetchingErr) { - err = fmt.Errorf("%w: %v", GasEstimationErr, err) + } else if !errors.Is(err, ErrBlockFetching) { + err = fmt.Errorf("%w: %v", ErrGasEstimation, err) return } else { L.Debug(). diff --git a/seth/gas_adjuster_test.go b/seth/gas_adjuster_test.go new file mode 100644 index 000000000..479ade9c3 --- /dev/null +++ b/seth/gas_adjuster_test.go @@ -0,0 +1,393 @@ +package seth + +import ( + "math" + "math/big" + "testing" +) + +// TestCalculateMagnitudeDifference tests the magnitude difference calculation +func TestGasAdjuster_CalculateMagnitudeDifference(t *testing.T) { + tests := []struct { + name string + first *big.Float + second *big.Float + expectedDiff int + expectedText string + }{ + { + name: "First much larger (5 orders)", + first: big.NewFloat(100_000_000_000), // 100 gwei + second: big.NewFloat(1_000_000), // 0.001 gwei + expectedDiff: 5, + expectedText: "5 orders of magnitude larger", + }, + { + name: "First much smaller (4 orders)", + first: big.NewFloat(1_000_000), // 0.001 gwei + second: big.NewFloat(10_000_000_000), // 10 gwei + expectedDiff: -4, + expectedText: "4 orders of magnitude smaller", + }, + { + name: "Similar magnitude (within 1 order)", + first: big.NewFloat(30_000_000_000), // 30 gwei + second: big.NewFloat(2_000_000_000), // 2 gwei + expectedDiff: 0, + expectedText: "the same order of magnitude", + }, + { + name: "Exactly 3 orders larger", + first: big.NewFloat(1_000_000_000), // 1 gwei + second: big.NewFloat(1_000_000), // 0.001 gwei + expectedDiff: 3, + expectedText: "3 orders of magnitude larger", + }, + { + name: "Exactly 3 orders smaller", + first: big.NewFloat(1_000_000), // 0.001 gwei + second: big.NewFloat(1_000_000_000), // 1 gwei + expectedDiff: -3, + expectedText: "3 orders of magnitude smaller", + }, + { + name: "First is zero", + first: big.NewFloat(0), + second: big.NewFloat(1_000_000), + expectedDiff: -0, + expectedText: "infinite orders of magnitude smaller", + }, + { + name: "Second is zero", + first: big.NewFloat(1_000_000), + second: big.NewFloat(0), + expectedDiff: -0, + expectedText: "infinite orders of magnitude larger", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + diff, text := calculateMagnitudeDifference(tt.first, tt.second) + + if diff != tt.expectedDiff { + t.Errorf("calculateMagnitudeDifference() diff = %v, want %v", diff, tt.expectedDiff) + } + + if text != tt.expectedText { + t.Errorf("calculateMagnitudeDifference() text = %v, want %v", text, tt.expectedText) + } + }) + } +} + +// TestFeeEqualizerLogic tests the Fee Equalizer experimental logic +// WARNING: This feature should ONLY be used on testnets, never on mainnet! +// It can cause massive overpayment or transaction failures on production networks. +func TestGasAdjuster_FeeEqualizerLogic(t *testing.T) { + tests := []struct { + name string + baseFee int64 // wei + tip int64 // wei + expectedBaseFee int64 // wei after adjustment + expectedTip int64 // wei after adjustment + shouldAdjustBaseFee bool + shouldAdjustTip bool + description string + }{ + { + name: "Base fee MUCH smaller than tip (testnet scenario)", + baseFee: 1_000_000, // 0.001 gwei + tip: 5_000_000_000, // 5 gwei + expectedBaseFee: 5_000_000_000, // Should be raised to tip value + expectedTip: 5_000_000_000, // No change + shouldAdjustBaseFee: true, + shouldAdjustTip: false, + description: "Testnet with very low base fee, high suggested tip - raise base fee to avoid tx failure", + }, + { + name: "Base fee MUCH larger than tip (congested testnet)", + baseFee: 100_000_000_000, // 100 gwei + tip: 1_000_000, // 0.001 gwei + expectedBaseFee: 100_000_000_000, // No change + expectedTip: 100_000_000_000, // Should be raised to base fee value + shouldAdjustBaseFee: false, + shouldAdjustTip: true, + description: "High congestion with tiny tip - raise tip to match base fee", + }, + { + name: "Similar magnitude - no adjustment needed", + baseFee: 30_000_000_000, // 30 gwei + tip: 2_000_000_000, // 2 gwei + expectedBaseFee: 30_000_000_000, // No change + expectedTip: 2_000_000_000, // No change + shouldAdjustBaseFee: false, + shouldAdjustTip: false, + description: "Both values are within acceptable range, no adjustment", + }, + { + name: "Exactly at threshold - 3 orders difference (smaller)", + baseFee: 1_000_000, // 0.001 gwei + tip: 1_000_000_000, // 1 gwei + expectedBaseFee: 1_000_000, // No change (exactly 3 orders, not >3) + expectedTip: 1_000_000_000, // No change + shouldAdjustBaseFee: false, + shouldAdjustTip: false, + description: "Exactly 3 orders of magnitude difference - no adjustment at boundary", + }, + { + name: "Exactly at threshold - 3 orders difference (larger)", + baseFee: 1_000_000_000, // 1 gwei + tip: 1_000_000, // 0.001 gwei + expectedBaseFee: 1_000_000_000, // No change (exactly 3 orders, not >3) + expectedTip: 1_000_000, // No change + shouldAdjustBaseFee: false, + shouldAdjustTip: false, + description: "Exactly 3 orders of magnitude difference - no adjustment at boundary", + }, + { + name: "Slightly over threshold - 4 orders (smaller)", + baseFee: 100_000, // 0.0001 gwei + tip: 1_000_000_000, // 1 gwei + expectedBaseFee: 1_000_000_000, // Adjusted + expectedTip: 1_000_000_000, // No change + shouldAdjustBaseFee: true, + shouldAdjustTip: false, + description: "4 orders of magnitude smaller - triggers adjustment", + }, + { + name: "Slightly over threshold - 4 orders (larger)", + baseFee: 1_000_000_000, // 1 gwei + tip: 100_000, // 0.0001 gwei + expectedBaseFee: 1_000_000_000, // No change + expectedTip: 1_000_000_000, // Adjusted + shouldAdjustBaseFee: false, + shouldAdjustTip: true, + description: "4 orders of magnitude larger - triggers adjustment", + }, + { + name: "Base fee is zero (edge case)", + baseFee: 0, + tip: 1_000_000_000, // 1 gwei + expectedBaseFee: 1_000_000_000, // Should use tip + expectedTip: 1_000_000_000, // No change + shouldAdjustBaseFee: true, + shouldAdjustTip: false, + description: "Zero base fee - use tip as base fee", + }, + { + name: "Tip is zero (edge case)", + baseFee: 1_000_000_000, // 1 gwei + tip: 0, + expectedBaseFee: 1_000_000_000, // No change + expectedTip: 1_000_000_000, // Should use base fee + shouldAdjustBaseFee: false, + shouldAdjustTip: true, + description: "Zero tip - use base fee as tip", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Simulate the Fee Equalizer logic + baseFee64 := float64(tt.baseFee) + currentGasTip := big.NewInt(tt.tip) + + baseFeeTipMagnitudeDiff, _ := calculateMagnitudeDifference( + big.NewFloat(baseFee64), + new(big.Float).SetInt(currentGasTip), + ) + + // Track if adjustments were made + baseFeeAdjusted := false + tipAdjusted := false + + // Apply the Fee Equalizer logic + if baseFeeTipMagnitudeDiff == -0 { + if baseFee64 == 0.0 { + baseFee64 = float64(currentGasTip.Int64()) + baseFeeAdjusted = true + } else { + currentGasTip = big.NewInt(int64(baseFee64)) + tipAdjusted = true + } + } else if baseFeeTipMagnitudeDiff < -3 { + // Base fee is MUCH SMALLER than tip (more than 3 orders of magnitude) + baseFee64 = float64(currentGasTip.Int64()) + baseFeeAdjusted = true + } else if baseFeeTipMagnitudeDiff > 3 { + // Base fee is MUCH LARGER than tip (more than 3 orders of magnitude) + currentGasTip = big.NewInt(int64(baseFee64)) + tipAdjusted = true + } + + // Verify results + resultBaseFee := int64(baseFee64) + resultTip := currentGasTip.Int64() + + if resultBaseFee != tt.expectedBaseFee { + t.Errorf("Base fee after adjustment = %d, want %d\nDescription: %s", + resultBaseFee, tt.expectedBaseFee, tt.description) + } + + if resultTip != tt.expectedTip { + t.Errorf("Tip after adjustment = %d, want %d\nDescription: %s", + resultTip, tt.expectedTip, tt.description) + } + + if baseFeeAdjusted != tt.shouldAdjustBaseFee { + t.Errorf("Base fee adjustment = %v, want %v\nDescription: %s", + baseFeeAdjusted, tt.shouldAdjustBaseFee, tt.description) + } + + if tipAdjusted != tt.shouldAdjustTip { + t.Errorf("Tip adjustment = %v, want %v\nDescription: %s", + tipAdjusted, tt.shouldAdjustTip, tt.description) + } + }) + } +} + +// TestFeeEqualizerDisasterScenarios tests scenarios that would be catastrophic on mainnet +// These tests document WHY this feature should NEVER be used on production networks +func TestGasAdjuster_FeeEqualizerDisasterScenarios(t *testing.T) { + tests := []struct { + name string + baseFee int64 + tip int64 + network string + disasterOutcome string + }{ + { + name: "Ethereum mainnet high gas", + baseFee: 100_000_000_000, // 100 gwei (typical high congestion) + tip: 1_000_000, // 0.001 gwei (would never happen naturally) + network: "Ethereum Mainnet", + disasterOutcome: "Tip raised to 100 gwei - would cost $100+ per transaction!", + }, + { + name: "Polygon PoS normal operation", + baseFee: 30_000_000_000, // 30 gwei + tip: 100_000, // Very low tip + network: "Polygon", + disasterOutcome: "Massive overpayment for network that normally costs fractions of a cent", + }, + { + name: "Arbitrum during congestion", + baseFee: 500_000_000_000, // 500 gwei + tip: 1_000_000, // 0.001 gwei + network: "Arbitrum", + disasterOutcome: "Tip becomes 500 gwei - would bankrupt users on a rollup!", + }, + } + + t.Log("⚠️ WARNING: Fee Equalizer Disaster Scenarios ⚠️") + t.Log("These scenarios demonstrate why Fee Equalizer should NEVER be enabled on mainnet!") + t.Log("") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + baseFee64 := float64(tt.baseFee) + currentGasTip := big.NewInt(tt.tip) + + baseFeeTipMagnitudeDiff, diffText := calculateMagnitudeDifference( + big.NewFloat(baseFee64), + new(big.Float).SetInt(currentGasTip), + ) + + originalTip := currentGasTip.Int64() + + // Apply Fee Equalizer logic + if baseFeeTipMagnitudeDiff > 3 { + currentGasTip = big.NewInt(int64(baseFee64)) + } + + resultTip := currentGasTip.Int64() + overpaymentMultiplier := float64(resultTip) / float64(originalTip) + + t.Logf("Network: %s", tt.network) + t.Logf("Original Base Fee: %d wei (%.2f gwei)", tt.baseFee, float64(tt.baseFee)/1_000_000_000) + t.Logf("Original Tip: %d wei (%.6f gwei)", originalTip, float64(originalTip)/1_000_000_000) + t.Logf("Magnitude Difference: %s", diffText) + t.Logf("Adjusted Tip: %d wei (%.2f gwei)", resultTip, float64(resultTip)/1_000_000_000) + t.Logf("Overpayment Multiplier: %.0fx", overpaymentMultiplier) + t.Logf("Disaster Outcome: %s", tt.disasterOutcome) + t.Logf("") + + // Assert that this would be a disaster + if overpaymentMultiplier > 1000 { + t.Logf("✗ DISASTER: This would cause >1000x overpayment on %s!", tt.network) + } + }) + } +} + +// TestGasAdjusterBoundaryConditions tests edge cases around the 3 order of magnitude threshold +func TestGasAdjuster_FeeEqualizerBoundaryConditions(t *testing.T) { + tests := []struct { + name string + baseFee float64 + tip float64 + expectedAdjustment string + }{ + { + name: "Exactly 3.0 orders (1000x) - no adjustment", + baseFee: 1_000_000_000.0, // 1 gwei + tip: 1_000_000.0, // 0.001 gwei + expectedAdjustment: "none", + }, + { + name: "Just over 3 orders (3.01) - should adjust", + baseFee: 1_023_000_000.0, // 1.023 gwei + tip: 1_000_000.0, // 0.001 gwei + expectedAdjustment: "tip", + }, + { + name: "Just under 3 orders (2.99) - no adjustment", + baseFee: 977_000_000.0, // 0.977 gwei + tip: 1_000_000.0, // 0.001 gwei + expectedAdjustment: "none", + }, + { + name: "Negative: Exactly -3.0 orders - no adjustment", + baseFee: 1_000_000.0, // 0.001 gwei + tip: 1_000_000_000.0, // 1 gwei + expectedAdjustment: "none", + }, + { + name: "Negative: Just over -3 orders (-3.01) - should adjust", + baseFee: 977_000.0, // 0.000977 gwei + tip: 1_000_000_000.0, // 1 gwei + expectedAdjustment: "baseFee", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + baseFee64 := tt.baseFee + currentGasTip := big.NewInt(int64(tt.tip)) + + diff, _ := calculateMagnitudeDifference( + big.NewFloat(baseFee64), + new(big.Float).SetInt(currentGasTip), + ) + + actualAdjustment := "none" + if diff < -3 { + actualAdjustment = "baseFee" + } else if diff > 3 { + actualAdjustment = "tip" + } + + magnitudeDiff := math.Log10(baseFee64) - math.Log10(tt.tip) + t.Logf("Magnitude difference: %.4f orders (threshold: ±3.0)", magnitudeDiff) + t.Logf("Integer diff: %d", diff) + t.Logf("Expected adjustment: %s", tt.expectedAdjustment) + t.Logf("Actual adjustment: %s", actualAdjustment) + + if actualAdjustment != tt.expectedAdjustment { + t.Errorf("Adjustment = %v, want %v", actualAdjustment, tt.expectedAdjustment) + } + }) + } +} From 6f36cf93a10536c884c91ff4d8acab6f6f02c9e1 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 16:01:44 +0200 Subject: [PATCH 4/8] fix test --- seth/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seth/client_test.go b/seth/client_test.go index 8425e4bb3..c842c7f0c 100644 --- a/seth/client_test.go +++ b/seth/client_test.go @@ -40,7 +40,7 @@ func TestRPCHealthCheckEnabled_Node_Unhealthy(t *testing.T) { _, err = seth.NewClientWithConfig(cfg) require.Error(t, err, "expected error when connecting to unhealthy node") - require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: insufficient funds for gas * price + value", "expected error message when connecting to dead node") + require.Contains(t, err.Error(), "RPC health check failed: failed to send transaction to network: Insufficient funds for gas", "expected error message when connecting to dead node") } func TestRPCHealthCheckDisabled_Node_Unhealthy(t *testing.T) { From 22bbb28589c6c2d7bf23cab3ebb7e6f749545218 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 16:20:50 +0200 Subject: [PATCH 5/8] add comment explaining defer close(ch) --- seth/gas_adjuster.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 7d590d854..5f5883d47 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -98,7 +98,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy var wg sync.WaitGroup dataCh := make(chan *types.Header) - defer close(dataCh) + defer close(dataCh) // defer in case something panics, once it is closed then draining goroutine will exit go func() { for header := range dataCh { From 0fd0912c87ee54d82e76ae80e49fca0a085b4e85 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 16:26:24 +0200 Subject: [PATCH 6/8] attempt decoding even without ABI --- seth/decode.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/seth/decode.go b/seth/decode.go index 26b41cfb7..0dd47f17f 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -191,11 +191,6 @@ func (m *Client) DecodeTx(tx *types.Transaction) (*DecodedTransaction, error) { Msg("No post-decode hook found. Skipping") } - if decodeErr != nil && errors.Is(decodeErr, ErrNoABIMethod) { - m.handleTxDecodingError(l, *decoded, decodeErr) - return decoded, revertErr - } - if m.Cfg.TracingLevel == TracingLevel_None { m.handleDisabledTracing(l, *decoded) return decoded, revertErr From 0879186d02f9aaae4ff9da195e2fcec0b47ea214 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 16:45:59 +0200 Subject: [PATCH 7/8] fix order of magnitude --- seth/gas_adjuster.go | 8 ++++-- seth/gas_adjuster_test.go | 55 ++++++++++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 5f5883d47..c8691390d 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -787,12 +787,16 @@ func calculateMagnitudeDifference(first, second *big.Float) (int, string) { secondOrderOfMagnitude := math.Log10(secondFloat) diff := firstOrderOfMagnitude - secondOrderOfMagnitude + absDiff := math.Abs(diff) + + // Values within the same order of magnitude (less than 10x difference) + if absDiff < 1 { + return 0, "the same order of magnitude" + } if diff < 0 { intDiff := math.Floor(diff) return int(intDiff), fmt.Sprintf("%d orders of magnitude smaller", int(math.Abs(intDiff))) - } else if diff > 0 && diff <= 1 { - return 0, "the same order of magnitude" } intDiff := int(math.Ceil(diff)) diff --git a/seth/gas_adjuster_test.go b/seth/gas_adjuster_test.go index 479ade9c3..e38d32160 100644 --- a/seth/gas_adjuster_test.go +++ b/seth/gas_adjuster_test.go @@ -32,10 +32,45 @@ func TestGasAdjuster_CalculateMagnitudeDifference(t *testing.T) { { name: "Similar magnitude (within 1 order)", first: big.NewFloat(30_000_000_000), // 30 gwei - second: big.NewFloat(2_000_000_000), // 2 gwei + second: big.NewFloat(31_000_000_000), // 31 gwei expectedDiff: 0, expectedText: "the same order of magnitude", }, + { + name: "Similar magnitude (within 1 order)", + first: big.NewFloat(100_000_000_000), // 100 gwei + second: big.NewFloat(99_999_999_999), // 99.999999999 gwei + expectedDiff: 0, + expectedText: "the same order of magnitude", + }, + { + name: "Similar magnitude (within 1 order)", + first: big.NewFloat(30_000_000_000), // 30 gwei + second: big.NewFloat(99_999_999_999), // 99.999999999 gwei + expectedDiff: 0, + expectedText: "the same order of magnitude", + }, + { + name: "Similar magnitude (within 1 order)", + first: big.NewFloat(99_999_999_999), // 99.999999999 gwei + second: big.NewFloat(30_000_000_000), // 30 gwei + expectedDiff: 0, + expectedText: "the same order of magnitude", + }, + { + name: "Similar magnitude (within 1 order)", + first: big.NewFloat(99_999_999_999), // 99.999999999 gwei + second: big.NewFloat(100_000_000_000), // 100 gwei + expectedDiff: 0, + expectedText: "the same order of magnitude", + }, + { + name: "Just under 1 order of magnitude (same order)", + first: big.NewFloat(9_999_999_999), // 9.999... gwei + second: big.NewFloat(1_000_000_000), // 1 gwei + expectedDiff: 0, // Still same order (diff = 0.9999...) + expectedText: "the same order of magnitude", + }, { name: "Exactly 3 orders larger", first: big.NewFloat(1_000_000_000), // 1 gwei @@ -71,11 +106,13 @@ func TestGasAdjuster_CalculateMagnitudeDifference(t *testing.T) { diff, text := calculateMagnitudeDifference(tt.first, tt.second) if diff != tt.expectedDiff { - t.Errorf("calculateMagnitudeDifference() diff = %v, want %v", diff, tt.expectedDiff) + t.Errorf("calculateMagnitudeDifference(first=%s wei, second=%s wei)\n diff = %v, want %v", + tt.first.Text('f', 0), tt.second.Text('f', 0), diff, tt.expectedDiff) } if text != tt.expectedText { - t.Errorf("calculateMagnitudeDifference() text = %v, want %v", text, tt.expectedText) + t.Errorf("calculateMagnitudeDifference(first=%s wei, second=%s wei)\n text = %q, want %q", + tt.first.Text('f', 0), tt.second.Text('f', 0), text, tt.expectedText) } }) } @@ -226,13 +263,17 @@ func TestGasAdjuster_FeeEqualizerLogic(t *testing.T) { resultTip := currentGasTip.Int64() if resultBaseFee != tt.expectedBaseFee { - t.Errorf("Base fee after adjustment = %d, want %d\nDescription: %s", - resultBaseFee, tt.expectedBaseFee, tt.description) + t.Errorf("Base fee after adjustment = %d wei (%.4f gwei), want %d wei (%.4f gwei)\nDescription: %s", + resultBaseFee, float64(resultBaseFee)/1e9, + tt.expectedBaseFee, float64(tt.expectedBaseFee)/1e9, + tt.description) } if resultTip != tt.expectedTip { - t.Errorf("Tip after adjustment = %d, want %d\nDescription: %s", - resultTip, tt.expectedTip, tt.description) + t.Errorf("Tip after adjustment = %d wei (%.4f gwei), want %d wei (%.4f gwei)\nDescription: %s", + resultTip, float64(resultTip)/1e9, + tt.expectedTip, float64(tt.expectedTip)/1e9, + tt.description) } if baseFeeAdjusted != tt.shouldAdjustBaseFee { From 10bc30897ab46a97ae5aff7266e9f571c5c2ac05 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Fri, 24 Oct 2025 17:04:36 +0200 Subject: [PATCH 8/8] remove unused function --- seth/decode.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/seth/decode.go b/seth/decode.go index 0dd47f17f..ba50f9dd6 100644 --- a/seth/decode.go +++ b/seth/decode.go @@ -258,32 +258,6 @@ func (m *Client) waitUntilMined(l zerolog.Logger, tx *types.Transaction) (*types return tx, receipt, nil } -func (m *Client) handleTxDecodingError(l zerolog.Logger, decoded DecodedTransaction, decodeErr error) { - tx := decoded.Transaction - - if m.Cfg.hasOutput(TraceOutput_JSON) { - l.Trace(). - Err(decodeErr). - Msg("Failed to decode transaction. Saving transaction data hash as JSON") - - err := CreateOrAppendToJsonArray(m.Cfg.revertedTransactionsFile, tx.Hash().Hex()) - if err != nil { - l.Warn(). - Err(err). - Str("TXHash", tx.Hash().Hex()). - Msg("Failed to save reverted transaction hash to file") - } else { - l.Trace(). - Str("TXHash", tx.Hash().Hex()). - Msg("Saved reverted transaction to file") - } - } - - if m.Cfg.hasOutput(TraceOutput_Console) { - m.printDecodedTXData(l, &decoded) - } -} - func (m *Client) handleTracingError(l zerolog.Logger, decoded DecodedTransaction, traceErr, revertErr error) { if m.Cfg.hasOutput(TraceOutput_JSON) { l.Trace().