From 5a4e927c308b5bdd1e1977283adcea2f45196dae Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Tue, 20 Aug 2024 22:46:10 +0200 Subject: [PATCH] locks and votes --- src/App.tsx | 19 ++-- src/components/LocksCard.tsx | 37 +++++++ src/consts.ts | 2 +- src/contexts/LocksContext.tsx | 68 ++++++++++++ src/lib/bnMin.ts | 1 + src/lib/constants.tsx | 14 +++ src/lib/currentVotesAndDelegations.tsx | 140 +++++++++++++++++++++++++ src/lib/locks.tsx | 108 +++++++++++++++++++ src/pages/Home/index.tsx | 2 + 9 files changed, 382 insertions(+), 9 deletions(-) create mode 100644 src/components/LocksCard.tsx create mode 100644 src/contexts/LocksContext.tsx create mode 100644 src/lib/bnMin.ts create mode 100644 src/lib/constants.tsx create mode 100644 src/lib/currentVotesAndDelegations.tsx create mode 100644 src/lib/locks.tsx diff --git a/src/App.tsx b/src/App.tsx index 097d5b4..693a2fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { config } from './walletConfigs' import { ReDotProvider, ReDotChainProvider } from '@reactive-dot/react' import { Suspense } from 'react' import { AccountContextProvider } from './contexts/AccountsContext' +import { LocksContextProvider } from './contexts/LocksContext' const App = () => { const [settings] = useLocalStorage('fellowship-settings', { @@ -24,15 +25,17 @@ const App = () => { - -
- -
-
- + + +
+ +
+
+ +
-
- + + diff --git a/src/components/LocksCard.tsx b/src/components/LocksCard.tsx new file mode 100644 index 0000000..f8e78d7 --- /dev/null +++ b/src/components/LocksCard.tsx @@ -0,0 +1,37 @@ +import { dotApi } from '@/clients' +import { useLocks } from '@/contexts/LocksContext' +import { useEffect, useState } from 'react' + +export const LocksCard = () => { + const { currentLocks } = useLocks() + const [currentBlock, setCurrentBlock] = useState(0) + + useEffect(() => { + const sub = dotApi.query.System.Number.watchValue('best').subscribe( + (value) => { + setCurrentBlock(value) + console.log('currentBlock', value) + }, + ) + + return sub.unsubscribe() + }, []) + + if (!currentLocks) return null + + return ( +
+ Current locks: +
+ {Object.entries(currentLocks).map(([track, lockValue]) => ( +
+
    +
  • track: {track}
  • +
  • Amount: {lockValue.lock.amount.toString()}
  • +
  • Release: {lockValue.lock.blockNumber}
  • +
+
+ ))} +
+ ) +} diff --git a/src/consts.ts b/src/consts.ts index f389645..acc71db 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -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' diff --git a/src/contexts/LocksContext.tsx b/src/contexts/LocksContext.tsx new file mode 100644 index 0000000..e9ae662 --- /dev/null +++ b/src/contexts/LocksContext.tsx @@ -0,0 +1,68 @@ +/* eslint-disable react-refresh/only-export-components */ +import React, { createContext, useContext, useEffect, useState } from 'react' +// import { dotApi, dotClient } from '../clients' +import { + Casting, + Delegating, + getVotingTrackInfo, +} from '../lib/currentVotesAndDelegations' +import { useAccounts } from './AccountsContext' +import { getLocksInfo, Locks } from '@/lib/locks' + +type LocksContextProps = { + children: React.ReactNode | React.ReactNode[] +} + +export interface ILocksContext { + currentVotes: Record | undefined + currentLocks: Locks | undefined +} + +const LocksContext = createContext(undefined) + +const LocksContextProvider = ({ children }: LocksContextProps) => { + const [currentVotes, setCurrentVotes] = useState< + Record | undefined + >() + const [currentLocks, setCurrentLocks] = useState() + + const { selectedAccount } = useAccounts() + + useEffect(() => { + if (!selectedAccount) { + setCurrentVotes(undefined) + return + } + + getVotingTrackInfo(selectedAccount.address) + .then((votes) => setCurrentVotes(votes)) + .catch(console.error) + }, [selectedAccount]) + + useEffect(() => { + if (!selectedAccount) { + setCurrentVotes(undefined) + return + } + + getLocksInfo(selectedAccount.address) + .then((locks) => setCurrentLocks(locks)) + .catch(console.error) + }, [selectedAccount]) + + return ( + + {children} + + ) +} + +const useLocks = () => { + const context = useContext(LocksContext) + if (context === undefined) { + throw new Error('useLocks must be used within a LocksContextProvider') + } + return context +} + +export { LocksContextProvider, useLocks } diff --git a/src/lib/bnMin.ts b/src/lib/bnMin.ts new file mode 100644 index 0000000..a5485a7 --- /dev/null +++ b/src/lib/bnMin.ts @@ -0,0 +1 @@ +export const bnMin = (n1: bigint, n2: bigint) => (n1 < n2 ? n1 : n2) diff --git a/src/lib/constants.tsx b/src/lib/constants.tsx new file mode 100644 index 0000000..5086b3e --- /dev/null +++ b/src/lib/constants.tsx @@ -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 = { + None: 0, + Locked1x: 1, + Locked2x: 2, + Locked3x: 4, + Locked4x: 8, + Locked5x: 16, + Locked6x: 32, +} diff --git a/src/lib/currentVotesAndDelegations.tsx b/src/lib/currentVotesAndDelegations.tsx new file mode 100644 index 0000000..498c5e0 --- /dev/null +++ b/src/lib/currentVotesAndDelegations.tsx @@ -0,0 +1,140 @@ +import { SS58String } from 'polkadot-api' +import { dotApi } from '../clients' +import { MultiAddress, VotingConviction } from '@polkadot-api/descriptors' +import { DEFAULT_TIME, ONE_DAY, THRESHOLD } from './constants' +import { bnMin } from './bnMin' + +// 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 +} + +export interface Delegating { + type: 'Delegating' + target: SS58String + amount: bigint + conviction: VotingConviction +} + +export const getTracks = async (): Promise> => + Object.fromEntries( + (await dotApi.constants.Referenda.Tracks()).map(([trackId, { name }]) => [ + trackId, + name + .split('_') + .map((part) => part[0].toUpperCase() + part.slice(1)) + .join(' '), + ]), + ) + +export const getVotingTrackInfo = async ( + address: SS58String, +): Promise> => { + const convictionVoting = + await dotApi.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, +) => { + const tracksInfo = await getVotingTrackInfo(from) + + const txs: Array< + | ReturnType + | ReturnType + | ReturnType + > = [] + 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( + dotApi.tx.ConvictionVoting.remove_vote({ + class: trackId, + index, + }), + ) + }) + } else + txs.push( + dotApi.tx.ConvictionVoting.undelegate({ + class: trackId, + }), + ) + } + + txs.push( + dotApi.tx.ConvictionVoting.delegate({ + class: trackId, + conviction, + to: MultiAddress.Id(target), + balance: amount, + }), + ) + }) + + return dotApi.tx.Utility.batch_all({ + calls: txs.map((tx) => tx.decodedCall), + }) +} + +export const getExpectedBlockTime = async (): Promise => { + const expectedBlockTime = await dotApi.constants.Babe.ExpectedBlockTime() + if (expectedBlockTime) { + return bnMin(ONE_DAY, expectedBlockTime) + } + + const thresholdCheck = + (await dotApi.constants.Timestamp.MinimumPeriod()) > THRESHOLD + + if (thresholdCheck) { + return bnMin( + ONE_DAY, + (await dotApi.constants.Timestamp.MinimumPeriod()) * 2n, + ) + } + + return bnMin(ONE_DAY, DEFAULT_TIME) +} diff --git a/src/lib/locks.tsx b/src/lib/locks.tsx new file mode 100644 index 0000000..4ffb57a --- /dev/null +++ b/src/lib/locks.tsx @@ -0,0 +1,108 @@ +import { SS58String } from 'polkadot-api' +import { dotApi } from '../clients' +import { DEFAULT_TIME, lockPeriod, ONE_DAY, THRESHOLD } from './constants' +import { bnMin } from './bnMin' + +export interface Locks { + [k: string]: { + type: 'Casting' | 'Delegating' + lock: { + blockNumber: number + amount: bigint + } + } +} + +const convictionList = Object.keys(lockPeriod) + +export const getExpectedBlockTime = async (): Promise => { + const expectedBlockTime = await dotApi.constants.Babe.ExpectedBlockTime() + if (expectedBlockTime) { + return bnMin(ONE_DAY, expectedBlockTime) + } + + const thresholdCheck = + (await dotApi.constants.Timestamp.MinimumPeriod()) > THRESHOLD + + if (thresholdCheck) { + return bnMin( + ONE_DAY, + (await dotApi.constants.Timestamp.MinimumPeriod()) * 2n, + ) + } + + return bnMin(ONE_DAY, DEFAULT_TIME) +} + +export const getLockTimes = async () => { + const voteLockingPeriod = + await dotApi.constants.ConvictionVoting.VoteLockingPeriod() + + const expectedBlockTime = await getExpectedBlockTime() + + const requests = convictionList.map((conviction) => { + const relativetime = + expectedBlockTime * + BigInt(voteLockingPeriod) * + BigInt(lockPeriod[conviction]) + + return [conviction, relativetime] as const + }) + + return requests.reduce( + (acc, [conviction, lockPeriod]) => { + acc[conviction] = lockPeriod + + return acc + }, + {} as Record, + ) +} + +export const getLocksInfo = async (address: SS58String) => { + const convictionVoting = + await dotApi.query.ConvictionVoting.VotingFor.getEntries(address) + + const allDelegationLocks = Object.fromEntries( + convictionVoting + .filter(({ value: convictionVoting }) => !!convictionVoting.value.prior) + .map(({ keyArgs: [, votingTrack], value: { type, value } }) => [ + votingTrack, + { + type, + lock: { + blockNumber: value.prior[0], + amount: value.prior[1], + }, + }, + ]), + ) + + return allDelegationLocks +} + +// const getTotalLocks = () => { +// if (votingService.isCasting(voting)) { +// const maxVote = Object.values(voting.votes).reduce((acc, vote) => { +// if (votingService.isStandardVote(vote)) { +// acc = bnMax(vote.balance, acc) +// } +// if (votingService.isSplitVote(vote)) { +// acc = bnMax(vote.aye.add(vote.nay), acc) +// } +// if (votingService.isSplitAbstainVote(vote)) { +// acc = bnMax(vote.aye.add(vote.nay).add(vote.abstain), acc) +// } + +// return acc +// }, BN_ZERO) + +// return bnMax(maxVote, voting.prior.amount) +// } + +// if (votingService.isDelegating(voting)) { +// return bnMax(voting.balance, voting.prior.amount) +// } + +// return BN_ZERO +// } diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 009b690..f41088f 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,3 +1,4 @@ +import { LocksCard } from '@/components/LocksCard' import { Button } from '@/components/ui/button' const openInNewTab = (url: string | URL | undefined) => { @@ -7,6 +8,7 @@ const openInNewTab = (url: string | URL | undefined) => { export const Home = () => { return (
+

Title