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

[2847] Bug in token balance #2866

Merged
merged 40 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
aa2eba8
fix(WatchAssetRequestComponent): Token balance
Jul 1, 2021
3025372
Merge branch 'develop' of https://github.com/MetaMask/metamask-mobile…
Jul 4, 2021
1e0c4b0
fix(WatchAssetRequestComponent): Update
Jul 5, 2021
ed4b0cb
Merge branch 'develop' into bug/token-correct-balance
Jul 12, 2021
8ec99d3
Merge branch 'develop' into bug/token-correct-balance
Jul 13, 2021
3816273
Merge branch 'develop' into bug/token-correct-balance
Jul 14, 2021
455e37c
Merge branch 'develop' into bug/token-correct-balance
Jul 28, 2021
5b22181
Merge branch 'develop' into bug/token-correct-balance
Aug 4, 2021
b4e5cf9
Merge branch 'develop' into bug/token-correct-balance
Aug 7, 2021
f0d5b7a
Merge branch 'develop' into bug/token-correct-balance
Aug 10, 2021
8fa1728
Merge branch 'develop' of https://github.com/MetaMask/metamask-mobile…
Aug 11, 2021
cc0f2b1
Merge branch 'develop' of https://github.com/MetaMask/metamask-mobile…
Aug 17, 2021
609837c
fix(TokenBalance): Add hook to fetch token balances
Aug 18, 2021
f2af3f0
refactor(TokenBalance): WatchAssetRequest component
Aug 18, 2021
87c45a4
Merge branch 'develop' of https://github.com/MetaMask/metamask-mobile…
Aug 19, 2021
fc8edb7
docs(TokenBalance): useTokenBalance hook
Aug 19, 2021
ee5d371
fix(TokenBalance): Change condition for balance source
Aug 19, 2021
b273d1b
fix(TokenBalance): Change data type
Aug 19, 2021
ba92e33
Merge branch 'develop' into bug/token-correct-balance
Aug 19, 2021
64953d0
Merge branch 'develop' into bug/token-correct-balance
Aug 19, 2021
33aa7e4
Merge branch 'develop' of https://github.com/MetaMask/metamask-mobile…
Aug 20, 2021
839167e
Merge branch 'bug/token-correct-balance' of https://github.com/MetaMa…
Aug 20, 2021
659c404
test(WatchAssetRequest): Update
Aug 21, 2021
d698c48
Merge branch 'develop' into bug/token-correct-balance
Aug 26, 2021
010e9fc
fix(TokenBalance): Add try-catch
Aug 28, 2021
db6619b
fix(TokenBalance): Change type from string to BN
Aug 28, 2021
3027227
Merge branch 'develop' of https://github.com/MetaMask/metamask-mobile…
Aug 28, 2021
59fce75
fix(TokenBalance): Remove props from unmount in useEffect hook
Aug 30, 2021
92690f4
Merge branch 'develop' into bug/token-correct-balance
Sep 1, 2021
2530a39
Merge branch 'develop' into bug/token-correct-balance
Sep 2, 2021
a172f1c
Merge branch 'develop' into bug/token-correct-balance
sethkfman Sep 2, 2021
030b4ed
Merge branch 'develop' into bug/token-correct-balance
sethkfman Sep 2, 2021
deae65c
Merge branch 'develop' into bug/token-correct-balance
Sep 7, 2021
8e83d21
fix(WatchAsset): Address review comments
Sep 7, 2021
6490c93
lint
Sep 7, 2021
7de95c0
Update docs
Sep 7, 2021
9699472
Merge branch 'develop' into bug/token-correct-balance
Sep 7, 2021
553e91a
Merge branch 'develop' into bug/token-correct-balance
Sep 7, 2021
0277d3c
fix(TokenBalance): Handle crash
Sep 7, 2021
ca83241
Merge branch 'bug/token-correct-balance' of https://github.com/MetaMa…
Sep 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 113 additions & 103 deletions app/components/UI/WatchAssetRequest/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { PureComponent } from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, View, Text } from 'react-native';
import { colors, fontStyles } from '../../../styles/common';
import { strings } from '../../../../locales/i18n';
import { connect } from 'react-redux';
import ActionView from '../ActionView';
import { renderFromTokenMinimalUnit } from '../../../util/number';
import { toLowerCaseEquals } from '../../../util/general';
import TokenImage from '../../UI/TokenImage';
import Device from '../../../util/device';
import Engine from '../../../core/Engine';
import URL from 'url-parse';
import AnalyticsV2 from '../../../util/analyticsV2';
import useTokenBalance from '../../hooks/useTokenBalance';

