Skip to content

Commit

Permalink
Show own delegations, delegation locks and allow to undelegate (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tbaut authored Sep 4, 2024
1 parent 6a75b42 commit 38a1210
Show file tree
Hide file tree
Showing 10 changed files with 504 additions and 119 deletions.
181 changes: 137 additions & 44 deletions src/components/LocksCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,43 @@ import { planckToUnit } from '@polkadot-ui/utils'
import { Button } from './ui/button'
import { Title } from './ui/title'
import { ContentReveal } from './ui/content-reveal'
import { Clock2, LockKeyholeOpen, Vote } from 'lucide-react'
import { BadgeCent, Clock2, LockKeyholeOpen, Vote } from 'lucide-react'
import { Badge } from './ui/badge'
import { dot } from '@polkadot-api/descriptors'
import { useAccounts } from '@/contexts/AccountsContext'
import { TypedApi } from 'polkadot-api'
import { getUnlockUnvoteTx } from '@/lib/utils'
import { useLocks, VoteLock } from '@/contexts/LocksContext'
import {
DelegationLock,
LockType,
useLocks,
VoteLock,
} from '@/contexts/LocksContext'
import { Skeleton } from './ui/skeleton'

export const LocksCard = () => {
const [currentBlock, setCurrentBlock] = useState(0)
const [expectedBlockTime, setExpectedBlockTime] = useState(0)
const { api } = useNetwork()
const { locks } = useLocks()
const { api, trackList } = useNetwork()
const { locks, delegationLocks } = useLocks()
const { assetInfo } = useNetwork()
const [ongoingVoteLocks, setOngoingVoteLocks] = useState<VoteLock[]>([])
const [freeLocks, setFreeLocks] = useState<VoteLock[]>([])
const [freeLocks, setFreeLocks] = useState<Array<VoteLock | DelegationLock>>(
[],
)
const [locksLoaded, setLocksLoaded] = useState<boolean>(false)
const [currentLocks, setCurrentLocks] = useState<VoteLock[]>([])
const [currentDelegationLocks, setCurrentDelegationLocks] = useState<
DelegationLock[]
>([])
const { selectedAccount } = useAccounts()
const [isUnlockingLoading, setIsUnlockingLoading] = useState(false)

useEffect(() => {
if (!currentBlock) return

const tempOngoingLocks: VoteLock[] = []
const tempFree: VoteLock[] = []
const tempFree: Array<VoteLock | DelegationLock> = []
const tempCurrent: VoteLock[] = []

locks.forEach((lock) => {
Expand All @@ -49,11 +59,25 @@ export const LocksCard = () => {
}
})

const tempDelegationLocks: DelegationLock[] = []

delegationLocks.forEach((lock) => {
// if the end block is in the future
// then the funds are locked
if (lock.endBlock >= currentBlock) {
tempDelegationLocks.push(lock)
} else {
// otherwise, the lock is elapsed and can be freed
tempFree.push(lock)
}
})

setOngoingVoteLocks(tempOngoingLocks)
setFreeLocks(tempFree)
setCurrentLocks(tempCurrent)
setCurrentDelegationLocks(tempDelegationLocks)
setLocksLoaded(true)
}, [currentBlock, locks])
}, [currentBlock, delegationLocks, locks])

