From a9ef2a049afc203280bc933d119fdc66b38e5b6d Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Fri, 20 Jan 2023 15:16:56 -0600 Subject: [PATCH] add numeric module (#17324) --- jest.config.js | 4 +- lavamoat/browserify/beta/policy.json | 47 +- lavamoat/browserify/flask/policy.json | 39 +- lavamoat/browserify/main/policy.json | 47 +- lavamoat/build-system/policy.json | 2 - package.json | 1 + shared/constants/common.ts | 5 + shared/modules/Numeric.test.ts | 541 ++++++++++++++++++++++ shared/modules/Numeric.ts | 620 ++++++++++++++++++++++++++ yarn.lock | 21 +- 10 files changed, 1266 insertions(+), 61 deletions(-) create mode 100644 shared/constants/common.ts create mode 100644 shared/modules/Numeric.test.ts create mode 100644 shared/modules/Numeric.ts diff --git a/jest.config.js b/jest.config.js index 1df970cb5..6fe44e336 100644 --- a/jest.config.js +++ b/jest.config.js @@ -42,8 +42,8 @@ module.exports = { '/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js', '/app/scripts/migrations/*.test.js', '/app/scripts/platforms/*.test.js', - '/shared/**/*.test.js', - '/ui/**/*.test.js', + '/shared/**/*.test.(js|ts)', + '/ui/**/*.test.(js|ts)', ], testTimeout: 2500, // We have to specify the environment we are running in, which is jsdom. The diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 122ac9e9a..46eca0fc7 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -602,7 +602,7 @@ }, "packages": { "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "node-fetch": true @@ -954,7 +954,7 @@ "packages": { "@metamask/rpc-methods>@metamask/key-tree": true, "@metamask/snaps-utils>@noble/hashes": true, - "eth-block-tracker>@metamask/utils": true + "@metamask/utils": true } }, "@metamask/rpc-methods>@metamask/browser-passworder": { @@ -978,7 +978,7 @@ "@metamask/rpc-methods>@metamask/key-tree>@scure/bip39": true, "@metamask/snaps-utils>@noble/hashes": true, "@metamask/snaps-utils>@scure/base": true, - "eth-block-tracker>@metamask/utils": true + "@metamask/utils": true } }, "@metamask/rpc-methods>@metamask/key-tree>@noble/ed25519": { @@ -1062,6 +1062,18 @@ "@metamask/base-controller": true } }, + "@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@apocentre/alias-sampling": true, @@ -2124,23 +2136,12 @@ "setTimeout": true }, "packages": { - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-block-tracker>pify": true, "eth-query>json-rpc-random-id": true, "json-rpc-engine>@metamask/safe-event-emitter": true } }, - "eth-block-tracker>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/snaps-ui>superstruct": true, - "browserify>buffer": true, - "nock>debug": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" @@ -2193,8 +2194,8 @@ "setTimeout": true }, "packages": { + "@metamask/utils": true, "browserify>browser-resolve": true, - "eth-block-tracker>@metamask/utils": true, "eth-json-rpc-middleware>@metamask/eth-sig-util": true, "eth-json-rpc-middleware>pify": true, "eth-rpc-errors": true, @@ -4104,6 +4105,20 @@ "webpack>events": true } }, + "semver": { + "globals": { + "console.error": true + }, + "packages": { + "browserify>process": true, + "semver>lru-cache": true + } + }, + "semver>lru-cache": { + "packages": { + "semver>lru-cache>yallist": true + } + }, "sinon>nise>path-to-regexp": { "packages": { "sinon>nise>path-to-regexp>isarray": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 57dbbdcf4..2040dc784 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -602,7 +602,7 @@ }, "packages": { "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "node-fetch": true @@ -1048,7 +1048,7 @@ "@metamask/snaps-ui>superstruct": true, "@metamask/snaps-utils": true, "@metamask/snaps-utils>@noble/hashes": true, - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-rpc-errors": true } }, @@ -1073,7 +1073,7 @@ "@metamask/rpc-methods>@metamask/key-tree>@scure/bip39": true, "@metamask/snaps-utils>@noble/hashes": true, "@metamask/snaps-utils>@scure/base": true, - "eth-block-tracker>@metamask/utils": true + "@metamask/utils": true } }, "@metamask/rpc-methods>@metamask/key-tree>@noble/ed25519": { @@ -1164,7 +1164,7 @@ "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, "@metamask/subject-metadata-controller": true, - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "pump": true @@ -1329,7 +1329,7 @@ "@metamask/snaps-ui": { "packages": { "@metamask/snaps-ui>superstruct": true, - "eth-block-tracker>@metamask/utils": true + "@metamask/utils": true } }, "@metamask/snaps-utils": { @@ -1344,7 +1344,7 @@ "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>rfdc": true, "@metamask/snaps-utils>validate-npm-package-name": true, - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "semver": true } }, @@ -1387,6 +1387,18 @@ "@metamask/base-controller": true } }, + "@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@apocentre/alias-sampling": true, @@ -2449,23 +2461,12 @@ "setTimeout": true }, "packages": { - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-block-tracker>pify": true, "eth-query>json-rpc-random-id": true, "json-rpc-engine>@metamask/safe-event-emitter": true } }, - "eth-block-tracker>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/snaps-ui>superstruct": true, - "browserify>buffer": true, - "nock>debug": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" @@ -2518,8 +2519,8 @@ "setTimeout": true }, "packages": { + "@metamask/utils": true, "browserify>browser-resolve": true, - "eth-block-tracker>@metamask/utils": true, "eth-json-rpc-middleware>@metamask/eth-sig-util": true, "eth-json-rpc-middleware>pify": true, "eth-rpc-errors": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 122ac9e9a..46eca0fc7 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -602,7 +602,7 @@ }, "packages": { "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "node-fetch": true @@ -954,7 +954,7 @@ "packages": { "@metamask/rpc-methods>@metamask/key-tree": true, "@metamask/snaps-utils>@noble/hashes": true, - "eth-block-tracker>@metamask/utils": true + "@metamask/utils": true } }, "@metamask/rpc-methods>@metamask/browser-passworder": { @@ -978,7 +978,7 @@ "@metamask/rpc-methods>@metamask/key-tree>@scure/bip39": true, "@metamask/snaps-utils>@noble/hashes": true, "@metamask/snaps-utils>@scure/base": true, - "eth-block-tracker>@metamask/utils": true + "@metamask/utils": true } }, "@metamask/rpc-methods>@metamask/key-tree>@noble/ed25519": { @@ -1062,6 +1062,18 @@ "@metamask/base-controller": true } }, + "@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@apocentre/alias-sampling": true, @@ -2124,23 +2136,12 @@ "setTimeout": true }, "packages": { - "eth-block-tracker>@metamask/utils": true, + "@metamask/utils": true, "eth-block-tracker>pify": true, "eth-query>json-rpc-random-id": true, "json-rpc-engine>@metamask/safe-event-emitter": true } }, - "eth-block-tracker>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/snaps-ui>superstruct": true, - "browserify>buffer": true, - "nock>debug": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" @@ -2193,8 +2194,8 @@ "setTimeout": true }, "packages": { + "@metamask/utils": true, "browserify>browser-resolve": true, - "eth-block-tracker>@metamask/utils": true, "eth-json-rpc-middleware>@metamask/eth-sig-util": true, "eth-json-rpc-middleware>pify": true, "eth-rpc-errors": true, @@ -4104,6 +4105,20 @@ "webpack>events": true } }, + "semver": { + "globals": { + "console.error": true + }, + "packages": { + "browserify>process": true, + "semver>lru-cache": true + } + }, + "semver>lru-cache": { + "packages": { + "semver>lru-cache>yallist": true + } + }, "sinon>nise>path-to-regexp": { "packages": { "sinon>nise>path-to-regexp>isarray": true diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 888c48556..dc50f82f7 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1910,7 +1910,6 @@ }, "packages": { "chokidar>braces": true, - "chokidar>fsevents": true, "chokidar>glob-parent": true, "chokidar>is-binary-path": true, "chokidar>normalize-path": true, @@ -5220,7 +5219,6 @@ "gulp-watch>path-is-absolute": true, "gulp>glob-watcher>anymatch": true, "gulp>glob-watcher>chokidar>braces": true, - "gulp>glob-watcher>chokidar>fsevents": true, "gulp>glob-watcher>chokidar>glob-parent": true, "gulp>glob-watcher>chokidar>is-binary-path": true, "gulp>glob-watcher>chokidar>readdirp": true, diff --git a/package.json b/package.json index 07580fe3f..1a1b60452 100644 --- a/package.json +++ b/package.json @@ -241,6 +241,7 @@ "@metamask/snaps-ui": "^0.27.1", "@metamask/snaps-utils": "^0.27.1", "@metamask/subject-metadata-controller": "^1.0.0", + "@metamask/utils": "^3.4.1", "@ngraveio/bc-ur": "^1.1.6", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.6.2", diff --git a/shared/constants/common.ts b/shared/constants/common.ts new file mode 100644 index 000000000..f45ec8abd --- /dev/null +++ b/shared/constants/common.ts @@ -0,0 +1,5 @@ +export enum EtherDenomination { + ETH = 'ETH', + GWEI = 'GWEI', + WEI = 'WEI', +} diff --git a/shared/modules/Numeric.test.ts b/shared/modules/Numeric.test.ts new file mode 100644 index 000000000..528870bf4 --- /dev/null +++ b/shared/modules/Numeric.test.ts @@ -0,0 +1,541 @@ +import { BigNumber } from 'bignumber.js'; +import { BN } from 'bn.js'; +import { EtherDenomination } from '../constants/common'; +import { Numeric } from './Numeric'; + +const ONE_ETH = new Numeric(1, 10, EtherDenomination.ETH); +const ONE_GWEI = new Numeric(1, 10, EtherDenomination.GWEI); +const ONE_WEI = new Numeric(1, 10, EtherDenomination.WEI); + +describe('Numeric', () => { + describe('Basic Numeric Construction', () => { + describe('From hexadeciaml strings', () => { + it('Should create a new Numeric from a hexadecimal string', () => { + const numeric = new Numeric('0xa', 16); + expect(numeric.value).toEqual(new BigNumber(10, 10)); + }); + + it('Should create a new Numeric from a hexadecimal string with a decimal', () => { + const numeric = new Numeric('0xa.7', 16); + expect(numeric.value).toEqual(new BigNumber(10.4375, 10)); + }); + + it('Should create a new Numeric from a hexadecimal string with negation', () => { + const numeric = new Numeric('-0xa', 16); + expect(numeric.value).toEqual(new BigNumber(-10, 10)); + }); + + it('Should create a new Numeric from a hexadecimal string with negation and decimal', () => { + const numeric = new Numeric('-0xa.7', 16); + expect(numeric.value).toEqual(new BigNumber(-10.4375, 10)); + }); + }); + + describe('From decimal strings', () => { + it('Should create a new Numeric from a decimal string', () => { + const numeric = new Numeric('10', 10); + expect(numeric.value).toEqual(new BigNumber(10, 10)); + }); + + it('Should create a new Numeric from a decimal string with a decimal', () => { + const numeric = new Numeric('10.4375', 10); + expect(numeric.value).toEqual(new BigNumber(10.4375, 10)); + }); + + it('Should create a new Numeric from a decimal string with negation', () => { + const numeric = new Numeric('-10', 10); + expect(numeric.value).toEqual(new BigNumber(-10, 10)); + }); + + it('Should create a new Numeric from a decimal string with negation and decimal', () => { + const numeric = new Numeric('-10.4375', 10); + expect(numeric.value).toEqual(new BigNumber(-10.4375, 10)); + }); + }); + + describe('From decimal numbers', () => { + it('Should create a new Numeric from a hexadecimal number', () => { + const numeric = new Numeric(10, 10); + expect(numeric.value).toEqual(new BigNumber(10, 10)); + }); + + it('Should create a new Numeric from a hexadecimal string with a decimal', () => { + const numeric = new Numeric(10.4375, 10); + expect(numeric.value).toEqual(new BigNumber(10.4375, 10)); + }); + + it('Should create a new Numeric from a hexadecimal string with negation', () => { + const numeric = new Numeric(-10, 10); + expect(numeric.value).toEqual(new BigNumber(-10, 10)); + }); + + it('Should create a new Numeric from a hexadecimal string with negation and decimal', () => { + const numeric = new Numeric(-10.4375, 16); + expect(numeric.value).toEqual(new BigNumber(-10.4375, 10)); + }); + }); + + describe('From BigNumbers or BN', () => { + it('Should create a new Numeric from a BigNumber', () => { + const numeric = new Numeric(new BigNumber(100, 10)); + expect(numeric.value).toEqual(new BigNumber(100, 10)); + }); + + it('Should create a new Numeric from a BN', () => { + const numeric = new Numeric(new BN(100, 10), 10); + expect(numeric.value).toEqual(new BigNumber(100, 10)); + }); + }); + }); + + describe('Error checking', () => { + it('Should throw an error for a non numeric string', () => { + expect(() => new Numeric('Hello there', 10)).toThrow( + 'String provided to stringToBigNumber is not a hexadecimal or decimal string: Hello there, 10', + ); + }); + + it('Should throw an error for a numeric string without a base', () => { + expect(() => new Numeric('10')).toThrow( + 'You must specify the base of the provided number if the value is not already a BigNumber', + ); + }); + + it('Should throw an error for a non numeric type', () => { + expect(() => new Numeric(true as unknown as number, 10)).toThrow( + 'Value: true is not a string, number, BigNumber or BN. Type is: boolean.', + ); + }); + }); + + describe('Erroneous behaviors that we are temporarily continuing', () => { + it('Handles values that are undefined, setting the value to 0', () => { + expect(new Numeric(undefined as unknown as number).toString()).toEqual( + '0', + ); + }); + + it('Handles values that are NaN, setting the value to 0', () => { + expect(new Numeric(NaN).toString()).toEqual('0'); + }); + }); + + describe('Ether denomination conversion', () => { + it('should convert 1 ETH to 1000000000 GWEI', () => { + expect(ONE_ETH.toDenomination(EtherDenomination.GWEI).toString()).toEqual( + '1000000000', + ); + }); + + it('should convert 1 ETH to 1000000000000000000 WEI', () => { + expect(ONE_ETH.toDenomination(EtherDenomination.WEI).toString()).toEqual( + '1000000000000000000', + ); + }); + + it('should convert 1 GWEI to 0.000000001 ETH', () => { + expect(ONE_GWEI.toDenomination(EtherDenomination.ETH).toString()).toEqual( + '0.000000001', + ); + }); + + it('should convert 1 GWEI to 1000000000 WEI', () => { + expect(ONE_GWEI.toDenomination(EtherDenomination.WEI).toString()).toEqual( + '1000000000', + ); + }); + + it('should convert 1 WEI to 0 ETH due to rounding', () => { + expect(ONE_WEI.toDenomination(EtherDenomination.ETH).toString()).toEqual( + '0', + ); + }); + + it('should convert 1 WEI to 0.000000001 GWEI', () => { + expect(ONE_WEI.toDenomination(EtherDenomination.GWEI).toString()).toEqual( + '0.000000001', + ); + }); + }); + + describe('Math operations', () => { + describe('Multiplication', () => { + it('Should compute correct results for simple multiplication', () => { + expect(new Numeric(5, 10).times(5, 10).toNumber()).toEqual(25); + + expect( + new Numeric(5, 10).times(new Numeric(10, 10)).toNumber(), + ).toEqual(50); + + expect( + new Numeric(25, 10).times(new Numeric(10, 10)).toNumber(), + ).toEqual(250); + }); + + it('Should compute correct results for multiplication of big numbers', () => { + expect( + new Numeric('175671432', 10).times('686216', 10).toString(), + ).toEqual('120548547381312'); + + expect( + new Numeric('1756714320', 10) + .times(new Numeric('686216', 10)) + .toString(), + ).toEqual('1205485473813120'); + + expect( + new Numeric('41756714320', 10) + .times(new Numeric('6862160', 10)) + .toString(), + ).toEqual('286541254738131200'); + }); + + it('Should compute correct results for multiplication of negative big numbers', () => { + expect( + new Numeric('175671432', 10).times('-686216', 10).toString(), + ).toEqual('-120548547381312'); + + expect( + new Numeric('1756714320', 10) + .times(new Numeric('-686216', 10)) + .toString(), + ).toEqual('-1205485473813120'); + + expect( + new Numeric('-41756714320', 10) + .times(new Numeric('-6862160', 10)) + .toString(), + ).toEqual('286541254738131200'); + }); + }); + + describe('Division', () => { + it('Should compute correct results for simple division', () => { + expect(new Numeric(25, 10).divide(5, 10).toNumber()).toEqual(5); + + expect( + new Numeric(50, 10).divide(new Numeric(10, 10)).toNumber(), + ).toEqual(5); + + expect( + new Numeric(250, 10).divide(new Numeric(10, 10)).toNumber(), + ).toEqual(25); + }); + + it('Should compute correct results for division of big numbers', () => { + expect( + new Numeric('175671432', 10).divide('686216', 10).toString(), + ).toEqual('256.00019818832554181191'); + + expect( + new Numeric('1756714320', 10) + .divide(new Numeric('686216', 10)) + .toString(), + ).toEqual('2560.00198188325541811908'); + + expect( + new Numeric('41756714320', 10) + .divide(new Numeric('6862160', 10)) + .toString(), + ).toEqual('6085.06859647691106007438'); + }); + + it('Should compute correct results for division of negative big numbers', () => { + expect( + new Numeric('175671432', 10).divide('-686216', 10).toString(), + ).toEqual('-256.00019818832554181191'); + + expect( + new Numeric('1756714320', 10) + .divide(new Numeric('-686216', 10)) + .toString(), + ).toEqual('-2560.00198188325541811908'); + + expect( + new Numeric('-41756714320', 10) + .divide(new Numeric('-6862160', 10)) + .toString(), + ).toEqual('6085.06859647691106007438'); + }); + }); + + describe('Addition', () => { + it('Should compute correct results for simple addition', () => { + expect(new Numeric(25, 10).add(5, 10).toNumber()).toEqual(30); + + expect(new Numeric(50, 10).add(new Numeric(10, 10)).toNumber()).toEqual( + 60, + ); + + expect( + new Numeric(250, 10).add(new Numeric(100, 10)).toNumber(), + ).toEqual(350); + }); + + it('Should compute correct results for addition of big numbers', () => { + expect( + new Numeric('175671432', 10).add('686216', 10).toString(), + ).toEqual('176357648'); + + expect( + new Numeric('1756714320', 10) + .add(new Numeric('686216', 10)) + .toString(), + ).toEqual('1757400536'); + + expect( + new Numeric('41756714320', 10) + .add(new Numeric('6862160', 10)) + .toString(), + ).toEqual('41763576480'); + }); + + it('Should compute correct results for addition of negative big numbers', () => { + expect( + new Numeric('175671432', 10).add('-686216', 10).toString(), + ).toEqual('174985216'); + + expect( + new Numeric('1756714320', 10) + .add(new Numeric('-686216', 10)) + .toString(), + ).toEqual('1756028104'); + + expect( + new Numeric('-41756714320', 10) + .add(new Numeric('-6862160', 10)) + .toString(), + ).toEqual('-41763576480'); + }); + }); + + describe('Subtraction', () => { + it('Should compute correct results for simple subtraction', () => { + expect(new Numeric(25, 10).minus(5, 10).toNumber()).toEqual(20); + + expect( + new Numeric(50, 10).minus(new Numeric(10, 10)).toNumber(), + ).toEqual(40); + + expect( + new Numeric(250, 10).minus(new Numeric(100, 10)).toNumber(), + ).toEqual(150); + }); + + it('Should compute correct results for subtraction of big numbers', () => { + expect( + new Numeric('175671432', 10).minus('686216', 10).toString(), + ).toEqual('174985216'); + + expect( + new Numeric('1756714320', 10) + .minus(new Numeric('686216', 10)) + .toString(), + ).toEqual('1756028104'); + + expect( + new Numeric('41756714320', 10) + .minus(new Numeric('6862160', 10)) + .toString(), + ).toEqual('41749852160'); + }); + + it('Should compute correct results for subtraction of negative big numbers', () => { + expect( + new Numeric('175671432', 10).minus('-686216', 10).toString(), + ).toEqual('176357648'); + + expect( + new Numeric('1756714320', 10) + .minus(new Numeric('-686216', 10)) + .toString(), + ).toEqual('1757400536'); + + expect( + new Numeric('-41756714320', 10) + .minus(new Numeric('-6862160', 10)) + .toString(), + ).toEqual('-41749852160'); + }); + }); + + describe('applyConversionRate', () => { + it('Should multiply the value by the conversionRate supplied', () => { + expect( + new Numeric(10, 10).applyConversionRate(468.5).toString(), + ).toEqual('4685'); + }); + + it('Should multiply the value by the conversionRate supplied when conversionRate is a BigNumber', () => { + expect( + new Numeric(10, 10) + .applyConversionRate(new BigNumber(468.5, 10)) + .toString(), + ).toEqual('4685'); + }); + + it('Should multiply the value by the inverse of conversionRate supplied when second parameter is true', () => { + expect( + new Numeric(10, 10).applyConversionRate(468.5, true).toString(), + ).toEqual('0.0213447171824973319'); + }); + + it('Should multiply the value by the inverse of the BigNumber conversionRate supplied when second parameter is true', () => { + expect( + new Numeric(10, 10) + .applyConversionRate(new BigNumber(468.5, 10), true) + .toString(), + ).toEqual('0.0213447171824973319'); + }); + }); + }); + + describe('Base conversion', () => { + it('should convert a hexadecimal string to a decimal string', () => { + expect(new Numeric('0x5208', 16).toBase(10).toString()).toEqual('21000'); + }); + + it('should convert a decimal string to a hexadecimal string', () => { + expect(new Numeric('21000', 10).toBase(16).toString()).toEqual('5208'); + }); + + it('should convert a decimal string to a 0x prefixed hexadecimal string', () => { + expect(new Numeric('21000', 10).toPrefixedHexString()).toEqual('0x5208'); + }); + + it('should convert a decimal number to a hexadecimal string', () => { + expect(new Numeric(21000, 10).toBase(16).toString()).toEqual('5208'); + }); + + it('should convert a decimal number to a 0x prefixed hexadecimal string', () => { + expect(new Numeric(21000, 10).toPrefixedHexString()).toEqual('0x5208'); + }); + }); + + describe('Comparisons', () => { + it('Should correctly identify that 0xa is greater than 0x9', () => { + expect(new Numeric('0xa', 16).greaterThan('0x9', 16)).toEqual(true); + }); + it('Should correctly identify that 0x9 is less than 0xa', () => { + expect(new Numeric('0x9', 16).lessThan('0xa', 16)).toEqual(true); + }); + it('Should correctly identify that 0xa is greater than or equal to 0xa', () => { + expect(new Numeric('0xa', 16).greaterThanOrEqualTo('0xa', 16)).toEqual( + true, + ); + }); + it('Should correctly identify that 0xa is less than or equal to 0xa', () => { + expect(new Numeric('0xa', 16).lessThanOrEqualTo('0xa', 16)).toEqual(true); + }); + + it('Should correctly identify that 0xa is greater than 9', () => { + expect(new Numeric('0xa', 16).greaterThan(9, 10)).toEqual(true); + }); + it('Should correctly identify that 0x9 is less than 10', () => { + expect(new Numeric('0x9', 16).lessThan(10, 10)).toEqual(true); + }); + it('Should correctly identify that 10 is greater than or equal to 0xa', () => { + expect(new Numeric(10, 10).greaterThanOrEqualTo('0xa', 16)).toEqual(true); + }); + it('Should correctly identify that 10 is less than or equal to 0xa', () => { + expect(new Numeric(10, 10).lessThanOrEqualTo('0xa', 16)).toEqual(true); + }); + }); + + describe('Positive and Negative determination', () => { + it('Should correctly identify a negative number with isNegative', () => { + expect(new Numeric(-10, 10).isNegative()).toEqual(true); + expect(new Numeric('-10', 10).isNegative()).toEqual(true); + expect(new Numeric('-0xa', 16).isNegative()).toEqual(true); + }); + it('Should return false for isNegative when number is positive', () => { + expect(new Numeric(10, 10).isNegative()).toEqual(false); + expect(new Numeric('10', 10).isNegative()).toEqual(false); + expect(new Numeric('0xa', 16).isNegative()).toEqual(false); + }); + it('Should correctly identify a positive number with isPositive', () => { + expect(new Numeric(10, 10).isPositive()).toEqual(true); + expect(new Numeric('10', 10).isPositive()).toEqual(true); + expect(new Numeric('0xa', 16).isPositive()).toEqual(true); + }); + it('Should return false for isPositive when number is negative', () => { + expect(new Numeric(-10, 10).isPositive()).toEqual(false); + expect(new Numeric('-10', 10).isPositive()).toEqual(false); + expect(new Numeric('-0xa', 16).isPositive()).toEqual(false); + }); + }); + + describe('Terminating functions, return values', () => { + describe('toString', () => { + it('Should return a string representation of provided hex', () => { + expect(new Numeric('0xa', 16).toString()).toEqual('a'); + }); + + it('Should return a string representation of provided decimal string', () => { + expect(new Numeric('10', 10).toString()).toEqual('10'); + }); + + it('Should return a string representation of provided number', () => { + expect(new Numeric(10, 10).toString()).toEqual('10'); + }); + + it('Should return a string representation of provided float', () => { + expect(new Numeric(10.5, 10).toString()).toEqual('10.5'); + }); + + it('Should return a string representation of provided BigNumber', () => { + expect(new Numeric(new BigNumber(10, 10)).toString()).toEqual('10'); + }); + + it('Should return a string representation of provided BN', () => { + expect(new Numeric(new BN(10, 10)).toString()).toEqual('10'); + }); + }); + + describe('toNumber', () => { + it('Should return a number representing provided hex', () => { + expect(new Numeric('0xa', 16).toNumber()).toEqual(10); + }); + + it('Should return a number representation of provided decimal string', () => { + expect(new Numeric('10', 10).toNumber()).toEqual(10); + }); + + it('Should return a number representation of provided number', () => { + expect(new Numeric(10, 10).toNumber()).toEqual(10); + }); + + it('Should return a number representation of provided float', () => { + expect(new Numeric(10.5, 10).toNumber()).toEqual(10.5); + }); + + it('Should return a number representation of provided BigNumber', () => { + expect(new Numeric(new BigNumber(10, 10)).toNumber()).toEqual(10); + }); + + it('Should return a number representation of provided BN', () => { + expect(new Numeric(new BN(10, 10)).toNumber()).toEqual(10); + }); + }); + + describe('toFixed', () => { + it('Should return a string representing provided hex to 2 decimal places', () => { + expect(new Numeric('0xa.7', 16).toFixed(2)).toEqual('10.44'); + }); + + it('Should return a string representation of provided decimal string to 2 decimal places', () => { + expect(new Numeric('10.4375', 10).toFixed(2)).toEqual('10.44'); + }); + + it('Should return a string representation of provided float to 2 decimal places', () => { + expect(new Numeric(10.4375, 10).toFixed(2)).toEqual('10.44'); + }); + + it('Should return a number representation of provided BigNumber to 2 decimal places', () => { + expect(new Numeric(new BigNumber(10.4375, 10)).toFixed(2)).toEqual( + '10.44', + ); + }); + }); + }); +}); diff --git a/shared/modules/Numeric.ts b/shared/modules/Numeric.ts new file mode 100644 index 000000000..b8149f601 --- /dev/null +++ b/shared/modules/Numeric.ts @@ -0,0 +1,620 @@ +import { BigNumber } from 'bignumber.js'; +import BN from 'bn.js'; +import { isHexString, isNullOrUndefined } from '@metamask/utils'; +import { addHexPrefix } from 'ethereumjs-util'; +import { EtherDenomination } from '../constants/common'; +import { stripHexPrefix } from './hexstring-utils'; + +export type NumericValue = string | number | BN | BigNumber; +export type NumericBase = 10 | 16; + +/** + * All variations of isHexString from our own utilities and etherumjs-utils + * return false for a '-' prefixed hex string. This utility method strips the + * possible '-' from the string before testing its validity so that negative + * hex values can be properly handled. + * + * @param value - The string to check + * @returns true if the value is a hex string (negative or otherwise) + */ +function isHexStringOrNegatedHexString(value: string): value is string { + return isHexString(value.replace('-', '')) || isHexString(value); +} + +/** + * BigNumber supports hex strings with '.' (aka decimals) in the string. + * No version of isHexString returs true if the string contains a decimal so + * this method is used to check if both parts of the string split by the + * decimal are hex strings. If so we can feed this value into BigNumber to get + * a valid Numeric. + * + * @param value - The string to check + * @returns true if the string is a hexadecimal split by '.' + */ +function isDecimalHex(value: string): boolean { + const parts = value.split('.'); + if (parts.length === 1) { + return false; + } + return parts.every((part) => isHexStringOrNegatedHexString(part)); +} + +/** + * Converts a hexadecimal in string or number format to a BigNumber. + * Note that in many places in our codebase we call 'addHexPrefix' on a negated + * hexadecimal string resulting in '0x-a' which will fail checks for + * isHexString. Sometimes we DO not add the 0x so we have to check for '-a' + * as well. + * + * @param value - hexadecimal value in string or number format. + * @returns A BigNumber representation of the value + */ +function hexadecimalToBigNumber(value: string | number): BigNumber { + const stringified = typeof value === 'number' ? `${value}` : value; + const isNegative = stripHexPrefix(stringified)[0] === '-'; + const valueWithoutNegation = stringified.replace('-', ''); + + const valueAsBigNumber = new BigNumber( + stripHexPrefix(valueWithoutNegation), + 16, + ); + + return isNegative ? valueAsBigNumber.negated() : valueAsBigNumber; +} + +/** + * Converts a decimal in string or number format to a BigNumber. + * + * @param value - decimal value in string or number format. + * @returns A BigNumber representation of the value + */ +function decimalToBigNumber(value: string | number) { + return new BigNumber(String(value), 10); +} + +/** + * This method is used to safely convert a string type value to a BigNumber. + * The only valid strings for this method are those that are either hexadecimal + * numeric values OR numeric strings that can be converted to BigNumbers. It is + * impossible to tell the difference between a hex value of 100000 vs a decimal + * value of 100000 so a second parameter indicating the numeric base of the + * string value must be provided. + * + * @param value - A hexadecimal or decimal string + * @param numericBase - Either 16 for a hexadeciaml or 10 for a decimal + * @returns A BigNumber representation of the value + */ +function stringToBigNumber(value: string, numericBase: NumericBase) { + if (typeof value !== 'string') { + throw new Error( + `Value of type ${typeof value} passed to stringToBigNumber`, + ); + } + if ( + numericBase === 16 && + (isHexStringOrNegatedHexString(value) || isDecimalHex(value)) + ) { + return hexadecimalToBigNumber(value); + } else if ( + numericBase === 10 && + // check if we have a finite integer or float + (isFinite(parseInt(value, 10)) || isFinite(parseFloat(value))) + ) { + return decimalToBigNumber(value); + } + throw new Error( + `String provided to stringToBigNumber is not a hexadecimal or decimal string: ${value}, ${numericBase}`, + ); +} + +/** + * This method will convert a hexadecimal or deciaml number into a BigNumber. + * The second parameter must be supplied and determines whether to treat the + * value as a hexadecimal or decimal value. + * + * @param value - hexadecimal or decimal number[] + * @param numericBase - 10 for decimal, 16 for hexadecimal + * @returns BigNumber representation of the value + */ +function numberToBigNumber(value: number, numericBase: NumericBase) { + if (typeof value !== 'number') { + throw new Error( + `Value of type ${typeof value} passed to numberToBigNumber`, + ); + } + if (numericBase === 16 && isHexString(`${value}`)) { + return new BigNumber(`${value}`, 16); + } + return new BigNumber(value, 10); +} + +/** + * Method to convert a BN to a BigNumber + * + * @param value - A BN representation of a value + * @returns A BigNumber representation of the BN's underlying value + */ +function bnToBigNumber(value: BN) { + if (value instanceof BN === false) { + throw new Error( + `value passed to bnToBigNumber is not a BN. Received type ${typeof value}`, + ); + } + return new BigNumber(value.toString(16), 16); +} + +/** + * Converts a value of the supported types (string, number, BN) to a BigNumber. + * + * @param value - The value to convert to a BigNumber + * @param numericBase - The numeric base of the underlying value + * @returns A BigNumber representation of the value + */ +function valueToBigNumber(value: string | number, numericBase: NumericBase) { + if (typeof value === 'string') { + return stringToBigNumber(value, numericBase); + } else if (typeof value === 'number' && isNaN(value) === false) { + return numberToBigNumber(value, numericBase); + } + + throw new Error( + `Value: ${value} is not a string, number, BigNumber or BN. Type is: ${typeof value}.`, + ); +} + +// Big Number Constants +const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000'); +const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000'); +const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1'); + +const toNormalizedDenomination = { + WEI: (bigNumber: BigNumber) => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER), + GWEI: (bigNumber: BigNumber) => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER), + ETH: (bigNumber: BigNumber) => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER), +}; +const toSpecifiedDenomination = { + WEI: (bigNumber: BigNumber) => + bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(), + GWEI: (bigNumber: BigNumber) => + bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9), + ETH: (bigNumber: BigNumber) => + bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).round(9), +}; + +/** + * Gets the value in ETH of the numeric supplied, used in this file only to + * convert to ETH prior to converting to another denomination. The following + * quirks were programmed into this method to replicate behavior of the + * predecessor to Numeric, which was 'conversionUtil'. If a denomination is + * not supplied, and toDenomination is called, then we assume the denomination + * was originally ETH, otherwise we convert it to ETH. + * + * @param numeric + * @returns value in ETH + */ +function getValueInETH(numeric: Numeric) { + if ( + numeric.denomination === EtherDenomination.ETH || + typeof numeric.denomination === 'undefined' + ) { + return numeric.value; + } + return toNormalizedDenomination[numeric.denomination](numeric.value); +} + +/** + * When applying operands to Numerics that have a specified Denomination then + * we should first convert the provided inputNumeric to the same Denomination + * as the baseNumeric. There are cases where this doesn't apply: + * + * 1. If the denominations are already the same. No conversion is necessary. + * 2. If the inputNumeric does not have a denomination set. We assume in this + * case that the value is already in the appropriate denomination. + * + * @param baseNumeric + * @param inputNumeric + * @returns + */ +function alignOperandDenominations( + baseNumeric: Numeric, + inputNumeric: Numeric, +) { + if ( + typeof inputNumeric.denomination !== 'undefined' && + baseNumeric.denomination !== inputNumeric.denomination + ) { + return inputNumeric.toDenomination(baseNumeric.denomination); + } + + return inputNumeric; +} + +/** + * Numeric is a class whose methods will always return a new, not mutated, + * value. This allows for chaining of non-terminating methods. Previously we + * had near a hundred helper methods that composed one-another, making tracking + * through the chain near impossible. This API is designed such that no helper + * methods should be needed. Take the case of hexWEIToDecGWEI, a helper method + * for taking a hex string representing a value in WEI and converting that to a + * decimal of GWEI. Prior to this class the method would call into our root + * level 'conversionUtil' which was the proverbial kitchen sink doing + * everything from denomination conversion, currency conversion (with provided + * conversionRate prop) and more. The same opeartion can now be expressed as: + * new Numeric(hexString, 16, EtherDenomination.WEI) + * .toDenomination(EtherDenomination.GWEI) + * .toBase(10) + * .toString(); + * This has the benefit of being fairly transparent as you can read each step + * in the chain and have a good sense of what is being done. It also is highly + * composable so that we shouldn't need tons of helper methods for shortcuts. + */ +export class Numeric { + /** + * The underlying value of the Numeric, always in BigNumber form + */ + value: BigNumber; + + /** + * The numeric base for this Numeric, either 10 for decimal or 16 for Hex + */ + base?: NumericBase; + + /** + * The current denomination, if any. The only supported denominations are + * ETH, GWEI, WEI. + */ + denomination?: EtherDenomination; + + constructor( + value: NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + this.base = base; + this.denomination = denomination; + if (value instanceof BigNumber) { + this.value = value; + } else if (value instanceof BN) { + this.value = bnToBigNumber(value); + } else if ( + isNullOrUndefined(value) || + (typeof value === 'number' && isNaN(value)) + ) { + // There are parts of the codebase that call this method without a value, + // or with a 'NaN' (which is probably a bug somewhere in our tests?). + // Over time of converting to TypeScript we will eradicate those, but the + // helper methods that those instances employ would default the value to + // 0. This block keeps that intact. + this.value = new BigNumber('0', 10); + this.base = 10; + } else if (base) { + this.value = valueToBigNumber(value, base); + } else { + throw new Error( + `You must specify the base of the provided number if the value is not already a BigNumber`, + ); + } + } + + /** + * This is a tool used internally to check if a value is already a Numeric + * and return it if it is, otherwise it uses the other provided arguments to + * create a new Numeric. + * + * @param value - The value of the Numeric + * @param base - Either undefined, 10 for decimal or 16 for hexadecimal + * @param denomination - The Ether denomination to set, if any + */ + static from( + value: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + if (value instanceof Numeric) { + if (base || denomination) { + throw new Error( + `Numeric.from was called with a value (${value.toString()}) that is already a Numeric but a base and/or denomination was provided. Only supply base or denomination when creating a new Numeric`, + ); + } + return value; + } + return new Numeric(value, base, denomination); + } + + /** Conversions */ + + /** + * Returns a new Numeric with the base value changed to the provided base, + * or the original Numeric if the base provided is the same as the current + * base. No computation or conversion happens here but rather the result of + * toString will be changed depending on the value of this.base when that + * method is invoked. + * + * @param base - The numeric base to change the Numeric to, either 10 or 16 + * @returns A new Numeric with the base updated + */ + toBase(base: NumericBase) { + if (this.base !== base) { + return new Numeric(this.value, base, this.denomination); + } + return this; + } + + /** + * Converts the value to the specified denomination. The following quirks of + * the predecessor to Numeric, 'conversionUtil', were programmed into this + * method: + * 1. You may supply a denomination that is undefined, which will result in + * nothing happening. Coincidently this is also useful due to the nature of + * chaining operations on Numeric. You may pass an undefined value in this + * method without breaking the chain to conditionally apply a operator. + * 2. If the numeric that .toDenomination is called on does not have a + * denomination set, that is it was constructed without the third parameter, + * then it is assumed to be in ETH. Otherwise we convert it to ETH prior to + * attempting to convert it to another denomination because all of the + * toSpecifiedDenomination methods assume a value in ETH is passed. + * + * @param denomination - The denomination to convert to + * @returns A new numeric with the same base as the previous, but the + * value and denomination changed accordingly + */ + toDenomination(denomination?: EtherDenomination) { + if (denomination && this.denomination !== denomination) { + const result = new Numeric( + toSpecifiedDenomination[denomination](getValueInETH(this)), + this.base, + denomination, + ); + return result; + } + return this; + } + + /** + * Replicates a method of BigNumber that is not in the version of BigNumber + * that we use currently. Essentially shifting the decimal point backwards by + * an amount equal to the positive number supplied to the decimals operator. + * For example, calling shiftedBy(10) on the value 10000000000 will result in + * a value of 1.0000000000. If passing a negative number, then the decimal + * position will move forward. 1.0000000000 shiftedBy(-10) yields 10000000000 + * + * @param decimals - The number of decimal places to move. Positive moves + * decimal backwards, creating a smaller number. Negative values move the + * decimal forwards, creating a larger number. + * @returns A new numeric with the same base and denomination as the current + * but with a new value. + */ + shiftedBy(decimals: number) { + const powerOf = new Numeric(Math.pow(10, decimals), 10); + return this.divide(powerOf); + } + + /** + * Applies a conversion rate to the Numeric. If rate is undefined returns the + * same instance that was operated on. Allowing an undefined value makes + * chaining this operator feasible with undefined values from the user or + * state without manipulating the number. For example: + * + * new Numeric(5, 10) + * .applyConversionRate(possiblyUndefinedRate) + * .toBase(16) + * .toString(); + * + * Will return a valid result as long as possiblyUndefinedRate is undefined, + * a BigNumber or a number. In some areas of the codebase we check to see if + * the target currency is different from the current currency before applying + * a conversionRate. This functionality is not built into Numeric and will + * require breaking the chain before calling this method: + * let value = new Numeric(5, 10); + * + * if (fromCurrency !== toCurrency) { + * value = value.applyConversionRate(possiblyUndefinedRate); + * } + * + * return value.toBase(16).toString(); + * + * @param rate - The multiplier to apply + * @param invert - if true, inverts the rate + * @returns New Numeric value with conversion rate applied. + */ + applyConversionRate(rate?: number | BigNumber, invert?: boolean) { + if (typeof rate === 'undefined') { + return this; + } + + let conversionRate = new Numeric(rate, 10); + if (invert) { + conversionRate = new Numeric(new BigNumber(1.0)).divide(conversionRate); + } + return this.times(conversionRate); + } + + round( + numberOfDecimals?: number, + roundingMode: number = BigNumber.ROUND_HALF_DOWN, + ) { + if (numberOfDecimals) { + return new Numeric( + this.value.round(numberOfDecimals, roundingMode), + this.base, + this.denomination, + ); + } + return this; + } + + /** + * TODO: make it possible to add ETH + GWEI value. So if you have + * Numeric 1 with denomination ETH and Numeric 2 with Denomination WEI, + * first convert Numeric 2 to ETH then add the amount to Numeric 1. + * + * @param value + * @param base + * @param denomination + */ + add( + value: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + const numeric = Numeric.from(value, base, denomination); + return new Numeric( + this.value.add(alignOperandDenominations(this, numeric).value), + this.base, + this.denomination, + ); + } + + /** + * TODO: make it possible to subtract ETH - GWEI value. So if you have + * Numeric 1 with denomination ETH and Numeric 2 with Denomination WEI, + * first convert Numeric 2 to ETH then subtract the amount from Numeric 1. + * + * @param value + * @param base + * @param denomination + */ + minus( + value: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + const numeric = Numeric.from(value, base, denomination); + + return new Numeric( + this.value.minus(alignOperandDenominations(this, numeric).value), + this.base, + this.denomination, + ); + } + + times( + multiplier: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + const multiplierNumeric = Numeric.from(multiplier, base, denomination); + return new Numeric( + this.value.times( + alignOperandDenominations(this, multiplierNumeric).value, + ), + this.base, + this.denomination, + ); + } + + /** + * Divides the Numeric by another supplied Numeric, carrying over the base + * and denomination from the current Numeric. + * + * @param divisor - The Numeric to divide this Numeric by + * @param base + * @param denomination + * @returns A new Numeric that contains the result of the division + */ + divide( + divisor: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + return new Numeric( + this.value.div( + alignOperandDenominations( + this, + Numeric.from(divisor, base, denomination), + ).value, + ), + this.base, + this.denomination, + ); + } + + greaterThan( + comparator: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + return this.value.greaterThan( + Numeric.from(comparator, base, denomination).value, + ); + } + + greaterThanOrEqualTo( + comparator: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + return this.value.greaterThanOrEqualTo( + Numeric.from(comparator, base, denomination).value, + ); + } + + lessThan( + comparator: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + return this.value.lessThan( + Numeric.from(comparator, base, denomination).value, + ); + } + + lessThanOrEqualTo( + comparator: Numeric | NumericValue, + base?: NumericBase, + denomination?: EtherDenomination, + ) { + return this.value.lessThanOrEqualTo( + Numeric.from(comparator, base, denomination).value, + ); + } + + isNegative() { + return this.value.isNegative(); + } + + isPositive() { + return this.isNegative() === false; + } + + /** + * Get a base 16 hexadecimal string representation of the Numeric that is + * 0x prefixed. This operation bypasses the currently set base of the + * Numeric. + * + * @returns 0x prefixed hexstring. + */ + toPrefixedHexString() { + return addHexPrefix(this.value.toString(16)); + } + + /** + * Gets the string representation of the Numeric, using the current value of + * this.base to determine if it should be a decimal or hexadecimal string. + * + * @returns the string representation of the Numeric + */ + toString() { + return this.value.toString(this.base); + } + + /** + * Returns a fixed-point decimal string representation of the Numeric + * + * @param decimals - the amount of decimal precision to use when rounding + * @returns A fixed point decimal string represenation of the Numeric + */ + toFixed(decimals: number) { + return this.value.toFixed(decimals); + } + + /** + * Converts the value to a JavaScript Number, with all of the inaccuracy that + * could come with that. + * + * @returns The value as a JS Number + */ + toNumber() { + return this.value.toNumber(); + } +} diff --git a/yarn.lock b/yarn.lock index 7e2582f84..2ee2a7ae2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4219,14 +4219,15 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^3.0.1, @metamask/utils@npm:^3.0.3, @metamask/utils@npm:^3.3.0, @metamask/utils@npm:^3.3.1": - version: 3.3.1 - resolution: "@metamask/utils@npm:3.3.1" +"@metamask/utils@npm:^3.0.1, @metamask/utils@npm:^3.0.3, @metamask/utils@npm:^3.3.0, @metamask/utils@npm:^3.3.1, @metamask/utils@npm:^3.4.1": + version: 3.4.1 + resolution: "@metamask/utils@npm:3.4.1" dependencies: "@types/debug": ^4.1.7 debug: ^4.3.4 - superstruct: ^0.16.7 - checksum: 5b6b6b54fdff4bc3f77b31ef50c23adca8fdf21d81d4f68d3d9c2b383b145cd61c2435f5ba0a11344484ae1f6d2355fab82eec58ce6b19eb35b476928b2e4ee6 + semver: ^7.3.8 + superstruct: ^1.0.3 + checksum: 0799cefc17effecba4b4cd34879113f9f826a7aff4d21bfdcca64ef31c117be3e6a30cdd49c0b91289f22efbf7e56901322f4ce1b4d638dd2fc3bc3e81e3c87d languageName: node linkType: hard @@ -24155,6 +24156,7 @@ __metadata: "@metamask/snaps-utils": ^0.27.1 "@metamask/subject-metadata-controller": ^1.0.0 "@metamask/test-dapp": ^5.2.1 + "@metamask/utils": ^3.4.1 "@ngraveio/bc-ur": ^1.1.6 "@popperjs/core": ^2.4.0 "@reduxjs/toolkit": ^1.6.2 @@ -30711,7 +30713,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7": +"semver@npm:^7.0.0, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8": version: 7.3.8 resolution: "semver@npm:7.3.8" dependencies: @@ -32409,6 +32411,13 @@ __metadata: languageName: node linkType: hard +"superstruct@npm:^1.0.3": + version: 1.0.3 + resolution: "superstruct@npm:1.0.3" + checksum: 761790bb111e6e21ddd608299c252f3be35df543263a7ebbc004e840d01fcf8046794c274bcb351bdf3eae4600f79d317d085cdbb19ca05803a4361840cc9bb1 + languageName: node + linkType: hard + "supports-color@npm:6.0.0": version: 6.0.0 resolution: "supports-color@npm:6.0.0"