From abecde9007d66a306347a324f116a244d86c9623 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 3 Sep 2024 05:27:20 -0400 Subject: [PATCH 01/54] feat(tangle-dapp): Create table foundations --- apps/tangle-dapp/app/liquid-staking/page.tsx | 3 + .../LiquidStaking/LsValidatorTable.tsx | 1 + .../LiquidStaking/StakedAssetsTable.tsx | 2 +- .../UnstakeRequestsTable.tsx | 2 +- .../components/ToggleableRadioInput.tsx | 29 +++ .../components/tableCells/TokenAmountCell.tsx | 7 +- .../constants/liquidStaking/types.ts | 12 ++ .../containers/ParachainPoolsTable.tsx | 193 ++++++++++++++++++ apps/tangle-dapp/hooks/useTokenPrice.ts | 17 ++ 9 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 apps/tangle-dapp/components/ToggleableRadioInput.tsx create mode 100644 apps/tangle-dapp/containers/ParachainPoolsTable.tsx create mode 100644 apps/tangle-dapp/hooks/useTokenPrice.ts diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index f3a67f350..713edb41e 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -7,6 +7,7 @@ import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeC import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; +import ParachainPoolsTable from '../../containers/ParachainPoolsTable'; import useSearchParamState from '../../hooks/useSearchParamState'; import TabListItem from '../restake/TabListItem'; import TabsList from '../restake/TabsList'; @@ -46,6 +47,8 @@ const LiquidStakingTokenPage: FC = () => {
{isStaking ? : } + + void 0} />
); diff --git a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx index e4666e6ed..1fbc03b0a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx @@ -38,6 +38,7 @@ const DEFAULT_PAGINATION: PaginationState = { }; const SELECTED_ITEMS_COLUMN_SORT = { + // TODO: Need to update the correct id. It seems that the `id` field no longer exists in the row's type. Need to statically-type-link-it instead of hard coding it, to avoid the same bug in the future. id: 'id', desc: false, } as const satisfies ColumnSort; diff --git a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx index 3080e8e86..0c14e7c01 100644 --- a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx @@ -58,7 +58,7 @@ const columns = [ columnHelper.accessor('amount', { header: () => , cell: (props) => { - return ; + return ; }, }), ]; diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index 06d971bc3..7c0871a8b 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -153,7 +153,7 @@ const COLUMNS = [ return ( diff --git a/apps/tangle-dapp/components/ToggleableRadioInput.tsx b/apps/tangle-dapp/components/ToggleableRadioInput.tsx new file mode 100644 index 000000000..2885bffa5 --- /dev/null +++ b/apps/tangle-dapp/components/ToggleableRadioInput.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react'; +import { twMerge } from 'tailwind-merge'; + +export type ToggleableRadioInputProps = { + isChecked: boolean; + onToggle: () => void; +}; + +const ToggleableRadioInput: FC = ({ + isChecked, + onToggle, +}) => { + return ( + + ); +}; + +export default ToggleableRadioInput; diff --git a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx index 9e7dbfcb2..a07c92cff 100644 --- a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx +++ b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx @@ -9,14 +9,14 @@ import formatTangleBalance from '../../utils/formatTangleBalance'; export type TokenAmountCellProps = { amount: BN; className?: string; - tokenSymbol?: string; + symbol?: string; decimals?: number; }; const TokenAmountCell: FC = ({ amount, className, - tokenSymbol, + symbol, decimals, }) => { const { nativeTokenSymbol } = useNetworkStore(); @@ -31,6 +31,7 @@ const TokenAmountCell: FC = ({ // Show small amounts. Without this, small amounts would // be displayed as 0. fractionMaxLength: undefined, + includeCommas: true, }); }, [amount, decimals]); @@ -49,7 +50,7 @@ const TokenAmountCell: FC = ({ {decimalPart !== undefined && `.${decimalPart}`}{' '} - {typeof tokenSymbol === 'string' ? tokenSymbol : nativeTokenSymbol} + {typeof symbol === 'string' ? symbol : nativeTokenSymbol} ); diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index e9353e2a8..9e963fea9 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -10,6 +10,8 @@ import { ProtocolEntity, } from '../../data/liquidStaking/adapter'; import { CrossChainTimeUnit } from '../../utils/CrossChainTime'; +import { SubstrateAddress } from '../../types/utils'; +import { TANGLE_MAINNET_SS58_PREFIX } from '../../../../libs/dapp-config/src/constants/tangle'; export enum LsProtocolId { POLKADOT, @@ -137,3 +139,13 @@ export type LsNetwork = { defaultProtocolId: LsProtocolId; protocols: LsProtocolDef[]; }; + +export type LsParachainPool = { + id: string; + owner: SubstrateAddress; + ownerStaked: BN; + chainId: LsParachainChainId; + validators: SubstrateAddress[]; + totalStaked: BN; + apyPermill: number; +}; diff --git a/apps/tangle-dapp/containers/ParachainPoolsTable.tsx b/apps/tangle-dapp/containers/ParachainPoolsTable.tsx new file mode 100644 index 000000000..fb6d220f9 --- /dev/null +++ b/apps/tangle-dapp/containers/ParachainPoolsTable.tsx @@ -0,0 +1,193 @@ +'use client'; + +import { BN } from '@polkadot/util'; +import { + createColumnHelper, + getCoreRowModel, + getExpandedRowModel, + getPaginationRowModel, + getSortedRowModel, + RowSelectionState, + Updater, + useReactTable, +} from '@tanstack/react-table'; +import { Table, Typography } from '@webb-tools/webb-ui-components'; +import assert from 'assert'; +import { FC, useCallback, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; + +import StatItem from '../components/StatItem'; +import TokenAmountCell from '../components/tableCells/TokenAmountCell'; +import TableCellWrapper from '../components/tables/TableCellWrapper'; +import ToggleableRadioInput from '../components/ToggleableRadioInput'; +import { + LsParachainPool, + LsProtocolId, +} from '../constants/liquidStaking/types'; +import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; + +const COLUMN_HELPER = createColumnHelper(); + +const COLUMNS = [ + COLUMN_HELPER.accessor('id', { + header: () => 'Asset ID', + cell: (props) => ( + +
+ + props.row.toggleSelected(!props.row.getIsSelected()) + } + /> + + + {props.getValue()} + +
+
+ ), + sortingFn: (rowA, rowB) => { + // NOTE: the sorting is reversed by default + return rowB.original.id.localeCompare(rowA.original.id); + }, + sortDescFirst: true, + }), + COLUMN_HELPER.accessor('validators', { + header: () => 'Validators', + cell: (props) => ( + + + + ), + }), + COLUMN_HELPER.accessor('ownerStaked', { + header: () => "Owner's Stake", + cell: (props) => { + const protocol = getLsProtocolDef(props.row.original.chainId); + + return ( + + + + ); + }, + }), + COLUMN_HELPER.accessor('totalStaked', { + header: () => 'Total Staked (TVL)', + cell: (props) => { + const protocol = getLsProtocolDef(props.row.original.chainId); + + return ( + + + + ); + }, + }), + COLUMN_HELPER.accessor('apyPermill', { + header: () => 'APY', + cell: (props) => ( + + + + ), + }), +]; + +export type ParachainPoolsTableProps = { + setSelectedPoolId: (poolId: string | null) => void; +}; + +const ParachainPoolsTable: FC = ({ + setSelectedPoolId, +}) => { + const [rowSelectionState, setRowSelectionState] = useState( + {}, + ); + + const handleRowSelectionChange = useCallback( + (updaterOrValue: Updater) => { + const newSelectionState = + typeof updaterOrValue === 'function' + ? updaterOrValue(rowSelectionState) + : updaterOrValue; + + const selectedRowIds = Object.keys(newSelectionState).filter( + (rowId) => newSelectionState[rowId], + ); + + assert(selectedRowIds.length <= 1, 'Only one row can ever be selected'); + setSelectedPoolId(selectedRowIds.length > 0 ? selectedRowIds[0] : null); + setRowSelectionState(newSelectionState); + }, + [rowSelectionState, setSelectedPoolId], + ); + + const rows: LsParachainPool[] = [ + { + id: 'abcdXYZ123', + owner: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' as any, + chainId: LsProtocolId.POLKADOT, + apyPermill: 0.3, + ownerStaked: new BN(123456).mul(new BN(10).pow(new BN(18))), + validators: [], + totalStaked: new BN(223456).mul(new BN(10).pow(new BN(18))), + }, + ]; + + // TODO: Row selection not updating/refreshing the UI. + const table = useReactTable({ + data: rows, + columns: COLUMNS, + state: { + rowSelection: rowSelectionState, + }, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableRowSelection: true, + autoResetPageIndex: false, + enableSortingRemoval: false, + // Use radio-style, single-row selection. + enableMultiRowSelection: false, + onRowSelectionChange: handleRowSelectionChange, + }); + + return ( + + ); +}; + +export default ParachainPoolsTable; diff --git a/apps/tangle-dapp/hooks/useTokenPrice.ts b/apps/tangle-dapp/hooks/useTokenPrice.ts new file mode 100644 index 000000000..3d0a84d2f --- /dev/null +++ b/apps/tangle-dapp/hooks/useTokenPrice.ts @@ -0,0 +1,17 @@ +export enum Currency { + USD, + EUR, +} + +const useTokenPrice = ( + _tokenSymbol: string, + _currency: Currency = Currency.USD, +) => { + const price: number | Error | null = 123_456; + + // TODO: Awaiting implementation. Meanwhile, this hook is used as a placeholder. The idea is that by once we implement this hook, it should automatically reflect the token price in the consumers of this hook, instead of having to manually integrate it into the codebase. + + return price; +}; + +export default useTokenPrice; From 68cb876e0d743ede5a165f69534de684a1052e89 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 3 Sep 2024 06:06:28 -0400 Subject: [PATCH 02/54] refactor(tangle-dapp): Remove `useTokenPrice` hook --- apps/tangle-dapp/hooks/useTokenPrice.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 apps/tangle-dapp/hooks/useTokenPrice.ts diff --git a/apps/tangle-dapp/hooks/useTokenPrice.ts b/apps/tangle-dapp/hooks/useTokenPrice.ts deleted file mode 100644 index 3d0a84d2f..000000000 --- a/apps/tangle-dapp/hooks/useTokenPrice.ts +++ /dev/null @@ -1,17 +0,0 @@ -export enum Currency { - USD, - EUR, -} - -const useTokenPrice = ( - _tokenSymbol: string, - _currency: Currency = Currency.USD, -) => { - const price: number | Error | null = 123_456; - - // TODO: Awaiting implementation. Meanwhile, this hook is used as a placeholder. The idea is that by once we implement this hook, it should automatically reflect the token price in the consumers of this hook, instead of having to manually integrate it into the codebase. - - return price; -}; - -export default useTokenPrice; From 09df2ab0ac11688f5a5c87e0f094de543ec3ad87 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 5 Sep 2024 04:49:18 -0400 Subject: [PATCH 03/54] feat(tangle-dapp): Add missing columns --- .../LiquidStaking/LsValidatorTable.tsx | 5 +- .../LiquidStaking/StakedAssetsTable.tsx | 136 ---------- .../UnstakePeriodDetailItem.tsx | 7 +- .../UnstakeRequestsTable.tsx | 1 - .../NominationsTable/NominationsTable.tsx | 14 +- .../components/PayoutTable/PayoutTable.tsx | 18 +- .../components/ToggleableRadioInput.tsx | 3 + .../ValidatorSelectionTable.tsx | 1 - .../ValidatorTable/ValidatorTable.tsx | 9 +- .../components/tableCells/TokenAmountCell.tsx | 5 +- .../constants/liquidStaking/types.ts | 1 + .../DelegateTxContainer/BondTokens.tsx | 2 +- .../containers/ParachainPoolsTable.tsx | 243 +++++++++++++----- .../data/liquidStaking/adapters/astar.tsx | 6 +- .../data/liquidStaking/adapters/manta.tsx | 6 +- .../data/liquidStaking/adapters/moonbeam.tsx | 6 +- .../data/liquidStaking/adapters/phala.tsx | 6 +- .../data/liquidStaking/adapters/polkadot.tsx | 6 +- .../data/liquidStaking/fetchHelpers.ts | 2 + .../useLsValidatorSelectionTableColumns.tsx | 72 ++---- apps/tangle-dapp/utils/formatBn.ts | 11 +- .../utils/liquidStaking/stringifyTimeUnit.ts | 5 +- apps/tangle-dapp/utils/pluralize.ts | 8 + .../components/AvatarGroup/AvatarGroup.tsx | 2 +- .../CopyWithTooltip/CopyWithTooltip.tsx | 44 ++-- .../src/components/Filter/utils.ts | 1 + .../src/components/buttons/Button.tsx | 1 + 27 files changed, 279 insertions(+), 342 deletions(-) delete mode 100644 apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx create mode 100644 apps/tangle-dapp/utils/pluralize.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx index 1fbc03b0a..1a749d8c3 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx @@ -31,6 +31,7 @@ import { LiquidStakingItem, LiquidStakingItemType, } from '../../types/liquidStaking'; +import pluralize from '../../utils/pluralize'; const DEFAULT_PAGINATION: PaginationState = { pageIndex: 0, @@ -186,7 +187,7 @@ export const LsValidatorTable = () => { } - placeholder="Search" + placeholder={`Search ${tableTitle.toLowerCase()}...`} value={searchValue} onChange={(newSearchValue) => setSearchValue(newSearchValue)} className="mb-1" @@ -216,7 +217,7 @@ export const LsValidatorTable = () => { canNextPage={table.getCanNextPage()} nextPage={table.nextPage} setPageIndex={table.setPageIndex} - title={itemText + 's'} + title={pluralize(itemText.toLowerCase(), data.length > 1)} className="!px-0 !py-0 !pt-6" /> )} diff --git a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx deleted file mode 100644 index 0c14e7c01..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/StakedAssetsTable.tsx +++ /dev/null @@ -1,136 +0,0 @@ -'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 { InformationLine } from '@webb-tools/icons'; -import { - Avatar, - AvatarGroup, - fuzzyFilter, - Table, - Typography, -} 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 AddressLink from './AddressLink'; - -type StakedAssetItem = { - id: HexString; - validators: AnySubstrateAddress[]; - amount: BN; -}; - -const columnHelper = createColumnHelper(); - -const columns = [ - columnHelper.accessor('id', { - header: () => , - cell: (props) => { - return ; - }, - }), - columnHelper.accessor('validators', { - header: () => ( - - ), - cell: (props) => { - return ( - - {props.getValue().map((address, index) => ( - - ))} - - ); - }, - }), - columnHelper.accessor('amount', { - header: () => , - cell: (props) => { - return ; - }, - }), -]; - -const StakedAssetsTable: FC = () => { - // TODO: Mock data. - const testAddresses = [ - '0x3a7f9e8c14b7d2f5', - '0xd5c4a2b1f3e8c7d9', - ] as AnySubstrateAddress[]; - - const data: StakedAssetItem[] = [ - { - id: '0x3a7f9e8c14b7d2f5', - validators: testAddresses, - amount: new BN(100), - }, - { - id: '0xd5c4a2b1f3e8c7d9', - validators: testAddresses, - amount: new BN(123), - }, - { - id: '0x9b3e47d8a5c2f1e4', - validators: testAddresses, - 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/stakeAndUnstake/UnstakePeriodDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/UnstakePeriodDetailItem.tsx index b1ca2efb3..f6778ccad 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/UnstakePeriodDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/UnstakePeriodDetailItem.tsx @@ -4,6 +4,7 @@ import { FC } from 'react'; import { LsProtocolId } from '../../../constants/liquidStaking/types'; import CrossChainTime from '../../../utils/CrossChainTime'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; +import pluralize from '../../../utils/pluralize'; import DetailItem from './DetailItem'; export type UnstakePeriodDetailItemProps = { @@ -28,13 +29,11 @@ const UnstakePeriodDetailItem: FC = ({ ); const days = unlockPeriod.toDays(); - - // TODO: Special case for 0 days? - const plurality = days > 1 ? 'days' : 'day'; const roundedDays = Math.round(days); return { - unit: plurality, + // TODO: Does 0 days mean it's past or unlocking today? + unit: days === 0 ? 'today' : pluralize('day', days > 1), value: roundedDays, isEstimate: days !== roundedDays, }; diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index 7c0871a8b..b776a3efa 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -155,7 +155,6 @@ const COLUMNS = [ amount={props.getValue()} symbol={tokenSymbol} decimals={props.row.original.decimals} - className="text-left" /> ); }, diff --git a/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx b/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx index 23f49d734..3f087805c 100644 --- a/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx +++ b/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx @@ -50,11 +50,7 @@ const columns = [ : identityName} - + ); }, @@ -77,18 +73,14 @@ const columns = [ }), columnHelper.accessor('selfStakeAmount', { header: () => , - cell: (props) => ( - - ), + cell: (props) => , sortingFn: sortBnValueForNomineeOrValidator, }), columnHelper.accessor('totalStakeAmount', { header: () => ( ), - cell: (props) => ( - - ), + cell: (props) => , sortingFn: sortBnValueForNomineeOrValidator, }), columnHelper.accessor('nominatorCount', { diff --git a/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx b/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx index d22d363c0..09cc26659 100644 --- a/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx +++ b/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx @@ -84,11 +84,7 @@ const PayoutTable: FC = ({ {identity === address ? shortenString(address, 6) : identity} - + ); }, @@ -106,9 +102,7 @@ const PayoutTable: FC = ({ header: () => ( ), - cell: (props) => ( - - ), + cell: (props) => , sortingFn: sortBnValueForPayout, }), columnHelper.accessor('nominators', { @@ -137,9 +131,7 @@ const PayoutTable: FC = ({ header: () => ( ), - cell: (props) => ( - - ), + cell: (props) => , sortingFn: sortBnValueForPayout, }), columnHelper.accessor('nominatorTotalReward', { @@ -147,9 +139,7 @@ const PayoutTable: FC = ({ ), cell: (props) => { - return ( - - ); + return ; }, sortingFn: sortBnValueForPayout, }), diff --git a/apps/tangle-dapp/components/ToggleableRadioInput.tsx b/apps/tangle-dapp/components/ToggleableRadioInput.tsx index 2885bffa5..6063fd13d 100644 --- a/apps/tangle-dapp/components/ToggleableRadioInput.tsx +++ b/apps/tangle-dapp/components/ToggleableRadioInput.tsx @@ -14,6 +14,9 @@ const ToggleableRadioInput: FC = ({ = ({ diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index 7ed0a40ed..cc576829b 100644 --- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx +++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx @@ -50,18 +50,14 @@ const getTableColumns = (isWaiting?: boolean) => [ className="justify-start" /> ), - cell: (props) => ( - - ), + cell: (props) => , sortingFn: sortBnValueForNomineeOrValidator, }), columnHelper.accessor('selfStakeAmount', { header: () => ( ), - cell: (props) => ( - - ), + cell: (props) => , sortingFn: sortBnValueForNomineeOrValidator, }), ]), @@ -147,7 +143,6 @@ const ValidatorTable: FC = ({ diff --git a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx index a07c92cff..e18495caa 100644 --- a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx +++ b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx @@ -28,9 +28,6 @@ const TokenAmountCell: FC = ({ } return formatBn(amount, decimals, { - // Show small amounts. Without this, small amounts would - // be displayed as 0. - fractionMaxLength: undefined, includeCommas: true, }); }, [amount, decimals]); @@ -42,7 +39,7 @@ const TokenAmountCell: FC = ({ return ( diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 9e963fea9..6d40dbe39 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -148,4 +148,5 @@ export type LsParachainPool = { validators: SubstrateAddress[]; totalStaked: BN; apyPermill: number; + commissionPermill: number; }; diff --git a/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx b/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx index 6cf254ad7..d1e1da597 100644 --- a/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx +++ b/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx @@ -59,7 +59,7 @@ const BondTokens: FC = ({ textToCopy={nominatorAddress} isButton={false} iconSize="lg" - className="text-mono-160 dark:text-mono-80 cursor-pointer" + className="text-mono-160 dark:text-mono-80" /> diff --git a/apps/tangle-dapp/containers/ParachainPoolsTable.tsx b/apps/tangle-dapp/containers/ParachainPoolsTable.tsx index fb6d220f9..afa58dd7c 100644 --- a/apps/tangle-dapp/containers/ParachainPoolsTable.tsx +++ b/apps/tangle-dapp/containers/ParachainPoolsTable.tsx @@ -5,26 +5,38 @@ import { createColumnHelper, getCoreRowModel, getExpandedRowModel, + getFilteredRowModel, getPaginationRowModel, getSortedRowModel, + PaginationState, RowSelectionState, Updater, useReactTable, } from '@tanstack/react-table'; -import { Table, Typography } from '@webb-tools/webb-ui-components'; +import { ArrowRight, ChainIcon, Search } from '@webb-tools/icons'; +import { + Avatar, + AvatarGroup, + Button, + CopyWithTooltip, + fuzzyFilter, + Input, + Table, + Typography, +} from '@webb-tools/webb-ui-components'; import assert from 'assert'; import { FC, useCallback, useState } from 'react'; -import { twMerge } from 'tailwind-merge'; -import StatItem from '../components/StatItem'; +import { GlassCard } from '../components'; +import { StringCell } from '../components/tableCells'; import TokenAmountCell from '../components/tableCells/TokenAmountCell'; -import TableCellWrapper from '../components/tables/TableCellWrapper'; import ToggleableRadioInput from '../components/ToggleableRadioInput'; import { LsParachainPool, LsProtocolId, } from '../constants/liquidStaking/types'; import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; +import pluralize from '../utils/pluralize'; const COLUMN_HELPER = createColumnHelper(); @@ -32,20 +44,18 @@ const COLUMNS = [ COLUMN_HELPER.accessor('id', { header: () => 'Asset ID', cell: (props) => ( - -
- - props.row.toggleSelected(!props.row.getIsSelected()) - } - /> - - - {props.getValue()} - -
-
+
+ props.row.toggleSelected(!props.row.getIsSelected())} + /> + + + {props.getValue()} + + + +
), sortingFn: (rowA, rowB) => { // NOTE: the sorting is reversed by default @@ -53,31 +63,67 @@ const COLUMNS = [ }, sortDescFirst: true, }), - COLUMN_HELPER.accessor('validators', { - header: () => 'Validators', + COLUMN_HELPER.accessor('chainId', { + header: () => 'Chain', + cell: (props) => { + const chain = getLsProtocolDef(props.row.original.chainId); + + return ( +
+ + + + {chain.name} + +
+ ); + }, + sortingFn: (rowA, rowB) => { + const chainA = getLsProtocolDef(rowA.original.chainId); + const chainB = getLsProtocolDef(rowB.original.chainId); + + return chainA.name.localeCompare(chainB.name); + }, + }), + COLUMN_HELPER.accessor('owner', { + header: () => 'Owner', cell: (props) => ( - - - + ), }), + COLUMN_HELPER.accessor('validators', { + header: () => 'Validators', + cell: (props) => + props.row.original.validators.length === 0 ? ( + 'None' + ) : ( + + {props.row.original.validators.map((substrateAddress) => ( + + ))} + + ), + }), COLUMN_HELPER.accessor('ownerStaked', { header: () => "Owner's Stake", cell: (props) => { const protocol = getLsProtocolDef(props.row.original.chainId); return ( - - - + ); }, }), @@ -87,30 +133,40 @@ const COLUMNS = [ const protocol = getLsProtocolDef(props.row.original.chainId); return ( - - - + ); }, }), + COLUMN_HELPER.accessor('commissionPermill', { + header: () => 'Commission', + cell: (props) => ( + + ), + }), COLUMN_HELPER.accessor('apyPermill', { header: () => 'APY', cell: (props) => ( - - - + ), }), ]; +const DEFAULT_PAGINATION_STATE: PaginationState = { + pageIndex: 0, + pageSize: 10, +}; + export type ParachainPoolsTableProps = { setSelectedPoolId: (poolId: string | null) => void; }; @@ -118,6 +174,12 @@ export type ParachainPoolsTableProps = { const ParachainPoolsTable: FC = ({ setSelectedPoolId, }) => { + const [searchQuery, setSearchQuery] = useState(''); + + const [paginationState, setPaginationState] = useState( + DEFAULT_PAGINATION_STATE, + ); + const [rowSelectionState, setRowSelectionState] = useState( {}, ); @@ -146,47 +208,98 @@ const ParachainPoolsTable: FC = ({ owner: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' as any, chainId: LsProtocolId.POLKADOT, apyPermill: 0.3, + ownerStaked: new BN(1234560000000000), + validators: [ + '5FfP4SU5jXY9ZVfR1kY1pUXuJ3G1bfjJoQDRz4p7wSH3Mmdn' as any, + '5FnL9Pj3NX7E6yC1a2tN4kVdR7y2sAqG8vRsF4PN6yLeu2mL' as any, + '5CF8H7P3qHfZzBtPXH6G6e3Wc3V2wVn6tQHgYJ5HGKK1eC5z' as any, + '5GV8vP8Bh3fGZm2P7YNxMzUd9Wy4k3RSRvkq7RXVjxGGM1cy' as any, + '5DPy4XU6nNV2t2NQkz3QvPB2X5GJ5ZJ1wqMzC4Rxn2WLbXVD' as any, + ], + totalStaked: new BN(12300003567), + commissionPermill: 0.1, + }, + { + id: 'zxcvbnm', + owner: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' as any, + chainId: LsProtocolId.POLKADOT, + apyPermill: 0.3, ownerStaked: new BN(123456).mul(new BN(10).pow(new BN(18))), - validators: [], + validators: [ + '5FfP4SU5jXY9ZVfR1kY1pUXuJ3G1bfjJoQDRz4p7wSH3Mmdn' as any, + '5FnL9Pj3NX7E6yC1a2tN4kVdR7y2sAqG8vRsF4PN6yLeu2mL' as any, + '5CF8H7P3qHfZzBtPXH6G6e3Wc3V2wVn6tQHgYJ5HGKK1eC5z' as any, + '5GV8vP8Bh3fGZm2P7YNxMzUd9Wy4k3RSRvkq7RXVjxGGM1cy' as any, + ], totalStaked: new BN(223456).mul(new BN(10).pow(new BN(18))), + commissionPermill: 0.1, }, ]; - // TODO: Row selection not updating/refreshing the UI. + // TODO: Sort by chain by default, otherwise rows would look messy if there are many pools from different chains. const table = useReactTable({ + getRowId: (row) => row.id, data: rows, columns: COLUMNS, state: { rowSelection: rowSelectionState, + globalFilter: searchQuery, + pagination: paginationState, }, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + globalFilterFn: fuzzyFilter, enableRowSelection: true, autoResetPageIndex: false, enableSortingRemoval: false, // Use radio-style, single-row selection. enableMultiRowSelection: false, + onPaginationChange: setPaginationState, onRowSelectionChange: handleRowSelectionChange, + onGlobalFilterChange: (newSearchQuery) => { + setPaginationState(DEFAULT_PAGINATION_STATE); + setSearchQuery(newSearchQuery); + }, }); return ( -
+
+ + + + + Select Token Vault + + + setSearchQuery(newValue)} + isControlled + rightIcon={} + /> + +
1)} + isPaginated + totalRecords={rows.length} + thClassName="!bg-inherit border-t-0 bg-mono-0 !px-3 !py-2 whitespace-nowrap" + trClassName="!bg-inherit" + tdClassName="!bg-inherit !px-3 !py-2 whitespace-nowrap" + /> + + ); }; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/astar.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/astar.tsx index f2cc9c07d..4862d2649 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/astar.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/astar.tsx @@ -126,11 +126,7 @@ const getTableColumns: GetTableColumnsFn = ( {dappName === address ? shortenString(address, 8) : dappName} - + ); diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/manta.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/manta.tsx index d36f43fce..dd9789dc5 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/manta.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/manta.tsx @@ -126,11 +126,7 @@ const getTableColumns: GetTableColumnsFn = ( {identity === address ? shortenString(address, 8) : identity} - + ); diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/moonbeam.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/moonbeam.tsx index 0c580b84d..677b28b51 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/moonbeam.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/moonbeam.tsx @@ -126,11 +126,7 @@ const getTableColumns: GetTableColumnsFn = ( {identity === address ? shortenString(address, 8) : identity} - + ); diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/phala.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/phala.tsx index e48a1142b..566989b51 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/phala.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/phala.tsx @@ -114,11 +114,7 @@ const getTableColumns: GetTableColumnsFn = ( #{id} - + ); diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx index 2ceb9186d..13cac11d4 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx @@ -127,11 +127,7 @@ const getTableColumns: GetTableColumnsFn = ( {identity === address ? shortenString(address, 8) : identity} - + ); diff --git a/apps/tangle-dapp/data/liquidStaking/fetchHelpers.ts b/apps/tangle-dapp/data/liquidStaking/fetchHelpers.ts index 0267c9a98..198ce50e0 100644 --- a/apps/tangle-dapp/data/liquidStaking/fetchHelpers.ts +++ b/apps/tangle-dapp/data/liquidStaking/fetchHelpers.ts @@ -235,6 +235,8 @@ export const fetchVaultsAndStakePools = async ( return poolsInPhalaBasePool.map((pool) => { const id = pool[0].args[0].toString(); + + // TODO: Avoid using `any`. const poolInfo = pool[1] as Option; let type = ''; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx b/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx index 99961bf68..a27faf765 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx +++ b/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { twMerge } from 'tailwind-merge'; import { StakingItemExternalLinkButton } from '../../components/LiquidStaking/StakingItemExternalLinkButton'; +import TokenAmountCell from '../../components/tableCells/TokenAmountCell'; import { Collator, Dapp, @@ -84,11 +85,7 @@ export const useLsValidatorSelectionTableColumns = ( {identity === address ? shortenString(address, 8) : identity} - + ); @@ -111,16 +108,11 @@ export const useLsValidatorSelectionTableColumns = ( ), cell: (props) => ( -
- - {formatBn(props.getValue(), props.row.original.chainDecimals) + - ` ${props.row.original.chainTokenSymbol}`} - -
+ ), sortingFn: sortValueStaked, }), @@ -140,7 +132,7 @@ export const useLsValidatorSelectionTableColumns = ( ), cell: (props) => ( -
+
- +
); @@ -230,16 +218,11 @@ export const useLsValidatorSelectionTableColumns = ( ), cell: (props) => ( -
- - {formatBn(props.getValue(), props.row.original.chainDecimals) + - ` ${props.row.original.chainTokenSymbol}`} - -
+ ), sortingFn: sortValueStaked, }), @@ -293,11 +276,7 @@ export const useLsValidatorSelectionTableColumns = ( #{id} - + ); @@ -346,16 +325,11 @@ export const useLsValidatorSelectionTableColumns = ( ), cell: (props) => ( -
- - {formatBn(props.getValue(), props.row.original.chainDecimals) + - ` ${props.row.original.chainTokenSymbol}`} - -
+ ), sortingFn: sortValueStaked, }), @@ -440,11 +414,7 @@ export const useLsValidatorSelectionTableColumns = ( {identity === address ? shortenString(address, 8) : identity} - + ); diff --git a/apps/tangle-dapp/utils/formatBn.ts b/apps/tangle-dapp/utils/formatBn.ts index a6c233a74..c1294f9ab 100644 --- a/apps/tangle-dapp/utils/formatBn.ts +++ b/apps/tangle-dapp/utils/formatBn.ts @@ -29,6 +29,7 @@ const DEFAULT_FORMAT_OPTIONS: FormatOptions = { trimTrailingZeroes: true, }; +// TODO: Break this function down into smaller local functions for improved legibility and modularity, since its logic is getting complex. Consider making it functional instead of modifying the various variables: Return {integerPart, fractionalPart} per transformation/function, so that it can be easily chainable monad-style. function formatBn( amount: BN, decimals: number, @@ -43,13 +44,19 @@ function formatBn( const integerPartBn = new BN(amount.toString()).div(chainUnitFactorBn); const remainderBn = amount.mod(chainUnitFactorBn); - let integerPart = integerPartBn.abs().toString(10); let fractionPart = remainderBn.abs().toString(10).padStart(decimals, '0'); - const amountStringLength = amount.toString().length; const partsLength = integerPart.length + fractionPart.length; + // Special case: If the integer part is 0, and options don't specify a + // fraction max length, then don't use the default value for the fraction + // max length. Instead keep it undefined. This is so that small, fractional + // amounts are always shown, which would otherwise be cut-off and shown as '0'. + if (integerPart === '0' && options?.fractionMaxLength === undefined) { + finalOptions.fractionMaxLength = undefined; + } + // 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 diff --git a/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts b/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts index 212761036..aa0cdb6d4 100644 --- a/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts +++ b/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts @@ -1,11 +1,12 @@ import { LsParachainSimpleTimeUnit } from '../../constants/liquidStaking/types'; +import pluralize from '../pluralize'; const stringifyTimeUnit = ( timeUnit: LsParachainSimpleTimeUnit, ): [number, string] => { - const plurality = timeUnit.value === 1 ? '' : 's'; + const unitString = pluralize(timeUnit.unit, timeUnit.value !== 1); - return [timeUnit.value, `${timeUnit.unit}${plurality}`] as const; + return [timeUnit.value, unitString] as const; }; export default stringifyTimeUnit; diff --git a/apps/tangle-dapp/utils/pluralize.ts b/apps/tangle-dapp/utils/pluralize.ts new file mode 100644 index 000000000..2d3bb1690 --- /dev/null +++ b/apps/tangle-dapp/utils/pluralize.ts @@ -0,0 +1,8 @@ +const pluralize = ( + value: T, + condition: boolean, +): T | `${T}s` => { + return condition ? value : `${value}s`; +}; + +export default pluralize; diff --git a/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx b/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx index a2981a1ca..2ae2f75b1 100644 --- a/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx +++ b/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx @@ -56,7 +56,7 @@ export const AvatarGroup = forwardRef( {extraAvatars > 0 && ( - +{extraAvatars} others + +{extraAvatars} other{extraAvatars > 1 && 's'} )} diff --git a/libs/webb-ui-components/src/components/CopyWithTooltip/CopyWithTooltip.tsx b/libs/webb-ui-components/src/components/CopyWithTooltip/CopyWithTooltip.tsx index 90bc29e70..c82e4eeb8 100644 --- a/libs/webb-ui-components/src/components/CopyWithTooltip/CopyWithTooltip.tsx +++ b/libs/webb-ui-components/src/components/CopyWithTooltip/CopyWithTooltip.tsx @@ -1,13 +1,14 @@ 'use client'; import { FileCopyLine } from '@webb-tools/icons/FileCopyLine'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { useCopyable } from '../../hooks'; import { Typography } from '../../typography/Typography'; import { Tooltip, TooltipBody, TooltipTrigger } from '../Tooltip'; import { Button } from '../buttons'; import { CopyWithTooltipProps, CopyWithTooltipUIProps } from './types'; +import { CheckLineIcon } from '@webb-tools/icons'; /** * The `CopyWithTooltip` component @@ -60,35 +61,48 @@ const CopyWithTooltipUI: React.FC = ({ copyLabel = 'Copy', }) => { const [isTooltipOpen, setIsTooltipOpen] = useState(false); + const IconTag = isCopied ? CheckLineIcon : FileCopyLine; + + const icon = ( + + ); + + const handleClick = useCallback(() => { + // Don't re-trigger the copy action if the text is already copied. + if (isCopied) { + return; + } + + onClick(); + }, [isCopied, onClick]); return ( - + setIsTooltipOpen(true)} onMouseLeave={() => setIsTooltipOpen(false)} - className={twMerge(isCopied ? 'cursor-not-allowed' : '', className)} - onClick={onClick} + className={twMerge( + isCopied ? 'cursor-default' : 'cursor-pointer', + className, + )} + onClick={handleClick} asChild > {isButton ? ( ) : ( - - - + {icon} )} + - {isCopied ? 'Copied!' : copyLabel} + {copyLabel} diff --git a/libs/webb-ui-components/src/components/Filter/utils.ts b/libs/webb-ui-components/src/components/Filter/utils.ts index 31dd1d1b1..1439dffa2 100644 --- a/libs/webb-ui-components/src/components/Filter/utils.ts +++ b/libs/webb-ui-components/src/components/Filter/utils.ts @@ -1,6 +1,7 @@ import { rankItem } from '@tanstack/match-sorter-utils'; import { FilterFn } from '@tanstack/react-table'; +// TODO: Find a way to avoid using `any` here, since `any` will propagate and possibly lead to logic bugs. export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { // Rank the item const itemRank = rankItem(row.getValue(columnId), value); diff --git a/libs/webb-ui-components/src/components/buttons/Button.tsx b/libs/webb-ui-components/src/components/buttons/Button.tsx index 9dcf02bad..b9af351ae 100644 --- a/libs/webb-ui-components/src/components/buttons/Button.tsx +++ b/libs/webb-ui-components/src/components/buttons/Button.tsx @@ -41,6 +41,7 @@ const Button = React.forwardRef((props, ref) => { isLoading, leftIcon, loadingText, + // TODO: Icons don't inherit the color of the button's variant, they just stay white. rightIcon, size = 'md', spinner, From 2d46984f29a49cdb4f33c5afb056284ed3ddfcd3 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 6 Sep 2024 05:52:45 -0400 Subject: [PATCH 04/54] refactor(tangle-dapp): Show corresponding table --- apps/tangle-dapp/app/liquid-staking/page.tsx | 18 +++++++++++++++--- .../LiquidStaking/LsValidatorTable.tsx | 3 ++- .../stakeAndUnstake/LsStakeCard.tsx | 1 + .../containers/ParachainPoolsTable.tsx | 19 ++++++++++--------- .../data/liquidStaking/useLsStore.ts | 17 +++++++++++++---- apps/tangle-dapp/utils/pluralize.ts | 2 +- 6 files changed, 42 insertions(+), 18 deletions(-) diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 713edb41e..8d0dfca38 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -8,7 +8,9 @@ import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnst import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; import ParachainPoolsTable from '../../containers/ParachainPoolsTable'; +import { useLsStore } from '../../data/liquidStaking/useLsStore'; import useSearchParamState from '../../hooks/useSearchParamState'; +import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId'; import TabListItem from '../restake/TabListItem'; import TabsList from '../restake/TabsList'; @@ -26,6 +28,10 @@ const LiquidStakingTokenPage: FC = () => { value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, }); + const { selectedProtocolId } = useLsStore(); + + const isParachainChain = isLsParachainChainId(selectedProtocolId); + return (
@@ -46,9 +52,15 @@ const LiquidStakingTokenPage: FC = () => {
- {isStaking ? : } - - void 0} /> + {isStaking ? ( + isParachainChain ? ( + + ) : ( + + ) + ) : ( + + )}
); diff --git a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx index 1a749d8c3..df09b6e14 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx @@ -45,7 +45,8 @@ const SELECTED_ITEMS_COLUMN_SORT = { } as const satisfies ColumnSort; export const LsValidatorTable = () => { - const { selectedProtocolId, setSelectedItems } = useLsStore(); + const { selectedProtocolId, setSelectedNetworkEntities: setSelectedItems } = + useLsStore(); const { isLoading, data, dataType } = useLsValidators(selectedProtocolId); const [searchValue, setSearchValue] = useState(''); const [rowSelection, setRowSelection] = useState({}); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index b983ab832..21e7b9cdf 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -68,6 +68,7 @@ const LsStakeCard: FC = () => { const selectedProtocol = getLsProtocolDef(selectedProtocolId); + // TODO: Not loading the correct protocol for: '?amount=123000000000000000000&protocol=7&network=1&action=stake'. When network=1, it switches to protocol=5 on load. Could this be because the protocol is reset to its default once the network is switched? useSearchParamSync({ key: LsSearchParamKey.PROTOCOL_ID, value: selectedProtocolId, diff --git a/apps/tangle-dapp/containers/ParachainPoolsTable.tsx b/apps/tangle-dapp/containers/ParachainPoolsTable.tsx index afa58dd7c..561fc8fea 100644 --- a/apps/tangle-dapp/containers/ParachainPoolsTable.tsx +++ b/apps/tangle-dapp/containers/ParachainPoolsTable.tsx @@ -35,6 +35,7 @@ import { LsParachainPool, LsProtocolId, } from '../constants/liquidStaking/types'; +import { useLsStore } from '../data/liquidStaking/useLsStore'; import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; import pluralize from '../utils/pluralize'; @@ -167,13 +168,8 @@ const DEFAULT_PAGINATION_STATE: PaginationState = { pageSize: 10, }; -export type ParachainPoolsTableProps = { - setSelectedPoolId: (poolId: string | null) => void; -}; - -const ParachainPoolsTable: FC = ({ - setSelectedPoolId, -}) => { +const ParachainPoolsTable: FC = () => { + const { setSelectedParachainPoolId } = useLsStore(); const [searchQuery, setSearchQuery] = useState(''); const [paginationState, setPaginationState] = useState( @@ -196,10 +192,15 @@ const ParachainPoolsTable: FC = ({ ); assert(selectedRowIds.length <= 1, 'Only one row can ever be selected'); - setSelectedPoolId(selectedRowIds.length > 0 ? selectedRowIds[0] : null); + + // TODO: Rows can only be selected, but once selected, one radio input/row must always remain selected. + setSelectedParachainPoolId( + selectedRowIds.length > 0 ? selectedRowIds[0] : null, + ); + setRowSelectionState(newSelectionState); }, - [rowSelectionState, setSelectedPoolId], + [rowSelectionState, setSelectedParachainPoolId], ); const rows: LsParachainPool[] = [ diff --git a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts index eb47616b3..7c6a97b74 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts @@ -6,24 +6,33 @@ import getLsNetwork from '../../utils/liquidStaking/getLsNetwork'; type State = { selectedNetworkId: LsNetworkId; selectedProtocolId: LsProtocolId; - selectedItems: Set; + selectedNetworkEntities: Set; + selectedParachainPoolId: string | null; }; type Actions = { setSelectedProtocolId: (newProtocolId: State['selectedProtocolId']) => void; - setSelectedItems: (selectedItems: State['selectedItems']) => void; setSelectedNetworkId: (newNetworkId: State['selectedNetworkId']) => void; + setSelectedParachainPoolId: (parachainPoolId: string) => void; + + setSelectedNetworkEntities: ( + selectedNetworkEntities: State['selectedNetworkEntities'], + ) => void; }; type Store = State & Actions; export const useLsStore = create((set) => ({ + selectedParachainPoolId: null, selectedNetworkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN, selectedProtocolId: LsProtocolId.POLKADOT, - selectedItems: new Set(), + selectedNetworkEntities: new Set(), + setSelectedParachainPoolId: (selectedParachainPoolId) => + set({ selectedParachainPoolId }), setSelectedProtocolId: (selectedChainId) => set({ selectedProtocolId: selectedChainId }), - setSelectedItems: (selectedItems) => set({ selectedItems }), + setSelectedNetworkEntities: (selectedNetworkEntities) => + set({ selectedNetworkEntities }), setSelectedNetworkId: (selectedNetworkId) => { const network = getLsNetwork(selectedNetworkId); const defaultProtocolId = network.defaultProtocolId; diff --git a/apps/tangle-dapp/utils/pluralize.ts b/apps/tangle-dapp/utils/pluralize.ts index 2d3bb1690..b2fc3f893 100644 --- a/apps/tangle-dapp/utils/pluralize.ts +++ b/apps/tangle-dapp/utils/pluralize.ts @@ -2,7 +2,7 @@ const pluralize = ( value: T, condition: boolean, ): T | `${T}s` => { - return condition ? value : `${value}s`; + return condition ? `${value}s` : value; }; export default pluralize; From 983421751e3ecbd059a778da7572075df9a92d3f Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sat, 7 Sep 2024 09:58:14 -0400 Subject: [PATCH 05/54] feat(tangle-dapp): Create `useLsPools` hook --- apps/tangle-dapp/app/liquid-staking/page.tsx | 6 +- .../components/LiquidStaking/AddressLink.tsx | 4 +- .../LiquidStaking/VaultsAndAssetsTable.tsx | 4 +- .../stakeAndUnstake/FeeDetailItem.tsx | 18 +-- .../stakeAndUnstake/LsFeeWarning.tsx | 8 +- .../stakeAndUnstake/SelectTokenModal.tsx | 8 +- .../stakeAndUnstake/TotalDetailItem.tsx | 20 +-- ...eLsFeePermill.ts => useLsFeePercentage.ts} | 12 +- .../constants/liquidStaking/types.ts | 16 +-- apps/tangle-dapp/constants/networks.ts | 9 +- ...rachainPoolsTable.tsx => LsPoolsTable.tsx} | 127 ++++++++---------- .../data/liquidStaking/adapters/polkadot.tsx | 5 +- .../data/liquidStaking/useLsPools.ts | 107 +++++++++++++++ .../data/liquidStaking/useParachainLsFees.ts | 8 +- apps/tangle-dapp/hooks/useApi.ts | 27 +++- apps/tangle-dapp/hooks/useApiRx.ts | 22 ++- apps/tangle-dapp/hooks/useNetworkFeatures.ts | 4 +- apps/tangle-dapp/types/index.ts | 1 + apps/tangle-dapp/types/utils.ts | 7 +- .../utils/assertAnySubstrateAddress.ts | 14 -- .../utils/assertSubstrateAddress.ts | 12 +- .../utils/isAnySubstrateAddress.ts | 4 +- apps/tangle-dapp/utils/permillToPercentage.ts | 2 +- apps/tangle-dapp/utils/polkadot/identity.ts | 5 +- .../utils/scaleAmountByPercentage.ts | 14 ++ .../tangle-dapp/utils/scaleAmountByPermill.ts | 14 -- libs/dapp-config/src/constants/tangle.ts | 4 +- .../src/constants/networks.ts | 10 +- 28 files changed, 303 insertions(+), 189 deletions(-) rename apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/{useLsFeePermill.ts => useLsFeePercentage.ts} (90%) rename apps/tangle-dapp/containers/{ParachainPoolsTable.tsx => LsPoolsTable.tsx} (71%) create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsPools.ts delete mode 100644 apps/tangle-dapp/utils/assertAnySubstrateAddress.ts create mode 100644 apps/tangle-dapp/utils/scaleAmountByPercentage.ts delete mode 100644 apps/tangle-dapp/utils/scaleAmountByPermill.ts diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 8d0dfca38..d6812958a 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -7,7 +7,7 @@ import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeC import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; -import ParachainPoolsTable from '../../containers/ParachainPoolsTable'; +import LsPoolsTable from '../../containers/LsPoolsTable'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; import useSearchParamState from '../../hooks/useSearchParamState'; import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId'; @@ -29,7 +29,7 @@ const LiquidStakingTokenPage: FC = () => { }); const { selectedProtocolId } = useLsStore(); - + const isParachainChain = isLsParachainChainId(selectedProtocolId); return ( @@ -54,7 +54,7 @@ const LiquidStakingTokenPage: FC = () => {
{isStaking ? ( isParachainChain ? ( - + ) : ( ) diff --git a/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx b/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx index 389a2918b..b899694aa 100644 --- a/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx @@ -3,10 +3,10 @@ import { ExternalLinkLine } from '@webb-tools/icons'; import { shortenString, Typography } from '@webb-tools/webb-ui-components'; import { FC, useCallback } from 'react'; -import { AnySubstrateAddress } from '../../types/utils'; +import { SubstrateAddress } from '../../types/utils'; export type AddressLinkProps = { - address: AnySubstrateAddress | HexString; + address: SubstrateAddress | HexString; }; const AddressLink: FC = ({ address }) => { diff --git a/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx index 1b7caf8c1..acdfa4e78 100644 --- a/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx @@ -14,6 +14,7 @@ import { twMerge } from 'tailwind-merge'; import useVaults from '../../app/liquid-staking/useVaults'; import StatItem from '../../components/StatItem'; import TableCellWrapper from '../../components/tables/TableCellWrapper'; +import { PagePath } from '../../types'; import { Asset, Vault } from '../../types/liquidStaking'; import LsTokenIcon from '../LsTokenIcon'; @@ -80,8 +81,7 @@ const vaultColumns = [ cell: ({ row }) => (
- {/* TODO: add proper href */} - + @@ -278,7 +263,7 @@ const ParachainPoolsTable: FC = () => { fw="bold" className="text-mono-200 dark:text-mono-0" > - Select Token Vault + Select Pool { ); }; -export default ParachainPoolsTable; +export default LsPoolsTable; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx index 13cac11d4..9e286c428 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx @@ -35,11 +35,10 @@ import { fetchTokenSymbol, } from '../fetchHelpers'; -const SS58_PREFIX = 0; const DECIMALS = 18; export type PolkadotValidator = { - address: SubstrateAddress; + address: SubstrateAddress; identity: string; commission: BN; apy?: number; @@ -70,7 +69,7 @@ const fetchValidators = async ( return { id: address.toString(), - address: assertSubstrateAddress(address.toString(), SS58_PREFIX), + address: assertSubstrateAddress(address.toString()), identity: identityName ?? address.toString(), totalValueStaked: totalValueStaked ?? BN_ZERO, apy: 0, diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts new file mode 100644 index 000000000..4f23b33e4 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -0,0 +1,107 @@ +import { BN, u8aToString } from '@polkadot/util'; +import { useCallback, useMemo } from 'react'; + +import { LsPool, LsProtocolId } from '../../constants/liquidStaking/types'; +import useApiRx from '../../hooks/useApiRx'; +import useNetworkFeatures from '../../hooks/useNetworkFeatures'; +import { NetworkFeature } from '../../types'; +import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; +import permillToPercentage from '../../utils/permillToPercentage'; + +const useLsPools = (): Map | null | Error => { + const networkFeatures = useNetworkFeatures(); + + if (!networkFeatures.includes(NetworkFeature.LsPools)) { + // TODO: Handle case where the active network doesn't support liquid staking pools. + } + + const { result: rawMetadataEntries } = useApiRx( + useCallback((api) => { + return api.query.lst.metadata.entries(); + }, []), + ); + + const { result: rawBondedPools } = useApiRx( + useCallback((api) => { + return api.query.lst.bondedPools.entries(); + }, []), + ); + + const tanglePools = useMemo(() => { + if (rawBondedPools === null) { + return null; + } + + return rawBondedPools.flatMap(([key, valueOpt]) => { + // Skip empty values. + if (valueOpt.isNone) { + return []; + } + + const tanglePool = valueOpt.unwrap(); + + // Ignore all non-open pools. + if (!tanglePool.state.isOpen) { + return []; + } + + return [[key.args[0].toNumber(), tanglePool] as const]; + }); + }, [rawBondedPools]); + + const poolsMap = useMemo(() => { + if (tanglePools === null) { + return null; + } + + const keyValuePairs = tanglePools.map(([id, tanglePool]) => { + const metadataEntryBytes = + rawMetadataEntries === null + ? undefined + : rawMetadataEntries.find( + ([idKey]) => idKey.args[0].toNumber() === id, + )?.[1]; + + const metadata = + metadataEntryBytes === undefined + ? undefined + : u8aToString(metadataEntryBytes); + + // TODO: Under what circumstances would this be `None`? During pool creation, the various addresses seem required, not optional. + const owner = assertSubstrateAddress( + tanglePool.roles.root.unwrap().toString(), + ); + + const commissionPercentage = tanglePool.commission.current.isNone + ? undefined + : permillToPercentage(tanglePool.commission.current.unwrap()[0]); + + const pool: LsPool = { + id, + metadata, + owner, + commissionPercentage, + // TODO: Dummy values. + apyPercentage: 0.1, + chainId: LsProtocolId.POLKADOT, + totalStaked: new BN(1234560000000000), + ownerStaked: new BN(12300003567), + validators: [ + '5FfP4SU5jXY9ZVfR1kY1pUXuJ3G1bfjJoQDRz4p7wSH3Mmdn' as any, + '5FnL9Pj3NX7E6yC1a2tN4kVdR7y2sAqG8vRsF4PN6yLeu2mL' as any, + '5CF8H7P3qHfZzBtPXH6G6e3Wc3V2wVn6tQHgYJ5HGKK1eC5z' as any, + '5GV8vP8Bh3fGZm2P7YNxMzUd9Wy4k3RSRvkq7RXVjxGGM1cy' as any, + '5DPy4XU6nNV2t2NQkz3QvPB2X5GJ5ZJ1wqMzC4Rxn2WLbXVD' as any, + ], + }; + + return [id, pool] as const; + }); + + return new Map(keyValuePairs); + }, [rawMetadataEntries, tanglePools]); + + return poolsMap; +}; + +export default useLsPools; diff --git a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts b/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts index a724c715d..b82a6577b 100644 --- a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts +++ b/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts @@ -10,12 +10,12 @@ const useParachainLsFees = () => { useCallback((api) => { return api.query.lstMinting.fees().pipe( map((fees) => { - const mintFee = permillToPercentage(fees[0]); - const redeemFee = permillToPercentage(fees[1]); + const mintFeePercentage = permillToPercentage(fees[0]); + const redeemFeePercentage = permillToPercentage(fees[1]); return { - mintFee, - redeemFee, + mintFeePercentage, + redeemFeePercentage, }; }), ); diff --git a/apps/tangle-dapp/hooks/useApi.ts b/apps/tangle-dapp/hooks/useApi.ts index 5c3011a84..4b5cc5b7f 100644 --- a/apps/tangle-dapp/hooks/useApi.ts +++ b/apps/tangle-dapp/hooks/useApi.ts @@ -2,6 +2,7 @@ import { ApiPromise } from '@polkadot/api'; import { useCallback, useEffect, useState } from 'react'; import useNetworkStore from '../context/useNetworkStore'; +import ensureError from '../utils/ensureError'; import { getApiPromise } from '../utils/polkadot'; import usePromise from './usePromise'; @@ -22,6 +23,7 @@ export type ApiFetcher = (api: ApiPromise) => Promise | T; */ function useApi(fetcher: ApiFetcher, overrideRpcEndpoint?: string) { const [result, setResult] = useState(null); + const [error, setError] = useState(null); const { rpcEndpoint } = useNetworkStore(); const { result: api } = usePromise( @@ -38,11 +40,32 @@ function useApi(fetcher: ApiFetcher, overrideRpcEndpoint?: string) { return; } - const newResult = fetcher(api); + let newResult; + + // Fetch the data, and catch any errors that are thrown. + // In certain cases, the fetcher may fail with an error. For example, + // if a pallet isn't available on the active chain. Another example would + // be if the active chain is mainnet, but the fetcher is trying to fetch + // data from a testnet pallet that hasn't been deployed to mainnet yet. + try { + newResult = fetcher(api); + } catch (possibleError) { + const error = ensureError(possibleError); + + console.error( + 'Error while fetching data, this can happen when TypeScript type definitions are outdated or accessing pallets on the wrong chain:', + error, + ); + + setError(error); + + return; + } if (newResult instanceof Promise) { newResult.then((data) => setResult(data)); } else { + setError(null); setResult(newResult); } }, [api, fetcher]); @@ -52,7 +75,7 @@ function useApi(fetcher: ApiFetcher, overrideRpcEndpoint?: string) { refetch(); }, [refetch]); - return { result, refetch }; + return { result, error, refetch }; } export default useApi; diff --git a/apps/tangle-dapp/hooks/useApiRx.ts b/apps/tangle-dapp/hooks/useApiRx.ts index 8bb08e736..d1f1863b1 100644 --- a/apps/tangle-dapp/hooks/useApiRx.ts +++ b/apps/tangle-dapp/hooks/useApiRx.ts @@ -62,7 +62,27 @@ function useApiRx( return; } - const observable = factory(apiRx); + let observable; + + // In certain cases, the factory may fail with an error. For example, + // if a pallet isn't available on the active chain. Another example would + // be if the active chain is mainnet, but the factory is trying to fetch + // data from a testnet pallet that hasn't been deployed to mainnet yet. + try { + observable = factory(apiRx); + } catch (possibleError) { + const error = ensureError(possibleError); + + console.error( + 'Error creating subscription, this can happen when TypeScript type definitions are outdated or accessing pallets on the wrong chain:', + error, + ); + + setError(error); + setLoading(false); + + return; + } // The factory is not yet ready to produce an observable. // Discard any previous data diff --git a/apps/tangle-dapp/hooks/useNetworkFeatures.ts b/apps/tangle-dapp/hooks/useNetworkFeatures.ts index 007f2e79a..0bc89f862 100644 --- a/apps/tangle-dapp/hooks/useNetworkFeatures.ts +++ b/apps/tangle-dapp/hooks/useNetworkFeatures.ts @@ -1,5 +1,3 @@ -import { useMemo } from 'react'; - import { NETWORK_FEATURE_MAP } from '../constants/networks'; import useNetworkStore from '../context/useNetworkStore'; import { NetworkFeature } from '../types'; @@ -7,7 +5,7 @@ import { NetworkFeature } from '../types'; const useNetworkFeatures = (): Readonly => { const { network } = useNetworkStore(); - return useMemo(() => NETWORK_FEATURE_MAP[network.id], [network.id]); + return NETWORK_FEATURE_MAP[network.id]; }; export default useNetworkFeatures; diff --git a/apps/tangle-dapp/types/index.ts b/apps/tangle-dapp/types/index.ts index 6f185859b..f1b6bae07 100755 --- a/apps/tangle-dapp/types/index.ts +++ b/apps/tangle-dapp/types/index.ts @@ -221,6 +221,7 @@ export type ServiceParticipant = { export enum NetworkFeature { Faucet, EraStakersOverview, + LsPools, } export const ExplorerType = { diff --git a/apps/tangle-dapp/types/utils.ts b/apps/tangle-dapp/types/utils.ts index 5ad4b1b95..bf2b77911 100644 --- a/apps/tangle-dapp/types/utils.ts +++ b/apps/tangle-dapp/types/utils.ts @@ -122,9 +122,4 @@ export type Brand = Type & { __brand: Name }; export type RemoveBrand = { __brand: never }; -export type AnySubstrateAddress = Brand; - -export type SubstrateAddress = Brand< - string, - 'SubstrateAddress' & { ss58Format: SS58 } ->; +export type SubstrateAddress = Brand; diff --git a/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts b/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts deleted file mode 100644 index b9217cb21..000000000 --- a/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isAddress } from '@polkadot/util-crypto'; -import assert from 'assert'; - -import { AnySubstrateAddress } from '../types/utils'; - -type AnySubstrateAddressAssertionFn = ( - address: string, -) => asserts address is AnySubstrateAddress; - -const assertAnySubstrateAddress: AnySubstrateAddressAssertionFn = (address) => { - assert(isAddress(address), 'Address should be a valid Substrate address'); -}; - -export default assertAnySubstrateAddress; diff --git a/apps/tangle-dapp/utils/assertSubstrateAddress.ts b/apps/tangle-dapp/utils/assertSubstrateAddress.ts index 615daedf5..195bb1da2 100644 --- a/apps/tangle-dapp/utils/assertSubstrateAddress.ts +++ b/apps/tangle-dapp/utils/assertSubstrateAddress.ts @@ -3,16 +3,10 @@ import assert from 'assert'; import { SubstrateAddress } from '../types/utils'; -const assertSubstrateAddress = ( - address: string, - ss58Prefix: SS58, -): SubstrateAddress => { - assert( - isAddress(address, undefined, ss58Prefix), - 'Address should be a valid Substrate address', - ); +const assertSubstrateAddress = (address: string): SubstrateAddress => { + assert(isAddress(address), 'Address should be a valid Substrate address'); - return address as SubstrateAddress; + return address as SubstrateAddress; }; export default assertSubstrateAddress; diff --git a/apps/tangle-dapp/utils/isAnySubstrateAddress.ts b/apps/tangle-dapp/utils/isAnySubstrateAddress.ts index 05c5fa94f..b42f0d40c 100644 --- a/apps/tangle-dapp/utils/isAnySubstrateAddress.ts +++ b/apps/tangle-dapp/utils/isAnySubstrateAddress.ts @@ -1,10 +1,10 @@ import { isAddress } from '@polkadot/util-crypto'; -import { AnySubstrateAddress, RemoveBrand } from '../types/utils'; +import { SubstrateAddress, RemoveBrand } from '../types/utils'; const isAnySubstrateAddress = ( address: string, -): address is AnySubstrateAddress & RemoveBrand => { +): address is SubstrateAddress & RemoveBrand => { return isAddress(address); }; diff --git a/apps/tangle-dapp/utils/permillToPercentage.ts b/apps/tangle-dapp/utils/permillToPercentage.ts index 6f448b6fd..5700ace97 100644 --- a/apps/tangle-dapp/utils/permillToPercentage.ts +++ b/apps/tangle-dapp/utils/permillToPercentage.ts @@ -1,6 +1,6 @@ import { Permill } from '@polkadot/types/interfaces'; -const permillToPercentage = (permill: Permill) => { +const permillToPercentage = (permill: Permill): number => { return permill.toNumber() / 1_000_000; }; diff --git a/apps/tangle-dapp/utils/polkadot/identity.ts b/apps/tangle-dapp/utils/polkadot/identity.ts index 8e6ac85b7..735ba2ada 100644 --- a/apps/tangle-dapp/utils/polkadot/identity.ts +++ b/apps/tangle-dapp/utils/polkadot/identity.ts @@ -21,7 +21,10 @@ export const extractDataFromIdentityInfo = ( type: IdentityDataType, ): string | null => { const displayData = info[type]; - if (displayData.isNone) return null; + + if (displayData.isNone) { + return null; + } const displayDataObject: { raw?: string } = JSON.parse( displayData.toString(), diff --git a/apps/tangle-dapp/utils/scaleAmountByPercentage.ts b/apps/tangle-dapp/utils/scaleAmountByPercentage.ts new file mode 100644 index 000000000..0a18cc1f0 --- /dev/null +++ b/apps/tangle-dapp/utils/scaleAmountByPercentage.ts @@ -0,0 +1,14 @@ +import { BN } from '@polkadot/util'; + +const scaleAmountByPercentage = (amount: BN, percentage: number): BN => { + // Scale factor for 4 decimal places (0.xxxx). + const scale = new BN(10_000); + + // Scale the percentage to an integer. + const scaledPercentage = new BN(Math.round(percentage * scale.toNumber())); + + // Multiply the amount by the scaled percentage and then divide by the scale. + return amount.mul(scaledPercentage).div(scale); +}; + +export default scaleAmountByPercentage; diff --git a/apps/tangle-dapp/utils/scaleAmountByPermill.ts b/apps/tangle-dapp/utils/scaleAmountByPermill.ts deleted file mode 100644 index 8c215f041..000000000 --- a/apps/tangle-dapp/utils/scaleAmountByPermill.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BN } from '@polkadot/util'; - -const scaleAmountByPermill = (amount: BN, permill: number): BN => { - // Scale factor for 4 decimal places (0.xxxx). - const scale = new BN(10_000); - - // Scale the permill to an integer. - const scaledPermill = new BN(Math.round(permill * scale.toNumber())); - - // Multiply the amount by the scaled permill and then divide by the scale. - return amount.mul(scaledPermill).div(scale); -}; - -export default scaleAmountByPermill; diff --git a/libs/dapp-config/src/constants/tangle.ts b/libs/dapp-config/src/constants/tangle.ts index 9652c2193..c8ffd76c8 100644 --- a/libs/dapp-config/src/constants/tangle.ts +++ b/libs/dapp-config/src/constants/tangle.ts @@ -30,9 +30,7 @@ export const TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL = getPolkadotJsDashboardUrl( TANGLE_LOCAL_WS_RPC_ENDPOINT, ); -export const TANGLE_MAINNET_SS58_PREFIX = 5845; -export const TANGLE_TESTNET_SS58_PREFIX = 5845; -export const TANGLE_LOCAL_SS58_PREFIX = 42; +export const TANGLE_SS58_PREFIX = 5845; // Note that the chain decimal count is usually constant, and set when // the blockchain is deployed. It could be technically changed due to diff --git a/libs/webb-ui-components/src/constants/networks.ts b/libs/webb-ui-components/src/constants/networks.ts index f779489c7..b6f7db91c 100644 --- a/libs/webb-ui-components/src/constants/networks.ts +++ b/libs/webb-ui-components/src/constants/networks.ts @@ -15,9 +15,7 @@ import { TANGLE_LOCAL_WS_RPC_ENDPOINT, TANGLE_LOCAL_HTTP_RPC_ENDPOINT, TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL, - TANGLE_MAINNET_SS58_PREFIX, - TANGLE_TESTNET_SS58_PREFIX, - TANGLE_LOCAL_SS58_PREFIX, + TANGLE_SS58_PREFIX, } from '@webb-tools/dapp-config/constants/tangle'; import { SUBQUERY_ENDPOINT } from './index'; @@ -75,7 +73,7 @@ export const TANGLE_MAINNET_NETWORK = { polkadotJsDashboardUrl: TANGLE_MAINNET_POLKADOT_JS_DASHBOARD_URL, nativeExplorerUrl: TANGLE_MAINNET_NATIVE_EXPLORER_URL, evmExplorerUrl: TANGLE_MAINNET_EVM_EXPLORER_URL, - ss58Prefix: TANGLE_MAINNET_SS58_PREFIX, + ss58Prefix: TANGLE_SS58_PREFIX, } as const satisfies Network; export const TANGLE_TESTNET_NATIVE_NETWORK = { @@ -91,7 +89,7 @@ export const TANGLE_TESTNET_NATIVE_NETWORK = { polkadotJsDashboardUrl: TANGLE_TESTNET_POLKADOT_JS_DASHBOARD_URL, nativeExplorerUrl: TANGLE_TESTNET_NATIVE_EXPLORER_URL, evmExplorerUrl: TANGLE_TESTNET_EVM_EXPLORER_URL, - ss58Prefix: TANGLE_TESTNET_SS58_PREFIX, + ss58Prefix: TANGLE_SS58_PREFIX, } as const satisfies Network; /** @@ -108,7 +106,7 @@ export const TANGLE_LOCAL_DEV_NETWORK = { wsRpcEndpoint: TANGLE_LOCAL_WS_RPC_ENDPOINT, httpRpcEndpoint: TANGLE_LOCAL_HTTP_RPC_ENDPOINT, polkadotJsDashboardUrl: TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL, - ss58Prefix: TANGLE_LOCAL_SS58_PREFIX, + ss58Prefix: 42, } as const satisfies Network; export const TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK = { From 4e5f8d15713c87d4477458a05dffcdf690f186a3 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 8 Sep 2024 06:39:28 -0400 Subject: [PATCH 06/54] feat(tangle-dapp): Create LS pools data fetching hooks --- .../data/liquidStaking/useLsAssetDetails.ts | 39 +++++++++++++++ .../liquidStaking/useLsPoolBondedAccounts.ts | 46 ++++++++++++++++++ .../liquidStaking/useLsPoolNominations.ts | 41 ++++++++++++++++ .../data/liquidStaking/useLsPools.ts | 26 +++++----- .../data/liquidStaking/useNominators.ts | 48 +++++++++++++++++++ 5 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsPoolNominations.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/useNominators.ts diff --git a/apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts b/apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts new file mode 100644 index 000000000..9e27c131e --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts @@ -0,0 +1,39 @@ +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../hooks/useApiRx'; + +const useLsAssetDetails = () => { + const { result: tangleAssetDetails } = useApiRx( + useCallback((api) => { + return api.query.assets.asset.entries(); + }, []), + ); + + const keyValuePairs = useMemo(() => { + if (tangleAssetDetails === null) { + return null; + } + + return tangleAssetDetails.flatMap(([poolIdKey, valueOpt]) => { + // Ignore empty values. + if (valueOpt.isNone) { + return []; + } + + // TODO: The key's type is u128, yet when creating pools, it uses u32 for the pool id. Is this a Tangle bug, or is there a reason for this? For now, assuming that all keys are max u32. + return [[poolIdKey.args[0].toNumber(), valueOpt.unwrap()]] as const; + }); + }, [tangleAssetDetails]); + + const map = useMemo(() => { + if (keyValuePairs === null) { + return null; + } + + return new Map(keyValuePairs); + }, [keyValuePairs]); + + return map; +}; + +export default useLsAssetDetails; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts new file mode 100644 index 000000000..cd276997a --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts @@ -0,0 +1,46 @@ +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../hooks/useApiRx'; +import { SubstrateAddress } from '../../types/utils'; +import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; + +const useLsPoolBondedAccounts = (): Map | null => { + const { result: entries } = useApiRx( + useCallback((api) => { + return api.query.lst.reversePoolIdLookup.entries(); + }, []), + ); + + const keyValuePairs = useMemo(() => { + if (entries === null) { + return null; + } + + return entries.flatMap(([key, valueOpt]) => { + // Ignore empty values. + if (valueOpt.isNone) { + return []; + } + + const poolId = valueOpt.unwrap().toNumber(); + + const bondedAccountAddress = assertSubstrateAddress( + key.args[0].toString(), + ); + + return [[poolId, bondedAccountAddress]] as const; + }); + }, [entries]); + + const map = useMemo(() => { + if (keyValuePairs === null) { + return null; + } + + return new Map(keyValuePairs); + }, [keyValuePairs]); + + return map; +}; + +export default useLsPoolBondedAccounts; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolNominations.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolNominations.ts new file mode 100644 index 000000000..320b987b3 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolNominations.ts @@ -0,0 +1,41 @@ +import { useMemo } from 'react'; + +import { SubstrateAddress } from '../../types/utils'; +import useLsPoolBondedAccounts from './useLsPoolBondedAccounts'; +import useNominators from './useNominators'; + +const useLsPoolNominations = (): Map | null => { + const nominators = useNominators(); + const poolBondedAccounts = useLsPoolBondedAccounts(); + + const targets = useMemo(() => { + if (poolBondedAccounts === null || nominators === null) { + return null; + } + + return Array.from(poolBondedAccounts.entries()).flatMap( + ([poolId, owner]) => { + const nominations = nominators.get(owner); + + // Ignore pools with no nominations. + if (nominations === undefined) { + return []; + } + + return [[poolId, nominations ?? []]] as const; + }, + ); + }, [nominators, poolBondedAccounts]); + + const map = useMemo(() => { + if (targets === null) { + return null; + } + + return new Map(targets); + }, [targets]); + + return map; +}; + +export default useLsPoolNominations; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 4f23b33e4..d5f5de3b6 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -7,9 +7,11 @@ import useNetworkFeatures from '../../hooks/useNetworkFeatures'; import { NetworkFeature } from '../../types'; import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; import permillToPercentage from '../../utils/permillToPercentage'; +import useLsPoolNominations from './useLsPoolNominations'; const useLsPools = (): Map | null | Error => { const networkFeatures = useNetworkFeatures(); + const poolNominations = useLsPoolNominations(); if (!networkFeatures.includes(NetworkFeature.LsPools)) { // TODO: Handle case where the active network doesn't support liquid staking pools. @@ -32,7 +34,7 @@ const useLsPools = (): Map | null | Error => { return null; } - return rawBondedPools.flatMap(([key, valueOpt]) => { + return rawBondedPools.flatMap(([poolIdKey, valueOpt]) => { // Skip empty values. if (valueOpt.isNone) { return []; @@ -45,21 +47,21 @@ const useLsPools = (): Map | null | Error => { return []; } - return [[key.args[0].toNumber(), tanglePool] as const]; + return [[poolIdKey.args[0].toNumber(), tanglePool] as const]; }); }, [rawBondedPools]); const poolsMap = useMemo(() => { - if (tanglePools === null) { + if (tanglePools === null || poolNominations === null) { return null; } - const keyValuePairs = tanglePools.map(([id, tanglePool]) => { + const keyValuePairs = tanglePools.map(([poolId, tanglePool]) => { const metadataEntryBytes = rawMetadataEntries === null ? undefined : rawMetadataEntries.find( - ([idKey]) => idKey.args[0].toNumber() === id, + ([idKey]) => idKey.args[0].toNumber() === poolId, )?.[1]; const metadata = @@ -77,29 +79,23 @@ const useLsPools = (): Map | null | Error => { : permillToPercentage(tanglePool.commission.current.unwrap()[0]); const pool: LsPool = { - id, + id: poolId, metadata, owner, commissionPercentage, + validators: poolNominations.get(poolId) ?? [], // TODO: Dummy values. apyPercentage: 0.1, chainId: LsProtocolId.POLKADOT, totalStaked: new BN(1234560000000000), ownerStaked: new BN(12300003567), - validators: [ - '5FfP4SU5jXY9ZVfR1kY1pUXuJ3G1bfjJoQDRz4p7wSH3Mmdn' as any, - '5FnL9Pj3NX7E6yC1a2tN4kVdR7y2sAqG8vRsF4PN6yLeu2mL' as any, - '5CF8H7P3qHfZzBtPXH6G6e3Wc3V2wVn6tQHgYJ5HGKK1eC5z' as any, - '5GV8vP8Bh3fGZm2P7YNxMzUd9Wy4k3RSRvkq7RXVjxGGM1cy' as any, - '5DPy4XU6nNV2t2NQkz3QvPB2X5GJ5ZJ1wqMzC4Rxn2WLbXVD' as any, - ], }; - return [id, pool] as const; + return [poolId, pool] as const; }); return new Map(keyValuePairs); - }, [rawMetadataEntries, tanglePools]); + }, [poolNominations, rawMetadataEntries, tanglePools]); return poolsMap; }; diff --git a/apps/tangle-dapp/data/liquidStaking/useNominators.ts b/apps/tangle-dapp/data/liquidStaking/useNominators.ts new file mode 100644 index 000000000..61ef3981d --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useNominators.ts @@ -0,0 +1,48 @@ +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../hooks/useApiRx'; +import { SubstrateAddress } from '../../types/utils'; +import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; + +const useNominators = (): Map | null => { + const { result: entries } = useApiRx( + useCallback((api) => { + return api.query.staking.nominators.entries(); + }, []), + ); + + const keyValuePairs = useMemo(() => { + if (entries === null) { + return null; + } + + return entries.flatMap(([key, value]) => { + // Ignore empty values. + if (value.isNone) { + return []; + } + + const targets = value + .unwrap() + .targets.map((accountId) => + assertSubstrateAddress(accountId.toString()), + ); + + const nominatorAddress = assertSubstrateAddress(key.args[0].toString()); + + return [[nominatorAddress, targets]] as const; + }); + }, [entries]); + + const map = useMemo(() => { + if (entries === null) { + return null; + } + + return new Map(keyValuePairs); + }, [entries, keyValuePairs]); + + return map; +}; + +export default useNominators; From 51ec94e1e120eadd2a0638e1b6399d76fd09667c Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:08:52 -0400 Subject: [PATCH 07/54] fix(tangle-dapp): Fix errors --- .../components/ToggleableRadioInput.tsx | 32 -------- .../constants/liquidStaking/types.ts | 3 +- apps/tangle-dapp/containers/LsPoolsTable.tsx | 73 ++++--------------- .../data/liquidStaking/useLsPools.ts | 5 +- 4 files changed, 16 insertions(+), 97 deletions(-) delete mode 100644 apps/tangle-dapp/components/ToggleableRadioInput.tsx diff --git a/apps/tangle-dapp/components/ToggleableRadioInput.tsx b/apps/tangle-dapp/components/ToggleableRadioInput.tsx deleted file mode 100644 index 6063fd13d..000000000 --- a/apps/tangle-dapp/components/ToggleableRadioInput.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { FC } from 'react'; -import { twMerge } from 'tailwind-merge'; - -export type ToggleableRadioInputProps = { - isChecked: boolean; - onToggle: () => void; -}; - -const ToggleableRadioInput: FC = ({ - isChecked, - onToggle, -}) => { - return ( - - ); -}; - -export default ToggleableRadioInput; diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 7f0201374..82f078bb7 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -143,8 +143,7 @@ export type LsPool = { id: number; metadata?: string; owner: SubstrateAddress; - ownerStaked: BN; - chainId: LsParachainChainId; + ownerStake: BN; validators: SubstrateAddress[]; totalStaked: BN; apyPercentage: number; diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index a9673dae3..3cf7cc044 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -12,7 +12,7 @@ import { Updater, useReactTable, } from '@tanstack/react-table'; -import { ArrowRight, ChainIcon, Search } from '@webb-tools/icons'; +import { ArrowRight, Search } from '@webb-tools/icons'; import { Avatar, AvatarGroup, @@ -29,12 +29,11 @@ import { FC, useCallback, useMemo, useState } from 'react'; import { GlassCard } from '../components'; import { StringCell } from '../components/tableCells'; import TokenAmountCell from '../components/tableCells/TokenAmountCell'; -import ToggleableRadioInput from '../components/ToggleableRadioInput'; import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; import { LsPool } from '../constants/liquidStaking/types'; import useLsPools from '../data/liquidStaking/useLsPools'; import { useLsStore } from '../data/liquidStaking/useLsStore'; -import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; +import RadioInput from '../data/liquidStaking/useLsValidatorSelectionTableColumns'; import pluralize from '../utils/pluralize'; const COLUMN_HELPER = createColumnHelper(); @@ -51,11 +50,9 @@ const COLUMNS = [ return (
- - props.row.toggleSelected(!props.row.getIsSelected()) - } + props.row.toggleSelected(e.target.checked)} /> @@ -68,28 +65,6 @@ const COLUMNS = [ }, sortDescFirst: true, }), - COLUMN_HELPER.accessor('chainId', { - header: () => 'Chain', - cell: (props) => { - const chain = getLsProtocolDef(props.row.original.chainId); - - return ( -
- - - - {chain.name} - -
- ); - }, - sortingFn: (rowA, rowB) => { - const chainA = getLsProtocolDef(rowA.original.chainId); - const chainB = getLsProtocolDef(rowB.original.chainId); - - return chainA.name.localeCompare(chainB.name); - }, - }), COLUMN_HELPER.accessor('owner', { header: () => 'Owner', cell: (props) => ( @@ -118,34 +93,15 @@ const COLUMNS = [ ), }), - COLUMN_HELPER.accessor('ownerStaked', { + COLUMN_HELPER.accessor('ownerStake', { header: () => "Owner's Stake", - cell: (props) => { - const protocol = getLsProtocolDef(props.row.original.chainId); - - return ( - - ); - }, + cell: (props) => , }), COLUMN_HELPER.accessor('totalStaked', { header: () => 'Total Staked (TVL)', - cell: (props) => { - const protocol = getLsProtocolDef(props.row.original.chainId); - - return ( - - ); - }, + cell: (props) => ( + + ), }), COLUMN_HELPER.accessor('commissionPercentage', { header: () => 'Commission', @@ -203,13 +159,10 @@ const LsPoolsTable: FC = () => { (rowId) => newSelectionState[rowId], ); - assert(selectedRowIds.length <= 1, 'Only one row can ever be selected'); - - // TODO: Rows can only be selected, but once selected, one radio input/row must always remain selected. - setSelectedParachainPoolId( - selectedRowIds.length > 0 ? selectedRowIds[0] : null, - ); + const selectedRow = selectedRowIds.at(0); + assert(selectedRow !== undefined, 'One row must always be selected'); + setSelectedParachainPoolId(selectedRow); setRowSelectionState(newSelectionState); }, [rowSelectionState, setSelectedParachainPoolId], diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index d5f5de3b6..dad7c7ec3 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -1,7 +1,7 @@ import { BN, u8aToString } from '@polkadot/util'; import { useCallback, useMemo } from 'react'; -import { LsPool, LsProtocolId } from '../../constants/liquidStaking/types'; +import { LsPool } from '../../constants/liquidStaking/types'; import useApiRx from '../../hooks/useApiRx'; import useNetworkFeatures from '../../hooks/useNetworkFeatures'; import { NetworkFeature } from '../../types'; @@ -86,9 +86,8 @@ const useLsPools = (): Map | null | Error => { validators: poolNominations.get(poolId) ?? [], // TODO: Dummy values. apyPercentage: 0.1, - chainId: LsProtocolId.POLKADOT, totalStaked: new BN(1234560000000000), - ownerStaked: new BN(12300003567), + ownerStake: new BN(12300003567), }; return [poolId, pool] as const; From 2e8cda2dec7f27ba09bad16ad86b39f8c3d792bc Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:23:45 -0400 Subject: [PATCH 08/54] feat(tangle-dapp): Derive LS pool owner's stake & TVL --- .../data/liquidStaking/useLsPoolMembers.ts | 43 +++++++++++++++++++ .../data/liquidStaking/useLsPools.ts | 29 ++++++++++--- 2 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts new file mode 100644 index 000000000..ce92fc8c2 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts @@ -0,0 +1,43 @@ +import { Option } from '@polkadot/types'; +import { PalletAssetsAssetAccount } from '@polkadot/types/lookup'; +import { useCallback } from 'react'; +import { map } from 'rxjs'; + +import useApiRx from '../../hooks/useApiRx'; +import { SubstrateAddress } from '../../types/utils'; +import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; + +const useLsPoolMembers = (): + | Readonly<[number, SubstrateAddress, PalletAssetsAssetAccount]>[] + | null => { + const { result: accounts } = useApiRx( + useCallback((api) => { + return api.query.assets.account.entries().pipe( + map((entries) => { + return entries.flatMap(([key, val]) => { + // TODO: Manually casting the type here, since the type is being inferred as `Codec` instead of `Option`. This might be a problem with the TS type generation. + const valOpt = val as Option; + + // Ignore empty values. + if (valOpt.isNone) { + return []; + } + + const poolId = key.args[0].toNumber(); + + const accountAddress = assertSubstrateAddress( + key.args[1].toString(), + ); + + return [[poolId, accountAddress, valOpt.unwrap()] as const]; + }); + }), + ); + }, []), + ); + + // TODO: Return a map instead for improved lookup efficiency: PoolId -> [MemberAddress, Account]. + return accounts; +}; + +export default useLsPoolMembers; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index dad7c7ec3..87898ecd2 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -1,4 +1,4 @@ -import { BN, u8aToString } from '@polkadot/util'; +import { BN_ZERO, u8aToString } from '@polkadot/util'; import { useCallback, useMemo } from 'react'; import { LsPool } from '../../constants/liquidStaking/types'; @@ -7,6 +7,7 @@ import useNetworkFeatures from '../../hooks/useNetworkFeatures'; import { NetworkFeature } from '../../types'; import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; import permillToPercentage from '../../utils/permillToPercentage'; +import useLsPoolMembers from './useLsPoolMembers'; import useLsPoolNominations from './useLsPoolNominations'; const useLsPools = (): Map | null | Error => { @@ -29,6 +30,8 @@ const useLsPools = (): Map | null | Error => { }, []), ); + const poolMembers = useLsPoolMembers(); + const tanglePools = useMemo(() => { if (rawBondedPools === null) { return null; @@ -74,6 +77,22 @@ const useLsPools = (): Map | null | Error => { tanglePool.roles.root.unwrap().toString(), ); + const ownerStake = + poolMembers + ?.find(([id, memberAddress]) => { + return id === poolId && memberAddress === owner; + })?.[2] + .balance.toBn() ?? BN_ZERO; + + const memberBalances = poolMembers?.filter(([id]) => { + return id === poolId; + }); + + const totalStaked = + memberBalances?.reduce((acc, [, , account]) => { + return acc.add(account.balance.toBn()); + }, BN_ZERO) ?? BN_ZERO; + const commissionPercentage = tanglePool.commission.current.isNone ? undefined : permillToPercentage(tanglePool.commission.current.unwrap()[0]); @@ -84,17 +103,17 @@ const useLsPools = (): Map | null | Error => { owner, commissionPercentage, validators: poolNominations.get(poolId) ?? [], - // TODO: Dummy values. + totalStaked, + ownerStake, + // TODO: Dummy value. apyPercentage: 0.1, - totalStaked: new BN(1234560000000000), - ownerStake: new BN(12300003567), }; return [poolId, pool] as const; }); return new Map(keyValuePairs); - }, [poolNominations, rawMetadataEntries, tanglePools]); + }, [poolMembers, poolNominations, rawMetadataEntries, tanglePools]); return poolsMap; }; From 373db835d36879750a11b6647078cf9a18ab28fc Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:52:22 -0400 Subject: [PATCH 09/54] feat(tangle-dapp): Implement `useLsPoolCompoundApys` hook --- .../data/ValidatorTables/useValidators.ts | 4 +- .../liquidStaking/apy/useActiveEraIndex.ts | 23 +++ .../data/liquidStaking/apy/useEraRewards.ts | 67 ++++++++ .../apy/useLsPoolCompoundApys.ts | 144 ++++++++++++++++++ .../data/liquidStaking/useLsBondedPools.ts | 37 +++++ .../data/liquidStaking/useLsPools.ts | 40 +---- .../data/staking/useAllStakingExposures.ts | 67 ++++++++ .../data/staking/useEraTotalRewards2.ts | 36 +++++ ...es2.ts => useValidatorStakingExposures.ts} | 4 +- 9 files changed, 386 insertions(+), 36 deletions(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/apy/useActiveEraIndex.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/apy/useEraRewards.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts create mode 100644 apps/tangle-dapp/data/staking/useAllStakingExposures.ts create mode 100644 apps/tangle-dapp/data/staking/useEraTotalRewards2.ts rename apps/tangle-dapp/data/staking/{useStakingExposures2.ts => useValidatorStakingExposures.ts} (93%) diff --git a/apps/tangle-dapp/data/ValidatorTables/useValidators.ts b/apps/tangle-dapp/data/ValidatorTables/useValidators.ts index 9e7ae0459..03fa5c858 100755 --- a/apps/tangle-dapp/data/ValidatorTables/useValidators.ts +++ b/apps/tangle-dapp/data/ValidatorTables/useValidators.ts @@ -4,8 +4,8 @@ import { useCallback, useMemo } from 'react'; import useApiRx from '../../hooks/useApiRx'; import { Validator } from '../../types'; import createValidator from '../../utils/staking/createValidator'; -import useStakingExposures2 from '../staking/useStakingExposures2'; import useValidatorPrefs from '../staking/useValidatorPrefs'; +import useValidatorStakingExposures from '../staking/useValidatorStakingExposures'; import useValidatorIdentityNames from './useValidatorIdentityNames'; export const useValidators = ( @@ -20,7 +20,7 @@ export const useValidators = ( useValidatorIdentityNames(); const { result: validatorPrefs, isLoading: isLoadingValidatorPrefs } = useValidatorPrefs(); - const { result: exposures } = useStakingExposures2(isActive); + const { result: exposures } = useValidatorStakingExposures(isActive); const { result: nominations, isLoading: isLoadingNominations } = useApiRx( useCallback((api) => api.query.staking.nominators.entries(), []), diff --git a/apps/tangle-dapp/data/liquidStaking/apy/useActiveEraIndex.ts b/apps/tangle-dapp/data/liquidStaking/apy/useActiveEraIndex.ts new file mode 100644 index 000000000..73e91bea2 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/apy/useActiveEraIndex.ts @@ -0,0 +1,23 @@ +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../../hooks/useApiRx'; + +const useActiveEraIndex = (): number | null => { + const { result: activeEraOpt } = useApiRx( + useCallback((api) => { + return api.query.staking.activeEra(); + }, []), + ); + + const activeEraIndex = useMemo(() => { + if (activeEraOpt === null || activeEraOpt.isNone) { + return null; + } + + return activeEraOpt.unwrap().index; + }, [activeEraOpt]); + + return activeEraIndex?.toNumber() ?? null; +}; + +export default useActiveEraIndex; diff --git a/apps/tangle-dapp/data/liquidStaking/apy/useEraRewards.ts b/apps/tangle-dapp/data/liquidStaking/apy/useEraRewards.ts new file mode 100644 index 000000000..ddc78d299 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/apy/useEraRewards.ts @@ -0,0 +1,67 @@ +import { useMemo } from 'react'; + +import useApiRx from '../../../hooks/useApiRx'; +import { SubstrateAddress } from '../../../types/utils'; +import assertSubstrateAddress from '../../../utils/assertSubstrateAddress'; +import useActiveEraIndex from './useActiveEraIndex'; + +export type EraRewardPointsEntry = { + total: number; + individual: Map; +}; + +/** + * Fetch the total and individual reward points for all the + * eras up to max depth. + */ +const useEraRewardPoints = (): Map | null => { + const activeEraIndex = useActiveEraIndex(); + + const { result: activeEraRewardPoints } = useApiRx((api) => { + if (activeEraIndex === null) { + return null; + } + + return api.query.staking.erasRewardPoints.entries(); + }); + + const keyValuePairs = useMemo(() => { + if (activeEraRewardPoints === null) { + return null; + } + + return activeEraRewardPoints.map(([key, value]) => { + const individualKeyValuePairs = Array.from( + value.individual.entries(), + ).map(([key, value]) => { + return [ + assertSubstrateAddress(key.toString()), + value.toNumber(), + ] as const; + }); + + const individualPointsMap = new Map( + individualKeyValuePairs, + ); + + const entry: EraRewardPointsEntry = { + total: value.total.toNumber(), + individual: individualPointsMap, + }; + + return [key.args[0].toNumber(), entry] as const; + }); + }, [activeEraRewardPoints]); + + const map = useMemo(() => { + if (keyValuePairs === null) { + return null; + } + + return new Map(keyValuePairs); + }, [keyValuePairs]); + + return map; +}; + +export default useEraRewardPoints; diff --git a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts new file mode 100644 index 000000000..f971433dd --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts @@ -0,0 +1,144 @@ +import assert from 'assert'; +import Decimal from 'decimal.js'; +import { useMemo } from 'react'; + +import useApi from '../../../hooks/useApi'; +import useAllStakingExposures from '../../staking/useAllStakingExposures'; +import useEraTotalRewards2 from '../../staking/useEraTotalRewards2'; +import useLsBondedPools from '../useLsBondedPools'; +import useLsPoolBondedAccounts from '../useLsPoolBondedAccounts'; +import useActiveEraIndex from './useActiveEraIndex'; +import useEraRewardPoints from './useEraRewards'; + +/** + * Calculate the compound APY for all liquid staking pools, and + * return a map of `poolId -> APY`. + * + * The worst-case scenario performance of this hook is `O(p * h)`, where `p` + * is the number of pools and `h` is the history depth. However, since the + * history depth is a constant value (typically `80`), the performance is + * effectively `O(p)`. + */ +const useLsPoolCompoundApys = (): Map | null => { + const activeEraIndex = useActiveEraIndex(); + + const { result: rawHistoryDepth } = useApi( + (api) => api.consts.staking.historyDepth, + ); + + const historyDepth = rawHistoryDepth?.toNumber() ?? null; + const bondedPools = useLsBondedPools(); + const poolBondedAccounts = useLsPoolBondedAccounts(); + const eraRewardPoints = useEraRewardPoints(); + const { result: allExposures } = useAllStakingExposures(); + const { data: eraTotalRewards } = useEraTotalRewards2(); + + const apys = useMemo(() => { + if ( + bondedPools === null || + historyDepth === null || + eraRewardPoints === null || + activeEraIndex === null || + poolBondedAccounts === null || + allExposures === null || + eraTotalRewards === null + ) { + return null; + } + + const apys = new Map(); + + for (const [poolId] of bondedPools) { + let perEraReturnSum = new Decimal(0); + const poolBondedAccountAddress = poolBondedAccounts.get(poolId); + + // Instead of using the history depth when calculating the avg., + // use a counter, since some eras are skipped due to missing data. + let actualErasConsidered = 0; + + assert( + poolBondedAccountAddress !== undefined, + 'Each pool id should always have a corresponding bonded account entry', + ); + + // Calculate the avg. per-era return rate for the last MAX_ERAS eras + // for the current pool. + for (let i = activeEraIndex - historyDepth; i < activeEraIndex; i++) { + const poolExposureAtEra = allExposures.find( + (entry) => + entry.address === poolBondedAccountAddress && entry.eraIndex === i, + ); + + // TODO: Shouldn't all eras have an exposure entry? + // No exposure entry exists at this era for this pool. Skip. + if (poolExposureAtEra === undefined) { + continue; + } + + // TODO: Need to get the specific portion of points & stake for the pool X (its bounded account), not just the entire era's. + const rewardPointsAtEra = eraRewardPoints.get(i); + + // TODO: Shouldn't all eras have a rewards entry? + // No rewards data exists at this era. Skip. + if (rewardPointsAtEra === undefined) { + continue; + } + + const totalRewardsAtEra = eraTotalRewards.get(i); + + // TODO: Shouldn't all eras have a total rewards entry? Would the total rewards ever be zero? + // No rewards entry exists at this era. Skip. + // Also ignore if the total rewards at this era is zero, to avoid division by zero. + if (totalRewardsAtEra === undefined || totalRewardsAtEra.isZero()) { + continue; + } + + const poolPoints = + rewardPointsAtEra.individual.get(poolBondedAccountAddress) ?? 0; + + const poolRewardAmount = totalRewardsAtEra + .muln(poolPoints) + .divn(rewardPointsAtEra.total); + + const eraTotalStakeForPool = poolExposureAtEra.metadata.total.toBn(); + + // TODO: Shouldn't this also be considered for the avg.? Count it as zero? + // Avoid potential division by zero. + if (eraTotalStakeForPool.isZero()) { + continue; + } + + // Per-era return rate = + // Pool's reward amount at era / total tokens staked by that pool at era + const erpt = new Decimal(poolRewardAmount.toString()).div( + eraTotalStakeForPool.toString(), + ); + + perEraReturnSum = perEraReturnSum.plus(erpt); + actualErasConsidered += 1; + } + + const avgPerEraReturnRate = perEraReturnSum.div(actualErasConsidered); + + // APY = (avg(ERPT) + 1) ^ 365 - 1. + // The reason why 365 is used is because the era duration is 24 hours (1 day). + const apy = avgPerEraReturnRate.plus(1).pow(365).minus(1); + + apys.set(poolId, apy); + } + + return apys; + }, [ + bondedPools, + historyDepth, + eraRewardPoints, + activeEraIndex, + poolBondedAccounts, + allExposures, + eraTotalRewards, + ]); + + return apys; +}; + +export default useLsPoolCompoundApys; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts new file mode 100644 index 000000000..ca3ccd56e --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts @@ -0,0 +1,37 @@ +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../hooks/useApiRx'; + +const useLsBondedPools = () => { + const { result: rawBondedPools } = useApiRx( + useCallback((api) => { + return api.query.lst.bondedPools.entries(); + }, []), + ); + + const tanglePools = useMemo(() => { + if (rawBondedPools === null) { + return null; + } + + return rawBondedPools.flatMap(([poolIdKey, valueOpt]) => { + // Skip empty values. + if (valueOpt.isNone) { + return []; + } + + const tanglePool = valueOpt.unwrap(); + + // Ignore all non-open pools. + if (!tanglePool.state.isOpen) { + return []; + } + + return [[poolIdKey.args[0].toNumber(), tanglePool] as const]; + }); + }, [rawBondedPools]); + + return tanglePools; +}; + +export default useLsBondedPools; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 87898ecd2..088cafbfe 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -7,6 +7,7 @@ import useNetworkFeatures from '../../hooks/useNetworkFeatures'; import { NetworkFeature } from '../../types'; import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; import permillToPercentage from '../../utils/permillToPercentage'; +import useLsBondedPools from './useLsBondedPools'; import useLsPoolMembers from './useLsPoolMembers'; import useLsPoolNominations from './useLsPoolNominations'; @@ -24,42 +25,15 @@ const useLsPools = (): Map | null | Error => { }, []), ); - const { result: rawBondedPools } = useApiRx( - useCallback((api) => { - return api.query.lst.bondedPools.entries(); - }, []), - ); - + const bondedPools = useLsBondedPools(); const poolMembers = useLsPoolMembers(); - const tanglePools = useMemo(() => { - if (rawBondedPools === null) { - return null; - } - - return rawBondedPools.flatMap(([poolIdKey, valueOpt]) => { - // Skip empty values. - if (valueOpt.isNone) { - return []; - } - - const tanglePool = valueOpt.unwrap(); - - // Ignore all non-open pools. - if (!tanglePool.state.isOpen) { - return []; - } - - return [[poolIdKey.args[0].toNumber(), tanglePool] as const]; - }); - }, [rawBondedPools]); - const poolsMap = useMemo(() => { - if (tanglePools === null || poolNominations === null) { + if (bondedPools === null || poolNominations === null) { return null; } - const keyValuePairs = tanglePools.map(([poolId, tanglePool]) => { + const keyValuePairs = bondedPools.map(([poolId, tanglePool]) => { const metadataEntryBytes = rawMetadataEntries === null ? undefined @@ -97,12 +71,14 @@ const useLsPools = (): Map | null | Error => { ? undefined : permillToPercentage(tanglePool.commission.current.unwrap()[0]); + const validators = poolNominations.get(poolId) ?? []; + const pool: LsPool = { id: poolId, metadata, owner, commissionPercentage, - validators: poolNominations.get(poolId) ?? [], + validators, totalStaked, ownerStake, // TODO: Dummy value. @@ -113,7 +89,7 @@ const useLsPools = (): Map | null | Error => { }); return new Map(keyValuePairs); - }, [poolMembers, poolNominations, rawMetadataEntries, tanglePools]); + }, [poolMembers, poolNominations, rawMetadataEntries, bondedPools]); return poolsMap; }; diff --git a/apps/tangle-dapp/data/staking/useAllStakingExposures.ts b/apps/tangle-dapp/data/staking/useAllStakingExposures.ts new file mode 100644 index 000000000..116e14e64 --- /dev/null +++ b/apps/tangle-dapp/data/staking/useAllStakingExposures.ts @@ -0,0 +1,67 @@ +import { Option, StorageKey, u32 } from '@polkadot/types'; +import { AccountId32 } from '@polkadot/types/interfaces'; +import { SpStakingPagedExposureMetadata } from '@polkadot/types/lookup'; +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../hooks/useApiRx'; +import { SubstrateAddress } from '../../types/utils'; +import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; + +export type StakingExposureEntry = { + eraIndex: number; + address: SubstrateAddress; + metadata: SpStakingPagedExposureMetadata; +}; + +const useAllStakingExposures = (): { + isLoading: boolean; + error: Error | null; + result: StakingExposureEntry[] | null; +} => { + const { result: exposures, ...other } = useApiRx< + [StorageKey<[u32, AccountId32]>, Option][] + >( + useCallback((api) => { + return api.query.staking.erasStakersOverview.entries(); + }, []), + ); + + const entriesAsTuples = useMemo(() => { + if (exposures === null) { + return null; + } + + return exposures.flatMap(([key, value]) => { + // Ignore empty values. + if (value.isNone) { + return []; + } + + const eraIndex = key.args[0].toNumber(); + const address = assertSubstrateAddress(key.args[1].toString()); + + return [[eraIndex, address, value.unwrap()] as const]; + }); + }, [exposures]); + + const entries = useMemo(() => { + if (entriesAsTuples === null) { + return null; + } + + return entriesAsTuples.map(([eraIndex, address, metadata]) => { + return { + eraIndex, + address, + metadata, + } satisfies StakingExposureEntry; + }); + }, [entriesAsTuples]); + + return { + result: entries, + ...other, + }; +}; + +export default useAllStakingExposures; diff --git a/apps/tangle-dapp/data/staking/useEraTotalRewards2.ts b/apps/tangle-dapp/data/staking/useEraTotalRewards2.ts new file mode 100644 index 000000000..a8e6d8691 --- /dev/null +++ b/apps/tangle-dapp/data/staking/useEraTotalRewards2.ts @@ -0,0 +1,36 @@ +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../hooks/useApiRx'; + +const useEraTotalRewards2 = () => { + const { result: erasValidatorRewards, ...other } = useApiRx( + useCallback((api) => api.query.staking.erasValidatorReward.entries(), []), + ); + + const keyValuePairs = useMemo(() => { + if (erasValidatorRewards === null) { + return null; + } + + return erasValidatorRewards.flatMap(([key, rewardOpt]) => { + // Ignore empty values. + if (rewardOpt.isNone) { + return []; + } + + return [[key.args[0].toNumber(), rewardOpt.unwrap().toBn()] as const]; + }); + }, [erasValidatorRewards]); + + const map = useMemo(() => { + if (keyValuePairs === null) { + return null; + } + + return new Map(keyValuePairs); + }, [keyValuePairs]); + + return { data: map, ...other }; +}; + +export default useEraTotalRewards2; diff --git a/apps/tangle-dapp/data/staking/useStakingExposures2.ts b/apps/tangle-dapp/data/staking/useValidatorStakingExposures.ts similarity index 93% rename from apps/tangle-dapp/data/staking/useStakingExposures2.ts rename to apps/tangle-dapp/data/staking/useValidatorStakingExposures.ts index 1a9c36154..e30afed98 100644 --- a/apps/tangle-dapp/data/staking/useStakingExposures2.ts +++ b/apps/tangle-dapp/data/staking/useValidatorStakingExposures.ts @@ -17,7 +17,7 @@ export type ExposureMapEntry = { exposureMeta: SpStakingPagedExposureMetadata | null; }; -const useStakingExposures2 = (isActive: boolean) => { +const useValidatorStakingExposures = (isActive: boolean) => { const { result: queryResults, ...other } = useApiRx( useCallback( (api) => { @@ -55,4 +55,4 @@ const useStakingExposures2 = (isActive: boolean) => { return { result: exposureMap, ...other }; }; -export default useStakingExposures2; +export default useValidatorStakingExposures; From f40d0db4bd15f62eb2c8294abb820a138a53cf33 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:03:08 -0400 Subject: [PATCH 10/54] feat(tangle-dapp): Use `useLsPoolCompoundApys` in `useLsPools` --- .../constants/liquidStaking/types.ts | 2 +- apps/tangle-dapp/containers/LsPoolsTable.tsx | 20 +++++++++++++------ .../apy/useLsPoolCompoundApys.ts | 6 ++++++ .../data/liquidStaking/useLsPools.ts | 15 +++++++++++--- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 82f078bb7..a17a19047 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -146,6 +146,6 @@ export type LsPool = { ownerStake: BN; validators: SubstrateAddress[]; totalStaked: BN; - apyPercentage: number; + apyPercentage?: number; commissionPercentage?: number; }; diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index 3cf7cc044..a26e77e95 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -122,12 +122,20 @@ const COLUMNS = [ }), COLUMN_HELPER.accessor('apyPercentage', { header: () => 'APY', - cell: (props) => ( - - ), + cell: (props) => { + const apyPercentage = props.getValue(); + + if (apyPercentage === undefined) { + return EMPTY_VALUE_PLACEHOLDER; + } + + return ( + + ); + }, }), ]; diff --git a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts index f971433dd..0ee628ede 100644 --- a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts +++ b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts @@ -118,6 +118,12 @@ const useLsPoolCompoundApys = (): Map | null => { actualErasConsidered += 1; } + // Skip if no eras were considered, which would mean + // that the pool has no previous nominations. + if (actualErasConsidered === 0) { + continue; + } + const avgPerEraReturnRate = perEraReturnSum.div(actualErasConsidered); // APY = (avg(ERPT) + 1) ^ 365 - 1. diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 088cafbfe..19da23cc5 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -10,6 +10,7 @@ import permillToPercentage from '../../utils/permillToPercentage'; import useLsBondedPools from './useLsBondedPools'; import useLsPoolMembers from './useLsPoolMembers'; import useLsPoolNominations from './useLsPoolNominations'; +import useLsPoolCompoundApys from './apy/useLsPoolCompoundApys'; const useLsPools = (): Map | null | Error => { const networkFeatures = useNetworkFeatures(); @@ -27,9 +28,14 @@ const useLsPools = (): Map | null | Error => { const bondedPools = useLsBondedPools(); const poolMembers = useLsPoolMembers(); + const compoundApys = useLsPoolCompoundApys(); const poolsMap = useMemo(() => { - if (bondedPools === null || poolNominations === null) { + if ( + bondedPools === null || + poolNominations === null || + compoundApys === null + ) { return null; } @@ -72,6 +78,10 @@ const useLsPools = (): Map | null | Error => { : permillToPercentage(tanglePool.commission.current.unwrap()[0]); const validators = poolNominations.get(poolId) ?? []; + const apyEntry = compoundApys.get(poolId); + + const apyPercentage = + apyEntry === undefined ? undefined : Number(apyEntry.toFixed(2)); const pool: LsPool = { id: poolId, @@ -81,8 +91,7 @@ const useLsPools = (): Map | null | Error => { validators, totalStaked, ownerStake, - // TODO: Dummy value. - apyPercentage: 0.1, + apyPercentage, }; return [poolId, pool] as const; From d4482bce515983c71f8ea282f9777a805cf3c053 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:11:20 -0400 Subject: [PATCH 11/54] ci(tangle-dapp): Bump Tangle Substrate types package --- package.json | 2 +- yarn.lock | 45 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8015ea715..ba9db0be1 100644 --- a/package.json +++ b/package.json @@ -238,7 +238,7 @@ "@webb-tools/interfaces": "1.0.11", "@webb-tools/sdk-core": "0.1.4-127", "@webb-tools/tangle-restaking-types": "^0.1.0", - "@webb-tools/tangle-substrate-types": "0.5.9", + "@webb-tools/tangle-substrate-types": "0.5.12", "@webb-tools/test-utils": "0.1.8", "@webb-tools/tokens": "1.0.11", "@webb-tools/utils": "1.0.11", diff --git a/yarn.lock b/yarn.lock index a7ee56059..de7704989 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18469,7 +18469,7 @@ __metadata: languageName: node linkType: hard -"@webb-tools/protocol-substrate-types@npm:@webb-tools/tangle-substrate-types@0.5.9, @webb-tools/tangle-substrate-types@npm:0.5.9": +"@webb-tools/protocol-substrate-types@npm:@webb-tools/tangle-substrate-types@0.5.9": version: 0.5.9 resolution: "@webb-tools/tangle-substrate-types@npm:0.5.9" dependencies: @@ -18569,6 +18569,47 @@ __metadata: languageName: node linkType: hard +"@webb-tools/tangle-substrate-types@npm:0.5.12": + version: 0.5.12 + resolution: "@webb-tools/tangle-substrate-types@npm:0.5.12" + dependencies: + "@babel/cli": "npm:^7.23.9" + "@babel/core": "npm:^7.24.0" + "@babel/plugin-proposal-nullish-coalescing-operator": "npm:^7.18.6" + "@babel/plugin-proposal-numeric-separator": "npm:^7.18.6" + "@babel/plugin-proposal-optional-chaining": "npm:^7.20.7" + "@babel/plugin-syntax-bigint": "npm:^7.8.3" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + "@babel/plugin-syntax-import-assertions": "npm:^7.23.3" + "@babel/plugin-syntax-import-meta": "npm:^7.10.4" + "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" + "@babel/plugin-transform-regenerator": "npm:^7.23.3" + "@babel/plugin-transform-runtime": "npm:^7.24.0" + "@babel/preset-env": "npm:^7.24.0" + "@babel/preset-react": "npm:^7.23.3" + "@babel/preset-typescript": "npm:^7.23.3" + "@babel/register": "npm:^7.23.7" + "@babel/runtime": "npm:^7.24.0" + "@open-web3/orml-types": "npm:^2.0.1" + "@polkadot/api": "npm:^11.0.2" + "@polkadot/dev": "npm:^0.78.11" + "@polkadot/typegen": "npm:^11.0.2" + "@polkadot/types": "npm:^11.0.2" + babel-jest: "npm:^29.7.0" + babel-plugin-module-extension-resolver: "npm:^1.0.0" + babel-plugin-module-resolver: "npm:^5.0.0" + babel-plugin-styled-components: "npm:^2.1.4" + ecpair: "npm:^2.1.0" + elliptic: "npm:^6.5.5" + fs-extra: "npm:^11.2.0" + glob2base: "npm:^0.0.12" + minimatch: "npm:^9.0.3" + mkdirp: "npm:^3.0.1" + tiny-secp256k1: "npm:^2.2.3" + checksum: 10c0/03521ed872493700c3d6145da1c5895fb0107400e8f5160d874e29c42122acfc4c2b9b1f4a496cab86aa0ecbb483de124781dd4a8d82bd2e32ead9a8be533235 + languageName: node + linkType: hard + "@webb-tools/test-utils@npm:0.1.8": version: 0.1.8 resolution: "@webb-tools/test-utils@npm:0.1.8" @@ -46763,7 +46804,7 @@ __metadata: "@webb-tools/proposals": "npm:^1.0.11" "@webb-tools/sdk-core": "npm:0.1.4-127" "@webb-tools/tangle-restaking-types": "npm:^0.1.0" - "@webb-tools/tangle-substrate-types": "npm:0.5.9" + "@webb-tools/tangle-substrate-types": "npm:0.5.12" "@webb-tools/test-utils": "npm:0.1.8" "@webb-tools/tokens": "npm:1.0.11" "@webb-tools/utils": "npm:1.0.11" From eeaf084945118138ccfa57fdb524fae89b981fad Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:44:12 -0400 Subject: [PATCH 12/54] ci(tangle-dapp): Fix lint --- apps/tangle-dapp/data/liquidStaking/useLsPools.ts | 10 ++++++++-- apps/tangle-dapp/utils/isAnySubstrateAddress.ts | 11 ----------- 2 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 apps/tangle-dapp/utils/isAnySubstrateAddress.ts diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 19da23cc5..f734745f3 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -7,10 +7,10 @@ import useNetworkFeatures from '../../hooks/useNetworkFeatures'; import { NetworkFeature } from '../../types'; import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; import permillToPercentage from '../../utils/permillToPercentage'; +import useLsPoolCompoundApys from './apy/useLsPoolCompoundApys'; import useLsBondedPools from './useLsBondedPools'; import useLsPoolMembers from './useLsPoolMembers'; import useLsPoolNominations from './useLsPoolNominations'; -import useLsPoolCompoundApys from './apy/useLsPoolCompoundApys'; const useLsPools = (): Map | null | Error => { const networkFeatures = useNetworkFeatures(); @@ -98,7 +98,13 @@ const useLsPools = (): Map | null | Error => { }); return new Map(keyValuePairs); - }, [poolMembers, poolNominations, rawMetadataEntries, bondedPools]); + }, [ + poolMembers, + poolNominations, + rawMetadataEntries, + bondedPools, + compoundApys, + ]); return poolsMap; }; diff --git a/apps/tangle-dapp/utils/isAnySubstrateAddress.ts b/apps/tangle-dapp/utils/isAnySubstrateAddress.ts deleted file mode 100644 index b42f0d40c..000000000 --- a/apps/tangle-dapp/utils/isAnySubstrateAddress.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { isAddress } from '@polkadot/util-crypto'; - -import { SubstrateAddress, RemoveBrand } from '../types/utils'; - -const isAnySubstrateAddress = ( - address: string, -): address is SubstrateAddress & RemoveBrand => { - return isAddress(address); -}; - -export default isAnySubstrateAddress; From edef04a7ef6fcdc6c82d8658baa63ec0af6b347a Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 12 Sep 2024 21:07:48 -0400 Subject: [PATCH 13/54] feat(tangle-dapp): Handle unsupported networks --- apps/tangle-dapp/containers/LsPoolsTable.tsx | 67 +++++++++++-------- .../data/liquidStaking/useLsPools.ts | 23 ++++--- 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index a26e77e95..991b68081 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -14,6 +14,7 @@ import { } from '@tanstack/react-table'; import { ArrowRight, Search } from '@webb-tools/icons'; import { + Alert, Avatar, AvatarGroup, Button, @@ -40,14 +41,10 @@ const COLUMN_HELPER = createColumnHelper(); const COLUMNS = [ COLUMN_HELPER.accessor('metadata', { - header: () => 'Metadata/name', + header: () => 'Name', cell: (props) => { const metadata = props.getValue(); - if (metadata === undefined) { - return EMPTY_VALUE_PLACEHOLDER; - } - return (
props.row.toggleSelected(e.target.checked)} /> - - {props.getValue()} - + {metadata === undefined ? ( + EMPTY_VALUE_PLACEHOLDER + ) : ( + <> + + {props.getValue()} + - + + + )}
); }, @@ -176,7 +179,7 @@ const LsPoolsTable: FC = () => { [rowSelectionState, setSelectedParachainPoolId], ); - // TODO: Handle possible error and loading states. + // TODO: Handle possible loading state. const poolsMap = useLsPools(); const rows: LsPool[] = useMemo(() => { @@ -227,24 +230,34 @@ const LsPoolsTable: FC = () => { Select Pool
- setSearchQuery(newValue)} - isControlled - rightIcon={} - /> + {poolsMap instanceof Error ? ( + + ) : ( + <> + setSearchQuery(newValue)} + isControlled + rightIcon={} + /> -
1)} - isPaginated - totalRecords={rows.length} - thClassName="!bg-inherit border-t-0 bg-mono-0 !px-3 !py-2 whitespace-nowrap" - trClassName="!bg-inherit" - tdClassName="!bg-inherit !px-3 !py-2 whitespace-nowrap" - /> +
1)} + isPaginated + totalRecords={rows.length} + thClassName="!bg-inherit border-t-0 bg-mono-0 !px-3 !py-2 whitespace-nowrap" + trClassName="!bg-inherit" + tdClassName="!bg-inherit !px-3 !py-2 whitespace-nowrap" + /> + + )} ); diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index f734745f3..b5242b0da 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -15,10 +15,7 @@ import useLsPoolNominations from './useLsPoolNominations'; const useLsPools = (): Map | null | Error => { const networkFeatures = useNetworkFeatures(); const poolNominations = useLsPoolNominations(); - - if (!networkFeatures.includes(NetworkFeature.LsPools)) { - // TODO: Handle case where the active network doesn't support liquid staking pools. - } + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); const { result: rawMetadataEntries } = useApiRx( useCallback((api) => { @@ -34,7 +31,8 @@ const useLsPools = (): Map | null | Error => { if ( bondedPools === null || poolNominations === null || - compoundApys === null + compoundApys === null || + !isSupported ) { return null; } @@ -99,13 +97,22 @@ const useLsPools = (): Map | null | Error => { return new Map(keyValuePairs); }, [ - poolMembers, - poolNominations, - rawMetadataEntries, bondedPools, + poolNominations, compoundApys, + isSupported, + rawMetadataEntries, + poolMembers, ]); + // In case that the user connects to testnet or mainnet, but the network + // doesn't have the liquid staking pools feature. + if (!isSupported) { + return new Error( + 'Liquid staking pools are not yet supported on this network.', + ); + } + return poolsMap; }; From e3e3f7cf4588bc9592c69c02408b249b4f7560c0 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 12 Sep 2024 21:14:54 -0400 Subject: [PATCH 14/54] fix(tangle-dapp): Check for LS pool support --- .../data/liquidStaking/useLsBondedPools.ts | 19 +++++- .../liquidStaking/useLsPoolBondedAccounts.ts | 19 +++++- .../data/liquidStaking/useLsPoolMembers.ts | 59 +++++++++++-------- .../data/liquidStaking/useLsPools.ts | 13 +++- apps/tangle-dapp/hooks/useApiRx.ts | 3 +- 5 files changed, 80 insertions(+), 33 deletions(-) diff --git a/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts index ca3ccd56e..e700bfb5d 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts @@ -1,12 +1,24 @@ import { useCallback, useMemo } from 'react'; import useApiRx from '../../hooks/useApiRx'; +import useNetworkFeatures from '../../hooks/useNetworkFeatures'; +import { NetworkFeature } from '../../types'; const useLsBondedPools = () => { + const networkFeatures = useNetworkFeatures(); + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); + const { result: rawBondedPools } = useApiRx( - useCallback((api) => { - return api.query.lst.bondedPools.entries(); - }, []), + useCallback( + (api) => { + if (!isSupported) { + return null; + } + + return api.query.lst.bondedPools.entries(); + }, + [isSupported], + ), ); const tanglePools = useMemo(() => { @@ -31,6 +43,7 @@ const useLsBondedPools = () => { }); }, [rawBondedPools]); + // TODO: Add error state. For example, in case that the active network doesn't support liquid staking pools. return tanglePools; }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts index cd276997a..41d9e8fe6 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts @@ -1,14 +1,26 @@ import { useCallback, useMemo } from 'react'; import useApiRx from '../../hooks/useApiRx'; +import useNetworkFeatures from '../../hooks/useNetworkFeatures'; +import { NetworkFeature } from '../../types'; import { SubstrateAddress } from '../../types/utils'; import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; const useLsPoolBondedAccounts = (): Map | null => { + const networkFeatures = useNetworkFeatures(); + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); + const { result: entries } = useApiRx( - useCallback((api) => { - return api.query.lst.reversePoolIdLookup.entries(); - }, []), + useCallback( + (api) => { + if (!isSupported) { + return null; + } + + return api.query.lst.reversePoolIdLookup.entries(); + }, + [isSupported], + ), ); const keyValuePairs = useMemo(() => { @@ -40,6 +52,7 @@ const useLsPoolBondedAccounts = (): Map | null => { return new Map(keyValuePairs); }, [keyValuePairs]); + // TODO: Add error state. For example, in case that the active network doesn't support liquid staking pools. return map; }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts index ce92fc8c2..40c51e965 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts @@ -4,38 +4,51 @@ import { useCallback } from 'react'; import { map } from 'rxjs'; import useApiRx from '../../hooks/useApiRx'; +import useNetworkFeatures from '../../hooks/useNetworkFeatures'; +import { NetworkFeature } from '../../types'; import { SubstrateAddress } from '../../types/utils'; import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; const useLsPoolMembers = (): | Readonly<[number, SubstrateAddress, PalletAssetsAssetAccount]>[] | null => { + const networkFeatures = useNetworkFeatures(); + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); + const { result: accounts } = useApiRx( - useCallback((api) => { - return api.query.assets.account.entries().pipe( - map((entries) => { - return entries.flatMap(([key, val]) => { - // TODO: Manually casting the type here, since the type is being inferred as `Codec` instead of `Option`. This might be a problem with the TS type generation. - const valOpt = val as Option; - - // Ignore empty values. - if (valOpt.isNone) { - return []; - } - - const poolId = key.args[0].toNumber(); - - const accountAddress = assertSubstrateAddress( - key.args[1].toString(), - ); - - return [[poolId, accountAddress, valOpt.unwrap()] as const]; - }); - }), - ); - }, []), + useCallback( + (api) => { + if (!isSupported) { + return null; + } + + return api.query.assets.account.entries().pipe( + map((entries) => { + return entries.flatMap(([key, val]) => { + // TODO: Manually casting the type here, since the type is being inferred as `Codec` instead of `Option`. This might be a problem with the TS type generation. + const valOpt = val as Option; + + // Ignore empty values. + if (valOpt.isNone) { + return []; + } + + const poolId = key.args[0].toNumber(); + + const accountAddress = assertSubstrateAddress( + key.args[1].toString(), + ); + + return [[poolId, accountAddress, valOpt.unwrap()] as const]; + }); + }), + ); + }, + [isSupported], + ), ); + // TODO: Add error state. For example, in case that the active network doesn't support liquid staking pools. // TODO: Return a map instead for improved lookup efficiency: PoolId -> [MemberAddress, Account]. return accounts; }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index b5242b0da..ba6dbf999 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -18,9 +18,16 @@ const useLsPools = (): Map | null | Error => { const isSupported = networkFeatures.includes(NetworkFeature.LsPools); const { result: rawMetadataEntries } = useApiRx( - useCallback((api) => { - return api.query.lst.metadata.entries(); - }, []), + useCallback( + (api) => { + if (!isSupported) { + return null; + } + + return api.query.lst.metadata.entries(); + }, + [isSupported], + ), ); const bondedPools = useLsBondedPools(); diff --git a/apps/tangle-dapp/hooks/useApiRx.ts b/apps/tangle-dapp/hooks/useApiRx.ts index d1f1863b1..45e66171a 100644 --- a/apps/tangle-dapp/hooks/useApiRx.ts +++ b/apps/tangle-dapp/hooks/useApiRx.ts @@ -62,7 +62,8 @@ function useApiRx( return; } - let observable; + // TODO: Also allow for `| Error` return value, to allow for error handling in the consumer. + let observable: Observable | null; // In certain cases, the factory may fail with an error. For example, // if a pallet isn't available on the active chain. Another example would From 490d90f1b4ce79dce90c565aa389c94b975cefb6 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 13 Sep 2024 22:09:40 -0400 Subject: [PATCH 15/54] feat(tangle-dapp): Add empty & loading states --- .../LiquidStaking/TableRowsSkeleton.tsx | 4 ++-- apps/tangle-dapp/containers/LsPoolsTable.tsx | 24 +++++++++++++++---- .../data/liquidStaking/apy/useEraRewards.ts | 19 +++++++++------ .../apy/useLsPoolCompoundApys.ts | 4 ++-- .../data/liquidStaking/useLsPools.ts | 2 +- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/TableRowsSkeleton.tsx b/apps/tangle-dapp/components/LiquidStaking/TableRowsSkeleton.tsx index 0734e1ff4..18efb4ed5 100644 --- a/apps/tangle-dapp/components/LiquidStaking/TableRowsSkeleton.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/TableRowsSkeleton.tsx @@ -3,12 +3,12 @@ import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; export type SkeletonLoaderSetProps = { - rowCount: number; + rowCount?: number; className?: string; }; const TableRowsSkeleton: FC = ({ - rowCount, + rowCount = 10, className, }) => { return ( diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index 991b68081..b56e321dc 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -22,12 +22,14 @@ import { fuzzyFilter, Input, Table, + TANGLE_DOCS_URL, Typography, } from '@webb-tools/webb-ui-components'; import assert from 'assert'; import { FC, useCallback, useMemo, useState } from 'react'; -import { GlassCard } from '../components'; +import { GlassCard, TableStatus } from '../components'; +import TableRowsSkeleton from '../components/LiquidStaking/TableRowsSkeleton'; import { StringCell } from '../components/tableCells'; import TokenAmountCell from '../components/tableCells/TokenAmountCell'; import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; @@ -179,7 +181,6 @@ const LsPoolsTable: FC = () => { [rowSelectionState, setSelectedParachainPoolId], ); - // TODO: Handle possible loading state. const poolsMap = useLsPools(); const rows: LsPool[] = useMemo(() => { @@ -221,7 +222,7 @@ const LsPoolsTable: FC = () => { Create Pool - + { Select Pool - {poolsMap instanceof Error ? ( + {poolsMap === null ? ( + + ) : poolsMap instanceof Error ? ( + ) : rows.length === 0 ? ( + ) : ( <> | null => { const activeEraIndex = useActiveEraIndex(); - const { result: activeEraRewardPoints } = useApiRx((api) => { - if (activeEraIndex === null) { - return null; - } + const { result: activeEraRewardPoints } = useApiRx( + useCallback( + (api) => { + if (activeEraIndex === null) { + return null; + } - return api.query.staking.erasRewardPoints.entries(); - }); + return api.query.staking.erasRewardPoints.entries(); + }, + [activeEraIndex], + ), + ); const keyValuePairs = useMemo(() => { if (activeEraRewardPoints === null) { diff --git a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts index 0ee628ede..262a392ff 100644 --- a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts +++ b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts @@ -1,6 +1,6 @@ import assert from 'assert'; import Decimal from 'decimal.js'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import useApi from '../../../hooks/useApi'; import useAllStakingExposures from '../../staking/useAllStakingExposures'; @@ -23,7 +23,7 @@ const useLsPoolCompoundApys = (): Map | null => { const activeEraIndex = useActiveEraIndex(); const { result: rawHistoryDepth } = useApi( - (api) => api.consts.staking.historyDepth, + useCallback((api) => api.consts.staking.historyDepth, []), ); const historyDepth = rawHistoryDepth?.toNumber() ?? null; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index ba6dbf999..ada05f955 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -116,7 +116,7 @@ const useLsPools = (): Map | null | Error => { // doesn't have the liquid staking pools feature. if (!isSupported) { return new Error( - 'Liquid staking pools are not yet supported on this network.', + 'Liquid staking pools are not yet supported on this network. Please try connecting to a different network that supports liquid staking pools.', ); } From 46be2de309aa6b730d5f520d4ffc1c2b9c483934 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:34:29 -0400 Subject: [PATCH 16/54] refactor(tangle-dapp): Resolve some TODOs --- .../data/liquidStaking/apy/useLsPoolCompoundApys.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts index 262a392ff..5a965a876 100644 --- a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts +++ b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts @@ -61,7 +61,7 @@ const useLsPoolCompoundApys = (): Map | null => { 'Each pool id should always have a corresponding bonded account entry', ); - // Calculate the avg. per-era return rate for the last MAX_ERAS eras + // Calculate the avg. per-era return rate for the last max eras (history depth) // for the current pool. for (let i = activeEraIndex - historyDepth; i < activeEraIndex; i++) { const poolExposureAtEra = allExposures.find( @@ -69,16 +69,13 @@ const useLsPoolCompoundApys = (): Map | null => { entry.address === poolBondedAccountAddress && entry.eraIndex === i, ); - // TODO: Shouldn't all eras have an exposure entry? // No exposure entry exists at this era for this pool. Skip. if (poolExposureAtEra === undefined) { continue; } - // TODO: Need to get the specific portion of points & stake for the pool X (its bounded account), not just the entire era's. const rewardPointsAtEra = eraRewardPoints.get(i); - // TODO: Shouldn't all eras have a rewards entry? // No rewards data exists at this era. Skip. if (rewardPointsAtEra === undefined) { continue; @@ -86,7 +83,6 @@ const useLsPoolCompoundApys = (): Map | null => { const totalRewardsAtEra = eraTotalRewards.get(i); - // TODO: Shouldn't all eras have a total rewards entry? Would the total rewards ever be zero? // No rewards entry exists at this era. Skip. // Also ignore if the total rewards at this era is zero, to avoid division by zero. if (totalRewardsAtEra === undefined || totalRewardsAtEra.isZero()) { @@ -102,7 +98,6 @@ const useLsPoolCompoundApys = (): Map | null => { const eraTotalStakeForPool = poolExposureAtEra.metadata.total.toBn(); - // TODO: Shouldn't this also be considered for the avg.? Count it as zero? // Avoid potential division by zero. if (eraTotalStakeForPool.isZero()) { continue; @@ -126,7 +121,7 @@ const useLsPoolCompoundApys = (): Map | null => { const avgPerEraReturnRate = perEraReturnSum.div(actualErasConsidered); - // APY = (avg(ERPT) + 1) ^ 365 - 1. + // APY = (avg(per era return rate) + 1) ^ 365 - 1. // The reason why 365 is used is because the era duration is 24 hours (1 day). const apy = avgPerEraReturnRate.plus(1).pow(365).minus(1); From d564cb5b46641d01717007a4d8b14739ec2eff3c Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:03:45 -0400 Subject: [PATCH 17/54] fix(tangle-dapp): Handle edge case where root role is `None` --- .../constants/liquidStaking/types.ts | 4 +-- apps/tangle-dapp/containers/LsPoolsTable.tsx | 14 +++++++--- .../useDelegationsOccupiedStatus.ts | 13 ---------- .../data/liquidStaking/useLsBondedPools.ts | 2 +- .../liquidStaking/useLsPoolBondedAccounts.ts | 2 +- .../data/liquidStaking/useLsPoolMembers.ts | 2 +- .../data/liquidStaking/useLsPools.ts | 26 +++++++++++-------- 7 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 apps/tangle-dapp/data/liquidStaking/useDelegationsOccupiedStatus.ts diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index a17a19047..6b07afd6a 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -142,8 +142,8 @@ export type LsNetwork = { export type LsPool = { id: number; metadata?: string; - owner: SubstrateAddress; - ownerStake: BN; + ownerAddress?: SubstrateAddress; + ownerStake?: BN; validators: SubstrateAddress[]; totalStaked: BN; apyPercentage?: number; diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index b56e321dc..e4c3f4c63 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -70,12 +70,12 @@ const COLUMNS = [ }, sortDescFirst: true, }), - COLUMN_HELPER.accessor('owner', { + COLUMN_HELPER.accessor('ownerAddress', { header: () => 'Owner', cell: (props) => ( ), @@ -100,7 +100,15 @@ const COLUMNS = [ }), COLUMN_HELPER.accessor('ownerStake', { header: () => "Owner's Stake", - cell: (props) => , + cell: (props) => { + const ownerStake = props.getValue(); + + if (ownerStake === undefined) { + return EMPTY_VALUE_PLACEHOLDER; + } + + return ; + }, }), COLUMN_HELPER.accessor('totalStaked', { header: () => 'Total Staked (TVL)', diff --git a/apps/tangle-dapp/data/liquidStaking/useDelegationsOccupiedStatus.ts b/apps/tangle-dapp/data/liquidStaking/useDelegationsOccupiedStatus.ts deleted file mode 100644 index b2a498af9..000000000 --- a/apps/tangle-dapp/data/liquidStaking/useDelegationsOccupiedStatus.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; - -import { ParachainCurrency } from '../../constants/liquidStaking/types'; -import useApiRx from '../../hooks/useApiRx'; - -// TODO: Do a bit more research on what this signifies and means. Currently, it is only known that this is a requirement/check that may prevent further redeeming. -const useDelegationsOccupiedStatus = (currency: ParachainCurrency) => { - return useApiRx((api) => { - return api.query.slp.delegationsOccupied({ Native: currency }); - }, TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint); -}; - -export default useDelegationsOccupiedStatus; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts index e700bfb5d..98dba3e2e 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsBondedPools.ts @@ -43,7 +43,7 @@ const useLsBondedPools = () => { }); }, [rawBondedPools]); - // TODO: Add error state. For example, in case that the active network doesn't support liquid staking pools. + // TODO: Add explicit error state: `| Error`. For example, in case that the active network doesn't support liquid staking pools. return tanglePools; }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts index 41d9e8fe6..d1cbe947f 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolBondedAccounts.ts @@ -52,7 +52,7 @@ const useLsPoolBondedAccounts = (): Map | null => { return new Map(keyValuePairs); }, [keyValuePairs]); - // TODO: Add error state. For example, in case that the active network doesn't support liquid staking pools. + // TODO: Add explicit error state: `| Error`. For example, in case that the active network doesn't support liquid staking pools. return map; }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts index 40c51e965..946dd2496 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolMembers.ts @@ -48,7 +48,7 @@ const useLsPoolMembers = (): ), ); - // TODO: Add error state. For example, in case that the active network doesn't support liquid staking pools. + // TODO: Add explicit error state: `| Error`. For example, in case that the active network doesn't support liquid staking pools. // TODO: Return a map instead for improved lookup efficiency: PoolId -> [MemberAddress, Account]. return accounts; }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index ada05f955..6630f068d 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -1,4 +1,4 @@ -import { BN_ZERO, u8aToString } from '@polkadot/util'; +import { BN, BN_ZERO, u8aToString } from '@polkadot/util'; import { useCallback, useMemo } from 'react'; import { LsPool } from '../../constants/liquidStaking/types'; @@ -57,17 +57,21 @@ const useLsPools = (): Map | null | Error => { ? undefined : u8aToString(metadataEntryBytes); - // TODO: Under what circumstances would this be `None`? During pool creation, the various addresses seem required, not optional. - const owner = assertSubstrateAddress( - tanglePool.roles.root.unwrap().toString(), - ); + // Root role can be `None` if its roles are updated, and the root + // role is removed. + const ownerAddress = tanglePool.roles.root.isNone + ? undefined + : assertSubstrateAddress(tanglePool.roles.root.unwrap().toString()); + + let ownerStake: BN | undefined = undefined; - const ownerStake = - poolMembers - ?.find(([id, memberAddress]) => { - return id === poolId && memberAddress === owner; + if (ownerAddress !== undefined && poolMembers !== null) { + ownerStake = poolMembers + .find(([id, memberAddress]) => { + return id === poolId && memberAddress === ownerAddress; })?.[2] - .balance.toBn() ?? BN_ZERO; + .balance.toBn(); + } const memberBalances = poolMembers?.filter(([id]) => { return id === poolId; @@ -91,7 +95,7 @@ const useLsPools = (): Map | null | Error => { const pool: LsPool = { id: poolId, metadata, - owner, + ownerAddress, commissionPercentage, validators, totalStaked, From 31834b1fb8762d7802014fb9b32f9db38ff1c24c Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:06:58 -0400 Subject: [PATCH 18/54] docs(tangle-dapp): Resolve more TODOs --- .../components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx | 2 -- apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx index 9a5dc62a8..1385ae19a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx @@ -23,8 +23,6 @@ const FeeDetailItem: FC = ({ const protocol = getLsProtocolDef(protocolId); - // TODO: Add liquifier fees, and select either parachain or liquifier fees based on the given protocol's id. - const feeAmount = useMemo(() => { // Propagate error or loading state. if (typeof feePercentage !== 'number') { diff --git a/apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts b/apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts index 9e27c131e..eddacb8ea 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsAssetDetails.ts @@ -20,7 +20,8 @@ const useLsAssetDetails = () => { return []; } - // TODO: The key's type is u128, yet when creating pools, it uses u32 for the pool id. Is this a Tangle bug, or is there a reason for this? For now, assuming that all keys are max u32. + // Ids larger than `u32::MAX` aren't expected, so it is safe to + // convert to a number here. return [[poolIdKey.args[0].toNumber(), valueOpt.unwrap()]] as const; }); }, [tangleAssetDetails]); From 7a495fd71d5ccbce2ebe4c38171f31238ae7615d Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:30:51 -0400 Subject: [PATCH 19/54] feat(tangle-dapp): Add initial Tangle mainnet network --- .../constants/liquidStaking/constants.ts | 9 + .../constants/liquidStaking/types.ts | 18 +- .../data/liquidStaking/adapters/tangle.tsx | 222 ++++++++++++++++++ .../utils/liquidStaking/getLsNetwork.ts | 3 + 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/adapters/tangle.tsx diff --git a/apps/tangle-dapp/constants/liquidStaking/constants.ts b/apps/tangle-dapp/constants/liquidStaking/constants.ts index 6da01ae0c..a46fc07b2 100644 --- a/apps/tangle-dapp/constants/liquidStaking/constants.ts +++ b/apps/tangle-dapp/constants/liquidStaking/constants.ts @@ -6,6 +6,7 @@ import MOONBEAM from '../../data/liquidStaking/adapters/moonbeam'; import PHALA from '../../data/liquidStaking/adapters/phala'; import POLKADOT from '../../data/liquidStaking/adapters/polkadot'; import POLYGON from '../../data/liquidStaking/adapters/polygon'; +import TANGLE from '../../data/liquidStaking/adapters/tangle'; import THE_GRAPH from '../../data/liquidStaking/adapters/theGraph'; import { IS_PRODUCTION_ENV } from '../env'; import { @@ -96,6 +97,14 @@ export const LS_TANGLE_RESTAKING_PARACHAIN: LsNetwork = { protocols: [POLKADOT, PHALA, MOONBEAM, ASTAR, MANTA] as LsProtocolDef[], }; +export const LS_TANGLE_MAINNET: LsNetwork = { + type: LsNetworkId.TANGLE_MAINNET, + networkName: 'Tangle Mainnet', + chainIconFileName: 'tangle', + defaultProtocolId: LsProtocolId.TANGLE, + protocols: [TANGLE], +}; + export const LS_NETWORKS: LsNetwork[] = [ LS_ETHEREUM_MAINNET_LIQUIFIER, LS_TANGLE_RESTAKING_PARACHAIN, diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 6b07afd6a..17273c1b9 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -9,8 +9,10 @@ import { LsNetworkEntityAdapter, ProtocolEntity, } from '../../data/liquidStaking/adapter'; +import { PolkadotValidator } from '../../data/liquidStaking/adapters/polkadot'; import { SubstrateAddress } from '../../types/utils'; import { CrossChainTimeUnit } from '../../utils/CrossChainTime'; +import { TANGLE_MAINNET_NETWORK } from '../../../../libs/webb-ui-components/src/constants/networks'; export enum LsProtocolId { POLKADOT, @@ -22,6 +24,7 @@ export enum LsProtocolId { THE_GRAPH, LIVEPEER, POLYGON, + TANGLE, } export type LsLiquifierProtocolId = @@ -62,10 +65,20 @@ type ProtocolDefCommon = { }; export enum LsNetworkId { + TANGLE_MAINNET, TANGLE_RESTAKING_PARACHAIN, ETHEREUM_MAINNET_LIQUIFIER, } +export interface LsTangleMainnetDef extends ProtocolDefCommon { + networkId: LsNetworkId.TANGLE_MAINNET; + id: LsProtocolId.TANGLE; + token: LsToken.TNT; + rpcEndpoint: string; + ss58Prefix: typeof TANGLE_MAINNET_NETWORK.ss58Prefix; + adapter: LsNetworkEntityAdapter; +} + export interface LsParachainChainDef extends ProtocolDefCommon { networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN; @@ -88,7 +101,10 @@ export interface LsLiquifierProtocolDef extends ProtocolDefCommon { unlocksContractAddress: HexString; } -export type LsProtocolDef = LsParachainChainDef | LsLiquifierProtocolDef; +export type LsProtocolDef = + | LsParachainChainDef + | LsLiquifierProtocolDef + | LsTangleMainnetDef; export type LsCardSearchParams = { amount: BN; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangle.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangle.tsx new file mode 100644 index 000000000..d375faf58 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangle.tsx @@ -0,0 +1,222 @@ +import { BN_ZERO } from '@polkadot/util'; +import { createColumnHelper, SortingFnOption } from '@tanstack/react-table'; +import { + Avatar, + CheckBox, + CopyWithTooltip, + shortenString, + Typography, +} from '@webb-tools/webb-ui-components'; +import { + TANGLE_LOCAL_DEV_NETWORK, + TANGLE_MAINNET_NETWORK, +} from '@webb-tools/webb-ui-components/constants/networks'; + +import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton'; +import { IS_PRODUCTION_ENV } from '../../../constants/env'; +import { + LsNetworkId, + LsProtocolId, + LsTangleMainnetDef, + LsToken, +} from '../../../constants/liquidStaking/types'; +import { LiquidStakingItem } from '../../../types/liquidStaking'; +import assertSubstrateAddress from '../../../utils/assertSubstrateAddress'; +import calculateCommission from '../../../utils/calculateCommission'; +import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; +import formatBn from '../../../utils/formatBn'; +import { GetTableColumnsFn } from '../adapter'; +import { + sortCommission, + sortSelected, + sortValueStaked, +} from '../columnSorting'; +import { + fetchChainDecimals, + fetchMappedIdentityNames, + fetchMappedValidatorsCommission, + fetchMappedValidatorsTotalValueStaked, + fetchTokenSymbol, +} from '../fetchHelpers'; +import { PolkadotValidator } from './polkadot'; + +const DECIMALS = 18; + +const fetchValidators = async ( + rpcEndpoint: string, +): Promise => { + const [ + validators, + mappedIdentityNames, + mappedTotalValueStaked, + mappedCommission, + ] = await Promise.all([ + fetchValidators(rpcEndpoint), + fetchMappedIdentityNames(rpcEndpoint), + fetchMappedValidatorsTotalValueStaked(rpcEndpoint), + fetchMappedValidatorsCommission(rpcEndpoint), + fetchChainDecimals(rpcEndpoint), + fetchTokenSymbol(rpcEndpoint), + ]); + + return validators.map((address) => { + const identityName = mappedIdentityNames.get(address.toString()); + const totalValueStaked = mappedTotalValueStaked.get(address.toString()); + const commission = mappedCommission.get(address.toString()); + + return { + id: address.toString(), + address: assertSubstrateAddress(address.toString()), + identity: identityName ?? address.toString(), + totalValueStaked: totalValueStaked ?? BN_ZERO, + apy: 0, + commission: commission ?? BN_ZERO, + itemType: LiquidStakingItem.VALIDATOR, + }; + }); +}; + +const getTableColumns: GetTableColumnsFn = ( + toggleSortSelectionHandlerRef, +) => { + const validatorColumnHelper = createColumnHelper(); + + return [ + validatorColumnHelper.accessor('address', { + header: ({ header }) => { + toggleSortSelectionHandlerRef.current = header.column.toggleSorting; + return ( + + Validator + + ); + }, + cell: (props) => { + const address = props.getValue(); + const identity = props.row.original.identity ?? address; + + return ( +
+ + +
+ + + + {identity === address ? shortenString(address, 8) : identity} + + + +
+
+ ); + }, + // TODO: Avoid casting sorting function. + sortingFn: sortSelected as SortingFnOption, + }), + validatorColumnHelper.accessor('totalValueStaked', { + header: ({ header }) => ( +
+ + Total Staked + +
+ ), + cell: (props) => ( +
+ + {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.DOT}`} + +
+ ), + sortingFn: sortValueStaked, + }), + validatorColumnHelper.accessor('commission', { + header: ({ header }) => ( +
+ + Commission + +
+ ), + cell: (props) => ( +
+ + {calculateCommission(props.getValue()).toFixed(2) + '%'} + +
+ ), + // TODO: Avoid casting sorting function. + sortingFn: sortCommission as SortingFnOption, + }), + validatorColumnHelper.display({ + id: 'href', + header: () => , + cell: (props) => { + const href = `https://polkadot.subscan.io/account/${props.getValue()}`; + + return ; + }, + }), + ]; +}; + +const TANGLE = { + networkId: LsNetworkId.TANGLE_MAINNET, + id: LsProtocolId.TANGLE, + name: 'Tangle', + token: LsToken.TNT, + chainIconFileName: 'tangle', + decimals: DECIMALS, + rpcEndpoint: IS_PRODUCTION_ENV + ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint + : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, + timeUnit: CrossChainTimeUnit.POLKADOT_ERA, + unstakingPeriod: 28, + ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, + adapter: { + fetchProtocolEntities: fetchValidators, + getTableColumns, + }, +} as const satisfies LsTangleMainnetDef; + +export default TANGLE; diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts index 811ebc96a..9f9ec7bb0 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts @@ -1,5 +1,6 @@ import { LS_ETHEREUM_MAINNET_LIQUIFIER, + LS_TANGLE_MAINNET, LS_TANGLE_RESTAKING_PARACHAIN, } from '../../constants/liquidStaking/constants'; import { LsNetwork, LsNetworkId } from '../../constants/liquidStaking/types'; @@ -10,6 +11,8 @@ const getLsNetwork = (networkId: LsNetworkId): LsNetwork => { return LS_ETHEREUM_MAINNET_LIQUIFIER; case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: return LS_TANGLE_RESTAKING_PARACHAIN; + case LsNetworkId.TANGLE_MAINNET: + return LS_TANGLE_MAINNET; } }; From 5fef25ff09690243dbb884e8ea50dd16b100546f Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 22 Sep 2024 23:19:01 -0400 Subject: [PATCH 20/54] feat(tangle-dapp): Add Tangle testnet to LS networks --- .../stakeAndUnstake/LsAgnosticBalance.tsx | 1 - .../stakeAndUnstake/LsStakeCard.tsx | 21 +- .../stakeAndUnstake/LsUnstakeCard.tsx | 17 +- .../stakeAndUnstake/NetworkSelector.tsx | 29 ++- .../stakeAndUnstake/ProtocolSelector.tsx | 29 ++- .../stakeAndUnstake/useLsAgnosticBalance.ts | 17 ++ .../stakeAndUnstake/useLsChangeNetwork.ts | 77 ++++++ .../constants/liquidStaking/constants.ts | 28 ++- .../constants/liquidStaking/types.ts | 36 ++- apps/tangle-dapp/containers/LsPoolsTable.tsx | 4 +- .../{tangle.tsx => tangleMainnet.tsx} | 11 +- .../liquidStaking/adapters/tangleTestnet.tsx | 224 ++++++++++++++++++ .../liquidStaking/useLsProtocolEntities.ts | 2 + .../data/liquidStaking/useLsStore.ts | 12 +- apps/tangle-dapp/hooks/useSearchParamSync.ts | 15 +- apps/tangle-dapp/utils/formatBn.ts | 2 +- .../utils/liquidStaking/getLsNetwork.ts | 3 + .../utils/liquidStaking/getLsProtocolDef.ts | 5 +- 18 files changed, 461 insertions(+), 72 deletions(-) create mode 100644 apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts rename apps/tangle-dapp/data/liquidStaking/adapters/{tangle.tsx => tangleMainnet.tsx} (97%) create mode 100644 apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index 8e185181c..f80d19203 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -58,7 +58,6 @@ const LsAgnosticBalance: FC = ({ } const formattedBalance = formatBn(balance, decimals, { - fractionMaxLength: undefined, includeCommas: true, }); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 21e7b9cdf..47a9c5a35 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -13,7 +13,7 @@ import { Input, Typography, } from '@webb-tools/webb-ui-components'; -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { z } from 'zod'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; @@ -40,6 +40,7 @@ import LsFeeWarning from './LsFeeWarning'; import LsInput from './LsInput'; import TotalDetailItem from './TotalDetailItem'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; +import useLsChangeNetwork from './useLsChangeNetwork'; import useLsSpendingLimits from './useLsSpendingLimits'; const LsStakeCard: FC = () => { @@ -50,12 +51,8 @@ const LsStakeCard: FC = () => { stringify: (value) => value?.toString(), }); - const { - selectedProtocolId, - setSelectedProtocolId, - selectedNetworkId, - setSelectedNetworkId, - } = useLsStore(); + const { selectedProtocolId, setSelectedProtocolId, selectedNetworkId } = + useLsStore(); const { execute: executeMintTx, status: mintTxStatus } = useMintTx(); const performLiquifierDeposit = useLiquifierDeposit(); @@ -67,6 +64,7 @@ const LsStakeCard: FC = () => { ); const selectedProtocol = getLsProtocolDef(selectedProtocolId); + const tryChangeNetwork = useLsChangeNetwork(); // TODO: Not loading the correct protocol for: '?amount=123000000000000000000&protocol=7&network=1&action=stake'. When network=1, it switches to protocol=5 on load. Could this be because the protocol is reset to its default once the network is switched? useSearchParamSync({ @@ -82,7 +80,7 @@ const LsStakeCard: FC = () => { value: selectedNetworkId, parse: (value) => z.nativeEnum(LsNetworkId).parse(parseInt(value)), stringify: (value) => value.toString(), - setValue: setSelectedNetworkId, + setValue: tryChangeNetwork, }); const { @@ -146,6 +144,11 @@ const LsStakeCard: FC = () => { /> ); + // Reset the input amount when the network changes. + useEffect(() => { + setFromAmount(null); + }, [setFromAmount, selectedNetworkId]); + return ( <> { setProtocolId={setSelectedProtocolId} minAmount={minSpendable ?? undefined} maxAmount={maxSpendable ?? undefined} - setNetworkId={setSelectedNetworkId} + setNetworkId={tryChangeNetwork} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index c6670437f..f1616327e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -35,19 +35,17 @@ import SelectTokenModal from './SelectTokenModal'; import TotalDetailItem from './TotalDetailItem'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import UnstakeRequestSubmittedModal from './UnstakeRequestSubmittedModal'; +import useLsChangeNetwork from './useLsChangeNetwork'; import useLsSpendingLimits from './useLsSpendingLimits'; const LsUnstakeCard: FC = () => { const [isSelectTokenModalOpen, setIsSelectTokenModalOpen] = useState(false); const [fromAmount, setFromAmount] = useState(null); const activeAccountAddress = useActiveAccountAddress(); + const tryChangeNetwork = useLsChangeNetwork(); - const { - selectedProtocolId, - setSelectedProtocolId, - selectedNetworkId, - setSelectedNetworkId, - } = useLsStore(); + const { selectedProtocolId, setSelectedProtocolId, selectedNetworkId } = + useLsStore(); const [didLiquifierUnlockSucceed, setDidLiquifierUnlockSucceed] = useState(false); @@ -153,6 +151,11 @@ const LsUnstakeCard: FC = () => { } }, [didLiquifierUnlockSucceed, redeemTxStatus]); + // Reset the input amount when the network changes. + useEffect(() => { + setFromAmount(null); + }, [setFromAmount, selectedNetworkId]); + const stakedWalletBalance = ( { = ({ ); + // Filter out networks that don't support liquid staking yet. + const supportedLsNetworks = LS_NETWORKS.filter((network) => { + if (network.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { + return true; + } + + // TODO: Obtain the Tangle network from the LS Network's properties instead. + const tangleNetwork = + network.id === LsNetworkId.TANGLE_MAINNET + ? TANGLE_MAINNET_NETWORK + : TANGLE_TESTNET_NATIVE_NETWORK; + + const networkFeatures = NETWORK_FEATURE_MAP[tangleNetwork.id]; + + return networkFeatures.includes(NetworkFeature.LsPools); + }); + return setNetworkId !== undefined ? ( {base} @@ -52,10 +75,10 @@ const NetworkSelector: FC = ({
    - {LS_NETWORKS.map((network) => { + {supportedLsNetworks.map((network) => { return ( -
  • - setNetworkId(network.type)}> +
  • + setNetworkId(network.id)}>
    diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx index aa27a55b5..d249c58a0 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ProtocolSelector.tsx @@ -7,7 +7,7 @@ import { Typography, } from '@webb-tools/webb-ui-components'; import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea'; -import { FC } from 'react'; +import { FC, useCallback } from 'react'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; import { @@ -35,19 +35,24 @@ const ProtocolSelector: FC = ({ const protocol = getLsProtocolDef(selectedProtocolId); const network = getLsNetwork(selectedNetworkId); - const trySetProtocolId = (newProtocolId: LsProtocolId) => { - return () => { - if (setProtocolId === undefined) { - return; - } + const trySetProtocolId = useCallback( + (newProtocolId: LsProtocolId) => { + return () => { + if (setProtocolId === undefined) { + return; + } - setProtocolId(newProtocolId); - }; - }; + setProtocolId(newProtocolId); + }; + }, + [setProtocolId], + ); return ( - +
    @@ -56,7 +61,9 @@ const ProtocolSelector: FC = ({ {protocol.token} - {setProtocolId !== undefined && } + {setProtocolId !== undefined && network.protocols.length > 1 && ( + + )}
    diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts index 42f70963b..fe139a4ed 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts @@ -8,6 +8,7 @@ import { LsNetworkId, LsProtocolId, } from '../../../constants/liquidStaking/types'; +import useBalances from '../../../data/balances/useBalances'; import useParachainBalances from '../../../data/liquidStaking/useParachainBalances'; import usePolling from '../../../data/liquidStaking/usePolling'; import useContractReadOnce from '../../../data/liquifier/useContractReadOnce'; @@ -50,6 +51,7 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { const activeAccountAddress = useActiveAccountAddress(); const evmAddress20 = useEvmAddress20(); const { nativeBalances, liquidBalances } = useParachainBalances(); + const { free: tangleBalance } = useBalances(); // TODO: Why not use the subscription hook variants (useContractRead) instead of manually utilizing usePolling? const readErc20 = useContractReadOnce(erc20Abi); @@ -115,6 +117,8 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { effect: isAccountConnected ? erc20BalanceFetcher : null, }); + // Update balance to the parachain balance when the restaking + // parachain is the active network. useEffect(() => { if ( protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN || @@ -128,6 +132,19 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { setBalance(createBalanceStateUpdater(newBalance)); }, [parachainBalances, protocol.token, protocol.networkId]); + // Update the balance to the Tangle balance when the Tangle + // network is the active network. + useEffect(() => { + if ( + protocol.networkId !== LsNetworkId.TANGLE_MAINNET || + tangleBalance === null + ) { + return; + } + + setBalance(createBalanceStateUpdater(tangleBalance)); + }, [protocol.networkId, tangleBalance]); + return { balance, isRefreshing }; }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts new file mode 100644 index 000000000..0869a4036 --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts @@ -0,0 +1,77 @@ +import { useWebbUI } from '@webb-tools/webb-ui-components'; +import { + TANGLE_MAINNET_NETWORK, + TANGLE_TESTNET_NATIVE_NETWORK, +} from '@webb-tools/webb-ui-components/constants/networks'; +import { useCallback } from 'react'; + +import { LsNetworkId } from '../../../constants/liquidStaking/types'; +import { NETWORK_FEATURE_MAP } from '../../../constants/networks'; +import useNetworkStore from '../../../context/useNetworkStore'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; +import { NetworkFeature } from '../../../types'; +import getLsNetwork from '../../../utils/liquidStaking/getLsNetwork'; +import testRpcEndpointConnection from '../../NetworkSelector/testRpcEndpointConnection'; + +const useLsChangeNetwork = () => { + const { selectedNetworkId, setSelectedNetworkId } = useLsStore(); + const { setNetwork } = useNetworkStore(); + const { notificationApi } = useWebbUI(); + + const tryChangeNetwork = useCallback( + async (newNetworkId: LsNetworkId) => { + // No need to change network if it's already selected. + if (selectedNetworkId === newNetworkId) { + return; + } + + const lsNetwork = getLsNetwork(newNetworkId); + + // Don't check connection to Ethereum mainnet liquifier; + // only verify RPC connection to Tangle networks. + if (lsNetwork.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { + setSelectedNetworkId(newNetworkId); + + return; + } + + const tangleNetwork = + lsNetwork.id === LsNetworkId.TANGLE_MAINNET + ? TANGLE_MAINNET_NETWORK + : TANGLE_TESTNET_NATIVE_NETWORK; + + const networkFeatures = NETWORK_FEATURE_MAP[tangleNetwork.id]; + + if (!networkFeatures.includes(NetworkFeature.LsPools)) { + notificationApi({ + message: 'Network does not support liquid staking yet', + variant: 'error', + }); + + return; + } + + // Try connecting to the new network. + const isRpcUp = await testRpcEndpointConnection( + tangleNetwork.wsRpcEndpoint, + ); + + if (!isRpcUp) { + notificationApi({ + message: 'Failed to connect to the network', + variant: 'error', + }); + + return; + } + + setSelectedNetworkId(newNetworkId); + setNetwork(tangleNetwork); + }, + [notificationApi, selectedNetworkId, setNetwork, setSelectedNetworkId], + ); + + return tryChangeNetwork; +}; + +export default useLsChangeNetwork; diff --git a/apps/tangle-dapp/constants/liquidStaking/constants.ts b/apps/tangle-dapp/constants/liquidStaking/constants.ts index a46fc07b2..c59016444 100644 --- a/apps/tangle-dapp/constants/liquidStaking/constants.ts +++ b/apps/tangle-dapp/constants/liquidStaking/constants.ts @@ -6,7 +6,8 @@ import MOONBEAM from '../../data/liquidStaking/adapters/moonbeam'; import PHALA from '../../data/liquidStaking/adapters/phala'; import POLKADOT from '../../data/liquidStaking/adapters/polkadot'; import POLYGON from '../../data/liquidStaking/adapters/polygon'; -import TANGLE from '../../data/liquidStaking/adapters/tangle'; +import TANGLE_MAINNET from '../../data/liquidStaking/adapters/tangleMainnet'; +import TANGLE_TESTNET from '../../data/liquidStaking/adapters/tangleTestnet'; import THE_GRAPH from '../../data/liquidStaking/adapters/theGraph'; import { IS_PRODUCTION_ENV } from '../env'; import { @@ -51,6 +52,8 @@ export const LS_LIQUIFIER_PROTOCOL_MAP: Record< export const LS_PROTOCOLS: LsProtocolDef[] = [ ...Object.values(LS_PARACHAIN_CHAIN_MAP), ...Object.values(LS_LIQUIFIER_PROTOCOL_MAP), + TANGLE_MAINNET, + TANGLE_TESTNET, ]; export const LS_LIQUIFIER_PROTOCOL_IDS = [ @@ -72,7 +75,6 @@ export const LS_PARACHAIN_TOKENS = [ LsToken.MANTA, LsToken.ASTAR, LsToken.PHALA, - LsToken.TNT, ] as const satisfies LsParachainToken[]; export const TVS_TOOLTIP = @@ -81,7 +83,7 @@ export const TVS_TOOLTIP = export const LS_DERIVATIVE_TOKEN_PREFIX = 'tg'; export const LS_ETHEREUM_MAINNET_LIQUIFIER: LsNetwork = { - type: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER, + id: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER, networkName: IS_PRODUCTION_ENV ? 'Ethereum Mainnet' : 'Sepolia Testnet', chainIconFileName: 'ethereum', defaultProtocolId: LsProtocolId.CHAINLINK, @@ -89,7 +91,7 @@ export const LS_ETHEREUM_MAINNET_LIQUIFIER: LsNetwork = { }; export const LS_TANGLE_RESTAKING_PARACHAIN: LsNetwork = { - type: LsNetworkId.TANGLE_RESTAKING_PARACHAIN, + id: LsNetworkId.TANGLE_RESTAKING_PARACHAIN, networkName: 'Tangle Parachain', chainIconFileName: 'tangle', defaultProtocolId: LsProtocolId.POLKADOT, @@ -98,16 +100,26 @@ export const LS_TANGLE_RESTAKING_PARACHAIN: LsNetwork = { }; export const LS_TANGLE_MAINNET: LsNetwork = { - type: LsNetworkId.TANGLE_MAINNET, + id: LsNetworkId.TANGLE_MAINNET, networkName: 'Tangle Mainnet', chainIconFileName: 'tangle', - defaultProtocolId: LsProtocolId.TANGLE, - protocols: [TANGLE], + defaultProtocolId: LsProtocolId.TANGLE_MAINNET, + protocols: [TANGLE_MAINNET], +}; + +export const LS_TANGLE_TESTNET: LsNetwork = { + id: LsNetworkId.TANGLE_MAINNET, + networkName: 'Tangle Testnet', + chainIconFileName: 'tangle', + defaultProtocolId: LsProtocolId.TANGLE_MAINNET, + protocols: [TANGLE_MAINNET], }; export const LS_NETWORKS: LsNetwork[] = [ - LS_ETHEREUM_MAINNET_LIQUIFIER, + LS_TANGLE_MAINNET, + LS_TANGLE_TESTNET, LS_TANGLE_RESTAKING_PARACHAIN, + LS_ETHEREUM_MAINNET_LIQUIFIER, ]; /** diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 17273c1b9..86dc850f0 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -12,7 +12,10 @@ import { import { PolkadotValidator } from '../../data/liquidStaking/adapters/polkadot'; import { SubstrateAddress } from '../../types/utils'; import { CrossChainTimeUnit } from '../../utils/CrossChainTime'; -import { TANGLE_MAINNET_NETWORK } from '../../../../libs/webb-ui-components/src/constants/networks'; +import { + TANGLE_MAINNET_NETWORK, + TANGLE_TESTNET_NATIVE_NETWORK, +} from '../../../../libs/webb-ui-components/src/constants/networks'; export enum LsProtocolId { POLKADOT, @@ -24,7 +27,8 @@ export enum LsProtocolId { THE_GRAPH, LIVEPEER, POLYGON, - TANGLE, + TANGLE_MAINNET, + TANGLE_TESTNET, } export type LsLiquifierProtocolId = @@ -42,6 +46,7 @@ export enum LsToken { ASTAR = 'ASTR', PHALA = 'PHALA', TNT = 'TNT', + TTNT = 'tTNT', LINK = 'LINK', GRT = 'GRT', LPT = 'LPT', @@ -54,7 +59,12 @@ export type LsLiquifierProtocolToken = | LsToken.LPT | LsToken.POL; -export type LsParachainToken = Exclude; +export type LsParachainToken = + | LsToken.DOT + | LsToken.GLMR + | LsToken.MANTA + | LsToken.ASTAR + | LsToken.PHALA; type ProtocolDefCommon = { name: string; @@ -65,18 +75,24 @@ type ProtocolDefCommon = { }; export enum LsNetworkId { + TANGLE_TESTNET, TANGLE_MAINNET, TANGLE_RESTAKING_PARACHAIN, ETHEREUM_MAINNET_LIQUIFIER, } -export interface LsTangleMainnetDef extends ProtocolDefCommon { - networkId: LsNetworkId.TANGLE_MAINNET; - id: LsProtocolId.TANGLE; - token: LsToken.TNT; +export interface LsTangleNetworkDef extends ProtocolDefCommon { + networkId: LsNetworkId.TANGLE_MAINNET | LsNetworkId.TANGLE_TESTNET; + id: LsProtocolId.TANGLE_MAINNET | LsProtocolId.TANGLE_TESTNET; + token: LsToken.TNT | LsToken.TTNT; rpcEndpoint: string; - ss58Prefix: typeof TANGLE_MAINNET_NETWORK.ss58Prefix; + ss58Prefix: + | typeof TANGLE_MAINNET_NETWORK.ss58Prefix + | typeof TANGLE_TESTNET_NATIVE_NETWORK.ss58Prefix; adapter: LsNetworkEntityAdapter; + tangleNetwork: + | typeof TANGLE_MAINNET_NETWORK + | typeof TANGLE_TESTNET_NATIVE_NETWORK; } export interface LsParachainChainDef @@ -104,7 +120,7 @@ export interface LsLiquifierProtocolDef extends ProtocolDefCommon { export type LsProtocolDef = | LsParachainChainDef | LsLiquifierProtocolDef - | LsTangleMainnetDef; + | LsTangleNetworkDef; export type LsCardSearchParams = { amount: BN; @@ -148,7 +164,7 @@ export type LsParachainSimpleTimeUnit = { }; export type LsNetwork = { - type: LsNetworkId; + id: LsNetworkId; networkName: string; chainIconFileName: string; defaultProtocolId: LsProtocolId; diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index e4c3f4c63..03e54bbdf 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -158,7 +158,7 @@ const DEFAULT_PAGINATION_STATE: PaginationState = { }; const LsPoolsTable: FC = () => { - const { setSelectedParachainPoolId } = useLsStore(); + const { setSelectedPoolId: setSelectedParachainPoolId } = useLsStore(); const [searchQuery, setSearchQuery] = useState(''); const [paginationState, setPaginationState] = useState( @@ -183,7 +183,7 @@ const LsPoolsTable: FC = () => { const selectedRow = selectedRowIds.at(0); assert(selectedRow !== undefined, 'One row must always be selected'); - setSelectedParachainPoolId(selectedRow); + setSelectedParachainPoolId(parseInt(selectedRow, 10)); setRowSelectionState(newSelectionState); }, [rowSelectionState, setSelectedParachainPoolId], diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangle.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx similarity index 97% rename from apps/tangle-dapp/data/liquidStaking/adapters/tangle.tsx rename to apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx index d375faf58..23efec195 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangle.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx @@ -17,7 +17,7 @@ import { IS_PRODUCTION_ENV } from '../../../constants/env'; import { LsNetworkId, LsProtocolId, - LsTangleMainnetDef, + LsTangleNetworkDef, LsToken, } from '../../../constants/liquidStaking/types'; import { LiquidStakingItem } from '../../../types/liquidStaking'; @@ -200,9 +200,9 @@ const getTableColumns: GetTableColumnsFn = ( ]; }; -const TANGLE = { +const TANGLE_MAINNET = { networkId: LsNetworkId.TANGLE_MAINNET, - id: LsProtocolId.TANGLE, + id: LsProtocolId.TANGLE_MAINNET, name: 'Tangle', token: LsToken.TNT, chainIconFileName: 'tangle', @@ -217,6 +217,7 @@ const TANGLE = { fetchProtocolEntities: fetchValidators, getTableColumns, }, -} as const satisfies LsTangleMainnetDef; + tangleNetwork: TANGLE_MAINNET_NETWORK, +} as const satisfies LsTangleNetworkDef; -export default TANGLE; +export default TANGLE_MAINNET; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx new file mode 100644 index 000000000..4b060e94f --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx @@ -0,0 +1,224 @@ +import { BN_ZERO } from '@polkadot/util'; +import { createColumnHelper, SortingFnOption } from '@tanstack/react-table'; +import { + Avatar, + CheckBox, + CopyWithTooltip, + shortenString, + Typography, +} from '@webb-tools/webb-ui-components'; +import { + TANGLE_LOCAL_DEV_NETWORK, + TANGLE_MAINNET_NETWORK, + TANGLE_TESTNET_NATIVE_NETWORK, +} from '@webb-tools/webb-ui-components/constants/networks'; + +import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton'; +import { IS_PRODUCTION_ENV } from '../../../constants/env'; +import { + LsNetworkId, + LsProtocolId, + LsTangleNetworkDef, + LsToken, +} from '../../../constants/liquidStaking/types'; +import { LiquidStakingItem } from '../../../types/liquidStaking'; +import assertSubstrateAddress from '../../../utils/assertSubstrateAddress'; +import calculateCommission from '../../../utils/calculateCommission'; +import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; +import formatBn from '../../../utils/formatBn'; +import { GetTableColumnsFn } from '../adapter'; +import { + sortCommission, + sortSelected, + sortValueStaked, +} from '../columnSorting'; +import { + fetchChainDecimals, + fetchMappedIdentityNames, + fetchMappedValidatorsCommission, + fetchMappedValidatorsTotalValueStaked, + fetchTokenSymbol, +} from '../fetchHelpers'; +import { PolkadotValidator } from './polkadot'; + +const DECIMALS = 18; + +const fetchValidators = async ( + rpcEndpoint: string, +): Promise => { + const [ + validators, + mappedIdentityNames, + mappedTotalValueStaked, + mappedCommission, + ] = await Promise.all([ + fetchValidators(rpcEndpoint), + fetchMappedIdentityNames(rpcEndpoint), + fetchMappedValidatorsTotalValueStaked(rpcEndpoint), + fetchMappedValidatorsCommission(rpcEndpoint), + fetchChainDecimals(rpcEndpoint), + fetchTokenSymbol(rpcEndpoint), + ]); + + return validators.map((address) => { + const identityName = mappedIdentityNames.get(address.toString()); + const totalValueStaked = mappedTotalValueStaked.get(address.toString()); + const commission = mappedCommission.get(address.toString()); + + return { + id: address.toString(), + address: assertSubstrateAddress(address.toString()), + identity: identityName ?? address.toString(), + totalValueStaked: totalValueStaked ?? BN_ZERO, + apy: 0, + commission: commission ?? BN_ZERO, + itemType: LiquidStakingItem.VALIDATOR, + }; + }); +}; + +const getTableColumns: GetTableColumnsFn = ( + toggleSortSelectionHandlerRef, +) => { + const validatorColumnHelper = createColumnHelper(); + + return [ + validatorColumnHelper.accessor('address', { + header: ({ header }) => { + toggleSortSelectionHandlerRef.current = header.column.toggleSorting; + return ( + + Validator + + ); + }, + cell: (props) => { + const address = props.getValue(); + const identity = props.row.original.identity ?? address; + + return ( +
    + + +
    + + + + {identity === address ? shortenString(address, 8) : identity} + + + +
    +
    + ); + }, + // TODO: Avoid casting sorting function. + sortingFn: sortSelected as SortingFnOption, + }), + validatorColumnHelper.accessor('totalValueStaked', { + header: ({ header }) => ( +
    + + Total Staked + +
    + ), + cell: (props) => ( +
    + + {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.DOT}`} + +
    + ), + sortingFn: sortValueStaked, + }), + validatorColumnHelper.accessor('commission', { + header: ({ header }) => ( +
    + + Commission + +
    + ), + cell: (props) => ( +
    + + {calculateCommission(props.getValue()).toFixed(2) + '%'} + +
    + ), + // TODO: Avoid casting sorting function. + sortingFn: sortCommission as SortingFnOption, + }), + validatorColumnHelper.display({ + id: 'href', + header: () => , + cell: (props) => { + const href = `https://polkadot.subscan.io/account/${props.getValue()}`; + + return ; + }, + }), + ]; +}; + +const TANGLE_TESTNET = { + networkId: LsNetworkId.TANGLE_TESTNET, + id: LsProtocolId.TANGLE_TESTNET, + name: 'Tangle', + token: LsToken.TNT, + chainIconFileName: 'tangle', + decimals: DECIMALS, + rpcEndpoint: IS_PRODUCTION_ENV + ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint + : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, + timeUnit: CrossChainTimeUnit.POLKADOT_ERA, + unstakingPeriod: 28, + ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, + adapter: { + fetchProtocolEntities: fetchValidators, + getTableColumns, + }, + tangleNetwork: TANGLE_TESTNET_NATIVE_NETWORK, +} as const satisfies LsTangleNetworkDef; + +export default TANGLE_TESTNET; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts index 1bddab5a3..b2ae069ab 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts @@ -74,6 +74,8 @@ const getDataType = (chain: LsProtocolId): LiquidStakingItem | null => { case LsProtocolId.LIVEPEER: case LsProtocolId.POLYGON: case LsProtocolId.THE_GRAPH: + case LsProtocolId.TANGLE_MAINNET: + case LsProtocolId.TANGLE_TESTNET: return null; } }; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts index 7c6a97b74..c12418f93 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts @@ -7,13 +7,13 @@ type State = { selectedNetworkId: LsNetworkId; selectedProtocolId: LsProtocolId; selectedNetworkEntities: Set; - selectedParachainPoolId: string | null; + selectedPoolId: number | null; }; type Actions = { setSelectedProtocolId: (newProtocolId: State['selectedProtocolId']) => void; setSelectedNetworkId: (newNetworkId: State['selectedNetworkId']) => void; - setSelectedParachainPoolId: (parachainPoolId: string) => void; + setSelectedPoolId: (poolId: number) => void; setSelectedNetworkEntities: ( selectedNetworkEntities: State['selectedNetworkEntities'], @@ -23,14 +23,12 @@ type Actions = { type Store = State & Actions; export const useLsStore = create((set) => ({ - selectedParachainPoolId: null, + selectedPoolId: null, selectedNetworkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN, selectedProtocolId: LsProtocolId.POLKADOT, selectedNetworkEntities: new Set(), - setSelectedParachainPoolId: (selectedParachainPoolId) => - set({ selectedParachainPoolId }), - setSelectedProtocolId: (selectedChainId) => - set({ selectedProtocolId: selectedChainId }), + setSelectedPoolId: (selectedPoolId) => set({ selectedPoolId }), + setSelectedProtocolId: (selectedProtocolId) => set({ selectedProtocolId }), setSelectedNetworkEntities: (selectedNetworkEntities) => set({ selectedNetworkEntities }), setSelectedNetworkId: (selectedNetworkId) => { diff --git a/apps/tangle-dapp/hooks/useSearchParamSync.ts b/apps/tangle-dapp/hooks/useSearchParamSync.ts index 307ac80a3..413d3e725 100644 --- a/apps/tangle-dapp/hooks/useSearchParamSync.ts +++ b/apps/tangle-dapp/hooks/useSearchParamSync.ts @@ -11,15 +11,16 @@ export type UseSearchParamSyncOptions = { setValue: (value: T) => unknown; }; -const createHref = (newSearchParams: ReadonlyURLSearchParams): string => { +const createHref = (newSearchParams?: ReadonlyURLSearchParams): string => { const newUrl = new URL(window.location.href); - for (const [key] of newUrl.searchParams) { - newUrl.searchParams.delete(key); - } + // Remove existing search params. + newUrl.search = ''; - for (const [key, value] of newSearchParams) { - newUrl.searchParams.set(key, value); + if (newSearchParams !== undefined) { + for (const [key, value] of newSearchParams) { + newUrl.searchParams.set(key, value); + } } return newUrl.toString(); @@ -114,7 +115,7 @@ const useSearchParamSync = ({ } const newSearchParams = updateSearchParam(key, stringifiedValue); - const href = createHref(new ReadonlyURLSearchParams(newSearchParams)); + const href = createHref(newSearchParams); console.debug('Syncing URL search param', key, stringifiedValue, href); router.push(href); diff --git a/apps/tangle-dapp/utils/formatBn.ts b/apps/tangle-dapp/utils/formatBn.ts index c1294f9ab..a70e40386 100644 --- a/apps/tangle-dapp/utils/formatBn.ts +++ b/apps/tangle-dapp/utils/formatBn.ts @@ -29,7 +29,7 @@ const DEFAULT_FORMAT_OPTIONS: FormatOptions = { trimTrailingZeroes: true, }; -// TODO: Break this function down into smaller local functions for improved legibility and modularity, since its logic is getting complex. Consider making it functional instead of modifying the various variables: Return {integerPart, fractionalPart} per transformation/function, so that it can be easily chainable monad-style. +// TODO: Break this function down into smaller local functions for improved legibility and modularity, since its logic is getting complex. Consider making it functional instead of modifying the various variables: Return {integerPart, fractionalPart} per transformation/function, so that it can be easily chainable monad-style. Also, prefer usage of Decimal.js instead of BN for better decimal handling without needing to manually handle the edge cases. function formatBn( amount: BN, decimals: number, diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts index 9f9ec7bb0..d2ee3d437 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts @@ -2,6 +2,7 @@ import { LS_ETHEREUM_MAINNET_LIQUIFIER, LS_TANGLE_MAINNET, LS_TANGLE_RESTAKING_PARACHAIN, + LS_TANGLE_TESTNET, } from '../../constants/liquidStaking/constants'; import { LsNetwork, LsNetworkId } from '../../constants/liquidStaking/types'; @@ -13,6 +14,8 @@ const getLsNetwork = (networkId: LsNetworkId): LsNetwork => { return LS_TANGLE_RESTAKING_PARACHAIN; case LsNetworkId.TANGLE_MAINNET: return LS_TANGLE_MAINNET; + case LsNetworkId.TANGLE_TESTNET: + return LS_TANGLE_TESTNET; } }; diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts index 685d7f852..faa8bf5a0 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import { LS_PROTOCOLS } from '../../constants/liquidStaking/constants'; +import { LsTangleNetworkDef } from '../../constants/liquidStaking/types'; import { LsLiquifierProtocolDef, LsParachainChainDef, @@ -10,7 +11,9 @@ import { type IdToDefMap = T extends LsParachainChainId ? LsParachainChainDef - : LsLiquifierProtocolDef; + : T extends LsProtocolId.TANGLE_MAINNET + ? LsTangleNetworkDef + : LsLiquifierProtocolDef; const getLsProtocolDef = (id: T): IdToDefMap => { const result = LS_PROTOCOLS.find((def) => def.id === id); From fb89adc183bbfac63c274afbeec756ab96d6fe4b Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 22 Sep 2024 23:52:35 -0400 Subject: [PATCH 21/54] feat(tangle-dapp): Add Tangle local to LS networks --- .../stakeAndUnstake/NetworkSelector.tsx | 27 ++- .../stakeAndUnstake/useLsChangeNetwork.ts | 13 +- .../NetworkSelectionButton.tsx | 8 +- .../constants/liquidStaking/constants.ts | 19 +- .../constants/liquidStaking/types.ts | 23 ++- .../liquidStaking/adapters/tangleLocal.tsx | 26 +++ .../liquidStaking/adapters/tangleMainnet.tsx | 195 +----------------- .../liquidStaking/adapters/tangleTestnet.tsx | 195 +----------------- .../utils/liquidStaking/getLsNetwork.ts | 3 + .../utils/liquidStaking/getLsTangleNetwork.ts | 31 +++ 10 files changed, 119 insertions(+), 421 deletions(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx create mode 100644 apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx index d22b3cbf4..d09eec2fb 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx @@ -7,17 +7,16 @@ import { DropdownMenuItem, Typography, } from '@webb-tools/webb-ui-components'; -import { - TANGLE_MAINNET_NETWORK, - TANGLE_TESTNET_NATIVE_NETWORK, -} from '@webb-tools/webb-ui-components/constants/networks'; +import assert from 'assert'; import { FC } from 'react'; +import { IS_PRODUCTION_ENV } from '../../../constants/env'; import { LS_NETWORKS } from '../../../constants/liquidStaking/constants'; import { LsNetworkId } from '../../../constants/liquidStaking/types'; import { NETWORK_FEATURE_MAP } from '../../../constants/networks'; import { NetworkFeature } from '../../../types'; import getLsNetwork from '../../../utils/liquidStaking/getLsNetwork'; +import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; import DropdownChevronIcon from './DropdownChevronIcon'; type NetworkSelectorProps = { @@ -53,19 +52,25 @@ const NetworkSelector: FC = ({ // Filter out networks that don't support liquid staking yet. const supportedLsNetworks = LS_NETWORKS.filter((network) => { - if (network.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { + // TODO: Check whether the restaking parachain supports liquid staking instead of hardcoding it. + if (network.id === LsNetworkId.TANGLE_RESTAKING_PARACHAIN) { + return true; + } else if (network.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { return true; } + // Exclude the local Tangle network in production. + else if (network.id === LsNetworkId.TANGLE_LOCAL && IS_PRODUCTION_ENV) { + return false; + } // TODO: Obtain the Tangle network from the LS Network's properties instead. - const tangleNetwork = - network.id === LsNetworkId.TANGLE_MAINNET - ? TANGLE_MAINNET_NETWORK - : TANGLE_TESTNET_NATIVE_NETWORK; + const tangleNetwork = getLsTangleNetwork(network.id); - const networkFeatures = NETWORK_FEATURE_MAP[tangleNetwork.id]; + assert(tangleNetwork !== null); - return networkFeatures.includes(NetworkFeature.LsPools); + return NETWORK_FEATURE_MAP[tangleNetwork.id].includes( + NetworkFeature.LsPools, + ); }); return setNetworkId !== undefined ? ( diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts index 0869a4036..e40f30c8b 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts @@ -1,8 +1,5 @@ import { useWebbUI } from '@webb-tools/webb-ui-components'; -import { - TANGLE_MAINNET_NETWORK, - TANGLE_TESTNET_NATIVE_NETWORK, -} from '@webb-tools/webb-ui-components/constants/networks'; +import assert from 'assert'; import { useCallback } from 'react'; import { LsNetworkId } from '../../../constants/liquidStaking/types'; @@ -11,6 +8,7 @@ import useNetworkStore from '../../../context/useNetworkStore'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import { NetworkFeature } from '../../../types'; import getLsNetwork from '../../../utils/liquidStaking/getLsNetwork'; +import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; import testRpcEndpointConnection from '../../NetworkSelector/testRpcEndpointConnection'; const useLsChangeNetwork = () => { @@ -35,10 +33,9 @@ const useLsChangeNetwork = () => { return; } - const tangleNetwork = - lsNetwork.id === LsNetworkId.TANGLE_MAINNET - ? TANGLE_MAINNET_NETWORK - : TANGLE_TESTNET_NATIVE_NETWORK; + const tangleNetwork = getLsTangleNetwork(newNetworkId); + + assert(tangleNetwork !== null); const networkFeatures = NETWORK_FEATURE_MAP[tangleNetwork.id]; diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx index 2fc775a9c..ddd5675db 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx @@ -16,7 +16,6 @@ import { TooltipTrigger, Typography, } from '@webb-tools/webb-ui-components'; -import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; import { usePathname } from 'next/navigation'; import { type FC, useCallback, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; @@ -99,11 +98,12 @@ const NetworkSelectionButton: FC = () => { // Network can't be switched from the Tangle Restaking Parachain while // on liquid staking page. else if (isInLiquidStakingPath) { - const liquidStakingNetworkName = isLiquifierProtocolId(selectedProtocolId) + // Special case when the liquifier is selected. + const lsNetworkName = isLiquifierProtocolId(selectedProtocolId) ? IS_PRODUCTION_ENV ? 'Ethereum Mainnet' : 'Sepolia Testnet' - : TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.name; + : networkName; const chainIconName = isLiquifierProtocolId(selectedProtocolId) ? 'ethereum' @@ -115,7 +115,7 @@ const NetworkSelectionButton: FC = () => { diff --git a/apps/tangle-dapp/constants/liquidStaking/constants.ts b/apps/tangle-dapp/constants/liquidStaking/constants.ts index c59016444..ee460ac6e 100644 --- a/apps/tangle-dapp/constants/liquidStaking/constants.ts +++ b/apps/tangle-dapp/constants/liquidStaking/constants.ts @@ -6,6 +6,7 @@ import MOONBEAM from '../../data/liquidStaking/adapters/moonbeam'; import PHALA from '../../data/liquidStaking/adapters/phala'; import POLKADOT from '../../data/liquidStaking/adapters/polkadot'; import POLYGON from '../../data/liquidStaking/adapters/polygon'; +import TANGLE_LOCAL from '../../data/liquidStaking/adapters/tangleLocal'; import TANGLE_MAINNET from '../../data/liquidStaking/adapters/tangleMainnet'; import TANGLE_TESTNET from '../../data/liquidStaking/adapters/tangleTestnet'; import THE_GRAPH from '../../data/liquidStaking/adapters/theGraph'; @@ -54,6 +55,7 @@ export const LS_PROTOCOLS: LsProtocolDef[] = [ ...Object.values(LS_LIQUIFIER_PROTOCOL_MAP), TANGLE_MAINNET, TANGLE_TESTNET, + TANGLE_LOCAL, ]; export const LS_LIQUIFIER_PROTOCOL_IDS = [ @@ -99,25 +101,34 @@ export const LS_TANGLE_RESTAKING_PARACHAIN: LsNetwork = { protocols: [POLKADOT, PHALA, MOONBEAM, ASTAR, MANTA] as LsProtocolDef[], }; -export const LS_TANGLE_MAINNET: LsNetwork = { +export const LS_TANGLE_MAINNET = { id: LsNetworkId.TANGLE_MAINNET, networkName: 'Tangle Mainnet', chainIconFileName: 'tangle', defaultProtocolId: LsProtocolId.TANGLE_MAINNET, protocols: [TANGLE_MAINNET], -}; +} as const satisfies LsNetwork; -export const LS_TANGLE_TESTNET: LsNetwork = { +export const LS_TANGLE_TESTNET = { id: LsNetworkId.TANGLE_MAINNET, networkName: 'Tangle Testnet', chainIconFileName: 'tangle', defaultProtocolId: LsProtocolId.TANGLE_MAINNET, protocols: [TANGLE_MAINNET], -}; +} as const satisfies LsNetwork; + +export const LS_TANGLE_LOCAL = { + id: LsNetworkId.TANGLE_LOCAL, + networkName: 'Tangle Local Dev', + chainIconFileName: 'tangle', + defaultProtocolId: LsProtocolId.TANGLE_LOCAL, + protocols: [TANGLE_LOCAL], +} as const satisfies LsNetwork; export const LS_NETWORKS: LsNetwork[] = [ LS_TANGLE_MAINNET, LS_TANGLE_TESTNET, + LS_TANGLE_LOCAL, LS_TANGLE_RESTAKING_PARACHAIN, LS_ETHEREUM_MAINNET_LIQUIFIER, ]; diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 86dc850f0..54309f2ae 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -9,12 +9,13 @@ import { LsNetworkEntityAdapter, ProtocolEntity, } from '../../data/liquidStaking/adapter'; -import { PolkadotValidator } from '../../data/liquidStaking/adapters/polkadot'; import { SubstrateAddress } from '../../types/utils'; import { CrossChainTimeUnit } from '../../utils/CrossChainTime'; import { + TANGLE_LOCAL_DEV_NETWORK, TANGLE_MAINNET_NETWORK, TANGLE_TESTNET_NATIVE_NETWORK, + Network as TangleNetwork, } from '../../../../libs/webb-ui-components/src/constants/networks'; export enum LsProtocolId { @@ -29,6 +30,7 @@ export enum LsProtocolId { POLYGON, TANGLE_MAINNET, TANGLE_TESTNET, + TANGLE_LOCAL, } export type LsLiquifierProtocolId = @@ -75,6 +77,7 @@ type ProtocolDefCommon = { }; export enum LsNetworkId { + TANGLE_LOCAL, TANGLE_TESTNET, TANGLE_MAINNET, TANGLE_RESTAKING_PARACHAIN, @@ -82,17 +85,21 @@ export enum LsNetworkId { } export interface LsTangleNetworkDef extends ProtocolDefCommon { - networkId: LsNetworkId.TANGLE_MAINNET | LsNetworkId.TANGLE_TESTNET; - id: LsProtocolId.TANGLE_MAINNET | LsProtocolId.TANGLE_TESTNET; + networkId: + | LsNetworkId.TANGLE_MAINNET + | LsNetworkId.TANGLE_TESTNET + | LsNetworkId.TANGLE_LOCAL; + id: + | LsProtocolId.TANGLE_MAINNET + | LsProtocolId.TANGLE_TESTNET + | LsProtocolId.TANGLE_LOCAL; token: LsToken.TNT | LsToken.TTNT; rpcEndpoint: string; ss58Prefix: | typeof TANGLE_MAINNET_NETWORK.ss58Prefix - | typeof TANGLE_TESTNET_NATIVE_NETWORK.ss58Prefix; - adapter: LsNetworkEntityAdapter; - tangleNetwork: - | typeof TANGLE_MAINNET_NETWORK - | typeof TANGLE_TESTNET_NATIVE_NETWORK; + | typeof TANGLE_TESTNET_NATIVE_NETWORK.ss58Prefix + | typeof TANGLE_LOCAL_DEV_NETWORK.ss58Prefix; + tangleNetwork: TangleNetwork; } export interface LsParachainChainDef diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx new file mode 100644 index 000000000..9afc6fc43 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx @@ -0,0 +1,26 @@ +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; +import { TANGLE_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; + +import { + LsNetworkId, + LsProtocolId, + LsTangleNetworkDef, + LsToken, +} from '../../../constants/liquidStaking/types'; +import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; + +const TANGLE_LOCAL = { + networkId: LsNetworkId.TANGLE_LOCAL, + id: LsProtocolId.TANGLE_LOCAL, + name: 'Tangle Local', + token: LsToken.TTNT, + chainIconFileName: 'tangle', + decimals: TANGLE_TOKEN_DECIMALS, + rpcEndpoint: TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, + timeUnit: CrossChainTimeUnit.POLKADOT_ERA, + unstakingPeriod: 28, + ss58Prefix: TANGLE_LOCAL_DEV_NETWORK.ss58Prefix, + tangleNetwork: TANGLE_LOCAL_DEV_NETWORK, +} as const satisfies LsTangleNetworkDef; + +export default TANGLE_LOCAL; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx index 23efec195..71ad06ff2 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx @@ -1,18 +1,9 @@ -import { BN_ZERO } from '@polkadot/util'; -import { createColumnHelper, SortingFnOption } from '@tanstack/react-table'; -import { - Avatar, - CheckBox, - CopyWithTooltip, - shortenString, - Typography, -} from '@webb-tools/webb-ui-components'; +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; import { TANGLE_LOCAL_DEV_NETWORK, TANGLE_MAINNET_NETWORK, } from '@webb-tools/webb-ui-components/constants/networks'; -import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton'; import { IS_PRODUCTION_ENV } from '../../../constants/env'; import { LsNetworkId, @@ -20,185 +11,7 @@ import { LsTangleNetworkDef, LsToken, } from '../../../constants/liquidStaking/types'; -import { LiquidStakingItem } from '../../../types/liquidStaking'; -import assertSubstrateAddress from '../../../utils/assertSubstrateAddress'; -import calculateCommission from '../../../utils/calculateCommission'; import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; -import formatBn from '../../../utils/formatBn'; -import { GetTableColumnsFn } from '../adapter'; -import { - sortCommission, - sortSelected, - sortValueStaked, -} from '../columnSorting'; -import { - fetchChainDecimals, - fetchMappedIdentityNames, - fetchMappedValidatorsCommission, - fetchMappedValidatorsTotalValueStaked, - fetchTokenSymbol, -} from '../fetchHelpers'; -import { PolkadotValidator } from './polkadot'; - -const DECIMALS = 18; - -const fetchValidators = async ( - rpcEndpoint: string, -): Promise => { - const [ - validators, - mappedIdentityNames, - mappedTotalValueStaked, - mappedCommission, - ] = await Promise.all([ - fetchValidators(rpcEndpoint), - fetchMappedIdentityNames(rpcEndpoint), - fetchMappedValidatorsTotalValueStaked(rpcEndpoint), - fetchMappedValidatorsCommission(rpcEndpoint), - fetchChainDecimals(rpcEndpoint), - fetchTokenSymbol(rpcEndpoint), - ]); - - return validators.map((address) => { - const identityName = mappedIdentityNames.get(address.toString()); - const totalValueStaked = mappedTotalValueStaked.get(address.toString()); - const commission = mappedCommission.get(address.toString()); - - return { - id: address.toString(), - address: assertSubstrateAddress(address.toString()), - identity: identityName ?? address.toString(), - totalValueStaked: totalValueStaked ?? BN_ZERO, - apy: 0, - commission: commission ?? BN_ZERO, - itemType: LiquidStakingItem.VALIDATOR, - }; - }); -}; - -const getTableColumns: GetTableColumnsFn = ( - toggleSortSelectionHandlerRef, -) => { - const validatorColumnHelper = createColumnHelper(); - - return [ - validatorColumnHelper.accessor('address', { - header: ({ header }) => { - toggleSortSelectionHandlerRef.current = header.column.toggleSorting; - return ( - - Validator - - ); - }, - cell: (props) => { - const address = props.getValue(); - const identity = props.row.original.identity ?? address; - - return ( -
    - - -
    - - - - {identity === address ? shortenString(address, 8) : identity} - - - -
    -
    - ); - }, - // TODO: Avoid casting sorting function. - sortingFn: sortSelected as SortingFnOption, - }), - validatorColumnHelper.accessor('totalValueStaked', { - header: ({ header }) => ( -
    - - Total Staked - -
    - ), - cell: (props) => ( -
    - - {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.DOT}`} - -
    - ), - sortingFn: sortValueStaked, - }), - validatorColumnHelper.accessor('commission', { - header: ({ header }) => ( -
    - - Commission - -
    - ), - cell: (props) => ( -
    - - {calculateCommission(props.getValue()).toFixed(2) + '%'} - -
    - ), - // TODO: Avoid casting sorting function. - sortingFn: sortCommission as SortingFnOption, - }), - validatorColumnHelper.display({ - id: 'href', - header: () => , - cell: (props) => { - const href = `https://polkadot.subscan.io/account/${props.getValue()}`; - - return ; - }, - }), - ]; -}; const TANGLE_MAINNET = { networkId: LsNetworkId.TANGLE_MAINNET, @@ -206,17 +19,13 @@ const TANGLE_MAINNET = { name: 'Tangle', token: LsToken.TNT, chainIconFileName: 'tangle', - decimals: DECIMALS, + decimals: TANGLE_TOKEN_DECIMALS, rpcEndpoint: IS_PRODUCTION_ENV ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, timeUnit: CrossChainTimeUnit.POLKADOT_ERA, unstakingPeriod: 28, ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, - adapter: { - fetchProtocolEntities: fetchValidators, - getTableColumns, - }, tangleNetwork: TANGLE_MAINNET_NETWORK, } as const satisfies LsTangleNetworkDef; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx index 4b060e94f..a20d054af 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx @@ -1,19 +1,10 @@ -import { BN_ZERO } from '@polkadot/util'; -import { createColumnHelper, SortingFnOption } from '@tanstack/react-table'; -import { - Avatar, - CheckBox, - CopyWithTooltip, - shortenString, - Typography, -} from '@webb-tools/webb-ui-components'; +import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; import { TANGLE_LOCAL_DEV_NETWORK, TANGLE_MAINNET_NETWORK, TANGLE_TESTNET_NATIVE_NETWORK, } from '@webb-tools/webb-ui-components/constants/networks'; -import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton'; import { IS_PRODUCTION_ENV } from '../../../constants/env'; import { LsNetworkId, @@ -21,185 +12,7 @@ import { LsTangleNetworkDef, LsToken, } from '../../../constants/liquidStaking/types'; -import { LiquidStakingItem } from '../../../types/liquidStaking'; -import assertSubstrateAddress from '../../../utils/assertSubstrateAddress'; -import calculateCommission from '../../../utils/calculateCommission'; import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; -import formatBn from '../../../utils/formatBn'; -import { GetTableColumnsFn } from '../adapter'; -import { - sortCommission, - sortSelected, - sortValueStaked, -} from '../columnSorting'; -import { - fetchChainDecimals, - fetchMappedIdentityNames, - fetchMappedValidatorsCommission, - fetchMappedValidatorsTotalValueStaked, - fetchTokenSymbol, -} from '../fetchHelpers'; -import { PolkadotValidator } from './polkadot'; - -const DECIMALS = 18; - -const fetchValidators = async ( - rpcEndpoint: string, -): Promise => { - const [ - validators, - mappedIdentityNames, - mappedTotalValueStaked, - mappedCommission, - ] = await Promise.all([ - fetchValidators(rpcEndpoint), - fetchMappedIdentityNames(rpcEndpoint), - fetchMappedValidatorsTotalValueStaked(rpcEndpoint), - fetchMappedValidatorsCommission(rpcEndpoint), - fetchChainDecimals(rpcEndpoint), - fetchTokenSymbol(rpcEndpoint), - ]); - - return validators.map((address) => { - const identityName = mappedIdentityNames.get(address.toString()); - const totalValueStaked = mappedTotalValueStaked.get(address.toString()); - const commission = mappedCommission.get(address.toString()); - - return { - id: address.toString(), - address: assertSubstrateAddress(address.toString()), - identity: identityName ?? address.toString(), - totalValueStaked: totalValueStaked ?? BN_ZERO, - apy: 0, - commission: commission ?? BN_ZERO, - itemType: LiquidStakingItem.VALIDATOR, - }; - }); -}; - -const getTableColumns: GetTableColumnsFn = ( - toggleSortSelectionHandlerRef, -) => { - const validatorColumnHelper = createColumnHelper(); - - return [ - validatorColumnHelper.accessor('address', { - header: ({ header }) => { - toggleSortSelectionHandlerRef.current = header.column.toggleSorting; - return ( - - Validator - - ); - }, - cell: (props) => { - const address = props.getValue(); - const identity = props.row.original.identity ?? address; - - return ( -
    - - -
    - - - - {identity === address ? shortenString(address, 8) : identity} - - - -
    -
    - ); - }, - // TODO: Avoid casting sorting function. - sortingFn: sortSelected as SortingFnOption, - }), - validatorColumnHelper.accessor('totalValueStaked', { - header: ({ header }) => ( -
    - - Total Staked - -
    - ), - cell: (props) => ( -
    - - {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.DOT}`} - -
    - ), - sortingFn: sortValueStaked, - }), - validatorColumnHelper.accessor('commission', { - header: ({ header }) => ( -
    - - Commission - -
    - ), - cell: (props) => ( -
    - - {calculateCommission(props.getValue()).toFixed(2) + '%'} - -
    - ), - // TODO: Avoid casting sorting function. - sortingFn: sortCommission as SortingFnOption, - }), - validatorColumnHelper.display({ - id: 'href', - header: () => , - cell: (props) => { - const href = `https://polkadot.subscan.io/account/${props.getValue()}`; - - return ; - }, - }), - ]; -}; const TANGLE_TESTNET = { networkId: LsNetworkId.TANGLE_TESTNET, @@ -207,17 +20,13 @@ const TANGLE_TESTNET = { name: 'Tangle', token: LsToken.TNT, chainIconFileName: 'tangle', - decimals: DECIMALS, + decimals: TANGLE_TOKEN_DECIMALS, rpcEndpoint: IS_PRODUCTION_ENV ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, timeUnit: CrossChainTimeUnit.POLKADOT_ERA, unstakingPeriod: 28, ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, - adapter: { - fetchProtocolEntities: fetchValidators, - getTableColumns, - }, tangleNetwork: TANGLE_TESTNET_NATIVE_NETWORK, } as const satisfies LsTangleNetworkDef; diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts index d2ee3d437..9969d0fe3 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts @@ -1,5 +1,6 @@ import { LS_ETHEREUM_MAINNET_LIQUIFIER, + LS_TANGLE_LOCAL, LS_TANGLE_MAINNET, LS_TANGLE_RESTAKING_PARACHAIN, LS_TANGLE_TESTNET, @@ -16,6 +17,8 @@ const getLsNetwork = (networkId: LsNetworkId): LsNetwork => { return LS_TANGLE_MAINNET; case LsNetworkId.TANGLE_TESTNET: return LS_TANGLE_TESTNET; + case LsNetworkId.TANGLE_LOCAL: + return LS_TANGLE_LOCAL; } }; diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts new file mode 100644 index 000000000..e0926314c --- /dev/null +++ b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts @@ -0,0 +1,31 @@ +import { + Network, + TANGLE_LOCAL_DEV_NETWORK, + TANGLE_MAINNET_NETWORK, + TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK, + TANGLE_RESTAKING_PARACHAIN_TESTNET_NETWORK, + TANGLE_TESTNET_NATIVE_NETWORK, +} from '@webb-tools/webb-ui-components/constants/networks'; + +import { LsNetworkId } from '../../constants/liquidStaking/types'; +import { IS_PRODUCTION_ENV } from '../../constants/env'; + +// TODO: Obtain the Tangle network directly from the adapter's `tangleNetwork` property instead of using this helper method. +const getLsTangleNetwork = (networkId: LsNetworkId): Network | null => { + switch (networkId) { + case LsNetworkId.TANGLE_MAINNET: + return TANGLE_MAINNET_NETWORK; + case LsNetworkId.TANGLE_TESTNET: + return TANGLE_TESTNET_NATIVE_NETWORK; + case LsNetworkId.TANGLE_LOCAL: + return TANGLE_LOCAL_DEV_NETWORK; + case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: + return IS_PRODUCTION_ENV + ? TANGLE_RESTAKING_PARACHAIN_TESTNET_NETWORK + : TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK; + case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: + return null; + } +}; + +export default getLsTangleNetwork; From 0dfecdba149043d2ad8e0491efc60810b6a0368a Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 23 Sep 2024 01:00:19 -0400 Subject: [PATCH 22/54] feat(tangle-dapp): Create `useLsPoolJoinTx` hook --- .../ExchangeRateDetailItem.tsx | 6 +- .../stakeAndUnstake/LsAgnosticBalance.tsx | 13 ++--- .../stakeAndUnstake/LsStakeCard.tsx | 56 ++++++++++++++----- .../stakeAndUnstake/LsUnstakeCard.tsx | 9 +-- .../stakeAndUnstake/NetworkSelector.tsx | 5 +- .../stakeAndUnstake/TotalDetailItem.tsx | 1 - .../stakeAndUnstake/useLsAgnosticBalance.ts | 31 +++++----- .../stakeAndUnstake/useLsFeePercentage.ts | 2 +- .../stakeAndUnstake/useLsSpendingLimits.ts | 2 +- .../NetworkSelectorDropdown.tsx | 6 +- apps/tangle-dapp/constants/index.ts | 1 + .../{ => parachain}/useMintTx.ts | 6 +- .../{ => parachain}/useParachainBalances.ts | 8 +-- .../{ => parachain}/useParachainLsFees.ts | 4 +- .../{ => parachain}/useRedeemTx.ts | 6 +- .../liquidStaking/tangle/useLsPoolJoinTx.ts | 28 ++++++++++ .../data/liquidStaking/useLsExchangeRate.ts | 28 ++++++---- .../liquidStaking/useLsProtocolEntities.ts | 1 + apps/tangle-dapp/hooks/useApiRx.ts | 6 +- apps/tangle-dapp/hooks/useSubstrateTx.ts | 3 +- apps/tangle-dapp/hooks/useTxNotification.tsx | 1 + .../src/constants/networks.ts | 2 +- 22 files changed, 138 insertions(+), 87 deletions(-) rename apps/tangle-dapp/data/liquidStaking/{ => parachain}/useMintTx.ts (88%) rename apps/tangle-dapp/data/liquidStaking/{ => parachain}/useParachainBalances.ts (89%) rename apps/tangle-dapp/data/liquidStaking/{ => parachain}/useParachainLsFees.ts (85%) rename apps/tangle-dapp/data/liquidStaking/{ => parachain}/useRedeemTx.ts (87%) create mode 100644 apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolJoinTx.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx index 6ebb815d8..fb1083c54 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx @@ -3,7 +3,7 @@ import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; -import { LsProtocolId, LsToken } from '../../../constants/liquidStaking/types'; +import { LsToken } from '../../../constants/liquidStaking/types'; import { ExchangeRateType } from '../../../data/liquidStaking/useLsExchangeRate'; import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate'; import DetailItem from './DetailItem'; @@ -11,15 +11,13 @@ import DetailItem from './DetailItem'; export type ExchangeRateDetailItemProps = { type: ExchangeRateType; token: LsToken; - protocolId: LsProtocolId; }; const ExchangeRateDetailItem: FC = ({ type, token, - protocolId, }) => { - const { exchangeRate, isRefreshing } = useLsExchangeRate(type, protocolId); + const { exchangeRate, isRefreshing } = useLsExchangeRate(type); const exchangeRateElement = exchangeRate instanceof Error ? ( diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index f80d19203..a558975dc 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -13,17 +13,14 @@ import { twMerge } from 'tailwind-merge'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; -import { - LsNetworkId, - LsProtocolId, -} from '../../../constants/liquidStaking/types'; +import { LsNetworkId } from '../../../constants/liquidStaking/types'; import formatBn from '../../../utils/formatBn'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import useLsAgnosticBalance from './useLsAgnosticBalance'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; export type LsAgnosticBalanceProps = { isNative?: boolean; - protocolId: LsProtocolId; tooltip?: string; onlyShowTooltipWhenBalanceIsSet?: boolean; onClick?: () => void; @@ -31,14 +28,14 @@ export type LsAgnosticBalanceProps = { const LsAgnosticBalance: FC = ({ isNative = true, - protocolId, tooltip, onlyShowTooltipWhenBalanceIsSet = true, onClick, }) => { const [isHovering, setIsHovering] = useState(false); - const { balance, isRefreshing } = useLsAgnosticBalance(isNative, protocolId); - const protocol = getLsProtocolDef(protocolId); + const { balance, isRefreshing } = useLsAgnosticBalance(isNative); + const { selectedProtocolId } = useLsStore(); + const protocol = getLsProtocolDef(selectedProtocolId); // Special case for liquid tokens on the `TgToken.sol` contract. // See: https://github.com/webb-tools/tnt-core/blob/1f371959884352e7af68e6091c5bb330fcaa58b8/src/lst/liquidtoken/TgToken.sol#L26 diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 47a9c5a35..15fa7e4cf 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -22,11 +22,12 @@ import { LsProtocolId, LsSearchParamKey, } from '../../../constants/liquidStaking/types'; +import useLsPoolJoinTx from '../../../data/liquidStaking/tangle/useLsPoolJoinTx'; import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useMintTx from '../../../data/liquidStaking/useMintTx'; +import useMintTx from '../../../data/liquidStaking/parachain/useMintTx'; import useLiquifierDeposit from '../../../data/liquifier/useLiquifierDeposit'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamState from '../../../hooks/useSearchParamState'; @@ -51,10 +52,15 @@ const LsStakeCard: FC = () => { stringify: (value) => value?.toString(), }); - const { selectedProtocolId, setSelectedProtocolId, selectedNetworkId } = - useLsStore(); + const { + selectedProtocolId, + setSelectedProtocolId, + selectedNetworkId, + selectedPoolId, + } = useLsStore(); - const { execute: executeMintTx, status: mintTxStatus } = useMintTx(); + const { execute: executeTanglePoolJoinTx } = useLsPoolJoinTx(); + const { execute: executeParachainMintTx, status: mintTxStatus } = useMintTx(); const performLiquifierDeposit = useLiquifierDeposit(); const activeAccountAddress = useActiveAccountAddress(); @@ -86,15 +92,17 @@ const LsStakeCard: FC = () => { const { exchangeRate: exchangeRateOrError, isRefreshing: isRefreshingExchangeRate, - } = useLsExchangeRate( - ExchangeRateType.NativeToDerivative, - selectedProtocolId, - ); + } = useLsExchangeRate(ExchangeRateType.NativeToDerivative); // TODO: Properly handle the error state. const exchangeRate = exchangeRateOrError instanceof Error ? null : exchangeRateOrError; + const isTangleNetwork = + selectedNetworkId === LsNetworkId.TANGLE_LOCAL || + selectedNetworkId === LsNetworkId.TANGLE_MAINNET || + selectedNetworkId === LsNetworkId.TANGLE_TESTNET; + const handleStakeClick = useCallback(async () => { // Not ready yet; no amount given. if (fromAmount === null) { @@ -103,9 +111,9 @@ const LsStakeCard: FC = () => { if ( selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeMintTx !== null + executeParachainMintTx !== null ) { - executeMintTx({ + executeParachainMintTx({ amount: fromAmount, currency: selectedProtocol.currency, }); @@ -114,8 +122,25 @@ const LsStakeCard: FC = () => { performLiquifierDeposit !== null ) { await performLiquifierDeposit(selectedProtocol.id, fromAmount); + } else if ( + isTangleNetwork && + executeTanglePoolJoinTx !== null && + selectedPoolId !== null + ) { + executeTanglePoolJoinTx({ + amount: fromAmount, + poolId: selectedPoolId, + }); } - }, [executeMintTx, fromAmount, performLiquifierDeposit, selectedProtocol]); + }, [ + executeParachainMintTx, + executeTanglePoolJoinTx, + fromAmount, + isTangleNetwork, + performLiquifierDeposit, + selectedProtocol, + selectedPoolId, + ]); const toAmount = useMemo(() => { if (fromAmount === null || exchangeRate === null) { @@ -128,13 +153,15 @@ const LsStakeCard: FC = () => { const canCallStake = (fromAmount !== null && selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeMintTx !== null) || + executeParachainMintTx !== null) || (selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierDeposit !== null); + performLiquifierDeposit !== null) || + (isTangleNetwork && + executeTanglePoolJoinTx !== null && + selectedPoolId !== null); const walletBalance = ( { if (maxSpendable !== null) { @@ -187,7 +214,6 @@ const LsStakeCard: FC = () => { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index f1616327e..b37a5361a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -20,7 +20,7 @@ import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useRedeemTx from '../../../data/liquidStaking/useRedeemTx'; +import useRedeemTx from '../../../data/liquidStaking/parachain/useRedeemTx'; import useLiquifierUnlock from '../../../data/liquifier/useLiquifierUnlock'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamSync from '../../../hooks/useSearchParamSync'; @@ -80,10 +80,7 @@ const LsUnstakeCard: FC = () => { const { exchangeRate: exchangeRateOrError, isRefreshing: isRefreshingExchangeRate, - } = useLsExchangeRate( - ExchangeRateType.DerivativeToNative, - selectedProtocol.id, - ); + } = useLsExchangeRate(ExchangeRateType.DerivativeToNative); // TODO: Properly handle the error state. const exchangeRate = @@ -159,7 +156,6 @@ const LsUnstakeCard: FC = () => { const stakedWalletBalance = ( setFromAmount(maxSpendable)} /> @@ -213,7 +209,6 @@ const LsUnstakeCard: FC = () => { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx index d09eec2fb..3ff8d4a65 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx @@ -52,10 +52,7 @@ const NetworkSelector: FC = ({ // Filter out networks that don't support liquid staking yet. const supportedLsNetworks = LS_NETWORKS.filter((network) => { - // TODO: Check whether the restaking parachain supports liquid staking instead of hardcoding it. - if (network.id === LsNetworkId.TANGLE_RESTAKING_PARACHAIN) { - return true; - } else if (network.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { + if (network.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { return true; } // Exclude the local Tangle network in production. diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx index 31041abb7..86a19cff2 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx @@ -30,7 +30,6 @@ const TotalDetailItem: FC = ({ isMinting ? ExchangeRateType.NativeToDerivative : ExchangeRateType.DerivativeToNative, - protocolId, ); const protocol = getLsProtocolDef(protocolId); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts index fe139a4ed..2a3045e03 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts @@ -4,17 +4,15 @@ import { erc20Abi } from 'viem'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import LIQUIFIER_TG_TOKEN_ABI from '../../../constants/liquidStaking/liquifierTgTokenAbi'; -import { - LsNetworkId, - LsProtocolId, -} from '../../../constants/liquidStaking/types'; +import { LsNetworkId } from '../../../constants/liquidStaking/types'; import useBalances from '../../../data/balances/useBalances'; -import useParachainBalances from '../../../data/liquidStaking/useParachainBalances'; +import useParachainBalances from '../../../data/liquidStaking/parachain/useParachainBalances'; import usePolling from '../../../data/liquidStaking/usePolling'; import useContractReadOnce from '../../../data/liquifier/useContractReadOnce'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useEvmAddress20 from '../../../hooks/useEvmAddress'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; type BalanceUpdater = ( prevBalance: BN | null | typeof EMPTY_VALUE_PLACEHOLDER, @@ -47,11 +45,12 @@ const createBalanceStateUpdater = ( }; }; -const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { +const useLsAgnosticBalance = (isNative: boolean) => { const activeAccountAddress = useActiveAccountAddress(); const evmAddress20 = useEvmAddress20(); const { nativeBalances, liquidBalances } = useParachainBalances(); - const { free: tangleBalance } = useBalances(); + const { free: tangleFreeBalance } = useBalances(); + const { selectedProtocolId, selectedNetworkId } = useLsStore(); // TODO: Why not use the subscription hook variants (useContractRead) instead of manually utilizing usePolling? const readErc20 = useContractReadOnce(erc20Abi); @@ -63,7 +62,7 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { const parachainBalances = isNative ? nativeBalances : liquidBalances; const isAccountConnected = activeAccountAddress !== null; - const protocol = getLsProtocolDef(protocolId); + const protocol = getLsProtocolDef(selectedProtocolId); // Reset balance to a placeholder when the active account is // disconnected, and to a loading state once an account is @@ -77,7 +76,7 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { if (isAccountConnected) { setBalance(null); } - }, [isAccountConnected, isNative, protocolId]); + }, [isAccountConnected, isNative, selectedProtocolId]); const erc20BalanceFetcher = useCallback(() => { if ( @@ -132,18 +131,20 @@ const useLsAgnosticBalance = (isNative: boolean, protocolId: LsProtocolId) => { setBalance(createBalanceStateUpdater(newBalance)); }, [parachainBalances, protocol.token, protocol.networkId]); + const isLsTangleNetwork = + selectedNetworkId === LsNetworkId.TANGLE_LOCAL || + selectedNetworkId === LsNetworkId.TANGLE_MAINNET || + selectedNetworkId === LsNetworkId.TANGLE_TESTNET; + // Update the balance to the Tangle balance when the Tangle // network is the active network. useEffect(() => { - if ( - protocol.networkId !== LsNetworkId.TANGLE_MAINNET || - tangleBalance === null - ) { + if (!isLsTangleNetwork || tangleFreeBalance === null) { return; } - setBalance(createBalanceStateUpdater(tangleBalance)); - }, [protocol.networkId, tangleBalance]); + setBalance(createBalanceStateUpdater(tangleFreeBalance)); + }, [protocol.networkId, tangleFreeBalance, isLsTangleNetwork]); return { balance, isRefreshing }; }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts index 397c3a40e..e7351fb7d 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts @@ -6,7 +6,7 @@ import { LsNetworkId, LsProtocolId, } from '../../../constants/liquidStaking/types'; -import useParachainLsFees from '../../../data/liquidStaking/useParachainLsFees'; +import useParachainLsFees from '../../../data/liquidStaking/parachain/useParachainLsFees'; import useContractRead from '../../../data/liquifier/useContractRead'; import { ContractReadOptions } from '../../../data/liquifier/useContractReadOnce'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts index efe9f2d75..ed3ef1933 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts @@ -20,7 +20,7 @@ const useLsSpendingLimits = ( isNative: boolean, protocolId: LsProtocolId, ): LsSpendingLimits => { - const { balance } = useLsAgnosticBalance(isNative, protocolId); + const { balance } = useLsAgnosticBalance(isNative); const { result: existentialDepositAmount } = useApi( useCallback((api) => api.consts.balances.existentialDeposit, []), diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx index 6b12d7748..497ebdc04 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectorDropdown.tsx @@ -30,14 +30,14 @@ export const NetworkSelectorDropdown: FC = ({ }) => { return (
    - {/* Mainnet network */} + {/* Tangle Mainnet */} onNetworkChange(TANGLE_MAINNET_NETWORK)} /> - {/* Testnet network */} + {/* Tangle Testnet */} = ({
    - {/* Local dev network */} + {/* Tangle Local Dev */} { const activeSubstrateAddress = useSubstrateAddress(); diff --git a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts b/apps/tangle-dapp/data/liquidStaking/parachain/useParachainLsFees.ts similarity index 85% rename from apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts rename to apps/tangle-dapp/data/liquidStaking/parachain/useParachainLsFees.ts index b82a6577b..24b0811b4 100644 --- a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts +++ b/apps/tangle-dapp/data/liquidStaking/parachain/useParachainLsFees.ts @@ -2,8 +2,8 @@ import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-u import { useCallback } from 'react'; import { map } from 'rxjs'; -import useApiRx from '../../hooks/useApiRx'; -import permillToPercentage from '../../utils/permillToPercentage'; +import useApiRx from '../../../hooks/useApiRx'; +import permillToPercentage from '../../../utils/permillToPercentage'; const useParachainLsFees = () => { return useApiRx( diff --git a/apps/tangle-dapp/data/liquidStaking/useRedeemTx.ts b/apps/tangle-dapp/data/liquidStaking/parachain/useRedeemTx.ts similarity index 87% rename from apps/tangle-dapp/data/liquidStaking/useRedeemTx.ts rename to apps/tangle-dapp/data/liquidStaking/parachain/useRedeemTx.ts index 9cfa6758f..8dbc610e6 100644 --- a/apps/tangle-dapp/data/liquidStaking/useRedeemTx.ts +++ b/apps/tangle-dapp/data/liquidStaking/parachain/useRedeemTx.ts @@ -5,12 +5,12 @@ import '@webb-tools/tangle-restaking-types'; import { BN } from '@polkadot/util'; import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; -import { TxName } from '../../constants'; +import { TxName } from '../../../constants'; import { LsParachainCurrencyKey, ParachainCurrency, -} from '../../constants/liquidStaking/types'; -import { useSubstrateTxWithNotification } from '../../hooks/useSubstrateTx'; +} from '../../../constants/liquidStaking/types'; +import { useSubstrateTxWithNotification } from '../../../hooks/useSubstrateTx'; export type RedeemTxContext = { amount: BN; diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolJoinTx.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolJoinTx.ts new file mode 100644 index 000000000..5f7eb0bda --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolJoinTx.ts @@ -0,0 +1,28 @@ +import { BN } from '@polkadot/util'; +import { useCallback } from 'react'; + +import { TxName } from '../../../constants'; +import { + SubstrateTxFactory, + useSubstrateTxWithNotification, +} from '../../../hooks/useSubstrateTx'; + +export type LsPoolJoinTxContext = { + poolId: number; + amount: BN; +}; + +const useLsPoolJoinTx = () => { + const substrateTxFactory: SubstrateTxFactory = + useCallback(async (api, _activeSubstrateAddress, { poolId, amount }) => { + return api.tx.lst.join(amount, poolId); + }, []); + + // TODO: Add EVM support once precompile(s) for the `lst` pallet are implemented on Tangle. + return useSubstrateTxWithNotification( + TxName.LS_TANGLE_POOL_JOIN, + substrateTxFactory, + ); +}; + +export default useLsPoolJoinTx; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts index 334434f8a..513ea30fe 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts @@ -8,7 +8,6 @@ import LIQUIFIER_TG_TOKEN_ABI from '../../constants/liquidStaking/liquifierTgTok import { LsNetworkId, LsParachainCurrencyKey, - LsProtocolId, } from '../../constants/liquidStaking/types'; import useApiRx from '../../hooks/useApiRx'; import calculateBnRatio from '../../utils/calculateBnRatio'; @@ -16,6 +15,7 @@ import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef'; import useContractRead from '../liquifier/useContractRead'; import { ContractReadOptions } from '../liquifier/useContractReadOnce'; import usePolling from './usePolling'; +import { useLsStore } from './useLsStore'; export enum ExchangeRateType { NativeToDerivative, @@ -47,13 +47,11 @@ const computeExchangeRate = ( const MAX_BN_OPERATION_NUMBER = 2 ** 26 - 1; -const useLsExchangeRate = ( - type: ExchangeRateType, - protocolId: LsProtocolId, -) => { +const useLsExchangeRate = (type: ExchangeRateType) => { const [exchangeRate, setExchangeRate] = useState(null); + const { selectedProtocolId, selectedNetworkId } = useLsStore(); - const protocol = getLsProtocolDef(protocolId); + const protocol = getLsProtocolDef(selectedProtocolId); const { result: tokenPoolAmount } = useApiRx((api) => { if (protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN) { @@ -144,10 +142,20 @@ const useLsExchangeRate = ( }, [liquifierTotalShares, tgTokenTotalSupply, type]); const fetch = useCallback(async () => { - const promise = - protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN - ? parachainExchangeRate - : fetchLiquifierExchangeRate(); + let promise: Promise; + + switch (selectedNetworkId) { + case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: + promise = fetchLiquifierExchangeRate(); + case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: + promise = parachainExchangeRate; + // Tangle networks with the `lst` pallet have a fixed exchange + // rate of 1:1. + case LsNetworkId.TANGLE_LOCAL: + case LsNetworkId.TANGLE_MAINNET: + case LsNetworkId.TANGLE_TESTNET: + promise = Promise.resolve(1); + } const newExchangeRate = await promise; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts index b2ae069ab..fa02539dc 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts @@ -76,6 +76,7 @@ const getDataType = (chain: LsProtocolId): LiquidStakingItem | null => { case LsProtocolId.THE_GRAPH: case LsProtocolId.TANGLE_MAINNET: case LsProtocolId.TANGLE_TESTNET: + case LsProtocolId.TANGLE_LOCAL: return null; } }; diff --git a/apps/tangle-dapp/hooks/useApiRx.ts b/apps/tangle-dapp/hooks/useApiRx.ts index 45e66171a..dd2ecd3ff 100644 --- a/apps/tangle-dapp/hooks/useApiRx.ts +++ b/apps/tangle-dapp/hooks/useApiRx.ts @@ -32,7 +32,7 @@ export type ObservableFactory = (api: ApiRx) => Observable | null; */ function useApiRx( factory: ObservableFactory, - overrideRpcEndpoint?: string, + rpcEndpointOverride?: string, ) { const [result, setResult] = useState(null); const [isLoading, setLoading] = useState(true); @@ -43,8 +43,8 @@ function useApiRx( const { result: apiRx } = usePromise( useCallback( - () => getApiRx(overrideRpcEndpoint ?? rpcEndpoint), - [overrideRpcEndpoint, rpcEndpoint], + () => getApiRx(rpcEndpointOverride ?? rpcEndpoint), + [rpcEndpointOverride, rpcEndpoint], ), null, ); diff --git a/apps/tangle-dapp/hooks/useSubstrateTx.ts b/apps/tangle-dapp/hooks/useSubstrateTx.ts index 7e4a30b79..184478a43 100644 --- a/apps/tangle-dapp/hooks/useSubstrateTx.ts +++ b/apps/tangle-dapp/hooks/useSubstrateTx.ts @@ -232,8 +232,7 @@ export function useSubstrateTxWithNotification( overrideRpcEndpoint, ); - const { notifyProcessing, notifySuccess, notifyError } = - useTxNotification(txName); + const { notifyProcessing, notifySuccess, notifyError } = useTxNotification(); const execute = useCallback( (context: Context) => { diff --git a/apps/tangle-dapp/hooks/useTxNotification.tsx b/apps/tangle-dapp/hooks/useTxNotification.tsx index 109654034..6c05ff185 100644 --- a/apps/tangle-dapp/hooks/useTxNotification.tsx +++ b/apps/tangle-dapp/hooks/useTxNotification.tsx @@ -36,6 +36,7 @@ const SUCCESS_MESSAGES: Record = { [TxName.LS_LIQUIFIER_APPROVE]: 'Liquifier approval successful', [TxName.LS_LIQUIFIER_UNLOCK]: 'Liquifier unlock successful', [TxName.LS_LIQUIFIER_WITHDRAW]: 'Liquifier withdrawal successful', + [TxName.LS_TANGLE_POOL_JOIN]: 'Joined liquid staking pool', }; const makeKey = (txName: TxName): `${TxName}-tx-notification` => diff --git a/libs/webb-ui-components/src/constants/networks.ts b/libs/webb-ui-components/src/constants/networks.ts index b6f7db91c..9781dafa2 100644 --- a/libs/webb-ui-components/src/constants/networks.ts +++ b/libs/webb-ui-components/src/constants/networks.ts @@ -99,7 +99,7 @@ export const TANGLE_LOCAL_DEV_NETWORK = { id: NetworkId.TANGLE_LOCAL_DEV, substrateChainId: SubstrateChainId.TangleLocalNative, evmChainId: EVMChainId.TangleLocalEVM, - name: 'Local endpoint', + name: 'Tangle Local Dev', tokenSymbol: TANGLE_TESTNET_NATIVE_TOKEN_SYMBOL, nodeType: 'standalone', subqueryEndpoint: 'http://localhost:4000/graphql', From 25d41e97eb3c57a0a07ccdc77ba157791be9348e Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 23 Sep 2024 01:06:48 -0400 Subject: [PATCH 23/54] refactor(tangle-dapp): Remove `FeedbackBanner` component --- .../containers/Layout/FeedbackBanner.tsx | 58 ------------------- apps/tangle-dapp/containers/Layout/Layout.tsx | 3 - apps/tangle-dapp/hooks/useLocalStorage.ts | 17 +++--- 3 files changed, 7 insertions(+), 71 deletions(-) delete mode 100644 apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx diff --git a/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx b/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx deleted file mode 100644 index 34db568db..000000000 --- a/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; - -import { Transition } from '@headlessui/react'; -import { BoxLine } from '@webb-tools/icons'; -import { Banner } from '@webb-tools/webb-ui-components/components/Banner'; -import { GITHUB_BUG_REPORT_URL } from '@webb-tools/webb-ui-components/constants'; -import { FC, useCallback, useEffect, useState } from 'react'; - -import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage'; - -const FeedbackBanner: FC = () => { - // Initially, the banner is hidden until the value is - // extracted from local storage. - const [showBanner, setShowBanner] = useState(false); - - const { - isSet: isBannerDismissalCacheSet, - set: setCachedWasBannerDismissed, - valueOpt: wasBannerDismissedOpt, - } = useLocalStorage(LocalStorageKey.WAS_BANNER_DISMISSED); - - // If there is no cache key, show the banner by default. - useEffect(() => { - if (!isBannerDismissalCacheSet()) { - setShowBanner(true); - } - }, [isBannerDismissalCacheSet, setShowBanner]); - - // If the banner was dismissed, do not show it to prevent - // annoying the user. - useEffect(() => { - if (wasBannerDismissedOpt?.value === true) { - setShowBanner(false); - } - }, [wasBannerDismissedOpt?.value]); - - const onCloseHandler = useCallback(() => { - setShowBanner(false); - setCachedWasBannerDismissed(true); - }, [setCachedWasBannerDismissed]); - - return ( - - - - ); -}; - -export default FeedbackBanner; diff --git a/apps/tangle-dapp/containers/Layout/Layout.tsx b/apps/tangle-dapp/containers/Layout/Layout.tsx index 38abe88bd..2b62adebe 100644 --- a/apps/tangle-dapp/containers/Layout/Layout.tsx +++ b/apps/tangle-dapp/containers/Layout/Layout.tsx @@ -21,7 +21,6 @@ import { IS_PRODUCTION_ENV } from '../../constants/env'; import ApiDevStatsContainer from '../DebugMetricsContainer'; import WalletAndChainContainer from '../WalletAndChainContainer/WalletAndChainContainer'; import { WalletModalContainer } from '../WalletModalContainer'; -import FeedbackBanner from './FeedbackBanner'; // Some specific overrides for the social links for use in the // footer in Tangle dApp, since it defaults to the Webb socials. @@ -51,8 +50,6 @@ const Layout: FC> = ({
    - -
    diff --git a/apps/tangle-dapp/hooks/useLocalStorage.ts b/apps/tangle-dapp/hooks/useLocalStorage.ts index ae4b9e110..25cc0c9fd 100644 --- a/apps/tangle-dapp/hooks/useLocalStorage.ts +++ b/apps/tangle-dapp/hooks/useLocalStorage.ts @@ -22,7 +22,6 @@ export enum LocalStorageKey { PAYOUTS = 'payouts', CUSTOM_RPC_ENDPOINT = 'customRpcEndpoint', KNOWN_NETWORK_ID = 'knownNetworkId', - WAS_BANNER_DISMISSED = 'wasBannerDismissed', SERVICES_CACHE = 'servicesCache', SUBSTRATE_WALLETS_METADATA = 'substrateWalletsMetadata', BRIDGE_TX_QUEUE_BY_ACC = 'bridgeTxQueue', @@ -72,15 +71,13 @@ export type LocalStorageValueOf = ? string : T extends LocalStorageKey.KNOWN_NETWORK_ID ? number - : T extends LocalStorageKey.WAS_BANNER_DISMISSED - ? boolean - : T extends LocalStorageKey.SUBSTRATE_WALLETS_METADATA - ? SubstrateWalletsMetadataCache - : T extends LocalStorageKey.BRIDGE_TX_QUEUE_BY_ACC - ? TxQueueByAccount - : T extends LocalStorageKey.LIQUID_STAKING_TABLE_DATA - ? LiquidStakingTableData - : never; + : T extends LocalStorageKey.SUBSTRATE_WALLETS_METADATA + ? SubstrateWalletsMetadataCache + : T extends LocalStorageKey.BRIDGE_TX_QUEUE_BY_ACC + ? TxQueueByAccount + : T extends LocalStorageKey.LIQUID_STAKING_TABLE_DATA + ? LiquidStakingTableData + : never; export const getJsonFromLocalStorage = ( key: Key, From ebb915e937be0bc03764958965c922a9233dbba6 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:04:40 -0400 Subject: [PATCH 24/54] fix(tangle-dapp): Fix Tangle networks id issue --- .../stakeAndUnstake/FeeDetailItem.tsx | 3 +++ .../LiquidStaking/stakeAndUnstake/LsStakeCard.tsx | 13 +++++++++---- .../stakeAndUnstake/useLsFeePercentage.ts | 15 ++++++++++++--- apps/tangle-dapp/constants/liquidStaking/types.ts | 12 +++++++++++- .../data/liquidStaking/useLsExchangeRate.ts | 8 ++++++-- apps/tangle-dapp/data/liquidStaking/useLsStore.ts | 6 ++++-- .../utils/liquidStaking/getLsProtocolDef.ts | 12 +++++++++--- 7 files changed, 54 insertions(+), 15 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx index 1385ae19a..7e1488ae7 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx @@ -1,6 +1,7 @@ import { BN, BN_ZERO } from '@polkadot/util'; import { FC, useMemo } from 'react'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LsProtocolId } from '../../../constants/liquidStaking/types'; import formatBn from '../../../utils/formatBn'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; @@ -36,6 +37,8 @@ const FeeDetailItem: FC = ({ // Propagate error or loading state. if (!(feeAmount instanceof BN)) { return feeAmount; + } else if (feeAmount.isZero()) { + return EMPTY_VALUE_PLACEHOLDER; } const formattedAmount = formatBn(feeAmount, protocol.decimals, { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 15fa7e4cf..0c68937c6 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -22,12 +22,12 @@ import { LsProtocolId, LsSearchParamKey, } from '../../../constants/liquidStaking/types'; +import useMintTx from '../../../data/liquidStaking/parachain/useMintTx'; import useLsPoolJoinTx from '../../../data/liquidStaking/tangle/useLsPoolJoinTx'; import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useMintTx from '../../../data/liquidStaking/parachain/useMintTx'; import useLiquifierDeposit from '../../../data/liquifier/useLiquifierDeposit'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamState from '../../../hooks/useSearchParamState'; @@ -59,8 +59,10 @@ const LsStakeCard: FC = () => { selectedPoolId, } = useLsStore(); - const { execute: executeTanglePoolJoinTx } = useLsPoolJoinTx(); - const { execute: executeParachainMintTx, status: mintTxStatus } = useMintTx(); + const { execute: executeTanglePoolJoinTx, status: tanglePoolJoinTxStatus } = + useLsPoolJoinTx(); + const { execute: executeParachainMintTx, status: parachainMintTxStatus } = + useMintTx(); const performLiquifierDeposit = useLiquifierDeposit(); const activeAccountAddress = useActiveAccountAddress(); @@ -242,7 +244,10 @@ const LsStakeCard: FC = () => { fromAmount === null || fromAmount.isZero() } - isLoading={mintTxStatus === TxStatus.PROCESSING} + isLoading={ + parachainMintTxStatus === TxStatus.PROCESSING || + tanglePoolJoinTxStatus === TxStatus.PROCESSING + } loadingText="Processing" onClick={handleStakeClick} isFullWidth diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts index e7351fb7d..db26b4d05 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts @@ -63,9 +63,18 @@ const useLsFeePercentage = ( ? null : Number(rawLiquifierFeeOrError) / 100; - return protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN - ? parachainFee - : liquifierFeePercentageOrError; + switch (protocol.networkId) { + case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: + return parachainFee; + case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: + return liquifierFeePercentageOrError; + // Tangle networks with the `lst` pallet have no fees for + // joining or leaving pools as of now. + case LsNetworkId.TANGLE_LOCAL: + case LsNetworkId.TANGLE_MAINNET: + case LsNetworkId.TANGLE_TESTNET: + return 0; + } }; export default useLsFeePercentage; diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 54309f2ae..8058e4466 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -39,7 +39,17 @@ export type LsLiquifierProtocolId = | LsProtocolId.LIVEPEER | LsProtocolId.POLYGON; -export type LsParachainChainId = Exclude; +export type LsParachainChainId = + | LsProtocolId.POLKADOT + | LsProtocolId.PHALA + | LsProtocolId.MOONBEAM + | LsProtocolId.ASTAR + | LsProtocolId.MANTA; + +export type LsTangleNetworkId = + | LsProtocolId.TANGLE_MAINNET + | LsProtocolId.TANGLE_TESTNET + | LsProtocolId.TANGLE_LOCAL; export enum LsToken { DOT = 'DOT', diff --git a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts index 513ea30fe..9df9b0a57 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts @@ -14,8 +14,8 @@ import calculateBnRatio from '../../utils/calculateBnRatio'; import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef'; import useContractRead from '../liquifier/useContractRead'; import { ContractReadOptions } from '../liquifier/useContractReadOnce'; -import usePolling from './usePolling'; import { useLsStore } from './useLsStore'; +import usePolling from './usePolling'; export enum ExchangeRateType { NativeToDerivative, @@ -147,8 +147,12 @@ const useLsExchangeRate = (type: ExchangeRateType) => { switch (selectedNetworkId) { case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: promise = fetchLiquifierExchangeRate(); + + break; case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: promise = parachainExchangeRate; + + break; // Tangle networks with the `lst` pallet have a fixed exchange // rate of 1:1. case LsNetworkId.TANGLE_LOCAL: @@ -166,7 +170,7 @@ const useLsExchangeRate = (type: ExchangeRateType) => { } setExchangeRate(newExchangeRate); - }, [fetchLiquifierExchangeRate, parachainExchangeRate, protocol]); + }, [fetchLiquifierExchangeRate, parachainExchangeRate, selectedNetworkId]); // Pause or resume ERC20-based exchange rate fetching based // on whether the requested protocol is a parachain or an ERC20 token. diff --git a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts index c12418f93..3e7d6d279 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts @@ -24,9 +24,11 @@ type Store = State & Actions; export const useLsStore = create((set) => ({ selectedPoolId: null, - selectedNetworkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN, - selectedProtocolId: LsProtocolId.POLKADOT, selectedNetworkEntities: new Set(), + // Default the selected network and protocol to the Tangle testnet, + // and tTNT, until liquid staking pools are deployed to mainnet. + selectedNetworkId: LsNetworkId.TANGLE_TESTNET, + selectedProtocolId: LsProtocolId.TANGLE_TESTNET, setSelectedPoolId: (selectedPoolId) => set({ selectedPoolId }), setSelectedProtocolId: (selectedProtocolId) => set({ selectedProtocolId }), setSelectedNetworkEntities: (selectedNetworkEntities) => diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts index faa8bf5a0..a47aade40 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts @@ -1,7 +1,11 @@ import assert from 'assert'; import { LS_PROTOCOLS } from '../../constants/liquidStaking/constants'; -import { LsTangleNetworkDef } from '../../constants/liquidStaking/types'; +import { + LsLiquifierProtocolId, + LsTangleNetworkDef, + LsTangleNetworkId, +} from '../../constants/liquidStaking/types'; import { LsLiquifierProtocolDef, LsParachainChainDef, @@ -11,9 +15,11 @@ import { type IdToDefMap = T extends LsParachainChainId ? LsParachainChainDef - : T extends LsProtocolId.TANGLE_MAINNET + : T extends LsTangleNetworkId ? LsTangleNetworkDef - : LsLiquifierProtocolDef; + : T extends LsLiquifierProtocolId + ? LsLiquifierProtocolDef + : never; const getLsProtocolDef = (id: T): IdToDefMap => { const result = LS_PROTOCOLS.find((def) => def.id === id); From 77794888f4508d3e612ac943acfbd7e589beb46a Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 26 Sep 2024 00:40:42 -0400 Subject: [PATCH 25/54] feat(tangle-dapp): Create `useLsPoolUnbondTx` hook --- .../stakeAndUnstake/LsAgnosticBalance.tsx | 2 +- .../stakeAndUnstake/LsStakeCard.tsx | 2 + .../stakeAndUnstake/LsUnstakeCard.tsx | 93 ++++++++++--------- .../stakeAndUnstake/useLsAgnosticBalance.ts | 27 +++++- .../UnstakeRequestsTable.tsx | 5 +- apps/tangle-dapp/constants/index.ts | 1 + .../liquidStaking/adapters/tangleLocal.tsx | 2 +- .../liquidStaking/adapters/tangleMainnet.tsx | 2 +- .../liquidStaking/adapters/tangleTestnet.tsx | 2 +- .../liquidStaking/tangle/useLsPoolBalance.ts | 49 ++++++++++ .../liquidStaking/tangle/useLsPoolUnbondTx.ts | 28 ++++++ apps/tangle-dapp/hooks/useTxNotification.tsx | 1 + apps/tangle-dapp/utils/Optional.ts | 24 ++++- 13 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolUnbondTx.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index a558975dc..58d11a69e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -14,10 +14,10 @@ import { twMerge } from 'tailwind-merge'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; import { LsNetworkId } from '../../../constants/liquidStaking/types'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import formatBn from '../../../utils/formatBn'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import useLsAgnosticBalance from './useLsAgnosticBalance'; -import { useLsStore } from '../../../data/liquidStaking/useLsStore'; export type LsAgnosticBalanceProps = { isNative?: boolean; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 0c68937c6..cda7cb3c0 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -61,8 +61,10 @@ const LsStakeCard: FC = () => { const { execute: executeTanglePoolJoinTx, status: tanglePoolJoinTxStatus } = useLsPoolJoinTx(); + const { execute: executeParachainMintTx, status: parachainMintTxStatus } = useMintTx(); + const performLiquifierDeposit = useLiquifierDeposit(); const activeAccountAddress = useActiveAccountAddress(); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index b37a5361a..2afd1a6b2 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -16,11 +16,12 @@ import { LsProtocolId, LsSearchParamKey, } from '../../../constants/liquidStaking/types'; +import useRedeemTx from '../../../data/liquidStaking/parachain/useRedeemTx'; +import useLsPoolUnbondTx from '../../../data/liquidStaking/tangle/useLsPoolUnbondTx'; import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useRedeemTx from '../../../data/liquidStaking/parachain/useRedeemTx'; import useLiquifierUnlock from '../../../data/liquifier/useLiquifierUnlock'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamSync from '../../../hooks/useSearchParamSync'; @@ -34,7 +35,6 @@ import LsInput from './LsInput'; import SelectTokenModal from './SelectTokenModal'; import TotalDetailItem from './TotalDetailItem'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; -import UnstakeRequestSubmittedModal from './UnstakeRequestSubmittedModal'; import useLsChangeNetwork from './useLsChangeNetwork'; import useLsSpendingLimits from './useLsSpendingLimits'; @@ -44,14 +44,12 @@ const LsUnstakeCard: FC = () => { const activeAccountAddress = useActiveAccountAddress(); const tryChangeNetwork = useLsChangeNetwork(); - const { selectedProtocolId, setSelectedProtocolId, selectedNetworkId } = - useLsStore(); - - const [didLiquifierUnlockSucceed, setDidLiquifierUnlockSucceed] = - useState(false); - - const [isRequestSubmittedModalOpen, setIsRequestSubmittedModalOpen] = - useState(false); + const { + selectedProtocolId, + setSelectedProtocolId, + selectedNetworkId, + selectedPoolId, + } = useLsStore(); // TODO: Won't both of these hooks be attempting to update the same state? useSearchParamSync({ @@ -62,11 +60,11 @@ const LsUnstakeCard: FC = () => { setValue: setSelectedProtocolId, }); - const { - execute: executeRedeemTx, - status: redeemTxStatus, - txHash: redeemTxHash, - } = useRedeemTx(); + const { execute: executeParachainRedeemTx, status: parachainRedeemTxStatus } = + useRedeemTx(); + + const { execute: executeTangleUnbondTx, status: tangleUnbondTxStatus } = + useLsPoolUnbondTx(); const performLiquifierUnlock = useLiquifierUnlock(); @@ -94,6 +92,11 @@ const LsUnstakeCard: FC = () => { stringify: (value) => value?.toString(), }); + const isTangleNetwork = + selectedNetworkId === LsNetworkId.TANGLE_LOCAL || + selectedNetworkId === LsNetworkId.TANGLE_MAINNET || + selectedNetworkId === LsNetworkId.TANGLE_TESTNET; + const handleUnstakeClick = useCallback(async () => { // Cannot perform transaction: Amount not set. if (fromAmount === null) { @@ -102,9 +105,9 @@ const LsUnstakeCard: FC = () => { if ( selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeRedeemTx !== null + executeParachainRedeemTx !== null ) { - executeRedeemTx({ + return executeParachainRedeemTx({ amount: fromAmount, currency: selectedProtocol.currency, }); @@ -112,16 +115,28 @@ const LsUnstakeCard: FC = () => { selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && performLiquifierUnlock !== null ) { - setDidLiquifierUnlockSucceed(false); - - const success = await performLiquifierUnlock( - selectedProtocol.id, - fromAmount, - ); + return performLiquifierUnlock(selectedProtocol.id, fromAmount); + } - setDidLiquifierUnlockSucceed(success); + if ( + isTangleNetwork && + executeTangleUnbondTx !== null && + selectedPoolId !== null + ) { + return executeTangleUnbondTx({ + points: fromAmount, + poolId: selectedPoolId, + }); } - }, [executeRedeemTx, fromAmount, performLiquifierUnlock, selectedProtocol]); + }, [ + executeParachainRedeemTx, + executeTangleUnbondTx, + fromAmount, + isTangleNetwork, + performLiquifierUnlock, + selectedPoolId, + selectedProtocol, + ]); const toAmount = useMemo(() => { if (fromAmount === null || exchangeRate === null) { @@ -140,14 +155,6 @@ const LsUnstakeCard: FC = () => { return [{ address: '0x123456' as any, amount: new BN(100), decimals: 18 }]; }, []); - // Open the request submitted modal when the redeem - // transaction is complete. - useEffect(() => { - if (redeemTxStatus === TxStatus.COMPLETE || didLiquifierUnlockSucceed) { - setIsRequestSubmittedModalOpen(true); - } - }, [didLiquifierUnlockSucceed, redeemTxStatus]); - // Reset the input amount when the network changes. useEffect(() => { setFromAmount(null); @@ -161,12 +168,15 @@ const LsUnstakeCard: FC = () => { /> ); - // TODO: Also check if the user has enough balance to unstake. + // TODO: Also check if the user has enough balance to unstake. Convert this into a self-executing function to break down the complexity of a one-liner. const canCallUnstake = (selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeRedeemTx !== null) || + executeParachainRedeemTx !== null) || (selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierUnlock !== null); + performLiquifierUnlock !== null) || + (isTangleNetwork && + executeTangleUnbondTx !== null && + selectedPoolId !== null); return ( <> @@ -237,7 +247,10 @@ const LsUnstakeCard: FC = () => { fromAmount === null || fromAmount.isZero() } - isLoading={redeemTxStatus === TxStatus.PROCESSING} + isLoading={ + parachainRedeemTxStatus === TxStatus.PROCESSING || + tangleUnbondTxStatus === TxStatus.PROCESSING + } loadingText="Processing" onClick={handleUnstakeClick} isFullWidth @@ -251,12 +264,6 @@ const LsUnstakeCard: FC = () => { onClose={() => setIsSelectTokenModalOpen(false)} onTokenSelect={handleTokenSelect} /> - - setIsRequestSubmittedModalOpen(false)} - txHash={redeemTxHash} - /> ); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts index 2a3045e03..b6a017a82 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts @@ -7,12 +7,13 @@ import LIQUIFIER_TG_TOKEN_ABI from '../../../constants/liquidStaking/liquifierTg import { LsNetworkId } from '../../../constants/liquidStaking/types'; import useBalances from '../../../data/balances/useBalances'; import useParachainBalances from '../../../data/liquidStaking/parachain/useParachainBalances'; +import useLsPoolBalance from '../../../data/liquidStaking/tangle/useLsPoolBalance'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import usePolling from '../../../data/liquidStaking/usePolling'; import useContractReadOnce from '../../../data/liquifier/useContractReadOnce'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useEvmAddress20 from '../../../hooks/useEvmAddress'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; -import { useLsStore } from '../../../data/liquidStaking/useLsStore'; type BalanceUpdater = ( prevBalance: BN | null | typeof EMPTY_VALUE_PLACEHOLDER, @@ -51,6 +52,7 @@ const useLsAgnosticBalance = (isNative: boolean) => { const { nativeBalances, liquidBalances } = useParachainBalances(); const { free: tangleFreeBalance } = useBalances(); const { selectedProtocolId, selectedNetworkId } = useLsStore(); + const tangleAssetBalance = useLsPoolBalance(); // TODO: Why not use the subscription hook variants (useContractRead) instead of manually utilizing usePolling? const readErc20 = useContractReadOnce(erc20Abi); @@ -139,12 +141,29 @@ const useLsAgnosticBalance = (isNative: boolean) => { // Update the balance to the Tangle balance when the Tangle // network is the active network. useEffect(() => { - if (!isLsTangleNetwork || tangleFreeBalance === null) { + if (!isLsTangleNetwork) { + return; + } + // Relevant balance hasn't loaded yet or isn't available. + else if ( + (isNative && tangleFreeBalance === null) || + (!isNative && tangleAssetBalance === null) + ) { return; } - setBalance(createBalanceStateUpdater(tangleFreeBalance)); - }, [protocol.networkId, tangleFreeBalance, isLsTangleNetwork]); + setBalance( + createBalanceStateUpdater( + isNative ? tangleFreeBalance : tangleAssetBalance, + ), + ); + }, [ + protocol.networkId, + tangleFreeBalance, + isLsTangleNetwork, + tangleAssetBalance, + isNative, + ]); return { balance, isRefreshing }; }; diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index b776a3efa..abc81f56c 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -320,18 +320,19 @@ const UnstakeRequestsTable: FC = () => { /> )} + {/* TODO: Assert that the id is either parachain or liquifier, if it isn't then we might need to hide this unstake requests table and show a specific one for Tangle networks (LS pools). */} {isLsParachainChainId(selectedProtocolId) ? ( - ) : ( + ) : isLiquifierProtocolId(selectedProtocolId) ? ( - )} + ) : undefined}
    )} diff --git a/apps/tangle-dapp/constants/index.ts b/apps/tangle-dapp/constants/index.ts index a713620f6..b7e6d0ba2 100644 --- a/apps/tangle-dapp/constants/index.ts +++ b/apps/tangle-dapp/constants/index.ts @@ -62,6 +62,7 @@ export enum TxName { LS_LIQUIFIER_UNLOCK = 'liquifier unlock', LS_LIQUIFIER_WITHDRAW = 'liquifier withdraw', LS_TANGLE_POOL_JOIN = 'join liquid staking pool', + LS_TANGLE_POOL_UNBOND = 'unbond from liquid staking pool', } export const PAYMENT_DESTINATION_OPTIONS: StakingRewardsDestinationDisplayText[] = diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx index 9afc6fc43..3dff97eeb 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleLocal.tsx @@ -18,7 +18,7 @@ const TANGLE_LOCAL = { decimals: TANGLE_TOKEN_DECIMALS, rpcEndpoint: TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, timeUnit: CrossChainTimeUnit.POLKADOT_ERA, - unstakingPeriod: 28, + unstakingPeriod: 14, ss58Prefix: TANGLE_LOCAL_DEV_NETWORK.ss58Prefix, tangleNetwork: TANGLE_LOCAL_DEV_NETWORK, } as const satisfies LsTangleNetworkDef; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx index 71ad06ff2..8b61eb3e9 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx @@ -24,7 +24,7 @@ const TANGLE_MAINNET = { ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, timeUnit: CrossChainTimeUnit.POLKADOT_ERA, - unstakingPeriod: 28, + unstakingPeriod: 14, ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, tangleNetwork: TANGLE_MAINNET_NETWORK, } as const satisfies LsTangleNetworkDef; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx index a20d054af..1e9a8f258 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx @@ -25,7 +25,7 @@ const TANGLE_TESTNET = { ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, timeUnit: CrossChainTimeUnit.POLKADOT_ERA, - unstakingPeriod: 28, + unstakingPeriod: 14, ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, tangleNetwork: TANGLE_TESTNET_NATIVE_NETWORK, } as const satisfies LsTangleNetworkDef; diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts new file mode 100644 index 000000000..14a8f3c28 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts @@ -0,0 +1,49 @@ +import { BN_ZERO } from '@polkadot/util'; +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../../hooks/useApiRx'; +import useNetworkFeatures from '../../../hooks/useNetworkFeatures'; +import useSubstrateAddress from '../../../hooks/useSubstrateAddress'; +import { NetworkFeature } from '../../../types'; +import { useLsStore } from '../useLsStore'; + +const useLsPoolBalance = () => { + const substrateAddress = useSubstrateAddress(); + const networkFeatures = useNetworkFeatures(); + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); + const { selectedPoolId } = useLsStore(); + + const { result: tanglePoolAssetAccountOpt } = useApiRx( + useCallback( + (api) => { + // The liquid staking pools functionality isn't available on the active + // network, the user hasn't selected a pool yet, or there is no active + // account. + if ( + !isSupported || + selectedPoolId === null || + substrateAddress === null + ) { + return null; + } + + return api.query.assets.account(selectedPoolId, substrateAddress); + }, + [isSupported, selectedPoolId, substrateAddress], + ), + ); + + const derivativeBalanceOpt = useMemo(() => { + if (tanglePoolAssetAccountOpt === null) { + return null; + } else if (tanglePoolAssetAccountOpt.isNone) { + return BN_ZERO; + } + + return tanglePoolAssetAccountOpt.unwrap().balance.toBn(); + }, [tanglePoolAssetAccountOpt]); + + return derivativeBalanceOpt; +}; + +export default useLsPoolBalance; diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolUnbondTx.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolUnbondTx.ts new file mode 100644 index 000000000..b72bd79a1 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolUnbondTx.ts @@ -0,0 +1,28 @@ +import { BN } from '@polkadot/util'; +import { useCallback } from 'react'; + +import { TxName } from '../../../constants'; +import { + SubstrateTxFactory, + useSubstrateTxWithNotification, +} from '../../../hooks/useSubstrateTx'; + +export type LsPoolUnbondTxContext = { + poolId: number; + points: BN; +}; + +const useLsPoolUnbondTx = () => { + const substrateTxFactory: SubstrateTxFactory = + useCallback(async (api, activeSubstrateAddress, { poolId, points }) => { + return api.tx.lst.unbond({ Id: activeSubstrateAddress }, poolId, points); + }, []); + + // TODO: Add EVM support once precompile(s) for the `lst` pallet are implemented on Tangle. + return useSubstrateTxWithNotification( + TxName.LS_TANGLE_POOL_UNBOND, + substrateTxFactory, + ); +}; + +export default useLsPoolUnbondTx; diff --git a/apps/tangle-dapp/hooks/useTxNotification.tsx b/apps/tangle-dapp/hooks/useTxNotification.tsx index 6c05ff185..f4b5e522a 100644 --- a/apps/tangle-dapp/hooks/useTxNotification.tsx +++ b/apps/tangle-dapp/hooks/useTxNotification.tsx @@ -37,6 +37,7 @@ const SUCCESS_MESSAGES: Record = { [TxName.LS_LIQUIFIER_UNLOCK]: 'Liquifier unlock successful', [TxName.LS_LIQUIFIER_WITHDRAW]: 'Liquifier withdrawal successful', [TxName.LS_TANGLE_POOL_JOIN]: 'Joined liquid staking pool', + [TxName.LS_TANGLE_POOL_UNBOND]: 'Unbonded from liquid staking pool', }; const makeKey = (txName: TxName): `${TxName}-tx-notification` => diff --git a/apps/tangle-dapp/utils/Optional.ts b/apps/tangle-dapp/utils/Optional.ts index ac952ecb4..00ebd86ef 100644 --- a/apps/tangle-dapp/utils/Optional.ts +++ b/apps/tangle-dapp/utils/Optional.ts @@ -19,9 +19,9 @@ * * @example * ``` - * const value = new Optional(42); + * const valueOpt = Optional.new(42); * - * if (value.value !== null) { + * if (valueOpt.value !== null) { * console.log(value.value); * } else { * console.log('Value is not present!'); @@ -29,6 +29,14 @@ * ``` */ class Optional> { + static empty>(): Optional { + return new Optional(); + } + + static new>(value: T): Optional { + return new Optional(value); + } + readonly value: T | null; constructor(value?: T) { @@ -37,10 +45,18 @@ class Optional> { map>(f: (value: T) => U): Optional { if (this.value === null) { - return new Optional(); + return Optional.empty(); + } + + return Optional.new(f(this.value)); + } + + unwrapOrThrow(): T { + if (this.value === null) { + throw new Error('Value is not present!'); } - return new Optional(f(this.value)); + return this.value; } get isPresent(): boolean { From 941fb7c90e2f0f8700c37828ca9fb37306332b95 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 03:44:28 -0400 Subject: [PATCH 26/54] ci(tangle-dapp): Fix lint errors --- .../constants/liquidStaking/types.ts | 17 +++++++---------- .../liquidStaking/adapters/tangleMainnet.tsx | 10 ++-------- .../liquidStaking/adapters/tangleTestnet.tsx | 13 +++---------- .../utils/liquidStaking/getLsTangleNetwork.ts | 2 +- 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 8058e4466..9e1bb6d02 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -4,6 +4,12 @@ import { } from '@polkadot/types/lookup'; import { BN } from '@polkadot/util'; import { HexString } from '@polkadot/util/types'; +import { + TANGLE_LOCAL_DEV_NETWORK, + TANGLE_MAINNET_NETWORK, + TANGLE_TESTNET_NATIVE_NETWORK, +} from '@webb-tools/webb-ui-components/constants/networks'; +import { Network as TangleNetwork } from '@webb-tools/webb-ui-components/constants/networks'; import { LsNetworkEntityAdapter, @@ -11,12 +17,6 @@ import { } from '../../data/liquidStaking/adapter'; import { SubstrateAddress } from '../../types/utils'; import { CrossChainTimeUnit } from '../../utils/CrossChainTime'; -import { - TANGLE_LOCAL_DEV_NETWORK, - TANGLE_MAINNET_NETWORK, - TANGLE_TESTNET_NATIVE_NETWORK, - Network as TangleNetwork, -} from '../../../../libs/webb-ui-components/src/constants/networks'; export enum LsProtocolId { POLKADOT, @@ -99,10 +99,7 @@ export interface LsTangleNetworkDef extends ProtocolDefCommon { | LsNetworkId.TANGLE_MAINNET | LsNetworkId.TANGLE_TESTNET | LsNetworkId.TANGLE_LOCAL; - id: - | LsProtocolId.TANGLE_MAINNET - | LsProtocolId.TANGLE_TESTNET - | LsProtocolId.TANGLE_LOCAL; + id: LsTangleNetworkId; token: LsToken.TNT | LsToken.TTNT; rpcEndpoint: string; ss58Prefix: diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx index 8b61eb3e9..6e908966a 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleMainnet.tsx @@ -1,10 +1,6 @@ import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; -import { - TANGLE_LOCAL_DEV_NETWORK, - TANGLE_MAINNET_NETWORK, -} from '@webb-tools/webb-ui-components/constants/networks'; +import { TANGLE_MAINNET_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; -import { IS_PRODUCTION_ENV } from '../../../constants/env'; import { LsNetworkId, LsProtocolId, @@ -20,9 +16,7 @@ const TANGLE_MAINNET = { token: LsToken.TNT, chainIconFileName: 'tangle', decimals: TANGLE_TOKEN_DECIMALS, - rpcEndpoint: IS_PRODUCTION_ENV - ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint - : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, + rpcEndpoint: TANGLE_MAINNET_NETWORK.wsRpcEndpoint, timeUnit: CrossChainTimeUnit.POLKADOT_ERA, unstakingPeriod: 14, ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx index 1e9a8f258..01fb3e36c 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/tangleTestnet.tsx @@ -1,11 +1,6 @@ import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config'; -import { - TANGLE_LOCAL_DEV_NETWORK, - TANGLE_MAINNET_NETWORK, - TANGLE_TESTNET_NATIVE_NETWORK, -} from '@webb-tools/webb-ui-components/constants/networks'; +import { TANGLE_TESTNET_NATIVE_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; -import { IS_PRODUCTION_ENV } from '../../../constants/env'; import { LsNetworkId, LsProtocolId, @@ -21,12 +16,10 @@ const TANGLE_TESTNET = { token: LsToken.TNT, chainIconFileName: 'tangle', decimals: TANGLE_TOKEN_DECIMALS, - rpcEndpoint: IS_PRODUCTION_ENV - ? TANGLE_MAINNET_NETWORK.wsRpcEndpoint - : TANGLE_LOCAL_DEV_NETWORK.wsRpcEndpoint, + rpcEndpoint: TANGLE_TESTNET_NATIVE_NETWORK.wsRpcEndpoint, timeUnit: CrossChainTimeUnit.POLKADOT_ERA, unstakingPeriod: 14, - ss58Prefix: TANGLE_MAINNET_NETWORK.ss58Prefix, + ss58Prefix: TANGLE_TESTNET_NATIVE_NETWORK.ss58Prefix, tangleNetwork: TANGLE_TESTNET_NATIVE_NETWORK, } as const satisfies LsTangleNetworkDef; diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts index e0926314c..6d0924c7b 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts @@ -7,8 +7,8 @@ import { TANGLE_TESTNET_NATIVE_NETWORK, } from '@webb-tools/webb-ui-components/constants/networks'; -import { LsNetworkId } from '../../constants/liquidStaking/types'; import { IS_PRODUCTION_ENV } from '../../constants/env'; +import { LsNetworkId } from '../../constants/liquidStaking/types'; // TODO: Obtain the Tangle network directly from the adapter's `tangleNetwork` property instead of using this helper method. const getLsTangleNetwork = (networkId: LsNetworkId): Network | null => { From 4669f026107f5b9916ca2cce9e186764f84986be Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 04:04:30 -0400 Subject: [PATCH 27/54] fix(tangle-dapp): Use `switchNetwork` instead of `setNetwork` --- .../stakeAndUnstake/LsStakeCard.tsx | 10 ++++---- .../stakeAndUnstake/useLsChangeNetwork.ts | 24 ++++--------------- apps/tangle-dapp/hooks/useNetworkSwitcher.ts | 6 +++-- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index cda7cb3c0..35437dba8 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -76,6 +76,11 @@ const LsStakeCard: FC = () => { const selectedProtocol = getLsProtocolDef(selectedProtocolId); const tryChangeNetwork = useLsChangeNetwork(); + const isTangleNetwork = + selectedNetworkId === LsNetworkId.TANGLE_LOCAL || + selectedNetworkId === LsNetworkId.TANGLE_MAINNET || + selectedNetworkId === LsNetworkId.TANGLE_TESTNET; + // TODO: Not loading the correct protocol for: '?amount=123000000000000000000&protocol=7&network=1&action=stake'. When network=1, it switches to protocol=5 on load. Could this be because the protocol is reset to its default once the network is switched? useSearchParamSync({ key: LsSearchParamKey.PROTOCOL_ID, @@ -102,11 +107,6 @@ const LsStakeCard: FC = () => { const exchangeRate = exchangeRateOrError instanceof Error ? null : exchangeRateOrError; - const isTangleNetwork = - selectedNetworkId === LsNetworkId.TANGLE_LOCAL || - selectedNetworkId === LsNetworkId.TANGLE_MAINNET || - selectedNetworkId === LsNetworkId.TANGLE_TESTNET; - const handleStakeClick = useCallback(async () => { // Not ready yet; no amount given. if (fromAmount === null) { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts index e40f30c8b..f467cc853 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts @@ -4,16 +4,15 @@ import { useCallback } from 'react'; import { LsNetworkId } from '../../../constants/liquidStaking/types'; import { NETWORK_FEATURE_MAP } from '../../../constants/networks'; -import useNetworkStore from '../../../context/useNetworkStore'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; +import useNetworkSwitcher from '../../../hooks/useNetworkSwitcher'; import { NetworkFeature } from '../../../types'; import getLsNetwork from '../../../utils/liquidStaking/getLsNetwork'; import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; -import testRpcEndpointConnection from '../../NetworkSelector/testRpcEndpointConnection'; const useLsChangeNetwork = () => { const { selectedNetworkId, setSelectedNetworkId } = useLsStore(); - const { setNetwork } = useNetworkStore(); + const { switchNetwork } = useNetworkSwitcher(); const { notificationApi } = useWebbUI(); const tryChangeNetwork = useCallback( @@ -48,24 +47,11 @@ const useLsChangeNetwork = () => { return; } - // Try connecting to the new network. - const isRpcUp = await testRpcEndpointConnection( - tangleNetwork.wsRpcEndpoint, - ); - - if (!isRpcUp) { - notificationApi({ - message: 'Failed to connect to the network', - variant: 'error', - }); - - return; + if (await switchNetwork(tangleNetwork, false)) { + setSelectedNetworkId(newNetworkId); } - - setSelectedNetworkId(newNetworkId); - setNetwork(tangleNetwork); }, - [notificationApi, selectedNetworkId, setNetwork, setSelectedNetworkId], + [notificationApi, selectedNetworkId, setSelectedNetworkId, switchNetwork], ); return tryChangeNetwork; diff --git a/apps/tangle-dapp/hooks/useNetworkSwitcher.ts b/apps/tangle-dapp/hooks/useNetworkSwitcher.ts index 9d79e26af..8d0516081 100644 --- a/apps/tangle-dapp/hooks/useNetworkSwitcher.ts +++ b/apps/tangle-dapp/hooks/useNetworkSwitcher.ts @@ -75,7 +75,7 @@ const useNetworkSwitcher = () => { async (newNetwork: Network, isCustom: boolean) => { // Already on the requested network. if (network.id === newNetwork.id) { - return; + return true; } // Test connection to the new network. else if (!(await testRpcEndpointConnection(newNetwork.wsRpcEndpoint))) { @@ -84,7 +84,7 @@ const useNetworkSwitcher = () => { message: `Unable to connect to the requested network: ${newNetwork.wsRpcEndpoint}`, }); - return; + return false; } if (activeWallet !== undefined) { @@ -122,6 +122,8 @@ const useNetworkSwitcher = () => { setIsCustom(isCustom); setNetwork(newNetwork); + + return true; }, [ activeWallet, From 8f56acf5731885f48af9e53c9901c2a386d6b5e4 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 04:10:46 -0400 Subject: [PATCH 28/54] fix(tangle-dapp): Fix network mismatch on load bug --- apps/tangle-dapp/app/liquid-staking/page.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index d6812958a..3f98607a4 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { LsValidatorTable } from '../../components/LiquidStaking/LsValidatorTable'; import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; @@ -8,8 +8,11 @@ import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnst import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; import LsPoolsTable from '../../containers/LsPoolsTable'; +import useNetworkStore from '../../context/useNetworkStore'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; +import useNetworkSwitcher from '../../hooks/useNetworkSwitcher'; import useSearchParamState from '../../hooks/useSearchParamState'; +import getLsTangleNetwork from '../../utils/liquidStaking/getLsTangleNetwork'; import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId'; import TabListItem from '../restake/TabListItem'; import TabsList from '../restake/TabsList'; @@ -28,10 +31,22 @@ const LiquidStakingTokenPage: FC = () => { value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, }); - const { selectedProtocolId } = useLsStore(); + const { selectedProtocolId, selectedNetworkId } = useLsStore(); + const { network } = useNetworkStore(); + const { switchNetwork } = useNetworkSwitcher(); + const lsTangleNetwork = getLsTangleNetwork(selectedNetworkId); const isParachainChain = isLsParachainChainId(selectedProtocolId); + // Sync the network with the selected liquid staking network on load. + // It might differ initially if the user navigates to the page and + // the active network differs from the default liquid staking network. + useEffect(() => { + if (lsTangleNetwork !== null && lsTangleNetwork.id !== network.id) { + switchNetwork(lsTangleNetwork, false); + } + }, [lsTangleNetwork, network.id, selectedNetworkId, switchNetwork]); + return (
    From fff064d0e7e45a3b62e1cd2e08fd5e57274d6818 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 04:44:17 -0400 Subject: [PATCH 29/54] refactor(tangle-dapp): Get rid of `TotalDetailItem` component --- .../stakeAndUnstake/FeeDetailItem.tsx | 6 +- .../stakeAndUnstake/LsStakeCard.tsx | 25 ++--- .../stakeAndUnstake/LsUnstakeCard.tsx | 25 ++--- .../stakeAndUnstake/TotalDetailItem.tsx | 92 ------------------- .../stakeAndUnstake/useLsFeePercentage.ts | 4 +- 5 files changed, 33 insertions(+), 119 deletions(-) delete mode 100644 apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx index 7e1488ae7..f90b8bcfb 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx @@ -10,17 +10,17 @@ import DetailItem from './DetailItem'; import useLsFeePercentage from './useLsFeePercentage'; export type FeeDetailItemProps = { - isMinting: boolean; + isStaking: boolean; inputAmount: BN | null; protocolId: LsProtocolId; }; const FeeDetailItem: FC = ({ - isMinting, + isStaking, inputAmount, protocolId, }) => { - const feePercentage = useLsFeePercentage(protocolId, isMinting); + const feePercentage = useLsFeePercentage(protocolId, isStaking); const protocol = getLsProtocolDef(protocolId); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 35437dba8..0d219a35a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -34,14 +34,15 @@ import useSearchParamState from '../../../hooks/useSearchParamState'; import useSearchParamSync from '../../../hooks/useSearchParamSync'; import { TxStatus } from '../../../hooks/useSubstrateTx'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; +import scaleAmountByPercentage from '../../../utils/scaleAmountByPercentage'; import ExchangeRateDetailItem from './ExchangeRateDetailItem'; import FeeDetailItem from './FeeDetailItem'; import LsAgnosticBalance from './LsAgnosticBalance'; import LsFeeWarning from './LsFeeWarning'; import LsInput from './LsInput'; -import TotalDetailItem from './TotalDetailItem'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; +import useLsFeePercentage from './useLsFeePercentage'; import useLsSpendingLimits from './useLsSpendingLimits'; const LsStakeCard: FC = () => { @@ -146,13 +147,21 @@ const LsStakeCard: FC = () => { selectedPoolId, ]); + const feePercentage = useLsFeePercentage(selectedProtocolId, true); + const toAmount = useMemo(() => { - if (fromAmount === null || exchangeRate === null) { + if ( + fromAmount === null || + exchangeRate === null || + typeof feePercentage !== 'number' + ) { return null; } - return fromAmount.muln(exchangeRate); - }, [fromAmount, exchangeRate]); + const feeAmount = scaleAmountByPercentage(fromAmount, feePercentage); + + return fromAmount.muln(exchangeRate).sub(feeAmount); + }, [fromAmount, exchangeRate, feePercentage]); const canCallStake = (fromAmount !== null && @@ -224,15 +233,9 @@ const LsStakeCard: FC = () => { - -
    diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index 2afd1a6b2..52bb6cb04 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -27,15 +27,16 @@ import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamSync from '../../../hooks/useSearchParamSync'; import { TxStatus } from '../../../hooks/useSubstrateTx'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; +import scaleAmountByPercentage from '../../../utils/scaleAmountByPercentage'; import ExchangeRateDetailItem from './ExchangeRateDetailItem'; import FeeDetailItem from './FeeDetailItem'; import LsAgnosticBalance from './LsAgnosticBalance'; import LsFeeWarning from './LsFeeWarning'; import LsInput from './LsInput'; import SelectTokenModal from './SelectTokenModal'; -import TotalDetailItem from './TotalDetailItem'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; +import useLsFeePercentage from './useLsFeePercentage'; import useLsSpendingLimits from './useLsSpendingLimits'; const LsUnstakeCard: FC = () => { @@ -138,13 +139,21 @@ const LsUnstakeCard: FC = () => { selectedProtocol, ]); + const feePercentage = useLsFeePercentage(selectedProtocolId, false); + const toAmount = useMemo(() => { - if (fromAmount === null || exchangeRate === null) { + if ( + fromAmount === null || + exchangeRate === null || + typeof feePercentage !== 'number' + ) { return null; } - return fromAmount.muln(exchangeRate); - }, [exchangeRate, fromAmount]); + const feeAmount = scaleAmountByPercentage(fromAmount, feePercentage); + + return fromAmount.divn(exchangeRate).sub(feeAmount); + }, [exchangeRate, feePercentage, fromAmount]); const handleTokenSelect = useCallback(() => { setIsSelectTokenModalOpen(false); @@ -225,13 +234,7 @@ const LsUnstakeCard: FC = () => { - -
    diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx deleted file mode 100644 index 86a19cff2..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TotalDetailItem.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { BN } from '@polkadot/util'; -import { FC, useMemo } from 'react'; - -import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; -import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; -import { LsProtocolId } from '../../../constants/liquidStaking/types'; -import useLsExchangeRate, { - ExchangeRateType, -} from '../../../data/liquidStaking/useLsExchangeRate'; -import formatBn from '../../../utils/formatBn'; -import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; -import scaleAmountByPercentage from '../../../utils/scaleAmountByPercentage'; -import DetailItem from './DetailItem'; -import useLsFeePercentage from './useLsFeePercentage'; - -export type TotalDetailItemProps = { - isMinting: boolean; - inputAmount: BN | null; - protocolId: LsProtocolId; -}; - -const TotalDetailItem: FC = ({ - isMinting, - inputAmount, - protocolId, -}) => { - const feePercentage = useLsFeePercentage(protocolId, isMinting); - - const { exchangeRate } = useLsExchangeRate( - isMinting - ? ExchangeRateType.NativeToDerivative - : ExchangeRateType.DerivativeToNative, - ); - - const protocol = getLsProtocolDef(protocolId); - - const totalAmount = useMemo(() => { - if ( - inputAmount === null || - exchangeRate === null || - feePercentage === null - ) { - return null; - } - // Propagate errors. - else if (feePercentage instanceof Error) { - return feePercentage; - } else if (exchangeRate instanceof Error) { - return exchangeRate; - } - - const feeAmount = scaleAmountByPercentage(inputAmount, feePercentage); - - return isMinting - ? inputAmount.muln(exchangeRate).sub(feeAmount) - : inputAmount.divn(exchangeRate).sub(feeAmount); - }, [exchangeRate, feePercentage, inputAmount, isMinting]); - - const formattedTotalAmount = useMemo(() => { - // Nothing to show if the input amount is not set. - if (inputAmount === null) { - return EMPTY_VALUE_PLACEHOLDER; - } - // Propagate error or loading state. - else if (!(totalAmount instanceof BN)) { - return totalAmount; - } - - return formatBn(totalAmount, protocol.decimals, { - includeCommas: true, - }); - }, [inputAmount, totalAmount, protocol.decimals]); - - const token = isMinting - ? `${LS_DERIVATIVE_TOKEN_PREFIX}${protocol.token}` - : protocol.token; - - const value = - typeof formattedTotalAmount === 'string' - ? `${formattedTotalAmount} ${token}` - : formattedTotalAmount; - - return ( - - ); -}; - -export default TotalDetailItem; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts index db26b4d05..0a4691201 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts @@ -13,7 +13,7 @@ import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; const useLsFeePercentage = ( protocolId: LsProtocolId, - isMinting: boolean, + isStaking: boolean, ): number | Error | null => { const { result: parachainFees } = useParachainLsFees(); @@ -22,7 +22,7 @@ const useLsFeePercentage = ( const parachainFee = parachainFees === null ? null - : isMinting + : isStaking ? parachainFees.mintFeePercentage : parachainFees.redeemFeePercentage; From 75d118c0f4ecf1f91b4645a1a7b2281e4ef1f10b Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 21:56:20 -0400 Subject: [PATCH 30/54] refactor(tangle-dapp): Start page merge process --- .../app/liquid-staking/overview/page.tsx | 5 +- .../LiquidStaking/VaultsAndAssetsTable.tsx | 190 ----------------- .../constants/liquidStaking/types.ts | 3 +- apps/tangle-dapp/containers/LsPoolsTable.tsx | 12 -- .../LsPoolsTable2/LsPoolsTable2.tsx | 159 +++++++++++++++ .../LsPoolsTable2/LsProtocolsTable.tsx | 191 ++++++++++++++++++ .../containers/LsPoolsTable2/index.ts | 1 + .../data/liquidStaking/useLsPools.ts | 24 +-- .../components/VaultsTable/AssetsTable.tsx | 78 ------- .../components/VaultsTable/VaultsTable.tsx | 92 --------- .../src/components/VaultsTable/index.ts | 1 - .../src/components/VaultsTable/types.ts | 16 -- .../src/components/index.ts | 2 +- 13 files changed, 368 insertions(+), 406 deletions(-) delete mode 100644 apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx create mode 100644 apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx create mode 100644 apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx create mode 100644 apps/tangle-dapp/containers/LsPoolsTable2/index.ts delete mode 100644 libs/webb-ui-components/src/components/VaultsTable/AssetsTable.tsx delete mode 100644 libs/webb-ui-components/src/components/VaultsTable/VaultsTable.tsx delete mode 100644 libs/webb-ui-components/src/components/VaultsTable/index.ts delete mode 100644 libs/webb-ui-components/src/components/VaultsTable/types.ts diff --git a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx b/apps/tangle-dapp/app/liquid-staking/overview/page.tsx index ec2d8b4db..fac3a614f 100644 --- a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/overview/page.tsx @@ -1,7 +1,6 @@ -import { Typography } from '@webb-tools/webb-ui-components'; +import { LsProtocolsTable, Typography } from '@webb-tools/webb-ui-components'; import { FC } from 'react'; -import VaultsAndAssetsTable from '../../../components/LiquidStaking/VaultsAndAssetsTable'; import StatItem from '../../../components/StatItem'; const LiquidStakingPage: FC = () => { @@ -28,7 +27,7 @@ const LiquidStakingPage: FC = () => {
    - +
    ); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx deleted file mode 100644 index acdfa4e78..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx +++ /dev/null @@ -1,190 +0,0 @@ -'use client'; - -import { createColumnHelper } from '@tanstack/react-table'; -import { ChevronUp } from '@webb-tools/icons'; -import { - Button, - getRoundedAmountString, - Typography, - VaultsTable, -} from '@webb-tools/webb-ui-components'; -import Link from 'next/link'; -import { twMerge } from 'tailwind-merge'; - -import useVaults from '../../app/liquid-staking/useVaults'; -import StatItem from '../../components/StatItem'; -import TableCellWrapper from '../../components/tables/TableCellWrapper'; -import { PagePath } from '../../types'; -import { Asset, Vault } from '../../types/liquidStaking'; -import LsTokenIcon from '../LsTokenIcon'; - -const vaultColumnHelper = createColumnHelper(); -const assetsColumnHelper = createColumnHelper(); - -const vaultColumns = [ - vaultColumnHelper.accessor('name', { - header: () => 'Token', - cell: (props) => ( - -
    - - - {props.getValue()} - -
    -
    - ), - sortingFn: (rowA, rowB) => { - // NOTE: the sorting is reversed by default - return rowB.original.name.localeCompare(rowA.original.name); - }, - sortDescFirst: true, - }), - vaultColumnHelper.accessor('tvl', { - header: () => 'TVL', - cell: (props) => ( - - - - ), - }), - vaultColumnHelper.accessor('derivativeTokens', { - header: () => 'Derivative Tokens', - cell: (props) => ( - - - - ), - }), - vaultColumnHelper.accessor('myStake', { - header: () => 'My Stake', - cell: (props) => ( - - - - ), - }), - vaultColumnHelper.accessor('assets', { - header: () => null, - cell: ({ row }) => ( - -
    - - - - - -
    -
    - ), - enableSorting: false, - }), -]; - -const assetsColumns = [ - assetsColumnHelper.accessor('id', { - header: () => 'Asset ID', - cell: (props) => ( - - {props.getValue()} - - ), - }), - assetsColumnHelper.accessor('token', { - header: () => 'Token', - cell: (props) => ( - - {props.getValue()} - - ), - }), - assetsColumnHelper.accessor('tvl', { - header: () => 'TVL', - cell: (props) => ( - - {getRoundedAmountString(props.getValue())} - - ), - }), - assetsColumnHelper.accessor('apy', { - header: () => 'APY', - cell: (props) => ( - - {getRoundedAmountString(props.getValue()) + '%'} - - ), - }), - assetsColumnHelper.accessor('myStake', { - header: () => 'My Stake', - cell: (props) => ( - - {getRoundedAmountString(props.getValue())} - - ), - }), -]; - -const VaultsAndAssetsTable = () => { - const vaults = useVaults(); - - return ( - - ); -}; - -export default VaultsAndAssetsTable; diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 9e1bb6d02..2fb564e0c 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -189,7 +189,8 @@ export type LsPool = { id: number; metadata?: string; ownerAddress?: SubstrateAddress; - ownerStake?: BN; + nominatorAddress?: SubstrateAddress; + bouncerAddress?: SubstrateAddress; validators: SubstrateAddress[]; totalStaked: BN; apyPercentage?: number; diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable.tsx index 03e54bbdf..2f9319bd6 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable.tsx @@ -98,18 +98,6 @@ const COLUMNS = [ ), }), - COLUMN_HELPER.accessor('ownerStake', { - header: () => "Owner's Stake", - cell: (props) => { - const ownerStake = props.getValue(); - - if (ownerStake === undefined) { - return EMPTY_VALUE_PLACEHOLDER; - } - - return ; - }, - }), COLUMN_HELPER.accessor('totalStaked', { header: () => 'Total Staked (TVL)', cell: (props) => ( diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx new file mode 100644 index 000000000..96a018925 --- /dev/null +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { useState, useMemo, FC } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + SortingState, + getPaginationRowModel, + createColumnHelper, +} from '@tanstack/react-table'; +import { Table } from '../../../../libs/webb-ui-components/src/components/Table'; +import { Pagination } from '../../../../libs/webb-ui-components/src/components/Pagination'; +import { twMerge } from 'tailwind-merge'; +import { LsPool } from '../../constants/liquidStaking/types'; +import { + Button, + getRoundedAmountString, + Typography, +} from '@webb-tools/webb-ui-components'; +import TokenAmountCell from '../../components/tableCells/TokenAmountCell'; +import pluralize from '../../utils/pluralize'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; +import { ArrowRight } from '@webb-tools/icons'; + +export interface LsPoolsTable2Props { + pools: LsPool[]; + isShown: boolean; +} + +const POOL_COLUMN_HELPER = createColumnHelper(); + +const POOL_COLUMNS = [ + POOL_COLUMN_HELPER.accessor('id', { + header: () => 'Name/id', + cell: (props) => ( + + {props.row.original.metadata}#{props.getValue()} + + ), + }), + POOL_COLUMN_HELPER.accessor('token', { + header: () => 'Token', + cell: (props) => ( + + {props.getValue()} + + ), + }), + POOL_COLUMN_HELPER.accessor('totalStaked', { + header: () => 'TVL', + // TODO: Decimals. + cell: (props) => , + }), + POOL_COLUMN_HELPER.accessor('apyPercentage', { + header: () => 'APY', + cell: (props) => { + const apy = props.getValue(); + + if (apy === undefined) { + return EMPTY_VALUE_PLACEHOLDER; + } + + return ( + + {getRoundedAmountString(props.getValue()) + '%'} + + ); + }, + }), + POOL_COLUMN_HELPER.display({ + id: 'actions', + header: () => 'Actions', + cell: (props) => ( +
    + +
    + ), + }), +]; + +const LsPoolsTable2: FC = ({ pools, isShown }) => { + const [sorting, setSorting] = useState([]); + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: 5, + }); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize], + ); + + const table = useReactTable({ + data: pools, + columns: POOL_COLUMNS, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + onPaginationChange: setPagination, + state: { + sorting, + pagination, + }, + autoResetPageIndex: false, + enableSortingRemoval: false, + }); + + return ( +
    +
+ + 1)} + className="border-t-0 py-5" + /> + + ); +}; + +export default LsPoolsTable2; diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx new file mode 100644 index 000000000..deb583576 --- /dev/null +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { + useReactTable, + getCoreRowModel, + getExpandedRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + Row, + createColumnHelper, +} from '@tanstack/react-table'; +import { Table } from '../../../../libs/webb-ui-components/src/components/Table'; +import { Typography } from '../../../../libs/webb-ui-components/src/typography'; +import { twMerge } from 'tailwind-merge'; +import LsPoolsTable2 from './LsPoolsTable2'; +import TableCellWrapper from '../../components/tables/TableCellWrapper'; +import LsTokenIcon from '../../components/LsTokenIcon'; +import StatItem from '../../components/StatItem'; +import { Button, getRoundedAmountString } from '@webb-tools/webb-ui-components'; +import { ChevronUp } from '@webb-tools/icons'; +import { LsPool } from '../../constants/liquidStaking/types'; +import { BN } from '@polkadot/util'; +import pluralize from '../../utils/pluralize'; + +export interface LsProtocolsTableProps { + initialSorting?: SortingState; +} + +export type LsProtocolRow = { + name: string; + tvl: number; + tvlInUsd: number; + iconName: string; + pools: LsPool[]; +}; + +const PROTOCOL_COLUMN_HELPER = createColumnHelper(); + +const PROTOCOL_COLUMNS = [ + PROTOCOL_COLUMN_HELPER.accessor('name', { + header: () => 'Token', + cell: (props) => ( + +
+ + + + {props.getValue()} + +
+
+ ), + sortingFn: (rowA, rowB) => { + // NOTE: The sorting is reversed by default. + return rowB.original.name.localeCompare(rowA.original.name); + }, + sortDescFirst: true, + }), + PROTOCOL_COLUMN_HELPER.accessor('tvl', { + header: () => 'Total Staked (TVL)', + cell: (props) => ( + + + + ), + }), + PROTOCOL_COLUMN_HELPER.accessor('pools', { + header: () => 'Pools', + cell: (props) => { + const length = props.getValue().length; + + return ( + + 1)} + removeBorder + /> + + ); + }, + }), + PROTOCOL_COLUMN_HELPER.display({ + id: 'expand/collapse', + header: () => null, + cell: ({ row }) => ( + +
+ +
+
+ ), + enableSorting: false, + }), +]; + +function LsProtocolsTable({ initialSorting = [] }: LsProtocolsTableProps) { + const [sorting, setSorting] = useState(initialSorting); + + const getExpandedRowContent = useCallback( + (row: Row) => ( +
+ +
+ ), + [], + ); + + const rows: LsProtocolRow[] = [ + { + name: 'Tangle', + iconName: 'tangle', + pools: [ + { + id: 1, + totalStaked: new BN(4234932942394239), + validators: [], + metadata: 'test', + }, + ], + tvl: 123.01, + tvlInUsd: 123.01, + }, + ]; + + const table = useReactTable({ + data: rows, + columns: PROTOCOL_COLUMNS, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + state: { + sorting, + }, + autoResetPageIndex: false, + enableSortingRemoval: false, + }); + + const onRowClick = useCallback( + (row: Row) => { + table.setExpanded({ [row.id]: !row.getIsExpanded() }); + }, + [table], + ); + + return ( +
+ + Liquid Staking Protocols + + +
+ + ); +} + +export default LsProtocolsTable; diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/index.ts b/apps/tangle-dapp/containers/LsPoolsTable2/index.ts new file mode 100644 index 000000000..87c1cc582 --- /dev/null +++ b/apps/tangle-dapp/containers/LsPoolsTable2/index.ts @@ -0,0 +1 @@ +export { default as LsProtocolsTable } from './LsProtocolsTable'; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 6630f068d..3f2585542 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -1,4 +1,4 @@ -import { BN, BN_ZERO, u8aToString } from '@polkadot/util'; +import { BN_ZERO, u8aToString } from '@polkadot/util'; import { useCallback, useMemo } from 'react'; import { LsPool } from '../../constants/liquidStaking/types'; @@ -57,21 +57,20 @@ const useLsPools = (): Map | null | Error => { ? undefined : u8aToString(metadataEntryBytes); - // Root role can be `None` if its roles are updated, and the root - // role is removed. + // Roles can be `None` if updated and removed. const ownerAddress = tanglePool.roles.root.isNone ? undefined : assertSubstrateAddress(tanglePool.roles.root.unwrap().toString()); - let ownerStake: BN | undefined = undefined; + const nominatorAddress = tanglePool.roles.nominator.isNone + ? undefined + : assertSubstrateAddress( + tanglePool.roles.nominator.unwrap().toString(), + ); - if (ownerAddress !== undefined && poolMembers !== null) { - ownerStake = poolMembers - .find(([id, memberAddress]) => { - return id === poolId && memberAddress === ownerAddress; - })?.[2] - .balance.toBn(); - } + const bouncerAddress = tanglePool.roles.bouncer.isNone + ? undefined + : assertSubstrateAddress(tanglePool.roles.bouncer.unwrap().toString()); const memberBalances = poolMembers?.filter(([id]) => { return id === poolId; @@ -96,10 +95,11 @@ const useLsPools = (): Map | null | Error => { id: poolId, metadata, ownerAddress, + nominatorAddress, + bouncerAddress, commissionPercentage, validators, totalStaked, - ownerStake, apyPercentage, }; diff --git a/libs/webb-ui-components/src/components/VaultsTable/AssetsTable.tsx b/libs/webb-ui-components/src/components/VaultsTable/AssetsTable.tsx deleted file mode 100644 index 1ddb73de8..000000000 --- a/libs/webb-ui-components/src/components/VaultsTable/AssetsTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client'; - -import { useState, useMemo } from 'react'; -import { - useReactTable, - getCoreRowModel, - getSortedRowModel, - SortingState, - getPaginationRowModel, -} from '@tanstack/react-table'; -import { Table } from '../Table'; -import { Pagination } from '../Pagination'; -import { twMerge } from 'tailwind-merge'; -import { AssetsTableProps } from './types'; - -function AssetsTable({ data, columns, isShown }: AssetsTableProps) { - const [sorting, setSorting] = useState([]); - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: 5, - }); - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize], - ); - - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onSortingChange: setSorting, - onPaginationChange: setPagination, - state: { - sorting, - pagination, - }, - autoResetPageIndex: false, - enableSortingRemoval: false, - }); - - return ( -
-
- - - - ); -} - -export default AssetsTable; diff --git a/libs/webb-ui-components/src/components/VaultsTable/VaultsTable.tsx b/libs/webb-ui-components/src/components/VaultsTable/VaultsTable.tsx deleted file mode 100644 index 0ad853d95..000000000 --- a/libs/webb-ui-components/src/components/VaultsTable/VaultsTable.tsx +++ /dev/null @@ -1,92 +0,0 @@ -'use client'; - -import { useState, useCallback } from 'react'; -import { - useReactTable, - getCoreRowModel, - getExpandedRowModel, - getPaginationRowModel, - getSortedRowModel, - SortingState, - Row, -} from '@tanstack/react-table'; -import { Table } from '../Table'; -import { Typography } from '../../typography'; -import { twMerge } from 'tailwind-merge'; -import AssetsTable from './AssetsTable'; -import { VaultsTableProps } from './types'; - -function VaultsTable({ - vaultsData, - vaultsColumns, - assetsColumns, - title, - initialSorting = [], - isPaginated = true, -}: VaultsTableProps) { - const [sorting, setSorting] = useState(initialSorting); - - const getExpandedRowContent = useCallback( - (row: Row) => ( -
- -
- ), - [assetsColumns], - ); - - const table = useReactTable({ - data: vaultsData, - columns: vaultsColumns, - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onSortingChange: setSorting, - state: { - sorting, - }, - autoResetPageIndex: false, - enableSortingRemoval: false, - }); - - const onRowClick = useCallback( - (row: Row) => { - table.setExpanded({ [row.id]: !row.getIsExpanded() }); - }, - [table], - ); - - return ( -
- - {title} - - -
- - ); -} - -export default VaultsTable; diff --git a/libs/webb-ui-components/src/components/VaultsTable/index.ts b/libs/webb-ui-components/src/components/VaultsTable/index.ts deleted file mode 100644 index 72879bfc4..000000000 --- a/libs/webb-ui-components/src/components/VaultsTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as VaultsTable } from './VaultsTable'; diff --git a/libs/webb-ui-components/src/components/VaultsTable/types.ts b/libs/webb-ui-components/src/components/VaultsTable/types.ts deleted file mode 100644 index 3d1202734..000000000 --- a/libs/webb-ui-components/src/components/VaultsTable/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ColumnDef, SortingState } from '@tanstack/react-table'; - -export interface VaultsTableProps { - vaultsData: TVault[]; - vaultsColumns: ColumnDef[]; - assetsColumns: ColumnDef[]; - title: string; - initialSorting?: SortingState; - isPaginated?: boolean; -} - -export interface AssetsTableProps { - data: T[]; - columns: ColumnDef[]; - isShown: boolean; -} diff --git a/libs/webb-ui-components/src/components/index.ts b/libs/webb-ui-components/src/components/index.ts index ac8bdbba9..8161f3c4d 100644 --- a/libs/webb-ui-components/src/components/index.ts +++ b/libs/webb-ui-components/src/components/index.ts @@ -80,7 +80,7 @@ export * from './Tooltip'; export * from './TransactionInputCard'; export * from './TxProgressor'; export { default as TxConfirmationRing } from './TxConfirmationRing'; -export * from './VaultsTable'; +export * from '../../../../apps/tangle-dapp/containers/LsPoolsTable2'; export * from './WalletConnectionCard'; export * from './WalletModal'; export * from './WebsiteFooter'; From 186ef8e855f18c1bb23b345c7688cf510cecda36 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:27:50 -0400 Subject: [PATCH 31/54] feat(tangle-dapp): Create `LsMyPoolsTable` container --- .../app/liquid-staking/overview/page.tsx | 3 + .../constants/liquidStaking/types.ts | 2 + .../tangle-dapp/containers/LsMyPoolsTable.tsx | 217 ++++++++++++++++++ .../LsPoolsTable2/LsPoolsTable2.tsx | 23 +- .../data/liquidStaking/useLsPools.ts | 17 +- apps/tangle-dapp/hooks/useSubstrateAddress.ts | 3 +- apps/tangle-dapp/utils/toSubstrateAddress.ts | 9 +- 7 files changed, 259 insertions(+), 15 deletions(-) create mode 100644 apps/tangle-dapp/containers/LsMyPoolsTable.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx b/apps/tangle-dapp/app/liquid-staking/overview/page.tsx index fac3a614f..76caf58ed 100644 --- a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/overview/page.tsx @@ -2,6 +2,7 @@ import { LsProtocolsTable, Typography } from '@webb-tools/webb-ui-components'; import { FC } from 'react'; import StatItem from '../../../components/StatItem'; +import LsMyPoolsTable from '../../../containers/LsMyPoolsTable'; const LiquidStakingPage: FC = () => { return ( @@ -28,6 +29,8 @@ const LiquidStakingPage: FC = () => { + + ); }; diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 2fb564e0c..034f4ae37 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -1,4 +1,5 @@ import { + PalletAssetsAssetAccount, TanglePrimitivesCurrencyTokenSymbol, TanglePrimitivesTimeUnit, } from '@polkadot/types/lookup'; @@ -195,4 +196,5 @@ export type LsPool = { totalStaked: BN; apyPercentage?: number; commissionPercentage?: number; + members: Map; }; diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx new file mode 100644 index 000000000..7cb3e24f5 --- /dev/null +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -0,0 +1,217 @@ +'use client'; + +import { useState, useMemo, FC } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + SortingState, + getPaginationRowModel, + createColumnHelper, +} from '@tanstack/react-table'; +import { Table } from '../../../libs/webb-ui-components/src/components/Table'; +import { Pagination } from '../../../libs/webb-ui-components/src/components/Pagination'; +import { LsPool } from '../constants/liquidStaking/types'; +import { + ActionsDropdown, + Avatar, + Button, + getRoundedAmountString, + Typography, +} from '@webb-tools/webb-ui-components'; +import TokenAmountCell from '../components/tableCells/TokenAmountCell'; +import pluralize from '../utils/pluralize'; +import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; +import { ArrowRight } from '@webb-tools/icons'; +import useLsPools from '../data/liquidStaking/useLsPools'; +import useSubstrateAddress from '../hooks/useSubstrateAddress'; +import { BN } from '@polkadot/util'; + +type MyLsPoolRow = LsPool & { + myStake: BN; + isRoot: boolean; + isNominator: boolean; + isBouncer: boolean; +}; + +const COLUMN_HELPER = createColumnHelper(); + +const POOL_COLUMNS = [ + COLUMN_HELPER.accessor('id', { + header: () => 'Name/id', + cell: (props) => ( + + {props.row.original.metadata}#{props.getValue()} + + ), + }), + COLUMN_HELPER.accessor('token', { + header: () => 'Token', + cell: (props) => ( + + {props.getValue()} + + ), + }), + COLUMN_HELPER.accessor('ownerAddress', { + header: () => 'Owner', + cell: (props) => ( + + ), + }), + COLUMN_HELPER.accessor('totalStaked', { + header: () => 'TVL', + // TODO: Decimals. + cell: (props) => , + }), + COLUMN_HELPER.accessor('myStake', { + header: () => 'My Stake', + cell: (props) => , + }), + COLUMN_HELPER.accessor('apyPercentage', { + header: () => 'APY', + cell: (props) => { + const apy = props.getValue(); + + if (apy === undefined) { + return EMPTY_VALUE_PLACEHOLDER; + } + + return ( + + {getRoundedAmountString(props.getValue()) + '%'} + + ); + }, + }), + COLUMN_HELPER.display({ + id: 'actions', + header: () => 'Actions', + cell: (props) => ( +
+ + + {/** + * Show management actions if the active user has any role in + * the pool. + */} + {props.row.original.isRoot || + props.row.original.isNominator || + (props.row.original.isBouncer && ( + void 0, + }, + { + label: 'Update Commission', + // TODO: Proper onClick handler. + onClick: () => void 0, + }, + { + label: 'Update Roles', + // TODO: Proper onClick handler. + onClick: () => void 0, + }, + ]} + /> + ))} +
+ ), + }), +]; + +const LsMyPoolsTable: FC = () => { + const substrateAddress = useSubstrateAddress(); + const [sorting, setSorting] = useState([]); + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: 5, + }); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize], + ); + + const lsPoolsMap = useLsPools(); + + const lsPools = + lsPoolsMap instanceof Map ? Array.from(lsPoolsMap.values()) : lsPoolsMap; + + const myPools = + substrateAddress === null || !Array.isArray(lsPools) + ? null + : lsPools.filter((lsPool) => lsPool.members.has(substrateAddress)); + + const table = useReactTable({ + data: myPools ?? [], + columns: POOL_COLUMNS, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + onPaginationChange: setPagination, + state: { + sorting, + pagination, + }, + autoResetPageIndex: false, + enableSortingRemoval: false, + }); + + return ( +
+
+ + 1)} + className="border-t-0 py-5" + /> + + ); +}; + +export default LsMyPoolsTable; diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx index 96a018925..54dbc7fea 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx @@ -14,6 +14,7 @@ import { Pagination } from '../../../../libs/webb-ui-components/src/components/P import { twMerge } from 'tailwind-merge'; import { LsPool } from '../../constants/liquidStaking/types'; import { + Avatar, Button, getRoundedAmountString, Typography, @@ -28,10 +29,10 @@ export interface LsPoolsTable2Props { isShown: boolean; } -const POOL_COLUMN_HELPER = createColumnHelper(); +const COLUMN_HELPER = createColumnHelper(); const POOL_COLUMNS = [ - POOL_COLUMN_HELPER.accessor('id', { + COLUMN_HELPER.accessor('id', { header: () => 'Name/id', cell: (props) => ( ), }), - POOL_COLUMN_HELPER.accessor('token', { + COLUMN_HELPER.accessor('token', { header: () => 'Token', cell: (props) => ( ), }), - POOL_COLUMN_HELPER.accessor('totalStaked', { + COLUMN_HELPER.accessor('ownerAddress', { + header: () => 'Owner', + cell: (props) => ( + + ), + }), + COLUMN_HELPER.accessor('totalStaked', { header: () => 'TVL', // TODO: Decimals. cell: (props) => , }), - POOL_COLUMN_HELPER.accessor('apyPercentage', { + COLUMN_HELPER.accessor('apyPercentage', { header: () => 'APY', cell: (props) => { const apy = props.getValue(); @@ -80,7 +91,7 @@ const POOL_COLUMNS = [ ); }, }), - POOL_COLUMN_HELPER.display({ + COLUMN_HELPER.display({ id: 'actions', header: () => 'Actions', cell: (props) => ( diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 3f2585542..edc6496a6 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -39,6 +39,7 @@ const useLsPools = (): Map | null | Error => { bondedPools === null || poolNominations === null || compoundApys === null || + poolMembers === null || !isSupported ) { return null; @@ -72,14 +73,13 @@ const useLsPools = (): Map | null | Error => { ? undefined : assertSubstrateAddress(tanglePool.roles.bouncer.unwrap().toString()); - const memberBalances = poolMembers?.filter(([id]) => { + const memberBalances = poolMembers.filter(([id]) => { return id === poolId; }); - const totalStaked = - memberBalances?.reduce((acc, [, , account]) => { - return acc.add(account.balance.toBn()); - }, BN_ZERO) ?? BN_ZERO; + const totalStaked = memberBalances.reduce((acc, [, , account]) => { + return acc.add(account.balance.toBn()); + }, BN_ZERO); const commissionPercentage = tanglePool.commission.current.isNone ? undefined @@ -91,6 +91,12 @@ const useLsPools = (): Map | null | Error => { const apyPercentage = apyEntry === undefined ? undefined : Number(apyEntry.toFixed(2)); + const membersKeyValuePairs = poolMembers.map( + ([, address, account]) => [address, account] as const, + ); + + const membersMap = new Map(membersKeyValuePairs); + const pool: LsPool = { id: poolId, metadata, @@ -101,6 +107,7 @@ const useLsPools = (): Map | null | Error => { validators, totalStaked, apyPercentage, + members: membersMap, }; return [poolId, pool] as const; diff --git a/apps/tangle-dapp/hooks/useSubstrateAddress.ts b/apps/tangle-dapp/hooks/useSubstrateAddress.ts index 24186473c..d9c8e34a1 100644 --- a/apps/tangle-dapp/hooks/useSubstrateAddress.ts +++ b/apps/tangle-dapp/hooks/useSubstrateAddress.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import useNetworkStore from '../context/useNetworkStore'; +import { SubstrateAddress } from '../types/utils'; import { toSubstrateAddress } from '../utils'; import useActiveAccountAddress from './useActiveAccountAddress'; @@ -14,7 +15,7 @@ import useActiveAccountAddress from './useActiveAccountAddress'; * If the active account is an EVM account, its EVM address will be * converted into a Substrate address via hashing. */ -const useSubstrateAddress = (): string | null => { +const useSubstrateAddress = (): SubstrateAddress | null => { const activeAccountAddress = useActiveAccountAddress(); const { network } = useNetworkStore(); diff --git a/apps/tangle-dapp/utils/toSubstrateAddress.ts b/apps/tangle-dapp/utils/toSubstrateAddress.ts index 8a637f3ae..8f0dedb78 100644 --- a/apps/tangle-dapp/utils/toSubstrateAddress.ts +++ b/apps/tangle-dapp/utils/toSubstrateAddress.ts @@ -6,6 +6,9 @@ import { import { isSubstrateAddress } from '@webb-tools/dapp-types'; import assert from 'assert'; +import { SubstrateAddress } from '../types/utils'; +import assertSubstrateAddress from './assertSubstrateAddress'; + /** * Converts an EVM address to a Substrate address. * @@ -25,12 +28,12 @@ import assert from 'assert'; export const toSubstrateAddress = ( address: string, ss58Format?: number, -): string => { +): SubstrateAddress => { // If it's an EVM address, convert it to a Substrate address. if (isEthereumAddress(address)) { // Different SS58 formats can be used for different networks, // which still represents the same account, but look different. - return evmToAddress(address, ss58Format); + return assertSubstrateAddress(evmToAddress(address, ss58Format)); } // Otherwise, it must be a valid Substrate address. @@ -42,5 +45,5 @@ export const toSubstrateAddress = ( // Process the address with the given SS58 format, in // case that the SS58 format given differs from that of the // address. - return encodeAddress(address, ss58Format); + return assertSubstrateAddress(encodeAddress(address, ss58Format)); }; From e21795d1788e0138fae9034b8dc018ab3b5c52b5 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:45:35 -0400 Subject: [PATCH 32/54] feat(tangle-dapp): Filter my pools --- .../NetworkSelectionButton.tsx | 8 +++-- .../tangle-dapp/containers/LsMyPoolsTable.tsx | 32 +++++++++++++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx index ddd5675db..d52de5717 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx @@ -60,7 +60,9 @@ const NetworkSelectionButton: FC = () => { // Disable network switching when in Liquid Staking page, // since it would have no effect there. - const isInLiquidStakingPath = pathname.startsWith(PagePath.LIQUID_STAKING); + const isInLiquidStakingPage = + pathname.startsWith(PagePath.LIQUID_STAKING) && + !pathname.startsWith(PagePath.LIQUID_STAKING_OVERVIEW); const isInBridgePath = useMemo( () => pathname.startsWith(PagePath.BRIDGE), @@ -96,8 +98,8 @@ const NetworkSelectionButton: FC = () => { return null; } // Network can't be switched from the Tangle Restaking Parachain while - // on liquid staking page. - else if (isInLiquidStakingPath) { + // on the liquid staking page. + else if (isInLiquidStakingPage) { // Special case when the liquifier is selected. const lsNetworkName = isLiquifierProtocolId(selectedProtocolId) ? IS_PRODUCTION_ENV diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index 7cb3e24f5..d7da43c77 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -26,6 +26,7 @@ import { ArrowRight } from '@webb-tools/icons'; import useLsPools from '../data/liquidStaking/useLsPools'; import useSubstrateAddress from '../hooks/useSubstrateAddress'; import { BN } from '@polkadot/util'; +import assert from 'assert'; type MyLsPoolRow = LsPool & { myStake: BN; @@ -165,13 +166,30 @@ const LsMyPoolsTable: FC = () => { const lsPools = lsPoolsMap instanceof Map ? Array.from(lsPoolsMap.values()) : lsPoolsMap; - const myPools = - substrateAddress === null || !Array.isArray(lsPools) - ? null - : lsPools.filter((lsPool) => lsPool.members.has(substrateAddress)); + const rows: MyLsPoolRow[] = useMemo(() => { + if (substrateAddress === null || !Array.isArray(lsPools)) { + return []; + } + + return lsPools + .filter((lsPool) => lsPool.members.has(substrateAddress)) + .map((lsPool) => { + const account = lsPool.members.get(substrateAddress); + + assert(account !== undefined); + + return { + ...lsPool, + myStake: account.balance.toBn(), + isRoot: lsPool.ownerAddress === substrateAddress, + isNominator: lsPool.nominatorAddress === substrateAddress, + isBouncer: lsPool.bouncerAddress === substrateAddress, + }; + }); + }, []); const table = useReactTable({ - data: myPools ?? [], + data: rows, columns: POOL_COLUMNS, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -199,7 +217,7 @@ const LsMyPoolsTable: FC = () => { { canNextPage={table.getCanNextPage()} nextPage={table.nextPage} setPageIndex={table.setPageIndex} - title={pluralize('pool', myPools.length === 0 || myPools.length > 1)} + title={pluralize('pool', rows.length === 0 || rows.length > 1)} className="border-t-0 py-5" /> From 8a5074b350219ffd7ccbb8d52aaf04356491d2d6 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 27 Sep 2024 23:09:17 -0400 Subject: [PATCH 33/54] refactor(tangle-dapp): Move LS cards to overview page --- .../app/liquid-staking/overview/page.tsx | 63 ++++++++++++++++++- .../tangle-dapp/containers/LsMyPoolsTable.tsx | 58 ++++++++++------- 2 files changed, 96 insertions(+), 25 deletions(-) diff --git a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx b/apps/tangle-dapp/app/liquid-staking/overview/page.tsx index 76caf58ed..c2f9c8e9f 100644 --- a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/overview/page.tsx @@ -1,12 +1,52 @@ +'use client'; + import { LsProtocolsTable, Typography } from '@webb-tools/webb-ui-components'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; +import LsStakeCard from '../../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; +import LsUnstakeCard from '../../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; import StatItem from '../../../components/StatItem'; +import { LsSearchParamKey } from '../../../constants/liquidStaking/types'; import LsMyPoolsTable from '../../../containers/LsMyPoolsTable'; +import useNetworkStore from '../../../context/useNetworkStore'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; +import useNetworkSwitcher from '../../../hooks/useNetworkSwitcher'; +import useSearchParamState from '../../../hooks/useSearchParamState'; +import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; +import TabListItem from '../../restake/TabListItem'; +import TabsList from '../../restake/TabsList'; + +enum SearchParamAction { + STAKE = 'stake', + UNSTAKE = 'unstake', +} const LiquidStakingPage: FC = () => { + const [isStaking, setIsStaking] = useSearchParamState({ + defaultValue: true, + key: LsSearchParamKey.ACTION, + parser: (value) => value === SearchParamAction.STAKE, + stringify: (value) => + value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, + }); + + const { selectedNetworkId } = useLsStore(); + const { network } = useNetworkStore(); + const { switchNetwork } = useNetworkSwitcher(); + + const lsTangleNetwork = getLsTangleNetwork(selectedNetworkId); + + // Sync the network with the selected liquid staking network on load. + // It might differ initially if the user navigates to the page and + // the active network differs from the default liquid staking network. + useEffect(() => { + if (lsTangleNetwork !== null && lsTangleNetwork.id !== network.id) { + switchNetwork(lsTangleNetwork, false); + } + }, [lsTangleNetwork, network.id, selectedNetworkId, switchNetwork]); + return ( -
+
@@ -28,9 +68,26 @@ const LiquidStakingPage: FC = () => {
- +
+ + setIsStaking(true)}> + Stake + + + setIsStaking(false)} + > + Unstake + + + + {isStaking ? : } +
+ +
); }; diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index d7da43c77..5dda1b769 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -27,6 +27,7 @@ import useLsPools from '../data/liquidStaking/useLsPools'; import useSubstrateAddress from '../hooks/useSubstrateAddress'; import { BN } from '@polkadot/util'; import assert from 'assert'; +import { GlassCard } from '../components'; type MyLsPoolRow = LsPool & { myStake: BN; @@ -204,30 +205,43 @@ const LsMyPoolsTable: FC = () => { enableSortingRemoval: false, }); + // Don't render if the user is not involved in any pools. + if (rows.length === 0) { + return; + } + return ( -
-
+
+ + Your Pools + - 1)} - className="border-t-0 py-5" - /> + +
+
+ + 1)} + className="border-t-0 py-5" + /> + + ); }; From 560d42565ef4dbe06257551af42a06bfc6d12219 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:27:16 -0400 Subject: [PATCH 34/54] refactor(tangle-dapp): Merge both pages --- .../app/liquid-staking/overview/page.tsx | 95 ------- apps/tangle-dapp/app/liquid-staking/page.tsx | 92 +++++-- .../app/liquid-staking/useVaults.ts | 259 ------------------ .../components/Breadcrumbs/utils.tsx | 1 - .../LiquidStaking/stakeAndUnstake/LsInput.tsx | 3 + .../stakeAndUnstake/LsStakeCard.tsx | 19 +- .../stakeAndUnstake/SelectedPoolIndicator.tsx | 39 +++ .../NetworkSelectionButton.tsx | 4 +- .../components/Sidebar/sidebarProps.ts | 2 +- .../tangle-dapp/containers/LsMyPoolsTable.tsx | 63 ++--- .../LsPoolsTable2/LsPoolsTable2.tsx | 156 ++++++----- .../LsPoolsTable2/LsProtocolsTable.tsx | 36 +-- apps/tangle-dapp/types/index.ts | 1 - 13 files changed, 256 insertions(+), 514 deletions(-) delete mode 100644 apps/tangle-dapp/app/liquid-staking/overview/page.tsx delete mode 100644 apps/tangle-dapp/app/liquid-staking/useVaults.ts create mode 100644 apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx b/apps/tangle-dapp/app/liquid-staking/overview/page.tsx deleted file mode 100644 index c2f9c8e9f..000000000 --- a/apps/tangle-dapp/app/liquid-staking/overview/page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; - -import { LsProtocolsTable, Typography } from '@webb-tools/webb-ui-components'; -import { FC, useEffect } from 'react'; - -import LsStakeCard from '../../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; -import LsUnstakeCard from '../../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; -import StatItem from '../../../components/StatItem'; -import { LsSearchParamKey } from '../../../constants/liquidStaking/types'; -import LsMyPoolsTable from '../../../containers/LsMyPoolsTable'; -import useNetworkStore from '../../../context/useNetworkStore'; -import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useNetworkSwitcher from '../../../hooks/useNetworkSwitcher'; -import useSearchParamState from '../../../hooks/useSearchParamState'; -import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; -import TabListItem from '../../restake/TabListItem'; -import TabsList from '../../restake/TabsList'; - -enum SearchParamAction { - STAKE = 'stake', - UNSTAKE = 'unstake', -} - -const LiquidStakingPage: FC = () => { - const [isStaking, setIsStaking] = useSearchParamState({ - defaultValue: true, - key: LsSearchParamKey.ACTION, - parser: (value) => value === SearchParamAction.STAKE, - stringify: (value) => - value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, - }); - - const { selectedNetworkId } = useLsStore(); - const { network } = useNetworkStore(); - const { switchNetwork } = useNetworkSwitcher(); - - const lsTangleNetwork = getLsTangleNetwork(selectedNetworkId); - - // Sync the network with the selected liquid staking network on load. - // It might differ initially if the user navigates to the page and - // the active network differs from the default liquid staking network. - useEffect(() => { - if (lsTangleNetwork !== null && lsTangleNetwork.id !== network.id) { - switchNetwork(lsTangleNetwork, false); - } - }, [lsTangleNetwork, network.id, selectedNetworkId, switchNetwork]); - - return ( -
-
-
- - Tangle Liquid Staking - - - - Get Liquid Staking Tokens (LSTs) to earn & unleash restaking on - Tangle Mainnet via delegation. - -
- -
- -
-
- -
- - setIsStaking(true)}> - Stake - - - setIsStaking(false)} - > - Unstake - - - - {isStaking ? : } -
- - - - -
- ); -}; - -export default LiquidStakingPage; diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 3f98607a4..e6870dea0 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -1,19 +1,25 @@ 'use client'; +import { + LsProtocolsTable, + TabContent, + TabsList as WebbTabsList, + TabsRoot, + TabTrigger, + Typography, +} from '@webb-tools/webb-ui-components'; import { FC, useEffect } from 'react'; -import { LsValidatorTable } from '../../components/LiquidStaking/LsValidatorTable'; import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; -import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; +import StatItem from '../../components/StatItem'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; -import LsPoolsTable from '../../containers/LsPoolsTable'; +import LsMyPoolsTable from '../../containers/LsMyPoolsTable'; import useNetworkStore from '../../context/useNetworkStore'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; import useNetworkSwitcher from '../../hooks/useNetworkSwitcher'; import useSearchParamState from '../../hooks/useSearchParamState'; import getLsTangleNetwork from '../../utils/liquidStaking/getLsTangleNetwork'; -import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId'; import TabListItem from '../restake/TabListItem'; import TabsList from '../restake/TabsList'; @@ -22,7 +28,12 @@ enum SearchParamAction { UNSTAKE = 'unstake', } -const LiquidStakingTokenPage: FC = () => { +enum Tab { + ALL_POOLS = 'All Pools', + MY_POOLS = 'My Pools', +} + +const LiquidStakingPage: FC = () => { const [isStaking, setIsStaking] = useSearchParamState({ defaultValue: true, key: LsSearchParamKey.ACTION, @@ -31,12 +42,11 @@ const LiquidStakingTokenPage: FC = () => { value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, }); - const { selectedProtocolId, selectedNetworkId } = useLsStore(); + const { selectedNetworkId } = useLsStore(); const { network } = useNetworkStore(); const { switchNetwork } = useNetworkSwitcher(); const lsTangleNetwork = getLsTangleNetwork(selectedNetworkId); - const isParachainChain = isLsParachainChainId(selectedProtocolId); // Sync the network with the selected liquid staking network on load. // It might differ initially if the user navigates to the page and @@ -48,8 +58,29 @@ const LiquidStakingTokenPage: FC = () => { }, [lsTangleNetwork, network.id, selectedNetworkId, switchNetwork]); return ( -
-
+
+
+
+ + Tangle Liquid Staking + + + + Get Liquid Staking Tokens (LSTs) to earn & unleash restaking on + Tangle Mainnet via delegation. + +
+ +
+ +
+
+ +
setIsStaking(true)}> Stake @@ -66,19 +97,38 @@ const LiquidStakingTokenPage: FC = () => { {isStaking ? : }
-
- {isStaking ? ( - isParachainChain ? ( - - ) : ( - - ) - ) : ( - - )} -
+ +
+ {/* Tabs List on the left */} + + {Object.values(Tab).map((tab, idx) => { + return ( + + + {tab} + + + ); + })} + +
+ + {/* Tabs Content */} + + + + + + + +
); }; -export default LiquidStakingTokenPage; +export default LiquidStakingPage; diff --git a/apps/tangle-dapp/app/liquid-staking/useVaults.ts b/apps/tangle-dapp/app/liquid-staking/useVaults.ts deleted file mode 100644 index f30e4edee..000000000 --- a/apps/tangle-dapp/app/liquid-staking/useVaults.ts +++ /dev/null @@ -1,259 +0,0 @@ -'use client'; - -import { LiquidStakingToken } from '../../types/liquidStaking'; -import { Vault } from '../../types/liquidStaking'; - -export default function useVaults(): Vault[] { - return [ - { - lstToken: LiquidStakingToken.DOT, - name: 'Tangle Liquid Polkadot', - tvl: { - value: 2000, - valueInUSD: 20000, - }, - derivativeTokens: 5, - myStake: { - value: 1000, - valueInUSD: 10000, - }, - assets: [ - { - id: '31234', - token: 'tgDOT_A', - tvl: 5588.23, - apy: 10.12, - myStake: 10.12, - }, - { - id: '31235', - token: 'tgDOT_B', - tvl: 2044.12, - apy: 0, - myStake: 0, - }, - { - id: '31236', - token: 'tgDOT_C', - tvl: 123.12, - apy: 16, - myStake: 16, - }, - { - id: '31237', - token: 'tgDOT_D', - tvl: 6938.87, - apy: 100, - myStake: 100, - }, - { - id: '31238', - token: 'tgDOT_E', - tvl: 0, - apy: 0, - myStake: 0, - }, - ], - }, - { - lstToken: LiquidStakingToken.ASTR, - name: 'Tangle Liquid Astar', - tvl: { - value: 48, - valueInUSD: 480, - }, - derivativeTokens: 10, - myStake: { - value: 23.34, - valueInUSD: 233.4, - }, - assets: [ - { - id: '31234', - token: 'tgDOT_A', - tvl: 5588.23, - apy: 10.12, - myStake: 10.12, - }, - { - id: '31235', - token: 'tgDOT_B', - tvl: 2044.12, - apy: 0, - myStake: 0, - }, - { - id: '31236', - token: 'tgDOT_C', - tvl: 123.12, - apy: 16, - myStake: 16, - }, - { - id: '31237', - token: 'tgDOT_D', - tvl: 6938.87, - apy: 100, - myStake: 100, - }, - { - id: '31238', - token: 'tgDOT_E', - tvl: 0, - apy: 0, - myStake: 0, - }, - ], - }, - { - lstToken: LiquidStakingToken.PHA, - name: 'Tangle Liquid Phala', - tvl: { - value: 60.13, - valueInUSD: 601.3, - }, - derivativeTokens: 7, - myStake: { - value: 50, - valueInUSD: 500, - }, - assets: [ - { - id: '31234', - token: 'tgDOT_A', - tvl: 5588.23, - apy: 10.12, - myStake: 10.12, - }, - { - id: '31235', - token: 'tgDOT_B', - tvl: 2044.12, - apy: 0, - myStake: 0, - }, - { - id: '31236', - token: 'tgDOT_C', - tvl: 123.12, - apy: 16, - myStake: 16, - }, - { - id: '31237', - token: 'tgDOT_D', - tvl: 6938.87, - apy: 100, - myStake: 100, - }, - { - id: '31238', - token: 'tgDOT_E', - tvl: 0, - apy: 0, - myStake: 0, - }, - ], - }, - { - lstToken: LiquidStakingToken.GLMR, - name: 'Tangle Liquid Glimmer', - tvl: { - value: 0, - valueInUSD: 0, - }, - derivativeTokens: 0, - myStake: { - value: 0, - valueInUSD: 0, - }, - assets: [ - { - id: '31234', - token: 'tgDOT_A', - tvl: 5588.23, - apy: 10.12, - myStake: 10.12, - }, - { - id: '31235', - token: 'tgDOT_B', - tvl: 2044.12, - apy: 0, - myStake: 0, - }, - { - id: '31236', - token: 'tgDOT_C', - tvl: 123.12, - apy: 16, - myStake: 16, - }, - { - id: '31237', - token: 'tgDOT_D', - tvl: 6938.87, - apy: 100, - myStake: 100, - }, - { - id: '31238', - token: 'tgDOT_E', - tvl: 0, - apy: 0, - myStake: 0, - }, - ], - }, - { - lstToken: LiquidStakingToken.MANTA, - name: 'Tangle Liquid Manta', - tvl: { - value: 0, - valueInUSD: 0, - }, - derivativeTokens: 0, - myStake: { - value: 0, - valueInUSD: 0, - }, - assets: [ - { - id: '31234', - token: 'tgDOT_A', - tvl: 5588.23, - apy: 10.12, - myStake: 10.12, - }, - { - id: '31235', - token: 'tgDOT_B', - tvl: 2044.12, - apy: 0, - myStake: 0, - }, - { - id: '31236', - token: 'tgDOT_C', - tvl: 123.12, - apy: 16, - myStake: 16, - }, - { - id: '31237', - token: 'tgDOT_D', - tvl: 6938.87, - apy: 100, - myStake: 100, - }, - { - id: '31238', - token: 'tgDOT_E', - tvl: 0, - apy: 0, - myStake: 0, - }, - ], - }, - ]; -} diff --git a/apps/tangle-dapp/components/Breadcrumbs/utils.tsx b/apps/tangle-dapp/components/Breadcrumbs/utils.tsx index 49f1c4776..c104c9f7c 100644 --- a/apps/tangle-dapp/components/Breadcrumbs/utils.tsx +++ b/apps/tangle-dapp/components/Breadcrumbs/utils.tsx @@ -29,7 +29,6 @@ const BREADCRUMB_ICONS: Record JSX.Element> = { [PagePath.RESTAKE_OPERATOR]: TokenSwapFill, [PagePath.BRIDGE]: ShuffleLine, [PagePath.LIQUID_STAKING]: WaterDropletIcon, - [PagePath.LIQUID_STAKING_OVERVIEW]: WaterDropletIcon, }; const BREADCRUMB_LABELS: Partial> = { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx index a6b04a836..25827ef7f 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx @@ -16,6 +16,7 @@ import useInputAmount from '../../../hooks/useInputAmount'; import formatBn from '../../../utils/formatBn'; import NetworkSelector from './NetworkSelector'; import ProtocolSelector from './ProtocolSelector'; +import SelectedPoolIndicator from './SelectedPoolIndicator'; export type LsInputProps = { id: string; @@ -132,6 +133,8 @@ const LsInput: FC = ({ setProtocolId={setProtocolId} isDerivativeVariant={isDerivativeVariant} /> + +
diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 0d219a35a..b7209e825 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -27,6 +27,7 @@ import useLsPoolJoinTx from '../../../data/liquidStaking/tangle/useLsPoolJoinTx' import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; +import useLsPoolMembers from '../../../data/liquidStaking/useLsPoolMembers'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import useLiquifierDeposit from '../../../data/liquifier/useLiquifierDeposit'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; @@ -76,6 +77,22 @@ const LsStakeCard: FC = () => { const selectedProtocol = getLsProtocolDef(selectedProtocolId); const tryChangeNetwork = useLsChangeNetwork(); + const lsPoolMembers = useLsPoolMembers(); + + const actionText = useMemo(() => { + const defaultText = 'Stake'; + + if (lsPoolMembers === null) { + return defaultText; + } + + const isMember = lsPoolMembers.some( + ([poolId, accountAddress]) => + poolId === selectedPoolId && accountAddress === activeAccountAddress, + ); + + return isMember ? 'Increase Stake' : defaultText; + }, [activeAccountAddress, lsPoolMembers, selectedPoolId]); const isTangleNetwork = selectedNetworkId === LsNetworkId.TANGLE_LOCAL || @@ -257,7 +274,7 @@ const LsStakeCard: FC = () => { onClick={handleStakeClick} isFullWidth > - Stake + {actionText} ); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx new file mode 100644 index 000000000..a83e43b2f --- /dev/null +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx @@ -0,0 +1,39 @@ +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC, useMemo } from 'react'; + +import useLsPools from '../../../data/liquidStaking/useLsPools'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; +import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; +import LsTokenIcon from '../../LsTokenIcon'; + +const SelectedPoolIndicator: FC = () => { + const { selectedPoolId, selectedProtocolId } = useLsStore(); + const lsPools = useLsPools(); + const selectedProtocol = getLsProtocolDef(selectedProtocolId); + + const selectedPool = useMemo(() => { + if (!(lsPools instanceof Map) || selectedPoolId === null) { + return null; + } + + return lsPools.get(selectedPoolId) ?? null; + }, [lsPools, selectedPoolId]); + + return ( +
+ + + {selectedPool === null ? ( + + Select a pool + + ) : ( + + {selectedPool.metadata}#{selectedPool.id} + + )} +
+ ); +}; + +export default SelectedPoolIndicator; diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx index d52de5717..7f238e267 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx @@ -60,9 +60,7 @@ const NetworkSelectionButton: FC = () => { // Disable network switching when in Liquid Staking page, // since it would have no effect there. - const isInLiquidStakingPage = - pathname.startsWith(PagePath.LIQUID_STAKING) && - !pathname.startsWith(PagePath.LIQUID_STAKING_OVERVIEW); + const isInLiquidStakingPage = pathname.startsWith(PagePath.LIQUID_STAKING); const isInBridgePath = useMemo( () => pathname.startsWith(PagePath.BRIDGE), diff --git a/apps/tangle-dapp/components/Sidebar/sidebarProps.ts b/apps/tangle-dapp/components/Sidebar/sidebarProps.ts index e58ad1e61..9ef5f1aa9 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[] = [ }, { name: 'Liquid Stake', - href: PagePath.LIQUID_STAKING_OVERVIEW, + href: PagePath.LIQUID_STAKING, environments: ['development', 'staging', 'test'], isInternal: true, isNext: true, diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index 5dda1b769..2283759ae 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -205,44 +205,33 @@ const LsMyPoolsTable: FC = () => { enableSortingRemoval: false, }); - // Don't render if the user is not involved in any pools. - if (rows.length === 0) { - return; - } - return ( -
- - Your Pools - - - -
-
- - 1)} - className="border-t-0 py-5" - /> - - - + +
+
+ + 1)} + className="border-t-0 py-5" + /> + + ); }; diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx index 54dbc7fea..665df0f74 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx @@ -23,6 +23,7 @@ import TokenAmountCell from '../../components/tableCells/TokenAmountCell'; import pluralize from '../../utils/pluralize'; import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import { ArrowRight } from '@webb-tools/icons'; +import { useLsStore } from '../../data/liquidStaking/useLsStore'; export interface LsPoolsTable2Props { pools: LsPool[]; @@ -31,79 +32,6 @@ export interface LsPoolsTable2Props { const COLUMN_HELPER = createColumnHelper(); -const POOL_COLUMNS = [ - COLUMN_HELPER.accessor('id', { - header: () => 'Name/id', - cell: (props) => ( - - {props.row.original.metadata}#{props.getValue()} - - ), - }), - COLUMN_HELPER.accessor('token', { - header: () => 'Token', - cell: (props) => ( - - {props.getValue()} - - ), - }), - COLUMN_HELPER.accessor('ownerAddress', { - header: () => 'Owner', - cell: (props) => ( - - ), - }), - COLUMN_HELPER.accessor('totalStaked', { - header: () => 'TVL', - // TODO: Decimals. - cell: (props) => , - }), - COLUMN_HELPER.accessor('apyPercentage', { - header: () => 'APY', - cell: (props) => { - const apy = props.getValue(); - - if (apy === undefined) { - return EMPTY_VALUE_PLACEHOLDER; - } - - return ( - - {getRoundedAmountString(props.getValue()) + '%'} - - ); - }, - }), - COLUMN_HELPER.display({ - id: 'actions', - header: () => 'Actions', - cell: (props) => ( -
- -
- ), - }), -]; - const LsPoolsTable2: FC = ({ pools, isShown }) => { const [sorting, setSorting] = useState([]); @@ -112,6 +40,8 @@ const LsPoolsTable2: FC = ({ pools, isShown }) => { pageSize: 5, }); + const { selectedPoolId, setSelectedPoolId } = useLsStore(); + const pagination = useMemo( () => ({ pageIndex, @@ -120,9 +50,87 @@ const LsPoolsTable2: FC = ({ pools, isShown }) => { [pageIndex, pageSize], ); + const columns = [ + COLUMN_HELPER.accessor('id', { + header: () => 'Name/id', + cell: (props) => ( + + {props.row.original.metadata}#{props.getValue()} + + ), + }), + COLUMN_HELPER.accessor('token', { + header: () => 'Token', + cell: (props) => ( + + {props.getValue()} + + ), + }), + COLUMN_HELPER.accessor('ownerAddress', { + header: () => 'Owner', + cell: (props) => ( + + ), + }), + COLUMN_HELPER.accessor('totalStaked', { + header: () => 'TVL', + // TODO: Decimals. + cell: (props) => , + }), + COLUMN_HELPER.accessor('apyPercentage', { + header: () => 'APY', + cell: (props) => { + const apy = props.getValue(); + + if (apy === undefined) { + return EMPTY_VALUE_PLACEHOLDER; + } + + return ( + + {getRoundedAmountString(props.getValue()) + '%'} + + ); + }, + }), + COLUMN_HELPER.display({ + id: 'actions', + cell: (props) => ( +
+ +
+ ), + }), + ]; + const table = useReactTable({ data: pools, - columns: POOL_COLUMNS, + columns: columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx index deb583576..8c149cd35 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx @@ -164,27 +164,21 @@ function LsProtocolsTable({ initialSorting = [] }: LsProtocolsTableProps) { ); return ( -
- - Liquid Staking Protocols - - -
- +
); } diff --git a/apps/tangle-dapp/types/index.ts b/apps/tangle-dapp/types/index.ts index dd77219e1..49e9ced5d 100755 --- a/apps/tangle-dapp/types/index.ts +++ b/apps/tangle-dapp/types/index.ts @@ -18,7 +18,6 @@ export enum PagePath { RESTAKE_STAKE = '/restake/stake', RESTAKE_OPERATOR = '/restake/operators', LIQUID_STAKING = '/liquid-staking', - LIQUID_STAKING_OVERVIEW = '/liquid-staking/overview', } export enum QueryParamKey { From aba16cfdc9e9f7187404a99d5fccd2e37e555977 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 05:17:10 -0400 Subject: [PATCH 35/54] refactor(tangle-dapp): Create `useLsPoolsMetadata` hook --- .../LiquidStaking/stakeAndUnstake/LsInput.tsx | 29 +++++++----- .../stakeAndUnstake/LsStakeCard.tsx | 2 - .../stakeAndUnstake/LsUnstakeCard.tsx | 2 - .../stakeAndUnstake/SelectedPoolIndicator.tsx | 30 ++++-------- .../stakeAndUnstake/TokenChip.tsx | 10 ++-- .../constants/liquidStaking/types.ts | 2 + .../tangle-dapp/containers/LsMyPoolsTable.tsx | 2 +- .../LsPoolsTable2/LsPoolsTable2.tsx | 2 +- .../LsPoolsTable2/LsProtocolsTable.tsx | 33 +++++++------ .../data/liquidStaking/useLsPools.ts | 37 ++++----------- .../data/liquidStaking/useLsPoolsMetadata.ts | 46 +++++++++++++++++++ .../useSelectedPoolDisplayName.ts | 13 ++++++ 12 files changed, 122 insertions(+), 86 deletions(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts create mode 100644 apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx index 25827ef7f..0c9f4cec3 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx @@ -12,16 +12,17 @@ import { LsToken, } from '../../../constants/liquidStaking/types'; import { ERROR_NOT_ENOUGH_BALANCE } from '../../../containers/ManageProfileModalContainer/Independent/IndependentAllocationInput'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import useInputAmount from '../../../hooks/useInputAmount'; import formatBn from '../../../utils/formatBn'; +import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import NetworkSelector from './NetworkSelector'; -import ProtocolSelector from './ProtocolSelector'; import SelectedPoolIndicator from './SelectedPoolIndicator'; +import TokenChip from './TokenChip'; export type LsInputProps = { id: string; networkId: LsNetworkId; - protocolId: LsProtocolId; decimals: number; amount: BN | null; isReadOnly?: boolean; @@ -33,6 +34,7 @@ export type LsInputProps = { maxAmount?: BN; maxErrorMessage?: string; className?: string; + showPoolIndicator?: boolean; onAmountChange?: (newAmount: BN | null) => void; setProtocolId?: (newProtocolId: LsProtocolId) => void; setNetworkId?: (newNetworkId: LsNetworkId) => void; @@ -47,17 +49,20 @@ const LsInput: FC = ({ placeholder = '0', isDerivativeVariant = false, rightElement, - protocolId, networkId, token, minAmount, maxAmount, maxErrorMessage = ERROR_NOT_ENOUGH_BALANCE, onAmountChange, - setProtocolId, setNetworkId, className, + showPoolIndicator = true, }) => { + const { selectedProtocolId } = useLsStore(); + + const selectedProtocol = getLsProtocolDef(selectedProtocolId); + const minErrorMessage = ((): string | undefined => { if (minAmount === undefined) { return undefined; @@ -127,14 +132,14 @@ const LsInput: FC = ({ readOnly={isReadOnly} /> - - - + {showPoolIndicator ? ( + + ) : ( + + )} diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index b7209e825..9b4aa493c 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -211,7 +211,6 @@ const LsStakeCard: FC = () => { { { id="liquid-staking-unstake-from" networkId={selectedNetworkId} setNetworkId={tryChangeNetwork} - protocolId={selectedProtocolId} setProtocolId={setSelectedProtocolId} token={selectedProtocol.token} amount={fromAmount} @@ -214,7 +213,6 @@ const LsUnstakeCard: FC = () => { { - const { selectedPoolId, selectedProtocolId } = useLsStore(); - const lsPools = useLsPools(); + const { selectedProtocolId } = useLsStore(); const selectedProtocol = getLsProtocolDef(selectedProtocolId); - - const selectedPool = useMemo(() => { - if (!(lsPools instanceof Map) || selectedPoolId === null) { - return null; - } - - return lsPools.get(selectedPoolId) ?? null; - }, [lsPools, selectedPoolId]); + const selectedPoolDisplayName = useSelectedPoolDisplayName(); return (
- {selectedPool === null ? ( - - Select a pool - - ) : ( - - {selectedPool.metadata}#{selectedPool.id} - - )} + + {selectedPoolDisplayName === null + ? 'Select a pool' + : selectedPoolDisplayName} +
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx index 8bac4d61d..f91ec5912 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx @@ -9,11 +9,15 @@ import DropdownChevronIcon from './DropdownChevronIcon'; type TokenChipProps = { token?: LsToken; - isLiquidVariant: boolean; + isDerivativeVariant: boolean; onClick?: () => void; }; -const TokenChip: FC = ({ token, isLiquidVariant, onClick }) => { +const TokenChip: FC = ({ + token, + isDerivativeVariant, + onClick, +}) => { return (
= ({ token, isLiquidVariant, onClick }) => { {token && } - {isLiquidVariant && LS_DERIVATIVE_TOKEN_PREFIX} + {isDerivativeVariant && LS_DERIVATIVE_TOKEN_PREFIX} {token} diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 034f4ae37..7e1a26d5e 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -198,3 +198,5 @@ export type LsPool = { commissionPercentage?: number; members: Map; }; + +export type LsPoolDisplayName = `${string}#${number}`; diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index 2283759ae..6ee5f0af7 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -40,7 +40,7 @@ const COLUMN_HELPER = createColumnHelper(); const POOL_COLUMNS = [ COLUMN_HELPER.accessor('id', { - header: () => 'Name/id', + header: () => 'ID', cell: (props) => ( = ({ pools, isShown }) => { const columns = [ COLUMN_HELPER.accessor('id', { - header: () => 'Name/id', + header: () => 'ID', cell: (props) => ( (() => { + if (!(lsPools instanceof Map)) { + return []; + } + + return Array.from(lsPools.values()); + }, []); + + const protocols: LsProtocolRow[] = [ { - name: 'Tangle', iconName: 'tangle', - pools: [ - { - id: 1, - totalStaked: new BN(4234932942394239), - validators: [], - metadata: 'test', - }, - ], - tvl: 123.01, - tvlInUsd: 123.01, + name: 'Tangle', + pools: pools, + tvl: 123.4567, + tvlInUsd: 123.3456, }, ]; const table = useReactTable({ - data: rows, + data: protocols, columns: PROTOCOL_COLUMNS, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index edc6496a6..e87958b25 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -1,8 +1,7 @@ -import { BN_ZERO, u8aToString } from '@polkadot/util'; -import { useCallback, useMemo } from 'react'; +import { BN_ZERO } from '@polkadot/util'; +import { useMemo } from 'react'; import { LsPool } from '../../constants/liquidStaking/types'; -import useApiRx from '../../hooks/useApiRx'; import useNetworkFeatures from '../../hooks/useNetworkFeatures'; import { NetworkFeature } from '../../types'; import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; @@ -11,25 +10,14 @@ import useLsPoolCompoundApys from './apy/useLsPoolCompoundApys'; import useLsBondedPools from './useLsBondedPools'; import useLsPoolMembers from './useLsPoolMembers'; import useLsPoolNominations from './useLsPoolNominations'; +import useLsPoolsMetadata from './useLsPoolsMetadata'; const useLsPools = (): Map | null | Error => { const networkFeatures = useNetworkFeatures(); const poolNominations = useLsPoolNominations(); const isSupported = networkFeatures.includes(NetworkFeature.LsPools); - const { result: rawMetadataEntries } = useApiRx( - useCallback( - (api) => { - if (!isSupported) { - return null; - } - - return api.query.lst.metadata.entries(); - }, - [isSupported], - ), - ); - + const metadatas = useLsPoolsMetadata(); const bondedPools = useLsBondedPools(); const poolMembers = useLsPoolMembers(); const compoundApys = useLsPoolCompoundApys(); @@ -40,23 +28,14 @@ const useLsPools = (): Map | null | Error => { poolNominations === null || compoundApys === null || poolMembers === null || + metadatas === null || !isSupported ) { return null; } const keyValuePairs = bondedPools.map(([poolId, tanglePool]) => { - const metadataEntryBytes = - rawMetadataEntries === null - ? undefined - : rawMetadataEntries.find( - ([idKey]) => idKey.args[0].toNumber() === poolId, - )?.[1]; - - const metadata = - metadataEntryBytes === undefined - ? undefined - : u8aToString(metadataEntryBytes); + const metadata = metadatas.get(poolId); // Roles can be `None` if updated and removed. const ownerAddress = tanglePool.roles.root.isNone @@ -118,9 +97,9 @@ const useLsPools = (): Map | null | Error => { bondedPools, poolNominations, compoundApys, - isSupported, - rawMetadataEntries, poolMembers, + metadatas, + isSupported, ]); // In case that the user connects to testnet or mainnet, but the network diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts new file mode 100644 index 000000000..9bffce528 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts @@ -0,0 +1,46 @@ +import { u8aToString } from '@polkadot/util'; +import { useCallback, useMemo } from 'react'; + +import useApiRx from '../../hooks/useApiRx'; +import useNetworkFeatures from '../../hooks/useNetworkFeatures'; +import { NetworkFeature } from '../../types'; + +const useLsPoolsMetadata = () => { + const networkFeatures = useNetworkFeatures(); + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); + + const { result: rawMetadataEntries } = useApiRx( + useCallback( + (api) => { + if (!isSupported) { + return null; + } + + return api.query.lst.metadata.entries(); + }, + [isSupported], + ), + ); + + const keyValuePairs = useMemo(() => { + if (rawMetadataEntries === null) { + return null; + } + + return rawMetadataEntries.map(([key, value]) => { + return [key.args[0].toNumber(), u8aToString(value)] as const; + }); + }, [rawMetadataEntries]); + + const map = useMemo(() => { + if (keyValuePairs === null) { + return null; + } + + return new Map(keyValuePairs); + }, [keyValuePairs]); + + return map; +}; + +export default useLsPoolsMetadata; diff --git a/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts b/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts new file mode 100644 index 000000000..76dc8c161 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts @@ -0,0 +1,13 @@ +import { LsPoolDisplayName } from '../../constants/liquidStaking/types'; +import useLsPoolsMetadata from './useLsPoolsMetadata'; +import { useLsStore } from './useLsStore'; + +const useSelectedPoolDisplayName = (): LsPoolDisplayName | null => { + const { selectedPoolId } = useLsStore(); + const lsPoolsMetadata = useLsPoolsMetadata(); + const name = lsPoolsMetadata?.get(selectedPoolId) ?? ''; + + return selectedPoolId === null ? null : `${name}#${selectedPoolId}`; +}; + +export default useSelectedPoolDisplayName; From fcbba2052ac51bec6decd5e975b659651e7cb1b9 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 05:47:59 -0400 Subject: [PATCH 36/54] fix(tangle-dapp): Missing property `showPoolIndicator` --- .../LiquidStaking/stakeAndUnstake/LsStakeCard.tsx | 1 + .../LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx | 1 + apps/tangle-dapp/containers/LsMyPoolsTable.tsx | 2 +- .../containers/LsPoolsTable2/LsPoolsTable2.tsx | 10 +++++++--- .../containers/LsPoolsTable2/LsProtocolsTable.tsx | 10 +++++----- apps/tangle-dapp/data/liquidStaking/useLsPools.ts | 2 ++ .../data/liquidStaking/useSelectedPoolDisplayName.ts | 7 ++++++- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 9b4aa493c..a54bbe95d 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -221,6 +221,7 @@ const LsStakeCard: FC = () => { minAmount={minSpendable ?? undefined} maxAmount={maxSpendable ?? undefined} setNetworkId={tryChangeNetwork} + showPoolIndicator={false} /> diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index 84fbc0ffd..91c107f42 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -206,6 +206,7 @@ const LsUnstakeCard: FC = () => { maxAmount={maxSpendable ?? undefined} maxErrorMessage="Not enough stake to redeem" onTokenClick={() => setIsSelectTokenModalOpen(true)} + showPoolIndicator={false} /> diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index 6ee5f0af7..60986aaae 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -74,7 +74,7 @@ const POOL_COLUMNS = [ ), }), COLUMN_HELPER.accessor('totalStaked', { - header: () => 'TVL', + header: () => 'Total Staked (TVL)', // TODO: Decimals. cell: (props) => , }), diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx index 8373ee2a6..a2612e160 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx @@ -86,7 +86,7 @@ const LsPoolsTable2: FC = ({ pools, isShown }) => { ), }), COLUMN_HELPER.accessor('totalStaked', { - header: () => 'TVL', + header: () => 'Total Staked (TVL)', // TODO: Decimals. cell: (props) => , }), @@ -117,11 +117,15 @@ const LsPoolsTable2: FC = ({ pools, isShown }) => {
), diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx index b9a75aa77..df32dfec2 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx @@ -36,10 +36,10 @@ export type LsProtocolRow = { pools: LsPool[]; }; -const PROTOCOL_COLUMN_HELPER = createColumnHelper(); +const COLUMN_HELPER = createColumnHelper(); const PROTOCOL_COLUMNS = [ - PROTOCOL_COLUMN_HELPER.accessor('name', { + COLUMN_HELPER.accessor('name', { header: () => 'Token', cell: (props) => ( @@ -58,7 +58,7 @@ const PROTOCOL_COLUMNS = [ }, sortDescFirst: true, }), - PROTOCOL_COLUMN_HELPER.accessor('tvl', { + COLUMN_HELPER.accessor('tvl', { header: () => 'Total Staked (TVL)', cell: (props) => ( @@ -70,7 +70,7 @@ const PROTOCOL_COLUMNS = [ ), }), - PROTOCOL_COLUMN_HELPER.accessor('pools', { + COLUMN_HELPER.accessor('pools', { header: () => 'Pools', cell: (props) => { const length = props.getValue().length; @@ -86,7 +86,7 @@ const PROTOCOL_COLUMNS = [ ); }, }), - PROTOCOL_COLUMN_HELPER.display({ + COLUMN_HELPER.display({ id: 'expand/collapse', header: () => null, cell: ({ row }) => ( diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index e87958b25..573662056 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -37,6 +37,8 @@ const useLsPools = (): Map | null | Error => { const keyValuePairs = bondedPools.map(([poolId, tanglePool]) => { const metadata = metadatas.get(poolId); + // TODO: `tanglePool.metadata.name` should be available based on the latest changes to Tangle. Need to regenerate the types. Might want to prefer that method vs. the metadata query. + // Roles can be `None` if updated and removed. const ownerAddress = tanglePool.roles.root.isNone ? undefined diff --git a/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts b/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts index 76dc8c161..3a3dc7a4c 100644 --- a/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts +++ b/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts @@ -5,9 +5,14 @@ import { useLsStore } from './useLsStore'; const useSelectedPoolDisplayName = (): LsPoolDisplayName | null => { const { selectedPoolId } = useLsStore(); const lsPoolsMetadata = useLsPoolsMetadata(); + + if (selectedPoolId === null) { + return null; + } + const name = lsPoolsMetadata?.get(selectedPoolId) ?? ''; - return selectedPoolId === null ? null : `${name}#${selectedPoolId}`; + return `${name}#${selectedPoolId}`; }; export default useSelectedPoolDisplayName; From c62fd672faa497d0b31f23950125c723a17e4bb8 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:31:49 -0400 Subject: [PATCH 37/54] refactor(tangle-dapp): Get rid of Liquifier remenants --- .../stakeAndUnstake/LsAgnosticBalance.tsx | 3 +- .../stakeAndUnstake/LsStakeCard.tsx | 10 - .../stakeAndUnstake/LsUnstakeCard.tsx | 13 - .../stakeAndUnstake/useLsAgnosticBalance.ts | 52 +-- .../stakeAndUnstake/useLsFeePercentage.ts | 4 +- .../stakeAndUnstake/useLsSpendingLimits.ts | 2 +- .../UnstakeRequestsTable.tsx | 39 +- .../WithdrawUnlockNftButton.tsx | 45 --- .../constants/liquidStaking/constants.ts | 32 +- .../constants/liquidStaking/liquifierAbi.ts | 190 ---------- .../liquidStaking/liquifierAdapterAbi.ts | 358 ------------------ .../liquidStaking/liquifierRegistryAbi.ts | 211 ----------- .../liquidStaking/liquifierTgTokenAbi.ts | 291 -------------- .../liquidStaking/liquifierUnlocksAbi.ts | 238 ------------ .../constants/liquidStaking/types.ts | 32 +- .../{liquifier => evm}/useContractRead.ts | 0 .../useContractReadBatch.ts | 0 .../{liquifier => evm}/useContractReadOnce.ts | 0 .../{liquifier => evm}/useContractWrite.ts | 0 .../useContractWriteBatch.ts | 0 .../useViemPublicClientWithChain.ts | 0 .../data/liquidStaking/adapters/chainlink.ts | 34 -- .../data/liquidStaking/adapters/livepeer.ts | 34 -- .../data/liquidStaking/adapters/polygon.ts | 34 -- .../data/liquidStaking/adapters/theGraph.ts | 34 -- .../data/liquidStaking/useLsExchangeRate.ts | 4 +- .../data/liquifier/useLiquifierDeposit.ts | 85 ----- .../data/liquifier/useLiquifierNftUnlocks.ts | 165 -------- .../data/liquifier/useLiquifierUnlock.ts | 45 --- .../data/liquifier/useLiquifierWithdraw.ts | 56 --- .../utils/liquidStaking/getLsProtocolDef.ts | 6 +- .../liquidStaking/isLiquifierProtocolId.ts | 10 - .../liquidStaking/isLsParachainChainId.ts | 4 +- 33 files changed, 23 insertions(+), 2008 deletions(-) delete mode 100644 apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawUnlockNftButton.tsx delete mode 100644 apps/tangle-dapp/constants/liquidStaking/liquifierAbi.ts delete mode 100644 apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts delete mode 100644 apps/tangle-dapp/constants/liquidStaking/liquifierRegistryAbi.ts delete mode 100644 apps/tangle-dapp/constants/liquidStaking/liquifierTgTokenAbi.ts delete mode 100644 apps/tangle-dapp/constants/liquidStaking/liquifierUnlocksAbi.ts rename apps/tangle-dapp/data/{liquifier => evm}/useContractRead.ts (100%) rename apps/tangle-dapp/data/{liquifier => evm}/useContractReadBatch.ts (100%) rename apps/tangle-dapp/data/{liquifier => evm}/useContractReadOnce.ts (100%) rename apps/tangle-dapp/data/{liquifier => evm}/useContractWrite.ts (100%) rename apps/tangle-dapp/data/{liquifier => evm}/useContractWriteBatch.ts (100%) rename apps/tangle-dapp/data/{liquifier => evm}/useViemPublicClientWithChain.ts (100%) delete mode 100644 apps/tangle-dapp/data/liquidStaking/adapters/chainlink.ts delete mode 100644 apps/tangle-dapp/data/liquidStaking/adapters/livepeer.ts delete mode 100644 apps/tangle-dapp/data/liquidStaking/adapters/polygon.ts delete mode 100644 apps/tangle-dapp/data/liquidStaking/adapters/theGraph.ts delete mode 100644 apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts delete mode 100644 apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts delete mode 100644 apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts delete mode 100644 apps/tangle-dapp/data/liquifier/useLiquifierWithdraw.ts delete mode 100644 apps/tangle-dapp/utils/liquidStaking/isLiquifierProtocolId.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index 58d11a69e..e24c686e1 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -33,7 +33,7 @@ const LsAgnosticBalance: FC = ({ onClick, }) => { const [isHovering, setIsHovering] = useState(false); - const { balance, isRefreshing } = useLsAgnosticBalance(isNative); + const balance = useLsAgnosticBalance(isNative); const { selectedProtocolId } = useLsStore(); const protocol = getLsProtocolDef(selectedProtocolId); @@ -85,7 +85,6 @@ const LsAgnosticBalance: FC = ({ className={twMerge( 'group flex gap-1 items-center justify-center', isClickable && 'cursor-pointer', - isRefreshing && 'animate-pulse', )} > {isHovering && isClickable ? ( diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index a54bbe95d..166ab92e0 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -29,7 +29,6 @@ import useLsExchangeRate, { } from '../../../data/liquidStaking/useLsExchangeRate'; import useLsPoolMembers from '../../../data/liquidStaking/useLsPoolMembers'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useLiquifierDeposit from '../../../data/liquifier/useLiquifierDeposit'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamState from '../../../hooks/useSearchParamState'; import useSearchParamSync from '../../../hooks/useSearchParamSync'; @@ -67,7 +66,6 @@ const LsStakeCard: FC = () => { const { execute: executeParachainMintTx, status: parachainMintTxStatus } = useMintTx(); - const performLiquifierDeposit = useLiquifierDeposit(); const activeAccountAddress = useActiveAccountAddress(); const { maxSpendable, minSpendable } = useLsSpendingLimits( @@ -139,11 +137,6 @@ const LsStakeCard: FC = () => { amount: fromAmount, currency: selectedProtocol.currency, }); - } else if ( - selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierDeposit !== null - ) { - await performLiquifierDeposit(selectedProtocol.id, fromAmount); } else if ( isTangleNetwork && executeTanglePoolJoinTx !== null && @@ -159,7 +152,6 @@ const LsStakeCard: FC = () => { executeTanglePoolJoinTx, fromAmount, isTangleNetwork, - performLiquifierDeposit, selectedProtocol, selectedPoolId, ]); @@ -184,8 +176,6 @@ const LsStakeCard: FC = () => { (fromAmount !== null && selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && executeParachainMintTx !== null) || - (selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierDeposit !== null) || (isTangleNetwork && executeTanglePoolJoinTx !== null && selectedPoolId !== null); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index 91c107f42..68a4b452f 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -22,7 +22,6 @@ import useLsExchangeRate, { ExchangeRateType, } from '../../../data/liquidStaking/useLsExchangeRate'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useLiquifierUnlock from '../../../data/liquifier/useLiquifierUnlock'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useSearchParamSync from '../../../hooks/useSearchParamSync'; import { TxStatus } from '../../../hooks/useSubstrateTx'; @@ -67,8 +66,6 @@ const LsUnstakeCard: FC = () => { const { execute: executeTangleUnbondTx, status: tangleUnbondTxStatus } = useLsPoolUnbondTx(); - const performLiquifierUnlock = useLiquifierUnlock(); - const { minSpendable, maxSpendable } = useLsSpendingLimits( false, selectedProtocolId, @@ -113,13 +110,6 @@ const LsUnstakeCard: FC = () => { currency: selectedProtocol.currency, }); } else if ( - selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierUnlock !== null - ) { - return performLiquifierUnlock(selectedProtocol.id, fromAmount); - } - - if ( isTangleNetwork && executeTangleUnbondTx !== null && selectedPoolId !== null @@ -134,7 +124,6 @@ const LsUnstakeCard: FC = () => { executeTangleUnbondTx, fromAmount, isTangleNetwork, - performLiquifierUnlock, selectedPoolId, selectedProtocol, ]); @@ -181,8 +170,6 @@ const LsUnstakeCard: FC = () => { const canCallUnstake = (selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && executeParachainRedeemTx !== null) || - (selectedProtocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER && - performLiquifierUnlock !== null) || (isTangleNetwork && executeTangleUnbondTx !== null && selectedPoolId !== null); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts index b6a017a82..62c0e835f 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts @@ -1,18 +1,13 @@ import { BN, BN_ZERO } from '@polkadot/util'; -import { useCallback, useEffect, useState } from 'react'; -import { erc20Abi } from 'viem'; +import { useEffect, useState } from 'react'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; -import LIQUIFIER_TG_TOKEN_ABI from '../../../constants/liquidStaking/liquifierTgTokenAbi'; import { LsNetworkId } from '../../../constants/liquidStaking/types'; import useBalances from '../../../data/balances/useBalances'; import useParachainBalances from '../../../data/liquidStaking/parachain/useParachainBalances'; import useLsPoolBalance from '../../../data/liquidStaking/tangle/useLsPoolBalance'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import usePolling from '../../../data/liquidStaking/usePolling'; -import useContractReadOnce from '../../../data/liquifier/useContractReadOnce'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; -import useEvmAddress20 from '../../../hooks/useEvmAddress'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; type BalanceUpdater = ( @@ -48,16 +43,11 @@ const createBalanceStateUpdater = ( const useLsAgnosticBalance = (isNative: boolean) => { const activeAccountAddress = useActiveAccountAddress(); - const evmAddress20 = useEvmAddress20(); const { nativeBalances, liquidBalances } = useParachainBalances(); const { free: tangleFreeBalance } = useBalances(); const { selectedProtocolId, selectedNetworkId } = useLsStore(); const tangleAssetBalance = useLsPoolBalance(); - // TODO: Why not use the subscription hook variants (useContractRead) instead of manually utilizing usePolling? - const readErc20 = useContractReadOnce(erc20Abi); - const readTgToken = useContractReadOnce(LIQUIFIER_TG_TOKEN_ABI); - const [balance, setBalance] = useState< BN | null | typeof EMPTY_VALUE_PLACEHOLDER >(EMPTY_VALUE_PLACEHOLDER); @@ -80,44 +70,6 @@ const useLsAgnosticBalance = (isNative: boolean) => { } }, [isAccountConnected, isNative, selectedProtocolId]); - const erc20BalanceFetcher = useCallback(() => { - if ( - protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER || - evmAddress20 === null - ) { - return; - } - - const target = isNative ? readErc20 : readTgToken; - - // There is an account connected, but the target read contract - // function is not yet ready (ie. the public client is being re-created). - if (target === null) { - setBalance(createBalanceStateUpdater(null)); - - return; - } - - return target({ - address: isNative - ? protocol.erc20TokenAddress - : protocol.tgTokenContractAddress, - functionName: 'balanceOf', - args: [evmAddress20], - }).then((result) => { - if (result instanceof Error) { - return; - } - - setBalance(createBalanceStateUpdater(new BN(result.toString()))); - }); - }, [evmAddress20, isNative, protocol, readErc20, readTgToken]); - - const isRefreshing = usePolling({ - // Pause polling if there's no active account. - effect: isAccountConnected ? erc20BalanceFetcher : null, - }); - // Update balance to the parachain balance when the restaking // parachain is the active network. useEffect(() => { @@ -165,7 +117,7 @@ const useLsAgnosticBalance = (isNative: boolean) => { isNative, ]); - return { balance, isRefreshing }; + return balance; }; export default useLsAgnosticBalance; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts index 0a4691201..a63a506c1 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts @@ -7,8 +7,8 @@ import { LsProtocolId, } from '../../../constants/liquidStaking/types'; import useParachainLsFees from '../../../data/liquidStaking/parachain/useParachainLsFees'; -import useContractRead from '../../../data/liquifier/useContractRead'; -import { ContractReadOptions } from '../../../data/liquifier/useContractReadOnce'; +import useContractRead from '../../../data/evm/useContractRead'; +import { ContractReadOptions } from '../../../data/evm/useContractReadOnce'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; const useLsFeePercentage = ( diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts index ed3ef1933..52c7c1bd9 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts @@ -20,7 +20,7 @@ const useLsSpendingLimits = ( isNative: boolean, protocolId: LsProtocolId, ): LsSpendingLimits => { - const { balance } = useLsAgnosticBalance(isNative); + const balance = useLsAgnosticBalance(isNative); const { result: existentialDepositAmount } = useApi( useCallback((api) => api.consts.balances.existentialDeposit, []), diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index abc81f56c..4b9076e5a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -32,12 +32,8 @@ import { ParachainCurrency, } from '../../../constants/liquidStaking/types'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useLiquifierNftUnlocks, { - LiquifierUnlockNftMetadata, -} from '../../../data/liquifier/useLiquifierNftUnlocks'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import addCommasToNumber from '../../../utils/addCommasToNumber'; -import isLiquifierProtocolId from '../../../utils/liquidStaking/isLiquifierProtocolId'; import isLsParachainChainId from '../../../utils/liquidStaking/isLsParachainChainId'; import stringifyTimeUnit from '../../../utils/liquidStaking/stringifyTimeUnit'; import GlassCard from '../../GlassCard'; @@ -48,7 +44,6 @@ import TableRowsSkeleton from '../TableRowsSkeleton'; import RebondLstUnstakeRequestButton from './RebondLstUnstakeRequestButton'; import useLstUnlockRequestTableRows from './useLstUnlockRequestTableRows'; import WithdrawLstUnstakeRequestButton from './WithdrawLstUnstakeRequestButton'; -import WithdrawUnlockNftButton from './WithdrawUnlockNftButton'; export type BaseUnstakeRequest = { unlockId: number; @@ -75,9 +70,7 @@ export type ParachainUnstakeRequest = BaseUnstakeRequest & { progress?: LsParachainSimpleTimeUnit; }; -type UnstakeRequestTableRow = - | LiquifierUnlockNftMetadata - | ParachainUnstakeRequest; +type UnstakeRequestTableRow = ParachainUnstakeRequest; const COLUMN_HELPER = createColumnHelper(); @@ -165,15 +158,7 @@ const COLUMNS = [ const UnstakeRequestsTable: FC = () => { const { selectedProtocolId } = useLsStore(); const activeAccountAddress = useActiveAccountAddress(); - const parachainRows = useLstUnlockRequestTableRows(); - const liquifierRows = useLiquifierNftUnlocks(); - - // Select the table rows based on whether the selected protocol - // is an EVM-based chain (liquifier contract) or a parachain-based - // Substrate chain. - const rows = isLiquifierProtocolId(selectedProtocolId) - ? liquifierRows - : parachainRows; + const rows = useLstUnlockRequestTableRows(); const tableOptions = useMemo>( () => ({ @@ -288,16 +273,6 @@ const UnstakeRequestsTable: FC = () => { }); }, [selectedRows]); - const nftUnlockIds = useMemo(() => { - return selectedRows.flatMap((row) => { - if (row.original.type !== 'liquifierUnlockNft') { - return []; - } - - return [row.original.unlockId]; - }); - }, [selectedRows]); - const isDataState = rows !== null && rows.length > 0 && activeAccountAddress !== null; @@ -321,18 +296,12 @@ const UnstakeRequestsTable: FC = () => { )} {/* TODO: Assert that the id is either parachain or liquifier, if it isn't then we might need to hide this unstake requests table and show a specific one for Tangle networks (LS pools). */} - {isLsParachainChainId(selectedProtocolId) ? ( + {isLsParachainChainId(selectedProtocolId) && ( - ) : isLiquifierProtocolId(selectedProtocolId) ? ( - - ) : undefined} + )} )} diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawUnlockNftButton.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawUnlockNftButton.tsx deleted file mode 100644 index ea3f1df24..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawUnlockNftButton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Button } from '@webb-tools/webb-ui-components'; -import { FC, useCallback, useState } from 'react'; - -import { LsLiquifierProtocolId } from '../../../constants/liquidStaking/types'; -import useLiquifierWithdraw from '../../../data/liquifier/useLiquifierWithdraw'; - -type WithdrawUnlockNftButtonProps = { - tokenId: LsLiquifierProtocolId; - canWithdraw: boolean; - unlockIds: number[]; -}; - -const WithdrawUnlockNftButton: FC = ({ - tokenId, - canWithdraw, - unlockIds, -}) => { - const [isProcessing, setIsProcessing] = useState(false); - const withdraw = useLiquifierWithdraw(); - - const handleClick = useCallback(async () => { - if (withdraw === null) { - return; - } - - setIsProcessing(true); - await withdraw(tokenId, unlockIds); - setIsProcessing(false); - }, [tokenId, unlockIds, withdraw]); - - return ( - - ); -}; - -export default WithdrawUnlockNftButton; diff --git a/apps/tangle-dapp/constants/liquidStaking/constants.ts b/apps/tangle-dapp/constants/liquidStaking/constants.ts index ee460ac6e..d3ebff5c2 100644 --- a/apps/tangle-dapp/constants/liquidStaking/constants.ts +++ b/apps/tangle-dapp/constants/liquidStaking/constants.ts @@ -12,8 +12,6 @@ import TANGLE_TESTNET from '../../data/liquidStaking/adapters/tangleTestnet'; import THE_GRAPH from '../../data/liquidStaking/adapters/theGraph'; import { IS_PRODUCTION_ENV } from '../env'; import { - LsLiquifierProtocolDef, - LsLiquifierProtocolId, LsNetwork, LsNetworkId, LsParachainChainDef, @@ -40,36 +38,20 @@ export const LS_PARACHAIN_CHAIN_MAP: Record< [LsProtocolId.MANTA]: MANTA, } as Record; -export const LS_LIQUIFIER_PROTOCOL_MAP: Record< - LsLiquifierProtocolId, - LsLiquifierProtocolDef -> = { - [LsProtocolId.CHAINLINK]: CHAINLINK, - [LsProtocolId.THE_GRAPH]: THE_GRAPH, - [LsProtocolId.LIVEPEER]: LIVEPEER, - [LsProtocolId.POLYGON]: POLYGON, -}; - export const LS_PROTOCOLS: LsProtocolDef[] = [ ...Object.values(LS_PARACHAIN_CHAIN_MAP), - ...Object.values(LS_LIQUIFIER_PROTOCOL_MAP), TANGLE_MAINNET, TANGLE_TESTNET, TANGLE_LOCAL, ]; -export const LS_LIQUIFIER_PROTOCOL_IDS = [ - LsProtocolId.CHAINLINK, - LsProtocolId.THE_GRAPH, - LsProtocolId.LIVEPEER, - LsProtocolId.POLYGON, -] as const satisfies LsLiquifierProtocolId[]; - -export const LS_PARACHAIN_CHAIN_IDS = Object.values(LsProtocolId).filter( - (value): value is LsParachainChainId => - typeof value !== 'string' && - !LS_LIQUIFIER_PROTOCOL_IDS.includes(value as LsLiquifierProtocolId), -) satisfies LsParachainChainId[]; +export const LS_PARACHAIN_PROTOCOL_IDS = [ + LsProtocolId.ASTAR, + LsProtocolId.PHALA, + LsProtocolId.MANTA, + LsProtocolId.MOONBEAM, + LsProtocolId.POLKADOT, +] as const satisfies LsParachainChainId[]; export const LS_PARACHAIN_TOKENS = [ LsToken.DOT, diff --git a/apps/tangle-dapp/constants/liquidStaking/liquifierAbi.ts b/apps/tangle-dapp/constants/liquidStaking/liquifierAbi.ts deleted file mode 100644 index af9ab6971..000000000 --- a/apps/tangle-dapp/constants/liquidStaking/liquifierAbi.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Abi } from 'viem'; - -const LIQUIFIER_ABI = [ - { - type: 'constructor', - stateMutability: 'nonpayable', - inputs: [ - { internalType: 'address', name: '_registry', type: 'address' }, - { internalType: 'address', name: '_unlocks', type: 'address' }, - ], - }, - { - type: 'function', - name: 'deposit', - inputs: [ - { internalType: 'address', name: 'receiver', type: 'address' }, - { internalType: 'uint256', name: 'assets', type: 'uint256' }, - ], - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'unlock', - inputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }], - outputs: [{ internalType: 'uint256', name: 'unlockID', type: 'uint256' }], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'withdraw', - inputs: [ - { internalType: 'address', name: 'receiver', type: 'address' }, - { internalType: 'uint256', name: 'unlockID', type: 'uint256' }, - ], - outputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'rebase', - inputs: [], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'name', - inputs: [], - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'symbol', - inputs: [], - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'transfer', - inputs: [ - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'transferFrom', - inputs: [ - { internalType: 'address', name: 'from', type: 'address' }, - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'previewDeposit', - inputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }], - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'previewWithdraw', - inputs: [{ internalType: 'uint256', name: 'unlockID', type: 'uint256' }], - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'unlockMaturity', - inputs: [{ internalType: 'uint256', name: 'unlockID', type: 'uint256' }], - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - }, - { - type: 'event', - name: 'Deposit', - inputs: [ - { - indexed: false, - internalType: 'address', - name: 'from', - type: 'address', - }, - { indexed: false, internalType: 'address', name: 'to', type: 'address' }, - { - indexed: false, - internalType: 'uint256', - name: 'assets', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'tgTokenOut', - type: 'uint256', - }, - ], - }, - { - type: 'event', - name: 'Unlock', - inputs: [ - { - indexed: false, - internalType: 'address', - name: 'from', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'assets', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'unlockID', - type: 'uint256', - }, - ], - }, - { - type: 'event', - name: 'Withdraw', - inputs: [ - { indexed: false, internalType: 'address', name: 'to', type: 'address' }, - { - indexed: false, - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'unlockID', - type: 'uint256', - }, - ], - }, - { - type: 'event', - name: 'Rebase', - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'currentStake', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'newStake', - type: 'uint256', - }, - ], - }, -] as const satisfies Abi; - -export default LIQUIFIER_ABI; diff --git a/apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts b/apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts deleted file mode 100644 index 92e37c7ba..000000000 --- a/apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { Abi } from 'viem'; - -const LIQUIFIER_ADAPTER_ABI = [ - { - constant: false, - inputs: [ - { - name: '_operator', - type: 'address', - }, - ], - name: 'addVault', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'currentTime', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'getMaxDeposits', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'getMinDeposits', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'getVaultDepositLimits', - outputs: [ - { - name: '', - type: 'uint256', - }, - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: '_token', - type: 'address', - }, - { - name: '_stakingPool', - type: 'address', - }, - { - name: '_stakeController', - type: 'address', - }, - { - name: '_vaultImplementation', - type: 'address', - }, - { - name: '_minDepositThreshold', - type: 'uint256', - }, - { - name: '_fees', - type: 'tuple[]', - }, - { - name: '_initialVaults', - type: 'address[]', - }, - ], - name: 'initialize', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: '_index', - type: 'uint256', - }, - { - name: '_operator', - type: 'address', - }, - ], - name: 'setOperator', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - { - name: 'amount', - type: 'uint256', - }, - ], - name: 'stake', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - { - name: 'currentStake', - type: 'uint256', - }, - ], - name: 'rebase', - outputs: [ - { - name: 'newStake', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [ - { - name: 'interfaceId', - type: 'bytes4', - }, - ], - name: 'supportsInterface', - outputs: [ - { - name: '', - type: 'bool', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [ - { - name: 'validator', - type: 'address', - }, - ], - name: 'isValidator', - outputs: [ - { - name: '', - type: 'bool', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [ - { - name: 'validator', - type: 'address', - }, - { - name: 'assets', - type: 'uint256', - }, - ], - name: 'previewDeposit', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'pure', - type: 'function', - }, - { - constant: true, - inputs: [ - { - name: 'validator', - type: 'uint256', - }, - ], - name: 'previewWithdraw', - outputs: [ - { - name: 'amount', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'unlockTime', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - { - name: 'amount', - type: 'uint256', - }, - ], - name: 'unstake', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - { - name: 'amount', - type: 'uint256', - }, - ], - name: 'withdraw', - outputs: [ - { - name: 'amount', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - name: 'operator', - type: 'address', - }, - ], - name: 'VaultAdded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - name: 'depositedAmount', - type: 'uint256', - }, - ], - name: 'DepositBufferedTokens', - type: 'event', - }, - { - name: 'totalShares', - type: 'function', - inputs: [], - stateMutability: 'view', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - }, -] as const satisfies Abi; - -export default LIQUIFIER_ADAPTER_ABI; diff --git a/apps/tangle-dapp/constants/liquidStaking/liquifierRegistryAbi.ts b/apps/tangle-dapp/constants/liquidStaking/liquifierRegistryAbi.ts deleted file mode 100644 index 4844bc35d..000000000 --- a/apps/tangle-dapp/constants/liquidStaking/liquifierRegistryAbi.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { Abi } from 'viem'; - -const LIQUIFIER_REGISTRY_ABI = [ - // Constructor - { - type: 'constructor', - stateMutability: 'nonpayable', - inputs: [], - }, - - // Initialize function - { - type: 'function', - name: 'initialize', - inputs: [ - { internalType: 'address', name: '_liquifier', type: 'address' }, - { internalType: 'address', name: '_unlocks', type: 'address' }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - - // Getters - { - type: 'function', - name: 'adapter', - inputs: [{ internalType: 'address', name: 'asset', type: 'address' }], - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'liquifier', - inputs: [], - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'treasury', - inputs: [], - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'unlocks', - inputs: [], - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'fee', - inputs: [{ internalType: 'address', name: 'asset', type: 'address' }], - outputs: [{ internalType: 'uint96', name: '', type: 'uint96' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'isLiquifier', - inputs: [{ internalType: 'address', name: 'liquifier', type: 'address' }], - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - }, - { - type: 'function', - name: 'getLiquifier', - inputs: [ - { internalType: 'address', name: 'asset', type: 'address' }, - { internalType: 'address', name: 'validator', type: 'address' }, - ], - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - }, - - // Setters - { - type: 'function', - name: 'registerAdapter', - inputs: [ - { internalType: 'address', name: 'asset', type: 'address' }, - { internalType: 'address', name: 'adapter', type: 'address' }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'registerLiquifier', - inputs: [ - { internalType: 'address', name: 'asset', type: 'address' }, - { internalType: 'address', name: 'validator', type: 'address' }, - { internalType: 'address', name: 'liquifier', type: 'address' }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'setFee', - inputs: [ - { internalType: 'address', name: 'asset', type: 'address' }, - { internalType: 'uint96', name: 'fee', type: 'uint96' }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'setTreasury', - inputs: [{ internalType: 'address', name: 'treasury', type: 'address' }], - outputs: [], - stateMutability: 'nonpayable', - }, - - // Required by UUPSUpgradeable - { - type: 'function', - name: '_authorizeUpgrade', - inputs: [{ internalType: 'address', name: '', type: 'address' }], - outputs: [], - stateMutability: 'nonpayable', - }, - - // Events - { - type: 'event', - name: 'AdapterRegistered', - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'asset', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'adapter', - type: 'address', - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'NewLiquifier', - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'asset', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'validator', - type: 'address', - }, - { - indexed: false, - internalType: 'address', - name: 'liquifier', - type: 'address', - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'FeeAdjusted', - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'asset', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'newFee', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'oldFee', - type: 'uint256', - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'TreasurySet', - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'treasury', - type: 'address', - }, - ], - anonymous: false, - }, -] as const satisfies Abi; - -export default LIQUIFIER_REGISTRY_ABI; diff --git a/apps/tangle-dapp/constants/liquidStaking/liquifierTgTokenAbi.ts b/apps/tangle-dapp/constants/liquidStaking/liquifierTgTokenAbi.ts deleted file mode 100644 index 1bea1272e..000000000 --- a/apps/tangle-dapp/constants/liquidStaking/liquifierTgTokenAbi.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { Abi } from 'viem'; - -const LIQUIFIER_TG_TOKEN_ABI = [ - { - inputs: [], - name: 'decimals', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], - stateMutability: 'pure', - type: 'function', - }, - { - inputs: [], - name: 'name', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'symbol', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'shares', - type: 'uint256', - }, - ], - name: 'convertToAssets', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'assets', - type: 'uint256', - }, - ], - name: 'convertToShares', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'account', - type: 'address', - }, - ], - name: 'balanceOf', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - ], - name: 'nonces', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'approve', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'to', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'transfer', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - ], - name: 'allowance', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'from', - type: 'address', - }, - { - internalType: 'address', - name: 'to', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'transferFrom', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'deadline', - type: 'uint256', - }, - { - internalType: 'uint8', - name: 'v', - type: 'uint8', - }, - { - internalType: 'bytes32', - name: 'r', - type: 'bytes32', - }, - { - internalType: 'bytes32', - name: 's', - type: 'bytes32', - }, - ], - name: 'permit', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'DOMAIN_SEPARATOR', - outputs: [ - { - internalType: 'bytes32', - name: '', - type: 'bytes32', - }, - ], - stateMutability: 'view', - type: 'function', - }, -] as const satisfies Abi; - -export default LIQUIFIER_TG_TOKEN_ABI; diff --git a/apps/tangle-dapp/constants/liquidStaking/liquifierUnlocksAbi.ts b/apps/tangle-dapp/constants/liquidStaking/liquifierUnlocksAbi.ts deleted file mode 100644 index e0b5071b3..000000000 --- a/apps/tangle-dapp/constants/liquidStaking/liquifierUnlocksAbi.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { Abi, erc721Abi } from 'viem'; - -const LIQUIFIER_UNLOCKS_ABI = [ - ...erc721Abi, - { - inputs: [ - { - internalType: 'address', - name: '_registry', - type: 'address', - }, - { - internalType: 'address', - name: '_renderer', - type: 'address', - }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [ - { - internalType: 'address', - name: 'receiver', - type: 'address', - }, - { - internalType: 'uint256', - name: 'unlockId', - type: 'uint256', - }, - ], - name: 'createUnlock', - outputs: [ - { - internalType: 'uint256', - name: 'tokenId', - type: 'uint256', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - internalType: 'uint256', - name: 'unlockId', - type: 'uint256', - }, - ], - name: 'useUnlock', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'tokenId', - type: 'uint256', - }, - ], - name: 'tokenURI', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'tokenId', - type: 'uint256', - }, - ], - name: 'getMetadata', - outputs: [ - { - components: [ - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'maturity', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'progress', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'unlockId', - type: 'uint256', - }, - { - internalType: 'string', - name: 'symbol', - type: 'string', - }, - { - internalType: 'string', - name: 'name', - type: 'string', - }, - { - internalType: 'address', - name: 'validator', - type: 'address', - }, - ], - internalType: 'struct Metadata', - name: 'metadata', - type: 'tuple', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'sender', - type: 'address', - }, - ], - name: '_isValidLiquifier', - outputs: [], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'liquifier', - type: 'address', - }, - { - internalType: 'uint96', - name: 'unlockId', - type: 'uint96', - }, - ], - name: '_encodeTokenId', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'pure', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'tokenId', - type: 'uint256', - }, - ], - name: '_decodeTokenId', - outputs: [ - { - internalType: 'address payable', - name: 'liquifier', - type: 'address', - }, - { - internalType: 'uint96', - name: 'unlockId', - type: 'uint96', - }, - ], - stateMutability: 'pure', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'tokenId', - type: 'uint256', - }, - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - internalType: 'address', - name: 'sender', - type: 'address', - }, - ], - name: 'NotOwnerOf', - type: 'error', - }, - { - inputs: [ - { - internalType: 'address', - name: 'sender', - type: 'address', - }, - ], - name: 'NotLiquifier', - type: 'error', - }, - { - inputs: [], - name: 'InvalidID', - type: 'error', - }, -] as const satisfies Abi; - -export default LIQUIFIER_UNLOCKS_ABI; diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 7e1a26d5e..dc392d3e1 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -4,7 +4,6 @@ import { TanglePrimitivesTimeUnit, } from '@polkadot/types/lookup'; import { BN } from '@polkadot/util'; -import { HexString } from '@polkadot/util/types'; import { TANGLE_LOCAL_DEV_NETWORK, TANGLE_MAINNET_NETWORK, @@ -34,12 +33,6 @@ export enum LsProtocolId { TANGLE_LOCAL, } -export type LsLiquifierProtocolId = - | LsProtocolId.CHAINLINK - | LsProtocolId.THE_GRAPH - | LsProtocolId.LIVEPEER - | LsProtocolId.POLYGON; - export type LsParachainChainId = | LsProtocolId.POLKADOT | LsProtocolId.PHALA @@ -60,18 +53,8 @@ export enum LsToken { PHALA = 'PHALA', TNT = 'TNT', TTNT = 'tTNT', - LINK = 'LINK', - GRT = 'GRT', - LPT = 'LPT', - POL = 'POL', } -export type LsLiquifierProtocolToken = - | LsToken.LINK - | LsToken.GRT - | LsToken.LPT - | LsToken.POL; - export type LsParachainToken = | LsToken.DOT | LsToken.GLMR @@ -122,20 +105,7 @@ export interface LsParachainChainDef adapter: LsNetworkEntityAdapter; } -export interface LsLiquifierProtocolDef extends ProtocolDefCommon { - networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER; - id: LsLiquifierProtocolId; - token: LsLiquifierProtocolToken; - erc20TokenAddress: HexString; - liquifierContractAddress: HexString; - tgTokenContractAddress: HexString; - unlocksContractAddress: HexString; -} - -export type LsProtocolDef = - | LsParachainChainDef - | LsLiquifierProtocolDef - | LsTangleNetworkDef; +export type LsProtocolDef = LsParachainChainDef | LsTangleNetworkDef; export type LsCardSearchParams = { amount: BN; diff --git a/apps/tangle-dapp/data/liquifier/useContractRead.ts b/apps/tangle-dapp/data/evm/useContractRead.ts similarity index 100% rename from apps/tangle-dapp/data/liquifier/useContractRead.ts rename to apps/tangle-dapp/data/evm/useContractRead.ts diff --git a/apps/tangle-dapp/data/liquifier/useContractReadBatch.ts b/apps/tangle-dapp/data/evm/useContractReadBatch.ts similarity index 100% rename from apps/tangle-dapp/data/liquifier/useContractReadBatch.ts rename to apps/tangle-dapp/data/evm/useContractReadBatch.ts diff --git a/apps/tangle-dapp/data/liquifier/useContractReadOnce.ts b/apps/tangle-dapp/data/evm/useContractReadOnce.ts similarity index 100% rename from apps/tangle-dapp/data/liquifier/useContractReadOnce.ts rename to apps/tangle-dapp/data/evm/useContractReadOnce.ts diff --git a/apps/tangle-dapp/data/liquifier/useContractWrite.ts b/apps/tangle-dapp/data/evm/useContractWrite.ts similarity index 100% rename from apps/tangle-dapp/data/liquifier/useContractWrite.ts rename to apps/tangle-dapp/data/evm/useContractWrite.ts diff --git a/apps/tangle-dapp/data/liquifier/useContractWriteBatch.ts b/apps/tangle-dapp/data/evm/useContractWriteBatch.ts similarity index 100% rename from apps/tangle-dapp/data/liquifier/useContractWriteBatch.ts rename to apps/tangle-dapp/data/evm/useContractWriteBatch.ts diff --git a/apps/tangle-dapp/data/liquifier/useViemPublicClientWithChain.ts b/apps/tangle-dapp/data/evm/useViemPublicClientWithChain.ts similarity index 100% rename from apps/tangle-dapp/data/liquifier/useViemPublicClientWithChain.ts rename to apps/tangle-dapp/data/evm/useViemPublicClientWithChain.ts diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/chainlink.ts b/apps/tangle-dapp/data/liquidStaking/adapters/chainlink.ts deleted file mode 100644 index 5da8814f9..000000000 --- a/apps/tangle-dapp/data/liquidStaking/adapters/chainlink.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IS_PRODUCTION_ENV } from '../../../constants/env'; -import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants'; -import { - LsLiquifierProtocolDef, - LsNetworkId, - LsProtocolId, - LsToken, -} from '../../../constants/liquidStaking/types'; -import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; - -const CHAINLINK = { - networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER, - id: LsProtocolId.CHAINLINK, - name: 'Chainlink', - chainIconFileName: 'chainlink', - token: LsToken.LINK, - decimals: 18, - erc20TokenAddress: IS_PRODUCTION_ENV - ? '0x514910771AF9Ca656af840dff83E8264EcF986CA' - : SEPOLIA_TESTNET_CONTRACTS.ERC20, - liquifierContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER, - tgTokenContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN, - unlocksContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS, - timeUnit: CrossChainTimeUnit.DAY, - unstakingPeriod: 7, -} as const satisfies LsLiquifierProtocolDef; - -export default CHAINLINK; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/livepeer.ts b/apps/tangle-dapp/data/liquidStaking/adapters/livepeer.ts deleted file mode 100644 index 4797c8652..000000000 --- a/apps/tangle-dapp/data/liquidStaking/adapters/livepeer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IS_PRODUCTION_ENV } from '../../../constants/env'; -import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants'; -import { - LsLiquifierProtocolDef, - LsNetworkId, - LsProtocolId, - LsToken, -} from '../../../constants/liquidStaking/types'; -import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; - -const LIVEPEER = { - networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER, - id: LsProtocolId.LIVEPEER, - name: 'Livepeer', - chainIconFileName: 'livepeer', - token: LsToken.LPT, - decimals: 18, - erc20TokenAddress: IS_PRODUCTION_ENV - ? '0x58b6A8A3302369DAEc383334672404Ee733aB239' - : SEPOLIA_TESTNET_CONTRACTS.ERC20, - liquifierContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER, - tgTokenContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN, - unlocksContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS, - timeUnit: CrossChainTimeUnit.LIVEPEER_ROUND, - unstakingPeriod: 7, -} as const satisfies LsLiquifierProtocolDef; - -export default LIVEPEER; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/polygon.ts b/apps/tangle-dapp/data/liquidStaking/adapters/polygon.ts deleted file mode 100644 index 574442fa6..000000000 --- a/apps/tangle-dapp/data/liquidStaking/adapters/polygon.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IS_PRODUCTION_ENV } from '../../../constants/env'; -import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants'; -import { - LsLiquifierProtocolDef, - LsNetworkId, - LsProtocolId, - LsToken, -} from '../../../constants/liquidStaking/types'; -import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; - -const POLYGON = { - networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER, - id: LsProtocolId.POLYGON, - name: 'Polygon', - chainIconFileName: 'polygon', - token: LsToken.POL, - decimals: 18, - erc20TokenAddress: IS_PRODUCTION_ENV - ? '0x0D500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' - : SEPOLIA_TESTNET_CONTRACTS.ERC20, - liquifierContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER, - tgTokenContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN, - unlocksContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS, - timeUnit: CrossChainTimeUnit.POLYGON_CHECKPOINT, - unstakingPeriod: 82, -} as const satisfies LsLiquifierProtocolDef; - -export default POLYGON; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/theGraph.ts b/apps/tangle-dapp/data/liquidStaking/adapters/theGraph.ts deleted file mode 100644 index b5a126efe..000000000 --- a/apps/tangle-dapp/data/liquidStaking/adapters/theGraph.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IS_PRODUCTION_ENV } from '../../../constants/env'; -import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants'; -import { - LsLiquifierProtocolDef, - LsNetworkId, - LsProtocolId, - LsToken, -} from '../../../constants/liquidStaking/types'; -import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; - -const THE_GRAPH = { - networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER, - id: LsProtocolId.THE_GRAPH, - name: 'The Graph', - chainIconFileName: 'the-graph', - token: LsToken.GRT, - decimals: 18, - erc20TokenAddress: IS_PRODUCTION_ENV - ? '0xc944E90C64B2c07662A292be6244BDf05Cda44a7' - : SEPOLIA_TESTNET_CONTRACTS.ERC20, - liquifierContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER, - tgTokenContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN, - unlocksContractAddress: IS_PRODUCTION_ENV - ? '0x' - : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS, - timeUnit: CrossChainTimeUnit.DAY, - unstakingPeriod: 28, -} as const satisfies LsLiquifierProtocolDef; - -export default THE_GRAPH; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts index 9df9b0a57..90ce55ed7 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts @@ -12,8 +12,8 @@ import { import useApiRx from '../../hooks/useApiRx'; import calculateBnRatio from '../../utils/calculateBnRatio'; import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef'; -import useContractRead from '../liquifier/useContractRead'; -import { ContractReadOptions } from '../liquifier/useContractReadOnce'; +import useContractRead from '../evm/useContractRead'; +import { ContractReadOptions } from '../evm/useContractReadOnce'; import { useLsStore } from './useLsStore'; import usePolling from './usePolling'; diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts b/apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts deleted file mode 100644 index ed1aea4f3..000000000 --- a/apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { BN } from '@polkadot/util'; -import assert from 'assert'; -import { useCallback } from 'react'; -import { erc20Abi } from 'viem'; - -import { TxName } from '../../constants'; -import { LS_LIQUIFIER_PROTOCOL_MAP } from '../../constants/liquidStaking/constants'; -import LIQUIFIER_ABI from '../../constants/liquidStaking/liquifierAbi'; -import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types'; -import useEvmAddress20 from '../../hooks/useEvmAddress'; -import useContractWrite from './useContractWrite'; - -/** - * 0. Token.balanceOf(sender) // Obtain the user's token balance (eg. LINK). - * - * 0. Liquifier.balanceOf(sender) // Obtain the user's liquid staking balance (eg. tgLINK). - * - * 1. Token.approve(address(liquifier), depositAmount) // Approve the Liquifier to spend the deposit amount. - * - * 2. Liquifier.deposit(sender, depositAmount) // Deposit the amount to the Liquifier. This is equivalent to calling `stake` or `mint` in Liquid Staking. - * - * Note: The balance of the token is obtained through the token's ERC20 contract. Would need to acquire the contract addresses for each token. - * - * Note: Liquifier.deposit() is invoked through viem.writeContract(), with the address of the Liquifier contract and the deposit amount as arguments. - * - * Note: The liquifier contract's address depends on the selected token. For example, for LINK, the contract address used would be the Chainlink Liquifier Adapter contract. - * - * See more: https://github.com/webb-tools/tnt-core/blob/1f371959884352e7af68e6091c5bb330fcaa58b8/script/XYZ_Stake.s.sol - */ -const useLiquifierDeposit = () => { - const activeEvmAddress20 = useEvmAddress20(); - const writeChainlinkErc20 = useContractWrite(erc20Abi); - const writeLiquifier = useContractWrite(LIQUIFIER_ABI); - - const isReady = - writeLiquifier !== null && - writeChainlinkErc20 !== null && - activeEvmAddress20 !== null; - - const deposit = useCallback( - async (tokenId: LsLiquifierProtocolId, amount: BN) => { - // TODO: Should the user balance check be done here or assume that the consumer of the hook will handle that? - - assert( - isReady, - 'Should not be able to call this function if the requirements are not ready yet', - ); - - const tokenDef = LS_LIQUIFIER_PROTOCOL_MAP[tokenId]; - - // TODO: Check for approval first, in case that it has already been granted. This prevents another unnecessary approval transaction (ex. if the transaction fails after the approval but before the deposit). - // Approve spending the token amount by the Liquifier contract. - const approveTxSucceeded = await writeChainlinkErc20({ - txName: TxName.LS_LIQUIFIER_APPROVE, - address: tokenDef.erc20TokenAddress, - functionName: 'approve', - args: [tokenDef.liquifierContractAddress, BigInt(amount.toString())], - notificationStep: { current: 1, total: 2 }, - }); - - if (!approveTxSucceeded) { - return false; - } - - const depositTxSucceeded = await writeLiquifier({ - txName: TxName.LS_LIQUIFIER_DEPOSIT, - // TODO: Does the adapter contract have a deposit function? It doesn't seem like so. In that case, will need to update the way that Liquifier contract's address is handled. - address: tokenDef.liquifierContractAddress, - functionName: 'deposit', - // TODO: Provide the first arg. (validator). Need to figure out how it works on Chainlink (vaults? single address?). See: https://github.com/webb-tools/tnt-core/blob/21c158d6cb11e2b5f50409d377431e7cd51ff72f/src/lst/adapters/ChainlinkAdapter.sol#L187 - args: [activeEvmAddress20, BigInt(amount.toString())], - notificationStep: { current: 2, total: 2 }, - }); - - return depositTxSucceeded; - }, - [activeEvmAddress20, isReady, writeChainlinkErc20, writeLiquifier], - ); - - // Wait for the requirements to be ready before - // returning the deposit function. - return !isReady ? null : deposit; -}; - -export default useLiquifierDeposit; diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts b/apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts deleted file mode 100644 index 90fb0e76c..000000000 --- a/apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { assert, BN } from '@polkadot/util'; -import { useCallback, useMemo } from 'react'; -import { Address } from 'viem'; - -import { BaseUnstakeRequest } from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; -import { IS_PRODUCTION_ENV } from '../../constants/env'; -import LIQUIFIER_UNLOCKS_ABI from '../../constants/liquidStaking/liquifierUnlocksAbi'; -import { LsNetworkId } from '../../constants/liquidStaking/types'; -import useEvmAddress20 from '../../hooks/useEvmAddress'; -import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef'; -import { useLsStore } from '../liquidStaking/useLsStore'; -import useContractRead from './useContractRead'; -import useContractReadBatch, { - ContractReadOptionsBatch, -} from './useContractReadBatch'; -import { ContractReadOptions } from './useContractReadOnce'; - -/** - * Represents the metadata of an ERC-721 NFT liquifier unlock request. - * - * See: https://github.com/webb-tools/tnt-core/blob/21c158d6cb11e2b5f50409d377431e7cd51ff72f/src/lst/unlocks/Unlocks.sol#L21 - */ -export type LiquifierUnlockNftMetadata = BaseUnstakeRequest & { - type: 'liquifierUnlockNft'; - symbol: string; - name: string; - validator: Address; - - /** - * How far the unlock request has progressed, in percentage (e.g. - * `0.5` for 50%). - */ - progress: number; - - /** - * A timestamp representing the date at which the unlock request - * can be fulfilled. - */ - maturityTimestamp: number; -}; - -/** - * In the case of liquifier unlock requests, they are represented - * by ERC-721 NFTs owned by the user. - * - * Each unlock NFT has associated metadata about the unlock request, - * including the progress of the unlock request, the amount of underlying - * stake tokens, and the maturity timestamp. - */ -const useLiquifierNftUnlocks = (): LiquifierUnlockNftMetadata[] | null => { - const { selectedProtocolId } = useLsStore(); - const activeEvmAddress20 = useEvmAddress20(); - - const protocol = getLsProtocolDef(selectedProtocolId); - - const getUnlockIdCountOptions = useCallback((): ContractReadOptions< - typeof LIQUIFIER_UNLOCKS_ABI, - 'balanceOf' - > | null => { - if ( - activeEvmAddress20 === null || - protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER - ) { - return null; - } - - return { - address: protocol.unlocksContractAddress, - functionName: 'balanceOf', - args: [activeEvmAddress20], - }; - }, [activeEvmAddress20, protocol]); - - const { value: rawUnlockIdCount } = useContractRead( - LIQUIFIER_UNLOCKS_ABI, - getUnlockIdCountOptions, - ); - - const unlockIds = useMemo(() => { - if (rawUnlockIdCount === null || rawUnlockIdCount instanceof Error) { - return null; - } - - // Extremely unlikely that the user would have this many unlock - // requests, but just in case. - assert( - rawUnlockIdCount <= Number.MAX_SAFE_INTEGER, - 'Unlock ID count exceeds maximum safe integer, user seems to have an unreasonable amount of unlock requests', - ); - - const unlockIdCount = Number(rawUnlockIdCount); - - return Array.from({ - length: unlockIdCount, - }).map((_, i) => BigInt(i)); - }, [rawUnlockIdCount]); - - const getMetadataOptions = useCallback((): ContractReadOptionsBatch< - typeof LIQUIFIER_UNLOCKS_ABI, - 'getMetadata' - > | null => { - if ( - // Do not fetch if there's no active EVM account. - activeEvmAddress20 === null || - protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER || - unlockIds === null - ) { - return null; - } - - const batchArgs = unlockIds.map((unlockId) => [BigInt(unlockId)] as const); - - return { - address: protocol.unlocksContractAddress, - functionName: 'getMetadata', - args: batchArgs, - }; - }, [activeEvmAddress20, protocol, unlockIds]); - - const { results: rawMetadatas } = useContractReadBatch( - LIQUIFIER_UNLOCKS_ABI, - getMetadataOptions, - ); - - const metadatas = useMemo(() => { - if (rawMetadatas === null) { - return null; - } - - return rawMetadatas.flatMap((metadata, index) => { - // Ignore failed metadata fetches and those that are still loading. - if (metadata === null || metadata instanceof Error) { - return []; - } - - // The Sepolia development contract always returns 0 for the - // unlock ID. Use the index number to differentiate between - // different unlock requests. - const unlockId = IS_PRODUCTION_ENV ? Number(metadata.unlockId) : index; - - // On development, mark some as completed for testing purposes. - const progress = IS_PRODUCTION_ENV - ? Number(metadata.progress) / 100 - : index < 10 - ? 1 - : 0.6123; - - return { - type: 'liquifierUnlockNft', - decimals: protocol.decimals, - unlockId, - symbol: metadata.symbol, - name: metadata.name, - validator: metadata.validator, - progress, - amount: new BN(metadata.amount.toString()), - maturityTimestamp: Number(metadata.maturity), - } satisfies LiquifierUnlockNftMetadata; - }); - }, [protocol.decimals, rawMetadatas]); - - return metadatas; -}; - -export default useLiquifierNftUnlocks; diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts b/apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts deleted file mode 100644 index cf7dcb317..000000000 --- a/apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { BN } from '@polkadot/util'; -import assert from 'assert'; -import { useCallback } from 'react'; - -import { TxName } from '../../constants'; -import { LS_LIQUIFIER_PROTOCOL_MAP } from '../../constants/liquidStaking/constants'; -import LIQUIFIER_ABI from '../../constants/liquidStaking/liquifierAbi'; -import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types'; -import useEvmAddress20 from '../../hooks/useEvmAddress'; -import useContractWrite from './useContractWrite'; - -const useLiquifierUnlock = () => { - const activeEvmAddress20 = useEvmAddress20(); - const writeLiquifier = useContractWrite(LIQUIFIER_ABI); - - const isReady = writeLiquifier !== null && activeEvmAddress20 !== null; - - const unlock = useCallback( - async (tokenId: LsLiquifierProtocolId, amount: BN): Promise => { - // TODO: Should the user balance check be done here or assume that the consumer of the hook will handle that? - - assert( - isReady, - 'Should not be able to call this function if the requirements are not ready yet', - ); - - const tokenDef = LS_LIQUIFIER_PROTOCOL_MAP[tokenId]; - - return writeLiquifier({ - txName: TxName.LS_LIQUIFIER_UNLOCK, - // TODO: Does the adapter contract have a unlock function? It doesn't seem like so. In that case, will need to update the way that Liquifier contract's address is handled. - address: tokenDef.liquifierContractAddress, - functionName: 'unlock', - args: [BigInt(amount.toString())], - }); - }, - [isReady, writeLiquifier], - ); - - // Wait for the requirements to be ready before - // returning the unlock function. - return !isReady ? null : unlock; -}; - -export default useLiquifierUnlock; diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierWithdraw.ts b/apps/tangle-dapp/data/liquifier/useLiquifierWithdraw.ts deleted file mode 100644 index 46b71282c..000000000 --- a/apps/tangle-dapp/data/liquifier/useLiquifierWithdraw.ts +++ /dev/null @@ -1,56 +0,0 @@ -import assert from 'assert'; -import { useCallback } from 'react'; - -import { TxName } from '../../constants'; -import { LS_LIQUIFIER_PROTOCOL_MAP } from '../../constants/liquidStaking/constants'; -import LIQUIFIER_ABI from '../../constants/liquidStaking/liquifierAbi'; -import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types'; -import useEvmAddress20 from '../../hooks/useEvmAddress'; -import { NotificationSteps } from '../../hooks/useTxNotification'; -import useContractWriteBatch from './useContractWriteBatch'; - -const useLiquifierWithdraw = () => { - const activeEvmAddress20 = useEvmAddress20(); - const writeLiquifierBatch = useContractWriteBatch(LIQUIFIER_ABI); - - const isReady = writeLiquifierBatch !== null && activeEvmAddress20 !== null; - - const withdraw = useCallback( - async ( - tokenId: LsLiquifierProtocolId, - unlockIds: number[], - notificationStep?: NotificationSteps, - ) => { - // TODO: Should the user balance check be done here or assume that the consumer of the hook will handle that? - - assert( - isReady, - 'Should not be able to call this function if the requirements are not ready yet', - ); - - const tokenDef = LS_LIQUIFIER_PROTOCOL_MAP[tokenId]; - - const batchArgs = unlockIds.map( - (unlockId) => [activeEvmAddress20, BigInt(unlockId)] as const, - ); - - const withdrawTxSucceeded = await writeLiquifierBatch({ - txName: TxName.LS_LIQUIFIER_WITHDRAW, - // TODO: Does the adapter contract have a deposit function? It doesn't seem like so. In that case, will need to update the way that Liquifier contract's address is handled. - address: tokenDef.liquifierContractAddress, - functionName: 'withdraw', - args: batchArgs, - notificationStep, - }); - - return withdrawTxSucceeded; - }, - [activeEvmAddress20, isReady, writeLiquifierBatch], - ); - - // Wait for the requirements to be ready before - // returning the withdraw function. - return !isReady ? null : withdraw; -}; - -export default useLiquifierWithdraw; diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts index a47aade40..92a7296a9 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts @@ -2,12 +2,10 @@ import assert from 'assert'; import { LS_PROTOCOLS } from '../../constants/liquidStaking/constants'; import { - LsLiquifierProtocolId, LsTangleNetworkDef, LsTangleNetworkId, } from '../../constants/liquidStaking/types'; import { - LsLiquifierProtocolDef, LsParachainChainDef, LsParachainChainId, LsProtocolId, @@ -17,9 +15,7 @@ type IdToDefMap = T extends LsParachainChainId ? LsParachainChainDef : T extends LsTangleNetworkId ? LsTangleNetworkDef - : T extends LsLiquifierProtocolId - ? LsLiquifierProtocolDef - : never; + : never; const getLsProtocolDef = (id: T): IdToDefMap => { const result = LS_PROTOCOLS.find((def) => def.id === id); diff --git a/apps/tangle-dapp/utils/liquidStaking/isLiquifierProtocolId.ts b/apps/tangle-dapp/utils/liquidStaking/isLiquifierProtocolId.ts deleted file mode 100644 index 1917fbf6b..000000000 --- a/apps/tangle-dapp/utils/liquidStaking/isLiquifierProtocolId.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { LS_LIQUIFIER_PROTOCOL_IDS } from '../../constants/liquidStaking/constants'; -import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types'; - -function isLiquifierProtocolId( - protocolId: number, -): protocolId is LsLiquifierProtocolId { - return LS_LIQUIFIER_PROTOCOL_IDS.includes(protocolId); -} - -export default isLiquifierProtocolId; diff --git a/apps/tangle-dapp/utils/liquidStaking/isLsParachainChainId.ts b/apps/tangle-dapp/utils/liquidStaking/isLsParachainChainId.ts index b5f1e962a..a76a77317 100644 --- a/apps/tangle-dapp/utils/liquidStaking/isLsParachainChainId.ts +++ b/apps/tangle-dapp/utils/liquidStaking/isLsParachainChainId.ts @@ -1,4 +1,4 @@ -import { LS_PARACHAIN_CHAIN_IDS } from '../../constants/liquidStaking/constants'; +import { LS_PARACHAIN_PROTOCOL_IDS } from '../../constants/liquidStaking/constants'; import { LsParachainChainId, LsProtocolId, @@ -7,7 +7,7 @@ import { function isLsParachainChainId( protocolId: LsProtocolId, ): protocolId is LsParachainChainId { - return LS_PARACHAIN_CHAIN_IDS.includes(protocolId as LsParachainChainId); + return LS_PARACHAIN_PROTOCOL_IDS.includes(protocolId as LsParachainChainId); } export default isLsParachainChainId; From 3f064fa3acb8438152d023548907818e644c4c87 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:38:10 -0400 Subject: [PATCH 38/54] refactor(tangle-dapp): Get rid of Liquifier remenants --- .../stakeAndUnstake/LsAgnosticBalance.tsx | 12 +---- .../stakeAndUnstake/NetworkSelector.tsx | 9 +--- .../stakeAndUnstake/useLsChangeNetwork.ts | 15 ------- .../stakeAndUnstake/useLsFeePercentage.ts | 45 ------------------- .../NetworkSelectionButton.tsx | 22 ++------- .../constants/liquidStaking/constants.ts | 13 ------ .../constants/liquidStaking/types.ts | 1 - .../utils/liquidStaking/getLsNetwork.ts | 3 -- .../utils/liquidStaking/getLsTangleNetwork.ts | 4 +- 9 files changed, 7 insertions(+), 117 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index e24c686e1..f2d232cd7 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -13,7 +13,6 @@ import { twMerge } from 'tailwind-merge'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; -import { LsNetworkId } from '../../../constants/liquidStaking/types'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import formatBn from '../../../utils/formatBn'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; @@ -37,13 +36,6 @@ const LsAgnosticBalance: FC = ({ const { selectedProtocolId } = useLsStore(); const protocol = getLsProtocolDef(selectedProtocolId); - // Special case for liquid tokens on the `TgToken.sol` contract. - // See: https://github.com/webb-tools/tnt-core/blob/1f371959884352e7af68e6091c5bb330fcaa58b8/src/lst/liquidtoken/TgToken.sol#L26 - const decimals = - !isNative && protocol.networkId === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER - ? 18 - : protocol.decimals; - const formattedBalance = useMemo(() => { // No account is active; display a placeholder instead of a loading state. if (balance === EMPTY_VALUE_PLACEHOLDER) { @@ -54,14 +46,14 @@ const LsAgnosticBalance: FC = ({ return null; } - const formattedBalance = formatBn(balance, decimals, { + const formattedBalance = formatBn(balance, protocol.decimals, { includeCommas: true, }); const derivativePrefix = isNative ? '' : LS_DERIVATIVE_TOKEN_PREFIX; return `${formattedBalance} ${derivativePrefix}${protocol.token}`; - }, [balance, decimals, isNative, protocol.token]); + }, [balance, protocol.decimals, isNative, protocol.token]); const isClickable = onlyShowTooltipWhenBalanceIsSet && diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx index 3ff8d4a65..1e52632d2 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/NetworkSelector.tsx @@ -7,7 +7,6 @@ import { DropdownMenuItem, Typography, } from '@webb-tools/webb-ui-components'; -import assert from 'assert'; import { FC } from 'react'; import { IS_PRODUCTION_ENV } from '../../../constants/env'; @@ -52,19 +51,13 @@ const NetworkSelector: FC = ({ // Filter out networks that don't support liquid staking yet. const supportedLsNetworks = LS_NETWORKS.filter((network) => { - if (network.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { - return true; - } - // Exclude the local Tangle network in production. - else if (network.id === LsNetworkId.TANGLE_LOCAL && IS_PRODUCTION_ENV) { + if (network.id === LsNetworkId.TANGLE_LOCAL && IS_PRODUCTION_ENV) { return false; } // TODO: Obtain the Tangle network from the LS Network's properties instead. const tangleNetwork = getLsTangleNetwork(network.id); - assert(tangleNetwork !== null); - return NETWORK_FEATURE_MAP[tangleNetwork.id].includes( NetworkFeature.LsPools, ); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts index f467cc853..116133f86 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsChangeNetwork.ts @@ -1,5 +1,4 @@ import { useWebbUI } from '@webb-tools/webb-ui-components'; -import assert from 'assert'; import { useCallback } from 'react'; import { LsNetworkId } from '../../../constants/liquidStaking/types'; @@ -7,7 +6,6 @@ import { NETWORK_FEATURE_MAP } from '../../../constants/networks'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import useNetworkSwitcher from '../../../hooks/useNetworkSwitcher'; import { NetworkFeature } from '../../../types'; -import getLsNetwork from '../../../utils/liquidStaking/getLsNetwork'; import getLsTangleNetwork from '../../../utils/liquidStaking/getLsTangleNetwork'; const useLsChangeNetwork = () => { @@ -22,20 +20,7 @@ const useLsChangeNetwork = () => { return; } - const lsNetwork = getLsNetwork(newNetworkId); - - // Don't check connection to Ethereum mainnet liquifier; - // only verify RPC connection to Tangle networks. - if (lsNetwork.id === LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { - setSelectedNetworkId(newNetworkId); - - return; - } - const tangleNetwork = getLsTangleNetwork(newNetworkId); - - assert(tangleNetwork !== null); - const networkFeatures = NETWORK_FEATURE_MAP[tangleNetwork.id]; if (!networkFeatures.includes(NetworkFeature.LsPools)) { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts index a63a506c1..790871aa4 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts @@ -1,14 +1,8 @@ -import { useCallback, useEffect } from 'react'; - -import { LS_REGISTRY_ADDRESS } from '../../../constants/liquidStaking/constants'; -import LIQUIFIER_REGISTRY_ABI from '../../../constants/liquidStaking/liquifierRegistryAbi'; import { LsNetworkId, LsProtocolId, } from '../../../constants/liquidStaking/types'; import useParachainLsFees from '../../../data/liquidStaking/parachain/useParachainLsFees'; -import useContractRead from '../../../data/evm/useContractRead'; -import { ContractReadOptions } from '../../../data/evm/useContractReadOnce'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; const useLsFeePercentage = ( @@ -26,48 +20,9 @@ const useLsFeePercentage = ( ? parachainFees.mintFeePercentage : parachainFees.redeemFeePercentage; - const getLiquifierFeeOptions = useCallback((): ContractReadOptions< - typeof LIQUIFIER_REGISTRY_ABI, - 'fee' - > | null => { - if (protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { - return null; - } - - return { - address: LS_REGISTRY_ADDRESS, - functionName: 'fee', - // TODO: Need to confirm whether this is the actual expected address. What address is the Liquifier using for fees? - args: [protocol.erc20TokenAddress], - }; - }, [protocol]); - - const { - value: rawLiquifierFeeOrError, - setIsPaused: setIsLiquifierFeePaused, - } = useContractRead(LIQUIFIER_REGISTRY_ABI, getLiquifierFeeOptions); - - // Pause liquifier fee fetching if the protocol is a parachain chain. - // This helps prevent unnecessary contract read calls. - useEffect(() => { - setIsLiquifierFeePaused( - protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN, - ); - }, [protocol.networkId, setIsLiquifierFeePaused]); - - // The fee should be returned as a per-mill value from the liquifier contract. - const liquifierFeePercentageOrError = - rawLiquifierFeeOrError instanceof Error - ? rawLiquifierFeeOrError - : rawLiquifierFeeOrError === null - ? null - : Number(rawLiquifierFeeOrError) / 100; - switch (protocol.networkId) { case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: return parachainFee; - case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: - return liquifierFeePercentageOrError; // Tangle networks with the `lst` pallet have no fees for // joining or leaving pools as of now. case LsNetworkId.TANGLE_LOCAL: diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx index 7f238e267..932b0a8b4 100644 --- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx +++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx @@ -20,13 +20,10 @@ import { usePathname } from 'next/navigation'; import { type FC, useCallback, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; -import { IS_PRODUCTION_ENV } from '../../constants/env'; import useNetworkStore from '../../context/useNetworkStore'; -import { useLsStore } from '../../data/liquidStaking/useLsStore'; import useNetworkSwitcher from '../../hooks/useNetworkSwitcher'; import { PagePath } from '../../types'; import createCustomNetwork from '../../utils/createCustomNetwork'; -import isLiquifierProtocolId from '../../utils/liquidStaking/isLiquifierProtocolId'; import { NetworkSelectorDropdown } from './NetworkSelectorDropdown'; // TODO: Currently hard-coded, but shouldn't it always be the Tangle icon, since it's not switching chains but rather networks within Tangle? If so, find some constant somewhere instead of having it hard-coded here. @@ -39,7 +36,6 @@ const NetworkSelectionButton: FC = () => { const { network } = useNetworkStore(); const { switchNetwork, isCustom } = useNetworkSwitcher(); const pathname = usePathname(); - const { selectedProtocolId } = useLsStore(); // TODO: Handle switching network on EVM wallet here. const switchToCustomNetwork = useCallback( @@ -95,28 +91,16 @@ const NetworkSelectionButton: FC = () => { if (isInBridgePath) { return null; } - // Network can't be switched from the Tangle Restaking Parachain while - // on the liquid staking page. + // Network can't be manually switched while on the liquid + // staking page. else if (isInLiquidStakingPage) { - // Special case when the liquifier is selected. - const lsNetworkName = isLiquifierProtocolId(selectedProtocolId) - ? IS_PRODUCTION_ENV - ? 'Ethereum Mainnet' - : 'Sepolia Testnet' - : networkName; - - const chainIconName = isLiquifierProtocolId(selectedProtocolId) - ? 'ethereum' - : TANGLE_TESTNET_CHAIN_NAME; - return ( diff --git a/apps/tangle-dapp/constants/liquidStaking/constants.ts b/apps/tangle-dapp/constants/liquidStaking/constants.ts index d3ebff5c2..548ae7dd6 100644 --- a/apps/tangle-dapp/constants/liquidStaking/constants.ts +++ b/apps/tangle-dapp/constants/liquidStaking/constants.ts @@ -1,15 +1,11 @@ import ASTAR from '../../data/liquidStaking/adapters/astar'; -import CHAINLINK from '../../data/liquidStaking/adapters/chainlink'; -import LIVEPEER from '../../data/liquidStaking/adapters/livepeer'; import MANTA from '../../data/liquidStaking/adapters/manta'; import MOONBEAM from '../../data/liquidStaking/adapters/moonbeam'; import PHALA from '../../data/liquidStaking/adapters/phala'; import POLKADOT from '../../data/liquidStaking/adapters/polkadot'; -import POLYGON from '../../data/liquidStaking/adapters/polygon'; import TANGLE_LOCAL from '../../data/liquidStaking/adapters/tangleLocal'; import TANGLE_MAINNET from '../../data/liquidStaking/adapters/tangleMainnet'; import TANGLE_TESTNET from '../../data/liquidStaking/adapters/tangleTestnet'; -import THE_GRAPH from '../../data/liquidStaking/adapters/theGraph'; import { IS_PRODUCTION_ENV } from '../env'; import { LsNetwork, @@ -66,14 +62,6 @@ export const TVS_TOOLTIP = export const LS_DERIVATIVE_TOKEN_PREFIX = 'tg'; -export const LS_ETHEREUM_MAINNET_LIQUIFIER: LsNetwork = { - id: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER, - networkName: IS_PRODUCTION_ENV ? 'Ethereum Mainnet' : 'Sepolia Testnet', - chainIconFileName: 'ethereum', - defaultProtocolId: LsProtocolId.CHAINLINK, - protocols: [CHAINLINK, THE_GRAPH, LIVEPEER, POLYGON], -}; - export const LS_TANGLE_RESTAKING_PARACHAIN: LsNetwork = { id: LsNetworkId.TANGLE_RESTAKING_PARACHAIN, networkName: 'Tangle Parachain', @@ -112,7 +100,6 @@ export const LS_NETWORKS: LsNetwork[] = [ LS_TANGLE_TESTNET, LS_TANGLE_LOCAL, LS_TANGLE_RESTAKING_PARACHAIN, - LS_ETHEREUM_MAINNET_LIQUIFIER, ]; /** diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index dc392d3e1..3949d8e8a 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -75,7 +75,6 @@ export enum LsNetworkId { TANGLE_TESTNET, TANGLE_MAINNET, TANGLE_RESTAKING_PARACHAIN, - ETHEREUM_MAINNET_LIQUIFIER, } export interface LsTangleNetworkDef extends ProtocolDefCommon { diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts index 9969d0fe3..2d21b3b9e 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts @@ -1,5 +1,4 @@ import { - LS_ETHEREUM_MAINNET_LIQUIFIER, LS_TANGLE_LOCAL, LS_TANGLE_MAINNET, LS_TANGLE_RESTAKING_PARACHAIN, @@ -9,8 +8,6 @@ import { LsNetwork, LsNetworkId } from '../../constants/liquidStaking/types'; const getLsNetwork = (networkId: LsNetworkId): LsNetwork => { switch (networkId) { - case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: - return LS_ETHEREUM_MAINNET_LIQUIFIER; case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: return LS_TANGLE_RESTAKING_PARACHAIN; case LsNetworkId.TANGLE_MAINNET: diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts index 6d0924c7b..16990b37b 100644 --- a/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts +++ b/apps/tangle-dapp/utils/liquidStaking/getLsTangleNetwork.ts @@ -11,7 +11,7 @@ import { IS_PRODUCTION_ENV } from '../../constants/env'; import { LsNetworkId } from '../../constants/liquidStaking/types'; // TODO: Obtain the Tangle network directly from the adapter's `tangleNetwork` property instead of using this helper method. -const getLsTangleNetwork = (networkId: LsNetworkId): Network | null => { +const getLsTangleNetwork = (networkId: LsNetworkId): Network => { switch (networkId) { case LsNetworkId.TANGLE_MAINNET: return TANGLE_MAINNET_NETWORK; @@ -23,8 +23,6 @@ const getLsTangleNetwork = (networkId: LsNetworkId): Network | null => { return IS_PRODUCTION_ENV ? TANGLE_RESTAKING_PARACHAIN_TESTNET_NETWORK : TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK; - case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: - return null; } }; From 081cdbb0bee60cc980ff73c4c9594456a3fa6bb2 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:43:53 -0400 Subject: [PATCH 39/54] refactor(tangle-dapp): Get rid of Liquifier remenants --- .../UnstakeRequestsTable.tsx | 17 +--- .../data/liquidStaking/useLsExchangeRate.ts | 86 +------------------ 2 files changed, 5 insertions(+), 98 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index 4b9076e5a..f8ed45efc 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -78,16 +78,11 @@ const COLUMNS = [ COLUMN_HELPER.accessor('unlockId', { header: () => , cell: (props) => { - const canSelect = - props.row.original.type === 'liquifierUnlockNft' - ? props.row.original.progress === 1 - : true; - return (
@@ -137,11 +132,7 @@ const COLUMNS = [ header: () => , cell: (props) => { const unstakeRequest = props.row.original; - - const tokenSymbol = - unstakeRequest.type === 'parachainUnstakeRequest' - ? unstakeRequest.currency.toUpperCase() - : unstakeRequest.symbol; + const tokenSymbol = unstakeRequest.currency.toUpperCase(); return ( { // If the remaining time unit is undefined, it means that the // request has completed its unlocking period. - return request.type === 'parachainUnstakeRequest' - ? request.progress === undefined - : request.progress === 1; + return request.progress === undefined; }); }, [selectedRowsUnlockIds, rows]); diff --git a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts index 90ce55ed7..b1819d6ab 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts @@ -1,10 +1,7 @@ import { BN } from '@polkadot/util'; import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { erc20Abi } from 'viem'; +import { useCallback, useMemo, useState } from 'react'; -import LIQUIFIER_ADAPTER_ABI from '../../constants/liquidStaking/liquifierAdapterAbi'; -import LIQUIFIER_TG_TOKEN_ABI from '../../constants/liquidStaking/liquifierTgTokenAbi'; import { LsNetworkId, LsParachainCurrencyKey, @@ -12,8 +9,6 @@ import { import useApiRx from '../../hooks/useApiRx'; import calculateBnRatio from '../../utils/calculateBnRatio'; import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef'; -import useContractRead from '../evm/useContractRead'; -import { ContractReadOptions } from '../evm/useContractReadOnce'; import { useLsStore } from './useLsStore'; import usePolling from './usePolling'; @@ -83,72 +78,10 @@ const useLsExchangeRate = (type: ExchangeRateType) => { return computeExchangeRate(type, tokenPoolAmount, lstTotalIssuance); }, [lstTotalIssuance, tokenPoolAmount, type]); - const getTgTokenTotalSupplyOptions = useCallback((): ContractReadOptions< - typeof LIQUIFIER_TG_TOKEN_ABI, - 'totalSupply' - > | null => { - if (protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { - return null; - } - - return { - address: protocol.erc20TokenAddress, - functionName: 'totalSupply', - args: [], - }; - }, [protocol]); - - const getLiquifierTotalSharesOptions = useCallback((): ContractReadOptions< - typeof LIQUIFIER_ADAPTER_ABI, - 'totalShares' - > | null => { - if (protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) { - return null; - } - - return { - address: protocol.liquifierContractAddress, - functionName: 'totalShares', - args: [], - }; - }, [protocol]); - - const { - value: tgTokenTotalSupply, - setIsPaused: setIsTgTokenTotalSupplyPaused, - } = useContractRead(erc20Abi, getTgTokenTotalSupplyOptions); - - const { - value: liquifierTotalShares, - setIsPaused: setIsLiquifierTotalSharesPaused, - } = useContractRead(LIQUIFIER_ADAPTER_ABI, getLiquifierTotalSharesOptions); - - const fetchLiquifierExchangeRate = useCallback(async () => { - // Propagate error or loading states. - if (typeof tgTokenTotalSupply !== 'bigint') { - return tgTokenTotalSupply; - } else if (typeof liquifierTotalShares !== 'bigint') { - return liquifierTotalShares; - } - - const tgTokenTotalSupplyBn = new BN(tgTokenTotalSupply.toString()); - const liquifierTotalSharesBn = new BN(liquifierTotalShares.toString()); - - return computeExchangeRate( - type, - tgTokenTotalSupplyBn, - liquifierTotalSharesBn, - ); - }, [liquifierTotalShares, tgTokenTotalSupply, type]); - const fetch = useCallback(async () => { let promise: Promise; switch (selectedNetworkId) { - case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER: - promise = fetchLiquifierExchangeRate(); - - break; case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: promise = parachainExchangeRate; @@ -170,22 +103,7 @@ const useLsExchangeRate = (type: ExchangeRateType) => { } setExchangeRate(newExchangeRate); - }, [fetchLiquifierExchangeRate, parachainExchangeRate, selectedNetworkId]); - - // Pause or resume ERC20-based exchange rate fetching based - // on whether the requested protocol is a parachain or an ERC20 token. - // This helps prevent unnecessary requests. - useEffect(() => { - const isPaused = - protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN; - - setIsTgTokenTotalSupplyPaused(isPaused); - setIsLiquifierTotalSharesPaused(isPaused); - }, [ - protocol.networkId, - setIsLiquifierTotalSharesPaused, - setIsTgTokenTotalSupplyPaused, - ]); + }, [parachainExchangeRate, selectedNetworkId]); const isRefreshing = usePolling({ effect: fetch }); From af6a02e649f0dfa0f3459fbebb775d51f9705146 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 21:26:46 -0400 Subject: [PATCH 40/54] refactor(tangle-dapp): Add missing table columns --- .../components/tableCells/PercentageCell.tsx | 26 +++++++++ .../constants/liquidStaking/types.ts | 4 -- .../tangle-dapp/containers/LsMyPoolsTable.tsx | 57 +++++++++---------- .../LsPoolsTable2/LsPoolsTable2.tsx | 55 ++++++++---------- .../liquidStaking/useLsProtocolEntities.ts | 4 -- .../data/liquidStaking/useLsValidators.ts | 2 - apps/tangle-dapp/hooks/useApiRx.ts | 3 - 7 files changed, 77 insertions(+), 74 deletions(-) create mode 100644 apps/tangle-dapp/components/tableCells/PercentageCell.tsx diff --git a/apps/tangle-dapp/components/tableCells/PercentageCell.tsx b/apps/tangle-dapp/components/tableCells/PercentageCell.tsx new file mode 100644 index 000000000..b5c78c7c4 --- /dev/null +++ b/apps/tangle-dapp/components/tableCells/PercentageCell.tsx @@ -0,0 +1,26 @@ +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; + +import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; + +export type PercentageCellProps = { + percentage?: number; +}; + +const PercentageCell: FC = ({ percentage }) => { + if (percentage === undefined) { + return EMPTY_VALUE_PLACEHOLDER; + } + + return ( + + {`${percentage.toFixed(2)}%`} + + ); +}; + +export default PercentageCell; diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts index 3949d8e8a..1bf44f5cf 100644 --- a/apps/tangle-dapp/constants/liquidStaking/types.ts +++ b/apps/tangle-dapp/constants/liquidStaking/types.ts @@ -24,10 +24,6 @@ export enum LsProtocolId { MOONBEAM, ASTAR, MANTA, - CHAINLINK, - THE_GRAPH, - LIVEPEER, - POLYGON, TANGLE_MAINNET, TANGLE_TESTNET, TANGLE_LOCAL, diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index 60986aaae..ac74eb944 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -15,19 +15,20 @@ import { LsPool } from '../constants/liquidStaking/types'; import { ActionsDropdown, Avatar, + AvatarGroup, Button, - getRoundedAmountString, Typography, } from '@webb-tools/webb-ui-components'; import TokenAmountCell from '../components/tableCells/TokenAmountCell'; import pluralize from '../utils/pluralize'; -import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; import { ArrowRight } from '@webb-tools/icons'; import useLsPools from '../data/liquidStaking/useLsPools'; import useSubstrateAddress from '../hooks/useSubstrateAddress'; import { BN } from '@polkadot/util'; import assert from 'assert'; import { GlassCard } from '../components'; +import PercentageCell from '../components/tableCells/PercentageCell'; +import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; type MyLsPoolRow = LsPool & { myStake: BN; @@ -51,18 +52,6 @@ const POOL_COLUMNS = [ ), }), - COLUMN_HELPER.accessor('token', { - header: () => 'Token', - cell: (props) => ( - - {props.getValue()} - - ), - }), COLUMN_HELPER.accessor('ownerAddress', { header: () => 'Owner', cell: (props) => ( @@ -73,6 +62,24 @@ const POOL_COLUMNS = [ /> ), }), + COLUMN_HELPER.accessor('validators', { + header: () => 'Validators', + cell: (props) => + props.row.original.validators.length === 0 ? ( + EMPTY_VALUE_PLACEHOLDER + ) : ( + + {props.row.original.validators.map((substrateAddress) => ( + + ))} + + ), + }), COLUMN_HELPER.accessor('totalStaked', { header: () => 'Total Staked (TVL)', // TODO: Decimals. @@ -82,25 +89,13 @@ const POOL_COLUMNS = [ header: () => 'My Stake', cell: (props) => , }), + COLUMN_HELPER.accessor('commissionPercentage', { + header: () => 'Commission', + cell: (props) => , + }), COLUMN_HELPER.accessor('apyPercentage', { header: () => 'APY', - cell: (props) => { - const apy = props.getValue(); - - if (apy === undefined) { - return EMPTY_VALUE_PLACEHOLDER; - } - - return ( - - {getRoundedAmountString(props.getValue()) + '%'} - - ); - }, + cell: (props) => , }), COLUMN_HELPER.display({ id: 'actions', diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx index a2612e160..8146d6c14 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable2/LsPoolsTable2.tsx @@ -15,8 +15,8 @@ import { twMerge } from 'tailwind-merge'; import { LsPool } from '../../constants/liquidStaking/types'; import { Avatar, + AvatarGroup, Button, - getRoundedAmountString, Typography, } from '@webb-tools/webb-ui-components'; import TokenAmountCell from '../../components/tableCells/TokenAmountCell'; @@ -24,6 +24,7 @@ import pluralize from '../../utils/pluralize'; import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import { ArrowRight } from '@webb-tools/icons'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; +import PercentageCell from '../../components/tableCells/PercentageCell'; export interface LsPoolsTable2Props { pools: LsPool[]; @@ -63,18 +64,6 @@ const LsPoolsTable2: FC = ({ pools, isShown }) => { ), }), - COLUMN_HELPER.accessor('token', { - header: () => 'Token', - cell: (props) => ( - - {props.getValue()} - - ), - }), COLUMN_HELPER.accessor('ownerAddress', { header: () => 'Owner', cell: (props) => ( @@ -85,30 +74,36 @@ const LsPoolsTable2: FC = ({ pools, isShown }) => { /> ), }), + COLUMN_HELPER.accessor('validators', { + header: () => 'Validators', + cell: (props) => + props.row.original.validators.length === 0 ? ( + EMPTY_VALUE_PLACEHOLDER + ) : ( + + {props.row.original.validators.map((substrateAddress) => ( + + ))} + + ), + }), COLUMN_HELPER.accessor('totalStaked', { header: () => 'Total Staked (TVL)', // TODO: Decimals. cell: (props) => , }), + COLUMN_HELPER.accessor('commissionPercentage', { + header: () => 'Commission', + cell: (props) => , + }), COLUMN_HELPER.accessor('apyPercentage', { header: () => 'APY', - cell: (props) => { - const apy = props.getValue(); - - if (apy === undefined) { - return EMPTY_VALUE_PLACEHOLDER; - } - - return ( - - {getRoundedAmountString(props.getValue()) + '%'} - - ); - }, + cell: (props) => , }), COLUMN_HELPER.display({ id: 'actions', diff --git a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts index fa02539dc..4659eb109 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts @@ -70,10 +70,6 @@ const getDataType = (chain: LsProtocolId): LiquidStakingItem | null => { return LiquidStakingItem.VAULT_OR_STAKE_POOL; case LsProtocolId.ASTAR: return LiquidStakingItem.DAPP; - case LsProtocolId.CHAINLINK: - case LsProtocolId.LIVEPEER: - case LsProtocolId.POLYGON: - case LsProtocolId.THE_GRAPH: case LsProtocolId.TANGLE_MAINNET: case LsProtocolId.TANGLE_TESTNET: case LsProtocolId.TANGLE_LOCAL: diff --git a/apps/tangle-dapp/data/liquidStaking/useLsValidators.ts b/apps/tangle-dapp/data/liquidStaking/useLsValidators.ts index 6462b7b9e..fe5e1b02e 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsValidators.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsValidators.ts @@ -84,8 +84,6 @@ const useLsValidators = (selectedChain: LsProtocolId) => { ); break; - // TODO: Add cases for ERC20 tokens/networks (Chainlink, etc.). - default: fetchedItems = []; break; diff --git a/apps/tangle-dapp/hooks/useApiRx.ts b/apps/tangle-dapp/hooks/useApiRx.ts index dd2ecd3ff..8b442c6ab 100644 --- a/apps/tangle-dapp/hooks/useApiRx.ts +++ b/apps/tangle-dapp/hooks/useApiRx.ts @@ -37,8 +37,6 @@ function useApiRx( const [result, setResult] = useState(null); const [isLoading, setLoading] = useState(true); const { rpcEndpoint } = useNetworkStore(); - - // TODO: Consider integrating the error right into the result: `result: T | Error | null`. This will force the consumer to handle the error case, which is what they should be doing anyway. const [error, setError] = useState(null); const { result: apiRx } = usePromise( @@ -62,7 +60,6 @@ function useApiRx( return; } - // TODO: Also allow for `| Error` return value, to allow for error handling in the consumer. let observable: Observable | null; // In certain cases, the factory may fail with an error. For example, From 5268d613c3f03866e1e3a15fc78cd1332b83139f Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:04:47 -0400 Subject: [PATCH 41/54] refactor(tangle-dapp): Progress on the table states --- .../stakeAndUnstake/LsStakeCard.tsx | 2 +- .../stakeAndUnstake/SelectedPoolIndicator.tsx | 7 ++- .../stakeAndUnstake/TokenChip.tsx | 4 +- apps/tangle-dapp/components/LsTokenIcon.tsx | 45 +++++++++++-------- .../LsPoolsTable.tsx} | 25 +++++++++-- .../LsProtocolsTable.tsx | 4 +- .../{LsPoolsTable2 => LsPoolsTable}/index.ts | 0 .../{LsPoolsTable.tsx => LsPoolsTableOld.tsx} | 4 +- .../src/components/Pagination/Pagination.tsx | 5 ++- .../src/components/index.ts | 1 - 10 files changed, 66 insertions(+), 31 deletions(-) rename apps/tangle-dapp/containers/{LsPoolsTable2/LsPoolsTable2.tsx => LsPoolsTable/LsPoolsTable.tsx} (87%) rename apps/tangle-dapp/containers/{LsPoolsTable2 => LsPoolsTable}/LsProtocolsTable.tsx (98%) rename apps/tangle-dapp/containers/{LsPoolsTable2 => LsPoolsTable}/index.ts (100%) rename apps/tangle-dapp/containers/{LsPoolsTable.tsx => LsPoolsTableOld.tsx} (99%) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 166ab92e0..eb23b34ca 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -78,7 +78,7 @@ const LsStakeCard: FC = () => { const lsPoolMembers = useLsPoolMembers(); const actionText = useMemo(() => { - const defaultText = 'Stake'; + const defaultText = 'Join Pool & Stake'; if (lsPoolMembers === null) { return defaultText; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx index 5de794fac..2993b13a3 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx @@ -13,7 +13,12 @@ const SelectedPoolIndicator: FC = () => { return (
- + {selectedPoolDisplayName === null diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx index f91ec5912..ca81f233f 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx @@ -26,7 +26,9 @@ const TokenChip: FC = ({ onClick !== undefined && 'cursor-pointer', )} > - {token && } + {token && ( + + )} {isDerivativeVariant && LS_DERIVATIVE_TOKEN_PREFIX} diff --git a/apps/tangle-dapp/components/LsTokenIcon.tsx b/apps/tangle-dapp/components/LsTokenIcon.tsx index d1d98e9f3..556ff2819 100644 --- a/apps/tangle-dapp/components/LsTokenIcon.tsx +++ b/apps/tangle-dapp/components/LsTokenIcon.tsx @@ -5,11 +5,16 @@ import { twMerge } from 'tailwind-merge'; type LsTokenIconSize = 'md' | 'lg'; interface LsTokenIconProps { - name: string; + name?: string; size?: LsTokenIconSize; + hasTangleBorder?: boolean; } -const LsTokenIcon: FC = ({ name, size = 'md' }) => { +const LsTokenIcon: FC = ({ + name, + size = 'md', + hasTangleBorder = true, +}) => { const { wrapperSizeClassName, iconSizeClassName, borderSize } = getSizeValues(size); @@ -21,22 +26,25 @@ const LsTokenIcon: FC = ({ name, size = 'md' }) => { wrapperSizeClassName, )} > - - } - /> + {name !== undefined && ( + + } + /> + )} + = ({ name, size = 'md' }) => { stroke="url(#paint0_linear_257_3557)" strokeWidth="1.25" /> + (); -const LsPoolsTable2: FC = ({ pools, isShown }) => { +const LsPoolsTable: FC = ({ pools, isShown }) => { const [sorting, setSorting] = useState([]); const [{ pageIndex, pageSize }, setPagination] = useState({ @@ -143,6 +145,21 @@ const LsPoolsTable2: FC = ({ pools, isShown }) => { enableSortingRemoval: false, }); + if (pools.length === 0) { + ; + } + return (
= ({ pools, isShown }) => { ); }; -export default LsPoolsTable2; +export default LsPoolsTable; diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx similarity index 98% rename from apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx rename to apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx index df32dfec2..bfaffcef5 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable2/LsProtocolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx @@ -14,7 +14,7 @@ import { import { Table } from '../../../../libs/webb-ui-components/src/components/Table'; import { Typography } from '../../../../libs/webb-ui-components/src/typography'; import { twMerge } from 'tailwind-merge'; -import LsPoolsTable2 from './LsPoolsTable2'; +import LsPoolsTable from './LsPoolsTable2'; import TableCellWrapper from '../../components/tables/TableCellWrapper'; import LsTokenIcon from '../../components/LsTokenIcon'; import StatItem from '../../components/StatItem'; @@ -115,7 +115,7 @@ function LsProtocolsTable({ initialSorting = [] }: LsProtocolsTableProps) { const getExpandedRowContent = useCallback( (row: Row) => (
- diff --git a/apps/tangle-dapp/containers/LsPoolsTable2/index.ts b/apps/tangle-dapp/containers/LsPoolsTable/index.ts similarity index 100% rename from apps/tangle-dapp/containers/LsPoolsTable2/index.ts rename to apps/tangle-dapp/containers/LsPoolsTable/index.ts diff --git a/apps/tangle-dapp/containers/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx similarity index 99% rename from apps/tangle-dapp/containers/LsPoolsTable.tsx rename to apps/tangle-dapp/containers/LsPoolsTableOld.tsx index 2f9319bd6..085d05faf 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx @@ -145,7 +145,7 @@ const DEFAULT_PAGINATION_STATE: PaginationState = { pageSize: 10, }; -const LsPoolsTable: FC = () => { +const LsPoolsTableOld: FC = () => { const { setSelectedPoolId: setSelectedParachainPoolId } = useLsStore(); const [searchQuery, setSearchQuery] = useState(''); @@ -275,4 +275,4 @@ const LsPoolsTable: FC = () => { ); }; -export default LsPoolsTable; +export default LsPoolsTableOld; diff --git a/libs/webb-ui-components/src/components/Pagination/Pagination.tsx b/libs/webb-ui-components/src/components/Pagination/Pagination.tsx index 7b052285d..9ec08e2ce 100644 --- a/libs/webb-ui-components/src/components/Pagination/Pagination.tsx +++ b/libs/webb-ui-components/src/components/Pagination/Pagination.tsx @@ -48,6 +48,7 @@ export const Pagination = React.forwardRef( // Otherwise, calculate the remaining items on the last page const remainingItems = totalItems ? totalItems % (itemsPerPage ?? 1) : 0; + return remainingItems > 0 ? remainingItems.toLocaleString() : (itemsPerPage?.toLocaleString() ?? '-'); @@ -67,7 +68,9 @@ export const Pagination = React.forwardRef(
{/** Left label */}

- Showing {showingItemsCount} {title} out of {totalItems ?? '-'} + {totalItems === 0 + ? 'No items' + : `Showing ${showingItemsCount} ${title} out of ${totalItems ?? '-'}`}

{/** Right buttons */} diff --git a/libs/webb-ui-components/src/components/index.ts b/libs/webb-ui-components/src/components/index.ts index 8161f3c4d..e30e4e85f 100644 --- a/libs/webb-ui-components/src/components/index.ts +++ b/libs/webb-ui-components/src/components/index.ts @@ -80,7 +80,6 @@ export * from './Tooltip'; export * from './TransactionInputCard'; export * from './TxProgressor'; export { default as TxConfirmationRing } from './TxConfirmationRing'; -export * from '../../../../apps/tangle-dapp/containers/LsPoolsTable2'; export * from './WalletConnectionCard'; export * from './WalletModal'; export * from './WebsiteFooter'; From 131818b4ed68f56c595715cc330be1c6cb6693a9 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 3 Oct 2024 02:44:32 -0400 Subject: [PATCH 42/54] refactor(tangle-dapp): Better names for properties in `useLsStore` --- apps/tangle-dapp/app/liquid-staking/page.tsx | 8 +- .../LiquidStaking/LsValidatorTable.tsx | 10 +- .../stakeAndUnstake/LsAgnosticBalance.tsx | 4 +- .../LiquidStaking/stakeAndUnstake/LsInput.tsx | 4 +- .../stakeAndUnstake/LsStakeCard.tsx | 53 ++-- .../stakeAndUnstake/LsUnstakeCard.tsx | 47 ++-- .../stakeAndUnstake/SelectedPoolIndicator.tsx | 4 +- .../stakeAndUnstake/useLsAgnosticBalance.ts | 12 +- .../stakeAndUnstake/useLsChangeNetwork.ts | 6 +- .../UnstakeRequestsTable.tsx | 6 +- .../tangle-dapp/containers/LsMyPoolsTable.tsx | 248 ++++++++++-------- .../containers/LsPoolsTable/LsPoolsTable.tsx | 38 +-- .../LsPoolsTable/LsProtocolsTable.tsx | 2 +- .../containers/LsPoolsTableOld.tsx | 6 +- apps/tangle-dapp/context/ErrorsContext.tsx | 17 -- .../liquidStaking/tangle/useLsPoolBalance.ts | 12 +- .../data/liquidStaking/useLsExchangeRate.ts | 8 +- .../data/liquidStaking/useLsStore.ts | 48 ++-- .../useSelectedPoolDisplayName.ts | 8 +- 19 files changed, 274 insertions(+), 267 deletions(-) diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index e6870dea0..78e7cdc3e 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -1,7 +1,6 @@ 'use client'; import { - LsProtocolsTable, TabContent, TabsList as WebbTabsList, TabsRoot, @@ -15,6 +14,7 @@ import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnst import StatItem from '../../components/StatItem'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; import LsMyPoolsTable from '../../containers/LsMyPoolsTable'; +import { LsProtocolsTable } from '../../containers/LsPoolsTable'; import useNetworkStore from '../../context/useNetworkStore'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; import useNetworkSwitcher from '../../hooks/useNetworkSwitcher'; @@ -42,11 +42,11 @@ const LiquidStakingPage: FC = () => { value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, }); - const { selectedNetworkId } = useLsStore(); + const { lsNetworkId } = useLsStore(); const { network } = useNetworkStore(); const { switchNetwork } = useNetworkSwitcher(); - const lsTangleNetwork = getLsTangleNetwork(selectedNetworkId); + const lsTangleNetwork = getLsTangleNetwork(lsNetworkId); // Sync the network with the selected liquid staking network on load. // It might differ initially if the user navigates to the page and @@ -55,7 +55,7 @@ const LiquidStakingPage: FC = () => { if (lsTangleNetwork !== null && lsTangleNetwork.id !== network.id) { switchNetwork(lsTangleNetwork, false); } - }, [lsTangleNetwork, network.id, selectedNetworkId, switchNetwork]); + }, [lsTangleNetwork, network.id, lsNetworkId, switchNetwork]); return (
diff --git a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx index df09b6e14..5483ce81a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsValidatorTable.tsx @@ -45,9 +45,9 @@ const SELECTED_ITEMS_COLUMN_SORT = { } as const satisfies ColumnSort; export const LsValidatorTable = () => { - const { selectedProtocolId, setSelectedNetworkEntities: setSelectedItems } = - useLsStore(); - const { isLoading, data, dataType } = useLsValidators(selectedProtocolId); + const { lsProtocolId, setNetworkEntities } = useLsStore(); + + const { isLoading, data, dataType } = useLsValidators(lsProtocolId); const [searchValue, setSearchValue] = useState(''); const [rowSelection, setRowSelection] = useState({}); @@ -63,8 +63,8 @@ export const LsValidatorTable = () => { >(null); useEffect(() => { - setSelectedItems(new Set(Object.keys(rowSelection))); - }, [rowSelection, setSelectedItems]); + setNetworkEntities(new Set(Object.keys(rowSelection))); + }, [rowSelection, setNetworkEntities]); const columns = useLsValidatorSelectionTableColumns( toggleSortSelectionHandlerRef, diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index f2d232cd7..ef9a22511 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -33,8 +33,8 @@ const LsAgnosticBalance: FC = ({ }) => { const [isHovering, setIsHovering] = useState(false); const balance = useLsAgnosticBalance(isNative); - const { selectedProtocolId } = useLsStore(); - const protocol = getLsProtocolDef(selectedProtocolId); + const { lsProtocolId } = useLsStore(); + const protocol = getLsProtocolDef(lsProtocolId); const formattedBalance = useMemo(() => { // No account is active; display a placeholder instead of a loading state. diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx index 0c9f4cec3..ec253d369 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx @@ -59,9 +59,9 @@ const LsInput: FC = ({ className, showPoolIndicator = true, }) => { - const { selectedProtocolId } = useLsStore(); + const { lsProtocolId } = useLsStore(); - const selectedProtocol = getLsProtocolDef(selectedProtocolId); + const selectedProtocol = getLsProtocolDef(lsProtocolId); const minErrorMessage = ((): string | undefined => { if (minAmount === undefined) { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index eb23b34ca..fee076f89 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -53,12 +53,7 @@ const LsStakeCard: FC = () => { stringify: (value) => value?.toString(), }); - const { - selectedProtocolId, - setSelectedProtocolId, - selectedNetworkId, - selectedPoolId, - } = useLsStore(); + const { lsProtocolId, setLsProtocolId, lsNetworkId, lsPoolId } = useLsStore(); const { execute: executeTanglePoolJoinTx, status: tanglePoolJoinTxStatus } = useLsPoolJoinTx(); @@ -70,10 +65,10 @@ const LsStakeCard: FC = () => { const { maxSpendable, minSpendable } = useLsSpendingLimits( true, - selectedProtocolId, + lsProtocolId, ); - const selectedProtocol = getLsProtocolDef(selectedProtocolId); + const selectedProtocol = getLsProtocolDef(lsProtocolId); const tryChangeNetwork = useLsChangeNetwork(); const lsPoolMembers = useLsPoolMembers(); @@ -86,29 +81,29 @@ const LsStakeCard: FC = () => { const isMember = lsPoolMembers.some( ([poolId, accountAddress]) => - poolId === selectedPoolId && accountAddress === activeAccountAddress, + poolId === lsPoolId && accountAddress === activeAccountAddress, ); return isMember ? 'Increase Stake' : defaultText; - }, [activeAccountAddress, lsPoolMembers, selectedPoolId]); + }, [activeAccountAddress, lsPoolMembers, lsPoolId]); const isTangleNetwork = - selectedNetworkId === LsNetworkId.TANGLE_LOCAL || - selectedNetworkId === LsNetworkId.TANGLE_MAINNET || - selectedNetworkId === LsNetworkId.TANGLE_TESTNET; + lsNetworkId === LsNetworkId.TANGLE_LOCAL || + lsNetworkId === LsNetworkId.TANGLE_MAINNET || + lsNetworkId === LsNetworkId.TANGLE_TESTNET; // TODO: Not loading the correct protocol for: '?amount=123000000000000000000&protocol=7&network=1&action=stake'. When network=1, it switches to protocol=5 on load. Could this be because the protocol is reset to its default once the network is switched? useSearchParamSync({ key: LsSearchParamKey.PROTOCOL_ID, - value: selectedProtocolId, + value: lsProtocolId, parse: (value) => z.nativeEnum(LsProtocolId).parse(parseInt(value)), stringify: (value) => value.toString(), - setValue: setSelectedProtocolId, + setValue: setLsProtocolId, }); useSearchParamSync({ key: LsSearchParamKey.NETWORK_ID, - value: selectedNetworkId, + value: lsNetworkId, parse: (value) => z.nativeEnum(LsNetworkId).parse(parseInt(value)), stringify: (value) => value.toString(), setValue: tryChangeNetwork, @@ -140,11 +135,11 @@ const LsStakeCard: FC = () => { } else if ( isTangleNetwork && executeTanglePoolJoinTx !== null && - selectedPoolId !== null + lsPoolId !== null ) { executeTanglePoolJoinTx({ amount: fromAmount, - poolId: selectedPoolId, + poolId: lsPoolId, }); } }, [ @@ -153,10 +148,10 @@ const LsStakeCard: FC = () => { fromAmount, isTangleNetwork, selectedProtocol, - selectedPoolId, + lsPoolId, ]); - const feePercentage = useLsFeePercentage(selectedProtocolId, true); + const feePercentage = useLsFeePercentage(lsProtocolId, true); const toAmount = useMemo(() => { if ( @@ -176,9 +171,7 @@ const LsStakeCard: FC = () => { (fromAmount !== null && selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && executeParachainMintTx !== null) || - (isTangleNetwork && - executeTanglePoolJoinTx !== null && - selectedPoolId !== null); + (isTangleNetwork && executeTanglePoolJoinTx !== null && lsPoolId !== null); const walletBalance = ( { // Reset the input amount when the network changes. useEffect(() => { setFromAmount(null); - }, [setFromAmount, selectedNetworkId]); + }, [setFromAmount, lsNetworkId]); return ( <> { { {/* Details */}
- + {
- + - - {/** - * Show management actions if the active user has any role in - * the pool. - */} - {props.row.original.isRoot || - props.row.original.isNominator || - (props.row.original.isBouncer && ( - void 0, - }, - { - label: 'Update Commission', - // TODO: Proper onClick handler. - onClick: () => void 0, - }, - { - label: 'Update Roles', - // TODO: Proper onClick handler. - onClick: () => void 0, - }, - ]} - /> - ))} -
- ), - }), -]; - const LsMyPoolsTable: FC = () => { const substrateAddress = useSubstrateAddress(); const [sorting, setSorting] = useState([]); @@ -184,9 +88,129 @@ const LsMyPoolsTable: FC = () => { }); }, []); + const handleUnstakeClick = useCallback((poolId: number) => {}, []); + + const columns = [ + COLUMN_HELPER.accessor('id', { + header: () => 'ID', + cell: (props) => ( + + {props.row.original.metadata}#{props.getValue()} + + ), + }), + COLUMN_HELPER.accessor('ownerAddress', { + header: () => 'Owner', + cell: (props) => ( + + ), + }), + COLUMN_HELPER.accessor('validators', { + header: () => 'Validators', + cell: (props) => + props.row.original.validators.length === 0 ? ( + EMPTY_VALUE_PLACEHOLDER + ) : ( + + {props.row.original.validators.map((substrateAddress) => ( + + ))} + + ), + }), + COLUMN_HELPER.accessor('totalStaked', { + header: () => 'Total Staked (TVL)', + // TODO: Decimals. + cell: (props) => , + }), + COLUMN_HELPER.accessor('myStake', { + header: () => 'My Stake', + cell: (props) => , + }), + COLUMN_HELPER.accessor('commissionPercentage', { + header: () => 'Commission', + cell: (props) => , + }), + COLUMN_HELPER.accessor('apyPercentage', { + header: () => 'APY', + cell: (props) => , + }), + COLUMN_HELPER.display({ + id: 'actions', + cell: (props) => { + const hasAnyRole = + props.row.original.isRoot || + props.row.original.isNominator || + props.row.original.isBouncer; + + const actionItems: ActionItemType[] = []; + + if (props.row.original.isNominator) { + actionItems.push({ + label: 'Update Nominations', + onClick: () => void 0, + }); + } + + if (props.row.original.isBouncer) { + actionItems.push({ + label: 'Update Commission', + onClick: () => void 0, + }); + } + + if (props.row.original.isRoot) { + actionItems.push({ + label: 'Update Roles', + onClick: () => void 0, + }); + } + + // If the user has any role in the pool, show the short button style + // to avoid taking up too much space. + const isShortButtonStyle = hasAnyRole; + + return ( +
+ {isShortButtonStyle ? ( + + + + ) : ( + + )} + + {/** + * Show management actions if the active user has any role in + * the pool. + */} + {hasAnyRole && ( + + )} +
+ ); + }, + }), + ]; + const table = useReactTable({ data: rows, - columns: POOL_COLUMNS, + columns: columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), @@ -200,6 +224,22 @@ const LsMyPoolsTable: FC = () => { enableSortingRemoval: false, }); + if (rows.length === 0) { + return ( + + ); + } + return (
diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx index e27261316..235107d95 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx @@ -43,7 +43,7 @@ const LsPoolsTable: FC = ({ pools, isShown }) => { pageSize: 5, }); - const { selectedPoolId, setSelectedPoolId } = useLsStore(); + const { lsPoolId, setLsPoolId } = useLsStore(); const pagination = useMemo( () => ({ @@ -112,17 +112,15 @@ const LsPoolsTable: FC = ({ pools, isShown }) => { cell: (props) => (
), @@ -146,18 +144,20 @@ const LsPoolsTable: FC = ({ pools, isShown }) => { }); if (pools.length === 0) { - ; + return ( + + ); } return ( diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx index bfaffcef5..905c7dfab 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx @@ -14,7 +14,7 @@ import { import { Table } from '../../../../libs/webb-ui-components/src/components/Table'; import { Typography } from '../../../../libs/webb-ui-components/src/typography'; import { twMerge } from 'tailwind-merge'; -import LsPoolsTable from './LsPoolsTable2'; +import LsPoolsTable from './LsPoolsTable'; import TableCellWrapper from '../../components/tables/TableCellWrapper'; import LsTokenIcon from '../../components/LsTokenIcon'; import StatItem from '../../components/StatItem'; diff --git a/apps/tangle-dapp/containers/LsPoolsTableOld.tsx b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx index 085d05faf..c49694e35 100644 --- a/apps/tangle-dapp/containers/LsPoolsTableOld.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx @@ -146,7 +146,7 @@ const DEFAULT_PAGINATION_STATE: PaginationState = { }; const LsPoolsTableOld: FC = () => { - const { setSelectedPoolId: setSelectedParachainPoolId } = useLsStore(); + const { setLsPoolId } = useLsStore(); const [searchQuery, setSearchQuery] = useState(''); const [paginationState, setPaginationState] = useState( @@ -171,10 +171,10 @@ const LsPoolsTableOld: FC = () => { const selectedRow = selectedRowIds.at(0); assert(selectedRow !== undefined, 'One row must always be selected'); - setSelectedParachainPoolId(parseInt(selectedRow, 10)); + setLsPoolId(parseInt(selectedRow, 10)); setRowSelectionState(newSelectionState); }, - [rowSelectionState, setSelectedParachainPoolId], + [rowSelectionState, setLsPoolId], ); const poolsMap = useLsPools(); diff --git a/apps/tangle-dapp/context/ErrorsContext.tsx b/apps/tangle-dapp/context/ErrorsContext.tsx index 65bddd855..3f0365a6a 100644 --- a/apps/tangle-dapp/context/ErrorsContext.tsx +++ b/apps/tangle-dapp/context/ErrorsContext.tsx @@ -1,11 +1,8 @@ import React, { Dispatch, - FC, - ReactNode, SetStateAction, useCallback, useContext, - useState, } from 'react'; const ErrorSetContext = React.createContext<{ @@ -18,20 +15,6 @@ const ErrorSetContext = React.createContext<{ }, }); -export const ErrorSetContextProvider: FC<{ children: ReactNode }> = ({ - children, -}) => { - const [errorSet, setErrorSet] = useState(new Set()); - - return ( - - {children} - - ); -}; - /** * Useful for inputs of a form or any other component that * needs to keep track of the number of errors that are currently diff --git a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts index 14a8f3c28..3e43de5d6 100644 --- a/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts +++ b/apps/tangle-dapp/data/liquidStaking/tangle/useLsPoolBalance.ts @@ -11,7 +11,7 @@ const useLsPoolBalance = () => { const substrateAddress = useSubstrateAddress(); const networkFeatures = useNetworkFeatures(); const isSupported = networkFeatures.includes(NetworkFeature.LsPools); - const { selectedPoolId } = useLsStore(); + const { lsPoolId } = useLsStore(); const { result: tanglePoolAssetAccountOpt } = useApiRx( useCallback( @@ -19,17 +19,13 @@ const useLsPoolBalance = () => { // The liquid staking pools functionality isn't available on the active // network, the user hasn't selected a pool yet, or there is no active // account. - if ( - !isSupported || - selectedPoolId === null || - substrateAddress === null - ) { + if (!isSupported || lsPoolId === null || substrateAddress === null) { return null; } - return api.query.assets.account(selectedPoolId, substrateAddress); + return api.query.assets.account(lsPoolId, substrateAddress); }, - [isSupported, selectedPoolId, substrateAddress], + [isSupported, lsPoolId, substrateAddress], ), ); diff --git a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts index b1819d6ab..604fb2703 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts @@ -44,9 +44,9 @@ const MAX_BN_OPERATION_NUMBER = 2 ** 26 - 1; const useLsExchangeRate = (type: ExchangeRateType) => { const [exchangeRate, setExchangeRate] = useState(null); - const { selectedProtocolId, selectedNetworkId } = useLsStore(); + const { lsProtocolId, lsNetworkId } = useLsStore(); - const protocol = getLsProtocolDef(selectedProtocolId); + const protocol = getLsProtocolDef(lsProtocolId); const { result: tokenPoolAmount } = useApiRx((api) => { if (protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN) { @@ -81,7 +81,7 @@ const useLsExchangeRate = (type: ExchangeRateType) => { const fetch = useCallback(async () => { let promise: Promise; - switch (selectedNetworkId) { + switch (lsNetworkId) { case LsNetworkId.TANGLE_RESTAKING_PARACHAIN: promise = parachainExchangeRate; @@ -103,7 +103,7 @@ const useLsExchangeRate = (type: ExchangeRateType) => { } setExchangeRate(newExchangeRate); - }, [parachainExchangeRate, selectedNetworkId]); + }, [parachainExchangeRate, lsNetworkId]); const isRefreshing = usePolling({ effect: fetch }); diff --git a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts index 3e7d6d279..524d67b80 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts @@ -4,39 +4,41 @@ import { LsNetworkId, LsProtocolId } from '../../constants/liquidStaking/types'; import getLsNetwork from '../../utils/liquidStaking/getLsNetwork'; type State = { - selectedNetworkId: LsNetworkId; - selectedProtocolId: LsProtocolId; - selectedNetworkEntities: Set; - selectedPoolId: number | null; + isStaking: boolean; + lsNetworkId: LsNetworkId; + lsProtocolId: LsProtocolId; + networkEntities: Set; + lsPoolId: number | null; }; type Actions = { - setSelectedProtocolId: (newProtocolId: State['selectedProtocolId']) => void; - setSelectedNetworkId: (newNetworkId: State['selectedNetworkId']) => void; - setSelectedPoolId: (poolId: number) => void; - - setSelectedNetworkEntities: ( - selectedNetworkEntities: State['selectedNetworkEntities'], - ) => void; + setIsStaking: (isStaking: State['isStaking']) => void; + setLsProtocolId: (newProtocolId: State['lsProtocolId']) => void; + setSelectedNetworkId: (newNetworkId: State['lsNetworkId']) => void; + setLsPoolId: (poolId: number) => void; + setNetworkEntities: (networkEntities: State['networkEntities']) => void; }; type Store = State & Actions; export const useLsStore = create((set) => ({ - selectedPoolId: null, - selectedNetworkEntities: new Set(), + isStaking: true, + lsPoolId: null, + networkEntities: new Set(), // Default the selected network and protocol to the Tangle testnet, // and tTNT, until liquid staking pools are deployed to mainnet. - selectedNetworkId: LsNetworkId.TANGLE_TESTNET, - selectedProtocolId: LsProtocolId.TANGLE_TESTNET, - setSelectedPoolId: (selectedPoolId) => set({ selectedPoolId }), - setSelectedProtocolId: (selectedProtocolId) => set({ selectedProtocolId }), - setSelectedNetworkEntities: (selectedNetworkEntities) => - set({ selectedNetworkEntities }), - setSelectedNetworkId: (selectedNetworkId) => { - const network = getLsNetwork(selectedNetworkId); - const defaultProtocolId = network.defaultProtocolId; + lsNetworkId: LsNetworkId.TANGLE_TESTNET, + lsProtocolId: LsProtocolId.TANGLE_TESTNET, + setIsStaking: (isStaking) => set({ isStaking }), + setLsPoolId: (lsPoolId) => set({ lsPoolId }), + setLsProtocolId: (lsProtocolId) => set({ lsProtocolId }), + setNetworkEntities: (networkEntities) => set({ networkEntities }), + setSelectedNetworkId: (lsNetworkId) => { + const network = getLsNetwork(lsNetworkId); - set({ selectedNetworkId, selectedProtocolId: defaultProtocolId }); + set({ + lsNetworkId, + lsProtocolId: network.defaultProtocolId, + }); }, })); diff --git a/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts b/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts index 3a3dc7a4c..4ed0e9350 100644 --- a/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts +++ b/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts @@ -3,16 +3,16 @@ import useLsPoolsMetadata from './useLsPoolsMetadata'; import { useLsStore } from './useLsStore'; const useSelectedPoolDisplayName = (): LsPoolDisplayName | null => { - const { selectedPoolId } = useLsStore(); + const { lsPoolId } = useLsStore(); const lsPoolsMetadata = useLsPoolsMetadata(); - if (selectedPoolId === null) { + if (lsPoolId === null) { return null; } - const name = lsPoolsMetadata?.get(selectedPoolId) ?? ''; + const name = lsPoolsMetadata?.get(lsPoolId) ?? ''; - return `${name}#${selectedPoolId}`; + return `${name}#${lsPoolId}`; }; export default useSelectedPoolDisplayName; From 8d505049bd54710ee0b4042a440b1ac2e1faab91 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 3 Oct 2024 03:27:05 -0400 Subject: [PATCH 43/54] feat(tangle-dapp): Add `Unstake` action to `LsMyPoolsTable` --- apps/tangle-dapp/app/liquid-staking/page.tsx | 19 ++++- .../tangle-dapp/components/BlueIconButton.tsx | 46 +++++++++++ .../tangle-dapp/containers/LsMyPoolsTable.tsx | 79 ++++++++++++++++--- .../hooks/useIsAccountConnected.ts | 9 +++ libs/icons/src/SubtractCircleLineIcon.tsx | 10 +++ libs/icons/src/index.ts | 1 + 6 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 apps/tangle-dapp/components/BlueIconButton.tsx create mode 100644 apps/tangle-dapp/hooks/useIsAccountConnected.ts create mode 100644 libs/icons/src/SubtractCircleLineIcon.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 78e7cdc3e..19db1cf57 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -42,7 +42,12 @@ const LiquidStakingPage: FC = () => { value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE, }); - const { lsNetworkId } = useLsStore(); + const { + lsNetworkId, + setIsStaking: setIsStakingInStore, + isStaking: isStakingInStore, + } = useLsStore(); + const { network } = useNetworkStore(); const { switchNetwork } = useNetworkSwitcher(); @@ -57,6 +62,18 @@ const LiquidStakingPage: FC = () => { } }, [lsTangleNetwork, network.id, lsNetworkId, switchNetwork]); + // Sync the Zustand store state of whether liquid staking or unstaking with + // the URL param state. + useEffect(() => { + setIsStakingInStore(isStaking); + }, [isStaking, setIsStakingInStore]); + + // Sync the URL param state of whether liquid staking or unstaking with + // the Zustand store state. + useEffect(() => { + setIsStaking(isStakingInStore); + }, [isStakingInStore, setIsStaking]); + return (
diff --git a/apps/tangle-dapp/components/BlueIconButton.tsx b/apps/tangle-dapp/components/BlueIconButton.tsx new file mode 100644 index 000000000..97f0b71f7 --- /dev/null +++ b/apps/tangle-dapp/components/BlueIconButton.tsx @@ -0,0 +1,46 @@ +import { IconBase } from '@webb-tools/icons/types'; +import { + IconButton, + Tooltip, + TooltipBody, + TooltipTrigger, +} from '@webb-tools/webb-ui-components'; +import { FC, JSX } from 'react'; +import { twMerge } from 'tailwind-merge'; + +export type BlueIconButtonProps = { + onClick: () => unknown; + tooltip: string; + Icon: (props: IconBase) => JSX.Element; + isDisabled?: boolean; +}; + +const BlueIconButton: FC = ({ + onClick, + tooltip, + Icon, + isDisabled = false, +}) => { + return ( + + + + + + + + + {tooltip} + + + ); +}; + +export default BlueIconButton; diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index 114547aae..99d84a891 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -11,19 +11,22 @@ import { } from '@tanstack/react-table'; import { Table } from '../../../libs/webb-ui-components/src/components/Table'; import { Pagination } from '../../../libs/webb-ui-components/src/components/Pagination'; -import { LsPool } from '../constants/liquidStaking/types'; +import { LsPool, LsProtocolId } from '../constants/liquidStaking/types'; import { ActionsDropdown, Avatar, AvatarGroup, Button, - IconButton, TANGLE_DOCS_URL, Typography, } from '@webb-tools/webb-ui-components'; import TokenAmountCell from '../components/tableCells/TokenAmountCell'; import pluralize from '../utils/pluralize'; -import { ArrowRight } from '@webb-tools/icons'; +import { + ArrowRight, + SubtractCircleLineIcon, + TokenIcon, +} from '@webb-tools/icons'; import useLsPools from '../data/liquidStaking/useLsPools'; import useSubstrateAddress from '../hooks/useSubstrateAddress'; import { BN } from '@polkadot/util'; @@ -32,11 +35,14 @@ import { GlassCard, TableStatus } from '../components'; import PercentageCell from '../components/tableCells/PercentageCell'; import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; import { ActionItemType } from '@webb-tools/webb-ui-components/components/ActionsDropdown/types'; -import { MinusCircledIcon } from '@radix-ui/react-icons'; import { useLsStore } from '../data/liquidStaking/useLsStore'; +import BlueIconButton from '../components/BlueIconButton'; +import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; +import useIsAccountConnected from '../hooks/useIsAccountConnected'; type MyLsPoolRow = LsPool & { myStake: BN; + lsProtocolId: LsProtocolId; isRoot: boolean; isNominator: boolean; isBouncer: boolean; @@ -45,8 +51,10 @@ type MyLsPoolRow = LsPool & { const COLUMN_HELPER = createColumnHelper(); const LsMyPoolsTable: FC = () => { + const isAccountConnected = useIsAccountConnected(); const substrateAddress = useSubstrateAddress(); const [sorting, setSorting] = useState([]); + const { setIsStaking, setLsPoolId, lsPoolId, isStaking } = useLsStore(); const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, @@ -84,11 +92,16 @@ const LsMyPoolsTable: FC = () => { isRoot: lsPool.ownerAddress === substrateAddress, isNominator: lsPool.nominatorAddress === substrateAddress, isBouncer: lsPool.bouncerAddress === substrateAddress, - }; + // TODO: Obtain which protocol this pool is associated with. For the parachain, there'd need to be some query to see what pools are associated with which parachain protocols. For Tangle networks, it's simply its own protocol. For now, using dummy data. + lsProtocolId: LsProtocolId.TANGLE_LOCAL, + } satisfies MyLsPoolRow; }); }, []); - const handleUnstakeClick = useCallback((poolId: number) => {}, []); + const handleUnstakeClick = useCallback((poolId: number) => { + setIsStaking(false); + setLsPoolId(poolId); + }, []); const columns = [ COLUMN_HELPER.accessor('id', { @@ -103,6 +116,14 @@ const LsMyPoolsTable: FC = () => { ), }), + COLUMN_HELPER.accessor('lsProtocolId', { + header: () => 'Protocol', + cell: (props) => { + const lsProtocol = getLsProtocolDef(props.getValue()); + + return ; + }, + }), COLUMN_HELPER.accessor('ownerAddress', { header: () => 'Owner', cell: (props) => ( @@ -161,6 +182,7 @@ const LsMyPoolsTable: FC = () => { if (props.row.original.isNominator) { actionItems.push({ label: 'Update Nominations', + // TODO: Implement onClick handler. onClick: () => void 0, }); } @@ -168,6 +190,7 @@ const LsMyPoolsTable: FC = () => { if (props.row.original.isBouncer) { actionItems.push({ label: 'Update Commission', + // TODO: Implement onClick handler. onClick: () => void 0, }); } @@ -175,22 +198,41 @@ const LsMyPoolsTable: FC = () => { if (props.row.original.isRoot) { actionItems.push({ label: 'Update Roles', + // TODO: Implement onClick handler. onClick: () => void 0, }); } + // Sanity check against logic errors. + if (hasAnyRole) { + assert(actionItems.length > 0); + } + // If the user has any role in the pool, show the short button style // to avoid taking up too much space. const isShortButtonStyle = hasAnyRole; + // Disable the stake button if the pool is currently selected, + // and the active intent is to unstake. + const isUnstakeDisabled = + lsPoolId === props.row.original.id && !isStaking; + return (
{isShortButtonStyle ? ( - - - + handleUnstakeClick(props.row.original.id)} + tooltip="Unstake" + Icon={SubtractCircleLineIcon} + /> ) : ( - )} @@ -224,7 +266,22 @@ const LsMyPoolsTable: FC = () => { enableSortingRemoval: false, }); - if (rows.length === 0) { + // TODO: Missing error and loading state. Should ideally abstract all these states into an abstract Table component, since it's getting reused in multiple places. + if (!isAccountConnected) { + return ( + + ); + } else if (rows.length === 0) { return ( { + const [activeAccount] = useActiveAccount(); + + return activeAccount?.address !== undefined; +}; + +export default useIsAccountConnected; diff --git a/libs/icons/src/SubtractCircleLineIcon.tsx b/libs/icons/src/SubtractCircleLineIcon.tsx new file mode 100644 index 000000000..2604970b9 --- /dev/null +++ b/libs/icons/src/SubtractCircleLineIcon.tsx @@ -0,0 +1,10 @@ +import { createIcon } from './create-icon'; +import { IconBase } from './types'; + +export const SubtractCircleLineIcon = (props: IconBase) => { + return createIcon({ + ...props, + d: 'M7.00065 14.1654C3.31875 14.1654 0.333984 11.1806 0.333984 7.4987C0.333984 3.8168 3.31875 0.832031 7.00065 0.832031C10.6825 0.832031 13.6673 3.8168 13.6673 7.4987C13.6673 11.1806 10.6825 14.1654 7.00065 14.1654ZM7.00065 12.832C9.94619 12.832 12.334 10.4442 12.334 7.4987C12.334 4.55318 9.94619 2.16536 7.00065 2.16536C4.05513 2.16536 1.66732 4.55318 1.66732 7.4987C1.66732 10.4442 4.05513 12.832 7.00065 12.832ZM3.66732 6.83203H10.334V8.16536H3.66732V6.83203Z', + displayName: 'SubtractCircleLineIcon', + }); +}; diff --git a/libs/icons/src/index.ts b/libs/icons/src/index.ts index 27a337a1d..0f15d47ee 100644 --- a/libs/icons/src/index.ts +++ b/libs/icons/src/index.ts @@ -149,6 +149,7 @@ export { default as WalletPayIcon } from './WalletPayIcon'; export * from './WaterDropletIcon'; export { default as WebbLogoIcon } from './WebbLogoIcon'; export * from './YouTubeFill'; +export * from './SubtractCircleLineIcon'; // Wallet icons export * from './wallets'; From 5b712b5cdb2bdd04f0a11b62ee6305c1b4b89cd2 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Thu, 3 Oct 2024 03:40:52 -0400 Subject: [PATCH 44/54] feat(tangle-dapp): Focus on LS input on mount --- .../LiquidStaking/stakeAndUnstake/LsInput.tsx | 216 +++++++++--------- .../stakeAndUnstake/LsStakeCard.tsx | 11 +- .../stakeAndUnstake/LsUnstakeCard.tsx | 11 +- 3 files changed, 131 insertions(+), 107 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx index ec253d369..b71b8dea4 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx @@ -2,7 +2,7 @@ import { BN } from '@polkadot/util'; import { Typography } from '@webb-tools/webb-ui-components'; -import { FC, ReactNode, useEffect } from 'react'; +import { forwardRef, ReactNode, useEffect } from 'react'; import { twMerge } from 'tailwind-merge'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; @@ -41,115 +41,121 @@ export type LsInputProps = { onTokenClick?: () => void; }; -const LsInput: FC = ({ - id, - amount, - decimals, - isReadOnly = false, - placeholder = '0', - isDerivativeVariant = false, - rightElement, - networkId, - token, - minAmount, - maxAmount, - maxErrorMessage = ERROR_NOT_ENOUGH_BALANCE, - onAmountChange, - setNetworkId, - className, - showPoolIndicator = true, -}) => { - const { lsProtocolId } = useLsStore(); - - const selectedProtocol = getLsProtocolDef(lsProtocolId); - - const minErrorMessage = ((): string | undefined => { - if (minAmount === undefined) { - return undefined; - } - - const unit = `${isDerivativeVariant ? LS_DERIVATIVE_TOKEN_PREFIX : ''}${token}`; - - const formattedMinAmount = formatBn(minAmount, decimals, { - fractionMaxLength: undefined, - includeCommas: true, - }); - - return `Amount must be at least ${formattedMinAmount} ${unit}`; - })(); - - const { displayAmount, handleChange, errorMessage, setDisplayAmount } = - useInputAmount({ +const LsInput = forwardRef( + ( + { + id, amount, - setAmount: onAmountChange, decimals, - min: minAmount, - minErrorMessage, - max: maxAmount, - maxErrorMessage, - }); - - // Update the display amount when the amount prop changes. - // Only do this for controlled (read-only) inputs. - useEffect(() => { - if (amount !== null) { - setDisplayAmount(amount); - } - }, [amount, setDisplayAmount]); - - const isError = errorMessage !== null; - - return ( - <> -
-
- - - {rightElement} -
+ isReadOnly = false, + placeholder = '0', + isDerivativeVariant = false, + rightElement, + networkId, + token, + minAmount, + maxAmount, + maxErrorMessage = ERROR_NOT_ENOUGH_BALANCE, + onAmountChange, + setNetworkId, + className, + showPoolIndicator = true, + }, + ref, + ) => { + const { lsProtocolId } = useLsStore(); + + const selectedProtocol = getLsProtocolDef(lsProtocolId); + + const minErrorMessage = ((): string | undefined => { + if (minAmount === undefined) { + return undefined; + } + + const unit = `${isDerivativeVariant ? LS_DERIVATIVE_TOKEN_PREFIX : ''}${token}`; + + const formattedMinAmount = formatBn(minAmount, decimals, { + fractionMaxLength: undefined, + includeCommas: true, + }); + + return `Amount must be at least ${formattedMinAmount} ${unit}`; + })(); + + const { displayAmount, handleChange, errorMessage, setDisplayAmount } = + useInputAmount({ + amount, + setAmount: onAmountChange, + decimals, + min: minAmount, + minErrorMessage, + max: maxAmount, + maxErrorMessage, + }); + + // Update the display amount when the amount prop changes. + // Only do this for controlled (read-only) inputs. + useEffect(() => { + if (amount !== null) { + setDisplayAmount(amount); + } + }, [amount, setDisplayAmount]); + + const isError = errorMessage !== null; + + return ( + <> +
+
+ -
+ {rightElement} +
+ +
+ +
+ handleChange(e.target.value)} + readOnly={isReadOnly} + /> -
- + ) : ( + )} - type="text" - placeholder={placeholder} - value={displayAmount} - onChange={(e) => handleChange(e.target.value)} - readOnly={isReadOnly} - /> - - {showPoolIndicator ? ( - - ) : ( - - )} +
-
- - {errorMessage !== null && ( - - * {errorMessage} - - )} - - ); -}; + + {errorMessage !== null && ( + + * {errorMessage} + + )} + + ); + }, +); export default LsInput; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index fee076f89..6dcad448c 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -13,7 +13,7 @@ import { Input, Typography, } from '@webb-tools/webb-ui-components'; -import React, { FC, useCallback, useEffect, useMemo } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react'; import { z } from 'zod'; import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; @@ -71,6 +71,7 @@ const LsStakeCard: FC = () => { const selectedProtocol = getLsProtocolDef(lsProtocolId); const tryChangeNetwork = useLsChangeNetwork(); const lsPoolMembers = useLsPoolMembers(); + const fromLsInputRef = useRef(null); const actionText = useMemo(() => { const defaultText = 'Join Pool & Stake'; @@ -189,9 +190,17 @@ const LsStakeCard: FC = () => { setFromAmount(null); }, [setFromAmount, lsNetworkId]); + // On mount, set the focus on the from input. + useEffect(() => { + if (fromLsInputRef.current !== null) { + fromLsInputRef.current.focus(); + } + }, []); + return ( <> { const [fromAmount, setFromAmount] = useState(null); const activeAccountAddress = useActiveAccountAddress(); const tryChangeNetwork = useLsChangeNetwork(); + const fromLsInputRef = useRef(null); const { lsProtocolId, setLsProtocolId, lsNetworkId, lsPoolId } = useLsStore(); @@ -153,6 +154,13 @@ const LsUnstakeCard: FC = () => { setFromAmount(null); }, [setFromAmount, lsNetworkId]); + // On mount, set the focus on the from input. + useEffect(() => { + if (fromLsInputRef.current !== null) { + fromLsInputRef.current.focus(); + } + }, []); + const stakedWalletBalance = ( { <> {/* TODO: Have a way to trigger a refresh of the amount once the wallet balance (max) button is clicked. Need to signal to the liquid staking input to update its display amount based on the `fromAmount` prop. */} Date: Thu, 3 Oct 2024 21:01:02 -0400 Subject: [PATCH 45/54] feat(tangle-dapp): Implement `OnboardingModal` component, update doc. links --- apps/tangle-dapp/app/liquid-staking/page.tsx | 188 ++++++++++++------ .../stakeAndUnstake/LsStakeCard.tsx | 6 +- .../stakeAndUnstake/LsUnstakeCard.tsx | 6 +- .../UnstakeRequestSubmittedModal.tsx | 6 +- .../CancelUnstakeModal.tsx | 6 +- .../UnstakeRequestsTable.tsx | 7 +- .../OnboardingModal/OnboardingItem.tsx | 31 +++ .../OnboardingModal/OnboardingModal.tsx | 97 +++++++++ .../components/OnboardingModal/index.ts | 2 + .../tangle-dapp/containers/LsMyPoolsTable.tsx | 22 +- .../containers/LsPoolsTable/LsPoolsTable.tsx | 5 +- .../LsPoolsTable/LsProtocolsTable.tsx | 10 +- .../containers/LsPoolsTableOld.tsx | 5 +- apps/tangle-dapp/hooks/useLocalStorage.ts | 6 +- apps/tangle-dapp/utils/Optional.ts | 4 + libs/icons/src/SubtractCircleLineIcon.tsx | 5 +- .../webb-ui-components/src/constants/index.ts | 2 + 17 files changed, 315 insertions(+), 93 deletions(-) create mode 100644 apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx create mode 100644 apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx create mode 100644 apps/tangle-dapp/components/OnboardingModal/index.ts diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 19db1cf57..7b09b788e 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -1,16 +1,28 @@ 'use client'; +import { + CoinIcon, + EditLine, + Search, + SparklingIcon, + WaterDropletIcon, +} from '@webb-tools/icons'; import { TabContent, TabsList as WebbTabsList, TabsRoot, TabTrigger, + TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; import { FC, useEffect } from 'react'; import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; +import OnboardingItem from '../../components/OnboardingModal/OnboardingItem'; +import OnboardingModal, { + OnboardingPageKey, +} from '../../components/OnboardingModal/OnboardingModal'; import StatItem from '../../components/StatItem'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; import LsMyPoolsTable from '../../containers/LsMyPoolsTable'; @@ -75,75 +87,123 @@ const LiquidStakingPage: FC = () => { }, [isStakingInStore, setIsStaking]); return ( -
-
-
- - Tangle Liquid Staking - - - - Get Liquid Staking Tokens (LSTs) to earn & unleash restaking on - Tangle Mainnet via delegation. - +
+ + + + + + + + + + + + +
+
+
+ + Tangle Liquid Staking + + + + Get Liquid Staking Tokens (LSTs) to earn & unleash restaking on + Tangle Mainnet via delegation. + +
+ +
+ +
-
- +
+ + setIsStaking(true)} + > + Stake + + + setIsStaking(false)} + > + Unstake + + + + {isStaking ? : }
-
-
- - setIsStaking(true)}> - Stake - - - setIsStaking(false)} - > - Unstake - - - - {isStaking ? : } + +
+ {/* Tabs List on the left */} + + {Object.values(Tab).map((tab, idx) => { + return ( + + + {tab} + + + ); + })} + +
+ + {/* Tabs Content */} + + + + + + + +
- - -
- {/* Tabs List on the left */} - - {Object.values(Tab).map((tab, idx) => { - return ( - - - {tab} - - - ); - })} - -
- - {/* Tabs Content */} - - - - - - - -
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 6dcad448c..4a2255dcb 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -16,7 +16,6 @@ import { import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react'; import { z } from 'zod'; -import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; import { LsNetworkId, LsProtocolId, @@ -44,6 +43,7 @@ import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; import useLsFeePercentage from './useLsFeePercentage'; import useLsSpendingLimits from './useLsSpendingLimits'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; const LsStakeCard: FC = () => { const [fromAmount, setFromAmount] = useSearchParamState({ @@ -207,7 +207,7 @@ const LsStakeCard: FC = () => { amount={fromAmount} decimals={selectedProtocol.decimals} onAmountChange={setFromAmount} - placeholder={`0 ${selectedProtocol.token}`} + placeholder="Enter amount to stake" rightElement={walletBalance} setProtocolId={setLsProtocolId} minAmount={minSpendable ?? undefined} @@ -221,7 +221,7 @@ const LsStakeCard: FC = () => { { const [isSelectTokenModalOpen, setIsSelectTokenModalOpen] = useState(false); @@ -188,7 +188,7 @@ const LsUnstakeCard: FC = () => { amount={fromAmount} decimals={selectedProtocol.decimals} onAmountChange={setFromAmount} - placeholder={`0 ${LS_DERIVATIVE_TOKEN_PREFIX}${selectedProtocol.token}`} + placeholder="Enter amount to unstake" rightElement={stakedWalletBalance} isDerivativeVariant minAmount={minSpendable ?? undefined} @@ -205,7 +205,7 @@ const LsUnstakeCard: FC = () => { networkId={lsNetworkId} amount={toAmount} decimals={selectedProtocol.decimals} - placeholder={`0 ${selectedProtocol.token}`} + placeholder={EMPTY_VALUE_PLACEHOLDER} token={selectedProtocol.token} isReadOnly className={isRefreshingExchangeRate ? 'animate-pulse' : undefined} diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/UnstakeRequestSubmittedModal.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/UnstakeRequestSubmittedModal.tsx index 1536b9f1c..8aecd0bb6 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/UnstakeRequestSubmittedModal.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/UnstakeRequestSubmittedModal.tsx @@ -6,7 +6,7 @@ import { ModalContent, ModalFooter, ModalHeader, - TANGLE_DOCS_URL, + TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks'; @@ -68,7 +68,9 @@ const UnstakeRequestSubmittedModal: FC = ({ tokens. - Learn More + + Learn More +
diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/CancelUnstakeModal.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/CancelUnstakeModal.tsx index 8eb8ba0cf..d8c902483 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/CancelUnstakeModal.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/CancelUnstakeModal.tsx @@ -5,7 +5,7 @@ import { ModalContent, ModalFooter, ModalHeader, - TANGLE_DOCS_URL, + TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; import { FC, useEffect } from 'react'; @@ -58,7 +58,9 @@ const CancelUnstakeModal: FC = ({ rewards. - Learn More + + Learn More +
diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index 369d91851..31739110e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -18,7 +18,7 @@ import { CheckBox, fuzzyFilter, Table, - TANGLE_DOCS_URL, + TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; import assert from 'assert'; @@ -297,7 +297,10 @@ const UnstakeRequestsTable: FC = () => { {rows !== null && rows.length === 0 && (
- + View Docs
diff --git a/apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx b/apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx new file mode 100644 index 000000000..641809e0e --- /dev/null +++ b/apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx @@ -0,0 +1,31 @@ +import { IconBase } from '@webb-tools/icons/types'; +import { Typography } from '@webb-tools/webb-ui-components'; +import { FC } from 'react'; + +export type OnboardingItemProps = { + Icon: (props: IconBase) => JSX.Element; + title: string; + description: string; +}; + +const OnboardingItem: FC = ({ + title, + description, + Icon, +}) => { + return ( +
+ + +
+ + {title} + + + {description} +
+
+ ); +}; + +export default OnboardingItem; diff --git a/apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx b/apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx new file mode 100644 index 000000000..5cdabb26f --- /dev/null +++ b/apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx @@ -0,0 +1,97 @@ +import { + Button, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + TANGLE_DOCS_URL, +} from '@webb-tools/webb-ui-components'; +import { FC, ReactElement, useEffect, useRef, useState } from 'react'; + +import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage'; +import { OnboardingItemProps } from './OnboardingItem'; + +export enum OnboardingPageKey { + BRIDGE, + LIQUID_STAKING, + RESTAKE, + BLUEPRINTS, + NOMINATE, +} + +export type OnboardingModalProps = { + pageKey: OnboardingPageKey; + linkHref?: string; + children: + | ReactElement + | ReactElement[]; +}; + +const OnboardingModal: FC = ({ + pageKey, + children, + linkHref = TANGLE_DOCS_URL, +}) => { + const [isOpen, setIsOpen] = useState(false); + const seenRef = useRef(false); + + const { setWithPreviousValue, refresh } = useLocalStorage( + LocalStorageKey.ONBOARDING_MODALS_SEEN, + ); + + // On load, check if the user has seen this modal before. + // If not, then trigger the modal to be shown, and remember + // that it has been seen. + useEffect(() => { + // Handle any possible data race. + if (seenRef.current) { + return; + } + + const seenOpt = refresh(); + const seen = seenOpt.orElse([]); + + // The user hasn't seen this modal yet. Show it, and remember + // that it has been seen. + if (!seen.includes(pageKey)) { + seenRef.current = true; + setIsOpen(true); + + setWithPreviousValue((prev) => { + if (prev === null || prev.value === null) { + return [pageKey]; + } + + return [...prev.value, pageKey]; + }); + } + }, [pageKey, refresh, setWithPreviousValue]); + + return ( + + + setIsOpen(false)}>Quick Start + +
{children}
+ + + + + + +
+
+ ); +}; + +export default OnboardingModal; diff --git a/apps/tangle-dapp/components/OnboardingModal/index.ts b/apps/tangle-dapp/components/OnboardingModal/index.ts new file mode 100644 index 000000000..0f1c0baf8 --- /dev/null +++ b/apps/tangle-dapp/components/OnboardingModal/index.ts @@ -0,0 +1,2 @@ +export * from './OnboardingModal'; +export * from './OnboardingItem'; diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index 99d84a891..b18eacaf1 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -17,7 +17,8 @@ import { Avatar, AvatarGroup, Button, - TANGLE_DOCS_URL, + ErrorFallback, + TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; import TokenAmountCell from '../components/tableCells/TokenAmountCell'; @@ -39,6 +40,7 @@ import { useLsStore } from '../data/liquidStaking/useLsStore'; import BlueIconButton from '../components/BlueIconButton'; import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; import useIsAccountConnected from '../hooks/useIsAccountConnected'; +import TableRowsSkeleton from '../components/LiquidStaking/TableRowsSkeleton'; type MyLsPoolRow = LsPool & { myStake: BN; @@ -98,6 +100,7 @@ const LsMyPoolsTable: FC = () => { }); }, []); + // TODO: Need to also switch network/protocol to the selected pool's network/protocol. const handleUnstakeClick = useCallback((poolId: number) => { setIsStaking(false); setLsPoolId(poolId); @@ -275,13 +278,21 @@ const LsMyPoolsTable: FC = () => { icon="🔍" buttonText="Learn More" buttonProps={{ - // TODO: Link to liquid staking pools docs page once implemented. - href: TANGLE_DOCS_URL, + href: TANGLE_DOCS_LIQUID_STAKING_URL, target: '_blank', }} /> ); - } else if (rows.length === 0) { + } else if (lsPools === null) { + return ; + } else if (lsPools instanceof Error) { + return ( + + ); + } else if (lsPools.length === 0) { return ( { icon="🔍" buttonText="Learn More" buttonProps={{ - // TODO: Link to liquid staking pools docs page once implemented. - href: TANGLE_DOCS_URL, + href: TANGLE_DOCS_LIQUID_STAKING_URL, target: '_blank', }} /> diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx index 235107d95..eaacef9e5 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx @@ -17,7 +17,7 @@ import { Avatar, AvatarGroup, Button, - TANGLE_DOCS_URL, + TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; import TokenAmountCell from '../../components/tableCells/TokenAmountCell'; @@ -152,8 +152,7 @@ const LsPoolsTable: FC = ({ pools, isShown }) => { icon="🔍" buttonText="Learn More" buttonProps={{ - // TODO: Link to liquid staking pools docs page once implemented. - href: TANGLE_DOCS_URL, + href: TANGLE_DOCS_LIQUID_STAKING_URL, target: '_blank', }} /> diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx index 905c7dfab..e1eb1090f 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, FC } from 'react'; import { useReactTable, getCoreRowModel, @@ -109,7 +109,10 @@ const PROTOCOL_COLUMNS = [ }), ]; -function LsProtocolsTable({ initialSorting = [] }: LsProtocolsTableProps) { +// TODO: Have the first row be expanded by default. +const LsProtocolsTable: FC = ({ + initialSorting = [], +}) => { const [sorting, setSorting] = useState(initialSorting); const getExpandedRowContent = useCallback( @@ -134,6 +137,7 @@ function LsProtocolsTable({ initialSorting = [] }: LsProtocolsTableProps) { return Array.from(lsPools.values()); }, []); + // TODO: Dummy data. Need to load actual protocol data or list it if it's hardcoded/limited to a few. const protocols: LsProtocolRow[] = [ { iconName: 'tangle', @@ -183,6 +187,6 @@ function LsProtocolsTable({ initialSorting = [] }: LsProtocolsTableProps) { tdClassName="border-0 !p-0 first:rounded-l-xl last:rounded-r-xl overflow-hidden" /> ); -} +}; export default LsProtocolsTable; diff --git a/apps/tangle-dapp/containers/LsPoolsTableOld.tsx b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx index c49694e35..eefda2a01 100644 --- a/apps/tangle-dapp/containers/LsPoolsTableOld.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx @@ -22,7 +22,7 @@ import { fuzzyFilter, Input, Table, - TANGLE_DOCS_URL, + TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; import assert from 'assert'; @@ -243,8 +243,7 @@ const LsPoolsTableOld: FC = () => { icon="🔍" buttonText="Learn More" buttonProps={{ - // TODO: Link to liquid staking pools docs page once implemented. - href: TANGLE_DOCS_URL, + href: TANGLE_DOCS_LIQUID_STAKING_URL, target: '_blank', }} /> diff --git a/apps/tangle-dapp/hooks/useLocalStorage.ts b/apps/tangle-dapp/hooks/useLocalStorage.ts index 25cc0c9fd..43b404532 100644 --- a/apps/tangle-dapp/hooks/useLocalStorage.ts +++ b/apps/tangle-dapp/hooks/useLocalStorage.ts @@ -3,6 +3,7 @@ import { HexString } from '@polkadot/util/types'; import { useCallback, useEffect, useState } from 'react'; +import { OnboardingPageKey } from '../components/OnboardingModal'; import { Payout, TangleTokenSymbol } from '../types'; import { BridgeQueueTxItem } from '../types/bridge'; import { @@ -26,6 +27,7 @@ export enum LocalStorageKey { SUBSTRATE_WALLETS_METADATA = 'substrateWalletsMetadata', BRIDGE_TX_QUEUE_BY_ACC = 'bridgeTxQueue', LIQUID_STAKING_TABLE_DATA = 'liquidStakingTableData', + ONBOARDING_MODALS_SEEN = 'onboardingModalsSeen', } export type PayoutsCache = { @@ -77,7 +79,9 @@ export type LocalStorageValueOf = ? TxQueueByAccount : T extends LocalStorageKey.LIQUID_STAKING_TABLE_DATA ? LiquidStakingTableData - : never; + : T extends LocalStorageKey.ONBOARDING_MODALS_SEEN + ? OnboardingPageKey[] + : never; export const getJsonFromLocalStorage = ( key: Key, diff --git a/apps/tangle-dapp/utils/Optional.ts b/apps/tangle-dapp/utils/Optional.ts index 00ebd86ef..5f635d4de 100644 --- a/apps/tangle-dapp/utils/Optional.ts +++ b/apps/tangle-dapp/utils/Optional.ts @@ -59,6 +59,10 @@ class Optional> { return this.value; } + orElse(value: T): T { + return this.value ?? value; + } + get isPresent(): boolean { return this.value !== null; } diff --git a/libs/icons/src/SubtractCircleLineIcon.tsx b/libs/icons/src/SubtractCircleLineIcon.tsx index 2604970b9..13ec69356 100644 --- a/libs/icons/src/SubtractCircleLineIcon.tsx +++ b/libs/icons/src/SubtractCircleLineIcon.tsx @@ -4,7 +4,10 @@ import { IconBase } from './types'; export const SubtractCircleLineIcon = (props: IconBase) => { return createIcon({ ...props, - d: 'M7.00065 14.1654C3.31875 14.1654 0.333984 11.1806 0.333984 7.4987C0.333984 3.8168 3.31875 0.832031 7.00065 0.832031C10.6825 0.832031 13.6673 3.8168 13.6673 7.4987C13.6673 11.1806 10.6825 14.1654 7.00065 14.1654ZM7.00065 12.832C9.94619 12.832 12.334 10.4442 12.334 7.4987C12.334 4.55318 9.94619 2.16536 7.00065 2.16536C4.05513 2.16536 1.66732 4.55318 1.66732 7.4987C1.66732 10.4442 4.05513 12.832 7.00065 12.832ZM3.66732 6.83203H10.334V8.16536H3.66732V6.83203Z', + width: 16, + height: 17, + viewBox: '0 0 16 17', + d: 'M7.33398 7.83203V5.16536H8.66732V7.83203H11.334V9.16536H8.66732V11.832H7.33398V9.16536H4.66732V7.83203H7.33398ZM8.00065 15.1654C4.31875 15.1654 1.33398 12.1806 1.33398 8.4987C1.33398 4.8168 4.31875 1.83203 8.00065 1.83203C11.6825 1.83203 14.6673 4.8168 14.6673 8.4987C14.6673 12.1806 11.6825 15.1654 8.00065 15.1654ZM8.00065 13.832C10.9462 13.832 13.334 11.4442 13.334 8.4987C13.334 5.55318 10.9462 3.16536 8.00065 3.16536C5.05513 3.16536 2.66732 5.55318 2.66732 8.4987C2.66732 11.4442 5.05513 13.832 8.00065 13.832Z', displayName: 'SubtractCircleLineIcon', }); }; diff --git a/libs/webb-ui-components/src/constants/index.ts b/libs/webb-ui-components/src/constants/index.ts index d401d2bc7..18b4b8ad3 100644 --- a/libs/webb-ui-components/src/constants/index.ts +++ b/libs/webb-ui-components/src/constants/index.ts @@ -42,6 +42,8 @@ export const TANGLE_PRESS_KIT_URL = 'https://www.tangle.tools/press-kit'; export const TANGLE_DOCS_URL = 'https://docs.tangle.tools/'; export const TANGLE_DOCS_STAKING_URL = 'https://docs.tangle.tools/restake/staking-intro'; +export const TANGLE_DOCS_LIQUID_STAKING_URL = + 'https://docs.tangle.tools/restake/lst-concepts'; export const TANGLE_GITHUB_URL = 'https://github.com/webb-tools/tangle'; export const WEBB_DOCS_URL = 'https://docs.webb.tools/'; From 5e284f3c23b464a0b9afe0b849d4b6231b73332e Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 4 Oct 2024 00:19:49 -0400 Subject: [PATCH 46/54] fix(tangle-dapp): Fix bug pool table rows not showing --- .../stakeAndUnstake/DetailItem.tsx | 2 +- .../stakeAndUnstake/LsAgnosticBalance.tsx | 15 +++- .../LiquidStaking/stakeAndUnstake/LsInput.tsx | 2 + .../stakeAndUnstake/LsStakeCard.tsx | 2 +- .../stakeAndUnstake/LsUnstakeCard.tsx | 6 +- .../stakeAndUnstake/SelectedPoolIndicator.tsx | 4 +- .../stakeAndUnstake/useLsAgnosticBalance.ts | 5 +- .../components/account/AccountAddress.tsx | 2 +- apps/tangle-dapp/constants/networks.ts | 4 +- .../tangle-dapp/containers/LsMyPoolsTable.tsx | 86 +++++++++++-------- .../LsPoolsTable/LsProtocolsTable.tsx | 2 +- .../IndependentAllocationInput.tsx | 5 +- .../UpdateNominationsTxContainer.tsx | 21 +++-- ...yName.ts => useLsActivePoolDisplayName.ts} | 4 +- apps/tangle-dapp/hooks/useEvmPrecompileFee.ts | 2 +- apps/tangle-dapp/hooks/useSubstrateTx.ts | 2 +- .../src/components/Avatar/Avatar.tsx | 58 +++++++------ .../src/components/Avatar/types.ts | 1 + .../components/AvatarGroup/AvatarGroup.tsx | 21 ++--- 19 files changed, 134 insertions(+), 110 deletions(-) rename apps/tangle-dapp/data/liquidStaking/{useSelectedPoolDisplayName.ts => useLsActivePoolDisplayName.ts} (78%) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/DetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/DetailItem.tsx index a6e3a876c..be7683f82 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/DetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/DetailItem.tsx @@ -21,7 +21,7 @@ const DetailItem: FC = ({ title, tooltip, value }) => { return (
- + {title} diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index ef9a22511..577061020 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -12,7 +12,7 @@ import { FC, useCallback, useMemo, useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; -import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; +import useLsActivePoolDisplayName from '../../../data/liquidStaking/useLsActivePoolDisplayName'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import formatBn from '../../../utils/formatBn'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; @@ -34,6 +34,7 @@ const LsAgnosticBalance: FC = ({ const [isHovering, setIsHovering] = useState(false); const balance = useLsAgnosticBalance(isNative); const { lsProtocolId } = useLsStore(); + const lsActivePoolDisplayName = useLsActivePoolDisplayName(); const protocol = getLsProtocolDef(lsProtocolId); const formattedBalance = useMemo(() => { @@ -50,10 +51,16 @@ const LsAgnosticBalance: FC = ({ includeCommas: true, }); - const derivativePrefix = isNative ? '' : LS_DERIVATIVE_TOKEN_PREFIX; + const unit = isNative ? protocol.token : lsActivePoolDisplayName; - return `${formattedBalance} ${derivativePrefix}${protocol.token}`; - }, [balance, protocol.decimals, isNative, protocol.token]); + return `${formattedBalance} ${unit}`; + }, [ + balance, + protocol.decimals, + protocol.token, + isNative, + lsActivePoolDisplayName, + ]); const isClickable = onlyShowTooltipWhenBalanceIsSet && diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx index b71b8dea4..d024b751a 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx @@ -158,4 +158,6 @@ const LsInput = forwardRef( }, ); +LsInput.displayName = 'LsInput'; + export default LsInput; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 4a2255dcb..7a3c9efae 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -16,6 +16,7 @@ import { import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react'; import { z } from 'zod'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LsNetworkId, LsProtocolId, @@ -43,7 +44,6 @@ import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; import useLsFeePercentage from './useLsFeePercentage'; import useLsSpendingLimits from './useLsSpendingLimits'; -import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; const LsStakeCard: FC = () => { const [fromAmount, setFromAmount] = useSearchParamState({ diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index 4278778dc..f17d3c637 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -10,6 +10,7 @@ import { Button } from '@webb-tools/webb-ui-components'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { z } from 'zod'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LsNetworkId, LsProtocolId, @@ -36,7 +37,6 @@ import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; import useLsFeePercentage from './useLsFeePercentage'; import useLsSpendingLimits from './useLsSpendingLimits'; -import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; const LsUnstakeCard: FC = () => { const [isSelectTokenModalOpen, setIsSelectTokenModalOpen] = useState(false); @@ -142,7 +142,7 @@ const LsUnstakeCard: FC = () => { const handleTokenSelect = useCallback(() => { setIsSelectTokenModalOpen(false); - }, []); + }, [setIsSelectTokenModalOpen]); const selectTokenModalOptions = useMemo(() => { // TODO: Dummy data. @@ -195,7 +195,6 @@ const LsUnstakeCard: FC = () => { maxAmount={maxSpendable ?? undefined} maxErrorMessage="Not enough stake to redeem" onTokenClick={() => setIsSelectTokenModalOpen(true)} - showPoolIndicator={false} /> @@ -209,6 +208,7 @@ const LsUnstakeCard: FC = () => { token={selectedProtocol.token} isReadOnly className={isRefreshingExchangeRate ? 'animate-pulse' : undefined} + showPoolIndicator={false} /> {/* Details */} diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx index 95181db8e..e89045a0e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx @@ -2,14 +2,14 @@ import { Typography } from '@webb-tools/webb-ui-components'; import { FC } from 'react'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useSelectedPoolDisplayName from '../../../data/liquidStaking/useSelectedPoolDisplayName'; +import useLsActivePoolDisplayName from '../../../data/liquidStaking/useLsActivePoolDisplayName'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import LsTokenIcon from '../../LsTokenIcon'; const SelectedPoolIndicator: FC = () => { const { lsProtocolId } = useLsStore(); const selectedProtocol = getLsProtocolDef(lsProtocolId); - const selectedPoolDisplayName = useSelectedPoolDisplayName(); + const selectedPoolDisplayName = useLsActivePoolDisplayName(); return (
diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts index b7c688a1f..900823cfc 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/useLsAgnosticBalance.ts @@ -7,7 +7,7 @@ import useBalances from '../../../data/balances/useBalances'; import useParachainBalances from '../../../data/liquidStaking/parachain/useParachainBalances'; import useLsPoolBalance from '../../../data/liquidStaking/tangle/useLsPoolBalance'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; +import useIsAccountConnected from '../../../hooks/useIsAccountConnected'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; type BalanceUpdater = ( @@ -42,7 +42,6 @@ const createBalanceStateUpdater = ( }; const useLsAgnosticBalance = (isNative: boolean) => { - const activeAccountAddress = useActiveAccountAddress(); const { nativeBalances, liquidBalances } = useParachainBalances(); const { free: tangleFreeBalance } = useBalances(); const { lsProtocolId, lsNetworkId } = useLsStore(); @@ -53,7 +52,7 @@ const useLsAgnosticBalance = (isNative: boolean) => { >(EMPTY_VALUE_PLACEHOLDER); const parachainBalances = isNative ? nativeBalances : liquidBalances; - const isAccountConnected = activeAccountAddress !== null; + const isAccountConnected = useIsAccountConnected(); const protocol = getLsProtocolDef(lsProtocolId); // Reset balance to a placeholder when the active account is diff --git a/apps/tangle-dapp/components/account/AccountAddress.tsx b/apps/tangle-dapp/components/account/AccountAddress.tsx index 34ed2c36a..75613100a 100644 --- a/apps/tangle-dapp/components/account/AccountAddress.tsx +++ b/apps/tangle-dapp/components/account/AccountAddress.tsx @@ -63,7 +63,7 @@ const AccountAddress: FC = () => { // Switch between EVM & Substrate addresses. const handleAddressTypeToggle = useCallback(() => { setIsDisplayingEvmAddress((previous) => !previous); - }, []); + }, [setIsDisplayingEvmAddress]); const iconFillColorClass = 'dark:!fill-mono-80 !fill-mono-160'; diff --git a/apps/tangle-dapp/constants/networks.ts b/apps/tangle-dapp/constants/networks.ts index 603e6aa0e..3c2ab1aa0 100644 --- a/apps/tangle-dapp/constants/networks.ts +++ b/apps/tangle-dapp/constants/networks.ts @@ -12,11 +12,11 @@ export const NETWORK_FEATURE_MAP: Record = { [NetworkId.TANGLE_MAINNET]: [NetworkFeature.EraStakersOverview], [NetworkId.TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV]: [], [NetworkId.TANGLE_RESTAKING_PARACHAIN_TESTNET]: [], + // Assume that local and custom endpoints are using an updated runtime + // version which includes support for the era stakers overview query. [NetworkId.TANGLE_LOCAL_DEV]: [ NetworkFeature.EraStakersOverview, NetworkFeature.LsPools, ], - // Assume that local and custom endpoints are using an updated runtime - // version which includes support for the era stakers overview query. [NetworkId.CUSTOM]: [NetworkFeature.EraStakersOverview], }; diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx index b18eacaf1..aa1db1540 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsMyPoolsTable.tsx @@ -16,7 +16,6 @@ import { ActionsDropdown, Avatar, AvatarGroup, - Button, ErrorFallback, TANGLE_DOCS_LIQUID_STAKING_URL, Typography, @@ -24,7 +23,7 @@ import { import TokenAmountCell from '../components/tableCells/TokenAmountCell'; import pluralize from '../utils/pluralize'; import { - ArrowRight, + AddCircleLineIcon, SubtractCircleLineIcon, TokenIcon, } from '@webb-tools/icons'; @@ -98,13 +97,25 @@ const LsMyPoolsTable: FC = () => { lsProtocolId: LsProtocolId.TANGLE_LOCAL, } satisfies MyLsPoolRow; }); - }, []); + }, [lsPools, substrateAddress]); // TODO: Need to also switch network/protocol to the selected pool's network/protocol. - const handleUnstakeClick = useCallback((poolId: number) => { - setIsStaking(false); - setLsPoolId(poolId); - }, []); + const handleUnstakeClick = useCallback( + (poolId: number) => { + setIsStaking(false); + setLsPoolId(poolId); + }, + [setIsStaking, setLsPoolId], + ); + + // TODO: Need to also switch network/protocol to the selected pool's network/protocol. + const handleIncreaseStakeClick = useCallback( + (poolId: number) => { + setIsStaking(true); + setLsPoolId(poolId); + }, + [setIsStaking, setLsPoolId], + ); const columns = [ COLUMN_HELPER.accessor('id', { @@ -124,7 +135,13 @@ const LsMyPoolsTable: FC = () => { cell: (props) => { const lsProtocol = getLsProtocolDef(props.getValue()); - return ; + return ( +
+ + + {lsProtocol.name} +
+ ); }, }), COLUMN_HELPER.accessor('ownerAddress', { @@ -147,6 +164,8 @@ const LsMyPoolsTable: FC = () => { {props.row.original.validators.map((substrateAddress) => ( { assert(actionItems.length > 0); } - // If the user has any role in the pool, show the short button style - // to avoid taking up too much space. - const isShortButtonStyle = hasAnyRole; - - // Disable the stake button if the pool is currently selected, + // Disable the unstake button if the pool is currently selected, // and the active intent is to unstake. - const isUnstakeDisabled = + const isUnstakeActionDisabled = lsPoolId === props.row.original.id && !isStaking; - return ( -
- {isShortButtonStyle ? ( - handleUnstakeClick(props.row.original.id)} - tooltip="Unstake" - Icon={SubtractCircleLineIcon} - /> - ) : ( - - )} + // Disable the stake button if the pool is currently selected, + // and the active intent is to stake. + const isStakeActionDisabled = + lsPoolId === props.row.original.id && isStaking; + return ( +
{/** * Show management actions if the active user has any role in * the pool. @@ -247,6 +249,20 @@ const LsMyPoolsTable: FC = () => { {hasAnyRole && ( )} + + handleUnstakeClick(props.row.original.id)} + tooltip="Unstake" + Icon={SubtractCircleLineIcon} + /> + + handleIncreaseStakeClick(props.row.original.id)} + tooltip="Increase Stake" + Icon={AddCircleLineIcon} + />
); }, @@ -284,7 +300,7 @@ const LsMyPoolsTable: FC = () => { /> ); } else if (lsPools === null) { - return ; + return ; } else if (lsPools instanceof Error) { return ( { description={[lsPools.message]} /> ); - } else if (lsPools.length === 0) { + } else if (rows.length === 0) { return ( = ({ } return Array.from(lsPools.values()); - }, []); + }, [lsPools]); // TODO: Dummy data. Need to load actual protocol data or list it if it's hardcoded/limited to a few. const protocols: LsProtocolRow[] = [ diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx index fa87f3f6b..ac1d26013 100644 --- a/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx +++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationInput.tsx @@ -51,10 +51,7 @@ const IndependentAllocationInput: FC = ({ // TODO: This is misleading, because it defaults to `false` when `servicesWithJobs` is still loading. It needs to default to `null` and have its loading state handled appropriately. const hasActiveJob = false; - const substrateAllocationAmount = useMemo(() => { - return null; - }, []); - + const substrateAllocationAmount = null; const min = substrateAllocationAmount; const [isDropdownVisible, setIsDropdownVisible] = useState(false); diff --git a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx b/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx index c5ff3e873..dcc37f8af 100644 --- a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx +++ b/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx @@ -98,15 +98,18 @@ const UpdateNominationsTxContainer: FC = ({ // so we need to handle the conversion between set <> array const handleSelectedValidatorsChange = useCallback< Dispatch>> - >((nextValueOrUpdater) => { - if (typeof nextValueOrUpdater === 'function') { - setSelectedValidators((prev) => { - return Array.from(nextValueOrUpdater(new Set(prev))); - }); - } else { - setSelectedValidators(Array.from(nextValueOrUpdater)); - } - }, []); + >( + (nextValueOrUpdater) => { + if (typeof nextValueOrUpdater === 'function') { + setSelectedValidators((prev) => { + return Array.from(nextValueOrUpdater(new Set(prev))); + }); + } else { + setSelectedValidators(Array.from(nextValueOrUpdater)); + } + }, + [setSelectedValidators], + ); return ( diff --git a/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts b/apps/tangle-dapp/data/liquidStaking/useLsActivePoolDisplayName.ts similarity index 78% rename from apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts rename to apps/tangle-dapp/data/liquidStaking/useLsActivePoolDisplayName.ts index 4ed0e9350..0a5a3c558 100644 --- a/apps/tangle-dapp/data/liquidStaking/useSelectedPoolDisplayName.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsActivePoolDisplayName.ts @@ -2,7 +2,7 @@ import { LsPoolDisplayName } from '../../constants/liquidStaking/types'; import useLsPoolsMetadata from './useLsPoolsMetadata'; import { useLsStore } from './useLsStore'; -const useSelectedPoolDisplayName = (): LsPoolDisplayName | null => { +const useLsActivePoolDisplayName = (): LsPoolDisplayName | null => { const { lsPoolId } = useLsStore(); const lsPoolsMetadata = useLsPoolsMetadata(); @@ -15,4 +15,4 @@ const useSelectedPoolDisplayName = (): LsPoolDisplayName | null => { return `${name}#${lsPoolId}`; }; -export default useSelectedPoolDisplayName; +export default useLsActivePoolDisplayName; diff --git a/apps/tangle-dapp/hooks/useEvmPrecompileFee.ts b/apps/tangle-dapp/hooks/useEvmPrecompileFee.ts index 580a12031..782f94206 100644 --- a/apps/tangle-dapp/hooks/useEvmPrecompileFee.ts +++ b/apps/tangle-dapp/hooks/useEvmPrecompileFee.ts @@ -59,7 +59,7 @@ function useEvmPrecompileFeeFetcher() { const resetStates = useCallback(() => { setStatus('idle'); setError(null); - }, []); + }, [setStatus, setError]); return { status, diff --git a/apps/tangle-dapp/hooks/useSubstrateTx.ts b/apps/tangle-dapp/hooks/useSubstrateTx.ts index 184478a43..426c0884b 100644 --- a/apps/tangle-dapp/hooks/useSubstrateTx.ts +++ b/apps/tangle-dapp/hooks/useSubstrateTx.ts @@ -174,7 +174,7 @@ function useSubstrateTx( setStatus(TxStatus.NOT_YET_INITIATED); setTxHash(null); setError(null); - }, []); + }, [setStatus, setTxHash, setError]); // Timeout the transaction if it's taking too long. This // won't cancel it, but it will alert the user that something diff --git a/libs/webb-ui-components/src/components/Avatar/Avatar.tsx b/libs/webb-ui-components/src/components/Avatar/Avatar.tsx index 6e543339c..295b17c96 100644 --- a/libs/webb-ui-components/src/components/Avatar/Avatar.tsx +++ b/libs/webb-ui-components/src/components/Avatar/Avatar.tsx @@ -7,6 +7,7 @@ import { Typography } from '../../typography/Typography'; import { Identicon } from './Identicon'; import { AvatarProps } from './types'; import { getAvatarClassNames, getAvatarSizeInPx } from './utils'; +import { Tooltip, TooltipBody, TooltipTrigger } from '../Tooltip'; /** * Webb Avatar component @@ -19,25 +20,25 @@ import { getAvatarClassNames, getAvatarSizeInPx } from './utils'; * - `alt`: Alternative text if source is unavailable * - `fallback`: Optional fallback text if source image is unavailable * - `className`: Outer class name + * - `tooltip`: Tooltip text to display on hover * * @example * * */ -export const Avatar: React.FC = (props) => { - const { - alt, - className: outerClassName, - darkMode, - fallback, - size = 'md', - sourceVariant = 'address', - src, - theme = 'polkadot', - value: valueProp, - } = props; - - const sizeClassName = useMemo(() => { +export const Avatar: React.FC = ({ + alt, + className: outerClassName, + darkMode, + fallback, + size = 'md', + sourceVariant = 'address', + src, + theme = 'polkadot', + value: valueProp, + tooltip, +}) => { + const sizeClassName = (() => { switch (size) { case 'sm': return 'w-4 h-4'; @@ -45,26 +46,17 @@ export const Avatar: React.FC = (props) => { return 'w-6 h-6'; case 'lg': return 'w-12 h-12'; - default: - return 'w-6 h-6'; } - }, [size]); + })(); const classNames = useMemo(() => { return getAvatarClassNames(darkMode); }, [darkMode]); - const typoVariant = useMemo( - () => (size === 'md' ? 'body4' : 'body1'), - [size], - ); - - const valueAddress = useMemo( - () => (sourceVariant === 'address' ? valueProp : undefined), - [valueProp, sourceVariant], - ); + const typoVariant = size === 'md' ? 'body4' : 'body1'; + const valueAddress = sourceVariant === 'address' ? valueProp : undefined; - return ( + const avatar = ( = (props) => { )} ); + + return tooltip !== undefined ? ( + + {avatar} + + + {tooltip} + + + ) : ( + avatar + ); }; diff --git a/libs/webb-ui-components/src/components/Avatar/types.ts b/libs/webb-ui-components/src/components/Avatar/types.ts index ac7c8fdd5..6b6401698 100644 --- a/libs/webb-ui-components/src/components/Avatar/types.ts +++ b/libs/webb-ui-components/src/components/Avatar/types.ts @@ -31,4 +31,5 @@ export interface AvatarProps extends IWebbComponentBase, IdenticonBaseProps { * @default "address" * */ sourceVariant?: 'address' | 'uri'; + tooltip?: string; } diff --git a/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx b/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx index 2ae2f75b1..5011ee8c5 100644 --- a/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx +++ b/libs/webb-ui-components/src/components/AvatarGroup/AvatarGroup.tsx @@ -30,27 +30,22 @@ export const AvatarGroup = forwardRef( ) as AvatarChildElement[]; }, [childrenProp]); - const totalAvatars = useMemo( - () => total || children.length, - [children.length, total], - ); - - const extraAvatars = useMemo(() => totalAvatars - max, [totalAvatars, max]); - - const mergedClsx = useMemo( - () => twMerge('flex items-center space-x-1', className), - [className], - ); + const totalAvatars = total || children.length; + const extraAvatars = totalAvatars - max; return ( -
+
{children.slice(0, max).map((child, index) => { return React.cloneElement(child, { key: index, ...child.props, size: 'md', - className: 'mx-[-4px] last:mx-0', + className: 'relative mx-[-4px] last:mx-0 hover:z-10', }); })}
From b09eff2cf7bfa36ddbe05b0a178eb297e63366b1 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 4 Oct 2024 03:44:01 -0400 Subject: [PATCH 47/54] refactor(tangle-dapp): Standardize percentage formatting --- apps/tangle-dapp/app/liquid-staking/page.tsx | 4 +- .../LiquidStaking}/LsMyPoolsTable.tsx | 139 +++------- .../stakeAndUnstake/FeeDetailItem.tsx | 3 +- .../UnstakeRequestsTable.tsx | 3 +- .../NominationsTable/NominationsTable.tsx | 6 +- .../ValidatorSelectionTable.tsx | 3 +- .../ValidatorTable/ValidatorTable.tsx | 6 +- .../components/tableCells/PercentageCell.tsx | 3 +- .../components/tables/Operators/index.tsx | 7 +- .../components/tables/TableCellWrapper.tsx | 6 +- .../components/tables/Vaults/index.tsx | 8 +- .../containers/LsMyProtocolsTable.tsx | 254 ++++++++++++++++++ .../LsPoolsTable/LsProtocolsTable.tsx | 25 +- .../containers/LsPoolsTableOld.tsx | 16 +- .../data/liquidStaking/adapters/polkadot.tsx | 3 +- .../data/liquidStaking/useLsPools.ts | 1 + .../useLsValidatorSelectionTableColumns.tsx | 5 +- apps/tangle-dapp/utils/formatPercentage.ts | 12 + libs/icons/src/SubtractCircleLineIcon.tsx | 2 +- 19 files changed, 342 insertions(+), 164 deletions(-) rename apps/tangle-dapp/{containers => components/LiquidStaking}/LsMyPoolsTable.tsx (64%) create mode 100644 apps/tangle-dapp/containers/LsMyProtocolsTable.tsx create mode 100644 apps/tangle-dapp/utils/formatPercentage.ts diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 7b09b788e..cde46dc81 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -25,7 +25,7 @@ import OnboardingModal, { } from '../../components/OnboardingModal/OnboardingModal'; import StatItem from '../../components/StatItem'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; -import LsMyPoolsTable from '../../containers/LsMyPoolsTable'; +import LsMyProtocolsTable from '../../containers/LsMyProtocolsTable'; import { LsProtocolsTable } from '../../containers/LsPoolsTable'; import useNetworkStore from '../../context/useNetworkStore'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; @@ -200,7 +200,7 @@ const LiquidStakingPage: FC = () => { - +
diff --git a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx similarity index 64% rename from apps/tangle-dapp/containers/LsMyPoolsTable.tsx rename to apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx index aa1db1540..f5bc2ae18 100644 --- a/apps/tangle-dapp/containers/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx @@ -9,39 +9,30 @@ import { getPaginationRowModel, createColumnHelper, } from '@tanstack/react-table'; -import { Table } from '../../../libs/webb-ui-components/src/components/Table'; -import { Pagination } from '../../../libs/webb-ui-components/src/components/Pagination'; -import { LsPool, LsProtocolId } from '../constants/liquidStaking/types'; +import { Table } from '../../../../libs/webb-ui-components/src/components/Table'; +import { LsPool, LsProtocolId } from '../../constants/liquidStaking/types'; import { ActionsDropdown, Avatar, AvatarGroup, - ErrorFallback, TANGLE_DOCS_LIQUID_STAKING_URL, Typography, } from '@webb-tools/webb-ui-components'; -import TokenAmountCell from '../components/tableCells/TokenAmountCell'; -import pluralize from '../utils/pluralize'; -import { - AddCircleLineIcon, - SubtractCircleLineIcon, - TokenIcon, -} from '@webb-tools/icons'; -import useLsPools from '../data/liquidStaking/useLsPools'; -import useSubstrateAddress from '../hooks/useSubstrateAddress'; +import TokenAmountCell from '../tableCells/TokenAmountCell'; +import { AddCircleLineIcon, SubtractCircleLineIcon } from '@webb-tools/icons'; import { BN } from '@polkadot/util'; import assert from 'assert'; -import { GlassCard, TableStatus } from '../components'; -import PercentageCell from '../components/tableCells/PercentageCell'; -import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; +import { TableStatus } from '..'; +import PercentageCell from '../tableCells/PercentageCell'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; import { ActionItemType } from '@webb-tools/webb-ui-components/components/ActionsDropdown/types'; -import { useLsStore } from '../data/liquidStaking/useLsStore'; -import BlueIconButton from '../components/BlueIconButton'; -import getLsProtocolDef from '../utils/liquidStaking/getLsProtocolDef'; -import useIsAccountConnected from '../hooks/useIsAccountConnected'; -import TableRowsSkeleton from '../components/LiquidStaking/TableRowsSkeleton'; +import { useLsStore } from '../../data/liquidStaking/useLsStore'; +import BlueIconButton from '../BlueIconButton'; +import useIsAccountConnected from '../../hooks/useIsAccountConnected'; +import { twMerge } from 'tailwind-merge'; +import pluralize from '../../utils/pluralize'; -type MyLsPoolRow = LsPool & { +export type LsMyPoolRow = LsPool & { myStake: BN; lsProtocolId: LsProtocolId; isRoot: boolean; @@ -49,11 +40,15 @@ type MyLsPoolRow = LsPool & { isBouncer: boolean; }; -const COLUMN_HELPER = createColumnHelper(); +const COLUMN_HELPER = createColumnHelper(); + +export type LsMyPoolsTableProps = { + pools: LsMyPoolRow[]; + isShown: boolean; +}; -const LsMyPoolsTable: FC = () => { +const LsMyPoolsTable: FC = ({ pools, isShown }) => { const isAccountConnected = useIsAccountConnected(); - const substrateAddress = useSubstrateAddress(); const [sorting, setSorting] = useState([]); const { setIsStaking, setLsPoolId, lsPoolId, isStaking } = useLsStore(); @@ -70,35 +65,6 @@ const LsMyPoolsTable: FC = () => { [pageIndex, pageSize], ); - const lsPoolsMap = useLsPools(); - - const lsPools = - lsPoolsMap instanceof Map ? Array.from(lsPoolsMap.values()) : lsPoolsMap; - - const rows: MyLsPoolRow[] = useMemo(() => { - if (substrateAddress === null || !Array.isArray(lsPools)) { - return []; - } - - return lsPools - .filter((lsPool) => lsPool.members.has(substrateAddress)) - .map((lsPool) => { - const account = lsPool.members.get(substrateAddress); - - assert(account !== undefined); - - return { - ...lsPool, - myStake: account.balance.toBn(), - isRoot: lsPool.ownerAddress === substrateAddress, - isNominator: lsPool.nominatorAddress === substrateAddress, - isBouncer: lsPool.bouncerAddress === substrateAddress, - // TODO: Obtain which protocol this pool is associated with. For the parachain, there'd need to be some query to see what pools are associated with which parachain protocols. For Tangle networks, it's simply its own protocol. For now, using dummy data. - lsProtocolId: LsProtocolId.TANGLE_LOCAL, - } satisfies MyLsPoolRow; - }); - }, [lsPools, substrateAddress]); - // TODO: Need to also switch network/protocol to the selected pool's network/protocol. const handleUnstakeClick = useCallback( (poolId: number) => { @@ -130,20 +96,6 @@ const LsMyPoolsTable: FC = () => { ), }), - COLUMN_HELPER.accessor('lsProtocolId', { - header: () => 'Protocol', - cell: (props) => { - const lsProtocol = getLsProtocolDef(props.getValue()); - - return ( -
- - - {lsProtocol.name} -
- ); - }, - }), COLUMN_HELPER.accessor('ownerAddress', { header: () => 'Owner', cell: (props) => ( @@ -270,7 +222,7 @@ const LsMyPoolsTable: FC = () => { ]; const table = useReactTable({ - data: rows, + data: pools, columns: columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -299,16 +251,7 @@ const LsMyPoolsTable: FC = () => { }} /> ); - } else if (lsPools === null) { - return ; - } else if (lsPools instanceof Error) { - return ( - - ); - } else if (rows.length === 0) { + } else if (pools.length === 0) { return ( { } return ( - -
-
- - 1)} - className="border-t-0 py-5" - /> - - +
1 || pools.length === 0)} + className={twMerge( + 'rounded-2xl overflow-hidden bg-mono-20 dark:bg-mono-200 px-3', + isShown ? 'animate-slide-down' : 'animate-slide-up', + )} + thClassName="py-3 !font-normal !bg-transparent border-t-0 border-b text-mono-120 dark:text-mono-100 border-mono-60 dark:border-mono-160" + tbodyClassName="!bg-transparent" + tdClassName="!bg-inherit border-t-0" + isPaginated + /> ); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx index f90b8bcfb..ac83621e4 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx @@ -4,6 +4,7 @@ import { FC, useMemo } from 'react'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LsProtocolId } from '../../../constants/liquidStaking/types'; import formatBn from '../../../utils/formatBn'; +import formatPercentage from '../../../utils/formatPercentage'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import scaleAmountByPercentage from '../../../utils/scaleAmountByPercentage'; import DetailItem from './DetailItem'; @@ -51,7 +52,7 @@ const FeeDetailItem: FC = ({ const feeTitle = typeof feePercentage !== 'number' ? 'Fee' - : `Fee (${(feePercentage * 100).toFixed(2)}%)`; + : `Fee (${formatPercentage(feePercentage * 100)}%)`; return ; }; diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx index 31739110e..d77c04009 100644 --- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable.tsx @@ -34,6 +34,7 @@ import { import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import addCommasToNumber from '../../../utils/addCommasToNumber'; +import formatPercentage from '../../../utils/formatPercentage'; import isLsParachainChainId from '../../../utils/liquidStaking/isLsParachainChainId'; import stringifyTimeUnit from '../../../utils/liquidStaking/stringifyTimeUnit'; import GlassCard from '../../GlassCard'; @@ -107,7 +108,7 @@ const COLUMNS = [ return undefined; } - return (progress * 100).toFixed(2) + '%'; + return formatPercentage(progress * 100); } // Otherwise, it must be a Parachain unstake request's // remaining time unit. diff --git a/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx b/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx index 3f087805c..f94c9cab5 100644 --- a/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx +++ b/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx @@ -27,6 +27,7 @@ import { sortBnValueForNomineeOrValidator, } from '../../utils/table'; import { HeaderCell, StringCell } from '../tableCells'; +import PercentageCell from '../tableCells/PercentageCell'; import TokenAmountCell from '../tableCells/TokenAmountCell'; const columnHelper = createColumnHelper(); @@ -92,10 +93,7 @@ const columns = [ columnHelper.accessor('commission', { header: () => , cell: (props) => ( - + ), sortingFn: sortBnValueForNomineeOrValidator, }), diff --git a/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx index aaf2a2b45..0275c61f1 100644 --- a/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx +++ b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx @@ -38,6 +38,7 @@ import React, { import { Validator } from '../../types'; import calculateCommission from '../../utils/calculateCommission'; +import formatPercentage from '../../utils/formatPercentage'; import { getSortAddressOrIdentityFnc, sortBnValueForNomineeOrValidator, @@ -187,7 +188,7 @@ const ValidatorSelectionTable: FC = ({ cell: (props) => (
- {calculateCommission(props.getValue()).toFixed(2)}% + {formatPercentage(calculateCommission(props.getValue()))}
), diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index cc576829b..8768c4c36 100644 --- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx +++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx @@ -32,6 +32,7 @@ import { sortBnValueForNomineeOrValidator, } from '../../utils/table'; import { HeaderCell, StringCell } from '../tableCells'; +import PercentageCell from '../tableCells/PercentageCell'; import TokenAmountCell from '../tableCells/TokenAmountCell'; import { ValidatorTableProps } from './types'; @@ -70,10 +71,7 @@ const getTableColumns = (isWaiting?: boolean) => [ columnHelper.accessor('commission', { header: () => , cell: (props) => ( - + ), sortingFn: sortBnValueForNomineeOrValidator, }), diff --git a/apps/tangle-dapp/components/tableCells/PercentageCell.tsx b/apps/tangle-dapp/components/tableCells/PercentageCell.tsx index b5c78c7c4..fb5dfab00 100644 --- a/apps/tangle-dapp/components/tableCells/PercentageCell.tsx +++ b/apps/tangle-dapp/components/tableCells/PercentageCell.tsx @@ -2,6 +2,7 @@ import { Typography } from '@webb-tools/webb-ui-components'; import { FC } from 'react'; import { EMPTY_VALUE_PLACEHOLDER } from '../../constants'; +import formatPercentage from '../../utils/formatPercentage'; export type PercentageCellProps = { percentage?: number; @@ -18,7 +19,7 @@ const PercentageCell: FC = ({ percentage }) => { fw="normal" className="text-mono-200 dark:text-mono-0" > - {`${percentage.toFixed(2)}%`} + {formatPercentage(percentage)} ); }; diff --git a/apps/tangle-dapp/components/tables/Operators/index.tsx b/apps/tangle-dapp/components/tables/Operators/index.tsx index f3ea298ef..c953058d8 100644 --- a/apps/tangle-dapp/components/tables/Operators/index.tsx +++ b/apps/tangle-dapp/components/tables/Operators/index.tsx @@ -21,6 +21,7 @@ import { twMerge } from 'tailwind-merge'; import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { PagePath, QueryParamKey } from '../../../types'; +import formatPercentage from '../../../utils/formatPercentage'; import getTVLToDisplay from '../../../utils/getTVLToDisplay'; import { getSortAddressOrIdentityFnc } from '../../../utils/table'; import { TableStatus } from '../../TableStatus'; @@ -93,7 +94,7 @@ const columns = [ > {typeof value !== 'number' ? EMPTY_VALUE_PLACEHOLDER - : `${value.toFixed(2)}%`} + : formatPercentage(value)} ); @@ -118,7 +119,7 @@ const columns = [ const tokensList = props.getValue(); return ( - + {tokensList.length > 0 ? ( ) : ( @@ -133,7 +134,7 @@ const columns = [ id: 'actions', header: () => null, cell: (props) => ( - +
+
+
+ ), + enableSorting: false, + }), +]; + +// TODO: Have the first row be expanded by default. +const LsMyProtocolsTable: FC = () => { + const substrateAddress = useSubstrateAddress(); + const [sorting, setSorting] = useState([]); + const { lsNetworkId } = useLsStore(); + + const lsNetwork = getLsNetwork(lsNetworkId); + + const getExpandedRowContent = useCallback( + (row: Row) => ( + + ), + [], + ); + + const lsPools = useLsPools(); + + const myPools: LsMyPoolRow[] = useMemo(() => { + if (substrateAddress === null || !(lsPools instanceof Map)) { + return []; + } + + const lsPoolsArray = Array.from(lsPools.values()); + + return lsPoolsArray + .filter((lsPool) => lsPool.members.has(substrateAddress)) + .map((lsPool) => { + const account = lsPool.members.get(substrateAddress); + + assert(account !== undefined); + + return { + ...lsPool, + myStake: account.balance.toBn(), + isRoot: lsPool.ownerAddress === substrateAddress, + isNominator: lsPool.nominatorAddress === substrateAddress, + isBouncer: lsPool.bouncerAddress === substrateAddress, + // TODO: Obtain which protocol this pool is associated with. For the parachain, there'd need to be some query to see what pools are associated with which parachain protocols. For Tangle networks, it's simply its own protocol. For now, using dummy data. + lsProtocolId: LsProtocolId.TANGLE_LOCAL, + } satisfies LsMyPoolRow; + }); + }, [lsPools, substrateAddress]); + + const myStake = useMemo(() => { + return myPools.reduce((acc, pool) => acc.add(pool.myStake), new BN(0)); + }, [myPools]); + + const rows = useMemo(() => { + return lsNetwork.protocols.map( + (lsProtocol) => + ({ + name: lsProtocol.name, + // TODO: Reduce the TVL of the pools associated with this protocol. + tvl: new BN(485348583485348), + iconName: lsProtocol.token, + myStake: myStake, + pools: myPools, + // TODO: Calculate the USD value of the TVL. + tvlInUsd: 123.3456, + token: lsProtocol.token, + decimals: lsProtocol.decimals, + }) satisfies LsMyProtocolRow, + ); + }, [lsNetwork.protocols, myPools, myStake]); + + const table = useReactTable({ + data: rows, + columns: PROTOCOL_COLUMNS, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + state: { + sorting, + }, + autoResetPageIndex: false, + enableSortingRemoval: false, + }); + + const onRowClick = useCallback( + (row: Row) => { + table.setExpanded({ [row.id]: !row.getIsExpanded() }); + }, + [table], + ); + + return ( +
+ ); +}; + +export default LsMyProtocolsTable; diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx index 64fdf5f28..11299a99d 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsProtocolsTable.tsx @@ -24,10 +24,6 @@ import { LsPool } from '../../constants/liquidStaking/types'; import pluralize from '../../utils/pluralize'; import useLsPools from '../../data/liquidStaking/useLsPools'; -export interface LsProtocolsTableProps { - initialSorting?: SortingState; -} - export type LsProtocolRow = { name: string; tvl: number; @@ -76,7 +72,7 @@ const PROTOCOL_COLUMNS = [ const length = props.getValue().length; return ( - + 1)} @@ -90,16 +86,16 @@ const PROTOCOL_COLUMNS = [ id: 'expand/collapse', header: () => null, cell: ({ row }) => ( - +
@@ -110,19 +106,12 @@ const PROTOCOL_COLUMNS = [ ]; // TODO: Have the first row be expanded by default. -const LsProtocolsTable: FC = ({ - initialSorting = [], -}) => { - const [sorting, setSorting] = useState(initialSorting); +const LsProtocolsTable: FC = () => { + const [sorting, setSorting] = useState([]); const getExpandedRowContent = useCallback( (row: Row) => ( -
- -
+ ), [], ); diff --git a/apps/tangle-dapp/containers/LsPoolsTableOld.tsx b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx index eefda2a01..ed072afff 100644 --- a/apps/tangle-dapp/containers/LsPoolsTableOld.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTableOld.tsx @@ -30,7 +30,7 @@ import { FC, useCallback, useMemo, useState } from 'react'; import { GlassCard, TableStatus } from '../components'; import TableRowsSkeleton from '../components/LiquidStaking/TableRowsSkeleton'; -import { StringCell } from '../components/tableCells'; +import PercentageCell from '../components/tableCells/PercentageCell'; import TokenAmountCell from '../components/tableCells/TokenAmountCell'; import { EMPTY_VALUE_PLACEHOLDER } from '../constants'; import { LsPool } from '../constants/liquidStaking/types'; @@ -113,12 +113,7 @@ const COLUMNS = [ return EMPTY_VALUE_PLACEHOLDER; } - return ( - - ); + return ; }, }), COLUMN_HELPER.accessor('apyPercentage', { @@ -130,12 +125,7 @@ const COLUMNS = [ return EMPTY_VALUE_PLACEHOLDER; } - return ( - - ); + return ; }, }), ]; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx index 9e286c428..85d8412fd 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx @@ -21,6 +21,7 @@ import assertSubstrateAddress from '../../../utils/assertSubstrateAddress'; import calculateCommission from '../../../utils/calculateCommission'; import { CrossChainTimeUnit } from '../../../utils/CrossChainTime'; import formatBn from '../../../utils/formatBn'; +import formatPercentage from '../../../utils/formatPercentage'; import { GetTableColumnsFn } from '../adapter'; import { sortCommission, @@ -184,7 +185,7 @@ const getTableColumns: GetTableColumnsFn = ( fw="normal" className="text-mono-200 dark:text-mono-0" > - {calculateCommission(props.getValue()).toFixed(2) + '%'} + {formatPercentage(calculateCommission(props.getValue()))} ), diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 573662056..5f17b3b16 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -69,6 +69,7 @@ const useLsPools = (): Map | null | Error => { const validators = poolNominations.get(poolId) ?? []; const apyEntry = compoundApys.get(poolId); + // TODO: Losing precision here by fixing it to two decimal places. Should be handling this instead on the UI side, not on this data fetching hook? const apyPercentage = apyEntry === undefined ? undefined : Number(apyEntry.toFixed(2)); diff --git a/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx b/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx index a27faf765..4e8a8f794 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx +++ b/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx @@ -27,6 +27,7 @@ import { } from '../../types/liquidStaking'; import calculateCommission from '../../utils/calculateCommission'; import formatBn from '../../utils/formatBn'; +import formatPercentage from '../../utils/formatPercentage'; const validatorColumnHelper = createColumnHelper(); const dappColumnHelper = createColumnHelper(); @@ -138,7 +139,7 @@ export const useLsValidatorSelectionTableColumns = ( fw="normal" className="text-mono-200 dark:text-mono-0" > - {calculateCommission(props.getValue()).toFixed(2) + '%'} + {formatPercentage(calculateCommission(props.getValue()))} ), @@ -355,7 +356,7 @@ export const useLsValidatorSelectionTableColumns = ( fw="normal" className="text-mono-200 dark:text-mono-0" > - {(Number(props.getValue().toString()) / 10000).toFixed(2)}% + {(Number(props.getValue().toString()) / 10_000).toFixed(2)}% ), diff --git a/apps/tangle-dapp/utils/formatPercentage.ts b/apps/tangle-dapp/utils/formatPercentage.ts new file mode 100644 index 000000000..4b21156c7 --- /dev/null +++ b/apps/tangle-dapp/utils/formatPercentage.ts @@ -0,0 +1,12 @@ +const formatPercentage = (percentage: number): `${string}%` => { + const percentageString = percentage.toFixed(2); + + // If the percentage is 0, we want to display '<0.01' instead of '0.00'. + // This helps avoid confusing the user to believe that the value is 0. + const finalPercentageString = + percentageString === '0.00' ? '<0.01' : percentageString; + + return `${finalPercentageString}%`; +}; + +export default formatPercentage; diff --git a/libs/icons/src/SubtractCircleLineIcon.tsx b/libs/icons/src/SubtractCircleLineIcon.tsx index 13ec69356..a16248bbc 100644 --- a/libs/icons/src/SubtractCircleLineIcon.tsx +++ b/libs/icons/src/SubtractCircleLineIcon.tsx @@ -7,7 +7,7 @@ export const SubtractCircleLineIcon = (props: IconBase) => { width: 16, height: 17, viewBox: '0 0 16 17', - d: 'M7.33398 7.83203V5.16536H8.66732V7.83203H11.334V9.16536H8.66732V11.832H7.33398V9.16536H4.66732V7.83203H7.33398ZM8.00065 15.1654C4.31875 15.1654 1.33398 12.1806 1.33398 8.4987C1.33398 4.8168 4.31875 1.83203 8.00065 1.83203C11.6825 1.83203 14.6673 4.8168 14.6673 8.4987C14.6673 12.1806 11.6825 15.1654 8.00065 15.1654ZM8.00065 13.832C10.9462 13.832 13.334 11.4442 13.334 8.4987C13.334 5.55318 10.9462 3.16536 8.00065 3.16536C5.05513 3.16536 2.66732 5.55318 2.66732 8.4987C2.66732 11.4442 5.05513 13.832 8.00065 13.832Z', + d: 'M8.00065 15.1654C4.31875 15.1654 1.33398 12.1806 1.33398 8.4987C1.33398 4.8168 4.31875 1.83203 8.00065 1.83203C11.6825 1.83203 14.6673 4.8168 14.6673 8.4987C14.6673 12.1806 11.6825 15.1654 8.00065 15.1654ZM8.00065 13.832C10.9462 13.832 13.334 11.4442 13.334 8.4987C13.334 5.55318 10.9462 3.16536 8.00065 3.16536C5.05513 3.16536 2.66732 5.55318 2.66732 8.4987C2.66732 11.4442 5.05513 13.832 8.00065 13.832ZM4.66732 7.83203H11.334V9.16536H4.66732V7.83203Z', displayName: 'SubtractCircleLineIcon', }); }; From 0d54a917f1762419c4fb04db9ccc93eb2208d8fe Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 4 Oct 2024 04:24:31 -0400 Subject: [PATCH 48/54] fix(tangle-dapp): Fix percentage display bug --- .../ExchangeRateDetailItem.tsx | 8 ++- .../stakeAndUnstake/FeeDetailItem.tsx | 2 +- .../containers/LsPoolsTable/LsPoolsTable.tsx | 1 - .../LsPoolsTable/LsProtocolsTable.tsx | 69 ++++++++++++------- apps/tangle-dapp/utils/formatPercentage.ts | 4 +- 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx index fb1083c54..33ad8d798 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx @@ -2,8 +2,9 @@ import { SkeletonLoader } from '@webb-tools/webb-ui-components'; import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; -import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants'; +import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants'; import { LsToken } from '../../../constants/liquidStaking/types'; +import useLsActivePoolDisplayName from '../../../data/liquidStaking/useLsActivePoolDisplayName'; import { ExchangeRateType } from '../../../data/liquidStaking/useLsExchangeRate'; import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate'; import DetailItem from './DetailItem'; @@ -17,6 +18,7 @@ const ExchangeRateDetailItem: FC = ({ type, token, }) => { + const lsActivePoolDisplayName = useLsActivePoolDisplayName(); const { exchangeRate, isRefreshing } = useLsExchangeRate(type); const exchangeRateElement = @@ -38,8 +40,8 @@ const ExchangeRateDetailItem: FC = ({ isRefreshing && 'animate-pulse', )} > - 1 {token} = {exchangeRateElement} {LS_DERIVATIVE_TOKEN_PREFIX} - {token} + 1 {token} = {exchangeRateElement}{' '} + {lsActivePoolDisplayName ?? EMPTY_VALUE_PLACEHOLDER} ); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx index ac83621e4..acec26ac6 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx @@ -52,7 +52,7 @@ const FeeDetailItem: FC = ({ const feeTitle = typeof feePercentage !== 'number' ? 'Fee' - : `Fee (${formatPercentage(feePercentage * 100)}%)`; + : `Fee (${formatPercentage(feePercentage * 100)})`; return ; }; diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx index eaacef9e5..2f393dd75 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx @@ -146,7 +146,6 @@ const LsPoolsTable: FC = ({ pools, isShown }) => { if (pools.length === 0) { return ( (); @@ -40,7 +45,7 @@ const PROTOCOL_COLUMNS = [ cell: (props) => (
- + {props.getValue()} @@ -56,15 +61,22 @@ const PROTOCOL_COLUMNS = [ }), COLUMN_HELPER.accessor('tvl', { header: () => 'Total Staked (TVL)', - cell: (props) => ( - - - - ), + cell: (props) => { + const formattedTvl = formatBn( + props.getValue(), + props.row.original.decimals, + ); + + return ( + + + + ); + }, }), COLUMN_HELPER.accessor('pools', { header: () => 'Pools', @@ -108,6 +120,9 @@ const PROTOCOL_COLUMNS = [ // TODO: Have the first row be expanded by default. const LsProtocolsTable: FC = () => { const [sorting, setSorting] = useState([]); + const { lsNetworkId } = useLsStore(); + + const lsNetwork = getLsNetwork(lsNetworkId); const getExpandedRowContent = useCallback( (row: Row) => ( @@ -127,18 +142,24 @@ const LsProtocolsTable: FC = () => { }, [lsPools]); // TODO: Dummy data. Need to load actual protocol data or list it if it's hardcoded/limited to a few. - const protocols: LsProtocolRow[] = [ - { - iconName: 'tangle', - name: 'Tangle', - pools: pools, - tvl: 123.4567, - tvlInUsd: 123.3456, - }, - ]; + const rows = useMemo(() => { + return lsNetwork.protocols.map( + (lsProtocol) => + ({ + name: lsProtocol.name, + // TODO: Reduce the TVL of the pools associated with this protocol. + tvl: new BN(485348583485348), + token: lsProtocol.token, + pools: pools, + // TODO: Calculate the USD value of the TVL. + tvlInUsd: 123.3456, + decimals: lsProtocol.decimals, + }) satisfies LsProtocolRow, + ); + }, [lsNetwork.protocols, pools]); const table = useReactTable({ - data: protocols, + data: rows, columns: PROTOCOL_COLUMNS, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), diff --git a/apps/tangle-dapp/utils/formatPercentage.ts b/apps/tangle-dapp/utils/formatPercentage.ts index 4b21156c7..2f99ec47f 100644 --- a/apps/tangle-dapp/utils/formatPercentage.ts +++ b/apps/tangle-dapp/utils/formatPercentage.ts @@ -4,7 +4,9 @@ const formatPercentage = (percentage: number): `${string}%` => { // If the percentage is 0, we want to display '<0.01' instead of '0.00'. // This helps avoid confusing the user to believe that the value is 0. const finalPercentageString = - percentageString === '0.00' ? '<0.01' : percentageString; + percentageString === '0.00' && percentage !== 0 + ? '<0.01' + : percentageString; return `${finalPercentageString}%`; }; From 3b8b758e4900ceb7361ea169a94fb13fbce3d697 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 4 Oct 2024 06:33:49 -0400 Subject: [PATCH 49/54] ui(tangle-dapp): UI fixes and improvements --- .../tangle-dapp/app/liquid-staking/layout.tsx | 18 ++ apps/tangle-dapp/app/liquid-staking/page.tsx | 10 +- apps/tangle-dapp/app/nomination/page.tsx | 71 ++++- apps/tangle-dapp/app/restake/TabListItem.tsx | 4 +- apps/tangle-dapp/app/restake/page.tsx | 154 ++++++---- .../components/Breadcrumbs/utils.tsx | 1 + .../LiquidStaking/LsMyPoolsTable.tsx | 17 +- .../ExchangeRateDetailItem.tsx | 2 +- .../stakeAndUnstake/LsAgnosticBalance.tsx | 6 +- .../LiquidStaking/stakeAndUnstake/LsInput.tsx | 2 +- .../stakeAndUnstake/LsOverviewItem.tsx | 85 ------ .../stakeAndUnstake/LsStakeCard.tsx | 7 +- .../stakeAndUnstake/SelectedPoolIndicator.tsx | 12 +- .../stakeAndUnstake/TokenChip.tsx | 2 +- apps/tangle-dapp/components/LsTokenIcon.tsx | 64 +++-- .../OnboardingModal/OnboardingHelpButton.tsx | 40 +++ .../OnboardingModal/OnboardingItem.tsx | 2 +- .../OnboardingModal/OnboardingModal.tsx | 34 ++- apps/tangle-dapp/constants/index.ts | 8 + .../constants/liquidStaking/types.ts | 2 +- apps/tangle-dapp/containers/Layout/Layout.tsx | 4 + .../containers/LsMyProtocolsTable.tsx | 6 +- .../containers/LsPoolsTable/LsPoolsTable.tsx | 14 +- .../LsPoolsTable/LsProtocolsTable.tsx | 6 +- .../containers/LsPoolsTableOld.tsx | 267 ------------------ .../context/useOnboardingStore.tsx | 12 + .../apy/useLsPoolCompoundApys.ts | 8 +- .../useLsActivePoolDisplayName.ts | 25 +- .../data/liquidStaking/useLsPools.ts | 25 +- .../data/liquidStaking/useLsPoolsMetadata.ts | 46 --- apps/tangle-dapp/hooks/useLocalStorage.ts | 2 +- apps/tangle-dapp/tailwind.config.js | 4 + libs/icons/src/ArrowDownIcon.tsx | 11 + libs/icons/src/index.ts | 1 + .../webb-ui-components/src/constants/index.ts | 2 + 35 files changed, 389 insertions(+), 585 deletions(-) create mode 100644 apps/tangle-dapp/app/liquid-staking/layout.tsx delete mode 100644 apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsOverviewItem.tsx create mode 100644 apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx delete mode 100644 apps/tangle-dapp/containers/LsPoolsTableOld.tsx create mode 100644 apps/tangle-dapp/context/useOnboardingStore.tsx delete mode 100644 apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts create mode 100644 libs/icons/src/ArrowDownIcon.tsx diff --git a/apps/tangle-dapp/app/liquid-staking/layout.tsx b/apps/tangle-dapp/app/liquid-staking/layout.tsx new file mode 100644 index 000000000..e1476e567 --- /dev/null +++ b/apps/tangle-dapp/app/liquid-staking/layout.tsx @@ -0,0 +1,18 @@ +import { Metadata } from 'next'; +import { FC, PropsWithChildren } from 'react'; + +import createPageMetadata from '../../utils/createPageMetadata'; + +export const dynamic = 'force-static'; + +export const metadata: Metadata = createPageMetadata({ + title: 'Liquid Staking', + description: + "Liquid stake onto liquid staking pools to obtain derivative tokens and remain liquid while earning staking rewards and participating in Tangle's restaking infrastructure.", +}); + +const Layout: FC = ({ children }: PropsWithChildren) => { + return children; +}; + +export default Layout; diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index cde46dc81..d48aea5a2 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -20,10 +20,9 @@ import { FC, useEffect } from 'react'; import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeCard'; import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; import OnboardingItem from '../../components/OnboardingModal/OnboardingItem'; -import OnboardingModal, { - OnboardingPageKey, -} from '../../components/OnboardingModal/OnboardingModal'; +import OnboardingModal from '../../components/OnboardingModal/OnboardingModal'; import StatItem from '../../components/StatItem'; +import { OnboardingPageKey } from '../../constants'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; import LsMyProtocolsTable from '../../containers/LsMyProtocolsTable'; import { LsProtocolsTable } from '../../containers/LsPoolsTable'; @@ -89,8 +88,9 @@ const LiquidStakingPage: FC = () => { return (
{
-
+
-
- - Overview - + <> + + - -
+ + + - + + - +
+
+ + Overview + - + +
- -
+ + + + + + + +
+ ); } diff --git a/apps/tangle-dapp/app/restake/TabListItem.tsx b/apps/tangle-dapp/app/restake/TabListItem.tsx index b188ade79..6831eb51e 100644 --- a/apps/tangle-dapp/app/restake/TabListItem.tsx +++ b/apps/tangle-dapp/app/restake/TabListItem.tsx @@ -23,7 +23,7 @@ export default function TabListItem({ {isActive && ( )} @@ -33,7 +33,7 @@ export default function TabListItem({ 'absolute body2 w-full p-2 text-center', isActive && 'font-bold', isActive - ? 'text-mono-200 dark:text-mono-0' + ? 'text-mono-200 dark:text-blue-50' : 'text-mono-120 dark:text-mono-80', )} > diff --git a/apps/tangle-dapp/app/restake/page.tsx b/apps/tangle-dapp/app/restake/page.tsx index a3e3cce1d..cb8a69054 100644 --- a/apps/tangle-dapp/app/restake/page.tsx +++ b/apps/tangle-dapp/app/restake/page.tsx @@ -1,11 +1,16 @@ 'use client'; +import { AddCircleLineIcon, Search, SparklingIcon } from '@webb-tools/icons'; +import { TANGLE_DOCS_RESTAKING_URL } from '@webb-tools/webb-ui-components'; import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; import { twMerge } from 'tailwind-merge'; import GlassCard from '../../components/GlassCard/GlassCard'; +import OnboardingItem from '../../components/OnboardingModal/OnboardingItem'; +import OnboardingModal from '../../components/OnboardingModal/OnboardingModal'; import StatItem from '../../components/StatItem'; +import { OnboardingPageKey } from '../../constants'; import useRestakeDelegatorInfo from '../../data/restake/useRestakeDelegatorInfo'; import useRestakeOperatorMap from '../../data/restake/useRestakeOperatorMap'; import useRestakeTVL from '../../data/restake/useRestakeTVL'; @@ -41,71 +46,106 @@ export default function RestakePage() { } = useRestakeTVL(operatorMap, delegatorInfo); return ( -
-
- - + + + + + + + + + + +
+
+ - {CONTENT.OVERVIEW} - - -
- - - -
-
- - -
- How it works + {CONTENT.OVERVIEW} - {CONTENT.HOW_IT_WORKS} -
+
+ + + +
+
- {/** TODO: Determine read more link here */} - - -
+
+ + How it works + + + {CONTENT.HOW_IT_WORKS} +
+ + {/** TODO: Determine read more link here */} + + +
- -
+ +
+ ); } diff --git a/apps/tangle-dapp/components/Breadcrumbs/utils.tsx b/apps/tangle-dapp/components/Breadcrumbs/utils.tsx index c104c9f7c..fd34cc236 100644 --- a/apps/tangle-dapp/components/Breadcrumbs/utils.tsx +++ b/apps/tangle-dapp/components/Breadcrumbs/utils.tsx @@ -34,6 +34,7 @@ const BREADCRUMB_ICONS: Record JSX.Element> = { const BREADCRUMB_LABELS: Partial> = { [PagePath.SERVICES]: 'Service Overview', [PagePath.CLAIM_AIRDROP]: 'Claim Airdrop', + [PagePath.LIQUID_STAKING]: 'Liquid Staking', }; const isSubstrateAddress = (address: string): boolean => { diff --git a/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx index f5bc2ae18..349dd9c96 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx @@ -10,7 +10,11 @@ import { createColumnHelper, } from '@tanstack/react-table'; import { Table } from '../../../../libs/webb-ui-components/src/components/Table'; -import { LsPool, LsProtocolId } from '../../constants/liquidStaking/types'; +import { + LsPool, + LsPoolDisplayName, + LsProtocolId, +} from '../../constants/liquidStaking/types'; import { ActionsDropdown, Avatar, @@ -31,6 +35,7 @@ import BlueIconButton from '../BlueIconButton'; import useIsAccountConnected from '../../hooks/useIsAccountConnected'; import { twMerge } from 'tailwind-merge'; import pluralize from '../../utils/pluralize'; +import { sharedTableStatusClxs } from '../tables/shared'; export type LsMyPoolRow = LsPool & { myStake: BN; @@ -92,7 +97,9 @@ const LsMyPoolsTable: FC = ({ pools, isShown }) => { fw="normal" className="text-mono-200 dark:text-mono-0" > - {props.row.original.metadata}#{props.getValue()} + {( + `${props.row.original.name}#${props.getValue()}` satisfies LsPoolDisplayName + ).toUpperCase()} ), }), @@ -241,14 +248,10 @@ const LsMyPoolsTable: FC = ({ pools, isShown }) => { if (!isAccountConnected) { return ( ); } else if (pools.length === 0) { diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx index 33ad8d798..006d40894 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx @@ -41,7 +41,7 @@ const ExchangeRateDetailItem: FC = ({ )} > 1 {token} = {exchangeRateElement}{' '} - {lsActivePoolDisplayName ?? EMPTY_VALUE_PLACEHOLDER} + {lsActivePoolDisplayName?.toUpperCase() ?? EMPTY_VALUE_PLACEHOLDER}
); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx index 577061020..4c610f563 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsAgnosticBalance.tsx @@ -51,9 +51,11 @@ const LsAgnosticBalance: FC = ({ includeCommas: true, }); - const unit = isNative ? protocol.token : lsActivePoolDisplayName; + const unit = isNative + ? protocol.token + : (lsActivePoolDisplayName?.toUpperCase() ?? EMPTY_VALUE_PLACEHOLDER); - return `${formattedBalance} ${unit}`; + return `${formattedBalance} ${unit}`.trim(); }, [ balance, protocol.decimals, diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx index d024b751a..3369b8fc8 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsInput.tsx @@ -107,7 +107,7 @@ const LsInput = forwardRef( <>
diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsOverviewItem.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsOverviewItem.tsx deleted file mode 100644 index c308c30e7..000000000 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsOverviewItem.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client'; - -import { BN } from '@polkadot/util'; -import { ArrowRight, ChainIcon } from '@webb-tools/icons'; -import { Button, Chip, Typography } from '@webb-tools/webb-ui-components'; -import { FC, useMemo } from 'react'; - -import StatItem from '../../../components/StatItem'; -import { - LS_DERIVATIVE_TOKEN_PREFIX, - TVS_TOOLTIP, -} from '../../../constants/liquidStaking/constants'; -import { LsToken } from '../../../constants/liquidStaking/types'; -import { PagePath } from '../../../types'; -import formatTangleBalance from '../../../utils/formatTangleBalance'; -import LsTokenIcon from '../../LsTokenIcon'; - -export type LsOverviewItemProps = { - title: string; - tokenSymbol: LsToken; - totalValueStaked: number; - totalStaked: string; -}; - -const LsOverviewItem: FC = ({ - title, - tokenSymbol, - totalValueStaked, - totalStaked, -}) => { - const formattedTotalValueStaked = totalValueStaked.toLocaleString('en-US', { - style: 'currency', - currency: 'USD', - }); - - const formattedTotalStaked = useMemo( - () => formatTangleBalance(new BN(totalStaked)), - [totalStaked], - ); - - return ( -
-
-
- - -
- -
-
- - - {title} - - - - {LS_DERIVATIVE_TOKEN_PREFIX} - {tokenSymbol.toUpperCase()} - -
- -
- - - - - -
-
- ); -}; - -export default LsOverviewItem; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index 7a3c9efae..24fa6bfac 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -5,8 +5,7 @@ import '@webb-tools/tangle-restaking-types'; import { BN } from '@polkadot/util'; -import { ArrowDownIcon } from '@radix-ui/react-icons'; -import { Search } from '@webb-tools/icons'; +import { ArrowDownIcon, Search } from '@webb-tools/icons'; import { Button, Chip, @@ -198,7 +197,7 @@ const LsStakeCard: FC = () => { }, []); return ( - <> +
{ > {actionText} - +
); }; diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx index e89045a0e..86d060a6e 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/SelectedPoolIndicator.tsx @@ -1,29 +1,29 @@ import { Typography } from '@webb-tools/webb-ui-components'; import { FC } from 'react'; -import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import useLsActivePoolDisplayName from '../../../data/liquidStaking/useLsActivePoolDisplayName'; +import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import LsTokenIcon from '../../LsTokenIcon'; const SelectedPoolIndicator: FC = () => { const { lsProtocolId } = useLsStore(); const selectedProtocol = getLsProtocolDef(lsProtocolId); - const selectedPoolDisplayName = useLsActivePoolDisplayName(); + const activeLsPoolDisplayName = useLsActivePoolDisplayName(); return (
- {selectedPoolDisplayName === null + {activeLsPoolDisplayName === null ? 'Select a pool' - : selectedPoolDisplayName} + : activeLsPoolDisplayName.toUpperCase()}
); diff --git a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx index ca81f233f..3a9237866 100644 --- a/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/stakeAndUnstake/TokenChip.tsx @@ -27,7 +27,7 @@ const TokenChip: FC = ({ )} > {token && ( - + )} diff --git a/apps/tangle-dapp/components/LsTokenIcon.tsx b/apps/tangle-dapp/components/LsTokenIcon.tsx index 556ff2819..e7e1e0d93 100644 --- a/apps/tangle-dapp/components/LsTokenIcon.tsx +++ b/apps/tangle-dapp/components/LsTokenIcon.tsx @@ -7,13 +7,13 @@ type LsTokenIconSize = 'md' | 'lg'; interface LsTokenIconProps { name?: string; size?: LsTokenIconSize; - hasTangleBorder?: boolean; + hasRainbowBorder?: boolean; } const LsTokenIcon: FC = ({ name, size = 'md', - hasTangleBorder = true, + hasRainbowBorder = false, }) => { const { wrapperSizeClassName, iconSizeClassName, borderSize } = getSizeValues(size); @@ -45,36 +45,38 @@ const LsTokenIcon: FC = ({ /> )} - - + {hasRainbowBorder && ( + + - - - - - - - + + + + + + + + )}
); }; diff --git a/apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx b/apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx new file mode 100644 index 000000000..7b603b126 --- /dev/null +++ b/apps/tangle-dapp/components/OnboardingModal/OnboardingHelpButton.tsx @@ -0,0 +1,40 @@ +import { IconButton, Typography } from '@webb-tools/webb-ui-components'; +import { usePathname } from 'next/navigation'; +import { FC } from 'react'; + +import useOnboardingStore from '../../context/useOnboardingStore'; +import { PagePath } from '../../types'; + +const PAGES_WITH_ONBOARDING: PagePath[] = [ + PagePath.LIQUID_STAKING, + PagePath.RESTAKE, + PagePath.NOMINATION, +]; + +const OnboardingHelpButton: FC = () => { + const { setOnboardingReopenFlag } = useOnboardingStore(); + const pathName = usePathname(); + + const isOnOnboardingPage = PAGES_WITH_ONBOARDING.some((pagePath) => + pathName.startsWith(pagePath), + ); + + // Don't render if on a page that doesn't have onboarding. + if (!isOnOnboardingPage) { + return null; + } + + return ( + setOnboardingReopenFlag(true)} + tooltip="Learn how to use this page" + className="rounded-full border-2 py-2 px-4 bg-mono-0/10 border-mono-60 dark:bg-mono-0/5 dark:border-mono-140 dark:hover:bg-mono-0/10" + > + + ? + + + ); +}; + +export default OnboardingHelpButton; diff --git a/apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx b/apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx index 641809e0e..e6e5bd98d 100644 --- a/apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx +++ b/apps/tangle-dapp/components/OnboardingModal/OnboardingItem.tsx @@ -1,6 +1,6 @@ import { IconBase } from '@webb-tools/icons/types'; import { Typography } from '@webb-tools/webb-ui-components'; -import { FC } from 'react'; +import { FC, JSX } from 'react'; export type OnboardingItemProps = { Icon: (props: IconBase) => JSX.Element; diff --git a/apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx b/apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx index 5cdabb26f..1a88f1cd2 100644 --- a/apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx +++ b/apps/tangle-dapp/components/OnboardingModal/OnboardingModal.tsx @@ -1,3 +1,5 @@ +'use client'; + import { Button, Modal, @@ -8,30 +10,29 @@ import { } from '@webb-tools/webb-ui-components'; import { FC, ReactElement, useEffect, useRef, useState } from 'react'; +import { OnboardingPageKey } from '../../constants'; +import useOnboardingStore from '../../context/useOnboardingStore'; import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage'; import { OnboardingItemProps } from './OnboardingItem'; -export enum OnboardingPageKey { - BRIDGE, - LIQUID_STAKING, - RESTAKE, - BLUEPRINTS, - NOMINATE, -} - export type OnboardingModalProps = { + title?: string; pageKey: OnboardingPageKey; - linkHref?: string; + learnMoreHref?: string; children: | ReactElement | ReactElement[]; }; const OnboardingModal: FC = ({ + title = 'Quick Start', pageKey, children, - linkHref = TANGLE_DOCS_URL, + learnMoreHref = TANGLE_DOCS_URL, }) => { + const { onboardingReopenFlag, setOnboardingReopenFlag } = + useOnboardingStore(); + const [isOpen, setIsOpen] = useState(false); const seenRef = useRef(false); @@ -39,6 +40,15 @@ const OnboardingModal: FC = ({ LocalStorageKey.ONBOARDING_MODALS_SEEN, ); + // Re-open the modal if the user has requested so by clicking the + // help button, which raises a global flag. + useEffect(() => { + if (onboardingReopenFlag) { + setOnboardingReopenFlag(false); + setIsOpen(true); + } + }, [onboardingReopenFlag, setOnboardingReopenFlag]); + // On load, check if the user has seen this modal before. // If not, then trigger the modal to be shown, and remember // that it has been seen. @@ -70,13 +80,13 @@ const OnboardingModal: FC = ({ return ( - setIsOpen(false)}>Quick Start + setIsOpen(false)}>{title}
{children}
- - - - Select Pool - - - {poolsMap === null ? ( - - ) : poolsMap instanceof Error ? ( - - ) : rows.length === 0 ? ( - - ) : ( - <> - setSearchQuery(newValue)} - isControlled - rightIcon={} - /> - -
1)} - isPaginated - totalRecords={rows.length} - thClassName="!bg-inherit border-t-0 bg-mono-0 !px-3 !py-2 whitespace-nowrap" - trClassName="!bg-inherit" - tdClassName="!bg-inherit !px-3 !py-2 whitespace-nowrap" - /> - - )} - - - ); -}; - -export default LsPoolsTableOld; diff --git a/apps/tangle-dapp/context/useOnboardingStore.tsx b/apps/tangle-dapp/context/useOnboardingStore.tsx new file mode 100644 index 000000000..045d90fa6 --- /dev/null +++ b/apps/tangle-dapp/context/useOnboardingStore.tsx @@ -0,0 +1,12 @@ +import { create } from 'zustand'; + +const useOnboardingStore = create<{ + onboardingReopenFlag: boolean; + setOnboardingReopenFlag: (flag: boolean) => void; +}>((set) => ({ + onboardingReopenFlag: false, + setOnboardingReopenFlag: (onboardingReopenFlag) => + set({ onboardingReopenFlag }), +})); + +export default useOnboardingStore; diff --git a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts index 5a965a876..69f860e50 100644 --- a/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts +++ b/apps/tangle-dapp/data/liquidStaking/apy/useLsPoolCompoundApys.ts @@ -1,4 +1,3 @@ -import assert from 'assert'; import Decimal from 'decimal.js'; import { useCallback, useMemo } from 'react'; @@ -56,10 +55,9 @@ const useLsPoolCompoundApys = (): Map | null => { // use a counter, since some eras are skipped due to missing data. let actualErasConsidered = 0; - assert( - poolBondedAccountAddress !== undefined, - 'Each pool id should always have a corresponding bonded account entry', - ); + if (poolBondedAccountAddress === undefined) { + continue; + } // Calculate the avg. per-era return rate for the last max eras (history depth) // for the current pool. diff --git a/apps/tangle-dapp/data/liquidStaking/useLsActivePoolDisplayName.ts b/apps/tangle-dapp/data/liquidStaking/useLsActivePoolDisplayName.ts index 0a5a3c558..035057809 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsActivePoolDisplayName.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsActivePoolDisplayName.ts @@ -1,18 +1,29 @@ +import { u8aToString } from '@polkadot/util'; +import { useMemo } from 'react'; + import { LsPoolDisplayName } from '../../constants/liquidStaking/types'; -import useLsPoolsMetadata from './useLsPoolsMetadata'; +import useLsBondedPools from './useLsBondedPools'; import { useLsStore } from './useLsStore'; const useLsActivePoolDisplayName = (): LsPoolDisplayName | null => { const { lsPoolId } = useLsStore(); - const lsPoolsMetadata = useLsPoolsMetadata(); + const bondedPools = useLsBondedPools(); + + const name = useMemo(() => { + if (bondedPools === null || lsPoolId === null) { + return null; + } + + const activePool = bondedPools.find(([id]) => id === lsPoolId); - if (lsPoolId === null) { - return null; - } + if (activePool === undefined) { + return null; + } - const name = lsPoolsMetadata?.get(lsPoolId) ?? ''; + return `${u8aToString(activePool[1].metadata.name)}#${lsPoolId}` satisfies LsPoolDisplayName; + }, [bondedPools, lsPoolId]); - return `${name}#${lsPoolId}`; + return name; }; export default useLsActivePoolDisplayName; diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts index 5f17b3b16..94add5cd7 100644 --- a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -1,4 +1,4 @@ -import { BN_ZERO } from '@polkadot/util'; +import { BN_ZERO, u8aToString } from '@polkadot/util'; import { useMemo } from 'react'; import { LsPool } from '../../constants/liquidStaking/types'; @@ -10,35 +10,28 @@ import useLsPoolCompoundApys from './apy/useLsPoolCompoundApys'; import useLsBondedPools from './useLsBondedPools'; import useLsPoolMembers from './useLsPoolMembers'; import useLsPoolNominations from './useLsPoolNominations'; -import useLsPoolsMetadata from './useLsPoolsMetadata'; const useLsPools = (): Map | null | Error => { const networkFeatures = useNetworkFeatures(); const poolNominations = useLsPoolNominations(); - const isSupported = networkFeatures.includes(NetworkFeature.LsPools); - - const metadatas = useLsPoolsMetadata(); const bondedPools = useLsBondedPools(); const poolMembers = useLsPoolMembers(); const compoundApys = useLsPoolCompoundApys(); + const isSupported = networkFeatures.includes(NetworkFeature.LsPools); + const poolsMap = useMemo(() => { if ( bondedPools === null || poolNominations === null || compoundApys === null || poolMembers === null || - metadatas === null || !isSupported ) { return null; } const keyValuePairs = bondedPools.map(([poolId, tanglePool]) => { - const metadata = metadatas.get(poolId); - - // TODO: `tanglePool.metadata.name` should be available based on the latest changes to Tangle. Need to regenerate the types. Might want to prefer that method vs. the metadata query. - // Roles can be `None` if updated and removed. const ownerAddress = tanglePool.roles.root.isNone ? undefined @@ -78,10 +71,11 @@ const useLsPools = (): Map | null | Error => { ); const membersMap = new Map(membersKeyValuePairs); + const name = u8aToString(tanglePool.metadata.name); const pool: LsPool = { id: poolId, - metadata, + name, ownerAddress, nominatorAddress, bouncerAddress, @@ -96,14 +90,7 @@ const useLsPools = (): Map | null | Error => { }); return new Map(keyValuePairs); - }, [ - bondedPools, - poolNominations, - compoundApys, - poolMembers, - metadatas, - isSupported, - ]); + }, [bondedPools, poolNominations, compoundApys, poolMembers, isSupported]); // In case that the user connects to testnet or mainnet, but the network // doesn't have the liquid staking pools feature. diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts b/apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts deleted file mode 100644 index 9bffce528..000000000 --- a/apps/tangle-dapp/data/liquidStaking/useLsPoolsMetadata.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { u8aToString } from '@polkadot/util'; -import { useCallback, useMemo } from 'react'; - -import useApiRx from '../../hooks/useApiRx'; -import useNetworkFeatures from '../../hooks/useNetworkFeatures'; -import { NetworkFeature } from '../../types'; - -const useLsPoolsMetadata = () => { - const networkFeatures = useNetworkFeatures(); - const isSupported = networkFeatures.includes(NetworkFeature.LsPools); - - const { result: rawMetadataEntries } = useApiRx( - useCallback( - (api) => { - if (!isSupported) { - return null; - } - - return api.query.lst.metadata.entries(); - }, - [isSupported], - ), - ); - - const keyValuePairs = useMemo(() => { - if (rawMetadataEntries === null) { - return null; - } - - return rawMetadataEntries.map(([key, value]) => { - return [key.args[0].toNumber(), u8aToString(value)] as const; - }); - }, [rawMetadataEntries]); - - const map = useMemo(() => { - if (keyValuePairs === null) { - return null; - } - - return new Map(keyValuePairs); - }, [keyValuePairs]); - - return map; -}; - -export default useLsPoolsMetadata; diff --git a/apps/tangle-dapp/hooks/useLocalStorage.ts b/apps/tangle-dapp/hooks/useLocalStorage.ts index 43b404532..23e888460 100644 --- a/apps/tangle-dapp/hooks/useLocalStorage.ts +++ b/apps/tangle-dapp/hooks/useLocalStorage.ts @@ -3,7 +3,7 @@ import { HexString } from '@polkadot/util/types'; import { useCallback, useEffect, useState } from 'react'; -import { OnboardingPageKey } from '../components/OnboardingModal'; +import { OnboardingPageKey } from '../constants'; import { Payout, TangleTokenSymbol } from '../types'; import { BridgeQueueTxItem } from '../types/bridge'; import { diff --git a/apps/tangle-dapp/tailwind.config.js b/apps/tangle-dapp/tailwind.config.js index f9833f435..38ba5a08b 100644 --- a/apps/tangle-dapp/tailwind.config.js +++ b/apps/tangle-dapp/tailwind.config.js @@ -43,6 +43,10 @@ module.exports = { 'linear-gradient(180deg, rgba(255, 255, 255, 0.5) -428.82%, rgba(255, 255, 255, 0) 180.01%)', liquid_staking_tokens_table_dark: 'linear-gradient(180deg, rgba(43, 47, 64, 0.5) -428.82%, rgba(43, 47, 64, 0) 180.01%)', + liquid_staking_input: + 'linear-gradient(360deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.6) 100%)', + liquid_staking_input_dark: + 'linear-gradient(180deg, rgba(43, 47, 64, 0.4) 0%, rgba(112, 122, 166, 0.04) 100%)', }, }, }, diff --git a/libs/icons/src/ArrowDownIcon.tsx b/libs/icons/src/ArrowDownIcon.tsx new file mode 100644 index 000000000..89ce1e750 --- /dev/null +++ b/libs/icons/src/ArrowDownIcon.tsx @@ -0,0 +1,11 @@ +import { createIcon } from './create-icon'; +import { IconBase } from './types'; + +export const ArrowDownIcon = (props: IconBase) => { + return createIcon({ + ...props, + viewBox: '0 0 24 24', + d: 'M12.9993 16.172L18.3633 10.808L19.7773 12.222L11.9993 20L4.22134 12.222L5.63534 10.808L10.9993 16.172L10.9993 4L12.9993 4L12.9993 16.172Z', + displayName: 'ArrowDownIcon', + }); +}; diff --git a/libs/icons/src/index.ts b/libs/icons/src/index.ts index 0f15d47ee..9c10df054 100644 --- a/libs/icons/src/index.ts +++ b/libs/icons/src/index.ts @@ -150,6 +150,7 @@ export * from './WaterDropletIcon'; export { default as WebbLogoIcon } from './WebbLogoIcon'; export * from './YouTubeFill'; export * from './SubtractCircleLineIcon'; +export * from './ArrowDownIcon'; // Wallet icons export * from './wallets'; diff --git a/libs/webb-ui-components/src/constants/index.ts b/libs/webb-ui-components/src/constants/index.ts index 18b4b8ad3..c45406383 100644 --- a/libs/webb-ui-components/src/constants/index.ts +++ b/libs/webb-ui-components/src/constants/index.ts @@ -44,6 +44,8 @@ export const TANGLE_DOCS_STAKING_URL = 'https://docs.tangle.tools/restake/staking-intro'; export const TANGLE_DOCS_LIQUID_STAKING_URL = 'https://docs.tangle.tools/restake/lst-concepts'; +export const TANGLE_DOCS_RESTAKING_URL = + 'https://docs.tangle.tools/restake/restake-introduction'; export const TANGLE_GITHUB_URL = 'https://github.com/webb-tools/tangle'; export const WEBB_DOCS_URL = 'https://docs.webb.tools/'; From e0f6671aa76a518a6fed18133fbeaf8c892fce91 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Fri, 4 Oct 2024 06:45:13 -0400 Subject: [PATCH 50/54] feat(tangle-dapp): Create `setLsStakingIntent` hook --- .../LiquidStaking/LsMyPoolsTable.tsx | 28 ++++--------------- .../containers/LsPoolsTable/LsPoolsTable.tsx | 6 ++-- .../liquidStaking/useLsSetStakingIntent.ts | 20 +++++++++++++ 3 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 apps/tangle-dapp/data/liquidStaking/useLsSetStakingIntent.ts diff --git a/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx index 349dd9c96..d7e14b4fa 100644 --- a/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/LsMyPoolsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useMemo, FC, useCallback } from 'react'; +import { useState, useMemo, FC } from 'react'; import { useReactTable, getCoreRowModel, @@ -36,6 +36,7 @@ import useIsAccountConnected from '../../hooks/useIsAccountConnected'; import { twMerge } from 'tailwind-merge'; import pluralize from '../../utils/pluralize'; import { sharedTableStatusClxs } from '../tables/shared'; +import useLsSetStakingIntent from '../../data/liquidStaking/useLsSetStakingIntent'; export type LsMyPoolRow = LsPool & { myStake: BN; @@ -55,7 +56,8 @@ export type LsMyPoolsTableProps = { const LsMyPoolsTable: FC = ({ pools, isShown }) => { const isAccountConnected = useIsAccountConnected(); const [sorting, setSorting] = useState([]); - const { setIsStaking, setLsPoolId, lsPoolId, isStaking } = useLsStore(); + const { lsPoolId, isStaking } = useLsStore(); + const setLsStakingIntent = useLsSetStakingIntent(); const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, @@ -70,24 +72,6 @@ const LsMyPoolsTable: FC = ({ pools, isShown }) => { [pageIndex, pageSize], ); - // TODO: Need to also switch network/protocol to the selected pool's network/protocol. - const handleUnstakeClick = useCallback( - (poolId: number) => { - setIsStaking(false); - setLsPoolId(poolId); - }, - [setIsStaking, setLsPoolId], - ); - - // TODO: Need to also switch network/protocol to the selected pool's network/protocol. - const handleIncreaseStakeClick = useCallback( - (poolId: number) => { - setIsStaking(true); - setLsPoolId(poolId); - }, - [setIsStaking, setLsPoolId], - ); - const columns = [ COLUMN_HELPER.accessor('id', { header: () => 'ID', @@ -211,14 +195,14 @@ const LsMyPoolsTable: FC = ({ pools, isShown }) => { handleUnstakeClick(props.row.original.id)} + onClick={() => setLsStakingIntent(props.row.original.id, false)} tooltip="Unstake" Icon={SubtractCircleLineIcon} /> handleIncreaseStakeClick(props.row.original.id)} + onClick={() => setLsStakingIntent(props.row.original.id, true)} tooltip="Increase Stake" Icon={AddCircleLineIcon} /> diff --git a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx index 9ecc50e1c..9dcb283b9 100644 --- a/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx +++ b/apps/tangle-dapp/containers/LsPoolsTable/LsPoolsTable.tsx @@ -27,6 +27,7 @@ import { useLsStore } from '../../data/liquidStaking/useLsStore'; import PercentageCell from '../../components/tableCells/PercentageCell'; import { TableStatus } from '../../components'; import { sharedTableStatusClxs } from '../../components/tables/shared'; +import useLsSetStakingIntent from '../../data/liquidStaking/useLsSetStakingIntent'; export type LsPoolsTableProps = { pools: LsPool[]; @@ -43,7 +44,8 @@ const LsPoolsTable: FC = ({ pools, isShown }) => { pageSize: 5, }); - const { lsPoolId, setLsPoolId } = useLsStore(); + const { lsPoolId } = useLsStore(); + const setLsStakingIntent = useLsSetStakingIntent(); const pagination = useMemo( () => ({ @@ -115,7 +117,7 @@ const LsPoolsTable: FC = ({ pools, isShown }) => {