From 0e63ee5cc650e9c1754c1a526a2c96ba3937a701 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 9 Nov 2021 17:51:12 +0100 Subject: [PATCH 1/2] 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)]; } From 71fd02bc5f991b3412f9b1a06cd4270d947215e5 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 17 Nov 2021 12:53:21 +0100 Subject: [PATCH 2/2] Update SDKs [skip ci] --- .yarn/sdks/eslint/lib/api.js | 6 ++-- .yarn/sdks/typescript/lib/tsserver.js | 37 +++++++++++++++++--- .yarn/sdks/typescript/lib/tsserverlibrary.js | 37 +++++++++++++++++--- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/.yarn/sdks/eslint/lib/api.js b/.yarn/sdks/eslint/lib/api.js index ac3c9fc0..3048c15e 100644 --- a/.yarn/sdks/eslint/lib/api.js +++ b/.yarn/sdks/eslint/lib/api.js @@ -11,10 +11,10 @@ const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { - // Setup the environment to be able to require eslint/lib/api.js + // Setup the environment to be able to require eslint require(absPnpApiPath).setup(); } } -// Defer to the real eslint/lib/api.js your application uses -module.exports = absRequire(`eslint/lib/api.js`); +// Defer to the real eslint your application uses +module.exports = absRequire(`eslint`); diff --git a/.yarn/sdks/typescript/lib/tsserver.js b/.yarn/sdks/typescript/lib/tsserver.js index 24871f04..c9184189 100644 --- a/.yarn/sdks/typescript/lib/tsserver.js +++ b/.yarn/sdks/typescript/lib/tsserver.js @@ -30,7 +30,7 @@ const moduleWrapper = tsserver => { function toEditorPath(str) { // We add the `zip:` prefix to both `.zip/` paths and virtual paths - if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || isVirtual(str))) { + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { // We also take the opportunity to turn virtual paths into physical ones; // this makes it much easier to work with workspaces that list peer // dependencies, since otherwise Ctrl+Click would bring us to the virtual @@ -60,10 +60,18 @@ const moduleWrapper = tsserver => { // // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 // - case `vscode`: { + // Update Oct 8 2021: VSCode changed their format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { str = `^zip:${str}`; } break; + case `vscode`: { + str = `^/zip/${str}`; + } break; + // To make "go to definition" work, // We have to resolve the actual file system path from virtual path // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) @@ -91,9 +99,25 @@ const moduleWrapper = tsserver => { } function fromEditorPath(str) { - return process.platform === `win32` - ? str.replace(/^\^?zip:\//, ``) - : str.replace(/^\^?zip:/, ``); + switch (hostInfo) { + case `coc-nvim`: + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `vscode`: + default: { + return process.platform === `win32` + ? str.replace(/^\^?(zip:|\/zip)\/+/, ``) + : str.replace(/^\^?(zip:|\/zip)\/+/, `/`); + } break; + } } // Force enable 'allowLocalPluginLoads' @@ -129,6 +153,9 @@ const moduleWrapper = tsserver => { typeof parsedMessage.arguments.hostInfo === `string` ) { hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) { + hostInfo += ` <1.61`; + } } return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => { diff --git a/.yarn/sdks/typescript/lib/tsserverlibrary.js b/.yarn/sdks/typescript/lib/tsserverlibrary.js index 060882ca..8ec31100 100644 --- a/.yarn/sdks/typescript/lib/tsserverlibrary.js +++ b/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -30,7 +30,7 @@ const moduleWrapper = tsserver => { function toEditorPath(str) { // We add the `zip:` prefix to both `.zip/` paths and virtual paths - if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || isVirtual(str))) { + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { // We also take the opportunity to turn virtual paths into physical ones; // this makes it much easier to work with workspaces that list peer // dependencies, since otherwise Ctrl+Click would bring us to the virtual @@ -60,10 +60,18 @@ const moduleWrapper = tsserver => { // // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 // - case `vscode`: { + // Update Oct 8 2021: VSCode changed their format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { str = `^zip:${str}`; } break; + case `vscode`: { + str = `^/zip/${str}`; + } break; + // To make "go to definition" work, // We have to resolve the actual file system path from virtual path // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) @@ -91,9 +99,25 @@ const moduleWrapper = tsserver => { } function fromEditorPath(str) { - return process.platform === `win32` - ? str.replace(/^\^?zip:\//, ``) - : str.replace(/^\^?zip:/, ``); + switch (hostInfo) { + case `coc-nvim`: + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `vscode`: + default: { + return process.platform === `win32` + ? str.replace(/^\^?(zip:|\/zip)\/+/, ``) + : str.replace(/^\^?(zip:|\/zip)\/+/, `/`); + } break; + } } // Force enable 'allowLocalPluginLoads' @@ -129,6 +153,9 @@ const moduleWrapper = tsserver => { typeof parsedMessage.arguments.hostInfo === `string` ) { hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) { + hostInfo += ` <1.61`; + } } return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {