From 7c9e33ebf3989c52df3f35ad40cacc4787ad928e Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 30 Jun 2024 18:36:46 -0400 Subject: [PATCH 01/53] feat(tangle-dapp): Add unstake card --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 6 +- .../LiquidStakeAndUnstakeCards.tsx | 48 +++++ ...uidStakingCard.tsx => LiquidStakeCard.tsx} | 26 +-- .../LiquidStaking/LiquidUnstakeCard.tsx | 185 ++++++++++++++++++ .../NetworkSelectionButton.tsx | 2 +- 5 files changed, 243 insertions(+), 24 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx rename apps/tangle-dapp/components/LiquidStaking/{LiquidStakingCard.tsx => LiquidStakeCard.tsx} (91%) create mode 100644 apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index 1160f7ab3..9cf5090a8 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -1,7 +1,7 @@ import { notFound } from 'next/navigation'; import { FC } from 'react'; -import LiquidStakingCard from '../../../components/LiquidStaking/LiquidStakingCard'; +import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; import TokenInfoCard from '../../../components/LiquidStaking/TokenInfoCard'; import { LIQUID_STAKING_TOKEN_PREFIX, @@ -24,6 +24,8 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { return (
+ + = ({ params: { tokenSymbol } }) => { }} tokenSymbol={tokenSymbol} /> - -
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx new file mode 100644 index 000000000..5fa056271 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; + +import LiquidStakeCard from './LiquidStakeCard'; +import LiquidUnstakeCard from './LiquidUnstakeCard'; + +const LiquidStakeAndUnstakeCards: FC = () => { + const [isStaking, setIsStaking] = useState(true); + const selectedClass = 'dark:text-mono-0'; + const unselectedClass = 'text-mono-100 dark:text-mono-100'; + + return ( +
+
+ setIsStaking(true)} + className={twMerge( + isStaking ? selectedClass : unselectedClass, + !isStaking && 'cursor-pointer', + )} + variant="h4" + fw="bold" + > + Stake + + + setIsStaking(false)} + className={twMerge( + !isStaking ? selectedClass : unselectedClass, + isStaking && 'cursor-pointer', + )} + variant="h4" + fw="bold" + > + Unstake + +
+ + {isStaking ? : } +
+ ); +}; + +export default LiquidStakeAndUnstakeCards; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx similarity index 91% rename from apps/tangle-dapp/components/LiquidStaking/LiquidStakingCard.tsx rename to apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 7eb6ddaaa..697f15302 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -31,7 +31,7 @@ import LiquidStakingInput from './LiquidStakingInput'; import SelectValidators from './SelectValidators'; import WalletBalance from './WalletBalance'; -const LiquidStakingCard: FC = () => { +const LiquidStakeCard: FC = () => { const [fromAmount, setFromAmount] = useState(null); // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. @@ -89,23 +89,9 @@ const LiquidStakingCard: FC = () => { }, [fromAmount, rate]); return ( -
-
- - Stake - - - - Unstake - -
- + <> { { > Stake -
+ ); }; @@ -250,4 +236,4 @@ export const SelectParachainContent: FC = ({ ); }; -export default LiquidStakingCard; +export default LiquidStakeCard; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx new file mode 100644 index 000000000..d36e98281 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -0,0 +1,185 @@ +'use client'; + +// This will override global types and provide type definitions for +// the `lstMinting` pallet for this file only. +import '@webb-tools/tangle-restaking-types'; + +import { BN } from '@polkadot/util'; +import { ArrowDownIcon } from '@radix-ui/react-icons'; +import { InformationLine } from '@webb-tools/icons'; +import { + Button, + IconWithTooltip, + Typography, +} from '@webb-tools/webb-ui-components'; +import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; +import { FC, useCallback, useMemo, useState } from 'react'; + +import { + LIQUID_STAKING_TOKEN_PREFIX, + LiquidStakingChain, + LS_CHAIN_TO_TOKEN, + LS_TOKEN_TO_CURRENCY, +} from '../../constants/liquidStaking'; +import useRedeemTx from '../../data/liquidStaking/useRedeemTx'; +import useApi from '../../hooks/useApi'; +import useApiRx from '../../hooks/useApiRx'; +import { TxStatus } from '../../hooks/useSubstrateTx'; +import LiquidStakingInput from './LiquidStakingInput'; +import WalletBalance from './WalletBalance'; + +const LiquidUnstakeCard: FC = () => { + const [toAmount, setToAmount] = useState(null); + + // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. + const [rate] = useState(1.0); + + const [selectedChain, setSelectedChain] = useState( + LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN, + ); + + const { execute: executeRedeemTx, status: redeemTxStatus } = useRedeemTx(); + + const selectedChainToken = LS_CHAIN_TO_TOKEN[selectedChain]; + + const { result: minimumRedeemAmount } = useApiRx( + useCallback( + (api) => + api.query.lstMinting.minimumRedeem({ + Native: LS_TOKEN_TO_CURRENCY[selectedChainToken], + }), + [selectedChainToken], + ), + TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint, + ); + + const { result: existentialDepositAmount } = useApi( + useCallback((api) => api.consts.balances.existentialDeposit, []), + TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint, + ); + + const minimumInputAmount = useMemo(() => { + if (minimumRedeemAmount === null || existentialDepositAmount === null) { + return null; + } + + return BN.max(minimumRedeemAmount, existentialDepositAmount); + }, [existentialDepositAmount, minimumRedeemAmount]); + + const handleUnstakeClick = useCallback(() => { + if (executeRedeemTx === null || toAmount === null) { + return; + } + + executeRedeemTx({ + amount: toAmount, + currency: LS_TOKEN_TO_CURRENCY[selectedChainToken], + }); + }, [executeRedeemTx, toAmount, selectedChainToken]); + + const fromAmount = useMemo(() => { + if (toAmount === null || rate === null) { + return null; + } + + return toAmount.muln(rate); + }, [toAmount, rate]); + + return ( + <> + } + isReadOnly + setChain={setSelectedChain} + minAmount={minimumInputAmount ?? undefined} + /> + + + + + + {/* Details */} +
+ + + + + +
+ + + + ); +}; + +type DetailItemProps = { + title: string; + tooltip?: string; + value: string; +}; + +/** @internal */ +const DetailItem: FC = ({ title, tooltip, value }) => { + return ( +
+
+ + {title} + + + {tooltip !== undefined && ( + + } + content={tooltip} + overrideTooltipBodyProps={{ + className: 'max-w-[350px]', + }} + /> + )} +
+ + + {value} + +
+ ); +}; + +export default LiquidUnstakeCard; diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx index b42ba922a..8ae591914 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx @@ -53,7 +53,7 @@ const NetworkSelectionButton: FC = () => { - + Network can't be changed while you're in this page. From f434c364dc74793fbcef61d284eb6b3d68b234d4 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:49:39 -0400 Subject: [PATCH 02/53] feat(tangle-dapp): Add other components to be used --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 5 +- .../LiquidStaking/AvailableWithdrawCard.tsx | 84 +++++++++++++++++++ .../LiquidStakeAndUnstakeCards.tsx | 2 +- .../LiquidStaking/LiquidUnstakeCard.tsx | 2 +- .../components/LiquidStaking/VaultListing.tsx | 25 ++++++ libs/icons/src/UndoIcon.tsx | 11 +++ libs/icons/src/index.ts | 1 + 7 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx create mode 100644 apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx create mode 100644 libs/icons/src/UndoIcon.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index 5562b6125..c977646e0 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation'; import { FC } from 'react'; +import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; import TokenInfoCard from '../../../components/LiquidStaking/TokenInfoCard'; import { LIQUID_STAKING_TOKEN_PREFIX } from '../../../constants/liquidStaking'; @@ -17,9 +18,11 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { } return ( -
+
+ + { + return ( + +
+
+ + Available + + + + 0.0 DOT + +
+ +
+ + + {/* TODO: Need a tooltip for this, since it's only an icon. */} + +
+
+ +
+ +
+
+ + My requests + + +
+ } + text="0" + /> + + } + text="1" + /> +
+
+ +
+ + + + 1.00 tgDOT + +
+
+
+ ); +}; + +type RequestItemProps = { + icon: ReactElement; + text: string; +}; + +/** @internal */ +const RequestItem: FC = ({ icon, text }) => { + return ( +
+ {icon} + + + {text} + +
+ ); +}; + +export default AvailableWithdrawCard; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx index 5fa056271..94879d059 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx @@ -13,7 +13,7 @@ const LiquidStakeAndUnstakeCards: FC = () => { const unselectedClass = 'text-mono-100 dark:text-mono-100'; return ( -
+
setIsStaking(true)} diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx index d36e98281..844a3bc35 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -96,6 +96,7 @@ const LiquidUnstakeCard: FC = () => { placeholder={`0 ${selectedChainToken}`} rightElement={} isReadOnly + isTokenLiquidVariant setChain={setSelectedChain} minAmount={minimumInputAmount ?? undefined} /> @@ -107,7 +108,6 @@ const LiquidUnstakeCard: FC = () => { chain={LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN} placeholder={`0 ${LIQUID_STAKING_TOKEN_PREFIX}${selectedChainToken}`} amount={fromAmount} - isTokenLiquidVariant token={LS_CHAIN_TO_TOKEN[selectedChain]} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx b/apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx new file mode 100644 index 000000000..7cc465156 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx @@ -0,0 +1,25 @@ +import { InformationLine } from '@webb-tools/icons'; +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; + +import GlassCard from '../GlassCard'; + +const VaultListing: FC = () => { + return ( + + {/** TODO: Table here. */} + +
+ + + + Select the token to unstake to receive 'Unstake NFT' + representing your assets. Redeem after the unbonding period to claim + funds. (Learn More) + +
+
+ ); +}; + +export default VaultListing; diff --git a/libs/icons/src/UndoIcon.tsx b/libs/icons/src/UndoIcon.tsx new file mode 100644 index 000000000..d079c413a --- /dev/null +++ b/libs/icons/src/UndoIcon.tsx @@ -0,0 +1,11 @@ +import { createIcon } from './create-icon'; +import { IconBase } from './types'; + +export const UndoIcon = (props: IconBase) => { + return createIcon({ + ...props, + viewBox: '0 0 13 14', + d: 'M4.33337 4.1665V6.83317L0.333374 3.49984L4.33337 0.166504V2.83317H7.66671C10.6122 2.83317 13 5.22098 13 8.1665C13 11.112 10.6122 13.4998 7.66671 13.4998H1.66671V12.1665H7.66671C9.87584 12.1665 11.6667 10.3756 11.6667 8.1665C11.6667 5.95736 9.87584 4.1665 7.66671 4.1665H4.33337Z', + displayName: 'UndoIcon', + }); +}; diff --git a/libs/icons/src/index.ts b/libs/icons/src/index.ts index d107cc66c..f1a05996b 100644 --- a/libs/icons/src/index.ts +++ b/libs/icons/src/index.ts @@ -143,6 +143,7 @@ export { default as WalletPayIcon } from './WalletPayIcon'; export { default as WebbLogoIcon } from './WebbLogoIcon'; export * from './YouTubeFill'; export * from './WaterDropletIcon'; +export * from './UndoIcon'; // Wallet icons export * from './wallets'; From 9919684a867bb0ac612106a21dadf1830344ec45 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:56:12 -0400 Subject: [PATCH 03/53] refactor(tangle-dapp): Remove outdated token info card --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 27 --- .../LiquidStakeAndUnstakeCards.tsx | 2 +- .../LiquidStaking/LiquidStakeCard.tsx | 2 +- .../LiquidStaking/TokenInfoCard.tsx | 182 ------------------ 4 files changed, 2 insertions(+), 211 deletions(-) delete mode 100644 apps/tangle-dapp/components/LiquidStaking/TokenInfoCard.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index c977646e0..28996d3b4 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -3,8 +3,6 @@ import { FC } from 'react'; import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; -import TokenInfoCard from '../../../components/LiquidStaking/TokenInfoCard'; -import { LIQUID_STAKING_TOKEN_PREFIX } from '../../../constants/liquidStaking'; import isLiquidStakingToken from '../../../utils/liquidStaking/isLiquidStakingToken'; type Props = { @@ -22,31 +20,6 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { - -
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx index 94879d059..1af8d40b4 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx @@ -13,7 +13,7 @@ const LiquidStakeAndUnstakeCards: FC = () => { const unselectedClass = 'text-mono-100 dark:text-mono-100'; return ( -
+
setIsStaking(true)} diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 697f15302..a26eb343e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -135,7 +135,7 @@ const LiquidStakeCard: FC = () => { value="7 days" /> - +
- - -
-
- -
- {TokenSVG && } -
-
- ); -}; - -type GridItemProps = { - title: string; - tooltip?: string; - value: string; - valueTooltip?: string; - tokenSymbol?: string; - fw?: 'normal' | 'bold'; -}; - -/** @internal */ -const GridItem: FC = ({ - title, - tooltip, - value, - valueTooltip, - tokenSymbol, - fw, -}) => { - return ( -
-
- - {title} - - - {tooltip !== undefined && ( - - } - content={tooltip} - overrideTooltipBodyProps={{ - className: 'max-w-[350px]', - }} - /> - )} -
- -
- - {value} - - - - {tokenSymbol} - - - {valueTooltip !== undefined && ( - - } - content={valueTooltip} - overrideTooltipBodyProps={{ - className: 'max-w-[350px]', - }} - /> - )} -
-
- ); -}; - -export default TokenInfoCard; From 536a1011cc106aa36c83897063acec2c6bd8bfd6 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:52:34 -0400 Subject: [PATCH 04/53] feat(tangle-dapp): Add Substrate address type utils --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 3 + .../LiquidStaking/HexagonAvatar.tsx | 11 ++ .../LiquidStaking/StakedAssetsTable.tsx | 135 ++++++++++++++++++ .../components/LiquidStaking/VaultListing.tsx | 25 ---- .../components/tableCells/TokenAmountCell.tsx | 38 ++++- apps/tangle-dapp/types/utils.ts | 11 ++ .../utils/assertAnySubstrateAddress.ts | 12 ++ .../utils/isAnySubstrateAddress.ts | 11 ++ 8 files changed, 215 insertions(+), 31 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx create mode 100644 apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx delete mode 100644 apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx create mode 100644 apps/tangle-dapp/utils/assertAnySubstrateAddress.ts create mode 100644 apps/tangle-dapp/utils/isAnySubstrateAddress.ts diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index 28996d3b4..0213e4a11 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -3,6 +3,7 @@ import { FC } from 'react'; import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; +import StakedAssetsTable from '../../../components/LiquidStaking/StakedAssetsTable'; import isLiquidStakingToken from '../../../utils/liquidStaking/isLiquidStakingToken'; type Props = { @@ -20,6 +21,8 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { + +
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx new file mode 100644 index 000000000..55e8566eb --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx @@ -0,0 +1,11 @@ +export type HexagonAvatarProps = { + address: string; +}; + +const HexagonAvatar = ({ address }: { address: string }) => { + return ( +
+ +
+ ); +}; diff --git a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx new file mode 100644 index 000000000..5954f1342 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { BN } from '@polkadot/util'; +import { HexString } from '@polkadot/util/types'; +import { + createColumnHelper, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { ExternalLinkLine, InformationLine } from '@webb-tools/icons'; +import { + fuzzyFilter, + shortenString, + Table, + Typography, +} from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; + +import GlassCard from '../GlassCard'; +import { HeaderCell } from '../tableCells'; +import TokenAmountCell from '../tableCells/TokenAmountCell'; + +type StakedAssetItem = { + id: HexString; + validators: string[]; + amount: BN; +}; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor('id', { + header: () => , + cell: (props) => { + // TODO: Get proper href. + const href = '#'; + + return ( + + + {shortenString(props.getValue(), 3)} + + + + + ); + }, + }), + columnHelper.accessor('validators', { + header: () => ( + + ), + cell: (props) => { + return props.getValue(); + }, + }), + columnHelper.accessor('amount', { + header: () => , + cell: (props) => { + return ( + + ); + }, + }), +]; + +const StakedAssetsTable: FC = () => { + const data: StakedAssetItem[] = [ + { + id: '0x3a7f9e8c14b7d2f5', + validators: ['Alice', 'Bob'], + amount: new BN(100), + }, + { + id: '0xd5c4a2b1f3e8c7d9', + validators: ['Alice', 'Bob'], + amount: new BN(123), + }, + { + id: '0x9b3e47d8a5c2f1e4', + validators: ['Alice', 'Bob'], + amount: new BN(321), + }, + ]; + + const table = useReactTable({ + data, + columns, + filterFns: { + fuzzy: fuzzyFilter, + }, + globalFilterFn: fuzzyFilter, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + return ( + + + Staked Assets + + +
+ + + +
+ + + + Select the token to unstake to receive 'Unstake NFT' + representing your assets. Redeem after the unbonding period to claim + funds. (Learn More) + +
+ + ); +}; + +export default StakedAssetsTable; diff --git a/apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx b/apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx deleted file mode 100644 index 7cc465156..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/VaultListing.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { InformationLine } from '@webb-tools/icons'; -import { Typography } from '@webb-tools/webb-ui-components'; -import { FC } from 'react'; - -import GlassCard from '../GlassCard'; - -const VaultListing: FC = () => { - return ( - - {/** TODO: Table here. */} - -
- - - - Select the token to unstake to receive 'Unstake NFT' - representing your assets. Redeem after the unbonding period to claim - funds. (Learn More) - -
-
- ); -}; - -export default VaultListing; diff --git a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx index 51c197928..94b3a5bd4 100644 --- a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx +++ b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx @@ -1,5 +1,5 @@ -import { BN } from '@polkadot/util'; -import { FC } from 'react'; +import { BN, formatBalance } from '@polkadot/util'; +import { FC, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; import useNetworkStore from '../../context/useNetworkStore'; @@ -8,11 +8,35 @@ import formatTangleBalance from '../../utils/formatTangleBalance'; export type TokenAmountCellProps = { amount: BN; className?: string; + tokenSymbol?: string; + decimals?: number; + alignCenter?: boolean; }; -const TokenAmountCell: FC = ({ amount, className }) => { +const TokenAmountCell: FC = ({ + amount, + className, + tokenSymbol, + decimals, + alignCenter = true, +}) => { const { nativeTokenSymbol } = useNetworkStore(); - const formattedBalance = formatTangleBalance(amount); + + const formattedBalance = useMemo(() => { + if (decimals === undefined) { + return formatTangleBalance(amount); + } + + return formatBalance(amount, { + decimals, + withZero: false, + // This ensures that the balance is always displayed in the + // base unit, preventing the conversion to larger or smaller + // units (e.g. kilo, milli, etc.). + forceUnit: '-', + withUnit: false, + }); + }, [amount, decimals]); const parts = formattedBalance.split('.'); const integerPart = parts[0]; @@ -21,14 +45,16 @@ const TokenAmountCell: FC = ({ amount, className }) => { return ( {integerPart} - {decimalPart !== undefined && `.${decimalPart}`} {nativeTokenSymbol} + {decimalPart !== undefined && `.${decimalPart}`}{' '} + {tokenSymbol ?? nativeTokenSymbol} ); diff --git a/apps/tangle-dapp/types/utils.ts b/apps/tangle-dapp/types/utils.ts index 833a46efd..5ad4b1b95 100644 --- a/apps/tangle-dapp/types/utils.ts +++ b/apps/tangle-dapp/types/utils.ts @@ -117,3 +117,14 @@ export type TransformEnum = ? never : AsEnumValuesToPrimitive >; + +export type Brand = Type & { __brand: Name }; + +export type RemoveBrand = { __brand: never }; + +export type AnySubstrateAddress = Brand; + +export type SubstrateAddress = Brand< + string, + 'SubstrateAddress' & { ss58Format: SS58 } +>; diff --git a/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts b/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts new file mode 100644 index 000000000..61c50d61e --- /dev/null +++ b/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts @@ -0,0 +1,12 @@ +import { isAddress } from '@polkadot/util-crypto'; +import assert from 'assert'; + +import { AnySubstrateAddress } from '../types/utils'; + +const assertAnySubstrateAddress = (address: string): AnySubstrateAddress => { + assert(isAddress(address), 'Address should be a valid Substrate address'); + + return address as AnySubstrateAddress; +}; + +export default assertAnySubstrateAddress; diff --git a/apps/tangle-dapp/utils/isAnySubstrateAddress.ts b/apps/tangle-dapp/utils/isAnySubstrateAddress.ts new file mode 100644 index 000000000..05c5fa94f --- /dev/null +++ b/apps/tangle-dapp/utils/isAnySubstrateAddress.ts @@ -0,0 +1,11 @@ +import { isAddress } from '@polkadot/util-crypto'; + +import { AnySubstrateAddress, RemoveBrand } from '../types/utils'; + +const isAnySubstrateAddress = ( + address: string, +): address is AnySubstrateAddress & RemoveBrand => { + return isAddress(address); +}; + +export default isAnySubstrateAddress; From 359170fb30d8280f299268af19f001df1f6367f8 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sat, 6 Jul 2024 18:55:07 -0400 Subject: [PATCH 05/53] feat(tangle-dapp): Add `HexagonAvatar` component --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 6 ++++ .../LiquidStaking/HexagonAvatar.tsx | 33 +++++++++++++++++-- .../LiquidStaking/HexagonAvatarList.tsx | 20 +++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index 0213e4a11..ad0c25e10 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -2,6 +2,7 @@ import { notFound } from 'next/navigation'; import { FC } from 'react'; import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; +import HexagonAvatar from '../../../components/LiquidStaking/HexagonAvatar'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; import StakedAssetsTable from '../../../components/LiquidStaking/StakedAssetsTable'; import isLiquidStakingToken from '../../../utils/liquidStaking/isLiquidStakingToken'; @@ -23,6 +24,11 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { + +
+ + hello world +
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx index 55e8566eb..4eb58223a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx @@ -1,11 +1,38 @@ +import { FC, ReactNode } from 'react'; + +import { AnySubstrateAddress } from '../../types/utils'; + export type HexagonAvatarProps = { - address: string; + address: AnySubstrateAddress; +}; + +const Hexagon: FC<{ children: ReactNode }> = ({ children }) => { + return ( + + + + + + + + {children} + + ); }; -const HexagonAvatar = ({ address }: { address: string }) => { +const HexagonAvatar: FC = ({ address }) => { return (
- + +
+
); }; + +export default HexagonAvatar; diff --git a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx new file mode 100644 index 000000000..8fdaeba43 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react'; + +import { AnySubstrateAddress } from '../../types/utils'; +import HexagonAvatar from './HexagonAvatar'; + +export type HexagonAvatarListProps = { + addresses: AnySubstrateAddress[]; +}; + +const HexagonAvatarList: FC = ({ addresses }) => { + return ( +
+ {addresses.map((address) => ( + + ))} +
+ ); +}; + +export default HexagonAvatarList; From 07ec2d7ac8ab3b3b062a8b750ae26eb730e65fb3 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:28:14 -0400 Subject: [PATCH 06/53] style(tangle-dapp): Adjust overview page to match design --- apps/tangle-dapp/app/liquid-staking/page.tsx | 58 ++++++++----------- .../LiquidStaking/LiquidStakingTokenItem.tsx | 19 ------ 2 files changed, 25 insertions(+), 52 deletions(-) diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 9169bec62..851e1c434 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -4,16 +4,12 @@ import { FC } from 'react'; import { GlassCard } from '../../components'; import LiquidStakingTokenItem from '../../components/LiquidStaking/LiquidStakingTokenItem'; import StatItem from '../../components/LiquidStaking/StatItem'; -import { LS_CHAIN_TO_TOKEN, TVS_TOOLTIP } from '../../constants/liquidStaking'; +import { LS_CHAIN_TO_TOKEN } from '../../constants/liquidStaking'; import entriesOf from '../../utils/entriesOf'; const LiquidStakingPage: FC = () => { return ( -
- - Overview - - +
@@ -22,44 +18,40 @@ const LiquidStakingPage: FC = () => { Get Liquid Staking Tokens (LSTs) to earn & unleash restaking on - Tangle via delegation. + Tangle Mainnet via delegation.
-
+
- - - -
- - +
+ Liquid Staking Tokens -
-
- {entriesOf(LS_CHAIN_TO_TOKEN).map(([chain, token]) => { - return ( - - ); - })} + +
+
+ {entriesOf(LS_CHAIN_TO_TOKEN).map(([chain, token]) => { + return ( + + ); + })} +
-
- + +
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx index e9fe1d433..3b269cb11 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx @@ -3,7 +3,6 @@ import { BN } from '@polkadot/util'; import { ArrowRight } from '@webb-tools/icons'; import { Button, Chip, Typography } from '@webb-tools/webb-ui-components'; -import assert from 'assert'; import Image from 'next/image'; import { FC, useMemo } from 'react'; @@ -25,12 +24,6 @@ export type LiquidStakingTokenItemProps = { tokenSymbol: LiquidStakingToken; totalValueStaked: number; totalStaked: string; - - /** - * Annual Percentage Yield (APY). Should a decimal value - * between 0 and 1. - */ - annualPercentageYield: number; }; const LiquidStakingTokenItem: FC = ({ @@ -38,23 +31,13 @@ const LiquidStakingTokenItem: FC = ({ chain, tokenSymbol, totalValueStaked, - annualPercentageYield, totalStaked, }) => { - assert( - annualPercentageYield >= 0 && annualPercentageYield <= 1, - 'APY should be between 0 and 1', - ); - const formattedTotalValueStaked = totalValueStaked.toLocaleString('en-US', { style: 'currency', currency: 'USD', }); - const formattedAnnualPercentageYield = (annualPercentageYield * 100).toFixed( - 2, - ); - const formattedTotalStaked = useMemo( () => formatTangleBalance(new BN(totalStaked)), [totalStaked], @@ -88,8 +71,6 @@ const LiquidStakingTokenItem: FC = ({
- - Date: Mon, 8 Jul 2024 22:36:38 -0400 Subject: [PATCH 07/53] feat(tangle-dapp): Finish design impl. of `StakedAssetsTable` --- .../components/LiquidStaking/AvatarList.tsx | 15 +++++++ .../LiquidStaking/HexagonAvatarList.tsx | 20 --------- .../LiquidStaking/StakedAssetsTable.tsx | 42 ++++++++++++------- 3 files changed, 42 insertions(+), 35 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx delete mode 100644 apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx b/apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx new file mode 100644 index 000000000..0fc138384 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx @@ -0,0 +1,15 @@ +import { FC, ReactNode } from 'react'; + +export type AvatarListProps = { + children: ReactNode; +}; + +const AvatarList: FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default AvatarList; diff --git a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx deleted file mode 100644 index 8fdaeba43..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatarList.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { FC } from 'react'; - -import { AnySubstrateAddress } from '../../types/utils'; -import HexagonAvatar from './HexagonAvatar'; - -export type HexagonAvatarListProps = { - addresses: AnySubstrateAddress[]; -}; - -const HexagonAvatarList: FC = ({ addresses }) => { - return ( -
- {addresses.map((address) => ( - - ))} -
- ); -}; - -export default HexagonAvatarList; diff --git a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx index 5954f1342..fc219f78a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx @@ -12,6 +12,7 @@ import { } from '@tanstack/react-table'; import { ExternalLinkLine, InformationLine } from '@webb-tools/icons'; import { + Avatar, fuzzyFilter, shortenString, Table, @@ -19,13 +20,15 @@ import { } from '@webb-tools/webb-ui-components'; import { FC } from 'react'; +import { AnySubstrateAddress } from '../../types/utils'; import GlassCard from '../GlassCard'; import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; +import AvatarList from './AvatarList'; type StakedAssetItem = { id: HexString; - validators: string[]; + validators: AnySubstrateAddress[]; amount: BN; }; @@ -39,7 +42,7 @@ const columns = [ const href = '#'; return ( - + {shortenString(props.getValue(), 3)} @@ -54,38 +57,44 @@ const columns = [ ), cell: (props) => { - return props.getValue(); + return ( + + {props.getValue().map((address, index) => ( + + ))} + + ); }, }), columnHelper.accessor('amount', { header: () => , cell: (props) => { - return ( - - ); + return ; }, }), ]; const StakedAssetsTable: FC = () => { + // TODO: Mock data. + const testAddresses = [ + '0x3a7f9e8c14b7d2f5', + '0xd5c4a2b1f3e8c7d9', + ] as AnySubstrateAddress[]; + const data: StakedAssetItem[] = [ { id: '0x3a7f9e8c14b7d2f5', - validators: ['Alice', 'Bob'], + validators: testAddresses, amount: new BN(100), }, { id: '0xd5c4a2b1f3e8c7d9', - validators: ['Alice', 'Bob'], + validators: testAddresses, amount: new BN(123), }, { id: '0x9b3e47d8a5c2f1e4', - validators: ['Alice', 'Bob'], + validators: testAddresses, amount: new BN(321), }, ]; @@ -109,7 +118,7 @@ const StakedAssetsTable: FC = () => { Staked Assets -
+
{ Select the token to unstake to receive 'Unstake NFT' representing your assets. Redeem after the unbonding period to claim - funds. (Learn More) + funds.{' '} + + Learn More + From 0d66e89f37a1949b2d3354d92d07707a24298956 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 9 Jul 2024 00:13:52 -0400 Subject: [PATCH 08/53] refactor(tangle-dapp): Prefer chain definitions instead of maps --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 6 - apps/tangle-dapp/app/liquid-staking/page.tsx | 11 +- .../components/LiquidStaking/ChainLogo.tsx | 26 ++-- .../LiquidStaking/LiquidStakeCard.tsx | 37 +++-- .../LiquidStaking/LiquidStakingInput.tsx | 27 ++-- .../LiquidStaking/LiquidStakingTokenItem.tsx | 8 +- .../LiquidStaking/LiquidUnstakeCard.tsx | 83 ++++++----- apps/tangle-dapp/constants/liquidStaking.ts | 138 ++++++++++++------ apps/tangle-dapp/hooks/useInputAmount.ts | 1 + 9 files changed, 195 insertions(+), 142 deletions(-) diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index ad0c25e10..0213e4a11 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -2,7 +2,6 @@ import { notFound } from 'next/navigation'; import { FC } from 'react'; import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; -import HexagonAvatar from '../../../components/LiquidStaking/HexagonAvatar'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; import StakedAssetsTable from '../../../components/LiquidStaking/StakedAssetsTable'; import isLiquidStakingToken from '../../../utils/liquidStaking/isLiquidStakingToken'; @@ -24,11 +23,6 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { - -
- - hello world -
); }; diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 851e1c434..167658b2c 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -4,8 +4,7 @@ import { FC } from 'react'; import { GlassCard } from '../../components'; import LiquidStakingTokenItem from '../../components/LiquidStaking/LiquidStakingTokenItem'; import StatItem from '../../components/LiquidStaking/StatItem'; -import { LS_CHAIN_TO_TOKEN } from '../../constants/liquidStaking'; -import entriesOf from '../../utils/entriesOf'; +import { LIQUID_STAKING_CHAINS } from '../../constants/liquidStaking'; const LiquidStakingPage: FC = () => { return ( @@ -35,13 +34,13 @@ const LiquidStakingPage: FC = () => {
- {entriesOf(LS_CHAIN_TO_TOKEN).map(([chain, token]) => { + {LIQUID_STAKING_CHAINS.map((chain) => { return ( { } }; -const getBackgroundColor = (chain: LiquidStakingChain) => { +const getBackgroundColor = (chain: LiquidStakingChainId) => { switch (chain) { - case LiquidStakingChain.MANTA: + case LiquidStakingChainId.MANTA: return 'bg-[#13101D] dark:bg-[#13101D]'; - case LiquidStakingChain.MOONBEAM: + case LiquidStakingChainId.MOONBEAM: return 'bg-[#1d1336] dark:bg-[#1d1336]'; - case LiquidStakingChain.PHALA: + case LiquidStakingChainId.PHALA: return 'bg-black dark:bg-black'; - case LiquidStakingChain.POLKADOT: + case LiquidStakingChainId.POLKADOT: return 'bg-mono-0 dark:bg-mono-0'; - case LiquidStakingChain.ASTAR: + case LiquidStakingChainId.ASTAR: // No background for Astar, since it looks better without // a background. return ''; @@ -51,7 +51,7 @@ const getBackgroundColor = (chain: LiquidStakingChain) => { }; const ChainLogo: FC = ({ - chain, + chainId, size = 'md', isRounded = false, }) => { @@ -61,11 +61,11 @@ const ChainLogo: FC = ({ diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index a26eb343e..2eb4467cf 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -18,10 +18,9 @@ import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-u import { FC, useCallback, useMemo, useState } from 'react'; import { + LIQUID_STAKING_CHAIN_MAP, LIQUID_STAKING_TOKEN_PREFIX, - LiquidStakingChain, - LS_CHAIN_TO_TOKEN, - LS_TOKEN_TO_CURRENCY, + LiquidStakingChainId, } from '../../constants/liquidStaking'; import useMintTx from '../../data/liquidStaking/useMintTx'; import useApi from '../../hooks/useApi'; @@ -37,21 +36,21 @@ const LiquidStakeCard: FC = () => { // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. const [rate] = useState(1.0); - const [selectedChain, setSelectedChain] = useState( - LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN, + const [selectedChainId, setSelectedChainId] = useState( + LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN, ); const { execute: executeMintTx, status: mintTxStatus } = useMintTx(); - const selectedChainToken = LS_CHAIN_TO_TOKEN[selectedChain]; + const selectedChain = LIQUID_STAKING_CHAIN_MAP[selectedChainId]; const { result: minimumMintingAmount } = useApiRx( useCallback( (api) => api.query.lstMinting.minimumMint({ - Native: LS_TOKEN_TO_CURRENCY[selectedChainToken], + Native: selectedChain.currency, }), - [selectedChainToken], + [selectedChain.currency], ), TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint, ); @@ -76,9 +75,9 @@ const LiquidStakeCard: FC = () => { executeMintTx({ amount: fromAmount, - currency: LS_TOKEN_TO_CURRENCY[selectedChainToken], + currency: selectedChain.currency, }); - }, [executeMintTx, fromAmount, selectedChainToken]); + }, [executeMintTx, fromAmount, selectedChain.currency]); const toAmount = useMemo(() => { if (fromAmount === null || rate === null) { @@ -92,13 +91,13 @@ const LiquidStakeCard: FC = () => { <> } - setChain={setSelectedChain} + setChain={setSelectedChainId} minAmount={minimumInputAmount ?? undefined} /> @@ -106,12 +105,12 @@ const LiquidStakeCard: FC = () => { } /> @@ -120,13 +119,13 @@ const LiquidStakeCard: FC = () => { void; + chain: LiquidStakingChainId; + setChain?: (newChain: LiquidStakingChainId) => void; amount: BN | null; setAmount?: (newAmount: BN | null) => void; isReadOnly?: boolean; @@ -76,7 +77,7 @@ const LiquidStakingInput: FC = ({ }); const handleChainChange = useCallback( - (newChain: LiquidStakingChain) => { + (newChain: LiquidStakingChainId) => { if (setChain !== undefined) { setChain(newChain); } @@ -136,11 +137,13 @@ type TokenChipProps = { /** @internal */ const TokenChip: FC = ({ token, isLiquidVariant }) => { - const chain = LS_TOKEN_TO_CHAIN[token]; + const chain = LIQUID_STAKING_CHAINS.find((chain) => chain.token === token); + + assert(chain !== undefined, 'All tokens should have a corresponding chain'); return (
- + {isLiquidVariant && LIQUID_STAKING_TOKEN_PREFIX} @@ -151,13 +154,13 @@ const TokenChip: FC = ({ token, isLiquidVariant }) => { }; type ChainSelectorProps = { - selectedChain: LiquidStakingChain; + selectedChain: LiquidStakingChainId; /** * If this function is not provided, the selector will be * considered read-only. */ - setChain?: (newChain: LiquidStakingChain) => void; + setChain?: (newChain: LiquidStakingChainId) => void; }; const ChainSelector: FC = ({ selectedChain, setChain }) => { @@ -172,7 +175,7 @@ const ChainSelector: FC = ({ selectedChain, setChain }) => { isReadOnly && 'px-3', )} > - + {LS_CHAIN_TO_NETWORK_NAME[selectedChain]} @@ -191,13 +194,13 @@ const ChainSelector: FC = ({ selectedChain, setChain }) => {
    - {Object.values(LiquidStakingChain) + {Object.values(LiquidStakingChainId) .filter((chain) => chain !== selectedChain) .map((chain) => { return (
  • } + startIcon={} onSelect={() => setChain(chain)} className="px-3 normal-case" > diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx index 3b269cb11..9111f97d6 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingTokenItem.tsx @@ -9,7 +9,7 @@ import { FC, useMemo } from 'react'; import { StaticAssetPath } from '../../constants'; import { LIQUID_STAKING_TOKEN_PREFIX, - LiquidStakingChain, + LiquidStakingChainId, LiquidStakingToken, TVS_TOOLTIP, } from '../../constants/liquidStaking'; @@ -19,7 +19,7 @@ import ChainLogo from './ChainLogo'; import StatItem from './StatItem'; export type LiquidStakingTokenItemProps = { - chain: LiquidStakingChain; + chainId: LiquidStakingChainId; title: string; tokenSymbol: LiquidStakingToken; totalValueStaked: number; @@ -28,7 +28,7 @@ export type LiquidStakingTokenItemProps = { const LiquidStakingTokenItem: FC = ({ title, - chain, + chainId, tokenSymbol, totalValueStaked, totalStaked, @@ -47,7 +47,7 @@ const LiquidStakingTokenItem: FC = ({
    - + { - const [toAmount, setToAmount] = useState(null); + const [fromAmount, setFromAmount] = useState(null); // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. const [rate] = useState(1.0); - const [selectedChain, setSelectedChain] = useState( - LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN, + const [selectedChainId, setSelectedChainId] = useState( + LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN, ); const { execute: executeRedeemTx, status: redeemTxStatus } = useRedeemTx(); - const selectedChainToken = LS_CHAIN_TO_TOKEN[selectedChain]; + const selectedChain = LIQUID_STAKING_CHAIN_MAP[selectedChainId]; const { result: minimumRedeemAmount } = useApiRx( useCallback( (api) => api.query.lstMinting.minimumRedeem({ - Native: LS_TOKEN_TO_CURRENCY[selectedChainToken], + Native: selectedChain.currency, }), - [selectedChainToken], + [selectedChain.currency], ), TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint, ); @@ -67,37 +66,35 @@ const LiquidUnstakeCard: FC = () => { }, [existentialDepositAmount, minimumRedeemAmount]); const handleUnstakeClick = useCallback(() => { - if (executeRedeemTx === null || toAmount === null) { + if (executeRedeemTx === null || fromAmount === null) { return; } executeRedeemTx({ - amount: toAmount, - currency: LS_TOKEN_TO_CURRENCY[selectedChainToken], + amount: fromAmount, + currency: selectedChain.currency, }); - }, [executeRedeemTx, toAmount, selectedChainToken]); + }, [executeRedeemTx, fromAmount, selectedChain.currency]); - const fromAmount = useMemo(() => { - if (toAmount === null || rate === null) { + const toAmount = useMemo(() => { + if (fromAmount === null || rate === null) { return null; } - return toAmount.muln(rate); - }, [toAmount, rate]); + return fromAmount.muln(rate); + }, [fromAmount, rate]); return ( <> } - isReadOnly isTokenLiquidVariant - setChain={setSelectedChain} minAmount={minimumInputAmount ?? undefined} /> @@ -105,10 +102,12 @@ const LiquidUnstakeCard: FC = () => { {/* Details */} @@ -116,25 +115,39 @@ const LiquidUnstakeCard: FC = () => { + 1 {selectedChain.token}{' '} + = {rate} {LIQUID_STAKING_TOKEN_PREFIX} + {selectedChain.token} + + } /> + 0.001984 {selectedChain.token} + + } /> + 7 days + + } />
    - + {value}
    diff --git a/apps/tangle-dapp/constants/liquidStaking.ts b/apps/tangle-dapp/constants/liquidStaking.ts index 151f408a1..6abc124e6 100644 --- a/apps/tangle-dapp/constants/liquidStaking.ts +++ b/apps/tangle-dapp/constants/liquidStaking.ts @@ -2,7 +2,7 @@ import { TanglePrimitivesCurrencyTokenSymbol } from '@polkadot/types/lookup'; import { StaticAssetPath } from '.'; -export enum LiquidStakingChain { +export enum LiquidStakingChainId { POLKADOT = 'Polkadot', PHALA = 'Phala', MOONBEAM = 'Moonbeam', @@ -20,60 +20,104 @@ export enum LiquidStakingToken { TNT = 'BNC', } -export const LS_CHAIN_TO_TOKEN: Record = - { - [LiquidStakingChain.POLKADOT]: LiquidStakingToken.DOT, - [LiquidStakingChain.PHALA]: LiquidStakingToken.PHALA, - [LiquidStakingChain.MOONBEAM]: LiquidStakingToken.GLMR, - [LiquidStakingChain.ASTAR]: LiquidStakingToken.ASTAR, - [LiquidStakingChain.MANTA]: LiquidStakingToken.MANTA, - [LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN]: LiquidStakingToken.TNT, - }; - -export const LS_TOKEN_TO_CHAIN: Record = - { - [LiquidStakingToken.DOT]: LiquidStakingChain.POLKADOT, - [LiquidStakingToken.PHALA]: LiquidStakingChain.PHALA, - [LiquidStakingToken.GLMR]: LiquidStakingChain.MOONBEAM, - [LiquidStakingToken.ASTAR]: LiquidStakingChain.ASTAR, - [LiquidStakingToken.MANTA]: LiquidStakingChain.MANTA, - [LiquidStakingToken.TNT]: LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN, - }; +export type LiquidStakingChainDef = { + id: LiquidStakingChainId; + name: string; + token: LiquidStakingToken; + logo: StaticAssetPath; + networkName: string; + currency: TanglePrimitivesCurrencyTokenSymbol['type']; + decimals: number; +}; -export const LS_CHAIN_TO_LOGO: Record = { - [LiquidStakingChain.POLKADOT]: StaticAssetPath.LIQUID_STAKING_TOKEN_POLKADOT, - [LiquidStakingChain.PHALA]: StaticAssetPath.LIQUID_STAKING_TOKEN_PHALA, - [LiquidStakingChain.MOONBEAM]: StaticAssetPath.LIQUID_STAKING_TOKEN_GLIMMER, - [LiquidStakingChain.ASTAR]: StaticAssetPath.LIQUID_STAKING_TOKEN_ASTAR, - [LiquidStakingChain.MANTA]: StaticAssetPath.LIQUID_STAKING_TOKEN_MANTA, - [LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN]: - StaticAssetPath.LIQUID_STAKING_TANGLE_LOGO, +const POLKADOT: LiquidStakingChainDef = { + id: LiquidStakingChainId.POLKADOT, + name: 'Polkadot', + token: LiquidStakingToken.DOT, + logo: StaticAssetPath.LIQUID_STAKING_TOKEN_POLKADOT, + networkName: 'Polkadot Mainnet', + currency: 'Dot', + decimals: 10, }; -// TODO: Instead of mapping to names, map to network/chain definitions themselves. This avoids redundancy and relies on a centralized definition for the network/chain which is better, since it simplifies future refactoring. -export const LS_CHAIN_TO_NETWORK_NAME: Record = { - [LiquidStakingChain.POLKADOT]: 'Polkadot Mainnet', - [LiquidStakingChain.PHALA]: 'Phala', - [LiquidStakingChain.MOONBEAM]: 'Moonbeam', - [LiquidStakingChain.ASTAR]: 'Astar', - [LiquidStakingChain.MANTA]: 'Manta', - [LiquidStakingChain.TANGLE_RESTAKING_PARACHAIN]: 'Tangle Parachain', +const PHALA: LiquidStakingChainDef = { + id: LiquidStakingChainId.PHALA, + name: 'Phala', + token: LiquidStakingToken.PHALA, + logo: StaticAssetPath.LIQUID_STAKING_TOKEN_PHALA, + networkName: 'Phala', + currency: 'Pha', + decimals: 18, }; -export const LS_TOKEN_TO_CURRENCY: Record< - LiquidStakingToken, - TanglePrimitivesCurrencyTokenSymbol['type'] -> = { - [LiquidStakingToken.DOT]: 'Dot', - [LiquidStakingToken.PHALA]: 'Pha', +const MOONBEAM: LiquidStakingChainDef = { + id: LiquidStakingChainId.MOONBEAM, + name: 'Moonbeam', + token: LiquidStakingToken.GLMR, + logo: StaticAssetPath.LIQUID_STAKING_TOKEN_GLIMMER, + networkName: 'Moonbeam', // TODO: No currency entry for GLMR in the Tangle Primitives? - [LiquidStakingToken.GLMR]: 'Dot', + currency: 'Dot', + decimals: 18, +}; + +const ASTAR: LiquidStakingChainDef = { + id: LiquidStakingChainId.ASTAR, + name: 'Astar', + token: LiquidStakingToken.ASTAR, + logo: StaticAssetPath.LIQUID_STAKING_TOKEN_ASTAR, + networkName: 'Astar', + // TODO: No currency entry for ASTAR in the Tangle Primitives? + currency: 'Dot', + decimals: 18, +}; + +const MANTA: LiquidStakingChainDef = { + id: LiquidStakingChainId.MANTA, + name: 'Manta', + token: LiquidStakingToken.MANTA, + logo: StaticAssetPath.LIQUID_STAKING_TOKEN_MANTA, + networkName: 'Manta', // TODO: No currency entry for ASTAR in the Tangle Primitives? - [LiquidStakingToken.ASTAR]: 'Dot', - // TODO: No currency entry for MANTA in the Tangle Primitives? - [LiquidStakingToken.MANTA]: 'Dot', + currency: 'Dot', + decimals: 18, +}; + +const TANGLE_RESTAKING_PARACHAIN: LiquidStakingChainDef = { + id: LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN, + name: 'Tangle Parachain', + token: LiquidStakingToken.TNT, + logo: StaticAssetPath.LIQUID_STAKING_TANGLE_LOGO, + networkName: 'Tangle Parachain', // TODO: This is temporary until the Tangle Primitives are updated with the correct currency token symbol for TNT. - [LiquidStakingToken.TNT]: 'Bnc', + currency: 'Bnc', + decimals: 18, +}; + +export const LIQUID_STAKING_CHAIN_MAP: Record< + LiquidStakingChainId, + LiquidStakingChainDef +> = { + [LiquidStakingChainId.POLKADOT]: POLKADOT, + [LiquidStakingChainId.PHALA]: PHALA, + [LiquidStakingChainId.MOONBEAM]: MOONBEAM, + [LiquidStakingChainId.ASTAR]: ASTAR, + [LiquidStakingChainId.MANTA]: MANTA, + [LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN]: TANGLE_RESTAKING_PARACHAIN, +}; + +export const LIQUID_STAKING_CHAINS: LiquidStakingChainDef[] = Object.values( + LIQUID_STAKING_CHAIN_MAP, +); + +// TODO: Instead of mapping to names, map to network/chain definitions themselves. This avoids redundancy and relies on a centralized definition for the network/chain which is better, since it simplifies future refactoring. +export const LS_CHAIN_TO_NETWORK_NAME: Record = { + [LiquidStakingChainId.POLKADOT]: 'Polkadot Mainnet', + [LiquidStakingChainId.PHALA]: 'Phala', + [LiquidStakingChainId.MOONBEAM]: 'Moonbeam', + [LiquidStakingChainId.ASTAR]: 'Astar', + [LiquidStakingChainId.MANTA]: 'Manta', + [LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN]: 'Tangle Parachain', }; export const TVS_TOOLTIP = diff --git a/apps/tangle-dapp/hooks/useInputAmount.ts b/apps/tangle-dapp/hooks/useInputAmount.ts index b2cb7ee0d..635899b5e 100644 --- a/apps/tangle-dapp/hooks/useInputAmount.ts +++ b/apps/tangle-dapp/hooks/useInputAmount.ts @@ -64,6 +64,7 @@ const useInputAmount = ({ decimals, }: Options) => { const [errorMessage, setErrorMessage] = useState(null); + const [displayAmount, setDisplayAmount] = useState( amount !== null ? formatBn(amount, decimals, INPUT_AMOUNT_FORMAT) : '', ); From b229496ba78db81463cb51b16194cb5d0650e2d3 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:59:06 -0400 Subject: [PATCH 09/53] refactor(tangle-dapp): `AvatarGroup` already exists --- .../components/LiquidStaking/AvatarList.tsx | 15 --------------- .../LiquidStaking/StakedAssetsTable.tsx | 6 +++--- 2 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx b/apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx deleted file mode 100644 index 0fc138384..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/AvatarList.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { FC, ReactNode } from 'react'; - -export type AvatarListProps = { - children: ReactNode; -}; - -const AvatarList: FC = ({ children }) => { - return ( -
    - {children} -
    - ); -}; - -export default AvatarList; diff --git a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx index fc219f78a..746ea7afb 100644 --- a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx @@ -13,6 +13,7 @@ import { import { ExternalLinkLine, InformationLine } from '@webb-tools/icons'; import { Avatar, + AvatarGroup, fuzzyFilter, shortenString, Table, @@ -24,7 +25,6 @@ import { AnySubstrateAddress } from '../../types/utils'; import GlassCard from '../GlassCard'; import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; -import AvatarList from './AvatarList'; type StakedAssetItem = { id: HexString; @@ -58,11 +58,11 @@ const columns = [ ), cell: (props) => { return ( - + {props.getValue().map((address, index) => ( ))} - + ); }, }), From 74c136fa2eb00d7df04cc35ec9a963f37910685d Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:14:09 -0400 Subject: [PATCH 10/53] feat(tangle-dapp): Create `SelectTokenModal` component --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 33 +++++ .../LiquidStaking/AvailableWithdrawCard.tsx | 2 +- .../LiquidStaking/SelectTokenModal.tsx | 129 ++++++++++++++++++ .../LiquidStaking/UnstakeRequestsTable.tsx | 30 ++++ .../data/liquidStaking/useMintTx.ts | 7 +- 5 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx create mode 100644 apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index 0213e4a11..74080557f 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -1,9 +1,14 @@ +'use client'; + +import { BN } from '@polkadot/util'; import { notFound } from 'next/navigation'; import { FC } from 'react'; import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; +import SelectTokenModal from '../../../components/LiquidStaking/SelectTokenModal'; import StakedAssetsTable from '../../../components/LiquidStaking/StakedAssetsTable'; +import UnstakeRequestsTable from '../../../components/LiquidStaking/UnstakeRequestsTable'; import isLiquidStakingToken from '../../../utils/liquidStaking/isLiquidStakingToken'; type Props = { @@ -23,6 +28,34 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { + + + + void 0} + onTokenSelect={() => void 0} + />
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx b/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx index f0eca096a..c90b44bbb 100644 --- a/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx @@ -6,7 +6,7 @@ import GlassCard from '../GlassCard'; const AvailableWithdrawCard: FC = () => { return ( - +
diff --git a/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx b/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx new file mode 100644 index 000000000..47af1f4cb --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx @@ -0,0 +1,129 @@ +import { BN } from '@polkadot/util'; +import { ExternalLinkLine } from '@webb-tools/icons'; +import { + GITHUB_BUG_REPORT_URL, + Modal, + ModalContent, + ModalHeader, + Typography, +} from '@webb-tools/webb-ui-components'; +import { FC, useEffect } from 'react'; + +import { LiquidStakingChainId } from '../../constants/liquidStaking'; +import { AnySubstrateAddress } from '../../types/utils'; +import ChainLogo from './ChainLogo'; + +export type SelectTokenModalProps = { + isOpen: boolean; + options: Omit[]; + onTokenSelect: (address: AnySubstrateAddress) => void; + onClose: () => void; +}; + +const SelectTokenModal: FC = ({ + isOpen, + options, + onTokenSelect, + onClose, +}) => { + // Sanity check: Ensure all addresses are unique. + useEffect(() => { + const seenAddresses = new Set(); + + for (const option of options) { + if (seenAddresses.has(option.address)) { + console.warn( + `Duplicate token address found: ${option.address}, expected all addresses to be unique`, + ); + } + } + }, [options]); + + return ( + + + + Select Token + + +
+ {options.map((option) => { + return ( + + ); + })} + + {/* No tokens available */} + {options.length === 0 && ( +
+ + No tokens available + + + + Think this is a bug?{' '} + + Report it here + + +
+ )} +
+
+
+ ); +}; + +export type TokenListItemProps = { + address: AnySubstrateAddress; + amount: BN; + onClick: () => void; +}; + +/** @internal */ +const TokenListItem: FC = () => { + return ( +
+ {/* Information */} +
+ + +
+ + tgDOT_A + + + + + tgbe12...006 + + + + +
+
+ + {/* Amount */} + + 88.29 + +
+ ); +}; + +export default SelectTokenModal; diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx new file mode 100644 index 000000000..a203fcbdd --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx @@ -0,0 +1,30 @@ +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; + +import GlassCard from '../GlassCard'; + +const UnstakeRequestsTable: FC = () => { + return ( + + + + ); +}; + +const NoUnstakeRequestsNotice: FC = () => { + return ( +
+ + No unstake requests + + + + You will be able to claim your tokens after the unstake request has been + processed. To unstake your tokens go to the unstake tab to schedule + request. + +
+ ); +}; + +export default UnstakeRequestsTable; diff --git a/apps/tangle-dapp/data/liquidStaking/useMintTx.ts b/apps/tangle-dapp/data/liquidStaking/useMintTx.ts index 29ad3e81c..66c7832a8 100644 --- a/apps/tangle-dapp/data/liquidStaking/useMintTx.ts +++ b/apps/tangle-dapp/data/liquidStaking/useMintTx.ts @@ -20,14 +20,15 @@ const useMintTx = () => { return useSubstrateTxWithNotification( TxName.MINT, - (api, _activeSubstrateAddress, context) => + (api, _activeSubstrateAddress, context) => { // TODO: Investigate what the `remark` and `channel` parameters are for, and whether they are relevant for us here. - api.tx.lstMinting.mint( + return api.tx.lstMinting.mint( { Native: context.currency }, context.amount, Bytes.from([]), null, - ), + ); + }, undefined, TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint, ); From fd2ae7677ec8856bfc89d2157c9ced0ef3330a43 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:34:04 -0400 Subject: [PATCH 11/53] feat(tangle-dapp): Trigger opening the `Select Token` modal --- .../app/liquid-staking/[tokenSymbol]/page.tsx | 28 --------- .../LiquidStaking/LiquidStakeCard.tsx | 6 +- .../LiquidStaking/LiquidStakingInput.tsx | 23 +++++-- .../LiquidStaking/LiquidUnstakeCard.tsx | 25 +++++++- .../LiquidStaking/ParachainWalletBalance.tsx | 61 +++++++++++++++++++ .../LiquidStaking/SelectTokenModal.tsx | 30 +++++++-- .../LiquidStaking/WalletBalance.tsx | 17 ------ 7 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx delete mode 100644 apps/tangle-dapp/components/LiquidStaking/WalletBalance.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index 74080557f..0f2ebcfd3 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -1,12 +1,10 @@ 'use client'; -import { BN } from '@polkadot/util'; import { notFound } from 'next/navigation'; import { FC } from 'react'; import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; -import SelectTokenModal from '../../../components/LiquidStaking/SelectTokenModal'; import StakedAssetsTable from '../../../components/LiquidStaking/StakedAssetsTable'; import UnstakeRequestsTable from '../../../components/LiquidStaking/UnstakeRequestsTable'; import isLiquidStakingToken from '../../../utils/liquidStaking/isLiquidStakingToken'; @@ -30,32 +28,6 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => { - - void 0} - onTokenSelect={() => void 0} - />
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 2eb4467cf..f03125292 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -21,14 +21,15 @@ import { LIQUID_STAKING_CHAIN_MAP, LIQUID_STAKING_TOKEN_PREFIX, LiquidStakingChainId, + LiquidStakingToken, } from '../../constants/liquidStaking'; import useMintTx from '../../data/liquidStaking/useMintTx'; import useApi from '../../hooks/useApi'; import useApiRx from '../../hooks/useApiRx'; import { TxStatus } from '../../hooks/useSubstrateTx'; import LiquidStakingInput from './LiquidStakingInput'; +import ParachainWalletBalance from './ParachainWalletBalance'; import SelectValidators from './SelectValidators'; -import WalletBalance from './WalletBalance'; const LiquidStakeCard: FC = () => { const [fromAmount, setFromAmount] = useState(null); @@ -96,7 +97,8 @@ const LiquidStakeCard: FC = () => { amount={fromAmount} setAmount={setFromAmount} placeholder={`0 ${selectedChain.token}`} - rightElement={} + // TODO: Temporary. Use actual token. + rightElement={} setChain={setSelectedChainId} minAmount={minimumInputAmount ?? undefined} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx index e4a6ae57c..8b8deb6e7 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx @@ -37,6 +37,7 @@ export type LiquidStakingInputProps = { placeholder?: string; rightElement?: ReactNode; token: LiquidStakingToken; + onTokenClick?: () => void; isTokenLiquidVariant?: boolean; minAmount?: BN; }; @@ -44,15 +45,16 @@ export type LiquidStakingInputProps = { const LiquidStakingInput: FC = ({ id, amount, - setAmount, isReadOnly = false, placeholder = '0', isTokenLiquidVariant = false, rightElement, chain, - setChain, token, minAmount, + setAmount, + setChain, + onTokenClick, }) => { const minErrorMessage = ((): string | undefined => { if (minAmount === undefined) { @@ -117,7 +119,11 @@ const LiquidStakingInput: FC = ({ readOnly={isReadOnly} /> - +
@@ -133,16 +139,23 @@ const LiquidStakingInput: FC = ({ type TokenChipProps = { token: LiquidStakingToken; isLiquidVariant: boolean; + onClick?: () => void; }; /** @internal */ -const TokenChip: FC = ({ token, isLiquidVariant }) => { +const TokenChip: FC = ({ token, isLiquidVariant, onClick }) => { const chain = LIQUID_STAKING_CHAINS.find((chain) => chain.token === token); assert(chain !== undefined, 'All tokens should have a corresponding chain'); return ( -
+
diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx index 7d691839f..da9b7c487 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -19,15 +19,18 @@ import { LIQUID_STAKING_CHAIN_MAP, LIQUID_STAKING_TOKEN_PREFIX, LiquidStakingChainId, + LiquidStakingToken, } from '../../constants/liquidStaking'; import useRedeemTx from '../../data/liquidStaking/useRedeemTx'; import useApi from '../../hooks/useApi'; import useApiRx from '../../hooks/useApiRx'; import { TxStatus } from '../../hooks/useSubstrateTx'; import LiquidStakingInput from './LiquidStakingInput'; -import WalletBalance from './WalletBalance'; +import ParachainWalletBalance from './ParachainWalletBalance'; +import SelectTokenModal from './SelectTokenModal'; const LiquidUnstakeCard: FC = () => { + const [isSelectTokenModalOpen, setIsSelectTokenModalOpen] = useState(false); const [fromAmount, setFromAmount] = useState(null); // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. @@ -84,6 +87,15 @@ const LiquidUnstakeCard: FC = () => { return fromAmount.muln(rate); }, [fromAmount, rate]); + const handleTokenSelect = useCallback(() => { + setIsSelectTokenModalOpen(false); + }, []); + + const selectTokenModalOptions = useMemo(() => { + // TODO: Dummy data. + return [{ address: '0x123456' as any, amount: new BN(100), decimals: 18 }]; + }, []); + return ( <> { amount={fromAmount} setAmount={setFromAmount} placeholder={`0 ${LIQUID_STAKING_TOKEN_PREFIX}${selectedChain.token}`} - rightElement={} + // TODO: Temporary. Use actual token. + rightElement={} isTokenLiquidVariant minAmount={minimumInputAmount ?? undefined} + onTokenClick={() => setIsSelectTokenModalOpen(true)} /> @@ -156,6 +170,13 @@ const LiquidUnstakeCard: FC = () => { > Unstake + + setIsSelectTokenModalOpen(false)} + onTokenSelect={handleTokenSelect} + /> ); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx new file mode 100644 index 000000000..d80c0af37 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx @@ -0,0 +1,61 @@ +import { BN_ZERO, formatBalance } from '@polkadot/util'; +import { WalletLineIcon } from '@webb-tools/icons'; +import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components'; +import { FC, useMemo } from 'react'; + +import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; +import { LiquidStakingToken } from '../../constants/liquidStaking'; +import useParachainBalances from '../../data/liquidStaking/useParachainBalances'; +import useSubstrateAddress from '../../hooks/useSubstrateAddress'; + +export type ParachainWalletBalanceProps = { + isNative?: boolean; + token: LiquidStakingToken; +}; + +const ParachainWalletBalance: FC = ({ + isNative, + token, +}) => { + const activeSubstrateAddress = useSubstrateAddress(); + const { nativeBalances, liquidBalances } = useParachainBalances(); + const map = isNative ? nativeBalances : liquidBalances; + + const balance = (() => { + if (map === null) { + return null; + } + + return map.get(token) ?? BN_ZERO; + })(); + + const formattedBalance = useMemo(() => { + // No account is active. + if (activeSubstrateAddress === null) { + return EMPTY_VALUE_PLACEHOLDER; + } + // Balance is still loading. + else if (balance === null) { + return null; + } + + return formatBalance(balance); + }, [activeSubstrateAddress, balance]); + + return ( + + {' '} + {formattedBalance === null ? ( + + ) : ( + formattedBalance + )} + + ); +}; + +export default ParachainWalletBalance; diff --git a/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx b/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx index 47af1f4cb..06066f336 100644 --- a/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx @@ -1,13 +1,14 @@ -import { BN } from '@polkadot/util'; +import { BN, formatBalance } from '@polkadot/util'; import { ExternalLinkLine } from '@webb-tools/icons'; import { GITHUB_BUG_REPORT_URL, Modal, ModalContent, ModalHeader, + shortenString, Typography, } from '@webb-tools/webb-ui-components'; -import { FC, useEffect } from 'react'; +import { FC, useEffect, useMemo } from 'react'; import { LiquidStakingChainId } from '../../constants/liquidStaking'; import { AnySubstrateAddress } from '../../types/utils'; @@ -84,14 +85,31 @@ const SelectTokenModal: FC = ({ export type TokenListItemProps = { address: AnySubstrateAddress; + decimals: number; amount: BN; onClick: () => void; }; /** @internal */ -const TokenListItem: FC = () => { +const TokenListItem: FC = ({ + address, + decimals, + amount, + onClick, +}) => { + const formattedAmount = useMemo(() => { + return formatBalance(amount, { + withSi: true, + decimals, + withUnit: false, + }); + }, [amount, decimals]); + return ( -
+
{/* Information */}
= () => { className="flex items-center justify-center gap-1 hover:underline" > - tgbe12...006 + {shortenString(address, 6)} @@ -120,7 +138,7 @@ const TokenListItem: FC = () => { {/* Amount */} - 88.29 + {formattedAmount}
); diff --git a/apps/tangle-dapp/components/LiquidStaking/WalletBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/WalletBalance.tsx deleted file mode 100644 index 79ea5125b..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/WalletBalance.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { WalletLineIcon } from '@webb-tools/icons'; -import { Typography } from '@webb-tools/webb-ui-components'; -import { FC } from 'react'; - -const WalletBalance: FC = () => { - return ( - - 0.00 - - ); -}; - -export default WalletBalance; From f66e0099f770d8a55afd00cf0c2b7268d377c0cc Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:56:38 -0400 Subject: [PATCH 12/53] feat(tangle-dapp): Create `AddressLink` component --- apps/tangle-dapp/app/liquid-staking/page.tsx | 2 +- .../components/LiquidStaking/AddressLink.tsx | 30 ++++ .../LiquidStaking/SelectTokenModal.tsx | 14 +- .../LiquidStaking/StakedAssetsTable.tsx | 17 +-- .../LiquidStaking/UnstakeRequestsTable.tsx | 140 +++++++++++++++++- .../utils/calculateTimeRemaining.ts | 5 +- 6 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 167658b2c..826bdffe5 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -39,7 +39,7 @@ const LiquidStakingPage: FC = () => { = ({ address }) => { + // TODO: Determine href. + const href = '#'; + + return ( + + + {shortenString(address, 6)} + + + + + ); +}; + +export default AddressLink; diff --git a/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx b/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx index 06066f336..dd4e948cd 100644 --- a/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/SelectTokenModal.tsx @@ -1,17 +1,16 @@ import { BN, formatBalance } from '@polkadot/util'; -import { ExternalLinkLine } from '@webb-tools/icons'; import { GITHUB_BUG_REPORT_URL, Modal, ModalContent, ModalHeader, - shortenString, Typography, } from '@webb-tools/webb-ui-components'; import { FC, useEffect, useMemo } from 'react'; import { LiquidStakingChainId } from '../../constants/liquidStaking'; import { AnySubstrateAddress } from '../../types/utils'; +import AddressLink from './AddressLink'; import ChainLogo from './ChainLogo'; export type SelectTokenModalProps = { @@ -123,16 +122,7 @@ const TokenListItem: FC = ({ tgDOT_A - - - {shortenString(address, 6)} - - - - +
diff --git a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx index 746ea7afb..3080e8e86 100644 --- a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx @@ -10,12 +10,11 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table'; -import { ExternalLinkLine, InformationLine } from '@webb-tools/icons'; +import { InformationLine } from '@webb-tools/icons'; import { Avatar, AvatarGroup, fuzzyFilter, - shortenString, Table, Typography, } from '@webb-tools/webb-ui-components'; @@ -25,6 +24,7 @@ import { AnySubstrateAddress } from '../../types/utils'; import GlassCard from '../GlassCard'; import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; +import AddressLink from './AddressLink'; type StakedAssetItem = { id: HexString; @@ -38,18 +38,7 @@ const columns = [ columnHelper.accessor('id', { header: () => , cell: (props) => { - // TODO: Get proper href. - const href = '#'; - - return ( - - - {shortenString(props.getValue(), 3)} - - - - - ); + return ; }, }), columnHelper.accessor('validators', { diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx index a203fcbdd..b696b71e2 100644 --- a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx @@ -1,13 +1,145 @@ -import { Typography } from '@webb-tools/webb-ui-components'; +import { + createColumnHelper, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { CheckboxCircleFill, TimeLineIcon } from '@webb-tools/icons'; +import { + Button, + CheckBox, + fuzzyFilter, + Table, + Typography, +} from '@webb-tools/webb-ui-components'; +import BN from 'bn.js'; import { FC } from 'react'; +import { AnySubstrateAddress } from '../../types/utils'; +import calculateTimeRemaining from '../../utils/calculateTimeRemaining'; import GlassCard from '../GlassCard'; +import { HeaderCell } from '../tableCells'; +import TokenAmountCell from '../tableCells/TokenAmountCell'; +import AddressLink from './AddressLink'; + +type UnstakeRequestItem = { + address: AnySubstrateAddress; + amount: BN; + endTimestamp?: number; +}; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor('address', { + header: () => ( + + ), + cell: (props) => { + return ( +
+ void 0} + wrapperClassName="pt-0.5" + /> + + +
+ ); + }, + }), + columnHelper.accessor('endTimestamp', { + header: () => , + cell: (props) => { + const endTimestamp = props.getValue(); + + const timeRemaining = + endTimestamp === undefined + ? undefined + : calculateTimeRemaining(new Date(endTimestamp)); + + const content = + timeRemaining === undefined ? ( + + ) : ( +
+ {timeRemaining} +
+ ); + + return
{content}
; + }, + }), + columnHelper.accessor('amount', { + header: () => , + cell: (props) => { + return ; + }, + }), +]; const UnstakeRequestsTable: FC = () => { + // TODO: Mock data. + const data: UnstakeRequestItem[] = [ + { + address: '0x3a7f9e8c14b7d2f5' as any, + endTimestamp: undefined, + amount: new BN(100), + }, + { + address: '0xd5c4a2b1f3e8c7d9' as any, + endTimestamp: 1720659733, + amount: new BN(123), + }, + { + address: '0x9b3e47d8a5c2f1e4' as any, + endTimestamp: 1720859733, + amount: new BN(321), + }, + ]; + + const table = useReactTable({ + data, + columns, + filterFns: { + fuzzy: fuzzyFilter, + }, + globalFilterFn: fuzzyFilter, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + return ( - - - +
+ + {data.length === 0 ? ( + + ) : ( +
+ )} + + +
+ + + +
+ ); }; diff --git a/apps/tangle-dapp/utils/calculateTimeRemaining.ts b/apps/tangle-dapp/utils/calculateTimeRemaining.ts index ddf1f0c5d..c7d69cb01 100644 --- a/apps/tangle-dapp/utils/calculateTimeRemaining.ts +++ b/apps/tangle-dapp/utils/calculateTimeRemaining.ts @@ -1,10 +1,7 @@ import { formatDistance } from 'date-fns'; import capitalize from 'lodash/capitalize'; -function calculateTimeRemaining( - futureDate: Date, - currentDate?: Date, -): string | null { +function calculateTimeRemaining(futureDate: Date, currentDate?: Date): string { return capitalize(formatDistance(futureDate, currentDate ?? new Date())); } From 1c27a9197fc56a4c593745ebf73b4bc1760da432 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 10 Jul 2024 22:44:29 -0400 Subject: [PATCH 13/53] feat(tangle-dapp): Add `CancelUnstakeModal` --- .../LiquidStaking/AvailableWithdrawCard.tsx | 6 +- .../LiquidStaking/CancelUnstakeModal.tsx | 73 +++++++++++++++++++ .../components/LiquidStaking/ExternalLink.tsx | 33 +++++++++ .../components/LiquidStaking/ModalIcon.tsx | 44 +++++++++++ .../LiquidStaking/UnstakeRequestsTable.tsx | 49 ++++++------- libs/icons/src/TimeFillIcon.tsx | 11 +++ libs/icons/src/index.ts | 1 + 7 files changed, 186 insertions(+), 31 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx create mode 100644 apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx create mode 100644 apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx create mode 100644 libs/icons/src/TimeFillIcon.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx b/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx index c90b44bbb..f99fe93ba 100644 --- a/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/AvailableWithdrawCard.tsx @@ -1,4 +1,4 @@ -import { CheckboxCircleFill, TimeLineIcon, UndoIcon } from '@webb-tools/icons'; +import { CheckboxCircleFill, TimeFillIcon, UndoIcon } from '@webb-tools/icons'; import { Button, Typography } from '@webb-tools/webb-ui-components'; import { FC, ReactElement } from 'react'; @@ -45,14 +45,14 @@ const AvailableWithdrawCard: FC = () => { /> } + icon={} text="1" />
- + 1.00 tgDOT diff --git a/apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx b/apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx new file mode 100644 index 000000000..013c99b47 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx @@ -0,0 +1,73 @@ +import { CloseCircleLineIcon } from '@webb-tools/icons'; +import { + Button, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + Typography, +} from '@webb-tools/webb-ui-components'; +import { FC, useCallback } from 'react'; + +import ExternalLink from './ExternalLink'; +import ModalIcon from './ModalIcon'; +import { UnstakeRequestItem } from './UnstakeRequestsTable'; + +export type CancelUnstakeModalProps = { + isOpen: boolean; + unstakeRequest: UnstakeRequestItem; + onClose: () => void; +}; + +const CancelUnstakeModal: FC = ({ + isOpen, + unstakeRequest, + onClose, +}) => { + const handleConfirm = useCallback(() => { + // TODO: Set button as loading, disable ability to close modal, and proceed to execute the unstake cancellation via its corresponding extrinsic call. + }, []); + + return ( + + + + Cancel Unstake + + +
+ + + + Are you sure you want to cancel your unstaking request? By + cancelling, your tokens will remain staked and continue earning + rewards. + + + {/* TODO: External link's href. */} + Learn More +
+ + + + + + +
+
+ ); +}; + +export default CancelUnstakeModal; diff --git a/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx b/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx new file mode 100644 index 000000000..38099485d --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx @@ -0,0 +1,33 @@ +import { ExternalLinkLine } from '@webb-tools/icons'; +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC, ReactNode } from 'react'; + +export type ExternalLinkProps = { + href: string; + children: ReactNode | string; +}; + +const ExternalLink: FC = ({ href, children }) => { + return ( + + {typeof children === 'string' ? ( + + {children} + + ) : ( + children + )} + + + + ); +}; + +export default ExternalLink; diff --git a/apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx b/apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx new file mode 100644 index 000000000..a9fd7518c --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx @@ -0,0 +1,44 @@ +import { IconBase } from '@webb-tools/icons/types'; +import { FC, ReactNode } from 'react'; +import { twMerge } from 'tailwind-merge'; + +export enum ModalIconCommonVariant { + SUCCESS, +} + +export type ModalIconProps = { + Icon: (props: IconBase) => ReactNode; + className?: string; + iconClassName?: string; + commonVariant?: ModalIconCommonVariant; +}; + +const ModalIcon: FC = ({ + Icon, + className, + iconClassName, + commonVariant, +}) => { + return ( +
+ +
+ ); +}; + +export default ModalIcon; diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx index b696b71e2..b6edcda08 100644 --- a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx @@ -6,7 +6,7 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table'; -import { CheckboxCircleFill, TimeLineIcon } from '@webb-tools/icons'; +import { CheckboxCircleFill, TimeFillIcon } from '@webb-tools/icons'; import { Button, CheckBox, @@ -23,8 +23,9 @@ import GlassCard from '../GlassCard'; import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; import AddressLink from './AddressLink'; +import CancelUnstakeModal from './CancelUnstakeModal'; -type UnstakeRequestItem = { +export type UnstakeRequestItem = { address: AnySubstrateAddress; amount: BN; endTimestamp?: number; @@ -66,7 +67,7 @@ const columns = [ ) : (
- {timeRemaining} + {timeRemaining}
); @@ -83,23 +84,7 @@ const columns = [ const UnstakeRequestsTable: FC = () => { // TODO: Mock data. - const data: UnstakeRequestItem[] = [ - { - address: '0x3a7f9e8c14b7d2f5' as any, - endTimestamp: undefined, - amount: new BN(100), - }, - { - address: '0xd5c4a2b1f3e8c7d9' as any, - endTimestamp: 1720659733, - amount: new BN(123), - }, - { - address: '0x9b3e47d8a5c2f1e4' as any, - endTimestamp: 1720859733, - amount: new BN(321), - }, - ]; + const data: UnstakeRequestItem[] = []; const table = useReactTable({ data, @@ -130,15 +115,23 @@ const UnstakeRequestsTable: FC = () => { )} -
- + {data.length > 0 && ( +
+ + + +
+ )} - -
+ void 0} + unstakeRequest={null as any} + />
); }; diff --git a/libs/icons/src/TimeFillIcon.tsx b/libs/icons/src/TimeFillIcon.tsx new file mode 100644 index 000000000..79c4bfc18 --- /dev/null +++ b/libs/icons/src/TimeFillIcon.tsx @@ -0,0 +1,11 @@ +import { createIcon } from './create-icon'; +import { IconBase } from './types'; + +export const TimeFillIcon = (props: IconBase) => { + return createIcon({ + ...props, + viewBox: '0 0 14 14', + d: 'M6.75004 13.6668C3.06814 13.6668 0.083374 10.682 0.083374 7.00016C0.083374 3.31826 3.06814 0.333496 6.75004 0.333496C10.4319 0.333496 13.4167 3.31826 13.4167 7.00016C13.4167 10.682 10.4319 13.6668 6.75004 13.6668ZM7.41671 7.00016V3.66683H6.08337V8.3335H10.0834V7.00016H7.41671Z', + displayName: 'TimeFillIcon', + }); +}; diff --git a/libs/icons/src/index.ts b/libs/icons/src/index.ts index ba344efec..9e1724ff4 100644 --- a/libs/icons/src/index.ts +++ b/libs/icons/src/index.ts @@ -146,6 +146,7 @@ export { default as WebbLogoIcon } from './WebbLogoIcon'; export * from './YouTubeFill'; export * from './WaterDropletIcon'; export * from './UndoIcon'; +export * from './TimeFillIcon'; // Wallet icons export * from './wallets'; From c7670dbe027b2856ab5107d2fb012457619bd6de Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 10 Jul 2024 22:58:07 -0400 Subject: [PATCH 14/53] refactor(tangle-dapp): There's already a component for external links --- .../components/LiquidStaking/ExternalLink.tsx | 25 ++++++------------- .../components/LiquidStaking/ModalIcon.tsx | 6 ++--- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx b/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx index 38099485d..a5edc39e6 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx @@ -1,5 +1,5 @@ import { ExternalLinkLine } from '@webb-tools/icons'; -import { Typography } from '@webb-tools/webb-ui-components'; +import { Button, Typography } from '@webb-tools/webb-ui-components'; import { FC, ReactNode } from 'react'; export type ExternalLinkProps = { @@ -9,24 +9,15 @@ export type ExternalLinkProps = { const ExternalLink: FC = ({ href, children }) => { return ( - } > - {typeof children === 'string' ? ( - - {children} - - ) : ( - children - )} - - - + {children} + ); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx b/apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx index a9fd7518c..f24813974 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ModalIcon.tsx @@ -22,16 +22,14 @@ const ModalIcon: FC = ({ return (
Date: Wed, 10 Jul 2024 23:21:20 -0400 Subject: [PATCH 15/53] style(tangle-dapp): Add actions to `UnstakeRequestsTable` --- .../components/LiquidStaking/IconButton.tsx | 42 +++++++++++++++ .../LiquidStaking/UnstakeRequestsTable.tsx | 51 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/IconButton.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx b/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx new file mode 100644 index 000000000..755933895 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx @@ -0,0 +1,42 @@ +import { IconBase } from '@webb-tools/icons/types'; +import { + Button, + Tooltip, + TooltipBody, + TooltipTrigger, +} from '@webb-tools/webb-ui-components'; +import { FC, ReactNode } from 'react'; +import { twMerge } from 'tailwind-merge'; + +export type IconButtonProps = { + tooltip: string; + Icon: (props: IconBase) => ReactNode; + className?: string; + onClick: () => void; +}; + +const IconButton: FC = ({ + tooltip, + Icon, + className, + onClick, +}) => { + return ( + + + + + + {tooltip} + + ); +}; + +export default IconButton; diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx index b6edcda08..4c7fbf90d 100644 --- a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx @@ -6,7 +6,12 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table'; -import { CheckboxCircleFill, TimeFillIcon } from '@webb-tools/icons'; +import { + CheckboxCircleFill, + Close, + TimeFillIcon, + WalletLineIcon, +} from '@webb-tools/icons'; import { Button, CheckBox, @@ -24,6 +29,7 @@ import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; import AddressLink from './AddressLink'; import CancelUnstakeModal from './CancelUnstakeModal'; +import IconButton from './IconButton'; export type UnstakeRequestItem = { address: AnySubstrateAddress; @@ -80,11 +86,52 @@ const columns = [ return ; }, }), + columnHelper.display({ + id: 'actions', + header: () => , + cell: (_props) => { + return ( +
+ {/* TODO: Implement onClick. */} + void 0} /> + + {/* TODO: Implement onClick. */} + void 0} + /> +
+ ); + }, + enableSorting: false, + }), ]; const UnstakeRequestsTable: FC = () => { // TODO: Mock data. - const data: UnstakeRequestItem[] = []; + const data: UnstakeRequestItem[] = [ + { + address: '0x123456' as any, + amount: new BN(100), + endTimestamp: Date.now() + 1000 * 60 * 60 * 24, + }, + { + address: '0x123456' as any, + amount: new BN(100), + endTimestamp: Date.now() + 1000 * 60 * 60 * 24, + }, + { + address: '0x123456' as any, + amount: new BN(100), + endTimestamp: Date.now() + 1000 * 60 * 60 * 24, + }, + { + address: '0x123456' as any, + amount: new BN(100), + endTimestamp: Date.now() + 1000 * 60 * 60 * 24, + }, + ]; const table = useReactTable({ data, From 9042755a57f06cee7e1eebd8f2d3aa0dc1d04fff Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 11 Jul 2024 23:58:28 -0400 Subject: [PATCH 16/53] style(tangle-dapp): Tidy up UI for unstake card --- .../components/LiquidStaking/AddressLink.tsx | 2 + .../components/LiquidStaking/ChainLogo.tsx | 14 ++++- .../components/LiquidStaking/ExternalLink.tsx | 12 +++- .../components/LiquidStaking/IconButton.tsx | 2 +- .../LiquidStaking/LiquidStakingInput.tsx | 49 +--------------- .../LiquidStaking/LiquidUnstakeCard.tsx | 8 ++- .../LiquidStaking/ParachainWalletBalance.tsx | 4 +- .../components/LiquidStaking/TokenChip.tsx | 56 +++++++++++++++++++ .../LiquidStaking/UnstakeRequestsTable.tsx | 21 ++++--- 9 files changed, 101 insertions(+), 67 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/TokenChip.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx b/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx index 3163ad24f..751107c29 100644 --- a/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx @@ -14,8 +14,10 @@ const AddressLink: FC = ({ address }) => { const href = '#'; return ( + // TODO: Need to prevent clicking this link causing the token to be chosen. Instead, it should only open the address in a new tab. diff --git a/apps/tangle-dapp/components/LiquidStaking/ChainLogo.tsx b/apps/tangle-dapp/components/LiquidStaking/ChainLogo.tsx index 3bce0e49a..2dc86655f 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ChainLogo.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ChainLogo.tsx @@ -10,7 +10,7 @@ import { export type ChainLogoSize = 'sm' | 'md'; export type ChainLogoProps = { - chainId: LiquidStakingChainId; + chainId?: LiquidStakingChainId; size: ChainLogoSize; isRounded?: boolean; }; @@ -57,6 +57,18 @@ const ChainLogo: FC = ({ }) => { const sizeNumber = getSizeNumber(size); + // In case the chain id is not provided, render a placeholder. + if (chainId === undefined) { + return ( +
+ ); + } + return ( ReactNode; }; -const ExternalLink: FC = ({ href, children }) => { +const ExternalLink: FC = ({ + href, + children, + Icon = ExternalLinkLine, +}) => { return ( diff --git a/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx b/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx index 755933895..75cdab856 100644 --- a/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx @@ -28,7 +28,7 @@ const IconButton: FC = ({ onClick={onClick} size="sm" variant="utility" - className={twMerge('', className)} + className={twMerge('px-2', className)} > diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx index 8b8deb6e7..dd34a3966 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx @@ -11,12 +11,10 @@ import { Typography, } from '@webb-tools/webb-ui-components'; import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea'; -import assert from 'assert'; -import { FC, ReactNode, useCallback } from 'react'; +import { FC, ReactNode } from 'react'; import { twMerge } from 'tailwind-merge'; import { - LIQUID_STAKING_CHAINS, LIQUID_STAKING_TOKEN_PREFIX, LiquidStakingChainId, LiquidStakingToken, @@ -26,6 +24,7 @@ import useInputAmount from '../../hooks/useInputAmount'; import formatBn from '../../utils/formatBn'; import ChainLogo from './ChainLogo'; import HoverButtonStyle from './HoverButtonStyle'; +import TokenChip from './TokenChip'; export type LiquidStakingInputProps = { id: string; @@ -78,15 +77,6 @@ const LiquidStakingInput: FC = ({ minErrorMessage, }); - const handleChainChange = useCallback( - (newChain: LiquidStakingChainId) => { - if (setChain !== undefined) { - setChain(newChain); - } - }, - [setChain], - ); - const isError = errorMessage !== null; return ( @@ -98,10 +88,7 @@ const LiquidStakingInput: FC = ({ )} >
- + {rightElement}
@@ -136,36 +123,6 @@ const LiquidStakingInput: FC = ({ ); }; -type TokenChipProps = { - token: LiquidStakingToken; - isLiquidVariant: boolean; - onClick?: () => void; -}; - -/** @internal */ -const TokenChip: FC = ({ token, isLiquidVariant, onClick }) => { - const chain = LIQUID_STAKING_CHAINS.find((chain) => chain.token === token); - - assert(chain !== undefined, 'All tokens should have a corresponding chain'); - - return ( -
- - - - {isLiquidVariant && LIQUID_STAKING_TOKEN_PREFIX} - {token} - -
- ); -}; - type ChainSelectorProps = { selectedChain: LiquidStakingChainId; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx index da9b7c487..3bd491a39 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -19,7 +19,6 @@ import { LIQUID_STAKING_CHAIN_MAP, LIQUID_STAKING_TOKEN_PREFIX, LiquidStakingChainId, - LiquidStakingToken, } from '../../constants/liquidStaking'; import useRedeemTx from '../../data/liquidStaking/useRedeemTx'; import useApi from '../../hooks/useApi'; @@ -96,6 +95,10 @@ const LiquidUnstakeCard: FC = () => { return [{ address: '0x123456' as any, amount: new BN(100), decimals: 18 }]; }, []); + const balance = ( + + ); + return ( <> { amount={fromAmount} setAmount={setFromAmount} placeholder={`0 ${LIQUID_STAKING_TOKEN_PREFIX}${selectedChain.token}`} - // TODO: Temporary. Use actual token. - rightElement={} + rightElement={balance} isTokenLiquidVariant minAmount={minimumInputAmount ?? undefined} onTokenClick={() => setIsSelectTokenModalOpen(true)} diff --git a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx index d80c0af37..194e17964 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx @@ -14,7 +14,7 @@ export type ParachainWalletBalanceProps = { }; const ParachainWalletBalance: FC = ({ - isNative, + isNative = true, token, }) => { const activeSubstrateAddress = useSubstrateAddress(); @@ -50,7 +50,7 @@ const ParachainWalletBalance: FC = ({ > {' '} {formattedBalance === null ? ( - + ) : ( formattedBalance )} diff --git a/apps/tangle-dapp/components/LiquidStaking/TokenChip.tsx b/apps/tangle-dapp/components/LiquidStaking/TokenChip.tsx new file mode 100644 index 000000000..cdd191ef7 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/TokenChip.tsx @@ -0,0 +1,56 @@ +import { ChevronDown } from '@webb-tools/icons'; +import { Typography } from '@webb-tools/webb-ui-components'; +import assert from 'assert'; +import { FC } from 'react'; +import { twMerge } from 'tailwind-merge'; + +import { + LIQUID_STAKING_CHAINS, + LIQUID_STAKING_TOKEN_PREFIX, + LiquidStakingToken, +} from '../../constants/liquidStaking'; +import ChainLogo from './ChainLogo'; + +type TokenChipProps = { + token?: LiquidStakingToken; + isLiquidVariant: boolean; + onClick?: () => void; +}; + +const TokenChip: FC = ({ token, isLiquidVariant, onClick }) => { + const chain = (() => { + if (token === undefined) { + return null; + } + + const result = LIQUID_STAKING_CHAINS.find((chain) => chain.token === token); + + assert( + result !== undefined, + 'All tokens should have a corresponding chain', + ); + + return result; + })(); + + return ( +
+ + + + {isLiquidVariant && LIQUID_STAKING_TOKEN_PREFIX} + {token} + + + {onClick !== undefined && } +
+ ); +}; + +export default TokenChip; diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx index 4c7fbf90d..872b8db3d 100644 --- a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestsTable.tsx @@ -7,16 +7,17 @@ import { useReactTable, } from '@tanstack/react-table'; import { + ArrowRightUp, CheckboxCircleFill, Close, TimeFillIcon, WalletLineIcon, } from '@webb-tools/icons'; import { - Button, CheckBox, fuzzyFilter, Table, + TANGLE_DOCS_URL, Typography, } from '@webb-tools/webb-ui-components'; import BN from 'bn.js'; @@ -29,6 +30,7 @@ import { HeaderCell } from '../tableCells'; import TokenAmountCell from '../tableCells/TokenAmountCell'; import AddressLink from './AddressLink'; import CancelUnstakeModal from './CancelUnstakeModal'; +import ExternalLink from './ExternalLink'; import IconButton from './IconButton'; export type UnstakeRequestItem = { @@ -162,20 +164,17 @@ const UnstakeRequestsTable: FC = () => { )} - {data.length > 0 && ( -
- - - + {data.length === 0 && ( +
+ + View Docs +
)} + {/* TODO: Handle this modal properly. */} void 0} unstakeRequest={null as any} /> From 249742149585018b73d769f8499bfe233a3910f5 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 12 Jul 2024 00:06:50 -0400 Subject: [PATCH 17/53] feat(tangle-dapp): Check available balance --- .../components/LiquidStaking/LiquidStakeCard.tsx | 13 ++++++++++++- .../components/LiquidStaking/LiquidStakingInput.tsx | 13 +++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index f03125292..2e6433e1c 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -4,7 +4,7 @@ // the `lstMinting` pallet for this file only. import '@webb-tools/tangle-restaking-types'; -import { BN } from '@polkadot/util'; +import { BN, BN_ZERO } from '@polkadot/util'; import { ArrowDownIcon } from '@radix-ui/react-icons'; import { InformationLine, Search } from '@webb-tools/icons'; import { @@ -24,6 +24,7 @@ import { LiquidStakingToken, } from '../../constants/liquidStaking'; import useMintTx from '../../data/liquidStaking/useMintTx'; +import useParachainBalances from '../../data/liquidStaking/useParachainBalances'; import useApi from '../../hooks/useApi'; import useApiRx from '../../hooks/useApiRx'; import { TxStatus } from '../../hooks/useSubstrateTx'; @@ -42,6 +43,7 @@ const LiquidStakeCard: FC = () => { ); const { execute: executeMintTx, status: mintTxStatus } = useMintTx(); + const { nativeBalances } = useParachainBalances(); const selectedChain = LIQUID_STAKING_CHAIN_MAP[selectedChainId]; @@ -69,6 +71,14 @@ const LiquidStakeCard: FC = () => { return BN.max(minimumMintingAmount, existentialDepositAmount); }, [existentialDepositAmount, minimumMintingAmount]); + const maximumInputAmount = useMemo(() => { + if (nativeBalances === null) { + return null; + } + + return nativeBalances.get(selectedChain.token) ?? BN_ZERO; + }, [nativeBalances, selectedChain.token]); + const handleStakeClick = useCallback(() => { if (executeMintTx === null || fromAmount === null) { return; @@ -101,6 +111,7 @@ const LiquidStakeCard: FC = () => { rightElement={} setChain={setSelectedChainId} minAmount={minimumInputAmount ?? undefined} + maxAmount={maximumInputAmount ?? undefined} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx index dd34a3966..e51a3133d 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx @@ -20,6 +20,7 @@ import { LiquidStakingToken, LS_CHAIN_TO_NETWORK_NAME, } from '../../constants/liquidStaking'; +import { ERROR_NOT_ENOUGH_BALANCE } from '../../containers/ManageProfileModalContainer/Independent/IndependentAllocationInput'; import useInputAmount from '../../hooks/useInputAmount'; import formatBn from '../../utils/formatBn'; import ChainLogo from './ChainLogo'; @@ -29,16 +30,17 @@ import TokenChip from './TokenChip'; export type LiquidStakingInputProps = { id: string; chain: LiquidStakingChainId; - setChain?: (newChain: LiquidStakingChainId) => void; amount: BN | null; - setAmount?: (newAmount: BN | null) => void; isReadOnly?: boolean; placeholder?: string; rightElement?: ReactNode; token: LiquidStakingToken; - onTokenClick?: () => void; isTokenLiquidVariant?: boolean; minAmount?: BN; + maxAmount?: BN; + setAmount?: (newAmount: BN | null) => void; + setChain?: (newChain: LiquidStakingChainId) => void; + onTokenClick?: () => void; }; const LiquidStakingInput: FC = ({ @@ -51,6 +53,7 @@ const LiquidStakingInput: FC = ({ chain, token, minAmount, + maxAmount, setAmount, setChain, onTokenClick, @@ -75,6 +78,8 @@ const LiquidStakingInput: FC = ({ decimals: TANGLE_TOKEN_DECIMALS, min: minAmount, minErrorMessage, + max: maxAmount, + maxErrorMessage: ERROR_NOT_ENOUGH_BALANCE, }); const isError = errorMessage !== null; @@ -114,7 +119,7 @@ const LiquidStakingInput: FC = ({
- {errorMessage && ( + {errorMessage !== null && ( * {errorMessage} From 6b7a050bcb77d37cefeaa723775b0411ba4fdc43 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 12 Jul 2024 00:24:34 -0400 Subject: [PATCH 18/53] refactor(tangle-dapp): Only show tooltip when appropriate --- .../LiquidStaking/LiquidStakeCard.tsx | 11 ++++- .../LiquidStaking/LiquidStakingInput.tsx | 4 +- .../LiquidStaking/LiquidUnstakeCard.tsx | 24 +++++++++-- .../LiquidStaking/ParachainWalletBalance.tsx | 41 +++++++++++++++++-- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 2e6433e1c..98d9e4f5e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -98,6 +98,14 @@ const LiquidStakeCard: FC = () => { return fromAmount.muln(rate); }, [fromAmount, rate]); + // TODO: Temporary. Use actual token. + const walletBalance = ( + + ); + return ( <> { amount={fromAmount} setAmount={setFromAmount} placeholder={`0 ${selectedChain.token}`} - // TODO: Temporary. Use actual token. - rightElement={} + rightElement={walletBalance} setChain={setSelectedChainId} minAmount={minimumInputAmount ?? undefined} maxAmount={maximumInputAmount ?? undefined} diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx index e51a3133d..6b87e8ef0 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx @@ -38,6 +38,7 @@ export type LiquidStakingInputProps = { isTokenLiquidVariant?: boolean; minAmount?: BN; maxAmount?: BN; + maxErrorMessage?: string; setAmount?: (newAmount: BN | null) => void; setChain?: (newChain: LiquidStakingChainId) => void; onTokenClick?: () => void; @@ -54,6 +55,7 @@ const LiquidStakingInput: FC = ({ token, minAmount, maxAmount, + maxErrorMessage = ERROR_NOT_ENOUGH_BALANCE, setAmount, setChain, onTokenClick, @@ -79,7 +81,7 @@ const LiquidStakingInput: FC = ({ min: minAmount, minErrorMessage, max: maxAmount, - maxErrorMessage: ERROR_NOT_ENOUGH_BALANCE, + maxErrorMessage, }); const isError = errorMessage !== null; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx index 3bd491a39..0c61efacb 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -4,7 +4,7 @@ // the `lstMinting` pallet for this file only. import '@webb-tools/tangle-restaking-types'; -import { BN } from '@polkadot/util'; +import { BN, BN_ZERO } from '@polkadot/util'; import { ArrowDownIcon } from '@radix-ui/react-icons'; import { InformationLine } from '@webb-tools/icons'; import { @@ -20,6 +20,7 @@ import { LIQUID_STAKING_TOKEN_PREFIX, LiquidStakingChainId, } from '../../constants/liquidStaking'; +import useParachainBalances from '../../data/liquidStaking/useParachainBalances'; import useRedeemTx from '../../data/liquidStaking/useRedeemTx'; import useApi from '../../hooks/useApi'; import useApiRx from '../../hooks/useApiRx'; @@ -40,6 +41,7 @@ const LiquidUnstakeCard: FC = () => { ); const { execute: executeRedeemTx, status: redeemTxStatus } = useRedeemTx(); + const { nativeBalances } = useParachainBalances(); const selectedChain = LIQUID_STAKING_CHAIN_MAP[selectedChainId]; @@ -67,6 +69,14 @@ const LiquidUnstakeCard: FC = () => { return BN.max(minimumRedeemAmount, existentialDepositAmount); }, [existentialDepositAmount, minimumRedeemAmount]); + const maximumInputAmount = useMemo(() => { + if (nativeBalances === null) { + return null; + } + + return nativeBalances.get(selectedChain.token) ?? BN_ZERO; + }, [nativeBalances, selectedChain.token]); + const handleUnstakeClick = useCallback(() => { if (executeRedeemTx === null || fromAmount === null) { return; @@ -95,8 +105,12 @@ const LiquidUnstakeCard: FC = () => { return [{ address: '0x123456' as any, amount: new BN(100), decimals: 18 }]; }, []); - const balance = ( - + const stakedBalance = ( + ); return ( @@ -108,9 +122,11 @@ const LiquidUnstakeCard: FC = () => { amount={fromAmount} setAmount={setFromAmount} placeholder={`0 ${LIQUID_STAKING_TOKEN_PREFIX}${selectedChain.token}`} - rightElement={balance} + rightElement={stakedBalance} isTokenLiquidVariant minAmount={minimumInputAmount ?? undefined} + maxAmount={maximumInputAmount ?? undefined} + maxErrorMessage="Not enough stake to redeem" onTokenClick={() => setIsSelectTokenModalOpen(true)} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx index 194e17964..4d7b91f02 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx @@ -1,7 +1,14 @@ import { BN_ZERO, formatBalance } from '@polkadot/util'; import { WalletLineIcon } from '@webb-tools/icons'; -import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components'; +import { + SkeletonLoader, + Tooltip, + TooltipBody, + TooltipTrigger, + Typography, +} from '@webb-tools/webb-ui-components'; import { FC, useMemo } from 'react'; +import { twMerge } from 'tailwind-merge'; import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import { LiquidStakingToken } from '../../constants/liquidStaking'; @@ -11,11 +18,17 @@ import useSubstrateAddress from '../../hooks/useSubstrateAddress'; export type ParachainWalletBalanceProps = { isNative?: boolean; token: LiquidStakingToken; + tooltip?: string; + onlyShowTooltipWhenBalanceIsSet?: boolean; + onClick?: () => void; }; const ParachainWalletBalance: FC = ({ isNative = true, token, + tooltip, + onlyShowTooltipWhenBalanceIsSet = true, + onClick, }) => { const activeSubstrateAddress = useSubstrateAddress(); const { nativeBalances, liquidBalances } = useParachainBalances(); @@ -42,11 +55,15 @@ const ParachainWalletBalance: FC = ({ return formatBalance(balance); }, [activeSubstrateAddress, balance]); - return ( + const content = ( {' '} {formattedBalance === null ? ( @@ -56,6 +73,24 @@ const ParachainWalletBalance: FC = ({ )} ); + + const shouldShowTooltip = + onlyShowTooltipWhenBalanceIsSet && balance !== null && balance.isZero(); + + if (tooltip === undefined || shouldShowTooltip) { + return content; + } + + // Otherwise, the tooltip is set and it should be shown. + return ( + + {content} + + + {tooltip} + + + ); }; export default ParachainWalletBalance; From 78fd809fad2b7ef38bb92fdc5bc8477fef8ba0cb Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sat, 13 Jul 2024 00:45:12 -0400 Subject: [PATCH 19/53] feat(tangle-dapp): Add `Request Submitted` modal --- .../LiquidStaking/LiquidStakeCard.tsx | 1 + .../LiquidStaking/LiquidUnstakeCard.tsx | 11 +++ .../LiquidStaking/ParachainWalletBalance.tsx | 5 +- .../UnstakeRequestSubmittedModal.tsx | 76 +++++++++++++++++++ apps/tangle-dapp/hooks/useInputAmount.ts | 27 ++++++- 5 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 98d9e4f5e..611da7622 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -103,6 +103,7 @@ const LiquidStakeCard: FC = () => { setFromAmount(maximumInputAmount)} /> ); diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx index 0c61efacb..ec24f0bec 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -28,11 +28,15 @@ import { TxStatus } from '../../hooks/useSubstrateTx'; import LiquidStakingInput from './LiquidStakingInput'; import ParachainWalletBalance from './ParachainWalletBalance'; import SelectTokenModal from './SelectTokenModal'; +import UnstakeRequestSubmittedModal from './UnstakeRequestSubmittedModal'; const LiquidUnstakeCard: FC = () => { const [isSelectTokenModalOpen, setIsSelectTokenModalOpen] = useState(false); const [fromAmount, setFromAmount] = useState(null); + const [isRequestSubmittedModalOpen, setIsRequestSubmittedModalOpen] = + useState(false); + // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. const [rate] = useState(1.0); @@ -110,6 +114,7 @@ const LiquidUnstakeCard: FC = () => { isNative={false} token={selectedChain.token} tooltip="Click to use all staked balance" + onClick={() => setFromAmount(maximumInputAmount)} /> ); @@ -195,6 +200,12 @@ const LiquidUnstakeCard: FC = () => { onClose={() => setIsSelectTokenModalOpen(false)} onTokenSelect={handleTokenSelect} /> + + setIsRequestSubmittedModalOpen(false)} + unstakeRequest={null as any} + /> ); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx index 4d7b91f02..6ffb7a210 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx @@ -74,10 +74,9 @@ const ParachainWalletBalance: FC = ({
); - const shouldShowTooltip = - onlyShowTooltipWhenBalanceIsSet && balance !== null && balance.isZero(); + const shouldShowTooltip = onlyShowTooltipWhenBalanceIsSet && balance !== null; - if (tooltip === undefined || shouldShowTooltip) { + if (tooltip === undefined || !shouldShowTooltip) { return content; } diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx new file mode 100644 index 000000000..fabd03112 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx @@ -0,0 +1,76 @@ +import { CheckboxCircleLine, WalletLineIcon } from '@webb-tools/icons'; +import { + Button, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + Typography, +} from '@webb-tools/webb-ui-components'; +import { FC, useCallback } from 'react'; + +import ExternalLink from './ExternalLink'; +import ModalIcon, { ModalIconCommonVariant } from './ModalIcon'; +import { UnstakeRequestItem } from './UnstakeRequestsTable'; + +export type UnstakeRequestSubmittedModalProps = { + isOpen: boolean; + unstakeRequest: UnstakeRequestItem; + onClose: () => void; +}; + +const UnstakeRequestSubmittedModal: FC = ({ + isOpen, + unstakeRequest, + onClose, +}) => { + const handleAddTokenToWallet = useCallback(() => { + // TODO: Handle this case. + }, []); + + return ( + + + + Unstake Request Submitted + + +
+ + + + After the unbonding period, you can redeem this Unstake NFT to + withdraw unstaked tokens. + + + {/* TODO: External link's href. */} + Learn More +
+ + + + +
+
+ ); +}; + +export default UnstakeRequestSubmittedModal; diff --git a/apps/tangle-dapp/hooks/useInputAmount.ts b/apps/tangle-dapp/hooks/useInputAmount.ts index 635899b5e..3a2e5411c 100644 --- a/apps/tangle-dapp/hooks/useInputAmount.ts +++ b/apps/tangle-dapp/hooks/useInputAmount.ts @@ -47,10 +47,10 @@ type Options = { min?: BN | null; max?: BN | null; errorOnEmptyValue?: boolean; - setAmount?: (newAmount: BN | null) => void; minErrorMessage?: string; maxErrorMessage?: string; decimals: number; + setAmount?: (newAmount: BN | null) => void; }; const useInputAmount = ({ @@ -58,10 +58,10 @@ const useInputAmount = ({ min = null, max = null, errorOnEmptyValue = false, - setAmount, minErrorMessage, maxErrorMessage, decimals, + setAmount, }: Options) => { const [errorMessage, setErrorMessage] = useState(null); @@ -130,10 +130,33 @@ const useInputAmount = ({ } }, [amount]); + const trySetAmount = useCallback( + (newAmount: BN): boolean => { + // Only accept the new amount if it is within the min and max bounds. + if (max !== null && newAmount.gt(max)) { + return false; + } else if (min !== null && newAmount.lt(min)) { + return false; + } + // No closure was provided to set the new amount. + else if (setAmount === undefined) { + return false; + } + + setAmount(newAmount); + + // TODO: Update the display amount to reflect the new amount. Must format the BN to a string. + + return true; + }, + [max, min, setAmount], + ); + return { displayAmount, errorMessage, handleChange, + trySetAmount, }; }; From 77069e5a5fda47903beb4d57ced3ada15a266156 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 14 Jul 2024 02:25:15 -0400 Subject: [PATCH 20/53] refactor(tangle-dapp): Hide unused components --- apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx | 6 ------ .../components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx | 2 +- .../components/LiquidStaking/LiquidStakeCard.tsx | 5 +---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx index 0f2ebcfd3..e6922744a 100644 --- a/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx @@ -3,9 +3,7 @@ import { notFound } from 'next/navigation'; import { FC } from 'react'; -import AvailableWithdrawCard from '../../../components/LiquidStaking/AvailableWithdrawCard'; import LiquidStakeAndUnstakeCards from '../../../components/LiquidStaking/LiquidStakeAndUnstakeCards'; -import StakedAssetsTable from '../../../components/LiquidStaking/StakedAssetsTable'; import UnstakeRequestsTable from '../../../components/LiquidStaking/UnstakeRequestsTable'; import isLiquidStakingToken from '../../../utils/liquidStaking/isLiquidStakingToken'; @@ -23,10 +21,6 @@ const LiquidStakingTokenPage: FC = ({ params: { tokenSymbol } }) => {
- - - -
); diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx index 1af8d40b4..63331a52f 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeAndUnstakeCards.tsx @@ -13,7 +13,7 @@ const LiquidStakeAndUnstakeCards: FC = () => { const unselectedClass = 'text-mono-100 dark:text-mono-100'; return ( -
+
setIsStaking(true)} diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 611da7622..406fa1bf2 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -21,7 +21,6 @@ import { LIQUID_STAKING_CHAIN_MAP, LIQUID_STAKING_TOKEN_PREFIX, LiquidStakingChainId, - LiquidStakingToken, } from '../../constants/liquidStaking'; import useMintTx from '../../data/liquidStaking/useMintTx'; import useParachainBalances from '../../data/liquidStaking/useParachainBalances'; @@ -98,10 +97,9 @@ const LiquidStakeCard: FC = () => { return fromAmount.muln(rate); }, [fromAmount, rate]); - // TODO: Temporary. Use actual token. const walletBalance = ( setFromAmount(maximumInputAmount)} /> @@ -139,7 +137,6 @@ const LiquidStakeCard: FC = () => {
From 11ca714e0db4334925fa0572e51322c195996f6d Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 14 Jul 2024 20:56:40 -0400 Subject: [PATCH 21/53] feat(tangle-dapp): Implement some basic LST hooks --- .../components/LiquidStaking/DetailItem.tsx | 43 ++++++++++++++ .../LiquidStaking/LiquidStakeCard.tsx | 49 +++------------- .../LiquidStaking/LiquidUnstakeCard.tsx | 25 +++++++- .../LiquidStaking/MintAndRedeemDetailItem.tsx | 57 +++++++++++++++++++ .../useDelegationsOccupiedStatus.ts | 15 +++++ .../liquidStaking/useMintAndRedeemFees.ts | 27 +++++++++ .../useParachainUnstakingRequests.ts | 10 ++++ .../data/liquidStaking/useRedeemTx.ts | 1 + apps/tangle-dapp/utils/permillToPercentage.ts | 7 +++ .../tangle-dapp/utils/scaleAmountByPermill.ts | 14 +++++ 10 files changed, 206 insertions(+), 42 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/DetailItem.tsx create mode 100644 apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx create mode 100644 apps/tangle-dapp/data/liquidStaking/useDelegationsOccupiedStatus.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/useMintAndRedeemFees.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/useParachainUnstakingRequests.ts create mode 100644 apps/tangle-dapp/utils/permillToPercentage.ts create mode 100644 apps/tangle-dapp/utils/scaleAmountByPermill.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/DetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/DetailItem.tsx new file mode 100644 index 000000000..7ced70e2a --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/DetailItem.tsx @@ -0,0 +1,43 @@ +import { InformationLine } from '@webb-tools/icons'; +import { IconWithTooltip, Typography } from '@webb-tools/webb-ui-components'; +import { FC, ReactNode } from 'react'; + +type DetailItemProps = { + title: string; + tooltip?: string; + value: ReactNode | string; +}; + +const DetailItem: FC = ({ title, tooltip, value }) => { + return ( +
+
+ + {title} + + + {tooltip !== undefined && ( + + } + content={tooltip} + overrideTooltipBodyProps={{ + className: 'max-w-[350px]', + }} + /> + )} +
+ + {typeof value === 'string' ? ( + + {value} + + ) : ( + value + )} +
+ ); +}; + +export default DetailItem; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 406fa1bf2..4f8a3d240 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -6,11 +6,10 @@ import '@webb-tools/tangle-restaking-types'; import { BN, BN_ZERO } from '@polkadot/util'; import { ArrowDownIcon } from '@radix-ui/react-icons'; -import { InformationLine, Search } from '@webb-tools/icons'; +import { Search } from '@webb-tools/icons'; import { Button, Chip, - IconWithTooltip, Input, Typography, } from '@webb-tools/webb-ui-components'; @@ -27,7 +26,9 @@ import useParachainBalances from '../../data/liquidStaking/useParachainBalances' import useApi from '../../hooks/useApi'; import useApiRx from '../../hooks/useApiRx'; import { TxStatus } from '../../hooks/useSubstrateTx'; +import DetailItem from './DetailItem'; import LiquidStakingInput from './LiquidStakingInput'; +import MintAndRedeemDetailItem from './MintAndRedeemDetailItem'; import ParachainWalletBalance from './ParachainWalletBalance'; import SelectValidators from './SelectValidators'; @@ -140,10 +141,10 @@ const LiquidStakeCard: FC = () => { value={`1 ${selectedChain.token} = ${rate} ${LIQUID_STAKING_TOKEN_PREFIX}${selectedChain.token}`} /> - {
+ {/* TODO: Disable stake button if no account is connected. Perhaps consider adding a tooltip instructing the user to connect an account in order to use this action. */}
+ {areAllDelegationsOccupied?.isTrue && ( + + )} + + {/* TODO: Disable unstake button if no account is connected. Perhaps consider adding a tooltip instructing the user to connect an account in order to use this action. */}
+ } + /> + ); +}; + +export default ExchangeRateDetailItem; diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 4f8a3d240..343b95ae9 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -8,6 +8,7 @@ import { BN, BN_ZERO } from '@polkadot/util'; import { ArrowDownIcon } from '@radix-ui/react-icons'; import { Search } from '@webb-tools/icons'; import { + Alert, Button, Chip, Input, @@ -21,12 +22,16 @@ import { LIQUID_STAKING_TOKEN_PREFIX, LiquidStakingChainId, } from '../../constants/liquidStaking'; +import useExchangeRate, { + ExchangeRateType, +} from '../../data/liquidStaking/useExchangeRate'; import useMintTx from '../../data/liquidStaking/useMintTx'; import useParachainBalances from '../../data/liquidStaking/useParachainBalances'; import useApi from '../../hooks/useApi'; import useApiRx from '../../hooks/useApiRx'; import { TxStatus } from '../../hooks/useSubstrateTx'; import DetailItem from './DetailItem'; +import ExchangeRateDetailItem from './ExchangeRateDetailItem'; import LiquidStakingInput from './LiquidStakingInput'; import MintAndRedeemDetailItem from './MintAndRedeemDetailItem'; import ParachainWalletBalance from './ParachainWalletBalance'; @@ -35,9 +40,6 @@ import SelectValidators from './SelectValidators'; const LiquidStakeCard: FC = () => { const [fromAmount, setFromAmount] = useState(null); - // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. - const [rate] = useState(1.0); - const [selectedChainId, setSelectedChainId] = useState( LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN, ); @@ -47,6 +49,11 @@ const LiquidStakeCard: FC = () => { const selectedChain = LIQUID_STAKING_CHAIN_MAP[selectedChainId]; + const exchangeRateOpt = useExchangeRate( + ExchangeRateType.NativeToLiquid, + selectedChain.currency, + ); + const { result: minimumMintingAmount } = useApiRx( useCallback( (api) => @@ -91,12 +98,16 @@ const LiquidStakeCard: FC = () => { }, [executeMintTx, fromAmount, selectedChain.currency]); const toAmount = useMemo(() => { - if (fromAmount === null || rate === null) { + if ( + fromAmount === null || + exchangeRateOpt === null || + exchangeRateOpt.value === null + ) { return null; } - return fromAmount.muln(rate); - }, [fromAmount, rate]); + return fromAmount.muln(exchangeRateOpt.value); + }, [fromAmount, exchangeRateOpt]); const walletBalance = ( { {/* Details */}
- {
+ {/* TODO: This should actually only apply to unstaking, since when staking the user itself is the liquidity provider, since they are minting LSTs. */} + {exchangeRateOpt?.isEmpty && ( + + )} + {/* TODO: Disable stake button if no account is connected. Perhaps consider adding a tooltip instructing the user to connect an account in order to use this action. */} Date: Tue, 16 Jul 2024 00:22:03 -0400 Subject: [PATCH 25/53] refactor(tangle-dapp): Improve implementation of `formatBn` --- .../LiquidStaking/LiquidStakeCard.tsx | 1 + .../LiquidStaking/LiquidUnstakeCard.tsx | 1 + .../LiquidStaking/MintAndRedeemDetailItem.tsx | 10 ++-- .../LiquidStaking/ParachainWalletBalance.tsx | 9 ++- .../LiquidStaking/SelectTokenModal.tsx | 9 +-- .../components/Sidebar/sidebarProps.ts | 2 +- .../UnbondingStatsItem/UnbondingStatsItem.tsx | 4 +- .../components/tableCells/TokenAmountCell.tsx | 14 ++--- apps/tangle-dapp/constants/liquidStaking.ts | 2 +- .../liquidStaking/useParachainBalances.ts | 13 ++-- apps/tangle-dapp/hooks/useLSTokenSVGs.ts | 3 +- ...tBnWithCommas.ts => addCommasToInteger.ts} | 10 ++-- apps/tangle-dapp/utils/formatBn.ts | 59 +++++++++---------- apps/tangle-dapp/utils/formatTangleBalance.ts | 1 + 14 files changed, 69 insertions(+), 69 deletions(-) rename apps/tangle-dapp/utils/{formatBnWithCommas.ts => addCommasToInteger.ts} (58%) diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index 343b95ae9..af23a774d 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -112,6 +112,7 @@ const LiquidStakeCard: FC = () => { const walletBalance = ( setFromAmount(maximumInputAmount)} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx index 8a93b782b..4ac0ae755 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -124,6 +124,7 @@ const LiquidUnstakeCard: FC = () => { setFromAmount(maximumInputAmount)} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx index 295596388..85626fa53 100644 --- a/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx @@ -1,10 +1,12 @@ -import { BN, formatBalance } from '@polkadot/util'; +import { BN } from '@polkadot/util'; +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; import { SkeletonLoader } from '@webb-tools/webb-ui-components'; import { FC, useMemo } from 'react'; import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import { LiquidStakingToken } from '../../constants/liquidStaking'; import useMintAndRedeemFees from '../../data/liquidStaking/useMintAndRedeemFees'; +import formatBn from '../../utils/formatBn'; import scaleAmountByPermill from '../../utils/scaleAmountByPermill'; import DetailItem from './DetailItem'; @@ -37,10 +39,8 @@ const MintAndRedeemDetailItem: FC = ({ return null; } - return formatBalance(feeAmount, { - withSi: true, - withUnit: false, - }); + // TODO: What token is charged as fee? The same as the intended token? TNT? Depending on which one it is, use its corresponding decimals. + return formatBn(feeAmount, TANGLE_TOKEN_DECIMALS); }, [fee, feeAmount]); const value = diff --git a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx index 6ffb7a210..798946b1e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/ParachainWalletBalance.tsx @@ -1,4 +1,4 @@ -import { BN_ZERO, formatBalance } from '@polkadot/util'; +import { BN_ZERO } from '@polkadot/util'; import { WalletLineIcon } from '@webb-tools/icons'; import { SkeletonLoader, @@ -14,10 +14,12 @@ import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import { LiquidStakingToken } from '../../constants/liquidStaking'; import useParachainBalances from '../../data/liquidStaking/useParachainBalances'; import useSubstrateAddress from '../../hooks/useSubstrateAddress'; +import formatBn from '../../utils/formatBn'; export type ParachainWalletBalanceProps = { isNative?: boolean; token: LiquidStakingToken; + decimals: number; tooltip?: string; onlyShowTooltipWhenBalanceIsSet?: boolean; onClick?: () => void; @@ -26,6 +28,7 @@ export type ParachainWalletBalanceProps = { const ParachainWalletBalance: FC = ({ isNative = true, token, + decimals, tooltip, onlyShowTooltipWhenBalanceIsSet = true, onClick, @@ -52,8 +55,8 @@ const ParachainWalletBalance: FC = ({ return null; } - return formatBalance(balance); - }, [activeSubstrateAddress, balance]); + return formatBn(balance, decimals); + }, [activeSubstrateAddress, balance, decimals]); const content = ( = ({ onClick, }) => { const formattedAmount = useMemo(() => { - return formatBalance(amount, { - withSi: true, - decimals, - withUnit: false, - }); + return formatBn(amount, decimals); }, [amount, decimals]); return ( diff --git a/apps/tangle-dapp/components/Sidebar/sidebarProps.ts b/apps/tangle-dapp/components/Sidebar/sidebarProps.ts index 14e949031..df47c38d7 100644 --- a/apps/tangle-dapp/components/Sidebar/sidebarProps.ts +++ b/apps/tangle-dapp/components/Sidebar/sidebarProps.ts @@ -75,7 +75,7 @@ const SIDEBAR_STATIC_ITEMS: SideBarItemProps[] = [ subItems: [], }, { - name: 'Claim', + name: 'Claim Airdrop', href: PagePath.CLAIM_AIRDROP, isInternal: true, isNext: true, diff --git a/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx b/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx index 9790e5e23..6c6def782 100644 --- a/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx +++ b/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx @@ -8,7 +8,7 @@ import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import useNetworkStore from '../../context/useNetworkStore'; import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount'; import useUnbonding from '../../data/staking/useUnbonding'; -import { formatBnWithCommas } from '../../utils/formatBnWithCommas'; +import { addCommasToInteger } from '../../utils/addCommasToInteger'; import formatTangleBalance from '../../utils/formatTangleBalance'; import { NominatorStatsItem } from '../NominatorStatsItem'; @@ -41,7 +41,7 @@ const UnbondingStatsItem: FC = () => {

{entry.remainingEras.gtn(0) && ( -

{formatBnWithCommas(entry.remainingEras)} eras remaining.

+

{addCommasToInteger(entry.remainingEras)} eras remaining.

)}
); diff --git a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx index 08f942c05..141bda7ee 100644 --- a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx +++ b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx @@ -1,8 +1,9 @@ -import { BN, formatBalance } from '@polkadot/util'; +import { BN } from '@polkadot/util'; import { FC, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; import useNetworkStore from '../../context/useNetworkStore'; +import formatBn from '../../utils/formatBn'; import formatTangleBalance from '../../utils/formatTangleBalance'; export type TokenAmountCellProps = { @@ -23,19 +24,12 @@ const TokenAmountCell: FC = ({ const { nativeTokenSymbol } = useNetworkStore(); const formattedBalance = useMemo(() => { + // Default to Tangle decimals if not provided. if (decimals === undefined) { return formatTangleBalance(amount); } - return formatBalance(amount, { - decimals, - withZero: false, - // This ensures that the balance is always displayed in the - // base unit, preventing the conversion to larger or smaller - // units (e.g. kilo, milli, etc.). - forceUnit: '-', - withUnit: false, - }); + return formatBn(amount, decimals); }, [amount, decimals]); const parts = formattedBalance.split('.'); diff --git a/apps/tangle-dapp/constants/liquidStaking.ts b/apps/tangle-dapp/constants/liquidStaking.ts index 78e8328ed..7e7af293e 100644 --- a/apps/tangle-dapp/constants/liquidStaking.ts +++ b/apps/tangle-dapp/constants/liquidStaking.ts @@ -17,7 +17,7 @@ export enum LiquidStakingToken { MANTA = 'MANTA', ASTAR = 'ASTAR', PHALA = 'PHALA', - TNT = 'BNC', + TNT = 'TNT', } // TODO: Temporary manual override until the Parachain types are updated. diff --git a/apps/tangle-dapp/data/liquidStaking/useParachainBalances.ts b/apps/tangle-dapp/data/liquidStaking/useParachainBalances.ts index a6c45fd3c..bffd30701 100644 --- a/apps/tangle-dapp/data/liquidStaking/useParachainBalances.ts +++ b/apps/tangle-dapp/data/liquidStaking/useParachainBalances.ts @@ -23,7 +23,6 @@ const useParachainBalances = () => { return null; } - // TODO: For some reason, the `api.query.tokens.accounts` method does not recognize passing in `null` for the token parameter, which is equivalent to passing `None` and should return the balance for all tokens. For now, manually casting the return type. return api.query.tokens.accounts.entries(activeSubstrateAddress); }, [activeSubstrateAddress], @@ -46,10 +45,14 @@ const useParachainBalances = () => { string | undefined >; - const entryValue = entry.lst ?? entry.Native; + // TODO: 'Native' balance isn't showing up on the parachain for some reason, not even under `api.query.balances.account()`. Is this a bug? Currently unable to obtain user account's native balance (defaulting to 0). Is it due to some sort of bridging mechanism? + const entryTokenValue = entry.lst ?? entry.Native; // Irrelevant entry, skip. - if (entryValue === undefined || !isLiquidStakingToken(entryValue)) { + if ( + entryTokenValue === undefined || + !isLiquidStakingToken(entryTokenValue) + ) { continue; } @@ -57,9 +60,9 @@ const useParachainBalances = () => { const balance = encodedBalance[1].free.toBn(); if (isLiquid) { - liquidBalances.set(entryValue, balance); + liquidBalances.set(entryTokenValue, balance); } else { - nativeBalances.set(entryValue, balance); + nativeBalances.set(entryTokenValue, balance); } } diff --git a/apps/tangle-dapp/hooks/useLSTokenSVGs.ts b/apps/tangle-dapp/hooks/useLSTokenSVGs.ts index 1cd261b94..e3a011ae4 100644 --- a/apps/tangle-dapp/hooks/useLSTokenSVGs.ts +++ b/apps/tangle-dapp/hooks/useLSTokenSVGs.ts @@ -16,8 +16,7 @@ const tokenSVGs: { GLMR, MANTA, PHALA, - // TODO: Awaiting internal renaming of BNC -> TNT in the Tangle Restaking Parachain. - BNC: TNT, + TNT, }; const useLSTokenSVGs = ( diff --git a/apps/tangle-dapp/utils/formatBnWithCommas.ts b/apps/tangle-dapp/utils/addCommasToInteger.ts similarity index 58% rename from apps/tangle-dapp/utils/formatBnWithCommas.ts rename to apps/tangle-dapp/utils/addCommasToInteger.ts index feff08031..952146856 100644 --- a/apps/tangle-dapp/utils/formatBnWithCommas.ts +++ b/apps/tangle-dapp/utils/addCommasToInteger.ts @@ -5,13 +5,15 @@ import { BN } from '@polkadot/util'; * * @example * ```ts - * formatBnWithCommas(new BN('123456789')); // '123,456,789' + * addCommasToInteger(new BN('123456789')); // '123,456,789' * ``` */ -export const formatBnWithCommas = (bn: BN): string => { - // TODO: Incorporate this into the logic of `formatBn` to consolidate balance formatting logic. +export const addCommasToInteger = ( + numberLike: BN | number | string, +): string => { + // TODO: Consider adding a sanity check for the input value, to ensure that only digits are passed. In case that a number is passed, only add commas to the integer part. - const valueAsString = bn.toString(); + const valueAsString = numberLike.toString(); let result = ''; let count = 0; diff --git a/apps/tangle-dapp/utils/formatBn.ts b/apps/tangle-dapp/utils/formatBn.ts index 01897527f..aba267248 100644 --- a/apps/tangle-dapp/utils/formatBn.ts +++ b/apps/tangle-dapp/utils/formatBn.ts @@ -1,5 +1,7 @@ import { BN } from '@polkadot/util'; +import { addCommasToInteger } from './addCommasToInteger'; + /** * When the user inputs an amount in the UI, say using an Input * component, the amount needs to be treated as if it were in chain @@ -9,10 +11,9 @@ import { BN } from '@polkadot/util'; * `1` token, and not the smallest unit possible. * * To have the amount be in proper form, it needs to be multiplied by - * this factor (input amount * 10^18). + * this factor (input amount * 10^decimals). */ - -const convertChainUnitFactor = (decimals: number) => { +const getChainUnitFactor = (decimals: number) => { return new BN(10).pow(new BN(decimals)); }; @@ -34,50 +35,48 @@ function formatBn( options?: Partial, ): string { const finalOptions = { ...DEFAULT_FORMAT_OPTIONS, ...options }; - const divisor = convertChainUnitFactor(decimals); - const divided = amount.div(divisor); - const remainder = amount.mod(divisor); + const chainUnitFactorBn = getChainUnitFactor(decimals); + const integerPartBn = amount.div(chainUnitFactorBn); + const remainderBn = amount.mod(chainUnitFactorBn); - let integerPart = divided.toString(10); + let integerPart = integerPartBn.toString(10); + let decimalPart = remainderBn.toString(10); - // Convert remainder to a string and pad with zeros if necessary. - let remainderString = remainder.toString(10); - - // There is a case when the decimals part has leading 0s, so that the remaining - // string can missing those 0s when we use `mod` method. - // Solution: Try to construct the string again and check the length, - // if the length is not the same, we can say that the remainder string is missing - // leading 0s, so we try to prepend those 0s to the remainder string - if (amount.toString().length !== (integerPart + remainderString).length) { + // Check for missing leading zeros in the decimal part. This + // edge case can happen when the remainder has fewer digits + // than the specified decimals, resulting in a loss of leading + // zeros when converting to a string, ex. 0001 -> 1. + if (amount.toString().length !== (integerPart + decimalPart).length) { + // Count how many leading zeros are missing. const missing0sCount = - amount.toString().length - (integerPart + remainderString).length; + amount.toString().length - (integerPart + decimalPart).length; - remainderString = - Array.from({ length: missing0sCount }) - .map(() => '0') - .join('') + remainderString; + // Add the missing leading zeros. + decimalPart = '0'.repeat(missing0sCount) + decimalPart; } if (finalOptions.padZerosInFraction) { - remainderString = remainderString.padStart(decimals, '0'); + decimalPart = decimalPart.padStart(decimals, '0'); } - remainderString = remainderString.substring(0, finalOptions.fractionLength); + // Trim the decimal part to the desired length. + if (finalOptions.fractionLength !== undefined) { + decimalPart = decimalPart.substring(0, finalOptions.fractionLength); + } // Remove trailing 0s. - while (remainderString.endsWith('0')) { - remainderString = remainderString.substring(0, remainderString.length - 1); + while (decimalPart.endsWith('0')) { + decimalPart = decimalPart.substring(0, decimalPart.length - 1); } // Insert commas in the integer part if requested. if (finalOptions.includeCommas) { - // TODO: Avoid using regex, it's confusing. - integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + integerPart = addCommasToInteger(integerPart); } - // TODO: Make the condition explicit. Is it checking for an empty string? - // Combine the integer and decimal parts. - return remainderString ? `${integerPart}.${remainderString}` : integerPart; + // Combine the integer and decimal parts. Only include the decimal + // part if it's available. + return decimalPart !== '' ? `${integerPart}.${decimalPart}` : integerPart; } export default formatBn; diff --git a/apps/tangle-dapp/utils/formatTangleBalance.ts b/apps/tangle-dapp/utils/formatTangleBalance.ts index 175a96342..634d6474e 100644 --- a/apps/tangle-dapp/utils/formatTangleBalance.ts +++ b/apps/tangle-dapp/utils/formatTangleBalance.ts @@ -12,6 +12,7 @@ const formatTangleBalance = ( return formatBalance(balance, { decimals: TANGLE_TOKEN_DECIMALS, withZero: false, + // TODO: There is a bug here, since small balances will show up as 0 because of this option. // This ensures that the balance is always displayed in the // base unit, preventing the conversion to larger or smaller // units (e.g. kilo, milli, etc.). From 14d95b300606f39380297f65f28dc750a4c34d34 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 16 Jul 2024 02:59:00 -0400 Subject: [PATCH 26/53] fix(tangle-dapp): Fix bug in BN formatting --- .../hooks/useFormattedAmountForSygmaTx.ts | 2 +- .../UnbondingStatsItem/UnbondingStatsItem.tsx | 4 +- apps/tangle-dapp/hooks/useInputAmount.ts | 2 +- ...ommasToInteger.ts => addCommasToNumber.ts} | 18 ++++-- apps/tangle-dapp/utils/formatBn.ts | 60 +++++++++++-------- 5 files changed, 53 insertions(+), 33 deletions(-) rename apps/tangle-dapp/utils/{addCommasToInteger.ts => addCommasToNumber.ts} (55%) diff --git a/apps/tangle-dapp/app/bridge/hooks/useFormattedAmountForSygmaTx.ts b/apps/tangle-dapp/app/bridge/hooks/useFormattedAmountForSygmaTx.ts index 6db960e72..ebce81f8b 100644 --- a/apps/tangle-dapp/app/bridge/hooks/useFormattedAmountForSygmaTx.ts +++ b/apps/tangle-dapp/app/bridge/hooks/useFormattedAmountForSygmaTx.ts @@ -17,7 +17,7 @@ export default function useAmountToTransfer() { ? parseUnits( formatBn(amount, decimals, { includeCommas: false, - fractionLength: undefined, + fractionMaxLength: undefined, }), decimals, ).toString() diff --git a/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx b/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx index 6c6def782..ae172165e 100644 --- a/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx +++ b/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx @@ -8,7 +8,7 @@ import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import useNetworkStore from '../../context/useNetworkStore'; import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount'; import useUnbonding from '../../data/staking/useUnbonding'; -import { addCommasToInteger } from '../../utils/addCommasToInteger'; +import addCommasToNumber from '../../utils/addCommasToNumber'; import formatTangleBalance from '../../utils/formatTangleBalance'; import { NominatorStatsItem } from '../NominatorStatsItem'; @@ -41,7 +41,7 @@ const UnbondingStatsItem: FC = () => {

{entry.remainingEras.gtn(0) && ( -

{addCommasToInteger(entry.remainingEras)} eras remaining.

+

{addCommasToNumber(entry.remainingEras)} eras remaining.

)}
); diff --git a/apps/tangle-dapp/hooks/useInputAmount.ts b/apps/tangle-dapp/hooks/useInputAmount.ts index 3a2e5411c..8d43df0ad 100644 --- a/apps/tangle-dapp/hooks/useInputAmount.ts +++ b/apps/tangle-dapp/hooks/useInputAmount.ts @@ -39,7 +39,7 @@ function safeParseInputAmount( const INPUT_AMOUNT_FORMAT: Partial = { includeCommas: true, - fractionLength: undefined, + fractionMaxLength: undefined, }; type Options = { diff --git a/apps/tangle-dapp/utils/addCommasToInteger.ts b/apps/tangle-dapp/utils/addCommasToNumber.ts similarity index 55% rename from apps/tangle-dapp/utils/addCommasToInteger.ts rename to apps/tangle-dapp/utils/addCommasToNumber.ts index 952146856..efeb06976 100644 --- a/apps/tangle-dapp/utils/addCommasToInteger.ts +++ b/apps/tangle-dapp/utils/addCommasToNumber.ts @@ -8,12 +8,18 @@ import { BN } from '@polkadot/util'; * addCommasToInteger(new BN('123456789')); // '123,456,789' * ``` */ -export const addCommasToInteger = ( - numberLike: BN | number | string, -): string => { - // TODO: Consider adding a sanity check for the input value, to ensure that only digits are passed. In case that a number is passed, only add commas to the integer part. - +const addCommasToNumber = (numberLike: BN | number | string): string => { const valueAsString = numberLike.toString(); + + // Sanity check that the value is not already formatted. + if (typeof numberLike === 'string' && numberLike.includes(',')) { + console.warn('Attempted to add commas to a number that already has commas'); + + return numberLike; + } + + // TODO: Add a sanity check for the input value, to ensure that only digits are passed. In case that a number is passed, only add commas to the integer part. + let result = ''; let count = 0; @@ -30,3 +36,5 @@ export const addCommasToInteger = ( return result; }; + +export default addCommasToNumber; diff --git a/apps/tangle-dapp/utils/formatBn.ts b/apps/tangle-dapp/utils/formatBn.ts index aba267248..4e4c05845 100644 --- a/apps/tangle-dapp/utils/formatBn.ts +++ b/apps/tangle-dapp/utils/formatBn.ts @@ -1,6 +1,6 @@ import { BN } from '@polkadot/util'; -import { addCommasToInteger } from './addCommasToInteger'; +import addCommasToNumber from './addCommasToNumber'; /** * When the user inputs an amount in the UI, say using an Input @@ -19,14 +19,14 @@ const getChainUnitFactor = (decimals: number) => { export type FormatOptions = { includeCommas: boolean; - fractionLength?: number; - padZerosInFraction: boolean; + fractionMaxLength?: number; + trimTrailingZeroes: boolean; }; const DEFAULT_FORMAT_OPTIONS: FormatOptions = { - fractionLength: 4, - includeCommas: true, - padZerosInFraction: false, + fractionMaxLength: 4, + includeCommas: false, + trimTrailingZeroes: true, }; function formatBn( @@ -40,43 +40,55 @@ function formatBn( const remainderBn = amount.mod(chainUnitFactorBn); let integerPart = integerPartBn.toString(10); - let decimalPart = remainderBn.toString(10); + let fractionPart = remainderBn.toString(10).padStart(decimals, '0'); - // Check for missing leading zeros in the decimal part. This + const amountStringLength = amount.toString().length; + const partsLength = integerPart.length + fractionPart.length; + + // Check for missing leading zeros in the fraction part. This // edge case can happen when the remainder has fewer digits // than the specified decimals, resulting in a loss of leading // zeros when converting to a string, ex. 0001 -> 1. - if (amount.toString().length !== (integerPart + decimalPart).length) { + if (amountStringLength !== partsLength) { // Count how many leading zeros are missing. - const missing0sCount = - amount.toString().length - (integerPart + decimalPart).length; + const missingZerosCount = amountStringLength - partsLength; - // Add the missing leading zeros. - decimalPart = '0'.repeat(missing0sCount) + decimalPart; + // Add the missing leading zeros. Use the max function to avoid + // strange situations where the count is negative (ie. the length + // of the number is greater than the length of the integer and fraction + // parts combined). + fractionPart = '0'.repeat(Math.max(missingZerosCount, 0)) + fractionPart; } - if (finalOptions.padZerosInFraction) { - decimalPart = decimalPart.padStart(decimals, '0'); + // Pad the end of the fraction part with zeros if applicable, + // ex. 0.001 -> 0.0010 when the requested fraction length is 4. + if (!finalOptions.trimTrailingZeroes) { + fractionPart = fractionPart.padEnd( + finalOptions.fractionMaxLength ?? decimals, + '0', + ); } - // Trim the decimal part to the desired length. - if (finalOptions.fractionLength !== undefined) { - decimalPart = decimalPart.substring(0, finalOptions.fractionLength); + // Trim the fraction part to the desired length. + if (finalOptions.fractionMaxLength !== undefined) { + fractionPart = fractionPart.substring(0, finalOptions.fractionMaxLength); } - // Remove trailing 0s. - while (decimalPart.endsWith('0')) { - decimalPart = decimalPart.substring(0, decimalPart.length - 1); + // Remove trailing zeroes if applicable. + if (finalOptions.trimTrailingZeroes) { + while (fractionPart.endsWith('0')) { + fractionPart = fractionPart.substring(0, fractionPart.length - 1); + } } // Insert commas in the integer part if requested. if (finalOptions.includeCommas) { - integerPart = addCommasToInteger(integerPart); + integerPart = addCommasToNumber(integerPart); } - // Combine the integer and decimal parts. Only include the decimal + // Combine the integer and fraction parts. Only include the fraction // part if it's available. - return decimalPart !== '' ? `${integerPart}.${decimalPart}` : integerPart; + return fractionPart !== '' ? `${integerPart}.${fractionPart}` : integerPart; } export default formatBn; From 3315e0be9c8d5472ac05054b7f669a7b92f88560 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 16 Jul 2024 03:03:49 -0400 Subject: [PATCH 27/53] ci(tangle-dapp): Fix warnings causing CI to fail --- .../LiquidStaking/CancelUnstakeModal.tsx | 3 +- .../LiquidStaking/HexagonAvatar.tsx | 38 ------------------- .../UnstakeRequestSubmittedModal.tsx | 3 +- .../useParachainUnstakingRequests.ts | 10 ----- 4 files changed, 4 insertions(+), 50 deletions(-) delete mode 100644 apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx delete mode 100644 apps/tangle-dapp/data/liquidStaking/useParachainUnstakingRequests.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx b/apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx index 013c99b47..db67c499e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/CancelUnstakeModal.tsx @@ -21,7 +21,8 @@ export type CancelUnstakeModalProps = { const CancelUnstakeModal: FC = ({ isOpen, - unstakeRequest, + // TODO: Make use of the unstake request data, which is relevant for the link's href. + unstakeRequest: _unstakeRequest, onClose, }) => { const handleConfirm = useCallback(() => { diff --git a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx b/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx deleted file mode 100644 index 4eb58223a..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/HexagonAvatar.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FC, ReactNode } from 'react'; - -import { AnySubstrateAddress } from '../../types/utils'; - -export type HexagonAvatarProps = { - address: AnySubstrateAddress; -}; - -const Hexagon: FC<{ children: ReactNode }> = ({ children }) => { - return ( - - - - - - - - {children} - - ); -}; - -const HexagonAvatar: FC = ({ address }) => { - return ( -
- -
-
-
- ); -}; - -export default HexagonAvatar; diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx index fabd03112..4a2106031 100644 --- a/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakeRequestSubmittedModal.tsx @@ -21,7 +21,8 @@ export type UnstakeRequestSubmittedModalProps = { const UnstakeRequestSubmittedModal: FC = ({ isOpen, - unstakeRequest, + // TODO: Make use of the unstake request data, which is relevant for the link's href. + unstakeRequest: _unstakeRequest, onClose, }) => { const handleAddTokenToWallet = useCallback(() => { diff --git a/apps/tangle-dapp/data/liquidStaking/useParachainUnstakingRequests.ts b/apps/tangle-dapp/data/liquidStaking/useParachainUnstakingRequests.ts deleted file mode 100644 index d74c8c191..000000000 --- a/apps/tangle-dapp/data/liquidStaking/useParachainUnstakingRequests.ts +++ /dev/null @@ -1,10 +0,0 @@ -import useApiRx from '../../hooks/useApiRx'; - -const useParachainUnstakingRequests = () => { - // TODO: Implement this hook. - const a = useApiRx((api) => { - return api.query.slp.delegationsOccupied(); - }); -}; - -export default useParachainUnstakingRequests; From 3be14ada2adcaf6f03135383b4fcc79a6b511f11 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Jul 2024 19:21:46 +0700 Subject: [PATCH 28/53] chore: update props field for TableAndChartTabs --- apps/bridge-dapp/src/pages/Account/index.tsx | 2 +- .../NominationsPayoutsContainer.tsx | 2 +- .../src/components/TableAndChartTabs/TableAndChartTabs.tsx | 4 ++-- .../src/components/TableAndChartTabs/types.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/bridge-dapp/src/pages/Account/index.tsx b/apps/bridge-dapp/src/pages/Account/index.tsx index ee7c6092e..39ee66fc5 100644 --- a/apps/bridge-dapp/src/pages/Account/index.tsx +++ b/apps/bridge-dapp/src/pages/Account/index.tsx @@ -134,7 +134,7 @@ const Account: FC = () => { tabs={[shieldedAssetsTab, spendNotesTab]} className="py-4 space-y-4" onValueChange={(val) => setActiveTable(val as typeof activeTable)} - filterComponent={ + additionalActionsCmp={ { onValueChange={(tabString) => setActiveTab(assertTab(tabString))} tabs={[...Object.values(NominationsAndPayoutsTab)]} headerClassName="w-full overflow-x-auto" - filterComponent={ + additionalActionsCmp={ activeAccount?.address && isBondedOrNominating ? ( activeTab === NominationsAndPayoutsTab.NOMINATIONS ? ( = ({ tabs, - filterComponent, + additionalActionsCmp, className, headerClassName, listClassName, @@ -55,7 +55,7 @@ export const TableAndChartTabs: FC = ({ {/* Component on the right */} - {filterComponent} + {additionalActionsCmp} {/* Tabs Content */} diff --git a/libs/webb-ui-components/src/components/TableAndChartTabs/types.ts b/libs/webb-ui-components/src/components/TableAndChartTabs/types.ts index 236c3f6bb..28cf36e3a 100644 --- a/libs/webb-ui-components/src/components/TableAndChartTabs/types.ts +++ b/libs/webb-ui-components/src/components/TableAndChartTabs/types.ts @@ -10,9 +10,9 @@ export interface TableAndChartTabsProps extends Tabs.TabsProps { tabs: string[]; /** - * Filter component for the charts and tables (optional) + * Components on the right side of the tabs to perform additional actions (optional) */ - filterComponent?: ReactNode; + additionalActionsCmp?: ReactNode; /** * The className for the whole component (optional) From 2442cef49b62430352d6f491b1950bf55c21bfd7 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Jul 2024 20:39:26 +0700 Subject: [PATCH 29/53] feat: add search functionalities for Validator tables --- .../ValidatorSelectionTable.tsx | 8 ++--- .../ValidatorTable/ValidatorTable.tsx | 23 +++++++++++- .../components/ValidatorTable/types.ts | 1 + .../ValidatorTablesContainer.tsx | 35 ++++++++++++++++--- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx index 79747b53a..f2731d168 100644 --- a/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx +++ b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx @@ -257,7 +257,7 @@ const ValidatorSelectionTable: FC = ({ <>
} placeholder="Search validators..." value={searchValue} @@ -266,11 +266,7 @@ const ValidatorSelectionTable: FC = ({ isControlled /> - {isLoading && ( - //
- - //
- )} + {isLoading && } {!isLoading && (allValidators.length === 0 ? ( diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index 2e12462b1..b8646c616 100644 --- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx +++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx @@ -101,7 +101,11 @@ const getStaticColumns = (isWaiting?: boolean) => [ ]), ]; -const ValidatorTable: FC = ({ data, isWaiting }) => { +const ValidatorTable: FC = ({ + data, + isWaiting, + searchValue, +}) => { const { network } = useNetworkStore(); const [sorting, setSorting] = useState([ @@ -152,6 +156,13 @@ const ValidatorTable: FC = ({ data, isWaiting }) => { ); }, sortingFn: sortAddressOrIdentityForNomineeOrValidator, + filterFn: (row, _, filterValue) => { + const { address, identityName } = row.original; + return ( + address.toLowerCase().includes(filterValue.toLowerCase()) || + identityName.toLowerCase().includes(filterValue.toLowerCase()) + ); + }, }), ...getStaticColumns(isWaiting), ], @@ -172,7 +183,17 @@ const ValidatorTable: FC = ({ data, isWaiting }) => { onSortingChange: setSorting, state: { sorting, + columnFilters: searchValue + ? [ + { + id: 'address', + value: searchValue, + }, + ] + : [], }, + getRowId: (row) => row.address, + autoResetPageIndex: false, enableSortingRemoval: false, }); diff --git a/apps/tangle-dapp/components/ValidatorTable/types.ts b/apps/tangle-dapp/components/ValidatorTable/types.ts index b6a9b33dc..3a064ea90 100644 --- a/apps/tangle-dapp/components/ValidatorTable/types.ts +++ b/apps/tangle-dapp/components/ValidatorTable/types.ts @@ -3,4 +3,5 @@ import { Validator } from '../../types'; export interface ValidatorTableProps { isWaiting?: boolean; data: Validator[]; + searchValue?: string; } diff --git a/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx b/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx index b5ae42c65..41523c820 100644 --- a/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx +++ b/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx @@ -1,6 +1,12 @@ 'use client'; -import { TabContent, TableAndChartTabs } from '@webb-tools/webb-ui-components'; +import { Search } from '@webb-tools/icons'; +import { + Input, + TabContent, + TableAndChartTabs, +} from '@webb-tools/webb-ui-components'; +import { useState } from 'react'; import { ContainerSkeleton, TableStatus } from '../../components'; import useNetworkStore from '../../context/useNetworkStore'; @@ -15,6 +21,9 @@ const ValidatorTablesContainer = () => { const { network } = useNetworkStore(); const { validators: activeValidatorsData } = useActiveValidators(); const { validators: waitingValidatorsData } = useWaitingValidators(); + + const [searchValue, setSearchValue] = useState(''); + const isActiveValidatorsLoading = activeValidatorsData === null; const isWaitingValidatorsLoading = waitingValidatorsData === null; @@ -23,7 +32,18 @@ const ValidatorTablesContainer = () => { return ( } + placeholder="Search validators..." + value={searchValue} + onChange={(val) => setSearchValue(val)} + isControlled + className="w-2/5" + /> + } > {/* Active Validators Table */} @@ -41,7 +61,10 @@ const ValidatorTablesContainer = () => { ) : isActiveValidatorsLoading ? ( ) : ( - + )} @@ -63,7 +86,11 @@ const ValidatorTablesContainer = () => { ) : isWaitingValidatorsLoading ? ( ) : ( - + )} From f236bac4dc735f31f26ec80f708265c4c327e6d7 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Jul 2024 20:52:02 +0700 Subject: [PATCH 30/53] chore: fix Max on AmountInput --- .../tangle-dapp/components/AmountInput/AmountInput.tsx | 10 ++++++++-- apps/tangle-dapp/hooks/useInputAmount.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/tangle-dapp/components/AmountInput/AmountInput.tsx b/apps/tangle-dapp/components/AmountInput/AmountInput.tsx index f687e47dc..0349ebdf9 100644 --- a/apps/tangle-dapp/components/AmountInput/AmountInput.tsx +++ b/apps/tangle-dapp/components/AmountInput/AmountInput.tsx @@ -53,7 +53,12 @@ const AmountInput: FC = ({ const inputRef = useRef(null); const { nativeTokenSymbol } = useNetworkStore(); - const { displayAmount, errorMessage, handleChange } = useInputAmount({ + const { + displayAmount, + errorMessage, + handleChange, + updateDisplayAmountManual, + } = useInputAmount({ amount, min, max, @@ -74,8 +79,9 @@ const AmountInput: FC = ({ const setMaxAmount = useCallback(() => { if (max !== null) { setAmount(max); + updateDisplayAmountManual(max); } - }, [max, setAmount]); + }, [max, setAmount, updateDisplayAmountManual]); const actions: ReactNode = useMemo( () => ( diff --git a/apps/tangle-dapp/hooks/useInputAmount.ts b/apps/tangle-dapp/hooks/useInputAmount.ts index b2cb7ee0d..b516d847c 100644 --- a/apps/tangle-dapp/hooks/useInputAmount.ts +++ b/apps/tangle-dapp/hooks/useInputAmount.ts @@ -120,6 +120,13 @@ const useInputAmount = ({ ], ); + const updateDisplayAmountManual = useCallback( + (amount: BN) => { + setDisplayAmount(formatBn(amount, decimals, INPUT_AMOUNT_FORMAT)); + }, + [decimals], + ); + useEffect(() => { // If the amount is null, then the display amount should always be empty. // This handle the case where the amount is set to null after submitting a tx @@ -133,6 +140,7 @@ const useInputAmount = ({ displayAmount, errorMessage, handleChange, + updateDisplayAmountManual, }; }; From 50442c82fbd140fe8948be0cb68e9a466784521e Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Jul 2024 22:01:29 +0700 Subject: [PATCH 31/53] chore: fix UI for Nomination modal --- .../NominatorStatsContainer/NominatorStatsContainer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/tangle-dapp/containers/NominatorStatsContainer/NominatorStatsContainer.tsx b/apps/tangle-dapp/containers/NominatorStatsContainer/NominatorStatsContainer.tsx index 6901abb4c..8696acb63 100644 --- a/apps/tangle-dapp/containers/NominatorStatsContainer/NominatorStatsContainer.tsx +++ b/apps/tangle-dapp/containers/NominatorStatsContainer/NominatorStatsContainer.tsx @@ -65,7 +65,7 @@ const NominatorStatsContainer: FC = () => { }, [bondedAmountOpt, nativeTokenSymbol]); return ( - <> +
{ isModalOpen={isWithdrawUnbondedModalOpen} setIsModalOpen={setIsWithdrawUnbondedModalOpen} /> - +
); }; From dd81063548daa542d68dc1228121bafbc5a6bb42 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Jul 2024 22:23:56 +0700 Subject: [PATCH 32/53] chore: update Creative Commons Attribution-ShareAlike link --- .github/CODE_OF_CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 5ea371dc7..5dd775578 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -75,6 +75,6 @@ You can contact Webb via Email: drew@commonwealth.im ## 10. License and attribution -This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). +This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](https://creativecommons.org/licenses/by-sa/4.0/). Portions of text derived from the the [Parity Code of Conduct](https://github.com/openethereum/parity-ethereum/blob/v2.7.2-stable/.github/CODE_OF_CONDUCT.md), and [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). From eaf8ed13637dc5b1f2005806c669a1b7b5bf0f2a Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 17 Jul 2024 00:50:28 -0400 Subject: [PATCH 33/53] refactor(tangle-dapp): Move things around to keep things organized --- .../LiquidStaking/LiquidStakeCard.tsx | 13 +-- .../LiquidStaking/LiquidStakingInput.tsx | 24 +++- .../LiquidStaking/LiquidUnstakeCard.tsx | 107 +++++------------- ...tem.tsx => MintAndRedeemFeeDetailItem.tsx} | 4 +- .../LiquidStaking/UnstakePeriodDetailItem.tsx | 20 ++++ .../data/liquidStaking/useExchangeRate.ts | 1 - 6 files changed, 76 insertions(+), 93 deletions(-) rename apps/tangle-dapp/components/LiquidStaking/{MintAndRedeemDetailItem.tsx => MintAndRedeemFeeDetailItem.tsx} (93%) create mode 100644 apps/tangle-dapp/components/LiquidStaking/UnstakePeriodDetailItem.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx index af23a774d..d15b95eb8 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakeCard.tsx @@ -33,9 +33,10 @@ import { TxStatus } from '../../hooks/useSubstrateTx'; import DetailItem from './DetailItem'; import ExchangeRateDetailItem from './ExchangeRateDetailItem'; import LiquidStakingInput from './LiquidStakingInput'; -import MintAndRedeemDetailItem from './MintAndRedeemDetailItem'; +import MintAndRedeemFeeDetailItem from './MintAndRedeemFeeDetailItem'; import ParachainWalletBalance from './ParachainWalletBalance'; import SelectValidators from './SelectValidators'; +import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; const LiquidStakeCard: FC = () => { const [fromAmount, setFromAmount] = useState(null); @@ -125,7 +126,7 @@ const LiquidStakeCard: FC = () => { chain={selectedChainId} token={selectedChain.token} amount={fromAmount} - setAmount={setFromAmount} + onAmountChange={setFromAmount} placeholder={`0 ${selectedChain.token}`} rightElement={walletBalance} setChain={setSelectedChainId} @@ -154,17 +155,13 @@ const LiquidStakeCard: FC = () => { type={ExchangeRateType.NativeToLiquid} /> - - +
diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx index 6b87e8ef0..e8b9872c3 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx @@ -11,7 +11,7 @@ import { Typography, } from '@webb-tools/webb-ui-components'; import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea'; -import { FC, ReactNode } from 'react'; +import { FC, ReactNode, useEffect } from 'react'; import { twMerge } from 'tailwind-merge'; import { @@ -39,7 +39,7 @@ export type LiquidStakingInputProps = { minAmount?: BN; maxAmount?: BN; maxErrorMessage?: string; - setAmount?: (newAmount: BN | null) => void; + onAmountChange?: (newAmount: BN | null) => void; setChain?: (newChain: LiquidStakingChainId) => void; onTokenClick?: () => void; }; @@ -56,7 +56,7 @@ const LiquidStakingInput: FC = ({ minAmount, maxAmount, maxErrorMessage = ERROR_NOT_ENOUGH_BALANCE, - setAmount, + onAmountChange, setChain, onTokenClick, }) => { @@ -73,9 +73,14 @@ const LiquidStakingInput: FC = ({ return `Amount must be at least ${formattedMinAmount} ${unit}`; })(); - const { displayAmount, handleChange, errorMessage } = useInputAmount({ + const { + displayAmount, + handleChange, + errorMessage, + updateDisplayAmountManual, + } = useInputAmount({ amount, - setAmount, + setAmount: onAmountChange, // TODO: Decimals must be based on the active token's chain decimals, not always the Tangle token decimals. decimals: TANGLE_TOKEN_DECIMALS, min: minAmount, @@ -84,6 +89,15 @@ const LiquidStakingInput: FC = ({ maxErrorMessage, }); + // Update the display amount when the amount prop changes. + // Only do this for controlled (read-only) inputs. + useEffect(() => { + if (isReadOnly && amount !== null) { + updateDisplayAmountManual(amount); + console.debug('set display amount manually', amount.toString()); + } + }, [amount, isReadOnly, updateDisplayAmountManual]); + const isError = errorMessage !== null; return ( diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx index 4ac0ae755..df8d2f117 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidUnstakeCard.tsx @@ -6,15 +6,9 @@ import '@webb-tools/tangle-restaking-types'; import { BN, BN_ZERO } from '@polkadot/util'; import { ArrowDownIcon } from '@radix-ui/react-icons'; -import { InformationLine } from '@webb-tools/icons'; -import { - Alert, - Button, - IconWithTooltip, - Typography, -} from '@webb-tools/webb-ui-components'; +import { Alert, Button } from '@webb-tools/webb-ui-components'; import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; -import { FC, ReactNode, useCallback, useMemo, useState } from 'react'; +import { FC, useCallback, useMemo, useState } from 'react'; import { LIQUID_STAKING_CHAIN_MAP, @@ -22,14 +16,20 @@ import { LiquidStakingChainId, } from '../../constants/liquidStaking'; import useDelegationsOccupiedStatus from '../../data/liquidStaking/useDelegationsOccupiedStatus'; +import useExchangeRate, { + ExchangeRateType, +} from '../../data/liquidStaking/useExchangeRate'; import useParachainBalances from '../../data/liquidStaking/useParachainBalances'; import useRedeemTx from '../../data/liquidStaking/useRedeemTx'; import useApi from '../../hooks/useApi'; import useApiRx from '../../hooks/useApiRx'; import { TxStatus } from '../../hooks/useSubstrateTx'; +import ExchangeRateDetailItem from './ExchangeRateDetailItem'; import LiquidStakingInput from './LiquidStakingInput'; +import MintAndRedeemFeeDetailItem from './MintAndRedeemFeeDetailItem'; import ParachainWalletBalance from './ParachainWalletBalance'; import SelectTokenModal from './SelectTokenModal'; +import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import UnstakeRequestSubmittedModal from './UnstakeRequestSubmittedModal'; const LiquidUnstakeCard: FC = () => { @@ -39,9 +39,6 @@ const LiquidUnstakeCard: FC = () => { const [isRequestSubmittedModalOpen, setIsRequestSubmittedModalOpen] = useState(false); - // TODO: The rate will likely be a hook on its own, likely needs to be extracted from the Tangle Restaking Parachain via a query/subscription. - const [rate] = useState(1.0); - const [selectedChainId, setSelectedChainId] = useState( LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN, ); @@ -51,6 +48,11 @@ const LiquidUnstakeCard: FC = () => { const selectedChain = LIQUID_STAKING_CHAIN_MAP[selectedChainId]; + const exchangeRateOpt = useExchangeRate( + ExchangeRateType.LiquidToNative, + selectedChain.currency, + ); + const { result: areAllDelegationsOccupiedOpt } = useDelegationsOccupiedStatus( selectedChain.currency, ); @@ -104,12 +106,16 @@ const LiquidUnstakeCard: FC = () => { }, [executeRedeemTx, fromAmount, selectedChain.currency]); const toAmount = useMemo(() => { - if (fromAmount === null || rate === null) { + if ( + fromAmount === null || + exchangeRateOpt === null || + exchangeRateOpt.value === null + ) { return null; } - return fromAmount.muln(rate); - }, [fromAmount, rate]); + return fromAmount.muln(exchangeRateOpt.value); + }, [exchangeRateOpt, fromAmount]); const handleTokenSelect = useCallback(() => { setIsSelectTokenModalOpen(false); @@ -137,7 +143,7 @@ const LiquidUnstakeCard: FC = () => { chain={LiquidStakingChainId.TANGLE_RESTAKING_PARACHAIN} token={selectedChain.token} amount={fromAmount} - setAmount={setFromAmount} + onAmountChange={setFromAmount} placeholder={`0 ${LIQUID_STAKING_TOKEN_PREFIX}${selectedChain.token}`} rightElement={stakedBalance} isTokenLiquidVariant @@ -161,37 +167,19 @@ const LiquidUnstakeCard: FC = () => { {/* Details */}
- - 1 {selectedChain.token}{' '} - = {rate} {LIQUID_STAKING_TOKEN_PREFIX} - {selectedChain.token} - - } + - - 0.001984 {selectedChain.token} - - } + - - 7 days - - } - /> +
{areAllDelegationsOccupied?.isTrue && ( @@ -234,39 +222,4 @@ const LiquidUnstakeCard: FC = () => { ); }; -type DetailItemProps = { - title: string; - tooltip?: string; - value: string | ReactNode; -}; - -/** @internal */ -const DetailItem: FC = ({ title, tooltip, value }) => { - return ( -
-
- - {title} - - - {tooltip !== undefined && ( - - } - content={tooltip} - overrideTooltipBodyProps={{ - className: 'max-w-[350px]', - }} - /> - )} -
- - - {value} - -
- ); -}; - export default LiquidUnstakeCard; diff --git a/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemFeeDetailItem.tsx similarity index 93% rename from apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx rename to apps/tangle-dapp/components/LiquidStaking/MintAndRedeemFeeDetailItem.tsx index 85626fa53..4713e48be 100644 --- a/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/MintAndRedeemFeeDetailItem.tsx @@ -16,7 +16,7 @@ export type MintAndRedeemFeeDetailItemProps = { token: LiquidStakingToken; }; -const MintAndRedeemDetailItem: FC = ({ +const MintAndRedeemFeeDetailItem: FC = ({ isMinting, intendedAmount, token, @@ -54,4 +54,4 @@ const MintAndRedeemDetailItem: FC = ({ return ; }; -export default MintAndRedeemDetailItem; +export default MintAndRedeemFeeDetailItem; diff --git a/apps/tangle-dapp/components/LiquidStaking/UnstakePeriodDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/UnstakePeriodDetailItem.tsx new file mode 100644 index 000000000..0bd518815 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/UnstakePeriodDetailItem.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react'; + +import DetailItem from './DetailItem'; + +const UnstakePeriodDetailItem: FC = () => { + // TODO: Load this info from the chain. Currently using dummy data. + return ( + + 7 days + + } + /> + ); +}; + +export default UnstakePeriodDetailItem; diff --git a/apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts index a8237b65e..93911e5ae 100644 --- a/apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts +++ b/apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts @@ -30,7 +30,6 @@ const useExchangeRate = ( const exchangeRate = useMemo | null>(() => { if (tokenPoolAmount === null || lstTotalIssuance === null) { - console.debug(tokenPoolAmount?.toString(), lstTotalIssuance?.toString()); return null; } From afd9edfe0d461ef374b7d8464e56954721c2d77b Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 17 Jul 2024 01:46:44 -0400 Subject: [PATCH 34/53] fix(tangle-dapp): Fix nesting errors --- .../components/LiquidStaking/IconButton.tsx | 2 +- .../LiquidStaking/ParachainWalletBalance.tsx | 27 ++++++++++--------- .../LiquidStaking/UnstakePeriodDetailItem.tsx | 4 +-- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx b/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx index 75cdab856..8acb16900 100644 --- a/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/IconButton.tsx @@ -23,7 +23,7 @@ const IconButton: FC = ({ }) => { return ( - +
- {/* TODO: This should actually only apply to unstaking, since when staking the user itself is the liquidity provider, since they are minting LSTs. */} - {exchangeRateOpt?.isEmpty && ( - - )} - From 4f4dd397032e2520e5827c5fc3bf16d8fa5adb58 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 19 Jul 2024 23:45:44 -0400 Subject: [PATCH 47/53] style(tangle-dapp): Fix some styling inconsistencies --- .yarnrc | 5 --- .../LiquidStaking/LiquidStakingInput.tsx | 26 ++++++----- .../LiquidStaking/ParachainWalletBalance.tsx | 43 +++++++++++++++---- .../LiquidStaking/SelectValidators.tsx | 25 +++++------ 4 files changed, 60 insertions(+), 39 deletions(-) delete mode 100644 .yarnrc diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index 85b738b8d..000000000 --- a/.yarnrc +++ /dev/null @@ -1,5 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -yarn-path ".yarn/releases/yarn-1.22.22.cjs" diff --git a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx index fbf60f61f..c0176d1fe 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LiquidStakingInput.tsx @@ -110,7 +110,7 @@ const LiquidStakingInput: FC = ({ )} >
- + {rightElement}
@@ -146,7 +146,7 @@ const LiquidStakingInput: FC = ({ }; type ChainSelectorProps = { - selectedChain: LiquidStakingChainId; + selectedChainId: LiquidStakingChainId; /** * If this function is not provided, the selector will be @@ -155,7 +155,11 @@ type ChainSelectorProps = { setChain?: (newChain: LiquidStakingChainId) => void; }; -const ChainSelector: FC = ({ selectedChain, setChain }) => { +/** @internal */ +const ChainSelector: FC = ({ + selectedChainId, + setChain, +}) => { const isReadOnly = setChain === undefined; const base = ( @@ -167,10 +171,10 @@ const ChainSelector: FC = ({ selectedChain, setChain }) => { isReadOnly && 'px-3', )} > - + - {LS_CHAIN_TO_NETWORK_NAME[selectedChain]} + {LS_CHAIN_TO_NETWORK_NAME[selectedChainId]} {!isReadOnly && } @@ -187,16 +191,16 @@ const ChainSelector: FC = ({ selectedChain, setChain }) => {