useEffect(() => {
if (!api) return
Expand Down Expand Up @@ -116,24 +140,53 @@ export const LocksCard = () => {
{freeLocks.length > 0 && (
<>
<Button
className="my-4 w-full"
className="mb-2 mt-4 w-full"
onClick={onUnlockClick}
disabled={isUnlockingLoading}
>
Unlock
</Button>
<ContentReveal>
{freeLocks.map(({ amount, refId, trackId }) => {
{freeLocks.map((lock) => {
if (lock.type === LockType.Delegating) {
const { amount, trackId } = lock
return (
<div key={trackId}>
<ul>
<li className="mb-2">
<div className="capitalize">
<span className="capitalize">
<Badge>{trackList[trackId]}</Badge> /{trackId}
</span>
<div>
<BadgeCent className="inline-block h-4 w-4 text-gray-500" />{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
</div>
</div>
</li>
</ul>
</div>
)
}

const { amount, refId } = lock
return (
<div key={refId}>
<ul>
<li className="mb-2">
{trackId} - <Badge>#{refId}</Badge>{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
<Badge>#{refId}</Badge>
<div>
<BadgeCent className="inline-block h-4 w-4 text-gray-500" />{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
</div>
</li>
</ul>
</div>
Expand All @@ -155,31 +208,68 @@ export const LocksCard = () => {
<Card className="h-full border-2 p-2 px-4">
<Title variant="h4">Locked</Title>
<div className="text-5xl font-bold">
{currentLocks.length}
{currentLocks.length + currentDelegationLocks.length}
<Clock2 className="inline-block h-8 w-8 rotate-[10deg] text-gray-200" />
</div>
<ContentReveal hidden={!currentLocks.length}>
{currentLocks.map(({ amount, endBlock, refId }) => {
const remainingTimeMs =
(Number(endBlock) - currentBlock) * expectedBlockTime
const remainingDisplay = convertMiliseconds(remainingTimeMs)
return (
<div key={refId}>
<ul>
<li>
<Badge>#{refId}</Badge>{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
<br />
Remaining: {displayRemainingTime(remainingDisplay)}
</li>
</ul>
</div>
)
})}
<ContentReveal
hidden={currentLocks.length + currentDelegationLocks.length === 0}
>
<>
{currentLocks.map(({ amount, endBlock, refId }) => {
const remainingTimeMs =
(Number(endBlock) - currentBlock) * expectedBlockTime
const remainingDisplay = convertMiliseconds(remainingTimeMs)
return (
<div key={refId}>
<ul>
<li>
<Badge>#{refId}</Badge>
<div>
<BadgeCent className="inline-block h-4 w-4 pt-2 text-gray-500" />{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
</div>
<div>
<Clock2 className="inline-block h-4 w-4 text-gray-500" />{' '}
{displayRemainingTime(remainingDisplay)}
</div>
</li>
</ul>
</div>
)
})}
{currentDelegationLocks.map(({ amount, endBlock, trackId }) => {
const remainingTimeMs =
(Number(endBlock) - currentBlock) * expectedBlockTime
const remainingDisplay = convertMiliseconds(remainingTimeMs)
return (
<div key={trackId}>
<ul>
<li>
<div className="capitalize">
<Badge>{trackList[trackId]}</Badge> /{trackId}
</div>
<div>
<BadgeCent className="inline-block h-4 w-4 text-gray-500" />{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
</div>
<div>
<Clock2 className="inline-block h-4 w-4 text-gray-500" />{' '}
{displayRemainingTime(remainingDisplay)}
</div>
</li>
</ul>
</div>
)
})}
</>
</ContentReveal>
</Card>
<Card className="h-full border-2 p-2 px-4">
Expand All @@ -195,12 +285,15 @@ export const LocksCard = () => {
<div key={refId}>
<ul>
<li>
<Badge>#{refId}</Badge>{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
<Badge>#{refId}</Badge>
<div>
<BadgeCent className="inline-block h-4 w-4 text-gray-500" />{' '}
{planckToUnit(
amount,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
</div>
</li>
</ul>
</div>
Expand Down
146 changes: 146 additions & 0 deletions src/components/MyDelegations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Card } from '@polkadot-ui/react'
import { Title } from './ui/title'
import { BadgeCent, TreePalm } from 'lucide-react'
import { useLocks } from '@/contexts/LocksContext'
import { useCallback, useMemo, useState } from 'react'
import { Skeleton } from './ui/skeleton'
import { useDelegates } from '@/contexts/DelegatesContext'
import { useNetwork } from '@/contexts/NetworkContext'
import { planckToUnit } from '@polkadot-ui/utils'
import { AddressDisplay } from './ui/address-display'
import { Badge } from './ui/badge'
import { Button } from './ui/button'
import { useAccounts } from '@/contexts/AccountsContext'
import { Transaction, TypedApi } from 'polkadot-api'
import { dot } from '@polkadot-api/descriptors'

export const MyDelegations = () => {
const { trackList, assetInfo, api } = useNetwork()
const { delegations, getConvictionLockTimeDisplay, refreshLocks } = useLocks()
const [delegateLoading, setDelegatesLoading] = useState<string[]>([])
const noDelegations = useMemo(
() => !!delegations && Object.entries(delegations).length === 0,
[delegations],
)
const { getDelegateByAddress } = useDelegates()
const { selectedAccount } = useAccounts()

const onUndelegate = useCallback(
(delegate: string) => {
if (!api || !selectedAccount || !delegations) return

const tracks = delegations[delegate].map((d) => d.trackId)

setDelegatesLoading((prev) => [...prev, delegate])

// @ts-expect-error we can't strongly type this
let tx: Transaction<undefined, unknown, unknown, undefined>

if (tracks.length === 1) {
tx = api.tx.ConvictionVoting.undelegate({ class: tracks[0] })
} else {
const batchTx = tracks.map(
(t) => api.tx.ConvictionVoting.undelegate({ class: t }).decodedCall,
)
tx = (api as TypedApi<typeof dot>).tx.Utility.batch({ calls: batchTx })
}

tx.signSubmitAndWatch(selectedAccount.polkadotSigner).subscribe({
next: (event) => {
console.log(event)
if (event.type === 'finalized') {
setDelegatesLoading((prev) => prev.filter((id) => id !== delegate))
refreshLocks()
}
},
error: (error) => {
console.error(error)
setDelegatesLoading((prev) => prev.filter((id) => id !== delegate))
},
})
},
[api, delegations, refreshLocks, selectedAccount],
)

return (
<>
<Title className="mb-4">My Delegations</Title>
<div className="grid w-full grid-cols-1 gap-2 md:grid-cols-2">
{delegations === undefined ? (
<Skeleton className="h-[116px] rounded-xl" />
) : noDelegations ? (
<Card className="col-span-2 mb-5 bg-accent p-4">
<div className="flex w-full flex-col justify-center">
<div className="flex h-full items-center justify-center">
<TreePalm className="h-12 w-12" />
</div>
<div className="mt-4 text-center">
No delegation yet, get started below!
</div>
</div>
</Card>
) : (
<div className="flex w-full gap-x-2">
{Object.entries(delegations).map(([key, value]) => {
const delegate = getDelegateByAddress(key)

return (
<Card
className="flex h-full flex-col border-2 bg-card p-2 px-4"
key={key}
>
<>
{delegate?.name ? (
<div className="flex items-center">
<img
src={delegate.image}
className="mr-2 w-12 rounded-full"
/>
{delegate.name}
</div>
) : (
<AddressDisplay address={key} size={'3rem'} />
)}
{value.map(({ balance, trackId, conviction }) => {
const { display, multiplier } =
getConvictionLockTimeDisplay(conviction.type)
return (
<div
key={trackId}
className="mb-2 ml-12 border-l-2 pl-2"
>
<div className="capitalize">
<Badge>{trackList[trackId]}</Badge> /{trackId}
</div>
<div>
<BadgeCent className="inline-block h-4 w-4 text-gray-500" />{' '}
{planckToUnit(
balance,
assetInfo.precision,
).toLocaleString('en')}{' '}
{assetInfo.symbol}
</div>
<div>
conviction: x{Number(multiplier)} | {display}
</div>
</div>
)
})}
<Button
className="mb-2 mt-4 w-full"
variant={'outline'}
onClick={() => onUndelegate(key)}
disabled={delegateLoading.includes(key)}
>
Undelegate
</Button>
</>
</Card>
)
})}
</div>
)}
</div>
</>
)
}
Loading

0 comments on commit 38a1210

Please sign in to comment.