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

Locks and votes #20

Merged
merged 11 commits into from
Aug 20, 2024
Merged
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
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