Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
a031f48
get data
LLPROG Aug 29, 2025
5832232
some fixes
LLPROG Sep 1, 2025
67a3c89
add localization
LLPROG Sep 1, 2025
1179373
display minute and seconds
LLPROG Sep 1, 2025
921270f
add utils
LLPROG Sep 2, 2025
5da848f
utils
LLPROG Sep 2, 2025
26859df
improve display time
LLPROG Sep 2, 2025
77d5cfe
add contract address
LLPROG Sep 2, 2025
ba02500
add connect button
LLPROG Sep 2, 2025
b816d0b
add vevote contract roles
LLPROG Sep 2, 2025
236937b
clean up
LLPROG Sep 2, 2025
9cca7fe
remove unused functions
LLPROG Sep 3, 2025
506864c
make it responsive
LLPROG Sep 3, 2025
c2dc606
add address schema
LLPROG Sep 3, 2025
7241b19
fix Admin card
LLPROG Sep 3, 2025
5f3a11d
fix localization roles
LLPROG Sep 3, 2025
fc377c1
Merge remote-tracking branch 'origin/main' into 221-create-admin-dash…
LLPROG Sep 3, 2025
0b43076
fix useContractRoles
LLPROG Sep 3, 2025
f4c8088
fix sonar issues
LLPROG Sep 3, 2025
896bd30
more sonar fixes
LLPROG Sep 3, 2025
a3e1d4c
remove duplication
LLPROG Sep 3, 2025
2fab776
fix Refactor this code to not use nested template literals
LLPROG Sep 3, 2025
8b44d56
fix stargate roles
LLPROG Sep 3, 2025
8bde8b1
fix grant/revoke role
LLPROG Sep 3, 2025
848b319
always show roles granted
LLPROG Sep 3, 2025
a5696d3
remove stargate roles info
LLPROG Sep 3, 2025
3e29111
get contract addresses from contract
LLPROG Sep 3, 2025
6ead17d
level multiplier card
LLPROG Sep 3, 2025
d5f8a11
voting power query
LLPROG Sep 3, 2025
e28389b
fix schemas
LLPROG Sep 3, 2025
8919179
table refactor
LLPROG Sep 3, 2025
57108b3
some UI fixes
LLPROG Sep 3, 2025
73ecebf
fix voting power query
LLPROG Sep 4, 2025
929d2d2
fix admin schema
LLPROG Sep 4, 2025
ea0d503
fix NodeManagementContractInfo card style
LLPROG Sep 4, 2025
29e87f8
Role Users utils
LLPROG Sep 4, 2025
dfa69e2
RoleUSerCard refinement
LLPROG Sep 4, 2025
6ae8579
add warning info
LLPROG Sep 4, 2025
1b11caa
fix actually grande role
LLPROG Sep 4, 2025
2fd1871
fix build
LLPROG Sep 4, 2025
ea11223
refinement
LLPROG Sep 4, 2025
4ce0249
remove readonly props specification
LLPROG Sep 4, 2025
e8cd383
clean up
LLPROG Sep 4, 2025
cdf9fb9
fix sonar issue
LLPROG Sep 4, 2025
49f0354
fix levelId input
LLPROG Sep 4, 2025
3e5e86a
try fix quorum numerator
LLPROG Sep 4, 2025
fe42601
try add brackets
LLPROG Sep 4, 2025
c788750
Remove optional specify twice
LLPROG Sep 5, 2025
df29cd3
remove permissions and add validator multiplier
LLPROG Sep 9, 2025
98b49c7
add validator to stargate table
LLPROG Sep 12, 2025
7852116
Merge remote-tracking branch 'origin/main' into 221-create-admin-dash…
Nov 6, 2025
97e129c
fix i18n
Nov 6, 2025
14775b3
Merge branch 'main' into 221-create-admin-dashboard
Nov 7, 2025
342c119
align with main
Nov 7, 2025
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
7 changes: 6 additions & 1 deletion apps/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { createBrowserRouter, RouterProvider, Outlet, useLocation } from "react-
import { Proposal } from "./pages/Proposal/Proposal";
import { Proposals } from "./pages/Proposals";
import { CreateProposal } from "./pages/CreateProposal/CreateProposal";
import { AdminDashboard } from "./pages/Admin/AdminDashboard";
import { Footer } from "./components/footer/Footer";
import { ScrollToTop } from "./components/ui/ScrollToTop";
import { useVoteCastInvalidate } from "./hooks/useVoteCastInvalidate";
import { useProposalCreatedInvalidate } from "./hooks/useProposalCreatedInvalidate";

