diff --git a/src/components/token/TokenBalances.js b/src/components/token/TokenBalances.js
index 6e766eb..0a7e80d 100644
--- a/src/components/token/TokenBalances.js
+++ b/src/components/token/TokenBalances.js
@@ -5,372 +5,367 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
+import { get, last, find, isEmpty } from 'lodash';
+import { useHistory } from 'react-router-dom';
+import { numberUtils, constants as hathorLibConstants } from '@hathor/wallet-lib';
import TokenBalancesTable from './TokenBalancesTable';
import tokensApi from '../../api/tokensApi';
-import { get, last, find, isEmpty } from 'lodash';
import PaginationURL from '../../utils/pagination';
-import { withRouter } from 'react-router-dom';
import ErrorMessageWithIcon from '../error/ErrorMessageWithIcon';
import TokenAutoCompleteField from './TokenAutoCompleteField';
-import helpers from '../../utils/helpers';
-import { numberUtils, constants as hathorLibConstants } from '@hathor/wallet-lib';
/**
* Displays custom tokens in a table with pagination buttons and a search bar.
*/
-class TokenBalances extends React.Component {
+function TokenBalances({ maintenanceMode }) {
+ const history = useHistory();
+
/**
- * Structure that contains the attributes that will be part of the page URL
+ * tokenBalances: List of token balances currently being rendered.
+ * Each token balance element must have the fields: address, locked_balance, unlocked_balance, total, token_id and sort.
+ * id, name, symbol are strings; nft is boolean; transaction_timestamp is long.
+ * Sort is an array with two strings, The value is given by ElasticSearch and it is passed back when we want to change page
+ * hasAfter: Indicates if a next page exists
+ * hasBefore: Indicates if a previous page exists
+ * sortBy: Which field to sort (uid, name, symbol)
+ * order: If sorted field must be ordered asc or desc
+ * page: Current page. Used to know if there is a previous page
+ * pageSearchAfter: Calculates the searchAfter param that needs to be passed to explorer-service in order to get the next/previous page.
+ * We use this array to store already-calculated values,
+ * so we do not need to recalculate them if user is requesting an already-navigated page.
+ * loading: Initial loading, when user clicks on the Tokens navigation item
+ * isSearchLoading: Indicates if search results are being retrieved from explorer-service
+ * calculatingPage: Indicates if next page is being retrieved from explorer-service
+ * error: Indicates if an unexpected error happened when calling the explorer-service
+ * tokenBalanceInformationError: Indicates if an unexpected error happened when calling the token balance information service
+ * maintenanceMode: Indicates if explorer-service or its downstream services are experiencing problems. If so, maintenance mode will be enabled on
+ * our feature toggle service (Unleash) to remove additional load until the team fixes the problem
+ * transactionsCount: Number of transactions for the searched token
+ * addressesCount: Number of addressed for the searched token
+ * tokensApiError: Flag indicating if the request to the token api failed, to decide wether to display or not the total number of transactions
*/
- pagination = new PaginationURL({
- sortBy: { required: false },
- order: { required: false },
- token: { required: false },
- });
-
- constructor(props) {
- super(props);
+ const [tokenId, setTokenId] = useState(hathorLibConstants.NATIVE_TOKEN_UID);
+ const [tokenBalances, setTokenBalances] = useState([]);
+ const [hasAfter, setHasAfter] = useState(false);
+ const [hasBefore, setHasBefore] = useState(false);
+ const [sortBy, setSortBy] = useState(null);
+ const [order, setOrder] = useState(null);
+ const [page, setPage] = useState(1);
+ const [pageSearchAfter, setPageSearchAfter] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [isSearchLoading, setIsSearchLoading] = useState(false);
+ const [calculatingPage, setCalculatingPage] = useState(false);
+ const [error, setError] = useState(false);
+ const [tokenBalanceInformationError, setTokenBalanceInformationError] = useState(false);
+ const [transactionsCount, setTransactionsCount] = useState(0);
+ const [addressesCount, setAddressesCount] = useState(0);
+ const [tokensApiError, setTokensApiError] = useState(false);
+ /**
+ * Structure that contains the attributes that will be part of the page URL
+ */
+ const pagination = useRef(
+ new PaginationURL({
+ sortBy: { required: false },
+ order: { required: false },
+ token: { required: false },
+ })
+ );
+
+ const fetchHTRTransactionCount = useCallback(async () => {
+ const tokenApiResponse = await tokensApi.getToken(hathorLibConstants.NATIVE_TOKEN_UID);
+
+ setTokensApiError(get(tokenApiResponse, 'error', false));
+ setTransactionsCount(get(tokenApiResponse, 'data.hits[0].transactions_count', 0));
+ }, []);
+
+ // Initialization effect, incorporating querystring parameters
+ useEffect(() => {
// Get default states from query params and default values
- const queryParams = this.pagination.obtainQueryParams();
-
- const sortBy = get(queryParams, 'sortBy', 'total');
- const order = get(queryParams, 'order', 'desc');
- const tokenId = get(queryParams, 'token', hathorLibConstants.NATIVE_TOKEN_UID);
-
- /**
- * tokenBalances: List of token balances currently being rendered.
- * Each token balance element must have the fields: address, locked_balance, unlocked_balance, total, token_id and sort.
- * id, name, symbol are strings; nft is boolean; transaction_timestamp is long.
- * Sort is an array with two strings, The value is given by ElasticSearch and it is passed back when we want to change page
- * hasAfter: Indicates if a next page exists
- * hasBefore: Indicates if a previous page exists
- * sortBy: Which field to sort (uid, name, symbol)
- * order: If sorted field must be ordered asc or desc
- * page: Current page. Used to know if there is a previous page
- * pageSearchAfter: Calculates the searchAfter param that needs to be passed to explorer-service in order to get the next/previous page.
- * We use this array to store already-calculated values,
- * so we do not need to recalculate them if user is requesting an already-navigated page.
- * loading: Initial loading, when user clicks on the Tokens navigation item
- * isSearchLoading: Indicates if search results are being retrieved from explorer-service
- * calculatingPage: Indicates if next page is being retrieved from explorer-service
- * error: Indicates if an unexpected error happened when calling the explorer-service
- * tokenBalanceInformationError: Indicates if an unexpected error happened when calling the token balance information service
- * maintenanceMode: Indicates if explorer-service or its downstream services are experiencing problems. If so, maintenance mode will be enabled on
- * our feature toggle service (Unleash) to remove additional load until the team fixes the problem
- * transactionsCount: Number of transactions for the searched token
- * addressesCount: Number of addressed for the searched token
- * tokensApiError: Flag indicating if the request to the token api failed, to decide wether to display or not the total number of transactions
- */
- this.state = {
- tokenId,
- tokenBalances: [],
- hasAfter: false,
- hasBefore: false,
- sortBy,
- order,
- page: 1,
- pageSearchAfter: [],
- loading: true,
- isSearchLoading: false,
- calculatingPage: false,
- error: false,
- tokenBalanceInformationError: false,
- maintenanceMode: this.props.maintenanceMode,
- transactionsCount: 0,
- addressesCount: 0,
- tokensApiError: false,
- };
- }
+ const queryParams = pagination.current.obtainQueryParams();
- componentDidMount = async () => {
- if (this.state.maintenanceMode) {
- return;
- }
+ const querySortBy = get(queryParams, 'sortBy', 'total');
+ const queryOrder = get(queryParams, 'order', 'desc');
+ const queryTokenId = get(queryParams, 'token', hathorLibConstants.NATIVE_TOKEN_UID);
- if (this.state.tokenId === hathorLibConstants.NATIVE_TOKEN_UID) {
+ setSortBy(querySortBy);
+ setOrder(queryOrder);
+ setTokenId(queryTokenId);
+
+ if (queryTokenId === hathorLibConstants.NATIVE_TOKEN_UID) {
// If we had a custom token as queryParam
// then we will perform the search after the token
// is found in the elastic search
// otherwise we just perform the search for HTR to show the default screen
- await this.performSearch();
+ setIsSearchLoading(true);
// Since we did not search for the HTR token (it is the default), we need to fetch
// it to retrieve the transactions count
- await this.fetchHTRTransactionCount();
-
- this.setState({
- loading: false,
- });
+ fetchHTRTransactionCount().catch(e =>
+ console.error('Error on initial fetchHTRTransactionCount', e)
+ );
}
- };
+ }, [fetchHTRTransactionCount]);
/**
* Call explorer-service to get list of token balances for a given tokenId
*
+ * @param queryTokenId
+ * @param querySortBy
+ * @param queryOrder
* @param {*} searchAfter Parameter needed by ElasticSearch for pagination purposes
* @returns tokens
*/
- getTokenBalances = async searchAfter => {
- const tokenBalancesRequest = await tokensApi.getBalances(
- this.state.tokenId,
- this.state.sortBy,
- this.state.order,
- searchAfter
- );
- this.setState({
- error: get(tokenBalancesRequest, 'error', false),
- });
- return get(tokenBalancesRequest, 'data', { hits: [], has_next: false });
- };
+ const getTokenBalances = useCallback(
+ async (queryTokenId, querySortBy, queryOrder, searchAfter) => {
+ const tokenBalancesResponse = await tokensApi.getBalances(
+ queryTokenId,
+ querySortBy,
+ queryOrder,
+ searchAfter
+ );
+ setError(tokenBalancesResponse.error || false);
+ return tokenBalancesResponse.data || { hits: [], has_next: false };
+ },
+ []
+ );
+
+ const loadTokenBalanceInformation = useCallback(async queryTokenId => {
+ const tokenBalanceInformationResponse = await tokensApi.getBalanceInformation(queryTokenId);
+ setTokenBalanceInformationError(tokenBalanceInformationResponse.error || false);
- loadTokenBalanceInformation = async () => {
- const tokenBalanceInformationRequest = await tokensApi.getBalanceInformation(
- this.state.tokenId
+ return (
+ tokenBalanceInformationResponse.data || {
+ transactions: 0,
+ addresses: 0,
+ }
);
- this.setState({
- tokenBalanceInformationError: get(tokenBalanceInformationRequest, 'error', false),
- });
-
- return get(tokenBalanceInformationRequest, 'data', {
- transactions: 0,
- addresses: 0,
- });
- };
+ }, []);
+
+ /**
+ * Update the URL, so user can share the results of a search
+ */
+ const updateURL = useCallback(
+ (newTokenId, newSortBy, newOrder) => {
+ const newURL = pagination.current.setURLParameters({
+ token: newTokenId,
+ sortBy: newSortBy,
+ order: newOrder,
+ });
+
+ history.push(newURL);
+ },
+ [history]
+ );
/**
+ * Effect that reacts to the `isSearchLoading` flag
* Calls ElasticSearch (through the Explorer Service) with state data, sets loading and URL information
*/
- performSearch = async () => {
- this.setState({ isSearchLoading: true });
- const tokenBalances = await this.getTokenBalances([]);
- const tokenBalanceInformation = await this.loadTokenBalanceInformation();
-
- this.setState({
- isSearchLoading: false,
- loading: false,
- page: 1,
- tokenBalances: tokenBalances.hits,
- addressesCount: tokenBalanceInformation.addresses,
- hasAfter: tokenBalances.has_next,
- hasBefore: false,
- pageSearchAfter: [
+ useEffect(() => {
+ const performSearch = async () => {
+ const fetchedTokenBalances = await getTokenBalances(tokenId, sortBy, order, []);
+ const tokenBalanceInformation = await loadTokenBalanceInformation(tokenId);
+
+ setIsSearchLoading(false);
+ setLoading(false);
+ setPage(1);
+ setTokenBalances(fetchedTokenBalances.hits);
+ setAddressesCount(tokenBalanceInformation.addresses);
+ setHasAfter(fetchedTokenBalances.has_next);
+ setHasBefore(false);
+ setPageSearchAfter([
{
page: 1,
searchAfter: [],
},
- ],
- });
+ ]);
- // This is ultimately called when search text, sort, or sort order changes
- this.updateURL();
- };
+ // This is ultimately called when search text, sort, or sort order changes
+ updateURL(tokenId, sortBy, order);
+ };
- /**
- * Update the URL, so user can share the results of a search
- */
- updateURL = () => {
- const newURL = this.pagination.setURLParameters({
- sortBy: this.state.sortBy,
- order: this.state.order,
- token: this.state.tokenId,
- });
-
- this.props.history.push(newURL);
- };
+ if (!isSearchLoading) {
+ // Ignore this effect it the isSearchLoading flag is inactive
+ return;
+ }
+ performSearch().catch(e => console.error('Error on performSearch effect', e));
+ }, [
+ isSearchLoading,
+ tokenId,
+ sortBy,
+ order,
+ getTokenBalances,
+ loadTokenBalanceInformation,
+ updateURL,
+ ]);
+
+ // TODO: Maybe those clicked functions must setCalculatingPage and let the rest be handled in an effect
/**
* Process events when next page is requested by user
*/
- nextPageClicked = async () => {
- this.setState({ calculatingPage: true });
+ const nextPageClicked = async () => {
+ setCalculatingPage(true);
- const nextPage = this.state.page + 1;
- let searchAfter = get(find(this.state.pageSearchAfter, { page: nextPage }), 'searchAfter', []);
+ const nextPage = page + 1;
+ let searchAfter = get(find(pageSearchAfter, { page: nextPage }), 'searchAfter', []);
// Calculate searchAfter of next page if not already calculated
if (isEmpty(searchAfter)) {
- const lastCurrentTokenSort = get(last(this.state.tokenBalances), 'sort', []);
+ const lastCurrentTokenSort = get(last(tokenBalances), 'sort', []);
const newEntry = {
page: nextPage,
searchAfter: lastCurrentTokenSort,
};
- this.setState({
- pageSearchAfter: [...this.state.pageSearchAfter, newEntry],
- });
+ setPageSearchAfter([...pageSearchAfter, newEntry]);
searchAfter = lastCurrentTokenSort;
}
- const tokenBalances = await this.getTokenBalances(searchAfter);
+ const fetchedTokenBalances = await getTokenBalances(tokenId, sortBy, order, searchAfter);
- this.setState({
- tokenBalances: tokenBalances.hits,
- hasAfter: tokenBalances.has_next,
- hasBefore: true,
- page: nextPage,
- calculatingPage: false,
- });
+ setTokenBalances(fetchedTokenBalances.hits);
+ setHasAfter(fetchedTokenBalances.has_next);
+ setHasBefore(true);
+ setPage(nextPage);
+ setCalculatingPage(false);
};
/**
* Process events when previous page is requested by user
*/
- previousPageClicked = async () => {
- this.setState({ calculatingPage: true });
-
- const previousPage = this.state.page - 1;
- const searchAfter = get(
- find(this.state.pageSearchAfter, { page: previousPage }),
- 'searchAfter',
- []
- );
- const tokenBalances = await this.getTokenBalances(searchAfter);
-
- this.setState({
- tokenBalances: tokenBalances.hits,
- hasAfter: true,
- hasBefore: previousPage === 1 ? false : true,
- page: previousPage,
- calculatingPage: false,
- });
- };
-
- fetchHTRTransactionCount = async () => {
- const tokenApiRequest = await tokensApi.getToken(hathorLibConstants.NATIVE_TOKEN_UID);
-
- this.setState({
- tokensApiError: get(tokenApiRequest, 'error', false),
- transactionsCount: get(tokenApiRequest, 'data.hits[0].transactions_count', 0),
- });
+ const previousPageClicked = async () => {
+ setCalculatingPage(true);
+
+ const previousPage = page - 1;
+ const searchAfter = get(find(pageSearchAfter, { page: previousPage }), 'searchAfter', []);
+ const fetchedTokenBalances = await getTokenBalances(tokenId, sortBy, order, searchAfter);
+
+ setTokenBalances(fetchedTokenBalances.hits);
+ setHasAfter(true);
+ setHasBefore(previousPage !== 1);
+ setPage(previousPage);
+ setCalculatingPage(false);
};
- onTokenSelected = async token => {
- if (!token) {
- await helpers.setStateAsync(this, {
- tokenId: hathorLibConstants.NATIVE_TOKEN_UID,
- });
+ const onTokenSelected = async newToken => {
+ const newTokenId = newToken?.id || hathorLibConstants.NATIVE_TOKEN_UID;
+ setTokenId(newTokenId);
+ if (!newToken) {
// HTR token is the default, so the search API is not called, we must forcefully call it
// so we can retrieve the transactions count information
- await this.fetchHTRTransactionCount();
-
- this.performSearch();
- return;
+ await fetchHTRTransactionCount(); // TODO: Confirm this behavior
+ } else {
+ setTransactionsCount(newToken.transactions_count);
+ setTokensApiError(false);
}
- await helpers.setStateAsync(this, {
- tokenId: token.id,
- transactionsCount: token.transactions_count,
- tokensApiError: false,
- });
-
- this.performSearch();
+ // Trigger the performSearch effect
+ setIsSearchLoading(true);
};
/**
* Process table header click. This indicates that user wants data to be sorted by a determined field
*
- * @param {*} event
+ * @param {*} _event
* @param {*} header
*/
- tableHeaderClicked = async (_event, header) => {
- if (header === this.state.sortBy) {
- await helpers.setStateAsync(this, { order: this.state.order === 'asc' ? 'desc' : 'asc' });
+ const tableHeaderClicked = async (_event, header) => {
+ if (header === sortBy) {
+ setOrder(order === 'asc' ? 'desc' : 'asc');
} else {
- await helpers.setStateAsync(this, { sortBy: header, order: 'asc' });
+ setSortBy(header);
+ setOrder('asc');
}
- this.performSearch();
+ // Triggers the performSearch effect
+ setIsSearchLoading(true);
};
/**
* Turn loading false.
* Useful to be used by autocomplete component when the first search doesn't find any token
*/
- loadingFinished = () => {
- this.setState({ loading: false });
+ const loadingFinished = () => {
+ setLoading(false);
};
- render() {
- if (this.state.maintenanceMode) {
- return (
-
- - Click here to see the token details - -
- )} - - {!this.state.tokenBalanceInformationError && ( -- Total number of addresses:{' '} - {numberUtils.prettyValue(this.state.addressesCount, 0)} -
- )} - - {!this.state.tokensApiError && ( -- Total number of transactions:{' '} - {numberUtils.prettyValue(this.state.transactionsCount, 0)} -
- )} - - {(this.state.tokensApiError || this.state.tokenBalanceInformationError) && ( -+ Click here to see the token details +
+ )} + + {!tokenBalanceInformationError && ( ++ Total number of addresses: {numberUtils.prettyValue(addressesCount, 0)} +
+ )} + + {!tokensApiError && ( ++ Total number of transactions: {numberUtils.prettyValue(transactionsCount, 0)} +
+ )} + + {(tokensApiError || tokenBalanceInformationError) && ( +