From 9c39d68cfcc2627158c533631de48339011ef4fa Mon Sep 17 00:00:00 2001 From: OttBunn <132830906+ottbunn@users.noreply.github.com> Date: Fri, 10 Nov 2023 19:44:23 +0100 Subject: [PATCH] Dedupe BigNumber libs in the utils package (#2828) ### Description Consistently use BigNumber.js across the Utils package ### Related issues - Fixes #2751 ### Backward compatibility No, minor adjustments needed where the utils consumers expected Ethers BigNumbers ### Testing Created unit tests --- typescript/infra/src/config/gas-oracle.ts | 14 ++- typescript/utils/.mocharc.json | 3 + typescript/utils/index.ts | 1 - typescript/utils/package.json | 6 +- typescript/utils/src/amount.ts | 131 +++++++++++----------- typescript/utils/src/big-numbers.test.ts | 87 ++++++++++++-- typescript/utils/src/big-numbers.ts | 76 ++++++++----- typescript/utils/src/multisig.ts | 7 +- yarn.lock | 3 +- 9 files changed, 216 insertions(+), 112 deletions(-) create mode 100644 typescript/utils/.mocharc.json diff --git a/typescript/infra/src/config/gas-oracle.ts b/typescript/infra/src/config/gas-oracle.ts index 41bf8ccac7..812314220a 100644 --- a/typescript/infra/src/config/gas-oracle.ts +++ b/typescript/infra/src/config/gas-oracle.ts @@ -1,7 +1,7 @@ import { BigNumber, ethers } from 'ethers'; import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; -import { convertDecimalsEthersBigNumber } from '@hyperlane-xyz/utils'; +import { convertDecimals } from '@hyperlane-xyz/utils'; import { mustGetChainNativeTokenDecimals } from '../utils/utils'; @@ -77,15 +77,17 @@ export function getTokenExchangeRateFromValues( localValue: BigNumber, remote: ChainName, remoteValue: BigNumber, -) { +): BigNumber { // This does not yet account for decimals! const exchangeRate = remoteValue .mul(TOKEN_EXCHANGE_RATE_MULTIPLIER) .div(localValue); - return convertDecimalsEthersBigNumber( - mustGetChainNativeTokenDecimals(remote), - mustGetChainNativeTokenDecimals(local), - exchangeRate, + return BigNumber.from( + convertDecimals( + mustGetChainNativeTokenDecimals(remote), + mustGetChainNativeTokenDecimals(local), + exchangeRate.toString(), + ), ); } diff --git a/typescript/utils/.mocharc.json b/typescript/utils/.mocharc.json new file mode 100644 index 0000000000..e516df998c --- /dev/null +++ b/typescript/utils/.mocharc.json @@ -0,0 +1,3 @@ +{ + "require": ["ts-node/register"] +} diff --git a/typescript/utils/index.ts b/typescript/utils/index.ts index a60d68b8d0..5c4486039e 100644 --- a/typescript/utils/index.ts +++ b/typescript/utils/index.ts @@ -30,7 +30,6 @@ export { } from './src/addresses'; export { convertDecimals, - convertDecimalsEthersBigNumber, eqAmountApproximate, fromWei, fromWeiRounded, diff --git a/typescript/utils/package.json b/typescript/utils/package.json index e679481873..3c2512fbda 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -5,7 +5,8 @@ "dependencies": { "@solana/web3.js": "^1.78.0", "bignumber.js": "^9.1.1", - "ethers": "^5.7.2" + "ethers": "^5.7.2", + "mocha": "^10.2.0" }, "devDependencies": { "chai": "^4.3.0", @@ -26,7 +27,8 @@ "build": "tsc", "clean": "rm -rf ./dist", "check": "tsc --noEmit", - "prettier": "prettier --write ./src" + "prettier": "prettier --write ./src", + "test:unit": "mocha --config .mocharc.json './src/**/*.test.ts'" }, "sideEffects": false, "types": "dist/index.d.ts", diff --git a/typescript/utils/src/amount.ts b/typescript/utils/src/amount.ts index 9aae2cad65..a47a0a1371 100644 --- a/typescript/utils/src/amount.ts +++ b/typescript/utils/src/amount.ts @@ -1,36 +1,43 @@ import { formatUnits, parseUnits } from '@ethersproject/units'; import BigNumber from 'bignumber.js'; -import { ethers } from 'ethers'; const DEFAULT_MIN_ROUNDED_VALUE = 0.00001; const DEFAULT_DISPLAY_DECIMALS = 4; const DEFAULT_TOKEN_DECIMALS = 18; -type NumberT = BigNumber.Value; +// Use toString(10) on bignumber.js to prevent ethers.js bigNumber error +// when parsing exponential string over e21 +/** + * Convert the given Wei value to Ether value + * @param value The value to convert. + * @returns Converted value in string type. + */ export function fromWei( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, decimals = DEFAULT_TOKEN_DECIMALS, -): number { - if (!value) return 0; - const valueString = value.toString().trim(); - const flooredValue = new BigNumber(valueString).toFixed( - 0, - BigNumber.ROUND_FLOOR, - ); - return parseFloat(formatUnits(flooredValue, decimals)); +): string { + if (!value) return (0).toString(); + const valueString = value.toString(10).trim(); + const flooredValue = BigNumber(valueString).toFixed(0, BigNumber.ROUND_FLOOR); + return parseFloat(formatUnits(flooredValue, decimals)).toString(); } -// Similar to fromWei above but rounds to set number of decimals -// with a minimum floor, configured per token +/** + * Convert the given Wei value to Ether value, + * round to set number of decimals with a minimum floor, configured per token + * @param value The value to convert. + * @param decimals + * @returns Converted value in string type. + */ export function fromWeiRounded( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, decimals = DEFAULT_TOKEN_DECIMALS, roundDownIfSmall = true, ): string { if (!value) return '0'; - const flooredValue = new BigNumber(value).toFixed(0, BigNumber.ROUND_FLOOR); - const amount = new BigNumber(formatUnits(flooredValue, decimals)); + const flooredValue = BigNumber(value).toFixed(0, BigNumber.ROUND_FLOOR); + const amount = BigNumber(formatUnits(flooredValue, decimals)); if (amount.isZero()) return '0'; // If amount is less than min value @@ -40,38 +47,49 @@ export function fromWeiRounded( } const displayDecimals = amount.gte(10000) ? 2 : DEFAULT_DISPLAY_DECIMALS; - return amount.toFixed(displayDecimals).toString(); + return amount.toFixed(displayDecimals); } +/** + * Convert the given value to Wei value + * @param value The value to convert. + * @returns Converted value in string type. + */ export function toWei( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, decimals = DEFAULT_TOKEN_DECIMALS, -): BigNumber { - if (!value) return new BigNumber(0); +): string { + if (!value) return BigNumber(0).toString(); // First convert to a BigNumber, and then call `toString` with the // explicit radix 10 such that the result is formatted as a base-10 string // and not in scientific notation. - const valueBN = new BigNumber(value); + const valueBN = BigNumber(value); const valueString = valueBN.toString(10).trim(); const components = valueString.split('.'); if (components.length === 1) { - return new BigNumber(parseUnits(valueString, decimals).toString()); + return parseUnits(valueString, decimals).toString(); } else if (components.length === 2) { const trimmedFraction = components[1].substring(0, decimals); - return new BigNumber( - parseUnits(`${components[0]}.${trimmedFraction}`, decimals).toString(), - ); + return parseUnits( + `${components[0]}.${trimmedFraction}`, + decimals, + ).toString(); } else { throw new Error(`Cannot convert ${valueString} to wei`); } } +/** + * Try to parse the given value into BigNumber.js BigNumber + * @param value The value to parse. + * @returns Parsed value in BigNumber.js BigNumber type. + */ export function tryParseAmount( - value: NumberT | null | undefined, + value: BigNumber.Value | null | undefined, ): BigNumber | null { try { if (!value) return null; - const parsed = new BigNumber(value); + const parsed = BigNumber(value); if (!parsed || parsed.isNaN() || !parsed.isFinite()) return null; else return parsed; } catch (error) { @@ -79,15 +97,20 @@ export function tryParseAmount( } } -// Checks if an amount is equal of nearly equal to balance within a small margin of error -// Necessary because amounts in the UI are often rounded +/** + * Checks if an amount is equal of nearly equal to balance within a small margin of error + * Necessary because amounts in the UI are often rounded + * @param amountInWei1 The amount to compare. + * @param amountInWei2 The amount to compare. + * @returns true/false. + */ export function eqAmountApproximate( - amountInWei1: BigNumber, - amountInWei2: NumberT, -) { + amountInWei1: BigNumber.Value, + amountInWei2: BigNumber.Value, +): boolean { const minValueWei = toWei(DEFAULT_MIN_ROUNDED_VALUE); // Is difference btwn amount and balance less than min amount shown for token - return amountInWei1.minus(amountInWei2).abs().lt(minValueWei); + return BigNumber(amountInWei1).minus(amountInWei2).abs().lt(minValueWei); } /** @@ -96,50 +119,26 @@ export function eqAmountApproximate( * @param fromDecimals The number of decimals `value` has. * @param toDecimals The number of decimals to convert `value` to. * @param value The value to convert. - * @returns `value` represented with `toDecimals` decimals. + * @returns `value` represented with `toDecimals` decimals in string type. */ export function convertDecimals( fromDecimals: number, toDecimals: number, - value: NumberT, -) { - const amount = new BigNumber(value); + value: BigNumber.Value, +): string { + const amount = BigNumber(value); - if (fromDecimals === toDecimals) return amount; + if (fromDecimals === toDecimals) return amount.toString(10); else if (fromDecimals > toDecimals) { const difference = fromDecimals - toDecimals; return amount - .div(new BigNumber(10).pow(difference)) - .integerValue(BigNumber.ROUND_FLOOR); - } - // fromDecimals < toDecimals - else { - const difference = toDecimals - fromDecimals; - return amount.times(new BigNumber(10).pow(difference)); - } -} - -/** - * Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals. - * Incurs a loss of precision when `fromDecimals` > `toDecimals`. - * @param fromDecimals The number of decimals `value` has. - * @param toDecimals The number of decimals to convert `value` to. - * @param value The value to convert. - * @returns `value` represented with `toDecimals` decimals. - */ -export function convertDecimalsEthersBigNumber( - fromDecimals: number, - toDecimals: number, - value: ethers.BigNumber, -) { - if (fromDecimals === toDecimals) return value; - else if (fromDecimals > toDecimals) { - const difference = fromDecimals - toDecimals; - return value.div(ethers.BigNumber.from('10').pow(difference)); + .div(BigNumber(10).pow(difference)) + .integerValue(BigNumber.ROUND_FLOOR) + .toString(10); } // fromDecimals < toDecimals else { const difference = toDecimals - fromDecimals; - return value.mul(ethers.BigNumber.from('10').pow(difference)); + return amount.times(BigNumber(10).pow(difference)).toString(10); } } diff --git a/typescript/utils/src/big-numbers.test.ts b/typescript/utils/src/big-numbers.test.ts index f3358098cb..1de260be67 100644 --- a/typescript/utils/src/big-numbers.test.ts +++ b/typescript/utils/src/big-numbers.test.ts @@ -1,15 +1,70 @@ +import BigNumber from 'bignumber.js'; import { expect } from 'chai'; -import { BigNumber, FixedNumber } from 'ethers'; +import { FixedNumber } from 'ethers'; -import { bigToFixed, fixedToBig, mulBigAndFixed } from './big-numbers'; +import { + BigNumberMax, + BigNumberMin, + bigToFixed, + fixedToBig, + isBigNumberish, + isZeroish, + mulBigAndFixed, +} from './big-numbers'; describe('utils', () => { + describe('isBigNumberish', () => { + const testCases = [ + { expect: false, context: 'invalid number', case: 'invalidNumber' }, + { expect: false, context: 'NaN', case: NaN }, + { expect: false, context: 'undefined', case: undefined }, + { expect: false, context: 'null', case: null }, + { expect: true, context: 'decimal', case: 123.123 }, + { expect: true, context: 'integer', case: 300_000 }, + { expect: true, context: 'hex 0', case: 0x00 }, + { expect: true, context: 'hex 0', case: 0x000 }, + { + expect: true, + context: 'address 0', + case: 0x0000000000000000000000000000000000000000, + }, + ]; + testCases.forEach((tc) => { + it(`returns ${tc.expect} for ${tc.case}`, () => { + expect(isBigNumberish(tc.case!)).to.equal(tc.expect); + }); + }); + }); + + describe('isZeroish', () => { + const testCases = [ + { expect: false, context: 'invalid number', case: 'invalidNumber' }, + { expect: false, context: 'NaN', case: NaN }, + { expect: false, context: 'undefined', case: undefined }, + { expect: false, context: 'null', case: null }, + { expect: false, context: 'non 0 decimal', case: 123.123 }, + { expect: false, context: 'non 0 integer', case: 123 }, + { expect: true, context: 'hex 0', case: 0x00 }, + { expect: true, context: 'hex 0', case: 0x000 }, + { + expect: true, + context: 'address 0', + case: 0x0000000000000000000000000000000000000000, + }, + ]; + testCases.forEach((tc) => { + it(`returns ${tc.expect} for ${tc.case}`, () => { + expect(isZeroish(tc.case!)).to.equal(tc.expect); + }); + }); + }); + describe('bigToFixed', () => { it('converts a BigNumber to a FixedNumber', () => { - const big = BigNumber.from('1234'); + const big = BigNumber('7.5e-10'); const fixed = bigToFixed(big); - expect(fixed.toUnsafeFloat()).to.equal(1234); + expect(fixed.toUnsafeFloat()).to.equal(7.5e-10); }); }); @@ -31,19 +86,35 @@ describe('utils', () => { describe('mulBigAndFixed', () => { it('gets the floored product of a BigNumber and FixedNumber', () => { - const big = BigNumber.from('1000'); + const big = BigNumber('1000'); const fixed = FixedNumber.from('1.2345'); const product = mulBigAndFixed(big, fixed); - expect(product.toNumber()).to.equal(1234); + expect(product).to.equal((1234).toString()); }); it('gets the ceilinged product of a BigNumber and FixedNumber', () => { - const big = BigNumber.from('1000'); + const big = BigNumber('1000'); const fixed = FixedNumber.from('1.2345'); const product = mulBigAndFixed(big, fixed, true); - expect(product.toNumber()).to.equal(1235); + expect(product).to.equal((1235).toString()); + }); + }); + + describe('BigNumberMin', () => { + it('gets the min between the two BigNumber', () => { + const big = BigNumber('1000'); + const bigger = BigNumber('10000'); + expect(BigNumberMin(big, bigger)).to.equal(big.toString()); + }); + }); + + describe('BigNumberMax', () => { + it('gets the max between the two BigNumber', () => { + const big = BigNumber('1000'); + const bigger = BigNumber('10000'); + expect(BigNumberMax(big, bigger)).to.equal(bigger.toString()); }); }); }); diff --git a/typescript/utils/src/big-numbers.ts b/typescript/utils/src/big-numbers.ts index 4e8b6dd733..3a85e289bf 100644 --- a/typescript/utils/src/big-numbers.ts +++ b/typescript/utils/src/big-numbers.ts @@ -1,26 +1,33 @@ -import { BigNumber, BigNumberish, FixedNumber, constants } from 'ethers'; +import BigNumber from 'bignumber.js'; +import { FixedNumber } from 'ethers'; -import { isNullish } from './typeof'; +// Use toString(10) on bignumber.js to prevent ethers.js bigNumber error +// when parsing exponential string over e21 -export function isBigNumberish(value: any): value is BigNumberish { +/** + * Check if a value is bigNumberish (e.g. valid numbers, bigNumber). + * @param value The value to check. + * @returns true/false. + */ +export function isBigNumberish( + value: BigNumber.Value | undefined | null, +): boolean { try { - if (isNullish(value)) return false; - return BigNumber.from(value)._isBigNumber; + const val = BigNumber(value!); + return !val.isNaN() && val.isFinite() && BigNumber.isBigNumber(val); } catch (error) { return false; } } -// If a value (e.g. hex string or number) is zeroish (0, 0x0, 0x00, etc.) -export function isZeroish(value: BigNumberish) { +/** + * Check if a value (e.g. hex string or number) is zeroish (0, 0x0, 0x00, etc.). + * @param value The value to check. + * @returns true/false. + */ +export function isZeroish(value: BigNumber.Value): boolean { try { - if ( - !value || - value === constants.HashZero || - value === constants.AddressZero - ) - return true; - return BigNumber.from(value).isZero(); + return BigNumber(value).isZero(); } catch (error) { return false; } @@ -31,8 +38,8 @@ export function isZeroish(value: BigNumberish) { * @param big The BigNumber to convert. * @returns A FixedNumber representation of a BigNumber. */ -export function bigToFixed(big: BigNumber): FixedNumber { - return FixedNumber.from(big.toString()); +export function bigToFixed(big: BigNumber.Value): FixedNumber { + return FixedNumber.from(big.toString(10)); } /** @@ -43,7 +50,7 @@ export function bigToFixed(big: BigNumber): FixedNumber { */ export function fixedToBig(fixed: FixedNumber, ceil = false): BigNumber { const fixedAsInteger = ceil ? fixed.ceiling() : fixed.floor(); - return BigNumber.from(fixedAsInteger.toFormat('fixed256x0').toString()); + return BigNumber(fixedAsInteger.toFormat('fixed256x0').toString()); } /** @@ -51,21 +58,40 @@ export function fixedToBig(fixed: FixedNumber, ceil = false): BigNumber { * @param big The BigNumber to multiply. * @param fixed The FixedNumber to multiply. * @param ceil If true, the ceiling of the product is used. Otherwise, the floor is used. - * @returns The BigNumber product. + * @returns The BigNumber product in string type. */ export function mulBigAndFixed( - big: BigNumber, + big: BigNumber.Value, fixed: FixedNumber, ceil = false, -): BigNumber { +): string { // Converts big to a FixedNumber, multiplies it by fixed, and converts the product back // to a BigNumber. - return fixedToBig(fixed.mulUnsafe(bigToFixed(big)), ceil); + return fixedToBig(fixed.mulUnsafe(bigToFixed(big)), ceil).toString(10); } -export function BigNumberMin(bn1: BigNumber, bn2: BigNumber) { - return bn1.gte(bn2) ? bn2 : bn1; +/** + * Return the smaller in the given two BigNumbers. + * @param bn1 The BigNumber to compare. + * @param bn2 The BigNumber to compare. + * @returns The smaller BigNumber in string type. + */ +export function BigNumberMin( + bn1: BigNumber.Value, + bn2: BigNumber.Value, +): string { + return BigNumber(bn1).gte(bn2) ? bn2.toString(10) : bn1.toString(10); } -export function BigNumberMax(bn1: BigNumber, bn2: BigNumber) { - return bn1.lte(bn2) ? bn2 : bn1; + +/** + * Return the bigger in the given two BigNumbers. + * @param bn1 The BigNumber to compare. + * @param bn2 The BigNumber to compare. + * @returns The bigger BigNumber in string type. + */ +export function BigNumberMax( + bn1: BigNumber.Value, + bn2: BigNumber.Value, +): string { + return BigNumber(bn1).lte(bn2) ? bn2.toString(10) : bn1.toString(10); } diff --git a/typescript/utils/src/multisig.ts b/typescript/utils/src/multisig.ts index 0f0f005657..334ef9ad9d 100644 --- a/typescript/utils/src/multisig.ts +++ b/typescript/utils/src/multisig.ts @@ -1,4 +1,5 @@ -import { BigNumber, utils } from 'ethers'; +import BigNumber from 'bignumber.js'; +import { utils } from 'ethers'; import { addressToBytes32 } from './addresses'; import { ParsedLegacyMultisigIsmMetadata } from './types'; @@ -18,7 +19,7 @@ export const parseLegacyMultisigIsmMetadata = ( const checkpointRoot = utils.hexlify( buf.slice(MERKLE_ROOT_OFFSET, MERKLE_INDEX_OFFSET), ); - const checkpointIndex = BigNumber.from( + const checkpointIndex = BigNumber( utils.hexlify(buf.slice(MERKLE_INDEX_OFFSET, ORIGIN_MAILBOX_OFFSET)), ).toNumber(); const originMailbox = utils.hexlify( @@ -30,7 +31,7 @@ export const parseLegacyMultisigIsmMetadata = ( ); }; const proof = parseBytesArray(MERKLE_PROOF_OFFSET, 32, 32); - const threshold = BigNumber.from( + const threshold = BigNumber( utils.hexlify(buf.slice(THRESHOLD_OFFSET, SIGNATURES_OFFSET)), ).toNumber(); const signatures = parseBytesArray( diff --git a/yarn.lock b/yarn.lock index 91517e2cd4..701c66a608 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4072,6 +4072,7 @@ __metadata: bignumber.js: ^9.1.1 chai: ^4.3.0 ethers: ^5.7.2 + mocha: ^10.2.0 prettier: ^2.8.8 typescript: ^5.1.6 languageName: unknown @@ -14460,7 +14461,7 @@ __metadata: languageName: node linkType: hard -"mocha@npm:^10.0.0": +"mocha@npm:^10.0.0, mocha@npm:^10.2.0": version: 10.2.0 resolution: "mocha@npm:10.2.0" dependencies: