diff --git a/packages/amino/src/coins.spec.ts b/packages/amino/src/coins.spec.ts new file mode 100644 index 00000000..5f74c4cb --- /dev/null +++ b/packages/amino/src/coins.spec.ts @@ -0,0 +1,107 @@ +import { coin, coins, parseCoins } from "./coins"; + +describe("coins", () => { + describe("coin", () => { + it("works for basic values", () => { + 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({ + amount: "9007199254740991", + denom: "utoken", + }); + expect(coin(+0, "utoken")).toEqual({ amount: "0", denom: "utoken" }); + expect(coin(-0, "utoken")).toEqual({ amount: "0", denom: "utoken" }); + }); + + 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(); + }); + + 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(); + }); + }); + + describe("coins", () => { + it("returns one element array of coin", () => { + expect(coins(123, "utoken")).toEqual([{ amount: "123", denom: "utoken" }]); + }); + }); + + describe("parseCoins", () => { + it("works for empty", () => { + expect(parseCoins("")).toEqual([]); + }); + + it("works for one element", () => { + expect(parseCoins("7643ureef")).toEqual([ + { + amount: "7643", + denom: "ureef", + }, + ]); + }); + + it("works for two", () => { + expect(parseCoins("819966000ucosm,700000000ustake")).toEqual([ + { + amount: "819966000", + denom: "ucosm", + }, + { + amount: "700000000", + denom: "ustake", + }, + ]); + }); + + it("ignores empty elements", () => { + // start + expect(parseCoins(",819966000ucosm,700000000ustake")).toEqual([ + { + amount: "819966000", + denom: "ucosm", + }, + { + amount: "700000000", + denom: "ustake", + }, + ]); + // middle + expect(parseCoins("819966000ucosm,,700000000ustake")).toEqual([ + { + amount: "819966000", + denom: "ucosm", + }, + { + amount: "700000000", + denom: "ustake", + }, + ]); + // end + expect(parseCoins("819966000ucosm,700000000ustake,")).toEqual([ + { + amount: "819966000", + denom: "ucosm", + }, + { + amount: "700000000", + denom: "ustake", + }, + ]); + }); + + it("throws for invalid inputs", () => { + // denom missing + expect(() => parseCoins("3456")).toThrowError(/invalid coin string/i); + + // amount missing + expect(() => parseCoins("ucosm")).toThrowError(/invalid coin string/i); + }); + }); +}); diff --git a/packages/amino/src/coins.ts b/packages/amino/src/coins.ts new file mode 100644 index 00000000..1254f27f --- /dev/null +++ b/packages/amino/src/coins.ts @@ -0,0 +1,38 @@ +import { Uint53, Uint64 } from "@cosmjs/math"; + +export interface Coin { + readonly denom: string; + readonly amount: string; +} + +/** + * Creates a coin. + */ +export function coin(amount: number, denom: string): Coin { + return { amount: new Uint53(amount).toString(), denom: denom }; +} + +/** + * Creates a list of coins with one element. + */ +export function coins(amount: number, denom: string): Coin[] { + return [coin(amount, denom)]; +} + +/** + * Takes a coins list like "819966000ucosm,700000000ustake" and parses it. + */ +export function parseCoins(input: string): Coin[] { + return input + .replace(/\s/g, "") + .split(",") + .filter(Boolean) + .map((part) => { + const match = part.match(/^([0-9]+)([a-zA-Z]+)/); + if (!match) throw new Error("Got an invalid coin string"); + return { + amount: Uint64.fromString(match[1]).toString(), + denom: match[2], + }; + }); +} diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index f6f8fc85..21bec81d 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -1,4 +1,5 @@ export { pubkeyToAddress, pubkeyToRawAddress, rawSecp256k1PubkeyToRawAddress } from "./addresses"; +export { Coin, coin, coins, parseCoins } from "./coins"; export { decodeAminoPubkey, decodeBech32Pubkey, @@ -23,6 +24,6 @@ export { makeCosmoshubPath } from "./paths"; export { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; export { Secp256k1Wallet } from "./secp256k1wallet"; export { decodeSignature, encodeSecp256k1Signature, StdSignature } from "./signature"; -export { AminoMsg, Coin, makeSignDoc, serializeSignDoc, StdFee, StdSignDoc } from "./signdoc"; +export { AminoMsg, makeSignDoc, serializeSignDoc, StdFee, StdSignDoc } from "./signdoc"; export { AccountData, Algo, AminoSignResponse, OfflineAminoSigner } from "./signer"; export { executeKdf, KdfConfiguration } from "./wallet"; diff --git a/packages/amino/src/signdoc.ts b/packages/amino/src/signdoc.ts index bfd75dce..54bf86b0 100644 --- a/packages/amino/src/signdoc.ts +++ b/packages/amino/src/signdoc.ts @@ -2,16 +2,13 @@ import { toUtf8 } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; +import { Coin } from "./coins"; + export interface AminoMsg { readonly type: string; readonly value: any; } -export interface Coin { - readonly denom: string; - readonly amount: string; -} - export interface StdFee { readonly amount: readonly Coin[]; readonly gas: string;