diff --git a/src/utils/TokenAmount.js b/src/utils/TokenAmount.js new file mode 100644 index 000000000..b28ebe62a --- /dev/null +++ b/src/utils/TokenAmount.js @@ -0,0 +1,92 @@ +import JSBI from 'jsbi' +import { toJsbi } from './math' +import { formatTokenAmount } from './format' + +class TokenAmount { + #amount + #decimals + #symbol + + /** + * Create a TokenAmount. + * @param {BigInt|string|number} amount The amount as an integer (e.g. in Wei for Ether). + * @param {BigInt|string|number} decimals The token decimals (e.g. 18 for Ether). + * @param {string} options.symbol The token symbol (e.g. ETH for Ether). + */ + constructor(amount, decimals, { symbol = '' } = {}) { + amount = toJsbi(amount) + decimals = toJsbi(decimals) + + if (JSBI.lessThan(decimals, 0)) { + throw new Error('TokenAmount: decimals cannot be negative') + } + + this.#amount = amount + this.#decimals = decimals + this.#symbol = symbol + } + + /** + * Get the amount of the token without the decimals (e.g. in Wei for Ether), + * as a string integer. + * @returns {string} + */ + amount() { + return this.#amount.toString() + } + + /** + * Get the decimals of the token. + * @returns {number} + */ + decimals() { + return this.#decimals.toNumber() + } + + /** + * Formats the token amount for display purposes. + * @param {string} options.displaySign Whether to display the sign or not. + * @param {string} options.displaySymbol Whether to display the token symbol or not. + * @param {string} options.digits The number of digits to appear after the decimal point. + * @returns {string} + */ + format({ sign = false, symbol = false, digits = 2 } = {}) { + return formatTokenAmount(this.#amount, this.#decimals, { + digits, + sign, + symbol: symbol ? this.#symbol : '', + }) + } + + /** + * Returns an object to be serialized by JSON.stringify(). + * @returns {object} + */ + toJSON() { + return { + amount: this.#amount.toString(), + decimals: this.#decimals.toString(), + symbol: this.#symbol, + } + } + + /** + * Instanciate a new TokenAmount from the data serialized by JSON.stringify(). + * @returns {object} + */ + static fromJSON(jsonData) { + try { + const { amount, decimals, symbol } = JSON.parse(jsonData) + if (amount === undefined || decimals === undefined) { + throw new Error() + } + return new TokenAmount(amount, decimals, { symbol }) + } catch (err) { + throw new Error( + 'The data passed to TokenAmount.fromJSON() seems incorrect or incomplete.' + ) + } + } +} + +export default TokenAmount diff --git a/src/utils/TokenAmount.test.js b/src/utils/TokenAmount.test.js new file mode 100644 index 000000000..d6c6e7119 --- /dev/null +++ b/src/utils/TokenAmount.test.js @@ -0,0 +1,52 @@ +import TokenAmount from './TokenAmount' + +describe('TokenAmount', () => { + test('should instanciate from an amount expressed as a Number', () => { + expect(new TokenAmount(91234, 4).format()).toEqual('9.12') + }) + + test('should throw if decimals are negative', () => { + expect(() => { + return new TokenAmount(91234, -1) + }).toThrow() + }) +}) + +describe('TokenAmount#amount()', () => { + test('should export the amount as a string integer', () => { + expect(new TokenAmount('9381295879707883945', 18).amount()).toEqual( + '9381295879707883945' + ) + }) +}) + +describe('TokenAmount#toJSON()', () => { + test('should serialize properly', () => { + expect(new TokenAmount('9381295879707883945', 18).toJSON()).toEqual({ + amount: '9381295879707883945', + decimals: '18', + symbol: '', + }) + expect(JSON.stringify(new TokenAmount('9381295879707883945', 18))).toEqual( + JSON.stringify({ + amount: '9381295879707883945', + decimals: '18', + symbol: '', + }) + ) + }) +}) + +describe('TokenAmount.fromJSON()', () => { + test('should deserialize properly', () => { + expect( + TokenAmount.fromJSON( + JSON.stringify(new TokenAmount('9381295879707883945', 18)) + ).toJSON() + ).toEqual({ + amount: '9381295879707883945', + decimals: '18', + symbol: '', + }) + }) +}) diff --git a/src/utils/format.js b/src/utils/format.js index 535c8da70..ae49b5088 100644 --- a/src/utils/format.js +++ b/src/utils/format.js @@ -1,6 +1,6 @@ import JSBI from 'jsbi' import { NO_BREAK_SPACE } from './characters' -import { divideRoundBigInt } from './math' +import { divideRoundBigInt, toJsbi } from './math' /** * Formats an integer based on a limited range. @@ -65,9 +65,9 @@ export function formatTokenAmount( decimals, { digits = 2, symbol = '', displaySign = false } = {} ) { - amount = JSBI.BigInt(String(amount)) - decimals = JSBI.BigInt(String(decimals)) - digits = JSBI.BigInt(String(digits)) + amount = toJsbi(amount) + decimals = toJsbi(decimals) + digits = toJsbi(digits) if (JSBI.lessThan(decimals, 0)) { throw new Error('formatTokenAmount(): decimals cannot be negative') diff --git a/src/utils/math.js b/src/utils/math.js index 3f50e7544..74c9f3c63 100644 --- a/src/utils/math.js +++ b/src/utils/math.js @@ -1,5 +1,15 @@ import JSBI from 'jsbi' +/** + * Converts BigInt-like values into JSBI objects. + * + * @param {JSBI.BigInt|BigInt|string|number} value The value to convert. + * @returns {JSBI.BigInt} + */ +export function toJsbi(value) { + return JSBI.BigInt(String(value)) +} + /** * Re-maps a number from one range to another. * @@ -108,8 +118,8 @@ export function random(min = 0, max = 1) { * @returns {string} */ export function divideRoundBigInt(dividend, divisor) { - dividend = JSBI.BigInt(String(dividend)) - divisor = JSBI.BigInt(String(divisor)) + dividend = toJsbi(dividend) + divisor = toJsbi(divisor) return JSBI.divide( JSBI.add(dividend, JSBI.divide(divisor, JSBI.BigInt(2))), divisor