Skip to content

Commit

Permalink
Merge pull request #48 from TalismanSociety/feat/voting-updates
Browse files Browse the repository at this point in the history
[FEAT] - SplitAbstain vote & Remove Vote
  • Loading branch information
UrbanWill authored May 23, 2024
2 parents 23bf020 + 88ad428 commit 6a1102d
Show file tree
Hide file tree
Showing 17 changed files with 823 additions and 190 deletions.
3 changes: 3 additions & 0 deletions apps/multisig/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"@talismn/siws": "0.0.8",
"@talismn/ui": "workspace:^",
"@talismn/util": "^0.2.0",
"@tanstack/react-query": "^5.37.1",
"@tanstack/react-query-devtools": "^5.37.1",
"@tanstack/react-table": "^8.16.0",
"@uiw/codemirror-themes": "^4.21.13",
"@uiw/react-codemirror": "^4.21.13",
Expand Down Expand Up @@ -146,6 +148,7 @@
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "^0.0.13",
"@talismn/development": "workspace:^",
"@tanstack/eslint-plugin-query": "^5.35.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
Expand Down
85 changes: 47 additions & 38 deletions apps/multisig/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,43 +32,52 @@ import { SkeletonLayout } from './layouts/SkeletonLayout'
import { Helmet } from 'react-helmet'
import { CONFIG } from '@lib/config'

const App: React.FC = () => (
<ThemeProvider>
<RecoilRoot>
<BalancesProvider
withTestnets
enabledChains={supportedChains.map(chain => chain.genesisHash)}
coingeckoApiUrl="https://coingecko.talismn.workers.dev"
>
<HasuraProvider>
<AzeroIDResolverProvider>
<Suspense fallback={<SkeletonLayout />}>
<WalletConnectProvider>
<Helmet>
<title>{CONFIG.IS_POLKADOT_MULTISIG ? 'Polkadot Multisig by Signet' : 'Signet'}</title>
</Helmet>
<Analytics />
{/* <PendingTransactionsWatcher /> */}
<BalancesWatcher />
<ExtensionWatcher />
<AccountWatcher />
<OffchainDataWatcher />
<NomPoolsWatcher />
<ValidatorsWatcher />
<ActiveMultisigWatcher />
<ConstsWatcher />
<RouterProvider router={router} />
<Toaster position="top-right" containerStyle={{ top: '6.4rem' }}>
{t => <ToastBar toast={t} />}
</Toaster>
<NewToaster />
</WalletConnectProvider>
</Suspense>
</AzeroIDResolverProvider>
</HasuraProvider>
</BalancesProvider>
</RecoilRoot>
</ThemeProvider>
)
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const App: React.FC = () => {
const queryClient = new QueryClient()
return (
<ThemeProvider>
<RecoilRoot>
<BalancesProvider
withTestnets
enabledChains={supportedChains.map(chain => chain.genesisHash)}
coingeckoApiUrl="https://coingecko.talismn.workers.dev"
>
<HasuraProvider>
<AzeroIDResolverProvider>
<Suspense fallback={<SkeletonLayout />}>
<WalletConnectProvider>
<Helmet>
<title>{CONFIG.IS_POLKADOT_MULTISIG ? 'Polkadot Multisig by Signet' : 'Signet'}</title>
</Helmet>
<Analytics />
{/* <PendingTransactionsWatcher /> */}
<BalancesWatcher />
<ExtensionWatcher />
<AccountWatcher />
<OffchainDataWatcher />
<NomPoolsWatcher />
<ValidatorsWatcher />
<ActiveMultisigWatcher />
<ConstsWatcher />
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
<Toaster position="top-right" containerStyle={{ top: '6.4rem' }}>
{t => <ToastBar toast={t} />}
</Toaster>
<NewToaster />
</WalletConnectProvider>
</Suspense>
</AzeroIDResolverProvider>
</HasuraProvider>
</BalancesProvider>
</RecoilRoot>
</ThemeProvider>
)
}

export default App
4 changes: 3 additions & 1 deletion apps/multisig/src/components/AmountFlexibleInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ type Props = {
setAmount: (a: string) => void
setSelectedToken?: (t: BaseToken) => void
amountPerBlockBn?: BN
placeholder?: string
}

