diff --git a/app/components/OceanInterface/OceanInterface.test.tsx b/app/components/OceanInterface/OceanInterface.test.tsx index cd7b112302..db59cf9f56 100644 --- a/app/components/OceanInterface/OceanInterface.test.tsx +++ b/app/components/OceanInterface/OceanInterface.test.tsx @@ -6,6 +6,7 @@ import { Provider } from "react-redux"; import { SmartBuffer } from 'smart-buffer'; import { RootState } from "../../store"; import { ocean } from "../../store/ocean"; +import { wallet } from "../../store/wallet"; import { OceanInterface } from "./OceanInterface"; jest.mock('../../contexts/WalletContext', () => ({ @@ -21,11 +22,16 @@ describe('oceanInterface', () => { height: 49, transactions: [], err: new Error('An unknown error has occurred') + }, + wallet: { + address: 'bcrt1q6np0fh47ykhznjhrtfvduh73cgjg32yac8t07d', + utxoBalance: '77', + tokens: [] } }; const store = configureStore({ preloadedState: initialState, - reducer: { ocean: ocean.reducer } + reducer: { ocean: ocean.reducer, wallet: wallet.reducer } }) const component = ( @@ -48,11 +54,16 @@ describe('oceanInterface', () => { broadcasted: false, sign: async () => signed }] + }, + wallet: { + address: 'bcrt1q6np0fh47ykhznjhrtfvduh73cgjg32yac8t07d', + utxoBalance: '77', + tokens: [] } }; const store = configureStore({ preloadedState: initialState, - reducer: { ocean: ocean.reducer } + reducer: { ocean: ocean.reducer, wallet: wallet.reducer } }) const component = ( diff --git a/app/components/OceanInterface/OceanInterface.tsx b/app/components/OceanInterface/OceanInterface.tsx index 8a81a07973..ae018282fa 100644 --- a/app/components/OceanInterface/OceanInterface.tsx +++ b/app/components/OceanInterface/OceanInterface.tsx @@ -1,19 +1,24 @@ import { CTransactionSegWit } from '@defichain/jellyfish-transaction/dist' import { WhaleApiClient } from '@defichain/whale-api-client' +import { Transaction } from '@defichain/whale-api-client/dist/api/transactions' import { MaterialIcons } from '@expo/vector-icons' import React, { useCallback, useEffect, useRef, useState } from 'react' import { ActivityIndicator, Animated, Linking, TouchableOpacity, View } from 'react-native' import { useDispatch, useSelector } from 'react-redux' import { Text } from '..' -import { Logging } from '../../api/logging' +import { Logging } from '../../api' import { useWallet } from '../../contexts/WalletContext' import { useWhaleApiClient } from '../../contexts/WhaleContext' +import { getEnvironment } from '../../environment' +import { fetchTokens } from '../../hooks/wallet/TokensAPI' import { RootState } from '../../store' import { firstTransactionSelector, ocean, OceanTransaction } from '../../store/ocean' import { tailwind } from '../../tailwind' import { translate } from '../../translations' const MAX_AUTO_RETRY = 1 +const MAX_TIMEOUT = 300000 +const INTERVAL_TIME = 5000 async function gotoExplorer (txid: string): Promise { // TODO(thedoublejay) explorer URL @@ -37,6 +42,38 @@ async function broadcastTransaction (tx: CTransactionSegWit, client: WhaleApiCli } } +async function waitForTxConfirmation (id: string, client: WhaleApiClient): Promise { + const initialTime = getEnvironment().debug ? 5000 : 30000 + let start = initialTime + + return await new Promise((resolve, reject) => { + let intervalID: number + const callTransaction = (): void => { + client.transactions.get(id).then((tx) => { + if (intervalID !== undefined) { + clearInterval(intervalID) + } + resolve(tx) + }).catch((e) => { + if (start >= MAX_TIMEOUT) { + Logging.error(e) + if (intervalID !== undefined) { + clearInterval(intervalID) + } + reject(e) + } + }) + } + setTimeout(() => { + callTransaction() + intervalID = setInterval(() => { + start += INTERVAL_TIME + callTransaction() + }, INTERVAL_TIME) + }, initialTime) + }) +} + /** * @description - Global component to be used for async calls, network errors etc. This component is positioned above the bottom tab. * Need to get the height of bottom tab via `useBottomTabBarHeight()` hook to be called on screen. @@ -50,6 +87,7 @@ export function OceanInterface (): JSX.Element | null { const { height, err: e } = useSelector((state: RootState) => state.ocean) const transaction = useSelector((state: RootState) => firstTransactionSelector(state.ocean)) const slideAnim = useRef(new Animated.Value(0)).current + const address = useSelector((state: RootState) => state.wallet.address) // state const [tx, setTx] = useState(transaction) const [err, setError] = useState(e) @@ -72,13 +110,30 @@ export function OceanInterface (): JSX.Element | null { transaction.sign(walletContext.get(0)) .then(async signedTx => { setTxid(signedTx.txId) + setTx({ + ...transaction, + title: translate('screens/OceanInterface', 'Broadcasting...') + }) await broadcastTransaction(signedTx, client) + setTx({ + ...transaction, + title: translate('screens/OceanInterface', 'Waiting for confirmation') + }) + + let title + try { + await waitForTxConfirmation(signedTx.txId, client) + title = 'Transaction Completed' + } catch (e) { + Logging.error(e) + title = 'Sent but not confirmed' + } + setTx({ + ...transaction, + broadcasted: true, + title: translate('screens/OceanInterface', title) + }) }) - .then(() => setTx({ - ...transaction, - broadcasted: true, - title: translate('screens/OceanInterface', 'Transaction Sent') - })) .catch((e: Error) => { let errMsg = e.message if (txid !== undefined) { @@ -86,7 +141,10 @@ export function OceanInterface (): JSX.Element | null { } setError(new Error(errMsg)) }) - .finally(() => dispatch(ocean.actions.popTransaction())) // remove the job as soon as completion + .finally(() => { + dispatch(ocean.actions.popTransaction()) + fetchTokens(client, address, dispatch) + }) // remove the job as soon as completion } }, [transaction, walletContext]) @@ -104,16 +162,19 @@ export function OceanInterface (): JSX.Element | null { { err !== undefined ? - : + : } ) } -function TransactionDetail ({ broadcasted, txid, onClose }: { broadcasted: boolean, txid?: string, onClose: () => void }): JSX.Element { - let title = 'Signing...' - if (txid !== undefined) title = 'Broadcasting...' - if (broadcasted) title = 'Transaction Sent' +function TransactionDetail ({ + broadcasted, + txid, + onClose, + title +}: { broadcasted: boolean, txid?: string, onClose: () => void, title?: string }): JSX.Element { + title = title ?? translate('screens/OceanInterface', 'Signing...') return ( <> { @@ -123,7 +184,7 @@ function TransactionDetail ({ broadcasted, txid, onClose }: { broadcasted: boole {translate('screens/OceanInterface', title)} + >{title} { txid !== undefined && await gotoExplorer(txid)} /> @@ -177,7 +238,8 @@ function TransactionIDButton ({ txid, onPress }: { txid: string, onPress?: () => function TransactionCloseButton (props: { onPress: () => void }): JSX.Element { return ( {translate('screens/OceanInterface', 'OK')} diff --git a/app/screens/AppNavigator/screens/Balances/screens/SendScreen.tsx b/app/screens/AppNavigator/screens/Balances/screens/SendScreen.tsx index e0ea93638a..1cd4c0aa23 100644 --- a/app/screens/AppNavigator/screens/Balances/screens/SendScreen.tsx +++ b/app/screens/AppNavigator/screens/Balances/screens/SendScreen.tsx @@ -12,7 +12,7 @@ import { ScrollView, TouchableOpacity, View } from 'react-native' import NumberFormat from 'react-number-format' import { useDispatch, useSelector } from 'react-redux' import { Dispatch } from 'redux' -import { Logging } from '../../../../../api/logging' +import { Logging } from '../../../../../api' import { Text, TextInput } from '../../../../../components' import { Button } from '../../../../../components/Button' import { getTokenIcon } from '../../../../../components/icons/tokens/_index' @@ -20,6 +20,7 @@ import { SectionTitle } from '../../../../../components/SectionTitle' import { AmountButtonTypes, SetAmountButton } from '../../../../../components/SetAmountButton' import { useNetworkContext } from '../../../../../contexts/NetworkContext' import { useWhaleApiClient } from '../../../../../contexts/WhaleContext' +import { useTokensAPI } from '../../../../../hooks/wallet/TokensAPI' import { RootState } from '../../../../../store' import { hasTxQueued, ocean } from '../../../../../store/ocean' import { WalletToken } from '../../../../../store/wallet' @@ -74,7 +75,8 @@ type Props = StackScreenProps export function SendScreen ({ route, navigation }: Props): JSX.Element { const { networkName } = useNetworkContext() const client = useWhaleApiClient() - const [token] = useState(route.params.token) + const tokens = useTokensAPI() + const [token, setToken] = useState(route.params.token) const { control, setValue, formState: { isValid }, getValues, trigger } = useForm({ mode: 'onChange' }) const dispatch = useDispatch() const [fee, setFee] = useState(new BigNumber(0.0001)) @@ -85,6 +87,13 @@ export function SendScreen ({ route, navigation }: Props): JSX.Element { client.transactions.estimateFee().then((f) => setFee(new BigNumber(f))).catch((e) => Logging.error(e)) }, []) + useEffect(() => { + const t = tokens.find((t) => t.id === token.id) + if (t !== undefined) { + setToken({ ...t }) + } + }, [tokens]) + async function onSubmit (): Promise { if (hasPendingJob) { return diff --git a/cypress/integration/functional/balances/convert/convert.spec.ts b/cypress/integration/functional/balances/convert/convert.spec.ts index f3bb9251c8..015964fad4 100644 --- a/cypress/integration/functional/balances/convert/convert.spec.ts +++ b/cypress/integration/functional/balances/convert/convert.spec.ts @@ -52,8 +52,8 @@ context('wallet/balances/convert - bi-direction success case', () => { it('utxosToToken: should be able to convert successfully', function () { cy.intercept('/v0/playground/transactions/send').as('sendRaw') - cy.getByTestID('button_continue_convert').click().wait(4000) - cy.getByTestID('oceanInterface_close').click() + cy.getByTestID('button_continue_convert').click() + cy.closeOceanInterface() // check UI redirected (balances root) // cy.getByTestID('balances_list').should('exist') @@ -113,8 +113,8 @@ context('wallet/balances/convert - bi-direction success case', () => { it('tokenToUtxos: should be able to convert successfully', function () { cy.intercept('/v0/playground/transactions/send').as('sendRaw') - cy.getByTestID('button_continue_convert').click().wait(4000) - cy.getByTestID('oceanInterface_close').click() + cy.getByTestID('button_continue_convert').click() + cy.closeOceanInterface() // check UI redirected (balances root) cy.getByTestID('balances_list').should('exist') diff --git a/cypress/integration/functional/balances/send.spec.ts b/cypress/integration/functional/balances/send.spec.ts index 0392a28772..68773bccc2 100644 --- a/cypress/integration/functional/balances/send.spec.ts +++ b/cypress/integration/functional/balances/send.spec.ts @@ -78,7 +78,7 @@ context('wallet/send', () => { cy.getByTestID('amount_input').clear().type('1') cy.getByTestID('send_submit_button').should('not.have.attr', 'disabled') cy.getByTestID('send_submit_button').click() - cy.wait(5000).getByTestID('oceanInterface_close').click().wait(5000) + cy.closeOceanInterface() cy.getByTestID('playground_wallet_fetch_balances').click() cy.getByTestID('bottom_tab_balances').click() }) @@ -115,7 +115,7 @@ context('wallet/send', () => { cy.getByTestID('address_input').type(address) cy.getByTestID('MAX_amount_button').click() cy.getByTestID('send_submit_button').click() - cy.wait(5000).getByTestID('oceanInterface_close').click().wait(5000) + cy.closeOceanInterface() cy.getByTestID('playground_wallet_fetch_balances').click() cy.getByTestID('bottom_tab_balances').click() cy.getByTestID('balances_row_1_amount').should('not.exist') diff --git a/cypress/integration/functional/dex/poolswap.spec.ts b/cypress/integration/functional/dex/poolswap.spec.ts index dcb81ded2d..8beeafeb6b 100644 --- a/cypress/integration/functional/dex/poolswap.spec.ts +++ b/cypress/integration/functional/dex/poolswap.spec.ts @@ -75,7 +75,7 @@ context('poolswap with values', () => { cy.getByTestID('text_price_row_minimum_0').then(() => { // const tokenValue = $txt[0].textContent.replace(' LTC', '').replace(',', '') cy.getByTestID('button_submit').click() - cy.wait(5000).getByTestID('oceanInterface_close').click().wait(5000) + cy.closeOceanInterface() cy.getByTestID('playground_wallet_fetch_balances').click() cy.getByTestID('bottom_tab_balances').click() cy.getByTestID('balances_row_4').should('exist') diff --git a/cypress/integration/functional/dex/remove_liquidity.spec.ts b/cypress/integration/functional/dex/remove_liquidity.spec.ts index 294894af27..e7eb0ecf29 100644 --- a/cypress/integration/functional/dex/remove_liquidity.spec.ts +++ b/cypress/integration/functional/dex/remove_liquidity.spec.ts @@ -52,7 +52,7 @@ context('app/dex/removeLiquidity', () => { cy.getByTestID('button_slider_max').click().wait(1000) cy.getByTestID('button_continue_remove_liq').click().wait(4000) cy.getByTestID('bottom_tab_dex').click().wait(1000) - cy.wait(5000).getByTestID('oceanInterface_close').click().wait(5000) + cy.closeOceanInterface() // redirected back to dex root page cy.getByTestID('liquidity_screen_list').should('exist') diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index cac72a7196..f1a4423379 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -33,32 +33,38 @@ declare global { * @example cy.getByTestID('settings') */ getByTestID (value: string): Chainable - + /** * @description Redirects to main page and creates an empty wallet for testing. Useful on starts of tests. * @param {boolean} [isRandom=false] default = false, creates randomly generated mnemonic seed or abandon x23 * @example cy.createEmptyWallet(isRandom?: boolean) */ createEmptyWallet (isRandom?: boolean): Chainable - + /** * @description Sends UTXO DFI to wallet. * @example cy.sendDFItoWallet().wait(4000) */ sendDFItoWallet (): Chainable - + /** * @description Sends DFI Token to wallet. * @example cy.sendDFITokentoWallet().wait(4000) */ sendDFITokentoWallet (): Chainable - + /** * @description Sends token to wallet. Accepts a list of token symbols to be sent. * @param {string[]} tokens to be sent * @example cy.sendTokenToWallet(['BTC', 'ETH']).wait(4000) */ sendTokenToWallet (tokens: string[]): Chainable + + /** + * @description Wait for the ocean interface to be confirmed then close the drawer + * @example cy.closeOceanInterface() + */ + closeOceanInterface (): Chainable } } } @@ -91,3 +97,7 @@ Cypress.Commands.add('sendTokenToWallet', (tokens: string[]) => { }) cy.wait(['@sendTokensToAddress']) }) + +Cypress.Commands.add('closeOceanInterface', () => { + cy.wait(5000).getByTestID('oceanInterface_close').click().wait(2000) +})