Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
09271f4
Init recovery pages, #6919
hyifeng Jun 25, 2026
48e518a
feat: implement my recovery page content, #6919
hyifeng Jun 25, 2026
852ce34
Update, #6919
hyifeng Jun 25, 2026
67d7d45
refactor: list all friends in place, #6919
hyifeng Jun 25, 2026
49b51f0
Update layout, #6919
hyifeng Jun 25, 2026
451fb07
Update border color, #6919
hyifeng Jun 25, 2026
2983bcc
feat: add friend group dialog, #6919
hyifeng Jun 25, 2026
383e5b9
fix: empty friend groups, #6919
hyifeng Jun 25, 2026
cb836dc
feat: implement add friend group submission, #6919
hyifeng Jun 25, 2026
f2b5d7d
feat: add inheritor field to friend group form, #6919
hyifeng Jun 25, 2026
819484b
Update, #6919
hyifeng Jun 25, 2026
6fa9a35
feat: add edit friend group dialog, #6919
hyifeng Jun 25, 2026
94a9b7f
feat: wrap Recovery component with RelayChainApiProvider
hyifeng Jun 25, 2026
2beb89d
feat: add remove friend group dialog, #6919
hyifeng Jun 25, 2026
a1f7f80
Update, #6919
hyifeng Jun 26, 2026
33512b6
refactor friend group layout, #6919
hyifeng Jun 26, 2026
5cfda4c
feat: add slash button, #6919
hyifeng Jun 26, 2026
fbc5045
Merge branch 'main' into recovery-6919
hyifeng Jun 29, 2026
a01f0db
Move recovery content into account page, #6919
hyifeng Jun 30, 2026
7bdd1c4
Refresh data after extrinsic inblock, #6919
hyifeng Jun 30, 2026
ba9a045
Implement help others to recover account, #6919
hyifeng Jun 30, 2026
97182ca
Add inherited page, #6919
hyifeng Jun 30, 2026
97dc278
fix: sort address, #6919
hyifeng Jun 30, 2026
a2f1239
fix, #6919
hyifeng Jun 30, 2026
9befa22
Supper revoke inheritor and start attempt, #6919
hyifeng Jun 30, 2026
86e6f82
Improve button state management, #6919
hyifeng Jun 30, 2026
80ada0c
Improve finish button, #6919
hyifeng Jun 30, 2026
edc8c3e
Update, # 6919
hyifeng Jun 30, 2026
8b71166
Merge branch 'main' into recovery-6919
hyifeng Jul 1, 2026
8c4fcf4
Improve cancel button, #6919
hyifeng Jul 1, 2026
e42279c
fix style, #6919
hyifeng Jul 1, 2026
d5a479e
Improve action button displaying, #6919
hyifeng Jul 1, 2026
7cf6457
Add button tooltips, #6919
hyifeng Jul 1, 2026
afd2988
Implement control inherited account, #6919
hyifeng Jul 1, 2026
e589d6b
Update, #6919
hyifeng Jul 1, 2026
cb849e5
refactor, #6919
hyifeng Jul 1, 2026
9e5dcb3
Update text, #6919
hyifeng Jul 1, 2026
357ec36
refactor query attempt hooks, #6919
hyifeng Jul 1, 2026
a44b5d9
refactor recovery hooks, #6919
hyifeng Jul 1, 2026
28649ac
Update text, #6919
hyifeng Jul 1, 2026
44dd27e
refactor recovery list columns, #6919
hyifeng Jul 1, 2026
b569b00
Add recovery prompts, #6919
hyifeng Jul 1, 2026
b669a38
Add cancel action for lost account, #6919
hyifeng Jul 1, 2026
7791a59
refactor, #6919
hyifeng Jul 1, 2026
d58f9b3
Update, #6919
hyifeng Jul 1, 2026
01fbf05
fix, #6919
hyifeng Jul 1, 2026
2bc8cad
refactor, #6919
hyifeng Jul 1, 2026
44bf35f
Sort my recovery tab sections, #6919
hyifeng Jul 1, 2026
fc0a18c
Sort sections, #6919
hyifeng Jul 1, 2026
7f47a55
Improve block time tooltip, #6919
hyifeng Jul 2, 2026
aca7b1e
Improve, #6919
hyifeng Jul 2, 2026
15a478b
Add inheritor info to revoke popup, #6919
hyifeng Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContextApi } from "next-common/context/api";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";

