From bb406c437d7edab4c8a3097694ddad59cb31fa09 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 22 Dec 2025 18:52:15 +0700 Subject: [PATCH 01/31] fix(liquid-staking): QA fixes for vault display and asset lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Normalize vault asset addresses to lowercase for proper indexer matching - Add TokenIcon to vault selection in deposit/redeem pages - Improve vault display names (use underlying asset symbol) - Simplify redeem page UI with cleaner shares display - Show approximate asset value on pending redeem claims - Fix TransactionInputCard max amount handling for small values - Update local Anvil contract and token addresses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../LiquidStaking/UserPositionsTable.tsx | 3 +- .../components/LiquidStaking/VaultsTable.tsx | 3 +- .../src/components/Lists/VaultListItem.tsx | 15 ++- .../pages/liquid-staking/deposit/index.tsx | 65 +++++----- .../src/pages/liquid-staking/redeem/index.tsx | 118 +++++++++--------- libs/dapp-config/src/contracts.ts | 12 +- libs/dapp-config/src/tokenMetadata.ts | 18 +-- .../TransactionInputCard.tsx | 12 +- 8 files changed, 127 insertions(+), 119 deletions(-) diff --git a/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx b/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx index 2f3572a34e..6318fac8e8 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx @@ -202,7 +202,8 @@ const UserPositionsTable: FC = () => { const balance = balanceResult.result as bigint; if (balance <= BigInt(0)) return; - const assetInfo = assets?.get(vault.asset); + // Normalize to lowercase since indexer stores addresses in lowercase + const assetInfo = assets?.get(vault.asset.toLowerCase() as Address); const conversionResult = conversionResults?.[conversionIdx]; conversionIdx++; diff --git a/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx b/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx index 3f3e90eb08..212aab2216 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx @@ -138,7 +138,8 @@ const LiquidDelegationVaultsTable: FC = () => { if (!vaults) return null; return vaults.map((vault) => { - const assetInfo = assets?.get(vault.asset); + // Normalize to lowercase since indexer stores addresses in lowercase + const assetInfo = assets?.get(vault.asset.toLowerCase() as Address); return { ...vault, assetSymbol: assetInfo?.metadata.symbol ?? 'Unknown', diff --git a/apps/tangle-dapp/src/components/Lists/VaultListItem.tsx b/apps/tangle-dapp/src/components/Lists/VaultListItem.tsx index 13c4822e5e..ed1f89d071 100644 --- a/apps/tangle-dapp/src/components/Lists/VaultListItem.tsx +++ b/apps/tangle-dapp/src/components/Lists/VaultListItem.tsx @@ -68,10 +68,23 @@ const VaultListItem: FC = ({ ? shortenHex(vaultEvmAddress) : 'Invalid vault'; + // Use underlying asset symbol for cleaner display, fall back to parsing vault name + const displayName = useMemo(() => { + if (tvlSymbol) { + return `${tvlSymbol} Vault`; + } + // Try to extract asset from vault name (format: "Liquid Delegation WETH Op-...") + const match = vaultName.match(/Liquid Delegation (\w+)/); + if (match) { + return `${match[1]} Vault`; + } + return vaultName; + }, [tvlSymbol, vaultName]); + return ( } - leftUpperContent={`${vaultName} (${vaultSymbol})`} + leftUpperContent={displayName} leftBottomContent={ operatorExplorerUrl ? ( { if (!selectedVault || !assets) { return null; } - return assets.get(selectedVault.asset) ?? null; + // Normalize to lowercase since indexer stores addresses in lowercase + // but blockchain reads return checksummed (mixed-case) addresses + const normalizedAsset = selectedVault.asset.toLowerCase() as Address; + return assets.get(normalizedAsset) ?? null; }, [selectedVault, assets]); const spender = selectedVault?.address ?? null; @@ -323,18 +327,24 @@ const LiquidStakingDepositForm: FC = () => { {...(selectedVault ? { renderBody: () => ( -
- - {selectedVault.name} ({selectedVault.symbol}) - - - {selectedVault.selectionMode === 0 - ? 'All blueprints' - : `${selectedVault.blueprintIds.length} blueprints`} - +
+ +
+ + {vaultAsset?.metadata.symbol ?? selectedVault.symbol} Vault + + + {selectedVault.selectionMode === 0 + ? 'All blueprints' + : `${selectedVault.blueprintIds.length} blueprints`} + +
), } @@ -349,33 +359,18 @@ const LiquidStakingDepositForm: FC = () => { disabled: , }).current } - onClick={() => { - if (formattedMaxAmount !== undefined) { - setValue('amount', formattedMaxAmount.toString(), { - shouldValidate: true, - }); - } + onAmountChange={(value) => { + setValue('amount', value, { shouldValidate: true }); }} /> , - isDisabled: true, - ...(vaultAsset - ? { - renderBody: () => ( - - {vaultAsset.metadata.symbol} - - ), - } - : {}), - }).current - } + tokenSelectorProps={{ + placeholder: , + isDisabled: true, + }} /> @@ -471,7 +466,7 @@ const LiquidStakingDepositForm: FC = () => { isLoading={isLoadingVaults} getItemKey={(vault) => vault.address} renderItem={(vault) => { - const asset = assets?.get(vault.asset); + const asset = assets?.get(vault.asset.toLowerCase() as Address); const tvl = formatUnits( vault.totalAssets, asset?.metadata.decimals ?? 18, diff --git a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx index 21b506a0d2..f587339f54 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx @@ -1,8 +1,9 @@ import { calculateTypedChainId } from '@tangle-network/dapp-types/TypedChainId'; import isDefined from '@tangle-network/dapp-types/utils/isDefined'; import { LockUnlockLineIcon } from '@tangle-network/icons/LockUnlockLineIcon'; +import { TokenIcon } from '@tangle-network/icons'; import ListModal from '@tangle-network/tangle-shared-ui/components/ListModal'; -import { Card } from '@tangle-network/ui-components'; +import { Card, getRoundedAmountString } from '@tangle-network/ui-components'; import Button from '@tangle-network/ui-components/components/buttons/Button'; import { Modal } from '@tangle-network/ui-components/components/Modal'; import type { TextFieldInputProps } from '@tangle-network/ui-components/components/TextField/types'; @@ -19,7 +20,6 @@ import StyleContainer from '../../../components/restaking/StyleContainer'; import { SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS } from '../../../constants/restake'; import useActiveTypedChainId from '../../../hooks/useActiveTypedChainId'; import decimalsToStep from '../../../utils/decimalsToStep'; -import AssetPlaceholder from '../../restake/AssetPlaceholder'; import SupportedChainModal from '../../restake/SupportedChainModal'; import useSwitchChain from '../../restake/useSwitchChain'; import LiquidStakingActionTabs from '../LiquidStakingActionTabs'; @@ -149,7 +149,10 @@ const LiquidStakingRedeemForm: FC = () => { if (!selectedVault || !assets) { return null; } - return assets.get(selectedVault.asset) ?? null; + // Normalize to lowercase since indexer stores addresses in lowercase + // but blockchain reads return checksummed (mixed-case) addresses + const normalizedAsset = selectedVault.asset.toLowerCase() as Address; + return assets.get(normalizedAsset) ?? null; }, [selectedVault, assets]); const { maxAmount, formattedMaxAmount } = useMemo(() => { @@ -218,6 +221,7 @@ const LiquidStakingRedeemForm: FC = () => { return; } + // Parse the input as shares (18 decimals) const sharesBigInt = parseUnits(amount, 18); if (sharesBigInt <= BigInt(0)) { @@ -243,10 +247,7 @@ const LiquidStakingRedeemForm: FC = () => {
- + { {...(selectedVault ? { renderBody: () => ( -
- - {selectedVault.name} ({selectedVault.symbol}) - - - {selectedVault.selectionMode === 0 - ? 'All blueprints' - : `${selectedVault.blueprintIds.length} blueprints`} - +
+ +
+ + {vaultAsset?.metadata.symbol ?? selectedVault.symbol} Vault + + + {selectedVault.selectionMode === 0 + ? 'All blueprints' + : `${selectedVault.blueprintIds.length} blueprints`} + +
), } @@ -273,6 +280,7 @@ const LiquidStakingRedeemForm: FC = () => { /> { disabled: , }).current } - onClick={() => { - if (formattedMaxAmount !== undefined) { - setValue('amount', formattedMaxAmount.toString(), { - shouldValidate: true, - }); - } + onAmountChange={(value) => { + setValue('amount', value, { shouldValidate: true }); }} /> , - isDisabled: true, - ...(selectedVault && formattedMaxAmount !== undefined - ? { - renderBody: () => ( -
- - ldTokens - -
- - Available: {formattedMaxAmount} - -
-
- ), - } - : {}), - }).current - } + tokenSelectorProps={{ + children: ( + + Shares + + ), + isDisabled: true, + isDropdown: false, + }} /> @@ -326,19 +314,20 @@ const LiquidStakingRedeemForm: FC = () => { {selectedVault && position && (
- Your Balance + Your Shares - {formatUnits(position.balance, 18)} shares + {formatUnits(position.balance, 18)}
- Value in Assets + + ≈ Value in {vaultAsset?.metadata.symbol ?? 'Assets'} + {formatUnits( position.balanceInAssets, vaultAsset?.metadata.decimals ?? 18, - )}{' '} - {vaultAsset?.metadata.symbol ?? 'tokens'} + )}
@@ -390,7 +379,7 @@ const LiquidStakingRedeemForm: FC = () => { {selectedVault && ( - +
Pending Redeem Requests @@ -410,6 +399,13 @@ const LiquidStakingRedeemForm: FC = () => { const claimableShares = claimableByRequestId.get(req.id) ?? BigInt(0); const isReadyToClaim = claimableShares > BigInt(0); + + // Calculate approximate asset value for display + const approxAssetValue = + position && position.balance > BigInt(0) + ? (req.shares * position.balanceInAssets) / position.balance + : BigInt(0); + return (
{ >
- {formatUnits(req.shares, 18)} shares + {getRoundedAmountString( + Number(formatUnits(req.shares, 18)), + 5, + )}{' '} + shares {isReadyToClaim - ? 'Ready to claim' + ? `Ready to claim ≈ ${getRoundedAmountString(Number(formatUnits(approxAssetValue, vaultAsset?.metadata.decimals ?? 18)), 5)} ${vaultAsset?.metadata.symbol ?? ''}` : 'Waiting for undelegate delay'}
@@ -475,7 +475,7 @@ const LiquidStakingRedeemForm: FC = () => { isLoading={isLoadingVaults} getItemKey={(vault) => vault.address} renderItem={(vault) => { - const asset = assets?.get(vault.asset); + const asset = assets?.get(vault.asset.toLowerCase() as Address); const tvl = formatUnits( vault.totalAssets, asset?.metadata.decimals ?? 18, diff --git a/libs/dapp-config/src/contracts.ts b/libs/dapp-config/src/contracts.ts index 45dc542a01..911c38f1bd 100644 --- a/libs/dapp-config/src/contracts.ts +++ b/libs/dapp-config/src/contracts.ts @@ -32,16 +32,16 @@ export const SP1_VERIFIER_GATEWAY = { baseMainnet: '0x397A5f7f3dBd538f23DE225B51f532c34448dA9B' as Address, }; -// Local Anvil deployment addresses (from tnt-core/indexer/config.yaml) +// Local Anvil deployment addresses (from LocalTestnet.s.sol deployment) export const LOCAL_CONTRACTS: ContractAddresses = { tangle: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9', multiAssetDelegation: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', masterBlueprintServiceManager: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853', - operatorStatusRegistry: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9', - rewardVaults: '0xc5a5C42992dECbae36851359345FE25997F5C42d', - inflationPool: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E', - credits: '0x0000000000000000000000651234512121212666', - liquidDelegationFactory: '0xCA8c8688914e0F7096c920146cd0Ad85cD7Ae8b9', + operatorStatusRegistry: '0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf', + rewardVaults: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823', + inflationPool: '0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43', + credits: '0x1fA02b2d6A771842690194Cf62D91bdd92BfE28d', + liquidDelegationFactory: '0x8F4ec854Dd12F1fe79500a1f53D0cbB30f9b6134', }; // Base Sepolia testnet addresses (local development) diff --git a/libs/dapp-config/src/tokenMetadata.ts b/libs/dapp-config/src/tokenMetadata.ts index 8016d23b3a..cbc806b87f 100644 --- a/libs/dapp-config/src/tokenMetadata.ts +++ b/libs/dapp-config/src/tokenMetadata.ts @@ -28,17 +28,17 @@ const KNOWN_TOKENS: Record = { }; // Known token addresses -> symbol mapping (from local Anvil deployment) -// These addresses are from the current tnt-core LocalTestnet deployment +// These are deterministic when Anvil starts fresh (nonce 0) and broadcast cache is cleared +// Addresses from tnt-core LocalTestnet.s.sol deployment (chain ID 31337) const KNOWN_TOKEN_ADDRESSES: Record = { - // TNT (bond asset) from tnt-core LocalTestnetSetup (Anvil 31337) '0x0165878a594ca255338adfa4d48449f69242eb8f': 'TNT', - '0x4a679253410272dd5232b3ff7cf5dbb88f295319': 'USDC', - '0x7a2088a1bfc9d81c55368ae168c2c02570cb814f': 'USDT', - '0x09635f643e140090a9a8dcd712ed6285858cebef': 'DAI', - '0x67d269191c92caf3cd7723f116c85e6e9bf55933': 'stETH', - '0xe6e340d132b5f46d1e472debcd681b2abc16e57e': 'wstETH', - '0xc3e53f4d16ae77db1c982e75a937b9f60fe63690': 'EIGEN', - '0xc5a5c42992decbae36851359345fe25997f5c42d': 'WETH', + '0x809d550fca64d94bd9f66e60752a544199cfac3d': 'USDC', + '0x4c5859f0f772848b2d91f1d83e2fe57935348029': 'USDT', + '0x1291be112d480055dafd8a610b7d1e203891c274': 'DAI', + '0x5f3f1dbd7b74c6b46e8c44f98792a1daf8d69154': 'WETH', + '0xb7278a61aa25c888815afc32ad3cc52ff24fe575': 'stETH', + '0xcd8a1c3ba11cf5ecfa6267617243239504a98d90': 'wstETH', + '0x82e01223d51eb87e16a03e24687edf0f294da6f1': 'EIGEN', }; // Runtime cache: address -> metadata (populated from on-chain fetches) diff --git a/libs/ui-components/src/components/TransactionInputCard/TransactionInputCard.tsx b/libs/ui-components/src/components/TransactionInputCard/TransactionInputCard.tsx index 8864eaf61e..41b20bdf34 100644 --- a/libs/ui-components/src/components/TransactionInputCard/TransactionInputCard.tsx +++ b/libs/ui-components/src/components/TransactionInputCard/TransactionInputCard.tsx @@ -22,7 +22,7 @@ import { } from 'react'; import { twMerge } from 'tailwind-merge'; import { Typography } from '../../typography'; -import { getRoundedAmountString, toFixed } from '../../utils'; +import { getRoundedAmountString, numberToString } from '../../utils'; import { AdjustAmount } from '../BridgeInputs'; import { Switcher } from '../Switcher'; import { Tooltip, TooltipTrigger, TooltipBody } from '../Tooltip'; @@ -214,14 +214,12 @@ const TransactionMaxAmountButton = forwardRef< const onAmountChange = onAmountChangeProp ?? context.onAmountChange; const buttonCnt = useMemo(() => { - const amount = + // Pass raw amount to getRoundedAmountString which handles small amounts with "< 0.00001" format + const formattedAmount = typeof maxAmount === 'number' - ? toFixed(maxAmount, 5) + ? getRoundedAmountString(maxAmount, 5) : EMPTY_VALUE_PLACEHOLDER; - const formattedAmount = - typeof amount === 'number' ? getRoundedAmountString(amount, 5) : amount; - const tokenSym = tokenSymbol ?? ''; return `${formattedAmount} ${tokenSym}`.trim(); @@ -252,7 +250,7 @@ const TransactionMaxAmountButton = forwardRef< disabled={disabled} onClick={ typeof maxAmount === 'number' - ? () => onAmountChange?.(`${toFixed(maxAmount, 5)}`) + ? () => onAmountChange?.(numberToString(maxAmount)) : undefined } Icon={ From 944bde0ce75c9b9e02caa34e60c47217f8d35444 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 22 Dec 2025 21:09:23 +0700 Subject: [PATCH 02/31] fix(liquid-staking): improve UI consistency and formatting in liquid staking pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use getRoundedAmountString for consistent amount formatting in tables - Add proper icons and avatars for selected operator/asset in create vault - Use OperatorListItem and AssetListItem components for better list rendering - Show approximate asset value inline with shares in redeem page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../LiquidStaking/UserPositionsTable.tsx | 9 +- .../components/LiquidStaking/VaultsTable.tsx | 9 +- .../liquid-staking/create-vault/index.tsx | 113 +++++++++++------- .../src/pages/liquid-staking/redeem/index.tsx | 11 +- 4 files changed, 86 insertions(+), 56 deletions(-) diff --git a/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx b/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx index 6318fac8e8..99158b4963 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx @@ -14,6 +14,7 @@ import { Table } from '@tangle-network/ui-components/components/Table'; import { TableVariant } from '@tangle-network/ui-components/components/Table/types'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import pluralize from '@tangle-network/ui-components/utils/pluralize'; +import { getRoundedAmountString } from '@tangle-network/ui-components'; import { createColumnHelper, getCoreRowModel, @@ -69,9 +70,7 @@ const COLUMNS = [ return ( - {Number(formatted).toLocaleString(undefined, { - maximumFractionDigits: 6, - })} + {getRoundedAmountString(Number(formatted), 5)} ); @@ -97,9 +96,7 @@ const COLUMNS = [ return ( - {Number(formatted).toLocaleString(undefined, { - maximumFractionDigits: 6, - })}{' '} + {getRoundedAmountString(Number(formatted), 5)}{' '} {props.row.original.assetSymbol} diff --git a/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx b/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx index 212aab2216..8a56663e40 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/VaultsTable.tsx @@ -15,6 +15,7 @@ import { Table } from '@tangle-network/ui-components/components/Table'; import { TableVariant } from '@tangle-network/ui-components/components/Table/types'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import pluralize from '@tangle-network/ui-components/utils/pluralize'; +import { getRoundedAmountString } from '@tangle-network/ui-components'; import { createColumnHelper, getCoreRowModel, @@ -76,9 +77,7 @@ const COLUMNS = [ return ( - {Number(formatted).toLocaleString(undefined, { - maximumFractionDigits: 4, - })}{' '} + {getRoundedAmountString(Number(formatted), 5)}{' '} {props.row.original.assetSymbol} @@ -102,9 +101,7 @@ const COLUMNS = [ return ( - {Number(formatted).toLocaleString(undefined, { - maximumFractionDigits: 4, - })} + {getRoundedAmountString(Number(formatted), 5)} ); diff --git a/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx index bd40dc926b..ac61818033 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx @@ -1,7 +1,8 @@ import { calculateTypedChainId } from '@tangle-network/dapp-types/TypedChainId'; import isDefined from '@tangle-network/dapp-types/utils/isDefined'; import ListModal from '@tangle-network/tangle-shared-ui/components/ListModal'; -import { Card, Input } from '@tangle-network/ui-components'; +import { TokenIcon } from '@tangle-network/icons'; +import { Avatar, Card, Input, shortenHex } from '@tangle-network/ui-components'; import Button from '@tangle-network/ui-components/components/buttons/Button'; import { Modal } from '@tangle-network/ui-components/components/Modal'; import { ModalContent } from '@tangle-network/ui-components/components/Modal/ModalContent'; @@ -31,9 +32,11 @@ import { } from '@tangle-network/tangle-shared-ui/data/graphql'; import { TxStatus } from '@tangle-network/tangle-shared-ui/hooks/useContractWrite'; import filterBy from '@tangle-network/tangle-shared-ui/utils/filterBy'; -import { formatUnits } from 'viem'; import { Switcher } from '@tangle-network/ui-components/components/Switcher'; import { CheckBox } from '@tangle-network/ui-components/components/CheckBox'; +import { BN } from '@polkadot/util'; +import OperatorListItem from '../../../components/Lists/OperatorListItem'; +import AssetListItem from '../../../components/Lists/AssetListItem'; type CreateVaultFormFields = { operator: Address; @@ -156,6 +159,11 @@ const CreateVaultForm: FC = () => { return assets.get(selectedAsset) ?? null; }, [selectedAsset, assets]); + const selectedOperatorData = useMemo(() => { + if (!selectedOperator || !operators.length) return null; + return operators.find((op) => op.address === selectedOperator) ?? null; + }, [selectedOperator, operators]); + const blueprintItems = useMemo(() => { return blueprints ?? []; }, [blueprints]); @@ -298,14 +306,31 @@ const CreateVaultForm: FC = () => { {...(selectedOperator ? { renderBody: () => ( -
- - {selectedOperator.slice(0, 8)}... - {selectedOperator.slice(-6)} - - - Operator - +
+ +
+ + {shortenHex(selectedOperator)} + + + {selectedOperatorData + ? `${selectedOperatorData.delegationCount} total delegations` + : 'Operator'} + +
), } @@ -322,13 +347,28 @@ const CreateVaultForm: FC = () => { {...(selectedAssetData ? { renderBody: () => ( -
- - {selectedAssetData.metadata.symbol} - - - {selectedAssetData.metadata.name} - +
+ +
+ + {selectedAssetData.metadata.symbol} + + + {selectedAssetData.metadata.name} + +
), } @@ -433,17 +473,12 @@ const CreateVaultForm: FC = () => { descriptionWhenEmpty="No active operators found on this network." items={operators} isLoading={isLoadingOperators} + getItemKey={(item) => item.address} renderItem={(operator) => ( -
-
- - {operator.address.slice(0, 10)}...{operator.address.slice(-8)} - - - {operator.delegationCount} delegations - -
-
+ )} /> @@ -465,23 +500,15 @@ const CreateVaultForm: FC = () => { descriptionWhenEmpty="No restaking assets found on this network." items={assetList} isLoading={isLoadingAssets} + getItemKey={(item) => item.id} renderItem={(asset) => ( -
-
- {asset.metadata.symbol} - - {asset.metadata.name} - -
-
- - {asset.balance !== null - ? formatUnits(asset.balance, asset.metadata.decimals) - : '0'} - - Balance -
-
+ )} /> diff --git a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx index f587339f54..fdcad81732 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx @@ -418,13 +418,22 @@ const LiquidStakingRedeemForm: FC = () => { 5, )}{' '} shares + {approxAssetValue > BigInt(0) && ( + + {' '} + ({getRoundedAmountString( + Number(formatUnits(approxAssetValue, vaultAsset?.metadata.decimals ?? 18)), + 5, + )} {vaultAsset?.metadata.symbol ?? ''}) + + )} {isReadyToClaim - ? `Ready to claim ≈ ${getRoundedAmountString(Number(formatUnits(approxAssetValue, vaultAsset?.metadata.decimals ?? 18)), 5)} ${vaultAsset?.metadata.symbol ?? ''}` + ? 'Ready to claim' : 'Waiting for undelegate delay'}
From c76443c295f406f4557ffcf50384d96c45b6cb49 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 22 Dec 2025 21:27:26 +0700 Subject: [PATCH 03/31] fix(liquid-staking): pre-select vault from URL query parameter in deposit/redeem forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When clicking Deposit/Redeem buttons in Vaults/Positions tables, the vault is now automatically pre-selected in the corresponding form based on the ?vault= query parameter. Also unified the Redeem button style to match the Deposit button. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../LiquidStaking/UserPositionsTable.tsx | 4 +--- .../pages/liquid-staking/deposit/index.tsx | 21 ++++++++++++++++++- .../src/pages/liquid-staking/redeem/index.tsx | 21 ++++++++++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx b/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx index 99158b4963..9045778906 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/UserPositionsTable.tsx @@ -118,9 +118,7 @@ const COLUMNS = [ to={`${PagePath.LIQUID_STAKING_REDEEM}?vault=${row.original.vaultAddress}`} onClick={(e) => e.stopPropagation()} > - +
diff --git a/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx index 8049e76457..efcb823ee8 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx @@ -13,7 +13,8 @@ import { useModal } from '@tangle-network/ui-components/hooks/useModal'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { erc20Abi, parseUnits, formatUnits, Address } from 'viem'; +import { useSearchParams } from 'react-router'; +import { erc20Abi, parseUnits, formatUnits, Address, isAddress } from 'viem'; import { useAccount, useChainId } from 'wagmi'; import ErrorMessage from '../../../components/ErrorMessage'; import ActionButtonBase from '../../../components/restaking/ActionButtonBase'; @@ -43,6 +44,7 @@ type DepositFormFields = { }; const LiquidStakingDepositForm: FC = () => { + const [searchParams] = useSearchParams(); const { address: userAddress } = useAccount(); const chainId = useChainId(); const _activeChain = useMemo(() => { @@ -90,6 +92,23 @@ const LiquidStakingDepositForm: FC = () => { reset(); }, [activeTypedChainId, reset]); + // Pre-select vault from URL query parameter + useEffect(() => { + const vaultParam = searchParams.get('vault'); + + if (!vaultParam || !vaults || !isAddress(vaultParam)) { + return; + } + + const matchingVault = vaults.find( + (v) => v.address.toLowerCase() === vaultParam.toLowerCase(), + ); + + if (matchingVault) { + setValue('vaultAddress', matchingVault.address); + } + }, [searchParams, vaults, setValue]); + const { status: vaultModalOpen, close: closeVaultModal, diff --git a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx index fdcad81732..a568d6a13e 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx @@ -12,7 +12,8 @@ import { useModal } from '@tangle-network/ui-components/hooks/useModal'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { FC, useCallback, useEffect, useMemo, useRef } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { parseUnits, formatUnits, Address } from 'viem'; +import { useSearchParams } from 'react-router'; +import { parseUnits, formatUnits, Address, isAddress } from 'viem'; import { useAccount, useReadContracts } from 'wagmi'; import ErrorMessage from '../../../components/ErrorMessage'; import ActionButtonBase from '../../../components/restaking/ActionButtonBase'; @@ -45,6 +46,7 @@ type RedeemFormFields = { }; const LiquidStakingRedeemForm: FC = () => { + const [searchParams] = useSearchParams(); const { address: userAddress } = useAccount(); const activeTypedChainId = useActiveTypedChainId(); const switchChain = useSwitchChain(); @@ -85,6 +87,23 @@ const LiquidStakingRedeemForm: FC = () => { reset(); }, [activeTypedChainId, reset]); + // Pre-select vault from URL query parameter + useEffect(() => { + const vaultParam = searchParams.get('vault'); + + if (!vaultParam || !vaults || !isAddress(vaultParam)) { + return; + } + + const matchingVault = vaults.find( + (v) => v.address.toLowerCase() === vaultParam.toLowerCase(), + ); + + if (matchingVault) { + setValue('vaultAddress', matchingVault.address); + } + }, [searchParams, vaults, setValue]); + const { status: vaultModalOpen, close: closeVaultModal, From 6e911d5199b22b2c1dfa5b3a118ceb1f8a63df90 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 22 Dec 2025 21:34:45 +0700 Subject: [PATCH 04/31] chore: format code --- apps/claim-relayer/src/index.ts | 36 +- .../components/ClaimCreditsButton.tsx | 5 +- .../liquid-staking/create-vault/index.tsx | 9 +- .../pages/liquid-staking/deposit/index.tsx | 9 +- .../src/pages/liquid-staking/redeem/index.tsx | 22 +- .../tangle-shared-ui/src/abi/creditsMerkle.ts | 35 +- .../src/abi/multiAssetDelegation.ts | 5038 ++++++++--------- libs/tangle-shared-ui/src/abi/tangle.ts | 456 +- .../src/data/credits/merkleTree.ts | 6 +- .../src/data/graphql/useOperatorManagement.ts | 4 +- .../src/data/graphql/useSlashing.ts | 3 +- .../src/data/migration/merkleTree.ts | 15 +- 12 files changed, 2846 insertions(+), 2792 deletions(-) diff --git a/apps/claim-relayer/src/index.ts b/apps/claim-relayer/src/index.ts index 0a48cb0e2b..5e244b86b7 100644 --- a/apps/claim-relayer/src/index.ts +++ b/apps/claim-relayer/src/index.ts @@ -367,24 +367,24 @@ const start = async () => { console.log( '═══════════════════════════════════════════════════════════════', ); - console.log(' TNT Migration Claim Relayer'); - console.log( - '═══════════════════════════════════════════════════════════════', - ); - console.log(` Port: ${PORT}`); - console.log(` Chain: ${chain.name || 'Custom'} (${CHAIN_ID})`); - console.log(` RPC: ${RPC_URL}`); - console.log(` Contract: ${MIGRATION_CONTRACT}`); - console.log(` Relayer: ${account.address}`); - console.log( - '═══════════════════════════════════════════════════════════════', - ); - console.log(''); - console.log('Endpoints:'); - console.log(` GET /health - Health check & relayer balance`); - console.log(` POST /claim - Submit a gasless claim`); - console.log(` GET /status/:pubkey - Check claim status`); - console.log(''); + console.log(' TNT Migration Claim Relayer'); + console.log( + '═══════════════════════════════════════════════════════════════', + ); + console.log(` Port: ${PORT}`); + console.log(` Chain: ${chain.name || 'Custom'} (${CHAIN_ID})`); + console.log(` RPC: ${RPC_URL}`); + console.log(` Contract: ${MIGRATION_CONTRACT}`); + console.log(` Relayer: ${account.address}`); + console.log( + '═══════════════════════════════════════════════════════════════', + ); + console.log(''); + console.log('Endpoints:'); + console.log(` GET /health - Health check & relayer balance`); + console.log(` POST /claim - Submit a gasless claim`); + console.log(` GET /status/:pubkey - Check claim status`); + console.log(''); }); }; diff --git a/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx b/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx index bf91e75f9f..080f7dcd6e 100644 --- a/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx +++ b/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx @@ -239,10 +239,7 @@ const CreditsButton = ({ - + + +
), enableSorting: false, diff --git a/apps/tangle-dapp/src/components/tables/RestakingAssetsTable.tsx b/apps/tangle-dapp/src/components/tables/RestakingAssetsTable.tsx index 969f9c409e..c20f42f2eb 100644 --- a/apps/tangle-dapp/src/components/tables/RestakingAssetsTable.tsx +++ b/apps/tangle-dapp/src/components/tables/RestakingAssetsTable.tsx @@ -2,6 +2,7 @@ import { FC, useMemo } from 'react'; import { BN } from '@polkadot/util'; import { TokenIcon } from '@tangle-network/icons'; import Spinner from '@tangle-network/icons/Spinner'; +import { useChainId } from 'wagmi'; import HeaderCell from '@tangle-network/tangle-shared-ui/components/tables/HeaderCell'; import TableCellWrapper from '@tangle-network/tangle-shared-ui/components/tables/TableCellWrapper'; import TableStatus from '@tangle-network/tangle-shared-ui/components/tables/TableStatus'; @@ -59,13 +60,17 @@ const TABLE_ACTION_BUTTON_CLASS = const isFallbackSymbol = (symbol: string) => symbol.startsWith('0x') || symbol.includes('...'); -const resolveTokenIconSymbol = (symbol: string, address: Address) => { - const cached = getCachedTokenMetadata(address); +const resolveTokenIconSymbol = ( + chainId: number, + symbol: string, + address: Address, +) => { + const cached = getCachedTokenMetadata(chainId, address); const candidate = cached?.symbol ?? symbol; return isFallbackSymbol(candidate) ? null : candidate; }; -const getColumns = () => [ +const getColumns = (chainId: number) => [ COLUMN_HELPER.accessor('id', { header: () => , cell: (props) => ( @@ -74,6 +79,7 @@ const getColumns = () => [
{(() => { const iconSymbol = resolveTokenIconSymbol( + chainId, props.row.original.symbol, props.row.original.tokenAddress, ); @@ -202,6 +208,8 @@ export const RestakingAssetsTable: FC = ({ delegator, isLoading, }) => { + const chainId = useChainId(); + const protocolAssetMap = useMemo(() => { const map = new Map(); restakingAssets?.forEach((asset) => { @@ -249,14 +257,14 @@ export const RestakingAssetsTable: FC = ({ () => ({ data: tableData, - columns: getColumns(), + columns: getColumns(chainId), getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), autoResetPageIndex: false, enableSortingRemoval: false, }) satisfies TableOptions, - [tableData], + [tableData, chainId], ), ); diff --git a/libs/tangle-shared-ui/src/hooks/useViemWalletClient.ts b/libs/tangle-shared-ui/src/hooks/useViemWalletClient.ts index 73d3e3a5e6..8262f8fd67 100644 --- a/libs/tangle-shared-ui/src/hooks/useViemWalletClient.ts +++ b/libs/tangle-shared-ui/src/hooks/useViemWalletClient.ts @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { createWalletClient, custom, + EIP1193Provider, http, Transport, WalletClient, From 3801b4092d6ad33f5815e1e2951cee517e8d0c13 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 23 Dec 2025 20:05:19 +0700 Subject: [PATCH 16/31] chore: format code --- .../src/components/TxHistoryDrawer.tsx | 9 ++- .../src/pages/liquid-staking/redeem/index.tsx | 60 ++++++++++--------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx b/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx index 96eee2cd39..21d066aa92 100644 --- a/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx +++ b/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx @@ -180,9 +180,14 @@ type DetailRowProps = { tokenMetadata?: TokenMetadata | null; }; -// Check if a string is a pure numeric value (all digits) +// Check if a string is a valid numeric value that can be parsed as BigInt const isNumericString = (value: string): boolean => { - return /^\d+$/.test(value); + try { + BigInt(value); + return true; + } catch { + return false; + } }; const DetailRow: FC = ({ diff --git a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx index f12bd2a957..19f531818a 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx @@ -46,6 +46,9 @@ type RedeemFormFields = { amount: string; }; +// ERC-4626/ERC-7540 vault shares always use 18 decimals regardless of the underlying asset +const VAULT_SHARES_DECIMALS = 18; + const LiquidStakingRedeemForm: FC = () => { const [searchParams] = useSearchParams(); const { address: userAddress } = useAccount(); @@ -141,33 +144,30 @@ const LiquidStakingRedeemForm: FC = () => { LiquidRedeemRequest[] >([]); - // Merge optimistic requests with indexer data, removing optimistic ones that have been indexed + // Clean up optimistic requests when they appear in the indexer data + useEffect(() => { + setOptimisticRequests((prev) => + prev.filter( + (opt) => + !indexerRedeemRequests.some( + (req) => + req.vaultAddress.toLowerCase() === + opt.vaultAddress.toLowerCase() && + req.shares === opt.shares && + req.controller.toLowerCase() === opt.controller.toLowerCase(), + ), + ), + ); + }, [indexerRedeemRequests]); + + // Merge optimistic requests with indexer data const redeemRequests = useMemo(() => { if (optimisticRequests.length === 0) { return indexerRedeemRequests; } - // Filter out optimistic requests that are now in the indexer data - // We match by shares amount and vault address since we don't have the real requestId yet - const pendingOptimistic = optimisticRequests.filter((opt) => { - // Check if this optimistic request has been indexed - const isIndexed = indexerRedeemRequests.some( - (req) => - req.vaultAddress.toLowerCase() === opt.vaultAddress.toLowerCase() && - req.shares === opt.shares && - req.controller.toLowerCase() === opt.controller.toLowerCase(), - ); - return !isIndexed; - }); - - // Update state if some optimistic requests have been indexed - if (pendingOptimistic.length !== optimisticRequests.length) { - // Use setTimeout to avoid state update during render - setTimeout(() => setOptimisticRequests(pendingOptimistic), 0); - } - // Prepend remaining optimistic requests to the list - return [...pendingOptimistic, ...indexerRedeemRequests]; + return [...optimisticRequests, ...indexerRedeemRequests]; }, [optimisticRequests, indexerRedeemRequests]); // Clear optimistic requests when vault changes @@ -224,7 +224,9 @@ const LiquidStakingRedeemForm: FC = () => { // position.balance already reflects the user's current share balance // after any pending redeem requests (shares are burned immediately // when requestRedeem is called in ERC-7540 vaults) - const formatted = Number(formatUnits(position.balance, 18)); + const formatted = Number( + formatUnits(position.balance, VAULT_SHARES_DECIMALS), + ); return { maxAmount: position.balance, @@ -233,7 +235,7 @@ const LiquidStakingRedeemForm: FC = () => { }, [position]); const customAmountProps = useMemo(() => { - const step = decimalsToStep(18); + const step = decimalsToStep(VAULT_SHARES_DECIMALS); return { type: 'number', @@ -242,7 +244,7 @@ const LiquidStakingRedeemForm: FC = () => { required: 'Amount is required', validate: (value) => { if (!position) return 'Select a vault first'; - const parsed = parseUnits(value, 18); + const parsed = parseUnits(value, VAULT_SHARES_DECIMALS); if (parsed <= BigInt(0)) return 'Amount must be greater than 0'; if (maxAmount && parsed > maxAmount) return 'Insufficient balance'; return true; @@ -285,8 +287,8 @@ const LiquidStakingRedeemForm: FC = () => { return; } - // Parse the input as shares (18 decimals) - const sharesBigInt = parseUnits(amount, 18); + // Parse the input as shares + const sharesBigInt = parseUnits(amount, VAULT_SHARES_DECIMALS); if (sharesBigInt <= BigInt(0)) { return; @@ -421,7 +423,7 @@ const LiquidStakingRedeemForm: FC = () => {
Your Shares - {formatUnits(position.balance, 18)} + {formatUnits(position.balance, VAULT_SHARES_DECIMALS)}
@@ -502,7 +504,9 @@ const LiquidStakingRedeemForm: FC = () => {
{getRoundedAmountString( - Number(formatUnits(req.shares, 18)), + Number( + formatUnits(req.shares, VAULT_SHARES_DECIMALS), + ), 5, )}{' '} shares From 5a4662a7921de8ae1b88d3642b40c99e94c1aa54 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 23 Dec 2025 20:21:27 +0700 Subject: [PATCH 17/31] chore: update yarn.lock --- yarn.lock | 546 ++---------------------------------------------------- 1 file changed, 12 insertions(+), 534 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3cba5e6e56..734539e389 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3918,13 +3918,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/aix-ppc64@npm:0.27.1" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-arm64@npm:0.17.19" @@ -3946,13 +3939,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/android-arm64@npm:0.27.1" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-arm@npm:0.17.19" @@ -3974,13 +3960,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/android-arm@npm:0.27.1" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-x64@npm:0.17.19" @@ -4002,13 +3981,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/android-x64@npm:0.27.1" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/darwin-arm64@npm:0.17.19" @@ -4030,13 +4002,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/darwin-arm64@npm:0.27.1" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/darwin-x64@npm:0.17.19" @@ -4058,13 +4023,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/darwin-x64@npm:0.27.1" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/freebsd-arm64@npm:0.17.19" @@ -4086,13 +4044,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/freebsd-arm64@npm:0.27.1" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/freebsd-x64@npm:0.17.19" @@ -4114,13 +4065,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/freebsd-x64@npm:0.27.1" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-arm64@npm:0.17.19" @@ -4142,13 +4086,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-arm64@npm:0.27.1" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-arm@npm:0.17.19" @@ -4170,13 +4107,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-arm@npm:0.27.1" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-ia32@npm:0.17.19" @@ -4198,13 +4128,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-ia32@npm:0.27.1" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-loong64@npm:0.17.19" @@ -4226,13 +4149,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-loong64@npm:0.27.1" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-mips64el@npm:0.17.19" @@ -4254,13 +4170,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-mips64el@npm:0.27.1" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-ppc64@npm:0.17.19" @@ -4282,13 +4191,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-ppc64@npm:0.27.1" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-riscv64@npm:0.17.19" @@ -4310,13 +4212,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-riscv64@npm:0.27.1" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-s390x@npm:0.17.19" @@ -4338,13 +4233,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-s390x@npm:0.27.1" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-x64@npm:0.17.19" @@ -4366,13 +4254,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-x64@npm:0.27.1" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/netbsd-arm64@npm:0.24.2" @@ -4387,13 +4268,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/netbsd-arm64@npm:0.27.1" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/netbsd-x64@npm:0.17.19" @@ -4415,13 +4289,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/netbsd-x64@npm:0.27.1" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/openbsd-arm64@npm:0.24.2" @@ -4436,13 +4303,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/openbsd-arm64@npm:0.27.1" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/openbsd-x64@npm:0.17.19" @@ -4464,20 +4324,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/openbsd-x64@npm:0.27.1" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openharmony-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/openharmony-arm64@npm:0.27.1" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/sunos-x64@npm:0.17.19" @@ -4499,13 +4345,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/sunos-x64@npm:0.27.1" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-arm64@npm:0.17.19" @@ -4527,13 +4366,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/win32-arm64@npm:0.27.1" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-ia32@npm:0.17.19" @@ -4555,13 +4387,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/win32-ia32@npm:0.27.1" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-x64@npm:0.17.19" @@ -4583,13 +4408,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/win32-x64@npm:0.27.1" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.5.1 resolution: "@eslint-community/eslint-utils@npm:4.5.1" @@ -16753,22 +16571,6 @@ __metadata: languageName: unknown linkType: soft -"@tangle-network/claim-relayer@workspace:apps/claim-relayer": - version: 0.0.0-use.local - resolution: "@tangle-network/claim-relayer@workspace:apps/claim-relayer" - dependencies: - "@types/cors": "npm:^2.8.17" - "@types/express": "npm:^4.17.21" - "@types/node": "npm:^20.10.0" - cors: "npm:^2.8.5" - dotenv: "npm:^16.3.1" - express: "npm:^4.18.2" - tsx: "npm:^4.6.2" - typescript: "npm:^5.3.2" - viem: "npm:^2.21.0" - languageName: unknown - linkType: soft - "@tangle-network/dapp-config@workspace:libs/dapp-config": version: 0.0.0-use.local resolution: "@tangle-network/dapp-config@workspace:libs/dapp-config" @@ -17218,15 +17020,6 @@ __metadata: languageName: node linkType: hard -"@types/cors@npm:^2.8.17": - version: 2.8.19 - resolution: "@types/cors@npm:2.8.19" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/b5dd407040db7d8aa1bd36e79e5f3f32292f6b075abc287529e9f48df1a25fda3e3799ba30b4656667ffb931d3b75690c1d6ca71e39f7337ea6dfda8581916d0 - languageName: node - linkType: hard - "@types/d3-array@npm:^3.0.3": version: 3.2.1 resolution: "@types/d3-array@npm:3.2.1" @@ -17386,18 +17179,6 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:^4.17.21": - version: 4.17.25 - resolution: "@types/express@npm:4.17.25" - dependencies: - "@types/body-parser": "npm:*" - "@types/express-serve-static-core": "npm:^4.17.33" - "@types/qs": "npm:*" - "@types/serve-static": "npm:^1" - checksum: 10c0/f42b616d2c9dbc50352c820db7de182f64ebbfa8dba6fb6c98e5f8f0e2ef3edde0131719d9dc6874803d25ad9ca2d53471d0fec2fbc60a6003a43d015bab72c4 - languageName: node - linkType: hard - "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -17601,15 +17382,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.10.0": - version: 20.19.26 - resolution: "@types/node@npm:20.19.26" - dependencies: - undici-types: "npm:~6.21.0" - checksum: 10c0/68e7d92dd2b7bddff9dffabb9c740e655906ceac428dcf070915cdcae720579e4d72261c55ed7eccbfa907a75cbb1ff3a9148ea49878a07a72d5dd6c9e06d9d7 - languageName: node - linkType: hard - "@types/node@npm:^20.17.9": version: 20.17.30 resolution: "@types/node@npm:20.17.30" @@ -17781,16 +17553,6 @@ __metadata: languageName: node linkType: hard -"@types/send@npm:<1": - version: 0.17.6 - resolution: "@types/send@npm:0.17.6" - dependencies: - "@types/mime": "npm:^1" - "@types/node": "npm:*" - checksum: 10c0/a9d76797f0637738062f1b974e0fcf3d396a28c5dc18c3f95ecec5dabda82e223afbc2d56a0bca46b6326fd7bb229979916cea40de2270a98128fd94441b87c2 - languageName: node - linkType: hard - "@types/serve-index@npm:^1.9.1": version: 1.9.4 resolution: "@types/serve-index@npm:1.9.4" @@ -17811,17 +17573,6 @@ __metadata: languageName: node linkType: hard -"@types/serve-static@npm:^1": - version: 1.15.10 - resolution: "@types/serve-static@npm:1.15.10" - dependencies: - "@types/http-errors": "npm:*" - "@types/node": "npm:*" - "@types/send": "npm:<1" - checksum: 10c0/842fca14c9e80468f89b6cea361773f2dcd685d4616a9f59013b55e1e83f536e4c93d6d8e3ba5072d40c4e7e64085210edd6646b15d538ded94512940a23021f - languageName: node - linkType: hard - "@types/sockjs@npm:^0.3.33": version: 0.3.36 resolution: "@types/sockjs@npm:0.3.36" @@ -21286,26 +21037,6 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:~1.20.3": - version: 1.20.4 - resolution: "body-parser@npm:1.20.4" - dependencies: - bytes: "npm:~3.1.2" - content-type: "npm:~1.0.5" - debug: "npm:2.6.9" - depd: "npm:2.0.0" - destroy: "npm:~1.2.0" - http-errors: "npm:~2.0.1" - iconv-lite: "npm:~0.4.24" - on-finished: "npm:~2.4.1" - qs: "npm:~6.14.0" - raw-body: "npm:~2.5.3" - type-is: "npm:~1.6.18" - unpipe: "npm:~1.0.0" - checksum: 10c0/569c1e896297d1fcd8f34026c8d0ab70b90d45343c15c5d8dff5de2bad08125fc1e2f8c2f3f4c1ac6c0caaad115218202594d37dcb8d89d9b5dcae1c2b736aa9 - languageName: node - linkType: hard - "bonjour-service@npm:^1.0.11": version: 1.3.0 resolution: "bonjour-service@npm:1.3.0" @@ -21680,7 +21411,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2, bytes@npm:~3.1.2": +"bytes@npm:3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e @@ -22726,7 +22457,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.4, content-disposition@npm:~0.5.2, content-disposition@npm:~0.5.4": +"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.4, content-disposition@npm:~0.5.2": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -22813,13 +22544,6 @@ __metadata: languageName: node linkType: hard -"cookie-signature@npm:~1.0.6": - version: 1.0.7 - resolution: "cookie-signature@npm:1.0.7" - checksum: 10c0/e7731ad2995ae2efeed6435ec1e22cdd21afef29d300c27281438b1eab2bae04ef0d1a203928c0afec2cee72aa36540b8747406ebe308ad23c8e8cc3c26c9c51 - languageName: node - linkType: hard - "cookie@npm:0.7.1": version: 0.7.1 resolution: "cookie@npm:0.7.1" @@ -22834,13 +22558,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:~0.7.1": - version: 0.7.2 - resolution: "cookie@npm:0.7.2" - checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 - languageName: node - linkType: hard - "cookies@npm:~0.9.0": version: 0.9.1 resolution: "cookies@npm:0.9.1" @@ -22883,7 +22600,7 @@ __metadata: languageName: node linkType: hard -"cors@npm:^2.8.1, cors@npm:^2.8.5": +"cors@npm:^2.8.1": version: 2.8.5 resolution: "cors@npm:2.8.5" dependencies: @@ -23902,7 +23619,7 @@ __metadata: languageName: node linkType: hard -"destroy@npm:1.2.0, destroy@npm:^1.0.4, destroy@npm:~1.2.0": +"destroy@npm:1.2.0, destroy@npm:^1.0.4": version: 1.2.0 resolution: "destroy@npm:1.2.0" checksum: 10c0/bd7633942f57418f5a3b80d5cb53898127bcf53e24cdf5d5f4396be471417671f0fee48a4ebe9a1e9defbde2a31280011af58a57e090ff822f589b443ed4e643 @@ -24344,13 +24061,6 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.3.1": - version: 16.6.1 - resolution: "dotenv@npm:16.6.1" - checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc - languageName: node - linkType: hard - "dotenv@npm:~16.4.5": version: 16.4.7 resolution: "dotenv@npm:16.4.7" @@ -25102,95 +24812,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:~0.27.0": - version: 0.27.1 - resolution: "esbuild@npm:0.27.1" - dependencies: - "@esbuild/aix-ppc64": "npm:0.27.1" - "@esbuild/android-arm": "npm:0.27.1" - "@esbuild/android-arm64": "npm:0.27.1" - "@esbuild/android-x64": "npm:0.27.1" - "@esbuild/darwin-arm64": "npm:0.27.1" - "@esbuild/darwin-x64": "npm:0.27.1" - "@esbuild/freebsd-arm64": "npm:0.27.1" - "@esbuild/freebsd-x64": "npm:0.27.1" - "@esbuild/linux-arm": "npm:0.27.1" - "@esbuild/linux-arm64": "npm:0.27.1" - "@esbuild/linux-ia32": "npm:0.27.1" - "@esbuild/linux-loong64": "npm:0.27.1" - "@esbuild/linux-mips64el": "npm:0.27.1" - "@esbuild/linux-ppc64": "npm:0.27.1" - "@esbuild/linux-riscv64": "npm:0.27.1" - "@esbuild/linux-s390x": "npm:0.27.1" - "@esbuild/linux-x64": "npm:0.27.1" - "@esbuild/netbsd-arm64": "npm:0.27.1" - "@esbuild/netbsd-x64": "npm:0.27.1" - "@esbuild/openbsd-arm64": "npm:0.27.1" - "@esbuild/openbsd-x64": "npm:0.27.1" - "@esbuild/openharmony-arm64": "npm:0.27.1" - "@esbuild/sunos-x64": "npm:0.27.1" - "@esbuild/win32-arm64": "npm:0.27.1" - "@esbuild/win32-ia32": "npm:0.27.1" - "@esbuild/win32-x64": "npm:0.27.1" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/openharmony-arm64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/8bfcf13a499a9e7b7da4b68273e12b453c7d7a5e39c944c2e5a4c64a0594d6df1391fc168a5353c22bc94eeae38dd9897199ddbbc4973525b0aae18186e996bd - languageName: node - linkType: hard - "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -26200,45 +25821,6 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.18.2": - version: 4.22.1 - resolution: "express@npm:4.22.1" - dependencies: - accepts: "npm:~1.3.8" - array-flatten: "npm:1.1.1" - body-parser: "npm:~1.20.3" - content-disposition: "npm:~0.5.4" - content-type: "npm:~1.0.4" - cookie: "npm:~0.7.1" - cookie-signature: "npm:~1.0.6" - debug: "npm:2.6.9" - depd: "npm:2.0.0" - encodeurl: "npm:~2.0.0" - escape-html: "npm:~1.0.3" - etag: "npm:~1.8.1" - finalhandler: "npm:~1.3.1" - fresh: "npm:~0.5.2" - http-errors: "npm:~2.0.0" - merge-descriptors: "npm:1.0.3" - methods: "npm:~1.1.2" - on-finished: "npm:~2.4.1" - parseurl: "npm:~1.3.3" - path-to-regexp: "npm:~0.1.12" - proxy-addr: "npm:~2.0.7" - qs: "npm:~6.14.0" - range-parser: "npm:~1.2.1" - safe-buffer: "npm:5.2.1" - send: "npm:~0.19.0" - serve-static: "npm:~1.16.2" - setprototypeof: "npm:1.2.0" - statuses: "npm:~2.0.1" - type-is: "npm:~1.6.18" - utils-merge: "npm:1.0.1" - vary: "npm:~1.1.2" - checksum: 10c0/ea57f512ab1e05e26b53a14fd432f65a10ec735ece342b37d0b63a7bcb8d337ffbb830ecb8ca15bcdfe423fbff88cea09786277baff200e8cde3ab40faa665cd - languageName: node - linkType: hard - "exsolve@npm:^1.0.1": version: 1.0.4 resolution: "exsolve@npm:1.0.4" @@ -26722,21 +26304,6 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:~1.3.1": - version: 1.3.2 - resolution: "finalhandler@npm:1.3.2" - dependencies: - debug: "npm:2.6.9" - encodeurl: "npm:~2.0.0" - escape-html: "npm:~1.0.3" - on-finished: "npm:~2.4.1" - parseurl: "npm:~1.3.3" - statuses: "npm:~2.0.2" - unpipe: "npm:~1.0.0" - checksum: 10c0/435a4fd65e4e4e4c71bb5474980090b73c353a123dd415583f67836bdd6516e528cf07298e219a82b94631dee7830eae5eece38d3c178073cf7df4e8c182f413 - languageName: node - linkType: hard - "find-babel-config@npm:^2.1.1": version: 2.1.2 resolution: "find-babel-config@npm:2.1.2" @@ -28323,19 +27890,6 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:~2.0.0, http-errors@npm:~2.0.1": - version: 2.0.1 - resolution: "http-errors@npm:2.0.1" - dependencies: - depd: "npm:~2.0.0" - inherits: "npm:~2.0.4" - setprototypeof: "npm:~1.2.0" - statuses: "npm:~2.0.2" - toidentifier: "npm:~1.0.1" - checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4 - languageName: node - linkType: hard - "http-https@npm:^1.0.0": version: 1.0.0 resolution: "http-https@npm:1.0.0" @@ -28534,7 +28088,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24, iconv-lite@npm:~0.4.24": +"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -32806,7 +32360,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:2.4.1, on-finished@npm:^2.3.0, on-finished@npm:~2.4.1": +"on-finished@npm:2.4.1, on-finished@npm:^2.3.0": version: 2.4.1 resolution: "on-finished@npm:2.4.1" dependencies: @@ -33494,7 +33048,7 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:0.1.12, path-to-regexp@npm:~0.1.12": +"path-to-regexp@npm:0.1.12": version: 0.1.12 resolution: "path-to-regexp@npm:0.1.12" checksum: 10c0/1c6ff10ca169b773f3bba943bbc6a07182e332464704572962d277b900aeee81ac6aa5d060ff9e01149636c30b1f63af6e69dd7786ba6e0ddb39d4dee1f0645b @@ -34887,7 +34441,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.12.3, qs@npm:^6.4.0, qs@npm:~6.14.0": +"qs@npm:^6.12.3, qs@npm:^6.4.0": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -35027,18 +34581,6 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:~2.5.3": - version: 2.5.3 - resolution: "raw-body@npm:2.5.3" - dependencies: - bytes: "npm:~3.1.2" - http-errors: "npm:~2.0.1" - iconv-lite: "npm:~0.4.24" - unpipe: "npm:~1.0.0" - checksum: 10c0/449844344fc90547fb994383a494b83300e4f22199f146a79f68d78a199a8f2a923ea9fd29c3be979bfd50291a3884733619ffc15ba02a32e703b612f8d3f74a - languageName: node - linkType: hard - "rc@npm:1.2.8, rc@npm:^1.2.7, rc@npm:^1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8" @@ -36670,27 +36212,6 @@ __metadata: languageName: node linkType: hard -"send@npm:~0.19.0": - version: 0.19.1 - resolution: "send@npm:0.19.1" - dependencies: - debug: "npm:2.6.9" - depd: "npm:2.0.0" - destroy: "npm:1.2.0" - encodeurl: "npm:~2.0.0" - escape-html: "npm:~1.0.3" - etag: "npm:~1.8.1" - fresh: "npm:0.5.2" - http-errors: "npm:2.0.0" - mime: "npm:1.6.0" - ms: "npm:2.1.3" - on-finished: "npm:2.4.1" - range-parser: "npm:~1.2.1" - statuses: "npm:2.0.1" - checksum: 10c0/ceb859859822bf55e705b96db9a909870626d1a6bfcf62a88648b9681048a7840c0ff1f4afd7babea4ccfabff7d64a7dda68a6f6c63c255cc83f40a412a1db8e - languageName: node - linkType: hard - "sentence-case@npm:^3.0.4": version: 3.0.4 resolution: "sentence-case@npm:3.0.4" @@ -36726,7 +36247,7 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:1.16.2, serve-static@npm:~1.16.2": +"serve-static@npm:1.16.2": version: 1.16.2 resolution: "serve-static@npm:1.16.2" dependencies: @@ -36816,7 +36337,7 @@ __metadata: languageName: node linkType: hard -"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": +"setprototypeof@npm:1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc @@ -37429,13 +36950,6 @@ __metadata: languageName: node linkType: hard -"statuses@npm:~2.0.1, statuses@npm:~2.0.2": - version: 2.0.2 - resolution: "statuses@npm:2.0.2" - checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f - languageName: node - linkType: hard - "std-env@npm:^3.8.1": version: 3.9.0 resolution: "std-env@npm:3.9.0" @@ -38686,7 +38200,7 @@ __metadata: languageName: node linkType: hard -"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": +"toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 @@ -39055,22 +38569,6 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^4.6.2": - version: 4.21.0 - resolution: "tsx@npm:4.21.0" - dependencies: - esbuild: "npm:~0.27.0" - fsevents: "npm:~2.3.3" - get-tsconfig: "npm:^4.7.5" - dependenciesMeta: - fsevents: - optional: true - bin: - tsx: dist/cli.mjs - checksum: 10c0/f5072923cd8459a1f9a26df87823a2ab5754641739d69df2a20b415f61814322b751fa6be85db7c6ec73cf68ba8fac2fd1cfc76bdb0aa86ded984d84d5d2126b - languageName: node - linkType: hard - "tty-browserify@npm:0.0.1": version: 0.0.1 resolution: "tty-browserify@npm:0.0.1" @@ -39320,16 +38818,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.3.2": - version: 5.9.3 - resolution: "typescript@npm:5.9.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 - languageName: node - linkType: hard - "typescript@npm:~5.7.2": version: 5.7.3 resolution: "typescript@npm:5.7.3" @@ -39380,16 +38868,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.3.2#optional!builtin": - version: 5.9.3 - resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 - languageName: node - linkType: hard - "typescript@patch:typescript@npm%3A~5.7.2#optional!builtin": version: 5.7.3 resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" @@ -40270,7 +39748,7 @@ __metadata: languageName: node linkType: hard -"viem@npm:>=2.29.0, viem@npm:^2.1.1, viem@npm:^2.21.0, viem@npm:^2.21.26, viem@npm:^2.27.2, viem@npm:^2.31.7, viem@npm:^2.33.2, viem@npm:^2.37.0, viem@npm:^2.41.2": +"viem@npm:>=2.29.0, viem@npm:^2.1.1, viem@npm:^2.21.26, viem@npm:^2.27.2, viem@npm:^2.31.7, viem@npm:^2.33.2, viem@npm:^2.37.0, viem@npm:^2.41.2": version: 2.41.2 resolution: "viem@npm:2.41.2" dependencies: From d3de41fea1cfb805f2c4d9632f22a09fd194a9dc Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Wed, 24 Dec 2025 09:01:15 +0700 Subject: [PATCH 18/31] fix: address PR review feedback for liquid staking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes: - Replace incorrect useRef with useMemo for static Icon objects - Fix memory leak from uncleaned setTimeout in redeem form - Fix optimistic requests claimable check by filtering out unindexed requests - Sync form state with useAllBlueprints in create-vault - Add timestamp tolerance to optimistic request cleanup to prevent race conditions Important fixes: - Remove fragile error string matching for ERC20 approval retry - Fix auto-close timer to check tx hash before closing modal - Limit dismissedHashes Set size to prevent memory growth - Standardize isNumericString to use faster regex implementation Suggestions: - Remove dead code exports from tokenMetadata.ts - Create useFormSetValue hook to consolidate setValue wrapper pattern - Standardize number formatting to use addCommasToNumber 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/components/TxHistoryDrawer.tsx | 10 +-- apps/tangle-dapp/src/hooks/useFormSetValue.ts | 33 ++++++++ .../liquid-staking/create-vault/index.tsx | 15 +--- .../pages/liquid-staking/deposit/index.tsx | 44 +++------- .../src/pages/liquid-staking/redeem/index.tsx | 84 +++++++++++++------ .../src/pages/restake/delegate/index.tsx | 12 +-- .../src/pages/restake/deposit/DepositForm.tsx | 12 +-- libs/dapp-config/src/tokenMetadata.ts | 42 ---------- .../TxConfirmationModal.tsx | 22 ++++- 9 files changed, 131 insertions(+), 143 deletions(-) create mode 100644 apps/tangle-dapp/src/hooks/useFormSetValue.ts diff --git a/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx b/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx index 21d066aa92..b6282024ca 100644 --- a/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx +++ b/apps/tangle-dapp/src/components/TxHistoryDrawer.tsx @@ -180,14 +180,10 @@ type DetailRowProps = { tokenMetadata?: TokenMetadata | null; }; -// Check if a string is a valid numeric value that can be parsed as BigInt +// Check if a string is a pure numeric value (all digits) +// Using regex instead of BigInt parsing for better performance const isNumericString = (value: string): boolean => { - try { - BigInt(value); - return true; - } catch { - return false; - } + return /^\d+$/.test(value); }; const DetailRow: FC = ({ diff --git a/apps/tangle-dapp/src/hooks/useFormSetValue.ts b/apps/tangle-dapp/src/hooks/useFormSetValue.ts new file mode 100644 index 0000000000..9ff60c5efb --- /dev/null +++ b/apps/tangle-dapp/src/hooks/useFormSetValue.ts @@ -0,0 +1,33 @@ +import { useCallback } from 'react'; +import { FieldValues, Path, PathValue, UseFormSetValue } from 'react-hook-form'; + +/** + * A wrapper around react-hook-form's setValue that automatically sets + * `shouldDirty` and `shouldValidate` to true for better UX. + * + * Usage: + * ```tsx + * const { setValue: setFormValue } = useForm(); + * const setValue = useFormSetValue(setFormValue); + * ``` + */ +const useFormSetValue = ( + setFormValue: UseFormSetValue, +) => { + return useCallback( + >( + name: TFieldName, + value: PathValue, + options?: Parameters>[2], + ) => { + setFormValue(name, value, { + shouldDirty: true, + shouldValidate: true, + ...options, + }); + }, + [setFormValue], + ); +}; + +export default useFormSetValue; diff --git a/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx index 66d56acc4f..a7fc4d21eb 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/create-vault/index.tsx @@ -11,6 +11,7 @@ import { TransactionInputCard } from '@tangle-network/ui-components/components/T import { useModal } from '@tangle-network/ui-components/hooks/useModal'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import useFormSetValue from '../../../hooks/useFormSetValue'; import { SubmitHandler, useForm } from 'react-hook-form'; import { Address } from 'viem'; import { useAccount } from 'wagmi'; @@ -87,16 +88,7 @@ const CreateVaultForm: FC = () => { }, }); - const setValue = useCallback( - (...params: Parameters) => { - setFormValue(params[0], params[1], { - shouldDirty: true, - shouldValidate: true, - ...params[2], - }); - }, - [setFormValue], - ); + const setValue = useFormSetValue(setFormValue); useEffect(() => { register('operator', { required: 'Operator is required' }); @@ -231,13 +223,14 @@ const CreateVaultForm: FC = () => { const handleAllBlueprintsChange = useCallback( (checked: boolean) => { setUseAllBlueprints(checked); + setValue('useAllBlueprints', checked); if (checked) { setSelectedBlueprintIds([]); } else { openBlueprintModal(); } }, - [openBlueprintModal], + [openBlueprintModal, setValue], ); const displayError = useMemo(() => { diff --git a/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx index 188e9238df..c853014259 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/deposit/index.tsx @@ -11,7 +11,8 @@ import type { TextFieldInputProps } from '@tangle-network/ui-components/componen import { TransactionInputCard } from '@tangle-network/ui-components/components/TransactionInputCard'; import { useModal } from '@tangle-network/ui-components/hooks/useModal'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import useFormSetValue from '../../../hooks/useFormSetValue'; import { SubmitHandler, useForm } from 'react-hook-form'; import { useSearchParams } from 'react-router'; import { erc20Abi, parseUnits, formatUnits, Address, isAddress } from 'viem'; @@ -73,16 +74,7 @@ const LiquidStakingDepositForm: FC = () => { mode: 'onChange', }); - const setValue = useCallback( - (...params: Parameters) => { - setFormValue(params[0], params[1], { - shouldDirty: true, - shouldValidate: true, - ...params[2], - }); - }, - [setFormValue], - ); + const setValue = useFormSetValue(setFormValue); useEffect(() => { register('vaultAddress', { required: 'Vault is required' }); @@ -145,8 +137,6 @@ const LiquidStakingDepositForm: FC = () => { const spender = selectedVault?.address ?? null; - const approveLastErrorRef = useRef(null); - const { status: approveTxStatus, execute: executeApproveTx, @@ -161,9 +151,6 @@ const LiquidStakingDepositForm: FC = () => { }), { txName: 'approve', - onError: (error) => { - approveLastErrorRef.current = error; - }, getSuccessMessage: () => 'Approval successful', }, ); @@ -178,7 +165,6 @@ const LiquidStakingDepositForm: FC = () => { return false; } - approveLastErrorRef.current = null; const firstAttempt = await executeApproveTx({ token: vaultAsset.id, spender, @@ -189,18 +175,9 @@ const LiquidStakingDepositForm: FC = () => { return true; } - // TS will narrow `ref.current` to `null` after assignment, even though the - // async write may set it via `onError`. Widen it back for the post-attempt check. - const message = - (approveLastErrorRef.current as Error | null)?.message?.toLowerCase() ?? - ''; - const looksLikeNonZeroAllowanceIssue = - message.includes('non-zero') && message.includes('allowance'); - - if (!looksLikeNonZeroAllowanceIssue) { - return false; - } - + // Some tokens (e.g., USDT) require resetting allowance to zero before + // setting a new value. Instead of fragile error message parsing, we always + // try the reset-to-zero pattern on failure - this is safe for all tokens. const zeroAttempt = await executeApproveTx({ token: vaultAsset.id, spender, @@ -378,12 +355,13 @@ const LiquidStakingDepositForm: FC = () => { ({ enabled: , disabled: , - }).current - } + }), + [], + )} onAmountChange={(value) => { setValue('amount', value, { shouldValidate: true }); }} diff --git a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx index 19f531818a..246f8f3727 100644 --- a/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx +++ b/apps/tangle-dapp/src/pages/liquid-staking/redeem/index.tsx @@ -11,6 +11,7 @@ import { TransactionInputCard } from '@tangle-network/ui-components/components/T import { useModal } from '@tangle-network/ui-components/hooks/useModal'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import useFormSetValue from '../../../hooks/useFormSetValue'; import { SubmitHandler, useForm } from 'react-hook-form'; import { useSearchParams } from 'react-router'; import { parseUnits, formatUnits, Address, isAddress } from 'viem'; @@ -72,16 +73,7 @@ const LiquidStakingRedeemForm: FC = () => { mode: 'onChange', }); - const setValue = useCallback( - (...params: Parameters) => { - setFormValue(params[0], params[1], { - shouldDirty: true, - shouldValidate: true, - ...params[2], - }); - }, - [setFormValue], - ); + const setValue = useFormSetValue(setFormValue); useEffect(() => { register('vaultAddress', { required: 'Vault is required' }); @@ -144,18 +136,36 @@ const LiquidStakingRedeemForm: FC = () => { LiquidRedeemRequest[] >([]); + // Ref to store timeout ID for cleanup + const refetchTimeoutRef = useRef | null>(null); + // Clean up optimistic requests when they appear in the indexer data + // Match by shares, controller, vault address, AND timestamp (within 60s tolerance) + // This prevents race conditions when multiple requests have the same share amount useEffect(() => { + const TIMESTAMP_TOLERANCE_SECONDS = BigInt(60); + setOptimisticRequests((prev) => prev.filter( (opt) => - !indexerRedeemRequests.some( - (req) => - req.vaultAddress.toLowerCase() === - opt.vaultAddress.toLowerCase() && - req.shares === opt.shares && - req.controller.toLowerCase() === opt.controller.toLowerCase(), - ), + !indexerRedeemRequests.some((req) => { + const sameVault = + req.vaultAddress.toLowerCase() === opt.vaultAddress.toLowerCase(); + const sameShares = req.shares === opt.shares; + const sameController = + req.controller.toLowerCase() === opt.controller.toLowerCase(); + + // Check if timestamps are within tolerance window + const timeDiff = + req.createdAt > opt.createdAt + ? req.createdAt - opt.createdAt + : opt.createdAt - req.createdAt; + const withinTimeWindow = timeDiff <= TIMESTAMP_TOLERANCE_SECONDS; + + return ( + sameVault && sameShares && sameController && withinTimeWindow + ); + }), ), ); }, [indexerRedeemRequests]); @@ -175,15 +185,30 @@ const LiquidStakingRedeemForm: FC = () => { setOptimisticRequests([]); }, [selectedVault?.address]); + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (refetchTimeoutRef.current !== null) { + clearTimeout(refetchTimeoutRef.current); + } + }; + }, []); + + // Only check claimable status for indexed requests (not optimistic ones) + // Optimistic requests have fake IDs starting with "optimistic-" and requestId of 0 + const indexedRequestsForClaimable = useMemo(() => { + return indexerRedeemRequests.filter((req) => req.requestId !== BigInt(0)); + }, [indexerRedeemRequests]); + const claimableContracts = useMemo(() => { - if (!selectedVault || redeemRequests.length === 0) return []; - return redeemRequests.map((req) => ({ + if (!selectedVault || indexedRequestsForClaimable.length === 0) return []; + return indexedRequestsForClaimable.map((req) => ({ address: selectedVault.address, abi: LIQUID_DELEGATION_VAULT_ABI, functionName: 'claimableRedeemRequest' as const, args: [req.requestId, req.controller] as const, })); - }, [redeemRequests, selectedVault]); + }, [indexedRequestsForClaimable, selectedVault]); const { data: claimableResults } = useReadContracts({ contracts: claimableContracts, @@ -199,12 +224,12 @@ const LiquidStakingRedeemForm: FC = () => { const map = new Map(); if (!claimableResults) return map; claimableResults.forEach((res, idx) => { - const req = redeemRequests[idx]; + const req = indexedRequestsForClaimable[idx]; if (!req || res?.status !== 'success') return; map.set(req.id, res.result as bigint); }); return map; - }, [claimableResults, redeemRequests]); + }, [claimableResults, indexedRequestsForClaimable]); const vaultAsset = useMemo(() => { if (!selectedVault || !assets) { @@ -327,8 +352,12 @@ const LiquidStakingRedeemForm: FC = () => { // Refetch redeem requests after a short delay to allow the indexer to process // the new transaction. The optimistic entry will be replaced once indexed. - setTimeout(() => { + if (refetchTimeoutRef.current !== null) { + clearTimeout(refetchTimeoutRef.current); + } + refetchTimeoutRef.current = setTimeout(() => { refetchRedeemRequests(); + refetchTimeoutRef.current = null; }, 3000); }, [ @@ -389,12 +418,13 @@ const LiquidStakingRedeemForm: FC = () => { maxAmount={formattedMaxAmount} tokenSymbol="shares" tooltipBody="Available Shares" - Icon={ - useRef({ + Icon={useMemo( + () => ({ enabled: , disabled: , - }).current - } + }), + [], + )} onAmountChange={(value) => { setValue('amount', value, { shouldValidate: true }); }} diff --git a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx index b39bf9f3aa..7d47a13843 100644 --- a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx @@ -17,6 +17,7 @@ import type { TextFieldInputProps } from '@tangle-network/ui-components/componen 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 useFormSetValue from '../../../hooks/useFormSetValue'; import { SubmitHandler, useForm } from 'react-hook-form'; import { Address, formatUnits, parseUnits } from 'viem'; import { BN } from '@polkadot/util'; @@ -93,16 +94,7 @@ const RestakeDelegateForm: FC = () => { QueryParamKey.RESTAKE_OPERATOR, ); - const setValue = useCallback( - (...params: Parameters) => { - setFormValue(params[0], params[1], { - shouldDirty: true, - shouldValidate: true, - ...params[2], - }); - }, - [setFormValue], - ); + const setValue = useFormSetValue(setFormValue); useEffect(() => { register('assetId', { required: 'Asset is required' }); diff --git a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx index d0d8957c16..e8efa4c127 100644 --- a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx +++ b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx @@ -13,6 +13,7 @@ import { TransactionInputCard } from '@tangle-network/ui-components/components/T import { useModal } from '@tangle-network/ui-components/hooks/useModal'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import useFormSetValue from '../../../hooks/useFormSetValue'; import { SubmitHandler, useForm } from 'react-hook-form'; import { erc20Abi, formatUnits, parseUnits, zeroAddress, Address } from 'viem'; import { BN } from '@polkadot/util'; @@ -108,16 +109,7 @@ const DepositForm: FC = () => { }, }); - const setValue = useCallback( - (...params: Parameters) => { - setFormValue(params[0], params[1], { - shouldDirty: true, - shouldValidate: true, - ...params[2], - }); - }, - [setFormValue], - ); + const setValue = useFormSetValue(setFormValue); useEffect(() => { register('depositAssetId', { required: 'Asset is required' }); diff --git a/libs/dapp-config/src/tokenMetadata.ts b/libs/dapp-config/src/tokenMetadata.ts index b0812b1b73..a30feb0c43 100644 --- a/libs/dapp-config/src/tokenMetadata.ts +++ b/libs/dapp-config/src/tokenMetadata.ts @@ -79,14 +79,6 @@ const metadataCache = new Map(); const buildCacheKey = (chainId: number, address: string): string => `${chainId}:${address.toLowerCase()}`; -/** - * Get token metadata by symbol (case-insensitive). - * Symbols are chain-agnostic (e.g., "USDC" metadata is the same across chains). - */ -export const getTokenBySymbol = (symbol: string): TokenMetadata | undefined => { - return KNOWN_TOKENS[symbol] ?? KNOWN_TOKENS[symbol.toUpperCase()]; -}; - /** * Get token metadata by chain ID and address. * Checks runtime cache first, then known addresses for the specific chain. @@ -131,37 +123,3 @@ export const cacheTokenMetadata = ( ): void => { metadataCache.set(buildCacheKey(chainId, address), metadata); }; - -/** - * Try to resolve token metadata from cache or known tokens. - * Returns undefined if not found - caller should fetch from chain. - * - * @param chainId - The EVM chain ID - * @param address - The token contract address - * @param symbol - Optional symbol hint for fallback lookup - */ -export const resolveTokenMetadata = ( - chainId: number, - address: Address, - symbol?: string, -): TokenMetadata | undefined => { - // Check chain-aware address cache first - const cached = getCachedTokenMetadata(chainId, address); - if (cached) return cached; - - // Try symbol lookup if provided (symbols are chain-agnostic) - if (symbol) { - const bySymbol = getTokenBySymbol(symbol); - if (bySymbol) { - cacheTokenMetadata(chainId, address, bySymbol); - return bySymbol; - } - } - - return undefined; -}; - -// Legacy exports for backward compatibility -export type LocalTokenConfig = TokenMetadata & { address: Address }; -export const getKnownTokenMetadata = getCachedTokenMetadata; -export const getTokenMetadataBySymbol = getTokenBySymbol; diff --git a/libs/tangle-shared-ui/src/components/TxConfirmationModal/TxConfirmationModal.tsx b/libs/tangle-shared-ui/src/components/TxConfirmationModal/TxConfirmationModal.tsx index 1dccbae1f7..e7bfe7fc59 100644 --- a/libs/tangle-shared-ui/src/components/TxConfirmationModal/TxConfirmationModal.tsx +++ b/libs/tangle-shared-ui/src/components/TxConfirmationModal/TxConfirmationModal.tsx @@ -28,6 +28,7 @@ import { Typography, } from '@tangle-network/ui-components'; import { EvmAddress } from '@tangle-network/ui-components/types/address'; +import addCommasToNumber from '@tangle-network/ui-components/utils/addCommasToNumber'; import { Modal, ModalContent, @@ -102,7 +103,7 @@ const DetailRow: FC = ({ const symbol = tokenMetadata?.symbol ?? nativeTokenSymbol; return `${formatted} ${symbol}`; } - return value.toLocaleString(); + return addCommasToNumber(value); } if (typeof value === 'string' && isEvmAddress(value)) { @@ -198,6 +199,8 @@ const TxConfirmationModal: FC = ({ const [activeHash, setActiveHash] = useState(null); // Prevent reopening the modal for the same tx after a user dismisses it. + // Limit size to prevent unbounded memory growth. + const MAX_DISMISSED_HASHES = 100; const dismissedHashes = useRef>(new Set()); const relevantTransactions = useMemo(() => { @@ -256,17 +259,22 @@ const TxConfirmationModal: FC = ({ }, [autoOpen, newestPending]); // Auto-close on success after a short delay. + // Capture tx.hash to ensure we only close if the same transaction is still active. useEffect(() => { if (!open || tx === null || tx.status !== 'finalized') { return; } + const currentHash = tx.hash; const timer = window.setTimeout(() => { - setOpen(false); + // Only close if the same transaction is still being displayed + if (activeHash === currentHash) { + setOpen(false); + } }, autoCloseSuccessMs); return () => window.clearTimeout(timer); - }, [autoCloseSuccessMs, open, tx]); + }, [autoCloseSuccessMs, open, tx, activeHash]); const explorerUrl = useMemo(() => { if (tx === null) { @@ -283,6 +291,14 @@ const TxConfirmationModal: FC = ({ const close = () => { if (activeHash !== null) { + // Limit dismissed hashes to prevent unbounded memory growth + if (dismissedHashes.current.size >= MAX_DISMISSED_HASHES) { + // Remove oldest entry (first item in Set iteration order) + const firstKey = dismissedHashes.current.values().next().value; + if (firstKey !== undefined) { + dismissedHashes.current.delete(firstKey); + } + } dismissedHashes.current.add(activeHash); } setOpen(false); From f5661b167b3c8eb03c9cbf589caec1749e3094ec Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Thu, 25 Dec 2025 09:47:54 +0700 Subject: [PATCH 19/31] fix: show wallet mode banner when relayer is unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display an orange warning banner and update button text to inform users when the relayer fails and they need to pay gas fees directly from their wallet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../claim/migration/hooks/useSubmitClaim.ts | 7 +++++ .../src/pages/claim/migration/index.tsx | 30 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/apps/tangle-dapp/src/pages/claim/migration/hooks/useSubmitClaim.ts b/apps/tangle-dapp/src/pages/claim/migration/hooks/useSubmitClaim.ts index e91faf6620..08a4b693e7 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/hooks/useSubmitClaim.ts +++ b/apps/tangle-dapp/src/pages/claim/migration/hooks/useSubmitClaim.ts @@ -272,6 +272,10 @@ const useSubmitClaim = () => { ? relayerError : writeError || confirmError; + // Track if we switched from relayer to wallet mode (indicates relayer failure) + const switchedToWalletMode = + defaultSubmissionMode === 'relayer' && submissionMode === 'wallet'; + return { submitClaim, reset, @@ -281,6 +285,9 @@ const useSubmitClaim = () => { isConfirmed: isConfirmedCombined, error: errorCombined, contractConfigured: !!migrationAddress, + submissionMode, + isRelayerConfigured: !!CLAIM_RELAYER_URL, + switchedToWalletMode, }; }; diff --git a/apps/tangle-dapp/src/pages/claim/migration/index.tsx b/apps/tangle-dapp/src/pages/claim/migration/index.tsx index c76709ed8e..bc7972856f 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/index.tsx +++ b/apps/tangle-dapp/src/pages/claim/migration/index.tsx @@ -147,6 +147,7 @@ const MigrationClaimPage: FC = () => { isConfirming, isConfirmed, error: submitError, + switchedToWalletMode, } = useSubmitClaim(); // Validate recipient address @@ -815,6 +816,30 @@ const MigrationClaimPage: FC = () => { exit={{ opacity: 0, y: -10 }} className="space-y-3" > + {switchedToWalletMode && ( +
+
+ +
+ + Wallet Mode Active + + + The relayer is unavailable. You will pay gas fees + directly from your wallet. + +
+
+
+ )} + {submitError && (
@@ -837,6 +862,7 @@ const MigrationClaimPage: FC = () => {
)} + )} From 25b6bda292f397b64957c565c2da1cd35c4b5c6b Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Sun, 28 Dec 2025 13:45:56 +0700 Subject: [PATCH 20/31] chore: add full TangleMigration ABI and Base Sepolia icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the TangleMigration contract ABI to include all functions, events, and error types. Add Base Sepolia chain icon for network selection UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../migration/hooks/useClaimEligibility.ts | 758 +++++++++++++++++- libs/icons/src/chains/base-sepolia.svg | 18 + 2 files changed, 756 insertions(+), 20 deletions(-) create mode 100644 libs/icons/src/chains/base-sepolia.svg diff --git a/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts b/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts index fe6c03f06e..85d5f80354 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts +++ b/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts @@ -29,33 +29,751 @@ const MIGRATION_RPC_URL = import.meta.env.VITE_MIGRATION_RPC_URL as // TangleMigration contract ABI (partial - only functions we use) const TANGLE_MIGRATION_ABI = [ { - name: 'getClaimedAmount', - type: 'function', - stateMutability: 'view', - inputs: [{ name: 'pubkey', type: 'bytes32' }], - outputs: [{ name: '', type: 'uint256' }], + "type": "constructor", + "inputs": [ + { + "name": "_token", + "type": "address", + "internalType": "address" + }, + { + "name": "_merkleRoot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "_zkVerifier", + "type": "address", + "internalType": "address" + }, + { + "name": "_owner", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "adminClaim", + "inputs": [ + { + "name": "pubkey", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "merkleProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "adminClaimDeadline", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "claimDeadline", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "claimWithZKProof", + "inputs": [ + { + "name": "pubkey", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "merkleProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "zkProof", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "claimed", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "emergencyWithdraw", + "inputs": [ + { + "name": "_token", + "type": "address", + "internalType": "address" + }, + { + "name": "_amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getClaimedAmount", + "inputs": [ + { + "name": "pubkey", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lockFactory", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract TNTLockFactory" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "merkleRoot", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "reduceAdminClaimDeadline", + "inputs": [ + { + "name": "_newDeadline", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setClaimDeadline", + "inputs": [ + { + "name": "_deadline", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setLockConfig", + "inputs": [ + { + "name": "_lockFactory", + "type": "address", + "internalType": "address" + }, + { + "name": "_unlockTimestamp", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_unlockedBps", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setMerkleRoot", + "inputs": [ + { + "name": "_merkleRoot", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setPaused", + "inputs": [ + { + "name": "_paused", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setZKVerifier", + "inputs": [ + { + "name": "_zkVerifier", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "token", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalClaimed", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unlockTimestamp", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "unlockedBps", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" }, { - name: 'claimDeadline', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ name: '', type: 'uint256' }], + "type": "function", + "name": "verifyMerkleProof", + "inputs": [ + { + "name": "pubkey", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "merkleProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ], + "outputs": [ + { + "name": "valid", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "zkVerifier", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IZKVerifier" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AdminClaimDeadlineUpdated", + "inputs": [ + { + "name": "oldDeadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newDeadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AdminClaimed", + "inputs": [ + { + "name": "pubkey", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ClaimDeadlineUpdated", + "inputs": [ + { + "name": "oldDeadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newDeadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Claimed", + "inputs": [ + { + "name": "pubkey", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "unlockedAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "lockedAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "lock", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "EmergencyWithdraw", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "LockConfigUpdated", + "inputs": [ + { + "name": "lockFactory", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "unlockTimestamp", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "unlockedBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MerkleRootUpdated", + "inputs": [ + { + "name": "oldRoot", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + }, + { + "name": "newRoot", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Paused", + "inputs": [ + { + "name": "isPaused", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ZKVerifierUpdated", + "inputs": [ + { + "name": "oldVerifier", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newVerifier", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AdminClaimWindowClosed", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyClaimed", + "inputs": [] + }, + { + "type": "error", + "name": "ClaimDeadlinePassed", + "inputs": [] + }, + { + "type": "error", + "name": "ClaimsPaused", + "inputs": [] + }, + { + "type": "error", + "name": "EmergencyWithdrawNotAllowed", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAdminClaimDeadline", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidBps", + "inputs": [] }, { - name: 'paused', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ name: '', type: 'bool' }], + "type": "error", + "name": "InvalidMerkleProof", + "inputs": [] }, { - name: 'merkleRoot', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ name: '', type: 'bytes32' }], + "type": "error", + "name": "InvalidZKProof", + "inputs": [] }, + { + "type": "error", + "name": "LockConfigLocked", + "inputs": [] + }, + { + "type": "error", + "name": "NoZKVerifier", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAmount", + "inputs": [] + } ] as const; /** diff --git a/libs/icons/src/chains/base-sepolia.svg b/libs/icons/src/chains/base-sepolia.svg new file mode 100644 index 0000000000..82b20042fe --- /dev/null +++ b/libs/icons/src/chains/base-sepolia.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + From 040125031523a032c098b9b9fa7485c6028eabdf Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Sun, 28 Dec 2025 13:48:10 +0700 Subject: [PATCH 21/31] chore: format code --- .../migration/hooks/useClaimEligibility.ts | 1046 ++++++++--------- .../src/data/graphql/useDelegator.ts | 23 +- .../src/data/graphql/useOperators.ts | 30 +- .../src/data/graphql/useRestakingRound.ts | 12 +- 4 files changed, 574 insertions(+), 537 deletions(-) diff --git a/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts b/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts index 85d5f80354..f27ee9745a 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts +++ b/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts @@ -29,751 +29,751 @@ const MIGRATION_RPC_URL = import.meta.env.VITE_MIGRATION_RPC_URL as // TangleMigration contract ABI (partial - only functions we use) const TANGLE_MIGRATION_ABI = [ { - "type": "constructor", - "inputs": [ + type: 'constructor', + inputs: [ { - "name": "_token", - "type": "address", - "internalType": "address" + name: '_token', + type: 'address', + internalType: 'address', }, { - "name": "_merkleRoot", - "type": "bytes32", - "internalType": "bytes32" + name: '_merkleRoot', + type: 'bytes32', + internalType: 'bytes32', }, { - "name": "_zkVerifier", - "type": "address", - "internalType": "address" + name: '_zkVerifier', + type: 'address', + internalType: 'address', }, { - "name": "_owner", - "type": "address", - "internalType": "address" - } + name: '_owner', + type: 'address', + internalType: 'address', + }, ], - "stateMutability": "nonpayable" + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "adminClaim", - "inputs": [ + type: 'function', + name: 'adminClaim', + inputs: [ { - "name": "pubkey", - "type": "bytes32", - "internalType": "bytes32" + name: 'pubkey', + type: 'bytes32', + internalType: 'bytes32', }, { - "name": "amount", - "type": "uint256", - "internalType": "uint256" + name: 'amount', + type: 'uint256', + internalType: 'uint256', }, { - "name": "merkleProof", - "type": "bytes32[]", - "internalType": "bytes32[]" + name: 'merkleProof', + type: 'bytes32[]', + internalType: 'bytes32[]', }, { - "name": "recipient", - "type": "address", - "internalType": "address" - } + name: 'recipient', + type: 'address', + internalType: 'address', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "adminClaimDeadline", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } + type: 'function', + name: 'adminClaimDeadline', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "claimDeadline", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } + type: 'function', + name: 'claimDeadline', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "claimWithZKProof", - "inputs": [ + type: 'function', + name: 'claimWithZKProof', + inputs: [ { - "name": "pubkey", - "type": "bytes32", - "internalType": "bytes32" + name: 'pubkey', + type: 'bytes32', + internalType: 'bytes32', }, { - "name": "amount", - "type": "uint256", - "internalType": "uint256" + name: 'amount', + type: 'uint256', + internalType: 'uint256', }, { - "name": "merkleProof", - "type": "bytes32[]", - "internalType": "bytes32[]" + name: 'merkleProof', + type: 'bytes32[]', + internalType: 'bytes32[]', }, { - "name": "zkProof", - "type": "bytes", - "internalType": "bytes" + name: 'zkProof', + type: 'bytes', + internalType: 'bytes', }, { - "name": "recipient", - "type": "address", - "internalType": "address" - } + name: 'recipient', + type: 'address', + internalType: 'address', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "claimed", - "inputs": [ + type: 'function', + name: 'claimed', + inputs: [ { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } + name: '', + type: 'bytes32', + internalType: 'bytes32', + }, ], - "outputs": [ + outputs: [ { - "name": "", - "type": "uint256", - "internalType": "uint256" - } + name: '', + type: 'uint256', + internalType: 'uint256', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "emergencyWithdraw", - "inputs": [ + type: 'function', + name: 'emergencyWithdraw', + inputs: [ { - "name": "_token", - "type": "address", - "internalType": "address" + name: '_token', + type: 'address', + internalType: 'address', }, { - "name": "_amount", - "type": "uint256", - "internalType": "uint256" - } + name: '_amount', + type: 'uint256', + internalType: 'uint256', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "getClaimedAmount", - "inputs": [ + type: 'function', + name: 'getClaimedAmount', + inputs: [ { - "name": "pubkey", - "type": "bytes32", - "internalType": "bytes32" - } + name: 'pubkey', + type: 'bytes32', + internalType: 'bytes32', + }, ], - "outputs": [ + outputs: [ { - "name": "", - "type": "uint256", - "internalType": "uint256" - } + name: '', + type: 'uint256', + internalType: 'uint256', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "lockFactory", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract TNTLockFactory" - } + type: 'function', + name: 'lockFactory', + inputs: [], + outputs: [ + { + name: '', + type: 'address', + internalType: 'contract TNTLockFactory', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "merkleRoot", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } + type: 'function', + name: 'merkleRoot', + inputs: [], + outputs: [ + { + name: '', + type: 'bytes32', + internalType: 'bytes32', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "owner", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } + type: 'function', + name: 'owner', + inputs: [], + outputs: [ + { + name: '', + type: 'address', + internalType: 'address', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "paused", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } + type: 'function', + name: 'paused', + inputs: [], + outputs: [ + { + name: '', + type: 'bool', + internalType: 'bool', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "reduceAdminClaimDeadline", - "inputs": [ + type: 'function', + name: 'reduceAdminClaimDeadline', + inputs: [ { - "name": "_newDeadline", - "type": "uint256", - "internalType": "uint256" - } + name: '_newDeadline', + type: 'uint256', + internalType: 'uint256', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "renounceOwnership", - "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" + type: 'function', + name: 'renounceOwnership', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "setClaimDeadline", - "inputs": [ + type: 'function', + name: 'setClaimDeadline', + inputs: [ { - "name": "_deadline", - "type": "uint256", - "internalType": "uint256" - } + name: '_deadline', + type: 'uint256', + internalType: 'uint256', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "setLockConfig", - "inputs": [ + type: 'function', + name: 'setLockConfig', + inputs: [ { - "name": "_lockFactory", - "type": "address", - "internalType": "address" + name: '_lockFactory', + type: 'address', + internalType: 'address', }, { - "name": "_unlockTimestamp", - "type": "uint64", - "internalType": "uint64" + name: '_unlockTimestamp', + type: 'uint64', + internalType: 'uint64', }, { - "name": "_unlockedBps", - "type": "uint16", - "internalType": "uint16" - } + name: '_unlockedBps', + type: 'uint16', + internalType: 'uint16', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "setMerkleRoot", - "inputs": [ + type: 'function', + name: 'setMerkleRoot', + inputs: [ { - "name": "_merkleRoot", - "type": "bytes32", - "internalType": "bytes32" - } + name: '_merkleRoot', + type: 'bytes32', + internalType: 'bytes32', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "setPaused", - "inputs": [ + type: 'function', + name: 'setPaused', + inputs: [ { - "name": "_paused", - "type": "bool", - "internalType": "bool" - } + name: '_paused', + type: 'bool', + internalType: 'bool', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "setZKVerifier", - "inputs": [ + type: 'function', + name: 'setZKVerifier', + inputs: [ { - "name": "_zkVerifier", - "type": "address", - "internalType": "address" - } + name: '_zkVerifier', + type: 'address', + internalType: 'address', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "token", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IERC20" - } + type: 'function', + name: 'token', + inputs: [], + outputs: [ + { + name: '', + type: 'address', + internalType: 'contract IERC20', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "totalClaimed", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } + type: 'function', + name: 'totalClaimed', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "transferOwnership", - "inputs": [ + type: 'function', + name: 'transferOwnership', + inputs: [ { - "name": "newOwner", - "type": "address", - "internalType": "address" - } + name: 'newOwner', + type: 'address', + internalType: 'address', + }, ], - "outputs": [], - "stateMutability": "nonpayable" + outputs: [], + stateMutability: 'nonpayable', }, { - "type": "function", - "name": "unlockTimestamp", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint64", - "internalType": "uint64" - } + type: 'function', + name: 'unlockTimestamp', + inputs: [], + outputs: [ + { + name: '', + type: 'uint64', + internalType: 'uint64', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "unlockedBps", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint16", - "internalType": "uint16" - } + type: 'function', + name: 'unlockedBps', + inputs: [], + outputs: [ + { + name: '', + type: 'uint16', + internalType: 'uint16', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "verifyMerkleProof", - "inputs": [ + type: 'function', + name: 'verifyMerkleProof', + inputs: [ { - "name": "pubkey", - "type": "bytes32", - "internalType": "bytes32" + name: 'pubkey', + type: 'bytes32', + internalType: 'bytes32', }, { - "name": "amount", - "type": "uint256", - "internalType": "uint256" + name: 'amount', + type: 'uint256', + internalType: 'uint256', }, { - "name": "merkleProof", - "type": "bytes32[]", - "internalType": "bytes32[]" - } + name: 'merkleProof', + type: 'bytes32[]', + internalType: 'bytes32[]', + }, ], - "outputs": [ + outputs: [ { - "name": "valid", - "type": "bool", - "internalType": "bool" - } + name: 'valid', + type: 'bool', + internalType: 'bool', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "function", - "name": "zkVerifier", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IZKVerifier" - } + type: 'function', + name: 'zkVerifier', + inputs: [], + outputs: [ + { + name: '', + type: 'address', + internalType: 'contract IZKVerifier', + }, ], - "stateMutability": "view" + stateMutability: 'view', }, { - "type": "event", - "name": "AdminClaimDeadlineUpdated", - "inputs": [ - { - "name": "oldDeadline", - "type": "uint256", - "indexed": false, - "internalType": "uint256" + type: 'event', + name: 'AdminClaimDeadlineUpdated', + inputs: [ + { + name: 'oldDeadline', + type: 'uint256', + indexed: false, + internalType: 'uint256', }, { - "name": "newDeadline", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } + name: 'newDeadline', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "AdminClaimed", - "inputs": [ - { - "name": "pubkey", - "type": "bytes32", - "indexed": true, - "internalType": "bytes32" + type: 'event', + name: 'AdminClaimed', + inputs: [ + { + name: 'pubkey', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', }, { - "name": "recipient", - "type": "address", - "indexed": true, - "internalType": "address" + name: 'recipient', + type: 'address', + indexed: true, + internalType: 'address', }, { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "ClaimDeadlineUpdated", - "inputs": [ - { - "name": "oldDeadline", - "type": "uint256", - "indexed": false, - "internalType": "uint256" + type: 'event', + name: 'ClaimDeadlineUpdated', + inputs: [ + { + name: 'oldDeadline', + type: 'uint256', + indexed: false, + internalType: 'uint256', }, { - "name": "newDeadline", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } + name: 'newDeadline', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "Claimed", - "inputs": [ - { - "name": "pubkey", - "type": "bytes32", - "indexed": true, - "internalType": "bytes32" + type: 'event', + name: 'Claimed', + inputs: [ + { + name: 'pubkey', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', }, { - "name": "recipient", - "type": "address", - "indexed": true, - "internalType": "address" + name: 'recipient', + type: 'address', + indexed: true, + internalType: 'address', }, { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', }, { - "name": "unlockedAmount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" + name: 'unlockedAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', }, { - "name": "lockedAmount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" + name: 'lockedAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', }, { - "name": "lock", - "type": "address", - "indexed": false, - "internalType": "address" - } + name: 'lock', + type: 'address', + indexed: false, + internalType: 'address', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "EmergencyWithdraw", - "inputs": [ - { - "name": "token", - "type": "address", - "indexed": false, - "internalType": "address" + type: 'event', + name: 'EmergencyWithdraw', + inputs: [ + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', }, { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "LockConfigUpdated", - "inputs": [ - { - "name": "lockFactory", - "type": "address", - "indexed": false, - "internalType": "address" + type: 'event', + name: 'LockConfigUpdated', + inputs: [ + { + name: 'lockFactory', + type: 'address', + indexed: false, + internalType: 'address', }, { - "name": "unlockTimestamp", - "type": "uint64", - "indexed": false, - "internalType": "uint64" + name: 'unlockTimestamp', + type: 'uint64', + indexed: false, + internalType: 'uint64', }, { - "name": "unlockedBps", - "type": "uint16", - "indexed": false, - "internalType": "uint16" - } + name: 'unlockedBps', + type: 'uint16', + indexed: false, + internalType: 'uint16', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "MerkleRootUpdated", - "inputs": [ - { - "name": "oldRoot", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" + type: 'event', + name: 'MerkleRootUpdated', + inputs: [ + { + name: 'oldRoot', + type: 'bytes32', + indexed: false, + internalType: 'bytes32', }, { - "name": "newRoot", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" - } + name: 'newRoot', + type: 'bytes32', + indexed: false, + internalType: 'bytes32', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "OwnershipTransferred", - "inputs": [ - { - "name": "previousOwner", - "type": "address", - "indexed": true, - "internalType": "address" + type: 'event', + name: 'OwnershipTransferred', + inputs: [ + { + name: 'previousOwner', + type: 'address', + indexed: true, + internalType: 'address', }, { - "name": "newOwner", - "type": "address", - "indexed": true, - "internalType": "address" - } + name: 'newOwner', + type: 'address', + indexed: true, + internalType: 'address', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "Paused", - "inputs": [ - { - "name": "isPaused", - "type": "bool", - "indexed": false, - "internalType": "bool" - } + type: 'event', + name: 'Paused', + inputs: [ + { + name: 'isPaused', + type: 'bool', + indexed: false, + internalType: 'bool', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "event", - "name": "ZKVerifierUpdated", - "inputs": [ - { - "name": "oldVerifier", - "type": "address", - "indexed": false, - "internalType": "address" + type: 'event', + name: 'ZKVerifierUpdated', + inputs: [ + { + name: 'oldVerifier', + type: 'address', + indexed: false, + internalType: 'address', }, { - "name": "newVerifier", - "type": "address", - "indexed": false, - "internalType": "address" - } + name: 'newVerifier', + type: 'address', + indexed: false, + internalType: 'address', + }, ], - "anonymous": false + anonymous: false, }, { - "type": "error", - "name": "AdminClaimWindowClosed", - "inputs": [] + type: 'error', + name: 'AdminClaimWindowClosed', + inputs: [], }, { - "type": "error", - "name": "AlreadyClaimed", - "inputs": [] + type: 'error', + name: 'AlreadyClaimed', + inputs: [], }, { - "type": "error", - "name": "ClaimDeadlinePassed", - "inputs": [] + type: 'error', + name: 'ClaimDeadlinePassed', + inputs: [], }, { - "type": "error", - "name": "ClaimsPaused", - "inputs": [] + type: 'error', + name: 'ClaimsPaused', + inputs: [], }, { - "type": "error", - "name": "EmergencyWithdrawNotAllowed", - "inputs": [] + type: 'error', + name: 'EmergencyWithdrawNotAllowed', + inputs: [], }, { - "type": "error", - "name": "InvalidAdminClaimDeadline", - "inputs": [] + type: 'error', + name: 'InvalidAdminClaimDeadline', + inputs: [], }, { - "type": "error", - "name": "InvalidBps", - "inputs": [] + type: 'error', + name: 'InvalidBps', + inputs: [], }, { - "type": "error", - "name": "InvalidMerkleProof", - "inputs": [] + type: 'error', + name: 'InvalidMerkleProof', + inputs: [], }, { - "type": "error", - "name": "InvalidZKProof", - "inputs": [] + type: 'error', + name: 'InvalidZKProof', + inputs: [], }, { - "type": "error", - "name": "LockConfigLocked", - "inputs": [] + type: 'error', + name: 'LockConfigLocked', + inputs: [], }, { - "type": "error", - "name": "NoZKVerifier", - "inputs": [] + type: 'error', + name: 'NoZKVerifier', + inputs: [], }, { - "type": "error", - "name": "OwnableInvalidOwner", - "inputs": [ + type: 'error', + name: 'OwnableInvalidOwner', + inputs: [ { - "name": "owner", - "type": "address", - "internalType": "address" - } - ] + name: 'owner', + type: 'address', + internalType: 'address', + }, + ], }, { - "type": "error", - "name": "OwnableUnauthorizedAccount", - "inputs": [ + type: 'error', + name: 'OwnableUnauthorizedAccount', + inputs: [ { - "name": "account", - "type": "address", - "internalType": "address" - } - ] + name: 'account', + type: 'address', + internalType: 'address', + }, + ], }, { - "type": "error", - "name": "ReentrancyGuardReentrantCall", - "inputs": [] + type: 'error', + name: 'ReentrancyGuardReentrantCall', + inputs: [], }, { - "type": "error", - "name": "SafeERC20FailedOperation", - "inputs": [ + type: 'error', + name: 'SafeERC20FailedOperation', + inputs: [ { - "name": "token", - "type": "address", - "internalType": "address" - } - ] + name: 'token', + type: 'address', + internalType: 'address', + }, + ], }, { - "type": "error", - "name": "ZeroAddress", - "inputs": [] + type: 'error', + name: 'ZeroAddress', + inputs: [], }, { - "type": "error", - "name": "ZeroAmount", - "inputs": [] - } + type: 'error', + name: 'ZeroAmount', + inputs: [], + }, ] as const; /** diff --git a/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts b/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts index 3038898066..bd9026f383 100644 --- a/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts +++ b/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts @@ -6,13 +6,15 @@ import { useQuery } from '@tanstack/react-query'; import { Address } from 'viem'; -import { useChainId } from 'wagmi'; +import { useAccount, useChainId } from 'wagmi'; import { executeEnvioGraphQL, gql, EnvioNetwork, + getEnvioNetworkFromChainId, } from '../../utils/executeEnvioGraphQL'; import { useEnvioHealthCheckByChainId } from '../../utils/checkEnvioHealth'; +import useNetworkStore from '../../context/useNetworkStore'; // Request status enum export type RequestStatus = 'PENDING' | 'READY' | 'EXECUTED' | 'CANCELLED'; @@ -278,22 +280,26 @@ export const useDelegator = ( ) => { const { network, enabled = true } = options ?? {}; const chainId = useChainId(); + const { isConnected } = useAccount(); + const networkChainId = useNetworkStore((store) => store.network2?.evmChainId); + const activeChainId = isConnected ? chainId : (networkChainId ?? chainId); + const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId); // Check if indexer is healthy before querying const { data: isIndexerHealthy, isLoading: isCheckingHealth } = - useEnvioHealthCheckByChainId(chainId); + useEnvioHealthCheckByChainId(activeChainId); const healthCheckComplete = !isCheckingHealth; const shouldQuery = healthCheckComplete && isIndexerHealthy === true; const queryResult = useQuery({ - queryKey: ['envio', 'delegator', address, network], + queryKey: ['envio', 'delegator', address, resolvedNetwork], queryFn: async () => { if (!address) return null; const result = await executeEnvioGraphQL< DelegatorQueryResult, { id: string } - >(DELEGATOR_QUERY, { id: address.toLowerCase() }, network); + >(DELEGATOR_QUERY, { id: address.toLowerCase() }, resolvedNetwork); return result.data.Delegator_by_pk ? parseDelegator(result.data.Delegator_by_pk) : null; @@ -412,14 +418,19 @@ export const useDelegatorCount = (options?: { enabled?: boolean; }) => { const { network, enabled = true } = options ?? {}; + const chainId = useChainId(); + const { isConnected } = useAccount(); + const networkChainId = useNetworkStore((store) => store.network2?.evmChainId); + const activeChainId = isConnected ? chainId : (networkChainId ?? chainId); + const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId); return useQuery({ - queryKey: ['envio', 'delegatorCount', network], + queryKey: ['envio', 'delegatorCount', resolvedNetwork], queryFn: async () => { const result = await executeEnvioGraphQL< DelegatorCountQueryResult, Record - >(DELEGATOR_COUNT_QUERY, {}, network); + >(DELEGATOR_COUNT_QUERY, {}, resolvedNetwork); return result.data.Delegator?.length ?? 0; }, enabled, diff --git a/libs/tangle-shared-ui/src/data/graphql/useOperators.ts b/libs/tangle-shared-ui/src/data/graphql/useOperators.ts index 1daf6c3db6..713d288814 100644 --- a/libs/tangle-shared-ui/src/data/graphql/useOperators.ts +++ b/libs/tangle-shared-ui/src/data/graphql/useOperators.ts @@ -5,11 +5,14 @@ import { useQuery } from '@tanstack/react-query'; import { Address } from 'viem'; +import { useAccount, useChainId } from 'wagmi'; import { executeEnvioGraphQL, gql, EnvioNetwork, + getEnvioNetworkFromChainId, } from '../../utils/executeEnvioGraphQL'; +import useNetworkStore from '../../context/useNetworkStore'; // Operator status enum matching the Envio schema export type RestakingOperatorStatus = 'ACTIVE' | 'LEAVING' | 'INACTIVE'; @@ -170,10 +173,15 @@ export const useOperators = (options?: { offset = 0, enabled = true, } = options ?? {}; + const chainId = useChainId(); + const { isConnected } = useAccount(); + const networkChainId = useNetworkStore((store) => store.network2?.evmChainId); + const activeChainId = isConnected ? chainId : (networkChainId ?? chainId); + const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId); return useQuery({ - queryKey: ['envio', 'operators', network, status, limit, offset], - queryFn: () => fetchOperators(network, status, limit, offset), + queryKey: ['envio', 'operators', resolvedNetwork, status, limit, offset], + queryFn: () => fetchOperators(resolvedNetwork, status, limit, offset), enabled, staleTime: 30_000, // 30 seconds refetchInterval: 30_000, @@ -188,12 +196,17 @@ export const useOperatorMap = (options?: { enabled?: boolean; }) => { const { network, status, enabled = true } = options ?? {}; + const chainId = useChainId(); + const { isConnected } = useAccount(); + const networkChainId = useNetworkStore((store) => store.network2?.evmChainId); + const activeChainId = isConnected ? chainId : (networkChainId ?? chainId); + const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId); return useQuery({ - queryKey: ['envio', 'operatorMap', network, status], + queryKey: ['envio', 'operatorMap', resolvedNetwork, status], queryFn: async () => { // Use limit/offset for Hasura pagination - const operators = await fetchOperators(network, status, 1000, 0); + const operators = await fetchOperators(resolvedNetwork, status, 1000, 0); const map = new Map(); for (const op of operators) { map.set(op.id as Address, op); @@ -216,6 +229,11 @@ export const useOperator = ( }, ) => { const { network, enabled = true } = options ?? {}; + const chainId = useChainId(); + const { isConnected } = useAccount(); + const networkChainId = useNetworkStore((store) => store.network2?.evmChainId); + const activeChainId = isConnected ? chainId : (networkChainId ?? chainId); + const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId); const OPERATOR_QUERY = gql` query Operator($id: String!) { @@ -236,13 +254,13 @@ export const useOperator = ( `; return useQuery({ - queryKey: ['envio', 'operator', address, network], + queryKey: ['envio', 'operator', address, resolvedNetwork], queryFn: async () => { if (!address) return null; const result = await executeEnvioGraphQL< { Operator_by_pk: OperatorsQueryResult['Operator'][number] | null }, { id: string } - >(OPERATOR_QUERY, { id: address.toLowerCase() }, network); + >(OPERATOR_QUERY, { id: address.toLowerCase() }, resolvedNetwork); return result.data.Operator_by_pk ? parseOperator(result.data.Operator_by_pk) diff --git a/libs/tangle-shared-ui/src/data/graphql/useRestakingRound.ts b/libs/tangle-shared-ui/src/data/graphql/useRestakingRound.ts index ce240a5127..83b105934e 100644 --- a/libs/tangle-shared-ui/src/data/graphql/useRestakingRound.ts +++ b/libs/tangle-shared-ui/src/data/graphql/useRestakingRound.ts @@ -4,11 +4,14 @@ */ import { useQuery } from '@tanstack/react-query'; +import { useAccount, useChainId } from 'wagmi'; import { executeEnvioGraphQL, gql, EnvioNetwork, + getEnvioNetworkFromChainId, } from '../../utils/executeEnvioGraphQL'; +import useNetworkStore from '../../context/useNetworkStore'; // Restaking round type export interface RestakingRound { @@ -68,10 +71,15 @@ export const useRestakingRound = (options?: { enabled?: boolean; }) => { const { network, enabled = true } = options ?? {}; + const chainId = useChainId(); + const { isConnected } = useAccount(); + const networkChainId = useNetworkStore((store) => store.network2?.evmChainId); + const activeChainId = isConnected ? chainId : (networkChainId ?? chainId); + const resolvedNetwork = network ?? getEnvioNetworkFromChainId(activeChainId); return useQuery({ - queryKey: ['envio', 'restakingRound', network], - queryFn: () => fetchCurrentRound(network), + queryKey: ['envio', 'restakingRound', resolvedNetwork], + queryFn: () => fetchCurrentRound(resolvedNetwork), enabled, staleTime: 30_000, // 30 seconds refetchInterval: 60_000, // Refetch every minute From 5bc8d8cf5dc7015d1c939c2d6973866e928a1580 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 29 Dec 2025 13:54:59 +0700 Subject: [PATCH 22/31] fix: simplify proof polling progress message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove attempt counter from the progress message to provide a cleaner user experience during proof generation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/pages/claim/migration/hooks/useGenerateProof.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tangle-dapp/src/pages/claim/migration/hooks/useGenerateProof.ts b/apps/tangle-dapp/src/pages/claim/migration/hooks/useGenerateProof.ts index 40c8332eb4..6a1686c9fb 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/hooks/useGenerateProof.ts +++ b/apps/tangle-dapp/src/pages/claim/migration/hooks/useGenerateProof.ts @@ -197,7 +197,7 @@ async function pollForProof( for (let attempt = 0; attempt < maxAttempts; attempt++) { await new Promise((resolve) => setTimeout(resolve, interval)); - setProgress(`Waiting for proof... (${attempt + 1}/${maxAttempts})`); + setProgress(`Waiting for proof...`); const response = await fetch(`${PROVER_API_URL}/status/${jobId}`); const result = await response.json(); From 65c30f90731b1570ec4a803cbe64a670482e49e7 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 29 Dec 2025 14:44:37 +0700 Subject: [PATCH 23/31] fix: remove spinner from proof progress indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/tangle-dapp/src/pages/claim/migration/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/tangle-dapp/src/pages/claim/migration/index.tsx b/apps/tangle-dapp/src/pages/claim/migration/index.tsx index bc7972856f..544ee5f916 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/index.tsx +++ b/apps/tangle-dapp/src/pages/claim/migration/index.tsx @@ -783,7 +783,6 @@ const MigrationClaimPage: FC = () => { > {proofProgress && (
-
{proofProgress} From c6e5258d443f0b54e82525c6f3332a8826a10550 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 29 Dec 2025 21:18:46 +0700 Subject: [PATCH 24/31] fix: use on-chain data as source of truth for restake pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Prefer on-chain data over indexer for pending unstake and withdraw requests - Derive selectedAssetItem from depositedAssets in delegate page to ensure it updates when metadata loads 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/pages/restake/delegate/index.tsx | 24 +++++---- .../src/pages/restake/unstake/index.tsx | 46 +++++++++------- .../src/pages/restake/withdraw/index.tsx | 54 ++++++++++--------- 3 files changed, 70 insertions(+), 54 deletions(-) diff --git a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx index 7d47a13843..d53ac1b8ef 100644 --- a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx @@ -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'; @@ -269,10 +269,6 @@ const RestakeDelegateForm: FC = () => { update: updateOperatorModal, } = useModal(false); - const [selectedAssetItem, setSelectedAssetItem] = useState( - null, - ); - const depositedAssets = useMemo(() => { if (!restakeAssets || !depositMap) { return []; @@ -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], @@ -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>( diff --git a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx b/apps/tangle-dapp/src/pages/restake/unstake/index.tsx index b570ed62f4..eda7c02db9 100644 --- a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/unstake/index.tsx @@ -396,31 +396,39 @@ const RestakeUnstakeForm: FC = () => { tokenMetadatas, ]); - // Get pending unstake requests + // Get pending unstake requests (prefer on-chain data as source of truth, fall back to indexer) const unstakeRequests = useMemo(() => { - if (delegator?.unstakeRequests && delegator.unstakeRequests.length > 0) { - return delegator.unstakeRequests.filter((r) => r.status === 'PENDING'); + // On-chain data is the source of truth - use it when available + if ( + Array.isArray(onChainPendingUnstakes) && + onChainPendingUnstakes.length > 0 + ) { + const delay = protocolConfig?.delegationBondLessDelay ?? BigInt(0); + return (onChainPendingUnstakes as OnChainPendingUnstake[]).map( + (r, idx) => ({ + id: `${r.operator.toLowerCase()}-${r.asset.token.toLowerCase()}-${r.requestedRound.toString()}-${idx}`, + operatorId: r.operator, + token: r.asset.token, + nonce: BigInt(0), + shares: r.shares, + estimatedAmount: r.shares, + requestedRound: r.requestedRound, + readyAtRound: r.requestedRound + delay, + status: 'PENDING' as const, + executedAt: null, + }), + ) satisfies UnstakeRequest[]; } - if (!onChainPendingUnstakes) { + // If on-chain returns empty array, it means no pending requests (source of truth) + if (Array.isArray(onChainPendingUnstakes)) { return []; } - const delay = protocolConfig?.delegationBondLessDelay ?? BigInt(0); - return (onChainPendingUnstakes as OnChainPendingUnstake[]).map( - (r, idx) => ({ - id: `${r.operator.toLowerCase()}-${r.asset.token.toLowerCase()}-${r.requestedRound.toString()}-${idx}`, - operatorId: r.operator, - token: r.asset.token, - nonce: BigInt(0), - shares: r.shares, - estimatedAmount: r.shares, - requestedRound: r.requestedRound, - readyAtRound: r.requestedRound + delay, - status: 'PENDING' as const, - executedAt: null, - }), - ) satisfies UnstakeRequest[]; + // Fall back to indexer only when on-chain data is not yet available + return ( + delegator?.unstakeRequests.filter((r) => r.status === 'PENDING') ?? [] + ); }, [ delegator?.unstakeRequests, onChainPendingUnstakes, diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx index d311d4a293..a1a498b1db 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx @@ -346,37 +346,41 @@ const RestakeWithdrawForm: FC = () => { }, }); - // Get pending withdraw requests (prefer indexer, but fall back to on-chain when it lags) + // Get pending withdraw requests (prefer on-chain data as source of truth, fall back to indexer) const withdrawRequests = useMemo(() => { - const fromIndexer = - delegator?.withdrawRequests.filter((r) => r.status === 'PENDING') ?? []; - if (fromIndexer.length > 0) { - return fromIndexer; + // On-chain data is the source of truth - use it when available + if ( + Array.isArray(onChainPendingWithdrawals) && + onChainPendingWithdrawals.length > 0 + ) { + const delayRounds = protocolConfig?.leaveDelegatorsDelay ?? BigInt(0); + return ( + onChainPendingWithdrawals as Array<{ + asset: { kind: number; token: Address }; + amount: bigint; + requestedRound: bigint; + }> + ).map((r, idx) => ({ + id: `${r.asset.token.toLowerCase()}-${r.requestedRound.toString()}-${idx}`, + token: r.asset.token, + nonce: BigInt(0), + amount: r.amount, + requestedRound: r.requestedRound, + readyAtRound: r.requestedRound + delayRounds, + status: 'PENDING' as const, + executedAt: null, + })) satisfies WithdrawRequest[]; } - if (!onChainPendingWithdrawals) { + // If on-chain returns empty array, it means no pending requests (source of truth) + if (Array.isArray(onChainPendingWithdrawals)) { return []; } - const delayRounds = protocolConfig?.leaveDelegatorsDelay ?? BigInt(0); - const requests = ( - onChainPendingWithdrawals as Array<{ - asset: { kind: number; token: Address }; - amount: bigint; - requestedRound: bigint; - }> - ).map((r, idx) => ({ - id: `${r.asset.token.toLowerCase()}-${r.requestedRound.toString()}-${idx}`, - token: r.asset.token, - nonce: BigInt(0), - amount: r.amount, - requestedRound: r.requestedRound, - readyAtRound: r.requestedRound + delayRounds, - status: 'PENDING' as const, - executedAt: null, - })) satisfies WithdrawRequest[]; - - return requests; + // Fall back to indexer only when on-chain data is not yet available + return ( + delegator?.withdrawRequests.filter((r) => r.status === 'PENDING') ?? [] + ); }, [ delegator?.withdrawRequests, onChainPendingWithdrawals, From f93cb2748b19c2f91dc0ccee5b0517de60d2995c Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 29 Dec 2025 21:56:07 +0700 Subject: [PATCH 25/31] refactor: rename unstake to undelegate for consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed all unstake-related terminology to undelegate throughout the restaking codebase for clearer naming. This includes renaming the unstake directory, types, hooks, and variables. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../restaking/RestakeOverviewTabs.tsx | 4 +- .../restaking/RestakeTabContent.tsx | 4 +- .../data/restaking/useUserRestakingStats.ts | 14 +-- .../{unstake => undelegate}/Details.tsx | 4 +- .../restake/{unstake => undelegate}/index.tsx | 118 +++++++++--------- .../src/pages/restake/withdraw/index.tsx | 2 +- apps/tangle-dapp/src/types/restake.ts | 2 +- .../src/data/graphql/index.ts | 6 +- .../src/data/graphql/useDelegator.ts | 10 +- .../src/data/graphql/useProtocolConfig.ts | 4 +- libs/tangle-shared-ui/src/data/tx/index.ts | 8 +- .../src/data/tx/useUndelegateTx.ts | 26 ++-- 12 files changed, 101 insertions(+), 101 deletions(-) rename apps/tangle-dapp/src/pages/restake/{unstake => undelegate}/Details.tsx (92%) rename apps/tangle-dapp/src/pages/restake/{unstake => undelegate}/index.tsx (90%) diff --git a/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx b/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx index ad21b34b69..636c3e6dfb 100644 --- a/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx +++ b/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx @@ -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'; @@ -87,7 +87,7 @@ const RestakeOverviewTabs: FC = ({ ) : action === RestakeAction.DELEGATE ? ( ) : action === RestakeAction.UNDELEGATE ? ( - + ) : null} diff --git a/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx b/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx index f9b2084aa3..20230d1ef6 100644 --- a/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx +++ b/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx @@ -4,7 +4,7 @@ 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'; @@ -76,7 +76,7 @@ const RestakeTabContentInner: FC = ({ tab }) => { case RestakeAction.DELEGATE: return ; case RestakeAction.UNDELEGATE: - return ; + return ; case RestakeTab.VAULTS: return ( { } } - // 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; } } @@ -127,7 +127,7 @@ const useUserRestakingStats = () => { totalDelegated: delegator.totalDelegated, withdrawQueueAmount, withdrawableAmount, - pendingUnstakeAmount, + pendingUndelegateAmount, pendingRewards, activeBalance, formatted: { @@ -135,7 +135,7 @@ const useUserRestakingStats = () => { totalDelegated: format(delegator.totalDelegated), withdrawQueueAmount: format(withdrawQueueAmount), withdrawableAmount: format(withdrawableAmount), - pendingUnstakeAmount: format(pendingUnstakeAmount), + pendingUndelegateAmount: format(pendingUndelegateAmount), pendingRewards: format(pendingRewards), activeBalance: format(activeBalance), }, diff --git a/apps/tangle-dapp/src/pages/restake/unstake/Details.tsx b/apps/tangle-dapp/src/pages/restake/undelegate/Details.tsx similarity index 92% rename from apps/tangle-dapp/src/pages/restake/unstake/Details.tsx rename to apps/tangle-dapp/src/pages/restake/undelegate/Details.tsx index f06e4ab097..06d1a92bd2 100644 --- a/apps/tangle-dapp/src/pages/restake/unstake/Details.tsx +++ b/apps/tangle-dapp/src/pages/restake/undelegate/Details.tsx @@ -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; } @@ -25,7 +25,7 @@ const Details: FC = () => { ); diff --git a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx b/apps/tangle-dapp/src/pages/restake/undelegate/index.tsx similarity index 90% rename from apps/tangle-dapp/src/pages/restake/unstake/index.tsx rename to apps/tangle-dapp/src/pages/restake/undelegate/index.tsx index eda7c02db9..e83cac8a7b 100644 --- a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/undelegate/index.tsx @@ -26,7 +26,7 @@ import RestakeDetailCard from '../../../components/RestakeDetailCard'; import ActionButtonBase from '../../../components/restaking/ActionButtonBase'; import { SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS } from '../../../constants/restake'; import useActiveTypedChainId from '../../../hooks/useActiveTypedChainId'; -import type { EvmUnstakeFormFields } from '../../../types/restake'; +import type { EvmUndelegateFormFields } from '../../../types/restake'; import decimalsToStep from '../../../utils/decimalsToStep'; import { AnimatedTable } from '../AnimatedTable'; import AssetPlaceholder from '../AssetPlaceholder'; @@ -40,11 +40,11 @@ import Details from './Details'; import { useDelegator, useProtocolConfig, - type UnstakeRequest, + type UndelegateRequest, } from '@tangle-network/tangle-shared-ui/data/graphql'; import { - useExecuteUnstakeTx, - useScheduleUnstakeTx, + useExecuteUndelegateTx, + useScheduleUndelegateTx, } from '@tangle-network/tangle-shared-ui/data/tx'; import { TxStatus } from '@tangle-network/tangle-shared-ui/hooks/useContractWrite'; import { useEvmAssetMetadatas } from '@tangle-network/tangle-shared-ui/hooks/useEvmAssetMetadatas'; @@ -73,10 +73,10 @@ type DelegationItem = { availableToUnstake: bigint; // amount available to schedule (safe max at current exchange rate) }; -const RestakeUnstakeForm: FC = () => { +const RestakeUndelegateForm: FC = () => { const { address: userAddress } = useAccount(); const chainId = useChainId(); - const [isUnstakeRequestTableOpen, setIsUnstakeRequestTableOpen] = + const [isUndelegateRequestTableOpen, setIsUndelegateRequestTableOpen] = useState(false); const { @@ -86,7 +86,7 @@ const RestakeUnstakeForm: FC = () => { setValue, formState: { errors, isValid, isSubmitting }, watch, - } = useForm({ + } = useForm({ mode: 'onChange', }); @@ -242,7 +242,7 @@ const RestakeUnstakeForm: FC = () => { return Array.from(byKey.values()); }, [delegator?.delegations, onChainDelegations]); - const pendingUnstakeSharesByDelegationKey = useMemo(() => { + const pendingUndelegateSharesByDelegationKey = useMemo(() => { const map = new Map(); if (delegator?.unstakeRequests && delegator.unstakeRequests.length > 0) { @@ -264,7 +264,7 @@ const RestakeUnstakeForm: FC = () => { } return map; - }, [delegator?.unstakeRequests, onChainPendingUnstakes]); + }, [delegator?.unstakeRequests, onChainPendingUnstakes]); // Note: delegator.unstakeRequests is from GraphQL schema const operatorAddresses = useMemo(() => { if (delegationsForUi.length === 0) return []; @@ -352,15 +352,15 @@ const RestakeUnstakeForm: FC = () => { if (!metadata) return null; - // Calculate available to unstake (shares minus pending unstakes) - const pendingUnstakes = - pendingUnstakeSharesByDelegationKey.get( + // Calculate available to undelegate (shares minus pending undelegates) + const pendingUndelegates = + pendingUndelegateSharesByDelegationKey.get( `${delegation.operatorId.toLowerCase()}-${delegation.token.toLowerCase()}`, ) ?? BigInt(0); const availableShares = - delegation.shares > pendingUnstakes - ? delegation.shares - pendingUnstakes + delegation.shares > pendingUndelegates + ? delegation.shares - pendingUndelegates : BigInt(0); // Only show delegations with available shares @@ -391,13 +391,13 @@ const RestakeUnstakeForm: FC = () => { .filter((item): item is DelegationItem => item !== null); }, [ delegationsForUi, - pendingUnstakeSharesByDelegationKey, + pendingUndelegateSharesByDelegationKey, operatorPoolMap, tokenMetadatas, ]); - // Get pending unstake requests (prefer on-chain data as source of truth, fall back to indexer) - const unstakeRequests = useMemo(() => { + // Get pending undelegate requests (prefer on-chain data as source of truth, fall back to indexer) + const undelegateRequests = useMemo(() => { // On-chain data is the source of truth - use it when available if ( Array.isArray(onChainPendingUnstakes) && @@ -417,7 +417,7 @@ const RestakeUnstakeForm: FC = () => { status: 'PENDING' as const, executedAt: null, }), - ) satisfies UnstakeRequest[]; + ) satisfies UndelegateRequest[]; } // If on-chain returns empty array, it means no pending requests (source of truth) @@ -425,7 +425,7 @@ const RestakeUnstakeForm: FC = () => { return []; } - // Fall back to indexer only when on-chain data is not yet available + // Fall back to indexer only when on-chain data is not yet available (note: unstakeRequests is GraphQL field name) return ( delegator?.unstakeRequests.filter((r) => r.status === 'PENDING') ?? [] ); @@ -483,15 +483,15 @@ const RestakeUnstakeForm: FC = () => { : undefined; })(); - const { execute: executeScheduleUnstake, status: unstakeTxStatus } = - useScheduleUnstakeTx(); + const { execute: executeScheduleUndelegate, status: undelegateTxStatus } = + useScheduleUndelegateTx(); - const isTransacting = isSubmitting || unstakeTxStatus === TxStatus.PROCESSING; + const isTransacting = isSubmitting || undelegateTxStatus === TxStatus.PROCESSING; const isReady = !isTransacting && selectedDelegation !== null && - executeScheduleUnstake !== null; + executeScheduleUndelegate !== null; const resetForm = useCallback(() => { setValue('amount', '', { shouldValidate: false }); @@ -514,7 +514,7 @@ const RestakeUnstakeForm: FC = () => { refetchOperatorRewardPools, ]); - const onSubmit = useCallback>( + const onSubmit = useCallback>( async ({ amount, assetId, operatorAddress }) => { if (!isReady || !selectedDelegation) { return; @@ -523,7 +523,7 @@ const RestakeUnstakeForm: FC = () => { const amountBigInt = parseUnits(amount, selectedDelegation.tokenDecimals); try { - await executeScheduleUnstake({ + await executeScheduleUndelegate({ operator: operatorAddress, token: assetId, amount: amountBigInt, @@ -535,7 +535,7 @@ const RestakeUnstakeForm: FC = () => { } }, [ - executeScheduleUnstake, + executeScheduleUndelegate, isReady, refreshAll, resetForm, @@ -559,11 +559,11 @@ const RestakeUnstakeForm: FC = () => { - {!isUnstakeRequestTableOpen && ( + {!isUndelegateRequestTableOpen && ( setIsUnstakeRequestTableOpen(true)} + tooltipContent="Undelegate requests" + onClick={() => setIsUndelegateRequestTableOpen(true)} /> )} @@ -690,7 +690,7 @@ const RestakeUnstakeForm: FC = () => { isLoading={isTransacting || isLoading} loadingText={loadingText} > - {displayError ?? 'Schedule Unstake'} + {displayError ?? 'Schedule Undelegate'} ); }} @@ -700,21 +700,21 @@ const RestakeUnstakeForm: FC = () => {
- setIsUnstakeRequestTableOpen(false)} + onClose={() => setIsUndelegateRequestTableOpen(false)} onRefresh={refreshAll} /> - setIsUnstakeRequestTableOpen(false)} + onClose={() => setIsUndelegateRequestTableOpen(false)} onRefresh={refreshAll} className="md:hidden" /> @@ -724,9 +724,9 @@ const RestakeUnstakeForm: FC = () => { isOpen={isDelegationModalOpen} setIsOpen={setIsDelegationModalOpen} titleWhenEmpty="No Delegations Found" - descriptionWhenEmpty="You don't have any active delegations to unstake." + descriptionWhenEmpty="You don't have any active delegations to undelegate." items={delegationItems} - searchInputId="restake-unstake-delegation-search" + searchInputId="restake-undelegate-delegation-search" searchPlaceholder="Search delegations..." getItemKey={(item) => item.id} onSelect={handleDelegationSelect} @@ -775,11 +775,11 @@ const RestakeUnstakeForm: FC = () => { ); }; -export default RestakeUnstakeForm; +export default RestakeUndelegateForm; -// Unstake requests view component -type UnstakeRequestsViewProps = { - unstakeRequests: UnstakeRequest[]; +// Undelegate requests view component +type UndelegateRequestsViewProps = { + undelegateRequests: UndelegateRequest[]; tokenMetadatas: | Array<{ id: EvmAddress; symbol: string; decimals: number }> | undefined; @@ -788,8 +788,8 @@ type UnstakeRequestsViewProps = { className?: string; }; -const UnstakeRequestsView: FC = ({ - unstakeRequests, +const UndelegateRequestsView: FC = ({ + undelegateRequests, tokenMetadatas, onClose, onRefresh, @@ -797,36 +797,36 @@ const UnstakeRequestsView: FC = ({ }) => { const { data: config } = useProtocolConfig(); const currentRound = config?.currentRound ?? BigInt(0); - const readyCount = unstakeRequests.filter( + const readyCount = undelegateRequests.filter( (r) => r.readyAtRound <= currentRound, ).length; - const { execute: executeUnstake, status: executeStatus } = - useExecuteUnstakeTx(); + const { execute: executeUndelegate, status: executeStatus } = + useExecuteUndelegateTx(); const isExecuting = executeStatus === TxStatus.PROCESSING; return ( -
+
0 - ? 'Unstake Requests' - : 'No Unstake Requests' + undelegateRequests.length > 0 + ? 'Undelegate Requests' + : 'No Undelegate Requests' } />
- {unstakeRequests.length > 0 && ( + {undelegateRequests.length > 0 && (
- {unstakeRequests.length > 0 ? ( + {undelegateRequests.length > 0 ? (
- {unstakeRequests.map((request) => { + {undelegateRequests.map((request) => { const metadata = tokenMetadatas?.find( (m) => m.id.toLowerCase() === request.token.toLowerCase(), ); @@ -885,7 +885,7 @@ const UnstakeRequestsView: FC = ({ variant="body1" className="text-mono-120 dark:text-mono-100" > - Your requests will appear here after scheduling an unstake. Requests + Your requests will appear here after scheduling an undelegate. Requests can be executed after the waiting period. )} diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx index a1a498b1db..e14451ad6c 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx @@ -781,7 +781,7 @@ const WithdrawRequestView: FC = ({ return ( -
+
0 diff --git a/apps/tangle-dapp/src/types/restake.ts b/apps/tangle-dapp/src/types/restake.ts index 232f2b76c2..1fcba73ae8 100644 --- a/apps/tangle-dapp/src/types/restake.ts +++ b/apps/tangle-dapp/src/types/restake.ts @@ -58,7 +58,7 @@ export type EvmDelegationFormFields = { assetId: Address; }; -export type EvmUnstakeFormFields = EvmDelegationFormFields; +export type EvmUndelegateFormFields = EvmDelegationFormFields; export type EvmWithdrawFormFields = { amount: string; diff --git a/libs/tangle-shared-ui/src/data/graphql/index.ts b/libs/tangle-shared-ui/src/data/graphql/index.ts index a2ba2a3453..2af2a1f8e6 100644 --- a/libs/tangle-shared-ui/src/data/graphql/index.ts +++ b/libs/tangle-shared-ui/src/data/graphql/index.ts @@ -18,13 +18,13 @@ export { useDelegatorDeposits, useDelegatorDelegations, useDelegatorWithdrawRequests, - useDelegatorUnstakeRequests, + useDelegatorUndelegateRequests, useDelegatorCount, type Delegator, type DelegatorAssetPosition, type DelegationPosition, type WithdrawRequest, - type UnstakeRequest, + type UndelegateRequest, type RequestStatus, type LockDuration, type BlueprintSelectionMode, @@ -65,7 +65,7 @@ export { export { useProtocolConfig, useWithdrawalDelay, - useUnstakeDelay, + useUndelegateDelay, type ProtocolConfig, } from './useProtocolConfig'; diff --git a/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts b/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts index bd9026f383..e21a344e33 100644 --- a/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts +++ b/libs/tangle-shared-ui/src/data/graphql/useDelegator.ts @@ -65,8 +65,8 @@ export interface WithdrawRequest { executedAt: bigint | null; } -// Unstake request -export interface UnstakeRequest { +// Undelegate request (mapped from GraphQL unstakeRequests) +export interface UndelegateRequest { id: string; operatorId: Address; token: Address; @@ -92,7 +92,7 @@ export interface Delegator { assetPositions: DelegatorAssetPosition[]; delegations: DelegationPosition[]; withdrawRequests: WithdrawRequest[]; - unstakeRequests: UnstakeRequest[]; + unstakeRequests: UndelegateRequest[]; } // GraphQL query for delegator (Hasura uses _by_pk for single row queries) @@ -383,8 +383,8 @@ export const useDelegatorWithdrawRequests = ( }; }; -// Hook to get delegator's pending unstake requests -export const useDelegatorUnstakeRequests = ( +// Hook to get delegator's pending undelegate requests +export const useDelegatorUndelegateRequests = ( address: Address | undefined, options?: { network?: EnvioNetwork; diff --git a/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts b/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts index 46918b68c9..dc2ab5205d 100644 --- a/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts +++ b/libs/tangle-shared-ui/src/data/graphql/useProtocolConfig.ts @@ -244,9 +244,9 @@ export const useWithdrawalDelay = () => { }; /** - * Hook to get unstake delay in human-readable format. + * Hook to get undelegate delay in human-readable format. */ -export const useUnstakeDelay = () => { +export const useUndelegateDelay = () => { const { data: config, isLoading } = useProtocolConfig(); if (isLoading || !config) { diff --git a/libs/tangle-shared-ui/src/data/tx/index.ts b/libs/tangle-shared-ui/src/data/tx/index.ts index 175ce7e1c2..ad075ae58f 100644 --- a/libs/tangle-shared-ui/src/data/tx/index.ts +++ b/libs/tangle-shared-ui/src/data/tx/index.ts @@ -9,11 +9,11 @@ export { useDepositTx, type DepositParams } from './useDepositTx'; // Delegate export { useDelegateTx, type DelegateParams } from './useDelegateTx'; -// Undelegate (unstake) +// Undelegate export { - useScheduleUnstakeTx, - useExecuteUnstakeTx, - type ScheduleUnstakeParams, + useScheduleUndelegateTx, + useExecuteUndelegateTx, + type ScheduleUndelegateParams, } from './useUndelegateTx'; // Withdraw diff --git a/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts b/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts index 4af904e597..de8d798b00 100644 --- a/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts +++ b/libs/tangle-shared-ui/src/data/tx/useUndelegateTx.ts @@ -1,5 +1,5 @@ /** - * Hooks for undelegating (scheduling unstake) from an operator. + * Hooks for undelegating from an operator. * Replaces the Substrate-based useRestakeUndelegateTx hook. */ @@ -9,20 +9,20 @@ import MULTI_ASSET_DELEGATION_ABI from '../../abi/multiAssetDelegation'; import { getContractsByChainId } from '@tangle-network/dapp-config/contracts'; import { useChainId } from 'wagmi'; -export interface ScheduleUnstakeParams { +export interface ScheduleUndelegateParams { operator: Address; token: Address; amount: bigint; } /** - * Hook to schedule unstaking shares from an operator. - * This initiates the unstaking process - shares will be available - * for withdrawal after the unstaking delay (in rounds). + * Hook to schedule undelegation from an operator. + * This initiates the undelegation process - shares will be available + * for withdrawal after the undelegation delay (in rounds). * * @example * ```tsx - * const { execute, status, error } = useScheduleUnstakeTx(); + * const { execute, status, error } = useScheduleUndelegateTx(); * * await execute({ * operator: '0x...', @@ -31,13 +31,13 @@ export interface ScheduleUnstakeParams { * }); * ``` */ -export const useScheduleUnstakeTx = () => { +export const useScheduleUndelegateTx = () => { const chainId = useChainId(); const contracts = getContractsByChainId(chainId); return useContractWrite( MULTI_ASSET_DELEGATION_ABI, - (params: ScheduleUnstakeParams, _activeAddress) => ({ + (params: ScheduleUndelegateParams, _activeAddress) => ({ address: contracts.multiAssetDelegation, abi: MULTI_ASSET_DELEGATION_ABI, functionName: 'scheduleDelegatorUnstake' as const, @@ -51,16 +51,16 @@ export const useScheduleUnstakeTx = () => { ['Token', params.token], ['Amount', params.amount.toString()], ]), - getSuccessMessage: () => 'Successfully scheduled unstake', + getSuccessMessage: () => 'Successfully scheduled undelegate', }, ); }; /** - * Hook to execute a scheduled unstake after the delay period. + * Hook to execute a scheduled undelegate after the delay period. * Call this after the readyAtRound has been reached. */ -export const useExecuteUnstakeTx = () => { +export const useExecuteUndelegateTx = () => { const chainId = useChainId(); const contracts = getContractsByChainId(chainId); @@ -74,9 +74,9 @@ export const useExecuteUnstakeTx = () => { }), { txName: 'restake execute undelegate', - getSuccessMessage: () => 'Successfully executed unstake', + getSuccessMessage: () => 'Successfully executed undelegate', }, ); }; -export default useScheduleUnstakeTx; +export default useScheduleUndelegateTx; From 8b2fbf5ee0c61fa9864f02ac3f1cdb1d42cee178 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 29 Dec 2025 22:22:36 +0700 Subject: [PATCH 26/31] fix: use on-chain data for delegator in RestakeContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vaults tab was showing stale delegatedAmount values because RestakeContext only fetched from the GraphQL indexer. This fix adds useOnChainDelegator and merges its data with GraphQL, using on-chain as source of truth for asset positions (same pattern as useRestakingData used by Dashboard). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/context/RestakeContext.tsx | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/libs/tangle-shared-ui/src/context/RestakeContext.tsx b/libs/tangle-shared-ui/src/context/RestakeContext.tsx index 48355514bc..b9f9a293d5 100644 --- a/libs/tangle-shared-ui/src/context/RestakeContext.tsx +++ b/libs/tangle-shared-ui/src/context/RestakeContext.tsx @@ -31,6 +31,7 @@ import { type RestakeAssetMap, } from '../data/graphql/useRestakeAssets'; import { useDelegator, type Delegator } from '../data/graphql/useDelegator'; +import { useOnChainDelegator } from '../data/restake/useOnChainDelegator'; import { useOperatorMap, type Operator } from '../data/graphql/useOperators'; import { useProtocolConfig, @@ -146,13 +147,57 @@ export const RestakeProvider: FC = ({ children }) => { enabled: !isCheckingHealth, }); - // Fetch delegator info (user's positions and pending requests) + // Get token addresses from assets for on-chain delegator query + const tokenAddresses = useMemo(() => { + return assetList.map((asset) => asset.id); + }, [assetList]); + + // Fetch delegator info from GraphQL (for rich data like delegations, requests) const { - data: delegator, - isLoading: isLoadingDelegator, - refetch: refetchDelegator, + data: graphqlDelegator, + isLoading: isLoadingGraphqlDelegator, + refetch: refetchGraphqlDelegator, } = useDelegator(userAddress); + // ALWAYS fetch delegator deposit data from on-chain for accurate amounts + // The indexer may have stale delegatedAmount values + const { + data: onChainDelegator, + isLoading: isLoadingOnChainDelegator, + refetch: refetchOnChainDelegator, + } = useOnChainDelegator(userAddress, { + tokenAddresses, + enabled: !!userAddress && tokenAddresses.length > 0, + }); + + // Merge data: use on-chain for accurate deposit/delegation amounts, + // GraphQL for rich data (delegations, requests, etc.) + const delegator = useMemo(() => { + // If we have on-chain data, use it for asset positions (source of truth) + if (onChainDelegator) { + // If we also have GraphQL data, merge them + if (graphqlDelegator) { + return { + ...graphqlDelegator, + // Override with on-chain data for accurate amounts + assetPositions: onChainDelegator.assetPositions, + totalDeposited: onChainDelegator.totalDeposited, + totalDelegated: onChainDelegator.totalDelegated, + }; + } + // Only on-chain data available + return onChainDelegator; + } + return graphqlDelegator ?? null; + }, [onChainDelegator, graphqlDelegator]); + + const isLoadingDelegator = + isLoadingOnChainDelegator || isLoadingGraphqlDelegator; + + const refetchDelegator = useCallback(async () => { + await Promise.all([refetchOnChainDelegator(), refetchGraphqlDelegator()]); + }, [refetchOnChainDelegator, refetchGraphqlDelegator]); + // Fetch operators const { data: operatorMap, From 31f6c4413e29ef88bc4eed78f2c48523f482b2de Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Mon, 29 Dec 2025 22:42:43 +0700 Subject: [PATCH 27/31] fix: navigate to delegate flow when clicking delegate on operators tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously clicking the delegate button on an operator would incorrectly navigate to the deposit flow. Now it navigates to the delegate flow with the operator pre-selected via query parameter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/components/tables/OperatorsTable.tsx | 10 ++++------ .../containers/restaking/RestakeTabContent.tsx | 16 +++++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/tangle-dapp/src/components/tables/OperatorsTable.tsx b/apps/tangle-dapp/src/components/tables/OperatorsTable.tsx index 5b9e124abe..56d5ae1663 100644 --- a/apps/tangle-dapp/src/components/tables/OperatorsTable.tsx +++ b/apps/tangle-dapp/src/components/tables/OperatorsTable.tsx @@ -9,7 +9,7 @@ import { TableVariant } from '@tangle-network/ui-components/components/Table/typ interface Props { operatorMap: Map | null; isLoading: boolean; - onRestakeClicked: () => void; + onDelegateClicked: (operatorAddress: Address) => void; } const formatDelegationCount = (count: bigint | null | undefined): number => { @@ -20,7 +20,7 @@ const formatDelegationCount = (count: bigint | null | undefined): number => { export const OperatorsTable: FC = ({ operatorMap, isLoading, - onRestakeClicked, + onDelegateClicked, }) => { const data = useMemo(() => { if (!operatorMap) return []; @@ -49,18 +49,16 @@ export const OperatorsTable: FC = ({ 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 }) => (