Skip to content

Commit

Permalink
feat: added support for job returning results in hex array (#1174)
Browse files Browse the repository at this point in the history
* feat: added support for jobs returning result in hex array

* feat: added tests for hex array decoding and regex functions

* refactor: fixed golangci-lint errors
  • Loading branch information
Yashk767 authored Dec 20, 2023
1 parent 0dc7960 commit 3bf2914
Show file tree
Hide file tree
Showing 4 changed files with 385 additions and 2 deletions.
6 changes: 6 additions & 0 deletions core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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+)\]$`
17 changes: 15 additions & 2 deletions utils/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}}`,
Expand Down Expand Up @@ -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,
},
Expand All @@ -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) {
Expand Down
102 changes: 102 additions & 0 deletions utils/math.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math/big"
mathRand "math/rand"
"razor/core"
"regexp"
"sort"
"strconv"
"strings"
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Loading

0 comments on commit 3bf2914

Please sign in to comment.