export function flattenRecoveryData(data) {
if (!data || data.length === 0) {
Expand All @@ -24,10 +24,13 @@ export function flattenRecoveryData(data) {
return rows;
}

export default function useQueryAllRecoveryData() {
export default function useQueryAllFriendGroups() {
const api = useContextApi();
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [fetchCount, setFetchCount] = useState(0);

const fetch = useCallback(() => setFetchCount((c) => c + 1), []);

useEffect(() => {
if (!api) {
Expand Down Expand Up @@ -83,7 +86,7 @@ export default function useQueryAllRecoveryData() {
return () => {
cancelled = true;
};
}, [api]);
}, [api, fetchCount]);

return { data, loading };
return { data, loading, fetch };
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContextApi } from "next-common/context/api";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";

function bitfieldToIndices(bitfield) {
const indices = [];
Expand All @@ -17,10 +17,70 @@ function bitfieldToIndices(bitfield) {
return indices;
}

export function buildFriendGroupsData(friendGroupsMap) {
return Object.entries(friendGroupsMap).map(([account, groups]) => ({
account,
friendGroups: groups.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,
})),
}));
}

function processAttempts(entries, friendGroupsMap) {
return 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;

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,
friendsNeeded: parseInt(friendGroup.friendsNeeded) || 0,
};
});
}

/**
* Fetch all recovery attempts and their friend groups.
*
* @returns {{ data: Array, loading: boolean, fetch: () => void, friendGroupsMap: object }}
* - data: all processed recovery attempt objects
* - loading: whether data is being fetched
* - fetch: trigger a re-fetch
* - friendGroupsMap: raw map of lostAccount -> friendGroups for building friendGroupsData
*/
export default function useQueryAllRecoveryAttempts() {
const api = useContextApi();
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [friendGroupsMap, setFriendGroupsMap] = useState({});
const [fetchCount, setFetchCount] = useState(0);

const fetch = useCallback(() => setFetchCount((c) => c + 1), []);

useEffect(() => {
if (!api) {
Expand All @@ -30,6 +90,7 @@ export default function useQueryAllRecoveryAttempts() {
if (!api?.query.recovery?.attempt) {
setLoading(false);
setData([]);
setFriendGroupsMap({});
return;
}

Expand All @@ -41,71 +102,46 @@ export default function useQueryAllRecoveryAttempts() {
.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 = {};
const fgMap = {};

await Promise.all(
lostAccounts.map(async (account) => {
try {
const value = await api.query.recovery.friendGroups(account);
const json = value.toJSON();
friendGroupsMap[account] = json?.[0] || [];
fgMap[account] = json?.[0] || [];
} catch {
friendGroupsMap[account] = [];
fgMap[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) return;

const result = processAttempts(entries, fgMap);

if (!cancelled) {
setData(result);
setFriendGroupsMap(fgMap);
setLoading(false);
}
})
.catch((error) => {
console.error("Failed to query recovery attempts", error);
if (!cancelled) {
setData([]);
setFriendGroupsMap({});
setLoading(false);
}
});

return () => {
cancelled = true;
};
}, [api]);
}, [api, fetchCount]);

return { data, loading };
return { data, loading, fetch, friendGroupsMap };
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useContextApi } from "next-common/context/api";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";

export default function useQueryAllRecoveryInheritors() {
const api = useContextApi();
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [fetchCount, setFetchCount] = useState(0);

const fetch = useCallback(() => setFetchCount((c) => c + 1), []);

useEffect(() => {
if (!api) {
Expand Down Expand Up @@ -58,7 +61,7 @@ export default function useQueryAllRecoveryInheritors() {
return () => {
cancelled = true;
};
}, [api]);
}, [api, fetchCount]);

return { data, loading };
return { data, loading, fetch };
}
6 changes: 3 additions & 3 deletions packages/next-common/components/data/recovery/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import PageHeader from "../common/pageHeader";
import RecoveryFriendGroupsTable from "./table";
import RecoveryAttemptsTable from "./table/attempts";
import RecoveryInheritorsTable from "./table/inheritors";
import useQueryAllRecoveryData, {
import useQueryAllFriendGroups, {
flattenRecoveryData,
} from "./hooks/useQueryAllRecoveryData";
} from "./hooks/useQueryAllFriendGroups";
import useQueryAllRecoveryAttempts from "./hooks/useQueryAllRecoveryAttempts";
import useQueryAllRecoveryInheritors from "./hooks/useQueryAllRecoveryInheritors";
import { searchAddress } from "./table";
Expand All @@ -21,7 +21,7 @@ export default function RecoveryExplorer() {
const search = router.query.search || "";

const { data: friendGroupsData, loading: friendGroupsLoading } =
useQueryAllRecoveryData();
useQueryAllFriendGroups();
const { data: attemptsData, loading: attemptsLoading } =
useQueryAllRecoveryAttempts();
const { data: inheritorsData, loading: inheritorsLoading } =
Expand Down
Loading
Loading