diff --git a/.storybook/__snapshots__/storyshots.test.js.snap b/.storybook/__snapshots__/storyshots.test.js.snap index 66dd7686221..072ce8f7d78 100644 --- a/.storybook/__snapshots__/storyshots.test.js.snap +++ b/.storybook/__snapshots__/storyshots.test.js.snap @@ -23125,7 +23125,7 @@ exports[` Storyshots Organisms/SwapQuote SwapQuote 1`] = `
- Max TX Fee + Estimated Cost
- 15 minutes + 31 seconds
diff --git a/__tests__/fixtures.js b/__tests__/fixtures.js index 9e69ffa5f3a..5927fa2f247 100644 --- a/__tests__/fixtures.js +++ b/__tests__/fixtures.js @@ -31,7 +31,8 @@ const PAGES = { ADD_ACCOUNT_WEB3: `${FIXTURES_CONST.BASE_URL}/add-account/web3`, SEND: `${FIXTURES_CONST.BASE_URL}/send`, ADD_ACCOUNT: `${FIXTURES_CONST.BASE_URL}/add-account`, - TX_STATUS: `${FIXTURES_CONST.BASE_URL}/tx-status` + TX_STATUS: `${FIXTURES_CONST.BASE_URL}/tx-status`, + SWAP: `${FIXTURES_CONST.BASE_URL}/swap` }; const FIXTURE_ETHEREUM = 'Ethereum'; diff --git a/__tests__/swap-page.po.js b/__tests__/swap-page.po.js new file mode 100644 index 00000000000..0f2075c6bf1 --- /dev/null +++ b/__tests__/swap-page.po.js @@ -0,0 +1,18 @@ +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { FIXTURE_SEND_AMOUNT, PAGES } from './fixtures'; + +export default class SwapPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.SWAP); + } + + async waitPageLoaded(timeout) { + await this.waitForPage(PAGES.SWAP, timeout); + } + + async fillForm() { + await t.typeText(Selector('input[name="swap-from"]').parent(), FIXTURE_SEND_AMOUNT); + } +} diff --git a/__tests__/swap.test.js b/__tests__/swap.test.js new file mode 100644 index 00000000000..f0233713a8a --- /dev/null +++ b/__tests__/swap.test.js @@ -0,0 +1,24 @@ +import { getByText } from '@testing-library/testcafe'; + +import { injectLS } from './clientScripts'; +import { FIXTURE_LOCALSTORAGE_WITH_ONE_ACC, FIXTURES_CONST, PAGES } from './fixtures'; +import SwapPage from './swap-page.po'; +import { findByTKey } from './translation-utils'; + +const swapPage = new SwapPage(); + +fixture('Swap') + .clientScripts({ content: injectLS(FIXTURE_LOCALSTORAGE_WITH_ONE_ACC) }) + .page(PAGES.SWAP); + +test('Can get swap quote', async (t) => { + await swapPage.waitPageLoaded(); + + /* Fill out form */ + await swapPage.fillForm(); + await t.wait(FIXTURES_CONST.TIMEOUT); + + // Has received swap quote + const quote = await getByText(findByTKey('YOUR_QUOTE')); + await t.expect(quote).ok(); +}); diff --git a/jest_config/__fixtures__/index.ts b/jest_config/__fixtures__/index.ts index 0686521d2b4..b32cea3e98a 100644 --- a/jest_config/__fixtures__/index.ts +++ b/jest_config/__fixtures__/index.ts @@ -75,5 +75,5 @@ export { fUserActions, fActionTemplates } from './userActions'; export { membershipApiResponse, accountWithMembership } from './membership'; export { default as APP_STATE } from './appState'; -export { fSwapQuote } from './swapQuote'; +export * from './swapQuote'; export { fBalances } from './balances'; diff --git a/jest_config/__fixtures__/swapQuote.ts b/jest_config/__fixtures__/swapQuote.ts index 07f657d4317..d0308a9cb77 100644 --- a/jest_config/__fixtures__/swapQuote.ts +++ b/jest_config/__fixtures__/swapQuote.ts @@ -62,3 +62,67 @@ export const fSwapQuote = { sellTokenToEthRate: '1547.962186957828746096', buyTokenToEthRate: '1' }; + +export const fSwapQuoteReverse = { + price: '1.305257025416429946', + guaranteedPrice: '1.318309595670594246', + to: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', + data: + '0x415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a2000000000000000000000000000000000000000000000000124b8c194d5703b80000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000124b8c194d5703b800000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000de99870435440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000442616c616e6365720000000000000000000000000000000000000000000000000000000000000000124b8c194d5703b80000000000000000000000000000000000000000000000000de998704354400000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000987d7cc04652710b74fff380403f5c02f82e290a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a20000000000000000000000000000000000000000000000000008e1bc9bf04000000000000000000000000000d8d46494e200fa585fc98f86e6a5ea0dc1f18ad00000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a2000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd000000000000000000000000100000000000000000000000000000000000001100000000000000000000000000000000000000000000008c9ad1818260a23bf6', + value: '1318301356235621304', + gas: '310000', + estimatedGas: '310000', + gasPrice: '66000000000', + protocolFee: '0', + minimumProtocolFee: '0', + buyTokenAddress: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', + sellTokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + buyAmount: '1000000000000000000', + sellAmount: '1305248867560021093', + sources: [ + { name: '0x', proportion: '0' }, + { name: 'Uniswap', proportion: '0' }, + { name: 'Uniswap_V2', proportion: '0' }, + { name: 'Eth2Dai', proportion: '0' }, + { name: 'Kyber', proportion: '0' }, + { name: 'Curve', proportion: '0' }, + { name: 'Balancer', proportion: '1' }, + { name: 'Balancer_V2', proportion: '0' }, + { name: 'Bancor', proportion: '0' }, + { name: 'mStable', proportion: '0' }, + { name: 'Mooniswap', proportion: '0' }, + { name: 'Swerve', proportion: '0' }, + { name: 'SnowSwap', proportion: '0' }, + { name: 'SushiSwap', proportion: '0' }, + { name: 'Shell', proportion: '0' }, + { name: 'MultiHop', proportion: '0' }, + { name: 'DODO', proportion: '0' }, + { name: 'DODO_V2', proportion: '0' }, + { name: 'CREAM', proportion: '0' }, + { name: 'LiquidityProvider', proportion: '0' }, + { name: 'CryptoCom', proportion: '0' }, + { name: 'Linkswap', proportion: '0' }, + { name: 'MakerPsm', proportion: '0' }, + { name: 'KyberDMM', proportion: '0' }, + { name: 'Smoothy', proportion: '0' }, + { name: 'Component', proportion: '0' }, + { name: 'Saddle', proportion: '0' }, + { name: 'xSigma', proportion: '0' }, + { name: 'Uniswap_V3', proportion: '0' } + ], + orders: [ + { + makerToken: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', + takerToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + makerAmount: '1002500000000000000', + takerAmount: '1305248867560021093', + fillData: { poolAddress: '0x987d7cc04652710b74fff380403f5c02f82e290a' }, + source: 'Balancer', + sourcePathId: '0x646bfe1bbac7acdc36fb60378450d6f9c56d26c7b6c1e03f142777ee6194a987', + type: 0 + } + ], + allowanceTarget: '0x0000000000000000000000000000000000000000', + sellTokenToEthRate: '1', + buyTokenToEthRate: '0.76609049757173667' +}; diff --git a/src/components/TimeCountdown.tsx b/src/components/TimeCountdown.tsx index bc1d8e0c97e..404b4d01099 100644 --- a/src/components/TimeCountdown.tsx +++ b/src/components/TimeCountdown.tsx @@ -9,7 +9,7 @@ const TimeCountdown = ({ value: number; format?: string[]; }) => { - const timeSince = (v: number) => formatTimeDuration(v, undefined, undefined, { format }); + const timeSince = (v: number) => formatTimeDuration(v, Date.now() / 1000, false, { format }); const [countdown, setCountdown] = useState(timeSince(value)); useInterval( diff --git a/src/features/SwapAssets/SwapAssetsFlow.test.tsx b/src/features/SwapAssets/SwapAssetsFlow.test.tsx index 64b43564bd1..4294f43b45c 100644 --- a/src/features/SwapAssets/SwapAssetsFlow.test.tsx +++ b/src/features/SwapAssets/SwapAssetsFlow.test.tsx @@ -3,8 +3,9 @@ import React from 'react'; import mockAxios from 'jest-mock-axios'; import { fireEvent, simpleRender, waitFor } from 'test-utils'; -import { fAccounts, fAssets, fSwapQuote } from '@fixtures'; +import { fAccounts, fAssets, fSwapQuote, fSwapQuoteReverse } from '@fixtures'; import { StoreContext } from '@services/Store'; +import { truncate } from '@utils'; import SwapAssetsFlow from './SwapAssetsFlow'; @@ -16,7 +17,7 @@ function getComponent() { assets: () => fAssets, accounts: fAccounts, userAssets: fAccounts.flatMap((a) => a.assets), - getDefaultAccount: () => undefined + getDefaultAccount: () => fAccounts[0] } as any) as any } > @@ -25,6 +26,16 @@ function getComponent() { ); } +const tokenResponse = { + data: { + records: [fAssets[0], fAssets[13]].map((a) => ({ + ...a, + symbol: a.ticker, + address: a.type === 'base' ? '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' : a.contractAddress + })) + } +}; + describe('SwapAssetsFlow', () => { afterEach(() => { mockAxios.reset(); @@ -32,15 +43,24 @@ describe('SwapAssetsFlow', () => { it('selects default tokens', async () => { const { getAllByText } = getComponent(); expect(mockAxios.get).toHaveBeenCalledWith('swap/v1/tokens'); - mockAxios.mockResponse({ data: { records: fAssets.map((a) => ({ ...a, symbol: a.ticker })) } }); + mockAxios.mockResponse(tokenResponse); await waitFor(() => expect(getAllByText(fAssets[0].ticker, { exact: false })).toBeDefined()); await waitFor(() => expect(getAllByText(fAssets[13].ticker, { exact: false })).toBeDefined()); }); + it('selects default account', async () => { + const { getByText } = getComponent(); + expect(mockAxios.get).toHaveBeenCalledWith('swap/v1/tokens'); + mockAxios.mockResponse(tokenResponse); + await waitFor(() => + expect(getByText(truncate(fAccounts[0].address), { exact: false })).toBeDefined() + ); + }); + it('calculates and shows to amount', async () => { const { getAllByText, getAllByDisplayValue, container } = getComponent(); expect(mockAxios.get).toHaveBeenCalledWith('swap/v1/tokens'); - mockAxios.mockResponse({ data: { records: fAssets.map((a) => ({ ...a, symbol: a.ticker })) } }); + mockAxios.mockResponse(tokenResponse); await waitFor(() => expect(getAllByText(fAssets[0].ticker, { exact: false })).toBeDefined()); await waitFor(() => expect(getAllByText(fAssets[13].ticker, { exact: false })).toBeDefined()); mockAxios.reset(); @@ -56,4 +76,24 @@ describe('SwapAssetsFlow', () => { expect(getAllByDisplayValue('0.000642566300455615', { exact: false })).toBeDefined() ); }); + + it('calculates and shows from amount', async () => { + const { getAllByText, getAllByDisplayValue, container } = getComponent(); + expect(mockAxios.get).toHaveBeenCalledWith('swap/v1/tokens'); + mockAxios.mockResponse(tokenResponse); + await waitFor(() => expect(getAllByText(fAssets[0].ticker, { exact: false })).toBeDefined()); + await waitFor(() => expect(getAllByText(fAssets[13].ticker, { exact: false })).toBeDefined()); + mockAxios.reset(); + fireEvent.change(container.querySelector('input[name="swap-to"]')!, { + target: { value: '1' } + }); + await waitFor(() => + expect(mockAxios.get).toHaveBeenCalledWith('swap/v1/quote', expect.anything()) + ); + mockAxios.mockResponse({ data: fSwapQuoteReverse }); + await waitFor(() => expect(getAllByDisplayValue('1', { exact: false })).toBeDefined()); + await waitFor(() => + expect(getAllByDisplayValue('1.305248867560021093', { exact: false })).toBeDefined() + ); + }); }); diff --git a/src/features/SwapAssets/SwapAssetsFlow.tsx b/src/features/SwapAssets/SwapAssetsFlow.tsx index 6d5a322fdb0..5d80cd37f2d 100644 --- a/src/features/SwapAssets/SwapAssetsFlow.tsx +++ b/src/features/SwapAssets/SwapAssetsFlow.tsx @@ -61,7 +61,8 @@ const SwapAssetsFlow = (props: RouteComponentProps) => { expiration, approvalTx, isEstimatingGas, - tradeTx + tradeTx, + selectedNetwork }: SwapFormState = formState; const [assetPair, setAssetPair] = useState({}); @@ -109,7 +110,8 @@ const SwapAssetsFlow = (props: RouteComponentProps) => { expiration, approvalTx, isEstimatingGas, - isSubmitting + isSubmitting, + selectedNetwork }, actions: { handleFromAssetSelected, @@ -133,7 +135,9 @@ const SwapAssetsFlow = (props: RouteComponentProps) => { initWith( () => Promise.resolve( - (approvalTx ? [approvalTx, tradeTx] : [tradeTx]).map(appendSender(account.address)) + (approvalTx ? [approvalTx, tradeTx!] : [tradeTx!]).map( + appendSender(account.address) + ) ), account, account.network diff --git a/src/features/SwapAssets/components/SwapAssets.test.tsx b/src/features/SwapAssets/components/SwapAssets.test.tsx index 2d287962da5..1872046c6c4 100644 --- a/src/features/SwapAssets/components/SwapAssets.test.tsx +++ b/src/features/SwapAssets/components/SwapAssets.test.tsx @@ -10,6 +10,7 @@ import { LAST_CHANGED_AMOUNT } from '../types'; import SwapAssets from './SwapAssets'; const defaultProps: React.ComponentProps = { + selectedNetwork: 'Ethereum', account: fAccounts[0], assets: fAssets, fromAsset: fAssets[0], diff --git a/src/features/SwapAssets/components/SwapAssets.tsx b/src/features/SwapAssets/components/SwapAssets.tsx index 1d0ffe41287..379b9beebd0 100644 --- a/src/features/SwapAssets/components/SwapAssets.tsx +++ b/src/features/SwapAssets/components/SwapAssets.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; import { @@ -18,15 +17,15 @@ import { import { useRates } from '@services/Rates'; import { StoreContext } from '@services/Store'; import { - AppState, getBaseAssetByNetwork, getIsDemoMode, getSettings, - selectDefaultNetwork + selectNetwork, + useSelector } from '@store'; import { SPACING } from '@theme'; import translate, { translateRaw } from '@translations'; -import { Asset, ExtendedAsset, ISwapAsset, Network, StoreAccount } from '@types'; +import { Asset, ISwapAsset, StoreAccount } from '@types'; import { bigify, getTimeDifference, totalTxFeeToString, useInterval } from '@utils'; import { useDebounce } from '@vendor'; @@ -42,7 +41,7 @@ const StyledButton = styled(Button)` } `; -type ISwapProps = SwapFormState & { +type Props = SwapFormState & { isSubmitting: boolean; txError?: CustomError; onSuccess(): void; @@ -59,6 +58,7 @@ type ISwapProps = SwapFormState & { const SwapAssets = (props: Props) => { const { + selectedNetwork, account, fromAmount, toAmount, @@ -87,12 +87,14 @@ const SwapAssets = (props: Props) => { tradeGasLimit, gasPrice, isEstimatingGas, - expiration, - isDemoMode, - baseAsset, - settings + expiration } = props; + const settings = useSelector(getSettings); + const isDemoMode = useSelector(getIsDemoMode); + const network = useSelector(selectNetwork(selectedNetwork)); + const baseAsset = useSelector(getBaseAssetByNetwork(network)); + const [isExpired, setIsExpired] = useState(false); const { accounts, userAssets } = useContext(StoreContext); const { getAssetRate } = useRates(); @@ -145,18 +147,29 @@ const SwapAssets = (props: Props) => { calculateNewToAmount(fromAmount); }, [toAsset]); + const estimatedGasFee = + gasPrice && + tradeGasLimit && + totalTxFeeToString( + gasPrice, + bigify(tradeGasLimit).plus(approvalGasLimit ? approvalGasLimit : 0) + ); + + // Accounts with a balance of the chosen asset and base asset + const filteredAccounts = fromAsset + ? getAccountsWithAssetBalance(accounts, fromAsset, fromAmount, baseAsset.uuid, estimatedGasFee) + : []; + useEffect(() => { if ( fromAmount && fromAsset && account && - !getAccountsWithAssetBalance(accounts, fromAsset, fromAmount).find( - (a) => a.uuid === account.uuid - ) + !filteredAccounts.find((a) => a.uuid === account.uuid) ) { handleAccountSelected(undefined); } - }, [fromAsset, fromAmount]); + }, [fromAsset, fromAmount, gasPrice, tradeGasLimit, approvalGasLimit]); useEffect(() => { handleRefreshQuote(); @@ -166,14 +179,6 @@ const SwapAssets = (props: Props) => { handleGasLimitEstimation(); }, [approvalTx, account]); - const estimatedGasFee = - gasPrice && - tradeGasLimit && - totalTxFeeToString( - gasPrice, - bigify(tradeGasLimit).plus(approvalGasLimit ? approvalGasLimit : 0) - ); - useInterval( () => { if (!expiration) { @@ -189,11 +194,6 @@ const SwapAssets = (props: Props) => { [expiration] ); - // Accounts with a balance of the chosen asset - const filteredAccounts = fromAsset - ? getAccountsWithAssetBalance(accounts, fromAsset, fromAmount, baseAsset.uuid, estimatedGasFee) - : []; - return ( <> @@ -242,6 +242,7 @@ const SwapAssets = (props: Props) => { { ); }; -const mapStateToProps = (state: AppState) => { - const network = selectDefaultNetwork(state) as Network; - - return { - isDemoMode: getIsDemoMode(state), - baseAsset: getBaseAssetByNetwork(network)(state) as ExtendedAsset, - settings: getSettings(state) - }; -}; - -const connector = connect(mapStateToProps); -type Props = ConnectedProps & ISwapProps; - -export default connector(SwapAssets); +export default SwapAssets; diff --git a/src/features/SwapAssets/components/SwapQuote.stories.tsx b/src/features/SwapAssets/components/SwapQuote.stories.tsx index 4935a0f3717..c12930bf6f3 100644 --- a/src/features/SwapAssets/components/SwapQuote.stories.tsx +++ b/src/features/SwapAssets/components/SwapQuote.stories.tsx @@ -1,8 +1,6 @@ import React from 'react'; -import { sub } from 'date-fns'; - -import { DAIUUID, ETHUUID } from '@config'; +import { DAIUUID, DEX_TRADE_EXPIRATION, ETHUUID } from '@config'; import { fAssets, fSettings } from '@fixtures'; import { ISwapAsset, TTicker, TUuid } from '@types'; import { bigify, noOp } from '@utils'; @@ -30,7 +28,6 @@ const defaultProps = { baseAssetRate: bigify('123'), settings: fSettings, isExpired: false, - expiration: sub(new Date(), { minutes: 15 }), // Component displays a time-from so we provide a relative date. estimatedGasFee: '123123', handleRefreshQuote: noOp }; @@ -38,7 +35,7 @@ const defaultProps = { export default { title: 'Organisms/SwapQuote', component: SwapQuote }; const Template = (args: React.ComponentProps) => { - return ; + return ; }; export const Hello = Template.bind({}); diff --git a/src/features/SwapAssets/components/SwapQuote.tsx b/src/features/SwapAssets/components/SwapQuote.tsx index 8d0d6bee864..2d892dd9846 100644 --- a/src/features/SwapAssets/components/SwapQuote.tsx +++ b/src/features/SwapAssets/components/SwapQuote.tsx @@ -94,7 +94,7 @@ export const SwapQuote = ({ - {translateRaw('MAX_TX_FEE')} + {translateRaw('ESTIMATED_COST')} {!isExpired ? ( - + ) : ( {translateRaw('EXPIRED')} diff --git a/src/features/SwapAssets/stateFormFactory.tsx b/src/features/SwapAssets/stateFormFactory.tsx index ca5f553484e..74aa075ea3c 100644 --- a/src/features/SwapAssets/stateFormFactory.tsx +++ b/src/features/SwapAssets/stateFormFactory.tsx @@ -1,9 +1,9 @@ import axios from 'axios'; -import { DEFAULT_NETWORK_TICKER, MYC_DEX_COMMISSION_RATE } from '@config'; +import { MYC_DEX_COMMISSION_RATE } from '@config'; import { checkRequiresApproval } from '@helpers'; import { DexAsset, DexService, getGasEstimate } from '@services'; -import { selectDefaultNetwork, useSelector } from '@store'; +import { selectNetwork, useSelector } from '@store'; import translate from '@translations'; import { ISwapAsset, ITxGasLimit, Network, StoreAccount } from '@types'; import { @@ -20,6 +20,7 @@ import { import { LAST_CHANGED_AMOUNT, SwapFormState } from './types'; const swapFormInitialState = { + selectedNetwork: 'Ethereum', assets: [], account: undefined, fromAsset: undefined, @@ -33,8 +34,10 @@ const swapFormInitialState = { lastChangedAmount: LAST_CHANGED_AMOUNT.FROM }; +const BASE_ASSET_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + const SwapFormFactory: TUseStateReducerFactory = ({ state, setState }) => { - const network = useSelector(selectDefaultNetwork) as Network; + const network = useSelector(selectNetwork(state.selectedNetwork)) as Network; const fetchSwapAssets = async () => { try { @@ -42,22 +45,20 @@ const SwapFormFactory: TUseStateReducerFactory = ({ state, setSta if (assets.length < 1) return; // sort assets alphabetically const newAssets = assets - .map( - ({ symbol, decimals, ...asset }: DexAsset): ISwapAsset => ({ - ...asset, - ticker: symbol, - decimal: decimals, - uuid: - symbol === DEFAULT_NETWORK_TICKER - ? generateAssetUUID(network.chainId) - : generateAssetUUID(network.chainId, asset.address) - }) - ) + .map(({ symbol, decimals, ...asset }: DexAsset) => ({ + ...asset, + ticker: symbol, + decimal: decimals, + uuid: + asset.address === BASE_ASSET_ADDRESS + ? generateAssetUUID(network.chainId) + : generateAssetUUID(network.chainId, asset.address) + })) .sort((asset1: ISwapAsset, asset2: ISwapAsset) => (asset1.ticker as string).localeCompare(asset2.ticker) ); // set fromAsset to default (ETH) - const fromAsset = newAssets.find((x: ISwapAsset) => x.ticker === network.baseUnit); + const fromAsset = newAssets.find((x) => x.address === BASE_ASSET_ADDRESS); const toAsset = newAssets[0]; return [newAssets, fromAsset, toAsset]; } catch (e) { @@ -128,6 +129,7 @@ const SwapFormFactory: TUseStateReducerFactory = ({ state, setSta })); const { price, sellAmount, ...rest } = await DexService.instance.getOrderDetailsTo( + network, account?.address, fromAsset, toAsset, @@ -197,6 +199,7 @@ const SwapFormFactory: TUseStateReducerFactory = ({ state, setSta })); const { price, buyAmount, ...rest } = await DexService.instance.getOrderDetailsFrom( + network, account?.address, fromAsset, toAsset, @@ -274,8 +277,10 @@ const SwapFormFactory: TUseStateReducerFactory = ({ state, setSta approvalTx && (await checkRequiresApproval(network, approvalTx.to!, account.address, approvalTx.data!)); + const { type, ...tx } = approvalTx; + const approvalGasLimit = inputGasLimitToHex( - requiresApproval ? await getGasEstimate(network, approvalTx!) : '0' + requiresApproval ? await getGasEstimate(network, tx!) : '0' ) as ITxGasLimit; setState((prevState: SwapFormState) => ({ diff --git a/src/features/SwapAssets/types.ts b/src/features/SwapAssets/types.ts index fd937dcf315..9e55176deaa 100644 --- a/src/features/SwapAssets/types.ts +++ b/src/features/SwapAssets/types.ts @@ -1,6 +1,15 @@ import { BigNumber } from 'bignumber.js'; -import { ISwapAsset, ITxGasLimit, ITxGasPrice, ITxObject, ITxStatus, StoreAccount } from '@types'; +import { + ISwapAsset, + ITxGasLimit, + ITxGasPrice, + ITxObject, + ITxStatus, + ITxType, + NetworkId, + StoreAccount +} from '@types'; export enum LAST_CHANGED_AMOUNT { FROM = 'FROM_AMOUNT', @@ -25,6 +34,7 @@ export interface SwapState { } export interface SwapFormState { + selectedNetwork: NetworkId; account: StoreAccount; assets: ISwapAsset[]; fromAsset: ISwapAsset; @@ -42,9 +52,11 @@ export interface SwapFormState { gasPrice?: ITxGasPrice; approvalGasLimit?: ITxGasLimit; tradeGasLimit?: ITxGasLimit; - approvalTx?: Partial; + approvalTx?: Pick & { + type: ITxType; + }; expiration?: number; - tradeTx?: Partial; + tradeTx?: Pick & { type: ITxType }; } export interface IAssetPair { diff --git a/src/hooks/useTxMulti/useTxMulti.spec.tsx b/src/hooks/useTxMulti/useTxMulti.spec.tsx index 3851ce3416f..780a38f9988 100644 --- a/src/hooks/useTxMulti/useTxMulti.spec.tsx +++ b/src/hooks/useTxMulti/useTxMulti.spec.tsx @@ -193,17 +193,15 @@ describe('useTxMulti', () => { await waitFor(() => expect(mockDispatch).toHaveBeenCalledWith( actionWithPayload({ - ...fAccount, - transactions: expect.arrayContaining([ - expect.objectContaining({ - amount: '0.0', - asset: fAssets[1], - baseAsset: fAssets[1], - hash: '0x1', - txType: ITxType.APPROVAL, - status: ITxStatus.PENDING - }) - ]) + account: fAccount, + tx: expect.objectContaining({ + amount: '0.0', + asset: fAssets[1], + baseAsset: fAssets[1], + hash: '0x1', + txType: ITxType.APPROVAL, + status: ITxStatus.PENDING + }) }) ) ); @@ -211,17 +209,15 @@ describe('useTxMulti', () => { await waitFor(() => expect(mockDispatch).toHaveBeenCalledWith( actionWithPayload({ - ...fAccount, - transactions: expect.arrayContaining([ - expect.objectContaining({ - amount: '0.0', - asset: fAssets[1], - baseAsset: fAssets[1], - hash: '0x2', - txType: ITxType.PURCHASE_MEMBERSHIP, - status: ITxStatus.PENDING - }) - ]) + account: fAccount, + tx: expect.objectContaining({ + amount: '0.0', + asset: fAssets[1], + baseAsset: fAssets[1], + hash: '0x2', + txType: ITxType.PURCHASE_MEMBERSHIP, + status: ITxStatus.PENDING + }) }) ) ); diff --git a/src/services/ApiService/Dex/Dex.spec.ts b/src/services/ApiService/Dex/Dex.spec.ts index 7041f29ad3f..24449ce9736 100644 --- a/src/services/ApiService/Dex/Dex.spec.ts +++ b/src/services/ApiService/Dex/Dex.spec.ts @@ -1,7 +1,7 @@ import mockAxios from 'jest-mock-axios'; -import { fAssets, fRopDAI, fSwapQuote } from '@fixtures'; -import { ITxData, ITxGasLimit, ITxGasPrice, ITxToAddress, ITxType, ITxValue } from '@types'; +import { fAssets, fNetwork, fRopDAI, fSwapQuote } from '@fixtures'; +import { ITxData, ITxGasPrice, ITxToAddress, ITxType, ITxValue } from '@types'; import { DexService } from '.'; import { formatTradeTx } from './Dex'; @@ -12,13 +12,19 @@ describe('SwapFlow', () => { }); describe('getOrderDetails', () => { it('returns the expected two transactions for a multi tx swap', async () => { - const promise = DexService.instance.getOrderDetailsFrom(null, fRopDAI, fAssets[0], '1'); + const promise = DexService.instance.getOrderDetailsFrom( + fNetwork, + null, + fRopDAI, + fAssets[0], + '1' + ); mockAxios.mockResponse({ data: { ...fSwapQuote } }); const result = await promise; expect(result.approvalTx).toStrictEqual({ - chainId: 1, + chainId: fNetwork.chainId, data: '0x095ea7b3000000000000000000000000def1c0ded9bec7f1a1670819833240f027b25eff0000000000000000000000000000000000000000000000000de0b6b3a7640000', from: undefined, @@ -28,11 +34,10 @@ describe('SwapFlow', () => { value: '0x0' }); expect(result.tradeTx).toStrictEqual({ - chainId: 1, + chainId: fNetwork.chainId, data: '0xd9627aa400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000002429108b8f331000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee869584cd0000000000000000000000001000000000000000000000000000000000000011000000000000000000000000000000000000000000000096596a1ef6601a8b3a', gasPrice: '0x23db1d8400', - gasLimit: '0x21340', to: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', type: 'SWAP', value: '0x0' @@ -47,14 +52,13 @@ describe('SwapFlow', () => { to: '0xA65440C4CC83D70b44cF244a0da5373acA16a9cb' as ITxToAddress, data: '0x5d46ec340000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000d8775f648430679a709e98d2b0cb6250d2887ef00000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000e807dc3fe542f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002a1530c4c41db0b0b2bb646cb5eb1a67b715866700000000000000000000000000000000000000000000000000000000000000010000000000000000000000002a1530c4c41db0b0b2bb646cb5eb1a67b715866700000000000000000000000000000000000000000000000000000000000000a4ddf7e1a700000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000e807dc3fe542f0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000005e6275fe0000000000000000000000000d8775f648430679a709e98d2b0cb6250d2887ef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000' as ITxData, value: '50000' as ITxValue, - gasLimit: '0x21340' as ITxGasLimit, - gasPrice: '0x23db1d8400' as ITxGasPrice + gasPrice: '0x23db1d8400' as ITxGasPrice, + chainId: 1 }) ).toEqual({ to: '0xA65440C4CC83D70b44cF244a0da5373acA16a9cb' as ITxToAddress, data: '0x5d46ec340000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000d8775f648430679a709e98d2b0cb6250d2887ef00000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000e807dc3fe542f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002a1530c4c41db0b0b2bb646cb5eb1a67b715866700000000000000000000000000000000000000000000000000000000000000010000000000000000000000002a1530c4c41db0b0b2bb646cb5eb1a67b715866700000000000000000000000000000000000000000000000000000000000000a4ddf7e1a700000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000e807dc3fe542f0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000005e6275fe0000000000000000000000000d8775f648430679a709e98d2b0cb6250d2887ef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000' as ITxData, value: '0xc350' as ITxValue, - gasLimit: '0x21340' as ITxGasLimit, gasPrice: '0x23db1d8400' as ITxGasPrice, chainId: 1, type: ITxType.SWAP diff --git a/src/services/ApiService/Dex/Dex.ts b/src/services/ApiService/Dex/Dex.ts index eb9af314e0a..98f843108fc 100644 --- a/src/services/ApiService/Dex/Dex.ts +++ b/src/services/ApiService/Dex/Dex.ts @@ -3,7 +3,6 @@ import axios, { AxiosInstance } from 'axios'; import { DEFAULT_ASSET_DECIMAL, - DEFAULT_NETWORK_CHAINID, DEX_BASE_URL, DEX_FEE_RECIPIENT, DEX_TRADE_EXPIRATION, @@ -17,6 +16,7 @@ import { ITxObject, ITxType, ITxValue, + Network, TAddress, TTicker } from '@types'; @@ -60,20 +60,23 @@ export default class DexService { }; public getOrderDetailsFrom = async ( + network: Network, account: string | null, from: ISwapAsset, to: ISwapAsset, fromAmount: string - ) => this.getOrderDetails(account, from, to, fromAmount); + ) => this.getOrderDetails(network, account, from, to, fromAmount); public getOrderDetailsTo = async ( + network: Network, account: string | null, from: ISwapAsset, to: ISwapAsset, toAmount: string - ) => this.getOrderDetails(account, from, to, undefined, toAmount); + ) => this.getOrderDetails(network, account, from, to, undefined, toAmount); private getOrderDetails = async ( + network: Network, account: string | null, sellToken: ISwapAsset, buyToken: ISwapAsset, @@ -96,7 +99,8 @@ export default class DexService { : undefined, feeRecipient: DEX_FEE_RECIPIENT, buyTokenPercentageFee: MYC_DEX_COMMISSION_RATE, - takerAddress: account ? account : undefined + takerAddress: account ? account : undefined, + skipValidation: true }, cancelToken: new CancelToken(function executor(c) { // An executor function receives a cancel function as a parameter @@ -112,13 +116,15 @@ export default class DexService { spenderAddress: data.allowanceTarget, hexGasPrice: addHexPrefix(bigify(data.gasPrice).toString(16)) as ITxGasPrice, baseTokenAmount: bigify(data.sellAmount), - chainId: DEFAULT_NETWORK_CHAINID + chainId: network.chainId }), type: ITxType.APPROVAL } : undefined; - const tradeGasLimit = addHexPrefix(bigify(data.gas).toString(16)) as ITxGasLimit; + const tradeGasLimit = addHexPrefix( + bigify(data.gas).multipliedBy(1.2).integerValue(7).toString(16) + ) as ITxGasLimit; return { price: bigify(data.price), @@ -137,8 +143,8 @@ export default class DexService { to: data.to, data: data.data, gasPrice: addHexPrefix(bigify(data.gasPrice).toString(16)) as ITxGasPrice, - gasLimit: tradeGasLimit, - value: data.value + value: data.value, + chainId: network.chainId }) }; }; @@ -149,15 +155,14 @@ export const formatTradeTx = ({ data, value, gasPrice, - gasLimit -}: Pick) => { + chainId +}: Pick) => { return { to, data, value: addHexPrefix(bigify(value || '0').toString(16)) as ITxValue, - chainId: DEFAULT_NETWORK_CHAINID, + chainId, gasPrice, - gasLimit, type: ITxType.SWAP }; }; diff --git a/src/services/Store/Account/useAccounts.spec.tsx b/src/services/Store/Account/useAccounts.spec.tsx index 9e6b8a53843..e4a05f27d08 100644 --- a/src/services/Store/Account/useAccounts.spec.tsx +++ b/src/services/Store/Account/useAccounts.spec.tsx @@ -70,8 +70,8 @@ describe('useAccounts', () => { result.current.addTxToAccount(fAccounts[0], fTxReceipt); expect(mockDispatch).toHaveBeenCalledWith( actionWithPayload({ - ...fAccounts[0], - transactions: [fTxReceipt] + account: fAccounts[0], + tx: fTxReceipt }) ); }); diff --git a/src/services/Store/Account/useAccounts.tsx b/src/services/Store/Account/useAccounts.tsx index bba49c074dc..00239c511e3 100644 --- a/src/services/Store/Account/useAccounts.tsx +++ b/src/services/Store/Account/useAccounts.tsx @@ -2,6 +2,7 @@ import { useSelector } from 'react-redux'; import { addAccounts, + addTxToAccount as addTxToAccountRedux, destroyAccount, getAccounts, updateAccount as updateAccountRedux, @@ -39,13 +40,8 @@ function useAccounts() { const updateAccount = (_: TUuid, account: IAccount) => dispatch(updateAccountRedux(account)); - const addTxToAccount = (accountData: IAccount, newTx: ITxReceipt) => { - const newAccountData = { - ...accountData, - transactions: [...accountData.transactions.filter((tx) => tx.hash !== newTx.hash), newTx] - }; - updateAccount(accountData.uuid, newAccountData); - }; + const addTxToAccount = (account: IAccount, tx: ITxReceipt) => + dispatch(addTxToAccountRedux({ account, tx })); const removeTxFromAccount = (accountData: IAccount, tx: ITxReceipt) => { const newAccountData = { diff --git a/src/services/Store/StoreProvider.tsx b/src/services/Store/StoreProvider.tsx index 51561699c80..86bd3811f2a 100644 --- a/src/services/Store/StoreProvider.tsx +++ b/src/services/Store/StoreProvider.tsx @@ -14,7 +14,6 @@ import { fetchAssets, fetchMemberships, isMyCryptoMember, - scanTokens, selectTxsByStatus, useDispatch, useSelector @@ -28,7 +27,6 @@ import { IAccountAdditionData, IPendingTxReceipt, ITxStatus, - ITxType, Network, NetworkId, StoreAccount, @@ -58,12 +56,7 @@ import { getNewDefaultAssetTemplateByNetwork, getTotalByAsset, useAssets } from import { getAccountsAssetsBalances } from './BalanceService'; import { useContacts } from './Contact'; import { findMultipleNextUnusedDefaultLabels } from './Contact/helpers'; -import { - getStoreAccounts, - getTxsFromAccount, - isNotExcludedAsset, - isTokenMigration -} from './helpers'; +import { getStoreAccounts, getTxsFromAccount, isNotExcludedAsset } from './helpers'; import { getNetworkById, useNetworks } from './Network'; import { useSettings } from './Settings'; @@ -238,14 +231,6 @@ export const StoreProvider: React.FC = ({ children }) => { txResponse.blockNumber ); addTxToAccount(senderAccount, finishedTxReceipt); - if ( - finishedTxReceipt.txType === ITxType.DEFIZAP || - isTokenMigration(finishedTxReceipt.txType) - ) { - dispatch(scanTokens({ accounts: [storeAccount] })); - } else if (finishedTxReceipt.txType === ITxType.PURCHASE_MEMBERSHIP) { - dispatch(fetchMemberships([storeAccount])); - } }); }); }); diff --git a/src/services/Store/store/account.slice.test.ts b/src/services/Store/store/account.slice.test.ts index 2c454bb8b71..a8c342dd6e0 100644 --- a/src/services/Store/store/account.slice.test.ts +++ b/src/services/Store/store/account.slice.test.ts @@ -1,17 +1,22 @@ import { BigNumber } from '@ethersproject/bignumber'; -import { mockAppState } from 'test-utils'; +import { expectSaga, mockAppState } from 'test-utils'; import { ETHUUID, REPV2UUID } from '@config'; -import { fAccount, fAccounts, fSettings, fTransaction } from '@fixtures'; -import { IAccount, ITxReceipt, TUuid } from '@types'; +import { fAccount, fAccounts, fSettings, fTransaction, fTxReceipt } from '@fixtures'; +import { IAccount, ITxReceipt, ITxStatus, ITxType, TUuid } from '@types'; import { + addTxToAccount, + addTxToAccountWorker, getAccounts, initialState, selectAccountTxs, selectCurrentAccounts, - default as slice + default as slice, + updateAccount } from './account.slice'; +import { fetchMemberships } from './membership.slice'; +import { scanTokens } from './tokenScanning.slice'; const reducer = slice.reducer; const { create, createMany, destroy, update, updateMany, reset, updateAssets } = slice.actions; @@ -157,4 +162,49 @@ describe('AccountSlice', () => { } ]); }); + + describe('addTxToAccountWorker', () => { + it('updates account with tx', () => { + return expectSaga(addTxToAccountWorker, addTxToAccount({ account: fAccount, tx: fTxReceipt })) + .put(updateAccount({ ...fAccount, transactions: [fTxReceipt] })) + .silentRun(); + }); + + it('scans for tokens if tx is a swap', () => { + return expectSaga( + addTxToAccountWorker, + addTxToAccount({ + account: fAccount, + tx: { ...fTxReceipt, txType: ITxType.SWAP, status: ITxStatus.SUCCESS } + }) + ) + .put(scanTokens({ accounts: [fAccount] })) + .silentRun(); + }); + + it('scans for membership if tx is a membership purchase', () => { + return expectSaga( + addTxToAccountWorker, + addTxToAccount({ + account: fAccount, + tx: { ...fTxReceipt, txType: ITxType.PURCHASE_MEMBERSHIP, status: ITxStatus.SUCCESS } + }) + ) + .put(fetchMemberships([fAccount])) + .silentRun(); + }); + + it('overwrites existing tx', () => { + const tx = { ...fTxReceipt, status: ITxStatus.SUCCESS }; + return expectSaga( + addTxToAccountWorker, + addTxToAccount({ + account: { ...fAccount, transactions: [fTxReceipt] }, + tx + }) + ) + .put(updateAccount({ ...fAccount, transactions: [tx] })) + .silentRun(); + }); + }); }); diff --git a/src/services/Store/store/account.slice.ts b/src/services/Store/store/account.slice.ts index 5b577762ec2..03cd9566cfd 100644 --- a/src/services/Store/store/account.slice.ts +++ b/src/services/Store/store/account.slice.ts @@ -1,21 +1,26 @@ import { BigNumber as EthersBN } from '@ethersproject/bignumber'; import { createAction, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { put, select, takeLatest } from 'redux-saga/effects'; +import { all, put, select, takeLatest } from 'redux-saga/effects'; import { AssetBalanceObject, ExtendedAsset, IAccount, IProvidersMappings, + ITxReceipt, ITxStatus, + ITxType, LSKeys, TUuid } from '@types'; import { findIndex, propEq } from '@vendor'; +import { isTokenMigration } from '../helpers'; import { getAssetByUUID } from './asset.slice'; +import { fetchMemberships } from './membership.slice'; import { getAppState } from './selectors'; import { addAccountsToFavorites, getFavorites, getIsDemoMode } from './settings.slice'; +import { scanTokens } from './tokenScanning.slice'; export const initialState = [] as IAccount[]; @@ -135,12 +140,18 @@ export const getAccountsAssetsMappings = createSelector([getAccountsAssets], (as * Actions */ export const addAccounts = createAction(`${slice.name}/addAccounts`); +export const addTxToAccount = createAction<{ account: IAccount; tx: ITxReceipt }>( + `${slice.name}/addTxToAccount` +); /** * Sagas */ export function* accountsSaga() { - yield takeLatest(addAccounts.type, handleAddAccounts); + yield all([ + takeLatest(addAccounts.type, handleAddAccounts), + takeLatest(addTxToAccount.type, addTxToAccountWorker) + ]); } export function* handleAddAccounts({ payload }: PayloadAction) { @@ -154,3 +165,27 @@ export function* handleAddAccounts({ payload }: PayloadAction) { yield put(addAccountsToFavorites(payload.map(({ uuid }) => uuid))); } } + +export function* addTxToAccountWorker({ + payload: { account, tx: newTx } +}: PayloadAction<{ account: IAccount; tx: ITxReceipt }>) { + const newAccountData = { + ...account, + transactions: [...account.transactions.filter((tx) => tx.hash !== newTx.hash), newTx] + }; + yield put(updateAccount(newAccountData)); + + if (newTx.status !== ITxStatus.SUCCESS) { + return; + } + + if ( + newTx.txType === ITxType.DEFIZAP || + isTokenMigration(newTx.txType) || + newTx.txType === ITxType.SWAP + ) { + yield put(scanTokens({ accounts: [account] })); + } else if (newTx.txType === ITxType.PURCHASE_MEMBERSHIP) { + yield put(fetchMemberships([account])); + } +} diff --git a/src/services/Store/store/asset.slice.ts b/src/services/Store/store/asset.slice.ts index d846d06e5ca..933725dc818 100644 --- a/src/services/Store/store/asset.slice.ts +++ b/src/services/Store/store/asset.slice.ts @@ -4,7 +4,7 @@ import { all, call, put, takeLatest } from 'redux-saga/effects'; import { EXCLUDED_ASSETS } from '@config'; import { MyCryptoApiService } from '@services'; import { ExtendedAsset, LSKeys, Network, TUuid } from '@types'; -import { filter, find, findIndex, map, mergeRight, pipe, propEq, toPairs } from '@vendor'; +import { filter, findIndex, map, mergeRight, pipe, propEq, toPairs } from '@vendor'; import { initialLegacyState } from './legacy.initialState'; import { appReset } from './root.reducer'; @@ -80,7 +80,7 @@ export default slice; export const getAssets = createSelector([getAppState], (s) => s[slice.name]); export const getBaseAssetByNetwork = (network: Network) => - createSelector(getAssets, find(propEq('uuid', network.baseAsset))); + createSelector(getAssets, (assets) => assets.find((asset) => asset.uuid === network.baseAsset)!); export const getAssetByUUID = (uuid: TUuid) => createSelector([getAssets], (a) => a.find((asset) => asset.uuid === uuid)); diff --git a/src/services/Store/store/index.ts b/src/services/Store/store/index.ts index 4bae6b301c1..81a17ae8a0d 100644 --- a/src/services/Store/store/index.ts +++ b/src/services/Store/store/index.ts @@ -23,7 +23,8 @@ export { addAccounts, selectCurrentAccounts, selectAccountTxs, - selectTxsByStatus + selectTxsByStatus, + addTxToAccount } from './account.slice'; export { createContact, diff --git a/src/services/Store/store/network.slice.ts b/src/services/Store/store/network.slice.ts index e36a4847156..027f79fd36f 100644 --- a/src/services/Store/store/network.slice.ts +++ b/src/services/Store/store/network.slice.ts @@ -4,7 +4,7 @@ import { all, call, put, select, takeLatest } from 'redux-saga/effects'; import { DEFAULT_NETWORK } from '@config'; import { EthersJS } from '@services/EthService/network/ethersJsProvider'; import { LSKeys, Network, NetworkId } from '@types'; -import { find, findIndex, propEq } from '@vendor'; +import { findIndex, propEq } from '@vendor'; import { initialLegacyState } from './legacy.initialState'; import { getAppState } from './selectors'; @@ -88,9 +88,7 @@ export default slice; * Selectors */ -// @ts-expect-error: TS fails to infer correct type from find -const findNetwork: (id: NetworkId) => (n: Network[]) => Network = (id: NetworkId) => - find(propEq('id', id)); +const findNetwork = (id: NetworkId) => (networks: Network[]) => networks.find((n) => n.id === id)!; export const selectNetworks = createSelector([getAppState], (s) => s[slice.name]); diff --git a/src/translations/lang/en.json b/src/translations/lang/en.json index 92399a0de6d..438d4e50492 100644 --- a/src/translations/lang/en.json +++ b/src/translations/lang/en.json @@ -975,9 +975,10 @@ "GET_NEW_QUOTE": "Get New Quote", "SWAP_FOR": "Swap $from for $to", "SWAP_AMOUNT_TOOLTIP": "This is confirming the amounts you want to convert and receive.", - "SWAP_TX_FEE_TOOLTIP": "This is an approximate maximum fee based off a combination of network and platform fees.", + "SWAP_TX_FEE_TOOLTIP": "The approximate amount you will pay to complete your swap. Includes platform fees, network fees and the cost to approve a token when necessary.", "SWAP_EXPIRY_TOOLTIP": "Your quoted amount and rate will expire after a time. To refresh this, select \"Get New Quote.\"", "EXPIRED": "Expired", - "SWAP_INSUFFICIENT_FUNDS": "Insufficient funds for transaction" + "SWAP_INSUFFICIENT_FUNDS": "Insufficient funds for transaction", + "ESTIMATED_COST": "Estimated Cost" } -} +} \ No newline at end of file