commit
21c6099775
@ -0,0 +1,21 @@ |
||||
package types |
||||
|
||||
import ( |
||||
"math/big" |
||||
) |
||||
|
||||
type ( |
||||
// Commission defines a commission parameters for a given validator.
|
||||
Commission struct { |
||||
CommissionRates `json:"commission_rates" yaml:"commission_rates"` |
||||
UpdateHeight big.Int `json:"update_time" yaml:"update_time"` // the block height the commission rate was last changed
|
||||
} |
||||
|
||||
// CommissionRates defines the initial commission rates to be used for creating a
|
||||
// validator.
|
||||
CommissionRates struct { |
||||
Rate Dec `json:"rate" yaml:"rate"` // the commission rate charged to delegators, as a fraction
|
||||
MaxRate Dec `json:"max_rate" yaml:"max_rate"` // maximum commission rate which validator can ever charge, as a fraction
|
||||
MaxChangeRate Dec `json:"max_change_rate" yaml:"max_change_rate"` // maximum increase of the validator commission every epoch, as a fraction
|
||||
} |
||||
) |
@ -0,0 +1,651 @@ |
||||
package types |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
"strconv" |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
// Dec represent a decimal. NOTE: never use new(Dec) or else we will panic unmarshalling into the
|
||||
// nil embedded big.Int
|
||||
type Dec struct { |
||||
*big.Int `json:"int"` |
||||
} |
||||
|
||||
// number of decimal places
|
||||
const ( |
||||
Precision = 18 |
||||
|
||||
// bytes required to represent the above precision
|
||||
// Ceiling[Log2[999 999 999 999 999 999]]
|
||||
DecimalPrecisionBits = 60 |
||||
) |
||||
|
||||
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) |
||||
) |
||||
|
||||
// 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) |
||||
} |
||||
|
||||
// ZeroDec ...
|
||||
func ZeroDec() Dec { return Dec{new(big.Int).Set(zeroInt)} } |
||||
|
||||
// OneDec ...
|
||||
func OneDec() Dec { return Dec{precisionInt()} } |
||||
|
||||
// SmallestDec ...
|
||||
func SmallestDec() Dec { return Dec{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] |
||||
} |
||||
|
||||
//______________________________________________________________________________________________
|
||||
|
||||
// NewDec creates a new Dec from integer assuming whole number
|
||||
func NewDec(i int64) Dec { |
||||
return NewDecWithPrec(i, 0) |
||||
} |
||||
|
||||
// NewDecWithPrec creates a new Dec from integer with decimal place at prec
|
||||
// CONTRACT: prec <= Precision
|
||||
func NewDecWithPrec(i, prec int64) Dec { |
||||
return Dec{ |
||||
new(big.Int).Mul(big.NewInt(i), precisionMultiplier(prec)), |
||||
} |
||||
} |
||||
|
||||
// NewDecFromBigInt creates a new Dec from big integer assuming whole numbers
|
||||
// CONTRACT: prec <= Precision
|
||||
func NewDecFromBigInt(i *big.Int) Dec { |
||||
return NewDecFromBigIntWithPrec(i, 0) |
||||
} |
||||
|
||||
// NewDecFromBigIntWithPrec creates a new Dec from big integer assuming whole numbers
|
||||
// CONTRACT: prec <= Precision
|
||||
func NewDecFromBigIntWithPrec(i *big.Int, prec int64) Dec { |
||||
return Dec{ |
||||
new(big.Int).Mul(i, precisionMultiplier(prec)), |
||||
} |
||||
} |
||||
|
||||
// NewDecFromInt creates a new Dec from big integer assuming whole numbers
|
||||
// CONTRACT: prec <= Precision
|
||||
func NewDecFromInt(i *big.Int) Dec { |
||||
return NewDecFromIntWithPrec(i, 0) |
||||
} |
||||
|
||||
// NewDecFromIntWithPrec creates a new Dec from big integer with decimal place at prec
|
||||
// CONTRACT: prec <= Precision
|
||||
func NewDecFromIntWithPrec(i *big.Int, prec int64) Dec { |
||||
return Dec{ |
||||
new(big.Int).Mul(i, precisionMultiplier(prec)), |
||||
} |
||||
} |
||||
|
||||
// NewDecFromStr creates 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) (d Dec, err error) { |
||||
if len(str) == 0 { |
||||
return d, errors.New("decimal string is empty") |
||||
} |
||||
|
||||
// first extract any negative symbol
|
||||
neg := false |
||||
if str[0] == '-' { |
||||
neg = true |
||||
str = str[1:] |
||||
} |
||||
|
||||
if len(str) == 0 { |
||||
return d, errors.New("decimal string is empty") |
||||
} |
||||
|
||||
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 d, errors.New("bad decimal length") |
||||
} |
||||
combinedStr += strs[1] |
||||
|
||||
} else if len(strs) > 2 { |
||||
return d, errors.New("too many periods to be a decimal string") |
||||
} |
||||
|
||||
if lenDecs > Precision { |
||||
return d, fmt.Errorf("too much precision, maximum %v, len decimal %v", 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 d, fmt.Errorf("bad string to integer conversion, combinedStr: %v", combinedStr) |
||||
} |
||||
if neg { |
||||
combined = new(big.Int).Neg(combined) |
||||
} |
||||
return Dec{combined}, nil |
||||
} |
||||
|
||||
// MustNewDecFromStr Decimal from string, panic on error
|
||||
func MustNewDecFromStr(s string) Dec { |
||||
dec, err := NewDecFromStr(s) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return dec |
||||
} |
||||
|
||||
// IsNil ...
|
||||
func (d Dec) IsNil() bool { return d.Int == nil } // is decimal nil
|
||||
// IsZero ...
|
||||
func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // is equal to zero
|
||||
// IsNegative ...
|
||||
func (d Dec) IsNegative() bool { return (d.Int).Sign() == -1 } // is negative
|
||||
// IsPositive ...
|
||||
func (d Dec) IsPositive() bool { return (d.Int).Sign() == 1 } // is positive
|
||||
// Equal ...
|
||||
func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 } // equal decimals
|
||||
// GT ...
|
||||
func (d Dec) GT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) > 0 } // greater than
|
||||
// GTE ...
|
||||
func (d Dec) GTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) >= 0 } // greater than or equal
|
||||
// LT ...
|
||||
func (d Dec) LT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) < 0 } // less than
|
||||
// LTE ...
|
||||
func (d Dec) LTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) <= 0 } // less than or equal
|
||||
// Neg ...
|
||||
func (d Dec) Neg() Dec { return Dec{new(big.Int).Neg(d.Int)} } // reverse the decimal sign
|
||||
// Abs ...
|
||||
func (d Dec) Abs() Dec { return Dec{new(big.Int).Abs(d.Int)} } // absolute value
|
||||
|
||||
// Add addition
|
||||
func (d Dec) Add(d2 Dec) Dec { |
||||
res := new(big.Int).Add(d.Int, d2.Int) |
||||
|
||||
if res.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{res} |
||||
} |
||||
|
||||
// Sub subtraction
|
||||
func (d Dec) Sub(d2 Dec) Dec { |
||||
res := new(big.Int).Sub(d.Int, d2.Int) |
||||
|
||||
if res.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{res} |
||||
} |
||||
|
||||
// Mul multiplication
|
||||
func (d Dec) Mul(d2 Dec) Dec { |
||||
mul := new(big.Int).Mul(d.Int, d2.Int) |
||||
chopped := chopPrecisionAndRound(mul) |
||||
|
||||
if chopped.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{chopped} |
||||
} |
||||
|
||||
// MulTruncate multiplication truncate
|
||||
func (d Dec) MulTruncate(d2 Dec) Dec { |
||||
mul := new(big.Int).Mul(d.Int, d2.Int) |
||||
chopped := chopPrecisionAndTruncate(mul) |
||||
|
||||
if chopped.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{chopped} |
||||
} |
||||
|
||||
// MulInt multiplication
|
||||
func (d Dec) MulInt(i *big.Int) Dec { |
||||
mul := new(big.Int).Mul(d.Int, i) |
||||
|
||||
if mul.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{mul} |
||||
} |
||||
|
||||
// MulInt64 - multiplication with int64
|
||||
func (d Dec) MulInt64(i int64) Dec { |
||||
mul := new(big.Int).Mul(d.Int, big.NewInt(i)) |
||||
|
||||
if mul.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{mul} |
||||
} |
||||
|
||||
// Quo quotient
|
||||
func (d Dec) Quo(d2 Dec) Dec { |
||||
|
||||
// multiply precision twice
|
||||
mul := new(big.Int).Mul(d.Int, precisionReuse) |
||||
mul.Mul(mul, precisionReuse) |
||||
|
||||
quo := new(big.Int).Quo(mul, d2.Int) |
||||
chopped := chopPrecisionAndRound(quo) |
||||
|
||||
if chopped.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{chopped} |
||||
} |
||||
|
||||
// QuoTruncate quotient truncate
|
||||
func (d Dec) QuoTruncate(d2 Dec) Dec { |
||||
|
||||
// multiply precision twice
|
||||
mul := new(big.Int).Mul(d.Int, precisionReuse) |
||||
mul.Mul(mul, precisionReuse) |
||||
|
||||
quo := new(big.Int).Quo(mul, d2.Int) |
||||
chopped := chopPrecisionAndTruncate(quo) |
||||
|
||||
if chopped.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{chopped} |
||||
} |
||||
|
||||
// QuoRoundUp quotient, round up
|
||||
func (d Dec) QuoRoundUp(d2 Dec) Dec { |
||||
// multiply precision twice
|
||||
mul := new(big.Int).Mul(d.Int, precisionReuse) |
||||
mul.Mul(mul, precisionReuse) |
||||
|
||||
quo := new(big.Int).Quo(mul, d2.Int) |
||||
chopped := chopPrecisionAndRoundUp(quo) |
||||
|
||||
if chopped.BitLen() > 255+DecimalPrecisionBits { |
||||
panic("Int overflow") |
||||
} |
||||
return Dec{chopped} |
||||
} |
||||
|
||||
// QuoInt quotient
|
||||
func (d Dec) QuoInt(i *big.Int) Dec { |
||||
mul := new(big.Int).Quo(d.Int, i) |
||||
return Dec{mul} |
||||
} |
||||
|
||||
// QuoInt64 - quotient with int64
|
||||
func (d Dec) QuoInt64(i int64) Dec { |
||||
mul := new(big.Int).Quo(d.Int, big.NewInt(i)) |
||||
return Dec{mul} |
||||
} |
||||
|
||||
// IsInteger is integer, e.g. decimals are zero
|
||||
func (d Dec) IsInteger() bool { |
||||
return new(big.Int).Rem(d.Int, precisionReuse).Sign() == 0 |
||||
} |
||||
|
||||
// Format decimal state
|
||||
func (d Dec) Format(s fmt.State, verb rune) { |
||||
_, err := s.Write([]byte(d.String())) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
func (d Dec) String() string { |
||||
if d.Int == nil { |
||||
return d.Int.String() |
||||
} |
||||
|
||||
isNeg := d.IsNegative() |
||||
if d.IsNegative() { |
||||
d = d.Neg() |
||||
} |
||||
|
||||
bzInt, err := d.Int.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) |
||||
} |
||||
|
||||
// ____
|
||||
// __| |__ "chop 'em
|
||||
// ` \ round!"
|
||||
// ___|| ~ _ -bankers
|
||||
// | | __
|
||||
// | | | __|__|__
|
||||
// |_____: / | $$$ |
|
||||
// |________|
|
||||
|
||||
// nolint - go-cyclo
|
||||
// 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 Dec) RoundInt64() int64 { |
||||
chopped := chopPrecisionAndRoundNonMutative(d.Int) |
||||
if !chopped.IsInt64() { |
||||
panic("Int64() out of bound") |
||||
} |
||||
return chopped.Int64() |
||||
} |
||||
|
||||
// RoundInt round the decimal using bankers rounding
|
||||
//func (d Dec) RoundInt() big.Int {
|
||||
// return NewIntFromBigInt(chopPrecisionAndRoundNonMutative(d.Int))
|
||||
//}
|
||||
|
||||
//___________________________________________________________________________________
|
||||
|
||||
// similar to chopPrecisionAndRound, but always rounds down
|
||||
func chopPrecisionAndTruncate(d *big.Int) *big.Int { |
||||
return d.Quo(d, precisionReuse) |
||||
} |
||||
|
||||
func chopPrecisionAndTruncateNonMutative(d *big.Int) *big.Int { |
||||
tmp := new(big.Int).Set(d) |
||||
return chopPrecisionAndTruncate(tmp) |
||||
} |
||||
|
||||
// TruncateInt64 truncates the decimals from the number and returns an int64
|
||||
func (d Dec) TruncateInt64() int64 { |
||||
chopped := chopPrecisionAndTruncateNonMutative(d.Int) |
||||
if !chopped.IsInt64() { |
||||
panic("Int64() out of bound") |
||||
} |
||||
return chopped.Int64() |
||||
} |
||||
|
||||
// TruncateInt truncates the decimals from the number and returns an Int
|
||||
//func (d Dec) TruncateInt() big.Int {
|
||||
// return NewIntFromBigInt(chopPrecisionAndTruncateNonMutative(d.Int))
|
||||
//}
|
||||
|
||||
// TruncateDec truncates the decimals from the number and returns a Dec
|
||||
func (d Dec) TruncateDec() Dec { |
||||
return NewDecFromBigInt(chopPrecisionAndTruncateNonMutative(d.Int)) |
||||
} |
||||
|
||||
// Ceil returns the smallest interger value (as a decimal) that is greater than
|
||||
// or equal to the given decimal.
|
||||
func (d Dec) Ceil() Dec { |
||||
tmp := new(big.Int).Set(d.Int) |
||||
|
||||
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)) |
||||
} |
||||
|
||||
//___________________________________________________________________________________
|
||||
|
||||
// reuse nil values
|
||||
var ( |
||||
nilAmino string |
||||
nilJSON []byte |
||||
) |
||||
|
||||
func init() { |
||||
empty := new(big.Int) |
||||
bz, err := empty.MarshalText() |
||||
if err != nil { |
||||
panic("bad nil amino init") |
||||
} |
||||
nilAmino = string(bz) |
||||
|
||||
nilJSON, err = json.Marshal(string(bz)) |
||||
if err != nil { |
||||
panic("bad nil json init") |
||||
} |
||||
} |
||||
|
||||
// MarshalAmino wraps d.MarshalText()
|
||||
func (d Dec) MarshalAmino() (string, error) { |
||||
if d.Int == nil { |
||||
return nilAmino, nil |
||||
} |
||||
bz, err := d.Int.MarshalText() |
||||
return string(bz), err |
||||
} |
||||
|
||||
// UnmarshalAmino requires a valid JSON string - strings quotes and calls UnmarshalText
|
||||
func (d *Dec) UnmarshalAmino(text string) (err error) { |
||||
tempInt := new(big.Int) |
||||
err = tempInt.UnmarshalText([]byte(text)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
d.Int = tempInt |
||||
return nil |
||||
} |
||||
|
||||
// MarshalJSON marshals the decimal
|
||||
func (d Dec) MarshalJSON() ([]byte, error) { |
||||
if d.Int == nil { |
||||
return nilJSON, nil |
||||
} |
||||
|
||||
return json.Marshal(d.String()) |
||||
} |
||||
|
||||
// UnmarshalJSON defines custom decoding scheme
|
||||
func (d *Dec) UnmarshalJSON(bz []byte) error { |
||||
if d.Int == nil { |
||||
d.Int = 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.Int = newDec.Int |
||||
return nil |
||||
} |
||||
|
||||
// MarshalYAML returns Ythe AML representation.
|
||||
func (d Dec) MarshalYAML() (interface{}, error) { return d.String(), nil } |
||||
|
||||
//___________________________________________________________________________________
|
||||
// helpers
|
||||
|
||||
// DecsEqual test if two decimal arrays are equal
|
||||
func DecsEqual(d1s, d2s []Dec) bool { |
||||
if len(d1s) != len(d2s) { |
||||
return false |
||||
} |
||||
|
||||
for i, d1 := range d1s { |
||||
if !d1.Equal(d2s[i]) { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// MinDec minimum decimal between two
|
||||
func MinDec(d1, d2 Dec) Dec { |
||||
if d1.LT(d2) { |
||||
return d1 |
||||
} |
||||
return d2 |
||||
} |
||||
|
||||
// MaxDec maximum decimal between two
|
||||
func MaxDec(d1, d2 Dec) Dec { |
||||
if d1.LT(d2) { |
||||
return d2 |
||||
} |
||||
return d1 |
||||
} |
||||
|
||||
// DecEq intended to be used with require/assert: require.True(DecEq(...))
|
||||
func DecEq(t *testing.T, exp, got Dec) (*testing.T, bool, string, string, string) { |
||||
return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() |
||||
} |
@ -0,0 +1,356 @@ |
||||
package types |
||||
|
||||
import ( |
||||
"math/big" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
// create a decimal from a decimal string (ex. "1234.5678")
|
||||
func mustNewDecFromStr(t *testing.T, str string) (d Dec) { |
||||
d, err := NewDecFromStr(str) |
||||
require.NoError(t, err) |
||||
return d |
||||
} |
||||
|
||||
//_______________________________________
|
||||
|
||||
func TestPrecisionMultiplier(t *testing.T) { |
||||
res := precisionMultiplier(5) |
||||
exp := big.NewInt(10000000000000) |
||||
require.Equal(t, 0, res.Cmp(exp), "equality was incorrect, res %v, exp %v", res, exp) |
||||
} |
||||
|
||||
func TestNewDecFromStr(t *testing.T) { |
||||
largeBigInt, success := new(big.Int).SetString("3144605511029693144278234343371835", 10) |
||||
require.True(t, success) |
||||
tests := []struct { |
||||
decimalStr string |
||||
expErr bool |
||||
exp Dec |
||||
}{ |
||||
{"", true, Dec{}}, |
||||
{"0.-75", true, Dec{}}, |
||||
{"0", false, NewDec(0)}, |
||||
{"1", false, NewDec(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, NewDec(3141203149163817869)}, |
||||
{"314460551102969314427823434337.1835718092488231350", |
||||
true, NewDecFromBigIntWithPrec(largeBigInt, 4)}, |
||||
{"314460551102969314427823434337.1835", |
||||
false, NewDecFromBigIntWithPrec(largeBigInt, 4)}, |
||||
{".", true, Dec{}}, |
||||
{".0", true, NewDec(0)}, |
||||
{"1.", true, NewDec(1)}, |
||||
{"foobar", true, Dec{}}, |
||||
{"0.foobar", true, Dec{}}, |
||||
{"0.foobar.", true, Dec{}}, |
||||
} |
||||
|
||||
for tcIndex, tc := range tests { |
||||
res, err := NewDecFromStr(tc.decimalStr) |
||||
if tc.expErr { |
||||
require.NotNil(t, err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) |
||||
} else { |
||||
require.Nil(t, err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) |
||||
require.True(t, 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 { |
||||
require.NotNil(t, err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) |
||||
} else { |
||||
require.Nil(t, err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) |
||||
exp := tc.exp.Mul(NewDec(-1)) |
||||
require.True(t, res.Equal(exp), "equality was incorrect, res %v, exp %v, tc %v", res, exp, tcIndex) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestDecString(t *testing.T) { |
||||
tests := []struct { |
||||
d Dec |
||||
want string |
||||
}{ |
||||
{NewDec(0), "0.000000000000000000"}, |
||||
{NewDec(1), "1.000000000000000000"}, |
||||
{NewDec(10), "10.000000000000000000"}, |
||||
{NewDec(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 { |
||||
assert.Equal(t, tc.want, tc.d.String(), "bad String(), index: %v", tcIndex) |
||||
} |
||||
} |
||||
|
||||
func TestEqualities(t *testing.T) { |
||||
tests := []struct { |
||||
d1, d2 Dec |
||||
gt, lt, eq bool |
||||
}{ |
||||
{NewDec(0), NewDec(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 { |
||||
require.Equal(t, tc.gt, tc.d1.GT(tc.d2), "GT result is incorrect, tc %d", tcIndex) |
||||
require.Equal(t, tc.lt, tc.d1.LT(tc.d2), "LT result is incorrect, tc %d", tcIndex) |
||||
require.Equal(t, tc.eq, tc.d1.Equal(tc.d2), "equality result is incorrect, tc %d", tcIndex) |
||||
} |
||||
|
||||
} |
||||
|
||||
func TestDecsEqual(t *testing.T) { |
||||
tests := []struct { |
||||
d1s, d2s []Dec |
||||
eq bool |
||||
}{ |
||||
{[]Dec{NewDec(0)}, []Dec{NewDec(0)}, true}, |
||||
{[]Dec{NewDec(0)}, []Dec{NewDec(1)}, false}, |
||||
{[]Dec{NewDec(0)}, []Dec{}, false}, |
||||
{[]Dec{NewDec(0), NewDec(1)}, []Dec{NewDec(0), NewDec(1)}, true}, |
||||
{[]Dec{NewDec(1), NewDec(0)}, []Dec{NewDec(1), NewDec(0)}, true}, |
||||
{[]Dec{NewDec(1), NewDec(0)}, []Dec{NewDec(0), NewDec(1)}, false}, |
||||
{[]Dec{NewDec(1), NewDec(0)}, []Dec{NewDec(1)}, false}, |
||||
{[]Dec{NewDec(1), NewDec(2)}, []Dec{NewDec(2), NewDec(4)}, false}, |
||||
{[]Dec{NewDec(3), NewDec(18)}, []Dec{NewDec(1), NewDec(6)}, false}, |
||||
} |
||||
|
||||
for tcIndex, tc := range tests { |
||||
require.Equal(t, tc.eq, DecsEqual(tc.d1s, tc.d2s), "equality of decional arrays is incorrect, tc %d", tcIndex) |
||||
require.Equal(t, tc.eq, DecsEqual(tc.d2s, tc.d1s), "equality of decional arrays is incorrect (converse), tc %d", tcIndex) |
||||
} |
||||
} |
||||
|
||||
func TestArithmetic(t *testing.T) { |
||||
tests := []struct { |
||||
d1, d2 Dec |
||||
expMul, expMulTruncate Dec |
||||
expQuo, expQuoRoundUp, expQuoTruncate Dec |
||||
expAdd, expSub Dec |
||||
}{ |
||||
// d1 d2 MUL MulTruncate QUO QUORoundUp QUOTrunctate ADD SUB
|
||||
{NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0)}, |
||||
{NewDec(1), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(1), NewDec(1)}, |
||||
{NewDec(0), NewDec(1), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(1), NewDec(-1)}, |
||||
{NewDec(0), NewDec(-1), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(-1), NewDec(1)}, |
||||
{NewDec(-1), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(0), NewDec(-1), NewDec(-1)}, |
||||
|
||||
{NewDec(1), NewDec(1), NewDec(1), NewDec(1), NewDec(1), NewDec(1), NewDec(1), NewDec(2), NewDec(0)}, |
||||
{NewDec(-1), NewDec(-1), NewDec(1), NewDec(1), NewDec(1), NewDec(1), NewDec(1), NewDec(-2), NewDec(0)}, |
||||
{NewDec(1), NewDec(-1), NewDec(-1), NewDec(-1), NewDec(-1), NewDec(-1), NewDec(-1), NewDec(0), NewDec(2)}, |
||||
{NewDec(-1), NewDec(1), NewDec(-1), NewDec(-1), NewDec(-1), NewDec(-1), NewDec(-1), NewDec(0), NewDec(-2)}, |
||||
|
||||
{NewDec(3), NewDec(7), NewDec(21), NewDec(21), |
||||
NewDecWithPrec(428571428571428571, 18), NewDecWithPrec(428571428571428572, 18), NewDecWithPrec(428571428571428571, 18), |
||||
NewDec(10), NewDec(-4)}, |
||||
{NewDec(2), NewDec(4), NewDec(8), NewDec(8), NewDecWithPrec(5, 1), NewDecWithPrec(5, 1), NewDecWithPrec(5, 1), |
||||
NewDec(6), NewDec(-2)}, |
||||
|
||||
{NewDec(100), NewDec(100), NewDec(10000), NewDec(10000), NewDec(1), NewDec(1), NewDec(1), NewDec(200), NewDec(0)}, |
||||
|
||||
{NewDecWithPrec(15, 1), NewDecWithPrec(15, 1), NewDecWithPrec(225, 2), NewDecWithPrec(225, 2), |
||||
NewDec(1), NewDec(1), NewDec(1), NewDec(3), NewDec(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 { |
||||
resAdd := tc.d1.Add(tc.d2) |
||||
resSub := tc.d1.Sub(tc.d2) |
||||
resMul := tc.d1.Mul(tc.d2) |
||||
resMulTruncate := tc.d1.MulTruncate(tc.d2) |
||||
require.True(t, tc.expAdd.Equal(resAdd), "exp %v, res %v, tc %d", tc.expAdd, resAdd, tcIndex) |
||||
require.True(t, tc.expSub.Equal(resSub), "exp %v, res %v, tc %d", tc.expSub, resSub, tcIndex) |
||||
require.True(t, tc.expMul.Equal(resMul), "exp %v, res %v, tc %d", tc.expMul, resMul, tcIndex) |
||||
require.True(t, tc.expMulTruncate.Equal(resMulTruncate), "exp %v, res %v, tc %d", tc.expMulTruncate, resMulTruncate, tcIndex) |
||||
|
||||
if tc.d2.IsZero() { // panic for divide by zero
|
||||
require.Panics(t, func() { tc.d1.Quo(tc.d2) }) |
||||
} else { |
||||
resQuo := tc.d1.Quo(tc.d2) |
||||
require.True(t, tc.expQuo.Equal(resQuo), "exp %v, res %v, tc %d", tc.expQuo.String(), resQuo.String(), tcIndex) |
||||
|
||||
resQuoRoundUp := tc.d1.QuoRoundUp(tc.d2) |
||||
require.True(t, tc.expQuoRoundUp.Equal(resQuoRoundUp), "exp %v, res %v, tc %d", |
||||
tc.expQuoRoundUp.String(), resQuoRoundUp.String(), tcIndex) |
||||
|
||||
resQuoTruncate := tc.d1.QuoTruncate(tc.d2) |
||||
require.True(t, tc.expQuoTruncate.Equal(resQuoTruncate), "exp %v, res %v, tc %d", |
||||
tc.expQuoTruncate.String(), resQuoTruncate.String(), tcIndex) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestBankerRoundChop(t *testing.T) { |
||||
tests := []struct { |
||||
d1 Dec |
||||
exp int64 |
||||
}{ |
||||
{mustNewDecFromStr(t, "0.25"), 0}, |
||||
{mustNewDecFromStr(t, "0"), 0}, |
||||
{mustNewDecFromStr(t, "1"), 1}, |
||||
{mustNewDecFromStr(t, "0.75"), 1}, |
||||
{mustNewDecFromStr(t, "0.5"), 0}, |
||||
{mustNewDecFromStr(t, "7.5"), 8}, |
||||
{mustNewDecFromStr(t, "1.5"), 2}, |
||||
{mustNewDecFromStr(t, "2.5"), 2}, |
||||
{mustNewDecFromStr(t, "0.545"), 1}, // 0.545-> 1 even though 5 is first decimal and 1 not even
|
||||
{mustNewDecFromStr(t, "1.545"), 2}, |
||||
} |
||||
|
||||
for tcIndex, tc := range tests { |
||||
resNeg := tc.d1.Neg().RoundInt64() |
||||
require.Equal(t, -1*tc.exp, resNeg, "negative tc %d", tcIndex) |
||||
|
||||
resPos := tc.d1.RoundInt64() |
||||
require.Equal(t, tc.exp, resPos, "positive tc %d", tcIndex) |
||||
} |
||||
} |
||||
|
||||
func TestTruncate(t *testing.T) { |
||||
tests := []struct { |
||||
d1 Dec |
||||
exp int64 |
||||
}{ |
||||
{mustNewDecFromStr(t, "0"), 0}, |
||||
{mustNewDecFromStr(t, "0.25"), 0}, |
||||
{mustNewDecFromStr(t, "0.75"), 0}, |
||||
{mustNewDecFromStr(t, "1"), 1}, |
||||
{mustNewDecFromStr(t, "1.5"), 1}, |
||||
{mustNewDecFromStr(t, "7.5"), 7}, |
||||
{mustNewDecFromStr(t, "7.6"), 7}, |
||||
{mustNewDecFromStr(t, "7.4"), 7}, |
||||
{mustNewDecFromStr(t, "100.1"), 100}, |
||||
{mustNewDecFromStr(t, "1000.1"), 1000}, |
||||
} |
||||
|
||||
for tcIndex, tc := range tests { |
||||
resNeg := tc.d1.Neg().TruncateInt64() |
||||
require.Equal(t, -1*tc.exp, resNeg, "negative tc %d", tcIndex) |
||||
|
||||
resPos := tc.d1.TruncateInt64() |
||||
require.Equal(t, tc.exp, resPos, "positive tc %d", tcIndex) |
||||
} |
||||
} |
||||
|
||||
func TestDecMarshalJSON(t *testing.T) { |
||||
decimal := func(i int64) Dec { |
||||
d := NewDec(0) |
||||
d.Int = new(big.Int).SetInt64(i) |
||||
return d |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
d Dec |
||||
want string |
||||
wantErr bool // if wantErr = false, will also attempt unmarshaling
|
||||
}{ |
||||
{"zero", decimal(0), "\"0.000000000000000000\"", false}, |
||||
{"one", decimal(1), "\"0.000000000000000001\"", false}, |
||||
{"ten", decimal(10), "\"0.000000000000000010\"", false}, |
||||
{"12340", decimal(12340), "\"0.000000000000012340\"", false}, |
||||
{"zeroInt", NewDec(0), "\"0.000000000000000000\"", false}, |
||||
{"oneInt", NewDec(1), "\"1.000000000000000000\"", false}, |
||||
{"tenInt", NewDec(10), "\"10.000000000000000000\"", false}, |
||||
{"12340Int", NewDec(12340), "\"12340.000000000000000000\"", false}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
got, err := tt.d.MarshalJSON() |
||||
if (err != nil) != tt.wantErr { |
||||
t.Errorf("Dec.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) |
||||
return |
||||
} |
||||
if !tt.wantErr { |
||||
assert.Equal(t, tt.want, string(got), "incorrect marshalled value") |
||||
unmarshalledDec := NewDec(0) |
||||
unmarshalledDec.UnmarshalJSON(got) |
||||
assert.Equal(t, tt.d, unmarshalledDec, "incorrect unmarshalled value") |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestSerializationText(t *testing.T) { |
||||
d := mustNewDecFromStr(t, "0.333") |
||||
|
||||
bz, err := d.MarshalText() |
||||
require.NoError(t, err) |
||||
|
||||
d2 := Dec{new(big.Int)} |
||||
err = d2.UnmarshalText(bz) |
||||
require.NoError(t, err) |
||||
require.True(t, d.Equal(d2), "original: %v, unmarshalled: %v", d, d2) |
||||
} |
||||
|
||||
type testDEmbedStruct struct { |
||||
Field1 string `json:"f1"` |
||||
Field2 int `json:"f2"` |
||||
Field3 Dec `json:"f3"` |
||||
} |
||||
|
||||
func TestStringOverflow(t *testing.T) { |
||||
// two random 64 bit primes
|
||||
dec1, err := NewDecFromStr("51643150036226787134389711697696177267") |
||||
require.NoError(t, err) |
||||
dec2, err := NewDecFromStr("-31798496660535729618459429845579852627") |
||||
require.NoError(t, err) |
||||
dec3 := dec1.Add(dec2) |
||||
require.Equal(t, |
||||
"19844653375691057515930281852116324640.000000000000000000", |
||||
dec3.String(), |
||||
) |
||||
} |
||||
|
||||
func TestDecCeil(t *testing.T) { |
||||
testCases := []struct { |
||||
input Dec |
||||
expected Dec |
||||
}{ |
||||
{NewDecWithPrec(1000000000000000, Precision), NewDec(1)}, // 0.001 => 1.0
|
||||
{NewDecWithPrec(-1000000000000000, Precision), ZeroDec()}, // -0.001 => 0.0
|
||||
{ZeroDec(), ZeroDec()}, // 0.0 => 0.0
|
||||
{NewDecWithPrec(900000000000000000, Precision), NewDec(1)}, // 0.9 => 1.0
|
||||
{NewDecWithPrec(4001000000000000000, Precision), NewDec(5)}, // 4.001 => 5.0
|
||||
{NewDecWithPrec(-4001000000000000000, Precision), NewDec(-4)}, // -4.001 => -4.0
|
||||
{NewDecWithPrec(4700000000000000000, Precision), NewDec(5)}, // 4.7 => 5.0
|
||||
{NewDecWithPrec(-4700000000000000000, Precision), NewDec(-4)}, // -4.7 => -4.0
|
||||
} |
||||
|
||||
for i, tc := range testCases { |
||||
res := tc.input.Ceil() |
||||
require.Equal(t, tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
package types |
||||
|
||||
import ( |
||||
"math/big" |
||||
|
||||
"github.com/harmony-one/bls/ffi/go/bls" |
||||
"github.com/harmony-one/harmony/internal/common" |
||||
) |
||||
|
||||
// Validator - data fields for a validator
|
||||
type Validator struct { |
||||
StakingAddress common.Address `json:"staking_address" yaml:"staking_address"` // ECDSA address of the validator
|
||||
ValidatingPubKey bls.PublicKey `json:"validating_pub_key" yaml:"validating_pub_key"` // The BLS public key of the validator for consensus
|
||||
Description Description `json:"description" yaml:"description"` // description for the validator
|
||||
Active bool `json:"active" yaml:"active"` // Is the validator active in the validating process or not
|
||||
Stake big.Int `json:"stake" yaml:"stake"` // The stake put by the validator itself
|
||||
UnbondingHeight big.Int `json:"unbonding_height" yaml:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding
|
||||
Commission Commission `json:"commission" yaml:"commission"` // commission parameters
|
||||
MinSelfDelegation big.Int `json:"min_self_delegation" yaml:"min_self_delegation"` // validator's self declared minimum self delegation
|
||||
} |
||||
|
||||
// Description - description fields for a validator
|
||||
type Description struct { |
||||
Name string `json:"name" yaml:"name"` // name
|
||||
Identity string `json:"identity" yaml:"identity"` // optional identity signature (ex. UPort or Keybase)
|
||||
Website string `json:"website" yaml:"website"` // optional website link
|
||||
SecurityContact string `json:"security_contact" yaml:"security_contact"` // optional security contact info
|
||||
Details string `json:"details" yaml:"details"` // optional details
|
||||
} |
Loading…
Reference in new issue