@@ -320,14 +320,12 @@ func (a ABI) ParseError(revertData []byte) (*Entry, *ComponentValue, bool) {
320320 return a .ParseErrorCtx (context .Background (), revertData )
321321}
322322
323- // Returns the components value from the parsed error
323+ // ParseErrorCtx returns the matched Entry and decoded ComponentValue from the
324+ // given revert data. The ABI's error entries are tried first, followed by the
325+ // built-in Error(string) and Panic(uint256).
324326func (a ABI ) ParseErrorCtx (ctx context.Context , revertData []byte ) (* Entry , * ComponentValue , bool ) {
325- // Always include the default error
326- a = append (ABI {
327- {Type : Error , Name : "Error" , Inputs : ParameterArray {{Name : "reason" , Type : "string" }}},
328- }, a ... )
329- for _ , e := range a {
330- if e .Type == Error {
327+ for _ , source := range []ABI {a .errors (), defaultErrorEntries } {
328+ for _ , e := range source {
331329 if cv , err := e .DecodeCallDataCtx (ctx , revertData ); err == nil {
332330 return e , cv , true
333331 }
@@ -349,103 +347,24 @@ func (a ABI) ErrorStringCtx(ctx context.Context, revertData []byte) (strError st
349347 return strError , ok
350348}
351349
352- const maxNestedRevertDepth = 10
353-
354350// UnwrapErrorStringCtx is like ErrorStringCtx but handles nested errors caused by
355- // Solidity contracts that catch a revert and re-throw using string(reason). This
356- // embeds raw ABI-encoded error data (including null bytes from ABI padding) inside
357- // the new Error(string). The function scans for all known error selectors,
358- // recursively decodes nested Error(string) chains, and formats known custom errors.
359- // Any undecoded binary data is hex-encoded.
351+ // Solidity contracts that catch a revert and re-throw using string(reason).
352+ // Delegates to DecodeRevertErrorCtx for structured decoding, then formats the
353+ // result as a human-readable string.
360354func (a ABI ) UnwrapErrorStringCtx (ctx context.Context , revertData []byte ) (string , bool ) {
361- e , cv , ok := a .ParseErrorCtx (ctx , revertData )
362- if ! ok {
355+ r := a .DecodeRevertErrorCtx (ctx , revertData )
356+ if r == nil {
363357 return "" , false
364358 }
365- // For Error(string), unwrap any nested errors inside the decoded string
366- if e .Name == "Error" && len (cv .Children ) == 1 {
367- if strVal , ok := cv .Children [0 ].Value .(string ); ok {
368- return unwrapNestedRevertReasons (ctx , a , strVal , 0 ), true
369- }
370- }
371- // For other error types, format directly
372- strError := FormatErrorStringCtx (ctx , e , cv )
373- return strError , strError != ""
359+ s := r .String ()
360+ return s , s != ""
374361}
375362
376363// UnwrapErrorString is a convenience wrapper for UnwrapErrorStringCtx.
377364func (a ABI ) UnwrapErrorString (revertData []byte ) (string , bool ) {
378365 return a .UnwrapErrorStringCtx (context .Background (), revertData )
379366}
380367
381- // unwrapNestedRevertReasons recursively decodes Error(string) values that contain
382- // embedded ABI-encoded error data from Solidity catch-and-rethrow patterns.
383- func unwrapNestedRevertReasons (ctx context.Context , a ABI , s string , depth int ) string {
384- if depth >= maxNestedRevertDepth {
385- return SanitizeBinaryString ([]byte (s ))
386- }
387-
388- raw := []byte (s )
389-
390- // Build a lookup map of 4-byte selectors so we can scan the string once
391- type selectorKey = [4 ]byte
392- selectors := make (map [selectorKey ]* Entry )
393- errorsWithDefault := append (ABI {
394- {Type : Error , Name : "Error" , Inputs : ParameterArray {{Name : "reason" , Type : "string" }}},
395- }, a ... )
396- for _ , e := range errorsWithDefault {
397- if e .Type != Error {
398- continue
399- }
400- sel := e .FunctionSelectorBytes ()
401- if len (sel ) >= 4 {
402- var key selectorKey
403- copy (key [:], sel [:4 ])
404- if _ , exists := selectors [key ]; ! exists {
405- selectors [key ] = e
406- }
407- }
408- }
409-
410- // Single pass: walk through the bytes looking for any known selector
411- bestIdx := - 1
412- var bestEntry * Entry
413- if len (raw ) >= 4 {
414- for i := 0 ; i <= len (raw )- 4 ; i ++ {
415- var key selectorKey
416- copy (key [:], raw [i :i + 4 ])
417- if e , ok := selectors [key ]; ok {
418- bestIdx = i
419- bestEntry = e
420- break
421- }
422- }
423- }
424-
425- if bestIdx < 0 {
426- return SanitizeBinaryString (raw )
427- }
428-
429- prefix := SanitizeBinaryString (raw [:bestIdx ])
430- embedded := raw [bestIdx :]
431-
432- cv , err := bestEntry .DecodeCallDataCtx (ctx , embedded )
433- if err == nil {
434- if bestEntry .Name == "Error" && len (cv .Children ) == 1 {
435- if nested , ok := cv .Children [0 ].Value .(string ); ok {
436- return prefix + unwrapNestedRevertReasons (ctx , a , nested , depth + 1 )
437- }
438- }
439- formatted := FormatErrorStringCtx (ctx , bestEntry , cv )
440- if formatted != "" {
441- return prefix + formatted
442- }
443- }
444-
445- log .L (ctx ).Debugf ("Could not decode nested revert at depth %d, hex-encoding remaining %d bytes" , depth , len (embedded ))
446- return prefix + "0x" + hex .EncodeToString (embedded )
447- }
448-
449368// SanitizeBinaryString returns the input as a text string if it is entirely
450369// printable ASCII, or hex-encodes the entire input otherwise. This ensures the
451370// output is always safe for database TEXT columns and human-readable logging.
0 commit comments