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: stake page [NET-165] #64

Draft
wants to merge 44 commits into
base: miguel/update-contracts
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7014ae7
add sepolia river token address
miguel-nascimento Dec 11, 2024
a4a6c8d
"""redelegate"""
miguel-nascimento Dec 11, 2024
7f1f794
remove river token mainnet
miguel-nascimento Dec 13, 2024
2758cb8
node operator contracts
miguel-nascimento Dec 14, 2024
713f751
dump
miguel-nascimento Dec 16, 2024
92cbf68
dark color-scheme
miguel-nascimento Dec 16, 2024
b0db5b1
redelegate modal
miguel-nascimento Dec 16, 2024
97402b6
join deposits <> node data
miguel-nascimento Dec 17, 2024
fc47461
lint & build
miguel-nascimento Dec 17, 2024
53de62e
mock data
miguel-nascimento Dec 17, 2024
e8ea103
update web3modal v4 -> reown appkit
miguel-nascimento Dec 17, 2024
d97ce2e
get total staked & invalidate when increase
miguel-nascimento Dec 17, 2024
f771470
polishes
miguel-nascimento Dec 17, 2024
3dc79f0
fix build (??)
miguel-nascimento Dec 17, 2024
443f720
estimated apr
miguel-nascimento Dec 18, 2024
f3520be
update lucide react
miguel-nascimento Dec 18, 2024
50e9e7f
withdraw timer + dropdown menu to redelegate/withdraw + tooltip
miguel-nascimento Dec 18, 2024
f46181e
HNT-6333 - get nodes from river registry
miguel-nascimento Dec 19, 2024
97a79f7
move getSsrChainId to utils
miguel-nascimento Dec 19, 2024
af6e265
pie chart
miguel-nascimento Dec 19, 2024
08f1f52
fix build
miguel-nascimento Dec 19, 2024
d827c59
use vercel_env instead of node_env to block networks
miguel-nascimento Dec 19, 2024
fa64a6f
force dynamic
miguel-nascimento Dec 19, 2024
8fd6c39
hydrate data from ssr, use live data in client
miguel-nascimento Dec 19, 2024
85a500d
fix: commision, apr, apy values
miguel-nascimento Jan 6, 2025
d6e0f2a
add cache time to viem public client
miguel-nascimento Jan 6, 2025
86df64a
chore: update river token total suppy to use mainnet value
miguel-nascimento Jan 6, 2025
f573761
feat: show wallet button in header if its in `/stake`
miguel-nascimento Jan 7, 2025
5de3121
providers component
miguel-nascimento Jan 8, 2025
ef67453
better naming about NodeData
miguel-nascimento Jan 8, 2025
f9afb06
UI to stake to operators, not individual nodes
miguel-nascimento Jan 9, 2025
4cd8a60
chore: operator name is `${company} {ocurrency}`
miguel-nascimento Jan 9, 2025
c08e228
chore: parse amount to number
miguel-nascimento Jan 9, 2025
d584663
chore: format rvr amount properly using Intl api
miguel-nascimento Jan 10, 2025
77ee3b5
new design for operator card ✨
miguel-nascimento Jan 10, 2025
71cb8f2
show operator address below the name
miguel-nascimento Jan 13, 2025
95f5e17
fix: fetch operators from gamma
miguel-nascimento Jan 13, 2025
01e81c3
fix: change data if you change river env
miguel-nascimento Jan 13, 2025
a40bc54
update reward distribution contract abi
miguel-nascimento Jan 14, 2025
0711dac
feat: approve first, then stake
miguel-nascimento Jan 14, 2025
02d182c
tons of cache invalidations
miguel-nascimento Jan 14, 2025
048dd7c
fix: staking / approving text
miguel-nascimento Jan 14, 2025
ffb41fd
fix cache invalidation
miguel-nascimento Jan 14, 2025
fd1637e
wip: my stakes profile page (slow - no ssr)
miguel-nascimento Jan 21, 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
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// https://github.com/vercel/geist-font/issues/13
/** @type {import('next').NextConfig} */
module.exports = {
webpack: (config) => {
config.externals.push('pino-pretty')
return config
},
reactStrictMode: true,
swcMinify: true,
transpilePackages: ['geist'],
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@
"@react-spring/three": "^9.7.3",
"@react-three/drei": "^9.105.6",
"@react-three/fiber": "^8.16.3",
"@reown/appkit": "^1.6.0",
"@reown/appkit-adapter-wagmi": "^1.6.0",
"@river-build/generated": "^0.0.127",
"@styled-jsx/plugin-sass": "^4.0.1",
"@tanstack/react-query": "^5.29.2",
"@tanstack/react-query-devtools": "^5.45.0",
"@types/seedrandom": "^3.0.8",
"@types/three": "^0.164.0",
"@use-gesture/react": "^10.3.1",
"@web3modal/wagmi": "^4.1.7",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"dotenv": "^16.4.5",
Expand All @@ -57,7 +58,7 @@
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"js-confetti": "^0.12.0",
"lucide-react": "^0.236.0",
"lucide-react": "^0.352.0",
"mini-svg-data-uri": "^1.4.4",
"mixpanel-browser": "^2.45.0",
"next": "^14.2.3",
Expand Down Expand Up @@ -109,6 +110,7 @@
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"css-loader": "^6.8.1",
"encoding": "^0.1.13",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-next": "^14.2.3",
Expand Down
1,294 changes: 758 additions & 536 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/app/(requires-wallet)/claim/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const Loading = () => {
)
}

