Skip to content
Open
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
3 changes: 2 additions & 1 deletion apps/hub/.env
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
# – https://vercel.com/docs/cli/env#exporting-development-environment-variables


NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="123"
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="7ab664eee6a734b14327cdf4678a3431"
NEXT_PUBLIC_API_URL=http://localhost:3001/api/v1
2 changes: 1 addition & 1 deletion apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"rehype-slug": "^6.0.0",
"siwe": "^2.3.2",
"ts-pattern": "^5.6.2",
"viem": "^2.21.1",
"viem": "^2.29.1",
"wagmi": "2.15.2",
"zod": "^3.24.1"
},
Expand Down
24 changes: 9 additions & 15 deletions apps/hub/src/app/_components/connect-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,39 @@

import { Button, ShortenAddress } from '@status-im/status-network/components'
import { ConnectKitButton } from 'connectkit'
import { useAccount } from 'wagmi'

import type { ComponentProps } from 'react'

type Props = {
size?: ComponentProps<typeof Button>['size']
label?: string
shortLabel?: string
className?: string
/** If true, shows the label instead of the shortened address when connected */
alwaysShowLabel?: boolean
}

const ConnectButton = (props: Props) => {
const {
size = '32',
label = 'Connect wallet',
shortLabel = 'Connect',
className,
alwaysShowLabel = false,
} = props

const { address, isConnected } = useAccount()

return (
<ConnectKitButton.Custom>
{({ show }) => {
{({ show, isConnected, address }) => {
return (
<Button
onClick={show}
variant={isConnected ? 'secondary' : 'primary'}
size={size}
className={className}
>
{address && isConnected ? (
{address && isConnected && !alwaysShowLabel ? (
<ShortenAddress address={address} />
) : (
<>
<span className="hidden whitespace-nowrap lg:block">
{label}
</span>
<span className="block whitespace-nowrap lg:hidden">
{shortLabel}
</span>
</>
label
)}
</Button>
)
Expand Down
129 changes: 75 additions & 54 deletions apps/hub/src/app/_components/vaults/table-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import { SNT_TOKEN } from '~constants/index'
import { type StakingVault } from '~hooks/useStakingVaults'
import { shortenAddress } from '~utils/address'
import { formatSNT } from '~utils/currency'
import {
calculateDaysUntilUnlock,
calculateVaultBoost,
isVaultLocked,
} from '~utils/vault'
import { calculateDaysUntilUnlock, isVaultLocked } from '~utils/vault'

import { LockVaultModal } from './modals/lock-vault-modal'
import { WithdrawVaultModal } from './modals/withdraw-vault-modal'
Expand All @@ -25,21 +21,56 @@ interface TableColumnsProps {
isConnected: boolean
}

// Calculate total staked across all vaults
const calculateTotalStaked = (vaults: StakingVault[]): bigint => {
return vaults.reduce(
(acc, vault) => acc + (vault.data?.stakedBalance || 0n),
BigInt(0)
)
}

// Calculate total karma across all vaults
const calculateTotalKarma = (vaults: StakingVault[]): bigint => {
return vaults.reduce(
(acc, vault) => acc + (vault.data?.rewardsAccrued || 0n),
BigInt(0)
)
}

// Cache current time to avoid calling Date.now() on every cell render
const getCurrentTimestamp = (): bigint => {
return BigInt(Math.floor(Date.now() / 1000))
}

// Validation function for lock time - extracted to avoid recreating on every render
const validateLockTime = (_: string, days: string): string | null => {
const totalDays = parseInt(days || '0')
// TODO: read this from the contract
return totalDays > 1460 ? 'Maximum lock time is 4 years' : null
}

// Modal action configurations - static, never change
const EXTEND_LOCK_ACTIONS = [
{ label: 'Cancel' },
{ label: 'Extend lock' },
] as const

const LOCK_VAULT_ACTIONS = [{ label: "Don't lock" }, { label: 'Lock' }] as const

const LOCK_INFO_MESSAGE =
'Boost the rate at which you receive Karma. The longer you lock your vault, the higher your boost, and the faster you accumulate Karma. You can add more SNT at any time, but withdrawing your SNT is only possible once the vault unlocks.' as const

export const createVaultTableColumns = ({
vaults = [],
openModalVaultId,
setOpenModalVaultId,
emergencyModeEnabled,
isConnected,
}: TableColumnsProps) => {
const totalStaked = vaults.reduce(
(acc, vault) => acc + (vault.data?.stakedBalance || 0n),
BigInt(0)
)
const totalKarma = vaults.reduce(
(acc, vault) => acc + (vault.data?.rewardsAccrued || 0n),
BigInt(0)
)
// Calculate totals and current time once per column creation
const totalStaked = calculateTotalStaked(vaults)
const totalKarma = calculateTotalKarma(vaults)
const currentTimestamp = getCurrentTimestamp()
const columnHelper = createColumnHelper<StakingVault>()

return [
Expand All @@ -48,14 +79,14 @@ export const createVaultTableColumns = ({
header: 'Vault',
cell: ({ row }) => {
return (
<span className="whitespace-pre text-13 font-medium text-neutral-100">
<span className="whitespace-pre text-[13px] font-medium text-neutral-100">
#{Number(row.index) + 1}
</span>
)
},
footer: () => {
return (
<span className="text-13 font-medium text-neutral-50">Total</span>
<span className="text-[13px] font-medium text-neutral-50">Total</span>
)
},
meta: {
Expand All @@ -67,7 +98,7 @@ export const createVaultTableColumns = ({
header: 'Address',
cell: ({ row }) => {
return (
<span className="whitespace-pre text-13 font-medium text-neutral-100">
<span className="whitespace-pre text-[13px] font-medium text-neutral-100">
{shortenAddress(row.original.address)}
</span>
)
Expand All @@ -82,7 +113,7 @@ export const createVaultTableColumns = ({
cell: ({ row }) => {
return (
<div className="flex items-center gap-1">
<span className="text-13 font-medium text-neutral-100">
<span className="text-[13px] font-medium text-neutral-100">
{formatSNT(row.original.data?.stakedBalance || 0n)}
<span className="ml-0.5 text-neutral-50">SNT</span>
</span>
Expand All @@ -91,7 +122,7 @@ export const createVaultTableColumns = ({
},
footer: () => {
return (
<span className="text-13 font-medium text-neutral-100">
<span className="text-[13px] font-medium text-neutral-100">
{formatSNT(totalStaked)}
<span className="ml-0.5 text-neutral-50">SNT</span>
</span>
Expand All @@ -116,7 +147,7 @@ export const createVaultTableColumns = ({

return (
<div className="flex items-center gap-0.5">
<span className="text-13 font-medium text-neutral-100">
<span className="text-[13px] font-medium leading-[1.4] tracking-[-0.039px] text-neutral-100">
{daysUntilUnlock}
<span className="ml-0.5 text-neutral-50">d</span>
</span>
Expand All @@ -140,13 +171,22 @@ export const createVaultTableColumns = ({
? (maxMP - mpAccrued) / stakedBalance
: undefined

// Calculate boost directly instead of recalculating for all vaults
// Boost = (mpAccrued / stakedBalance) + 1 (base multiplier)
const currentBoost =
stakedBalance > 0n
? Number(formatUnits(mpAccrued, SNT_TOKEN.decimals)) /
Number(formatUnits(stakedBalance, SNT_TOKEN.decimals)) +
1
: 1

return (
<div className="flex items-center gap-3">
<span className="text-13 font-medium text-neutral-100">
x{calculateVaultBoost(vaults, row.original.address)}
<span className="text-[13px] font-medium leading-[1.4] tracking-[-0.039px] text-neutral-100">
x{currentBoost.toFixed(2)}
</span>
{potentialBoost && (
<span className="text-13 font-medium text-purple">
<span className="text-[13px] font-medium leading-[1.4] tracking-[-0.039px] text-[#7140fd]">
x{formatSNT(formatUnits(potentialBoost, SNT_TOKEN.decimals))} if
locked
</span>
Expand All @@ -165,7 +205,7 @@ export const createVaultTableColumns = ({
const karma = Number(row.original.data?.rewardsAccrued) / 1e18
return (
<div className="flex items-center gap-1">
<span className="text-13 font-medium text-neutral-100">
<span className="text-[13px] font-medium leading-[1.4] tracking-[-0.039px] text-neutral-100">
{formatSNT(karma)}
<span className="ml-0.5 text-neutral-50">KARMA</span>
</span>
Expand All @@ -174,7 +214,7 @@ export const createVaultTableColumns = ({
},
footer: () => {
return (
<span className="text-13 font-medium text-neutral-100">
<span className="text-[13px] font-medium text-neutral-100">
{formatSNT(totalKarma)}
<span className="ml-0.5 text-neutral-50">KARMA</span>
</span>
Expand All @@ -198,7 +238,7 @@ export const createVaultTableColumns = ({
) : (
<UnlockedIcon className="text-purple" />
)}
<span className="text-13 font-medium capitalize text-neutral-100">
<span className="text-[13px] font-medium capitalize leading-[1.4] tracking-[-0.039px] text-neutral-100">
{isLocked ? 'Locked' : 'Open'}
</span>
</div>
Expand All @@ -218,8 +258,9 @@ export const createVaultTableColumns = ({
const isWithdrawModalOpen = openModalVaultId === withdrawModalId
const isLockModalOpen = openModalVaultId === lockModalId

// Use cached timestamp instead of calling Date.now() on every render
const isLocked = row.original?.data?.lockUntil
? row.original.data.lockUntil > BigInt(Math.floor(Date.now() / 1000))
? row.original.data.lockUntil > currentTimestamp
: false

return (
Expand All @@ -239,7 +280,7 @@ export const createVaultTableColumns = ({
variant="danger"
size="32"
disabled={!isConnected}
className="min-w-fit bg-danger-50 text-13 text-white-100 hover:bg-danger-60"
className="min-w-fit bg-danger-50 text-[13px] text-white-100 hover:bg-danger-60"
>
<AlertIcon className="shrink-0" />
<span className="hidden whitespace-nowrap xl:inline">
Expand All @@ -261,22 +302,15 @@ export const createVaultTableColumns = ({
initialYears="2"
initialDays="732"
description="Extending lock time increasing Karma boost"
actions={[
{
label: 'Cancel',
},
{
label: 'Extend lock',
},
]}
actions={[...EXTEND_LOCK_ACTIONS]}
onClose={() => setOpenModalVaultId(null)}
infoMessage="Boost the rate at which you receive Karma. The longer you lock your vault, the higher your boost, and the faster you accumulate Karma. You can add more SNT at any time, but withdrawing your SNT is only possible once the vault unlocks."
infoMessage={LOCK_INFO_MESSAGE}
>
<Button
variant="primary"
size="32"
disabled={!isConnected}
className="min-w-fit text-13"
className="min-w-fit text-[13px]"
>
<TimeIcon className="shrink-0" />
<span className="hidden whitespace-nowrap xl:inline">
Expand All @@ -295,29 +329,16 @@ export const createVaultTableColumns = ({
vaultAddress={row.original.address}
title="Do you want to lock the vault?"
description="Lock this vault to receive more Karma"
actions={[
{
label: "Don't lock",
},
{
label: 'Lock',
},
]}
actions={[...LOCK_VAULT_ACTIONS]}
onClose={() => setOpenModalVaultId(null)}
infoMessage="Boost the rate at which you receive Karma. The longer you lock your vault, the higher your boost, and the faster you accumulate Karma. You can add more SNT at any time, but withdrawing your SNT is only possible once the vault unlocks."
onValidate={(_, days) => {
const totalDays = parseInt(days || '0')
// TODO: read this from the contract
return totalDays > 1460
? 'Maximum lock time is 4 years'
: null
}}
infoMessage={LOCK_INFO_MESSAGE}
onValidate={validateLockTime}
>
<Button
variant="primary"
size="32"
disabled={!isConnected}
className="min-w-fit text-13"
className="min-w-fit text-[13px]"
>
<LockedIcon fill="white" className="shrink-0" />
<span className="whitespace-nowrap">Lock</span>
Expand Down
Loading
Loading