From fe5019808617292d12328ee656606b92b866c014 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 26 Apr 2022 17:05:27 +0200 Subject: [PATCH 1/2] Add Decimal.floor/.ceil --- CHANGELOG.md | 4 ++++ packages/math/src/decimal.spec.ts | 36 +++++++++++++++++++++++++++++++ packages/math/src/decimal.ts | 31 ++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857340fe8e..9dc2be2b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to ## [Unreleased] +### Added + +- @cosmjs/math: Add `Decimal.floor` and `Decimal.ceil`. + ## [0.28.4] - 2022-04-15 ### Added diff --git a/packages/math/src/decimal.spec.ts b/packages/math/src/decimal.spec.ts index 1526c9260e..52b876f7d5 100644 --- a/packages/math/src/decimal.spec.ts +++ b/packages/math/src/decimal.spec.ts @@ -177,6 +177,42 @@ describe("Decimal", () => { }); }); + describe("floor", () => { + it("works", () => { + // whole numbers + expect(Decimal.fromUserInput("0", 0).floor().toString()).toEqual("0"); + expect(Decimal.fromUserInput("1", 0).floor().toString()).toEqual("1"); + expect(Decimal.fromUserInput("44", 0).floor().toString()).toEqual("44"); + expect(Decimal.fromUserInput("0", 3).floor().toString()).toEqual("0"); + expect(Decimal.fromUserInput("1", 3).floor().toString()).toEqual("1"); + expect(Decimal.fromUserInput("44", 3).floor().toString()).toEqual("44"); + + // with fractional part + expect(Decimal.fromUserInput("0.001", 3).floor().toString()).toEqual("0"); + expect(Decimal.fromUserInput("1.999", 3).floor().toString()).toEqual("1"); + expect(Decimal.fromUserInput("0.000000000000000001", 18).floor().toString()).toEqual("0"); + expect(Decimal.fromUserInput("1.999999999999999999", 18).floor().toString()).toEqual("1"); + }); + }); + + describe("ceil", () => { + it("works", () => { + // whole numbers + expect(Decimal.fromUserInput("0", 0).ceil().toString()).toEqual("0"); + expect(Decimal.fromUserInput("1", 0).ceil().toString()).toEqual("1"); + expect(Decimal.fromUserInput("44", 0).ceil().toString()).toEqual("44"); + expect(Decimal.fromUserInput("0", 3).ceil().toString()).toEqual("0"); + expect(Decimal.fromUserInput("1", 3).ceil().toString()).toEqual("1"); + expect(Decimal.fromUserInput("44", 3).ceil().toString()).toEqual("44"); + + // with fractional part + expect(Decimal.fromUserInput("0.001", 3).ceil().toString()).toEqual("1"); + expect(Decimal.fromUserInput("1.999", 3).ceil().toString()).toEqual("2"); + expect(Decimal.fromUserInput("0.000000000000000001", 18).ceil().toString()).toEqual("1"); + expect(Decimal.fromUserInput("1.999999999999999999", 18).ceil().toString()).toEqual("2"); + }); + }); + describe("toString", () => { it("displays no decimal point for full numbers", () => { expect(Decimal.fromUserInput("44", 0).toString()).toEqual("44"); diff --git a/packages/math/src/decimal.ts b/packages/math/src/decimal.ts index 6bb8a1f69a..91ec2cc181 100644 --- a/packages/math/src/decimal.ts +++ b/packages/math/src/decimal.ts @@ -113,6 +113,37 @@ export class Decimal { }; } + /** Creates a new instance with the same value */ + private clone(): Decimal { + return new Decimal(this.atomics, this.fractionalDigits); + } + + /** Returns the greatest decimal <= this which has no fractional part (rounding down) */ + public floor(): Decimal { + const factor = new BN(10).pow(new BN(this.data.fractionalDigits)); + const whole = this.data.atomics.div(factor); + const fractional = this.data.atomics.mod(factor); + + if (fractional.isZero()) { + return this.clone(); + } else { + return Decimal.fromAtomics(whole.mul(factor).toString(), this.fractionalDigits); + } + } + + /** Returns the smallest decimal >= this which has no fractional part (rounding up) */ + public ceil(): Decimal { + const factor = new BN(10).pow(new BN(this.data.fractionalDigits)); + const whole = this.data.atomics.div(factor); + const fractional = this.data.atomics.mod(factor); + + if (fractional.isZero()) { + return this.clone(); + } else { + return Decimal.fromAtomics(whole.addn(1).mul(factor).toString(), this.fractionalDigits); + } + } + public toString(): string { const factor = new BN(10).pow(new BN(this.data.fractionalDigits)); const whole = this.data.atomics.div(factor); From 15bf277754ef4dbf07d8efcf538db88d73f65637 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 26 Apr 2022 17:12:38 +0200 Subject: [PATCH 2/2] Let calculateFee handle fee amounts that exceed the safe integer range --- CHANGELOG.md | 5 +++++ packages/stargate/src/fee.spec.ts | 11 +++++++++++ packages/stargate/src/fee.ts | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc2be2b4a..33078bbbd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to - @cosmjs/math: Add `Decimal.floor` and `Decimal.ceil`. +### Changed + +- @cosmjs/stargate: Let `calculateFee` handle fee amounts that exceed the safe + integer range. + ## [0.28.4] - 2022-04-15 ### Added diff --git a/packages/stargate/src/fee.spec.ts b/packages/stargate/src/fee.spec.ts index 03a9fca743..38736f1770 100644 --- a/packages/stargate/src/fee.spec.ts +++ b/packages/stargate/src/fee.spec.ts @@ -93,4 +93,15 @@ describe("calculateFee", () => { gas: "80000", }); }); + + it("works with large gas price", () => { + // "The default gas price is 5000000000000 (5e^12), as the native coin has 18 decimals it is exceeding the max safe integer" + // https://github.com/cosmos/cosmjs/issues/1134 + const gasPrice = GasPrice.fromString("5000000000000tiny"); + const fee = calculateFee(500_000, gasPrice); + expect(fee).toEqual({ + amount: [{ amount: "2500000000000000000", denom: "tiny" }], + gas: "500000", + }); + }); }); diff --git a/packages/stargate/src/fee.ts b/packages/stargate/src/fee.ts index 55fb70a933..3f210e9f56 100644 --- a/packages/stargate/src/fee.ts +++ b/packages/stargate/src/fee.ts @@ -60,7 +60,9 @@ export class GasPrice { export function calculateFee(gasLimit: number, gasPrice: GasPrice | string): StdFee { const processedGasPrice = typeof gasPrice === "string" ? GasPrice.fromString(gasPrice) : gasPrice; const { denom, amount: gasPriceAmount } = processedGasPrice; - const amount = Math.ceil(gasPriceAmount.multiply(new Uint53(gasLimit)).toFloatApproximation()); + // Note: Amount can exceed the safe integer range (https://github.com/cosmos/cosmjs/issues/1134), + // which we handle by converting from Decimal to string without going through number. + const amount = gasPriceAmount.multiply(new Uint53(gasLimit)).ceil().toString(); return { amount: coins(amount, denom), gas: gasLimit.toString(),