From f87be84a65c98018b029a39c0d937617d91d0b3a Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Sun, 12 Apr 2020 20:53:40 +0100 Subject: [PATCH 1/6] Add TokenAmount --- src/utils/TokenAmount.js | 92 +++++++++++++++++++++++++++++++++++ src/utils/TokenAmount.test.js | 53 ++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 src/utils/TokenAmount.js create mode 100644 src/utils/TokenAmount.test.js diff --git a/src/utils/TokenAmount.js b/src/utils/TokenAmount.js new file mode 100644 index 000000000..df82794c1 --- /dev/null +++ b/src/utils/TokenAmount.js @@ -0,0 +1,92 @@ +/* global BigInt */ + +import JSBI from 'jsbi' +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 Ethers). + * @param {BigInt|string|number} decimals The token decimals (e.g. 18 for Ethers). + * @param {string} options.symbol The token symbol (e.g. ETH for Ethers). + */ + constructor(amount, decimals, { symbol = '' } = {}) { + this.#amount = JSBI.BigInt(amount) + this.#decimals = JSBI.BigInt(decimals) + this.#symbol = symbol + } + + /** + * Get the amount as an integer (e.g. in Wei for Ethers). + * @returns {BigInt} + */ + amount() { + return BigInt(this.#amount.toString()) + } + + /** + * Get the amount as an integer (e.g. in Wei for Ethers). + * @returns {string} + */ + amountString() { + 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..ba2f8225c --- /dev/null +++ b/src/utils/TokenAmount.test.js @@ -0,0 +1,53 @@ +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') + }) +}) +describe('TokenAmount#amount()', () => { + test('should export the amount as a BigInt', () => { + expect(new TokenAmount('9381295879707883945', 18).amount()).toEqual( + 9381295879707883945n + ) + }) +}) + +describe('TokenAmount#amountString()', () => { + test('should export the amount as a String', () => { + expect(new TokenAmount('9381295879707883945', 18).amountString()).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: '', + }) + }) +}) From c15f5b79a9dbbf2f0b0570d5b3781cb7daba6089 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Tue, 14 Apr 2020 11:51:14 +0100 Subject: [PATCH 2/6] New formatTokenAmount() API --- src/utils/TokenAmount.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/TokenAmount.js b/src/utils/TokenAmount.js index df82794c1..5e9b736f3 100644 --- a/src/utils/TokenAmount.js +++ b/src/utils/TokenAmount.js @@ -52,7 +52,8 @@ class TokenAmount { * @returns {string} */ format({ sign = false, symbol = false, digits = 2 } = {}) { - return formatTokenAmount(this.#amount, this.#decimals, digits, { + return formatTokenAmount(this.#amount, this.#decimals, { + digits, sign, symbol: symbol ? this.#symbol : '', }) From fb6bcbec6aba7226bbb4c4f2ddf8fcd330f08b35 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Tue, 14 Apr 2020 11:52:26 +0100 Subject: [PATCH 3/6] Throw if decimals are negative + add a toJsbi() utility --- src/utils/TokenAmount.js | 12 ++++++++++-- src/utils/TokenAmount.test.js | 7 +++++++ src/utils/format.js | 8 ++++---- src/utils/math.js | 10 ++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/utils/TokenAmount.js b/src/utils/TokenAmount.js index 5e9b736f3..8582f6c26 100644 --- a/src/utils/TokenAmount.js +++ b/src/utils/TokenAmount.js @@ -1,6 +1,7 @@ /* global BigInt */ import JSBI from 'jsbi' +import { toJsbi } from './math' import { formatTokenAmount } from './format' class TokenAmount { @@ -15,8 +16,15 @@ class TokenAmount { * @param {string} options.symbol The token symbol (e.g. ETH for Ethers). */ constructor(amount, decimals, { symbol = '' } = {}) { - this.#amount = JSBI.BigInt(amount) - this.#decimals = JSBI.BigInt(decimals) + 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 } diff --git a/src/utils/TokenAmount.test.js b/src/utils/TokenAmount.test.js index ba2f8225c..5c4f93324 100644 --- a/src/utils/TokenAmount.test.js +++ b/src/utils/TokenAmount.test.js @@ -4,7 +4,14 @@ 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 BigInt', () => { expect(new TokenAmount('9381295879707883945', 18).amount()).toEqual( 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..d457ab9dc 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. * From 9736c813500c2853ee01bcd5ca86ec0e698e9481 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Tue, 14 Apr 2020 13:03:18 +0100 Subject: [PATCH 4/6] Ethers => Ether --- src/utils/TokenAmount.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/TokenAmount.js b/src/utils/TokenAmount.js index 8582f6c26..35818f8b5 100644 --- a/src/utils/TokenAmount.js +++ b/src/utils/TokenAmount.js @@ -11,9 +11,9 @@ class TokenAmount { /** * Create a TokenAmount. - * @param {BigInt|string|number} amount The amount as an integer (e.g. in Wei for Ethers). - * @param {BigInt|string|number} decimals The token decimals (e.g. 18 for Ethers). - * @param {string} options.symbol The token symbol (e.g. ETH for Ethers). + * @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) @@ -29,7 +29,7 @@ class TokenAmount { } /** - * Get the amount as an integer (e.g. in Wei for Ethers). + * Get the amount as an integer (e.g. in Wei for Ether). * @returns {BigInt} */ amount() { @@ -37,7 +37,7 @@ class TokenAmount { } /** - * Get the amount as an integer (e.g. in Wei for Ethers). + * Get the amount as an integer (e.g. in Wei for Ether). * @returns {string} */ amountString() { From 5431650d51665a1fed0c8acf47d78e4d6b474d02 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Tue, 14 Apr 2020 15:27:49 +0100 Subject: [PATCH 5/6] Move from returning a BigInt to a string integer --- src/utils/TokenAmount.js | 15 +++------------ src/utils/TokenAmount.test.js | 10 +--------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/utils/TokenAmount.js b/src/utils/TokenAmount.js index 35818f8b5..b28ebe62a 100644 --- a/src/utils/TokenAmount.js +++ b/src/utils/TokenAmount.js @@ -1,5 +1,3 @@ -/* global BigInt */ - import JSBI from 'jsbi' import { toJsbi } from './math' import { formatTokenAmount } from './format' @@ -29,18 +27,11 @@ class TokenAmount { } /** - * Get the amount as an integer (e.g. in Wei for Ether). - * @returns {BigInt} - */ - amount() { - return BigInt(this.#amount.toString()) - } - - /** - * Get the amount as an integer (e.g. in Wei for Ether). + * Get the amount of the token without the decimals (e.g. in Wei for Ether), + * as a string integer. * @returns {string} */ - amountString() { + amount() { return this.#amount.toString() } diff --git a/src/utils/TokenAmount.test.js b/src/utils/TokenAmount.test.js index 5c4f93324..d6c6e7119 100644 --- a/src/utils/TokenAmount.test.js +++ b/src/utils/TokenAmount.test.js @@ -13,16 +13,8 @@ describe('TokenAmount', () => { }) describe('TokenAmount#amount()', () => { - test('should export the amount as a BigInt', () => { + test('should export the amount as a string integer', () => { expect(new TokenAmount('9381295879707883945', 18).amount()).toEqual( - 9381295879707883945n - ) - }) -}) - -describe('TokenAmount#amountString()', () => { - test('should export the amount as a String', () => { - expect(new TokenAmount('9381295879707883945', 18).amountString()).toEqual( '9381295879707883945' ) }) From d7cf58b5b9945a04184f73f5054ea27fb470d295 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Tue, 14 Apr 2020 15:29:39 +0100 Subject: [PATCH 6/6] Use toJsBi() --- src/utils/math.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/math.js b/src/utils/math.js index d457ab9dc..74c9f3c63 100644 --- a/src/utils/math.js +++ b/src/utils/math.js @@ -118,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