github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/articulate/function.go (about)

     1  package articulate
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    10  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/decode"
    11  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    12  	"github.com/ethereum/go-ethereum/accounts/abi"
    13  	"github.com/ethereum/go-ethereum/common"
    14  )
    15  
    16  func ArticulateFunction(function *types.Function, inputData string, outputData string) (err error) {
    17  	abiMethod, err := function.GetAbiMethod()
    18  	if err != nil {
    19  		return
    20  	}
    21  
    22  	if len(inputData) > 0 {
    23  		if err = articulateArguments(abiMethod.Inputs, inputData, nil, function.Inputs); err != nil {
    24  			return fmt.Errorf("error processing inputs of %s: %w", abiMethod.Sig, err)
    25  		}
    26  	}
    27  
    28  	if len(outputData) > 0 {
    29  		if err = articulateArguments(abiMethod.Outputs, outputData, nil, function.Outputs); err != nil {
    30  			return fmt.Errorf("error processing output of %s: %w", abiMethod.Sig, err)
    31  		}
    32  	}
    33  
    34  	return
    35  }
    36  
    37  func articulateArguments(args abi.Arguments, data string, topics []base.Hash, abiMap []types.Parameter) (err error) {
    38  	dataBytes, err := hex.DecodeString(data)
    39  	if err != nil {
    40  		return
    41  	}
    42  
    43  	// First, we have to construct slices of indexed and non-indexed
    44  	// arguments. We will use data for non-indexed ones and topics[1:]
    45  	// for the indexed.
    46  	// argNameToIndex will help us keep track on the argument index in
    47  	// abiMap, as we will need to update the entry there
    48  	argNameToIndex := make(map[string]int, len(args))
    49  	nameToIndexed := make(map[string]abi.Argument)
    50  	indexed := make(abi.Arguments, 0, len(args))
    51  	nonIndexed := make(abi.Arguments, 0, len(args))
    52  
    53  	// In some cases, `data` can be too short, because only some values are present there.
    54  	// See: https://github.com/TrueBlocks/trueblocks-core/issues/1366
    55  	dataLen := len(data) * 4
    56  	nonIndexedSize := 0
    57  	for _, nonIndexedArg := range args.NonIndexed() {
    58  		nonIndexedSize += nonIndexedArg.Type.Size
    59  	}
    60  
    61  	for i := 0; i < len(args); i++ {
    62  		index := i
    63  		if dataLen < nonIndexedSize {
    64  			fixFalseNonIndexedArgument(&args[i], dataLen)
    65  		}
    66  		arg := args[i]
    67  		argNameToIndex[arg.Name] = index
    68  		if arg.Indexed {
    69  			indexed = append(indexed, arg)
    70  			nameToIndexed[arg.Name] = arg
    71  			continue
    72  		}
    73  		nonIndexed = append(nonIndexed, arg)
    74  	}
    75  
    76  	unpacked, err := args.Unpack(dataBytes)
    77  	if err != nil {
    78  		return fmt.Errorf("error unpacking arguments %w", err)
    79  	}
    80  
    81  	// Set values of non-indexed arguments
    82  	for index, unpackedArg := range unpacked {
    83  		currentArg := nonIndexed[index]
    84  		result, err := formatValue(&currentArg.Type, unpackedArg)
    85  		if err != nil {
    86  			return err
    87  		}
    88  		var abiMapIndex int
    89  		if currentArg.Name != "" {
    90  			var ok bool
    91  			abiMapIndex, ok = argNameToIndex[currentArg.Name]
    92  			if !ok {
    93  				return fmt.Errorf("cannot find abiMap index of argument %s", currentArg.Name)
    94  			}
    95  		} else {
    96  			abiMapIndex = index
    97  		}
    98  		abiMap[abiMapIndex].Value = result
    99  	}
   100  
   101  	// Sometimes there are topics, but no indexed parameters:
   102  	// https://github.com/TrueBlocks/trueblocks-core/issues/1366
   103  	if len(topics) == 0 || len(indexed) == 0 {
   104  		return
   105  	}
   106  
   107  	// Set values of indexed arguments
   108  	out := make(map[string]any, len(indexed))
   109  	tops := []common.Hash{}
   110  	for _, hash := range topics {
   111  		tops = append(tops, hash.Common())
   112  	}
   113  	if err = abi.ParseTopicsIntoMap(out, indexed, tops[1:]); err != nil {
   114  		return err
   115  	}
   116  	for name, value := range out {
   117  		currentArg, ok := nameToIndexed[name]
   118  		if !ok {
   119  			return fmt.Errorf("cannot find indexed argument:%s", name)
   120  		}
   121  		result, err := formatValue(&currentArg.Type, value)
   122  		if err != nil {
   123  			return err
   124  		}
   125  		abiMapIndex, ok := argNameToIndex[currentArg.Name]
   126  		if !ok {
   127  			return fmt.Errorf("cannot find abiMap index of argument %s", currentArg.Name)
   128  		}
   129  		abiMap[abiMapIndex].Value = result
   130  	}
   131  	return
   132  }
   133  
   134  // fixFalseNonIndexedArgument turns false non-indexed argument to indexed one
   135  func fixFalseNonIndexedArgument(arg *abi.Argument, dataLen int) {
   136  	if arg.Indexed {
   137  		return
   138  	}
   139  
   140  	if dataLen == arg.Type.Size {
   141  		return
   142  	}
   143  	arg.Indexed = true
   144  }
   145  
   146  func formatValue(argType *abi.Type, value any) (result any, err error) {
   147  	switch argType.T {
   148  	case abi.TupleTy:
   149  		tuple := reflect.ValueOf(value)
   150  		numField := tuple.NumField()
   151  		articulatedTuple := make(map[string]any, numField)
   152  		for i := 0; i < numField; i++ {
   153  			fieldValue := tuple.Field(i).Interface()
   154  			itemAbiType := argType.TupleElems[i]
   155  			articulatedItem, err := formatValue(itemAbiType, fieldValue)
   156  			if err != nil {
   157  				return "", nil
   158  			}
   159  			articulatedTuple[argType.TupleRawNames[i]] = articulatedItem
   160  		}
   161  		return articulatedTuple, nil
   162  	case abi.ArrayTy:
   163  		fallthrough
   164  	case abi.SliceTy:
   165  		slice := reflect.ValueOf(value)
   166  		articulatedSlice := make([]any, 0, slice.Len())
   167  		for i := 0; i < slice.Len(); i++ {
   168  			item := slice.Index(i).Interface()
   169  			articulatedItem, err := formatValue(argType.Elem, item)
   170  			if err != nil {
   171  				return "", err
   172  			}
   173  			articulatedSlice = append(articulatedSlice, articulatedItem)
   174  		}
   175  		result = articulatedSlice
   176  	case abi.StringTy:
   177  		strValue, ok := value.(string)
   178  		if ok {
   179  			return decode.SanitizeString(strValue), nil
   180  		}
   181  		fallthrough
   182  	case abi.IntTy, abi.UintTy:
   183  		// Because values coming from Ethereum can exceed JSON maximum safe integer, we
   184  		// return all numbers as strings.
   185  		// We could return numbers or strings depending on whether or not the value is safe,
   186  		// but the consumer would then have to check the type of returned value first and only
   187  		// then try to interpet the actual value.
   188  		// In at least some languages, strings can be used to create big Int equivalents, so
   189  		// using strings only should make it easier for the consumers.
   190  		result = fmt.Sprint(value)
   191  	case abi.BoolTy:
   192  		result = value
   193  	case abi.BytesTy:
   194  		result = fmt.Sprintf("0x%x", value)
   195  	case abi.FunctionTy:
   196  		item, ok := value.([]byte)
   197  		if ok {
   198  			result = base.Bytes2Hex(item)
   199  			break
   200  		}
   201  		result = value
   202  	case abi.FixedBytesTy:
   203  		result = articulateFixedBytes(argType, value)
   204  	case abi.AddressTy:
   205  		addr, ok := value.(common.Address)
   206  		if !ok {
   207  			result = fmt.Sprint(value)
   208  			return
   209  		}
   210  		ourAddr := base.HexToAddress(addr.Hex())
   211  		result = ourAddr.Hex()
   212  	case abi.HashTy:
   213  		result = strings.ToLower(fmt.Sprint(value))
   214  	default:
   215  		return "", fmt.Errorf("cannot cast type %s", argType.String())
   216  	}
   217  
   218  	return
   219  }
   220  
   221  func articulateFixedBytes(abiType *abi.Type, data any) string {
   222  	addressLike, ok := data.([20]byte)
   223  	if ok && abiType.Size == 20 {
   224  		addr := base.BytesToAddress(addressLike[:])
   225  		return addr.Hex()
   226  	}
   227  
   228  	hashLike, ok := data.([32]byte)
   229  	if ok && abiType.Size == 32 {
   230  		hash := base.BytesToHash(hashLike[:])
   231  		articulated, ok := decode.ArticulateString(hash.Hex())
   232  		if ok {
   233  			return articulated
   234  		}
   235  		return hash.Hex()
   236  	}
   237  
   238  	return fmt.Sprint(data)
   239  }