diff --git a/contracts/lib/breadchain b/contracts/lib/breadchain
index 952757e8..006c2006 160000
--- a/contracts/lib/breadchain
+++ b/contracts/lib/breadchain
@@ -1 +1 @@
-Subproject commit 952757e85598922624ddf0d9f4bb363c04e72f25
+Subproject commit 006c2006e8e37c7a15f4c2c10c600449fa3918e1
diff --git a/src/abi/Distributor.ts b/src/abi/Distributor.ts
index 9f2db166..66595685 100644
--- a/src/abi/Distributor.ts
+++ b/src/abi/Distributor.ts
@@ -57,12 +57,30 @@ export const distributorAbi = [
{
type: "function",
name: "allowlistedMultipliers",
- inputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ inputs: [{ name: "index", type: "uint256", internalType: "uint256" }],
outputs: [
- { name: "", type: "address", internalType: "contract IMultiplier" },
+ {
+ name: "multiplier",
+ type: "address",
+ internalType: "contract IMultiplier",
+ },
],
stateMutability: "view",
},
+ {
+ type: "function",
+ name: "calculateTotalMultipliers",
+ inputs: [
+ { name: "_user", type: "address", internalType: "address" },
+ {
+ name: "_multiplierIndexes",
+ type: "uint256[]",
+ internalType: "uint256[]",
+ },
+ ],
+ outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
+ stateMutability: "nonpayable",
+ },
{
type: "function",
name: "castVote",
@@ -136,20 +154,6 @@ export const distributorAbi = [
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
- {
- type: "function",
- name: "getTotalMultipliers",
- inputs: [
- { name: "_user", type: "address", internalType: "address" },
- {
- name: "_multiplierIndexes",
- type: "uint256[]",
- internalType: "uint256[]",
- },
- ],
- outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
- stateMutability: "view",
- },
{
type: "function",
name: "getValidMultiplierIndexes",
@@ -178,7 +182,11 @@ export const distributorAbi = [
name: "initialize",
inputs: [
{ name: "_bread", type: "address", internalType: "address" },
- { name: "_butteredBread", type: "address", internalType: "address" },
+ {
+ name: "_butteredBread",
+ type: "address",
+ internalType: "address",
+ },
{ name: "_precision", type: "uint256", internalType: "uint256" },
{
name: "_minRequiredVotingPower",
@@ -186,7 +194,11 @@ export const distributorAbi = [
internalType: "uint256",
},
{ name: "_maxPoints", type: "uint256", internalType: "uint256" },
- { name: "_cycleLength", type: "uint256", internalType: "uint256" },
+ {
+ name: "_cycleLength",
+ type: "uint256",
+ internalType: "uint256",
+ },
{
name: "_yieldFixedSplitDivisor",
type: "uint256",
@@ -197,11 +209,22 @@ export const distributorAbi = [
type: "uint256",
internalType: "uint256",
},
- { name: "_projects", type: "address[]", internalType: "address[]" },
+ {
+ name: "_projects",
+ type: "address[]",
+ internalType: "address[]",
+ },
],
outputs: [],
stateMutability: "nonpayable",
},
+ {
+ type: "function",
+ name: "initialize",
+ inputs: [],
+ outputs: [],
+ stateMutability: "nonpayable",
+ },
{
type: "function",
name: "lastClaimedBlockNumber",
@@ -320,7 +343,11 @@ export const distributorAbi = [
type: "function",
name: "setButteredBread",
inputs: [
- { name: "_butteredBread", type: "address", internalType: "address" },
+ {
+ name: "_butteredBread",
+ type: "address",
+ internalType: "address",
+ },
],
outputs: [],
stateMutability: "nonpayable",
diff --git a/src/app/core/components/Icons/CheckDouble.tsx b/src/app/core/components/Icons/CheckDouble.tsx
new file mode 100644
index 00000000..3b520240
--- /dev/null
+++ b/src/app/core/components/Icons/CheckDouble.tsx
@@ -0,0 +1,18 @@
+function CheckDoubleIcon() {
+ return (
+
+
+
+ );
+}
+
+export default CheckDoubleIcon;
+
diff --git a/src/app/core/components/MobileMenu/MobileNavigation.tsx b/src/app/core/components/MobileMenu/MobileNavigation.tsx
index 2878abd2..b168d380 100644
--- a/src/app/core/components/MobileMenu/MobileNavigation.tsx
+++ b/src/app/core/components/MobileMenu/MobileNavigation.tsx
@@ -75,6 +75,29 @@ export function MobileNavigation({ handleNavToggle }: IProps) {
Vaults
+ {" "}
+ handleNavToggle()}
+ className="text-neutral-900 dark:text-breadgray-rye dark:hover:text-breadgray-light-grey flex gap-2 items-center justify-end px-2"
+ >
+
+
+
+
+
+
+ Boosters
>
)}
diff --git a/src/app/governance/boosters/BoosterPage.tsx b/src/app/governance/boosters/BoosterPage.tsx
new file mode 100644
index 00000000..9c260102
--- /dev/null
+++ b/src/app/governance/boosters/BoosterPage.tsx
@@ -0,0 +1,55 @@
+"use client";
+import { useEffect, useState } from "react";
+import { useConnectedUser } from "@/app/core/hooks/useConnectedUser";
+import { getConfig } from "@/app/core/hooks/WagmiProvider/config/getConfig";
+import { PageGrid } from "@/app/governance/components/PageGrid";
+import { VotingPowerPanel } from "./components/VotingPowerPanel";
+import { BoosterCard } from "./components/BoosterCard";
+import { boostData, mapBoostToCardProps } from "./data/BoostData";
+import Button from "@/app/core/components/Button";
+
+export function BoosterPage() {
+ const { user } = useConnectedUser();
+
+ return (
+
+
+
+
+
+
+
+
+
All boosters?
+
+ {BoosterList()}
+
+
+
+ );
+}
+
+function TitleSection() {
+ return (
+
+
+ Voting Power Boosters
+
+
+
+ Active voter? Top baker? If this is you, the Breadchain cooperative
+ rewards you with voting power boosters for your engaging support of
+ post-capitalism. Be active and your voice will be amplified.{" "}
+
+
+
+ );
+}
+
+function BoosterList() {
+ return boostData.map((boost, index) => {
+ return (
+
+ );
+ });
+}
diff --git a/src/app/governance/boosters/components/BoosterCard.tsx b/src/app/governance/boosters/components/BoosterCard.tsx
new file mode 100644
index 00000000..401069e6
--- /dev/null
+++ b/src/app/governance/boosters/components/BoosterCard.tsx
@@ -0,0 +1,229 @@
+import { ReactElement } from "react";
+import { CheckIcon } from "@/app/core/components/Icons/CheckIcon";
+import Tooltip from "@/app/core/components/Tooltip";
+import BoosterIcon, {
+ IconName,
+} from "@/app/governance/boosters/components/BoosterIcon";
+import { Boost } from "@/app/governance/boosters/data/BoostData";
+import { useModal } from "@/app/core/context/ModalContext";
+import { DetailedBoosterCard } from "./DetailedBoosterCard";
+import { mapBoostToDetailedCardProps } from "../data/BoostData";
+
+export function BoosterCard({
+ boost,
+ iconName,
+ boosterName,
+ verified,
+ boostAmount,
+ descriptionShort,
+ descriptionLong,
+ expiration,
+ expirationUrgent = false,
+}: {
+ boost: Boost;
+ iconName: string;
+ boosterName: string;
+ verified: boolean;
+ boostAmount: string;
+ descriptionShort: string;
+ descriptionLong: string;
+ expiration: number | undefined;
+ expirationUrgent: boolean;
+}) {
+ return (
+
+ {header(iconName, boosterName, verified)}
+ {boostPowerSection(boostAmount)}
+
{descriptionShort}
+
+ {expiry(expiration, expirationUrgent, "Helpful information loading...")}
+
+ );
+}
+
+export function header(
+ iconName: string,
+ boosterName: string,
+ verified: boolean,
+ extraItem?: ReactElement | null
+): ReactElement {
+ return (
+
+
+ {getIcon(iconName)}
+
+ {boosterName}
+
+ {extraItem && verifiedBadge(verified)}
+
+ {extraItem ? extraItem : verifiedBadge(verified)}
+
+ );
+}
+
+function verifiedBadge(verified: boolean): ReactElement {
+ const colorClass = verified
+ ? "text-status-success bg-status-success/10"
+ : "bg-[rgba(152,151,151,0.1)]";
+ return (
+
+ {verified ? "Verified" : "Unverified"}
+
+ );
+}
+
+export function boostPowerSection(amount: string): ReactElement {
+ return (
+
+
+ {/* This element simply adds the gradient. It has opacity and layers ontop of the solid background */}
+
+
+ x{amount}
+
+
+
Voting power boost
+
Helpful information loading...
+
+
+ );
+}
+
+function getIcon(iconName: string): ReactElement {
+ // To be updated once we have actual icons
+ const iconNames = Object.values(IconName);
+ const randomIconName = iconNames[
+ Math.floor(Math.random() * iconNames.length)
+ ] as IconName;
+ return (
+
+ );
+}
+
+// This is React function since it makes use of useModel, which requires special React magic
+const ViewButton = ({
+ verified,
+ boost,
+}: {
+ verified: boolean;
+ boost: Boost;
+}) => {
+ const { setModal } = useModal();
+
+ const openModal = () => {
+ setModal({
+ type: "GENERIC_MODAL",
+ showCloseButton: false,
+ includeContainerStyling: false,
+ children: (
+ setModal(null)}
+ {...mapBoostToDetailedCardProps(boost)}
+ />
+ ),
+ });
+ };
+
+ const buttonStyles = verified
+ ? "bg-[rgba(152,151,151,0.1)] dark:bg-breadgray-charcoal text-status-success" // Verified
+ : "bg-[#FFCCF1] dark:bg-[#402639] text-breadviolet-violet dark:text-breadpink-shaded"; // Not verified
+
+ const verifiedButtonContent = (
+
+
+
+
+
Verified
+
+ view
+
+
+ );
+
+ return boosterCardButton(
+ openModal,
+ buttonStyles,
+ verified ? verifiedButtonContent : "View"
+ );
+};
+
+export function boosterCardButton(
+ action: () => void,
+ styles: String,
+ content: String | ReactElement
+): ReactElement {
+ return (
+ action()}
+ className={`
+ w-full h-[50px] mb-[10px] rounded-[10px]
+ flex items-center justify-center
+ ${styles}
+ `}
+ >
+ {typeof content === "string" ? (
+ {content}
+ ) : (
+ content
+ )}
+
+ );
+}
+
+export function expiry(
+ expiration: number | undefined,
+ expirationUrgent: boolean,
+ tooltipContent: string
+): ReactElement | undefined {
+ if (!expiration) {
+ return undefined;
+ }
+ const textColorClass = expirationUrgent
+ ? "text-[#F2D54E]"
+ : "text-breadgray-grey";
+ return (
+
+
+ {expiration} days until booster expires
+
+ {tooltipContent}
+
+ );
+}
diff --git a/src/app/governance/boosters/components/BoosterIcon.tsx b/src/app/governance/boosters/components/BoosterIcon.tsx
new file mode 100644
index 00000000..9ddb1c57
--- /dev/null
+++ b/src/app/governance/boosters/components/BoosterIcon.tsx
@@ -0,0 +1,42 @@
+import React, { ReactElement } from "react";
+import { Metamask } from "@/app/core/components/Icons/Metamask";
+import { Coinbase } from "@/app/core/components/Icons/Coinbase";
+import { BreadSVG } from "@/app/core/components/Icons/Bread";
+
+// To be filled in once we have the appropriate icons
+export enum IconName {
+ MetaMask = "metaMask",
+ Coinbase = "coinBase",
+ Network = "network"
+}
+
+interface SvgIconProps {
+ name: IconName;
+ className?: string;
+}
+
+const getIcon = (name: IconName): ReactElement => {
+ switch (name) {
+ case "metaMask":
+ return Metamask();
+ case "coinBase":
+ return Coinbase();
+ case "network":
+ return BreadSVG({});
+ default:
+ throw new Error("Invalid icon name");
+ }
+};
+
+function BoosterIcon({ name, className }: SvgIconProps) {
+ const wrapperClasses = `inline-flex items-center justify-center rounded-full overflow-hidden w-[45px] h-[45px] ${className || ""}`;
+ const IconComponent = getIcon(name);
+
+ return (
+
+ {IconComponent}
+
+ );
+}
+
+export default BoosterIcon;
diff --git a/src/app/governance/boosters/components/DetailedBoosterCard.tsx b/src/app/governance/boosters/components/DetailedBoosterCard.tsx
new file mode 100644
index 00000000..c2072baf
--- /dev/null
+++ b/src/app/governance/boosters/components/DetailedBoosterCard.tsx
@@ -0,0 +1,192 @@
+import React, { ReactElement } from "react";
+import {
+ header,
+ boostPowerSection,
+ boosterCardButton,
+ expiry,
+} from "@/app/governance/boosters/components/BoosterCard";
+import CloseIcon from "@/app/core/components/Icons/CloseIcon";
+import CheckDoubleIcon from "@/app/core/components/Icons/CheckDouble";
+
+export interface ProgressItem {
+ title: string;
+ value: number;
+ achieved: boolean;
+}
+
+export function DetailedBoosterCard({
+ iconName,
+ boosterName,
+ verified,
+ boostAmount,
+ descriptionShort,
+ descriptionLong,
+ expiration,
+ expirationUrgent = false,
+ progress,
+ requirements,
+ close,
+}: {
+ iconName: string;
+ boosterName: string;
+ verified: boolean;
+ boostAmount: string;
+ descriptionShort: string;
+ descriptionLong: string;
+ expiration: number | undefined;
+ expirationUrgent: boolean;
+ progress: ProgressItem[];
+ requirements: ProgressItem[];
+ close: () => void;
+}) {
+ return (
+
+ {header(iconName, boosterName, verified, closeIcon(close))}
+ {boostPowerSection(boostAmount)}
+ {detailsSection(descriptionLong, requirements, progress, boostAmount)}
+ {buttons()}
+ {expiry(expiration, expirationUrgent, "Helpful information loading...")}
+
+ );
+}
+
+function closeIcon(close: () => void): ReactElement {
+ return (
+
+ {CloseIcon()}
+
+ );
+}
+
+function buttons(): ReactElement {
+ const buttonStyleVerify =
+ "bg-[#FFCCF1] dark:bg-[#402639] text-breadviolet-violet dark:text-breadpink-shaded";
+ const buttonStyleGet =
+ "text-[#FFCCF1] dark:text-[#402639] bg-breadviolet-violet dark:bg-breadpink-shaded";
+ return (
+ <>
+ {boosterCardButton(close, buttonStyleGet, "Get")}
+ {boosterCardButton(close, buttonStyleVerify, "Verify")}
+ >
+ );
+}
+
+function detailsSection(
+ description: String,
+ requirements: ProgressItem[],
+ progress: ProgressItem[],
+ boostAmount: string
+): ReactElement {
+ return (
+
+ {progress.length > 0 && boostProgress(progress, Number(boostAmount))}
+ {requirements.length > 0 && requirementsList(requirements)}
+
{description}
+
+ );
+}
+
+// Displays the horizontal progress bar with text beneath it
+// Note, this component expects Boost Progress to be ordered with completed items at the start.
+function boostProgress(
+ progress: ProgressItem[],
+ boostAmount: number
+): ReactElement {
+ let numberItems = progress.length;
+ let numberCompleted = progress.findIndex((item) => item.value > boostAmount);
+ if (numberCompleted == -1) {
+ numberCompleted = numberItems;
+ }
+ const percentComplete =
+ numberItems > 0 ? (numberCompleted / numberItems) * 100 : 5;
+ return (
+
+ {progressBar(percentComplete)}
+ {/* Sequential items display */}
+
+ {progress.map((item, index) => (
+
+ {item.title != "" && (
+
+ {progressText(item.value.toString())}
+
{item.title}
+
+ )}
+
+ ))}
+
+
+ );
+}
+
+function progressText(title: String | null): ReactElement {
+ return (
+
+ x{title}
+
+ );
+}
+
+function progressBar(percentComplete: Number): ReactElement {
+ return (
+
+ );
+}
+
+function requirementsList(requirements: ProgressItem[]): ReactElement {
+ return (
+
+ {requirements.map((item) => (
+
+ {requirement(item.title, item.achieved)}
+
+ ))}
+
+ );
+}
+
+// Displays a requirement line item, with the tick or cross icon next to it
+function requirement(text: String, complete: Boolean): ReactElement {
+ return (
+
+ {complete ? (
+
+ {CheckDoubleIcon()}
+
+ ) : (
+
+ {CloseIcon()}
+
+ )}
+
+ {text}
+
+
+ );
+}
diff --git a/src/app/governance/boosters/components/VotingPowerPanel.tsx b/src/app/governance/boosters/components/VotingPowerPanel.tsx
new file mode 100644
index 00000000..fa8ab7d4
--- /dev/null
+++ b/src/app/governance/boosters/components/VotingPowerPanel.tsx
@@ -0,0 +1,228 @@
+import { ReactElement } from "react";
+import { CardBox } from "@/app/core/components/CardBox";
+import { FistIcon } from "@/app/core/components/Icons/FistIcon";
+import { AccountMenu } from "@/app/core/components/Header/AccountMenu";
+import { LinkIcon } from "@/app/core/components/Icons/LinkIcon";
+import {
+ TUserConnected,
+ useConnectedUser,
+} from "@/app/core/hooks/useConnectedUser";
+import { useVotingPower } from "../../context/VotingPowerContext";
+import { formatBalance } from "@/app/core/util/formatter";
+import { useCurrentAccumulatedVotingPower } from "../../useCurrentAccumulatedVotingPower";
+import Elipsis from "@/app/core/components/Elipsis";
+import { useTokenBalances } from "@/app/core/context/TokenBalanceContext/TokenBalanceContext";
+
+import { useCycleLength } from "../../useCycleLength";
+import Tooltip from "@/app/core/components/Tooltip";
+import { useDistributions } from "../../useDistributions";
+import { useVotingPowerMultiplier } from "../../useVotingPowerMultiplier";
+
+export function VotingPowerPanel() {
+ const { user } = useConnectedUser();
+ const votingPower = useVotingPower();
+ const { totalDistributions } = useDistributions();
+ const { BREAD } = useTokenBalances();
+
+ const renderFormattedDecimalNumber = (
+ number: string,
+ icon?: ReactElement
+ ) => {
+ const part1 = number.split(".")[0];
+ const part2 = number.split(".")[1];
+
+ return (
+
+
+ {icon &&
{icon}
}
+
{part1}
+
+
.
+
{part2}
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ MY VOTING POWER
+
+
+
+
+ {votingPower &&
+ votingPower.bread.status === "success" &&
+ votingPower.butteredBread.status === "success" ? (
+ renderFormattedDecimalNumber(
+ formatBalance(
+ Number(
+ votingPower.bread.value +
+ votingPower.butteredBread.value
+ ) /
+ 10 ** 18,
+ 1
+ ),
+
+ )
+ ) : (
+
+
+ -
+
+ )}
+
+
+
+ {user.status === "CONNECTED" ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>>
+ )}
+
+
+ Accessible voting power
+
+ Your total available voting power for the current voting cycle
+ #{totalDistributions ? totalDistributions + 1 + "." : "-"}
+
+
+
+
+ {/* voting power grid */}
+
+
+
+
+ Voting power from locked LP
+
+
+
+ {votingPower && votingPower.butteredBread.status === "success"
+ ? formatBalance(
+ Number(votingPower.butteredBread.value) / 10 ** 18,
+ 1
+ )
+ : "-"}
+
+
+
+ Voting power from $BREAD
+
+
+ {votingPower && votingPower.bread.status === "success"
+ ? formatBalance(Number(votingPower.bread.value) / 10 ** 18, 1)
+ : "-"}
+
+
+
+
+ Total locked LP tokens
+
+
+
+ TODO
+
+
+ {user.status === "CONNECTED" && (
+ <>
+
+ Total $BREAD baked
+
+
+
+ {BREAD && BREAD.status === "SUCCESS"
+ ? formatBalance(parseFloat(BREAD.value), 2)
+ : "-"}
+
+ >
+ )}
+
+ {user.status === "CONNECTED" ? (
+ <>
+
+
+
+
+ Pending voting power
+
+ The voting power you will receive in the next voting
+ cycle.
+
+
+
+
+
+ TODO{" "}
+
+ >
+ ) : (
+ <>>
+ )}
+
+ {user.status === "CONNECTED" ? (
+ <>>
+ ) : (
+
+ )}
+
+
+ How does this work?
+
+
+
+
+
+
+
+
+ );
+}
+
+function VotingMultiplierDisplay({ user }: { user: TUserConnected }) {
+ const {
+ status: votingPowerMultiplierStatus,
+ data: votingPowerMultiplierData,
+ } = useVotingPowerMultiplier(user);
+
+ return votingPowerMultiplierStatus === "success" &&
+ votingPowerMultiplierData ? (
+ formatBalance(Number(votingPowerMultiplierData) / 10 ** 18, 2)
+ ) : (
+
+ );
+}
+
+function DividerWithText({ text }: { text: string }) {
+ return (
+
+ );
+}
+
+function Divider() {
+ return (
+
+ );
+}
diff --git a/src/app/governance/boosters/data/BoostData.tsx b/src/app/governance/boosters/data/BoostData.tsx
new file mode 100644
index 00000000..88ab8115
--- /dev/null
+++ b/src/app/governance/boosters/data/BoostData.tsx
@@ -0,0 +1,127 @@
+// Boost is just an example interface, to be replaced by whatever
+// aggregate of data sources end up being needed.
+// As long as the mapping functions can still map the real data
+// sources to the input props, all will be well.
+export interface Boost {
+ iconName: string;
+ boosterName: string;
+ verified: boolean;
+ boostAmount: string;
+ descriptionShort: string;
+ descriptionLong: string;
+ expiration: Date;
+ expirationUrgent: boolean;
+ progress: BoostProgress[];
+ requirements: BoostRequirement[];
+}
+
+export interface BoostProgress {
+ name: string;
+ subtitle: string;
+ value: number;
+ achieved: boolean;
+}
+
+export interface BoostRequirement {
+ name: string;
+ achieved: boolean;
+}
+
+// Mapping Boost to view properties
+export function mapBoostToCardProps(boost: Boost) {
+ const now = new Date();
+ const timeDifference = boost.expiration.getTime() - now.getTime();
+ const daysUntilExpiration = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));
+
+ return {
+ iconName: boost.iconName,
+ boosterName: boost.boosterName,
+ verified: boost.verified,
+ boostAmount: boost.boostAmount,
+ descriptionShort: boost.descriptionShort,
+ descriptionLong: boost.descriptionLong,
+ expiration: daysUntilExpiration,
+ expirationUrgent: boost.expirationUrgent,
+ };
+}
+
+export function mapBoostToDetailedCardProps(boost: Boost) {
+ const baseProps = mapBoostToCardProps(boost);
+ const progress = boost.progress.map((item) => ({
+ title: item.name,
+ subtitle: item.subtitle,
+ value: item.value,
+ }));
+ const requirements = boost.requirements.map((item) => ({
+ title: item.name,
+ subtitle: null,
+ achieved: item.achieved,
+ }));
+
+ return {
+ ...baseProps,
+ progress: progress,
+ requirements: requirements,
+ };
+}
+
+const boostData: Boost[] = [
+ {
+ iconName: "bullseye",
+ boosterName: "Voting Streaks",
+ verified: false, // TODO: should check if user has an active boost and if so, set to true
+ boostAmount: "1.02", // TODO: should fetch from smart contract
+ descriptionShort: "Maintain a Breadchain voting cycle streak.",
+ descriptionLong:
+ "Maintain a Breadchain voting cycle streak. Break your streak and you will have to start over.",
+ expiration: new Date("2025-12-31"), // TODO: should fetch from userToValidUntil
+ expirationUrgent: false, // TODO: should be true if expiration is within 30 days
+ progress: [
+ {
+ name: "Start (no booster)",
+ subtitle: "",
+ value: 1.0,
+ achieved: true,
+ },
+ {
+ name: "",
+ subtitle: "",
+ value: 1.01,
+ achieved: true,
+ },
+ {
+ name: "Streak of 2 voting cycles",
+ subtitle: "",
+ value: 1.02,
+ achieved: true,
+ },
+ {
+ name: "",
+ subtitle: "",
+ value: 1.03,
+ achieved: true,
+ },
+ {
+ name: "Streak of 4 voting cycles",
+ subtitle: "",
+ value: 1.04,
+ achieved: false,
+ },
+ {
+ name: "",
+ subtitle: "",
+ value: 1.05,
+ achieved: true,
+ },
+ {
+ name: "Streak of 6 voting cycles",
+ subtitle: "",
+ value: 1.06,
+ achieved: false,
+ },
+ ],
+ requirements: [],
+ },
+];
+
+export { boostData };
diff --git a/src/app/governance/boosters/page.tsx b/src/app/governance/boosters/page.tsx
new file mode 100644
index 00000000..0d8eb3d7
--- /dev/null
+++ b/src/app/governance/boosters/page.tsx
@@ -0,0 +1,18 @@
+import { notFound } from "next/navigation";
+import { Metadata } from "next";
+import { parseFeatureVar } from "@/app/core/util/parseFeatureVar";
+import { BoosterPage } from "./BoosterPage";
+
+export const metadata: Metadata = {
+ title: "Voting power boosters",
+ description:
+ "Get rewarded with voting power boosters. Fund post-capitalist web3.",
+};
+
+export default function Boosters() {
+ if (!parseFeatureVar(process.env.FEATURE_BOOSTERS)) {
+ notFound();
+ }
+
+ return ;
+}
diff --git a/src/app/governance/layout.tsx b/src/app/governance/layout.tsx
index 47e1fbce..b1b0ecda 100644
--- a/src/app/governance/layout.tsx
+++ b/src/app/governance/layout.tsx
@@ -67,6 +67,23 @@ function GovernanceNavigation() {
LP Vaults
+
+
+ Voting power boosters
+
);
diff --git a/src/app/governance/useVotingPowerMultiplier.tsx b/src/app/governance/useVotingPowerMultiplier.tsx
new file mode 100644
index 00000000..e9e4fdab
--- /dev/null
+++ b/src/app/governance/useVotingPowerMultiplier.tsx
@@ -0,0 +1,20 @@
+import { TUserConnected } from "@/app/core/hooks/useConnectedUser";
+import { getChain } from "@/chainConfig";
+import { DISTRIBUTOR_ABI } from "@/abi";
+import { useReadContract } from "wagmi";
+
+export function useVotingPowerMultiplier(user: TUserConnected) {
+ const chainConfig = getChain(user.chain.id);
+
+ const { status, data } = useReadContract({
+ address: chainConfig.DISBURSER.address,
+ abi: DISTRIBUTOR_ABI,
+ functionName: "getTotalMultipliers",
+ args: [user.address],
+ });
+
+ return {
+ data,
+ status,
+ };
+}