{t('noConversionRateAvailable')}
@@ -150,48 +170,43 @@ export default function CurrencyInput({
return null;
}
- if (shouldUseFiat) {
- // Display ETH
- currency = preferredCurrency || EtherDenomination.ETH;
- numberOfDecimals = 8;
+ if (isTokenPrimary) {
+ // Display fiat; `displayValue` bypasses calculations
+ displayValue = formatCurrency(
+ new Numeric(fiatDecimalValue, 10).toString(),
+ secondaryCurrency,
+ );
} else {
- // Display Fiat
- currency = isOriginalNativeSymbol ? secondaryCurrency : null;
- numberOfDecimals = 2;
+ // Display token
+ suffix = primarySuffix;
+ displayValue = new Numeric(tokenDecimalValue, 10).toString();
}
return (
);
};
return (
{renderConversionComponent()}
@@ -200,9 +215,15 @@ export default function CurrencyInput({
CurrencyInput.propTypes = {
hexValue: PropTypes.string,
- featureSecondary: PropTypes.bool,
+ isFiatPreferred: PropTypes.bool,
onChange: PropTypes.func,
onPreferenceToggle: PropTypes.func,
swapIcon: PropTypes.func,
className: PropTypes.string,
+ asset: PropTypes.shape({
+ address: PropTypes.string,
+ symbol: PropTypes.string,
+ decimals: PropTypes.number,
+ isERC721: PropTypes.bool,
+ }),
};
diff --git a/ui/components/app/currency-input/currency-input.stories.js b/ui/components/app/currency-input/currency-input.stories.js
index 895de2876a70..170f17eeb81c 100644
--- a/ui/components/app/currency-input/currency-input.stories.js
+++ b/ui/components/app/currency-input/currency-input.stories.js
@@ -8,7 +8,7 @@ export default {
hexValue: {
control: 'text',
},
- featureSecondary: {
+ isFiatPreferred: {
control: 'boolean',
},
onChange: {
diff --git a/ui/components/app/currency-input/currency-input.test.js b/ui/components/app/currency-input/currency-input.test.js
index baf5edf6d91e..efc57df3ea04 100644
--- a/ui/components/app/currency-input/currency-input.test.js
+++ b/ui/components/app/currency-input/currency-input.test.js
@@ -10,6 +10,7 @@ jest.mock('../../../hooks/useIsOriginalNativeTokenSymbol', () => {
useIsOriginalNativeTokenSymbol: jest.fn(),
};
});
+
describe('CurrencyInput Component', () => {
useIsOriginalNativeTokenSymbol.mockReturnValue(true);
@@ -60,8 +61,8 @@ describe('CurrencyInput Component', () => {
const props = {
onChange: jest.fn(),
- hexValue: 'f602f2234d0ea',
- featureSecondary: true,
+ hexValue: '0xf602f2234d0ea',
+ isFiatPreferred: true,
};
const { container } = renderWithProvider(
@@ -87,8 +88,8 @@ describe('CurrencyInput Component', () => {
const props = {
onChange: jest.fn(),
- hexValue: 'f602f2234d0ea',
- featureSecondary: true,
+ hexValue: '0xf602f2234d0ea',
+ isFiatPreferred: true,
};
const { container } = renderWithProvider(
@@ -99,11 +100,13 @@ describe('CurrencyInput Component', () => {
expect(container).toMatchSnapshot();
});
- it('should render properly small number', () => {
+ it('should render small number properly', () => {
const store = configureMockStore()(mockStore);
const props = {
+ onChange: jest.fn(),
hexValue: '174876e800',
+ isFiatPreferred: false,
};
const { getByTestId } = renderWithProvider(
@@ -123,20 +126,22 @@ describe('CurrencyInput Component', () => {
const props = {
onChange: jest.fn(),
- hexValue: 'f602f2234d0ea',
+ hexValue: '0xf602f2234d0ea',
};
- const { queryByTestId, queryByTitle } = renderWithProvider(
+ const { queryByTestId, queryByTitle, rerender } = renderWithProvider(
,
store,
);
const currencyInput = queryByTestId('currency-input');
-
fireEvent.change(currencyInput, { target: { value: 1 } });
- expect(props.onChange).toHaveBeenCalledWith('de0b6b3a7640000');
- expect(queryByTitle('$231.06 USD')).toBeInTheDocument();
+ expect(props.onChange).toHaveBeenCalledWith('0xde0b6b3a7640000');
+ // assume the onChange function updates the hexValue
+ rerender(
);
+
+ expect(queryByTitle('$231.06')).toBeInTheDocument();
});
it('should call onChange on input changes with the hex value for fiat', () => {
@@ -144,8 +149,8 @@ describe('CurrencyInput Component', () => {
const props = {
onChange: jest.fn(),
- hexValue: 'f602f2234d0ea',
- featureSecondary: true,
+ hexValue: '0xf602f2234d0ea',
+ isFiatPreferred: true,
};
const { queryByTestId, queryByTitle } = renderWithProvider(
@@ -157,8 +162,8 @@ describe('CurrencyInput Component', () => {
fireEvent.change(currencyInput, { target: { value: 1 } });
- expect(props.onChange).toHaveBeenCalledWith('f602f2234d0ea');
- expect(queryByTitle('0.00432788 ETH')).toBeInTheDocument();
+ expect(props.onChange).toHaveBeenCalledWith('0xf604b06968000');
+ expect(queryByTitle('0.004328 ETH')).toBeInTheDocument();
});
it('should swap selected currency when swap icon is clicked', async () => {
@@ -166,11 +171,11 @@ describe('CurrencyInput Component', () => {
const props = {
onChange: jest.fn(),
onPreferenceToggle: jest.fn(),
- hexValue: 'f602f2234d0ea',
- featureSecondary: true,
+ hexValue: '0xf602f2234d0ea',
+ isFiatPreferred: true,
};
- const { queryByTestId, queryByTitle } = renderWithProvider(
+ const { queryByTestId, queryByTitle, rerender } = renderWithProvider(
,
store,
);
@@ -178,13 +183,16 @@ describe('CurrencyInput Component', () => {
const currencyInput = queryByTestId('currency-input');
fireEvent.change(currencyInput, { target: { value: 1 } });
- expect(queryByTitle('0.00432788 ETH')).toBeInTheDocument();
+ expect(queryByTitle('0.004328 ETH')).toBeInTheDocument();
const currencySwap = queryByTestId('currency-swap');
fireEvent.click(currencySwap);
+ // expect isFiatPreferred to update
+ rerender(
);
+
await waitFor(() => {
- expect(queryByTitle('$1.00 USD')).toBeInTheDocument();
+ expect(queryByTitle('$1.00')).toBeInTheDocument();
});
});
});
diff --git a/ui/components/app/currency-input/hooks/useProcessNewDecimalValue.test.tsx b/ui/components/app/currency-input/hooks/useProcessNewDecimalValue.test.tsx
new file mode 100644
index 000000000000..54ff4c6a04f6
--- /dev/null
+++ b/ui/components/app/currency-input/hooks/useProcessNewDecimalValue.test.tsx
@@ -0,0 +1,164 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { Numeric } from '../../../../../shared/modules/Numeric';
+import useProcessNewDecimalValue from './useProcessNewDecimalValue';
+
+const renderUseProcessNewDecimalValue = (
+ assetDecimals: number,
+ isFiatPrimary: boolean,
+ tokenToFiatConversionRate: Numeric,
+) => {
+ return renderHook(() =>
+ useProcessNewDecimalValue(
+ assetDecimals,
+ isFiatPrimary,
+ tokenToFiatConversionRate,
+ ),
+ );
+};
+
+describe('useProcessNewDecimalValue', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('Fiat is primary', () => {
+ const {
+ result: { current: processingFunction },
+ } = renderUseProcessNewDecimalValue(6, false, new Numeric(0.5, 10));
+
+ expect(processingFunction(1)).toStrictEqual({
+ newFiatDecimalValue: '1.00',
+ newTokenDecimalValue: '2',
+ });
+ expect(processingFunction(0)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '0',
+ });
+ expect(processingFunction(1.66666666)).toStrictEqual({
+ newFiatDecimalValue: '1.67',
+ newTokenDecimalValue: '3.333333',
+ });
+
+ expect(processingFunction(1.123456789)).toStrictEqual({
+ newFiatDecimalValue: '1.12',
+ newTokenDecimalValue: '2.246914',
+ });
+ });
+
+ it('Token is primary', () => {
+ const {
+ result: { current: processingFunction },
+ } = renderUseProcessNewDecimalValue(6, true, new Numeric(0.5, 10));
+
+ expect(processingFunction(1)).toStrictEqual({
+ newFiatDecimalValue: '0.50',
+ newTokenDecimalValue: '1',
+ });
+ expect(processingFunction(0)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '0',
+ });
+ expect(processingFunction(1.66666666)).toStrictEqual({
+ newFiatDecimalValue: '0.83',
+ newTokenDecimalValue: '1.666667',
+ });
+ expect(processingFunction(1.123456789)).toStrictEqual({
+ newFiatDecimalValue: '0.56',
+ newTokenDecimalValue: '1.123457',
+ });
+ });
+
+ it('Fiat is primary; conversion is zero', () => {
+ const {
+ result: { current: processingFunction },
+ } = renderUseProcessNewDecimalValue(6, false, new Numeric(0, 10));
+
+ expect(processingFunction(1)).toStrictEqual({
+ newFiatDecimalValue: '1.00',
+ newTokenDecimalValue: 'Infinity',
+ });
+ expect(processingFunction(0)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: 'NaN',
+ });
+ expect(processingFunction(1.66666666)).toStrictEqual({
+ newFiatDecimalValue: '1.67',
+ newTokenDecimalValue: 'Infinity',
+ });
+
+ expect(processingFunction(1.123456789)).toStrictEqual({
+ newFiatDecimalValue: '1.12',
+ newTokenDecimalValue: 'Infinity',
+ });
+ });
+
+ it('Token is primary; conversion is zero', () => {
+ const {
+ result: { current: processingFunction },
+ } = renderUseProcessNewDecimalValue(6, true, new Numeric(0, 10));
+
+ expect(processingFunction(1)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '1',
+ });
+ expect(processingFunction(0)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '0',
+ });
+ expect(processingFunction(1.66666666)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '1.666667',
+ });
+ expect(processingFunction(1.123456789)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '1.123457',
+ });
+ });
+
+ it('Fiat is primary; decimals are 0', () => {
+ const {
+ result: { current: processingFunction },
+ } = renderUseProcessNewDecimalValue(0, false, new Numeric(0.5, 10));
+
+ expect(processingFunction(1)).toStrictEqual({
+ newFiatDecimalValue: '1.00',
+ newTokenDecimalValue: '2',
+ });
+ expect(processingFunction(0)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '0',
+ });
+ expect(processingFunction(1.66666666)).toStrictEqual({
+ newFiatDecimalValue: '1.67',
+ newTokenDecimalValue: '3',
+ });
+
+ expect(processingFunction(1.123456789)).toStrictEqual({
+ newFiatDecimalValue: '1.12',
+ newTokenDecimalValue: '2',
+ });
+ });
+
+ it('Token is primary; decimals are 0', () => {
+ const {
+ result: { current: processingFunction },
+ } = renderUseProcessNewDecimalValue(0, true, new Numeric(0.5, 10));
+
+ expect(processingFunction(1)).toStrictEqual({
+ newFiatDecimalValue: '0.50',
+ newTokenDecimalValue: '1',
+ });
+ expect(processingFunction(0)).toStrictEqual({
+ newFiatDecimalValue: '0.00',
+ newTokenDecimalValue: '0',
+ });
+ expect(processingFunction(1.66666666)).toStrictEqual({
+ newFiatDecimalValue: '0.83',
+ newTokenDecimalValue: '2',
+ });
+ expect(processingFunction(1.123456789)).toStrictEqual({
+ newFiatDecimalValue: '0.56',
+ newTokenDecimalValue: '1',
+ });
+ });
+});
diff --git a/ui/components/app/currency-input/hooks/useProcessNewDecimalValue.tsx b/ui/components/app/currency-input/hooks/useProcessNewDecimalValue.tsx
new file mode 100644
index 000000000000..7c3d869d7831
--- /dev/null
+++ b/ui/components/app/currency-input/hooks/useProcessNewDecimalValue.tsx
@@ -0,0 +1,60 @@
+import { useCallback } from 'react';
+import { Numeric } from '../../../../../shared/modules/Numeric';
+
+const MAX_DECIMALS_TOKEN_SECONDARY = 6;
+
+/**
+ * A hook that creates a function which processes a new decimal value and returns the new fiat and token decimal values
+ *
+ * @param assetDecimals - The number of decimals that asset supports
+ * @param isTokenPrimary - If the token is the input currency
+ * @param tokenToFiatConversionRate - The conversion rate from the asset to the user's fiat currency
+ * @returns A function that processes a new decimal value and returns the new fiat and token decimal values
+ */
+export default function useProcessNewDecimalValue(
+ assetDecimals: number,
+ isTokenPrimary: boolean,
+ tokenToFiatConversionRate: Numeric | undefined,
+) {
+ return useCallback(
+ (newDecimalValue) => {
+ let newFiatDecimalValue, newTokenDecimalValue;
+
+ const truncateToDecimals = (
+ numeric: Numeric,
+ maxDecimals = assetDecimals,
+ ) => {
+ const digitsAfterDecimal = numeric.toString().split('.')[1] || '';
+
+ const maxPossibleDecimals = Math.min(maxDecimals, assetDecimals);
+
+ const digitsCutoff = Math.min(
+ digitsAfterDecimal.length,
+ maxPossibleDecimals,
+ );
+
+ return numeric.toFixed(digitsCutoff);
+ };
+
+ const numericDecimalValue = new Numeric(newDecimalValue, 10);
+
+ if (isTokenPrimary) {
+ newFiatDecimalValue = tokenToFiatConversionRate
+ ? numericDecimalValue.times(tokenToFiatConversionRate).toFixed(2)
+ : undefined;
+ newTokenDecimalValue = truncateToDecimals(numericDecimalValue);
+ } else {
+ newFiatDecimalValue = numericDecimalValue.toFixed(2);
+ newTokenDecimalValue = tokenToFiatConversionRate
+ ? truncateToDecimals(
+ numericDecimalValue.divide(tokenToFiatConversionRate),
+ MAX_DECIMALS_TOKEN_SECONDARY,
+ )
+ : undefined;
+ }
+
+ return { newFiatDecimalValue, newTokenDecimalValue };
+ },
+ [tokenToFiatConversionRate, isTokenPrimary, assetDecimals],
+ );
+}
diff --git a/ui/components/app/currency-input/hooks/useTokenExchangeRate.test.tsx b/ui/components/app/currency-input/hooks/useTokenExchangeRate.test.tsx
new file mode 100644
index 000000000000..f6c933797eae
--- /dev/null
+++ b/ui/components/app/currency-input/hooks/useTokenExchangeRate.test.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { renderHook } from '@testing-library/react-hooks';
+import mockState from '../../../../../test/data/mock-state.json';
+import configureStore from '../../../../store/store';
+import useTokenExchangeRate from './useTokenExchangeRate';
+
+const renderUseTokenExchangeRate = (tokenAddress?: string) => {
+ const state = {
+ ...mockState,
+ metamask: {
+ ...mockState.metamask,
+ currencyRates: {
+ ETH: {
+ conversionRate: 11.1,
+ },
+ },
+ contractExchangeRates: {
+ '0xdAC17F958D2ee523a2206206994597C13D831ec7': 0.5,
+ '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': 3.304588,
+ },
+ providerConfig: {
+ ticker: 'ETH',
+ },
+ },
+ };
+
+ const wrapper = ({ children }: any) => (
+
{children}
+ );
+
+ return renderHook(() => useTokenExchangeRate(tokenAddress), { wrapper });
+};
+
+describe('useProcessNewDecimalValue', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('ERC-20: price is available', () => {
+ const {
+ result: { current: exchangeRate },
+ } = renderUseTokenExchangeRate(
+ '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ );
+
+ expect(String(exchangeRate?.value)).toEqual('5.55');
+ });
+
+ it('ERC-20: price is unavailable', () => {
+ const {
+ result: { current: exchangeRate },
+ } = renderUseTokenExchangeRate(
+ '0x0000000000000000000000000000000000000001',
+ );
+
+ expect(exchangeRate?.value).toBe(undefined);
+ });
+
+ it('native: price is available', () => {
+ const {
+ result: { current: exchangeRate },
+ } = renderUseTokenExchangeRate(undefined);
+
+ expect(String(exchangeRate?.value)).toBe('11.1');
+ });
+});
diff --git a/ui/components/app/currency-input/hooks/useTokenExchangeRate.tsx b/ui/components/app/currency-input/hooks/useTokenExchangeRate.tsx
new file mode 100644
index 000000000000..836a54594495
--- /dev/null
+++ b/ui/components/app/currency-input/hooks/useTokenExchangeRate.tsx
@@ -0,0 +1,39 @@
+import { useMemo } from 'react';
+import { toChecksumAddress } from 'ethereumjs-util';
+import { shallowEqual, useSelector } from 'react-redux';
+import { getTokenExchangeRates } from '../../../../selectors';
+import { Numeric } from '../../../../../shared/modules/Numeric';
+import { getConversionRate } from '../../../../ducks/metamask/metamask';
+
+/**
+ * A hook that returns the exchange rate of the given token –– assumes native if no token address is passed.
+ *
+ * @param tokenAddress - the address of the token. If not provided, the function will return the native exchange rate.
+ * @returns the exchange rate of the token
+ */
+export default function useTokenExchangeRate(
+ tokenAddress?: string,
+): Numeric | undefined {
+ const selectedNativeConversionRate = useSelector(getConversionRate);
+ const nativeConversionRate = new Numeric(selectedNativeConversionRate, 10);
+
+ const contractExchangeRates = useSelector(
+ getTokenExchangeRates,
+ shallowEqual,
+ );
+
+ return useMemo(() => {
+ if (!tokenAddress) {
+ return nativeConversionRate;
+ }
+
+ const contractExchangeRate =
+ contractExchangeRates[toChecksumAddress(tokenAddress)];
+
+ if (!contractExchangeRate) {
+ return undefined;
+ }
+
+ return new Numeric(contractExchangeRate, 10).times(nativeConversionRate);
+ }, [tokenAddress, nativeConversionRate, contractExchangeRates]);
+}
diff --git a/ui/components/app/token-cell/__snapshots__/token-cell.test.js.snap b/ui/components/app/token-cell/__snapshots__/token-cell.test.js.snap
index 93d406a12fb2..e34371e11b7d 100644
--- a/ui/components/app/token-cell/__snapshots__/token-cell.test.js.snap
+++ b/ui/components/app/token-cell/__snapshots__/token-cell.test.js.snap
@@ -15,7 +15,7 @@ exports[`Token Cell should match snapshot 1`] = `
class="mm-box mm-badge-wrapper mm-box--margin-right-3 mm-box--display-inline-block"
>
`;
diff --git a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js
index eb49f74000f9..70b232848d16 100644
--- a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js
+++ b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js
@@ -6,6 +6,7 @@ export default class UserPreferencedCurrencyInput extends PureComponent {
static propTypes = {
useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
sendInputCurrencySwitched: PropTypes.bool,
+ ...CurrencyInput.propTypes,
};
render() {
@@ -18,7 +19,7 @@ export default class UserPreferencedCurrencyInput extends PureComponent {
return (