const excludedRoutes = ["/create-proposal"];
const excludedRoutes = ["/create-proposal", "/admin"];

function Layout() {
const { pathname } = useLocation();
Expand Down Expand Up @@ -46,6 +47,10 @@ const router = createBrowserRouter([
path: "create-proposal",
element: <CreateProposal />,
},
{
path: "admin",
element: <AdminDashboard />,
},
],
},
]);
Expand Down
6 changes: 5 additions & 1 deletion apps/frontend/src/components/ui/InputMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ export const InputMessage = ({ error, message }: InputMessageProps) => {
<Text fontSize={"12px"}>{error}</Text>
</FormErrorMessage>
);
return <FormHelperText fontSize={"12px"}>{message}</FormHelperText>;
return (
<FormHelperText fontSize={"12px"} whiteSpace={"pre"}>
{message}
</FormHelperText>
);
};
2 changes: 2 additions & 0 deletions apps/frontend/src/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const IconByVote = {
[SingleChoiceEnum.FOR]: ThumbsUpIcon,
[SingleChoiceEnum.ABSTAIN]: AbstainIcon,
};

export const VALIDATOR_STAKED_VET_REQUIREMENT = BigInt("25000000000000000000000000"); // 25 million VET
69 changes: 69 additions & 0 deletions apps/frontend/src/hooks/useAdminUserRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { executeMultipleClauses } from "@/utils/contract";
import { getConfig } from "@repo/config";
import { VeVote__factory } from "@vechain/vevote-contracts";
import { useQuery } from "@tanstack/react-query";
import { useCallback } from "react";

const ROLES = [
"DEFAULT_ADMIN_ROLE",
"EXECUTOR_ROLE",
"SETTINGS_MANAGER_ROLE",
"NODE_WEIGHT_MANAGER_ROLE",
"UPGRADER_ROLE",
"WHITELISTED_ROLE",
"WHITELIST_ADMIN_ROLE",
] as const;

const contractAddress = getConfig(import.meta.env.VITE_APP_ENV).vevoteContractAddress;
const contractInterface = VeVote__factory.createInterface();

export const useUserAdminRoles = (userAddress: string) => {
const checkUserRoles = useCallback(async () => {
try {
const roleHashMethods = ROLES.map(role => ({
method: role,
args: [],
}));

const roleHashResults = await executeMultipleClauses({
contractAddress,
contractInterface,
methodsWithArgs: roleHashMethods,
});

const roleHashes = roleHashResults
.map((result, index) => ({
name: ROLES[index],
hash: result?.success ? result.result.plain : null,
}))
.filter(role => role.hash);

const hasRoleMethods = roleHashes.map(role => ({
method: "hasRole" as const,
args: [role.hash, userAddress],
}));

const hasRoleResults = await executeMultipleClauses({
contractAddress,
contractInterface,
methodsWithArgs: hasRoleMethods,
});

const userRoles = roleHashes
.filter((_, index) => hasRoleResults[index]?.success && hasRoleResults[index].result.plain === true)
.map(role => role.name);

return userRoles;
} catch (error) {
console.error("Error checking user roles:", error);
return [];
}
}, [userAddress]);

return useQuery({
queryKey: ["userRoles", userAddress],
queryFn: checkUserRoles,
enabled: Boolean(userAddress),
staleTime: 30000,
});
};
85 changes: 85 additions & 0 deletions apps/frontend/src/hooks/useContractRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { executeMultipleClauses } from "@/utils/contract";
import { getConfig } from "@repo/config";
import { VeVote__factory } from "@vechain/vevote-contracts";
import { NodeManagement__factory, StargateNFT__factory } from "@vechain/vevote-contracts/typechain-types";
import { useQuery } from "@tanstack/react-query";
import { useCallback } from "react";
import { useWallet } from "@vechain/vechain-kit";
import { CONTRACT_CONFIGS, ContractType } from "@/pages/Admin/constants/contracts";