export const AmountFlexibleInput: React.FC<Props> = ({
amountPerBlockBn,
tokens,
selectedToken,
leadingLabel,
placeholder,
setAmount,
setSelectedToken,
}) => {
Expand Down Expand Up @@ -79,7 +81,7 @@ export const AmountFlexibleInput: React.FC<Props> = ({
<div className="flex w-full gap-[12px]">
<div className="flex flex-1 items-center">
<Input
placeholder={`0 ${unit}`}
placeholder={placeholder || `0 ${unit}`}
label={leadingLabel ?? `Amount`}
labelTrailing={
amountPerBlockBn ? (
Expand Down
31 changes: 28 additions & 3 deletions apps/multisig/src/domains/multisig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,13 +568,23 @@ export const extrinsicToDecoded = (
// Check if it's a Vote type
for (const arg of args) {
const obj: any = arg.toHuman()
if (obj?.section === 'convictionVoting' && obj?.method === 'vote') {
const { poll_index, vote } = obj.args
if (obj?.section === 'convictionVoting') {
const { poll_index, vote, index } = obj.args
let voteDetails: VoteDetails | undefined

if (vote.Standard) {
if (obj?.method === 'removeVote') {
voteDetails = {
referendumId: index,
method: obj.method,
details: {},
}
}

if (vote?.Standard) {
voteDetails = {
referendumId: poll_index,
method: obj.method,
convictionVote: 'Standard',
details: {
Standard: {
balance: new BN(vote.Standard.balance.replaceAll(',', '')),
Expand All @@ -587,6 +597,21 @@ export const extrinsicToDecoded = (
}
}

if (vote?.SplitAbstain) {
voteDetails = {
referendumId: poll_index,
method: obj.method,
convictionVote: 'SplitAbstain',
details: {
SplitAbstain: {
aye: new BN(vote.SplitAbstain.aye.replaceAll(',', '')),
nay: new BN(vote.SplitAbstain.nay.replaceAll(',', '')),
abstain: new BN(vote.SplitAbstain.abstain.replaceAll(',', '')),
},
},
}
}

if (voteDetails) {
const token = chainTokens.find(t => t.type === 'substrate-native')
if (!token) throw Error(`Chain does not have a native token!`)
Expand Down
41 changes: 36 additions & 5 deletions apps/multisig/src/domains/referenda/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,28 @@ export type SplitAbstainVoteParams = {
abstain: BN
} & SplitVoteParams

export type ConvictionVote = 'Standard' | 'SplitAbstain' | 'Split'
export type VoteMethod = 'vote' | 'removeVote'

export type VoteDetails = {
referendumId?: number
convictionVote?: ConvictionVote
method: VoteMethod
details: {
Standard?: StandardVoteParams
Split?: SplitVoteParams
SplitAbstain?: SplitAbstainVoteParams
}
}

export type VoteDetailsForm = Omit<VoteDetails, 'details'> & {
details: {
Standard: StandardVoteParams
Split: SplitVoteParams
SplitAbstain: SplitAbstainVoteParams
}
}

type ReferendumBasicInfo = {
index: number
isOngoing: boolean
Expand All @@ -56,11 +69,18 @@ export const defaultVoteDetails: Required<VoteDetails['details']> = {
},
}

export const defaultVote: VoteDetailsForm = {
convictionVote: 'Standard',
method: 'vote',
details: defaultVoteDetails,
}

export const isVoteFeatureSupported = (api: ApiPromise) =>
!!api.query.referenda?.referendumInfoFor && !!api.tx.convictionVoting?.vote

export const useReferenda = (chain: Chain) => {
const [referendums, setReferendums] = useState<ReferendumBasicInfo[] | undefined>()
const [isLoading, setIsLoading] = useState<boolean>(true)
const apiLoadable = useRecoilValueLoadable(pjsApiSelector(chain.genesisHash))

const isPalletSupported = useMemo(() => {
Expand All @@ -69,13 +89,17 @@ export const useReferenda = (chain: Chain) => {
}, [apiLoadable])

const getReferendums = useCallback(async () => {
setIsLoading(true)
if (apiLoadable.state !== 'hasValue' || isPalletSupported === undefined) return

if (!isPalletSupported) {
console.error(`referenda or conviction_voting pallets not supported on this chain ${chain.chainName}`)
// treat it as 0 referendum created if required pallets are not supported
setReferendums([])
} else {
setIsLoading(false)
return
}
try {
const referendumCount = await apiLoadable.contents.query.referenda.referendumCount()
const ids = Array.from(Array(referendumCount.toNumber()).keys())
const rawReferendums = await apiLoadable.contents.query.referenda.referendumInfoFor.multi(ids)
Expand All @@ -86,6 +110,10 @@ export const useReferenda = (chain: Chain) => {
isOngoing: raw.value.isOngoing,
}))
)
} catch (error) {
console.error(`Error while fetching referenda: ${error}`)
} finally {
setIsLoading(false)
}
}, [apiLoadable, chain.chainName, isPalletSupported])

Expand All @@ -98,17 +126,20 @@ export const useReferenda = (chain: Chain) => {
getReferendums()
}, [getReferendums])

return { referendums, isPalletSupported }
return { referendums, isPalletSupported, isLoading }
}

export const isVoteDetailsComplete = (voteDetails: VoteDetails) => {
export const isVoteDetailsComplete = (voteDetails: VoteDetailsForm) => {
if (voteDetails.referendumId === undefined) return false

if (voteDetails.details.Standard) {
if (voteDetails.convictionVote === 'Standard') {
const { balance } = voteDetails.details.Standard
return balance.gt(new BN(0))
} else if (voteDetails.convictionVote === 'SplitAbstain') {
const { aye, nay, abstain } = voteDetails.details.SplitAbstain
return aye.gt(new BN(0)) || nay.gt(new BN(0)) || abstain.gt(new BN(0))
}
return !!voteDetails.details.Split || !!voteDetails.details.SplitAbstain
return !!voteDetails.details.Split
}

/** Expects conviction string (e.g. Locked1x, Locked2x, ..., or None) */
Expand Down
51 changes: 51 additions & 0 deletions apps/multisig/src/hooks/queries/useGetReferendums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useQueries } from '@tanstack/react-query'
import { SupportedChainIds } from '@domains/chains/generated-chains'

const supportedChains: Partial<Record<SupportedChainIds, string>> = {
'polkadot': 'polkadot',
'kusama': 'kusama',
'acala': 'acala',
'bifrost-polkadot': 'bifrost',
'bifrost-kusama': 'bifrost-kusama',
'hydradx': 'hydradx',
'phala': 'phala',
'khala': 'khala',
'karura': 'karura',
'kintsugi': 'kintsugi',
// testnets
'rococo-testnet': 'rococo',
}
// This only a partial Referendum interface
interface Referendum {
title: string
referendumIndex: number
}

const fetchReferendums = async ({ chain, id }: { chain: string | undefined; id: string }): Promise<Referendum> => {
const data = await fetch(`https://${chain}.subsquare.io/api/gov2/referendums/${id}`).then(res => res.json())
return data
}

interface UseGetReferendums {
ids: string[]
chainId: SupportedChainIds
}

export default function useGetReferendums({ ids, chainId }: UseGetReferendums) {
const chain = supportedChains[chainId]

return useQueries({
queries: ids.map(id => ({
queryKey: [chainId, { isChainSupported: !!chain }, id],
queryFn: () => fetchReferendums({ chain, id }),
enabled: !!id && !!chain,
})),
combine: results => {
return {
data: results.map(result => result.data),
pending: results.some(result => result.isPending),
isLoading: results.some(result => result.isLoading),
}
},
})
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { css } from '@emotion/css'
import { Select } from '@talismn/ui'
import { VoteDetailsForm } from '@domains/referenda'

type Props = {
conviction: number
onChange: (conviction: number) => void
setVoteDetails: React.Dispatch<React.SetStateAction<VoteDetailsForm>>
}

const CONVICTIONS = [1, 2, 4, 8, 16, 32].map((lock, index): [value: number, duration: number] => [index + 1, lock])
Expand All @@ -20,16 +21,25 @@ export function createConvictionsOpts(): { headlineText: string; value: number }
}

// ref: https://github.com/polkadot-js/apps/blob/master/packages/react-components/src/ConvictionDropdown.tsx
const ConvictionsDropdown: React.FC<Props> = ({ conviction, onChange }) => {
const ConvictionsDropdown: React.FC<Props> = ({ conviction, setVoteDetails }) => {
const options = createConvictionsOpts()

const handleChange = (value: number) => {
setVoteDetails(prev => {
const updatedVal = { ...prev }
updatedVal.details.Standard.vote.conviction = value
return updatedVal
})
}

return (
<Select
className={css`
button {
height: 56px;
}
`}
onChange={onChange}
onChange={handleChange}
value={conviction}
>
{options.map(({ headlineText, value }) => (
Expand Down
Loading

0 comments on commit 6a1102d

Please sign in to comment.