code.vegaprotocol.io/vega@v0.79.0/core/datasource/external/ethcall/result.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package ethcall
    17  
    18  import (
    19  	"bytes"
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strconv"
    24  	"text/scanner"
    25  
    26  	ethcallcommon "code.vegaprotocol.io/vega/core/datasource/external/ethcall/common"
    27  
    28  	"github.com/PaesslerAG/gval"
    29  	"github.com/PaesslerAG/jsonpath"
    30  )
    31  
    32  type Result struct {
    33  	Bytes         []byte
    34  	Values        []any
    35  	Normalised    map[string]string
    36  	PassesFilters bool
    37  }
    38  
    39  func NewResult(spec ethcallcommon.Spec, bytes []byte) (Result, error) {
    40  	call, err := NewCall(spec)
    41  	if err != nil {
    42  		return Result{}, fmt.Errorf("failed to create result: %w", err)
    43  	}
    44  
    45  	return newResult(call, bytes)
    46  }
    47  
    48  func newResult(call Call, bytes []byte) (Result, error) {
    49  	values, err := call.abi.Unpack(call.method, bytes)
    50  	if err != nil {
    51  		return Result{}, fmt.Errorf("failed to unpack contract call result: %w", err)
    52  	}
    53  
    54  	normalised, err := normaliseValues(values, call.spec.Normalisers)
    55  	if err != nil {
    56  		return Result{}, fmt.Errorf("failed to normalise contract call result: %w", err)
    57  	}
    58  
    59  	passesFilters, err := call.filters.Match(normalised)
    60  	if err != nil {
    61  		return Result{}, fmt.Errorf("error evaluating filters: %w", err)
    62  	}
    63  
    64  	return Result{
    65  		Bytes:         bytes,
    66  		Values:        values,
    67  		Normalised:    normalised,
    68  		PassesFilters: passesFilters,
    69  	}, nil
    70  }
    71  
    72  func normaliseValues(values []any, rules map[string]string) (map[string]string, error) {
    73  	// The data in 'values' is relatively well typed, after being unpacked by the ABI.
    74  	// For example, a uint256 will be a big.Int, structs returned from the contract call
    75  	// will be anonymous go structs rather than a string map etc.. In order to fish data
    76  	// out with jsonpath it need to be simple lists, maps, strings and numbers; so we
    77  	// serialise to json and then deserialise to an into an []interface{}.
    78  	valuesJson, err := json.Marshal(values)
    79  	if err != nil {
    80  		return nil, fmt.Errorf("unable to serialse values: %v", err)
    81  	}
    82  
    83  	valuesSimple := []interface{}{}
    84  	d := json.NewDecoder(bytes.NewBuffer(valuesJson))
    85  	// Keep numbers as a json.Number, which holds the original string representation
    86  	// otherwise all numbers get cast to float64 which is no good for e.g uint256.
    87  	d.UseNumber()
    88  	err = d.Decode(&valuesSimple)
    89  	if err != nil {
    90  		return nil, fmt.Errorf("unable to deserialse values: %v", err)
    91  	}
    92  
    93  	res := make(map[string]string)
    94  
    95  	for key, path := range rules {
    96  		value, err := myJSONPathGet(path, valuesSimple)
    97  		if err != nil {
    98  			return nil, fmt.Errorf("unable to normalise key %v: %v", key, err)
    99  		}
   100  		switch v := value.(type) {
   101  		case json.Number:
   102  			res[key] = v.String()
   103  		case int64:
   104  			// all of the numbers in the json from the ethereum call result will be
   105  			// json.Number and handled above; this case is just for the corner case
   106  			// where someone specifies a number as a static value in the json path itself
   107  			res[key] = strconv.FormatInt(v, 10)
   108  		case string:
   109  			res[key] = v
   110  		case bool:
   111  			res[key] = strconv.FormatBool(v)
   112  		default:
   113  			return nil, fmt.Errorf("unable to normalise key %v of type %T", key, value)
   114  		}
   115  	}
   116  
   117  	return res, nil
   118  }
   119  
   120  // myJSONPathGet works exactly like jsonpath.Get(path, values), except that any numbers found
   121  // are returned as int64 rather than float64 which is the default.
   122  // ** in the path expression itself, not the json being queried **
   123  // Evaluation will fail if numbers in the path are not integers.
   124  func myJSONPathGet(path string, values []interface{}) (interface{}, error) {
   125  	baselang := jsonpath.Language()
   126  	mylang := gval.PrefixExtension(scanner.Int, parseNumberAsInt64)
   127  	lang := gval.NewLanguage(baselang, mylang)
   128  
   129  	eval, err := lang.NewEvaluable(path)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	value, err := eval(context.Background(), values)
   134  	return value, err
   135  }
   136  
   137  func parseNumberAsInt64(c context.Context, p *gval.Parser) (gval.Evaluable, error) {
   138  	n, err := strconv.ParseInt(p.TokenText(), 10, 64)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	return p.Const(n), nil
   143  }