Skip to content

Commit

Permalink
Dedupe BigNumber libs in the utils package (#2828)
Browse files Browse the repository at this point in the history
### 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
  • Loading branch information
ottbunn authored Nov 10, 2023
1 parent b230aa7 commit 9c39d68
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 112 deletions.
14 changes: 8 additions & 6 deletions typescript/infra/src/config/gas-oracle.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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(),
),
);
}
3 changes: 3 additions & 0 deletions typescript/utils/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"require": ["ts-node/register"]
}
1 change: 0 additions & 1 deletion typescript/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export {
} from './src/addresses';
export {
convertDecimals,
convertDecimalsEthersBigNumber,
eqAmountApproximate,
fromWei,
fromWeiRounded,
Expand Down
6 changes: 4 additions & 2 deletions typescript/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
131 changes: 65 additions & 66 deletions typescript/utils/src/amount.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -40,54 +47,70 @@ 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) {
return null;
}
}

// 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);
}

/**
Expand All @@ -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);
}
}
87 changes: 79 additions & 8 deletions typescript/utils/src/big-numbers.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

Expand All @@ -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());
});
});
});
Loading

0 comments on commit 9c39d68

Please sign in to comment.