Skip to content

Commit

Permalink
[Wallet] Fix Race condition with fetching verification state and bala…
Browse files Browse the repository at this point in the history
…nces (#5178)

### Description

We need to wait until balances are first fetched before determining state of verification.

### Tested

iOS simulator

### Related issues

- Fixes #5170

### Backwards compatibility

Yes
  • Loading branch information
i1skn authored Sep 24, 2020
1 parent 4cfde0d commit 946b6c4
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 4 deletions.
66 changes: 64 additions & 2 deletions packages/mobile/src/identity/verification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { ActionableAttestation } from '@celo/contractkit/lib/wrappers/Attestatio
import { expectSaga } from 'redux-saga-test-plan'
import * as matchers from 'redux-saga-test-plan/matchers'
import { throwError } from 'redux-saga-test-plan/providers'
import { call, delay, select } from 'redux-saga/effects'
import { all, call, delay, race, select } from 'redux-saga/effects'
import { e164NumberSelector } from 'src/account/selectors'
import { showError } from 'src/alert/actions'
import { AppEvents, VerificationEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { setNumberVerified } from 'src/app/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { celoTokenBalanceSelector } from 'src/goldToken/selectors'
import {
Actions,
cancelVerification,
Expand All @@ -27,13 +28,16 @@ import {
import { VerificationStatus } from 'src/identity/types'
import {
AttestationCode,
BALANCE_CHECK_TIMEOUT,
doVerificationFlow,
fetchVerificationState,
NUM_ATTESTATIONS_REQUIRED,
requestAndRetrieveAttestations,
startVerification,
VERIFICATION_TIMEOUT,
} from 'src/identity/verification'
import { waitFor } from 'src/redux/sagas-helpers'
import { stableTokenBalanceSelector } from 'src/stableToken/reducer'
import { getContractKitAsync } from 'src/web3/contracts'
import { getConnectedAccount, getConnectedUnlockedAccount } from 'src/web3/saga'
import { dataEncryptionKeySelector } from 'src/web3/selectors'
Expand Down Expand Up @@ -242,6 +246,16 @@ describe(fetchVerificationState, () => {
pepper: mockE164NumberPepper,
},
],
[
race({
balances: all([
call(waitFor, stableTokenBalanceSelector),
call(waitFor, celoTokenBalanceSelector),
]),
timeout: delay(BALANCE_CHECK_TIMEOUT),
}),
{ timeout: false },
],
[select(dataEncryptionKeySelector), mockPrivateDEK],
[select(isBalanceSufficientForSigRetrievalSelector), true],
])
Expand Down Expand Up @@ -275,6 +289,16 @@ describe(fetchVerificationState, () => {
pepper: mockE164NumberPepper,
},
],
[
race({
balances: all([
call(waitFor, stableTokenBalanceSelector),
call(waitFor, celoTokenBalanceSelector),
]),
timeout: delay(BALANCE_CHECK_TIMEOUT),
}),
{ timeout: false },
],
[select(dataEncryptionKeySelector), mockPrivateDEK],
[select(isBalanceSufficientForSigRetrievalSelector), true],
])
Expand Down Expand Up @@ -304,9 +328,47 @@ describe(fetchVerificationState, () => {
call(fetchPhoneHashPrivate, mockE164Number),
{ phoneHash: mockE164NumberHash, e164Number: mockE164Number },
],
[
race({
balances: all([
call(waitFor, stableTokenBalanceSelector),
call(waitFor, celoTokenBalanceSelector),
]),
timeout: delay(BALANCE_CHECK_TIMEOUT),
}),
{ timeout: false },
],
[select(dataEncryptionKeySelector), mockPrivateDEK],
[select(isBalanceSufficientForSigRetrievalSelector), false],
[select(verificationStateSelector), mockVerificationStateUnverified],
])
.not.put.like({ action: { type: Actions.UPDATE_VERIFICATION_STATE } })
.run()
})
it('catches when balances are not fetched', async () => {
const contractKit = await getContractKitAsync()
await expectSaga(fetchVerificationState)
.provide([
[call(getConnectedUnlockedAccount), mockAccount],
[select(e164NumberSelector), mockE164Number],
[
call([contractKit.contracts, contractKit.contracts.getAttestations]),
mockAttestationsWrapperUnverified,
],
[call([contractKit.contracts, contractKit.contracts.getAccounts]), mockAccountsWrapper],
[
call(fetchPhoneHashPrivate, mockE164Number),
{ phoneHash: mockE164NumberHash, e164Number: mockE164Number },
],
[
race({
balances: all([
call(waitFor, stableTokenBalanceSelector),
call(waitFor, celoTokenBalanceSelector),
]),
timeout: delay(BALANCE_CHECK_TIMEOUT),
}),
{ timeout: true },
],
])
.not.put.like({ action: { type: Actions.UPDATE_VERIFICATION_STATE } })
.run()
Expand Down
15 changes: 15 additions & 0 deletions packages/mobile/src/identity/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { setNumberVerified } from 'src/app/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { DEFAULT_TESTNET, SMS_RETRIEVER_APP_SIGNATURE } from 'src/config'
import { features } from 'src/flags'
import { celoTokenBalanceSelector } from 'src/goldToken/selectors'
import { refreshAllBalances } from 'src/home/actions'
import {
Actions,
Expand All @@ -47,6 +48,7 @@ import {
} from 'src/identity/reducer'
import { startAutoSmsRetrieval } from 'src/identity/smsRetrieval'
import { VerificationStatus } from 'src/identity/types'
import { waitFor } from 'src/redux/sagas-helpers'
import { stableTokenBalanceSelector } from 'src/stableToken/reducer'
import { sendTransaction } from 'src/transactions/send'
import { newTransactionContext } from 'src/transactions/types'
Expand All @@ -60,6 +62,7 @@ const TAG = 'identity/verification'
export const NUM_ATTESTATIONS_REQUIRED = 3
export const ESTIMATED_COST_PER_ATTESTATION = 0.051
export const VERIFICATION_TIMEOUT = 10 * 60 * 1000 // 10 minutes
export const BALANCE_CHECK_TIMEOUT = 5 * 1000 // 5 seconds
const REVEAL_RETRY_DELAY = 10 * 1000 // 10 seconds

export enum CodeInputType {
Expand All @@ -85,10 +88,22 @@ export function* fetchVerificationState() {

let phoneHash: string
let phoneHashDetails: PhoneNumberHashDetails
const { timeout } = yield race({
balances: all([
call(waitFor, stableTokenBalanceSelector),
call(waitFor, celoTokenBalanceSelector),
]),
timeout: delay(BALANCE_CHECK_TIMEOUT),
})
if (timeout) {
Logger.debug(TAG, '@fetchVerificationState', 'Token balances is null or undefined')
return
}
const isBalanceSufficientForSigRetrieval = yield select(
isBalanceSufficientForSigRetrievalSelector
)
if (!isBalanceSufficientForSigRetrieval) {
Logger.debug(TAG, '@fetchVerificationState', 'Insufficient balance for sig retrieval')
return
}

Expand Down
13 changes: 12 additions & 1 deletion packages/mobile/src/redux/sagas-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { call, delay, race } from 'redux-saga/effects'
import { call, delay, race, select, take } from 'redux-saga/effects'
import { RootState } from 'src/redux/reducers'

export function withTimeout<Fn extends (...args: any[]) => any>(
wait: number,
Expand All @@ -16,3 +17,13 @@ export function withTimeout<Fn extends (...args: any[]) => any>(
return res
}
}

export function* waitFor<Value = any>(selector: (state: RootState) => Value) {
while (true) {
const value: Value = yield select(selector)
if (value != null) {
return value
}
yield take('*')
}
}
2 changes: 1 addition & 1 deletion packages/mobile/src/tokens/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ interface TokenFetchFactory {
export function tokenFetchFactory({ actionName, token, actionCreator, tag }: TokenFetchFactory) {
function* tokenFetch() {
try {
Logger.debug(tag, 'Fetching balance')
Logger.debug(tag, `Fetching ${token} balance`)
const account = yield call(getConnectedAccount)
const tokenContract = yield call(getTokenContract, token)
const balanceInWei: BigNumber = yield call([tokenContract, tokenContract.balanceOf], account)
Expand Down

0 comments on commit 946b6c4

Please sign in to comment.