From 95cea54f44b5ed9be3fbd78f1a32917057284abe Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Wed, 21 Aug 2024 03:13:53 +0200 Subject: [PATCH] Complex extrinsic (#25) --- src/Content.tsx | 5 ++ src/components/DelegateeCard.tsx | 86 +++++++++++++++++++++++ src/components/LocksCard.tsx | 27 +------ src/contexts/AccountsContext.tsx | 28 +++++--- src/contexts/DelegateesContext.tsx | 16 +++-- src/lib/convertMiliseconds.ts | 20 ++++++ src/lib/currentVotesAndDelegations.tsx | 41 ++++------- src/pages/Delegate/index.tsx | 97 ++++++++++++++++++++++++++ src/pages/Home/index.tsx | 74 +------------------- 9 files changed, 257 insertions(+), 137 deletions(-) create mode 100644 src/components/DelegateeCard.tsx create mode 100644 src/lib/convertMiliseconds.ts create mode 100644 src/pages/Delegate/index.tsx diff --git a/src/Content.tsx b/src/Content.tsx index c07591c..817a8c8 100644 --- a/src/Content.tsx +++ b/src/Content.tsx @@ -1,6 +1,7 @@ import { Route, Routes } from 'react-router-dom' import { Home } from '@/pages/Home' +import { Delegate } from '@/pages/Delegate' import { toast } from 'sonner' import { useEffect } from 'react' import { useNetwork } from './contexts/NetworkContext' @@ -13,6 +14,10 @@ const pages = [ path: '/home', element: , }, + { + path: '/delegate/:address', + element: , + }, ] export const Content = () => { diff --git a/src/components/DelegateeCard.tsx b/src/components/DelegateeCard.tsx new file mode 100644 index 0000000..9a670a9 --- /dev/null +++ b/src/components/DelegateeCard.tsx @@ -0,0 +1,86 @@ +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { Check, Copy, Ellipsis } from 'lucide-react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTrigger, +} from '@/components/ui/dialog' +import { useNavigate } from 'react-router-dom' +import { ellipsisFn } from '@polkadot-ui/utils' + +import copy from 'copy-to-clipboard' +import { useEffect, useState } from 'react' +import { Delegatee } from '@/contexts/DelegateesContext' + +interface Props { + delegatee: Delegatee +} +export const DelegateeCard = ({ delegatee: d }: Props) => { + const [copied, setCopied] = useState(false) + const navigate = useNavigate() + + useEffect(() => { + if (copied) { + setTimeout(() => setCopied(false), 1000) + } + }, [copied]) + + const onDelegate = () => { + navigate(`/delegate/${d.address}`) + } + return ( + + + + + + + {d.name} + {d.shortDescription} + + Delegate + + + + + console.log('read more')} + > + + + + + + {d.name} + + + {ellipsisFn(d.address)} + + {copied ? ( + + ) : ( + { + setCopied(true) + copy(d.address) + }} + /> + )} + + + + {d.longDescription} + + + + + + + ) +} diff --git a/src/components/LocksCard.tsx b/src/components/LocksCard.tsx index 96e4805..53098ac 100644 --- a/src/components/LocksCard.tsx +++ b/src/components/LocksCard.tsx @@ -1,30 +1,9 @@ import { useLocks } from '@/contexts/LocksContext' import { useNetwork } from '@/contexts/NetworkContext' -import { getExpectedBlockTime } from '@/lib/currentVotesAndDelegations' +import { convertMiliseconds } from '@/lib/convertMiliseconds' +import { getExpectedBlockTime } from '@/lib/locks' import { Card } from '@polkadot-ui/react' import { useEffect, useState } from 'react' -import moment from 'moment' - -function convertMiliseconds(miliseconds: number) { - let days = 0 - let hours = 0 - let minutes = 0 - let seconds = 0 - let total_hours = 0 - let total_minutes = 0 - let total_seconds = 0 - - total_seconds = Math.floor(miliseconds / 1000) - total_minutes = Math.floor(total_seconds / 60) - total_hours = Math.floor(total_minutes / 60) - days = Math.floor(total_hours / 24) - - seconds = total_seconds % 60 - minutes = total_minutes % 60 - hours = total_hours % 24 - - return { d: days, h: hours, m: minutes, s: seconds } -} export const LocksCard = () => { const { currentLocks } = useLocks() @@ -52,7 +31,7 @@ export const LocksCard = () => { .catch(console.error) }, [api]) - if (!currentLocks) return null + if (!currentLocks || !Object.entries(currentLocks).length) return null return ( <> diff --git a/src/contexts/AccountsContext.tsx b/src/contexts/AccountsContext.tsx index fb40945..33bc1b2 100644 --- a/src/contexts/AccountsContext.tsx +++ b/src/contexts/AccountsContext.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useEffect, } from 'react' -import { InjectedAccount } from 'polkadot-api/pjs-signer' +import { InjectedPolkadotAccount } from 'polkadot-api/pjs-signer' import { useAccounts as useRedotAccounts } from '@reactive-dot/react' const LOCALSTORAGE_SELECTED_ACCOUNT_KEY = 'delegit.selectedAccount' @@ -16,22 +16,30 @@ type AccountContextProps = { } export interface IAccountContext { - selectedAccount?: InjectedAccount - accounts: InjectedAccount[] - selectAccount: (account: InjectedAccount | undefined) => void + selectedAccount?: InjectedPolkadotAccount + accounts: InjectedPolkadotAccount[] + selectAccount: (account: InjectedPolkadotAccount | undefined) => void } const AccountContext = createContext(undefined) const AccountContextProvider = ({ children }: AccountContextProps) => { const accounts = useRedotAccounts() - const [selectedAccount, setSelected] = useState() + const [selectedAccount, setSelected] = useState< + InjectedPolkadotAccount | undefined + >() - const selectAccount = useCallback((account: InjectedAccount | undefined) => { - account?.address && - localStorage.setItem(LOCALSTORAGE_SELECTED_ACCOUNT_KEY, account?.address) - setSelected(account) - }, []) + const selectAccount = useCallback( + (account: InjectedPolkadotAccount | undefined) => { + account?.address && + localStorage.setItem( + LOCALSTORAGE_SELECTED_ACCOUNT_KEY, + account?.address, + ) + setSelected(account) + }, + [], + ) useEffect(() => { const previousAccountAddress = localStorage.getItem( diff --git a/src/contexts/DelegateesContext.tsx b/src/contexts/DelegateesContext.tsx index dff2b98..2dc66a4 100644 --- a/src/contexts/DelegateesContext.tsx +++ b/src/contexts/DelegateesContext.tsx @@ -13,7 +13,7 @@ type DelegateesContextProps = { children: React.ReactNode | React.ReactNode[] } -type DelegateeProps = { +export type Delegatee = { address: string name: string image: string @@ -23,7 +23,8 @@ type DelegateeProps = { } export interface IDelegateesContext { - delegetees: DelegateeProps[] + delegetees: Delegatee[] + getDelegateeByAddress: (address?: string) => Delegatee | undefined } const DelegateesContext = createContext( @@ -32,16 +33,21 @@ const DelegateesContext = createContext( const DelegateeContextProvider = ({ children }: DelegateesContextProps) => { const { network } = useNetwork() - const [delegetees, setDelegatees] = useState([]) + const [delegetees, setDelegatees] = useState([]) useEffect(() => { setDelegatees( (network === 'polkadot' ? polkadotList - : kusamaList) as unknown as DelegateeProps[], + : kusamaList) as unknown as Delegatee[], ) }, [network]) + const getDelegateeByAddress = (address?: string) => { + if (!address) return undefined + + return delegetees.find((d) => d.address === address) + } // Votes thingy - pause for now // useEffect(() => { // const a = async (delegetees: any[]) => { @@ -56,7 +62,7 @@ const DelegateeContextProvider = ({ children }: DelegateesContextProps) => { // }, [delegetees]) return ( - + {children} ) diff --git a/src/lib/convertMiliseconds.ts b/src/lib/convertMiliseconds.ts new file mode 100644 index 0000000..e3ce5c9 --- /dev/null +++ b/src/lib/convertMiliseconds.ts @@ -0,0 +1,20 @@ +export function convertMiliseconds(miliseconds: number) { + let days = 0 + let hours = 0 + let minutes = 0 + let seconds = 0 + let total_hours = 0 + let total_minutes = 0 + let total_seconds = 0 + + total_seconds = Math.floor(miliseconds / 1000) + total_minutes = Math.floor(total_seconds / 60) + total_hours = Math.floor(total_minutes / 60) + days = Math.floor(total_hours / 24) + + seconds = total_seconds % 60 + minutes = total_minutes % 60 + hours = total_hours % 24 + + return { d: days, h: hours, m: minutes, s: seconds } +} diff --git a/src/lib/currentVotesAndDelegations.tsx b/src/lib/currentVotesAndDelegations.tsx index 8165975..426c5cd 100644 --- a/src/lib/currentVotesAndDelegations.tsx +++ b/src/lib/currentVotesAndDelegations.tsx @@ -1,7 +1,5 @@ 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 ( @@ -65,14 +63,21 @@ export const getVotingTrackInfo = async ( ) } -export const getDelegateTx = async ( - from: SS58String, - target: SS58String, - conviction: VotingConviction, - amount: bigint, - tracks: Array, - api: ApiType, -) => { +export const getDelegateTx = async ({ + from, + target, + conviction, + amount, + tracks, + api, +}: { + from: SS58String + target: SS58String + conviction: VotingConviction + amount: bigint + tracks: Array + api: ApiType +}) => { const tracksInfo = await getVotingTrackInfo(from, api) const txs: Array< @@ -124,19 +129,3 @@ export const getDelegateTx = async ( calls: txs.map((tx) => tx.decodedCall), }) } - -export const getExpectedBlockTime = async (api: ApiType): Promise => { - 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) -} diff --git a/src/pages/Delegate/index.tsx b/src/pages/Delegate/index.tsx new file mode 100644 index 0000000..d75ecea --- /dev/null +++ b/src/pages/Delegate/index.tsx @@ -0,0 +1,97 @@ +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { useDelegatees } from '@/contexts/DelegateesContext' +import { useNetwork } from '@/contexts/NetworkContext' +import { getLockTimes } from '@/lib/locks' +import { VotingConviction } from '@polkadot-api/descriptors' +import { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { convertMiliseconds } from '@/lib/convertMiliseconds' +import { Button } from '@/components/ui/button' +import { getDelegateTx } from '@/lib/currentVotesAndDelegations' +import { useAccounts } from '@/contexts/AccountsContext' + +export const Delegate = () => { + const { address } = useParams() + const { getDelegateeByAddress } = useDelegatees() + const [delegatee, setDelegatee] = useState(getDelegateeByAddress(address)) + const [amount, setAmount] = useState(0) + const [conviction, setConviction] = useState( + VotingConviction.None, + ) + const [convictionList, setConvictionList] = useState>( + {}, + ) + const { api } = useNetwork() + const { selectedAccount } = useAccounts() + + console.log('conviction', conviction) + useEffect(() => { + if (!api) return + getLockTimes(api).then(setConvictionList).catch(console.error) + }, [api]) + + useEffect(() => { + if (delegatee) return + + setDelegatee(getDelegateeByAddress(address)) + }, [address, delegatee, getDelegateeByAddress]) + + if (!delegatee || !api) return No delegatee found + + const onChangeAmount = (e: React.ChangeEvent) => { + setAmount(Number(e.target.value)) + } + + if (!api || !selectedAccount) return No account found + + const onSign = async () => { + const allTracks = await api.constants.Referenda.Tracks() + .then((tracks) => { + return tracks.map(([track]) => track) + }) + .catch(console.error) + + const tx = getDelegateTx({ + from: selectedAccount?.address, + target: delegatee.address, + conviction: conviction, + amount: BigInt(amount), + tracks: allTracks || [], + api, + }) + + ;(await tx) + .signSubmitAndWatch(selectedAccount.polkadotSigner) + .forEach((value) => console.log('value', value)) + } + + return ( + + + Delegate to {delegatee.name} + + + Amount + + + Conviction + + setConviction( + VotingConviction[e.target.value as keyof typeof VotingConviction], + ) + } + > + {Object.entries(convictionList).map(([label, timeLock]) => { + return ( + + {label} - {convertMiliseconds(Number(timeLock)).d} days + + ) + })} + + Delegate + + ) +} diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index fd626c6..e4693f4 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,32 +1,10 @@ import { LocksCard } from '@/components/LocksCard' -import { Button } from '@/components/ui/button' -import { Card } from '@/components/ui/card' import { useDelegatees } from '@/contexts/DelegateesContext' -import { Check, Copy, Ellipsis } from 'lucide-react' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTrigger, -} from '@/components/ui/dialog' - -import { ellipsisFn } from '@polkadot-ui/utils' - -import copy from 'copy-to-clipboard' -import { useEffect, useState } from 'react' +import { DelegateeCard } from '@/components/DelegateeCard' export const Home = () => { const { delegetees } = useDelegatees() - const [copied, setCopied] = useState(false) - - useEffect(() => { - if (copied) { - setTimeout(() => setCopied(false), 1000) - } - }, [copied]) - return ( @@ -34,55 +12,7 @@ export const Home = () => { Delegetees - {delegetees?.map((d) => ( - - - - - - - {d.name} - {d.shortDescription} - - - - console.log('read more')} - > - - - - - - {d.name} - - - {ellipsisFn(d.address)} - - {copied ? ( - - ) : ( - { - setCopied(true) - copy(d.address) - }} - /> - )} - - - - {d.longDescription} - - - - - - - ))} + {delegetees?.map((d) => )} )