diff --git a/apps/bridge-dapp/src/components/Header/TxProgressDropdown/TxItem.tsx b/apps/bridge-dapp/src/components/Header/TxProgressDropdown/TxItem.tsx index 0e9e959310..f87014c12c 100644 --- a/apps/bridge-dapp/src/components/Header/TxProgressDropdown/TxItem.tsx +++ b/apps/bridge-dapp/src/components/Header/TxProgressDropdown/TxItem.tsx @@ -1,10 +1,10 @@ import type { Transaction } from '@webb-tools/abstract-api-provider/transaction'; -import type { WebbProviderType } from '@webb-tools/abstract-api-provider/webb-provider.interface'; +import type { WebbProviderType } from '@webb-tools/abstract-api-provider/types'; import { - getExplorerURI, getTxMessageFromStatus, transactionItemStatusFromTxStatus, } from '@webb-tools/api-provider-environment/transaction/useTransactionQueue'; +import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/utils'; import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; import TxProgressor from '@webb-tools/webb-ui-components/components/TxProgressor'; import type { TxInfo } from '@webb-tools/webb-ui-components/components/TxProgressor/types'; diff --git a/apps/bridge-dapp/src/components/Header/TxProgressDropdown/index.tsx b/apps/bridge-dapp/src/components/Header/TxProgressDropdown/index.tsx index 00de266b4a..5d5c982276 100644 --- a/apps/bridge-dapp/src/components/Header/TxProgressDropdown/index.tsx +++ b/apps/bridge-dapp/src/components/Header/TxProgressDropdown/index.tsx @@ -14,6 +14,7 @@ import type { LoadingPillStatus } from '@webb-tools/webb-ui-components/component import type { TransactionItemStatus } from '@webb-tools/webb-ui-components/containers/TransactionProgressCard'; import { useEffect, useMemo, useState } from 'react'; import TxItem from './TxItem'; +import useCurrentTx from '../../../hooks/useCurrentTx'; const TxProgressDropdown = () => { const { txQueue: txQueue_ } = useWebContext(); @@ -25,7 +26,7 @@ const TxProgressDropdown = () => { // Sort the latest tx to the top const sortedTxQueue = useSortedTxQueue(txQueue); - const currentTx = useCurrentTx(sortedTxQueue, currentTxId); + const currentTx = useCurrentTx(sortedTxQueue, currentTxId, { latest: true }); useEffect(() => { if (!currentTx) { @@ -48,7 +49,7 @@ const TxProgressDropdown = () => { } return ( - + @@ -76,20 +77,6 @@ const useSortedTxQueue = (txQueue: Array>) => { ); }; -const useCurrentTx = ( - txQueue: Array>, - txId: string | null -) => { - return useMemo(() => { - if (typeof txId === 'string') { - return txQueue.find((tx) => tx.id === txId); - } - - // Get the latest tx - return txQueue[0]; - }, [txId, txQueue]); -}; - const getPillStatus = (txStatus: TransactionItemStatus): LoadingPillStatus => txStatus === 'completed' ? 'success' diff --git a/apps/bridge-dapp/src/containers/DepositConfirmContainer/DepositConfirmContainer.tsx b/apps/bridge-dapp/src/containers/DepositConfirmContainer/DepositConfirmContainer.tsx index 07462ade06..8309050d96 100644 --- a/apps/bridge-dapp/src/containers/DepositConfirmContainer/DepositConfirmContainer.tsx +++ b/apps/bridge-dapp/src/containers/DepositConfirmContainer/DepositConfirmContainer.tsx @@ -12,7 +12,11 @@ import { isViemError } from '@webb-tools/web3-api-provider'; import { DepositConfirm } from '@webb-tools/webb-ui-components'; import { forwardRef, useCallback, useMemo, useState } from 'react'; import { ContractFunctionRevertedError, formatUnits } from 'viem'; -import { useEnqueueSubmittedTx, useLatestTransactionStage } from '../../hooks'; +import { + useCurrentTx, + useEnqueueSubmittedTx, + useLatestTransactionStage, +} from '../../hooks'; import { captureSentryException, getCardTitle, @@ -52,7 +56,6 @@ const DepositConfirmContainer = forwardRef< const [totalStep, setTotalStep] = useState(); const stage = useLatestTransactionStage(txId); - const depositTxInProgress = useMemo( () => stage !== TransactionState.Ideal, [stage] @@ -61,6 +64,8 @@ const DepositConfirmContainer = forwardRef< const { activeApi, activeAccount, activeChain, apiConfig, txQueue } = useWebContext(); + const currentTx = useCurrentTx(txQueue.txQueue, txId); + const enqueueSubmittedTx = useEnqueueSubmittedTx(); const { api: txQueueApi, txPayloads } = txQueue; @@ -239,12 +244,16 @@ const DepositConfirmContainer = forwardRef< ); const cardTitle = useMemo(() => { - return getCardTitle(stage, wrappingFlow).trim(); - }, [stage, wrappingFlow]); + if (!currentTx) { + return undefined; + } - const [txStatusMessage, currentStep] = useMemo(() => { + return getCardTitle(stage, currentTx.name, wrappingFlow).trim(); + }, [currentTx, stage, wrappingFlow]); + + const [txStatusMessage, currentStep, txStatus] = useMemo(() => { if (!txId) { - return ['', undefined]; + return ['', undefined, undefined]; } const txPayload = txPayloads.find((txPayload) => txPayload.id === txId); @@ -253,7 +262,9 @@ const DepositConfirmContainer = forwardRef< : ''; const step = txPayload?.currentStep; - return [message, step]; + const status = txPayload?.txStatus.status; + + return [message, step, status]; }, [txId, txPayloads]); return ( @@ -285,6 +296,13 @@ const DepositConfirmContainer = forwardRef< sourceChain={sourceChain} destChain={destChain} wrappableTokenSymbol={wrappableToken?.view.symbol} + txStatusColor={ + txStatus === 'completed' + ? 'green' + : txStatus === 'warning' + ? 'red' + : undefined + } txStatusMessage={txStatusMessage} onClose={onClose} /> diff --git a/apps/bridge-dapp/src/containers/Layout/Layout.tsx b/apps/bridge-dapp/src/containers/Layout/Layout.tsx index 50cf2188e9..e79586569a 100644 --- a/apps/bridge-dapp/src/containers/Layout/Layout.tsx +++ b/apps/bridge-dapp/src/containers/Layout/Layout.tsx @@ -11,6 +11,8 @@ import { Header } from '../../components/Header'; import { WEBB_DAPP_NEW_ISSUE_URL } from '../../constants'; import sidebarProps from '../../constants/sidebar'; +const heightClsx = cx('min-h-screen h-full'); + export const Layout: FC<{ children?: React.ReactNode }> = ({ children }) => { const [showBanner, setShowBanner] = useState(true); @@ -19,11 +21,17 @@ export const Layout: FC<{ children?: React.ReactNode }> = ({ children }) => { }; return ( -
-
+
+
-
+
diff --git a/apps/bridge-dapp/src/containers/TransferConfirmContainer/TransferConfirmContainer.tsx b/apps/bridge-dapp/src/containers/TransferConfirmContainer/TransferConfirmContainer.tsx index dede0e2115..491d091399 100644 --- a/apps/bridge-dapp/src/containers/TransferConfirmContainer/TransferConfirmContainer.tsx +++ b/apps/bridge-dapp/src/containers/TransferConfirmContainer/TransferConfirmContainer.tsx @@ -18,9 +18,14 @@ import { import { forwardRef, useCallback, useMemo, useState } from 'react'; import type { Hash } from 'viem'; import { ContractFunctionRevertedError, formatEther } from 'viem'; -import { useEnqueueSubmittedTx, useLatestTransactionStage } from '../../hooks'; +import { + useCurrentTx, + useEnqueueSubmittedTx, + useLatestTransactionStage, +} from '../../hooks'; import { captureSentryException, + getCardTitle, getErrorMessage, getTokenURI, getTransactionHash, @@ -94,7 +99,7 @@ const TransferConfirmContainer = forwardRef< : undefined, }); - const isTransfering = useMemo( + const isTransferring = useMemo( () => stage !== TransactionState.Ideal, [stage] ); @@ -122,7 +127,7 @@ const TransferConfirmContainer = forwardRef< return; } - if (isTransfering) { + if (isTransferring) { txQueueApi.startNewTransaction(); onResetState?.(); return; @@ -248,7 +253,7 @@ const TransferConfirmContainer = forwardRef< await removeNoteFromNoteManager(note); } } catch (error) { - console.error('Error occured while transfering', error); + console.error('Error occured while transferring', error); changeNote && (await removeNoteFromNoteManager(changeNote)); tx.txHash = getTransactionHash(error); @@ -271,12 +276,22 @@ const TransferConfirmContainer = forwardRef< } }, // prettier-ignore - [activeApi, activeRelayer, addNoteToNoteManager, amount, apiConfig, changeNote, changeUtxo, currency.id, enqueueSubmittedTx, feeAmount, inputNotes, isTransfering, noteManager, onResetState, recipient, refundAmount, refundRecipient, removeNoteFromNoteManager, transferUtxo, txQueueApi, vAnchorApi] + [activeApi, activeRelayer, addNoteToNoteManager, amount, apiConfig, changeNote, changeUtxo, currency.id, enqueueSubmittedTx, feeAmount, inputNotes, isTransferring, noteManager, onResetState, recipient, refundAmount, refundRecipient, removeNoteFromNoteManager, transferUtxo, txQueueApi, vAnchorApi] ); - const [txStatusMessage, currentStep] = useMemo(() => { + const currentTx = useCurrentTx(txQueue.txQueue, txId); + + const cardTitle = useMemo(() => { + if (!currentTx) { + return; + } + + return getCardTitle(stage, currentTx.name); + }, [currentTx, stage]); + + const [txStatusMessage, currentStep, txStatus] = useMemo(() => { if (!txId) { - return ['', undefined]; + return ['', undefined, undefined]; } const txPayload = txPayloads.find((txPayload) => txPayload.id === txId); @@ -284,7 +299,9 @@ const TransferConfirmContainer = forwardRef< ? txPayload.txStatus.message?.replace('...', '') : ''; - return [message, txPayload?.currentStep]; + const txStatus = txPayload?.txStatus.status; + + return [message, txPayload?.currentStep, txStatus]; }, [txId, txPayloads]); const formattedFee = useMemo(() => { @@ -304,7 +321,7 @@ const TransferConfirmContainer = forwardRef< {...props} className="min-h-[var(--card-height)]" ref={ref} - title={isTransfering ? 'Transfer in Progress...' : undefined} + title={cardTitle} totalProgress={totalStep} progress={currentStep} amount={amount} @@ -339,8 +356,15 @@ const TransferConfirmContainer = forwardRef< actionBtnProps={{ isDisabled: changeNote ? !isChecked : false, onClick: handleTransferExecute, - children: isTransfering ? 'Make Another Transaction' : 'Transfer', + children: isTransferring ? 'Make Another Transaction' : 'Transfer', }} + txStatusColor={ + txStatus === 'completed' + ? 'green' + : txStatus === 'warning' + ? 'red' + : undefined + } txStatusMessage={txStatusMessage} refundAmount={ typeof refundAmount === 'bigint' diff --git a/apps/bridge-dapp/src/containers/WithdrawConfirmContainer/WithdrawConfirmContainer.tsx b/apps/bridge-dapp/src/containers/WithdrawConfirmContainer/WithdrawConfirmContainer.tsx index 6203107bc8..f728c30654 100644 --- a/apps/bridge-dapp/src/containers/WithdrawConfirmContainer/WithdrawConfirmContainer.tsx +++ b/apps/bridge-dapp/src/containers/WithdrawConfirmContainer/WithdrawConfirmContainer.tsx @@ -21,9 +21,14 @@ import { formatEther, formatUnits, } from 'viem'; -import { useEnqueueSubmittedTx, useLatestTransactionStage } from '../../hooks'; +import { + useCurrentTx, + useEnqueueSubmittedTx, + useLatestTransactionStage, +} from '../../hooks'; import { captureSentryException, + getCardTitle, getErrorMessage, getTokenURI, getTransactionHash, @@ -101,39 +106,15 @@ const WithdrawConfirmContainer = forwardRef< : 'substrate'; }, [targetTypedChainId]); - const cardTitle = useMemo(() => { - let status = ''; - - switch (stage) { - case TransactionState.Ideal: { - break; - } + const currentTx = useCurrentTx(txQueue.txQueue, txId); - case TransactionState.Done: { - status = 'Completed'; - break; - } - - case TransactionState.Failed: { - status = 'Failed'; - break; - } - - default: { - status = 'in Progress...'; - break; - } + const cardTitle = useMemo(() => { + if (!currentTx) { + return; } - if (!status) - return unwrapCurrency - ? 'Confirm Unwrap and Withdraw' - : 'Confirm Withdraw'; - - return unwrapCurrency - ? `Unwrap and Withdraw ${status}` - : `Withdraw ${status}`; - }, [stage, unwrapCurrency]); + return getCardTitle(stage, currentTx.name, Boolean(unwrapCurrency)); + }, [currentTx, stage, unwrapCurrency]); // The main action onClick handler const handleExecuteWithdraw = useCallback( @@ -309,16 +290,18 @@ const WithdrawConfirmContainer = forwardRef< [activeApi, activeRelayer, addNoteToNoteManager, amountAfterFee, apiConfig, availableNotes, changeNote, changeUtxo, enqueueSubmittedTx, fee, onResetState, recipient, refundAmount, removeNoteFromNoteManager, txQueueApi, unwrapCurrency, vAnchorApi, withdrawTxInProgress] ); - const [txStatusMessage, currentStep] = useMemo(() => { + const [txStatusMessage, currentStep, txStatus] = useMemo(() => { if (!txId) { - return ['', undefined]; + return ['', undefined, undefined]; } const txPayload = txPayloads.find((txPayload) => txPayload.id === txId); const message = txPayload ? txPayload.txStatus.message?.replace('...', '') : ''; - return [message, txPayload?.currentStep]; + + const txStatus = txPayload?.txStatus.status; + return [message, txPayload?.currentStep, txStatus]; }, [txId, txPayloads]); const formattedFee = useMemo(() => { @@ -406,6 +389,13 @@ const WithdrawConfirmContainer = forwardRef< relayerAvatarTheme={avatarTheme} fungibleTokenSymbol={fungibleCurrency.view.symbol} wrappableTokenSymbol={unwrapCurrency?.view.symbol} + txStatusColor={ + txStatus === 'completed' + ? 'green' + : txStatus === 'warning' + ? 'red' + : undefined + } txStatusMessage={txStatusMessage} onClose={onClose} /> diff --git a/apps/bridge-dapp/src/hooks/index.ts b/apps/bridge-dapp/src/hooks/index.ts index 10a3124128..2bc4123de1 100644 --- a/apps/bridge-dapp/src/hooks/index.ts +++ b/apps/bridge-dapp/src/hooks/index.ts @@ -1,14 +1,19 @@ export * from './useAddCurrency'; export { default as useAmountWithRoute } from './useAmountWithRoute'; export { default as useChainsFromNote } from './useChainsFromNote'; +export { default as useChainsFromRoute } from './useChainsFromRoute'; export * from './useConnectWallet'; export { default as useCurrenciesFromRoute } from './useCurrenciesFromRoute'; +export { default as useTxTabFromRoute } from './useTxTabFromRoute'; +export { default as useDefaultChainAndPool } from './useDefaultChainAndPool'; export { default as useEnqueueSubmittedTx } from './useEnqueueSubmittedTx'; export * from './useLatestTransactionStage'; export * from './useMaxFeeInfo'; export { default as useNavigateWithPersistParams } from './useNavigateWithPersistParams'; export * from './useRelayerManager'; +export { default as useRelayerWithRoute } from './useRelayerWithRoute'; export * from './useShieldedAssets'; export * from './useSpendNotes'; export { default as useStateWithRoute } from './useStateWithRoute'; +export { default as useCurrentTx } from './useCurrentTx'; export * from './useTryAnotherWalletWithView'; diff --git a/apps/bridge-dapp/src/hooks/useAmountWithRoute.ts b/apps/bridge-dapp/src/hooks/useAmountWithRoute.ts index 3e58496c12..53ab8a5aa9 100644 --- a/apps/bridge-dapp/src/hooks/useAmountWithRoute.ts +++ b/apps/bridge-dapp/src/hooks/useAmountWithRoute.ts @@ -2,18 +2,33 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { formatEther, parseEther } from 'viem'; import { AMOUNT_KEY } from '../constants'; +import { StringParam, useQueryParam } from 'use-query-params'; const useAmountWithRoute = (key = AMOUNT_KEY) => { - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const amountStr = useMemo(() => { const amountStr = searchParams.get(key) ?? ''; + if (amountStr.length === 0) { + return ''; + } - return amountStr.length ? formatEther(BigInt(amountStr)) : ''; + try { + return formatEther(BigInt(amountStr)); + } catch (error) { + console.error(error); + return ''; + } }, [key, searchParams]); const [amount, setAmount] = useState(amountStr); + useEffect(() => { + setAmount(amountStr); + }, [amountStr]); + + const [, setAmountParam] = useQueryParam(AMOUNT_KEY, StringParam); + const onAmountChange = useCallback( (amount: string) => { const validationRegex = /^\d*\.?\d*$/; @@ -29,18 +44,15 @@ const useAmountWithRoute = (key = AMOUNT_KEY) => { useEffect(() => { function updateParams() { if (!amount) { - return setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.delete(key); - return nextParams; - }); + setAmountParam(undefined); + return; } - setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.set(key, `${parseEther(amount)}`); - return nextParams; - }); + try { + setAmountParam(parseEther(amount).toString()); + } catch (error) { + console.error(error); + } } const timeout = setTimeout(updateParams, 500); @@ -48,11 +60,7 @@ const useAmountWithRoute = (key = AMOUNT_KEY) => { return () => { clearTimeout(timeout); }; - }, [amount, key, setSearchParams]); - - useEffect(() => { - setAmount(amountStr); - }, [amountStr]); + }, [amount, key, setAmountParam]); return [amount, onAmountChange] as const; }; diff --git a/apps/bridge-dapp/src/hooks/useChainsFromRoute.ts b/apps/bridge-dapp/src/hooks/useChainsFromRoute.ts new file mode 100644 index 0000000000..6884cf2cfa --- /dev/null +++ b/apps/bridge-dapp/src/hooks/useChainsFromRoute.ts @@ -0,0 +1,48 @@ +import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; +import { useMemo } from 'react'; +import { NumberParam, useQueryParams } from 'use-query-params'; +import { DEST_CHAIN_KEY, SOURCE_CHAIN_KEY } from '../constants'; + +/** + * Get the source chain and destination chain info from search params + * @returns an object containing: + * - srcChainCfg: the source chain config + * - srcTypedChainId: the source chain typed chain id + * - destChainCfg: the destination chain config + * - destTypedChainId: the destination chain typed chain id + */ +function useChainsFromRoute() { + const { apiConfig } = useWebContext(); + + const [ + { [SOURCE_CHAIN_KEY]: srcTypedChainId, [DEST_CHAIN_KEY]: destTypedChainId }, + ] = useQueryParams({ + [SOURCE_CHAIN_KEY]: NumberParam, + [DEST_CHAIN_KEY]: NumberParam, + }); + + const srcChainCfg = useMemo(() => { + if (typeof srcTypedChainId !== 'number') { + return; + } + + return apiConfig.chains[srcTypedChainId]; + }, [apiConfig.chains, srcTypedChainId]); + + const destChainCfg = useMemo(() => { + if (typeof destTypedChainId !== 'number') { + return; + } + + return apiConfig.chains[destTypedChainId]; + }, [apiConfig.chains, destTypedChainId]); + + return { + destChainCfg, + destTypedChainId, + srcChainCfg, + srcTypedChainId, + }; +} + +export default useChainsFromRoute; diff --git a/apps/bridge-dapp/src/hooks/useCurrenciesFromRoute.ts b/apps/bridge-dapp/src/hooks/useCurrenciesFromRoute.ts index f2bf59cf57..79ad89ada9 100644 --- a/apps/bridge-dapp/src/hooks/useCurrenciesFromRoute.ts +++ b/apps/bridge-dapp/src/hooks/useCurrenciesFromRoute.ts @@ -5,6 +5,8 @@ import { CurrencyRole } from '@webb-tools/dapp-types/Currency'; import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { POOL_KEY, SOURCE_CHAIN_KEY, TOKEN_KEY } from '../constants'; +import { getParam } from '../utils'; +import { NumberParam } from 'use-query-params'; function useCurrenciesFromRoute(typedChainId?: number) { const { @@ -14,34 +16,25 @@ function useCurrenciesFromRoute(typedChainId?: number) { const [searhParams] = useSearchParams(); const srcTypedChainId = useMemo(() => { - const sourceStr = searhParams.get(SOURCE_CHAIN_KEY); - if (!sourceStr) { - return undefined; - } - - if (Number.isNaN(parseInt(sourceStr))) { - return undefined; - } - - return parseInt(sourceStr); + return getParam(searhParams, SOURCE_CHAIN_KEY, NumberParam); }, [searhParams]); const fungibleCfg = useMemo(() => { - const fungibleId = searhParams.get(POOL_KEY); - if (!fungibleId) { + const fungibleId = getParam(searhParams, POOL_KEY, NumberParam); + if (typeof fungibleId !== 'number') { return undefined; } - return currencies[parseInt(fungibleId)]; + return currencies[fungibleId]; }, [currencies, searhParams]); const wrappableCfg = useMemo(() => { - const tokenId = searhParams.get(TOKEN_KEY); - if (!tokenId) { + const tokenId = getParam(searhParams, TOKEN_KEY, NumberParam); + if (typeof tokenId !== 'number') { return undefined; } - return currencies[parseInt(tokenId)]; + return currencies[tokenId]; }, [currencies, searhParams]); const fungibleCurrencies = useMemo(() => { @@ -49,44 +42,42 @@ function useCurrenciesFromRoute(typedChainId?: number) { (currencyCfg) => currencyCfg.role === CurrencyRole.Governable ); - if (typeof typedChainId === 'number') { - return currencyCfgs.filter((currencyCfg) => - Array.from(currencyCfg.addresses.keys()).includes(typedChainId) - ); - } - - if (typeof srcTypedChainId !== 'number') { + const typedChainIdToUse = typedChainId ?? srcTypedChainId; + if (typeof typedChainIdToUse !== 'number') { return currencyCfgs; } return currencyCfgs.filter((currencyCfg) => - Array.from(currencyCfg.addresses.keys()).includes(srcTypedChainId) + Array.from(currencyCfg.addresses.keys()).includes(typedChainIdToUse) ); }, [currencies, srcTypedChainId, typedChainId]); - const wrappableCurrencies = useMemo>(() => { - if (!fungibleCfg) { - return []; - } - - const wrappableMap = fungibleToWrappableMap.get(fungibleCfg.id); - if (!wrappableMap) { - return []; - } - - const wrappableSet = - typeof typedChainId === 'number' - ? wrappableMap.get(typedChainId) - : typeof srcTypedChainId === 'number' - ? wrappableMap.get(srcTypedChainId) - : undefined; - - if (!wrappableSet) { - return []; - } - - return Array.from(wrappableSet.values()).map((id) => currencies[id]); - }, [currencies, fungibleCfg, fungibleToWrappableMap, srcTypedChainId, typedChainId]); // prettier-ignore + const wrappableCurrencies = useMemo>( + () => { + if (!fungibleCfg) { + return []; + } + + const wrappableMap = fungibleToWrappableMap.get(fungibleCfg.id); + if (!wrappableMap) { + return []; + } + + const typedChainIdToUse = typedChainId ?? srcTypedChainId; + if (typeof typedChainIdToUse !== 'number') { + return []; + } + + const wrappableSet = wrappableMap.get(typedChainIdToUse); + if (!wrappableSet) { + return []; + } + + return Array.from(wrappableSet.values()).map((id) => currencies[id]); + }, + // prettier-ignore + [currencies, fungibleCfg, fungibleToWrappableMap, srcTypedChainId, typedChainId] + ); const allCurrencyCfgs = useMemo(() => { return [...fungibleCurrencies, ...wrappableCurrencies]; diff --git a/apps/bridge-dapp/src/hooks/useCurrentTx.ts b/apps/bridge-dapp/src/hooks/useCurrentTx.ts new file mode 100644 index 0000000000..6c5adf888b --- /dev/null +++ b/apps/bridge-dapp/src/hooks/useCurrentTx.ts @@ -0,0 +1,32 @@ +import type { Transaction } from '@webb-tools/abstract-api-provider/transaction'; +import { useMemo } from 'react'; + +/** + * Get the current transaction from the transaction queue + * @param txQueue the transaction queue to search + * @param txId the optional transaction id to search for + * @returns the transaction if found, otherwise the latest transaction + */ +const useCurrentTx = ( + txQueue: Array>, + txId?: string | null, + opts?: { + /** + * If true, return the latest tx if the txId is not found + */ + latest?: boolean; + } +) => { + return useMemo(() => { + if (typeof txId === 'string') { + return txQueue.find((tx) => tx.id === txId); + } + + // Get the latest tx + if (opts?.latest) { + return txQueue[0]; + } + }, [opts?.latest, txId, txQueue]); +}; + +export default useCurrentTx; diff --git a/apps/bridge-dapp/src/hooks/useDefaultChainAndPool.ts b/apps/bridge-dapp/src/hooks/useDefaultChainAndPool.ts new file mode 100644 index 0000000000..2068a8f1e1 --- /dev/null +++ b/apps/bridge-dapp/src/hooks/useDefaultChainAndPool.ts @@ -0,0 +1,94 @@ +import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; +import { calculateTypedChainId } from '@webb-tools/sdk-core/typed-chain-id'; +import { useEffect, useMemo } from 'react'; +import { NumberParam, useQueryParams } from 'use-query-params'; +import { POOL_KEY, SOURCE_CHAIN_KEY } from '../constants'; +import objectToSearchString from '../utils/objectToSearchString'; + +/** + * Hook containing side effects to set default source chain and pool id + */ +const useDefaultChainAndPool = () => { + const { loading, isConnecting, apiConfig, activeChain, activeApi } = + useWebContext(); + + const [query, setQuery] = useQueryParams( + { + [SOURCE_CHAIN_KEY]: NumberParam, + [POOL_KEY]: NumberParam, + }, + { objectToSearchString } + ); + + const { [SOURCE_CHAIN_KEY]: srcTypedChainId } = query; + + const hasDefaultChain = useMemo(() => { + // If the app is loading or connecting, no need to check + if (loading || isConnecting) { + return true; + } + + if (typeof srcTypedChainId === 'number') { + return true; + } + + return false; + }, [loading, isConnecting, srcTypedChainId]); + + // Side effect to set default source chain + useEffect(() => { + if (hasDefaultChain) { + return; + } + + const defaultChain = Object.values(apiConfig.chains)[0]; + const typedChainId = activeChain + ? calculateTypedChainId(activeChain.chainType, activeChain.id) + : calculateTypedChainId(defaultChain.chainType, defaultChain.id); + + setQuery({ [SOURCE_CHAIN_KEY]: typedChainId }); + }, [activeChain, apiConfig.chains, hasDefaultChain, setQuery]); + + const activeBridge = useMemo(() => { + return activeApi?.state.activeBridge; + }, [activeApi]); + + // Find default pool id when source chain is changed + const defaultPoolId = useMemo(() => { + if (typeof srcTypedChainId !== 'number') { + return; + } + + const activeBridgeSupported = + activeBridge && + Object.keys(activeBridge.targets).includes(`${srcTypedChainId}`); + + if (activeBridgeSupported) { + return activeBridge.currency.id; + } + + const anchor = Object.entries(apiConfig.anchors).find( + ([, anchorsRecord]) => { + return Object.keys(anchorsRecord).includes(`${srcTypedChainId}`); + } + ); + + const pool = anchor?.[0]; + if (typeof pool !== 'string') { + return; + } + + return Number(pool); + }, [activeBridge, apiConfig.anchors, srcTypedChainId]); + + // Side effect to set default pool id + useEffect(() => { + if (typeof defaultPoolId !== 'number') { + return; + } + + setQuery({ [POOL_KEY]: defaultPoolId }); + }, [defaultPoolId, setQuery]); +}; + +export default useDefaultChainAndPool; diff --git a/apps/bridge-dapp/src/hooks/useEnqueueSubmittedTx.tsx b/apps/bridge-dapp/src/hooks/useEnqueueSubmittedTx.tsx index 4498c4624f..e5f680878c 100644 --- a/apps/bridge-dapp/src/hooks/useEnqueueSubmittedTx.tsx +++ b/apps/bridge-dapp/src/hooks/useEnqueueSubmittedTx.tsx @@ -1,5 +1,5 @@ import { useModalQueueManager } from '@webb-tools/api-provider-environment/modal-queue-manager'; -import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/useTransactionQueue'; +import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/utils'; import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; import { type ChainConfig } from '@webb-tools/dapp-config/chains/chain-config.interface'; import { useCallback } from 'react'; diff --git a/apps/bridge-dapp/src/hooks/useNavigateWithPersistParams.ts b/apps/bridge-dapp/src/hooks/useNavigateWithPersistParams.ts index b65618dc13..310a067771 100644 --- a/apps/bridge-dapp/src/hooks/useNavigateWithPersistParams.ts +++ b/apps/bridge-dapp/src/hooks/useNavigateWithPersistParams.ts @@ -1,16 +1,30 @@ import { useCallback } from 'react'; -import { NavigateOptions, To, useNavigate } from 'react-router'; +import { NavigateOptions, To, useLocation, useNavigate } from 'react-router'; import { useSearchParams } from 'react-router-dom'; import merge from 'lodash/merge'; +/** + * Custom the `useNaviagte` hook from `react-router` to persist the search params + * @returns a navigate function that will persist the search params + */ const useNavigateWithPersistParams = (): ReturnType => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const { pathname } = useLocation(); return useCallback( (toOrDelta: To | number, options?: NavigateOptions) => { if (typeof toOrDelta === 'number') { - navigate(toOrDelta); + const path = pathname.split('/').slice(0, -1).join('/'); + const args = + toOrDelta !== -1 + ? toOrDelta + : ({ + search: searchParams.toString(), + pathname: path, + } satisfies To); + + typeof args === 'number' ? navigate(args) : navigate(args, options); } else if (typeof toOrDelta === 'string') { navigate( { search: searchParams.toString(), pathname: toOrDelta }, @@ -23,7 +37,7 @@ const useNavigateWithPersistParams = (): ReturnType => { ); } }, - [navigate, searchParams] + [navigate, pathname, searchParams] ); }; diff --git a/apps/bridge-dapp/src/hooks/useRelayerWithRoute.ts b/apps/bridge-dapp/src/hooks/useRelayerWithRoute.ts new file mode 100644 index 0000000000..f037538e5a --- /dev/null +++ b/apps/bridge-dapp/src/hooks/useRelayerWithRoute.ts @@ -0,0 +1,121 @@ +import { type OptionalActiveRelayer } from '@webb-tools/abstract-api-provider/relayer/types'; +import { WebbRelayer } from '@webb-tools/abstract-api-provider/relayer/webb-relayer'; +import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; +import { useEffect, useMemo } from 'react'; +import { + BooleanParam, + NumberParam, + StringParam, + useQueryParams, +} from 'use-query-params'; +import { + HAS_REFUND_KEY, + NO_RELAYER_KEY, + POOL_KEY, + REFUND_RECIPIENT_KEY, + RELAYER_ENDPOINT_KEY, +} from '../constants'; + +const useRelayerWithRoute = (typedChainId?: number | null) => { + const { activeApi, apiConfig } = useWebContext(); + + // State for active relayer + const [query, setQuery] = useQueryParams({ + [RELAYER_ENDPOINT_KEY]: StringParam, + [POOL_KEY]: NumberParam, + [NO_RELAYER_KEY]: BooleanParam, + [HAS_REFUND_KEY]: BooleanParam, + [REFUND_RECIPIENT_KEY]: StringParam, + }); + + const { + [RELAYER_ENDPOINT_KEY]: relayerUrl, + [NO_RELAYER_KEY]: noRelayer, + [POOL_KEY]: poolId, + } = query; + + // Side effect for active relayer subsription + useEffect(() => { + const sub = activeApi?.relayerManager.activeRelayerWatcher.subscribe( + (relayer) => { + setQuery({ + [RELAYER_ENDPOINT_KEY]: relayer?.endpoint, + ...(relayer == null + ? { + [HAS_REFUND_KEY]: undefined, + [REFUND_RECIPIENT_KEY]: undefined, + } + : {}), + }); + } + ); + + return () => sub?.unsubscribe(); + }, [activeApi?.relayerManager.activeRelayerWatcher, setQuery]); + + const anchorId = useMemo(() => { + if (typeof poolId !== 'number' || typeof typedChainId !== 'number') { + return; + } + + return apiConfig.anchors[poolId]?.[typedChainId]; + }, [apiConfig.anchors, poolId, typedChainId]); + + // Side effect to check if active relayer is supported + // If not, set the first supported relayer as active + useEffect(() => { + if (typeof anchorId !== 'string' || typeof typedChainId !== 'number') { + return; + } + + if (noRelayer || !activeApi?.relayerManager) { + return; + } + + const manager = activeApi.relayerManager; + const supportedRelayers = manager.getRelayersByChainAndAddress( + typedChainId, + anchorId + ); + + if (!supportedRelayers || supportedRelayers.length === 0) { + manager.setActiveRelayer(null, typedChainId); + return; + } + + const active = manager.activeRelayer; + const isActiveRelayerSupported = supportedRelayers.find( + (r) => r.endpoint === active?.endpoint + ); + if (isActiveRelayerSupported) { + return; + } + + manager.setActiveRelayer(supportedRelayers[0], typedChainId); + }, [activeApi?.relayerManager, anchorId, noRelayer, typedChainId]); + + const activeRelayer = useMemo(() => { + if (!relayerUrl || !activeApi?.relayerManager) { + return null; + } + + if (typeof typedChainId !== 'number') { + return null; + } + + const relayers = activeApi.relayerManager.getRelayers({}); + const relayer = relayers.find((r) => r.endpoint === relayerUrl); + if (!relayer) { + return null; + } + + return WebbRelayer.intoActiveWebRelayer(relayer, { + typedChainId, + basedOn: activeApi.relayerManager.cmdKey, + }); + }, [relayerUrl, activeApi?.relayerManager, typedChainId]); + + return activeRelayer; +}; + +export default useRelayerWithRoute; diff --git a/apps/bridge-dapp/src/hooks/useStateWithRoute.ts b/apps/bridge-dapp/src/hooks/useStateWithRoute.ts index 596e3c0208..eb4faa4bc1 100644 --- a/apps/bridge-dapp/src/hooks/useStateWithRoute.ts +++ b/apps/bridge-dapp/src/hooks/useStateWithRoute.ts @@ -1,46 +1,19 @@ -import { useEffect, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; - -const useStateWithRoute = (key: string) => { - const [searchParams, setSearchParams] = useSearchParams(); - - const initial = useMemo(() => { - return searchParams.get(key) ?? ''; - }, [key, searchParams]); - - const [state, setState] = useState(initial); - - // Update amount on search params with debounce - useEffect(() => { - function updateParams() { - if (!state) { - setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.delete(key); - return nextParams; - }); - return; - } - - setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.set(key, state); - return nextParams; - }); +import { useQueryParam, type QueryParamConfig } from 'use-query-params'; +import objectToSearchString from '../utils/objectToSearchString'; + +const QueryParamConfig = { + encode: (value) => value, + decode: (value) => { + if (value == null) { + return ''; } - const timeout = setTimeout(updateParams, 500); - - return () => { - clearTimeout(timeout); - }; - }, [key, setSearchParams, state]); + return String(value); + }, +} satisfies QueryParamConfig; - useEffect(() => { - setState(initial); - }, [initial]); - - return [state, setState] as const; +const useStateWithRoute = (key: string) => { + return useQueryParam(key, QueryParamConfig, { objectToSearchString }); }; export default useStateWithRoute; diff --git a/apps/bridge-dapp/src/hooks/useTxTabFromRoute.ts b/apps/bridge-dapp/src/hooks/useTxTabFromRoute.ts new file mode 100644 index 0000000000..913fbbf1d9 --- /dev/null +++ b/apps/bridge-dapp/src/hooks/useTxTabFromRoute.ts @@ -0,0 +1,19 @@ +import { useMemo } from 'react'; +import { useLocation } from 'react-router'; +import { BRIDGE_TABS } from '../constants'; + +/** + * Returns the current transaction tab on the bridge + * based on the current location pathname + * @returns the current transaction tab on the bridge, + * available values are `deposit`, `withdraw`, `transfer` or `undefined` + */ +const useTxTabFromRoute = () => { + const { pathname } = useLocation(); + + return useMemo(() => { + return BRIDGE_TABS.find((tab) => pathname.includes(tab)); + }, [pathname]); +}; + +export default useTxTabFromRoute; diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/index.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/index.tsx index 37e311fda8..be6c5a52e0 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/index.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/index.tsx @@ -8,7 +8,7 @@ import { useCheckMobile, } from '@webb-tools/webb-ui-components'; import { useMemo } from 'react'; -import { Outlet, useLocation, useNavigate } from 'react-router'; +import { Outlet, useLocation } from 'react-router'; import SlideAnimation from '../../../../components/SlideAnimation'; import { BRIDGE_TABS, @@ -18,11 +18,12 @@ import { SELECT_TOKEN_PATH, } from '../../../../constants'; import BridgeTabsContainer from '../../../../containers/BridgeTabsContainer'; +import useNavigateWithPersistParams from '../../../../hooks/useNavigateWithPersistParams'; import useDepositButtonProps from './private/useDepositButtonProps'; import useWatchSearchParams from './private/useWatchSearchParams'; const Deposit = () => { - const navigate = useNavigate(); + const navigate = useNavigateWithPersistParams(); const { isMobile } = useCheckMobile(); @@ -34,7 +35,6 @@ const Deposit = () => { destTypedChainId, fungibleCfg, onAmountChange, - searchParams, srcTypedChainId, wrappableCfg, } = useWatchSearchParams(); @@ -81,23 +81,14 @@ const Deposit = () => { > - navigate({ - pathname: SELECT_SOURCE_CHAIN_PATH, - search: searchParams.toString(), - }) - } + onClick={() => navigate(SELECT_SOURCE_CHAIN_PATH)} /> - navigate({ - pathname: SELECT_TOKEN_PATH, - search: searchParams.toString(), - }), + onClick: () => navigate(SELECT_TOKEN_PATH), }} /> @@ -112,12 +103,7 @@ const Deposit = () => { > - navigate({ - pathname: SELECT_DESTINATION_CHAIN_PATH, - search: searchParams.toString(), - }) - } + onClick={() => navigate(SELECT_DESTINATION_CHAIN_PATH)} /> @@ -125,11 +111,7 @@ const Deposit = () => { - navigate({ - pathname: SELECT_SHIELDED_POOL_PATH, - search: searchParams.toString(), - }), + onClick: () => navigate(SELECT_SHIELDED_POOL_PATH), }} /> diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useDepositButtonProps.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useDepositButtonProps.tsx index 060686be38..0078ff01ab 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useDepositButtonProps.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useDepositButtonProps.tsx @@ -5,7 +5,7 @@ import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types'; import { useNoteAccount } from '@webb-tools/react-hooks/useNoteAccount'; import numberToString from '@webb-tools/webb-ui-components/utils/numberToString'; import { ComponentProps, useCallback, useMemo, useState } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { formatEther, parseEther } from 'viem'; import { AMOUNT_KEY, @@ -19,6 +19,7 @@ import { import DepositConfirmContainer from '../../../../../containers/DepositConfirmContainer/DepositConfirmContainer'; import { useConnectWallet } from '../../../../../hooks/useConnectWallet'; import handleTxError from '../../../../../utils/handleTxError'; +import { NumberParam, StringParam, useQueryParams } from 'use-query-params'; function useDepositButtonProps({ balance, @@ -42,7 +43,6 @@ function useDepositButtonProps({ const { hasNoteAccount, setOpenNoteAccountModal } = useNoteAccount(); - const [searchParams] = useSearchParams(); const navigate = useNavigate(); const [generatingNote, setGeneratingNote] = useState(false); @@ -53,58 +53,63 @@ function useDepositButtonProps({ typeof DepositConfirmContainer > | null>(null); - const [amount, tokenId, poolId, srcTypedId, destTypedId] = useMemo(() => { - return [ - searchParams.get(AMOUNT_KEY) ?? '', - searchParams.get(TOKEN_KEY) ?? '', - searchParams.get(POOL_KEY) ?? '', - searchParams.get(SOURCE_CHAIN_KEY) ?? '', - searchParams.get(DEST_CHAIN_KEY) ?? '', - ]; - }, [searchParams]); + const [query] = useQueryParams({ + [AMOUNT_KEY]: StringParam, + [POOL_KEY]: NumberParam, + [TOKEN_KEY]: NumberParam, + [SOURCE_CHAIN_KEY]: NumberParam, + [DEST_CHAIN_KEY]: NumberParam, + }); + + const { + [AMOUNT_KEY]: amount, + [POOL_KEY]: poolId, + [TOKEN_KEY]: tokenId, + [SOURCE_CHAIN_KEY]: srcTypedId, + [DEST_CHAIN_KEY]: destTypedId, + } = query; const validAmount = useMemo(() => { - if (!amount) { + if (typeof amount !== 'string' || amount.length === 0) { return false; } - const amountFloat = parseFloat(amount); + const amountBI = BigInt(amount); // amount from search params is parsed already // If balance is not a number, but amount is entered and > 0, // it means user not connected to wallet but entered amount // so we allow it - if (typeof balance !== 'number' && amountFloat > 0) { + if (typeof balance !== 'number' && amountBI > 0) { return true; } - if (!balance || amountFloat <= 0) { + if (!balance || amountBI <= 0) { return false; } const parsedBalance = parseEther(numberToString(balance)); - const parsedAmount = BigInt(amount); // amount from search params is parsed already - return parsedAmount !== ZERO_BIG_INT && parsedAmount <= parsedBalance; + return amountBI !== ZERO_BIG_INT && amountBI <= parsedBalance; }, [amount, balance]); const inputCnt = useMemo(() => { - if (!tokenId) { + if (typeof tokenId !== 'number') { return 'Select token'; } - if (!destTypedId) { + if (typeof destTypedId !== 'number') { return 'Select destination chain'; } - if (!amount) { + if (typeof amount !== 'string' || amount.length === 0) { return 'Enter amount'; } - if (!poolId) { + if (typeof poolId !== 'number') { return 'Select pool'; } - if (!srcTypedId) { + if (typeof srcTypedId !== 'number') { return 'Select source chain'; } @@ -121,7 +126,7 @@ function useDepositButtonProps({ } const activeId = activeApi.typedChainidSubject.getValue(); - if (`${activeId}` !== srcTypedId) { + if (activeId !== srcTypedId) { return 'Switch Chain'; } @@ -129,7 +134,7 @@ function useDepositButtonProps({ }, [activeApi, hasNoteAccount, srcTypedId]); const amountCnt = useMemo(() => { - if (BigInt(amount) === ZERO_BIG_INT) { + if (typeof amount !== 'string' || BigInt(amount) === ZERO_BIG_INT) { return 'Enter amount'; } @@ -181,114 +186,136 @@ function useDepositButtonProps({ return 'Connecting...'; }, [generatingNote]); - const handleSwitchChain = useCallback(async () => { - const nextChain = chainsPopulated[Number(srcTypedId)]; - if (!nextChain) { - throw WebbError.from(WebbErrorCodes.UnsupportedChain); - } - - const isNextChainActive = activeChain?.id === nextChain.id && activeChain?.chainType === nextChain.chainType; - - if (!isWalletConnected || !isNextChainActive) { - if (activeWallet && nextChain.wallets.includes(activeWallet.id)) { - await switchChain(nextChain, activeWallet); - } else { - toggleModal(true, nextChain); - } - return; - } - - if (!hasNoteAccount) { - setOpenNoteAccountModal(true); - } - }, [activeChain?.chainType, activeChain?.id, activeWallet, hasNoteAccount, isWalletConnected, setOpenNoteAccountModal, srcTypedId, switchChain, toggleModal]); // prettier-ignore - - const handleBtnClick = useCallback(async () => { - try { - if (conncnt) { - return await handleSwitchChain(); - } - - if (!noteManager || !activeApi) { - throw WebbError.from(WebbErrorCodes.ApiNotReady); - } - - if (!fungible) { - throw WebbError.from(WebbErrorCodes.NoFungibleTokenAvailable); - } - - const srcTypedIdNum = Number(srcTypedId); - const destTypedIdNum = Number(destTypedId); - const poolIdNum = Number(poolId); - - if (Number.isNaN(srcTypedIdNum) || Number.isNaN(destTypedIdNum) || Number.isNaN(poolIdNum)) { + const handleSwitchChain = useCallback( + async () => { + const nextChain = chainsPopulated[Number(srcTypedId)]; + if (!nextChain) { throw WebbError.from(WebbErrorCodes.UnsupportedChain); } - const srcChain = chainsPopulated[srcTypedIdNum]; - const destChain = chainsPopulated[destTypedIdNum]; - - if (!srcChain || !destChain) { - throw WebbError.from(WebbErrorCodes.UnsupportedChain); + const isNextChainActive = + activeChain?.id === nextChain.id && + activeChain?.chainType === nextChain.chainType; + + if (!isWalletConnected || !isNextChainActive) { + if (activeWallet && nextChain.wallets.includes(activeWallet.id)) { + await switchChain(nextChain, activeWallet); + } else { + toggleModal(true, nextChain); + } + return; } - setGeneratingNote(true); - - const srcAnchorId = apiConfig.getAnchorIdentifier( - poolIdNum, - srcTypedIdNum - ); - - const destAnchorId = apiConfig.getAnchorIdentifier( - poolIdNum, - destTypedIdNum - ); - - if (!srcAnchorId || !destAnchorId) { - throw WebbError.from(WebbErrorCodes.AnchorIdNotFound); + if (!hasNoteAccount) { + setOpenNoteAccountModal(true); } - - const amountBig = BigInt(amount); - const transactNote = await noteManager.generateNote( - activeApi.backend, - srcTypedIdNum, - srcAnchorId, - destTypedIdNum, - destAnchorId, - fungible.symbol, - fungible.decimals, - amountBig - ) - - setGeneratingNote(false); - - setDepositConfirmComponent( - { - setDepositConfirmComponent(null) - navigate(`/${BRIDGE_PATH}/${DEPOSIT_PATH}`) - }} - onClose={() => { - setDepositConfirmComponent(null) - }} - /> - ) - } catch (error) { - handleTxError(error, 'Deposit'); - } - }, [activeApi, amount, apiConfig, conncnt, destTypedId, fungible, handleSwitchChain, navigate, noteManager, poolId, srcTypedId, tokenId]); // prettier-ignore + }, + // prettier-ignore + [activeChain?.chainType, activeChain?.id, activeWallet, hasNoteAccount, isWalletConnected, setOpenNoteAccountModal, srcTypedId, switchChain, toggleModal] + ); + + const handleBtnClick = useCallback( + async () => { + try { + if (conncnt) { + return await handleSwitchChain(); + } + + if (!noteManager || !activeApi) { + throw WebbError.from(WebbErrorCodes.ApiNotReady); + } + + if (!fungible) { + throw WebbError.from(WebbErrorCodes.NoFungibleTokenAvailable); + } + + const srcTypedIdNum = Number(srcTypedId); + const destTypedIdNum = Number(destTypedId); + const poolIdNum = Number(poolId); + + if ( + Number.isNaN(srcTypedIdNum) || + Number.isNaN(destTypedIdNum) || + Number.isNaN(poolIdNum) + ) { + throw WebbError.from(WebbErrorCodes.UnsupportedChain); + } + + const srcChain = chainsPopulated[srcTypedIdNum]; + const destChain = chainsPopulated[destTypedIdNum]; + + if (!srcChain || !destChain) { + throw WebbError.from(WebbErrorCodes.UnsupportedChain); + } + + if (typeof amount !== 'string' || amount.length === 0) { + throw WebbError.from(WebbErrorCodes.InvalidAmount); + } + + setGeneratingNote(true); + + const srcAnchorId = apiConfig.getAnchorIdentifier( + poolIdNum, + srcTypedIdNum + ); + + const destAnchorId = apiConfig.getAnchorIdentifier( + poolIdNum, + destTypedIdNum + ); + + if (!srcAnchorId || !destAnchorId) { + throw WebbError.from(WebbErrorCodes.AnchorIdNotFound); + } + + const amountBig = BigInt(amount); + const transactNote = await noteManager.generateNote( + activeApi.backend, + srcTypedIdNum, + srcAnchorId, + destTypedIdNum, + destAnchorId, + fungible.symbol, + fungible.decimals, + amountBig + ); + + setGeneratingNote(false); + + setDepositConfirmComponent( + { + setDepositConfirmComponent(null); + navigate(`/${BRIDGE_PATH}/${DEPOSIT_PATH}`); + }} + onClose={() => { + setDepositConfirmComponent(null); + }} + /> + ); + } catch (error) { + handleTxError(error, 'Deposit'); + } + }, + // prettier-ignore + [activeApi, amount, apiConfig, conncnt, destTypedId, fungible, handleSwitchChain, navigate, noteManager, poolId, srcTypedId, tokenId] + ); return { children, diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useWatchSearchParams.ts b/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useWatchSearchParams.ts index 88eaf3406f..a785fd5dd5 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useWatchSearchParams.ts +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Deposit/private/useWatchSearchParams.ts @@ -1,151 +1,33 @@ -import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; -import { calculateTypedChainId } from '@webb-tools/sdk-core/typed-chain-id'; -import { useEffect, useMemo } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { - DEST_CHAIN_KEY, - POOL_KEY, - SOURCE_CHAIN_KEY, - TOKEN_KEY, -} from '../../../../../constants'; +import { NumberParam, useQueryParams } from 'use-query-params'; +import { DEST_CHAIN_KEY, SOURCE_CHAIN_KEY } from '../../../../../constants'; import useAmountWithRoute from '../../../../../hooks/useAmountWithRoute'; import useCurrenciesFromRoute from '../../../../../hooks/useCurrenciesFromRoute'; +import useDefaultChainAndPool from '../../../../../hooks/useDefaultChainAndPool'; function useWatchSearchParams() { - const [searchParams, setSearchParams] = useSearchParams(); - const { allCurrencies, fungibleCfg, wrappableCfg } = useCurrenciesFromRoute(); - const { activeChain, apiConfig, activeApi, loading, isConnecting } = - useWebContext(); - - const [srcTypedChainId, destTypedChainId, tokenId] = useMemo(() => { - const sourceStr = searchParams.get(SOURCE_CHAIN_KEY) ?? ''; - const destStr = searchParams.get(DEST_CHAIN_KEY) ?? ''; - - const tokenStr = searchParams.get(TOKEN_KEY) ?? ''; + const [query] = useQueryParams({ + [SOURCE_CHAIN_KEY]: NumberParam, + [DEST_CHAIN_KEY]: NumberParam, + }); - return [ - !Number.isNaN(parseInt(sourceStr)) ? parseInt(sourceStr) : undefined, - !Number.isNaN(parseInt(destStr)) ? parseInt(destStr) : undefined, - !Number.isNaN(parseInt(tokenStr)) ? parseInt(tokenStr) : undefined, - ]; - }, [searchParams]); + const { + [SOURCE_CHAIN_KEY]: srcTypedChainId, + [DEST_CHAIN_KEY]: destTypedChainId, + } = query; const [amount, setAmount] = useAmountWithRoute(); - useEffect(() => { - if (loading || isConnecting) { - return; - } - - if (srcTypedChainId) { - return; - } - - const defaultChain = Object.values(apiConfig.chains)[0]; - const typedChainId = activeChain - ? calculateTypedChainId(activeChain.chainType, activeChain.id) - : calculateTypedChainId(defaultChain.chainType, defaultChain.id); - - setSearchParams(prev => { - const nextParams = new URLSearchParams(prev); - nextParams.set(SOURCE_CHAIN_KEY, `${typedChainId}`); - return nextParams; - }) - - }, [activeChain, apiConfig.chains, isConnecting, loading, setSearchParams, srcTypedChainId]); // prettier-ignore - - const activeBridge = useMemo(() => { - return activeApi?.state.activeBridge; - }, [activeApi]); - - // Find default pool id when source chain is changed - const defaultPoolId = useMemo(() => { - if (!srcTypedChainId) { - return; - } - - if ( - activeBridge && - Object.keys(activeBridge.targets).includes(`${srcTypedChainId}`) - ) { - return `${activeBridge.currency.id}`; - } - - const anchor = Object.entries(apiConfig.anchors).find( - ([, anchorsRecord]) => { - return Object.keys(anchorsRecord).includes(`${srcTypedChainId}`); - } - ); - - return anchor?.[0]; - }, [activeBridge, apiConfig.anchors, srcTypedChainId]); - - useEffect(() => { - if (!defaultPoolId) { - return; - } - - setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.set(POOL_KEY, `${defaultPoolId}`); - - return nextParams; - }); - }, [defaultPoolId, setSearchParams]); - - // Remove token if it is not supported - useEffect(() => { - if (typeof tokenId !== 'number') { - return; - } - - const currency = allCurrencies.find((c) => c.id === tokenId); - // No currency means the token is not supported - if (!currency) { - setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.delete(TOKEN_KEY); - return nextParams; - }); - } - }, [allCurrencies, setSearchParams, tokenId]); - - // Remove destination chain if it is not supported - useEffect(() => { - if (!destTypedChainId) { - return; - } - - if (!fungibleCfg) { - setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.delete(DEST_CHAIN_KEY); - return nextParams; - }); - return; - } - - const anchor = apiConfig.anchors[fungibleCfg.id]; - if (!Object.keys(anchor).includes(`${destTypedChainId}`)) { - setSearchParams((prev) => { - const nextParams = new URLSearchParams(prev); - nextParams.delete(DEST_CHAIN_KEY); - return nextParams; - }); - } - }, [apiConfig.anchors, destTypedChainId, fungibleCfg, setSearchParams]); + useDefaultChainAndPool(); return { allCurrencies, amount, - destTypedChainId, + destTypedChainId: destTypedChainId ?? undefined, fungibleCfg, onAmountChange: setAmount, - searchParams, - setSearchParams, - srcTypedChainId, + srcTypedChainId: srcTypedChainId ?? undefined, wrappableCfg, }; } diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx index d73a30a03c..be4554c1a3 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx @@ -1,143 +1,74 @@ import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; +import { type ApiConfig } from '@webb-tools/dapp-config/api-config'; +import type { ChainConfig } from '@webb-tools/dapp-config/chains/chain-config.interface'; import chainsPopulated from '@webb-tools/dapp-config/chains/chainsPopulated'; import { calculateTypedChainId } from '@webb-tools/sdk-core/typed-chain-id'; import ChainListCard from '@webb-tools/webb-ui-components/components/ListCard/ChainListCard'; -import { +import type { ChainListCardProps, ChainType, } from '@webb-tools/webb-ui-components/components/ListCard/types'; -import { FC, useCallback, useMemo } from 'react'; +import { useCallback, useMemo, type FC } from 'react'; import { useLocation } from 'react-router'; import { useSearchParams } from 'react-router-dom'; +import { NumberParam } from 'use-query-params'; import SlideAnimation from '../../../components/SlideAnimation'; import { - BRIDGE_TABS, DEST_CHAIN_KEY, + POOL_KEY, SOURCE_CHAIN_KEY, + TOKEN_KEY, } from '../../../constants'; +import useChainsFromRoute from '../../../hooks/useChainsFromRoute'; import { useConnectWallet } from '../../../hooks/useConnectWallet'; import useCurrenciesFromRoute from '../../../hooks/useCurrenciesFromRoute'; +import useTxTabFromRoute from '../../../hooks/useTxTabFromRoute'; import useNavigateWithPersistParams from '../../../hooks/useNavigateWithPersistParams'; import { getActiveSourceChains } from '../../../utils/getActiveSourceChains'; +import getParam from '../../../utils/getParam'; const SelectChain: FC<{ chainType: ChainListCardProps['chainType'] }> = ({ chainType, }) => { - const { apiConfig, activeWallet, activeChain, loading, switchChain } = - useWebContext(); - + const { activeWallet, activeChain, loading, switchChain } = useWebContext(); const { toggleModal } = useConnectWallet(); const { pathname } = useLocation(); - - const navigate = useNavigateWithPersistParams(); - const [searchParams] = useSearchParams(); + const navigate = useNavigateWithPersistParams(); - const { fungibleCfg } = useCurrenciesFromRoute(); - - const currentTab = useMemo(() => { - return BRIDGE_TABS.find((tab) => pathname.includes(tab)); - }, [pathname]); - - const srcChain = useMemo(() => { - const typedChainId = searchParams.get(SOURCE_CHAIN_KEY); - if (!typedChainId) { - return undefined; - } - - return apiConfig.chains[parseInt(typedChainId)]; - }, [apiConfig.chains, searchParams]); - - const destChain = useMemo(() => { - const typedChainId = searchParams.get(DEST_CHAIN_KEY); - if (!typedChainId) { - return undefined; - } + const chainsCfg = useChains(chainType); + const currentTab = useTxTabFromRoute(); - return apiConfig.chains[parseInt(typedChainId)]; - }, [apiConfig.chains, searchParams]); + const updateParams = useUpdateParams(); - const chains = useMemo>(() => { - if (currentTab === 'withdraw' && chainType === 'dest') { - return Object.values(getActiveSourceChains(apiConfig.chains)).map( + const chains = useMemo>( + () => + chainsCfg.map( (c) => ({ name: c.name, tag: c.tag, } satisfies ChainType) - ); - } - - if (chainType === 'dest') { - if (!fungibleCfg) { - return []; - } - - return Object.keys(apiConfig.anchors[fungibleCfg.id]) - .map((typedChainId) => apiConfig.chains[parseInt(typedChainId)]) - .map( - (c) => - ({ - name: c.name, - tag: c.tag, - } satisfies ChainType) - ); - } - - return getActiveSourceChains(apiConfig.chains).map((val) => { - return { - name: val.name, - tag: val.tag, - } satisfies ChainType; - }); - }, [apiConfig.anchors, apiConfig.chains, chainType, currentTab, fungibleCfg]); - - const defaultCategory = useMemo(() => { - if (chainType === 'dest') { - return; - } - - if (!currentTab || currentTab === 'deposit') { - return srcChain?.tag ?? activeChain?.tag; - } - }, [activeChain?.tag, chainType, currentTab, srcChain?.tag]); - - const onlyCategory = useMemo(() => { - if (chainType === 'source') { - return; - } - - if (currentTab === 'deposit' || currentTab === 'transfer') { - return srcChain?.tag ?? destChain?.tag; - } - - return destChain?.tag; - }, [chainType, currentTab, destChain?.tag, srcChain?.tag]); + ), + [chainsCfg] + ); const handleClose = useCallback( - (typedChainId?: number) => { - if (currentTab == null) { - navigate(-1); - return; - } - + (nextTypedChainId?: number) => { const path = pathname.split('/').slice(0, -1).join('/'); + let nextParams = new URLSearchParams(searchParams); - const params = new URLSearchParams(searchParams); - if (typedChainId) { - params.set( - chainType === 'dest' ? DEST_CHAIN_KEY : SOURCE_CHAIN_KEY, - typedChainId.toString() - ); + if (typeof nextTypedChainId === 'number') { + nextParams = updateParams(nextParams, nextTypedChainId, chainType); } navigate({ pathname: path, - search: params.toString(), + search: nextParams.toString(), }); }, - [chainType, currentTab, navigate, pathname, searchParams] + [chainType, navigate, pathname, searchParams, updateParams] ); const handleChainChange = useCallback( @@ -168,6 +99,8 @@ const SelectChain: FC<{ chainType: ChainListCardProps['chainType'] }> = ({ [activeWallet, currentTab, handleClose, switchChain, toggleModal] ); + const { defaultCategory, onlyCategory } = useChainCategoryProps(chainType); + return ( = ({ }; export default SelectChain; + +/** + * Get the chains to select for the given chain type + * @param chainType the chain type for getting the chains + * @returns the chains to select for the given chain type + */ +const useChains = ( + chainType: ChainListCardProps['chainType'] = 'source' +): ReadonlyArray => { + const { apiConfig } = useWebContext(); + + const { fungibleCfg } = useCurrenciesFromRoute(); + + if (chainType === 'source') { + return getActiveSourceChains(apiConfig.chains); + } + + if (!fungibleCfg) { + return []; + } + + const anchorRec = apiConfig.anchors[fungibleCfg.id]; + if (!anchorRec) { + return []; + } + + return Object.keys(anchorRec).map((typedChainId) => { + return apiConfig.chains[parseInt(typedChainId)]; + }); +}; + +/** + * Get the default and only category for the chain list card + * + * - Default category: + * + Deposit & Transfer + * * Chain type: source => activeChain ?? 'test' + * * Chain type: dest => undefined + * + Withdraw => source => active chain ?? 'test' + * + * - Only category: + * + Deposit & Transfer + * * Chain type: source => undefined + * * Chain type: dest => category of source chain ?? undefined + * + Withdraw => undefined + * + * @param chainType whether 'source' or 'dest' (default: 'source') + * @return {defaultCategory, onlyCategory} + */ +const useChainCategoryProps = ( + chainType: ChainListCardProps['chainType'] = 'source' +) => { + const { activeChain } = useWebContext(); + + const currentTx = useTxTabFromRoute(); + + const { srcChainCfg } = useChainsFromRoute(); + + const defaultCategory = useMemo(() => { + if (chainType === 'source') { + return srcChainCfg?.tag ?? activeChain?.tag ?? 'test'; + } + }, [activeChain?.tag, chainType, srcChainCfg?.tag]); + + const onlyCategory = useMemo(() => { + if ( + (currentTx === 'deposit' || currentTx === 'transfer') && + chainType === 'dest' + ) { + return srcChainCfg?.tag; + } + }, [chainType, currentTx, srcChainCfg?.tag]); + + return { defaultCategory, onlyCategory }; +}; + +/** + * Check whether the current token id is supported for the given typed chain id + * @param params the query params to get the token id and pool id to check + * @param nextTypedChainId the typed chain id for checking the token id + * @param apiCfg the api config + * @returns Check whether the token id is supported for the given typed chain id + */ +const isTokenSupported = ( + params: URLSearchParams, + nextTypedChainId: number, + fungibleToWrappableMap: ApiConfig['fungibleToWrappableMap'] +): boolean => { + const poolId = getParam(params, POOL_KEY, NumberParam); + if (typeof poolId !== 'number') { + return false; + } + + const tokenId = getParam(params, TOKEN_KEY, NumberParam); + if (typeof tokenId !== 'number') { + return false; + } + + const wrappableMap = fungibleToWrappableMap.get(poolId); + if (!wrappableMap) { + return false; + } + + const wrappableSet = wrappableMap.get(nextTypedChainId); + if (!wrappableSet) { + return false; + } + + return wrappableSet.has(tokenId); +}; + +/** + * Check whether the dest chain id is supported for the given typed chain id + * @param params the query params to get the dest chain id + * @param nextTypedChainId the next typed chain id for checking the dest chain id + * @param anchorsCfg the anchor config + * @returns Check whether the dest chain id is supported for the given typed chain id + */ +const isDestChainSupported = ( + params: URLSearchParams, + nextTypedChainId: number, + anchorsCfg: ApiConfig['anchors'] +): boolean => { + const destChainId = getParam(params, DEST_CHAIN_KEY, NumberParam); + if (typeof destChainId !== 'number') { + return false; + } + + // Check if exist in the anchors config + // at least one record has the dest chain id and the next typed chain id + return Object.values(anchorsCfg).some((anchorRecord) => { + const keys = Object.keys(anchorRecord); + + const includeDest = keys.includes(destChainId.toString()); + const includeNext = keys.includes(nextTypedChainId.toString()); + + return includeDest && includeNext; + }); +}; + +const useUpdateParams = () => { + const { apiConfig } = useWebContext(); + + return useCallback( + ( + prevParams: URLSearchParams, + nextTypedChainId: number, + chainType: ChainListCardProps['chainType'] + ) => { + const nextParams = new URLSearchParams(prevParams); + const key = chainType === 'source' ? SOURCE_CHAIN_KEY : DEST_CHAIN_KEY; + nextParams.set(key, nextTypedChainId.toString()); + + if (chainType === 'dest') { + return nextParams; + } + + // For source chain, we need to check + // 1. Whether the current selected token is still supported + // 2. Whether the current destination chain is still supported + + const tokenSupported = isTokenSupported( + nextParams, + nextTypedChainId, + apiConfig.fungibleToWrappableMap + ); + if (!tokenSupported) { + nextParams.delete(TOKEN_KEY); + } + + const destChainSupported = isDestChainSupported( + nextParams, + nextTypedChainId, + apiConfig.anchors + ); + if (!destChainSupported) { + nextParams.delete(DEST_CHAIN_KEY); + } + + return nextParams; + }, + [apiConfig.anchors, apiConfig.fungibleToWrappableMap] + ); +}; diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectRelayer.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectRelayer.tsx index e7e0eceb3d..234a2b890e 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectRelayer.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectRelayer.tsx @@ -15,13 +15,12 @@ import ToggleCard from '@webb-tools/webb-ui-components/components/ToggleCard'; import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; import IconButton from '@webb-tools/webb-ui-components/components/buttons/IconButton'; import cx from 'classnames'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router-dom'; +import { useCallback, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { BooleanParam, NumberParam, useQueryParams } from 'use-query-params'; import SlideAnimation from '../../../components/SlideAnimation'; import { BRIDGE_PATH, - BRIDGE_TABS, - DEST_CHAIN_KEY, NO_RELAYER_KEY, POOL_KEY, SELECT_SOURCE_CHAIN_PATH, @@ -30,42 +29,31 @@ import { import { useConnectWallet } from '../../../hooks/useConnectWallet'; import useNavigateWithPersistParams from '../../../hooks/useNavigateWithPersistParams'; import { useRelayerManager } from '../../../hooks/useRelayerManager'; -import useStateWithRoute from '../../../hooks/useStateWithRoute'; const SelectRelayer = () => { const { pathname } = useLocation(); - const [searchParams] = useSearchParams(); - const { apiConfig, loading, isConnecting, activeApi } = useWebContext(); const navigate = useNavigateWithPersistParams(); const { toggleModal } = useConnectWallet(); - const [noRelayer, setNoRelayer] = useStateWithRoute(NO_RELAYER_KEY); - const [customRelayer, setCustomRelayer] = useState(''); const [customerRelayerError, setCustomerRelayerError] = useState(''); const [customRelayerLoading, setCustomRelayerLoading] = useState(false); - const txType = useMemo(() => { - return BRIDGE_TABS.find((tab) => pathname.includes(tab)); - }, [pathname]); - - const [typedChainId, poolId] = useMemo(() => { - const typedChainId = - searchParams.get( - txType === 'transfer' ? SOURCE_CHAIN_KEY : DEST_CHAIN_KEY - ) ?? ''; - - const poolId = searchParams.get(POOL_KEY) ?? ''; + const [query, setQuery] = useQueryParams({ + [NO_RELAYER_KEY]: BooleanParam, + [SOURCE_CHAIN_KEY]: NumberParam, + [POOL_KEY]: NumberParam, + }); - return [ - Number.isNaN(parseInt(typedChainId)) ? undefined : parseInt(typedChainId), - Number.isNaN(parseInt(poolId)) ? undefined : parseInt(poolId), - ]; - }, [searchParams, txType]); + const { + [NO_RELAYER_KEY]: noRelayer, + [SOURCE_CHAIN_KEY]: typedChainId, + [POOL_KEY]: poolId, + } = query; const useRelayersArgs = useMemo( () => ({ @@ -84,13 +72,6 @@ const SelectRelayer = () => { setRelayer, } = useRelayers(useRelayersArgs); - // If no relayer is selected, set the active relayer to null - useEffect(() => { - if (noRelayer && activeRelayer) { - setRelayer(null); - } - }, [activeRelayer, noRelayer, setRelayer]); - const { getInfo, addRelayer } = useRelayerManager(); const chainCfg = useMemo(() => { @@ -130,7 +111,7 @@ const SelectRelayer = () => { return r; }) .filter((r): r is RelayerType => r !== undefined); - }, [chainCfg, noRelayer, relayers]); + }, [noRelayer, chainCfg, relayers]); const selectedRelayer = useMemo(() => { return activeRelayer?.beneficiary && chainCfg @@ -187,7 +168,7 @@ const SelectRelayer = () => { if (!customRelayer) { return; } - setCustomRelayerLoading(true); + const error = 'Invalid input. Pleas check your search and try again.'; if (!isValidUrl(customRelayer)) { setCustomerRelayerError(error); @@ -199,11 +180,12 @@ const SelectRelayer = () => { return; // If relayer already exists, do nothing } + setCustomRelayerLoading(true); const info = await getInfo(customRelayer); if (!info) { setCustomerRelayerError(error); } else { - addRelayer(customRelayer); + await addRelayer(customRelayer); setCustomerRelayerError(''); } @@ -217,9 +199,16 @@ const SelectRelayer = () => { const toggleNoRelayer = useCallback( (nextChecked: boolean) => { - setNoRelayer(() => (nextChecked ? '1' : '')); + if (!nextChecked) { + setQuery({ [NO_RELAYER_KEY]: undefined }); + return; + } + + // If no relayer is selected, set the active relayer to null + setQuery({ [NO_RELAYER_KEY]: nextChecked }); + setRelayer(null); }, - [setNoRelayer] + [setQuery, setRelayer] ); const isDisabled = useMemo(() => { @@ -271,7 +260,7 @@ const SelectRelayer = () => { className={cx('max-w-none', { hidden: isDisconnected })} Icon={} switcherProps={{ - checked: !!noRelayer, + checked: Boolean(noRelayer), onCheckedChange: toggleNoRelayer, }} title="No relayer (not recommended)" diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/index.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/index.tsx index ff1da91c19..fb62211a85 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/index.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/index.tsx @@ -24,49 +24,37 @@ import { useWebbUI, } from '@webb-tools/webb-ui-components'; import { FeeItem } from '@webb-tools/webb-ui-components/components/FeeDetails/types'; -import { FC, useCallback, useEffect, useMemo } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import { Outlet, useLocation } from 'react-router'; -import { useSearchParams } from 'react-router-dom'; import { formatEther, parseEther } from 'viem'; import SlideAnimation from '../../../../components/SlideAnimation'; import { BRIDGE_TABS, - DEST_CHAIN_KEY, - POOL_KEY, SELECT_DESTINATION_CHAIN_PATH, SELECT_RELAYER_PATH, SELECT_SHIELDED_POOL_PATH, SELECT_SOURCE_CHAIN_PATH, - SOURCE_CHAIN_KEY, } from '../../../../constants'; import BridgeTabsContainer from '../../../../containers/BridgeTabsContainer'; import TxInfoContainer from '../../../../containers/TxInfoContainer'; +import useChainsFromRoute from '../../../../hooks/useChainsFromRoute'; +import useCurrenciesFromRoute from '../../../../hooks/useCurrenciesFromRoute'; import useNavigateWithPersistParams from '../../../../hooks/useNavigateWithPersistParams'; +import useRelayerWithRoute from '../../../../hooks/useRelayerWithRoute'; import useFeeCalculation from './private/useFeeCalculation'; import useInputs from './private/useInputs'; -import useRelayerWithRoute from './private/useRelayerWithRoute'; import useTransferButtonProps from './private/useTransferButtonProps'; const Transfer = () => { const { pathname } = useLocation(); - const [searchParams, setSearchParams] = useSearchParams(); - - const { balances, initialized } = useBalancesFromNotes(); + const { balances } = useBalancesFromNotes(); const navigate = useNavigateWithPersistParams(); const { isMobile } = useCheckMobile(); - const { - apiConfig, - activeApi, - activeChain, - activeAccount, - loading, - isConnecting, - noteManager, - } = useWebContext(); + const { activeAccount, activeChain, noteManager } = useWebContext(); const { notificationApi } = useWebbUI(); @@ -74,52 +62,22 @@ const Transfer = () => { amount, hasRefund, recipient, + recipientErrorMsg, refundRecipient, - setRefundRecipient, refundRecipientErrorMsg, - recipientErrorMsg, setAmount, setHasRefund, setRecipient, + setRefundRecipient, } = useInputs(); - const { activeRelayer } = useRelayerWithRoute(); - - const [srcTypedChainId, destTypedChainId, poolId] = useMemo(() => { - const srcTypedId = parseInt(searchParams.get(SOURCE_CHAIN_KEY) ?? ''); - const destTypedId = parseInt(searchParams.get(DEST_CHAIN_KEY) ?? ''); - - const poolId = parseInt(searchParams.get(POOL_KEY) ?? ''); - - return [ - Number.isNaN(srcTypedId) ? undefined : srcTypedId, - Number.isNaN(destTypedId) ? undefined : destTypedId, - Number.isNaN(poolId) ? undefined : poolId, - ]; - }, [searchParams]); - - const [srcChainCfg, destChainCfg] = useMemo(() => { - const src = - typeof srcTypedChainId === 'number' - ? apiConfig.chains[srcTypedChainId] - : undefined; - - const dest = - typeof destTypedChainId === 'number' - ? apiConfig.chains[destTypedChainId] - : undefined; - - return [src, dest]; - }, [apiConfig.chains, destTypedChainId, srcTypedChainId]); + const { srcChainCfg, srcTypedChainId, destChainCfg, destTypedChainId } = + useChainsFromRoute(); - const fungibleCfg = useMemo(() => { - return typeof poolId === 'number' - ? apiConfig.currencies[poolId] - : undefined; - }, [poolId, apiConfig.currencies]); + const { fungibleCfg } = useCurrenciesFromRoute(); const fungibleMaxAmount = useMemo(() => { - if (!srcTypedChainId) { + if (typeof srcTypedChainId !== 'number') { return; } @@ -128,98 +86,6 @@ const Transfer = () => { } }, [balances, fungibleCfg, srcTypedChainId]); - const activeBridge = useMemo(() => { - return activeApi?.state.activeBridge; - }, [activeApi?.state.activeBridge]); - - // Set default poolId and destTypedChainId on first render - useEffect( - () => { - if (loading || isConnecting || !initialized) { - return; - } - - if (typeof srcTypedChainId === 'number' && typeof poolId === 'number') { - return; - } - - const entries = Object.entries(balances); - if (entries.length > 0) { - // Find first pool & destTypedChainId from balances - const [currencyId, balanceRecord] = entries[0]; - const [typedChainId] = Object.entries(balanceRecord)?.[0] ?? []; - - if (currencyId && typedChainId) { - setSearchParams((prev) => { - const params = new URLSearchParams(prev); - - if (typeof srcTypedChainId !== 'number') { - params.set(SOURCE_CHAIN_KEY, typedChainId); - } - - if (typeof poolId !== 'number') { - params.set(POOL_KEY, currencyId); - } - - return params; - }); - return; - } - } - - if (activeChain && activeBridge) { - setSearchParams((prev) => { - const next = new URLSearchParams(prev); - - const typedChainId = calculateTypedChainId( - activeChain.chainType, - activeChain.id - ); - - if (typeof srcTypedChainId !== 'number') { - next.set(SOURCE_CHAIN_KEY, `${typedChainId}`); - } - - if (typeof poolId !== 'number') { - next.set(POOL_KEY, `${activeBridge.currency.id}`); - } - - return next; - }); - return; - } - - // Here is when no balances and active connection - const [defaultPool, anchors] = Object.entries(apiConfig.anchors)[0]; - const [defaultTypedId] = Object.entries(anchors)[0]; - - const nextParams = new URLSearchParams(); - if (typeof poolId !== 'number' && defaultPool) { - nextParams.set(POOL_KEY, defaultPool); - } - - if (typeof srcTypedChainId !== 'number' && defaultTypedId) { - nextParams.set(SOURCE_CHAIN_KEY, defaultTypedId); - } - - setSearchParams(nextParams); - }, - // prettier-ignore - [activeBridge, activeChain, apiConfig.anchors, balances, initialized, isConnecting, loading, poolId, setSearchParams, srcTypedChainId] - ); - - // If no active relayer, reset refund states - useEffect( - () => { - if (!activeRelayer && (hasRefund || refundRecipient)) { - setHasRefund(''); - setRefundRecipient(''); - } - }, - // prettier-ignore - [activeRelayer, hasRefund, refundRecipient, setHasRefund, setRefundRecipient] - ); - const handleChainClick = useCallback( (destChain?: boolean) => { navigate( @@ -274,6 +140,18 @@ const Transfer = () => { setRecipient(noteAccPub); }, [noteManager, notificationApi, setRecipient]); + const typedChainId = useMemo(() => { + if (typeof srcTypedChainId === 'number') { + return srcTypedChainId; + } + + if (activeChain) { + return calculateTypedChainId(activeChain.chainType, activeChain.id); + } + }, [activeChain, srcTypedChainId]); + + const activeRelayer = useRelayerWithRoute(typedChainId); + const { gasFeeInfo, isLoading: isFeeLoading, @@ -282,6 +160,7 @@ const Transfer = () => { totalFeeToken, totalFeeWei, } = useFeeCalculation({ + typedChainId: typedChainId, activeRelayer, recipientErrorMsg, refundRecipientError: refundRecipientErrorMsg, @@ -306,11 +185,11 @@ const Transfer = () => { }, [activeRelayer, amount, totalFeeWei]); const remainingBalance = useMemo(() => { - if (!poolId || !srcTypedChainId) { + if (!fungibleCfg?.id || !srcTypedChainId) { return; } - const balance = balances[poolId]?.[srcTypedChainId]; + const balance = balances[fungibleCfg.id]?.[srcTypedChainId]; if (typeof balance !== 'bigint') { return; } @@ -325,7 +204,7 @@ const Transfer = () => { } return Number(formatEther(remain)); - }, [amount, balances, poolId, srcTypedChainId]); + }, [amount, balances, fungibleCfg?.id, srcTypedChainId]); const { transferConfirmComponent, ...buttonProps } = useTransferButtonProps({ balances, @@ -358,7 +237,7 @@ const Transfer = () => {
{ { { } className="max-w-none" switcherProps={{ - checked: !!hasRefund, + checked: hasRefund, disabled: !activeRelayer, - onCheckedChange: () => - setHasRefund((prev) => (prev.length > 0 ? '' : '1')), + onCheckedChange: () => setHasRefund((prev) => !prev), }} /> @@ -489,7 +367,7 @@ const Transfer = () => { /> { - return [ - searchParams.get(AMOUNT_KEY), - searchParams.get(POOL_KEY), - searchParams.get(SOURCE_CHAIN_KEY), - searchParams.get(RECIPIENT_KEY), - ]; - }, [searchParams]); - - const [hasRefund, refundRecipient] = useMemo(() => { - return [ - !!searchParams.get(HAS_REFUND_KEY), - searchParams.get(REFUND_RECIPIENT_KEY), - ]; - }, [searchParams]); - - const fungibleCfg = useMemo(() => { - if (poolId) { - return apiConfig.currencies[parseInt(poolId)]; - } - }, [apiConfig.currencies, poolId]); - - const srcChainCfg = useMemo(() => { - if (srcTypedChainId) { - return apiConfig.chains[parseInt(srcTypedChainId)]; - } - }, [apiConfig.chains, srcTypedChainId]); +}) { + const { + activeRelayer, + refundRecipientError, + recipientErrorMsg, + typedChainId, + } = args; + + const { activeApi, apiConfig } = useWebContext(); + + const [query] = useQueryParams({ + [AMOUNT_KEY]: StringParam, + [RECIPIENT_KEY]: StringParam, + [HAS_REFUND_KEY]: BooleanParam, + [REFUND_RECIPIENT_KEY]: StringParam, + }); + + const { + [AMOUNT_KEY]: amount, + [HAS_REFUND_KEY]: hasRefund, + [REFUND_RECIPIENT_KEY]: refundRecipient, + [RECIPIENT_KEY]: recipient, + } = query; + + const { fungibleCfg } = useCurrenciesFromRoute(); const feeArgs = useMemo( () => ({ fungibleCurrencyId: fungibleCfg?.id, - typedChainId: srcTypedChainId ? parseInt(srcTypedChainId) : undefined, + typedChainId: + typeof typedChainId === 'number' ? typedChainId : undefined, } satisfies MaxFeeInfoOption), - [fungibleCfg?.id, srcTypedChainId] + [fungibleCfg?.id, typedChainId] ); const { isLoading, feeInfo, fetchFeeInfo, resetMaxFeeInfo } = @@ -122,14 +113,26 @@ export default function useFeeCalculation(args: UseFeeCalculationArgs) { return fungibleCfg?.symbol; } - return srcChainCfg?.nativeCurrency.symbol; - }, [activeRelayer, fungibleCfg?.symbol, srcChainCfg?.nativeCurrency.symbol]); + if (typeof typedChainId !== 'number') { + return; + } + + return chainsPopulated[typedChainId].nativeCurrency.symbol; + }, [activeRelayer, fungibleCfg?.symbol, typedChainId]); + + const anchorId = useMemo(() => { + if (typeof typedChainId !== 'number' || !fungibleCfg) { + return; + } + + return apiConfig.getAnchorIdentifier(fungibleCfg.id, typedChainId); + }, [apiConfig, fungibleCfg, typedChainId]); // Side effect for auto fetching fee info // when all inputs are filled and valid useEffect( () => { - if (!amount || !fungibleCfg) { + if (!amount || !anchorId || typeof typedChainId !== 'number') { return; } @@ -142,10 +145,19 @@ export default function useFeeCalculation(args: UseFeeCalculationArgs) { return; } - fetchFeeInfo(hasRefund ? activeRelayer : undefined); + const hasSupport = + activeRelayer && + activeApi?.relayerManager && + activeRelayer.isSupported( + typedChainId, + anchorId, + activeApi.relayerManager.cmdKey + ); + + fetchFeeInfo(hasRefund && hasSupport ? activeRelayer : undefined); }, // prettier-ignore - [activeRelayer, amount, fetchFeeInfo, fungibleCfg, hasRefund, recipient, recipientErrorMsg, refundRecipient, refundRecipientError] + [activeApi?.relayerManager, activeRelayer, amount, anchorId, fetchFeeInfo, hasRefund, recipient, recipientErrorMsg, refundRecipient, refundRecipientError, typedChainId] ); return { diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useInputs.ts b/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useInputs.ts index ca6643d159..88d13a10bc 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useInputs.ts +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useInputs.ts @@ -1,36 +1,46 @@ import isValidAddress from '@webb-tools/dapp-types/utils/isValidAddress'; import isValidPublicKey from '@webb-tools/dapp-types/utils/isValidPublicKey'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { + BooleanParam, + StringParam, + useQueryParams, + type UrlUpdateType, +} from 'use-query-params'; import { HAS_REFUND_KEY, RECIPIENT_KEY, REFUND_RECIPIENT_KEY, } from '../../../../../constants'; import useAmountWithRoute from '../../../../../hooks/useAmountWithRoute'; -import useStateWithRoute from '../../../../../hooks/useStateWithRoute'; +import useDefaultChainAndPool from '../../../../../hooks/useDefaultChainAndPool'; +import objectToSearchString from '../../../../../utils/objectToSearchString'; const useInputs = () => { const [amount, setAmount] = useAmountWithRoute(); - const [recipient, setRecipient] = useStateWithRoute(RECIPIENT_KEY); - - const [hasRefund, setHasRefund] = useStateWithRoute(HAS_REFUND_KEY); + const [query, setQuery] = useQueryParams( + { + [RECIPIENT_KEY]: StringParam, + [HAS_REFUND_KEY]: BooleanParam, + [REFUND_RECIPIENT_KEY]: StringParam, + }, + { objectToSearchString } + ); - const [refundRecipient, setRefundRecipient] = - useStateWithRoute(REFUND_RECIPIENT_KEY); + const { + [RECIPIENT_KEY]: recipient, + [HAS_REFUND_KEY]: hasRefund, + [REFUND_RECIPIENT_KEY]: refundRecipient, + } = query; const [recipientErrorMsg, setRecipientErrorMsg] = useState(''); const [refundRecipientErrorMsg, setRefundRecipientErrorMsg] = useState(''); - // Reset the refund recipient if the user toggles the refund switch - useEffect(() => { - if (!hasRefund) { - setRefundRecipient(''); - } - }, [hasRefund, setRefundRecipient]); + useDefaultChainAndPool(); - // Validate recipient input address after 0.5s + // Validate recipient public key after 0.5s useEffect(() => { const timeout = setTimeout(() => { if (recipient && !isValidPublicKey(recipient)) { @@ -43,7 +53,7 @@ const useInputs = () => { return () => clearTimeout(timeout); }, [recipient]); - // Validate refund recipient input address after 0.5s + // Validate refund recipient wallet address after 0.5s useEffect(() => { const timeout = setTimeout(() => { if (refundRecipient && !isValidAddress(refundRecipient)) { @@ -56,17 +66,48 @@ const useInputs = () => { return () => clearTimeout(timeout); }, [refundRecipient]); + const onHasRefundChange = useCallback( + ( + newValue: + | typeof hasRefund + | ((latestValue: typeof hasRefund) => typeof hasRefund), + updateType?: UrlUpdateType + ) => { + let nextValue: typeof hasRefund; + if (typeof newValue === 'function') { + nextValue = newValue(hasRefund); + } else { + nextValue = newValue; + } + + setQuery( + { + [HAS_REFUND_KEY]: nextValue, + ...(Boolean(nextValue) === false + ? { + [REFUND_RECIPIENT_KEY]: undefined, + } + : {}), + }, + updateType + ); + }, + [hasRefund, setQuery] + ); + return { amount, - hasRefund, - recipient, + hasRefund: Boolean(hasRefund), + recipient: recipient ?? '', recipientErrorMsg, - refundRecipient, + refundRecipient: refundRecipient ?? '', refundRecipientErrorMsg, setAmount, - setHasRefund, - setRecipient, - setRefundRecipient, + setHasRefund: onHasRefundChange, + setRecipient: (recipient: string) => + setQuery({ [RECIPIENT_KEY]: recipient }), + setRefundRecipient: (refundRecipient: string) => + setQuery({ [REFUND_RECIPIENT_KEY]: refundRecipient }), }; }; diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useRelayerWithRoute.ts b/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useRelayerWithRoute.ts deleted file mode 100644 index fb67710b30..0000000000 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useRelayerWithRoute.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { OptionalActiveRelayer } from '@webb-tools/abstract-api-provider/relayer/types'; -import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { - NO_RELAYER_KEY, - POOL_KEY, - RELAYER_ENDPOINT_KEY, - SOURCE_CHAIN_KEY, -} from '../../../../../constants'; -import useStateWithRoute from '../../../../../hooks/useStateWithRoute'; - -const useRelayerWithRoute = () => { - const [searchParams] = useSearchParams(); - - const { activeApi, apiConfig } = useWebContext(); - - // State for active selected relayer - const [relayer, setRelayer] = useStateWithRoute(RELAYER_ENDPOINT_KEY); - const [activeRelayer, setActiveRelayer] = - useState(null); - - const [srcTypedChainId, poolId, noRelayer] = useMemo(() => { - const srcTypedId = parseInt(searchParams.get(SOURCE_CHAIN_KEY) ?? ''); - const poolId = parseInt(searchParams.get(POOL_KEY) ?? ''); - - return [ - Number.isNaN(srcTypedId) ? undefined : srcTypedId, - Number.isNaN(poolId) ? undefined : poolId, - !!searchParams.get(NO_RELAYER_KEY), - ]; - }, [searchParams]); - - // Side effect for active relayer subsription - useEffect(() => { - if (!activeApi) { - return; - } - - const sub = activeApi.relayerManager.activeRelayerWatcher.subscribe( - (relayer) => { - // console.log('relayer', relayer); - setRelayer(relayer?.endpoint ?? ''); - setActiveRelayer(relayer); - } - ); - - return () => sub.unsubscribe(); - }, [activeApi, setRelayer]); - - // Side effect for reset the active relayer - // when no relayer is selected - useEffect(() => { - if (!noRelayer || !activeApi) { - return; - } - - const active = activeApi.relayerManager.activeRelayer; - if (active) { - activeApi.relayerManager.setActiveRelayer( - null, - activeApi.typedChainidSubject.getValue() - ); - } - }, [activeApi, noRelayer]); - - const hasSetDefaultRelayer = useRef(false); - - // Side effect for setting the default relayer - // when the relayer list is loaded and no active relayer - useEffect(() => { - if (!activeApi || noRelayer || hasSetDefaultRelayer.current) { - return; - } - - const sub = activeApi.relayerManager.listUpdated.subscribe(async () => { - const typedChainIdToUse = - srcTypedChainId ?? activeApi.typedChainidSubject.getValue(); - - const target = - typeof poolId === 'number' - ? apiConfig.anchors[poolId]?.[typedChainIdToUse] - : ''; - - const relayers = - await activeApi.relayerManager.getRelayersByChainAndAddress( - typedChainIdToUse, - target - ); - - const active = activeApi.relayerManager.activeRelayer; - if (!active && relayers.length > 0) { - activeApi.relayerManager.setActiveRelayer( - relayers[0], - typedChainIdToUse - ); - hasSetDefaultRelayer.current = true; - } - }); - - // trigger the relayer list update on mount - activeApi.relayerManager.listUpdated$.next(); - - return () => sub.unsubscribe(); - }, [activeApi, apiConfig.anchors, srcTypedChainId, noRelayer, poolId]); - - // Side effect to update the active relayer - // when the destination chain id or pool id changes - // and the current active relayer not support the chain or pool - useEffect(() => { - if (typeof srcTypedChainId !== 'number' || typeof poolId !== 'number') { - return; - } - - const relayerManager = activeApi?.relayerManager; - if (!relayerManager) { - return; - } - - const anchorId = apiConfig.anchors[poolId]?.[srcTypedChainId]; - if (!anchorId) { - return; - } - - const relayers = relayerManager.getRelayersByChainAndAddress( - srcTypedChainId, - anchorId - ); - - const active = relayerManager.activeRelayer; - if (!active) { - return; - } - - const found = relayers.find((r) => r.endpoint === active.endpoint); - if (found) { - return; - } - - relayerManager.setActiveRelayer(relayers[0] ?? null, srcTypedChainId); - }, [poolId, srcTypedChainId, activeApi?.relayerManager, apiConfig.anchors]); - - return { - relayer, - activeRelayer, - }; -}; - -export default useRelayerWithRoute; diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useTransferButtonProps.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useTransferButtonProps.tsx index a314efe74a..e6e8883c7a 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useTransferButtonProps.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Transfer/private/useTransferButtonProps.tsx @@ -9,26 +9,24 @@ import { NoteManager } from '@webb-tools/note-manager/'; import { useBalancesFromNotes } from '@webb-tools/react-hooks/currency/useBalancesFromNotes'; import { useNoteAccount } from '@webb-tools/react-hooks/useNoteAccount'; import { useVAnchor } from '@webb-tools/react-hooks/vanchor/useVAnchor'; -import { Keypair } from '@webb-tools/sdk-core'; +import { Keypair, calculateTypedChainId } from '@webb-tools/sdk-core'; import { ComponentProps, useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router'; -import { useSearchParams } from 'react-router-dom'; -import { formatEther, parseEther, parseUnits } from 'viem'; +import { BooleanParam, StringParam, useQueryParams } from 'use-query-params'; +import { formatEther } from 'viem'; import { AMOUNT_KEY, BRIDGE_PATH, - DEST_CHAIN_KEY, HAS_REFUND_KEY, - POOL_KEY, RECIPIENT_KEY, REFUND_RECIPIENT_KEY, - SOURCE_CHAIN_KEY, TRANSFER_PATH, } from '../../../../../constants'; import TransferConfirmContainer from '../../../../../containers/TransferConfirmContainer/TransferConfirmContainer'; +import useChainsFromRoute from '../../../../../hooks/useChainsFromRoute'; import { useConnectWallet } from '../../../../../hooks/useConnectWallet'; +import useCurrenciesFromRoute from '../../../../../hooks/useCurrenciesFromRoute'; import handleTxError from '../../../../../utils/handleTxError'; -import validateNoteLeafIndex from '../../../../../utils/validateNoteLeafIndex'; export type UseTransferButtonPropsArgs = { balances: ReturnType['balances']; @@ -57,12 +55,9 @@ function useTransferButtonProps({ }: UseTransferButtonPropsArgs) { const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const { activeApi, activeChain, - apiConfig, isConnecting, loading, switchChain, @@ -70,56 +65,28 @@ function useTransferButtonProps({ noteManager, } = useWebContext(); - const [amount, poolId, recipient] = useMemo(() => { - const amountStr = searchParams.get(AMOUNT_KEY) ?? ''; - - const poolId = searchParams.get(POOL_KEY) ?? ''; - - const recipientStr = searchParams.get(RECIPIENT_KEY) ?? ''; + const [query] = useQueryParams({ + [AMOUNT_KEY]: StringParam, + [RECIPIENT_KEY]: StringParam, + [HAS_REFUND_KEY]: BooleanParam, + [REFUND_RECIPIENT_KEY]: StringParam, + }); - return [ - amountStr ? formatEther(BigInt(amountStr)) : undefined, - !Number.isNaN(parseInt(poolId)) ? parseInt(poolId) : undefined, - recipientStr ? recipientStr : undefined, - ]; - }, [searchParams]); - - const [hasRefund, refundRecipient] = useMemo(() => { - const hasRefund = searchParams.has(HAS_REFUND_KEY); - const refundRecipientStr = searchParams.get(REFUND_RECIPIENT_KEY) ?? ''; - - return [!!hasRefund, refundRecipientStr ? refundRecipientStr : undefined]; - }, [searchParams]); + const { + [AMOUNT_KEY]: amount, + [RECIPIENT_KEY]: recipient, + [HAS_REFUND_KEY]: hasRefund, + [REFUND_RECIPIENT_KEY]: refundRecipient, + } = query; - const [srcTypedChainId, destTypedChainId] = useMemo(() => { - const srcTypedChainId = searchParams.get(SOURCE_CHAIN_KEY) ?? ''; - const destTypedIdStr = searchParams.get(DEST_CHAIN_KEY) ?? ''; + const { + srcChainCfg: srcChain, + srcTypedChainId, + destChainCfg: destChain, + destTypedChainId, + } = useChainsFromRoute(); - return [ - !Number.isNaN(parseInt(srcTypedChainId)) - ? parseInt(srcTypedChainId) - : undefined, - !Number.isNaN(parseInt(destTypedIdStr)) - ? parseInt(destTypedIdStr) - : undefined, - ]; - }, [searchParams]); - - const [fungibleCfg, srcChain, destChain] = useMemo( - () => { - return [ - typeof poolId === 'number' ? apiConfig.currencies[poolId] : undefined, - typeof srcTypedChainId === 'number' - ? chainsPopulated[srcTypedChainId] - : undefined, - typeof destTypedChainId === 'number' - ? chainsPopulated[destTypedChainId] - : undefined, - ]; - }, - // prettier-ignore - [apiConfig.currencies, destTypedChainId, poolId, srcTypedChainId] - ); + const { fungibleCfg } = useCurrenciesFromRoute(); const { hasNoteAccount, setOpenNoteAccountModal } = useNoteAccount(); @@ -134,7 +101,7 @@ function useTransferButtonProps({ return false; } - if (!amount || Number.isNaN(Number(amount)) || Number(amount) <= 0) { + if (typeof amount !== 'string' || amount.length === 0) { return false; } @@ -147,7 +114,15 @@ function useTransferButtonProps({ return false; } - return parseEther(amount) <= balance && receivingAmount >= 0; + try { + const amountBI = BigInt(amount); + return ( + amountBI > ZERO_BIG_INT && amountBI <= balance && receivingAmount >= 0 + ); + } catch (error) { + console.error(error); + return false; + } }, [amount, balances, srcTypedChainId, fungibleCfg, receivingAmount]); const connCnt = useMemo(() => { @@ -169,15 +144,20 @@ function useTransferButtonProps({ const inputCnt = useMemo( () => { - if (!srcTypedChainId || !destTypedChainId) { - return 'Select chain'; + if (typeof srcTypedChainId !== 'number') { + return 'Select source chain'; + } + + if (typeof destTypedChainId !== 'number') { + return 'Select destination chain'; } if (!fungibleCfg) { return 'Select pool'; } - if (!amount || Number.isNaN(Number(amount)) || Number(amount) <= 0) { + const amountFilled = typeof amount === 'string' && amount.length > 0; + if (amountFilled && BigInt(amount) <= ZERO_BIG_INT) { return 'Enter amount'; } @@ -212,7 +192,8 @@ function useTransferButtonProps({ const isDisabled = useMemo( () => { const allInputsFilled = - !!amount && + typeof amount === 'string' && + amount.length > 0 && !!fungibleCfg && !!recipient && typeof destTypedChainId === 'number'; @@ -343,7 +324,7 @@ function useTransferButtonProps({ ); const fungibleDecimals = fungibleCfg.decimals; - const amountBig = parseUnits(amount, fungibleDecimals); + const amountBig = BigInt(amount); // Get the notes that will be spent for this withdraw const inputNotes = NoteManager.getNotesFifo(avaiNotes, amountBig); @@ -352,23 +333,12 @@ function useTransferButtonProps({ } // Validate the input notes - const edges = await vAnchorApi.getLatestNeighborEdges( - fungibleCfg.id, - srcTypedChainId - ); - const nextIdx = await vAnchorApi.getNextIndex( + const valid = await vAnchorApi.validateInputNotes( + inputNotes, srcTypedChainId, fungibleCfg.id ); - const valid = inputNotes.every((note) => { - if (note.note.sourceChainId === srcTypedChainId.toString()) { - return note.note.index ? BigInt(note.note.index) < nextIdx : true; - } else { - return validateNoteLeafIndex(note, edges); - } - }); - if (!valid) { throw WebbError.from(WebbErrorCodes.NotesNotReady); } @@ -447,7 +417,11 @@ function useTransferButtonProps({ feeToken={feeToken} changeAmount={Number(formatEther(changeAmount))} currency={new Currency(fungibleCfg)} - destChain={destChain} + destChain={ + chainsPopulated[ + calculateTypedChainId(destChain.chainType, destChain.id) + ] + } recipient={recipient} relayer={activeRelayer} note={changeNote} @@ -463,7 +437,7 @@ function useTransferButtonProps({ }} refundAmount={refundAmount} refundToken={refundToken} - refundRecipient={refundRecipient} + refundRecipient={refundRecipient ?? ''} // Already checked in `allInputsFilled` /> ); } catch (error) { diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/index.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/index.tsx index f96b204595..d9626cd0fd 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/index.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/index.tsx @@ -23,20 +23,18 @@ import { useWebbUI, } from '@webb-tools/webb-ui-components'; import { FeeItem } from '@webb-tools/webb-ui-components/components/FeeDetails/types'; -import { useCallback, useEffect, useMemo } from 'react'; -import { Outlet, useLocation, useSearchParams } from 'react-router-dom'; +import { useCallback, useMemo } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; +import { BooleanParam, useQueryParam } from 'use-query-params'; import { formatEther, parseEther } from 'viem'; import SlideAnimation from '../../../../components/SlideAnimation'; import { BRIDGE_TABS, - DEST_CHAIN_KEY, NO_RELAYER_KEY, - POOL_KEY, - SELECT_DESTINATION_CHAIN_PATH, SELECT_RELAYER_PATH, SELECT_SHIELDED_POOL_PATH, + SELECT_SOURCE_CHAIN_PATH, SELECT_TOKEN_PATH, - TOKEN_KEY, } from '../../../../constants'; import { CUSTOM_AMOUNT_TOOLTIP_CONTENT, @@ -44,10 +42,12 @@ import { } from '../../../../constants/tooltipContent'; import BridgeTabsContainer from '../../../../containers/BridgeTabsContainer'; import TxInfoContainer from '../../../../containers/TxInfoContainer'; +import useChainsFromRoute from '../../../../hooks/useChainsFromRoute'; +import useCurrenciesFromRoute from '../../../../hooks/useCurrenciesFromRoute'; import useNavigateWithPersistParams from '../../../../hooks/useNavigateWithPersistParams'; +import useRelayerWithRoute from '../../../../hooks/useRelayerWithRoute'; import useFeeCalculation from './private/useFeeCalculation'; import useInputs from './private/useInputs'; -import useRelayerWithRoute from './private/useRelayerWithRoute'; import useWithdrawButtonProps from './private/useWithdrawButtonProps'; const Withdraw = () => { @@ -57,18 +57,9 @@ const Withdraw = () => { const { isMobile } = useCheckMobile(); - const [searchParams, setSearchParams] = useSearchParams(); + const { balances } = useBalancesFromNotes(); - const { balances, initialized } = useBalancesFromNotes(); - - const { - apiConfig, - activeApi, - activeAccount, - activeChain, - loading, - isConnecting, - } = useWebContext(); + const { activeAccount, activeChain } = useWebContext(); const { notificationApi } = useWebbUI(); @@ -81,143 +72,40 @@ const Withdraw = () => { recipient, recipientErrorMsg, setAmount, + setCustomAmount, setHasRefund, - setIsCustom, setRecipient, } = useInputs(); - const { activeRelayer } = useRelayerWithRoute(); - - const [destTypedChainId, poolId, tokenId, noRelayer] = useMemo(() => { - const destTypedId = parseInt(searchParams.get(DEST_CHAIN_KEY) ?? ''); - - const poolId = parseInt(searchParams.get(POOL_KEY) ?? ''); - const tokenId = parseInt(searchParams.get(TOKEN_KEY) ?? ''); - - const noRelayer = searchParams.get(NO_RELAYER_KEY); + const [noRelayer] = useQueryParam(NO_RELAYER_KEY, BooleanParam); - return [ - Number.isNaN(destTypedId) ? undefined : destTypedId, - Number.isNaN(poolId) ? undefined : poolId, - Number.isNaN(tokenId) ? undefined : tokenId, - Boolean(noRelayer), - ]; - }, [searchParams]); + const { fungibleCfg, wrappableCfg } = useCurrenciesFromRoute(); + const { srcChainCfg, srcTypedChainId } = useChainsFromRoute(); - const [fungibleCfg, wrappableCfg] = useMemo(() => { - return [ - typeof poolId === 'number' ? apiConfig.currencies[poolId] : undefined, - typeof tokenId === 'number' ? apiConfig.currencies[tokenId] : undefined, - ]; - }, [poolId, tokenId, apiConfig.currencies]); - - const fungibleMaxAmount = useMemo(() => { - if (!destTypedChainId) { - return; + const typedChainId = useMemo(() => { + if (typeof srcTypedChainId === 'number') { + return srcTypedChainId; } - if (fungibleCfg && balances[fungibleCfg.id]?.[destTypedChainId]) { - return Number(formatEther(balances[fungibleCfg.id][destTypedChainId])); + if (activeChain) { + return calculateTypedChainId(activeChain.chainType, activeChain.id); } - }, [balances, destTypedChainId, fungibleCfg]); + }, [activeChain, srcTypedChainId]); - const activeBridge = useMemo(() => { - return activeApi?.state.activeBridge; - }, [activeApi?.state.activeBridge]); + const activeRelayer = useRelayerWithRoute(typedChainId); - const destChainCfg = useMemo(() => { - if (typeof destTypedChainId !== 'number') { + const fungibleMaxAmount = useMemo(() => { + if (typeof srcTypedChainId !== 'number') { return; } - return apiConfig.chains[destTypedChainId]; - }, [apiConfig.chains, destTypedChainId]); - - // Set default poolId and destTypedChainId on first render - useEffect( - () => { - if (loading || isConnecting || !initialized) { - return; - } - - if (typeof destTypedChainId === 'number' && typeof poolId === 'number') { - return; - } - - const entries = Object.entries(balances); - if (entries.length > 0) { - // Find first pool & destTypedChainId from balances - const [currencyId, balanceRecord] = entries[0]; - const [typedChainId] = Object.entries(balanceRecord)?.[0] ?? []; - - if (currencyId && typedChainId) { - setSearchParams((prev) => { - const params = new URLSearchParams(prev); - - if (typeof destTypedChainId !== 'number') { - params.set(DEST_CHAIN_KEY, typedChainId); - } - - if (typeof poolId !== 'number') { - params.set(POOL_KEY, currencyId); - } - - return params; - }); - return; - } - } - - if (activeChain && activeBridge) { - setSearchParams((prev) => { - const next = new URLSearchParams(prev); - - const typedChainId = calculateTypedChainId( - activeChain.chainType, - activeChain.id - ); - - if (typeof destTypedChainId !== 'number') { - next.set(DEST_CHAIN_KEY, `${typedChainId}`); - } - - if (typeof poolId !== 'number') { - next.set(POOL_KEY, `${activeBridge.currency.id}`); - } - - return next; - }); - return; - } - - // Here is when no balances and active connection - const [defaultPool, anchors] = Object.entries(apiConfig.anchors)[0]; - const [defaultTypedId] = Object.entries(anchors)[0]; - - const nextParams = new URLSearchParams(); - if (typeof poolId !== 'number' && defaultPool) { - nextParams.set(POOL_KEY, defaultPool); - } - - if (typeof destTypedChainId !== 'number' && defaultTypedId) { - nextParams.set(DEST_CHAIN_KEY, defaultTypedId); - } - - setSearchParams(nextParams); - }, - // prettier-ignore - [activeBridge, activeChain, apiConfig.anchors, balances, destTypedChainId, initialized, isConnecting, loading, poolId, setSearchParams] - ); - - // If no active relayer, reset refund states - useEffect(() => { - if (!activeRelayer && hasRefund) { - setHasRefund(''); + if (fungibleCfg && balances[fungibleCfg.id]?.[srcTypedChainId]) { + return Number(formatEther(balances[fungibleCfg.id][srcTypedChainId])); } - }, [activeRelayer, hasRefund, setHasRefund]); + }, [balances, fungibleCfg, srcTypedChainId]); const handleChainClick = useCallback(() => { - navigate(SELECT_DESTINATION_CHAIN_PATH); + navigate(SELECT_SOURCE_CHAIN_PATH); }, [navigate]); const handleTokenClick = useCallback( @@ -231,7 +119,7 @@ const Withdraw = () => { try { const addr = await window.navigator.clipboard.readText(); - setRecipient(addr); + setRecipient(addr.slice(0, 200)); // limit to 200 chars } catch (e) { notificationApi({ message: 'Failed to read clipboard', @@ -262,7 +150,11 @@ const Withdraw = () => { resetMaxFeeInfo, totalFeeToken, totalFeeWei, - } = useFeeCalculation({ activeRelayer, recipientErrorMsg }); + } = useFeeCalculation({ + activeRelayer, + recipientErrorMsg, + typedChainId: srcTypedChainId, + }); const receivingAmount = useMemo(() => { if (!amount) { @@ -283,11 +175,11 @@ const Withdraw = () => { }, [activeRelayer, amount, totalFeeWei]); const remainingBalance = useMemo(() => { - if (!poolId || !destTypedChainId) { + if (!fungibleCfg?.id || typeof srcTypedChainId !== 'number') { return; } - const balance = balances[poolId]?.[destTypedChainId]; + const balance = balances[fungibleCfg.id]?.[srcTypedChainId]; if (typeof balance !== 'bigint') { return; } @@ -302,7 +194,7 @@ const Withdraw = () => { } return Number(formatEther(remain)); - }, [amount, balances, destTypedChainId, poolId]); + }, [amount, balances, fungibleCfg?.id, srcTypedChainId]); const { withdrawConfirmComponent, ...buttonProps } = useWithdrawButtonProps({ balances, @@ -331,15 +223,13 @@ const Withdraw = () => {
- setIsCustom((prev) => (prev.length > 0 ? '' : '1')) - } + onIsFixedAmountChange={() => setCustomAmount(!isCustom)} > @@ -368,14 +258,12 @@ const Withdraw = () => { - setIsCustom((prev) => (prev.length > 0 ? '' : '1')) - } + onIsFixedAmountChange={() => setCustomAmount(!isCustom)} > @@ -391,6 +279,9 @@ const Withdraw = () => { tokenSelectorProps={{ onClick: () => handleTokenClick(), }} + fixedAmountProps={{ + step: 0.01, + }} /> @@ -456,16 +347,15 @@ const Withdraw = () => { title="Enable refund" Icon={} description={ - destChainCfg - ? `Get ${destChainCfg.nativeCurrency.symbol} on transactions on ${destChainCfg.name}` + srcChainCfg + ? `Get ${srcChainCfg.nativeCurrency.symbol} on transactions on ${srcChainCfg.name}` : undefined } className="max-w-none" switcherProps={{ - checked: !!hasRefund, + checked: hasRefund, disabled: !activeRelayer, - onCheckedChange: () => - setHasRefund((prev) => (prev.length > 0 ? '' : '1')), + onCheckedChange: () => setHasRefund(!hasRefund), }} /> @@ -487,7 +377,7 @@ const Withdraw = () => { isLoading: isFeeLoading, Icon: , value: parseFloat(formatEther(gasFeeInfo)), - tokenSymbol: destChainCfg?.nativeCurrency.symbol, + tokenSymbol: srcChainCfg?.nativeCurrency.symbol, } satisfies FeeItem) : undefined, ].filter((item) => Boolean(item)) as Array @@ -495,7 +385,7 @@ const Withdraw = () => { /> { - return [ - searchParams.get(AMOUNT_KEY), - searchParams.get(POOL_KEY), - searchParams.get(TOKEN_KEY), - searchParams.get(DEST_CHAIN_KEY), - ]; - }, [searchParams]); - - const [hasRefund, recipient] = useMemo( - () => [searchParams.get(HAS_REFUND_KEY), searchParams.get(RECIPIENT_KEY)], - [searchParams] - ); - - const fungibleCfg = useMemo(() => { - if (poolId) { - return apiConfig.currencies[parseInt(poolId)]; + typedChainId?: number | null; +}) { + const { activeRelayer, recipientErrorMsg, typedChainId } = args; + + const [query] = useQueryParams({ + [AMOUNT_KEY]: StringParam, + [HAS_REFUND_KEY]: BooleanParam, + [RECIPIENT_KEY]: StringParam, + }); + + const { + [AMOUNT_KEY]: amount, + [HAS_REFUND_KEY]: hasRefund, + [RECIPIENT_KEY]: recipient, + } = query; + + const chain = useMemo(() => { + if (typeof typedChainId === 'number') { + return chainsPopulated[typedChainId]; } - }, [apiConfig.currencies, poolId]); + }, [typedChainId]); - const destChainCfg = useMemo(() => { - if (destChainId) { - return apiConfig.chains[parseInt(destChainId)]; - } - }, [apiConfig.chains, destChainId]); + const { activeApi, apiConfig } = useWebContext(); + + const { fungibleCfg, wrappableCfg } = useCurrenciesFromRoute(); const feeArgs = useMemo( () => ({ fungibleCurrencyId: fungibleCfg?.id, - typedChainId: destChainId ? parseInt(destChainId) : undefined, + typedChainId: + typeof typedChainId === 'number' ? typedChainId : undefined, } satisfies MaxFeeInfoOption), - [destChainId, fungibleCfg?.id] + [fungibleCfg?.id, typedChainId] ); const { isLoading, feeInfo, fetchFeeInfo, resetMaxFeeInfo } = @@ -119,25 +110,48 @@ export default function useFeeCalculation(args: UseFeeCalculationArgs) { return fungibleCfg?.symbol; } - return destChainCfg?.nativeCurrency.symbol; - }, [activeRelayer, destChainCfg?.nativeCurrency.symbol, fungibleCfg?.symbol]); + return chain?.nativeCurrency.symbol; + }, [activeRelayer, chain?.nativeCurrency.symbol, fungibleCfg?.symbol]); + + const anchorId = useMemo(() => { + if (typeof typedChainId !== 'number' || !fungibleCfg) { + return; + } + + return apiConfig.getAnchorIdentifier(fungibleCfg.id, typedChainId); + }, [apiConfig, fungibleCfg, typedChainId]); + + const allInputFilled = useMemo(() => { + return [amount, wrappableCfg, chain, recipient, !recipientErrorMsg].every( + (item) => Boolean(item) + ); + }, [amount, chain, recipient, recipientErrorMsg, wrappableCfg]); // Side effect for auto fetching fee info // when all inputs are filled and valid useEffect( () => { - if (!amount || !fungibleCfg || !tokenId) { + if (!allInputFilled) { return; } - if (!destChainCfg || !recipient || recipientErrorMsg) { + if (typeof typedChainId !== 'number' || !anchorId) { return; } - fetchFeeInfo(hasRefund ? activeRelayer : undefined); + const hasSupport = + activeRelayer && + activeApi?.relayerManager && + activeRelayer.isSupported( + typedChainId, + anchorId, + activeApi.relayerManager.cmdKey + ); + + fetchFeeInfo(hasRefund && hasSupport ? activeRelayer : undefined); }, // prettier-ignore - [activeRelayer, amount, destChainCfg, fetchFeeInfo, fungibleCfg, hasRefund, recipient, recipientErrorMsg, tokenId] + [activeApi?.relayerManager, activeRelayer, allInputFilled, anchorId, fetchFeeInfo, hasRefund, typedChainId] ); return { diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useInputs.ts b/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useInputs.ts index 34332f0130..20d69bc057 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useInputs.ts +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useInputs.ts @@ -1,24 +1,39 @@ import isValidAddress from '@webb-tools/dapp-types/utils/isValidAddress'; import { useEffect, useState } from 'react'; +import { BooleanParam, StringParam, useQueryParams } from 'use-query-params'; import { HAS_REFUND_KEY, IS_CUSTOM_AMOUNT_KEY, RECIPIENT_KEY, } from '../../../../../constants'; import useAmountWithRoute from '../../../../../hooks/useAmountWithRoute'; -import useStateWithRoute from '../../../../../hooks/useStateWithRoute'; +import useDefaultChainAndPool from '../../../../../hooks/useDefaultChainAndPool'; +import objectToSearchString from '../../../../../utils/objectToSearchString'; const useInputs = () => { const [amount, setAmount] = useAmountWithRoute(); - const [recipient, setRecipient] = useStateWithRoute(RECIPIENT_KEY); + const [query, setQuery] = useQueryParams( + { + [RECIPIENT_KEY]: StringParam, + [HAS_REFUND_KEY]: BooleanParam, + [IS_CUSTOM_AMOUNT_KEY]: BooleanParam, + }, + { + objectToSearchString, + } + ); - const [hasRefund, setHasRefund] = useStateWithRoute(HAS_REFUND_KEY); - - const [isCustom, setIsCustom] = useStateWithRoute(IS_CUSTOM_AMOUNT_KEY); + const { + [RECIPIENT_KEY]: recipient, + [HAS_REFUND_KEY]: hasRefund, + [IS_CUSTOM_AMOUNT_KEY]: isCustom, + } = query; const [recipientErrorMsg, setRecipientErrorMsg] = useState(''); + useDefaultChainAndPool(); + // Validate recipient input address after 1s useEffect(() => { const timeout = setTimeout(() => { @@ -35,12 +50,15 @@ const useInputs = () => { return { amount, setAmount, - recipient, - setRecipient, - hasRefund, - setHasRefund, - isCustom, - setIsCustom, + recipient: recipient ?? '', + hasRefund: Boolean(hasRefund), + isCustom: Boolean(isCustom), + setRecipient: (recipient: string) => + setQuery({ [RECIPIENT_KEY]: recipient }), + setHasRefund: (hasRefund: boolean) => + setQuery({ [HAS_REFUND_KEY]: hasRefund }), + setCustomAmount: (isCustom: boolean) => + setQuery({ [IS_CUSTOM_AMOUNT_KEY]: isCustom }), recipientErrorMsg, }; }; diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useRelayerWithRoute.ts b/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useRelayerWithRoute.ts deleted file mode 100644 index 359f0337c4..0000000000 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useRelayerWithRoute.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { useSearchParams } from 'react-router-dom'; -import useStateWithRoute from '../../../../../hooks/useStateWithRoute'; -import { - DEST_CHAIN_KEY, - NO_RELAYER_KEY, - POOL_KEY, - RELAYER_ENDPOINT_KEY, -} from '../../../../../constants'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; -import { OptionalActiveRelayer } from '@webb-tools/abstract-api-provider/relayer/types'; - -const useRelayerWithRoute = () => { - const [searchParams] = useSearchParams(); - - const { activeApi, apiConfig } = useWebContext(); - - // State for active selected relayer - const [relayer, setRelayer] = useStateWithRoute(RELAYER_ENDPOINT_KEY); - const [activeRelayer, setActiveRelayer] = - useState(null); - - const [destTypedChainId, poolId, noRelayer] = useMemo(() => { - const destTypedId = parseInt(searchParams.get(DEST_CHAIN_KEY) ?? ''); - const poolId = parseInt(searchParams.get(POOL_KEY) ?? ''); - - return [ - Number.isNaN(destTypedId) ? undefined : destTypedId, - Number.isNaN(poolId) ? undefined : poolId, - !!searchParams.get(NO_RELAYER_KEY), - ]; - }, [searchParams]); - - // Side effect for active relayer subsription - useEffect(() => { - if (!activeApi) { - return; - } - - const sub = activeApi.relayerManager.activeRelayerWatcher.subscribe( - (relayer) => { - // console.log('relayer', relayer); - setRelayer(relayer?.endpoint ?? ''); - setActiveRelayer(relayer); - } - ); - - return () => sub.unsubscribe(); - }, [activeApi, setRelayer]); - - // Side effect for reset the active relayer - // when no relayer is selected - useEffect(() => { - if (!noRelayer || !activeApi) { - return; - } - - const active = activeApi.relayerManager.activeRelayer; - if (active) { - activeApi.relayerManager.setActiveRelayer( - null, - activeApi.typedChainidSubject.getValue() - ); - } - }, [activeApi, noRelayer]); - - const hasSetDefaultRelayer = useRef(false); - - // Side effect for setting the default relayer - // when the relayer list is loaded and no active relayer - useEffect(() => { - if (!activeApi || noRelayer || hasSetDefaultRelayer.current) { - return; - } - - const sub = activeApi.relayerManager.listUpdated.subscribe(async () => { - const typedChainIdToUse = - destTypedChainId ?? activeApi.typedChainidSubject.getValue(); - - const target = - typeof poolId === 'number' - ? apiConfig.anchors[poolId]?.[typedChainIdToUse] - : ''; - - const relayers = - await activeApi.relayerManager.getRelayersByChainAndAddress( - typedChainIdToUse, - target - ); - - const active = activeApi.relayerManager.activeRelayer; - if (!active && relayers.length > 0) { - activeApi.relayerManager.setActiveRelayer( - relayers[0], - typedChainIdToUse - ); - hasSetDefaultRelayer.current = true; - } - }); - - // trigger the relayer list update on mount - activeApi.relayerManager.listUpdated$.next(); - - return () => sub.unsubscribe(); - }, [activeApi, apiConfig.anchors, destTypedChainId, noRelayer, poolId]); - - // Side effect to update the active relayer - // when the destination chain id or pool id changes - // and the current active relayer not support the chain or pool - useEffect(() => { - if (typeof destTypedChainId !== 'number' || typeof poolId !== 'number') { - return; - } - - const relayerManager = activeApi?.relayerManager; - if (!relayerManager) { - return; - } - - const anchorId = apiConfig.anchors[poolId]?.[destTypedChainId]; - if (!anchorId) { - return; - } - - const relayers = relayerManager.getRelayersByChainAndAddress( - destTypedChainId, - anchorId - ); - - const active = relayerManager.activeRelayer; - if (!active) { - return; - } - - const found = relayers.find((r) => r.endpoint === active.endpoint); - if (found) { - return; - } - - relayerManager.setActiveRelayer(relayers[0] ?? null, destTypedChainId); - }, [poolId, destTypedChainId, activeApi?.relayerManager, apiConfig.anchors]); - - return { - relayer, - activeRelayer, - }; -}; - -export default useRelayerWithRoute; diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useWithdrawButtonProps.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useWithdrawButtonProps.tsx index 31d442fa3c..24553b62d1 100644 --- a/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useWithdrawButtonProps.tsx +++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/Withdraw/private/useWithdrawButtonProps.tsx @@ -13,22 +13,21 @@ import { useVAnchor, } from '@webb-tools/react-hooks'; import { ComponentProps, useCallback, useMemo, useState } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { BooleanParam, StringParam, useQueryParams } from 'use-query-params'; import { formatEther, formatUnits, parseEther, parseUnits } from 'viem'; import { AMOUNT_KEY, BRIDGE_PATH, - DEST_CHAIN_KEY, HAS_REFUND_KEY, - POOL_KEY, RECIPIENT_KEY, - TOKEN_KEY, WITHDRAW_PATH, } from '../../../../../constants'; import WithdrawConfirmContainer from '../../../../../containers/WithdrawConfirmContainer/WithdrawConfirmContainer'; +import useChainsFromRoute from '../../../../../hooks/useChainsFromRoute'; import { useConnectWallet } from '../../../../../hooks/useConnectWallet'; +import useCurrenciesFromRoute from '../../../../../hooks/useCurrenciesFromRoute'; import handleTxError from '../../../../../utils/handleTxError'; -import validateNoteLeafIndex from '../../../../../utils/validateNoteLeafIndex'; export type UseWithdrawButtonPropsArgs = { balances: ReturnType['balances']; @@ -49,12 +48,9 @@ function useWithdrawButtonProps({ }: UseWithdrawButtonPropsArgs) { const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const { activeApi, activeChain, - apiConfig, isConnecting, loading, switchChain, @@ -62,51 +58,47 @@ function useWithdrawButtonProps({ noteManager, } = useWebContext(); - const [amount, destTypedChainId, poolId, tokenId] = useMemo(() => { - const amountStr = searchParams.get(AMOUNT_KEY) ?? ''; - const destTypedIdStr = searchParams.get(DEST_CHAIN_KEY) ?? ''; - const poolId = searchParams.get(POOL_KEY) ?? ''; - const tokenId = searchParams.get(TOKEN_KEY) ?? ''; - - return [ - amountStr ? formatEther(BigInt(amountStr)) : undefined, - !Number.isNaN(parseInt(destTypedIdStr)) - ? parseInt(destTypedIdStr) - : undefined, - !Number.isNaN(parseInt(poolId)) ? parseInt(poolId) : undefined, - !Number.isNaN(parseInt(tokenId)) ? parseInt(tokenId) : undefined, - ]; - }, [searchParams]); - - const [recipient, hasRefund] = useMemo(() => { - const recipientStr = searchParams.get(RECIPIENT_KEY) ?? ''; - const hasRefundStr = searchParams.get(HAS_REFUND_KEY) ?? ''; - - return [recipientStr ? recipientStr : undefined, !!hasRefundStr]; - }, [searchParams]); - - const [fungibleCfg, wrappableCfg, destChainCfg] = useMemo(() => { - return [ - typeof poolId === 'number' ? apiConfig.currencies[poolId] : undefined, - typeof tokenId === 'number' ? apiConfig.currencies[tokenId] : undefined, - typeof destTypedChainId === 'number' ? apiConfig.chains[destTypedChainId] : undefined - ]; - }, [apiConfig.chains, apiConfig.currencies, destTypedChainId, poolId, tokenId]); // prettier-ignore + const [query] = useQueryParams({ + [AMOUNT_KEY]: StringParam, + [RECIPIENT_KEY]: StringParam, + [HAS_REFUND_KEY]: BooleanParam, + }); + + const { + [AMOUNT_KEY]: amountStr, + [RECIPIENT_KEY]: recipient, + [HAS_REFUND_KEY]: hasRefund, + } = query; + + const amount = useMemo(() => { + if (typeof amountStr !== 'string' || amountStr.length === 0) { + return; + } + + try { + return formatEther(BigInt(amountStr)); + } catch (error) { + console.error(error); + } + }, [amountStr]); + + const { fungibleCfg, wrappableCfg } = useCurrenciesFromRoute(); + const { srcChainCfg, srcTypedChainId } = useChainsFromRoute(); const fungibleAddress = useMemo(() => { - if (typeof destTypedChainId !== 'number' || !fungibleCfg) { + if (typeof srcTypedChainId !== 'number' || !fungibleCfg) { return undefined; } - return fungibleCfg.addresses.get(destTypedChainId); - }, [destTypedChainId, fungibleCfg]); + return fungibleCfg.addresses.get(srcTypedChainId); + }, [fungibleCfg, srcTypedChainId]); const liquidityPool = useCurrencyBalance( wrappableCfg && wrappableCfg.role !== CurrencyRole.Governable ? wrappableCfg.id : undefined, fungibleAddress, - destTypedChainId + srcTypedChainId ?? undefined ); const { hasNoteAccount, setOpenNoteAccountModal } = useNoteAccount(); @@ -147,7 +139,7 @@ function useWithdrawButtonProps({ return false; } - if (typeof destTypedChainId !== 'number') { + if (typeof srcTypedChainId !== 'number') { return false; } @@ -156,7 +148,7 @@ function useWithdrawButtonProps({ } const amountFloat = parseFloat(amount); - const balance = balances[fungibleCfg.id]?.[destTypedChainId]; + const balance = balances[fungibleCfg.id]?.[srcTypedChainId]; if (typeof balance !== 'bigint' && amountFloat > 0) { return true; } @@ -170,7 +162,7 @@ function useWithdrawButtonProps({ } return parseEther(amount) <= balance && receivingAmount >= 0; - }, [amount, balances, destTypedChainId, fungibleCfg, receivingAmount]); + }, [amount, balances, srcTypedChainId, fungibleCfg, receivingAmount]); const connCnt = useMemo(() => { if (!activeApi) { @@ -182,16 +174,16 @@ function useWithdrawButtonProps({ } const activeId = activeApi.typedChainidSubject.getValue(); - if (activeId !== destTypedChainId) { + if (activeId !== srcTypedChainId) { return 'Switch Chain'; } return undefined; - }, [activeApi, destTypedChainId, hasNoteAccount]); + }, [activeApi, srcTypedChainId, hasNoteAccount]); const inputCnt = useMemo( () => { - if (!destTypedChainId) { + if (typeof srcTypedChainId !== 'number') { return 'Select chain'; } @@ -220,7 +212,7 @@ function useWithdrawButtonProps({ } }, // prettier-ignore - [amount, destTypedChainId, fungibleCfg, isSucficientLiq, isValidAmount, recipient, wrappableCfg] + [amount, srcTypedChainId, fungibleCfg, isSucficientLiq, isValidAmount, recipient, wrappableCfg] ); const btnText = useMemo(() => { @@ -255,18 +247,18 @@ function useWithdrawButtonProps({ return false; } - const isDestChainActive = - destChainCfg && - destChainCfg.id === activeChain?.id && - destChainCfg.chainType === activeChain?.chainType; - if (!activeChain || !isDestChainActive) { + const isChainActive = + srcChainCfg && + srcChainCfg.id === activeChain?.id && + srcChainCfg.chainType === activeChain?.chainType; + if (!activeChain || !isChainActive) { return false; } return false; }, // prettier-ignore - [activeChain, amount, destChainCfg, fungibleCfg, hasNoteAccount, isFeeLoading, isSucficientLiq, isValidAmount, isWalletConnected, recipient, wrappableCfg] + [activeChain, amount, fungibleCfg, hasNoteAccount, isFeeLoading, isSucficientLiq, isValidAmount, isWalletConnected, recipient, srcChainCfg, wrappableCfg] ); const isLoading = useMemo(() => { @@ -283,11 +275,11 @@ function useWithdrawButtonProps({ const handleSwitchChain = useCallback( async () => { - if (typeof destTypedChainId !== 'number') { + if (typeof srcTypedChainId !== 'number') { return; } - const nextChain = chainsPopulated[destTypedChainId]; + const nextChain = chainsPopulated[srcTypedChainId]; if (!nextChain) { throw WebbError.from(WebbErrorCodes.UnsupportedChain); } @@ -310,7 +302,7 @@ function useWithdrawButtonProps({ } }, // prettier-ignore - [activeChain?.chainType, activeChain?.id, activeWallet, destTypedChainId, hasNoteAccount, isWalletConnected, setOpenNoteAccountModal, switchChain, toggleModal] + [activeChain?.chainType, activeChain?.id, activeWallet, hasNoteAccount, isWalletConnected, setOpenNoteAccountModal, srcTypedChainId, switchChain, toggleModal] ); const handleWithdrawBtnClick = useCallback( @@ -325,9 +317,9 @@ function useWithdrawButtonProps({ isValidAmount && !!amount && typeof receivingAmount === 'number'; const allInputsFilled = - !!destChainCfg && + !!srcChainCfg && !!fungibleCfg && - !!destTypedChainId && + !!srcTypedChainId && !!recipient && _validAmount; @@ -342,15 +334,15 @@ function useWithdrawButtonProps({ throw WebbError.from(WebbErrorCodes.InvalidArguments); } - const anchorId = activeApi.state.activeBridge.targets[destTypedChainId]; + const anchorId = activeApi.state.activeBridge.targets[srcTypedChainId]; if (!anchorId) { throw WebbError.from(WebbErrorCodes.AnchorIdNotFound); } const resourceId = await vAnchorApi.getResourceId( anchorId, - destChainCfg.id, - destChainCfg.chainType + srcChainCfg.id, + srcChainCfg.chainType ); const avaiNotes = ( @@ -372,23 +364,12 @@ function useWithdrawButtonProps({ } // Validate the input notes - const edges = await vAnchorApi.getLatestNeighborEdges( - fungibleCfg.id, - destTypedChainId - ); - const nextIdx = await vAnchorApi.getNextIndex( - destTypedChainId, + const valid = await vAnchorApi.validateInputNotes( + inputNotes, + srcTypedChainId, fungibleCfg.id ); - const valid = inputNotes.every((note) => { - if (note.note.sourceChainId === destTypedChainId.toString()) { - return note.note.index ? BigInt(note.note.index) < nextIdx : true; - } else { - return validateNoteLeafIndex(note, edges); - } - }); - if (!valid) { throw WebbError.from(WebbErrorCodes.NotesNotReady); } @@ -413,9 +394,9 @@ function useWithdrawButtonProps({ changeAmount > 0 ? await noteManager.generateNote( activeApi.backend, - destTypedChainId, + srcTypedChainId, anchorId, - destTypedChainId, + srcTypedChainId, anchorId, fungibleCfg.symbol, fungibleDecimals, @@ -433,9 +414,9 @@ function useWithdrawButtonProps({ curve: noteManager.defaultNoteGenInput.curve, backend: activeApi.backend, amount: changeAmount.toString(), - chainId: `${destTypedChainId}`, + chainId: `${srcTypedChainId}`, keypair, - originChainId: `${destTypedChainId}`, + originChainId: `${srcTypedChainId}`, index: activeApi.state.defaultUtxoIndex.toString(), }); @@ -446,13 +427,13 @@ function useWithdrawButtonProps({ changeAmount={parseFloat( formatUnits(changeAmount, fungibleDecimals) )} - sourceTypedChainId={destTypedChainId} - targetTypedChainId={destTypedChainId} + sourceTypedChainId={srcTypedChainId} + targetTypedChainId={srcTypedChainId} availableNotes={inputNotes} amount={amountFloat} fee={typeof totalFeeWei === 'bigint' ? totalFeeWei : ZERO_BIG_INT} amountAfterFee={parseEther(`${receivingAmount}`)} - isRefund={hasRefund} + isRefund={Boolean(hasRefund)} fungibleCurrency={{ value: new Currency(fungibleCfg), }} @@ -462,7 +443,7 @@ function useWithdrawButtonProps({ : undefined } refundAmount={hasRefund ? refundAmount : undefined} - refundToken={destChainCfg.nativeCurrency.symbol} + refundToken={srcChainCfg.nativeCurrency.symbol} recipient={recipient} onResetState={() => { resetFeeInfo?.(); @@ -479,7 +460,7 @@ function useWithdrawButtonProps({ } }, // prettier-ignore - [activeApi, amount, connCnt, destChainCfg, destTypedChainId, fungibleCfg, handleSwitchChain, hasRefund, isValidAmount, navigate, noteManager, receivingAmount, recipient, refundAmount, resetFeeInfo, totalFeeWei, vAnchorApi, wrappableCfg] + [activeApi, amount, connCnt, fungibleCfg, handleSwitchChain, hasRefund, isValidAmount, navigate, noteManager, receivingAmount, recipient, refundAmount, resetFeeInfo, srcChainCfg, srcTypedChainId, totalFeeWei, vAnchorApi, wrappableCfg] ); return { diff --git a/apps/bridge-dapp/src/routes/index.tsx b/apps/bridge-dapp/src/routes/index.tsx index 5aa7b5b70d..2cf8c7367f 100644 --- a/apps/bridge-dapp/src/routes/index.tsx +++ b/apps/bridge-dapp/src/routes/index.tsx @@ -1,9 +1,12 @@ import { BareProps } from '@webb-tools/dapp-types'; import { Spinner } from '@webb-tools/icons'; import { AnimatePresence } from 'framer-motion'; +import qs from 'query-string'; import { FC, lazy, Suspense } from 'react'; import { Navigate, Route, Routes } from 'react-router'; import { HashRouter } from 'react-router-dom'; +import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; import { BRIDGE_PATH, DEPOSIT_PATH, @@ -21,11 +24,11 @@ import { import { Layout } from '../containers'; import Deposit from '../pages/Hubble/Bridge/Deposit'; import SelectChain from '../pages/Hubble/Bridge/SelectChain'; +import SelectPool from '../pages/Hubble/Bridge/SelectPool'; import SelectRelayer from '../pages/Hubble/Bridge/SelectRelayer'; import SelectToken from '../pages/Hubble/Bridge/SelectToken'; import Transfer from '../pages/Hubble/Bridge/Transfer'; import Withdraw from '../pages/Hubble/Bridge/Withdraw'; -import SelectPool from '../pages/Hubble/Bridge/SelectPool'; const Bridge = lazy(() => import('../pages/Hubble/Bridge')); const WrapAndUnwrap = lazy(() => import('../pages/Hubble/WrapAndUnwrap')); @@ -50,110 +53,124 @@ const BridgeRoutes = () => { return ( - - - - - } - > + + - + } > - {/** Deposit */} - }> - } - /> - } - /> - } /> - } - /> - + + + + } + > + {/** Deposit */} + }> + } + /> + } + /> + } /> + } + /> + - {/** Transfer */} - }> + {/** Transfer */} + }> + } + /> + } + /> + } + /> + } + /> + + + {/** Withdraw */} + }> + } + /> + } + /> + } /> + } + /> + + + {/** Select connected chain */} } /> - } - /> - } - /> - } /> - - {/** Withdraw */} - }> - } - /> - } - /> - } /> - } /> + } /> - {/** Select connected chain */} } + path={WRAP_UNWRAP_PATH} + element={ + + + + } /> - } /> - - - - - - } - /> - - - - - } - /> + + + + } + /> - - - - } - /> + + + + } + /> - } /> - - + } /> + + + ); diff --git a/apps/bridge-dapp/src/utils/getCardTitle.ts b/apps/bridge-dapp/src/utils/getCardTitle.ts index 5988224893..fa7954da88 100644 --- a/apps/bridge-dapp/src/utils/getCardTitle.ts +++ b/apps/bridge-dapp/src/utils/getCardTitle.ts @@ -1,8 +1,12 @@ -import { TransactionState } from '@webb-tools/abstract-api-provider'; +import { + type TransactionName, + TransactionState, +} from '@webb-tools/abstract-api-provider/transaction'; export const getCardTitle = ( stage: TransactionState, - wrappingFlow: boolean + txName: TransactionName, + wrappingFlow?: boolean ) => { let status = ''; @@ -27,8 +31,48 @@ export const getCardTitle = ( } } - if (!status) - return wrappingFlow ? 'Confirm Wrap and Deposit' : 'Confirm Deposit'; + if (!status) { + return getDefaultTitle(txName, wrappingFlow); + } + return getStatusTitle(status, txName, wrappingFlow); +}; - return wrappingFlow ? `Wrap and Deposit ${status}` : `Deposit ${status}`; +const getDefaultTitle = (txName: TransactionName, wrappingFlow?: boolean) => { + switch (txName) { + case 'Deposit': { + return wrappingFlow ? 'Confirm Wrap and Deposit' : 'Confirm Deposit'; + } + + case 'Withdraw': { + return wrappingFlow ? 'Confirm Unwrap and Withdraw' : 'Confirm Withdraw'; + } + + case 'Transfer': { + return 'Confirm Transfer'; + } + } +}; + +const getStatusTitle = ( + status: string, + txName: TransactionName, + wrappingFlow?: boolean +) => { + switch (txName) { + case 'Deposit': { + return wrappingFlow + ? `Wrapping and Depositing ${status}` + : `Depositing ${status}`; + } + + case 'Withdraw': { + return wrappingFlow + ? `Unwrapping and Withdrawing ${status}` + : `Withdrawing ${status}`; + } + + case 'Transfer': { + return `Transferring ${status}`; + } + } }; diff --git a/apps/bridge-dapp/src/utils/getMessageFromTransactionState.ts b/apps/bridge-dapp/src/utils/getMessageFromTransactionState.ts deleted file mode 100644 index c7b42e2003..0000000000 --- a/apps/bridge-dapp/src/utils/getMessageFromTransactionState.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TransactionState } from '@webb-tools/abstract-api-provider'; - -export const getMessageFromTransactionState = (state: TransactionState) => { - switch (state) { - case TransactionState.FetchingFixtures: { - return 'Fetching ZK Fixtures'; - } - - case TransactionState.FetchingLeaves: { - return 'Fetching Leaves'; - } - - case TransactionState.GeneratingZk: { - return 'Generating ZK Proof'; - } - - case TransactionState.SendingTransaction: { - return 'Sending Transaction'; - } - - default: { - return ''; - } - } -}; diff --git a/apps/bridge-dapp/src/utils/getParam.ts b/apps/bridge-dapp/src/utils/getParam.ts new file mode 100644 index 0000000000..8fa5b3dae1 --- /dev/null +++ b/apps/bridge-dapp/src/utils/getParam.ts @@ -0,0 +1,12 @@ +import { QueryParamConfig } from 'use-query-params'; + +const getParam = ( + params: URLSearchParams, + key: string, + ParamConfig: QueryParamConfig +) => { + const value = params.get(key); + return ParamConfig.decode(value); +}; + +export default getParam; diff --git a/apps/bridge-dapp/src/utils/index.ts b/apps/bridge-dapp/src/utils/index.ts index edac1e2d5c..a7d8bf43f4 100644 --- a/apps/bridge-dapp/src/utils/index.ts +++ b/apps/bridge-dapp/src/utils/index.ts @@ -3,11 +3,11 @@ export * from './downloadNotes'; export * from './errors'; export * from './getCardTitle'; export * from './getDefaultConnection'; -export * from './getMessageFromTransactionState'; +export { default as getParam } from './getParam'; export * from './getTokenURI'; export { default as getVAnchorActionClass } from './getVAnchorActionClass'; export { default as handleMutateNoteIndex } from './handleMutateNoteIndex'; export { default as handleStoreNote } from './handleStoreNote'; export { default as handleTxError } from './handleTxError'; export * from './isValidNote'; -export { default as validateNoteLeafIndex } from './validateNoteLeafIndex'; +export { default as objectToSearchString } from './objectToSearchString'; diff --git a/apps/bridge-dapp/src/utils/objectToSearchString.ts b/apps/bridge-dapp/src/utils/objectToSearchString.ts new file mode 100644 index 0000000000..f2e8c4dc58 --- /dev/null +++ b/apps/bridge-dapp/src/utils/objectToSearchString.ts @@ -0,0 +1,11 @@ +import qs from 'query-string'; +import { type EncodedQuery } from 'use-query-params'; + +const objectToSearchString = (encodedParams: EncodedQuery) => { + return qs.stringify(encodedParams, { + skipEmptyString: true, + skipNull: true, + }); +}; + +export default objectToSearchString; diff --git a/apps/hubble-stats/app/layout.tsx b/apps/hubble-stats/app/layout.tsx index 9db0b628ce..0d26e27933 100644 --- a/apps/hubble-stats/app/layout.tsx +++ b/apps/hubble-stats/app/layout.tsx @@ -1,8 +1,8 @@ -import { WebbUIProvider } from '@webb-tools/webb-ui-components'; import '@webb-tools/webb-ui-components/tailwind.css'; import { Metadata } from 'next'; import { Layout } from '../containers'; +import { NextThemeProvider } from './providers'; export const metadata: Metadata = { title: 'Hubble Stats', @@ -18,13 +18,12 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - {/* TODO: Upgrade to Next.js 13.4.2 might resolve this issue */} - {/* https://github.com/webb-tools/webb-dapp/issues/1228 */} - {/* @ts-expect-error Server Component */} - {children} - + + + + {children} + + ); } diff --git a/apps/hubble-stats/app/pool/[slug]/page.tsx b/apps/hubble-stats/app/pool/[slug]/page.tsx index 99450e6f5a..136afd2fc7 100644 --- a/apps/hubble-stats/app/pool/[slug]/page.tsx +++ b/apps/hubble-stats/app/pool/[slug]/page.tsx @@ -21,13 +21,12 @@ export default function Pool({ params }: { params: { slug: string } }) { return (
-
-
- {/* TypeScript doesn't understand async components. */} - {/* Current approach: https://github.com/vercel/next.js/issues/42292#issuecomment-1298459024 */} - {/* @ts-expect-error Server Component */} - -
+
+ {/* TypeScript doesn't understand async components. */} + {/* Current approach: https://github.com/vercel/next.js/issues/42292#issuecomment-1298459024 */} + {/* @ts-expect-error Server Component */} + + {/* @ts-expect-error Server Component */} diff --git a/apps/hubble-stats/app/providers.tsx b/apps/hubble-stats/app/providers.tsx new file mode 100644 index 0000000000..e67031488e --- /dev/null +++ b/apps/hubble-stats/app/providers.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { ThemeProvider } from 'next-themes'; + +export function NextThemeProvider({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/apps/hubble-stats/components/KeyMetricItem/KeyMetricItem.tsx b/apps/hubble-stats/components/KeyMetricItem/KeyMetricItem.tsx index d7df942ed0..2cf3fe46d3 100644 --- a/apps/hubble-stats/components/KeyMetricItem/KeyMetricItem.tsx +++ b/apps/hubble-stats/components/KeyMetricItem/KeyMetricItem.tsx @@ -38,9 +38,7 @@ const KeyMetricItem: FC = ({ className="text-mono-140 dark:text-mono-40" > {typeof value === 'number' && (prefix ?? '')} - {typeof value === 'number' && value < 10000 - ? Math.floor(value * 100) / 100 - : getRoundedDownNumberWith2Decimals(value)} + {getRoundedDownNumberWith2Decimals(value)} {typeof value === 'number' && suffix && ( = ({ href, typedChainId }) => { + const chainIconAndName = useMemo( + () => ( +
+ + + {getShortenChainName(typedChainId)} + +
+ ), + [typedChainId] + ); + + if (href === undefined) { + return ( +
+ {chainIconAndName} +
+ ); + } + + return ( + { + e.preventDefault(); + window.open(href, '_blank'); + }} + > + {chainIconAndName} + + + ); +}; + const ExplorerUrlsDropdown: FC<{ data: AddressWithExplorerUrlsType; }> = ({ data }) => { @@ -27,50 +71,35 @@ const ExplorerUrlsDropdown: FC<{
- - -
- - View on Explorer - - -
-
- - - {Object.keys(data.urls).map((typedChainId) => ( -
-
- - - {getShortenChainName(+typedChainId)} - -
- 0 && ( + + +
+ - - + View on Explorer + +
- ))} - - + + + + {Object.keys(data.urls).map((typedChainId) => ( + + ))} + + + )}
); }; diff --git a/apps/hubble-stats/components/PoolMetadataTable/types.ts b/apps/hubble-stats/components/PoolMetadataTable/types.ts index f5a186a954..7a9f95e58d 100644 --- a/apps/hubble-stats/components/PoolMetadataTable/types.ts +++ b/apps/hubble-stats/components/PoolMetadataTable/types.ts @@ -1,5 +1,5 @@ export type WrappingFeesByChainType = Record; -export type ExplorerUrlsByChainType = Record; +export type ExplorerUrlsByChainType = Record; export type AddressWithExplorerUrlsType = { address: string; @@ -9,8 +9,6 @@ export type AddressWithExplorerUrlsType = { export type PoolAttributeType = { name: string; detail?: string | AddressWithExplorerUrlsType | WrappingFeesByChainType; - externalLink?: string; - isAddress?: boolean; }; export interface PoolMetadataTableProps { diff --git a/apps/hubble-stats/components/PoolOverviewCardItem/PoolOverviewCardItem.tsx b/apps/hubble-stats/components/PoolOverviewCardItem/PoolOverviewCardItem.tsx index 3920616b8d..2d9ee94b0c 100644 --- a/apps/hubble-stats/components/PoolOverviewCardItem/PoolOverviewCardItem.tsx +++ b/apps/hubble-stats/components/PoolOverviewCardItem/PoolOverviewCardItem.tsx @@ -4,6 +4,7 @@ import { Typography } from '@webb-tools/webb-ui-components'; import { getRoundedAmountString } from '@webb-tools/webb-ui-components/utils'; import { ArrowRight } from '@webb-tools/icons'; +import { getRoundedDownNumberWith2Decimals } from '../../utils'; import { PoolOverviewCardItemProps } from './types'; const PoolOverviewCardItem: FC = ({ @@ -20,10 +21,7 @@ const PoolOverviewCardItem: FC = ({
{typeof value === 'number' && prefix} - {getRoundedAmountString(value, 2, { - roundingFunction: Math.floor, - totalLength: 0, - })} + {getRoundedDownNumberWith2Decimals(value)} {typeof value === 'number' && ( = ({ )}
- {changeRate && ( + {typeof changeRate === 'number' && ( = ({ 'rotate-90 !fill-red-70': changeRate < 0, })} /> - {getRoundedAmountString(Math.abs(changeRate), 1)}% + {getRoundedAmountString(Math.abs(changeRate), 2)}% )}
diff --git a/apps/hubble-stats/components/PoolOverviewTable/PoolOverviewTable.tsx b/apps/hubble-stats/components/PoolOverviewTable/PoolOverviewTable.tsx index 702f618e49..3deb92c164 100644 --- a/apps/hubble-stats/components/PoolOverviewTable/PoolOverviewTable.tsx +++ b/apps/hubble-stats/components/PoolOverviewTable/PoolOverviewTable.tsx @@ -8,7 +8,7 @@ import { useReactTable, } from '@tanstack/react-table'; import { chainsConfig } from '@webb-tools/dapp-config/chains'; -import { ShieldKeyholeLineIcon } from '@webb-tools/icons'; +import { ShieldedAssetIcon } from '@webb-tools/icons'; import { ChainChip, Table, @@ -28,7 +28,7 @@ const staticColumns: ColumnDef[] = [ header: () => null, cell: (props) => (
- + (); const columns: ColumnDef[] = [ columnHelper.accessor('activity', { header: () => , - cell: (props) => ( - - ), + cell: (props) => , }), columnHelper.accessor('tokenAmount', { header: () => , @@ -91,15 +88,35 @@ const PoolTransactionsTable: FC = ({ getPaginationRowModel: getPaginationRowModel(), }); + const onRowClick = (row: Row) => { + const sourceTypedChainId = row.original.sourceTypedChainId; + const txHash = row.original.txHash; + + const blockExplorerUrl = + chainsConfig[sourceTypedChainId]?.blockExplorers?.default?.url; + + if (blockExplorerUrl !== undefined) { + const txExplorerURI = getExplorerURI( + blockExplorerUrl, + txHash, + 'tx', + 'web3' + ); + window.open(txExplorerURI, '_blank'); + } + }; + return (
} isPaginated totalRecords={data.length} + onRowClick={onRowClick} /> ); diff --git a/apps/hubble-stats/components/ShieldedAssetsTable/ShieldedAssetsTable.tsx b/apps/hubble-stats/components/ShieldedAssetsTable/ShieldedAssetsTable.tsx index ebafe4227a..816e1466ed 100644 --- a/apps/hubble-stats/components/ShieldedAssetsTable/ShieldedAssetsTable.tsx +++ b/apps/hubble-stats/components/ShieldedAssetsTable/ShieldedAssetsTable.tsx @@ -1,6 +1,7 @@ 'use client'; -import { FC } from 'react'; +import { FC, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; import { createColumnHelper, useReactTable, @@ -10,6 +11,7 @@ import { getPaginationRowModel, ColumnDef, Table as RTTable, + Row, } from '@tanstack/react-table'; import { Typography, @@ -34,7 +36,6 @@ const columns: ColumnDef[] = [ ), }), @@ -99,6 +100,8 @@ const ShieldedAssetsTable: FC = ({ data = [], pageSize, }) => { + const router = useRouter(); + const table = useReactTable({ data, columns, @@ -117,15 +120,24 @@ const ShieldedAssetsTable: FC = ({ getPaginationRowModel: getPaginationRowModel(), }); + const onRowClick = useCallback( + (row: Row) => { + router.push(`/pool/${row.original.poolAddress}`); + }, + [router] + ); + return (
} isPaginated totalRecords={data.length} + onRowClick={onRowClick} /> ); diff --git a/apps/hubble-stats/components/ShieldedPoolsTable/ShieldedPoolsTable.tsx b/apps/hubble-stats/components/ShieldedPoolsTable/ShieldedPoolsTable.tsx index 996a7758d3..0fc5eb18e1 100644 --- a/apps/hubble-stats/components/ShieldedPoolsTable/ShieldedPoolsTable.tsx +++ b/apps/hubble-stats/components/ShieldedPoolsTable/ShieldedPoolsTable.tsx @@ -1,6 +1,7 @@ 'use client'; -import { FC } from 'react'; +import { FC, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; import { createColumnHelper, useReactTable, @@ -10,6 +11,7 @@ import { getPaginationRowModel, ColumnDef, Table as RTTable, + Row, } from '@tanstack/react-table'; import { Table, IconsGroup, fuzzyFilter } from '@webb-tools/webb-ui-components'; @@ -29,7 +31,6 @@ const columns: ColumnDef[] = [ ), }), @@ -79,6 +80,8 @@ const ShieldedPoolsTable: FC = ({ data = [], pageSize, }) => { + const router = useRouter(); + const table = useReactTable({ data, columns, @@ -97,15 +100,24 @@ const ShieldedPoolsTable: FC = ({ getPaginationRowModel: getPaginationRowModel(), }); + const onRowClick = useCallback( + (row: Row) => { + router.push(`/pool/${row.original.address}`); + }, + [router] + ); + return (
} isPaginated totalRecords={data.length} + onRowClick={onRowClick} /> ); diff --git a/apps/hubble-stats/components/charts/AreaChart.tsx b/apps/hubble-stats/components/charts/AreaChart.tsx index d9f2b688cc..183edbdd17 100644 --- a/apps/hubble-stats/components/charts/AreaChart.tsx +++ b/apps/hubble-stats/components/charts/AreaChart.tsx @@ -8,7 +8,7 @@ import { Tooltip, XAxis, } from 'recharts'; -import { useDarkMode } from '@webb-tools/webb-ui-components'; +import { useNextDarkMode as useDarkMode } from '@webb-tools/webb-ui-components'; import { ChartTooltip } from '..'; import { AreaChartProps } from './types'; diff --git a/apps/hubble-stats/components/charts/BarChart.tsx b/apps/hubble-stats/components/charts/BarChart.tsx index 776ddac303..5ffbdfdd29 100644 --- a/apps/hubble-stats/components/charts/BarChart.tsx +++ b/apps/hubble-stats/components/charts/BarChart.tsx @@ -8,7 +8,7 @@ import { Tooltip, Bar, } from 'recharts'; -import { useDarkMode } from '@webb-tools/webb-ui-components'; +import { useNextDarkMode as useDarkMode } from '@webb-tools/webb-ui-components'; import { ChartTooltip } from '..'; import { BarChartProps } from './types'; @@ -62,7 +62,7 @@ const BarChart: FC = ({ interval="preserveStartEnd" /> { if (active && payload && payload.length) { setValue && setValue(payload[0].payload['value']); diff --git a/apps/hubble-stats/components/charts/VolumeChart.tsx b/apps/hubble-stats/components/charts/VolumeChart.tsx index e87847f7f5..6865f79ade 100644 --- a/apps/hubble-stats/components/charts/VolumeChart.tsx +++ b/apps/hubble-stats/components/charts/VolumeChart.tsx @@ -2,6 +2,7 @@ import { FC } from 'react'; import { ResponsiveContainer, BarChart, XAxis, Tooltip, Bar } from 'recharts'; +import { useNextDarkMode as useDarkMode } from '@webb-tools/webb-ui-components'; import { ChartTooltip } from '..'; import { VolumeChartProps } from './types'; @@ -15,6 +16,8 @@ const VolumeChart: FC = ({ tooltipValuePrefix = '', tooltipValueSuffix = '', }) => { + const [isDarkMode] = useDarkMode(); + return ( = ({ interval="preserveStartEnd" /> { if (active && payload && payload.length) { setValue && setValue(payload[0].payload['deposit']); diff --git a/apps/hubble-stats/components/sideBar/sideBarProps.tsx b/apps/hubble-stats/components/sideBar/sideBarProps.tsx index 74ca964044..945f3433bf 100644 --- a/apps/hubble-stats/components/sideBar/sideBarProps.tsx +++ b/apps/hubble-stats/components/sideBar/sideBarProps.tsx @@ -64,6 +64,7 @@ const sideBarFooter: SideBarFooterType = { isInternal: false, href: WEBB_DOCS_URL, Icon: DocumentationIcon, + useNextThemesForThemeToggle: true, }; const sideBarProps: SidebarProps = { diff --git a/apps/hubble-stats/components/table/ActivityCell.tsx b/apps/hubble-stats/components/table/ActivityCell.tsx index 68d37b09c7..b72278b71f 100644 --- a/apps/hubble-stats/components/table/ActivityCell.tsx +++ b/apps/hubble-stats/components/table/ActivityCell.tsx @@ -3,16 +3,14 @@ import { Typography } from '@webb-tools/webb-ui-components'; import { ActivityCellProps } from './types'; -const ActivityCell: FC = ({ txHash, activity }) => { +const ActivityCell: FC = ({ activity }) => { return ( - - - {activity} - - + + {activity} + ); }; diff --git a/apps/hubble-stats/components/table/NumberCell.tsx b/apps/hubble-stats/components/table/NumberCell.tsx index ef8f1b61a5..6332c94f91 100644 --- a/apps/hubble-stats/components/table/NumberCell.tsx +++ b/apps/hubble-stats/components/table/NumberCell.tsx @@ -1,8 +1,8 @@ import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; import { Typography } from '@webb-tools/webb-ui-components'; -import { getRoundedAmountString } from '@webb-tools/webb-ui-components/utils'; +import { getRoundedDownNumberWith2Decimals } from '../../utils'; import { NumberCellProps } from './types'; const NumberCell: FC = ({ @@ -18,12 +18,7 @@ const NumberCell: FC = ({ > {typeof value === 'number' && (prefix ?? '')} - {isProtected - ? '****' - : getRoundedAmountString(value, 2, { - roundingFunction: Math.floor, - totalLength: 0, - })} + {isProtected ? '****' : getRoundedDownNumberWith2Decimals(value)} {typeof value === 'number' && diff --git a/apps/hubble-stats/components/table/ShieldedCell.tsx b/apps/hubble-stats/components/table/ShieldedCell.tsx index 12a5e4756b..d42c74d681 100644 --- a/apps/hubble-stats/components/table/ShieldedCell.tsx +++ b/apps/hubble-stats/components/table/ShieldedCell.tsx @@ -1,29 +1,22 @@ +import { FC } from 'react'; import { Typography } from '@webb-tools/webb-ui-components'; import { shortenHex } from '@webb-tools/webb-ui-components/utils'; -import Link from 'next/link'; -import { FC } from 'react'; import ShieldedAssetIcon from '@webb-tools/icons/ShieldedAssetIcon'; import { ShieldedCellProps } from './types'; -const ShieldedCell: FC = ({ - title, - address, - poolAddress, -}) => { +const ShieldedCell: FC = ({ title, address }) => { return (
- - - {title} - - + + {title} +
{ +const Layout: FC = ({ children }) => { return ( - + <>
@@ -27,9 +27,9 @@ const Layout = async ({ children }: { children?: React.ReactNode }) => {
{/* Footer */} -
+
- + ); }; diff --git a/apps/hubble-stats/containers/OverviewChipsContainer/OverviewChipsContainer.tsx b/apps/hubble-stats/containers/OverviewChipsContainer/OverviewChipsContainer.tsx index 520491b561..5f922edd26 100644 --- a/apps/hubble-stats/containers/OverviewChipsContainer/OverviewChipsContainer.tsx +++ b/apps/hubble-stats/containers/OverviewChipsContainer/OverviewChipsContainer.tsx @@ -1,4 +1,10 @@ -import { Chip } from '@webb-tools/webb-ui-components'; +import { + Chip, + Tooltip, + TooltipBody, + TooltipTrigger, + Typography, +} from '@webb-tools/webb-ui-components'; import { ArrowRightUp, DatabaseLine } from '@webb-tools/icons'; import { getOverviewChipsData } from '../../data'; @@ -18,14 +24,24 @@ export default async function OverviewChipsContainer() { webbtTNT - - - DEPOSITS:{' '} - {typeof deposit === 'number' - ? getRoundedDownNumberWith2Decimals(deposit) - : '-'}{' '} - webbtTNT - + + + + + DEPOSITS:{' '} + {typeof deposit === 'number' + ? getRoundedDownNumberWith2Decimals(deposit) + : '-'}{' '} + webbtTNT + + + + Historical Deposit Volume + +
); } diff --git a/apps/hubble-stats/containers/PoolOverviewChartsContainer/PoolOverviewChartsCmp.tsx b/apps/hubble-stats/containers/PoolOverviewChartsContainer/PoolOverviewChartsCmp.tsx index 6088768fc3..3723b0b366 100644 --- a/apps/hubble-stats/containers/PoolOverviewChartsContainer/PoolOverviewChartsCmp.tsx +++ b/apps/hubble-stats/containers/PoolOverviewChartsContainer/PoolOverviewChartsCmp.tsx @@ -11,7 +11,7 @@ import { AreaChart, BarChart, VolumeChart } from '../../components'; import { PoolOverviewChartsDataType } from '../../data'; const tvlTab = 'TVL'; -const volumeTab = 'Volume 24H'; +const volumeTab = 'Volume'; const relayerEarningTab = 'Relayer Earnings'; const PoolOverviewChartsCmp: FC = ({ diff --git a/apps/hubble-stats/data/getKeyMetricsData.ts b/apps/hubble-stats/data/getKeyMetricsData.ts index 9f9ebab79b..8ae883b963 100644 --- a/apps/hubble-stats/data/getKeyMetricsData.ts +++ b/apps/hubble-stats/data/getKeyMetricsData.ts @@ -3,7 +3,12 @@ import vAnchorClient from '@webb-tools/vanchor-client'; import { getTvl, getDeposit24h } from './reusable'; import { VANCHOR_ADDRESSES, ACTIVE_SUBGRAPH_URLS } from '../constants'; -import { getValidDatesToQuery, getChangeRate } from '../utils'; +import { + getValidDatesToQuery, + getChangeRate, + getEpochStart, + getEpoch24H, +} from '../utils'; type KeyMetricDataType = { tvl: number | undefined; @@ -16,6 +21,8 @@ type KeyMetricDataType = { export default async function getKeyMetricsData(): Promise { const [_, date24h, date48h] = getValidDatesToQuery(); + const epochStart = getEpochStart(); + const epoch24h = getEpoch24H(); const tvl = await getTvl(); const deposit24h = await getDeposit24h(); @@ -67,6 +74,34 @@ export default async function getKeyMetricsData(): Promise { wrappingFees = undefined; } + let tvl24h: number | undefined; + try { + const latestTvlByVAnchorsByChains = + await vAnchorClient.TotalValueLocked.GetVAnchorsByChainsLatestTVLInTimeRange( + ACTIVE_SUBGRAPH_URLS, + VANCHOR_ADDRESSES, + epochStart, + epoch24h + ); + + tvl24h = Object.values(latestTvlByVAnchorsByChains).reduce( + (total, tvlByVAnchorsByChain) => { + const latestTvlByChain = tvlByVAnchorsByChain.reduce( + (totalByChain, tvlByVAnchor) => + totalByChain + + +formatEther(BigInt(tvlByVAnchor.totalValueLocked ?? 0)), + 0 + ); + return total + latestTvlByChain; + }, + 0 + ); + } catch { + tvl24h = undefined; + } + + const tvlChangeRate = getChangeRate(tvl, tvl24h); + let deposit48h: number | undefined; try { const depositVAnchorsByChainsData = @@ -96,8 +131,7 @@ export default async function getKeyMetricsData(): Promise { return { tvl, - // tvl calculation for 24h is not correct at the moment - tvlChangeRate: undefined, + tvlChangeRate, deposit24h, depositChangeRate, relayerFees, diff --git a/apps/hubble-stats/data/getPoolMetadataData.ts b/apps/hubble-stats/data/getPoolMetadataData.ts index a56d39469f..99b8474cbe 100644 --- a/apps/hubble-stats/data/getPoolMetadataData.ts +++ b/apps/hubble-stats/data/getPoolMetadataData.ts @@ -1,6 +1,7 @@ import { getDateFromEpoch, getWrappingFeesPercentageByFungibleToken, + getExplorerUrlByAddressByChains, } from '../utils'; import { VANCHORS_MAP } from '../constants'; import { @@ -22,25 +23,24 @@ type PoolMetadataDataType = { export default async function getPoolMetadataData( poolAddress: string ): Promise { - const vanchor = VANCHORS_MAP[poolAddress]; - const creationDate = getDateFromEpoch(vanchor.creationTimestamp); - - const supportedChains = vanchor.supportedChains; - - // TODO: Replace this with the real explorer URLs - const explorerUrls = supportedChains.reduce((map, typedChainId) => { - return { - ...map, - [typedChainId]: '#', - }; - }, {}); + const vAnchor = VANCHORS_MAP[poolAddress]; + const { + creationTimestamp, + supportedChains, + fungibleTokenName, + fungibleTokenSymbol, + fungibleTokenAddress, + signatureBridge, + treasuryAddress, + } = vAnchor; + const creationDate = getDateFromEpoch(creationTimestamp); const wrappingFees: WrappingFeesByChainType = {}; for (const typedChainId of supportedChains) { let feesPercentage: number | undefined; try { feesPercentage = await getWrappingFeesPercentageByFungibleToken( - vanchor.fungibleTokenAddress, + fungibleTokenAddress, typedChainId ); } catch { @@ -50,17 +50,26 @@ export default async function getPoolMetadataData( } return { - name: vanchor.fungibleTokenName, - symbol: vanchor.fungibleTokenSymbol, - signatureBridge: { address: vanchor.signatureBridge, urls: explorerUrls }, - vAnchor: { address: poolAddress, urls: explorerUrls }, + name: fungibleTokenName, + symbol: fungibleTokenSymbol, + signatureBridge: { + address: signatureBridge, + urls: getExplorerUrlByAddressByChains(signatureBridge, supportedChains), + }, + vAnchor: { + address: poolAddress, + urls: getExplorerUrlByAddressByChains(poolAddress, supportedChains), + }, fungibleToken: { - address: vanchor.fungibleTokenAddress, - urls: explorerUrls, + address: fungibleTokenAddress, + urls: getExplorerUrlByAddressByChains( + fungibleTokenAddress, + supportedChains + ), }, treasuryAddress: { - address: vanchor.treasuryAddress, - urls: explorerUrls, + address: treasuryAddress, + urls: getExplorerUrlByAddressByChains(treasuryAddress, supportedChains), }, wrappingFees, creationDate: new Date(creationDate).toLocaleDateString('en-US', { diff --git a/apps/hubble-stats/data/getPoolOverviewCardData.ts b/apps/hubble-stats/data/getPoolOverviewCardData.ts index 062e01bb5b..e876ea2eb4 100644 --- a/apps/hubble-stats/data/getPoolOverviewCardData.ts +++ b/apps/hubble-stats/data/getPoolOverviewCardData.ts @@ -3,7 +3,12 @@ import vAnchorClient from '@webb-tools/vanchor-client'; import { getTvlByVAnchor, getDeposit24hByVAnchor } from './reusable'; import { ACTIVE_SUBGRAPH_URLS, VANCHORS_MAP } from '../constants'; -import { getValidDatesToQuery } from '../utils'; +import { + getValidDatesToQuery, + getChangeRate, + getEpochStart, + getEpoch24H, +} from '../utils'; import { PoolType } from '../components/PoolTypeChip/types'; @@ -21,11 +26,39 @@ export default async function getPoolOverviewCardData( poolAddress: string ): Promise { const vanchor = VANCHORS_MAP[poolAddress]; - const [dateNow, date24h, date48h] = getValidDatesToQuery(); + const [_, date24h, date48h] = getValidDatesToQuery(); + const epochStart = getEpochStart(); + const epoch24h = getEpoch24H(); const tvl = await getTvlByVAnchor(poolAddress); const deposit24h = await getDeposit24hByVAnchor(poolAddress); + let tvl24h: number | undefined; + try { + const latestTvlByChains = await Promise.all( + ACTIVE_SUBGRAPH_URLS.map(async (subgraphUrl) => { + const latestTvlByVAnchorByChain = + await vAnchorClient.TotalValueLocked.GetVAnchorByChainLatestTVLInTimeRange( + subgraphUrl, + poolAddress, + epochStart, + epoch24h + ); + return latestTvlByVAnchorByChain.totalValueLocked; + }) + ); + + tvl24h = latestTvlByChains.reduce( + (total, latestTvlByChain) => + total + +formatEther(BigInt(latestTvlByChain ?? 0)), + 0 + ); + } catch { + tvl24h = undefined; + } + + const tvlChangeRate = getChangeRate(tvl, tvl24h); + let deposit48h: number | undefined; try { const depositVAnchorByChainsData = @@ -51,10 +84,7 @@ export default async function getPoolOverviewCardData( deposit48h = undefined; } - const depositChangeRate = - deposit24h && deposit48h - ? ((deposit24h - deposit48h) / deposit48h) * 100 - : undefined; + const depositChangeRate = getChangeRate(deposit24h, deposit48h); return { name: vanchor.fungibleTokenName, @@ -63,7 +93,6 @@ export default async function getPoolOverviewCardData( deposit24h, depositChangeRate, tvl, - // tvl calculation for 24h is not correct at the moment - tvlChangeRate: undefined, + tvlChangeRate, }; } diff --git a/apps/hubble-stats/tsconfig.json b/apps/hubble-stats/tsconfig.json index 575fc31ac0..fd0241e72c 100644 --- a/apps/hubble-stats/tsconfig.json +++ b/apps/hubble-stats/tsconfig.json @@ -16,7 +16,8 @@ "name": "next" } ], - "types": ["jest", "node"] + "types": ["jest", "node"], + "skipLibCheck": true }, "include": [ "**/*.ts", diff --git a/apps/hubble-stats/utils/date.ts b/apps/hubble-stats/utils/date.ts index d2b4337a79..fa546940eb 100644 --- a/apps/hubble-stats/utils/date.ts +++ b/apps/hubble-stats/utils/date.ts @@ -21,6 +21,8 @@ export const getEpochArray = ( const EPOCH_DAY_INTERVAL = 24 * 60 * 60; export const getEpochNow = () => getEpochFromDate(new Date()); +export const getEpoch24H = () => + getEpochFromDate(new Date()) - EPOCH_DAY_INTERVAL; const EPOCH_START = process.env.HUBBLE_STATS_EPOCH_START ? +process.env.HUBBLE_STATS_EPOCH_START diff --git a/apps/hubble-stats/utils/getExplorerUrlByAddressByChains.ts b/apps/hubble-stats/utils/getExplorerUrlByAddressByChains.ts new file mode 100644 index 0000000000..b85ebd96ae --- /dev/null +++ b/apps/hubble-stats/utils/getExplorerUrlByAddressByChains.ts @@ -0,0 +1,26 @@ +import { chainsConfig } from '@webb-tools/dapp-config/chains'; +import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/utils'; + +const getExplorerUrlByAddressByChains = ( + address: string, + typedChainIds: number[] +): Record => { + return typedChainIds.reduce((map, typedChainId) => { + const blockExplorerUrl = + chainsConfig[typedChainId]?.blockExplorers?.default?.url; + + return { + ...map, + [typedChainId]: blockExplorerUrl + ? getExplorerURI( + blockExplorerUrl, + address, + 'address', + 'web3' + ).toString() + : undefined, + }; + }, {}); +}; + +export default getExplorerUrlByAddressByChains; diff --git a/apps/hubble-stats/utils/getShortenChainName.ts b/apps/hubble-stats/utils/getShortenChainName.ts index 74008b1402..7cb8184091 100644 --- a/apps/hubble-stats/utils/getShortenChainName.ts +++ b/apps/hubble-stats/utils/getShortenChainName.ts @@ -2,12 +2,6 @@ import { chainsConfig } from '@webb-tools/dapp-config/chains'; const getShortenChainName = (typedChainId: number) => { const fullChainName = chainsConfig[typedChainId].name; - - // check for Orbit chains - if (fullChainName.toLowerCase().includes('orbit')) { - return fullChainName.split(' ')[0]; - } - return fullChainName.split(' ').pop(); }; diff --git a/apps/hubble-stats/utils/index.ts b/apps/hubble-stats/utils/index.ts index ab1acffbb6..7e2e43d82e 100644 --- a/apps/hubble-stats/utils/index.ts +++ b/apps/hubble-stats/utils/index.ts @@ -1,4 +1,5 @@ export { default as getAggregateValue } from './getAggregateValue'; +export { default as getExplorerUrlByAddressByChains } from './getExplorerUrlByAddressByChains'; export { default as getChainNamesByTypedId } from './getChainNamesByTypedId'; export { default as getChangeRate } from './getChangeRate'; export { default as getFormattedDataForBasicChart } from './getFormattedDataForBasicChart'; diff --git a/libs/abstract-api-provider/src/relayer/webb-relayer-manager.ts b/libs/abstract-api-provider/src/relayer/webb-relayer-manager.ts index a60da8d02d..d6ce2b0c28 100644 --- a/libs/abstract-api-provider/src/relayer/webb-relayer-manager.ts +++ b/libs/abstract-api-provider/src/relayer/webb-relayer-manager.ts @@ -14,11 +14,15 @@ import { TransactionState, } from '../transaction'; import calculateProvingLeavesAndCommitmentIndex from '../utils/calculateProvingLeavesAndCommitmentIndex'; -import { WebbProviderType } from '../webb-provider.interface'; +import { WebbProviderType } from '../types'; import { OptionalActiveRelayer, OptionalRelayer, RelayerQuery } from './types'; import { WebbRelayer } from './webb-relayer'; +import { type RelayerCMDBase } from '@webb-tools/dapp-config/relayer-config'; -export abstract class WebbRelayerManager { +export abstract class WebbRelayerManager< + Provider extends WebbProviderType, + CMDKey extends RelayerCMDBase +> { protected supportedPallet: string | undefined; protected readonly logger = LoggerService.get('RelayerManager'); @@ -35,6 +39,8 @@ export abstract class WebbRelayerManager { protected relayers: WebbRelayer[]; public activeRelayer: OptionalActiveRelayer = null; + abstract cmdKey: CMDKey; + constructor(relayers: WebbRelayer[]) { this.relayers = relayers; this.activeRelayerWatcher = this.activeRelayerSubject.asObservable(); diff --git a/libs/abstract-api-provider/src/relayer/webb-relayer.ts b/libs/abstract-api-provider/src/relayer/webb-relayer.ts index b7f1974fde..2ee0766bf5 100644 --- a/libs/abstract-api-provider/src/relayer/webb-relayer.ts +++ b/libs/abstract-api-provider/src/relayer/webb-relayer.ts @@ -1,16 +1,19 @@ // Copyright 2022 @webb-tools/ // SPDX-License-Identifier: Apache-2.0 -import { ChainType, parseTypedChainId } from '@webb-tools/sdk-core'; -import { BehaviorSubject, Observable, Subject, firstValueFrom } from 'rxjs'; -import { filter } from 'rxjs/operators'; - import { LoggerService } from '@webb-tools/browser-utils'; -import { RelayerCMDBase } from '@webb-tools/dapp-config/relayer-config'; +import type { RelayerCMDBase } from '@webb-tools/dapp-config/relayer-config'; import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types'; import { + ChainType, + parseTypedChainId, +} from '@webb-tools/sdk-core/typed-chain-id'; +import { BehaviorSubject, Observable, Subject, firstValueFrom } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import type { CMDSwitcher, Capabilities, + RelayedChainConfig, RelayedChainInput, RelayerCMDKey, RelayerMessage, @@ -326,6 +329,57 @@ export class WebbRelayer { return new URL(this.infoRoute, this.endpoint).toString(); } + private isEVMSupported( + relayerChainCfg: RelayedChainConfig<'evm'>, + anchorId: string + ): boolean { + const supportAnchor = relayerChainCfg.contracts.find( + (contract) => BigInt(contract.address) === BigInt(anchorId) // Use BigInt to prevent case-sensitive comparison + ); + + return Boolean(supportAnchor); + } + + private isSubstrateSupported( + relayerChainCfg: RelayedChainConfig<'substrate'>, + anchorId: string + ): boolean { + const supportAnchor = relayerChainCfg.pallets.find( + (pallet) => BigInt(pallet.pallet) === BigInt(anchorId) // Use BigInt to prevent case-sensitive comparison + ); + + return Boolean(supportAnchor); + } + + isSupported( + typedChainId: number, + anchorId: string, + basedOn: RelayerCMDBase + ): boolean { + if (basedOn === 'evm') { + const relayerChainCfg = + this.capabilities.supportedChains.evm.get(typedChainId); + if (!relayerChainCfg) { + return false; + } + + return this.isEVMSupported(relayerChainCfg, anchorId); + } + + if (basedOn === 'substrate') { + const relayerChainCfg = + this.capabilities.supportedChains.substrate.get(typedChainId); + if (!relayerChainCfg) { + return false; + } + + return this.isSubstrateSupported(relayerChainCfg, anchorId); + } + + console.error(WebbError.getErrorMessage(WebbErrorCodes.InvalidArguments)); + return false; + } + async initWithdraw(target: Target) { return new RelayedWithdraw(new URL(this.endpoint), target); } diff --git a/libs/abstract-api-provider/src/transaction.ts b/libs/abstract-api-provider/src/transaction.ts index e41fbb358b..f2cac88e31 100644 --- a/libs/abstract-api-provider/src/transaction.ts +++ b/libs/abstract-api-provider/src/transaction.ts @@ -3,7 +3,7 @@ import { notificationApi } from '@webb-tools/webb-ui-components'; import { BehaviorSubject, Observable } from 'rxjs'; import { CancellationToken } from './cancelation-token'; -import { WebbProviderType } from './webb-provider.interface'; +import { WebbProviderType } from './types'; export interface TXresultBase { // method: MethodPath; diff --git a/libs/abstract-api-provider/src/types.ts b/libs/abstract-api-provider/src/types.ts new file mode 100644 index 0000000000..ab026138f6 --- /dev/null +++ b/libs/abstract-api-provider/src/types.ts @@ -0,0 +1 @@ +export type WebbProviderType = 'web3' | 'polkadot'; diff --git a/apps/bridge-dapp/src/utils/validateNoteLeafIndex.ts b/libs/abstract-api-provider/src/utils/validateNoteLeafIndex.ts similarity index 79% rename from apps/bridge-dapp/src/utils/validateNoteLeafIndex.ts rename to libs/abstract-api-provider/src/utils/validateNoteLeafIndex.ts index 307e52edde..dc09d72bbe 100644 --- a/apps/bridge-dapp/src/utils/validateNoteLeafIndex.ts +++ b/libs/abstract-api-provider/src/utils/validateNoteLeafIndex.ts @@ -1,5 +1,5 @@ -import { NeighborEdge } from '@webb-tools/abstract-api-provider/vanchor/types'; -import { Note } from '@webb-tools/sdk-core/note'; +import type { Note } from '@webb-tools/sdk-core/note'; +import type { NeighborEdge } from '../vanchor/types'; function validateNoteLeafIndex( note: Note, diff --git a/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts b/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts index 2d4736aee5..ad9765bb9a 100644 --- a/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts +++ b/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts @@ -18,10 +18,8 @@ import { Transaction, TransactionState, } from '../transaction'; -import type { - WebbApiProvider, - WebbProviderType, -} from '../webb-provider.interface'; +import type { WebbApiProvider } from '../webb-provider.interface'; +import { WebbProviderType } from '../types'; import { NeighborEdge } from './types'; export type ParametersOfTransactMethod = @@ -239,4 +237,17 @@ export abstract class VAnchorActions< fungibleId: number, typedChainId?: number ): Promise>; + + /** + * Validate whether all the input notes are valid + * to be spent in the corresponding typed chain id + * @param notes the input notes to validate + * @param typedChainId the typed chain id where the notes are going to be used + * @param fungibleId the fungible id to get the anchor identifier + */ + abstract validateInputNotes( + notes: ReadonlyArray, + typedChainId: number, + fungibleId: number + ): Promise; } diff --git a/libs/abstract-api-provider/src/webb-provider.interface.ts b/libs/abstract-api-provider/src/webb-provider.interface.ts index d625fd7718..2f748c03cf 100644 --- a/libs/abstract-api-provider/src/webb-provider.interface.ts +++ b/libs/abstract-api-provider/src/webb-provider.interface.ts @@ -6,6 +6,7 @@ import { EventBus } from '@webb-tools/app-util'; import { BridgeStorage } from '@webb-tools/browser-utils'; import { VAnchor__factory } from '@webb-tools/contracts'; import { ApiConfig } from '@webb-tools/dapp-config'; +import { type RelayerCMDBase } from '@webb-tools/dapp-config/relayer-config'; import { InteractiveFeedback, Storage } from '@webb-tools/dapp-types'; import { NoteManager } from '@webb-tools/note-manager'; import { Utxo, UtxoGenInput } from '@webb-tools/sdk-core'; @@ -23,6 +24,7 @@ import { ActionEvent, NewNotesTxResult, Transaction } from './transaction'; import { BridgeApi } from './vanchor'; import { VAnchorActions } from './vanchor/vanchor-actions'; import { WrapUnwrap } from './wrap-unwrap'; +import { WebbProviderType } from './types'; export interface RelayChainMethods> { // Crowdloan API @@ -189,8 +191,6 @@ export type NotificationHandler = (( remove(key: string | number): void; }; -export type WebbProviderType = 'web3' | 'polkadot'; - /** * The representation of an api provider * @@ -224,7 +224,7 @@ export interface WebbApiProvider extends EventBus { endSession?(): Promise; - relayerManager: WebbRelayerManager; + relayerManager: WebbRelayerManager; getProvider(): any; diff --git a/libs/api-provider-environment/src/transaction/useTransactionQueue.tsx b/libs/api-provider-environment/src/transaction/useTransactionQueue.tsx index 506f6384a6..2b4708ca59 100644 --- a/libs/api-provider-environment/src/transaction/useTransactionQueue.tsx +++ b/libs/api-provider-environment/src/transaction/useTransactionQueue.tsx @@ -4,8 +4,7 @@ import { TransactionState, TransactionStatusMap, TransactionStatusValue, - WebbProviderType, -} from '@webb-tools/abstract-api-provider'; +} from '@webb-tools/abstract-api-provider/transaction'; import calculateProgressPercentage from '@webb-tools/abstract-api-provider/utils/calculateProgressPercentage'; import { ApiConfig, ChainConfig } from '@webb-tools/dapp-config'; import { ChainIcon } from '@webb-tools/icons'; @@ -18,6 +17,7 @@ import { import { useObservableState } from 'observable-hooks'; import { useCallback, useEffect, useRef, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; +import { getExplorerURI } from './utils'; export function transactionItemStatusFromTxStatus( txStatus: TransactionState @@ -34,26 +34,6 @@ export function transactionItemStatusFromTxStatus( } } -export const getExplorerURI = ( - explorerUri: string, - addOrTxHash: string, - variant: 'tx' | 'address', - txProviderType: WebbProviderType -): URL => { - switch (txProviderType) { - case 'web3': - return new URL(`${variant}/${addOrTxHash}`, explorerUri); - - case 'polkadot': { - const path = variant === 'tx' ? `explorer/query/${addOrTxHash}` : ''; - return new URL(`${path}`, explorerUri); - } - - default: - return new URL(''); - } -}; - function mapTxToPayload( tx: Transaction, chainConfig: Record, diff --git a/libs/api-provider-environment/src/transaction/utils/getExplorerURI.ts b/libs/api-provider-environment/src/transaction/utils/getExplorerURI.ts new file mode 100644 index 0000000000..a1b2708364 --- /dev/null +++ b/libs/api-provider-environment/src/transaction/utils/getExplorerURI.ts @@ -0,0 +1,23 @@ +import { WebbProviderType } from '@webb-tools/abstract-api-provider/types'; + +export const getExplorerURI = ( + explorerUri: string, + addOrTxHash: string, + variant: 'tx' | 'address', + txProviderType: WebbProviderType +): URL => { + switch (txProviderType) { + case 'web3': + return new URL(`${variant}/${addOrTxHash}`, explorerUri); + + case 'polkadot': { + const path = variant === 'tx' ? `explorer/query/${addOrTxHash}` : ''; + return new URL(`${path}`, explorerUri); + } + + default: + return new URL(''); + } +}; + +export default getExplorerURI; diff --git a/libs/api-provider-environment/src/transaction/utils/index.ts b/libs/api-provider-environment/src/transaction/utils/index.ts new file mode 100644 index 0000000000..acc19e6678 --- /dev/null +++ b/libs/api-provider-environment/src/transaction/utils/index.ts @@ -0,0 +1 @@ +export { default as getExplorerURI } from './getExplorerURI'; diff --git a/libs/dapp-config/src/anchors/anchor-config.interface.ts b/libs/dapp-config/src/anchors/anchor-config.interface.ts index 544ee215b2..4ad1ba57de 100644 --- a/libs/dapp-config/src/anchors/anchor-config.interface.ts +++ b/libs/dapp-config/src/anchors/anchor-config.interface.ts @@ -1,6 +1,12 @@ // Copyright 2022 @webb-tools/ // SPDX-License-Identifier: Apache-2.0 -// The ChainAddressConfig maps the TypedChainId to the appropriate address or treeId. +/** + * A record of typed chain id to anchor identifier (address on evm and tree id on substrate) + */ export type ChainAddressConfig = Record; + +/** + * {@inheritdoc ChainAddressConfig} + */ export type AnchorConfigEntry = ChainAddressConfig; diff --git a/libs/dapp-config/src/api-config.ts b/libs/dapp-config/src/api-config.ts index c6bbe48211..ebeb7d7d92 100644 --- a/libs/dapp-config/src/api-config.ts +++ b/libs/dapp-config/src/api-config.ts @@ -43,12 +43,29 @@ export type ApiConfigInput = { export class ApiConfig { constructor( + /** + * id -> wallet config + */ public wallets: Record, + /** + * typed chain id -> chain config + */ public chains: Record, + /** + * id -> currency config + */ public currencies: Record, + /** + * fungible currency id -> bridge config entry + */ public bridgeByAsset: Record, + /** + * fungible currency id -> anchor config entry + */ public anchors: Record, - // fungible currency id -> typed chain id -> wrappable currency ids + /** + * fungible currency id -> typed chain id -> wrappable currency ids + */ public fungibleToWrappableMap: Map>> ) {} diff --git a/libs/dapp-config/src/bridges/bridge-config.interface.ts b/libs/dapp-config/src/bridges/bridge-config.interface.ts index 189fb86e86..c375f22d8c 100644 --- a/libs/dapp-config/src/bridges/bridge-config.interface.ts +++ b/libs/dapp-config/src/bridges/bridge-config.interface.ts @@ -4,6 +4,13 @@ import { AnchorConfigEntry } from '../anchors/anchor-config.interface'; export interface BridgeConfigEntry { + /** + * The fungible currency id + */ asset: number; + + /** + * Map of typed chain id to anchor identifier (address on evm and tree id on substrate) + */ anchors: AnchorConfigEntry; } diff --git a/libs/dapp-config/src/currencies/currency-config.interface.ts b/libs/dapp-config/src/currencies/currency-config.interface.ts index e15020f506..1feaf86811 100644 --- a/libs/dapp-config/src/currencies/currency-config.interface.ts +++ b/libs/dapp-config/src/currencies/currency-config.interface.ts @@ -12,6 +12,9 @@ export interface CurrencyView { } export interface CurrencyConfig extends CurrencyView { + /** + * Map of typed chain id to anchor identifier (address on evm and tree id on substrate) + */ addresses: Map; role: CurrencyRole; } diff --git a/libs/dapp-config/src/wallets/wallet-config.interface.ts b/libs/dapp-config/src/wallets/wallet-config.interface.ts index 960d44a896..bb60d4e64b 100644 --- a/libs/dapp-config/src/wallets/wallet-config.interface.ts +++ b/libs/dapp-config/src/wallets/wallet-config.interface.ts @@ -14,20 +14,34 @@ export interface WalletConfig { title: string; platform: string; - // Homepage url of the wallet + /** + * Homepage url of the wallet + */ homeLink: string; - // Install urls of the wallet in chrome, firefox, etc... + /** + * Install urls of the wallet in chrome, firefox, etc... + */ installLinks?: Record | undefined; - // the wallet isn't live yet + /** + * the wallet isn't live yet + */ enabled: boolean; - /// a function that will tell weather the wallet is installed or reachable + /** + * a function that will tell weather the wallet is installed or reachable + */ detect?(): boolean | Promise; + /** + * a list of supported typed chain ids + */ supportedChainIds: number[]; + /** + * The wagmi connector for EVM wallets + */ connector?: SupportedConnector; } diff --git a/libs/dapp-types/src/WebbError.ts b/libs/dapp-types/src/WebbError.ts index 1d226cc5e8..9e75a8eb05 100644 --- a/libs/dapp-types/src/WebbError.ts +++ b/libs/dapp-types/src/WebbError.ts @@ -73,6 +73,8 @@ export enum WebbErrorCodes { KeyPairNotFound, // Notes are not ready NotesNotReady, + // Invalid amount + InvalidAmount, // Unknown error UnknownError, } @@ -329,6 +331,12 @@ export class WebbError extends Error { 'Some of the notes are not ready, maybe waiting for 5-20 minutes and try again', }; + case WebbErrorCodes.InvalidAmount: + return { + code, + message: 'Invalid amount', + }; + default: return { code, diff --git a/libs/polkadot-api-provider/src/webb-provider/relayer-manager.ts b/libs/polkadot-api-provider/src/webb-provider/relayer-manager.ts index 4d975ae4e3..169929c404 100644 --- a/libs/polkadot-api-provider/src/webb-provider/relayer-manager.ts +++ b/libs/polkadot-api-provider/src/webb-provider/relayer-manager.ts @@ -20,8 +20,12 @@ import { BridgeStorage } from '@webb-tools/browser-utils'; import Storage from '@webb-tools/dapp-types/Storage'; import { ChainType, Note, calculateTypedChainId } from '@webb-tools/sdk-core'; -export class PolkadotRelayerManager extends WebbRelayerManager<'polkadot'> { +export class PolkadotRelayerManager extends WebbRelayerManager< + 'polkadot', + 'substrate' +> { supportedPallet = 'VAnchorBn254'; + cmdKey = 'substrate' as const; mapRelayerIntoActive( relayer: OptionalRelayer, diff --git a/libs/polkadot-api-provider/src/webb-provider/vanchor-actions.ts b/libs/polkadot-api-provider/src/webb-provider/vanchor-actions.ts index 1a1bd9ea9d..39426db5a9 100644 --- a/libs/polkadot-api-provider/src/webb-provider/vanchor-actions.ts +++ b/libs/polkadot-api-provider/src/webb-provider/vanchor-actions.ts @@ -99,6 +99,8 @@ export class PolkadotVAnchorActions extends VAnchorActions< payload: TransactionPayloadType, wrapUnwrapAssetId: string ): Promise> | never { + tx.next(TransactionState.Intermediate, { name: 'Preparing transaction' }); + // If the wrapUnwrapAssetId is empty, we use the bridge fungible token if (!wrapUnwrapAssetId) { const activeBridge = this.inner.state.activeBridge; @@ -429,6 +431,14 @@ export class PolkadotVAnchorActions extends VAnchorActions< throw WebbError.from(WebbErrorCodes.NotImplemented); } + async validateInputNotes( + notes: readonly Note[], + typedChainId: number, + fungibleId: number + ): Promise { + throw WebbError.from(WebbErrorCodes.NotImplemented); + } + // ------------------ Private ------------------ private async prepareDepositTransaction( diff --git a/libs/web3-api-provider/src/webb-provider/relayer-manager.ts b/libs/web3-api-provider/src/webb-provider/relayer-manager.ts index 02fb197032..49025293f1 100644 --- a/libs/web3-api-provider/src/webb-provider/relayer-manager.ts +++ b/libs/web3-api-provider/src/webb-provider/relayer-manager.ts @@ -27,7 +27,9 @@ import { VAnchor__factory } from '@webb-tools/contracts'; import { LOCALNET_CHAIN_IDS } from '@webb-tools/dapp-config'; import { GetContractReturnType, PublicClient } from 'viem'; -export class Web3RelayerManager extends WebbRelayerManager<'web3'> { +export class Web3RelayerManager extends WebbRelayerManager<'web3', 'evm'> { + cmdKey = 'evm' as const; + mapRelayerIntoActive( relayer: OptionalRelayer, typedChainId: number diff --git a/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts b/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts index ef487f2611..442575d7b8 100644 --- a/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts +++ b/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts @@ -16,6 +16,7 @@ import { utxoFromVAnchorNote, VAnchorActions, } from '@webb-tools/abstract-api-provider'; +import validateNoteLeafIndex from '@webb-tools/abstract-api-provider/utils/validateNoteLeafIndex'; import { NeighborEdge } from '@webb-tools/abstract-api-provider/vanchor/types'; import { bridgeStorageFactory, @@ -94,6 +95,8 @@ export class Web3VAnchorActions extends VAnchorActions< payload: TransactionPayloadType, wrapUnwrapToken: string ): Promise> | never { + tx.next(TransactionState.Intermediate, { name: 'Preparing transaction' }); + if (isVAnchorDepositPayload(payload)) { // Get the wrapped token and check the balance and approvals const tokenWrapper = await this.getTokenWrapperContract(payload); @@ -675,6 +678,23 @@ export class Web3VAnchorActions extends VAnchorActions< return new ResourceId(anchorAddress, chainType, chainId); } + async validateInputNotes( + notes: readonly Note[], + typedChainId: number, + fungibleId: number + ): Promise { + const edges = await this.getLatestNeighborEdges(fungibleId, typedChainId); + const nextIdx = await this.getNextIndex(typedChainId, fungibleId); + + return notes.every((note) => { + if (note.note.sourceChainId === typedChainId.toString()) { + return note.note.index ? BigInt(note.note.index) < nextIdx : true; + } else { + return validateNoteLeafIndex(note, edges); + } + }); + } + // ================== PRIVATE METHODS =================== private async fetchNoteLeaves( diff --git a/libs/webb-ui-components/src/components/SideBar/Footer.tsx b/libs/webb-ui-components/src/components/SideBar/Footer.tsx index fe2f4b7fc9..639491bfad 100644 --- a/libs/webb-ui-components/src/components/SideBar/Footer.tsx +++ b/libs/webb-ui-components/src/components/SideBar/Footer.tsx @@ -14,6 +14,7 @@ export const SideBarFooter: FC = ({ href, className, isExpanded, + useNextThemesForThemeToggle, }) => { return (
@@ -56,7 +57,9 @@ export const SideBarFooter: FC = ({ )}
- {isExpanded && } + {isExpanded && ( + + )}
); }; diff --git a/libs/webb-ui-components/src/components/SideBar/SideBar.tsx b/libs/webb-ui-components/src/components/SideBar/SideBar.tsx index ac0fd4b4d5..e33d8c6ec8 100644 --- a/libs/webb-ui-components/src/components/SideBar/SideBar.tsx +++ b/libs/webb-ui-components/src/components/SideBar/SideBar.tsx @@ -48,7 +48,7 @@ export const SideBar = forwardRef( >
@@ -73,6 +73,7 @@ export const SideBar = forwardRef( Icon={footer.Icon} isInternal={footer.isInternal} href={footer.href} + useNextThemesForThemeToggle={footer.useNextThemesForThemeToggle} isExpanded={isSidebarOpen} className={isSidebarOpen ? 'p-2' : 'pl-1'} /> diff --git a/libs/webb-ui-components/src/components/SideBar/SideBarMenu.tsx b/libs/webb-ui-components/src/components/SideBar/SideBarMenu.tsx index eaa90384ff..d9ea673ff2 100644 --- a/libs/webb-ui-components/src/components/SideBar/SideBarMenu.tsx +++ b/libs/webb-ui-components/src/components/SideBar/SideBarMenu.tsx @@ -58,6 +58,7 @@ export const SideBarMenu = forwardRef( Icon={footer.Icon} isInternal={footer.isInternal} href={footer.href} + useNextThemesForThemeToggle={footer.useNextThemesForThemeToggle} isExpanded className="gap-2 p-2" /> diff --git a/libs/webb-ui-components/src/components/SideBar/types.ts b/libs/webb-ui-components/src/components/SideBar/types.ts index 8d8b445078..9702d27047 100644 --- a/libs/webb-ui-components/src/components/SideBar/types.ts +++ b/libs/webb-ui-components/src/components/SideBar/types.ts @@ -7,6 +7,7 @@ export type SideBarFooterType = { isInternal: boolean; href: string; Icon: (props: IconBase) => JSX.Element; + useNextThemesForThemeToggle?: boolean; }; export interface SideBarFooterProps extends SideBarFooterType { diff --git a/libs/webb-ui-components/src/components/Table/Table.tsx b/libs/webb-ui-components/src/components/Table/Table.tsx index c11610fb97..cc88df82a8 100644 --- a/libs/webb-ui-components/src/components/Table/Table.tsx +++ b/libs/webb-ui-components/src/components/Table/Table.tsx @@ -15,9 +15,11 @@ const TableComp = ( totalRecords = 0, tableClassName, thClassName, + trClassName, tdClassName, paginationClassName, title, + onRowClick, ...props }: TableProps, ref: React.ForwardedRef @@ -45,7 +47,14 @@ const TableComp = (
{table.getRowModel().rows.map((row) => ( - + { + e.preventDefault(); + if (onRowClick !== undefined) onRowClick(row); + }} + > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/libs/webb-ui-components/src/components/Table/types.ts b/libs/webb-ui-components/src/components/Table/types.ts index 87fa00e790..393dca5821 100644 --- a/libs/webb-ui-components/src/components/Table/types.ts +++ b/libs/webb-ui-components/src/components/Table/types.ts @@ -1,4 +1,4 @@ -import { RowData, useReactTable } from '@tanstack/react-table'; +import { RowData, useReactTable, Row } from '@tanstack/react-table'; import { PropsOf, WebbComponentBase, IWebbComponentBase } from '../../types'; /** @@ -34,6 +34,11 @@ export interface TableProps extends WebbComponentBase { */ thClassName?: string; + /** + * The optional class name for overriding style table row component + */ + trClassName?: string; + /** * The optional class name for overriding style TData component */ @@ -52,7 +57,7 @@ export interface TableProps extends WebbComponentBase { /** * Handle when the row is clicked */ - onRowClick?: () => void; + onRowClick?: (row: Row) => void; } /** diff --git a/libs/webb-ui-components/src/components/ThemeSwitcher/ThemeSwitcherMenuItem.tsx b/libs/webb-ui-components/src/components/ThemeSwitcher/ThemeSwitcherMenuItem.tsx index 2f3f8ca0bb..ae7cfeb3ca 100644 --- a/libs/webb-ui-components/src/components/ThemeSwitcher/ThemeSwitcherMenuItem.tsx +++ b/libs/webb-ui-components/src/components/ThemeSwitcher/ThemeSwitcherMenuItem.tsx @@ -28,7 +28,7 @@ const ThemeSwitcherMenuItem = (props: ThemeSwitcherMenuItemProps) => { return ( toggleThemeMode(isDarkMode ? 'light' : 'dark')} + onClick={() => toggleThemeMode()} icon={Icon} className={props.className} > diff --git a/libs/webb-ui-components/src/components/ThemeToggle/ThemeToggle.tsx b/libs/webb-ui-components/src/components/ThemeToggle/ThemeToggle.tsx index 6ae58f735b..feb7907f67 100644 --- a/libs/webb-ui-components/src/components/ThemeToggle/ThemeToggle.tsx +++ b/libs/webb-ui-components/src/components/ThemeToggle/ThemeToggle.tsx @@ -1,5 +1,14 @@ +'use client'; + import { MoonLine, SunLine } from '@webb-tools/icons'; -import { useDarkMode } from '../../hooks/useDarkMode'; +import cx from 'classnames'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { + useNextDarkMode, + useDarkMode as useNormalDarkMode, +} from '../../hooks/useDarkMode'; + +import { ThemeToggleProps } from './types'; /** * ThemeToggle (Dark/Light) Component @@ -9,14 +18,42 @@ import { useDarkMode } from '../../hooks/useDarkMode'; * * ``` */ -export const ThemeToggle = () => { + +export const ThemeToggle: FC = ({ + useNextThemes = false, +}) => { + const useDarkMode = useMemo( + () => (useNextThemes ? useNextDarkMode : useNormalDarkMode), + [useNextThemes] + ); + + const [isMounted, setIsMounted] = useState(false); const [isDarkMode, toggleThemeMode] = useDarkMode(); + // useEffect only runs on the client, so now we can safely show the UI + useEffect(() => { + setIsMounted(true); + }, []); + + // If we use next themes, we don't want to show the UI + // until the compoennt is mounted, this prevents + // hydration mismatch + // Check: https://github.com/pacocoursey/next-themes#avoid-hydration-mismatch + if (!isMounted && useNextThemes) { + return null; + } + return (
{ eve.preventDefault(); toggleThemeMode(); @@ -26,7 +63,7 @@ export const ThemeToggle = () => { type="checkbox" name="toggle" id="toggle" - className="hidden toggle-checkbox" + className="hidden" checked={isDarkMode} onChange={(eve) => { eve.stopPropagation(); @@ -35,12 +72,15 @@ export const ThemeToggle = () => { />
diff --git a/libs/webb-ui-components/src/containers/ConfirmationCard/TransferConfirm.tsx b/libs/webb-ui-components/src/containers/ConfirmationCard/TransferConfirm.tsx index 108799b5c3..91a954b2ce 100644 --- a/libs/webb-ui-components/src/containers/ConfirmationCard/TransferConfirm.tsx +++ b/libs/webb-ui-components/src/containers/ConfirmationCard/TransferConfirm.tsx @@ -58,6 +58,7 @@ export const TransferConfirm = forwardRef( relayerExternalUrl, relayerAvatarTheme, sourceChain, + txStatusColor = 'blue', txStatusMessage, title = 'Confirm Transfer', fungibleTokenSymbol: token1Symbol, @@ -148,7 +149,7 @@ export const TransferConfirm = forwardRef( variant="utility" titleClassName="text-mono-200 dark:text-mono-0" /> - {txStatusMessage} + {txStatusMessage} @@ -332,8 +333,7 @@ export const TransferConfirm = forwardRef( @@ -350,7 +350,6 @@ export const TransferConfirm = forwardRef( leftTextProps={{ variant: 'body1', title: 'Change Amount', - info: 'Change Amount', }} rightContent={changeAmountContent} /> diff --git a/libs/webb-ui-components/src/containers/ConfirmationCard/WithdrawConfirm.tsx b/libs/webb-ui-components/src/containers/ConfirmationCard/WithdrawConfirm.tsx index f54eb1694b..813e40c645 100644 --- a/libs/webb-ui-components/src/containers/ConfirmationCard/WithdrawConfirm.tsx +++ b/libs/webb-ui-components/src/containers/ConfirmationCard/WithdrawConfirm.tsx @@ -53,6 +53,7 @@ export const WithdrawConfirm = forwardRef< relayerAddress, relayerAvatarTheme, txStatusMessage, + txStatusColor = 'blue', relayerExternalUrl, remainingAmount, sourceChain, @@ -126,7 +127,7 @@ export const WithdrawConfirm = forwardRef< variant="utility" titleClassName="text-mono-200 dark:text-mono-0" /> - {txStatusMessage} + {txStatusMessage} diff --git a/libs/webb-ui-components/src/containers/ConfirmationCard/types.ts b/libs/webb-ui-components/src/containers/ConfirmationCard/types.ts index 236c31aa8f..3788690cb4 100644 --- a/libs/webb-ui-components/src/containers/ConfirmationCard/types.ts +++ b/libs/webb-ui-components/src/containers/ConfirmationCard/types.ts @@ -1,8 +1,13 @@ -import { PropsOf } from '../../types'; -import { ComponentProps } from 'react'; -import { Avatar, Button, CheckBox, TitleWithInfo } from '../../components'; -import { UseCopyableReturnType } from '../../hooks'; -import { ChainGroup } from '@webb-tools/dapp-config'; +import type { PropsOf } from '../../types'; +import type { ComponentProps } from 'react'; +import type { + Avatar, + Button, + CheckBox, + ChipColors, + TitleWithInfo, +} from '../../components'; +import type { ChainGroup } from '@webb-tools/dapp-config'; export interface ConfirmationCardProps extends PropsOf<'div'> { /** @@ -82,6 +87,12 @@ export interface ConfirmationCardProps extends PropsOf<'div'> { * The transaction status message */ txStatusMessage?: string; + + /** + * The status chip color + * @default 'blue' + */ + txStatusColor?: ChipColors; } export interface DepositConfirmProps extends ConfirmationCardProps { diff --git a/libs/webb-ui-components/src/hooks/useDarkMode.ts b/libs/webb-ui-components/src/hooks/useDarkMode.ts index 6d54c5dbf8..8aaeece1a1 100644 --- a/libs/webb-ui-components/src/hooks/useDarkMode.ts +++ b/libs/webb-ui-components/src/hooks/useDarkMode.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo } from 'react'; +import { useTheme } from 'next-themes'; import useLocalStorageState from 'use-local-storage-state'; type SupportTheme = 'light' | 'dark'; @@ -73,3 +74,21 @@ export function useDarkMode( return [isDarkMode, toggleThemeMode]; } + +export function useNextDarkMode(): [boolean, ToggleThemeModeFunc] { + const { theme, setTheme } = useTheme(); + + const isDarkMode = useMemo(() => theme === 'dark', [theme]); + + const toggleThemeMode = useCallback(() => { + if (!isBrowser()) return; + + const _nextThemeMode = theme === 'dark' ? 'light' : 'dark'; + + if (_nextThemeMode === theme) return; + + setTheme(_nextThemeMode); + }, [theme, setTheme]); + + return [isDarkMode, toggleThemeMode]; +} diff --git a/package.json b/package.json index cacd3c55bb..6086836b00 100644 --- a/package.json +++ b/package.json @@ -112,10 +112,12 @@ "next": "13.3.0", "next-secure-headers": "^2.2.0", "next-seo": "^6.0.0", + "next-themes": "^0.2.1", "notion-client": "^6.15.6", "notion-types": "^6.16.0", "notistack": "^3.0.1", "observable-hooks": "^4.2.1", + "query-string": "^8.1.0", "react": "18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "18.2.0", @@ -134,6 +136,7 @@ "tslib": "^2.3.0", "twitter-api-v2": "^1.14.2", "use-local-storage-state": "^18.3.0", + "use-query-params": "^2.2.1", "viem": "^1.5.3", "wagmi": "^1.3.9" }, @@ -177,7 +180,7 @@ "@storybook/addon-links": "^7.2.3", "@storybook/addon-mdx-gfm": "^7.2.3", "@storybook/addon-styling": "^1.3.4", - "@storybook/core-server": "7.2.3", + "@storybook/core-server": "7.4.1", "@storybook/react": "7.2.3", "@storybook/react-webpack5": "^7.2.3", "@storybook/testing-library": "^0.2.0", @@ -207,11 +210,11 @@ "@webb-tools/test-utils": "0.1.4-126", "@webb-tools/tokens": "1.0.8", "@webb-tools/utils": "0.5.39", - "@webb-tools/vanchor-client": "0.1.23", + "@webb-tools/vanchor-client": "0.1.24", "@webb-tools/wasm-utils": "0.1.4-126", "autoprefixer": "10.4.14", "babel-jest": "29.5.0", - "babel-loader": "9.1.2", + "babel-loader": "9.1.3", "babel-plugin-preval": "^5.1.0", "babel-plugin-transform-class-properties": "^6.24.1", "browserify-zlib": "^0.2.0", diff --git a/yarn.lock b/yarn.lock index e7a28f398a..517c8aa049 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7886,15 +7886,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-manager@7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@storybook/builder-manager/-/builder-manager-7.2.3.tgz#15059d9f035acde703ff67553cc131d441693ae5" - integrity sha512-ywAFjqJ1gHEW6vj52r1syz+PaUc6OLN65IQiWhrhfzYdXdGiIdnWSOQOIh6LSrB6p9/M21/JFtWHCKtaEKXC9w== +"@storybook/builder-manager@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@storybook/builder-manager/-/builder-manager-7.4.0.tgz#80cf72ea83f88e16d585c5bdb40d563874c7d8ca" + integrity sha512-4fuxVzBIBbZh2aVBizSOU5EJ8b74IhR6x2TAZjifZZf5Gdxgfgio8sAyrrd/C78vrFOFhFEgmQhMqZRuCLHxvQ== dependencies: "@fal-works/esbuild-plugin-global-externals" "^2.1.2" - "@storybook/core-common" "7.2.3" - "@storybook/manager" "7.2.3" - "@storybook/node-logger" "7.2.3" + "@storybook/core-common" "7.4.0" + "@storybook/manager" "7.4.0" + "@storybook/node-logger" "7.4.0" "@types/ejs" "^3.1.1" "@types/find-cache-dir" "^3.2.1" "@yarnpkg/esbuild-plugin-pnp" "^3.0.0-rc.10" @@ -7908,15 +7908,15 @@ process "^0.11.10" util "^0.12.4" -"@storybook/builder-manager@7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@storybook/builder-manager/-/builder-manager-7.4.0.tgz#80cf72ea83f88e16d585c5bdb40d563874c7d8ca" - integrity sha512-4fuxVzBIBbZh2aVBizSOU5EJ8b74IhR6x2TAZjifZZf5Gdxgfgio8sAyrrd/C78vrFOFhFEgmQhMqZRuCLHxvQ== +"@storybook/builder-manager@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/builder-manager/-/builder-manager-7.4.1.tgz#5502fb58be25d6e57885064a327f158ffa34d409" + integrity sha512-5zD10jO+vxpbkz9yPdPy0ysRRd+81GmZ1yf12xARREy2hp+KeIIC228QDVA1OAsYcfnqREgCAnQslzhR57739A== dependencies: "@fal-works/esbuild-plugin-global-externals" "^2.1.2" - "@storybook/core-common" "7.4.0" - "@storybook/manager" "7.4.0" - "@storybook/node-logger" "7.4.0" + "@storybook/core-common" "7.4.1" + "@storybook/manager" "7.4.1" + "@storybook/node-logger" "7.4.1" "@types/ejs" "^3.1.1" "@types/find-cache-dir" "^3.2.1" "@yarnpkg/esbuild-plugin-pnp" "^3.0.0-rc.10" @@ -7996,6 +7996,18 @@ telejson "^7.2.0" tiny-invariant "^1.3.1" +"@storybook/channels@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-7.4.1.tgz#20ec7a52db11c1bbcebf9ce209dc391181276026" + integrity sha512-gnE1mNrRF+9oCVRMq6MS/tLXJbYmf9P02PCC3KpMLcSsABdH5jcrACejzJVo/kE223knFH7NJc4BBj7+5h0uXA== + dependencies: + "@storybook/client-logger" "7.4.1" + "@storybook/core-events" "7.4.1" + "@storybook/global" "^5.0.0" + qs "^6.10.0" + telejson "^7.2.0" + tiny-invariant "^1.3.1" + "@storybook/cli@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/cli/-/cli-7.4.0.tgz#a50f435d55e3056547c983c0bfacb2eed63cd692" @@ -8056,6 +8068,13 @@ dependencies: "@storybook/global" "^5.0.0" +"@storybook/client-logger@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-7.4.1.tgz#2973878a201c4d563a831165b90f2b11ffa2e71b" + integrity sha512-2j0DQlKlPNY8XAaEZv+mUYEUm4dOWg6/Q92UNbvYPRK5qbXUvbMiQco5nmvg4LvMT6y99LhRSW2xrwEx5xKAKw== + dependencies: + "@storybook/global" "^5.0.0" + "@storybook/codemod@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-7.4.0.tgz#c23ef80253b5a5998c83e49e74bd6ff62683d27a" @@ -8152,7 +8171,7 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/core-common@7.4.0", "@storybook/core-common@^7.0.12": +"@storybook/core-common@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-7.4.0.tgz#da71afd79a12cfb5565351f184f6797214a5da79" integrity sha512-QKrBL46ZFdfTjlZE3f7b59Q5+frOHWIJ64sC9BZ2PHkZkGjFeYRDdJJ6EHLYBb+nToynl33dYN1GQz+hQn2vww== @@ -8180,38 +8199,74 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/core-common@7.4.1", "@storybook/core-common@^7.0.12": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-7.4.1.tgz#ed36552f477481a0bf345f959a88026869a118c9" + integrity sha512-dvHY515l9yyH3Yki9CuGF/LG85yWDmhjtlbHJ7mrMSreaAgvDs7O5Q2iVh6DXg3oMspQvKlLii/ZLzu+3uxMbg== + dependencies: + "@storybook/core-events" "7.4.1" + "@storybook/node-logger" "7.4.1" + "@storybook/types" "7.4.1" + "@types/find-cache-dir" "^3.2.1" + "@types/node" "^16.0.0" + "@types/node-fetch" "^2.6.4" + "@types/pretty-hrtime" "^1.0.0" + chalk "^4.1.0" + esbuild "^0.18.0" + esbuild-register "^3.4.0" + file-system-cache "2.3.0" + find-cache-dir "^3.0.0" + find-up "^5.0.0" + fs-extra "^11.1.0" + glob "^10.0.0" + handlebars "^4.7.7" + lazy-universal-dotenv "^4.0.0" + node-fetch "^2.0.0" + picomatch "^2.3.0" + pkg-dir "^5.0.0" + pretty-hrtime "^1.0.3" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + "@storybook/core-events@7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.2.3.tgz#ad1badbfb468ca97237370fc7e7a8dc6a5a1922c" integrity sha512-WWpdORiEvOl3/71xFghfEwid7ptgm9U6OxoJm8hU9e5xNuj80k2B+t4sv/iVnz872UuI5xXJqamzCqGVTblPlg== -"@storybook/core-events@7.4.0", "@storybook/core-events@^7.0.12": +"@storybook/core-events@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.4.0.tgz#0d50d254d65a678065d5906ac1dcab64396f2f6a" integrity sha512-JavEo4dw7TQdF5pSKjk4RtqLgsG2R/eWRI8vZ3ANKa0ploGAnQR/eMTfSxf6TUH3ElBWLJhi+lvUCkKXPQD+dw== dependencies: ts-dedent "^2.0.0" -"@storybook/core-server@7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-7.2.3.tgz#0c1cc2a779088c5621e0ae58b5efb55857261af5" - integrity sha512-e+PPbP9XWHmJNBRpbDFRn40lv7QiMTA0jhggp9bPgdBUIcRU1qh4yVP/nsWhKMMGBL4aAZUfK0dllWS0rvhV8g== +"@storybook/core-events@7.4.1", "@storybook/core-events@^7.0.12": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.4.1.tgz#4346db1717495b39404ca14b12e58afa03c07fbd" + integrity sha512-F1tGb32XZ4FRfbtXdi4b+zdzWUjFz5rn3TF18mSuBGGXvxKU+4tywgjGQ3dKGdvuP754czn3poSdz2ZW08bLsQ== + dependencies: + ts-dedent "^2.0.0" + +"@storybook/core-server@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-7.4.0.tgz#9e624789ff30d9538ac014b038c48fac0ebb7272" + integrity sha512-AcbfXatHVx1by4R2CiPIMgjQlOL3sUbVarkhmgUcL0AWT0zC0SCQWUZdo22en+jZhAraazgXyLGNCVP7A+6Tqg== dependencies: "@aw-web-design/x-default-browser" "1.4.126" "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-manager" "7.2.3" - "@storybook/channels" "7.2.3" - "@storybook/core-common" "7.2.3" - "@storybook/core-events" "7.2.3" + "@storybook/builder-manager" "7.4.0" + "@storybook/channels" "7.4.0" + "@storybook/core-common" "7.4.0" + "@storybook/core-events" "7.4.0" "@storybook/csf" "^0.1.0" - "@storybook/csf-tools" "7.2.3" + "@storybook/csf-tools" "7.4.0" "@storybook/docs-mdx" "^0.1.0" "@storybook/global" "^5.0.0" - "@storybook/manager" "7.2.3" - "@storybook/node-logger" "7.2.3" - "@storybook/preview-api" "7.2.3" - "@storybook/telemetry" "7.2.3" - "@storybook/types" "7.2.3" + "@storybook/manager" "7.4.0" + "@storybook/node-logger" "7.4.0" + "@storybook/preview-api" "7.4.0" + "@storybook/telemetry" "7.4.0" + "@storybook/types" "7.4.0" "@types/detect-port" "^1.3.0" "@types/node" "^16.0.0" "@types/pretty-hrtime" "^1.0.0" @@ -8232,7 +8287,7 @@ read-pkg-up "^7.0.1" semver "^7.3.7" serve-favicon "^2.5.0" - telejson "^7.0.3" + telejson "^7.2.0" tiny-invariant "^1.3.1" ts-dedent "^2.0.0" util "^0.12.4" @@ -8240,26 +8295,26 @@ watchpack "^2.2.0" ws "^8.2.3" -"@storybook/core-server@7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-7.4.0.tgz#9e624789ff30d9538ac014b038c48fac0ebb7272" - integrity sha512-AcbfXatHVx1by4R2CiPIMgjQlOL3sUbVarkhmgUcL0AWT0zC0SCQWUZdo22en+jZhAraazgXyLGNCVP7A+6Tqg== +"@storybook/core-server@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-7.4.1.tgz#60cb252bfaf748cebf0486584a116ec488facb7b" + integrity sha512-8JJGci8eyNSfiHJ+Xr46Jv95fqQbjrd+ecQJvpyRqwN1LFdCM6QtHYmjt6LzuK16/by5jYXJ7+f8SA+gvW8SbQ== dependencies: "@aw-web-design/x-default-browser" "1.4.126" "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-manager" "7.4.0" - "@storybook/channels" "7.4.0" - "@storybook/core-common" "7.4.0" - "@storybook/core-events" "7.4.0" + "@storybook/builder-manager" "7.4.1" + "@storybook/channels" "7.4.1" + "@storybook/core-common" "7.4.1" + "@storybook/core-events" "7.4.1" "@storybook/csf" "^0.1.0" - "@storybook/csf-tools" "7.4.0" + "@storybook/csf-tools" "7.4.1" "@storybook/docs-mdx" "^0.1.0" "@storybook/global" "^5.0.0" - "@storybook/manager" "7.4.0" - "@storybook/node-logger" "7.4.0" - "@storybook/preview-api" "7.4.0" - "@storybook/telemetry" "7.4.0" - "@storybook/types" "7.4.0" + "@storybook/manager" "7.4.1" + "@storybook/node-logger" "7.4.1" + "@storybook/preview-api" "7.4.1" + "@storybook/telemetry" "7.4.1" + "@storybook/types" "7.4.1" "@types/detect-port" "^1.3.0" "@types/node" "^16.0.0" "@types/pretty-hrtime" "^1.0.0" @@ -8345,6 +8400,21 @@ recast "^0.23.1" ts-dedent "^2.0.0" +"@storybook/csf-tools@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-7.4.1.tgz#84fa125155db9eb9318b47ffa30601e13322a75e" + integrity sha512-mzzsAtB9CYSgxCvZJ4xQrC7QIhMR5MXGBohADiNhnuRXLdZ6wXBhWkRi/sY7Wh5Uh8DdgHkGPJHJxcyYG+FYQw== + dependencies: + "@babel/generator" "^7.22.9" + "@babel/parser" "^7.22.7" + "@babel/traverse" "^7.22.8" + "@babel/types" "^7.22.5" + "@storybook/csf" "^0.1.0" + "@storybook/types" "7.4.1" + fs-extra "^11.1.0" + recast "^0.23.1" + ts-dedent "^2.0.0" + "@storybook/csf@^0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.1.tgz#95901507dc02f0bc6f9ac8ee1983e2fc5bb98ce6" @@ -8451,16 +8521,16 @@ telejson "^7.2.0" ts-dedent "^2.0.0" -"@storybook/manager@7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@storybook/manager/-/manager-7.2.3.tgz#3176b1c5ec08c0e1e37b1840f741ad9bd488aaab" - integrity sha512-i8HfB00GU7Mlua2HXjUp5phVdcTlHE9iwLJc217oCWnQ5377J7VW4ADMDYNLN/CzfAwwZZSVLCRc1wRG1KblUQ== - "@storybook/manager@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/manager/-/manager-7.4.0.tgz#21a825c9145f56ca6c38d3e9d3546b311a6db14e" integrity sha512-uOSdPBEBKg8WORUZ5HKHb4KnKcTyA5j5Q8MWy/NBaRd22JR3fQkZiKuHer9WJIOQTU+fb6KDmzhZbCTKg5Euog== +"@storybook/manager@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/manager/-/manager-7.4.1.tgz#e0454c2fb9de573361be62fad10dc71feaf2b072" + integrity sha512-LaORUHqfinhKk6Ysz7LyBYqblr/Oj+H5jXeMidSWYor+cJ6AZp1BtCUwWAqtjBliZ8vfASxME1CCImENG11eSA== + "@storybook/mdx2-csf@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@storybook/mdx2-csf/-/mdx2-csf-1.1.0.tgz#97f6df04d0bf616991cc1005a073ac004a7281e5" @@ -8471,11 +8541,16 @@ resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-7.2.3.tgz#42efba67bd4dceba71988e1c1890e8fae8bb1232" integrity sha512-7oUDf3kNtUXn04tMscvUVb9joYT11vPN25OAoCoLVP/qvED1EdDmAaNC2MzBzCatzGmto67aGbY5F4gjC+sY1w== -"@storybook/node-logger@7.4.0", "@storybook/node-logger@^7.0.12": +"@storybook/node-logger@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-7.4.0.tgz#808ed8a63e3bc2f97a2d276b4e8ddaa72b79deb0" integrity sha512-tWSWkYyAvp6SxjIBaTklg29avzv/3Lv4c0dOG2o5tz79PyZkq9v6sQtwLLoI8EJA9Mo8Z08vaJp8NZyDQ9RCuA== +"@storybook/node-logger@7.4.1", "@storybook/node-logger@^7.0.12": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-7.4.1.tgz#92daf9561d1d4a228fc7ea5a18ac0eb64df207fa" + integrity sha512-P7rR/WoHCR2zdDo8bDowIBlB3wRrVNHHIfyWxubbzj/AA2uPv7cpdjDA+NDHAIq8MkuxZqfqhatjrHLFwMHDBg== + "@storybook/postinstall@7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-7.2.3.tgz#d374bc77d873c604e02814204e3fecf4c04073a6" @@ -8528,7 +8603,7 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/preview-api@7.4.0", "@storybook/preview-api@^7.0.12": +"@storybook/preview-api@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.4.0.tgz#46818910545735bef43965651eef380a6f481f4b" integrity sha512-ndXO0Nx+eE7ktVE4EqHpQZ0guX7yYBdruDdJ7B739C0+OoPWsJN7jAzUqq0NXaBcYrdaU5gTy+KnWJUt8R+OyA== @@ -8548,6 +8623,26 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/preview-api@7.4.1", "@storybook/preview-api@^7.0.12": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-7.4.1.tgz#392b8bf0d25266f65772850288efef7b28db3afb" + integrity sha512-swmosWK73lP0CXDKMOwYIaaId28+muPDYX2V/0JmIOA+45HFXimeXZs3XsgVgQMutVF51QqnDA0pfrNgRofHgQ== + dependencies: + "@storybook/channels" "7.4.1" + "@storybook/client-logger" "7.4.1" + "@storybook/core-events" "7.4.1" + "@storybook/csf" "^0.1.0" + "@storybook/global" "^5.0.0" + "@storybook/types" "7.4.1" + "@types/qs" "^6.9.5" + dequal "^2.0.2" + lodash "^4.17.21" + memoizerific "^1.11.3" + qs "^6.10.0" + synchronous-promise "^2.0.15" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/preview@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/preview/-/preview-7.4.0.tgz#a58756ac9b12ea21f203032eca47991946257b53" @@ -8658,20 +8753,6 @@ memoizerific "^1.11.3" qs "^6.10.0" -"@storybook/telemetry@7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-7.2.3.tgz#80ef039f516a722aaf7e52a3401d67fd8270634a" - integrity sha512-cqsLkPgwvvW3oZD5DuXFovfDYJPbqxwH1OI2SzF3lLP1NBQS+ufhp1PinfyrWQ2lTSuf9aFroBbr5GwpLHCwsg== - dependencies: - "@storybook/client-logger" "7.2.3" - "@storybook/core-common" "7.2.3" - "@storybook/csf-tools" "7.2.3" - chalk "^4.1.0" - detect-package-manager "^2.0.1" - fetch-retry "^5.0.2" - fs-extra "^11.1.0" - read-pkg-up "^7.0.1" - "@storybook/telemetry@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-7.4.0.tgz#04e47a2d9decf7671273130a9af9d231a8c3d2e8" @@ -8686,6 +8767,20 @@ fs-extra "^11.1.0" read-pkg-up "^7.0.1" +"@storybook/telemetry@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-7.4.1.tgz#e46ef09f88bf708f18c0a8d6c8f1f51178d13e17" + integrity sha512-53eQPm22Fa7qzjXFSE++bJv5qNG/89rRLU5xywuSYmjQgtaS6HKLPjIRtNPPbU50gRvklVedDDxD8UqN73mD3w== + dependencies: + "@storybook/client-logger" "7.4.1" + "@storybook/core-common" "7.4.1" + "@storybook/csf-tools" "7.4.1" + chalk "^4.1.0" + detect-package-manager "^2.0.1" + fetch-retry "^5.0.2" + fs-extra "^11.1.0" + read-pkg-up "^7.0.1" + "@storybook/testing-library@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@storybook/testing-library/-/testing-library-0.2.0.tgz#09202b90ea5bd67b503dbb1a0b1f3ab3eb005d04" @@ -8725,7 +8820,7 @@ "@types/express" "^4.7.0" file-system-cache "2.3.0" -"@storybook/types@7.4.0", "@storybook/types@^7.0.12": +"@storybook/types@7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@storybook/types/-/types-7.4.0.tgz#71ce550d4d469f6aaf9777fc7432db9fb67f53f9" integrity sha512-XyzYkmeklywxvElPrIWLczi/PWtEdgTL6ToT3++FVxptsC2LZKS3Ue+sBcQ9xRZhkRemw4HQHwed5EW3dO8yUg== @@ -8736,6 +8831,16 @@ "@types/react" "^16.14.34" file-system-cache "2.3.0" +"@storybook/types@7.4.1", "@storybook/types@^7.0.12": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@storybook/types/-/types-7.4.1.tgz#1eb706f2a73e634694c2a6d576cda7160d689f05" + integrity sha512-bjt1YDG9AocFBhIFRvGGbYZPlD223p+qAFcFgYdezU16fFE4ZGFUzUuq2ERkOofL7a2+OzLTCQ/SKe1jFkXCxQ== + dependencies: + "@storybook/channels" "7.4.1" + "@types/babel__core" "^7.0.0" + "@types/express" "^4.7.0" + file-system-cache "2.3.0" + "@substrate/connect-extension-protocol@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@substrate/connect-extension-protocol/-/connect-extension-protocol-1.0.1.tgz#fa5738039586c648013caa6a0c95c43265dbe77d" @@ -10953,10 +11058,10 @@ ethers "5.7.0" snarkjs "^0.6.10" -"@webb-tools/vanchor-client@0.1.23": - version "0.1.23" - resolved "https://registry.yarnpkg.com/@webb-tools/vanchor-client/-/vanchor-client-0.1.23.tgz#6ad33c6a9fd7e0f45fd60b7e6fa1c60c5671a864" - integrity sha512-SemxBMrZ5M/CwZr3di4IprYWJ6opAjjJXWLOTc5Si6dhPhu8o5ZSO8Y9ibhxq6XmP6923VdWXWIX9j97+Usuww== +"@webb-tools/vanchor-client@0.1.24": + version "0.1.24" + resolved "https://registry.yarnpkg.com/@webb-tools/vanchor-client/-/vanchor-client-0.1.24.tgz#08b5cba247afe4f5e16d0deac9fac1426df1227e" + integrity sha512-4Obuw3A2GC4WD77f/19bKPWk1n1HkWyquJFRJwbqnMlg6dBT4uRi9twT1wV+BhVcN3GvtceSd1PX9k913OK5fQ== dependencies: "@graphprotocol/client-cli" "3.0.0" ts-node "10.9.1" @@ -12127,15 +12232,7 @@ babel-jest@^29.4.1, babel-jest@^29.6.4: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@9.1.2: - version "9.1.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" - integrity sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA== - dependencies: - find-cache-dir "^3.3.2" - schema-utils "^4.0.0" - -babel-loader@^9.0.0, babel-loader@^9.1.2: +babel-loader@9.1.3, babel-loader@^9.0.0, babel-loader@^9.1.2: version "9.1.3" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== @@ -14943,6 +15040,11 @@ decode-uri-component@^0.2.0, decode-uri-component@^0.2.2: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== +decode-uri-component@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5" + integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -23433,6 +23535,11 @@ next-seo@^6.0.0: resolved "https://registry.yarnpkg.com/next-seo/-/next-seo-6.1.0.tgz#b60b06958cc77e7ed56f0a61b2d6cd0afed88ebb" integrity sha512-iMBpFoJsR5zWhguHJvsoBDxDSmdYTHtnVPB1ij+CD0NReQCP78ZxxbdL9qkKIf4oEuZEqZkrjAQLB0bkII7RYA== +next-themes@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45" + integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A== + next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -26149,6 +26256,15 @@ query-string@^6.13.5: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" +query-string@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-8.1.0.tgz#e7f95367737219544cd360a11a4f4ca03836e115" + integrity sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw== + dependencies: + decode-uri-component "^0.4.1" + filter-obj "^5.1.0" + split-on-first "^3.0.0" + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -27764,6 +27880,11 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +serialize-query-params@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-2.0.2.tgz#598a3fb9e13f4ea1c1992fbd20231aa16b31db81" + integrity sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q== + serve-favicon@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.0.tgz#935d240cdfe0f5805307fdfe967d88942a2cbcf0" @@ -28302,6 +28423,11 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== +split-on-first@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7" + integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -30336,6 +30462,13 @@ use-local-storage-state@^18.3.0: resolved "https://registry.yarnpkg.com/use-local-storage-state/-/use-local-storage-state-18.3.3.tgz#26be3ff48dcf89b2f332d1684167156cc6123d2a" integrity sha512-SwwW6LPbxf3q5XimJyYE2jBefpvEJTjAgBO47wCs0+ZkL/Hx8heF/0wtBJ7Df0SiSwyfNDIPHo+8Z3q569jlow== +use-query-params@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-2.2.1.tgz#c558ab70706f319112fbccabf6867b9f904e947d" + integrity sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q== + dependencies: + serialize-query-params "^2.0.2" + use-resize-observer@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-9.1.0.tgz#14735235cf3268569c1ea468f8a90c5789fc5c6c"