Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/seth-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 5 additions & 3 deletions seth/abi_finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"+
Expand All @@ -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
Expand Down
72 changes: 62 additions & 10 deletions seth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -532,8 +536,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"+
Expand Down Expand Up @@ -989,7 +991,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, 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
}
Expand Down Expand Up @@ -1345,12 +1347,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().
Expand Down Expand Up @@ -1384,6 +1387,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
Expand Down Expand Up @@ -1421,19 +1425,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)
Expand All @@ -1455,12 +1468,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 {
Expand All @@ -1484,6 +1521,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) {
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using reflect.DeepEqual to compare ABIs in a loop (iterating through all stored ABIs) is expensive. Consider using a hash-based lookup or caching ABI names in a map for O(1) lookup instead of O(n) with expensive deep comparisons. This could significantly impact performance when many ABIs are loaded.

Suggested change
if reflect.DeepEqual(storedABI, *targetABI) {
if storedABI == targetABI {

Copilot uses AI. Check for mistakes.
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)
Expand Down
2 changes: 1 addition & 1 deletion seth/client_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func NewClientBuilder() *ClientBuilder {
DialTimeout: MustMakeDuration(DefaultDialTimeout),
TransferGasFee: DefaultTransferGasFee,
GasPriceEstimationEnabled: true,
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Changed from 200 to 20 blocks for gas price estimation. This is a significant change (10x reduction) that could affect gas price accuracy. While this might be intentional to reduce RPC load, it should be documented in the changeset or commit message why this default was changed, as it could impact users relying on the previous default.

Suggested change
GasPriceEstimationEnabled: true,
GasPriceEstimationEnabled: true,
// [NOTE] Changed default from 200 to 20 blocks for gas price estimation to reduce RPC load.
// This may impact gas price accuracy, but was deemed an acceptable tradeoff for performance.

Copilot uses AI. Check for mistakes.
GasPriceEstimationBlocks: 200,
GasPriceEstimationBlocks: 20,
GasPriceEstimationTxPriority: Priority_Standard,
GasPrice: DefaultGasPrice,
GasFeeCap: DefaultGasFeeCap,
Expand Down
119 changes: 65 additions & 54 deletions seth/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +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"`
}

// 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 {
Expand Down Expand Up @@ -179,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, errors.New(ErrNoABIMethod)) {
m.handleTxDecodingError(l, *decoded, decodeErr)
return decoded, revertErr
}

if m.Cfg.TracingLevel == TracingLevel_None {
m.handleDisabledTracing(l, *decoded)
return decoded, revertErr
Expand Down Expand Up @@ -251,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().
Expand Down Expand Up @@ -448,6 +429,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")
Expand All @@ -463,7 +445,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
}
Expand All @@ -475,12 +458,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
Expand All @@ -499,7 +483,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")
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion seth/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" +
Expand All @@ -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})
Expand Down
Loading
Loading