From 0e63ee5cc650e9c1754c1a526a2c96ba3937a701 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 9 Nov 2021 17:51:12 +0100 Subject: [PATCH] Let coin/coins take amount as number | string --- CHANGELOG.md | 6 ++++ packages/amino/src/coins.spec.ts | 50 +++++++++++++++++++++++++++----- packages/amino/src/coins.ts | 30 +++++++++++++++++-- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e7ed0b..021ae856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to ## [Unreleased] +### Added + +- @cosmjs/amino: The `coin` and `coins` helpers now support both `number` and + `string` as input types for the amount. This is useful if your values exceed + the safe integer range. + ## [0.26.4] - 2021-10-28 ### Fixed diff --git a/packages/amino/src/coins.spec.ts b/packages/amino/src/coins.spec.ts index 5f74c4cb..133d14fc 100644 --- a/packages/amino/src/coins.spec.ts +++ b/packages/amino/src/coins.spec.ts @@ -2,7 +2,7 @@ import { coin, coins, parseCoins } from "./coins"; describe("coins", () => { describe("coin", () => { - it("works for basic values", () => { + it("works for number amounts", () => { expect(coin(123, "utoken")).toEqual({ amount: "123", denom: "utoken" }); expect(coin(123.0, "utoken")).toEqual({ amount: "123", denom: "utoken" }); expect(coin(Number.MAX_SAFE_INTEGER, "utoken")).toEqual({ @@ -14,22 +14,56 @@ describe("coins", () => { }); it("throws for non-safe-integer values", () => { - expect(() => coin(1.23, "utoken")).toThrow(); - expect(() => coin(NaN, "utoken")).toThrow(); - expect(() => coin(Number.POSITIVE_INFINITY, "utoken")).toThrow(); - expect(() => coin(Number.MAX_SAFE_INTEGER + 1, "utoken")).toThrow(); + expect(() => coin(1.23, "utoken")).toThrowError(/Given amount is not a safe integer/i); + expect(() => coin(NaN, "utoken")).toThrowError(/Given amount is not a safe integer/i); + expect(() => coin(Number.POSITIVE_INFINITY, "utoken")).toThrowError( + /Given amount is not a safe integer/i, + ); + expect(() => coin(Number.MAX_SAFE_INTEGER + 1, "utoken")).toThrowError( + /Given amount is not a safe integer/i, + ); }); it("throws for negative values", () => { - expect(() => coin(-1, "utoken")).toThrow(); - expect(() => coin(Number.MIN_SAFE_INTEGER, "utoken")).toThrow(); - expect(() => coin(Number.NEGATIVE_INFINITY, "utoken")).toThrow(); + expect(() => coin(-1, "utoken")).toThrowError(/Given amount is not a safe integer/i); + expect(() => coin(Number.MIN_SAFE_INTEGER, "utoken")).toThrowError( + /Given amount is not a safe integer/i, + ); + expect(() => coin(Number.NEGATIVE_INFINITY, "utoken")).toThrowError( + /Given amount is not a safe integer/i, + ); + }); + + it("works for string amounts", () => { + expect(coin("0", "utoken")).toEqual({ amount: "0", denom: "utoken" }); + expect(coin("1", "utoken")).toEqual({ amount: "1", denom: "utoken" }); + expect(coin("00123", "utoken")).toEqual({ amount: "123", denom: "utoken" }); + expect(coin("12300", "utoken")).toEqual({ amount: "12300", denom: "utoken" }); + expect(coin("9007199254740992", "utoken")).toEqual({ amount: "9007199254740992", denom: "utoken" }); + // ETH supply (~118 mio ETH) + expect(coin("118273505060000000000000000", "wei")).toEqual({ + amount: "118273505060000000000000000", + denom: "wei", + }); + }); + + it("throws for invalid amount strings", () => { + expect(() => coin("-1", "utoken")).toThrowError(/Invalid unsigned integer string format/i); + expect(() => coin("0x01", "utoken")).toThrowError(/Invalid unsigned integer string format/i); + expect(() => coin("NaN", "utoken")).toThrowError(/Invalid unsigned integer string format/i); + expect(() => coin("1.0", "utoken")).toThrowError(/Invalid unsigned integer string format/i); + expect(() => coin("1 ", "utoken")).toThrowError(/Invalid unsigned integer string format/i); + expect(() => coin(" 1", "utoken")).toThrowError(/Invalid unsigned integer string format/i); + expect(() => coin("1.1827350506e+26", "utoken")).toThrowError( + /Invalid unsigned integer string format/i, + ); }); }); describe("coins", () => { it("returns one element array of coin", () => { expect(coins(123, "utoken")).toEqual([{ amount: "123", denom: "utoken" }]); + expect(coins("123", "utoken")).toEqual([{ amount: "123", denom: "utoken" }]); }); }); diff --git a/packages/amino/src/coins.ts b/packages/amino/src/coins.ts index 92123cf5..21554904 100644 --- a/packages/amino/src/coins.ts +++ b/packages/amino/src/coins.ts @@ -7,15 +7,39 @@ export interface Coin { /** * Creates a coin. + * + * If your values do not exceed the safe integer range of JS numbers (53 bit), + * you can use the number type here. This is the case for all typical Cosmos SDK + * chains that use the default 6 decimals. + * + * In case you need to supportr larger values, use unsigned integer strings instead. */ -export function coin(amount: number, denom: string): Coin { - return { amount: new Uint53(amount).toString(), denom: denom }; +export function coin(amount: number | string, denom: string): Coin { + let outAmount: string; + if (typeof amount === "number") { + try { + outAmount = new Uint53(amount).toString(); + } catch (_err) { + throw new Error( + "Given amount is not a safe integer. Consider using a string instead to overcome the limitations of JS numbers.", + ); + } + } else { + if (!amount.match(/^[0-9]+$/)) { + throw new Error("Invalid unsigned integer string format"); + } + outAmount = amount.replace(/^0*/, "") || "0"; + } + return { + amount: outAmount, + denom: denom, + }; } /** * Creates a list of coins with one element. */ -export function coins(amount: number, denom: string): Coin[] { +export function coins(amount: number | string, denom: string): Coin[] { return [coin(amount, denom)]; }