diff --git a/pkg/math/slice.go b/pkg/math/slice.go new file mode 100644 index 0000000000..2b2b78761b --- /dev/null +++ b/pkg/math/slice.go @@ -0,0 +1,53 @@ +package math + +import "slices" + +type number interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 +} + +// SliceMedianValue returns the median value of the given slice. +// Returns 0 for an empty slice. If inPlace is true, the input slice will be sorted in place. +// Otherwise, a copy of the input slice will be sorted. +func SliceMedianValue[T number](items []T, inPlace bool) T { + if inPlace { + return sliceMedianValue(items) + } + + copied := make([]T, len(items)) + copy(copied, items) + + out := sliceMedianValue(copied) + + // We don't need the copy anymore + //nolint:ineffassign // let's help the garbage collector :) + copied = nil + + return out +} + +func sliceMedianValue[T number](items []T) T { + switch len(items) { + case 0: + return 0 + case 1: + return items[0] + } + + slices.Sort(items) + + // note that int division is used here e.g. 5/2 => 2 + + // []int{1 2 3 4 5} => items[(5/2)] => items[2] => 3 + if len(items)%2 == 1 { + return items[len(items)/2] + } + + // odd number of items + rightIndex := len(items) / 2 + leftIndex := rightIndex - 1 + + // []int{1 2 3 4} => (items[1] + items[2]) / 2 => 5/2 => 2 + return (items[leftIndex] + items[rightIndex]) / 2 +} diff --git a/pkg/math/slice_test.go b/pkg/math/slice_test.go new file mode 100644 index 0000000000..558842dd98 --- /dev/null +++ b/pkg/math/slice_test.go @@ -0,0 +1,75 @@ +package math + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSliceMedianValue(t *testing.T) { + for _, tt := range []struct { + name string + input []int + expected int + inPlace bool + }{ + { + name: "empty", + input: nil, + expected: 0, + inPlace: false, + }, + { + name: "single", + input: []int{10}, + expected: 10, + }, + { + name: "two", + input: []int{10, 20}, + expected: 15, + }, + { + name: "even", + input: []int{30, 20, 10, 20}, + expected: 20, + }, + { + name: "even in-place", + input: []int{30, 20, 10, 20}, + expected: 20, + inPlace: true, + }, + { + name: "odd", + input: []int{5, 5, 6, 1, 1, 1, 4}, + expected: 4, + }, + { + name: "odd in-place", + input: []int{1, 1, 1, 1, 7, 7, 7, 7}, + expected: 4, + }, + } { + t.Run(tt.name, func(t *testing.T) { + // ASSERT + // Given a copy of the input slice + var snapshot []int + for _, v := range tt.input { + snapshot = append(snapshot, v) + } + + // ACT + out := SliceMedianValue(tt.input, tt.inPlace) + + // ASSERT + assert.Equal(t, tt.expected, out) + + // Check that elements of the input slice are unchanged + if !tt.inPlace { + assert.Equal(t, snapshot, tt.input) + } + }) + } + +} diff --git a/x/crosschain/keeper/gas_price.go b/x/crosschain/keeper/gas_price.go index 9622acc7d6..d5857b7820 100644 --- a/x/crosschain/keeper/gas_price.go +++ b/x/crosschain/keeper/gas_price.go @@ -6,8 +6,8 @@ import ( "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - "golang.org/x/exp/slices" + slicemath "github.com/zeta-chain/zetacore/pkg/math" "github.com/zeta-chain/zetacore/x/crosschain/types" ) @@ -44,36 +44,12 @@ func (k Keeper) GetMedianGasPriceInUint(ctx sdk.Context, chainID int64) (math.Ui var ( gasPrice = math.NewUint(entity.Prices[entity.MedianIndex]) - priorityFee = math.NewUint(medianValue(entity.PriorityFees)) + priorityFee = math.NewUint(slicemath.SliceMedianValue(entity.PriorityFees, false)) ) return gasPrice, priorityFee, true } -// medianValue returns the median value of a slice -// example: [ 1 7 5 2 3 6 4 ] => [ 1 2 3 4 5 6 7 ] => 4 -func medianValue(items []uint64) uint64 { - switch len(items) { - case 0: - return 0 - case 1: - return items[0] - } - - // We don't want to modify the original slice - copiedItems := make([]uint64, len(items)) - copy(copiedItems, items) - - slices.Sort(copiedItems) - mv := copiedItems[len(copiedItems)/2] - - // We don't need the copy anymore - //nolint:ineffassign // let's help garbage collector :) - copiedItems = nil - - return mv -} - // RemoveGasPrice removes a gasPrice from the store func (k Keeper) RemoveGasPrice(ctx sdk.Context, index string) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.GasPriceKey))