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(¤tArg.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(¤tArg.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 }