Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bb406c4
fix(liquid-staking): QA fixes for vault display and asset lookups
vutuanlinh2k2 Dec 22, 2025
944bde0
fix(liquid-staking): improve UI consistency and formatting in liquid …
vutuanlinh2k2 Dec 22, 2025
c76443c
fix(liquid-staking): pre-select vault from URL query parameter in dep…
vutuanlinh2k2 Dec 22, 2025
6e911d5
chore: format code
vutuanlinh2k2 Dec 22, 2025
a19eacb
refactor(tangle): remove unused code and clean up ABI definition
vutuanlinh2k2 Dec 22, 2025
9c2c842
fix(claim-relayer): update pubkey and merkle proof validation to chec…
vutuanlinh2k2 Dec 22, 2025
fe53c11
fix(dapp-config): make token metadata chain-aware to prevent cross-ch…
vutuanlinh2k2 Dec 23, 2025
c275076
Merge remote-tracking branch 'origin/v2' into linh/qa/liquid-staking
vutuanlinh2k2 Dec 23, 2025
bba09ea
feat(liquid-staking): add transaction history details for liquid stak…
vutuanlinh2k2 Dec 23, 2025
6cd22cc
fix(liquid-staking): improve redeem UX with optimistic updates and cl…
vutuanlinh2k2 Dec 23, 2025
ea26f96
refactor: format code and simplify liquid staking redeem UI
vutuanlinh2k2 Dec 23, 2025
e6029a5
fix: revert ABI files to restore typecheck compatibility
vutuanlinh2k2 Dec 23, 2025
5253fe8
chore: remove redundant comma
vutuanlinh2k2 Dec 23, 2025
97ae28d
chore: format code
vutuanlinh2k2 Dec 23, 2025
79ec721
Merge remote-tracking branch 'origin/v2' into linh/qa/liquid-staking
vutuanlinh2k2 Dec 23, 2025
9a4f453
chore: format ABI files with prettier
vutuanlinh2k2 Dec 23, 2025
3084407
fix: resolve typecheck errors and update deposit button style
vutuanlinh2k2 Dec 23, 2025
3801b40
chore: format code
vutuanlinh2k2 Dec 23, 2025
5a4662a
chore: update yarn.lock
vutuanlinh2k2 Dec 23, 2025
d3de41f
fix: address PR review feedback for liquid staking
vutuanlinh2k2 Dec 24, 2025
cbfbbcb
Merge branch 'v2' into linh/qa/liquid-staking
vutuanlinh2k2 Dec 24, 2025
f5661b1
fix: show wallet mode banner when relayer is unavailable
vutuanlinh2k2 Dec 25, 2025
25b6bda
chore: add full TangleMigration ABI and Base Sepolia icon
vutuanlinh2k2 Dec 28, 2025
0401250
chore: format code
vutuanlinh2k2 Dec 28, 2025
5bc8d8c
fix: simplify proof polling progress message
vutuanlinh2k2 Dec 29, 2025
65c30f9
fix: remove spinner from proof progress indicator
vutuanlinh2k2 Dec 29, 2025
c6e5258
fix: use on-chain data as source of truth for restake pages
vutuanlinh2k2 Dec 29, 2025
f93cb27
refactor: rename unstake to undelegate for consistency
vutuanlinh2k2 Dec 29, 2025
8b2fbf5
fix: use on-chain data for delegator in RestakeContext
vutuanlinh2k2 Dec 29, 2025
31f6c44
fix: navigate to delegate flow when clicking delegate on operators tab
vutuanlinh2k2 Dec 29, 2025
5a32b59
feat: show request count badge on expand table button
vutuanlinh2k2 Dec 30, 2025
230daee
fix: use network store for chain ID in useRestakingAssets hooks
vutuanlinh2k2 Dec 30, 2025
5b165ad
chore: format code
vutuanlinh2k2 Dec 30, 2025
1be98c8
Merge branch 'v2' into linh/qa/restaking-2 and fix conflicts
vutuanlinh2k2 Dec 30, 2025
5cc76bf
refactor: implement PR review recommendations
vutuanlinh2k2 Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions apps/tangle-dapp/src/components/tables/OperatorsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { TableVariant } from '@tangle-network/ui-components/components/Table/typ
interface Props {
operatorMap: Map<Address, Operator> | null;
isLoading: boolean;
onRestakeClicked: () => void;
onDelegateClicked: (operatorAddress: Address) => void;
}

