Skip to content

Commit

Permalink
feat(tangle-dapp): Implement Liquifier unlock NFTs unstake requests…
Browse files Browse the repository at this point in the history
… table (#2521)
  • Loading branch information
yurixander authored Aug 30, 2024
1 parent 6a0f28e commit a881ede
Show file tree
Hide file tree
Showing 88 changed files with 3,824 additions and 1,710 deletions.
21 changes: 5 additions & 16 deletions apps/tangle-dapp/app/liquid-staking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@

import { FC } from 'react';

import { LiquidStakingSelectionTable } from '../../components/LiquidStaking/LiquidStakingSelectionTable';
import LiquidStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LiquidStakeCard';
import LiquidUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LiquidUnstakeCard';
import UnlockNftsTable from '../../components/LiquidStaking/unlockNftsTable/UnlockNftsTable';
import { LsValidatorTable } from '../../components/LiquidStaking/LsValidatorTable';
import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeCard';
import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard';
import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable';
import { LsSearchParamKey } from '../../constants/liquidStaking/types';
import { useLiquidStakingStore } from '../../data/liquidStaking/useLiquidStakingStore';
import useSearchParamState from '../../hooks/useSearchParamState';
import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId';
import TabListItem from '../restake/TabListItem';
import TabsList from '../restake/TabsList';

Expand All @@ -28,8 +25,6 @@ const LiquidStakingTokenPage: FC = () => {
value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE,
});

const { selectedProtocolId } = useLiquidStakingStore();

