Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: voting list #148

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
22 changes: 2 additions & 20 deletions apps/frontend-v3/app/(app)/vebal/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
'use client'

import { TokenBalancesProvider } from '@repo/lib/modules/tokens/TokenBalancesProvider'
import { DefaultPageContainer } from '@repo/lib/shared/components/containers/DefaultPageContainer'
import { PropsWithChildren } from 'react'
import { useTokens } from '@repo/lib/modules/tokens/TokensProvider'
import { CrossChainSyncProvider } from '@repo/lib/modules/vebal/cross-chain/CrossChainSyncProvider'
import { TransactionStateProvider } from '@repo/lib/modules/transactions/transaction-steps/TransactionStateProvider'

export default function VebalLayout({ children }: PropsWithChildren) {
const { vebalBptToken } = useTokens()

if (!vebalBptToken) throw new Error('vebalBptToken not found')

return (
<TokenBalancesProvider initTokens={[vebalBptToken]}>
<CrossChainSyncProvider>
<TransactionStateProvider>
<DefaultPageContainer>{children}</DefaultPageContainer>
</TransactionStateProvider>
</CrossChainSyncProvider>
</TokenBalancesProvider>
)
export default async function VeBALLayout({ children }: PropsWithChildren) {
return <DefaultPageContainer minH="100vh">{children}</DefaultPageContainer>
}
24 changes: 24 additions & 0 deletions apps/frontend-v3/app/(app)/vebal/manage/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import { TokenBalancesProvider } from '@repo/lib/modules/tokens/TokenBalancesProvider'
import { DefaultPageContainer } from '@repo/lib/shared/components/containers/DefaultPageContainer'
import { PropsWithChildren } from 'react'
import { useTokens } from '@repo/lib/modules/tokens/TokensProvider'
import { CrossChainSyncProvider } from '@repo/lib/modules/vebal/cross-chain/CrossChainSyncProvider'
import { TransactionStateProvider } from '@repo/lib/modules/transactions/transaction-steps/TransactionStateProvider'

export default function VebalLayout({ children }: PropsWithChildren) {
const { vebalBptToken } = useTokens()

if (!vebalBptToken) throw new Error('vebalBptToken not found')

return (
<TokenBalancesProvider initTokens={[vebalBptToken]}>
<CrossChainSyncProvider>
<TransactionStateProvider>
<DefaultPageContainer>{children}</DefaultPageContainer>
</TransactionStateProvider>
</CrossChainSyncProvider>
</TokenBalancesProvider>
)
}
2 changes: 1 addition & 1 deletion apps/frontend-v3/app/(app)/vebal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button, Stack } from '@chakra-ui/react'

import NextLink from 'next/link'

