From 864e051e5ccdc12a359d4179f33f2a54877db711 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Thu, 15 Aug 2024 09:19:47 -0300 Subject: [PATCH 01/11] feat: add list pagination to the nano contract history table --- src/api/nanoApi.js | 27 ++- src/components/nano/NanoContractHistory.js | 234 +++++++++++++++++++++ src/constants.js | 3 + src/screens/nano/NanoContractDetail.js | 63 +----- 4 files changed, 259 insertions(+), 68 deletions(-) create mode 100644 src/components/nano/NanoContractHistory.js diff --git a/src/api/nanoApi.js b/src/api/nanoApi.js index d0d2d151..a589e1e6 100644 --- a/src/api/nanoApi.js +++ b/src/api/nanoApi.js @@ -34,19 +34,28 @@ const nanoApi = { * Get the history of transactions of a nano contract * * @param {string} id Nano contract id + * @param {number | null} count Number of elements to get the history + * @param {string | null} after Hash of the tx to get as reference for after pagination + * @param {string | null} before Hash of the tx to get as reference for before pagination * * For more details, see full node api docs */ - getHistory(id) { + getHistory(id, count, after, before) { const data = { id }; - return requestExplorerServiceV1.get(`node_api/nc_history`, { params: data }).then( - res => { - return res.data; - }, - res => { - throw new Error(res.data.message); - } - ); + if (count) { + data['count'] = count; + } + if (after) { + data['after'] = after; + } + if (before) { + data['before'] = before; + } + return requestExplorerServiceV1.get(`node_api/nc_history`, {params: data}).then((res) => { + return res.data + }, (res) => { + throw new Error(res.data.message); + }); }, /** diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js new file mode 100644 index 00000000..4399b3df --- /dev/null +++ b/src/components/nano/NanoContractHistory.js @@ -0,0 +1,234 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useEffect, useState } from 'react'; +import Loading from '../../components/Loading'; +import { Link, useLocation } from 'react-router-dom'; +import { NANO_CONTRACT_TX_HISTORY_COUNT } from '../../constants'; +import TxRow from '../tx/TxRow'; +import helpers from '../../utils/helpers'; +import nanoApi from '../../api/nanoApi'; +import WebSocketHandler from '../../WebSocketHandler'; +import PaginationURL from '../../utils/pagination'; +import hathorLib from '@hathor/wallet-lib'; +import { reverse } from 'lodash'; + + +/** + * Displays nano tx history in a table with pagination buttons. As the user navigates through the history, + * the URL parameters 'hash' and 'page' are updated. + * + * Either all URL parameters are set or they are all missing. + * + * Example 1: + * hash = "00000000001b328fafb336b4515bb9557733fe93cf685dfd0c77cae3131f3fff" + * page = "previous" + * + * Example 2: + * hash = "00000000001b328fafb336b4515bb9557733fe93cf685dfd0c77cae3131f3fff" + * page = "next" + */ +function NanoContractHistory({ ncId }) { + const pagination = new PaginationURL({ + 'hash': {'required': false}, + 'page': {'required': false} + }); + + const location = useLocation(); + + // loading {boolean} Bool to show/hide loading element + const [loading, setLoading] = useState(true); + // history {Array} Nano contract history + const [history, setHistory] = useState([]); + // errorMessage {string} Message to show when error happens on history load + const [errorMessage, setErrorMessage] = useState(''); + // hasBefore {boolean} If 'Previous' button should be enabled + const [hasBefore, setHasBefore] = useState(false); + // hasAfter {boolean} If 'Next' button should be enabled + const [hasAfter, setHasAfter] = useState(false); + + useEffect(() => { + const queryParams = pagination.obtainQueryParams(); + let after = null; + let before = null; + if (queryParams.hash) { + if (queryParams.page === 'previous') { + before = queryParams.hash; + } else if (queryParams.page === 'next') { + after = queryParams.hash; + } else { + // Params are wrong + pagination.clearOptionalQueryParams(); + } + } + + loadData(after, before); + + // Handle new txs in the network to update the list in real time + WebSocketHandler.on('network', handleWebsocket); + + return () => { + WebSocketHandler.removeListener('network', handleWebsocket); + }; + }, [location]); + + const loadData = async (after, before) => { + try { + const data = await nanoApi.getHistory(ncId, NANO_CONTRACT_TX_HISTORY_COUNT, after, before); + if (data.history.length === 0) { + // XXX + // The hathor-core API does not return if it has more or not, so if the last page + // has exactly the number of elements of the list, we need to fetch another page + // to understand that the previous was the final one. In that case, we just + // return without updating the state + if (before) { + // It means we reached the first page going back, then we clear the query params + pagination.clearOptionalQueryParams(); + setHasBefore(false); + } + if (after) { + // It means we reached the last page going forward + setHasAfter(false); + } + return; + } + if (before) { + // When we are querying the previous set of transactions + // the API return the oldest first, so we need to revert the history + reverse(data.history); + } + setHistory(data.history); + + if (data.history.length < NANO_CONTRACT_TX_HISTORY_COUNT) { + // This was the last page + if (!after && !before) { + // This is the first load without query params, so we do nothing because + // previous and next are already disabled + return; + } + + if (after) { + setHasAfter(false); + setHasBefore(true); + return; + } + + if (before) { + setHasAfter(true); + setHasBefore(false); + return; + } + } else { + // This is not the last page + if (!after && !before) { + // This is the first load without query params, so we need to + // enable only the next button + setHasAfter(true); + setHasBefore(false); + return; + } + + // In all other cases, we must enable both buttons + // because this is not the last page + setHasAfter(true); + setHasBefore(true); + } + } catch (e) { + // Error in request + setErrorMessage('Error getting nano contract history.'); + } finally { + setLoading(false); + } + } + + const handleWebsocket = (wsData) => { + if (wsData.type === 'network:new_tx_accepted') { + updateListWs(wsData); + } + } + + const updateListWs = (tx) => { + // We only add to the list if it's the first page and it's a new tx from this nano + if (!hasBefore) { + if (tx.version === hathorLib.constants.NANO_CONTRACTS_VERSION && tx.nc_id === ncId) { + let nanoHistory = [...history]; + const willHaveAfter = (hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT) + // This updates the list with the new element at first + nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT); + + // Now update the history + setHistory(nanoHistory); + setHasAfter(willHaveAfter); + } + } + } + + if (errorMessage) { + return

{errorMessage}

; + } + + if (loading) { + return ; + } + + const loadTable = () => { + return ( +
+ + + + + + + + + + {loadTableBody()} + +
HashTimestampHash
Timestamp
+
+ ); + } + + const loadTableBody = () => { + return history.map((tx, idx) => { + // For some reason this API returns tx.hash instead of tx.tx_id like the others + tx.tx_id = tx.hash; + return ( + + ); + }); + } + + const loadPagination = () => { + if (history.length === 0) { + return null; + } else { + return ( + + ); + } + } + + return ( +
+ {loadTable()} + {loadPagination()} +
+ ); +} + +export default NanoContractHistory; diff --git a/src/constants.js b/src/constants.js index 205c2592..c8f134aa 100644 --- a/src/constants.js +++ b/src/constants.js @@ -107,3 +107,6 @@ export const UNLEASH_TIME_SERIES_FEATURE_FLAG = `explorer-timeseries-${REACT_APP export const { REACT_APP_TIMESERIES_DASHBOARD_ID } = process.env; export const TIMESERIES_DASHBOARD_URL = `https://hathor-explorer-75a9f9.kb.eu-central-1.aws.cloud.es.io:9243/s/anonymous-user/app/dashboards?auth_provider_hint=anonymous1#/view/${REACT_APP_TIMESERIES_DASHBOARD_ID}?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-1w%2Cto%3Anow))&show-time-filter=true&hide-filter-bar=true`; export const SCREEN_STATUS_LOOP_INTERVAL_IN_SECONDS = 60; // This is the interval that ElasticSearch takes to ingest data from blocks + +// Number of elements in the nano contract transaction history table +export const NANO_CONTRACT_TX_HISTORY_COUNT = 5; diff --git a/src/screens/nano/NanoContractDetail.js b/src/screens/nano/NanoContractDetail.js index f39f2e30..25c47a0c 100644 --- a/src/screens/nano/NanoContractDetail.js +++ b/src/screens/nano/NanoContractDetail.js @@ -6,6 +6,7 @@ */ import React, { useEffect, useState } from 'react'; +import NanoContractHistory from '../../components/nano/NanoContractHistory'; import hathorLib from '@hathor/wallet-lib'; import { Link } from 'react-router-dom'; import { useSelector } from 'react-redux'; @@ -26,14 +27,10 @@ function NanoContractDetail(props) { const [ncState, setNcState] = useState(null); // blueprintInformation {Object | null} Blueprint Information from API const [blueprintInformation, setBlueprintInformation] = useState(null); - // history {Array | null} Nano contract history - const [history, setHistory] = useState(null); // txData {Object | null} Nano contract transaction data const [txData, setTxData] = useState(null); // loadingDetail {boolean} Bool to show/hide loading when getting transaction detail const [loadingDetail, setLoadingDetail] = useState(true); - // loadingHistory {boolean} Bool to show/hide loading when getting nano history - const [loadingHistory, setLoadingHistory] = useState(true); // errorMessage {string | null} Error message in case a request to get nano contract data fails const [errorMessage, setErrorMessage] = useState(null); @@ -52,7 +49,7 @@ function NanoContractDetail(props) { const transactionData = await txApi.getTransaction(ncId); if (transactionData.tx.version !== hathorLib.constants.NANO_CONTRACTS_VERSION) { if (ignore) { - // This is to prevent setting a state after the componenet has been already cleaned + // This is to prevent setting a state after the component has been already cleaned return; } setErrorMessage('Transaction is not a nano contract.'); @@ -86,30 +83,7 @@ function NanoContractDetail(props) { } } - async function loadNCHistory() { - setLoadingHistory(true); - setHistory(null); - - try { - const data = await nanoApi.getHistory(ncId); - if (ignore) { - // This is to prevent setting a state after the componenet has been already cleaned - return; - } - setHistory(data.history); - setLoadingHistory(false); - } catch (e) { - if (ignore) { - // This is to prevent setting a state after the componenet has been already cleaned - return; - } - // Error in request - setErrorMessage('Error getting nano contract history.'); - setLoadingHistory(false); - } - } loadBlueprintInformation(); - loadNCHistory(); return () => { ignore = true; @@ -120,39 +94,10 @@ function NanoContractDetail(props) { return

{errorMessage}

; } - if (loadingHistory || loadingDetail) { + if (loadingDetail) { return ; } - const loadTable = () => { - return ( -
- - - - - - - - - {loadTableBody()} -
HashTimestamp - Hash -
- Timestamp -
-
- ); - }; - - const loadTableBody = () => { - return history.map((tx, idx) => { - // For some reason this API returns tx.hash instead of tx.tx_id like the others - tx.tx_id = tx.hash; - return ; - }); - }; - const renderBalances = () => { return Object.entries(ncState.balances).map(([tokenUid, data]) => ( @@ -238,7 +183,7 @@ function NanoContractDetail(props) { {renderNCBalances()}

History

- {history && loadTable()} + ); From c70b72c14ca2d67a5a806b1349d8be7cb0dc616a Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Thu, 15 Aug 2024 19:00:07 -0300 Subject: [PATCH 02/11] fix: handle WS method should be a useCallback and pagination object a useMemo --- src/components/nano/NanoContractHistory.js | 109 ++++++++++++--------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index 4399b3df..56c70820 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import Loading from '../../components/Loading'; import { Link, useLocation } from 'react-router-dom'; import { NANO_CONTRACT_TX_HISTORY_COUNT } from '../../constants'; @@ -33,10 +33,14 @@ import { reverse } from 'lodash'; * page = "next" */ function NanoContractHistory({ ncId }) { - const pagination = new PaginationURL({ - 'hash': {'required': false}, - 'page': {'required': false} - }); + // We must use memo here because we were creating a new pagination + // object in every new render, so the useEffect was being called forever + const pagination = useMemo(() => + new PaginationURL({ + 'hash': {'required': false}, + 'page': {'required': false} + }) + , []); const location = useLocation(); @@ -51,32 +55,32 @@ function NanoContractHistory({ ncId }) { // hasAfter {boolean} If 'Next' button should be enabled const [hasAfter, setHasAfter] = useState(false); - useEffect(() => { - const queryParams = pagination.obtainQueryParams(); - let after = null; - let before = null; - if (queryParams.hash) { - if (queryParams.page === 'previous') { - before = queryParams.hash; - } else if (queryParams.page === 'next') { - after = queryParams.hash; - } else { - // Params are wrong - pagination.clearOptionalQueryParams(); + // useCallback is important here to update this method with new history state + // otherwise it would be fixed the moment the event listener is started in the useEffect + // with the history as an empty array + const updateListWs = useCallback((tx) => { + // We only add to the list if it's the first page and it's a new tx from this nano + if (!hasBefore) { + if (tx.version === hathorLib.constants.NANO_CONTRACTS_VERSION && tx.nc_id === ncId) { + let nanoHistory = [...history]; + const willHaveAfter = (hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT) + // This updates the list with the new element at first + nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT); + + // Now update the history + setHistory(nanoHistory); + setHasAfter(willHaveAfter); } } - - loadData(after, before); - - // Handle new txs in the network to update the list in real time - WebSocketHandler.on('network', handleWebsocket); - - return () => { - WebSocketHandler.removeListener('network', handleWebsocket); - }; - }, [location]); - - const loadData = async (after, before) => { + }, [history, hasAfter, hasBefore, ncId]); + + /** + * useCallback is needed here because this method is used as a dependency in the useEffect + * + * after {string | null} Hash to use for pagination when user clicks to fetch the next page + * before {string | null} Hash to use for pagination when user clicks to fetch the previous page + */ + const loadData = useCallback(async (after, before) => { try { const data = await nanoApi.getHistory(ncId, NANO_CONTRACT_TX_HISTORY_COUNT, after, before); if (data.history.length === 0) { @@ -143,27 +147,40 @@ function NanoContractHistory({ ncId }) { } finally { setLoading(false); } - } + }, [ncId, pagination]); - const handleWebsocket = (wsData) => { - if (wsData.type === 'network:new_tx_accepted') { - updateListWs(wsData); + useEffect(() => { + // Handle load history depending on the query params in the URL + const queryParams = pagination.obtainQueryParams(); + let after = null; + let before = null; + if (queryParams.hash) { + if (queryParams.page === 'previous') { + before = queryParams.hash; + } else if (queryParams.page === 'next') { + after = queryParams.hash; + } else { + // Params are wrong + pagination.clearOptionalQueryParams(); + } } - } - const updateListWs = (tx) => { - // We only add to the list if it's the first page and it's a new tx from this nano - if (!hasBefore) { - if (tx.version === hathorLib.constants.NANO_CONTRACTS_VERSION && tx.nc_id === ncId) { - let nanoHistory = [...history]; - const willHaveAfter = (hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT) - // This updates the list with the new element at first - nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT); + loadData(after, before); - // Now update the history - setHistory(nanoHistory); - setHasAfter(willHaveAfter); - } + }, [location, loadData, pagination]); + + useEffect(() => { + // Handle new txs in the network to update the list in real time + WebSocketHandler.on('network', handleWebsocket); + + return () => { + WebSocketHandler.removeListener('network', handleWebsocket); + }; + }); + + const handleWebsocket = (wsData) => { + if (wsData.type === 'network:new_tx_accepted') { + updateListWs(wsData); } } From ac15674f30b6e29082f1560d1f3eebe7bf76fa3a Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Fri, 16 Aug 2024 11:00:15 -0300 Subject: [PATCH 03/11] feat: improve pagination logic with new has_more API response value --- src/components/nano/NanoContractHistory.js | 87 +++++++++------------- 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index 56c70820..ce1da773 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -55,9 +55,13 @@ function NanoContractHistory({ ncId }) { // hasAfter {boolean} If 'Next' button should be enabled const [hasAfter, setHasAfter] = useState(false); - // useCallback is important here to update this method with new history state - // otherwise it would be fixed the moment the event listener is started in the useEffect - // with the history as an empty array + /** + * useCallback is important here to update this method with new history state + * otherwise it would be fixed the moment the event listener is started in the useEffect + * with the history as an empty array + * + * tx {Transaction} Transaction object that arrived from the websocket + */ const updateListWs = useCallback((tx) => { // We only add to the list if it's the first page and it's a new tx from this nano if (!hasBefore) { @@ -83,23 +87,6 @@ function NanoContractHistory({ ncId }) { const loadData = useCallback(async (after, before) => { try { const data = await nanoApi.getHistory(ncId, NANO_CONTRACT_TX_HISTORY_COUNT, after, before); - if (data.history.length === 0) { - // XXX - // The hathor-core API does not return if it has more or not, so if the last page - // has exactly the number of elements of the list, we need to fetch another page - // to understand that the previous was the final one. In that case, we just - // return without updating the state - if (before) { - // It means we reached the first page going back, then we clear the query params - pagination.clearOptionalQueryParams(); - setHasBefore(false); - } - if (after) { - // It means we reached the last page going forward - setHasAfter(false); - } - return; - } if (before) { // When we are querying the previous set of transactions // the API return the oldest first, so we need to revert the history @@ -107,39 +94,33 @@ function NanoContractHistory({ ncId }) { } setHistory(data.history); - if (data.history.length < NANO_CONTRACT_TX_HISTORY_COUNT) { - // This was the last page - if (!after && !before) { - // This is the first load without query params, so we do nothing because - // previous and next are already disabled - return; - } - - if (after) { - setHasAfter(false); - setHasBefore(true); - return; - } + if (!after && !before) { + // This is the first load without query params, so if has_more === true + // we must enable next button + setHasAfter(data.has_more); + setHasBefore(false); + return; + } - if (before) { - setHasAfter(true); - setHasBefore(false); - return; - } - } else { - // This is not the last page - if (!after && !before) { - // This is the first load without query params, so we need to - // enable only the next button - setHasAfter(true); - setHasBefore(false); - return; - } + if (after) { + // We clicked the next button, so we have before page + // and we will have the next page if has_more === true + setHasAfter(data.has_more); + setHasBefore(true); + return; + } - // In all other cases, we must enable both buttons - // because this is not the last page + if (before) { + // We clicked the previous button, so we have next page + // and we will have the previous page if has_more === true setHasAfter(true); - setHasBefore(true); + setHasBefore(data.has_more); + if (!data.has_more) { + // We are in the first page and clicked the Previous button + // so we must clear the query params + pagination.clearOptionalQueryParams(); + } + return; } } catch (e) { // Error in request @@ -178,6 +159,12 @@ function NanoContractHistory({ ncId }) { }; }); + /** + * Method to handle websocket messages that arrive in the network scope + * This method will discard any messages that are not new transactions + * + * wsData {Object} Data send in the websocket message + */ const handleWebsocket = (wsData) => { if (wsData.type === 'network:new_tx_accepted') { updateListWs(wsData); From b879bb04d69023c2e65adddeef3762c6d5290732 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:08:03 -0300 Subject: [PATCH 04/11] fix: websocker handler effect should run only once --- src/components/nano/NanoContractHistory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index ce1da773..2ffba46e 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -157,7 +157,7 @@ function NanoContractHistory({ ncId }) { return () => { WebSocketHandler.removeListener('network', handleWebsocket); }; - }); + }, []); /** * Method to handle websocket messages that arrive in the network scope From 3c5cf6f045350c20e0c34e900210002f36738560 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:08:35 -0300 Subject: [PATCH 05/11] docs: fix param docstring --- src/components/nano/NanoContractHistory.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index 2ffba46e..d91af07e 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -60,7 +60,7 @@ function NanoContractHistory({ ncId }) { * otherwise it would be fixed the moment the event listener is started in the useEffect * with the history as an empty array * - * tx {Transaction} Transaction object that arrived from the websocket + * @param {Transaction} tx Transaction object that arrived from the websocket */ const updateListWs = useCallback((tx) => { // We only add to the list if it's the first page and it's a new tx from this nano @@ -81,8 +81,8 @@ function NanoContractHistory({ ncId }) { /** * useCallback is needed here because this method is used as a dependency in the useEffect * - * after {string | null} Hash to use for pagination when user clicks to fetch the next page - * before {string | null} Hash to use for pagination when user clicks to fetch the previous page + * @param {string | null} after Hash to use for pagination when user clicks to fetch the next page + * @param {string | null} before Hash to use for pagination when user clicks to fetch the previous page */ const loadData = useCallback(async (after, before) => { try { From 385b62e6bee107040117aa0c50e752357fd9f3dc Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:08:53 -0300 Subject: [PATCH 06/11] refactor: improve code readability --- src/components/nano/NanoContractHistory.js | 26 +++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index d91af07e..25c03e7c 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -64,18 +64,22 @@ function NanoContractHistory({ ncId }) { */ const updateListWs = useCallback((tx) => { // We only add to the list if it's the first page and it's a new tx from this nano - if (!hasBefore) { - if (tx.version === hathorLib.constants.NANO_CONTRACTS_VERSION && tx.nc_id === ncId) { - let nanoHistory = [...history]; - const willHaveAfter = (hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT) - // This updates the list with the new element at first - nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT); - - // Now update the history - setHistory(nanoHistory); - setHasAfter(willHaveAfter); - } + if (hasBefore) { + return; + } + + if (tx.version !== hathorLib.constants.NANO_CONTRACTS_VERSION || tx.nc_id !== ncId) { + return; } + + let nanoHistory = [...history]; + const willHaveAfter = (hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT) + // This updates the list with the new element at first + nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT); + + // Now update the history + setHistory(nanoHistory); + setHasAfter(willHaveAfter); }, [history, hasAfter, hasBefore, ncId]); /** From 86d12583e1c92c3058888354112d4a370c885d31 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:14:19 -0300 Subject: [PATCH 07/11] chore: apply linter --- src/components/nano/NanoContractHistory.js | 214 +++++++++++---------- 1 file changed, 117 insertions(+), 97 deletions(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index 25c03e7c..2c3715c9 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -6,17 +6,16 @@ */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import Loading from '../../components/Loading'; import { Link, useLocation } from 'react-router-dom'; +import hathorLib from '@hathor/wallet-lib'; +import { reverse } from 'lodash'; +import Loading from '../Loading'; import { NANO_CONTRACT_TX_HISTORY_COUNT } from '../../constants'; import TxRow from '../tx/TxRow'; import helpers from '../../utils/helpers'; import nanoApi from '../../api/nanoApi'; import WebSocketHandler from '../../WebSocketHandler'; import PaginationURL from '../../utils/pagination'; -import hathorLib from '@hathor/wallet-lib'; -import { reverse } from 'lodash'; - /** * Displays nano tx history in a table with pagination buttons. As the user navigates through the history, @@ -35,12 +34,14 @@ import { reverse } from 'lodash'; function NanoContractHistory({ ncId }) { // We must use memo here because we were creating a new pagination // object in every new render, so the useEffect was being called forever - const pagination = useMemo(() => - new PaginationURL({ - 'hash': {'required': false}, - 'page': {'required': false} - }) - , []); + const pagination = useMemo( + () => + new PaginationURL({ + hash: { required: false }, + page: { required: false }, + }), + [] + ); const location = useLocation(); @@ -50,9 +51,9 @@ function NanoContractHistory({ ncId }) { const [history, setHistory] = useState([]); // errorMessage {string} Message to show when error happens on history load const [errorMessage, setErrorMessage] = useState(''); - // hasBefore {boolean} If 'Previous' button should be enabled + // hasBefore {boolean} If 'Previous' button should be enabled const [hasBefore, setHasBefore] = useState(false); - // hasAfter {boolean} If 'Next' button should be enabled + // hasAfter {boolean} If 'Next' button should be enabled const [hasAfter, setHasAfter] = useState(false); /** @@ -62,25 +63,28 @@ function NanoContractHistory({ ncId }) { * * @param {Transaction} tx Transaction object that arrived from the websocket */ - const updateListWs = useCallback((tx) => { - // We only add to the list if it's the first page and it's a new tx from this nano - if (hasBefore) { - return; - } + const updateListWs = useCallback( + tx => { + // We only add to the list if it's the first page and it's a new tx from this nano + if (hasBefore) { + return; + } - if (tx.version !== hathorLib.constants.NANO_CONTRACTS_VERSION || tx.nc_id !== ncId) { - return; - } + if (tx.version !== hathorLib.constants.NANO_CONTRACTS_VERSION || tx.nc_id !== ncId) { + return; + } - let nanoHistory = [...history]; - const willHaveAfter = (hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT) - // This updates the list with the new element at first - nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT); + let nanoHistory = [...history]; + const willHaveAfter = hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT; + // This updates the list with the new element at first + nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT); - // Now update the history - setHistory(nanoHistory); - setHasAfter(willHaveAfter); - }, [history, hasAfter, hasBefore, ncId]); + // Now update the history + setHistory(nanoHistory); + setHasAfter(willHaveAfter); + }, + [history, hasAfter, hasBefore, ncId] + ); /** * useCallback is needed here because this method is used as a dependency in the useEffect @@ -88,51 +92,54 @@ function NanoContractHistory({ ncId }) { * @param {string | null} after Hash to use for pagination when user clicks to fetch the next page * @param {string | null} before Hash to use for pagination when user clicks to fetch the previous page */ - const loadData = useCallback(async (after, before) => { - try { - const data = await nanoApi.getHistory(ncId, NANO_CONTRACT_TX_HISTORY_COUNT, after, before); - if (before) { - // When we are querying the previous set of transactions - // the API return the oldest first, so we need to revert the history - reverse(data.history); - } - setHistory(data.history); + const loadData = useCallback( + async (after, before) => { + try { + const data = await nanoApi.getHistory(ncId, NANO_CONTRACT_TX_HISTORY_COUNT, after, before); + if (before) { + // When we are querying the previous set of transactions + // the API return the oldest first, so we need to revert the history + reverse(data.history); + } + setHistory(data.history); - if (!after && !before) { - // This is the first load without query params, so if has_more === true - // we must enable next button - setHasAfter(data.has_more); - setHasBefore(false); - return; - } + if (!after && !before) { + // This is the first load without query params, so if has_more === true + // we must enable next button + setHasAfter(data.has_more); + setHasBefore(false); + return; + } - if (after) { - // We clicked the next button, so we have before page - // and we will have the next page if has_more === true - setHasAfter(data.has_more); - setHasBefore(true); - return; - } + if (after) { + // We clicked the next button, so we have before page + // and we will have the next page if has_more === true + setHasAfter(data.has_more); + setHasBefore(true); + return; + } - if (before) { - // We clicked the previous button, so we have next page - // and we will have the previous page if has_more === true - setHasAfter(true); - setHasBefore(data.has_more); - if (!data.has_more) { - // We are in the first page and clicked the Previous button - // so we must clear the query params - pagination.clearOptionalQueryParams(); + if (before) { + // We clicked the previous button, so we have next page + // and we will have the previous page if has_more === true + setHasAfter(true); + setHasBefore(data.has_more); + if (!data.has_more) { + // We are in the first page and clicked the Previous button + // so we must clear the query params + pagination.clearOptionalQueryParams(); + } + return; } - return; + } catch (e) { + // Error in request + setErrorMessage('Error getting nano contract history.'); + } finally { + setLoading(false); } - } catch (e) { - // Error in request - setErrorMessage('Error getting nano contract history.'); - } finally { - setLoading(false); - } - }, [ncId, pagination]); + }, + [ncId, pagination] + ); useEffect(() => { // Handle load history depending on the query params in the URL @@ -151,7 +158,6 @@ function NanoContractHistory({ ncId }) { } loadData(after, before); - }, [location, loadData, pagination]); useEffect(() => { @@ -169,14 +175,14 @@ function NanoContractHistory({ ncId }) { * * wsData {Object} Data send in the websocket message */ - const handleWebsocket = (wsData) => { + const handleWebsocket = wsData => { if (wsData.type === 'network:new_tx_accepted') { updateListWs(wsData); } - } + }; if (errorMessage) { - return

{errorMessage}

; + return

{errorMessage}

; } if (loading) { @@ -191,45 +197,59 @@ function NanoContractHistory({ ncId }) { Hash Timestamp - Hash
Timestamp + + Hash +
+ Timestamp + - - {loadTableBody()} - + {loadTableBody()} ); - } + }; const loadTableBody = () => { - return history.map((tx, idx) => { + return history.map(tx => { // For some reason this API returns tx.hash instead of tx.tx_id like the others - tx.tx_id = tx.hash; - return ( - - ); + const rowTx = { ...tx }; + rowTx.tx_id = rowTx.hash; + return ; }); - } + }; const loadPagination = () => { if (history.length === 0) { return null; - } else { - return ( - - ); } - } + return ( + + ); + }; return (
From 8d548300439357b36bc77e22ed68f16fe86af434 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:27:17 -0300 Subject: [PATCH 08/11] chore: remove leftover import --- src/screens/nano/NanoContractDetail.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/screens/nano/NanoContractDetail.js b/src/screens/nano/NanoContractDetail.js index 25c47a0c..59b43cee 100644 --- a/src/screens/nano/NanoContractDetail.js +++ b/src/screens/nano/NanoContractDetail.js @@ -11,7 +11,6 @@ import hathorLib from '@hathor/wallet-lib'; import { Link } from 'react-router-dom'; import { useSelector } from 'react-redux'; import Loading from '../../components/Loading'; -import TxRow from '../../components/tx/TxRow'; import nanoApi from '../../api/nanoApi'; import txApi from '../../api/txApi'; From 5de13b1958114f10d96865dc1719e4447874f912 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:29:10 -0300 Subject: [PATCH 09/11] chore: fix linter --- src/api/nanoApi.js | 19 +++++++++++-------- src/screens/nano/NanoContractDetail.js | 8 ++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/api/nanoApi.js b/src/api/nanoApi.js index a589e1e6..f0228450 100644 --- a/src/api/nanoApi.js +++ b/src/api/nanoApi.js @@ -43,19 +43,22 @@ const nanoApi = { getHistory(id, count, after, before) { const data = { id }; if (count) { - data['count'] = count; + data.count = count; } if (after) { - data['after'] = after; + data.after = after; } if (before) { - data['before'] = before; + data.before = before; } - return requestExplorerServiceV1.get(`node_api/nc_history`, {params: data}).then((res) => { - return res.data - }, (res) => { - throw new Error(res.data.message); - }); + return requestExplorerServiceV1.get(`node_api/nc_history`, { params: data }).then( + res => { + return res.data; + }, + res => { + throw new Error(res.data.message); + } + ); }, /** diff --git a/src/screens/nano/NanoContractDetail.js b/src/screens/nano/NanoContractDetail.js index 59b43cee..62431a9e 100644 --- a/src/screens/nano/NanoContractDetail.js +++ b/src/screens/nano/NanoContractDetail.js @@ -6,10 +6,10 @@ */ import React, { useEffect, useState } from 'react'; -import NanoContractHistory from '../../components/nano/NanoContractHistory'; import hathorLib from '@hathor/wallet-lib'; import { Link } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import NanoContractHistory from '../../components/nano/NanoContractHistory'; import Loading from '../../components/Loading'; import nanoApi from '../../api/nanoApi'; import txApi from '../../api/txApi'; @@ -55,12 +55,12 @@ function NanoContractDetail(props) { return; } - const blueprintInformation = await nanoApi.getBlueprintInformation( + const blueprintInformationData = await nanoApi.getBlueprintInformation( transactionData.tx.nc_blueprint_id ); const dataState = await nanoApi.getState( ncId, - Object.keys(blueprintInformation.attributes), + Object.keys(blueprintInformationData.attributes), ['__all__'], [] ); @@ -68,7 +68,7 @@ function NanoContractDetail(props) { // This is to prevent setting a state after the componenet has been already cleaned return; } - setBlueprintInformation(blueprintInformation); + setBlueprintInformation(blueprintInformationData); setNcState(dataState); setTxData(transactionData.tx); setLoadingDetail(false); From 960d86bc98537907c965164885b27a7dc125790b Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:32:08 -0300 Subject: [PATCH 10/11] feat: add usecallback to the handle websocket method --- src/components/nano/NanoContractHistory.js | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index 2c3715c9..12c08f7f 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -141,6 +141,21 @@ function NanoContractHistory({ ncId }) { [ncId, pagination] ); + /** + * useCallback is needed here because this method is used as a dependency in the useEffect + * + * Method to handle websocket messages that arrive in the network scope + * This method will discard any messages that are not new transactions + * + * wsData {Object} Data send in the websocket message + */ + const handleWebsocket = useCallback( + wsData => { + if (wsData.type === 'network:new_tx_accepted') { + updateListWs(wsData); + } + }, [updateListWs]); + useEffect(() => { // Handle load history depending on the query params in the URL const queryParams = pagination.obtainQueryParams(); @@ -167,19 +182,7 @@ function NanoContractHistory({ ncId }) { return () => { WebSocketHandler.removeListener('network', handleWebsocket); }; - }, []); - - /** - * Method to handle websocket messages that arrive in the network scope - * This method will discard any messages that are not new transactions - * - * wsData {Object} Data send in the websocket message - */ - const handleWebsocket = wsData => { - if (wsData.type === 'network:new_tx_accepted') { - updateListWs(wsData); - } - }; + }, [handleWebsocket]); if (errorMessage) { return

{errorMessage}

; From 0de8d2773c8c50672dc5873c887607ee3e4cfde8 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Mon, 19 Aug 2024 23:35:22 -0300 Subject: [PATCH 11/11] chore: run prettier fix --- src/components/nano/NanoContractHistory.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js index 12c08f7f..a536ec0f 100644 --- a/src/components/nano/NanoContractHistory.js +++ b/src/components/nano/NanoContractHistory.js @@ -151,10 +151,12 @@ function NanoContractHistory({ ncId }) { */ const handleWebsocket = useCallback( wsData => { - if (wsData.type === 'network:new_tx_accepted') { - updateListWs(wsData); - } - }, [updateListWs]); + if (wsData.type === 'network:new_tx_accepted') { + updateListWs(wsData); + } + }, + [updateListWs] + ); useEffect(() => { // Handle load history depending on the query params in the URL