Skip to content

Commit

Permalink
Locks and votes (#20)
Browse files Browse the repository at this point in the history
Co-authored-by: Nikos Kontakis <[email protected]>
  • Loading branch information
Tbaut and wirednkod authored Aug 20, 2024
1 parent 0e93367 commit 3fa1f9f
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 34 deletions.
21 changes: 12 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import 'dot-connect/font.css'
import { config } from './walletConfigs'
import { ReDotProvider, ReDotChainProvider } from '@reactive-dot/react'
import { Suspense } from 'react'
import { AccountContextProvider } from '@/contexts/AccountsContext'
import { AccountContextProvider } from './contexts/AccountsContext'
import { LocksContextProvider } from './contexts/LocksContext'
import { DelegateeContextProvider } from '@/contexts/DelegateesContext'
import { NetworkContextProvider } from './contexts/NetworkContext'

Expand All @@ -28,15 +29,17 @@ const App = () => {
<NetworkContextProvider>
<DelegateeContextProvider>
<AccountContextProvider>
<TooltipProvider>
<div className="flex min-h-screen w-full flex-col bg-muted/40">
<Navigation />
<div className="flex flex-col sm:gap-4 sm:py-4 sm:pl-14">
<Header />
<Content />
<LocksContextProvider>
<TooltipProvider>
<div className="flex min-h-screen w-full flex-col bg-muted/40">
<Navigation />
<div className="flex flex-col sm:gap-4 sm:py-4 sm:pl-14">
<Header />
<Content />
</div>
</div>
</div>
</TooltipProvider>
</TooltipProvider>
</LocksContextProvider>
</AccountContextProvider>
</DelegateeContextProvider>
</NetworkContextProvider>
Expand Down
17 changes: 0 additions & 17 deletions src/clients.tsx

This file was deleted.

41 changes: 41 additions & 0 deletions src/components/LocksCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useLocks } from '@/contexts/LocksContext'
import { useNetwork } from '@/contexts/NetworkContext'
import { useEffect, useState } from 'react'

export const LocksCard = () => {
const { currentLocks } = useLocks()
const [currentBlock, setCurrentBlock] = useState(0)
const { api } = useNetwork()

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

const sub = api.query.System.Number.watchValue('best').subscribe(
(value) => {
setCurrentBlock(value)
},
)

return () => sub.unsubscribe()
}, [api])

if (!currentLocks) return null

return (
<div>
Current block: {currentBlock}
<br />
Current locks:
<br />
{Object.entries(currentLocks).map(([track, lockValue]) => (
<div key={track}>
<ul>
<li>track: {track}</li>
<li>Amount: {lockValue.lock.amount.toString()}</li>
<li>Release: {lockValue.lock.blockNumber}</li>
</ul>
</div>
))}
</div>
)
}
2 changes: 1 addition & 1 deletion src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Global Constants
*/
const AppVersion = '0.1.1'
const DappName = 'Polkadot DelegIt Dashboard'
const DappName = 'Delegit'
const PolkadotUrl = 'https://delegit-xyz.github.io/dashboard'

const GithubOwner = 'delegit-xyz'
Expand Down
69 changes: 69 additions & 0 deletions src/contexts/LocksContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable react-refresh/only-export-components */
import React, { createContext, useContext, useEffect, useState } from 'react'
import {
Casting,
Delegating,
getVotingTrackInfo,
} from '../lib/currentVotesAndDelegations'
import { useAccounts } from './AccountsContext'
import { getLocksInfo, Locks } from '@/lib/locks'
import { useNetwork } from './NetworkContext'

type LocksContextProps = {
children: React.ReactNode | React.ReactNode[]
}

export interface ILocksContext {
currentVotes: Record<number, Casting | Delegating> | undefined
currentLocks: Locks | undefined
}

const LocksContext = createContext<ILocksContext | undefined>(undefined)

const LocksContextProvider = ({ children }: LocksContextProps) => {
const [currentVotes, setCurrentVotes] = useState<
Record<number, Casting | Delegating> | undefined
>()
const [currentLocks, setCurrentLocks] = useState<Locks | undefined>()

const { selectedAccount } = useAccounts()
const { api } = useNetwork()

useEffect(() => {
if (!selectedAccount || !api) {
setCurrentVotes(undefined)
return
}

getVotingTrackInfo(selectedAccount.address, api)
.then((votes) => setCurrentVotes(votes))
.catch(console.error)
}, [selectedAccount, api])

useEffect(() => {
if (!selectedAccount || !api) {
setCurrentVotes(undefined)
return
}

getLocksInfo(selectedAccount.address, api)
.then((locks) => setCurrentLocks(locks))
.catch(console.error)
}, [selectedAccount, api])

return (
<LocksContext.Provider value={{ currentVotes, currentLocks }}>
{children}
</LocksContext.Provider>
)
}

const useLocks = () => {
const context = useContext(LocksContext)
if (context === undefined) {
throw new Error('useLocks must be used within a LocksContextProvider')
}
return context
}