const getContractInterface = (contractType: ContractType) => {
switch (contractType) {
case "vevote":
return VeVote__factory.createInterface();
case "nodeManagement":
return NodeManagement__factory.createInterface();
case "stargate":
return StargateNFT__factory.createInterface();
default:
return VeVote__factory.createInterface();
}
};

export const useContractRoles = (contractType: ContractType = "vevote") => {
const { account } = useWallet();
const config = CONTRACT_CONFIGS[contractType];
const contractAddress = getConfig(import.meta.env.VITE_APP_ENV)[config.addressKey];
const contractInterface = getContractInterface(contractType);

const checkUserRoles = useCallback(async () => {
if (!account?.address) return [];

try {
const roleHashMethods = config.roles.map(role => ({
method: role,
args: [],
}));

const roleHashResults = await executeMultipleClauses({
contractAddress,
contractInterface,
methodsWithArgs: roleHashMethods,
});

const roleHashes = roleHashResults
.map((result, index) => ({
name: config.roles[index],
hash: result?.success ? result.result.plain : null,
}))
.filter(role => role.hash);

const hasRoleMethods = roleHashes.map(role => ({
method: "hasRole" as const,
args: [role.hash, account.address],
}));

const hasRoleResults = await executeMultipleClauses({
contractAddress,
contractInterface,
methodsWithArgs: hasRoleMethods,
});

const userRoles = roleHashes
.filter((_, index) => hasRoleResults[index]?.success && hasRoleResults[index].result.plain === true)
.map(role => role.name);

return userRoles;
} catch (error) {
console.error("Error checking user roles:", error);
return [];
}
}, [account?.address, contractAddress, config.roles, contractInterface]);

const query = useQuery({
queryKey: ["contractRoles", contractType, account?.address],
queryFn: checkUserRoles,
enabled: Boolean(account?.address),
staleTime: 30000,
});

return {
defaultRoles: config.roles,
roles: query.data || [],
isLoading: query.isLoading,
};
};
29 changes: 29 additions & 0 deletions apps/frontend/src/hooks/useFormatTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback } from "react";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { useI18nContext } from "@/i18n/i18n-react";

dayjs.extend(duration);

export const useFormatTime = () => {
const { LL } = useI18nContext();

const formatTime = useCallback((seconds: number) => {
if (seconds < 60) {
return LL.common.time.seconds({ count: seconds });
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
return LL.common.time.minutes({ count: minutes });
} else if (seconds < 86400) {
const hours = Math.floor(seconds / 3600);
return LL.common.time.hours({ count: hours });
} else {
const days = Math.floor(seconds / 86400);
return LL.common.time.days({ count: days });
}
}, [LL.common.time]);

return {
formatTime,
};
};
94 changes: 94 additions & 0 deletions apps/frontend/src/hooks/useGovernanceSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { getConfig } from "@repo/config";
import { VeVote__factory } from "@vechain/vevote-contracts";
import { EnhancedClause } from "@vechain/vechain-kit";
import { useVevoteSendTransaction } from "@/utils/hooks/useVevoteSendTransaction";
import { useCallback } from "react";

type GovernanceSettingsProps = {
updateQuorumNumerator?: number;
setMinVotingDelay?: number;
setMinVotingDuration?: number;
setMaxVotingDuration?: number;
setMinStakedVetAmount?: bigint;
};

const contractAddress = getConfig(import.meta.env.VITE_APP_ENV).vevoteContractAddress;
const contractInterface = VeVote__factory.createInterface();