const ConnectWallet = dynamic(
() => import('../../../components/claim/connect-wallet').then((mod) => mod.ConnectWallet),
const ConnectWalletSection = dynamic(
() => import('../../../components/claim/connect-wallet').then((mod) => mod.ConnectWalletSection),
{
loading: Loading,
ssr: false,
Expand All @@ -32,7 +32,7 @@ const ClaimPage = dynamic(

const Page = () => {
const { isConnected } = useAccount()
return isConnected ? <ClaimPage /> : <ConnectWallet />
return isConnected ? <ClaimPage /> : <ConnectWalletSection />
}

export default Page
7 changes: 4 additions & 3 deletions src/app/(requires-wallet)/delegate/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ const Loading = () => {
)
}

const ConnectWallet = dynamic(
() => import('../../../components/delegate/connect-wallet').then((mod) => mod.ConnectWallet),
const ConnectWalletSection = dynamic(
() =>
import('../../../components/delegate/connect-wallet').then((mod) => mod.ConnectWalletSection),
{
loading: Loading,
ssr: false,
Expand All @@ -32,7 +33,7 @@ const DelegateSection = dynamic(

const DelegatePage = () => {
const { isConnected } = useAccount()
return isConnected ? <DelegateSection /> : <ConnectWallet />
return isConnected ? <DelegateSection /> : <ConnectWalletSection />
}

export default DelegatePage
20 changes: 4 additions & 16 deletions src/app/(requires-wallet)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import { WalletConnectProvider } from '@/components/wallet-connect'
import { wagmiConfig } from '@/lib/wagmi'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

import { FooterDivider } from '@/components/footer/footer-divider'
import { TanstackQueryProvider } from '@/lib/context/tanstack-query-provider'
import { headers } from 'next/headers'
import { cookieToInitialState } from 'wagmi'

const WalletLayout = ({ children }: { children: React.ReactNode }) => {
const initialState = cookieToInitialState(wagmiConfig, headers().get('cookie'))

return (
<WalletConnectProvider initialState={initialState}>
<TanstackQueryProvider>
{children}
<FooterDivider />
<ReactQueryDevtools initialIsOpen={false} />
</TanstackQueryProvider>
</WalletConnectProvider>
<>
{children}
<FooterDivider />
</>
)
}

Expand Down
32 changes: 32 additions & 0 deletions src/app/(requires-wallet)/stake/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { sharedMetadata } from '@/constants/metadata'
import { Metadata } from 'next'

const metadataTitle = 'Stake - River Protocol'
const metadataDescription = 'Stake your tokens and earn rewards'

export const metadata: Metadata = {
title: metadataTitle,
description: metadataDescription,
openGraph: {
...sharedMetadata.openGraph,
url: 'https://river.build/stake',
title: metadataTitle,
description: metadataDescription,
images: [
{
url: '/og-image-stake.jpg',
alt: metadataTitle,
},
],
},
twitter: {
...sharedMetadata.twitter,
description: metadataDescription,
},
}

const StakeLayout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>
}

export default StakeLayout
41 changes: 41 additions & 0 deletions src/app/(requires-wallet)/stake/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getSsrChainId } from '@/app/utils'
import { AllOperators } from '@/components/stake/all-operators'
import { TotalSupplyCard } from '@/components/stake/total-supply'
import { YourAccountCard } from '@/components/stake/your-account'
import { YourRewardsCard } from '@/components/stake/your-rewards'
import { SwitchToBase } from '@/components/switch-to-base'
import { getStakeableOperators } from '@/data/requests'
import { cn } from '@/lib/utils'
import { baseSepolia } from 'viem/chains'

// TODO: add cache later
// export const revalidate = 60
export const dynamic = 'force-dynamic'

const StakePage = async () => {
const chainId = getSsrChainId()
const env = chainId === baseSepolia.id ? 'gamma' : 'omega'
const initialData = await getStakeableOperators(env).catch(() => undefined)

return (
<section
className={cn(
'hero-glow relative w-full overflow-x-clip bg-gray-90 text-white',
'pb-24 pt-[88px]',
)}
>
<div className="container space-y-12 px-4 md:px-8 xl:max-w-screen-xl">
<SwitchToBase />

<TotalSupplyCard initialData={initialData} />
<div className="grid gap-6 md:grid-cols-2">
<YourAccountCard />
<YourRewardsCard />
</div>
<AllOperators initialData={initialData} />
</div>
</section>
)
}

export default StakePage
32 changes: 32 additions & 0 deletions src/app/(requires-wallet)/stake/profile/connect-wallet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DelegateAscii } from '@/components/delegate/delegate-ascii'
import { ConnectWalletButton } from '@/components/ui/connect-wallet-button'
import { Typography } from '@/components/ui/typography'
import { cn } from '@/lib/utils'

export const ConnectWalletSection = () => {
return (
<section
className={cn(
'hero-glow relative w-full overflow-x-clip bg-gray-90',
'flex w-full flex-col items-start justify-center gap-6',
'px-4 py-24',
'min-h-screen items-center',
)}
>
<DelegateAscii />
<div className="flex flex-col items-center gap-2">
<Typography
as="h1"
size="4xl"
className="text-[32px] font-semibold leading-[44px] text-gray-10"
>
My Stakes
</Typography>
<Typography size="sm" as="span" className="text-gray-20">
View your staked RVR
</Typography>
</div>
<ConnectWalletButton>Connect Wallet</ConnectWalletButton>
</section>
)
}
116 changes: 116 additions & 0 deletions src/app/(requires-wallet)/stake/profile/my-stakes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { OperatorCard } from '@/components/stake/operator-card'
import { SwitchToBase } from '@/components/switch-to-base'
import { Button, buttonVariants } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { useOperatorsWithDeposits, type Deposit } from '@/lib/hooks/use-stakeable-operators'
import { useWithdraw } from '@/lib/hooks/use-withdraw'
import { useWithdrawTimer } from '@/lib/hooks/use-withdraw-timer'
import { cn } from '@/lib/utils'
import { formatRVRAmount } from '@/lib/utils/formatRVRAmount'
import Link from 'next/link'
import { useMemo } from 'react'

export const MyStakesSection = () => {
// TODO: this is slow and should be better with a better initial data
const { data: operators, pendingWithdrawals } = useOperatorsWithDeposits()

const myStakes = useMemo(() => operators.filter((operator) => operator.deposits), [operators])

return (
<section
className={cn(
'hero-glow relative w-full overflow-x-clip bg-gray-90 text-white',
'pb-24 pt-[88px]',
)}
>
<div className="container space-y-12 px-4 md:px-8 xl:max-w-screen-xl">
<SwitchToBase />

<section className="mx-auto max-w-lg space-y-4">
<h1 className="text-center text-2xl font-bold">My Stakes</h1>
{myStakes.length === 0 ? (
<div className="flex w-full items-center justify-center py-12">
<div className="flex flex-col items-center justify-center gap-4">
<p className="text-center text-gray-20">
You currently have no stakes. Stake to start earning rewards.
</p>
<div className="w-3/4">
<Link
href="/stake"
className={cn(
buttonVariants({ variant: 'primary' }),
'w-full bg-gray-10 text-gray-90',
)}
>
Stake
</Link>
</div>
</div>
</div>
) : (
<div
className={cn(
'grid grid-cols-1 gap-4',
myStakes.length > 2 ? 'md:grid-cols-2' : 'md:grid-cols-1',
)}
>
{myStakes.map((operator) => (
<OperatorCard key={operator.address} operator={operator} showButton />
))}
</div>
)}
</section>
{pendingWithdrawals && (
<section className="mx-auto max-w-3xl space-y-4">
<h1 className="text-center text-2xl font-bold">Pending Withdrawals</h1>
<div
className={cn(
'grid grid-cols-2 place-items-center gap-4',
pendingWithdrawals.length > 2 ? 'grid-cols-2' : 'grid-cols-1',
)}
>
{pendingWithdrawals.map((withdrawal) => (
<WithdrawalCard
key={`${withdrawal.delegatee}-${withdrawal.pendingWithdrawal}-${withdrawal.amount}`}
withdrawal={withdrawal}
/>
))}
</div>
</section>
)}
</div>
</section>
)
}

const WithdrawalCard = ({ withdrawal }: { withdrawal: Deposit }) => {
const { lockCooldown } = useWithdraw(withdrawal.depositId)
const withdrawTimer = useWithdrawTimer(lockCooldown)
const isWithdrawable = withdrawTimer === null

return (
<Card className="max-w-sm">
<CardHeader className="p-4 pb-0">
<CardTitle className="text-center text-xl">
{formatRVRAmount(withdrawal.pendingWithdrawal)} RVR
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 p-4 pt-0">
<h2 className="text-center text-lg font-bold"></h2>
<div className="flex flex-col items-center gap-2 sm:flex-row sm:justify-between">
<span className="text-gray-20">Time left</span>
<span className="font-medium">{withdrawTimer}</span>
</div>
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center">
<Button
className={isWithdrawable ? 'w-full' : 'p-4'}
variant={isWithdrawable ? 'secondary' : 'primary'}
>
Redelegate
</Button>
{isWithdrawable && <Button className="-order-1 w-full">Withdrawable</Button>}
</div>
</CardContent>
</Card>
)
}
35 changes: 35 additions & 0 deletions src/app/(requires-wallet)/stake/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client'

import { Loader2 } from 'lucide-react'
import dynamic from 'next/dynamic'
import { useAccount } from 'wagmi'

const Loading = () => {
return (
<section className="hero-glow relative flex w-full flex-col justify-center overflow-x-clip bg-gray-90 pb-24 pt-[88px] md:min-h-screen">
<div className="flex h-full w-full flex-grow items-center justify-center text-white">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
</section>
)
}

const ConnectWalletSection = dynamic(
() => import('./connect-wallet').then((mod) => mod.ConnectWalletSection),
{
loading: Loading,
ssr: false,
},
)

const MyStakesSection = dynamic(() => import('./my-stakes').then((mod) => mod.MyStakesSection), {
loading: Loading,
ssr: false,
})

const MyStakesPage = () => {
const { isConnected } = useAccount()
return isConnected ? <MyStakesSection /> : <ConnectWalletSection />
}

export default MyStakesPage
Loading