export { LocksContextProvider, useLocks }
5 changes: 3 additions & 2 deletions src/contexts/NetworkContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type NetworkContextProps = {
// const kusamaEndpoints = ['wss://rpc.ibp.network/kusama']

export type NetworkProps = 'polkadot' | 'kusama'
export type ApiType = TypedApi<typeof dot | typeof ksm>

export interface INetworkContext {
network: NetworkProps
Expand All @@ -24,12 +25,12 @@ const NetworkContext = createContext<INetworkContext | undefined>(undefined)

const NetworkContextProvider = ({ children }: NetworkContextProps) => {
const [client, setClient] = useState<PolkadotClient>()
const [api, setApi] = useState<TypedApi<typeof dot | typeof ksm>>()
const [api, setApi] = useState<ApiType>()
const [network, setNetwork] = useState<NetworkProps>('polkadot')

useEffect(() => {
let cl: PolkadotClient
let typedApi: TypedApi<typeof dot | typeof ksm>
let typedApi: ApiType
if (network === 'polkadot') {
cl = createClient(getWsProvider('wss://rpc.ibp.network/polkadot'))
typedApi = cl.getTypedApi(dot)
Expand Down
1 change: 1 addition & 0 deletions src/lib/bnMin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const bnMin = (n1: bigint, n2: bigint) => (n1 < n2 ? n1 : n2)
14 changes: 14 additions & 0 deletions src/lib/constants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable react-refresh/only-export-components */
export const THRESHOLD = BigInt(500)
export const DEFAULT_TIME = BigInt(6000)
export const ONE_DAY = BigInt(24 * 60 * 60 * 1000)

export const lockPeriod: Record<string, number> = {
None: 0,
Locked1x: 1,
Locked2x: 2,
Locked3x: 4,
Locked4x: 8,
Locked5x: 16,
Locked6x: 32,
}
142 changes: 142 additions & 0 deletions src/lib/currentVotesAndDelegations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { SS58String } from 'polkadot-api'
import { MultiAddress, VotingConviction } from '@polkadot-api/descriptors'
import { DEFAULT_TIME, ONE_DAY, THRESHOLD } from './constants'
import { bnMin } from './bnMin'
import { ApiType } from '@/contexts/NetworkContext'

// export const getOptimalAmount = async (
// account: SS58String,
// at: string = 'best',
// ) => (await dotApi.query.Staking.Ledger.getValue(account, { at }))?.active

export interface Casting {
type: 'Casting'
referendums: Array<number>
}

export interface Delegating {
type: 'Delegating'
target: SS58String
amount: bigint
conviction: VotingConviction
}

export const getTracks = async (
api: ApiType,
): Promise<Record<number, string>> =>
Object.fromEntries(
(await api.constants.Referenda.Tracks()).map(([trackId, { name }]) => [
trackId,
name
.split('_')
.map((part) => part[0].toUpperCase() + part.slice(1))
.join(' '),
]),
)

export const getVotingTrackInfo = async (
address: SS58String,
api: ApiType,
): Promise<Record<number, Casting | Delegating>> => {
const convictionVoting =
await api.query.ConvictionVoting.VotingFor.getEntries(address)

return Object.fromEntries(
convictionVoting
.filter(
({ value: convictionVote }) =>
convictionVote.type === 'Delegating' ||
convictionVote.value.votes.length > 0,
)
.map(({ keyArgs: [, votingClass], value: { type, value } }) => [
votingClass,
type === 'Casting'
? {
type: 'Casting',
referendums: value.votes.map(([refId]) => refId),
}
: {
type: 'Delegating',
target: value.target,
amount: value.balance,
conviction: value.conviction,
},
]),
)
}

export const getDelegateTx = async (
from: SS58String,
target: SS58String,
conviction: VotingConviction,
amount: bigint,
tracks: Array<number>,
api: ApiType,
) => {
const tracksInfo = await getVotingTrackInfo(from, api)

const txs: Array<
| ReturnType<typeof api.tx.ConvictionVoting.remove_vote>
| ReturnType<typeof api.tx.ConvictionVoting.undelegate>
| ReturnType<typeof api.tx.ConvictionVoting.delegate>
> = []
tracks.forEach((trackId) => {
const trackInfo = tracksInfo[trackId]

if (trackInfo) {
if (
trackInfo.type === 'Delegating' &&
trackInfo.target === target &&
conviction.type === trackInfo.conviction.type &&
amount === trackInfo.amount
)
return

if (trackInfo.type === 'Casting') {
trackInfo.referendums.forEach((index) => {
txs.push(
api.tx.ConvictionVoting.remove_vote({
class: trackId,
index,
}),
)
})
} else
txs.push(
api.tx.ConvictionVoting.undelegate({
class: trackId,
}),
)
}

txs.push(
api.tx.ConvictionVoting.delegate({
class: trackId,
conviction,
to: MultiAddress.Id(target),
balance: amount,
}),
)
})

// @ts-expect-error we need to fix this, it appeared once we added the dynamic api (either kusama or polkadot)
return api.tx.Utility.batch_all({
calls: txs.map((tx) => tx.decodedCall),
})
}

export const getExpectedBlockTime = async (api: ApiType): Promise<bigint> => {
const expectedBlockTime = await api.constants.Babe.ExpectedBlockTime()
if (expectedBlockTime) {
return bnMin(ONE_DAY, expectedBlockTime)
}

const thresholdCheck =
(await api.constants.Timestamp.MinimumPeriod()) > THRESHOLD

if (thresholdCheck) {
return bnMin(ONE_DAY, (await api.constants.Timestamp.MinimumPeriod()) * 2n)
}

return bnMin(ONE_DAY, DEFAULT_TIME)
}
Loading

0 comments on commit 3fa1f9f

Please sign in to comment.