Skip to content

Commit 2f10bbe

Browse files
authored
chore(tangle-dapp): Restake Undelegate & Withdraw Improvements (#3041)
1 parent b29b631 commit 2f10bbe

File tree

15 files changed

+560
-130
lines changed

15 files changed

+560
-130
lines changed

apps/tangle-dapp/src/components/Lists/AssetListItem.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { makeExplorerUrl } from '@tangle-network/api-provider-environment/transa
1212
import { useActiveChain } from '@tangle-network/api-provider-environment/hooks/useActiveChain';
1313
import LogoListItem from './LogoListItem';
1414
import { BN } from '@polkadot/util';
15+
import { getAssetLabelColorClasses } from '../../utils/getAssetLabelColorClasses';
1516

1617
type Props = {
1718
assetId: string;
@@ -99,13 +100,7 @@ const AssetListItem = ({
99100
{name} ({symbol})
100101
</span>
101102
<span
102-
className={`px-2 py-1 rounded text-xs font-medium ${
103-
labelColor === 'green'
104-
? 'bg-green-100 text-mono-0 dark:bg-green-900 dark:text-mono-0'
105-
: labelColor === 'purple'
106-
? 'bg-purple-900 text-mono-0 dark:bg-purple-900 dark:text-mono-0'
107-
: 'bg-blue-900 text-mono-0 dark:bg-blue-900 dark:text-mono-0'
108-
}`}
103+
className={`px-2 py-1 rounded text-xs font-medium ${getAssetLabelColorClasses(labelColor)}`}
109104
>
110105
{label}
111106
</span>

apps/tangle-dapp/src/components/Lists/LogoListItem.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type Props = {
99
leftUpperContent: ReactNode | string;
1010
leftBottomContent?: ReactNode | string;
1111
leftBottomContentTwo?: ReactNode | string;
12-
rightUpperText?: string;
12+
rightUpperText?: string | ReactNode;
1313
rightBottomText?: string;
1414
};
1515

@@ -69,12 +69,18 @@ const LogoListItem: FC<Props> = ({
6969

7070
{(rightUpperText !== undefined || rightBottomText !== undefined) && (
7171
<div className="flex flex-col items-end justify-center">
72-
<Typography
73-
variant="body1"
74-
className="text-mono-200 dark:text-mono-0"
75-
>
76-
{rightUpperText ?? EMPTY_VALUE_PLACEHOLDER}
77-
</Typography>
72+
{typeof rightUpperText === 'string' ? (
73+
<Typography
74+
variant="body1"
75+
className="text-mono-200 dark:text-mono-0"
76+
>
77+
{rightUpperText ?? EMPTY_VALUE_PLACEHOLDER}
78+
</Typography>
79+
) : (
80+
<div className="text-mono-200 dark:text-mono-0">
81+
{rightUpperText ?? EMPTY_VALUE_PLACEHOLDER}
82+
</div>
83+
)}
7884

7985
{rightBottomText !== undefined && (
8086
<Typography

apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import LogoListItem from './LogoListItem';
1010
type Props = {
1111
accountAddress: SubstrateAddress;
1212
identity?: string;
13-
rightUpperText?: string;
13+
rightUpperText?: string | React.ReactNode;
1414
rightBottomText?: string;
1515
};
1616

apps/tangle-dapp/src/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export enum TxName {
8181
RESTAKE_NATIVE_DELEGATE = 'restake delegate nomination',
8282
RESTAKE_NATIVE_UNSTAKE = 'restake undelegate native',
8383
RESTAKE_NATIVE_UNSTAKE_EXECUTE = 'restake execute undelegate native',
84+
RESTAKE_DEPOSITED_UNSTAKE_EXECUTE = 'restake execute undelegate deposited',
8485
RESTAKE_NATIVE_UNSTAKE_CANCEL = 'restake cancel undelegate native',
8586
}
8687

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { DelegatorInfo } from '@tangle-network/tangle-shared-ui/types/restake';
2+
import type { IdentityType } from '@tangle-network/tangle-shared-ui/utils/polkadot/identity';
3+
import { useMemo } from 'react';
4+
import ListModal from '@tangle-network/tangle-shared-ui/components/ListModal';
5+
import { formatUnits } from 'viem';
6+
import { getAssetLabelColorClasses } from '../../utils/getAssetLabelColorClasses';
7+
import OperatorListItem from '../../components/Lists/OperatorListItem';
8+
import {
9+
AmountFormatStyle,
10+
formatDisplayAmount,
11+
} from '@tangle-network/ui-components';
12+
import { BN } from '@polkadot/util';
13+
import useRestakeAssets from '@tangle-network/tangle-shared-ui/data/restake/useRestakeAssets';
14+
import filterBy from '@tangle-network/tangle-shared-ui/utils/filterBy';
15+
import { Typography } from '@tangle-network/ui-components/typography/Typography';
16+
import useRestakeCurrentRound from '../../data/restake/useRestakeCurrentRound';
17+
18+
type Props = {
19+
delegatorInfo: DelegatorInfo | null;
20+
operatorIdentities?: Map<string, IdentityType | null> | null;
21+
isOpen: boolean;
22+
setIsOpen: (isOpen: boolean) => void;
23+
24+
onItemSelected: (
25+
item: DelegatorInfo['delegations'][number] & {
26+
formattedAmount: string;
27+
isReadyToUnstake: boolean;
28+
undelegatableAmount: bigint;
29+
},
30+
) => void;
31+
};
32+
33+
const calculateUndelegatableAmount = (
34+
delegation: DelegatorInfo['delegations'][number],
35+
unstakeRequests: DelegatorInfo['unstakeRequests'],
36+
): bigint => {
37+
const pendingUnstakeAmount = unstakeRequests
38+
.filter(
39+
(req) =>
40+
req.operatorAccountId === delegation.operatorAccountId &&
41+
req.assetId === delegation.assetId &&
42+
req.isNomination === delegation.isNomination,
43+
)
44+
.reduce((sum, req) => sum + req.amount, BigInt(0));
45+
46+
const availableAmount = delegation.amountBonded - pendingUnstakeAmount;
47+
return availableAmount > BigInt(0) ? availableAmount : BigInt(0);
48+
};
49+
50+
const SelectOperatorModalEnhanced = ({
51+
delegatorInfo,
52+
isOpen,
53+
setIsOpen,
54+
onItemSelected,
55+
operatorIdentities,
56+
}: Props) => {
57+
const { assets } = useRestakeAssets();
58+
const { result: currentRound } = useRestakeCurrentRound();
59+
60+
const delegations = delegatorInfo?.delegations;
61+
const unstakeRequests = useMemo(
62+
() => delegatorInfo?.unstakeRequests || [],
63+
[delegatorInfo?.unstakeRequests],
64+
);
65+
66+
const isLoading = delegatorInfo === null || assets === undefined;
67+
68+
const availableDelegations = useMemo(() => {
69+
if (isLoading || !Array.isArray(delegations)) {
70+
return undefined;
71+
}
72+
73+
return delegations
74+
.map((delegation) => {
75+
const undelegatableAmount = calculateUndelegatableAmount(
76+
delegation,
77+
unstakeRequests,
78+
);
79+
80+
const pendingRequest = unstakeRequests.find(
81+
(req) =>
82+
req.operatorAccountId === delegation.operatorAccountId &&
83+
req.assetId === delegation.assetId,
84+
);
85+
86+
const isRequestReady =
87+
!pendingRequest ||
88+
(currentRound !== null &&
89+
pendingRequest.requestedRound <= currentRound) ||
90+
false;
91+
92+
return {
93+
...delegation,
94+
undelegatableAmount,
95+
pendingUnstakeRequest: pendingRequest,
96+
isRequestReady,
97+
};
98+
})
99+
.filter((item) => item.undelegatableAmount > BigInt(0));
100+
}, [isLoading, delegations, unstakeRequests, currentRound]);
101+
102+
return (
103+
<ListModal
104+
isOpen={isOpen}
105+
setIsOpen={setIsOpen}
106+
searchInputId="restake-undelegate-operator-search"
107+
searchPlaceholder="Search operators..."
108+
items={availableDelegations}
109+
isLoading={isLoading}
110+
title="Select Operator to Unstake"
111+
titleWhenEmpty="No Delegations Found"
112+
descriptionWhenEmpty="No delegations available for unstaking. You may need to wait for pending unstake requests to complete or delegate assets first."
113+
onSelect={(item) => {
114+
const asset = assets?.get(item.assetId);
115+
116+
if (asset === undefined) {
117+
console.error(
118+
`SelectOperatorModalEnhanced: Asset with ID ${item.assetId} not found in assets map. Available assets:`,
119+
Array.from(assets?.keys() || []),
120+
);
121+
return;
122+
}
123+
124+
const fmtAmount = formatUnits(
125+
item.undelegatableAmount,
126+
asset.metadata.decimals,
127+
);
128+
129+
onItemSelected({
130+
...item,
131+
formattedAmount: fmtAmount,
132+
isReadyToUnstake: item.isRequestReady,
133+
undelegatableAmount: item.undelegatableAmount,
134+
});
135+
}}
136+
filterItem={(delegation, query) => {
137+
const asset = assets?.get(delegation.assetId);
138+
139+
if (asset === undefined) {
140+
return false;
141+
}
142+
143+
const assetSymbol = asset.metadata.symbol;
144+
145+
const identityName = operatorIdentities?.get(
146+
delegation.operatorAccountId,
147+
)?.name;
148+
149+
return filterBy(query, [
150+
delegation.operatorAccountId,
151+
assetSymbol,
152+
identityName,
153+
]);
154+
}}
155+
renderItem={(item) => {
156+
const asset = assets?.get(item.assetId);
157+
158+
if (asset === undefined) {
159+
return null;
160+
}
161+
162+
const fmtAvailableAmount = formatDisplayAmount(
163+
new BN(item.undelegatableAmount.toString()),
164+
asset.metadata.decimals,
165+
AmountFormatStyle.SHORT,
166+
);
167+
168+
const identityName = operatorIdentities?.get(
169+
item.operatorAccountId,
170+
)?.name;
171+
172+
const assetLabel = item.isNomination ? 'Nominated' : 'Deposited';
173+
const labelColor = item.isNomination ? 'purple' : 'green';
174+
175+
return (
176+
<div className="flex items-center justify-between w-full">
177+
<OperatorListItem
178+
accountAddress={item.operatorAccountId}
179+
identity={identityName ?? undefined}
180+
rightUpperText={
181+
<div className="flex items-center gap-2">
182+
<span>
183+
{fmtAvailableAmount} {asset.metadata.symbol}
184+
</span>
185+
<span
186+
className={`px-2 py-1 rounded text-xs font-medium ${getAssetLabelColorClasses(labelColor)}`}
187+
>
188+
{assetLabel}
189+
</span>
190+
</div>
191+
}
192+
rightBottomText="Balance"
193+
/>
194+
{item.pendingUnstakeRequest &&
195+
currentRound &&
196+
!item.isRequestReady && (
197+
<Typography variant="body2" className="text-mono-100 ml-2">
198+
{item.pendingUnstakeRequest.requestedRound - currentRound}{' '}
199+
rounds left
200+
</Typography>
201+
)}
202+
</div>
203+
);
204+
}}
205+
/>
206+
);
207+
};
208+
209+
export default SelectOperatorModalEnhanced;

apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type UnstakeRequestTableRow = {
4343
sessionDurationMs: number;
4444
operatorAccountId: SubstrateAddress;
4545
operatorIdentityName?: string;
46+
isNomination: boolean;
4647
};
4748

4849
const COLUMN_HELPER = createColumnHelper<UnstakeRequestTableRow>();
@@ -71,8 +72,23 @@ const COLUMNS = [
7172
COLUMN_HELPER.accessor('amount', {
7273
header: () => <TableCell>Amount</TableCell>,
7374
cell: (props) => (
74-
<TableCell fw="normal" className="text-mono-200 dark:text-mono-0">
75-
{props.getValue()} {props.row.original.assetSymbol}
75+
<TableCell>
76+
<div className="flex items-center gap-2">
77+
<Typography variant="body2" className="font-bold">
78+
{props.getValue()}
79+
</Typography>
80+
{props.row.original.assetId === '0' && (
81+
<span
82+
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
83+
props.row.original.isNomination
84+
? 'bg-purple-900 text-mono-0'
85+
: 'bg-green-100 text-mono-0 dark:bg-green-900'
86+
}`}
87+
>
88+
{props.row.original.isNomination ? 'Nominated' : 'Deposited'}
89+
</span>
90+
)}
91+
</div>
7692
</TableCell>
7793
),
7894
}),
@@ -129,7 +145,13 @@ const UnstakeRequestTable: FC<Props> = ({
129145
}
130146

131147
return unstakeRequests.flatMap(
132-
({ assetId, amount, requestedRound, operatorAccountId }) => {
148+
({
149+
assetId,
150+
amount,
151+
requestedRound,
152+
operatorAccountId,
153+
isNomination,
154+
}) => {
133155
const asset = assets?.get(assetId);
134156

135157
// Skip requests that are lacking metadata.
@@ -159,6 +181,7 @@ const UnstakeRequestTable: FC<Props> = ({
159181
operatorAccountId,
160182
operatorIdentityName:
161183
operatorIdentities.get(operatorAccountId)?.name ?? undefined,
184+
isNomination,
162185
} satisfies UnstakeRequestTableRow;
163186
},
164187
);

0 commit comments

Comments
 (0)