From 47018d2b6f87d859565cf4903b7ba66ef988afd6 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 19 Aug 2020 10:37:15 +0100 Subject: [PATCH] math: Change Decimal.multiply to operate on Uint classes --- packages/launchpad/src/gas.ts | 5 +- packages/math/src/decimal.spec.ts | 87 +++++++++++++++++++++++-------- packages/math/src/decimal.ts | 10 ++-- packages/math/types/decimal.d.ts | 5 +- 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/packages/launchpad/src/gas.ts b/packages/launchpad/src/gas.ts index a888b94b..8f27dfdc 100644 --- a/packages/launchpad/src/gas.ts +++ b/packages/launchpad/src/gas.ts @@ -1,4 +1,4 @@ -import { Decimal } from "@cosmjs/math"; +import { Decimal, Uint53 } from "@cosmjs/math"; import { coins } from "./coins"; import { StdFee } from "./types"; @@ -34,8 +34,7 @@ export type GasLimits> = { }; function calculateFee(gasLimit: number, { denom, amount: gasPriceAmount }: GasPrice): StdFee { - const gasLimitDecimal = Decimal.fromUserInput(gasLimit.toString(), gasPriceAmount.fractionalDigits); - const amount = Math.ceil(gasPriceAmount.multiply(gasLimitDecimal).toFloatApproximation()); + const amount = Math.ceil(gasPriceAmount.multiply(new Uint53(gasLimit)).toFloatApproximation()); return { amount: coins(amount, denom), gas: gasLimit.toString(), diff --git a/packages/math/src/decimal.spec.ts b/packages/math/src/decimal.spec.ts index b59e556b..9f9dee5b 100644 --- a/packages/math/src/decimal.spec.ts +++ b/packages/math/src/decimal.spec.ts @@ -1,4 +1,5 @@ import { Decimal } from "./decimal"; +import { Uint32, Uint53, Uint64 } from "./integers"; describe("Decimal", () => { describe("fromAtomics", () => { @@ -212,27 +213,24 @@ describe("Decimal", () => { }); describe("multiply", () => { - it("returns correct values", () => { + it("returns correct values for Uint32", () => { const zero = Decimal.fromUserInput("0", 5); - expect(zero.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(0)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(1)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(2)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(4294967295)).toString()).toEqual("0"); const one = Decimal.fromUserInput("1", 5); - expect(one.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); - expect(one.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("1"); - expect(one.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("2"); - expect(one.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("2.8"); - expect(one.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.12345"); + expect(one.multiply(new Uint32(0)).toString()).toEqual("0"); + expect(one.multiply(new Uint32(1)).toString()).toEqual("1"); + expect(one.multiply(new Uint32(2)).toString()).toEqual("2"); + expect(one.multiply(new Uint32(4294967295)).toString()).toEqual("4294967295"); const oneDotFive = Decimal.fromUserInput("1.5", 5); - expect(oneDotFive.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); - expect(oneDotFive.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("1.5"); - expect(oneDotFive.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("3"); - expect(oneDotFive.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("4.2"); - expect(oneDotFive.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.18517"); + expect(oneDotFive.multiply(new Uint32(0)).toString()).toEqual("0"); + expect(oneDotFive.multiply(new Uint32(1)).toString()).toEqual("1.5"); + expect(oneDotFive.multiply(new Uint32(2)).toString()).toEqual("3"); + expect(oneDotFive.multiply(new Uint32(4294967295)).toString()).toEqual("6442450942.5"); // original value remain unchanged expect(zero.toString()).toEqual("0"); @@ -240,15 +238,58 @@ describe("Decimal", () => { expect(oneDotFive.toString()).toEqual("1.5"); }); - it("throws for different fractional digits", () => { + it("returns correct values for Uint53", () => { const zero = Decimal.fromUserInput("0", 5); - expect(() => zero.multiply(Decimal.fromUserInput("1", 1))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 2))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 3))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 4))).toThrowError(/do not match/i); + expect(zero.multiply(new Uint53(0)).toString()).toEqual("0"); + expect(zero.multiply(new Uint53(1)).toString()).toEqual("0"); + expect(zero.multiply(new Uint53(2)).toString()).toEqual("0"); + expect(zero.multiply(new Uint53(9007199254740991)).toString()).toEqual("0"); - expect(() => zero.multiply(Decimal.fromUserInput("1", 6))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 7))).toThrowError(/do not match/i); + const one = Decimal.fromUserInput("1", 5); + expect(one.multiply(new Uint53(0)).toString()).toEqual("0"); + expect(one.multiply(new Uint53(1)).toString()).toEqual("1"); + expect(one.multiply(new Uint53(2)).toString()).toEqual("2"); + expect(one.multiply(new Uint53(9007199254740991)).toString()).toEqual("9007199254740991"); + + const oneDotFive = Decimal.fromUserInput("1.5", 5); + expect(oneDotFive.multiply(new Uint53(0)).toString()).toEqual("0"); + expect(oneDotFive.multiply(new Uint53(1)).toString()).toEqual("1.5"); + expect(oneDotFive.multiply(new Uint53(2)).toString()).toEqual("3"); + expect(oneDotFive.multiply(new Uint53(9007199254740991)).toString()).toEqual("13510798882111486.5"); + + // original value remain unchanged + expect(zero.toString()).toEqual("0"); + expect(one.toString()).toEqual("1"); + expect(oneDotFive.toString()).toEqual("1.5"); + }); + + it("returns correct values for Uint64", () => { + const zero = Decimal.fromUserInput("0", 5); + expect(zero.multiply(Uint64.fromString("0")).toString()).toEqual("0"); + expect(zero.multiply(Uint64.fromString("1")).toString()).toEqual("0"); + expect(zero.multiply(Uint64.fromString("2")).toString()).toEqual("0"); + expect(zero.multiply(Uint64.fromString("18446744073709551615")).toString()).toEqual("0"); + + const one = Decimal.fromUserInput("1", 5); + expect(one.multiply(Uint64.fromString("0")).toString()).toEqual("0"); + expect(one.multiply(Uint64.fromString("1")).toString()).toEqual("1"); + expect(one.multiply(Uint64.fromString("2")).toString()).toEqual("2"); + expect(one.multiply(Uint64.fromString("18446744073709551615")).toString()).toEqual( + "18446744073709551615", + ); + + const oneDotFive = Decimal.fromUserInput("1.5", 5); + expect(oneDotFive.multiply(Uint64.fromString("0")).toString()).toEqual("0"); + expect(oneDotFive.multiply(Uint64.fromString("1")).toString()).toEqual("1.5"); + expect(oneDotFive.multiply(Uint64.fromString("2")).toString()).toEqual("3"); + expect(oneDotFive.multiply(Uint64.fromString("18446744073709551615")).toString()).toEqual( + "27670116110564327422.5", + ); + + // original value remain unchanged + expect(zero.toString()).toEqual("0"); + expect(one.toString()).toEqual("1"); + expect(oneDotFive.toString()).toEqual("1.5"); }); }); diff --git a/packages/math/src/decimal.ts b/packages/math/src/decimal.ts index 969f811c..75ddf103 100644 --- a/packages/math/src/decimal.ts +++ b/packages/math/src/decimal.ts @@ -1,5 +1,7 @@ import BN from "bn.js"; +import { Uint32, Uint53, Uint64 } from "./integers"; + // Too large values lead to massive memory usage. Limit to something sensible. // The largest value we need is 18 (Ether). const maxFractionalDigits = 100; @@ -127,12 +129,10 @@ export class Decimal { /** * a.multiply(b) returns a*b. * - * Both values need to have the same fractional digits. + * We only allow multiplication by unsigned integers to avoid rounding errors. */ - public multiply(b: Decimal): Decimal { - if (this.fractionalDigits !== b.fractionalDigits) throw new Error("Fractional digits do not match"); - const factor = new BN(10).pow(new BN(this.data.fractionalDigits)); - const product = this.data.atomics.mul(new BN(b.atomics)).div(factor); + public multiply(b: Uint32 | Uint53 | Uint64): Decimal { + const product = this.data.atomics.mul(new BN(b.toString())); return new Decimal(product.toString(), this.fractionalDigits); } diff --git a/packages/math/types/decimal.d.ts b/packages/math/types/decimal.d.ts index 102498d4..e2828055 100644 --- a/packages/math/types/decimal.d.ts +++ b/packages/math/types/decimal.d.ts @@ -1,3 +1,4 @@ +import { Uint32, Uint53, Uint64 } from "./integers"; /** * A type for arbitrary precision, non-negative decimals. * @@ -27,9 +28,9 @@ export declare class Decimal { /** * a.multiply(b) returns a*b. * - * Both values need to have the same fractional digits. + * We only allow multiplication by unsigned integers to avoid rounding errors. */ - multiply(b: Decimal): Decimal; + multiply(b: Uint32 | Uint53 | Uint64): Decimal; equals(b: Decimal): boolean; isLessThan(b: Decimal): boolean; isLessThanOrEqual(b: Decimal): boolean;