diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index d6812958a..3f98607a4 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { LsValidatorTable } from '../../components/LiquidStaking/LsValidatorTable'; import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; @@ -8,8 +8,11 @@ import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnst import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; import LsPoolsTable from '../../containers/LsPoolsTable'; +import useNetworkStore from '../../context/useNetworkStore'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; +import useNetworkSwitcher from '../../hooks/useNetworkSwitcher'; import useSearchParamState from '../../hooks/useSearchParamState'; +import getLsTangleNetwork from '../../utils/liquidStaking/getLsTangleNetwork'; import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId'; import TabListItem from '../restake/TabListItem'; import TabsList from '../restake/TabsList'; @@ -28,10 +31,22 @@ const LiquidStakingTokenPage: FC = () => { value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, }); - const { selectedProtocolId } = useLsStore(); + const { selectedProtocolId, selectedNetworkId } = useLsStore(); + const { network } = useNetworkStore(); + const { switchNetwork } = useNetworkSwitcher(); + const lsTangleNetwork = getLsTangleNetwork(selectedNetworkId); const isParachainChain = isLsParachainChainId(selectedProtocolId); + // Sync the network with the selected liquid staking network on load. + // It might differ initially if the user navigates to the page and + // the active network differs from the default liquid staking network. + useEffect(() => { + if (lsTangleNetwork !== null && lsTangleNetwork.id !== network.id) { + switchNetwork(lsTangleNetwork, false); + } + }, [lsTangleNetwork, network.id, selectedNetworkId, switchNetwork]); + return (
diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx index 6ebb815d8..fb1083c54 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx @@ -3,7 +3,7 @@ import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; -import { LsProtocolId, LsToken } from '../../../constants/liquidStaking/types'; +import { LsToken } from '../../../constants/liquidStaking/types'; import { ExchangeRateType } from '../../../data/liquidStaking/useLsExchangeRate'; import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate'; import DetailItem from './DetailItem'; @@ -11,15 +11,13 @@ import DetailItem from './DetailItem'; export type ExchangeRateDetailItemProps = { type: ExchangeRateType; token: LsToken; - protocolId: LsProtocolId; }; const ExchangeRateDetailItem: FC = ({ type, token, - protocolId, }) => { - const { exchangeRate, isRefreshing } = useLsExchangeRate(type, protocolId); + const { exchangeRate, isRefreshing } = useLsExchangeRate(type); const exchangeRateElement = exchangeRate instanceof Error ? ( diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx index 1385ae19a..7e1488ae7 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx @@ -1,6 +1,7 @@ import { BN, BN_ZERO } from '@polkadot/util'; import { FC, useMemo } from 'react'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LsProtocolId } from '../../../constants/liquidStaking/types'; import formatBn from '../../../utils/formatBn'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; @@ -36,6 +37,8 @@ const FeeDetailItem: FC = ({ // Propagate error or loading state. if (!(feeAmount instanceof BN)) { return feeAmount; + } else if (feeAmount.isZero()) { + return EMPTY_VALUE_PLACEHOLDER; } const formattedAmount = formatBn(feeAmount, protocol.decimals, { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index 8e185181c..58d11a69e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -13,17 +13,14 @@ import { twMerge } from 'tailwind-merge'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; -import { - LsNetworkId, - LsProtocolId, -} from '../../../constants/liquidStaking/types'; +import { LsNetworkId } from '../../../constants/liquidStaking/types'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import formatBn from '../../../utils/formatBn'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import useLsAgnosticBalance from './useLsAgnosticBalance'; export type LsAgnosticBalanceProps = { isNative?: boolean; - protocolId: LsProtocolId; tooltip?: string; onlyShowTooltipWhenBalanceIsSet?: boolean; onClick?: () => void; @@ -31,14 +28,14 @@ export type LsAgnosticBalanceProps = { const LsAgnosticBalance: FC = ({ isNative = true, - protocolId, tooltip, onlyShowTooltipWhenBalanceIsSet = true, onClick, }) => { const [isHovering, setIsHovering] = useState(false); - const { balance, isRefreshing } = useLsAgnosticBalance(isNative, protocolId); - const protocol = getLsProtocolDef(protocolId); + const { balance, isRefreshing } = useLsAgnosticBalance(isNative); + const { selectedProtocolId } = useLsStore(); + const protocol = getLsProtocolDef(selectedProtocolId); // Special case for liquid tokens on the `TgToken.sol` contract. // See: https://github.com/webb-tools/tnt-core/blob/1f371959884352e7af68e6091c5bb330fcaa58b8/src/lst/liquidtoken/TgToken.sol#L26 @@ -58,7 +55,6 @@ const LsAgnosticBalance: FC = ({ } const formattedBalance = formatBn(balance, decimals, { - fractionMaxLength: undefined, includeCommas: true, }); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 21e7b9cdf..35437dba8 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -13,7 +13,7 @@ import { Input, Typography, } from '@webb-tools/webb-ui-components'; -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { z } from 'zod'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; @@ -22,11 +22,12 @@ import { LsProtocolId, LsSearchParamKey, } from '../../../constants/liquidStaking/types'; +import useMintTx from '../../../data/liquidStaking/parachain/useMintTx'; +import useLsPoolJoinTx from '../../../data/liquidStaking/tangle/useLsPoolJoinTx'; import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useMintTx from '../../../data/liquidStaking/useMintTx'; import useLiquifierDeposit from '../../../data/liquifier/useLiquifierDeposit'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamState from '../../../hooks/useSearchParamState'; @@ -40,6 +41,7 @@ import LsFeeWarning from './LsFeeWarning'; import LsInput from './LsInput'; import TotalDetailItem from './TotalDetailItem'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; +import useLsChangeNetwork from './useLsChangeNetwork'; import useLsSpendingLimits from './useLsSpendingLimits'; const LsStakeCard: FC = () => { @@ -54,10 +56,15 @@ const LsStakeCard: FC = () => { selectedProtocolId, setSelectedProtocolId, selectedNetworkId, - setSelectedNetworkId, + selectedPoolId, } = useLsStore(); - const { execute: executeMintTx, status: mintTxStatus } = useMintTx(); + const { execute: executeTanglePoolJoinTx, status: tanglePoolJoinTxStatus } = + useLsPoolJoinTx(); + + const { execute: executeParachainMintTx, status: parachainMintTxStatus } = + useMintTx(); + const performLiquifierDeposit = useLiquifierDeposit(); const activeAccountAddress = useActiveAccountAddress(); @@ -67,6 +74,12 @@ const LsStakeCard: FC = () => { ); const selectedProtocol = getLsProtocolDef(selectedProtocolId); + const tryChangeNetwork = useLsChangeNetwork(); + + const isTangleNetwork = + selectedNetworkId === LsNetworkId.TANGLE_LOCAL || + selectedNetworkId === LsNetworkId.TANGLE_MAINNET || + selectedNetworkId === LsNetworkId.TANGLE_TESTNET; // TODO: Not loading the correct protocol for: '?amount=123000000000000000000&protocol=7&network=1&action=stake'. When network=1, it switches to protocol=5 on load. Could this be because the protocol is reset to its default once the network is switched? useSearchParamSync({ @@ -82,16 +95,13 @@ const LsStakeCard: FC = () => { value: selectedNetworkId, parse: (value) => z.nativeEnum(LsNetworkId).parse(parseInt(value)), stringify: (value) => value.toString(), - setValue: setSelectedNetworkId, + setValue: tryChangeNetwork, }); const { exchangeRate: exchangeRateOrError, isRefreshing: isRefreshingExchangeRate, - } = useLsExchangeRate( - ExchangeRateType.NativeToDerivative, - selectedProtocolId, - ); + } = useLsExchangeRate(ExchangeRateType.NativeToDerivative); // TODO: Properly handle the error state. const exchangeRate = @@ -105,9 +115,9 @@ const LsStakeCard: FC = () => { if ( selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeMintTx !== null + executeParachainMintTx !== null ) { - executeMintTx({ + executeParachainMintTx({ amount: fromAmount, currency: selectedProtocol.currency, }); @@ -116,8 +126,25 @@ const LsStakeCard: FC = () => { performLiquifierDeposit !== null ) { await performLiquifierDeposit(selectedProtocol.id, fromAmount); + } else if ( + isTangleNetwork && + executeTanglePoolJoinTx !== null && + selectedPoolId !== null + ) { + executeTanglePoolJoinTx({ + amount: fromAmount, + poolId: selectedPoolId, + }); } - }, [executeMintTx, fromAmount, performLiquifierDeposit, selectedProtocol]); + }, [ + executeParachainMintTx, + executeTanglePoolJoinTx, + fromAmount, + isTangleNetwork, + performLiquifierDeposit, + selectedProtocol, + selectedPoolId, + ]); const toAmount = useMemo(() => { if (fromAmount === null || exchangeRate === null) { @@ -130,13 +157,15 @@ const LsStakeCard: FC = () => { const canCallStake = (fromAmount !== null && selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeMintTx !== null) || + executeParachainMintTx !== null) || (selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierDeposit !== null); + performLiquifierDeposit !== null) || + (isTangleNetwork && + executeTanglePoolJoinTx !== null && + selectedPoolId !== null); const walletBalance = ( { if (maxSpendable !== null) { @@ -146,6 +175,11 @@ const LsStakeCard: FC = () => { /> ); + // Reset the input amount when the network changes. + useEffect(() => { + setFromAmount(null); + }, [setFromAmount, selectedNetworkId]); + return ( <> { setProtocolId={setSelectedProtocolId} minAmount={minSpendable ?? undefined} maxAmount={maxSpendable ?? undefined} - setNetworkId={setSelectedNetworkId} + setNetworkId={tryChangeNetwork} /> @@ -184,7 +218,6 @@ const LsStakeCard: FC = () => { @@ -213,7 +246,10 @@ const LsStakeCard: FC = () => { fromAmount === null || fromAmount.isZero() } - isLoading={mintTxStatus === TxStatus.PROCESSING} + isLoading={ + parachainMintTxStatus === TxStatus.PROCESSING || + tanglePoolJoinTxStatus === TxStatus.PROCESSING + } loadingText="Processing" onClick={handleStakeClick} isFullWidth diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index c6670437f..2afd1a6b2 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -16,11 +16,12 @@ import { LsProtocolId, LsSearchParamKey, } from '../../../constants/liquidStaking/types'; +import useRedeemTx from '../../../data/liquidStaking/parachain/useRedeemTx'; +import useLsPoolUnbondTx from '../../../data/liquidStaking/tangle/useLsPoolUnbondTx'; import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useRedeemTx from '../../../data/liquidStaking/useRedeemTx'; import useLiquifierUnlock from '../../../data/liquifier/useLiquifierUnlock'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamSync from '../../../hooks/useSearchParamSync'; @@ -34,27 +35,22 @@ import LsInput from './LsInput'; import SelectTokenModal from './SelectTokenModal'; import TotalDetailItem from './TotalDetailItem'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; -import UnstakeRequestSubmittedModal from './UnstakeRequestSubmittedModal'; +import useLsChangeNetwork from './useLsChangeNetwork'; import useLsSpendingLimits from './useLsSpendingLimits'; const LsUnstakeCard: FC = () => { const [isSelectTokenModalOpen, setIsSelectTokenModalOpen] = useState(false); const [fromAmount, setFromAmount] = useState(null); const activeAccountAddress = useActiveAccountAddress(); + const tryChangeNetwork = useLsChangeNetwork(); const { selectedProtocolId, setSelectedProtocolId, selectedNetworkId, - setSelectedNetworkId, + selectedPoolId, } = useLsStore(); - const [didLiquifierUnlockSucceed, setDidLiquifierUnlockSucceed] = - useState(false); - - const [isRequestSubmittedModalOpen, setIsRequestSubmittedModalOpen] = - useState(false); - // TODO: Won't both of these hooks be attempting to update the same state? useSearchParamSync({ key: LsSearchParamKey.PROTOCOL_ID, @@ -64,11 +60,11 @@ const LsUnstakeCard: FC = () => { setValue: setSelectedProtocolId, }); - const { - execute: executeRedeemTx, - status: redeemTxStatus, - txHash: redeemTxHash, - } = useRedeemTx(); + const { execute: executeParachainRedeemTx, status: parachainRedeemTxStatus } = + useRedeemTx(); + + const { execute: executeTangleUnbondTx, status: tangleUnbondTxStatus } = + useLsPoolUnbondTx(); const performLiquifierUnlock = useLiquifierUnlock(); @@ -82,10 +78,7 @@ const LsUnstakeCard: FC = () => { const { exchangeRate: exchangeRateOrError, isRefreshing: isRefreshingExchangeRate, - } = useLsExchangeRate( - ExchangeRateType.DerivativeToNative, - selectedProtocol.id, - ); + } = useLsExchangeRate(ExchangeRateType.DerivativeToNative); // TODO: Properly handle the error state. const exchangeRate = @@ -99,6 +92,11 @@ const LsUnstakeCard: FC = () => { stringify: (value) => value?.toString(), }); + const isTangleNetwork = + selectedNetworkId === LsNetworkId.TANGLE_LOCAL || + selectedNetworkId === LsNetworkId.TANGLE_MAINNET || + selectedNetworkId === LsNetworkId.TANGLE_TESTNET; + const handleUnstakeClick = useCallback(async () => { // Cannot perform transaction: Amount not set. if (fromAmount === null) { @@ -107,9 +105,9 @@ const LsUnstakeCard: FC = () => { if ( selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeRedeemTx !== null + executeParachainRedeemTx !== null ) { - executeRedeemTx({ + return executeParachainRedeemTx({ amount: fromAmount, currency: selectedProtocol.currency, }); @@ -117,16 +115,28 @@ const LsUnstakeCard: FC = () => { selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && performLiquifierUnlock !== null ) { - setDidLiquifierUnlockSucceed(false); - - const success = await performLiquifierUnlock( - selectedProtocol.id, - fromAmount, - ); + return performLiquifierUnlock(selectedProtocol.id, fromAmount); + } - setDidLiquifierUnlockSucceed(success); + if ( + isTangleNetwork && + executeTangleUnbondTx !== null && + selectedPoolId !== null + ) { + return executeTangleUnbondTx({ + points: fromAmount, + poolId: selectedPoolId, + }); } - }, [executeRedeemTx, fromAmount, performLiquifierUnlock, selectedProtocol]); + }, [ + executeParachainRedeemTx, + executeTangleUnbondTx, + fromAmount, + isTangleNetwork, + performLiquifierUnlock, + selectedPoolId, + selectedProtocol, + ]); const toAmount = useMemo(() => { if (fromAmount === null || exchangeRate === null) { @@ -145,29 +155,28 @@ const LsUnstakeCard: FC = () => { return [{ address: '0x123456' as any, amount: new BN(100), decimals: 18 }]; }, []); - // Open the request submitted modal when the redeem - // transaction is complete. + // Reset the input amount when the network changes. useEffect(() => { - if (redeemTxStatus === TxStatus.COMPLETE || didLiquifierUnlockSucceed) { - setIsRequestSubmittedModalOpen(true); - } - }, [didLiquifierUnlockSucceed, redeemTxStatus]); + setFromAmount(null); + }, [setFromAmount, selectedNetworkId]); const stakedWalletBalance = ( setFromAmount(maxSpendable)} /> ); - // TODO: Also check if the user has enough balance to unstake. + // TODO: Also check if the user has enough balance to unstake. Convert this into a self-executing function to break down the complexity of a one-liner. const canCallUnstake = (selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeRedeemTx !== null) || + executeParachainRedeemTx !== null) || (selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierUnlock !== null); + performLiquifierUnlock !== null) || + (isTangleNetwork && + executeTangleUnbondTx !== null && + selectedPoolId !== null); return ( <> @@ -175,7 +184,7 @@ const LsUnstakeCard: FC = () => { { @@ -239,7 +247,10 @@ const LsUnstakeCard: FC = () => { fromAmount === null || fromAmount.isZero() } - isLoading={redeemTxStatus === TxStatus.PROCESSING} + isLoading={ + parachainRedeemTxStatus === TxStatus.PROCESSING || + tangleUnbondTxStatus === TxStatus.PROCESSING + } loadingText="Processing" onClick={handleUnstakeClick} isFullWidth @@ -253,12 +264,6 @@ const LsUnstakeCard: FC = () => { onClose={() => setIsSelectTokenModalOpen(false)} onTokenSelect={handleTokenSelect} /> - - setIsRequestSubmittedModalOpen(false)} - txHash={redeemTxHash} - /> ); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx index fa176d2a3..3ff8d4a65 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx @@ -7,11 +7,16 @@ import { DropdownMenuItem, Typography, } from '@webb-tools/webb-ui-components'; +import assert from 'assert'; import { FC } from 'react'; +import { IS_PRODUCTION_ENV } from '../../../constants/env'; import { LS_NETWORKS } from '../../../constants/liquidStaking/constants'; import { LsNetworkId } from '../../../constants/liquidStaking/types'; +import { NETWORK_FEATURE_MAP } from '../../../constants/networks'; +import { NetworkFeature } from '../../../types'; import getLsNetwork from '../../../utils/liquidStaking/getLsNetwork'; +import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; import DropdownChevronIcon from './DropdownChevronIcon'; type NetworkSelectorProps = { @@ -45,6 +50,26 @@ const NetworkSelector: FC = ({
); + // Filter out networks that don't support liquid staking yet. + const supportedLsNetworks = LS_NETWORKS.filter((network) => { + if (network.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { + return true; + } + // Exclude the local Tangle network in production. + else if (network.id === LsNetworkId.TANGLE_LOCAL && IS_PRODUCTION_ENV) { + return false; + } + + // TODO: Obtain the Tangle network from the LS Network's properties instead. + const tangleNetwork = getLsTangleNetwork(network.id); + + assert(tangleNetwork !== null); + + return NETWORK_FEATURE_MAP[tangleNetwork.id].includes( + NetworkFeature.LsPools, + ); + }); + return setNetworkId !== undefined ? ( {base} @@ -52,10 +77,10 @@ const NetworkSelector: FC = ({
    - {LS_NETWORKS.map((network) => { + {supportedLsNetworks.map((network) => { return ( -
  • - setNetworkId(network.type)}> +
  • + setNetworkId(network.id)}>
    diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx index aa27a55b5..d249c58a0 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx @@ -7,7 +7,7 @@ import { Typography, } from '@webb-tools/webb-ui-components'; import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea'; -import { FC } from 'react'; +import { FC, useCallback } from 'react'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; import { @@ -35,19 +35,24 @@ const ProtocolSelector: FC = ({ const protocol = getLsProtocolDef(selectedProtocolId); const network = getLsNetwork(selectedNetworkId); - const trySetProtocolId = (newProtocolId: LsProtocolId) => { - return () => { - if (setProtocolId === undefined) { - return; - } + const trySetProtocolId = useCallback( + (newProtocolId: LsProtocolId) => { + return () => { + if (setProtocolId === undefined) { + return; + } - setProtocolId(newProtocolId); - }; - }; + setProtocolId(newProtocolId); + }; + }, + [setProtocolId], + ); return ( - +
    @@ -56,7 +61,9 @@ const ProtocolSelector: FC = ({ {protocol.token} - {setProtocolId !== undefined && } + {setProtocolId !== undefined && network.protocols.length > 1 && ( + + )}
    diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx index 31041abb7..86a19cff2 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx @@ -30,7 +30,6 @@ const TotalDetailItem: FC = ({ isMinting ? ExchangeRateType.NativeToDerivative : ExchangeRateType.DerivativeToNative, - protocolId, ); const protocol = getLsProtocolDef(protocolId); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts index 42f70963b..b6a017a82 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts @@ -4,11 +4,11 @@ import { erc20Abi } from 'viem'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import LIQUIFIER_TG_TOKEN_ABI from '../../../constants/liquidStaking/liquifierTgTokenAbi'; -import { - LsNetworkId, - LsProtocolId, -} from '../../../constants/liquidStaking/types'; -import useParachainBalances from '../../../data/liquidStaking/useParachainBalances'; +import { LsNetworkId } from '../../../constants/liquidStaking/types'; +import useBalances from '../../../data/balances/useBalances'; +import useParachainBalances from '../../../data/liquidStaking/parachain/useParachainBalances'; +import useLsPoolBalance from '../../../data/liquidStaking/tangle/useLsPoolBalance'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import usePolling from '../../../data/liquidStaking/usePolling'; import useContractReadOnce from '../../../data/liquifier/useContractReadOnce'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; @@ -46,10 +46,13 @@ const createBalanceStateUpdater = ( }; }; -const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { +const useLsAgnosticBalance = (isNative: boolean) => { const activeAccountAddress = useActiveAccountAddress(); const evmAddress20 = useEvmAddress20(); const { nativeBalances, liquidBalances } = useParachainBalances(); + const { free: tangleFreeBalance } = useBalances(); + const { selectedProtocolId, selectedNetworkId } = useLsStore(); + const tangleAssetBalance = useLsPoolBalance(); // TODO: Why not use the subscription hook variants (useContractRead) instead of manually utilizing usePolling? const readErc20 = useContractReadOnce(erc20Abi); @@ -61,7 +64,7 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { const parachainBalances = isNative ? nativeBalances : liquidBalances; const isAccountConnected = activeAccountAddress !== null; - const protocol = getLsProtocolDef(protocolId); + const protocol = getLsProtocolDef(selectedProtocolId); // Reset balance to a placeholder when the active account is // disconnected, and to a loading state once an account is @@ -75,7 +78,7 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { if (isAccountConnected) { setBalance(null); } - }, [isAccountConnected, isNative, protocolId]); + }, [isAccountConnected, isNative, selectedProtocolId]); const erc20BalanceFetcher = useCallback(() => { if ( @@ -115,6 +118,8 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { effect: isAccountConnected ? erc20BalanceFetcher : null, }); + // Update balance to the parachain balance when the restaking + // parachain is the active network. useEffect(() => { if ( protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN || @@ -128,6 +133,38 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { setBalance(createBalanceStateUpdater(newBalance)); }, [parachainBalances, protocol.token, protocol.networkId]); + const isLsTangleNetwork = + selectedNetworkId === LsNetworkId.TANGLE_LOCAL || + selectedNetworkId === LsNetworkId.TANGLE_MAINNET || + selectedNetworkId === LsNetworkId.TANGLE_TESTNET; + + // Update the balance to the Tangle balance when the Tangle + // network is the active network. + useEffect(() => { + if (!isLsTangleNetwork) { + return; + } + // Relevant balance hasn't loaded yet or isn't available. + else if ( + (isNative && tangleFreeBalance === null) || + (!isNative && tangleAssetBalance === null) + ) { + return; + } + + setBalance( + createBalanceStateUpdater( + isNative ? tangleFreeBalance : tangleAssetBalance, + ), + ); + }, [ + protocol.networkId, + tangleFreeBalance, + isLsTangleNetwork, + tangleAssetBalance, + isNative, + ]); + return { balance, isRefreshing }; }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts new file mode 100644 index 000000000..f467cc853 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts @@ -0,0 +1,60 @@ +import { useWebbUI } from '@webb-tools/webb-ui-components'; +import assert from 'assert'; +import { useCallback } from 'react'; + +import { LsNetworkId } from '../../../constants/liquidStaking/types'; +import { NETWORK_FEATURE_MAP } from '../../../constants/networks'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; +import useNetworkSwitcher from '../../../hooks/useNetworkSwitcher'; +import { NetworkFeature } from '../../../types'; +import getLsNetwork from '../../../utils/liquidStaking/getLsNetwork'; +import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; + +const useLsChangeNetwork = () => { + const { selectedNetworkId, setSelectedNetworkId } = useLsStore(); + const { switchNetwork } = useNetworkSwitcher(); + const { notificationApi } = useWebbUI(); + + const tryChangeNetwork = useCallback( + async (newNetworkId: LsNetworkId) => { + // No need to change network if it's already selected. + if (selectedNetworkId === newNetworkId) { + return; + } + + const lsNetwork = getLsNetwork(newNetworkId); + + // Don't check connection to Ethereum mainnet liquifier; + // only verify RPC connection to Tangle networks. + if (lsNetwork.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { + setSelectedNetworkId(newNetworkId); + + return; + } + + const tangleNetwork = getLsTangleNetwork(newNetworkId); + + assert(tangleNetwork !== null); + + const networkFeatures = NETWORK_FEATURE_MAP[tangleNetwork.id]; + + if (!networkFeatures.includes(NetworkFeature.LsPools)) { + notificationApi({ + message: 'Network does not support liquid staking yet', + variant: 'error', + }); + + return; + } + + if (await switchNetwork(tangleNetwork, false)) { + setSelectedNetworkId(newNetworkId); + } + }, + [notificationApi, selectedNetworkId, setSelectedNetworkId, switchNetwork], + ); + + return tryChangeNetwork; +}; + +export default useLsChangeNetwork; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts index 397c3a40e..db26b4d05 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts @@ -6,7 +6,7 @@ import { LsNetworkId, LsProtocolId, } from '../../../constants/liquidStaking/types'; -import useParachainLsFees from '../../../data/liquidStaking/useParachainLsFees'; +import useParachainLsFees from '../../../data/liquidStaking/parachain/useParachainLsFees'; import useContractRead from '../../../data/liquifier/useContractRead'; import { ContractReadOptions } from '../../../data/liquifier/useContractReadOnce'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; @@ -63,9 +63,18 @@ const useLsFeePercentage = ( ? null : Number(rawLiquifierFeeOrError) / 100; - return protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN - ? parachainFee - : liquifierFeePercentageOrError; + switch (protocol.networkId) { + case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: + return parachainFee; + case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: + return liquifierFeePercentageOrError; + // Tangle networks with the `lst` pallet have no fees for + // joining or leaving pools as of now. + case LsNetworkId.TANGLE_LOCAL: + case LsNetworkId.TANGLE_MAINNET: + case LsNetworkId.TANGLE_TESTNET: + return 0; + } }; export default useLsFeePercentage; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts index efe9f2d75..ed3ef1933 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts @@ -20,7 +20,7 @@ const useLsSpendingLimits = ( isNative: boolean, protocolId: LsProtocolId, ): LsSpendingLimits => { - const { balance } = useLsAgnosticBalance(isNative, protocolId); + const { balance } = useLsAgnosticBalance(isNative); const { result: existentialDepositAmount } = useApi( useCallback((api) => api.consts.balances.existentialDeposit, []), diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index b776a3efa..abc81f56c 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -320,18 +320,19 @@ const UnstakeRequestsTable: FC = () => { /> )} + {/* TODO: Assert that the id is either parachain or liquifier, if it isn't then we might need to hide this unstake requests table and show a specific one for Tangle networks (LS pools). */} {isLsParachainChainId(selectedProtocolId) ? ( - ) : ( + ) : isLiquifierProtocolId(selectedProtocolId) ? ( - )} + ) : undefined}
    )} diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx index 2fc775a9c..ddd5675db 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx @@ -16,7 +16,6 @@ import { TooltipTrigger, Typography, } from '@webb-tools/webb-ui-components'; -import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; import { usePathname } from 'next/navigation'; import { type FC, useCallback, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; @@ -99,11 +98,12 @@ const NetworkSelectionButton: FC = () => { // Network can't be switched from the Tangle Restaking Parachain while // on liquid staking page. else if (isInLiquidStakingPath) { - const liquidStakingNetworkName = isLiquifierProtocolId(selectedProtocolId) + // Special case when the liquifier is selected. + const lsNetworkName = isLiquifierProtocolId(selectedProtocolId) ? IS_PRODUCTION_ENV ? 'Ethereum Mainnet' : 'Sepolia Testnet' - : TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.name; + : networkName; const chainIconName = isLiquifierProtocolId(selectedProtocolId) ? 'ethereum' @@ -115,7 +115,7 @@ const NetworkSelectionButton: FC = () => { diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx index 6b12d7748..497ebdc04 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx @@ -30,14 +30,14 @@ export const NetworkSelectorDropdown: FC = ({ }) => { return (
    - {/* Mainnet network */} + {/* Tangle Mainnet */} onNetworkChange(TANGLE_MAINNET_NETWORK)} /> - {/* Testnet network */} + {/* Tangle Testnet */} = ({
    - {/* Local dev network */} + {/* Tangle Local Dev */} ; +export type LsParachainChainId = + | LsProtocolId.POLKADOT + | LsProtocolId.PHALA + | LsProtocolId.MOONBEAM + | LsProtocolId.ASTAR + | LsProtocolId.MANTA; + +export type LsTangleNetworkId = + | LsProtocolId.TANGLE_MAINNET + | LsProtocolId.TANGLE_TESTNET + | LsProtocolId.TANGLE_LOCAL; export enum LsToken { DOT = 'DOT', @@ -39,6 +58,7 @@ export enum LsToken { ASTAR = 'ASTR', PHALA = 'PHALA', TNT = 'TNT', + TTNT = 'tTNT', LINK = 'LINK', GRT = 'GRT', LPT = 'LPT', @@ -51,7 +71,12 @@ export type LsLiquifierProtocolToken = | LsToken.LPT | LsToken.POL; -export type LsParachainToken = Exclude; +export type LsParachainToken = + | LsToken.DOT + | LsToken.GLMR + | LsToken.MANTA + | LsToken.ASTAR + | LsToken.PHALA; type ProtocolDefCommon = { name: string; @@ -62,10 +87,28 @@ type ProtocolDefCommon = { }; export enum LsNetworkId { + TANGLE_LOCAL, + TANGLE_TESTNET, + TANGLE_MAINNET, TANGLE_RESTAKING_PARACHAIN, ETHEREUM_MAINNET_LIQUIFIER, } +export interface LsTangleNetworkDef extends ProtocolDefCommon { + networkId: + | LsNetworkId.TANGLE_MAINNET + | LsNetworkId.TANGLE_TESTNET + | LsNetworkId.TANGLE_LOCAL; + id: LsTangleNetworkId; + token: LsToken.TNT | LsToken.TTNT; + rpcEndpoint: string; + ss58Prefix: + | typeof TANGLE_MAINNET_NETWORK.ss58Prefix + | typeof TANGLE_TESTNET_NATIVE_NETWORK.ss58Prefix + | typeof TANGLE_LOCAL_DEV_NETWORK.ss58Prefix; + tangleNetwork: TangleNetwork; +} + export interface LsParachainChainDef extends ProtocolDefCommon { networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN; @@ -88,7 +131,10 @@ export interface LsLiquifierProtocolDef extends ProtocolDefCommon { unlocksContractAddress: HexString; } -export type LsProtocolDef = LsParachainChainDef | LsLiquifierProtocolDef; +export type LsProtocolDef = + | LsParachainChainDef + | LsLiquifierProtocolDef + | LsTangleNetworkDef; export type LsCardSearchParams = { amount: BN; @@ -132,7 +178,7 @@ export type LsParachainSimpleTimeUnit = { }; export type LsNetwork = { - type: LsNetworkId; + id: LsNetworkId; networkName: string; chainIconFileName: string; defaultProtocolId: LsProtocolId; diff --git a/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx b/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx deleted file mode 100644 index 34db568db..000000000 --- a/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; - -import { Transition } from '@headlessui/react'; -import { BoxLine } from '@webb-tools/icons'; -import { Banner } from '@webb-tools/webb-ui-components/components/Banner'; -import { GITHUB_BUG_REPORT_URL } from '@webb-tools/webb-ui-components/constants'; -import { FC, useCallback, useEffect, useState } from 'react'; - -import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage'; - -const FeedbackBanner: FC = () => { - // Initially, the banner is hidden until the value is - // extracted from local storage. - const [showBanner, setShowBanner] = useState(false); - - const { - isSet: isBannerDismissalCacheSet, - set: setCachedWasBannerDismissed, - valueOpt: wasBannerDismissedOpt, - } = useLocalStorage(LocalStorageKey.WAS_BANNER_DISMISSED); - - // If there is no cache key, show the banner by default. - useEffect(() => { - if (!isBannerDismissalCacheSet()) { - setShowBanner(true); - } - }, [isBannerDismissalCacheSet, setShowBanner]); - - // If the banner was dismissed, do not show it to prevent - // annoying the user. - useEffect(() => { - if (wasBannerDismissedOpt?.value === true) { - setShowBanner(false); - } - }, [wasBannerDismissedOpt?.value]); - - const onCloseHandler = useCallback(() => { - setShowBanner(false); - setCachedWasBannerDismissed(true); - }, [setCachedWasBannerDismissed]); - - return ( - - - - ); -}; - -export default FeedbackBanner; diff --git a/apps/tangle-dapp/containers/Layout/Layout.tsx b/apps/tangle-dapp/containers/Layout/Layout.tsx index 38abe88bd..2b62adebe 100644 --- a/apps/tangle-dapp/containers/Layout/Layout.tsx +++ b/apps/tangle-dapp/containers/Layout/Layout.tsx @@ -21,7 +21,6 @@ import { IS_PRODUCTION_ENV } from '../../constants/env'; import ApiDevStatsContainer from '../DebugMetricsContainer'; import WalletAndChainContainer from '../WalletAndChainContainer/WalletAndChainContainer'; import { WalletModalContainer } from '../WalletModalContainer'; -import FeedbackBanner from './FeedbackBanner'; // Some specific overrides for the social links for use in the // footer in Tangle dApp, since it defaults to the Webb socials. @@ -51,8 +50,6 @@ const Layout: FC> = ({
    - -
    diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index e4c3f4c63..03e54bbdf 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -158,7 +158,7 @@ const DEFAULT_PAGINATION_STATE: PaginationState = { }; const LsPoolsTable: FC = () => { - const { setSelectedParachainPoolId } = useLsStore(); + const { setSelectedPoolId: setSelectedParachainPoolId } = useLsStore(); const [searchQuery, setSearchQuery] = useState(''); const [paginationState, setPaginationState] = useState( @@ -183,7 +183,7 @@ const LsPoolsTable: FC = () => { const selectedRow = selectedRowIds.at(0); assert(selectedRow !== undefined, 'One row must always be selected'); - setSelectedParachainPoolId(selectedRow); + setSelectedParachainPoolId(parseInt(selectedRow, 10)); setRowSelectionState(newSelectionState); }, [rowSelectionState, setSelectedParachainPoolId], diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx new file mode 100644 index 000000000..3dff97eeb --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx @@ -0,0 +1,26 @@ +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; +import { TANGLE_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; + +import { + LsNetworkId, + LsProtocolId, + LsTangleNetworkDef, + LsToken, +} from '../../../constants/liquidStaking/types'; +import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; + +const TANGLE_LOCAL = { + networkId: LsNetworkId.TANGLE_LOCAL, + id: LsProtocolId.TANGLE_LOCAL, + name: 'Tangle Local', + token: LsToken.TTNT, + chainIconFileName: 'tangle', + decimals: TANGLE_TOKEN_DECIMALS, + rpcEndpoint: TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, + timeUnit: CrossChainTimeUnit.POLKADOT_ERA, + unstakingPeriod: 14, + ss58Prefix: TANGLE_LOCAL_DEV_NETWORK.ss58Prefix, + tangleNetwork: TANGLE_LOCAL_DEV_NETWORK, +} as const satisfies LsTangleNetworkDef; + +export default TANGLE_LOCAL; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx new file mode 100644 index 000000000..6e908966a --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx @@ -0,0 +1,26 @@ +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; +import { TANGLE_MAINNET_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; + +import { + LsNetworkId, + LsProtocolId, + LsTangleNetworkDef, + LsToken, +} from '../../../constants/liquidStaking/types'; +import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; + +const TANGLE_MAINNET = { + networkId: LsNetworkId.TANGLE_MAINNET, + id: LsProtocolId.TANGLE_MAINNET, + name: 'Tangle', + token: LsToken.TNT, + chainIconFileName: 'tangle', + decimals: TANGLE_TOKEN_DECIMALS, + rpcEndpoint: TANGLE_MAINNET_NETWORK.wsRpcEndpoint, + timeUnit: CrossChainTimeUnit.POLKADOT_ERA, + unstakingPeriod: 14, + ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, + tangleNetwork: TANGLE_MAINNET_NETWORK, +} as const satisfies LsTangleNetworkDef; + +export default TANGLE_MAINNET; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx new file mode 100644 index 000000000..01fb3e36c --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx @@ -0,0 +1,26 @@ +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; +import { TANGLE_TESTNET_NATIVE_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; + +import { + LsNetworkId, + LsProtocolId, + LsTangleNetworkDef, + LsToken, +} from '../../../constants/liquidStaking/types'; +import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; + +const TANGLE_TESTNET = { + networkId: LsNetworkId.TANGLE_TESTNET, + id: LsProtocolId.TANGLE_TESTNET, + name: 'Tangle', + token: LsToken.TNT, + chainIconFileName: 'tangle', + decimals: TANGLE_TOKEN_DECIMALS, + rpcEndpoint: TANGLE_TESTNET_NATIVE_NETWORK.wsRpcEndpoint, + timeUnit: CrossChainTimeUnit.POLKADOT_ERA, + unstakingPeriod: 14, + ss58Prefix: TANGLE_TESTNET_NATIVE_NETWORK.ss58Prefix, + tangleNetwork: TANGLE_TESTNET_NATIVE_NETWORK, +} as const satisfies LsTangleNetworkDef; + +export default TANGLE_TESTNET; diff --git a/apps/tangle-dapp/data/liquidStaking/useMintTx.ts b/apps/tangle-dapp/data/liquidStaking/parachain/useMintTx.ts similarity index 88% rename from apps/tangle-dapp/data/liquidStaking/useMintTx.ts rename to apps/tangle-dapp/data/liquidStaking/parachain/useMintTx.ts index 8a04cdbf7..a391960c0 100644 --- a/apps/tangle-dapp/data/liquidStaking/useMintTx.ts +++ b/apps/tangle-dapp/data/liquidStaking/parachain/useMintTx.ts @@ -6,12 +6,12 @@ import { Bytes } from '@polkadot/types'; import { BN } from '@polkadot/util'; import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; -import { TxName } from '../../constants'; +import { TxName } from '../../../constants'; import { LsParachainCurrencyKey, ParachainCurrency, -} from '../../constants/liquidStaking/types'; -import { useSubstrateTxWithNotification } from '../../hooks/useSubstrateTx'; +} from '../../../constants/liquidStaking/types'; +import { useSubstrateTxWithNotification } from '../../../hooks/useSubstrateTx'; export type MintTxContext = { amount: BN; diff --git a/apps/tangle-dapp/data/liquidStaking/useParachainBalances.ts b/apps/tangle-dapp/data/liquidStaking/parachain/useParachainBalances.ts similarity index 89% rename from apps/tangle-dapp/data/liquidStaking/useParachainBalances.ts rename to apps/tangle-dapp/data/liquidStaking/parachain/useParachainBalances.ts index 5c4487957..865adf545 100644 --- a/apps/tangle-dapp/data/liquidStaking/useParachainBalances.ts +++ b/apps/tangle-dapp/data/liquidStaking/parachain/useParachainBalances.ts @@ -8,10 +8,10 @@ import { BN } from '@polkadot/util'; import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; import { useCallback, useMemo } from 'react'; -import { LsParachainToken } from '../../constants/liquidStaking/types'; -import useApiRx from '../../hooks/useApiRx'; -import useSubstrateAddress from '../../hooks/useSubstrateAddress'; -import isLsParachainToken from '../../utils/liquidStaking/isLsParachainToken'; +import { LsParachainToken } from '../../../constants/liquidStaking/types'; +import useApiRx from '../../../hooks/useApiRx'; +import useSubstrateAddress from '../../../hooks/useSubstrateAddress'; +import isLsParachainToken from '../../../utils/liquidStaking/isLsParachainToken'; const useParachainBalances = () => { const activeSubstrateAddress = useSubstrateAddress(); diff --git a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts b/apps/tangle-dapp/data/liquidStaking/parachain/useParachainLsFees.ts similarity index 85% rename from apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts rename to apps/tangle-dapp/data/liquidStaking/parachain/useParachainLsFees.ts index b82a6577b..24b0811b4 100644 --- a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts +++ b/apps/tangle-dapp/data/liquidStaking/parachain/useParachainLsFees.ts @@ -2,8 +2,8 @@ import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-u import { useCallback } from 'react'; import { map } from 'rxjs'; -import useApiRx from '../../hooks/useApiRx'; -import permillToPercentage from '../../utils/permillToPercentage'; +import useApiRx from '../../../hooks/useApiRx'; +import permillToPercentage from '../../../utils/permillToPercentage'; const useParachainLsFees = () => { return useApiRx( diff --git a/apps/tangle-dapp/data/liquidStaking/useRedeemTx.ts b/apps/tangle-dapp/data/liquidStaking/parachain/useRedeemTx.ts similarity index 87% rename from apps/tangle-dapp/data/liquidStaking/useRedeemTx.ts rename to apps/tangle-dapp/data/liquidStaking/parachain/useRedeemTx.ts index 9cfa6758f..8dbc610e6 100644 --- a/apps/tangle-dapp/data/liquidStaking/useRedeemTx.ts +++ b/apps/tangle-dapp/data/liquidStaking/parachain/useRedeemTx.ts @@ -5,12 +5,12 @@ import '@webb-tools/tangle-restaking-types'; import { BN } from '@polkadot/util'; import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; -import { TxName } from '../../constants'; +import { TxName } from '../../../constants'; import { LsParachainCurrencyKey, ParachainCurrency, -} from '../../constants/liquidStaking/types'; -import { useSubstrateTxWithNotification } from '../../hooks/useSubstrateTx'; +} from '../../../constants/liquidStaking/types'; +import { useSubstrateTxWithNotification } from '../../../hooks/useSubstrateTx'; export type RedeemTxContext = { amount: BN; diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts new file mode 100644 index 000000000..14a8f3c28 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts @@ -0,0 +1,49 @@ +import { BN_ZERO } from '@polkadot/util'; +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../../hooks/useApiRx'; +import useNetworkFeatures from '../../../hooks/useNetworkFeatures'; +import useSubstrateAddress from '../../../hooks/useSubstrateAddress'; +import { NetworkFeature } from '../../../types'; +import { useLsStore } from '../useLsStore'; + +const useLsPoolBalance = () => { + const substrateAddress = useSubstrateAddress(); + const networkFeatures = useNetworkFeatures(); + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); + const { selectedPoolId } = useLsStore(); + + const { result: tanglePoolAssetAccountOpt } = useApiRx( + useCallback( + (api) => { + // The liquid staking pools functionality isn't available on the active + // network, the user hasn't selected a pool yet, or there is no active + // account. + if ( + !isSupported || + selectedPoolId === null || + substrateAddress === null + ) { + return null; + } + + return api.query.assets.account(selectedPoolId, substrateAddress); + }, + [isSupported, selectedPoolId, substrateAddress], + ), + ); + + const derivativeBalanceOpt = useMemo(() => { + if (tanglePoolAssetAccountOpt === null) { + return null; + } else if (tanglePoolAssetAccountOpt.isNone) { + return BN_ZERO; + } + + return tanglePoolAssetAccountOpt.unwrap().balance.toBn(); + }, [tanglePoolAssetAccountOpt]); + + return derivativeBalanceOpt; +}; + +export default useLsPoolBalance; diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolJoinTx.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolJoinTx.ts new file mode 100644 index 000000000..5f7eb0bda --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolJoinTx.ts @@ -0,0 +1,28 @@ +import { BN } from '@polkadot/util'; +import { useCallback } from 'react'; + +import { TxName } from '../../../constants'; +import { + SubstrateTxFactory, + useSubstrateTxWithNotification, +} from '../../../hooks/useSubstrateTx'; + +export type LsPoolJoinTxContext = { + poolId: number; + amount: BN; +}; + +const useLsPoolJoinTx = () => { + const substrateTxFactory: SubstrateTxFactory = + useCallback(async (api, _activeSubstrateAddress, { poolId, amount }) => { + return api.tx.lst.join(amount, poolId); + }, []); + + // TODO: Add EVM support once precompile(s) for the `lst` pallet are implemented on Tangle. + return useSubstrateTxWithNotification( + TxName.LS_TANGLE_POOL_JOIN, + substrateTxFactory, + ); +}; + +export default useLsPoolJoinTx; diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolUnbondTx.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolUnbondTx.ts new file mode 100644 index 000000000..b72bd79a1 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolUnbondTx.ts @@ -0,0 +1,28 @@ +import { BN } from '@polkadot/util'; +import { useCallback } from 'react'; + +import { TxName } from '../../../constants'; +import { + SubstrateTxFactory, + useSubstrateTxWithNotification, +} from '../../../hooks/useSubstrateTx'; + +export type LsPoolUnbondTxContext = { + poolId: number; + points: BN; +}; + +const useLsPoolUnbondTx = () => { + const substrateTxFactory: SubstrateTxFactory = + useCallback(async (api, activeSubstrateAddress, { poolId, points }) => { + return api.tx.lst.unbond({ Id: activeSubstrateAddress }, poolId, points); + }, []); + + // TODO: Add EVM support once precompile(s) for the `lst` pallet are implemented on Tangle. + return useSubstrateTxWithNotification( + TxName.LS_TANGLE_POOL_UNBOND, + substrateTxFactory, + ); +}; + +export default useLsPoolUnbondTx; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts index 334434f8a..9df9b0a57 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts @@ -8,13 +8,13 @@ import LIQUIFIER_TG_TOKEN_ABI from '../../constants/liquidStaking/liquifierTgTok import { LsNetworkId, LsParachainCurrencyKey, - LsProtocolId, } from '../../constants/liquidStaking/types'; import useApiRx from '../../hooks/useApiRx'; import calculateBnRatio from '../../utils/calculateBnRatio'; import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef'; import useContractRead from '../liquifier/useContractRead'; import { ContractReadOptions } from '../liquifier/useContractReadOnce'; +import { useLsStore } from './useLsStore'; import usePolling from './usePolling'; export enum ExchangeRateType { @@ -47,13 +47,11 @@ const computeExchangeRate = ( const MAX_BN_OPERATION_NUMBER = 2 ** 26 - 1; -const useLsExchangeRate = ( - type: ExchangeRateType, - protocolId: LsProtocolId, -) => { +const useLsExchangeRate = (type: ExchangeRateType) => { const [exchangeRate, setExchangeRate] = useState(null); + const { selectedProtocolId, selectedNetworkId } = useLsStore(); - const protocol = getLsProtocolDef(protocolId); + const protocol = getLsProtocolDef(selectedProtocolId); const { result: tokenPoolAmount } = useApiRx((api) => { if (protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN) { @@ -144,10 +142,24 @@ const useLsExchangeRate = ( }, [liquifierTotalShares, tgTokenTotalSupply, type]); const fetch = useCallback(async () => { - const promise = - protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN - ? parachainExchangeRate - : fetchLiquifierExchangeRate(); + let promise: Promise; + + switch (selectedNetworkId) { + case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: + promise = fetchLiquifierExchangeRate(); + + break; + case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: + promise = parachainExchangeRate; + + break; + // Tangle networks with the `lst` pallet have a fixed exchange + // rate of 1:1. + case LsNetworkId.TANGLE_LOCAL: + case LsNetworkId.TANGLE_MAINNET: + case LsNetworkId.TANGLE_TESTNET: + promise = Promise.resolve(1); + } const newExchangeRate = await promise; @@ -158,7 +170,7 @@ const useLsExchangeRate = ( } setExchangeRate(newExchangeRate); - }, [fetchLiquifierExchangeRate, parachainExchangeRate, protocol]); + }, [fetchLiquifierExchangeRate, parachainExchangeRate, selectedNetworkId]); // Pause or resume ERC20-based exchange rate fetching based // on whether the requested protocol is a parachain or an ERC20 token. diff --git a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts index 1bddab5a3..fa02539dc 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts @@ -74,6 +74,9 @@ const getDataType = (chain: LsProtocolId): LiquidStakingItem | null => { case LsProtocolId.LIVEPEER: case LsProtocolId.POLYGON: case LsProtocolId.THE_GRAPH: + case LsProtocolId.TANGLE_MAINNET: + case LsProtocolId.TANGLE_TESTNET: + case LsProtocolId.TANGLE_LOCAL: return null; } }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts index 7c6a97b74..3e7d6d279 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts @@ -7,13 +7,13 @@ type State = { selectedNetworkId: LsNetworkId; selectedProtocolId: LsProtocolId; selectedNetworkEntities: Set; - selectedParachainPoolId: string | null; + selectedPoolId: number | null; }; type Actions = { setSelectedProtocolId: (newProtocolId: State['selectedProtocolId']) => void; setSelectedNetworkId: (newNetworkId: State['selectedNetworkId']) => void; - setSelectedParachainPoolId: (parachainPoolId: string) => void; + setSelectedPoolId: (poolId: number) => void; setSelectedNetworkEntities: ( selectedNetworkEntities: State['selectedNetworkEntities'], @@ -23,14 +23,14 @@ type Actions = { type Store = State & Actions; export const useLsStore = create((set) => ({ - selectedParachainPoolId: null, - selectedNetworkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN, - selectedProtocolId: LsProtocolId.POLKADOT, + selectedPoolId: null, selectedNetworkEntities: new Set(), - setSelectedParachainPoolId: (selectedParachainPoolId) => - set({ selectedParachainPoolId }), - setSelectedProtocolId: (selectedChainId) => - set({ selectedProtocolId: selectedChainId }), + // Default the selected network and protocol to the Tangle testnet, + // and tTNT, until liquid staking pools are deployed to mainnet. + selectedNetworkId: LsNetworkId.TANGLE_TESTNET, + selectedProtocolId: LsProtocolId.TANGLE_TESTNET, + setSelectedPoolId: (selectedPoolId) => set({ selectedPoolId }), + setSelectedProtocolId: (selectedProtocolId) => set({ selectedProtocolId }), setSelectedNetworkEntities: (selectedNetworkEntities) => set({ selectedNetworkEntities }), setSelectedNetworkId: (selectedNetworkId) => { diff --git a/apps/tangle-dapp/hooks/useApiRx.ts b/apps/tangle-dapp/hooks/useApiRx.ts index 45e66171a..dd2ecd3ff 100644 --- a/apps/tangle-dapp/hooks/useApiRx.ts +++ b/apps/tangle-dapp/hooks/useApiRx.ts @@ -32,7 +32,7 @@ export type ObservableFactory = (api: ApiRx) => Observable | null; */ function useApiRx( factory: ObservableFactory, - overrideRpcEndpoint?: string, + rpcEndpointOverride?: string, ) { const [result, setResult] = useState(null); const [isLoading, setLoading] = useState(true); @@ -43,8 +43,8 @@ function useApiRx( const { result: apiRx } = usePromise( useCallback( - () => getApiRx(overrideRpcEndpoint ?? rpcEndpoint), - [overrideRpcEndpoint, rpcEndpoint], + () => getApiRx(rpcEndpointOverride ?? rpcEndpoint), + [rpcEndpointOverride, rpcEndpoint], ), null, ); diff --git a/apps/tangle-dapp/hooks/useLocalStorage.ts b/apps/tangle-dapp/hooks/useLocalStorage.ts index ae4b9e110..25cc0c9fd 100644 --- a/apps/tangle-dapp/hooks/useLocalStorage.ts +++ b/apps/tangle-dapp/hooks/useLocalStorage.ts @@ -22,7 +22,6 @@ export enum LocalStorageKey { PAYOUTS = 'payouts', CUSTOM_RPC_ENDPOINT = 'customRpcEndpoint', KNOWN_NETWORK_ID = 'knownNetworkId', - WAS_BANNER_DISMISSED = 'wasBannerDismissed', SERVICES_CACHE = 'servicesCache', SUBSTRATE_WALLETS_METADATA = 'substrateWalletsMetadata', BRIDGE_TX_QUEUE_BY_ACC = 'bridgeTxQueue', @@ -72,15 +71,13 @@ export type LocalStorageValueOf = ? string : T extends LocalStorageKey.KNOWN_NETWORK_ID ? number - : T extends LocalStorageKey.WAS_BANNER_DISMISSED - ? boolean - : T extends LocalStorageKey.SUBSTRATE_WALLETS_METADATA - ? SubstrateWalletsMetadataCache - : T extends LocalStorageKey.BRIDGE_TX_QUEUE_BY_ACC - ? TxQueueByAccount - : T extends LocalStorageKey.LIQUID_STAKING_TABLE_DATA - ? LiquidStakingTableData - : never; + : T extends LocalStorageKey.SUBSTRATE_WALLETS_METADATA + ? SubstrateWalletsMetadataCache + : T extends LocalStorageKey.BRIDGE_TX_QUEUE_BY_ACC + ? TxQueueByAccount + : T extends LocalStorageKey.LIQUID_STAKING_TABLE_DATA + ? LiquidStakingTableData + : never; export const getJsonFromLocalStorage = ( key: Key, diff --git a/apps/tangle-dapp/hooks/useNetworkSwitcher.ts b/apps/tangle-dapp/hooks/useNetworkSwitcher.ts index 9d79e26af..8d0516081 100644 --- a/apps/tangle-dapp/hooks/useNetworkSwitcher.ts +++ b/apps/tangle-dapp/hooks/useNetworkSwitcher.ts @@ -75,7 +75,7 @@ const useNetworkSwitcher = () => { async (newNetwork: Network, isCustom: boolean) => { // Already on the requested network. if (network.id === newNetwork.id) { - return; + return true; } // Test connection to the new network. else if (!(await testRpcEndpointConnection(newNetwork.wsRpcEndpoint))) { @@ -84,7 +84,7 @@ const useNetworkSwitcher = () => { message: `Unable to connect to the requested network: ${newNetwork.wsRpcEndpoint}`, }); - return; + return false; } if (activeWallet !== undefined) { @@ -122,6 +122,8 @@ const useNetworkSwitcher = () => { setIsCustom(isCustom); setNetwork(newNetwork); + + return true; }, [ activeWallet, diff --git a/apps/tangle-dapp/hooks/useSearchParamSync.ts b/apps/tangle-dapp/hooks/useSearchParamSync.ts index 307ac80a3..413d3e725 100644 --- a/apps/tangle-dapp/hooks/useSearchParamSync.ts +++ b/apps/tangle-dapp/hooks/useSearchParamSync.ts @@ -11,15 +11,16 @@ export type UseSearchParamSyncOptions = { setValue: (value: T) => unknown; }; -const createHref = (newSearchParams: ReadonlyURLSearchParams): string => { +const createHref = (newSearchParams?: ReadonlyURLSearchParams): string => { const newUrl = new URL(window.location.href); - for (const [key] of newUrl.searchParams) { - newUrl.searchParams.delete(key); - } + // Remove existing search params. + newUrl.search = ''; - for (const [key, value] of newSearchParams) { - newUrl.searchParams.set(key, value); + if (newSearchParams !== undefined) { + for (const [key, value] of newSearchParams) { + newUrl.searchParams.set(key, value); + } } return newUrl.toString(); @@ -114,7 +115,7 @@ const useSearchParamSync = ({ } const newSearchParams = updateSearchParam(key, stringifiedValue); - const href = createHref(new ReadonlyURLSearchParams(newSearchParams)); + const href = createHref(newSearchParams); console.debug('Syncing URL search param', key, stringifiedValue, href); router.push(href); diff --git a/apps/tangle-dapp/hooks/useSubstrateTx.ts b/apps/tangle-dapp/hooks/useSubstrateTx.ts index 7e4a30b79..184478a43 100644 --- a/apps/tangle-dapp/hooks/useSubstrateTx.ts +++ b/apps/tangle-dapp/hooks/useSubstrateTx.ts @@ -232,8 +232,7 @@ export function useSubstrateTxWithNotification( overrideRpcEndpoint, ); - const { notifyProcessing, notifySuccess, notifyError } = - useTxNotification(txName); + const { notifyProcessing, notifySuccess, notifyError } = useTxNotification(); const execute = useCallback( (context: Context) => { diff --git a/apps/tangle-dapp/hooks/useTxNotification.tsx b/apps/tangle-dapp/hooks/useTxNotification.tsx index 109654034..f4b5e522a 100644 --- a/apps/tangle-dapp/hooks/useTxNotification.tsx +++ b/apps/tangle-dapp/hooks/useTxNotification.tsx @@ -36,6 +36,8 @@ const SUCCESS_MESSAGES: Record = { [TxName.LS_LIQUIFIER_APPROVE]: 'Liquifier approval successful', [TxName.LS_LIQUIFIER_UNLOCK]: 'Liquifier unlock successful', [TxName.LS_LIQUIFIER_WITHDRAW]: 'Liquifier withdrawal successful', + [TxName.LS_TANGLE_POOL_JOIN]: 'Joined liquid staking pool', + [TxName.LS_TANGLE_POOL_UNBOND]: 'Unbonded from liquid staking pool', }; const makeKey = (txName: TxName): `${TxName}-tx-notification` => diff --git a/apps/tangle-dapp/utils/Optional.ts b/apps/tangle-dapp/utils/Optional.ts index ac952ecb4..00ebd86ef 100644 --- a/apps/tangle-dapp/utils/Optional.ts +++ b/apps/tangle-dapp/utils/Optional.ts @@ -19,9 +19,9 @@ * * @example * ``` - * const value = new Optional(42); + * const valueOpt = Optional.new(42); * - * if (value.value !== null) { + * if (valueOpt.value !== null) { * console.log(value.value); * } else { * console.log('Value is not present!'); @@ -29,6 +29,14 @@ * ``` */ class Optional> { + static empty>(): Optional { + return new Optional(); + } + + static new>(value: T): Optional { + return new Optional(value); + } + readonly value: T | null; constructor(value?: T) { @@ -37,10 +45,18 @@ class Optional> { map>(f: (value: T) => U): Optional { if (this.value === null) { - return new Optional(); + return Optional.empty(); + } + + return Optional.new(f(this.value)); + } + + unwrapOrThrow(): T { + if (this.value === null) { + throw new Error('Value is not present!'); } - return new Optional(f(this.value)); + return this.value; } get isPresent(): boolean { diff --git a/apps/tangle-dapp/utils/formatBn.ts b/apps/tangle-dapp/utils/formatBn.ts index c1294f9ab..a70e40386 100644 --- a/apps/tangle-dapp/utils/formatBn.ts +++ b/apps/tangle-dapp/utils/formatBn.ts @@ -29,7 +29,7 @@ const DEFAULT_FORMAT_OPTIONS: FormatOptions = { trimTrailingZeroes: true, }; -// TODO: Break this function down into smaller local functions for improved legibility and modularity, since its logic is getting complex. Consider making it functional instead of modifying the various variables: Return {integerPart, fractionalPart} per transformation/function, so that it can be easily chainable monad-style. +// TODO: Break this function down into smaller local functions for improved legibility and modularity, since its logic is getting complex. Consider making it functional instead of modifying the various variables: Return {integerPart, fractionalPart} per transformation/function, so that it can be easily chainable monad-style. Also, prefer usage of Decimal.js instead of BN for better decimal handling without needing to manually handle the edge cases. function formatBn( amount: BN, decimals: number, diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts index 811ebc96a..9969d0fe3 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts @@ -1,6 +1,9 @@ import { LS_ETHEREUM_MAINNET_LIQUIFIER, + LS_TANGLE_LOCAL, + LS_TANGLE_MAINNET, LS_TANGLE_RESTAKING_PARACHAIN, + LS_TANGLE_TESTNET, } from '../../constants/liquidStaking/constants'; import { LsNetwork, LsNetworkId } from '../../constants/liquidStaking/types'; @@ -10,6 +13,12 @@ const getLsNetwork = (networkId: LsNetworkId): LsNetwork => { return LS_ETHEREUM_MAINNET_LIQUIFIER; case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: return LS_TANGLE_RESTAKING_PARACHAIN; + case LsNetworkId.TANGLE_MAINNET: + return LS_TANGLE_MAINNET; + case LsNetworkId.TANGLE_TESTNET: + return LS_TANGLE_TESTNET; + case LsNetworkId.TANGLE_LOCAL: + return LS_TANGLE_LOCAL; } }; diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts index 685d7f852..a47aade40 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts @@ -1,6 +1,11 @@ import assert from 'assert'; import { LS_PROTOCOLS } from '../../constants/liquidStaking/constants'; +import { + LsLiquifierProtocolId, + LsTangleNetworkDef, + LsTangleNetworkId, +} from '../../constants/liquidStaking/types'; import { LsLiquifierProtocolDef, LsParachainChainDef, @@ -10,7 +15,11 @@ import { type IdToDefMap = T extends LsParachainChainId ? LsParachainChainDef - : LsLiquifierProtocolDef; + : T extends LsTangleNetworkId + ? LsTangleNetworkDef + : T extends LsLiquifierProtocolId + ? LsLiquifierProtocolDef + : never; const getLsProtocolDef = (id: T): IdToDefMap => { const result = LS_PROTOCOLS.find((def) => def.id === id); diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts new file mode 100644 index 000000000..6d0924c7b --- /dev/null +++ b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts @@ -0,0 +1,31 @@ +import { + Network, + TANGLE_LOCAL_DEV_NETWORK, + TANGLE_MAINNET_NETWORK, + TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK, + TANGLE_RESTAKING_PARACHAIN_TESTNET_NETWORK, + TANGLE_TESTNET_NATIVE_NETWORK, +} from '@webb-tools/webb-ui-components/constants/networks'; + +import { IS_PRODUCTION_ENV } from '../../constants/env'; +import { LsNetworkId } from '../../constants/liquidStaking/types'; + +// TODO: Obtain the Tangle network directly from the adapter's `tangleNetwork` property instead of using this helper method. +const getLsTangleNetwork = (networkId: LsNetworkId): Network | null => { + switch (networkId) { + case LsNetworkId.TANGLE_MAINNET: + return TANGLE_MAINNET_NETWORK; + case LsNetworkId.TANGLE_TESTNET: + return TANGLE_TESTNET_NATIVE_NETWORK; + case LsNetworkId.TANGLE_LOCAL: + return TANGLE_LOCAL_DEV_NETWORK; + case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: + return IS_PRODUCTION_ENV + ? TANGLE_RESTAKING_PARACHAIN_TESTNET_NETWORK + : TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK; + case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: + return null; + } +}; + +export default getLsTangleNetwork; diff --git a/libs/webb-ui-components/src/constants/networks.ts b/libs/webb-ui-components/src/constants/networks.ts index b6f7db91c..9781dafa2 100644 --- a/libs/webb-ui-components/src/constants/networks.ts +++ b/libs/webb-ui-components/src/constants/networks.ts @@ -99,7 +99,7 @@ export const TANGLE_LOCAL_DEV_NETWORK = { id: NetworkId.TANGLE_LOCAL_DEV, substrateChainId: SubstrateChainId.TangleLocalNative, evmChainId: EVMChainId.TangleLocalEVM, - name: 'Local endpoint', + name: 'Tangle Local Dev', tokenSymbol: TANGLE_TESTNET_NATIVE_TOKEN_SYMBOL, nodeType: 'standalone', subqueryEndpoint: 'http://localhost:4000/graphql',