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/components/AddressInput/AddressInput.tsx b/apps/tangle-dapp/components/AddressInput/AddressInput.tsx index d1e403072..38ab6c1b6 100644 --- a/apps/tangle-dapp/components/AddressInput/AddressInput.tsx +++ b/apps/tangle-dapp/components/AddressInput/AddressInput.tsx @@ -17,6 +17,7 @@ export type AddressInputProps = { id: string; title: string; placeholder?: string; + tooltip?: string; type: AddressType; showPasteButton?: boolean; value: string; @@ -29,6 +30,7 @@ export type AddressInputProps = { const AddressInput: FC = ({ id, title, + tooltip, placeholder, type, value, @@ -104,6 +106,7 @@ const AddressInput: 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, 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 ( = ({ 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/constants/index.ts b/apps/tangle-dapp/constants/index.ts index 2b6cb6ad0..fdc2fc7c8 100644 --- a/apps/tangle-dapp/constants/index.ts +++ b/apps/tangle-dapp/constants/index.ts @@ -63,6 +63,7 @@ export enum TxName { LS_LIQUIFIER_WITHDRAW = 'liquifier withdraw', LS_TANGLE_POOL_JOIN = 'join liquid staking pool', LS_TANGLE_POOL_UNBOND = 'unbond from liquid staking pool', + LS_TANGLE_POOL_CREATE = 'create liquid staking pool', } export const PAYMENT_DESTINATION_OPTIONS: StakingRewardsDestinationDisplayText[] = diff --git a/apps/tangle-dapp/containers/LsCreatePoolModal.tsx b/apps/tangle-dapp/containers/LsCreatePoolModal.tsx new file mode 100644 index 000000000..d175787fd --- /dev/null +++ b/apps/tangle-dapp/containers/LsCreatePoolModal.tsx @@ -0,0 +1,203 @@ +import { BN } from '@polkadot/util'; +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; +import { + Alert, + Button, + Input, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + TANGLE_DOCS_LS_CREATE_POOL_URL, +} from '@webb-tools/webb-ui-components'; +import assert from 'assert'; +import { FC, useCallback, useState } from 'react'; + +import AddressInput, { + AddressType, +} from '../components/AddressInput/AddressInput'; +import AmountInput from '../components/AmountInput/AmountInput'; +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 useInputAmount from '../hooks/useInputAmount'; +import useSubstrateAddress from '../hooks/useSubstrateAddress'; +import { TxStatus } from '../hooks/useSubstrateTx'; +import { SubstrateAddress } from '../types/utils'; +import getLsNetwork from '../utils/liquidStaking/getLsNetwork'; +import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; + +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 [rootAddress, setRootAddress] = useState(activeSubstrateAddress); + const { free: freeBalance } = useBalances(); + + const [nominatorAddress, setNominatorAddress] = + useState(activeSubstrateAddress); + + const [bouncerAddress, setBouncerAddress] = useState( + activeSubstrateAddress, + ); + + const [initialBondAmount, setInitialBondAmount] = useState(null); + const [lsProtocolId, setLsProtocolId] = useState(null); + const { lsNetworkId } = useLsStore(); + + const lsProtocol = + lsProtocolId === null ? null : getLsProtocolDef(lsProtocolId); + + const lsNetwork = getLsNetwork(lsNetworkId); + + const { displayAmount, errorMessage } = useInputAmount({ + amount: initialBondAmount, + setAmount: setInitialBondAmount, + // Default to TNT's decimals if the protocol hasn't been selected + // yet. + decimals: lsProtocol?.decimals ?? TANGLE_TOKEN_DECIMALS, + }); + + // TODO: Also add Restaking Parachain when its non-testnet version is available. + const isLiveNetwork = lsNetworkId === LsNetworkId.TANGLE_MAINNET; + + const { execute, status } = useLsCreatePoolTx(); + + const handleCreatePoolClick = useCallback(async () => { + // TODO: Add form validation, then remove this check. + if ( + initialBondAmount === null || + rootAddress === null || + nominatorAddress === null || + bouncerAddress === null + ) { + return; + } + + assert( + execute !== null, + 'Button should have been disabled if execute is null.', + ); + + await execute({ + name, + initialBondAmount, + rootAddress, + nominatorAddress, + bouncerAddress, + }); + }, [ + bouncerAddress, + execute, + initialBondAmount, + name, + nominatorAddress, + rootAddress, + ]); + + 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/data/liquidStaking/tangle/useLsCreatePoolTx.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsCreatePoolTx.ts new file mode 100644 index 000000000..963c1994b --- /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_JOIN, + substrateTxFactory, + ); +}; + +export default useLsCreatePoolTx; 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/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/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..8dc96c298 100644 --- a/libs/icons/src/index.ts +++ b/libs/icons/src/index.ts @@ -151,6 +151,7 @@ export { default as WebbLogoIcon } from './WebbLogoIcon'; export * from './YouTubeFill'; export * from './SubtractCircleLineIcon'; export * from './ArrowDownIcon'; +export * from './AddLineIcon' // Wallet icons export * from './wallets'; 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';