diff --git a/webapp/app/[locale]/tunnel/transaction-history/_components/reloadHistory.tsx b/webapp/app/[locale]/tunnel/transaction-history/_components/reloadHistory.tsx
new file mode 100644
index 00000000..81a7985f
--- /dev/null
+++ b/webapp/app/[locale]/tunnel/transaction-history/_components/reloadHistory.tsx
@@ -0,0 +1,29 @@
+import { useTunnelHistory } from 'hooks/useTunnelHistory'
+import { useTranslations } from 'next-intl'
+
+const ReloadIcon = () => (
+
+)
+
+export const ReloadHistory = function () {
+ const t = useTranslations()
+ const { resyncHistory } = useTunnelHistory()
+
+ return (
+
+ )
+}
diff --git a/webapp/app/[locale]/tunnel/transaction-history/_components/transactionHistory.tsx b/webapp/app/[locale]/tunnel/transaction-history/_components/transactionHistory.tsx
index 0b9aa868..a7d7f9a7 100644
--- a/webapp/app/[locale]/tunnel/transaction-history/_components/transactionHistory.tsx
+++ b/webapp/app/[locale]/tunnel/transaction-history/_components/transactionHistory.tsx
@@ -35,6 +35,7 @@ import { Amount } from './amount'
import { Chain as ChainComponent } from './chain'
import { DepositAction } from './depositAction'
import { Paginator } from './paginator'
+import { ReloadHistory } from './reloadHistory'
import { TxLink } from './txLink'
import { TxStatus } from './txStatus'
import { TxTime } from './txTime'
@@ -192,7 +193,7 @@ const columnsBuilder = (
) : (
),
- header: () => ,
+ header: () => ,
id: 'action',
},
]
diff --git a/webapp/context/tunnelHistoryContext/index.tsx b/webapp/context/tunnelHistoryContext/index.tsx
index 8d06e153..952fc86b 100644
--- a/webapp/context/tunnelHistoryContext/index.tsx
+++ b/webapp/context/tunnelHistoryContext/index.tsx
@@ -1,7 +1,6 @@
'use client'
import { featureFlags } from 'app/featureFlags'
-import { useBitcoin } from 'hooks/useBitcoin'
import { useHemi } from 'hooks/useHemi'
import { useNetworks } from 'hooks/useNetworks'
import { useSyncHistory } from 'hooks/useSyncHistory'
@@ -10,12 +9,11 @@ import {
type HistoryReducerState,
} from 'hooks/useSyncHistory/types'
import dynamic from 'next/dynamic'
-import { createContext, useMemo, ReactNode } from 'react'
+import { createContext, ReactNode } from 'react'
import { type RemoteChain } from 'types/chain'
import {
type DepositTunnelOperation,
type EvmWithdrawOperation,
- type WithdrawTunnelOperation,
} from 'types/tunnel'
import { useAccount } from 'wagmi'
@@ -58,6 +56,7 @@ type TunnelHistoryContext = {
withdrawal: Omit,
) => void
deposits: DepositTunnelOperation[]
+ resyncHistory: () => void
syncStatus: HistoryReducerState['status']
updateDeposit: (
deposit: DepositTunnelOperation,
@@ -74,6 +73,7 @@ export const TunnelHistoryContext = createContext({
addDepositToTunnelHistory: () => undefined,
addWithdrawalToTunnelHistory: () => undefined,
deposits: [],
+ resyncHistory: () => undefined,
syncStatus: 'idle',
updateDeposit: () => undefined,
updateWithdrawal: () => undefined,
@@ -86,12 +86,11 @@ type Props = {
export const TunnelHistoryProvider = function ({ children }: Props) {
const { address, isConnected } = useAccount()
- const bitcoin = useBitcoin()
const l2ChainId = useHemi().id
const { remoteNetworks } = useNetworks()
- const [history, dispatch] = useSyncHistory(l2ChainId)
+ const [history, dispatch, context] = useSyncHistory(l2ChainId)
const historyChainSync = []
@@ -114,41 +113,8 @@ export const TunnelHistoryProvider = function ({ children }: Props) {
)
}
- const value = useMemo(
- () => ({
- addDepositToTunnelHistory: (deposit: DepositTunnelOperation) =>
- dispatch({ payload: deposit, type: 'add-deposit' }),
- addWithdrawalToTunnelHistory: (withdrawal: WithdrawTunnelOperation) =>
- dispatch({ payload: withdrawal, type: 'add-withdraw' }),
- deposits: history.deposits
- .filter(d => featureFlags.btcTunnelEnabled || d.chainId !== bitcoin.id)
- .flatMap(d => d.content),
- syncStatus: history.status,
- updateDeposit: (
- deposit: DepositTunnelOperation,
- updates: Partial,
- ) =>
- dispatch({
- payload: { deposit, updates },
- type: 'update-deposit',
- }),
- updateWithdrawal: (
- withdraw: WithdrawTunnelOperation,
- updates: Partial,
- ) =>
- dispatch({
- payload: { updates, withdraw },
- type: 'update-withdraw',
- }),
- withdrawals: history.withdrawals
- .filter(w => featureFlags.btcTunnelEnabled || w.chainId !== bitcoin.id)
- .flatMap(w => w.content),
- }),
- [bitcoin.id, dispatch, history],
- )
-
return (
-
+
{/* Move to web worker https://github.com/hemilabs/ui-monorepo/issues/487 */}
{/* Track updates on bitcoin deposits, in bitcoin or in Hemi */}
{featureFlags.btcTunnelEnabled && }
diff --git a/webapp/hooks/useSyncHistory/index.ts b/webapp/hooks/useSyncHistory/index.ts
index d03d539f..11c9dcc6 100644
--- a/webapp/hooks/useSyncHistory/index.ts
+++ b/webapp/hooks/useSyncHistory/index.ts
@@ -1,8 +1,9 @@
-import { hemiSepolia } from 'hemi-viem'
+import { featureFlags } from 'app/featureFlags'
+import { useBitcoin } from 'hooks/useBitcoin'
import { useConnectedToSupportedEvmChain } from 'hooks/useConnectedToSupportedChain'
import { useNetworks } from 'hooks/useNetworks'
import debounce from 'lodash/debounce'
-import { useEffect, useReducer, useState } from 'react'
+import { useEffect, useMemo, useReducer, useState } from 'react'
import { type RemoteChain } from 'types/chain'
import {
type DepositTunnelOperation,
@@ -14,178 +15,23 @@ import { chainConfiguration } from 'utils/sync-history/chainConfiguration'
import { type Address, type Chain } from 'viem'
import { useAccount } from 'wagmi'
+import { historyReducer, initialState } from './reducer'
+import { type HistoryReducerState, type StorageChain } from './types'
import {
- type HistoryActions,
- type HistoryReducerState,
- type StorageChain,
-} from './types'
-import {
- addOperation,
- getSyncStatus,
- getTunnelHistoryDepositFallbackStorageKey,
getTunnelHistoryDepositStorageKey,
getTunnelHistoryWithdrawStorageKey,
- getTunnelHistoryWithdrawStorageKeyFallback,
- syncContent,
- updateChainSyncStatus,
- updateOperation,
} from './utils'
-// the _:never is used to fail compilation if a case is missing
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const compilationError = function (_: never): never {
- throw new Error('Missing implementation of action in reducer')
-}
-
-const initialState: HistoryReducerState = {
- deposits: [],
- status: 'idle',
- withdrawals: [],
-}
-
-const historyReducer = function (
- state: HistoryReducerState,
- action: HistoryActions,
-): HistoryReducerState {
- const getNewState = function (): Omit {
- const { type } = action
- switch (type) {
- case 'add-deposit': {
- const { payload: newDeposit } = action
- const deposits = addOperation(state.deposits, newDeposit)
-
- return {
- ...state,
- deposits,
- }
- }
- case 'add-withdraw': {
- const { payload: newWithdrawal } = action
- const withdrawals = addOperation(state.withdrawals, newWithdrawal)
- return {
- ...state,
- withdrawals,
- }
- }
- case 'reset':
- return { ...initialState }
- case 'restore': {
- const { payload } = action
- return {
- ...state,
- deposits: payload.deposits.map(chainDeposits => ({
- ...chainDeposits,
- // See https://github.com/hemilabs/ui-monorepo/issues/376
- content: chainDeposits.content.map(
- deposit =>
- ({
- ...deposit,
- l1ChainId: deposit.l1ChainId,
- l2ChainId: deposit.l2ChainId ?? hemiSepolia.id,
- }) as DepositTunnelOperation,
- ),
- status: 'ready',
- })),
- withdrawals: payload.withdrawals.map(chainWithdrawals => ({
- ...chainWithdrawals,
- // See https://github.com/hemilabs/ui-monorepo/issues/376
- content: chainWithdrawals.content.map(
- withdrawal =>
- ({
- ...withdrawal,
- l1ChainId: withdrawal.l1ChainId,
- l2ChainId: withdrawal.l2ChainId ?? hemiSepolia.id,
- }) as WithdrawTunnelOperation,
- ),
- status: 'ready',
- })),
- }
- }
- case 'sync': {
- const { chainId } = action.payload
- return updateChainSyncStatus(state, chainId, 'syncing')
- }
- case 'sync-deposits': {
- const { chainId } = action.payload
- const deposits = state.deposits.map(currentDeposits =>
- currentDeposits.chainId === chainId
- ? syncContent(currentDeposits, action.payload)
- : currentDeposits,
- )
-
- return {
- ...state,
- deposits,
- }
- }
- case 'sync-finished': {
- const { chainId } = action.payload
- return updateChainSyncStatus(state, chainId, 'finished')
- }
- case 'sync-withdrawals': {
- const { chainId } = action.payload
- const withdrawals = state.withdrawals.map(chainWithdrawals =>
- chainWithdrawals.chainId === chainId
- ? syncContent(chainWithdrawals, action.payload)
- : chainWithdrawals,
- )
- return {
- ...state,
- withdrawals,
- }
- }
- case 'update-deposit': {
- const { deposit, updates } = action.payload
- const deposits = updateOperation(state.deposits, {
- operation: deposit,
- updates,
- })
-
- return {
- ...state,
- deposits,
- }
- }
- case 'update-withdraw': {
- const { withdraw, updates } = action.payload
- const withdrawals = updateOperation(state.withdrawals, {
- operation: withdraw,
- updates,
- })
-
- return {
- ...state,
- withdrawals,
- }
- }
- default:
- // if a switch statement is missing on all possible actions
- // this will fail on compile time
- return compilationError(type)
- }
- }
-
- const newState = getNewState()
- return {
- ...newState,
- status: getSyncStatus(newState),
- }
-}
-
const readStateFromStorage = function ({
chainId,
configChainId,
- fallbackKey,
key,
}: {
chainId: RemoteChain['id']
configChainId: RemoteChain['id']
- fallbackKey?: string
key: string
}) {
- const restored =
- localStorage.getItem(key) ||
- (fallbackKey ? localStorage.getItem(fallbackKey) : null)
+ const restored = localStorage.getItem(key)
if (!restored) {
const chain = findChainById(chainId)
if (isEvmNetwork(chain)) {
@@ -213,6 +59,24 @@ const readStateFromStorage = function ({
}
}
+const clearHistoryInLocalStorage = ({
+ address,
+ l2ChainId,
+ remoteNetworks,
+}: {
+ address: Address
+ l2ChainId: Chain['id']
+ remoteNetworks: RemoteChain[]
+}) =>
+ remoteNetworks.forEach(function (remoteNetwork) {
+ localStorage.removeItem(
+ getTunnelHistoryWithdrawStorageKey(remoteNetwork.id, l2ChainId, address),
+ )
+ localStorage.removeItem(
+ getTunnelHistoryDepositStorageKey(remoteNetwork.id, l2ChainId, address),
+ )
+ })
+
const debounceTime = 500
const debouncedSaveToStorage = debounce(
({
@@ -265,9 +129,14 @@ const debouncedSaveToStorage = debounce(
export const useSyncHistory = function (l2ChainId: Chain['id']) {
const { address } = useAccount()
+ const bitcoin = useBitcoin()
const { remoteNetworks } = useNetworks()
+ // use this boolean to check if the past history was restored from local storage
const [loadedFromLocalStorage, setLoadedFromLocalStorage] = useState(false)
+ // use this boolean to force a resync of the history
+ const [forceResync, setForceResync] = useState(false)
+
const reducer = useReducer(historyReducer, initialState)
const supportedEvmChain = useConnectedToSupportedEvmChain()
@@ -302,10 +171,6 @@ export const useSyncHistory = function (l2ChainId: Chain['id']) {
readStateFromStorage({
chainId: id,
configChainId: id,
- fallbackKey: getTunnelHistoryDepositFallbackStorageKey(
- id,
- address,
- ),
key: getTunnelHistoryDepositStorageKey(id, l2ChainId, address),
}) as StorageChain,
)
@@ -317,10 +182,6 @@ export const useSyncHistory = function (l2ChainId: Chain['id']) {
readStateFromStorage({
chainId: id,
configChainId: l2ChainId,
- fallbackKey: getTunnelHistoryWithdrawStorageKeyFallback(
- l2ChainId,
- address,
- ),
key: getTunnelHistoryWithdrawStorageKey(id, l2ChainId, address),
}) as StorageChain,
)
@@ -348,7 +209,9 @@ export const useSyncHistory = function (l2ChainId: Chain['id']) {
if (
!supportedEvmChain ||
!loadedFromLocalStorage ||
- !['finished', 'syncing'].includes(history.status)
+ !['finished', 'syncing'].includes(history.status) ||
+ // if we started resync, do not save!
+ forceResync
) {
return
}
@@ -356,6 +219,7 @@ export const useSyncHistory = function (l2ChainId: Chain['id']) {
},
[
address,
+ forceResync,
history,
l2ChainId,
loadedFromLocalStorage,
@@ -364,5 +228,69 @@ export const useSyncHistory = function (l2ChainId: Chain['id']) {
],
)
- return reducer
+ // re-sync needs to take place in an effect, because we need the workers to be tear down
+ // (so they start again from the last block), and then, clear the local storage
+ // (as local storage is saved in a debounce process, we need to ensure no other effect
+ // saves its sync data, overriding what we have just cleared). Once all that is completed
+ // we just reset the state of this hook so it starts all over.
+ useEffect(
+ function clearAndResyncHistory() {
+ if (!forceResync) {
+ return
+ }
+ // clear local storage
+ clearHistoryInLocalStorage({ address, l2ChainId, remoteNetworks })
+ // reset the history in memory
+ dispatch({ type: 'reset' })
+ // update flag so data is reloaded again
+ setLoadedFromLocalStorage(false)
+ // mark resync as finished
+ setForceResync(false)
+ },
+ [
+ address,
+ dispatch,
+ forceResync,
+ l2ChainId,
+ remoteNetworks,
+ setForceResync,
+ setLoadedFromLocalStorage,
+ ],
+ )
+
+ const historyContext = useMemo(
+ () => ({
+ addDepositToTunnelHistory: (deposit: DepositTunnelOperation) =>
+ dispatch({ payload: deposit, type: 'add-deposit' }),
+ addWithdrawalToTunnelHistory: (withdrawal: WithdrawTunnelOperation) =>
+ dispatch({ payload: withdrawal, type: 'add-withdraw' }),
+ deposits: history.deposits
+ .filter(d => featureFlags.btcTunnelEnabled || d.chainId !== bitcoin.id)
+ .flatMap(d => d.content),
+ resyncHistory: () => setForceResync(true),
+ syncStatus: history.status,
+ updateDeposit: (
+ deposit: DepositTunnelOperation,
+ updates: Partial,
+ ) =>
+ dispatch({
+ payload: { deposit, updates },
+ type: 'update-deposit',
+ }),
+ updateWithdrawal: (
+ withdraw: WithdrawTunnelOperation,
+ updates: Partial,
+ ) =>
+ dispatch({
+ payload: { updates, withdraw },
+ type: 'update-withdraw',
+ }),
+ withdrawals: history.withdrawals
+ .filter(w => featureFlags.btcTunnelEnabled || w.chainId !== bitcoin.id)
+ .flatMap(w => w.content),
+ }),
+ [bitcoin.id, dispatch, history],
+ )
+
+ return [...reducer, historyContext] as const
}
diff --git a/webapp/hooks/useSyncHistory/reducer.ts b/webapp/hooks/useSyncHistory/reducer.ts
new file mode 100644
index 00000000..15a2fb23
--- /dev/null
+++ b/webapp/hooks/useSyncHistory/reducer.ts
@@ -0,0 +1,155 @@
+import { hemiSepolia } from 'hemi-viem'
+import {
+ type DepositTunnelOperation,
+ type WithdrawTunnelOperation,
+} from 'types/tunnel'
+
+import { type HistoryActions, type HistoryReducerState } from './types'
+import {
+ addOperation,
+ getSyncStatus,
+ syncContent,
+ updateChainSyncStatus,
+ updateOperation,
+} from './utils'
+
+// the _:never is used to fail compilation if a case is missing
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const compilationError = function (_: never): never {
+ throw new Error('Missing implementation of action in reducer')
+}
+
+export const initialState: HistoryReducerState = {
+ deposits: [],
+ status: 'idle',
+ withdrawals: [],
+}
+
+export const historyReducer = function (
+ state: HistoryReducerState,
+ action: HistoryActions,
+): HistoryReducerState {
+ const getNewState = function (): Omit {
+ const { type } = action
+ switch (type) {
+ case 'add-deposit': {
+ const { payload: newDeposit } = action
+ const deposits = addOperation(state.deposits, newDeposit)
+
+ return {
+ ...state,
+ deposits,
+ }
+ }
+ case 'add-withdraw': {
+ const { payload: newWithdrawal } = action
+ const withdrawals = addOperation(state.withdrawals, newWithdrawal)
+ return {
+ ...state,
+ withdrawals,
+ }
+ }
+ case 'reset':
+ return { ...initialState }
+ case 'restore': {
+ const { payload } = action
+ return {
+ ...state,
+ deposits: payload.deposits.map(chainDeposits => ({
+ ...chainDeposits,
+ // See https://github.com/hemilabs/ui-monorepo/issues/376
+ content: chainDeposits.content.map(
+ deposit =>
+ ({
+ ...deposit,
+ l1ChainId: deposit.l1ChainId,
+ l2ChainId: deposit.l2ChainId ?? hemiSepolia.id,
+ }) as DepositTunnelOperation,
+ ),
+ status: 'ready',
+ })),
+ withdrawals: payload.withdrawals.map(chainWithdrawals => ({
+ ...chainWithdrawals,
+ // See https://github.com/hemilabs/ui-monorepo/issues/376
+ content: chainWithdrawals.content.map(
+ withdrawal =>
+ ({
+ ...withdrawal,
+ l1ChainId: withdrawal.l1ChainId,
+ l2ChainId: withdrawal.l2ChainId ?? hemiSepolia.id,
+ }) as WithdrawTunnelOperation,
+ ),
+ status: 'ready',
+ })),
+ }
+ }
+ case 'sync': {
+ const { chainId } = action.payload
+ return updateChainSyncStatus(state, chainId, 'syncing')
+ }
+ case 'sync-deposits': {
+ const { chainId } = action.payload
+ const deposits = state.deposits.map(currentDeposits =>
+ currentDeposits.chainId === chainId
+ ? syncContent(currentDeposits, action.payload)
+ : currentDeposits,
+ )
+
+ return {
+ ...state,
+ deposits,
+ }
+ }
+ case 'sync-finished': {
+ const { chainId } = action.payload
+ return updateChainSyncStatus(state, chainId, 'finished')
+ }
+ case 'sync-withdrawals': {
+ const { chainId } = action.payload
+ const withdrawals = state.withdrawals.map(chainWithdrawals =>
+ chainWithdrawals.chainId === chainId
+ ? syncContent(chainWithdrawals, action.payload)
+ : chainWithdrawals,
+ )
+ return {
+ ...state,
+ withdrawals,
+ }
+ }
+ case 'update-deposit': {
+ const { deposit, updates } = action.payload
+ const deposits = updateOperation(state.deposits, {
+ operation: deposit,
+ updates,
+ })
+
+ return {
+ ...state,
+ deposits,
+ }
+ }
+ case 'update-withdraw': {
+ const { withdraw, updates } = action.payload
+ const withdrawals = updateOperation(state.withdrawals, {
+ operation: withdraw,
+ updates,
+ })
+
+ return {
+ ...state,
+ withdrawals,
+ }
+ }
+ default:
+ // if a switch statement is missing on all possible actions
+ // this will fail on compile time
+ return compilationError(type)
+ }
+ }
+
+ const newState = getNewState()
+ return {
+ ...newState,
+ status: getSyncStatus(newState),
+ }
+}
diff --git a/webapp/hooks/useSyncHistory/utils.ts b/webapp/hooks/useSyncHistory/utils.ts
index b05245fb..aa89201b 100644
--- a/webapp/hooks/useSyncHistory/utils.ts
+++ b/webapp/hooks/useSyncHistory/utils.ts
@@ -10,11 +10,6 @@ import {
type SyncType,
} from './types'
-export const getTunnelHistoryDepositFallbackStorageKey = (
- l1ChainId: RemoteChain['id'],
- address: Address,
-) => `portal.transaction-history-L1-${l1ChainId}-${address}-deposits`
-
export const getTunnelHistoryDepositStorageKey = (
l1ChainId: RemoteChain['id'],
l2ChainId: Chain['id'],
@@ -28,11 +23,6 @@ export const getTunnelHistoryWithdrawStorageKey = (
) =>
`portal.transaction-history-${l1ChainId}-${l2ChainId}-${address}-withdrawals`
-export const getTunnelHistoryWithdrawStorageKeyFallback = (
- l2ChainId: Chain['id'],
- address: Address,
-) => `portal.transaction-history-L2-${l2ChainId}-${address}-withdrawals`
-
const removeDuplicates = (merged: T[]) =>
Array.from(new Set(merged.map(({ transactionHash }) => transactionHash))).map(
transactionHash =>
diff --git a/webapp/messages/en.json b/webapp/messages/en.json
index c4dc6b2d..28b0ac95 100644
--- a/webapp/messages/en.json
+++ b/webapp/messages/en.json
@@ -68,6 +68,7 @@
"column-headers": {
"amount": "Amount",
"from": "From",
+ "reload": "Reload",
"status": "Status",
"time": "Time",
"to": "To",
diff --git a/webapp/messages/es.json b/webapp/messages/es.json
index 750192bd..77f2e22c 100644
--- a/webapp/messages/es.json
+++ b/webapp/messages/es.json
@@ -68,6 +68,7 @@
"column-headers": {
"amount": "Monto",
"from": "Desde",
+ "reload": "Recargar",
"status": "Estado",
"time": "Hora",
"to": "Hacia",