return (
<div className="flex flex-wrap gap-12">
<div className="flex flex-col gap-4 w-full min-w-[450px] max-w-[600px]">
Expand All @@ -46,17 +41,11 @@ const LiquidStakingTokenPage: FC = () => {
</TabListItem>
</TabsList>

{isStaking ? <LiquidStakeCard /> : <LiquidUnstakeCard />}
{isStaking ? <LsStakeCard /> : <LsUnstakeCard />}
</div>

<div className="flex flex-col flex-grow w-min gap-4 min-w-[370px]">
{isStaking ? (
<LiquidStakingSelectionTable />
) : isLsParachainChainId(selectedProtocolId) ? (
<UnstakeRequestsTable />
) : (
<UnlockNftsTable tokenId={selectedProtocolId} />
)}
{isStaking ? <LsValidatorTable /> : <UnstakeRequestsTable />}
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions apps/tangle-dapp/components/LiquidStaking/ExternalLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ExternalLink: FC<ExternalLinkProps> = ({
<Button
className="group"
href={href}
target="_blank"
size="sm"
variant="link"
rightIcon={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import {
} from '@webb-tools/webb-ui-components';
import { useEffect, useMemo, useRef, useState } from 'react';

import useLiquidStakingItems from '../../data/liquidStaking/useLiquidStakingItems';
import { useLiquidStakingStore } from '../../data/liquidStaking/useLiquidStakingStore';
import { useLiquidStakingSelectionTableColumns } from '../../hooks/LiquidStaking/useLiquidStakingSelectionTableColumns';
import { useLsStore } from '../../data/liquidStaking/useLsStore';
import useLsValidators from '../../data/liquidStaking/useLsValidators';
import { useLsValidatorSelectionTableColumns } from '../../data/liquidStaking/useLsValidatorSelectionTableColumns';
import {
LiquidStakingItem,
LiquidStakingItemType,
Expand All @@ -42,20 +42,16 @@ const SELECTED_ITEMS_COLUMN_SORT = {
desc: false,
} as const satisfies ColumnSort;

export const LiquidStakingSelectionTable = () => {
const selectedChainId = useLiquidStakingStore(
(state) => state.selectedProtocolId,
);
const setSelectedItems = useLiquidStakingStore(
(state) => state.setSelectedItems,
);
const { isLoading, data, dataType } = useLiquidStakingItems(selectedChainId);

export const LsValidatorTable = () => {
const { selectedProtocolId, setSelectedItems } = useLsStore();
const { isLoading, data, dataType } = useLsValidators(selectedProtocolId);
const [searchValue, setSearchValue] = useState('');
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const [sorting, setSorting] = useState<SortingState>([
SELECTED_ITEMS_COLUMN_SORT,
]);

const [pagination, setPagination] =
useState<PaginationState>(DEFAULT_PAGINATION);

Expand All @@ -67,7 +63,7 @@ export const LiquidStakingSelectionTable = () => {
setSelectedItems(new Set(Object.keys(rowSelection)));
}, [rowSelection, setSelectedItems]);

const columns = useLiquidStakingSelectionTableColumns(
const columns = useLsValidatorSelectionTableColumns(
toggleSortSelectionHandlerRef,
dataType,
) as ColumnDef<LiquidStakingItemType, unknown>[];
Expand Down Expand Up @@ -99,15 +95,18 @@ export const LiquidStakingSelectionTable = () => {
}, [dataType]);

const tableData = useMemo(() => (isLoading ? [] : data), [data, isLoading]);

const tableColumns = useMemo(
() => (isLoading ? [] : columns),
[columns, isLoading],
);

const tableIsLoading = useMemo(() => {
return (
(data.length > 0 && data[0].itemType !== dataType) || isLoading === true
);
if (isLoading) {
return true;
}

return data.length > 0 && data[0].itemType !== dataType;
}, [data, dataType, isLoading]);

const tableProps = useMemo<TableOptions<LiquidStakingItemType>>(
Expand Down Expand Up @@ -136,6 +135,7 @@ export const LiquidStakingSelectionTable = () => {
typeof updaterOrValue === 'function'
? updaterOrValue(prev)
: updaterOrValue;

return newSorting.length === 0
? [SELECTED_ITEMS_COLUMN_SORT]
: newSorting[0].id === 'id'
Expand Down Expand Up @@ -183,11 +183,11 @@ export const LiquidStakingSelectionTable = () => {
</Typography>

<Input
id="search"
id="ls-validator-selection-search"
rightIcon={<Search className="mr-2" />}
placeholder="Search"
value={searchValue}
onChange={(val) => setSearchValue(val)}
onChange={(newSearchValue) => setSearchValue(newSearchValue)}
className="mb-1"
debounceTime={300}
/>
Expand Down Expand Up @@ -224,6 +224,7 @@ export const LiquidStakingSelectionTable = () => {
<div className="flex justify-center items-center min-h-[600px]">
<div className="flex items-center justify-center gap-1">
<Spinner size="md" />

<Typography
variant="body1"
fw="normal"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { InformationLine } from '@webb-tools/icons';
import { IconWithTooltip, Typography } from '@webb-tools/webb-ui-components';
import {
Chip,
GITHUB_BUG_REPORT_URL,
IconWithTooltip,
InfoIconWithTooltip,
SkeletonLoader,
Typography,
} from '@webb-tools/webb-ui-components';
import { FC, ReactNode } from 'react';

import ExternalLink from '../ExternalLink';

type DetailItemProps = {
title: string;
tooltip?: string;
value: ReactNode | string;
value: Error | ReactNode | string | null;
};

const DetailItem: FC<DetailItemProps> = ({ title, tooltip, value }) => {
Expand Down Expand Up @@ -33,11 +42,38 @@ const DetailItem: FC<DetailItemProps> = ({ title, tooltip, value }) => {
<Typography className="dark:text-mono-0" variant="body1" fw="bold">
{value}
</Typography>
) : value === null ? (
<SkeletonLoader className="max-w-[80px]" />
) : value instanceof Error ? (
<ErrorChip error={value} />
) : (
value
)}
</div>
);
};

/** @internal */
const ErrorChip: FC<{ error: Error }> = ({ error }) => {
return (
<Chip color="red">
Error{' '}
<InfoIconWithTooltip
className="dark:fill-red-400 fill-red-400"
content={
<>
<Typography variant="body1">
{error.name}: {error.message}
</Typography>

<ExternalLink href={GITHUB_BUG_REPORT_URL}>
Report issue
</ExternalLink>
</>
}
/>
</Chip>
);
};

export default DetailItem;
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { SkeletonLoader } from '@webb-tools/webb-ui-components';
import { FC } from 'react';
import { twMerge } from 'tailwind-merge';

import { LST_PREFIX } from '../../../constants/liquidStaking/constants';
import { LS_DERIVATIVE_TOKEN_PREFIX } from '../../../constants/liquidStaking/constants';
import { LsProtocolId, LsToken } from '../../../constants/liquidStaking/types';
import { ExchangeRateType } from '../../../data/liquidStaking/useExchangeRate';
import useExchangeRate from '../../../data/liquidStaking/useExchangeRate';
import { ExchangeRateType } from '../../../data/liquidStaking/useLsExchangeRate';
import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate';
import DetailItem from './DetailItem';

export type ExchangeRateDetailItemProps = {
Expand All @@ -18,28 +19,33 @@ const ExchangeRateDetailItem: FC<ExchangeRateDetailItemProps> = ({
token,
protocolId,
}) => {
const { exchangeRate, isRefreshing } = useExchangeRate(type, protocolId);
const { exchangeRate, isRefreshing } = useLsExchangeRate(type, protocolId);

const exchangeRateElement =
exchangeRate === null ? (
exchangeRate instanceof Error ? (
exchangeRate
) : exchangeRate === null ? (
<SkeletonLoader className="w-[50px]" />
) : isRefreshing ? (
<div className="animate-pulse">{exchangeRate}</div>
) : (
exchangeRate
);

return (
<DetailItem
title="Rate"
value={
<div className="flex gap-1 items-center justify-center whitespace-nowrap">
1 {token} = {exchangeRateElement} {LST_PREFIX}
{token}
</div>
}
/>
);
const value =
exchangeRateElement instanceof Error ? (
exchangeRateElement
) : (
<div
className={twMerge(
'flex gap-1 items-center justify-center whitespace-nowrap',
isRefreshing && 'animate-pulse',
)}
>
1 {token} = {exchangeRateElement} {LS_DERIVATIVE_TOKEN_PREFIX}
{token}
</div>
);

return <DetailItem title="Rate" value={value} />;
};

export default ExchangeRateDetailItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { BN, BN_ZERO } from '@polkadot/util';
import { FC, useMemo } from 'react';

import { LsProtocolId } from '../../../constants/liquidStaking/types';
import formatBn from '../../../utils/formatBn';
import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef';
import scaleAmountByPermill from '../../../utils/scaleAmountByPermill';
import DetailItem from './DetailItem';
import useLsFeePermill from './useLsFeePermill';

export type FeeDetailItemProps = {
isMinting: boolean;
inputAmount: BN | null;
protocolId: LsProtocolId;
};

const FeeDetailItem: FC<FeeDetailItemProps> = ({
isMinting,
inputAmount,
protocolId,
}) => {
const feePermill = useLsFeePermill(protocolId, isMinting);

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 feePermill !== 'number') {
return feePermill;
}

return scaleAmountByPermill(inputAmount ?? BN_ZERO, feePermill);
}, [feePermill, inputAmount]);

const formattedFeeAmount = useMemo(() => {
// Propagate error or loading state.
if (!(feeAmount instanceof BN)) {
return feeAmount;
}

const formattedAmount = formatBn(feeAmount, protocol.decimals, {
includeCommas: true,
});

return `${formattedAmount} ${protocol.token}`;
}, [feeAmount, protocol.decimals, protocol.token]);

const feeTitle =
typeof feePermill !== 'number'
? 'Fee'
: `Fee (${(feePermill * 100).toFixed(2)}%)`;

return <DetailItem title={feeTitle} value={formattedFeeAmount} />;
};

export default FeeDetailItem;
Loading

0 comments on commit a881ede

Please sign in to comment.