Skip to content

Commit

Permalink
feat(tangle-dapp): Implement stake & unstake liquifier functionality …
Browse files Browse the repository at this point in the history
…for EVM-based liquid staking (#2500)
  • Loading branch information
yurixander authored Aug 26, 2024
1 parent 37aafc3 commit de9b89f
Show file tree
Hide file tree
Showing 87 changed files with 3,687 additions and 1,236 deletions.
4 changes: 2 additions & 2 deletions apps/tangle-dapp/app/blueprints/[name]/OperatorsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Link from 'next/link';
import { FC, useMemo, useState } from 'react';
import { twMerge } from 'tailwind-merge';

import LSTTokenIcon from '../../../components/LSTTokenIcon';
import LsTokenIcon from '../../../components/LsTokenIcon';
import { TableStatus } from '../../../components/TableStatus';
import useNetworkStore from '../../../context/useNetworkStore';
import { ExplorerType } from '../../../types';
Expand Down Expand Up @@ -96,7 +96,7 @@ const staticColumns = [
.getValue()
.sort() // sort alphabetically
.map((vault, index) => (
<LSTTokenIcon key={index} name={vault} />
<LsTokenIcon key={index} name={vault} />
))}
</div>
</TableCellWrapper>
Expand Down
4 changes: 2 additions & 2 deletions apps/tangle-dapp/app/blueprints/[name]/TvlTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Link from 'next/link';
import { FC, useCallback, useState } from 'react';
import { twMerge } from 'tailwind-merge';

import LSTTokenIcon from '../../../components/LSTTokenIcon';
import LsTokenIcon from '../../../components/LsTokenIcon';
import { Vault } from '../../../types/blueprint';
import TableCellWrapper from './TableCellWrapper';
import useVaults from './useVaults';
Expand All @@ -35,7 +35,7 @@ const columns = [
cell: (props) => (
<TableCellWrapper className="pl-3">
<div className="flex items-center gap-2">
<LSTTokenIcon name={props.row.original.lstToken} size="lg" />
<LsTokenIcon name={props.row.original.lstToken} size="lg" />
<Typography variant="h5" className="whitespace-nowrap">
{props.getValue()}
</Typography>
Expand Down
59 changes: 0 additions & 59 deletions apps/tangle-dapp/app/liquid-staking/[tokenSymbol]/page.tsx

This file was deleted.

61 changes: 61 additions & 0 deletions apps/tangle-dapp/app/liquid-staking/overview/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Typography } from '@webb-tools/webb-ui-components';
import { FC } from 'react';

import { GlassCard } from '../../../components';
import LsOverviewItem from '../../../components/LiquidStaking/stakeAndUnstake/LsOverviewItem';
import StatItem from '../../../components/LiquidStaking/StatItem';
import { LS_PROTOCOLS } from '../../../constants/liquidStaking/constants';
import { LsProtocolId } from '../../../constants/liquidStaking/types';

const LiquidStakingPage: FC = () => {
return (
<div className="flex flex-col gap-10">
<GlassCard className="flex flex-row items-center justify-between w-full overflow-x-auto">
<div className="flex flex-col gap-2">
<Typography variant="h5" fw="bold">
Tangle Liquid Staking
</Typography>

<Typography variant="body1" fw="normal">
Get Liquid Staking Tokens (LSTs) to earn & unleash restaking on
Tangle Mainnet via delegation.
</Typography>
</div>

<div className="flex gap-6 h-full">
<StatItem title="$123.01" subtitle="My Total Staking" />
</div>
</GlassCard>

<div className="flex flex-col gap-4">
<Typography variant="h4" fw="bold">
Liquid Staking Tokens
</Typography>

<GlassCard className="space-y-4">
<div className="overflow-x-auto">
<div className="flex flex-col gap-4 min-w-[750px]">
{LS_PROTOCOLS.map((protocol) => {
return (
<LsOverviewItem
key={protocol.id}
title={`Tangle Liquid ${protocol.name}`}
tokenSymbol={protocol.token}
// TODO: Can't pass non-plain objects as props to Client components from Server components (this page). For now, passing in as a string then creating BN instance inside the component.
totalStaked="100000000"
totalValueStaked={220_000_123}
hasLiquidIndicator={
protocol.id !== LsProtocolId.TANGLE_RESTAKING_PARACHAIN
}
/>
);
})}
</div>
</div>
</GlassCard>
</div>
</div>
);
};

export default LiquidStakingPage;
104 changes: 56 additions & 48 deletions apps/tangle-dapp/app/liquid-staking/page.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,65 @@
import { Typography } from '@webb-tools/webb-ui-components';
'use client';

import { FC } from 'react';

import { GlassCard } from '../../components';
import LiquidStakingTokenItem from '../../components/LiquidStaking/stakeAndUnstake/LiquidStakingTokenItem';
import StatItem from '../../components/LiquidStaking/StatItem';
import { LIQUID_STAKING_CHAINS } from '../../constants/liquidStaking';
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 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';

enum SearchParamAction {
STAKE = 'stake',
UNSTAKE = 'unstake',
}

const LiquidStakingTokenPage: FC = () => {
const [isStaking, setIsStaking] = useSearchParamState({
defaultValue: true,
key: LsSearchParamKey.ACTION,
parser: (value) => value === SearchParamAction.STAKE,
stringify: (value) =>
value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE,
});

const { selectedProtocolId } = useLiquidStakingStore();

const LiquidStakingPage: FC = () => {
return (
<div className="flex flex-col gap-10">
<GlassCard className="flex flex-row items-center justify-between w-full overflow-x-auto">
<div className="flex flex-col gap-2">
<Typography variant="h5" fw="bold">
Tangle Liquid Staking
</Typography>

<Typography variant="body1" fw="normal">
Get Liquid Staking Tokens (LSTs) to earn & unleash restaking on
Tangle Mainnet via delegation.
</Typography>
</div>

<div className="flex gap-6 h-full">
<StatItem title="$123.01" subtitle="My Total Staking" />
</div>
</GlassCard>

<div className="flex flex-col gap-4">
<Typography variant="h4" fw="bold">
Liquid Staking Tokens
</Typography>

<GlassCard className="space-y-4">
<div className="overflow-x-auto">
<div className="flex flex-col gap-4 min-w-[750px]">
{LIQUID_STAKING_CHAINS.map((chain) => {
return (
<LiquidStakingTokenItem
key={chain.id}
title={`Tangle ${chain.name}`}
tokenSymbol={chain.token}
// TODO: Can't pass non-plain objects as props to Client components from Server components (this page). For now, passing in as a string then creating BN instance inside the component.
totalStaked="100000000"
totalValueStaked={220_000_123}
/>
);
})}
</div>
</div>
</GlassCard>
<div className="flex flex-wrap gap-12">
<div className="flex flex-col gap-4 w-full min-w-[450px] max-w-[600px]">
<TabsList className="w-full">
<TabListItem isActive={isStaking} onClick={() => setIsStaking(true)}>
Stake
</TabListItem>

<TabListItem
isActive={!isStaking}
onClick={() => setIsStaking(false)}
>
Unstake
</TabListItem>
</TabsList>

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

<div className="flex flex-col flex-grow w-min gap-4 min-w-[370px]">
{isStaking ? (
<LiquidStakingSelectionTable />
) : isLsParachainChainId(selectedProtocolId) ? (
<UnstakeRequestsTable />
) : (
<UnlockNftsTable tokenId={selectedProtocolId} />
)}
</div>
</div>
);
};

export default LiquidStakingPage;
export default LiquidStakingTokenPage;
32 changes: 14 additions & 18 deletions apps/tangle-dapp/components/AmountInput/AmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,17 @@ const AmountInput: FC<AmountInputProps> = ({
const inputRef = useRef<HTMLInputElement>(null);
const { nativeTokenSymbol } = useNetworkStore();

const {
displayAmount,
errorMessage,
handleChange,
updateDisplayAmountManual,
} = useInputAmount({
amount,
min,
max,
decimals,
errorOnEmptyValue,
setAmount,
minErrorMessage,
maxErrorMessage,
});
const { displayAmount, errorMessage, handleChange, setDisplayAmount } =
useInputAmount({
amount,
min,
max,
decimals,
errorOnEmptyValue,
setAmount,
minErrorMessage,
maxErrorMessage,
});

// Set the error message in the parent component.
useEffect(() => {
Expand All @@ -77,11 +73,11 @@ const AmountInput: FC<AmountInputProps> = ({
}, [errorMessage, setErrorMessage]);

const setMaxAmount = useCallback(() => {
if (max !== null) {
if (max !== null && amount?.toString() !== max.toString()) {
setAmount(max);
updateDisplayAmountManual(max);
setDisplayAmount(max);
}
}, [max, setAmount, updateDisplayAmountManual]);
}, [amount, max, setAmount, setDisplayAmount]);

const actions: ReactNode = useMemo(
() => (
Expand Down
11 changes: 1 addition & 10 deletions apps/tangle-dapp/components/Breadcrumbs/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import assert from 'assert';
import capitalize from 'lodash/capitalize';
import { JSX } from 'react';

import { LST_PREFIX } from '../../constants/liquidStaking';
import { PagePath } from '../../types';

const BREADCRUMB_ICONS: Record<PagePath, (props: IconBase) => JSX.Element> = {
Expand All @@ -29,6 +28,7 @@ const BREADCRUMB_ICONS: Record<PagePath, (props: IconBase) => JSX.Element> = {
[PagePath.RESTAKE_DELEGATE]: TokenSwapFill,
[PagePath.BRIDGE]: ShuffleLine,
[PagePath.LIQUID_STAKING]: WaterDropletIcon,
[PagePath.LIQUID_STAKING_OVERVIEW]: WaterDropletIcon,
};

const BREADCRUMB_LABELS: Partial<Record<PagePath, string>> = {
Expand Down Expand Up @@ -82,15 +82,6 @@ export const getBreadcrumbLabel = (
) {
return `Details: ${pathName}`;
}
// Special case for Liquid Staking individual token pages.
// Show it as something like `tgDOT` instead of `Dot`.
else if (
pathNames.length === 2 &&
index === 1 &&
pathNames[0] === PagePath.LIQUID_STAKING.substring(1)
) {
return `${LST_PREFIX}${pathName.toUpperCase()}`;
}

const pathNameWithSlash = '/' + pathName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
} from '@webb-tools/webb-ui-components';
import { useEffect, useMemo, useRef, useState } from 'react';

import { useLiquidStakingStore } from '../../data/liquidStaking/store';
import useLiquidStakingItems from '../../data/liquidStaking/useLiquidStakingItems';
import { useLiquidStakingStore } from '../../data/liquidStaking/useLiquidStakingStore';
import { useLiquidStakingSelectionTableColumns } from '../../hooks/LiquidStaking/useLiquidStakingSelectionTableColumns';
import {
LiquidStakingItem,
Expand All @@ -44,7 +44,7 @@ const SELECTED_ITEMS_COLUMN_SORT = {

export const LiquidStakingSelectionTable = () => {
const selectedChainId = useLiquidStakingStore(
(state) => state.selectedChainId,
(state) => state.selectedProtocolId,
);
const setSelectedItems = useLiquidStakingStore(
(state) => state.setSelectedItems,
Expand Down
Loading

0 comments on commit de9b89f

Please sign in to comment.