From f6411e001a7d836e5f5cdf86ce8400c2177d5135 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 16 Jun 2026 16:25:47 +0800 Subject: [PATCH 01/16] Add recovery tab to data page --- .../components/data/context/tabs.js | 8 + .../next-common/components/data/index.jsx | 9 ++ .../recovery/hooks/useQueryAllRecoveryData.js | 66 ++++++++ .../components/data/recovery/index.jsx | 11 ++ .../data/recovery/table/columns.jsx | 124 +++++++++++++++ .../components/data/recovery/table/index.jsx | 142 ++++++++++++++++++ .../components/multisigs/fields.js | 8 +- .../next-common/utils/consts/menu/data.js | 36 +++-- .../next-common/utils/consts/menu/index.js | 4 +- .../utils/consts/settings/common/modules.js | 1 + .../utils/consts/settings/westend/index.js | 1 + .../utils/viewfuncs/estimateTimeFromBlocks.js | 30 ++++ packages/next/pages/recovery/index.jsx | 13 ++ 13 files changed, 437 insertions(+), 16 deletions(-) create mode 100644 packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js create mode 100644 packages/next-common/components/data/recovery/index.jsx create mode 100644 packages/next-common/components/data/recovery/table/columns.jsx create mode 100644 packages/next-common/components/data/recovery/table/index.jsx create mode 100644 packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js create mode 100644 packages/next/pages/recovery/index.jsx diff --git a/packages/next-common/components/data/context/tabs.js b/packages/next-common/components/data/context/tabs.js index 0bc88957a7..b7c7d177d4 100644 --- a/packages/next-common/components/data/context/tabs.js +++ b/packages/next-common/components/data/context/tabs.js @@ -25,6 +25,14 @@ function generateTabs() { }); } + if (modules?.recovery) { + TABS.push({ + tabId: "/recovery", + tabTitle: "Recovery", + pageTitle: "Recovery Explorer", + }); + } + return TABS; } diff --git a/packages/next-common/components/data/index.jsx b/packages/next-common/components/data/index.jsx index 6a42bce4f1..3fba63ac09 100644 --- a/packages/next-common/components/data/index.jsx +++ b/packages/next-common/components/data/index.jsx @@ -2,6 +2,7 @@ import DataBaseLayout from "./common/baseLayout"; import CommonTabs from "./common/tabs"; import ProxyExplorer from "./proxies"; import MultisigExplorer from "./multisig"; +import RecoveryExplorer from "./recovery"; function DataPageWithLayout({ children }) { return ( @@ -27,3 +28,11 @@ export function DataMultisig() { ); } + +export function DataRecovery() { + return ( + + + + ); +} diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js new file mode 100644 index 0000000000..7b8eeec7e1 --- /dev/null +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js @@ -0,0 +1,66 @@ +import { useContextApi } from "next-common/context/api"; +import { useEffect, useState } from "react"; + +export default function useQueryAllRecoveryData() { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!api?.query.recovery?.friendGroups) { + if (api) { + setLoading(false); + setData([]); + } + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.friendGroups + .entries() + .then((entries) => { + if (cancelled) return; + + const result = entries.map(([storageKey, value]) => { + const account = storageKey.args?.[0]?.toString(); + // value is a Codec tuple (FriendGroupVec, Ticket) + // Use toHuman() to get a plain JS array, then access elements + const humanValue = value.toHuman(); + const friendGroupVec = Array.isArray(humanValue?.[0]) + ? humanValue[0] + : []; + + return { + account, + friendGroups: friendGroupVec.map((group, index) => ({ + index, + friends: group.friends || [], + friendsNeeded: parseInt(group.friendsNeeded) || 0, + inheritor: group.inheritor || "", + inheritancePriority: parseInt(group.inheritancePriority) || 0, + inheritanceDelay: parseInt(group.inheritanceDelay) || 0, + cancelDelay: parseInt(group.cancelDelay) || 0, + })), + }; + }); + + setData(result); + setLoading(false); + }) + .catch((error) => { + console.error("Failed to query recovery friend groups", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api]); + + return { data, loading }; +} diff --git a/packages/next-common/components/data/recovery/index.jsx b/packages/next-common/components/data/recovery/index.jsx new file mode 100644 index 0000000000..e641ac4681 --- /dev/null +++ b/packages/next-common/components/data/recovery/index.jsx @@ -0,0 +1,11 @@ +import PageHeader from "../common/pageHeader"; +import RecoveryExplorerTable from "./table"; + +export default function RecoveryExplorer() { + return ( + <> + + + + ); +} diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx new file mode 100644 index 0000000000..5cc1dd6de0 --- /dev/null +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -0,0 +1,124 @@ +import AddressUser from "next-common/components/user/addressUser"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import { estimateTimeFromBlocks } from "next-common/utils/viewfuncs/estimateTimeFromBlocks"; +import { useChainSettings } from "next-common/context/chain"; +import { isNil } from "lodash-es"; + +function FriendsCount({ friends = [] }) { + if (isNil(friends)) { + return null; + } + + return ( + } + > + + {friends?.length || 0} + + + ); +} + +function DelayBlock({ blocks }) { + const { blockTime } = useChainSettings(); + + if (isNil(blocks)) { + return null; + } + + return ( + + + {blocks?.toLocaleString() || 0} + + + ); +} + +export const desktopColumns = [ + { + name: "Account", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Index", + className: "w-[80px] text-left", + render: (item) => ( + {item.index} + ), + }, + { + name: "Priority", + className: "w-[100px] text-left", + render: (item) => ( + + {item.inheritancePriority} + + ), + }, + { + name: "Friends", + className: "w-[100px] text-left", + render: (item) => , + }, + { + name: "Threshold", + className: "w-[120px] text-left", + render: (item) => ( + + {item.friendsNeeded} + + ), + }, + { + name: "Inheritance Delay", + className: "w-[180px] text-left", + render: (item) => , + }, + { + name: "Cancel Delay", + className: "w-[160px] text-left", + render: (item) => , + }, +]; + +export const mobileColumns = [ + { + name: "Account", + className: "text-left", + render: (item) => ( + <> + + + + ), + }, + { + name: "Priority / Index", + className: "text-right", + render: (item) => ( + + P{item.inheritancePriority} / I{item.index} + + ), + }, + { + name: "Threshold", + className: "text-right", + render: (item) => ( + + {item.friendsNeeded} + + ), + }, + { + name: "Inheritance Delay", + className: "text-right", + render: (item) => , + }, +]; diff --git a/packages/next-common/components/data/recovery/table/index.jsx b/packages/next-common/components/data/recovery/table/index.jsx new file mode 100644 index 0000000000..81899be415 --- /dev/null +++ b/packages/next-common/components/data/recovery/table/index.jsx @@ -0,0 +1,142 @@ +import { desktopColumns, mobileColumns } from "./columns"; +import useQueryAllRecoveryData from "../hooks/useQueryAllRecoveryData"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useEffect, useMemo, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import { useRouter } from "next/router"; +import useSearchComponent from "../../common/useSearchComponent"; +import { TitleContainer } from "next-common/components/styled/containers/titleContainer"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; +import { addRouterQuery } from "next-common/utils/router"; + +export default function RecoveryExplorerTable() { + const [navCollapsed] = useNavCollapsed(); + const router = useRouter(); + const [dataList, setDataList] = useState([]); + const { search = "", component: SearchBoxComponent } = useSearchComponent({ + placeholder: "Search by account address", + className: "my-0 ml-2 flex-1 max-sm:w-full max-sm:ml-6", + size: "small", + }); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const { data, loading: isLoading } = useQueryAllRecoveryData(); + + // Flatten: each account may have multiple friend groups, each becomes a row + const flattenedData = useMemo(() => { + if (!data || data.length === 0) { + return []; + } + + const rows = []; + for (const entry of data) { + for (const group of entry.friendGroups) { + rows.push({ + account: entry.account, + index: group.index, + inheritancePriority: group.inheritancePriority, + friends: group.friends, + friendsNeeded: group.friendsNeeded, + inheritor: group.inheritor, + inheritanceDelay: group.inheritanceDelay, + cancelDelay: group.cancelDelay, + }); + } + } + return rows; + }, [data]); + + // Apply search filter on account field + const filteredData = useMemo(() => { + if (!search) { + return flattenedData; + } + + const lowerSearch = search.toLowerCase(); + return flattenedData.filter((row) => + row.account?.toLowerCase().includes(lowerSearch), + ); + }, [flattenedData, search]); + + const total = useMemo(() => { + return filteredData?.length || 0; + }, [filteredData]); + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(data)) { + return; + } + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(filteredData?.slice(startIndex, endIndex)); + setLoading(false); + }, [data, isLoading, page, total, filteredData]); + + useEffect(() => { + addRouterQuery(router, "page", 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router.query.search]); + + useEffect(() => { + addRouterQuery(router, "page", page); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + + return ( +
+
+ + + List + + {!loading && total} + + + +
+
+ {SearchBoxComponent} +
+ + + + + + + {total > 0 && pageComponent} + +
+ ); +} diff --git a/packages/next-common/components/multisigs/fields.js b/packages/next-common/components/multisigs/fields.js index 34ce13ee0b..2115b723f3 100644 --- a/packages/next-common/components/multisigs/fields.js +++ b/packages/next-common/components/multisigs/fields.js @@ -40,7 +40,7 @@ export function When({ height, index }) { return ( {height.toLocaleString()}-{index} @@ -99,14 +99,14 @@ export function AddressesTooltip({ } return ( -
    +
      {(addresses || []).map((address, index) => (
    • ))} @@ -116,7 +116,7 @@ export function AddressesTooltip({ export function Approving({ approvals, threshold }) { return ( -
      +
      }> {approvals?.length || 0} diff --git a/packages/next-common/utils/consts/menu/data.js b/packages/next-common/utils/consts/menu/data.js index bec6b36812..513038f54d 100644 --- a/packages/next-common/utils/consts/menu/data.js +++ b/packages/next-common/utils/consts/menu/data.js @@ -1,12 +1,11 @@ import { MenuData } from "@osn/icons/subsquare"; +import getChainSettings from "../settings"; +import { CHAIN } from "next-common/utils/constants"; -const Data = { - name: "Data", - value: "data", - pathname: "/proxies", - icon: , - extraMatchNavMenuActivePathnames: ["/proxies", "/multisigs"], - children: [ +function getDataMenu() { + const { modules } = getChainSettings(CHAIN); + + const children = [ { name: "Proxies", value: "proxies", @@ -17,7 +16,24 @@ const Data = { value: "multisig", pathname: "/multisigs", }, - ], -}; + ]; + + if (modules?.recovery) { + children.push({ + name: "Recovery", + value: "recovery", + pathname: "/recovery", + }); + } + + return { + name: "Data", + value: "data", + pathname: "/proxies", + icon: , + extraMatchNavMenuActivePathnames: children.map((c) => c.pathname), + children, + }; +} -export default Data; +export default getDataMenu; diff --git a/packages/next-common/utils/consts/menu/index.js b/packages/next-common/utils/consts/menu/index.js index 9620609dae..e1cf81ebc7 100644 --- a/packages/next-common/utils/consts/menu/index.js +++ b/packages/next-common/utils/consts/menu/index.js @@ -21,7 +21,7 @@ import { coretimeMenu } from "./coretime"; import { getPeopleMenu } from "./people"; import { stakingMenu } from "./staking"; import whitelist from "./whitelist"; -import Data from "./data"; +import getDataMenu from "./data"; import vesting from "./vesting"; import getAdvancedMenu from "next-common/utils/consts/menu/advanced"; import { NAV_MENU_TYPE } from "next-common/utils/constants"; @@ -69,7 +69,7 @@ export function getHomeMenu({ modules?.vesting && vesting, modules?.scheduler && scheduler, modules?.whitelist && whitelist, - (modules?.proxy || hasMultisig) && Data, + (modules?.proxy || hasMultisig) && getDataMenu(), calendarMenu, votingSpace && votingMenu, navigationMenu, diff --git a/packages/next-common/utils/consts/settings/common/modules.js b/packages/next-common/utils/consts/settings/common/modules.js index 52aa39633b..ec01b85207 100644 --- a/packages/next-common/utils/consts/settings/common/modules.js +++ b/packages/next-common/utils/consts/settings/common/modules.js @@ -45,6 +45,7 @@ const base = { provider: "server", }, vesting: false, + recovery: false, }; /** diff --git a/packages/next-common/utils/consts/settings/westend/index.js b/packages/next-common/utils/consts/settings/westend/index.js index 0394764a47..0ddd7eb7a2 100644 --- a/packages/next-common/utils/consts/settings/westend/index.js +++ b/packages/next-common/utils/consts/settings/westend/index.js @@ -47,6 +47,7 @@ const westend = { vesting: true, staking: true, scheduler: true, + recovery: true, treasury: { spends: true, proposals: true, diff --git a/packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js b/packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js new file mode 100644 index 0000000000..9ff5de4ba3 --- /dev/null +++ b/packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js @@ -0,0 +1,30 @@ +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; + +dayjs.extend(duration); + +export function estimateTimeFromBlocks(blocks, blockTime = 6000) { + if (!blocks || blocks <= 0) { + return "0s"; + } + + const milliseconds = blocks * blockTime; + const dur = dayjs.duration(milliseconds); + + const yrs = dur.years(); + const mos = dur.months(); + const d = dur.days(); + const hrs = dur.hours(); + const mins = dur.minutes(); + const secs = dur.seconds(); + + const parts = []; + if (yrs > 0) parts.push(`${yrs}yr`); + if (mos > 0) parts.push(`${mos}mo`); + if (d > 0) parts.push(`${d}d`); + if (hrs > 0) parts.push(`${hrs}hr`); + if (mins > 0) parts.push(`${mins}min`); + if (secs > 0 || parts.length === 0) parts.push(`${secs}s`); + + return parts.slice(0, 3).join(" "); +} diff --git a/packages/next/pages/recovery/index.jsx b/packages/next/pages/recovery/index.jsx new file mode 100644 index 0000000000..71aac5fdc8 --- /dev/null +++ b/packages/next/pages/recovery/index.jsx @@ -0,0 +1,13 @@ +import { getServerSidePropsWithTracks } from "next-common/services/serverSide/serverSidePropsWithTracks"; +import { DataRecovery } from "next-common/components/data"; +import DataTabsProvider from "next-common/components/data/context/tabs"; + +export default function RecoveryPage() { + return ( + + + + ); +} + +export const getServerSideProps = getServerSidePropsWithTracks; From dcfbdf5773975a6ec2f0ac98fa4110ac702ca0e5 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 16 Jun 2026 18:12:10 +0800 Subject: [PATCH 02/16] Update --- .../components/data/recovery/table/columns.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx index 5cc1dd6de0..5cc1c1c52c 100644 --- a/packages/next-common/components/data/recovery/table/columns.jsx +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -14,7 +14,7 @@ function FriendsCount({ friends = [] }) { } > - + {friends?.length || 0} @@ -29,8 +29,13 @@ function DelayBlock({ blocks }) { } return ( - - + + {blocks?.toLocaleString() || 0} From c3be1c8576411e09fef3e489553184fd2ef62f8b Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 16 Jun 2026 18:26:49 +0800 Subject: [PATCH 03/16] refactor, --- .../recovery/hooks/useQueryAllRecoveryData.js | 18 ++--- .../components/data/recovery/table/index.jsx | 77 +++++++++---------- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js index 7b8eeec7e1..74a3b90f24 100644 --- a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js @@ -7,11 +7,13 @@ export default function useQueryAllRecoveryData() { const [loading, setLoading] = useState(true); useEffect(() => { + if (!api) { + return; + } + if (!api?.query.recovery?.friendGroups) { - if (api) { - setLoading(false); - setData([]); - } + setLoading(false); + setData([]); return; } @@ -25,11 +27,9 @@ export default function useQueryAllRecoveryData() { const result = entries.map(([storageKey, value]) => { const account = storageKey.args?.[0]?.toString(); - // value is a Codec tuple (FriendGroupVec, Ticket) - // Use toHuman() to get a plain JS array, then access elements - const humanValue = value.toHuman(); - const friendGroupVec = Array.isArray(humanValue?.[0]) - ? humanValue[0] + const jsonValue = value.toJSON(); + const friendGroupVec = Array.isArray(jsonValue?.[0]) + ? jsonValue[0] : []; return { diff --git a/packages/next-common/components/data/recovery/table/index.jsx b/packages/next-common/components/data/recovery/table/index.jsx index 81899be415..186b487ba3 100644 --- a/packages/next-common/components/data/recovery/table/index.jsx +++ b/packages/next-common/components/data/recovery/table/index.jsx @@ -1,6 +1,38 @@ import { desktopColumns, mobileColumns } from "./columns"; import useQueryAllRecoveryData from "../hooks/useQueryAllRecoveryData"; import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; + +function flattenRecoveryData(data) { + if (!data || data.length === 0) { + return []; + } + + const rows = []; + for (const entry of data) { + for (const group of entry.friendGroups) { + rows.push({ + account: entry.account, + index: group.index, + inheritancePriority: group.inheritancePriority, + friends: group.friends, + friendsNeeded: group.friendsNeeded, + inheritor: group.inheritor, + inheritanceDelay: group.inheritanceDelay, + cancelDelay: group.cancelDelay, + }); + } + } + return rows; +} + +function searchAddress(list, keyword) { + if (!keyword) { + return list; + } + + const lowerSearch = keyword.toLowerCase(); + return list.filter((row) => row.account?.toLowerCase().includes(lowerSearch)); +} import { defaultPageSize } from "next-common/utils/constants"; import { useEffect, useMemo, useState } from "react"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; @@ -32,46 +64,13 @@ export default function RecoveryExplorerTable() { ); const { data, loading: isLoading } = useQueryAllRecoveryData(); + const flattenedData = useMemo(() => flattenRecoveryData(data), [data]); + const filteredData = useMemo( + () => searchAddress(flattenedData, search), + [flattenedData, search], + ); - // Flatten: each account may have multiple friend groups, each becomes a row - const flattenedData = useMemo(() => { - if (!data || data.length === 0) { - return []; - } - - const rows = []; - for (const entry of data) { - for (const group of entry.friendGroups) { - rows.push({ - account: entry.account, - index: group.index, - inheritancePriority: group.inheritancePriority, - friends: group.friends, - friendsNeeded: group.friendsNeeded, - inheritor: group.inheritor, - inheritanceDelay: group.inheritanceDelay, - cancelDelay: group.cancelDelay, - }); - } - } - return rows; - }, [data]); - - // Apply search filter on account field - const filteredData = useMemo(() => { - if (!search) { - return flattenedData; - } - - const lowerSearch = search.toLowerCase(); - return flattenedData.filter((row) => - row.account?.toLowerCase().includes(lowerSearch), - ); - }, [flattenedData, search]); - - const total = useMemo(() => { - return filteredData?.length || 0; - }, [filteredData]); + const total = filteredData?.length || 0; useEffect(() => { setLoading(true); From 1a67767d1918964e951754c943b347e43618f684 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 16 Jun 2026 18:29:22 +0800 Subject: [PATCH 04/16] refactor --- .../components/data/recovery/table/index.jsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/next-common/components/data/recovery/table/index.jsx b/packages/next-common/components/data/recovery/table/index.jsx index 186b487ba3..b26178707e 100644 --- a/packages/next-common/components/data/recovery/table/index.jsx +++ b/packages/next-common/components/data/recovery/table/index.jsx @@ -2,6 +2,19 @@ import { desktopColumns, mobileColumns } from "./columns"; import useQueryAllRecoveryData from "../hooks/useQueryAllRecoveryData"; import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useEffect, useMemo, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import { useRouter } from "next/router"; +import useSearchComponent from "../../common/useSearchComponent"; +import { TitleContainer } from "next-common/components/styled/containers/titleContainer"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; +import { addRouterQuery } from "next-common/utils/router"; + function flattenRecoveryData(data) { if (!data || data.length === 0) { return []; @@ -33,18 +46,6 @@ function searchAddress(list, keyword) { const lowerSearch = keyword.toLowerCase(); return list.filter((row) => row.account?.toLowerCase().includes(lowerSearch)); } -import { defaultPageSize } from "next-common/utils/constants"; -import { useEffect, useMemo, useState } from "react"; -import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; -import { MapDataList } from "next-common/components/dataList"; -import ScrollerX from "next-common/components/styled/containers/scrollerX"; -import { useRouter } from "next/router"; -import useSearchComponent from "../../common/useSearchComponent"; -import { TitleContainer } from "next-common/components/styled/containers/titleContainer"; -import { useNavCollapsed } from "next-common/context/nav"; -import { cn } from "next-common/utils"; -import { isNil } from "lodash-es"; -import { addRouterQuery } from "next-common/utils/router"; export default function RecoveryExplorerTable() { const [navCollapsed] = useNavCollapsed(); From 9b699c401d3e7464d82dd164f9c850e57f24e4a6 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 16 Jun 2026 18:37:11 +0800 Subject: [PATCH 05/16] refactor --- .../data/recovery/table/columns.jsx | 12 ++------ .../utils/viewfuncs/estimateTimeFromBlocks.js | 30 ------------------- 2 files changed, 3 insertions(+), 39 deletions(-) delete mode 100644 packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx index 5cc1c1c52c..da9663feb0 100644 --- a/packages/next-common/components/data/recovery/table/columns.jsx +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -1,8 +1,7 @@ import AddressUser from "next-common/components/user/addressUser"; import Tooltip from "next-common/components/tooltip"; import { AddressesTooltip } from "next-common/components/multisigs/fields"; -import { estimateTimeFromBlocks } from "next-common/utils/viewfuncs/estimateTimeFromBlocks"; -import { useChainSettings } from "next-common/context/chain"; +import { useEstimateBlocksTime } from "next-common/utils/hooks"; import { isNil } from "lodash-es"; function FriendsCount({ friends = [] }) { @@ -22,19 +21,14 @@ function FriendsCount({ friends = [] }) { } function DelayBlock({ blocks }) { - const { blockTime } = useChainSettings(); + const estimatedTime = useEstimateBlocksTime(blocks); if (isNil(blocks)) { return null; } return ( - + {blocks?.toLocaleString() || 0} diff --git a/packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js b/packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js deleted file mode 100644 index 9ff5de4ba3..0000000000 --- a/packages/next-common/utils/viewfuncs/estimateTimeFromBlocks.js +++ /dev/null @@ -1,30 +0,0 @@ -import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; - -dayjs.extend(duration); - -export function estimateTimeFromBlocks(blocks, blockTime = 6000) { - if (!blocks || blocks <= 0) { - return "0s"; - } - - const milliseconds = blocks * blockTime; - const dur = dayjs.duration(milliseconds); - - const yrs = dur.years(); - const mos = dur.months(); - const d = dur.days(); - const hrs = dur.hours(); - const mins = dur.minutes(); - const secs = dur.seconds(); - - const parts = []; - if (yrs > 0) parts.push(`${yrs}yr`); - if (mos > 0) parts.push(`${mos}mo`); - if (d > 0) parts.push(`${d}d`); - if (hrs > 0) parts.push(`${hrs}hr`); - if (mins > 0) parts.push(`${mins}min`); - if (secs > 0 || parts.length === 0) parts.push(`${secs}s`); - - return parts.slice(0, 3).join(" "); -} From ee9993d362743ef65fd7d699599535559aeeeadf Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 16 Jun 2026 18:39:51 +0800 Subject: [PATCH 06/16] Update --- packages/next-common/components/data/recovery/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/data/recovery/index.jsx b/packages/next-common/components/data/recovery/index.jsx index e641ac4681..9e140b56f0 100644 --- a/packages/next-common/components/data/recovery/index.jsx +++ b/packages/next-common/components/data/recovery/index.jsx @@ -4,7 +4,7 @@ import RecoveryExplorerTable from "./table"; export default function RecoveryExplorer() { return ( <> - + ); From fe248def4988fc56d11a54569b99f614a10f4148 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 16 Jun 2026 18:43:34 +0800 Subject: [PATCH 07/16] Remove link --- packages/next-common/components/data/common/pageHeader.jsx | 2 +- packages/next-common/components/data/recovery/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next-common/components/data/common/pageHeader.jsx b/packages/next-common/components/data/common/pageHeader.jsx index 10a4ce4121..e38f66daa8 100644 --- a/packages/next-common/components/data/common/pageHeader.jsx +++ b/packages/next-common/components/data/common/pageHeader.jsx @@ -36,7 +36,7 @@ export default function PageHeader({ href = "" }) {
      {title} - + {href && }
      ); diff --git a/packages/next-common/components/data/recovery/index.jsx b/packages/next-common/components/data/recovery/index.jsx index 9e140b56f0..9e99457e4d 100644 --- a/packages/next-common/components/data/recovery/index.jsx +++ b/packages/next-common/components/data/recovery/index.jsx @@ -4,7 +4,7 @@ import RecoveryExplorerTable from "./table"; export default function RecoveryExplorer() { return ( <> - + ); From dd595ad6a34c51f1d43b547dda1275decd8fb6fe Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 09:38:46 +0800 Subject: [PATCH 08/16] fix: text align --- packages/next-common/components/data/recovery/table/columns.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx index da9663feb0..cc674965b6 100644 --- a/packages/next-common/components/data/recovery/table/columns.jsx +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -81,7 +81,7 @@ export const desktopColumns = [ }, { name: "Cancel Delay", - className: "w-[160px] text-left", + className: "w-[160px] text-right", render: (item) => , }, ]; From 3934351f4a9d5dbd2c45200eadb709d8fdbaa9c8 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 09:47:48 +0800 Subject: [PATCH 09/16] Update --- .../data/recovery/table/columns.jsx | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx index cc674965b6..8894533f33 100644 --- a/packages/next-common/components/data/recovery/table/columns.jsx +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -74,6 +74,13 @@ export const desktopColumns = [
      ), }, + { + name: "Inheritor", + className: "min-w-[160px] text-left", + render: (item) => ( + + ), + }, { name: "Inheritance Delay", className: "w-[180px] text-left", @@ -90,22 +97,31 @@ export const mobileColumns = [ { name: "Account", className: "text-left", + render: (item) => , + }, + { + name: "Index", + className: "text-right", render: (item) => ( - <> - - - + #{item.index} ), }, + { - name: "Priority / Index", + name: "Priority", className: "text-right", render: (item) => ( - P{item.inheritancePriority} / I{item.index} + {item.inheritancePriority} ), }, + { + name: "Friends", + className: "text-left", + render: (item) => , + }, + { name: "Threshold", className: "text-right", @@ -115,9 +131,19 @@ export const mobileColumns = [ ), }, + { + name: "Inheritor", + className: "text-right", + render: (item) => , + }, { name: "Inheritance Delay", className: "text-right", render: (item) => , }, + { + name: "Cancel Delay", + className: "text-right", + render: (item) => , + }, ]; From 7732f374d7334f9d24d2f273cfad4787617e462e Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 09:48:22 +0800 Subject: [PATCH 10/16] Update --- packages/next-common/components/data/recovery/table/columns.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx index 8894533f33..55ad10be50 100644 --- a/packages/next-common/components/data/recovery/table/columns.jsx +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -48,7 +48,7 @@ export const desktopColumns = [ name: "Index", className: "w-[80px] text-left", render: (item) => ( - {item.index} + #{item.index} ), }, { From b935e34af3183fcc47accf370adb24fae6174121 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 10:07:50 +0800 Subject: [PATCH 11/16] Update list title --- packages/next-common/components/data/recovery/table/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-common/components/data/recovery/table/index.jsx b/packages/next-common/components/data/recovery/table/index.jsx index b26178707e..f331ba0ff8 100644 --- a/packages/next-common/components/data/recovery/table/index.jsx +++ b/packages/next-common/components/data/recovery/table/index.jsx @@ -104,7 +104,7 @@ export default function RecoveryExplorerTable() {
      - List + Friend Groups {!loading && total} From bf46330fd74c8ec372b5b914800b1d73a2b32f27 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 15:04:19 +0800 Subject: [PATCH 12/16] Add recovery attempts table, #6919 --- .../hooks/useQueryAllRecoveryAttempts.js | 111 ++++++++++++++++ .../recovery/hooks/useQueryAllRecoveryData.js | 23 ++++ .../components/data/recovery/index.jsx | 71 ++++++++++- .../data/recovery/table/attempts.jsx | 66 ++++++++++ .../data/recovery/table/attemptsColumns.jsx | 120 ++++++++++++++++++ .../data/recovery/table/columns.jsx | 6 +- .../components/data/recovery/table/index.jsx | 51 ++------ 7 files changed, 401 insertions(+), 47 deletions(-) create mode 100644 packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js create mode 100644 packages/next-common/components/data/recovery/table/attempts.jsx create mode 100644 packages/next-common/components/data/recovery/table/attemptsColumns.jsx diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js new file mode 100644 index 0000000000..650f4ee07b --- /dev/null +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryAttempts.js @@ -0,0 +1,111 @@ +import { useContextApi } from "next-common/context/api"; +import { useEffect, useState } from "react"; + +function bitfieldToIndices(bitfield) { + const indices = []; + let bitIndex = 0; + for (const byte of bitfield) { + if (typeof byte === "number") { + for (let b = 0; b < 8; b++) { + if (byte & (1 << b)) { + indices.push(bitIndex + b); + } + } + } + bitIndex += 8; + } + return indices; +} + +export default function useQueryAllRecoveryAttempts() { + const api = useContextApi(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!api) { + return; + } + + if (!api?.query.recovery?.attempt) { + setLoading(false); + setData([]); + return; + } + + let cancelled = false; + setLoading(true); + + api.query.recovery.attempt + .entries() + .then(async (entries) => { + if (cancelled) return; + + // Build a map of (lostAccount) -> friendGroups to resolve bitfield indices to addresses + const lostAccounts = [ + ...new Set(entries.map(([key]) => key.args?.[0]?.toString())), + ]; + const friendGroupsMap = {}; + + await Promise.all( + lostAccounts.map(async (account) => { + try { + const value = await api.query.recovery.friendGroups(account); + const json = value.toJSON(); + friendGroupsMap[account] = json?.[0] || []; + } catch { + friendGroupsMap[account] = []; + } + }), + ); + + const result = entries.map(([storageKey, value]) => { + const lostAccount = storageKey.args?.[0]?.toString(); + const friendGroupIndex = storageKey.args?.[1]?.toNumber(); + + const json = value.toJSON(); + const attempt = json?.[0] || {}; + + const approvalsBitfield = attempt.approvals || []; + const approvedIndices = bitfieldToIndices(approvalsBitfield); + const approvalsCount = approvedIndices.length; + + // Resolve approved indices to actual friend addresses + const friendGroup = + (friendGroupsMap[lostAccount] || [])[friendGroupIndex] || {}; + const friends = friendGroup.friends || []; + const approvedAddresses = approvedIndices + .filter((i) => i < friends.length) + .map((i) => friends[i]); + + return { + lostAccount, + friendGroupIndex, + initiator: attempt.initiator || "", + initBlock: parseInt(attempt.initBlock) || 0, + lastApprovalBlock: parseInt(attempt.lastApprovalBlock) || 0, + approvalsCount, + approvedAddresses, + }; + }); + + if (!cancelled) { + setData(result); + setLoading(false); + } + }) + .catch((error) => { + console.error("Failed to query recovery attempts", error); + if (!cancelled) { + setData([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [api]); + + return { data, loading }; +} diff --git a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js index 74a3b90f24..df56c3d169 100644 --- a/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js +++ b/packages/next-common/components/data/recovery/hooks/useQueryAllRecoveryData.js @@ -1,6 +1,29 @@ import { useContextApi } from "next-common/context/api"; import { useEffect, useState } from "react"; +export function flattenRecoveryData(data) { + if (!data || data.length === 0) { + return []; + } + + const rows = []; + for (const entry of data) { + for (const group of entry.friendGroups) { + rows.push({ + account: entry.account, + index: group.index, + inheritancePriority: group.inheritancePriority, + friends: group.friends, + friendsNeeded: group.friendsNeeded, + inheritor: group.inheritor, + inheritanceDelay: group.inheritanceDelay, + cancelDelay: group.cancelDelay, + }); + } + } + return rows; +} + export default function useQueryAllRecoveryData() { const api = useContextApi(); const [data, setData] = useState([]); diff --git a/packages/next-common/components/data/recovery/index.jsx b/packages/next-common/components/data/recovery/index.jsx index 9e99457e4d..8765c032d0 100644 --- a/packages/next-common/components/data/recovery/index.jsx +++ b/packages/next-common/components/data/recovery/index.jsx @@ -1,11 +1,78 @@ +import { useMemo, useState } from "react"; +import { useRouter } from "next/router"; +import TabsList from "next-common/components/tabs/list"; +import { TabLabel } from "next-common/components/assethubMigrationAssets/tabs"; import PageHeader from "../common/pageHeader"; -import RecoveryExplorerTable from "./table"; +import RecoveryFriendGroupsTable from "./table"; +import RecoveryAttemptsTable from "./table/attempts"; +import useQueryAllRecoveryData, { + flattenRecoveryData, +} from "./hooks/useQueryAllRecoveryData"; +import useQueryAllRecoveryAttempts from "./hooks/useQueryAllRecoveryAttempts"; +import { searchAddress } from "./table"; export default function RecoveryExplorer() { + const [activeTab, setActiveTab] = useState("friendGroups"); + const router = useRouter(); + const search = router.query.search || ""; + + const { data: friendGroupsData, loading: friendGroupsLoading } = + useQueryAllRecoveryData(); + const { data: attemptsData, loading: attemptsLoading } = + useQueryAllRecoveryAttempts(); + + const flattened = useMemo( + () => flattenRecoveryData(friendGroupsData), + [friendGroupsData], + ); + const filteredFlattened = useMemo( + () => searchAddress(flattened, search), + [flattened, search], + ); + const friendGroupsTotal = filteredFlattened.length; + const attemptsTotal = attemptsData?.length || 0; + + const tabs = [ + { + value: "friendGroups", + label: ( + + ), + }, + { + value: "attempts", + label: ( + + ), + }, + ]; + return ( <> - +
      + setActiveTab(tab.value)} + /> +
      + {activeTab === "friendGroups" ? ( + + ) : ( + + )} ); } diff --git a/packages/next-common/components/data/recovery/table/attempts.jsx b/packages/next-common/components/data/recovery/table/attempts.jsx new file mode 100644 index 0000000000..e00f3b25fc --- /dev/null +++ b/packages/next-common/components/data/recovery/table/attempts.jsx @@ -0,0 +1,66 @@ +import { desktopColumns, mobileColumns } from "./attemptsColumns"; +import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; +import { defaultPageSize } from "next-common/utils/constants"; +import { useEffect, useState } from "react"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import { MapDataList } from "next-common/components/dataList"; +import ScrollerX from "next-common/components/styled/containers/scrollerX"; +import { useNavCollapsed } from "next-common/context/nav"; +import { cn } from "next-common/utils"; +import { isNil } from "lodash-es"; + +export default function RecoveryAttemptsTable({ data, loading: isLoading }) { + const [navCollapsed] = useNavCollapsed(); + const [dataList, setDataList] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + + const { page, component: pageComponent } = usePaginationComponent( + totalCount, + defaultPageSize, + ); + + const total = data?.length || 0; + + useEffect(() => { + setLoading(true); + }, [page]); + + useEffect(() => { + if (isLoading || isNil(data)) { + return; + } + + setTotalCount(total); + const startIndex = (page - 1) * defaultPageSize; + const endIndex = startIndex + defaultPageSize; + setDataList(data?.slice(startIndex, endIndex)); + setLoading(false); + }, [data, isLoading, page, total]); + + return ( + + + + + + + {total > 0 && pageComponent} + + ); +} diff --git a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx new file mode 100644 index 0000000000..0beb3d3f7a --- /dev/null +++ b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx @@ -0,0 +1,120 @@ +import AddressUser from "next-common/components/user/addressUser"; +import Tooltip from "next-common/components/tooltip"; +import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import { isNil } from "lodash-es"; + +function BlockNumber({ blocks }) { + if (isNil(blocks)) { + return null; + } + + return ( + + #{blocks?.toLocaleString() || 0} + + ); +} + +export const desktopColumns = [ + { + name: "Lost Account", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Group Index", + className: "w-[120px] text-left", + render: (item) => ( + + #{item.friendGroupIndex} + + ), + }, + { + name: "Initiator", + className: "min-w-[200px] text-left", + render: (item) => ( + + ), + }, + { + name: "Init Block", + className: "w-[180px] text-left", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "w-[200px] text-left", + render: (item) => , + }, + { + name: "Approvals", + className: "w-[120px] text-left", + render: (item) => ( + + } + > + + {item.approvalsCount} + + + ), + }, +]; + +export const mobileColumns = [ + { + name: "Lost Account", + className: "text-left", + render: (item) => , + }, + { + name: "Group Index", + className: "text-right", + render: (item) => ( + + #{item.friendGroupIndex} + + ), + }, + { + name: "Initiator", + className: "text-left", + render: (item) => , + }, + { + name: "Init Block", + className: "text-right", + render: (item) => , + }, + { + name: "Last Approval Block", + className: "w-[200px] text-left", + render: (item) => , + }, + { + name: "Approvals", + className: "text-right", + render: (item) => ( + + } + > + + {item.approvalsCount} + + + ), + }, +]; diff --git a/packages/next-common/components/data/recovery/table/columns.jsx b/packages/next-common/components/data/recovery/table/columns.jsx index 55ad10be50..0b5e683918 100644 --- a/packages/next-common/components/data/recovery/table/columns.jsx +++ b/packages/next-common/components/data/recovery/table/columns.jsx @@ -45,8 +45,8 @@ export const desktopColumns = [ ), }, { - name: "Index", - className: "w-[80px] text-left", + name: "Group Index", + className: "w-[120px] text-left", render: (item) => ( #{item.index} ), @@ -100,7 +100,7 @@ export const mobileColumns = [ render: (item) => , }, { - name: "Index", + name: "Group Index", className: "text-right", render: (item) => ( #{item.index} diff --git a/packages/next-common/components/data/recovery/table/index.jsx b/packages/next-common/components/data/recovery/table/index.jsx index f331ba0ff8..d2e4981591 100644 --- a/packages/next-common/components/data/recovery/table/index.jsx +++ b/packages/next-common/components/data/recovery/table/index.jsx @@ -1,7 +1,5 @@ import { desktopColumns, mobileColumns } from "./columns"; -import useQueryAllRecoveryData from "../hooks/useQueryAllRecoveryData"; import usePaginationComponent from "next-common/components/pagination/usePaginationComponent"; - import { defaultPageSize } from "next-common/utils/constants"; import { useEffect, useMemo, useState } from "react"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; @@ -9,36 +7,13 @@ import { MapDataList } from "next-common/components/dataList"; import ScrollerX from "next-common/components/styled/containers/scrollerX"; import { useRouter } from "next/router"; import useSearchComponent from "../../common/useSearchComponent"; -import { TitleContainer } from "next-common/components/styled/containers/titleContainer"; import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; import { addRouterQuery } from "next-common/utils/router"; +import { flattenRecoveryData } from "../../recovery/hooks/useQueryAllRecoveryData"; -function flattenRecoveryData(data) { - if (!data || data.length === 0) { - return []; - } - - const rows = []; - for (const entry of data) { - for (const group of entry.friendGroups) { - rows.push({ - account: entry.account, - index: group.index, - inheritancePriority: group.inheritancePriority, - friends: group.friends, - friendsNeeded: group.friendsNeeded, - inheritor: group.inheritor, - inheritanceDelay: group.inheritanceDelay, - cancelDelay: group.cancelDelay, - }); - } - } - return rows; -} - -function searchAddress(list, keyword) { +export function searchAddress(list, keyword) { if (!keyword) { return list; } @@ -47,7 +22,10 @@ function searchAddress(list, keyword) { return list.filter((row) => row.account?.toLowerCase().includes(lowerSearch)); } -export default function RecoveryExplorerTable() { +export default function RecoveryFriendGroupsTable({ + data: rawData, + loading: isLoading, +}) { const [navCollapsed] = useNavCollapsed(); const router = useRouter(); const [dataList, setDataList] = useState([]); @@ -64,8 +42,7 @@ export default function RecoveryExplorerTable() { defaultPageSize, ); - const { data, loading: isLoading } = useQueryAllRecoveryData(); - const flattenedData = useMemo(() => flattenRecoveryData(data), [data]); + const flattenedData = useMemo(() => flattenRecoveryData(rawData), [rawData]); const filteredData = useMemo( () => searchAddress(flattenedData, search), [flattenedData, search], @@ -78,7 +55,7 @@ export default function RecoveryExplorerTable() { }, [page]); useEffect(() => { - if (isLoading || isNil(data)) { + if (isLoading || isNil(rawData)) { return; } @@ -87,7 +64,7 @@ export default function RecoveryExplorerTable() { const endIndex = startIndex + defaultPageSize; setDataList(filteredData?.slice(startIndex, endIndex)); setLoading(false); - }, [data, isLoading, page, total, filteredData]); + }, [rawData, isLoading, page, total, filteredData]); useEffect(() => { addRouterQuery(router, "page", 1); @@ -101,16 +78,6 @@ export default function RecoveryExplorerTable() { return (
      -
      - - - Friend Groups - - {!loading && total} - - - -
      {SearchBoxComponent}
      From d3d445e70c4f4ffe94907de844196608b236bc6c Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 15:45:22 +0800 Subject: [PATCH 13/16] Show elapse time on block height, #6919 --- .../data/recovery/table/attemptsColumns.jsx | 38 ++++++++++++++----- packages/next/pages/recovery/index.jsx | 9 +++-- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx index 0beb3d3f7a..76ae87b6c0 100644 --- a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx +++ b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx @@ -1,17 +1,31 @@ import AddressUser from "next-common/components/user/addressUser"; import Tooltip from "next-common/components/tooltip"; import { AddressesTooltip } from "next-common/components/multisigs/fields"; +import { useRelayChainApi } from "next-common/context/relayChain"; +import { useChainSettings } from "next-common/context/chain"; +import { estimateBlocksTime } from "next-common/utils"; +import useCall from "next-common/utils/hooks/useCall"; import { isNil } from "lodash-es"; -function BlockNumber({ blocks }) { - if (isNil(blocks)) { +function BlockNumberWithTooltip({ height }) { + const api = useRelayChainApi(); + const { blockTime } = useChainSettings(); + const { value: currentNumber } = useCall(api?.query?.system?.number, []); + const currentHeight = currentNumber?.toNumber(); + + if (isNil(height) || isNil(currentHeight)) { return null; } + const diff = Math.max(0, currentHeight - height); + const estimatedTime = diff > 0 ? estimateBlocksTime(diff, blockTime) : null; + return ( - - #{blocks?.toLocaleString() || 0} - + + + #{height?.toLocaleString() || 0} + + ); } @@ -42,12 +56,14 @@ export const desktopColumns = [ { name: "Init Block", className: "w-[180px] text-left", - render: (item) => , + render: (item) => , }, { name: "Last Approval Block", className: "w-[200px] text-left", - render: (item) => , + render: (item) => ( + + ), }, { name: "Approvals", @@ -92,12 +108,14 @@ export const mobileColumns = [ { name: "Init Block", className: "text-right", - render: (item) => , + render: (item) => , }, { name: "Last Approval Block", - className: "w-[200px] text-left", - render: (item) => , + className: "text-right", + render: (item) => ( + + ), }, { name: "Approvals", diff --git a/packages/next/pages/recovery/index.jsx b/packages/next/pages/recovery/index.jsx index 71aac5fdc8..9ae958a185 100644 --- a/packages/next/pages/recovery/index.jsx +++ b/packages/next/pages/recovery/index.jsx @@ -1,12 +1,15 @@ import { getServerSidePropsWithTracks } from "next-common/services/serverSide/serverSidePropsWithTracks"; import { DataRecovery } from "next-common/components/data"; import DataTabsProvider from "next-common/components/data/context/tabs"; +import { RelayChainApiProvider } from "next-common/context/relayChain"; export default function RecoveryPage() { return ( - - - + + + + + ); } From 5e02ec1e63041ba05ff77ab0338fe55492bac580 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 16:19:00 +0800 Subject: [PATCH 14/16] Enhance attempts table data, #6919 --- .../components/data/recovery/index.jsx | 6 ++- .../data/recovery/table/attempts.jsx | 23 ++++++++-- .../data/recovery/table/attemptsColumns.jsx | 43 +++++++++++++------ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/next-common/components/data/recovery/index.jsx b/packages/next-common/components/data/recovery/index.jsx index 8765c032d0..e0d20465a6 100644 --- a/packages/next-common/components/data/recovery/index.jsx +++ b/packages/next-common/components/data/recovery/index.jsx @@ -71,7 +71,11 @@ export default function RecoveryExplorer() { loading={friendGroupsLoading} /> ) : ( - + )} ); diff --git a/packages/next-common/components/data/recovery/table/attempts.jsx b/packages/next-common/components/data/recovery/table/attempts.jsx index e00f3b25fc..daf2cc82d7 100644 --- a/packages/next-common/components/data/recovery/table/attempts.jsx +++ b/packages/next-common/components/data/recovery/table/attempts.jsx @@ -9,7 +9,19 @@ import { useNavCollapsed } from "next-common/context/nav"; import { cn } from "next-common/utils"; import { isNil } from "lodash-es"; -export default function RecoveryAttemptsTable({ data, loading: isLoading }) { +function enhanceAttemptWithFriendGroup(attempt, friendGroups) { + const fgList = friendGroups?.find((fg) => fg.account === attempt.lostAccount); + const fgGroup = fgList?.friendGroups?.[attempt.friendGroupIndex]; + const threshold = fgGroup?.friendsNeeded || 0; + const friends = fgGroup?.friends || []; + return { ...attempt, threshold, friends }; +} + +export default function RecoveryAttemptsTable({ + data, + loading: isLoading, + friendGroups = [], +}) { const [navCollapsed] = useNavCollapsed(); const [dataList, setDataList] = useState([]); const [totalCount, setTotalCount] = useState(0); @@ -31,12 +43,17 @@ export default function RecoveryAttemptsTable({ data, loading: isLoading }) { return; } + // Enhance attempts with threshold/friends from friendGroups + const enhanced = (data || []).map((attempt) => + enhanceAttemptWithFriendGroup(attempt, friendGroups), + ); + setTotalCount(total); const startIndex = (page - 1) * defaultPageSize; const endIndex = startIndex + defaultPageSize; - setDataList(data?.slice(startIndex, endIndex)); + setDataList(enhanced?.slice(startIndex, endIndex)); setLoading(false); - }, [data, isLoading, page, total]); + }, [data, isLoading, page, total, friendGroups]); return ( diff --git a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx index 76ae87b6c0..e551dcd281 100644 --- a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx +++ b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx @@ -14,7 +14,11 @@ function BlockNumberWithTooltip({ height }) { const currentHeight = currentNumber?.toNumber(); if (isNil(height) || isNil(currentHeight)) { - return null; + return ( + + #{height?.toLocaleString() || 0} + + ); } const diff = Math.max(0, currentHeight - height); @@ -41,9 +45,15 @@ export const desktopColumns = [ name: "Group Index", className: "w-[120px] text-left", render: (item) => ( - - #{item.friendGroupIndex} - + + } + > + + #{item.friendGroupIndex} + + ), }, { @@ -66,8 +76,8 @@ export const desktopColumns = [ ), }, { - name: "Approvals", - className: "w-[120px] text-left", + name: "Threshold / Approvals", + className: "w-[160px] text-right", render: (item) => ( } > - - {item.approvalsCount} + + + {item.approvalsCount} /{" "} + + {item.threshold} ), @@ -95,9 +108,15 @@ export const mobileColumns = [ name: "Group Index", className: "text-right", render: (item) => ( - - #{item.friendGroupIndex} - + + } + > + + #{item.friendGroupIndex} + + ), }, { @@ -130,7 +149,7 @@ export const mobileColumns = [ } > - {item.approvalsCount} + {item.approvalsCount}/{item.threshold} ), From 957e0586fb8f6ce04e385688400090ab0811cf08 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 16:27:57 +0800 Subject: [PATCH 15/16] Update, #6919 --- .../components/data/recovery/table/attemptsColumns.jsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx index e551dcd281..f0c703390e 100644 --- a/packages/next-common/components/data/recovery/table/attemptsColumns.jsx +++ b/packages/next-common/components/data/recovery/table/attemptsColumns.jsx @@ -88,9 +88,7 @@ export const desktopColumns = [ } > - - {item.approvalsCount} /{" "} - + {item.approvalsCount} / {item.threshold} @@ -137,7 +135,7 @@ export const mobileColumns = [ ), }, { - name: "Approvals", + name: "Threshold / Approvals", className: "text-right", render: (item) => ( - {item.approvalsCount}/{item.threshold} + {item.approvalsCount} / + {item.threshold} ), From e4ac218d6447f32d5c1bc4e1dd06298215499b31 Mon Sep 17 00:00:00 2001 From: chaojun Date: Wed, 17 Jun 2026 16:34:46 +0800 Subject: [PATCH 16/16] Show loading, #6919 --- packages/next-common/components/data/recovery/index.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/next-common/components/data/recovery/index.jsx b/packages/next-common/components/data/recovery/index.jsx index e0d20465a6..a21f6b6416 100644 --- a/packages/next-common/components/data/recovery/index.jsx +++ b/packages/next-common/components/data/recovery/index.jsx @@ -2,6 +2,7 @@ import { useMemo, useState } from "react"; import { useRouter } from "next/router"; import TabsList from "next-common/components/tabs/list"; import { TabLabel } from "next-common/components/assethubMigrationAssets/tabs"; +import Loading from "next-common/components/loading"; import PageHeader from "../common/pageHeader"; import RecoveryFriendGroupsTable from "./table"; import RecoveryAttemptsTable from "./table/attempts"; @@ -38,7 +39,9 @@ export default function RecoveryExplorer() { label: ( : friendGroupsTotal + } isActive={activeTab === "friendGroups"} /> ), @@ -48,7 +51,7 @@ export default function RecoveryExplorer() { label: ( : attemptsTotal} isActive={activeTab === "attempts"} /> ),