Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion packages/next-common/components/data/common/pageHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function PageHeader({ href = "" }) {
<div className="w-full py-6 flex items-center justify-center">
<TitleContainer className="!text20Bold">
{title}
<PolkadotWikiLink href={href} />
{href && <PolkadotWikiLink href={href} />}
</TitleContainer>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions packages/next-common/components/data/context/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ function generateTabs() {
});
}

if (modules?.recovery) {
TABS.push({
tabId: "/recovery",
tabTitle: "Recovery",
pageTitle: "Recovery Explorer",
});
}

return TABS;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/next-common/components/data/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -27,3 +28,11 @@ export function DataMultisig() {
</DataPageWithLayout>
);
}

export function DataRecovery() {
return (
<DataPageWithLayout>
<RecoveryExplorer />
</DataPageWithLayout>
);
}
Original file line number Diff line number Diff line change
@@ -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 };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
if (!api) {
return;
}

if (!api?.query.recovery?.friendGroups) {
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();
const jsonValue = value.toJSON();
const friendGroupVec = Array.isArray(jsonValue?.[0])
? jsonValue[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 };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useContextApi } from "next-common/context/api";
import { useEffect, useState } from "react";

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

useEffect(() => {
if (!api) {
return;
}

if (!api?.query.recovery?.inheritor) {
setLoading(false);
setData([]);
return;
}

let cancelled = false;
setLoading(true);

api.query.recovery.inheritor
.entries()
.then((entries) => {
if (cancelled) return;

const result = entries.map(([storageKey, value]) => {
const account = storageKey.args?.[0]?.toString();
const jsonValue = value.toJSON();

const inheritancePriority = parseInt(jsonValue?.[0]) || 0;
const inheritor = jsonValue?.[1] || "";
const consideration = jsonValue?.[2] || {};
const depositor = consideration.depositor || "";
const ticket = parseInt(consideration.ticket) || 0;

return {
account,
inheritancePriority,
inheritor,
depositor,
ticket,
};
});

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

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

return { data, loading };
}
Loading
Loading