diff --git a/package.json b/package.json index 6c1b018..5ba7f29 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "date-fns": "^2.30.0", "dayjs": "^1.11.7", "ethers": "^6.3.0", + "history": "^5.3.0", "react": "17.0.2", "react-dom": "17.0.2", "react-router-dom": "^6.14.2", diff --git a/src/components/ExplorerTable/ExplorerTable.tsx b/src/components/ExplorerTable/ExplorerTable.tsx index 3cbc479..1568d5e 100644 --- a/src/components/ExplorerTable/ExplorerTable.tsx +++ b/src/components/ExplorerTable/ExplorerTable.tsx @@ -63,7 +63,11 @@ const ExplorerTable: React.FC = ({ state, sharedConfig }: Explore {txHash !== undefined ? ( - + {txHash} ) : ( diff --git a/src/context/ExplorerContext.tsx b/src/context/ExplorerContext.tsx index 00041ff..a7e9c16 100644 --- a/src/context/ExplorerContext.tsx +++ b/src/context/ExplorerContext.tsx @@ -1,11 +1,12 @@ import React, { useEffect } from "react" -import type { ExplorerContextState, ExplorerContext as ExplorerContextType, ExplorerState, SharedConfig, SharedConfigDomain } from "../types" +import type { ExplorerContextState, ExplorerContext as ExplorerContextType, ExplorerState } from "../types" import { getAccount, getChainId } from "./connection" import { routes } from "./data" import { reducer } from "./reducer" import { useGetTransferData } from "./useGetTransferData" +import { useGetSharedConfig } from "./useGetSharedConfig" const ExplorerCtx = React.createContext(undefined) @@ -22,6 +23,7 @@ const ExplorerProvider = ({ children }: { children: React.ReactNode | React.Reac transferDetails: undefined, pillColorStatus: undefined, account: undefined, + sharedConfig: [], } const [explorerContextState, explorerContextDispatcher] = React.useReducer(reducer, explorerPageContextState) @@ -30,20 +32,12 @@ const ExplorerProvider = ({ children }: { children: React.ReactNode | React.Reac const [account, setAccount] = React.useState(undefined) const [explorerUrls, setExplorerUrls] = React.useState<[] | ExplorerState["explorerUrls"]>([]) - const [sharedConfig, setSharedConfig] = React.useState([]) - - const getSharedConfig = async (): Promise => { - const reponse = await fetch(import.meta.env.VITE_SHARED_CONFIG_URL as string) - const domainsData = (await reponse.json()) as SharedConfig - - setSharedConfig(domainsData.domains) - localStorage.setItem("sharedConfig", JSON.stringify(domainsData)) - } - const { search } = window.location const urlParams = new URLSearchParams(search) const page = urlParams.get("page") + useGetSharedConfig(explorerContextDispatcher) + useGetTransferData(routes(), explorerContextDispatcher, explorerContextState, Number(page)) useEffect(() => { @@ -57,8 +51,6 @@ const ExplorerProvider = ({ children }: { children: React.ReactNode | React.Reac }) } - void getSharedConfig() - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument setExplorerUrls(JSON.parse(import.meta.env.VITE_EXPLORER_URLS)) @@ -80,8 +72,6 @@ const ExplorerProvider = ({ children }: { children: React.ReactNode | React.Reac chainId, account, routes: routes(), - sharedConfig, - setSharedConfig, explorerUrls, }} > diff --git a/src/context/data/data.ts b/src/context/data/data.ts index b995511..41f2b28 100644 --- a/src/context/data/data.ts +++ b/src/context/data/data.ts @@ -12,6 +12,12 @@ export const fetchTransfer = async (url: string): Promise => { return transfer } +export const fetchTransferByTxHash = async (url: string): Promise => { + const response = await fetch(url) + const data = (await response.json()) as Transfer | Transfer[] + return data +} + export const routes = (): Routes => { const { VITE_INDEXER_URL } = import.meta.env @@ -23,5 +29,6 @@ export const routes = (): Routes => { transfer: async (id: string) => await fetchTransfer(`${indexerUrl}/transfers/${id}`), transferBySender: async (sender: string, page: string, limit: string) => await fetchTransfers(`${indexerUrl}/sender/${sender}/transfers?limit=${limit}&page=${page}`), + transferByTransactionHash: async (txHash: string) => await fetchTransferByTxHash(`${indexerUrl}/transfers/txHash/${txHash}`), } } diff --git a/src/context/reducer.ts b/src/context/reducer.ts index 09ecf89..c8aecd4 100644 --- a/src/context/reducer.ts +++ b/src/context/reducer.ts @@ -33,6 +33,11 @@ export function reducer(state: ExplorerContextState, action: Actions): ExplorerC ...state, error: action.payload, } + case "fetch_shared_config": + return { + ...state, + sharedConfig: action.payload, + } default: return state } diff --git a/src/context/useGetSharedConfig.ts b/src/context/useGetSharedConfig.ts new file mode 100644 index 0000000..a9bc379 --- /dev/null +++ b/src/context/useGetSharedConfig.ts @@ -0,0 +1,18 @@ +import { useEffect } from "react" +import { Actions, SharedConfig } from "../types" + +export function useGetSharedConfig(explorerContextDispatcher: React.Dispatch): void { + const getSharedConfig = async (): Promise => { + const reponse = await fetch(import.meta.env.VITE_SHARED_CONFIG_URL as string) + const domainsData = (await reponse.json()) as SharedConfig + + explorerContextDispatcher({ + type: "fetch_shared_config", + payload: domainsData.domains, + }) + } + + useEffect(() => { + void getSharedConfig() + }, []) +} diff --git a/src/pages/DetailView/DetailView.tsx b/src/pages/DetailView/DetailView.tsx index a98a43c..3f20eda 100644 --- a/src/pages/DetailView/DetailView.tsx +++ b/src/pages/DetailView/DetailView.tsx @@ -30,14 +30,18 @@ import useUpdateInterval from "./hooks/useUpdateInterval" dayjs.extend(localizedFormat) +export type LocationT = { + state: { id: string; page: number; txHash: string } +} + export default function DetailView() { const explorerContext = useExplorer() - const { sharedConfig, setSharedConfig, explorerUrls, routes } = explorerContext + const { explorerUrls, routes, explorerContextState } = explorerContext const { classes } = useStyles() - const { state: data } = useLocation() as { state: { id: string; page: number } } + const { state: data } = useLocation() as LocationT const initState: DetailViewState = { transferDetails: null, @@ -46,20 +50,22 @@ export default function DetailView() { clipboardMessageT2: "Copy to clipboard", delay: 5000, fetchingStatus: "idle", + isLoading: "none", + fallbackPage: 1, } const [state, dispatcher] = useReducer(reducer, initState) useClipboard(state, dispatcher) - useFetchTransfer(routes, sharedConfig, setSharedConfig, data, dispatcher) + useFetchTransfer(routes, data?.txHash, dispatcher) - useUpdateInterval(state, dispatcher, data, routes) + useUpdateInterval(state, dispatcher, data?.txHash, routes) // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const renderTransferDetails = (transfer: Transfer | null) => { - const fromDomainInfo = getDomainData(transfer?.fromDomainId!, sharedConfig) - const toDomainInfo = getDomainData(transfer?.toDomainId!, sharedConfig) + const fromDomainInfo = getDomainData(transfer?.fromDomainId!, explorerContextState.sharedConfig) + const toDomainInfo = getDomainData(transfer?.toDomainId!, explorerContextState.sharedConfig) const { resource, usdValue } = transfer as Transfer @@ -207,6 +213,7 @@ export default function DetailView() { Fees: {getFormatedFee(transfer?.fee!, fromDomainInfo!)} + {Array.isArray(state.transferDetails) &&
} ) } @@ -214,11 +221,11 @@ export default function DetailView() { return ( - {state.transferStatus !== "none" ? ( + {state.isLoading === "done" && explorerContextState.sharedConfig.length && (
Transaction Detail - {renderTransferDetails(state.transferDetails)} + + {Array.isArray(state.transferDetails) + ? state.transferDetails.map(transfer => renderTransferDetails(transfer)) + : renderTransferDetails(state.transferDetails)} +
- ) : ( - + )} + {state.isLoading === "loading" && ( + + + )}
diff --git a/src/pages/DetailView/hooks/useFetchTransfer.ts b/src/pages/DetailView/hooks/useFetchTransfer.ts index 0060009..a681936 100644 --- a/src/pages/DetailView/hooks/useFetchTransfer.ts +++ b/src/pages/DetailView/hooks/useFetchTransfer.ts @@ -1,23 +1,28 @@ import { useEffect } from "react" -import { useParams } from "react-router-dom" +import history from "history/browser" import { sanitizeTransferData } from "../../../utils/Helpers" -import { Routes, SharedConfig, SharedConfigDomain, Transfer } from "../../../types" +import { Routes, Transfer } from "../../../types" import { DetailViewActions } from "../reducer" -export default function useFetchTransfer( - routes: Routes, - sharedConfig: SharedConfigDomain[] | [], - setSharedConfig: React.Dispatch>, - transferId: { id: string } | null, - dispatcher: React.Dispatch, -): void { - const fetchTransfer = async (): Promise => { - const transfer = await routes.transfer(transferId!.id) - const sanitizedTransfer = sanitizeTransferData([transfer]) +export default function useFetchTransfer(routes: Routes, txHash: string, dispatcher: React.Dispatch): void { + const fetchTransfer = async (txHashFallback?: string): Promise => { + dispatcher({ + type: "fetch_transfer", + }) + + let transfer: Transfer | Transfer[] + + if (txHashFallback) { + transfer = await routes.transferByTransactionHash(txHashFallback) + } else { + transfer = await routes.transferByTransactionHash(txHash) + } + + const sanitizedTransfer = Array.isArray(transfer) ? sanitizeTransferData([...transfer]) : sanitizeTransferData([transfer]) dispatcher({ type: "set_transfer_details", - payload: sanitizedTransfer[0], + payload: sanitizedTransfer, }) dispatcher({ @@ -31,50 +36,14 @@ export default function useFetchTransfer( }) } - // fallback when you are opening the detail view on new tab - const params = useParams() - - const getTransfersFromLocalStorage = (): void => { - const transfers = localStorage.getItem("transfers") - const { txHash } = params - const parsedTransfers = JSON.parse(transfers!) as Transfer[] - const transfer = parsedTransfers.find(transfer => transfer.deposit?.txHash === txHash) - - if (transfer) { - dispatcher({ - type: "set_transfer_details", - payload: transfer, - }) - - dispatcher({ - type: "set_transfer_status", - payload: "completed", - }) - - dispatcher({ - type: "update_fetching_status", - payload: "fetching", - }) - } - } - - const getSharedConfigFromLocalStorage = (): void => { - const sharedConfig = localStorage.getItem("sharedConfig") - const parsedSharedConfig = JSON.parse(sharedConfig!) as SharedConfig - - setSharedConfig(parsedSharedConfig.domains) - } - useEffect(() => { - if (transferId !== null) { + if (txHash !== undefined) { void fetchTransfer() } else { - getTransfersFromLocalStorage() - } - - // fallback because ExplorerState is new coming to a new tab - if (sharedConfig.length === 0) { - getSharedConfigFromLocalStorage() + const { pathname } = history.location + const txHashFallback = pathname.split("/").filter(Boolean)[1] + history.replace(history.location.pathname, { txHash, page: 1, id: "" }) + void fetchTransfer(txHashFallback) } - }, []) + }, [txHash]) } diff --git a/src/pages/DetailView/hooks/useUpdateInterval.ts b/src/pages/DetailView/hooks/useUpdateInterval.ts index 9186d4c..dd2f4e8 100644 --- a/src/pages/DetailView/hooks/useUpdateInterval.ts +++ b/src/pages/DetailView/hooks/useUpdateInterval.ts @@ -6,22 +6,30 @@ import { Routes } from "../../../types" export default function useUpdateInterval( state: DetailViewState, dispatcher: React.Dispatch, - transferId: { id: string } | null, + txHash: string, routes: Routes, ): void { const fetchUpdatedTransfer = async (): Promise => { - const transfer = await routes.transfer(transferId!.id) - const sanitizedTransfer = sanitizeTransferData([transfer]) + dispatcher({ + type: "fetch_transfer", + }) + + const transfer = await routes.transferByTransactionHash(txHash) + const sanitizedTransfer = Array.isArray(transfer) ? sanitizeTransferData([...transfer]) : sanitizeTransferData([transfer]) dispatcher({ type: "set_transfer_details", - payload: sanitizedTransfer[0], + payload: sanitizedTransfer, }) } useInterval( () => { - if (state.transferDetails?.status !== "executed") { + const isExecuted = Array.isArray(state.transferDetails) + ? state.transferDetails.every(transfer => transfer.status === "executed") + : state.transferDetails?.status === "executed" + + if (!isExecuted) { void fetchUpdatedTransfer() } else { dispatcher({ diff --git a/src/pages/DetailView/reducer.ts b/src/pages/DetailView/reducer.ts index 69974af..9a255ad 100644 --- a/src/pages/DetailView/reducer.ts +++ b/src/pages/DetailView/reducer.ts @@ -1,20 +1,23 @@ import { Transfer } from "../../types" export type DetailViewState = { - transferDetails: Transfer | null + transferDetails: Transfer | Transfer[] | null transferStatus: "none" | "completed" clipboardMessageT1: string clipboardMessageT2: string delay: number fetchingStatus: "fetching" | "idle" + isLoading: "none" | "loading" | "done" + fallbackPage: number } export type DetailViewActions = - | { type: "set_transfer_details"; payload: Transfer } + | { type: "set_transfer_details"; payload: Transfer | Transfer[] } | { type: "set_transfer_status"; payload: "none" | "completed" } | { type: "set_clipboard_message_t1"; payload: string } | { type: "set_clipboard_message_t2"; payload: string } | { type: "update_fetching_status"; payload: "fetching" | "idle" } + | { type: "fetch_transfer" } export function reducer(state: DetailViewState, action: DetailViewActions): DetailViewState { switch (action.type) { @@ -22,6 +25,7 @@ export function reducer(state: DetailViewState, action: DetailViewActions): Deta return { ...state, transferDetails: action.payload, + isLoading: "done", } } case "set_transfer_status": { @@ -48,6 +52,12 @@ export function reducer(state: DetailViewState, action: DetailViewActions): Deta fetchingStatus: action.payload, } } + case "fetch_transfer": { + return { + ...state, + isLoading: "loading", + } + } default: return state } diff --git a/src/pages/DetailView/styles.ts b/src/pages/DetailView/styles.ts index d9de332..e4b1ee4 100644 --- a/src/pages/DetailView/styles.ts +++ b/src/pages/DetailView/styles.ts @@ -17,6 +17,13 @@ export const useStyles = makeStyles()(theme => { gridTemplateColumns: "repeat(12, 1fr)", }, }, + circularProgress: { + gridColumn: "6 / span 2", + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "100%", + }, sectionContainer: { display: "flex", flexDirection: "column", diff --git a/src/pages/ExplorerPage/ExplorerPage.tsx b/src/pages/ExplorerPage/ExplorerPage.tsx index b6a2c5f..ddafe3c 100644 --- a/src/pages/ExplorerPage/ExplorerPage.tsx +++ b/src/pages/ExplorerPage/ExplorerPage.tsx @@ -9,7 +9,7 @@ import { useStyles } from "./styles" const ExplorerPage = (): JSX.Element => { const explorerContext = useExplorer() - const { explorerContextDispatcher, explorerContextState, sharedConfig } = explorerContext + const { explorerContextDispatcher, explorerContextState } = explorerContext const { transfers } = explorerContextState @@ -54,8 +54,14 @@ const ExplorerPage = (): JSX.Element => { }} >
- {transfers.length !== 0 && sharedConfig.length !== 0 ? ( - + {transfers.length !== 0 && explorerContextState.sharedConfig.length !== 0 ? ( + ) : explorerContextState.account !== undefined ? ( No transactions for the selected account! ) : ( diff --git a/src/types/explorer.ts b/src/types/explorer.ts index eb233cf..cd51431 100644 --- a/src/types/explorer.ts +++ b/src/types/explorer.ts @@ -146,6 +146,7 @@ export type ExplorerContextState = { background: string } account: string | undefined + sharedConfig: SharedConfigDomain[] | [] } export type Actions = @@ -164,11 +165,13 @@ export type Actions = | { type: "loading_done" } | { type: "loading_transfers" } | { type: "fetch_transfer_error"; payload: string } + | { type: "fetch_shared_config"; payload: SharedConfigDomain[] } export type Routes = { transfers: (page: string, limit: string, status?: string) => Promise transfer: (id: string) => Promise transferBySender: (sender: string, page: string, limit: string) => Promise + transferByTransactionHash: (txHash: string) => Promise } export type ExplorerContext = { @@ -179,8 +182,6 @@ export type ExplorerContext = { chainId: number | undefined account: string | undefined routes: Routes - sharedConfig: SharedConfigDomain[] | [] - setSharedConfig: React.Dispatch> explorerUrls: [] | ExplorerState["explorerUrls"] } diff --git a/yarn.lock b/yarn.lock index b526133..cc3eb15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -211,7 +211,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.1", "@babel/runtime@^7.23.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.1", "@babel/runtime@^7.23.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== @@ -3927,6 +3927,13 @@ hasown@^2.0.0, hasown@^2.0.1: dependencies: function-bind "^1.1.2" +history@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"