const styles = StyleSheet.create({
root: {
Expand Down Expand Up @@ -78,44 +80,29 @@ const styles = StyleSheet.create({
}
});

/**
* PureComponent that renders watch asset content
*/
class WatchAssetRequest extends PureComponent {
static propTypes = {
/**
* Callback triggered when this message signature is rejected
*/
onCancel: PropTypes.func,
/**
* Callback triggered when this message signature is approved
*/
onConfirm: PropTypes.func,
/**
* Token object
*/
suggestedAssetMeta: PropTypes.object,
/**
* Object containing token balances in the format address => balance
*/
contractBalances: PropTypes.object,
/**
* Object containing current page title, url, and icon href
*/
currentPageInformation: PropTypes.object
};
const WatchAssetRequest = ({
suggestedAssetMeta,
contractBalances,
currentPageInformation,
selectedAddress,
onCancel,
onConfirm
}) => {
useEffect(
() => async () => {
const { TokensController } = Engine.context;
typeof suggestedAssetMeta !== undefined && (await TokensController.rejectWatchAsset(suggestedAssetMeta.id));
},
[suggestedAssetMeta]
);

getAnalyticsParams = () => {
const getAnalyticsParams = () => {
try {
const {
suggestedAssetMeta: { asset },
currentPageInformation
} = this.props;

const { asset } = suggestedAssetMeta;
const { NetworkController } = Engine.context;
const { chainId, type } = NetworkController?.state?.provider || {};

const url = new URL(currentPageInformation?.url);

return {
token_address: asset?.address,
token_symbol: asset?.symbol,
Expand All @@ -130,92 +117,115 @@ class WatchAssetRequest extends PureComponent {
}
};

componentWillUnmount = async () => {
const { TokensController } = Engine.context;
const { suggestedAssetMeta } = this.props;
await TokensController.rejectWatchAsset(suggestedAssetMeta.id);
};

onConfirm = async () => {
const { onConfirm, suggestedAssetMeta } = this.props;
const onConfirmPress = async () => {
const { TokensController } = Engine.context;
await TokensController.acceptWatchAsset(suggestedAssetMeta.id);
AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, this.getAnalyticsParams());
AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, getAnalyticsParams());
onConfirm && onConfirm();
};

render() {
const {
suggestedAssetMeta: { asset },
contractBalances
} = this.props;
const balance =
asset.address in contractBalances
? renderFromTokenMinimalUnit(contractBalances[asset.address], asset.decimals)
: '0';
return (
<View style={styles.root}>
<View style={styles.titleWrapper}>
<Text style={styles.title} onPress={this.cancelSignature}>
{strings('watch_asset_request.title')}
</Text>
</View>
<ActionView
cancelTestID={'request-signature-cancel-button'}
confirmTestID={'request-signature-confirm-button'}
cancelText={strings('watch_asset_request.cancel')}
confirmText={strings('watch_asset_request.add')}
onCancelPress={this.props.onCancel}
onConfirmPress={this.onConfirm}
>
<View style={styles.children}>
<View style={styles.addMessage}>
<Text style={styles.signText}>{strings('watch_asset_request.message')}</Text>
</View>
const { asset } = suggestedAssetMeta;

<View style={styles.tokenInformation}>
<View style={styles.tokenInfo}>
<View style={styles.infoTitleWrapper}>
<Text style={styles.infoTitle}>{strings('watch_asset_request.token')}</Text>
</View>
const address = Object.keys(contractBalances).find(key => toLowerCaseEquals(key, asset.address));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gantunesr This will refer to line 129->136: What do you think about ditching contractBalances since based on the logic and what we've observed, it doesn't seem to be a 100% reliable source (Since it's possible for balances to not exist in the object). Based on your implementation, useTokenBalance is already called every time the function is rendered and we can just rely on that for now. This gives us one less condition to check/worry about and it'll clean up the code. The only downside is that we may not be showing existing contract balances right away, but I think that's totally fine. As for what happens if the token balances call fails: It looks like you've exposed a loading and error state in useTokenBalance, which would give us control over the UI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I was thinking to pass the balance value of contractBalances to the hook and only fetch the new balance if the value was undefined but in a second thought, maybe is better to remove contractBalances as a dependence.

let [balance] = useTokenBalance(asset.address, selectedAddress);

<View style={styles.infoToken}>
<View style={styles.token}>
<View style={styles.identicon}>
<TokenImage
asset={{
...asset,
logo: asset.image
}}
logoDefined
/>
</View>
<Text style={styles.text}>{asset.symbol}</Text>
try {
balance = renderFromTokenMinimalUnit(address ? contractBalances[address] : balance, asset.decimals);
} catch (e) {
balance = 0;
}

return (
<View style={styles.root}>
<View style={styles.titleWrapper}>
<Text style={styles.title} onPress={this.cancelSignature}>
{strings('watch_asset_request.title')}
</Text>
</View>
<ActionView
cancelTestID={'request-signature-cancel-button'}
confirmTestID={'request-signature-confirm-button'}
cancelText={strings('watch_asset_request.cancel')}
confirmText={strings('watch_asset_request.add')}
onCancelPress={onCancel}
onConfirmPress={onConfirmPress}
>
<View style={styles.children}>
<View style={styles.addMessage}>
<Text style={styles.signText}>{strings('watch_asset_request.message')}</Text>
</View>

<View style={styles.tokenInformation}>
<View style={styles.tokenInfo}>
<View style={styles.infoTitleWrapper}>
<Text style={styles.infoTitle}>{strings('watch_asset_request.token')}</Text>
</View>

<View style={styles.infoToken}>
<View style={styles.token}>
<View style={styles.identicon}>
<TokenImage
asset={{
...asset,
logo: asset.image
}}
logoDefined
/>
</View>
<Text style={styles.text}>{asset.symbol}</Text>
</View>
</View>
</View>

<View style={styles.tokenInfo}>
<View style={styles.infoTitleWrapper}>
<Text style={styles.infoTitle}>{strings('watch_asset_request.balance')}</Text>
</View>
<View style={styles.tokenInfo}>
<View style={styles.infoTitleWrapper}>
<Text style={styles.infoTitle}>{strings('watch_asset_request.balance')}</Text>
</View>

<View style={styles.infoBalance}>
<Text style={styles.text}>
{balance} {asset.symbol}
</Text>
</View>
<View style={styles.infoBalance}>
<Text style={styles.text}>
{balance} {asset.symbol}
</Text>
</View>
</View>
</View>
</ActionView>
</View>
);
}
}
</View>
</ActionView>
</View>
);
};

WatchAssetRequest.propTypes = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gantunesr Not a blocker, but just wanted to bring up that propTypes here are never exposed when consuming this component because this component is wrapped in a HOC caused by connect(). We'll eventually want to convert this component to TypeScript, which will fix this scenario. Totally up to you if you want to convert it now or leave it for the next person who touches this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Cal-L I'll open another PR for the next release to convert this component to TS, for the moment I would merged as it is.

/**
* Callback triggered when this message signature is rejected
*/
onCancel: PropTypes.func,
/**
* Callback triggered when this message signature is approved
*/
onConfirm: PropTypes.func,
/**
* Token object
*/
suggestedAssetMeta: PropTypes.object,
/**
* Current public address
*/
selectedAddress: PropTypes.string,
/**
* Object containing token balances in the format address => balance
*/
contractBalances: PropTypes.object,
/**
* Object containing current page title, url, and icon href
*/
currentPageInformation: PropTypes.object
};

const mapStateToProps = state => ({
contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances
contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances,
selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress,
provider: state.engine.backgroundState.NetworkController.provider
});

export default connect(mapStateToProps)(WatchAssetRequest);
9 changes: 9 additions & 0 deletions app/components/UI/WatchAssetRequest/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import WatchAssetRequest from './';
import configureMockStore from 'redux-mock-store';
import { BN } from 'ethereumjs-util';
import { ROPSTEN } from '../../../constants/network';

const mockStore = configureMockStore();

Expand All @@ -13,6 +14,14 @@ describe('WatchAssetRequest', () => {
backgroundState: {
TokenBalancesController: {
contractBalances: { '0x2': new BN(0) }
},
PreferencesController: {
selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1'
},
NetworkController: {
provider: {
type: ROPSTEN
}
}
}
}
Expand Down
34 changes: 34 additions & 0 deletions app/components/hooks/useTokenBalance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useState, useEffect, Dispatch, SetStateAction } from 'react';
import Engine from '../../core/Engine';
import { BN } from '@metamask/controllers';

/**
* Hook to handle the balance of ERC20 tokens
* @param {String} tokenAddress Token contract address
* @param {String} userAddress Public address which holds the token
* @returns {Handlers} Handlers `[balance, loading, error]`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is TypeScript, we could remove the types from the TSDocs.
Ref: https://github.com/microsoft/tsdoc/tree/master/tsdoc

For returns, I wasn't really sure what Handlers meant in this context. We could do something like:
@returns Array that consists of [balance, loading, error] to be more explicit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks for the reference

*/

const useTokenBalance = (tokenAddress: string, userAddress: string): [any, boolean, boolean] => {
// This hook should be only used with ERC20 tokens
const [balance, setBalance]: [BN | null, Dispatch<SetStateAction<BN | null>>] = useState<BN | null>(null);
const [loading, setLoading]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const [error, setError]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
const { TokenBalancesController }: any = Engine.context;

const fetchBalance = async (tokenAddress: string, userAddress: string): Promise<void> => {
TokenBalancesController.getBalanceOf(tokenAddress, userAddress)
.then((balance: BN) => setBalance(balance))
.catch(() => setError(true))
.finally(() => setLoading(false));
};

useEffect(() => {
fetchBalance(tokenAddress, userAddress);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tokenAddress, userAddress]);

return [balance, loading, error];
};

export default useTokenBalance;