From 8a980697eeeeceb604f27856ca3e7f3297dc1b0b Mon Sep 17 00:00:00 2001 From: "Matt, Park" <45252226+mattverse@users.noreply.github.com> Date: Tue, 3 May 2022 15:28:28 +0900 Subject: [PATCH 1/9] Add `BigInt`, `BigDec` to osmomath (#1231) * Add decimal and int * change variable names Co-authored-by: Dev Ojha --- osmomath/decimal.go | 802 +++++++++++++++++++++++++++++++++++++++ osmomath/decimal_test.go | 563 +++++++++++++++++++++++++++ osmomath/int.go | 437 +++++++++++++++++++++ osmomath/int_test.go | 472 +++++++++++++++++++++++ 4 files changed, 2274 insertions(+) create mode 100644 osmomath/decimal.go create mode 100644 osmomath/decimal_test.go create mode 100644 osmomath/int.go create mode 100644 osmomath/int_test.go diff --git a/osmomath/decimal.go b/osmomath/decimal.go new file mode 100644 index 00000000000..b082fad37f4 --- /dev/null +++ b/osmomath/decimal.go @@ -0,0 +1,802 @@ +package osmomath + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "testing" +) + +// NOTE: never use new(BigDec) or else we will panic unmarshalling into the +// nil embedded big.Int +type BigDec struct { + i *big.Int +} + +const ( + // number of decimal places + Precision = 18 + + // bytes required to represent the above precision + // Ceiling[Log2[999 999 999 999 999 999]] + DecimalPrecisionBits = 60 + + maxDecBitLen = maxBitLen + DecimalPrecisionBits + + // max number of iterations in ApproxRoot function + maxApproxRootIterations = 100 +) + +var ( + precisionReuse = new(big.Int).Exp(big.NewInt(10), big.NewInt(Precision), nil) + fivePrecision = new(big.Int).Quo(precisionReuse, big.NewInt(2)) + precisionMultipliers []*big.Int + zeroInt = big.NewInt(0) + oneInt = big.NewInt(1) + tenInt = big.NewInt(10) +) + +// Decimal errors +var ( + ErrEmptyDecimalStr = errors.New("decimal string cannot be empty") + ErrInvalidDecimalLength = errors.New("invalid decimal length") + ErrInvalidDecimalStr = errors.New("invalid decimal string") +) + +// Set precision multipliers +func init() { + precisionMultipliers = make([]*big.Int, Precision+1) + for i := 0; i <= Precision; i++ { + precisionMultipliers[i] = calcPrecisionMultiplier(int64(i)) + } +} + +func precisionInt() *big.Int { + return new(big.Int).Set(precisionReuse) +} + +func ZeroDec() BigDec { return BigDec{new(big.Int).Set(zeroInt)} } +func OneDec() BigDec { return BigDec{precisionInt()} } +func SmallestDec() BigDec { return BigDec{new(big.Int).Set(oneInt)} } + +// calculate the precision multiplier +func calcPrecisionMultiplier(prec int64) *big.Int { + if prec > Precision { + panic(fmt.Sprintf("too much precision, maximum %v, provided %v", Precision, prec)) + } + zerosToAdd := Precision - prec + multiplier := new(big.Int).Exp(tenInt, big.NewInt(zerosToAdd), nil) + return multiplier +} + +// get the precision multiplier, do not mutate result +func precisionMultiplier(prec int64) *big.Int { + if prec > Precision { + panic(fmt.Sprintf("too much precision, maximum %v, provided %v", Precision, prec)) + } + return precisionMultipliers[prec] +} + +// create a new NewBigDec from integer assuming whole number +func NewBigDec(i int64) BigDec { + return NewDecWithPrec(i, 0) +} + +// create a new BigDec from integer with decimal place at prec +// CONTRACT: prec <= Precision +func NewDecWithPrec(i, prec int64) BigDec { + return BigDec{ + new(big.Int).Mul(big.NewInt(i), precisionMultiplier(prec)), + } +} + +// create a new BigDec from big integer assuming whole numbers +// CONTRACT: prec <= Precision +func NewDecFromBigInt(i *big.Int) BigDec { + return NewDecFromBigIntWithPrec(i, 0) +} + +// create a new BigDec from big integer assuming whole numbers +// CONTRACT: prec <= Precision +func NewDecFromBigIntWithPrec(i *big.Int, prec int64) BigDec { + return BigDec{ + new(big.Int).Mul(i, precisionMultiplier(prec)), + } +} + +// create a new BigDec from big integer assuming whole numbers +// CONTRACT: prec <= Precision +func NewDecFromInt(i BigInt) BigDec { + return NewDecFromIntWithPrec(i, 0) +} + +// create a new BigDec from big integer with decimal place at prec +// CONTRACT: prec <= Precision +func NewDecFromIntWithPrec(i BigInt, prec int64) BigDec { + return BigDec{ + new(big.Int).Mul(i.BigInt(), precisionMultiplier(prec)), + } +} + +// create a decimal from an input decimal string. +// valid must come in the form: +// (-) whole integers (.) decimal integers +// examples of acceptable input include: +// -123.456 +// 456.7890 +// 345 +// -456789 +// +// NOTE - An error will return if more decimal places +// are provided in the string than the constant Precision. +// +// CONTRACT - This function does not mutate the input str. +func NewDecFromStr(str string) (BigDec, error) { + if len(str) == 0 { + return BigDec{}, ErrEmptyDecimalStr + } + + // first extract any negative symbol + neg := false + if str[0] == '-' { + neg = true + str = str[1:] + } + + if len(str) == 0 { + return BigDec{}, ErrEmptyDecimalStr + } + + strs := strings.Split(str, ".") + lenDecs := 0 + combinedStr := strs[0] + + if len(strs) == 2 { // has a decimal place + lenDecs = len(strs[1]) + if lenDecs == 0 || len(combinedStr) == 0 { + return BigDec{}, ErrInvalidDecimalLength + } + combinedStr += strs[1] + } else if len(strs) > 2 { + return BigDec{}, ErrInvalidDecimalStr + } + + if lenDecs > Precision { + return BigDec{}, fmt.Errorf("invalid precision; max: %d, got: %d", Precision, lenDecs) + } + + // add some extra zero's to correct to the Precision factor + zerosToAdd := Precision - lenDecs + zeros := fmt.Sprintf(`%0`+strconv.Itoa(zerosToAdd)+`s`, "") + combinedStr += zeros + + combined, ok := new(big.Int).SetString(combinedStr, 10) // base 10 + if !ok { + return BigDec{}, fmt.Errorf("failed to set decimal string: %s", combinedStr) + } + if combined.BitLen() > maxBitLen { + return BigDec{}, fmt.Errorf("decimal out of range; bitLen: got %d, max %d", combined.BitLen(), maxBitLen) + } + if neg { + combined = new(big.Int).Neg(combined) + } + + return BigDec{combined}, nil +} + +// Decimal from string, panic on error +func MustNewDecFromStr(s string) BigDec { + dec, err := NewDecFromStr(s) + if err != nil { + panic(err) + } + return dec +} + +func (d BigDec) IsNil() bool { return d.i == nil } // is decimal nil +func (d BigDec) IsZero() bool { return (d.i).Sign() == 0 } // is equal to zero +func (d BigDec) IsNegative() bool { return (d.i).Sign() == -1 } // is negative +func (d BigDec) IsPositive() bool { return (d.i).Sign() == 1 } // is positive +func (d BigDec) Equal(d2 BigDec) bool { return (d.i).Cmp(d2.i) == 0 } // equal decimals +func (d BigDec) GT(d2 BigDec) bool { return (d.i).Cmp(d2.i) > 0 } // greater than +func (d BigDec) GTE(d2 BigDec) bool { return (d.i).Cmp(d2.i) >= 0 } // greater than or equal +func (d BigDec) LT(d2 BigDec) bool { return (d.i).Cmp(d2.i) < 0 } // less than +func (d BigDec) LTE(d2 BigDec) bool { return (d.i).Cmp(d2.i) <= 0 } // less than or equal +func (d BigDec) Neg() BigDec { return BigDec{new(big.Int).Neg(d.i)} } // reverse the decimal sign +func (d BigDec) Abs() BigDec { return BigDec{new(big.Int).Abs(d.i)} } // absolute value + +// BigInt returns a copy of the underlying big.Int. +func (d BigDec) BigInt() *big.Int { + if d.IsNil() { + return nil + } + + cp := new(big.Int) + return cp.Set(d.i) +} + +// addition +func (d BigDec) Add(d2 BigDec) BigDec { + res := new(big.Int).Add(d.i, d2.i) + + if res.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{res} +} + +// subtraction +func (d BigDec) Sub(d2 BigDec) BigDec { + res := new(big.Int).Sub(d.i, d2.i) + + if res.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{res} +} + +// multiplication +func (d BigDec) Mul(d2 BigDec) BigDec { + mul := new(big.Int).Mul(d.i, d2.i) + chopped := chopPrecisionAndRound(mul) + + if chopped.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{chopped} +} + +// multiplication truncate +func (d BigDec) MulTruncate(d2 BigDec) BigDec { + mul := new(big.Int).Mul(d.i, d2.i) + chopped := chopPrecisionAndTruncate(mul) + + if chopped.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{chopped} +} + +// multiplication +func (d BigDec) MulInt(i BigInt) BigDec { + mul := new(big.Int).Mul(d.i, i.i) + + if mul.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{mul} +} + +// MulInt64 - multiplication with int64 +func (d BigDec) MulInt64(i int64) BigDec { + mul := new(big.Int).Mul(d.i, big.NewInt(i)) + + if mul.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{mul} +} + +// quotient +func (d BigDec) Quo(d2 BigDec) BigDec { + // multiply precision twice + mul := new(big.Int).Mul(d.i, precisionReuse) + mul.Mul(mul, precisionReuse) + + quo := new(big.Int).Quo(mul, d2.i) + chopped := chopPrecisionAndRound(quo) + + if chopped.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{chopped} +} + +// quotient truncate +func (d BigDec) QuoTruncate(d2 BigDec) BigDec { + // multiply precision twice + mul := new(big.Int).Mul(d.i, precisionReuse) + mul.Mul(mul, precisionReuse) + + quo := mul.Quo(mul, d2.i) + chopped := chopPrecisionAndTruncate(quo) + + if chopped.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{chopped} +} + +// quotient, round up +func (d BigDec) QuoRoundUp(d2 BigDec) BigDec { + // multiply precision twice + mul := new(big.Int).Mul(d.i, precisionReuse) + mul.Mul(mul, precisionReuse) + + quo := new(big.Int).Quo(mul, d2.i) + chopped := chopPrecisionAndRoundUp(quo) + + if chopped.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return BigDec{chopped} +} + +// quotient +func (d BigDec) QuoInt(i BigInt) BigDec { + mul := new(big.Int).Quo(d.i, i.i) + return BigDec{mul} +} + +// QuoInt64 - quotient with int64 +func (d BigDec) QuoInt64(i int64) BigDec { + mul := new(big.Int).Quo(d.i, big.NewInt(i)) + return BigDec{mul} +} + +// ApproxRoot returns an approximate estimation of a Dec's positive real nth root +// using Newton's method (where n is positive). The algorithm starts with some guess and +// computes the sequence of improved guesses until an answer converges to an +// approximate answer. It returns `|d|.ApproxRoot() * -1` if input is negative. +// A maximum number of 100 iterations is used a backup boundary condition for +// cases where the answer never converges enough to satisfy the main condition. +func (d BigDec) ApproxRoot(root uint64) (guess BigDec, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = errors.New("out of bounds") + } + } + }() + + if d.IsNegative() { + absRoot, err := d.MulInt64(-1).ApproxRoot(root) + return absRoot.MulInt64(-1), err + } + + if root == 1 || d.IsZero() || d.Equal(OneDec()) { + return d, nil + } + + if root == 0 { + return OneDec(), nil + } + + rootInt := NewIntFromUint64(root) + guess, delta := OneDec(), OneDec() + + for iter := 0; delta.Abs().GT(SmallestDec()) && iter < maxApproxRootIterations; iter++ { + prev := guess.Power(root - 1) + if prev.IsZero() { + prev = SmallestDec() + } + delta = d.Quo(prev) + delta = delta.Sub(guess) + delta = delta.QuoInt(rootInt) + + guess = guess.Add(delta) + } + + return guess, nil +} + +// Power returns a the result of raising to a positive integer power +func (d BigDec) Power(power uint64) BigDec { + if power == 0 { + return OneDec() + } + tmp := OneDec() + + for i := power; i > 1; { + if i%2 != 0 { + tmp = tmp.Mul(d) + } + i /= 2 + d = d.Mul(d) + } + + return d.Mul(tmp) +} + +// ApproxSqrt is a wrapper around ApproxRoot for the common special case +// of finding the square root of a number. It returns -(sqrt(abs(d)) if input is negative. +func (d BigDec) ApproxSqrt() (BigDec, error) { + return d.ApproxRoot(2) +} + +// is integer, e.g. decimals are zero +func (d BigDec) IsInteger() bool { + return new(big.Int).Rem(d.i, precisionReuse).Sign() == 0 +} + +// format decimal state +func (d BigDec) Format(s fmt.State, verb rune) { + _, err := s.Write([]byte(d.String())) + if err != nil { + panic(err) + } +} + +func (d BigDec) String() string { + if d.i == nil { + return d.i.String() + } + + isNeg := d.IsNegative() + + if isNeg { + d = d.Neg() + } + + bzInt, err := d.i.MarshalText() + if err != nil { + return "" + } + inputSize := len(bzInt) + + var bzStr []byte + + // TODO: Remove trailing zeros + // case 1, purely decimal + if inputSize <= Precision { + bzStr = make([]byte, Precision+2) + + // 0. prefix + bzStr[0] = byte('0') + bzStr[1] = byte('.') + + // set relevant digits to 0 + for i := 0; i < Precision-inputSize; i++ { + bzStr[i+2] = byte('0') + } + + // set final digits + copy(bzStr[2+(Precision-inputSize):], bzInt) + } else { + // inputSize + 1 to account for the decimal point that is being added + bzStr = make([]byte, inputSize+1) + decPointPlace := inputSize - Precision + + copy(bzStr, bzInt[:decPointPlace]) // pre-decimal digits + bzStr[decPointPlace] = byte('.') // decimal point + copy(bzStr[decPointPlace+1:], bzInt[decPointPlace:]) // post-decimal digits + } + + if isNeg { + return "-" + string(bzStr) + } + + return string(bzStr) +} + +// Float64 returns the float64 representation of a BigDec. +// Will return the error if the conversion failed. +func (d BigDec) Float64() (float64, error) { + return strconv.ParseFloat(d.String(), 64) +} + +// MustFloat64 returns the float64 representation of a BigDec. +// Would panic if the conversion failed. +func (d BigDec) MustFloat64() float64 { + if value, err := strconv.ParseFloat(d.String(), 64); err != nil { + panic(err) + } else { + return value + } +} + +// ____ +// __| |__ "chop 'em +// ` \ round!" +// ___|| ~ _ -bankers +// | | __ +// | | | __|__|__ +// |_____: / | $$$ | +// |________| + +// Remove a Precision amount of rightmost digits and perform bankers rounding +// on the remainder (gaussian rounding) on the digits which have been removed. +// +// Mutates the input. Use the non-mutative version if that is undesired +func chopPrecisionAndRound(d *big.Int) *big.Int { + // remove the negative and add it back when returning + if d.Sign() == -1 { + // make d positive, compute chopped value, and then un-mutate d + d = d.Neg(d) + d = chopPrecisionAndRound(d) + d = d.Neg(d) + return d + } + + // get the truncated quotient and remainder + quo, rem := d, big.NewInt(0) + quo, rem = quo.QuoRem(d, precisionReuse, rem) + + if rem.Sign() == 0 { // remainder is zero + return quo + } + + switch rem.Cmp(fivePrecision) { + case -1: + return quo + case 1: + return quo.Add(quo, oneInt) + default: // bankers rounding must take place + // always round to an even number + if quo.Bit(0) == 0 { + return quo + } + return quo.Add(quo, oneInt) + } +} + +func chopPrecisionAndRoundUp(d *big.Int) *big.Int { + // remove the negative and add it back when returning + if d.Sign() == -1 { + // make d positive, compute chopped value, and then un-mutate d + d = d.Neg(d) + // truncate since d is negative... + d = chopPrecisionAndTruncate(d) + d = d.Neg(d) + return d + } + + // get the truncated quotient and remainder + quo, rem := d, big.NewInt(0) + quo, rem = quo.QuoRem(d, precisionReuse, rem) + + if rem.Sign() == 0 { // remainder is zero + return quo + } + + return quo.Add(quo, oneInt) +} + +func chopPrecisionAndRoundNonMutative(d *big.Int) *big.Int { + tmp := new(big.Int).Set(d) + return chopPrecisionAndRound(tmp) +} + +// RoundInt64 rounds the decimal using bankers rounding +func (d BigDec) RoundInt64() int64 { + chopped := chopPrecisionAndRoundNonMutative(d.i) + if !chopped.IsInt64() { + panic("Int64() out of bound") + } + return chopped.Int64() +} + +// RoundInt round the decimal using bankers rounding +func (d BigDec) RoundInt() BigInt { + return NewIntFromBigInt(chopPrecisionAndRoundNonMutative(d.i)) +} + +// chopPrecisionAndTruncate is similar to chopPrecisionAndRound, +// but always rounds down. It does not mutate the input. +func chopPrecisionAndTruncate(d *big.Int) *big.Int { + return new(big.Int).Quo(d, precisionReuse) +} + +// TruncateInt64 truncates the decimals from the number and returns an int64 +func (d BigDec) TruncateInt64() int64 { + chopped := chopPrecisionAndTruncate(d.i) + if !chopped.IsInt64() { + panic("Int64() out of bound") + } + return chopped.Int64() +} + +// TruncateInt truncates the decimals from the number and returns an Int +func (d BigDec) TruncateInt() BigInt { + return NewIntFromBigInt(chopPrecisionAndTruncate(d.i)) +} + +// TruncateDec truncates the decimals from the number and returns a Dec +func (d BigDec) TruncateDec() BigDec { + return NewDecFromBigInt(chopPrecisionAndTruncate(d.i)) +} + +// Ceil returns the smallest interger value (as a decimal) that is greater than +// or equal to the given decimal. +func (d BigDec) Ceil() BigDec { + tmp := new(big.Int).Set(d.i) + + quo, rem := tmp, big.NewInt(0) + quo, rem = quo.QuoRem(tmp, precisionReuse, rem) + + // no need to round with a zero remainder regardless of sign + if rem.Cmp(zeroInt) == 0 { + return NewDecFromBigInt(quo) + } + + if rem.Sign() == -1 { + return NewDecFromBigInt(quo) + } + + return NewDecFromBigInt(quo.Add(quo, oneInt)) +} + +// MaxSortableDec is the largest Dec that can be passed into SortableDecBytes() +// Its negative form is the least Dec that can be passed in. +var MaxSortableDec = OneDec().Quo(SmallestDec()) + +// ValidSortableDec ensures that a Dec is within the sortable bounds, +// a BigDec can't have a precision of less than 10^-18. +// Max sortable decimal was set to the reciprocal of SmallestDec. +func ValidSortableDec(dec BigDec) bool { + return dec.Abs().LTE(MaxSortableDec) +} + +// SortableDecBytes returns a byte slice representation of a Dec that can be sorted. +// Left and right pads with 0s so there are 18 digits to left and right of the decimal point. +// For this reason, there is a maximum and minimum value for this, enforced by ValidSortableDec. +func SortableDecBytes(dec BigDec) []byte { + if !ValidSortableDec(dec) { + panic("dec must be within bounds") + } + // Instead of adding an extra byte to all sortable decs in order to handle max sortable, we just + // makes its bytes be "max" which comes after all numbers in ASCIIbetical order + if dec.Equal(MaxSortableDec) { + return []byte("max") + } + // For the same reason, we make the bytes of minimum sortable dec be --, which comes before all numbers. + if dec.Equal(MaxSortableDec.Neg()) { + return []byte("--") + } + // We move the negative sign to the front of all the left padded 0s, to make negative numbers come before positive numbers + if dec.IsNegative() { + return append([]byte("-"), []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", Precision*2+1), dec.Abs().String()))...) + } + return []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", Precision*2+1), dec.String())) +} + +// reuse nil values +var nilJSON []byte + +func init() { + empty := new(big.Int) + bz, _ := empty.MarshalText() + nilJSON, _ = json.Marshal(string(bz)) +} + +// MarshalJSON marshals the decimal +func (d BigDec) MarshalJSON() ([]byte, error) { + if d.i == nil { + return nilJSON, nil + } + return json.Marshal(d.String()) +} + +// UnmarshalJSON defines custom decoding scheme +func (d *BigDec) UnmarshalJSON(bz []byte) error { + if d.i == nil { + d.i = new(big.Int) + } + + var text string + err := json.Unmarshal(bz, &text) + if err != nil { + return err + } + + // TODO: Reuse dec allocation + newDec, err := NewDecFromStr(text) + if err != nil { + return err + } + + d.i = newDec.i + return nil +} + +// MarshalYAML returns the YAML representation. +func (d BigDec) MarshalYAML() (interface{}, error) { + return d.String(), nil +} + +// Marshal implements the gogo proto custom type interface. +func (d BigDec) Marshal() ([]byte, error) { + if d.i == nil { + d.i = new(big.Int) + } + return d.i.MarshalText() +} + +// MarshalTo implements the gogo proto custom type interface. +func (d *BigDec) MarshalTo(data []byte) (n int, err error) { + if d.i == nil { + d.i = new(big.Int) + } + + if d.i.Cmp(zeroInt) == 0 { + copy(data, []byte{0x30}) + return 1, nil + } + + bz, err := d.Marshal() + if err != nil { + return 0, err + } + + copy(data, bz) + return len(bz), nil +} + +// Unmarshal implements the gogo proto custom type interface. +func (d *BigDec) Unmarshal(data []byte) error { + if len(data) == 0 { + d = nil + return nil + } + + if d.i == nil { + d.i = new(big.Int) + } + + if err := d.i.UnmarshalText(data); err != nil { + return err + } + + if d.i.BitLen() > maxBitLen { + return fmt.Errorf("decimal out of range; got: %d, max: %d", d.i.BitLen(), maxBitLen) + } + + return nil +} + +// Size implements the gogo proto custom type interface. +func (d *BigDec) Size() int { + bz, _ := d.Marshal() + return len(bz) +} + +// Override Amino binary serialization by proxying to protobuf. +func (d BigDec) MarshalAmino() ([]byte, error) { return d.Marshal() } +func (d *BigDec) UnmarshalAmino(bz []byte) error { return d.Unmarshal(bz) } + +// helpers + +// test if two decimal arrays are equal +func DecsEqual(d1s, d2s []BigDec) bool { + if len(d1s) != len(d2s) { + return false + } + + for i, d1 := range d1s { + if !d1.Equal(d2s[i]) { + return false + } + } + return true +} + +// minimum decimal between two +func MinDec(d1, d2 BigDec) BigDec { + if d1.LT(d2) { + return d1 + } + return d2 +} + +// maximum decimal between two +func MaxDec(d1, d2 BigDec) BigDec { + if d1.LT(d2) { + return d2 + } + return d1 +} + +// intended to be used with require/assert: require.True(DecEq(...)) +func DecEq(t *testing.T, exp, got BigDec) (*testing.T, bool, string, string, string) { + return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() +} + +func DecApproxEq(t *testing.T, d1 BigDec, d2 BigDec, tol BigDec) (*testing.T, bool, string, string, string) { + diff := d1.Sub(d2).Abs() + return t, diff.LTE(tol), "expected |d1 - d2| <:\t%v\ngot |d1 - d2| = \t\t%v", tol.String(), diff.String() +} diff --git a/osmomath/decimal_test.go b/osmomath/decimal_test.go new file mode 100644 index 00000000000..6456e385e82 --- /dev/null +++ b/osmomath/decimal_test.go @@ -0,0 +1,563 @@ +package osmomath + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "gopkg.in/yaml.v2" +) + +type decimalTestSuite struct { + suite.Suite +} + +func TestDecimalTestSuite(t *testing.T) { + suite.Run(t, new(decimalTestSuite)) +} + +func TestDecApproxEq(t *testing.T) { + // d1 = 0.55, d2 = 0.6, tol = 0.1 + d1 := NewDecWithPrec(55, 2) + d2 := NewDecWithPrec(6, 1) + tol := NewDecWithPrec(1, 1) + + require.True(DecApproxEq(t, d1, d2, tol)) + + // d1 = 0.55, d2 = 0.6, tol = 1E-5 + d1 = NewDecWithPrec(55, 2) + d2 = NewDecWithPrec(6, 1) + tol = NewDecWithPrec(1, 5) + + require.False(DecApproxEq(t, d1, d2, tol)) + + // d1 = 0.6, d2 = 0.61, tol = 0.01 + d1 = NewDecWithPrec(6, 1) + d2 = NewDecWithPrec(61, 2) + tol = NewDecWithPrec(1, 2) + + require.True(DecApproxEq(t, d1, d2, tol)) +} + +// create a decimal from a decimal string (ex. "1234.5678") +func (s *decimalTestSuite) mustNewDecFromStr(str string) (d BigDec) { + d, err := NewDecFromStr(str) + s.Require().NoError(err) + + return d +} + +func (s *decimalTestSuite) TestNewDecFromStr() { + largeBigInt, success := new(big.Int).SetString("3144605511029693144278234343371835", 10) + s.Require().True(success) + + tests := []struct { + decimalStr string + expErr bool + exp BigDec + }{ + {"", true, BigDec{}}, + {"0.-75", true, BigDec{}}, + {"0", false, NewBigDec(0)}, + {"1", false, NewBigDec(1)}, + {"1.1", false, NewDecWithPrec(11, 1)}, + {"0.75", false, NewDecWithPrec(75, 2)}, + {"0.8", false, NewDecWithPrec(8, 1)}, + {"0.11111", false, NewDecWithPrec(11111, 5)}, + {"314460551102969.3144278234343371835", true, NewBigDec(3141203149163817869)}, + {"314460551102969314427823434337.1835718092488231350", + true, NewDecFromBigIntWithPrec(largeBigInt, 4)}, + {"314460551102969314427823434337.1835", + false, NewDecFromBigIntWithPrec(largeBigInt, 4)}, + {".", true, BigDec{}}, + {".0", true, NewBigDec(0)}, + {"1.", true, NewBigDec(1)}, + {"foobar", true, BigDec{}}, + {"0.foobar", true, BigDec{}}, + {"0.foobar.", true, BigDec{}}, + {"23258839177459420497578361852416145099316523541994177929007686373780457219628733546438113622840434097944400691400517693873107252115668992", true, BigDec{}}, + } + + for tcIndex, tc := range tests { + res, err := NewDecFromStr(tc.decimalStr) + if tc.expErr { + s.Require().NotNil(err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + } else { + s.Require().Nil(err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + s.Require().True(res.Equal(tc.exp), "equality was incorrect, res %v, exp %v, tc %v", res, tc.exp, tcIndex) + } + + // negative tc + res, err = NewDecFromStr("-" + tc.decimalStr) + if tc.expErr { + s.Require().NotNil(err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + } else { + s.Require().Nil(err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + exp := tc.exp.Mul(NewBigDec(-1)) + s.Require().True(res.Equal(exp), "equality was incorrect, res %v, exp %v, tc %v", res, exp, tcIndex) + } + } +} + +func (s *decimalTestSuite) TestDecString() { + tests := []struct { + d BigDec + want string + }{ + {NewBigDec(0), "0.000000000000000000"}, + {NewBigDec(1), "1.000000000000000000"}, + {NewBigDec(10), "10.000000000000000000"}, + {NewBigDec(12340), "12340.000000000000000000"}, + {NewDecWithPrec(12340, 4), "1.234000000000000000"}, + {NewDecWithPrec(12340, 5), "0.123400000000000000"}, + {NewDecWithPrec(12340, 8), "0.000123400000000000"}, + {NewDecWithPrec(1009009009009009009, 17), "10.090090090090090090"}, + } + for tcIndex, tc := range tests { + s.Require().Equal(tc.want, tc.d.String(), "bad String(), index: %v", tcIndex) + } +} + +func (s *decimalTestSuite) TestDecFloat64() { + tests := []struct { + d BigDec + want float64 + }{ + {NewBigDec(0), 0.000000000000000000}, + {NewBigDec(1), 1.000000000000000000}, + {NewBigDec(10), 10.000000000000000000}, + {NewBigDec(12340), 12340.000000000000000000}, + {NewDecWithPrec(12340, 4), 1.234000000000000000}, + {NewDecWithPrec(12340, 5), 0.123400000000000000}, + {NewDecWithPrec(12340, 8), 0.000123400000000000}, + {NewDecWithPrec(1009009009009009009, 17), 10.090090090090090090}, + } + for tcIndex, tc := range tests { + value, err := tc.d.Float64() + s.Require().Nil(err, "error getting Float64(), index: %v", tcIndex) + s.Require().Equal(tc.want, value, "bad Float64(), index: %v", tcIndex) + s.Require().Equal(tc.want, tc.d.MustFloat64(), "bad MustFloat64(), index: %v", tcIndex) + } +} + +func (s *decimalTestSuite) TestEqualities() { + tests := []struct { + d1, d2 BigDec + gt, lt, eq bool + }{ + {NewBigDec(0), NewBigDec(0), false, false, true}, + {NewDecWithPrec(0, 2), NewDecWithPrec(0, 4), false, false, true}, + {NewDecWithPrec(100, 0), NewDecWithPrec(100, 0), false, false, true}, + {NewDecWithPrec(-100, 0), NewDecWithPrec(-100, 0), false, false, true}, + {NewDecWithPrec(-1, 1), NewDecWithPrec(-1, 1), false, false, true}, + {NewDecWithPrec(3333, 3), NewDecWithPrec(3333, 3), false, false, true}, + + {NewDecWithPrec(0, 0), NewDecWithPrec(3333, 3), false, true, false}, + {NewDecWithPrec(0, 0), NewDecWithPrec(100, 0), false, true, false}, + {NewDecWithPrec(-1, 0), NewDecWithPrec(3333, 3), false, true, false}, + {NewDecWithPrec(-1, 0), NewDecWithPrec(100, 0), false, true, false}, + {NewDecWithPrec(1111, 3), NewDecWithPrec(100, 0), false, true, false}, + {NewDecWithPrec(1111, 3), NewDecWithPrec(3333, 3), false, true, false}, + {NewDecWithPrec(-3333, 3), NewDecWithPrec(-1111, 3), false, true, false}, + + {NewDecWithPrec(3333, 3), NewDecWithPrec(0, 0), true, false, false}, + {NewDecWithPrec(100, 0), NewDecWithPrec(0, 0), true, false, false}, + {NewDecWithPrec(3333, 3), NewDecWithPrec(-1, 0), true, false, false}, + {NewDecWithPrec(100, 0), NewDecWithPrec(-1, 0), true, false, false}, + {NewDecWithPrec(100, 0), NewDecWithPrec(1111, 3), true, false, false}, + {NewDecWithPrec(3333, 3), NewDecWithPrec(1111, 3), true, false, false}, + {NewDecWithPrec(-1111, 3), NewDecWithPrec(-3333, 3), true, false, false}, + } + + for tcIndex, tc := range tests { + s.Require().Equal(tc.gt, tc.d1.GT(tc.d2), "GT result is incorrect, tc %d", tcIndex) + s.Require().Equal(tc.lt, tc.d1.LT(tc.d2), "LT result is incorrect, tc %d", tcIndex) + s.Require().Equal(tc.eq, tc.d1.Equal(tc.d2), "equality result is incorrect, tc %d", tcIndex) + } + +} + +func (s *decimalTestSuite) TestDecsEqual() { + tests := []struct { + d1s, d2s []BigDec + eq bool + }{ + {[]BigDec{NewBigDec(0)}, []BigDec{NewBigDec(0)}, true}, + {[]BigDec{NewBigDec(0)}, []BigDec{NewBigDec(1)}, false}, + {[]BigDec{NewBigDec(0)}, []BigDec{}, false}, + {[]BigDec{NewBigDec(0), NewBigDec(1)}, []BigDec{NewBigDec(0), NewBigDec(1)}, true}, + {[]BigDec{NewBigDec(1), NewBigDec(0)}, []BigDec{NewBigDec(1), NewBigDec(0)}, true}, + {[]BigDec{NewBigDec(1), NewBigDec(0)}, []BigDec{NewBigDec(0), NewBigDec(1)}, false}, + {[]BigDec{NewBigDec(1), NewBigDec(0)}, []BigDec{NewBigDec(1)}, false}, + {[]BigDec{NewBigDec(1), NewBigDec(2)}, []BigDec{NewBigDec(2), NewBigDec(4)}, false}, + {[]BigDec{NewBigDec(3), NewBigDec(18)}, []BigDec{NewBigDec(1), NewBigDec(6)}, false}, + } + + for tcIndex, tc := range tests { + s.Require().Equal(tc.eq, DecsEqual(tc.d1s, tc.d2s), "equality of decional arrays is incorrect, tc %d", tcIndex) + s.Require().Equal(tc.eq, DecsEqual(tc.d2s, tc.d1s), "equality of decional arrays is incorrect (converse), tc %d", tcIndex) + } +} + +func (s *decimalTestSuite) TestArithmetic() { + tests := []struct { + d1, d2 BigDec + expMul, expMulTruncate BigDec + expQuo, expQuoRoundUp, expQuoTruncate BigDec + expAdd, expSub BigDec + }{ + // d1 d2 MUL MulTruncate QUO QUORoundUp QUOTrunctate ADD SUB + {NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0)}, + {NewBigDec(1), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(1), NewBigDec(1)}, + {NewBigDec(0), NewBigDec(1), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(1), NewBigDec(-1)}, + {NewBigDec(0), NewBigDec(-1), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(-1), NewBigDec(1)}, + {NewBigDec(-1), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(0), NewBigDec(-1), NewBigDec(-1)}, + + {NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(2), NewBigDec(0)}, + {NewBigDec(-1), NewBigDec(-1), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(-2), NewBigDec(0)}, + {NewBigDec(1), NewBigDec(-1), NewBigDec(-1), NewBigDec(-1), NewBigDec(-1), NewBigDec(-1), NewBigDec(-1), NewBigDec(0), NewBigDec(2)}, + {NewBigDec(-1), NewBigDec(1), NewBigDec(-1), NewBigDec(-1), NewBigDec(-1), NewBigDec(-1), NewBigDec(-1), NewBigDec(0), NewBigDec(-2)}, + + {NewBigDec(3), NewBigDec(7), NewBigDec(21), NewBigDec(21), + NewDecWithPrec(428571428571428571, 18), NewDecWithPrec(428571428571428572, 18), NewDecWithPrec(428571428571428571, 18), + NewBigDec(10), NewBigDec(-4)}, + {NewBigDec(2), NewBigDec(4), NewBigDec(8), NewBigDec(8), NewDecWithPrec(5, 1), NewDecWithPrec(5, 1), NewDecWithPrec(5, 1), + NewBigDec(6), NewBigDec(-2)}, + + {NewBigDec(100), NewBigDec(100), NewBigDec(10000), NewBigDec(10000), NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(200), NewBigDec(0)}, + + {NewDecWithPrec(15, 1), NewDecWithPrec(15, 1), NewDecWithPrec(225, 2), NewDecWithPrec(225, 2), + NewBigDec(1), NewBigDec(1), NewBigDec(1), NewBigDec(3), NewBigDec(0)}, + {NewDecWithPrec(3333, 4), NewDecWithPrec(333, 4), NewDecWithPrec(1109889, 8), NewDecWithPrec(1109889, 8), + MustNewDecFromStr("10.009009009009009009"), MustNewDecFromStr("10.009009009009009010"), MustNewDecFromStr("10.009009009009009009"), + NewDecWithPrec(3666, 4), NewDecWithPrec(3, 1)}, + } + + for tcIndex, tc := range tests { + tc := tc + resAdd := tc.d1.Add(tc.d2) + resSub := tc.d1.Sub(tc.d2) + resMul := tc.d1.Mul(tc.d2) + resMulTruncate := tc.d1.MulTruncate(tc.d2) + s.Require().True(tc.expAdd.Equal(resAdd), "exp %v, res %v, tc %d", tc.expAdd, resAdd, tcIndex) + s.Require().True(tc.expSub.Equal(resSub), "exp %v, res %v, tc %d", tc.expSub, resSub, tcIndex) + s.Require().True(tc.expMul.Equal(resMul), "exp %v, res %v, tc %d", tc.expMul, resMul, tcIndex) + s.Require().True(tc.expMulTruncate.Equal(resMulTruncate), "exp %v, res %v, tc %d", tc.expMulTruncate, resMulTruncate, tcIndex) + + if tc.d2.IsZero() { // panic for divide by zero + s.Require().Panics(func() { tc.d1.Quo(tc.d2) }) + } else { + resQuo := tc.d1.Quo(tc.d2) + s.Require().True(tc.expQuo.Equal(resQuo), "exp %v, res %v, tc %d", tc.expQuo.String(), resQuo.String(), tcIndex) + + resQuoRoundUp := tc.d1.QuoRoundUp(tc.d2) + s.Require().True(tc.expQuoRoundUp.Equal(resQuoRoundUp), "exp %v, res %v, tc %d", + tc.expQuoRoundUp.String(), resQuoRoundUp.String(), tcIndex) + + resQuoTruncate := tc.d1.QuoTruncate(tc.d2) + s.Require().True(tc.expQuoTruncate.Equal(resQuoTruncate), "exp %v, res %v, tc %d", + tc.expQuoTruncate.String(), resQuoTruncate.String(), tcIndex) + } + } +} + +func (s *decimalTestSuite) TestBankerRoundChop() { + tests := []struct { + d1 BigDec + exp int64 + }{ + {s.mustNewDecFromStr("0.25"), 0}, + {s.mustNewDecFromStr("0"), 0}, + {s.mustNewDecFromStr("1"), 1}, + {s.mustNewDecFromStr("0.75"), 1}, + {s.mustNewDecFromStr("0.5"), 0}, + {s.mustNewDecFromStr("7.5"), 8}, + {s.mustNewDecFromStr("1.5"), 2}, + {s.mustNewDecFromStr("2.5"), 2}, + {s.mustNewDecFromStr("0.545"), 1}, // 0.545-> 1 even though 5 is first decimal and 1 not even + {s.mustNewDecFromStr("1.545"), 2}, + } + + for tcIndex, tc := range tests { + resNeg := tc.d1.Neg().RoundInt64() + s.Require().Equal(-1*tc.exp, resNeg, "negative tc %d", tcIndex) + + resPos := tc.d1.RoundInt64() + s.Require().Equal(tc.exp, resPos, "positive tc %d", tcIndex) + } +} + +func (s *decimalTestSuite) TestTruncate() { + tests := []struct { + d1 BigDec + exp int64 + }{ + {s.mustNewDecFromStr("0"), 0}, + {s.mustNewDecFromStr("0.25"), 0}, + {s.mustNewDecFromStr("0.75"), 0}, + {s.mustNewDecFromStr("1"), 1}, + {s.mustNewDecFromStr("1.5"), 1}, + {s.mustNewDecFromStr("7.5"), 7}, + {s.mustNewDecFromStr("7.6"), 7}, + {s.mustNewDecFromStr("7.4"), 7}, + {s.mustNewDecFromStr("100.1"), 100}, + {s.mustNewDecFromStr("1000.1"), 1000}, + } + + for tcIndex, tc := range tests { + resNeg := tc.d1.Neg().TruncateInt64() + s.Require().Equal(-1*tc.exp, resNeg, "negative tc %d", tcIndex) + + resPos := tc.d1.TruncateInt64() + s.Require().Equal(tc.exp, resPos, "positive tc %d", tcIndex) + } +} + +func (s *decimalTestSuite) TestStringOverflow() { + // two random 64 bit primes + dec1, err := NewDecFromStr("51643150036226787134389711697696177267") + s.Require().NoError(err) + dec2, err := NewDecFromStr("-31798496660535729618459429845579852627") + s.Require().NoError(err) + dec3 := dec1.Add(dec2) + s.Require().Equal( + "19844653375691057515930281852116324640.000000000000000000", + dec3.String(), + ) +} + +func (s *decimalTestSuite) TestDecMulInt() { + tests := []struct { + sdkDec BigDec + sdkInt BigInt + want BigDec + }{ + {NewBigDec(10), NewInt(2), NewBigDec(20)}, + {NewBigDec(1000000), NewInt(100), NewBigDec(100000000)}, + {NewDecWithPrec(1, 1), NewInt(10), NewBigDec(1)}, + {NewDecWithPrec(1, 5), NewInt(20), NewDecWithPrec(2, 4)}, + } + for i, tc := range tests { + got := tc.sdkDec.MulInt(tc.sdkInt) + s.Require().Equal(tc.want, got, "Incorrect result on test case %d", i) + } +} + +func (s *decimalTestSuite) TestDecCeil() { + testCases := []struct { + input BigDec + expected BigDec + }{ + {NewDecWithPrec(1000000000000000, Precision), NewBigDec(1)}, // 0.001 => 1.0 + {NewDecWithPrec(-1000000000000000, Precision), ZeroDec()}, // -0.001 => 0.0 + {ZeroDec(), ZeroDec()}, // 0.0 => 0.0 + {NewDecWithPrec(900000000000000000, Precision), NewBigDec(1)}, // 0.9 => 1.0 + {NewDecWithPrec(4001000000000000000, Precision), NewBigDec(5)}, // 4.001 => 5.0 + {NewDecWithPrec(-4001000000000000000, Precision), NewBigDec(-4)}, // -4.001 => -4.0 + {NewDecWithPrec(4700000000000000000, Precision), NewBigDec(5)}, // 4.7 => 5.0 + {NewDecWithPrec(-4700000000000000000, Precision), NewBigDec(-4)}, // -4.7 => -4.0 + } + + for i, tc := range testCases { + res := tc.input.Ceil() + s.Require().Equal(tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestPower() { + testCases := []struct { + input BigDec + power uint64 + expected BigDec + }{ + {OneDec(), 10, OneDec()}, // 1.0 ^ (10) => 1.0 + {NewDecWithPrec(5, 1), 2, NewDecWithPrec(25, 2)}, // 0.5 ^ 2 => 0.25 + {NewDecWithPrec(2, 1), 2, NewDecWithPrec(4, 2)}, // 0.2 ^ 2 => 0.04 + {NewDecFromInt(NewInt(3)), 3, NewDecFromInt(NewInt(27))}, // 3 ^ 3 => 27 + {NewDecFromInt(NewInt(-3)), 4, NewDecFromInt(NewInt(81))}, // -3 ^ 4 = 81 + {NewDecWithPrec(1414213562373095049, 18), 2, NewDecFromInt(NewInt(2))}, // 1.414213562373095049 ^ 2 = 2 + } + + for i, tc := range testCases { + res := tc.input.Power(tc.power) + s.Require().True(tc.expected.Sub(res).Abs().LTE(SmallestDec()), "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestApproxRoot() { + testCases := []struct { + input BigDec + root uint64 + expected BigDec + }{ + {OneDec(), 10, OneDec()}, // 1.0 ^ (0.1) => 1.0 + {NewDecWithPrec(25, 2), 2, NewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5 + {NewDecWithPrec(4, 2), 2, NewDecWithPrec(2, 1)}, // 0.04 ^ (0.5) => 0.2 + {NewDecFromInt(NewInt(27)), 3, NewDecFromInt(NewInt(3))}, // 27 ^ (1/3) => 3 + {NewDecFromInt(NewInt(-81)), 4, NewDecFromInt(NewInt(-3))}, // -81 ^ (0.25) => -3 + {NewDecFromInt(NewInt(2)), 2, NewDecWithPrec(1414213562373095049, 18)}, // 2 ^ (0.5) => 1.414213562373095049 + {NewDecWithPrec(1005, 3), 31536000, MustNewDecFromStr("1.000000000158153904")}, // 1.005 ^ (1/31536000) ≈ 1.00000000016 + {SmallestDec(), 2, NewDecWithPrec(1, 9)}, // 1e-18 ^ (0.5) => 1e-9 + {SmallestDec(), 3, MustNewDecFromStr("0.000000999999999997")}, // 1e-18 ^ (1/3) => 1e-6 + {NewDecWithPrec(1, 8), 3, MustNewDecFromStr("0.002154434690031900")}, // 1e-8 ^ (1/3) ≈ 0.00215443469 + } + + // In the case of 1e-8 ^ (1/3), the result repeats every 5 iterations starting from iteration 24 + // (i.e. 24, 29, 34, ... give the same result) and never converges enough. The maximum number of + // iterations (100) causes the result at iteration 100 to be returned, regardless of convergence. + + for i, tc := range testCases { + res, err := tc.input.ApproxRoot(tc.root) + s.Require().NoError(err) + s.Require().True(tc.expected.Sub(res).Abs().LTE(SmallestDec()), "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestApproxSqrt() { + testCases := []struct { + input BigDec + expected BigDec + }{ + {OneDec(), OneDec()}, // 1.0 => 1.0 + {NewDecWithPrec(25, 2), NewDecWithPrec(5, 1)}, // 0.25 => 0.5 + {NewDecWithPrec(4, 2), NewDecWithPrec(2, 1)}, // 0.09 => 0.3 + {NewDecFromInt(NewInt(9)), NewDecFromInt(NewInt(3))}, // 9 => 3 + {NewDecFromInt(NewInt(-9)), NewDecFromInt(NewInt(-3))}, // -9 => -3 + {NewDecFromInt(NewInt(2)), NewDecWithPrec(1414213562373095049, 18)}, // 2 => 1.414213562373095049 + } + + for i, tc := range testCases { + res, err := tc.input.ApproxSqrt() + s.Require().NoError(err) + s.Require().Equal(tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestDecSortableBytes() { + tests := []struct { + d BigDec + want []byte + }{ + {NewBigDec(0), []byte("000000000000000000.000000000000000000")}, + {NewBigDec(1), []byte("000000000000000001.000000000000000000")}, + {NewBigDec(10), []byte("000000000000000010.000000000000000000")}, + {NewBigDec(12340), []byte("000000000000012340.000000000000000000")}, + {NewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")}, + {NewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")}, + {NewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")}, + {NewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")}, + {NewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")}, + {NewBigDec(1000000000000000000), []byte("max")}, + {NewBigDec(-1000000000000000000), []byte("--")}, + } + for tcIndex, tc := range tests { + s.Require().Equal(tc.want, SortableDecBytes(tc.d), "bad String(), index: %v", tcIndex) + } + + s.Require().Panics(func() { SortableDecBytes(NewBigDec(1000000000000000001)) }) + s.Require().Panics(func() { SortableDecBytes(NewBigDec(-1000000000000000001)) }) +} + +func (s *decimalTestSuite) TestDecEncoding() { + testCases := []struct { + input BigDec + rawBz string + jsonStr string + yamlStr string + }{ + { + NewBigDec(0), "30", + "\"0.000000000000000000\"", + "\"0.000000000000000000\"\n", + }, + { + NewDecWithPrec(4, 2), + "3430303030303030303030303030303030", + "\"0.040000000000000000\"", + "\"0.040000000000000000\"\n", + }, + { + NewDecWithPrec(-4, 2), + "2D3430303030303030303030303030303030", + "\"-0.040000000000000000\"", + "\"-0.040000000000000000\"\n", + }, + { + NewDecWithPrec(1414213562373095049, 18), + "31343134323133353632333733303935303439", + "\"1.414213562373095049\"", + "\"1.414213562373095049\"\n", + }, + { + NewDecWithPrec(-1414213562373095049, 18), + "2D31343134323133353632333733303935303439", + "\"-1.414213562373095049\"", + "\"-1.414213562373095049\"\n", + }, + } + + for _, tc := range testCases { + bz, err := tc.input.Marshal() + s.Require().NoError(err) + s.Require().Equal(tc.rawBz, fmt.Sprintf("%X", bz)) + + var other BigDec + s.Require().NoError((&other).Unmarshal(bz)) + s.Require().True(tc.input.Equal(other)) + + bz, err = json.Marshal(tc.input) + s.Require().NoError(err) + s.Require().Equal(tc.jsonStr, string(bz)) + s.Require().NoError(json.Unmarshal(bz, &other)) + s.Require().True(tc.input.Equal(other)) + + bz, err = yaml.Marshal(tc.input) + s.Require().NoError(err) + s.Require().Equal(tc.yamlStr, string(bz)) + } +} + +// Showcase that different orders of operations causes different results. +func (s *decimalTestSuite) TestOperationOrders() { + n1 := NewBigDec(10) + n2 := NewBigDec(1000000010) + s.Require().Equal(n1.Mul(n2).Quo(n2), NewBigDec(10)) + s.Require().NotEqual(n1.Mul(n2).Quo(n2), n1.Quo(n2).Mul(n2)) +} + +func BenchmarkMarshalTo(b *testing.B) { + b.ReportAllocs() + bis := []struct { + in BigDec + want []byte + }{ + { + NewBigDec(1e8), []byte{ + 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + }, + }, + {NewBigDec(0), []byte{0x30}}, + } + data := make([]byte, 100) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + for _, bi := range bis { + if n, err := bi.in.MarshalTo(data); err != nil { + b.Fatal(err) + } else { + if !bytes.Equal(data[:n], bi.want) { + b.Fatalf("Mismatch\nGot: % x\nWant: % x\n", data[:n], bi.want) + } + } + } + } +} diff --git a/osmomath/int.go b/osmomath/int.go new file mode 100644 index 00000000000..1be621e7820 --- /dev/null +++ b/osmomath/int.go @@ -0,0 +1,437 @@ +package osmomath + +import ( + "encoding" + "encoding/json" + "fmt" + "testing" + + "math/big" +) + +const maxBitLen = 512 + +func newIntegerFromString(s string) (*big.Int, bool) { + return new(big.Int).SetString(s, 0) +} + +func equal(i *big.Int, i2 *big.Int) bool { return i.Cmp(i2) == 0 } + +func gt(i *big.Int, i2 *big.Int) bool { return i.Cmp(i2) == 1 } + +func gte(i *big.Int, i2 *big.Int) bool { return i.Cmp(i2) >= 0 } + +func lt(i *big.Int, i2 *big.Int) bool { return i.Cmp(i2) == -1 } + +func lte(i *big.Int, i2 *big.Int) bool { return i.Cmp(i2) <= 0 } + +func add(i *big.Int, i2 *big.Int) *big.Int { return new(big.Int).Add(i, i2) } + +func sub(i *big.Int, i2 *big.Int) *big.Int { return new(big.Int).Sub(i, i2) } + +func mul(i *big.Int, i2 *big.Int) *big.Int { return new(big.Int).Mul(i, i2) } + +func div(i *big.Int, i2 *big.Int) *big.Int { return new(big.Int).Quo(i, i2) } + +func mod(i *big.Int, i2 *big.Int) *big.Int { return new(big.Int).Mod(i, i2) } + +func neg(i *big.Int) *big.Int { return new(big.Int).Neg(i) } + +func abs(i *big.Int) *big.Int { return new(big.Int).Abs(i) } + +func min(i *big.Int, i2 *big.Int) *big.Int { + if i.Cmp(i2) == 1 { + return new(big.Int).Set(i2) + } + + return new(big.Int).Set(i) +} + +func max(i *big.Int, i2 *big.Int) *big.Int { + if i.Cmp(i2) == -1 { + return new(big.Int).Set(i2) + } + + return new(big.Int).Set(i) +} + +func unmarshalText(i *big.Int, text string) error { + if err := i.UnmarshalText([]byte(text)); err != nil { + return err + } + + if i.BitLen() > maxBitLen { + return fmt.Errorf("integer out of range: %s", text) + } + + return nil +} + +// Int wraps big.Int with a 257 bit range bound +// Checks overflow, underflow and division by zero +// Exists in range from -(2^256 - 1) to 2^256 - 1 +type BigInt struct { + i *big.Int +} + +// BigInt converts Int to big.Int +func (i BigInt) BigInt() *big.Int { + if i.IsNil() { + return nil + } + return new(big.Int).Set(i.i) +} + +// IsNil returns true if Int is uninitialized +func (i BigInt) IsNil() bool { + return i.i == nil +} + +// NewInt constructs Int from int64 +func NewInt(n int64) BigInt { + return BigInt{big.NewInt(n)} +} + +// NewIntFromUint64 constructs an Int from a uint64. +func NewIntFromUint64(n uint64) BigInt { + b := big.NewInt(0) + b.SetUint64(n) + return BigInt{b} +} + +// NewIntFromBigInt constructs Int from big.Int. If the provided big.Int is nil, +// it returns an empty instance. This function panics if the bit length is > 256. +func NewIntFromBigInt(i *big.Int) BigInt { + if i == nil { + return BigInt{} + } + + if i.BitLen() > maxBitLen { + panic("NewIntFromBigInt() out of bound") + } + return BigInt{i} +} + +// NewIntFromString constructs Int from string +func NewIntFromString(s string) (res BigInt, ok bool) { + i, ok := newIntegerFromString(s) + if !ok { + return + } + // Check overflow + if i.BitLen() > maxBitLen { + ok = false + return + } + return BigInt{i}, true +} + +// NewIntWithDecimal constructs Int with decimal +// Result value is n*10^dec +func NewIntWithDecimal(n int64, dec int) BigInt { + if dec < 0 { + panic("NewIntWithDecimal() decimal is negative") + } + exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(dec)), nil) + i := new(big.Int) + i.Mul(big.NewInt(n), exp) + + // Check overflow + if i.BitLen() > maxBitLen { + panic("NewIntWithDecimal() out of bound") + } + return BigInt{i} +} + +// ZeroInt returns Int value with zero +func ZeroInt() BigInt { return BigInt{big.NewInt(0)} } + +// OneInt returns Int value with one +func OneInt() BigInt { return BigInt{big.NewInt(1)} } + +// ToDec converts Int to Dec +func (i BigInt) ToDec() BigDec { + return NewDecFromInt(i) +} + +// Int64 converts Int to int64 +// Panics if the value is out of range +func (i BigInt) Int64() int64 { + if !i.i.IsInt64() { + panic("Int64() out of bound") + } + return i.i.Int64() +} + +// IsInt64 returns true if Int64() not panics +func (i BigInt) IsInt64() bool { + return i.i.IsInt64() +} + +// Uint64 converts Int to uint64 +// Panics if the value is out of range +func (i BigInt) Uint64() uint64 { + if !i.i.IsUint64() { + panic("Uint64() out of bounds") + } + return i.i.Uint64() +} + +// IsUint64 returns true if Uint64() not panics +func (i BigInt) IsUint64() bool { + return i.i.IsUint64() +} + +// IsZero returns true if Int is zero +func (i BigInt) IsZero() bool { + return i.i.Sign() == 0 +} + +// IsNegative returns true if Int is negative +func (i BigInt) IsNegative() bool { + return i.i.Sign() == -1 +} + +// IsPositive returns true if Int is positive +func (i BigInt) IsPositive() bool { + return i.i.Sign() == 1 +} + +// Sign returns sign of Int +func (i BigInt) Sign() int { + return i.i.Sign() +} + +// Equal compares two Ints +func (i BigInt) Equal(i2 BigInt) bool { + return equal(i.i, i2.i) +} + +// GT returns true if first Int is greater than second +func (i BigInt) GT(i2 BigInt) bool { + return gt(i.i, i2.i) +} + +// GTE returns true if receiver Int is greater than or equal to the parameter +// Int. +func (i BigInt) GTE(i2 BigInt) bool { + return gte(i.i, i2.i) +} + +// LT returns true if first Int is lesser than second +func (i BigInt) LT(i2 BigInt) bool { + return lt(i.i, i2.i) +} + +// LTE returns true if first Int is less than or equal to second +func (i BigInt) LTE(i2 BigInt) bool { + return lte(i.i, i2.i) +} + +// Add adds Int from another +func (i BigInt) Add(i2 BigInt) (res BigInt) { + res = BigInt{add(i.i, i2.i)} + // Check overflow + if res.i.BitLen() > maxBitLen { + panic("Int overflow") + } + return +} + +// AddRaw adds int64 to Int +func (i BigInt) AddRaw(i2 int64) BigInt { + return i.Add(NewInt(i2)) +} + +// Sub subtracts Int from another +func (i BigInt) Sub(i2 BigInt) (res BigInt) { + res = BigInt{sub(i.i, i2.i)} + // Check overflow + if res.i.BitLen() > maxBitLen { + panic("Int overflow") + } + return +} + +// SubRaw subtracts int64 from Int +func (i BigInt) SubRaw(i2 int64) BigInt { + return i.Sub(NewInt(i2)) +} + +// Mul multiples two Ints +func (i BigInt) Mul(i2 BigInt) (res BigInt) { + // Check overflow + if i.i.BitLen()+i2.i.BitLen()-1 > maxBitLen { + panic("Int overflow") + } + res = BigInt{mul(i.i, i2.i)} + // Check overflow if sign of both are same + if res.i.BitLen() > maxBitLen { + panic("Int overflow") + } + return +} + +// MulRaw multipies Int and int64 +func (i BigInt) MulRaw(i2 int64) BigInt { + return i.Mul(NewInt(i2)) +} + +// Quo divides Int with Int +func (i BigInt) Quo(i2 BigInt) (res BigInt) { + // Check division-by-zero + if i2.i.Sign() == 0 { + panic("Division by zero") + } + return BigInt{div(i.i, i2.i)} +} + +// QuoRaw divides Int with int64 +func (i BigInt) QuoRaw(i2 int64) BigInt { + return i.Quo(NewInt(i2)) +} + +// Mod returns remainder after dividing with Int +func (i BigInt) Mod(i2 BigInt) BigInt { + if i2.Sign() == 0 { + panic("division-by-zero") + } + return BigInt{mod(i.i, i2.i)} +} + +// ModRaw returns remainder after dividing with int64 +func (i BigInt) ModRaw(i2 int64) BigInt { + return i.Mod(NewInt(i2)) +} + +// Neg negates Int +func (i BigInt) Neg() (res BigInt) { + return BigInt{neg(i.i)} +} + +// Abs returns the absolute value of Int. +func (i BigInt) Abs() BigInt { + return BigInt{abs(i.i)} +} + +// return the minimum of the ints +func MinInt(i1, i2 BigInt) BigInt { + return BigInt{min(i1.BigInt(), i2.BigInt())} +} + +// MaxInt returns the maximum between two integers. +func MaxInt(i, i2 BigInt) BigInt { + return BigInt{max(i.BigInt(), i2.BigInt())} +} + +// Human readable string +func (i BigInt) String() string { + return i.i.String() +} + +// MarshalJSON defines custom encoding scheme +func (i BigInt) MarshalJSON() ([]byte, error) { + if i.i == nil { // Necessary since default Uint initialization has i.i as nil + i.i = new(big.Int) + } + return marshalJSON(i.i) +} + +// UnmarshalJSON defines custom decoding scheme +func (i *BigInt) UnmarshalJSON(bz []byte) error { + if i.i == nil { // Necessary since default Int initialization has i.i as nil + i.i = new(big.Int) + } + return unmarshalJSON(i.i, bz) +} + +// MarshalJSON for custom encoding scheme +// Must be encoded as a string for JSON precision +func marshalJSON(i encoding.TextMarshaler) ([]byte, error) { + text, err := i.MarshalText() + if err != nil { + return nil, err + } + + return json.Marshal(string(text)) +} + +// UnmarshalJSON for custom decoding scheme +// Must be encoded as a string for JSON precision +func unmarshalJSON(i *big.Int, bz []byte) error { + var text string + if err := json.Unmarshal(bz, &text); err != nil { + return err + } + + return unmarshalText(i, text) +} + +// MarshalYAML returns the YAML representation. +func (i BigInt) MarshalYAML() (interface{}, error) { + return i.String(), nil +} + +// Marshal implements the gogo proto custom type interface. +func (i BigInt) Marshal() ([]byte, error) { + if i.i == nil { + i.i = new(big.Int) + } + return i.i.MarshalText() +} + +// MarshalTo implements the gogo proto custom type interface. +func (i *BigInt) MarshalTo(data []byte) (n int, err error) { + if i.i == nil { + i.i = new(big.Int) + } + if i.i.BitLen() == 0 { // The value 0 + copy(data, []byte{0x30}) + return 1, nil + } + + bz, err := i.Marshal() + if err != nil { + return 0, err + } + + copy(data, bz) + return len(bz), nil +} + +// Unmarshal implements the gogo proto custom type interface. +func (i *BigInt) Unmarshal(data []byte) error { + if len(data) == 0 { + i = nil + return nil + } + + if i.i == nil { + i.i = new(big.Int) + } + + if err := i.i.UnmarshalText(data); err != nil { + return err + } + + if i.i.BitLen() > maxBitLen { + return fmt.Errorf("integer out of range; got: %d, max: %d", i.i.BitLen(), maxBitLen) + } + + return nil +} + +// Size implements the gogo proto custom type interface. +func (i *BigInt) Size() int { + bz, _ := i.Marshal() + return len(bz) +} + +// Override Amino binary serialization by proxying to protobuf. +func (i BigInt) MarshalAmino() ([]byte, error) { return i.Marshal() } +func (i *BigInt) UnmarshalAmino(bz []byte) error { return i.Unmarshal(bz) } + +// intended to be used with require/assert: require.True(IntEq(...)) +func IntEq(t *testing.T, exp, got BigInt) (*testing.T, bool, string, string, string) { + return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() +} diff --git a/osmomath/int_test.go b/osmomath/int_test.go new file mode 100644 index 00000000000..a770301c017 --- /dev/null +++ b/osmomath/int_test.go @@ -0,0 +1,472 @@ +package osmomath + +import ( + "fmt" + "math/big" + "math/rand" + "strconv" + "testing" + + "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type intTestSuite struct { + suite.Suite +} + +func TestIntTestSuite(t *testing.T) { + suite.Run(t, new(intTestSuite)) +} + +func (s *intTestSuite) SetupSuite() { + s.T().Parallel() +} + +func (s *intTestSuite) TestFromInt64() { + for n := 0; n < 20; n++ { + r := rand.Int63() + s.Require().Equal(r, NewInt(r).Int64()) + } +} + +func (s *intTestSuite) TestFromUint64() { + for n := 0; n < 20; n++ { + r := rand.Uint64() + s.Require().True(NewIntFromUint64(r).IsUint64()) + s.Require().Equal(r, NewIntFromUint64(r).Uint64()) + } +} + +func (s *intTestSuite) TestIntPanic() { + // Max Int = 2^512-1 = 1.340781e+154 + // Min Int = -(2^512-1) = -1.340781e+154 + s.Require().NotPanics(func() { NewIntWithDecimal(4, 153) }) + i1 := NewIntWithDecimal(5, 153) + s.Require().NotPanics(func() { NewIntWithDecimal(5, 153) }) + i2 := NewIntWithDecimal(6, 153) + s.Require().NotPanics(func() { NewIntWithDecimal(6, 153) }) + i3 := NewIntWithDecimal(7, 153) + + s.Require().Panics(func() { NewIntWithDecimal(2, 154) }) + s.Require().Panics(func() { NewIntWithDecimal(9, 160) }) + + // Overflow check + s.Require().NotPanics(func() { i1.Add(i1) }) + s.Require().NotPanics(func() { i2.Add(i2) }) + s.Require().Panics(func() { i3.Add(i3) }) + + s.Require().NotPanics(func() { i1.Sub(i1.Neg()) }) + s.Require().NotPanics(func() { i2.Sub(i2.Neg()) }) + s.Require().Panics(func() { i3.Sub(i3.Neg()) }) + + s.Require().Panics(func() { i1.Mul(i1) }) + s.Require().Panics(func() { i2.Mul(i2) }) + s.Require().Panics(func() { i3.Mul(i3) }) + + s.Require().Panics(func() { i1.Neg().Mul(i1.Neg()) }) + s.Require().Panics(func() { i2.Neg().Mul(i2.Neg()) }) + s.Require().Panics(func() { i3.Neg().Mul(i3.Neg()) }) + + // Underflow check + i3n := i3.Neg() + s.Require().NotPanics(func() { i3n.Sub(i1) }) + s.Require().NotPanics(func() { i3n.Sub(i2) }) + s.Require().Panics(func() { i3n.Sub(i3) }) + + s.Require().NotPanics(func() { i3n.Add(i1.Neg()) }) + s.Require().NotPanics(func() { i3n.Add(i2.Neg()) }) + s.Require().Panics(func() { i3n.Add(i3.Neg()) }) + + s.Require().Panics(func() { i1.Mul(i1.Neg()) }) + s.Require().Panics(func() { i2.Mul(i2.Neg()) }) + s.Require().Panics(func() { i3.Mul(i3.Neg()) }) + + // Bound check + intmax := NewIntFromBigInt(new(big.Int).Sub(new(big.Int).Exp(big.NewInt(2), big.NewInt(512), nil), big.NewInt(1))) + intmin := intmax.Neg() + s.Require().NotPanics(func() { intmax.Add(ZeroInt()) }) + s.Require().NotPanics(func() { intmin.Sub(ZeroInt()) }) + s.Require().Panics(func() { intmax.Add(OneInt()) }) + s.Require().Panics(func() { intmin.Sub(OneInt()) }) + + s.Require().NotPanics(func() { NewIntFromBigInt(nil) }) + s.Require().True(NewIntFromBigInt(nil).IsNil()) + + // Division-by-zero check + s.Require().Panics(func() { i1.Quo(NewInt(0)) }) + + s.Require().NotPanics(func() { BigInt{}.BigInt() }) +} + +// Tests below uses randomness +// Since we are using *big.Int as underlying value +// and (U/)Int is immutable value(see TestImmutability(U/)Int) +// it is safe to use randomness in the tests +func (s *intTestSuite) TestIdentInt() { + for d := 0; d < 1000; d++ { + n := rand.Int63() + i := NewInt(n) + + ifromstr, ok := NewIntFromString(strconv.FormatInt(n, 10)) + s.Require().True(ok) + + cases := []int64{ + i.Int64(), + i.BigInt().Int64(), + ifromstr.Int64(), + NewIntFromBigInt(big.NewInt(n)).Int64(), + NewIntWithDecimal(n, 0).Int64(), + } + + for tcnum, tc := range cases { + s.Require().Equal(n, tc, "Int is modified during conversion. tc #%d", tcnum) + } + } +} + +func minint(i1, i2 int64) int64 { + if i1 < i2 { + return i1 + } + return i2 +} + +func maxint(i1, i2 int64) int64 { + if i1 > i2 { + return i1 + } + return i2 +} + +func (s *intTestSuite) TestArithInt() { + for d := 0; d < 1000; d++ { + n1 := int64(rand.Int31()) + i1 := NewInt(n1) + n2 := int64(rand.Int31()) + i2 := NewInt(n2) + + cases := []struct { + ires BigInt + nres int64 + }{ + {i1.Add(i2), n1 + n2}, + {i1.Sub(i2), n1 - n2}, + {i1.Mul(i2), n1 * n2}, + {i1.Quo(i2), n1 / n2}, + {i1.AddRaw(n2), n1 + n2}, + {i1.SubRaw(n2), n1 - n2}, + {i1.MulRaw(n2), n1 * n2}, + {i1.QuoRaw(n2), n1 / n2}, + {MinInt(i1, i2), minint(n1, n2)}, + {MaxInt(i1, i2), maxint(n1, n2)}, + {i1.Neg(), -n1}, + {i1.Abs(), n1}, + {i1.Neg().Abs(), n1}, + } + + for tcnum, tc := range cases { + s.Require().Equal(tc.nres, tc.ires.Int64(), "Int arithmetic operation does not match with int64 operation. tc #%d", tcnum) + } + } + +} + +func (s *intTestSuite) TestCompInt() { + for d := 0; d < 1000; d++ { + n1 := int64(rand.Int31()) + i1 := NewInt(n1) + n2 := int64(rand.Int31()) + i2 := NewInt(n2) + + cases := []struct { + ires bool + nres bool + }{ + {i1.Equal(i2), n1 == n2}, + {i1.GT(i2), n1 > n2}, + {i1.LT(i2), n1 < n2}, + {i1.LTE(i2), n1 <= n2}, + } + + for tcnum, tc := range cases { + s.Require().Equal(tc.nres, tc.ires, "Int comparison operation does not match with int64 operation. tc #%d", tcnum) + } + } +} + +func randint() BigInt { + return NewInt(rand.Int63()) +} + +func (s *intTestSuite) TestImmutabilityAllInt() { + ops := []func(*BigInt){ + func(i *BigInt) { _ = i.Add(randint()) }, + func(i *BigInt) { _ = i.Sub(randint()) }, + func(i *BigInt) { _ = i.Mul(randint()) }, + func(i *BigInt) { _ = i.Quo(randint()) }, + func(i *BigInt) { _ = i.AddRaw(rand.Int63()) }, + func(i *BigInt) { _ = i.SubRaw(rand.Int63()) }, + func(i *BigInt) { _ = i.MulRaw(rand.Int63()) }, + func(i *BigInt) { _ = i.QuoRaw(rand.Int63()) }, + func(i *BigInt) { _ = i.Neg() }, + func(i *BigInt) { _ = i.Abs() }, + func(i *BigInt) { _ = i.IsZero() }, + func(i *BigInt) { _ = i.Sign() }, + func(i *BigInt) { _ = i.Equal(randint()) }, + func(i *BigInt) { _ = i.GT(randint()) }, + func(i *BigInt) { _ = i.LT(randint()) }, + func(i *BigInt) { _ = i.String() }, + } + + for i := 0; i < 1000; i++ { + n := rand.Int63() + ni := NewInt(n) + + for opnum, op := range ops { + op(&ni) + + s.Require().Equal(n, ni.Int64(), "Int is modified by operation. tc #%d", opnum) + s.Require().Equal(NewInt(n), ni, "Int is modified by operation. tc #%d", opnum) + } + } +} + +func (s *intTestSuite) TestEncodingTableInt() { + var i BigInt + + cases := []struct { + i BigInt + jsonBz []byte + rawBz []byte + }{ + { + NewInt(0), + []byte("\"0\""), + []byte{0x30}, + }, + { + NewInt(100), + []byte("\"100\""), + []byte{0x31, 0x30, 0x30}, + }, + { + NewInt(-100), + []byte("\"-100\""), + []byte{0x2d, 0x31, 0x30, 0x30}, + }, + { + NewInt(51842), + []byte("\"51842\""), + []byte{0x35, 0x31, 0x38, 0x34, 0x32}, + }, + { + NewInt(-51842), + []byte("\"-51842\""), + []byte{0x2d, 0x35, 0x31, 0x38, 0x34, 0x32}, + }, + { + NewInt(19513368), + []byte("\"19513368\""), + []byte{0x31, 0x39, 0x35, 0x31, 0x33, 0x33, 0x36, 0x38}, + }, + { + NewInt(-19513368), + []byte("\"-19513368\""), + []byte{0x2d, 0x31, 0x39, 0x35, 0x31, 0x33, 0x33, 0x36, 0x38}, + }, + { + NewInt(999999999999), + []byte("\"999999999999\""), + []byte{0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39}, + }, + { + NewInt(-999999999999), + []byte("\"-999999999999\""), + []byte{0x2d, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39}, + }, + } + + for tcnum, tc := range cases { + bz, err := tc.i.MarshalJSON() + s.Require().Nil(err, "Error marshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.jsonBz, bz, "Marshaled value is different from exported. tc #%d", tcnum) + + err = (&i).UnmarshalJSON(bz) + s.Require().Nil(err, "Error unmarshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.i, i, "Unmarshaled value is different from exported. tc #%d", tcnum) + + bz, err = tc.i.Marshal() + s.Require().Nil(err, "Error marshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.rawBz, bz, "Marshaled value is different from exported. tc #%d", tcnum) + + err = (&i).Unmarshal(bz) + s.Require().Nil(err, "Error unmarshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.i, i, "Unmarshaled value is different from exported. tc #%d", tcnum) + } +} + +func (s *intTestSuite) TestEncodingTableUint() { + var i sdk.Uint + + cases := []struct { + i sdk.Uint + jsonBz []byte + rawBz []byte + }{ + { + sdk.NewUint(0), + []byte("\"0\""), + []byte{0x30}, + }, + { + sdk.NewUint(100), + []byte("\"100\""), + []byte{0x31, 0x30, 0x30}, + }, + { + sdk.NewUint(51842), + []byte("\"51842\""), + []byte{0x35, 0x31, 0x38, 0x34, 0x32}, + }, + { + sdk.NewUint(19513368), + []byte("\"19513368\""), + []byte{0x31, 0x39, 0x35, 0x31, 0x33, 0x33, 0x36, 0x38}, + }, + { + sdk.NewUint(999999999999), + []byte("\"999999999999\""), + []byte{0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39}, + }, + } + + for tcnum, tc := range cases { + bz, err := tc.i.MarshalJSON() + s.Require().Nil(err, "Error marshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.jsonBz, bz, "Marshaled value is different from exported. tc #%d", tcnum) + + err = (&i).UnmarshalJSON(bz) + s.Require().Nil(err, "Error unmarshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.i, i, "Unmarshaled value is different from exported. tc #%d", tcnum) + + bz, err = tc.i.Marshal() + s.Require().Nil(err, "Error marshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.rawBz, bz, "Marshaled value is different from exported. tc #%d", tcnum) + + err = (&i).Unmarshal(bz) + s.Require().Nil(err, "Error unmarshaling Int. tc #%d, err %s", tcnum, err) + s.Require().Equal(tc.i, i, "Unmarshaled value is different from exported. tc #%d", tcnum) + } +} + +func (s *intTestSuite) TestIntMod() { + tests := []struct { + name string + x int64 + y int64 + ret int64 + wantPanic bool + }{ + {"3 % 10", 3, 10, 3, false}, + {"10 % 3", 10, 3, 1, false}, + {"4 % 2", 4, 2, 0, false}, + {"2 % 0", 2, 0, 0, true}, + } + + for _, tt := range tests { + if tt.wantPanic { + s.Require().Panics(func() { NewInt(tt.x).Mod(NewInt(tt.y)) }) + s.Require().Panics(func() { NewInt(tt.x).ModRaw(tt.y) }) + return + } + s.Require().True(NewInt(tt.x).Mod(NewInt(tt.y)).Equal(NewInt(tt.ret))) + s.Require().True(NewInt(tt.x).ModRaw(tt.y).Equal(NewInt(tt.ret))) + } +} + +func (s *intTestSuite) TestIntEq() { + _, resp, _, _, _ := IntEq(s.T(), ZeroInt(), ZeroInt()) + s.Require().True(resp) + _, resp, _, _, _ = IntEq(s.T(), OneInt(), ZeroInt()) + s.Require().False(resp) +} + +func TestRoundTripMarshalToInt(t *testing.T) { + var values = []int64{ + 0, + 1, + 1 << 10, + 1<<10 - 3, + 1<<63 - 1, + 1<<32 - 7, + 1<<22 - 8, + } + + for _, value := range values { + value := value + t.Run(fmt.Sprintf("%d", value), func(t *testing.T) { + t.Parallel() + + var scratch [20]byte + iv := NewInt(value) + n, err := iv.MarshalTo(scratch[:]) + if err != nil { + t.Fatal(err) + } + rt := new(BigInt) + if err := rt.Unmarshal(scratch[:n]); err != nil { + t.Fatal(err) + } + if !rt.Equal(iv) { + t.Fatalf("roundtrip=%q != original=%q", rt, iv) + } + }) + } +} + +func (s *intTestSuite) TestEncodingRandom() { + for i := 0; i < 1000; i++ { + n := rand.Int63() + ni := NewInt(n) + var ri BigInt + + str, err := ni.Marshal() + s.Require().Nil(err) + err = (&ri).Unmarshal(str) + s.Require().Nil(err) + + s.Require().Equal(ni, ri, "binary mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String()) + s.Require().True(ni.i != ri.i, "pointer addresses are equal; tc #%d", i) + + bz, err := ni.MarshalJSON() + s.Require().Nil(err) + err = (&ri).UnmarshalJSON(bz) + s.Require().Nil(err) + + s.Require().Equal(ni, ri, "json mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String()) + s.Require().True(ni.i != ri.i, "pointer addresses are equal; tc #%d", i) + } + + for i := 0; i < 1000; i++ { + n := rand.Uint64() + ni := sdk.NewUint(n) + + var ri sdk.Uint + + str, err := ni.Marshal() + s.Require().Nil(err) + err = (&ri).Unmarshal(str) + s.Require().Nil(err) + + s.Require().Equal(ni, ri, "binary mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String()) + + bz, err := ni.MarshalJSON() + s.Require().Nil(err) + err = (&ri).UnmarshalJSON(bz) + s.Require().Nil(err) + + s.Require().Equal(ni, ri, "json mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String()) + } +} From cf3317c0ad58d2f054bf8d985657f6553a1ad460 Mon Sep 17 00:00:00 2001 From: mconcat Date: Tue, 3 May 2022 22:40:57 +0900 Subject: [PATCH 2/9] Antehandler for blocking validator configured sender (#1282) --- ante/sendblock.go | 83 ++++++++++++++++++++++++++++++++++++++++++ ante/sendblock_test.go | 37 +++++++++++++++++++ app/ante.go | 4 ++ 3 files changed, 124 insertions(+) create mode 100644 ante/sendblock.go create mode 100644 ante/sendblock_test.go diff --git a/ante/sendblock.go b/ante/sendblock.go new file mode 100644 index 00000000000..c34faba4d38 --- /dev/null +++ b/ante/sendblock.go @@ -0,0 +1,83 @@ +package ante + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/spf13/cast" + + servertypes "github.com/cosmos/cosmos-sdk/server/types" +) + +type SendBlockOptions struct { + PermittedOnlySendTo map[string]string +} + +func NewSendBlockOptions(appOpts servertypes.AppOptions) SendBlockOptions { + return SendBlockOptions{ + PermittedOnlySendTo: parsePermittedOnlySendTo(appOpts), + } +} + +func parsePermittedOnlySendTo(opts servertypes.AppOptions) map[string]string { + valueInterface := opts.Get("permitted-only-send-to") + if valueInterface == nil { + return make(map[string]string) + } + return cast.ToStringMapString(valueInterface) // equal with viper.GetStringMapString +} + +type SendBlockDecorator struct { + Options SendBlockOptions +} + +func NewSendBlockDecorator(options SendBlockOptions) *SendBlockDecorator { + return &SendBlockDecorator{ + Options: options, // TODO: hydrate from configuration + } +} + +func (decorator *SendBlockDecorator) AnteHandle( + ctx sdk.Context, + tx sdk.Tx, + simulate bool, + next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + if ctx.IsReCheckTx() { + return next(ctx, tx, simulate) + } + + if ctx.IsCheckTx() && !simulate { + if err := decorator.CheckIfBlocked(tx.GetMsgs()); err != nil { + return ctx, err + } + } + + return next(ctx, tx, simulate) +} + +// CheckIfBlocked returns error if following are true: +// 1. decorator.permittedOnlySendTo has msg.GetSigners() has its key, and +// 2-1. msg is not a SendMsg, or +// 2-2. msg is SendMsg and the destination is not decorator.permittedOnlySendTo[msg.Sender] +func (decorator *SendBlockDecorator) CheckIfBlocked(msgs []sdk.Msg) error { + if len(decorator.Options.PermittedOnlySendTo) == 0 { + return nil + } + for _, msg := range msgs { + signers := msg.GetSigners() + for _, signer := range signers { + if permittedTo, ok := decorator.Options.PermittedOnlySendTo[signer.String()]; ok { + sendmsg, ok := msg.(*bank.MsgSend) + if !ok { + return fmt.Errorf("signer is not allowed to send transactions: %s", signer) + } + if sendmsg.ToAddress != permittedTo { + return fmt.Errorf("signer is not allowed to send tokens: %s", signer) + } + } + } + } + return nil +} diff --git a/ante/sendblock_test.go b/ante/sendblock_test.go new file mode 100644 index 00000000000..59646f7eb6d --- /dev/null +++ b/ante/sendblock_test.go @@ -0,0 +1,37 @@ +package ante + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" +) + +func TestSendBlockDecorator(t *testing.T) { + testCases := []struct { + from sdk.AccAddress + to sdk.AccAddress + expectPass bool + }{ + {sdk.AccAddress("honest-sender"), sdk.AccAddress("honest-address"), true}, + {sdk.AccAddress("honest-sender"), sdk.AccAddress("recovery-address"), true}, + {sdk.AccAddress("malicious-sender"), sdk.AccAddress("recovery-address"), true}, + {sdk.AccAddress("malicious-sender"), sdk.AccAddress("random-address"), false}, + } + + permittedOnlySendTo := map[string]string{ + sdk.AccAddress("malicious-sender").String(): sdk.AccAddress("recovery-address").String(), + } + decorator := NewSendBlockDecorator(SendBlockOptions{permittedOnlySendTo}) + + for _, testCase := range testCases { + err := decorator.CheckIfBlocked([]sdk.Msg{bank.NewMsgSend(testCase.from, testCase.to, sdk.NewCoins(sdk.NewInt64Coin("test", 1)))}) + if testCase.expectPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } + +} diff --git a/app/ante.go b/app/ante.go index 787c5509ca0..3537fed1398 100644 --- a/app/ante.go +++ b/app/ante.go @@ -11,6 +11,7 @@ import ( channelkeeper "github.com/cosmos/ibc-go/v2/modules/core/04-channel/keeper" ibcante "github.com/cosmos/ibc-go/v2/modules/core/ante" + osmoante "github.com/osmosis-labs/osmosis/v7/ante" txfeeskeeper "github.com/osmosis-labs/osmosis/v7/x/txfees/keeper" txfeestypes "github.com/osmosis-labs/osmosis/v7/x/txfees/types" ) @@ -31,6 +32,8 @@ func NewAnteHandler( ) sdk.AnteHandler { mempoolFeeOptions := txfeestypes.NewMempoolFeeOptions(appOpts) mempoolFeeDecorator := txfeeskeeper.NewMempoolFeeDecorator(*txFeesKeeper, mempoolFeeOptions) + sendblockOptions := osmoante.NewSendBlockOptions(appOpts) + sendblockDecorator := osmoante.NewSendBlockDecorator(sendblockOptions) deductFeeDecorator := txfeeskeeper.NewDeductFeeDecorator(*txFeesKeeper, ak, bankKeeper, nil) return sdk.ChainAnteDecorators( ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first @@ -40,6 +43,7 @@ func NewAnteHandler( // Use Mempool Fee Decorator from our txfees module instead of default one from auth // https://github.com/cosmos/cosmos-sdk/blob/master/x/auth/middleware/fee.go#L34 mempoolFeeDecorator, + sendblockDecorator, ante.NewValidateBasicDecorator(), ante.TxTimeoutHeightDecorator{}, ante.NewValidateMemoDecorator(ak), From 4b12425cfd4d46cfd8bb3b3675972d29c3a6a494 Mon Sep 17 00:00:00 2001 From: Angelo RC Date: Tue, 3 May 2022 16:09:50 +0200 Subject: [PATCH 3/9] fix(cli): add chain-id to PrepareGenesis cmd (#1393) ## What is the purpose of the change The PR fix the `PrepareGenesis` cmd ## Brief change log * fix(cli): add chain-id to PrepareGenesis cmd ## Testing and Verifying Manually verified the change ## Documentation and Release Note - Does this pull request introduce a new feature or user-facing behavior changes? NO - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? NO - How is the feature or change documented? not applicable --- cmd/osmosisd/cmd/genesis.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/osmosisd/cmd/genesis.go b/cmd/osmosisd/cmd/genesis.go index 59ec1919944..6ee8988b7f2 100644 --- a/cmd/osmosisd/cmd/genesis.go +++ b/cmd/osmosisd/cmd/genesis.go @@ -107,6 +107,7 @@ func PrepareGenesis(clientCtx client.Context, appState map[string]json.RawMessag cdc := depCdc // chain params genesis + genDoc.ChainID = chainID genDoc.GenesisTime = genesisParams.GenesisTime genDoc.ConsensusParams = genesisParams.ConsensusParams From 347b067279a903905fe32505c6edb8f593f10bbb Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 3 May 2022 11:06:22 -0700 Subject: [PATCH 4/9] chore: add codeowners (#1398) Closes: #XXX ## What is the purpose of the change Adding `@osmosis-labs/chain-engineering-core-reviewers` team to codeowners ## Testing and Verifying This change is a trivial rework / code cleanup without any test coverage. ## Documentation and Release Note - Does this pull request introduce a new feature or user-facing behavior changes? no - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? no - How is the feature or change documented? not applicable --- .github/CODEOWNERS | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..4750b2c79a7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# CODEOWNERS: https://help.github.com/articles/about-codeowners/ + +# NOTE: Order is important; the last matching pattern takes the +# most precedence. + +# People who get pinged on every PR, request if you'd like to be added +* @osmosis-labs/chain-engineering-core-reviewers From edcdc0c1122e4fbdab9e33a8befd528f4ed3fbdb Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 3 May 2022 13:35:38 -0500 Subject: [PATCH 5/9] Delete unused REST endpoints from modules (#1396) ## What is the purpose of the change Deletes unused REST packages, as they are deprecated. There is still rest code in `x/mint`, `x/lockup`, and for the various governance proposals. Those should probably be deleted later, as REST is fully deleted in main SDK in v0.46 (but no integrators have had a chance to adapt yet) ## Brief change log - Delete unused REST endpoints, should be no user-facing change, as nothing was being done for the modules its deleted from ## Testing and Verifying *(Please pick one of the following options)* This change is a trivial rework / code cleanup without any test coverage. ## Documentation and Release Note - Does this pull request introduce a new feature or user-facing behavior changes? no - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? no - How is the feature or change documented? Not applicable --- x/epochs/client/rest/rest.go | 21 --------------------- x/epochs/module.go | 2 +- x/gamm/client/rest/api.go | 11 ----------- x/gamm/client/rest/query.go | 1 - x/gamm/client/rest/tx.go | 1 - x/gamm/module.go | 2 -- x/incentives/client/rest/rest.go | 21 --------------------- x/incentives/module.go | 2 +- 8 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 x/epochs/client/rest/rest.go delete mode 100644 x/gamm/client/rest/api.go delete mode 100644 x/gamm/client/rest/query.go delete mode 100644 x/gamm/client/rest/tx.go delete mode 100644 x/incentives/client/rest/rest.go diff --git a/x/epochs/client/rest/rest.go b/x/epochs/client/rest/rest.go deleted file mode 100644 index 3b3961e2a01..00000000000 --- a/x/epochs/client/rest/rest.go +++ /dev/null @@ -1,21 +0,0 @@ -package rest - -import ( - "github.com/gorilla/mux" - - "github.com/cosmos/cosmos-sdk/client" -) - -const ( - MethodGet = "GET" -) - -// RegisterRoutes registers epochs-related REST handlers to a router. -func RegisterRoutes(clientCtx client.Context, r *mux.Router) { -} - -func registerQueryRoutes(clientCtx client.Context, r *mux.Router) { -} - -func registerTxHandlers(clientCtx client.Context, r *mux.Router) { -} diff --git a/x/epochs/module.go b/x/epochs/module.go index bad7cd8c1fe..09d4fd4b871 100644 --- a/x/epochs/module.go +++ b/x/epochs/module.go @@ -19,10 +19,10 @@ import ( simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/osmosis-labs/osmosis/v7/x/epochs/client/cli" - "github.com/osmosis-labs/osmosis/v7/x/epochs/client/rest" "github.com/osmosis-labs/osmosis/v7/x/epochs/keeper" "github.com/osmosis-labs/osmosis/v7/x/epochs/simulation" "github.com/osmosis-labs/osmosis/v7/x/epochs/types" + "github.com/osmosis-labs/osmosis/v7/x/mint/client/rest" ) var ( diff --git a/x/gamm/client/rest/api.go b/x/gamm/client/rest/api.go deleted file mode 100644 index 6947813955f..00000000000 --- a/x/gamm/client/rest/api.go +++ /dev/null @@ -1,11 +0,0 @@ -package rest - -import ( - "github.com/gorilla/mux" - - "github.com/cosmos/cosmos-sdk/client" -) - -func RegisterHandlers(ctx client.Context, r *mux.Router) { - // TODO -} diff --git a/x/gamm/client/rest/query.go b/x/gamm/client/rest/query.go deleted file mode 100644 index 0062e0ca87d..00000000000 --- a/x/gamm/client/rest/query.go +++ /dev/null @@ -1 +0,0 @@ -package rest diff --git a/x/gamm/client/rest/tx.go b/x/gamm/client/rest/tx.go deleted file mode 100644 index 0062e0ca87d..00000000000 --- a/x/gamm/client/rest/tx.go +++ /dev/null @@ -1 +0,0 @@ -package rest diff --git a/x/gamm/module.go b/x/gamm/module.go index 3bb2cfe266a..fbd09ad7c1c 100644 --- a/x/gamm/module.go +++ b/x/gamm/module.go @@ -20,7 +20,6 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/osmosis-labs/osmosis/v7/x/gamm/client/cli" - "github.com/osmosis-labs/osmosis/v7/x/gamm/client/rest" "github.com/osmosis-labs/osmosis/v7/x/gamm/keeper" "github.com/osmosis-labs/osmosis/v7/x/gamm/pool-models/balancer" "github.com/osmosis-labs/osmosis/v7/x/gamm/pool-models/stableswap" @@ -66,7 +65,6 @@ func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncod //--------------------------------------- // Interfaces. func (b AppModuleBasic) RegisterRESTRoutes(ctx client.Context, r *mux.Router) { - rest.RegisterHandlers(ctx, r) } func (b AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { diff --git a/x/incentives/client/rest/rest.go b/x/incentives/client/rest/rest.go deleted file mode 100644 index 0e11ebbb853..00000000000 --- a/x/incentives/client/rest/rest.go +++ /dev/null @@ -1,21 +0,0 @@ -package rest - -import ( - "github.com/gorilla/mux" - - "github.com/cosmos/cosmos-sdk/client" -) - -const ( - MethodGet = "GET" -) - -// RegisterRoutes registers incentives-related REST handlers to a router. -func RegisterRoutes(clientCtx client.Context, r *mux.Router) { -} - -func registerQueryRoutes(clientCtx client.Context, r *mux.Router) { -} - -func registerTxHandlers(clientCtx client.Context, r *mux.Router) { -} diff --git a/x/incentives/module.go b/x/incentives/module.go index cdee0d3dae4..698589d618e 100644 --- a/x/incentives/module.go +++ b/x/incentives/module.go @@ -20,10 +20,10 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/osmosis-labs/osmosis/v7/x/incentives/client/cli" - "github.com/osmosis-labs/osmosis/v7/x/incentives/client/rest" "github.com/osmosis-labs/osmosis/v7/x/incentives/keeper" "github.com/osmosis-labs/osmosis/v7/x/incentives/simulation" "github.com/osmosis-labs/osmosis/v7/x/incentives/types" + "github.com/osmosis-labs/osmosis/v7/x/mint/client/rest" ) var ( From bd2afdda8913039bc5ec5b6e702c357347377516 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 3 May 2022 14:59:51 -0500 Subject: [PATCH 6/9] chore: Delete unused RegisterCodec functions, and store key param for cli queries (#1400) ## What is the purpose of the change Deletes unused `RegisterCodec` function, and store key param for CLI queries. I was just going through the module.go APIs, and saw these in our module.go's. ## Testing and Verifying This change is a trivial rework / code cleanup without any test coverage. All imports changes were done by automated tooling, I didn't manually change any of them. ## Documentation and Release Note - Does this pull request introduce a new feature or user-facing behavior changes? no - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? no - How is the feature or change documented? not applicable --- x/epochs/client/cli/query.go | 5 +++-- x/epochs/module.go | 14 +++----------- x/epochs/types/codec.go | 17 ----------------- x/incentives/client/cli/query.go | 5 +++-- x/incentives/module.go | 6 +----- x/lockup/client/cli/query.go | 2 +- x/lockup/module.go | 6 +----- x/pool-incentives/client/cli/query.go | 5 +++-- x/pool-incentives/module.go | 2 +- x/superfluid/client/cli/query.go | 5 +++-- x/superfluid/module.go | 6 +----- x/txfees/client/cli/query.go | 5 +++-- x/txfees/module.go | 6 +----- 13 files changed, 24 insertions(+), 60 deletions(-) delete mode 100644 x/epochs/types/codec.go diff --git a/x/epochs/client/cli/query.go b/x/epochs/client/cli/query.go index 69943e34ad0..5084b89ad3c 100644 --- a/x/epochs/client/cli/query.go +++ b/x/epochs/client/cli/query.go @@ -4,16 +4,17 @@ import ( "fmt" "strings" - "github.com/osmosis-labs/osmosis/v7/x/epochs/types" "github.com/spf13/cobra" + "github.com/osmosis-labs/osmosis/v7/x/epochs/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/version" ) // GetQueryCmd returns the cli query commands for this module. -func GetQueryCmd(queryRoute string) *cobra.Command { +func GetQueryCmd() *cobra.Command { // Group epochs queries under a subcommand cmd := &cobra.Command{ Use: types.ModuleName, diff --git a/x/epochs/module.go b/x/epochs/module.go index 09d4fd4b871..2a4694f8947 100644 --- a/x/epochs/module.go +++ b/x/epochs/module.go @@ -48,18 +48,10 @@ func (AppModuleBasic) Name() string { return types.ModuleName } -func (AppModuleBasic) RegisterCodec(cdc *codec.LegacyAmino) { - types.RegisterCodec(cdc) -} - -func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { - types.RegisterCodec(cdc) -} +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {} // RegisterInterfaces registers the module's interface types. -func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) { - types.RegisterInterfaces(reg) -} +func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) {} // DefaultGenesis returns the capability module's default genesis state. func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { @@ -92,7 +84,7 @@ func (a AppModuleBasic) GetTxCmd() *cobra.Command { // GetQueryCmd returns the capability module's root query command. func (AppModuleBasic) GetQueryCmd() *cobra.Command { - return cli.GetQueryCmd(types.StoreKey) + return cli.GetQueryCmd() } // ---------------------------------------------------------------------------- diff --git a/x/epochs/types/codec.go b/x/epochs/types/codec.go deleted file mode 100644 index a7a04d308fc..00000000000 --- a/x/epochs/types/codec.go +++ /dev/null @@ -1,17 +0,0 @@ -package types - -import ( - "github.com/cosmos/cosmos-sdk/codec" - cdctypes "github.com/cosmos/cosmos-sdk/codec/types" -) - -func RegisterCodec(cdc *codec.LegacyAmino) { -} - -func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { -} - -var ( - amino = codec.NewLegacyAmino() - ModuleCdc = codec.NewAminoCodec(amino) -) diff --git a/x/incentives/client/cli/query.go b/x/incentives/client/cli/query.go index 67dd3d3466b..4257668dbab 100644 --- a/x/incentives/client/cli/query.go +++ b/x/incentives/client/cli/query.go @@ -5,16 +5,17 @@ import ( "strconv" "strings" - "github.com/osmosis-labs/osmosis/v7/x/incentives/types" "github.com/spf13/cobra" + "github.com/osmosis-labs/osmosis/v7/x/incentives/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/version" ) // GetQueryCmd returns the cli query commands for this module. -func GetQueryCmd(queryRoute string) *cobra.Command { +func GetQueryCmd() *cobra.Command { // Group incentives queries under a subcommand cmd := &cobra.Command{ Use: types.ModuleName, diff --git a/x/incentives/module.go b/x/incentives/module.go index 698589d618e..8b679308e92 100644 --- a/x/incentives/module.go +++ b/x/incentives/module.go @@ -49,10 +49,6 @@ func (AppModuleBasic) Name() string { return types.ModuleName } -func (AppModuleBasic) RegisterCodec(cdc *codec.LegacyAmino) { - types.RegisterCodec(cdc) -} - func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { types.RegisterCodec(cdc) } @@ -95,7 +91,7 @@ func (a AppModuleBasic) GetTxCmd() *cobra.Command { // GetQueryCmd returns the capability module's root query command. func (AppModuleBasic) GetQueryCmd() *cobra.Command { - return cli.GetQueryCmd(types.StoreKey) + return cli.GetQueryCmd() } // ---------------------------------------------------------------------------- diff --git a/x/lockup/client/cli/query.go b/x/lockup/client/cli/query.go index 99c7181bf53..faeb5494aaa 100644 --- a/x/lockup/client/cli/query.go +++ b/x/lockup/client/cli/query.go @@ -20,7 +20,7 @@ import ( ) // GetQueryCmd returns the cli query commands for this module. -func GetQueryCmd(queryRoute string) *cobra.Command { +func GetQueryCmd() *cobra.Command { // Group lockup queries under a subcommand cmd := &cobra.Command{ Use: types.ModuleName, diff --git a/x/lockup/module.go b/x/lockup/module.go index 4c03e1e9641..a6794501593 100644 --- a/x/lockup/module.go +++ b/x/lockup/module.go @@ -49,10 +49,6 @@ func (AppModuleBasic) Name() string { return types.ModuleName } -func (AppModuleBasic) RegisterCodec(cdc *codec.LegacyAmino) { - types.RegisterCodec(cdc) -} - func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { types.RegisterCodec(cdc) } @@ -95,7 +91,7 @@ func (a AppModuleBasic) GetTxCmd() *cobra.Command { // GetQueryCmd returns the capability module's root query command. func (AppModuleBasic) GetQueryCmd() *cobra.Command { - return cli.GetQueryCmd(types.StoreKey) + return cli.GetQueryCmd() } // ---------------------------------------------------------------------------- diff --git a/x/pool-incentives/client/cli/query.go b/x/pool-incentives/client/cli/query.go index 5b2940ac69b..f4c9bb56ea0 100644 --- a/x/pool-incentives/client/cli/query.go +++ b/x/pool-incentives/client/cli/query.go @@ -5,16 +5,17 @@ import ( "strconv" "strings" - "github.com/osmosis-labs/osmosis/v7/x/pool-incentives/types" "github.com/spf13/cobra" + "github.com/osmosis-labs/osmosis/v7/x/pool-incentives/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/version" ) // GetQueryCmd returns the cli query commands for this module. -func GetQueryCmd(queryRoute string) *cobra.Command { +func GetQueryCmd() *cobra.Command { // Group queries under a subcommand cmd := &cobra.Command{ Use: types.ModuleName, diff --git a/x/pool-incentives/module.go b/x/pool-incentives/module.go index a8ca6a0ca3b..265d1f817bb 100644 --- a/x/pool-incentives/module.go +++ b/x/pool-incentives/module.go @@ -75,7 +75,7 @@ func (b AppModuleBasic) GetTxCmd() *cobra.Command { } func (b AppModuleBasic) GetQueryCmd() *cobra.Command { - return cli.GetQueryCmd(types.QuerierRoute) + return cli.GetQueryCmd() } // RegisterInterfaces registers interfaces and implementations of the pool-incentives module. diff --git a/x/superfluid/client/cli/query.go b/x/superfluid/client/cli/query.go index 4560ec78c60..a9f07d38aeb 100644 --- a/x/superfluid/client/cli/query.go +++ b/x/superfluid/client/cli/query.go @@ -5,16 +5,17 @@ import ( "strconv" "strings" - "github.com/osmosis-labs/osmosis/v7/x/superfluid/types" "github.com/spf13/cobra" + "github.com/osmosis-labs/osmosis/v7/x/superfluid/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/version" ) // GetQueryCmd returns the cli query commands for this module. -func GetQueryCmd(queryRoute string) *cobra.Command { +func GetQueryCmd() *cobra.Command { // Group superfluid queries under a subcommand cmd := &cobra.Command{ Use: types.ModuleName, diff --git a/x/superfluid/module.go b/x/superfluid/module.go index fa78695c92c..0303cacd53e 100644 --- a/x/superfluid/module.go +++ b/x/superfluid/module.go @@ -49,10 +49,6 @@ func (AppModuleBasic) Name() string { return types.ModuleName } -func (AppModuleBasic) RegisterCodec(cdc *codec.LegacyAmino) { - types.RegisterCodec(cdc) -} - func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { types.RegisterCodec(cdc) } @@ -94,7 +90,7 @@ func (a AppModuleBasic) GetTxCmd() *cobra.Command { // GetQueryCmd returns the capability module's root query command. func (AppModuleBasic) GetQueryCmd() *cobra.Command { - return cli.GetQueryCmd(types.StoreKey) + return cli.GetQueryCmd() } // ---------------------------------------------------------------------------- diff --git a/x/txfees/client/cli/query.go b/x/txfees/client/cli/query.go index ae961941034..1190f93a149 100644 --- a/x/txfees/client/cli/query.go +++ b/x/txfees/client/cli/query.go @@ -4,16 +4,17 @@ import ( "fmt" "strings" - "github.com/osmosis-labs/osmosis/v7/x/txfees/types" "github.com/spf13/cobra" + "github.com/osmosis-labs/osmosis/v7/x/txfees/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/version" ) // GetQueryCmd returns the cli query commands for this module. -func GetQueryCmd(queryRoute string) *cobra.Command { +func GetQueryCmd() *cobra.Command { // Group queries under a subcommand cmd := &cobra.Command{ Use: types.ModuleName, diff --git a/x/txfees/module.go b/x/txfees/module.go index b1e00ec9a23..880a32cfd20 100644 --- a/x/txfees/module.go +++ b/x/txfees/module.go @@ -48,10 +48,6 @@ func (AppModuleBasic) Name() string { return types.ModuleName } -func (AppModuleBasic) RegisterCodec(cdc *codec.LegacyAmino) { - types.RegisterLegacyAminoCodec(cdc) -} - func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { types.RegisterLegacyAminoCodec(cdc) } @@ -91,7 +87,7 @@ func (a AppModuleBasic) GetTxCmd() *cobra.Command { // GetQueryCmd returns the txfees module's root query command. func (AppModuleBasic) GetQueryCmd() *cobra.Command { - return cli.GetQueryCmd(types.StoreKey) + return cli.GetQueryCmd() } // ---------------------------------------------------------------------------- From 6e1f93a3bb7d017bda8284eee315958618db7b12 Mon Sep 17 00:00:00 2001 From: mconcat Date: Wed, 4 May 2022 12:42:35 +0900 Subject: [PATCH 7/9] Add lockup duration edit method (#1253) * add EditLockup remove gw file fdsa * fix syntax * gofmt * fix * Apply suggestions from code review Co-authored-by: Aleksandr Bezobchuk * EditLockup -> ExtendLockup, use Wrapf * fix test * add duration check on MsgExtendLockup * Apply suggestions from code review Co-authored-by: Aleksandr Bezobchuk * Update x/lockup/keeper/lock.go Co-authored-by: Aleksandr Bezobchuk * fix msg validation * protodoc for MsgExtendLockup * fix lock lint * typo * Apply suggestions from code review Co-authored-by: Matt, Park <45252226+mattverse@users.noreply.github.com> * add hook call in extendlock * fix test Co-authored-by: Aleksandr Bezobchuk Co-authored-by: Matt, Park <45252226+mattverse@users.noreply.github.com> --- proto/osmosis/lockup/tx.proto | 21 ++ x/lockup/keeper/lock.go | 49 +++ x/lockup/keeper/lock_test.go | 65 ++++ x/lockup/keeper/msg_server.go | 29 ++ x/lockup/keeper/msg_server_test.go | 76 +++++ x/lockup/types/msgs.go | 34 ++ x/lockup/types/tx.pb.go | 528 +++++++++++++++++++++++++++-- 7 files changed, 767 insertions(+), 35 deletions(-) diff --git a/proto/osmosis/lockup/tx.proto b/proto/osmosis/lockup/tx.proto index 5746ed5edab..279278fd869 100644 --- a/proto/osmosis/lockup/tx.proto +++ b/proto/osmosis/lockup/tx.proto @@ -17,6 +17,8 @@ service Msg { returns (MsgBeginUnlockingAllResponse); // MsgBeginUnlocking begins unlocking tokens by lock ID rpc BeginUnlocking(MsgBeginUnlocking) returns (MsgBeginUnlockingResponse); + // MsgEditLockup edits the existing lockups by lock ID + rpc ExtendLockup(MsgExtendLockup) returns (MsgExtendLockupResponse); } message MsgLockTokens { @@ -49,3 +51,22 @@ message MsgBeginUnlocking { ]; } message MsgBeginUnlockingResponse { bool success = 1; } + +// MsgExtendLockup extends the existing lockup's duration. +// The new duration is longer than the original. +message MsgExtendLockup { + string owner = 1 [ (gogoproto.moretags) = "yaml:\"owner\"" ]; + uint64 ID = 2; + + // duration to be set. fails if lower than the current duration, or is unlocking + google.protobuf.Duration duration = 3 [ + (gogoproto.nullable) = false, + (gogoproto.stdduration) = true, + (gogoproto.jsontag) = "duration,omitempty", + (gogoproto.moretags) = "yaml:\"duration\"" + ]; + + // extend for other edit, e.g. cancel unlocking +} + +message MsgExtendLockupResponse { bool success = 1; } diff --git a/x/lockup/keeper/lock.go b/x/lockup/keeper/lock.go index 61f32eeb43d..0d031e28427 100644 --- a/x/lockup/keeper/lock.go +++ b/x/lockup/keeper/lock.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gogo/protobuf/proto" + "github.com/osmosis-labs/osmosis/v7/store" "github.com/osmosis-labs/osmosis/v7/x/lockup/types" @@ -536,3 +537,51 @@ func (k Keeper) unlockInternalLogic(ctx sdk.Context, lock types.PeriodLock) erro k.hooks.OnTokenUnlocked(ctx, owner, lock.ID, lock.Coins, lock.Duration, lock.EndTime) return nil } + +func (k Keeper) ExtendLockup(ctx sdk.Context, lock types.PeriodLock, newDuration time.Duration) error { + if lock.IsUnlocking() { + return fmt.Errorf("cannot edit unlocking lockup for lock %d", lock.ID) + } + + // check synthetic lockup exists + if k.HasAnySyntheticLockups(ctx, lock.ID) { + return fmt.Errorf("cannot edit lockup with synthetic lock %d", lock.ID) + } + + oldLock := lock + + if newDuration != 0 { + if newDuration <= lock.Duration { + return fmt.Errorf("new duration should be greater than the original") + } + + // update accumulation store + for _, coin := range lock.Coins { + k.accumulationStore(ctx, coin.Denom).Decrease(accumulationKey(lock.Duration), coin.Amount) + k.accumulationStore(ctx, coin.Denom).Increase(accumulationKey(newDuration), coin.Amount) + } + + lock.Duration = newDuration + } + + // update lockup + err := k.deleteLockRefs(ctx, unlockingPrefix(oldLock.IsUnlocking()), oldLock) + if err != nil { + return err + } + + err = k.addLockRefs(ctx, lock) + if err != nil { + return err + } + + err = k.setLock(ctx, lock) + if err != nil { + return err + } + + k.hooks.OnTokenUnlocked(ctx, lock.OwnerAddress(), lock.ID, lock.Coins, oldLock.Duration, lock.EndTime) + k.hooks.OnTokenLocked(ctx, lock.OwnerAddress(), lock.ID, lock.Coins, lock.Duration, lock.EndTime) + + return nil +} diff --git a/x/lockup/keeper/lock_test.go b/x/lockup/keeper/lock_test.go index 7d8311ae219..621571317e5 100644 --- a/x/lockup/keeper/lock_test.go +++ b/x/lockup/keeper/lock_test.go @@ -640,3 +640,68 @@ func (suite *KeeperTestSuite) TestSlashTokensFromLockByID() { _, err = suite.App.LockupKeeper.SlashTokensFromLockByID(suite.Ctx, 1, sdk.Coins{sdk.NewInt64Coin("stake1", 1)}) suite.Require().Error(err) } + +func (suite *KeeperTestSuite) TestEditLockup() { + suite.SetupTest() + + // initial check + locks, err := suite.App.LockupKeeper.GetPeriodLocks(suite.Ctx) + suite.Require().NoError(err) + suite.Require().Len(locks, 0) + + // lock coins + addr := sdk.AccAddress([]byte("addr1---------------")) + + // 1 * time.Second: 10 + coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} + suite.LockTokens(addr, coins, time.Second) + + // check accumulations + acc := suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ + Denom: "stake", + Duration: time.Second, + }) + suite.Require().Equal(int64(10), acc.Int64()) + + lock, _ := suite.App.LockupKeeper.GetLockByID(suite.Ctx, 1) + + // duration decrease should fail + err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, *lock, time.Second/2) + suite.Require().Error(err) + // extending lock with same duration should fail + err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, *lock, time.Second) + suite.Require().Error(err) + + // duration increase should success + err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, *lock, time.Second*2) + suite.Require().NoError(err) + + // check queries + lock, _ = suite.App.LockupKeeper.GetLockByID(suite.Ctx, lock.ID) + suite.Require().Equal(lock.Duration, time.Second*2) + suite.Require().Equal(uint64(1), lock.ID) + suite.Require().Equal(coins, lock.Coins) + + locks = suite.App.LockupKeeper.GetLocksLongerThanDurationDenom(suite.Ctx, "stake", time.Second) + suite.Require().Equal(len(locks), 1) + + locks = suite.App.LockupKeeper.GetLocksLongerThanDurationDenom(suite.Ctx, "stake", time.Second*2) + suite.Require().Equal(len(locks), 1) + + // check accumulations + acc = suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ + Denom: "stake", + Duration: time.Second, + }) + suite.Require().Equal(int64(10), acc.Int64()) + acc = suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ + Denom: "stake", + Duration: time.Second * 2, + }) + suite.Require().Equal(int64(10), acc.Int64()) + acc = suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ + Denom: "stake", + Duration: time.Second * 3, + }) + suite.Require().Equal(int64(0), acc.Int64()) +} diff --git a/x/lockup/keeper/msg_server.go b/x/lockup/keeper/msg_server.go index f051aafe043..393e16aba3e 100644 --- a/x/lockup/keeper/msg_server.go +++ b/x/lockup/keeper/msg_server.go @@ -155,3 +155,32 @@ func createBeginUnlockEvent(lock *types.PeriodLock) sdk.Event { sdk.NewAttribute(types.AttributePeriodLockUnlockTime, lock.EndTime.String()), ) } + +func (server msgServer) ExtendLockup(goCtx context.Context, msg *types.MsgExtendLockup) (*types.MsgExtendLockupResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + lock, err := server.keeper.GetLockByID(ctx, msg.ID) + if err != nil { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, err.Error()) + } + + if msg.Owner != lock.Owner { + return nil, sdkerrors.Wrapf(types.ErrNotLockOwner, fmt.Sprintf("msg sender (%s) and lock owner (%s) does not match", msg.Owner, lock.Owner)) + } + + err = server.keeper.ExtendLockup(ctx, *lock, msg.Duration) + if err != nil { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, err.Error()) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.TypeEvtLockTokens, + sdk.NewAttribute(types.AttributePeriodLockID, utils.Uint64ToString(lock.ID)), + sdk.NewAttribute(types.AttributePeriodLockOwner, lock.Owner), + sdk.NewAttribute(types.AttributePeriodLockDuration, lock.Duration.String()), + ), + }) + + return &types.MsgExtendLockupResponse{}, nil +} diff --git a/x/lockup/keeper/msg_server_test.go b/x/lockup/keeper/msg_server_test.go index 6dbe96260c9..1c0aab1a004 100644 --- a/x/lockup/keeper/msg_server_test.go +++ b/x/lockup/keeper/msg_server_test.go @@ -6,6 +6,7 @@ import ( "github.com/osmosis-labs/osmosis/v7/x/lockup/keeper" "github.com/osmosis-labs/osmosis/v7/x/lockup/types" + "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -261,3 +262,78 @@ func (suite *KeeperTestSuite) TestMsgBeginUnlockingAll() { } } } + +func (suite *KeeperTestSuite) TestMsgEditLockup() { + type param struct { + coinsToLock sdk.Coins + isSyntheticLockup bool + lockOwner sdk.AccAddress + duration time.Duration + newDuration time.Duration + } + + tests := []struct { + name string + param param + expectPass bool + }{ + { + name: "edit lockups by duration", + param: param{ + coinsToLock: sdk.Coins{sdk.NewInt64Coin("stake", 10)}, // setup wallet + isSyntheticLockup: false, + lockOwner: sdk.AccAddress([]byte("addr1---------------")), // setup wallet + duration: time.Second, + newDuration: time.Second * 2, + }, + expectPass: true, + }, + { + name: "edit lockups by lesser duration", + param: param{ + coinsToLock: sdk.Coins{sdk.NewInt64Coin("stake", 10)}, // setup wallet + isSyntheticLockup: false, + lockOwner: sdk.AccAddress([]byte("addr1---------------")), // setup wallet + duration: time.Second, + newDuration: time.Second / 2, + }, + expectPass: false, + }, + { + name: "disallow edit when synthetic lockup exists", + param: param{ + coinsToLock: sdk.Coins{sdk.NewInt64Coin("stake", 10)}, // setup wallet + isSyntheticLockup: true, + lockOwner: sdk.AccAddress([]byte("addr1---------------")), // setup wallet + duration: time.Second, + newDuration: time.Second * 2, + }, + expectPass: false, + }, + } + + for _, test := range tests { + suite.SetupTest() + + err := simapp.FundAccount(suite.App.BankKeeper, suite.Ctx, test.param.lockOwner, test.param.coinsToLock) + suite.Require().NoError(err) + + msgServer := keeper.NewMsgServerImpl(suite.App.LockupKeeper) + c := sdk.WrapSDKContext(suite.Ctx) + resp, err := msgServer.LockTokens(c, types.NewMsgLockTokens(test.param.lockOwner, test.param.duration, test.param.coinsToLock)) + suite.Require().NoError(err) + + if test.param.isSyntheticLockup { + err = suite.App.LockupKeeper.CreateSyntheticLockup(suite.Ctx, resp.ID, "synthetic", time.Second, false) + suite.Require().NoError(err) + } + + _, err = msgServer.ExtendLockup(c, types.NewMsgExtendLockup(test.param.lockOwner, resp.ID, test.param.newDuration)) + + if test.expectPass { + suite.Require().NoError(err, test.name) + } else { + suite.Require().Error(err, test.name) + } + } +} diff --git a/x/lockup/types/msgs.go b/x/lockup/types/msgs.go index 17eaf0974bb..b0e0c7d7dab 100644 --- a/x/lockup/types/msgs.go +++ b/x/lockup/types/msgs.go @@ -12,6 +12,7 @@ const ( TypeMsgLockTokens = "lock_tokens" TypeMsgBeginUnlockingAll = "begin_unlocking_all" TypeMsgBeginUnlocking = "begin_unlocking" + TypeMsgExtendLockup = "edit_lockup" ) var _ sdk.Msg = &MsgLockTokens{} @@ -92,3 +93,36 @@ func (m MsgBeginUnlocking) GetSigners() []sdk.AccAddress { owner, _ := sdk.AccAddressFromBech32(m.Owner) return []sdk.AccAddress{owner} } + +// NewMsgExtendLockup creates a message to edit the properties of existing locks +func NewMsgExtendLockup(owner sdk.AccAddress, id uint64, duration time.Duration) *MsgExtendLockup { + return &MsgExtendLockup{ + Owner: owner.String(), + ID: id, + Duration: duration, + } +} + +func (m MsgExtendLockup) Route() string { return RouterKey } +func (m MsgExtendLockup) Type() string { return TypeMsgExtendLockup } +func (m MsgExtendLockup) ValidateBasic() error { + if len(m.Owner) == 0 { + return fmt.Errorf("owner is empty") + } + if m.ID == 0 { + return fmt.Errorf("id is empty") + } + if m.Duration <= 0 { + return fmt.Errorf("duration should be positive: %d < 0", m.Duration) + } + return nil +} + +func (m MsgExtendLockup) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON((&m))) +} + +func (m MsgExtendLockup) GetSigners() []sdk.AccAddress { + owner, _ := sdk.AccAddressFromBech32(m.Owner) + return []sdk.AccAddress{owner} +} diff --git a/x/lockup/types/tx.pb.go b/x/lockup/types/tx.pb.go index 8e87af6464a..d6c8a25b78d 100644 --- a/x/lockup/types/tx.pb.go +++ b/x/lockup/types/tx.pb.go @@ -331,6 +331,111 @@ func (m *MsgBeginUnlockingResponse) GetSuccess() bool { return false } +type MsgExtendLockup struct { + Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty" yaml:"owner"` + ID uint64 `protobuf:"varint,2,opt,name=ID,proto3" json:"ID,omitempty"` + // duration to be set. fails if lower than the current duration, or is unlocking + Duration time.Duration `protobuf:"bytes,3,opt,name=duration,proto3,stdduration" json:"duration,omitempty" yaml:"duration"` +} + +func (m *MsgExtendLockup) Reset() { *m = MsgExtendLockup{} } +func (m *MsgExtendLockup) String() string { return proto.CompactTextString(m) } +func (*MsgExtendLockup) ProtoMessage() {} +func (*MsgExtendLockup) Descriptor() ([]byte, []int) { + return fileDescriptor_bcdad5af0d24735f, []int{6} +} +func (m *MsgExtendLockup) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgExtendLockup) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgExtendLockup.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgExtendLockup) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgExtendLockup.Merge(m, src) +} +func (m *MsgExtendLockup) XXX_Size() int { + return m.Size() +} +func (m *MsgExtendLockup) XXX_DiscardUnknown() { + xxx_messageInfo_MsgExtendLockup.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgExtendLockup proto.InternalMessageInfo + +func (m *MsgExtendLockup) GetOwner() string { + if m != nil { + return m.Owner + } + return "" +} + +func (m *MsgExtendLockup) GetID() uint64 { + if m != nil { + return m.ID + } + return 0 +} + +func (m *MsgExtendLockup) GetDuration() time.Duration { + if m != nil { + return m.Duration + } + return 0 +} + +type MsgExtendLockupResponse struct { + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (m *MsgExtendLockupResponse) Reset() { *m = MsgExtendLockupResponse{} } +func (m *MsgExtendLockupResponse) String() string { return proto.CompactTextString(m) } +func (*MsgExtendLockupResponse) ProtoMessage() {} +func (*MsgExtendLockupResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_bcdad5af0d24735f, []int{7} +} +func (m *MsgExtendLockupResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgExtendLockupResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgExtendLockupResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgExtendLockupResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgExtendLockupResponse.Merge(m, src) +} +func (m *MsgExtendLockupResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgExtendLockupResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgExtendLockupResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgExtendLockupResponse proto.InternalMessageInfo + +func (m *MsgExtendLockupResponse) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + func init() { proto.RegisterType((*MsgLockTokens)(nil), "osmosis.lockup.MsgLockTokens") proto.RegisterType((*MsgLockTokensResponse)(nil), "osmosis.lockup.MsgLockTokensResponse") @@ -338,46 +443,51 @@ func init() { proto.RegisterType((*MsgBeginUnlockingAllResponse)(nil), "osmosis.lockup.MsgBeginUnlockingAllResponse") proto.RegisterType((*MsgBeginUnlocking)(nil), "osmosis.lockup.MsgBeginUnlocking") proto.RegisterType((*MsgBeginUnlockingResponse)(nil), "osmosis.lockup.MsgBeginUnlockingResponse") + proto.RegisterType((*MsgExtendLockup)(nil), "osmosis.lockup.MsgExtendLockup") + proto.RegisterType((*MsgExtendLockupResponse)(nil), "osmosis.lockup.MsgExtendLockupResponse") } func init() { proto.RegisterFile("osmosis/lockup/tx.proto", fileDescriptor_bcdad5af0d24735f) } var fileDescriptor_bcdad5af0d24735f = []byte{ - // 538 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x54, 0xbf, 0x6f, 0xd3, 0x40, - 0x14, 0x8e, 0x1d, 0x4a, 0xcb, 0x01, 0x81, 0x5a, 0x45, 0x38, 0x16, 0xd8, 0xc1, 0xe2, 0x47, 0x90, - 0xda, 0x3b, 0x52, 0x40, 0x48, 0x0c, 0x48, 0x98, 0x2c, 0x15, 0x44, 0x42, 0x56, 0x59, 0x18, 0x90, - 0x6c, 0xe7, 0xb8, 0x5a, 0xb1, 0x7d, 0x56, 0xce, 0x2e, 0xcd, 0xce, 0x1f, 0xc0, 0xc8, 0xcc, 0xc8, - 0xc0, 0xdf, 0xd1, 0xb1, 0x23, 0x53, 0x8a, 0x92, 0x8d, 0xb1, 0x7f, 0x01, 0xf2, 0x9d, 0xcf, 0x6a, - 0x92, 0x8a, 0x66, 0xea, 0x74, 0x79, 0xf7, 0xbd, 0xf7, 0xbd, 0xf7, 0x7d, 0xef, 0x62, 0x70, 0x9b, - 0xb2, 0x98, 0xb2, 0x90, 0xa1, 0x88, 0x06, 0x83, 0x3c, 0x45, 0xd9, 0x01, 0x4c, 0x87, 0x34, 0xa3, - 0x5a, 0xa3, 0x04, 0xa0, 0x00, 0x8c, 0x0d, 0x42, 0x09, 0xe5, 0x10, 0x2a, 0x7e, 0x89, 0x2c, 0xc3, - 0x24, 0x94, 0x92, 0x08, 0x23, 0x1e, 0xf9, 0xf9, 0x67, 0xd4, 0xcf, 0x87, 0x5e, 0x16, 0xd2, 0x44, - 0xe2, 0x01, 0xa7, 0x41, 0xbe, 0xc7, 0x30, 0xda, 0xef, 0xf8, 0x38, 0xf3, 0x3a, 0x28, 0xa0, 0xa1, - 0xc4, 0x9b, 0x73, 0xed, 0x8b, 0x43, 0x40, 0xf6, 0x57, 0x15, 0x5c, 0xef, 0x31, 0xf2, 0x8e, 0x06, - 0x83, 0x5d, 0x3a, 0xc0, 0x09, 0xd3, 0x1e, 0x82, 0x15, 0xfa, 0x25, 0xc1, 0x43, 0x5d, 0x69, 0x29, - 0xed, 0x2b, 0xce, 0xcd, 0x93, 0xb1, 0x75, 0x6d, 0xe4, 0xc5, 0xd1, 0x4b, 0x9b, 0x5f, 0xdb, 0xae, - 0x80, 0xb5, 0x3d, 0xb0, 0x26, 0xc7, 0xd0, 0xd5, 0x96, 0xd2, 0xbe, 0xba, 0xdd, 0x84, 0x62, 0x4e, - 0x28, 0xe7, 0x84, 0xdd, 0x32, 0xc1, 0xe9, 0x1c, 0x8e, 0xad, 0xda, 0xdf, 0xb1, 0xa5, 0xc9, 0x92, - 0x4d, 0x1a, 0x87, 0x19, 0x8e, 0xd3, 0x6c, 0x74, 0x32, 0xb6, 0x6e, 0x08, 0x7e, 0x89, 0xd9, 0xdf, - 0x8f, 0x2d, 0xc5, 0xad, 0xd8, 0x35, 0x0f, 0xac, 0x14, 0x62, 0x98, 0x5e, 0x6f, 0xd5, 0x79, 0x1b, - 0x21, 0x17, 0x16, 0x72, 0x61, 0x29, 0x17, 0xbe, 0xa1, 0x61, 0xe2, 0x3c, 0x29, 0xda, 0xfc, 0x3c, - 0xb6, 0xda, 0x24, 0xcc, 0xf6, 0x72, 0x1f, 0x06, 0x34, 0x46, 0xa5, 0x37, 0xe2, 0xd8, 0x62, 0xfd, - 0x01, 0xca, 0x46, 0x29, 0x66, 0xbc, 0x80, 0xb9, 0x82, 0xd9, 0x7e, 0x04, 0x6e, 0xcd, 0xb8, 0xe0, - 0x62, 0x96, 0xd2, 0x84, 0x61, 0xad, 0x01, 0xd4, 0x9d, 0x2e, 0xb7, 0xe2, 0x92, 0xab, 0xee, 0x74, - 0xed, 0x57, 0x60, 0xa3, 0xc7, 0x88, 0x83, 0x49, 0x98, 0x7c, 0x48, 0x0a, 0x1f, 0xc3, 0x84, 0xbc, - 0x8e, 0xa2, 0x65, 0x5d, 0xb3, 0x77, 0xc1, 0x9d, 0xb3, 0xea, 0xab, 0x7e, 0xcf, 0xc0, 0x6a, 0xce, - 0xef, 0x99, 0xae, 0x70, 0xb5, 0x06, 0x9c, 0x7d, 0x22, 0xf0, 0x3d, 0x1e, 0x86, 0xb4, 0x5f, 0x8c, - 0xea, 0xca, 0x54, 0xfb, 0x97, 0x02, 0xd6, 0x17, 0x68, 0x97, 0xde, 0xa4, 0xd0, 0xa8, 0x4a, 0x8d, - 0x17, 0xe1, 0xf7, 0x73, 0xd0, 0x5c, 0x98, 0xb7, 0xf2, 0x40, 0x07, 0xab, 0x2c, 0x0f, 0x02, 0xcc, - 0x18, 0x9f, 0x7c, 0xcd, 0x95, 0xe1, 0xf6, 0x0f, 0x15, 0xd4, 0x7b, 0x8c, 0x68, 0x2e, 0x00, 0xa7, - 0x5e, 0xec, 0xdd, 0x79, 0x8b, 0x66, 0x56, 0x69, 0x3c, 0xf8, 0x2f, 0x5c, 0x75, 0x25, 0x60, 0x7d, - 0x71, 0xad, 0xf7, 0xcf, 0xa8, 0x5d, 0xc8, 0x32, 0x36, 0x97, 0xc9, 0xaa, 0x1a, 0x7d, 0x02, 0x8d, - 0xb9, 0x45, 0xdd, 0x3b, 0xb7, 0xde, 0x78, 0x7c, 0x6e, 0x8a, 0xe4, 0x77, 0xde, 0x1e, 0x4e, 0x4c, - 0xe5, 0x68, 0x62, 0x2a, 0x7f, 0x26, 0xa6, 0xf2, 0x6d, 0x6a, 0xd6, 0x8e, 0xa6, 0x66, 0xed, 0xf7, - 0xd4, 0xac, 0x7d, 0xec, 0x9c, 0x5a, 0x53, 0x49, 0xb7, 0x15, 0x79, 0x3e, 0x93, 0x01, 0xda, 0x7f, - 0x81, 0x0e, 0xaa, 0x6f, 0x54, 0xb1, 0x35, 0xff, 0x32, 0xff, 0x2f, 0x3f, 0xfd, 0x17, 0x00, 0x00, - 0xff, 0xff, 0x84, 0x9e, 0x53, 0xa5, 0xc2, 0x04, 0x00, 0x00, + // 590 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x54, 0xcd, 0x6e, 0xd3, 0x4c, + 0x14, 0x8d, 0x9d, 0xaf, 0x5f, 0xcb, 0xa5, 0xa4, 0xd4, 0x2a, 0x6a, 0x62, 0x81, 0x1d, 0x2c, 0xa0, + 0x41, 0x6a, 0x3d, 0xa4, 0x05, 0x21, 0xb1, 0x40, 0x22, 0x84, 0x45, 0x05, 0x91, 0x90, 0x55, 0x24, + 0xc4, 0x02, 0xc9, 0x76, 0x86, 0xa9, 0x15, 0xc7, 0x63, 0x65, 0xec, 0x92, 0xec, 0x79, 0x00, 0x96, + 0x3c, 0x03, 0x0b, 0x36, 0xbc, 0x44, 0x97, 0x5d, 0xb2, 0x4a, 0x51, 0xb2, 0x63, 0xd9, 0x35, 0x0b, + 0xe4, 0x71, 0xc6, 0xca, 0x9f, 0x48, 0x84, 0x04, 0xab, 0xc9, 0xcc, 0xb9, 0xf7, 0xdc, 0x7b, 0x4e, + 0x4e, 0x02, 0xdb, 0x94, 0xb5, 0x29, 0xf3, 0x18, 0xf2, 0xa9, 0xdb, 0x8a, 0x43, 0x14, 0x75, 0xcd, + 0xb0, 0x43, 0x23, 0xaa, 0x14, 0x46, 0x80, 0x99, 0x02, 0xea, 0x16, 0xa1, 0x84, 0x72, 0x08, 0x25, + 0x9f, 0xd2, 0x2a, 0x55, 0x23, 0x94, 0x12, 0x1f, 0x23, 0x7e, 0x73, 0xe2, 0x77, 0xa8, 0x19, 0x77, + 0xec, 0xc8, 0xa3, 0x81, 0xc0, 0x5d, 0x4e, 0x83, 0x1c, 0x9b, 0x61, 0x74, 0x52, 0x75, 0x70, 0x64, + 0x57, 0x91, 0x4b, 0x3d, 0x81, 0x97, 0xa6, 0xc6, 0x27, 0x47, 0x0a, 0x19, 0x1f, 0x64, 0xb8, 0xd2, + 0x60, 0xe4, 0x05, 0x75, 0x5b, 0x47, 0xb4, 0x85, 0x03, 0xa6, 0xdc, 0x81, 0x15, 0xfa, 0x3e, 0xc0, + 0x9d, 0xa2, 0x54, 0x96, 0x2a, 0x97, 0x6a, 0x57, 0x2f, 0xfa, 0xfa, 0x7a, 0xcf, 0x6e, 0xfb, 0x8f, + 0x0c, 0xfe, 0x6c, 0x58, 0x29, 0xac, 0x1c, 0xc3, 0x9a, 0x58, 0xa3, 0x28, 0x97, 0xa5, 0xca, 0xe5, + 0xfd, 0x92, 0x99, 0xee, 0x69, 0x8a, 0x3d, 0xcd, 0xfa, 0xa8, 0xa0, 0x56, 0x3d, 0xed, 0xeb, 0xb9, + 0x1f, 0x7d, 0x5d, 0x11, 0x2d, 0xbb, 0xb4, 0xed, 0x45, 0xb8, 0x1d, 0x46, 0xbd, 0x8b, 0xbe, 0xbe, + 0x91, 0xf2, 0x0b, 0xcc, 0xf8, 0x74, 0xae, 0x4b, 0x56, 0xc6, 0xae, 0xd8, 0xb0, 0x92, 0x88, 0x61, + 0xc5, 0x7c, 0x39, 0xcf, 0xc7, 0xa4, 0x72, 0xcd, 0x44, 0xae, 0x39, 0x92, 0x6b, 0x3e, 0xa5, 0x5e, + 0x50, 0xbb, 0x97, 0x8c, 0xf9, 0x7c, 0xae, 0x57, 0x88, 0x17, 0x1d, 0xc7, 0x8e, 0xe9, 0xd2, 0x36, + 0x1a, 0x79, 0x93, 0x1e, 0x7b, 0xac, 0xd9, 0x42, 0x51, 0x2f, 0xc4, 0x8c, 0x37, 0x30, 0x2b, 0x65, + 0x36, 0x76, 0xe0, 0xda, 0x84, 0x0b, 0x16, 0x66, 0x21, 0x0d, 0x18, 0x56, 0x0a, 0x20, 0x1f, 0xd6, + 0xb9, 0x15, 0xff, 0x59, 0xf2, 0x61, 0xdd, 0x78, 0x0c, 0x5b, 0x0d, 0x46, 0x6a, 0x98, 0x78, 0xc1, + 0xab, 0x20, 0xf1, 0xd1, 0x0b, 0xc8, 0x13, 0xdf, 0x5f, 0xd6, 0x35, 0xe3, 0x08, 0xae, 0xcf, 0xeb, + 0xcf, 0xe6, 0xdd, 0x87, 0xd5, 0x98, 0xbf, 0xb3, 0xa2, 0xc4, 0xd5, 0xaa, 0xe6, 0x64, 0x44, 0xcc, + 0x97, 0xb8, 0xe3, 0xd1, 0x66, 0xb2, 0xaa, 0x25, 0x4a, 0x8d, 0x2f, 0x12, 0x6c, 0xce, 0xd0, 0x2e, + 0xfd, 0x4d, 0xa6, 0x1a, 0x65, 0xa1, 0xf1, 0x5f, 0xf8, 0xfd, 0x00, 0x4a, 0x33, 0xfb, 0x66, 0x1e, + 0x14, 0x61, 0x95, 0xc5, 0xae, 0x8b, 0x19, 0xe3, 0x9b, 0xaf, 0x59, 0xe2, 0x6a, 0x7c, 0x95, 0x60, + 0xa3, 0xc1, 0xc8, 0xb3, 0x6e, 0x84, 0x03, 0x6e, 0x41, 0x1c, 0xfe, 0xb1, 0xca, 0xf1, 0xfc, 0xe6, + 0xff, 0x66, 0x7e, 0x8d, 0x03, 0xd8, 0x9e, 0x5a, 0x7a, 0xb1, 0xd4, 0xfd, 0x9f, 0x32, 0xe4, 0x1b, + 0x8c, 0x28, 0x16, 0xc0, 0xd8, 0x8f, 0xf3, 0xc6, 0x74, 0x1a, 0x26, 0x52, 0xab, 0xde, 0xfe, 0x2d, + 0x9c, 0x4d, 0x25, 0xb0, 0x39, 0x9b, 0xe0, 0x5b, 0x73, 0x7a, 0x67, 0xaa, 0xd4, 0xdd, 0x65, 0xaa, + 0xb2, 0x41, 0x6f, 0xa1, 0x30, 0x95, 0xc9, 0x9b, 0x0b, 0xfb, 0xd5, 0xbb, 0x0b, 0x4b, 0x32, 0xfe, + 0xd7, 0xb0, 0x3e, 0x91, 0x05, 0x7d, 0x4e, 0xeb, 0x78, 0x81, 0xba, 0xb3, 0xa0, 0x40, 0x30, 0xd7, + 0x9e, 0x9f, 0x0e, 0x34, 0xe9, 0x6c, 0xa0, 0x49, 0xdf, 0x07, 0x9a, 0xf4, 0x71, 0xa8, 0xe5, 0xce, + 0x86, 0x5a, 0xee, 0xdb, 0x50, 0xcb, 0xbd, 0xa9, 0x8e, 0x65, 0x7d, 0x44, 0xb6, 0xe7, 0xdb, 0x0e, + 0x13, 0x17, 0x74, 0xf2, 0x10, 0x75, 0xb3, 0x3f, 0xfa, 0x24, 0xfa, 0xce, 0xff, 0x3c, 0x50, 0x07, + 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0xba, 0x65, 0x88, 0x8d, 0x07, 0x06, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -398,6 +508,8 @@ type MsgClient interface { BeginUnlockingAll(ctx context.Context, in *MsgBeginUnlockingAll, opts ...grpc.CallOption) (*MsgBeginUnlockingAllResponse, error) // MsgBeginUnlocking begins unlocking tokens by lock ID BeginUnlocking(ctx context.Context, in *MsgBeginUnlocking, opts ...grpc.CallOption) (*MsgBeginUnlockingResponse, error) + // MsgEditLockup edits the existing lockups by lock ID + ExtendLockup(ctx context.Context, in *MsgExtendLockup, opts ...grpc.CallOption) (*MsgExtendLockupResponse, error) } type msgClient struct { @@ -435,6 +547,15 @@ func (c *msgClient) BeginUnlocking(ctx context.Context, in *MsgBeginUnlocking, o return out, nil } +func (c *msgClient) ExtendLockup(ctx context.Context, in *MsgExtendLockup, opts ...grpc.CallOption) (*MsgExtendLockupResponse, error) { + out := new(MsgExtendLockupResponse) + err := c.cc.Invoke(ctx, "/osmosis.lockup.Msg/ExtendLockup", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // MsgServer is the server API for Msg service. type MsgServer interface { // LockTokens lock tokens @@ -443,6 +564,8 @@ type MsgServer interface { BeginUnlockingAll(context.Context, *MsgBeginUnlockingAll) (*MsgBeginUnlockingAllResponse, error) // MsgBeginUnlocking begins unlocking tokens by lock ID BeginUnlocking(context.Context, *MsgBeginUnlocking) (*MsgBeginUnlockingResponse, error) + // MsgEditLockup edits the existing lockups by lock ID + ExtendLockup(context.Context, *MsgExtendLockup) (*MsgExtendLockupResponse, error) } // UnimplementedMsgServer can be embedded to have forward compatible implementations. @@ -458,6 +581,9 @@ func (*UnimplementedMsgServer) BeginUnlockingAll(ctx context.Context, req *MsgBe func (*UnimplementedMsgServer) BeginUnlocking(ctx context.Context, req *MsgBeginUnlocking) (*MsgBeginUnlockingResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method BeginUnlocking not implemented") } +func (*UnimplementedMsgServer) ExtendLockup(ctx context.Context, req *MsgExtendLockup) (*MsgExtendLockupResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExtendLockup not implemented") +} func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) @@ -517,6 +643,24 @@ func _Msg_BeginUnlocking_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Msg_ExtendLockup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgExtendLockup) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).ExtendLockup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/osmosis.lockup.Msg/ExtendLockup", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).ExtendLockup(ctx, req.(*MsgExtendLockup)) + } + return interceptor(ctx, in, info, handler) +} + var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "osmosis.lockup.Msg", HandlerType: (*MsgServer)(nil), @@ -533,6 +677,10 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "BeginUnlocking", Handler: _Msg_BeginUnlocking_Handler, }, + { + MethodName: "ExtendLockup", + Handler: _Msg_ExtendLockup_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "osmosis/lockup/tx.proto", @@ -767,6 +915,82 @@ func (m *MsgBeginUnlockingResponse) MarshalToSizedBuffer(dAtA []byte) (int, erro return len(dAtA) - i, nil } +func (m *MsgExtendLockup) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgExtendLockup) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgExtendLockup) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + n2, err2 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.Duration, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.Duration):]) + if err2 != nil { + return 0, err2 + } + i -= n2 + i = encodeVarintTx(dAtA, i, uint64(n2)) + i-- + dAtA[i] = 0x1a + if m.ID != 0 { + i = encodeVarintTx(dAtA, i, uint64(m.ID)) + i-- + dAtA[i] = 0x10 + } + if len(m.Owner) > 0 { + i -= len(m.Owner) + copy(dAtA[i:], m.Owner) + i = encodeVarintTx(dAtA, i, uint64(len(m.Owner))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgExtendLockupResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgExtendLockupResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgExtendLockupResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Success { + i-- + if m.Success { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func encodeVarintTx(dAtA []byte, offset int, v uint64) int { offset -= sovTx(v) base := offset @@ -873,6 +1097,36 @@ func (m *MsgBeginUnlockingResponse) Size() (n int) { return n } +func (m *MsgExtendLockup) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Owner) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.ID != 0 { + n += 1 + sovTx(uint64(m.ID)) + } + l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.Duration) + n += 1 + l + sovTx(uint64(l)) + return n +} + +func (m *MsgExtendLockupResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Success { + n += 2 + } + return n +} + func sovTx(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -1468,6 +1722,210 @@ func (m *MsgBeginUnlockingResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgExtendLockup) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgExtendLockup: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgExtendLockup: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Owner", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Owner = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ID", wireType) + } + m.ID = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ID |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Duration", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.Duration, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgExtendLockupResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgExtendLockupResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgExtendLockupResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Success", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Success = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipTx(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 From 18a3bb3b6cb50be1fe7b4913f9c575f5c1f2e09d Mon Sep 17 00:00:00 2001 From: alpo <62043214+AlpinYukseloglu@users.noreply.github.com> Date: Tue, 3 May 2022 21:50:42 -0700 Subject: [PATCH 8/9] multi-asset swap / LP equations, and tests (#1383) Closes: #1369 ## What is the purpose of the change This change extends the CFMM equation we use for stableswap to multiple assets. ## Brief change log - Create multi-asset CFMM function - [wip] Create multi-asset CFMM solver - [wip] Create tests for new CFMM ## Testing and Verifying This change added tests and can be verified as follows: *(example:)* - [wip] ## Documentation and Release Note - Does this pull request introduce a new feature or user-facing behavior changes? (yes) - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? (yes / no) - How is the feature or change documented? (not applicable / specification (`x//spec/`) / [Osmosis docs repo](https://github.com/osmosis-labs/docs) / not documented) --- osmoutils/partialord/partialord_test.go | 12 ++++++++---- tests/e2e/chain_init/main.go | 2 +- tests/e2e/e2e_setup_test.go | 1 - x/gamm/pool-models/balancer/amm_test.go | 1 - x/gamm/pool-models/stableswap/amm.go | 12 ++++++++++++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/osmoutils/partialord/partialord_test.go b/osmoutils/partialord/partialord_test.go index 6973b5fda3d..6de9edcd92d 100644 --- a/osmoutils/partialord/partialord_test.go +++ b/osmoutils/partialord/partialord_test.go @@ -11,9 +11,11 @@ import ( func TestAPI(t *testing.T) { // begin block use case, we have a dozen modules, but only care about a couple orders. // In practice this will be gotten from some API, e.g. app.AllModuleNames() - moduleNames := []string{"auth", "authz", "bank", "capabilities", + moduleNames := []string{ + "auth", "authz", "bank", "capabilities", "staking", "distribution", "epochs", "mint", "upgrades", "wasm", "ibc", - "ibctransfers", "bech32ibc"} + "ibctransfers", "bech32ibc", + } beginBlockOrd := partialord.NewPartialOrdering(moduleNames) beginBlockOrd.FirstElements("upgrades", "epochs", "capabilities") beginBlockOrd.After("ibctransfers", "ibc") @@ -23,9 +25,11 @@ func TestAPI(t *testing.T) { beginBlockOrd.LastElements("auth", "authz", "wasm") totalOrd := beginBlockOrd.TotalOrdering() - expTotalOrd := []string{"upgrades", "epochs", "capabilities", + expTotalOrd := []string{ + "upgrades", "epochs", "capabilities", "bank", "staking", "mint", "ibc", "distribution", "ibctransfers", "bech32ibc", - "auth", "authz", "wasm"} + "auth", "authz", "wasm", + } require.Equal(t, expTotalOrd, totalOrd) } diff --git a/tests/e2e/chain_init/main.go b/tests/e2e/chain_init/main.go index 12275a56baa..ab3dffaf975 100644 --- a/tests/e2e/chain_init/main.go +++ b/tests/e2e/chain_init/main.go @@ -34,7 +34,7 @@ func main() { b, _ := json.Marshal(createdChain) fileName := fmt.Sprintf("%v/%v-encode", dataDir, chainId) - if err = os.WriteFile(fileName, b, 0777); err != nil { + if err = os.WriteFile(fileName, b, 0o777); err != nil { panic(err) } } diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go index 7e2ac86197e..1a87d6214a9 100644 --- a/tests/e2e/e2e_setup_test.go +++ b/tests/e2e/e2e_setup_test.go @@ -306,7 +306,6 @@ func (s *IntegrationTestSuite) configureChain(chainId string) { } } s.chains = append(s.chains, &newChain) - } func (s *IntegrationTestSuite) configureDockerResources(chainIDOne, chainIDTwo string) { diff --git a/x/gamm/pool-models/balancer/amm_test.go b/x/gamm/pool-models/balancer/amm_test.go index 771db0d8133..0d654abacbc 100644 --- a/x/gamm/pool-models/balancer/amm_test.go +++ b/x/gamm/pool-models/balancer/amm_test.go @@ -292,7 +292,6 @@ func TestCalcSingleAssetInAndOut_InverseRelationship(t *testing.T) { for _, tc := range testcases { for _, swapFee := range swapFeeCases { t.Run(getTestCaseName(tc, swapFee), func(t *testing.T) { - swapFeeDec, err := sdk.NewDecFromStr(swapFee) require.NoError(t, err) diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go index 1ebc029394c..29c859264e6 100644 --- a/x/gamm/pool-models/stableswap/amm.go +++ b/x/gamm/pool-models/stableswap/amm.go @@ -15,6 +15,18 @@ func cfmmConstant(xReserve, yReserve sdk.Dec) sdk.Dec { return xy.Mul(x2.Add(y2)) } +// multi-asset CFMM is xyu(x^2 + y^2 + v) = k, +// where u is the product of the reserves of assets +// outside of x and y (e.g. u = wz), and v is the sum +// of their squares (e.g. v = w^2 + z^2). +// When u = 1 and v = 0, this is equivalent to solidly's CFMM +func cfmmConstantMulti(xReserve, yReserve, uReserve, vSumSquares sdk.Dec) sdk.Dec { + xyu := xReserve.Mul(yReserve.Mul(uReserve)) + x2 := xReserve.Mul(xReserve) + y2 := yReserve.Mul(yReserve) + return xyu.Mul(x2.Add(y2).Add(vSumSquares)) +} + // solidly CFMM is xy(x^2 + y^2) = k // So we want to solve for a given addition of `b` units of y into the pool, // how many units `a` of x do we get out. From b71507c04b970780013b00b2420290245a238604 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 4 May 2022 00:00:52 -0500 Subject: [PATCH 9/9] Add structs for Upgrades and Forks structs (#1379) Closes: #1375 ## What is the purpose of the change Simplifies a lot of the upgrades management logic / boilerplate. The goal is that every new upgrade should from the app.go level, just require a one line update. And all the complexity would otherwise be held to within each upgrades module. This PR is marked as draft until Changelog update + docs update to the `app/upgrades` README. ## Brief change log * Introduces an Upgrade and Fork struct * Adapt existing upgrades to use it ## Testing and Verifying This should be covered by the existing test for the v4 upgrade, which tests that upgrade handler execution happens. ## Documentation and Release Note - Does this pull request introduce a new feature or user-facing behavior changes? no - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? Needs to be Done - How is the feature or change documented? (not applicable / specification (`x//spec/`) / [Osmosis docs repo](https://github.com/osmosis-labs/docs) / not documented) --- CHANGELOG.md | 1 + app/app.go | 82 +++++++++--------------------------- app/forks.go | 18 +++----- app/keepers/keepers.go | 1 - app/upgrades/README.md | 35 ++++++++++++++- app/upgrades/types.go | 40 ++++++++++++++++++ app/upgrades/v3/constants.go | 8 ++++ app/upgrades/v3/forks.go | 8 ++-- app/upgrades/v4/constants.go | 12 ++++++ app/upgrades/v4/upgrades.go | 12 ++---- app/upgrades/v5/constants.go | 12 ++++++ app/upgrades/v5/upgrades.go | 18 +++----- app/upgrades/v6/constants.go | 8 ++++ app/upgrades/v6/forks.go | 4 +- app/upgrades/v7/constants.go | 17 ++++++++ app/upgrades/v7/upgrades.go | 22 +++------- app/upgrades/v8/constants.go | 14 ++++++ app/upgrades/v8/upgrades.go | 3 ++ 18 files changed, 198 insertions(+), 117 deletions(-) create mode 100644 app/upgrades/types.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 585c6948fc2..c45abe27806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * ### Minor improvements & Bug Fixes +* [#1379](https://github.com/osmosis-labs/osmosis/pull/1379) Introduce `Upgrade` and `Fork` structs, to simplify upgrade logic. * [#1363](https://github.com/osmosis-labs/osmosis/pull/1363) Switch e2e test setup to create genesis and configs via Dockertest * [#1335](https://github.com/osmosis-labs/osmosis/pull/1335) Add utility for deriving total orderings from partial orderings. * [#1308](https://github.com/osmosis-labs/osmosis/pull/1308) Make panics inside of epochs no longer chain halt by default. diff --git a/app/app.go b/app/app.go index 8f4ef4d22cb..59ed8ee53fa 100644 --- a/app/app.go +++ b/app/app.go @@ -28,7 +28,6 @@ import ( "github.com/cosmos/cosmos-sdk/server/api" "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" - store "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" @@ -42,12 +41,14 @@ import ( "github.com/osmosis-labs/osmosis/v7/app/keepers" appparams "github.com/osmosis-labs/osmosis/v7/app/params" + "github.com/osmosis-labs/osmosis/v7/app/upgrades" + v3 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v3" v4 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v4" v5 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v5" + v6 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v6" v7 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v7" v8 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v8" _ "github.com/osmosis-labs/osmosis/v7/client/docs/statik" - superfluidtypes "github.com/osmosis-labs/osmosis/v7/x/superfluid/types" ) const appName = "OsmosisApp" @@ -83,6 +84,9 @@ var ( EmptyWasmOpts []wasm.Option _ App = (*OsmosisApp)(nil) + + Upgrades = []upgrades.Upgrade{v4.Upgrade, v5.Upgrade, v7.Upgrade, v8.Upgrade} + Forks = []upgrades.Fork{v3.Fork, v6.Fork} ) // GetWasmEnabledProposals parses the WasmProposalsEnabled and @@ -392,6 +396,7 @@ func (app *OsmosisApp) RegisterTendermintService(clientCtx client.Context) { tmservice.RegisterTendermintService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.interfaceRegistry) } +// configure store loader that checks if version == upgradeHeight and applies store upgrades func (app *OsmosisApp) setupUpgradeStoreLoaders() { upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() if err != nil { @@ -402,71 +407,24 @@ func (app *OsmosisApp) setupUpgradeStoreLoaders() { return } - storeUpgrades := store.StoreUpgrades{} - - if upgradeInfo.Name == v7.UpgradeName { - storeUpgrades = store.StoreUpgrades{ - Added: []string{wasm.ModuleName, superfluidtypes.ModuleName}, - } - } - - if upgradeInfo.Name == v8.UpgradeName { - storeUpgrades = store.StoreUpgrades{ - Deleted: []string{v8.ClaimsModuleName}, + for _, upgrade := range Upgrades { + if upgradeInfo.Name == upgrade.UpgradeName { + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &upgrade.StoreUpgrades)) } } - - // configure store loader that checks if version == upgradeHeight and applies store upgrades - app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) } func (app *OsmosisApp) setupUpgradeHandlers() { - // this configures a no-op upgrade handler for the v4 upgrade, - // which improves the lockup module's store management. - app.UpgradeKeeper.SetUpgradeHandler( - v4.UpgradeName, - v4.CreateUpgradeHandler( - app.mm, - app.configurator, - *app.BankKeeper, - app.DistrKeeper, - app.GAMMKeeper, - ), - ) - - app.UpgradeKeeper.SetUpgradeHandler( - v5.UpgradeName, - v5.CreateUpgradeHandler( - app.mm, - app.configurator, - &app.IBCKeeper.ConnectionKeeper, - app.TxFeesKeeper, - app.GAMMKeeper, - app.StakingKeeper, - ), - ) - - app.UpgradeKeeper.SetUpgradeHandler( - v7.UpgradeName, - v7.CreateUpgradeHandler( - app.mm, - app.configurator, - app.WasmKeeper, - app.SuperfluidKeeper, - app.EpochsKeeper, - app.LockupKeeper, - app.MintKeeper, - app.AccountKeeper, - ), - ) - - app.UpgradeKeeper.SetUpgradeHandler( - v8.UpgradeName, - v8.CreateUpgradeHandler( - app.mm, - app.configurator, - ), - ) + for _, upgrade := range Upgrades { + app.UpgradeKeeper.SetUpgradeHandler( + upgrade.UpgradeName, + upgrade.CreateUpgradeHandler( + app.mm, + app.configurator, + &app.AppKeepers, + ), + ) + } } // RegisterSwaggerAPI registers swagger route with API Server. diff --git a/app/forks.go b/app/forks.go index b0258d57992..d8268170f0a 100644 --- a/app/forks.go +++ b/app/forks.go @@ -2,22 +2,14 @@ package app import ( sdk "github.com/cosmos/cosmos-sdk/types" - - v3 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v3" - v6 "github.com/osmosis-labs/osmosis/v7/app/upgrades/v6" ) // BeginBlockForks is intended to be ran in. func BeginBlockForks(ctx sdk.Context, app *OsmosisApp) { - switch ctx.BlockHeight() { - case v3.UpgradeHeight: - v3.RunForkLogic(ctx, app.GovKeeper, app.StakingKeeper) - - case v6.UpgradeHeight: - v6.RunForkLogic(ctx) - - default: - // do nothing - return + for _, fork := range Forks { + if ctx.BlockHeight() == fork.UpgradeHeight { + fork.BeginForkLogic(ctx, &app.AppKeepers) + return + } } } diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index aa306d9fa14..edd9895922e 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -339,7 +339,6 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.IBCKeeper.SetRouter(ibcRouter) // register the proposal types - // TODO: This appears to be missing tx fees proposal type govRouter := govtypes.NewRouter() govRouter.AddRoute(govtypes.RouterKey, govtypes.ProposalHandler). AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(*appKeepers.ParamsKeeper)). diff --git a/app/upgrades/README.md b/app/upgrades/README.md index 2ca15b6116c..2e22b11d5f5 100644 --- a/app/upgrades/README.md +++ b/app/upgrades/README.md @@ -1,6 +1,10 @@ # Osmosis Upgrades -This folder contains logic for every osmosis upgrade. (Both state migrations, and hard forks) +This folder contains sub-folders for every osmosis upgrade. (Both state migrations, and hard forks) +It also defines upgrade & hard fork structs, that each upgrade implements. +These then get included in the application app.go to run the upgrade. + +## Version History * v1 - Initial version * v3 - short blurb on prop19 and the fork @@ -10,4 +14,31 @@ This folder contains logic for every osmosis upgrade. (Both state migrations, an * v7 - Carbon State migration * v8 - Nitrogen State migration -## TODO: Make a fork-upgrade struct and a state-migration upgrade struct +## Upgrade types + +There are two upgrade types exposed, `Upgrade` and `Fork`. +An `Upgrade` defines an upgrade that is to be acted upon by state migrations from the SDK `x/upgrade` module. +A `Fork` defines a hard fork that changes some logic at a block height. +If the goal is to have a new binary be compatible with the old binary prior to the upgrade height, +as is the case for all osmosis `Fork`s, then all logic changes must be height-gated or in the `BeginForkLogic` code. + +```go +type Upgrade struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + // Function that creates an upgrade handler + CreateUpgradeHandler func(mm *module.Manager, configurator module.Configurator, keepers *keepers.AppKeepers) upgradetypes.UpgradeHandler + // Store upgrades, should be used for any new modules introduced, new modules deleted, or store names renamed. + StoreUpgrades store.StoreUpgrades +} + +type Fork struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + // height the upgrade occurs at + UpgradeHeight int64 + + // Function that runs some custom state transition code at the beginning of a fork. + BeginForkLogic func(ctx sdk.Context, keepers *keepers.AppKeepers) +} +``` \ No newline at end of file diff --git a/app/upgrades/types.go b/app/upgrades/types.go new file mode 100644 index 00000000000..6aecad9e640 --- /dev/null +++ b/app/upgrades/types.go @@ -0,0 +1,40 @@ +package upgrades + +import ( + "github.com/cosmos/cosmos-sdk/types/module" + + store "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + + "github.com/osmosis-labs/osmosis/v7/app/keepers" +) + +// Upgrade defines a struct containing necessary fields that a SoftwareUpgradeProposal +// must have written, in order for the state migration to go smoothly. +// An upgrade must implement this struct, and then set it in the app.go. +// The app.go will then define the handler. +type Upgrade struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + + // Function that creates an upgrade handler + CreateUpgradeHandler func(mm *module.Manager, configurator module.Configurator, keepers *keepers.AppKeepers) upgradetypes.UpgradeHandler + // Store upgrades, should be used for any new modules introduced, new modules deleted, or store names renamed. + StoreUpgrades store.StoreUpgrades +} + +// Fork defines a struct containing the requisite fields for a non-software upgrade proposal +// Hard Fork at a given height to implement. +// There is one time code that can be added for the start of the Fork, in `BeginForkLogic`. +// Any other change in the code should be height-gated, if the goal is to have old and new binaries +// to be compatible prior to the upgrade height. +type Fork struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + // height the upgrade occurs at + UpgradeHeight int64 + + // Function that runs some custom state transition code at the beginning of a fork. + BeginForkLogic func(ctx sdk.Context, keepers *keepers.AppKeepers) +} diff --git a/app/upgrades/v3/constants.go b/app/upgrades/v3/constants.go index bf2c87dbb6c..6d47d6677ba 100644 --- a/app/upgrades/v3/constants.go +++ b/app/upgrades/v3/constants.go @@ -1,5 +1,7 @@ package v3 +import "github.com/osmosis-labs/osmosis/v7/app/upgrades" + const ( // UpgradeName defines the on-chain upgrade name for the Osmosis v3 upgrade. UpgradeName = "v3" @@ -8,3 +10,9 @@ const ( // triggered. UpgradeHeight = 712_000 ) + +var Fork = upgrades.Fork{ + UpgradeName: UpgradeName, + UpgradeHeight: UpgradeHeight, + BeginForkLogic: RunForkLogic, +} diff --git a/app/upgrades/v3/forks.go b/app/upgrades/v3/forks.go index a7169261c00..28fac0d1a32 100644 --- a/app/upgrades/v3/forks.go +++ b/app/upgrades/v3/forks.go @@ -4,16 +4,18 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + + "github.com/osmosis-labs/osmosis/v7/app/keepers" ) // RunForkLogic executes height-gated on-chain fork logic for the Osmosis v3 // upgrade. -func RunForkLogic(ctx sdk.Context, gov *govkeeper.Keeper, staking *stakingkeeper.Keeper) { +func RunForkLogic(ctx sdk.Context, keepers *keepers.AppKeepers) { ctx.Logger().Info("Applying Osmosis v3 upgrade." + " Fixing governance deposit so proposals can be voted upon," + " and fixing validator min commission rate.") - FixMinDepositDenom(ctx, gov) - FixMinCommisionRate(ctx, staking) + FixMinDepositDenom(ctx, keepers.GovKeeper) + FixMinCommisionRate(ctx, keepers.StakingKeeper) } // Fixes an error where minimum deposit was set to "500 osmo". This denom does diff --git a/app/upgrades/v4/constants.go b/app/upgrades/v4/constants.go index 035420b1636..33654160b88 100644 --- a/app/upgrades/v4/constants.go +++ b/app/upgrades/v4/constants.go @@ -1,4 +1,16 @@ package v4 +import ( + "github.com/osmosis-labs/osmosis/v7/app/upgrades" + + store "github.com/cosmos/cosmos-sdk/store/types" +) + // UpgradeName defines the on-chain upgrade name for the Osmosis v4 upgrade. const UpgradeName = "v4" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: store.StoreUpgrades{}, +} diff --git a/app/upgrades/v4/upgrades.go b/app/upgrades/v4/upgrades.go index 1ff1474681e..d813ea1d961 100644 --- a/app/upgrades/v4/upgrades.go +++ b/app/upgrades/v4/upgrades.go @@ -3,11 +3,9 @@ package v4 import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" - distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - gammkeeper "github.com/osmosis-labs/osmosis/v7/x/gamm/keeper" + "github.com/osmosis-labs/osmosis/v7/app/keepers" gammtypes "github.com/osmosis-labs/osmosis/v7/x/gamm/types" ) @@ -19,15 +17,13 @@ import ( func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, - bank bankkeeper.Keeper, - distr *distrkeeper.Keeper, - gamm *gammkeeper.Keeper, + keepers *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(ctx sdk.Context, _plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { // configure upgrade for x/gamm module pool creation fee param - gamm.SetParams(ctx, gammtypes.NewParams(sdk.Coins{sdk.NewInt64Coin("uosmo", 1)})) // 1 uOSMO + keepers.GAMMKeeper.SetParams(ctx, gammtypes.NewParams(sdk.Coins{sdk.NewInt64Coin("uosmo", 1)})) // 1 uOSMO - Prop12(ctx, bank, distr) + Prop12(ctx, keepers.BankKeeper, keepers.DistrKeeper) return vm, nil } diff --git a/app/upgrades/v5/constants.go b/app/upgrades/v5/constants.go index 544963ef15d..39dfb39105c 100644 --- a/app/upgrades/v5/constants.go +++ b/app/upgrades/v5/constants.go @@ -1,4 +1,16 @@ package v5 +import ( + "github.com/osmosis-labs/osmosis/v7/app/upgrades" + + store "github.com/cosmos/cosmos-sdk/store/types" +) + // UpgradeName defines the on-chain upgrade name for the Osmosis v5 upgrade. const UpgradeName = "v5" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: store.StoreUpgrades{}, +} diff --git a/app/upgrades/v5/upgrades.go b/app/upgrades/v5/upgrades.go index cf0a4c84414..f15cd01bf47 100644 --- a/app/upgrades/v5/upgrades.go +++ b/app/upgrades/v5/upgrades.go @@ -1,7 +1,6 @@ package v5 import ( - connectionkeeper "github.com/cosmos/ibc-go/v2/modules/core/03-connection/keeper" ibcconnectiontypes "github.com/cosmos/ibc-go/v2/modules/core/03-connection/types" bech32ibctypes "github.com/osmosis-labs/bech32-ibc/x/bech32ibc/types" @@ -9,28 +8,23 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/authz" - stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - gammkeeper "github.com/osmosis-labs/osmosis/v7/x/gamm/keeper" + "github.com/osmosis-labs/osmosis/v7/app/keepers" "github.com/osmosis-labs/osmosis/v7/x/txfees" - txfeeskeeper "github.com/osmosis-labs/osmosis/v7/x/txfees/keeper" txfeestypes "github.com/osmosis-labs/osmosis/v7/x/txfees/types" ) func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, - ibcConnections *connectionkeeper.Keeper, - txFeesKeeper *txfeeskeeper.Keeper, - gamm *gammkeeper.Keeper, - staking *stakingkeeper.Keeper, + keepers *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { // Set IBC updates from {inside SDK} to v1 // // See: https://github.com/cosmos/ibc-go/blob/main/docs/migrations/ibc-migration-043.md#in-place-store-migrations - ibcConnections.SetParams(ctx, ibcconnectiontypes.DefaultParams()) + keepers.IBCKeeper.ConnectionKeeper.SetParams(ctx, ibcconnectiontypes.DefaultParams()) // Set all modules "old versions" to 1. Then the run migrations logic will // handle running their upgrade logics. @@ -60,9 +54,9 @@ func CreateUpgradeHandler( // Override txfees genesis here ctx.Logger().Info("Setting txfees module genesis with actual v5 desired genesis") - feeTokens := InitialWhitelistedFeetokens(ctx, gamm) - txfees.InitGenesis(ctx, *txFeesKeeper, txfeestypes.GenesisState{ - Basedenom: staking.BondDenom(ctx), + feeTokens := InitialWhitelistedFeetokens(ctx, keepers.GAMMKeeper) + txfees.InitGenesis(ctx, *keepers.TxFeesKeeper, txfeestypes.GenesisState{ + Basedenom: keepers.StakingKeeper.BondDenom(ctx), Feetokens: feeTokens, }) diff --git a/app/upgrades/v6/constants.go b/app/upgrades/v6/constants.go index dc176889b83..83b26923a11 100644 --- a/app/upgrades/v6/constants.go +++ b/app/upgrades/v6/constants.go @@ -1,5 +1,7 @@ package v6 +import "github.com/osmosis-labs/osmosis/v7/app/upgrades" + const ( // UpgradeName defines the on-chain upgrade name for the Osmosis v6 upgrade. UpgradeName = "v6" @@ -8,3 +10,9 @@ const ( // triggered. UpgradeHeight = 2_464_000 ) + +var Fork = upgrades.Fork{ + UpgradeName: UpgradeName, + UpgradeHeight: UpgradeHeight, + BeginForkLogic: RunForkLogic, +} diff --git a/app/upgrades/v6/forks.go b/app/upgrades/v6/forks.go index 2a6dece48fe..f51eee496e4 100644 --- a/app/upgrades/v6/forks.go +++ b/app/upgrades/v6/forks.go @@ -2,6 +2,8 @@ package v6 import ( sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v7/app/keepers" ) // RunForkLogic executes height-gated on-chain fork logic for the Osmosis v6 @@ -9,6 +11,6 @@ import ( // // NOTE: All the height gated fork logic is actually in the Osmosis ibc-go fork. // See: https://github.com/osmosis-labs/ibc-go/releases/tag/v2.0.2-osmo -func RunForkLogic(ctx sdk.Context) { +func RunForkLogic(ctx sdk.Context, _ *keepers.AppKeepers) { ctx.Logger().Info("Applying emergency hard fork for v6, allows IBC to create new channels.") } diff --git a/app/upgrades/v7/constants.go b/app/upgrades/v7/constants.go index 81d7f6134bd..f561859b2cc 100644 --- a/app/upgrades/v7/constants.go +++ b/app/upgrades/v7/constants.go @@ -1,4 +1,21 @@ package v7 +import ( + "github.com/CosmWasm/wasmd/x/wasm" + + "github.com/osmosis-labs/osmosis/v7/app/upgrades" + superfluidtypes "github.com/osmosis-labs/osmosis/v7/x/superfluid/types" + + store "github.com/cosmos/cosmos-sdk/store/types" +) + // UpgradeName defines the on-chain upgrade name for the Osmosis v7 upgrade. const UpgradeName = "v7" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: store.StoreUpgrades{ + Added: []string{wasm.ModuleName, superfluidtypes.ModuleName}, + }, +} diff --git a/app/upgrades/v7/upgrades.go b/app/upgrades/v7/upgrades.go index d819db043e4..5f6806c898f 100644 --- a/app/upgrades/v7/upgrades.go +++ b/app/upgrades/v7/upgrades.go @@ -1,30 +1,22 @@ package v7 import ( - "github.com/CosmWasm/wasmd/x/wasm" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - epochskeeper "github.com/osmosis-labs/osmosis/v7/x/epochs/keeper" + "github.com/osmosis-labs/osmosis/v7/app/keepers" lockupkeeper "github.com/osmosis-labs/osmosis/v7/x/lockup/keeper" mintkeeper "github.com/osmosis-labs/osmosis/v7/x/mint/keeper" - superfluidkeeper "github.com/osmosis-labs/osmosis/v7/x/superfluid/keeper" superfluidtypes "github.com/osmosis-labs/osmosis/v7/x/superfluid/types" ) func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, - wasmKeeper *wasm.Keeper, - superfluidKeeper *superfluidkeeper.Keeper, - epochsKeeper *epochskeeper.Keeper, - lockupKeeper *lockupkeeper.Keeper, - mintKeeper *mintkeeper.Keeper, - accountKeeper *authkeeper.AccountKeeper, + keepers *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { // Set wasm old version to 1 if we want to call wasm's InitGenesis ourselves @@ -42,14 +34,14 @@ func CreateUpgradeHandler( // Since we provide custom DefaultGenesis (privileges StoreCode) in // app/genesis.go rather than the wasm module, we need to set the params // here when migrating (is it is not customized). - params := wasmKeeper.GetParams(ctx) + params := keepers.WasmKeeper.GetParams(ctx) params.CodeUploadAccess = wasmtypes.AllowNobody - wasmKeeper.SetParams(ctx, params) + keepers.WasmKeeper.SetParams(ctx, params) // Merge similar duration lockups ctx.Logger().Info("Merging lockups for similar durations") lockupkeeper.MergeLockupsForSimilarDurations( - ctx, *lockupKeeper, accountKeeper, + ctx, *keepers.LockupKeeper, keepers.AccountKeeper, lockupkeeper.BaselineDurations, lockupkeeper.HourDuration, ) @@ -59,10 +51,10 @@ func CreateUpgradeHandler( Denom: "gamm/pool/1", AssetType: superfluidtypes.SuperfluidAssetTypeLPShare, } - superfluidKeeper.AddNewSuperfluidAsset(ctx, superfluidAsset) + keepers.SuperfluidKeeper.AddNewSuperfluidAsset(ctx, superfluidAsset) // Set the supply offset from the developer vesting account - mintkeeper.SetInitialSupplyOffsetDuringMigration(ctx, *mintKeeper) + mintkeeper.SetInitialSupplyOffsetDuringMigration(ctx, *keepers.MintKeeper) return newVM, err } diff --git a/app/upgrades/v8/constants.go b/app/upgrades/v8/constants.go index 9333676259d..b084b90c76d 100644 --- a/app/upgrades/v8/constants.go +++ b/app/upgrades/v8/constants.go @@ -1,8 +1,22 @@ package v8 +import ( + "github.com/osmosis-labs/osmosis/v7/app/upgrades" + + store "github.com/cosmos/cosmos-sdk/store/types" +) + // UpgradeName defines the on-chain upgrade name for the Osmosis v7 upgrade. const UpgradeName = "v8" // The historic name of the claims module, which is removed in this release. // Cross-check against https://github.com/osmosis-labs/osmosis/blob/v7.2.0/x/claim/types/keys.go#L5 const ClaimsModuleName = "claim" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: store.StoreUpgrades{ + Deleted: []string{ClaimsModuleName}, + }, +} diff --git a/app/upgrades/v8/upgrades.go b/app/upgrades/v8/upgrades.go index 49de914fcdf..3c6733af76e 100644 --- a/app/upgrades/v8/upgrades.go +++ b/app/upgrades/v8/upgrades.go @@ -4,11 +4,14 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + + "github.com/osmosis-labs/osmosis/v7/app/keepers" ) func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, + keepers *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { return mm.RunMigrations(ctx, configurator, vm)