const formatDelegationCount = (count: bigint | null | undefined): number => {
Expand All @@ -20,7 +20,7 @@ const formatDelegationCount = (count: bigint | null | undefined): number => {
export const OperatorsTable: FC<Props> = ({
operatorMap,
isLoading,
onRestakeClicked,
onDelegateClicked,
}) => {
const data = useMemo<RestakeOperator[]>(() => {
if (!operatorMap) return [];
Expand Down Expand Up @@ -49,18 +49,16 @@ export const OperatorsTable: FC<Props> = ({
emptyTableProps={{
title: 'No Operators Available',
description: 'Be the first to register as a restaking operator.',
buttonText: 'Register as Operator',
buttonProps: { onClick: onRestakeClicked },
}}
tableProps={{
variant: TableVariant.GLASS_OUTER,
}}
RestakeOperatorAction={({ address: _address }) => (
RestakeOperatorAction={({ address }) => (
<Button
variant="secondary"
size="sm"
onClick={() => {
onRestakeClicked();
onDelegateClicked(address as Address);
}}
className="min-w-24"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { RestakeAction } from '../../constants';
import BlueprintListing from '../../pages/blueprints/BlueprintListing';
import RestakeDelegateForm from '../../pages/restake/delegate';
import DepositForm from '../../pages/restake/deposit/DepositForm';
import RestakeUnstakeForm from '../../pages/restake/unstake';
import RestakeUndelegateForm from '../../pages/restake/undelegate';
import RestakeWithdrawForm from '../../pages/restake/withdraw';
import { PagePath, QueryParamKey } from '../../types';

Expand Down Expand Up @@ -87,7 +87,7 @@ const RestakeOverviewTabs: FC<Props> = ({
) : action === RestakeAction.DELEGATE ? (
<RestakeDelegateForm />
) : action === RestakeAction.UNDELEGATE ? (
<RestakeUnstakeForm />
<RestakeUndelegateForm />
) : null}
</TabContent>

Expand Down
20 changes: 13 additions & 7 deletions apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { RestakeAction, RestakeTab } from '../../constants';
import DepositForm from '../../pages/restake/deposit/DepositForm';
import RestakeWithdrawForm from '../../pages/restake/withdraw';
import RestakeDelegateForm from '../../pages/restake/delegate';
import RestakeUnstakeForm from '../../pages/restake/unstake';
import RestakeUndelegateForm from '../../pages/restake/undelegate';
import BlueprintListing from '../../pages/blueprints/BlueprintListing';
import { useNavigate } from 'react-router';
import { PagePath } from '../../types';
import { PagePath, QueryParamKey } from '../../types';
import type { Address } from 'viem';
import { NetworkGuard } from '../../components/NetworkGuard';
import { RestakingAssetsTable } from '../../components/tables/RestakingAssetsTable';
import { OperatorsTable } from '../../components/tables/OperatorsTable';
Expand Down Expand Up @@ -42,9 +43,14 @@ const RestakeTabContentInner: FC<Props> = ({ tab }) => {
const context = useOptionalRestakeContext();

// Must call useCallback before any conditional returns (React hooks rules)
const handleRestakeClicked = useCallback(() => {
navigate(PagePath.RESTAKE_DEPOSIT);
}, [navigate]);
const handleDelegateClicked = useCallback(
(operatorAddress: Address) => {
navigate(
`${PagePath.RESTAKE_DELEGATE}?${QueryParamKey.RESTAKE_OPERATOR}=${operatorAddress}`,
);
},
[navigate],
);

// If context is not available (e.g., during HMR), show loading state
if (!context) {
Expand Down Expand Up @@ -76,7 +82,7 @@ const RestakeTabContentInner: FC<Props> = ({ tab }) => {
case RestakeAction.DELEGATE:
return <RestakeDelegateForm />;
case RestakeAction.UNDELEGATE:
return <RestakeUnstakeForm />;
return <RestakeUndelegateForm />;
case RestakeTab.VAULTS:
return (
<RestakingAssetsTable
Expand All @@ -93,7 +99,7 @@ const RestakeTabContentInner: FC<Props> = ({ tab }) => {
<OperatorsTable
operatorMap={operatorMap}
isLoading={isLoadingOperators}
onRestakeClicked={handleRestakeClicked}
onDelegateClicked={handleDelegateClicked}
/>
);
case RestakeTab.BLUEPRINTS:
Expand Down
14 changes: 7 additions & 7 deletions apps/tangle-dapp/src/data/restaking/useUserRestakingStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface UserRestakingStats {
totalDelegated: bigint;
withdrawQueueAmount: bigint;
withdrawableAmount: bigint;
pendingUnstakeAmount: bigint;
pendingUndelegateAmount: bigint;

// Claimable Reward Value Card
pendingRewards: bigint;
Expand All @@ -31,7 +31,7 @@ export interface UserRestakingStats {
totalDelegated: string;
withdrawQueueAmount: string;
withdrawableAmount: string;
pendingUnstakeAmount: string;
pendingUndelegateAmount: string;
pendingRewards: string;
activeBalance: string;
};
Expand Down Expand Up @@ -98,11 +98,11 @@ const useUserRestakingStats = () => {
}
}

// Calculate pending unstake amounts
let pendingUnstakeAmount = BigInt(0);
// Calculate pending undelegate amounts (note: unstakeRequests is the GraphQL field name)
let pendingUndelegateAmount = BigInt(0);
for (const req of delegator.unstakeRequests) {
if (req.status === 'PENDING') {
pendingUnstakeAmount += req.estimatedAmount;
pendingUndelegateAmount += req.estimatedAmount;
}
}

Expand All @@ -127,15 +127,15 @@ const useUserRestakingStats = () => {
totalDelegated: delegator.totalDelegated,
withdrawQueueAmount,
withdrawableAmount,
pendingUnstakeAmount,
pendingUndelegateAmount,
pendingRewards,
activeBalance,
formatted: {
totalDeposited: format(delegator.totalDeposited),
totalDelegated: format(delegator.totalDelegated),
withdrawQueueAmount: format(withdrawQueueAmount),
withdrawableAmount: format(withdrawableAmount),
pendingUnstakeAmount: format(pendingUnstakeAmount),
pendingUndelegateAmount: format(pendingUndelegateAmount),
pendingRewards: format(pendingRewards),
activeBalance: format(activeBalance),
},
Expand Down
54 changes: 1 addition & 53 deletions apps/tangle-dapp/src/pages/claim/migration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,66 +21,14 @@ import { type Hex, formatUnits, isAddress, keccak256, toHex } from 'viem';
import { twMerge } from 'tailwind-merge';
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
import { AnimatePresence, motion } from 'framer-motion';
import parseTransactionError from '@tangle-network/tangle-shared-ui/utils/parseTransactionError';
import SubstrateWalletSelector from './components/SubstrateWalletSelector';
import useClaimEligibility, {
generateChallenge,
} from './hooks/useClaimEligibility';
import useGenerateProof from './hooks/useGenerateProof';
import useSubmitClaim from './hooks/useSubmitClaim';

// Parse transaction errors into user-friendly messages
const parseTransactionError = (error: Error): string => {
const message = error.message || String(error);

// Common error patterns
if (message.includes('intrinsic gas too low')) {
return 'Transaction failed: Gas limit too low. Please try again.';
}
if (message.includes('insufficient funds')) {
return 'Insufficient funds to cover gas fees.';
}
if (message.includes('user rejected') || message.includes('User rejected')) {
return 'Transaction was rejected by user.';
}
if (message.includes('Failed to fetch')) {
return 'Network request failed. If you are using the local relayer, ensure `VITE_CLAIM_RELAYER_URL` is reachable from the browser.';
}
if (message.includes('nonce too low')) {
return 'Transaction nonce conflict. Please try again.';
}
if (message.includes('already claimed')) {
return 'This address has already claimed its allocation.';
}
if (message.includes('InvalidMerkleProof')) {
return 'Invalid Merkle proof for this account and amount. Double-check you selected the correct Polkadot account and you are using the correct migration proofs dataset for this network.';
}
if (message.includes('invalid proof')) {
return 'Invalid proof. Please regenerate and try again.';
}
if (message.includes('not in merkle tree')) {
return 'Address not found in the merkle tree.';
}
if (message.includes('claim period ended')) {
return 'The claim period has ended.';
}
if (message.includes('paused')) {
return 'Claims are currently paused.';
}

// Extract "Details:" section if present
const detailsMatch = message.match(/Details:\s*([^V]+?)(?:Version:|$)/);
if (detailsMatch) {
return detailsMatch[1].trim();
}

// Fallback: truncate long messages
if (message.length > 100) {
return 'Transaction failed. Please try again.';
}

return message;
};

enum ClaimStep {
CONNECT_WALLETS = 0,
CHECK_ELIGIBILITY = 1,
Expand Down
12 changes: 11 additions & 1 deletion apps/tangle-dapp/src/pages/restake/ExpandTableButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum NotificationVariant {
export type ExpandTableButtonProps = ComponentProps<'button'> & {
notificationVariant?: NotificationVariant;
tooltipContent?: ReactNode;
requestCount?: number;
};

const COLOR_CLASSES = {
Expand All @@ -35,15 +36,24 @@ const COLOR_CLASSES = {
export const ExpandTableButton: FC<ExpandTableButtonProps> = ({
notificationVariant,
tooltipContent,
requestCount,
...props
}) => {
const showCount = requestCount !== undefined && requestCount > 0;

return (
<Tooltip>
<TooltipTrigger asChild>
<IconButton {...props}>
<DoubleArrowRightIcon />

{notificationVariant && (
{showCount && (
<span className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-semibold text-white bg-blue-500 rounded-full">
{requestCount > 99 ? '99+' : requestCount}
</span>
)}

{notificationVariant && !showCount && (
<span className="absolute top-0 right-0 flex w-2 h-2 -mt-0.5 -mr-0.5">
<span
className={twMerge(
Expand Down
24 changes: 14 additions & 10 deletions apps/tangle-dapp/src/pages/restake/delegate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Modal } from '@tangle-network/ui-components/components/Modal';
import type { TextFieldInputProps } from '@tangle-network/ui-components/components/TextField/types';
import { TransactionInputCard } from '@tangle-network/ui-components/components/TransactionInputCard';
import { useModal } from '@tangle-network/ui-components/hooks/useModal';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import useFormSetValue from '../../../hooks/useFormSetValue';
import { SubmitHandler, useForm } from 'react-hook-form';
import { Address, formatUnits, parseUnits } from 'viem';
Expand Down Expand Up @@ -269,10 +269,6 @@ const RestakeDelegateForm: FC = () => {
update: updateOperatorModal,
} = useModal(false);

const [selectedAssetItem, setSelectedAssetItem] = useState<AssetItem | null>(
null,
);

const depositedAssets = useMemo<AssetItem[]>(() => {
if (!restakeAssets || !depositMap) {
return [];
Expand Down Expand Up @@ -308,18 +304,27 @@ const RestakeDelegateForm: FC = () => {
.filter((item): item is AssetItem => item !== null);
}, [depositMap, restakeAssets, tokenAddresses]);

// Derive selectedAssetItem from depositedAssets to ensure it updates when metadata loads
const selectedAssetItem = useMemo(() => {
if (!selectedAssetId || depositedAssets.length === 0) {
return null;
}
return (
depositedAssets.find((asset) => asset.id === selectedAssetId) ?? null
);
}, [depositedAssets, selectedAssetId]);

// Auto-select first asset when available and none selected
useEffect(() => {
if (depositedAssets.length > 0 && !selectedAssetItem) {
if (depositedAssets.length > 0 && !selectedAssetId) {
const firstAsset = depositedAssets[0];
setValue('assetId', firstAsset.id);
setSelectedAssetItem(firstAsset);
}
}, [depositedAssets, setValue, selectedAssetItem]);
}, [depositedAssets, setValue, selectedAssetId]);

const handleAssetSelect = useCallback(
(asset: AssetItem) => {
setValue('assetId', asset.id);
setSelectedAssetItem(asset);
closeAssetModal();
},
[closeAssetModal, setValue],
Expand Down Expand Up @@ -427,7 +432,6 @@ const RestakeDelegateForm: FC = () => {
setValue('amount', '', { shouldValidate: false });
setValue('assetId', '' as Address, { shouldValidate: false });
setValue('operatorAddress', '' as Address, { shouldValidate: false });
setSelectedAssetItem(null);
}, [setValue]);

const onSubmit = useCallback<SubmitHandler<EvmDelegationFormFields>>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import formatMsDuration from '../../../utils/formatMsDuration';
const Details: FC = () => {
const { data: config } = useProtocolConfig();

const unstakePeriod = useMemo(() => {
const undelegatePeriod = useMemo(() => {
if (!config) {
return null;
}
Expand All @@ -25,7 +25,7 @@ const Details: FC = () => {
<DetailItem
title="Undelegate period"
tooltip="Waiting time between scheduling and executing an undelegation"
value={unstakePeriod}
value={undelegatePeriod}
/>
</DetailsContainer>
);
Expand Down
Loading