export const useGovernanceSettings = () => {
const buildClauses = useCallback((props: GovernanceSettingsProps) => {
const clauses: EnhancedClause[] = [];

try {
const baseClause = {
to: contractAddress,
value: 0,
};

if (props.updateQuorumNumerator !== undefined) {
const clause = {
...baseClause,
data: contractInterface.encodeFunctionData("updateQuorumNumerator", [props.updateQuorumNumerator]),
abi: JSON.parse(JSON.stringify(contractInterface.getFunction("updateQuorumNumerator"))),
comment: `Update quorum numerator to ${props.updateQuorumNumerator}`,
};
clauses.push(clause as EnhancedClause);
}

if (props.setMinVotingDelay !== undefined) {
const blocksValue = Math.floor(props.setMinVotingDelay / 10);
const clause = {
...baseClause,
data: contractInterface.encodeFunctionData("setMinVotingDelay", [blocksValue]),
abi: JSON.parse(JSON.stringify(contractInterface.getFunction("setMinVotingDelay"))),
comment: `Set min voting delay to ${props.setMinVotingDelay} seconds (${blocksValue} blocks)`,
};
clauses.push(clause as EnhancedClause);
}

if (props.setMinVotingDuration !== undefined) {
const blocksValue = Math.floor(props.setMinVotingDuration / 10);
const clause = {
...baseClause,
data: contractInterface.encodeFunctionData("setMinVotingDuration", [blocksValue]),
abi: JSON.parse(JSON.stringify(contractInterface.getFunction("setMinVotingDuration"))),
comment: `Set min voting duration to ${props.setMinVotingDuration} seconds (${blocksValue} blocks)`,
};
clauses.push(clause as EnhancedClause);
}

if (props.setMaxVotingDuration !== undefined) {
const blocksValue = Math.floor(props.setMaxVotingDuration / 10);
const clause = {
...baseClause,
data: contractInterface.encodeFunctionData("setMaxVotingDuration", [blocksValue]),
abi: JSON.parse(JSON.stringify(contractInterface.getFunction("setMaxVotingDuration"))),
comment: `Set max voting duration to ${props.setMaxVotingDuration} seconds (${blocksValue} blocks)`,
};
clauses.push(clause as EnhancedClause);
}

if (props.setMinStakedVetAmount !== undefined) {
const clause = {
...baseClause,
data: contractInterface.encodeFunctionData("setMinStakedVetAmount", [props.setMinStakedVetAmount]),
abi: JSON.parse(JSON.stringify(contractInterface.getFunction("setMinStakedVetAmount"))),
comment: `Set minimum staked VET amount to ${props.setMinStakedVetAmount.toString()}`,
};
clauses.push(clause as EnhancedClause);
}

return clauses;
} catch (error) {
console.error(error);
return [];
}
}, []);

return useVevoteSendTransaction({
clauseBuilder: buildClauses,
delayedRefetchKeys: [["veVoteInfo"]],
refetchDelay: 500,
maxRetries: 1,
});
};
45 changes: 45 additions & 0 deletions apps/frontend/src/hooks/useLevelMultipliers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getConfig } from "@repo/config";
import { VeVote__factory } from "@vechain/vevote-contracts";
import { EnhancedClause } from "@vechain/vechain-kit";
import { useVevoteSendTransaction } from "@/utils/hooks/useVevoteSendTransaction";
import { useCallback } from "react";

type LevelMultipliersProps = {
updateLevelIdMultipliers: number[];
};

const contractAddress = getConfig(import.meta.env.VITE_APP_ENV).vevoteContractAddress;
const contractInterface = VeVote__factory.createInterface();

export const useLevelMultipliers = () => {
const buildClauses = useCallback((props: LevelMultipliersProps) => {
const clauses: EnhancedClause[] = [];

try {
const baseClause = {
to: contractAddress,
value: 0,
};

const clause = {
...baseClause,
data: contractInterface.encodeFunctionData("updateLevelIdMultipliers", [props.updateLevelIdMultipliers]),
abi: JSON.parse(JSON.stringify(contractInterface.getFunction("updateLevelIdMultipliers"))),
comment: `Update level ID multipliers: [${props.updateLevelIdMultipliers.join(", ")}]`,
};
clauses.push(clause as EnhancedClause);

return clauses;
} catch (error) {
console.error(error);
return [];
}
}, []);

return useVevoteSendTransaction({
clauseBuilder: buildClauses,
delayedRefetchKeys: [["veVoteInfo"], ["levelMultipliers"]],
refetchDelay: 500,
maxRetries: 1,
});
};
Loading