|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
import TokenTracker from '@metamask/eth-token-tracker';
|
|
|
|
import { useSelector } from 'react-redux';
|
|
|
|
import { getCurrentChainId, getSelectedAddress } from '../selectors';
|
|
|
|
import { SECOND } from '../../shared/constants/time';
|
|
|
|
import { isEqualCaseInsensitive } from '../helpers/utils/util';
|
|
|
|
import { useEqualityCheck } from './useEqualityCheck';
|
|
|
|
|
|
|
|
export function useTokenTracker(
|
|
|
|
tokens,
|
|
|
|
includeFailedTokens = false,
|
|
|
|
hideZeroBalanceTokens = false,
|
|
|
|
) {
|
|
|
|
const chainId = useSelector(getCurrentChainId);
|
|
|
|
const userAddress = useSelector(getSelectedAddress);
|
|
|
|
const [loading, setLoading] = useState(() => tokens?.length >= 0);
|
|
|
|
const [tokensWithBalances, setTokensWithBalances] = useState([]);
|
|
|
|
const [error, setError] = useState(null);
|
|
|
|
const tokenTracker = useRef(null);
|
|
|
|
const memoizedTokens = useEqualityCheck(tokens);
|
|
|
|
|
|
|
|
const updateBalances = useCallback(
|
|
|
|
(tokenWithBalances) => {
|
|
|
|
const matchingTokens = hideZeroBalanceTokens
|
|
|
|
? tokenWithBalances.filter((token) => Number(token.balance) > 0)
|
|
|
|
: tokenWithBalances;
|
|
|
|
// TODO: improve this pattern for adding this field when we improve support for
|
|
|
|
// EIP721 tokens.
|
|
|
|
const matchingTokensWithIsERC721Flag = matchingTokens.map((token) => {
|
|
|
|
const additionalTokenData = memoizedTokens.find((t) =>
|
|
|
|
isEqualCaseInsensitive(t.address, token.address),
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
...token,
|
|
|
|
isERC721: additionalTokenData?.isERC721,
|
|
|
|
image: additionalTokenData?.image,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
setTokensWithBalances(matchingTokensWithIsERC721Flag);
|
|
|
|
setLoading(false);
|
|
|
|
setError(null);
|
|
|
|
},
|
|
|
|
[hideZeroBalanceTokens, memoizedTokens],
|
|
|
|
);
|
|
|
|
|
|
|
|
const showError = useCallback((err) => {
|
|
|
|
setError(err);
|
|
|
|
setLoading(false);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const teardownTracker = useCallback(() => {
|
|
|
|
if (tokenTracker.current) {
|
|
|
|
tokenTracker.current.stop();
|
|
|
|
tokenTracker.current.removeAllListeners('update');
|
|
|
|
tokenTracker.current.removeAllListeners('error');
|
|
|
|
tokenTracker.current = null;
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const buildTracker = useCallback(
|
|
|
|
(address, tokenList) => {
|
|
|
|
// clear out previous tracker, if it exists.
|
|
|
|
teardownTracker();
|
|
|
|
tokenTracker.current = new TokenTracker({
|
|
|
|
userAddress: address,
|
|
|
|
provider: global.ethereumProvider,
|
|
|
|
tokens: tokenList,
|
|
|
|
includeFailedTokens,
|
|
|
|
pollingInterval: SECOND * 8,
|
|
|
|
});
|
|
|
|
|
|
|
|
tokenTracker.current.on('update', updateBalances);
|
|
|
|
tokenTracker.current.on('error', showError);
|
|
|
|
tokenTracker.current.updateBalances();
|
|
|
|
},
|
|
|
|
[updateBalances, includeFailedTokens, showError, teardownTracker],
|
|
|
|
);
|
|
|
|
|
|
|
|
// Effect to remove the tracker when the component is removed from DOM
|
|
|
|
// Do not overload this effect with additional dependencies. teardownTracker
|
|
|
|
// is the only dependency here, which itself has no dependencies and will
|
|
|
|
// never update. The lack of dependencies that change is what confirms
|
|
|
|
// that this effect only runs on mount/unmount
|
|
|
|
useEffect(() => {
|
|
|
|
return teardownTracker;
|
|
|
|
}, [teardownTracker]);
|
|
|
|
|
|
|
|
// Effect to set loading state and initialize tracker when values change
|
|
|
|
useEffect(() => {
|
|
|
|
// This effect will only run initially and when:
|
|
|
|
// 1. chainId is updated,
|
|
|
|
// 2. userAddress is changed,
|
|
|
|
// 3. token list is updated and not equal to previous list
|
|
|
|
// in any of these scenarios, we should indicate to the user that their token
|
|
|
|
// values are in the process of updating by setting loading state.
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
if (!userAddress || chainId === undefined || !global.ethereumProvider) {
|
|
|
|
// If we do not have enough information to build a TokenTracker, we exit early
|
|
|
|
// When the values above change, the effect will be restarted. We also teardown
|
|
|
|
// tracker because inevitably this effect will run again momentarily.
|
|
|
|
teardownTracker();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (memoizedTokens.length === 0) {
|
|
|
|
// sets loading state to false and token list to empty
|
|
|
|
updateBalances([]);
|
|
|
|
}
|
|
|
|
|
|
|
|
buildTracker(userAddress, memoizedTokens);
|
|
|
|
}, [
|
|
|
|
userAddress,
|
|
|
|
teardownTracker,
|
|
|
|
chainId,
|
|
|
|
memoizedTokens,
|
|
|
|
updateBalances,
|
|
|
|
buildTracker,
|
|
|
|
]);
|
|
|
|
|
|
|
|
return { loading, tokensWithBalances, error };
|
|
|
|
}
|