diff --git a/CHANGELOG.md b/CHANGELOG.md index 70f1f71034..4c85db5e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ and this project adheres to ## [Unreleased] +### Changed + +- @cosmjs/amino: Add IBC denom support to `parseCoins` and use the same + implementation in all those imports: + + ```ts + import { parseCoins } from "@cosmjs/proto-signing"; + // equals + import { parseCoins } from "@cosmjs/stargate"; + // equals + import { parseCoins } from "@cosmjs/amino"; + ``` + ## [0.32.2] - 2023-12-19 ### Fixed diff --git a/packages/amino/src/coins.spec.ts b/packages/amino/src/coins.spec.ts index 4e36dce774..ab363fe431 100644 --- a/packages/amino/src/coins.spec.ts +++ b/packages/amino/src/coins.spec.ts @@ -81,15 +81,41 @@ describe("coins", () => { ]); }); - it("works for two", () => { - expect(parseCoins("819966000ucosm,700000000ustake")).toEqual([ + it("works for various denoms", () => { + // very short (3) + expect(parseCoins("7643bar")).toEqual([ { - amount: "819966000", - denom: "ucosm", + amount: "7643", + denom: "bar", }, + ]); + + // very long (128) + expect( + parseCoins( + "7643abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + ), + ).toEqual([ { - amount: "700000000", - denom: "ustake", + amount: "7643", + denom: + "abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + }, + ]); + + // IBC denom (https://github.com/cosmos/cosmos-sdk/blob/v0.42.7/types/coin_test.go#L512-L519) + expect(parseCoins("7643ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2")).toEqual([ + { + amount: "7643", + denom: "ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", + }, + ]); + + // Token factory denom (https://docs.osmosis.zone/osmosis-core/modules/tokenfactory/) + expect(parseCoins("100000000000factory/osmo1c584m4lq25h83yp6ag8hh4htjr92d954vklzja/ufoo")).toEqual([ + { + amount: "100000000000", + denom: "factory/osmo1c584m4lq25h83yp6ag8hh4htjr92d954vklzja/ufoo", }, ]); }); @@ -144,6 +170,19 @@ describe("coins", () => { ]); }); + 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([ @@ -186,6 +225,20 @@ describe("coins", () => { // amount missing expect(() => parseCoins("ucosm")).toThrowError(/invalid coin string/i); + + // denom starting with slash + expect(() => parseCoins("3456/ibc")).toThrowError(/invalid coin string/i); + + // denom too short + expect(() => parseCoins("3456a")).toThrowError(/invalid coin string/i); + expect(() => parseCoins("3456aa")).toThrowError(/invalid coin string/i); + + // denom too long + expect(() => + parseCoins( + "3456abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgha", + ), + ).toThrowError(/invalid coin string/i); }); }); diff --git a/packages/amino/src/coins.ts b/packages/amino/src/coins.ts index a8faa09832..8f279c07ef 100644 --- a/packages/amino/src/coins.ts +++ b/packages/amino/src/coins.ts @@ -46,13 +46,21 @@ export function coins(amount: number | string, denom: string): Coin[] { /** * Takes a coins list like "819966000ucosm,700000000ustake" and parses it. * - * A Stargate-ready variant of this function is available via: + * Starting with CosmJS 0.32.3, the following imports are all synonym and support + * a variety of denom types such as IBC denoms or tokenfactory. If you need to + * restrict the denom to something very minimal, this needs to be implemented + * separately in the caller. * * ``` * import { parseCoins } from "@cosmjs/proto-signing"; - * // or + * // equals * import { parseCoins } from "@cosmjs/stargate"; + * // equals + * import { parseCoins } from "@cosmjs/amino"; * ``` + * + * This function is not made for supporting decimal amounts and does not support + * parsing gas prices. */ export function parseCoins(input: string): Coin[] { return input @@ -60,7 +68,8 @@ export function parseCoins(input: string): Coin[] { .split(",") .filter(Boolean) .map((part) => { - const match = part.match(/^([0-9]+)([a-zA-Z]+)/); + // Denom regex from Stargate (https://github.com/cosmos/cosmos-sdk/blob/v0.42.7/types/coin.go#L599-L601) + const match = part.match(/^([0-9]+)([a-zA-Z][a-zA-Z0-9/]{2,127})$/); if (!match) throw new Error("Got an invalid coin string"); return { amount: match[1].replace(/^0+/, "") || "0", diff --git a/packages/proto-signing/src/coins.spec.ts b/packages/proto-signing/src/coins.spec.ts deleted file mode 100644 index b8ef7a4193..0000000000 --- a/packages/proto-signing/src/coins.spec.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { parseCoins } from "./coins"; - -describe("coins", () => { - 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 various denoms", () => { - // very short (3) - expect(parseCoins("7643bar")).toEqual([ - { - amount: "7643", - denom: "bar", - }, - ]); - - // very long (128) - expect( - parseCoins( - "7643abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", - ), - ).toEqual([ - { - amount: "7643", - denom: - "abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", - }, - ]); - - // IBC denom (https://github.com/cosmos/cosmos-sdk/blob/v0.42.7/types/coin_test.go#L512-L519) - expect(parseCoins("7643ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2")).toEqual([ - { - amount: "7643", - denom: "ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", - }, - ]); - }); - - it("trims leading zeros", () => { - expect(parseCoins("07643ureef")).toEqual([ - { - amount: "7643", - denom: "ureef", - }, - ]); - expect(parseCoins("007643ureef")).toEqual([ - { - amount: "7643", - denom: "ureef", - }, - ]); - expect(parseCoins("0ureef")).toEqual([ - { - amount: "0", - denom: "ureef", - }, - ]); - expect(parseCoins("0000ureef")).toEqual([ - { - amount: "0", - denom: "ureef", - }, - ]); - }); - - it("works for large numbers", () => { - expect(parseCoins(`${Number.MAX_SAFE_INTEGER}ureef`)).toEqual([ - { - amount: "9007199254740991", - denom: "ureef", - }, - ]); - // 2**64-1 - expect(parseCoins("18446744073709551615ureef")).toEqual([ - { - amount: "18446744073709551615", - denom: "ureef", - }, - ]); - // 2**128-1 - expect(parseCoins("340282366920938463463374607431768211455ureef")).toEqual([ - { - amount: "340282366920938463463374607431768211455", - 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); - - // denom starting with slash - expect(() => parseCoins("3456/ibc")).toThrowError(/invalid coin string/i); - - // denom too short - expect(() => parseCoins("3456a")).toThrowError(/invalid coin string/i); - expect(() => parseCoins("3456aa")).toThrowError(/invalid coin string/i); - - // denom too long - expect(() => - parseCoins( - "3456abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgha", - ), - ).toThrowError(/invalid coin string/i); - }); - }); -}); diff --git a/packages/proto-signing/src/coins.ts b/packages/proto-signing/src/coins.ts deleted file mode 100644 index 0c85c6ac12..0000000000 --- a/packages/proto-signing/src/coins.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Coin } from "@cosmjs/amino"; - -/** - * Takes a coins list like "819966000ucosm,700000000ustake" and parses it. - * - * This is a Stargate ready version of parseCoins from @cosmjs/amino. - * It supports more denoms. - */ -export function parseCoins(input: string): Coin[] { - return input - .replace(/\s/g, "") - .split(",") - .filter(Boolean) - .map((part) => { - // Denom regex from Stargate (https://github.com/cosmos/cosmos-sdk/blob/v0.42.7/types/coin.go#L599-L601) - const match = part.match(/^([0-9]+)([a-zA-Z][a-zA-Z0-9/]{2,127})$/); - if (!match) throw new Error("Got an invalid coin string"); - return { - amount: match[1].replace(/^0+/, "") || "0", - denom: match[2], - }; - }); -} diff --git a/packages/proto-signing/src/index.ts b/packages/proto-signing/src/index.ts index 9c25e61a4d..71c076677d 100644 --- a/packages/proto-signing/src/index.ts +++ b/packages/proto-signing/src/index.ts @@ -1,5 +1,4 @@ // This type happens to be shared between Amino and Direct sign modes -export { parseCoins } from "./coins"; export { DecodedTxRaw, decodeTxRaw } from "./decode"; export { DirectSecp256k1HdWallet, @@ -31,4 +30,6 @@ export { } from "./signer"; export { makeAuthInfoBytes, makeSignBytes, makeSignDoc } from "./signing"; export { executeKdf, KdfConfiguration } from "./wallet"; -export { Coin, coin, coins } from "@cosmjs/amino"; + +// re-exports +export { Coin, coin, coins, parseCoins } from "@cosmjs/amino";