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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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 (