Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ocean interface - wait for transaction confirmation #395

Merged
merged 13 commits into from
Aug 3, 2021
15 changes: 13 additions & 2 deletions app/components/OceanInterface/OceanInterface.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -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 = (
<Provider store={store}>
Expand All @@ -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 = (
<Provider store={store}>
Expand Down
90 changes: 76 additions & 14 deletions app/components/OceanInterface/OceanInterface.tsx
Original file line number Diff line number Diff line change
@@ -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<void> {
// TODO(thedoublejay) explorer URL
Expand All @@ -37,6 +42,38 @@ async function broadcastTransaction (tx: CTransactionSegWit, client: WhaleApiCli
}
}

async function waitForTxConfirmation (id: string, client: WhaleApiClient): Promise<Transaction> {
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.
Expand All @@ -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)
thedoublejay marked this conversation as resolved.
Show resolved Hide resolved
// state
const [tx, setTx] = useState<OceanTransaction | undefined>(transaction)
const [err, setError] = useState<Error | undefined>(e)
Expand All @@ -72,21 +110,41 @@ 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) {
errMsg = `${errMsg}. Txid: ${txid}`
}
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])

Expand All @@ -104,16 +162,19 @@ export function OceanInterface (): JSX.Element | null {
{
err !== undefined
? <TransactionError errMsg={err.message} onClose={dismissDrawer} />
: <TransactionDetail broadcasted={tx.broadcasted} txid={txid} onClose={dismissDrawer} />
: <TransactionDetail broadcasted={tx.broadcasted} title={tx.title} txid={txid} onClose={dismissDrawer} />
}
</Animated.View>
)
}

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 (
<>
{
Expand All @@ -123,7 +184,7 @@ function TransactionDetail ({ broadcasted, txid, onClose }: { broadcasted: boole
<View style={tailwind('flex-auto mx-6 justify-center items-center text-center')}>
<Text
style={tailwind('text-sm font-bold')}
>{translate('screens/OceanInterface', title)}
>{title}
</Text>
{
txid !== undefined && <TransactionIDButton txid={txid} onPress={async () => await gotoExplorer(txid)} />
Expand Down Expand Up @@ -177,7 +238,8 @@ function TransactionIDButton ({ txid, onPress }: { txid: string, onPress?: () =>
function TransactionCloseButton (props: { onPress: () => void }): JSX.Element {
return (
<TouchableOpacity
testID='oceanInterface_close' onPress={props.onPress} style={tailwind('px-2 py-1 rounded border border-gray-300 rounded flex-row justify-center items-center')}
testID='oceanInterface_close' onPress={props.onPress}
style={tailwind('px-2 py-1 rounded border border-gray-300 rounded flex-row justify-center items-center')}
>
<Text style={tailwind('text-sm text-primary')}>
{translate('screens/OceanInterface', 'OK')}
Expand Down
13 changes: 11 additions & 2 deletions app/screens/AppNavigator/screens/Balances/screens/SendScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ 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'
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'
Expand Down Expand Up @@ -74,7 +75,8 @@ type Props = StackScreenProps<BalanceParamList, 'SendScreen'>
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<BigNumber>(new BigNumber(0.0001))
Expand All @@ -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<void> {
if (hasPendingJob) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions cypress/integration/functional/balances/send.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion cypress/integration/functional/dex/poolswap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
18 changes: 14 additions & 4 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,38 @@ declare global {
* @example cy.getByTestID('settings')
*/
getByTestID (value: string): Chainable<Element>

/**
* @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<Element>

/**
* @description Sends UTXO DFI to wallet.
* @example cy.sendDFItoWallet().wait(4000)
*/
sendDFItoWallet (): Chainable<Element>

/**
* @description Sends DFI Token to wallet.
* @example cy.sendDFITokentoWallet().wait(4000)
*/
sendDFITokentoWallet (): Chainable<Element>

/**
* @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<Element>

/**
* @description Wait for the ocean interface to be confirmed then close the drawer
* @example cy.closeOceanInterface()
*/
closeOceanInterface (): Chainable<Element>
}
}
}
Expand Down Expand Up @@ -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)
})