diff --git a/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx b/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx index 7f3bb7510..054fcfdec 100644 --- a/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx +++ b/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx @@ -15,7 +15,7 @@ import Decimal from 'decimal.js'; import { FC, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; -import AmountInput from '../../components/AmountInput/AmountInput'; +import AmountInput from '../../components/AmountInput'; import { BRIDGE_SUPPORTED_TOKENS } from '../../constants/bridge'; import { useBridge } from '../../context/BridgeContext'; import convertDecimalToBn from '../../utils/convertDecimalToBn'; @@ -68,7 +68,7 @@ const AmountAndTokenInput: FC = () => { title="Amount" amount={amount} setAmount={setAmount} - baseInputOverrides={{ + wrapperOverrides={{ isFullWidth: true, }} placeholder="" diff --git a/apps/tangle-dapp/app/bridge/BridgeContainer.tsx b/apps/tangle-dapp/app/bridge/BridgeContainer.tsx index 4c5845f4e..05438d80c 100644 --- a/apps/tangle-dapp/app/bridge/BridgeContainer.tsx +++ b/apps/tangle-dapp/app/bridge/BridgeContainer.tsx @@ -4,9 +4,7 @@ import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; import { FC, useMemo, useState } from 'react'; import { twMerge } from 'tailwind-merge'; -import AddressInput, { - AddressType, -} from '../../components/AddressInput/AddressInput'; +import AddressInput, { AddressType } from '../../components/AddressInput'; import { useBridge } from '../../context/BridgeContext'; import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; import { isEVMChain } from '../../utils/bridge'; @@ -84,7 +82,7 @@ const BridgeContainer: FC = ({ className }) => { : AddressType.Substrate } title="Receiver Address" - baseInputOverrides={{ isFullWidth: true }} + wrapperOverrides={{ isFullWidth: true }} value={destinationAddress} setValue={setDestinationAddress} setErrorMessage={(error) => diff --git a/apps/tangle-dapp/app/bridge/page.tsx b/apps/tangle-dapp/app/bridge/page.tsx index aa2ef0e92..3415afe1d 100644 --- a/apps/tangle-dapp/app/bridge/page.tsx +++ b/apps/tangle-dapp/app/bridge/page.tsx @@ -1,6 +1,15 @@ +import { + ExchangeFunds, + TokenSwapLineIcon, + WalletLineIcon, +} from '@webb-tools/icons'; +import { TANGLE_DOCS_URL } from '@webb-tools/webb-ui-components'; import { Metadata } from 'next'; import { FC } from 'react'; +import OnboardingItem from '../../components/OnboardingModal/OnboardingItem'; +import OnboardingModal from '../../components/OnboardingModal/OnboardingModal'; +import { OnboardingPageKey } from '../../constants'; import createPageMetadata from '../../utils/createPageMetadata'; import BridgeContainer from './BridgeContainer'; @@ -11,6 +20,30 @@ export const metadata: Metadata = createPageMetadata({ const Bridge: FC = () => { return (
+ + + + + + + +
); diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index d48aea5a2..391308b04 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { + AddLineIcon, CoinIcon, EditLine, Search, @@ -8,6 +9,7 @@ import { WaterDropletIcon, } from '@webb-tools/icons'; import { + Button, TabContent, TabsList as WebbTabsList, TabsRoot, @@ -15,7 +17,7 @@ import { TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; -import { FC, useEffect } from 'react'; +import { FC, useEffect, useState } from 'react'; import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; @@ -24,6 +26,7 @@ import OnboardingModal from '../../components/OnboardingModal/OnboardingModal'; import StatItem from '../../components/StatItem'; import { OnboardingPageKey } from '../../constants'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; +import LsCreatePoolModal from '../../containers/LsCreatePoolModal'; import LsMyProtocolsTable from '../../containers/LsMyProtocolsTable'; import { LsProtocolsTable } from '../../containers/LsPoolsTable'; import useNetworkStore from '../../context/useNetworkStore'; @@ -61,6 +64,7 @@ const LiquidStakingPage: FC = () => { const { network } = useNetworkStore(); const { switchNetwork } = useNetworkSwitcher(); + const [isCreatePoolModalOpen, setIsCreatePoolModalOpen] = useState(false); const lsTangleNetwork = getLsTangleNetwork(lsNetworkId); @@ -87,6 +91,11 @@ const LiquidStakingPage: FC = () => { return (
+ + {
- +
@@ -192,6 +197,20 @@ const LiquidStakingPage: FC = () => { ); })} + + {/** + * TODO: Check what's the min. amount required to create a new pool. If the free balance doesn't meet the min, disable the button and show a tooltip with the reason. + */} + {/* Tabs Content */} diff --git a/apps/tangle-dapp/app/restake/layout.tsx b/apps/tangle-dapp/app/restake/layout.tsx index 167385abc..5952c40d0 100644 --- a/apps/tangle-dapp/app/restake/layout.tsx +++ b/apps/tangle-dapp/app/restake/layout.tsx @@ -1,9 +1,17 @@ +import { Metadata } from 'next'; import { FC, PropsWithChildren } from 'react'; +import createPageMetadata from '../../utils/createPageMetadata'; import Providers from './providers'; export const dynamic = 'force-static'; +export const metadata: Metadata = createPageMetadata({ + title: 'Restake', + description: + "Explore vaults, deposit assets, and select operators to earn rewards with Tangle's restaking infrastructure.", +}); + const RestakeLayout: FC = ({ children }) => { return {children}; }; diff --git a/apps/tangle-dapp/app/restake/page.tsx b/apps/tangle-dapp/app/restake/page.tsx index cb8a69054..c87e04613 100644 --- a/apps/tangle-dapp/app/restake/page.tsx +++ b/apps/tangle-dapp/app/restake/page.tsx @@ -1,6 +1,11 @@ 'use client'; -import { AddCircleLineIcon, Search, SparklingIcon } from '@webb-tools/icons'; +import { + AddCircleLineIcon, + Search, + SparklingIcon, + UserLineIcon, +} from '@webb-tools/icons'; import { TANGLE_DOCS_RESTAKING_URL } from '@webb-tools/webb-ui-components'; import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; @@ -65,7 +70,7 @@ export default function RestakePage() { /> @@ -125,9 +130,9 @@ export default function RestakePage() { {CONTENT.HOW_IT_WORKS} - {/** TODO: Determine read more link here */} - )} - {baseInputOverrides?.actions} - - ); + {wrapperOverrides?.actions} + + ) : ( + wrapperOverrides?.actions + ); const handleChange = useCallback( (newValue: string) => { + setValue(newValue); + if (newValue === '') { setErrorMessage(null); - setValue(newValue); return; } @@ -87,8 +90,6 @@ const AddressInput: FC = ({ } else { setErrorMessage(null); } - - setValue(newValue); }, [setValue, type], ); @@ -100,18 +101,25 @@ const AddressInput: FC = ({ } }, [errorMessage, setErrorMessageOnParent]); + const isEvm = isEvmAddress(value); + return ( - + + = ({ isControlled spellCheck={false} /> - + ); }; diff --git a/apps/tangle-dapp/components/AmountInput/AmountInput.tsx b/apps/tangle-dapp/components/AmountInput.tsx similarity index 85% rename from apps/tangle-dapp/components/AmountInput/AmountInput.tsx rename to apps/tangle-dapp/components/AmountInput.tsx index cef785430..5f935a261 100644 --- a/apps/tangle-dapp/components/AmountInput/AmountInput.tsx +++ b/apps/tangle-dapp/components/AmountInput.tsx @@ -3,9 +3,9 @@ import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config/constants/tangle' import { Button, Input } from '@webb-tools/webb-ui-components'; import { FC, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'; -import useNetworkStore from '../../context/useNetworkStore'; -import useInputAmount from '../../hooks/useInputAmount'; -import BaseInput, { BaseInputProps } from './BaseInput'; +import useNetworkStore from '../context/useNetworkStore'; +import useInputAmount from '../hooks/useInputAmount'; +import InputWrapper, { InputWrapperProps } from './InputWrapper'; export type AmountInputProps = { id: string; @@ -18,7 +18,7 @@ export type AmountInputProps = { amount: BN | null; decimals?: number; isDisabled?: boolean; - baseInputOverrides?: Partial; + wrapperOverrides?: Partial; errorOnEmptyValue?: boolean; setAmount: (newAmount: BN | null) => void; setErrorMessage?: (error: string | null) => void; @@ -36,12 +36,13 @@ const AmountInput: FC = ({ setAmount, min = null, max = null, - decimals = TANGLE_TOKEN_DECIMALS, // Default to the Tangle token decimals. + // Default to the Tangle token decimals. + decimals = TANGLE_TOKEN_DECIMALS, minErrorMessage, maxErrorMessage, showMaxAction = true, isDisabled = false, - baseInputOverrides, + wrapperOverrides, errorOnEmptyValue = false, setErrorMessage, placeholder, @@ -95,19 +96,19 @@ const AmountInput: FC = ({ )} - {baseInputOverrides?.actions} + {wrapperOverrides?.actions} ), - [max, showMaxAction, amount, isDisabled, setMaxAmount, baseInputOverrides], + [max, showMaxAction, amount, isDisabled, setMaxAmount, wrapperOverrides], ); return ( - = ({ isDisabled={isDisabled} isControlled /> - + ); }; diff --git a/apps/tangle-dapp/components/AmountInput/BaseInput.tsx b/apps/tangle-dapp/components/InputWrapper.tsx similarity index 94% rename from apps/tangle-dapp/components/AmountInput/BaseInput.tsx rename to apps/tangle-dapp/components/InputWrapper.tsx index 08c7a3665..71cfd7c7a 100644 --- a/apps/tangle-dapp/components/AmountInput/BaseInput.tsx +++ b/apps/tangle-dapp/components/InputWrapper.tsx @@ -16,10 +16,10 @@ import { } from 'react'; import { twMerge } from 'tailwind-merge'; -import InputAction from '../../containers/ManageProfileModalContainer/InputAction'; -import { useErrorCountContext } from '../../context/ErrorsContext'; +import InputAction from '../containers/ManageProfileModalContainer/InputAction'; +import { useErrorCountContext } from '../context/ErrorsContext'; -export type BaseInputProps = { +export type InputWrapperProps = { title: string; id: string; children: ReactNode; @@ -40,7 +40,7 @@ export type BaseInputProps = { tooltip?: ReactNode; }; -const BaseInput: FC = ({ +const InputWrapper: FC = ({ id, title, children, @@ -172,4 +172,4 @@ const BaseInput: FC = ({ ); }; -export default BaseInput; +export default InputWrapper; diff --git a/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx b/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx index 6faf77969..c7b229aed 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx @@ -21,9 +21,7 @@ const ExternalLink: FC = ({ target="_blank" size="sm" variant="link" - rightIcon={ - - } + rightIcon={} > {children} diff --git a/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx index f4c91e85e..4dfab053a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx @@ -243,6 +243,7 @@ const LsMyPoolsTable: FC = ({ pools, isShown }) => { return ( void; + isDerivativeVariant: boolean; +}; + +const LsProtocolDropdownInput: FC = ({ + id, + networkId, + protocolId, + setProtocolId, + isDerivativeVariant, +}) => { + const protocol = getLsProtocolDef(protocolId); + const network = getLsNetwork(networkId); + + const trySetProtocolId = useCallback( + (newProtocolId: LsProtocolId) => { + return () => { + if (setProtocolId === undefined) { + return; + } + + setProtocolId(newProtocolId); + }; + }, + [setProtocolId], + ); + + return ( + + + +
+ + + + {isDerivativeVariant && LS_DERIVATIVE_TOKEN_PREFIX} + {protocol.token} + + + {setProtocolId !== undefined && network.protocols.length > 1 && ( + + )} +
+
+ + + +
    + {network.protocols.map((protocol) => { + return ( +
  • + +
    + + + + {protocol.token} + +
    +
    +
  • + ); + })} +
+
+
+
+
+ ); +}; + +export default LsProtocolDropdownInput; diff --git a/apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx b/apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx index 7b603b126..14c57d9f6 100644 --- a/apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx +++ b/apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx @@ -9,6 +9,7 @@ const PAGES_WITH_ONBOARDING: PagePath[] = [ PagePath.LIQUID_STAKING, PagePath.RESTAKE, PagePath.NOMINATION, + PagePath.BRIDGE, ]; const OnboardingHelpButton: FC = () => { diff --git a/apps/tangle-dapp/components/PercentageInput.tsx b/apps/tangle-dapp/components/PercentageInput.tsx new file mode 100644 index 000000000..34f3311d9 --- /dev/null +++ b/apps/tangle-dapp/components/PercentageInput.tsx @@ -0,0 +1,96 @@ +import BackspaceDeleteFillIcon from '@webb-tools/icons/BackspaceDeleteFillIcon'; +import { Button, Input } from '@webb-tools/webb-ui-components'; +import { FC, useCallback } from 'react'; + +import useCustomInputValue from '../hooks/useCustomInputValue'; +import ensureError from '../utils/ensureError'; +import InputWrapper, { InputWrapperProps } from './InputWrapper'; + +export type PercentageInputProps = { + id: string; + title: string; + value: number | null; + setValue: (newValue: number | null) => void; + placeholder?: string; + isDisabled?: boolean; + wrapperOverrides?: Partial; + useStandardBoundaries?: boolean; +}; + +const PercentageInput: FC = ({ + id, + title, + value, + setValue: setValueOnParent, + placeholder, + isDisabled = false, + wrapperOverrides, + useStandardBoundaries = true, +}) => { + const parsePercentage = useCallback( + (string: string) => { + try { + const parsedValue = parseFloat(string) / 100; + + if (isNaN(parsedValue)) { + return new Error('Invalid percentage'); + } + + // Limit the value between 0 to 100% if requested. + // This is the most common percentage behavior. + return useStandardBoundaries + ? Math.max(0, Math.min(1, parsedValue)) + : parsedValue; + } catch (error) { + return ensureError(error); + } + }, + [useStandardBoundaries], + ); + + const formatPercentage = useCallback( + (value: number): string => (value * 100).toString(), + [], + ); + + const { displayValue, setDisplayValue, errorMessage, setValue } = + useCustomInputValue({ + setValue: setValueOnParent, + format: formatPercentage, + parse: parsePercentage, + suffix: '%', + }); + + const clearAction = + value === null ? undefined : ( + + ); + + return ( + + + + ); +}; + +export default PercentageInput; diff --git a/apps/tangle-dapp/components/StatItem.tsx b/apps/tangle-dapp/components/StatItem.tsx index 8d6867a0e..c7356be65 100644 --- a/apps/tangle-dapp/components/StatItem.tsx +++ b/apps/tangle-dapp/components/StatItem.tsx @@ -9,7 +9,6 @@ export type StatItemProps = { title: string; subtitle: string; tooltip?: string; - largeSubtitle?: boolean; removeBorder?: boolean; }; @@ -17,7 +16,6 @@ const StatItem: FC = ({ title, subtitle, tooltip, - largeSubtitle = false, removeBorder = false, }) => { const className = cx('flex flex-col items-start justify-center px-3', { @@ -26,13 +24,13 @@ const StatItem: FC = ({ return (
- + {title}
diff --git a/apps/tangle-dapp/components/TextInput.tsx b/apps/tangle-dapp/components/TextInput.tsx new file mode 100644 index 000000000..0f627d572 --- /dev/null +++ b/apps/tangle-dapp/components/TextInput.tsx @@ -0,0 +1,57 @@ +import BackspaceDeleteFillIcon from '@webb-tools/icons/BackspaceDeleteFillIcon'; +import { Button, Input } from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; + +import InputWrapper, { InputWrapperProps } from './InputWrapper'; + +export type TextInputProps = { + id: string; + title: string; + value: string; + setValue: (newValue: string) => void; + placeholder?: string; + isDisabled?: boolean; + wrapperOverrides?: Partial; +}; + +const TextInput: FC = ({ + id, + title, + value, + setValue, + placeholder, + isDisabled = false, + wrapperOverrides, +}) => { + const clearAction = + value === '' ? undefined : ( + + ); + + return ( + + + + ); +}; + +export default TextInput; diff --git a/apps/tangle-dapp/components/tables/Operators/index.tsx b/apps/tangle-dapp/components/tables/Operators/index.tsx index c953058d8..6493e2d92 100644 --- a/apps/tangle-dapp/components/tables/Operators/index.tsx +++ b/apps/tangle-dapp/components/tables/Operators/index.tsx @@ -210,7 +210,7 @@ const OperatorsTable: FC = ({ return ( = ({ return ( = ({ max={walletBalance?.value1 ?? undefined} amount={amountToBond} setAmount={setAmountToBond} - baseInputOverrides={{ isFullWidth: true }} + wrapperOverrides={{ isFullWidth: true }} maxErrorMessage="Not enough available balance" setErrorMessage={handleSetErrorMessage} isDisabled={isBondMoreTxLoading} diff --git a/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx b/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx index d1e1da597..3192a37c1 100644 --- a/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx +++ b/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx @@ -9,7 +9,7 @@ import _ from 'lodash'; import { type FC, useCallback } from 'react'; import z from 'zod'; -import AmountInput from '../../components/AmountInput/AmountInput'; +import AmountInput from '../../components/AmountInput'; import { STAKING_PAYEE_TEXT_TO_VALUE_MAP, STAKING_PAYEE_VALUE_TO_TEXT_MAP, @@ -77,7 +77,7 @@ const BondTokens: FC = ({ max={freeBalance ?? undefined} amount={amountToBond} setAmount={setAmountToBond} - baseInputOverrides={{ isFullWidth: true }} + wrapperOverrides={{ isFullWidth: true }} maxErrorMessage="Not enough available balance" setErrorMessage={handleAmountToBondError} /> diff --git a/apps/tangle-dapp/containers/LsCreatePoolModal.tsx b/apps/tangle-dapp/containers/LsCreatePoolModal.tsx new file mode 100644 index 000000000..4bbc62a8a --- /dev/null +++ b/apps/tangle-dapp/containers/LsCreatePoolModal.tsx @@ -0,0 +1,222 @@ +import { BN } from '@polkadot/util'; +import { isAddress } from '@polkadot/util-crypto'; +import { + Alert, + Button, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + TANGLE_DOCS_LS_CREATE_POOL_URL, +} from '@webb-tools/webb-ui-components'; +import { FC, useCallback, useEffect, useState } from 'react'; + +import AddressInput, { AddressType } from '../components/AddressInput'; +import AmountInput from '../components/AmountInput'; +import LsProtocolDropdownInput from '../components/LiquidStaking/LsProtocolDropdownInput'; +import TextInput from '../components/TextInput'; +import { LsNetworkId, LsProtocolId } from '../constants/liquidStaking/types'; +import useBalances from '../data/balances/useBalances'; +import useLsCreatePoolTx from '../data/liquidStaking/tangle/useLsCreatePoolTx'; +import { useLsStore } from '../data/liquidStaking/useLsStore'; +import useSubstrateAddress from '../hooks/useSubstrateAddress'; +import { TxStatus } from '../hooks/useSubstrateTx'; +import assertSubstrateAddress from '../utils/assertSubstrateAddress'; +import getLsNetwork from '../utils/liquidStaking/getLsNetwork'; +import { ERROR_NOT_ENOUGH_BALANCE } from './ManageProfileModalContainer/Independent/IndependentAllocationInput'; + +export type LsCreatePoolModalProps = { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +}; + +const LsCreatePoolModal: FC = ({ + isOpen, + setIsOpen, +}) => { + const activeSubstrateAddress = useSubstrateAddress(); + // TODO: Use form validation for the properties/inputs. + const [name, setName] = useState(''); + const { free: freeBalance } = useBalances(); + const [rootAddress, setRootAddress] = useState(''); + const [nominatorAddress, setNominatorAddress] = useState(''); + const [bouncerAddress, setBouncerAddress] = useState(''); + + const [initialBondAmount, setInitialBondAmount] = useState(null); + const [protocolId, setProtocolId] = useState(null); + const { lsNetworkId } = useLsStore(); + + const lsNetwork = getLsNetwork(lsNetworkId); + + // TODO: Also add Restaking Parachain when its non-testnet version is available. + const isLiveNetwork = lsNetworkId === LsNetworkId.TANGLE_MAINNET; + + const { execute, status } = useLsCreatePoolTx(); + + const isSubstrateAddresses = + isAddress(rootAddress) && + isAddress(nominatorAddress) && + isAddress(bouncerAddress); + + const handleCreatePoolClick = useCallback(async () => { + if ( + initialBondAmount === null || + !isSubstrateAddresses || + execute === null + ) { + return; + } + + await execute({ + name, + initialBondAmount, + rootAddress: assertSubstrateAddress(rootAddress), + nominatorAddress: assertSubstrateAddress(nominatorAddress), + bouncerAddress: assertSubstrateAddress(bouncerAddress), + }); + }, [ + bouncerAddress, + execute, + initialBondAmount, + isSubstrateAddresses, + name, + nominatorAddress, + rootAddress, + ]); + + // When the active address changes and it is set, + // update the address inputs with the user's address + // as the default value. + useEffect(() => { + if (activeSubstrateAddress !== null) { + setRootAddress(activeSubstrateAddress); + setNominatorAddress(activeSubstrateAddress); + setBouncerAddress(activeSubstrateAddress); + } + }, [activeSubstrateAddress]); + + // Automatically close the modal when the transaction + // is successful. + useEffect(() => { + if (status === TxStatus.COMPLETE) { + setIsOpen(false); + } + }, [setIsOpen, status]); + + return ( + + + setIsOpen(false)}> + Create a Liquid Staking Pool + + +
+
+ {/** + * In case that a testnet is selected, it's helpful to let the users + * know that the pool will be created on the testnet, and that + * it won't be accessible on other networks. + */} + {!isLiveNetwork && ( + + )} + +
+ + + +
+ + {/** TODO: Protocol selection dropdown. */} + + + + + + + + +
+
+ + + + + + +
+
+ ); +}; + +export default LsCreatePoolModal; diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx index bc07f08a6..5a6ef6a69 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx @@ -120,7 +120,9 @@ const LsPoolsTable: FC = ({ pools, isShown }) => { isDisabled={lsPoolId === props.row.original.id} onClick={() => setLsStakingIntent(props.row.original.id, true)} rightIcon={ - lsPoolId !== props.row.original.id ? : undefined + lsPoolId !== props.row.original.id ? ( + + ) : undefined } variant="utility" size="sm" diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx index ac1d26013..d0043ef55 100644 --- a/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx +++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx @@ -4,7 +4,7 @@ import { Close, LockLineIcon } from '@webb-tools/icons'; import { Chip, Input, SkeletonLoader } from '@webb-tools/webb-ui-components'; import { FC, useCallback, useMemo, useState } from 'react'; -import BaseInput from '../../../components/AmountInput/BaseInput'; +import InputWrapper from '../../../components/InputWrapper'; import useNetworkStore from '../../../context/useNetworkStore'; import useInputAmount from '../../../hooks/useInputAmount'; import { RestakingService } from '../../../types'; @@ -150,7 +150,7 @@ const IndependentAllocationInput: FC = ({ const hasDropdownBody = !hasActiveJob && setService !== undefined; return ( - = ({ isReadOnly={availableBalance === null} isControlled /> - + ); }; diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedAmountInput.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedAmountInput.tsx index 03c20493e..518f13e43 100644 --- a/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedAmountInput.tsx +++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedAmountInput.tsx @@ -3,7 +3,7 @@ import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config/constants/tangle' import { Button, Input } from '@webb-tools/webb-ui-components'; import { FC, useRef } from 'react'; -import BaseInput from '../../../components/AmountInput/BaseInput'; +import InputWrapper from '../../../components/InputWrapper'; import useNetworkStore from '../../../context/useNetworkStore'; import useInputAmount from '../../../hooks/useInputAmount'; import { @@ -58,7 +58,7 @@ const SharedAmountInput: FC = ({ ]; return ( - = ({ isInvalid={errorMessage !== null} isControlled /> - + ); }; diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx index dee9b459f..9aa9c9cd1 100644 --- a/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx +++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx @@ -3,7 +3,7 @@ import { CheckBox, Chip, Typography } from '@webb-tools/webb-ui-components'; import { FC, useCallback, useMemo, useState } from 'react'; import { twMerge } from 'tailwind-merge'; -import BaseInput from '../../../components/AmountInput/BaseInput'; +import InputWrapper from '../../../components/InputWrapper'; import { RestakingService } from '../../../types'; import { getChartDataAreaColorByServiceType, @@ -110,7 +110,7 @@ const SharedRolesInput: FC = ({ ); return ( - = ({ Select Role(s) )} - + ); }; diff --git a/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx b/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx index c1d88268e..4b876affd 100644 --- a/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx +++ b/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx @@ -13,7 +13,7 @@ import { TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constant import { type FC, useCallback, useState } from 'react'; import { BondedTokensBalanceInfo } from '../../components'; -import AmountInput from '../../components/AmountInput/AmountInput'; +import AmountInput from '../../components/AmountInput'; import useUnbondedAmount from '../../data/NominatorStats/useUnbondedAmount'; import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount'; import useRebondTx from '../../data/staking/useRebondTx'; @@ -87,7 +87,7 @@ const RebondTxContainer: FC = ({ max={totalUnbondingAmount?.value ?? undefined} amount={amountToRebond} setAmount={setAmountToRebond} - baseInputOverrides={{ isFullWidth: true }} + wrapperOverrides={{ isFullWidth: true }} maxErrorMessage="Not enough unbonding balance" setErrorMessage={handleSetErrorMessage} isDisabled={rebondTxStatus === TxStatus.PROCESSING} diff --git a/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx b/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx index ea4ba775a..0ae502194 100644 --- a/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx +++ b/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx @@ -23,10 +23,8 @@ import { } from 'react'; import { isHex } from 'viem'; -import AddressInput, { - AddressType, -} from '../../components/AddressInput/AddressInput'; -import AmountInput from '../../components/AmountInput/AmountInput'; +import AddressInput, { AddressType } from '../../components/AddressInput'; +import AmountInput from '../../components/AmountInput'; import useNetworkStore from '../../context/useNetworkStore'; import useBalances from '../../data/balances/useBalances'; import useExistentialDeposit from '../../data/balances/useExistentialDeposit'; @@ -211,7 +209,7 @@ const TransferTxContainer: FC = ({ isDisabled={!isReady} amount={amount} setAmount={setAmount} - baseInputOverrides={{ + wrapperOverrides={{ isFullWidth: true, tooltip: transferableBalanceTooltip, }} @@ -228,7 +226,7 @@ const TransferTxContainer: FC = ({ type={AddressType.Both} title="Receiver Address" placeholder="EVM or Substrate" - baseInputOverrides={{ isFullWidth: true }} + wrapperOverrides={{ isFullWidth: true }} value={receiverAddress} setValue={setReceiverAddress} isDisabled={!isReady} diff --git a/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx b/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx index e3cee09a2..a8a91769c 100644 --- a/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx +++ b/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx @@ -13,7 +13,7 @@ import { import { TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constants'; import { type FC, useCallback, useMemo, useState } from 'react'; -import AmountInput from '../../components/AmountInput/AmountInput'; +import AmountInput from '../../components/AmountInput'; import useNetworkStore from '../../context/useNetworkStore'; import useTotalStakedAmountSubscription from '../../data/NominatorStats/useTotalStakedAmountSubscription'; import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount'; @@ -124,7 +124,7 @@ const UnbondTxContainer: FC = ({ max={remainingStakedBalanceToUnbond} amount={amount} setAmount={setAmount} - baseInputOverrides={{ isFullWidth: true }} + wrapperOverrides={{ isFullWidth: true }} maxErrorMessage="Not enough staked balance" setErrorMessage={handleSetErrorMessage} isDisabled={unbondTxStatus === TxStatus.PROCESSING} diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsCreatePoolTx.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsCreatePoolTx.ts new file mode 100644 index 000000000..bcbaab882 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsCreatePoolTx.ts @@ -0,0 +1,51 @@ +import { BN } from '@polkadot/util'; +import { useCallback } from 'react'; + +import { TxName } from '../../../constants'; +import { + SubstrateTxFactory, + useSubstrateTxWithNotification, +} from '../../../hooks/useSubstrateTx'; +import { SubstrateAddress } from '../../../types/utils'; + +export type LsCreatePoolTxContext = { + name: string; + initialBondAmount: BN; + rootAddress: SubstrateAddress; + nominatorAddress: SubstrateAddress; + bouncerAddress: SubstrateAddress; +}; + +const useLsCreatePoolTx = () => { + const substrateTxFactory: SubstrateTxFactory = + useCallback( + async ( + api, + _activeSubstrateAddress, + { + name, + initialBondAmount, + rootAddress, + nominatorAddress, + bouncerAddress, + }, + ) => { + return api.tx.lst.create( + initialBondAmount, + rootAddress, + nominatorAddress, + bouncerAddress, + name, + ); + }, + [], + ); + + // TODO: Add EVM support once precompile(s) for the `lst` pallet are implemented on Tangle. + return useSubstrateTxWithNotification( + TxName.LS_TANGLE_POOL_CREATE, + substrateTxFactory, + ); +}; + +export default useLsCreatePoolTx; diff --git a/apps/tangle-dapp/hooks/useCustomInputValue.ts b/apps/tangle-dapp/hooks/useCustomInputValue.ts new file mode 100644 index 000000000..067aad35a --- /dev/null +++ b/apps/tangle-dapp/hooks/useCustomInputValue.ts @@ -0,0 +1,115 @@ +import { useCallback, useEffect, useState } from 'react'; + +import cleanNumericInputString from '../utils/cleanNumericInputString'; + +export type CustomInputValueProps = { + value?: T | null; + isNumericDecimal?: boolean; + suffix?: string; + setValue: (newValue: T | null) => void; + parse: (string: string) => T | Error; + format: (value: T) => string; +}; + +const END_ZEROES_REGEX = /\.0+$/; + +function useCustomInputValue({ + value: initialValue, + isNumericDecimal = true, + suffix, + setValue: setValueOnConsumer, + parse, + format, +}: CustomInputValueProps) { + const formatDisplayValue = useCallback( + (value: T | null) => { + if (value === null) { + return ''; + } + + return `${format(value)}${suffix ?? ''}`; + }, + [format, suffix], + ); + + const [displayValue, setDisplayValue] = useState( + formatDisplayValue(initialValue ?? null), + ); + + const [errorMessage, setErrorMessage] = useState(null); + + // Attempt to parse the display value once the input's value + // changes. + useEffect(() => { + if (displayValue === '') { + return; + } + + const suffixLength = suffix?.length ?? 0; + + const displayValueMinusSuffix = displayValue.substring( + 0, + displayValue.length - suffixLength, + ); + + const cleanDisplayValue = isNumericDecimal + ? cleanNumericInputString(displayValue) + : displayValueMinusSuffix; + + // Edge cases when parsing numeric decimal values. + if (isNumericDecimal) { + // Let the user type the decimal digits before attempting to + // parse the value. + if (cleanDisplayValue.endsWith('.')) { + return; + } + // Special case to allow for values like `1.0001` without immediately + // parsing it as `1.0` -> `1`. + else if (END_ZEROES_REGEX.test(cleanDisplayValue)) { + return; + } + } + + const parsedValue = parse(cleanDisplayValue); + + if (parsedValue instanceof Error) { + setErrorMessage(parsedValue.message); + } else { + setErrorMessage(null); + setValueOnConsumer(parsedValue); + setDisplayValue(formatDisplayValue(parsedValue)); + } + }, [ + format, + formatDisplayValue, + displayValue, + isNumericDecimal, + parse, + setDisplayValue, + setValueOnConsumer, + suffix, + ]); + + const setValue = useCallback( + (newValue: T | null) => { + setValueOnConsumer(newValue); + + if (newValue === null) { + setDisplayValue(''); + setErrorMessage(null); + } else { + setDisplayValue(formatDisplayValue(newValue)); + } + }, + [setValueOnConsumer, formatDisplayValue], + ); + + return { + displayValue, + setDisplayValue, + setValue, + errorMessage, + }; +} + +export default useCustomInputValue; diff --git a/apps/tangle-dapp/hooks/useInputAmount.ts b/apps/tangle-dapp/hooks/useInputAmount.ts index 90fac09da..f403f746b 100644 --- a/apps/tangle-dapp/hooks/useInputAmount.ts +++ b/apps/tangle-dapp/hooks/useInputAmount.ts @@ -1,6 +1,7 @@ import { BN } from '@polkadot/util'; import { useCallback, useEffect, useState } from 'react'; +import cleanNumericInputString from '../utils/cleanNumericInputString'; import formatBn, { FormatOptions } from '../utils/formatBn'; import parseChainUnits, { ChainUnitParsingError, @@ -73,21 +74,7 @@ const useInputAmount = ({ const handleChange = useCallback( (newAmountString: string) => { - let wasPeriodSeen = false; - - const cleanAmountString = newAmountString - .split('') - .filter((char) => { - if (char === '.' && !wasPeriodSeen) { - wasPeriodSeen = true; - - return true; - } - - // Only consider digits. Ignore any other characters. - return char.match(/\d/); - }) - .join(''); + const cleanAmountString = cleanNumericInputString(newAmountString); // Nothing to do. if (displayAmount === cleanAmountString) { diff --git a/apps/tangle-dapp/hooks/useTxNotification.tsx b/apps/tangle-dapp/hooks/useTxNotification.tsx index f4b5e522a..67f7e773e 100644 --- a/apps/tangle-dapp/hooks/useTxNotification.tsx +++ b/apps/tangle-dapp/hooks/useTxNotification.tsx @@ -38,6 +38,7 @@ const SUCCESS_MESSAGES: Record = { [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', + [TxName.LS_TANGLE_POOL_CREATE]: 'Created liquid staking pool', }; const makeKey = (txName: TxName): `${TxName}-tx-notification` => diff --git a/apps/tangle-dapp/utils/cleanNumericInputString.ts b/apps/tangle-dapp/utils/cleanNumericInputString.ts new file mode 100644 index 000000000..aa8cef6ed --- /dev/null +++ b/apps/tangle-dapp/utils/cleanNumericInputString.ts @@ -0,0 +1,19 @@ +const cleanNumericInputString = (input: string): string => { + let wasPeriodSeen = false; + + return input + .split('') + .filter((char) => { + if (char === '.' && !wasPeriodSeen) { + wasPeriodSeen = true; + + return true; + } + + // Only consider digits. Ignore any other characters. + return /\d/.test(char); + }) + .join(''); +}; + +export default cleanNumericInputString; diff --git a/libs/icons/src/AddLineIcon.tsx b/libs/icons/src/AddLineIcon.tsx new file mode 100644 index 000000000..8e9f3f3e6 --- /dev/null +++ b/libs/icons/src/AddLineIcon.tsx @@ -0,0 +1,11 @@ +import { createIcon } from './create-icon'; +import { IconBase } from './types'; + +export const AddLineIcon = (props: IconBase) => { + return createIcon({ + ...props, + viewBox: '0 0 16 16', + d: 'M7.33398 7.33203V3.33203H8.66732V7.33203H12.6673V8.66536H8.66732V12.6654H7.33398V8.66536H3.33398V7.33203H7.33398Z', + displayName: 'AddLineIcon', + }); +}; diff --git a/libs/icons/src/BackspaceDeleteFillIcon.tsx b/libs/icons/src/BackspaceDeleteFillIcon.tsx new file mode 100644 index 000000000..4b3d9f7df --- /dev/null +++ b/libs/icons/src/BackspaceDeleteFillIcon.tsx @@ -0,0 +1,13 @@ +import { createIcon } from './create-icon'; +import { IconBase } from './types'; + +const BackspaceDeleteFillIcon = (props: IconBase) => { + return createIcon({ + ...props, + viewBox: '0 0 24 24', + d: 'M6.53451 3H20.9993C21.5516 3 21.9993 3.44772 21.9993 4V20C21.9993 20.5523 21.5516 21 20.9993 21H6.53451C6.20015 21 5.88792 20.8329 5.70246 20.5547L0.369122 12.5547C0.145189 12.2188 0.145189 11.7812 0.369122 11.4453L5.70246 3.4453C5.88792 3.1671 6.20015 3 6.53451 3ZM12.9993 10.5858L10.1709 7.75736L8.75668 9.17157L11.5851 12L8.75668 14.8284L10.1709 16.2426L12.9993 13.4142L15.8277 16.2426L17.242 14.8284L14.4135 12L17.242 9.17157L15.8277 7.75736L12.9993 10.5858Z', + displayName: 'BackspaceDeleteFillIcon', + }); +}; + +export default BackspaceDeleteFillIcon; diff --git a/libs/icons/src/create-icon.tsx b/libs/icons/src/create-icon.tsx index 68fb271af..0d7929c72 100644 --- a/libs/icons/src/create-icon.tsx +++ b/libs/icons/src/create-icon.tsx @@ -60,7 +60,7 @@ export function createIcon(options: CreateIconOptions) { const path_ = Children.toArray(path); const size_ = getIconSizeInPixel(size); - const className_ = colorUsingStroke + const colorClassName = colorUsingStroke ? getStrokeColor(darkMode) : getFillColor(darkMode); @@ -76,7 +76,7 @@ export function createIcon(options: CreateIconOptions) { height={size_} style={{ minWidth: size_, minHeight: size_ }} className={twMerge( - className_, + colorClassName, colorUsingStroke ? 'fill-transparent' : 'stroke-transparent', minSizeClassName, className, diff --git a/libs/icons/src/index.ts b/libs/icons/src/index.ts index 9c10df054..7ffc514ec 100644 --- a/libs/icons/src/index.ts +++ b/libs/icons/src/index.ts @@ -151,6 +151,8 @@ export { default as WebbLogoIcon } from './WebbLogoIcon'; export * from './YouTubeFill'; export * from './SubtractCircleLineIcon'; export * from './ArrowDownIcon'; +export * from './AddLineIcon'; +export * from './BackspaceDeleteFillIcon'; // Wallet icons export * from './wallets'; diff --git a/libs/webb-ui-components/src/components/Input/Input.tsx b/libs/webb-ui-components/src/components/Input/Input.tsx index 9d7d75c0a..af3e30c4e 100644 --- a/libs/webb-ui-components/src/components/Input/Input.tsx +++ b/libs/webb-ui-components/src/components/Input/Input.tsx @@ -163,7 +163,7 @@ export const Input: React.FC = (props) => { inputClsxDisabled, ) : cx( - `border-none w-full bg-transparent focus:ring-0 p-0 h4 leading-[30px] font-bold`, + `border-none w-full bg-transparent focus:ring-0 p-0 h5 leading-[30px] font-bold`, 'text-mono-200 dark:text-mono-0', ), inputClassName, diff --git a/libs/webb-ui-components/src/constants/index.ts b/libs/webb-ui-components/src/constants/index.ts index c45406383..e17f088c9 100644 --- a/libs/webb-ui-components/src/constants/index.ts +++ b/libs/webb-ui-components/src/constants/index.ts @@ -44,6 +44,8 @@ export const TANGLE_DOCS_STAKING_URL = 'https://docs.tangle.tools/restake/staking-intro'; export const TANGLE_DOCS_LIQUID_STAKING_URL = 'https://docs.tangle.tools/restake/lst-concepts'; +export const TANGLE_DOCS_LS_CREATE_POOL_URL = + 'https://docs.tangle.tools/restake/lst-pool-create#introduction-to-liquid-staking-pools'; export const TANGLE_DOCS_RESTAKING_URL = 'https://docs.tangle.tools/restake/restake-introduction'; export const TANGLE_GITHUB_URL = 'https://github.com/webb-tools/tangle';