Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d83f75c
Add utility function to attempt to decode nested errors from reverts
davecrighton Feb 27, 2026
ca9568a
remove duplication
davecrighton Feb 27, 2026
4cfdcaa
Do a single efficient pass on string instead of a pass per selector
davecrighton Feb 27, 2026
66d919f
dont checkin allow for local rebuild
davecrighton Mar 2, 2026
4309585
Create structs to represent nested errors
davecrighton Apr 14, 2026
3cd7701
Add parsing methods for simple (non nested cases)
davecrighton Apr 14, 2026
84f3ed0
Add unwrapping functionality for revert errors
davecrighton Apr 14, 2026
63dfb28
Move unwrap error to use new method
davecrighton Apr 14, 2026
d6fcfb9
Add method for getting error string without unwrapping for backwards …
davecrighton Apr 14, 2026
ae0074e
go mod updates
davecrighton Apr 15, 2026
fae3fbe
Rename Nested to Cause to better encapsulate meaning
davecrighton Apr 16, 2026
b971489
Update go.sum
davecrighton Apr 16, 2026
7a8f952
Clarify some comments
davecrighton Apr 16, 2026
0f33fb8
go mod updates
davecrighton Apr 16, 2026
41816c0
Update to use InnerError as spelling
davecrighton Apr 21, 2026
d9ab824
Expose selectorMap as an external and introduce FilterType helper
davecrighton Apr 21, 2026
3536d3b
Use existing helper methods to encode test data
davecrighton Apr 21, 2026
46f1c0d
Align migration case on single call signature
davecrighton Apr 21, 2026
4b81bbb
Cap scan length to 1024 ensure that string is long enough to contain …
davecrighton Apr 21, 2026
0c389ec
Add tests for assembly revert pattern
davecrighton Apr 21, 2026
77652c2
Update DecodeRevertErrorCtx to use ErrorFormatOptions
davecrighton Apr 21, 2026
26efec7
Rename option and update description
davecrighton Apr 21, 2026
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: 4 additions & 0 deletions internal/signermsgs/en_field_descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ var (
EIP712ResultR = ffm("EIP712Result.r", "The R value of the ECDSA signature as a 32byte hex encoded array")
EIP712ResultS = ffm("EIP712Result.s", "The S value of the ECDSA signature as a 32byte hex encoded array")

RevertErrorErrorEntry = ffm("RevertError.errorEntry", "The matched ABI error entry at this level of the error chain")
RevertErrorPrefix = ffm("RevertError.prefix", "The readable text prefix before the nested inner error, extracted from the outer Error(string) value")
RevertErrorInnerError = ffm("RevertError.innerError", "The recursively decoded inner error, forming the next level of the error chain, or nil if this is the leaf")

TypedDataDomain = ffm("TypedData.domain", "The data to encode into the EIP712Domain as part fo signing the transaction")
TypedDataMessage = ffm("TypedData.message", "The data to encode into primaryType structure, with nested values for any sub-structures")
TypedDataTypes = ffm("TypedData.types", "Array of types to use when encoding, which must include the primaryType and the EIP712Domain (noting the primary type can be EIP712Domain if the message is empty)")
Expand Down
109 changes: 94 additions & 15 deletions pkg/abi/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,19 +315,53 @@ func (a ABI) Errors() map[string]*Entry {
return m
}

// FilterType returns a new ABI containing only the entries whose Type matches t,
// preserving their original order. Returns nil if no entries match.
// Use this to narrow an ABI before passing it to SelectorMap or other helpers,
// e.g. a.FilterType(Error) to obtain only error definitions.
func (a ABI) FilterType(t EntryType) ABI {
var out ABI
for _, e := range a {
if e.Type == t {
out = append(out, e)
}
}
return out
}

// SelectorMap builds a map from 4-byte ABI selectors to their corresponding
// entries. When two entries in the ABI produce the same 4-byte selector, the
// first one wins. Entries for which selector generation fails are silently
// skipped.
//
// SelectorMap operates on all entry types — combine with FilterType to restrict
// to a specific type, e.g. a.FilterType(Error).SelectorMap().
func (a ABI) SelectorMap() map[[4]byte]*Entry {
m := make(map[[4]byte]*Entry)
for _, e := range a {
sel := e.FunctionSelectorBytes()
if len(sel) >= 4 {
var key [4]byte
copy(key[:], sel[:4])
if _, exists := m[key]; !exists {
m[key] = e
}
}
}
return m
}

// Returns the components value from the parsed error
func (a ABI) ParseError(revertData []byte) (*Entry, *ComponentValue, bool) {
return a.ParseErrorCtx(context.Background(), revertData)
}

// Returns the components value from the parsed error
// ParseErrorCtx returns the matched Entry and decoded ComponentValue from the
// given revert data. The ABI's error entries are tried first, followed by the
// built-in Error(string) and Panic(uint256).
func (a ABI) ParseErrorCtx(ctx context.Context, revertData []byte) (*Entry, *ComponentValue, bool) {
// Always include the default error
a = append(ABI{
{Type: Error, Name: "Error", Inputs: ParameterArray{{Name: "reason", Type: "string"}}},
}, a...)
for _, e := range a {
if e.Type == Error {
for _, source := range []ABI{a.FilterType(Error), defaultErrorEntries} {
for _, e := range source {
if cv, err := e.DecodeCallDataCtx(ctx, revertData); err == nil {
return e, cv, true
}
Expand All @@ -336,20 +370,65 @@ func (a ABI) ParseErrorCtx(ctx context.Context, revertData []byte) (*Entry, *Com
return nil, nil, false
}

func (a ABI) ErrorString(revertData []byte) (string, bool) {
return a.ErrorStringCtx(context.Background(), revertData)
// ErrorFormatOption configures the behaviour of ErrorString and ErrorStringCtx.
type ErrorFormatOption struct {
// There is a pattern used in some smart contracts, where a string error is used
// to embed the raw bytes of an ABI encoded sub-error.
// While uncommon, this pattern is supported by the library if you set this switch.
// When set, every standard revert `Error(string)` error will be traversed to look for
// eyecatcher (4 byte signatures) of other errors binary encoded within the string.
SearchForWrappedBinaryErrors bool
}

// ErrorString formats raw EVM revert data as a human-readable string.
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to decode inner errors
// that are binary-encoded within an Error(string) value.
func (a ABI) ErrorString(revertData []byte, options ...ErrorFormatOption) (string, bool) {
return a.ErrorStringCtx(context.Background(), revertData, options...)
}

// ErrorStringCtx formats raw EVM revert data as a human-readable string.
// The ABI's own error entries are tried first, followed by the built-in
// Error(string) and Panic(uint256).
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to decode inner errors
// that are binary-encoded within an Error(string) value.
func (a ABI) ErrorStringCtx(ctx context.Context, revertData []byte, options ...ErrorFormatOption) (string, bool) {
searchBinary := false
for _, o := range options {
searchBinary = searchBinary || o.SearchForWrappedBinaryErrors
}
if searchBinary {
r := a.DecodeRevertErrorCtx(ctx, revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
if r == nil {
return "", false
}
s := r.String()
return s, s != ""
}
e, cv, ok := a.ParseErrorCtx(ctx, revertData)
if !ok {
return "", false
}
s := FormatErrorStringCtx(ctx, e, cv)
return s, s != ""
}

func (a ABI) ErrorStringCtx(ctx context.Context, revertData []byte) (strError string, ok bool) {
e, cv, ok := a.ParseErrorCtx(ctx, revertData)
if ok {
strError = FormatErrorStringCtx(ctx, e, cv)
ok = strError != ""
// SanitizeBinaryString returns the input as a text string if it is entirely
// printable ASCII, or hex-encodes the entire input otherwise. This ensures the
// output is always safe for database TEXT columns and human-readable logging.
func SanitizeBinaryString(raw []byte) string {
for _, b := range raw {
if b < 32 || b >= 127 {
return "0x" + hex.EncodeToString(raw)
}
}
return strError, ok
return string(raw)
}

func FormatErrorStringCtx(ctx context.Context, e *Entry, cv *ComponentValue) string {
if e == nil || cv == nil {
return ""
}
var ok bool
var parsed []interface{}
if res, err := NewSerializer().
Expand Down
Loading