export default function VebalPage() {
export default function VeBALPage() {
return (
<Stack gap="lg" maxW="200px">
<Button as={NextLink} href="/vebal/manage" size="lg" variant="primary">
Expand Down
14 changes: 14 additions & 0 deletions apps/frontend-v3/app/(app)/vebal/vote/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Metadata } from 'next'
import { PropsWithChildren } from 'react'

export const metadata: Metadata = {
title: 'Vote and earn external incentives',
description: `
Voting on pool gauges helps to direct weekly BAL liquidity mining incentives.
Voters are also eligible to earn additional 3rd party voting incentives.
`,
}

export default async function Pools({ children }: PropsWithChildren) {
return <>{children}</>
}
13 changes: 11 additions & 2 deletions apps/frontend-v3/app/(app)/vebal/vote/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Stack } from '@chakra-ui/react'
import { Skeleton } from '@chakra-ui/react'
import FadeInOnView from '@repo/lib/shared/components/containers/FadeInOnView'
import { Suspense } from 'react'
import { VoteList } from '@repo/lib/modules/vebal/vote/VoteList/VoteList'

export default function VotePage() {
return <Stack>Vote</Stack>
return (
<FadeInOnView animateOnce={false}>
<Suspense fallback={<Skeleton h="500px" w="full" />}>
<VoteList />
</Suspense>
</FadeInOnView>
)
}
1 change: 1 addition & 0 deletions packages/lib/config/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface ContractsConfig {
permit2?: Address
omniVotingEscrow?: Address
gaugeWorkingBalanceHelper?: Address
gaugeController?: Address
}
export interface PoolsConfig {
issues: Partial<Record<PoolIssue, string[]>>
Expand Down
1 change: 1 addition & 0 deletions packages/lib/config/networks/mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const networkConfig: NetworkConfig = {
veDelegationProxy: '0x6f5a2eE11E7a772AeB5114A20d0D7c0ff61EB8A0',
veBAL: '0xC128a9954e6c874eA3d62ce62B468bA073093F25',
omniVotingEscrow: '0x96484f2aBF5e58b15176dbF1A799627B53F13B6d',
gaugeController: '0xC128468b7Ce63eA702C1f104D55A2566b13D3ABD',
},

pools: convertHexToLowerCase({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ export default function PoolMetaBadges() {
width={20}
/>
</Badge>
<PoolListTokenPills pool={pool} px="sm" py="2" />
<PoolListTokenPills
chain={pool.chain}
displayTokens={pool.displayTokens}
px="sm"
py="2"
type={pool.type}
/>
<PoolVersionTag isSmall pool={pool} />
<PoolTypeTag pool={pool} />
{hasHook && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ export function PoolListTableRow({ pool, keyValue, ...rest }: Props) {
</GridItem>
<GridItem>
<PoolListTokenPills
chain={pool.chain}
displayTokens={pool.displayTokens}
h={['32px', '36px']}
iconSize={20}
p={['xxs', 'sm']}
pool={pool}
pr={[1.5, 'ms']}
type={pool.type}
/>
</GridItem>
<GridItem minW="32">
Expand Down
48 changes: 25 additions & 23 deletions packages/lib/modules/pool/PoolList/PoolListTokenPills.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Badge, BadgeProps, HStack, Text, Wrap } from '@chakra-ui/react'
import { GqlChain, GqlPoolTokenDisplay } from '@repo/lib/shared/services/api/generated/graphql'
import { PoolListItem } from '../pool.types'
import {
GqlChain,
GqlPoolTokenDisplay,
GqlPoolType,
} from '@repo/lib/shared/services/api/generated/graphql'
import { TokenIcon } from '../../tokens/TokenIcon'
import { fNum } from '@repo/lib/shared/utils/numbers'
import { isStableLike, isWeightedLike } from '../pool.helpers'
import { Pool } from '../PoolProvider'

type DisplayToken = Pick<GqlPoolTokenDisplay, 'address' | 'symbol' | 'weight'>

function WeightedTokenPills({
tokens,
chain,
iconSize = 24,
...badgeProps
}: { tokens: GqlPoolTokenDisplay[]; chain: GqlChain; iconSize?: number } & BadgeProps) {
}: { tokens: DisplayToken[]; chain: GqlChain; iconSize?: number } & BadgeProps) {
return (
<Wrap spacing="xs">
{tokens.map(token => {
Expand Down Expand Up @@ -51,7 +55,7 @@ function StableTokenPills({
chain,
iconSize = 24,
...badgeProps
}: { tokens: GqlPoolTokenDisplay[]; chain: GqlChain; iconSize?: number } & BadgeProps) {
}: { tokens: DisplayToken[]; chain: GqlChain; iconSize?: number } & BadgeProps) {
const isFirstToken = (index: number) => index === 0
const zIndices = Array.from({ length: tokens.length }, (_, index) => index).reverse()

Expand Down Expand Up @@ -90,42 +94,40 @@ function StableTokenPills({
}

type Props = {
pool: Pool | PoolListItem
type: GqlPoolType
chain: GqlChain
displayTokens: DisplayToken[]
iconSize?: number
}

export function PoolListTokenPills({ pool, iconSize = 24, ...badgeProps }: Props & BadgeProps) {
const shouldUseWeightedPills = isWeightedLike(pool.type)
const shouldUseStablePills = isStableLike(pool.type)
export function PoolListTokenPills({
chain,
type,
displayTokens,
iconSize = 24,
...badgeProps
}: Props & BadgeProps) {
const shouldUseWeightedPills = isWeightedLike(type)
const shouldUseStablePills = isStableLike(type)

if (shouldUseStablePills) {
return (
<StableTokenPills
chain={pool.chain}
iconSize={iconSize}
tokens={pool.displayTokens}
{...badgeProps}
/>
<StableTokenPills chain={chain} iconSize={iconSize} tokens={displayTokens} {...badgeProps} />
)
}

if (shouldUseWeightedPills) {
return (
<WeightedTokenPills
chain={pool.chain}
chain={chain}
iconSize={iconSize}
tokens={pool.displayTokens}
tokens={displayTokens}
{...badgeProps}
/>
)
}

return (
<WeightedTokenPills
chain={pool.chain}
iconSize={iconSize}
tokens={pool.displayTokens}
{...badgeProps}
/>
<WeightedTokenPills chain={chain} iconSize={iconSize} tokens={displayTokens} {...badgeProps} />
)
}
19 changes: 10 additions & 9 deletions packages/lib/modules/pool/pool.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,26 @@ export const chainToSlugMap: Record<GqlChain, ChainSlug> = {
}
export const slugToChainMap = invert(chainToSlugMap) as Record<ChainSlug, GqlChain>

function getVariant(pool: Pool | PoolListItem): PoolVariant {
function getVariant(type: GqlPoolType, protocolVersion: number | undefined): PoolVariant {
// if a pool has certain properties return a custom variant
if (pool.type === GqlPoolType.CowAmm) return PartnerVariant.cow
if (pool.protocolVersion === 3) return BaseVariant.v3
if (type === GqlPoolType.CowAmm) return PartnerVariant.cow
if (protocolVersion === 3) return BaseVariant.v3

// default variant
return BaseVariant.v2
}

/**
* Constructs path to pool detail page.
* @param {String} id Pool ID could be ID or address depending on variant.
* @param {GqlChain} chain Chain enum.
* @param {String} variant Pool variant, defaults to v2.
* @returns {String} Path to pool detail page.
*/
export function getPoolPath(pool: Pool | PoolListItem) {
const variant = getVariant(pool)
return `/pools/${chainToSlugMap[pool.chain]}/${variant}/${pool.id}`
export function getPoolPath(
params: Pick<Pool | PoolListItem, 'id' | 'chain' | 'type'> & {
protocolVersion: number | undefined
}
) {
const variant = getVariant(params.type, params.protocolVersion)
return `/pools/${chainToSlugMap[params.chain]}/${variant}/${params.id}`
}

// TODO: the following 2 functions (getAprLabel & getTotalAprLabel) most likely need revisiting somewhere in the near future and refactored to just one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@ export function PortfolioTableRow({ pool, keyValue, veBalBoostMap, ...rest }: Pr
</GridItem>
<GridItem>
<PoolListTokenPills
chain={pool.chain}
displayTokens={pool.displayTokens}
h={['32px', '36px']}
iconSize={20}
p={['xxs', 'sm']}
pool={pool}
pr={[1.5, 'ms']}
type={pool.type}
/>
</GridItem>
<GridItem>
Expand Down
57 changes: 57 additions & 0 deletions packages/lib/modules/vebal/vote/VoteCapTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
HStack,
Text,
Popover,
PopoverTrigger,
Portal,
PopoverContent,
VStack,
} from '@chakra-ui/react'
import { VoteCapIcon } from '@repo/lib/shared/components/icons/VoteCapIcon'
import { fNum } from '@repo/lib/shared/utils/numbers'
import { VotesState } from '@repo/lib/modules/vebal/vote/vote.types'

interface Props {
relativeWeightCap: number
votesState?: VotesState
usePortal?: boolean
}

export function VoteCapTooltip({ relativeWeightCap, votesState, usePortal = true }: Props) {
const votesColor =
votesState === 'normal' ? undefined : votesState === 'close' ? 'font.warning' : 'red.400'

const voteCapText =
votesState === 'normal'
? 'vote cap'
: votesState === 'close'
? 'vote cap is close'
: 'vote cap exceeded'

const popoverContent = (
<PopoverContent bg="background.level3" minWidth={['100px', '170px']} p="sm" shadow="3xl">
<VStack alignItems="start" spacing="sm" width="full">
<Text color={votesColor ?? 'font.secondary'} fontSize="sm" fontWeight={700}>
{fNum('apr', relativeWeightCap)} {voteCapText}
</Text>
<Text color="font.secondary" fontSize="sm">
Governance by veBAL holders have set a cap on this gauge. Any votes that push the vote
above this cap will be disregarded.
</Text>
</VStack>
</PopoverContent>
)
return (
<Popover trigger="hover">
<>
<PopoverTrigger>
<HStack color={votesColor ?? 'font.secondary'}>
<VoteCapIcon height="16px" width="16px" />
</HStack>
</PopoverTrigger>

{usePortal ? <Portal>{popoverContent}</Portal> : popoverContent}
</>
</Popover>
)
}
42 changes: 42 additions & 0 deletions packages/lib/modules/vebal/vote/VoteList/VoteList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PoolListProvider } from '@repo/lib/modules/pool/PoolList/PoolListProvider'
import { VoteListLayout } from './VoteListLayout'
import { VoteListProvider } from '@repo/lib/modules/vebal/vote/VoteList/VoteListProvider'
import { getHiddenHandVotingIncentives } from '@repo/lib/modules/vebal/vote/hidden-hand/getHiddenHandVotingIncentives'
import { GetVeBalVotingListDocument } from '@repo/lib/shared/services/api/generated/graphql'
import { mins } from '@repo/lib/shared/utils/time'
import { getApolloServerClient } from '@repo/lib/shared/services/api/apollo-server.client'
import { errorToJson } from '@repo/lib/shared/utils/errors'

export async function VoteList() {
const client = getApolloServerClient()

const [{ data: voteListQueryData, error }, [votingIncentives, votingIncentivesError]] =
await Promise.all([
client.query({
query: GetVeBalVotingListDocument,
context: {
fetchOptions: {
next: { revalidate: mins(1).toSecs() },
},
},
}),
getHiddenHandVotingIncentives()
.then(value => [value, undefined] as const)
.catch(err => [undefined, err] as const),
])

return (
<VoteListProvider
data={voteListQueryData}
// Only plain objects are allowed as props within RSC
error={errorToJson(error)}
votingIncentives={votingIncentives}
votingIncentivesError={errorToJson(votingIncentivesError)}
>
{/* fix: remove PoolListProvider when voteFilters implemented */}
<PoolListProvider>
<VoteListLayout />
</PoolListProvider>
</VoteListProvider>
)
}
Loading
Loading