From efa880944252b006aab55cc6b0cab05f1705070c Mon Sep 17 00:00:00 2001 From: Pavan Soratur Date: Fri, 15 Aug 2025 09:12:31 -0700 Subject: [PATCH 1/6] refactor: update form validation mode to onChange and add tangle address formatting --- .windsurfrules => .windsurf/rules/general.md | 6 +++++- .../src/pages/blueprints/[id]/page.tsx | 3 +-- apps/tangle-cloud/src/types/index.ts | 3 ++- .../src/components/AvatarWithText.tsx | 20 ++++++++++++++----- .../src/components/Lists/OperatorListItem.tsx | 18 +++++++++++++---- .../restaking/RestakeOverviewTabs.tsx | 3 +++ .../src/pages/restake/delegate/index.tsx | 2 +- .../src/pages/restake/deposit/DepositForm.tsx | 2 +- apps/tangle-dapp/src/pages/restake/index.tsx | 20 +++++++++++++------ .../src/pages/restake/unstake/index.tsx | 2 +- .../src/pages/restake/withdraw/index.tsx | 2 +- 11 files changed, 58 insertions(+), 23 deletions(-) rename .windsurfrules => .windsurf/rules/general.md (99%) diff --git a/.windsurfrules b/.windsurf/rules/general.md similarity index 99% rename from .windsurfrules rename to .windsurf/rules/general.md index 5702b5e6f4..2d3d71108a 100644 --- a/.windsurfrules +++ b/.windsurf/rules/general.md @@ -1,3 +1,7 @@ +--- +trigger: always_on +--- + # Tangle dApp Monorepo This project is Tangle dApp - a monorepo containing multiple dApp projects that serve as the frontend for our custom Substrate-based network/node 'Tangle'. Tangle is a cryptocurrency network created using the Substrate framework, which is part of the Polkadot ecosystem. Tangle is a layer 1 for on-demand services where developers can build and monetize decentralized services using Tangle Blueprints. They can also deploy innovative infrastructure in any blockchain ecosystem. @@ -61,4 +65,4 @@ Tangle dApp is the main product and focus of the monorepo. Here's the tech stack # Library: `libs/tangle-shared-ui` -Contains shared logic, hooks, and utility functions between multiple Tangle dApps, such as `tangle-cloud` & `tangle-dapp`. This differs from `ui-components` in that `ui-components` is more geared towards general & re-usable components, not necesarily tied to any context, whereas `libs/tangle-shared-ui` is specific to Tangle-related logic. +Contains shared logic, hooks, and utility functions between multiple Tangle dApps, such as `tangle-cloud` & `tangle-dapp`. This differs from `ui-components` in that `ui-components` is more geared towards general & re-usable components, not necesarily tied to any context, whereas `libs/tangle-shared-ui` is specific to Tangle-related logic. \ No newline at end of file diff --git a/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx b/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx index 53da0e4716..498fef7d1b 100644 --- a/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx +++ b/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx @@ -22,11 +22,10 @@ import { z } from 'zod'; const RestakeOperatorAction: FC> = ({ children, - address, }) => { return ( {children} diff --git a/apps/tangle-cloud/src/types/index.ts b/apps/tangle-cloud/src/types/index.ts index 65596cfd2d..cb6af682d3 100644 --- a/apps/tangle-cloud/src/types/index.ts +++ b/apps/tangle-cloud/src/types/index.ts @@ -14,8 +14,9 @@ export enum PagePath { } export enum TangleDAppPagePath { - RESTAKE_OPERATOR = `${TANGLE_DAPP_URL}restake/operators`, RESTAKE_DEPOSIT = `${TANGLE_DAPP_URL}restake?vault={{vault}}`, + RESTAKE_DELEGATE = `${TANGLE_DAPP_URL}restake/delegate`, + RESTAKE_OPERATOR = `${TANGLE_DAPP_URL}restake/operators`, } export type ApprovalConfirmationFormFields = { diff --git a/apps/tangle-dapp/src/components/AvatarWithText.tsx b/apps/tangle-dapp/src/components/AvatarWithText.tsx index e663c65491..0308e52f63 100644 --- a/apps/tangle-dapp/src/components/AvatarWithText.tsx +++ b/apps/tangle-dapp/src/components/AvatarWithText.tsx @@ -4,8 +4,10 @@ import { Avatar } from '@tangle-network/ui-components/components/Avatar'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { shortenHex } from '@tangle-network/ui-components/utils/shortenHex'; import { shortenString } from '@tangle-network/ui-components/utils/shortenString'; +import { toSubstrateAddress } from '@tangle-network/ui-components/utils/toSubstrateAddress'; +import useNetworkStore from '@tangle-network/tangle-shared-ui/context/useNetworkStore'; import isEqual from 'lodash/isEqual'; -import { type ComponentProps, memo, type ReactNode } from 'react'; +import { type ComponentProps, memo, type ReactNode, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; import { isHex } from 'viem'; @@ -26,6 +28,14 @@ const AvatarWithText = ({ overrideTypographyProps, ...props }: Props) => { + const ss58Prefix = useNetworkStore((store) => store.network.ss58Prefix); + + const tangleFormattedAddress = useMemo(() => { + return isEthereumAddress(accountAddress) + ? accountAddress + : toSubstrateAddress(accountAddress, ss58Prefix); + }, [accountAddress, ss58Prefix]); + return (
{identityName || - (isHex(accountAddress) - ? shortenHex(accountAddress) - : shortenString(accountAddress))} + (isHex(tangleFormattedAddress) + ? shortenHex(tangleFormattedAddress) + : shortenString(tangleFormattedAddress))} {description} diff --git a/apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx b/apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx index bff2dcca90..bac1fb2743 100644 --- a/apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx +++ b/apps/tangle-dapp/src/components/Lists/OperatorListItem.tsx @@ -3,8 +3,10 @@ import { Avatar, KeyValueWithButton, shortenString, + toSubstrateAddress, } from '@tangle-network/ui-components'; -import { FC } from 'react'; +import { FC, useMemo } from 'react'; +import useNetworkStore from '@tangle-network/tangle-shared-ui/context/useNetworkStore'; import LogoListItem from './LogoListItem'; type Props = { @@ -20,17 +22,25 @@ const OperatorListItem: FC = ({ rightUpperText, rightBottomText, }) => { - const shortAccountAddress = shortenString(accountAddress); + const ss58Prefix = useNetworkStore((store) => store.network.ss58Prefix); + + const tangleFormattedAddress = useMemo(() => { + return toSubstrateAddress(accountAddress, ss58Prefix); + }, [accountAddress, ss58Prefix]); + + const shortAccountAddress = shortenString(tangleFormattedAddress); const leftUpperContent = identity ?? shortAccountAddress; return ( } + logo={ + + } leftUpperContent={leftUpperContent} leftBottomContent={ diff --git a/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx b/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx index 8f283ac4a3..c15d524659 100644 --- a/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx +++ b/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx @@ -37,6 +37,7 @@ type Props = { operatorMap: OperatorMap; operatorTVL?: OperatorTvlGroup['operatorTvl']; action: RestakeAction; + onOperatorJoined?: () => void; }; const RestakeOverviewTabs: FC = ({ @@ -44,6 +45,7 @@ const RestakeOverviewTabs: FC = ({ operatorMap, operatorTVL, action, + onOperatorJoined, }) => { const [tab, setTab] = useState(RestakeTab.RESTAKE); @@ -97,6 +99,7 @@ const RestakeOverviewTabs: FC = ({ onRestakeClickedPagePath={PagePath.RESTAKE_DELEGATE} onRestakeClickedQueryParamKey={QueryParamKey.RESTAKE_OPERATOR} isLoading={isLoadingAssets} + onOperatorJoined={onOperatorJoined} /> diff --git a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx index 14b2c1ccfb..9378defc9d 100644 --- a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx @@ -69,7 +69,7 @@ const RestakeDelegateForm: FC = ({ assets }) => { watch, formState: { errors, isValid, isSubmitting }, } = useForm({ - mode: 'onBlur', + mode: 'onChange', }); const selectedOperatorAddress = watch('operatorAccountId'); diff --git a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx index a8ed6c3cda..c384a7c324 100644 --- a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx +++ b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx @@ -59,7 +59,7 @@ const DepositForm: FC = ({ watch, formState: { errors, isSubmitting, isValid }, } = useForm({ - mode: 'onBlur', + mode: 'onChange', defaultValues: { sourceTypedChainId: getDefaultTypedChainId(activeTypedChainId), }, diff --git a/apps/tangle-dapp/src/pages/restake/index.tsx b/apps/tangle-dapp/src/pages/restake/index.tsx index 569d03ceb1..10485f9bfc 100644 --- a/apps/tangle-dapp/src/pages/restake/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/index.tsx @@ -1,22 +1,29 @@ import useRestakeDelegatorInfo from '@tangle-network/tangle-shared-ui/data/restake/useRestakeDelegatorInfo'; import useRestakeOperatorMap from '@tangle-network/tangle-shared-ui/data/restake/useRestakeOperatorMap'; import useRestakeTvl from '@tangle-network/tangle-shared-ui/data/restake/useRestakeTvl'; -import { FC } from 'react'; -import { Navigate, useParams } from 'react-router'; -import { RestakeAction } from '../../constants'; import RestakeOverviewTabs from '../../containers/restaking/RestakeOverviewTabs'; import { PagePath } from '../../types'; +import { RestakeAction } from '../../constants'; +import { Navigate, useParams } from 'react-router'; import isEnumValue from '../../utils/isEnumValue'; +import { FC, useCallback, useState } from 'react'; const RestakePage: FC = () => { const { action } = useParams(); + const [refreshTrigger, setRefreshTrigger] = useState(0); const { result: delegatorInfo } = useRestakeDelegatorInfo(); - const { result: operatorMap } = useRestakeOperatorMap(); + const { result: operatorMap } = useRestakeOperatorMap(refreshTrigger); const { operatorConcentration, operatorTvl } = useRestakeTvl(delegatorInfo); + const handleOperatorJoined = useCallback(() => { + setTimeout(() => { + setRefreshTrigger((v) => v + 1); + }, 2000); + }, []); + // If provided, make sure that the action parameter is valid. - if (action !== undefined && !isEnumValue(action, RestakeAction)) { + if (action !== undefined && !isEnumValue(RestakeAction, action)) { return ; } else if (action === undefined) { return ; @@ -28,7 +35,8 @@ const RestakePage: FC = () => { operatorMap={operatorMap} operatorTVL={operatorTvl} operatorConcentration={operatorConcentration} - action={action} + action={action as RestakeAction} + onOperatorJoined={handleOperatorJoined} />
); diff --git a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx b/apps/tangle-dapp/src/pages/restake/unstake/index.tsx index 736960d632..ff8ad42c9d 100644 --- a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/unstake/index.tsx @@ -63,7 +63,7 @@ const RestakeUnstakeForm: FC = ({ assets }) => { formState: { errors, isValid, isSubmitting }, watch, } = useForm({ - mode: 'onBlur', + mode: 'onChange', }); const switchChain = useSwitchChain(); diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx index 43fec30e31..6cdbbdfa57 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx @@ -61,7 +61,7 @@ const RestakeWithdrawForm: FC = ({ assets }) => { reset, formState: { errors, isValid, isSubmitting }, } = useForm({ - mode: 'onBlur', + mode: 'onChange', }); const switchChain = useSwitchChain(); From 635306f2fd9b6a4fc4a76d81e2f53e8056afa66c Mon Sep 17 00:00:00 2001 From: Pavan Soratur Date: Fri, 15 Aug 2025 09:13:00 -0700 Subject: [PATCH 2/6] style: format code --- apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx b/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx index 498fef7d1b..2d04941db3 100644 --- a/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx +++ b/apps/tangle-cloud/src/pages/blueprints/[id]/page.tsx @@ -24,10 +24,7 @@ const RestakeOperatorAction: FC> = ({ children, }) => { return ( - + {children} ); From b0867f0bc5d955438124f744fd23935e54b57bf9 Mon Sep 17 00:00:00 2001 From: Pavan Soratur Date: Mon, 25 Aug 2025 06:01:14 -0700 Subject: [PATCH 3/6] feat: implement refresh mechanism for operator map in RestakeTabContent --- .../src/containers/restaking/RestakeTabContent.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx b/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx index d60020c66f..fdc2897181 100644 --- a/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx +++ b/apps/tangle-dapp/src/containers/restaking/RestakeTabContent.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useCallback, useEffect, type FC } from 'react'; +import { ReactNode, useCallback, useEffect, useState, type FC } from 'react'; import RestakeTabs from '../../pages/restake/RestakeTabs'; import { RestakeAction, RestakeTab } from '../../constants'; import DepositForm from '../../pages/restake/deposit/DepositForm'; @@ -30,11 +30,20 @@ type Props = { const RestakeTabContent: FC = ({ tab }) => { const { result: delegatorInfo } = useRestakeDelegatorInfo(); + + const [refreshTrigger, setRefreshTrigger] = useState(0); + + const handleOperatorJoined = useCallback(() => { + setTimeout(() => { + setRefreshTrigger((v) => v + 1); + }, 2000); + }, []); + const { result: operatorMap, isLoading: isLoadingOperators, error: operatorMapError, - } = useRestakeOperatorMap(); + } = useRestakeOperatorMap(refreshTrigger); const { operatorConcentration, operatorTvl } = useRestakeTvl(delegatorInfo); const navigate = useNavigate(); @@ -84,6 +93,7 @@ const RestakeTabContent: FC = ({ tab }) => { onRestakeClickedPagePath={PagePath.RESTAKE_DELEGATE} onRestakeClickedQueryParamKey={QueryParamKey.RESTAKE_OPERATOR} isLoading={isLoadingOperators} + onOperatorJoined={handleOperatorJoined} /> ); case RestakeTab.BLUEPRINTS: From 15acdbffbb6e6b4a0280f46671f2c1fcbf9519b4 Mon Sep 17 00:00:00 2001 From: Pavan Soratur Date: Mon, 25 Aug 2025 06:10:08 -0700 Subject: [PATCH 4/6] refactor: replace string literals with NetworkType constants in leaderboard and points hooks --- .../src/features/leaderboard/components/LeaderboardTable.tsx | 4 +++- .../src/features/points/data/useActiveAccountPoints.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx b/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx index 73a01a1535..dca2ec5b87 100644 --- a/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx +++ b/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx @@ -206,7 +206,9 @@ export const LeaderboardTable = () => { pageSize: 15, }); - const [networkTab, setNetworkTab] = useState('MAINNET'); + const [networkTab, setNetworkTab] = useState( + NetworkType.Mainnet, + ); const { data: latestBlock, diff --git a/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts b/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts index 8ce50eecb4..cef77150a8 100644 --- a/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts +++ b/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts @@ -37,7 +37,9 @@ export default function useActiveAccountPoints() { queryKey: [ReactQueryKey.GetAccountPoints, activeAccount], queryFn: () => fetcher( - network.id === NetworkId.TANGLE_MAINNET ? 'MAINNET' : 'TESTNET', + network.id === NetworkId.TANGLE_MAINNET + ? NetworkType.Mainnet + : NetworkType.Testnet, activeAccount, ), retry: 10, From 1d3aa8469f9a6806d5976b28d617be7c33be69cb Mon Sep 17 00:00:00 2001 From: Pavan Soratur Date: Mon, 25 Aug 2025 06:28:07 -0700 Subject: [PATCH 5/6] fix: improve network selection logic in NetworkSelectorDropdown component --- .../src/components/NetworkSelectorDropdown/index.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx b/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx index a4848c0434..8caa400b0f 100644 --- a/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx +++ b/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx @@ -76,15 +76,21 @@ const NetworkSelectionButton: FC = ({ () => { if (isConnecting) { return 'Connecting'; - } else if (loading) { + } + + if (loading) { return 'Loading'; } const UNKNOWN_NETWORK = 'Unknown Network'; + if (!activeWallet) { + return network?.name ?? UNKNOWN_NETWORK; + } + if (disableChainSelection) { return activeChain?.name === 'Tangle Mainnet' - ? activeChain.name + ? (activeChain?.name ?? network?.name ?? UNKNOWN_NETWORK) : (activeChain?.displayName ?? activeChain?.name ?? network?.name ?? @@ -99,7 +105,7 @@ const NetworkSelectionButton: FC = ({ ); }, // prettier-ignore - [isConnecting, loading, disableChainSelection, network?.name, activeChain?.displayName, activeChain?.name], + [isConnecting, loading, disableChainSelection, network?.name, activeWallet, activeChain?.displayName, activeChain?.name], ); const isWrongEvmNetwork = useMemo(() => { From 4f4f355645187e9398ae3cf3bcef985432048dfe Mon Sep 17 00:00:00 2001 From: Pavan Soratur Date: Mon, 25 Aug 2025 06:50:58 -0700 Subject: [PATCH 6/6] fix: ci --- .../features/leaderboard/components/LeaderboardTable.tsx | 4 ++-- .../src/features/points/data/useActiveAccountPoints.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx b/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx index dca2ec5b87..2e6de97321 100644 --- a/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx +++ b/apps/leaderboard/src/features/leaderboard/components/LeaderboardTable.tsx @@ -2,7 +2,7 @@ import { CrossCircledIcon } from '@radix-ui/react-icons'; import { Spinner } from '@tangle-network/icons'; import { Search } from '@tangle-network/icons/Search'; import TableStatus from '@tangle-network/tangle-shared-ui/components/tables/TableStatus'; -import { NetworkType } from '@tangle-network/tangle-shared-ui/graphql/graphql'; +import type { NetworkType } from '@tangle-network/tangle-shared-ui/graphql/graphql'; import { Input, isSubstrateAddress, @@ -207,7 +207,7 @@ export const LeaderboardTable = () => { }); const [networkTab, setNetworkTab] = useState( - NetworkType.Mainnet, + 'MAINNET' as NetworkType, ); const { diff --git a/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts b/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts index cef77150a8..70903924aa 100644 --- a/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts +++ b/apps/tangle-dapp/src/features/points/data/useActiveAccountPoints.ts @@ -6,7 +6,7 @@ import { useMemo } from 'react'; import { ReactQueryKey } from '../../../constants/reactQuery'; import useNetworkStore from '@tangle-network/tangle-shared-ui/context/useNetworkStore'; import { NetworkId } from '@tangle-network/ui-components/constants/networks'; -import { NetworkType } from '@tangle-network/tangle-shared-ui/graphql/graphql'; +import type { NetworkType } from '@tangle-network/tangle-shared-ui/graphql/graphql'; const GetAccountPointsQueryDocument = graphql(/* GraphQL */ ` query GetAccountPoints($account: String!) { @@ -37,9 +37,9 @@ export default function useActiveAccountPoints() { queryKey: [ReactQueryKey.GetAccountPoints, activeAccount], queryFn: () => fetcher( - network.id === NetworkId.TANGLE_MAINNET - ? NetworkType.Mainnet - : NetworkType.Testnet, + (network.id === NetworkId.TANGLE_MAINNET + ? 'MAINNET' + : 'TESTNET') as NetworkType, activeAccount, ), retry: 10,