diff --git a/core/constants.go b/core/constants.go index 3a954990a..fb7d069aa 100644 --- a/core/constants.go +++ b/core/constants.go @@ -66,3 +66,9 @@ var SwitchClientDuration = 5 * EpochLength // HexReturnType is the ReturnType for a job if that job returns a hex value var HexReturnType = "hex" + +// HexArrayReturnType is the ReturnType for a job if that job returns a hex array value +var HexArrayReturnType = "^hexArray\\[\\d+\\]$" + +// HexArrayExtractIndexRegex will be used as a regular expression to extract index from hexArray return type +var HexArrayExtractIndexRegex = `^hexArray\[(\d+)\]$` diff --git a/utils/asset_test.go b/utils/asset_test.go index 411b69629..98dde03de 100644 --- a/utils/asset_test.go +++ b/utils/asset_test.go @@ -658,11 +658,16 @@ func TestGetDataToCommitFromJob(t *testing.T) { Url: "https://api.gemini.com/v1/pubticker/ethusd/apiKey=${SAMPLE_API_KEY_NEW}", } - postJob := bindings.StructsJob{Id: 1, SelectorType: 0, Weight: 100, + postJobUniswapV3 := bindings.StructsJob{Id: 1, SelectorType: 0, Weight: 100, Power: 2, Name: "ethusd_sample", Selector: "result", Url: `{"type": "POST","url": "https://rpc.ankr.com/eth","body": {"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0xb27308f9f90d607463bb33ea1bebb41c27ce5ab6","data":"0xf7729d43000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000"},"latest"],"id":5},"header": {"content-type": "application/json"}, "returnType": "hex"}`, } + postJobUniswapV2 := bindings.StructsJob{Id: 1, SelectorType: 0, Weight: 100, + Power: 6, Name: "ethusd_sample", Selector: "result", + Url: `{"type": "POST","url": "https://rpc.ankr.com/eth","body": {"jsonrpc":"2.0","id":7269270904970082,"method":"eth_call","params":[{"from":"0x0000000000000000000000000000000000000000","data":"0xd06ca61f0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000050de6856358cc35f3a9a57eaaa34bd4cb707d2cd0000000000000000000000008e870d67f660d95d5be530380d0ec0bd388289e1","to":"0x7a250d5630b4cf539739df2c5dacb4c659f2488d"},"latest"]},"header": {"content-type": "application/json"}, "returnType": "hexArray[1]"}`, + } + invalidDataSourceStructJob := bindings.StructsJob{Id: 1, SelectorType: 0, Weight: 100, Power: 2, Name: "ethusd_sample", Selector: "result", Url: `{"type": true,"url1": {}}`, @@ -700,7 +705,7 @@ func TestGetDataToCommitFromJob(t *testing.T) { { name: "Test 3: When GetDataToCommitFromJob() executes successfully for a POST Job", args: args{ - job: postJob, + job: postJobUniswapV3, }, wantErr: false, }, @@ -720,6 +725,14 @@ func TestGetDataToCommitFromJob(t *testing.T) { want: nil, wantErr: true, }, + { + name: "Test 6: When GetDataToCommitFromJob() executes successfully for a POST uniswap v2 Job", + args: args{ + job: postJobUniswapV2, + }, + want: nil, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/utils/math.go b/utils/math.go index 52d08092a..976ec9202 100644 --- a/utils/math.go +++ b/utils/math.go @@ -7,6 +7,7 @@ import ( "math/big" mathRand "math/rand" "razor/core" + "regexp" "sort" "strconv" "strings" @@ -26,6 +27,9 @@ func ConvertToNumber(num interface{}, returnType string) (*big.Float, error) { if strings.ToLower(returnType) == core.HexReturnType { return ConvertHexToBigFloat(v) } + if isHexArrayPattern(returnType) { + return HandleHexArray(v, returnType) + } convertedNumber, err := strconv.ParseFloat(v, 64) if err != nil { log.Error("Error in converting from string to float: ", err) @@ -192,3 +196,101 @@ func Shuffle(slice []uint32) []uint32 { } return copiedSlice } + +func HandleHexArray(hexStr string, returnType string) (*big.Float, error) { + decodedHexArray, err := decodeHexString(hexStr) + if err != nil { + log.Error("Error in decoding hex array: ", err) + return big.NewFloat(0), err + } + log.Info("HandleHexArray: decoded hex array: ", decodedHexArray) + + index, err := extractIndex(returnType) + if err != nil { + log.Error("Error in extracting value from decoded hex array: ", err) + return big.NewFloat(0), err + } + log.Debug("HandleHexArray: extracted index: ", index) + + // Check if index is within the bounds of decodedHexArray + if index < 0 || index >= len(decodedHexArray) { + log.Error("extracted index is out of bounds for decoded hex array") + return big.NewFloat(0), errors.New("extracted index is out of bounds") + } + + // decodedHexArray[index] returns value in wei, so it needs to be converted to eth + valueInEth, err := ConvertWeiToEth(decodedHexArray[index]) + if err != nil { + log.Error("Error in converting wei to eth: ", err) + return big.NewFloat(0), err + } + + return valueInEth, nil +} + +func decodeHexString(hexStr string) ([]*big.Int, error) { + // Remove the "0x" prefix if present + hexStr = strings.TrimPrefix(hexStr, "0x") + // The length of uint256 in hex (32 bytes) + const uint256HexLength = 64 + + // Make sure the string length is at least enough for the offset and length + if len(hexStr) < 2*uint256HexLength { + return nil, errors.New("hex string too short to contain valid data") + } + + // Getting the starting position of the array data (skipping this step as per Ethereum ABI encoding) + // Skip the length of the array (next 32 bytes) + lengthStr := hexStr[uint256HexLength : 2*uint256HexLength] + length, success := new(big.Int).SetString(lengthStr, 16) + if !success { + log.Error("Invalid length of the array from the hex string") + return nil, errors.New("invalid length") + } + + // The remaining part of the string are the uint256 values + valuesStr := hexStr[2*uint256HexLength:] + + // Each value is 32 bytes long, so check if the length matches + if len(valuesStr) != int(length.Int64())*uint256HexLength { + return nil, errors.New("data length does not match length specifier") + } + + var values []*big.Int + for i := 0; i < int(length.Int64()); i++ { + start := i * uint256HexLength + end := start + uint256HexLength + n := new(big.Int) + n, success := n.SetString(valuesStr[start:end], 16) + if !success { + log.Errorf("Invalid uint256 value at index %d", i) + return nil, errors.New("invalid uint256 value at index") + } + values = append(values, n) + } + + return values, nil +} + +func extractIndex(s string) (int, error) { + re := regexp.MustCompile(core.HexArrayExtractIndexRegex) + + matches := re.FindStringSubmatch(s) + if len(matches) < 2 { + return 0, errors.New("no index found in string") + } + + // Converting the captured substring to an integer + index, err := strconv.Atoi(matches[1]) + if err != nil { + return 0, errors.New("invalid index format in string") + } + + return index, nil +} + +func isHexArrayPattern(s string) bool { + pattern := core.HexArrayReturnType + re := regexp.MustCompile(pattern) + return re.MatchString(s) +} diff --git a/utils/math_test.go b/utils/math_test.go index 60d73faaa..9106c9fd7 100644 --- a/utils/math_test.go +++ b/utils/math_test.go @@ -1035,3 +1035,265 @@ func TestConvertHexToBigFloat(t *testing.T) { }) } } + +func TestHandleHexArray(t *testing.T) { + type args struct { + hexStr string + returnType string + } + tests := []struct { + name string + args args + want *big.Float + wantErr bool + }{ + { + name: "Test 1: Valid token price input", + args: args{ + hexStr: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000012aee97c8ee4b8", + returnType: "hexArray[1]", + }, + want: big.NewFloat(0.00525886742), + wantErr: false, + }, + { + name: "Test 2: Valid another token price input", + args: args{ + hexStr: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000ba121", + returnType: "hexArray[1]", + }, + want: big.NewFloat(0.000000000000762145), + wantErr: false, + }, + { + name: "Test 3: Invalid hex string", + args: args{ + hexStr: "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002", + returnType: "hexArray[1]", + }, + want: big.NewFloat(0), + wantErr: true, + }, + { + name: "Test 4: Invalid return type to extract index", + args: args{ + hexStr: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000012aee97c8ee4b8", + returnType: "hexArray[1a]", + }, + want: big.NewFloat(0), + wantErr: true, + }, + { + name: "Test 5: When decoded value of data is 0, wei to eth conversion will throw error", + args: args{ + hexStr: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", + returnType: "hexArray[0]", + }, + want: big.NewFloat(0), + wantErr: true, + }, + { + name: "Test 6: When extracted index is out of bounds", + args: args{ + hexStr: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", + returnType: "hexArray[1]", + }, + want: big.NewFloat(0), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := HandleHexArray(tt.args.hexStr, tt.args.returnType) + if (err != nil) != tt.wantErr { + t.Errorf("HandleHexArray() error = %v, wantErr %v", err, tt.wantErr) + return + } + // Use a small tolerance for comparison + tolerance := big.NewFloat(1e-10).SetPrec(1024) + + diff := new(big.Float).Sub(got, tt.want) + diff.Abs(diff) + + // Check if the difference is greater than or equal to tolerance + if diff.Cmp(tolerance) >= 0 { + t.Errorf("HandleHexArray() got = %v, want %v, difference = %v", got, tt.want, diff) + } + }) + } +} + +func Test_decodeHexString(t *testing.T) { + type args struct { + hexStr string + } + tests := []struct { + name string + args args + want []*big.Int + wantErr bool + }{ + { + name: "Valid hex string which is a result from uniswap v2 datasource", + args: args{hexStr: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000012aee97c8ee4b8"}, + want: []*big.Int{ + big.NewInt(1000000000000000000), + big.NewInt(5258867421144248), + }, + wantErr: false, + }, + { + name: "Valid single element", + args: args{hexStr: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"}, + want: []*big.Int{big.NewInt(1)}, + wantErr: false, + }, + { + name: "Valid hex string is provided but length of array doesnt match", + args: args{hexStr: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a7640000"}, + want: nil, + wantErr: true, + }, + { + name: "Invalid hex string", + args: args{hexStr: "0x12345"}, + want: nil, + wantErr: true, + }, + { + name: "Invalid length field", + args: args{hexStr: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000Z"}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeHexString(tt.args.hexStr) + if (err != nil) != tt.wantErr { + t.Errorf("decodeHexString() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeHexString() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_extractIndex(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want int + wantErr bool + }{ + { + name: "Valid input with index 1", + args: args{s: "hexArray[1]"}, + want: 1, + wantErr: false, + }, + { + name: "Valid input with index 123", + args: args{s: "hexArray[123]"}, + want: 123, + wantErr: false, + }, + { + name: "Invalid input - missing brackets", + args: args{s: "hexArray1"}, + want: 0, + wantErr: true, + }, + { + name: "Invalid input - non-numeric index", + args: args{s: "hexArray[abc]"}, + want: 0, + wantErr: true, + }, + { + name: "Invalid input - negative index", + args: args{s: "hexArray[-1]"}, + want: 0, + wantErr: true, + }, + { + name: "Invalid input - empty string", + args: args{s: ""}, + want: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := extractIndex(tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("extractIndex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("extractIndex() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isHexArrayPattern(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Valid pattern with index 0", + args: args{s: "hexArray[0]"}, + want: true, + }, + { + name: "Valid pattern with index 123", + args: args{s: "hexArray[123]"}, + want: true, + }, + { + name: "Invalid pattern - missing brackets", + args: args{s: "hexArray1"}, + want: false, + }, + { + name: "Invalid pattern - non-numeric index", + args: args{s: "hexArray[abc]"}, + want: false, + }, + { + name: "Invalid pattern - negative index", + args: args{s: "hexArray[-1]"}, + want: false, + }, + { + name: "Invalid pattern - empty string", + args: args{s: ""}, + want: false, + }, + { + name: "Invalid pattern - extra characters", + args: args{s: "hexArray[10]abc"}, + want: false, + }, + // Additional test cases can be added here. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isHexArrayPattern(tt.args.s); got != tt.want { + t.Errorf("isHexArrayPattern() = %v, want %v", got, tt.want) + } + }) + } +}