From d84430922146da9ddf534b9a99f834ac30893060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadej=20Jane=C5=BE?= Date: Fri, 24 Jul 2020 14:06:02 +0200 Subject: [PATCH 1/3] go/common/prettyprint: Add FractionBase10() --- .changelog/3129.feature.md | 1 + go/common/prettyprint/quantity.go | 33 ++++++++++++++++++++++ go/common/prettyprint/quantity_test.go | 38 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 .changelog/3129.feature.md create mode 100644 go/common/prettyprint/quantity.go create mode 100644 go/common/prettyprint/quantity_test.go diff --git a/.changelog/3129.feature.md b/.changelog/3129.feature.md new file mode 100644 index 00000000000..5a0b0f02dfc --- /dev/null +++ b/.changelog/3129.feature.md @@ -0,0 +1 @@ +go/common/prettyprint: Add `FractionBase10()` diff --git a/go/common/prettyprint/quantity.go b/go/common/prettyprint/quantity.go new file mode 100644 index 00000000000..cf951e2a0a6 --- /dev/null +++ b/go/common/prettyprint/quantity.go @@ -0,0 +1,33 @@ +package prettyprint + +import ( + "fmt" + "math/big" + "strings" + + "github.com/oasisprotocol/oasis-core/go/common/quantity" +) + +// FractionBase10 returns a decimal representation of a fraction from fraction's +// numerator and denominator's base-10 exponent. +func FractionBase10(numerator quantity.Quantity, denominatorExp uint8) string { + denominator := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(denominatorExp)), nil) + + // NOTE: We use DivMod() and manual string construction to avoid conversion + // to other types and support arbitrarily large amounts. + var quotient, remainder *big.Int + quotient, remainder = new(big.Int).DivMod(numerator.ToBigInt(), denominator, new(big.Int)) + + // Prefix the remainder with the appropriate number of zeros. + remainderStr := fmt.Sprintf("%0*s", denominatorExp, remainder) + // Trim trailing zeros from the remainder. + remainderStr = strings.TrimRight(remainderStr, "0") + // Ensure remainder is not empty. + if remainderStr == "" { + remainderStr = "0" + } + + // Combine quotient and remainder to a string representing the decimal + // representation of the given fraction. + return fmt.Sprintf("%s.%s", quotient, remainderStr) +} diff --git a/go/common/prettyprint/quantity_test.go b/go/common/prettyprint/quantity_test.go new file mode 100644 index 00000000000..23475513887 --- /dev/null +++ b/go/common/prettyprint/quantity_test.go @@ -0,0 +1,38 @@ +package prettyprint + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/oasisprotocol/oasis-core/go/common/quantity" +) + +func TestBase10Fraction(t *testing.T) { + require := require.New(t) + + for _, t := range []struct { + expectedOutput string + numerator *quantity.Quantity + denominatorExp uint8 + }{ + {"10000000000.0", quantity.NewFromUint64(10000000000000000000), 9}, + {"100.0", quantity.NewFromUint64(100000000000), 9}, + {"7999217230.11968289", quantity.NewFromUint64(7999217230119682890), 9}, + {"7999217230.1196", quantity.NewFromUint64(7999217230119600000), 9}, + {"7999217230.1", quantity.NewFromUint64(7999217230100000000), 9}, + {"0.0", quantity.NewFromUint64(0), 9}, + // Checks for large and small denominator base-10 exponents. + {"0.010000000000000000001", quantity.NewFromUint64(10000000000000000001), 21}, + {"10.0", quantity.NewFromUint64(10000000000000000000), 18}, + {"10.000000000000000001", quantity.NewFromUint64(10000000000000000001), 18}, + {"0.0000001", quantity.NewFromUint64(100000000000), 18}, + {"0.0", quantity.NewFromUint64(0), 18}, + {"10000000000000000000.0", quantity.NewFromUint64(10000000000000000000), 0}, + {"10000000000000000001.0", quantity.NewFromUint64(10000000000000000001), 0}, + {"0.0", quantity.NewFromUint64(0), 0}, + } { + output := FractionBase10(*t.numerator, t.denominatorExp) + require.Equal(t.expectedOutput, output, "obtained pretty print didn't match expected value") + } +} From 60f4545e56cd7a5e7d169906b84e785cdf3b7de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadej=20Jane=C5=BE?= Date: Fri, 24 Jul 2020 14:11:37 +0200 Subject: [PATCH 2/3] go/staking/api: Set CommissionRateDenominator via its base-10 exponent Set its base-10 exponent in commissionRateDenominatorExponent and use it to compute the CommissionRateDenominator. --- go/staking/api/commission.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/go/staking/api/commission.go b/go/staking/api/commission.go index 6cf36aa0109..b49834eb252 100644 --- a/go/staking/api/commission.go +++ b/go/staking/api/commission.go @@ -12,6 +12,13 @@ import ( epochtime "github.com/oasisprotocol/oasis-core/go/epochtime/api" ) +// commissionRateDenominatorExponent is the commission rate denominator's +// base-10 exponent. +// +// NOTE: Setting it to 5 means commission rates are denominated in 1000ths of a +// percent. +const commissionRateDenominatorExponent uint8 = 5 + var ( // CommissionRateDenominator is the denominator for the commission rate. CommissionRateDenominator *quantity.Quantity @@ -388,9 +395,11 @@ func (cs *CommissionSchedule) CurrentRate(now epochtime.EpochTime) *quantity.Qua } func init() { - // Denominated in 1000th of a percent. + // Compute CommissionRateDenominator from its base-10 exponent. CommissionRateDenominator = quantity.NewQuantity() - err := CommissionRateDenominator.FromInt64(100_000) + err := CommissionRateDenominator.FromBigInt( + new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(commissionRateDenominatorExponent)), nil), + ) if err != nil { panic(err) } From ae3c93734c5daa169a99eaa815d944c70c4ae377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadej=20Jane=C5=BE?= Date: Fri, 24 Jul 2020 14:08:49 +0200 Subject: [PATCH 3/3] go/staking/api: Use go/common/prettyprint.FractionBase10() Make ConvertToTokenAmount() and PrettyPrintCommissionRatePercentage() use the newly introduced FractionBase10 function. Add tests for PrettyPrintCommissionRatePercentage() and move it to go/staking/api/prettyprint.go. --- go/staking/api/commission.go | 18 +++------------ go/staking/api/prettyprint.go | 36 +++++++++++++----------------- go/staking/api/prettyprint_test.go | 22 ++++++++++++++++++ 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/go/staking/api/commission.go b/go/staking/api/commission.go index b49834eb252..6227ef3fb44 100644 --- a/go/staking/api/commission.go +++ b/go/staking/api/commission.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "math/big" - "strconv" "github.com/oasisprotocol/oasis-core/go/common/prettyprint" "github.com/oasisprotocol/oasis-core/go/common/quantity" @@ -28,17 +27,6 @@ var ( _ prettyprint.PrettyPrinter = (*CommissionSchedule)(nil) ) -// CommissionRatePercentage returns the string representing the commission rate -// in percentage for the given commission rate numerator. -func CommissionRatePercentage(rateNumerator quantity.Quantity) string { - rate := big.NewRat(rateNumerator.ToBigInt().Int64(), CommissionRateDenominator.ToBigInt().Int64()) - // Multiply rate by 100 to convert it to percentage. - rate.Mul(rate, big.NewRat(100, 1)) - // Return string representation of the rate that omits the trailing zeros. - rateFloat, _ := rate.Float64() - return strconv.FormatFloat(rateFloat, 'f', -1, 64) -} - // CommissionScheduleRules controls how commission schedule rates and rate // bounds are allowed to be changed. type CommissionScheduleRules struct { @@ -66,7 +54,7 @@ type CommissionRateStep struct { func (crs CommissionRateStep) PrettyPrint(ctx context.Context, prefix string, w io.Writer) { fmt.Fprintf(w, "%s- Start: epoch %d\n", prefix, crs.Start) - fmt.Fprintf(w, "%s Rate: %s %%\n", prefix, CommissionRatePercentage(crs.Rate)) + fmt.Fprintf(w, "%s Rate: %s\n", prefix, PrettyPrintCommissionRatePercentage(crs.Rate)) } // PrettyType returns a representation of CommissionRateStep that can be used @@ -91,8 +79,8 @@ type CommissionRateBoundStep struct { func (crbs CommissionRateBoundStep) PrettyPrint(ctx context.Context, prefix string, w io.Writer) { fmt.Fprintf(w, "%s- Start: epoch %d\n", prefix, crbs.Start) - fmt.Fprintf(w, "%s Minimum Rate: %s %%\n", prefix, CommissionRatePercentage(crbs.RateMin)) - fmt.Fprintf(w, "%s Maximum Rate: %s %%\n", prefix, CommissionRatePercentage(crbs.RateMax)) + fmt.Fprintf(w, "%s Minimum Rate: %s\n", prefix, PrettyPrintCommissionRatePercentage(crbs.RateMin)) + fmt.Fprintf(w, "%s Maximum Rate: %s\n", prefix, PrettyPrintCommissionRatePercentage(crbs.RateMax)) } // PrettyType returns a representation of CommissionRateBoundStep that can be diff --git a/go/staking/api/prettyprint.go b/go/staking/api/prettyprint.go index 5f076fdb7f9..5b94691cb47 100644 --- a/go/staking/api/prettyprint.go +++ b/go/staking/api/prettyprint.go @@ -4,9 +4,8 @@ import ( "context" "fmt" "io" - "math/big" - "strings" + "github.com/oasisprotocol/oasis-core/go/common/prettyprint" "github.com/oasisprotocol/oasis-core/go/common/quantity" ) @@ -29,24 +28,7 @@ func ConvertToTokenAmount(amount quantity.Quantity, tokenValueExponent uint8) (s return "", ErrInvalidTokenValueExponent } - divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenValueExponent)), nil) - - // NOTE: We use DivMod() and manual string construction to avoid conversion - // to other types and support arbitrarily large amounts. - var quotient, remainder *big.Int - quotient, remainder = new(big.Int).DivMod(amount.ToBigInt(), divisor, new(big.Int)) - - // Prefix the remainder with the appropriate number of zeros. - remainderStr := fmt.Sprintf("%0*s", tokenValueExponent, remainder) - // Trim trailing zeros from the remainder. - remainderStr = strings.TrimRight(remainderStr, "0") - // Ensure remainder is not empty. - if remainderStr == "" { - remainderStr = "0" - } - - // Combine quotient and remainder to a string representing the token amount. - return fmt.Sprintf("%s.%s", quotient, remainderStr), nil + return prettyprint.FractionBase10(amount, tokenValueExponent), nil } // PrettyPrintAmount writes a pretty-printed representation of the given amount @@ -82,3 +64,17 @@ func PrettyPrintAmount(ctx context.Context, amount quantity.Quantity, w io.Write fmt.Fprintf(w, "%s %s", symbol, tokenAmount) } } + +// PrettyPrintCommissionRatePercentage returns the string representing the +// commission rate (bound) in percentage for the given commission rate (bound) +// numerator. +func PrettyPrintCommissionRatePercentage(rateNumerator quantity.Quantity) string { + // Handle invalid commission rate (bound) numerator. + if rateNumerator.Cmp(CommissionRateDenominator) > 0 { + return "(invalid)" + } + // Reduce commission rate denominator's base-10 exponent by 2 to obtain the + // value in percentage. + denominatorExp := commissionRateDenominatorExponent - 2 + return fmt.Sprintf("%s%%", prettyprint.FractionBase10(rateNumerator, denominatorExp)) +} diff --git a/go/staking/api/prettyprint_test.go b/go/staking/api/prettyprint_test.go index f25445a11e4..5502a1d73d4 100644 --- a/go/staking/api/prettyprint_test.go +++ b/go/staking/api/prettyprint_test.go @@ -89,3 +89,25 @@ func TestPrettyPrintAmount(t *testing.T) { "pretty printing stake amount didn't return the expected result") } } + +func TestPrettyPrintCommissionRatePercentage(t *testing.T) { + require := require.New(t) + + for _, t := range []struct { + expectedRate string + rateNumerator *quantity.Quantity + }{ + {"0.0%", quantity.NewFromUint64(0)}, + {"50.0%", quantity.NewFromUint64(50_000)}, + {"100.0%", quantity.NewFromUint64(100_000)}, + {"20.2%", quantity.NewFromUint64(20_200)}, + {"30.03%", quantity.NewFromUint64(30_030)}, + {"12.345%", quantity.NewFromUint64(12_345)}, + // Checks for invalid commission rate numerators. + {"(invalid)", quantity.NewFromUint64(100_001)}, + {"(invalid)", quantity.NewFromUint64(123_456)}, + } { + rate := PrettyPrintCommissionRatePercentage(*t.rateNumerator) + require.Equal(t.expectedRate, rate, "obtained pretty print didn't match expected value") + } +}